bauiv1lib.account.v2proxy
V2 account ui bits.
1# Released under the MIT License. See LICENSE for details. 2# 3"""V2 account ui bits.""" 4 5from __future__ import annotations 6 7import time 8import logging 9 10from efro.error import CommunicationError 11import bacommon.cloud 12import bauiv1 as bui 13 14STATUS_CHECK_INTERVAL_SECONDS = 2.0 15 16 17class V2ProxySignInWindow(bui.Window): 18 """A window allowing signing in to a v2 account.""" 19 20 def __init__(self, origin_widget: bui.Widget): 21 self._width = 600 22 self._height = 550 23 self._proxyid: str | None = None 24 self._proxykey: str | None = None 25 self._overlay_web_browser_open = False 26 27 assert bui.app.classic is not None 28 uiscale = bui.app.ui_v1.uiscale 29 super().__init__( 30 root_widget=bui.containerwidget( 31 size=(self._width, self._height), 32 transition='in_scale', 33 scale_origin_stack_offset=( 34 origin_widget.get_screen_space_center() 35 ), 36 scale=( 37 1.25 38 if uiscale is bui.UIScale.SMALL 39 else 1.05 if uiscale is bui.UIScale.MEDIUM else 0.9 40 ), 41 ) 42 ) 43 44 self._state_text = bui.textwidget( 45 parent=self._root_widget, 46 position=(self._width * 0.5, self._height * 0.6), 47 h_align='center', 48 v_align='center', 49 size=(0, 0), 50 scale=1.4, 51 maxwidth=0.9 * self._width, 52 text=bui.Lstr( 53 value='${A}...', 54 subs=[('${A}', bui.Lstr(resource='loadingText'))], 55 ), 56 color=(1, 1, 1), 57 ) 58 self._sub_state_text = bui.textwidget( 59 parent=self._root_widget, 60 position=(self._width * 0.5, self._height * 0.55), 61 h_align='center', 62 v_align='top', 63 scale=0.85, 64 size=(0, 0), 65 maxwidth=0.9 * self._width, 66 text='', 67 ) 68 self._sub_state_text2 = bui.textwidget( 69 parent=self._root_widget, 70 position=(self._width * 0.1, self._height * 0.3), 71 h_align='left', 72 v_align='top', 73 scale=0.7, 74 size=(0, 0), 75 maxwidth=0.9 * self._width, 76 text='', 77 ) 78 79 self._cancel_button = bui.buttonwidget( 80 parent=self._root_widget, 81 position=(30, self._height - 65), 82 size=(130, 50), 83 scale=0.8, 84 label=bui.Lstr(resource='cancelText'), 85 on_activate_call=self._done, 86 autoselect=True, 87 ) 88 89 bui.containerwidget( 90 edit=self._root_widget, cancel_button=self._cancel_button 91 ) 92 93 self._message_in_flight = False 94 self._complete = False 95 self._connection_wait_timeout_time = time.monotonic() + 10.0 96 97 self._update_timer = bui.AppTimer( 98 0.371, bui.WeakCall(self._update), repeat=True 99 ) 100 bui.pushcall(bui.WeakCall(self._update)) 101 102 def _update(self) -> None: 103 104 plus = bui.app.plus 105 assert plus is not None 106 107 # If we've opened an overlay web browser, all we do is kill 108 # ourselves when it closes. 109 if self._overlay_web_browser_open: 110 if not bui.overlay_web_browser_is_open(): 111 self._overlay_web_browser_open = False 112 self._done() 113 return 114 115 if self._message_in_flight or self._complete: 116 return 117 118 now = time.monotonic() 119 120 # Spin for a moment if it looks like we have no server 121 # connection; it might still be getting on its feet. 122 if ( 123 not plus.cloud.connected 124 and now < self._connection_wait_timeout_time 125 ): 126 return 127 128 plus.cloud.send_message_cb( 129 bacommon.cloud.LoginProxyRequestMessage(), 130 on_response=bui.WeakCall(self._on_proxy_request_response), 131 ) 132 self._message_in_flight = True 133 134 def _get_server_address(self) -> str: 135 plus = bui.app.plus 136 assert plus is not None 137 out = plus.get_master_server_address(version=2) 138 assert isinstance(out, str) 139 return out 140 141 def _set_error_state(self, error_location: str) -> None: 142 msaddress = self._get_server_address() 143 addr = msaddress.removeprefix('https://') 144 bui.textwidget( 145 edit=self._state_text, 146 text=f'Unable to connect to {addr}.', 147 color=(1, 0, 0), 148 ) 149 support_email = 'support@froemling.net' 150 bui.textwidget( 151 edit=self._sub_state_text, 152 text=( 153 f'Usually this means your internet is down.\n' 154 f'Please contact {support_email} if this is not the case.' 155 ), 156 color=(1, 0, 0), 157 ) 158 bui.textwidget( 159 edit=self._sub_state_text2, 160 text=( 161 f'debug-info:\n' 162 f' error-location: {error_location}\n' 163 f' connectivity: {bui.app.net.connectivity_state}\n' 164 f' transport: {bui.app.net.transport_state}' 165 ), 166 color=(0.8, 0.2, 0.3), 167 flatness=1.0, 168 shadow=0.0, 169 ) 170 171 def _on_proxy_request_response( 172 self, response: bacommon.cloud.LoginProxyRequestResponse | Exception 173 ) -> None: 174 plus = bui.app.plus 175 assert plus is not None 176 177 if not self._message_in_flight: 178 logging.warning( 179 'v2proxy got _on_proxy_request_response' 180 ' without _message_in_flight set; unexpected.' 181 ) 182 self._message_in_flight = False 183 184 # Something went wrong. Show an error message and schedule retry. 185 if isinstance(response, Exception): 186 self._set_error_state(f'response exc ({type(response).__name__})') 187 self._complete = True 188 return 189 190 self._complete = True 191 192 # Clear out stuff we use to show progress/errors. 193 self._sub_state_text.delete() 194 self._sub_state_text2.delete() 195 196 # If we have overlay-web-browser functionality, bring up 197 # an inline sign-in dialog. 198 if bui.overlay_web_browser_is_supported(): 199 bui.textwidget( 200 edit=self._state_text, 201 text=bui.Lstr(resource='pleaseWaitText'), 202 ) 203 self._show_overlay_sign_in_ui(response) 204 self._overlay_web_browser_open = True 205 else: 206 # Otherwise just show link-button/qr-code for the sign-in. 207 self._state_text.delete() 208 self._show_standard_sign_in_ui(response) 209 210 # In either case, start querying for results now. 211 self._proxyid = response.proxyid 212 self._proxykey = response.proxykey 213 bui.apptimer( 214 STATUS_CHECK_INTERVAL_SECONDS, bui.WeakCall(self._ask_for_status) 215 ) 216 217 def _show_overlay_sign_in_ui( 218 self, response: bacommon.cloud.LoginProxyRequestResponse 219 ) -> None: 220 msaddress = self._get_server_address() 221 address = msaddress + response.url_overlay 222 bui.overlay_web_browser_open_url(address) 223 224 def _show_standard_sign_in_ui( 225 self, response: bacommon.cloud.LoginProxyRequestResponse 226 ) -> None: 227 msaddress = self._get_server_address() 228 229 # Show link(s) the user can use to sign in. 230 address = msaddress + response.url 231 address_pretty = address.removeprefix('https://') 232 233 assert bui.app.classic is not None 234 bui.textwidget( 235 parent=self._root_widget, 236 position=(self._width * 0.5, self._height - 95), 237 size=(0, 0), 238 text=bui.Lstr( 239 resource='accountSettingsWindow.v2LinkInstructionsText' 240 ), 241 color=bui.app.ui_v1.title_color, 242 maxwidth=self._width * 0.9, 243 h_align='center', 244 v_align='center', 245 ) 246 button_width = 450 247 if bui.is_browser_likely_available(): 248 bui.buttonwidget( 249 parent=self._root_widget, 250 position=( 251 (self._width * 0.5 - button_width * 0.5), 252 self._height - 185, 253 ), 254 autoselect=True, 255 size=(button_width, 60), 256 label=bui.Lstr(value=address_pretty), 257 color=(0.55, 0.5, 0.6), 258 textcolor=(0.75, 0.7, 0.8), 259 on_activate_call=lambda: bui.open_url(address), 260 ) 261 qroffs = 0.0 262 else: 263 bui.textwidget( 264 parent=self._root_widget, 265 position=(self._width * 0.5 - 200, self._height - 180), 266 size=(button_width - 50, 50), 267 text=bui.Lstr(value=address_pretty), 268 flatness=1.0, 269 maxwidth=self._width, 270 scale=0.75, 271 h_align='center', 272 v_align='center', 273 autoselect=True, 274 on_activate_call=bui.Call(self._copy_link, address_pretty), 275 selectable=True, 276 ) 277 qroffs = 20.0 278 279 qr_size = 270 280 bui.imagewidget( 281 parent=self._root_widget, 282 position=( 283 self._width * 0.5 - qr_size * 0.5, 284 self._height * 0.36 + qroffs - qr_size * 0.5, 285 ), 286 size=(qr_size, qr_size), 287 texture=bui.get_qrcode_texture(address), 288 ) 289 290 def _ask_for_status(self) -> None: 291 assert self._proxyid is not None 292 assert self._proxykey is not None 293 assert bui.app.plus is not None 294 bui.app.plus.cloud.send_message_cb( 295 bacommon.cloud.LoginProxyStateQueryMessage( 296 proxyid=self._proxyid, proxykey=self._proxykey 297 ), 298 on_response=bui.WeakCall(self._got_status), 299 ) 300 301 def _got_status( 302 self, response: bacommon.cloud.LoginProxyStateQueryResponse | Exception 303 ) -> None: 304 if ( 305 isinstance(response, bacommon.cloud.LoginProxyStateQueryResponse) 306 and response.state is response.State.FAIL 307 ): 308 logging.info('LoginProxy failed.') 309 bui.getsound('error').play() 310 bui.screenmessage(bui.Lstr(resource='errorText'), color=(1, 0, 0)) 311 self._done() 312 return 313 314 # If we got a token, set ourself as signed in. Hooray! 315 if ( 316 isinstance(response, bacommon.cloud.LoginProxyStateQueryResponse) 317 and response.state is response.State.SUCCESS 318 ): 319 plus = bui.app.plus 320 assert plus is not None 321 assert response.credentials is not None 322 plus.accounts.set_primary_credentials(response.credentials) 323 324 # As a courtesy, tell the server we're done with this proxy 325 # so it can clean up (not a huge deal if this fails) 326 assert self._proxyid is not None 327 try: 328 plus.cloud.send_message_cb( 329 bacommon.cloud.LoginProxyCompleteMessage( 330 proxyid=self._proxyid 331 ), 332 on_response=bui.WeakCall(self._proxy_complete_response), 333 ) 334 except CommunicationError: 335 pass 336 except Exception: 337 logging.warning( 338 'Unexpected error sending login-proxy-complete message', 339 exc_info=True, 340 ) 341 342 self._done() 343 return 344 345 # If we're still waiting, ask again soon. 346 if ( 347 isinstance(response, Exception) 348 or response.state is response.State.WAITING 349 ): 350 bui.apptimer( 351 STATUS_CHECK_INTERVAL_SECONDS, 352 bui.WeakCall(self._ask_for_status), 353 ) 354 355 def _proxy_complete_response(self, response: None | Exception) -> None: 356 del response # Not used. 357 # We could do something smart like retry on exceptions here, but 358 # this isn't critical so we'll just let anything slide. 359 360 def _copy_link(self, link: str) -> None: 361 if bui.clipboard_is_supported(): 362 bui.clipboard_set_text(link) 363 bui.screenmessage( 364 bui.Lstr(resource='copyConfirmText'), color=(0, 1, 0) 365 ) 366 367 def _done(self) -> None: 368 # no-op if our underlying widget is dead or on its way out. 369 if not self._root_widget or self._root_widget.transitioning_out: 370 return 371 372 # If we've got an inline browser up, tell it to close. 373 if self._overlay_web_browser_open: 374 bui.overlay_web_browser_close() 375 376 bui.containerwidget(edit=self._root_widget, transition='out_scale')
STATUS_CHECK_INTERVAL_SECONDS =
2.0
class
V2ProxySignInWindow(bauiv1._uitypes.Window):
18class V2ProxySignInWindow(bui.Window): 19 """A window allowing signing in to a v2 account.""" 20 21 def __init__(self, origin_widget: bui.Widget): 22 self._width = 600 23 self._height = 550 24 self._proxyid: str | None = None 25 self._proxykey: str | None = None 26 self._overlay_web_browser_open = False 27 28 assert bui.app.classic is not None 29 uiscale = bui.app.ui_v1.uiscale 30 super().__init__( 31 root_widget=bui.containerwidget( 32 size=(self._width, self._height), 33 transition='in_scale', 34 scale_origin_stack_offset=( 35 origin_widget.get_screen_space_center() 36 ), 37 scale=( 38 1.25 39 if uiscale is bui.UIScale.SMALL 40 else 1.05 if uiscale is bui.UIScale.MEDIUM else 0.9 41 ), 42 ) 43 ) 44 45 self._state_text = bui.textwidget( 46 parent=self._root_widget, 47 position=(self._width * 0.5, self._height * 0.6), 48 h_align='center', 49 v_align='center', 50 size=(0, 0), 51 scale=1.4, 52 maxwidth=0.9 * self._width, 53 text=bui.Lstr( 54 value='${A}...', 55 subs=[('${A}', bui.Lstr(resource='loadingText'))], 56 ), 57 color=(1, 1, 1), 58 ) 59 self._sub_state_text = bui.textwidget( 60 parent=self._root_widget, 61 position=(self._width * 0.5, self._height * 0.55), 62 h_align='center', 63 v_align='top', 64 scale=0.85, 65 size=(0, 0), 66 maxwidth=0.9 * self._width, 67 text='', 68 ) 69 self._sub_state_text2 = bui.textwidget( 70 parent=self._root_widget, 71 position=(self._width * 0.1, self._height * 0.3), 72 h_align='left', 73 v_align='top', 74 scale=0.7, 75 size=(0, 0), 76 maxwidth=0.9 * self._width, 77 text='', 78 ) 79 80 self._cancel_button = bui.buttonwidget( 81 parent=self._root_widget, 82 position=(30, self._height - 65), 83 size=(130, 50), 84 scale=0.8, 85 label=bui.Lstr(resource='cancelText'), 86 on_activate_call=self._done, 87 autoselect=True, 88 ) 89 90 bui.containerwidget( 91 edit=self._root_widget, cancel_button=self._cancel_button 92 ) 93 94 self._message_in_flight = False 95 self._complete = False 96 self._connection_wait_timeout_time = time.monotonic() + 10.0 97 98 self._update_timer = bui.AppTimer( 99 0.371, bui.WeakCall(self._update), repeat=True 100 ) 101 bui.pushcall(bui.WeakCall(self._update)) 102 103 def _update(self) -> None: 104 105 plus = bui.app.plus 106 assert plus is not None 107 108 # If we've opened an overlay web browser, all we do is kill 109 # ourselves when it closes. 110 if self._overlay_web_browser_open: 111 if not bui.overlay_web_browser_is_open(): 112 self._overlay_web_browser_open = False 113 self._done() 114 return 115 116 if self._message_in_flight or self._complete: 117 return 118 119 now = time.monotonic() 120 121 # Spin for a moment if it looks like we have no server 122 # connection; it might still be getting on its feet. 123 if ( 124 not plus.cloud.connected 125 and now < self._connection_wait_timeout_time 126 ): 127 return 128 129 plus.cloud.send_message_cb( 130 bacommon.cloud.LoginProxyRequestMessage(), 131 on_response=bui.WeakCall(self._on_proxy_request_response), 132 ) 133 self._message_in_flight = True 134 135 def _get_server_address(self) -> str: 136 plus = bui.app.plus 137 assert plus is not None 138 out = plus.get_master_server_address(version=2) 139 assert isinstance(out, str) 140 return out 141 142 def _set_error_state(self, error_location: str) -> None: 143 msaddress = self._get_server_address() 144 addr = msaddress.removeprefix('https://') 145 bui.textwidget( 146 edit=self._state_text, 147 text=f'Unable to connect to {addr}.', 148 color=(1, 0, 0), 149 ) 150 support_email = 'support@froemling.net' 151 bui.textwidget( 152 edit=self._sub_state_text, 153 text=( 154 f'Usually this means your internet is down.\n' 155 f'Please contact {support_email} if this is not the case.' 156 ), 157 color=(1, 0, 0), 158 ) 159 bui.textwidget( 160 edit=self._sub_state_text2, 161 text=( 162 f'debug-info:\n' 163 f' error-location: {error_location}\n' 164 f' connectivity: {bui.app.net.connectivity_state}\n' 165 f' transport: {bui.app.net.transport_state}' 166 ), 167 color=(0.8, 0.2, 0.3), 168 flatness=1.0, 169 shadow=0.0, 170 ) 171 172 def _on_proxy_request_response( 173 self, response: bacommon.cloud.LoginProxyRequestResponse | Exception 174 ) -> None: 175 plus = bui.app.plus 176 assert plus is not None 177 178 if not self._message_in_flight: 179 logging.warning( 180 'v2proxy got _on_proxy_request_response' 181 ' without _message_in_flight set; unexpected.' 182 ) 183 self._message_in_flight = False 184 185 # Something went wrong. Show an error message and schedule retry. 186 if isinstance(response, Exception): 187 self._set_error_state(f'response exc ({type(response).__name__})') 188 self._complete = True 189 return 190 191 self._complete = True 192 193 # Clear out stuff we use to show progress/errors. 194 self._sub_state_text.delete() 195 self._sub_state_text2.delete() 196 197 # If we have overlay-web-browser functionality, bring up 198 # an inline sign-in dialog. 199 if bui.overlay_web_browser_is_supported(): 200 bui.textwidget( 201 edit=self._state_text, 202 text=bui.Lstr(resource='pleaseWaitText'), 203 ) 204 self._show_overlay_sign_in_ui(response) 205 self._overlay_web_browser_open = True 206 else: 207 # Otherwise just show link-button/qr-code for the sign-in. 208 self._state_text.delete() 209 self._show_standard_sign_in_ui(response) 210 211 # In either case, start querying for results now. 212 self._proxyid = response.proxyid 213 self._proxykey = response.proxykey 214 bui.apptimer( 215 STATUS_CHECK_INTERVAL_SECONDS, bui.WeakCall(self._ask_for_status) 216 ) 217 218 def _show_overlay_sign_in_ui( 219 self, response: bacommon.cloud.LoginProxyRequestResponse 220 ) -> None: 221 msaddress = self._get_server_address() 222 address = msaddress + response.url_overlay 223 bui.overlay_web_browser_open_url(address) 224 225 def _show_standard_sign_in_ui( 226 self, response: bacommon.cloud.LoginProxyRequestResponse 227 ) -> None: 228 msaddress = self._get_server_address() 229 230 # Show link(s) the user can use to sign in. 231 address = msaddress + response.url 232 address_pretty = address.removeprefix('https://') 233 234 assert bui.app.classic is not None 235 bui.textwidget( 236 parent=self._root_widget, 237 position=(self._width * 0.5, self._height - 95), 238 size=(0, 0), 239 text=bui.Lstr( 240 resource='accountSettingsWindow.v2LinkInstructionsText' 241 ), 242 color=bui.app.ui_v1.title_color, 243 maxwidth=self._width * 0.9, 244 h_align='center', 245 v_align='center', 246 ) 247 button_width = 450 248 if bui.is_browser_likely_available(): 249 bui.buttonwidget( 250 parent=self._root_widget, 251 position=( 252 (self._width * 0.5 - button_width * 0.5), 253 self._height - 185, 254 ), 255 autoselect=True, 256 size=(button_width, 60), 257 label=bui.Lstr(value=address_pretty), 258 color=(0.55, 0.5, 0.6), 259 textcolor=(0.75, 0.7, 0.8), 260 on_activate_call=lambda: bui.open_url(address), 261 ) 262 qroffs = 0.0 263 else: 264 bui.textwidget( 265 parent=self._root_widget, 266 position=(self._width * 0.5 - 200, self._height - 180), 267 size=(button_width - 50, 50), 268 text=bui.Lstr(value=address_pretty), 269 flatness=1.0, 270 maxwidth=self._width, 271 scale=0.75, 272 h_align='center', 273 v_align='center', 274 autoselect=True, 275 on_activate_call=bui.Call(self._copy_link, address_pretty), 276 selectable=True, 277 ) 278 qroffs = 20.0 279 280 qr_size = 270 281 bui.imagewidget( 282 parent=self._root_widget, 283 position=( 284 self._width * 0.5 - qr_size * 0.5, 285 self._height * 0.36 + qroffs - qr_size * 0.5, 286 ), 287 size=(qr_size, qr_size), 288 texture=bui.get_qrcode_texture(address), 289 ) 290 291 def _ask_for_status(self) -> None: 292 assert self._proxyid is not None 293 assert self._proxykey is not None 294 assert bui.app.plus is not None 295 bui.app.plus.cloud.send_message_cb( 296 bacommon.cloud.LoginProxyStateQueryMessage( 297 proxyid=self._proxyid, proxykey=self._proxykey 298 ), 299 on_response=bui.WeakCall(self._got_status), 300 ) 301 302 def _got_status( 303 self, response: bacommon.cloud.LoginProxyStateQueryResponse | Exception 304 ) -> None: 305 if ( 306 isinstance(response, bacommon.cloud.LoginProxyStateQueryResponse) 307 and response.state is response.State.FAIL 308 ): 309 logging.info('LoginProxy failed.') 310 bui.getsound('error').play() 311 bui.screenmessage(bui.Lstr(resource='errorText'), color=(1, 0, 0)) 312 self._done() 313 return 314 315 # If we got a token, set ourself as signed in. Hooray! 316 if ( 317 isinstance(response, bacommon.cloud.LoginProxyStateQueryResponse) 318 and response.state is response.State.SUCCESS 319 ): 320 plus = bui.app.plus 321 assert plus is not None 322 assert response.credentials is not None 323 plus.accounts.set_primary_credentials(response.credentials) 324 325 # As a courtesy, tell the server we're done with this proxy 326 # so it can clean up (not a huge deal if this fails) 327 assert self._proxyid is not None 328 try: 329 plus.cloud.send_message_cb( 330 bacommon.cloud.LoginProxyCompleteMessage( 331 proxyid=self._proxyid 332 ), 333 on_response=bui.WeakCall(self._proxy_complete_response), 334 ) 335 except CommunicationError: 336 pass 337 except Exception: 338 logging.warning( 339 'Unexpected error sending login-proxy-complete message', 340 exc_info=True, 341 ) 342 343 self._done() 344 return 345 346 # If we're still waiting, ask again soon. 347 if ( 348 isinstance(response, Exception) 349 or response.state is response.State.WAITING 350 ): 351 bui.apptimer( 352 STATUS_CHECK_INTERVAL_SECONDS, 353 bui.WeakCall(self._ask_for_status), 354 ) 355 356 def _proxy_complete_response(self, response: None | Exception) -> None: 357 del response # Not used. 358 # We could do something smart like retry on exceptions here, but 359 # this isn't critical so we'll just let anything slide. 360 361 def _copy_link(self, link: str) -> None: 362 if bui.clipboard_is_supported(): 363 bui.clipboard_set_text(link) 364 bui.screenmessage( 365 bui.Lstr(resource='copyConfirmText'), color=(0, 1, 0) 366 ) 367 368 def _done(self) -> None: 369 # no-op if our underlying widget is dead or on its way out. 370 if not self._root_widget or self._root_widget.transitioning_out: 371 return 372 373 # If we've got an inline browser up, tell it to close. 374 if self._overlay_web_browser_open: 375 bui.overlay_web_browser_close() 376 377 bui.containerwidget(edit=self._root_widget, transition='out_scale')
A window allowing signing in to a v2 account.
V2ProxySignInWindow(origin_widget: _bauiv1.Widget)
21 def __init__(self, origin_widget: bui.Widget): 22 self._width = 600 23 self._height = 550 24 self._proxyid: str | None = None 25 self._proxykey: str | None = None 26 self._overlay_web_browser_open = False 27 28 assert bui.app.classic is not None 29 uiscale = bui.app.ui_v1.uiscale 30 super().__init__( 31 root_widget=bui.containerwidget( 32 size=(self._width, self._height), 33 transition='in_scale', 34 scale_origin_stack_offset=( 35 origin_widget.get_screen_space_center() 36 ), 37 scale=( 38 1.25 39 if uiscale is bui.UIScale.SMALL 40 else 1.05 if uiscale is bui.UIScale.MEDIUM else 0.9 41 ), 42 ) 43 ) 44 45 self._state_text = bui.textwidget( 46 parent=self._root_widget, 47 position=(self._width * 0.5, self._height * 0.6), 48 h_align='center', 49 v_align='center', 50 size=(0, 0), 51 scale=1.4, 52 maxwidth=0.9 * self._width, 53 text=bui.Lstr( 54 value='${A}...', 55 subs=[('${A}', bui.Lstr(resource='loadingText'))], 56 ), 57 color=(1, 1, 1), 58 ) 59 self._sub_state_text = bui.textwidget( 60 parent=self._root_widget, 61 position=(self._width * 0.5, self._height * 0.55), 62 h_align='center', 63 v_align='top', 64 scale=0.85, 65 size=(0, 0), 66 maxwidth=0.9 * self._width, 67 text='', 68 ) 69 self._sub_state_text2 = bui.textwidget( 70 parent=self._root_widget, 71 position=(self._width * 0.1, self._height * 0.3), 72 h_align='left', 73 v_align='top', 74 scale=0.7, 75 size=(0, 0), 76 maxwidth=0.9 * self._width, 77 text='', 78 ) 79 80 self._cancel_button = bui.buttonwidget( 81 parent=self._root_widget, 82 position=(30, self._height - 65), 83 size=(130, 50), 84 scale=0.8, 85 label=bui.Lstr(resource='cancelText'), 86 on_activate_call=self._done, 87 autoselect=True, 88 ) 89 90 bui.containerwidget( 91 edit=self._root_widget, cancel_button=self._cancel_button 92 ) 93 94 self._message_in_flight = False 95 self._complete = False 96 self._connection_wait_timeout_time = time.monotonic() + 10.0 97 98 self._update_timer = bui.AppTimer( 99 0.371, bui.WeakCall(self._update), repeat=True 100 ) 101 bui.pushcall(bui.WeakCall(self._update))