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