bauiv1lib.account.settings

Provides UI for account functionality.

   1# Released under the MIT License. See LICENSE for details.
   2#
   3"""Provides UI for account functionality."""
   4# pylint: disable=too-many-lines
   5
   6from __future__ import annotations
   7
   8import time
   9import logging
  10from typing import override
  11
  12from bacommon.cloud import WebLocation
  13from bacommon.login import LoginType
  14import bacommon.cloud
  15import bauiv1 as bui
  16
  17
  18# These days we're directing people to the web based account settings
  19# for V2 account linking and trying to get them to disconnect remaining
  20# V1 links, but leaving this escape hatch here in case needed.
  21FORCE_ENABLE_V1_LINKING = False
  22
  23
  24class AccountSettingsWindow(bui.MainWindow):
  25    """Window for account related functionality."""
  26
  27    def __init__(
  28        self,
  29        transition: str | None = 'in_right',
  30        modal: bool = False,
  31        origin_widget: bui.Widget | None = None,
  32        close_once_signed_in: bool = False,
  33    ):
  34        # pylint: disable=too-many-statements
  35
  36        plus = bui.app.plus
  37        assert plus is not None
  38
  39        self._sign_in_v2_proxy_button: bui.Widget | None = None
  40        self._sign_in_device_button: bui.Widget | None = None
  41
  42        self._show_legacy_unlink_button = False
  43
  44        self._signing_in_adapter: bui.LoginAdapter | None = None
  45        self._close_once_signed_in = close_once_signed_in
  46        bui.set_analytics_screen('Account Window')
  47
  48        self._explicitly_signed_out_of_gpgs = False
  49
  50        self._r = 'accountSettingsWindow'
  51        self._modal = modal
  52        self._needs_refresh = False
  53        self._v1_signed_in = plus.get_v1_account_state() == 'signed_in'
  54        self._v1_account_state_num = plus.get_v1_account_state_num()
  55        self._check_sign_in_timer = bui.AppTimer(
  56            1.0, bui.WeakCall(self._update), repeat=True
  57        )
  58
  59        self._can_reset_achievements = False
  60
  61        app = bui.app
  62        assert app.classic is not None
  63        uiscale = app.ui_v1.uiscale
  64
  65        self._width = 850 if uiscale is bui.UIScale.SMALL else 660
  66        x_offs = 70 if uiscale is bui.UIScale.SMALL else 0
  67        self._height = (
  68            380
  69            if uiscale is bui.UIScale.SMALL
  70            else 430 if uiscale is bui.UIScale.MEDIUM else 490
  71        )
  72
  73        self._sign_in_button = None
  74        self._sign_in_text = None
  75
  76        self._scroll_width = self._width - (100 + x_offs * 2)
  77        self._scroll_height = self._height - 120
  78        self._sub_width = self._scroll_width - 20
  79
  80        # Determine which sign-in/sign-out buttons we should show.
  81        self._show_sign_in_buttons: list[str] = []
  82
  83        if LoginType.GPGS in plus.accounts.login_adapters:
  84            self._show_sign_in_buttons.append('Google Play')
  85
  86        if LoginType.GAME_CENTER in plus.accounts.login_adapters:
  87            self._show_sign_in_buttons.append('Game Center')
  88
  89        # Always want to show our web-based v2 login option.
  90        self._show_sign_in_buttons.append('V2Proxy')
  91
  92        # Legacy v1 device accounts available only if the user
  93        # has explicitly enabled deprecated login types.
  94        if bui.app.config.resolve('Show Deprecated Login Types'):
  95            self._show_sign_in_buttons.append('Device')
  96
  97        top_extra = 15 if uiscale is bui.UIScale.SMALL else 0
  98        super().__init__(
  99            root_widget=bui.containerwidget(
 100                size=(self._width, self._height + top_extra),
 101                # transition=transition,
 102                toolbar_visibility=(
 103                    'menu_minimal'
 104                    if uiscale is bui.UIScale.SMALL
 105                    else 'menu_full'
 106                ),
 107                scale=(
 108                    2.07
 109                    if uiscale is bui.UIScale.SMALL
 110                    else 1.4 if uiscale is bui.UIScale.MEDIUM else 1.0
 111                ),
 112                stack_offset=(
 113                    (0, 8) if uiscale is bui.UIScale.SMALL else (0, 0)
 114                ),
 115            ),
 116            transition=transition,
 117            origin_widget=origin_widget,
 118        )
 119        if uiscale is bui.UIScale.SMALL:
 120            self._back_button = None
 121            bui.containerwidget(
 122                edit=self._root_widget, on_cancel_call=self.main_window_back
 123            )
 124        else:
 125            self._back_button = btn = bui.buttonwidget(
 126                parent=self._root_widget,
 127                position=(51 + x_offs, self._height - 62),
 128                size=(120, 60),
 129                scale=0.8,
 130                text_scale=1.2,
 131                autoselect=True,
 132                label=bui.Lstr(
 133                    resource='doneText' if self._modal else 'backText'
 134                ),
 135                button_type='regular' if self._modal else 'back',
 136                on_activate_call=self.main_window_back,
 137            )
 138            bui.containerwidget(edit=self._root_widget, cancel_button=btn)
 139            if not self._modal:
 140                bui.buttonwidget(
 141                    edit=btn,
 142                    button_type='backSmall',
 143                    size=(60, 56),
 144                    label=bui.charstr(bui.SpecialChar.BACK),
 145                )
 146
 147        titleyoffs = -12 if uiscale is bui.UIScale.SMALL else 0
 148        titlescale = 0.6 if uiscale is bui.UIScale.SMALL else 1.0
 149        bui.textwidget(
 150            parent=self._root_widget,
 151            position=(self._width * 0.5, self._height - 41 + titleyoffs),
 152            size=(0, 0),
 153            text=bui.Lstr(resource=f'{self._r}.titleText'),
 154            color=app.ui_v1.title_color,
 155            scale=titlescale,
 156            maxwidth=self._width - 340,
 157            h_align='center',
 158            v_align='center',
 159        )
 160
 161        self._scrollwidget = bui.scrollwidget(
 162            parent=self._root_widget,
 163            highlight=False,
 164            position=(
 165                (self._width - self._scroll_width) * 0.5,
 166                self._height - 65 - self._scroll_height,
 167            ),
 168            size=(self._scroll_width, self._scroll_height),
 169            claims_left_right=True,
 170            claims_tab=True,
 171            selection_loops_to_parent=True,
 172        )
 173        self._subcontainer: bui.Widget | None = None
 174        self._refresh()
 175        self._restore_state()
 176
 177    @override
 178    def get_main_window_state(self) -> bui.MainWindowState:
 179        # Support recreating our window for back/refresh purposes.
 180        cls = type(self)
 181        return bui.BasicMainWindowState(
 182            create_call=lambda transition, origin_widget: cls(
 183                transition=transition, origin_widget=origin_widget
 184            )
 185        )
 186
 187    @override
 188    def on_main_window_close(self) -> None:
 189        self._save_state()
 190
 191    def _update(self) -> None:
 192        plus = bui.app.plus
 193        assert plus is not None
 194
 195        # If they want us to close once we're signed in, do so.
 196        if self._close_once_signed_in and self._v1_signed_in:
 197            self.main_window_back()
 198            return
 199
 200        # Hmm should update this to use get_account_state_num.
 201        # Theoretically if we switch from one signed-in account to another
 202        # in the background this would break.
 203        v1_account_state_num = plus.get_v1_account_state_num()
 204        v1_account_state = plus.get_v1_account_state()
 205        show_legacy_unlink_button = self._should_show_legacy_unlink_button()
 206
 207        if (
 208            v1_account_state_num != self._v1_account_state_num
 209            or show_legacy_unlink_button != self._show_legacy_unlink_button
 210            or self._needs_refresh
 211        ):
 212            self._v1_account_state_num = v1_account_state_num
 213            self._v1_signed_in = v1_account_state == 'signed_in'
 214            self._show_legacy_unlink_button = show_legacy_unlink_button
 215            self._refresh()
 216
 217        # Go ahead and refresh some individual things
 218        # that may change under us.
 219        self._update_linked_accounts_text()
 220        self._update_unlink_accounts_button()
 221        self._refresh_campaign_progress_text()
 222        self._refresh_achievements()
 223        self._refresh_tickets_text()
 224        self._refresh_account_name_text()
 225
 226    def _refresh(self) -> None:
 227        # pylint: disable=too-many-statements
 228        # pylint: disable=too-many-branches
 229        # pylint: disable=too-many-locals
 230        # pylint: disable=cyclic-import
 231
 232        plus = bui.app.plus
 233        assert plus is not None
 234
 235        via_lines: list[str] = []
 236
 237        primary_v2_account = plus.accounts.primary
 238
 239        v1_state = plus.get_v1_account_state()
 240        v1_account_type = (
 241            plus.get_v1_account_type() if v1_state == 'signed_in' else 'unknown'
 242        )
 243
 244        # We expose GPGS-specific functionality only if it is 'active'
 245        # (meaning the current GPGS player matches one of our account's
 246        # logins).
 247        adapter = plus.accounts.login_adapters.get(LoginType.GPGS)
 248        gpgs_active = adapter is not None and adapter.is_back_end_active()
 249
 250        # Ditto for Game Center.
 251        adapter = plus.accounts.login_adapters.get(LoginType.GAME_CENTER)
 252        game_center_active = (
 253            adapter is not None and adapter.is_back_end_active()
 254        )
 255
 256        show_signed_in_as = self._v1_signed_in
 257        signed_in_as_space = 95.0
 258
 259        # To reduce confusion about the whole V2 account situation for
 260        # people used to seeing their Google Play Games or Game Center
 261        # account name and icon and whatnot, let's show those underneath
 262        # the V2 tag to help communicate that they are in fact logged in
 263        # through that account.
 264        via_space = 25.0
 265        if show_signed_in_as and bui.app.plus is not None:
 266            accounts = bui.app.plus.accounts
 267            if accounts.primary is not None:
 268                # For these login types, we show 'via' IF there is a
 269                # login of that type attached to our account AND it is
 270                # currently active (We don't want to show 'via Game
 271                # Center' if we're signed out of Game Center or
 272                # currently running on Steam, even if there is a Game
 273                # Center login attached to our account).
 274                for ltype, lchar in [
 275                    (LoginType.GPGS, bui.SpecialChar.GOOGLE_PLAY_GAMES_LOGO),
 276                    (LoginType.GAME_CENTER, bui.SpecialChar.GAME_CENTER_LOGO),
 277                ]:
 278                    linfo = accounts.primary.logins.get(ltype)
 279                    ladapter = accounts.login_adapters.get(ltype)
 280                    if (
 281                        linfo is not None
 282                        and ladapter is not None
 283                        and ladapter.is_back_end_active()
 284                    ):
 285                        via_lines.append(f'{bui.charstr(lchar)}{linfo.name}')
 286
 287                # TEMP TESTING
 288                if bool(False):
 289                    icontxt = bui.charstr(bui.SpecialChar.GAME_CENTER_LOGO)
 290                    via_lines.append(f'{icontxt}FloofDibble')
 291                    icontxt = bui.charstr(
 292                        bui.SpecialChar.GOOGLE_PLAY_GAMES_LOGO
 293                    )
 294                    via_lines.append(f'{icontxt}StinkBobble')
 295
 296        show_sign_in_benefits = not self._v1_signed_in
 297        sign_in_benefits_space = 80.0
 298
 299        show_signing_in_text = (
 300            v1_state == 'signing_in' or self._signing_in_adapter is not None
 301        )
 302        signing_in_text_space = 80.0
 303
 304        show_google_play_sign_in_button = (
 305            v1_state == 'signed_out'
 306            and self._signing_in_adapter is None
 307            and 'Google Play' in self._show_sign_in_buttons
 308        )
 309        show_game_center_sign_in_button = (
 310            v1_state == 'signed_out'
 311            and self._signing_in_adapter is None
 312            and 'Game Center' in self._show_sign_in_buttons
 313        )
 314        show_v2_proxy_sign_in_button = (
 315            v1_state == 'signed_out'
 316            and self._signing_in_adapter is None
 317            and 'V2Proxy' in self._show_sign_in_buttons
 318        )
 319        show_device_sign_in_button = (
 320            v1_state == 'signed_out'
 321            and self._signing_in_adapter is None
 322            and 'Device' in self._show_sign_in_buttons
 323        )
 324        sign_in_button_space = 70.0
 325        deprecated_space = 60
 326
 327        # Game Center currently has a single UI for everything.
 328        show_game_service_button = game_center_active
 329        game_service_button_space = 60.0
 330
 331        # Phasing this out (for V2 accounts at least).
 332        show_linked_accounts_text = (
 333            self._v1_signed_in and v1_account_type != 'V2'
 334        )
 335        linked_accounts_text_space = 60.0
 336
 337        # Always show achievements except in the game-center case where
 338        # its unified UI covers them.
 339        # show_achievements_button =
 340        # self._v1_signed_in and not game_center_active
 341
 342        # Update: No longer showing this since its visible on main
 343        # toolbar.
 344        show_achievements_button = False
 345        achievements_button_space = 60.0
 346
 347        # show_achievements_text = (
 348        #     self._v1_signed_in and not show_achievements_button
 349        # )
 350
 351        # Update: No longer showing this since its visible on main
 352        # toolbar.
 353        show_achievements_text = False
 354        achievements_text_space = 27.0
 355
 356        show_leaderboards_button = self._v1_signed_in and gpgs_active
 357        leaderboards_button_space = 60.0
 358
 359        # Update: No longer showing this; trying to get progress type
 360        # stuff out of the account panel.
 361        # show_campaign_progress = self._v1_signed_in
 362        show_campaign_progress = False
 363        campaign_progress_space = 27.0
 364
 365        # show_tickets = self._v1_signed_in
 366        show_tickets = False
 367        tickets_space = 27.0
 368
 369        show_manage_account_button = primary_v2_account is not None
 370        manage_account_button_space = 70.0
 371
 372        show_delete_account_button = primary_v2_account is not None
 373        delete_account_button_space = 70.0
 374
 375        show_link_accounts_button = self._v1_signed_in and (
 376            primary_v2_account is None or FORCE_ENABLE_V1_LINKING
 377        )
 378        link_accounts_button_space = 70.0
 379
 380        show_unlink_accounts_button = show_link_accounts_button
 381        unlink_accounts_button_space = 90.0
 382
 383        # Phasing this out.
 384        show_v2_link_info = False
 385        v2_link_info_space = 70.0
 386
 387        legacy_unlink_button_space = 120.0
 388
 389        show_sign_out_button = primary_v2_account is not None or (
 390            self._v1_signed_in and v1_account_type == 'Local'
 391        )
 392        sign_out_button_space = 70.0
 393
 394        # We can show cancel if we're either waiting on an adapter to
 395        # provide us with v2 credentials or waiting for those
 396        # credentials to be verified.
 397        show_cancel_sign_in_button = self._signing_in_adapter is not None or (
 398            plus.accounts.have_primary_credentials()
 399            and primary_v2_account is None
 400        )
 401        cancel_sign_in_button_space = 70.0
 402
 403        if self._subcontainer is not None:
 404            self._subcontainer.delete()
 405        self._sub_height = 60.0
 406        if show_signed_in_as:
 407            self._sub_height += signed_in_as_space
 408        self._sub_height += via_space * len(via_lines)
 409        if show_signing_in_text:
 410            self._sub_height += signing_in_text_space
 411        if show_google_play_sign_in_button:
 412            self._sub_height += sign_in_button_space
 413        if show_game_center_sign_in_button:
 414            self._sub_height += sign_in_button_space
 415        if show_v2_proxy_sign_in_button:
 416            self._sub_height += sign_in_button_space
 417        if show_device_sign_in_button:
 418            self._sub_height += sign_in_button_space + deprecated_space
 419        if show_game_service_button:
 420            self._sub_height += game_service_button_space
 421        if show_linked_accounts_text:
 422            self._sub_height += linked_accounts_text_space
 423        if show_achievements_text:
 424            self._sub_height += achievements_text_space
 425        if show_achievements_button:
 426            self._sub_height += achievements_button_space
 427        if show_leaderboards_button:
 428            self._sub_height += leaderboards_button_space
 429        if show_campaign_progress:
 430            self._sub_height += campaign_progress_space
 431        if show_tickets:
 432            self._sub_height += tickets_space
 433        if show_sign_in_benefits:
 434            self._sub_height += sign_in_benefits_space
 435        if show_manage_account_button:
 436            self._sub_height += manage_account_button_space
 437        if show_link_accounts_button:
 438            self._sub_height += link_accounts_button_space
 439        if show_unlink_accounts_button:
 440            self._sub_height += unlink_accounts_button_space
 441        if show_v2_link_info:
 442            self._sub_height += v2_link_info_space
 443        if self._show_legacy_unlink_button:
 444            self._sub_height += legacy_unlink_button_space
 445        if show_sign_out_button:
 446            self._sub_height += sign_out_button_space
 447        if show_delete_account_button:
 448            self._sub_height += delete_account_button_space
 449        if show_cancel_sign_in_button:
 450            self._sub_height += cancel_sign_in_button_space
 451        self._subcontainer = bui.containerwidget(
 452            parent=self._scrollwidget,
 453            size=(self._sub_width, self._sub_height),
 454            background=False,
 455            claims_left_right=True,
 456            claims_tab=True,
 457            selection_loops_to_parent=True,
 458        )
 459
 460        first_selectable = None
 461        v = self._sub_height - 10.0
 462
 463        assert bui.app.classic is not None
 464        self._account_name_text: bui.Widget | None
 465        if show_signed_in_as:
 466            v -= signed_in_as_space * 0.2
 467            txt = bui.Lstr(
 468                resource='accountSettingsWindow.youAreSignedInAsText',
 469                fallback_resource='accountSettingsWindow.youAreLoggedInAsText',
 470            )
 471            bui.textwidget(
 472                parent=self._subcontainer,
 473                position=(self._sub_width * 0.5, v),
 474                size=(0, 0),
 475                text=txt,
 476                scale=0.9,
 477                color=bui.app.ui_v1.title_color,
 478                maxwidth=self._sub_width * 0.9,
 479                h_align='center',
 480                v_align='center',
 481            )
 482            v -= signed_in_as_space * 0.5
 483            self._account_name_text = bui.textwidget(
 484                parent=self._subcontainer,
 485                position=(self._sub_width * 0.5, v),
 486                size=(0, 0),
 487                scale=1.5,
 488                maxwidth=self._sub_width * 0.9,
 489                res_scale=1.5,
 490                color=(1, 1, 1, 1),
 491                h_align='center',
 492                v_align='center',
 493            )
 494
 495            self._refresh_account_name_text()
 496
 497            v -= signed_in_as_space * 0.4
 498
 499            for via in via_lines:
 500                v -= via_space * 0.1
 501                sscale = 0.7
 502                swidth = (
 503                    bui.get_string_width(via, suppress_warning=True) * sscale
 504                )
 505                bui.textwidget(
 506                    parent=self._subcontainer,
 507                    position=(self._sub_width * 0.5, v),
 508                    size=(0, 0),
 509                    text=via,
 510                    scale=sscale,
 511                    color=(0.6, 0.6, 0.6),
 512                    flatness=1.0,
 513                    shadow=0.0,
 514                    h_align='center',
 515                    v_align='center',
 516                )
 517                bui.textwidget(
 518                    parent=self._subcontainer,
 519                    position=(self._sub_width * 0.5 - swidth * 0.5 - 5, v),
 520                    size=(0, 0),
 521                    text=bui.Lstr(
 522                        value='(${VIA}',
 523                        subs=[('${VIA}', bui.Lstr(resource='viaText'))],
 524                    ),
 525                    scale=0.5,
 526                    color=(0.4, 0.6, 0.4, 0.5),
 527                    flatness=1.0,
 528                    shadow=0.0,
 529                    h_align='right',
 530                    v_align='center',
 531                )
 532                bui.textwidget(
 533                    parent=self._subcontainer,
 534                    position=(self._sub_width * 0.5 + swidth * 0.5 + 10, v),
 535                    size=(0, 0),
 536                    text=')',
 537                    scale=0.5,
 538                    color=(0.4, 0.6, 0.4, 0.5),
 539                    flatness=1.0,
 540                    shadow=0.0,
 541                    h_align='right',
 542                    v_align='center',
 543                )
 544
 545                v -= via_space * 0.9
 546
 547        else:
 548            self._account_name_text = None
 549
 550        if self._back_button is None:
 551            bbtn = bui.get_special_widget('back_button')
 552        else:
 553            bbtn = self._back_button
 554
 555        if show_sign_in_benefits:
 556            v -= sign_in_benefits_space
 557            bui.textwidget(
 558                parent=self._subcontainer,
 559                position=(
 560                    self._sub_width * 0.5,
 561                    v + sign_in_benefits_space * 0.4,
 562                ),
 563                size=(0, 0),
 564                text=bui.Lstr(resource=f'{self._r}.signInInfoText'),
 565                max_height=sign_in_benefits_space * 0.9,
 566                scale=0.9,
 567                color=(0.75, 0.7, 0.8),
 568                maxwidth=self._sub_width * 0.8,
 569                h_align='center',
 570                v_align='center',
 571            )
 572
 573        if show_signing_in_text:
 574            v -= signing_in_text_space
 575
 576            bui.textwidget(
 577                parent=self._subcontainer,
 578                position=(
 579                    self._sub_width * 0.5,
 580                    v + signing_in_text_space * 0.5,
 581                ),
 582                size=(0, 0),
 583                text=bui.Lstr(resource='accountSettingsWindow.signingInText'),
 584                scale=0.9,
 585                color=(0, 1, 0),
 586                maxwidth=self._sub_width * 0.8,
 587                h_align='center',
 588                v_align='center',
 589            )
 590
 591        if show_google_play_sign_in_button:
 592            button_width = 350
 593            v -= sign_in_button_space
 594            self._sign_in_google_play_button = btn = bui.buttonwidget(
 595                parent=self._subcontainer,
 596                position=((self._sub_width - button_width) * 0.5, v - 20),
 597                autoselect=True,
 598                size=(button_width, 60),
 599                label=bui.Lstr(
 600                    value='${A} ${B}',
 601                    subs=[
 602                        (
 603                            '${A}',
 604                            bui.charstr(bui.SpecialChar.GOOGLE_PLAY_GAMES_LOGO),
 605                        ),
 606                        (
 607                            '${B}',
 608                            bui.Lstr(
 609                                resource=f'{self._r}.signInWithText',
 610                                subs=[
 611                                    (
 612                                        '${SERVICE}',
 613                                        bui.Lstr(resource='googlePlayText'),
 614                                    )
 615                                ],
 616                            ),
 617                        ),
 618                    ],
 619                ),
 620                on_activate_call=lambda: self._sign_in_press(LoginType.GPGS),
 621            )
 622            if first_selectable is None:
 623                first_selectable = btn
 624            bui.widget(
 625                edit=btn, right_widget=bui.get_special_widget('squad_button')
 626            )
 627            bui.widget(edit=btn, left_widget=bbtn)
 628            bui.widget(edit=btn, show_buffer_bottom=40, show_buffer_top=100)
 629            self._sign_in_text = None
 630
 631        if show_game_center_sign_in_button:
 632            button_width = 350
 633            v -= sign_in_button_space
 634            self._sign_in_google_play_button = btn = bui.buttonwidget(
 635                parent=self._subcontainer,
 636                position=((self._sub_width - button_width) * 0.5, v - 20),
 637                autoselect=True,
 638                size=(button_width, 60),
 639                # Note: Apparently Game Center is just called 'Game Center'
 640                # in all languages. Can revisit if not true.
 641                # https://developer.apple.com/forums/thread/725779
 642                label=bui.Lstr(
 643                    value='${A} ${B}',
 644                    subs=[
 645                        (
 646                            '${A}',
 647                            bui.charstr(bui.SpecialChar.GAME_CENTER_LOGO),
 648                        ),
 649                        (
 650                            '${B}',
 651                            bui.Lstr(
 652                                resource=f'{self._r}.signInWithText',
 653                                subs=[('${SERVICE}', 'Game Center')],
 654                            ),
 655                        ),
 656                    ],
 657                ),
 658                on_activate_call=lambda: self._sign_in_press(
 659                    LoginType.GAME_CENTER
 660                ),
 661            )
 662            if first_selectable is None:
 663                first_selectable = btn
 664            bui.widget(
 665                edit=btn, right_widget=bui.get_special_widget('squad_button')
 666            )
 667            bui.widget(edit=btn, left_widget=bbtn)
 668            bui.widget(edit=btn, show_buffer_bottom=40, show_buffer_top=100)
 669            self._sign_in_text = None
 670
 671        if show_v2_proxy_sign_in_button:
 672            button_width = 350
 673            v -= sign_in_button_space
 674            self._sign_in_v2_proxy_button = btn = bui.buttonwidget(
 675                parent=self._subcontainer,
 676                position=((self._sub_width - button_width) * 0.5, v - 20),
 677                autoselect=True,
 678                size=(button_width, 60),
 679                label='',
 680                on_activate_call=self._v2_proxy_sign_in_press,
 681            )
 682
 683            v2labeltext: bui.Lstr | str = (
 684                bui.Lstr(resource=f'{self._r}.signInWithAnEmailAddressText')
 685                if show_game_center_sign_in_button
 686                or show_google_play_sign_in_button
 687                or show_device_sign_in_button
 688                else bui.Lstr(resource=f'{self._r}.signInText')
 689            )
 690            v2infotext: bui.Lstr | str | None = None
 691
 692            bui.textwidget(
 693                parent=self._subcontainer,
 694                draw_controller=btn,
 695                h_align='center',
 696                v_align='center',
 697                size=(0, 0),
 698                position=(
 699                    self._sub_width * 0.5,
 700                    v + (17 if v2infotext is not None else 10),
 701                ),
 702                text=bui.Lstr(
 703                    value='${A} ${B}',
 704                    subs=[
 705                        ('${A}', bui.charstr(bui.SpecialChar.V2_LOGO)),
 706                        (
 707                            '${B}',
 708                            v2labeltext,
 709                        ),
 710                    ],
 711                ),
 712                maxwidth=button_width * 0.8,
 713                color=(0.75, 1.0, 0.7),
 714            )
 715            if v2infotext is not None:
 716                bui.textwidget(
 717                    parent=self._subcontainer,
 718                    draw_controller=btn,
 719                    h_align='center',
 720                    v_align='center',
 721                    size=(0, 0),
 722                    position=(self._sub_width * 0.5, v - 4),
 723                    text=v2infotext,
 724                    flatness=1.0,
 725                    scale=0.57,
 726                    maxwidth=button_width * 0.9,
 727                    color=(0.55, 0.8, 0.5),
 728                )
 729            if first_selectable is None:
 730                first_selectable = btn
 731            bui.widget(
 732                edit=btn, right_widget=bui.get_special_widget('squad_button')
 733            )
 734            bui.widget(edit=btn, left_widget=bbtn)
 735            bui.widget(edit=btn, show_buffer_bottom=40, show_buffer_top=100)
 736            self._sign_in_text = None
 737
 738        if show_device_sign_in_button:
 739            button_width = 350
 740            v -= sign_in_button_space + deprecated_space
 741            self._sign_in_device_button = btn = bui.buttonwidget(
 742                parent=self._subcontainer,
 743                position=((self._sub_width - button_width) * 0.5, v - 20),
 744                autoselect=True,
 745                size=(button_width, 60),
 746                label='',
 747                on_activate_call=lambda: self._sign_in_press('Local'),
 748            )
 749            bui.textwidget(
 750                parent=self._subcontainer,
 751                h_align='center',
 752                v_align='center',
 753                size=(0, 0),
 754                position=(self._sub_width * 0.5, v + 60),
 755                text=bui.Lstr(resource='deprecatedText'),
 756                scale=0.8,
 757                maxwidth=300,
 758                color=(0.6, 0.55, 0.45),
 759            )
 760
 761            bui.textwidget(
 762                parent=self._subcontainer,
 763                draw_controller=btn,
 764                h_align='center',
 765                v_align='center',
 766                size=(0, 0),
 767                position=(self._sub_width * 0.5, v + 17),
 768                text=bui.Lstr(
 769                    value='${A} ${B}',
 770                    subs=[
 771                        ('${A}', bui.charstr(bui.SpecialChar.LOCAL_ACCOUNT)),
 772                        (
 773                            '${B}',
 774                            bui.Lstr(
 775                                resource=f'{self._r}.signInWithDeviceText'
 776                            ),
 777                        ),
 778                    ],
 779                ),
 780                maxwidth=button_width * 0.8,
 781                color=(0.75, 1.0, 0.7),
 782            )
 783            bui.textwidget(
 784                parent=self._subcontainer,
 785                draw_controller=btn,
 786                h_align='center',
 787                v_align='center',
 788                size=(0, 0),
 789                position=(self._sub_width * 0.5, v - 4),
 790                text=bui.Lstr(resource=f'{self._r}.signInWithDeviceInfoText'),
 791                flatness=1.0,
 792                scale=0.57,
 793                maxwidth=button_width * 0.9,
 794                color=(0.55, 0.8, 0.5),
 795            )
 796            if first_selectable is None:
 797                first_selectable = btn
 798            bui.widget(
 799                edit=btn, right_widget=bui.get_special_widget('squad_button')
 800            )
 801            bui.widget(edit=btn, left_widget=bbtn)
 802            bui.widget(edit=btn, show_buffer_bottom=40, show_buffer_top=100)
 803            self._sign_in_text = None
 804
 805        if show_manage_account_button:
 806            button_width = 300
 807            v -= manage_account_button_space
 808            self._manage_button = btn = bui.buttonwidget(
 809                parent=self._subcontainer,
 810                position=((self._sub_width - button_width) * 0.5, v),
 811                autoselect=True,
 812                size=(button_width, 60),
 813                label=bui.Lstr(resource=f'{self._r}.manageAccountText'),
 814                color=(0.55, 0.5, 0.6),
 815                icon=bui.gettexture('settingsIcon'),
 816                textcolor=(0.75, 0.7, 0.8),
 817                on_activate_call=bui.WeakCall(self._on_manage_account_press),
 818            )
 819            if first_selectable is None:
 820                first_selectable = btn
 821            bui.widget(
 822                edit=btn, right_widget=bui.get_special_widget('squad_button')
 823            )
 824            bui.widget(edit=btn, left_widget=bbtn)
 825
 826        # the button to go to OS-Specific leaderboards/high-score-lists/etc.
 827        if show_game_service_button:
 828            button_width = 300
 829            v -= game_service_button_space * 0.6
 830            if game_center_active:
 831                # Note: Apparently Game Center is just called 'Game Center'
 832                # in all languages. Can revisit if not true.
 833                # https://developer.apple.com/forums/thread/725779
 834                game_service_button_label = bui.Lstr(
 835                    value=bui.charstr(bui.SpecialChar.GAME_CENTER_LOGO)
 836                    + 'Game Center'
 837                )
 838            else:
 839                raise ValueError(
 840                    "unknown account type: '" + str(v1_account_type) + "'"
 841                )
 842            self._game_service_button = btn = bui.buttonwidget(
 843                parent=self._subcontainer,
 844                position=((self._sub_width - button_width) * 0.5, v),
 845                color=(0.55, 0.5, 0.6),
 846                textcolor=(0.75, 0.7, 0.8),
 847                autoselect=True,
 848                on_activate_call=self._on_game_service_button_press,
 849                size=(button_width, 50),
 850                label=game_service_button_label,
 851            )
 852            if first_selectable is None:
 853                first_selectable = btn
 854            bui.widget(
 855                edit=btn, right_widget=bui.get_special_widget('squad_button')
 856            )
 857            bui.widget(edit=btn, left_widget=bbtn)
 858            v -= game_service_button_space * 0.4
 859        else:
 860            self.game_service_button = None
 861
 862        self._achievements_text: bui.Widget | None
 863        if show_achievements_text:
 864            v -= achievements_text_space * 0.5
 865            self._achievements_text = bui.textwidget(
 866                parent=self._subcontainer,
 867                position=(self._sub_width * 0.5, v),
 868                size=(0, 0),
 869                scale=0.9,
 870                color=(0.75, 0.7, 0.8),
 871                maxwidth=self._sub_width * 0.8,
 872                h_align='center',
 873                v_align='center',
 874            )
 875            v -= achievements_text_space * 0.5
 876        else:
 877            self._achievements_text = None
 878
 879        self._achievements_button: bui.Widget | None
 880        if show_achievements_button:
 881            button_width = 300
 882            v -= achievements_button_space * 0.85
 883            self._achievements_button = btn = bui.buttonwidget(
 884                parent=self._subcontainer,
 885                position=((self._sub_width - button_width) * 0.5, v),
 886                color=(0.55, 0.5, 0.6),
 887                textcolor=(0.75, 0.7, 0.8),
 888                autoselect=True,
 889                icon=bui.gettexture(
 890                    'googlePlayAchievementsIcon'
 891                    if gpgs_active
 892                    else 'achievementsIcon'
 893                ),
 894                icon_color=(
 895                    (0.8, 0.95, 0.7) if gpgs_active else (0.85, 0.8, 0.9)
 896                ),
 897                on_activate_call=(
 898                    self._on_custom_achievements_press
 899                    if gpgs_active
 900                    else self._on_achievements_press
 901                ),
 902                size=(button_width, 50),
 903                label='',
 904            )
 905            if first_selectable is None:
 906                first_selectable = btn
 907            bui.widget(
 908                edit=btn, right_widget=bui.get_special_widget('squad_button')
 909            )
 910            bui.widget(edit=btn, left_widget=bbtn)
 911            v -= achievements_button_space * 0.15
 912        else:
 913            self._achievements_button = None
 914
 915        if show_achievements_text or show_achievements_button:
 916            self._refresh_achievements()
 917
 918        self._leaderboards_button: bui.Widget | None
 919        if show_leaderboards_button:
 920            button_width = 300
 921            v -= leaderboards_button_space * 0.85
 922            self._leaderboards_button = btn = bui.buttonwidget(
 923                parent=self._subcontainer,
 924                position=((self._sub_width - button_width) * 0.5, v),
 925                color=(0.55, 0.5, 0.6),
 926                textcolor=(0.75, 0.7, 0.8),
 927                autoselect=True,
 928                icon=bui.gettexture('googlePlayLeaderboardsIcon'),
 929                icon_color=(0.8, 0.95, 0.7),
 930                on_activate_call=self._on_leaderboards_press,
 931                size=(button_width, 50),
 932                label=bui.Lstr(resource='leaderboardsText'),
 933            )
 934            if first_selectable is None:
 935                first_selectable = btn
 936            bui.widget(
 937                edit=btn, right_widget=bui.get_special_widget('squad_button')
 938            )
 939            bui.widget(edit=btn, left_widget=bbtn)
 940            v -= leaderboards_button_space * 0.15
 941        else:
 942            self._leaderboards_button = None
 943
 944        self._campaign_progress_text: bui.Widget | None
 945        if show_campaign_progress:
 946            v -= campaign_progress_space * 0.5
 947            self._campaign_progress_text = bui.textwidget(
 948                parent=self._subcontainer,
 949                position=(self._sub_width * 0.5, v),
 950                size=(0, 0),
 951                scale=0.9,
 952                color=(0.75, 0.7, 0.8),
 953                maxwidth=self._sub_width * 0.8,
 954                h_align='center',
 955                v_align='center',
 956            )
 957            v -= campaign_progress_space * 0.5
 958            self._refresh_campaign_progress_text()
 959        else:
 960            self._campaign_progress_text = None
 961
 962        self._tickets_text: bui.Widget | None
 963        if show_tickets:
 964            v -= tickets_space * 0.5
 965            self._tickets_text = bui.textwidget(
 966                parent=self._subcontainer,
 967                position=(self._sub_width * 0.5, v),
 968                size=(0, 0),
 969                scale=0.9,
 970                color=(0.75, 0.7, 0.8),
 971                maxwidth=self._sub_width * 0.8,
 972                flatness=1.0,
 973                h_align='center',
 974                v_align='center',
 975            )
 976            v -= tickets_space * 0.5
 977            self._refresh_tickets_text()
 978
 979        else:
 980            self._tickets_text = None
 981
 982        # bit of spacing before the reset/sign-out section
 983        # v -= 5
 984
 985        button_width = 250
 986
 987        self._linked_accounts_text: bui.Widget | None
 988        if show_linked_accounts_text:
 989            v -= linked_accounts_text_space * 0.8
 990            self._linked_accounts_text = bui.textwidget(
 991                parent=self._subcontainer,
 992                position=(self._sub_width * 0.5, v),
 993                size=(0, 0),
 994                scale=0.9,
 995                color=(0.75, 0.7, 0.8),
 996                maxwidth=self._sub_width * 0.95,
 997                text=bui.Lstr(resource=f'{self._r}.linkedAccountsText'),
 998                h_align='center',
 999                v_align='center',
1000            )
1001            v -= linked_accounts_text_space * 0.2
1002            self._update_linked_accounts_text()
1003        else:
1004            self._linked_accounts_text = None
1005
1006        # Show link/unlink buttons only for V1 accounts.
1007
1008        if show_link_accounts_button:
1009            v -= link_accounts_button_space
1010            self._link_accounts_button = btn = bui.buttonwidget(
1011                parent=self._subcontainer,
1012                position=((self._sub_width - button_width) * 0.5, v),
1013                autoselect=True,
1014                size=(button_width, 60),
1015                label='',
1016                color=(0.55, 0.5, 0.6),
1017                on_activate_call=self._link_accounts_press,
1018            )
1019            bui.textwidget(
1020                parent=self._subcontainer,
1021                draw_controller=btn,
1022                h_align='center',
1023                v_align='center',
1024                size=(0, 0),
1025                position=(self._sub_width * 0.5, v + 17 + 20),
1026                text=bui.Lstr(resource=f'{self._r}.linkAccountsText'),
1027                maxwidth=button_width * 0.8,
1028                color=(0.75, 0.7, 0.8),
1029            )
1030            bui.textwidget(
1031                parent=self._subcontainer,
1032                draw_controller=btn,
1033                h_align='center',
1034                v_align='center',
1035                size=(0, 0),
1036                position=(self._sub_width * 0.5, v - 4 + 20),
1037                text=bui.Lstr(resource=f'{self._r}.linkAccountsInfoText'),
1038                flatness=1.0,
1039                scale=0.5,
1040                maxwidth=button_width * 0.8,
1041                color=(0.75, 0.7, 0.8),
1042            )
1043            if first_selectable is None:
1044                first_selectable = btn
1045            bui.widget(
1046                edit=btn, right_widget=bui.get_special_widget('squad_button')
1047            )
1048            bui.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=50)
1049
1050        self._unlink_accounts_button: bui.Widget | None
1051        if show_unlink_accounts_button:
1052            v -= unlink_accounts_button_space
1053            self._unlink_accounts_button = btn = bui.buttonwidget(
1054                parent=self._subcontainer,
1055                position=((self._sub_width - button_width) * 0.5, v + 25),
1056                autoselect=True,
1057                size=(button_width, 60),
1058                label='',
1059                color=(0.55, 0.5, 0.6),
1060                on_activate_call=self._unlink_accounts_press,
1061            )
1062            self._unlink_accounts_button_label = bui.textwidget(
1063                parent=self._subcontainer,
1064                draw_controller=btn,
1065                h_align='center',
1066                v_align='center',
1067                size=(0, 0),
1068                position=(self._sub_width * 0.5, v + 55),
1069                text=bui.Lstr(resource=f'{self._r}.unlinkAccountsText'),
1070                maxwidth=button_width * 0.8,
1071                color=(0.75, 0.7, 0.8),
1072            )
1073            if first_selectable is None:
1074                first_selectable = btn
1075            bui.widget(
1076                edit=btn, right_widget=bui.get_special_widget('squad_button')
1077            )
1078            bui.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=50)
1079            self._update_unlink_accounts_button()
1080        else:
1081            self._unlink_accounts_button = None
1082
1083        if show_v2_link_info:
1084            v -= v2_link_info_space
1085            bui.textwidget(
1086                parent=self._subcontainer,
1087                h_align='center',
1088                v_align='center',
1089                size=(0, 0),
1090                position=(self._sub_width * 0.5, v + v2_link_info_space - 20),
1091                text=bui.Lstr(resource='v2AccountLinkingInfoText'),
1092                flatness=1.0,
1093                scale=0.8,
1094                maxwidth=450,
1095                color=(0.5, 0.45, 0.55),
1096            )
1097
1098        if self._show_legacy_unlink_button:
1099            v -= legacy_unlink_button_space
1100            button_width_w = button_width * 1.5
1101            bui.textwidget(
1102                parent=self._subcontainer,
1103                position=(self._sub_width * 0.5 - 150.0, v + 75),
1104                size=(300.0, 60),
1105                text=bui.Lstr(resource='whatIsThisText'),
1106                scale=0.8,
1107                color=(0.3, 0.7, 0.05),
1108                maxwidth=200.0,
1109                h_align='center',
1110                v_align='center',
1111                autoselect=True,
1112                selectable=True,
1113                on_activate_call=show_what_is_legacy_unlinking_page,
1114                click_activate=True,
1115            )
1116            btn = bui.buttonwidget(
1117                parent=self._subcontainer,
1118                position=((self._sub_width - button_width_w) * 0.5, v + 25),
1119                autoselect=True,
1120                size=(button_width_w, 60),
1121                label=bui.Lstr(
1122                    resource=f'{self._r}.unlinkLegacyV1AccountsText'
1123                ),
1124                textcolor=(0.8, 0.4, 0),
1125                color=(0.55, 0.5, 0.6),
1126                on_activate_call=self._unlink_accounts_press,
1127            )
1128
1129        if show_sign_out_button:
1130            v -= sign_out_button_space
1131            self._sign_out_button = btn = bui.buttonwidget(
1132                parent=self._subcontainer,
1133                position=((self._sub_width - button_width) * 0.5, v),
1134                size=(button_width, 60),
1135                label=bui.Lstr(resource=f'{self._r}.signOutText'),
1136                color=(0.55, 0.5, 0.6),
1137                textcolor=(0.75, 0.7, 0.8),
1138                autoselect=True,
1139                on_activate_call=self._sign_out_press,
1140            )
1141            if first_selectable is None:
1142                first_selectable = btn
1143            bui.widget(
1144                edit=btn, right_widget=bui.get_special_widget('squad_button')
1145            )
1146            bui.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=15)
1147
1148        if show_cancel_sign_in_button:
1149            v -= cancel_sign_in_button_space
1150            self._cancel_sign_in_button = btn = bui.buttonwidget(
1151                parent=self._subcontainer,
1152                position=((self._sub_width - button_width) * 0.5, v),
1153                size=(button_width, 60),
1154                label=bui.Lstr(resource='cancelText'),
1155                color=(0.55, 0.5, 0.6),
1156                textcolor=(0.75, 0.7, 0.8),
1157                autoselect=True,
1158                on_activate_call=self._cancel_sign_in_press,
1159            )
1160            if first_selectable is None:
1161                first_selectable = btn
1162            bui.widget(
1163                edit=btn, right_widget=bui.get_special_widget('squad_button')
1164            )
1165            bui.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=15)
1166
1167        if show_delete_account_button:
1168            v -= delete_account_button_space
1169            self._delete_account_button = btn = bui.buttonwidget(
1170                parent=self._subcontainer,
1171                position=((self._sub_width - button_width) * 0.5, v),
1172                size=(button_width, 60),
1173                label=bui.Lstr(resource=f'{self._r}.deleteAccountText'),
1174                color=(0.85, 0.5, 0.6),
1175                textcolor=(0.9, 0.7, 0.8),
1176                autoselect=True,
1177                on_activate_call=self._on_delete_account_press,
1178            )
1179            if first_selectable is None:
1180                first_selectable = btn
1181            bui.widget(
1182                edit=btn, right_widget=bui.get_special_widget('squad_button')
1183            )
1184            bui.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=15)
1185
1186        # Whatever the topmost selectable thing is, we want it to scroll all
1187        # the way up when we select it.
1188        if first_selectable is not None:
1189            bui.widget(
1190                edit=first_selectable, up_widget=bbtn, show_buffer_top=400
1191            )
1192            # (this should re-scroll us to the top..)
1193            bui.containerwidget(
1194                edit=self._subcontainer, visible_child=first_selectable
1195            )
1196        self._needs_refresh = False
1197
1198    def _on_game_service_button_press(self) -> None:
1199        if bui.app.plus is not None:
1200            bui.app.plus.show_game_service_ui()
1201        else:
1202            logging.warning(
1203                'game-service-ui not available without plus feature-set.'
1204            )
1205
1206    def _on_custom_achievements_press(self) -> None:
1207        if bui.app.plus is not None:
1208            bui.apptimer(
1209                0.15,
1210                bui.Call(bui.app.plus.show_game_service_ui, 'achievements'),
1211            )
1212        else:
1213            logging.warning('show_game_service_ui requires plus feature-set.')
1214
1215    def _on_achievements_press(self) -> None:
1216        # pylint: disable=cyclic-import
1217        from bauiv1lib import achievements
1218
1219        assert self._achievements_button is not None
1220        achievements.AchievementsWindow(
1221            position=self._achievements_button.get_screen_space_center()
1222        )
1223
1224    def _on_manage_account_press(self) -> None:
1225        self._do_manage_account_press(WebLocation.ACCOUNT_EDITOR)
1226
1227    def _on_delete_account_press(self) -> None:
1228        self._do_manage_account_press(WebLocation.ACCOUNT_DELETE_SECTION)
1229
1230    def _do_manage_account_press(self, weblocation: WebLocation) -> None:
1231        plus = bui.app.plus
1232        assert plus is not None
1233
1234        # Preemptively fail if it looks like we won't be able to talk to
1235        # the server anyway.
1236        if not plus.cloud.connected:
1237            bui.screenmessage(
1238                bui.Lstr(resource='internal.unavailableNoConnectionText'),
1239                color=(1, 0, 0),
1240            )
1241            bui.getsound('error').play()
1242            return
1243
1244        bui.screenmessage(bui.Lstr(resource='oneMomentText'))
1245
1246        # We expect to have a v2 account signed in if we get here.
1247        if plus.accounts.primary is None:
1248            logging.exception(
1249                'got manage-account press without v2 account present'
1250            )
1251            return
1252
1253        with plus.accounts.primary:
1254            plus.cloud.send_message_cb(
1255                bacommon.cloud.ManageAccountMessage(weblocation=weblocation),
1256                on_response=bui.WeakCall(self._on_manage_account_response),
1257            )
1258
1259    def _on_manage_account_response(
1260        self, response: bacommon.cloud.ManageAccountResponse | Exception
1261    ) -> None:
1262        if isinstance(response, Exception) or response.url is None:
1263            logging.warning(
1264                'Got error in manage-account-response: %s.', response
1265            )
1266            bui.screenmessage(bui.Lstr(resource='errorText'), color=(1, 0, 0))
1267            bui.getsound('error').play()
1268            return
1269
1270        bui.open_url(response.url)
1271
1272    def _on_leaderboards_press(self) -> None:
1273        if bui.app.plus is not None:
1274            bui.apptimer(
1275                0.15,
1276                bui.Call(bui.app.plus.show_game_service_ui, 'leaderboards'),
1277            )
1278        else:
1279            logging.warning('show_game_service_ui requires classic')
1280
1281    def _have_unlinkable_v1_accounts(self) -> bool:
1282        plus = bui.app.plus
1283        assert plus is not None
1284
1285        # if this is not present, we haven't had contact from the server so
1286        # let's not proceed..
1287        if plus.get_v1_account_public_login_id() is None:
1288            return False
1289        accounts = plus.get_v1_account_misc_read_val_2('linkedAccounts', [])
1290        return len(accounts) > 1
1291
1292    def _update_unlink_accounts_button(self) -> None:
1293        if self._unlink_accounts_button is None:
1294            return
1295        if self._have_unlinkable_v1_accounts():
1296            clr = (0.75, 0.7, 0.8, 1.0)
1297        else:
1298            clr = (1.0, 1.0, 1.0, 0.25)
1299        bui.textwidget(edit=self._unlink_accounts_button_label, color=clr)
1300
1301    def _should_show_legacy_unlink_button(self) -> bool:
1302        plus = bui.app.plus
1303        assert plus is not None
1304
1305        # Only show this when fully signed in to a v2 account.
1306        if not self._v1_signed_in or plus.accounts.primary is None:
1307            return False
1308
1309        out = self._have_unlinkable_v1_accounts()
1310        return out
1311
1312    def _update_linked_accounts_text(self) -> None:
1313        plus = bui.app.plus
1314        assert plus is not None
1315
1316        if self._linked_accounts_text is None:
1317            return
1318
1319        # Disable this by default when signed in to a V2 account
1320        # (since this shows V1 links which we should no longer care about).
1321        if plus.accounts.primary is not None and not FORCE_ENABLE_V1_LINKING:
1322            return
1323
1324        # if this is not present, we haven't had contact from the server so
1325        # let's not proceed..
1326        if plus.get_v1_account_public_login_id() is None:
1327            num = int(time.time()) % 4
1328            accounts_str = num * '.' + (4 - num) * ' '
1329        else:
1330            accounts = plus.get_v1_account_misc_read_val_2('linkedAccounts', [])
1331            # UPDATE - we now just print the number here; not the actual
1332            # accounts (they can see that in the unlink section if they're
1333            # curious)
1334            accounts_str = str(max(0, len(accounts) - 1))
1335        bui.textwidget(
1336            edit=self._linked_accounts_text,
1337            text=bui.Lstr(
1338                value='${L} ${A}',
1339                subs=[
1340                    (
1341                        '${L}',
1342                        bui.Lstr(resource=f'{self._r}.linkedAccountsText'),
1343                    ),
1344                    ('${A}', accounts_str),
1345                ],
1346            ),
1347        )
1348
1349    def _refresh_campaign_progress_text(self) -> None:
1350        if self._campaign_progress_text is None:
1351            return
1352        p_str: str | bui.Lstr
1353        try:
1354            assert bui.app.classic is not None
1355            campaign = bui.app.classic.getcampaign('Default')
1356            levels = campaign.levels
1357            levels_complete = sum((1 if l.complete else 0) for l in levels)
1358
1359            # Last level cant be completed; hence the -1;
1360            progress = min(1.0, float(levels_complete) / (len(levels) - 1))
1361            p_str = bui.Lstr(
1362                resource=f'{self._r}.campaignProgressText',
1363                subs=[('${PROGRESS}', str(int(progress * 100.0)) + '%')],
1364            )
1365        except Exception:
1366            p_str = '?'
1367            logging.exception('Error calculating co-op campaign progress.')
1368        bui.textwidget(edit=self._campaign_progress_text, text=p_str)
1369
1370    def _refresh_tickets_text(self) -> None:
1371        plus = bui.app.plus
1372        assert plus is not None
1373
1374        if self._tickets_text is None:
1375            return
1376        try:
1377            tc_str = str(plus.get_v1_account_ticket_count())
1378        except Exception:
1379            logging.exception('error refreshing tickets text')
1380            tc_str = '-'
1381        bui.textwidget(
1382            edit=self._tickets_text,
1383            text=bui.Lstr(
1384                resource=f'{self._r}.ticketsText', subs=[('${COUNT}', tc_str)]
1385            ),
1386        )
1387
1388    def _refresh_account_name_text(self) -> None:
1389        plus = bui.app.plus
1390        assert plus is not None
1391
1392        if self._account_name_text is None:
1393            return
1394        try:
1395            name_str = plus.get_v1_account_display_string()
1396        except Exception:
1397            logging.exception('error refreshing tickets text')
1398            name_str = '??'
1399
1400        bui.textwidget(edit=self._account_name_text, text=name_str)
1401
1402    def _refresh_achievements(self) -> None:
1403        assert bui.app.classic is not None
1404        if (
1405            self._achievements_text is None
1406            and self._achievements_button is None
1407        ):
1408            return
1409        complete = sum(
1410            1 if a.complete else 0 for a in bui.app.classic.ach.achievements
1411        )
1412        total = len(bui.app.classic.ach.achievements)
1413        txt_final = bui.Lstr(
1414            resource=f'{self._r}.achievementProgressText',
1415            subs=[('${COUNT}', str(complete)), ('${TOTAL}', str(total))],
1416        )
1417
1418        if self._achievements_text is not None:
1419            bui.textwidget(edit=self._achievements_text, text=txt_final)
1420        if self._achievements_button is not None:
1421            bui.buttonwidget(edit=self._achievements_button, label=txt_final)
1422
1423    def _link_accounts_press(self) -> None:
1424        # pylint: disable=cyclic-import
1425        from bauiv1lib.account.link import AccountLinkWindow
1426
1427        AccountLinkWindow(origin_widget=self._link_accounts_button)
1428
1429    def _unlink_accounts_press(self) -> None:
1430        # pylint: disable=cyclic-import
1431        from bauiv1lib.account.unlink import AccountUnlinkWindow
1432
1433        if not self._have_unlinkable_v1_accounts():
1434            bui.getsound('error').play()
1435            return
1436
1437        AccountUnlinkWindow(origin_widget=self._unlink_accounts_button)
1438
1439    def _cancel_sign_in_press(self) -> None:
1440        # If we're waiting on an adapter to give us credentials, abort.
1441        self._signing_in_adapter = None
1442
1443        plus = bui.app.plus
1444        assert plus is not None
1445
1446        # Say we don't wanna be signed in anymore if we are.
1447        plus.accounts.set_primary_credentials(None)
1448
1449        self._needs_refresh = True
1450
1451        # Speed UI updates along.
1452        bui.apptimer(0.1, bui.WeakCall(self._update))
1453
1454    def _sign_out_press(self) -> None:
1455        plus = bui.app.plus
1456        assert plus is not None
1457
1458        if plus.accounts.have_primary_credentials():
1459            if (
1460                plus.accounts.primary is not None
1461                and LoginType.GPGS in plus.accounts.primary.logins
1462            ):
1463                self._explicitly_signed_out_of_gpgs = True
1464            plus.accounts.set_primary_credentials(None)
1465        else:
1466            plus.sign_out_v1()
1467
1468        cfg = bui.app.config
1469
1470        # Also take note that its our *explicit* intention to not be
1471        # signed in at this point (affects v1 accounts).
1472        cfg['Auto Account State'] = 'signed_out'
1473        cfg.commit()
1474        bui.buttonwidget(
1475            edit=self._sign_out_button,
1476            label=bui.Lstr(resource=f'{self._r}.signingOutText'),
1477        )
1478
1479        # Speed UI updates along.
1480        bui.apptimer(0.1, bui.WeakCall(self._update))
1481
1482    def _sign_in_press(self, login_type: str | LoginType) -> None:
1483        from bauiv1lib.connectivity import wait_for_connectivity
1484
1485        # If we're still waiting for our master-server connection,
1486        # keep the user informed of this instead of rushing in and
1487        # failing immediately.
1488        wait_for_connectivity(on_connected=lambda: self._sign_in(login_type))
1489
1490    def _sign_in(self, login_type: str | LoginType) -> None:
1491        plus = bui.app.plus
1492        assert plus is not None
1493
1494        # V1 login types are strings.
1495        if isinstance(login_type, str):
1496            plus.sign_in_v1(login_type)
1497
1498            # Make note of the type account we're *wanting*
1499            # to be signed in with.
1500            cfg = bui.app.config
1501            cfg['Auto Account State'] = login_type
1502            cfg.commit()
1503            self._needs_refresh = True
1504            bui.apptimer(0.1, bui.WeakCall(self._update))
1505            return
1506
1507        # V2 login sign-in buttons generally go through adapters.
1508        adapter = plus.accounts.login_adapters.get(login_type)
1509        if adapter is not None:
1510            self._signing_in_adapter = adapter
1511            adapter.sign_in(
1512                result_cb=bui.WeakCall(self._on_adapter_sign_in_result),
1513                description='account settings button',
1514            )
1515            # Will get 'Signing in...' to show.
1516            self._needs_refresh = True
1517            bui.apptimer(0.1, bui.WeakCall(self._update))
1518        else:
1519            bui.screenmessage(f'Unsupported login_type: {login_type.name}')
1520
1521    def _on_adapter_sign_in_result(
1522        self,
1523        adapter: bui.LoginAdapter,
1524        result: bui.LoginAdapter.SignInResult | Exception,
1525    ) -> None:
1526        is_us = self._signing_in_adapter is adapter
1527
1528        # If this isn't our current one we don't care.
1529        if not is_us:
1530            return
1531
1532        # If it is us, note that we're done.
1533        self._signing_in_adapter = None
1534
1535        if isinstance(result, Exception):
1536            # For now just make a bit of noise if anything went wrong;
1537            # can get more specific as needed later.
1538            logging.warning('Got error in v2 sign-in result: %s', result)
1539            bui.screenmessage(
1540                bui.Lstr(resource='internal.signInNoConnectionText'),
1541                color=(1, 0, 0),
1542            )
1543            bui.getsound('error').play()
1544        else:
1545            # Success! Plug in these credentials which will begin
1546            # verifying them and set our primary account-handle when
1547            # finished.
1548            plus = bui.app.plus
1549            assert plus is not None
1550            plus.accounts.set_primary_credentials(result.credentials)
1551
1552            # Special case - if the user has explicitly signed out and
1553            # signed in again with GPGS via this button, warn them that
1554            # they need to use the app if they want to switch to a
1555            # different GPGS account.
1556            if (
1557                self._explicitly_signed_out_of_gpgs
1558                and adapter.login_type is LoginType.GPGS
1559            ):
1560                # Delay this slightly so it hopefully pops up after
1561                # credentials go through and the account name shows up.
1562                bui.apptimer(
1563                    1.5,
1564                    bui.Call(
1565                        bui.screenmessage,
1566                        bui.Lstr(
1567                            resource=self._r
1568                            + '.googlePlayGamesAccountSwitchText'
1569                        ),
1570                    ),
1571                )
1572
1573        # Speed any UI updates along.
1574        self._needs_refresh = True
1575        bui.apptimer(0.1, bui.WeakCall(self._update))
1576
1577    def _v2_proxy_sign_in_press(self) -> None:
1578        # pylint: disable=cyclic-import
1579        from bauiv1lib.connectivity import wait_for_connectivity
1580
1581        # If we're still waiting for our master-server connection, keep
1582        # the user informed of this instead of rushing in and failing
1583        # immediately.
1584        wait_for_connectivity(on_connected=self._v2_proxy_sign_in)
1585
1586    def _v2_proxy_sign_in(self) -> None:
1587        # pylint: disable=cyclic-import
1588        from bauiv1lib.account.v2proxy import V2ProxySignInWindow
1589
1590        assert self._sign_in_v2_proxy_button is not None
1591        V2ProxySignInWindow(origin_widget=self._sign_in_v2_proxy_button)
1592
1593    def _save_state(self) -> None:
1594        try:
1595            sel = self._root_widget.get_selected_child()
1596            if sel == self._back_button:
1597                sel_name = 'Back'
1598            elif sel == self._scrollwidget:
1599                sel_name = 'Scroll'
1600            else:
1601                raise ValueError('unrecognized selection')
1602            assert bui.app.classic is not None
1603            bui.app.ui_v1.window_states[type(self)] = sel_name
1604        except Exception:
1605            logging.exception('Error saving state for %s.', self)
1606
1607    def _restore_state(self) -> None:
1608        try:
1609            assert bui.app.classic is not None
1610            sel_name = bui.app.ui_v1.window_states.get(type(self))
1611            if sel_name == 'Back':
1612                sel = self._back_button
1613            elif sel_name == 'Scroll':
1614                sel = self._scrollwidget
1615            else:
1616                sel = self._back_button
1617            bui.containerwidget(edit=self._root_widget, selected_child=sel)
1618        except Exception:
1619            logging.exception('Error restoring state for %s.', self)
1620
1621
1622def show_what_is_legacy_unlinking_page() -> None:
1623    """Show the webpage describing legacy unlinking."""
1624    plus = bui.app.plus
1625    assert plus is not None
1626
1627    bamasteraddr = plus.get_master_server_address(version=2)
1628    bui.open_url(f'{bamasteraddr}/whatarev1links')
FORCE_ENABLE_V1_LINKING = False
class AccountSettingsWindow(bauiv1._uitypes.MainWindow):
  25class AccountSettingsWindow(bui.MainWindow):
  26    """Window for account related functionality."""
  27
  28    def __init__(
  29        self,
  30        transition: str | None = 'in_right',
  31        modal: bool = False,
  32        origin_widget: bui.Widget | None = None,
  33        close_once_signed_in: bool = False,
  34    ):
  35        # pylint: disable=too-many-statements
  36
  37        plus = bui.app.plus
  38        assert plus is not None
  39
  40        self._sign_in_v2_proxy_button: bui.Widget | None = None
  41        self._sign_in_device_button: bui.Widget | None = None
  42
  43        self._show_legacy_unlink_button = False
  44
  45        self._signing_in_adapter: bui.LoginAdapter | None = None
  46        self._close_once_signed_in = close_once_signed_in
  47        bui.set_analytics_screen('Account Window')
  48
  49        self._explicitly_signed_out_of_gpgs = False
  50
  51        self._r = 'accountSettingsWindow'
  52        self._modal = modal
  53        self._needs_refresh = False
  54        self._v1_signed_in = plus.get_v1_account_state() == 'signed_in'
  55        self._v1_account_state_num = plus.get_v1_account_state_num()
  56        self._check_sign_in_timer = bui.AppTimer(
  57            1.0, bui.WeakCall(self._update), repeat=True
  58        )
  59
  60        self._can_reset_achievements = False
  61
  62        app = bui.app
  63        assert app.classic is not None
  64        uiscale = app.ui_v1.uiscale
  65
  66        self._width = 850 if uiscale is bui.UIScale.SMALL else 660
  67        x_offs = 70 if uiscale is bui.UIScale.SMALL else 0
  68        self._height = (
  69            380
  70            if uiscale is bui.UIScale.SMALL
  71            else 430 if uiscale is bui.UIScale.MEDIUM else 490
  72        )
  73
  74        self._sign_in_button = None
  75        self._sign_in_text = None
  76
  77        self._scroll_width = self._width - (100 + x_offs * 2)
  78        self._scroll_height = self._height - 120
  79        self._sub_width = self._scroll_width - 20
  80
  81        # Determine which sign-in/sign-out buttons we should show.
  82        self._show_sign_in_buttons: list[str] = []
  83
  84        if LoginType.GPGS in plus.accounts.login_adapters:
  85            self._show_sign_in_buttons.append('Google Play')
  86
  87        if LoginType.GAME_CENTER in plus.accounts.login_adapters:
  88            self._show_sign_in_buttons.append('Game Center')
  89
  90        # Always want to show our web-based v2 login option.
  91        self._show_sign_in_buttons.append('V2Proxy')
  92
  93        # Legacy v1 device accounts available only if the user
  94        # has explicitly enabled deprecated login types.
  95        if bui.app.config.resolve('Show Deprecated Login Types'):
  96            self._show_sign_in_buttons.append('Device')
  97
  98        top_extra = 15 if uiscale is bui.UIScale.SMALL else 0
  99        super().__init__(
 100            root_widget=bui.containerwidget(
 101                size=(self._width, self._height + top_extra),
 102                # transition=transition,
 103                toolbar_visibility=(
 104                    'menu_minimal'
 105                    if uiscale is bui.UIScale.SMALL
 106                    else 'menu_full'
 107                ),
 108                scale=(
 109                    2.07
 110                    if uiscale is bui.UIScale.SMALL
 111                    else 1.4 if uiscale is bui.UIScale.MEDIUM else 1.0
 112                ),
 113                stack_offset=(
 114                    (0, 8) if uiscale is bui.UIScale.SMALL else (0, 0)
 115                ),
 116            ),
 117            transition=transition,
 118            origin_widget=origin_widget,
 119        )
 120        if uiscale is bui.UIScale.SMALL:
 121            self._back_button = None
 122            bui.containerwidget(
 123                edit=self._root_widget, on_cancel_call=self.main_window_back
 124            )
 125        else:
 126            self._back_button = btn = bui.buttonwidget(
 127                parent=self._root_widget,
 128                position=(51 + x_offs, self._height - 62),
 129                size=(120, 60),
 130                scale=0.8,
 131                text_scale=1.2,
 132                autoselect=True,
 133                label=bui.Lstr(
 134                    resource='doneText' if self._modal else 'backText'
 135                ),
 136                button_type='regular' if self._modal else 'back',
 137                on_activate_call=self.main_window_back,
 138            )
 139            bui.containerwidget(edit=self._root_widget, cancel_button=btn)
 140            if not self._modal:
 141                bui.buttonwidget(
 142                    edit=btn,
 143                    button_type='backSmall',
 144                    size=(60, 56),
 145                    label=bui.charstr(bui.SpecialChar.BACK),
 146                )
 147
 148        titleyoffs = -12 if uiscale is bui.UIScale.SMALL else 0
 149        titlescale = 0.6 if uiscale is bui.UIScale.SMALL else 1.0
 150        bui.textwidget(
 151            parent=self._root_widget,
 152            position=(self._width * 0.5, self._height - 41 + titleyoffs),
 153            size=(0, 0),
 154            text=bui.Lstr(resource=f'{self._r}.titleText'),
 155            color=app.ui_v1.title_color,
 156            scale=titlescale,
 157            maxwidth=self._width - 340,
 158            h_align='center',
 159            v_align='center',
 160        )
 161
 162        self._scrollwidget = bui.scrollwidget(
 163            parent=self._root_widget,
 164            highlight=False,
 165            position=(
 166                (self._width - self._scroll_width) * 0.5,
 167                self._height - 65 - self._scroll_height,
 168            ),
 169            size=(self._scroll_width, self._scroll_height),
 170            claims_left_right=True,
 171            claims_tab=True,
 172            selection_loops_to_parent=True,
 173        )
 174        self._subcontainer: bui.Widget | None = None
 175        self._refresh()
 176        self._restore_state()
 177
 178    @override
 179    def get_main_window_state(self) -> bui.MainWindowState:
 180        # Support recreating our window for back/refresh purposes.
 181        cls = type(self)
 182        return bui.BasicMainWindowState(
 183            create_call=lambda transition, origin_widget: cls(
 184                transition=transition, origin_widget=origin_widget
 185            )
 186        )
 187
 188    @override
 189    def on_main_window_close(self) -> None:
 190        self._save_state()
 191
 192    def _update(self) -> None:
 193        plus = bui.app.plus
 194        assert plus is not None
 195
 196        # If they want us to close once we're signed in, do so.
 197        if self._close_once_signed_in and self._v1_signed_in:
 198            self.main_window_back()
 199            return
 200
 201        # Hmm should update this to use get_account_state_num.
 202        # Theoretically if we switch from one signed-in account to another
 203        # in the background this would break.
 204        v1_account_state_num = plus.get_v1_account_state_num()
 205        v1_account_state = plus.get_v1_account_state()
 206        show_legacy_unlink_button = self._should_show_legacy_unlink_button()
 207
 208        if (
 209            v1_account_state_num != self._v1_account_state_num
 210            or show_legacy_unlink_button != self._show_legacy_unlink_button
 211            or self._needs_refresh
 212        ):
 213            self._v1_account_state_num = v1_account_state_num
 214            self._v1_signed_in = v1_account_state == 'signed_in'
 215            self._show_legacy_unlink_button = show_legacy_unlink_button
 216            self._refresh()
 217
 218        # Go ahead and refresh some individual things
 219        # that may change under us.
 220        self._update_linked_accounts_text()
 221        self._update_unlink_accounts_button()
 222        self._refresh_campaign_progress_text()
 223        self._refresh_achievements()
 224        self._refresh_tickets_text()
 225        self._refresh_account_name_text()
 226
 227    def _refresh(self) -> None:
 228        # pylint: disable=too-many-statements
 229        # pylint: disable=too-many-branches
 230        # pylint: disable=too-many-locals
 231        # pylint: disable=cyclic-import
 232
 233        plus = bui.app.plus
 234        assert plus is not None
 235
 236        via_lines: list[str] = []
 237
 238        primary_v2_account = plus.accounts.primary
 239
 240        v1_state = plus.get_v1_account_state()
 241        v1_account_type = (
 242            plus.get_v1_account_type() if v1_state == 'signed_in' else 'unknown'
 243        )
 244
 245        # We expose GPGS-specific functionality only if it is 'active'
 246        # (meaning the current GPGS player matches one of our account's
 247        # logins).
 248        adapter = plus.accounts.login_adapters.get(LoginType.GPGS)
 249        gpgs_active = adapter is not None and adapter.is_back_end_active()
 250
 251        # Ditto for Game Center.
 252        adapter = plus.accounts.login_adapters.get(LoginType.GAME_CENTER)
 253        game_center_active = (
 254            adapter is not None and adapter.is_back_end_active()
 255        )
 256
 257        show_signed_in_as = self._v1_signed_in
 258        signed_in_as_space = 95.0
 259
 260        # To reduce confusion about the whole V2 account situation for
 261        # people used to seeing their Google Play Games or Game Center
 262        # account name and icon and whatnot, let's show those underneath
 263        # the V2 tag to help communicate that they are in fact logged in
 264        # through that account.
 265        via_space = 25.0
 266        if show_signed_in_as and bui.app.plus is not None:
 267            accounts = bui.app.plus.accounts
 268            if accounts.primary is not None:
 269                # For these login types, we show 'via' IF there is a
 270                # login of that type attached to our account AND it is
 271                # currently active (We don't want to show 'via Game
 272                # Center' if we're signed out of Game Center or
 273                # currently running on Steam, even if there is a Game
 274                # Center login attached to our account).
 275                for ltype, lchar in [
 276                    (LoginType.GPGS, bui.SpecialChar.GOOGLE_PLAY_GAMES_LOGO),
 277                    (LoginType.GAME_CENTER, bui.SpecialChar.GAME_CENTER_LOGO),
 278                ]:
 279                    linfo = accounts.primary.logins.get(ltype)
 280                    ladapter = accounts.login_adapters.get(ltype)
 281                    if (
 282                        linfo is not None
 283                        and ladapter is not None
 284                        and ladapter.is_back_end_active()
 285                    ):
 286                        via_lines.append(f'{bui.charstr(lchar)}{linfo.name}')
 287
 288                # TEMP TESTING
 289                if bool(False):
 290                    icontxt = bui.charstr(bui.SpecialChar.GAME_CENTER_LOGO)
 291                    via_lines.append(f'{icontxt}FloofDibble')
 292                    icontxt = bui.charstr(
 293                        bui.SpecialChar.GOOGLE_PLAY_GAMES_LOGO
 294                    )
 295                    via_lines.append(f'{icontxt}StinkBobble')
 296
 297        show_sign_in_benefits = not self._v1_signed_in
 298        sign_in_benefits_space = 80.0
 299
 300        show_signing_in_text = (
 301            v1_state == 'signing_in' or self._signing_in_adapter is not None
 302        )
 303        signing_in_text_space = 80.0
 304
 305        show_google_play_sign_in_button = (
 306            v1_state == 'signed_out'
 307            and self._signing_in_adapter is None
 308            and 'Google Play' in self._show_sign_in_buttons
 309        )
 310        show_game_center_sign_in_button = (
 311            v1_state == 'signed_out'
 312            and self._signing_in_adapter is None
 313            and 'Game Center' in self._show_sign_in_buttons
 314        )
 315        show_v2_proxy_sign_in_button = (
 316            v1_state == 'signed_out'
 317            and self._signing_in_adapter is None
 318            and 'V2Proxy' in self._show_sign_in_buttons
 319        )
 320        show_device_sign_in_button = (
 321            v1_state == 'signed_out'
 322            and self._signing_in_adapter is None
 323            and 'Device' in self._show_sign_in_buttons
 324        )
 325        sign_in_button_space = 70.0
 326        deprecated_space = 60
 327
 328        # Game Center currently has a single UI for everything.
 329        show_game_service_button = game_center_active
 330        game_service_button_space = 60.0
 331
 332        # Phasing this out (for V2 accounts at least).
 333        show_linked_accounts_text = (
 334            self._v1_signed_in and v1_account_type != 'V2'
 335        )
 336        linked_accounts_text_space = 60.0
 337
 338        # Always show achievements except in the game-center case where
 339        # its unified UI covers them.
 340        # show_achievements_button =
 341        # self._v1_signed_in and not game_center_active
 342
 343        # Update: No longer showing this since its visible on main
 344        # toolbar.
 345        show_achievements_button = False
 346        achievements_button_space = 60.0
 347
 348        # show_achievements_text = (
 349        #     self._v1_signed_in and not show_achievements_button
 350        # )
 351
 352        # Update: No longer showing this since its visible on main
 353        # toolbar.
 354        show_achievements_text = False
 355        achievements_text_space = 27.0
 356
 357        show_leaderboards_button = self._v1_signed_in and gpgs_active
 358        leaderboards_button_space = 60.0
 359
 360        # Update: No longer showing this; trying to get progress type
 361        # stuff out of the account panel.
 362        # show_campaign_progress = self._v1_signed_in
 363        show_campaign_progress = False
 364        campaign_progress_space = 27.0
 365
 366        # show_tickets = self._v1_signed_in
 367        show_tickets = False
 368        tickets_space = 27.0
 369
 370        show_manage_account_button = primary_v2_account is not None
 371        manage_account_button_space = 70.0
 372
 373        show_delete_account_button = primary_v2_account is not None
 374        delete_account_button_space = 70.0
 375
 376        show_link_accounts_button = self._v1_signed_in and (
 377            primary_v2_account is None or FORCE_ENABLE_V1_LINKING
 378        )
 379        link_accounts_button_space = 70.0
 380
 381        show_unlink_accounts_button = show_link_accounts_button
 382        unlink_accounts_button_space = 90.0
 383
 384        # Phasing this out.
 385        show_v2_link_info = False
 386        v2_link_info_space = 70.0
 387
 388        legacy_unlink_button_space = 120.0
 389
 390        show_sign_out_button = primary_v2_account is not None or (
 391            self._v1_signed_in and v1_account_type == 'Local'
 392        )
 393        sign_out_button_space = 70.0
 394
 395        # We can show cancel if we're either waiting on an adapter to
 396        # provide us with v2 credentials or waiting for those
 397        # credentials to be verified.
 398        show_cancel_sign_in_button = self._signing_in_adapter is not None or (
 399            plus.accounts.have_primary_credentials()
 400            and primary_v2_account is None
 401        )
 402        cancel_sign_in_button_space = 70.0
 403
 404        if self._subcontainer is not None:
 405            self._subcontainer.delete()
 406        self._sub_height = 60.0
 407        if show_signed_in_as:
 408            self._sub_height += signed_in_as_space
 409        self._sub_height += via_space * len(via_lines)
 410        if show_signing_in_text:
 411            self._sub_height += signing_in_text_space
 412        if show_google_play_sign_in_button:
 413            self._sub_height += sign_in_button_space
 414        if show_game_center_sign_in_button:
 415            self._sub_height += sign_in_button_space
 416        if show_v2_proxy_sign_in_button:
 417            self._sub_height += sign_in_button_space
 418        if show_device_sign_in_button:
 419            self._sub_height += sign_in_button_space + deprecated_space
 420        if show_game_service_button:
 421            self._sub_height += game_service_button_space
 422        if show_linked_accounts_text:
 423            self._sub_height += linked_accounts_text_space
 424        if show_achievements_text:
 425            self._sub_height += achievements_text_space
 426        if show_achievements_button:
 427            self._sub_height += achievements_button_space
 428        if show_leaderboards_button:
 429            self._sub_height += leaderboards_button_space
 430        if show_campaign_progress:
 431            self._sub_height += campaign_progress_space
 432        if show_tickets:
 433            self._sub_height += tickets_space
 434        if show_sign_in_benefits:
 435            self._sub_height += sign_in_benefits_space
 436        if show_manage_account_button:
 437            self._sub_height += manage_account_button_space
 438        if show_link_accounts_button:
 439            self._sub_height += link_accounts_button_space
 440        if show_unlink_accounts_button:
 441            self._sub_height += unlink_accounts_button_space
 442        if show_v2_link_info:
 443            self._sub_height += v2_link_info_space
 444        if self._show_legacy_unlink_button:
 445            self._sub_height += legacy_unlink_button_space
 446        if show_sign_out_button:
 447            self._sub_height += sign_out_button_space
 448        if show_delete_account_button:
 449            self._sub_height += delete_account_button_space
 450        if show_cancel_sign_in_button:
 451            self._sub_height += cancel_sign_in_button_space
 452        self._subcontainer = bui.containerwidget(
 453            parent=self._scrollwidget,
 454            size=(self._sub_width, self._sub_height),
 455            background=False,
 456            claims_left_right=True,
 457            claims_tab=True,
 458            selection_loops_to_parent=True,
 459        )
 460
 461        first_selectable = None
 462        v = self._sub_height - 10.0
 463
 464        assert bui.app.classic is not None
 465        self._account_name_text: bui.Widget | None
 466        if show_signed_in_as:
 467            v -= signed_in_as_space * 0.2
 468            txt = bui.Lstr(
 469                resource='accountSettingsWindow.youAreSignedInAsText',
 470                fallback_resource='accountSettingsWindow.youAreLoggedInAsText',
 471            )
 472            bui.textwidget(
 473                parent=self._subcontainer,
 474                position=(self._sub_width * 0.5, v),
 475                size=(0, 0),
 476                text=txt,
 477                scale=0.9,
 478                color=bui.app.ui_v1.title_color,
 479                maxwidth=self._sub_width * 0.9,
 480                h_align='center',
 481                v_align='center',
 482            )
 483            v -= signed_in_as_space * 0.5
 484            self._account_name_text = bui.textwidget(
 485                parent=self._subcontainer,
 486                position=(self._sub_width * 0.5, v),
 487                size=(0, 0),
 488                scale=1.5,
 489                maxwidth=self._sub_width * 0.9,
 490                res_scale=1.5,
 491                color=(1, 1, 1, 1),
 492                h_align='center',
 493                v_align='center',
 494            )
 495
 496            self._refresh_account_name_text()
 497
 498            v -= signed_in_as_space * 0.4
 499
 500            for via in via_lines:
 501                v -= via_space * 0.1
 502                sscale = 0.7
 503                swidth = (
 504                    bui.get_string_width(via, suppress_warning=True) * sscale
 505                )
 506                bui.textwidget(
 507                    parent=self._subcontainer,
 508                    position=(self._sub_width * 0.5, v),
 509                    size=(0, 0),
 510                    text=via,
 511                    scale=sscale,
 512                    color=(0.6, 0.6, 0.6),
 513                    flatness=1.0,
 514                    shadow=0.0,
 515                    h_align='center',
 516                    v_align='center',
 517                )
 518                bui.textwidget(
 519                    parent=self._subcontainer,
 520                    position=(self._sub_width * 0.5 - swidth * 0.5 - 5, v),
 521                    size=(0, 0),
 522                    text=bui.Lstr(
 523                        value='(${VIA}',
 524                        subs=[('${VIA}', bui.Lstr(resource='viaText'))],
 525                    ),
 526                    scale=0.5,
 527                    color=(0.4, 0.6, 0.4, 0.5),
 528                    flatness=1.0,
 529                    shadow=0.0,
 530                    h_align='right',
 531                    v_align='center',
 532                )
 533                bui.textwidget(
 534                    parent=self._subcontainer,
 535                    position=(self._sub_width * 0.5 + swidth * 0.5 + 10, v),
 536                    size=(0, 0),
 537                    text=')',
 538                    scale=0.5,
 539                    color=(0.4, 0.6, 0.4, 0.5),
 540                    flatness=1.0,
 541                    shadow=0.0,
 542                    h_align='right',
 543                    v_align='center',
 544                )
 545
 546                v -= via_space * 0.9
 547
 548        else:
 549            self._account_name_text = None
 550
 551        if self._back_button is None:
 552            bbtn = bui.get_special_widget('back_button')
 553        else:
 554            bbtn = self._back_button
 555
 556        if show_sign_in_benefits:
 557            v -= sign_in_benefits_space
 558            bui.textwidget(
 559                parent=self._subcontainer,
 560                position=(
 561                    self._sub_width * 0.5,
 562                    v + sign_in_benefits_space * 0.4,
 563                ),
 564                size=(0, 0),
 565                text=bui.Lstr(resource=f'{self._r}.signInInfoText'),
 566                max_height=sign_in_benefits_space * 0.9,
 567                scale=0.9,
 568                color=(0.75, 0.7, 0.8),
 569                maxwidth=self._sub_width * 0.8,
 570                h_align='center',
 571                v_align='center',
 572            )
 573
 574        if show_signing_in_text:
 575            v -= signing_in_text_space
 576
 577            bui.textwidget(
 578                parent=self._subcontainer,
 579                position=(
 580                    self._sub_width * 0.5,
 581                    v + signing_in_text_space * 0.5,
 582                ),
 583                size=(0, 0),
 584                text=bui.Lstr(resource='accountSettingsWindow.signingInText'),
 585                scale=0.9,
 586                color=(0, 1, 0),
 587                maxwidth=self._sub_width * 0.8,
 588                h_align='center',
 589                v_align='center',
 590            )
 591
 592        if show_google_play_sign_in_button:
 593            button_width = 350
 594            v -= sign_in_button_space
 595            self._sign_in_google_play_button = btn = bui.buttonwidget(
 596                parent=self._subcontainer,
 597                position=((self._sub_width - button_width) * 0.5, v - 20),
 598                autoselect=True,
 599                size=(button_width, 60),
 600                label=bui.Lstr(
 601                    value='${A} ${B}',
 602                    subs=[
 603                        (
 604                            '${A}',
 605                            bui.charstr(bui.SpecialChar.GOOGLE_PLAY_GAMES_LOGO),
 606                        ),
 607                        (
 608                            '${B}',
 609                            bui.Lstr(
 610                                resource=f'{self._r}.signInWithText',
 611                                subs=[
 612                                    (
 613                                        '${SERVICE}',
 614                                        bui.Lstr(resource='googlePlayText'),
 615                                    )
 616                                ],
 617                            ),
 618                        ),
 619                    ],
 620                ),
 621                on_activate_call=lambda: self._sign_in_press(LoginType.GPGS),
 622            )
 623            if first_selectable is None:
 624                first_selectable = btn
 625            bui.widget(
 626                edit=btn, right_widget=bui.get_special_widget('squad_button')
 627            )
 628            bui.widget(edit=btn, left_widget=bbtn)
 629            bui.widget(edit=btn, show_buffer_bottom=40, show_buffer_top=100)
 630            self._sign_in_text = None
 631
 632        if show_game_center_sign_in_button:
 633            button_width = 350
 634            v -= sign_in_button_space
 635            self._sign_in_google_play_button = btn = bui.buttonwidget(
 636                parent=self._subcontainer,
 637                position=((self._sub_width - button_width) * 0.5, v - 20),
 638                autoselect=True,
 639                size=(button_width, 60),
 640                # Note: Apparently Game Center is just called 'Game Center'
 641                # in all languages. Can revisit if not true.
 642                # https://developer.apple.com/forums/thread/725779
 643                label=bui.Lstr(
 644                    value='${A} ${B}',
 645                    subs=[
 646                        (
 647                            '${A}',
 648                            bui.charstr(bui.SpecialChar.GAME_CENTER_LOGO),
 649                        ),
 650                        (
 651                            '${B}',
 652                            bui.Lstr(
 653                                resource=f'{self._r}.signInWithText',
 654                                subs=[('${SERVICE}', 'Game Center')],
 655                            ),
 656                        ),
 657                    ],
 658                ),
 659                on_activate_call=lambda: self._sign_in_press(
 660                    LoginType.GAME_CENTER
 661                ),
 662            )
 663            if first_selectable is None:
 664                first_selectable = btn
 665            bui.widget(
 666                edit=btn, right_widget=bui.get_special_widget('squad_button')
 667            )
 668            bui.widget(edit=btn, left_widget=bbtn)
 669            bui.widget(edit=btn, show_buffer_bottom=40, show_buffer_top=100)
 670            self._sign_in_text = None
 671
 672        if show_v2_proxy_sign_in_button:
 673            button_width = 350
 674            v -= sign_in_button_space
 675            self._sign_in_v2_proxy_button = btn = bui.buttonwidget(
 676                parent=self._subcontainer,
 677                position=((self._sub_width - button_width) * 0.5, v - 20),
 678                autoselect=True,
 679                size=(button_width, 60),
 680                label='',
 681                on_activate_call=self._v2_proxy_sign_in_press,
 682            )
 683
 684            v2labeltext: bui.Lstr | str = (
 685                bui.Lstr(resource=f'{self._r}.signInWithAnEmailAddressText')
 686                if show_game_center_sign_in_button
 687                or show_google_play_sign_in_button
 688                or show_device_sign_in_button
 689                else bui.Lstr(resource=f'{self._r}.signInText')
 690            )
 691            v2infotext: bui.Lstr | str | None = None
 692
 693            bui.textwidget(
 694                parent=self._subcontainer,
 695                draw_controller=btn,
 696                h_align='center',
 697                v_align='center',
 698                size=(0, 0),
 699                position=(
 700                    self._sub_width * 0.5,
 701                    v + (17 if v2infotext is not None else 10),
 702                ),
 703                text=bui.Lstr(
 704                    value='${A} ${B}',
 705                    subs=[
 706                        ('${A}', bui.charstr(bui.SpecialChar.V2_LOGO)),
 707                        (
 708                            '${B}',
 709                            v2labeltext,
 710                        ),
 711                    ],
 712                ),
 713                maxwidth=button_width * 0.8,
 714                color=(0.75, 1.0, 0.7),
 715            )
 716            if v2infotext is not None:
 717                bui.textwidget(
 718                    parent=self._subcontainer,
 719                    draw_controller=btn,
 720                    h_align='center',
 721                    v_align='center',
 722                    size=(0, 0),
 723                    position=(self._sub_width * 0.5, v - 4),
 724                    text=v2infotext,
 725                    flatness=1.0,
 726                    scale=0.57,
 727                    maxwidth=button_width * 0.9,
 728                    color=(0.55, 0.8, 0.5),
 729                )
 730            if first_selectable is None:
 731                first_selectable = btn
 732            bui.widget(
 733                edit=btn, right_widget=bui.get_special_widget('squad_button')
 734            )
 735            bui.widget(edit=btn, left_widget=bbtn)
 736            bui.widget(edit=btn, show_buffer_bottom=40, show_buffer_top=100)
 737            self._sign_in_text = None
 738
 739        if show_device_sign_in_button:
 740            button_width = 350
 741            v -= sign_in_button_space + deprecated_space
 742            self._sign_in_device_button = btn = bui.buttonwidget(
 743                parent=self._subcontainer,
 744                position=((self._sub_width - button_width) * 0.5, v - 20),
 745                autoselect=True,
 746                size=(button_width, 60),
 747                label='',
 748                on_activate_call=lambda: self._sign_in_press('Local'),
 749            )
 750            bui.textwidget(
 751                parent=self._subcontainer,
 752                h_align='center',
 753                v_align='center',
 754                size=(0, 0),
 755                position=(self._sub_width * 0.5, v + 60),
 756                text=bui.Lstr(resource='deprecatedText'),
 757                scale=0.8,
 758                maxwidth=300,
 759                color=(0.6, 0.55, 0.45),
 760            )
 761
 762            bui.textwidget(
 763                parent=self._subcontainer,
 764                draw_controller=btn,
 765                h_align='center',
 766                v_align='center',
 767                size=(0, 0),
 768                position=(self._sub_width * 0.5, v + 17),
 769                text=bui.Lstr(
 770                    value='${A} ${B}',
 771                    subs=[
 772                        ('${A}', bui.charstr(bui.SpecialChar.LOCAL_ACCOUNT)),
 773                        (
 774                            '${B}',
 775                            bui.Lstr(
 776                                resource=f'{self._r}.signInWithDeviceText'
 777                            ),
 778                        ),
 779                    ],
 780                ),
 781                maxwidth=button_width * 0.8,
 782                color=(0.75, 1.0, 0.7),
 783            )
 784            bui.textwidget(
 785                parent=self._subcontainer,
 786                draw_controller=btn,
 787                h_align='center',
 788                v_align='center',
 789                size=(0, 0),
 790                position=(self._sub_width * 0.5, v - 4),
 791                text=bui.Lstr(resource=f'{self._r}.signInWithDeviceInfoText'),
 792                flatness=1.0,
 793                scale=0.57,
 794                maxwidth=button_width * 0.9,
 795                color=(0.55, 0.8, 0.5),
 796            )
 797            if first_selectable is None:
 798                first_selectable = btn
 799            bui.widget(
 800                edit=btn, right_widget=bui.get_special_widget('squad_button')
 801            )
 802            bui.widget(edit=btn, left_widget=bbtn)
 803            bui.widget(edit=btn, show_buffer_bottom=40, show_buffer_top=100)
 804            self._sign_in_text = None
 805
 806        if show_manage_account_button:
 807            button_width = 300
 808            v -= manage_account_button_space
 809            self._manage_button = btn = bui.buttonwidget(
 810                parent=self._subcontainer,
 811                position=((self._sub_width - button_width) * 0.5, v),
 812                autoselect=True,
 813                size=(button_width, 60),
 814                label=bui.Lstr(resource=f'{self._r}.manageAccountText'),
 815                color=(0.55, 0.5, 0.6),
 816                icon=bui.gettexture('settingsIcon'),
 817                textcolor=(0.75, 0.7, 0.8),
 818                on_activate_call=bui.WeakCall(self._on_manage_account_press),
 819            )
 820            if first_selectable is None:
 821                first_selectable = btn
 822            bui.widget(
 823                edit=btn, right_widget=bui.get_special_widget('squad_button')
 824            )
 825            bui.widget(edit=btn, left_widget=bbtn)
 826
 827        # the button to go to OS-Specific leaderboards/high-score-lists/etc.
 828        if show_game_service_button:
 829            button_width = 300
 830            v -= game_service_button_space * 0.6
 831            if game_center_active:
 832                # Note: Apparently Game Center is just called 'Game Center'
 833                # in all languages. Can revisit if not true.
 834                # https://developer.apple.com/forums/thread/725779
 835                game_service_button_label = bui.Lstr(
 836                    value=bui.charstr(bui.SpecialChar.GAME_CENTER_LOGO)
 837                    + 'Game Center'
 838                )
 839            else:
 840                raise ValueError(
 841                    "unknown account type: '" + str(v1_account_type) + "'"
 842                )
 843            self._game_service_button = btn = bui.buttonwidget(
 844                parent=self._subcontainer,
 845                position=((self._sub_width - button_width) * 0.5, v),
 846                color=(0.55, 0.5, 0.6),
 847                textcolor=(0.75, 0.7, 0.8),
 848                autoselect=True,
 849                on_activate_call=self._on_game_service_button_press,
 850                size=(button_width, 50),
 851                label=game_service_button_label,
 852            )
 853            if first_selectable is None:
 854                first_selectable = btn
 855            bui.widget(
 856                edit=btn, right_widget=bui.get_special_widget('squad_button')
 857            )
 858            bui.widget(edit=btn, left_widget=bbtn)
 859            v -= game_service_button_space * 0.4
 860        else:
 861            self.game_service_button = None
 862
 863        self._achievements_text: bui.Widget | None
 864        if show_achievements_text:
 865            v -= achievements_text_space * 0.5
 866            self._achievements_text = bui.textwidget(
 867                parent=self._subcontainer,
 868                position=(self._sub_width * 0.5, v),
 869                size=(0, 0),
 870                scale=0.9,
 871                color=(0.75, 0.7, 0.8),
 872                maxwidth=self._sub_width * 0.8,
 873                h_align='center',
 874                v_align='center',
 875            )
 876            v -= achievements_text_space * 0.5
 877        else:
 878            self._achievements_text = None
 879
 880        self._achievements_button: bui.Widget | None
 881        if show_achievements_button:
 882            button_width = 300
 883            v -= achievements_button_space * 0.85
 884            self._achievements_button = btn = bui.buttonwidget(
 885                parent=self._subcontainer,
 886                position=((self._sub_width - button_width) * 0.5, v),
 887                color=(0.55, 0.5, 0.6),
 888                textcolor=(0.75, 0.7, 0.8),
 889                autoselect=True,
 890                icon=bui.gettexture(
 891                    'googlePlayAchievementsIcon'
 892                    if gpgs_active
 893                    else 'achievementsIcon'
 894                ),
 895                icon_color=(
 896                    (0.8, 0.95, 0.7) if gpgs_active else (0.85, 0.8, 0.9)
 897                ),
 898                on_activate_call=(
 899                    self._on_custom_achievements_press
 900                    if gpgs_active
 901                    else self._on_achievements_press
 902                ),
 903                size=(button_width, 50),
 904                label='',
 905            )
 906            if first_selectable is None:
 907                first_selectable = btn
 908            bui.widget(
 909                edit=btn, right_widget=bui.get_special_widget('squad_button')
 910            )
 911            bui.widget(edit=btn, left_widget=bbtn)
 912            v -= achievements_button_space * 0.15
 913        else:
 914            self._achievements_button = None
 915
 916        if show_achievements_text or show_achievements_button:
 917            self._refresh_achievements()
 918
 919        self._leaderboards_button: bui.Widget | None
 920        if show_leaderboards_button:
 921            button_width = 300
 922            v -= leaderboards_button_space * 0.85
 923            self._leaderboards_button = btn = bui.buttonwidget(
 924                parent=self._subcontainer,
 925                position=((self._sub_width - button_width) * 0.5, v),
 926                color=(0.55, 0.5, 0.6),
 927                textcolor=(0.75, 0.7, 0.8),
 928                autoselect=True,
 929                icon=bui.gettexture('googlePlayLeaderboardsIcon'),
 930                icon_color=(0.8, 0.95, 0.7),
 931                on_activate_call=self._on_leaderboards_press,
 932                size=(button_width, 50),
 933                label=bui.Lstr(resource='leaderboardsText'),
 934            )
 935            if first_selectable is None:
 936                first_selectable = btn
 937            bui.widget(
 938                edit=btn, right_widget=bui.get_special_widget('squad_button')
 939            )
 940            bui.widget(edit=btn, left_widget=bbtn)
 941            v -= leaderboards_button_space * 0.15
 942        else:
 943            self._leaderboards_button = None
 944
 945        self._campaign_progress_text: bui.Widget | None
 946        if show_campaign_progress:
 947            v -= campaign_progress_space * 0.5
 948            self._campaign_progress_text = bui.textwidget(
 949                parent=self._subcontainer,
 950                position=(self._sub_width * 0.5, v),
 951                size=(0, 0),
 952                scale=0.9,
 953                color=(0.75, 0.7, 0.8),
 954                maxwidth=self._sub_width * 0.8,
 955                h_align='center',
 956                v_align='center',
 957            )
 958            v -= campaign_progress_space * 0.5
 959            self._refresh_campaign_progress_text()
 960        else:
 961            self._campaign_progress_text = None
 962
 963        self._tickets_text: bui.Widget | None
 964        if show_tickets:
 965            v -= tickets_space * 0.5
 966            self._tickets_text = bui.textwidget(
 967                parent=self._subcontainer,
 968                position=(self._sub_width * 0.5, v),
 969                size=(0, 0),
 970                scale=0.9,
 971                color=(0.75, 0.7, 0.8),
 972                maxwidth=self._sub_width * 0.8,
 973                flatness=1.0,
 974                h_align='center',
 975                v_align='center',
 976            )
 977            v -= tickets_space * 0.5
 978            self._refresh_tickets_text()
 979
 980        else:
 981            self._tickets_text = None
 982
 983        # bit of spacing before the reset/sign-out section
 984        # v -= 5
 985
 986        button_width = 250
 987
 988        self._linked_accounts_text: bui.Widget | None
 989        if show_linked_accounts_text:
 990            v -= linked_accounts_text_space * 0.8
 991            self._linked_accounts_text = bui.textwidget(
 992                parent=self._subcontainer,
 993                position=(self._sub_width * 0.5, v),
 994                size=(0, 0),
 995                scale=0.9,
 996                color=(0.75, 0.7, 0.8),
 997                maxwidth=self._sub_width * 0.95,
 998                text=bui.Lstr(resource=f'{self._r}.linkedAccountsText'),
 999                h_align='center',
1000                v_align='center',
1001            )
1002            v -= linked_accounts_text_space * 0.2
1003            self._update_linked_accounts_text()
1004        else:
1005            self._linked_accounts_text = None
1006
1007        # Show link/unlink buttons only for V1 accounts.
1008
1009        if show_link_accounts_button:
1010            v -= link_accounts_button_space
1011            self._link_accounts_button = btn = bui.buttonwidget(
1012                parent=self._subcontainer,
1013                position=((self._sub_width - button_width) * 0.5, v),
1014                autoselect=True,
1015                size=(button_width, 60),
1016                label='',
1017                color=(0.55, 0.5, 0.6),
1018                on_activate_call=self._link_accounts_press,
1019            )
1020            bui.textwidget(
1021                parent=self._subcontainer,
1022                draw_controller=btn,
1023                h_align='center',
1024                v_align='center',
1025                size=(0, 0),
1026                position=(self._sub_width * 0.5, v + 17 + 20),
1027                text=bui.Lstr(resource=f'{self._r}.linkAccountsText'),
1028                maxwidth=button_width * 0.8,
1029                color=(0.75, 0.7, 0.8),
1030            )
1031            bui.textwidget(
1032                parent=self._subcontainer,
1033                draw_controller=btn,
1034                h_align='center',
1035                v_align='center',
1036                size=(0, 0),
1037                position=(self._sub_width * 0.5, v - 4 + 20),
1038                text=bui.Lstr(resource=f'{self._r}.linkAccountsInfoText'),
1039                flatness=1.0,
1040                scale=0.5,
1041                maxwidth=button_width * 0.8,
1042                color=(0.75, 0.7, 0.8),
1043            )
1044            if first_selectable is None:
1045                first_selectable = btn
1046            bui.widget(
1047                edit=btn, right_widget=bui.get_special_widget('squad_button')
1048            )
1049            bui.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=50)
1050
1051        self._unlink_accounts_button: bui.Widget | None
1052        if show_unlink_accounts_button:
1053            v -= unlink_accounts_button_space
1054            self._unlink_accounts_button = btn = bui.buttonwidget(
1055                parent=self._subcontainer,
1056                position=((self._sub_width - button_width) * 0.5, v + 25),
1057                autoselect=True,
1058                size=(button_width, 60),
1059                label='',
1060                color=(0.55, 0.5, 0.6),
1061                on_activate_call=self._unlink_accounts_press,
1062            )
1063            self._unlink_accounts_button_label = bui.textwidget(
1064                parent=self._subcontainer,
1065                draw_controller=btn,
1066                h_align='center',
1067                v_align='center',
1068                size=(0, 0),
1069                position=(self._sub_width * 0.5, v + 55),
1070                text=bui.Lstr(resource=f'{self._r}.unlinkAccountsText'),
1071                maxwidth=button_width * 0.8,
1072                color=(0.75, 0.7, 0.8),
1073            )
1074            if first_selectable is None:
1075                first_selectable = btn
1076            bui.widget(
1077                edit=btn, right_widget=bui.get_special_widget('squad_button')
1078            )
1079            bui.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=50)
1080            self._update_unlink_accounts_button()
1081        else:
1082            self._unlink_accounts_button = None
1083
1084        if show_v2_link_info:
1085            v -= v2_link_info_space
1086            bui.textwidget(
1087                parent=self._subcontainer,
1088                h_align='center',
1089                v_align='center',
1090                size=(0, 0),
1091                position=(self._sub_width * 0.5, v + v2_link_info_space - 20),
1092                text=bui.Lstr(resource='v2AccountLinkingInfoText'),
1093                flatness=1.0,
1094                scale=0.8,
1095                maxwidth=450,
1096                color=(0.5, 0.45, 0.55),
1097            )
1098
1099        if self._show_legacy_unlink_button:
1100            v -= legacy_unlink_button_space
1101            button_width_w = button_width * 1.5
1102            bui.textwidget(
1103                parent=self._subcontainer,
1104                position=(self._sub_width * 0.5 - 150.0, v + 75),
1105                size=(300.0, 60),
1106                text=bui.Lstr(resource='whatIsThisText'),
1107                scale=0.8,
1108                color=(0.3, 0.7, 0.05),
1109                maxwidth=200.0,
1110                h_align='center',
1111                v_align='center',
1112                autoselect=True,
1113                selectable=True,
1114                on_activate_call=show_what_is_legacy_unlinking_page,
1115                click_activate=True,
1116            )
1117            btn = bui.buttonwidget(
1118                parent=self._subcontainer,
1119                position=((self._sub_width - button_width_w) * 0.5, v + 25),
1120                autoselect=True,
1121                size=(button_width_w, 60),
1122                label=bui.Lstr(
1123                    resource=f'{self._r}.unlinkLegacyV1AccountsText'
1124                ),
1125                textcolor=(0.8, 0.4, 0),
1126                color=(0.55, 0.5, 0.6),
1127                on_activate_call=self._unlink_accounts_press,
1128            )
1129
1130        if show_sign_out_button:
1131            v -= sign_out_button_space
1132            self._sign_out_button = btn = bui.buttonwidget(
1133                parent=self._subcontainer,
1134                position=((self._sub_width - button_width) * 0.5, v),
1135                size=(button_width, 60),
1136                label=bui.Lstr(resource=f'{self._r}.signOutText'),
1137                color=(0.55, 0.5, 0.6),
1138                textcolor=(0.75, 0.7, 0.8),
1139                autoselect=True,
1140                on_activate_call=self._sign_out_press,
1141            )
1142            if first_selectable is None:
1143                first_selectable = btn
1144            bui.widget(
1145                edit=btn, right_widget=bui.get_special_widget('squad_button')
1146            )
1147            bui.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=15)
1148
1149        if show_cancel_sign_in_button:
1150            v -= cancel_sign_in_button_space
1151            self._cancel_sign_in_button = btn = bui.buttonwidget(
1152                parent=self._subcontainer,
1153                position=((self._sub_width - button_width) * 0.5, v),
1154                size=(button_width, 60),
1155                label=bui.Lstr(resource='cancelText'),
1156                color=(0.55, 0.5, 0.6),
1157                textcolor=(0.75, 0.7, 0.8),
1158                autoselect=True,
1159                on_activate_call=self._cancel_sign_in_press,
1160            )
1161            if first_selectable is None:
1162                first_selectable = btn
1163            bui.widget(
1164                edit=btn, right_widget=bui.get_special_widget('squad_button')
1165            )
1166            bui.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=15)
1167
1168        if show_delete_account_button:
1169            v -= delete_account_button_space
1170            self._delete_account_button = btn = bui.buttonwidget(
1171                parent=self._subcontainer,
1172                position=((self._sub_width - button_width) * 0.5, v),
1173                size=(button_width, 60),
1174                label=bui.Lstr(resource=f'{self._r}.deleteAccountText'),
1175                color=(0.85, 0.5, 0.6),
1176                textcolor=(0.9, 0.7, 0.8),
1177                autoselect=True,
1178                on_activate_call=self._on_delete_account_press,
1179            )
1180            if first_selectable is None:
1181                first_selectable = btn
1182            bui.widget(
1183                edit=btn, right_widget=bui.get_special_widget('squad_button')
1184            )
1185            bui.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=15)
1186
1187        # Whatever the topmost selectable thing is, we want it to scroll all
1188        # the way up when we select it.
1189        if first_selectable is not None:
1190            bui.widget(
1191                edit=first_selectable, up_widget=bbtn, show_buffer_top=400
1192            )
1193            # (this should re-scroll us to the top..)
1194            bui.containerwidget(
1195                edit=self._subcontainer, visible_child=first_selectable
1196            )
1197        self._needs_refresh = False
1198
1199    def _on_game_service_button_press(self) -> None:
1200        if bui.app.plus is not None:
1201            bui.app.plus.show_game_service_ui()
1202        else:
1203            logging.warning(
1204                'game-service-ui not available without plus feature-set.'
1205            )
1206
1207    def _on_custom_achievements_press(self) -> None:
1208        if bui.app.plus is not None:
1209            bui.apptimer(
1210                0.15,
1211                bui.Call(bui.app.plus.show_game_service_ui, 'achievements'),
1212            )
1213        else:
1214            logging.warning('show_game_service_ui requires plus feature-set.')
1215
1216    def _on_achievements_press(self) -> None:
1217        # pylint: disable=cyclic-import
1218        from bauiv1lib import achievements
1219
1220        assert self._achievements_button is not None
1221        achievements.AchievementsWindow(
1222            position=self._achievements_button.get_screen_space_center()
1223        )
1224
1225    def _on_manage_account_press(self) -> None:
1226        self._do_manage_account_press(WebLocation.ACCOUNT_EDITOR)
1227
1228    def _on_delete_account_press(self) -> None:
1229        self._do_manage_account_press(WebLocation.ACCOUNT_DELETE_SECTION)
1230
1231    def _do_manage_account_press(self, weblocation: WebLocation) -> None:
1232        plus = bui.app.plus
1233        assert plus is not None
1234
1235        # Preemptively fail if it looks like we won't be able to talk to
1236        # the server anyway.
1237        if not plus.cloud.connected:
1238            bui.screenmessage(
1239                bui.Lstr(resource='internal.unavailableNoConnectionText'),
1240                color=(1, 0, 0),
1241            )
1242            bui.getsound('error').play()
1243            return
1244
1245        bui.screenmessage(bui.Lstr(resource='oneMomentText'))
1246
1247        # We expect to have a v2 account signed in if we get here.
1248        if plus.accounts.primary is None:
1249            logging.exception(
1250                'got manage-account press without v2 account present'
1251            )
1252            return
1253
1254        with plus.accounts.primary:
1255            plus.cloud.send_message_cb(
1256                bacommon.cloud.ManageAccountMessage(weblocation=weblocation),
1257                on_response=bui.WeakCall(self._on_manage_account_response),
1258            )
1259
1260    def _on_manage_account_response(
1261        self, response: bacommon.cloud.ManageAccountResponse | Exception
1262    ) -> None:
1263        if isinstance(response, Exception) or response.url is None:
1264            logging.warning(
1265                'Got error in manage-account-response: %s.', response
1266            )
1267            bui.screenmessage(bui.Lstr(resource='errorText'), color=(1, 0, 0))
1268            bui.getsound('error').play()
1269            return
1270
1271        bui.open_url(response.url)
1272
1273    def _on_leaderboards_press(self) -> None:
1274        if bui.app.plus is not None:
1275            bui.apptimer(
1276                0.15,
1277                bui.Call(bui.app.plus.show_game_service_ui, 'leaderboards'),
1278            )
1279        else:
1280            logging.warning('show_game_service_ui requires classic')
1281
1282    def _have_unlinkable_v1_accounts(self) -> bool:
1283        plus = bui.app.plus
1284        assert plus is not None
1285
1286        # if this is not present, we haven't had contact from the server so
1287        # let's not proceed..
1288        if plus.get_v1_account_public_login_id() is None:
1289            return False
1290        accounts = plus.get_v1_account_misc_read_val_2('linkedAccounts', [])
1291        return len(accounts) > 1
1292
1293    def _update_unlink_accounts_button(self) -> None:
1294        if self._unlink_accounts_button is None:
1295            return
1296        if self._have_unlinkable_v1_accounts():
1297            clr = (0.75, 0.7, 0.8, 1.0)
1298        else:
1299            clr = (1.0, 1.0, 1.0, 0.25)
1300        bui.textwidget(edit=self._unlink_accounts_button_label, color=clr)
1301
1302    def _should_show_legacy_unlink_button(self) -> bool:
1303        plus = bui.app.plus
1304        assert plus is not None
1305
1306        # Only show this when fully signed in to a v2 account.
1307        if not self._v1_signed_in or plus.accounts.primary is None:
1308            return False
1309
1310        out = self._have_unlinkable_v1_accounts()
1311        return out
1312
1313    def _update_linked_accounts_text(self) -> None:
1314        plus = bui.app.plus
1315        assert plus is not None
1316
1317        if self._linked_accounts_text is None:
1318            return
1319
1320        # Disable this by default when signed in to a V2 account
1321        # (since this shows V1 links which we should no longer care about).
1322        if plus.accounts.primary is not None and not FORCE_ENABLE_V1_LINKING:
1323            return
1324
1325        # if this is not present, we haven't had contact from the server so
1326        # let's not proceed..
1327        if plus.get_v1_account_public_login_id() is None:
1328            num = int(time.time()) % 4
1329            accounts_str = num * '.' + (4 - num) * ' '
1330        else:
1331            accounts = plus.get_v1_account_misc_read_val_2('linkedAccounts', [])
1332            # UPDATE - we now just print the number here; not the actual
1333            # accounts (they can see that in the unlink section if they're
1334            # curious)
1335            accounts_str = str(max(0, len(accounts) - 1))
1336        bui.textwidget(
1337            edit=self._linked_accounts_text,
1338            text=bui.Lstr(
1339                value='${L} ${A}',
1340                subs=[
1341                    (
1342                        '${L}',
1343                        bui.Lstr(resource=f'{self._r}.linkedAccountsText'),
1344                    ),
1345                    ('${A}', accounts_str),
1346                ],
1347            ),
1348        )
1349
1350    def _refresh_campaign_progress_text(self) -> None:
1351        if self._campaign_progress_text is None:
1352            return
1353        p_str: str | bui.Lstr
1354        try:
1355            assert bui.app.classic is not None
1356            campaign = bui.app.classic.getcampaign('Default')
1357            levels = campaign.levels
1358            levels_complete = sum((1 if l.complete else 0) for l in levels)
1359
1360            # Last level cant be completed; hence the -1;
1361            progress = min(1.0, float(levels_complete) / (len(levels) - 1))
1362            p_str = bui.Lstr(
1363                resource=f'{self._r}.campaignProgressText',
1364                subs=[('${PROGRESS}', str(int(progress * 100.0)) + '%')],
1365            )
1366        except Exception:
1367            p_str = '?'
1368            logging.exception('Error calculating co-op campaign progress.')
1369        bui.textwidget(edit=self._campaign_progress_text, text=p_str)
1370
1371    def _refresh_tickets_text(self) -> None:
1372        plus = bui.app.plus
1373        assert plus is not None
1374
1375        if self._tickets_text is None:
1376            return
1377        try:
1378            tc_str = str(plus.get_v1_account_ticket_count())
1379        except Exception:
1380            logging.exception('error refreshing tickets text')
1381            tc_str = '-'
1382        bui.textwidget(
1383            edit=self._tickets_text,
1384            text=bui.Lstr(
1385                resource=f'{self._r}.ticketsText', subs=[('${COUNT}', tc_str)]
1386            ),
1387        )
1388
1389    def _refresh_account_name_text(self) -> None:
1390        plus = bui.app.plus
1391        assert plus is not None
1392
1393        if self._account_name_text is None:
1394            return
1395        try:
1396            name_str = plus.get_v1_account_display_string()
1397        except Exception:
1398            logging.exception('error refreshing tickets text')
1399            name_str = '??'
1400
1401        bui.textwidget(edit=self._account_name_text, text=name_str)
1402
1403    def _refresh_achievements(self) -> None:
1404        assert bui.app.classic is not None
1405        if (
1406            self._achievements_text is None
1407            and self._achievements_button is None
1408        ):
1409            return
1410        complete = sum(
1411            1 if a.complete else 0 for a in bui.app.classic.ach.achievements
1412        )
1413        total = len(bui.app.classic.ach.achievements)
1414        txt_final = bui.Lstr(
1415            resource=f'{self._r}.achievementProgressText',
1416            subs=[('${COUNT}', str(complete)), ('${TOTAL}', str(total))],
1417        )
1418
1419        if self._achievements_text is not None:
1420            bui.textwidget(edit=self._achievements_text, text=txt_final)
1421        if self._achievements_button is not None:
1422            bui.buttonwidget(edit=self._achievements_button, label=txt_final)
1423
1424    def _link_accounts_press(self) -> None:
1425        # pylint: disable=cyclic-import
1426        from bauiv1lib.account.link import AccountLinkWindow
1427
1428        AccountLinkWindow(origin_widget=self._link_accounts_button)
1429
1430    def _unlink_accounts_press(self) -> None:
1431        # pylint: disable=cyclic-import
1432        from bauiv1lib.account.unlink import AccountUnlinkWindow
1433
1434        if not self._have_unlinkable_v1_accounts():
1435            bui.getsound('error').play()
1436            return
1437
1438        AccountUnlinkWindow(origin_widget=self._unlink_accounts_button)
1439
1440    def _cancel_sign_in_press(self) -> None:
1441        # If we're waiting on an adapter to give us credentials, abort.
1442        self._signing_in_adapter = None
1443
1444        plus = bui.app.plus
1445        assert plus is not None
1446
1447        # Say we don't wanna be signed in anymore if we are.
1448        plus.accounts.set_primary_credentials(None)
1449
1450        self._needs_refresh = True
1451
1452        # Speed UI updates along.
1453        bui.apptimer(0.1, bui.WeakCall(self._update))
1454
1455    def _sign_out_press(self) -> None:
1456        plus = bui.app.plus
1457        assert plus is not None
1458
1459        if plus.accounts.have_primary_credentials():
1460            if (
1461                plus.accounts.primary is not None
1462                and LoginType.GPGS in plus.accounts.primary.logins
1463            ):
1464                self._explicitly_signed_out_of_gpgs = True
1465            plus.accounts.set_primary_credentials(None)
1466        else:
1467            plus.sign_out_v1()
1468
1469        cfg = bui.app.config
1470
1471        # Also take note that its our *explicit* intention to not be
1472        # signed in at this point (affects v1 accounts).
1473        cfg['Auto Account State'] = 'signed_out'
1474        cfg.commit()
1475        bui.buttonwidget(
1476            edit=self._sign_out_button,
1477            label=bui.Lstr(resource=f'{self._r}.signingOutText'),
1478        )
1479
1480        # Speed UI updates along.
1481        bui.apptimer(0.1, bui.WeakCall(self._update))
1482
1483    def _sign_in_press(self, login_type: str | LoginType) -> None:
1484        from bauiv1lib.connectivity import wait_for_connectivity
1485
1486        # If we're still waiting for our master-server connection,
1487        # keep the user informed of this instead of rushing in and
1488        # failing immediately.
1489        wait_for_connectivity(on_connected=lambda: self._sign_in(login_type))
1490
1491    def _sign_in(self, login_type: str | LoginType) -> None:
1492        plus = bui.app.plus
1493        assert plus is not None
1494
1495        # V1 login types are strings.
1496        if isinstance(login_type, str):
1497            plus.sign_in_v1(login_type)
1498
1499            # Make note of the type account we're *wanting*
1500            # to be signed in with.
1501            cfg = bui.app.config
1502            cfg['Auto Account State'] = login_type
1503            cfg.commit()
1504            self._needs_refresh = True
1505            bui.apptimer(0.1, bui.WeakCall(self._update))
1506            return
1507
1508        # V2 login sign-in buttons generally go through adapters.
1509        adapter = plus.accounts.login_adapters.get(login_type)
1510        if adapter is not None:
1511            self._signing_in_adapter = adapter
1512            adapter.sign_in(
1513                result_cb=bui.WeakCall(self._on_adapter_sign_in_result),
1514                description='account settings button',
1515            )
1516            # Will get 'Signing in...' to show.
1517            self._needs_refresh = True
1518            bui.apptimer(0.1, bui.WeakCall(self._update))
1519        else:
1520            bui.screenmessage(f'Unsupported login_type: {login_type.name}')
1521
1522    def _on_adapter_sign_in_result(
1523        self,
1524        adapter: bui.LoginAdapter,
1525        result: bui.LoginAdapter.SignInResult | Exception,
1526    ) -> None:
1527        is_us = self._signing_in_adapter is adapter
1528
1529        # If this isn't our current one we don't care.
1530        if not is_us:
1531            return
1532
1533        # If it is us, note that we're done.
1534        self._signing_in_adapter = None
1535
1536        if isinstance(result, Exception):
1537            # For now just make a bit of noise if anything went wrong;
1538            # can get more specific as needed later.
1539            logging.warning('Got error in v2 sign-in result: %s', result)
1540            bui.screenmessage(
1541                bui.Lstr(resource='internal.signInNoConnectionText'),
1542                color=(1, 0, 0),
1543            )
1544            bui.getsound('error').play()
1545        else:
1546            # Success! Plug in these credentials which will begin
1547            # verifying them and set our primary account-handle when
1548            # finished.
1549            plus = bui.app.plus
1550            assert plus is not None
1551            plus.accounts.set_primary_credentials(result.credentials)
1552
1553            # Special case - if the user has explicitly signed out and
1554            # signed in again with GPGS via this button, warn them that
1555            # they need to use the app if they want to switch to a
1556            # different GPGS account.
1557            if (
1558                self._explicitly_signed_out_of_gpgs
1559                and adapter.login_type is LoginType.GPGS
1560            ):
1561                # Delay this slightly so it hopefully pops up after
1562                # credentials go through and the account name shows up.
1563                bui.apptimer(
1564                    1.5,
1565                    bui.Call(
1566                        bui.screenmessage,
1567                        bui.Lstr(
1568                            resource=self._r
1569                            + '.googlePlayGamesAccountSwitchText'
1570                        ),
1571                    ),
1572                )
1573
1574        # Speed any UI updates along.
1575        self._needs_refresh = True
1576        bui.apptimer(0.1, bui.WeakCall(self._update))
1577
1578    def _v2_proxy_sign_in_press(self) -> None:
1579        # pylint: disable=cyclic-import
1580        from bauiv1lib.connectivity import wait_for_connectivity
1581
1582        # If we're still waiting for our master-server connection, keep
1583        # the user informed of this instead of rushing in and failing
1584        # immediately.
1585        wait_for_connectivity(on_connected=self._v2_proxy_sign_in)
1586
1587    def _v2_proxy_sign_in(self) -> None:
1588        # pylint: disable=cyclic-import
1589        from bauiv1lib.account.v2proxy import V2ProxySignInWindow
1590
1591        assert self._sign_in_v2_proxy_button is not None
1592        V2ProxySignInWindow(origin_widget=self._sign_in_v2_proxy_button)
1593
1594    def _save_state(self) -> None:
1595        try:
1596            sel = self._root_widget.get_selected_child()
1597            if sel == self._back_button:
1598                sel_name = 'Back'
1599            elif sel == self._scrollwidget:
1600                sel_name = 'Scroll'
1601            else:
1602                raise ValueError('unrecognized selection')
1603            assert bui.app.classic is not None
1604            bui.app.ui_v1.window_states[type(self)] = sel_name
1605        except Exception:
1606            logging.exception('Error saving state for %s.', self)
1607
1608    def _restore_state(self) -> None:
1609        try:
1610            assert bui.app.classic is not None
1611            sel_name = bui.app.ui_v1.window_states.get(type(self))
1612            if sel_name == 'Back':
1613                sel = self._back_button
1614            elif sel_name == 'Scroll':
1615                sel = self._scrollwidget
1616            else:
1617                sel = self._back_button
1618            bui.containerwidget(edit=self._root_widget, selected_child=sel)
1619        except Exception:
1620            logging.exception('Error restoring state for %s.', self)

