mirror of
https://github.com/SeedSigner/seedsigner.git
synced 2026-10-05 23:18:25 +00:00
SeedQR transcription UI; generalized LargeIconStatusScreen
This commit is contained in:
@@ -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()
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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[:]
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user