From aeb0279f877701bb1312c8eb019e2deb11521f3b Mon Sep 17 00:00:00 2001 From: kdmukai Date: Sat, 12 Mar 2022 22:14:31 -0600 Subject: [PATCH] SeedQR transcription UI; generalized LargeIconStatusScreen --- src/seedsigner/controller.py | 4 +- src/seedsigner/gui/components.py | 20 +- src/seedsigner/gui/renderer.py | 14 +- src/seedsigner/gui/screens/psbt_screens.py | 2 +- src/seedsigner/gui/screens/scan_screens.py | 2 +- src/seedsigner/gui/screens/screen.py | 99 ++--- src/seedsigner/gui/screens/seed_screens.py | 276 ++++++++++++- src/seedsigner/views/old/seed_tools_view.py | 219 ----------- src/seedsigner/views/psbt_views.py | 6 +- src/seedsigner/views/seed_views.py | 406 ++++++++++++++++---- src/seedsigner/views/tools_views.py | 7 +- src/seedsigner/views/view.py | 4 +- 12 files changed, 689 insertions(+), 370 deletions(-) diff --git a/src/seedsigner/controller.py b/src/seedsigner/controller.py index 5d436509..331612d8 100644 --- a/src/seedsigner/controller.py +++ b/src/seedsigner/controller.py @@ -150,8 +150,8 @@ class Controller(Singleton): # TODO: Remove for v0.5.0 production release WarningScreen( title="Warning", - warning_headline="Pre-Release Code", - warning_text="Do not use this with real funds or to create new secure keys!", + status_headline="Pre-Release Code", + text="Do not use this with real funds or to create new secure keys!", show_back_button=False, ).display() diff --git a/src/seedsigner/gui/components.py b/src/seedsigner/gui/components.py index 1a622522..f4db52e9 100644 --- a/src/seedsigner/gui/components.py +++ b/src/seedsigner/gui/components.py @@ -18,6 +18,7 @@ class GUIConstants: BACKGROUND_COLOR = "black" WARNING_COLOR = "#FFD60A" DIRE_WARNING_COLOR = "red" + SUCCESS_COLOR = "#00dd00" BITCOIN_ORANGE = "#ff9416" ACCENT_COLOR = "orange" @@ -320,12 +321,18 @@ class TextArea(BaseComponent): # No whitespace chars to split on! raise TextDoesNotFitException("Text cannot fit in target rect with this font/size") - words = self.text.split() - while words: - (index, tw) = _binary_len_search(0, len(words)) - _add_text_line(" ".join(words[0:index]), tw) - words = words[index:] + for line in self.text.split("\n"): + words = line.split() + if not words: + # It's a blank line + _add_text_line("", 0) + else: + while words: + (index, tw) = _binary_len_search(0, len(words)) + _add_text_line(" ".join(words[0:index]), tw) + words = words[index:] + # TODO: Don't render blank lines as full height total_text_height = self.bbox_height * len(self.text_lines) + self.line_spacing * (len(self.text_lines) - 1) if self.height is None: # Autoscale height to text lines @@ -412,6 +419,7 @@ class IconTextLine(BaseComponent): font_name: str = GUIConstants.BODY_FONT_NAME font_size: int = GUIConstants.BODY_FONT_SIZE is_text_centered: bool = False + auto_line_break: bool = False screen_x: int = 0 screen_y: int = 0 @@ -468,7 +476,7 @@ class IconTextLine(BaseComponent): font_size=self.font_size, edge_padding=0, is_text_centered=False, - auto_line_break=False, + auto_line_break=self.auto_line_break, screen_x=text_screen_x, screen_y=value_textarea_screen_y, ) diff --git a/src/seedsigner/gui/renderer.py b/src/seedsigner/gui/renderer.py index f5e48cdf..e0e321e5 100644 --- a/src/seedsigner/gui/renderer.py +++ b/src/seedsigner/gui/renderer.py @@ -39,15 +39,16 @@ class Renderer(ConfigurableSingleton): def show_image(self, image=None, alpha_overlay=None): - if image == None: - image = self.canvas - if alpha_overlay: + if image == None: + image = self.canvas image = Image.alpha_composite(image, alpha_overlay) - # Always write to the current canvas, rather than trying to replace it - self.canvas.paste(image) - self.disp.ShowImage(image, 0, 0) + if image: + # Always write to the current canvas, rather than trying to replace it + self.canvas.paste(image) + + self.disp.ShowImage(self.canvas, 0, 0) def show_image_pan(self, image, start_x, start_y, end_x, end_y, rate, alpha_overlay=None): @@ -87,7 +88,6 @@ class Renderer(ConfigurableSingleton): # TODO: Remove all references def show_image_with_text(self, image, text, font=None, text_color="GREY", text_background=None): image_copy = image.copy().convert("RGBA") - draw = ImageDraw.Draw(image_copy) text_overlay = Image.new("RGBA", (self.canvas_width, self.canvas_height), (255,255,255,0)) text_overlay_draw = ImageDraw.Draw(text_overlay) diff --git a/src/seedsigner/gui/screens/psbt_screens.py b/src/seedsigner/gui/screens/psbt_screens.py index 028d8a01..c899381e 100644 --- a/src/seedsigner/gui/screens/psbt_screens.py +++ b/src/seedsigner/gui/screens/psbt_screens.py @@ -721,7 +721,7 @@ class PSBTFinalizeScreen(ButtonListScreen): icon = Icon( icon_name=FontAwesomeIconConstants.PAPER_PLANE, - icon_color="#00dd00", + icon_color=GUIConstants.SUCCESS_COLOR, icon_size=GUIConstants.ICON_LARGE_BUTTON_SIZE, screen_y=self.top_nav.height + GUIConstants.COMPONENT_PADDING ) diff --git a/src/seedsigner/gui/screens/scan_screens.py b/src/seedsigner/gui/screens/scan_screens.py index 1df21285..06b620bf 100644 --- a/src/seedsigner/gui/screens/scan_screens.py +++ b/src/seedsigner/gui/screens/scan_screens.py @@ -15,6 +15,7 @@ from seedsigner.models import DecodeQR, DecodeQRStatus @dataclass class ScanScreen(BaseTopNavScreen): decoder: DecodeQR = None + instructions_text: str = "Scan a QR code" def __post_init__(self): from seedsigner.hardware.camera import Camera @@ -33,7 +34,6 @@ class ScanScreen(BaseTopNavScreen): self.instructions_background_y = self.canvas_height - self.instructions_background.height # Pre-calc where the instruction text goes - self.instructions_text = "Scan a QR code" self.instructions_font = Fonts.get_font(GUIConstants.BUTTON_FONT_NAME, GUIConstants.BUTTON_FONT_SIZE) # TODO: Add the QR code icon and adjust start_x diff --git a/src/seedsigner/gui/screens/screen.py b/src/seedsigner/gui/screens/screen.py index fa906e63..34dfa43d 100644 --- a/src/seedsigner/gui/screens/screen.py +++ b/src/seedsigner/gui/screens/screen.py @@ -694,6 +694,50 @@ class QRDisplayScreen(BaseScreen): +@dataclass +class LargeIconStatusScreen(ButtonListScreen): + title: str = "Success!" + status_icon_name: str = SeedSignerCustomIconConstants.CIRCLE_CHECK + status_icon_size: int = GUIConstants.ICON_PRIMARY_SCREEN_SIZE + status_color: str = GUIConstants.SUCCESS_COLOR + status_headline: str = "Success!" # The colored text under the large icon + text: str = "" # The body text of the screen + button_data: list = None + + def __post_init__(self): + self.is_bottom_list: bool = True + if not self.button_data: + self.button_data = ["OK"] + super().__post_init__() + + self.status_icon = Icon( + icon_name=self.status_icon_name, + icon_size=self.status_icon_size, + icon_color=self.status_color, + ) + self.status_icon.screen_y = self.top_nav.height - int(GUIConstants.COMPONENT_PADDING/2) + self.status_icon.screen_x = int((self.canvas_width - self.status_icon.width) / 2) + self.components.append(self.status_icon) + + next_y = self.status_icon.screen_y + self.status_icon.height + 4 + if self.status_headline: + self.warning_headline_textarea = TextArea( + text=self.status_headline, + width=self.canvas_width, + screen_y=next_y, + font_color=self.status_color, + ) + self.components.append(self.warning_headline_textarea) + next_y = next_y + self.warning_headline_textarea.height + + self.components.append(TextArea( + height=self.buttons[0].screen_y - next_y, + text=self.text, + width=self.canvas_width, + screen_y=next_y, + )) + + class WarningEdgesThread(BaseThread): def __init__(self, args): super().__init__() @@ -707,15 +751,15 @@ class WarningEdgesThread(BaseThread): inhale_hold = 8 cur_inhale_hold = 0 inhale_factor = 0 - rgb = ImageColor.getrgb(screen.warning_color) + rgb = ImageColor.getrgb(screen.status_color) def render_border(color, width): - screen.image_draw.rounded_rectangle( + screen.image_draw.rectangle( (0, 0, screen.canvas_width, screen.canvas_height), fill=None, outline=color, width=width, - radius=5 + # radius=5 ) while self.keep_running: @@ -755,7 +799,7 @@ class WarningEdgesThread(BaseThread): @dataclass class WarningEdgesMixin: - warning_color: str = GUIConstants.WARNING_COLOR + status_color: str = GUIConstants.WARNING_COLOR def __post_init__(self): super().__post_init__() @@ -765,54 +809,21 @@ class WarningEdgesMixin: @dataclass -class WarningScreen(WarningEdgesMixin, ButtonListScreen): +class WarningScreen(WarningEdgesMixin, LargeIconStatusScreen): title: str = "Caution" - warning_icon_name: str = SeedSignerCustomIconConstants.CIRCLE_EXCLAMATION - warning_icon_size=GUIConstants.ICON_PRIMARY_SCREEN_SIZE - warning_headline: str = "Privacy Leak!" # The colored text under the alert icon - warning_text: str = "" # The body text of the warning - button_data: list = None + status_icon_name: str = SeedSignerCustomIconConstants.CIRCLE_EXCLAMATION + status_color: str = "yellow" + status_headline: str = "Privacy Leak!" # The colored text under the alert icon def __post_init__(self): - self.is_bottom_list: bool = True if not self.button_data: self.button_data = ["I Understand"] super().__post_init__() - self.warning_icon = Icon( - icon_name=self.warning_icon_name, - icon_size=self.warning_icon_size, - icon_color=self.warning_color, - ) - self.warning_icon.screen_y = self.top_nav.height - int(GUIConstants.COMPONENT_PADDING/2) - self.warning_icon.screen_x = int((self.canvas_width - self.warning_icon.width) / 2) - self.components.append(self.warning_icon) - - next_y = self.warning_icon.screen_y + self.warning_icon.height + 4 - if self.warning_headline: - self.warning_headline_textarea = TextArea( - text=self.warning_headline, - width=self.canvas_width, - screen_y=next_y, - font_color=self.warning_color, - ) - self.components.append(self.warning_headline_textarea) - next_y = next_y + self.warning_headline_textarea.height - - self.warning_text_textarea = TextArea( - height=self.buttons[0].screen_y - next_y, - text=self.warning_text, - width=self.canvas_width, - screen_y=next_y, - ) - self.components.append(self.warning_text_textarea) - - @dataclass class DireWarningScreen(WarningScreen): - title: str = "Caution" - warning_icon_name: str = SeedSignerCustomIconConstants.CIRCLE_EXCLAMATION - warning_headline: str = "Classified Info!" # The colored text under the alert icon - warning_color: str = GUIConstants.DIRE_WARNING_COLOR + status_headline: str = "Classified Info!" # The colored text under the alert icon + status_color: str = GUIConstants.DIRE_WARNING_COLOR + diff --git a/src/seedsigner/gui/screens/seed_screens.py b/src/seedsigner/gui/screens/seed_screens.py index b2c1877c..f46a3447 100644 --- a/src/seedsigner/gui/screens/seed_screens.py +++ b/src/seedsigner/gui/screens/seed_screens.py @@ -6,12 +6,14 @@ from typing import List from PIL import Image, ImageDraw, ImageFilter from seedsigner.gui.renderer import Renderer +from seedsigner.helpers.qr import QR +from seedsigner.models.qr_type import QRType from seedsigner.models.threads import BaseThread, ThreadsafeCounter from seedsigner.models.seed import Seed from seedsigner.models.settings_definition import SettingsConstants -from .screen import RET_CODE__BACK_BUTTON, BaseTopNavScreen, ButtonListScreen, WarningEdgesMixin +from .screen import RET_CODE__BACK_BUTTON, BaseScreen, BaseTopNavScreen, ButtonListScreen, WarningEdgesMixin from ..components import (FontAwesomeIconConstants, Fonts, FormattedAddress, IconTextLine, SeedSignerCustomIconConstants, TextArea, GUIConstants, calc_text_centering) @@ -447,7 +449,7 @@ class SeedWordsScreen(WarningEdgesMixin, ButtonListScreen): page_index: int = 0 num_pages: int = 3 is_bottom_list: bool = True - warning_color: str = GUIConstants.DIRE_WARNING_COLOR + status_color: str = GUIConstants.DIRE_WARNING_COLOR def __post_init__(self): @@ -1072,6 +1074,276 @@ class SeedReviewPassphraseScreen(ButtonListScreen): +@dataclass +class SeedTranscribeSeedQRFormatScreen(ButtonListScreen): + def __post_init__(self): + self.is_bottom_list = True + super().__post_init__() + + self.components.append(IconTextLine( + label_text="Standard", + value_text="BIP-39 wordlist indices", + is_text_centered=False, + auto_line_break=True, + screen_x=GUIConstants.EDGE_PADDING, + screen_y=self.top_nav.height + GUIConstants.COMPONENT_PADDING, + )) + self.components.append(IconTextLine( + label_text="Compact", + value_text="Raw entropy bits", + is_text_centered=False, + screen_x=GUIConstants.EDGE_PADDING, + screen_y=self.components[-1].screen_y + self.components[-1].height + 2*GUIConstants.COMPONENT_PADDING, + )) + + # self.components.append(TextArea( + # text="Learn more about the SeedQR formats at:", + # is_text_centered=True, + # screen_y=self.top_nav.height + GUIConstants.COMPONENT_PADDING, + # )) + # self.components.append(TextArea( + # text="seedsigner.com", + # is_text_centered=True, + # screen_y=self.components[-1].screen_y + self.components[-1].height, + # height=self.buttons[0].screen_y - (self.components[-1].screen_y + self.components[-1].height), + # )) + + +@dataclass +class SeedTranscribeSeedQRWholeQRScreen(WarningEdgesMixin, ButtonListScreen): + qr_data: str = None + num_modules: int = None + + def __post_init__(self): + self.title = "Transcribe SeedQR" + self.button_data = [f"Begin {self.num_modules}x{self.num_modules}"] + self.is_bottom_list = True + self.status_color = GUIConstants.DIRE_WARNING_COLOR + super().__post_init__() + + qr_height = self.buttons[0].screen_y - self.top_nav.height - GUIConstants.COMPONENT_PADDING + qr_width = qr_height + + qr = QR() + qr_image = qr.qrimage( + data=self.qr_data, + width=qr_width, + height=qr_height, + border=1, + style=QR.STYLE__ROUNDED + ).convert("RGBA") + + self.paste_images.append((qr_image, (int((self.canvas_width - qr_width)/2), self.top_nav.height))) + + # self.instructions = TextArea( + # text="< back | click to begin", + # background_color=GUIConstants.BACKGROUND_COLOR, + # is_text_centered=False, + # screen_y=self.canvas_height - GUIConstants.BODY_FONT_SIZE - 2*GUIConstants.COMPONENT_PADDING, + # height=GUIConstants.BODY_FONT_SIZE + 2*GUIConstants.COMPONENT_PADDING, + # ) + + + +@dataclass +class SeedTranscribeSeedQRZoomedInScreen(BaseScreen): + qr_data: str = None + num_modules: int = None + + def __post_init__(self): + super().__post_init__() + + # Render an oversized QR code that we can view up close + self.pixels_per_block = 24 + + # Border must accommodate the 3 blocks outside the center 5x5 mask plus up to + # 1 empty block inside the 5x5 mask (29x29 has a 4-block final col/row). + self.qr_border = 4 + if self.num_modules == 21: + # Optimize for 21x21 + self.qr_blocks_per_zoom = 7 + else: + self.qr_blocks_per_zoom = 5 + + self.qr_width = (self.qr_border + self.num_modules + self.qr_border) * self.pixels_per_block + self.height = self.qr_width + qr = QR() + self.qr_image = qr.qrimage( + self.qr_data, + width=self.qr_width, + height=self.height, + border=self.qr_border, + style=QR.STYLE__ROUNDED + ).convert("RGBA") + + # Render gridlines but leave the 1-block border as-is + draw = ImageDraw.Draw(self.qr_image) + for i in range(self.qr_border, math.floor(self.qr_width/self.pixels_per_block) - self.qr_border): + draw.line((i * self.pixels_per_block, self.qr_border * self.pixels_per_block, i * self.pixels_per_block, self.height - self.qr_border * self.pixels_per_block), fill="#bbb") + draw.line((self.qr_border * self.pixels_per_block, i * self.pixels_per_block, self.qr_width - self.qr_border * self.pixels_per_block, i * self.pixels_per_block), fill="#bbb") + + # Prep the semi-transparent mask overlay + # make a blank image for the overlay, initialized to transparent + self.block_mask = Image.new("RGBA", (self.canvas_width, self.canvas_height), (255,255,255,0)) + draw = ImageDraw.Draw(self.block_mask) + + self.mask_width = int((self.canvas_width - self.qr_blocks_per_zoom * self.pixels_per_block)/2) + self.mask_height = int((self.canvas_height - self.qr_blocks_per_zoom * self.pixels_per_block)/2) + mask_rgba = (0, 0, 0, 226) + draw.rectangle((0, 0, self.canvas_width, self.mask_height), fill=mask_rgba) + draw.rectangle((0, self.canvas_height - self.mask_height - 1, self.canvas_width, self.canvas_height), fill=mask_rgba) + draw.rectangle((0, self.mask_height, self.mask_width, self.canvas_height - self.mask_height), fill=mask_rgba) + draw.rectangle((self.canvas_width - self.mask_width - 1, self.mask_height, self.canvas_width, self.canvas_height - self.mask_height), fill=mask_rgba) + + # Draw a box around the cutout portion of the mask for better visibility + draw.line((self.mask_width, self.mask_height, self.mask_width, self.canvas_height - self.mask_height), fill=GUIConstants.ACCENT_COLOR) + draw.line((self.canvas_width - self.mask_width, self.mask_height, self.canvas_width - self.mask_width, self.canvas_height - self.mask_height), fill=GUIConstants.ACCENT_COLOR) + draw.line((self.mask_width, self.mask_height, self.canvas_width - self.mask_width, self.mask_height), fill=GUIConstants.ACCENT_COLOR) + draw.line((self.mask_width, self.canvas_height - self.mask_height, self.canvas_width - self.mask_width, self.canvas_height - self.mask_height), fill=GUIConstants.ACCENT_COLOR) + + msg = "click to exit" + font = Fonts.get_font(GUIConstants.BODY_FONT_NAME, GUIConstants.BODY_FONT_SIZE) + (left, top, right, bottom) = font.getbbox(msg, anchor="ls") + msg_height = -1 * top + msg_width = right + # draw.rectangle( + # ( + # int((self.canvas_width - msg_width)/2 - GUIConstants.COMPONENT_PADDING), + # self.canvas_height - msg_height - GUIConstants.COMPONENT_PADDING, + # int((self.canvas_width + msg_width)/2 + GUIConstants.COMPONENT_PADDING), + # self.canvas_height + # ), + # fill=GUIConstants.BACKGROUND_COLOR, + # ) + # draw.text( + # (int(self.canvas_width/2), self.canvas_height - int(GUIConstants.COMPONENT_PADDING/2)), + # msg, + # fill=GUIConstants.BODY_FONT_COLOR, + # font=font, + # anchor="ms" # Middle, baSeline + # ) + TextArea( + canvas=self.block_mask, + image_draw=draw, + text=msg, + background_color=GUIConstants.BACKGROUND_COLOR, + is_text_centered=True, + screen_y=self.canvas_height - GUIConstants.BODY_FONT_SIZE - GUIConstants.COMPONENT_PADDING, + height=GUIConstants.BODY_FONT_SIZE + GUIConstants.COMPONENT_PADDING, + ).render() + + + def draw_block_labels(self, cur_block_x, cur_block_y): + # Create overlay for block labels (e.g. "D-5") + block_labels_x = ["1", "2", "3", "4", "5", "6"] + block_labels_y = ["A", "B", "C", "D", "E", "F"] + + block_labels = Image.new("RGBA", (self.canvas_width, self.canvas_height), (255,255,255,0)) + draw = ImageDraw.Draw(block_labels) + draw.rectangle((self.mask_width, 0, self.canvas_width - self.mask_width, self.pixels_per_block), fill=GUIConstants.ACCENT_COLOR) + draw.rectangle((0, self.mask_height, self.pixels_per_block, self.canvas_height - self.mask_height), fill=GUIConstants.ACCENT_COLOR) + + label_font = Fonts.get_font(GUIConstants.FIXED_WIDTH_EMPHASIS_FONT_NAME, GUIConstants.TOP_NAV_TITLE_FONT_SIZE + 8) + x_label = block_labels_x[cur_block_x] + (left, top, right, bottom) = label_font.getbbox(x_label, anchor="ls") + x_label_height = -1 * top + + draw.text( + (int(self.canvas_width/2), self.pixels_per_block - int((self.pixels_per_block - x_label_height)/2)), + text=x_label, + fill=GUIConstants.BUTTON_SELECTED_FONT_COLOR, + font=label_font, + anchor="ms", # Middle, baSeline + ) + + y_label = block_labels_y[cur_block_y] + (left, top, right, bottom) = label_font.getbbox(y_label, anchor="ls") + y_label_height = -1 * top + draw.text( + (int(self.pixels_per_block/2), int((self.canvas_height + y_label_height) / 2)), + text=y_label, + fill=GUIConstants.BUTTON_SELECTED_FONT_COLOR, + font=label_font, + anchor="ms", # Middle, baSeline + ) + + return block_labels + + + def _run(self): + # Track our current coordinates for the upper left corner of our view + cur_block_x = 0 + cur_block_y = 0 + cur_x = self.qr_border * self.pixels_per_block - self.mask_width + cur_y = self.qr_border * self.pixels_per_block - self.mask_height + next_x = cur_x + next_y = cur_y + + block_labels = self.draw_block_labels(0, 0) + + self.renderer.show_image( + self.qr_image.crop((cur_x, cur_y, cur_x + self.canvas_width, cur_y + self.canvas_height)), + alpha_overlay=Image.alpha_composite(self.block_mask, block_labels) + ) + + while True: + input = self.hw_inputs.wait_for(HardwareButtonsConstants.KEYS__LEFT_RIGHT_UP_DOWN + [HardwareButtonsConstants.KEY_PRESS]) + if input == HardwareButtonsConstants.KEY_RIGHT: + next_x = cur_x + self.qr_blocks_per_zoom * self.pixels_per_block + cur_block_x += 1 + if next_x > self.qr_width - self.canvas_width: + next_x = cur_x + cur_block_x -= 1 + elif input == HardwareButtonsConstants.KEY_LEFT: + next_x = cur_x - self.qr_blocks_per_zoom * self.pixels_per_block + cur_block_x -= 1 + if next_x < 0: + next_x = cur_x + cur_block_x += 1 + elif input == HardwareButtonsConstants.KEY_DOWN: + next_y = cur_y + self.qr_blocks_per_zoom * self.pixels_per_block + cur_block_y += 1 + if next_y > self.height - self.canvas_height: + next_y = cur_y + cur_block_y -= 1 + elif input == HardwareButtonsConstants.KEY_UP: + next_y = cur_y - self.qr_blocks_per_zoom * self.pixels_per_block + cur_block_y -= 1 + if next_y < 0: + next_y = cur_y + cur_block_y += 1 + elif input == HardwareButtonsConstants.KEY_PRESS: + return + + # Create overlay for block labels (e.g. "D-5") + block_labels = self.draw_block_labels(cur_block_x, cur_block_y) + + if cur_x != next_x or cur_y != next_y: + self.renderer.show_image_pan( + self.qr_image, + cur_x, cur_y, next_x, next_y, + rate=self.pixels_per_block, + alpha_overlay=Image.alpha_composite(self.block_mask, block_labels) + ) + cur_x = next_x + cur_y = next_y + + + +@dataclass +class SeedTranscribeSeedQRConfirmQRPromptScreen(ButtonListScreen): + def __post_init__(self): + self.is_bottom_list = True + super().__post_init__() + + self.components.append(TextArea( + text="Optionally scan your transcribed SeedQR to confirm that it reads back correctly.", + screen_y=self.top_nav.height, + height=self.buttons[0].screen_y - self.top_nav.height, + )) + + + @dataclass class SingleSigAddressVerificationScreen(ButtonListScreen): """ diff --git a/src/seedsigner/views/old/seed_tools_view.py b/src/seedsigner/views/old/seed_tools_view.py index 83fdc3ef..1aec77aa 100644 --- a/src/seedsigner/views/old/seed_tools_view.py +++ b/src/seedsigner/views/old/seed_tools_view.py @@ -252,222 +252,3 @@ class SeedToolsView(View): self.renderer.show_image() self.dice_selected = dice_selected - - ### - ### Display Seed Phrase - ### - - def display_seed_qr_options(self, seed_phrase) -> int: - if len(seed_phrase) == 24: - standard = "29x29" - compact = "25x25" - else: - standard = "25x25" - compact = "21x21" - r = self.controller.menu_view.display_generic_selection_menu( - ["... [ Return to Seed Tools ]", f"Standard SeedQR: {standard}", f"Compact SeedQR: {compact}"], - "Transcribe SeedQR" - ) - if r == 2: - return self.seed_phrase_as_qr(seed_phrase, is_compact_seedqr=False) - elif r == 3: - return self.seed_phrase_as_qr(seed_phrase, is_compact_seedqr=True) - else: - return None - - - def seed_phrase_as_qr(self, seed_phrase, is_compact_seedqr=False): - if is_compact_seedqr: - e = EncodeQR(seed_phrase=seed_phrase, qr_type=QRType.SEED__COMPACTSEEDQR, wordlist=self.settings.wordlist) - else: - e = EncodeQR(seed_phrase=seed_phrase, qr_type=QRType.SEED__SEEDQR, wordlist=self.settings.wordlist) - data = e.next_part() - qr = QR() - image = qr.qrimage( - data=data, - width=240, - height=240, - border=3, - style=QR.STYLE__ROUNDED).convert("RGBA") - View.DispShowImageWithText(image, "click to zoom, right to exit", font=View.ASSISTANT18, text_color="BLACK", text_background="ORANGE") - - input = self.buttons.wait_for([B.KEY_RIGHT, B.KEY_PRESS]) - if input == B.KEY_RIGHT: - return - - elif input == B.KEY_PRESS: - # Render an oversized QR code that we can view up close - pixels_per_block = 24 - - # Border must accommodate the 3 blocks outside the center 5x5 mask plus up to - # 4 empty blocks inside the 5x5 mask (21x21 has a 1-block final col/row). - qr_border = 7 - qr_blocks_per_zoom = 5 - if len(seed_phrase) == 24: - if is_compact_seedqr: - num_modules = 25 - else: - num_modules = 29 - else: - if is_compact_seedqr: - num_modules = 21 - - # Optimize for 21x21 - qr_blocks_per_zoom = 7 - else: - num_modules = 25 - - width = (qr_border + num_modules + qr_border) * pixels_per_block - height = width - data = e.next_part() - qr = QR() - image = qr.qrimage( - data, - width=width, - height=height, - border=qr_border, - style=QR.STYLE__ROUNDED).convert("RGBA") - - # Render gridlines but leave the 1-block border as-is - draw = ImageDraw.Draw(image) - for i in range(qr_border, math.floor(width/pixels_per_block) - qr_border): - draw.line((i * pixels_per_block, qr_border * pixels_per_block, i * pixels_per_block, height - qr_border * pixels_per_block), fill="#bbb") - draw.line((qr_border * pixels_per_block, i * pixels_per_block, width - qr_border * pixels_per_block, i * pixels_per_block), fill="#bbb") - - # Prep the semi-transparent mask overlay - # make a blank image for the overlay, initialized to transparent - block_mask = Image.new("RGBA", (self.canvas_width, self.canvas_height), (255,255,255,0)) - draw = ImageDraw.Draw(block_mask) - - mask_width = int((self.canvas_width - qr_blocks_per_zoom * pixels_per_block)/2) - mask_height = int((self.canvas_height - qr_blocks_per_zoom * pixels_per_block)/2) - mask_rgba = (0, 0, 0, 226) - draw.rectangle((0, 0, self.canvas_width, mask_height), fill=mask_rgba) - draw.rectangle((0, self.canvas_height - mask_height - 1, self.canvas_width, self.canvas_height), fill=mask_rgba) - draw.rectangle((0, mask_height, mask_width, self.canvas_height - mask_height), fill=mask_rgba) - draw.rectangle((self.canvas_width - mask_width - 1, mask_height, self.canvas_width, self.canvas_height - mask_height), fill=mask_rgba) - - # Draw a box around the cutout portion of the mask for better visibility - draw.line((mask_width, mask_height, mask_width, self.canvas_height - mask_height), fill=self.color) - draw.line((self.canvas_width - mask_width, mask_height, self.canvas_width - mask_width, self.canvas_height - mask_height), fill=self.color) - draw.line((mask_width, mask_height, self.canvas_width - mask_width, mask_height), fill=self.color) - draw.line((mask_width, self.canvas_height - mask_height, self.canvas_width - mask_width, self.canvas_height - mask_height), fill=self.color) - - msg = "click to exit" - tw, th = draw.textsize(msg, font=Fonts.get_font("Assistant-Medium", 18)) - draw.text(((self.canvas_width - tw) / 2, self.canvas_height - th - 2), msg, fill=self.color, font=Fonts.get_font("Assistant-Medium", 18)) - - def draw_block_labels(cur_block_x, cur_block_y): - # Create overlay for block labels (e.g. "D-5") - block_labels_x = ["1", "2", "3", "4", "5", "6"] - block_labels_y = ["A", "B", "C", "D", "E", "F"] - - block_labels = Image.new("RGBA", (self.canvas_width, self.canvas_height), (255,255,255,0)) - draw = ImageDraw.Draw(block_labels) - draw.rectangle((mask_width, 0, self.canvas_width - mask_width, pixels_per_block), fill=self.color) - draw.rectangle((0, mask_height, pixels_per_block, self.canvas_height - mask_height), fill=self.color) - - label_font = Fonts.get_font("Assistant-Medium", 26) - x_label = block_labels_x[cur_block_x] - tw, th = draw.textsize(x_label, font=label_font) - - # note: have to nudge the y-coord up (the extra "- 4") for some reason - draw.text(((self.canvas_width - tw) / 2, ((pixels_per_block - th) / 2) - 4), x_label, fill="BLACK", font=label_font) - - y_label = block_labels_y[cur_block_y] - tw, th = draw.textsize(y_label, font=label_font) - draw.text(((pixels_per_block - tw) / 2, (self.canvas_height - th) / 2), y_label, fill="BLACK", font=label_font) - - return block_labels - - block_labels = draw_block_labels(0, 0) - - # Track our current coordinates for the upper left corner of our view - cur_block_x = 0 - cur_block_y = 0 - cur_x = qr_border * pixels_per_block - mask_width - cur_y = qr_border * pixels_per_block - mask_height - next_x = cur_x - next_y = cur_y - - self.renderer.show_image( - image.crop((cur_x, cur_y, cur_x + self.canvas_width, cur_y + self.canvas_height)), - alpha_overlay=Image.alpha_composite(block_mask, block_labels) - ) - - while True: - input = self.buttons.wait_for([B.KEY_RIGHT, B.KEY_LEFT, B.KEY_UP, B.KEY_DOWN, B.KEY_PRESS]) - if input == B.KEY_RIGHT: - next_x = cur_x + qr_blocks_per_zoom * pixels_per_block - cur_block_x += 1 - if next_x > width - self.canvas_width: - next_x = cur_x - cur_block_x -= 1 - elif input == B.KEY_LEFT: - next_x = cur_x - qr_blocks_per_zoom * pixels_per_block - cur_block_x -= 1 - if next_x < 0: - next_x = cur_x - cur_block_x += 1 - elif input == B.KEY_DOWN: - next_y = cur_y + qr_blocks_per_zoom * pixels_per_block - cur_block_y += 1 - if next_y > height - self.canvas_height: - next_y = cur_y - cur_block_y -= 1 - elif input == B.KEY_UP: - next_y = cur_y - qr_blocks_per_zoom * pixels_per_block - cur_block_y -= 1 - if next_y < 0: - next_y = cur_y - cur_block_y += 1 - elif input == B.KEY_PRESS: - return - - # Create overlay for block labels (e.g. "D-5") - block_labels = draw_block_labels(cur_block_x, cur_block_y) - - if cur_x != next_x or cur_y != next_y: - self.renderer.show_image_pan( - image, - cur_x, cur_y, next_x, next_y, - rate=pixels_per_block, - alpha_overlay=Image.alpha_composite(block_mask, block_labels) - ) - cur_x = next_x - cur_y = next_y - - def read_seed_phrase_qr(self): - self.draw_modal(["Scanning..."], "Seed QR" ,"Right to Exit") - try: - self.controller.camera.start_video_stream_mode(resolution=(480, 480), framerate=12, format="rgb") - decoder = DecodeQR(wordlist=self.settings.wordlist) - while True: - frame = self.controller.camera.read_video_stream(as_image=True) - if frame is not None: - self.renderer.show_image_with_text(frame.resize((240,240)), "Scan Seed QR", font=Fonts.get_font("Assistant-Medium", 22), text_color=self.color, text_background=(0,0,0,225)) - status = decoder.add_image(frame) - - if status in (DecodeQRStatus.COMPLETE, DecodeQRStatus.INVALID): - break - - if self.buttons.check_for_low(B.KEY_RIGHT) or self.buttons.check_for_low(B.KEY_LEFT): - self.controller.camera.stop_video_stream_mode() - self.words = [] - return self.words[:] - - if decoder.is_complete and decoder.is_seed: - self.words = decoder.get_seed_phrase() - elif not decoder.is_psbt: - self.draw_modal(["Not a valid Seed QR"], "", "Right to Exit") - input = self.buttons.wait_for([B.KEY_RIGHT]) - else: - self.draw_modal(["QR Parsing Failed"], "", "Right to Exit") - input = self.buttons.wait_for([B.KEY_RIGHT]) - self.words = [] - - finally: - self.controller.camera.stop_video_stream_mode() - - return self.words[:] - diff --git a/src/seedsigner/views/psbt_views.py b/src/seedsigner/views/psbt_views.py index 4e2ff558..cefd007d 100644 --- a/src/seedsigner/views/psbt_views.py +++ b/src/seedsigner/views/psbt_views.py @@ -433,9 +433,9 @@ class PSBTSigningErrorView(View): selected_menu_num = WarningScreen( title="PSBT Error", - warning_icon_name=SeedSignerCustomIconConstants.CIRCLE_EXCLAMATION, - warning_headline="Signing Failed", - warning_text="Signing with this seed did not add a valid signature.", + status_icon_name=SeedSignerCustomIconConstants.CIRCLE_EXCLAMATION, + status_headline="Signing Failed", + text="Signing with this seed did not add a valid signature.", button_data=["Select Diff Seed"], ).display() diff --git a/src/seedsigner/views/seed_views.py b/src/seedsigner/views/seed_views.py index 356fe979..8a87276f 100644 --- a/src/seedsigner/views/seed_views.py +++ b/src/seedsigner/views/seed_views.py @@ -3,6 +3,7 @@ import embit import time from binascii import hexlify from embit.networks import NETWORKS +from seedsigner.models.decode_qr import DecodeQR from seedsigner.models.settings_definition import SettingsDefinition @@ -11,7 +12,7 @@ from .view import NotYetImplementedView, View, Destination, BackStackView, MainM from seedsigner.gui.components import FontAwesomeIconConstants, SeedSignerCustomIconConstants from seedsigner.gui.screens import (RET_CODE__BACK_BUTTON, ButtonListScreen, LargeButtonScreen, WarningScreen, DireWarningScreen, seed_screens) -from seedsigner.gui.screens.screen import LoadingScreenThread, QRDisplayScreen +from seedsigner.gui.screens.screen import LargeIconStatusScreen, LoadingScreenThread, QRDisplayScreen from seedsigner.models.threads import BaseThread, ThreadsafeCounter from seedsigner.models.encode_qr import EncodeQR from seedsigner.models.psbt_parser import PSBTParser @@ -35,7 +36,7 @@ class SeedsMenuView(View): def run(self): if not self.seeds: # Nothing to do here unless we have a seed loaded - return Destination(LoadSeedView) + return Destination(LoadSeedView, clear_history=True) button_data = [] for seed in self.seeds: @@ -179,8 +180,8 @@ class SeedMnemonicInvalidView(View): selected_menu_num = WarningScreen( title="Invalid Mnemonic!", - warning_headline=None, - warning_text=f"Checksum failure; not a valid seed phrase.", + status_headline=None, + text=f"Checksum failure; not a valid seed phrase.", show_back_button=False, button_data=button_data, ).display() @@ -304,8 +305,8 @@ class SeedDiscardView(View): fingerprint = self.seed.get_fingerprint(self.settings.get_value(SettingsConstants.SETTING__NETWORK)) selected_menu_num = WarningScreen( title="Discard Seed?", - warning_headline=None, - warning_text=f"Wipe seed {fingerprint} from the device?", + status_headline=None, + text=f"Wipe seed {fingerprint} from the device?", show_back_button=False, button_data=button_data, ).display() @@ -388,81 +389,13 @@ class SeedOptionsView(View): return Destination(SeedExportXpubSigTypeView, view_args={"seed_num": self.seed_num}) elif button_data[selected_menu_num] == EXPORT_SEEDQR: - return Destination(NotYetImplementedView) - # return Destination(SeedExportSeedQRView, view_args={"seed_num": self.seed_num}) + return Destination(SeedTranscribeSeedQRFormatView, view_args={"seed_num": self.seed_num}) elif button_data[selected_menu_num] == DISCARD: return Destination(SeedDiscardView, view_args={"seed_num": self.seed_num}) -"""**************************************************************************** - View Seed Words flow -****************************************************************************""" -class SeedWordsWarningView(View): - def __init__(self, seed_num: int): - super().__init__() - self.seed_num = seed_num - - - def run(self): - destination = Destination(SeedWordsView, view_args={"seed_num": self.seed_num, "page_index": 0}) - if self.settings.get_value(SettingsConstants.SETTING__DIRE_WARNINGS) == SettingsConstants.OPTION__DISABLED: - # Forward straight to showing the words - destination.skip_current_view = True - return destination - - selected_menu_num = DireWarningScreen( - warning_text="""You must keep your seed words private & away from all online devices.""", - ).display() - - if selected_menu_num == 0: - # User clicked "I Understand" - return destination - - elif selected_menu_num == RET_CODE__BACK_BUTTON: - return Destination(BackStackView) - - - -class SeedWordsView(View): - def __init__(self, seed_num: int, page_index: int = 0): - super().__init__() - self.seed_num = seed_num - self.seed = self.controller.get_seed(self.seed_num) - self.page_index = page_index - self.num_pages=int(len(self.seed.mnemonic_list)/4) - - - def run(self): - NEXT = "Next" - DONE = "Done" - - button_data = [] - if self.page_index < self.num_pages - 1: - button_data.append(NEXT) - else: - button_data.append(DONE) - - selected_menu_num = seed_screens.SeedWordsScreen( - seed=self.seed, - page_index=self.page_index, - num_pages=self.num_pages, - button_data=button_data, - ).display() - - if selected_menu_num == RET_CODE__BACK_BUTTON: - return Destination(BackStackView) - - if button_data[selected_menu_num] == NEXT: - return Destination(SeedWordsView, view_args={"seed_num": self.seed_num, "page_index": self.page_index + 1}) - - elif button_data[selected_menu_num] == DONE: - # Must clear history to avoid BACK button returning to private info - return Destination(SeedOptionsView, view_args={"seed_num": self.seed_num}, clear_history=True) - - - """**************************************************************************** Export Xpub flow ****************************************************************************""" @@ -624,8 +557,8 @@ class SeedExportXpubWarningView(View): return destination selected_menu_num = WarningScreen( - warning_headline="Privacy Leak!", - warning_text="""Xpub can be used to view all future transactions.""", + status_headline="Privacy Leak!", + text="""Xpub can be used to view all future transactions.""", ).display() if selected_menu_num == 0: @@ -753,6 +686,325 @@ class SeedExportXpubQRDisplayView(View): +"""**************************************************************************** + View Seed Words flow +****************************************************************************""" +class SeedWordsWarningView(View): + def __init__(self, seed_num: int): + super().__init__() + self.seed_num = seed_num + + + def run(self): + destination = Destination(SeedWordsView, view_args={"seed_num": self.seed_num, "page_index": 0}) + if self.settings.get_value(SettingsConstants.SETTING__DIRE_WARNINGS) == SettingsConstants.OPTION__DISABLED: + # Forward straight to showing the words + destination.skip_current_view = True + return destination + + selected_menu_num = DireWarningScreen( + text="""You must keep your seed words private & away from all online devices.""", + ).display() + + if selected_menu_num == 0: + # User clicked "I Understand" + return destination + + elif selected_menu_num == RET_CODE__BACK_BUTTON: + return Destination(BackStackView) + + + +class SeedWordsView(View): + def __init__(self, seed_num: int, page_index: int = 0): + super().__init__() + self.seed_num = seed_num + self.seed = self.controller.get_seed(self.seed_num) + self.page_index = page_index + self.num_pages=int(len(self.seed.mnemonic_list)/4) + + + def run(self): + NEXT = "Next" + DONE = "Done" + + button_data = [] + if self.page_index < self.num_pages - 1: + button_data.append(NEXT) + else: + button_data.append(DONE) + + selected_menu_num = seed_screens.SeedWordsScreen( + seed=self.seed, + page_index=self.page_index, + num_pages=self.num_pages, + button_data=button_data, + ).display() + + if selected_menu_num == RET_CODE__BACK_BUTTON: + return Destination(BackStackView) + + if button_data[selected_menu_num] == NEXT: + return Destination(SeedWordsView, view_args={"seed_num": self.seed_num, "page_index": self.page_index + 1}) + + elif button_data[selected_menu_num] == DONE: + # Must clear history to avoid BACK button returning to private info + return Destination(SeedOptionsView, view_args={"seed_num": self.seed_num}, clear_history=True) + + + +"""**************************************************************************** + Export as SeedQR +****************************************************************************""" +class SeedTranscribeSeedQRFormatView(View): + def __init__(self, seed_num: int): + super().__init__() + self.seed_num = seed_num + + + def run(self): + if self.settings.get_value(SettingsConstants.SETTING__COMPACT_SEEDQR) != SettingsConstants.OPTION__ENABLED: + # Only configured for standard SeedQR + return Destination( + SeedTranscribeSeedQRWarningView, + view_args={ + "seed_num": self.seed_num, + "seedqr_format": QRType.SEED__SEEDQR, + }, + skip_current_view=True, + ) + + seed = self.controller.get_seed(self.seed_num) + if len(seed.mnemonic_list) == 12: + STANDARD = "Standard: 25x25" + COMPACT = "Compact: 21x21" + num_modules_standard = 25 + num_modules_compact = 21 + else: + STANDARD = "Standard: 29x29" + COMPACT = "Compact: 25x25" + num_modules_standard = 29 + num_modules_compact = 25 + + button_data = [STANDARD, COMPACT] + + selected_menu_num = seed_screens.SeedTranscribeSeedQRFormatScreen( + title="SeedQR Format", + button_data=button_data, + ).display() + + if selected_menu_num == RET_CODE__BACK_BUTTON: + return Destination(BackStackView) + + if button_data[selected_menu_num] == STANDARD: + seedqr_format = QRType.SEED__SEEDQR + num_modules = num_modules_standard + else: + seedqr_format = QRType.SEED__COMPACTSEEDQR + num_modules = num_modules_compact + + return Destination( + SeedTranscribeSeedQRWarningView, + view_args={ + "seed_num": self.seed_num, + "seedqr_format": seedqr_format, + "num_modules": num_modules, + } + ) + + + +class SeedTranscribeSeedQRWarningView(View): + def __init__(self, seed_num: int, seedqr_format: str = QRType.SEED__SEEDQR, num_modules: int = 29): + super().__init__() + self.seed_num = seed_num + self.seedqr_format = seedqr_format + self.num_modules = num_modules + + + def run(self): + destination = Destination( + SeedTranscribeSeedQRWholeQRView, + view_args={ + "seed_num": self.seed_num, + "seedqr_format": self.seedqr_format, + "num_modules": self.num_modules, + } + ) + + if self.settings.get_value(SettingsConstants.SETTING__DIRE_WARNINGS) == SettingsConstants.OPTION__DISABLED: + # Forward straight to transcribing the SeedQR + destination.skip_current_view = True + return destination + + selected_menu_num = DireWarningScreen( + status_headline="SeedQR is your private key!", + text="""Never photograph it or scan it into an online device.""", + ).display() + + if selected_menu_num == RET_CODE__BACK_BUTTON: + return Destination(BackStackView) + + else: + # User clicked "I Understand" + return destination + + + +class SeedTranscribeSeedQRWholeQRView(View): + def __init__(self, seed_num: int, seedqr_format: str, num_modules: int): + super().__init__() + self.seed_num = seed_num + self.seedqr_format = seedqr_format + self.num_modules = num_modules + self.seed = self.controller.get_seed(seed_num) + + + def run(self): + e = EncodeQR( + seed_phrase=self.seed.mnemonic_list, + qr_type=self.seedqr_format, + wordlist_language_code=self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE) + ) + data = e.next_part() + + ret = seed_screens.SeedTranscribeSeedQRWholeQRScreen( + qr_data=data, + num_modules=self.num_modules, + ).display() + + if ret == RET_CODE__BACK_BUTTON: + return Destination(BackStackView) + + else: + return Destination( + SeedTranscribeSeedQRZoomedInView, + view_args={ + "seed_num": self.seed_num, + "seedqr_format": self.seedqr_format + } + ) + + + +class SeedTranscribeSeedQRZoomedInView(View): + def __init__(self, seed_num: int, seedqr_format: str): + super().__init__() + self.seed_num = seed_num + self.seedqr_format = seedqr_format + self.seed = self.controller.get_seed(seed_num) + + + def run(self): + e = EncodeQR( + seed_phrase=self.seed.mnemonic_list, + qr_type=self.seedqr_format, + wordlist_language_code=self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE) + ) + data = e.next_part() + + if len(self.seed.mnemonic_list) == 24: + if self.seedqr_format == QRType.SEED__COMPACTSEEDQR: + num_modules = 25 + else: + num_modules = 29 + else: + if self.seedqr_format == QRType.SEED__COMPACTSEEDQR: + num_modules = 21 + else: + num_modules = 25 + + seed_screens.SeedTranscribeSeedQRZoomedInScreen( + qr_data=data, + num_modules=num_modules, + ).display() + + return Destination(SeedTranscribeSeedQRConfirmQRPromptView, view_args={"seed_num": self.seed_num}) + + + +class SeedTranscribeSeedQRConfirmQRPromptView(View): + def __init__(self, seed_num: int): + super().__init__() + self.seed_num = seed_num + self.seed = self.controller.get_seed(seed_num) + + + def run(self): + SCAN = ("Confirm SeedQR", FontAwesomeIconConstants.QRCODE) + DONE = "Done" + button_data = [SCAN, DONE] + + selected_menu_option = seed_screens.SeedTranscribeSeedQRConfirmQRPromptScreen( + title="Confirm SeedQR?", + button_data=button_data, + ).display() + + if selected_menu_option == RET_CODE__BACK_BUTTON: + return Destination(BackStackView) + + elif button_data[selected_menu_option] == SCAN: + return Destination(SeedTranscribeSeedQRConfirmScanView, view_args={"seed_num": self.seed_num}) + + elif button_data[selected_menu_option] == DONE: + return Destination(SeedOptionsView, view_args={"seed_num": self.seed_num}, clear_history=True) + + + +class SeedTranscribeSeedQRConfirmScanView(View): + def __init__(self, seed_num: int): + super().__init__() + self.seed_num = seed_num + self.seed = self.controller.get_seed(seed_num) + + def run(self): + from seedsigner.gui.screens.scan_screens import ScanScreen + + # Run the live preview and QR code capture process + # TODO: Does this belong in its own BaseThread? + wordlist_language_code = self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE) + self.decoder = DecodeQR(wordlist_language_code=wordlist_language_code) + ScanScreen(decoder=self.decoder, instructions_text="Scan your SeedQR").display() + + if self.decoder.is_complete: + if self.decoder.is_seed: + seed_mnemonic = self.decoder.get_seed_phrase() + # Found a valid mnemonic seed! But does it match? + if seed_mnemonic != self.seed.mnemonic_list: + DireWarningScreen( + title="Confirm SeedQR", + status_headline="Error!", + text="Your transcribed SeedQR does not match your original seed!", + show_back_button=False, + button_data=["Review SeedQR"], + ).display() + + return Destination(BackStackView, skip_current_view=True) + + else: + LargeIconStatusScreen( + title="Confirm SeedQR", + status_headline="Success!", + text="Your transcribed SeedQR successfully scanned and yielded the same seed.", + show_back_button=False, + button_data=["OK"], + ).display() + + return Destination(SeedOptionsView, view_args={"seed_num": self.seed_num}) + + else: + # Will this case ever happen? Will trigger if a different kind of QR code is scanned + DireWarningScreen( + title="Confirm SeedQR", + status_headline="Error!", + text="Your transcribed SeedQR could not be read!", + show_back_button=False, + button_data=["Review SeedQR"], + ).display() + + return Destination(BackStackView, skip_current_view=True) + + class SeedSingleSigAddressVerificationView(View): """ TODO: Reserved for Nick. diff --git a/src/seedsigner/views/tools_views.py b/src/seedsigner/views/tools_views.py index 17d08d74..25bc4e40 100644 --- a/src/seedsigner/views/tools_views.py +++ b/src/seedsigner/views/tools_views.py @@ -103,12 +103,7 @@ class ToolsImageEntropyMnemonicLengthView(View): TWENTYFOUR_WORDS = "24 words" button_data = [TWELVE_WORDS, TWENTYFOUR_WORDS] - # selected_menu_num = ButtonListScreen( - # title="Mnemonic Length?", - # button_data=button_data, - # ).display() - - selected_menu_num = LargeButtonScreen( + selected_menu_num = ButtonListScreen( title="Mnemonic Length?", button_data=button_data, ).display() diff --git a/src/seedsigner/views/view.py b/src/seedsigner/views/view.py index 9677185e..f4117221 100644 --- a/src/seedsigner/views/view.py +++ b/src/seedsigner/views/view.py @@ -210,8 +210,8 @@ class NotYetImplementedView(View): def run(self): WarningScreen( title="Work In Progress", - warning_headline="Not Yet Implemented", - warning_text="This is still on our to-do list!", + status_headline="Not Yet Implemented", + text="This is still on our to-do list!", button_data=["Back to Main Menu"], ).display()