Window for account related functionality.

AccountSettingsWindow( transition: str | None = 'in_right', modal: bool = False, origin_widget: _bauiv1.Widget | None = None, close_once_signed_in: bool = False)
 28    def __init__(
 29        self,
 30        transition: str | None = 'in_right',
 31        modal: bool = False,
 32        origin_widget: bui.Widget | None = None,
 33        close_once_signed_in: bool = False,
 34    ):
 35        # pylint: disable=too-many-statements
 36
 37        plus = bui.app.plus
 38        assert plus is not None
 39
 40        self._sign_in_v2_proxy_button: bui.Widget | None = None
 41        self._sign_in_device_button: bui.Widget | None = None
 42
 43        self._show_legacy_unlink_button = False
 44
 45        self._signing_in_adapter: bui.LoginAdapter | None = None
 46        self._close_once_signed_in = close_once_signed_in
 47        bui.set_analytics_screen('Account Window')
 48
 49        self._explicitly_signed_out_of_gpgs = False
 50
 51        self._r = 'accountSettingsWindow'
 52        self._modal = modal
 53        self._needs_refresh = False
 54        self._v1_signed_in = plus.get_v1_account_state() == 'signed_in'
 55        self._v1_account_state_num = plus.get_v1_account_state_num()
 56        self._check_sign_in_timer = bui.AppTimer(
 57            1.0, bui.WeakCall(self._update), repeat=True
 58        )
 59
 60        self._can_reset_achievements = False
 61
 62        app = bui.app
 63        assert app.classic is not None
 64        uiscale = app.ui_v1.uiscale
 65
 66        self._width = 850 if uiscale is bui.UIScale.SMALL else 660
 67        x_offs = 70 if uiscale is bui.UIScale.SMALL else 0
 68        self._height = (
 69            380
 70            if uiscale is bui.UIScale.SMALL
 71            else 430 if uiscale is bui.UIScale.MEDIUM else 490
 72        )
 73
 74        self._sign_in_button = None
 75        self._sign_in_text = None
 76
 77        self._scroll_width = self._width - (100 + x_offs * 2)
 78        self._scroll_height = self._height - 120
 79        self._sub_width = self._scroll_width - 20
 80
 81        # Determine which sign-in/sign-out buttons we should show.
 82        self._show_sign_in_buttons: list[str] = []
 83
 84        if LoginType.GPGS in plus.accounts.login_adapters:
 85            self._show_sign_in_buttons.append('Google Play')
 86
 87        if LoginType.GAME_CENTER in plus.accounts.login_adapters:
 88            self._show_sign_in_buttons.append('Game Center')
 89
 90        # Always want to show our web-based v2 login option.
 91        self._show_sign_in_buttons.append('V2Proxy')
 92
 93        # Legacy v1 device accounts available only if the user
 94        # has explicitly enabled deprecated login types.
 95        if bui.app.config.resolve('Show Deprecated Login Types'):
 96            self._show_sign_in_buttons.append('Device')
 97
 98        top_extra = 15 if uiscale is bui.UIScale.SMALL else 0
 99        super().__init__(
100            root_widget=bui.containerwidget(
101                size=(self._width, self._height + top_extra),
102                # transition=transition,
103                toolbar_visibility=(
104                    'menu_minimal'
105                    if uiscale is bui.UIScale.SMALL
106                    else 'menu_full'
107                ),
108                scale=(
109                    2.07
110                    if uiscale is bui.UIScale.SMALL
111                    else 1.4 if uiscale is bui.UIScale.MEDIUM else 1.0
112                ),
113                stack_offset=(
114                    (0, 8) if uiscale is bui.UIScale.SMALL else (0, 0)
115                ),
116            ),
117            transition=transition,
118            origin_widget=origin_widget,
119        )
120        if uiscale is bui.UIScale.SMALL:
121            self._back_button = None
122            bui.containerwidget(
123                edit=self._root_widget, on_cancel_call=self.main_window_back
124            )
125        else:
126            self._back_button = btn = bui.buttonwidget(
127                parent=self._root_widget,
128                position=(51 + x_offs, self._height - 62),
129                size=(120, 60),
130                scale=0.8,
131                text_scale=1.2,
132                autoselect=True,
133                label=bui.Lstr(
134                    resource='doneText' if self._modal else 'backText'
135                ),
136                button_type='regular' if self._modal else 'back',
137                on_activate_call=self.main_window_back,
138            )
139            bui.containerwidget(edit=self._root_widget, cancel_button=btn)
140            if not self._modal:
141                bui.buttonwidget(
142                    edit=btn,
143                    button_type='backSmall',
144                    size=(60, 56),
145                    label=bui.charstr(bui.SpecialChar.BACK),
146                )
147
148        titleyoffs = -12 if uiscale is bui.UIScale.SMALL else 0
149        titlescale = 0.6 if uiscale is bui.UIScale.SMALL else 1.0
150        bui.textwidget(
151            parent=self._root_widget,
152            position=(self._width * 0.5, self._height - 41 + titleyoffs),
153            size=(0, 0),
154            text=bui.Lstr(resource=f'{self._r}.titleText'),
155            color=app.ui_v1.title_color,
156            scale=titlescale,
157            maxwidth=self._width - 340,
158            h_align='center',
159            v_align='center',
160        )
161
162        self._scrollwidget = bui.scrollwidget(
163            parent=self._root_widget,
164            highlight=False,
165            position=(
166                (self._width - self._scroll_width) * 0.5,
167                self._height - 65 - self._scroll_height,
168            ),
169            size=(self._scroll_width, self._scroll_height),
170            claims_left_right=True,
171            claims_tab=True,
172            selection_loops_to_parent=True,
173        )
174        self._subcontainer: bui.Widget | None = None
175        self._refresh()
176        self._restore_state()

