From d045e0089d9407a21f672e4bc4f7a5f268fdec92 Mon Sep 17 00:00:00 2001 From: grdddj Date: Thu, 20 Oct 2022 14:26:06 +0200 Subject: [PATCH] feat(core/ui): delete old UI code --- ci/build.yml | 15 - ci/deploy.yml | 19 - ci/prepare_ui_artifacts.py | 3 - ci/test.yml | 49 +- core/Makefile | 17 +- core/SConscript.firmware | 51 +- core/SConscript.unix | 51 +- core/src/all_modules.py | 56 - core/src/trezor/res/resources.py | 98 -- core/src/trezor/res/resources.py.mako | 26 - core/src/trezor/ui/__init__.py | 86 +- .../trezor/ui/components/common/__init__.py | 11 - .../trezor/ui/components/common/confirm.py | 49 +- core/src/trezor/ui/components/common/text.py | 536 -------- core/src/trezor/ui/components/tt/button.py | 253 ---- core/src/trezor/ui/components/tt/checklist.py | 67 - core/src/trezor/ui/components/tt/confirm.py | 257 ---- core/src/trezor/ui/components/tt/info.py | 87 -- .../trezor/ui/components/tt/keyboard_bip39.py | 220 ---- .../ui/components/tt/keyboard_slip39.py | 230 ---- core/src/trezor/ui/components/tt/num_input.py | 51 - .../src/trezor/ui/components/tt/passphrase.py | 262 ---- core/src/trezor/ui/components/tt/pin.py | 183 --- core/src/trezor/ui/components/tt/recovery.py | 41 - core/src/trezor/ui/components/tt/reset.py | 156 --- core/src/trezor/ui/components/tt/scroll.py | 430 ------- core/src/trezor/ui/components/tt/swipe.py | 114 -- core/src/trezor/ui/components/tt/text.py | 145 --- core/src/trezor/ui/components/tt/webauthn.py | 34 - .../trezor/ui/components/tt/word_select.py | 56 - core/src/trezor/ui/constants/__init__.py | 10 - core/src/trezor/ui/constants/t1.py | 9 - core/src/trezor/ui/constants/tr.py | 9 - core/src/trezor/ui/constants/tt.py | 16 - core/src/trezor/ui/container.py | 16 - core/src/trezor/ui/layouts/__init__.py | 11 +- core/src/trezor/ui/layouts/altcoin.py | 7 +- core/src/trezor/ui/layouts/common.py | 7 +- core/src/trezor/ui/layouts/recovery.py | 7 +- core/src/trezor/ui/layouts/reset.py | 7 +- core/src/trezor/ui/layouts/t1.py | 13 +- core/src/trezor/ui/layouts/tt/__init__.py | 1122 ----------------- core/src/trezor/ui/layouts/tt/altcoin.py | 81 -- core/src/trezor/ui/layouts/tt/recovery.py | 135 -- core/src/trezor/ui/layouts/tt/reset.py | 363 ------ core/src/trezor/ui/layouts/tt/webauthn.py | 34 - core/src/trezor/ui/layouts/webauthn.py | 7 +- core/src/trezor/ui/loader.py | 8 - core/src/trezor/ui/qr.py | 44 - core/src/trezor/ui/style.py | 42 +- core/tests/test_trezor.ui.text.py | 150 --- docs/ci/jobs.md | 115 +- tests/ui_tests/__init__.py | 3 - 53 files changed, 114 insertions(+), 5755 deletions(-) delete mode 100644 core/src/trezor/res/resources.py.mako delete mode 100644 core/src/trezor/ui/components/common/text.py delete mode 100644 core/src/trezor/ui/components/tt/button.py delete mode 100644 core/src/trezor/ui/components/tt/checklist.py delete mode 100644 core/src/trezor/ui/components/tt/confirm.py delete mode 100644 core/src/trezor/ui/components/tt/info.py delete mode 100644 core/src/trezor/ui/components/tt/keyboard_bip39.py delete mode 100644 core/src/trezor/ui/components/tt/keyboard_slip39.py delete mode 100644 core/src/trezor/ui/components/tt/num_input.py delete mode 100644 core/src/trezor/ui/components/tt/passphrase.py delete mode 100644 core/src/trezor/ui/components/tt/pin.py delete mode 100644 core/src/trezor/ui/components/tt/recovery.py delete mode 100644 core/src/trezor/ui/components/tt/reset.py delete mode 100644 core/src/trezor/ui/components/tt/scroll.py delete mode 100644 core/src/trezor/ui/components/tt/swipe.py delete mode 100644 core/src/trezor/ui/components/tt/text.py delete mode 100644 core/src/trezor/ui/components/tt/webauthn.py delete mode 100644 core/src/trezor/ui/components/tt/word_select.py delete mode 100644 core/src/trezor/ui/constants/__init__.py delete mode 100644 core/src/trezor/ui/constants/t1.py delete mode 100644 core/src/trezor/ui/constants/tr.py delete mode 100644 core/src/trezor/ui/constants/tt.py delete mode 100644 core/src/trezor/ui/container.py delete mode 100644 core/src/trezor/ui/layouts/tt/__init__.py delete mode 100644 core/src/trezor/ui/layouts/tt/altcoin.py delete mode 100644 core/src/trezor/ui/layouts/tt/recovery.py delete mode 100644 core/src/trezor/ui/layouts/tt/reset.py delete mode 100644 core/src/trezor/ui/layouts/tt/webauthn.py delete mode 100644 core/src/trezor/ui/qr.py delete mode 100644 core/tests/test_trezor.ui.text.py diff --git a/ci/build.yml b/ci/build.yml index 8d996fc7d..d15929d74 100644 --- a/ci/build.yml +++ b/ci/build.yml @@ -251,21 +251,6 @@ core unix frozen debug build: untracked: true expire_in: 1 week - -core unix frozen ui2 debug build: - stage: build - <<: *gitlab_caching - needs: [] - variables: - PYOPT: "0" - UI2: "1" - script: - - nix-shell --run "poetry run make -C core build_unix_frozen" - artifacts: - name: "$CI_JOB_NAME-$CI_COMMIT_SHORT_SHA" - untracked: true - expire_in: 1 week - core unix frozen debug asan build: stage: build <<: *gitlab_caching diff --git a/ci/deploy.yml b/ci/deploy.yml index 3b0a9ac10..b34e76f26 100644 --- a/ci/deploy.yml +++ b/ci/deploy.yml @@ -246,25 +246,6 @@ ui tests fixtures deploy: tags: - deploy -ui tests ui2 fixtures deploy: - stage: deploy - variables: - DEPLOY_PATH: "${DEPLOY_BASE_DIR}/ui_tests/" - BUCKET: "data.trezor.io" - GIT_SUBMODULE_STRATEGY: "none" - before_script: [] # no poetry - needs: - - core device ui2 test - script: - - echo "Deploying to $DEPLOY_PATH" - - rsync --delete -va ci/ui_test_records/* "$DEPLOY_PATH" - - source ${AWS_DEPLOY_DATA} - - aws s3 sync $DEPLOY_PATH s3://$BUCKET/dev/firmware/ui_tests - # This "hack" is needed because aws does not have an easy option to generate autoindex. We fetch the one autogenerated by nginx on local server. - - wget https://firmware.corp.sldev.cz/ui_tests/ -O index.html && aws s3 cp index.html s3://$BUCKET/dev/firmware/ui_tests/ - tags: - - deploy - # sync to aws sync emulators to aws: diff --git a/ci/prepare_ui_artifacts.py b/ci/prepare_ui_artifacts.py index 3f2589ce8..0efa556a2 100644 --- a/ci/prepare_ui_artifacts.py +++ b/ci/prepare_ui_artifacts.py @@ -1,4 +1,3 @@ -import os import shutil import sys from pathlib import Path @@ -18,8 +17,6 @@ if len(sys.argv) > 1 and sys.argv[1].upper() == "T1": model = "T1" else: model = "TT" -if os.getenv("UI2") == "1": - model += "ui2" model_file_hashes = {k: v for k, v in FILE_HASHES.items() if k.startswith(f"{model}_")} for test_case, expected_hash in model_file_hashes.items(): diff --git a/ci/test.yml b/ci/test.yml index e5df4d288..37cdc8dc6 100644 --- a/ci/test.yml +++ b/ci/test.yml @@ -11,8 +11,8 @@ image: registry.gitlab.com/satoshilabs/trezor/trezor-firmware/trezor-firmware-en # Core -# Python and rust unit tests, checking TT functionality. -core unit test: +# Python unit tests, checking core functionality. +core unit python test: stage: test <<: *gitlab_caching needs: @@ -20,14 +20,15 @@ core unit test: script: - nix-shell --run "poetry run make -C core test | ts -s" -core unit ui2 test: +# Rust unit tests. +core unit rust test: stage: test <<: *gitlab_caching needs: - - core unix frozen ui2 debug build + - core unix frozen debug build script: - nix-shell --run "poetry run make -C core clippy | ts -s" - - nix-shell --run "poetry run make -C core test_rust UI2=1 | ts -s" + - nix-shell --run "poetry run make -C core test_rust | ts -s" core unit asan test: stage: test @@ -43,8 +44,8 @@ core unit asan test: LSAN_OPTIONS: "suppressions=../../asan_suppressions.txt" script: - nix-shell --run "poetry run make -C core test | ts -s" - - nix-shell --run "poetry run make -C core clean build_unix UI2=1 | ts -s" - - nix-shell --run "poetry run make -C core test_rust UI2=1 | ts -s" + - nix-shell --run "poetry run make -C core clean build_unix | ts -s" + - nix-shell --run "poetry run make -C core test_rust | ts -s" core unit t1 test: stage: test @@ -90,40 +91,6 @@ core device test: reports: junit: tests/junit.xml -core device ui2 test: - stage: test - <<: *gitlab_caching - when: manual - needs: - - core unix frozen ui2 debug build - variables: - TREZOR_PROFILING: "1" - UI2: "1" - script: - - nix-shell --run "poetry run make -C core test_emu_ui | ts -s" - # - mv core/src/.coverage core/.coverage.test_emu - after_script: - - mv tests/ui_tests/reporting/reports/test/ test_ui_report - - nix-shell --run "poetry run python ci/prepare_ui_artifacts.py TTui2 | ts -s" - - diff tests/ui_tests/fixtures.json tests/ui_tests/fixtures.suggestion.json - - nix-shell --run "cd tests/ui_tests ; poetry run python reporting/report_master_diff.py TTui2_" - - mv tests/ui_tests/reporting/reports/master_diff/ . - artifacts: - name: "$CI_JOB_NAME-$CI_COMMIT_SHORT_SHA" - paths: - - ci/ui_test_records/ - - test_ui_report - - tests/ui_tests/screens/ - - tests/ui_tests/fixtures.suggestion.json - - tests/junit.xml - - tests/trezor.log - # - core/.coverage.* - - master_diff - when: always - expire_in: 1 week - reports: - junit: tests/junit.xml - core device asan test: stage: test <<: *gitlab_caching diff --git a/core/Makefile b/core/Makefile index 3307c76a8..78ed915e3 100644 --- a/core/Makefile +++ b/core/Makefile @@ -24,7 +24,6 @@ BITCOIN_ONLY ?= 0 TREZOR_MODEL ?= T TREZOR_MEMPERF ?= 0 ADDRESS_SANITIZER ?= 0 -UI2 ?= 0 # OpenOCD interface default. Alternative: ftdi/olimex-arm-usb-tiny-h OPENOCD_INTERFACE ?= stlink @@ -107,16 +106,16 @@ test_emu_click: ## run click tests $(EMU_TEST) $(PYTEST) $(TESTPATH)/click_tests $(TESTOPTS) test_emu_ui: ## run ui integration tests - UI2="$(UI2)" $(EMU_TEST) $(PYTEST) $(TESTPATH)/device_tests --ui=test --ui-check-missing $(TESTOPTS) + $(EMU_TEST) $(PYTEST) $(TESTPATH)/device_tests --ui=test --ui-check-missing $(TESTOPTS) test_emu_ui_multicore: ## run ui integration tests using multiple cores - UI2="$(UI2)" $(PYTEST) -n auto $(TESTPATH)/device_tests $(TESTOPTS) --ui=test --ui-check-missing --control-emulators --model=core --random-order-seed=$(shell echo $$RANDOM) + $(PYTEST) -n auto $(TESTPATH)/device_tests $(TESTOPTS) --ui=test --ui-check-missing --control-emulators --model=core --random-order-seed=$(shell echo $$RANDOM) test_emu_ui_record: ## record and hash screens for ui integration tests - UI2="$(UI2)" $(EMU_TEST) $(PYTEST) $(TESTPATH)/device_tests --ui=record --ui-check-missing $(TESTOPTS) + $(EMU_TEST) $(PYTEST) $(TESTPATH)/device_tests --ui=record --ui-check-missing $(TESTOPTS) test_emu_ui_record_multicore: ## record and hash screens for ui integration tests using multiple cores - UI2="$(UI2)" $(PYTEST) -n auto $(TESTPATH)/device_tests $(TESTOPTS) --ui=record --ui-check-missing --control-emulators --model=core --random-order-seed=$(shell echo $$RANDOM) + $(PYTEST) -n auto $(TESTPATH)/device_tests $(TESTOPTS) --ui=record --ui-check-missing --control-emulators --model=core --random-order-seed=$(shell echo $$RANDOM) pylint: ## run pylint on application sources and tests pylint -E $(shell find src tests -name *.py) @@ -165,16 +164,16 @@ build_reflash: ## build reflash firmware + reflash image dd if=build/bootloader/bootloader.bin of=$(REFLASH_BUILD_DIR)/sdimage.bin bs=1 seek=49152 build_firmware: templates build_cross ## build firmware with frozen modules - $(SCONS) CFLAGS="$(CFLAGS)" PRODUCTION="$(PRODUCTION)" TREZOR_MODEL="$(TREZOR_MODEL)" PYOPT="$(PYOPT)" BITCOIN_ONLY="$(BITCOIN_ONLY)" UI2="$(UI2)" $(FIRMWARE_BUILD_DIR)/firmware.bin + $(SCONS) CFLAGS="$(CFLAGS)" PRODUCTION="$(PRODUCTION)" TREZOR_MODEL="$(TREZOR_MODEL)" PYOPT="$(PYOPT)" BITCOIN_ONLY="$(BITCOIN_ONLY)" $(FIRMWARE_BUILD_DIR)/firmware.bin build_unix: templates ## build unix port - $(SCONS) CFLAGS="$(CFLAGS)" $(UNIX_BUILD_DIR)/trezor-emu-core $(UNIX_PORT_OPTS) TREZOR_MODEL="$(TREZOR_MODEL)" PYOPT="0" BITCOIN_ONLY="$(BITCOIN_ONLY)" TREZOR_EMULATOR_ASAN="$(ADDRESS_SANITIZER)" UI2="$(UI2)" + $(SCONS) CFLAGS="$(CFLAGS)" $(UNIX_BUILD_DIR)/trezor-emu-core $(UNIX_PORT_OPTS) TREZOR_MODEL="$(TREZOR_MODEL)" PYOPT="0" BITCOIN_ONLY="$(BITCOIN_ONLY)" TREZOR_EMULATOR_ASAN="$(ADDRESS_SANITIZER)" build_unix_frozen: templates build_cross ## build unix port with frozen modules - $(SCONS) CFLAGS="$(CFLAGS)" $(UNIX_BUILD_DIR)/trezor-emu-core $(UNIX_PORT_OPTS) TREZOR_MODEL="$(TREZOR_MODEL)" PYOPT="$(PYOPT)" BITCOIN_ONLY="$(BITCOIN_ONLY)" TREZOR_EMULATOR_ASAN="$(ADDRESS_SANITIZER)" UI2="$(UI2)" TREZOR_MEMPERF="$(TREZOR_MEMPERF)" TREZOR_EMULATOR_FROZEN=1 + $(SCONS) CFLAGS="$(CFLAGS)" $(UNIX_BUILD_DIR)/trezor-emu-core $(UNIX_PORT_OPTS) TREZOR_MODEL="$(TREZOR_MODEL)" PYOPT="$(PYOPT)" BITCOIN_ONLY="$(BITCOIN_ONLY)" TREZOR_EMULATOR_ASAN="$(ADDRESS_SANITIZER)" TREZOR_MEMPERF="$(TREZOR_MEMPERF)" TREZOR_EMULATOR_FROZEN=1 build_unix_debug: templates ## build unix port - $(SCONS) --max-drift=1 CFLAGS="$(CFLAGS)" $(UNIX_BUILD_DIR)/trezor-emu-core $(UNIX_PORT_OPTS) TREZOR_MODEL="$(TREZOR_MODEL)" BITCOIN_ONLY="$(BITCOIN_ONLY)" TREZOR_EMULATOR_ASAN=1 UI2="$(UI2)" TREZOR_EMULATOR_DEBUGGABLE=1 + $(SCONS) --max-drift=1 CFLAGS="$(CFLAGS)" $(UNIX_BUILD_DIR)/trezor-emu-core $(UNIX_PORT_OPTS) TREZOR_MODEL="$(TREZOR_MODEL)" BITCOIN_ONLY="$(BITCOIN_ONLY)" TREZOR_EMULATOR_ASAN=1 TREZOR_EMULATOR_DEBUGGABLE=1 build_cross: ## build mpy-cross port $(MAKE) -C vendor/micropython/mpy-cross $(CROSS_PORT_OPTS) diff --git a/core/SConscript.firmware b/core/SConscript.firmware index 65e90232e..01a174a13 100644 --- a/core/SConscript.firmware +++ b/core/SConscript.firmware @@ -7,7 +7,6 @@ import tools BITCOIN_ONLY = ARGUMENTS.get('BITCOIN_ONLY', '0') EVERYTHING = BITCOIN_ONLY != '1' TREZOR_MODEL = ARGUMENTS.get('TREZOR_MODEL', 'T') -UI2 = ARGUMENTS.get('UI2', '0') == '1' or TREZOR_MODEL in ('1', 'R') DMA2D = TREZOR_MODEL in ('T', ) FEATURE_FLAGS = { @@ -30,16 +29,10 @@ if TREZOR_MODEL in ('1', 'R'): FONT_BOLD='Font_PixelOperator_Bold_8' FONT_MONO='Font_PixelOperatorMono_Regular_8' if TREZOR_MODEL in ('T', ): - if UI2: - FONT_NORMAL='Font_TTHoves_Regular_18' - FONT_DEMIBOLD='Font_TTHoves_DemiBold_18' - FONT_BOLD='Font_TTHoves_Bold_16' - FONT_MONO='Font_RobotoMono_Regular_20' - else: - FONT_NORMAL='Font_Roboto_Regular_20' - FONT_DEMIBOLD=None - FONT_BOLD='Font_Roboto_Bold_20' - FONT_MONO='Font_RobotoMono_Regular_20' + FONT_NORMAL='Font_TTHoves_Regular_18' + FONT_DEMIBOLD='Font_TTHoves_DemiBold_18' + FONT_BOLD='Font_TTHoves_Bold_16' + FONT_MONO='Font_RobotoMono_Regular_20' # modtrezorconfig CPPPATH_MOD += [ @@ -191,11 +184,10 @@ SOURCE_MOD += [ 'vendor/micropython/lib/uzlib/crc32.c', 'vendor/micropython/lib/uzlib/tinflate.c', ] -if UI2: - CPPDEFINES_MOD += [ - 'TREZOR_UI2', - 'USE_RUST_LOADER' - ] +CPPDEFINES_MOD += [ + 'TREZOR_UI2', + 'USE_RUST_LOADER' +] # modtrezorutils SOURCE_MOD += [ @@ -205,11 +197,8 @@ SOURCE_MOD += [ # rust mods SOURCE_MOD += [ 'embed/extmod/rustmods/modtrezorproto.c', + 'embed/extmod/rustmods/modtrezorui2.c', ] -if UI2: - SOURCE_MOD += [ - 'embed/extmod/rustmods/modtrezorui2.c', - ] # modutime SOURCE_MOD += [ @@ -584,7 +573,6 @@ if FROZEN: SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/*.py')) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/components/*.py')) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/components/common/*.py')) - SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/constants/__init__.py')) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/__init__.py')) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/common.py')) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/reset.py')) @@ -592,28 +580,16 @@ if FROZEN: if EVERYTHING: SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/altcoin.py')) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/webauthn.py')) - if TREZOR_MODEL in ('T',) and UI2: - SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/constants/tt.py')) + if TREZOR_MODEL in ('T',): SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt_v2/__init__.py')) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt_v2/reset.py')) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt_v2/recovery.py')) if EVERYTHING: SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt_v2/altcoin.py')) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt_v2/webauthn.py')) - elif TREZOR_MODEL in ('T',): - SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/components/tt/*.py')) - SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/constants/tt.py')) - SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt/__init__.py')) - SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt/reset.py')) - SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt/recovery.py')) - if EVERYTHING: - SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt/altcoin.py')) - SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt/webauthn.py')) elif TREZOR_MODEL in ('1',): - SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/constants/t1.py')) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/t1.py')) elif TREZOR_MODEL in ('R',): - SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/constants/tr.py')) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tr/__init__.py')) else: raise ValueError('Unknown Trezor model') @@ -742,10 +718,9 @@ def cargo_build(): features = ['micropython', 'protobuf', f'model_t{TREZOR_MODEL.lower()}'] if BITCOIN_ONLY == '1': features.append('bitcoin_only') - if UI2: - features.append('ui') - if PYOPT == '0': - features.append('ui_debug') + features.append('ui') + if PYOPT == '0': + features.append('ui_debug') if DMA2D: features.append('dma2d') diff --git a/core/SConscript.unix b/core/SConscript.unix index 05e4412d3..d90ec3163 100644 --- a/core/SConscript.unix +++ b/core/SConscript.unix @@ -7,7 +7,6 @@ import tools BITCOIN_ONLY = ARGUMENTS.get('BITCOIN_ONLY', '0') EVERYTHING = BITCOIN_ONLY != '1' TREZOR_MODEL = ARGUMENTS.get('TREZOR_MODEL', 'T') -UI2 = ARGUMENTS.get('UI2', '0') == '1' or TREZOR_MODEL in ('1', 'R') DMA2D = TREZOR_MODEL in ('T', ) FEATURE_FLAGS = { @@ -31,16 +30,10 @@ if TREZOR_MODEL in ('1', 'R'): FONT_BOLD='Font_PixelOperator_Bold_8' FONT_MONO='Font_PixelOperatorMono_Regular_8' if TREZOR_MODEL in ('T', ): - if UI2: - FONT_NORMAL='Font_TTHoves_Regular_18' - FONT_DEMIBOLD='Font_TTHoves_DemiBold_18' - FONT_BOLD='Font_TTHoves_Bold_16' - FONT_MONO='Font_RobotoMono_Regular_20' - else: - FONT_NORMAL='Font_Roboto_Regular_20' - FONT_DEMIBOLD=None - FONT_BOLD='Font_Roboto_Bold_20' - FONT_MONO='Font_RobotoMono_Regular_20' + FONT_NORMAL='Font_TTHoves_Regular_18' + FONT_DEMIBOLD='Font_TTHoves_DemiBold_18' + FONT_BOLD='Font_TTHoves_Bold_16' + FONT_MONO='Font_RobotoMono_Regular_20' # modtrezorconfig CPPPATH_MOD += [ @@ -187,11 +180,10 @@ SOURCE_MOD += [ 'vendor/micropython/lib/uzlib/crc32.c', 'vendor/micropython/lib/uzlib/tinflate.c', ] -if UI2: - CPPDEFINES_MOD += [ - 'TREZOR_UI2', - 'USE_RUST_LOADER' - ] +CPPDEFINES_MOD += [ + 'TREZOR_UI2', + 'USE_RUST_LOADER' +] if FROZEN: CPPDEFINES_MOD += ['TREZOR_EMULATOR_FROZEN'] if RASPI: @@ -205,11 +197,8 @@ SOURCE_MOD += [ # rust mods SOURCE_MOD += [ 'embed/extmod/rustmods/modtrezorproto.c', + 'embed/extmod/rustmods/modtrezorui2.c', ] -if UI2: - SOURCE_MOD += [ - 'embed/extmod/rustmods/modtrezorui2.c', - ] # modutime SOURCE_MOD += [ @@ -538,7 +527,6 @@ if FROZEN: SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/*.py')) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/components/*.py')) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/components/common/*.py')) - SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/constants/__init__.py')) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/__init__.py')) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/common.py')) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/reset.py')) @@ -546,28 +534,16 @@ if FROZEN: if EVERYTHING: SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/altcoin.py')) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/webauthn.py')) - if TREZOR_MODEL in ('T',) and UI2: - SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/constants/tt.py')) + if TREZOR_MODEL in ('T',): SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt_v2/__init__.py')) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt_v2/reset.py')) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt_v2/recovery.py')) if EVERYTHING: SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt_v2/altcoin.py')) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt_v2/webauthn.py')) - elif TREZOR_MODEL in ('T',): - SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/components/tt/*.py')) - SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/constants/tt.py')) - SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt/__init__.py')) - SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt/reset.py')) - SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt/recovery.py')) - if EVERYTHING: - SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt/altcoin.py')) - SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tt/webauthn.py')) elif TREZOR_MODEL in ('1',): - SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/constants/t1.py')) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/t1.py')) elif TREZOR_MODEL in ('R',): - SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/constants/tr.py')) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/ui/layouts/tr/__init__.py')) else: raise ValueError('Unknown Trezor model') @@ -696,10 +672,9 @@ def cargo_build(): features = ['micropython', 'protobuf', f'model_t{TREZOR_MODEL.lower()}'] if BITCOIN_ONLY == '1': features.append('bitcoin_only') - if UI2: - features.append('ui') - if PYOPT == '0': - features.append('debug') + features.append('ui') + if PYOPT == '0': + features.append('debug') if DMA2D: features.append('dma2d') diff --git a/core/src/all_modules.py b/core/src/all_modules.py index 2434e26ba..96c4cf991 100644 --- a/core/src/all_modules.py +++ b/core/src/all_modules.py @@ -151,48 +151,6 @@ trezor.ui.components.common import trezor.ui.components.common trezor.ui.components.common.confirm import trezor.ui.components.common.confirm -trezor.ui.components.common.text -import trezor.ui.components.common.text -trezor.ui.components.tt.button -import trezor.ui.components.tt.button -trezor.ui.components.tt.checklist -import trezor.ui.components.tt.checklist -trezor.ui.components.tt.confirm -import trezor.ui.components.tt.confirm -trezor.ui.components.tt.info -import trezor.ui.components.tt.info -trezor.ui.components.tt.keyboard_bip39 -import trezor.ui.components.tt.keyboard_bip39 -trezor.ui.components.tt.keyboard_slip39 -import trezor.ui.components.tt.keyboard_slip39 -trezor.ui.components.tt.num_input -import trezor.ui.components.tt.num_input -trezor.ui.components.tt.passphrase -import trezor.ui.components.tt.passphrase -trezor.ui.components.tt.pin -import trezor.ui.components.tt.pin -trezor.ui.components.tt.recovery -import trezor.ui.components.tt.recovery -trezor.ui.components.tt.reset -import trezor.ui.components.tt.reset -trezor.ui.components.tt.scroll -import trezor.ui.components.tt.scroll -trezor.ui.components.tt.swipe -import trezor.ui.components.tt.swipe -trezor.ui.components.tt.text -import trezor.ui.components.tt.text -trezor.ui.components.tt.word_select -import trezor.ui.components.tt.word_select -trezor.ui.constants -import trezor.ui.constants -trezor.ui.constants.t1 -import trezor.ui.constants.t1 -trezor.ui.constants.tr -import trezor.ui.constants.tr -trezor.ui.constants.tt -import trezor.ui.constants.tt -trezor.ui.container -import trezor.ui.container trezor.ui.layouts import trezor.ui.layouts trezor.ui.layouts.altcoin @@ -207,14 +165,6 @@ trezor.ui.layouts.t1 import trezor.ui.layouts.t1 trezor.ui.layouts.tr import trezor.ui.layouts.tr -trezor.ui.layouts.tt -import trezor.ui.layouts.tt -trezor.ui.layouts.tt.altcoin -import trezor.ui.layouts.tt.altcoin -trezor.ui.layouts.tt.recovery -import trezor.ui.layouts.tt.recovery -trezor.ui.layouts.tt.reset -import trezor.ui.layouts.tt.reset trezor.ui.layouts.tt_v2 import trezor.ui.layouts.tt_v2 trezor.ui.layouts.tt_v2.altcoin @@ -227,8 +177,6 @@ trezor.ui.loader import trezor.ui.loader trezor.ui.popup import trezor.ui.popup -trezor.ui.qr -import trezor.ui.qr trezor.ui.style import trezor.ui.style trezor.utils @@ -463,10 +411,6 @@ if not utils.BITCOIN_ONLY: import trezor.enums.TezosContractType trezor.ui.components.common.webauthn import trezor.ui.components.common.webauthn - trezor.ui.components.tt.webauthn - import trezor.ui.components.tt.webauthn - trezor.ui.layouts.tt.webauthn - import trezor.ui.layouts.tt.webauthn trezor.ui.layouts.tt_v2.webauthn import trezor.ui.layouts.tt_v2.webauthn trezor.ui.layouts.webauthn diff --git a/core/src/trezor/res/resources.py b/core/src/trezor/res/resources.py index 15e53f9ca..1483dc59b 100644 --- a/core/src/trezor/res/resources.py +++ b/core/src/trezor/res/resources.py @@ -4,113 +4,15 @@ # fmt: off def load_resource(name: str) -> bytes: - if name == "trezor/res/cancel.toif": - return b'TOIG\x10\x00\x10\x00f\x00\x00\x00\x1d\x8c\xc1\r@@\x10E\xbfP\x80\x02\xd0\x81\xb5\x1d\xd8\x02D\x07\xa2V\x89\x02\x908o\x88\xbb\x84\xb8\xc9~3\xe64\xff\xcd\x7f\x936\x90\x19\xa3\xe1L\x00\x1b\xb2\x975\xb0\xd3?db\x03{G\x16\x9b\xec\xb8y\x06\xbd\t\xd0\x08\xcc\xa4Qw"s\xa8+' if name == "trezor/res/check.toif": return b'TOIG@\x00@\x00\xa8\x00\x00\x00\xed\xd21\x12\x820\x14\x04\xd0\x0f:\x06\xba\xdc\x84#x\x04<\x82\xad\x9d7\x80\x1b\xe81\xed\xac\x82\xb1Zc\x10\x1c\xc7\xfce\xc6\xd6l\x99\x9d\xf7C\x12\n\xa1\xb1\x92\xf3\x879\xf0\xba\xc7\x9a\xf6\x1e-\xab\xcf\x00\x1d\xe0C\xdfr\x0el\xd4\xfe\x1e\xfb\x9a\xf3\xd3\x02\xaf~\xe5^\xe1v\x81_Vt\xf7=\x1a\xca\x07\xa0\xa4\x1c\xd8N\xbc\xfb\xe6\xb7\xe7z\xa9\xf2>\xae7\xea\xee\xc7X\\Gn$\xf9yS\xba\xd4\xad\xed\xde\xbdI^\xeb@\xf9x@\xc2E\x1c\xe5\xf3\x00\xa3>\xac\xa3\xfcu\x04C~kGy\x1c\xc0x\x18@yx\x85Zrr>R\xf0\xda>\x00' if name == "trezor/res/click.toif": return b"TOIG\x10\x00\x15\x00\x81\x00\x00\x00-\xcb1\n\xc2@\x14\x84\xe1_Q\xb4\x10\xdc\x1b\x98#x\x83\x15\xecMa+\xe4\x00\x82\xae'07\xc8;\x90\x85\x07\x11\xd2\xdb\x04\x15L\x11\xe5\xf96\xeb4\x1f\xcc0\xb8N7X\xda\xc3\xfa3\x83P\x80\xab\xe19\xb6\xee1\xe5\x1e\xb70\xe7\x8c\xdb\x91yr\xca\x06\n\x8eH\x83\xf3\xdc\xa2\xb6\xbfFf6D\xbc\xd4\xdb\xf8\xe9J\xd5\xc9\x00\x96\xe6wa\x85\xa8j\xb5O\xaa\x86\x9f\xb3\xc8\xc1-j\xcex\xa43\x1c\x19\xfb\xde\xa8<\xf2Y"7\x13\xc7\xd3\xf5e\xea^3\x7ft\xc3\xb2\x003f\x9f\x87\x8d\xe2\x17\xb3x\x86\xb5?\x9bcP\xd1)\xe6\xa7\xde`\xc7\xde\x15>\xe8\x05\xa9\xce3\x83+\xe4\xffr~\x17\xe5\x04I\xfd\x9ex@\x9d\xd9\xc4\x04y~\x8f8`\xcc\xd87\x89}\x92\xb5\x9f\x10\xfb8]\xbdE\xec\xa3\xac\xfd\xb8^\xc7\'\xdc\xd8\xf7\xff\x13;\x1c\xa9\x91"\xf6@\x84a\x96\xd8\xfd\xfb\xc7\xce5\xadF\x89,DAc\xb0\xd4\xa7\x7f\xc2!X\xf6\xec\x7f\xc2.jR*\xbc}\xa9\xe3\x94\x84\x96\xd7\xbc\xdd\x83\xa4\xacx\xb6g{\xb6g\xff-\xf6\xe0\xa1\xec\xab\xad2\xb9\xe3=oc5\xda$r\xdb\x87\xb2\x06o\x03\xe4F\x8f\xf1\xf2\xf9\x89\xbd\x86A\xdb\x00j\xdcg\x91og\xe9{\xd6\x86\xe2d\x80vc\x99\n\xd8\xd8\x80\x17\x87\x1b\xed\xd3\xeb\x06\xd8\xda\x00\xe5\x07\xa1\x03W\xc9n[\xefx\x1bpa\xa0&\xcf~3\xb8\xab\xe5]\xe0)\xab~%\xbf-\xb8\x98O\xad\x1b\x82r\xb1"(\xc2R?\xba\xfc\xb2\n\xae\xd0\xefDka%7\xdd\xc8s}\xcd\x07\xaf\xefM;\xb6\xd7GO\x9b\x1f\x1b\x1c\xfa\xe8\xd08\xca\x84\x1c|V\x91\xbb\xc5\x91\x16\xeb\xfa(y\xc98x1 \xda\xcd\xa7%\xc1g\xe2\xc2E\xc9\xda\x0b"\xf9\x13\x14\x92\x92b\xe3\xc6oG|\xc8\x86\xd8\x02\xf5\xda\xf2T\x0890\x96\'\xed\xd5a\xe4\x8c\x92\xd9o\xbf1\x1eBn\x08&\xb3\x06\xa8}>\xe4\x92p\x9a\n\x9a\xe67' - if name == "trezor/res/left.toif": - return b'TOIG\x10\x00\x10\x00=\x00\x00\x00c``h\xe0b\x00\x83o\xfb\x19AT\xc2\xff\xff`\xfa\xdb\x7f9\x08\xf7\x0bd\xb6q\'\xc9\x92,\x90\x1d\xd466\x87}i\x9d56\xa2\xe5\xf8|\xfe\xcdf\xe4x`\x8e\x92}\x06\xd1\xac\x05\xaeh\xc1,\xced\x7f?G\x8f\xd0\x1cZ\xdf\x0c\x9a\xfc\xa5|/~\xf8=\xe4\xd1\xfb%[%\xd6\xed!\x19\xe2\xf49\xda\xf5\x8dd\xd3\xa2\x0c3\xcc\xbcWXo2\x8f\n\xaf\x90L\x13\x8fZ\xe3\x8c\x8dA?\xbc5-\xe7O(\xdf(\x9f\x84\xb9\x81m\xad3\xe5\xa2+\x0fcX\xd9\xfc\x86\xd1\xc9\x92\x11\x13\xe0\x0c\x81PK\xb3\x17ZhR\x8b#\xa8\xd9\x144-[u\xce\xe3\x186\x99\x92\xee\xac\xb4\xa8n\xa9_8\x86\x0ei\x8fl\xfa\xa6\xb0\xc1\x8cF\xbd\x89f\xaf\x99\x04\x92-\x9a\x02\xf4\x98\x0e\xb4\x92" \x17\x92Q\xb0A\xd2\x8f\xc6\x9d\xde\x8898\xc7\x82\xa6Z\xc2\x1e%\xcc\xe9sz?O\xc4/\xa2\xb5\xd7\xe2\xd6s\xf7F\x98@\xce\t2j\x92\'\xf6\xee\xd9\x1b&"\xec2\xe5\x91x^\x05M\xcf\xa9\xcc~\xeb<"\xeb\x1ag\xbd\xa2W \x191FX0-\xa6\xd8\xa2\xd9\xb0\xc9kz\xd9\xf3\xf4(\x1cY3F\x14\xd0B+\x98\x80\xfa\x03\x86p[X(%\xc9\x1d\xeaDz\xd65\x1aO\xb2M\xa7xG\xc8\xfb\x0e\x84p\x91\xfas5G\xdd\xe2\x11a\xf8|\xe8\xd0\x9d&\x05\xe4\xd3\n \x9b^\x11h\xf6\x12\xbd\t\xe6\x8ee \x1c\x91\x84\xb2\x01\x9e\xbc\xba\x05&e\xc2d\xf4r\x95\xd0\xfa^\xf1r\x15\xb6\x1aw\x90e\xeb\x0e\xbb\xc1\xbb6\xce\xc0\xa4;\xce\x1f\xdf\xa1\x96\x8c\x02\xe5f\xb8gW|\xfe\xf5\x9bC\xf9f\xcc\x88Z\x13\xc8\x92I\x15\xc2\x08Zl\x05\xbd\xc2\x8eV\xf3\xbc|\x8fT\x92\xa27\t\x99\xcf\x1cY%\x99\xf8\x06\\\x89\x91\xf8C\x99guW\x90K/c\x16\x89\xd4\xc0iO\x991!\xe8}\x8dt\xe0\xe4\xa3p\xa4\xf9\xd9Z\xc3\xe6\xe3\xf5\x81\x06\xb2IFP\x8e\x12\xa8{Em=\xed\x98L\xd0F\xfa\x12\xd7\x16d\\\xf3\xef+\xbb\x1fZ\x99\xb5\x98\x80g\x94\x8b\xa0\xe9\x1b\xd5\xf8<\x86\xc6\xf4\xaa\xa4\x08\x9a\x1b\x14hF\x1cjI\xc9\xdfel\x0c\xb4i\xbf\xa0\xf5r\xf5\xca\x18H\x8dte\x80\xdf\x92\x8b\t\xf1\xc26\xd8\x931\xab\x04\xf2\x87h\x8a/\xca\xa4\x89\x86\t74\xc7\xb5\x9f\xbd;#\xcad$d8x\x8c\x89d\xe2\x8e\xd0\t\xd42\xe5\x92%Eo\xaet\x93\xc9 \xc6\xb1^1h#\x15\x8f\xdb$T2MfA\xad\x8c{\t\x93\xdaC\x93\x1c\x13?\xeaV%\xaf\x89A\xcf\x90PR\xc0\r\x8e\xc4 e\xe4q\xc2\xf3\xc3\n\xeb\x8c8$+\x82\x96\xe4>\nZ\x87m,\xc3\x96\xa5\xa1\xd43G\xb67AZ\x81\x10A&D\x01V\xb4\xee\xa8\x9f\xc1\x1ePC{2\x9f\x85c\x05\x0c\xf7\xc4\x9b_\x88I\xc8\xe8\x99\xb9\x93h\x16\x99\xad3\xc9\x90]\xa1l\xc8\x98\x91\x8e%\x15\xb1h\xca\x18\x93l\xd9=Z\xeaJ\xaf\x92y&\xd3\xa4\xd1\xcc2k1\xa1\xa0)=0.\xc9\x82\xa6F\xcb\xb3&\x11\x9a\xbeK0\xb6\'\xfe\x9c=\x0bB\xbd9f\x93L\xc0\x0f\xc9\x8c\xdaB!\x10/\xc4 \xf6\xaa\x95\x10C\xc8[=\xc9\x90oR\xf4\x8apt\x81\xef\x08\xfe@;\xcb\xf7\xb0\xcf?\xcctM \x06a`+\xc9\x92\xacqGGy\x06\xd8\xed\xb19\xc1\x9e\xcf\x86\x98\xb2Q\x9f3+j?]:\x15X\xa0YaYl\xe4G\x19\xb2\r\xc9\xc8\x8e\xb05bSo2\x88\xe5{\xfd&s\xe6%\x85\x90\x9f\x90\xa3-\xfb]\x06\xe3\xb8\x0f\x99\xc9$\xb0\xdf\xb8\x03\x9c\'\x94\xf7\x9d\xc6\x99\xfa\x02\xb3{H\'`\x1eglU\xd8\xac\xf3z\xda\x0e\x11\x91\x11\xd8\xae|\x8a\xd0\xb57)\xb5\xac\x11\xf0&\x13 \x0by\xf7&\x98U\xe2\xbe\xe0u\xb8\xa7~_\x08\x19\x1c\xe3\x9b\x10\xeb^\x9e\x87-f\x02\xac21\x14\xea\x84\xbc\n\x9a=\xafG\x04\x81P\xd0\xd4hx\x84B\xf1(\xdc{\x9a\x84\x98\x0e\xe4X\t\xa7Q\xa8\x91y6\x10\x1fr\x00f\x82\x96\x185\x91o\nY\xa3\xde\x8c\xc4\x0c\x9f\xf1\x04\xfa0\xd2\xabz\x93\x04\x02\xbfOxUF|\xf0\xdbf\xc0&1\x0b\x18\x0b\x98:\x99\xc5;+\x19\xa2\xd1J\x80)\x12A\xab\xc8\xbe\xbd\xb0\xd9r&=\xf1\xd3\x9b\x93/\x99\x10\x99\xf4\xe6\xc9\x9c\xb3"\x9f\\v>3<\xbf?Vx4G\xde\xea\xcc\xdb;cwzD\xb47?l{;\x16\x8e\x07\xd0j\x02\x03$\x92\x11\x91\x91\x90\xe0\x9e\x90\xe12\xc9\xd4\x18\xe6\x826q\xbf\xf1\x94\x08\x85\x05RH\xe6\xd6y\r\xf1\xe20v\xf9\xf6&\x8e\x8e\xc7\xa0\xa8q\xd3\xd5\xc5\x16\xae\xae\x04N\xe8\x16Y4\x12/Z\xe2PKv!\xb7\x024\xa8I\xcc\xd4-\x8f\x10\x073\xc1\x96\xf2R\xbf\x90O\xeb\x89\x08\x85\xc8e\x92\xc6\x94O\xd1\xab\xe4\xf4sK(\xf3\x98\xc1\xcad\x93QV\xb3\xc0\x92H*XIy\xea\x9ea\x96\xcc\xd9\x9dp\xe2\xfc\xec\xf1\xba\x0c\xcb\xa0\xc5\x9cdW&\xa5\xc4\x90\x1dg\x9d9\x7f\x94\x06\xbf\x8d;\xc8\x07]bNH\x07\xe9=\x11\x9d\t\x9f\xb7GD\xa8$\x0e\x92E!\x84\xf4\xee\xd5\xb2\xcc\xbe\x94\x0b\xf1D&s;_\x08\x1f\x8a\x7f\\M\xbf\xf8\xf4Og7\xef\xbex\xff\xf4\xfe\xe9\xdd\x17\xcfn~\xfa\xa7\xe9\x07\xff\xb8rw\xd0\x9b\x04\xad\xc31x\xc2t24\x06\x18P/\x10B\x81\x170\x8f\x9cZ\x8e\x12\xb2Pj\xd0\xde\xe3RZ\x8fKG\xbcH\x90\x8c\xde\xad\x8e\xd6YH\xaeI\xe5\xf9\x92%u\xbf2\xf9\x1a\xc6\x82\x8c|*\x8e\xae\x7f|\xfb\xde\xdb\xff{w{\xdc{\xfb\xe3\xdbG\x07\xc4\xa7\n[\x83vR\x04b%\xe6\x89\x0c4\x12>\x03\xd9\xc3\x16\x11\x9bV\xd8Tn\xc8G<:cs \xf6\x90\x10\xc5\xe3\xb93I \xa0\x9cs\x8fG\x84\x0c\xb6F\xcaF\xce\xca\xb5\x8ef\x83\xb68\xa61\x01~\xcawtt\xb0\xfa\xaf]d\xfcX\xdd<\xda\x0f\x80h V&bm\xa2~:\xa7FL\x00\xb9\xa8A\xa1P\xa1q\xa4\xb7\xe7\xf3\x11:h\xd2c\xf1\x818\x01:\'\x1b| S0\xaa\xa0\xdc\xe1\\p\xbb\x995\xc9\x19\xc5V\x15\xd6;&K\xfd\xc3\xf1\xd9\xcd\x87a\xa3qv\xf3\x1fJ{\x17\xd5E>I\x91\x8c\xc8\x1a\xa0\x885\xfa\x86>\x05\xc2\x0b\xcckZ\x97\xf6\xac=\xcd$C64\xf6Xt\x92\x93\xba=46O\xb85\x92\xec\xe9\xb56Y\xa7\xbc(\xc461\xef\xf5\xeb\xf7N\x1f\x85\x0e\xc6\xfd\xd3\xd7o\xf0\xae\x83x\xe3\x1eZ\x01\xe4\x0f\x04Z\xc4\x85\x995\x85\\\xd3\xe7\x14\x08\x1f"\xc2\xe4\x9a\xc4\n\xfa\xe1k\x8fe\x8f\x90H\xdb\x83\x9c2\x84\xfc,\x1f\xce\xd8\xf1\x80\xb7W\xd91/\x02\xcc\xaa\x1dr9\x99~\xf9\xa8l4\xde\xfb@8\xe8\xceji\x0b\xd9\x80;\xc0\x89\xcc*\xbc9\xa6D\x8c\xd4\x1f\xe1\xe4F\xc9\xa8G\xa5\x93,L\x1bX@\x84\xf8\x9e\xe0\x89<65~\xc0\x8d,J\xe3\x1e\xaeP\xabFL+\x8d\xd3\x98<,\x92\xaa\xfc\xfe\xe3\xd2\xc1\x98~i\x8d$\xear3\x85%Q$\xdcVF#\xb5)\x19\xd1L\x8b)\x85\xad\x04f\xd8! NN\x85\xcdG\xa2\x03D,\x9bI\xf8#\x19\xb5LhT\xd9L\x88\x0f\xba5I*8\xe3\\\x1cbN\x93\x1e\xee\x81\xa4&\xef\x98\'\xa1\x83\xf1oFv-\xc4\xa0\x91w\x17J\xac%\xe3#r\xbeE!\x1e\x01\xa5&\xd6\x87q\xe3\x0e\xb5\xd4*\x8a\x87\xe637m\xe4\x03\xd6\x00!\xa4\x819\xf7\xe6i\x9b\xfe\xe9\x05\x9aA|`V\xa80E]W\xd0(\xb5_\xbf\xf1\xa4t0^\xbfaoF\xf9\xd4\x02\xfe\xdb\x12\xaf\xa0\x0c\x98\x8ez\x93\xf5\xc0\x96$\x132\xad\xc0c\x13>\x14\x1f\xe6\x01q\x02\xc1\xe7\x006\x89\xf8\xd2\x9b\x0f\x8e\xd3\x8ee\xd3f\x93\xacK\x13w\xf2d~\x98+\x17\x8e\xde\xfc\xde[O\x83\xcf\xbd\xb7\xe8&\xd4\x99"\x99\x88\'#\xb2c"\xd4Z^\x00\x89\xa4\x08j\xa6h\x0f\xe7d|\x0f\xe2\x81\x84\xd2\xf6 \xe7\xdc!\xc3\x99\xcb\\)\xd8\xb2\xa0\x1a])\xac\'Z#O\xf4iD\xbd\x82bR~\xff\x81\x99\x9f\xfe\xf7_\xef\xbet\xef\xf4A\xeb>|\x81\xffAt \x82,\xa9\x88\x17\xd9\x94 \xad\xec\xe599\xd4bk\x80B&4\x1c\x9f\x16\xfbC3\xc4&\xc9\x82\xbd\x07\xd8\xc3\xd6\x10\x8bdr\x98[7N\xb0_sd\x1e\x9e$\xf3Wr8\x99e\xa3\xec\xd07\x1c\xa5\x1a\xe6\xc0\xa4\xc9ayY\xc6g\xff\xf5Nw\x90\x83\x8f\x8b7\xff\xe6p=(\xff\xed\x8d\xd5G\x97\xad\xc7\x1d\x85;\xc7\xc8ER\xf4\xaa\xa0\x95\xb0G\x19f\xefJn%\x99Pcz\x8e\xdb\x1e\xf2z \x9f\x13\xf2\xa7\x03\x99.\x929\x9c\x19YL6\x02\xb9\xcd\xc9e\xca>\x99\xa3\xf3\xccg\x90S=g\x07\xa5\x9c\x7f|{W\xaew_<\xba\x81LM\xfbp\x96vL\xc7\xe4\xa6\x9d\xb6\xd3\xfc\xe8\xc6\xdd\x97v}\xf3\xf1mq\xb5\xb0\xb7\xe3\x11B\x9fzs6*\xc8\x80U\x81\xcc\x98\x17\xd8\x864\x94\x9c\xba\xf4\x9a\xf3\xea\x12:\x98\xf3\x89\xf5\x83\xf2\xb5\x06\xf9d&\xa6m\xda\xda\xa6\xf3\xcd\xc3\xf9\x06\xc5\xb9\x8e!\x9ddq\xeft{\xa6\xbf\xed\xa510\xcc\xc1\x9e\x1c\xd7\xa5\xc7)\xb4h\x7f\xb8\x9b\xcfz\xdb\xbf\xbaw*\xde\x16\x96\x91\xa3\x04\\L\xfc\xca\x0cHd\xa1\x8b\x91\xf8\xd4\xa4\xb7\xc5\xeeh \xab\xe0\x81\x84\xf0\x0e\x91\x87\xbbu\xb0a,|4{<\x17\xf3\x81y*\x1d\x0f\xae+\x18\xad\x98\x1e\xaf\n\'G7\xb6\xe7yv\x13v\xa7\xb5\x83|0C>\xa6\x03\x0e\xb5\xe1\x96\x84\xed\xd9\xcd\xed_\xbe~Cn\xc62J\xd8\xe7"\xa1\xb1`DL\xc8\x18r%\xc3\x99\xa0\xc5\xee\x00\x85\x96O\xc4\x99\xb4\x9bOHd\x88\x8b\xf2\x98\x0b\x91\xb95\xabR\x16x\xdb\x9e3s5J\xbe\xab8\xbbA\xce\x0c?\xbe\xbd-\xc7\xfb\xa7\x831~\x9d\x10i\xb0\xa6\x13\x92k`Q\x9e\xc8\xde\x83\xf6\xfd\xad\xe6}|\xdb\xddWP\xf4\xe6r6$UQ9Rgz\x95gQ\xc6,\xb0\x0e\x85I\xd8\x0c6"\xcc\xb6\xf3\x91?N6\x8c\xf0\xfb\xcaN\xcdp!\x14\xd3\xb6\xe7\x19\x8d\x0c \xd7\xff\xf9\xeb\xb6\x1c\xdf\xfb\x12VU\xc8\x1av[ \xa1pa\xf7\x9b\x18\xb9\xa9\xf7\xbe\xdc\xf6\xed\xff\xfcU\xfc\xa9\x9c\xd5\xb0\x1e\x89(\x9dL\tQ\xa8E\xf0"\x01m%E\xc2\xcc2\xa6v)\x9f\x1a\x0f\x13\'~_\x89T\x1e\xab\xc5EB\xe98\x8a\xdd\xaaD\xe6\xb6;pX\xc2\xae\xbc\xfa$Y\x98\x8eiG\xed\xc1lp\xcc\xe701\xce\x99\xf6+\xab\xed\xee!\x159c\x01e\xc1\xf6\x04\xcc\'#F\x85\x94lN\x16\xb4\xd4\xa0@Xa$D\x86\xf8X\x83\xb6\xd0!\xfe\x89c\xc4\xf7?\xa1\xfb\x1dc\xa6\xe9LID\x9b\xee@\x16H\x0f\xc7\xcd\x988\xfa\x14\xc1\xa9m\x19\xde}\x91\xec\xa9\xf8Oh\x0fx\xd49\\#\x1f\xa0\x85;V`#\xfc\xff\xee\x8b\xdb\xbeGztb\xeb\x0f\xf1@Z\x95\x10r\xbcF@c4h\x03\x95\xd7 \x90\x15\x19\xe5s\n\xd9\xab\x1d\x06Y.\xce\x86\xc9aNNTpR\x18?\\\xab\rii\xf2:\'\xbcy\xec[\xc7*\xfe\x92\xad\xdbn\xc0\xd9G\xe2\\Ey.p\xed\xe0\x16Y\xb9@\xa6iN\x7f\x86\xd9\xdf\xde\xdc\xf6\xfd++\xba\xb5\x98o\x96O\x1f\x8czUo\xced\x88\xd7\xc8\xab\x0b5)d>\xc2&hj\xcb\xf9\x136\xb7\xf9S\xe33\xf1F*\xce\xc1\xa7\x81\xf7\x1dY*L\n\xef\\\xdb\x9a7\xf29\xba\xb1-\xbf\xdf\xf4\xe0\x8b\x98\xbe\x8b\xf9\x8b\xc1,\xcd\xdd?d\x1cN\xf0\xe9\x9f\xb6}\xff\xc3\x83p\x92\xc6`\xd8\xc4\xc4b\x0f\xbc\xe4\x85\xb4]\x04\x1e\x9b\x84\xddBS\x84\x13\x8e\x04-\xac\x99\r\x96\xdb\xe8\xd0\x7f\xd0\x8fq\x84\xb6\x8fq,m\x0frg\x94\xa57O\xf3\xc35\xd0\xe8\x903\x8b\x88^\x0c\x93\x8bCs\xb6\x0e\xa9\x1e\xfdj[~\x9f\xfe\x19-q;\xf2Z\xe4n\xac\x87|\xa2O\xff\xbc\x8b\x0f\x9a\x93\xf8w\x89,F\x9e?.F\xe4P\x86%P\x19\x05HC\xbdQ\x93\x80Y\x02\x16Q\xbfFH\xef;m3\x03:\xdf\x04G\xa4\xb4Fh\xee\x10y:\x03\x1a\x1d#\x0ea\ts\x0b\xfe\x1aFb\xf5\xe2\x877v\xf1\xb9\x18\x86oF\xfe\x87\x7f\t\xe7\x9f\xf5v\xf11\xf1 G>\xf0\xa7\x8ay\xd0_\x0bG\xeb\x02\xa3,T.\xd6\xa8@)\x81;\x89\xf4\xb7\xf2\x01[\xa0\xac\x84\xc3\x04M\xc2V\x14\'\x17\x08\xb1-d\x8cW\x1b)e\x8d\xf0y\xfd\x0f\xbb\xf8\x10Q)\xd5K\xec!%\xa2\x83\xf5.>7\xdcI\x939\xb5\x8b\xa4\x92\xf3\xd7\x8c\x02"T&\xc0-\x10V\x01S\xc8B\xcf\x1f!Ct|>r\xc6\xca\x9aS\xb9\xd3\x12\x89x0\x0e\xab(6\xb4\xc603\xcbE\xe9\x80I\xb9\xb4;\xf5\xac/\xf1g\x11.`}\xee\xd6\x1a6\x91\xdc31\xfes0\xfb\xf4\xcf;\xfc!\x16F\xee\xf4\x955RJ\xf1KaC&\x15J\xa4\xe6\xd3/\xd5\x1a.\x13\xdb\xa6x\r\xc2#\x14\xb5\x93\xf9\xe1\x8c\xa9\xd0\x9dWf,\x0eLt\x14)q\x16\xc8\x07\xcf\x8d7\x9d\xce\x8c\xd2\xc1:\x8f<\x87\xd8#\xcc};\x9f\xdf\xf4<\xfbbg\x9bk\xe1\xec`\x16u.\xe1\xe3\xbc\xaeR\xc8\x02\t\x89Kj\x91\xf2\xa9\x92*(RK\x0cL\xfb%\xb47\xbc\xf1\xa9\x05\x99\xe3C\x96`6\xb1\xd2\xd0\xf3\xf1\xdf\xc9\x1d=\t\xd3b.\xb9\xc17\'\x1a\xb9\x8e\xfe\xfcK\x88\x0f\xca\x17\xde\xc3\xfa\xc3\x9f\x7f\xf9\xeb\x9fl\xcb\xef\xbf\xffJ\xebh-\xae\xa2\x1a\xe3\x0b\x88/\xdf\x83(_\xf8\xf9\x97\xbf\xf8\xdbO\xfaw_\xda\xf6\xfd\xaf\x7f"\xebmX\x07c{Vg\x8fDR\x1f\xc9\x82Q\xe8,*\x98VR\x845>\x86\xee<-\xd1\xeed\x9e\xe6\x17\\\x1f\x0fJ\xb4\x05\xb8\xa0\xf1\xb9\xdc7\x11I\xf3\xc3\xb5\xb2\xd2z[6_G\xb0G|\xd7pr1(\xa9\x06c\xb0E\x89L\xc4\xa0Q@\xb5\xf3\x87\xc6\xa5\xe7\xf3\t\x17\x94[\x07M`*\xd0\x17_(cd\xd26\xdcr\x14\xa0\x95\xe6\x9eG\xc7\xe8\x11\xb5;\xcf\x91\x8f\xbbY\xbd\xe5\xea\x95UX\x05\xc2\xc5\xf9\x84d\x88\xd0(te\x16\x8a9\x1a\xc8\x873Ls\xca/\xb6\xd9\xb7#/\x06\xb3\xc3\x95p\xb0c\x86\xa8b\rl;L\x88g\x9e/\x9f\xa8\xed8\xb1AdR\x85&)\xa5\xbaM\xe1\x88K\x8f\xcf\x08=\n\x0b\xf2Gv\x1c\x94\x98\x1f\xe6\xaf\xe6\x08\x0b\xf0\'\xcd\xd3\x99\xa3\xc5\xa6\xb8\xd2\xcc\xb8\x05s\xd4~~|\x8c=?d\x10S=!FUP\t%v\x07\xe2p\x9c\xc6\x01R\xb1^9oB)\x91\x8f\xb1\xbc)\xf7\x8e\x01[L[I\xa1Q\x83\x19\x9646\x16"\xca\'\x17>\x18\xc7\xd2\xcb\x9f\xaf?\xbe=\xc9\x9c\xcda\x8b\xa8\'\xe6\x04\x85\xe3b\xc7\x84\x0c\xb9C\x84.\xf2ao\xc4\x1c\xe2\x83,\xc2y\x9a\x0fJ\x1a\x1bG\xe4\x12p86\xb3t\xcd\x94\x94L\xd4\xc1\x99\xe7\xc9\xc7\xc4\xe2\r\xb2`:U@t$&@\x02\xfa\t\xcf\x00\x15h1\xa5,D\x9b\x90\x0f\xb1\n\x85\x0f\xed\xca\xb6,\x8c\xb01\x1d\xcc\x94\x82\xf8\xb09i\x0e\x94VjOz\x0b\x08Q\x1b\x98\x8d\x89Q\x99\xce\xbeA\xfe(\x91*\xa8\xc4\x9b\x89\x89\xc3\xca\xc4\tpI*\xe4\xe2|r\x91 \x1br\x89\xf9\x90%\x90;Q\x00\x02\xc0\x06Hq\xcf\xf9t\xf5\x80\x0c\x99\xa5b\x8a\x99\xb15BF8\xa1O\xd1s\xe5\x139\x7f\xe6\xc4\xc5\xb1B\x8b\xb8U0\x9fD\xe8\x04\x85\xd6\xda\n3\xe5c\x88\x0e\xee\xa1\x99\x8b7\x1d\xe4c\xacE\xc2\xa9\x04\x92\xb3\xe1\x15Z\x87\\\x84\x12\x8c\x0b\xa9\xe89\xfa\x83l\xe8\xacq\xcd"\xcf\xa6\x80]bV\xbb\xfdy\xe7\x8d7\xbb\x9b\xf13\x1e3\xf4\x9a\x9fQI\xa3F\xe3_\xff\xc0\xaf\xce\xbf\xf3\xfa\xa5|\xc4\xa0`\x8em\xd3F2\xc8\xacNH\xd8MB&"T &\xb6\x85A|\xc8\x8e6x\x93\x8b)DF\xc6\xf3t&\xe3J\xa7#\x96\x8c\x99\x92\x99\r\x0f\xcc\x1a\xf8`\xe4\\\xef:\xff`M\xa6\xe1\xd7\xc7\xeaZ4\x96\xfdi?\xa3\xfd\x18\xfbt\xae\xdc\xbb\x1f\x9aK\xcb\x9d|\x88\x0c\xac\x1aG\xd6\x19\xac\x99\x92\xcf\xc7\xc4u\x8b\x02\x9f\x11\xd9D\xf5(,\xc0\x1f\x979\xc68j\xabS\x98\xcd`\x9d\xcep\x842hG\xd6\x1e\xfaf\x16\x81?\xd7\xae\xb09D\xa9Lg\x83\xf5\xae\xf3\x0f\x0fR^\x99\xc3m\xcc\x84\xf28-\xcd\x98ys\xdfF^sW\x08=\x80\x8f[\xad\xc6\xcc\x89Y\xdb\xf2\x81~P\x85^\x0cJ\xa2\x85\x8c&\xe1$\x006I\x95\x00\xad\x04g\x91\x8f:\x82t\x88\x00\x9eBZ3\xd3qwh\x94\x0c\xe7VF\\\xcf"\xf1\xc6\x94\x97\xfb\xc3t(\x80\x0f\xfdu\xac\xc4-\xf3\xdc\xdd\x83%\x857F\xee\xe0\xec`v)\x9f1\xdd8\x99\x84\xf1\xbd\xd8\xc4l\x13ZE\xd6\xcc\xc5\x9d9s\xb1\xad\x82)ap/\x8d\xc3\x82\xf6\xecH\xfe\x1d\xc3\xa7\x12\x16r^\xe78\xb1\xc1\xbc\xc0\x93\x15\x92H\xd7\xe2\r\xb4\x84\xd1e|VBc\x86\xfe0\x13\xe8\x95\x10\xca\x83\xf9\x8c\x85\xbf\x9c\xc5\x9e\x80\xe6v\xf31zv\xf4\x0c\x98\xa4cCd\xa0\xddFK\xb1\xc5\xaf\xb8TI=\xe1:\x00\x7f\xc2s\xa0\x83\x84\xa4$o;\x96@\'\xca\x99M\xba\xa6\xfb\xe5\xf1\xb1\x9c\rg \xff\x08y\xcc\xd0\x9f\xe1\x01\xd0\xf1\xc8`\\\xbdr)\x9f2\xa2\x1d"^\xadwP\x12\xf1R)\xc9+t\xeaF=\x04\x9f1\xf3\xc1:\xa8\x90\xc7`5X\xa9=in\xbd\xd1(\x80\x8c\xb4\xc9\x9eBK\xe0\x83<\xda\xe4D\x87\xee\xc9Z\xc3g\x82\xf6\x98\xcd\xe1\x9b\xe7\xbc$\xbb\xb5\xbc\x12@\x8bL\xdau\xfe\xe1\x01\xd0)y-\xfb\x06=\xfe\x1f\x8c\xa7+`\x9e\x93Kc\xe2\xe8\xf3\x19\x0b\x1f\x88t\xb5k\x7f!\xd9F6H\x83K\xa4\xc2\xce\x04\xd4JK\x1eu\x94\xa8U(\x9d@\xe8p\x98\xb6\x1aL\xd6\xa83\x1d\x1e\x1b\xac\x0c\x1b/|\xd2\x95\x1a4\xbcb\xb9\xdc"J\xc2):\x1e\x1e\xec\xe6c\x90N\xa9_F\xd8\xc6@N%\xd2\xa7\xbf\xe5\xc8\x89m\xd1\xbf\xdb\x18_\xcag,L\x89O\x84O,\xd1fB\xcal\x8bA\x96\t\xd4\xe7\xd0\xfb%\xf7\xac%\x1dsl\xe4\x14\xc2H\x9d\x11\xa7d\x8e\xf2\x82\xc8\x07kv\x00\xc9\xa4\xeb\xfe\x15\xc9\x18h\xed:?\xd0foV\xce9\xf0\x06\xf6\x89J\xa1V\xca-1\xaf\x9c\xd8YV\xec\xd1n>\x86\xf9\x8c\xa1\x8e]\xd0(\xd4\xdfC2\xf3m|\x82\x82\xf9\xa4q\xc2\x06\x9d\x13%z\xf1<\x87k\xf4\xc5\xe4\xde=\xe5\x86\t\x94\xdc\x03[\xa4\x85\xf9\xa8+\xd6\x02\xa2\x14\xb1E\x10\xbb\xfdIW\xe0\x900%k\xf8\xdb\x15\xb5WL\x07\xff\xc1\xac\xf4\x8f\x98\x1f~\x83_\xf4\x0f\xc4\xa4\x15\xe6\x9f\xae\xc4*\xb2\x86\x8c\x9a\x19\xe5C$\xa5]\x0e\x1e\xc4\x07\xfd\x19\xa7\xd3A\td\x88\x17\xfa\x94\x96X\xa2?H\x01Kj\xcd\x036\xe8<@"\xd8>\xe7>\x1b\x15\x9e\x0b\x8d\x99c\x13y5\xb7\xc9\xf5\x99]\x87/\x9cwx\xe3\xea\xaf\x94\x0c0\x93\x12\xb2\xbe\x84\x8f\xf0`&\\\xc3N@c\x86\xb4\x88\x1d\x99\x03\x7f,#\xe6SJ\xf0\xdf\xa1~0\x1f?LI>\x91C\xec\x162\xe2:\xac\xd2\xb1\x89\x95\x0f\xda\x84\xfe`\x0b\xe9\xf0k<\x16\x96\xce\xcc3\xc7\x95\xa5\xc4\x8cb=\xbca\xd6\xc0gE\x99\xac\x80\xce,\x82<\xb1\xde\xc9\xe7z\x1f\t\xad"\xe13<\xe8\x1fDl\x93\xd8\xd3?\x18^\x89x\x7fb\x81\xffKWL\xe8*\x11\x8c\xa0\xff\x90|\x98\xcb\x91\xd4m;\x12\xb3C\xc4\x05\x98\xa4c\xb2\x06\x03\xfaD\x87\r\xc2(\x88\xc5,\x9d\xc1\xf7\x8eS)\xb7\xc8\xbe\xd3i\xbbpn\x0cc\xe3\xda\x1f\x88\t\x86\xe7\x10\xae\xd8\xc9\x07\x88\xa2_b\x0f1\x89V\xd6\'\xb7\xf7\x8a\xf6-\xe1V\xc8$\xbe\x17u\xe9\x12>%\xf92&\x16\x10\xe9l\xb0\x8abj\xd7\xe80![G\xc2\xa3\x12NEx.NA\xab\xd1@?\xe0\x8e\xdaJH\xcda\x17\xbc3\xaf\x89\x0f\xdc&9\x80\xd6\xe0H)\xebV\xc4\x88\xd6\xee\xe4s\x05\xbf\xec\x1ex|\xbc@\xb7\xe4\x06\x94VY\xe3\xc3\xadY\xba\xbe\xcc\x1f\xe2\x93\x0b\x1f$Q\xa2\x88\xd6J\tw\x04?\x95NiI-\x85\r\x1b\x05\xcc.\xe1\x83k\x84\x11\xd1\x99Z\x16\xb1A\x8b\x8e\xd0\x1a\xc3\x06m\xf0qt\x1c\xa1F\xc3\xe7\x93\xae\xd3ud\x8d&B\xc4&bf4N\'\x16\x83x\xae\x7f\xe5\xda\x7f0%qbu\t\x9f\xf7\x85\xca\n\t\xf5q=}\xc9\\\\I\xfb\xa3\xcbK\xb6\x99\xcc\xd9\xe7S\x99rxp\t\x9f\xb1\x9c\xddZb\x19\x1d1\x11r\xa9\x1c\xac\x9c7\x11=\xca\x85_n+\x1f\x93\xd3\x9e\\\x96p~\xe5\xb2"F\xab\xfe\x15<3\x07\xb5\xd6\xc8\x88)\x995QYK\xb6\x94\xf9n\x7f\xfad\x0f\xafB\xa2\xf8u_\xa8\xea\xb8Y\xa5\xcc\x9fy,\xb9\xc5\xb3\xf8\xcf\xe1\xf5K\xfd\x99z\x84\xa6\x96\x8c2"g\xcc8-}>\xcc\x03\xdbZ\x86\x95\x89\x9d?\xe2LN\x1e;gVb\xd0\x8c\xcf\xaf}Ki\x85\\\x0c\xb3\x82\xb7\x7f\xa09\xee\xe6C\x8e\xad\x98\x11\xf1]\x1b\xdeE\xfb\x14}\x9c_F\x18\xe0\x0b\x9e\xaa\xbb\xdf?\x80\x1e\xfeu\xf9@>S\xfc\x8a\xea\xa9\xf3H\xfdaB\xf5\x00\xa7\xd8"*\x03kP\xc3>\xe0\xce\x8c\x08\xad\xa8&Frc\xe8\xcf\r\xf4\x1e\xbcb2\xe4\x0ef\x8a\xe3\xccF\xf8\xad8\x87\xdd|\x90\x04\xd2Q\xd7,#\xbf^\x11\x17Gli\xc7qf9<\xb8\x94\x8f3hZ\xf3\xe7\xc8\x8c\x8d\x18D-\x8f\x8e\xeb\xa9I\xf0\xc6\r\xef\xa1\xfd\xf2TL\x16oV\xd6\x1a\x9b\x8b5G\xfc\x89\xa4\xe7\xb2\xc1\xecw\xf3a\xc7\xba\xfb\xc8G\xbe[\x03\xe7\x03G\xcc\x0f\xf6\x0c\xdd\x06\x9b\xf0\x84t\xbe\x07\xf2QB\xcc\xe7H\xf8p\xebH\x08M\xcd\xf8\xa2I\xc8\t\xe9 \x1f\xdf\x1e\xe1S\x0e\xd1\x87\x99\xf0\xa9\x111\x178p\xbf{0|\x9fs\x05\xbb\xc0\x1d\xc8\x1cr\xdfu\xfe\xab\xef\xf7\xafk\xd6\xfd\x83\xbepA/\xf1+\x88}\xcf\x14\xaf\x8e\x96`\xef>\xba\x13\x81K\x0f\xe1\xcf\xd4#4%>G\xc2\x89Y\xc5\x1b\xc4\xe2\x1a\xab\x88(\xd5\xfc\x01B9\xee\xd8?\xa0["g\x9c\'\x1c\xee\x86\x9d5\xc0\x07\x99\xf27x\xfeU\xffFw\x7f\xd7\xf9\xbfz\xfb\x17?\xee\xcb.XGK\xdd\x13\xd9\xc0{\xdd\xfd\x83Y\xe1\xaeT\xe3\xde%p\xda\x7f\xf7\x85\xaf\xde\xbe\x94\xcf\xd4\xf2\xd1\xd6\xd8#s\x04\xe6L\xadS\xc2\xc7\x8c\xd1\x1c\x8e\xefEX\xd6\xed\x11>@\x08\xce2C\xe7\xeb\xde\x08\x1ftc?Z\xf9\xe4\xfcy\x1b\xcb]\xe7\xc7\xb8\x7f\xfa\xe1\xed\xab7,\xe9\xa5\xb2\xe8\x1e\xc4\xfb\xfc\x07b\xb5\xef\xf1\x81@>\xc3\xeb\xe5\xed\xfb\xa7\x97\xedm\x99,\xb1\xa4\xef\x90\x18\xbb\xa2\xac\xa6\x96\xcc.\x7f\xd0\xa0\xe8"\x1f\xb9Sp\x87\x18\t\x9f\xee\x81\xb5\xa9\xec_w\xe6\xd0\x9d\x1ft\xc9\x02q\xc9\xb2\xbb,\x07\x8eOz?\xf8=\x12\xe8_\xef\x939\xb1\x10\x82\xfeu\xb3r| \x96\xd8\xba\xf6\xfe\'\xbd\x07\xef\xca\xfe\x18\xa1\xc4|L\x99.\xadI8\x8a\xfeL\x85\xcb\xd8\x06\xd3\xb2t.\xf2\x01B@\x05H\x94\x96\x07\x93X\x13\xa1Z`.\xca\x83\xea\xf2Q\xf9`|vsl\xfa\xd7\xd5\x16\xb6\xa7\xbfo\x96z7\xb2\xdf\xf2U\xf3\xd9\xcd\x87\xdb\x11\xbe-\x8dOCx\x19\xe2"\x16\x8d\x81\xdf\x91\x8d\xb1Pr\x0eE\xf86\xb6<\x9c%\xba\x03\xbb\xce\x94\x91YC\xb0\xe9|\xe2%\x8f#!\xbe_"\xea\xe5\xd4\xdd\x7f\xb8l0V\x7fy\xf3\x03!\x846^\x87\xba\xe3\xb3~\xf3\x83\xd5_\x1e~7\xe6\xb3AH\xa8\xa4K&D\xf6 \x8f#4\xc9\x8c\xadE1\xf1b{\xe2K\xf8P\xa03\\r\xee\x92?\xb2X\x1a\xb5f\x15\xef\x0b\xa1\x15f\xc8_\xa2\x07\x0f\x9f\x11\xc6\xdd\x97\xde\xf9\xb2\x7f\xa0\x1eu;\x10d\xd3\xbb/\xdc}\xe9\xd1v\x92\xf3\xf9|\xa6\x92\xbdm\x03\xa7R\ty\x0eQ\xa4Su\xa8\xb1\xf5\x11\x16j\x0e\xb8\x83\x96\xf8\x8c\x9c?\xec\n\x8d.\x99\xde\xf0\xc6\xf0\xfd\x98\xf2|\xb4\xac0\xee\x9d\xbe\xfb\xe3\xfe\r!\xb4\xdf\xbf\xfe\xee\x8f\xbfz\xfb\xd1w\x81\xb3a0\x8b\x05\x99]\xd6XM\xd1\x9ftii\x1dm\x89\x9dt\xd0 b3\xa3z\xa6\x9c \xca\xc8QZb\x06@\xe7\x00K\xe4\xe5\x08\x1a\x19\xfd\xedG\x8f\x9e\xdb\xff\xde\xbd\x7f\xfa\xe1\xed\xe1\xf5\xab\xb7>\xbc}\xff\xf4q\xbe?\xfbH\xf8P\xfeDgz!\x80\x8f\x91\xb6)\xcdt+\xa1\x9d|,!\xe2\x83\xf9F%\x12\x91 \n\xfd\xeb\xfd\x03"\xb1o\xe9\xc9\xac\xde>\x8e\xfd\xf0\xf7\xbf\xf9\xe9\xe3\xe4\xf8\xb8\xf1\x9b\x9f\xfe\xe0wF\xcf\x89\\\x96f\x1b\xa51\x042q=\xdf\x1b(\r\xbc\x8d\xc6\xa5|l0\x1f\xfc#\xfdEh \x01C\xd6\x98\r>\x11Q\x13\xbb\xa0\xbc\xf6\xfe\'\xbd\xfb\xcf\x9c\xcc\xfd\xbb\xbf\xfe\xd3\xd5[p&<\xc3\x82\xce\xba\x1c\xee\xf7;uo\xc8\x155\x87\xf8\x98i\x8d\x8e%t9\x1f%dJ"R\x12\x9f\xd2,\xfb\xc4\xc4\xe3\xb1b\x1eB\x0f"\xde\xd7\xf1\xee~L\x0c\x91\xd1\xf0z\xf9\x93{\xa7\xcf\x8a\xcd\xbd\xd3\xf2\xf6\xf0\x98O\xd0\xed`i\x16fI\xa7]\n\x99\x85\xf0\xa9\xd1\x02\nc3\xdd\xa0#O\xa3\xf1@>S\xcf"\xf25]\r\xf7\xb1Mn0\x85e\xcd\x1d\x8a\xfeu\x9c\x8f\xc9\x9fHL\x02\xcb\xe0\xb4\xff\xfe\xfd\xaf\xdez\xdal\xbez\xeb\xdd\xef#\x07\xbe\x1d\x83\xee\x10\x1b\xack\x01l|:\xd4;\xd9`\xf3#\xcb\xe7\xa8\xd1x\x08B%\xedU\xd2\x7f\x85Q$\xe70K6\xc7X\x7f\xe4\xfe\xf6\xbb\xc7D\x0f\xc6\xc1%\x9a#F\x1d~\x7f\xf6\xc5\x1f_|Zl\xfe\xf8\xe2\xcf\xfe\xb3\xdb\xe1\xd3t;\xc3[X\x0f\xafci\x16p\x82\x05\xce\x11-\xe2\x03-\xa4\xb4L\x97LG\xf9\x98:\xa5\xc8\x00\xa7\xc6C<\xc4`\xaa\x16\xf1\xfd\x10\x9f\xa9\xa3Q\x0f\xc3\xdcl\xe9\xc6\x89\xd01\x12\x8a;\xdd\xce\x1b\xff|\xf6\xd1\x93\xb29\xfb\xe8\x8d\x7f\xeev\xcc\xd2E\x7f\x1f\xfb\xc8\x84\xc7\xe1\xbf\xc4\xc6\xda\xc4\xd6,\xd8#\xca\x8d\x89l\xf0\x01\x8b\x1e\x8a\x0f\x184\xf5\t\x89G\xf0\xf7\xfe\xb1\xf2\xa93\xea\xee\xa3\xe9:\xd7g\x8f\x96|\xde\xee\xfe\xf0\x16\xf8\xc3\x01\xb4^M>\xbb\xf9\xb8l>\xfb\xe9\xabI\xac{)\x0b\xfc\x0b\xb3\xa0?\xb27}Gp\x81d\xa2\x93a\x87s"{\xf0\xd5`:?zx:L\x08\x19\x99)yS\xf2M\xe0M\xa9C\xdd}\x9f\x90\x9e\x95l\xe9\x0c\xaf\xe3l\xdc\x89%\x0b\x1c\xf3\x02z\xd7~w\xfaw\xf7\x1f\x89\xcc\xfd\xbb\x9f\xfc\xdd\xb5\xdf1\xe1n\x8d\x90!B\x18p\x92E\xdc!\x1e\x0b\xa2\xa4t\x16D\x05\x98\x88AD\x08\xda\'\xc6\x122G\x8fB\xc7\xf2a\x8b\x90\xc14b\xfa\x0b%\xa4\xb7g\x96\xbe+\xe4\xcb\xfbL\xaf{\xdc=\x8e;l^\xb7\x13\xb7\xb1\x04\xb7\x84\xd2\xd5[\xe5\xed\xfb\xa7\x0f\xc5\xe6\xb4\xbc\r\x0e\xee\xdb\xe8\xd4\xa3\xcfl\xe8/\xd1\x82\x8986\xc8\x8ah\x10\x1f\xe4\x81T\x8c\x1at$\x94\x1e\x91O\xa3a\x94\x8b\xe3\xc4\xec\x85\x90\x91\x17\xedVb\xb1w\xa3t\xab\xc7X\xaaI\xf1F^1\xd0z\xf7_\xbez\xeb26_\xbd\xf5\xee\xbf\x10\xd3\xfd\xee\xb1\xdaW\x0f\xb3\x00c\x8f\xc9\x97\xc5\xf6\xc0[\x85\x98\n\x1dls\x8d\xc4\xc4!s\xd2x\xc4\xc7\x12Y\x18%3\xf5(M\xf1\xa6\xd0h\xbc+n3\x156\xaa\xbf\xdf\xdfWR\x8e\x8d\x10\xcamn0;\xbc\xf5\xce\x17w_\xdc\xc6\xe6\x8f/\xbe\xf3\x85G"\xdf\xca\xa7M~\x10\x1d\xa6u\x91\x0e\xd9\xa3=\xe1c\xa6\xfdN\xba\x8c,\x9dG\xe7\x83\x84L\x8d\x8a\x1f\xc6rQ\xa7\xa3\x05\xd3\x89\x96\xca\xc8Xb\x9cI\xb4\x88\xdb\xcc\xa8\x7f\\\xcf\xb2\x7f\xfc\xe6\x7f\x9e\xfd\xc5gs\xf6\x977\xfeC\t\xd4\xd6\xe6u:\x18u\x1e\xd8\x97\xf19\x04\x8f\x82)?\xb8\xd5\xef@\x1fY\x10\x1f*m4\x1e\xeb\xa9ysr\x81\x0fQ\xe1\xfb\x8b\xe4\xf6\xc0(\x08>\xbd\x92\xaae#\x19\xc7v\x0c\x99\xf1\xf8\xab\xc9g?E6\x9f\xdd|\xf5\x9f\xe2N\xec\xd6(\x99:\x9b\x8e\xe5\xe0\x9d\xc5\x1b\x99{\xaf\x0b%\x84t\xe6OFG\t\x89\x9f\xc2Jy86|\xb6>z\xd0\xf1\xcda\xef\xa1\xb7\x00_\xda8\xdb\xa7\x0c\xa3\x05\x11j\x13\xd3\xb6\xe7\x07\xb4\xaf\xad\xaf\xad\x89D\xbb\xeeH\xff\x18\xf7\x88\x91&\xcd\xc4\x1e\x17\xd9\xa3\xed\x1b\x14\xcd\xa3\xb9rJ\xe1l}j\xc3\xf9N\x84\x19\xc7I\xf4Dt\x1a\r\xa3|`\'2\xc8\xf2\x88<6\xda\xef\x1f\x13\x1f9%Y\xd5f\x1a\x94c\xde\xc7\xbc)G\xff\xedz\xb5\xe6\xba=xG\xc7\x83_\rGG\tDs\xb6\x87\xc6\xe6T\xcf7\x02x5\x1aOFH\xd8P\x98\rw\x94\x91Y\xd4g\x94\x90rbW(\x1b\xa1T\xcf\xad\x1e\xf1\x8e\xbe7\x9e\xbb\x11|\xfbm\xde\xd5\xcc\xb9\xe4\xe0\x96\xfb\x9bg\x8e\xd29i<\xf1\xe3\xf8\xe0\x1d@=g\x7fa\xdcR\xda\xac\x85\x89\xdc9ga\xef\xde\xde<\x8e\xf4\x8f\xe3\xad\\\xe2v\xb7\xfd\xb0\xfc\xfc "\xb1\xc7\x80mZH{\xc3\x9f\xc6Sy\x80\xf3\x02Y\xb3A\xfc?\xe4D\x94\xb0\xb4\x1eE\x1b&\xb15\xce\x14e$\xb4\xc4%\xca7\xee\xc6\xb1\xf8\x10?2\x1d\xd9\xc1\x8d\xc4\x1c\xe2Q\xac\xa5s\xeb\xe9\xd1\xc1\xe7j\x8et\x8c\xf2Y\x88M5BWgl\x0b3\xe2\xfb\x8a\xa4\xc4\x0c|N\xb82\xb6|\\Vq\xdc\xad\x11\xc2}$W\x7f]\xdem?06\x08\xb9x\xfat\xf0\x89N8\x88\xc6\t\xff\xc17\x888(\x9d\xb9\xce\xbbU\xc8\xc8\xe7\xa3,\xd5\xa8n\xdc\x1fw\xe3h\x8e\x84b%\xd3\xa6>\x8e\x089\xcf\x9e\xb8k\xc7\xec\xc8\x05:\xe4L\xad\xf5l\xe8\xd4\t\xd9\xccO\\\xf6l\n\x19\xb3p\xdc\xea\xf38\xe7g\x19#\xa3\xb9\xf4\xf8\xfcc&\x89#D&\xc6~\x0c%fo\xf4\xfbX\xe9D53\xbam\xebc\\7\xc6o=\x1b:L\x08\x99\x10#\xf1(\x9aD5\n\xca\xa6\xc6\xc4\x92b\x83\xa2E\xdc\x8e\x9d\x07m\xbeO>=\x7f\x15sF\x94=\xf3\xe2\xec\xd9$/\xe3\xf6\xd5\x997\xdb\xde\xe6\xcff<;:\xbeCH\xc6H\xe6\x8e\x90\xdf\x037(O\x7f\xd6f\x11\xc7\xb1f\x8c\xab8_l\xc5\xec\x05\xb4\xfby?\'6J&\xb6=/\xfac\xd9S\xe8]\xce\x86\xf94\x9e\xe9\x03\xe6L\x80M\x85\xe6\x98y\xbf\r\xad\x9a\'.\xf8\xaep<\x8e\xd5\x0b\xa1\x14+!\xa6\xa3\x992\x1dg\x88\x12\xd11\xcb5\xde \xe5\xb3i\x0b\xb5\x1d\xd1x\xe6\x0f\x93\x11\x1e\x15\x10\x12;\\\xf8}\xe6\xa2,"K\xe7\xeal\x98S\xde\xf6\xc6\xdd\xd3\xf5x\xc4\xb5\xec\xe2K\xdd\xc0\x9d\x86y\xbf\xbd\x9bO\xe3ky\x94\x8d\xbc\x1e\tm\xe3\xfdC\xafB&l\x0c\x96|nl\xc5\xf1p\xdc\x1f\xd7Y\xe0Z\x9fQ\xb7f\x8d\x99w\xe3\x87\x89a\xd9o?_:\xec\x10\xb31\x15\x95D\xc2w\xc7\'\x15y\x0e\xb9Ly\x84\x1f^\xb7i\x87\xf1V\xe8\xfa\x87\xb2\xe8\xb9\xd3\xe1\xc7T\xfd\xb6P\x12w\x1c\x15#\x94\xb0\xc5\xf9\xb35j\x8c\xa3\xb5;\xd7\xa8fL\xbc\xd1\x7f@\x1c\xd5[\x8d\xe7\xf2\xa0Eh\x10\x85R"\xa3\xe8\xfe\xc9/\xc8\xb3\x8a#nAD\x06z]\xcbHI\xc1\x1a\xe9\xe3\xf74\x1e\xa9E\xea\x12\x8f\xf3.\x0f\xb4H\xf8\xf4\x8f\xb0\xd5xn\x0f\x10\xaa\x90\x92\xd83!\x9b\x94Xe$8g C9n\x0ba\x16\xd1|\xc4\x81\xcc\x98M$;\x08-\x9aU~\x1a\xfd\xa3]\xac\x1a\xcf\xf9AO"\xc9\x81\xe98*\xae\x8d3\xe8\x0e\xe6\x8568\x03b6\x02n:\x8e\xc8\x8c(f\x021~\xed[\xd4U{8\xb6\xf8\xc2\xa4\x1c\xab\xc67\xe4\xf1\x9c\x99;.\x91\xc7IIa\xee\x9cu4\xc7\xb5\x94\xaf\xe7\x0c\xcc\xd9\xfc\xf9\x1b!\x11\x19\xa6\xc55\xee\x11\x01\x87h\x975\xdf\x1c6\xfcD\x93\x9a-\x17j\x9de_\xb0\xc61)\xa3\x8b\xe1v\x13\x87d\xac\x1b)!k\x91\xbe\xb1O\xab\xf1\r|\xa2\r\x16\xa6\xea\x8f\x8duHo\x9d\xf3\xa3\xd1\xf9&\x13\xcd<\xb6k\xea\xa4\xa2\xca\xf9c\xdd\xb2\xfe\t\xc9o$\x1b\x9f\x91#\x14\x89#\xbeKu\x82\xcaF\xdb\x9a{T9\xb3\xd4\x9a\xc8#\xe3\x98\xbb\xaf\xf1m|\x0b\x1e\xc7#\x9205\x07\xfc\xd9X\xf2\x8b\xcekT\xcf\x95\x03\x8e\xf4/\xb0ql}\xc6\x8do\xd1\xe3\xfb2<\xda\xa4\xc2V9Gd\xed9P\x82W\x99R\xfb<\xaa\xcc\x96\x88\xc9}\\j\xe77\x8e>\x1d\xad\xa5\xbb\x12\x89\x8f\x82\xf5kU}\x08\x97\xd5;\x1e\xb2\xc2\xfe\xe3\x12\xb9\x9f\xe4j\x1a\x8am\x1c\xd1Y\xdayx\x07\xbb\xe3\x18\xdc!\xf8\x99\x7f\xac~A\xd3mpu\xb3h\xce7}\x8av{\x15r\x7f%\x087\n\x1a\x9de\xb6\x83b\xe7q\xb4\xced\xed2~\x98\xb0\xaf_\xd3\xb6{?\xf3\x87\xe1\xc9\x89$\xd7\xe0Y\xff\xc6\x11\xdc\x87\\\xc71\xb4\xde\xee\x91\xfb\xa14\xceHt\x9asC\x04)u6\x03\xebv\x1etKr\xac?\x93]\xe8=\xa6\xc1\xd9\xcc\xa0\x92\x0c\xf3\xad\x82\xdeo\xb6\xe8\xcc;^|\x8a\x18\xb95c\xf3Q\x1c[\x8d\x02gh\xbdf\xf67}\xfaz76%\x94\xe1V\x87\xdd\xd5\r\xda\x1d\xc7`\xe7coM5\xd1\xefT\x11\xbf\x00\xceBi\xb4\x9e*\xa2\xf5\xa7\xa3\xd7\xd7\xdf\xf1\xc2\xcc\xe0\x00\x98\xe7\x9b\xe5\x80\x8e\xc9nSB,\x8cw\xf0\x8b\xed\x1e\x8a\x9e\xcd\xa0\xb5\'\x05\xee\x98s\xbf\xb4\xee"+\x98\xff\xcbn\xba>T,hpv\x1eG\xbb\x06\x0f.\x00OA\xbf\x0b\x96G\xf0\xeb\x9av\x92\xfd\xea}N\xdd\x0f\x13\x99,\xb0\x8c\xb2h\xd5\xb1"]_\xb0\xa6\x83\\\xc8\x1b\xff\x1a:\x07\x82(\xd6\x95\x88G\xdby\x9c\x8fK\x11\xd7\xcfvN\xb2\xac\xff\xa9\xea]\xa9\xa6\xe9\xb6X\xb4|\t\xfd\x9c\xc71\xd3cE\xc4\x15\xa9\t~]\t\x92\x9c\xf3\xd1\x85\x1e\x9d)w\x01\xda\x8c\x82\xf5\xc3\x8d\xf1\x0e\x97:\x0c\xaf\x04\x9b\xd2y|\xaf2X\xff0\x8cv\x97U\xcc\xed_K(\xba\xd9\xd2\xf5\xef\x17\x88\x9f\x82\x86\xd8\xf6\xb1t^\x1f{\x18\x9e*\x16~MV\xd3\xec;@>\xd2\xf5\xab\x9b$Gb\x93\x13\x83\xf5\x17M\xcf\xd7\x0c\x07\xccGQ|\xd2T=\x93E\xeb$wf\xfac\xc3\xd6\xef\x82\xa1\x13\x9ef\xfa\xd4\xceqp\xb6\x1a\xa5\xa3\xe0\'R\x9fa.\xab\xe0\xef\xa9"\xf1\x19\xb0\xb2\x12\xec\xc5\x89\x13\xec\xe1\xb1\xfc@\xcf\x88\x9d\x07\'\x87a:\nS\xea\xfa\x1d\x8f#\xabf\xa4\x17\x1f\xef\x90I\xf79\x14\x0b\xa55\xa3\xb7[\x9b\xdc\x04\xa0\x1a\x9dox~.Eb\r~\xb6\x03Q\xa2\xd8\x1d\x0f*\rr\x85\xf6\x9b-\xa4\x8dV\xa59\x1fFS\xd2-\xfdF\x81\\\xa4\xeb\x05\x8d\xf5C?]\tW\xd7\xf5\xb54\xb9\x97\xbb\xc0\xd1\x1d/\x8e\xb5\xf38F\xeb\x97\xbb\x88\xb4\xe6\xa3\xe0\x11\xe4\xc3\xa6\xb4\xddCw\xc6\x8av\xdc\xec\x86q\x8e\x85\xde\xb8U\xb9\x9d\x07\xbdiOs\x96\x86\x05\x8d\xbc\xedJ\xf8-\xcd\xec\xf7\x0b\xec\xf8\xd9\x0e\x99e\xbc\xd3\xe0\xed\xd59\x8e\xe9\x14\n\xda\x97\x18\xc3\xcc\xb9k\xda\xed\x80*X\xc3\xbd\n\x89\x81*\xba\x1ei\xd1\xf9\xba\xd6\xbc4\xf8\xd3;\xfeq\x7fLNxR{\x95\xcb\xeaXq>\x8a}\xb4\xe3]v\x0f\xde\xfa\xea\r\x04]\xfe}n8\x16\x08\x0eO\xe6c\x03\xc1Pz\xaf\x12im\xb6.\xab\xa14\xf1\x91\x83\xff\x1bd^\x89\xc8I5\xc7\x1c\xa89qK\xf4QG3\x82\xef}k\x0b\xbe\xc4\xdfJ\xbf>\xf0\t\xeb\xbf:\x0b(}\xb9\\w\xbdW\xf5zD\xe9\x0b?\xeaM\xb9,\x96\xc4\xb2\xda\x86\xfaJ\x86\xdc\x10}\xf2\xbd\xd1QIu\xfd\t%\xea\x11T_\xdc\xa2c\x8a\x00}\xc9e9\xf0n*\xb0\x8c\x17\xf46+\x05\x86O\xd5\x9c\xa5\xcc\xb5\xb8\x95\xe0\xdfxf\x97\xb2\x8b8N\x84\x8c\xf5\xa2\xc1~_dm<\x1a\x10\x7fX\xea\xf4\x95\x06\xf3\xf9-\xba\xa8\xbb\xc4-\xac\xb9P\x12P\xd5\x03\x83\xfd\xb2\xfdm\x91U2\xb4S\xd5\xb6\xd1w@\xe4^^W\xe6\x12\xa1zF\xb8\xc6\x99\xc4{%d\x9d\xdc\x8f\xb2?\xd4q\xb9\xde\xa7?\xa5\xcf\xfc\xc3\x1c\x08\xebr@\x11\x9e\xae\xaa\x08F\xb7\xeb\xc2\x01sM\xbfO<$v1\xdfr\xa0\x8e\xb8\xfd\rT\x8fX\x1a\xec\x81|\x86FM\xb9,\x94\x0c\xbfd\xd0\xa7\xe6\x84\x03\xb9\\ob\xed\x06\xbf\xc4\x83\xb2K\xeb(\x94\x12\x0fu\xd7s\x18T\x84\x84\xe9\x99\xd7\x7fj\x9bY\x1f\xd4\xceP\xe5\xe1)\xee\xed<\xa9\xe6^\xde\x85\xdaN$U\xcfh6\x81}\xde\xec#\xf0\n\xebb\xc9\x9ea\xda\'j\xdbP%B\xf3\xfd\xb60\xd8\x9cQ"\xe2\x96\xb2k\xa8^2:*!\x1f$\x92\xccg\xc3\xed.\x81q\xe0\xc0\x81\x03\x07\x0e\x1c\xfc\x1e\xff\x01' - if name == "apps/webauthn/res/icon_binance.toif": - return b'TOIF@\x00@\x00\x1f\x02\x00\x00\xc5\xd2\xadn\x02A\x14\x86\xe1\x85 V"+\xa1\xb4I\x91H\x1c\xa2\x08l%\nH\xb8\x01d\x15\t\xe1**\xa1\x0e\xd9\x0b(\x14\x1c\xb2\x95\x88\xa6]\x89\xc47[\xba\xd9\xce\xcew\xe6\x8f=\xb3\x07\xc7\xcf\xbc\x0f\xb3\'\x08\xdcgP\xbd\x7f\xa9\xb7\x82\x82fP]nw\xa7qT\x8c\xe0\\\x8f_E\x08\xd2z\x11\x02\xb1\xee[ \xd7}\np\xdd\x97\x80\xae\xfb\x10\xa8\xeb\xdc\x02}\x9dS`V\xe7\x12\x98\xd79\x04v\xf5\xbc\x05\xf6\xf5<\x05\xeaze\xf9|\xe4\x14\xa8\xeb\x8dY\x10\xac{|\x02}=\x1e.\x81Y\x9dK`^\xe7\x11TV\xe6u\x9d\xa0\x1buB\xdb\xfe\xe7]\xf7`^W\x0b\xca\x0f.O\x00\x0b\xa8:-p\xabc\x81\xaa\x8e\x05\xeeuY\xa0\xab\xcb\x02\xdb\xfa\xb4J\x0bP\xbd\xd4T\tP].\xa43l\xcf\x8f\xa3\x11\x16P;O\xbd\x8f\xeb\x9b\xc98\xaa\xb7\xe8z\xacF\x82\xafG\xfa\x7fb\x01\xae\xc7\xdf\xc7\x82\xa4\x8e\x05\xea\xe7l\xb2\x17I\x1d\x0b\xfe\xd7M\x04\xd9=\xd7\x0b\xd2\xba,\xc8\xd6u\x82l]/\x10\xeb\xa2\x00\xd5U\x02TW\x0b\xe4z*\xa0\xea\x94\x80\xaa\xd3\x02\\\xff\x134_k\xb7\x1f\xf8\xd3\xf9q\xd8\x96O+5\xbb\x07\xea\xbc\xeb\xbe\xcd}\xedN\xd1~z\x15\x04X\x80\xeb*A\xb9o\xb3/I=\x1eY@\xd7)\x01]\xc7\x82\xb4.\x0b\xd4u$P\xd7e\x81X\x17\x05\xfazV\xa0\xaf\x8b\x02\xb9\x9e\nP}\xdd\xab\xacj!%@\xf5\xcd\xa41\xa3\x04\xb8~\x16D{T\x8f\x7fuC\x08p=va\xc1vK\xd5u\xb7\x86\x04\x9d\x90\xaaS\x02\xbb\x11\xb7\x06\t\xe8\xfa\xe5\x02\xb1n"\x10\xeb\x97\t\xe4\xbaN \xd7\xdd\x05\xb8\xae\x12\xe0\xba\xab`\xf1D\x9d\x86\x05t}w\xfa\xdewB{\xc1\x9b\x85@]\x9fV\xdd6\xc0T\xc0S7\x15\xf0\xd5M\x04\xbc\xf5_\xc1BQx\xe7\xae\xeb\xee\x80\xbf\xee"\xc8\xb7n+\xc8\xbfn#\xe0\xa9\x9b\n\xf8\xea&\x02\xde\xbaN\xc0_W\t\xfc\xd4)\x81\xbf:\x12\xf8\xadg\x05\xfe\xeb\xff\x05\xc5\xd4\x13Aq\xf5x\x1a\xb3K\xeb?' - if name == "apps/webauthn/res/icon_bitbucket.toif": - return b'TOIF@\x00@\x00U\x03\x00\x00\xed\xd2?L\x13Q\x1c\x07\xf0_\xbb\xd0w]\xe8\xbb\xa1\xe6\xdep\xb9\xdc\r\x8a\x0b\xe8b\x02IcG\x99 :\x90t\x02\'a\x02\x13\x12\x84\xc9nH\x1c\x8c&\xa6\xe9\x08\x13\xea\x02\x89\x10\xca\x06L\xc68\x14"\x11G`2$&%\x96\x04\xed\xf5\x1fw\xbdw\xef\xfd\xda\xaao\xe1\xf7[\xfb\xcb\xe7\xdbw_\x80\xebQ9YX3\x92L\xcd\xae\x19Y\x18c)G\xdd\x8e1\xd3Q\xe9\x9b\xce\x9c\xad\xd2\x9f\xb3\x8b\x86J\xbfh\xf4R\x95~/}BT\xfa\x93d\x19T\xfa;`\x81\xa9L7\x1d\x0b\x00\xe6lU~\xce\xae\xf0P4T\xf9%\xc3\xf5\xfb\xa9*\x7f\x80\xba\xfe$Q\xe5/\x12\xd7\xdf\x01U\xfen\xcc\xf5-0\x95\xe8\xa6s\x1fj\xf3\xccV\xe1\xe7\xec:\x0fEC\x85_2\x1a~?U\xe1\x8f\xd0\x86\xbfHT\xf8\xab\xa4\xe1\xef\xc60\xbf?\xb2\xa7)fgh\x81\x15X\xde\xbf\xfaB`\xf7\x1a\x7f\x91`\xfc$\xc3\xf8_\x9b\xbe\xa8\x05\xb7}\xfeq\x0c\xe3\x1f!\x1a\x98\x86\xc1\xf8`\xdc\x9b\xe0\x90\x9b`\xc5we\x81\xe9`\x12<\x97\xfa{0\xd4\xf4E=L\xb7\xdc\xe5l\x8c\xff@\xda\xc0)2\xa4\x87%\xb8j\xc1l\xb4\xf5\xaed`\xfc^i\x03\xcfHF\xf7&\xa8\xb7\xa0%\x81\x1e\xf0G(\xc6\xdf7d\xfek-\xa3W\x12\xc4\x03\t|-\xf8\x10i\xbd{A0\xfe\x96\xb4\x81\x86^\xf5\xab/0\x18\xda\x82\xe1\x80\x7f\x1c\xc3\xf8\xa63.\xd4\'`\xbe\xea\xfb\xbfA\xb0\x05{\x81\xcb4\x98\x0e&\xc1Ma\x03O\x9b~0\x81\xb7\x05\x13\x9c\xdb-\x1b\xe3\xffH\x88\xfc%\xb2\xa0\xf3\x13x[\xf01\xca\xbb=70\xfe\x9a\xb0\x81w\xb4\x05\xfd*AX\x0b\x1eq\xfd\x11\x8a\xf1s\xc2\x06\x1eP\xd7\x97%\xf8\x12\xe1\xdd\xae&p\r\xb4\x04~F\xcf\xd7}Q\x0f\xa7\xb8\xfeI,\x85j\xe0I,L\xcfB\x9eU\xd6\xf7\x02\xbc\x16\x9cr\xaf\xd3`\xa2\xfc{\xec\x15uw\xa6\xb9\xa3\xf5]\xa7U?\x98 \xeeO\xf08$\xff\x96\x8d{\x01\xf7+\xb8\xfb\xdbv\xf7\xd2\xfe\xce\xdc\xdd\xael\xa1\xee\xe7\x05-(ka\xef\x97dX\xdf\x9b\xa0\xe1\xfb\x13\xf0zX\xf3\xdf\x84\xfa\xa3\xb4]\xdf\x9f`;\x90\x80\xd7C\x1a\xea\xaf&\xf0>>Ak\x0f\x97"a\xfeI\xac\x1d_\xda\x02\xc6o\x01\t\xf5\xd3`:\x9d\'\xc0\xb6 \x0b\xe1\xf3\xcd\xee\xc4\x97\xf5\xd0\xdb\x02\x16\xb7\x04~\x92\xa5\x9c\xee\x13\x88Zp\xa0\tx\x98\xa6\xed\xfa\xa2\x16\xf0\x12\xdc\x15\xfa\xef\x12\xed\xfb\xd2\x1e\xea\xde\x04\xef\x89\xc8\xef!\x9d\xf8)T\x0bj\t\xfa""\x7f\x1c\xcc.|y\x0b\xe6\xf5e\x10\xcf\x91\x9d\xfa\x87\t2\xba%\xf1o\xb0\xce|i\x0b\xaa\t~Q\t\x0f\xd3\xb4S\xbf\xa5\x05\xdc\x1e>\x95\xfa\x9f\x12\x9d\xfb\xf2\x1e\x9e\x11\x99\xdfC\xba\xf7\xc3[0,\xf5\x01\xc6\xd8\xdfy\x81\xd6\x16\x14\xd8\x02\xcb\xcayHW:\x90d\xdd\xee\xb9Q\xdb\x03\xba_\xdfuz\x0b\xf1\xef\xaf\xe7\xff\xce\x1f' - if name == "apps/webauthn/res/icon_bitfinex.toif": - return b'TOIF@\x00@\x00s\x04\x00\x00\xbd\xd2=o\xe2H\x18\x07\xf0\x11B\xb9\x915\x85\xcb\xd0\xd9h\x15a\xba\xa4teM6\x1b\xd6))\xdd\x049\x88\xe3\xe2\xd2\xa5\x1b\x04#\x84"\x97\x94FB\x88\x89\x10\x9f\x82\xd5\xde\xe6r\xd9O\x81"\x0e8>\xc41\xebp\xbc\xac\x01\xbfL\xf6y\xe4\xce\x9e\x9f\x9fy\xfe\x00\xfc\x8a\x92\x80\x06naW\xfc"\x8e\x97OW,A\rJ\xef\xe8i`,\xd6\x95\x8f\xeaD\xaf\x18\xbf\xdd\x15\xac\x96\xfd\xec\xecv\xd5\xfe\xbb\xd1\x15\xf9N\x99\xca\xa4/Nn\xe0]\x90\xb7\xdd3\xe7\x0fW =\x85\x8f<\x16?\xa8\x15\xa3zT]\xb7\xee^\x17\x17\x1e\xc1I\'\xc6\xf2\x9f8g\x85w\xfd\xce\x93\xa61\xbdG\xf4\xac\x98`\xe6\xd3>.D\x96Y\x0b\xa4e_\xca\x0b\x0f\xd1\'3^\xba^\xcf\x9f\xcc8\xb2\xbfy\x81t\xc5\x82\x85(\xa2\xd0\x8aj\xd7 \xc1\xbf\xdbqm\xa6\xebn*\xd3SP\x87\xf9\xb9H~W\x9c\xe8\x83\x04\xb6\x9f\xfa\x9e"\x81y\x83\xe9\x88\xb6\xec\xf0s\'\xb5\xfd\xd4\xb3\xc4\x13\xec\xeb\x88\xce\x1a\xe1\xf6]V[\x89\xed\xa5NX\xdek\xf0\xa1\xbd\xf2\xe7!\xfc\x9e\x12/\xe5\xbb\x9d\'M\x83\x9d\xd74V:\xa2y\xf7\xd8\xc6+E\x1e6\xd3\x9fLiy"\x96\x17\xde\xda\x17\xc8!=}\xc1\xe3\xd6Y\x0b\xa4`3}\x9d\xbcc\xfb\xd7\xe0\'N\x933\xbdjk\x90\x9d:\xd17uD[N\xb0>\x12\xe1\x1d?\xbd\xe5\x94~\xe8\xa9\xcc\xe6\xdd\xb3\xceYA:\x96\xab6?}\xe0\xd4~\xe8\x12\x188\xdb:\xa2\xd0\n\xd2[\xef\xa0\x03\xf0M\xdf\xd5\x11}2\xdfWo\xd9+=\x95y\xf1~\xf6\x9b\xc6\xee\xdey\xeaU\xbb\xf4\xa6\x07\xdd=\xeb\x89\xbe\xa9\xdf\xc2\x9c\xc5O/X\x1a\\\x9d|V\x0c\xd2\x11-\xe3M\xffS\x91\x97\x9e\'\x7f\x99\xd2\xff\xe7\xd6\x95\x85\x17\xec\xf7\x94\xb5\xfez\xceO\xaf\x18k\xbd+\xde\xbb\xc1:\xea\x8cOWo\xd5 \xaf\xcd\xeb\xa4\xbf\xb1U\tT\xed=:\xfd\xd7[o\xe8\xe4\x86\x87=[\xce\x9e\xbe\xd8\xde\xe9>\x1d\xd1\xbc\xbbzk|\xcaG\x9f7.\xe5M\xfd\x83\x8a:\xfb\xfd\x9c\xc53y\x02\xa9\xda]qS\xc7\xf2\x8b\xb7_G\xf4\xac\xe8\xbf7\x12yd\xaeih\x9b\xf82w7\xee!\x1d\xd1\xd7s\xff\xcd>N\xbe\xf5+u\x0b\x07\x1a|v\x0e\xeb\xa83\x12\xfd\x84\x16\xacd\xfa\xc0\xc9f\xb6u\t\xe4\xac#:\xbdyK_6\x93\xf8\xde!\xd8\xa9\xa6qLGtz\x9f\xf4\xf6Y\xde\xd3\x17\xe0\xa7:+\x1e\xd7\x11-c\xff\xed\xa9\x197\xefSs;\xef~\x9d\x84\xd2W\xdb\xd7\xe0\xc0\x8e5\xb9\x93VA@Mt\xd4\t\xe3\xcf\x1bq\xb7?[N\xde4j0H\xff\x16RG\xf4\xac\xe8\x7f\x91\xbe\x88z\xeb\x05\xdb\x94\x01H2;\xbb\xfd\xac\x1c=}3g\xe0|P\xa5`\xec3\x19ZWj\r\x82#E\xf0\xc2\x8b\xaa\xb7\x9c\xcd\x13\xb6\xef\x9fm\xfa\xd1\xb8RG"8Z%8\xbd\x8fj/\xbb\xd3Sv}\x96\xed\x82U1\xca*\x965\x10\xae\xea\x8a\xee\xc6\xd0\xe9g{\xfb\x9cl\xa6\xae\xa42%\x08"\x94\x06\xaf\x8d\xe8\xf7\xcez\xe1e3 a\xd5\x15\x81\xc4\xb1YW\x8cdvW\x9c\x9a\xa8\x13W\xcf\xbb\xb70\xbe\xad\xc1\x89>l\xc7\xb5\xd9\xdd\xd7\x95\xd86 \xf8\xc6\x8do\xb3>)\xc6\xb3K\xb0\x8f\xf5\x846\xcb\xbd\x14\xc3Ne\xae\x8d\x87vR\x1bQ\x81\x94`\xd4\xa9\xaf\xd4\x96\x13?k\x9b\xad\xbb#1\xbc<\x12\xcb\xf8\xb3\xfd\xdd\xe3!\xfb\xfa\x171L\xba\xb3r\x19?\x9ay\xb2\xe0&\xb3\x9e7\xc2\xcc\xfe\xf5\xfc\x85\xab\xbaN\xddm\xc8\xbd\x97\xf1\x82\xf3\x1f\x0c\xbd>\x8e\x92\xb8\x9e\xc2#\xe9o\xdd\xc9YQ2\xb7J\xde2\xf1\x1ct\x81|=\x07\xb1J\x02}\xa5s\xbb\xd4\xdd\x1ap\xdb\'\x9f\xa7\xee\xd6\xfbBp\xea* o\xffI\xf0\'\xb1\xcbA\x90\x7f\x1d\xafO\xa0\'\xa0\xde/\xe4\xd3\x9bV\x06U\x1b\x8d%T\xff\xeez\x06\x0e\x8e\x10\xefE\xcdv2J\xdd@\xf9\xb7\x82\xe7#\xff\xbe\xe5d\x18\xd4\t\x9c\x80\xdd\xfa\x90\xfa\x19\xf8\x8a\x1aVm+\x0f\x1f\xb5\xf7\xa5J3\xeaS\x9f\xfa\xd4\xa7>\xf5\xa9O\xfd\xff\xcb\xaf\x1bi|SN\xeb_X\xfe\xaa\xa1H\xeaC`\xcd\xd3\xf8c\xd1_\xa3\xd9\xe85\xd1~\xdbJ\xe3\xd7F\xfe\x9as=\x89\xffEO\xe3_<\xfak\xae\xfbI|\xbc\t\x8c\xff{+d\xfa\xe2|\x08J\xb3d~\xb5\x13\x9c\x9d\xda(\xac\x7f\x9c\xaf\xa8I|S\xd6\xec\xe0\nSN\xeaC\xb0\x9c\x92\xfac)x\xf3\x8esx\x15\xde=\xde7\xa5\x89M\xe2/\xe4M\xbdn\x0c\xc54~\xfc\x1dl\xae\xdd\xfcZk\xbe\x90\xa3z\xe3\xf8\x10\xfc\xba\xc4\xf3\x0f\xaf\x82S\xe7\xa6\xaaFw\xc6\xf3\xdd\xceq\xbe)\x97f\xdbo>\x9e\xc5\xf5\xc5\xf5Wg0\x88\xf2\xff|\x9e\xcc\xb7\x9f+j|W|\x1f\x82\xeb\xbe5G\xf9\x9f\xce\xda\xc6\xf6\xb3\x89]\xec\xe0\xf4$\xf1!\xa8v4\xdb\xc1H\xdd0e\xbc\x8ed\xfe\xea\x96\xa5\xe54N?\xd7\x87"n?R\xdf\xad\xbbK\xf4=\xb8\xd1\xec\xf8\x99C\xf9M@\xb2\xcb\x94\xef\xbf\xa3\xf4\xd6\xcdX"\xe9s\xb2\xf6\x9f\x05\x08\xc8JQ\xdbV\xd0^N\x8b\x1fH{\xf4\x04\xcfo\xf0\xa4{!\x18\x8aw\xfd\xd1\xa3k\x97fd\xa7\xee\x15\xcfz\xfe>\x9bd\xbf\xfb\r\x8aZ\xed$\xdb\x0b\xc1\x9b\xb7\x9e_Np\x03i\xab\xc73\x81|+\xe4\xed\xff\xdec6\xa2sy\xea?\x8a\xcc?\x19p\xbb\xd4\xdd\x1c\x17~\n\xaf\x7f\xef\x0f{Lh\xca\xaboP\xb9[\xa1\x99\xb9[Y\xc9<\xeb\xcf<\xcdK\xfe\x02' - if name == "apps/webauthn/res/icon_cloudflare.toif": - return b"TOIF@\x00@\x00[\x02\x00\x00\xed\x92?h\x13Q\x1c\x80\x8f$`@\xa1A\x97\xb8]R\x95\x0c\x82\x19\xbaD\x1dz\x11\x848\x14\x02.\xc1\x0c\x8d\x81zY\x94:\xc6\x0c\xe9\t\x1dnt\x0c\x15\xf2\x87\x0e\xa6K1\x8aB$\xa4g\x8b\x83\x11\x94\x14A*\xa5m:\x19\xaa\xcb]\x82\xc2\xbd\x9f\xf7\xee\n\xf9c.i\xc2\xbb\x82\xf0\xbeo=\xee\xfb\xfd\xde{\x0cC\xa1P(\x14\n\x85\xf2?\x91f=\xdcR$\x17[\x8ax\xb8\xb4\xfb4\xcb{\xfe\xfb\xcf\n;v\xb9\xdb\xa7\xf5\xa0X\xf5Y\xdf\xae\xfam\xa5\xder\xb7R1\xcbZY\xb7%\xcd\xdb\x86\x8d\xe6F\xc2\x9a6\xeb\x94\x8a\xa3\xea\x86B\xc6\x8a\xfe\xc1\t\xeb\xd6L\x10\x14O^\xc7\xee/\x92\xac{\xb8\xf1\xea\xd8\xaa\x9f\\?^\x1f\xbf?]\x9e\xbc7\xefZ\x8ax\x05\xaf\x90\x8b\xa5Y\x86\x89E\xc6\xafc=\xdcd\xef<(6\x9a\x9d\xbf\xe43\xb7\xdeO\xd6\x97\xf2\x93\xd4\xed\xe5\xc9j\xff\xdah\xb2\xceq\xfbB\x86T\x1d\xfb\xe317\xd6\x04Y\x1f\xc9\xfa\x03\x19\xe0\xd7\xd1\x99\xa3\x0f\xdf\xaf\xbc;\x14\xb9\xc0\xe8\xbeW \xd9\xff\xa2T\x80W;.\xd4\xf2\xe1\xe1}\xa9H\xae\xee\x90\xa3\xe0\xee\xe9cW\xd7\xe6]\xe6\xfd\x83\x97\xe4\xfag\x15\x800\xea\xef\xf3\xea\xf2v\xce=\xb8\xce:\xa5<\xb9\xed\xff \x80O*\xaf\x0e\x9a\xa0\xff\x0c\xb2lP\x8c\x7f$y\xf7\x17Z\xa0\xf1s`_\xbb\x85B\xf7\xd6A\x91d\xd9._\x92g\x10\xe8\xbcF\xbc\xc9\x04B\xc8\xa8\xcf\xba\xa6\xcb$\xdb\x0e\xf9\xaar\x03:\x84M&\xb8\xbd\xa9\xef\xce\xd8\x87\xd6_\xc8s\xca\xc3c\xd7\x95\xcf*\xf6<\xd2m\x19\xce\xe8\xdem\xbdA\xd8\xdf\xe89\xf4\xb3\x02\x01\xf8\x8a^\xa1\x12ZDS\xad\xce\x04\xfb>\x86\xb1%\x87o\xf3H}\x8bt[\xd8o\xc7^\x07\xec=H\xe9F!\xda\xc6\xa6\xda)\x18\xcc\x8a\xf6\xd5\x13m\x8a\x8b=\xf7QH\xce\xba\x1aM\x92'\xef\x90/\xcb\xeb\xcanW9\x00S\xa6/`um#A\xf6\xdd\x19\xce)F{\x17\xae\x99\xb6\xb1\x0b5\xa9hE\xdf!\xe37P\x81\x9a\xca\x0fuy;^\xb7\xa2o\x97o\xb6a\xc8\xb9w\xf6/\xecX\xb3\x7fE\xbb\xf7Qu^\xdd*Y\xb3\xff9\xed\xfe\xfdht\xffP\xd8\xf3s!\xf2n\x85\x12w6C\xf9\x91\xa6]\x0c\x85B\xa1P(\x14\xca)\xf3\x17" - if name == "apps/webauthn/res/icon_coinbase.toif": - return b'TOIF@\x00@\x00,\x02\x00\x00\xed\x92\xa1\x8e\xdc0\x10\x86\xa5\x85\x81\x86=`\x90G(,0Xh\x1a\xbe\xc4\xf0\xa0\x0bW~\x8a\xe0y\x8b\xa5f\x81y\x85\x81\x81~\x84\xa9{\x91V\xad\xeaI\xec\xd8w\xabJ\x99\xa1\x1e\x7f\xff\xfc\xf3\xc3\x05\xce>\xfb\xec\xb3\xff\xa3F!\x070d\xe5]\xde\xc9\x82\x91\x03\x89/"k\x18\x15R\xa2\x14\xc2H\xfa\xf3\xc8\xbeC\x9b&\xff\xa5bA\xeb\xbb\xf6tg\\\xa0\xccr\x01LK\xb6\xea\xddD\x85\x05\x93\xea\x1b]\xfc\n\x81\x0eT\x9c\xd2-|G:ZH\xee\xbd\x96N\x95U\xa3\x00\xafX\x8b\x8f\x1e\x1c\xbdB\xccO\xa0\x06\x05\xe1X\x12\xcb3\xcf*\x98>\xe7\xf2.H\x94\xe82\\\x02SF\xf7\x9d[\xb6\xfe\xf33\x18\xf9tU\xbe\x81\xf1\xf3\xb6R\xdf\x15%\xcf\xf2\x7f\xa9%\x9d(\xd2j#\xaeh\x8b\xb2\xc7\xfet{\xa0`U\x0b\xf5\xe0U\x17l\xafy\xba\xdf\xbe\xdb\x85W\x90v-\x99\xbd\x91\xdb\x81\xdf\xfd\x0f\x0f\x98\xe4\xc0\x98\xcb\x97X\xb3\x011\xee)\xcct_\xb0\x99\xcf\xd4\xefgF\xbf\xa8\xd1\x0f&;A&\xfd\x83\x1c\xaa\xa6\xfb\xec\xfb\xf55\x1b\x90M\xce\x86\xec\xed\x7f\'\xf8\xbb\xfb\xf1o\xd3\xb7,\xf5?k\xd2S\xdf\xaf\xe6\xbf\xda\xff\x97\xe7O\xd7Lon0\xe4L\xa3HO\xfb9\x97\xef\xe7\xf4\x0f$2\xfdCf^\xd7\xf8W\x90\xe0\x91\xf9aA\xb1\xef\x9eZ\x98\xfb\x8d\xb9|d6 \xba=\xfc\xb6\xf3\x97\xdb\x83\x9b\xcdsom\x85\xbc\x02\xde\x83\xb8;KWK>=\xfed\xd9-\xe2O\xe9MH\xf3\xaa\x89\xd0\x96\xf0}w[h\xa3\xfc\x0cF\xf6\xcf\xbc\xbe\x81a3\xffQ.\xf8\xae\x84\x0f\x17gh\xb7\\\x90(\xd1\x85\xfd\x97`\xca\xe8\x1f\n&jT0\x95\xd3c\x06{\x08M\xe8A\xf5G\xf81\x85W\xac\xa6\xc7\x1f\xf41zn\nv2\xf2~\x9c\xbe*\xc0\x8a\xddk\xe9\xeb\x15\x8e\xe5 N\xe9z\xfa\x9aD7\x95g\xfeh\xea\xb8;\xb8l\x17\\\x00\xd3\x92\xbd\xb6\xef\xd0\xaae\x8f\xad\x16\xb4\xbekO\x7f\xa6A\xc3(\x93\x91T\x08c\xab\x8b\xef\xaa\x10r\x00CV\xde\xe5\x9d,\x189\x90\xf8\x1a\xf2\xd9g\x9f}v\xab\xfe\x05' - if name == "apps/webauthn/res/icon_dashlane.toif": - return b'TOIF@\x00@\x00b\x08\x00\x00\xbd\xd2\x7fP\x13g\x1a\x07\xf0M6\x9c\x04V\x05\xb2*\x84\xb5\x83\xb2\xb9\x82]\xa8\xd6\xc4\xb3\x1a:\xd2\x01\x14\r\xa5\xb4\x80\x15\xccL\x8f)?:\x07v\xec\x11\xb8"0\xa3\xa7\x88=a\xeah#\'\x90\x9b\x16\x8f\x82 3\x15E0^\x98\x1a\xca\xafX\xb99Z\x81\xc63\xe3\xa0e\x00k\nx\xa6\xa1:{\xef\xbb\x1b\x84\xc4\x10H@\x9f\xef_\x99\xd9\xcd\xe7\xd9\xe7y\x10\xc4\xb5\xba\x8b\xac\xe3\xc6\xa2\xdfq\xd39\xe1\xc8\x8b-7\xce.\xee~t\xfci\xfe\x83\xfe\xcc\xfd\x8a\x93\xf2\xdc\xdd\x00D\xc4\xc9\xe4\x9e@\'f\xd83\xb3\x0c\xed\xe4\x1cz.\xf2\x1a$\x8dS\xcf\xbd\x00\xe4\xa9\x8c\xcf\x9aW\xd1aN\xd5\xa2\xc9\xe1\xc8Q\xce\r\xee\x04*\xe0M\xa0\xd6\x19G\x05\xd8\xca\x98l\xca~\x17\xb1h0\xf7\xee\x82\xe4\x14\xa4\x93\xb3\x1c\x188\x0cO\xc0d\xca\xde\x88\xef\x96\x977\x18L4\xa8\xb0>\xf3\x91Sb\xfb]|\x86\xee\xe4\xf28\xce\xcaU\xc80\xffmO1\xc6\x06\xb7\xea \x9e\xf8K\xeaU\x8d\x81\xb6-\xb9\xc1\xaf\xf4\x07\xe9l]\xfc\x89+\xe2\x04\xccCnC^\xf18!H\xc0\x99`\x12\xab\x0e\x14d\xb3\xe2q;\xed\xb0TC[T\xff\x93\x8d{\xdav0\x86z\xf3\xfe\x8d\xd6qSg\xed\xc2\x8d\x93\xe9\xd1+\xc8!r\xf0\x1c<\x91\xf5q\x89\xa5\x83&\xf1@\xc1\xea\x1ez\xde\xa51\xe6U\xafL\xf0\xc1\xa6u\xe8\xb3\xf9\x05\x1d\xe5\x16q\xc2\xad\xect\xbe\x988\r\xa2$X?\xc7\xe2\'\xe07\xa5\xc2Ry\x1f\xedR\x19L\xe5\r\x89\xf2\xd7\xf01+\x7f\x03o=\x06\xe3\xe99\xed\xef\xf2\xb9L^\xb6\xe9`UL\xb2\xb2\xd0@/\xb8\x0c\xb4\xba)7\xf5\x1d\xc2\xda\x7f\x1b{\x15\xb3\xf1\xc9\xd3\x96\x0e\xdeK8\xa0R\x19\xe9E\xae\xdf\xda/+>&\xbdy\xeb\xed\xfa\xcd$\x9c@\x1fU\xd1\xd0\xea\x84|KS8\xe4\\\x17DO\x82\xdc\xbe\x0f;\xf0\x8dt\xee\xdf\x92T\x13\xe8%\xa9_\xa9\xdc\x89=\xfd\xbd\x1a\xea\xb1X\xa8\x95\xdfO\xc2\x0e\x9c\xf5\r\xf4\xe7\xd4\x04s\xe1\xa7\xa8\xfe#a\xf3\xba\xd3O\xec\xf8\xafX\xfc_\xa5\xce\xdfx\x92\xea\x145\x8e\xb2\xc9&\x9b\x15\x8fo\xcc\xed\xc7b\x1fY\xf9\x1e\xc0\x87\x1d\xfc*v\xed\xb6ni\x1e\xc9\xa6z\x18G\xdf%r\xb3\xf4\x9a\xd9\x9e}O\x15\xcb\xf8Kg\xf8#\xde\xac\xdf/v\xfd\xbe\xc3\xfav\xa7N\xf70\x86\xbe\xae\xb4\xff\xdc\xe6R\xe8\xef\xc30|\xda\xef\xe2\x0f\xb0>\xe5\x9aM\xf4\x98KV\xc6l\xc4\xa7u\x1f\xacp\xc8\xfe\xb3\xab\n\xa1\xbe\x0f\xd7yN\xfb\xc5\xfc\x01\x8a\x9d\x80\xc1\x85\xd9\x0b\xf0\xe9\xef\x86\x97\xf8\xbaR\xdf4\x9bN\xd3\x8f\n\xde\x02~\t~N0\xedo\x7f\xea\xb7\x1a\x9d\xffz8\xf7ST\x99b\x93\x1c\xfaI\xd5\x8e\x9f\xbe\xa4\x80__\x82\x7f8\xc3w\xe3\x9b)v\x03\xb3\xf7\xed\xa8T\xa0\xebV\xd3\xbb\x04\xf4\x0b\xe6\xf8\x87\x93\xa9P?\x8f\x07\xcd\xf0\xab\x90)\xff\x8d\x1eW\xef/\xd7r}sm\xf0\xb5\x98\x12\xe0\xd7\xe3C\x1e\xd3~8\xf4A\x07\xcd\xe4\x13\x8dkz^\xf5\xd4\xe5\xa9\xe6\xd8\xa0W\x18\xd4\xaf\x13E3|\x04if\xfc~\xb2\xa2\xc1\x15\xfdq\xbb\x0f6\xe5\'\xa9\x1c?\xdb@\x1eg|\x91\x95/!\xe1\x06\x06Hi\xa9\xf3\xfa-\x8d\xe0\xa9>\x86\xfe\x99l59z\xba\xd2\xc8\xe6\xc3\x9d3\xfd\xc6@\xd6\xffM\xe1\xac\x9e\xa4\x9a\xf0du\xd6\x1fC\x1f\xca4\xc6\xb9\xdf+\xda0\xd3_\xee\xcf^@^\x82sw\xff\x07\xf9\x04:\x8eZ\xfbch\x1c\xa1\xcc\xda\xac\xfc\xa4\xfaL\xc3\x95&\x98\x1f\x99L6\x99\x9b\xcc\x1a\x18\xbfV\xbfV\x91\xb0\x8a\xaf\xe5\x7fc\xf13}\xcc\x14\x9c\x80Y:_\xbb\xd5d.\x11\xe3\xf6\xf4_\x98,\xe7\xc1\x84\xf2B\xb1P,\x06$\x0b;\x8e\xc3\xcd\xd7\xe1:b\x94\xf0X\xbdN\xb4S\x14\x1d2\xc8g\xfd\x1a\xfe$\xc5n\xa0\xd58\x9f{+\xcb\x826\xcc|\xf5\xcc\xa7\xba\x8e\x18!\xc2\xc7\xd6\xff>\x90\xdd\x80>\xcb\xde\xae}c\x18\xfd\x19\x1f\xda\xd9T\xff\x11i\xdf|n&\xb9\x0f~\xfd\x08\xe9!z\x93o\xeb\xdf\x10\xb2\xbe_\x98\xddk3\xe6\x90\xcf\xfa\x02\xbc,ku\x0f=\xefR\xaa\xba\x19}\x98\xdcf\xcb#5\xde\x93\xd4$\xb3\x81B\xbb3\x14\x96Z\xcf_AmQY\xefv\xee\xfa:\x95\xf5\x83\x88gx$\x05\x81>\x9c@E\xb5\xbdw\xaf6M\xfb\x9fS\x07\x1ah\x17\xea>5BB\xff[\x1f\xc4N\xad Y\xff\x8c\xfc\xd97UC}R\xd6O \x92U\xb4K%\xec\xe8&\x19?$\x85o\xcf/\x11\nC\x98\rP\xadF\xeb7\x9f\xb4\x9f&7a\x12\x90\x96\x82V\x13\xedb\t\n\x81Oy\x84tQ\x01\xf6x\xe4\xae;\xf4\xe1\x04*\xad6\xe0_\x9a\x88o\xc26a}\xd2\xb0>z\x015*\xed\xa6F(~HT 2KIH\xf6\x06\x93b\xa6\xde\xf9\x87qO\xc2n|7\x9e\x88\xffX`X\x08N\xf77A}\x98\xe2K\xaa\xbcg\xf3e>S\x13x\xa9\x83\x9d{?\x95\x8b\xe7\xe2\x97\xa9\'\xed\xf4\x02k\x9f\x1c\xf8\xe2aq\xf1,\xd3\x87\x95\x82\x98)f\x02d9\xb8Aii\x19YF\xe4\x12y\xa9\xb6\xf7\xe0\xc2\xed\xf5t\x8b\xbb\x80>,\xfe\xa7\x10qP\xcb\xfd-7Hn\x89i\x01z3\x99_M/B}\x94\xda\x05}i\x97\xf4\xa0#\x1e\xa9B&-\x13\x18 [@\x0f\x85\x86\xc5\xd0\xfb5\xb5@\xee\x92\x0eK\xdd\x02\x919\xcakj\x02\x94\xb4\x94^\x94\xbam\xc2\x12\xa0?\x1c\xd1)\xfd\xc2\xdd\xb1\xde\xe6.\xa1\x84\x92IjK${\x81\x8bQ\x02emDmDW\xc4p\xc4\xd9\xb5\x8e\xf5\x11o\xb3x\xabD(y\xac`/Nn\\\xb8\xee\xd7S+\x03\xbe\xacK\xb6#b\x9b\x03;\x1c\x89\x0bL\x0eK\x0e\x13Jv\tM\xff\x82on\xbe\x19Z\xf1\xe8\xbb\x85\xe9\x95F\xdd\xdec2\xd0\x81\xacSv\xc8\xcf\xd1\xb7\x9f_\x9b\x1f\x99\x1c\x96#\xbe\x076D\xae\x91\x1b7~\xbb\xec\xec\xd2\xfa\x9f\xebZ~r]7\xd0!\x8ac\xf1\xc7\xe2k@\xdc6:\x9e}8\xd2/]\x1f\xb8\xc6\xf2\xeb\xa7\xe8e\x17\x97^\xbc\xdfx\xbf\x11\xbb\xe8\xef\xf2\x16>+\xc9\x88/\x06\xa9\x89_"\xdb6\xd7\xe1#)V\xbfj\xf2\xee\xab\xef\xabG\xd5\xbaF\xec\xeaV\x93+\xfa\xd7\x95\x19{3\xf6\x16\x83d\xec\xbd\xe3\x858Y\x01\xee\xc1\xe5\xa3Z\x9d\xb6[\xdd\xad\xc6\xae:?\x83\x13\x95;>\xc8\x00)\x069\xb4\x06q\xa1\xfe\xe8\x15\xdd\xd8\xad;\xa7\x05\x01shy0\x7f\xfb6\x1d[\xb9#sGf\x06\x13t\x1d\xe2b\x1d\xf4\x8dV\x9f\xd3\xd5\xeaj\xb50+\xe6y\x89\xf9\xc6\xeb\xc7\x83\xb2w0I\xcf\x8e\xd8\x82,\xa0\x0e\xfa\x06\xa9k{\x8f\xe9\x8e\xe9\x8au\xc5Z\xaf\x1f4s\xea\xcd7\xa3\x0f\xbf\x9c\x0f\xb3=\x7f{v\xe0\x82tf\x0b\xbeC\x17\x8a{\x99\x80\x1e<\xae\x0f8\xd8\x83\x86\xde\x7fi\xc9\xf1%\x87\x97\x1c~\x19&_\xbb\x01Y\x84\np?r&C\x9f\xaeO\xefM\xef=\xaa;\xaa\xf5\xfao\xa5]\xfd\xf4\xed\xe8\x8a\xa1\x93C\x7f\x03\x1d\x80\xb4\xff\xf5\xcd`d\xd1J\xbb\x7f\xbb\xa5\x83t]\xba\xaeF\xd7x\xcfz\x13\xe6\x07\xf5\x17;\xca;\xca\x87\xcaA\x07 \x91\x07\xbe\xf0E\x16\xb5\xeel\xfe\x9d.J\x9f\xa6O\xeb\x05\xd1\xa5\xe9:A\x0f\xec\x1c\xca\x1e\xd4\xb7U\xd7\x81T\xb5\xc3\x94\xb7\x97G$\x05 \x8b_\x01^\xbcO\xa3\x06\xa3\xf4 \xbdQL\x0fE\xda\xf3\xdf\xf3\xdb\x8a\xd4E\x8dE\x8d\xd5\x8dL\x0fu\xa2\x93\x9c`\xe4\xb9\xd57\x9b\x0f_\xf9=\xe8!R\x1f\xd9\x0b\x02zH\xd3\x82\xa8\xd3\xd4\xb0\x8b\xd4\xba\xc1=\x01\xee\xc8s\xae\xb5qg\xaf\x89\x06Ez\x10\xa6\x07&\xdaH5\xfa\xf1\xfb^\xc8\x0b*N\xdc\x07\x17\xdc\x06\xdd\xf4l\x17\xf7\xaei\xf7\xbf\xef\x8b\xbc\xe0:\x18\xf4\xe5\xc1\xc3\xd7\xd0/\xd7\xc6-l\xe6\xff\x07' - if name == "apps/webauthn/res/icon_dropbox.toif": - return b'TOIF@\x00@\x00H\x03\x00\x00\xed\xd2-t\xe2@\x14\x05\xe0\xfb\xee:j\x9b\xda=\xc1\x16\xdd\xfa\xc4\x96\xb5P\xbd\xd1`K}j\x8b\x06\x0fv\xa9\x05\x0f\x1al\x83^j\x8b\x0eK\x80P~\xf233\xd0\xdd\xb3\xe7\xf4>\t\xdf\xccd\xe6\x02_\xf9JZ\xec\xe5\xfc;\xff\x1b}\x0e\xf8f\xecg\x18,\xfd\xcc\xc8:\x98\xcb\xf7o\xeb\xb1\xe8\x1ax\x8b\xb1\x9f\x8b\xa3\xa9\xaf%\xd8\xeah\xa6\xbc\x13\x1d_:\xf0\x01K\xca\xde\xc3\xed\x9e\x8d\xa7J_\xd1W\x12}\x85\x9e\x82\xaeI\x98\xa8\xa3Y\xf0YN\xf1!\xeb\x99\xfe\t\x8dT\x1bO\x8b\xdd\x93|\x83\x9dDk\xa3\'y6\x9e\xb1\xd8\x89~A5\xdf;\xf2#\xb4\xa9\xba{4\x03\xceN\xf2\xfd\x1d\xef`.:6\x1e\x8b\xee\xca\x17\xf1n\xe4\xe7\xe2,\xf5\x85\x044\xd1\xd1\x04,IA^O\xf0\xd72B\xdf\xd8G\xaf0D\xcb\xd8\xf7\xf9{\xd5\x9c_\xca\xcdIna\xd3\xc8\xef\xb6\xd0\xc7\xbd\xd6\n-v\xf7\xfa\xe7\xa3\xaa\xe5\x1b\xec\xe00e\x99*\xad\xb1\xe0\xb3 !%\xc5&\x85\xac%z\xc0\x85\x95\xbbB\x95>\xd2\xe2(\xf8\n=d\xe5\r\x03\xa6\xf7\xf5N\x90\x93Y\xa6/\xe5\xfa\xa8\x8fcI\xd2\x16]\xa8\xc4F/\xd1_\xd2\x81j\xbah\xed}\xc5\x803\xe8\xe4\t\x8d=\xdf\xd7\xf4Q\xea\x12n\xd6\x18\x8b\r\xfd\xd4\xb6\xbeg\xe4\x01\x0f\x15\xb6\xd8\x85i<\xdc\xf0\x91\x1dc_\xc4D^\x0c\xcf\xben\xc2\x8b\xf4D\x0c\xf5\xdb\xa6\xc9m\x8e\x8c\xfcl\xeb\x87\xda\xd6\x85\xb5\xd7\x9f\xb98Z\xde\xc1\xe5\x9e\x9fHQC\x97e\xba\xa7\xa3\tx\xa1|\x91%\t\x8e\xfc+\x0bJ\xdeG\xf5\xc8\xc6sKO\xa9\xb3i\xfe\x8an\x8e~\x96E\xaa\x8e&d-\xf3+j\x12f\xfa)\xcb\xa9\xbe\x8bV\xa6\x8d\xa7\xc1\xa7D\xdf\xc1\xa3\x92\x7f\xa0\x7fdm\x8cE\xc5\xaeg\xc1\x9e\xd8\x07\xbe\xa7\xe5\x9b\xb2{\r3\x0c\xa8\xae\xd7\xd3\xe7h\xc7\xf7\xb5}\x9b\xc3\x95uai\xdbx\xe6\xe2\xc0\xc1\\L\xfdD\x8a\xf8e\xac\xa3y\x97\xbc\xc6f\xcfx\xf9\x0cC\xb4\x8dV\x08x!:\xbd=\x9c\x01\x0b\x9b\x12\x08\x9a\xda_1_\xde\xddG\xf4\xfd\xe4\xa0\xc1>\x1aT\xefN\xdc=\x0f?\xb7\xfe^\xd9\xb76\xdd;LM\xc2\xdc5\x16\xecmO^_\xfe?dYb\xffC\xc97\x05\xa9\xf1p\x9b\xb9B\x83O\x9b\x7fvv^\xfd\x9e\xbe\xa2\xafn\xff\x99\x9ek\t\x12\xd7\x08\xf9csr\x1b/\x07\xef\x1d}U|+\xa5T_\x16(\xc5\x81u\xb4\xc2\r\xbd\xcd\xaf3\x0c\x98\xdd\n7\xc1_\xd1\x85Nvw\tx!\xf1\xc9.3\xef\xf7]\x8a\x1b\xff\xb6\xe3_Y\x10h\xc7\xc6X"=\x17\'\xe7fw\xe7c\xaf"&+?\xd9\x9eI?]\x8c\xb6\xcd\xaa\xe4\xee\xfd\xf1V\xf1]\x8f0\xc49R\x93Py\xf7h\xa6\xca]\xcbO\x07\x8fZ{\xc7\xf3@\xff\x0c\xbb\xf7\xc4d\xef\xf5,X?\xf9\x16|<\xd0t\xff\n\xbd\xb3\xbc@Y\xa6\xdag\x08X:[\x03\x00\x17WZ\'\xb8\xa4\x83s\xa7 \xafJg\x18p\x86\xcfI\x11\x93\xdc6\xbe\x88\x8d\xcf\xcc\x08\xed\xd4[h\xb1\x83\xcf\x8f\xa0)\x8b\xa33\x84\xac\x0b\xfeZ|\xdc\xef\x9d\xa0B\x0f\x7f;e\x99\xae\xce\x10\xb0$\xf8\'\xf9\x89\x1bZt\xf1\x95\xaf\xfc\xaf\xf9\x03' - if name == "apps/webauthn/res/icon_duo.toif": - return b'TOIF@\x00@\x00\xc3\x01\x00\x00\xed\x92\xc1j\xc2@\x10\x86\'\x14\xca^s\xdc\xdb\xc6\xa0\x94=\xee5\x17qQ<\xe4\x98k D\x82>\x86\x15\xa1\xbeFB\xc4\xe7P\xd4>\x88\xd2\xbeF\xd5V\x8c1&\x13\x0b\xede\xbe\xff\xb8\xb3\xff\x97d\x02@\x10\x04A\x10\x04A\x10\x7fG\xaa\x8a\xa2e\xc8Enr\xc9n\xe7Bn\x98Z\xe6\xb3d\xd77\x05\x84\\\xcbTM\xe5\xca\x1a\xe7\xceF\xd1\xecN\xfa~\xe4\xc4\x99\xe9\x84\xdf\xce\xac\x95\xddXx\xf9$\xfcr+f]\xa7\xefg\xef\xb4\xdc\xec\xf9}\xff\xe9\x19\xa2\x81\xfc\x8d\xdf\x96\xd7\xees\xb6:@\xf9\x8fi:\x8f\xfa\xf7\xea~\xeb\xc2\xfb\xdeD\xb5\x7f\x16\xa5\xf2\x11\xff@\x96\xb7\x0e=\x81\xf4\x8f"\xc3\xac\xeb_\xb2\xea\xe6\xbd\xc2\xf9g\xd1\x87\xae\xeb\x9fk\xcc{\x8d\x19\xce?\x8b^Y\x1d\x7f\x80\xec\x9dJ\xac\xdfn\xd4\xf1\xef,\\k\xcb\xc5\xfa\x9bN\x1d\x7f\xd9\x9f\x9f\xcd\x8bo7l\x89I\xc8\xeb\xf8;\x1c\xd7j\xcbT\xadQYY\x93\x1a\xfe\x84\xe3ZSu{\xbb8Si\x98x\xff@\xe2Z{.\xd6\x9fp\x01}\x1f\xeb\x0f9\xae\xb5\xeb\xe0\xe6\xde\xbc6\x00\xcc5\xd6/`\x88\xea\xddY8\xff^\x1d\xf4\xd0\xe1X?\x00f\xb3CW\x00\xc6>t\xc7\x0cNl4\xd6\x1f@\xf5nw\x16\xa0\xfc\x13\x0e?\x04\xec\xd3\xc3\xf9\x8f_\xab\xbc\xb5\xe9\x1c\xa70;\x82\x0cK\xf6\xee\xe1\xfc\x00I\xc9\x13D\x8e\x80j\x7f\xcb\xed\x98\x90#`\x97-\x94\xfb\x01,\xf3\xb9p\x0b\x03y\x9e\xb8\xef\xde\xea\xa7\x86\x80bB\xbe\xd1}\xbf\xda\x0f `g\xcd\xf5\xe5\xac\xe7\xa6*f\x97s\xcb,\x8aa\x06P\x85\x00\xc3\x8c\xd9\x98\xdd\xde\x0eX~\xb6}\x98=\x9e\xc4\x0c\x08\x82 \x08\x82 \x08\xe2\x1f\xf9\x02' - if name == "apps/webauthn/res/icon_facebook.toif": - return b'TOIF@\x00@\x00\xdf\x02\x00\x00\xc5\xd2-o\xe3@\x10\x06\xe0\xa1\xbb\xc8\xd9!\x89,\xa5\nn\x89\x7f\xc1IV\x91U\x14\x1d\xb2\xda\x94\x94\x1e,\x8a.%.\xf3\xc1\x16Tv\x8b|\xcce\x86u\xff\x81a\x8e%\xd00\xf0\x82\xe2\x8b\x1b\xe5\xda\xb4\xebxw\xfd\x91w\xa8=\x8f=3\x00rq\xc0"\x1a\x8dX\x0f\xc7\xf8\xa2\xc7\xebz\xd1\xc7\xd8\xc3\x88i\xd4"\x0e4\x97\x14\x0cjc\xac\xef/\x1b\r\x9a\xd6,\x9b\x90\x90ry\xf7+\x12b\xd6dw\xa8/eo\xcb\xc7\x0e\xad\xfa\r!\xf1\x94\xecmy\x18\x12U;\x80n%{[]\x0c\x14t\x97\xf8\xb5\xe8\x9bM\xb8\x92S\x18\xd2\xba\xecm\r\xa9\xa8=\x80\x88\xd5\xad\xe7\x15\xb1\x81\x90>mD\xcfk*\xf0\x05Qc\xfaf\x06m\xef]\xe6\x0e\\\xa2\xdaur:\xba~\xbd\x9d\xff\xca\xeb\xe9vt\xbd\xba\x9a\x9c\xcd\n\x9eu\t_\x0f\xc0Gy\xb9\x7f2\xbf\xbbI3nVW\xbc7|\x0c\xb8\xfeRa\xf3\xa3\x9f\xaf\x7f\xb3\xc2\xf4\x7f\xf0\xdfZr\xae T\x98\xfd\xd1c\xb67E~\xac\x87\x9fv`\x82\'=\xfb\xd7\xbb,S\xf5=4w\xfc\x8e\xf4\xdd?\x9ee\x99\xba\x1f\xeb\x1d\xfa\xf1\xef\xe5/o\x9eT\xf3\xfd\x0f\x13H\xa4w?9\xcd\xb2j~\xac\'\xffo\xc0\x96\xdf\xfdmu\xdf\xc6\x8d\x9e\x82\xac^<\xfd\xcb\xe7\xd9y\xffd&\xd8%}\xf3\r*\xef?-x\xfa\xeaJ\xae\x8bA\xd5\xa6\xbf\xfe\x7f\x8e\xfe\xed\xb7l\x97|\x03\x8e\xc2\xf4g:\xef\xefg\xe7\xf2\x9d\x1c\xb0\x88\xfc[1\xd7_\x1d\xcb\xf7\xb1\x88F\xeb\xf2U\xfah4b\x87\xf4#\xd6\xc3C\xfa=\x1c\x1f\xd4\x1f\xe3\x8b~H_M\xaf\xcf\x17\xab\xc9\xf7L!7i]\x13P\xf3\xe7\x89\x88.r\x7fj~v\'r\x7f\xbd\xc6\xfc\xc9uy\xe7\x1eF\xac)\x7fv^\xde9b\x1am\xca\xef\x9f\x94w\xd6\xa8E\x9a\xf1\x9f\x16\xb1\xc0e[\xc4\x81f\xfc\xa3?"\xbe\x03\x0066\xe1_>\x97\xeb6\xaey0J/`u\xdc\xff\xb1[\xdc}\xef<1:-\xf7\r\x9a\xfb)\x88Lj\xb7x\xbe|\x97\x146)\xdf@\x13\xfef\xfay\x12r\x08?![\xdf\x04\x1f\xdb\xf6}4\xe1=\x1d\xda\xb6\xdf\xa1\x1f\xf8\xf5\x04OH\xda\xf4C\x02_\xb2dm\xf9K\x06\x9c\x04\x12WX\xc5\xf71\x00~\\\xd2\x86\xef\x12(\xcc\x906\xed\x0f)\xecM\xc4\x9a\xf4#\x06%\x19\xc0\x945\xe5O\xd9\x00\xca3\x10\x98\x81\x8a\x1f\t\xe9bw \xef\x97\xed\xfds\\\xe2c]\xbe\x8f\xfbn\xbe(\x01t\xb1\x0e\xbf\x8b\x01\xa8&$\x1eV\xf1=\x0c\tT\x8a\t\x1a\xfd\xba\t\x11\xdfG\x8d\x9aPGLH\x88\x8d2\xbe\x8d\t\xa9\xc7~O\n\x06\xdd~E\xb1o\xa3ASh.\x0eXD\xa3\xf7\xd1\xc3\xf4b1\x7fs\xe7\xd9\xc5\xe2az\x1fi\xd4"\x8et\xbf\x7f' - if name == "apps/webauthn/res/icon_fastmail.toif": - return b'TOIF@\x00@\x00\x95\x04\x00\x00\xd5\x92?h"Y\x1c\xc7\x7f\xf3\x86\\\x8641\x07.\xccm\xa3\xa9\xdc\x11\xce\xac\x07.d\xb6X\x15\x0e<\x1bs\x0c\xd9\xc2\xb1QfsH,\xb6\xd9\x85\xc3"$\xcb]\xe0\xaa\xb5\x11\x16\xbb-\x15\x0e\xce\xe2,\xb6\xba$p\x81#\x8d\x16\xd6\x87\x08[x\x16g\n\x9b\xe5\x1c\x8d\xe7{o\xe6\xcd\x1fgR\xec\xefW9\xbe\xf7>\xbf\xef\xf7\xf7\x05\xf8\xbc*\x05\x9b\\\x93\x8b\xa1\x1a\x9a\xa0\x07\xfc\x93Y?\xe0\'\xb3_1\xd4\xe46\xb9\xd4=\x92\xcf\xa0\xc5\xd5\xd0\x13\xde\xbak\xa8\xc5\x9d\xf9\xce\x8e8 \x93SD8\xbf\xd8\x19\xae\xe7\x8a\xbd\xec\x1e\xcax\x9e\xe1\n\xdak\xb1\x97\xddFWk\xb3C0\xe2\xbc\xb0\x97=\xe2Bk\xa5\xcdJy\x90\x7f\x8b\xb6\xd1\x88k\xcez\xc4m\xa3\xb7(h\xe9\x82\xdbD\x0e\xc1\xfc\xbd\x04\x9f\x9bm\xf5\x8c1q\x86\xcb\xa1\x04c\xde\xa1\xab\xb4\'L\xf3T\xe6\x92\xb6w\x93P6\xcdk\x82\x8fpN\xe9\xc6\xdb\xb7.\xb3\x9c\xe1n\xd1z\x13\x0c!a\x92\xa0\xb0\xeb\xfc\x84M\xd2\x9b\xb0\xdd\xc2\x99a\xef\xee6Gk\xa1]\x08\xf2VI\x0c\x192\xdfE%\xf0R%\xe8R/\xb6Q\x88y\x9av\xac\x86R\xe0\xb5RPC\xf46\xcdO^\x01\xad\xdd;}1\x01\xeeA\xff\xa1\\8\x12\xcd\xce\x91\xde\x07\xf9\x12\xf8U\xa5\xbb\x1c<\xe7\xdf=mT\x1a\x95\xdf\x15\xe3\x99\x0c\xe5\xfd\x10\xfc\xac!,\x94\xebt\xbd\xb3\x12}\xa2\x87\x9c\xech\xfd\xfauw\xc9\xd6{\xaa\x92)\x8c\x10\xea\'(\xec7\x1e\x92\xf0U\x01\x9f`\x8bp\x80\xcch\xc6w\xf5ze%\x9c\xff\n\xcb\xc0\x1b\xc0\xe9=\x04\xf7R!\x98\xaa\xf8\x04\x83\xc0\xf2\x9f\x16\xe1~\x99R\x7fR>\t\xac\xc7\xbb\xa0\xee\x1d\x13\x0e\xfc\x137s?\xc1\xd3\xbb\x7f\xf6\xe1d\x04\xdf\xbb\xa5\xbf\x0f\xfc\xf9\xe8\x05\xf5TQ\xa8j+~\xfdn\x03)\xc2\xfd\x9c\xc1\xfdg\x1f\xe0\xd3\xac[\xce]\x08\xc1\xb7\xe1\x8f\x8f?>~a\x88\xf1$\x8b;p*\xe8\xdf69\xeb\xec\xdd\xf1?9uAW\xae\xd3\xcd\xf8d\x06\xaf\xe7\xff7\t~\tX|\'.,\x95\xb3\xf8\x97\x01c\x02b\xd8\xf6\x83\xf5Z^\x1b\x04\xec\xeeT\xa9;\x8dJNN\xbaf\'!\'\xd3\xefT\x1di\xd9\xdam\x18&\x10\x0bQ\xc9\r=*\x89\x05\xfa\x8d\x86\xb6\xb5\xeb\xec\xf6\xd6\xae\xd1\x83Fe\xaa\x1eKE\xfb\xbc\xc1\xb14U\x8d\xb7\xab\x8e\xe9\x8b-\xe4M&\xd0_\xf97\x1b\x95.M3y)D\xa5n\xb6jz/\xef:E\x83@G5{i\xd1\xb2v\xa8\xd4\xd2{\xf2M\xfc&\xbe\'\xd7\xd2\x87\x8a\xac\xb1Ow\x14\xbb\xd4\x99\'\xe8@f\xbf\xe9\xbc\x0f\xd6H\xef\xb2\x8e\xc4\x8e\xe2\x85\xddQ\x8eD\xf0XY\xd3<\xd9\xf7T\xcdJ!\xf0\xa3B\x10\x95\xea\xae|\xa8+Q\x9f\xd8\xab\xda\xd8\x19\xc7\xed\xa7\xa8+\xe3\xf8\xc6\x0e\xdc[\x9d\n\xd7\xe1q\xbc\x9b\xae+b\xa1:\xcf|U\x13\x0bu\xa5\x9b\x1e\xc7\xaf\xc3\xa7\x02|f\xf5\x1f' - if name == "apps/webauthn/res/icon_fedora.toif": - return b'TOIF@\x00@\x00\x95\x04\x00\x00\xbd\xd21h\x1bW\x18\x07\xf07\xdet\xc8I\x8ay)\x1d^&q\x83L\xce2\x01\x1dY\xe4%\xc4\x83l\x14C\xb0\xd2\xa9^\xa4\x18*\xdcD\x08,\xb2\x9c\'\xe9\nq\xb0\x8a!$Y\xe4I\xa4\xc5\xa2\n\x94\xb6\xf2$Mg\r\n\\1\xd8\xd5v\x1e]\xaa\x12\xa1\xd2p\x95\xa3\x9c\xee\xde\xbbw\'\xbd\x93\xec\xef\xdbtO\xfc\xde\xfb\xbe?\x00\xac%sz \x08S\xb0\x8c\xd4\xcf]F)\x18\x84z@\xe6\xc0%V\x14\xe8\x81\x02\xe2\x05\xef\xce\xf7\xef\x11\x9d\xba]\x1fC\xb6w\x01\xd5\x03\xd3\xb2\xf5\x80\xcad\x9b\xad"}\xe2;\x948\x7f\xb6u\x87\x92\xefL \x90\x84\x93\xd8f\'!\xf2\xa1\xcb\x13\xbe\x1c\x9f\x82\xcc\xb1\xee\xdc\x8f\xf3Sd3\x11~\xf2 \xf7\xcfw\xb7\xe3!\xe2\x1bK\x16\x82\xccs\x9f\x89g\xf7\xef\x9e\x19\xb6:\xec\xfe[\xbd\xb68g;\x13\x84\xe3\xe9\xf7\x18\xf5\x99\xa5\xb5\x86A\xadgg\x7f,\xb5Dk\x12\xf7\xe0\xf4\xdf\xae)m\xc3\xbd\xbe\xfc1#\xadJ\xe9\xb1g\xc0\xb6\xf7\x90\xd0\xfb\xdd\xf0\xac\xd3fF\xba\xe8\x03q\x9c\x1c\xc8\x1c\x9b~\xb3f\x8c\xa8G\xb5\x81o\xbf\x81\xcc\xd1u\x04T\xc4\xe2\xdfy5Jo\x1b\x0f\xd7M?3\xdc\x82\x8a\x10\xd5O2m~3A\x15\xbb\'\xdaV\xe3D{}~\xd8=mf7,\xfd"\x07f\x12\x93\x94\x14\x948\xb6\xdc\xfd\xf7\'i\xbfl\xbe\x7fle=-,H\x19\xa2[\xc3\x1d\x94\x1c;`\x9b}\xe71\xa9W\x15\xe7\xa9\x96H\xde 4\xdc\xc1$\xb9\xe7\x85\xad\x06\xae?\xdd\xa6\x9f#oP\x19N@\x0f\xf8\x7f\xfd\xb7\x11\\\x875\xf7\xb3\xf8\x16\xc2\x12O\x99@\x9d\xf1\xf5\xef\x89\xe9\xffu\xdf\xfd\xecs\x01\x9f\x80\xf5\xa5>\x9c@\x01\xb1\xf9\x9ab\xd7\x7f\xd3\xbcO\xe3~l\xf8{\xfe\xf3\x04\xa2\x80M\xe7\x85\xbd\xb7v?\xbb\xef}:\x8c\xf9i\xdb\x97\xa8\xaf\xec\xf1B\xafj\xf7?\xecz\x9f\xbe1o\xf7wD\xeb\xcb \x83\xac\xd3\'}M\x19\xe1Kn~\x1e^\xf8\xac:\xab\xbf\x8a\xf9\xcb\xd87\x00d\xeer\xfd4\x91\xbf\x10\xf6U\xe6\xd8\xb7\xcf\xe6\xe3\xdb_\x90\xf0\xafz \x08/\xd3\xdf\x11\xf1\xd7\xb7D\xfc{\x10\xa6.\xd1\'\xf5\x8c\x14#N\xa4`\x19M\xea\xcf\x16ig\xe6\x84\x8eC\xef\x88\xe4\xa92R=\xfd\x98\xf0\\8\x10\xaf->\xdd\xd6\x14\xabO4\xbb\xff\xec\\S\xaa\xdbf\x87\x9f\xfc\x1di\x89\xf8\xd6\xdd^\xcf\x0b\xaa\xab\x1f\xeb\xdf\x7f\xf5\xd3\xbfz\xb9\xb6\xc1R\xaf\xcf\xb3\xeb4\xfd@\xe4)\xfe\x11\xa2\xd9\xd6\xfd\xbbKl\xfa`"?,\x8e\x9e\xbd\x9b_\xc1\xf6\x16\xf9\xde\xf0Q\xd9\x8dqt\x9aO\xa6f\xcd\x97\xbf\x87\xf9\x15\xd1-_e\xc2wfv\xf6\x01\xbb~\xd8}7\x9c\x7f\x98\x92:\xabS\xd0\xeeWDZn\xd6\x8alz\xdb\xe8\xe5\x06\xff\xbc>\x9f\xf6\xb0/:h\xf3c\x02M_\xe8\xef\xee\xe7\xc4l1\xbbo\xf6q\x15\xf7N\xb4_\xdeZ}\xa7\xb8\x1bo\x89\x07\xe2(y\xd0z\xc0\xf2oH\xce\xcc\xd2g\x17\xa9\xd9}M\xe1\x05\xbf-s\xa6\xbf\xecx\xfd\x8ekjz\xd5)\xf9\xb7\x000}2y\xee\xfa\xf4\xfc<\xb4|r\xf2^\xff\x9b\x96\xaf\x07L\x9f\xcc^L\xb8\n?\nL\x7f\x07\x9b~X\xe2\xaf\xc0\xcf#0\xf4+\xe2\xf8\xd3\x9f\x96_\xe7\xdc\xfc\xd6\x15\xf8\xea\xa7\xd7\xfb{\x7f\xa4a\xf7\xdf)~\xb3\x07|\xee\xff\xebs\xbb\x1f\xce\xf9\x7f\xbd\xe9\xc7\x04\xbb\x9f\x91\xe6<\xfe\xbb\x990\xb0\xba\x1dg\xf7K\x1c\xee\xf3\x84\xef\xb5\x815l\xfa\xb5.\xbb\x9e\x84\x00\x90~G\xc4o\x90v\xf9oq\x1b\x7f\xfdq\x95U?B\x088\xfd41\x01\xfa\rH\xdd0^$X}\x99\x03\x14\x9f\x17\xc2\x12y\x83\x8eh\xcf\xc1J\x02\x9f\xfcE\xbdl\xfa\xcd\xbd\xd3_&&\xf0p\xfdQ\xed\xa3\xb6\xd5\xe8U\x8f\xab7kw\xcf\x0cJ\xb1f/8\x03\x80\x9b\xcf\x0b-[\x06\xf66\xda\xc6\xa8\xfa\xb0\xcb\xa8C\x00\xbc|^\xb8>o\xfa\xa7\xcdQ:\xacM\xfav\xa7o\xdd\xe0Mw\x94\x1e\x9ah\xeff\xa9\x88<9\xd8\xc2G\xcdK\x9f-\xb2\xd8G\x08\xcf\xbc\xbd\x92\xd0y~Y\x08\xf7\xf7\xeff\xff\xda\\aJ]\x12"\xe0^%\x8e\xfe\xaf\xb4\xf0\xc5\xfaWD\x04\x0f\xbb\xbd\xea\x8boXl\x15\x9580\xa2\n\xc8\xfd\xff3K\xe1\x9c\xa6tw5e5\xb7\x12g\xdb\xb8\x8a\xdcv\x8eW\x94\x92\x81I\xbb\x80\xea\x1c\x18\xbb\xa2\x9e3`\xea[\xf9\xfe\xab\xa3\x80\xbdJ\\\x12\xaa\xe8\x88\xb9\xd5~\x97Q\n\x06\xa1\x1e\x909v\xf7\x7f' - if name == "apps/webauthn/res/icon_gandi.toif": - return b"TOIF@\x00@\x00\xc9\x04\x00\x00\xbd\x921h\xf2N\x18\xc6op\xc8\xd0\xe1\x86\x0e\x19\x1c\x12\xe8\xa0\xe0\xf0\t\x1d*\xb8(t\xa8\xe0P\xe1?4\xe0 \xe2P\x82C\t\xdfP$\x8b\x88C\x11\x87\x0fq\x10q\x10t(\xb4\x83\x10\x07\xc1,\x82\x1d\x04\x1d\x84t(\x98\xa1C\x06\x87\x1b:\xdc\xe0\x90\x7f\x8b\xb5\xc9i\xac\xb5I\xfc\x8dw\xef\xdd\xf3\xbe\xcf\xfb\x00\xf0;\x18\xd0\x84\x1e\xf6\xdc\xefa\xf3\x10\x1c\x98\x08\xe5\x8b\xbe\xd5kJ\x12\x89(\xac]\x8d/K\xc5 s0\xf5\x01\xfdVo`\x9d \xacMr)\xea\x10\xea',\xee\xeb\x16\xa8z\xa7\xe2~\x07yX\x95\xf4-\xa8:,\xb8\xbd\x858\xaf\xea\xdbI\xa2\xa7\x90\xbb\xd3_\x8d\xd7g&\x99\xd6\xddt\xe0)d\xce]M\x19q\xed`\x96\x0fk\xc6\xd9\xcb\xac\t\xdd\xd3\x0f\xa4\xcd^\xb7\x83\xcb\xd3\xa3\xb4\xe1B\x03\x17\x83\xee\xe9?\n\x86\xfe\xd9p\x95\xf6s\x7f\x12\xadNeLE\xdd\xd3\xcf\xf2f\xa7\x07\xf4\xf2T3m\xa5\x81\xddL\xe0uL5%\xef\xb2\xd4\x84\x0c\xf0\xb0]\xc9\xbc\x95W\xda=}\x0f\xeb5eM}O`WZ\xcct\x13\xcf\xfd\x08\xe5\x9e>\x03:\x15\xfd\x1bT}\xce9\xa9V\x0cNr\x9dJ\x9c7<\xcd\xf8\xc9yI\xbaR\x1e\xae*\x9bp\xce\xfd\xab\xc0\x82\x16b~\xa9>\xe7\xc2\x9fn\xdf\x8e\x8b\xc1\xd5\xf9\x88\x13\x91\xb5zO1\xaa^\xe9\xae\xb4\xcc\x8a\x88&\xb9\xdftpB\xec\xfal\xb8\xf2\x80\x01w\t+\x0f\xae\xc6\xed/\xf5\x14u\xdc2n\x1a\xd8\x17\xdd_?\x90&\xff?n\x19\xde\x9e\xfb\xa7\xf5\xb0\xa6~\xde\xc8\xb8\xa7Lr\x83\xaf\x1dE(XP\x89\xb77\xa5\xfd\xf5\xcb\xc2z\xb6\xa6u\xa3\x03\x06x\xd8\xfb\xc4\xa3\xf0\xa7P\x16|QC\xfbC\xfdQ\x901\xf9vZ\xdf\x7f\x03\xbeh\x03\xafwp\xdcz\xa5\xbf\x7f\xd5\x84\x97%y\xe3] \xbd\xff\xfc\x11b\x87\xc6\x96\xef\x12\x11j[b\xb5\xd0s_\xddxC\x0f\x07\xf4/\x02\xf8\xee\xf0\xd5x\xb3\x03\x19w\xa5\x11\xf7J\x9b\xbb`\xc0\x80\xbe\x8e\x9d\xb6D\xb4Y\xbf\x98\x19\xb9\xdc\x97\xa7\x90U\xd2?\x1c\xf5j\xcf\xfdN\xe5Q\x88\xf3e\xe1\xa6T\x95\x163\xd5\xaaP\x17\xd1\x88\x036\xb8\x88y5\xfd\xd74p\x96g\x80=|Qk\x0fv#\xa2\xb8m\xf5\xe5\x16\xacr\xb0\x8b\xc5\xec.\xe1\x84\xfa2\x89\xd3z\x03\xff\\[\xc6\x7f\x1f2~\xe0 \x11\xea:F\x0f\xe5\x1f\xf5\xd0S\x8e\xd2)\n8N\x1e\xde'\xfe>|\xef\x83\x88\xfeU<,p\t\xe6\xdb4\xca\xb8*i!\x06\xb8\xa7~\x94\x0ek\xd6\xda\xaa~6\xbcKD(\xd7\xc4\xdf\x130\xc9Y{\xaf\xea5%\x90\xceC\x00\xdcU\xb7N\x9fWC\xb9\x01\r\\&\x90\xb6\x9a\xbd\x81\x8f[\x19?p\x1d-d\xb5\xf7\x9a\xe2\xee\xc6W4!=\xdc\xdc\xf9q\xcb\xc3\x82\x83\x90\xe5\xd5\r\xdfa!E\x1dF\xbd\tk\xca\xbaz\x9cg6\xeaRT\x136\xa1\xf3]\x8d8u\xcd\xf9G\x81T\xcf\xc3\xeb\xd8\xb4~6\xec)=\x85\x1ev*\xbe\xa8s]0\xe0\xb4EN\xff\xdf\x83\xf9w\x06PQ\xdc\x97\xb1\xb9B\xc6]\xe9)\xc48\xe4\xfe\xcb\xcc\xfc\xb7\x88\x9eBf\xcfQND\xba\x05am\xce9\xd1A;\xd8 f\xebJ\x91\xaf\xe9#\xd4eI\xd5\xb7!\xa2\x11\xe7\xfc\xf6'9\xe3.\xce\x93\xbe\xaf\xb3\x98\x9d\xfb\xed\xeagy2{\xd71#u5E\xdfA\xa7bw\x07\x93\x1c\xa9\xdf\x0e\xaenN\xd8$\xda\xa5\x1f\xd6\xec:\x80\xb6\xea\x0fh\xafF\xa6\xfelx;^\xcfC\x9c\xb7\xa7_\x16\xb6\xf9\xcf\x80i\xdd\x9c\xb6\xa3t\x8a\xca\xc3\x9b\x12\xa9\xffVw6\x7f\xc8\x94?\x0f\xdb\x95\x96\xb7^m\xce1\x9fg\xe4V\x9e\xfb\x11\xca\x8e~1\xd8 2N\x0fS\xa6\xff\xf2\xf0.\x81rq\xde\xd8r\x1e\xf6\x94\xed\xf5\xfb\xd3\\\xfb\xaf\x81}\xd1\xef\xea\xd7Saw~r\xcb\xcb\x1f\xf3p{\xfd|m_\xa7-\xc6^\x00\xc0EL\xc6d\x06\xff\x14\xb6\xcd\x94\xf1\x93n\xe9\xfa\xa3`S\xfe}\xa3\xf4\x90\xfcS\xc6\x7f\n\x9b[e\x80\x16\xba\x1d\x93\x95\r\xdc\x0e\x02\xdb\x8c8\xd2\x81\x0f\x0f\xaaR;h8\x1b\xa12\xfe\x9b\x92\x88\xf45\xaa\x92\xbd\xed/IQUI\xdf@DU\t\xe5\x02\xe9\xb2pSz\xee'\xd1f\xc5\xae\xac\xfe\x9cbp1\xd3\xf7\xa6Sqb\xfa%\xf7\t\x11\xed\xa7\xde\x95\x064p\x0c\x06\x04\xd2\xfbt\x80\xfb\x1e\x168\n\x03\xe6\\X\xfb\x89\xb6\xaa\x9f\xb6^i\xe0\x02Z\x08\xf7\xd5\x1d\xea\x8bY\x96OQ\xc0%\x9a\xb0,\xf4\x14u\xcb\xdc/3Xp\xda\xf7M\x06\xf4\x9c;m\xbd\xcc\x92H\xc6\xea\xbb\xaa\x8c\xc3ZMy\xab\x8f\xb8\x01\xcd\x80\xc3\xc0\x80<\xcc\xf8\xa9\xe8E\xcc\x17\xd5B'l\x8a:\x94\xb2\xd3\xfc\x0f" - if name == "apps/webauthn/res/icon_gemini.toif": - return b'TOIF@\x00@\x00\x06\x04\x00\x00\xdd\x92+p\xe3H\x10\x86\xdbp\xa6JD\xe3\xca\x81\x1dU9@]er\xc0\x01\x061\x11\xf0\x82\x80[\x10*b\x03\x83\x1c0\r\x90\x89\x80\x03\x0c\x1cp \x0bDb\x10p$\xc0\x01!\xa6\x17\x98\x03"\x06\xc2!\x86\'\xe0\xabZ\x9d\xb7\xb2\x9a\x199~\xe8y`\xbb\x994\xdd_\xf7\xdf?\xc0\xcf\x10=x#S6\xe7w\xc6\xda\xec\xe0\'\xfcj\xf8\xfc\x915\xa9\xfb?\xb0\x9bt\xce\x17\x18\xed\xcc\x00\xdb\xc6\x846*cO\xe8\xa7=d5;\xf8++\x7f\x06\xb3\xf6\xd5\x880m.\xcd\x17R&]c\xc1N\xce\x02G\xb8\xd8s\x8bGV\x0e\xbb\x01\x97|\xbb\xf7?|\xa5\x0fHO\xf8q@Vz\xc8\xb7g\xf4\xb9U\x02}\x9e\xa0;8d\xf7{\xde\xba\xf0\x1b\x1b%&h\x1bE\'8\xab\xcbn\xdf\xb0U\xef\x1fy\xdf\x07\xbd\xae\xea\x10\xf2bw\x97\x9dFHh\xba\xaa&\xf5\x94\t\xf2\xfb\xe03\x91\x9bt\xb0[K_\xd9%\xb6\xa2\xda\x0b\xc9\xc7\xbf3\xe2\x1e\x1e\x9a\xb5l\xb5\xdd\x9a\xd4\xe0_\xf34\x07\xfdO*\xfd\x9eV\xf9\xe4\x15\xe4\r\xb4\x1c7X\x9bq\xb5^\xcf\xa7_Kx\xd7A+\xf7\xf4\x0e\xf6s\xfa\xc7\x85\x91P`\x92QA\x9f\xc7\x95_r\xfb\x17`\xc8\xe2.m#K\x9d\x05\x81\xb8\xbd\x9b\x1f\x0f\xf79\xfb\xbc\x91x\xee\x90C\xa1\x08\x85\x8eW\x19.0\x15\xba\xad\xf4b|Mt\xfa;\xc3\x1d\xe7b\xea1)\xc6\xbf\x11J\x9e\xa4p@\x7f\xe3\xfbG\xf6\xc4=\xe1[\x1b;\x85\xd2\x16\x9d\x9e\xcdC\xe4\x06L\xe8\x89\x11\x88\xd7\xe5\xa7\x8d\xfb\xd9\x1a;\xaf\x90\xfc\x9e\xce\x1e\xfe\x0bY\x9a\x11V\x9f\xbb\xf7\x9f\xb2C\x9a\x078:\x90A\x8aW\xd2I\xcf\x1f\xfcg\x81\xcf\xb7y\'\x86\xc6\xc6\xc49\xa2\xd9{t\x8e^\x16`L\xe2\xde!\xdf\xa6\xb7\r\x95\xed\xe1\x94\xcd~\xfc\xb3K\xe3k,\xee\xff\xca\x92\x7f\x92\xbb_rW\xf9W\x1e?\x14\x94\x0b\xaa~\xbfe\xea\xe6\xc9\x7f\xe5\xf1g =\xa2\xee\xf7\x17\x91\xaesp@\xb6\xeb\xca\xe2O\xc5\x96m\xc5}\xa7\xb06\xe5\xee\x1f\xe9e\xf1]\xf0\xc4\x96\x13\xba\xcb\x13\xd1\x07\xe5\xcb\xe4_\x8a\xdb\xdbh)\xbe\xb7\x15\xd7\xed\xae,\x83\x7fA\xe5\x96\x1a\x93\xdf\'Tj\xefBU\xfc\x01\x91\xda/\xcdS\xe5O\xdb\x88\xbfO\x19T\xc4\x1f\x10G\xd0\x03| \xaa\'\x02\xf1}\x06\xd5\xf0/\xa8\xdc}{\xcb+\xa1~\xc8\xa1\x02\xbe\xab\xb8\xee{\xfa[\x94W\xb6\xcb\x13\xe5\xf0g0e\xea\xe6\x11\xb6\rk\xab&\x14\xd3\xdd\x90\xb2\xf8\x0e\x8e\x89\xc6N\x8c \xc1\x8e\xf0\x89[\x1fj\x9e\x85\xfb<\x1c\xedM\xe9\x91Q\x8aW\xbb2\xc0[vh\xe6jsi>\x90\xc3\xcaV\x97\x9d\x8d\xb3\x1aGoVE\x06\x18\xf2+\xda8d\x19\xe5\xfe\xf6f\xd2")\x95\xf4p\xceo\x19\xa1=8\x1e!\x8f\xabn\x08\x14\x8a1\x89;\xcdy\xfa\xaaW\x16Wi\xac\x18\x7f\xa5\xc7\x9d\xa6\x19:]\xd0\xb8*\xe4\xc5\xf8R\xc9\xb7\x0cJ\xba\x10\x08\xb7\xdc\x17\xa0\xdf+}\xacL\x95m\xe1\xc0a\x81\x0b\x0c\xc5\x1d\xfd\x8c:N\xc4\x05\x1c\xec\xe7\xa4\xf7\xc1\xc1\xb8K\x93f\xab=\x05[\xd4\xb6\xea\xf9\xf8\xadz\xdcamf\xaf\xd6\x84v\xd9\xa7\xff\x1eM*\xeb\'9\xea\x1b\xb06\xe3z\x0f?\x93l\xd5\xdd\x9a\'\xe8\xd7F>\xfd\x1eH z\xd8\xd8\xcd0A\x97\xc8\xeb\x05\x99*\x93q\xab\xdc\xc0K}\x85&\x95\xbbG\xa81(\x10>\x97\x9d\xbe\xe1Y\xbd\x7f\xd4\xf3\xadz\xa0\xd0\xcf\xeaP(,\xf8\xc5\x88\x94~#\x1c\xb2\xd9\x9e\xb7.|a#T_\xcfy\x03\x8a\x86\x05O\xb29\xbf3\xd6\xe6\xf9f\xdfk\xe3\x0f>d/\xc4\x82\x9f#\xfe\x03' - if name == "apps/webauthn/res/icon_github.toif": - return b'TOIF@\x00@\x008\x04\x00\x00\xed\x92!\x88*k\x14\xc7\'l\x98`\x98`\x98p\x83\x82a\x07\x0c\n\x1b\x1c\xd8\xa2`\xb8\x03\x13\xd6\xa6`\xb8\x88A\x06\x83\xc8\x0b"SD6\x88\x18\x86\xe1\x85E\x0c\x0bnXx\x1b\x84\xdd h\x114,\xcc\r\x82\x86\x05\r\x86\t\x1b\xbe\xf0\xc2\x176|o\xe5\xe2u\x9c9\xce:\xfa\t/\xdc\x7f=\xe7\xfc\x7f\xe7;\xdf\x9fa\xfe\xc8\xab\x02L^(H\xb2\xf2\xb3\xca\xd5\xb9\xfa\xcf\xaa\xac\x14\xa4\xbc\x108;7\xce~\x974\xbdb,\t\xa4%\xa9\x18\x9a\xfe]\x8a\xb3\xe7`\x9b\xa2\xbf\xdb\xc1\xe4\x00u\xb0\xbfk\x8a4\xd9\x8dT\xda \x1e\x956\x1a)\x1a\xec\x89\xe8\x9d\xbd\xdda"\x9e\xf6\xdf=\x9d\x9c\xa8\x9e~l\x1e\xf2\xc2\xdb\x82P\xd0\xdb"/x\xa7?\xa6\x86\x98P\xd2\x10?z\xccBQ!\x94UT\x0e\xa7\xb7\xca\xe4\x0cj\x95\x0f\xa3\xfbr\xe4L\xf2\xe5\xbe\xa6\xb3\x89\xe5\xb9\xf0dI\xd8\x84;}\xc5g\xd1\xee\x8c\x8a:G\xe7\xb0\x83U\x9b[\x16\xadx7>\x1e\xd87\xbe\xe7\xd6[\xc9J\xc5\xf0B\xae\x18\xb2\xb2&\xdds\xf6k\xe2\xc1~z\xd8\xf1\xf3\xfcx[\xbdL\x1c\xb6C\xc5\xb8\xb4\\\x99\x1f\xdb\xeb\xe1=)\xa8q\xf6k\x11Rj\xda7TQ\xda\xd0\xf4p\x8eM\x84\x825.\xce\xc6\xd9\x1a\x17\n\xb2\x89pN\xd3\xd3\x86\x8adew\xa2\xd4\xb4{\xaa\xa8\xc6A|Mw\xbe\xc5\xee\xe6]E\xc5\xe9\xaa\xe9\xce\xbe\x11?\x04r\xe6\xcb\x9d\xca\xf7\xe5\x9c\xaeC!n|\x15\xd1\xe4C\x17v\xe7\x13\x12gi\xd1\xe3,Lp\xdf.\x14\xa4\xc5O\n\x90\xff\xf6\xff?\x16P\xfd5C\x8b\xff\x9a\x81\xfc?\x16\x9b:\x1e@\xf5\x9eN\x8b\xdf\xd3!\x7f<\xd8\xd4\xa7m\xa8~m\xd2\xe2_\x9b\x90\xff\xb4\xbd\xa9\xcb\n\x9c\x8fF\x8a\x06\xbd\x91\x82\xddee\xd3q\x1b\x85;\xeef\x81\x93\xe9\x01\xe6n\x06\xbb\xdfF\xb7]*\x82{P\xf5T>\xaa\xc2\xce*\xb2v\xf9\xbbd\x8f^3\xa7\xd0\xe1\xe4\xaf\xe5\xefZ\xfb\n\x12\xd9\xabV\xf9Xz\xab\xbc\xdf\xb5 \xed\xfeR\x16\xed\xef\xc5\x83\xbc\xe0\x95\x9d\x17\xf0`\xbfc\x16\xd9\x93\x15\xa9[s?\x11[\xe5o\xa6u\xe2\xe5\xf91\xf5\x83=\x84\\\xe3^3s\x17\xf6Z\x91\xba}j\xc4\x0f\xf1\xa6\xca}VM\xd1\x14\xafl\xa9X\x92\xd8X\xd3e\xe5"\x08qC\xc1Vy\xda\xae\x18K\xf2\x95\x86x\xc4;\xe7K\xcdm\x07?\xaeq\x17\xc1H\xfd\xaf\x7f\x9c\xd3\xfd\x19|\x87\x1f\xec\xdb\x82\x1c\xa4R\x13\xbe\x9bj\xc9@l\xbc\xfe\xa1F\xca\x99\x8b\xdd\xe4\x1c\x9a\xe2\xadTT\xe3\xe0\xf9p\xce\xda\x87\xaa\x0cs\xd3\xbcL,m\xc9q\xfb{\xb7\x14o\x14\xce\xed\x9f\xb7\xe6f\x88\xef?\xf7\x94\x95\xa7\x9d$\xfa\xbbn\xfc\xbf\x9f\xbf\xa2\xcf\x07n\xf3#\xde\xfa\x82\xf5\x05\x92B^0\xc5\xa72W\xbfi\x16\x95\xf7\xcc\xbe\xdb\xfdROw\xa7g\x11\x94<\xabLq{\xef\x0e\xfe\x95\xf4\xcbD#\xb5\xe2\xeff\x81\xcf\x8c&\x05\xb7\xe9\x9b\xa6\x1b}I\xd8\x04\xf3\xa5|\x96\x14\xbc-BA\xc6\x83\xdc\xf9\xbe\xdca.E\xc5\x9a\x82H\xfd\xc2\xb2C\xc0uRs\xb9\x7fQ9\xfc\x1d\xb2\xb2;\xfb\xcd\x8c\x8d\xe7\x83\xb4\x91E7\xcd\xe3\xf8\xb2\xc2xR#\xd5\xc1\x90\x8f\xa6{\xe7\x0fq#\xc5xVR\xe8\xcfh\xf0\xfb\xb3\xbc\xc0\x1c\xa58[j\x9e\xca\xd7\xf48\xcb\x9c\xa0\x87hl|,?6~\x882\x14T\x90\xb6;\xb8\xf3\xb7\xf7\x8a\x8d\x0b\x12CQ\x0f\xd1i[E*\x9a\x88n]\x13q\xdd\xf3o\xdb\xbd\xeb\x8f\xfe?\xfa\x0f' - if name == "apps/webauthn/res/icon_gitlab.toif": - return b'TOIF@\x00@\x00\xac\x03\x00\x00\xed\x92?S\xe2@\x18\xc6w$\xccX\xa6\xb4\xcc\x900CI)]\x18\xb1\xa0\xa3\xa5\xd316t\xb6t!\xb1\xf0\xbe\x81C\x833\xa1\xb8\xa1\xb2\xbeF\xb9\xd8\xf1!0\xc35\x0c\x95)\xb3\xcc\x9c\xcb\x1a\xf2g\xff\x90M\xa26\xbe[]ny\x9e\xf5\xf7{\x01\xf8\x19r\xe6\xbdy\xef\xfb\xf2\xcc\xd3\xaaW\xf5.\x1be\xb5_6P\x9ey\x9a\xf5\xfe\xd9\x1d\xba\xaf\x9ae\xf5\xab&\xca;\xbb\xcbz\xdf}F\xf7\xdd\xe7\xb2\xfa\xc5\xf20\xad\xf2\x0c\x88\xe6aZ\xe5\x19\x10\xcd\xc3\xb4\xca3 \x96\x17\xd1*\xc7\x80h^D\x0b\x1d\xef\xa2h\xff\xd1M<\xef\xb0\x81\x88\x16:\xcec\xd1~\xe71\x9ew\xc8@\x92\x16:\xfaI\x91v\xfd$\x9d\xc77\x90\xa4U\xdc\x80w\x91\xce;\xba\xc9N\xab\xb8\x01\xb1<\x92V\xd5\xab\x170\xa0\x9f\xd4=2\x91\x9dG\xd2*f@4\x8f\xa4U\xcc\x80X\x1e\x9dV~\x03\xa2ytZ\xe8\xcc{y\xfa\xe7=V\x1e\xdd\x00\x9d\x16:W\xf7y\xfa\xaf\xeeYy4\x03,Z\x98\x98r,\xda\xae\x1c\xf3\xf2H\x03lZ\xf9\x0c\xf0\xf3H\x03lZ\xf9\x0c\x88\xe5\xf1h\xe51 \x9a\xf7\xd0\xef\xf8\x1a\xf7Hs\xebw\xf6#\xcd\xf9i\x1d\xff\xa1\x1f\xef\xb7f\x0e4\x02\xde\x91\xfc\xaa\x97\xfdH>?\xcd\x81\xd6,j\xd7ei+m\x8d\x03/\xa8gn\xaf{\xfc$\x03\xa2>]\x0e\xfb_\xfb\xe8\xdf\xef\x87\xfb\x82\xdbu\xd6\xfe\xdb5\x97$\xc4m\xaf\xfd\x88\xfeG\xff\xd6\xe1\xfc\xee\xdf[\xd6\xfe\x7fo\x1c\xf2A\xd8\x15\x1a\xc0\xf4\xf1q\xa1\xcd\xfce\'\xa3\x81\xba\xd7af\xd8\x81\x0b\xa36l\xc0\xecF_\xa4\xad\r\x8b\x1a\xe0\xd1\xb7a\xbc\xcb\xec\xa2~u\x1c\xff&m5X\xcc\x00\x9b\xbe\x06\x93M\xea\x18\xf5\xbb\x9b\xe4\xd7w\x0b\x05\x0c\xb0\xe9\xbb\x01\xd1\xb3I\xd3\xc7g\xc5\xdc\x82\xeb\x83\x06\xae\xd7,\xf3+H6\x99\xdd4}|\x1c\x98\xd7\x00\x8b\xbe\x03i=\xea\x98\xa4\x8f\x8f\x01\xf3\x18`\xd2\x87\xf4\x16w\xc3\xea\x7f?P\xdc\x00\x9d\xbe\x04Y\x1d\xee\xa66`\xf6o\x1da\x034\xfaN\xc0n\xa8\r\xd0\x06\xb2\x18\xb8\x94=\xe4\x19\xa0\xd1\xb7\x03\x97\xc9\xde\xec\x82\xddL\x94\xca\x82~\xc7\x86"\x06h\xf4mF{e1Q@lFC\xfa=\x8dx\xc1\x0b\xd3\xc0\x0bA_c\xb4\x8f\x86\x80\x98v\x8b\xee\xc1\xcdh\x80\xa4\xbf\n\xe8\xdc\xdb-@\x1d]\xb6f\xe4\xfd\x15\xb1\x05\xd7t\xfa^\xda\xfc\x8a\xf2\xd7[3]\x06\x9c\xa9\r\xc8\xdf80\x8b\x814}\xda\xde\xa1}?4\xcb\xa6Jl\xa3\x91x\x81\x16\xd0\xfa\xb5$%\xa2]],\x9b \xd3\xe8\xb2:&\xde\x9ex\xc19\xd1~\x9e\xa0/\x91\xedc>\xf7\xf4\x98\xdd\xf46:q\x03k\x82\xfe:\xfa_\'Ho\x9c\xd9\x05\xc23Q*\t\x0fnl\x0fI\x03\x1as\xef*\x8b\x89\x02r\xceh\x18O\xb2!\xcb@\x9c\xbe\x9dh\x1f\rA\xa1i\xb7\xe2\x1e\xb4\xfd\x0b\xa6\t\x03\xd3=}\r\xc6\xb9\xb7[\xa0\xf0\xe8\xb25\x8b2Wa\x8f\x9f\xa0\xef\xe3\xaf\xab\x98yk&\xb6q\xbc\xa9\r\xf6\xfd\xfb-8\'\xe8\xc7\xcd\xd7\x06\xa0\xd4Y6\xd5E\xb8\x87i\x03!}\x17\x86\x1b\xb7l\x82\xd2G\x97\xd51\xce7`\xd2\x80\x84\xe9\x7f\xb4\xab\xe3\xf2\xb8\xa7\xe7\xb5\xff\xb1\x8d\xbb\x17\xd4w\xed\xf5\x1d}\t\xe2\x8d3\xbb\xe0Sg\xa2Tv\x1e\x9c\xbd\x01D\xdf\t0\xf7\x89\x02\xbe`\xe6\xbf\xf0\x1eJ>\xa6\x8f\xf7n4\x04_6\xed\x96\xbb\xb1!2\x80\xe8\xdb\xd0\xdd\xb4[\xe0KG\x97\xff\xfe\xd1\xe0t=]k\xd0\x9a}\xde\xc6\xf1\xa660|\xc3\xaf\r\xc0\xb7\xcdS\xe3\xa9\x01~\xa6\xc0\xfc\x07' - if name == "apps/webauthn/res/icon_google.toif": - return b'TOIF@\x00@\x00#\x04\x00\x00\xc5\xd2?h\x1bg\x14\x00\xf0\x07N\xe5O\x1eZIC\x90e\x0c\x1f\xba \x84z\xc56\xd2\xb5\tr\xcd\xd9\\\x8arPP\x86R\x0f\r\xc8\x8aIC \x83\x03\x85\x88.>,\xa7U\x02\x85\x16JI\'\xc9\xb6\x06\x81\x97\x0e\x1d\x142HJ\x8e\xa2\xa1CJ;\xa8\xb4 +\xd5\x19%\xcdP\x88\xc6\x83\xdcY8\xb6"}\xba\xff\xd5{\xe3}\xf7\xfd\xde\xf7\xde\x030\x1a,\xae%\xa9\xcc\xfa\xfdbY\xacK\r\xd7\x9fRCz"V\xb3%.\xf74%\xcc\x83\x83Q`\xb9\x9cXw=\x1b\x99\xadl\xa9\x95\xc2\x1e{e\xd6Oe\xa4\'\x1ar_\x15\xeb\x05\x81\xb5\xc7^\xc6\xdc\xb7\xae\x96\x01\xfbu\x16\xcb;\tk6F\x94`\xce>\xce\xed\xd2Z\xd8\xac.\xb0\x9a\xd3\xd65\x8b\x89\r3\xba\xf2\xf2gve\xb1\xcc\xfa\r\xf5\xdd\x93-\xd9\xa7\xab)5\x84\xf3\xfa\xb7]\xac\xda\xab\xabS\xa8%u\xeb\xf5\xf1\xe9\xd83\xce\xb7\x03\x14\x7f\x1a\xa7\xce\xe5\xc6\xa9\xd7\x92zo\x15\xab\\\xae\xb6*\x9c_\xc6\xac\x7f\x19\xaf\xcd\xef$&6\xb2%W\xcb\x8a\x8e=RC[\x96\x1a\x94\xb0\x16&\xdc\x80j\xab\xc5\xb29\x1d`\xfd\xbev/\'2\x18i\xdd\xb3\x93\x10\xeb\xc6ua^K/\x96\x97\xb1\xce\xcb\x10%\x18\xd3\x01\xf64\xf6\x9e\xcb\x81\xa1`\xfdFN\xa7.\x04\xbbwF\xe8T\x06\x9c\x8d\xfd\xa6,\xc8\x17\x9f\x13\xf4\x9c\xb3x\xde_\xe96e5\xa9\x7fC\x03z\xb6\xe4\xf0\xdb!\x7f\xab\xa7\xab\xd9z\xd9?\x07\xa9al\x92fb\xf3\xf1\x89\xaf\xcc\xa1\xfb\xd6\xa99\xb4R\x8e\xeb\x9e\x8a\xdc\xec\xcb\x8a\xfc\xcf\xf3\xde\x1c\xc4:8\x1e\xcd\xcbMy0k/\xaf\xfd/\xaf\x07\xc0\xf7\x86\xf9My\xad+\xb60r\xdeg\x1f\x0e\xf7\x9b2\xeck\xff-\xa1\xdbQ\xb39\x17\x19\xae\xab)\x06\xcc\xe8\x1bA\xb2\xfeA\xb4\xff\xec$M\xd6\xd5\x0c\xcd`\x83zr\x84\xbe\xc4\xdd\xf0\xf6\x9f\x0e\xbaG\xfb<\x13\xa6\xabH\xaf\x9d\x86nd\x94\xfec|\xf0\x9fk\x94V\x05S\xca\x1cXM\x1b\xc3\x19\xdf7\xb1\xcf\xb8Q\xfe\xdd\xe9\xc1\xff\x04\xe8\xc4\xb4*\xe0\x99N\xac\x1d8 \xf6\xa1\x00\xd9@\xe9h\x924\x93Y$\xe9\x9fF\x87\xff\xfd\xc8\xab\xed\xf7\xf2:\x1d\xa2\xce\xf8V\xdc\x07\xa8\xa0\x98-\x94v\x9f\xf3\xb9f\x12t\xa4\xef\xd4\xcfC+\x98\xe5Db\xfd!Jo\x05\xfa\xf2\xbf\xc5+\x03\xfe\\p\xd4\xec\x10mo\x05\xef1_\xf6\xe9\xefGGoO\n\xae\xdb\\\xc1\xbb\xcc\x0f\xaf\xe7\xf0 \x9e\xd6\xdc\xdf\x02\xe4l\xae\x80g~;\xaa\xe0\xaf\xf8\x1e\x02\x1d!\xd8\xde\x03\x9ey\x9b\x99^\x14u\xe9\xbd)\x1c\xda\xbc\x899\xba\xaa[\xefE;\x10\xb1M?\xa4R`<\xd2\xee\x92\rs\x98b\xb6\x03`2X\xa5\x0bS\x96\xf4I\xfa\x00\x81\xa5\xa8\xa2\x8f(s\x93H\xd0\xbb^\xb0%\n \x06\xea1\xfdr\x84\xf9\x85\xda\xb2\xc9>\x0e\x0c[\xee\x10\xa5\xb5\x11S\xca\xaeq>\x01\x9c\x8b\x02<\xf2\x8a\x81\xaf\xa80\x9d\xa3\xebt\'\xd6\x89\xd5\xe9\x04}H\x85f8\xdf\x8a\x1b\x1b\xbc\xed\x15' - if name == "apps/webauthn/res/icon_invity.toif": - return b'TOIF@\x00@\x00|\x00\x00\x00\xed\xd2\xc9\r\x80 \x10@Q\x18\xf0H\xb7\xd6@\x05\xb69\x15x\xf1(.\t\xb2\xa8\x89\xfe?W\x99gPc\x88\x88\x88Z\nn\x1e\xce\'8||\xfc\xfa\xd2m*\xa5o\xf2-?\xbf\r\x1f\x1f\xbf\xdeW\xd9\x9e\xcfMo\x7f\x7f\x1e\xff_\xfe\xdb\xff_\xb4\xc1\x8d\xfe\xaa~\xdc\xde\xea\x1f+\xdb\x86\x8f\x8f\x7f\x97\xaf\x92{Z\xa5\xb7?\xf9u[\xb4\xcf\xfbe\xf5\xbf\x7f\xfc\x92\xf8\xfe\xf8\xe9\xa8\x18""\xa2M\x0b' - if name == "apps/webauthn/res/icon_keeper.toif": - return b'TOIF@\x00@\x00\xd7\x05\x00\x00\xdd\x92?h\x1bI\x14\xc6\xa78\xb8-Rl\x91B\xe5\x18r\xe0\xedN\xe5v+q)V\x90\xc2[n5\xde\xc4\x07R*\xcb\xa4\xb1\x9a!\n*,\x93\xe2\x14\xdc(\xc5\x9c\xad\xb8\x08\x9b\x14A\xee\xb6\x18\x88\xc0\x85-\xae8T\n\x06\x82\xce\x85\x11)\x0e\xa547\xb3\x7f\xa4\x95\xb4+\x8d\x1c\xf9\xcc\xddL5\xb3o\xf6\xf7\xde\xf7}\x00\xac\xbe>g\x8a\x85\x8f{\xbb\x87\x9f\xde\xfc\xf6\xe6\xe7WO\x9eo\xe6\x8fU\xf0/\xad\x1dM}\xf5\xe2\xc3\xd1\xd9\xd1\xd9\xc3w\xbb\x87_\xab_+[\xbc\x8f?\xdf\xbe\xde\xfb\x92\xb9k\xf6\xb6\xf2\xe4y\xc0\xde=\xbcv\x8a\x85\xe9\xbdY\xd8\xd1r\xca\xdd\xd1\x8fU\xf5U0\xf7\x83\xa7\xc5\xc2{\xeb\xc2\xbe\xb6/\xf8>\xb0&=\\\xe9w\xe5\xc4K\xf5\x8f\x8a\xa0\x7fzs\xc0\xc9\x0f\x9e\xc6\xb9\xa7\xd9s}3\x1f\xa9p\x17>@pmG\xf4k\xe7\xc2V\xf2?ll+\x90\xdf\xbfT\x1fm\xech\xa7\xd9\x1a\xef\xc1\xf4;0\x0b\x9f\xd7\xde\xc1N\xf6\xe1\xbb\xa3\xb3\xbf\xdf\x1eX\x0f\x9e\xbe\xb7\x1em\xcc\xabS\xcb\x9e\xf2\x1d\xa8\xb0\x99\xdf^\xab\x0b9\xe5\xf5\xde\xd1\xd9\x8b\x0f\x17\xbe\xdfi\xfa>\xda8\xd7O\xb3J>\xc8\x01\\#\xff\xb1V,\xbc\xf8\xf0q\xaf\xc8\xa7\x7f\xac\xa5\xd7}\x81\xa2\x83@\x83y\x8dn\xbf\xae\xf8_w\x0f\x8b\x85\x0b\xbbX\x80K:\x15.\x84\x1e(\xeb\xa1o+J\xfeJ\x17\xec\xc5\xd3\x07\xeb\\\xaf\x85\n,\xaf\x95[_2\xe7\xba\xffG\x9e\xfc\xdc\xd2\x99\x8e\xd5s\xfdJ\x17|%\x0f\xd7\xe4\xfei\xd6\xe4\xff;\xb0\x0e,\x99\xfaZ\xe8\x80Y8V\xd7\xc5/\xfa|\xb3 \xa7WP\xbf\xae\x0cN\xf8E)>\xe4\x19\x08\xf8\xe7\xfa}\xf0\x85\x03\x01\x7f3\x7f\x1f\xfa\x03pz\'\xfc\xf7\xff\x19~\xed\x9e\xf9;Z\xc4\x87\xff\x03\xfe\x8eV\xcb\x9a+\xf1\x1f\x87|\xb3\x90Sn\xcf5\x14\xc3\x01-\xda\xcc\xbb\xbf\xb8?\xb5\x06\x8d\xbf\x1a\xbfV\xb0\x0e4\xac\x1a\xca]\xf3\xb1\xca\xca\xb0\x87\x07\xe0\x06&n\xf0\x8d\x7f\xeb\x12\x0f\xba\xb8\x0e*\xb8\xc4L\xa4S\x08\x95u\xf0\x91jTQ\x1f\x0f\xc4\x867\xabm:\xe2]{\xdb\xee3w\xd0\xacU\x7f|\xfe\xbbz\x8b\xd9s\x01\xfb6\xfc\x84\x8e\x86\xa0k\xb4Q\x9d\x94H\xce\x90R\x83\xac\x95\x1f\xdfH\x97\xe1SS\x9e\xcfF\xa4\x83\x1bl\x9f\x96i\x19V\xd9\t\xe8\x82\x05\xd5x\xad|\xec\x01k^Q\x9e\x1e\x9bvR^\xe4d\xf8L\x86\xdf\x9b\xfc\x8bd\xa8i\xd8\xc8\x816\xd6\x91\x1a\xcd`\xf4\xe7_\x11)>\xb0\x96\xf1A+\x98\x1b\xa9l\x1f\xf6f\x1c\xb9$%\xf1\xd5P\xa0;\x97ES\x06\x0f\x97\xf0i3\xa8\xc3\x0e\x1d\xa6\xe8<\x00\x96\xafBs\xa679\xbe\xbd\x88\x8f\xda~\x8d\x02\xdc%Y\xaf\xf3*\x80\xbd)\xdd,\x19\xbe\xe1\xa4\xf3\xf1@8\x0c\x01jO)2$\x1e\xf6P\x7f\xbe\x03\x92a\xa3U\xf9h\x01\x1f\xfa\x7f 1]\xd1\x00\xda0JbN\xa4\x8etH3P\x87m\xf1\xcbJ\xec\xb5-\xc3\xc7\xa5\x88Nf\xf8\xf42\xc8v\xdc\x8d \xf1\x90O\xca=\x018c\xf4\x85\xcb0\xeb\x7f\xeds\x07T0\xae6\xa4\xf8$\x95\xef\xcf\x03&\x89\xe7\xee\xfa/XY$\x91\x8d@\x85\xbb\x97\xe5D\xae\xe1$q\x93\x0c G\x86O\xcb\xc9|~\x12,s\x92\x05\xec\xcfN\x1a\xb1|\x94B\r\xc3;\xee\x04\x80\xd5\xd5\xf8,\x8d\xdf\xf4?\xbb\xd3jr\xf7\xa7\xf2\x19\xfe\xa3\x13:\xd6\xe1\x8a\xd8\xb3\xdd-\xe1\xef\'\xf3\x83\xf4\x92aH\xef\xfbte6\xa3\x0c\x8a\xdbq\xea{\xf1\xbc\x10)>\xa8$\xf3\xc5\x9f)\x1cwS\x99\x9f\xde\xef+\xcb\xaf\xb5qU7\xee\x18-\xcb\xe0a5\xe2\xc7g\xe3)\x16\xf9\xce\x8d\xbd\xd4\xfd\xe2\xd6,\x1fh\\qg|v\xf9\x9b\xd2x\x82}\x19\xbe\x91\xc8\'C\xdf\x9b\xad1\xdf\xcf\x1e\xec\xcd\xd0\xbf\xf9]6\xe2\x8e\xd3\xe6\xb4fK\xf9\xf5$\xfd\x91\xef7\xb0"5\xc2\x9c\xcf\xb8\x8f=qK\xc3\xf4\xb1\x11V\xa7j\xaa2|4\xe1\x0fc\xf3\xfb\xc9\x9ed\t&\xf2EF!\x88\xd2\x87\xeaS/n\x0c)>n$\xf1\xb9\xb2 \x9e,\xe6\x1f\xb17\x95\xd0K\x10\xaf\xe9\x19\xdc\x0bv\x19\xe3\xd7e\xf8$\x91\xcf\xd3\x9b\xe1\xb3)\xd1l\xc2Y\xee\x95\x1d\xfb>\x0cz\x82\xfe\x9d\xd1\'\x99x\xf6"=\x96/\xdaL\xe6\x0bm\xb9;\xed\xf0\xd4\r\xcb[\xe1l}\x98\ro4v\x89\x1b"\x9fH\xe7\xaa\xc5\xd3\xd1\x90\xe1\xb3\x14>mN\xa6\xf3\xcf\xe5\xb0_\x93\x96\xd9\x16T\xf8\xf0\xad)\x1ds\x91V\xe3\x0cI\xf1A+\x85?\x14~B\xc0\'\x8d\xf4tb\xb4\x0c\x11Y\xb0\xc6\xa7F\xa8K\x9b6\xd1 >\xc1\xd2\xe5&\xf3\xa3\x89\xb1\x1eS\xd43\x1c\xac3\x137\xa2Yi\xc7pA7\xee8O\xc5\xcd*|\x98\xce\x1f\x8aL\xcd\xa6*}\x0b? \xa0\x9d\xe8\xccNd\xf8F*\x9f;\xd8\x81\x8a\xdf\xa35\xeb\xed\x1c;T\x8b6\xa7\xfbY\xbeP;\x9d\xcf\xfdt\x83\x0e(\x8c\x92\x9f\xc4\x06-\xae:\x9f\x9d\x9dL}qe\xf8\xd8[\xc4\x17\x0esv\x982Z\x16\xdd\x82\x90\xcaF\xa0\xc7N\x90\x83U\xf1\x95\xc1\x89\xf2Q\xef\xab\xf1\xe90y>6beC\x89%F\xc1*V\xe37$\xc3\x93\xf7mN\xbb\xb6\x0c\x9f,\xe5\x07Y\xc4\r\x92\x8b3\xfdN\x00\xd0\xb0\x83\xda \xf1\r\x92\xe2\xd3\x8e\x0c?r\x1a\xf6\xb0\x07\\\xee\xb3\x8b=\xd8[\x9cJ^)\xb1\xd8\xa5<\x7f\x95\x8d\x06\xb4|\x1f|24\xda\x9c\xac\x01\xc9EJh\r|<@\x9eQ\x87\x16\x83`\xf5\xa5\xa1\xf6\xea|2\xa4\x1d\xd0\x82U\xc3F:R\xc0w.\xec\x18\xfdE|<\x00]\xd4\xa6MXE\x0e\xcd\x01\xcd\xf8n\xe2\xec\xa2\x19\xd0B}\xd8#\x1d\xa3\xcdNp\x1dTp\tX\x84\xb3\xb0\n\xd7F\xf9\x07' - if name == "apps/webauthn/res/icon_kraken.toif": - return b"TOIF@\x00@\x00\x82\x02\x00\x00\xed\x921\xb6\xa20\x14\x86c\x97FO\xca\x1c\xab@%\xcd@\xca\x94\x0c6\xe9\x8ckx\xda\x0f\xeeA\x16\xa0\xee\x01\x160\xcc\x1ad\xfa\xc1=\xe8\x02\x94j&\xcf\xe3(IP\xdfS\xe1\xcc\x19\xfe\xdb\xc1\x9f\xfb\xdd\xfc\xb9\x00\xb4j\xf5/)\x801\xde\r<\x1a\xb2\xa5/k\xe1\x87\xcc\xa5[\x12a\x1b\xbe\x92\xdb\x011\xf6h\xce\xfb\xe3\xeaJ\xb9K#\xf4|\xf6\x04y\xf4 \xae\x91/k\xc3{N\xf0\xb4,\xa6h\xc6\xee%\x9f\xab\x10\x82\r\xe1\xe3o\xfd\xed\x13\xec\xf3\x0c.\xb5\x1e\xa0\xff$\xf7g^U9\x8f\xf1g\xd86\x08\xd9\xa3\xecS\xb9\xb4\xf3A\xfa\x1b\xfa\xc1\x9fE\x97\xb5\xf2\xed\x0f\xec\xc2\x1c=\x9e\xbb\xfe\x0e\xf7nc\x84\x9fO\x97\xb5\xb9k\x82W\xdc\xfd\x9cApc\x82!\xdc\xbf\x8c.+\xe5\xf6\x15\xba\x05R\xfeJ\xba\xac\x19\xab\xe6\x0b\xfaj\xba\xac\xdd\xc0LOp\x1d\xf4\xfe\xb8\x10\x13d\xca~\xc3\xeb\xe1\xf7\xc7+_\xe7\xbb\xb4.\xba\xac\x8c\x94\xe9\x01,D\x9d\xfc\r\xb7J|\xaf\xd6\xdb\xab[\x18\xc0\x83\xa8\x9b\x9f\xf3\xce_~\xd7\xa9\x9b.+\xc1'~\xce\x9b\xe0/\xfc#}\x8e\x9a\xa0\xcb\xb2\xa1\xe4\xbb\xb4)~F$?\xe5M\xf1C\x06\xc0W\xd8\x14\xbd?\xdep\x00b\xdc\x1c\xbf?\x1e\xc2\xae\xd3$?\xc1\x82\x99\xff\xa4\xd9A\x0c!\xb8\xd0\xc27u^\x93K\x8f\xf9\x15]z\xe9\x89\xb0\x9e\xa6)\xbd\x9e\x03J\x9a \xbd\xf3\xd2\x07\x8aL\xf7\xb0\x15\x8f\x9e\xe5w\xc3\xdd\xa6\xe8v\xef\xdd@\xf5xT\xbf\x9b\xea\xc9\x88\xbee:\xbf\x9c\xbe\xd4R\x9b2#\xaaG\xdf\x81\x11U=\x11\xd67M\xe7\x07w\xf0\xd7\x1a_\xdf\x00\xaf\xe5\xb7\xfc\x96\xdf\xf2[~\xcbo\x80?\xba\x83\xff\x8b\xef\x95\t\na\xab\xc7@\xc8\xd4s\x11V=[\xa2zz\x8e\xea\x99 \xd5\xb3\xf4GT\xfd\xa2\xe1A\xa6\xf4>\x08K\xf3\x0c\xa1\xda{\x8e\xf4Nj\xde\xbbA\x007\xe2\xb2\xf3\xc4p\n\x80U\xe9\x05\xb6\xc4\xe4qK7\x11\xcc\xe4Ip9}\xeb=\x95\x94\x9f\xb6A\xcf\xf5(\x1b\xce\xd8iB3]\xea\x0b-\xc4\xe9\xed;\x15\x9e\x8c\xec\xc5)\xe9\xf3\xa6M\xd1\x9a\xcc\x11\xb8\xaa7\x98\xe0\x18\xdbW=6\x8cq\x82\xf5\r\xbe\x94\xf5g\x0f\x13<\xb9Ak\xd5\xea\x7f\xd0o" - if name == "apps/webauthn/res/icon_login.gov.toif": - return b'TOIF@\x00@\x00\x7f\x02\x00\x00\xed\xd2\xbfk\x13a\x18\xc0\xf1g\xff\x7f\xa3\xf7\xce\xd7\xfb3z\xf1\xa1\xbd\xbe\x85\xd4*\x0b\xdf\xe8\x15M;\xbf\xa5\x91\xe8^\xf8\xa2>g\xd9\xc1F\x87L\xf7\xc27z\x8f-\x7f`\x9e\xa9\xff\xc1\xf2\xfdo\xfa\xec|N\xefZ\xb6\xff\xe5\x90\x9d\x7f\xe1\xab\xf5\xfa\xba\xe8\xea:+?>D6]\xab\xb1\xf2_\x8f\xec\xfc\x87\x1a+\xbfm\xda\xf9\x1b\x1dV\xfee;\x1e\x1d}c\xe3\x8b\xba-\x8fn\x1c\xb1\xf1\x97\x07\x08\xd3\xad*\x0b\x7f}\x88\xf3\xf35\x16\xbe9\xc2\xf9-\x8d\x85?w\x8c\xf3/\xf6Y\xf8\x1f\xf5\x83\xc1\xa7\xe1\xa3\x919\xbam\x16\xcd\x85\xfe\xf5\xbe\xd6\x99\xef\xbc\xd2\xf2\xb5%\x97\xfb\x17\x02$\xfe\xd9I\xef\x91\xdd\xfc\xe9\xd4%\x98PY\xa1\xedg\x13\x93\xfch\x88\xb2/\x01\x07\x13\x93\x0b4\xfdd\x18\xa6\xc5\xabMr?\xe6\xc8/e\x80\xa0h\x98\xdco4\xc9\xf5\x1d\tx \xea\xde\n\r?"\x02q\xf2[\xaf\xfdd\x0c\x9c\xc4\x95+^\xfa\xd9Ep\x1a\xd7P\xbc\xf2\xb3i\x98%\xae\xacx\xe1\xcf\xf0\xed\x7f^ Kn\xfdl\x02\\\x95{\xee\xc6O\x86\xc1u\xa9E\xa3=\x8b\xbf#ED\xf0\xa4H\xb0\xb1\xed\xd4/e\x80\x07\xef\xe2\xe5\x82\x13\xffn\x1cE9\xf7\x05" - if name == "apps/webauthn/res/icon_mojeid.toif": - return b'TOIF@\x00@\x00&\x05\x00\x00\xed\xd2?L\x1bW\x1c\x07\xf0g\x0b\xa9\x1e\xda\xca\x03R=dxg\x18@\xaa\xaaX\xca\x80%W\xf2Y\xca\x10$\x06,e\xc0\x12\x92M\x94\x01\x98\x001\x10\x0b\xe9\xf8#*%\xa8\x03\xa0\x0c\x18\xa13\x84\x0e\x082TN\x874A6Pg\xa0\xb8\x03\xc2\x19\x8c\x8d\x81\xa2\x1b\xc0\xb9!:<\xa4\xf8\xea\xe7\xf3q\x7f\xde\xb39\x12\xd3.\xfe\xfe\xc6\xfb\xbd\xfb\xbc??\x00\xea\xa9\xa7\x9e\xff7\x10\xb04\x08\xc2 \xf4\xfa\xad\xff\xbd\xeew2\xe9@\x81.\x95\xe9\x82\xed#\xf5|\\\xe8\xe6\x8d\xd6Hbx\x9d\x0fZ<\xb4\xc5\x88\xce\xd2\xd4\x05]P*P\x00A\xbckx]\xbcq6\xf2\xef\xdf\xaez\xaf\xbby&\xad\xd6\xa5\x82\xad\xb5\xf0\xa5\xb8\xb8\x9f\x87\xaa\xdc\x84\x17\xd7\xe9B4T;\x1f\xe5S\xa6\xd2=\xf8\x9f\x91|\xf7~m}\x94\x87\xeb\xe3\x84\xd9fgI>\x93\xae\xbd\x8fn\xe11\xf6\xael\x1f\xc9\xf7Gn\xc3\x17\xc5\xc5\xbc\xc5\xa3\xf3m\x14\xa6\x07\nn\xdf\xed\xf8\xa2\xb8\x91\xe7\x9c\xda?\xc3\xa0\xde\xcfn\xe3\xefT+_\x14G\xf9\xfb\x9aW\x80\x9a\x19\x08\x14u\xd6f\xd4?\xc4\xcaHR\x99\x80E\xf7\n4x\xe1\xde\xf7\xa7\xfd\x11\x7f\x0f\x04\xa4\x90\xfc3\x1f\xde\xb7d}\xdc\xda\xdb~wr>Ym\x07{\x0b\xe0\x861\xea+\xb1x\xde\xbf\xad\xbc\x83\x16\xcfm\xfb(\xab\xden\x9e\xec\xcf\'\xbf\xd4?\x14w|\xd7\xafk\xa0R\x19\xf2\x0e\xe4\xddg\xa1\xdf\xa9-\xb7\xe3:\x1fM\xdaF\xde\x88\x8f&\x82\xbc\x83\x91D\xb9\xe1\x05\x9a{u\xb1\xa7\xd5|d/\xe6\x17\xf9Q~\xd5k\xec\xee\x9a\xa9\xc5\xfc\x94Ie~O\xf6\xb6\xab\x1aZ\xd9>\xe6\x8d\xe9B\xfb\'\xb8\x0f\x83Q\x88>\xef\xf8H7\xd0@\x91\xfc0\xc1o\\\x19\xe5\x7f,\x9eZ\xb6\xe7\x93O\x12\x0fJ~\x14f\x07\xa9}J\xf7\x0f\xd3nt0\x0b\x95\xf5\x10\x8c\xf2\xb8_\x9a`C\xfe\xc7\x05t\xe3\xc8~]\xb2\xbb\x12\xcexo{\xb63\xbcMa\xab\xdd\x13\xa0\xb5\xda\x04+\xb9;\x89\xfbTa\x8c\xe0\xef\xcd\xa52\xa9\xa4b\x7f\x17\xff!s"`;\xdfv\xfb`\x85\x89|9\x84\xfb\xf7V\xf4>U0\x15\x02\x04\x7ff\x0e\xd9#e\xfb\x9b\x8c\xbd\xd8Ii\xd6\xb9#~\'\xa8\x92\xdev\xdc\x7f\xfeJ\xedS%\x9d\xec\x0fL#\xbb\xadh\x7f[\xb6\xd5\xbe?\xe2v\x80k\xc29q\xdf\x16W|Y?\xba\xa4\t~\xe7t[\xdc\x16\xb7&\x9b.\xa8\x02\xa5\xf1M\xbb\x8c\x13\x18Hu_\xd17\x89\xbeuR\xf8\xe3\xab\x0fj\x1bU,\xc7\xf4\x00\x83y\xea\xad|\xffj}\xf3\xd2N\xf0\x7fZ\xb3\x17\xf4:\xb5\xc6X\x81\xe1\xf0A\xdco,\xcd\x9fd\xcb\xfa\x92`NkWFax[o\x87s\xc0\x0bn\x94\xdf^\xe1>\x1f\x04\xe0hY\xb2e}\\\xe7g;c9\xbd>\xf6\xe6&\'G\xa1-\x8by\xdc_\xf5J\xbeZ\x1f\x17\x8e\x0f\x94up\xc2\x84\xdd;\x9c\x80\xc5\xff\xed-\xec\xf8\x8c\xfb\x1d\xfd"![6\x00b\xcbe\xbb\xac\xf7\x08\x9be\xdfm\xa1#z\xfbH\xc8v\x02\xd0L\xbdN\x8a\xe2\x99a\x1f\x82T\x06\xd7\xe7\x93\xe8[lY\xab\xcb>c\x8d\xfd\x89\xdd\xfb)t\x00\xd0\xe2\x91\xee\xd2\xb8\xffW\x90t\xfa\xcei\xd9_\x12d\xddS\xac\xa5\x034st\x1a\x9b\xf7t\x14\x02\xf0\xf5\xa3\xc3\xf2z\xa3\xfe\x94\xe3\x90\xc4\x8b\x8f[\xd1\xd7pH99\xd2\xed\xc2\xf8A\xb6%p\xaa\xd7M\xbbL\xf1\xb5x\xd5I\x8c\xf9\xcd\x94\x8b#\xe9\xb6\xb8\xf4=\x1cRt\xbb\xd0$\x98s\xc7\x7f\x93t\xbf\x05\x00\xeb\xa4\xfa\x0fF\xfc)\x07Y\x17\xc5\xdev\xa9c,\xa4\xd5\xed\xc2\xd1%6\xf3\xfb\x8cU{vc\xfe\xcb\xa1\x8d\xdf:\xef\x11\xd4\xba\xa9\xf4\xf2\x8cS\xe9\xaa\x85\xdf\x95h\xa0\x00\x16*\xb4\xa5\xbb{\xc9\x0f\xdf\x1fI\xec\xf8@\x95\x98g\x8f\xb0\xd3\x9b\xd6\xf4]\x9f\xe3\xdf\xe1~\x9d\xab|n9\xb1\'\xe1m}\xb16}\xd7\xcc\xdc\xeb\xa4\xb1\xeaJ<\x7f50}\xe6k\xa6@=\xf5\xd4SO\x95\xfc\x0b' - if name == "apps/webauthn/res/icon_namecheap.toif": - return b'TOIF@\x00@\x00W\x03\x00\x00\xed\x92\xbfO\xdb@\x1c\xc5\x0f\x1a$\xc6\x8c\x1d\xaf@%\xc6\x8c\xed\x16\x84\x8a\xd2\x8d\xfe\x07NS(\x1d\xbb\x95-n~T]\xd9\xcc\xe4\x04\x10\xa2\x1b\xedD\xbb8!m%w*Lf\x8a\xafHH\xc7v\xe9de\xa8/n\xc8\xf9\xfc=\xdb\t\xab\xdf[\xcf\xf7>\xef\x9d\x11\xca\x94)S\xa6L\x992\x85\xb5\xf4\xfc{{\xfbWS\xf2\xc6y}\xcf-\xccp\xdd"*[\xa7\xc8\xb6\xce\x8az\x11\'\x1d\xc6\xf9\xdcI\xc3\xf90\xd8\x19B.\r\x1b{\xd3\x85k\xab\xf8\xa2\xea\x8d\xad1\xf4&\x9e\xb5rZw\xb6|\x1f+\x08v\x86G\xfb\xe9\xd3\x8b\xf9"\x99\xa4\x07Fe\xf5\xf9\xf9\xb7\xd7n\xcf=t\xea\xce\xc2Ui\xa8"XZK\x9b_5\xe4t\xbe\x01\xce\xab\xb6\x7fp\xb5Br$`x}\xab\xca\xdf8O\xb9=\x8e\xa6\x8f\x16P\xbc\xc1\xdan\x85\xae\xd3u\x120<&\xf7]\x80\xb77!\x02\x03n\xff\x92\xd4h\x8d\x8e\x18\xe8\xb2\xcf\xd0\x1c\xdcg\x81"\xe6\xe9\x81\xc3\xf9f\x0b:_\xdem\xb36;\xa0\x13\x86W\xf7Z\xc04LO\xb4\xf0\x07\xe8P\xfb6\xe9\xb2.\x13\x19*t\xf6\x054l\xb2p\xbe@P\x88\x9e\xd7w\t#\xac\xfb\xdfc\x86C:\xeb\x02r\xfb1\x81\xe9\xf5\xcf\x80\xb7\xcaw(\xcf\x8f2\xcc\xb6\x00ooy&\xec\'\xc0\x9f\xaa\xbb\x9e\xcb\\\x16e\xa8\xd8\xa5\xbf\xd3/`\x1a\x96\x17XNW\xb5\xf7\xf3=\x88\xc1\xdd\xac\xefM\xbb\x80\x86-6\xce\x8f0\xa8\xdaO,0\xbc\xb4\xfd?\xe3\xe1\xb4\x0bX\x86\x98.2$\xb4\x0fQ\xf0|\xf4\x82\x9f\x88Y\xe0\x1b\xdc\xbe\xef\xf5=\x90!\xb9\xbd\xe0\xb2\x1d\x9c\x98n\x01\xcb\xe0\xe9\x81\xc3\xf9p{\x8b\xf2\x93P~\xd0\x9e\xab\x91z\x01\rw\x98\xeb\xf5=\x88\x01n?9\t\xb7\x0f\x16X\x1d\xaa\x08\xe6\xd7\xc2\xed\xc7\xdf\xcb\x0c\xe8+\xd4\xde\xa4\xa3e\xbc(\xc5\xa4}\xfa\x054\xec\xff\xbdB\x07\x91\x00n/\xbcN\x88A\xb3\xc3\'\xd3-\xe0\x1a\xd1W\x0cnD\x9f\xc1\xf6L\xfaC&\xde\x94O\'/ \xb7\x17\x19\xa0\xf6\x9anyQ\xf3t\xb9}\xba\x05:\x06a.\x83\x18\xe0\xf6Ufz\xdc\x00\xc3&\x02\x14\xbf\x80\x86\xbb\x940\xee(\x03\xd8\xfec\x90\x1ee(\xda\x08T\xec\x02O;F\x97q\x13&S<\x8am\x1fe\x80\xdb\x8f\x16\xd8W\xe57/\xdb\xb4\xcd\xba\x0c``\xc9\xedE\x06U\xfb\xd1\x02X\xbd@\xee\xf6\xc0\'\x882\xcc\x81\xed5V\xf5\x14\x04%\x14\xa3#\xe5\x027\x83\x1a\xadQ\x9fA\xda\xc1,\x00\xed\xf5\xaa\x17XN\xc76\x8a\x95\x8eK\xca\x05z\xb4\xe2\x9bSL\x18\xe6>A\xb7\xe0\xabq\xbe\xcc\x10\xdf>~\x81\x9f\x83e\xb2~\xc7\x10\xbcF\xb5\x00\xdd!\xa6\x8b\x14I\xed\x93\x168"+Dd \'\xf0\x1d\xfc\xf5\xa1\xfc\xe4\xf6I\x0b\\\xbb9\x92\xbbc\x80\xdb#\xd4\xef@\x0b \x1b\xa5R\xdc\x02[\xa4\xe7\x8e\x19\xde\xb7T7\xf45(?]\xfb\xa4\x05\xea\xce\xa1\xd3s\xae}\n\xad\xa0\xbe\xa1\x7f\x16\xd9\xbf\x85RK\xcb\xdf\xfcQ\x11\xfc [\x0eg\x98\xd7\xe3n\xc0\x8b\xd6\xa9\x94\xbe\x88\xa6P\x0b\x1f_\xc2\xf9%\x9f\xe0\xd9o\xf2.\xf9\x0e\xab\xe4\xa7^ \xdbl\x99E4\xb50:(/|i^\x1eK\xde>o\xec\xb5\n(S\xa6L\x992e\xca$\xe9\x1f' - if name == "apps/webauthn/res/icon_proton.toif": - return b'TOIF@\x00@\x00?\x02\x00\x00\xed\xd2\xb1n\xd3@\x18\xc0\xf1/\x9b\x17\x88\xd5.\'\xd5\x91L\xba\x98\xa9\xb6\xe8b\x89\x9cd\xc1\xe2\xf1$:\xb8\x03\'\x9dp\x81{\x80>B\x1cv"x\x84\xf8\x05\x1a\xf5\x15\xd2\x851\xcd3\xa4\x12\x1dI\x18 \xaaP\x1ar\xd7\xea\x1c\xfb;\t\xf1\xfdw\xff\xec\xcf\x1f\xc0z\xe6\x01\x97\xd5\xa3\xd2\x13c\xc6\xe2\x89\xff\x0c\xaa\xcdn\xfe\xfd\x86\xe9\x88\xd8\xf4W\xfd\xc8Jb\xd3\xbf\xdbC\xd7\xb1\xe9s\xd9\x11\x03b\xd3_u\x15\xd8\xf5\xb9\x9c\x07v}.G\xc4\xae\xdf\x13\xaf\x1d\x9b>\x97Sf\xd7\x7f\xfc\nt~\xe9?\xfe\xef\xbaNA\xc2\xd8\x13\x0f\xf9\x9eh5\xe6\xff\x99\xa7\x11\xad\xbc\x81:|\x80\xdc\xd5oa\x915\xef\x03\xbcs{\xda7\xc8\xdd\xe6}\x80\x89\xaf\xf3\xdb\x11\x86\x0f0f\xeag\x8dS\x1c\xbf\xd4l\xa0#p\xfc\x16P\xcd\x1f\xe8:\x18>\xc053\xbd\xc0\x89\xbf\xc8T\rH\x15\xff"U\xfb}\x17PF\xe7\x17\x04\xc77\xdf\x7f\x9d\xd3\x82\xb7\xc6\xf7W\xe7L|\xb5N\x05\xce\xf6\xa7\x9a\xedO\x99\xcd\xaf\xe72\x8c\x9b\xd7s\xb7\'t~A\x9a\xd6\xcf\\O\xab{\xa2\xe9\xaboG\xba\xbb_u\x145\xe5\x1e:#\xc2\xe2\x97\x82?\xa0S\xd1u\xcc\x9f\xfd\xca\xb9\t>\'\x8b\x8cJ\xbec\xe6__\x90\x8b\x94\xcbz:8m\x19\xd9\x87\xce\xb06{\x95\xd9\xe5\x8fHG\xd4\xa9?yn\xa2_\x05u\xda\\\xb2\xd8D\x9f\xf86\xf5\xdc\xa5\x167\x0fp\x9d\xd5g/\xb3>1\xd3oj\xfb\xf3T\x1cE-0\x1d\xaf\x96\xdd\x1f\x9c\xb6\xa3\xaec\x8c\xef|y=1fa\\\x10\xa88\xc3T\xf7\xdc\x19{\x93\xcc\x83\x82\xf4]]\xb9[\xe5\x8b7Gu\xf9\x97l?\xfe\x16~ \xd0\xf8\xe4\xee\xb6\xbd\x17\xdf\x86\xb7\xe1G\x04\x1d\xa0\xdc\xf8\xfb\x1dq\x9c\xec\xc5\xab\xbe\x87\x802\xf3`\xad/\xb3\x17\xc9~|\xd7\'\x1f\xdb_f\xc7\xc9\xba3\x17\xd7\xf7\xc4Ir?\\\x9f\xca\xf3\xf4d#\\\xff\x92\x9d\xa7\x9ba\xfa\x1d\xf1%\xfd\xbb\xf7\x04\xcf\x9f\xb1\xaf[\x15h>\xfd\xbd\xfd\xed\xf0|O\xcc\xb2\xed\x06h\xfe2S\x85\xe7\xff\xccT\r\x10\xf7\xafj\x84\xe6\xf7\x84\xaa\x12\xcd\xa7RU\xe9c\xf9\\\xaa\xfa\xef\xff\xbb\xfe/' - if name == "apps/webauthn/res/icon_slushpool.toif": - return b'TOIF@\x00@\x00\xf1\x03\x00\x00\xed\x92=h*K\x14\xc7\xa7H\xb1E\n\x0b\x8b)R(\xbc\xc2\x05\x8b+\xdc"\x82M\x16,v\xc1"\x0b)\x14,\x82\xa4\x08\x8bE\x90\x14\x12l$\xa4\x10\xb1\x08\x92"\x88E@\x0b\xc1W\\\xd0B\x88\x8d\xb0\x16\x81M\x11\xf0\x16\x01-,,Rla\xb1E\x8a\xf3<\xce\xdb\xeb\xac\x1a_>\xc0Wd\xff\xa7\x99=sv~\xe7\x8b\x10W\xae\\\xb9r\xe5\xea{\xaa\xa0\xde\xb4\xa8Nuo\r\xbf\x0e\x8bT\xaf\x87\xf04\xa6\xe8\x9dV\xf0\xdcP\xa9^P\xf1\x14\x15\xaf\xcb\xe8\xa7zT$\xa4\x1e\xa2\xfaY\x11\xfd\x01\x89\xea1\xed\xe3\xf4`\n`\x04U\xabj\xed\xeb\xf8}\xd3\x02\x90\x15\xc6\xef\x0c\x00\xb2\x06\x9e\xd3\x1a@0EH\x8fF&0\x8f\xce\x99W!\xa4\x02\xb4[\x18\xf1\x90\x008,~\x9co\xdd\x03\xb0\xca\x98\x16|Bv\xfc]\xcb\xc9G\xcaQ\xf3@\xb0\xa3\xbf\xce?j\x02\xc4\r\xcfeAe\xafn\xe6\xcb\n@\xd2|\xaa\x04S;~\x9b\xdf\x19\x942\xa5\x8c\xb7\xf69~T\xec\x0cF\x80\x8a\x1b\xc7\xc2\x7f\xf1}\xc4[\xebZ\x18]\xb5\x02\x12\xe3/\xf4\x19>\xea*TP\xb3\x06@Cu\xf2\xc7\xd4\xe6\x972\x8c\x8f\xeaQY9+\x02\xfc\xbeg\xfc}]Vd\xe5\xc7\xe5\xe7\xf8\x824\xad\x8c\xa9\x8f\x9c\xcf\xe6\xf0\x90`\xfc\xddTT\x8c\x8a\xc7\x82\x8f$\xcd\x9c\x89\xa7v\x8be\x97\xf7\x1c5\x85Y\xdd\x05u\xc1\xff\xfa\xfeug\xfb\x0c\xf0:\xcc{\x18\x7f47\xccfZ\xb1o#\x13\xbc}I\xe0m\xce\xc4{\xcc\xe7\xeb\xfc\xbc\xa7\x94\xa1z\xdc8+\xb2\x8d\xc2Mb\xd6\x0f\x13r,<^\xec\xcfn\x9f*Q\x91\xc5\xcb\xcay3k\xdc\xb4\xd8\x8cN\xc4\x9f\xb5\xbf3x\xea\x87\xbd5\xcc\xd8\xd5G\xfb\xdf\xa3\xdb5\'\xff\xa8Y\xb5\xb6iI\x13\xf7x!\xdc\xf7m\xaaj}o~\xd7\xbas\xf0\x7f\xd6`\xcb:Ux\xfeKb\xdb\xfc\xa7\n\xcf\x1f\xd3\xae\xb5:\xa3\x9ci[u\xe9vo\x925n\x07\xd5\x95\x7f"k\xfc\xdd\xf9;\xcb\x91\xafC\xe2P\xdcX\x8e\x90\xb9\x0e\xf5hZ\xb3_\xb5\xee\x05\x89y\xef<\xa5\xcc\xe2\xe5}= \xf9\xe6\xfe\xbc\'\xa6%Mg\xa5\xab\x19\xd4C<\xff\xf1b\x13\x1f\x15L\xa1\xf7\xa6\xe5sx\xa3bd\x82~\xaa\x1f\x08\xbc\xff/\xff\xebp3\xff\xf1\x82\x8f\xaf\x87Fk\xf8\xa7Jd\x82\x96\xd6\x08\xf1\x11\xac\xe9v@\xc8\x8e\x7fZy\x1e\xc6\x8d\x98\x86\x7f\n\x12\xfe\xd9\x19\xe0\x14\x7f\x95\x9f\x87Y#\xada\x8e\xfd0\x9b\xe9[\xfc\xb8\xe1\xacoo\xb2\xcao\xa8v\xcf1\xa23\xc0i\x06\xa4\xc8\x9f\xc8\xeb2\xfa\xdb-\x80\xd1,\xde\xae\x18`:g\x9e77\xf1\xbb\xd6\x8e\x9f\xe7\xff*\xbf\xcd\xf7\xd60\x82\xbd\xcf\xe79\x82\x13\xd1\x9e\x8c3\xff~\x98\x90\x87\xc4&>\xc0K\x82\xe7\x9f*\xab\xfc\x1e\x95\x15Y\tHy\xcf\xba\t\xa1vS\x84\xc8\xca\xaa?8\xf3O\xc2\x9b\xf9\xe7M\x9e\x9f\xf78c\x9c\xfb7\xa6\xb7\x835xh\xa8\xab\x99\xdb\xb5\x05\xa4\xcd\xfc\xa4\xe9\xdcY\x9c\xa3\x93\x7f\x15\x9aV\xd0\xda\xad\xaa\xb5\x8e\x1e\x99\xdc\xcd:c^,\xfbsf\x8f\x12\x92\xd66\xf3\x97k\xdcM\xbd5\x7f\xa7\xec\\\x92\xe6\xe9,\xe6@x\x1e\xda\x1be\xd3\xb1+>\xc2:\xb6\x89\xcf\xf6w\xd1\xe3\xd1\xbb\xf8\xaf\xc3\x80\x14\xd3vSX#!?.\x17\xdd\x10\xe6~\xb6\xd7\x8f\xffv\x85\xf1cZ)\xc3\xac\xcbu\xf2y\xe8\xe3\x13 Y\xe3=|\x80z\x98\xc5\xfbf\xbd\xe7s\x9e\xfc\xf1\x972\xb6\x9f\xf1\x17\xe2;1\x82\x13\x91\xbf\xf3\\.\xee<\x97\r\xf5\xb0\xb8\x9e\x7f]>\x11\xfb\xe1`\x8a\xcf\x97\xb1\xaeB\xf5\xd0K"\xce\xf9\x7f\xdf7T\xde\x9c\x9bT\xca\xf0\xfc~\x18\xde\xa9\xd1\x07\xfdo\x89\xea<\xdfG\xf6&\xb0Uu-\xb6E\xb6\x9e*\xb0e\x05$\x9e\x1f\xd3\\\xfe\xff\xc9\x97\x95vk\xbbv\x15"\xae\\\xb9r\xe5\xea[\xea\x1f' - if name == "apps/webauthn/res/icon_stripe.toif": - return b"TOIF@\x00@\x00\x90\x02\x00\x00\xed\x92\xa1b\xdb0\x10\x86\x8fMPl\n\xab\xa0\xa0\xa0\xcd*6A\x05-b\x13k\xd8\nShVC\xc3\xc0\x85m0}\x04C\xb3\xbao\xb0@\x0f-\x85\xb3c\x9f\xadd^\xd6\xb4\xcbB\xf4\x1f\x88\xa2\xb3\xee;\xe9~\x80\xa0\xa0\xa0\xa0\xa0\xa0\xa0\xa0\xa0S\xc4a\xc3\xc8\xe42\xecJn\xcd\xd2-\x9dQ\xe7\xba[\xceV\xf4\xcf\xf9\x96~\x1e~!\xcb]\xf5B^\x86\x8f\xb5\x8f\xf1K\xf3dd|\xc3\x1d\xb9\x0c\xff_\xc9\x91B\xae5\xb3lZ\x1a\xa94\xe7\x1e\xbf44\xa6q\xd3\xc5\\4+\x1a;\xa2y\xa63\xad\xc87\xd9\xeeh>d\x13\xfa\x9d}V\xcff[W:\xf4\x8e#UT\x1af\x9b\x9a_\xfa\xdc\\,\xdcr/R\x01\xf0l\xfc\x9d\xc8\x02d\xba]?v\xbf\x8e\xf8\xf3\xc7\xecv\xef\xdcW1\xd0\xef\x0f8\xe9.\xb7aK\xb7<\x89\x8f1\xce?\x8c\xe6m\xda[\x8e\xe7\xd6\xfa\xbc\xfc\x99\xe5\xbb\x97\xc7\xbb/\xea\x89.\xbc\x1c\xaeo\x15\x87+H\xe8\\\xac\xb5\xcf7*guP\x9fP\x9aT\xa4\xe2\x1a\xc6\xf8\x0bw/\x1c\xc9\xe9\xcf\xbe\xffy]\xad\x8a\xda5\xb3\x8e\x00(\xf2~\x8a9\xfcj\x8d\x0f\xd5\t\xf9\x8d\xf3Z\r\x13\xc6\x9d1\xfeZ\xb7\xb9O\xfd\x8d\xb3:\xcb\xec\xd0K#\xcd\xbb[\xeb\xa1\xcf\x99\x93\xf1\x86\xfd\x9d\x8f\x84\xe3|\x00\xa3pv\x8a \xa3\x92s\xd1D%1G\xf8\xfe\xbc\xb6\xa6\xed\xe1\xed\xfcT\xe0DVt\xdc\x1bM\x00\x08>\xb3\xfb{\x82\xbf\x9e\xff\xd8O\x07g{\xe7\xf2\xa3|\x00\x0e\xf7\xa2\xf4\xfc\x1e\xdbab\xa7\xf2\x1fz~!\xd1\xe5\t\x1df\x1cY?&vp\xdc\x8a\x0e=\xe4\x94Mqb\xaf\xe5c\xb5\xd2p\xb8\xeb\xea\x1e\x98\xbc\x16\x8d\xc9\x04\xd7d\x82\xfc\rC~\xa6O\xe3\xb3\xeeF7\xbd\xafh\x0c\xf0`\xd0]~\x03\tik\xcc\xec\xad*d!Y\xef\x83\xc4{\x8bL\xd3x\xdcac\xfc\xa6\x03\x19\x0f\xff\xea\xb7d\x83\x17\x1a\xc7Wr.RQE\rA\xf55\xfc\xd8\xd6oXI\x7f'\xb2/\xe7\xef\x07~=\xc6\x19\xe7\xcf\xdc\x8a\x02(\xf2\xd1\xbe\x9d\x1fYG\xdao\x1dav\x8c\x9f\n\xbf\x83;w\xab\x92\xeeDB\x9f\xfa\xcc\xb3y\xf1\xfc\xa7C'\x99Fz#\x0e?\xa2\xc5^oE\xe7mE6Ls\xcd7\xec\xfa\xc0\x9d+\xaa\xb9\xe09m\xcfc\x0c\x15\xdb\xb8:pg\xb2;\xe5\xb3QW\x90\xd3\x0f5\x89L\x92\x91\xec[\xf4\xfb\xeb\xfc_\x05\xfe\xa2fW\xd1\xbb\xc9e\xf8\x8e@PPPPPPPP\xd0\x0b\xf4\x0b" - if name == "apps/webauthn/res/icon_tutanota.toif": - return b'TOIF@\x00@\x00z\x02\x00\x00\xed\xd2\xb1\x8b\xdaP\x00\xc7\xf1\xa4 \xd4\xe1\xe0\x0e\x1c\x1c\x9c\x1c\x84+d\x10Zp\xb0p\\\xc1\xb1\xd0\x83\x0e7\x04\x0e\x1c\xee\xb0 \x87\x90R\n)\x19:D\x84+q80\xc3\x1b\xc4\xc97\xdc\x90\xad\x971\x82C\x1f\x14\x0e\xe3\x10p3Z\x10\x9e\xd0\x83Wpi\xa3\xb4\xb6\xbd\xdaKb^\xb2$\xdf?\xe0\xf3\xcb{\x8fa\x9c\x8f=`\xcf\x1fH\xe1\xc5\x9e\xb3\x07\xccCf\xf5\xed\x86k\xaf70\xbb\xcb\x7f?\x8bB_.8\xfb\xc1\x17\xa2\xd2\x9d\x98\x02{\x12\xa5\xcf\x9e\xb0o"\xf5#\xd5\xe3\xe2\xe2\xe2\xe2\xe2\x82\xe8\x994\x96\xa3]\x90\x95**lG\xbb\xe1J\xe3`\x94\xfe\x91\xfe\xb2\x17\x85[\x96\xf7U\x0e~\x1b\x14q\xbfEW\xca\xd5\x1bm\x0eV\xb5\xf9\xf5\x8d1D\x05S\x1dYv\x11\x8b\x04\x90\x0cN\xf5J\n=y_\xfdj,l@V\xf1\xd8\xb2\x8f\xcd!\xba5\xe6\xd7Um\xd6\x9d*4\xff\xba#\x0b\xe8\xdd/9\xaf\'\xd5l\xa8\xf7{\x89~\xfe7 WZ\xf8\xef\x8b\xc7k\x7fa\xcf\xba\x87!\xfb\x17\xf0\xf7\x05\x80\x14\xb1\x80v\xe0\xa9\x12\xde\x82\xbe2\x1c\x88\x04\xfc\x95e\x0b\xe89\xfc\xd2\ngCR\x15\x90\x88\x01\xb9[\x06\x93AUK\xaa\xf47\x94\x94\x9a^\xfc\xe7\x86\xd5\x8eK\xc4\xc1D\x9d\xee\x06C\x9ek\x99\xc9\xa6\r\x80\x88\xf8x\xf4A+Q}\x1d\x1d\xf9\x85\xce\xe3\xcd\x1b\x9c\x15is\xd6-S\xbd\x8b\x82\xf9\xbf\x05\xcb\x1b\x99\xe4\xf5\x9eLk\xc1\xa1tc\xdc\xb7\x00\x90"\xce\xeb\x1dj\x1bn],\x00da7\xdat\xfc\xf7\xb2e\xbbY \xe2\xa6Ng\xc1g\xcd\x8d\xef$ \x1a\xef\xb1\xd1v\xeb\x03\xf2\x1a\x05\xef\xbf\xf2\xe0\x03R\x0b\xfc\x16\xaa\x9a\x17_\xc4A\xbfDu\xe4\xc5\x07daw\xe4\x00\xff\x1ez\xd3\x9d\x8e\x02\xbb\x83S\xe5)\xf6\xee\xf3x\x1c\xc8\t\x94\x94\x85\xed]w\xda\x0b\xe0\x04\xfa\xad\xcc\xc4\x9f\x0e\x88eo\xab_@\x1e\xfb\xd5\x9d*\xaa\x7f{,\x0bh\x1b\xdb\xa9i\xf8\xd59\xe8\xff\xdc\xd7}\xf4u\x03\xd3V\xda\xdc\xdev\x12\xb1W;\xa7\xa4z"\x0eFw\xf2b\'\xeaM\x83\x0f\xd0\xf6\xe2\'\xea5=h\xdb\xad\x9fS4\x83\x86\xed\xc6\xef+\x8f{\xb4\xec\xfb\xfc\x8a:\x1c\x88\x14\xed\xcd~Y\xda\x81oGt\xe5M\xfeX\xde\xd33\x930\xec\xbb\xfe\xb4\x95\xa2z\xdb\x9b\xfd\xac\xf4\xa9\x9d6E\x02Bnu\xe2s\xcd\xb2\x01\x89\xa2G\xea\x13\x14\xee\x89\xff\xd9w' - if name == "apps/webauthn/res/icon_webauthn.toif": - return b"TOIF@\x00@\x00c\x04\x00\x00\xb5R!\x8c\xeaJ\x14\xad\xa8@\x8c\xa8x\xa2\x02SWQA\x93\x15\x8f\xe4\xa9\xbaN\x82\xf8M\x104AU5dU\x83 d\x1dA\x90\x1f\xc4\xcb\xe6\x8b\x97\r\x82d\x11M@\xfcdW\x90\x80D\xf6\x8bMXA\x02\x02QQQ\xf1E\x05b>Cw\xff\xb2\xfbh{\x07\xe8\xb9\x06fn\xef\xb9s\xce\xe186\xf0\x92_\xd6\xb1k\x04&\xb2\x94\xb7BV`\xba\x86\x8e\xfd2/q\xb9a\xa4\xda80\x15+\xbd\x02\xd3\xc6#\xf5\xba\xcc[Q\xd6\xb2\x99?o!k[\xf1\x1a\xdc\xbc\xa4cd)g\x14\xb2t|\xa9\x1f\xb2v\x1e\xf7\xc7\x0e\xb2v.wWu\r\xc5\xba\xbc\\\xa3{F\x1e\n\x17\xbe\xfc\xb3\n\x05&\x15\x86\x82\x8d\x15\xeb\xbae\xe3\xa1\x00M\\\xcf\xb86;\xad\x9e\x01Ic^\xec\xb0\r\x86B~\xec\xf1\x06\xe9.\\\xdf\xf7\xdfs\x90\x96\xf9\xbc\xd9i\x15\xb4\xd3\xec]\x151NBV`\x06&\xfbW]\xf5\x14\xbf\xcb\xe0|\xa5Q\x1dO\x97\xf5p\x1e\xcd\xa3z8]V\xc7\x95\x06\xfck\xd7\xf8\x9d]\xd6\xe0\xdc\xd1lC\xbebC\xa2\x19|\x07\xf9\x8b\x07\xbc\x04UQ\xe8\xd4\xc3\x98o\xb5~\x9dU\xc7\xd5\xf1\xebl\xb5\x8e\xf7\xa9\x87B\x07\xea\x01/\x1d\xf3\xeb\x18\xca>\x88(S\xcd\x9b8\xc7\xe7\x13\xa7\xe6\xd1\xf3A\x04\xdd@\xc7\x1f\xec[\x11\xf6\xfa\xbeC\xd97\xe4\xe5\xe1\xd4\xed\xcb\xc3\xe6\xb0A\xdf\x81)\xb0\x15Y\xbdoy\x94\xfd\xe7}\xd2\xfd\xcf{\xbaA\xcbc\xcd@`B\xfaK\x1d\xaa\xf0\xeb,\xad\xe7uF{J \x0f\x023f\x1f\xa9\xb0}\xbf/\xa8\xba\xe9\x19\xaf4\xa8C\xdf\x17\xb0\x89#\x95\xf2\xdb\xc0\xec\xd1\xdc\x8b\x99\x93\xc5\xfd\x96\xf5\x106\xd1\xc6p\xf5'\x0eU6\xd9\xfb\x8f\x0c\xd0\xbe\x89\x03u\x80\x97`\xbb\n\x1d\x9a\xbd\xb0\x9d\xd5\x17\xb6i\x06\x85\x0el*/\xf9eXg\xe9\xc0\x9f\xfd\xae\x89\xb3\x01'P\xb1\xfc\xb2\x0et?l\xc3\x92M\xf7$\x00\x9d\xe2\xd2\xb1k\xc0:o\x1b\xf3}\xb2o\x1e\xb3\xfan\x1e\t\x99G\xb7\r\xd8T\xd7\x80\xa5O\xb1\x90\xb5[\x13\xb2Z\xa3\x8c\xae\xd5\xbek\x97\xd1u\x9c@h\xa7b5\xc7$3Y\xc2A\xfd\xe6\x18:\x13\xceN\x1d\x18\xec\x1d(\xfai=E\x9f\x90\x01X}\xd6\xa2\xde\x12R\xf3\x92\xeek\x1e\xbd\xcf\xce\xc8\xb9\x85\xde\x18V\xeb\xbe\xf3\xf5\xae\xefP\xe7\xe9v\xc8Rr\xac\xd6a\x83y$.\xc2\xf6\xfbY\xd8\x16\x17\xf3\x88\x9e\xb7<\xf67\xb1\xf6??mH\x8cAT\xf4\x8b\xfe \x8a\xffm\xc8\xf3\x13\xfb\xb4\xc0d\xfb\xe2\xb6\xf1\xfct\x17\x92\x13\xa8\x87\xcdq\x851y\x81\xe9\x1a,\xfd\xcd\xf1\xfc\xed\xb5\xf3h\xb7\xaey\xe2B\\\xd4\xbc\xdd\xfa\xfd\xf4.\xfc\xc6\x94>\xd7\xd01\xb4\xb7\xefL\x97\xb1\xce\xbf\x96\x7f\xdf\xa3#\xe7\xe8\xaf\x97\x87\xf8\x96\xeco\xe1*\xe8\xd8/\xc3:\xffi\xd7\x0f\xaaO\x97B\x07%xY\xea\x14}\xdaS\xf4\xfb\x0el\xaa_\xe6%\x98\xeb1{v\xc2\xa2Y\xbc\x01L\x03^\xe28H\x02cmo\x1e\x11 \xd1\x7f=\xd1\xde\x96\x07I\x1f\xb7\x87\x9d\x99\x80\x9b\xc7\xf8\xed0M\xd1\x9b\x06\xff>du\xda\x98\xf2\x8f\xd4\xf4\xaeJc\xb0O\xf7n\x8d\x18rMsp\x17fu\x8dT\xee\x80t\x07\x9ac\xfa\x9aRGa\xe0\xff\xe3\xcf\xd8\xafl\xf5)d-\xeb-\xd3%\xcb\xeb\xa9\x07\xab5MaZ\x8f\xac\xbd\xf3o\xc5\xe4\xe9\x13\x07\xe6\xe5\xd7\xfav\xc8L\xdfI\xdep+r\xffCO\xcc`u\xaf\xfe\x86\xb0\xb2\xd3Jw@\xc7\xdc\x11x)I\x01qA\xc8\x0f\x1f1\xb3#\xeb\xc7\xde\xb7h\x96t\xcbK\xdc'$e`\xba\xa4\xee\x9f\xf3~\xfa\xe5\xafe\x96\xf7\x1fp\x8d\xd3S6\xa4\xe5\x9d\xc3\xdf\xf26\t\x9b\xbb\x06w\x02]\xf5\x94\xca\x81\x19\x98\xc8:\x87\x1f\x1d\xbe=u\xdeU\xb9\x93(h\x8a\x95\x7f\x154.\x116\xce\x9b\xdd\xc6\\\n\x86B\xcf\xc8\x93\xbdg\x0c\x05.\x15\xbc\x94\xdf\x06=\x83\x97\xb8L\xe4\xb5\x01\x8c=v\xe1\xfa9\xb0q\x96\xf2\x9fQ\xd0\xd0\xd5\xb8Qj\xe6\x93\xd0U\xdd\xab\xf8\xe0\x1a]\x95;\x13\xf2\x85* K\xd6\xb8\x8b\xc0K:Fgr\xeb\x18\x9a\xb8tlEY\x0bL\x16\xee\xc0\x94\xb5\xad\xc8]\x15#\xd5\xc6\xd9[\x04\xa6\x8dG*\x97\x1bx\xc9/\xeb\xd85\x02\x13\x1d)\x1d\x98\xae\xa1c\xbf\xcc\xae\xf7\x7f" return bytes() diff --git a/core/src/trezor/res/resources.py.mako b/core/src/trezor/res/resources.py.mako deleted file mode 100644 index 0f829157c..000000000 --- a/core/src/trezor/res/resources.py.mako +++ /dev/null @@ -1,26 +0,0 @@ -# generated from resources.py.mako -# (by running `make templates` in `core`) -# do not edit manually! -# fmt: off -<% -from pathlib import Path -from itertools import chain - -THIS = Path(local.filename).resolve() -SRCDIR = THIS.parent.parent.parent - -PATTERNS = ( - "trezor/res/**/*.toif", - "apps/*/res/**/*.toif", -) - -resfiles = chain.from_iterable(sorted(SRCDIR.glob(p)) for p in PATTERNS) -%>\ - -def load_resource(name: str) -> bytes: -% for resfile in resfiles: - if name == "${resfile.relative_to(SRCDIR)}": - return ${repr(resfile.read_bytes())} -% endfor - - return bytes() diff --git a/core/src/trezor/ui/__init__.py b/core/src/trezor/ui/__init__.py index b9eb9b38b..31e9a98a0 100644 --- a/core/src/trezor/ui/__init__.py +++ b/core/src/trezor/ui/__init__.py @@ -1,18 +1,11 @@ # pylint: disable=wrong-import-position -import math import utime from micropython import const from trezorui import Display -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any, Awaitable, Generator from trezor import io, loop, res, utils, workflow -if TYPE_CHECKING: - from typing import Any, Awaitable, Generator - - Pos = tuple[int, int] - Area = tuple[int, int, int, int] - # all rendering is done through a singleton of `Display` display = Display() @@ -23,10 +16,6 @@ MONO: int = Display.FONT_MONO WIDTH: int = Display.WIDTH HEIGHT: int = Display.HEIGHT -# viewport margins -VIEWX = const(6) -_VIEWY = const(9) - # channel used to cancel layouts, see `Cancelled` exception layout_chan = loop.chan() @@ -53,22 +42,10 @@ if utils.EMULATOR or utils.MODEL in ("1", "R"): loop.after_step_hook = refresh -def lerpi(a: int, b: int, t: float) -> int: - return int(a + t * (b - a)) - - def rgb(r: int, g: int, b: int) -> int: return ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | ((b & 0xF8) >> 3) -def blend(ca: int, cb: int, t: float) -> int: - return rgb( - lerpi((ca >> 8) & 0xF8, (cb >> 8) & 0xF8, t), - lerpi((ca >> 3) & 0xFC, (cb >> 3) & 0xFC, t), - lerpi((ca << 3) & 0xF8, (cb << 3) & 0xF8, t), - ) - - # import style later to avoid circular dep from trezor.ui import style # isort:skip @@ -76,11 +53,6 @@ from trezor.ui import style # isort:skip from trezor.ui.style import * # isort:skip # noqa: F401,F403 -def pulse(period: int, offset: int = 0) -> float: - # normalize sin from interval -1:1 to 0:1 - return 0.5 + 0.5 * math.sin(2 * math.pi * (utime.ticks_us() + offset) / period) - - async def _alert(count: int) -> None: short_sleep = loop.sleep(20) long_sleep = loop.sleep(80) @@ -157,62 +129,6 @@ def get_header_height() -> int: return MODEL_HEADER_HEIGHTS[utils.MODEL] -def draw_simple(t: "Component") -> None: - """Render a component synchronously. - - Useful when you need to put something on screen and go on to do other things. - - This function bypasses the UI workflow engine, so other layouts will not know - that something was drawn over them. In particular, if no other Layout is shown - in a workflow, the homescreen will not redraw when the workflow is finished. - Make sure you use `workflow.close_others()` before invoking this function - (note that `workflow.close_others()` is implicitly called with `button_request()`). - """ - backlight_fade(style.BACKLIGHT_DIM) - display.clear() - t.on_render() - refresh() - backlight_fade(style.BACKLIGHT_NORMAL) - - -def grid( - i: int, # i-th cell of the table of which we wish to return Area (snake-like starting with 0) - n_x: int = 3, # number of rows in the table - n_y: int = 5, # number of columns in the table - start_x: int = VIEWX, # where the table starts on x-axis - start_y: int = _VIEWY, # where the table starts on y-axis - end_x: int = (WIDTH - VIEWX), # where the table ends on x-axis - end_y: int = (HEIGHT - _VIEWY), # where the table ends on y-axis - cells_x: int = 1, # number of cells to be merged into one in the direction of x-axis - cells_y: int = 1, # number of cells to be merged into one in the direction of y-axis - spacing: int = 0, # spacing size between cells -) -> Area: - """ - Returns area (tuple of four integers, in pixels) of a cell on i-th position - in a table you define yourself. Example: - - >>> ui.grid(4, n_x=2, n_y=3, start_x=20, start_y=20) - (20, 160, 107, 70) - - Returns 5th cell from the following table. It has two columns, three rows - and starts on coordinates 20-20. - - |____|____| - |____|____| - |XXXX|____| - """ - w = (end_x - start_x) // n_x - h = (end_y - start_y) // n_y - x = (i % n_x) * w - y = (i // n_x) * h - return (x + start_x, y + start_y, (w - spacing) * cells_x, (h - spacing) * cells_y) - - -def in_area(area: Area, x: int, y: int) -> bool: - ax, ay, aw, ah = area - return ax <= x < ax + aw and ay <= y < ay + ah - - # Component events. Should be different from `io.TOUCH_*` events. # Event dispatched when components should draw to the display, if they are # marked for re-paint. diff --git a/core/src/trezor/ui/components/common/__init__.py b/core/src/trezor/ui/components/common/__init__.py index 365572b16..dde316375 100644 --- a/core/src/trezor/ui/components/common/__init__.py +++ b/core/src/trezor/ui/components/common/__init__.py @@ -8,14 +8,3 @@ SWIPE_UP = const(0x01) SWIPE_DOWN = const(0x02) SWIPE_LEFT = const(0x04) SWIPE_RIGHT = const(0x08) - - -def break_path_to_lines(path_str: str, per_line: int) -> list[str]: - lines = [] - while len(path_str) > per_line: - i = path_str[:per_line].rfind("/") - lines.append(path_str[:i]) - path_str = path_str[i:] - lines.append(path_str) - - return lines diff --git a/core/src/trezor/ui/components/common/confirm.py b/core/src/trezor/ui/components/common/confirm.py index 86676dd3b..d8f247fd9 100644 --- a/core/src/trezor/ui/components/common/confirm.py +++ b/core/src/trezor/ui/components/common/confirm.py @@ -1,17 +1,17 @@ from typing import TYPE_CHECKING -from trezor import loop, ui, wire +from trezor import ui, wire + +import trezorui2 if TYPE_CHECKING: from typing import Callable, Any, Awaitable, TypeVar T = TypeVar("T") -CONFIRMED = object() -CANCELLED = object() -INFO = object() -GO_BACK = object() -SHOW_PAGINATED = object() +CONFIRMED = trezorui2.CONFIRMED +CANCELLED = trezorui2.CANCELLED +INFO = trezorui2.INFO def is_confirmed(x: Any) -> bool: @@ -39,43 +39,6 @@ async def is_confirmed_info( return is_confirmed(result) -class ConfirmBase(ui.Layout): - def __init__( - self, - content: ui.Component, - confirm: ui.Component | None = None, - cancel: ui.Component | None = None, - ) -> None: - super().__init__() - self.content = content - self.confirm = confirm - self.cancel = cancel - - def dispatch(self, event: int, x: int, y: int) -> None: - super().dispatch(event, x, y) - self.content.dispatch(event, x, y) - if self.confirm is not None: - self.confirm.dispatch(event, x, y) - if self.cancel is not None: - self.cancel.dispatch(event, x, y) - - def on_confirm(self) -> None: - raise ui.Result(CONFIRMED) - - def on_cancel(self) -> None: - raise ui.Result(CANCELLED) - - if __debug__: - - def read_content(self) -> list[str]: - return self.content.read_content() - - def create_tasks(self) -> tuple[loop.AwaitableTask, ...]: - from apps.debug import confirm_signal - - return super().create_tasks() + (confirm_signal(),) - - class Pageable: def __init__(self) -> None: self._page = 0 diff --git a/core/src/trezor/ui/components/common/text.py b/core/src/trezor/ui/components/common/text.py deleted file mode 100644 index 033badf90..000000000 --- a/core/src/trezor/ui/components/common/text.py +++ /dev/null @@ -1,536 +0,0 @@ -from micropython import const -from typing import TYPE_CHECKING - -from trezor import ui - -from ...constants import ( - PAGINATION_MARGIN_RIGHT, - TEXT_HEADER_HEIGHT, - TEXT_LINE_HEIGHT, - TEXT_LINE_HEIGHT_HALF, - TEXT_MARGIN_LEFT, - TEXT_MAX_LINES, - TEXT_MAX_LINES_NO_HEADER, -) - -LINE_WIDTH = ui.WIDTH - TEXT_MARGIN_LEFT -LINE_WIDTH_PAGINATED = LINE_WIDTH - PAGINATION_MARGIN_RIGHT - -if TYPE_CHECKING: - from typing import Any, Sequence - - TextContent = str | int - -# needs to be different from all colors and font ids -BR = const(-256) -BR_HALF = const(-257) - -_FONTS = (ui.NORMAL, ui.BOLD, ui.MONO) - -DASH_WIDTH = ui.display.text_width("-", ui.BOLD) - - -class Span: - def __init__( - self, - string: str = "", - start: int = 0, - font: int = ui.NORMAL, - line_width: int = LINE_WIDTH, - offset_x: int = 0, - break_words: bool = False, - ) -> None: - self.reset(string, start, font, line_width, offset_x, break_words) - - def reset( - self, - string: str, - start: int, - font: int, - line_width: int = LINE_WIDTH, - offset_x: int = 0, - break_words: bool = False, - ) -> None: - self.string = string - self.start = start - self.font = font - self.line_width = line_width - self.offset_x = offset_x - self.break_words = break_words - - self.length = 0 - self.width = 0 - self.word_break = False - self.advance_whitespace = False - - def count_lines(self) -> int: - """Get a number of lines in the specified string. - - Should be used with a cleanly reset span. Leaves the span in the final position. - """ - n_lines = 0 - while self.next_line(): - n_lines += 1 - # deal with trailing newlines: if the final span does not have any content, - # do not count it - if self.length > 0: - n_lines += 1 - return n_lines - - def has_more_content(self) -> bool: - """Look ahead to check if there is more content after the current span is - consumed. - """ - start = self.start + self.length - if self.advance_whitespace: - start += 1 - return start < len(self.string) - - def next_line(self) -> bool: - """Advance the span to point to contents of the next line. - - Returns True if the renderer should make newline afterwards, False if this is - the end of the text. - - Within the renderer, we use this as: - - >>> while span.next_line(): - >>> render_the_line(span) - >>> go_to_next_line() - >>> render_the_line(span) # final line without linebreak - - This is unsuitable for other uses however. To count lines (as in - `apps.common.layout.paginate_text`), use instead: - - >>> while span.has_more_content(): - >>> span.next_line() - """ - # We are making copies of most class variables so that the lookup is faster. - # This also allows us to pick defaults independently of the current status - string = self.string - start = self.start + self.length - line_width = self.line_width - self.offset_x - break_words = self.break_words - font = self.font - - self.offset_x = 0 - width = 0 - result_width = 0 - length = 0 - - if start >= len(string): - return False - - # advance over the left-over whitespace character from last time - if self.advance_whitespace: - start += 1 - - word_break = True - advance_whitespace = False - for i in range(len(string) - start): - nextchar_width = ui.display.text_width(string[start + i], font) - - if string[start + i] in " \n": - word_break = False - length = i # break is _before_ the whitespace - advance_whitespace = True - result_width = width - if string[start + i] == "\n": - # do not continue over newline - break - - elif width + nextchar_width > line_width: - # this char would overflow the line. end loop, use last result - break - - elif ( - break_words or word_break - ) and width + nextchar_width + DASH_WIDTH <= line_width: - # Trying a possible break in the middle of a word. - # We can do this if: - # - we haven't found a space yet (word_break is still True) -- if a word - # doesn't fit on a single line, this will place a break in it - # - we are allowed to break words (break_words is True) - # AND the current character and a word-break dash will fit on the line. - result_width = width + nextchar_width - length = i + 1 # break is _after_ current character - advance_whitespace = False - word_break = True - - width += nextchar_width - - else: - # whole string (from offset) fits - word_break = False - advance_whitespace = False - result_width = width - length = len(string) - start - - self.start = start - self.length = length - self.width = result_width - self.word_break = word_break - self.advance_whitespace = advance_whitespace - return start + length < len(string) - - -_WORKING_SPAN = Span() - - -def render_text( - items: Sequence[TextContent], - new_lines: bool, - max_lines: int, - font: int = ui.NORMAL, - fg: int = ui.FG, - bg: int = ui.BG, - offset_x: int = TEXT_MARGIN_LEFT, - offset_y: int = TEXT_HEADER_HEIGHT + TEXT_LINE_HEIGHT, - line_width: int = ui.WIDTH - TEXT_MARGIN_LEFT, - item_offset: int = 0, - char_offset: int = 0, - break_words: bool = False, - render_page_overflow: bool = True, -) -> None: - """Render a sequence of items on screen. - - The items can either be strings, or rendering instructions specified as ints. - They can change font, insert an explicit linebreak, or change color of the following - text. - - If `new_lines` is true, a linebreak is rendered after every string. In effect, the - following calls are equivalent: - - >>> render_text(["hello", "world"], new_lines=True) - >>> render_text(["hello\nworld"], new_lines=False) - - TODO, we should get rid of all cases that use `new_lines=True` - - If the rendered text ends up longer than `max_lines`, a trailing "..." is rendered - at end. This indicates to the user that the full contents have not been shown. - It is possible to override this behavior via `render_page_overflow` argument -- - if false, the trailing "..." is not shown. This is useful when the rendered text is - in fact paginated. - - `font` specifies the default font, but that can be overridden by font instructions - in `items`. - `fg` specifies default foreground color, which can also be overridden by instructions - in `items`. - `bg` specifies background color. This cannot be overridden. - - `offset_x` and `offset_y` specify starting XY position of the text bounding box. - `line_width` specifies width of the bounding box. Height of the bounding box is - calculated as `max_lines * TEXT_LINE_HEIGHT`. - - `item_offset` and `char_offset` must be specified together. Item offset specifies - the first element of `items` which should be considered, and char offset specifies - the first character of the indicated item which should be considered. - The purpose is to allow rendering different "pages" of text, using the same `items` - argument (slicing the list could be expensive in terms of memory). - - If `break_words` is false (default), linebreaks will only be rendered (a) at - whitespace, or (b) in case a word does not fit on a single line. If true, whitespace - is ignored and linebreaks are inserted after the last character that fits. - """ - # initial rendering state - INITIAL_OFFSET_X = offset_x - offset_y_max = offset_y + (TEXT_LINE_HEIGHT * (max_lines - 1)) - span = _WORKING_SPAN - - # scan through up to item_offset so that the current font & color is up to date - for item_index in range(item_offset): - item = items[item_index] - if isinstance(item, int): - if item is BR or item is BR_HALF: - # do nothing - pass - elif item in _FONTS: - font = item - else: - fg = item - - for item_index in range(item_offset, len(items)): - # load current item - item = items[item_index] - - if isinstance(item, int): - if item is BR or item is BR_HALF: - # line break or half-line break - if offset_y > offset_y_max: - if render_page_overflow: - ui.display.text(offset_x, offset_y, "...", ui.BOLD, ui.GREY, bg) - return - offset_x = INITIAL_OFFSET_X - offset_y += TEXT_LINE_HEIGHT if item is BR else TEXT_LINE_HEIGHT_HALF - elif item in _FONTS: - # change of font style - font = item - else: - # change of foreground color - fg = item - continue - - # XXX hack: - # if the upcoming word does not fit on this line but fits on the following, - # render it after a linebreak - item_width = ui.display.text_width(item, font) - if ( - item_width <= line_width # pylint: disable=chained-comparison - and item_width + offset_x - INITIAL_OFFSET_X > line_width - and "\n" not in item - ): - offset_y += TEXT_LINE_HEIGHT - ui.display.text(INITIAL_OFFSET_X, offset_y, item, font, fg, bg) - offset_x = INITIAL_OFFSET_X + item_width - continue - - span.reset( - item, - char_offset, - font, - line_width=line_width, - offset_x=offset_x - INITIAL_OFFSET_X, - break_words=break_words, - ) - char_offset = 0 - while span.next_line(): - ui.display.text( - offset_x, offset_y, item, font, fg, bg, span.start, span.length - ) - end_of_page = offset_y >= offset_y_max - have_more_content = span.has_more_content() or item_index < len(items) - 1 - - if end_of_page and have_more_content and render_page_overflow: - ui.display.text( - offset_x + span.width, offset_y, "...", ui.BOLD, ui.GREY, bg - ) - elif span.word_break: - ui.display.text( - offset_x + span.width, offset_y, "-", ui.BOLD, ui.GREY, bg - ) - - if end_of_page: - return - - offset_x = INITIAL_OFFSET_X - offset_y += TEXT_LINE_HEIGHT - - # render last chunk - ui.display.text(offset_x, offset_y, item, font, fg, bg, span.start, span.length) - - if new_lines: - offset_x = INITIAL_OFFSET_X - offset_y += TEXT_LINE_HEIGHT - elif span.width > 0: - # only advance cursor if we actually rendered anything - offset_x += span.width - - -if __debug__: - - class DisplayMock: - """Mock Display class that stores rendered text in an array. - - Used to extract data for unit tests. - """ - - def __init__(self) -> None: - self.screen_contents: list[str] = [] - self.orig_display = ui.display - - def __getattr__(self, key: str) -> Any: - return getattr(self.orig_display, key) - - def __enter__(self) -> None: - ui.display = self - - def __exit__(self, exc: Any, exc_type: Any, tb: Any) -> None: - ui.display = self.orig_display - - def text( - self, - offset_x: int, - offset_y: int, - string: str, - font: int, - fg: int, - bg: int, - start: int = 0, - length: int | None = None, - ) -> None: - if length is None: - length = len(string) - start - self.screen_contents.append(string[start : start + length]) - - -class TextBase(ui.Component): - def __init__( - self, - header_text: str | None, - header_icon: str = ui.ICON_DEFAULT, - icon_color: int = ui.ORANGE_ICON, - max_lines: int | None = None, - new_lines: bool = True, - break_words: bool = False, - render_page_overflow: bool = True, - content_offset: int = 0, - char_offset: int = 0, - line_width: int = ui.WIDTH - TEXT_MARGIN_LEFT, - ): - super().__init__() - self.header_text = header_text - self.header_icon = header_icon - self.icon_color = icon_color - - if max_lines is None: - self.max_lines = ( - TEXT_MAX_LINES_NO_HEADER if self.header_text is None else TEXT_MAX_LINES - ) - else: - self.max_lines = max_lines - - self.new_lines = new_lines - self.break_words = break_words - self.render_page_overflow = render_page_overflow - self.content: list[TextContent] = [] - self.content_offset = content_offset - self.char_offset = char_offset - self.line_width = line_width - - def normal(self, *content: TextContent) -> None: - self.content.append(ui.NORMAL) - self.content.extend(content) - - def bold(self, *content: TextContent) -> None: - self.content.append(ui.BOLD) - self.content.extend(content) - - def mono(self, *content: TextContent) -> None: - self.content.append(ui.MONO) - self.content.extend(content) - - def br(self) -> None: - self.content.append(BR) - - def br_half(self) -> None: - self.content.append(BR_HALF) - - def format_parametrized( - self, - format_string: str, - *params: str, - font: int = ui.NORMAL, - param_font: int = ui.BOLD, - ) -> None: - parts = format_string.split("{}", len(params)) - for i in range(len(parts)): # pylint: disable=consider-using-enumerate - self.content.append(font) - self.content.append(parts[i]) - if i < len(parts) - 1 and i < len(params): - param = params[i] - self.content.append(param_font) - self.content.append(param) - - def on_render(self) -> None: - pass - - if __debug__: - - def read_content(self) -> list[str]: - display_mock = DisplayMock() - should_repaint = self.repaint - try: - with display_mock: - self.repaint = True - self.on_render() - finally: - self.repaint = should_repaint - return [self.header_text or ""] + display_mock.screen_contents - - -LABEL_LEFT = const(0) -LABEL_CENTER = const(1) -LABEL_RIGHT = const(2) - - -class Label(ui.Component): - def __init__( - self, - area: ui.Area, - content: str, - align: int = LABEL_LEFT, - style: int = ui.NORMAL, - ) -> None: - super().__init__() - self.area = area - self.content = content - self.align = align - self.style = style - - def on_render(self) -> None: - if self.repaint: - align = self.align - ax, ay, aw, ah = self.area - ui.display.bar(ax, ay, aw, ah, ui.BG) - tx = ax + aw // 2 - ty = ay + ah // 2 + 8 - if align is LABEL_LEFT: - ui.display.text(tx, ty, self.content, self.style, ui.FG, ui.BG) - elif align is LABEL_CENTER: - ui.display.text_center(tx, ty, self.content, self.style, ui.FG, ui.BG) - elif align is LABEL_RIGHT: - ui.display.text_right(tx, ty, self.content, self.style, ui.FG, ui.BG) - self.repaint = False - - if __debug__: - - def read_content(self) -> list[str]: - return [self.content] - - -def text_center_trim_left( - x: int, y: int, text: str, font: int = ui.NORMAL, width: int = ui.WIDTH - 16 -) -> None: - if ui.display.text_width(text, font) <= width: - ui.display.text_center(x, y, text, font, ui.FG, ui.BG) - return - - ELLIPSIS_WIDTH = ui.display.text_width("...", ui.BOLD) - if width < ELLIPSIS_WIDTH: - return - - text_length = 0 - for i in range(1, len(text)): - if ui.display.text_width(text[-i:], font) + ELLIPSIS_WIDTH > width: - text_length = i - 1 - break - - text_width = ui.display.text_width(text[-text_length:], font) - x -= (text_width + ELLIPSIS_WIDTH) // 2 - ui.display.text(x, y, "...", ui.BOLD, ui.GREY, ui.BG) - x += ELLIPSIS_WIDTH - ui.display.text(x, y, text[-text_length:], font, ui.FG, ui.BG) - - -def text_center_trim_right( - x: int, y: int, text: str, font: int = ui.NORMAL, width: int = ui.WIDTH - 16 -) -> None: - if ui.display.text_width(text, font) <= width: - ui.display.text_center(x, y, text, font, ui.FG, ui.BG) - return - - ELLIPSIS_WIDTH = ui.display.text_width("...", ui.BOLD) - if width < ELLIPSIS_WIDTH: - return - - text_length = 0 - for i in range(1, len(text)): - if ui.display.text_width(text[:i], font) + ELLIPSIS_WIDTH > width: - text_length = i - 1 - break - - text_width = ui.display.text_width(text[:text_length], font) - x -= (text_width + ELLIPSIS_WIDTH) // 2 - ui.display.text(x, y, text[:text_length], font, ui.FG, ui.BG) - x += text_width - ui.display.text(x, y, "...", ui.BOLD, ui.GREY, ui.BG) diff --git a/core/src/trezor/ui/components/tt/button.py b/core/src/trezor/ui/components/tt/button.py deleted file mode 100644 index c82f614c1..000000000 --- a/core/src/trezor/ui/components/tt/button.py +++ /dev/null @@ -1,253 +0,0 @@ -from micropython import const -from typing import TYPE_CHECKING - -from trezor import ui -from trezor.ui import display, in_area - -if TYPE_CHECKING: - ButtonContent = str | bytes - ButtonStyleType = type["ButtonDefault"] - ButtonStyleStateType = type["ButtonDefault.normal"] - - -class ButtonDefault: - class normal: - bg_color = ui.BLACKISH - fg_color = ui.FG - text_style = ui.BOLD - border_color = ui.BG - radius = ui.RADIUS - - class active(normal): - bg_color = ui.FG - fg_color = ui.BLACKISH - text_style = ui.BOLD - border_color = ui.FG - radius = ui.RADIUS - - class disabled(normal): - bg_color = ui.BG - fg_color = ui.GREY - text_style = ui.NORMAL - border_color = ui.BG - radius = ui.RADIUS - - -class ButtonMono(ButtonDefault): - class normal(ButtonDefault.normal): - text_style = ui.MONO - - class active(ButtonDefault.active): - text_style = ui.MONO - - class disabled(ButtonDefault.disabled): - text_style = ui.MONO - - -class ButtonMonoDark(ButtonDefault): - class normal: - bg_color = ui.DARK_BLACK - fg_color = ui.DARK_WHITE - text_style = ui.MONO - border_color = ui.BG - radius = ui.RADIUS - - class active(normal): - bg_color = ui.FG - fg_color = ui.DARK_BLACK - text_style = ui.MONO - border_color = ui.FG - radius = ui.RADIUS - - class disabled(normal): - bg_color = ui.DARK_BLACK - fg_color = ui.GREY - text_style = ui.MONO - border_color = ui.BG - radius = ui.RADIUS - - -class ButtonConfirm(ButtonDefault): - class normal(ButtonDefault.normal): - bg_color = ui.GREEN - - class active(ButtonDefault.active): - fg_color = ui.GREEN - - -class ButtonCancel(ButtonDefault): - class normal(ButtonDefault.normal): - bg_color = ui.RED - - class active(ButtonDefault.active): - fg_color = ui.RED - - -class ButtonAbort(ButtonDefault): - class normal(ButtonDefault.normal): - bg_color = ui.DARK_GREY - - class active(ButtonDefault.active): - fg_color = ui.DARK_GREY - - -class ButtonClear(ButtonDefault): - class normal(ButtonDefault.normal): - bg_color = ui.ORANGE - - class active(ButtonDefault.active): - fg_color = ui.ORANGE - - -class ButtonMonoConfirm(ButtonDefault): - class normal(ButtonDefault.normal): - text_style = ui.MONO - bg_color = ui.GREEN - - class active(ButtonDefault.active): - text_style = ui.MONO - fg_color = ui.GREEN - - class disabled(ButtonDefault.disabled): - text_style = ui.MONO - - -# button states -_INITIAL = const(0) -_PRESSED = const(1) -_RELEASED = const(2) -_DISABLED = const(3) - -# button constants -_ICON = const(16) # icon size in pixels -_BORDER = const(4) # border size in pixels - - -class Button(ui.Component): - def __init__( - self, - area: ui.Area, - content: ButtonContent, - style: ButtonStyleType = ButtonDefault, - ) -> None: - super().__init__() - if isinstance(content, str): - self.text = content - self.icon = b"" - elif isinstance(content, bytes): - self.icon = content - self.text = "" - else: - raise TypeError - self.area = area - self.normal_style = style.normal - self.active_style = style.active - self.disabled_style = style.disabled - self.state = _INITIAL - - def enable(self) -> None: - if self.state is not _INITIAL: - self.state = _INITIAL - self.repaint = True - - def disable(self) -> None: - if self.state is not _DISABLED: - self.state = _DISABLED - self.repaint = True - - def on_render(self) -> None: - if self.repaint: - if self.state is _INITIAL or self.state is _RELEASED: - s = self.normal_style - elif self.state is _DISABLED: - s = self.disabled_style - elif self.state is _PRESSED: - s = self.active_style - else: - raise RuntimeError # invalid state - ax, ay, aw, ah = self.area - self.render_background(s, ax, ay, aw, ah) - self.render_content(s, ax, ay, aw, ah) - self.repaint = False - - def render_background( - self, s: ButtonStyleStateType, ax: int, ay: int, aw: int, ah: int - ) -> None: - radius = s.radius - bg_color = s.bg_color - border_color = s.border_color - if border_color == bg_color: - # we don't need to render the border - display.bar_radius(ax, ay, aw, ah, bg_color, ui.BG, radius) - else: - # render border and background on top of it - display.bar_radius(ax, ay, aw, ah, border_color, ui.BG, radius) - display.bar_radius( - ax + _BORDER, - ay + _BORDER, - aw - _BORDER * 2, - ah - _BORDER * 2, - bg_color, - border_color, - radius, - ) - - def render_content( - self, s: ButtonStyleStateType, ax: int, ay: int, aw: int, ah: int - ) -> None: - tx = ax + aw // 2 - ty = ay + ah // 2 + 8 - t = self.text - if t: - display.text_center(tx, ty, t, s.text_style, s.fg_color, s.bg_color) - return - i = self.icon - if i: - display.icon(tx - _ICON // 2, ty - _ICON, i, s.fg_color, s.bg_color) - return - - def on_touch_start(self, x: int, y: int) -> None: - if self.state is _DISABLED: - return - if in_area(self.area, x, y): - self.state = _PRESSED - self.repaint = True - self.on_press_start() - - def on_touch_move(self, x: int, y: int) -> None: - if self.state is _DISABLED: - return - if in_area(self.area, x, y): - if self.state is _RELEASED: - self.state = _PRESSED - self.repaint = True - self.on_press_start() - else: - if self.state is _PRESSED: - self.state = _RELEASED - self.repaint = True - self.on_press_end() - - def on_touch_end(self, x: int, y: int) -> None: - state = self.state - if state is not _INITIAL and state is not _DISABLED: - self.state = _INITIAL - self.repaint = True - if in_area(self.area, x, y): - if state is _PRESSED: - self.on_press_end() - self.on_click() - - def on_press_start(self) -> None: - pass - - def on_press_end(self) -> None: - pass - - def on_click(self) -> None: - pass - - if __debug__: - - def read_content(self) -> list[str]: - return [f""] diff --git a/core/src/trezor/ui/components/tt/checklist.py b/core/src/trezor/ui/components/tt/checklist.py deleted file mode 100644 index de1a2167c..000000000 --- a/core/src/trezor/ui/components/tt/checklist.py +++ /dev/null @@ -1,67 +0,0 @@ -from micropython import const -from typing import TYPE_CHECKING - -from trezor import res, ui - -from ...constants import TEXT_HEADER_HEIGHT, TEXT_LINE_HEIGHT - -if TYPE_CHECKING: - from typing import Iterable - - ChecklistItem = str | Iterable[str] - -_CHECKLIST_MAX_LINES = const(5) -_CHECKLIST_OFFSET_X = const(24) -_CHECKLIST_OFFSET_X_ICON = const(0) - - -class Checklist(ui.Component): - def __init__(self, title: str, icon: str) -> None: - super().__init__() - self.title = title - self.icon = icon - self.items: list[ChecklistItem] = [] - self.active = 0 - - def add(self, item: ChecklistItem) -> None: - self.items.append(item) - - def select(self, active: int) -> None: - self.active = active - - def on_render(self) -> None: - if self.repaint: - ui.header(self.title, self.icon) - self.render_items() - self.repaint = False - - def render_items(self) -> None: - offset_x = _CHECKLIST_OFFSET_X - offset_y = TEXT_HEADER_HEIGHT + TEXT_LINE_HEIGHT - bg = ui.BG - - for index, item in enumerate(self.items): - # compute font and color - if index < self.active: - font = ui.BOLD - fg = ui.GREEN - elif index == self.active: - font = ui.BOLD - fg = ui.FG - else: # index > self.active - font = ui.NORMAL - fg = ui.GREY - - # render item icon in past items - if index < self.active: - icon = res.load(ui.ICON_CONFIRM) - ui.display.icon(0, offset_y - 14, icon, fg, bg) - - # render item text - if isinstance(item, str): - ui.display.text(offset_x, offset_y, item, font, fg, bg) - offset_y += TEXT_LINE_HEIGHT - else: - for line in item: - ui.display.text(offset_x, offset_y, line, font, fg, bg) - offset_y += TEXT_LINE_HEIGHT diff --git a/core/src/trezor/ui/components/tt/confirm.py b/core/src/trezor/ui/components/tt/confirm.py deleted file mode 100644 index bd3b082a5..000000000 --- a/core/src/trezor/ui/components/tt/confirm.py +++ /dev/null @@ -1,257 +0,0 @@ -from micropython import const -from typing import TYPE_CHECKING - -from trezor import loop, res, ui, utils -from trezor.ui.loader import Loader, LoaderDefault - -from ..common.confirm import CANCELLED, CONFIRMED, INFO, ConfirmBase, Pageable -from .button import Button, ButtonAbort, ButtonCancel, ButtonConfirm, ButtonDefault - -if TYPE_CHECKING: - from typing import Any - from .button import ButtonContent, ButtonStyleType - from trezor.ui.loader import LoaderStyleType - - -class Confirm(ConfirmBase): - DEFAULT_CONFIRM = res.load(ui.ICON_CONFIRM) - DEFAULT_CONFIRM_STYLE = ButtonConfirm - DEFAULT_CANCEL = res.load(ui.ICON_CANCEL) - DEFAULT_CANCEL_STYLE = ButtonCancel - - def __init__( - self, - content: ui.Component, - confirm: ButtonContent | None = DEFAULT_CONFIRM, - confirm_style: ButtonStyleType = DEFAULT_CONFIRM_STYLE, - cancel: ButtonContent | None = DEFAULT_CANCEL, - cancel_style: ButtonStyleType = DEFAULT_CANCEL_STYLE, - major_confirm: bool = False, - ) -> None: - self.content = content - button_confirm: Button | None = None - button_cancel: Button | None = None - - if confirm is not None: - if cancel is None: - area = ui.grid(4, n_x=1) - elif major_confirm: - area = ui.grid(13, cells_x=2) - else: - area = ui.grid(9, n_x=2) - button_confirm = Button(area, confirm, confirm_style) - button_confirm.on_click = self.on_confirm - - if cancel is not None: - if confirm is None: - area = ui.grid(4, n_x=1) - elif major_confirm: - area = ui.grid(12, cells_x=1) - else: - area = ui.grid(8, n_x=2) - button_cancel = Button(area, cancel, cancel_style) - button_cancel.on_click = self.on_cancel - - super().__init__(content, button_confirm, button_cancel) - - -class ConfirmPageable(Confirm): - def __init__(self, pageable: Pageable, *args: Any, **kwargs: Any): - super().__init__(*args, **kwargs) - self.pageable = pageable - - async def handle_paging(self) -> None: - from .swipe import SWIPE_HORIZONTAL, SWIPE_LEFT, SWIPE_RIGHT, Swipe - - if self.pageable.is_first(): - directions = SWIPE_LEFT - elif self.pageable.is_last(): - directions = SWIPE_RIGHT - else: - directions = SWIPE_HORIZONTAL - - if __debug__: - from apps.debug import swipe_signal - - swipe = await loop.race(Swipe(directions), swipe_signal()) - else: - swipe = await Swipe(directions) - - if swipe == SWIPE_LEFT: - self.pageable.next() - else: - self.pageable.prev() - - self.content.repaint = True - if self.confirm is not None: - self.confirm.repaint = True - if self.cancel is not None: - self.cancel.repaint = True - - def create_tasks(self) -> tuple[loop.AwaitableTask, ...]: - tasks = super().create_tasks() - if self.pageable.page_count() > 1: - return tasks + (self.handle_paging(),) - else: - return tasks - - def on_render(self) -> None: - PULSE_PERIOD = const(1_200_000) - - super().on_render() - - if not self.pageable.is_first(): - t = ui.pulse(PULSE_PERIOD) - c = ui.blend(ui.GREY, ui.DARK_GREY, t) - icon = res.load(ui.ICON_SWIPE_RIGHT) - if utils.DISABLE_ANIMATION: - ui.display.icon(18, 68, icon, ui.GREY, ui.BG) - else: - ui.display.icon(18, 68, icon, c, ui.BG) - - if not self.pageable.is_last(): - t = ui.pulse(PULSE_PERIOD, PULSE_PERIOD // 2) - c = ui.blend(ui.GREY, ui.DARK_GREY, t) - icon = res.load(ui.ICON_SWIPE_LEFT) - if utils.DISABLE_ANIMATION: - ui.display.icon(205, 68, icon, ui.GREY, ui.BG) - else: - ui.display.icon(205, 68, icon, c, ui.BG) - - -class InfoConfirm(ui.Layout): - DEFAULT_CONFIRM = res.load(ui.ICON_CONFIRM) - DEFAULT_CONFIRM_STYLE = ButtonConfirm - DEFAULT_CANCEL = res.load(ui.ICON_CANCEL) - DEFAULT_CANCEL_STYLE = ButtonCancel - DEFAULT_INFO = res.load(ui.ICON_CLICK) # TODO: this should be (i) icon, not click - DEFAULT_INFO_STYLE = ButtonDefault - - def __init__( - self, - content: ui.Component, - confirm: ButtonContent = DEFAULT_CONFIRM, - confirm_style: ButtonStyleType = DEFAULT_CONFIRM_STYLE, - cancel: ButtonContent = DEFAULT_CANCEL, - cancel_style: ButtonStyleType = DEFAULT_CANCEL_STYLE, - info: ButtonContent = DEFAULT_INFO, - info_style: ButtonStyleType = DEFAULT_INFO_STYLE, - ) -> None: - super().__init__() - self.content = content - - self.confirm = Button(ui.grid(14), confirm, confirm_style) - self.confirm.on_click = self.on_confirm - - self.info = Button(ui.grid(13), info, info_style) - self.info.on_click = self.on_info - - self.cancel = Button(ui.grid(12), cancel, cancel_style) - self.cancel.on_click = self.on_cancel - - def dispatch(self, event: int, x: int, y: int) -> None: - self.content.dispatch(event, x, y) - if self.confirm is not None: - self.confirm.dispatch(event, x, y) - if self.cancel is not None: - self.cancel.dispatch(event, x, y) - if self.info is not None: - self.info.dispatch(event, x, y) - - def on_confirm(self) -> None: - raise ui.Result(CONFIRMED) - - def on_cancel(self) -> None: - raise ui.Result(CANCELLED) - - def on_info(self) -> None: - raise ui.Result(INFO) - - if __debug__: - - def read_content(self) -> list[str]: - return self.content.read_content() - - def create_tasks(self) -> tuple[loop.AwaitableTask, ...]: - from apps.debug import confirm_signal - - return super().create_tasks() + (confirm_signal(),) - - -class HoldToConfirm(ui.Layout): - DEFAULT_CONFIRM = "Hold to confirm" - DEFAULT_CONFIRM_STYLE = ButtonConfirm - DEFAULT_LOADER_STYLE = LoaderDefault - - def __init__( - self, - content: ui.Component, - confirm: str = DEFAULT_CONFIRM, - confirm_style: ButtonStyleType = DEFAULT_CONFIRM_STYLE, - loader_style: LoaderStyleType = DEFAULT_LOADER_STYLE, - cancel: bool = True, - ): - super().__init__() - self.content = content - - self.loader = Loader(loader_style) - self.loader.on_start = self._on_loader_start - - if cancel: - self.confirm = Button(ui.grid(17, n_x=4, cells_x=3), confirm, confirm_style) - else: - self.confirm = Button(ui.grid(4, n_x=1), confirm, confirm_style) - self.confirm.on_press_start = self._on_press_start - self.confirm.on_press_end = self._on_press_end - self.confirm.on_click = self._on_click - - self.cancel = None - if cancel: - self.cancel = Button( - ui.grid(16, n_x=4), res.load(ui.ICON_CANCEL), ButtonAbort - ) - self.cancel.on_click = self.on_cancel - - def _on_press_start(self) -> None: - self.loader.start() - - def _on_press_end(self) -> None: - self.loader.stop() - - def _on_loader_start(self) -> None: - # Loader has either started growing, or returned to the 0-position. - # In the first case we need to clear the content leftovers, in the latter - # we need to render the content again. - ui.display.bar(0, 0, ui.WIDTH, ui.HEIGHT - 58, ui.BG) - self.content.dispatch(ui.REPAINT, 0, 0) - - def _on_click(self) -> None: - if self.loader.elapsed_ms() >= self.loader.target_ms: - self.on_confirm() - - def dispatch(self, event: int, x: int, y: int) -> None: - if self.loader.start_ms is not None: - if utils.DISABLE_ANIMATION: - self.on_confirm() - self.loader.dispatch(event, x, y) - else: - self.content.dispatch(event, x, y) - self.confirm.dispatch(event, x, y) - if self.cancel: - self.cancel.dispatch(event, x, y) - - def on_confirm(self) -> None: - raise ui.Result(CONFIRMED) - - def on_cancel(self) -> None: - raise ui.Result(CANCELLED) - - if __debug__: - - def read_content(self) -> list[str]: - return self.content.read_content() - - def create_tasks(self) -> tuple[loop.AwaitableTask, ...]: - from apps.debug import confirm_signal - - return super().create_tasks() + (confirm_signal(),) diff --git a/core/src/trezor/ui/components/tt/info.py b/core/src/trezor/ui/components/tt/info.py deleted file mode 100644 index 9c3dc08b8..000000000 --- a/core/src/trezor/ui/components/tt/info.py +++ /dev/null @@ -1,87 +0,0 @@ -from typing import TYPE_CHECKING - -from trezor import loop, res, ui - -from ...constants import TEXT_LINE_HEIGHT, TEXT_MARGIN_LEFT -from .button import Button, ButtonConfirm -from .confirm import CONFIRMED -from .text import render_text - -if TYPE_CHECKING: - from .button import ButtonContent - - InfoConfirmStyleType = type["DefaultInfoConfirm"] - - -class DefaultInfoConfirm: - - fg_color = ui.LIGHT_GREY - bg_color = ui.BLACKISH - - class button(ButtonConfirm): - class normal(ButtonConfirm.normal): - border_color = ui.BLACKISH - - class disabled(ButtonConfirm.disabled): - border_color = ui.BLACKISH - - -class InfoConfirm(ui.Layout): - DEFAULT_CONFIRM = res.load(ui.ICON_CONFIRM) - DEFAULT_STYLE = DefaultInfoConfirm - - def __init__( - self, - text: str, - confirm: ButtonContent = DEFAULT_CONFIRM, - style: InfoConfirmStyleType = DEFAULT_STYLE, - ) -> None: - super().__init__() - self.text = [text] - self.style = style - panel_area = ui.grid(0, n_x=1, n_y=1) - self.panel_area = panel_area - confirm_area = ui.grid(4, n_x=1) - self.confirm = Button(confirm_area, confirm, style.button) - self.confirm.on_click = self.on_confirm - - def dispatch(self, event: int, x: int, y: int) -> None: - if event == ui.RENDER: - self.on_render() - self.confirm.dispatch(event, x, y) - - def on_render(self) -> None: - if self.repaint: - x, y, w, h = self.panel_area - fg_color = self.style.fg_color - bg_color = self.style.bg_color - - # render the background panel - ui.display.bar_radius(x, y, w, h, bg_color, ui.BG, ui.RADIUS) - - # render the info text - render_text( - self.text, - new_lines=False, - max_lines=6, - offset_y=y + TEXT_LINE_HEIGHT, - offset_x=x + TEXT_MARGIN_LEFT - ui.VIEWX, - line_width=w - TEXT_MARGIN_LEFT, - fg=fg_color, - bg=bg_color, - ) - - self.repaint = False - - def on_confirm(self) -> None: - raise ui.Result(CONFIRMED) - - if __debug__: - - def read_content(self) -> list[str]: - return self.text - - def create_tasks(self) -> tuple[loop.AwaitableTask, ...]: - from apps.debug import confirm_signal - - return super().create_tasks() + (confirm_signal(),) diff --git a/core/src/trezor/ui/components/tt/keyboard_bip39.py b/core/src/trezor/ui/components/tt/keyboard_bip39.py deleted file mode 100644 index 8aa105f6c..000000000 --- a/core/src/trezor/ui/components/tt/keyboard_bip39.py +++ /dev/null @@ -1,220 +0,0 @@ -from typing import TYPE_CHECKING - -from trezor import io, loop, res, ui, workflow -from trezor.crypto import bip39 -from trezor.ui import display - -from .button import Button, ButtonClear, ButtonMono, ButtonMonoConfirm - -if TYPE_CHECKING: - from .button import ButtonContent, ButtonStyleStateType - - -def compute_mask(text: str) -> int: - mask = 0 - for c in text: - shift = ord(c) - 97 # ord('a') == 97 - if shift < 0: - continue - mask |= 1 << shift - return mask - - -class KeyButton(Button): - def __init__( - self, area: ui.Area, content: ButtonContent, keyboard: "Bip39Keyboard" - ): - self.keyboard = keyboard - super().__init__(area, content) - - def on_click(self) -> None: - self.keyboard.on_key_click(self) - - -class InputButton(Button): - def __init__(self, area: ui.Area, text: str, word: str) -> None: - super().__init__(area, text) - self.word = word - self.pending = False - self.disable() - - def edit(self, text: str, word: str, pending: bool) -> None: - self.word = word - self.text = text - self.pending = pending - self.repaint = True - if word: - if text == word: # confirm button - self.enable() - self.normal_style = ButtonMonoConfirm.normal - self.active_style = ButtonMonoConfirm.active - self.icon = res.load(ui.ICON_CONFIRM) - else: # auto-complete button - self.enable() - self.normal_style = ButtonMono.normal - self.active_style = ButtonMono.active - self.icon = res.load(ui.ICON_CLICK) - else: # disabled button - self.disabled_style = ButtonMono.disabled - self.disable() - self.icon = b"" - - def render_content( - self, s: ButtonStyleStateType, ax: int, ay: int, aw: int, ah: int - ) -> None: - text_style = s.text_style - fg_color = s.fg_color - bg_color = s.bg_color - - tx = ax + 16 # x-offset of the content - ty = ay + ah // 2 + 8 # y-offset of the content - - # entered content - display.text(tx, ty, self.text, text_style, fg_color, bg_color) - # word suggestion - suggested_word = self.word[len(self.text) :] - width = display.text_width(self.text, text_style) - display.text(tx + width, ty, suggested_word, text_style, ui.GREY, bg_color) - - if self.pending: - pw = display.text_width(self.text[-1:], text_style) - px = tx + width - pw - display.bar(px, ty + 2, pw + 1, 3, fg_color) - - if self.icon: - ix = ax + aw - 16 * 2 - iy = ty - 16 - display.icon(ix, iy, self.icon, fg_color, bg_color) - - -class Prompt(ui.Component): - def __init__(self, prompt: str) -> None: - super().__init__() - self.prompt = prompt - - def on_render(self) -> None: - if self.repaint: - display.bar(0, 8, ui.WIDTH, 60, ui.BG) - display.text(20, 40, self.prompt, ui.BOLD, ui.GREY, ui.BG) - self.repaint = False - - -class Bip39Keyboard(ui.Layout): - def __init__(self, prompt: str) -> None: - super().__init__() - self.prompt = Prompt(prompt) - - icon_back = res.load(ui.ICON_BACK) - self.back = Button(ui.grid(0, n_x=3, n_y=4), icon_back, ButtonClear) - self.back.on_click = self.on_back_click - - self.input = InputButton(ui.grid(1, n_x=3, n_y=4, cells_x=2), "", "") - self.input.on_click = self.on_input_click - - self.keys = [ - KeyButton(ui.grid(i + 3, n_y=4), k, self) - for i, k in enumerate( - ("abc", "def", "ghi", "jkl", "mno", "pqr", "stu", "vwx", "yz") - ) - ] - self.pending_button: Button | None = None - self.pending_index = 0 - - def dispatch(self, event: int, x: int, y: int) -> None: - for btn in self.keys: - btn.dispatch(event, x, y) - if self.input.text: - self.input.dispatch(event, x, y) - self.back.dispatch(event, x, y) - else: - self.prompt.dispatch(event, x, y) - - def on_back_click(self) -> None: - # Backspace was clicked, let's delete the last character of input. - self.edit(self.input.text[:-1]) - - def on_input_click(self) -> None: - # Input button was clicked. If the content matches the suggested word, - # let's confirm it, otherwise just auto-complete. - text = self.input.text - word = self.input.word - if word and word == text: - self.edit("") - self.on_confirm(word) - else: - self.edit(word) - - def on_key_click(self, btn: Button) -> None: - # Key button was clicked. If this button is pending, let's cycle the - # pending character in input. If not, let's just append the first - # character. - if self.pending_button is btn: - index = (self.pending_index + 1) % len(btn.text) - text = self.input.text[:-1] + btn.text[index] - else: - index = 0 - text = self.input.text + btn.text[0] - self.edit(text, btn, index) - - def on_timeout(self) -> None: - # Timeout occurred. If we can auto-complete current input, let's just - # reset the pending marker. If not, input is invalid, let's backspace - # the last character. - if self.input.word: - self.edit(self.input.text) - else: - self.edit(self.input.text[:-1]) - - def on_confirm(self, word: str) -> None: - # Word was confirmed by the user. - raise ui.Result(word) - - def edit(self, text: str, button: Button | None = None, index: int = 0) -> None: - self.pending_button = button - self.pending_index = index - - # find the completions - pending = button is not None - word = bip39.complete_word(text) or "" - mask = bip39.word_completion_mask(text) - - # modify the input state - self.input.edit(text, word, pending) - - # enable or disable key buttons - for btn in self.keys: - if btn is button or compute_mask(btn.text) & mask: - btn.enable() - else: - btn.disable() - - # invalidate the prompt if we display it next frame - if not self.input.text: - self.prompt.repaint = True - - async def handle_input(self) -> None: - touch = loop.wait(io.TOUCH) - timeout = loop.sleep(1000) - race_touch = loop.race(touch) - race_timeout = loop.race(touch, timeout) - - while True: - if self.pending_button is not None: - race = race_timeout - else: - race = race_touch - result = await race - - if touch in race.finished: - event, x, y = result - workflow.idle_timer.touch() - self.dispatch(event, x, y) - else: - self.on_timeout() - - if __debug__: - - def create_tasks(self) -> tuple[loop.AwaitableTask, ...]: - from apps.debug import input_signal - - return super().create_tasks() + (input_signal(),) diff --git a/core/src/trezor/ui/components/tt/keyboard_slip39.py b/core/src/trezor/ui/components/tt/keyboard_slip39.py deleted file mode 100644 index 49ad76050..000000000 --- a/core/src/trezor/ui/components/tt/keyboard_slip39.py +++ /dev/null @@ -1,230 +0,0 @@ -from typing import TYPE_CHECKING - -from trezor import io, loop, res, ui, workflow -from trezor.crypto import slip39 -from trezor.ui import display - -from .button import Button, ButtonClear, ButtonMono, ButtonMonoConfirm - -if TYPE_CHECKING: - from .button import ButtonContent, ButtonStyleStateType - - -class KeyButton(Button): - def __init__( - self, - area: ui.Area, - content: ButtonContent, - keyboard: "Slip39Keyboard", - index: int, - ): - self.keyboard = keyboard - self.index = index - super().__init__(area, content) - - def on_click(self) -> None: - self.keyboard.on_key_click(self) - - -class InputButton(Button): - def __init__(self, area: ui.Area, keyboard: "Slip39Keyboard") -> None: - super().__init__(area, "") - self.word = "" - self.pending_button: Button | None = None - self.pending_index: int | None = None - self.keyboard = keyboard - self.disable() - - def edit( - self, - text: str, - word: str, - pending_button: Button | None, - pending_index: int | None, - ) -> None: - self.word = word - self.text = text - self.pending_button = pending_button - self.pending_index = pending_index - self.repaint = True - if word: # confirm button - self.enable() - self.normal_style = ButtonMonoConfirm.normal - self.active_style = ButtonMonoConfirm.active - self.icon = res.load(ui.ICON_CONFIRM) - else: # disabled button - self.disabled_style = ButtonMono.disabled - self.disable() - self.icon = b"" - - def render_content( - self, s: ButtonStyleStateType, ax: int, ay: int, aw: int, ah: int - ) -> None: - text_style = s.text_style - fg_color = s.fg_color - bg_color = s.bg_color - - tx = ax + 16 # x-offset of the content - ty = ay + ah // 2 + 8 # y-offset of the content - - if not self.keyboard.is_input_final(): - pending_button = self.pending_button - pending_index = self.pending_index - to_display = len(self.text) * "*" - if pending_button and pending_index is not None: - to_display = to_display[:-1] + pending_button.text[pending_index] - else: - to_display = self.word - - display.text(tx, ty, to_display, text_style, fg_color, bg_color) - - if self.pending_button and not self.keyboard.is_input_final(): - width = display.text_width(to_display, text_style) - pw = display.text_width(self.text[-1:], text_style) - px = tx + width - pw - display.bar(px, ty + 2, pw + 1, 3, fg_color) - - if self.icon: - ix = ax + aw - 16 * 2 - iy = ty - 16 - display.icon(ix, iy, self.icon, fg_color, bg_color) - - -class Prompt(ui.Component): - def __init__(self, prompt: str) -> None: - super().__init__() - self.prompt = prompt - - def on_render(self) -> None: - if self.repaint: - display.bar(0, 8, ui.WIDTH, 60, ui.BG) - display.text(20, 40, self.prompt, ui.BOLD, ui.GREY, ui.BG) - self.repaint = False - - -class Slip39Keyboard(ui.Layout): - def __init__(self, prompt: str) -> None: - super().__init__() - self.prompt = Prompt(prompt) - - icon_back = res.load(ui.ICON_BACK) - self.back = Button(ui.grid(0, n_x=3, n_y=4), icon_back, ButtonClear) - self.back.on_click = self.on_back_click - - self.input = InputButton(ui.grid(1, n_x=3, n_y=4, cells_x=2), self) - self.input.on_click = self.on_input_click - - self.keys = [ - KeyButton(ui.grid(i + 3, n_y=4), k, self, i + 1) - for i, k in enumerate( - ("ab", "cd", "ef", "ghij", "klm", "nopq", "rs", "tuv", "wxyz") - ) - ] - self.pending_button: Button | None = None - self.pending_index = 0 - self.button_sequence = "" - self.mask = slip39.KEYBOARD_FULL_MASK - - def dispatch(self, event: int, x: int, y: int) -> None: - for btn in self.keys: - btn.dispatch(event, x, y) - if self.input.text: - self.input.dispatch(event, x, y) - self.back.dispatch(event, x, y) - else: - self.prompt.dispatch(event, x, y) - - def on_back_click(self) -> None: - # Backspace was clicked, let's delete the last character of input. - self.button_sequence = self.button_sequence[:-1] - self.edit() - - def on_input_click(self) -> None: - # Input button was clicked. If the content matches the suggested word, - # let's confirm it, otherwise just auto-complete. - result = self.input.word - if self.is_input_final(): - self.button_sequence = "" - self.edit() - self.on_confirm(result) - - def on_key_click(self, btn: KeyButton) -> None: - # Key button was clicked. If this button is pending, let's cycle the - # pending character in input. If not, let's just append the first - # character. - if self.pending_button is btn: - index = (self.pending_index + 1) % len(btn.text) - else: - index = 0 - self.button_sequence += str(btn.index) - self.edit(btn, index) - - def on_timeout(self) -> None: - # Timeout occurred. Let's redraw to draw asterisks. - self.edit() - - def on_confirm(self, word: str) -> None: - # Word was confirmed by the user. - raise ui.Result(word) - - def edit(self, button: Button | None = None, index: int = 0) -> None: - self.pending_button = button - self.pending_index = index - - # find the completions - word = "" - self.mask = slip39.word_completion_mask(self.button_sequence) - if self.is_input_final(): - word = slip39.button_sequence_to_word(self.button_sequence) - - # modify the input state - self.input.edit( - self.button_sequence, word, self.pending_button, self.pending_index - ) - - # enable or disable key buttons - for btn in self.keys: - if self.is_input_final(): - btn.disable() - elif btn is button or self.check_mask(btn.index): - btn.enable() - else: - btn.disable() - - # invalidate the prompt if we display it next frame - if not self.input.text: - self.prompt.repaint = True - - def is_input_final(self) -> bool: - # returns True if mask has exactly one bit set to 1 or is 0 - return not self.mask & (self.mask - 1) - - def check_mask(self, index: int) -> bool: - return bool((1 << (index - 1)) & self.mask) - - async def handle_input(self) -> None: - touch = loop.wait(io.TOUCH) - timeout = loop.sleep(1000) - race_touch = loop.race(touch) - race_timeout = loop.race(touch, timeout) - - while True: - if self.pending_button is not None: - race = race_timeout - else: - race = race_touch - result = await race - - if touch in race.finished: - event, x, y = result - workflow.idle_timer.touch() - self.dispatch(event, x, y) - else: - self.on_timeout() - - if __debug__: - - def create_tasks(self) -> tuple[loop.AwaitableTask, ...]: - from apps.debug import input_signal - - return super().create_tasks() + (input_signal(),) diff --git a/core/src/trezor/ui/components/tt/num_input.py b/core/src/trezor/ui/components/tt/num_input.py deleted file mode 100644 index f519535ac..000000000 --- a/core/src/trezor/ui/components/tt/num_input.py +++ /dev/null @@ -1,51 +0,0 @@ -from trezor import ui - -from .button import Button -from .text import LABEL_CENTER, Label - - -class NumInput(ui.Component): - def __init__(self, count: int = 5, max_count: int = 16, min_count: int = 1) -> None: - super().__init__() - self.count = count - self.max_count = max_count - self.min_count = min_count - - self.minus = Button(ui.grid(3), "-") - self.minus.on_click = self.on_minus - self.plus = Button(ui.grid(5), "+") - self.plus.on_click = self.on_plus - self.text = Label(ui.grid(4), "", LABEL_CENTER, ui.BOLD) - - self.edit(count) - - def dispatch(self, event: int, x: int, y: int) -> None: - self.minus.dispatch(event, x, y) - self.plus.dispatch(event, x, y) - self.text.dispatch(event, x, y) - - def on_minus(self) -> None: - self.edit(self.count - 1) - - def on_plus(self) -> None: - self.edit(self.count + 1) - - def edit(self, count: int) -> None: - count = max(count, self.min_count) - count = min(count, self.max_count) - if self.count != count: - self.on_change(count) - self.count = count - self.text.content = str(count) - self.text.repaint = True - if self.count == self.min_count: - self.minus.disable() - else: - self.minus.enable() - if self.count == self.max_count: - self.plus.disable() - else: - self.plus.enable() - - def on_change(self, count: int) -> None: - pass diff --git a/core/src/trezor/ui/components/tt/passphrase.py b/core/src/trezor/ui/components/tt/passphrase.py deleted file mode 100644 index 5726e5587..000000000 --- a/core/src/trezor/ui/components/tt/passphrase.py +++ /dev/null @@ -1,262 +0,0 @@ -from micropython import const -from typing import TYPE_CHECKING - -from trezor import io, loop, res, ui, workflow -from trezor.ui import display - -from .button import Button, ButtonClear, ButtonConfirm -from .swipe import SWIPE_HORIZONTAL, SWIPE_LEFT, Swipe - -if TYPE_CHECKING: - from typing import Iterable - from .button import ButtonContent, ButtonStyleStateType - -SPACE = res.load(ui.ICON_SPACE) - -KEYBOARD_KEYS = ( - ("1", "2", "3", "4", "5", "6", "7", "8", "9", "0"), - (SPACE, "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz", "*#"), - (SPACE, "ABC", "DEF", "GHI", "JKL", "MNO", "PQRS", "TUV", "WXYZ", "*#"), - ("_<>", ".:@", "/|\\", "!()", "+%&", "-[]", "?{}", ",'`", ';"~', "$^="), -) - - -def digit_area(i: int) -> ui.Area: - if i == 9: # 0-position - i = 10 # display it in the middle - return ui.grid(i + 3) # skip the first line - - -def render_scrollbar(page: int) -> None: - BBOX = const(240) - SIZE = const(8) - pages = len(KEYBOARD_KEYS) - - padding = 12 - if pages * padding > BBOX: - padding = BBOX // pages - - x = (BBOX // 2) - (pages // 2) * padding - Y = const(44) - - for i in range(0, pages): - if i == page: - fg = ui.FG - else: - fg = ui.DARK_GREY - ui.display.bar_radius(x + i * padding, Y, SIZE, SIZE, fg, ui.BG, SIZE // 2) - - -class KeyButton(Button): - def __init__( - self, area: ui.Area, content: ButtonContent, keyboard: "PassphraseKeyboard" - ) -> None: - self.keyboard = keyboard - super().__init__(area, content) - - def on_click(self) -> None: - self.keyboard.on_key_click(self) - - def get_text_content(self) -> str: - if self.text: - return self.text - elif self.icon is SPACE: - return " " - else: - raise TypeError - - -def key_buttons( - keys: Iterable[ButtonContent], keyboard: "PassphraseKeyboard" -) -> list[KeyButton]: - return [KeyButton(digit_area(i), k, keyboard) for i, k in enumerate(keys)] - - -class Input(Button): - def __init__(self, area: ui.Area, text: str) -> None: - super().__init__(area, text) - self.pending = False - self.disable() - - def edit(self, text: str, pending: bool) -> None: - self.text = text - self.pending = pending - self.repaint = True - - def render_content( - self, s: ButtonStyleStateType, ax: int, ay: int, aw: int, ah: int - ) -> None: - text_style = s.text_style - fg_color = s.fg_color - bg_color = s.bg_color - - p = self.pending # should we draw the pending marker? - t = self.text # input content - - tx = ax + 24 # x-offset of the content - ty = ay + ah // 2 + 8 # y-offset of the content - maxlen = const(14) # maximum text length - - # input content - if len(t) > maxlen: - t = "<" + t[-maxlen:] # too long, align to the right - width = display.text_width(t, text_style) - display.text(tx, ty, t, text_style, fg_color, bg_color) - - if p: # pending marker - pw = display.text_width(t[-1:], text_style) - px = tx + width - pw - display.bar(px, ty + 2, pw + 1, 3, fg_color) - else: # cursor - cx = tx + width + 1 - display.bar(cx, ty - 18, 2, 22, fg_color) - - def on_click(self) -> None: - pass - - -class Prompt(ui.Component): - def __init__(self, text: str) -> None: - super().__init__() - self.text = text - - def on_render(self) -> None: - if self.repaint: - display.bar(0, 0, ui.WIDTH, 48, ui.BG) - display.text_center(ui.WIDTH // 2, 32, self.text, ui.BOLD, ui.GREY, ui.BG) - self.repaint = False - - -CANCELLED = object() - - -class PassphraseKeyboard(ui.Layout): - def __init__(self, prompt: str, max_length: int, page: int = 1) -> None: - super().__init__() - self.prompt = Prompt(prompt) - self.max_length = max_length - self.page = page - - self.input = Input(ui.grid(0, n_x=1, n_y=6), "") - - self.back = Button(ui.grid(12), res.load(ui.ICON_BACK), ButtonClear) - self.back.on_click = self.on_back_click - self.back.disable() - - self.done = Button(ui.grid(14), res.load(ui.ICON_CONFIRM), ButtonConfirm) - self.done.on_click = self.on_confirm - - self.keys = key_buttons(KEYBOARD_KEYS[self.page], self) - self.pending_button: KeyButton | None = None - self.pending_index = 0 - - def dispatch(self, event: int, x: int, y: int) -> None: - if self.input.text: - self.input.dispatch(event, x, y) - else: - self.prompt.dispatch(event, x, y) - self.back.dispatch(event, x, y) - self.done.dispatch(event, x, y) - for btn in self.keys: - btn.dispatch(event, x, y) - - if event == ui.RENDER: - render_scrollbar(self.page) - - def on_back_click(self) -> None: - # Backspace was clicked. If we have any content in the input, let's delete - # the last character. Otherwise cancel. - text = self.input.text - if text: - self.edit(text[:-1]) - else: - self.on_cancel() - - def on_key_click(self, button: KeyButton) -> None: - # Key button was clicked. If this button is pending, let's cycle the - # pending character in input. If not, let's just append the first - # character. - button_text = button.get_text_content() - if self.pending_button is button: - index = (self.pending_index + 1) % len(button_text) - prefix = self.input.text[:-1] - else: - index = 0 - prefix = self.input.text - if len(button_text) > 1: - self.edit(prefix + button_text[index], button, index) - else: - self.edit(prefix + button_text[index]) - - def on_timeout(self) -> None: - # Timeout occurred, let's just reset the pending marker. - self.edit(self.input.text) - - def edit(self, text: str, button: KeyButton | None = None, index: int = 0) -> None: - if len(text) > self.max_length: - return - - self.pending_button = button - self.pending_index = index - - # modify the input state - pending = button is not None - self.input.edit(text, pending) - - if text: - self.back.enable() - else: - self.back.disable() - self.prompt.repaint = True - - async def handle_input(self) -> None: - touch = loop.wait(io.TOUCH) - timeout = loop.sleep(1000) - race_touch = loop.race(touch) - race_timeout = loop.race(touch, timeout) - - while True: - if self.pending_button is not None: - race = race_timeout - else: - race = race_touch - result = await race - - if touch in race.finished: - event, x, y = result - workflow.idle_timer.touch() - self.dispatch(event, x, y) - else: - self.on_timeout() - - async def handle_paging(self) -> None: - swipe = await Swipe(SWIPE_HORIZONTAL) - if swipe == SWIPE_LEFT: - self.page = (self.page + 1) % len(KEYBOARD_KEYS) - else: - self.page = (self.page - 1) % len(KEYBOARD_KEYS) - self.keys = key_buttons(KEYBOARD_KEYS[self.page], self) - self.back.repaint = True - self.done.repaint = True - self.input.repaint = True - self.prompt.repaint = True - - def on_cancel(self) -> None: - raise ui.Result(CANCELLED) - - def on_confirm(self) -> None: - raise ui.Result(self.input.text) - - def create_tasks(self) -> tuple[loop.AwaitableTask, ...]: - tasks: tuple[loop.Task, ...] = ( - self.handle_input(), - self.handle_rendering(), - self.handle_paging(), - ) - - if __debug__: - from apps.debug import input_signal - - return tasks + (input_signal(),) - else: - return tasks diff --git a/core/src/trezor/ui/components/tt/pin.py b/core/src/trezor/ui/components/tt/pin.py deleted file mode 100644 index 1fdc97bfc..000000000 --- a/core/src/trezor/ui/components/tt/pin.py +++ /dev/null @@ -1,183 +0,0 @@ -from micropython import const -from typing import TYPE_CHECKING - -from trezor import config, res, ui -from trezor.crypto import random -from trezor.ui import display - -from .button import Button, ButtonCancel, ButtonClear, ButtonConfirm, ButtonMono - -if TYPE_CHECKING: - from trezor import loop - from typing import Iterable - - -def digit_area(i: int) -> ui.Area: - if i == 9: # 0-position - i = 10 # display it in the middle - return ui.grid(i + 3) # skip the first line - - -def generate_digits() -> Iterable[int]: - digits = list(range(0, 10)) # 0-9 - random.shuffle(digits) - # We lay out the buttons top-left to bottom-right, but the order - # of the digits is defined as bottom-left to top-right (on numpad). - return digits[6:] + digits[3:6] + digits[:3] - - -class PinInput(ui.Component): - def __init__(self, prompt: str, subprompt: str | None, pin: str) -> None: - super().__init__() - self.prompt = prompt - self.subprompt = subprompt - self.pin = pin - - def on_render(self) -> None: - if self.repaint: - if self.pin: - self.render_pin() - else: - self.render_prompt() - self.repaint = False - - def render_pin(self) -> None: - MAX_LENGTH = const(14) # maximum length of displayed PIN - CONTD_MARK = "<" - BOX_WIDTH = const(240) - DOT_SIZE = const(10) - PADDING = const(4) - RENDER_Y = const(20) - TWITCH = const(3) - - display.bar(0, 0, ui.WIDTH, 50, ui.BG) - - if len(self.pin) > MAX_LENGTH: - contd_width = display.text_width(CONTD_MARK, ui.BOLD) + PADDING - twitch = TWITCH * (len(self.pin) % 2) - else: - contd_width = 0 - twitch = 0 - - count = min(len(self.pin), MAX_LENGTH) - render_x = (BOX_WIDTH - count * (DOT_SIZE + PADDING) - contd_width) // 2 - - if contd_width: - display.text( - render_x, RENDER_Y + DOT_SIZE, CONTD_MARK, ui.BOLD, ui.GREY, ui.BG - ) - - for i in range(0, count): - display.bar_radius( - render_x + contd_width + twitch + i * (DOT_SIZE + PADDING), - RENDER_Y, - DOT_SIZE, - DOT_SIZE, - ui.GREY, - ui.BG, - 4, - ) - - def render_prompt(self) -> None: - display.bar(0, 0, ui.WIDTH, 50, ui.BG) - if self.subprompt: - display.text_center(ui.WIDTH // 2, 20, self.prompt, ui.BOLD, ui.GREY, ui.BG) - display.text_center( - ui.WIDTH // 2, 46, self.subprompt, ui.NORMAL, ui.GREY, ui.BG - ) - else: - display.text_center(ui.WIDTH // 2, 36, self.prompt, ui.BOLD, ui.GREY, ui.BG) - - -class PinButton(Button): - def __init__(self, index: int, digit: int, dialog: "PinDialog"): - self.dialog = dialog - super().__init__(digit_area(index), str(digit), ButtonMono) - - def on_click(self) -> None: - self.dialog.assign(self.dialog.input.pin + self.text) - - -CANCELLED = object() - - -class PinDialog(ui.Layout): - def __init__( - self, - prompt: str, - subprompt: str | None, - allow_cancel: bool = True, - maxlength: int = 50, - ) -> None: - super().__init__() - self.maxlength = maxlength - self.input = PinInput(prompt, subprompt, "") - - icon_confirm = res.load(ui.ICON_CONFIRM) - self.confirm_button = Button(ui.grid(14), icon_confirm, ButtonConfirm) - self.confirm_button.on_click = self.on_confirm - self.confirm_button.disable() - - icon_back = res.load(ui.ICON_BACK) - self.reset_button = Button(ui.grid(12), icon_back, ButtonClear) - self.reset_button.on_click = self.on_reset - - if allow_cancel: - icon_lock = res.load( - ui.ICON_CANCEL if config.is_unlocked() else ui.ICON_LOCK - ) - self.cancel_button = Button(ui.grid(12), icon_lock, ButtonCancel) - self.cancel_button.on_click = self.on_cancel - else: - self.cancel_button = Button(ui.grid(12), "") - self.cancel_button.disable() - - self.pin_buttons = [ - PinButton(i, d, self) for i, d in enumerate(generate_digits()) - ] - - def dispatch(self, event: int, x: int, y: int) -> None: - self.input.dispatch(event, x, y) - if self.input.pin: - self.reset_button.dispatch(event, x, y) - else: - self.cancel_button.dispatch(event, x, y) - self.confirm_button.dispatch(event, x, y) - for btn in self.pin_buttons: - btn.dispatch(event, x, y) - - def assign(self, pin: str) -> None: - if len(pin) > self.maxlength: - return - for btn in self.pin_buttons: - if len(pin) < self.maxlength: - btn.enable() - else: - btn.disable() - if pin: - self.confirm_button.enable() - self.reset_button.enable() - self.cancel_button.disable() - else: - self.confirm_button.disable() - self.reset_button.disable() - self.cancel_button.enable() - self.input.pin = pin - self.input.repaint = True - - def on_reset(self) -> None: - self.assign("") - - def on_cancel(self) -> None: - raise ui.Result(CANCELLED) - - def on_confirm(self) -> None: - if self.input.pin: - raise ui.Result(self.input.pin) - - if __debug__: - - def create_tasks(self) -> tuple[loop.AwaitableTask, ...]: - from apps.debug import input_signal - - return super().create_tasks() + (input_signal(),) diff --git a/core/src/trezor/ui/components/tt/recovery.py b/core/src/trezor/ui/components/tt/recovery.py deleted file mode 100644 index 480e8c7d5..000000000 --- a/core/src/trezor/ui/components/tt/recovery.py +++ /dev/null @@ -1,41 +0,0 @@ -from trezor import ui - - -class RecoveryHomescreen(ui.Component): - def __init__(self, dry_run: bool, text: str, subtext: str | None = None): - super().__init__() - self.text = text - self.subtext = subtext - self.dry_run = dry_run - - def on_render(self) -> None: - if not self.repaint: - return - - if self.dry_run: - heading = "SEED CHECK" - else: - heading = "RECOVERY MODE" - ui.header_warning(heading) - - if not self.subtext: - ui.display.text_center(ui.WIDTH // 2, 80, self.text, ui.BOLD, ui.FG, ui.BG) - else: - ui.display.text_center(ui.WIDTH // 2, 65, self.text, ui.BOLD, ui.FG, ui.BG) - ui.display.text_center( - ui.WIDTH // 2, 92, self.subtext, ui.NORMAL, ui.FG, ui.BG - ) - - ui.display.text_center( - ui.WIDTH // 2, 130, "It is safe to eject Trezor", ui.NORMAL, ui.GREY, ui.BG - ) - ui.display.text_center( - ui.WIDTH // 2, 155, "and continue later", ui.NORMAL, ui.GREY, ui.BG - ) - - self.repaint = False - - if __debug__: - - def read_content(self) -> list[str]: - return [self.__class__.__name__, self.text, self.subtext or ""] diff --git a/core/src/trezor/ui/components/tt/reset.py b/core/src/trezor/ui/components/tt/reset.py deleted file mode 100644 index 8105090ed..000000000 --- a/core/src/trezor/ui/components/tt/reset.py +++ /dev/null @@ -1,156 +0,0 @@ -from typing import TYPE_CHECKING - -from trezor import ui - -from .button import Button -from .num_input import NumInput -from .text import Text - -if TYPE_CHECKING: - from trezor import loop - from typing import Callable, NoReturn, Sequence - -if __debug__: - from apps import debug - - -class Slip39NumInput(ui.Component): - SET_SHARES = object() - SET_THRESHOLD = object() - SET_GROUPS = object() - SET_GROUP_THRESHOLD = object() - - def __init__( - self, - step: object, - count: int, - min_count: int, - max_count: int, - group_id: int | None = None, - ) -> None: - super().__init__() - self.step = step - self.input = NumInput(count, min_count=min_count, max_count=max_count) - self.input.on_change = self.on_change - self.group_id = group_id - - def dispatch(self, event: int, x: int, y: int) -> None: - self.input.dispatch(event, x, y) - if event is ui.RENDER: - self.on_render() - - def on_render(self) -> None: - if self.repaint: - count = self.input.count - - # render the headline - if self.step is Slip39NumInput.SET_SHARES: - header = "Set num. of shares" - elif self.step is Slip39NumInput.SET_THRESHOLD: - header = "Set threshold" - elif self.step is Slip39NumInput.SET_GROUPS: - header = "Set num. of groups" - elif self.step is Slip39NumInput.SET_GROUP_THRESHOLD: - header = "Set group threshold" - else: - raise RuntimeError # invalid step - ui.header(header, ui.ICON_RESET, ui.TITLE_GREY, ui.BG, ui.ORANGE_ICON) - - # render the counter - if self.step is Slip39NumInput.SET_SHARES: - if self.group_id is None: - if count == 1: - first_line_text = "Only one share will" - second_line_text = "be created." - else: - first_line_text = f"{count} people or locations" - second_line_text = "will each hold one share." - else: - first_line_text = "Set the total number of" - second_line_text = f"shares in Group {self.group_id + 1}." - ui.display.bar(0, 110, ui.WIDTH, 52, ui.BG) - ui.display.text(12, 130, first_line_text, ui.NORMAL, ui.FG, ui.BG) - ui.display.text(12, 156, second_line_text, ui.NORMAL, ui.FG, ui.BG) - elif self.step is Slip39NumInput.SET_THRESHOLD: - if self.group_id is None: - first_line_text = "For recovery you need" - if count == 1: - second_line_text = "1 share." - elif count == self.input.max_count: - second_line_text = f"all {count} of the shares." - else: - second_line_text = f"any {count} of the shares." - else: - first_line_text = "The required number of " - second_line_text = f"shares to form Group {self.group_id + 1}." - ui.display.bar(0, 110, ui.WIDTH, 52, ui.BG) - ui.display.text(12, 130, first_line_text, ui.NORMAL, ui.FG, ui.BG) - ui.display.text(12, 156, second_line_text, ui.NORMAL, ui.FG, ui.BG) - elif self.step is Slip39NumInput.SET_GROUPS: - ui.display.bar(0, 110, ui.WIDTH, 52, ui.BG) - ui.display.text( - 12, 130, "A group is made up of", ui.NORMAL, ui.FG, ui.BG - ) - ui.display.text(12, 156, "recovery shares.", ui.NORMAL, ui.FG, ui.BG) - elif self.step is Slip39NumInput.SET_GROUP_THRESHOLD: - ui.display.bar(0, 110, ui.WIDTH, 52, ui.BG) - ui.display.text( - 12, 130, "The required number of", ui.NORMAL, ui.FG, ui.BG - ) - ui.display.text( - 12, 156, "groups for recovery.", ui.NORMAL, ui.FG, ui.BG - ) - - self.repaint = False - - def on_change(self, count: int) -> None: - self.repaint = True - - -class MnemonicWordSelect(ui.Layout): - NUM_OF_CHOICES = 3 - - def __init__( - self, - words: Sequence[str], - share_index: int | None, - word_index: int, - count: int, - group_index: int | None = None, - ) -> None: - super().__init__() - self.words = words - self.share_index = share_index - self.word_index = word_index - self.buttons = [] - for i, word in enumerate(words): - area = ui.grid(i + 2, n_x=1) - btn = Button(area, word) - btn.on_click = self.select(word) - self.buttons.append(btn) - if share_index is None: - self.text: ui.Component = Text("Check seed") - elif group_index is None: - self.text = Text(f"Check share #{share_index + 1}") - else: - self.text = Text(f"Check G{group_index + 1} - Share {share_index + 1}") - self.text.normal(f"Select word {word_index + 1} of {count}:") - - def dispatch(self, event: int, x: int, y: int) -> None: - for btn in self.buttons: - btn.dispatch(event, x, y) - self.text.dispatch(event, x, y) - - def select(self, word: str) -> Callable: - def fn() -> NoReturn: - raise ui.Result(word) - - return fn - - if __debug__: - - def read_content(self) -> list[str]: - return self.text.read_content() + [b.text for b in self.buttons] - - def create_tasks(self) -> tuple[loop.AwaitableTask, ...]: - return super().create_tasks() + (debug.input_signal(),) diff --git a/core/src/trezor/ui/components/tt/scroll.py b/core/src/trezor/ui/components/tt/scroll.py deleted file mode 100644 index f49f0f0a6..000000000 --- a/core/src/trezor/ui/components/tt/scroll.py +++ /dev/null @@ -1,430 +0,0 @@ -from micropython import const -from typing import TYPE_CHECKING - -from trezor import loop, res, ui, utils, wire, workflow -from trezor.enums import ButtonRequestType -from trezor.messages import ButtonAck, ButtonRequest - -from ..common.confirm import CANCELLED, CONFIRMED, GO_BACK, SHOW_PAGINATED -from .button import Button, ButtonCancel, ButtonConfirm, ButtonDefault -from .confirm import Confirm -from .swipe import SWIPE_DOWN, SWIPE_UP, SWIPE_VERTICAL, Swipe -from .text import ( - LINE_WIDTH_PAGINATED, - TEXT_MAX_LINES, - TEXT_MAX_LINES_NO_HEADER, - Span, - Text, -) - -if TYPE_CHECKING: - from typing import Any, Callable, Iterable - - from ..common.text import TextContent - - -WAS_PAGED = object() - - -def render_scrollbar(pages: int, page: int) -> None: - BBOX = const(220) - SIZE = const(8) - - padding = 14 - if pages * padding > BBOX: - padding = BBOX // pages - - X = const(220) - Y = (BBOX // 2) - (pages // 2) * padding - - for i in range(0, pages): - if i == page: - fg = ui.FG - else: - fg = ui.GREY - ui.display.bar_radius(X, Y + i * padding, SIZE, SIZE, fg, ui.BG, 4) - - -def render_swipe_icon(x_offset: int = 0) -> None: - if utils.DISABLE_ANIMATION: - c = ui.GREY - else: - PULSE_PERIOD = const(1_200_000) - t = ui.pulse(PULSE_PERIOD) - c = ui.blend(ui.GREY, ui.DARK_GREY, t) - - icon = res.load(ui.ICON_SWIPE) - ui.display.icon(70 + x_offset, 205, icon, c, ui.BG) - - -def render_swipe_text(x_offset: int = 0) -> None: - ui.display.text_center(130 + x_offset, 220, "Swipe", ui.BOLD, ui.GREY, ui.BG) - - -class Paginated(ui.Layout): - def __init__( - self, pages: list[ui.Component], page: int = 0, back_button: bool = False - ): - super().__init__() - self.pages = pages - self.page = page - self.back_button = None - if back_button: - area = ui.grid(16, n_x=4) - icon = res.load(ui.ICON_BACK) - self.back_button = Button(area, icon, ButtonDefault) - self.back_button.on_click = self.on_back_click - - def dispatch(self, event: int, x: int, y: int) -> None: - pages = self.pages - page = self.page - length = len(pages) - last_page = page >= length - 1 - x_offset = 0 - - pages[page].dispatch(event, x, y) - if self.back_button is not None and not last_page: - self.back_button.dispatch(event, x, y) - x_offset = 30 - - if event is ui.REPAINT: - self.repaint = True - elif event is ui.RENDER: - if not last_page: - render_swipe_icon(x_offset=x_offset) - if self.repaint: - render_swipe_text(x_offset=x_offset) - if self.repaint: - render_scrollbar(length, page) - self.repaint = False - - async def handle_paging(self) -> None: - if self.page == 0: - directions = SWIPE_UP - elif self.page == len(self.pages) - 1: - directions = SWIPE_DOWN - else: - directions = SWIPE_VERTICAL - - if __debug__: - from apps.debug import swipe_signal - - swipe = await loop.race(Swipe(directions), swipe_signal()) - else: - swipe = await Swipe(directions) - - if swipe is SWIPE_UP: - self.page = min(self.page + 1, len(self.pages) - 1) - elif swipe is SWIPE_DOWN: - self.page = max(self.page - 1, 0) - - self.on_change() - raise ui.Result(WAS_PAGED) - - async def interact( - self, - ctx: wire.GenericContext, - code: ButtonRequestType = ButtonRequestType.Other, - ) -> Any: - workflow.close_others() - await ctx.call(ButtonRequest(code=code, pages=len(self.pages)), ButtonAck) - result = WAS_PAGED - while result is WAS_PAGED: - result = await ctx.wait(self) - - return result - - def create_tasks(self) -> tuple[loop.AwaitableTask, ...]: - tasks: tuple[loop.AwaitableTask, ...] = ( - self.handle_input(), - self.handle_rendering(), - self.handle_paging(), - ) - - if __debug__: - # XXX This isn't strictly correct, as it allows *any* Paginated layout to be - # shut down by a DebugLink confirm, even if used outside of a confirm() call - # But we don't have any such usages in the codebase, and it doesn't actually - # make much sense to use a Paginated without a way to confirm it. - from apps.debug import confirm_signal - - return tasks + (confirm_signal(),) - else: - return tasks - - def on_change(self) -> None: - pass - - def on_back_click(self) -> None: - raise ui.Result(GO_BACK) - - if __debug__: - - def read_content(self) -> list[str]: - return self.pages[self.page].read_content() - - -class AskPaginated(ui.Component): - def __init__(self, content: ui.Component, button_text: str = "Show all") -> None: - super().__init__() - self.content = content - self.button = Button(ui.grid(3, n_x=1), button_text, ButtonDefault) - self.button.on_click = self.on_show_paginated_click - - def dispatch(self, event: int, x: int, y: int) -> None: - self.content.dispatch(event, x, y) - self.button.dispatch(event, x, y) - - def on_show_paginated_click(self) -> None: - raise ui.Result(SHOW_PAGINATED) - - if __debug__: - - def read_content(self) -> list[str]: - return self.content.read_content() - - -class PageWithButtons(ui.Component): - def __init__( - self, - content: ui.Component, - paginated: "PaginatedWithButtons", - index: int, - count: int, - ) -> None: - super().__init__() - self.content = content - self.paginated = paginated - self.index = index - self.count = count - - # somewhere in the middle, we can go up or down - left = res.load(ui.ICON_BACK) - left_style = ButtonDefault - right = res.load(ui.ICON_CLICK) - right_style = ButtonDefault - - if self.index == 0: - # first page, we can cancel or go down - left = res.load(ui.ICON_CANCEL) - left_style = ButtonCancel - right = res.load(ui.ICON_CLICK) - right_style = ButtonDefault - elif self.index == count - 1: - # last page, we can go up or confirm - left = res.load(ui.ICON_BACK) - left_style = ButtonDefault - right = res.load(ui.ICON_CONFIRM) - right_style = ButtonConfirm - - self.left = Button(ui.grid(8, n_x=2), left, left_style) - self.left.on_click = self.on_left - - self.right = Button(ui.grid(9, n_x=2), right, right_style) - self.right.on_click = self.on_right - - def dispatch(self, event: int, x: int, y: int) -> None: - self.content.dispatch(event, x, y) - self.left.dispatch(event, x, y) - self.right.dispatch(event, x, y) - - def on_left(self) -> None: - if self.index == 0: - self.paginated.on_cancel() - else: - self.paginated.on_up() - - def on_right(self) -> None: - if self.index == self.count - 1: - self.paginated.on_confirm() - else: - self.paginated.on_down() - - if __debug__: - - def read_content(self) -> list[str]: - return self.content.read_content() - - -class PaginatedWithButtons(ui.Layout): - def __init__( - self, pages: list[ui.Component], page: int = 0, one_by_one: bool = False - ) -> None: - super().__init__() - self.pages = [ - PageWithButtons(p, self, i, len(pages)) for i, p in enumerate(pages) - ] - self.page = page - self.one_by_one = one_by_one - - def dispatch(self, event: int, x: int, y: int) -> None: - pages = self.pages - page = self.page - pages[page].dispatch(event, x, y) - if event is ui.RENDER: - render_scrollbar(len(pages), page) - - def on_up(self) -> None: - self.page = max(self.page - 1, 0) - self.pages[self.page].dispatch(ui.REPAINT, 0, 0) - self.on_change() - - def on_down(self) -> None: - self.page = min(self.page + 1, len(self.pages) - 1) - self.pages[self.page].dispatch(ui.REPAINT, 0, 0) - self.on_change() - - def on_confirm(self) -> None: - raise ui.Result(CONFIRMED) - - def on_cancel(self) -> None: - raise ui.Result(CANCELLED) - - def on_change(self) -> None: - if self.one_by_one: - raise ui.Result(self.page) - - if __debug__: - - def read_content(self) -> list[str]: - return self.pages[self.page].read_content() - - def create_tasks(self) -> tuple[loop.AwaitableTask, ...]: - from apps.debug import confirm_signal - - return super().create_tasks() + (confirm_signal(),) - - -def paginate_text( - text: str, - header: str, - font: int = ui.NORMAL, - header_icon: str = ui.ICON_DEFAULT, - icon_color: int = ui.ORANGE_ICON, - break_words: bool = False, - confirm: Callable[[ui.Component], ui.Layout] = Confirm, -) -> ui.Layout: - span = Span(text, 0, font, break_words=break_words) - if span.count_lines() <= TEXT_MAX_LINES: - result = Text( - header, - header_icon=header_icon, - icon_color=icon_color, - new_lines=False, - break_words=break_words, - ) - result.content = [font, text] - return confirm(result) - - else: - pages: list[ui.Component] = [] - span.reset( - text, 0, font, break_words=break_words, line_width=LINE_WIDTH_PAGINATED - ) - while span.has_more_content(): - # advance to first line of the page - span.next_line() - page = Text( - header, - header_icon=header_icon, - icon_color=icon_color, - new_lines=False, - content_offset=0, - char_offset=span.start, - line_width=LINE_WIDTH_PAGINATED, - break_words=break_words, - render_page_overflow=False, - ) - page.content = [font, text] - pages.append(page) - - # roll over the remaining lines on the page - for _ in range(TEXT_MAX_LINES - 1): - span.next_line() - - pages[-1] = confirm(pages[-1]) - return Paginated(pages) - - -PAGEBREAK = 0, "" - - -def paginate_paragraphs( - para: Iterable[tuple[int, str]], - header: str | None, - header_icon: str = ui.ICON_DEFAULT, - icon_color: int = ui.ORANGE_ICON, - break_words: bool = False, - confirm: Callable[[ui.Component], ui.Layout] = Confirm, - back_button: bool = False, -) -> ui.Layout: - span = Span("", 0, ui.NORMAL, break_words=break_words) - lines = 0 - content: list[TextContent] = [] - max_lines = TEXT_MAX_LINES_NO_HEADER if header is None else TEXT_MAX_LINES - for item in para: - if item is PAGEBREAK: - continue - span.reset(item[1], 0, item[0], break_words=break_words) - lines += span.count_lines() - - # we'll need this for multipage too - if content: - content.append("\n") - content.extend(item) - - if lines <= max_lines: - result = Text( - header, - header_icon=header_icon, - icon_color=icon_color, - new_lines=False, - break_words=break_words, - ) - result.content = content - return confirm(result) - - else: - pages: list[ui.Component] = [] - lines_left = 0 - content_ctr = 0 - page: Text | None = None - for item in para: - if item is PAGEBREAK: - if page is not None: - page.max_lines -= lines_left - lines_left = 0 - continue - - span.reset( - item[1], - 0, - item[0], - break_words=break_words, - line_width=LINE_WIDTH_PAGINATED, - ) - - while span.has_more_content(): - span.next_line() - if lines_left <= 0: - page = Text( - header, - header_icon=header_icon, - icon_color=icon_color, - new_lines=False, - content_offset=content_ctr * 3 + 1, # font, _text_, newline - char_offset=span.start, - line_width=LINE_WIDTH_PAGINATED, - render_page_overflow=False, - break_words=break_words, - ) - page.content = content - pages.append(page) - lines_left = max_lines - 1 - else: - lines_left -= 1 - - content_ctr += 1 - - pages[-1] = confirm(pages[-1]) - return Paginated(pages, back_button=back_button) diff --git a/core/src/trezor/ui/components/tt/swipe.py b/core/src/trezor/ui/components/tt/swipe.py deleted file mode 100644 index c321a30fd..000000000 --- a/core/src/trezor/ui/components/tt/swipe.py +++ /dev/null @@ -1,114 +0,0 @@ -from micropython import const -from typing import Generator - -from trezor import io, loop, ui -from trezor.ui.components.common import SWIPE_DOWN, SWIPE_LEFT, SWIPE_RIGHT, SWIPE_UP - -SWIPE_VERTICAL = SWIPE_UP | SWIPE_DOWN -SWIPE_HORIZONTAL = SWIPE_LEFT | SWIPE_RIGHT -SWIPE_ALL = SWIPE_VERTICAL | SWIPE_HORIZONTAL - -_SWIPE_DISTANCE = const(120) -_SWIPE_TRESHOLD = const(30) - - -class Swipe(ui.Component): - def __init__( - self, directions: int = SWIPE_ALL, area: ui.Area | None = None - ) -> None: - super().__init__() - if area is None: - area = (0, 0, ui.WIDTH, ui.HEIGHT) - self.area = area - self.directions = directions - self.started = False - self.start_x = 0 - self.start_y = 0 - self.light_origin = ui.BACKLIGHT_NORMAL - self.light_target = ui.BACKLIGHT_NONE - - def on_touch_start(self, x: int, y: int) -> None: - if ui.in_area(self.area, x, y): - self.start_x = x - self.start_y = y - self.light_origin = ui.BACKLIGHT_NORMAL - self.started = True - - def on_touch_move(self, x: int, y: int) -> None: - if not self.started: - return # not started in our area - - dirs = self.directions - pdx = x - self.start_x - pdy = y - self.start_y - pdxa = abs(pdx) - pdya = abs(pdy) - if pdxa > pdya and dirs & SWIPE_HORIZONTAL: - # horizontal direction - if (pdx > 0 and dirs & SWIPE_RIGHT) or (pdx < 0 and dirs & SWIPE_LEFT): - ui.display.backlight( - ui.lerpi( - self.light_origin, - self.light_target, - min(pdxa / _SWIPE_DISTANCE, 1), - ) - ) - elif pdxa < pdya and dirs & SWIPE_VERTICAL: - # vertical direction - if (pdy > 0 and dirs & SWIPE_DOWN) or (pdy < 0 and dirs & SWIPE_UP): - ui.display.backlight( - ui.lerpi( - self.light_origin, - self.light_target, - min(pdya / _SWIPE_DISTANCE, 1), - ) - ) - - def on_touch_end(self, x: int, y: int) -> None: - if not self.started: - return # not started in our area - - dirs = self.directions - pdx = x - self.start_x - pdy = y - self.start_y - pdxa = abs(pdx) - pdya = abs(pdy) - if pdxa > pdya and dirs & SWIPE_HORIZONTAL: - # horizontal direction - ratio = min(pdxa / _SWIPE_DISTANCE, 1) - if ratio * 100 >= _SWIPE_TRESHOLD: - if pdx > 0 and dirs & SWIPE_RIGHT: - self.on_swipe(SWIPE_RIGHT) - return - elif pdx < 0 and dirs & SWIPE_LEFT: - self.on_swipe(SWIPE_LEFT) - return - elif pdxa < pdya and dirs & SWIPE_VERTICAL: - # vertical direction - ratio = min(pdya / _SWIPE_DISTANCE, 1) - if ratio * 100 >= _SWIPE_TRESHOLD: - if pdy > 0 and dirs & SWIPE_DOWN: - self.on_swipe(SWIPE_DOWN) - return - elif pdy < 0 and dirs & SWIPE_UP: - self.on_swipe(SWIPE_UP) - return - - # no swipe detected, reset the state - ui.display.backlight(self.light_origin) - self.started = False - - def on_swipe(self, swipe: int) -> None: - raise ui.Result(swipe) - - def __await__(self) -> Generator: - return self.__iter__() # type: ignore [Expression of type "Task" cannot be assigned to return type "Generator[Unknown, Unknown, Unknown]"] - - def __iter__(self) -> loop.Task: # type: ignore [awaitable-is-generator] - try: - touch = loop.wait(io.TOUCH) - while True: - event, x, y = yield touch - self.dispatch(event, x, y) - except ui.Result as result: - return result.value diff --git a/core/src/trezor/ui/components/tt/text.py b/core/src/trezor/ui/components/tt/text.py deleted file mode 100644 index ce724126c..000000000 --- a/core/src/trezor/ui/components/tt/text.py +++ /dev/null @@ -1,145 +0,0 @@ -from micropython import const - -from trezor import res, ui -from trezor.ui import display, style - -from ..common.text import ( # noqa: F401 - BR, - BR_HALF, - LINE_WIDTH, - LINE_WIDTH_PAGINATED, - TEXT_HEADER_HEIGHT, - TEXT_LINE_HEIGHT, - TEXT_MAX_LINES, - TEXT_MAX_LINES_NO_HEADER, - Span, - TextBase, - render_text, -) - - -def header( - title: str, - icon: str = style.ICON_DEFAULT, - fg: int = style.FG, - bg: int = style.BG, - ifg: int = style.GREEN, -) -> None: - if icon is not None: - display.icon(14, 15, res.load(icon), ifg, bg) - display.text(44, 35, title, ui.BOLD, fg, bg) - - -class Text(TextBase): - def on_render(self) -> None: - if self.repaint: - offset_y = TEXT_LINE_HEIGHT - if self.header_text is not None: - header( - self.header_text, - self.header_icon, - ui.TITLE_GREY, - ui.BG, - self.icon_color, - ) - offset_y = TEXT_HEADER_HEIGHT + TEXT_LINE_HEIGHT - render_text( - self.content, - self.new_lines, - self.max_lines, - item_offset=self.content_offset, - char_offset=self.char_offset, - break_words=self.break_words, - line_width=self.line_width, - render_page_overflow=self.render_page_overflow, - offset_y=offset_y, - ) - self.repaint = False - - -LABEL_LEFT = const(0) -LABEL_CENTER = const(1) -LABEL_RIGHT = const(2) - - -class Label(ui.Component): - def __init__( - self, - area: ui.Area, - content: str, - align: int = LABEL_LEFT, - style: int = ui.NORMAL, - ) -> None: - super().__init__() - self.area = area - self.content = content - self.align = align - self.style = style - - def on_render(self) -> None: - if self.repaint: - align = self.align - ax, ay, aw, ah = self.area - ui.display.bar(ax, ay, aw, ah, ui.BG) - tx = ax + aw // 2 - ty = ay + ah // 2 + 8 - if align is LABEL_LEFT: - ui.display.text(tx, ty, self.content, self.style, ui.FG, ui.BG) - elif align is LABEL_CENTER: - ui.display.text_center(tx, ty, self.content, self.style, ui.FG, ui.BG) - elif align is LABEL_RIGHT: - ui.display.text_right(tx, ty, self.content, self.style, ui.FG, ui.BG) - self.repaint = False - - if __debug__: - - def read_content(self) -> list[str]: - return [self.content] - - -def text_center_trim_left( - x: int, y: int, text: str, font: int = ui.NORMAL, width: int = ui.WIDTH - 16 -) -> None: - if ui.display.text_width(text, font) <= width: - ui.display.text_center(x, y, text, font, ui.FG, ui.BG) - return - - ELLIPSIS_WIDTH = ui.display.text_width("...", ui.BOLD) - if width < ELLIPSIS_WIDTH: - return - - text_length = 0 - for i in range(1, len(text)): - if ui.display.text_width(text[-i:], font) + ELLIPSIS_WIDTH > width: - text_length = i - 1 - break - - text_width = ui.display.text_width(text[-text_length:], font) - x -= (text_width + ELLIPSIS_WIDTH) // 2 - ui.display.text(x, y, "...", ui.BOLD, ui.GREY, ui.BG) - x += ELLIPSIS_WIDTH - ui.display.text(x, y, text[-text_length:], font, ui.FG, ui.BG) - - -def text_center_trim_right( - x: int, y: int, text: str, font: int = ui.NORMAL, width: int = ui.WIDTH - 16 -) -> None: - if ui.display.text_width(text, font) <= width: - ui.display.text_center(x, y, text, font, ui.FG, ui.BG) - return - - ELLIPSIS_WIDTH = ui.display.text_width("...", ui.BOLD) - if width < ELLIPSIS_WIDTH: - return - - text_length = 0 - for i in range(1, len(text)): - if ui.display.text_width(text[:i], font) + ELLIPSIS_WIDTH > width: - text_length = i - 1 - break - - text_width = ui.display.text_width(text[:text_length], font) - x -= (text_width + ELLIPSIS_WIDTH) // 2 - ui.display.text(x, y, text[:text_length], font, ui.FG, ui.BG) - x += text_width - ui.display.text(x, y, "...", ui.BOLD, ui.GREY, ui.BG) diff --git a/core/src/trezor/ui/components/tt/webauthn.py b/core/src/trezor/ui/components/tt/webauthn.py deleted file mode 100644 index 6196d3035..000000000 --- a/core/src/trezor/ui/components/tt/webauthn.py +++ /dev/null @@ -1,34 +0,0 @@ -from trezor import ui - -from ..common.webauthn import ConfirmInfo -from .text import text_center_trim_left, text_center_trim_right - - -class ConfirmContent(ui.Component): - def __init__(self, info: ConfirmInfo) -> None: - super().__init__() - self.info = info - - def on_render(self) -> None: - if self.repaint: - header = self.info.get_header() - ui.header(header, ui.ICON_DEFAULT, ui.GREEN, ui.BG, ui.GREEN) - - if self.info.app_icon is not None: - ui.display.image((ui.WIDTH - 64) // 2, 48, self.info.app_icon) - - app_name = self.info.app_name() - account_name = self.info.account_name() - - # Dummy requests usually have some text as both app_name and account_name, - # in that case show the text only once. - if account_name is not None: - if app_name != account_name: - text_center_trim_left(ui.WIDTH // 2, 140, app_name) - text_center_trim_right(ui.WIDTH // 2, 172, account_name) - else: - text_center_trim_right(ui.WIDTH // 2, 156, account_name) - else: - text_center_trim_left(ui.WIDTH // 2, 156, app_name) - - self.repaint = False diff --git a/core/src/trezor/ui/components/tt/word_select.py b/core/src/trezor/ui/components/tt/word_select.py deleted file mode 100644 index 319d61e0c..000000000 --- a/core/src/trezor/ui/components/tt/word_select.py +++ /dev/null @@ -1,56 +0,0 @@ -from typing import TYPE_CHECKING - -from trezor import ui - -from .button import Button - -if TYPE_CHECKING: - from trezor import loop - -# todo improve? - - -class WordSelector(ui.Layout): - def __init__(self, content: ui.Component) -> None: - super().__init__() - self.content = content - self.w12 = Button(ui.grid(6, n_y=4), "12") - self.w12.on_click = self.on_w12 - self.w18 = Button(ui.grid(7, n_y=4), "18") - self.w18.on_click = self.on_w18 - self.w20 = Button(ui.grid(8, n_y=4), "20") - self.w20.on_click = self.on_w20 - self.w24 = Button(ui.grid(9, n_y=4), "24") - self.w24.on_click = self.on_w24 - self.w33 = Button(ui.grid(10, n_y=4), "33") - self.w33.on_click = self.on_w33 - - def dispatch(self, event: int, x: int, y: int) -> None: - self.content.dispatch(event, x, y) - self.w12.dispatch(event, x, y) - self.w18.dispatch(event, x, y) - self.w20.dispatch(event, x, y) - self.w24.dispatch(event, x, y) - self.w33.dispatch(event, x, y) - - def on_w12(self) -> None: - raise ui.Result(12) - - def on_w18(self) -> None: - raise ui.Result(18) - - def on_w20(self) -> None: - raise ui.Result(20) - - def on_w24(self) -> None: - raise ui.Result(24) - - def on_w33(self) -> None: - raise ui.Result(33) - - if __debug__: - - def create_tasks(self) -> tuple[loop.AwaitableTask, ...]: - from apps.debug import input_signal - - return super().create_tasks() + (input_signal(),) diff --git a/core/src/trezor/ui/constants/__init__.py b/core/src/trezor/ui/constants/__init__.py deleted file mode 100644 index 55c1c66f1..000000000 --- a/core/src/trezor/ui/constants/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -from trezor import utils - -if utils.MODEL in ("1",): - from .t1 import * # noqa: F401,F403 -elif utils.MODEL in ("R",): - from .tr import * # noqa: F401,F403 -elif utils.MODEL in ("T",): - from .tt import * # noqa: F401,F403 -else: - raise ValueError("Unknown Trezor model") diff --git a/core/src/trezor/ui/constants/t1.py b/core/src/trezor/ui/constants/t1.py deleted file mode 100644 index 91be0728d..000000000 --- a/core/src/trezor/ui/constants/t1.py +++ /dev/null @@ -1,9 +0,0 @@ -from micropython import const - -TEXT_HEADER_HEIGHT = const(13) -TEXT_LINE_HEIGHT = const(9) -TEXT_LINE_HEIGHT_HALF = const(4) -TEXT_MARGIN_LEFT = const(0) -TEXT_MAX_LINES = const(4) -TEXT_MAX_LINES_NO_HEADER = const(5) -PAGINATION_MARGIN_RIGHT = const(4) diff --git a/core/src/trezor/ui/constants/tr.py b/core/src/trezor/ui/constants/tr.py deleted file mode 100644 index 91be0728d..000000000 --- a/core/src/trezor/ui/constants/tr.py +++ /dev/null @@ -1,9 +0,0 @@ -from micropython import const - -TEXT_HEADER_HEIGHT = const(13) -TEXT_LINE_HEIGHT = const(9) -TEXT_LINE_HEIGHT_HALF = const(4) -TEXT_MARGIN_LEFT = const(0) -TEXT_MAX_LINES = const(4) -TEXT_MAX_LINES_NO_HEADER = const(5) -PAGINATION_MARGIN_RIGHT = const(4) diff --git a/core/src/trezor/ui/constants/tt.py b/core/src/trezor/ui/constants/tt.py deleted file mode 100644 index 1cad82ac3..000000000 --- a/core/src/trezor/ui/constants/tt.py +++ /dev/null @@ -1,16 +0,0 @@ -from micropython import const - -TEXT_HEADER_HEIGHT = const(48) -TEXT_LINE_HEIGHT = const(26) -TEXT_LINE_HEIGHT_HALF = const(13) -TEXT_MARGIN_LEFT = const(14) -TEXT_MAX_LINES = const(5) -TEXT_MAX_LINES_NO_HEADER = const(7) -PAGINATION_MARGIN_RIGHT = const(22) - -MONO_ADDR_PER_LINE = const(17) -MONO_HEX_PER_LINE = const(18) - -QR_X = const(120) -QR_Y = const(112) -QR_SIDE_MAX = const(140) diff --git a/core/src/trezor/ui/container.py b/core/src/trezor/ui/container.py deleted file mode 100644 index d11360737..000000000 --- a/core/src/trezor/ui/container.py +++ /dev/null @@ -1,16 +0,0 @@ -from trezor import ui - - -class Container(ui.Component): - def __init__(self, *children: ui.Component): - super().__init__() - self.children = children - - def dispatch(self, event: int, x: int, y: int) -> None: - for child in self.children: - child.dispatch(event, x, y) - - if __debug__: - - def read_content(self) -> list[str]: - return sum((c.read_content() for c in self.children), []) diff --git a/core/src/trezor/ui/layouts/__init__.py b/core/src/trezor/ui/layouts/__init__.py index dca4f8dc8..a90247cc3 100644 --- a/core/src/trezor/ui/layouts/__init__.py +++ b/core/src/trezor/ui/layouts/__init__.py @@ -2,12 +2,6 @@ from trezor import utils from .common import * # noqa: F401,F403 -try: - UI2 = True - import trezorui2 # noqa: F401 -except ImportError: - UI2 = False - # NOTE: using any import magic probably causes mypy not to check equivalence of # layout type signatures across models if utils.MODEL in ("1",): @@ -15,9 +9,6 @@ if utils.MODEL in ("1",): elif utils.MODEL in ("R",): from .tr import * # noqa: F401,F403 elif utils.MODEL in ("T",): - if not UI2: - from .tt import * # noqa: F401,F403 - else: - from .tt_v2 import * # noqa: F401,F403 + from .tt_v2 import * # noqa: F401,F403 else: raise ValueError("Unknown Trezor model") diff --git a/core/src/trezor/ui/layouts/altcoin.py b/core/src/trezor/ui/layouts/altcoin.py index 0e46ac724..d9709f9cb 100644 --- a/core/src/trezor/ui/layouts/altcoin.py +++ b/core/src/trezor/ui/layouts/altcoin.py @@ -1,6 +1 @@ -from . import UI2 - -if UI2: - from .tt_v2.altcoin import * # noqa: F401,F403 -else: - from .tt.altcoin import * # noqa: F401,F403 +from .tt_v2.altcoin import * # noqa: F401,F403 diff --git a/core/src/trezor/ui/layouts/common.py b/core/src/trezor/ui/layouts/common.py index 1fbeba029..7f068a9ba 100644 --- a/core/src/trezor/ui/layouts/common.py +++ b/core/src/trezor/ui/layouts/common.py @@ -33,12 +33,7 @@ async def interact( br_type: str, br_code: ButtonRequestType = ButtonRequestType.Other, ) -> Any: - if layout.__class__.__name__ == "Paginated": - from ..components.tt.scroll import Paginated - - assert isinstance(layout, Paginated) - return await layout.interact(ctx, code=br_code) - elif hasattr(layout, "page_count") and layout.page_count() > 1: # type: ignore [Cannot access member "page_count" for type "LayoutType"] + if hasattr(layout, "page_count") and layout.page_count() > 1: # type: ignore [Cannot access member "page_count" for type "LayoutType"] await button_request(ctx, br_type, br_code, pages=layout.page_count()) # type: ignore [Cannot access member "page_count" for type "LayoutType"] return await ctx.wait(layout) else: diff --git a/core/src/trezor/ui/layouts/recovery.py b/core/src/trezor/ui/layouts/recovery.py index 5c4e8673f..1866e3002 100644 --- a/core/src/trezor/ui/layouts/recovery.py +++ b/core/src/trezor/ui/layouts/recovery.py @@ -1,6 +1 @@ -from . import UI2 - -if UI2: - from .tt_v2.recovery import * # noqa: F401,F403 -else: - from .tt.recovery import * # noqa: F401,F403 +from .tt_v2.recovery import * # noqa: F401,F403 diff --git a/core/src/trezor/ui/layouts/reset.py b/core/src/trezor/ui/layouts/reset.py index 442213309..4e611559b 100644 --- a/core/src/trezor/ui/layouts/reset.py +++ b/core/src/trezor/ui/layouts/reset.py @@ -1,6 +1 @@ -from . import UI2 - -if UI2: - from .tt_v2.reset import * # noqa: F401,F403 -else: - from .tt.reset import * # noqa: F401,F403 +from .tt_v2.reset import * # noqa: F401,F403 diff --git a/core/src/trezor/ui/layouts/t1.py b/core/src/trezor/ui/layouts/t1.py index f6927aa3e..420c1f085 100644 --- a/core/src/trezor/ui/layouts/t1.py +++ b/core/src/trezor/ui/layouts/t1.py @@ -5,6 +5,7 @@ from trezor.enums import ButtonRequestType import trezorui2 +from ..components.common.confirm import is_confirmed from .common import interact if TYPE_CHECKING: @@ -62,11 +63,7 @@ async def confirm_action( verb: str | bytes | None = "OK", verb_cancel: str | bytes | None = "cancel", hold: bool = False, - hold_danger: bool = False, - icon: str | None = None, - icon_color: int | None = None, reverse: bool = False, - larger_vspace: bool = False, exc: ExceptionType = wire.ActionCancelled, br_code: ButtonRequestType = ButtonRequestType.Other, ) -> None: @@ -97,7 +94,7 @@ async def confirm_action( br_type, br_code, ) - if result is not trezorui2.CONFIRMED: + if not is_confirmed(result): raise exc @@ -108,8 +105,6 @@ async def confirm_text( data: str, description: str | None = None, br_code: ButtonRequestType = ButtonRequestType.Other, - icon: str = ui.ICON_SEND, # TODO cleanup @ redesign - icon_color: int = ui.GREEN, # TODO cleanup @ redesign ) -> None: result = await interact( ctx, @@ -123,7 +118,7 @@ async def confirm_text( br_type, br_code, ) - if result is not trezorui2.CONFIRMED: + if not is_confirmed(result): raise wire.ActionCancelled @@ -131,10 +126,8 @@ async def show_error_and_raise( ctx: wire.GenericContext, br_type: str, content: str, - header: str = "Error", subheader: str | None = None, button: str = "Close", - red: bool = False, exc: ExceptionType = wire.ActionCancelled, ) -> NoReturn: raise NotImplementedError diff --git a/core/src/trezor/ui/layouts/tt/__init__.py b/core/src/trezor/ui/layouts/tt/__init__.py deleted file mode 100644 index 5988d39b2..000000000 --- a/core/src/trezor/ui/layouts/tt/__init__.py +++ /dev/null @@ -1,1122 +0,0 @@ -from micropython import const -from typing import TYPE_CHECKING -from ubinascii import hexlify - -from trezor import ui, wire -from trezor.enums import ButtonRequestType -from trezor.ui.container import Container -from trezor.ui.loader import LoaderDanger -from trezor.ui.popup import Popup -from trezor.ui.qr import Qr -from trezor.utils import chunks, chunks_intersperse - -from ...components.common import break_path_to_lines -from ...components.common.confirm import ( - CONFIRMED, - GO_BACK, - SHOW_PAGINATED, - is_confirmed, - raise_if_cancelled, -) -from ...components.tt import passphrase, pin -from ...components.tt.button import ButtonCancel, ButtonDefault -from ...components.tt.confirm import Confirm, HoldToConfirm, InfoConfirm -from ...components.tt.scroll import ( - PAGEBREAK, - AskPaginated, - Paginated, - paginate_paragraphs, -) -from ...components.tt.text import LINE_WIDTH_PAGINATED, Span, Text -from ...constants.tt import ( - MONO_ADDR_PER_LINE, - MONO_HEX_PER_LINE, - QR_X, - QR_Y, - TEXT_MAX_LINES, -) -from ..common import button_request, interact - -if TYPE_CHECKING: - from typing import Any, Awaitable, Iterable, Iterator, NoReturn, Sequence - - from ..common import PropertyType, ExceptionType - from ...components.tt.button import ButtonContent - - -__all__ = ( - "confirm_action", - "confirm_address", - "confirm_text", - "confirm_amount", - "confirm_reset_device", - "confirm_backup", - "confirm_path_warning", - "confirm_sign_identity", - "confirm_signverify", - "show_address", - "show_error_and_raise", - "show_pubkey", - "show_success", - "show_xpub", - "show_warning", - "confirm_output", - "confirm_payment_request", - "confirm_blob", - "confirm_properties", - "confirm_total", - "confirm_joint_total", - "confirm_metadata", - "confirm_replacement", - "confirm_modify_output", - "confirm_modify_fee", - "confirm_coinjoin", - "show_coinjoin", - "show_popup", - "draw_simple_text", - "request_passphrase_on_device", - "request_pin_on_device", - "should_show_more", -) - - -async def confirm_action( - ctx: wire.GenericContext, - br_type: str, - title: str, - action: str | None = None, - description: str | None = None, - description_param: str | None = None, - description_param_font: int = ui.BOLD, - verb: ButtonContent = Confirm.DEFAULT_CONFIRM, - verb_cancel: ButtonContent | None = Confirm.DEFAULT_CANCEL, - hold: bool = False, - hold_danger: bool = False, - icon: str | None = None, # TODO cleanup @ redesign - icon_color: int | None = None, # TODO cleanup @ redesign - reverse: bool = False, # TODO cleanup @ redesign - larger_vspace: bool = False, # TODO cleanup @ redesign - exc: ExceptionType = wire.ActionCancelled, - br_code: ButtonRequestType = ButtonRequestType.Other, -) -> None: - text = Text( - title, - icon if icon is not None else ui.ICON_DEFAULT, - icon_color if icon_color is not None else ui.ORANGE_ICON, - new_lines=False, - ) - - if reverse and description is not None: - text.format_parametrized( - description, - description_param if description_param is not None else "", - param_font=description_param_font, - ) - elif action is not None: - text.bold(action) - - if action is not None and description is not None: - text.br() - if larger_vspace: - text.br_half() - - if reverse and action is not None: - text.bold(action) - elif description is not None: - text.format_parametrized( - description, - description_param if description_param is not None else "", - param_font=description_param_font, - ) - - layout: ui.Layout - if hold_danger: - assert isinstance(verb, str) - layout = HoldToConfirm( - text, - confirm=verb, - loader_style=LoaderDanger, - confirm_style=ButtonCancel, - cancel=verb_cancel is not None, - ) - elif hold: - assert isinstance(verb, str) - layout = HoldToConfirm(text, confirm=verb, cancel=verb_cancel is not None) - else: - layout = Confirm(text, confirm=verb, cancel=verb_cancel) - await raise_if_cancelled( - interact(ctx, layout, br_type, br_code), - exc, - ) - - -async def confirm_reset_device( - ctx: wire.GenericContext, prompt: str, recovery: bool = False -) -> None: - if recovery: - text = Text("Recovery mode", ui.ICON_RECOVERY, new_lines=False) - else: - text = Text("Create new wallet", ui.ICON_RESET, new_lines=False) - text.bold(prompt) - text.br() - text.br_half() - text.normal("By continuing you agree") - text.br() - text.normal("to ") - text.bold("https://trezor.io/tos") - await raise_if_cancelled( - interact( - ctx, - Confirm(text, major_confirm=not recovery), - "recover_device" if recovery else "setup_device", - ButtonRequestType.ProtectCall - if recovery - else ButtonRequestType.ResetDevice, - ) - ) - - -# TODO cleanup @ redesign -async def confirm_backup(ctx: wire.GenericContext) -> bool: - text1 = Text("Success", ui.ICON_CONFIRM, ui.GREEN, new_lines=False) - text1.bold("New wallet created successfully!\n") - text1.br_half() - text1.normal("You should back up your new wallet right now.") - - text2 = Text("Warning", ui.ICON_WRONG, ui.RED, new_lines=False) - text2.bold("Are you sure you want to skip the backup?\n") - text2.br_half() - text2.normal("You can back up your Trezor once, at any time.") - - if is_confirmed( - await interact( - ctx, - Confirm(text1, cancel="Skip", confirm="Back up", major_confirm=True), - "backup_device", - ButtonRequestType.ResetDevice, - ) - ): - return True - - confirmed = is_confirmed( - await interact( - ctx, - Confirm(text2, cancel="Skip", confirm="Back up", major_confirm=True), - "backup_device", - ButtonRequestType.ResetDevice, - ) - ) - return confirmed - - -async def confirm_path_warning( - ctx: wire.GenericContext, path: str, path_type: str = "Path" -) -> None: - text = Text("Confirm path", ui.ICON_WRONG, ui.RED) - text.normal(path_type) - text.mono(*break_path_to_lines(path, MONO_ADDR_PER_LINE)) - text.normal("is unknown.", "Are you sure?") - await raise_if_cancelled( - interact( - ctx, - Confirm(text), - "path_warning", - ButtonRequestType.UnknownDerivationPath, - ) - ) - - -def _show_qr( - address: str, - case_sensitive: bool, - title: str, - cancel: str = "Address", -) -> Confirm: - qr = Qr(address, case_sensitive, QR_X, QR_Y) - text = Text(title, ui.ICON_RECEIVE, ui.GREEN) - - return Confirm(Container(qr, text), cancel=cancel, cancel_style=ButtonDefault) - - -def _truncate_hex( - hex_data: str, - lines: int = TEXT_MAX_LINES, - width: int = MONO_HEX_PER_LINE, - middle: bool = False, - ellipsis: str = "...", # TODO: cleanup @ redesign -) -> Iterator[str]: - ell_len = len(ellipsis) - if len(hex_data) > width * lines: - if middle: - hex_data = ( - hex_data[: lines * width // 2 - (ell_len // 2)] - + ellipsis - + hex_data[-lines * width // 2 + (ell_len - ell_len // 2) :] - ) - else: - hex_data = hex_data[: (width * lines - ell_len)] + ellipsis - return chunks_intersperse(hex_data, width) - - -def _show_address( - address: str, - title: str, - network: str | None = None, - extra: str | None = None, -) -> ui.Layout: - para = [(ui.NORMAL, f"{network} network")] if network is not None else [] - if extra is not None: - para.append((ui.BOLD, extra)) - para.extend( - (ui.MONO, address_line) for address_line in chunks(address, MONO_ADDR_PER_LINE) - ) - return paginate_paragraphs( - para, - header=title, - header_icon=ui.ICON_RECEIVE, - icon_color=ui.GREEN, - confirm=lambda content: Confirm( - content, cancel="QR", cancel_style=ButtonDefault - ), - ) - - -def _show_xpub(xpub: str, title: str, cancel: str) -> Paginated: - pages: list[ui.Component] = [] - for lines in chunks(list(chunks_intersperse(xpub, 16)), TEXT_MAX_LINES * 2): - text = Text(title, ui.ICON_RECEIVE, ui.GREEN, new_lines=False) - text.mono(*lines) - pages.append(text) - - content = Paginated(pages) - - content.pages[-1] = Confirm( - content.pages[-1], - cancel=cancel, - cancel_style=ButtonDefault, - ) - - return content - - -async def show_xpub( - ctx: wire.GenericContext, xpub: str, title: str, cancel: str -) -> None: - await raise_if_cancelled( - interact( - ctx, - _show_xpub(xpub, title, cancel), - "show_xpub", - ButtonRequestType.PublicKey, - ) - ) - - -async def show_address( - ctx: wire.GenericContext, - address: str, - *, - address_qr: str | None = None, - case_sensitive: bool = True, - title: str = "Confirm address", - network: str | None = None, - multisig_index: int | None = None, - xpubs: Sequence[str] = (), - address_extra: str | None = None, - title_qr: str | None = None, -) -> None: - is_multisig = len(xpubs) > 0 - while True: - if is_confirmed( - await interact( - ctx, - _show_address( - address, - title, - network, - extra=address_extra, - ), - "show_address", - ButtonRequestType.Address, - ) - ): - break - if is_confirmed( - await interact( - ctx, - _show_qr( - address if address_qr is None else address_qr, - case_sensitive, - title if title_qr is None else title_qr, - cancel="XPUBs" if is_multisig else "Address", - ), - "show_qr", - ButtonRequestType.Address, - ) - ): - break - - if is_multisig: - for i, xpub in enumerate(xpubs): - cancel = "Next" if i < len(xpubs) - 1 else "Address" - title_xpub = f"XPUB #{i + 1}" - title_xpub += " (yours)" if i == multisig_index else " (cosigner)" - if is_confirmed( - await interact( - ctx, - _show_xpub(xpub, title=title_xpub, cancel=cancel), - "show_xpub", - ButtonRequestType.PublicKey, - ) - ): - return - - -def show_pubkey( - ctx: wire.Context, pubkey: str, title: str = "Confirm public key" -) -> Awaitable[None]: - return confirm_blob( - ctx, - br_type="show_pubkey", - title="Confirm public key", - data=pubkey, - br_code=ButtonRequestType.PublicKey, - icon=ui.ICON_RECEIVE, - ) - - -async def _show_modal( - ctx: wire.GenericContext, - br_type: str, - br_code: ButtonRequestType, - header: str, - subheader: str | None, - content: str, - button_confirm: str | None, - button_cancel: str | None, - icon: str, - icon_color: int, - exc: ExceptionType = wire.ActionCancelled, -) -> None: - text = Text(header, icon, icon_color, new_lines=False) - if subheader: - text.bold(subheader) - text.br() - text.br_half() - text.normal(content) - await raise_if_cancelled( - interact( - ctx, - Confirm(text, confirm=button_confirm, cancel=button_cancel), - br_type, - br_code, - ), - exc, - ) - - -async def show_error_and_raise( - ctx: wire.GenericContext, - br_type: str, - content: str, - header: str = "Error", - subheader: str | None = None, - button: str = "Close", - red: bool = False, - exc: ExceptionType = wire.ActionCancelled, -) -> NoReturn: - await _show_modal( - ctx, - br_type=br_type, - br_code=ButtonRequestType.Other, - header=header, - subheader=subheader, - content=content, - button_confirm=None, - button_cancel=button, - icon=ui.ICON_WRONG, - icon_color=ui.RED if red else ui.ORANGE_ICON, - exc=exc, - ) - raise exc - - -def show_warning( - ctx: wire.GenericContext, - br_type: str, - content: str, - header: str = "Warning", - subheader: str | None = None, - button: str = "Try again", - br_code: ButtonRequestType = ButtonRequestType.Warning, - icon: str = ui.ICON_WRONG, - icon_color: int = ui.RED, -) -> Awaitable[None]: - return _show_modal( - ctx, - br_type=br_type, - br_code=br_code, - header=header, - subheader=subheader, - content=content, - button_confirm=button, - button_cancel=None, - icon=icon, - icon_color=icon_color, - ) - - -def show_success( - ctx: wire.GenericContext, - br_type: str, - content: str, - subheader: str | None = None, - button: str = "Continue", -) -> Awaitable[None]: - return _show_modal( - ctx, - br_type=br_type, - br_code=ButtonRequestType.Success, - header="Success", - subheader=subheader, - content=content, - button_confirm=button, - button_cancel=None, - icon=ui.ICON_CONFIRM, - icon_color=ui.GREEN, - ) - - -async def confirm_output( - ctx: wire.GenericContext, - address: str, - amount: str, - font_amount: int = ui.NORMAL, # TODO cleanup @ redesign - title: str = "Confirm sending", - subtitle: str | None = None, # TODO cleanup @ redesign - color_to: int = ui.FG, # TODO cleanup @ redesign - to_str: str = " to\n", # TODO cleanup @ redesign - to_paginated: bool = False, # TODO cleanup @ redesign - width: int = MONO_ADDR_PER_LINE, - width_paginated: int = MONO_ADDR_PER_LINE - 1, - br_code: ButtonRequestType = ButtonRequestType.ConfirmOutput, - icon: str = ui.ICON_SEND, -) -> None: - header_lines = to_str.count("\n") + int(subtitle is not None) - if len(address) > (TEXT_MAX_LINES - header_lines) * width: - para = [] - if subtitle is not None: - para.append((ui.NORMAL, subtitle)) - para.append((font_amount, amount)) - if to_paginated: - para.append((ui.NORMAL, "to")) - para.extend((ui.MONO, line) for line in chunks(address, width_paginated)) - content: ui.Layout = paginate_paragraphs(para, title, icon, ui.GREEN) - else: - text = Text(title, icon, ui.GREEN, new_lines=False) - if subtitle is not None: - text.normal(subtitle, "\n") - text.content = [font_amount, amount, ui.NORMAL, color_to, to_str, ui.FG] - text.mono(*chunks_intersperse(address, width)) - content = Confirm(text) - - await raise_if_cancelled(interact(ctx, content, "confirm_output", br_code)) - - -async def confirm_payment_request( - ctx: wire.GenericContext, - recipient_name: str, - amount: str, - memos: list[str], -) -> Any: - para = [(ui.NORMAL, f"{amount} to\n{recipient_name}")] - para.extend((ui.NORMAL, memo) for memo in memos) - content = paginate_paragraphs( - para, - "Confirm sending", - ui.ICON_SEND, - ui.GREEN, - confirm=lambda text: InfoConfirm(text, info="Details"), - ) - return await raise_if_cancelled( - interact( - ctx, content, "confirm_payment_request", ButtonRequestType.ConfirmOutput - ) - ) - - -async def should_show_more( - ctx: wire.GenericContext, - title: str, - para: Iterable[tuple[int, str]], - button_text: str = "Show all", - br_type: str = "should_show_more", - br_code: ButtonRequestType = ButtonRequestType.Other, - icon: str = ui.ICON_DEFAULT, - icon_color: int = ui.ORANGE_ICON, - confirm: str | bytes | None = None, - major_confirm: bool = False, -) -> bool: - """Return True if the user wants to show more (they click a special button) - and False when the user wants to continue without showing details. - - Raises ActionCancelled if the user cancels. - """ - if confirm is None: - confirm = Confirm.DEFAULT_CONFIRM - - page = Text( - title, - header_icon=icon, - icon_color=icon_color, - new_lines=False, - max_lines=TEXT_MAX_LINES - 2, - ) - for font, text in para: - page.content.extend((font, text, "\n")) - - ask_dialog = Confirm( - AskPaginated(page, button_text), - confirm=confirm, - major_confirm=major_confirm, - ) - - result = await raise_if_cancelled(interact(ctx, ask_dialog, br_type, br_code)) - assert result in (SHOW_PAGINATED, CONFIRMED) - - return result is SHOW_PAGINATED - - -async def _confirm_ask_pagination( - ctx: wire.GenericContext, - br_type: str, - title: str, - para: Iterable[tuple[int, str]], - para_truncated: Iterable[tuple[int, str]], - br_code: ButtonRequestType, - icon: str, - icon_color: int, -) -> None: - paginated: ui.Layout | None = None - while True: - if not await should_show_more( - ctx, - title, - para=para_truncated, - br_type=br_type, - br_code=br_code, - icon=icon, - icon_color=icon_color, - ): - return - - if paginated is None: - paginated = paginate_paragraphs( - para, - header=None, - back_button=True, - confirm=lambda content: Confirm( - content, cancel=None, confirm="Close", confirm_style=ButtonDefault - ), - ) - result = await interact(ctx, paginated, br_type, br_code) - assert result in (CONFIRMED, GO_BACK) - - assert False - - -async def confirm_blob( - ctx: wire.GenericContext, - br_type: str, - title: str, - data: bytes | str, - description: str | None = None, - hold: bool = False, - br_code: ButtonRequestType = ButtonRequestType.Other, - icon: str = ui.ICON_SEND, # TODO cleanup @ redesign - icon_color: int = ui.GREEN, # TODO cleanup @ redesign - ask_pagination: bool = False, -) -> None: - """Confirm data blob. - - Applicable for public keys, signatures, hashes. In general, any kind of - data that is not human-readable, and can be wrapped at any character. - - For addresses, use `confirm_address`. - - Displays in monospace font. Paginates automatically. - If data is provided as bytes or bytearray, it is converted to hex. - """ - if isinstance(data, (bytes, bytearray)): - data_str = hexlify(data).decode() - else: - data_str = data - - span = Span() - lines = 0 - if description is not None: - span.reset(description, 0, ui.NORMAL) - lines += span.count_lines() - data_lines = (len(data_str) + MONO_HEX_PER_LINE - 1) // MONO_HEX_PER_LINE - lines += data_lines - - if lines <= TEXT_MAX_LINES: - text = Text(title, icon, icon_color, new_lines=False) - if description is not None: - text.normal(description) - text.br() - - # special case: - if len(data_str) % 16 == 0: - # sanity checks: - # (a) we must not exceed MONO_HEX_PER_LINE - assert MONO_HEX_PER_LINE > 16 - # (b) we must not increase number of lines - assert (len(data_str) // 16) <= data_lines - # the above holds true for MONO_HEX_PER_LINE == 18 and TEXT_MAX_LINES == 5 - per_line = 16 - - else: - per_line = MONO_HEX_PER_LINE - text.mono(ui.FG, *chunks_intersperse(data_str, per_line)) - content: ui.Layout = HoldToConfirm(text) if hold else Confirm(text) - return await raise_if_cancelled(interact(ctx, content, br_type, br_code)) - - elif ask_pagination: - para = [(ui.MONO, line) for line in chunks(data_str, MONO_HEX_PER_LINE - 2)] - - para_truncated = [] - if description is not None: - para_truncated.append((ui.NORMAL, description)) - para_truncated.extend(para[:TEXT_MAX_LINES]) - - return await _confirm_ask_pagination( - ctx, br_type, title, para, para_truncated, br_code, icon, icon_color - ) - - else: - para = [] - if description is not None: - para.append((ui.NORMAL, description)) - para.extend((ui.MONO, line) for line in chunks(data_str, MONO_HEX_PER_LINE - 2)) - - paginated = paginate_paragraphs( - para, title, icon, icon_color, confirm=HoldToConfirm if hold else Confirm - ) - return await raise_if_cancelled(interact(ctx, paginated, br_type, br_code)) - - -def confirm_address( - ctx: wire.GenericContext, - title: str, - address: str, - description: str | None = "Address:", - br_type: str = "confirm_address", - br_code: ButtonRequestType = ButtonRequestType.Other, - icon: str = ui.ICON_SEND, # TODO cleanup @ redesign - icon_color: int = ui.GREEN, # TODO cleanup @ redesign -) -> Awaitable[None]: - # TODO clarify API - this should be pretty limited to support mainly confirming - # destinations and similar - return confirm_blob( - ctx, - br_type=br_type, - title=title, - data=address, - description=description, - br_code=br_code, - icon=icon, - icon_color=icon_color, - ) - - -async def confirm_text( - ctx: wire.GenericContext, - br_type: str, - title: str, - data: str, - description: str | None = None, - br_code: ButtonRequestType = ButtonRequestType.Other, - icon: str = ui.ICON_SEND, # TODO cleanup @ redesign - icon_color: int = ui.GREEN, # TODO cleanup @ redesign -) -> None: - """Confirm textual data. - - Applicable for human-readable strings, numbers, date/time values etc. - - For amounts, use `confirm_amount`. - - Displays in bold font. Paginates automatically. - """ - span = Span() - lines = 0 - if description is not None: - span.reset(description, 0, ui.NORMAL) - lines += span.count_lines() - span.reset(data, 0, ui.BOLD) - lines += span.count_lines() - - if lines <= TEXT_MAX_LINES: - text = Text(title, icon, icon_color, new_lines=False) - if description is not None: - text.normal(description) - text.br() - text.bold(data) - content: ui.Layout = Confirm(text) - - else: - para = [] - if description is not None: - para.append((ui.NORMAL, description)) - para.append((ui.BOLD, data)) - content = paginate_paragraphs(para, title, icon, icon_color) - await raise_if_cancelled(interact(ctx, content, br_type, br_code)) - - -def confirm_amount( - ctx: wire.GenericContext, - title: str, - amount: str, - description: str = "Amount:", - br_type: str = "confirm_amount", - br_code: ButtonRequestType = ButtonRequestType.Other, - icon: str = ui.ICON_SEND, # TODO cleanup @ redesign - icon_color: int = ui.GREEN, # TODO cleanup @ redesign -) -> Awaitable[None]: - """Confirm amount.""" - # TODO clarify API - this should be pretty limited to support mainly confirming - # destinations and similar - return confirm_text( - ctx, - br_type=br_type, - title=title, - data=amount, - description=description, - br_code=br_code, - icon=icon, - icon_color=icon_color, - ) - - -_SCREEN_FULL_THRESHOLD = const(2) - - -# TODO keep name and value on the same page if possible -async def confirm_properties( - ctx: wire.GenericContext, - br_type: str, - title: str, - props: Iterable[PropertyType], - icon: str = ui.ICON_SEND, # TODO cleanup @ redesign - icon_color: int = ui.GREEN, # TODO cleanup @ redesign - hold: bool = False, - br_code: ButtonRequestType = ButtonRequestType.ConfirmOutput, -) -> None: - span = Span() - para = [] - used_lines = 0 - for key, val in props: - span.reset(key or "", 0, ui.NORMAL, line_width=LINE_WIDTH_PAGINATED) - key_lines = span.count_lines() - - if isinstance(val, str): - span.reset(val, 0, ui.BOLD, line_width=LINE_WIDTH_PAGINATED) - val_lines = span.count_lines() - elif isinstance(val, bytes): - val_lines = (len(val) * 2 + MONO_HEX_PER_LINE - 1) // MONO_HEX_PER_LINE - else: - val_lines = 0 - - remaining_lines = TEXT_MAX_LINES - used_lines - used_lines = (used_lines + key_lines + val_lines) % TEXT_MAX_LINES - - if key_lines + val_lines > remaining_lines: - if remaining_lines <= _SCREEN_FULL_THRESHOLD: - # there are only 2 remaining lines, don't try to fit and put everything - # on next page - para.append(PAGEBREAK) - used_lines = (key_lines + val_lines) % TEXT_MAX_LINES - - elif val_lines > 0 and key_lines >= remaining_lines: - # more than 2 remaining lines so try to fit something -- but won't fit - # at least one line of value - para.append(PAGEBREAK) - used_lines = (key_lines + val_lines) % TEXT_MAX_LINES - - elif key_lines + val_lines <= TEXT_MAX_LINES: - # Whole property won't fit to the page, but it will fit on a page - # by itself - para.append(PAGEBREAK) - used_lines = (key_lines + val_lines) % TEXT_MAX_LINES - - # else: - # None of the above. Continue fitting on the same page. - - if key: - para.append((ui.NORMAL, key)) - if isinstance(val, bytes): - para.extend( - (ui.MONO, line) - for line in chunks(hexlify(val).decode(), MONO_HEX_PER_LINE - 2) - ) - elif isinstance(val, str): - para.append((ui.BOLD, val)) - content = paginate_paragraphs( - para, title, icon, icon_color, confirm=HoldToConfirm if hold else Confirm - ) - await raise_if_cancelled(interact(ctx, content, br_type, br_code)) - - -async def confirm_total( - ctx: wire.GenericContext, - total_amount: str, - fee_amount: str, - fee_rate_amount: str | None = None, - title: str = "Confirm transaction", - total_label: str = "Total amount:\n", - fee_label: str = "\nincluding fee:\n", - icon_color: int = ui.GREEN, - br_type: str = "confirm_total", - br_code: ButtonRequestType = ButtonRequestType.SignTx, -) -> None: - text = Text(title, ui.ICON_SEND, icon_color, new_lines=False) - text.normal(total_label) - text.bold(total_amount) - text.normal(fee_label) - text.bold(fee_amount) - - if fee_rate_amount is not None: - text.normal(f"\n({fee_rate_amount})") - - await raise_if_cancelled(interact(ctx, HoldToConfirm(text), br_type, br_code)) - - -async def confirm_joint_total( - ctx: wire.GenericContext, spending_amount: str, total_amount: str -) -> None: - text = Text("Joint transaction", ui.ICON_SEND, ui.GREEN, new_lines=False) - text.normal("You are contributing:\n") - text.bold(spending_amount) - text.normal("\nto the total amount:\n") - text.bold(total_amount) - await raise_if_cancelled( - interact( - ctx, HoldToConfirm(text), "confirm_joint_total", ButtonRequestType.SignTx - ) - ) - - -async def confirm_metadata( - ctx: wire.GenericContext, - br_type: str, - title: str, - content: str, - param: str | None = None, - br_code: ButtonRequestType = ButtonRequestType.SignTx, - hide_continue: bool = False, - hold: bool = False, - param_font: int = ui.BOLD, - icon: str = ui.ICON_SEND, # TODO cleanup @ redesign - icon_color: int = ui.GREEN, # TODO cleanup @ redesign - larger_vspace: bool = False, # TODO cleanup @ redesign -) -> None: - text = Text(title, icon, icon_color, new_lines=False) - text.format_parametrized( - content, param if param is not None else "", param_font=param_font - ) - - if not hide_continue: - text.br() - if larger_vspace: - text.br_half() - text.normal("Continue?") - - cls = HoldToConfirm if hold else Confirm - - await raise_if_cancelled(interact(ctx, cls(text), br_type, br_code)) - - -async def confirm_replacement( - ctx: wire.GenericContext, description: str, txid: str -) -> None: - text = Text(description, ui.ICON_SEND, ui.GREEN, new_lines=False) - text.normal("Confirm transaction ID:\n") - text.mono(*_truncate_hex(txid, TEXT_MAX_LINES - 1)) - await raise_if_cancelled( - interact(ctx, Confirm(text), "confirm_replacement", ButtonRequestType.SignTx) - ) - - -async def confirm_modify_output( - ctx: wire.GenericContext, - address: str, - sign: int, - amount_change: str, - amount_new: str, -) -> None: - page1 = Text("Modify amount", ui.ICON_SEND, ui.GREEN, new_lines=False) - page1.normal("Address:\n") - page1.br_half() - page1.mono(*chunks_intersperse(address, MONO_ADDR_PER_LINE)) - - page2 = Text("Modify amount", ui.ICON_SEND, ui.GREEN, new_lines=False) - if sign < 0: - page2.normal("Decrease amount by:\n") - else: - page2.normal("Increase amount by:\n") - page2.bold(amount_change) - page2.br_half() - page2.normal("\nNew amount:\n") - page2.bold(amount_new) - - await raise_if_cancelled( - interact( - ctx, - Paginated([page1, Confirm(page2)]), - "modify_output", - ButtonRequestType.ConfirmOutput, - ) - ) - - -async def confirm_modify_fee( - ctx: wire.GenericContext, - sign: int, - user_fee_change: str, - total_fee_new: str, - fee_rate_amount: str | None = None, -) -> None: - text = Text("Modify fee", ui.ICON_SEND, ui.GREEN, new_lines=False) - if sign == 0: - text.normal("Your fee did not change.\n") - else: - if sign < 0: - text.normal("Decrease your fee by:\n") - else: - text.normal("Increase your fee by:\n") - text.bold(user_fee_change) - text.br() - text.normal("Transaction fee:\n") - text.bold(total_fee_new) - if fee_rate_amount is not None: - text.normal(f"\n({fee_rate_amount})") - await raise_if_cancelled( - interact(ctx, HoldToConfirm(text), "modify_fee", ButtonRequestType.SignTx) - ) - - -async def confirm_coinjoin( - ctx: wire.GenericContext, max_rounds: int, max_fee_per_vbyte: str -) -> None: - text = Text("Authorize CoinJoin", ui.ICON_RECOVERY, new_lines=False) - text.normal("Maximum rounds: ") - text.bold(f"{max_rounds}\n") - text.br_half() - text.normal("Maximum mining fee:\n") - text.bold(max_fee_per_vbyte) - await raise_if_cancelled( - interact(ctx, HoldToConfirm(text), "coinjoin_final", ButtonRequestType.Other) - ) - - -def show_coinjoin() -> None: - text = Text("Please wait", ui.ICON_CONFIG, ui.RED) - text.normal("CoinJoin in progress.") - text.br() - text.bold("Do not disconnect your Trezor.") - ui.draw_simple(text) - - -# TODO cleanup @ redesign -async def confirm_sign_identity( - ctx: wire.GenericContext, proto: str, identity: str, challenge_visual: str | None -) -> None: - text = Text(f"Sign {proto}", new_lines=False) - if challenge_visual: - text.normal(challenge_visual) - text.br() - text.mono(*chunks_intersperse(identity, 18)) - await raise_if_cancelled( - interact(ctx, Confirm(text), "sign_identity", ButtonRequestType.Other) - ) - - -async def confirm_signverify( - ctx: wire.GenericContext, coin: str, message: str, address: str, verify: bool -) -> None: - if verify: - header = f"Verify {coin} message" - br_type = "verify_message" - else: - header = f"Sign {coin} message" - br_type = "sign_message" - - text = Text(header, new_lines=False) - text.bold("Confirm address:\n") - text.mono(*chunks_intersperse(address, MONO_ADDR_PER_LINE)) - await raise_if_cancelled( - interact(ctx, Confirm(text), br_type, ButtonRequestType.Other) - ) - - para = [(ui.BOLD, "Confirm message:"), (ui.MONO, message)] - content = paginate_paragraphs(para, header) - await raise_if_cancelled(interact(ctx, content, br_type, ButtonRequestType.Other)) - - -async def show_popup( - title: str, - description: str, - subtitle: str | None = None, - description_param: str = "", - timeout_ms: int = 3000, -) -> None: - text = Text(title, ui.ICON_WRONG, ui.RED) - if subtitle is not None: - text.bold(subtitle) - text.br_half() - text.format_parametrized(description, description_param) - await Popup(text, timeout_ms) - - -def draw_simple_text(title: str, description: str = "") -> None: - text = Text(title, ui.ICON_CONFIG, new_lines=False) - text.normal(description) - ui.draw_simple(text) - - -async def request_passphrase_on_device(ctx: wire.GenericContext, max_len: int) -> str: - await button_request( - ctx, "passphrase_device", code=ButtonRequestType.PassphraseEntry - ) - - keyboard = passphrase.PassphraseKeyboard("Enter passphrase", max_len) - result = await ctx.wait(keyboard) - if result is passphrase.CANCELLED: - raise wire.ActionCancelled("Passphrase entry cancelled") - - assert isinstance(result, str) - return result - - -async def request_pin_on_device( - ctx: wire.GenericContext, - prompt: str, - attempts_remaining: int | None, - allow_cancel: bool, -) -> str: - await button_request(ctx, "pin_device", code=ButtonRequestType.PinEntry) - - if attempts_remaining is None: - subprompt = None - elif attempts_remaining == 1: - subprompt = "This is your last attempt" - else: - subprompt = f"{attempts_remaining} attempts remaining" - - dialog = pin.PinDialog(prompt, subprompt, allow_cancel) - while True: - result = await ctx.wait(dialog) - if result is pin.CANCELLED: - raise wire.PinCancelled - assert isinstance(result, str) - return result diff --git a/core/src/trezor/ui/layouts/tt/altcoin.py b/core/src/trezor/ui/layouts/tt/altcoin.py deleted file mode 100644 index 755f24d13..000000000 --- a/core/src/trezor/ui/layouts/tt/altcoin.py +++ /dev/null @@ -1,81 +0,0 @@ -from typing import Sequence - -from trezor import ui, wire -from trezor.enums import ButtonRequestType -from trezor.utils import chunks_intersperse - -from ...components.common.confirm import raise_if_cancelled -from ...components.tt.confirm import Confirm, HoldToConfirm -from ...components.tt.scroll import Paginated -from ...components.tt.text import Text -from ...constants.tt import MONO_ADDR_PER_LINE -from ..common import interact - - -async def confirm_total_ethereum( - ctx: wire.GenericContext, total_amount: str, gas_price: str, fee_max: str -) -> None: - text = Text("Confirm transaction", ui.ICON_SEND, ui.GREEN, new_lines=False) - text.bold(total_amount) - text.normal(" ", ui.GREY, "Gas price:", ui.FG) - text.bold(gas_price) - text.normal(" ", ui.GREY, "Maximum fee:", ui.FG) - text.bold(fee_max) - await raise_if_cancelled( - interact(ctx, HoldToConfirm(text), "confirm_total", ButtonRequestType.SignTx) - ) - - -async def confirm_total_ripple( - ctx: wire.GenericContext, - address: str, - amount: str, -) -> None: - title = "Confirm sending" - text = Text(title, ui.ICON_SEND, ui.GREEN, new_lines=False) - text.bold(f"{amount} XRP\n") - text.normal("to\n") - text.mono(*chunks_intersperse(address, MONO_ADDR_PER_LINE)) - - await raise_if_cancelled( - interact(ctx, HoldToConfirm(text), "confirm_output", ButtonRequestType.SignTx) - ) - - -async def confirm_transfer_binance( - ctx: wire.GenericContext, inputs_outputs: Sequence[tuple[str, str, str]] -) -> None: - pages: list[ui.Component] = [] - for title, amount, address in inputs_outputs: - coin_page = Text(title, ui.ICON_SEND, icon_color=ui.GREEN, new_lines=False) - coin_page.bold(amount) - coin_page.normal("\nto\n") - coin_page.mono(*chunks_intersperse(address, MONO_ADDR_PER_LINE)) - pages.append(coin_page) - - pages[-1] = HoldToConfirm(pages[-1]) - - await raise_if_cancelled( - interact( - ctx, Paginated(pages), "confirm_transfer", ButtonRequestType.ConfirmOutput - ) - ) - - -async def confirm_decred_sstx_submission( - ctx: wire.GenericContext, - address: str, - amount: str, -) -> None: - text = Text("Purchase ticket", ui.ICON_SEND, ui.GREEN, new_lines=False) - text.normal(amount) - text.normal("\nwith voting rights to\n") - text.mono(*chunks_intersperse(address, MONO_ADDR_PER_LINE)) - await raise_if_cancelled( - interact( - ctx, - Confirm(text), - "confirm_decred_sstx_submission", - ButtonRequestType.ConfirmOutput, - ) - ) diff --git a/core/src/trezor/ui/layouts/tt/recovery.py b/core/src/trezor/ui/layouts/tt/recovery.py deleted file mode 100644 index 31cd03a2b..000000000 --- a/core/src/trezor/ui/layouts/tt/recovery.py +++ /dev/null @@ -1,135 +0,0 @@ -from typing import Callable, Iterable - -from trezor import strings, ui, wire -from trezor.crypto.slip39 import MAX_SHARE_COUNT -from trezor.enums import ButtonRequestType - -from ...components.common.confirm import ( - is_confirmed, - is_confirmed_info, - raise_if_cancelled, -) -from ...components.tt.confirm import Confirm, InfoConfirm -from ...components.tt.keyboard_bip39 import Bip39Keyboard -from ...components.tt.keyboard_slip39 import Slip39Keyboard -from ...components.tt.recovery import RecoveryHomescreen -from ...components.tt.scroll import Paginated -from ...components.tt.text import Text -from ...components.tt.word_select import WordSelector -from ..common import button_request, interact - - -async def request_word_count(ctx: wire.GenericContext, dry_run: bool) -> int: - await button_request(ctx, "word_count", code=ButtonRequestType.MnemonicWordCount) - - if dry_run: - text = Text("Seed check", ui.ICON_RECOVERY) - else: - text = Text("Recovery mode", ui.ICON_RECOVERY) - text.normal("Number of words?") - - count = await ctx.wait(WordSelector(text)) - # WordSelector can return int, or string if the value came from debuglink - # ctx.wait has a return type Any - # Hence, it is easier to convert the returned value to int explicitly - return int(count) - - -async def request_word( - ctx: wire.GenericContext, word_index: int, word_count: int, is_slip39: bool -) -> str: - if is_slip39: - keyboard: Slip39Keyboard | Bip39Keyboard = Slip39Keyboard( - f"Type word {word_index + 1} of {word_count}:" - ) - else: - keyboard = Bip39Keyboard(f"Type word {word_index + 1} of {word_count}:") - - word: str = await ctx.wait(keyboard) - return word - - -async def show_remaining_shares( - ctx: wire.GenericContext, - groups: Iterable[tuple[int, tuple[str, ...]]], # remaining + list 3 words - shares_remaining: list[int], - group_threshold: int, -) -> None: - pages: list[ui.Component] = [] - for remaining, group in groups: - if 0 < remaining < MAX_SHARE_COUNT: - text = Text("Remaining Shares") - text.bold( - strings.format_plural( - "{count} more {plural} starting", remaining, "share" - ) - ) - for word in group: - text.normal(word) - pages.append(text) - elif ( - remaining == MAX_SHARE_COUNT and shares_remaining.count(0) < group_threshold - ): - text = Text("Remaining Shares") - groups_remaining = group_threshold - shares_remaining.count(0) - text.bold( - strings.format_plural( - "{count} more {plural} starting", groups_remaining, "group" - ) - ) - for word in group: - text.normal(word) - pages.append(text) - - pages[-1] = Confirm(pages[-1], cancel=None) - await raise_if_cancelled( - interact(ctx, Paginated(pages), "show_shares", ButtonRequestType.Other) - ) - - -async def show_group_share_success( - ctx: wire.GenericContext, share_index: int, group_index: int -) -> None: - text = Text("Success", ui.ICON_CONFIRM) - text.bold("You have entered") - text.bold(f"Share {share_index + 1}") - text.normal("from") - text.bold(f"Group {group_index + 1}") - - await raise_if_cancelled( - interact( - ctx, - Confirm(text, confirm="Continue", cancel=None), - "share_success", - ButtonRequestType.Other, - ) - ) - - -async def continue_recovery( - ctx: wire.GenericContext, - button_label: str, - text: str, - subtext: str | None, - info_func: Callable | None, - dry_run: bool, -) -> bool: - homepage = RecoveryHomescreen(dry_run, text, subtext) - if info_func is not None: - content = InfoConfirm( - homepage, - confirm=button_label, - info="Info", - cancel="Abort", - ) - await button_request(ctx, "recovery", ButtonRequestType.RecoveryHomepage) - return await is_confirmed_info(ctx, content, info_func) - else: - return is_confirmed( - await interact( - ctx, - Confirm(homepage, confirm=button_label, major_confirm=True), - "recovery", - ButtonRequestType.RecoveryHomepage, - ) - ) diff --git a/core/src/trezor/ui/layouts/tt/reset.py b/core/src/trezor/ui/layouts/tt/reset.py deleted file mode 100644 index 5172eb61f..000000000 --- a/core/src/trezor/ui/layouts/tt/reset.py +++ /dev/null @@ -1,363 +0,0 @@ -from typing import TYPE_CHECKING - -from trezor import ui, utils, wire -from trezor.enums import BackupType, ButtonRequestType - -from ...components.common.confirm import is_confirmed, raise_if_cancelled -from ...components.tt.button import ButtonDefault -from ...components.tt.checklist import Checklist -from ...components.tt.confirm import Confirm, HoldToConfirm -from ...components.tt.info import InfoConfirm -from ...components.tt.reset import MnemonicWordSelect, Slip39NumInput -from ...components.tt.scroll import Paginated -from ...components.tt.text import Text -from ..common import interact -from . import confirm_action - -if TYPE_CHECKING: - from typing import Sequence - - NumberedWords = Sequence[tuple[int, str]] - -if __debug__: - from apps import debug - - -async def show_share_words( - ctx: wire.GenericContext, - share_words: Sequence[str], - share_index: int | None = None, - group_index: int | None = None, -) -> None: - first, chunks, last = _split_share_into_pages(share_words) - - if share_index is None: - header_title = "Recovery seed" - elif group_index is None: - header_title = f"Recovery share #{share_index + 1}" - else: - header_title = f"Group {group_index + 1} - Share {share_index + 1}" - header_icon = ui.ICON_RESET - pages: list[ui.Component] = [] # ui page components - shares_words_check = [] # check we display correct data - - # first page - text = Text(header_title, header_icon) - text.bold("Write down these") - text.bold(f"{len(share_words)} words:") - text.br_half() - for index, word in first: - text.mono(f"{index + 1}. {word}") - shares_words_check.append(word) - pages.append(text) - - # middle pages - for chunk in chunks: - text = Text(header_title, header_icon) - for index, word in chunk: - text.mono(f"{index + 1}. {word}") - shares_words_check.append(word) - pages.append(text) - - # last page - text = Text(header_title, header_icon) - for index, word in last: - text.mono(f"{index + 1}. {word}") - shares_words_check.append(word) - text.br_half() - text.bold(f"I wrote down all {len(share_words)}") - text.bold("words in order.") - pages.append(text) - - pages[-1] = HoldToConfirm(pages[-1], cancel=False) - - # pagination - paginated = Paginated(pages) - - if __debug__: - - word_pages = [first] + chunks + [last] - - def export_displayed_words() -> None: - # export currently displayed mnemonic words into debuglink - words = [w for _, w in word_pages[paginated.page]] - debug.reset_current_words.publish(words) - - paginated.on_change = export_displayed_words - export_displayed_words() - - # make sure we display correct data - utils.ensure(share_words == shares_words_check) - - # confirm the share - await raise_if_cancelled( - interact( - ctx, - paginated, - "backup_words", - ButtonRequestType.ResetDevice, - ) - ) - - -async def select_word( - ctx: wire.GenericContext, - words: Sequence[str], - share_index: int | None, - checked_index: int, - count: int, - group_index: int | None = None, -) -> str: - # let the user pick a word - select = MnemonicWordSelect(words, share_index, checked_index, count, group_index) - selected_word: str = await ctx.wait(select) - return selected_word - - -def _split_share_into_pages( - share_words: Sequence[str], -) -> tuple[NumberedWords, list[NumberedWords], NumberedWords]: - share = list(enumerate(share_words)) # we need to keep track of the word indices - first = share[:2] # two words on the first page - length = len(share_words) - if length in (12, 20, 24): - middle = share[2:-2] - last = share[-2:] # two words on the last page - elif length in (18, 33): - middle = share[2:] - last = [] # no words at the last page, because it does not add up - else: - # Invalid number of shares. SLIP-39 allows 20 or 33 words, BIP-39 12 or 24 - raise RuntimeError - - chunks = utils.chunks(middle, 4) # 4 words on the middle pages - return first, list(chunks), last - - -async def slip39_show_checklist( - ctx: wire.GenericContext, step: int, backup_type: BackupType -) -> None: - checklist = Checklist("Backup checklist", ui.ICON_RESET) - if backup_type is BackupType.Slip39_Basic: - checklist.add("Set number of shares") - checklist.add("Set threshold") - checklist.add(("Write down and check", "all recovery shares")) - elif backup_type is BackupType.Slip39_Advanced: - checklist.add("Set number of groups") - checklist.add("Set group threshold") - checklist.add(("Set size and threshold", "for each group")) - checklist.select(step) - - await raise_if_cancelled( - interact( - ctx, - Confirm(checklist, confirm="Continue", cancel=None), - "slip39_checklist", - ButtonRequestType.ResetDevice, - ) - ) - - -async def slip39_prompt_threshold( - ctx: wire.GenericContext, num_of_shares: int, group_id: int | None = None -) -> int: - count = num_of_shares // 2 + 1 - # min value of share threshold is 2 unless the number of shares is 1 - # number of shares 1 is possible in advnaced slip39 - min_count = min(2, num_of_shares) - max_count = num_of_shares - - while True: - shares = Slip39NumInput( - Slip39NumInput.SET_THRESHOLD, count, min_count, max_count, group_id - ) - confirmed = is_confirmed( - await interact( - ctx, - Confirm( - shares, - confirm="Continue", - cancel="Info", - major_confirm=True, - cancel_style=ButtonDefault, - ), - "slip39_threshold", - ButtonRequestType.ResetDevice, - ) - ) - - count = shares.input.count - if confirmed: - break - - text = "The threshold sets the number of shares " - if group_id is None: - text += "needed to recover your wallet. " - text += f"Set it to {count} and you will need " - if num_of_shares == 1: - text += "1 share." - elif num_of_shares == count: - text += f"all {count} of your {num_of_shares} shares." - else: - text += f"any {count} of your {num_of_shares} shares." - else: - text += "needed to form a group. " - text += f"Set it to {count} and you will " - if num_of_shares == 1: - text += "need 1 share " - elif num_of_shares == count: - text += f"need all {count} of {num_of_shares} shares " - else: - text += f"need any {count} of {num_of_shares} shares " - text += f"to form Group {group_id + 1}." - info = InfoConfirm(text) - await info - - return count - - -async def slip39_prompt_number_of_shares( - ctx: wire.GenericContext, group_id: int | None = None -) -> int: - count = 5 - min_count = 1 - max_count = 16 - - while True: - shares = Slip39NumInput( - Slip39NumInput.SET_SHARES, count, min_count, max_count, group_id - ) - confirmed = is_confirmed( - await interact( - ctx, - Confirm( - shares, - confirm="Continue", - cancel="Info", - major_confirm=True, - cancel_style=ButtonDefault, - ), - "slip39_shares", - ButtonRequestType.ResetDevice, - ) - ) - count = shares.input.count - if confirmed: - break - - if group_id is None: - info = InfoConfirm( - "Each recovery share is a " - "sequence of 20 words. " - "Next you will choose " - "how many shares you " - "need to recover your " - "wallet." - ) - else: - info = InfoConfirm( - "Each recovery share is a " - "sequence of 20 words. " - "Next you will choose " - "the threshold number of " - "shares needed to form " - f"Group {group_id + 1}." - ) - await info - - return count - - -async def slip39_advanced_prompt_number_of_groups(ctx: wire.GenericContext) -> int: - count = 5 - min_count = 2 - max_count = 16 - - while True: - shares = Slip39NumInput(Slip39NumInput.SET_GROUPS, count, min_count, max_count) - confirmed = is_confirmed( - await interact( - ctx, - Confirm( - shares, - confirm="Continue", - cancel="Info", - major_confirm=True, - cancel_style=ButtonDefault, - ), - "slip39_groups", - ButtonRequestType.ResetDevice, - ) - ) - count = shares.input.count - if confirmed: - break - - info = InfoConfirm( - "Each group has a set " - "number of shares and " - "its own threshold. In the " - "next steps you will set " - "the numbers of shares " - "and the thresholds." - ) - await info - - return count - - -async def slip39_advanced_prompt_group_threshold( - ctx: wire.GenericContext, num_of_groups: int -) -> int: - count = num_of_groups // 2 + 1 - min_count = 1 - max_count = num_of_groups - - while True: - shares = Slip39NumInput( - Slip39NumInput.SET_GROUP_THRESHOLD, count, min_count, max_count - ) - confirmed = is_confirmed( - await interact( - ctx, - Confirm( - shares, - confirm="Continue", - cancel="Info", - major_confirm=True, - cancel_style=ButtonDefault, - ), - "slip39_group_threshold", - ButtonRequestType.ResetDevice, - ) - ) - count = shares.input.count - if confirmed: - break - else: - info = InfoConfirm( - "The group threshold " - "specifies the number of " - "groups required to " - "recover your wallet. " - ) - await info - - return count - - -async def show_warning_backup(ctx: wire.GenericContext, slip39: bool) -> None: - if slip39: - description = "Never make a digital copy of your recovery shares and never upload them online!" - else: - description = "Never make a digital copy of your recovery seed and never upload\nit online!" - await confirm_action( - ctx, - "backup_warning", - "Caution", - description=description, - verb="I understand", - verb_cancel=None, - icon=ui.ICON_NOCOPY, - br_code=ButtonRequestType.ResetDevice, - ) diff --git a/core/src/trezor/ui/layouts/tt/webauthn.py b/core/src/trezor/ui/layouts/tt/webauthn.py deleted file mode 100644 index 083ea6db9..000000000 --- a/core/src/trezor/ui/layouts/tt/webauthn.py +++ /dev/null @@ -1,34 +0,0 @@ -from trezor import ui, wire -from trezor.enums import ButtonRequestType - -from ...components.common.confirm import is_confirmed -from ...components.common.webauthn import ConfirmInfo -from ...components.tt.confirm import Confirm, ConfirmPageable, Pageable -from ...components.tt.text import Text -from ...components.tt.webauthn import ConfirmContent -from ..common import interact - - -async def confirm_webauthn( - ctx: wire.GenericContext | None, - info: ConfirmInfo, - pageable: Pageable | None = None, -) -> bool: - if pageable is not None: - confirm: ui.Layout = ConfirmPageable(pageable, ConfirmContent(info)) - else: - confirm = Confirm(ConfirmContent(info)) - - if ctx is None: - return is_confirmed(await confirm) - else: - return is_confirmed( - await interact(ctx, confirm, "confirm_webauthn", ButtonRequestType.Other) - ) - - -async def confirm_webauthn_reset() -> bool: - text = Text("FIDO2 Reset", ui.ICON_CONFIG) - text.normal("Do you really want to") - text.bold("erase all credentials?") - return is_confirmed(await Confirm(text)) diff --git a/core/src/trezor/ui/layouts/webauthn.py b/core/src/trezor/ui/layouts/webauthn.py index 909113792..3cd18dfb1 100644 --- a/core/src/trezor/ui/layouts/webauthn.py +++ b/core/src/trezor/ui/layouts/webauthn.py @@ -1,6 +1 @@ -from . import UI2 - -if UI2: - from .tt_v2.webauthn import * # noqa: F401,F403 -else: - from .tt.webauthn import * # noqa: F401,F403 +from .tt_v2.webauthn import * # noqa: F401,F403 diff --git a/core/src/trezor/ui/loader.py b/core/src/trezor/ui/loader.py index 71e84107a..97c6ffd04 100644 --- a/core/src/trezor/ui/loader.py +++ b/core/src/trezor/ui/loader.py @@ -20,14 +20,6 @@ class LoaderDefault: icon_fg_color: int | None = ui.WHITE -class LoaderDanger(LoaderDefault): - class normal(LoaderDefault.normal): - fg_color = ui.RED - - class active(LoaderDefault.active): - fg_color = ui.RED - - class LoaderNeutral(LoaderDefault): class normal(LoaderDefault.normal): fg_color = ui.FG diff --git a/core/src/trezor/ui/qr.py b/core/src/trezor/ui/qr.py deleted file mode 100644 index 6761795a1..000000000 --- a/core/src/trezor/ui/qr.py +++ /dev/null @@ -1,44 +0,0 @@ -from typing import Sequence - -from trezor import ui -from trezor.ui.constants import QR_SIDE_MAX - -_QR_WIDTHS = (21, 25, 29, 33, 37, 41, 45, 49, 53, 57) -_THRESHOLDS_BINARY = (14, 26, 42, 62, 84, 106, 122, 152, 180, 213) -_THRESHOLDS_ALPHANUM = (20, 38, 61, 90, 122, 154, 178, 221, 262, 311) - - -def _qr_version_index(data: str, thresholds: Sequence[int]) -> int: - for i, threshold in enumerate(thresholds): - if len(data) <= threshold: - return i - raise ValueError # data too long - - -def is_alphanum_only(data: str) -> bool: - return all(c in "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $*+-./:" for c in data) - - -class Qr(ui.Component): - def __init__(self, data: str, case_sensitive: bool, x: int, y: int) -> None: - super().__init__() - if case_sensitive and not is_alphanum_only(data): - # must encode in BINARY mode - version_idx = _qr_version_index(data, _THRESHOLDS_BINARY) - else: - # can make the QR code more readable by using the best version - version_idx = _qr_version_index(data, _THRESHOLDS_ALPHANUM) - if len(data) > _THRESHOLDS_BINARY[version_idx]: - data = data.upper() - - size = _QR_WIDTHS[version_idx] - - self.data = data - self.x = x - self.y = y - self.scale = QR_SIDE_MAX // size - - def on_render(self) -> None: - if self.repaint: - ui.display.qrcode(self.x, self.y, self.data, self.scale) - self.repaint = False diff --git a/core/src/trezor/ui/style.py b/core/src/trezor/ui/style.py index bc63e514c..531375905 100644 --- a/core/src/trezor/ui/style.py +++ b/core/src/trezor/ui/style.py @@ -2,9 +2,6 @@ from micropython import const from trezor.ui import rgb -# radius for buttons and other elements -RADIUS = const(2) - # backlight brightness BACKLIGHT_NORMAL = const(150) BACKLIGHT_LOW = const(45) @@ -14,58 +11,21 @@ BACKLIGHT_MAX = const(255) # color palette RED = rgb(0xFF, 0x00, 0x00) -PINK = rgb(0xE9, 0x1E, 0x63) -PURPLE = rgb(0x9C, 0x27, 0xB0) -DEEP_PURPLE = rgb(0x67, 0x3A, 0xB7) -INDIGO = rgb(0x3F, 0x51, 0xB5) BLUE = rgb(0x21, 0x96, 0xF3) -LIGHT_BLUE = rgb(0x03, 0xA9, 0xF4) -CYAN = rgb(0x00, 0xBC, 0xD4) -TEAL = rgb(0x00, 0x96, 0x88) GREEN = rgb(0x00, 0xAE, 0x0B) -LIGHT_GREEN = rgb(0x87, 0xCE, 0x26) -LIME = rgb(0xCD, 0xDC, 0x39) YELLOW = rgb(0xFF, 0xEB, 0x3B) -AMBER = rgb(0xFF, 0xC1, 0x07) -ORANGE = rgb(0xFF, 0x98, 0x00) -DEEP_ORANGE = rgb(0xFF, 0x57, 0x22) -BROWN = rgb(0x79, 0x55, 0x48) -LIGHT_GREY = rgb(0xDA, 0xDD, 0xD8) -GREY = rgb(0x9E, 0x9E, 0x9E) -DARK_GREY = rgb(0x3E, 0x3E, 0x3E) -BLUE_GRAY = rgb(0x60, 0x7D, 0x8B) BLACK = rgb(0x00, 0x00, 0x00) WHITE = rgb(0xFA, 0xFA, 0xFA) -BLACKISH = rgb(0x30, 0x30, 0x30) -DARK_BLACK = rgb(0x10, 0x10, 0x10) -DARK_WHITE = rgb(0xE8, 0xE8, 0xE8) TITLE_GREY = rgb(0x9B, 0x9B, 0x9B) -ORANGE_ICON = rgb(0xF5, 0xA6, 0x23) # common color styles BG = BLACK FG = WHITE # icons -ICON_RESET = "trezor/res/header_icons/reset.toif" -ICON_WIPE = "trezor/res/header_icons/wipe.toif" -ICON_RECOVERY = "trezor/res/header_icons/recovery.toif" -ICON_NOCOPY = "trezor/res/header_icons/nocopy.toif" -ICON_WRONG = "trezor/res/header_icons/wrong.toif" ICON_CONFIG = "trezor/res/header_icons/cog.toif" -ICON_RECEIVE = "trezor/res/header_icons/receive.toif" ICON_SEND = "trezor/res/header_icons/send.toif" - -ICON_DEFAULT = ICON_CONFIG - -ICON_CANCEL = "trezor/res/cancel.toif" -ICON_CONFIRM = "trezor/res/confirm.toif" -ICON_LOCK = "trezor/res/lock.toif" ICON_CLICK = "trezor/res/click.toif" -ICON_BACK = "trezor/res/left.toif" -ICON_SWIPE = "trezor/res/swipe.toif" -ICON_SWIPE_LEFT = "trezor/res/swipe_left.toif" -ICON_SWIPE_RIGHT = "trezor/res/swipe_right.toif" ICON_CHECK = "trezor/res/check.toif" -ICON_SPACE = "trezor/res/space.toif" +ICON_DEFAULT = ICON_CONFIG diff --git a/core/tests/test_trezor.ui.text.py b/core/tests/test_trezor.ui.text.py deleted file mode 100644 index 823a11b94..000000000 --- a/core/tests/test_trezor.ui.text.py +++ /dev/null @@ -1,150 +0,0 @@ -import mock -from common import * - -from trezor import ui -from trezor.ui import display -from trezor.ui.components.common import text - -if False: - from typing import List, Tuple - - -class TestTextSpan(unittest.TestCase): - def lines(self, span: text.Span) -> List[Tuple[str, int, bool]]: - result = [] - while True: - should_continue = span.next_line() - substr = span.string[span.start : span.start + span.length] - result.append((substr, span.width, span.word_break)) - if not should_continue: - break - return result - - def checkSpanWithoutWidths( - self, span: text.Span, expected: List[Tuple[str, bool]] - ) -> None: - expected_with_calculated_widths = [ - (string, ui.display.text_width(string, span.font), word_break) - for string, word_break in expected - ] - self.assertListEqual(self.lines(span), expected_with_calculated_widths) - - def test_basic(self): - span = text.Span("hello") - self.checkSpanWithoutWidths( - span, - [("hello", False)], - ) - - span.reset("world", start=0, font=ui.NORMAL) - self.checkSpanWithoutWidths( - span, - [("world", False)], - ) - - span.reset("", start=0, font=ui.NORMAL) - self.checkSpanWithoutWidths( - span, - [("", False)], - ) - - def test_two_lines(self): - line_width = display.text_width("hello world", ui.NORMAL) - 1 - span = text.Span("hello world", line_width=line_width) - self.checkSpanWithoutWidths( - span, - [ - ("hello", False), - ("world", False), - ], - ) - - def test_newlines(self): - span = text.Span("hello\nworld") - self.checkSpanWithoutWidths( - span, - [ - ("hello", False), - ("world", False), - ], - ) - - span = text.Span("\nhello\nworld\n") - self.checkSpanWithoutWidths( - span, - [ - ("", False), - ("hello", False), - ("world", False), - ("", False), - ], - ) - - def test_break_words(self): - line_width = display.text_width("hello w", ui.NORMAL) + text.DASH_WIDTH - span = text.Span("hello world", line_width=line_width, break_words=True) - self.checkSpanWithoutWidths( - span, - [ - ("hello w", True), - ("orld", False), - ], - ) - - def test_long_word(self): - line_width = display.text_width("establishme", ui.NORMAL) + text.DASH_WIDTH - span = text.Span( - "Down with the establishment!", line_width=line_width, break_words=False - ) - self.checkSpanWithoutWidths( - span, - [ - ("Down with", False), - ("the", False), - ("establishme", True), - ("nt!", False), - ], - ) - - def test_has_more_content(self): - line_width = display.text_width("hello world", ui.NORMAL) - 1 - span = text.Span("hello world", line_width=line_width) - self.assertTrue(span.has_more_content()) - self.assertTrue(span.next_line()) - self.assertEqual("hello", span.string[span.start : span.start + span.length]) - - # has_more_content is True because there's text remaining on the line - self.assertTrue(span.has_more_content()) - # next_line is False because we should not continue iterating - self.assertFalse(span.next_line()) - self.assertEqual("world", span.string[span.start : span.start + span.length]) - - self.assertFalse(span.has_more_content()) - self.assertFalse(span.next_line()) - self.assertEqual("world", span.string[span.start : span.start + span.length]) - - - def test_has_more_content_trailing_newline(self): - span = text.Span("1\n2\n3\n") - - self.assertTrue(span.has_more_content()) - self.assertTrue(span.next_line()) - self.assertEqual("1", span.string[span.start : span.start + span.length]) - - self.assertTrue(span.has_more_content()) - self.assertTrue(span.next_line()) - self.assertEqual("2", span.string[span.start : span.start + span.length]) - - self.assertTrue(span.has_more_content()) - self.assertTrue(span.next_line()) - self.assertEqual("3", span.string[span.start : span.start + span.length]) - - # has_more_content is False because the "remaining" text is empty - self.assertFalse(span.has_more_content()) - # next_line is False because we should not continue iterating - self.assertFalse(span.next_line()) - self.assertEqual("", span.string[span.start : span.start + span.length]) - - -if __name__ == "__main__": - unittest.main() diff --git a/docs/ci/jobs.md b/docs/ci/jobs.md index 5d7ff7e59..389975cd7 100644 --- a/docs/ci/jobs.md +++ b/docs/ci/jobs.md @@ -54,7 +54,7 @@ or contain `[no changelog]` in the commit message. ## BUILD stage - [build.yml](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml) All builds are published as artifacts so they can be downloaded and used. -Consists of **28 jobs** below: +Consists of **27 jobs** below: ### [core fw regular build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L20) Build of Core into firmware. Regular version. @@ -102,130 +102,127 @@ Frozen version. That means you do not need any other files to run it, it is just a single binary file that you can execute directly. **Are you looking for a Trezor T emulator? This is most likely it.** -### [core unix frozen ui2 debug build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L255) +### [core unix frozen debug asan build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L254) -### [core unix frozen debug asan build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L269) +### [core unix frozen debug build arm](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L270) -### [core unix frozen debug build arm](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L285) +### [core unix frozen btconly debug t1 build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L292) -### [core unix frozen btconly debug t1 build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L307) +### [core macos frozen regular build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L308) -### [core macos frozen regular build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L323) - -### [crypto build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L348) +### [crypto build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L333) Build of our cryptographic library, which is then incorporated into the other builds. -### [legacy fw regular build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L377) +### [legacy fw regular build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L362) -### [legacy fw regular debug build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L393) +### [legacy fw regular debug build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L378) -### [legacy fw btconly build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L410) +### [legacy fw btconly build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L395) -### [legacy fw btconly debug build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L429) +### [legacy fw btconly debug build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L414) -### [legacy emu regular debug build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L450) +### [legacy emu regular debug build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L435) Regular version (not only Bitcoin) of above. **Are you looking for a Trezor One emulator? This is most likely it.** -### [legacy emu regular debug asan build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L465) +### [legacy emu regular debug asan build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L450) -### [legacy emu regular debug build arm](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L483) +### [legacy emu regular debug build arm](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L468) -### [legacy emu btconly debug build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L509) +### [legacy emu btconly debug build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L494) Build of Legacy into UNIX emulator. Use keyboard arrows to emulate button presses. Bitcoin-only version. -### [legacy emu btconly debug asan build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L526) +### [legacy emu btconly debug asan build](https://github.com/trezor/trezor-firmware/blob/master/ci/build.yml#L511) --- ## TEST stage - [test.yml](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml) All the tests run test cases on the freshly built emulators from the previous `BUILD` stage. -Consists of **35 jobs** below: +Consists of **34 jobs** below: -### [core unit test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L15) -Python and rust unit tests, checking TT functionality. +### [core unit python test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L15) +Python unit tests, checking core functionality. -### [core unit ui2 test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L23) +### [core unit rust test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L24) +Rust unit tests. -### [core unit asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L32) +### [core unit asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L33) -### [core unit t1 test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L49) +### [core unit t1 test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L50) -### [core device test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L64) +### [core device test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L65) Device tests for Core. Running device tests and also comparing screens with the expected UI result. See artifacts for a comprehensive report of UI. See [docs/tests/ui-tests](../tests/ui-tests.md) for more info. -### [core device ui2 test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L93) - -### [core device asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L127) +### [core device asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L94) -### [core btconly device test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L146) +### [core btconly device test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L113) Device tests excluding altcoins, only for BTC. -### [core btconly device asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L166) +### [core btconly device asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L133) -### [core monero test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L187) +### [core monero test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L154) Monero tests. -### [core monero asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L206) +### [core monero asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L173) -### [core u2f test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L228) +### [core u2f test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L195) Tests for U2F and HID. -### [core u2f asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L247) +### [core u2f asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L214) -### [core fido2 test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L265) +### [core fido2 test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L232) FIDO2 device tests. -### [core fido2 asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L288) +### [core fido2 asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L255) -### [core click test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L308) +### [core click test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L275) Click tests. See [docs/tests/click-tests](../tests/click-tests.md) for more info. -### [core click asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L325) +### [core click asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L292) -### [core upgrade test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L346) +### [core upgrade test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L313) Upgrade tests. See [docs/tests/upgrade-tests](../tests/upgrade-tests.md) for more info. -### [core upgrade asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L365) +### [core upgrade asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L332) -### [core persistence test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L387) +### [core persistence test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L354) Persistence tests. -### [core persistence asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L403) +### [core persistence asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L370) -### [core hwi test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L421) +### [core hwi test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L388) -### [crypto test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L439) +### [crypto test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L406) -### [legacy device test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L470) +### [legacy device test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L437) -### [legacy asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L497) +### [legacy asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L464) -### [legacy btconly test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L509) +### [legacy btconly test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L476) -### [legacy btconly asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L529) +### [legacy btconly asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L496) -### [legacy upgrade test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L544) +### [legacy upgrade test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L511) -### [legacy upgrade asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L563) +### [legacy upgrade asan test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L530) -### [legacy hwi test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L584) +### [legacy hwi test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L551) -### [python test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L603) +### [python test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L570) -### [python support test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L622) +### [python support test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L589) -### [storage test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L632) +### [storage test](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L599) -### [core unix memory profiler](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L656) +### [core unix memory profiler](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L623) -### [connect test core](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L680) +### [connect test core](https://github.com/trezor/trezor-firmware/blob/master/ci/test.yml#L647) --- ## TEST-HW stage - [test-hw.yml](https://github.com/trezor/trezor-firmware/blob/master/ci/test-hw.yml) @@ -272,7 +269,7 @@ Consists of **2 jobs** below: --- ## DEPLOY stage - [deploy.yml](https://github.com/trezor/trezor-firmware/blob/master/ci/deploy.yml) -Consists of **14 jobs** below: +Consists of **13 jobs** below: ### [release core fw regular deploy](https://github.com/trezor/trezor-firmware/blob/master/ci/deploy.yml#L5) @@ -296,10 +293,8 @@ Consists of **14 jobs** below: ### [ui tests fixtures deploy](https://github.com/trezor/trezor-firmware/blob/master/ci/deploy.yml#L229) -### [ui tests ui2 fixtures deploy](https://github.com/trezor/trezor-firmware/blob/master/ci/deploy.yml#L249) - -### [sync emulators to aws](https://github.com/trezor/trezor-firmware/blob/master/ci/deploy.yml#L270) +### [sync emulators to aws](https://github.com/trezor/trezor-firmware/blob/master/ci/deploy.yml#L251) -### [common sync](https://github.com/trezor/trezor-firmware/blob/master/ci/deploy.yml#L295) +### [common sync](https://github.com/trezor/trezor-firmware/blob/master/ci/deploy.yml#L276) --- diff --git a/tests/ui_tests/__init__.py b/tests/ui_tests/__init__.py index c4ad71f49..77f02b7cf 100644 --- a/tests/ui_tests/__init__.py +++ b/tests/ui_tests/__init__.py @@ -1,6 +1,5 @@ import hashlib import json -import os import re import shutil from contextlib import contextmanager @@ -123,8 +122,6 @@ def screen_recording( # Making the model global for other functions global MODEL MODEL = f"T{client.features.model}" - if os.getenv("UI2") == "1": - MODEL += "ui2" test_name = f"{MODEL}_{test_name}"