mirror of
https://github.com/SeedSigner/seedsigner.git
synced 2026-10-05 23:18:25 +00:00
FORCE PUSH Merge branch dev into messaging toast
This commit is contained in:
@@ -27,6 +27,9 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
# Needs to also pull the seedsigner-translations repo
|
||||
submodules: recursive
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
@@ -45,16 +48,20 @@ jobs:
|
||||
--cov=seedsigner \
|
||||
--cov-append \
|
||||
--cov-branch \
|
||||
--cov-report term \
|
||||
--cov-report html \
|
||||
--cov-report html:./artifacts/cov_html \
|
||||
--cov-report xml \
|
||||
--durations 5 \
|
||||
-vv
|
||||
- name: Generate screenshots
|
||||
run: |
|
||||
python -m pytest tests/screenshot_generator/generator.py
|
||||
python -m pytest tests/screenshot_generator/generator.py \
|
||||
--color=yes \
|
||||
--cov=seedsigner \
|
||||
--cov-append \
|
||||
--cov-branch \
|
||||
--cov-report html:./artifacts/cov_html \
|
||||
-vv
|
||||
cp -r ./seedsigner-screenshots ./artifacts/
|
||||
- name: Coverage report
|
||||
run: coverage report
|
||||
- name: Archive CI Artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
|
||||
+4
-3
@@ -5,6 +5,7 @@ src/seedsigner.egg-info/
|
||||
.vscode
|
||||
src/seedsigner/models/settings_definition.json
|
||||
.idea
|
||||
*.mo
|
||||
.coverage
|
||||
seedsigner-screenshots
|
||||
.coverage*
|
||||
|
||||
*.po
|
||||
*.mo
|
||||
@@ -0,0 +1,9 @@
|
||||
[submodule "src/seedsigner/resources/seedsigner-translations"]
|
||||
path = src/seedsigner/resources/seedsigner-translations
|
||||
url = https://github.com/SeedSigner/seedsigner-translations.git
|
||||
branch = dev
|
||||
[submodule "seedsigner-screenshots"]
|
||||
path = seedsigner-screenshots
|
||||
url = https://github.com/SeedSigner/seedsigner-screenshots.git
|
||||
branch = dev
|
||||
update = none
|
||||
@@ -7,7 +7,7 @@
|
||||
* [Project Summary](#project-summary)
|
||||
* [Shopping List](#shopping-list)
|
||||
* [Software Installation](#software-installation)
|
||||
* [Verifying the Software](#verifying-the-software)
|
||||
* [Verifying your download](#verifying-your-download)
|
||||
* [Enclosure Designs](#enclosure-designs)
|
||||
* [SeedQR Printable Templates](#seedqr-printable-templates)
|
||||
* [Build from Source](#build-from-source)
|
||||
@@ -21,7 +21,7 @@
|
||||
[](https://github.com/SeedSigner/seedsigner/actions/workflows/tests.yml)
|
||||
[](https://github.com/SeedSigner/seedsigner/actions/workflows/build.yml)
|
||||
|
||||
The goal of SeedSigner is to lower the cost and complexity of Bitcoin multi-signature wallet use. To accomplish this goal, SeedSigner offers anyone the opportunity to build a verifiably air-gapped, stateless Bitcoin signing device using inexpensive, publicly available hardware components (usually < $50). SeedSigner helps users save with Bitcoin by assisting with trustless private key generation and multisignature (aka "multisig") wallet setup, and helps users transact with Bitcoin via a secure, air-gapped QR-exchange signing model.
|
||||
The goal of SeedSigner is to lower the cost and complexity of Bitcoin multisignature wallet use. To accomplish this goal, SeedSigner offers anyone the opportunity to build a verifiably air-gapped, stateless Bitcoin signing device using inexpensive, publicly available hardware components (usually < $50). SeedSigner helps users save with Bitcoin by assisting with trustless private key generation and multisignature (aka "multisig") wallet setup, and helps users transact with Bitcoin via a secure, air-gapped QR-exchange signing model.
|
||||
|
||||
Additional information about the project can be found at [SeedSigner.com](https://seedsigner.com).
|
||||
|
||||
@@ -30,40 +30,55 @@ You can follow [@SeedSigner](https://twitter.com/SeedSigner) on Twitter for the
|
||||
If you have specific questions about the project, our [Telegram Group](https://t.me/joinchat/GHNuc_nhNQjLPWsS) is a great place to ask them.
|
||||
|
||||
### Feature Highlights:
|
||||
* Calculate the final word (aka checksum) of a 12- or 24-word BIP39 seed phrase
|
||||
* Create a 24-word BIP39 seed phrase with 99 dice rolls or a 12-word with 50 rolls [(Verifying dice seed generation)](docs/dice_verification.md)
|
||||
* Create a 12- or 24-word BIP39 seed phrase via image entropy from the onboard camera
|
||||
* Temporarily stores seeds in memory while the device is powered; all memory is wiped when power is removed
|
||||
* SD card removable after boot to ensure no secret data can be written to it
|
||||
* Guided interface to manually transcribe a seed to the SeedQR format for instant seed loading [(demo video here)](https://youtu.be/c1-PqTNx1vc)
|
||||
* BIP39 passphrase (aka "word 25") support
|
||||
* Native Segwit Multisig XPUB generation
|
||||
* PSBT-compliant; scan and parse transaction data from animated QR codes
|
||||
* Sign transactions & transfer XPUB data using animated QR codes [(demo video here)](https://youtu.be/LPqvdQ2gSzs)
|
||||
* Live preview during image entropy seed generation and QR scanning UX
|
||||
* Optimized seed word entry interface
|
||||
* Support for Bitcoin Mainnet & Testnet
|
||||
* Support for custom user-defined derivation paths
|
||||
* Support for loading Electrum Segwit seed phrases with feature limitations: [Electrum support info](docs/electrum.md)
|
||||
* On-demand receive address verification
|
||||
* Address Explorer for single sig and multisig wallets
|
||||
* User-configurable QR code display density
|
||||
* Responsive, event-driven user interface
|
||||
* Stateless, air-gapped operation:
|
||||
* Temporarily stores seeds in memory while the device is powered; all memory is wiped when power is removed.
|
||||
* SD card removable after boot to ensure no secret data can be written to it.
|
||||
* No wifi or Bluetooth hardware onboard.
|
||||
* Can only receive data via reading QR codes with its camera.
|
||||
* Can only send data by displaying QR codes on its screen.
|
||||
|
||||
### Considerations:
|
||||
* Built for compatibility with Specter Desktop, Sparrow, and BlueWallet Vaults
|
||||
* Device takes up to 60 seconds to boot before menu appears (be patient!)
|
||||
* Always test your setup before transferring larger amounts of bitcoin (try Testnet first!)
|
||||
* Taproot not quite yet supported
|
||||
* Slightly rotating the screen clockwise or counter-clockwise should resolve lighting/glare issues
|
||||
* If you think SeedSigner adds value to the Bitcoin ecosystem, please help us spread the word! (tweets, pics, videos, etc.)
|
||||
* Trustless, auditable:
|
||||
* Completely FOSS code, MIT license
|
||||
* Reproducible builds
|
||||
* Created and maintained by volunteers. There is no corporation. No profit motive.
|
||||
|
||||
* Creating and handling seeds:
|
||||
* Create a seed phrase by picking BIP39 words, calculates the final word (aka checksum).
|
||||
* Create a seed phrase [via dice rolls](docs/dice_verification.md).
|
||||
* Create a seed phrase via image entropy from the onboard camera.
|
||||
* Guided interface to manually transcribe a seed to the SeedQR format for instant seed loading [(video)](https://youtu.be/c1-PqTNx1vc).
|
||||
* BIP39 passphrase (aka 13th or 25th word) support.
|
||||
* Import any existing seed phrase via an optimized seed word entry interface.
|
||||
* Partial support for Electrum Segwit seed phrases [(info)](docs/electrum.md).
|
||||
|
||||
* Wallet setup and transaction signing
|
||||
* Script types: Taproot, native segwit, nested segwit, legacy (p2pkh).
|
||||
* Single sig and multisig xpub export.
|
||||
* Support for user-defined custom derivation paths.
|
||||
* In-depth transaction (aka PSBT) review flow before signing.
|
||||
* Verify the PSBT's single sig or multisig change outputs or self-transfer outputs.
|
||||
* Mainnet, testnet, and regtest.
|
||||
|
||||
* Additional utilities
|
||||
* [SettingsQR](https://github.com/SeedSigner/seedsigner-settings-generator) to instantly reconfigure a SeedSigner for beginners, advanced users, or tailored to your preferences.
|
||||
* Scan a software wallet's receive or change address to verify that it's correct.
|
||||
* Address Explorer for single sig and multisig wallets.
|
||||
* Message signing to prove address ownership.
|
||||
* BIP85 child seed generation.
|
||||
|
||||
* Compatible with:
|
||||
* Sparrow
|
||||
* Nunchuk
|
||||
* Keeper
|
||||
* BlueWallet
|
||||
* Specter Desktop
|
||||
* Any bitcoin wallet software that supports QR codes
|
||||
|
||||
* Supported languages:
|
||||
* English
|
||||
* Español
|
||||
* Many more coming soon!
|
||||
|
||||
### Planned Upcoming Improvements / Functionality:
|
||||
* Multi-language support
|
||||
* Significantly faster boot time
|
||||
* Reproducible builds
|
||||
* Port to MicroPython to broaden the range of compatible hardware to include low-cost microcontrollers
|
||||
* Other optimizations based on user feedback!
|
||||
|
||||
---------------
|
||||
|
||||
@@ -71,14 +86,17 @@ If you have specific questions about the project, our [Telegram Group](https://t
|
||||
|
||||
To build a SeedSigner, you will need:
|
||||
|
||||
* Raspberry Pi Zero (preferably version 1.3 with no WiFi/Bluetooth capability, but any Raspberry Pi 2/3/4 or Zero model will work, Raspberry Pi 1 devices will require a hardware modification to the Waveshare LCD Hat, as per the [instructions here](./docs/legacy_hardware.md))
|
||||
* Waveshare 1.3" 240x240 pxl LCD (correct pixel count is important, more info at https://www.waveshare.com/wiki/1.3inch_LCD_HAT)
|
||||
* Pi Zero-compatible camera (tested to work with the Aokin / AuviPal 5MP 1080p with OV5647 Sensor)
|
||||
* Raspberry Pi Zero
|
||||
* Preferably version 1.3 which has no WiFi/Bluetooth capability, but any Raspberry Pi 2/3/4 or Zero "W"/"2W" model will work.
|
||||
* Waveshare 1.3" 240x240 LCD (MUST be the 240x240 version!) https://www.waveshare.com/wiki/1.3inch_LCD_HAT.
|
||||
* Pi Zero-compatible camera (tested to work with the Aokin / AuviPal 5MP 1080p with OV5647 Sensor).
|
||||
|
||||
Notes:
|
||||
* You will need to solder the 40 GPIO pins (20 pins per row) to the Raspberry Pi Zero board. If you don't want to solder, purchase "GPIO Hammer Headers" for a solderless experience.
|
||||
* Other cameras with the above sensor module should work, but may not fit in the Orange Pill enclosure
|
||||
* Choose the Waveshare screen carefully; make sure to purchase the model that has a resolution of 240x240 pixels
|
||||
* You may need to solder the 40 GPIO pins (20 pins per row) to the Raspberry Pi Zero board. If you don't want to solder, most stores offer the board "with headers" already soldered on.
|
||||
* The Pi Zero "W" or "2W" is often easier to find but has wifi/Bluetooth hardware. You can still use these boards and can optionally [disable the wifi/Bluetooth hardware](https://github.com/DesobedienteTecnologico/rpi_disable_wifi_and_bt_by_hardware).
|
||||
* Other cameras with the above sensor module should work, but may not fit in the Orange Pill enclosure.
|
||||
* Choose the Waveshare screen carefully; they make a number of different boards that look very similar but ARE NOT COMPATIBLE! Make sure you purchase the model that has a resolution of 240x240 pixels.
|
||||
* Raspberry Pi 1 is also compatible, but will require a [hardware modification to the Waveshare LCD Hat](./docs/legacy_hardware.md).
|
||||
|
||||
---------------
|
||||
|
||||
@@ -119,13 +137,13 @@ Once the files have all finished downloading, follow the steps below to verify t
|
||||
[Our previous software versions are available here](https://github.com/SeedSigner/seedsigner/releases). Choose a specific version and then expand the *Assets* sub-heading to display the .img file binary and also the 2 associated signature files. **Note:** The prior version files will have lower numbers than the scripts and examples provided in this document, but the naming format will be the same, so you can edit them as required for signature verification etc.
|
||||
|
||||
|
||||
## Verifying that the downloaded files are authentic (optional but highly recommended!)
|
||||
## Verifying your download
|
||||
|
||||
You can quickly verify that the software you just downloaded is both authentic and unaltered, by following these instructions.
|
||||
We assume you are running the commands from a computer where both [GPG](https://gnupg.org/download/index.html) and [shasum](https://command-not-found.com/shasum) are already installed, and that you also know [how to navigate on a terminal](https://terminalcheatsheet.com/guides/navigate-terminal).
|
||||
You can quickly verify that the software you just downloaded is both authentic and unaltered by following these instructions.
|
||||
We assume you are running the commands from a computer where both [GPG](https://gnupg.org/download/index.html) and [shasum](https://command-not-found.com/shasum) are already installed and that you also know [how to navigate on a terminal](https://terminalcheatsheet.com/guides/navigate-terminal).
|
||||
|
||||
> You must run the following verification before opening or mounting the .img file.
|
||||
> Some operating systems modify the file on mount causing verification to fail.
|
||||
> Some operating systems modify the file on mount, causing verification to fail.
|
||||
|
||||
### Step 1. Verify that the signature (.sig) file is genuine:
|
||||
|
||||
@@ -141,7 +159,7 @@ The result should confirm that 1 key was *either* imported or updated. *Ignore*
|
||||
|
||||
Next, you will run the *verify* command on the signature (.sig) file. (*Verify* must be run from inside the same folder that you downloaded the files into earlier. The `*`'s in this command will auto-fill the version from your current folder, so it should be copied and pasted as-is.)
|
||||
```
|
||||
gpg --verify seedsigner.0.7.*.sha256.txt.sig
|
||||
gpg --verify seedsigner.0.8.*.sha256.txt.sig
|
||||
```
|
||||
|
||||
When the verify command completes successfully, it should display output like this:
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 48 KiB |
@@ -122,7 +122,7 @@ SeedSigner requires `zbar` at 0.23.x or higher.
|
||||
|
||||
Download the binary:
|
||||
```bash
|
||||
curl -L http://raspbian.raspberrypi.org/raspbian/pool/main/z/zbar/libzbar0_0.23.90-1_armhf.deb --output libzbar0_0.23.90-1_armhf.deb
|
||||
curl -L http://raspbian.raspberrypi.org/raspbian/pool/main/z/zbar/libzbar0_0.23.90-1+deb11u1_armhf.deb --output libzbar0_0.23.90-1_armhf.deb
|
||||
```
|
||||
|
||||
And then install it:
|
||||
@@ -211,7 +211,9 @@ Description=Seedsigner
|
||||
[Service]
|
||||
User=pi
|
||||
WorkingDirectory=/home/pi/seedsigner/src/
|
||||
ExecStart=/usr/bin/python3 main.py > /dev/null 2>&1
|
||||
ExecStart=/usr/bin/python3 main.py
|
||||
StandardOutput=null
|
||||
ErrorOutput=null
|
||||
Restart=always
|
||||
|
||||
[Install]
|
||||
@@ -220,7 +222,7 @@ WantedBy=multi-user.target
|
||||
|
||||
_Note: For local dev you'll want to edit the `Restart=always` line to `Restart=no`. This way when your dev code crashes it won't keep trying to restart itself. Note that the UI "Reset" will no longer work when auto-restarts are disabled._
|
||||
|
||||
_Note: Debugging output is completely wiped via routing the output to `/dev/null 2>&1`. When working in local dev, you'll `kill` the `systemd` SeedSigner service and just directly run the code on demand so you can see all the debugging output live._
|
||||
_Note: Debugging output is completely wiped via routing the stdout and stderr to `/dev/null`. When working in local dev, you'll `kill` the `systemd` SeedSigner service and just directly run the code on demand so you can see all the debugging output live._
|
||||
|
||||
Use `CTRL-X` and `y` to exit and save changes.
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,19 @@
|
||||
## The Orange Pill Mini Enclosure
|
||||
|
||||
<img src="/docs/img/OrangePillMini_Thumb.jpg" width="800" height="600">
|
||||
|
||||
- The Orange Pill Mini is a smaller, hommage to the original Orange Pill enclosure.
|
||||
- Hardware free build, no screws or spacers needed.
|
||||
- This enclosure requires use of the so called "zerocam", which allows for a more compact form factor.
|
||||
- The enclosure offers a very comfortable handling experience.
|
||||
- Top and bottom part snap fit together.
|
||||
- SD-card removable
|
||||
- Data port is closed.
|
||||
|
||||
|
||||
### Printing tips
|
||||
- OrangePillMini_Bottom.3mf is ready for multicolor printing but can also be printed in a single color.
|
||||
- Files are preoriented for optimal printing, no supports required.
|
||||
- Controls - buttons, thumbstick and trackball work best if printed from TPU.
|
||||
- The included controls can be printed from PLA, but TPU is recommended.
|
||||
- Recommended shore hardness is 95A, but any TPU will do.
|
||||
Binary file not shown.
Binary file not shown.
+316
@@ -0,0 +1,316 @@
|
||||
# Localization (l10n) Developer Notes
|
||||
|
||||
## High-level overview
|
||||
1. Python code indicates text that needs to be translated.
|
||||
1. Those marked strings are extracted into a master `messages.pot` file.
|
||||
1. That file is uploaded [Transifex](https://app.transifex.com/seedsigner/seedsigner).
|
||||
1. Translators work within Transifex on their respective languages.
|
||||
1. Completed translations are downloaded as `messages.po` files for each language.
|
||||
1. Python "compiles" them into `messages.mo` files ready for use.
|
||||
1. The `*.po` and `*.mo` files are written to the [seedsigner-translations](https://github.com/SeedSigner/seedsigner-translations) repo.
|
||||
1. That repo is linked as a submodule here as `seedsigner.resources.seedsigner-translations`.
|
||||
1. Python code retrieves a translation on demand.
|
||||
|
||||
|
||||
## "Wrapping" text for translation
|
||||
Any text that we want to be presented in multiple languages needs to "wrapped".
|
||||
|
||||
The CORE CONCEPT to understand is that wrapping is used in TWO different contexts:
|
||||
1. Pre-translation: This is how we identify text that translators need to translate. Any wrapped string literals will appear in translators' Transifex UI.
|
||||
2. Post-translation: Return the locale-specific translation for that source string (defaults to the English string if no translation is found).
|
||||
|
||||
We have three techniques to wrap code, depending on which of the above contexts we're in and where we are in the code:
|
||||
|
||||
|
||||
#### Technique 1: `ButtonOption`
|
||||
Most `View` classes will render themselves via some variation of the `ButtonListScreen` which takes a `button_data` list as an input. Each entry in
|
||||
`button_data` must be a `ButtonOption`. The first argument for `ButtonOption` is the `button_label` string. This is the English string literal that
|
||||
is displayed in that button. If you look at `setup.cfg` you'll see that `ButtonOption` is listed as a keyword in `extract_messages`. That means
|
||||
that the first argument for `ButtonOption` -- its `button_label` string -- will be marked for translation (by default the `extract_messages`
|
||||
integration will only look at the first argument of any method listed in `keywords`).
|
||||
|
||||
```python
|
||||
class SomeView(View):
|
||||
# These string literals will be marked for translation
|
||||
OPTION_1 = ButtonOption("Option 1!")
|
||||
OPTION_2 = ButtonOption("Option 2!")
|
||||
|
||||
def run(self):
|
||||
button_data = [self.OPTION_1, self.OPTION_2]
|
||||
|
||||
# No way for `extract_messages` to know what's in `some_var`; won't be marked for
|
||||
# translation unless it's specified elsewhere.
|
||||
some_var = some_value
|
||||
button_data.append(ButtonOption(some_var))
|
||||
```
|
||||
|
||||
These `ButtonOption` values are generally specified in class-level attributes, as in the above example. Classes in python are imported once, after
|
||||
which class-level attributes are never reinterpreted again; **the value at import time for a class-level attribute is its value for the duration of
|
||||
the program execution.**
|
||||
|
||||
This means that we must assume that `ButtonOption.button_label` strings are ALWAYS the original English string. This is crucial because the English
|
||||
values are the lookup keys for the translations:
|
||||
|
||||
* `ButtonOption.button_label` = "Hello!" in the python code.
|
||||
* Run the code, the class that contains our `ButtonOption` as a class-level attribute is imported.
|
||||
* Regardless of language selection, that `ButtonOption` will always return "Hello!".
|
||||
* `Screen` then uses "Hello!" as a key to find the translation "¡Hola!".
|
||||
* User sees "¡Hola!".
|
||||
|
||||
IF `ButtonOption` were wired to return the translated string, we'd have a problem:
|
||||
* User sets their language to Spanish and enables persistent settings.
|
||||
* Launch SeedSigner. At import time the `button_label`'s value is translated to "¡Hola!".
|
||||
* User sees "¡Hola!" in the UI. All good.
|
||||
* User changes language to English (or any other language).
|
||||
* Now the `Screen` must find the matching string in a different translation file.
|
||||
* But the `button_label` value was fixed at import time; it's still providing "¡Hola!" as the lookup key.
|
||||
* Since all the translation files map English -> translation, no such "¡Hola!" match exists in any translation file.
|
||||
* So the translation falls back to just displaying the unmatched key: "¡Hola!"
|
||||
|
||||
tldr: `ButtonOption` marks its `button_label` English string literal for translation, but NEVER provides a translated value.
|
||||
|
||||
---
|
||||
|
||||
#### Technique 2: `seedsigner.helpers.l10n.mark_for_translation`
|
||||
You'll see that `mark_for_translation` is imported as `_mft` for short.
|
||||
|
||||
As far as translations are concerned, `_mft` serves the same purpose as `ButtonOption`. The only difference is that `_mft` is for all other
|
||||
(non-`button_data`) class-level attributes.
|
||||
|
||||
```python
|
||||
from seedsigner.helpers.l10n import mark_for_translation as _mft
|
||||
|
||||
@classmethod
|
||||
class SomeView(View):
|
||||
title: str = _mft("Default Title")
|
||||
text: str = _mft("My default body text")
|
||||
|
||||
def run(self):
|
||||
self.run_screen(
|
||||
SomeScreen,
|
||||
title=self.title,
|
||||
text=self.text
|
||||
)
|
||||
```
|
||||
|
||||
In general we try to avoid using `_mft` at all, but some class-level attributes just can't be avoided.
|
||||
|
||||
---
|
||||
|
||||
#### Technique 3: `gettext`, aka `_()`
|
||||
This is the way you'll see text wrapping handled in the vast majority of tutorials.
|
||||
|
||||
```python
|
||||
from gettext import gettext as _
|
||||
|
||||
my_text = _("Hello!")
|
||||
|
||||
# Specify Spanish
|
||||
os.environ['LANGUAGE'] = "es"
|
||||
print(my_text)
|
||||
>> ¡Hola!
|
||||
|
||||
# Specify English
|
||||
os.environ['LANGUAGE'] = "en"
|
||||
print(my_text)
|
||||
>> Hello!
|
||||
```
|
||||
|
||||
This approach marks string literals for translation AND retrieves the translated text.
|
||||
|
||||
We do the same in SeedSigner code, but only when the string literal is in a part of the code that is dynamically evaluated:
|
||||
|
||||
```python
|
||||
from gettext import gettext as _
|
||||
|
||||
class SomeView(View):
|
||||
def __init__(self):
|
||||
# Mark string literal for translation AND dynamically retrieve its translated value
|
||||
self.some_var = _("I will be dynamically fetched")
|
||||
```
|
||||
|
||||
Though note that there are times when we use `_()` only for the retrieval side:
|
||||
|
||||
```python
|
||||
from seedsigner.helpers.l10n import mark_for_translation as _mft
|
||||
|
||||
class SomeView(View):
|
||||
message = _mft("Hello!") # mark for translation, but always return "Hello!"
|
||||
|
||||
def run(self):
|
||||
self.run_screen(
|
||||
SomeScreen,
|
||||
message=self.title
|
||||
)
|
||||
|
||||
# elsewhere...
|
||||
@dataclass
|
||||
class SomeScreen(Screen):
|
||||
message: str = None
|
||||
|
||||
def __post_init__(self):
|
||||
message_display = TextArea(
|
||||
text=_(self.message) # The _() wrapping here now retrieves the translated value, if one is available
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Basic rules
|
||||
* English string literals in class-level attributes should be wrapped with either `ButtonOption` (for `button_data` entries) or `_mft` (for misc class-level attrs) so they'll be picked up for translation.
|
||||
* English string literals anywhere else should be wrapped with `_()` to be marked for translation AND provide the dynamic translated value.
|
||||
* In general, don't go out of your way to translate text before passing it into `Screen` classes.
|
||||
* The `Screen` itself should do most of the `_()` calls to fetch translations for final display.
|
||||
* Minor risk of double-translation weirdness otherwise.
|
||||
|
||||
Mark for translation in the `View`. Retrieve translated values in the `Screen`. Pass final display text into the basic gui `Component`s.
|
||||
|
||||
---
|
||||
|
||||
## Provide translation context hints
|
||||
In many cases the English string literal on its own does not provide enough context for translators to understand how the word is being used.
|
||||
|
||||
For example, is "change" referring to altering a value OR is it the amount coming back to you in a transaction?
|
||||
|
||||
Whenever necessary, add explanatory context as a comment. This applies to all three ways of marking strings for translation.
|
||||
|
||||
The `extract_messages` command is explictly looking for the exact string: `# TRANSLATOR_NOTE:` in comments.
|
||||
|
||||
```python
|
||||
class SeedAddressVerificationView(View):
|
||||
# TRANSLATOR_NOTE: Option when scanning for a matching address; skips ten addresses ahead
|
||||
SKIP_10 = ButtonOption("Skip 10")
|
||||
```
|
||||
|
||||
Note that the comment MUST be on the preceding line of executable code for it to work:
|
||||
|
||||
```python
|
||||
class SettingsConstants
|
||||
# TRANSLATOR_NOTE: QR code density option: Low, Medium, High <-- ✅ Correct way to add context
|
||||
density_low = _mft("Low")
|
||||
|
||||
ALL_DENSITIES = [
|
||||
(DENSITY__LOW, density_low),
|
||||
# TRANSLATOR_NOTE: QR code density option: Low, Medium, High <-- ❌ Note will NOT be picked up
|
||||
(DENSITY__MEDIUM, "Medium"),
|
||||
(DENSITY__HIGH, "High"),
|
||||
]
|
||||
```
|
||||
|
||||
```python
|
||||
# TRANSLATOR_NOTE: Refers to the user's change output in a psbt
|
||||
some_var = _("change")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## `_()` Wrapping syntax details
|
||||
* Use `.format()` to wrap strings with variable injections. Note that `.format()` is OUTSIDE the `_()` wrapping.
|
||||
```python
|
||||
mystr = f"My dad's name is {dad.name} and my name is {self.name}."
|
||||
mystr = _("My dad's name is {} and my name is {}").format(dad.name, self.name)
|
||||
```
|
||||
|
||||
The translators will only see: "My dad's name is {} and my name is {}" in Transifex. Often the English string literal is
|
||||
basically incomprehensible on its own so always provide an explanation for what is being injected:
|
||||
|
||||
```python
|
||||
# TRANSLATOR_NOTE: Address verification success message (e.g. "bc1qabc = seed 12345678's receive address #0.")
|
||||
text = _("{} = {}'s {} address #{}.").format(...)
|
||||
```
|
||||
|
||||
If there are a lot of variables to inject, placeholder names can be used (TODO: how does Transifex display this?):
|
||||
```python
|
||||
mystr = _("My dad's name is {dad_name} and my name is {my_name}").format(dad_name=dad.name, my_name=self.name)
|
||||
```
|
||||
* Use `ngettext` to dynamically handle singular vs plural forms based on an integer quantity:
|
||||
```python
|
||||
n = 1
|
||||
print(ngettext("apple", "apples", n))
|
||||
>> apple
|
||||
|
||||
n = 5
|
||||
print(ngettext("apple", "apples", n))
|
||||
>> apples
|
||||
```
|
||||
|
||||
Transifex will ask translators to provide the singular and plural forms on a language-specific basis (e.g. Arabic as THREE plural forms!).
|
||||
|
||||
---
|
||||
|
||||
## Set up localization dependencies
|
||||
```bash
|
||||
pip install -r l10n/requirements-l10n.txt
|
||||
```
|
||||
|
||||
Make sure that your local repo has fetched the `seedsigner-translations` submodule. It's configured to add it in src/seedsigner/resources.
|
||||
```bash
|
||||
# Need --remote in order to respect the target branch listed in .gitmodules
|
||||
git submodule update --remote
|
||||
```
|
||||
|
||||
|
||||
### Pre-configured `babel` commands
|
||||
The `setup.cfg` file in the project root specifies params for the various `babel` commands discussed below.
|
||||
|
||||
You should have already added the local code as an editable project in pip:
|
||||
```bash
|
||||
# From the repo root
|
||||
pip install -e .
|
||||
```
|
||||
|
||||
|
||||
### Rescanning for text that needs translations
|
||||
Re-generate the `messages.pot` file:
|
||||
|
||||
```bash
|
||||
python setup.py extract_messages
|
||||
```
|
||||
|
||||
This will rescan all wrapped text, picking up new strings as well as updating existings strings that have been edited.
|
||||
|
||||
_TODO: Github Action to auto-generate messages.pot and fail a PR update if the PR has an out of date messages.pot?_
|
||||
|
||||
|
||||
### Making new text available to translators
|
||||
Upload the master `messages.pot` to Transifex. It will automatically update each language with the new or changed source strings.
|
||||
|
||||
_TODO: Look into Transifex options to automatically pull updates?_
|
||||
|
||||
|
||||
### Once new translations are complete
|
||||
The translation file for each language will need to be downloaded via Transifex's "Download for use" option (sends you a `messages.po` file for that language).
|
||||
|
||||
This updated `messages.po` should be added to the seedsigner-translations repo in l10n/`{TARGET_LOCALE}`/LC_MESSAGES.
|
||||
|
||||
|
||||
### Compile all the translations
|
||||
The `messages.po` files must be compiled into `*.mo` files:
|
||||
|
||||
```bash
|
||||
python setup.py compile_catalog
|
||||
|
||||
# Or target a specific language code:
|
||||
python setup.py compile_catalog -l es
|
||||
```
|
||||
|
||||
### Unused babel commands
|
||||
Transifex eliminates the need for the `init_catalog` and `update_catalog` commands.
|
||||
|
||||
|
||||
## Keep the seedsigner-translations repo up to date
|
||||
The *.po files for each language and their compiled *.mo files should all be kept up to date in the seedsigner-translations repo.
|
||||
|
||||
_TODO: Github Actions automation to regenerate / verify that the *.mo files have been updated after *.po changes._
|
||||
|
||||
---
|
||||
|
||||
## Generate screenshots in each language
|
||||
Simply run the screenshot generator:
|
||||
|
||||
```bash
|
||||
pytest tests/screenshot_generator/generator.py
|
||||
|
||||
# Or target a specific language code:
|
||||
pytest tests/screenshot_generator/generator.py --locale es
|
||||
```
|
||||
+1492
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
Babel==2.16.0
|
||||
+3
-4
@@ -13,7 +13,7 @@ description = "Build an offline, airgapped Bitcoin signing device for less than
|
||||
name = "seedsigner"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
version = "0.8.0"
|
||||
version = "0.8.5-rc1"
|
||||
|
||||
[project.urls]
|
||||
"Bug Tracker" = "https://github.com/SeedSigner/seedsigner/issues"
|
||||
@@ -47,13 +47,12 @@ exclude_lines = [
|
||||
omit = [
|
||||
"*/__init__.py",
|
||||
"*/tests/*",
|
||||
"*/pyzbar/*",
|
||||
"*/gui/*"
|
||||
]
|
||||
skip_covered = true
|
||||
skip_covered = false
|
||||
skip_empty = true
|
||||
|
||||
[tool.coverage.run]
|
||||
source = ["src"]
|
||||
branch = true
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
|
||||
Submodule
+1
Submodule seedsigner-screenshots added at f313906766
@@ -0,0 +1,12 @@
|
||||
[extract_messages]
|
||||
keywords = _ _mft mark_for_translation ButtonOption
|
||||
add_comments = TRANSLATOR_NOTE:
|
||||
strip_comments = yes
|
||||
add_location = file
|
||||
input_dirs = src
|
||||
output_file = l10n/messages.pot
|
||||
|
||||
|
||||
[compile_catalog]
|
||||
use_fuzzy = yes
|
||||
directory = src/seedsigner/resources/seedsigner-translations/l10n
|
||||
@@ -0,0 +1,8 @@
|
||||
"""
|
||||
Empty setup.py necessary for the python-babel integration (e.g. python setup.py extract_messages).
|
||||
|
||||
See the configuration in setup.cfg.
|
||||
"""
|
||||
import setuptools
|
||||
|
||||
setuptools.setup()
|
||||
@@ -99,7 +99,7 @@ class Controller(Singleton):
|
||||
rather than at the top in order avoid circular imports.
|
||||
"""
|
||||
|
||||
VERSION = "0.8.0"
|
||||
VERSION = "0.8.5-rc1"
|
||||
|
||||
# Declare class member vars with type hints to enable richer IDE support throughout
|
||||
# the code.
|
||||
@@ -147,20 +147,20 @@ class Controller(Singleton):
|
||||
else:
|
||||
# Instantiate the one and only Controller instance
|
||||
return cls.configure_instance()
|
||||
|
||||
|
||||
@classmethod
|
||||
def reset_instance(cls):
|
||||
"""
|
||||
Currently used by the screenshot generator, but could potentially be used to
|
||||
wipe and reset the state of the device.
|
||||
"""
|
||||
cls._instance = None
|
||||
cls.configure_instance()
|
||||
|
||||
|
||||
@classmethod
|
||||
def configure_instance(cls, disable_hardware=False):
|
||||
"""
|
||||
- `disable_hardware` is only meant to be used by the test suite so that it
|
||||
can keep re-initializing a Controller in however many tests it needs to. But
|
||||
this is only possible if the hardware isn't already being reserved. Without
|
||||
this you get:
|
||||
|
||||
RuntimeError: Conflicting edge detection already enabled for this GPIO channel
|
||||
|
||||
each time you try to re-initialize a Controller.
|
||||
"""
|
||||
def configure_instance(cls):
|
||||
from seedsigner.gui.renderer import Renderer
|
||||
from seedsigner.hardware.microsd import MicroSD
|
||||
|
||||
@@ -248,10 +248,10 @@ class Controller(Singleton):
|
||||
used. Only used by the test suite.
|
||||
"""
|
||||
from seedsigner.views import MainMenuView, BackStackView
|
||||
from seedsigner.views.screensaver import OpeningSplashScreen
|
||||
from seedsigner.views.screensaver import OpeningSplashView
|
||||
from seedsigner.gui.toast import RemoveSDCardToastManagerThread
|
||||
|
||||
OpeningSplashScreen().start()
|
||||
OpeningSplashView().run()
|
||||
|
||||
""" Class references can be stored as variables in python!
|
||||
|
||||
|
||||
+624
-129
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,7 @@
|
||||
from dataclasses import dataclass
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
from typing import Tuple
|
||||
from gettext import gettext as _
|
||||
|
||||
from seedsigner.gui.components import Fonts, GUIConstants
|
||||
from seedsigner.hardware.buttons import HardwareButtonsConstants
|
||||
@@ -8,6 +9,10 @@ from seedsigner.hardware.buttons import HardwareButtonsConstants
|
||||
|
||||
|
||||
class Keyboard:
|
||||
"""
|
||||
Note that it is up to the calling Screen to manage the Renderer.lock. The Keyboard
|
||||
and its child Keys should NOT attempt to manage the lock.
|
||||
"""
|
||||
WRAP_TOP = "wrap_top"
|
||||
WRAP_BOTTOM = "wrap_bottom"
|
||||
WRAP_LEFT = "wrap_left"
|
||||
@@ -27,39 +32,43 @@ class Keyboard:
|
||||
REGULAR_KEY_FONT = "regular"
|
||||
COMPACT_KEY_FONT = "compact"
|
||||
|
||||
# TRANSLATOR_NOTE: The abbreviated label for the special key <del> on a standard keyboard.
|
||||
del_label = _("del")
|
||||
KEY_BACKSPACE = {
|
||||
"code": "DEL",
|
||||
"letter": "del",
|
||||
"letter": del_label,
|
||||
"font": COMPACT_KEY_FONT,
|
||||
"size": 2,
|
||||
}
|
||||
# TRANSLATOR_NOTE: The abbreviated label for the special key <space> on a standard keyboard.
|
||||
space_label = _("space")
|
||||
KEY_SPACE = {
|
||||
"code": "SPACE",
|
||||
"letter": "space",
|
||||
"letter": space_label,
|
||||
"font": COMPACT_KEY_FONT,
|
||||
"size": 1,
|
||||
}
|
||||
KEY_SPACE_2 = {
|
||||
"code": "SPACE",
|
||||
"letter": "space",
|
||||
"letter": space_label,
|
||||
"font": COMPACT_KEY_FONT,
|
||||
"size": 2,
|
||||
}
|
||||
KEY_SPACE_3 = {
|
||||
"code": "SPACE",
|
||||
"letter": "space",
|
||||
"letter": space_label,
|
||||
"font": COMPACT_KEY_FONT,
|
||||
"size": 3,
|
||||
}
|
||||
KEY_SPACE_4 = {
|
||||
"code": "SPACE",
|
||||
"letter": "space",
|
||||
"letter": space_label,
|
||||
"font": COMPACT_KEY_FONT,
|
||||
"size": 4,
|
||||
}
|
||||
KEY_SPACE_5 = {
|
||||
"code": "SPACE",
|
||||
"letter": "space",
|
||||
"letter": space_label,
|
||||
"font": COMPACT_KEY_FONT,
|
||||
"size": 5,
|
||||
}
|
||||
@@ -158,7 +167,7 @@ class Keyboard:
|
||||
self.screen_x + int(self.keyboard.key_width * self.size / 2),
|
||||
self.screen_y + self.keyboard.key_height - int((self.keyboard.key_height - text_height)/2)
|
||||
),
|
||||
self.letter,
|
||||
_(self.letter),
|
||||
fill=font_color,
|
||||
font=font,
|
||||
anchor="ms"
|
||||
@@ -208,6 +217,7 @@ class Keyboard:
|
||||
|
||||
# Set up the rendering and state params
|
||||
self.active_keys = list(self.charset)
|
||||
|
||||
self.additonal_key_compact_font = Fonts.get_font("RobotoCondensed-Bold", 18)
|
||||
self.x_start = rect[0]
|
||||
self.y_start = rect[1]
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from PIL import Image, ImageDraw
|
||||
from threading import Lock
|
||||
|
||||
from seedsigner.gui.components import Fonts, GUIConstants
|
||||
from seedsigner.hardware.ST7789 import ST7789
|
||||
from seedsigner.models.singleton import ConfigurableSingleton
|
||||
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
from dataclasses import dataclass
|
||||
import math
|
||||
from PIL import Image, ImageDraw, ImageFilter
|
||||
from typing import List
|
||||
import time
|
||||
|
||||
from dataclasses import dataclass
|
||||
from gettext import gettext as _
|
||||
from gettext import ngettext
|
||||
from PIL import Image, ImageDraw, ImageFilter
|
||||
|
||||
from seedsigner.gui.components import (BtcAmount, Icon, FontAwesomeIconConstants, IconTextLine, FormattedAddress, GUIConstants, Fonts, SeedSignerIconConstants, TextArea,
|
||||
calc_bezier_curve, linear_interp)
|
||||
from seedsigner.gui.renderer import Renderer
|
||||
from seedsigner.models.threads import BaseThread
|
||||
|
||||
from .screen import ButtonListScreen, WarningScreen
|
||||
from ..components import (BtcAmount, Button, Icon, FontAwesomeIconConstants, IconTextLine, FormattedAddress, GUIConstants, Fonts, SeedSignerIconConstants, TextArea,
|
||||
calc_bezier_curve, linear_interp)
|
||||
from .screen import ButtonListScreen, ButtonOption
|
||||
|
||||
|
||||
|
||||
@@ -21,15 +23,15 @@ class PSBTOverviewScreen(ButtonListScreen):
|
||||
num_inputs: int = 0
|
||||
num_self_transfer_outputs: int = 0
|
||||
num_change_outputs: int = 0
|
||||
destination_addresses: List[str] = None
|
||||
destination_addresses: list[str] = None
|
||||
has_op_return: bool = False
|
||||
|
||||
|
||||
def __post_init__(self):
|
||||
# Customize defaults
|
||||
self.title = "Review PSBT"
|
||||
self.title = _("Review PSBT")
|
||||
self.is_bottom_list = True
|
||||
self.button_data = ["Review Details"]
|
||||
self.button_data = [ButtonOption("Review Details")]
|
||||
|
||||
# This screen can take a while to load while parsing the PSBT
|
||||
self.show_loading_screen = True
|
||||
@@ -69,7 +71,7 @@ class PSBTOverviewScreen(ButtonListScreen):
|
||||
draw = ImageDraw.Draw(image)
|
||||
|
||||
font_size = GUIConstants.BODY_FONT_MIN_SIZE * ssf
|
||||
font = Fonts.get_font(GUIConstants.BODY_FONT_NAME, font_size)
|
||||
font = Fonts.get_font(GUIConstants.get_body_font_name(), font_size)
|
||||
|
||||
(left, top, right, bottom) = font.getbbox(text="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890[]", anchor="lt")
|
||||
chart_text_height = bottom
|
||||
@@ -86,16 +88,18 @@ class PSBTOverviewScreen(ButtonListScreen):
|
||||
# First calculate how wide the inputs col will be
|
||||
inputs_column = []
|
||||
if self.num_inputs == 1:
|
||||
inputs_column.append("1 input")
|
||||
inputs_column.append(_("1 input"))
|
||||
elif self.num_inputs > 5:
|
||||
inputs_column.append("input 1")
|
||||
inputs_column.append("input 2")
|
||||
inputs_column.append("[ ... ]")
|
||||
inputs_column.append(f"input {self.num_inputs-1}")
|
||||
inputs_column.append(f"input {self.num_inputs}")
|
||||
inputs_column.append(_("input 1"))
|
||||
inputs_column.append(_("input 2"))
|
||||
# TRANSLATOR_NOTE: Indicates that items have been omitted from a series: e.g. "1, 2, 3, [...], 8"
|
||||
inputs_column.append(_("[ ... ]"))
|
||||
# TRANSLATOR_NOTE: Input number will be inserted (e.g. "input 3")
|
||||
inputs_column.append(_("input {}").format(self.num_inputs-1))
|
||||
inputs_column.append(_("input {}").format(self.num_inputs))
|
||||
else:
|
||||
for i in range(0, self.num_inputs):
|
||||
inputs_column.append(f"input {i+1}")
|
||||
inputs_column.append(_("input {}").format(i+1))
|
||||
|
||||
max_inputs_text_width = 0
|
||||
for input in inputs_column:
|
||||
@@ -123,11 +127,12 @@ class PSBTOverviewScreen(ButtonListScreen):
|
||||
# Now let's maximize the actual destination col by adjusting our addr truncation
|
||||
def calculate_destination_col_width(truncate_at: int = 0):
|
||||
def truncate_destination_addr(addr):
|
||||
# TODO: Properly handle the ellipsis truncation in different languages
|
||||
if len(addr) <= truncate_at + len("..."):
|
||||
# TRANSLATOR_NOTE: Ellipsis ("...") characters used to truncate an address (e.g. "bc1qabc...")
|
||||
if len(addr) <= truncate_at + len(_("...")):
|
||||
# No point in truncating
|
||||
return addr
|
||||
return f"{addr[:truncate_at]}..."
|
||||
|
||||
return addr[:truncate_at] + _("...")
|
||||
|
||||
destination_column = []
|
||||
|
||||
@@ -136,21 +141,25 @@ class PSBTOverviewScreen(ButtonListScreen):
|
||||
destination_column.append(truncate_destination_addr(addr))
|
||||
|
||||
for i in range(0, self.num_self_transfer_outputs):
|
||||
destination_column.append(truncate_destination_addr("self-transfer"))
|
||||
destination_column.append(truncate_destination_addr(_("self-transfer")))
|
||||
else:
|
||||
# destination_column.append(f"{len(self.destination_addresses)} recipients")
|
||||
destination_column.append(f"recipient 1")
|
||||
destination_column.append(f"[ ... ]")
|
||||
destination_column.append(f"recipient {len(self.destination_addresses) + self.num_self_transfer_outputs}")
|
||||
destination_column.append(_("recipient 1"))
|
||||
# TRANSLATOR_NOTE: Indicates that items have been omitted from a series: e.g. "1, 2, 3, [...], 8"
|
||||
destination_column.append(_("[ ... ]"))
|
||||
# TRANSLATOR_NOTE: Inserts the recipient number (e.g. the fifth one is: "recipient 5")
|
||||
destination_column.append(_("recipient {}").format(len(self.destination_addresses) + self.num_self_transfer_outputs))
|
||||
|
||||
destination_column.append(f"fee")
|
||||
destination_column.append(_("fee"))
|
||||
|
||||
if self.has_op_return:
|
||||
destination_column.append("OP_RETURN")
|
||||
# TRANSLATOR_NOTE: Technical term, should probably NOT be translated in most languages
|
||||
destination_column.append(_("OP_RETURN"))
|
||||
|
||||
if self.num_change_outputs > 0:
|
||||
for i in range(0, self.num_change_outputs):
|
||||
destination_column.append("change")
|
||||
# TRANSLATOR_NOTE: Label for a change output in the PSBT Overview flow diagram
|
||||
destination_column.append(_("change"))
|
||||
|
||||
max_destination_text_width = 0
|
||||
for destination in destination_column:
|
||||
@@ -164,10 +173,16 @@ class PSBTOverviewScreen(ButtonListScreen):
|
||||
# We're not going to display any destination addrs so truncation doesn't matter
|
||||
(destination_text_width, destination_column) = calculate_destination_col_width()
|
||||
else:
|
||||
destination_text_width = None
|
||||
destination_column = None
|
||||
# Steadliy widen out the destination column until we run out of space
|
||||
for i in range(6, 14):
|
||||
(new_width, new_col_text) = calculate_destination_col_width(truncate_at=i)
|
||||
if new_width > max_destination_col_width:
|
||||
if not destination_text_width:
|
||||
destination_text_width = new_width
|
||||
if not destination_column:
|
||||
destination_column = new_col_text
|
||||
break
|
||||
destination_text_width = new_width
|
||||
destination_column = new_col_text
|
||||
@@ -349,7 +364,7 @@ class PSBTOverviewScreen(ButtonListScreen):
|
||||
destination_y += destination_y_spacing
|
||||
|
||||
# Resize to target and sharpen final image
|
||||
image = image.resize((self.canvas_width, chart_height), Image.LANCZOS)
|
||||
image = image.resize((self.canvas_width, chart_height), Image.Resampling.LANCZOS)
|
||||
self.paste_images.append((image.filter(ImageFilter.SHARPEN), (self.chart_x, self.chart_y)))
|
||||
|
||||
# Pass input and output curves to the animation thread
|
||||
@@ -463,14 +478,14 @@ class PSBTMathScreen(ButtonListScreen):
|
||||
|
||||
def __post_init__(self):
|
||||
# Customize defaults
|
||||
self.title = "PSBT Math"
|
||||
self.button_data = ["Review Recipients"]
|
||||
self.title = _("PSBT Math")
|
||||
self.button_data = [ButtonOption("Review Recipients")]
|
||||
self.is_bottom_list = True
|
||||
|
||||
super().__post_init__()
|
||||
|
||||
if self.input_amount > 1e6:
|
||||
denomination = "btc"
|
||||
denomination = _("btc")
|
||||
self.input_amount /= 1e8
|
||||
self.spend_amount /= 1e8
|
||||
self.change_amount /= 1e8
|
||||
@@ -482,7 +497,7 @@ class PSBTMathScreen(ButtonListScreen):
|
||||
# lines up properly.
|
||||
self.fee_amount = f"{self.fee_amount:10}"
|
||||
else:
|
||||
denomination = "sats"
|
||||
denomination = _("sats")
|
||||
self.input_amount = f"{self.input_amount:,}"
|
||||
self.spend_amount = f"{self.spend_amount:,}"
|
||||
self.fee_amount = f"{self.fee_amount:,}"
|
||||
@@ -509,9 +524,9 @@ class PSBTMathScreen(ButtonListScreen):
|
||||
image = Image.new("RGB", (body_width*ssf, body_height*ssf))
|
||||
draw = ImageDraw.Draw(image)
|
||||
|
||||
body_font = Fonts.get_font(GUIConstants.BODY_FONT_NAME, (GUIConstants.BODY_FONT_SIZE)*ssf)
|
||||
fixed_width_font = Fonts.get_font(GUIConstants.FIXED_WIDTH_FONT_NAME, (GUIConstants.BODY_FONT_SIZE + 6)*ssf)
|
||||
left, top, right, bottom = fixed_width_font.getbbox(self.input_amount + "Q")
|
||||
body_font = Fonts.get_font(GUIConstants.get_body_font_name(), (GUIConstants.get_body_font_size())*ssf)
|
||||
fixed_width_font = Fonts.get_font(GUIConstants.FIXED_WIDTH_FONT_NAME, (GUIConstants.get_body_font_size() + 6)*ssf)
|
||||
left, top, right, bottom = fixed_width_font.getbbox(self.input_amount + "+")
|
||||
digits_width, digits_height = right - left, bottom - top
|
||||
|
||||
# Draw each line of the equation
|
||||
@@ -524,7 +539,7 @@ class PSBTMathScreen(ButtonListScreen):
|
||||
# secondary_digit_color = GUIConstants.BODY_FONT_COLOR
|
||||
# tertiary_digit_color = GUIConstants.BODY_FONT_COLOR
|
||||
# digit_group_spacing = 0
|
||||
if denomination == 'btc':
|
||||
if denomination == _('btc'):
|
||||
display_str = amount_str
|
||||
main_zone = display_str[:-6]
|
||||
mid_zone = display_str[-6:-3]
|
||||
@@ -538,13 +553,12 @@ class PSBTMathScreen(ButtonListScreen):
|
||||
draw.text((main_zone_width + digit_group_spacing + mid_zone_width + digit_group_spacing, cur_y), text=end_zone, font=fixed_width_font, fill=tertiary_digit_color)
|
||||
else:
|
||||
draw.text((0, cur_y), text=amount_str, font=fixed_width_font, fill=GUIConstants.BODY_FONT_COLOR)
|
||||
draw.text((digits_width + 2*digit_group_spacing, cur_y), text=info_text, font=body_font, fill=info_text_color)
|
||||
draw.text((digits_width + 3*digit_group_spacing, cur_y), text=info_text, font=body_font, fill=info_text_color)
|
||||
|
||||
render_amount(
|
||||
cur_y,
|
||||
f" {self.input_amount}",
|
||||
# info_text=f""" {self.num_inputs} input{"s" if self.num_inputs > 1 else ""}""",
|
||||
info_text=f""" input{"s" if self.num_inputs > 1 else ""}""",
|
||||
info_text=ngettext("input", "inputs", self.num_inputs),
|
||||
)
|
||||
|
||||
# spend_amount will be zero on self-transfers; only display when there's an
|
||||
@@ -554,15 +568,14 @@ class PSBTMathScreen(ButtonListScreen):
|
||||
render_amount(
|
||||
cur_y,
|
||||
f"-{self.spend_amount}",
|
||||
# info_text=f""" {self.num_recipients} recipient{"s" if self.num_recipients > 1 else ""}""",
|
||||
info_text=f""" recipient{"s" if self.num_recipients > 1 else ""}""",
|
||||
info_text=ngettext("recipient", "recipients", self.num_recipients),
|
||||
)
|
||||
|
||||
cur_y += digits_height + GUIConstants.BODY_LINE_SPACING * ssf
|
||||
render_amount(
|
||||
cur_y,
|
||||
f"-{self.fee_amount}",
|
||||
info_text=f""" fee""",
|
||||
info_text=_("fee"),
|
||||
)
|
||||
|
||||
cur_y += digits_height + GUIConstants.BODY_LINE_SPACING * ssf
|
||||
@@ -572,12 +585,13 @@ class PSBTMathScreen(ButtonListScreen):
|
||||
render_amount(
|
||||
cur_y,
|
||||
f" {self.change_amount}",
|
||||
info_text=f" {denomination} change",
|
||||
# TRANSLATOR_NOTE: Denonination is inserted (e.g. your "btc change" or "sats change")
|
||||
info_text=_("{} change").format(denomination),
|
||||
info_text_color="darkorange" # super-sampling alters the perceived color
|
||||
)
|
||||
|
||||
# Resize to target and sharpen final image
|
||||
image = image.resize((body_width, body_height), Image.LANCZOS)
|
||||
image = image.resize((body_width, body_height), Image.Resampling.LANCZOS)
|
||||
self.paste_images.append((image.filter(ImageFilter.SHARPEN), (GUIConstants.EDGE_PADDING, self.top_nav.height + GUIConstants.COMPONENT_PADDING)))
|
||||
|
||||
|
||||
@@ -635,7 +649,6 @@ class PSBTAddressDetailsScreen(ButtonListScreen):
|
||||
|
||||
@dataclass
|
||||
class PSBTChangeDetailsScreen(ButtonListScreen):
|
||||
title: str = "Your Change"
|
||||
amount: int = 0
|
||||
address: str = None
|
||||
is_multisig: bool = False
|
||||
@@ -662,10 +675,20 @@ class PSBTChangeDetailsScreen(ButtonListScreen):
|
||||
))
|
||||
|
||||
screen_y = self.components[-1].screen_y + self.components[-1].height + 2*GUIConstants.COMPONENT_PADDING
|
||||
|
||||
change_type = _("Multisig") if self.is_multisig else self.fingerprint
|
||||
|
||||
if self.is_change_derivation_path :
|
||||
addr_type = _("Change")
|
||||
else:
|
||||
# TRANSLATOR_NOTE: Abbreviation for receive address
|
||||
addr_type = _("Addr")
|
||||
|
||||
value_text = "{}: {} #{}".format(change_type, addr_type, self.derivation_path_addr_index)
|
||||
self.components.append(IconTextLine(
|
||||
value_text=value_text,
|
||||
icon_name=SeedSignerIconConstants.FINGERPRINT,
|
||||
icon_color=GUIConstants.INFO_COLOR,
|
||||
value_text=f"""{"Multisig" if self.is_multisig else self.fingerprint}: {"Change" if self.is_change_derivation_path else "Addr"} #{self.derivation_path_addr_index}""",
|
||||
is_text_centered=False,
|
||||
screen_x=GUIConstants.EDGE_PADDING,
|
||||
screen_y=screen_y,
|
||||
@@ -675,7 +698,7 @@ class PSBTChangeDetailsScreen(ButtonListScreen):
|
||||
self.components.append(IconTextLine(
|
||||
icon_name=SeedSignerIconConstants.SUCCESS,
|
||||
icon_color=GUIConstants.SUCCESS_COLOR,
|
||||
value_text="Address verified!",
|
||||
value_text=_("Address verified!"),
|
||||
is_text_centered=False,
|
||||
screen_x=GUIConstants.EDGE_PADDING,
|
||||
screen_y=self.components[-1].screen_y + self.components[-1].height + GUIConstants.COMPONENT_PADDING,
|
||||
@@ -697,7 +720,7 @@ class PSBTOpReturnScreen(ButtonListScreen):
|
||||
# Simple case: display human-readable text
|
||||
self.components.append(TextArea(
|
||||
text=self.op_return_data.decode(errors="strict"), # "strict" is a good enough heuristic to decide if it's human readable
|
||||
font_size=GUIConstants.TOP_NAV_TITLE_FONT_SIZE,
|
||||
font_size=GUIConstants.get_top_nav_title_font_size(),
|
||||
is_text_centered=True,
|
||||
allow_text_overflow=True,
|
||||
screen_y=self.top_nav.height + GUIConstants.COMPONENT_PADDING,
|
||||
@@ -707,7 +730,7 @@ class PSBTOpReturnScreen(ButtonListScreen):
|
||||
except UnicodeDecodeError:
|
||||
# Contains data that can't be converted to UTF-8; probably encoded and not
|
||||
# meant to be human readable.
|
||||
font = Fonts.get_font(GUIConstants.FIXED_WIDTH_FONT_NAME, size=GUIConstants.BODY_FONT_SIZE)
|
||||
font = Fonts.get_font(GUIConstants.FIXED_WIDTH_FONT_NAME, size=GUIConstants.get_body_font_size())
|
||||
(left, top, right, bottom) = font.getbbox("X", anchor="ls")
|
||||
chars_per_line = int((self.canvas_width - 2*GUIConstants.EDGE_PADDING) / (right - left))
|
||||
decoded_str = self.op_return_data.hex()
|
||||
@@ -717,8 +740,10 @@ class PSBTOpReturnScreen(ButtonListScreen):
|
||||
text += (decoded_str[i*chars_per_line:(i+1)*chars_per_line]) + "\n"
|
||||
text = text[:-1]
|
||||
|
||||
# TRANSLATOR_NOTE: Shown when displaying OP_RETURN as non-human-readable hexadecimal data
|
||||
hex_label = _("raw hex data")
|
||||
label = TextArea(
|
||||
text="raw hex data",
|
||||
text=hex_label,
|
||||
font_color=GUIConstants.LABEL_FONT_COLOR,
|
||||
font_size=GUIConstants.LABEL_FONT_SIZE,
|
||||
screen_y=self.top_nav.height,
|
||||
@@ -728,7 +753,7 @@ class PSBTOpReturnScreen(ButtonListScreen):
|
||||
self.components.append(TextArea(
|
||||
text=text,
|
||||
font_name=GUIConstants.FIXED_WIDTH_FONT_NAME,
|
||||
font_size=GUIConstants.BODY_FONT_SIZE,
|
||||
font_size=GUIConstants.get_body_font_size(),
|
||||
screen_y=label.screen_y + label.height + GUIConstants.COMPONENT_PADDING,
|
||||
))
|
||||
|
||||
@@ -738,12 +763,12 @@ class PSBTOpReturnScreen(ButtonListScreen):
|
||||
class PSBTFinalizeScreen(ButtonListScreen):
|
||||
def __post_init__(self):
|
||||
# Customize defaults
|
||||
self.title = "Sign PSBT"
|
||||
self.title = _("Sign PSBT")
|
||||
self.is_bottom_list = True
|
||||
super().__post_init__()
|
||||
|
||||
icon = Icon(
|
||||
icon_name=FontAwesomeIconConstants.PAPER_PLANE,
|
||||
icon_name=SeedSignerIconConstants.SIGN,
|
||||
icon_color=GUIConstants.INFO_COLOR,
|
||||
icon_size=GUIConstants.ICON_LARGE_BUTTON_SIZE,
|
||||
screen_y=self.top_nav.height + GUIConstants.COMPONENT_PADDING
|
||||
@@ -752,6 +777,6 @@ class PSBTFinalizeScreen(ButtonListScreen):
|
||||
self.components.append(icon)
|
||||
|
||||
self.components.append(TextArea(
|
||||
text="Click to authorize this transaction",
|
||||
screen_y=icon.screen_y + icon.height + GUIConstants.COMPONENT_PADDING
|
||||
text=_("Click to approve this transaction"),
|
||||
screen_y=icon.screen_y + icon.height + 2*GUIConstants.COMPONENT_PADDING
|
||||
))
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import time
|
||||
|
||||
from dataclasses import dataclass
|
||||
from gettext import gettext as _
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
from seedsigner.gui import renderer
|
||||
from seedsigner.hardware.buttons import HardwareButtonsConstants
|
||||
from seedsigner.hardware.camera import Camera
|
||||
from seedsigner.models.decode_qr import DecodeQR, DecodeQRStatus
|
||||
from seedsigner.gui.components import GUIConstants, Fonts
|
||||
from seedsigner.models.decode_qr import DecodeQR
|
||||
from seedsigner.models.threads import BaseThread, ThreadsafeCounter
|
||||
|
||||
from .screen import BaseScreen
|
||||
from ..components import GUIConstants, Fonts, SeedSignerIconConstants
|
||||
|
||||
|
||||
|
||||
@@ -56,7 +55,8 @@ class ScanScreen(BaseScreen):
|
||||
# Initialize the base class
|
||||
super().__post_init__()
|
||||
|
||||
self.instructions_text = "< back | " + self.instructions_text
|
||||
# TODO: Arrange this with UI elements rather than text
|
||||
self.instructions_text = "< " + _("back") + " | " + _(self.instructions_text)
|
||||
|
||||
self.camera = Camera.get_instance()
|
||||
self.camera.start_video_stream_mode(resolution=self.resolution, framerate=self.framerate, format="rgb")
|
||||
@@ -65,7 +65,6 @@ class ScanScreen(BaseScreen):
|
||||
self.frames_decoded_counter = ThreadsafeCounter()
|
||||
|
||||
self.threads.append(ScanScreen.LivePreviewThread(
|
||||
camera=self.camera,
|
||||
decoder=self.decoder,
|
||||
renderer=self.renderer,
|
||||
instructions_text=self.instructions_text,
|
||||
@@ -76,8 +75,10 @@ class ScanScreen(BaseScreen):
|
||||
|
||||
|
||||
class LivePreviewThread(BaseThread):
|
||||
def __init__(self, camera: Camera, decoder: DecodeQR, renderer: renderer.Renderer, instructions_text: str, render_rect: tuple[int,int,int,int], frame_decode_status: ThreadsafeCounter, frames_decoded_counter: ThreadsafeCounter):
|
||||
self.camera = camera
|
||||
def __init__(self, decoder: DecodeQR, renderer: renderer.Renderer, instructions_text: str, render_rect: tuple[int,int,int,int], frame_decode_status: ThreadsafeCounter, frames_decoded_counter: ThreadsafeCounter):
|
||||
from seedsigner.hardware.camera import Camera
|
||||
|
||||
self.camera = Camera.get_instance()
|
||||
self.decoder = decoder
|
||||
self.renderer = renderer
|
||||
self.instructions_text = instructions_text
|
||||
@@ -96,10 +97,12 @@ class ScanScreen(BaseScreen):
|
||||
|
||||
|
||||
def run(self):
|
||||
instructions_font = Fonts.get_font(GUIConstants.BODY_FONT_NAME, GUIConstants.BUTTON_FONT_SIZE)
|
||||
from timeit import default_timer as timer
|
||||
|
||||
instructions_font = Fonts.get_font(GUIConstants.get_body_font_name(), GUIConstants.get_button_font_size())
|
||||
|
||||
# pre-calculate how big the animated QR percent display can be
|
||||
left, _, right, _ = instructions_font.getbbox("100%")
|
||||
left, top, right, bottom = instructions_font.getbbox("100%")
|
||||
progress_text_width = right - left
|
||||
|
||||
start_time = time.time()
|
||||
@@ -201,11 +204,12 @@ class ScanScreen(BaseScreen):
|
||||
radius=8
|
||||
)
|
||||
|
||||
# TRANSLATOR_NOTE: Inserts the percentage value of the animated QR scan progress
|
||||
text = _("{}%").format(progress_percentage)
|
||||
|
||||
draw.text(
|
||||
xy=(rectangle.width - GUIConstants.EDGE_PADDING, int(rectangle.height / 2)),
|
||||
text=f"{progress_percentage}%",
|
||||
# text=f"100%",
|
||||
text=text,
|
||||
fill=GUIConstants.BODY_FONT_COLOR,
|
||||
font=instructions_font,
|
||||
anchor="rm", # right-justified, middle
|
||||
@@ -248,6 +252,9 @@ class ScanScreen(BaseScreen):
|
||||
Screen. Once interaction starts, the display updates have to be managed in
|
||||
_run(). The live preview is an extra-complex case.
|
||||
"""
|
||||
from seedsigner.hardware.buttons import HardwareButtonsConstants
|
||||
from seedsigner.models.decode_qr import DecodeQRStatus
|
||||
|
||||
num_frames = 0
|
||||
start_time = time.time()
|
||||
while True:
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
import logging
|
||||
import time
|
||||
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass, field
|
||||
from gettext import gettext as _
|
||||
from PIL import Image, ImageDraw, ImageColor
|
||||
from typing import Any, List, Tuple
|
||||
|
||||
from seedsigner.helpers.l10n import mark_for_translation as _mft
|
||||
from seedsigner.gui.components import (GUIConstants,
|
||||
BaseComponent, Button, Icon, IconButton, LargeIconButton,
|
||||
SeedSignerIconConstants, TopNav, TextArea, load_image)
|
||||
from seedsigner.gui.keyboard import Keyboard, TextEntryDisplay
|
||||
from seedsigner.gui.renderer import Renderer
|
||||
from seedsigner.hardware.buttons import HardwareButtonsConstants, HardwareButtons
|
||||
from seedsigner.models.encode_qr import BaseQrEncoder
|
||||
from seedsigner.models.settings import SettingsConstants
|
||||
from seedsigner.models.threads import BaseThread, ThreadsafeCounter
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Must be huge numbers to avoid conflicting with the selected_button returned by the
|
||||
# screens with buttons.
|
||||
@@ -43,6 +47,13 @@ class BaseScreen(BaseComponent):
|
||||
|
||||
# Tracks position on scrollable pages, determines which elements are visible.
|
||||
self.scroll_y = 0
|
||||
|
||||
|
||||
def get_threads(self) -> List[BaseThread]:
|
||||
threads = self.threads.copy()
|
||||
for component in self.components:
|
||||
threads += component.threads
|
||||
return threads
|
||||
|
||||
|
||||
def display(self) -> Any:
|
||||
@@ -51,17 +62,25 @@ class BaseScreen(BaseComponent):
|
||||
self._render()
|
||||
self.renderer.show_image()
|
||||
|
||||
for t in self.threads:
|
||||
t.start()
|
||||
for t in self.get_threads():
|
||||
if not t.is_alive():
|
||||
t.start()
|
||||
|
||||
return self._run()
|
||||
except Exception as e:
|
||||
repr(e)
|
||||
raise e
|
||||
finally:
|
||||
for t in self.threads:
|
||||
for t in self.get_threads():
|
||||
t.stop()
|
||||
|
||||
for t in self.get_threads():
|
||||
# Wait for each thread to stop; equivalent to `join()` but gracefully
|
||||
# handles threads that were never run (necessary for screenshot generator
|
||||
# compatibility, perhaps other edge cases).
|
||||
while t.is_alive():
|
||||
time.sleep(0.01)
|
||||
|
||||
|
||||
def clear_screen(self):
|
||||
# Clear the whole canvas
|
||||
@@ -99,19 +118,17 @@ class BaseScreen(BaseComponent):
|
||||
|
||||
For example: A basic menu screen where the user can key up and down. The
|
||||
Screen can handle the UI updates to light up the currently selected menu item
|
||||
on its own. Only when the user clicks to make a selection would run() exit
|
||||
and returns the selected option.
|
||||
on its own. Only when the user clicks to make a selection would _run() exit
|
||||
and return the selected option.
|
||||
|
||||
But an alternate use case returns immediately after each user input so the
|
||||
View can update its controlling logic accordingly (e.g. as the user joysticks
|
||||
over different letters in the keyboard UI, we need to make matching changes
|
||||
to the list of mnemonic seed words that match the new letter).
|
||||
In general, _run() will be implemented as a continuous loop waiting for user
|
||||
input and redrawing the screen as needed. When it redraws, it must claim
|
||||
the `Renderer.lock` to ensure that its updates don't conflict with any other
|
||||
threads that might be updating the screen at the same time (e.g. flashing
|
||||
warning edges, auto-scrolling long titles or buttons, etc).
|
||||
|
||||
In this case, it would be called repeatedly in a loop:
|
||||
* run() and wait for it to handle user input
|
||||
* run() exits and returns the user input (e.g. KEY_UP)
|
||||
* View updates its state of the world accordingly
|
||||
* loop and call run() again
|
||||
Just note that this loop cannot hold the lock indefinitely! Each iteration
|
||||
through the loop should claim the lock, render, and then release it.
|
||||
"""
|
||||
raise Exception("Must implement in a child class")
|
||||
|
||||
@@ -124,6 +141,7 @@ class LoadingScreenThread(BaseThread):
|
||||
|
||||
|
||||
def run(self):
|
||||
from seedsigner.gui.renderer import Renderer
|
||||
renderer: Renderer = Renderer.get_instance()
|
||||
|
||||
center_image = load_image("btc_logo_60x60.png")
|
||||
@@ -147,7 +165,7 @@ class LoadingScreenThread(BaseThread):
|
||||
if self.text:
|
||||
TextArea(
|
||||
text=self.text,
|
||||
font_size=GUIConstants.TOP_NAV_TITLE_FONT_SIZE,
|
||||
font_size=GUIConstants.get_top_nav_title_font_size(),
|
||||
screen_y=int((renderer.canvas_height - bounding_box[3])/2),
|
||||
).render()
|
||||
|
||||
@@ -190,7 +208,7 @@ class BaseTopNavScreen(BaseScreen):
|
||||
top_nav_icon_name: str = None
|
||||
top_nav_icon_color: str = None
|
||||
title: str = "Screen Title"
|
||||
title_font_size: int = GUIConstants.TOP_NAV_TITLE_FONT_SIZE
|
||||
title_font_size: int = GUIConstants.get_top_nav_title_font_size()
|
||||
show_back_button: bool = True
|
||||
show_power_button: bool = False
|
||||
|
||||
@@ -199,7 +217,7 @@ class BaseTopNavScreen(BaseScreen):
|
||||
self.top_nav = TopNav(
|
||||
icon_name=self.top_nav_icon_name,
|
||||
icon_color=self.top_nav_icon_color,
|
||||
text=self.title,
|
||||
text=_(self.title), # Wrap here for just-in-time translations
|
||||
font_size=self.title_font_size,
|
||||
width=self.canvas_width,
|
||||
height=GUIConstants.TOP_NAV_HEIGHT,
|
||||
@@ -218,11 +236,7 @@ class BaseTopNavScreen(BaseScreen):
|
||||
time.sleep(0.1)
|
||||
continue
|
||||
|
||||
user_input = self.hw_inputs.wait_for(
|
||||
HardwareButtonsConstants.ALL_KEYS,
|
||||
check_release=True,
|
||||
release_keys=HardwareButtonsConstants.KEYS__ANYCLICK
|
||||
)
|
||||
user_input = self.hw_inputs.wait_for(HardwareButtonsConstants.ALL_KEYS)
|
||||
|
||||
with self.renderer.lock:
|
||||
if not self.top_nav.is_selected and user_input in [
|
||||
@@ -251,14 +265,29 @@ class BaseTopNavScreen(BaseScreen):
|
||||
|
||||
|
||||
|
||||
@dataclass
|
||||
class ButtonOption:
|
||||
"""
|
||||
Note: The babel config in setup.cfg will extract the `button_label` string for translation
|
||||
"""
|
||||
button_label: str
|
||||
icon_name: str = None
|
||||
icon_color: str = None
|
||||
right_icon_name: str = None
|
||||
button_label_color: str = None
|
||||
return_data: Any = None
|
||||
active_button_label: str = None # Changes displayed button label when button is active
|
||||
|
||||
|
||||
|
||||
@dataclass
|
||||
class ButtonListScreen(BaseTopNavScreen):
|
||||
button_data: list = None # list can be a mix of str or tuple(label: str, icon_name: str)
|
||||
button_data: list[ButtonOption] = None
|
||||
selected_button: int = 0
|
||||
is_button_text_centered: bool = True
|
||||
is_bottom_list: bool = False
|
||||
button_font_name: str = GUIConstants.BUTTON_FONT_NAME
|
||||
button_font_size: int = GUIConstants.BUTTON_FONT_SIZE
|
||||
button_font_name: str = None
|
||||
button_font_size: int = None
|
||||
button_selected_color: str = GUIConstants.ACCENT_COLOR
|
||||
|
||||
# Params for version of list used for Settings
|
||||
@@ -270,7 +299,12 @@ class ButtonListScreen(BaseTopNavScreen):
|
||||
|
||||
|
||||
def __post_init__(self):
|
||||
if not self.button_font_name:
|
||||
self.button_font_name = GUIConstants.get_button_font_name()
|
||||
if not self.button_font_size:
|
||||
self.button_font_size = GUIConstants.get_button_font_size()
|
||||
super().__post_init__()
|
||||
|
||||
button_height = GUIConstants.BUTTON_HEIGHT
|
||||
if len(self.button_data) == 1:
|
||||
button_list_height = button_height
|
||||
@@ -289,29 +323,26 @@ class ButtonListScreen(BaseTopNavScreen):
|
||||
self.has_scroll_arrows = True
|
||||
|
||||
self.buttons: List[Button] = []
|
||||
for i, button_label in enumerate(self.button_data):
|
||||
for i, button_option in enumerate(self.button_data):
|
||||
icon_name = None
|
||||
icon_color = None
|
||||
right_icon_name = None
|
||||
button_label_color = None
|
||||
|
||||
# TODO: Define an actual class for button_data?
|
||||
if type(button_label) == tuple:
|
||||
if len(button_label) == 2:
|
||||
(button_label, icon_name) = button_label
|
||||
icon_color = GUIConstants.BUTTON_FONT_COLOR
|
||||
|
||||
elif len(button_label) == 3:
|
||||
(button_label, icon_name, icon_color) = button_label
|
||||
|
||||
elif len(button_label) == 4:
|
||||
(button_label, icon_name, icon_color, button_label_color) = button_label
|
||||
|
||||
elif len(button_label) == 5:
|
||||
(button_label, icon_name, icon_color, button_label_color, right_icon_name) = button_label
|
||||
if type(button_option) == ButtonOption:
|
||||
button_label = button_option.button_label
|
||||
icon_name = button_option.icon_name
|
||||
icon_color = button_option.icon_color
|
||||
right_icon_name = button_option.right_icon_name
|
||||
button_label_color = button_option.button_label_color
|
||||
active_button_label = button_option.active_button_label
|
||||
|
||||
else:
|
||||
raise Exception("Refactor to ButtonOption approach needed!")
|
||||
|
||||
button_kwargs = dict(
|
||||
text=button_label,
|
||||
text=_(button_label), # Wrap here for just-in-time translations
|
||||
active_text=_(active_button_label), # Wrap here for just-in-time translations
|
||||
icon_name=icon_name,
|
||||
icon_color=icon_color if icon_color else GUIConstants.BUTTON_FONT_COLOR,
|
||||
is_icon_inline=True,
|
||||
@@ -325,13 +356,14 @@ class ButtonListScreen(BaseTopNavScreen):
|
||||
font_name=self.button_font_name,
|
||||
font_size=self.button_font_size,
|
||||
font_color=button_label_color if button_label_color else GUIConstants.BUTTON_FONT_COLOR,
|
||||
selected_color=self.button_selected_color
|
||||
selected_color=self.button_selected_color,
|
||||
is_scrollable_text=True, # We need to use the ScrollableText class for long button labels
|
||||
)
|
||||
if self.checked_buttons and i in self.checked_buttons:
|
||||
button_kwargs["is_checked"] = True
|
||||
button = self.Button_cls(**button_kwargs)
|
||||
self.buttons.append(button)
|
||||
|
||||
|
||||
if self.has_scroll_arrows:
|
||||
self.arrow_half_width = 10
|
||||
self.cur_scroll_y = self.scroll_y_initial_offset if self.scroll_y_initial_offset is not None else 0
|
||||
@@ -352,10 +384,21 @@ class ButtonListScreen(BaseTopNavScreen):
|
||||
cur_selected_button.is_selected = True
|
||||
|
||||
|
||||
def get_threads(self) -> List[BaseThread]:
|
||||
threads = super().get_threads()
|
||||
for button in self.buttons:
|
||||
if button.is_scrollable_text:
|
||||
threads += button.threads
|
||||
return threads
|
||||
|
||||
|
||||
def _render(self):
|
||||
super()._render()
|
||||
self._render_visible_buttons()
|
||||
|
||||
# Write the screen updates
|
||||
self.renderer.show_image()
|
||||
|
||||
|
||||
def _render_visible_buttons(self):
|
||||
if self.has_scroll_arrows:
|
||||
@@ -410,6 +453,7 @@ class ButtonListScreen(BaseTopNavScreen):
|
||||
while True:
|
||||
ret = self._run_callback()
|
||||
if ret is not None:
|
||||
logging.info("Exiting ButtonListScreen due to _run_callback")
|
||||
return ret
|
||||
|
||||
user_input = self.hw_inputs.wait_for(
|
||||
@@ -418,9 +462,7 @@ class ButtonListScreen(BaseTopNavScreen):
|
||||
HardwareButtonsConstants.KEY_DOWN,
|
||||
HardwareButtonsConstants.KEY_LEFT,
|
||||
HardwareButtonsConstants.KEY_RIGHT,
|
||||
] + HardwareButtonsConstants.KEYS__ANYCLICK,
|
||||
check_release=True,
|
||||
release_keys=HardwareButtonsConstants.KEYS__ANYCLICK
|
||||
] + HardwareButtonsConstants.KEYS__ANYCLICK
|
||||
)
|
||||
|
||||
with self.renderer.lock:
|
||||
@@ -510,13 +552,18 @@ class ButtonListScreen(BaseTopNavScreen):
|
||||
|
||||
@dataclass
|
||||
class LargeButtonScreen(BaseTopNavScreen):
|
||||
button_data: list = None # list can be a mix of str or tuple(label: str, icon_name: str)
|
||||
button_font_name: str = GUIConstants.BUTTON_FONT_NAME
|
||||
button_font_size: int = 20
|
||||
button_data: list = None
|
||||
button_font_name: str = None
|
||||
button_font_size: int = None
|
||||
button_selected_color: str = GUIConstants.ACCENT_COLOR
|
||||
selected_button: int = 0
|
||||
|
||||
def __post_init__(self):
|
||||
if not self.button_font_name:
|
||||
self.button_font_name = GUIConstants.get_button_font_name()
|
||||
if not self.button_font_size:
|
||||
self.button_font_size = GUIConstants.get_button_font_size() + 2
|
||||
|
||||
super().__post_init__()
|
||||
|
||||
if len(self.button_data) not in [2, 4]:
|
||||
@@ -533,11 +580,20 @@ class LargeButtonScreen(BaseTopNavScreen):
|
||||
button_start_y = self.top_nav.height + int((self.canvas_height - (self.top_nav.height + GUIConstants.COMPONENT_PADDING) - (2 * button_height) - GUIConstants.COMPONENT_PADDING) / 2)
|
||||
|
||||
self.buttons = []
|
||||
for i, button_label in enumerate(self.button_data):
|
||||
if type(button_label) == tuple:
|
||||
(button_label, icon_name) = button_label
|
||||
for i, button_option in enumerate(self.button_data):
|
||||
if type(button_option) == ButtonOption:
|
||||
button_label = button_option.button_label
|
||||
icon_name = button_option.icon_name
|
||||
else:
|
||||
icon_name = None
|
||||
raise Exception("Refactor needed!")
|
||||
|
||||
# elif type(button_option) == str:
|
||||
# button_label = button_option
|
||||
# icon_name = None
|
||||
# elif type(button_option) == tuple:
|
||||
# (button_label, icon_name) = button_option
|
||||
# else:
|
||||
# print(type(button_option))
|
||||
|
||||
if i % 2 == 0:
|
||||
button_start_x = GUIConstants.EDGE_PADDING
|
||||
@@ -545,7 +601,7 @@ class LargeButtonScreen(BaseTopNavScreen):
|
||||
button_start_x = GUIConstants.EDGE_PADDING + button_width + GUIConstants.COMPONENT_PADDING
|
||||
|
||||
button_args = {
|
||||
"text": button_label,
|
||||
"text": _(button_label), # Wrap here for just-in-time translations
|
||||
"screen_x": button_start_x,
|
||||
"screen_y": button_start_y,
|
||||
"width": button_width,
|
||||
@@ -590,9 +646,7 @@ class LargeButtonScreen(BaseTopNavScreen):
|
||||
HardwareButtonsConstants.KEY_DOWN,
|
||||
HardwareButtonsConstants.KEY_LEFT,
|
||||
HardwareButtonsConstants.KEY_RIGHT
|
||||
] + HardwareButtonsConstants.KEYS__ANYCLICK,
|
||||
check_release=True,
|
||||
release_keys=HardwareButtonsConstants.KEYS__ANYCLICK
|
||||
] + HardwareButtonsConstants.KEYS__ANYCLICK
|
||||
)
|
||||
|
||||
with self.renderer.lock:
|
||||
@@ -661,12 +715,12 @@ class QRDisplayScreen(BaseScreen):
|
||||
qr_encoder: BaseQrEncoder = None
|
||||
|
||||
class QRDisplayThread(BaseThread):
|
||||
def __init__(self, qr_encoder: BaseQrEncoder, qr_brightness: ThreadsafeCounter, renderer: Renderer,
|
||||
tips_start_time: ThreadsafeCounter):
|
||||
def __init__(self, qr_encoder: BaseQrEncoder, qr_brightness: ThreadsafeCounter, tips_start_time: ThreadsafeCounter):
|
||||
from seedsigner.gui.renderer import Renderer
|
||||
super().__init__()
|
||||
self.qr_encoder = qr_encoder
|
||||
self.qr_brightness = qr_brightness
|
||||
self.renderer = renderer
|
||||
self.renderer = Renderer.get_instance()
|
||||
self.tips_start_time = tips_start_time
|
||||
|
||||
|
||||
@@ -676,7 +730,7 @@ class QRDisplayScreen(BaseScreen):
|
||||
|
||||
# Instantiate a temp Image and ImageDraw object to draw on
|
||||
rectangle_width = image.width
|
||||
rectangle_height = GUIConstants.COMPONENT_PADDING * 2 + GUIConstants.BODY_FONT_SIZE * 2 + GUIConstants.BODY_LINE_SPACING
|
||||
rectangle_height = GUIConstants.COMPONENT_PADDING * 2 + GUIConstants.get_body_font_size() * 2 + GUIConstants.BODY_LINE_SPACING
|
||||
rectangle = Image.new('RGBA', (rectangle_width, rectangle_height), (0, 0, 0, 0))
|
||||
img_draw = ImageDraw.Draw(rectangle)
|
||||
|
||||
@@ -691,7 +745,7 @@ class QRDisplayScreen(BaseScreen):
|
||||
screen_x=GUIConstants.EDGE_PADDING*2 + 1,
|
||||
screen_y=GUIConstants.COMPONENT_PADDING + 4, # +4 fudge factor to account for where the chevron is drawn relative to baseline
|
||||
icon_name=SeedSignerIconConstants.CHEVRON_UP,
|
||||
icon_size=GUIConstants.BODY_FONT_SIZE,
|
||||
icon_size=GUIConstants.get_body_font_size(),
|
||||
)
|
||||
chevron_up_icon.render()
|
||||
|
||||
@@ -705,12 +759,14 @@ class QRDisplayScreen(BaseScreen):
|
||||
)
|
||||
chevron_down_icon.render()
|
||||
|
||||
# TRANSLATOR_NOTE: Increase QR code screen brightness
|
||||
text = _("Brighter")
|
||||
TextArea(
|
||||
image_draw=img_draw,
|
||||
canvas=rectangle,
|
||||
text="Brighter",
|
||||
font_size=GUIConstants.BODY_FONT_SIZE,
|
||||
font_name=GUIConstants.BUTTON_FONT_NAME,
|
||||
text=text,
|
||||
font_size=GUIConstants.get_body_font_size(),
|
||||
font_name=GUIConstants.get_button_font_name(),
|
||||
background_color=(0, 0, 0, overlay_opacity),
|
||||
edge_padding=0,
|
||||
is_text_centered=False,
|
||||
@@ -721,12 +777,14 @@ class QRDisplayScreen(BaseScreen):
|
||||
allow_text_overflow=False
|
||||
).render()
|
||||
|
||||
# TRANSLATOR_NOTE: Decrease QR code screen brightness
|
||||
text = _("Darker")
|
||||
TextArea(
|
||||
image_draw=img_draw,
|
||||
canvas=rectangle,
|
||||
text="Darker",
|
||||
font_size=GUIConstants.BODY_FONT_SIZE,
|
||||
font_name=GUIConstants.BUTTON_FONT_NAME,
|
||||
text=text,
|
||||
font_size=GUIConstants.get_body_font_size(),
|
||||
font_name=GUIConstants.get_button_font_name(),
|
||||
background_color=(0, 0, 0, overlay_opacity),
|
||||
edge_padding=0,
|
||||
is_text_centered=False,
|
||||
@@ -789,7 +847,6 @@ class QRDisplayScreen(BaseScreen):
|
||||
self.threads.append(QRDisplayScreen.QRDisplayThread(
|
||||
qr_encoder=self.qr_encoder,
|
||||
qr_brightness=self.qr_brightness,
|
||||
renderer=self.renderer,
|
||||
tips_start_time=self.tips_start_time
|
||||
))
|
||||
|
||||
@@ -804,9 +861,7 @@ class QRDisplayScreen(BaseScreen):
|
||||
HardwareButtonsConstants.KEY_DOWN,
|
||||
HardwareButtonsConstants.KEY_LEFT,
|
||||
HardwareButtonsConstants.KEY_RIGHT,
|
||||
] + HardwareButtonsConstants.KEYS__ANYCLICK,
|
||||
check_release=True,
|
||||
release_keys=HardwareButtonsConstants.KEYS__ANYCLICK
|
||||
] + HardwareButtonsConstants.KEYS__ANYCLICK
|
||||
)
|
||||
if user_input == HardwareButtonsConstants.KEY_DOWN:
|
||||
# Reduce QR code background brightness
|
||||
@@ -831,11 +886,11 @@ class QRDisplayScreen(BaseScreen):
|
||||
|
||||
@dataclass
|
||||
class LargeIconStatusScreen(ButtonListScreen):
|
||||
title: str = "Success!"
|
||||
title: str = _mft("Success!")
|
||||
status_icon_name: str = SeedSignerIconConstants.SUCCESS
|
||||
status_icon_size: int = GUIConstants.ICON_PRIMARY_SCREEN_SIZE
|
||||
status_color: str = GUIConstants.SUCCESS_COLOR
|
||||
status_headline: str = "Success!" # The colored text under the large icon
|
||||
status_headline: str = None
|
||||
text: str = "" # The body text of the screen
|
||||
text_edge_padding: int = GUIConstants.EDGE_PADDING
|
||||
button_data: list = None
|
||||
@@ -843,9 +898,9 @@ class LargeIconStatusScreen(ButtonListScreen):
|
||||
|
||||
|
||||
def __post_init__(self):
|
||||
self.is_bottom_list: bool = True
|
||||
if not self.button_data:
|
||||
self.button_data = ["OK"]
|
||||
self.button_data = [ButtonOption("OK")]
|
||||
self.is_bottom_list = True
|
||||
super().__post_init__()
|
||||
|
||||
self.status_icon = Icon(
|
||||
@@ -860,23 +915,23 @@ class LargeIconStatusScreen(ButtonListScreen):
|
||||
next_y = self.status_icon.screen_y + self.status_icon.height + int(GUIConstants.COMPONENT_PADDING/2)
|
||||
if self.status_headline:
|
||||
self.warning_headline_textarea = TextArea(
|
||||
text=self.status_headline,
|
||||
text=_(self.status_headline), # Wrap here for just-in-time translations
|
||||
width=self.canvas_width,
|
||||
screen_y=next_y,
|
||||
font_color=self.status_color,
|
||||
allow_text_overflow=self.allow_text_overflow,
|
||||
auto_line_break=False, # Force headline to be on one line
|
||||
)
|
||||
self.components.append(self.warning_headline_textarea)
|
||||
next_y = next_y + self.warning_headline_textarea.height
|
||||
|
||||
self.components.append(TextArea(
|
||||
height=self.buttons[0].screen_y - next_y,
|
||||
text=self.text,
|
||||
width=self.canvas_width,
|
||||
edge_padding=self.text_edge_padding, # Don't render all the way up to the far left/right edges
|
||||
screen_y=next_y,
|
||||
allow_text_overflow=self.allow_text_overflow,
|
||||
))
|
||||
if self.text:
|
||||
self.components.append(TextArea(
|
||||
height=self.buttons[0].screen_y - next_y,
|
||||
text=_(self.text),
|
||||
width=self.canvas_width,
|
||||
edge_padding=self.text_edge_padding, # Don't render all the way up to the far left/right edges
|
||||
screen_y=next_y,
|
||||
))
|
||||
|
||||
|
||||
|
||||
@@ -958,22 +1013,17 @@ class WarningEdgesMixin:
|
||||
|
||||
@dataclass
|
||||
class WarningScreen(WarningEdgesMixin, LargeIconStatusScreen):
|
||||
title: str = "Caution"
|
||||
title: str = _mft("Caution")
|
||||
status_icon_name: str = SeedSignerIconConstants.WARNING
|
||||
status_color: str = "yellow"
|
||||
status_headline: str = "Privacy Leak!" # The colored text under the alert icon
|
||||
|
||||
def __post_init__(self):
|
||||
if not self.button_data:
|
||||
self.button_data = ["I Understand"]
|
||||
|
||||
super().__post_init__()
|
||||
status_headline: str = _mft("Privacy Leak!") # The colored text under the alert icon
|
||||
button_data: list = field(default_factory=lambda: [ButtonOption("I Understand")])
|
||||
|
||||
|
||||
|
||||
@dataclass
|
||||
class DireWarningScreen(WarningScreen):
|
||||
status_headline: str = "Classified Info!" # The colored text under the alert icon
|
||||
status_headline: str = _mft("Classified Info!") # The colored text under the alert icon
|
||||
status_color: str = GUIConstants.DIRE_WARNING_COLOR
|
||||
|
||||
|
||||
@@ -981,12 +1031,12 @@ class DireWarningScreen(WarningScreen):
|
||||
@dataclass
|
||||
class ResetScreen(BaseTopNavScreen):
|
||||
def __post_init__(self):
|
||||
self.title = "Restarting"
|
||||
self.title = _("Restarting")
|
||||
self.show_back_button = False
|
||||
super().__post_init__()
|
||||
|
||||
self.components.append(TextArea(
|
||||
text="SeedSigner is restarting.\n\nAll in-memory data will be wiped.",
|
||||
text=_("SeedSigner is restarting.\n\nAll in-memory data will be wiped."),
|
||||
screen_y=self.top_nav.height,
|
||||
height=self.canvas_height - self.top_nav.height,
|
||||
))
|
||||
@@ -996,12 +1046,12 @@ class ResetScreen(BaseTopNavScreen):
|
||||
@dataclass
|
||||
class PowerOffScreen(BaseTopNavScreen):
|
||||
def __post_init__(self):
|
||||
self.title = "Powering Off"
|
||||
self.title = _("Powering Off")
|
||||
self.show_back_button = False
|
||||
super().__post_init__()
|
||||
|
||||
self.components.append(TextArea(
|
||||
text="Please wait about 30 seconds before disconnecting power.",
|
||||
text=_("Please wait about 30 seconds before disconnecting power."),
|
||||
screen_y=self.top_nav.height,
|
||||
height=self.canvas_height - self.top_nav.height,
|
||||
))
|
||||
@@ -1011,12 +1061,12 @@ class PowerOffScreen(BaseTopNavScreen):
|
||||
@dataclass
|
||||
class PowerOffNotRequiredScreen(BaseTopNavScreen):
|
||||
def __post_init__(self):
|
||||
self.title = "Just Unplug It"
|
||||
self.title = _("Just Unplug It")
|
||||
self.show_back_button = True
|
||||
super().__post_init__()
|
||||
|
||||
self.components.append(TextArea(
|
||||
text="It is safe to disconnect power at any time.",
|
||||
text=_("It is safe to disconnect power at any time."),
|
||||
screen_y=self.top_nav.height,
|
||||
height=self.canvas_height - self.top_nav.height,
|
||||
))
|
||||
@@ -1044,7 +1094,7 @@ class KeyboardScreen(BaseTopNavScreen):
|
||||
rows: int = None
|
||||
cols: int = None
|
||||
keyboard_font_name: str = GUIConstants.FIXED_WIDTH_EMPHASIS_FONT_NAME
|
||||
keyboard_font_size: int = GUIConstants.TOP_NAV_TITLE_FONT_SIZE + 2
|
||||
keyboard_font_size: int = None
|
||||
key_height: int = None
|
||||
keys_charset: str = None
|
||||
keys_to_values: dict = None
|
||||
@@ -1053,6 +1103,9 @@ class KeyboardScreen(BaseTopNavScreen):
|
||||
initial_value: str = ""
|
||||
|
||||
def __post_init__(self):
|
||||
if self.keyboard_font_size is None:
|
||||
self.keyboard_font_size = GUIConstants.get_top_nav_title_font_size() + 2
|
||||
|
||||
super().__post_init__()
|
||||
|
||||
if self.initial_value:
|
||||
@@ -1132,7 +1185,7 @@ class KeyboardScreen(BaseTopNavScreen):
|
||||
self.text_entry_display.render()
|
||||
|
||||
self.renderer.show_image()
|
||||
|
||||
|
||||
|
||||
def _run(self):
|
||||
self.cursor_position = len(self.user_input)
|
||||
@@ -1140,88 +1193,87 @@ class KeyboardScreen(BaseTopNavScreen):
|
||||
# Start the interactive update loop
|
||||
while True:
|
||||
input = self.hw_inputs.wait_for(
|
||||
HardwareButtonsConstants.KEYS__LEFT_RIGHT_UP_DOWN + [HardwareButtonsConstants.KEY_PRESS, HardwareButtonsConstants.KEY3],
|
||||
check_release=True,
|
||||
release_keys=[HardwareButtonsConstants.KEY_PRESS, HardwareButtonsConstants.KEY3]
|
||||
HardwareButtonsConstants.KEYS__LEFT_RIGHT_UP_DOWN + [HardwareButtonsConstants.KEY_PRESS, HardwareButtonsConstants.KEY3]
|
||||
)
|
||||
|
||||
# Check possible exit conditions
|
||||
if self.top_nav.is_selected and input == HardwareButtonsConstants.KEY_PRESS:
|
||||
return RET_CODE__BACK_BUTTON
|
||||
|
||||
elif self.show_save_button and input == HardwareButtonsConstants.KEY3:
|
||||
# Save!
|
||||
if len(self.user_input) == 0:
|
||||
# Don't try to submit zero input
|
||||
|
||||
with self.renderer.lock:
|
||||
# Check possible exit conditions
|
||||
if self.top_nav.is_selected and input == HardwareButtonsConstants.KEY_PRESS:
|
||||
return RET_CODE__BACK_BUTTON
|
||||
|
||||
elif self.show_save_button and input == HardwareButtonsConstants.KEY3:
|
||||
# Save!
|
||||
if len(self.user_input) == 0:
|
||||
# Don't try to submit zero input
|
||||
continue
|
||||
|
||||
# First show the save button reacting to the click
|
||||
self.save_button.is_selected = True
|
||||
self.save_button.render()
|
||||
self.renderer.show_image()
|
||||
|
||||
# Then return the input to the View
|
||||
return self.user_input.strip()
|
||||
|
||||
# Process normal input
|
||||
if input in [HardwareButtonsConstants.KEY_UP, HardwareButtonsConstants.KEY_DOWN] and self.top_nav.is_selected:
|
||||
# We're navigating off the previous button
|
||||
self.top_nav.is_selected = False
|
||||
self.top_nav.render_buttons()
|
||||
|
||||
# Override the actual input w/an ENTER signal for the Keyboard
|
||||
if input == HardwareButtonsConstants.KEY_DOWN:
|
||||
input = Keyboard.ENTER_TOP
|
||||
else:
|
||||
input = Keyboard.ENTER_BOTTOM
|
||||
elif input in [HardwareButtonsConstants.KEY_LEFT, HardwareButtonsConstants.KEY_RIGHT] and self.top_nav.is_selected:
|
||||
# ignore
|
||||
continue
|
||||
|
||||
# First show the save button reacting to the click
|
||||
self.save_button.is_selected = True
|
||||
self.save_button.render()
|
||||
self.renderer.show_image()
|
||||
ret_val = self.keyboard.update_from_input(input)
|
||||
|
||||
# Then return the input to the View
|
||||
return self.user_input.strip()
|
||||
|
||||
# Process normal input
|
||||
if input in [HardwareButtonsConstants.KEY_UP, HardwareButtonsConstants.KEY_DOWN] and self.top_nav.is_selected:
|
||||
# We're navigating off the previous button
|
||||
self.top_nav.is_selected = False
|
||||
self.top_nav.render_buttons()
|
||||
|
||||
# Override the actual input w/an ENTER signal for the Keyboard
|
||||
if input == HardwareButtonsConstants.KEY_DOWN:
|
||||
input = Keyboard.ENTER_TOP
|
||||
else:
|
||||
input = Keyboard.ENTER_BOTTOM
|
||||
elif input in [HardwareButtonsConstants.KEY_LEFT, HardwareButtonsConstants.KEY_RIGHT] and self.top_nav.is_selected:
|
||||
# ignore
|
||||
continue
|
||||
|
||||
ret_val = self.keyboard.update_from_input(input)
|
||||
|
||||
# Now process the result from the keyboard
|
||||
if ret_val in Keyboard.EXIT_DIRECTIONS:
|
||||
self.top_nav.is_selected = True
|
||||
self.top_nav.render_buttons()
|
||||
|
||||
elif ret_val in Keyboard.ADDITIONAL_KEYS and input == HardwareButtonsConstants.KEY_PRESS:
|
||||
if ret_val == Keyboard.KEY_BACKSPACE["code"]:
|
||||
if len(self.user_input) > 0:
|
||||
self.user_input = self.user_input[:-1]
|
||||
self.cursor_position -= 1
|
||||
|
||||
elif input == HardwareButtonsConstants.KEY_PRESS and ret_val not in Keyboard.ADDITIONAL_KEYS:
|
||||
# User has locked in the current letter
|
||||
if self.keys_to_values:
|
||||
# Map the Key display char to its output value (e.g. dice icon to digit)
|
||||
ret_val = self.keys_to_values[ret_val]
|
||||
self.user_input += ret_val
|
||||
self.cursor_position += 1
|
||||
|
||||
if self.cursor_position == self.return_after_n_chars:
|
||||
return self.user_input
|
||||
|
||||
# Render a new TextArea over the TopNav title bar
|
||||
if self.update_title():
|
||||
TextArea(
|
||||
text=self.title,
|
||||
font_name=GUIConstants.TOP_NAV_TITLE_FONT_NAME,
|
||||
font_size=GUIConstants.TOP_NAV_TITLE_FONT_SIZE,
|
||||
height=self.top_nav.height,
|
||||
).render()
|
||||
# Now process the result from the keyboard
|
||||
if ret_val in Keyboard.EXIT_DIRECTIONS:
|
||||
self.top_nav.is_selected = True
|
||||
self.top_nav.render_buttons()
|
||||
|
||||
elif input in HardwareButtonsConstants.KEYS__LEFT_RIGHT_UP_DOWN:
|
||||
# Live joystick movement; haven't locked this new letter in yet.
|
||||
# Leave current spot blank for now. Only update the active keyboard keys
|
||||
# when a selection has been locked in (KEY_PRESS) or removed ("del").
|
||||
pass
|
||||
|
||||
# Render the text entry display and cursor block
|
||||
self.text_entry_display.render(self.user_input)
|
||||
|
||||
self.renderer.show_image()
|
||||
|
||||
elif ret_val in Keyboard.ADDITIONAL_KEYS and input == HardwareButtonsConstants.KEY_PRESS:
|
||||
if ret_val == Keyboard.KEY_BACKSPACE["code"]:
|
||||
if len(self.user_input) > 0:
|
||||
self.user_input = self.user_input[:-1]
|
||||
self.cursor_position -= 1
|
||||
|
||||
elif input == HardwareButtonsConstants.KEY_PRESS and ret_val not in Keyboard.ADDITIONAL_KEYS:
|
||||
# User has locked in the current letter
|
||||
if self.keys_to_values:
|
||||
# Map the Key display char to its output value (e.g. dice icon to digit)
|
||||
ret_val = self.keys_to_values[ret_val]
|
||||
self.user_input += ret_val
|
||||
self.cursor_position += 1
|
||||
|
||||
if self.cursor_position == self.return_after_n_chars:
|
||||
return self.user_input
|
||||
|
||||
# Render a new TextArea over the TopNav title bar
|
||||
if self.update_title():
|
||||
TextArea(
|
||||
text=self.title,
|
||||
font_name=GUIConstants.get_top_nav_title_font_name(),
|
||||
font_size=GUIConstants.get_top_nav_title_font_size(),
|
||||
height=self.top_nav.height,
|
||||
).render()
|
||||
self.top_nav.render_buttons()
|
||||
|
||||
elif input in HardwareButtonsConstants.KEYS__LEFT_RIGHT_UP_DOWN:
|
||||
# Live joystick movement; haven't locked this new letter in yet.
|
||||
# Leave current spot blank for now. Only update the active keyboard keys
|
||||
# when a selection has been locked in (KEY_PRESS) or removed ("del").
|
||||
pass
|
||||
|
||||
# Render the text entry display and cursor block
|
||||
self.text_entry_display.render(self.user_input)
|
||||
|
||||
self.renderer.show_image()
|
||||
|
||||
|
||||
def update_title(self) -> bool:
|
||||
@@ -1229,7 +1281,7 @@ class KeyboardScreen(BaseTopNavScreen):
|
||||
Optionally update the self.title after each completed key input.
|
||||
|
||||
e.g. to increment the dice roll count:
|
||||
self.title = f"Roll {self.cursor_position + 1}"
|
||||
self.title = _("Roll {}".format(self.cursor_position + 1))
|
||||
"""
|
||||
return False
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,17 +1,20 @@
|
||||
import time
|
||||
|
||||
from dataclasses import dataclass
|
||||
from gettext import gettext as _
|
||||
from PIL.ImageOps import autocontrast
|
||||
from typing import List
|
||||
|
||||
from seedsigner.helpers.l10n import mark_for_translation as _mft
|
||||
from seedsigner.gui.components import Button, CheckboxButton, CheckedSelectionButton, FontAwesomeIconConstants, Fonts, GUIConstants, Icon, IconButton, IconTextLine, TextArea
|
||||
from seedsigner.gui.screens.scan_screens import ScanScreen
|
||||
|
||||
from seedsigner.gui.screens.screen import BaseScreen, BaseTopNavScreen, ButtonListScreen
|
||||
from seedsigner.gui.screens.screen import BaseScreen, BaseTopNavScreen, ButtonListScreen, ButtonOption
|
||||
from seedsigner.hardware.buttons import HardwareButtonsConstants
|
||||
from seedsigner.hardware.camera import Camera
|
||||
from seedsigner.models.settings import SettingsConstants
|
||||
|
||||
|
||||
|
||||
@dataclass
|
||||
class SettingsEntryUpdateSelectionScreen(ButtonListScreen):
|
||||
display_name: str = None
|
||||
@@ -21,7 +24,7 @@ class SettingsEntryUpdateSelectionScreen(ButtonListScreen):
|
||||
selected_button: int = 0
|
||||
|
||||
def __post_init__(self):
|
||||
self.title = "Settings"
|
||||
self.title = _("Settings")
|
||||
self.is_bottom_list = True
|
||||
self.use_checked_selection_buttons = True
|
||||
if self.settings_entry_type == SettingsConstants.TYPE__MULTISELECT:
|
||||
@@ -31,20 +34,21 @@ class SettingsEntryUpdateSelectionScreen(ButtonListScreen):
|
||||
super().__post_init__()
|
||||
|
||||
self.components.append(TextArea(
|
||||
text=self.display_name,
|
||||
text=_(self.display_name),
|
||||
font_size=GUIConstants.BODY_FONT_MAX_SIZE,
|
||||
is_text_centered=True,
|
||||
auto_line_break=False,
|
||||
auto_line_break=True,
|
||||
screen_y=self.top_nav.height + GUIConstants.COMPONENT_PADDING
|
||||
))
|
||||
|
||||
if self.help_text:
|
||||
prev_component_bottom = self.components[-1].screen_y + self.components[-1].height
|
||||
self.components.append(TextArea(
|
||||
text=self.help_text,
|
||||
text=_(self.help_text),
|
||||
font_color=GUIConstants.LABEL_FONT_COLOR,
|
||||
is_text_centered=True,
|
||||
screen_y=prev_component_bottom + GUIConstants.COMPONENT_PADDING,
|
||||
auto_line_break=True,
|
||||
))
|
||||
|
||||
|
||||
@@ -52,7 +56,8 @@ class SettingsEntryUpdateSelectionScreen(ButtonListScreen):
|
||||
@dataclass
|
||||
class IOTestScreen(BaseTopNavScreen):
|
||||
def __post_init__(self):
|
||||
self.title = "I/O Test"
|
||||
# TRANSLATOR_NOTE: Short for "Input/Output"; screen to make sure the buttons and camera are working properly
|
||||
self.title = _("I/O Test")
|
||||
self.show_back_button = False
|
||||
self.resolution = (96, 96)
|
||||
self.framerate = 10
|
||||
@@ -122,8 +127,8 @@ class IOTestScreen(BaseTopNavScreen):
|
||||
self.components.append(self.joystick_right_button)
|
||||
|
||||
# Hardware keys UI
|
||||
font = Fonts.get_font(GUIConstants.BUTTON_FONT_NAME, GUIConstants.BUTTON_FONT_SIZE)
|
||||
(left, top, text_width, bottom) = font.getbbox(text="Clear", anchor="ls")
|
||||
font = Fonts.get_font(GUIConstants.get_button_font_name(), GUIConstants.get_button_font_size())
|
||||
(left, top, text_width, bottom) = font.getbbox(text=_("Clear"), anchor="ls")
|
||||
icon = Icon(
|
||||
icon_name=FontAwesomeIconConstants.CAMERA,
|
||||
icon_size=GUIConstants.ICON_INLINE_FONT_SIZE,
|
||||
@@ -133,12 +138,14 @@ class IOTestScreen(BaseTopNavScreen):
|
||||
key2_y = int(self.canvas_height/2) - int(key_button_height/2)
|
||||
|
||||
self.key2_button = Button(
|
||||
text="Clear", # Initialize with text to set vertical centering
|
||||
# TRANSLATOR_NOTE: Blank the screen
|
||||
text=_("Clear"), # Initialize with text to set vertical centering
|
||||
width=key_button_width,
|
||||
height=key_button_height,
|
||||
screen_x=self.canvas_width - key_button_width + GUIConstants.EDGE_PADDING,
|
||||
screen_y=key2_y,
|
||||
outline_color=GUIConstants.ACCENT_COLOR,
|
||||
is_scrollable_text=False, # Text has to dynamically update, can't use scrollable Button
|
||||
)
|
||||
self.key2_button.text = " " # but default state is empty
|
||||
self.components.append(self.key2_button)
|
||||
@@ -154,12 +161,13 @@ class IOTestScreen(BaseTopNavScreen):
|
||||
self.components.append(self.key1_button)
|
||||
|
||||
self.key3_button = Button(
|
||||
text="Exit",
|
||||
text=_("Exit"),
|
||||
width=key_button_width,
|
||||
height=key_button_height,
|
||||
screen_x=self.canvas_width - key_button_width + GUIConstants.EDGE_PADDING,
|
||||
screen_y=key2_y + 3*GUIConstants.COMPONENT_PADDING + key_button_height,
|
||||
outline_color=GUIConstants.ACCENT_COLOR,
|
||||
is_scrollable_text=False, # No help for l10n, but currently ScrollableTextLine interferes with the small button's left edge. (TODO:)
|
||||
)
|
||||
self.components.append(self.key3_button)
|
||||
|
||||
@@ -168,31 +176,35 @@ class IOTestScreen(BaseTopNavScreen):
|
||||
cur_selected_button = self.key1_button
|
||||
msg_height = GUIConstants.ICON_LARGE_BUTTON_SIZE + 2*GUIConstants.COMPONENT_PADDING
|
||||
camera_message = TextArea(
|
||||
text="Capturing image...",
|
||||
font_size=GUIConstants.TOP_NAV_TITLE_FONT_SIZE,
|
||||
text=_("Capturing image..."),
|
||||
font_size=GUIConstants.get_top_nav_title_font_size(),
|
||||
is_text_centered=True,
|
||||
height=msg_height,
|
||||
screen_y=int((self.canvas_height - msg_height)/ 2),
|
||||
)
|
||||
while True:
|
||||
input = self.hw_inputs.wait_for(keys=HardwareButtonsConstants.ALL_KEYS, check_release=False)
|
||||
input = self.hw_inputs.wait_for(keys=HardwareButtonsConstants.ALL_KEYS)
|
||||
|
||||
if input == HardwareButtonsConstants.KEY1:
|
||||
# Note that there are three distinct screen updates that happen at
|
||||
# different times, therefore we claim the `Renderer.lock` three separate
|
||||
# times.
|
||||
cur_selected_button = self.key1_button
|
||||
|
||||
with self.renderer.lock:
|
||||
cur_selected_button.is_selected = True
|
||||
cur_selected_button.render()
|
||||
camera_message.render()
|
||||
# Render edges around message box
|
||||
self.image_draw.rectangle(
|
||||
(
|
||||
-1, int((self.canvas_height - msg_height)/ 2) - 1,
|
||||
self.canvas_width + 1, int((self.canvas_height + msg_height)/ 2) + 1
|
||||
),
|
||||
fill="black",
|
||||
outline=GUIConstants.ACCENT_COLOR,
|
||||
width=1,
|
||||
)
|
||||
cur_selected_button.is_selected = True
|
||||
cur_selected_button.render()
|
||||
camera_message.render()
|
||||
self.renderer.show_image()
|
||||
|
||||
# Snap a pic, render it as the background, re-render all onscreen elements
|
||||
@@ -214,7 +226,7 @@ class IOTestScreen(BaseTopNavScreen):
|
||||
)
|
||||
with self.renderer.lock:
|
||||
self.canvas.paste(display_version, (0, self.top_nav.height))
|
||||
self.key2_button.text = "Clear"
|
||||
self.key2_button.text = _("Clear")
|
||||
for component in self.components:
|
||||
component.render()
|
||||
self.renderer.show_image()
|
||||
@@ -281,17 +293,19 @@ class IOTestScreen(BaseTopNavScreen):
|
||||
@dataclass
|
||||
class DonateScreen(BaseTopNavScreen):
|
||||
def __post_init__(self):
|
||||
self.title = "Donate"
|
||||
self.title = _("Donate")
|
||||
super().__post_init__()
|
||||
|
||||
self.components.append(TextArea(
|
||||
text="SeedSigner is 100% free & open source, funded solely by the Bitcoin community.\n\nDonate onchain or LN at:",
|
||||
# TRANSLATOR_NOTE: If your language uses the percent sign ("%"), your translation must also use two percent signs ("%%") due to python formatting oddities. "100%%" will be rendered as "100%".
|
||||
text=_("SeedSigner is 100%% free & open source, funded solely by the Bitcoin community.\n\nDonate onchain or LN at:").replace("%%", "%"),
|
||||
screen_y=self.top_nav.height + 3*GUIConstants.COMPONENT_PADDING,
|
||||
))
|
||||
|
||||
self.components.append(TextArea(
|
||||
text="seedsigner.com",
|
||||
font_size=GUIConstants.TOP_NAV_TITLE_FONT_SIZE + 8,
|
||||
font_name=GUIConstants.get_body_font_name(),
|
||||
font_size=28,
|
||||
font_color=GUIConstants.ACCENT_COLOR,
|
||||
supersampling_factor=1,
|
||||
screen_y=self.components[-1].screen_y + self.components[-1].height + GUIConstants.COMPONENT_PADDING
|
||||
@@ -302,20 +316,20 @@ class DonateScreen(BaseTopNavScreen):
|
||||
@dataclass
|
||||
class SettingsQRConfirmationScreen(ButtonListScreen):
|
||||
config_name: str = None
|
||||
title: str = "Settings QR"
|
||||
status_message: str = "Settings updated..."
|
||||
title: str = _mft("Settings QR")
|
||||
status_message: str = _mft("Settings updated...")
|
||||
is_bottom_list: bool = True
|
||||
|
||||
def __post_init__(self):
|
||||
# Customize defaults
|
||||
self.button_data = ["Home"]
|
||||
self.button_data = [ButtonOption("Home")]
|
||||
self.show_back_button = False
|
||||
super().__post_init__()
|
||||
|
||||
start_y = self.top_nav.height + 20
|
||||
if self.config_name:
|
||||
self.config_name_textarea = TextArea(
|
||||
text=f'"{self.config_name}"',
|
||||
text=f'"{self.config_name}"', # User-supplied string (from SettingsQR); don't wrap to translate
|
||||
is_text_centered=True,
|
||||
auto_line_break=True,
|
||||
screen_y=start_y
|
||||
@@ -324,7 +338,7 @@ class SettingsQRConfirmationScreen(ButtonListScreen):
|
||||
start_y = self.config_name_textarea.screen_y + 50
|
||||
|
||||
self.components.append(TextArea(
|
||||
text=self.status_message,
|
||||
text=_(self.status_message),
|
||||
is_text_centered=True,
|
||||
auto_line_break=True,
|
||||
screen_y=start_y
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import time
|
||||
|
||||
from dataclasses import dataclass
|
||||
from gettext import gettext as _
|
||||
from typing import Any
|
||||
from PIL.Image import Image
|
||||
from seedsigner.hardware.camera import Camera
|
||||
@@ -15,10 +16,6 @@ from seedsigner.models.settings_definition import SettingsConstants, SettingsDef
|
||||
@dataclass
|
||||
class ToolsImageEntropyLivePreviewScreen(BaseScreen):
|
||||
def __post_init__(self):
|
||||
# Customize defaults
|
||||
self.title = "Initializing Camera..."
|
||||
|
||||
# Initialize the base class
|
||||
super().__post_init__()
|
||||
|
||||
self.camera = Camera.get_instance()
|
||||
@@ -29,10 +26,9 @@ class ToolsImageEntropyLivePreviewScreen(BaseScreen):
|
||||
# save preview image frames to use as additional entropy below
|
||||
preview_images = []
|
||||
max_entropy_frames = 50
|
||||
instructions_font = Fonts.get_font(GUIConstants.BODY_FONT_NAME, GUIConstants.BUTTON_FONT_SIZE)
|
||||
instructions_font = Fonts.get_font(GUIConstants.get_body_font_name(), GUIConstants.get_button_font_size())
|
||||
|
||||
while True:
|
||||
# Check for BACK button press
|
||||
if self.hw_inputs.check_for_low(HardwareButtonsConstants.KEY_LEFT):
|
||||
# Have to manually update last input time since we're not in a wait_for loop
|
||||
self.hw_inputs.update_last_input_time()
|
||||
@@ -53,6 +49,27 @@ class ToolsImageEntropyLivePreviewScreen(BaseScreen):
|
||||
self.hw_inputs.update_last_input_time()
|
||||
self.camera.stop_video_stream_mode()
|
||||
|
||||
with self.renderer.lock:
|
||||
self.renderer.canvas.paste(frame)
|
||||
|
||||
self.renderer.draw.text(
|
||||
xy=(
|
||||
int(self.renderer.canvas_width/2),
|
||||
self.renderer.canvas_height - GUIConstants.EDGE_PADDING
|
||||
),
|
||||
text=_("Capturing image..."),
|
||||
fill=GUIConstants.ACCENT_COLOR,
|
||||
font=instructions_font,
|
||||
stroke_width=4,
|
||||
stroke_fill=GUIConstants.BACKGROUND_COLOR,
|
||||
anchor="ms"
|
||||
)
|
||||
self.renderer.show_image()
|
||||
|
||||
return preview_images
|
||||
|
||||
# If we're still here, it's just another preview frame loop
|
||||
with self.renderer.lock:
|
||||
self.renderer.canvas.paste(frame)
|
||||
|
||||
self.renderer.draw.text(
|
||||
@@ -60,8 +77,8 @@ class ToolsImageEntropyLivePreviewScreen(BaseScreen):
|
||||
int(self.renderer.canvas_width/2),
|
||||
self.renderer.canvas_height - GUIConstants.EDGE_PADDING
|
||||
),
|
||||
text="Capturing image...",
|
||||
fill=GUIConstants.ACCENT_COLOR,
|
||||
text="< " + _("back") + " | " + _("click joystick"), # TODO: Render with UI elements instead of text
|
||||
fill=GUIConstants.BODY_FONT_COLOR,
|
||||
font=instructions_font,
|
||||
stroke_width=4,
|
||||
stroke_fill=GUIConstants.BACKGROUND_COLOR,
|
||||
@@ -69,25 +86,6 @@ class ToolsImageEntropyLivePreviewScreen(BaseScreen):
|
||||
)
|
||||
self.renderer.show_image()
|
||||
|
||||
return preview_images
|
||||
|
||||
# If we're still here, it's just another preview frame loop
|
||||
self.renderer.canvas.paste(frame)
|
||||
|
||||
self.renderer.draw.text(
|
||||
xy=(
|
||||
int(self.renderer.canvas_width/2),
|
||||
self.renderer.canvas_height - GUIConstants.EDGE_PADDING
|
||||
),
|
||||
text="< back | click joystick",
|
||||
fill=GUIConstants.BODY_FONT_COLOR,
|
||||
font=instructions_font,
|
||||
stroke_width=4,
|
||||
stroke_fill=GUIConstants.BACKGROUND_COLOR,
|
||||
anchor="ms"
|
||||
)
|
||||
self.renderer.show_image()
|
||||
|
||||
if len(preview_images) == max_entropy_frames:
|
||||
# Keep a moving window of the last n preview frames; pop the oldest
|
||||
# before we add the currest frame.
|
||||
@@ -101,22 +99,29 @@ class ToolsImageEntropyFinalImageScreen(BaseScreen):
|
||||
final_image: Image = None
|
||||
|
||||
def _run(self):
|
||||
instructions_font = Fonts.get_font(GUIConstants.BODY_FONT_NAME, GUIConstants.BUTTON_FONT_SIZE)
|
||||
instructions_font = Fonts.get_font(GUIConstants.get_body_font_name(), GUIConstants.get_button_font_size())
|
||||
|
||||
self.renderer.canvas.paste(self.final_image)
|
||||
self.renderer.draw.text(
|
||||
xy=(
|
||||
int(self.renderer.canvas_width/2),
|
||||
self.renderer.canvas_height - GUIConstants.EDGE_PADDING
|
||||
),
|
||||
text=" < reshoot | accept > ",
|
||||
fill=GUIConstants.BODY_FONT_COLOR,
|
||||
font=instructions_font,
|
||||
stroke_width=4,
|
||||
stroke_fill=GUIConstants.BACKGROUND_COLOR,
|
||||
anchor="ms"
|
||||
)
|
||||
self.renderer.show_image()
|
||||
with self.renderer.lock:
|
||||
self.renderer.canvas.paste(self.final_image)
|
||||
|
||||
# TRANSLATOR_NOTE: A prompt to the user to either accept or reshoot the image
|
||||
reshoot = _("reshoot")
|
||||
|
||||
# TRANSLATOR_NOTE: A prompt to the user to either accept or reshoot the image
|
||||
accept = _("accept")
|
||||
self.renderer.draw.text(
|
||||
xy=(
|
||||
int(self.renderer.canvas_width/2),
|
||||
self.renderer.canvas_height - GUIConstants.EDGE_PADDING
|
||||
),
|
||||
text=" < " + reshoot + " | " + accept + " > ",
|
||||
fill=GUIConstants.BODY_FONT_COLOR,
|
||||
font=instructions_font,
|
||||
stroke_width=4,
|
||||
stroke_fill=GUIConstants.BACKGROUND_COLOR,
|
||||
anchor="ms"
|
||||
)
|
||||
self.renderer.show_image()
|
||||
|
||||
input = self.hw_inputs.wait_for([HardwareButtonsConstants.KEY_LEFT, HardwareButtonsConstants.KEY_RIGHT])
|
||||
if input == HardwareButtonsConstants.KEY_LEFT:
|
||||
@@ -126,9 +131,10 @@ class ToolsImageEntropyFinalImageScreen(BaseScreen):
|
||||
|
||||
@dataclass
|
||||
class ToolsDiceEntropyEntryScreen(KeyboardScreen):
|
||||
|
||||
def __post_init__(self):
|
||||
# Override values set by the parent class
|
||||
self.title = f"Dice Roll 1/{self.return_after_n_chars}"
|
||||
# TRANSLATOR_NOTE: current roll number vs total rolls (e.g. roll 7 of 50)
|
||||
self.title = _("Dice Roll {}/{}").format(1, self.return_after_n_chars)
|
||||
|
||||
# Specify the keys in the keyboard
|
||||
self.rows = 3
|
||||
@@ -159,7 +165,7 @@ class ToolsDiceEntropyEntryScreen(KeyboardScreen):
|
||||
|
||||
|
||||
def update_title(self) -> bool:
|
||||
self.title = f"Dice Roll {self.cursor_position + 1}/{self.return_after_n_chars}"
|
||||
self.title = _("Dice Roll {}/{}").format(self.cursor_position + 1, self.return_after_n_chars)
|
||||
return True
|
||||
|
||||
|
||||
@@ -170,13 +176,15 @@ class ToolsCalcFinalWordFinalizePromptScreen(ButtonListScreen):
|
||||
num_entropy_bits: int = None
|
||||
|
||||
def __post_init__(self):
|
||||
self.title = "Build Final Word"
|
||||
# TRANSLATOR_NOTE: Build the last word in a 12 or 24 word BIP-39 mnemonic seed phrase.
|
||||
self.title = _("Build Final Word")
|
||||
self.is_bottom_list = True
|
||||
self.is_button_text_centered = True
|
||||
super().__post_init__()
|
||||
|
||||
self.components.append(TextArea(
|
||||
text=f"The {self.mnemonic_length}th word is built from {self.num_entropy_bits} more entropy bits plus auto-calculated checksum.",
|
||||
# TRANSLATOR_NOTE: Final word calc. `mnemonic_length` = 12 or 24. `num_bits` = 7 or 3 (bits of entropy in final word).
|
||||
text=_("The {mnemonic_length}th word is built from {num_bits} more entropy bits plus auto-calculated checksum.").format(mnemonic_length=self.mnemonic_length, num_bits=self.num_entropy_bits),
|
||||
screen_y=self.top_nav.height + int(GUIConstants.COMPONENT_PADDING/2),
|
||||
))
|
||||
|
||||
@@ -186,29 +194,33 @@ class ToolsCalcFinalWordFinalizePromptScreen(ButtonListScreen):
|
||||
class ToolsCoinFlipEntryScreen(KeyboardScreen):
|
||||
def __post_init__(self):
|
||||
# Override values set by the parent class
|
||||
self.title = f"Coin Flip 1/{self.return_after_n_chars}"
|
||||
# TRANSLATOR_NOTE: current coin-flip number vs total flips (e.g. flip 3 of 4)
|
||||
self.title = _("Coin Flip {}/{}").format(1, self.return_after_n_chars)
|
||||
|
||||
# Specify the keys in the keyboard
|
||||
self.rows = 1
|
||||
self.cols = 4
|
||||
self.key_height = GUIConstants.TOP_NAV_TITLE_FONT_SIZE + 2 + 2*GUIConstants.EDGE_PADDING
|
||||
self.key_height = GUIConstants.get_top_nav_title_font_size() + 2 + 2*GUIConstants.EDGE_PADDING
|
||||
self.keys_charset = "10"
|
||||
|
||||
# Now initialize the parent class
|
||||
super().__post_init__()
|
||||
|
||||
self.components.append(TextArea(
|
||||
text="Heads = 1",
|
||||
# TRANSLATOR_NOTE: How we call the "front" side result during a coin toss.
|
||||
text=_("Heads = 1"),
|
||||
screen_y = self.keyboard.rect[3] + 4*GUIConstants.COMPONENT_PADDING,
|
||||
))
|
||||
self.components.append(TextArea(
|
||||
text="Tails = 0",
|
||||
# TRANSLATOR_NOTE: How we call the "back" side result during a coin toss.
|
||||
text=_("Tails = 0"),
|
||||
screen_y = self.components[-1].screen_y + self.components[-1].height + GUIConstants.COMPONENT_PADDING,
|
||||
))
|
||||
|
||||
|
||||
def update_title(self) -> bool:
|
||||
self.title = f"Coin Flip {self.cursor_position + 1}/{self.return_after_n_chars}"
|
||||
# l10n_note already done.
|
||||
self.title = _("Coin Flip {}/{}").format(self.cursor_position + 1, self.return_after_n_chars)
|
||||
return True
|
||||
|
||||
|
||||
@@ -225,7 +237,7 @@ class ToolsCalcFinalWordScreen(ButtonListScreen):
|
||||
super().__post_init__()
|
||||
|
||||
# First what's the total bit display width and where do the checksum bits start?
|
||||
bit_font_size = GUIConstants.BUTTON_FONT_SIZE + 2
|
||||
bit_font_size = GUIConstants.get_button_font_size() + 2
|
||||
font = Fonts.get_font(GUIConstants.FIXED_WIDTH_EMPHASIS_FONT_NAME, bit_font_size)
|
||||
(left, top, bit_display_width, bit_font_height) = font.getbbox("0" * 11, anchor="lt")
|
||||
(left, top, checksum_x, bottom) = font.getbbox("0" * (11 - len(self.checksum_bits)), anchor="lt")
|
||||
@@ -249,8 +261,10 @@ class ToolsCalcFinalWordScreen(ButtonListScreen):
|
||||
# significant n bits always rendered in same column)
|
||||
discard_selected_bits = "_" * (len(self.checksum_bits))
|
||||
|
||||
# TRANSLATOR_NOTE: The additional entropy the user supplied (e.g. coin flips)
|
||||
your_input = _('Your input: "{}"').format(selection_text)
|
||||
self.components.append(TextArea(
|
||||
text=f"""Your input: \"{selection_text}\"""",
|
||||
text=your_input,
|
||||
screen_y=self.top_nav.height + GUIConstants.COMPONENT_PADDING - 2, # Nudge to last line doesn't get too close to "Next" button
|
||||
height_ignores_below_baseline=True, # Keep the next line (bits display) snugged up, regardless of text rendering below the baseline
|
||||
))
|
||||
@@ -285,7 +299,8 @@ class ToolsCalcFinalWordScreen(ButtonListScreen):
|
||||
|
||||
# Show the checksum..
|
||||
self.components.append(TextArea(
|
||||
text="Checksum",
|
||||
# TRANSLATOR_NOTE: A function of "x" to be used for detecting errors in "x"
|
||||
text=_("Checksum"),
|
||||
edge_padding=0,
|
||||
screen_y=first_bits_line.screen_y + first_bits_line.height + 2*GUIConstants.COMPONENT_PADDING,
|
||||
))
|
||||
@@ -321,7 +336,8 @@ class ToolsCalcFinalWordScreen(ButtonListScreen):
|
||||
|
||||
# And now the *actual* final word after merging the bit data
|
||||
self.components.append(TextArea(
|
||||
text=f"""Final Word: \"{self.actual_final_word}\"""",
|
||||
# TRANSLATOR_NOTE: labeled presentation of the last word in a BIP-39 mnemonic seed phrase.
|
||||
text=_('Final Word: "{}"').format(self.actual_final_word),
|
||||
screen_y=self.components[-1].screen_y + self.components[-1].height + 2*GUIConstants.COMPONENT_PADDING,
|
||||
height_ignores_below_baseline=True, # Keep the next line (bits display) snugged up, regardless of text rendering below the baseline
|
||||
))
|
||||
@@ -361,15 +377,20 @@ class ToolsCalcFinalWordDoneScreen(ButtonListScreen):
|
||||
fingerprint: str = None
|
||||
|
||||
def __post_init__(self):
|
||||
# Customize defaults
|
||||
self.title = f"{self.mnemonic_word_length}th Word"
|
||||
# Manually specify 12 vs 24 case for easier ordinal translation
|
||||
if self.mnemonic_word_length == 12:
|
||||
# TRANSLATOR_NOTE: a label for the last word of a 12-word BIP-39 mnemonic seed phrase
|
||||
self.title = _("12th Word")
|
||||
else:
|
||||
# TRANSLATOR_NOTE: a label for the last word of a 24-word BIP-39 mnemonic seed phrase
|
||||
self.title = _("24th Word")
|
||||
self.is_bottom_list = True
|
||||
|
||||
super().__post_init__()
|
||||
|
||||
self.components.append(TextArea(
|
||||
text=f"""\"{self.final_word}\"""",
|
||||
font_size=GUIConstants.TOP_NAV_TITLE_FONT_SIZE + 6,
|
||||
font_size=26,
|
||||
is_text_centered=True,
|
||||
screen_y=self.top_nav.height + GUIConstants.COMPONENT_PADDING,
|
||||
))
|
||||
@@ -377,7 +398,8 @@ class ToolsCalcFinalWordDoneScreen(ButtonListScreen):
|
||||
self.components.append(IconTextLine(
|
||||
icon_name=SeedSignerIconConstants.FINGERPRINT,
|
||||
icon_color=GUIConstants.INFO_COLOR,
|
||||
label_text="fingerprint",
|
||||
# TRANSLATOR_NOTE: a label for the shortened Key-id of a BIP-32 master HD wallet
|
||||
label_text=_("fingerprint"),
|
||||
value_text=self.fingerprint,
|
||||
is_text_centered=True,
|
||||
screen_y=self.components[-1].screen_y + self.components[-1].height + 3*GUIConstants.COMPONENT_PADDING,
|
||||
@@ -393,7 +415,8 @@ class ToolsAddressExplorerAddressTypeScreen(ButtonListScreen):
|
||||
custom_derivation_path: str = None
|
||||
|
||||
def __post_init__(self):
|
||||
self.title = "Address Explorer"
|
||||
# TRANSLATOR_NOTE: a label for the tool to explore public addresses for this seed.
|
||||
self.title = _("Address Explorer")
|
||||
self.is_bottom_list = True
|
||||
super().__post_init__()
|
||||
|
||||
@@ -401,7 +424,8 @@ class ToolsAddressExplorerAddressTypeScreen(ButtonListScreen):
|
||||
self.components.append(IconTextLine(
|
||||
icon_name=SeedSignerIconConstants.FINGERPRINT,
|
||||
icon_color=GUIConstants.INFO_COLOR,
|
||||
label_text="Fingerprint",
|
||||
# TRANSLATOR_NOTE: a label for the shortened Key-id of a BIP-32 master HD wallet
|
||||
label_text=_("Fingerprint"),
|
||||
value_text=self.fingerprint,
|
||||
screen_x=GUIConstants.EDGE_PADDING,
|
||||
screen_y=self.top_nav.height + GUIConstants.COMPONENT_PADDING,
|
||||
@@ -410,7 +434,8 @@ class ToolsAddressExplorerAddressTypeScreen(ButtonListScreen):
|
||||
if self.script_type != SettingsConstants.CUSTOM_DERIVATION:
|
||||
self.components.append(IconTextLine(
|
||||
icon_name=SeedSignerIconConstants.DERIVATION,
|
||||
label_text="Derivation",
|
||||
# TRANSLATOR_NOTE: a label for the derivation-path into a BIP-32 HD wallet
|
||||
label_text=_("Derivation"),
|
||||
value_text=SettingsDefinition.get_settings_entry(attr_name=SettingsConstants.SETTING__SCRIPT_TYPES).get_selection_option_display_name_by_value(value=self.script_type),
|
||||
screen_x=GUIConstants.EDGE_PADDING,
|
||||
screen_y=self.components[-1].screen_y + self.components[-1].height + 2*GUIConstants.COMPONENT_PADDING,
|
||||
@@ -418,7 +443,8 @@ class ToolsAddressExplorerAddressTypeScreen(ButtonListScreen):
|
||||
else:
|
||||
self.components.append(IconTextLine(
|
||||
icon_name=SeedSignerIconConstants.DERIVATION,
|
||||
label_text="Derivation",
|
||||
# l10n_note already exists.
|
||||
label_text=_("Derivation"),
|
||||
value_text=self.custom_derivation_path,
|
||||
screen_x=GUIConstants.EDGE_PADDING,
|
||||
screen_y=self.components[-1].screen_y + self.components[-1].height + 2*GUIConstants.COMPONENT_PADDING,
|
||||
@@ -426,8 +452,9 @@ class ToolsAddressExplorerAddressTypeScreen(ButtonListScreen):
|
||||
|
||||
else:
|
||||
self.components.append(IconTextLine(
|
||||
label_text="Wallet descriptor",
|
||||
value_text=self.wallet_descriptor_display_name,
|
||||
# TRANSLATOR_NOTE: a label for a BIP-380-ish Output Descriptor
|
||||
label_text=_("Wallet descriptor"),
|
||||
value_text=self.wallet_descriptor_display_name, # TODO: English text from embit (e.g. "1 / 2 multisig"); make l10 friendly
|
||||
is_text_centered=True,
|
||||
screen_x=GUIConstants.EDGE_PADDING,
|
||||
screen_y=self.top_nav.height + GUIConstants.COMPONENT_PADDING,
|
||||
|
||||
+29
-16
@@ -1,6 +1,8 @@
|
||||
import logging
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from gettext import gettext as _
|
||||
|
||||
from seedsigner.gui.components import BaseComponent, GUIConstants, Icon, SeedSignerIconConstants, TextArea
|
||||
from seedsigner.models.threads import BaseThread
|
||||
|
||||
@@ -200,21 +202,26 @@ class BaseToastOverlayManagerThread(BaseThread):
|
||||
|
||||
|
||||
class RemoveSDCardToastManagerThread(BaseToastOverlayManagerThread):
|
||||
def __init__(self, activation_delay=3):
|
||||
# Note: activation_delay is configurable so the screenshot generator can get the
|
||||
# toast to immediately render.
|
||||
def __init__(self, activation_delay: int = 3, duration: int = 1e6):
|
||||
"""
|
||||
* activation_delay: configurable so the screenshot generator can get the
|
||||
toast to immediately render.
|
||||
* duration: default value is essentially forever. Overrideable for the
|
||||
screenshot generator.
|
||||
"""
|
||||
super().__init__(
|
||||
activation_delay=activation_delay, # seconds
|
||||
duration=1e6, # seconds ("forever")
|
||||
activation_delay=activation_delay,
|
||||
duration=duration,
|
||||
)
|
||||
|
||||
|
||||
def instantiate_toast(self) -> ToastOverlay:
|
||||
body_font_size = GUIConstants.get_body_font_size()
|
||||
return ToastOverlay(
|
||||
icon_name=SeedSignerIconConstants.MICROSD,
|
||||
label_text="You can remove\nthe SD card now",
|
||||
font_size=GUIConstants.BODY_FONT_SIZE,
|
||||
height=GUIConstants.BODY_FONT_SIZE * 2 + GUIConstants.BODY_LINE_SPACING + GUIConstants.EDGE_PADDING,
|
||||
label_text=_("You can remove\nthe SD card now"),
|
||||
font_size=body_font_size,
|
||||
height=body_font_size * 2 + GUIConstants.BODY_LINE_SPACING + GUIConstants.EDGE_PADDING,
|
||||
)
|
||||
|
||||
|
||||
@@ -232,7 +239,7 @@ class SDCardStateChangeToastManagerThread(BaseToastOverlayManagerThread):
|
||||
from seedsigner.hardware.microsd import MicroSD
|
||||
if action not in [MicroSD.ACTION__INSERTED, MicroSD.ACTION__REMOVED]:
|
||||
raise Exception(f"Invalid MicroSD action: {action}")
|
||||
self.message = "SD card removed" if action == MicroSD.ACTION__REMOVED else "SD card inserted"
|
||||
self.message = _("SD card removed") if action == MicroSD.ACTION__REMOVED else _("SD card inserted")
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@@ -262,69 +269,75 @@ class DefaultToast(BaseToastOverlayManagerThread):
|
||||
|
||||
|
||||
def instantiate_toast(self) -> ToastOverlay:
|
||||
body_font_size = GUIConstants.get_body_font_size()
|
||||
return ToastOverlay(
|
||||
label_text=self.label_text,
|
||||
color=GUIConstants.BODY_FONT_COLOR,
|
||||
font_color=GUIConstants.BODY_FONT_COLOR,
|
||||
font_size=GUIConstants.BODY_FONT_SIZE,
|
||||
font_size=body_font_size,
|
||||
)
|
||||
|
||||
|
||||
|
||||
class InfoToast(DefaultToast):
|
||||
def instantiate_toast(self) -> ToastOverlay:
|
||||
body_font_size = GUIConstants.get_body_font_size()
|
||||
return ToastOverlay(
|
||||
icon_name=SeedSignerIconConstants.WARNING,
|
||||
icon_name=SeedSignerIconConstants.INFO,
|
||||
label_text=self.label_text,
|
||||
color=GUIConstants.INFO_COLOR,
|
||||
font_color=GUIConstants.BODY_FONT_COLOR,
|
||||
font_size=GUIConstants.BODY_FONT_SIZE,
|
||||
font_size=body_font_size,
|
||||
)
|
||||
|
||||
|
||||
|
||||
class SuccessToast(DefaultToast):
|
||||
def instantiate_toast(self) -> ToastOverlay:
|
||||
body_font_size = GUIConstants.get_body_font_size()
|
||||
return ToastOverlay(
|
||||
icon_name=SeedSignerIconConstants.SUCCESS,
|
||||
label_text=self.label_text,
|
||||
color=GUIConstants.SUCCESS_COLOR,
|
||||
font_color=GUIConstants.BODY_FONT_COLOR,
|
||||
font_size=GUIConstants.BODY_FONT_SIZE,
|
||||
font_size=body_font_size,
|
||||
)
|
||||
|
||||
|
||||
|
||||
class WarningToast(DefaultToast):
|
||||
def instantiate_toast(self) -> ToastOverlay:
|
||||
body_font_size = GUIConstants.get_body_font_size()
|
||||
return ToastOverlay(
|
||||
icon_name=SeedSignerIconConstants.WARNING,
|
||||
label_text=self.label_text,
|
||||
color=GUIConstants.WARNING_COLOR,
|
||||
font_color=GUIConstants.BODY_FONT_COLOR,
|
||||
font_size=GUIConstants.BODY_FONT_SIZE,
|
||||
font_size=body_font_size,
|
||||
)
|
||||
|
||||
|
||||
|
||||
class DireWarningToast(DefaultToast):
|
||||
def instantiate_toast(self) -> ToastOverlay:
|
||||
body_font_size = GUIConstants.get_body_font_size()
|
||||
return ToastOverlay(
|
||||
icon_name=SeedSignerIconConstants.WARNING,
|
||||
label_text=self.label_text,
|
||||
color=GUIConstants.DIRE_WARNING_COLOR,
|
||||
font_color=GUIConstants.BODY_FONT_COLOR,
|
||||
font_size=GUIConstants.BODY_FONT_SIZE,
|
||||
font_size=body_font_size,
|
||||
)
|
||||
|
||||
|
||||
|
||||
class ErrorToast(DefaultToast):
|
||||
def instantiate_toast(self) -> ToastOverlay:
|
||||
body_font_size = GUIConstants.get_body_font_size()
|
||||
return ToastOverlay(
|
||||
icon_name=SeedSignerIconConstants.ERROR,
|
||||
label_text=self.label_text,
|
||||
color=GUIConstants.ERROR_COLOR,
|
||||
font_color=GUIConstants.BODY_FONT_COLOR,
|
||||
font_size=GUIConstants.BODY_FONT_SIZE,
|
||||
font_size=body_font_size,
|
||||
)
|
||||
@@ -33,6 +33,7 @@ class HardwareButtons(Singleton):
|
||||
KEY2_PIN = 12
|
||||
KEY3_PIN = 8
|
||||
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls):
|
||||
# This is the only way to access the one and only instance
|
||||
@@ -53,8 +54,6 @@ class HardwareButtons(Singleton):
|
||||
cls._instance.GPIO = GPIO
|
||||
cls._instance.override_ind = False
|
||||
|
||||
cls._instance.add_events([HardwareButtonsConstants.KEY_UP, HardwareButtonsConstants.KEY_DOWN, HardwareButtonsConstants.KEY_PRESS, HardwareButtonsConstants.KEY_LEFT, HardwareButtonsConstants.KEY_RIGHT, HardwareButtonsConstants.KEY1, HardwareButtonsConstants.KEY2, HardwareButtonsConstants.KEY3])
|
||||
|
||||
# Track state over time so we can apply input delays/ignores as needed
|
||||
cls._instance.cur_input = None # Track which direction or button was last pressed
|
||||
cls._instance.cur_input_started = None # Track when that input began
|
||||
@@ -65,16 +64,30 @@ class HardwareButtons(Singleton):
|
||||
return cls._instance
|
||||
|
||||
|
||||
def wait_for(self, keys=[], check_release=True, release_keys=[]) -> int:
|
||||
@classmethod
|
||||
def get_instance_no_hardware(cls):
|
||||
# This is the only way to access the one and only instance
|
||||
if cls._instance is None:
|
||||
cls._instance = cls.__new__(cls)
|
||||
|
||||
|
||||
def wait_for(self, keys=[]) -> int:
|
||||
"""
|
||||
Block execution until one of the target keys is pressed.
|
||||
|
||||
Optionally override the wait by calling `trigger_override()`.
|
||||
"""
|
||||
# TODO: Refactor to keep control in the Controller and not here
|
||||
from seedsigner.controller import Controller
|
||||
controller = Controller.get_instance()
|
||||
|
||||
if not release_keys:
|
||||
release_keys = keys
|
||||
self.override_ind = False
|
||||
|
||||
while True:
|
||||
if self.override_ind:
|
||||
# Break out of the wait_for without waiting for user input
|
||||
self.override_ind = False
|
||||
return HardwareButtonsConstants.OVERRIDE
|
||||
|
||||
cur_time = int(time.time() * 1000)
|
||||
if cur_time - self.last_input_time > controller.screensaver_activation_ms and not controller.is_screensaver_running:
|
||||
# Start the screensaver. Will block execution until input detected.
|
||||
@@ -90,48 +103,42 @@ class HardwareButtons(Singleton):
|
||||
# Resume from a fresh loop
|
||||
continue
|
||||
|
||||
# Check each candidate key to see if it was pressed
|
||||
for key in keys:
|
||||
if not check_release or ((check_release and key in release_keys and HardwareButtonsConstants.release_lock) or check_release and key not in release_keys):
|
||||
# when check release is False or the release lock is released (True)
|
||||
if self.GPIO.input(key) == GPIO.LOW or self.override_ind:
|
||||
HardwareButtonsConstants.release_lock = False
|
||||
if self.override_ind:
|
||||
self.override_ind = False
|
||||
return HardwareButtonsConstants.OVERRIDE
|
||||
if self.GPIO.input(key) == GPIO.LOW:
|
||||
if self.cur_input != key:
|
||||
self.cur_input = key
|
||||
self.cur_input_started = int(time.time() * 1000) # in milliseconds
|
||||
self.last_input_time = self.cur_input_started
|
||||
return key
|
||||
|
||||
if self.cur_input != key:
|
||||
self.cur_input = key
|
||||
self.cur_input_started = int(time.time() * 1000) # in milliseconds
|
||||
self.last_input_time = self.cur_input_started
|
||||
else:
|
||||
# Still pressing the same input
|
||||
if cur_time - self.last_input_time > self.next_repeat_threshold:
|
||||
# Too much time has elapsed to consider this the same
|
||||
# continuous input. Treat as a new separate press.
|
||||
self.cur_input_started = cur_time
|
||||
self.last_input_time = cur_time
|
||||
return key
|
||||
|
||||
elif cur_time - self.cur_input_started > self.first_repeat_threshold:
|
||||
# We're good to relay this immediately as continuous
|
||||
# input.
|
||||
self.last_input_time = cur_time
|
||||
return key
|
||||
|
||||
else:
|
||||
# Still pressing the same input
|
||||
if cur_time - self.last_input_time > self.next_repeat_threshold:
|
||||
# Too much time has elapsed to consider this the same
|
||||
# continuous input. Treat as a new separate press.
|
||||
self.cur_input_started = cur_time
|
||||
self.last_input_time = cur_time
|
||||
return key
|
||||
|
||||
elif cur_time - self.cur_input_started > self.first_repeat_threshold:
|
||||
# We're good to relay this immediately as continuous
|
||||
# input.
|
||||
self.last_input_time = cur_time
|
||||
return key
|
||||
|
||||
else:
|
||||
# We're not yet at the first repeat threshold; triggering
|
||||
# a key now would be too soon and yields a bad user
|
||||
# experience when only a single click was intended but
|
||||
# a second input is processed because of race condition
|
||||
# against human response time to release the button.
|
||||
# So there has to be a delay before we allow the first
|
||||
# continuous repeat to register. So we'll ignore this
|
||||
# round's input and **won't update any of our
|
||||
# timekeeping vars**. But once we cross the threshold,
|
||||
# we let the repeats fly.
|
||||
pass
|
||||
# We're not yet at the first repeat threshold; triggering
|
||||
# a key now would be too soon and yields a bad user
|
||||
# experience when only a single click was intended but
|
||||
# a second input is processed because of race condition
|
||||
# against human response time to release the button.
|
||||
# So there has to be a delay before we allow the first
|
||||
# continuous repeat to register. So we'll ignore this
|
||||
# round's input and **won't update any of our
|
||||
# timekeeping vars**. But once we cross the threshold,
|
||||
# we let the repeats fly.
|
||||
pass
|
||||
|
||||
time.sleep(0.01) # wait 10 ms to give CPU chance to do other things
|
||||
|
||||
@@ -140,29 +147,13 @@ class HardwareButtons(Singleton):
|
||||
self.last_input_time = int(time.time() * 1000)
|
||||
|
||||
|
||||
def add_events(self, keys=[]):
|
||||
for key in keys:
|
||||
GPIO.add_event_detect(key, self.GPIO.RISING, callback=HardwareButtons.rising_callback)
|
||||
def trigger_override(self) -> bool:
|
||||
""" Set the override flag to break out of the current `wait_for` loop """
|
||||
self.override_ind = True
|
||||
|
||||
|
||||
def rising_callback(channel):
|
||||
HardwareButtonsConstants.release_lock = True
|
||||
|
||||
|
||||
def trigger_override(self, force_release = False) -> bool:
|
||||
if force_release:
|
||||
HardwareButtonsConstants.release_lock = True
|
||||
|
||||
if not self.override_ind:
|
||||
self.override_ind = True
|
||||
return True
|
||||
return False
|
||||
|
||||
def force_release(self) -> bool:
|
||||
HardwareButtonsConstants.release_lock = True
|
||||
return True
|
||||
|
||||
def check_for_low(self, key: int = None, keys: List[int] = None) -> bool:
|
||||
""" Returns True if one of the target keys/key is pressed """
|
||||
if key:
|
||||
keys = [key]
|
||||
for key in keys:
|
||||
@@ -172,15 +163,16 @@ class HardwareButtons(Singleton):
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def has_any_input(self) -> bool:
|
||||
""" Returns True if any of the keys are pressed """
|
||||
for key in HardwareButtonsConstants.ALL_KEYS:
|
||||
if self.GPIO.input(key) == GPIO.LOW:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
# class used as short hand for static button/channel lookup values
|
||||
# TODO: Implement `release_lock` functionality as a global somewhere. Mixes up design
|
||||
# patterns to have a static constants class plus a settable global value.
|
||||
class HardwareButtonsConstants:
|
||||
if GPIO.RPI_INFO['P1_REVISION'] == 3: #This indicates that we have revision 3 GPIO
|
||||
KEY_UP = 31
|
||||
@@ -218,5 +210,3 @@ class HardwareButtonsConstants:
|
||||
|
||||
KEYS__LEFT_RIGHT_UP_DOWN = [KEY_LEFT, KEY_RIGHT, KEY_UP, KEY_DOWN]
|
||||
KEYS__ANYCLICK = [KEY_PRESS, KEY1, KEY2, KEY3]
|
||||
|
||||
release_lock = True # released when True, locked when False
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import io
|
||||
|
||||
from picamera import PiCamera
|
||||
from PIL import Image
|
||||
from seedsigner.hardware.pivideostream import PiVideoStream
|
||||
from seedsigner.models.settings import Settings, SettingsConstants
|
||||
@@ -23,6 +22,7 @@ class Camera(Singleton):
|
||||
|
||||
|
||||
def start_video_stream_mode(self, resolution=(512, 384), framerate=12, format="bgr"):
|
||||
from seedsigner.hardware.pivideostream import PiVideoStream
|
||||
if self._video_stream is not None:
|
||||
self.stop_video_stream_mode()
|
||||
|
||||
@@ -49,6 +49,7 @@ class Camera(Singleton):
|
||||
|
||||
|
||||
def start_single_frame_mode(self, resolution=(720, 480)):
|
||||
from picamera import PiCamera
|
||||
if self._video_stream is not None:
|
||||
self.stop_video_stream_mode()
|
||||
if self._picamera is not None:
|
||||
|
||||
@@ -50,7 +50,7 @@ def get_standard_derivation_path(network: str = SettingsConstants.MAINNET, walle
|
||||
elif script_type == SettingsConstants.NATIVE_SEGWIT:
|
||||
return f"m/48'/{network_path}/0'/2'"
|
||||
elif script_type == SettingsConstants.TAPROOT:
|
||||
raise Exception("Taproot multisig/musig not yet supported")
|
||||
raise Exception("Taproot multisig not yet supported")
|
||||
else:
|
||||
raise Exception("Unexpected script type")
|
||||
else:
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
def mark_for_translation(message: str) -> str:
|
||||
# Wraps the target string literal for translation but does NOT return the translated string.
|
||||
return message
|
||||
@@ -102,6 +102,6 @@ class QR:
|
||||
# if qrencode fails, fall back to only encoder
|
||||
if rv != 0:
|
||||
return self.qrimage(data,width,height,border)
|
||||
img = Image.open("/tmp/qrcode.png").resize((width,height), Image.NEAREST).convert("RGBA")
|
||||
img = Image.open("/tmp/qrcode.png").resize((width,height), Image.Resampling.NEAREST).convert("RGBA")
|
||||
|
||||
return img
|
||||
|
||||
@@ -414,6 +414,14 @@ class DecodeQR:
|
||||
pass
|
||||
|
||||
# Is it byte data?
|
||||
if not isinstance(s, bytes):
|
||||
try:
|
||||
# TODO: remove this check & conversion once above cast to str is removed
|
||||
s = s.encode()
|
||||
except UnicodeError:
|
||||
# Couldn't convert back to bytes; shouldn't happen
|
||||
raise Exception("Conversion to bytes failed")
|
||||
|
||||
# 32 bytes for 24-word CompactSeedQR; 16 bytes for 12-word CompactSeedQR
|
||||
if len(s) == 32 or len(s) == 16:
|
||||
try:
|
||||
|
||||
@@ -15,7 +15,7 @@ from seedsigner.models.seed import Seed
|
||||
from seedsigner.models.settings import SettingsConstants
|
||||
|
||||
from urtypes.crypto import PSBT as UR_PSBT
|
||||
from urtypes.crypto import Account, HDKey, Output, Keypath, PathComponent, SCRIPT_EXPRESSION_TAG_MAP
|
||||
from urtypes.crypto import Account, HDKey, Output, Keypath, PathComponent, SCRIPT_EXPRESSION_TAG_MAP, CoinInfo
|
||||
|
||||
|
||||
|
||||
@@ -344,11 +344,13 @@ class UrXpubQrEncoder(BaseFountainQrEncoder, BaseXpubQrEncoder):
|
||||
return Keypath(arr, self.root.my_fingerprint, len(arr))
|
||||
|
||||
origin = derivation_to_keypath(self.derivation)
|
||||
self.use_info = None if self.network == SettingsConstants.MAINNET else CoinInfo(type=None, network=1)
|
||||
|
||||
self.ur_hdkey = HDKey({ 'key': self.xpub.key.serialize(),
|
||||
'chain_code': self.xpub.chain_code,
|
||||
'origin': origin,
|
||||
'parent_fingerprint': self.xpub.fingerprint})
|
||||
'parent_fingerprint': self.xpub.fingerprint,
|
||||
'use_info': self.use_info })
|
||||
|
||||
ur_outputs = []
|
||||
|
||||
|
||||
@@ -78,6 +78,11 @@ class Seed:
|
||||
return unicodedata.normalize("NFC", " ".join(self._mnemonic)).split()
|
||||
|
||||
|
||||
@property
|
||||
def has_passphrase(self):
|
||||
return self._passphrase != ""
|
||||
|
||||
|
||||
@property
|
||||
def passphrase(self):
|
||||
return self._passphrase
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import gettext
|
||||
import logging
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
import platform
|
||||
|
||||
from typing import List
|
||||
@@ -36,6 +38,19 @@ class Settings(Singleton):
|
||||
with open(Settings.SETTINGS_FILENAME) as settings_file:
|
||||
settings.update(json.load(settings_file))
|
||||
|
||||
# Setup multilanguage support
|
||||
path = os.path.join(
|
||||
pathlib.Path(__file__).parent.resolve().parent.resolve(),
|
||||
"resources",
|
||||
"seedsigner-translations",
|
||||
"l10n"
|
||||
)
|
||||
gettext.bindtextdomain('messages', localedir=path)
|
||||
gettext.textdomain('messages')
|
||||
|
||||
# Load default/persistent locale setting
|
||||
settings.load_locale()
|
||||
|
||||
return cls._instance
|
||||
|
||||
|
||||
@@ -141,11 +156,8 @@ class Settings(Singleton):
|
||||
# Break comma-separated SettingsQR input into List
|
||||
new_settings[entry.attr_name] = new_settings[entry.attr_name].split(",")
|
||||
|
||||
# Can't just merge the _data dict; have to replace keys they have in common
|
||||
# (otherwise list values will be merged instead of replaced).
|
||||
for key, value in new_settings.items():
|
||||
self._data.pop(key, None)
|
||||
self._data[key] = value
|
||||
self.set_value(key, value)
|
||||
|
||||
|
||||
def set_value(self, attr_name: str, value: any):
|
||||
@@ -155,7 +167,9 @@ class Settings(Singleton):
|
||||
Note that for multiselect, the value must be a List.
|
||||
"""
|
||||
if attr_name not in self._data:
|
||||
raise Exception(f"Setting for {attr_name} not found")
|
||||
# Outdated settings
|
||||
print(f"Setting {attr_name} not recognized. Ignoring.")
|
||||
return
|
||||
|
||||
if SettingsDefinition.get_settings_entry(attr_name).type == SettingsConstants.TYPE__MULTISELECT:
|
||||
if type(value) != list:
|
||||
@@ -171,7 +185,11 @@ class Settings(Singleton):
|
||||
|
||||
self._data[attr_name] = value
|
||||
self.save()
|
||||
|
||||
|
||||
# Special handling for localization
|
||||
if attr_name == SettingsConstants.SETTING__LOCALE:
|
||||
self.load_locale()
|
||||
|
||||
|
||||
def get_value(self, attr_name: str):
|
||||
"""
|
||||
@@ -222,13 +240,22 @@ class Settings(Singleton):
|
||||
return display_names
|
||||
|
||||
|
||||
def load_locale(self):
|
||||
locale = self.get_value(SettingsConstants.SETTING__LOCALE)
|
||||
os.environ['LANGUAGE'] = locale
|
||||
|
||||
# Re-initialize with the new locale
|
||||
print(f"Set LANGUAGE locale to {os.environ['LANGUAGE']}")
|
||||
|
||||
|
||||
|
||||
"""
|
||||
Intentionally keeping the properties very limited to avoid an expectation of
|
||||
boilerplate property code for every SettingsEntry.
|
||||
|
||||
It's more cumbersome, but instead use:
|
||||
|
||||
settings.get_value(SettingsConstants.SETTING__MY_SETTING_ATTR)
|
||||
Settings.get_instance().get_value(SettingsConstants.SETTING__MY_SETTING_ATTR)
|
||||
"""
|
||||
@property
|
||||
def debug(self) -> bool:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, List
|
||||
|
||||
from seedsigner.helpers.l10n import mark_for_translation as _mft
|
||||
|
||||
|
||||
class SettingsConstants:
|
||||
@@ -10,25 +11,25 @@ class SettingsConstants:
|
||||
OPTION__PROMPT = "P"
|
||||
OPTION__REQUIRED = "R"
|
||||
OPTIONS__ENABLED_DISABLED = [
|
||||
(OPTION__ENABLED, "Enabled"),
|
||||
(OPTION__DISABLED, "Disabled"),
|
||||
(OPTION__ENABLED, _mft("Enabled")),
|
||||
(OPTION__DISABLED, _mft("Disabled")),
|
||||
]
|
||||
OPTIONS__ONLY_DISABLED = [
|
||||
(OPTION__DISABLED, "Disabled"),
|
||||
(OPTION__DISABLED, _mft("Disabled")),
|
||||
]
|
||||
OPTIONS__PROMPT_REQUIRED_DISABLED = [
|
||||
(OPTION__PROMPT, "Prompt"),
|
||||
(OPTION__REQUIRED, "Required"),
|
||||
(OPTION__DISABLED, "Disabled"),
|
||||
(OPTION__PROMPT, _mft("Prompt")),
|
||||
(OPTION__REQUIRED, _mft("Required")),
|
||||
(OPTION__DISABLED, _mft("Disabled")),
|
||||
]
|
||||
OPTIONS__ENABLED_DISABLED_REQUIRED = OPTIONS__ENABLED_DISABLED +[
|
||||
(OPTION__REQUIRED, "Required"),
|
||||
(OPTION__REQUIRED, _mft("Required")),
|
||||
]
|
||||
OPTIONS__ENABLED_DISABLED_PROMPT = OPTIONS__ENABLED_DISABLED + [
|
||||
(OPTION__PROMPT, "Prompt"),
|
||||
(OPTION__PROMPT, _mft("Prompt")),
|
||||
]
|
||||
ALL_OPTIONS = OPTIONS__ENABLED_DISABLED_PROMPT + [
|
||||
(OPTION__REQUIRED, "Required"),
|
||||
(OPTION__REQUIRED, _mft("Required")),
|
||||
]
|
||||
|
||||
# User-facing selection options
|
||||
@@ -45,9 +46,30 @@ class SettingsConstants:
|
||||
(COORDINATOR__KEEPER, "Keeper"),
|
||||
]
|
||||
|
||||
LANGUAGE__ENGLISH = "en"
|
||||
ALL_LANGUAGES = [
|
||||
(LANGUAGE__ENGLISH, "English"),
|
||||
LOCALE__ARABIC = "ar"
|
||||
LOCALE__CZECH = "cs"
|
||||
LOCALE__ENGLISH = "en"
|
||||
LOCALE__FRENCH = "fr"
|
||||
LOCALE__GERMAN = "de"
|
||||
LOCALE__HEBREW = "he"
|
||||
LOCALE__JAPANESE = "ja"
|
||||
LOCALE__KOREAN = "kr"
|
||||
LOCALE__PORTUGUESE = "pt"
|
||||
LOCALE__RUSSIAN = "ru"
|
||||
LOCALE__SPANISH = "es"
|
||||
# Do not wrap for translation; always present each language in its native form
|
||||
ALL_LOCALES = [
|
||||
# (LOCALE__ARABIC, "Arabic"),
|
||||
# (LOCALE__CZECH, "čeština"),
|
||||
(LOCALE__ENGLISH, "English"),
|
||||
# (LOCALE__FRENCH, "Français"),
|
||||
# (LOCALE__GERMAN, "Deutsch"),
|
||||
# (LOCALE__HEBREW, "Hebrew"),
|
||||
# (LOCALE__JAPANESE, "Japanese"),
|
||||
# (LOCALE__KOREAN, "Korean"),
|
||||
# (LOCALE__PORTUGUESE, "Português"),
|
||||
# (LOCALE__RUSSIAN, "русский"),
|
||||
(LOCALE__SPANISH, "Español"),
|
||||
]
|
||||
|
||||
BTC_DENOMINATION__BTC = "btc"
|
||||
@@ -55,10 +77,10 @@ class SettingsConstants:
|
||||
BTC_DENOMINATION__THRESHOLD = "thr"
|
||||
BTC_DENOMINATION__BTCSATSHYBRID = "hyb"
|
||||
ALL_BTC_DENOMINATIONS = [
|
||||
(BTC_DENOMINATION__BTC, "BTC"),
|
||||
(BTC_DENOMINATION__SATS, "sats"),
|
||||
(BTC_DENOMINATION__THRESHOLD, "Threshold at 0.01"),
|
||||
(BTC_DENOMINATION__BTCSATSHYBRID, "BTC | sats hybrid"),
|
||||
(BTC_DENOMINATION__BTC, _mft("BTC")),
|
||||
(BTC_DENOMINATION__SATS, _mft("sats")),
|
||||
(BTC_DENOMINATION__THRESHOLD, _mft("Threshold at 0.01")),
|
||||
(BTC_DENOMINATION__BTCSATSHYBRID, _mft("BTC | sats hybrid")),
|
||||
]
|
||||
|
||||
CAMERA_ROTATION__0 = 0
|
||||
@@ -66,20 +88,26 @@ class SettingsConstants:
|
||||
CAMERA_ROTATION__180 = 180
|
||||
CAMERA_ROTATION__270 = 270
|
||||
ALL_CAMERA_ROTATIONS = [
|
||||
(CAMERA_ROTATION__0, "0°"),
|
||||
(CAMERA_ROTATION__90, "90°"),
|
||||
(CAMERA_ROTATION__180, "180°"),
|
||||
(CAMERA_ROTATION__270, "270°"),
|
||||
(CAMERA_ROTATION__0, _mft("0°")),
|
||||
(CAMERA_ROTATION__90, _mft("90°")),
|
||||
(CAMERA_ROTATION__180, _mft("180°")),
|
||||
(CAMERA_ROTATION__270, _mft("270°")),
|
||||
]
|
||||
|
||||
# QR code constants
|
||||
DENSITY__LOW = "L"
|
||||
DENSITY__MEDIUM = "M"
|
||||
DENSITY__HIGH = "H"
|
||||
# TRANSLATOR_NOTE: QR code density option: Low, Medium, High
|
||||
density_low = _mft("Low")
|
||||
# TRANSLATOR_NOTE: QR code density option: Low, Medium, High
|
||||
density_medium = _mft("Medium")
|
||||
# TRANSLATOR_NOTE: QR code density option: Low, Medium, High
|
||||
density_high = _mft("High")
|
||||
ALL_DENSITIES = [
|
||||
(DENSITY__LOW, "Low"),
|
||||
(DENSITY__MEDIUM, "Medium"),
|
||||
(DENSITY__HIGH, "High"),
|
||||
(DENSITY__LOW, density_low),
|
||||
(DENSITY__MEDIUM, density_medium),
|
||||
(DENSITY__HIGH, density_high),
|
||||
]
|
||||
|
||||
# Seed-related constants
|
||||
@@ -87,13 +115,14 @@ class SettingsConstants:
|
||||
TESTNET = "T"
|
||||
REGTEST = "R"
|
||||
ALL_NETWORKS = [
|
||||
(MAINNET, "Mainnet"),
|
||||
(TESTNET, "Testnet"),
|
||||
(REGTEST, "Regtest")
|
||||
(MAINNET, _mft("Mainnet")),
|
||||
(TESTNET, _mft("Testnet")),
|
||||
(REGTEST, _mft("Regtest"))
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def map_network_to_embit(cls, network) -> str:
|
||||
# Note these are `embit` constants; do not wrap for translation
|
||||
if network == SettingsConstants.MAINNET:
|
||||
return "main"
|
||||
elif network == SettingsConstants.TESTNET:
|
||||
@@ -101,14 +130,14 @@ class SettingsConstants:
|
||||
if network == SettingsConstants.REGTEST:
|
||||
return "regtest"
|
||||
|
||||
PERSISTENT_SETTINGS__SD_INSERTED__HELP_TEXT = "Store Settings on SD card"
|
||||
PERSISTENT_SETTINGS__SD_REMOVED__HELP_TEXT = "Insert SD card to enable"
|
||||
PERSISTENT_SETTINGS__SD_INSERTED__HELP_TEXT = _mft("Store Settings on SD card")
|
||||
PERSISTENT_SETTINGS__SD_REMOVED__HELP_TEXT = _mft("Insert SD card to enable")
|
||||
|
||||
SINGLE_SIG = "ss"
|
||||
MULTISIG = "ms"
|
||||
ALL_SIG_TYPES = [
|
||||
(SINGLE_SIG, "Single Sig"),
|
||||
(MULTISIG, "Multisig"),
|
||||
(SINGLE_SIG, _mft("Single Sig")),
|
||||
(MULTISIG, _mft("Multisig")),
|
||||
]
|
||||
|
||||
LEGACY_P2PKH = "leg"
|
||||
@@ -117,11 +146,11 @@ class SettingsConstants:
|
||||
TAPROOT = "tr"
|
||||
CUSTOM_DERIVATION = "cus"
|
||||
ALL_SCRIPT_TYPES = [
|
||||
(NATIVE_SEGWIT, "Native Segwit"),
|
||||
(NESTED_SEGWIT, "Nested Segwit"),
|
||||
(LEGACY_P2PKH, "Legacy"),
|
||||
(TAPROOT, "Taproot"),
|
||||
(CUSTOM_DERIVATION, "Custom Derivation"),
|
||||
(NATIVE_SEGWIT, _mft("Native Segwit")),
|
||||
(NESTED_SEGWIT, _mft("Nested Segwit")),
|
||||
(LEGACY_P2PKH, _mft("Legacy")),
|
||||
(TAPROOT, _mft("Taproot")),
|
||||
(CUSTOM_DERIVATION, _mft("Custom Derivation")),
|
||||
]
|
||||
|
||||
WORDLIST_LANGUAGE__ENGLISH = "en"
|
||||
@@ -145,7 +174,8 @@ class SettingsConstants:
|
||||
|
||||
|
||||
# Individual SettingsEntry attr_names
|
||||
SETTING__LANGUAGE = "language"
|
||||
# Note: attr_names are internal constants; do not wrap for translation
|
||||
SETTING__LOCALE = "locale"
|
||||
SETTING__WORDLIST_LANGUAGE = "wordlist_language"
|
||||
SETTING__PERSISTENT_SETTINGS = "persistent_settings"
|
||||
SETTING__COORDINATORS = "coordinators"
|
||||
@@ -207,8 +237,10 @@ class SettingsConstants:
|
||||
ELECTRUM_PBKDF2_ROUNDS=2048
|
||||
|
||||
# Label strings
|
||||
LABEL__BIP39_PASSPHRASE = "BIP-39 Passphrase"
|
||||
LABEL__CUSTOM_EXTENSION = "Custom Extension" # Terminology used by Electrum seeds
|
||||
LABEL__BIP39_PASSPHRASE = _mft("BIP-39 Passphrase")
|
||||
# TRANSLATOR_NOTE: Terminology used by Electrum seeds; equivalent to bip39 passphrase
|
||||
custom_extension = _mft("Custom Extension")
|
||||
LABEL__CUSTOM_EXTENSION = custom_extension
|
||||
|
||||
|
||||
|
||||
@@ -279,7 +311,7 @@ class SettingsEntry:
|
||||
option_value = option
|
||||
display_name = option
|
||||
if option_value == value:
|
||||
return display_name
|
||||
return _mft(display_name)
|
||||
|
||||
|
||||
def get_selection_option_value_by_display_name(self, display_name: str):
|
||||
@@ -345,21 +377,19 @@ class SettingsDefinition:
|
||||
settings_entries: List[SettingsEntry] = [
|
||||
# General options
|
||||
|
||||
# TODO: Full babel multilanguage support! Until then, type == HIDDEN
|
||||
SettingsEntry(category=SettingsConstants.CATEGORY__SYSTEM,
|
||||
attr_name=SettingsConstants.SETTING__LANGUAGE,
|
||||
attr_name=SettingsConstants.SETTING__LOCALE,
|
||||
abbreviated_name="lang",
|
||||
display_name="Language",
|
||||
display_name=_mft("Language"),
|
||||
type=SettingsConstants.TYPE__SELECT_1,
|
||||
visibility=SettingsConstants.VISIBILITY__HIDDEN,
|
||||
selection_options=SettingsConstants.ALL_LANGUAGES,
|
||||
default_value=SettingsConstants.LANGUAGE__ENGLISH),
|
||||
selection_options=SettingsConstants.ALL_LOCALES,
|
||||
default_value=SettingsConstants.LOCALE__ENGLISH),
|
||||
|
||||
# TODO: Support other bip-39 wordlist languages! Until then, type == HIDDEN
|
||||
SettingsEntry(category=SettingsConstants.CATEGORY__SYSTEM,
|
||||
attr_name=SettingsConstants.SETTING__WORDLIST_LANGUAGE,
|
||||
abbreviated_name="wordlist_lang",
|
||||
display_name="Mnemonic language",
|
||||
display_name=_mft("Mnemonic language"),
|
||||
type=SettingsConstants.TYPE__SELECT_1,
|
||||
visibility=SettingsConstants.VISIBILITY__HIDDEN,
|
||||
selection_options=SettingsConstants.ALL_WORDLIST_LANGUAGES,
|
||||
@@ -368,14 +398,14 @@ class SettingsDefinition:
|
||||
SettingsEntry(category=SettingsConstants.CATEGORY__SYSTEM,
|
||||
attr_name=SettingsConstants.SETTING__PERSISTENT_SETTINGS,
|
||||
abbreviated_name="persistent",
|
||||
display_name="Persistent settings",
|
||||
display_name=_mft("Persistent settings"),
|
||||
help_text=SettingsConstants.PERSISTENT_SETTINGS__SD_INSERTED__HELP_TEXT,
|
||||
default_value=SettingsConstants.OPTION__DISABLED),
|
||||
|
||||
SettingsEntry(category=SettingsConstants.CATEGORY__WALLET,
|
||||
attr_name=SettingsConstants.SETTING__COORDINATORS,
|
||||
abbreviated_name="coords",
|
||||
display_name="Coordinator software",
|
||||
display_name=_mft("Coordinator software"),
|
||||
type=SettingsConstants.TYPE__MULTISELECT,
|
||||
selection_options=SettingsConstants.ALL_COORDINATORS,
|
||||
default_value=[
|
||||
@@ -388,7 +418,7 @@ class SettingsDefinition:
|
||||
SettingsEntry(category=SettingsConstants.CATEGORY__SYSTEM,
|
||||
attr_name=SettingsConstants.SETTING__BTC_DENOMINATION,
|
||||
abbreviated_name="denom",
|
||||
display_name="Denomination display",
|
||||
display_name=_mft("Denomination display"),
|
||||
type=SettingsConstants.TYPE__SELECT_1,
|
||||
selection_options=SettingsConstants.ALL_BTC_DENOMINATIONS,
|
||||
default_value=SettingsConstants.BTC_DENOMINATION__THRESHOLD),
|
||||
@@ -397,7 +427,7 @@ class SettingsDefinition:
|
||||
# Advanced options
|
||||
SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES,
|
||||
attr_name=SettingsConstants.SETTING__NETWORK,
|
||||
display_name="Bitcoin network",
|
||||
display_name=_mft("Bitcoin network"),
|
||||
type=SettingsConstants.TYPE__SELECT_1,
|
||||
visibility=SettingsConstants.VISIBILITY__ADVANCED,
|
||||
selection_options=SettingsConstants.ALL_NETWORKS,
|
||||
@@ -405,7 +435,7 @@ class SettingsDefinition:
|
||||
|
||||
SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES,
|
||||
attr_name=SettingsConstants.SETTING__QR_DENSITY,
|
||||
display_name="QR code density",
|
||||
display_name=_mft("QR code density"),
|
||||
type=SettingsConstants.TYPE__SELECT_1,
|
||||
visibility=SettingsConstants.VISIBILITY__ADVANCED,
|
||||
selection_options=SettingsConstants.ALL_DENSITIES,
|
||||
@@ -413,14 +443,14 @@ class SettingsDefinition:
|
||||
|
||||
SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES,
|
||||
attr_name=SettingsConstants.SETTING__XPUB_EXPORT,
|
||||
display_name="Xpub export",
|
||||
display_name=_mft("Xpub export"),
|
||||
visibility=SettingsConstants.VISIBILITY__ADVANCED,
|
||||
default_value=SettingsConstants.OPTION__ENABLED),
|
||||
|
||||
SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES,
|
||||
attr_name=SettingsConstants.SETTING__SIG_TYPES,
|
||||
abbreviated_name="sigs",
|
||||
display_name="Sig types",
|
||||
display_name=_mft("Sig types"),
|
||||
type=SettingsConstants.TYPE__MULTISELECT,
|
||||
visibility=SettingsConstants.VISIBILITY__ADVANCED,
|
||||
selection_options=SettingsConstants.ALL_SIG_TYPES,
|
||||
@@ -429,7 +459,7 @@ class SettingsDefinition:
|
||||
SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES,
|
||||
attr_name=SettingsConstants.SETTING__SCRIPT_TYPES,
|
||||
abbreviated_name="scripts",
|
||||
display_name="Script types",
|
||||
display_name=_mft("Script types"),
|
||||
type=SettingsConstants.TYPE__MULTISELECT,
|
||||
visibility=SettingsConstants.VISIBILITY__ADVANCED,
|
||||
selection_options=SettingsConstants.ALL_SCRIPT_TYPES,
|
||||
@@ -437,13 +467,13 @@ class SettingsDefinition:
|
||||
|
||||
SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES,
|
||||
attr_name=SettingsConstants.SETTING__XPUB_DETAILS,
|
||||
display_name="Show xpub details",
|
||||
display_name=_mft("Show xpub details"),
|
||||
visibility=SettingsConstants.VISIBILITY__ADVANCED,
|
||||
default_value=SettingsConstants.OPTION__ENABLED),
|
||||
|
||||
SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES,
|
||||
attr_name=SettingsConstants.SETTING__PASSPHRASE,
|
||||
display_name="BIP-39 passphrase",
|
||||
display_name=_mft("BIP-39 passphrase"),
|
||||
type=SettingsConstants.TYPE__SELECT_1,
|
||||
visibility=SettingsConstants.VISIBILITY__ADVANCED,
|
||||
selection_options=SettingsConstants.OPTIONS__ENABLED_DISABLED_REQUIRED,
|
||||
@@ -452,7 +482,7 @@ class SettingsDefinition:
|
||||
SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES,
|
||||
attr_name=SettingsConstants.SETTING__CAMERA_ROTATION,
|
||||
abbreviated_name="camera",
|
||||
display_name="Camera rotation",
|
||||
display_name=_mft("Camera rotation"),
|
||||
type=SettingsConstants.TYPE__SELECT_1,
|
||||
visibility=SettingsConstants.VISIBILITY__ADVANCED,
|
||||
selection_options=SettingsConstants.ALL_CAMERA_ROTATIONS,
|
||||
@@ -460,55 +490,55 @@ class SettingsDefinition:
|
||||
|
||||
SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES,
|
||||
attr_name=SettingsConstants.SETTING__COMPACT_SEEDQR,
|
||||
display_name="CompactSeedQR",
|
||||
display_name=_mft("Compact SeedQR"),
|
||||
visibility=SettingsConstants.VISIBILITY__ADVANCED,
|
||||
default_value=SettingsConstants.OPTION__ENABLED),
|
||||
|
||||
SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES,
|
||||
attr_name=SettingsConstants.SETTING__BIP85_CHILD_SEEDS,
|
||||
abbreviated_name="bip85",
|
||||
display_name="BIP-85 child seeds",
|
||||
display_name=_mft("BIP-85 child seeds"),
|
||||
visibility=SettingsConstants.VISIBILITY__ADVANCED,
|
||||
default_value=SettingsConstants.OPTION__DISABLED),
|
||||
|
||||
SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES,
|
||||
attr_name=SettingsConstants.SETTING__ELECTRUM_SEEDS,
|
||||
abbreviated_name="electrum",
|
||||
display_name="Electrum seeds",
|
||||
help_text="Native Segwit only",
|
||||
display_name=_mft("Electrum seeds"),
|
||||
help_text=_mft("Native Segwit only"),
|
||||
visibility=SettingsConstants.VISIBILITY__ADVANCED,
|
||||
default_value=SettingsConstants.OPTION__DISABLED),
|
||||
|
||||
SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES,
|
||||
attr_name=SettingsConstants.SETTING__MESSAGE_SIGNING,
|
||||
display_name="Message signing",
|
||||
display_name=_mft("Message signing"),
|
||||
visibility=SettingsConstants.VISIBILITY__ADVANCED,
|
||||
default_value=SettingsConstants.OPTION__DISABLED),
|
||||
|
||||
SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES,
|
||||
attr_name=SettingsConstants.SETTING__PRIVACY_WARNINGS,
|
||||
abbreviated_name="priv_warn",
|
||||
display_name="Show privacy warnings",
|
||||
display_name=_mft("Show privacy warnings"),
|
||||
visibility=SettingsConstants.VISIBILITY__ADVANCED,
|
||||
default_value=SettingsConstants.OPTION__ENABLED),
|
||||
|
||||
SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES,
|
||||
attr_name=SettingsConstants.SETTING__DIRE_WARNINGS,
|
||||
abbreviated_name="dire_warn",
|
||||
display_name="Show dire warnings",
|
||||
display_name=_mft("Show dire warnings"),
|
||||
visibility=SettingsConstants.VISIBILITY__ADVANCED,
|
||||
default_value=SettingsConstants.OPTION__ENABLED),
|
||||
|
||||
SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES,
|
||||
attr_name=SettingsConstants.SETTING__QR_BRIGHTNESS_TIPS,
|
||||
display_name="Show QR brightness tips",
|
||||
display_name=_mft("Show QR brightness tips"),
|
||||
visibility=SettingsConstants.VISIBILITY__ADVANCED,
|
||||
default_value=SettingsConstants.OPTION__ENABLED),
|
||||
|
||||
SettingsEntry(category=SettingsConstants.CATEGORY__FEATURES,
|
||||
attr_name=SettingsConstants.SETTING__PARTNER_LOGOS,
|
||||
abbreviated_name="partners",
|
||||
display_name="Show partner logos",
|
||||
display_name=_mft("Show partner logos"),
|
||||
visibility=SettingsConstants.VISIBILITY__ADVANCED,
|
||||
default_value=SettingsConstants.OPTION__ENABLED),
|
||||
|
||||
@@ -524,7 +554,7 @@ class SettingsDefinition:
|
||||
SettingsEntry(category=SettingsConstants.CATEGORY__SYSTEM,
|
||||
attr_name=SettingsConstants.SETTING__QR_BRIGHTNESS,
|
||||
abbreviated_name="qr_brightness",
|
||||
display_name="QR background color",
|
||||
display_name=_mft("QR background color"),
|
||||
type=SettingsConstants.TYPE__FREE_ENTRY,
|
||||
visibility=SettingsConstants.VISIBILITY__HIDDEN,
|
||||
default_value=62),
|
||||
|
||||
@@ -21,7 +21,7 @@ class ConfigurableSingleton(Singleton):
|
||||
if cls._instance:
|
||||
return cls._instance
|
||||
else:
|
||||
raise Exception("Must call %s.configure_instance(config) first" % cls.__name__)
|
||||
raise Exception("Must call %s.configure_instance() first" % cls.__name__)
|
||||
|
||||
|
||||
@classmethod
|
||||
|
||||
Binary file not shown.
+1
Submodule src/seedsigner/resources/seedsigner-translations added at 281a3dab83
@@ -1,26 +1,23 @@
|
||||
from embit.psbt import PSBT
|
||||
from embit import script
|
||||
from embit.networks import NETWORKS
|
||||
from seedsigner.controller import Controller
|
||||
from gettext import gettext as _
|
||||
|
||||
from seedsigner.gui.components import FontAwesomeIconConstants, SeedSignerIconConstants
|
||||
from seedsigner.models.encode_qr import UrPsbtQrEncoder
|
||||
from seedsigner.models.psbt_parser import PSBTParser
|
||||
from seedsigner.models.settings import SettingsConstants
|
||||
from seedsigner.gui.screens.psbt_screens import PSBTOpReturnScreen, PSBTOverviewScreen, PSBTMathScreen, PSBTAddressDetailsScreen, PSBTChangeDetailsScreen, PSBTFinalizeScreen
|
||||
from seedsigner.gui.screens.screen import (RET_CODE__BACK_BUTTON, ButtonListScreen, WarningScreen, DireWarningScreen, QRDisplayScreen)
|
||||
from seedsigner.gui.components import FontAwesomeIconConstants, SeedSignerIconConstants
|
||||
from seedsigner.gui.screens.screen import (RET_CODE__BACK_BUTTON, ButtonListScreen, ButtonOption, WarningScreen, DireWarningScreen, QRDisplayScreen)
|
||||
from seedsigner.views.view import BackStackView, MainMenuView, NotYetImplementedView, View, Destination
|
||||
|
||||
|
||||
|
||||
class PSBTSelectSeedView(View):
|
||||
SCAN_SEED = ("Scan a seed", SeedSignerIconConstants.QRCODE)
|
||||
TYPE_12WORD = ("Enter 12-word seed", FontAwesomeIconConstants.KEYBOARD)
|
||||
TYPE_24WORD = ("Enter 24-word seed", FontAwesomeIconConstants.KEYBOARD)
|
||||
TYPE_ELECTRUM = ("Enter Electrum seed", FontAwesomeIconConstants.KEYBOARD)
|
||||
SCAN_SEED = ButtonOption("Scan a seed", SeedSignerIconConstants.QRCODE)
|
||||
TYPE_12WORD = ButtonOption("Enter 12-word seed", FontAwesomeIconConstants.KEYBOARD)
|
||||
TYPE_24WORD = ButtonOption("Enter 24-word seed", FontAwesomeIconConstants.KEYBOARD)
|
||||
TYPE_ELECTRUM = ButtonOption("Enter Electrum seed", FontAwesomeIconConstants.KEYBOARD)
|
||||
|
||||
|
||||
def run(self):
|
||||
from seedsigner.controller import Controller
|
||||
|
||||
# Note: we can't just autoroute to the PSBT Overview because we might have a
|
||||
# multisig where we want to sign with more than one key on this device.
|
||||
if not self.controller.psbt:
|
||||
@@ -38,9 +35,10 @@ class PSBTSelectSeedView(View):
|
||||
button_str = seed.get_fingerprint(self.settings.get_value(SettingsConstants.SETTING__NETWORK))
|
||||
if not PSBTParser.has_matching_input_fingerprint(psbt=self.controller.psbt, seed=seed, network=self.settings.get_value(SettingsConstants.SETTING__NETWORK)):
|
||||
# Doesn't look like this seed can sign the current PSBT
|
||||
button_str += " (?)"
|
||||
# TRANSLATOR_NOTE: Inserts fingerprint w/"?" to indicate that this seed can't sign the current PSBT
|
||||
button_str = _("{} (?)").format(button_str)
|
||||
|
||||
button_data.append((button_str, SeedSignerIconConstants.FINGERPRINT))
|
||||
button_data.append(ButtonOption(button_str, SeedSignerIconConstants.FINGERPRINT))
|
||||
|
||||
button_data.append(self.SCAN_SEED)
|
||||
button_data.append(self.TYPE_12WORD)
|
||||
@@ -50,7 +48,7 @@ class PSBTSelectSeedView(View):
|
||||
|
||||
selected_menu_num = self.run_screen(
|
||||
ButtonListScreen,
|
||||
title="Select Signer",
|
||||
title=_("Select Signer"),
|
||||
is_button_text_centered=False,
|
||||
button_data=button_data
|
||||
)
|
||||
@@ -94,7 +92,7 @@ class PSBTOverviewView(View):
|
||||
# The PSBTParser takes a while to read the PSBT. Run the loading screen while
|
||||
# we wait.
|
||||
from seedsigner.gui.screens.screen import LoadingScreenThread
|
||||
self.loading_screen = LoadingScreenThread(text="Parsing PSBT...")
|
||||
self.loading_screen = LoadingScreenThread(text=_("Parsing PSBT..."))
|
||||
self.loading_screen.start()
|
||||
|
||||
try:
|
||||
@@ -109,6 +107,7 @@ class PSBTOverviewView(View):
|
||||
|
||||
|
||||
def run(self):
|
||||
from seedsigner.gui.screens.psbt_screens import PSBTOverviewScreen
|
||||
psbt_parser = self.controller.psbt_parser
|
||||
|
||||
change_data = psbt_parser.change_data
|
||||
@@ -168,9 +167,9 @@ class PSBTOverviewView(View):
|
||||
class PSBTUnsupportedScriptTypeWarningView(View):
|
||||
def run(self):
|
||||
selected_menu_num = WarningScreen(
|
||||
status_headline="Unsupported Script Type!",
|
||||
text="PSBT has unsupported input script type, please verify your change addresses.",
|
||||
button_data=["Continue"],
|
||||
status_headline=_("Unsupported Script Type!"),
|
||||
text=_("PSBT has unsupported input script type, please verify your change addresses."),
|
||||
button_data=[ButtonOption("Continue")],
|
||||
).display()
|
||||
|
||||
if selected_menu_num == RET_CODE__BACK_BUTTON:
|
||||
@@ -188,9 +187,10 @@ class PSBTUnsupportedScriptTypeWarningView(View):
|
||||
class PSBTNoChangeWarningView(View):
|
||||
def run(self):
|
||||
selected_menu_num = WarningScreen(
|
||||
status_headline="Full Spend!",
|
||||
text="This PSBT spends its entire input value. No change is coming back to your wallet.",
|
||||
button_data=["Continue"],
|
||||
# TRANSLATOR_NOTE: User will receive no change back; the inputs to this transaction are fully spent
|
||||
status_headline=_("Full Spend!"),
|
||||
text=_("This PSBT spends its entire input value. No change is coming back to your wallet."),
|
||||
button_data=[ButtonOption("Continue")],
|
||||
).display()
|
||||
|
||||
if selected_menu_num == RET_CODE__BACK_BUTTON:
|
||||
@@ -214,6 +214,7 @@ class PSBTMathView(View):
|
||||
+ change value
|
||||
"""
|
||||
def run(self):
|
||||
from seedsigner.gui.screens.psbt_screens import PSBTMathScreen
|
||||
psbt_parser: PSBTParser = self.controller.psbt_parser
|
||||
if not psbt_parser:
|
||||
# Should not be able to get here
|
||||
@@ -250,20 +251,24 @@ class PSBTAddressDetailsView(View):
|
||||
|
||||
|
||||
def run(self):
|
||||
from seedsigner.gui.screens.psbt_screens import PSBTAddressDetailsScreen
|
||||
psbt_parser: PSBTParser = self.controller.psbt_parser
|
||||
|
||||
if not psbt_parser:
|
||||
# Should not be able to get here
|
||||
raise Exception("Routing error")
|
||||
|
||||
title = "Will Send"
|
||||
# TRANSLATOR_NOTE: Future-tense used to indicate that this transaction will send this amount, as opposed to "Send" on its own which could be misread as an instant command (e.g. "Send Now").
|
||||
title = _("Will Send")
|
||||
if psbt_parser.num_destinations > 1:
|
||||
title += f" (#{self.address_num + 1})"
|
||||
|
||||
button_data = []
|
||||
if self.address_num < psbt_parser.num_destinations - 1:
|
||||
button_data = ["Next Recipient"]
|
||||
button_data.append(ButtonOption("Next Recipient"))
|
||||
else:
|
||||
button_data = ["Next"]
|
||||
# TRANSLATOR_NOTE: Short for "Next step"
|
||||
button_data.append(ButtonOption("Next"))
|
||||
|
||||
selected_menu_num = self.run_screen(
|
||||
PSBTAddressDetailsScreen,
|
||||
@@ -294,10 +299,9 @@ class PSBTAddressDetailsView(View):
|
||||
|
||||
|
||||
class PSBTChangeDetailsView(View):
|
||||
NEXT = "Next"
|
||||
SKIP_VERIFICATION = "Skip Verification"
|
||||
VERIFY_MULTISIG = "Verify Multisig Change"
|
||||
|
||||
NEXT = ButtonOption("Next")
|
||||
SKIP_VERIFICATION = ButtonOption("Skip Verification")
|
||||
VERIFY_MULTISIG = ButtonOption("Verify Multisig Change")
|
||||
|
||||
def __init__(self, change_address_num):
|
||||
super().__init__()
|
||||
@@ -305,6 +309,7 @@ class PSBTChangeDetailsView(View):
|
||||
|
||||
|
||||
def run(self):
|
||||
from seedsigner.gui.screens.psbt_screens import PSBTChangeDetailsScreen
|
||||
psbt_parser: PSBTParser = self.controller.psbt_parser
|
||||
|
||||
if not psbt_parser:
|
||||
@@ -339,11 +344,11 @@ class PSBTChangeDetailsView(View):
|
||||
derivation_path_addr_index = int(derivation_path.split("/")[-1])
|
||||
|
||||
if is_change_derivation_path:
|
||||
title = "Your Change"
|
||||
self.VERIFY_MULTISIG = "Verify Multisig Change"
|
||||
# TRANSLATOR_NOTE: The amount you're receiving back from the transaction
|
||||
title = _("Your Change")
|
||||
else:
|
||||
title = "Self-Transfer"
|
||||
self.VERIFY_MULTISIG = "Verify Multisig Addr"
|
||||
title = _("Self-Transfer")
|
||||
self.VERIFY_MULTISIG.button_label = _("Verify Multisig Addr")
|
||||
# if psbt_parser.num_change_outputs > 1:
|
||||
# title += f" (#{self.change_address_num + 1})"
|
||||
|
||||
@@ -361,10 +366,13 @@ class PSBTChangeDetailsView(View):
|
||||
else:
|
||||
# Single sig
|
||||
try:
|
||||
from embit import script
|
||||
from embit.networks import NETWORKS
|
||||
|
||||
if is_change_derivation_path:
|
||||
loading_screen_text = "Verifying Change..."
|
||||
loading_screen_text = _("Verifying Change...")
|
||||
else:
|
||||
loading_screen_text = "Verifying Self-Transfer..."
|
||||
loading_screen_text = _("Verifying Self-Transfer...")
|
||||
from seedsigner.gui.screens.screen import LoadingScreenThread
|
||||
loading_screen = LoadingScreenThread(text=loading_screen_text)
|
||||
loading_screen.start()
|
||||
@@ -436,6 +444,7 @@ class PSBTChangeDetailsView(View):
|
||||
return Destination(PSBTFinalizeView)
|
||||
|
||||
elif button_data[selected_menu_num] == self.VERIFY_MULTISIG:
|
||||
from seedsigner.controller import Controller
|
||||
from seedsigner.views.seed_views import LoadMultisigWalletDescriptorView
|
||||
self.controller.resume_main_flow = Controller.FLOW__PSBT
|
||||
return Destination(LoadMultisigWalletDescriptorView)
|
||||
@@ -451,17 +460,19 @@ class PSBTAddressVerificationFailedView(View):
|
||||
|
||||
def run(self):
|
||||
if self.is_multisig:
|
||||
title = "Caution"
|
||||
text = f"""PSBT's {"change" if self.is_change else "self-transfer"} address could not be verified with your multisig wallet descriptor."""
|
||||
title = _("Caution")
|
||||
# TRANSLATOR_NOTE: Variable is either "change" or "self-transfer".
|
||||
text = _("PSBT's {} address could not be verified with your multisig wallet descriptor.").format(_("change") if self.is_change else _("self-transfer"))
|
||||
else:
|
||||
title = "Suspicious PSBT"
|
||||
text = f"""PSBT's {"change" if self.is_change else "self-transfer"} address could not be generated from your seed."""
|
||||
title = _("Suspicious PSBT")
|
||||
# TRANSLATOR_NOTE: Variable is either "change" or "self-transfer".
|
||||
text = _("PSBT's {} address could not be generated from your seed.").format(_("change") if self.is_change else _("self-transfer"))
|
||||
|
||||
DireWarningScreen(
|
||||
title=title,
|
||||
status_headline="Address Verification Failed",
|
||||
status_headline=_("Address Verification Failed"),
|
||||
text=text,
|
||||
button_data=["Discard PSBT"],
|
||||
button_data=[ButtonOption("Discard PSBT")],
|
||||
show_back_button=False,
|
||||
).display()
|
||||
|
||||
@@ -476,14 +487,15 @@ class PSBTOpReturnView(View):
|
||||
Shows the OP_RETURN data
|
||||
"""
|
||||
def run(self):
|
||||
from seedsigner.gui.screens.psbt_screens import PSBTOpReturnScreen
|
||||
psbt_parser: PSBTParser = self.controller.psbt_parser
|
||||
|
||||
if not psbt_parser:
|
||||
# Should not be able to get here
|
||||
raise Exception("Routing error")
|
||||
|
||||
title = "OP_RETURN"
|
||||
button_data = ["Next"]
|
||||
title = _("OP_RETURN")
|
||||
button_data = [ButtonOption("Next")]
|
||||
|
||||
selected_menu_num = self.run_screen(
|
||||
PSBTOpReturnScreen,
|
||||
@@ -502,10 +514,13 @@ class PSBTOpReturnView(View):
|
||||
class PSBTFinalizeView(View):
|
||||
"""
|
||||
"""
|
||||
APPROVE_PSBT = "Approve PSBT"
|
||||
APPROVE_PSBT = ButtonOption("Approve PSBT")
|
||||
|
||||
|
||||
def run(self):
|
||||
from embit.psbt import PSBT
|
||||
from seedsigner.gui.screens.psbt_screens import PSBTFinalizeScreen
|
||||
|
||||
psbt_parser: PSBTParser = self.controller.psbt_parser
|
||||
psbt: PSBT = self.controller.psbt
|
||||
|
||||
@@ -541,6 +556,8 @@ class PSBTFinalizeView(View):
|
||||
|
||||
class PSBTSignedQRDisplayView(View):
|
||||
def run(self):
|
||||
from seedsigner.models.encode_qr import UrPsbtQrEncoder
|
||||
|
||||
qr_encoder = UrPsbtQrEncoder(
|
||||
psbt=self.controller.psbt,
|
||||
qr_density=self.settings.get_value(SettingsConstants.SETTING__QR_DENSITY),
|
||||
@@ -554,7 +571,7 @@ class PSBTSignedQRDisplayView(View):
|
||||
|
||||
|
||||
class PSBTSigningErrorView(View):
|
||||
SELECT_DIFF_SEED = "Select Diff Seed"
|
||||
SELECT_DIFF_SEED = ButtonOption("Select Diff Seed")
|
||||
|
||||
def run(self):
|
||||
psbt_parser: PSBTParser = self.controller.psbt_parser
|
||||
@@ -565,10 +582,10 @@ class PSBTSigningErrorView(View):
|
||||
# Just a WarningScreen here; only use DireWarningScreen for true security risks.
|
||||
selected_menu_num = self.run_screen(
|
||||
WarningScreen,
|
||||
title="PSBT Error",
|
||||
title=_("PSBT Error"),
|
||||
status_icon_name=SeedSignerIconConstants.WARNING,
|
||||
status_headline="Signing Failed",
|
||||
text="Signing with this seed did not add a valid signature.",
|
||||
status_headline=_("Signing Failed"),
|
||||
text=_("Signing with this seed did not add a valid signature."),
|
||||
button_data=[self.SELECT_DIFF_SEED]
|
||||
)
|
||||
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
import logging
|
||||
import re
|
||||
|
||||
from embit.descriptor import Descriptor
|
||||
|
||||
from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON
|
||||
from seedsigner.models.decode_qr import DecodeQR
|
||||
from seedsigner.models.seed import Seed
|
||||
from gettext import gettext as _
|
||||
from seedsigner.helpers.l10n import mark_for_translation as _mft
|
||||
from seedsigner.models.settings import SettingsConstants
|
||||
from seedsigner.views.settings_views import SettingsIngestSettingsQRView
|
||||
from seedsigner.views.view import BackStackView, ErrorView, MainMenuView, NotYetImplementedView, OptionDisabledView, View, Destination
|
||||
from seedsigner.views.view import BackStackView, ErrorView, MainMenuView, NotYetImplementedView, View, Destination
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
||||
class ScanView(View):
|
||||
"""
|
||||
The catch-all generic scanning View that will accept any of our supported QR
|
||||
@@ -22,11 +19,13 @@ class ScanView(View):
|
||||
dedicated errors when an unexpected QR type is scanned (e.g. Scan PSBT was
|
||||
selected but a SeedQR was scanned).
|
||||
"""
|
||||
instructions_text = "Scan a QR code"
|
||||
invalid_qr_type_message = "QRCode not recognized or not yet supported."
|
||||
instructions_text = _mft("Scan a QR code")
|
||||
invalid_qr_type_message = _mft("QRCode not recognized or not yet supported.")
|
||||
|
||||
|
||||
def __init__(self):
|
||||
from seedsigner.models.decode_qr import DecodeQR
|
||||
|
||||
super().__init__()
|
||||
# Define the decoder here to make it available to child classes' is_valid_qr_type
|
||||
# checks and so we can inject data into it in the test suite's `before_run()`.
|
||||
@@ -60,10 +59,11 @@ class ScanView(View):
|
||||
# current flow.
|
||||
# Report QR types in more human-readable text (e.g. QRType
|
||||
# `seed__compactseedqr` as "seed: compactseedqr").
|
||||
# TODO: cleanup l10n presentation
|
||||
return Destination(ErrorView, view_args=dict(
|
||||
title="Error",
|
||||
status_headline="Wrong QR Type",
|
||||
text=self.invalid_qr_type_message + f""", received "{self.decoder.qr_type.replace("__", ": ").replace("_", " ")}\" format""",
|
||||
status_headline=_("Wrong QR Type"),
|
||||
text=_(self.invalid_qr_type_message) + f""", received "{self.decoder.qr_type.replace("__", ": ").replace("_", " ")}\" format""",
|
||||
button_text="Back",
|
||||
next_destination=Destination(BackStackView, skip_current_view=True),
|
||||
))
|
||||
@@ -73,10 +73,11 @@ class ScanView(View):
|
||||
|
||||
if not seed_mnemonic:
|
||||
# seed is not valid, Exit if not valid with message
|
||||
raise Exception("Not yet implemented!")
|
||||
return Destination(NotYetImplementedView)
|
||||
else:
|
||||
# Found a valid mnemonic seed! All new seeds should be considered
|
||||
# pending (might set a passphrase, SeedXOR, etc) until finalized.
|
||||
from seedsigner.models.seed import Seed
|
||||
from .seed_views import SeedFinalizeView
|
||||
self.controller.storage.set_pending_seed(
|
||||
Seed(mnemonic=seed_mnemonic, wordlist_language_code=self.wordlist_language_code)
|
||||
@@ -95,10 +96,12 @@ class ScanView(View):
|
||||
return Destination(PSBTSelectSeedView, skip_current_view=True)
|
||||
|
||||
elif self.decoder.is_settings:
|
||||
from seedsigner.views.settings_views import SettingsIngestSettingsQRView
|
||||
data = self.decoder.get_settings_data()
|
||||
return Destination(SettingsIngestSettingsQRView, view_args=dict(data=data))
|
||||
|
||||
elif self.decoder.is_wallet_descriptor:
|
||||
from embit.descriptor import Descriptor
|
||||
from seedsigner.views.seed_views import MultisigWalletDescriptorView
|
||||
descriptor_str = self.decoder.get_wallet_descriptor()
|
||||
|
||||
@@ -161,10 +164,10 @@ class ScanView(View):
|
||||
# start everything over.
|
||||
self.controller.resume_main_flow = None
|
||||
return Destination(ErrorView, view_args=dict(
|
||||
title="Error",
|
||||
status_headline="Unknown QR Type",
|
||||
text="QRCode is invalid or is a data format not yet supported.",
|
||||
button_text="Done",
|
||||
title=_("Error"),
|
||||
status_headline=_("Unknown QR Type"),
|
||||
text=_("QRCode is invalid or is a data format not yet supported."),
|
||||
button_text=_("Done"),
|
||||
next_destination=Destination(MainMenuView, clear_history=True),
|
||||
))
|
||||
|
||||
@@ -173,8 +176,8 @@ class ScanView(View):
|
||||
|
||||
|
||||
class ScanPSBTView(ScanView):
|
||||
instructions_text = "Scan PSBT"
|
||||
invalid_qr_type_message = "Expected a PSBT"
|
||||
instructions_text = _mft("Scan PSBT")
|
||||
invalid_qr_type_message = _mft("Expected a PSBT")
|
||||
|
||||
@property
|
||||
def is_valid_qr_type(self):
|
||||
@@ -183,8 +186,8 @@ class ScanPSBTView(ScanView):
|
||||
|
||||
|
||||
class ScanSeedQRView(ScanView):
|
||||
instructions_text = "Scan SeedQR"
|
||||
invalid_qr_type_message = f"Expected a SeedQR"
|
||||
instructions_text = _mft("Scan SeedQR")
|
||||
invalid_qr_type_message = _mft("Expected a SeedQR")
|
||||
|
||||
@property
|
||||
def is_valid_qr_type(self):
|
||||
@@ -193,8 +196,8 @@ class ScanSeedQRView(ScanView):
|
||||
|
||||
|
||||
class ScanWalletDescriptorView(ScanView):
|
||||
instructions_text = "Scan descriptor"
|
||||
invalid_qr_type_message = "Expected a wallet descriptor QR"
|
||||
instructions_text = _mft("Scan descriptor")
|
||||
invalid_qr_type_message = _mft("Expected a wallet descriptor QR")
|
||||
|
||||
@property
|
||||
def is_valid_qr_type(self):
|
||||
@@ -203,8 +206,8 @@ class ScanWalletDescriptorView(ScanView):
|
||||
|
||||
|
||||
class ScanAddressView(ScanView):
|
||||
instructions_text = "Scan address QR"
|
||||
invalid_qr_type_message = "Expected an address QR"
|
||||
instructions_text = _mft("Scan address QR")
|
||||
invalid_qr_type_message = _mft("Expected an address QR")
|
||||
|
||||
@property
|
||||
def is_valid_qr_type(self):
|
||||
|
||||
@@ -3,16 +3,19 @@ import os
|
||||
import random
|
||||
import time
|
||||
|
||||
from PIL import Image
|
||||
from dataclasses import dataclass
|
||||
from gettext import gettext as _
|
||||
|
||||
from seedsigner.gui.components import Fonts, GUIConstants, load_image
|
||||
from seedsigner.gui.screens.screen import BaseScreen
|
||||
from seedsigner.models.settings import Settings
|
||||
from seedsigner.models.settings_definition import SettingsConstants
|
||||
from seedsigner.views.view import View
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
||||
# TODO: This early code is now outdated vis-a-vis Screen vs View distinctions
|
||||
class LogoScreen(BaseScreen):
|
||||
def __init__(self):
|
||||
@@ -29,49 +32,90 @@ class LogoScreen(BaseScreen):
|
||||
self.partner_logos[partner] = load_image(logo_url)
|
||||
|
||||
|
||||
def _run(self):
|
||||
pass
|
||||
|
||||
|
||||
def get_random_partner(self) -> str:
|
||||
return self.partners[random.randrange(len(self.partners))]
|
||||
|
||||
|
||||
|
||||
@dataclass
|
||||
class OpeningSplashView(View):
|
||||
is_screenshot_renderer: bool = False
|
||||
force_partner_logos: bool|None = None
|
||||
|
||||
def run(self):
|
||||
self.run_screen(
|
||||
OpeningSplashScreen,
|
||||
is_screenshot_renderer=self.is_screenshot_renderer,
|
||||
force_partner_logos=self.force_partner_logos
|
||||
)
|
||||
|
||||
|
||||
|
||||
class OpeningSplashScreen(LogoScreen):
|
||||
def start(self):
|
||||
def __init__(self, is_screenshot_renderer=False, force_partner_logos=None):
|
||||
self.is_screenshot_renderer = is_screenshot_renderer
|
||||
self.force_partner_logos = force_partner_logos
|
||||
super().__init__()
|
||||
|
||||
|
||||
def _render(self):
|
||||
from PIL import Image
|
||||
from seedsigner.controller import Controller
|
||||
controller = Controller.get_instance()
|
||||
|
||||
# TODO: Fix for the screenshot generator. When generating screenshots for
|
||||
# multiple locales, there is a button still in the canvas from the previous
|
||||
# screenshot, even though the Renderer has been reconfigured and re-
|
||||
# instantiated. This is a hack to clear the screen for now.
|
||||
self.clear_screen()
|
||||
|
||||
show_partner_logos = Settings.get_instance().get_value(SettingsConstants.SETTING__PARTNER_LOGOS) == SettingsConstants.OPTION__ENABLED
|
||||
if self.force_partner_logos is not None:
|
||||
show_partner_logos = self.force_partner_logos
|
||||
|
||||
if show_partner_logos:
|
||||
logo_offset_y = -56
|
||||
else:
|
||||
logo_offset_y = 0
|
||||
|
||||
# Fade in alpha
|
||||
for i in range(250, -1, -25):
|
||||
self.logo.putalpha(255 - i)
|
||||
background = Image.new("RGBA", size=self.logo.size, color="black")
|
||||
self.renderer.canvas.paste(Image.alpha_composite(background, self.logo), (0, logo_offset_y))
|
||||
self.renderer.show_image()
|
||||
background = Image.new("RGBA", size=self.logo.size, color="black")
|
||||
if not self.is_screenshot_renderer:
|
||||
# Fade in alpha
|
||||
for i in range(250, -1, -25):
|
||||
self.logo.putalpha(255 - i)
|
||||
self.renderer.canvas.paste(Image.alpha_composite(background, self.logo), (0, logo_offset_y))
|
||||
self.renderer.show_image()
|
||||
else:
|
||||
# Skip animation for the screenshot generator
|
||||
self.renderer.canvas.paste(self.logo, (0, logo_offset_y))
|
||||
|
||||
# Display version num below SeedSigner logo
|
||||
font = Fonts.get_font(GUIConstants.BODY_FONT_NAME, GUIConstants.TOP_NAV_TITLE_FONT_SIZE)
|
||||
font = Fonts.get_font(GUIConstants.get_body_font_name(), GUIConstants.get_top_nav_title_font_size())
|
||||
version = f"v{controller.VERSION}"
|
||||
(left, top, version_tw, version_th) = font.getbbox(version, anchor="lt")
|
||||
|
||||
# The logo png is 240x240, but the actual logo is 70px tall, vertically centered
|
||||
logo_height = 70
|
||||
version_x = int(self.renderer.canvas_width/2)
|
||||
version_y = int(self.canvas_height/2) + 35 + logo_offset_y + GUIConstants.COMPONENT_PADDING
|
||||
version_y = int(self.canvas_height/2) + int(logo_height/2) + logo_offset_y + GUIConstants.COMPONENT_PADDING
|
||||
self.renderer.draw.text(xy=(version_x, version_y), text=version, font=font, fill=GUIConstants.ACCENT_COLOR, anchor="mt")
|
||||
self.renderer.show_image()
|
||||
|
||||
if not self.is_screenshot_renderer:
|
||||
self.renderer.show_image()
|
||||
|
||||
if show_partner_logos:
|
||||
# Hold on the version num for a moment
|
||||
time.sleep(1)
|
||||
if not self.is_screenshot_renderer:
|
||||
# Hold on the version num for a moment
|
||||
time.sleep(1)
|
||||
|
||||
# Set up the partner logo
|
||||
partner_logo: Image.Image = self.partner_logos[self.get_random_partner()]
|
||||
font = Fonts.get_font(GUIConstants.TOP_NAV_TITLE_FONT_NAME, GUIConstants.BODY_FONT_SIZE)
|
||||
sponsor_text = "With support from:"
|
||||
font = Fonts.get_font(GUIConstants.get_top_nav_title_font_name(), GUIConstants.get_body_font_size())
|
||||
# TRANSLATOR_NOTE: This is on the opening splash screen, displayed above the HRF logo
|
||||
sponsor_text = _("With support from:")
|
||||
(left, top, tw, th) = font.getbbox(sponsor_text, anchor="lt")
|
||||
|
||||
x = int((self.renderer.canvas_width) / 2)
|
||||
@@ -87,12 +131,15 @@ class OpeningSplashScreen(LogoScreen):
|
||||
|
||||
self.renderer.show_image()
|
||||
|
||||
time.sleep(2)
|
||||
if not self.is_screenshot_renderer:
|
||||
# Hold on the splash screen for a moment
|
||||
time.sleep(2)
|
||||
|
||||
|
||||
|
||||
class ScreensaverScreen(LogoScreen):
|
||||
def __init__(self, buttons):
|
||||
from PIL import Image
|
||||
super().__init__()
|
||||
|
||||
self.buttons = buttons
|
||||
|
||||
+350
-288
File diff suppressed because it is too large
Load Diff
@@ -1,19 +1,20 @@
|
||||
import logging
|
||||
from gettext import gettext as _
|
||||
|
||||
from seedsigner.gui.components import SeedSignerIconConstants
|
||||
from seedsigner.hardware.microsd import MicroSD
|
||||
from seedsigner.gui.screens import (RET_CODE__BACK_BUTTON, ButtonListScreen, settings_screens)
|
||||
from seedsigner.gui.screens.screen import ButtonOption
|
||||
from seedsigner.models.settings import Settings, SettingsConstants, SettingsDefinition
|
||||
|
||||
from .view import View, Destination, MainMenuView
|
||||
|
||||
from seedsigner.gui.screens import (RET_CODE__BACK_BUTTON, ButtonListScreen, settings_screens)
|
||||
from seedsigner.models.settings import Settings, SettingsConstants, SettingsDefinition
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
||||
class SettingsMenuView(View):
|
||||
IO_TEST = "I/O test"
|
||||
DONATE = "Donate"
|
||||
IO_TEST = ButtonOption("I/O test")
|
||||
DONATE = ButtonOption("Donate")
|
||||
|
||||
def __init__(self, visibility: str = SettingsConstants.VISIBILITY__GENERAL, selected_attr: str = None, initial_scroll: int = 0):
|
||||
super().__init__()
|
||||
@@ -28,7 +29,7 @@ class SettingsMenuView(View):
|
||||
settings_entries = SettingsDefinition.get_settings_entries(
|
||||
visibility=self.visibility
|
||||
)
|
||||
button_data=[e.display_name for e in settings_entries]
|
||||
button_data=[ButtonOption(e.display_name) for e in settings_entries]
|
||||
|
||||
selected_button = 0
|
||||
if self.selected_attr:
|
||||
@@ -38,17 +39,17 @@ class SettingsMenuView(View):
|
||||
break
|
||||
|
||||
if self.visibility == SettingsConstants.VISIBILITY__GENERAL:
|
||||
title = "Settings"
|
||||
title = _("Settings")
|
||||
|
||||
# Set up the next nested level of menuing
|
||||
button_data.append(("Advanced", None, None, None, SeedSignerIconConstants.CHEVRON_RIGHT))
|
||||
button_data.append(ButtonOption("Advanced", right_icon_name=SeedSignerIconConstants.CHEVRON_RIGHT))
|
||||
next_destination = Destination(SettingsMenuView, view_args={"visibility": SettingsConstants.VISIBILITY__ADVANCED})
|
||||
|
||||
button_data.append(self.IO_TEST)
|
||||
button_data.append(self.DONATE)
|
||||
|
||||
elif self.visibility == SettingsConstants.VISIBILITY__ADVANCED:
|
||||
title = "Advanced"
|
||||
title = _("Advanced")
|
||||
|
||||
# So far there are no real Developer options; disabling for now
|
||||
# button_data.append(("Developer Options", None, None, None, SeedSignerIconConstants.CHEVRON_RIGHT))
|
||||
@@ -56,7 +57,7 @@ class SettingsMenuView(View):
|
||||
next_destination = None
|
||||
|
||||
elif self.visibility == SettingsConstants.VISIBILITY__DEVELOPER:
|
||||
title = "Dev Options"
|
||||
title = _("Dev Options")
|
||||
next_destination = None
|
||||
|
||||
selected_menu_num = self.run_screen(
|
||||
@@ -114,8 +115,7 @@ class SettingsEntryUpdateSelectionView(View):
|
||||
value, display_name = value
|
||||
else:
|
||||
display_name = value
|
||||
|
||||
button_data.append(display_name)
|
||||
button_data.append(ButtonOption(display_name))
|
||||
|
||||
if (type(initial_value) == list and value in initial_value) or value == initial_value:
|
||||
checked_buttons.append(i)
|
||||
@@ -191,6 +191,7 @@ class SettingsEntryUpdateSelectionView(View):
|
||||
|
||||
class SettingsIngestSettingsQRView(View):
|
||||
def __init__(self, data: str):
|
||||
from seedsigner.hardware.microsd import MicroSD
|
||||
super().__init__()
|
||||
|
||||
# May raise an Exception which will bubble up to the Controller to display to the
|
||||
@@ -200,15 +201,16 @@ class SettingsIngestSettingsQRView(View):
|
||||
self.settings.update(settings_update_dict)
|
||||
|
||||
if MicroSD.get_instance().is_inserted and self.settings.get_value(SettingsConstants.SETTING__PERSISTENT_SETTINGS) == SettingsConstants.OPTION__ENABLED:
|
||||
self.status_message = "Persistent Settings enabled. Settings saved to SD card."
|
||||
self.status_message = _("Persistent Settings enabled. Settings saved to SD card.")
|
||||
else:
|
||||
self.status_message = "Settings updated in temporary memory"
|
||||
self.status_message = _("Settings updated in temporary memory")
|
||||
|
||||
|
||||
def run(self):
|
||||
from seedsigner.gui.screens.settings_screens import SettingsQRConfirmationScreen
|
||||
self.run_screen(
|
||||
SettingsQRConfirmationScreen,
|
||||
title=_("Settings QR"),
|
||||
config_name=self.config_name,
|
||||
status_message=self.status_message,
|
||||
)
|
||||
|
||||
@@ -1,21 +1,14 @@
|
||||
from dataclasses import dataclass
|
||||
import hashlib
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
|
||||
from embit.descriptor import Descriptor
|
||||
from PIL import Image
|
||||
from PIL.ImageOps import autocontrast
|
||||
from gettext import gettext as _
|
||||
|
||||
from seedsigner.controller import Controller
|
||||
from seedsigner.gui.components import FontAwesomeIconConstants, GUIConstants, SeedSignerIconConstants
|
||||
from seedsigner.gui.screens import (RET_CODE__BACK_BUTTON, ButtonListScreen, WarningScreen)
|
||||
from seedsigner.gui.screens.tools_screens import (ToolsCalcFinalWordDoneScreen, ToolsCalcFinalWordFinalizePromptScreen,
|
||||
ToolsCalcFinalWordScreen, ToolsCoinFlipEntryScreen, ToolsDiceEntropyEntryScreen, ToolsImageEntropyFinalImageScreen,
|
||||
ToolsImageEntropyLivePreviewScreen, ToolsAddressExplorerAddressTypeScreen)
|
||||
from seedsigner.helpers import embit_utils, mnemonic_generation
|
||||
from seedsigner.models.encode_qr import GenericStaticQrEncoder
|
||||
from seedsigner.gui.screens import RET_CODE__BACK_BUTTON, ButtonListScreen
|
||||
from seedsigner.gui.screens.screen import ButtonOption
|
||||
from seedsigner.helpers import mnemonic_generation
|
||||
from seedsigner.models.seed import Seed
|
||||
from seedsigner.models.settings_definition import SettingsConstants
|
||||
from seedsigner.views.seed_views import SeedDiscardView, SeedFinalizeView, SeedMnemonicEntryView, SeedOptionsView, SeedWordsWarningView, SeedExportXpubScriptTypeView
|
||||
@@ -25,19 +18,20 @@ from .view import View, Destination, BackStackView
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
||||
class ToolsMenuView(View):
|
||||
IMAGE = (" New seed", FontAwesomeIconConstants.CAMERA)
|
||||
DICE = ("New seed", FontAwesomeIconConstants.DICE)
|
||||
KEYBOARD = ("Calc 12th/24th word", FontAwesomeIconConstants.KEYBOARD)
|
||||
ADDRESS_EXPLORER = "Address Explorer"
|
||||
VERIFY_ADDRESS = "Verify address"
|
||||
IMAGE = ButtonOption("New seed", FontAwesomeIconConstants.CAMERA)
|
||||
DICE = ButtonOption("New seed", FontAwesomeIconConstants.DICE)
|
||||
KEYBOARD = ButtonOption("Calc 12th/24th word", FontAwesomeIconConstants.KEYBOARD)
|
||||
ADDRESS_EXPLORER = ButtonOption("Address Explorer")
|
||||
VERIFY_ADDRESS = ButtonOption("Verify Address")
|
||||
|
||||
def run(self):
|
||||
button_data = [self.IMAGE, self.DICE, self.KEYBOARD, self.ADDRESS_EXPLORER, self.VERIFY_ADDRESS]
|
||||
|
||||
selected_menu_num = self.run_screen(
|
||||
ButtonListScreen,
|
||||
title="Tools",
|
||||
title=_("Tools"),
|
||||
is_button_text_centered=False,
|
||||
button_data=button_data
|
||||
)
|
||||
@@ -68,6 +62,7 @@ class ToolsMenuView(View):
|
||||
****************************************************************************"""
|
||||
class ToolsImageEntropyLivePreviewView(View):
|
||||
def run(self):
|
||||
from seedsigner.gui.screens.tools_screens import ToolsImageEntropyLivePreviewScreen
|
||||
self.controller.image_entropy_preview_frames = None
|
||||
ret = ToolsImageEntropyLivePreviewScreen().display()
|
||||
|
||||
@@ -81,6 +76,9 @@ class ToolsImageEntropyLivePreviewView(View):
|
||||
|
||||
class ToolsImageEntropyFinalImageView(View):
|
||||
def run(self):
|
||||
from PIL import Image
|
||||
from PIL.ImageOps import autocontrast
|
||||
from seedsigner.gui.screens.tools_screens import ToolsImageEntropyFinalImageScreen
|
||||
if not self.controller.image_entropy_final_image:
|
||||
from seedsigner.hardware.camera import Camera
|
||||
# Take the final full-res image
|
||||
@@ -99,7 +97,7 @@ class ToolsImageEntropyFinalImageView(View):
|
||||
).crop(
|
||||
(120, 0, 600, 480)
|
||||
).resize(
|
||||
(self.canvas_width, self.canvas_height), Image.BICUBIC
|
||||
(self.canvas_width, self.canvas_height), Image.Resampling.BICUBIC
|
||||
)
|
||||
|
||||
ret = ToolsImageEntropyFinalImageScreen(
|
||||
@@ -116,23 +114,21 @@ class ToolsImageEntropyFinalImageView(View):
|
||||
|
||||
|
||||
class ToolsImageEntropyMnemonicLengthView(View):
|
||||
TWELVE_WORDS = ButtonOption("12 words", return_data=12)
|
||||
TWENTYFOUR_WORDS = ButtonOption("24 words", return_data=24)
|
||||
|
||||
def run(self):
|
||||
TWELVE_WORDS = "12 words"
|
||||
TWENTYFOUR_WORDS = "24 words"
|
||||
button_data = [TWELVE_WORDS, TWENTYFOUR_WORDS]
|
||||
button_data = [self.TWELVE_WORDS, self.TWENTYFOUR_WORDS]
|
||||
|
||||
selected_menu_num = ButtonListScreen(
|
||||
title="Mnemonic Length?",
|
||||
title=_("Mnemonic Length?"),
|
||||
button_data=button_data,
|
||||
).display()
|
||||
|
||||
if selected_menu_num == RET_CODE__BACK_BUTTON:
|
||||
return Destination(BackStackView)
|
||||
|
||||
if button_data[selected_menu_num] == TWELVE_WORDS:
|
||||
mnemonic_length = 12
|
||||
else:
|
||||
mnemonic_length = 24
|
||||
mnemonic_length = button_data[selected_menu_num].return_data
|
||||
|
||||
preview_images = self.controller.image_entropy_preview_frames
|
||||
seed_entropy_image = self.controller.image_entropy_final_image
|
||||
@@ -189,12 +185,20 @@ class ToolsImageEntropyMnemonicLengthView(View):
|
||||
****************************************************************************"""
|
||||
class ToolsDiceEntropyMnemonicLengthView(View):
|
||||
def run(self):
|
||||
TWELVE = f"12 words ({mnemonic_generation.DICE__NUM_ROLLS__12WORD} rolls)"
|
||||
TWENTY_FOUR = f"24 words ({mnemonic_generation.DICE__NUM_ROLLS__24WORD} rolls)"
|
||||
|
||||
# Since we're dynamically building the ButtonOption button_labels here, it's too
|
||||
# awkward to use the usual class-level attr approach.
|
||||
|
||||
# TRANSLATOR_NOTE: Inserts the number of dice rolls needed for a 12-word mnemonic
|
||||
twelve = _("12 words ({} rolls)").format(mnemonic_generation.DICE__NUM_ROLLS__12WORD)
|
||||
TWELVE = ButtonOption(twelve, return_data=mnemonic_generation.DICE__NUM_ROLLS__12WORD)
|
||||
|
||||
# TRANSLATOR_NOTE: Inserts the number of dice rolls needed for a 24-word mnemonic
|
||||
twenty_four = _("24 words ({} rolls)").format(mnemonic_generation.DICE__NUM_ROLLS__24WORD)
|
||||
TWENTY_FOUR = ButtonOption(twenty_four, return_data=mnemonic_generation.DICE__NUM_ROLLS__24WORD)
|
||||
|
||||
button_data = [TWELVE, TWENTY_FOUR]
|
||||
selected_menu_num = ButtonListScreen(
|
||||
title="Mnemonic Length",
|
||||
title=_("Mnemonic Length"),
|
||||
is_bottom_list=True,
|
||||
is_button_text_centered=True,
|
||||
button_data=button_data,
|
||||
@@ -218,6 +222,7 @@ class ToolsDiceEntropyEntryView(View):
|
||||
|
||||
|
||||
def run(self):
|
||||
from seedsigner.gui.screens.tools_screens import ToolsDiceEntropyEntryScreen
|
||||
ret = ToolsDiceEntropyEntryScreen(
|
||||
return_after_n_chars=self.total_rolls,
|
||||
).display()
|
||||
@@ -240,15 +245,15 @@ class ToolsDiceEntropyEntryView(View):
|
||||
Calc final word Views
|
||||
****************************************************************************"""
|
||||
class ToolsCalcFinalWordNumWordsView(View):
|
||||
TWELVE = "12 words"
|
||||
TWENTY_FOUR = "24 words"
|
||||
TWELVE = ButtonOption("12 words", return_data=12)
|
||||
TWENTY_FOUR = ButtonOption("24 words", return_data=24)
|
||||
|
||||
def run(self):
|
||||
button_data = [self.TWELVE, self.TWENTY_FOUR]
|
||||
|
||||
selected_menu_num = self.run_screen(
|
||||
ButtonListScreen,
|
||||
title="Mnemonic Length",
|
||||
title=_("Mnemonic Length"),
|
||||
is_bottom_list=True,
|
||||
is_button_text_centered=True,
|
||||
button_data=button_data,
|
||||
@@ -257,22 +262,24 @@ class ToolsCalcFinalWordNumWordsView(View):
|
||||
if selected_menu_num == RET_CODE__BACK_BUTTON:
|
||||
return Destination(BackStackView)
|
||||
|
||||
elif button_data[selected_menu_num] == self.TWELVE:
|
||||
self.controller.storage.init_pending_mnemonic(12)
|
||||
self.controller.storage.init_pending_mnemonic(button_data[selected_menu_num].return_data)
|
||||
|
||||
# return Destination(SeedMnemonicEntryView, view_args=dict(is_calc_final_word=True))
|
||||
return Destination(SeedMnemonicEntryView, view_args=dict(is_calc_final_word=True))
|
||||
|
||||
elif button_data[selected_menu_num] == self.TWENTY_FOUR:
|
||||
self.controller.storage.init_pending_mnemonic(24)
|
||||
|
||||
# return Destination(SeedMnemonicEntryView, view_args=dict(is_calc_final_word=True))
|
||||
return Destination(SeedMnemonicEntryView, view_args=dict(is_calc_final_word=True))
|
||||
return Destination(SeedMnemonicEntryView, view_args=dict(is_calc_final_word=True))
|
||||
|
||||
|
||||
|
||||
class ToolsCalcFinalWordFinalizePromptView(View):
|
||||
# TRANSLATOR_NOTE: Label to gather entropy through coin tosses
|
||||
COIN_FLIPS = ButtonOption("Coin flip entropy")
|
||||
|
||||
# TRANSLATOR_NOTE: Label to gather entropy through user specified BIP-39 word
|
||||
SELECT_WORD = ButtonOption("Word selection entropy")
|
||||
|
||||
# TRANSLATOR_NOTE: Label to allow user to default entropy as all-zeros
|
||||
ZEROS = ButtonOption("Finalize with zeros")
|
||||
|
||||
def run(self):
|
||||
from seedsigner.gui.screens.tools_screens import ToolsCalcFinalWordFinalizePromptScreen
|
||||
mnemonic = self.controller.storage.pending_mnemonic
|
||||
mnemonic_length = len(mnemonic)
|
||||
if mnemonic_length == 12:
|
||||
@@ -280,11 +287,7 @@ class ToolsCalcFinalWordFinalizePromptView(View):
|
||||
else:
|
||||
num_entropy_bits = 3
|
||||
|
||||
COIN_FLIPS = "Coin flip entropy"
|
||||
SELECT_WORD = f"Word selection entropy"
|
||||
ZEROS = "Finalize with zeros"
|
||||
|
||||
button_data = [COIN_FLIPS, SELECT_WORD, ZEROS]
|
||||
button_data = [self.COIN_FLIPS, self.SELECT_WORD, self.ZEROS]
|
||||
selected_menu_num = ToolsCalcFinalWordFinalizePromptScreen(
|
||||
mnemonic_length=mnemonic_length,
|
||||
num_entropy_bits=num_entropy_bits,
|
||||
@@ -294,15 +297,15 @@ class ToolsCalcFinalWordFinalizePromptView(View):
|
||||
if selected_menu_num == RET_CODE__BACK_BUTTON:
|
||||
return Destination(BackStackView)
|
||||
|
||||
elif button_data[selected_menu_num] == COIN_FLIPS:
|
||||
elif button_data[selected_menu_num] == self.COIN_FLIPS:
|
||||
return Destination(ToolsCalcFinalWordCoinFlipsView)
|
||||
|
||||
elif button_data[selected_menu_num] == SELECT_WORD:
|
||||
elif button_data[selected_menu_num] == self.SELECT_WORD:
|
||||
# Clear the final word slot, just in case we're returning via BACK button
|
||||
self.controller.storage.update_pending_mnemonic(None, mnemonic_length - 1)
|
||||
return Destination(SeedMnemonicEntryView, view_args=dict(is_calc_final_word=True, cur_word_index=mnemonic_length - 1))
|
||||
|
||||
elif button_data[selected_menu_num] == ZEROS:
|
||||
elif button_data[selected_menu_num] == self.ZEROS:
|
||||
# User skipped the option to select a final word to provide last bits of
|
||||
# entropy. We'll insert all zeros and piggy-back on the coin flip attr
|
||||
wordlist_language_code = self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE)
|
||||
@@ -313,6 +316,7 @@ class ToolsCalcFinalWordFinalizePromptView(View):
|
||||
|
||||
class ToolsCalcFinalWordCoinFlipsView(View):
|
||||
def run(self):
|
||||
from seedsigner.gui.screens.tools_screens import ToolsCoinFlipEntryScreen
|
||||
mnemonic_length = len(self.controller.storage.pending_mnemonic)
|
||||
|
||||
if mnemonic_length == 12:
|
||||
@@ -333,6 +337,8 @@ class ToolsCalcFinalWordCoinFlipsView(View):
|
||||
|
||||
|
||||
class ToolsCalcFinalWordShowFinalWordView(View):
|
||||
NEXT = ButtonOption("Next")
|
||||
|
||||
def __init__(self, coin_flips: str = None):
|
||||
super().__init__()
|
||||
# Construct the actual final word. The user's selected_final_word
|
||||
@@ -385,11 +391,15 @@ class ToolsCalcFinalWordShowFinalWordView(View):
|
||||
|
||||
|
||||
def run(self):
|
||||
NEXT = "Next"
|
||||
button_data = [NEXT]
|
||||
from seedsigner.gui.screens.tools_screens import ToolsCalcFinalWordScreen
|
||||
button_data = [self.NEXT]
|
||||
|
||||
# TRANSLATOR_NOTE: label to calculate the last word of a BIP-39 mnemonic seed phrase
|
||||
title = _("Final Word Calc")
|
||||
|
||||
selected_menu_num = self.run_screen(
|
||||
ToolsCalcFinalWordScreen,
|
||||
title="Final Word Calc",
|
||||
title=title,
|
||||
button_data=button_data,
|
||||
selected_final_word=self.selected_final_word,
|
||||
selected_final_bits=self.selected_final_bits,
|
||||
@@ -400,20 +410,22 @@ class ToolsCalcFinalWordShowFinalWordView(View):
|
||||
if selected_menu_num == RET_CODE__BACK_BUTTON:
|
||||
return Destination(BackStackView)
|
||||
|
||||
elif button_data[selected_menu_num] == NEXT:
|
||||
elif button_data[selected_menu_num] == self.NEXT:
|
||||
return Destination(ToolsCalcFinalWordDoneView)
|
||||
|
||||
|
||||
|
||||
class ToolsCalcFinalWordDoneView(View):
|
||||
LOAD = ButtonOption("Load seed")
|
||||
DISCARD = ButtonOption("Discard", button_label_color="red")
|
||||
|
||||
def run(self):
|
||||
from seedsigner.gui.screens.tools_screens import ToolsCalcFinalWordDoneScreen
|
||||
mnemonic = self.controller.storage.pending_mnemonic
|
||||
mnemonic_word_length = len(mnemonic)
|
||||
final_word = mnemonic[-1]
|
||||
|
||||
LOAD = "Load seed"
|
||||
DISCARD = ("Discard", None, None, "red")
|
||||
button_data = [LOAD, DISCARD]
|
||||
button_data = [self.LOAD, self.DISCARD]
|
||||
|
||||
selected_menu_num = ToolsCalcFinalWordDoneScreen(
|
||||
final_word=final_word,
|
||||
@@ -427,37 +439,39 @@ class ToolsCalcFinalWordDoneView(View):
|
||||
|
||||
self.controller.storage.convert_pending_mnemonic_to_pending_seed()
|
||||
|
||||
if button_data[selected_menu_num] == LOAD:
|
||||
if button_data[selected_menu_num] == self.LOAD:
|
||||
return Destination(SeedFinalizeView)
|
||||
|
||||
elif button_data[selected_menu_num] == DISCARD:
|
||||
elif button_data[selected_menu_num] == self.DISCARD:
|
||||
return Destination(SeedDiscardView)
|
||||
|
||||
|
||||
|
||||
"""****************************************************************************
|
||||
Address Explorer Views
|
||||
****************************************************************************"""
|
||||
class ToolsAddressExplorerSelectSourceView(View):
|
||||
SCAN_SEED = ("Scan a seed", SeedSignerIconConstants.QRCODE)
|
||||
SCAN_DESCRIPTOR = ("Scan wallet descriptor", SeedSignerIconConstants.QRCODE)
|
||||
TYPE_12WORD = ("Enter 12-word seed", FontAwesomeIconConstants.KEYBOARD)
|
||||
TYPE_24WORD = ("Enter 24-word seed", FontAwesomeIconConstants.KEYBOARD)
|
||||
TYPE_ELECTRUM = ("Enter Electrum seed", FontAwesomeIconConstants.KEYBOARD)
|
||||
|
||||
SCAN_SEED = ButtonOption("Scan a seed", SeedSignerIconConstants.QRCODE)
|
||||
SCAN_DESCRIPTOR = ButtonOption("Scan wallet descriptor", SeedSignerIconConstants.QRCODE)
|
||||
TYPE_12WORD = ButtonOption("Enter 12-word seed", FontAwesomeIconConstants.KEYBOARD, return_data=12)
|
||||
TYPE_24WORD = ButtonOption("Enter 24-word seed", FontAwesomeIconConstants.KEYBOARD, return_data=24)
|
||||
TYPE_ELECTRUM = ButtonOption("Enter Electrum seed", FontAwesomeIconConstants.KEYBOARD)
|
||||
|
||||
def run(self):
|
||||
from seedsigner.controller import Controller
|
||||
|
||||
seeds = self.controller.storage.seeds
|
||||
button_data = []
|
||||
for seed in seeds:
|
||||
button_str = seed.get_fingerprint(self.settings.get_value(SettingsConstants.SETTING__NETWORK))
|
||||
button_data.append((button_str, SeedSignerIconConstants.FINGERPRINT))
|
||||
button_data.append(ButtonOption(button_str, SeedSignerIconConstants.FINGERPRINT))
|
||||
button_data = button_data + [self.SCAN_SEED, self.SCAN_DESCRIPTOR, self.TYPE_12WORD, self.TYPE_24WORD]
|
||||
if self.settings.get_value(SettingsConstants.SETTING__ELECTRUM_SEEDS) == SettingsConstants.OPTION__ENABLED:
|
||||
button_data.append(self.TYPE_ELECTRUM)
|
||||
|
||||
|
||||
selected_menu_num = self.run_screen(
|
||||
ButtonListScreen,
|
||||
title="Address Explorer",
|
||||
title=_("Address Explorer"),
|
||||
button_data=button_data,
|
||||
is_button_text_centered=False,
|
||||
is_bottom_list=True,
|
||||
@@ -491,10 +505,7 @@ class ToolsAddressExplorerSelectSourceView(View):
|
||||
|
||||
elif button_data[selected_menu_num] in [self.TYPE_12WORD, self.TYPE_24WORD]:
|
||||
from seedsigner.views.seed_views import SeedMnemonicEntryView
|
||||
if button_data[selected_menu_num] == self.TYPE_12WORD:
|
||||
self.controller.storage.init_pending_mnemonic(num_words=12)
|
||||
else:
|
||||
self.controller.storage.init_pending_mnemonic(num_words=24)
|
||||
self.controller.storage.init_pending_mnemonic(num_words=button_data[selected_menu_num].return_data)
|
||||
return Destination(SeedMnemonicEntryView)
|
||||
|
||||
elif button_data[selected_menu_num] == self.TYPE_ELECTRUM:
|
||||
@@ -504,8 +515,11 @@ class ToolsAddressExplorerSelectSourceView(View):
|
||||
|
||||
|
||||
class ToolsAddressExplorerAddressTypeView(View):
|
||||
RECEIVE = "Receive Addresses"
|
||||
CHANGE = "Change Addresses"
|
||||
# TRANSLATOR_NOTE: label for addresses where others send us incoming payments
|
||||
RECEIVE = ButtonOption("Receive Addresses")
|
||||
|
||||
# TRANSLATOR_NOTE: label for addresses that collect the change from our own outgoing payments
|
||||
CHANGE = ButtonOption("Change Addresses")
|
||||
|
||||
|
||||
def __init__(self, seed_num: int = None, script_type: str = None, custom_derivation: str = None):
|
||||
@@ -541,6 +555,7 @@ class ToolsAddressExplorerAddressTypeView(View):
|
||||
elif seed_derivation_override:
|
||||
derivation_path = seed_derivation_override
|
||||
else:
|
||||
from seedsigner.helpers import embit_utils
|
||||
derivation_path = embit_utils.get_standard_derivation_path(
|
||||
network=self.settings.get_value(SettingsConstants.SETTING__NETWORK),
|
||||
wallet_type=SettingsConstants.SINGLE_SIG,
|
||||
@@ -557,11 +572,13 @@ class ToolsAddressExplorerAddressTypeView(View):
|
||||
|
||||
|
||||
def run(self):
|
||||
from seedsigner.gui.screens.tools_screens import ToolsAddressExplorerAddressTypeScreen
|
||||
data = self.controller.address_explorer_data
|
||||
|
||||
wallet_descriptor_display_name = None
|
||||
if "wallet_descriptor" in data:
|
||||
wallet_descriptor_display_name = data["wallet_descriptor"].brief_policy.replace(" (sorted)", "")
|
||||
wallet_descriptor_display_name = " / ".join(wallet_descriptor_display_name.split(" of ")) # i18n w/o l10n since coming from non-l10n embit
|
||||
|
||||
script_type = data["script_type"] if "script_type" in data else None
|
||||
|
||||
@@ -620,7 +637,9 @@ class ToolsAddressExplorerAddressListView(View):
|
||||
else:
|
||||
try:
|
||||
from seedsigner.gui.screens.screen import LoadingScreenThread
|
||||
self.loading_screen = LoadingScreenThread(text="Calculating addrs...")
|
||||
from seedsigner.helpers import embit_utils
|
||||
# TRANSLATOR_NOTE: a status message that our payment addresses are being calculated
|
||||
self.loading_screen = LoadingScreenThread(text=_("Calculating addrs..."))
|
||||
self.loading_screen.start()
|
||||
|
||||
if addr_storage_key not in data:
|
||||
@@ -636,9 +655,10 @@ class ToolsAddressExplorerAddressListView(View):
|
||||
data[addr_storage_key].append(address)
|
||||
else:
|
||||
# TODO: Custom derivation path
|
||||
raise Exception("Custom Derivation address explorer not yet implemented")
|
||||
|
||||
raise Exception(_("Custom Derivation address explorer not yet implemented"))
|
||||
|
||||
elif "wallet_descriptor" in data:
|
||||
from embit.descriptor import Descriptor
|
||||
descriptor: Descriptor = data["wallet_descriptor"]
|
||||
if descriptor.is_basic_multisig:
|
||||
for i in range(self.start_index, self.start_index + addrs_per_screen):
|
||||
@@ -647,7 +667,7 @@ class ToolsAddressExplorerAddressListView(View):
|
||||
data[addr_storage_key].append(address)
|
||||
|
||||
else:
|
||||
raise Exception("Single sig descriptors not yet supported")
|
||||
raise Exception(_("Single sig descriptors not yet supported"))
|
||||
finally:
|
||||
# Everything is set. Stop the loading screen
|
||||
self.loading_screen.stop()
|
||||
@@ -663,16 +683,18 @@ class ToolsAddressExplorerAddressListView(View):
|
||||
end_digits = -5
|
||||
else:
|
||||
end_digits = -4
|
||||
button_data.append(f"{cur_index}:{address[:8]}...{address[end_digits:]}")
|
||||
button_data.append(ButtonOption(f"{cur_index}:{address[:8]}...{address[end_digits:]}", active_button_label=f"{cur_index}:{address}"))
|
||||
|
||||
button_data.append(("Next {}".format(addrs_per_screen), None, None, None, SeedSignerIconConstants.CHEVRON_RIGHT))
|
||||
# TRANSLATOR_NOTE: Insert the number of addrs displayed per screen (e.g. "Next 10")
|
||||
button_label = _("Next {}").format(addrs_per_screen)
|
||||
button_data.append(ButtonOption(button_label, right_icon_name=SeedSignerIconConstants.CHEVRON_RIGHT))
|
||||
|
||||
selected_menu_num = self.run_screen(
|
||||
ButtonListScreen,
|
||||
title="{} Addrs".format("Receive" if not self.is_change else "Change"),
|
||||
title=_("Receive Addrs") if not self.is_change else _("Change Addrs"),
|
||||
button_data=button_data,
|
||||
button_font_name=GUIConstants.FIXED_WIDTH_EMPHASIS_FONT_NAME,
|
||||
button_font_size=GUIConstants.BUTTON_FONT_SIZE + 4,
|
||||
button_font_size=GUIConstants.get_button_font_size() + 4,
|
||||
is_button_text_centered=False,
|
||||
is_bottom_list=True,
|
||||
selected_button=self.selected_button_index,
|
||||
@@ -695,6 +717,7 @@ class ToolsAddressExplorerAddressListView(View):
|
||||
|
||||
|
||||
class ToolsAddressExplorerAddressView(View):
|
||||
# TODO: pull address str from controller.address_explorer_data and pass addr_storage_key and addr_index instead
|
||||
def __init__(self, index: int, address: str, is_change: bool, start_index: int, parent_initial_scroll: int = 0):
|
||||
super().__init__()
|
||||
self.index = index
|
||||
@@ -706,6 +729,8 @@ class ToolsAddressExplorerAddressView(View):
|
||||
|
||||
def run(self):
|
||||
from seedsigner.gui.screens.screen import QRDisplayScreen
|
||||
from seedsigner.models.encode_qr import GenericStaticQrEncoder
|
||||
|
||||
qr_encoder = GenericStaticQrEncoder(data=self.address)
|
||||
self.run_screen(
|
||||
QRDisplayScreen,
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
from dataclasses import dataclass, field
|
||||
from dataclasses import dataclass
|
||||
from gettext import gettext as _
|
||||
from typing import Type
|
||||
|
||||
from seedsigner.gui.components import FontAwesomeIconConstants, SeedSignerIconConstants
|
||||
from seedsigner.helpers.l10n import mark_for_translation as _mft
|
||||
from seedsigner.gui.components import SeedSignerIconConstants
|
||||
from seedsigner.gui.screens import RET_CODE__POWER_BUTTON, RET_CODE__BACK_BUTTON
|
||||
from seedsigner.gui.screens.screen import BaseScreen, DireWarningScreen, LargeButtonScreen, PowerOffScreen, PowerOffNotRequiredScreen, ResetScreen, WarningScreen
|
||||
from seedsigner.gui.screens.screen import BaseScreen, ButtonOption, DireWarningScreen, LargeButtonScreen, PowerOffScreen, PowerOffNotRequiredScreen, ResetScreen, WarningScreen
|
||||
from seedsigner.models.settings import Settings, SettingsConstants
|
||||
from seedsigner.models.settings_definition import SettingsDefinition
|
||||
from seedsigner.models.threads import BaseThread
|
||||
@@ -17,6 +19,7 @@ class BackStackView:
|
||||
pass
|
||||
|
||||
|
||||
|
||||
"""
|
||||
Views contain the biz logic to handle discrete tasks, exactly analogous to a Flask
|
||||
request/response function or a Django View. Each page/screen displayed to the user
|
||||
@@ -181,18 +184,17 @@ class Destination:
|
||||
#
|
||||
#########################################################################################
|
||||
class MainMenuView(View):
|
||||
SCAN = ("Scan", SeedSignerIconConstants.SCAN)
|
||||
SEEDS = ("Seeds", SeedSignerIconConstants.SEEDS)
|
||||
TOOLS = ("Tools", SeedSignerIconConstants.TOOLS)
|
||||
SETTINGS = ("Settings", SeedSignerIconConstants.SETTINGS)
|
||||
|
||||
SCAN = ButtonOption("Scan", SeedSignerIconConstants.SCAN)
|
||||
SEEDS = ButtonOption("Seeds", SeedSignerIconConstants.SEEDS)
|
||||
TOOLS = ButtonOption("Tools", SeedSignerIconConstants.TOOLS)
|
||||
SETTINGS = ButtonOption("Settings", SeedSignerIconConstants.SETTINGS)
|
||||
|
||||
def run(self):
|
||||
from seedsigner.gui.screens.screen import MainMenuScreen
|
||||
button_data = [self.SCAN, self.SEEDS, self.TOOLS, self.SETTINGS]
|
||||
selected_menu_num = self.run_screen(
|
||||
MainMenuScreen,
|
||||
title="Home",
|
||||
title=_("Home"),
|
||||
button_data=button_data,
|
||||
)
|
||||
|
||||
@@ -218,14 +220,14 @@ class MainMenuView(View):
|
||||
|
||||
|
||||
class PowerOptionsView(View):
|
||||
RESET = ("Restart", SeedSignerIconConstants.RESTART)
|
||||
POWER_OFF = ("Power Off", SeedSignerIconConstants.POWER)
|
||||
RESET = ButtonOption("Restart", SeedSignerIconConstants.RESTART)
|
||||
POWER_OFF = ButtonOption("Power Off", SeedSignerIconConstants.POWER)
|
||||
|
||||
def run(self):
|
||||
button_data = [self.RESET, self.POWER_OFF]
|
||||
selected_menu_num = self.run_screen(
|
||||
LargeButtonScreen,
|
||||
title="Reset / Power",
|
||||
title=_("Reset / Power"),
|
||||
show_back_button=True,
|
||||
button_data=button_data
|
||||
)
|
||||
@@ -268,38 +270,26 @@ class RestartView(View):
|
||||
|
||||
class PowerOffView(View):
|
||||
def run(self):
|
||||
if Settings.HOSTNAME == Settings.SEEDSIGNER_OS:
|
||||
self.run_screen(PowerOffNotRequiredScreen)
|
||||
return Destination(BackStackView)
|
||||
else:
|
||||
thread = PowerOffView.PowerOffThread()
|
||||
thread.start()
|
||||
self.run_screen(PowerOffScreen)
|
||||
|
||||
|
||||
class PowerOffThread(BaseThread):
|
||||
def run(self):
|
||||
import time
|
||||
from subprocess import call
|
||||
while self.keep_running:
|
||||
time.sleep(5)
|
||||
call("sudo shutdown --poweroff now", shell=True)
|
||||
self.run_screen(PowerOffNotRequiredScreen)
|
||||
return Destination(BackStackView)
|
||||
|
||||
|
||||
|
||||
@dataclass
|
||||
class NotYetImplementedView(View):
|
||||
text: str = "This is still on our to-do list!"
|
||||
"""
|
||||
Temporary View to use during dev.
|
||||
"""
|
||||
text: str = _mft("This is still on our to-do list!")
|
||||
|
||||
|
||||
def run(self):
|
||||
self.run_screen(
|
||||
WarningScreen,
|
||||
title="Work In Progress",
|
||||
status_headline="Not Yet Implemented",
|
||||
title=_("Work In Progress"),
|
||||
status_headline=_("Not Yet Implemented"),
|
||||
text=self.text,
|
||||
button_data=["Back to Main Menu"],
|
||||
button_data=[ButtonOption("Back to Main Menu")],
|
||||
)
|
||||
|
||||
return Destination(MainMenuView)
|
||||
@@ -308,13 +298,12 @@ class NotYetImplementedView(View):
|
||||
|
||||
@dataclass
|
||||
class ErrorView(View):
|
||||
title: str = "Error"
|
||||
title: str = _mft("Error")
|
||||
show_back_button: bool = True
|
||||
status_headline: str = None
|
||||
text: str = None
|
||||
button_text: str = None
|
||||
next_destination: Destination = field(default_factory=lambda: Destination(MainMenuView, clear_history=True))
|
||||
|
||||
next_destination: Destination = None
|
||||
|
||||
def run(self):
|
||||
self.run_screen(
|
||||
@@ -322,30 +311,30 @@ class ErrorView(View):
|
||||
title=self.title,
|
||||
status_headline=self.status_headline,
|
||||
text=self.text,
|
||||
button_data=[self.button_text],
|
||||
button_data=[ButtonOption(self.button_text)],
|
||||
show_back_button=self.show_back_button,
|
||||
)
|
||||
|
||||
return self.next_destination
|
||||
return self.next_destination if self.next_destination else Destination(MainMenuView, clear_history=True)
|
||||
|
||||
|
||||
|
||||
@dataclass
|
||||
class NetworkMismatchErrorView(ErrorView):
|
||||
title: str = "Network Mismatch"
|
||||
show_back_button: bool = False
|
||||
button_text: str = "Change Setting"
|
||||
next_destination: Destination = None
|
||||
|
||||
derivation_path: str = None
|
||||
|
||||
def __post_init__(self):
|
||||
from seedsigner.views.settings_views import SettingsEntryUpdateSelectionView
|
||||
self.title: str = _("Network Mismatch")
|
||||
self.show_back_button: bool = False
|
||||
self.button_text: str = _("Change Setting")
|
||||
self.next_destination = Destination(SettingsEntryUpdateSelectionView, view_args=dict(attr_name=SettingsConstants.SETTING__NETWORK), clear_history=True)
|
||||
super().__post_init__()
|
||||
if not self.text:
|
||||
self.text = f"Current network setting ({self.settings.get_value_display_name(SettingsConstants.SETTING__NETWORK)}) doesn't match current action."
|
||||
|
||||
if not self.next_destination:
|
||||
from seedsigner.views.settings_views import SettingsEntryUpdateSelectionView
|
||||
self.next_destination = Destination(SettingsEntryUpdateSelectionView, view_args=dict(attr_name=SettingsConstants.SETTING__NETWORK), clear_history=True)
|
||||
# TRANSLATOR_NOTE: Inserts mainnet/testnet/regtest and derivation path
|
||||
self.text = _("Current network setting ({}) doesn't match {}.").format(
|
||||
self.settings.get_value_display_name(SettingsConstants.SETTING__NETWORK),
|
||||
self.derivation_path,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -357,11 +346,9 @@ class UnhandledExceptionView(View):
|
||||
def run(self):
|
||||
self.run_screen(
|
||||
DireWarningScreen,
|
||||
title="System Error",
|
||||
title=_("System Error"),
|
||||
status_headline=self.error[0],
|
||||
text=self.error[1] + "\n" + self.error[2],
|
||||
button_data=["OK"],
|
||||
show_back_button=False,
|
||||
allow_text_overflow=True, # Fit what we can, let the rest go off the edges
|
||||
)
|
||||
|
||||
@@ -371,21 +358,25 @@ class UnhandledExceptionView(View):
|
||||
|
||||
@dataclass
|
||||
class OptionDisabledView(View):
|
||||
UPDATE_SETTING = "Update Setting"
|
||||
DONE = "Done"
|
||||
UPDATE_SETTING = ButtonOption("Update Setting")
|
||||
DONE = ButtonOption("Done")
|
||||
settings_attr: str
|
||||
|
||||
def __post_init__(self):
|
||||
super().__post_init__()
|
||||
self.settings_entry = SettingsDefinition.get_settings_entry(self.settings_attr)
|
||||
self.error_msg = f"\"{self.settings_entry.display_name}\" is currently disabled in Settings."
|
||||
|
||||
# TRANSLATOR_NOTE: Inserts the name of a settings option (e.g. "Persistent Settings" is currently...)
|
||||
self.error_msg = _("\"{}\" is currently disabled in Settings.").format(
|
||||
_(self.settings_entry.display_name),
|
||||
)
|
||||
|
||||
|
||||
def run(self):
|
||||
button_data = [self.UPDATE_SETTING, self.DONE]
|
||||
selected_menu_num = self.run_screen(
|
||||
WarningScreen,
|
||||
title="Option Disabled",
|
||||
title=_("Option Disabled"),
|
||||
status_headline=None,
|
||||
text=self.error_msg,
|
||||
button_data=button_data,
|
||||
@@ -398,26 +389,3 @@ class OptionDisabledView(View):
|
||||
return Destination(SettingsEntryUpdateSelectionView, view_args=dict(attr_name=self.settings_attr), clear_history=True)
|
||||
else:
|
||||
return Destination(MainMenuView, clear_history=True)
|
||||
|
||||
|
||||
|
||||
class RemoveMicroSDWarningView(View):
|
||||
"""
|
||||
Warning to remove the microsd
|
||||
"""
|
||||
def __init__(self, next_view: View):
|
||||
super().__init__()
|
||||
self.next_view = next_view
|
||||
|
||||
def run(self):
|
||||
self.run_screen(
|
||||
WarningScreen,
|
||||
title="Security Tip",
|
||||
status_icon_name=FontAwesomeIconConstants.SDCARD,
|
||||
status_headline="",
|
||||
text="For maximum security,\nremove the MicroSD card\nbefore continuing.",
|
||||
show_back_button=False,
|
||||
button_data=["Continue"],
|
||||
)
|
||||
|
||||
return Destination(self.next_view, clear_history=True)
|
||||
|
||||
+27
-6
@@ -2,6 +2,7 @@
|
||||
|
||||
The tests are designed to be run on non-Raspi hardware.
|
||||
|
||||
## Setup
|
||||
On your testing machine you'll have to install:
|
||||
```bash
|
||||
# general dependencies
|
||||
@@ -16,6 +17,14 @@ Then make the `seedsigner` python module visible/importable to the tests by inst
|
||||
pip3 install -e .
|
||||
```
|
||||
|
||||
## Running all tests, calculating overall test coverage
|
||||
tldr: just run the convenience script from the project root:
|
||||
|
||||
```bash
|
||||
./tests/run_full_coverage.sh
|
||||
```
|
||||
|
||||
## Running tests manually
|
||||
Run the whole test suite:
|
||||
```
|
||||
pytest
|
||||
@@ -45,18 +54,30 @@ Annoying complications:
|
||||
* Better idea: use a proper logger in the test file and use one of the above options to display logs
|
||||
|
||||
|
||||
### Test Coverage
|
||||
## Screenshot generator
|
||||
The screenshot generator is meant to mostly be a utility and not really part of the test suite. However,
|
||||
it is actually implemented to be run by `pytest`.
|
||||
|
||||
see: [Screenshot generator README](screenshot_generator/README.md)
|
||||
|
||||
|
||||
## Generate coverage manually
|
||||
Run tests and generate test coverage
|
||||
```
|
||||
```bash
|
||||
coverage run -m pytest
|
||||
```
|
||||
|
||||
Show the resulting test coverage details:
|
||||
The screenshots can generate their own separate coverage report:
|
||||
```bash
|
||||
coverage run -m pytest tests/screenshot_generator/generator.py --locale es
|
||||
```
|
||||
|
||||
Show the resulting test coverage details:
|
||||
```bash
|
||||
coverage report
|
||||
```
|
||||
|
||||
Generate the html overview:
|
||||
```
|
||||
Generate the interactive html report:
|
||||
```bash
|
||||
coverage html
|
||||
```
|
||||
```
|
||||
Executable
+20
@@ -0,0 +1,20 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Clean up any prior coverage results
|
||||
coverage erase
|
||||
|
||||
# Run the full test suite; `--parallel` writes coverage results to a separate file
|
||||
# (otherwise it'll be overwritten in the next step)
|
||||
coverage run --parallel -m pytest
|
||||
|
||||
# Generate screenshots (only need to run for one locale to assess coverage)
|
||||
coverage run --parallel -m pytest tests/screenshot_generator/generator.py --locale es
|
||||
|
||||
# Combine the above coverage results
|
||||
coverage combine
|
||||
|
||||
# Show the report in the terminal
|
||||
coverage report
|
||||
|
||||
# Generate the interactive html report
|
||||
coverage html
|
||||
@@ -1,8 +1,21 @@
|
||||
# Screenshot Generator
|
||||
|
||||
From the project root, run:
|
||||
```
|
||||
```bash
|
||||
# Generate screenshots for a specific locale
|
||||
pytest tests/screenshot_generator/generator.py --locale es
|
||||
|
||||
# Generate screenshots for all supported locales
|
||||
pytest tests/screenshot_generator/generator.py
|
||||
```
|
||||
|
||||
You can also run a `coverage` report to see exactly what the screenshots are and are not hitting:
|
||||
```bash
|
||||
coverage erase
|
||||
coverage run -m pytest tests/screenshot_generator/generator.py --locale es && coverage combine && coverage report
|
||||
|
||||
# Generate the interactive html report
|
||||
coverage html
|
||||
```
|
||||
|
||||
Writes the screenshots to a dir in the project root: `seedsigner-screenshots`.
|
||||
|
||||
@@ -1,48 +1,113 @@
|
||||
import embit
|
||||
import pathlib
|
||||
import pytest
|
||||
import os
|
||||
import random
|
||||
import sys
|
||||
import time
|
||||
from unittest.mock import Mock, patch, MagicMock
|
||||
from seedsigner.helpers import embit_utils
|
||||
|
||||
from embit import compact
|
||||
from embit.psbt import PSBT, OutputScope
|
||||
from embit.script import Script
|
||||
|
||||
from seedsigner.helpers import embit_utils
|
||||
from seedsigner.models.psbt_parser import OPCODES, PSBTParser
|
||||
|
||||
|
||||
# Prevent importing modules w/Raspi hardware dependencies.
|
||||
# These must precede any SeedSigner imports.
|
||||
sys.modules['seedsigner.hardware.ST7789'] = MagicMock()
|
||||
sys.modules['seedsigner.gui.screens.screensaver'] = MagicMock()
|
||||
sys.modules['seedsigner.views.screensaver'] = MagicMock()
|
||||
sys.modules['seedsigner.views.screensaver.ScreensaverScreen'] = MagicMock()
|
||||
sys.modules['RPi'] = MagicMock()
|
||||
sys.modules['RPi.GPIO'] = MagicMock()
|
||||
sys.modules['seedsigner.hardware.camera'] = MagicMock()
|
||||
sys.modules['seedsigner.hardware.microsd'] = MagicMock()
|
||||
|
||||
# Force the screenshots to mimic Pi Zero's output without libraqm
|
||||
patch('PIL.ImageFont.core.HAVE_RAQM', False).start()
|
||||
|
||||
from seedsigner.controller import Controller
|
||||
from seedsigner.gui.renderer import Renderer
|
||||
from seedsigner.gui.screens.seed_screens import SeedAddPassphraseScreen
|
||||
from seedsigner.gui.toast import BaseToastOverlayManagerThread, RemoveSDCardToastManagerThread, SDCardStateChangeToastManagerThread
|
||||
from seedsigner.gui.toast import DefaultToast, InfoToast, SuccessToast, WarningToast, ErrorToast, DireWarningToast
|
||||
from seedsigner.hardware.microsd import MicroSD
|
||||
from seedsigner.helpers import embit_utils
|
||||
from seedsigner.models.decode_qr import DecodeQR
|
||||
from seedsigner.models.psbt_parser import OPCODES, PSBTParser
|
||||
from seedsigner.models.qr_type import QRType
|
||||
from seedsigner.models.seed import Seed
|
||||
from seedsigner.models.settings import Settings
|
||||
from seedsigner.models.settings_definition import SettingsConstants, SettingsDefinition
|
||||
from seedsigner.views import (MainMenuView, PowerOptionsView, RestartView, NotYetImplementedView, UnhandledExceptionView,
|
||||
psbt_views, seed_views, settings_views, tools_views)
|
||||
from seedsigner.views.screensaver import OpeningSplashView
|
||||
from seedsigner.views.view import ErrorView, NetworkMismatchErrorView, OptionDisabledView, PowerOffView, View
|
||||
|
||||
from .utils import ScreenshotComplete, ScreenshotRenderer
|
||||
from .utils import ScreenshotComplete, ScreenshotConfig, ScreenshotRenderer
|
||||
|
||||
import warnings; warnings.warn = lambda *args, **kwargs: None
|
||||
|
||||
|
||||
|
||||
def test_generate_screenshots(target_locale):
|
||||
# Dynamically generate a pytest test run for each locale
|
||||
@pytest.mark.parametrize("locale", [x for x, y in SettingsConstants.ALL_LOCALES])
|
||||
def test_generate_all(locale, target_locale):
|
||||
"""
|
||||
`target_locale` is a fixture created in conftest.py via the `--locale` command line arg.
|
||||
|
||||
Optionally skips all other locales.
|
||||
"""
|
||||
if target_locale and locale != target_locale:
|
||||
pytest.skip(f"Skipping {locale}")
|
||||
|
||||
generate_screenshots(locale)
|
||||
|
||||
|
||||
|
||||
"""**************************************************************************************
|
||||
Set up global test data that will be re-used across a variety of screenshots and for
|
||||
all locales.
|
||||
**************************************************************************************"""
|
||||
BASE64_PSBT_1 = """cHNidP8BAP06AQIAAAAC5l4E3oEjI+H0im8t/K2nLmF5iJFdKEiuQs8ESveWJKcAAAAAAP3///8iBZMRhYIq4s/LmnTmKBi79M8ITirmsbO++63evK4utwAAAAAA/f///wZYQuoDAAAAACIAIAW5jm3UnC5fyjKCUZ8LTzjENtb/ioRTaBMXeSXsB3n+bK2fCgAAAAAWABReJY7akT1+d+jx475yBRWORdBd7VxbUgUAAAAAFgAU4wj9I/jB3GjNQudNZAca+7g9R16iWtYOAAAAABYAFIotPApLZlfscg8f3ppKqO3qA5nv7BnMFAAAAAAiACAs6SGc8qv4FwuNl0G0SpMZG8ODUEk5RXiWUcuzzw5iaRSfAhMAAAAAIgAgW0f5QxQIgVCGQqKzsvfkXZjUxdFop5sfez6Pt8mUbmZ1AgAAAAEAkgIAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////BQIRAgEB/////wJAvkAlAAAAACIAIIRPoo2LvkrwrhrYFhLhlP43izxbA4Eo6Y6iFFiQYdXRAAAAAAAAAAAmaiSqIant4vYcP3HR3v0/qZnfo2lTdVxpBol5mWK0i+vYNpdOjPkAAAAAAQErQL5AJQAAAAAiACCET6KNi75K8K4a2BYS4ZT+N4s8WwOBKOmOohRYkGHV0QEFR1EhArGhNdUqlR4BAOLGTMrY2ZJYTQNRudp7fU7i8crRJqgEIQNDxn7PjUzvsP6KYw4s7dmoZE0qO1K6MaM+2ScRZ7hyxFKuIgYCsaE11SqVHgEA4sZMytjZklhNA1G52nt9TuLxytEmqAQcc8XaCjAAAIABAACAAAAAgAIAAIAAAAAAAwAAACIGA0PGfs+NTO+w/opjDizt2ahkTSo7Uroxoz7ZJxFnuHLEHCK94akwAACAAQAAgAAAAIACAACAAAAAAAMAAAAAAQCSAgAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP////8FAhACAQH/////AkC+QCUAAAAAIgAghE+ijYu+SvCuGtgWEuGU/jeLPFsDgSjpjqIUWJBh1dEAAAAAAAAAACZqJKohqe3i9hw/cdHe/T+pmd+jaVN1XGkGiXmZYrSL69g2l06M+QAAAAABAStAvkAlAAAAACIAIIRPoo2LvkrwrhrYFhLhlP43izxbA4Eo6Y6iFFiQYdXRAQVHUSECsaE11SqVHgEA4sZMytjZklhNA1G52nt9TuLxytEmqAQhA0PGfs+NTO+w/opjDizt2ahkTSo7Uroxoz7ZJxFnuHLEUq4iBgKxoTXVKpUeAQDixkzK2NmSWE0DUbnae31O4vHK0SaoBBxzxdoKMAAAgAEAAIAAAACAAgAAgAAAAAADAAAAIgYDQ8Z+z41M77D+imMOLO3ZqGRNKjtSujGjPtknEWe4csQcIr3hqTAAAIABAACAAAAAgAIAAIAAAAAAAwAAAAABAUdRIQJ5XLCBS0hdo4NANq4lNhimzhyHj7dvObmPAwNj8L2xASEC9mwwoH28/WHnxbb6z05sJ/lHuvrLs/wOooHgFn5ulI1SriICAnlcsIFLSF2jg0A2riU2GKbOHIePt285uY8DA2PwvbEBHCK94akwAACAAQAAgAAAAIACAACAAQAAAAEAAAAiAgL2bDCgfbz9YefFtvrPTmwn+Ue6+suz/A6igeAWfm6UjRxzxdoKMAAAgAEAAIAAAACAAgAAgAEAAAABAAAAAAAAAAEBR1EhAgpbWcEh7rgvRE5UaCcqzWL/TR1B/DS8UeZsKVEvuKLrIQOwLg0emiQbbxafIh69Xjtpj4eclsMhKq1y/7vYDdE7LVKuIgICCltZwSHuuC9ETlRoJyrNYv9NHUH8NLxR5mwpUS+4ouscc8XaCjAAAIABAACAAAAAgAIAAIAAAAAABQAAACICA7AuDR6aJBtvFp8iHr1eO2mPh5yWwyEqrXL/u9gN0TstHCK94akwAACAAQAAgAAAAIACAACAAAAAAAUAAAAAAQFHUSECk50GLh/YhZaLJkDq/dugU3H/WvE6rTgQuY6N57pI4ykhA/H8MdLVP9SA/Hg8l3hvibSaC1bCBzwz7kTW+rsEZ8uFUq4iAgKTnQYuH9iFlosmQOr926BTcf9a8TqtOBC5jo3nukjjKRxzxdoKMAAAgAEAAIAAAACAAgAAgAAAAAAGAAAAIgID8fwx0tU/1ID8eDyXeG+JtJoLVsIHPDPuRNb6uwRny4UcIr3hqTAAAIABAACAAAAAgAIAAIAAAAAABgAAAAA="""
|
||||
mnemonic_12b = ["abandon"] * 11 + ["about"]
|
||||
seed_12b = Seed(mnemonic=mnemonic_12b, wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH)
|
||||
|
||||
def add_op_return_to_psbt(psbt: PSBT, raw_payload_data: bytes):
|
||||
data = (compact.to_bytes(OPCODES.OP_RETURN) +
|
||||
compact.to_bytes(OPCODES.OP_PUSHDATA1) +
|
||||
compact.to_bytes(len(raw_payload_data)) +
|
||||
raw_payload_data)
|
||||
script = Script(data)
|
||||
output = OutputScope()
|
||||
output.script_pubkey = script
|
||||
output.value = 0
|
||||
psbt.outputs.append(output)
|
||||
return psbt.to_string()
|
||||
|
||||
# Prep a PSBT with a human-readable OP_RETURN
|
||||
raw_payload_data = "Chancellor on the brink of third bailout for banks".encode()
|
||||
psbt = PSBT.from_base64(BASE64_PSBT_1)
|
||||
|
||||
# Simplify the output side
|
||||
output = psbt.outputs[-1]
|
||||
psbt.outputs.clear()
|
||||
psbt.outputs.append(output)
|
||||
assert len(psbt.outputs) == 1
|
||||
BASE64_PSBT_WITH_OP_RETURN_TEXT = add_op_return_to_psbt(psbt, raw_payload_data)
|
||||
|
||||
# Prep a PSBT with a (repeatably) random 80-byte OP_RETURN
|
||||
random.seed(6102)
|
||||
BASE64_PSBT_WITH_OP_RETURN_RAW_BYTES = add_op_return_to_psbt(PSBT.from_base64(BASE64_PSBT_1), random.randbytes(80))
|
||||
|
||||
mnemonic_12 = "forum undo fragile fade shy sign arrest garment culture tube off merit".split()
|
||||
mnemonic_24 = "attack pizza motion avocado network gather crop fresh patrol unusual wild holiday candy pony ranch winter theme error hybrid van cereal salon goddess expire".split()
|
||||
seed_12 = Seed(mnemonic=mnemonic_12, passphrase="cap*BRACKET3stove", wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH)
|
||||
seed_24 = Seed(mnemonic=mnemonic_24, passphrase="some-PASS*phrase9", wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH)
|
||||
seed_24_w_passphrase = Seed(mnemonic=mnemonic_24, passphrase="some-PASS*phrase9", wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH)
|
||||
|
||||
MULTISIG_WALLET_DESCRIPTOR = """wsh(sortedmulti(1,[22bde1a9/48h/1h/0h/2h]tpubDFfsBrmpj226ZYiRszYi2qK6iGvh2vkkghfGB2YiRUVY4rqqedHCFEgw12FwDkm7rUoVtq9wLTKc6BN2sxswvQeQgp7m8st4FP8WtP8go76/{0,1}/*,[73c5da0a/48h/1h/0h/2h]tpubDFH9dgzveyD8zTbPUFuLrGmCydNvxehyNdUXKJAQN8x4aZ4j6UZqGfnqFrD4NqyaTVGKbvEW54tsvPTK2UoSbCC1PJY8iCNiwTL3RWZEheQ/{0,1}/*))#3jhtf6yx"""
|
||||
|
||||
|
||||
|
||||
def generate_screenshots(locale):
|
||||
"""
|
||||
The `Renderer` class is mocked so that calls in the normal code are ignored
|
||||
(necessary to avoid having it trying to wire up hardware dependencies).
|
||||
@@ -59,329 +124,364 @@ def test_generate_screenshots(target_locale):
|
||||
Renderer.configure_instance = Mock()
|
||||
Renderer.get_instance = Mock(return_value=screenshot_renderer)
|
||||
|
||||
# Additional mocks needed
|
||||
PowerOffView.PowerOffThread = Mock() # Don't let this View actually send the `shutdown` command!
|
||||
|
||||
controller = Controller.get_instance()
|
||||
def setup_screenshots(locale: str) -> dict[str, list[ScreenshotConfig]]:
|
||||
""" Set up some test data that we'll need in the `Controller` for certain Views """
|
||||
# Must reset the Controller so each locale gets a fresh start
|
||||
Controller.reset_instance()
|
||||
controller = Controller.get_instance()
|
||||
|
||||
# Set up some test data that we'll need in the `Controller` for certain Views
|
||||
mnemonic_12 = "forum undo fragile fade shy sign arrest garment culture tube off merit".split()
|
||||
mnemonic_24 = "attack pizza motion avocado network gather crop fresh patrol unusual wild holiday candy pony ranch winter theme error hybrid van cereal salon goddess expire".split()
|
||||
mnemonic_12b = ["abandon"] * 11 + ["about"]
|
||||
seed_12 = Seed(mnemonic=mnemonic_12, passphrase="cap*BRACKET3stove", wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH)
|
||||
seed_12b = Seed(mnemonic=mnemonic_12b, wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH)
|
||||
seed_24 = Seed(mnemonic=mnemonic_24, wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH)
|
||||
seed_24_w_passphrase = Seed(mnemonic=mnemonic_24, passphrase="some-PASS*phrase9", wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH)
|
||||
controller.storage.seeds.append(seed_12)
|
||||
controller.storage.seeds.append(seed_12b)
|
||||
controller.storage.seeds.append(seed_24)
|
||||
controller.storage.set_pending_seed(seed_24_w_passphrase)
|
||||
UnhandledExceptionViewFood = ["IndexError", "line 1, in some_buggy_code.py", "list index out of range"]
|
||||
controller.settings.set_value(SettingsConstants.SETTING__SIG_TYPES, [attr for attr, name in SettingsConstants.ALL_SIG_TYPES])
|
||||
controller.settings.set_value(SettingsConstants.SETTING__SCRIPT_TYPES, [attr for attr, name in SettingsConstants.ALL_SCRIPT_TYPES])
|
||||
|
||||
# Pending mnemonic for ToolsCalcFinalWordShowFinalWordView
|
||||
controller.storage.init_pending_mnemonic(num_words=12)
|
||||
for i, word in enumerate(mnemonic_12[:11]):
|
||||
controller.storage.update_pending_mnemonic(word=word, index=i)
|
||||
controller.storage.update_pending_mnemonic(word="satoshi", index=11) # random last word; not supposed to be a valid checksum (yet)
|
||||
controller.storage.seeds.append(seed_12)
|
||||
controller.storage.seeds.append(seed_12b)
|
||||
controller.storage.seeds.append(seed_24)
|
||||
controller.storage.set_pending_seed(seed_24_w_passphrase)
|
||||
|
||||
# Load a PSBT into memory
|
||||
BASE64_PSBT_1 = """cHNidP8BAP06AQIAAAAC5l4E3oEjI+H0im8t/K2nLmF5iJFdKEiuQs8ESveWJKcAAAAAAP3///8iBZMRhYIq4s/LmnTmKBi79M8ITirmsbO++63evK4utwAAAAAA/f///wZYQuoDAAAAACIAIAW5jm3UnC5fyjKCUZ8LTzjENtb/ioRTaBMXeSXsB3n+bK2fCgAAAAAWABReJY7akT1+d+jx475yBRWORdBd7VxbUgUAAAAAFgAU4wj9I/jB3GjNQudNZAca+7g9R16iWtYOAAAAABYAFIotPApLZlfscg8f3ppKqO3qA5nv7BnMFAAAAAAiACAs6SGc8qv4FwuNl0G0SpMZG8ODUEk5RXiWUcuzzw5iaRSfAhMAAAAAIgAgW0f5QxQIgVCGQqKzsvfkXZjUxdFop5sfez6Pt8mUbmZ1AgAAAAEAkgIAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/////BQIRAgEB/////wJAvkAlAAAAACIAIIRPoo2LvkrwrhrYFhLhlP43izxbA4Eo6Y6iFFiQYdXRAAAAAAAAAAAmaiSqIant4vYcP3HR3v0/qZnfo2lTdVxpBol5mWK0i+vYNpdOjPkAAAAAAQErQL5AJQAAAAAiACCET6KNi75K8K4a2BYS4ZT+N4s8WwOBKOmOohRYkGHV0QEFR1EhArGhNdUqlR4BAOLGTMrY2ZJYTQNRudp7fU7i8crRJqgEIQNDxn7PjUzvsP6KYw4s7dmoZE0qO1K6MaM+2ScRZ7hyxFKuIgYCsaE11SqVHgEA4sZMytjZklhNA1G52nt9TuLxytEmqAQcc8XaCjAAAIABAACAAAAAgAIAAIAAAAAAAwAAACIGA0PGfs+NTO+w/opjDizt2ahkTSo7Uroxoz7ZJxFnuHLEHCK94akwAACAAQAAgAAAAIACAACAAAAAAAMAAAAAAQCSAgAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP////8FAhACAQH/////AkC+QCUAAAAAIgAghE+ijYu+SvCuGtgWEuGU/jeLPFsDgSjpjqIUWJBh1dEAAAAAAAAAACZqJKohqe3i9hw/cdHe/T+pmd+jaVN1XGkGiXmZYrSL69g2l06M+QAAAAABAStAvkAlAAAAACIAIIRPoo2LvkrwrhrYFhLhlP43izxbA4Eo6Y6iFFiQYdXRAQVHUSECsaE11SqVHgEA4sZMytjZklhNA1G52nt9TuLxytEmqAQhA0PGfs+NTO+w/opjDizt2ahkTSo7Uroxoz7ZJxFnuHLEUq4iBgKxoTXVKpUeAQDixkzK2NmSWE0DUbnae31O4vHK0SaoBBxzxdoKMAAAgAEAAIAAAACAAgAAgAAAAAADAAAAIgYDQ8Z+z41M77D+imMOLO3ZqGRNKjtSujGjPtknEWe4csQcIr3hqTAAAIABAACAAAAAgAIAAIAAAAAAAwAAAAABAUdRIQJ5XLCBS0hdo4NANq4lNhimzhyHj7dvObmPAwNj8L2xASEC9mwwoH28/WHnxbb6z05sJ/lHuvrLs/wOooHgFn5ulI1SriICAnlcsIFLSF2jg0A2riU2GKbOHIePt285uY8DA2PwvbEBHCK94akwAACAAQAAgAAAAIACAACAAQAAAAEAAAAiAgL2bDCgfbz9YefFtvrPTmwn+Ue6+suz/A6igeAWfm6UjRxzxdoKMAAAgAEAAIAAAACAAgAAgAEAAAABAAAAAAAAAAEBR1EhAgpbWcEh7rgvRE5UaCcqzWL/TR1B/DS8UeZsKVEvuKLrIQOwLg0emiQbbxafIh69Xjtpj4eclsMhKq1y/7vYDdE7LVKuIgICCltZwSHuuC9ETlRoJyrNYv9NHUH8NLxR5mwpUS+4ouscc8XaCjAAAIABAACAAAAAgAIAAIAAAAAABQAAACICA7AuDR6aJBtvFp8iHr1eO2mPh5yWwyEqrXL/u9gN0TstHCK94akwAACAAQAAgAAAAIACAACAAAAAAAUAAAAAAQFHUSECk50GLh/YhZaLJkDq/dugU3H/WvE6rTgQuY6N57pI4ykhA/H8MdLVP9SA/Hg8l3hvibSaC1bCBzwz7kTW+rsEZ8uFUq4iAgKTnQYuH9iFlosmQOr926BTcf9a8TqtOBC5jo3nukjjKRxzxdoKMAAAgAEAAIAAAACAAgAAgAAAAAAGAAAAIgID8fwx0tU/1ID8eDyXeG+JtJoLVsIHPDPuRNb6uwRny4UcIr3hqTAAAIABAACAAAAAgAIAAIAAAAAABgAAAAA="""
|
||||
decoder = DecodeQR()
|
||||
decoder.add_data(BASE64_PSBT_1)
|
||||
controller.psbt = decoder.get_psbt()
|
||||
controller.psbt_seed = seed_12b
|
||||
# Pending mnemonic for ToolsCalcFinalWordShowFinalWordView
|
||||
controller.storage.init_pending_mnemonic(num_words=12)
|
||||
for i, word in enumerate(mnemonic_12[:11]):
|
||||
controller.storage.update_pending_mnemonic(word=word, index=i)
|
||||
controller.storage.update_pending_mnemonic(word="satoshi", index=11) # random last word; not supposed to be a valid checksum (yet)
|
||||
|
||||
def add_op_return_to_psbt(psbt: PSBT, raw_payload_data: bytes):
|
||||
data = (compact.to_bytes(OPCODES.OP_RETURN) +
|
||||
compact.to_bytes(OPCODES.OP_PUSHDATA1) +
|
||||
compact.to_bytes(len(raw_payload_data)) +
|
||||
raw_payload_data)
|
||||
script = Script(data)
|
||||
output = OutputScope()
|
||||
output.script_pubkey = script
|
||||
output.value = 0
|
||||
psbt.outputs.append(output)
|
||||
return psbt.to_string()
|
||||
# Load a PSBT into memory
|
||||
decoder = DecodeQR()
|
||||
decoder.add_data(BASE64_PSBT_1)
|
||||
controller.psbt = decoder.get_psbt()
|
||||
controller.psbt_seed = seed_12b
|
||||
|
||||
# Prep a PSBT with a human-readable OP_RETURN
|
||||
raw_payload_data = "Chancellor on the brink of third bailout for banks".encode()
|
||||
psbt = PSBT.from_base64(BASE64_PSBT_1)
|
||||
# Message signing data
|
||||
derivation_path = "m/84h/0h/0h/0/0"
|
||||
controller.sign_message_data = {
|
||||
"seed_num": 0,
|
||||
"derivation_path": derivation_path,
|
||||
"message": "I attest that I control this bitcoin address blah blah blah",
|
||||
"addr_format": embit_utils.parse_derivation_path(derivation_path)
|
||||
}
|
||||
|
||||
# Simplify the output side
|
||||
output = psbt.outputs[-1]
|
||||
psbt.outputs.clear()
|
||||
psbt.outputs.append(output)
|
||||
assert len(psbt.outputs) == 1
|
||||
BASE64_PSBT_WITH_OP_RETURN_TEXT = add_op_return_to_psbt(psbt, raw_payload_data)
|
||||
# Automatically populate all Settings options Views
|
||||
settings_views_list = []
|
||||
settings_views_list.append(ScreenshotConfig(settings_views.SettingsMenuView))
|
||||
settings_views_list.append(
|
||||
ScreenshotConfig(
|
||||
settings_views.SettingsMenuView,
|
||||
dict(
|
||||
visibility=SettingsConstants.VISIBILITY__ADVANCED,
|
||||
selected_attr=SettingsConstants.SETTING__ELECTRUM_SEEDS,
|
||||
initial_scroll=240, # Just guessing how many pixels to scroll down
|
||||
),
|
||||
screenshot_name="SettingsMenuView__Advanced"
|
||||
)
|
||||
)
|
||||
|
||||
# Prep a PSBT with a (repeatably) random 80-byte OP_RETURN
|
||||
random.seed(6102)
|
||||
BASE64_PSBT_WITH_OP_RETURN_RAW_BYTES = add_op_return_to_psbt(PSBT.from_base64(BASE64_PSBT_1), random.randbytes(80))
|
||||
# so we get a choice for transcribe seed qr format
|
||||
controller.settings.set_value(
|
||||
attr_name=SettingsConstants.SETTING__COMPACT_SEEDQR,
|
||||
value=SettingsConstants.OPTION__ENABLED
|
||||
)
|
||||
for settings_entry in SettingsDefinition.settings_entries:
|
||||
if settings_entry.visibility == SettingsConstants.VISIBILITY__HIDDEN:
|
||||
continue
|
||||
|
||||
settings_views_list.append(ScreenshotConfig(settings_views.SettingsEntryUpdateSelectionView, dict(attr_name=settings_entry.attr_name), screenshot_name=f"SettingsEntryUpdateSelectionView_{settings_entry.attr_name}"))
|
||||
|
||||
settingsqr_data_persistent = f"settings::v1 name=English_noob_mode persistent=E coords=spa,spd denom=thr network=M qr_density=M xpub_export=E sigs=ss scripts=nat xpub_details=E passphrase=E camera=0 compact_seedqr=E bip85=D priv_warn=E dire_warn=E partners=E locale={locale}"
|
||||
settingsqr_data_not_persistent = f"settings::v1 name=Mode_Ephemeral persistent=D coords=spa,spd denom=thr network=M qr_density=M xpub_export=E sigs=ss scripts=nat xpub_details=E passphrase=E camera=0 compact_seedqr=E bip85=D priv_warn=E dire_warn=E partners=E locale={locale}"
|
||||
|
||||
|
||||
# Multisig wallet descriptor for the multisig in the above PSBT
|
||||
MULTISIG_WALLET_DESCRIPTOR = """wsh(sortedmulti(1,[22bde1a9/48h/1h/0h/2h]tpubDFfsBrmpj226ZYiRszYi2qK6iGvh2vkkghfGB2YiRUVY4rqqedHCFEgw12FwDkm7rUoVtq9wLTKc6BN2sxswvQeQgp7m8st4FP8WtP8go76/{0,1}/*,[73c5da0a/48h/1h/0h/2h]tpubDFH9dgzveyD8zTbPUFuLrGmCydNvxehyNdUXKJAQN8x4aZ4j6UZqGfnqFrD4NqyaTVGKbvEW54tsvPTK2UoSbCC1PJY8iCNiwTL3RWZEheQ/{0,1}/*))#3jhtf6yx"""
|
||||
controller.multisig_wallet_descriptor = embit.descriptor.Descriptor.from_string(MULTISIG_WALLET_DESCRIPTOR)
|
||||
|
||||
# Message signing data
|
||||
derivation_path = "m/84h/0h/0h/0/0"
|
||||
controller.sign_message_data = {
|
||||
"seed_num": 0,
|
||||
"derivation_path": derivation_path,
|
||||
"message": "I attest that I control this bitcoin address blah blah blah",
|
||||
"addr_format": embit_utils.parse_derivation_path(derivation_path)
|
||||
}
|
||||
# Set up screenshot-specific callbacks to inject data before the View is run and
|
||||
# reset data after the View is run.
|
||||
def load_basic_psbt_cb():
|
||||
decoder = DecodeQR()
|
||||
decoder.add_data(BASE64_PSBT_1)
|
||||
controller.psbt = decoder.get_psbt()
|
||||
controller.psbt_seed = seed_12b
|
||||
controller.multisig_wallet_descriptor = None
|
||||
|
||||
# Automatically populate all Settings options Views
|
||||
settings_views_list = []
|
||||
settings_views_list.append(settings_views.SettingsMenuView)
|
||||
settings_views_list.append((
|
||||
settings_views.SettingsMenuView,
|
||||
dict(
|
||||
visibility=SettingsConstants.VISIBILITY__ADVANCED,
|
||||
selected_attr=SettingsConstants.SETTING__ELECTRUM_SEEDS,
|
||||
initial_scroll=240, # Just guessing how many pixels to scroll down
|
||||
),
|
||||
"SettingsMenuView__Advanced"
|
||||
))
|
||||
|
||||
# so we get a choice for transcribe seed qr format
|
||||
controller.settings.set_value(
|
||||
attr_name=SettingsConstants.SETTING__COMPACT_SEEDQR,
|
||||
value=SettingsConstants.OPTION__ENABLED
|
||||
)
|
||||
for settings_entry in SettingsDefinition.settings_entries:
|
||||
if settings_entry.visibility == SettingsConstants.VISIBILITY__HIDDEN:
|
||||
continue
|
||||
def load_multisig_wallet_descriptor_cb():
|
||||
controller.multisig_wallet_descriptor = embit.descriptor.Descriptor.from_string(MULTISIG_WALLET_DESCRIPTOR)
|
||||
|
||||
settings_views_list.append((settings_views.SettingsEntryUpdateSelectionView, dict(attr_name=settings_entry.attr_name), f"SettingsEntryUpdateSelectionView_{settings_entry.attr_name}"))
|
||||
|
||||
|
||||
settingsqr_data_persistent = "settings::v1 name=Total_noob_mode persistent=E coords=spa,spd denom=thr network=M qr_density=M xpub_export=E sigs=ss scripts=nat xpub_details=E passphrase=E camera=0 compact_seedqr=E bip85=D priv_warn=E dire_warn=E partners=E"
|
||||
settingsqr_data_not_persistent = "settings::v1 name=Ephemeral_noob_mode persistent=D coords=spa,spd denom=thr network=M qr_density=M xpub_export=E sigs=ss scripts=nat xpub_details=E passphrase=E camera=0 compact_seedqr=E bip85=D priv_warn=E dire_warn=E partners=E"
|
||||
def load_address_verification_data_cb():
|
||||
controller.unverified_address = dict(
|
||||
# These are all totally fake data
|
||||
address="bc1q6p00wazu4nnqac29fvky6vhjnnhku5u2g9njss62rvy7e0yuperq86f5ek",
|
||||
network=SettingsConstants.MAINNET,
|
||||
sig_type=SettingsConstants.SINGLE_SIG,
|
||||
script_type=SettingsConstants.NATIVE_SEGWIT,
|
||||
derivation_path = "m/84h/0h/0h",
|
||||
verified_index=5,
|
||||
verified_index_is_change=False
|
||||
)
|
||||
|
||||
screenshot_sections = {
|
||||
"Main Menu Views": [
|
||||
MainMenuView,
|
||||
(MainMenuView, {}, 'MainMenuView_SDCardStateChangeToast_removed', SDCardStateChangeToastManagerThread(action=MicroSD.ACTION__REMOVED)),
|
||||
(MainMenuView, {}, 'MainMenuView_SDCardStateChangeToast_inserted', SDCardStateChangeToastManagerThread(action=MicroSD.ACTION__INSERTED)),
|
||||
(MainMenuView, {}, 'MainMenuView_RemoveSDCardToast', RemoveSDCardToastManagerThread(activation_delay=0)),
|
||||
(MainMenuView, {}, 'MainMenuView_DefaultToast', DefaultToast("This is a default text toast!")),
|
||||
(MainMenuView, {}, 'MainMenuView_InfoToast', InfoToast("This is an info toast!")),
|
||||
(MainMenuView, {}, 'MainMenuView_SuccessToast', SuccessToast("This is a success toast!")),
|
||||
(MainMenuView, {}, 'MainMenuView_WarningToast', WarningToast("This is a warning toast!")),
|
||||
(MainMenuView, {}, 'MainMenuView_DireWarningToast', DireWarningToast("This is a dire warning toast!")),
|
||||
(MainMenuView, {}, 'MainMenuView_ErrorToast', ErrorToast("This is an error toast!")),
|
||||
PowerOptionsView,
|
||||
RestartView,
|
||||
PowerOffView,
|
||||
],
|
||||
"Seed Views": [
|
||||
seed_views.SeedsMenuView,
|
||||
seed_views.LoadSeedView,
|
||||
seed_views.SeedMnemonicEntryView,
|
||||
seed_views.SeedMnemonicInvalidView,
|
||||
seed_views.SeedFinalizeView,
|
||||
seed_views.SeedAddPassphraseView,
|
||||
seed_views.SeedAddPassphraseExitDialogView,
|
||||
seed_views.SeedReviewPassphraseView,
|
||||
|
||||
(seed_views.SeedOptionsView, dict(seed_num=0)),
|
||||
(seed_views.SeedBackupView, dict(seed_num=0)),
|
||||
(seed_views.SeedExportXpubSigTypeView, dict(seed_num=0)),
|
||||
(seed_views.SeedExportXpubScriptTypeView, dict(seed_num=0, sig_type="msig")),
|
||||
(seed_views.SeedExportXpubCustomDerivationView, dict(seed_num=0, sig_type="ss", script_type="")),
|
||||
(seed_views.SeedExportXpubCoordinatorView, dict(seed_num=0, sig_type="ss", script_type="nat")),
|
||||
(seed_views.SeedExportXpubWarningView, dict(seed_num=0, sig_type="msig", script_type="nes", coordinator="spd", custom_derivation="")),
|
||||
(seed_views.SeedExportXpubDetailsView, dict(seed_num=0, sig_type="ss", script_type="nat", coordinator="bw", custom_derivation="")),
|
||||
#SeedExportXpubQRDisplayView,
|
||||
(seed_views.SeedWordsWarningView, dict(seed_num=0)),
|
||||
(seed_views.SeedWordsView, dict(seed_num=0)),
|
||||
(seed_views.SeedWordsView, dict(seed_num=0, page_index=2), "SeedWordsView_2"),
|
||||
(seed_views.SeedBIP85ApplicationModeView, dict(seed_num=0)),
|
||||
(seed_views.SeedBIP85SelectChildIndexView, dict(seed_num=0, num_words=24)),
|
||||
(seed_views.SeedBIP85InvalidChildIndexView, dict(seed_num=0, num_words=12)),
|
||||
(seed_views.SeedWordsBackupTestPromptView, dict(seed_num=0)),
|
||||
(seed_views.SeedWordsBackupTestView, dict(seed_num=0)),
|
||||
(seed_views.SeedWordsBackupTestMistakeView, dict(seed_num=0, cur_index=7, wrong_word="unlucky")),
|
||||
(seed_views.SeedWordsBackupTestSuccessView, dict(seed_num=0)),
|
||||
(seed_views.SeedTranscribeSeedQRFormatView, dict(seed_num=0)),
|
||||
(seed_views.SeedTranscribeSeedQRWarningView, dict(seed_num=0)),
|
||||
(seed_views.SeedTranscribeSeedQRWholeQRView, dict(seed_num=0, seedqr_format=QRType.SEED__COMPACTSEEDQR, num_modules=21), "SeedTranscribeSeedQRWholeQRView_12_Compact"),
|
||||
(seed_views.SeedTranscribeSeedQRWholeQRView, dict(seed_num=0, seedqr_format=QRType.SEED__SEEDQR, num_modules=25), "SeedTranscribeSeedQRWholeQRView_12_Standard"),
|
||||
(seed_views.SeedTranscribeSeedQRWholeQRView, dict(seed_num=2, seedqr_format=QRType.SEED__COMPACTSEEDQR, num_modules=25), "SeedTranscribeSeedQRWholeQRView_24_Compact"),
|
||||
(seed_views.SeedTranscribeSeedQRWholeQRView, dict(seed_num=2, seedqr_format=QRType.SEED__SEEDQR, num_modules=29), "SeedTranscribeSeedQRWholeQRView_24_Standard"),
|
||||
|
||||
# Screenshot doesn't render properly due to how the transparency mask is pre-rendered
|
||||
# (seed_views.SeedTranscribeSeedQRZoomedInView, dict(seed_num=0, seedqr_format=QRType.SEED__SEEDQR)),
|
||||
def PSBTSelectSeedView_cb_before():
|
||||
# Have to ensure this is cleared out in order to get the seed selection screen
|
||||
controller.psbt_seed = None
|
||||
|
||||
(seed_views.SeedTranscribeSeedQRConfirmQRPromptView, dict(seed_num=0)),
|
||||
|
||||
# Screenshot can't render live preview screens
|
||||
# (seed_views.SeedTranscribeSeedQRConfirmScanView, dict(seed_num=0)),
|
||||
def PSBTOverviewView_op_return_cb_before():
|
||||
controller.psbt_seed = seed_12b
|
||||
decoder = DecodeQR()
|
||||
decoder.add_data(BASE64_PSBT_WITH_OP_RETURN_TEXT)
|
||||
controller.psbt = decoder.get_psbt()
|
||||
controller.psbt_parser = PSBTParser(p=controller.psbt, seed=seed_12b)
|
||||
|
||||
|
||||
#(seed_views.AddressVerificationStartView, dict(address=, script_type="nat", network="M")),
|
||||
#seed_views.AddressVerificationSigTypeView,
|
||||
#seed_views.SeedSingleSigAddressVerificationSelectSeedView,
|
||||
#seed_views.SeedAddressVerificationView,
|
||||
#seed_views.AddressVerificationSuccessView,
|
||||
def PSBTOpReturnView_raw_hex_data_cb_before():
|
||||
decoder.add_data(BASE64_PSBT_WITH_OP_RETURN_RAW_BYTES)
|
||||
controller.psbt = decoder.get_psbt()
|
||||
controller.psbt_parser = PSBTParser(p=controller.psbt, seed=seed_12b)
|
||||
|
||||
seed_views.LoadMultisigWalletDescriptorView,
|
||||
seed_views.MultisigWalletDescriptorView,
|
||||
(seed_views.SeedDiscardView, dict(seed_num=0)),
|
||||
|
||||
seed_views.SeedSignMessageConfirmMessageView,
|
||||
seed_views.SeedSignMessageConfirmAddressView,
|
||||
screenshot_sections = {
|
||||
"Main Menu Views": [
|
||||
ScreenshotConfig(OpeningSplashView, dict(is_screenshot_renderer=True, force_partner_logos=True)),
|
||||
ScreenshotConfig(OpeningSplashView, dict(is_screenshot_renderer=True, force_partner_logos=False), screenshot_name="OpeningSplashView_no_partner_logos"),
|
||||
ScreenshotConfig(MainMenuView),
|
||||
ScreenshotConfig(MainMenuView, screenshot_name='MainMenuView_SDCardStateChangeToast_removed', toast_thread=SDCardStateChangeToastManagerThread(action=MicroSD.ACTION__REMOVED, activation_delay=0, duration=0)),
|
||||
ScreenshotConfig(MainMenuView, screenshot_name='MainMenuView_SDCardStateChangeToast_inserted', toast_thread=SDCardStateChangeToastManagerThread(action=MicroSD.ACTION__INSERTED, activation_delay=0, duration=0)),
|
||||
ScreenshotConfig(MainMenuView, screenshot_name='MainMenuView_RemoveSDCardToast', toast_thread=RemoveSDCardToastManagerThread(activation_delay=0, duration=0)),
|
||||
ScreenshotConfig(MainMenuView, screenshot_name='MainMenuView_DefaultToast', toast_thread=DefaultToast("This is a default text toast!", activation_delay=0, duration=0)),
|
||||
ScreenshotConfig(MainMenuView, screenshot_name='MainMenuView_InfoToast', toast_thread=InfoToast("This is an info toast!", activation_delay=0, duration=0)),
|
||||
ScreenshotConfig(MainMenuView, screenshot_name='MainMenuView_SuccessToast', toast_thread=SuccessToast("This is a success toast!", activation_delay=0, duration=0)),
|
||||
ScreenshotConfig(MainMenuView, screenshot_name='MainMenuView_WarningToast', toast_thread=WarningToast("This is a warning toast!", activation_delay=0, duration=0)),
|
||||
ScreenshotConfig(MainMenuView, screenshot_name='MainMenuView_DireWarningToast', toast_thread=DireWarningToast("This is a dire warning toast!", activation_delay=0, duration=0)),
|
||||
ScreenshotConfig(MainMenuView, screenshot_name='MainMenuView_ErrorToast', toast_thread=ErrorToast("This is an error toast!", activation_delay=0, duration=0)),
|
||||
ScreenshotConfig(PowerOptionsView),
|
||||
ScreenshotConfig(RestartView),
|
||||
ScreenshotConfig(PowerOffView),
|
||||
],
|
||||
"Seed Views": [
|
||||
ScreenshotConfig(seed_views.SeedsMenuView),
|
||||
ScreenshotConfig(seed_views.LoadSeedView),
|
||||
ScreenshotConfig(seed_views.SeedMnemonicEntryView),
|
||||
ScreenshotConfig(seed_views.SeedMnemonicInvalidView),
|
||||
ScreenshotConfig(seed_views.SeedFinalizeView),
|
||||
ScreenshotConfig(seed_views.SeedAddPassphraseView, screenshot_name="SeedAddPassphraseView_lowercase"),
|
||||
ScreenshotConfig(seed_views.SeedAddPassphraseView, dict(initial_keyboard=SeedAddPassphraseScreen.KEYBOARD__UPPERCASE_BUTTON_TEXT), screenshot_name="SeedAddPassphraseView_uppercase"),
|
||||
ScreenshotConfig(seed_views.SeedAddPassphraseView, dict(initial_keyboard=SeedAddPassphraseScreen.KEYBOARD__DIGITS_BUTTON_TEXT), screenshot_name="SeedAddPassphraseView_digits"),
|
||||
ScreenshotConfig(seed_views.SeedAddPassphraseView, dict(initial_keyboard=SeedAddPassphraseScreen.KEYBOARD__SYMBOLS_1_BUTTON_TEXT), screenshot_name="SeedAddPassphraseView_symbols_1"),
|
||||
ScreenshotConfig(seed_views.SeedAddPassphraseView, dict(initial_keyboard=SeedAddPassphraseScreen.KEYBOARD__SYMBOLS_2_BUTTON_TEXT), screenshot_name="SeedAddPassphraseView_symbols_2"),
|
||||
ScreenshotConfig(seed_views.SeedAddPassphraseExitDialogView),
|
||||
ScreenshotConfig(seed_views.SeedReviewPassphraseView),
|
||||
|
||||
ScreenshotConfig(seed_views.SeedOptionsView, dict(seed_num=0)),
|
||||
ScreenshotConfig(seed_views.SeedBackupView, dict(seed_num=0)),
|
||||
ScreenshotConfig(seed_views.SeedExportXpubSigTypeView, dict(seed_num=0)),
|
||||
ScreenshotConfig(seed_views.SeedExportXpubScriptTypeView, dict(seed_num=0, sig_type="msig")),
|
||||
ScreenshotConfig(seed_views.SeedExportXpubCustomDerivationView, dict(seed_num=0, sig_type="ss", script_type="")),
|
||||
ScreenshotConfig(seed_views.SeedExportXpubCoordinatorView, dict(seed_num=0, sig_type="ss", script_type="nat")),
|
||||
ScreenshotConfig(seed_views.SeedExportXpubWarningView, dict(seed_num=0, sig_type="msig", script_type="nes", coordinator="spd", custom_derivation="")),
|
||||
ScreenshotConfig(seed_views.SeedExportXpubDetailsView, dict(seed_num=0, sig_type="ss", script_type="nat", coordinator="bw", custom_derivation="")),
|
||||
#ScreenshotConfig(SeedExportXpubQRDisplayView),
|
||||
ScreenshotConfig(seed_views.SeedWordsWarningView, dict(seed_num=0)),
|
||||
ScreenshotConfig(seed_views.SeedWordsView, dict(seed_num=0)),
|
||||
ScreenshotConfig(seed_views.SeedWordsView, dict(seed_num=0, page_index=2), screenshot_name="SeedWordsView_2"),
|
||||
ScreenshotConfig(seed_views.SeedBIP85ApplicationModeView, dict(seed_num=0)),
|
||||
ScreenshotConfig(seed_views.SeedBIP85SelectChildIndexView, dict(seed_num=0, num_words=24)),
|
||||
ScreenshotConfig(seed_views.SeedBIP85InvalidChildIndexView, dict(seed_num=0, num_words=12)),
|
||||
ScreenshotConfig(seed_views.SeedWordsBackupTestPromptView, dict(seed_num=0)),
|
||||
ScreenshotConfig(seed_views.SeedWordsBackupTestView, dict(seed_num=0, rand_seed=6102)),
|
||||
ScreenshotConfig(seed_views.SeedWordsBackupTestMistakeView, dict(seed_num=0, cur_index=7, wrong_word="satoshi")),
|
||||
ScreenshotConfig(seed_views.SeedWordsBackupTestSuccessView, dict(seed_num=0)),
|
||||
ScreenshotConfig(seed_views.SeedTranscribeSeedQRFormatView, dict(seed_num=0)),
|
||||
ScreenshotConfig(seed_views.SeedTranscribeSeedQRWarningView, dict(seed_num=0)),
|
||||
ScreenshotConfig(seed_views.SeedTranscribeSeedQRWholeQRView, dict(seed_num=0, seedqr_format=QRType.SEED__COMPACTSEEDQR, num_modules=21), screenshot_name="SeedTranscribeSeedQRWholeQRView_12_Compact"),
|
||||
ScreenshotConfig(seed_views.SeedTranscribeSeedQRWholeQRView, dict(seed_num=0, seedqr_format=QRType.SEED__SEEDQR, num_modules=25), screenshot_name="SeedTranscribeSeedQRWholeQRView_12_Standard"),
|
||||
ScreenshotConfig(seed_views.SeedTranscribeSeedQRWholeQRView, dict(seed_num=2, seedqr_format=QRType.SEED__COMPACTSEEDQR, num_modules=25), screenshot_name="SeedTranscribeSeedQRWholeQRView_24_Compact"),
|
||||
ScreenshotConfig(seed_views.SeedTranscribeSeedQRWholeQRView, dict(seed_num=2, seedqr_format=QRType.SEED__SEEDQR, num_modules=29), screenshot_name="SeedTranscribeSeedQRWholeQRView_24_Standard"),
|
||||
ScreenshotConfig(seed_views.SeedTranscribeSeedQRZoomedInView, dict(seed_num=0, seedqr_format=QRType.SEED__COMPACTSEEDQR, initial_block_x=1, initial_block_y=1), screenshot_name="SeedTranscribeSeedQRZoomedInView_12_Compact"),
|
||||
ScreenshotConfig(seed_views.SeedTranscribeSeedQRZoomedInView, dict(seed_num=0, seedqr_format=QRType.SEED__SEEDQR, initial_block_x=2, initial_block_y=2), screenshot_name="SeedTranscribeSeedQRZoomedInView_12_Standard"),
|
||||
|
||||
seed_views.SeedElectrumMnemonicStartView,
|
||||
],
|
||||
"PSBT Views": [
|
||||
psbt_views.PSBTSelectSeedView, # this will fail, be rerun below
|
||||
psbt_views.PSBTOverviewView,
|
||||
psbt_views.PSBTUnsupportedScriptTypeWarningView,
|
||||
psbt_views.PSBTNoChangeWarningView,
|
||||
psbt_views.PSBTMathView,
|
||||
(psbt_views.PSBTAddressDetailsView, dict(address_num=0)),
|
||||
ScreenshotConfig(seed_views.SeedTranscribeSeedQRConfirmQRPromptView, dict(seed_num=0)),
|
||||
|
||||
(NotYetImplementedView, {}, "PSBTChangeDetailsView_multisig_unverified"), # Must manually re-run this below
|
||||
(psbt_views.PSBTChangeDetailsView, dict(change_address_num=0), "PSBTChangeDetailsView_multisig_verified"),
|
||||
# Screenshot can't render live preview screens
|
||||
# ScreenshotConfig(seed_views.SeedTranscribeSeedQRConfirmScanView, dict(seed_num=0)),
|
||||
|
||||
(NotYetImplementedView, {}, "PSBTOverviewView_op_return"), # Placeholder
|
||||
(NotYetImplementedView, {}, "PSBTOpReturnView_text"), # Placeholder
|
||||
(NotYetImplementedView, {}, "PSBTOpReturnView_raw_hex_data"), # Placeholder
|
||||
ScreenshotConfig(seed_views.SeedSelectSeedView, dict(flow=Controller.FLOW__VERIFY_SINGLESIG_ADDR), screenshot_name="SeedSelectSeedView_address_verification"),
|
||||
ScreenshotConfig(seed_views.AddressVerificationSigTypeView),
|
||||
ScreenshotConfig(seed_views.SeedAddressVerificationView, dict(seed_num=0), run_before=load_address_verification_data_cb),
|
||||
ScreenshotConfig(seed_views.SeedAddressVerificationSuccessView, dict(seed_num=0)), # Relies on callback above
|
||||
|
||||
(psbt_views.PSBTAddressVerificationFailedView, dict(is_change=True, is_multisig=False), "PSBTAddressVerificationFailedView_singlesig_change"),
|
||||
(psbt_views.PSBTAddressVerificationFailedView, dict(is_change=False, is_multisig=False), "PSBTAddressVerificationFailedView_singlesig_selftransfer"),
|
||||
(psbt_views.PSBTAddressVerificationFailedView, dict(is_change=True, is_multisig=True), "PSBTAddressVerificationFailedView_multisig_change"),
|
||||
(psbt_views.PSBTAddressVerificationFailedView, dict(is_change=False, is_multisig=True), "PSBTAddressVerificationFailedView_multisig_selftransfer"),
|
||||
psbt_views.PSBTFinalizeView,
|
||||
#PSBTSignedQRDisplayView
|
||||
psbt_views.PSBTSigningErrorView,
|
||||
],
|
||||
"Tools Views": [
|
||||
tools_views.ToolsMenuView,
|
||||
#ToolsImageEntropyLivePreviewView
|
||||
#ToolsImageEntropyFinalImageView
|
||||
tools_views.ToolsImageEntropyMnemonicLengthView,
|
||||
tools_views.ToolsDiceEntropyMnemonicLengthView,
|
||||
(tools_views.ToolsDiceEntropyEntryView, dict(total_rolls=50)),
|
||||
tools_views.ToolsCalcFinalWordNumWordsView,
|
||||
tools_views.ToolsCalcFinalWordFinalizePromptView,
|
||||
tools_views.ToolsCalcFinalWordCoinFlipsView,
|
||||
(tools_views.ToolsCalcFinalWordShowFinalWordView, {}, "ToolsCalcFinalWordShowFinalWordView_pick_word"),
|
||||
(tools_views.ToolsCalcFinalWordShowFinalWordView, dict(coin_flips="0010101"), "ToolsCalcFinalWordShowFinalWordView_coin_flips"),
|
||||
#tools_views.ToolsCalcFinalWordDoneView,
|
||||
tools_views.ToolsAddressExplorerSelectSourceView,
|
||||
tools_views.ToolsAddressExplorerAddressTypeView,
|
||||
tools_views.ToolsAddressExplorerAddressListView,
|
||||
#tools_views.ToolsAddressExplorerAddressView,
|
||||
],
|
||||
"Settings Views": settings_views_list + [
|
||||
settings_views.IOTestView,
|
||||
settings_views.DonateView,
|
||||
(settings_views.SettingsIngestSettingsQRView, dict(data=settingsqr_data_persistent), "SettingsIngestSettingsQRView_persistent"),
|
||||
(settings_views.SettingsIngestSettingsQRView, dict(data=settingsqr_data_not_persistent), "SettingsIngestSettingsQRView_not_persistent"),
|
||||
],
|
||||
"Misc Error Views": [
|
||||
NotYetImplementedView,
|
||||
(UnhandledExceptionView, dict(error=UnhandledExceptionViewFood)),
|
||||
NetworkMismatchErrorView,
|
||||
(OptionDisabledView, dict(settings_attr=SettingsConstants.SETTING__MESSAGE_SIGNING)),
|
||||
(ErrorView, dict(
|
||||
title="Error",
|
||||
status_headline="Unknown QR Type",
|
||||
text="QRCode is invalid or is a data format not yet supported.",
|
||||
button_text="Back",
|
||||
)),
|
||||
]
|
||||
}
|
||||
ScreenshotConfig(seed_views.LoadMultisigWalletDescriptorView),
|
||||
ScreenshotConfig(seed_views.MultisigWalletDescriptorView, run_before=load_multisig_wallet_descriptor_cb),
|
||||
ScreenshotConfig(seed_views.SeedDiscardView, dict(seed_num=0)),
|
||||
|
||||
readme = f"""# SeedSigner Screenshots\n"""
|
||||
ScreenshotConfig(seed_views.SeedSelectSeedView, dict(flow=Controller.FLOW__SIGN_MESSAGE), screenshot_name="SeedSelectSeedView_sign_message"),
|
||||
ScreenshotConfig(seed_views.SeedSignMessageConfirmMessageView),
|
||||
ScreenshotConfig(seed_views.SeedSignMessageConfirmAddressView),
|
||||
|
||||
def screencap_view(view_cls: View, view_args: dict = {}, view_name: str = None, toast_thread: BaseToastOverlayManagerThread = None):
|
||||
if not view_name:
|
||||
view_name = view_cls.__name__
|
||||
screenshot_renderer.set_screenshot_filename(f"{view_name}.png")
|
||||
ScreenshotConfig(seed_views.SeedElectrumMnemonicStartView),
|
||||
],
|
||||
"PSBT Views": [
|
||||
ScreenshotConfig(psbt_views.PSBTSelectSeedView, run_before=PSBTSelectSeedView_cb_before),
|
||||
ScreenshotConfig(psbt_views.PSBTOverviewView, run_before=load_basic_psbt_cb),
|
||||
ScreenshotConfig(psbt_views.PSBTUnsupportedScriptTypeWarningView),
|
||||
ScreenshotConfig(psbt_views.PSBTNoChangeWarningView),
|
||||
ScreenshotConfig(psbt_views.PSBTMathView),
|
||||
ScreenshotConfig(psbt_views.PSBTAddressDetailsView, dict(address_num=0)),
|
||||
|
||||
ScreenshotConfig(psbt_views.PSBTChangeDetailsView, dict(change_address_num=0), screenshot_name="PSBTChangeDetailsView_multisig_unverified", run_before=load_basic_psbt_cb),
|
||||
ScreenshotConfig(psbt_views.PSBTChangeDetailsView, dict(change_address_num=0), screenshot_name="PSBTChangeDetailsView_multisig_verified", run_before=load_multisig_wallet_descriptor_cb),
|
||||
ScreenshotConfig(psbt_views.PSBTOverviewView, screenshot_name="PSBTOverviewView_op_return", run_before=PSBTOverviewView_op_return_cb_before),
|
||||
ScreenshotConfig(psbt_views.PSBTOpReturnView, screenshot_name="PSBTOpReturnView_text"), # Relies on callback above
|
||||
ScreenshotConfig(psbt_views.PSBTOpReturnView, screenshot_name="PSBTOpReturnView_raw_hex_data", run_before=PSBTOpReturnView_raw_hex_data_cb_before),
|
||||
ScreenshotConfig(psbt_views.PSBTAddressVerificationFailedView, dict(is_change=True, is_multisig=False), screenshot_name="PSBTAddressVerificationFailedView_singlesig_change"),
|
||||
ScreenshotConfig(psbt_views.PSBTAddressVerificationFailedView, dict(is_change=False, is_multisig=False), screenshot_name="PSBTAddressVerificationFailedView_singlesig_selftransfer"),
|
||||
ScreenshotConfig(psbt_views.PSBTAddressVerificationFailedView, dict(is_change=True, is_multisig=True), screenshot_name="PSBTAddressVerificationFailedView_multisig_change"),
|
||||
ScreenshotConfig(psbt_views.PSBTAddressVerificationFailedView, dict(is_change=False, is_multisig=True), screenshot_name="PSBTAddressVerificationFailedView_multisig_selftransfer"),
|
||||
ScreenshotConfig(psbt_views.PSBTFinalizeView),
|
||||
#ScreenshotConfig(PSBTSignedQRDisplayViewScreenshotConfig),
|
||||
ScreenshotConfig(psbt_views.PSBTSigningErrorView),
|
||||
],
|
||||
"Tools Views": [
|
||||
ScreenshotConfig(tools_views.ToolsMenuView),
|
||||
#ScreenshotConfig(ToolsImageEntropyLivePreviewView),
|
||||
#ScreenshotConfig(ToolsImageEntropyFinalImageView),
|
||||
ScreenshotConfig(tools_views.ToolsImageEntropyMnemonicLengthView),
|
||||
ScreenshotConfig(tools_views.ToolsDiceEntropyMnemonicLengthView),
|
||||
ScreenshotConfig(tools_views.ToolsDiceEntropyEntryView, dict(total_rolls=50)),
|
||||
ScreenshotConfig(tools_views.ToolsCalcFinalWordNumWordsView),
|
||||
ScreenshotConfig(tools_views.ToolsCalcFinalWordFinalizePromptView),
|
||||
ScreenshotConfig(tools_views.ToolsCalcFinalWordCoinFlipsView),
|
||||
ScreenshotConfig(tools_views.ToolsCalcFinalWordShowFinalWordView, screenshot_name="ToolsCalcFinalWordShowFinalWordView_pick_word"),
|
||||
ScreenshotConfig(tools_views.ToolsCalcFinalWordShowFinalWordView, dict(coin_flips="0010101"), screenshot_name="ToolsCalcFinalWordShowFinalWordView_coin_flips"),
|
||||
ScreenshotConfig(tools_views.ToolsCalcFinalWordDoneView),
|
||||
ScreenshotConfig(tools_views.ToolsAddressExplorerSelectSourceView),
|
||||
ScreenshotConfig(tools_views.ToolsAddressExplorerAddressTypeView),
|
||||
ScreenshotConfig(tools_views.ToolsAddressExplorerAddressListView),
|
||||
# ScreenshotConfig(tools_views.ToolsAddressExplorerAddressView),
|
||||
],
|
||||
"Settings Views": settings_views_list + [
|
||||
ScreenshotConfig(settings_views.IOTestView),
|
||||
ScreenshotConfig(settings_views.DonateView),
|
||||
ScreenshotConfig(settings_views.SettingsIngestSettingsQRView, dict(data=settingsqr_data_persistent), screenshot_name="SettingsIngestSettingsQRView_persistent"),
|
||||
ScreenshotConfig(settings_views.SettingsIngestSettingsQRView, dict(data=settingsqr_data_not_persistent), screenshot_name="SettingsIngestSettingsQRView_not_persistent"),
|
||||
],
|
||||
"Misc Error Views": [
|
||||
ScreenshotConfig(NotYetImplementedView),
|
||||
ScreenshotConfig(UnhandledExceptionView, dict(error=["IndexError", "line 1, in some_buggy_code.py", "list index out of range"])),
|
||||
ScreenshotConfig(NetworkMismatchErrorView, dict(derivation_path="m/84'/1'/0'")),
|
||||
ScreenshotConfig(OptionDisabledView, dict(settings_attr=SettingsConstants.SETTING__MESSAGE_SIGNING)),
|
||||
ScreenshotConfig(ErrorView, dict(
|
||||
title="Error",
|
||||
status_headline="Unknown QR Type",
|
||||
text="QRCode is invalid or is a data format not yet supported.",
|
||||
button_text="Back",
|
||||
)),
|
||||
]
|
||||
}
|
||||
|
||||
return screenshot_sections
|
||||
|
||||
|
||||
def screencap_view(screenshot_config: ScreenshotConfig):
|
||||
# Block until we have exclusive access to the screenshot renderer. Without this
|
||||
# we were occasionally running into confusing race conditions where the next
|
||||
# screenshot would begin rendering over the previous one. Claiming the lock
|
||||
# guarantees that the previous screenshot has been fully rendered and saved.
|
||||
with screenshot_renderer.lock:
|
||||
screenshot_renderer.set_screenshot_filename(f"{screenshot_config.screenshot_name}.png")
|
||||
|
||||
controller = Controller.get_instance()
|
||||
toast_thread = screenshot_config.toast_thread
|
||||
try:
|
||||
print(f"Running {view_name}")
|
||||
print(f"Running {screenshot_config.screenshot_name}")
|
||||
try:
|
||||
view_cls(**view_args).run()
|
||||
screenshot_config.run_callback_before()
|
||||
screenshot_config.View_cls(**screenshot_config.view_kwargs).run()
|
||||
except ScreenshotComplete:
|
||||
# The target View has run and its Screen has rendered what it needs to
|
||||
if toast_thread is not None:
|
||||
# Now run the Toast so it can render on top of the current image buffer
|
||||
controller.activate_toast(toast_thread)
|
||||
while controller.toast_notification_thread.is_alive():
|
||||
time.sleep(0.1)
|
||||
# Give the Toast a moment to complete its work
|
||||
|
||||
time.sleep(0.01)
|
||||
|
||||
# TODO: Necessary now that the lock is in place?
|
||||
# Whenever possible, clean up toast thread HERE before killing the
|
||||
# main thread with ScreenshotComplete.
|
||||
toast_thread.stop()
|
||||
toast_thread.join()
|
||||
raise ScreenshotComplete()
|
||||
except ScreenshotComplete:
|
||||
# Slightly hacky way to exit ScreenshotRenderer as expected
|
||||
pass
|
||||
print(f"Completed {view_name}")
|
||||
print(f"Completed {screenshot_config.screenshot_name}")
|
||||
except Exception as e:
|
||||
# Something else went wrong
|
||||
from traceback import print_exc
|
||||
print_exc()
|
||||
raise e
|
||||
finally:
|
||||
if toast_thread:
|
||||
if toast_thread and toast_thread.is_alive():
|
||||
toast_thread.stop()
|
||||
toast_thread.join()
|
||||
|
||||
for section_name, screenshot_list in screenshot_sections.items():
|
||||
screenshot_config.run_callback_after()
|
||||
|
||||
|
||||
# Parse the main `l10n/messages.pot` for overall stats
|
||||
messages_source_path = os.path.join(pathlib.Path(__file__).parent.resolve().parent.resolve().parent.resolve(), "l10n", "messages.pot")
|
||||
with open(messages_source_path, 'r') as messages_source_file:
|
||||
num_source_messages = messages_source_file.read().count("msgid \"") - 1
|
||||
|
||||
locale_tuple_list = [locale_tuple for locale_tuple in SettingsConstants.ALL_LOCALES if locale_tuple[0] == locale]
|
||||
if not locale_tuple_list:
|
||||
raise Exception(f"Invalid locale: {locale}")
|
||||
|
||||
locale, display_name = locale_tuple_list[0]
|
||||
|
||||
Settings.get_instance().set_value(SettingsConstants.SETTING__LOCALE, value=locale)
|
||||
|
||||
locale_readme = f"""# SeedSigner Screenshots: {display_name}\n"""
|
||||
|
||||
# Report the translation progress
|
||||
if locale != SettingsConstants.LOCALE__ENGLISH:
|
||||
try:
|
||||
translated_messages_path = os.path.join(pathlib.Path(__file__).parent.resolve().parent.resolve().parent.resolve(), "src", "seedsigner", "resources", "seedsigner-translations", "l10n", locale, "LC_MESSAGES", "messages.po")
|
||||
with open(translated_messages_path, 'r') as translation_file:
|
||||
locale_translations = translation_file.read()
|
||||
num_locale_translations = locale_translations.count("msgid \"") - locale_translations.count("""msgstr ""\n\n""") - 1
|
||||
|
||||
if locale != "en":
|
||||
locale_readme += f"## Translation progress: {num_locale_translations / num_source_messages:.1%}\n\n"
|
||||
locale_readme += "---\n\n"
|
||||
except Exception as e:
|
||||
from traceback import print_exc
|
||||
print_exc()
|
||||
|
||||
for section_name, screenshot_list in setup_screenshots(locale).items():
|
||||
subdir = section_name.lower().replace(" ", "_")
|
||||
screenshot_renderer.set_screenshot_path(os.path.join(screenshot_root, subdir))
|
||||
readme += "\n\n---\n\n"
|
||||
readme += f"## {section_name}\n\n"
|
||||
readme += """<table style="border: 0;">"""
|
||||
readme += f"""<tr><td align="center">\n"""
|
||||
for screenshot in screenshot_list:
|
||||
if type(screenshot) == tuple:
|
||||
if len(screenshot) == 2:
|
||||
view_cls, view_args = screenshot
|
||||
view_name = view_cls.__name__
|
||||
elif len(screenshot) == 3:
|
||||
view_cls, view_args, view_name = screenshot
|
||||
elif len(screenshot) == 4:
|
||||
view_cls, view_args, view_name, toast_thread = screenshot
|
||||
else:
|
||||
view_cls = screenshot
|
||||
view_args = {}
|
||||
view_name = view_cls.__name__
|
||||
toast_thread = None
|
||||
screenshot_renderer.set_screenshot_path(os.path.join(screenshot_root, locale, subdir))
|
||||
locale_readme += "\n\n---\n\n"
|
||||
locale_readme += f"## {section_name}\n\n"
|
||||
locale_readme += """<table style="border: 0;">"""
|
||||
locale_readme += f"""<tr><td align="center">"""
|
||||
for screenshot_config in screenshot_list:
|
||||
screencap_view(screenshot_config)
|
||||
locale_readme += """ <table align="left" style="border: 1px solid gray;">"""
|
||||
locale_readme += f"""<tr><td align="center">{screenshot_config.screenshot_name}<br/><br/><img src="{subdir}/{screenshot_config.screenshot_name}.png"></td></tr>"""
|
||||
locale_readme += """</table>\n"""
|
||||
|
||||
screencap_view(view_cls, view_args=view_args, view_name=view_name, toast_thread=toast_thread)
|
||||
readme += """ <table align="left" style="border: 1px solid gray;">"""
|
||||
readme += f"""<tr><td align="center">{view_name}<br/><br/><img src="{subdir}/{view_name}.png"></td></tr>"""
|
||||
readme += """</table>\n"""
|
||||
locale_readme += "</td></tr></table>"
|
||||
|
||||
readme += "</td></tr></table>"
|
||||
with open(os.path.join(screenshot_root, locale, "README.md"), 'w') as readme_file:
|
||||
readme_file.write(locale_readme)
|
||||
|
||||
# Re-render some screens that require more manual intervention / setup than the above
|
||||
# scripting can support.
|
||||
screenshot_renderer.set_screenshot_path(os.path.join(screenshot_root, "psbt_views"))
|
||||
print(f"Done with locale: {locale}.")
|
||||
|
||||
# Render the PSBTChangeDetailsView_multisig_unverified screenshot
|
||||
decoder = DecodeQR()
|
||||
decoder.add_data(BASE64_PSBT_1)
|
||||
controller.psbt = decoder.get_psbt()
|
||||
controller.psbt_seed = seed_12b
|
||||
controller.multisig_wallet_descriptor = None
|
||||
screencap_view(psbt_views.PSBTChangeDetailsView, view_name='PSBTChangeDetailsView_multisig_unverified', view_args=dict(change_address_num=0))
|
||||
# Write the main README; ensure it writes all locales, not just the one that may
|
||||
# have been specified for this run.
|
||||
with open(os.path.join("tests", "screenshot_generator", "template.md"), 'r') as readme_template:
|
||||
main_readme = readme_template.read()
|
||||
|
||||
controller.psbt_seed = None
|
||||
screencap_view(psbt_views.PSBTSelectSeedView, view_name='PSBTSelectSeedView')
|
||||
|
||||
# Render OP_RETURN screens for real
|
||||
controller.psbt_seed = seed_12b
|
||||
decoder = DecodeQR()
|
||||
decoder.add_data(BASE64_PSBT_WITH_OP_RETURN_TEXT)
|
||||
controller.psbt = decoder.get_psbt()
|
||||
controller.psbt_parser = PSBTParser(p=controller.psbt, seed=seed_12b)
|
||||
screencap_view(psbt_views.PSBTOverviewView, view_name='PSBTOverviewView_op_return')
|
||||
screencap_view(psbt_views.PSBTOpReturnView, view_name="PSBTOpReturnView_text")
|
||||
|
||||
decoder.add_data(BASE64_PSBT_WITH_OP_RETURN_RAW_BYTES)
|
||||
controller.psbt = decoder.get_psbt()
|
||||
controller.psbt_parser = PSBTParser(p=controller.psbt, seed=seed_12b)
|
||||
screencap_view(psbt_views.PSBTOpReturnView, view_name="PSBTOpReturnView_raw_hex_data")
|
||||
for locale, display_name in SettingsConstants.ALL_LOCALES:
|
||||
main_readme += f"* [{display_name}]({locale}/README.md)\n"
|
||||
|
||||
with open(os.path.join(screenshot_root, "README.md"), 'w') as readme_file:
|
||||
readme_file.write(readme)
|
||||
readme_file.write(main_readme)
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
# SeedSigner Screenshots
|
||||
|
||||
SeedSigner screenshots can be freely used in any tutorial, article, video, etc. As a courtesy, please link back to this repo or the SeedSigner website in your attribution.
|
||||
|
||||
 
|
||||
|
||||
 
|
||||
|
||||
|
||||
## Generating screenshots
|
||||
The screenshot generator is integrated into the SeedSigner test suite and requires a local copy of the SeedSigner repo with the test suite dependencies installed.
|
||||
|
||||
see: https://github.com/SeedSigner/seedsigner/blob/dev/tests/screenshot_generator/README.md
|
||||
|
||||
|
||||
## Currently supported or in-progress languages
|
||||
@@ -1,6 +1,11 @@
|
||||
import os
|
||||
|
||||
from dataclasses import dataclass
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
from seedsigner.gui.renderer import Renderer
|
||||
from seedsigner.gui.toast import BaseToastOverlayManagerThread
|
||||
from seedsigner.views.view import View
|
||||
|
||||
|
||||
|
||||
@@ -37,7 +42,7 @@ class ScreenshotRenderer(Renderer):
|
||||
self.screenshot_path = path
|
||||
|
||||
|
||||
def show_image(self, image=None, alpha_overlay=None, is_background_thread: bool = False):
|
||||
def show_image(self, image=None, alpha_overlay=None, is_background_thread: bool = False):
|
||||
if is_background_thread:
|
||||
return
|
||||
|
||||
@@ -53,3 +58,30 @@ class ScreenshotRenderer(Renderer):
|
||||
self.canvas.save(os.path.join(self.screenshot_path, self.screenshot_filename))
|
||||
raise ScreenshotComplete()
|
||||
|
||||
|
||||
|
||||
@dataclass
|
||||
class ScreenshotConfig:
|
||||
View_cls: View
|
||||
view_kwargs: dict = None
|
||||
screenshot_name: str = None
|
||||
toast_thread: BaseToastOverlayManagerThread = None
|
||||
run_before: callable = None
|
||||
run_after: callable = None
|
||||
|
||||
|
||||
def __post_init__(self):
|
||||
if not self.view_kwargs:
|
||||
self.view_kwargs = {}
|
||||
if not self.screenshot_name:
|
||||
self.screenshot_name = self.View_cls.__name__
|
||||
|
||||
|
||||
def run_callback_before(self):
|
||||
if self.run_before:
|
||||
self.run_before()
|
||||
|
||||
|
||||
def run_callback_after(self):
|
||||
if self.run_after:
|
||||
self.run_after()
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
import pytest
|
||||
from unittest.mock import MagicMock
|
||||
from seedsigner.models.seed import Seed
|
||||
from embit import bip39
|
||||
|
||||
from seedsigner.models.settings import SettingsConstants
|
||||
|
||||
|
||||
|
||||
def test_derive_child_mnemonic():
|
||||
|
||||
@@ -88,7 +88,7 @@ class TestController(BaseTest):
|
||||
controller = Controller.get_instance()
|
||||
|
||||
# Settings defaults
|
||||
assert controller.settings.get_value(SettingsConstants.SETTING__LANGUAGE) == SettingsConstants.LANGUAGE__ENGLISH
|
||||
assert controller.settings.get_value(SettingsConstants.SETTING__LOCALE) == SettingsConstants.LOCALE__ENGLISH
|
||||
assert controller.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE) == SettingsConstants.WORDLIST_LANGUAGE__ENGLISH
|
||||
assert controller.settings.get_value(SettingsConstants.SETTING__PERSISTENT_SETTINGS) == SettingsConstants.OPTION__DISABLED
|
||||
assert controller.settings.get_value(SettingsConstants.SETTING__COORDINATORS) == [i for i,j in SettingsConstants.ALL_COORDINATORS if i!="kpr"]
|
||||
|
||||
@@ -66,14 +66,28 @@ def test_specter_xpub_qr():
|
||||
def test_ur_xpub_qr():
|
||||
mnemonic = "obscure bone gas open exotic abuse virus bunker shuffle nasty ship dash"
|
||||
|
||||
e = UrXpubQrEncoder(
|
||||
seed=Seed(mnemonic.split(), passphrase="pass"),
|
||||
network=SettingsConstants.MAINNET,
|
||||
derivation="m/48h/1h/0h/2h",
|
||||
qr_density=SettingsConstants.DENSITY__MEDIUM
|
||||
)
|
||||
|
||||
assert e.next_part() == "UR:CRYPTO-ACCOUNT/1-4/LPADAACSKPCYMOMNLGRYHDCKOEADCYSSMECPONAOLYTAADMETAADDLOXAXHDCLAOKSRLNLKPUEGYATHPMNSNIYMUECBY"
|
||||
assert e.next_part() == "UR:CRYPTO-ACCOUNT/2-4/LPAOAACSKPCYMOMNLGRYHDCKKKGHZMLUZORPVDGUOTECSTTKTOLPCWPTNTLKZTTIZTBEAAHDCXVDTPMYRSTDMOPSCXFZ"
|
||||
assert e.next_part() == "UR:CRYPTO-ACCOUNT/3-4/LPAXAACSKPCYMOMNLGRYHDCKSPZSBZSPGERLGDATUYNLPYBTGYIYYKBTWTAOSWKSVTSGCHBYDKYAVDAMTAADMONDGDFD"
|
||||
assert e.next_part() == "UR:CRYPTO-ACCOUNT/4-4/LPAAAACSKPCYMOMNLGRYHDCKDYOTADLOCSDYYKADYKAEYKAOYKAOCYSSMECPONAXAAAYCYIOREKKJKAEAEAEWZWDMYON"
|
||||
|
||||
|
||||
e = UrXpubQrEncoder(
|
||||
seed=Seed(mnemonic.split(), passphrase="pass"),
|
||||
network=SettingsConstants.TESTNET,
|
||||
derivation="m/48h/1h/0h/2h",
|
||||
qr_density=SettingsConstants.DENSITY__MEDIUM
|
||||
)
|
||||
|
||||
assert e.next_part() == "UR:CRYPTO-ACCOUNT/1-4/LPADAACSKPCYMOMNLGRYHDCKOEADCYSSMECPONAOLYTAADMETAADDLOXAXHDCLAOKSRLNLKPUEGYATHPMNSNIYMUECBY"
|
||||
assert e.next_part() == "UR:CRYPTO-ACCOUNT/2-4/LPAOAACSKPCYMOMNLGRYHDCKKKGHZMLUZORPVDGUOTECSTTKTOLPCWPTNTLKZTTIZTBEAAHDCXVDTPMYRSTDMOPSCXFZ"
|
||||
assert e.next_part() == "UR:CRYPTO-ACCOUNT/3-4/LPAXAACSKPCYMOMNLGRYHDCKSPZSBZSPGERLGDATUYNLPYBTGYIYYKBTWTAOSWKSVTSGCHBYDKYAVDAMTAADMONDGDFD"
|
||||
assert e.next_part() == "UR:CRYPTO-ACCOUNT/4-4/LPAAAACSKPCYMOMNLGRYHDCKDYOTADLOCSDYYKADYKAEYKAOYKAOCYSSMECPONAXAAAYCYIOREKKJKAEAEAEWZWDMYON"
|
||||
|
||||
assert e.next_part() == "UR:CRYPTO-ACCOUNT/1-5/LPADAHCSKECYRTPEDKMOHDCFOEADCYSSMECPONAOLYTAADMETAADDLONAXHDCLAOKSRLNLKPUENSAHBTHS"
|
||||
assert e.next_part() == "UR:CRYPTO-ACCOUNT/2-5/LPAOAHCSKECYRTPEDKMOHDCFGYATHPMNSNKKGHZMLUZORPVDGUOTECSTTKTOLPCWPTNTLKZTTIZTNDJSCF"
|
||||
assert e.next_part() == "UR:CRYPTO-ACCOUNT/3-5/LPAXAHCSKECYRTPEDKMOHDCFZTBEAAHDCXVDTPMYRSTDSPZSBZSPGERLGDATUYNLPYBTGYIYYKBDFGWPKE"
|
||||
assert e.next_part() == "UR:CRYPTO-ACCOUNT/4-5/LPAAAHCSKECYRTPEDKMOHDCFBTWTAOSWKSVTSGCHBYDKYAVDAHTAADEHOYAOADAMTAADDYOTADGYBKBWFE"
|
||||
assert e.next_part() == "UR:CRYPTO-ACCOUNT/5-5/LPAHAHCSKECYRTPEDKMOHDCFLOCSDYYKADYKAEYKAOYKAOCYSSMECPONAXAAAYCYIOREKKJKAETODLFYWP"
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
from gettext import gettext as _
|
||||
|
||||
# Must import test base before the Controller
|
||||
from base import FlowTest, FlowStep
|
||||
|
||||
from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON, ButtonOption
|
||||
from seedsigner.models.settings_definition import SettingsConstants, SettingsDefinition
|
||||
from seedsigner.views import settings_views
|
||||
from seedsigner.views.view import MainMenuView
|
||||
|
||||
|
||||
|
||||
class TestL10nFlows(FlowTest):
|
||||
def test_change_locale(self):
|
||||
settings_entry = SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__LOCALE)
|
||||
spanish_display_name = [locale_tuple[1] for locale_tuple in SettingsConstants.ALL_LOCALES if locale_tuple[0] == SettingsConstants.LOCALE__SPANISH][0]
|
||||
|
||||
# Initially we get English
|
||||
assert _(MainMenuView.SCAN.button_label) == "Scan"
|
||||
|
||||
self.run_sequence([
|
||||
FlowStep(MainMenuView, button_data_selection=MainMenuView.SETTINGS),
|
||||
FlowStep(settings_views.SettingsMenuView, button_data_selection=ButtonOption(settings_entry.display_name)),
|
||||
FlowStep(settings_views.SettingsEntryUpdateSelectionView, button_data_selection=ButtonOption(spanish_display_name)),
|
||||
])
|
||||
|
||||
# Now we don't get English
|
||||
assert _(MainMenuView.SCAN.button_label) != "Scan"
|
||||
@@ -115,7 +115,7 @@ class TestPSBTFlows(FlowTest):
|
||||
FlowStep(psbt_views.PSBTSigningErrorView, button_data_selection=psbt_views.PSBTSigningErrorView.SELECT_DIFF_SEED),
|
||||
FlowStep(psbt_views.PSBTSelectSeedView, button_data_selection=psbt_views.PSBTSelectSeedView.SCAN_SEED),
|
||||
FlowStep(scan_views.ScanSeedQRView, before_run=load_seed_into_decoder),
|
||||
FlowStep(seed_views.SeedFinalizeView, button_data_selection=SettingsConstants.LABEL__BIP39_PASSPHRASE),
|
||||
FlowStep(seed_views.SeedFinalizeView, button_data_selection=seed_views.SeedFinalizeView.PASSPHRASE),
|
||||
FlowStep(seed_views.SeedAddPassphraseView, screen_return_value=dict(passphrase="abc")),
|
||||
FlowStep(seed_views.SeedReviewPassphraseView, button_data_selection=seed_views.SeedReviewPassphraseView.DONE),
|
||||
FlowStep(seed_views.SeedOptionsView, is_redirect=True),
|
||||
|
||||
+23
-12
@@ -5,7 +5,7 @@ import pytest
|
||||
from base import BaseTest, FlowTest, FlowStep
|
||||
from base import FlowTestInvalidButtonDataSelectionException
|
||||
|
||||
from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON
|
||||
from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON, ButtonOption
|
||||
from seedsigner.models.settings import Settings, SettingsConstants
|
||||
from seedsigner.models.seed import ElectrumSeed, Seed
|
||||
from seedsigner.views.view import ErrorView, MainMenuView, OptionDisabledView, View, NetworkMismatchErrorView
|
||||
@@ -40,10 +40,10 @@ class TestSeedFlows(FlowTest):
|
||||
self.run_sequence([
|
||||
FlowStep(MainMenuView, button_data_selection=MainMenuView.SCAN),
|
||||
FlowStep(scan_views.ScanView, before_run=load_seed_into_decoder), # simulate read SeedQR; ret val is ignored
|
||||
FlowStep(seed_views.SeedFinalizeView, button_data_selection=SettingsConstants.LABEL__BIP39_PASSPHRASE),
|
||||
FlowStep(seed_views.SeedFinalizeView, button_data_selection=seed_views.SeedFinalizeView.PASSPHRASE),
|
||||
FlowStep(seed_views.SeedAddPassphraseView, screen_return_value=dict(passphrase="muhpassphrase", is_back_button=True)),
|
||||
FlowStep(seed_views.SeedAddPassphraseExitDialogView, button_data_selection=seed_views.SeedAddPassphraseExitDialogView.DISCARD),
|
||||
FlowStep(seed_views.SeedFinalizeView, button_data_selection=SettingsConstants.LABEL__BIP39_PASSPHRASE),
|
||||
FlowStep(seed_views.SeedFinalizeView, button_data_selection=seed_views.SeedFinalizeView.PASSPHRASE),
|
||||
FlowStep(seed_views.SeedAddPassphraseView, screen_return_value=dict(passphrase="muhpassphrase", is_back_button=True)),
|
||||
FlowStep(seed_views.SeedAddPassphraseExitDialogView, button_data_selection=seed_views.SeedAddPassphraseExitDialogView.EDIT),
|
||||
FlowStep(seed_views.SeedAddPassphraseView, screen_return_value=dict(passphrase="muhpassphrase")),
|
||||
@@ -183,15 +183,18 @@ class TestSeedFlows(FlowTest):
|
||||
"""
|
||||
Selecting "Export XPUB" from the SeedOptionsView should enter the Export XPUB flow and end at the MainMenuView
|
||||
"""
|
||||
|
||||
def flowtest_standard_xpub(sig_tuple, script_tuple, coord_tuple):
|
||||
if sig_tuple[0] == SettingsConstants.SINGLE_SIG:
|
||||
sig_selection = seed_views.SeedExportXpubSigTypeView.SINGLE_SIG
|
||||
else:
|
||||
sig_selection = seed_views.SeedExportXpubSigTypeView.MULTISIG
|
||||
self.run_sequence(
|
||||
initial_destination_view_args=dict(seed_num=0),
|
||||
sequence=[
|
||||
FlowStep(seed_views.SeedOptionsView, button_data_selection=seed_views.SeedOptionsView.EXPORT_XPUB),
|
||||
FlowStep(seed_views.SeedExportXpubSigTypeView, button_data_selection=sig_tuple[1]),
|
||||
FlowStep(seed_views.SeedExportXpubScriptTypeView, button_data_selection=script_tuple[1]),
|
||||
FlowStep(seed_views.SeedExportXpubCoordinatorView, button_data_selection=coord_tuple[1]),
|
||||
FlowStep(seed_views.SeedExportXpubSigTypeView, button_data_selection=sig_selection),
|
||||
FlowStep(seed_views.SeedExportXpubScriptTypeView, button_data_selection=ButtonOption(script_tuple[1], return_data=script_tuple[0])),
|
||||
FlowStep(seed_views.SeedExportXpubCoordinatorView, button_data_selection=ButtonOption(coord_tuple[1], return_data=coord_tuple[0])),
|
||||
FlowStep(seed_views.SeedExportXpubWarningView, screen_return_value=0),
|
||||
FlowStep(seed_views.SeedExportXpubDetailsView, screen_return_value=0),
|
||||
FlowStep(seed_views.SeedExportXpubQRDisplayView, screen_return_value=0),
|
||||
@@ -303,10 +306,18 @@ class TestSeedFlows(FlowTest):
|
||||
SettingsConstants.CUSTOM_DERIVATION
|
||||
])
|
||||
|
||||
# get display names to access button choices in the views (ugh: hardcoding, is there a better way?)
|
||||
sig_type = self.settings.get_multiselect_value_display_names(SettingsConstants.SETTING__SIG_TYPES)[0] # single sig
|
||||
script_type = self.settings.get_multiselect_value_display_names(SettingsConstants.SETTING__SCRIPT_TYPES)[2] # custom derivation
|
||||
coordinator = self.settings.get_multiselect_value_display_names(SettingsConstants.SETTING__COORDINATORS)[3] # specter
|
||||
# Ensure that all coordinators are enabled
|
||||
self.settings.set_value(SettingsConstants.SETTING__COORDINATORS, [x for x, y in SettingsConstants.ALL_COORDINATORS])
|
||||
|
||||
# Set up button_data selections
|
||||
sig_type = seed_views.SeedExportXpubSigTypeView.SINGLE_SIG
|
||||
|
||||
custom_derivation = SettingsConstants.CUSTOM_DERIVATION
|
||||
script_type = ButtonOption(self.settings.get_multiselect_value_display_names(SettingsConstants.SETTING__SCRIPT_TYPES)[2], return_data=custom_derivation)
|
||||
|
||||
specter = SettingsConstants.COORDINATOR__SPECTER_DESKTOP
|
||||
assert SettingsConstants.ALL_COORDINATORS[3][0] == specter
|
||||
coordinator = ButtonOption(self.settings.get_multiselect_value_display_names(SettingsConstants.SETTING__COORDINATORS)[3], return_data=specter)
|
||||
|
||||
self.run_sequence(
|
||||
initial_destination_view_args=dict(seed_num=0),
|
||||
@@ -377,7 +388,7 @@ class TestSeedFlows(FlowTest):
|
||||
|
||||
# Skips past the script type options via redirect
|
||||
FlowStep(seed_views.SeedExportXpubScriptTypeView, is_redirect=True),
|
||||
FlowStep(seed_views.SeedExportXpubCoordinatorView, button_data_selection=self.settings.get_multiselect_value_display_names(SettingsConstants.SETTING__COORDINATORS)[0]),
|
||||
FlowStep(seed_views.SeedExportXpubCoordinatorView, button_data_selection=ButtonOption(self.settings.get_multiselect_value_display_names(SettingsConstants.SETTING__COORDINATORS)[0], return_data=SettingsConstants.ALL_COORDINATORS[0][0])),
|
||||
FlowStep(seed_views.SeedExportXpubWarningView, screen_return_value=0),
|
||||
FlowStep(seed_views.SeedExportXpubDetailsView, screen_return_value=0),
|
||||
FlowStep(seed_views.SeedExportXpubQRDisplayView, screen_return_value=0),
|
||||
|
||||
@@ -8,7 +8,7 @@ from base import FlowTest, FlowStep
|
||||
|
||||
from seedsigner.models.settings import Settings
|
||||
from seedsigner.models.settings_definition import SettingsDefinition, SettingsConstants
|
||||
from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON
|
||||
from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON, ButtonOption
|
||||
from seedsigner.hardware.microsd import MicroSD
|
||||
from seedsigner.views.view import MainMenuView
|
||||
from seedsigner.views import scan_views, settings_views
|
||||
@@ -26,8 +26,8 @@ class TestSettingsFlows(FlowTest):
|
||||
|
||||
self.run_sequence([
|
||||
FlowStep(MainMenuView, button_data_selection=MainMenuView.SETTINGS),
|
||||
FlowStep(settings_views.SettingsMenuView, button_data_selection=settings_entry.display_name),
|
||||
FlowStep(settings_views.SettingsEntryUpdateSelectionView, button_data_selection=settings_entry.get_selection_option_display_name_by_value(SettingsConstants.OPTION__ENABLED)),
|
||||
FlowStep(settings_views.SettingsMenuView, button_data_selection=ButtonOption(settings_entry.display_name)),
|
||||
FlowStep(settings_views.SettingsEntryUpdateSelectionView, button_data_selection=ButtonOption(settings_entry.get_selection_option_display_name_by_value(SettingsConstants.OPTION__ENABLED))),
|
||||
FlowStep(settings_views.SettingsEntryUpdateSelectionView, screen_return_value=RET_CODE__BACK_BUTTON),
|
||||
FlowStep(settings_views.SettingsMenuView),
|
||||
])
|
||||
@@ -43,7 +43,7 @@ class TestSettingsFlows(FlowTest):
|
||||
|
||||
self.run_sequence([
|
||||
FlowStep(MainMenuView, button_data_selection=MainMenuView.SETTINGS),
|
||||
FlowStep(settings_views.SettingsMenuView, button_data_selection=settings_entry.display_name),
|
||||
FlowStep(settings_views.SettingsMenuView, button_data_selection=ButtonOption(settings_entry.display_name)),
|
||||
FlowStep(settings_views.SettingsEntryUpdateSelectionView, screen_return_value=0), # select/deselect first option
|
||||
FlowStep(settings_views.SettingsEntryUpdateSelectionView, screen_return_value=1), # select/deselect second option
|
||||
FlowStep(settings_views.SettingsEntryUpdateSelectionView, screen_return_value=1), # select/deselect second option
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
from base import FlowTest, FlowStep
|
||||
|
||||
from seedsigner.controller import Controller
|
||||
from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON
|
||||
from seedsigner.gui.screens.screen import RET_CODE__BACK_BUTTON, ButtonOption
|
||||
from seedsigner.models.seed import Seed
|
||||
from seedsigner.models.settings_definition import SettingsConstants, SettingsDefinition
|
||||
from seedsigner.views.view import ErrorView, MainMenuView
|
||||
@@ -25,7 +25,7 @@ class TestToolsFlows(FlowTest):
|
||||
FlowStep(MainMenuView, button_data_selection=MainMenuView.TOOLS),
|
||||
FlowStep(tools_views.ToolsMenuView, button_data_selection=tools_views.ToolsMenuView.ADDRESS_EXPLORER),
|
||||
FlowStep(tools_views.ToolsAddressExplorerSelectSourceView, screen_return_value=0), # ret 1st onboard seed
|
||||
FlowStep(seed_views.SeedExportXpubScriptTypeView, button_data_selection=SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__SCRIPT_TYPES).get_selection_option_display_name_by_value(SettingsConstants.NATIVE_SEGWIT)),
|
||||
FlowStep(seed_views.SeedExportXpubScriptTypeView, button_data_selection=ButtonOption(SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__SCRIPT_TYPES).get_selection_option_display_name_by_value(SettingsConstants.NATIVE_SEGWIT), return_data=SettingsConstants.NATIVE_SEGWIT)),
|
||||
FlowStep(tools_views.ToolsAddressExplorerAddressTypeView, button_data_selection=tools_views.ToolsAddressExplorerAddressTypeView.RECEIVE),
|
||||
FlowStep(tools_views.ToolsAddressExplorerAddressListView, screen_return_value=10), # ret NEXT page of addrs
|
||||
FlowStep(tools_views.ToolsAddressExplorerAddressListView, screen_return_value=4), # ret a specific addr from the list
|
||||
@@ -62,7 +62,7 @@ class TestToolsFlows(FlowTest):
|
||||
# Finalize the new seed w/passphrase
|
||||
self.run_sequence(
|
||||
sequence=[
|
||||
FlowStep(seed_views.SeedFinalizeView, button_data_selection=SettingsConstants.LABEL__BIP39_PASSPHRASE),
|
||||
FlowStep(seed_views.SeedFinalizeView, button_data_selection=seed_views.SeedFinalizeView.PASSPHRASE),
|
||||
FlowStep(seed_views.SeedAddPassphraseView, screen_return_value=dict(passphrase="mypassphrase")),
|
||||
FlowStep(seed_views.SeedReviewPassphraseView, button_data_selection=seed_views.SeedReviewPassphraseView.DONE),
|
||||
FlowStep(seed_views.SeedOptionsView, is_redirect=True),
|
||||
@@ -242,6 +242,31 @@ class TestToolsFlows(FlowTest):
|
||||
FlowStep(scan_views.ScanWalletDescriptorView, before_run=load_descriptor_into_decoder), # simulate read descriptor QR
|
||||
FlowStep(seed_views.MultisigWalletDescriptorView, screen_return_value=0),
|
||||
FlowStep(seed_views.SeedAddressVerificationView),
|
||||
FlowStep(seed_views.AddressVerificationSuccessView),
|
||||
FlowStep(seed_views.SeedAddressVerificationSuccessView),
|
||||
])
|
||||
|
||||
|
||||
def test__verify_address__singlesig__flow(self):
|
||||
"""
|
||||
Address Explorer should be able to scan a singlesig address and
|
||||
verify it against a loaded key.
|
||||
"""
|
||||
controller = Controller.get_instance()
|
||||
controller.storage.set_pending_seed(Seed(mnemonic=["abandon "* 11 + "about"]))
|
||||
controller.storage.finalize_pending_seed()
|
||||
settings = controller.settings
|
||||
settings.set_value(SettingsConstants.SETTING__NETWORK, SettingsConstants.REGTEST)
|
||||
|
||||
def load_address_into_decoder(view: scan_views.ScanView):
|
||||
# Native segwit regtest receive addr @ index 6
|
||||
view.decoder.add_data("bcrt1q4e9q5taxnsvc6m0uxv6h75mkzvnkxeqk6l90u2")
|
||||
|
||||
self.run_sequence([
|
||||
FlowStep(MainMenuView, button_data_selection=MainMenuView.TOOLS),
|
||||
FlowStep(tools_views.ToolsMenuView, button_data_selection=tools_views.ToolsMenuView.VERIFY_ADDRESS),
|
||||
FlowStep(scan_views.ScanAddressView, before_run=load_address_into_decoder), # simulate read address QR
|
||||
FlowStep(seed_views.AddressVerificationStartView, is_redirect=True),
|
||||
FlowStep(seed_views.SeedSelectSeedView, screen_return_value=0),
|
||||
FlowStep(seed_views.SeedAddressVerificationView),
|
||||
FlowStep(seed_views.SeedAddressVerificationSuccessView),
|
||||
])
|
||||
|
||||
@@ -28,14 +28,6 @@ class TestViewFlows(FlowTest):
|
||||
"""
|
||||
Basic flow from MainMenuView to PowerOffView
|
||||
"""
|
||||
with patch('seedsigner.views.view.PowerOffView.PowerOffThread'):
|
||||
self.run_sequence([
|
||||
FlowStep(MainMenuView, screen_return_value=RET_CODE__POWER_BUTTON),
|
||||
FlowStep(PowerOptionsView, button_data_selection=PowerOptionsView.POWER_OFF),
|
||||
FlowStep(PowerOffView),
|
||||
])
|
||||
|
||||
# And again, but this time as if we were in the SeedSigner OS
|
||||
Settings.HOSTNAME = Settings.SEEDSIGNER_OS
|
||||
self.run_sequence([
|
||||
FlowStep(MainMenuView, screen_return_value=RET_CODE__POWER_BUTTON),
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
from gettext import gettext as _
|
||||
|
||||
from base import BaseTest
|
||||
from seedsigner.gui.screens.screen import ButtonOption
|
||||
from seedsigner.helpers.l10n import mark_for_translation as _mft
|
||||
from seedsigner.models.settings import Settings
|
||||
from seedsigner.models.settings_definition import SettingsConstants
|
||||
from seedsigner.views.view import MainMenuView
|
||||
|
||||
|
||||
|
||||
class TestGettext(BaseTest):
|
||||
def test_english_as_default(self):
|
||||
# Key is available in other languages, but we get English back
|
||||
assert _("Home") == "Home"
|
||||
|
||||
def test_missing_key_returns_english_key(self):
|
||||
test_str = "This is not in our translation library"
|
||||
assert _(test_str) == test_str
|
||||
|
||||
|
||||
def test_basic_spanish(self):
|
||||
settings = Settings.get_instance()
|
||||
settings.set_value(SettingsConstants.SETTING__LOCALE, SettingsConstants.LOCALE__SPANISH)
|
||||
assert _("Home") != "Home"
|
||||
|
||||
|
||||
def test_locale_changes(self):
|
||||
settings = Settings.get_instance()
|
||||
|
||||
settings.set_value(SettingsConstants.SETTING__LOCALE, SettingsConstants.LOCALE__SPANISH)
|
||||
spanish_str = _("Home")
|
||||
|
||||
settings.set_value(SettingsConstants.SETTING__LOCALE, SettingsConstants.LOCALE__ENGLISH)
|
||||
assert spanish_str != _("Home")
|
||||
assert _("Home") == "Home"
|
||||
|
||||
|
||||
|
||||
class TestButtonOption(BaseTest):
|
||||
def test_english_key_not_translated(self):
|
||||
""" ButtonOption should always return its English key, regardless of current locale setting. """
|
||||
button_option = ButtonOption("Home")
|
||||
settings = Settings.get_instance()
|
||||
settings.set_value(SettingsConstants.SETTING__LOCALE, SettingsConstants.LOCALE__SPANISH)
|
||||
|
||||
assert button_option.button_label == "Home"
|
||||
|
||||
brand_new_button_option = ButtonOption("Tools")
|
||||
assert brand_new_button_option.button_label == "Tools"
|
||||
|
||||
|
||||
def test_class_level_button_option_english_key_not_translated(self):
|
||||
settings = Settings.get_instance()
|
||||
settings.set_value(SettingsConstants.SETTING__LOCALE, SettingsConstants.LOCALE__SPANISH)
|
||||
|
||||
class FooClass:
|
||||
HOME = ButtonOption("Home")
|
||||
|
||||
assert FooClass.HOME.button_label == "Home"
|
||||
|
||||
|
||||
def test_gettext_translates_class_level_button_option(self):
|
||||
settings = Settings.get_instance()
|
||||
settings.set_value(SettingsConstants.SETTING__LOCALE, SettingsConstants.LOCALE__SPANISH)
|
||||
|
||||
class BarClass:
|
||||
HOME = ButtonOption("Home")
|
||||
|
||||
assert _(BarClass.HOME.button_label) != "Home"
|
||||
|
||||
|
||||
|
||||
class TestMarkForTranslation(BaseTest):
|
||||
def test_english_key_not_translated(self):
|
||||
""" _mft() should always return its English key, regardless of current locale setting. """
|
||||
mft_attr = _mft("Home")
|
||||
settings = Settings.get_instance()
|
||||
settings.set_value(SettingsConstants.SETTING__LOCALE, SettingsConstants.LOCALE__SPANISH)
|
||||
|
||||
assert mft_attr == "Home"
|
||||
|
||||
brand_new_mft_attr = _mft("Tools")
|
||||
assert brand_new_mft_attr == "Tools"
|
||||
|
||||
|
||||
def test_class_level_mft_attr_english_key_not_translated(self):
|
||||
settings = Settings.get_instance()
|
||||
settings.set_value(SettingsConstants.SETTING__LOCALE, SettingsConstants.LOCALE__SPANISH)
|
||||
|
||||
class FooClass:
|
||||
home = _mft("Home")
|
||||
|
||||
assert FooClass.home == "Home"
|
||||
|
||||
|
||||
def test_gettext_translates_class_level_mft_attr(self):
|
||||
settings = Settings.get_instance()
|
||||
settings.set_value(SettingsConstants.SETTING__LOCALE, SettingsConstants.LOCALE__SPANISH)
|
||||
|
||||
class BarClass:
|
||||
home = _mft("Home")
|
||||
|
||||
assert _(BarClass.home) != "Home"
|
||||
+25
-1
@@ -4,7 +4,6 @@ from seedsigner.helpers.qr import QR
|
||||
from seedsigner.models.decode_qr import DecodeQR, DecodeQRStatus
|
||||
from seedsigner.models.encode_qr import SeedQrEncoder, CompactSeedQrEncoder
|
||||
from seedsigner.models.qr_type import QRType
|
||||
from seedsigner.models.seed import Seed
|
||||
|
||||
|
||||
|
||||
@@ -104,3 +103,28 @@ def test_compact_seedqr_handles_null_bytes():
|
||||
# 12-word seed, multiple null bytes in a row
|
||||
entropy = os.urandom(10) + b'\x00\x00' + os.urandom(4)
|
||||
run_encode_decode_test(entropy, mnemonic_length=12, qr_type=QRType.SEED__COMPACTSEEDQR)
|
||||
|
||||
|
||||
def test_compact_seedqr_bytes_interpretable_as_str():
|
||||
"""
|
||||
Should successfully decode a Compact SeedQR whose bytes can be interpreted as a valid
|
||||
string. Most Compact SeedQR byte data will raise a UnicodeDecodeError when attempting to
|
||||
interpret it as a string, but edge cases are possible.
|
||||
|
||||
see: Issue #656
|
||||
"""
|
||||
# Randomly generated to pass the str.decode() step; 12- and 24-word entropy.
|
||||
entropy_bytes_tests = [
|
||||
b'\x00' * 16, # abandon * 11 + about
|
||||
b'\x12\x15\\1j`3\x0bkL}f\x00ZYK',
|
||||
b'tv\x1bZjmqN@t\x13\x1aK\\v)',
|
||||
b'|9\x05\x1aHF9j\xda\xb6v\x05\x08#\x12=',
|
||||
b"iHK`4\x1a5\xd3\xaf\xd3\xb47htJ.}<\xea\xbf\x88Xh\x01.?R2^\xc2\xb1'",
|
||||
b'|Z\x11\x1dt\xdd\x97~t&f &G$H|^[\xd3\x9d<q]z\x14.\x11`!\xd1\x91',
|
||||
b'0\xd4\xb3\\,\xcd\x8d7c/Rp\x0e\xc2\xbb\xe4\x99\xa3=j5,\xcc\x9a[>\x19Z{\ng^',
|
||||
]
|
||||
|
||||
for entropy_bytes in entropy_bytes_tests:
|
||||
entropy_bytes.decode() # should not raise an exception
|
||||
mnemonic_length = 12 if len(entropy_bytes) == 16 else 24
|
||||
run_encode_decode_test(entropy_bytes, mnemonic_length=mnemonic_length, qr_type=QRType.SEED__COMPACTSEEDQR)
|
||||
|
||||
@@ -31,9 +31,9 @@ if __name__ == "__main__":
|
||||
seed_phrase = input("\nEnter 12- or 24-word test seed phrase: ").strip().split(" ")
|
||||
|
||||
if format == COMPACT:
|
||||
encoder = CompactSeedQrEncoder(seed_phrase=seed_phrase, wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH)
|
||||
encoder = CompactSeedQrEncoder(mnemonic=seed_phrase, wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH)
|
||||
else:
|
||||
encoder = SeedQrEncoder(seed_phrase=seed_phrase, wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH)
|
||||
encoder = SeedQrEncoder(mnemonic=seed_phrase, wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH)
|
||||
|
||||
qr = qrcode.QRCode( version=1, error_correction=qrcode.constants.ERROR_CORRECT_L, box_size=5, border=3)
|
||||
qr.add_data(encoder.next_part())
|
||||
|
||||
Reference in New Issue
Block a user