Create a MainWindow given a root widget and transition info.

Automatically handles in and out transitions on the provided widget, so there is no need to set transitions when creating it.

@override
def get_main_window_state(self) -> bauiv1.MainWindowState:
178    @override
179    def get_main_window_state(self) -> bui.MainWindowState:
180        # Support recreating our window for back/refresh purposes.
181        cls = type(self)
182        return bui.BasicMainWindowState(
183            create_call=lambda transition, origin_widget: cls(
184                transition=transition, origin_widget=origin_widget
185            )
186        )

Return a WindowState to recreate this window, if supported.

@override
def on_main_window_close(self) -> None:
188    @override
189    def on_main_window_close(self) -> None:
190        self._save_state()

Called before transitioning out a main window.

A good opportunity to save window state/etc.

Inherited Members
bauiv1._uitypes.MainWindow
main_window_back_state
main_window_is_top_level
main_window_close
main_window_has_control
main_window_back
main_window_replace
bauiv1._uitypes.Window
get_root_widget
def show_what_is_legacy_unlinking_page() -> None:
1623def show_what_is_legacy_unlinking_page() -> None:
1624    """Show the webpage describing legacy unlinking."""
1625    plus = bui.app.plus
1626    assert plus is not None
1627
1628    bamasteraddr = plus.get_master_server_address(version=2)
1629    bui.open_url(f'{bamasteraddr}/whatarev1links')

Show the webpage describing legacy unlinking.