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))