bastd.ui.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
   9from typing import TYPE_CHECKING
  10
  11import _ba
  12import ba
  13
  14if TYPE_CHECKING:
  15    pass
  16
  17
  18class AccountSettingsWindow(ba.Window):
  19    """Window for account related functionality."""
  20
  21    def __init__(self,
  22                 transition: str = 'in_right',
  23                 modal: bool = False,
  24                 origin_widget: ba.Widget | None = None,
  25                 close_once_signed_in: bool = False):
  26        # pylint: disable=too-many-statements
  27
  28        self._sign_in_game_circle_button: ba.Widget | None = None
  29        self._sign_in_v2_button: ba.Widget | None = None
  30        self._sign_in_device_button: ba.Widget | None = None
  31
  32        self._close_once_signed_in = close_once_signed_in
  33        ba.set_analytics_screen('Account Window')
  34
  35        # If they provided an origin-widget, scale up from that.
  36        scale_origin: tuple[float, float] | None
  37        if origin_widget is not None:
  38            self._transition_out = 'out_scale'
  39            scale_origin = origin_widget.get_screen_space_center()
  40            transition = 'in_scale'
  41        else:
  42            self._transition_out = 'out_right'
  43            scale_origin = None
  44
  45        self._r = 'accountSettingsWindow'
  46        self._modal = modal
  47        self._needs_refresh = False
  48        self._signed_in = (_ba.get_v1_account_state() == 'signed_in')
  49        self._account_state_num = _ba.get_v1_account_state_num()
  50        self._show_linked = (self._signed_in
  51                             and _ba.get_v1_account_misc_read_val(
  52                                 'allowAccountLinking2', False))
  53        self._check_sign_in_timer = ba.Timer(1.0,
  54                                             ba.WeakCall(self._update),
  55                                             timetype=ba.TimeType.REAL,
  56                                             repeat=True)
  57
  58        # Currently we can only reset achievements on game-center.
  59        account_type: str | None
  60        if self._signed_in:
  61            account_type = _ba.get_v1_account_type()
  62        else:
  63            account_type = None
  64        self._can_reset_achievements = (account_type == 'Game Center')
  65
  66        app = ba.app
  67        uiscale = app.ui.uiscale
  68
  69        self._width = 760 if uiscale is ba.UIScale.SMALL else 660
  70        x_offs = 50 if uiscale is ba.UIScale.SMALL else 0
  71        self._height = (390 if uiscale is ba.UIScale.SMALL else
  72                        430 if uiscale is ba.UIScale.MEDIUM else 490)
  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 app.platform == 'android' and app.subplatform == 'google':
  85            self._show_sign_in_buttons.append('Google Play')
  86
  87        elif app.platform == 'android' and app.subplatform == 'amazon':
  88            self._show_sign_in_buttons.append('Game Circle')
  89
  90        # Local accounts are generally always available with a few key
  91        # exceptions.
  92        self._show_sign_in_buttons.append('Local')
  93
  94        # Ditto with shiny new V2 ones.
  95        if bool(True):
  96            self._show_sign_in_buttons.append('V2')
  97
  98        top_extra = 15 if uiscale is ba.UIScale.SMALL else 0
  99        super().__init__(root_widget=ba.containerwidget(
 100            size=(self._width, self._height + top_extra),
 101            transition=transition,
 102            toolbar_visibility='menu_minimal',
 103            scale_origin_stack_offset=scale_origin,
 104            scale=(2.09 if uiscale is ba.UIScale.SMALL else
 105                   1.4 if uiscale is ba.UIScale.MEDIUM else 1.0),
 106            stack_offset=(0, -19) if uiscale is ba.UIScale.SMALL else (0, 0)))
 107        if uiscale is ba.UIScale.SMALL and ba.app.ui.use_toolbars:
 108            self._back_button = None
 109            ba.containerwidget(edit=self._root_widget,
 110                               on_cancel_call=self._back)
 111        else:
 112            self._back_button = btn = ba.buttonwidget(
 113                parent=self._root_widget,
 114                position=(51 + x_offs, self._height - 62),
 115                size=(120, 60),
 116                scale=0.8,
 117                text_scale=1.2,
 118                autoselect=True,
 119                label=ba.Lstr(
 120                    resource='doneText' if self._modal else 'backText'),
 121                button_type='regular' if self._modal else 'back',
 122                on_activate_call=self._back)
 123            ba.containerwidget(edit=self._root_widget, cancel_button=btn)
 124            if not self._modal:
 125                ba.buttonwidget(edit=btn,
 126                                button_type='backSmall',
 127                                size=(60, 56),
 128                                label=ba.charstr(ba.SpecialChar.BACK))
 129
 130        ba.textwidget(parent=self._root_widget,
 131                      position=(self._width * 0.5, self._height - 41),
 132                      size=(0, 0),
 133                      text=ba.Lstr(resource=self._r + '.titleText'),
 134                      color=ba.app.ui.title_color,
 135                      maxwidth=self._width - 340,
 136                      h_align='center',
 137                      v_align='center')
 138
 139        self._scrollwidget = ba.scrollwidget(
 140            parent=self._root_widget,
 141            highlight=False,
 142            position=((self._width - self._scroll_width) * 0.5,
 143                      self._height - 65 - self._scroll_height),
 144            size=(self._scroll_width, self._scroll_height),
 145            claims_left_right=True,
 146            claims_tab=True,
 147            selection_loops_to_parent=True)
 148        self._subcontainer: ba.Widget | None = None
 149        self._refresh()
 150        self._restore_state()
 151
 152    def _update(self) -> None:
 153
 154        # If they want us to close once we're signed in, do so.
 155        if self._close_once_signed_in and self._signed_in:
 156            self._back()
 157            return
 158
 159        # Hmm should update this to use get_account_state_num.
 160        # Theoretically if we switch from one signed-in account to another
 161        # in the background this would break.
 162        account_state_num = _ba.get_v1_account_state_num()
 163        account_state = _ba.get_v1_account_state()
 164
 165        show_linked = (self._signed_in and _ba.get_v1_account_misc_read_val(
 166            'allowAccountLinking2', False))
 167
 168        if (account_state_num != self._account_state_num
 169                or self._show_linked != show_linked or self._needs_refresh):
 170            self._show_linked = show_linked
 171            self._account_state_num = account_state_num
 172            self._signed_in = (account_state == 'signed_in')
 173            self._refresh()
 174
 175        # Go ahead and refresh some individual things
 176        # that may change under us.
 177        self._update_linked_accounts_text()
 178        self._update_unlink_accounts_button()
 179        self._refresh_campaign_progress_text()
 180        self._refresh_achievements()
 181        self._refresh_tickets_text()
 182        self._refresh_account_name_text()
 183
 184    def _get_sign_in_text(self) -> ba.Lstr:
 185        return ba.Lstr(resource=self._r + '.signInText')
 186
 187    def _refresh(self) -> None:
 188        # pylint: disable=too-many-statements
 189        # pylint: disable=too-many-branches
 190        # pylint: disable=too-many-locals
 191        # pylint: disable=cyclic-import
 192        from bastd.ui import confirm
 193
 194        account_state = _ba.get_v1_account_state()
 195        account_type = (_ba.get_v1_account_type()
 196                        if account_state == 'signed_in' else 'unknown')
 197
 198        is_google = account_type == 'Google Play'
 199
 200        show_local_signed_in_as = False
 201        local_signed_in_as_space = 50.0
 202
 203        show_signed_in_as = self._signed_in
 204        signed_in_as_space = 95.0
 205
 206        show_sign_in_benefits = not self._signed_in
 207        sign_in_benefits_space = 80.0
 208
 209        show_signing_in_text = account_state == 'signing_in'
 210        signing_in_text_space = 80.0
 211
 212        show_google_play_sign_in_button = (account_state == 'signed_out'
 213                                           and 'Google Play'
 214                                           in self._show_sign_in_buttons)
 215        show_game_circle_sign_in_button = (account_state == 'signed_out'
 216                                           and 'Game Circle'
 217                                           in self._show_sign_in_buttons)
 218        show_device_sign_in_button = (account_state == 'signed_out' and 'Local'
 219                                      in self._show_sign_in_buttons)
 220        show_v2_sign_in_button = (account_state == 'signed_out'
 221                                  and 'V2' in self._show_sign_in_buttons)
 222        sign_in_button_space = 70.0
 223
 224        show_game_service_button = (self._signed_in and account_type
 225                                    in ['Game Center', 'Game Circle'])
 226        game_service_button_space = 60.0
 227
 228        show_linked_accounts_text = (self._signed_in
 229                                     and _ba.get_v1_account_misc_read_val(
 230                                         'allowAccountLinking2', False))
 231        linked_accounts_text_space = 60.0
 232
 233        show_achievements_button = (
 234            self._signed_in
 235            and account_type in ('Google Play', 'Alibaba', 'Local', 'OUYA'))
 236        achievements_button_space = 60.0
 237
 238        show_achievements_text = (self._signed_in
 239                                  and not show_achievements_button)
 240        achievements_text_space = 27.0
 241
 242        show_leaderboards_button = (self._signed_in and is_google)
 243        leaderboards_button_space = 60.0
 244
 245        show_campaign_progress = self._signed_in
 246        campaign_progress_space = 27.0
 247
 248        show_tickets = self._signed_in
 249        tickets_space = 27.0
 250
 251        show_reset_progress_button = False
 252        reset_progress_button_space = 70.0
 253
 254        show_player_profiles_button = self._signed_in
 255        player_profiles_button_space = 100.0
 256
 257        show_link_accounts_button = (self._signed_in
 258                                     and _ba.get_v1_account_misc_read_val(
 259                                         'allowAccountLinking2', False))
 260        link_accounts_button_space = 70.0
 261
 262        show_unlink_accounts_button = show_link_accounts_button
 263        unlink_accounts_button_space = 90.0
 264
 265        show_sign_out_button = (self._signed_in and account_type
 266                                in ['Local', 'Google Play', 'V2'])
 267        sign_out_button_space = 70.0
 268
 269        show_cancel_v2_sign_in_button = (
 270            account_state == 'signing_in'
 271            and ba.app.accounts_v2.have_primary_credentials())
 272        cancel_v2_sign_in_button_space = 70.0
 273
 274        if self._subcontainer is not None:
 275            self._subcontainer.delete()
 276        self._sub_height = 60.0
 277        if show_local_signed_in_as:
 278            self._sub_height += local_signed_in_as_space
 279        if show_signed_in_as:
 280            self._sub_height += signed_in_as_space
 281        if show_signing_in_text:
 282            self._sub_height += signing_in_text_space
 283        if show_google_play_sign_in_button:
 284            self._sub_height += sign_in_button_space
 285        if show_game_circle_sign_in_button:
 286            self._sub_height += sign_in_button_space
 287        if show_device_sign_in_button:
 288            self._sub_height += sign_in_button_space
 289        if show_v2_sign_in_button:
 290            self._sub_height += sign_in_button_space
 291        if show_game_service_button:
 292            self._sub_height += game_service_button_space
 293        if show_linked_accounts_text:
 294            self._sub_height += linked_accounts_text_space
 295        if show_achievements_text:
 296            self._sub_height += achievements_text_space
 297        if show_achievements_button:
 298            self._sub_height += achievements_button_space
 299        if show_leaderboards_button:
 300            self._sub_height += leaderboards_button_space
 301        if show_campaign_progress:
 302            self._sub_height += campaign_progress_space
 303        if show_tickets:
 304            self._sub_height += tickets_space
 305        if show_sign_in_benefits:
 306            self._sub_height += sign_in_benefits_space
 307        if show_reset_progress_button:
 308            self._sub_height += reset_progress_button_space
 309        if show_player_profiles_button:
 310            self._sub_height += player_profiles_button_space
 311        if show_link_accounts_button:
 312            self._sub_height += link_accounts_button_space
 313        if show_unlink_accounts_button:
 314            self._sub_height += unlink_accounts_button_space
 315        if show_sign_out_button:
 316            self._sub_height += sign_out_button_space
 317        if show_cancel_v2_sign_in_button:
 318            self._sub_height += cancel_v2_sign_in_button_space
 319        self._subcontainer = ba.containerwidget(parent=self._scrollwidget,
 320                                                size=(self._sub_width,
 321                                                      self._sub_height),
 322                                                background=False,
 323                                                claims_left_right=True,
 324                                                claims_tab=True,
 325                                                selection_loops_to_parent=True)
 326
 327        first_selectable = None
 328        v = self._sub_height - 10.0
 329
 330        if show_local_signed_in_as:
 331            v -= local_signed_in_as_space * 0.6
 332            ba.textwidget(
 333                parent=self._subcontainer,
 334                position=(self._sub_width * 0.5, v),
 335                size=(0, 0),
 336                text=ba.Lstr(
 337                    resource='accountSettingsWindow.deviceSpecificAccountText',
 338                    subs=[('${NAME}', _ba.get_v1_account_display_string())]),
 339                scale=0.7,
 340                color=(0.5, 0.5, 0.6),
 341                maxwidth=self._sub_width * 0.9,
 342                flatness=1.0,
 343                h_align='center',
 344                v_align='center')
 345            v -= local_signed_in_as_space * 0.4
 346
 347        self._account_name_text: ba.Widget | None
 348        if show_signed_in_as:
 349            v -= signed_in_as_space * 0.2
 350            txt = ba.Lstr(
 351                resource='accountSettingsWindow.youAreSignedInAsText',
 352                fallback_resource='accountSettingsWindow.youAreLoggedInAsText')
 353            ba.textwidget(parent=self._subcontainer,
 354                          position=(self._sub_width * 0.5, v),
 355                          size=(0, 0),
 356                          text=txt,
 357                          scale=0.9,
 358                          color=ba.app.ui.title_color,
 359                          maxwidth=self._sub_width * 0.9,
 360                          h_align='center',
 361                          v_align='center')
 362            v -= signed_in_as_space * 0.4
 363            self._account_name_text = ba.textwidget(
 364                parent=self._subcontainer,
 365                position=(self._sub_width * 0.5, v),
 366                size=(0, 0),
 367                scale=1.5,
 368                maxwidth=self._sub_width * 0.9,
 369                res_scale=1.5,
 370                color=(1, 1, 1, 1),
 371                h_align='center',
 372                v_align='center')
 373            self._refresh_account_name_text()
 374            v -= signed_in_as_space * 0.4
 375        else:
 376            self._account_name_text = None
 377
 378        if self._back_button is None:
 379            bbtn = _ba.get_special_widget('back_button')
 380        else:
 381            bbtn = self._back_button
 382
 383        if show_sign_in_benefits:
 384            v -= sign_in_benefits_space
 385            app = ba.app
 386            extra: str | ba.Lstr | None
 387            if (app.platform in ['mac', 'ios']
 388                    and app.subplatform == 'appstore'):
 389                extra = ba.Lstr(
 390                    value='\n${S}',
 391                    subs=[('${S}',
 392                           ba.Lstr(resource='signInWithGameCenterText'))])
 393            else:
 394                extra = ''
 395
 396            ba.textwidget(parent=self._subcontainer,
 397                          position=(self._sub_width * 0.5,
 398                                    v + sign_in_benefits_space * 0.4),
 399                          size=(0, 0),
 400                          text=ba.Lstr(value='${A}${B}',
 401                                       subs=[('${A}',
 402                                              ba.Lstr(resource=self._r +
 403                                                      '.signInInfoText')),
 404                                             ('${B}', extra)]),
 405                          max_height=sign_in_benefits_space * 0.9,
 406                          scale=0.9,
 407                          color=(0.75, 0.7, 0.8),
 408                          maxwidth=self._sub_width * 0.8,
 409                          h_align='center',
 410                          v_align='center')
 411
 412        if show_signing_in_text:
 413            v -= signing_in_text_space
 414
 415            ba.textwidget(
 416                parent=self._subcontainer,
 417                position=(self._sub_width * 0.5,
 418                          v + signing_in_text_space * 0.5),
 419                size=(0, 0),
 420                text=ba.Lstr(resource='accountSettingsWindow.signingInText'),
 421                scale=0.9,
 422                color=(0, 1, 0),
 423                maxwidth=self._sub_width * 0.8,
 424                h_align='center',
 425                v_align='center')
 426
 427        if show_google_play_sign_in_button:
 428            button_width = 350
 429            v -= sign_in_button_space
 430            self._sign_in_google_play_button = btn = ba.buttonwidget(
 431                parent=self._subcontainer,
 432                position=((self._sub_width - button_width) * 0.5, v - 20),
 433                autoselect=True,
 434                size=(button_width, 60),
 435                label=ba.Lstr(
 436                    value='${A}${B}',
 437                    subs=[('${A}',
 438                           ba.charstr(ba.SpecialChar.GOOGLE_PLAY_GAMES_LOGO)),
 439                          ('${B}',
 440                           ba.Lstr(resource=self._r +
 441                                   '.signInWithGooglePlayText'))]),
 442                on_activate_call=lambda: self._sign_in_press('Google Play'))
 443            if first_selectable is None:
 444                first_selectable = btn
 445            if ba.app.ui.use_toolbars:
 446                ba.widget(edit=btn,
 447                          right_widget=_ba.get_special_widget('party_button'))
 448            ba.widget(edit=btn, left_widget=bbtn)
 449            ba.widget(edit=btn, show_buffer_bottom=40, show_buffer_top=100)
 450            self._sign_in_text = None
 451
 452        if show_game_circle_sign_in_button:
 453            button_width = 350
 454            v -= sign_in_button_space
 455            self._sign_in_game_circle_button = btn = ba.buttonwidget(
 456                parent=self._subcontainer,
 457                position=((self._sub_width - button_width) * 0.5, v - 20),
 458                autoselect=True,
 459                size=(button_width, 60),
 460                label=ba.Lstr(value='${A}${B}',
 461                              subs=[('${A}',
 462                                     ba.charstr(
 463                                         ba.SpecialChar.GAME_CIRCLE_LOGO)),
 464                                    ('${B}',
 465                                     ba.Lstr(resource=self._r +
 466                                             '.signInWithGameCircleText'))]),
 467                on_activate_call=lambda: self._sign_in_press('Game Circle'))
 468            if first_selectable is None:
 469                first_selectable = btn
 470            if ba.app.ui.use_toolbars:
 471                ba.widget(edit=btn,
 472                          right_widget=_ba.get_special_widget('party_button'))
 473            ba.widget(edit=btn, left_widget=bbtn)
 474            ba.widget(edit=btn, show_buffer_bottom=40, show_buffer_top=100)
 475            self._sign_in_text = None
 476
 477        if show_v2_sign_in_button:
 478            button_width = 350
 479            v -= sign_in_button_space
 480            self._sign_in_v2_button = btn = ba.buttonwidget(
 481                parent=self._subcontainer,
 482                position=((self._sub_width - button_width) * 0.5, v - 20),
 483                autoselect=True,
 484                size=(button_width, 60),
 485                label='',
 486                on_activate_call=self._v2_sign_in_press)
 487            ba.textwidget(
 488                parent=self._subcontainer,
 489                draw_controller=btn,
 490                h_align='center',
 491                v_align='center',
 492                size=(0, 0),
 493                position=(self._sub_width * 0.5, v + 17),
 494                text=ba.Lstr(
 495                    value='${A}${B}',
 496                    subs=[('${A}', ba.charstr(ba.SpecialChar.V2_LOGO)),
 497                          ('${B}',
 498                           ba.Lstr(resource=self._r + '.signInWithV2Text'))]),
 499                maxwidth=button_width * 0.8,
 500                color=(0.75, 1.0, 0.7))
 501            ba.textwidget(parent=self._subcontainer,
 502                          draw_controller=btn,
 503                          h_align='center',
 504                          v_align='center',
 505                          size=(0, 0),
 506                          position=(self._sub_width * 0.5, v - 4),
 507                          text=ba.Lstr(resource=self._r +
 508                                       '.signInWithV2InfoText'),
 509                          flatness=1.0,
 510                          scale=0.57,
 511                          maxwidth=button_width * 0.9,
 512                          color=(0.55, 0.8, 0.5))
 513            if first_selectable is None:
 514                first_selectable = btn
 515            if ba.app.ui.use_toolbars:
 516                ba.widget(edit=btn,
 517                          right_widget=_ba.get_special_widget('party_button'))
 518            ba.widget(edit=btn, left_widget=bbtn)
 519            ba.widget(edit=btn, show_buffer_bottom=40, show_buffer_top=100)
 520            self._sign_in_text = None
 521
 522        if show_device_sign_in_button:
 523            button_width = 350
 524            v -= sign_in_button_space
 525            self._sign_in_device_button = btn = ba.buttonwidget(
 526                parent=self._subcontainer,
 527                position=((self._sub_width - button_width) * 0.5, v - 20),
 528                autoselect=True,
 529                size=(button_width, 60),
 530                label='',
 531                on_activate_call=lambda: self._sign_in_press('Local'))
 532            ba.textwidget(parent=self._subcontainer,
 533                          draw_controller=btn,
 534                          h_align='center',
 535                          v_align='center',
 536                          size=(0, 0),
 537                          position=(self._sub_width * 0.5, v + 17),
 538                          text=ba.Lstr(
 539                              value='${A}${B}',
 540                              subs=[('${A}',
 541                                     ba.charstr(ba.SpecialChar.LOCAL_ACCOUNT)),
 542                                    ('${B}',
 543                                     ba.Lstr(resource=self._r +
 544                                             '.signInWithDeviceText'))]),
 545                          maxwidth=button_width * 0.8,
 546                          color=(0.75, 1.0, 0.7))
 547            ba.textwidget(parent=self._subcontainer,
 548                          draw_controller=btn,
 549                          h_align='center',
 550                          v_align='center',
 551                          size=(0, 0),
 552                          position=(self._sub_width * 0.5, v - 4),
 553                          text=ba.Lstr(resource=self._r +
 554                                       '.signInWithDeviceInfoText'),
 555                          flatness=1.0,
 556                          scale=0.57,
 557                          maxwidth=button_width * 0.9,
 558                          color=(0.55, 0.8, 0.5))
 559            if first_selectable is None:
 560                first_selectable = btn
 561            if ba.app.ui.use_toolbars:
 562                ba.widget(edit=btn,
 563                          right_widget=_ba.get_special_widget('party_button'))
 564            ba.widget(edit=btn, left_widget=bbtn)
 565            ba.widget(edit=btn, show_buffer_bottom=40, show_buffer_top=100)
 566            self._sign_in_text = None
 567
 568        if show_player_profiles_button:
 569            button_width = 300
 570            v -= player_profiles_button_space
 571            self._player_profiles_button = btn = ba.buttonwidget(
 572                parent=self._subcontainer,
 573                position=((self._sub_width - button_width) * 0.5, v + 30),
 574                autoselect=True,
 575                size=(button_width, 60),
 576                label=ba.Lstr(resource='playerProfilesWindow.titleText'),
 577                color=(0.55, 0.5, 0.6),
 578                icon=ba.gettexture('cuteSpaz'),
 579                textcolor=(0.75, 0.7, 0.8),
 580                on_activate_call=self._player_profiles_press)
 581            if first_selectable is None:
 582                first_selectable = btn
 583            if ba.app.ui.use_toolbars:
 584                ba.widget(edit=btn,
 585                          right_widget=_ba.get_special_widget('party_button'))
 586            ba.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=0)
 587
 588        # the button to go to OS-Specific leaderboards/high-score-lists/etc.
 589        if show_game_service_button:
 590            button_width = 300
 591            v -= game_service_button_space * 0.85
 592            account_type = _ba.get_v1_account_type()
 593            if account_type == 'Game Center':
 594                account_type_name = ba.Lstr(resource='gameCenterText')
 595            elif account_type == 'Game Circle':
 596                account_type_name = ba.Lstr(resource='gameCircleText')
 597            else:
 598                raise ValueError("unknown account type: '" +
 599                                 str(account_type) + "'")
 600            self._game_service_button = btn = ba.buttonwidget(
 601                parent=self._subcontainer,
 602                position=((self._sub_width - button_width) * 0.5, v),
 603                color=(0.55, 0.5, 0.6),
 604                textcolor=(0.75, 0.7, 0.8),
 605                autoselect=True,
 606                on_activate_call=_ba.show_online_score_ui,
 607                size=(button_width, 50),
 608                label=account_type_name)
 609            if first_selectable is None:
 610                first_selectable = btn
 611            if ba.app.ui.use_toolbars:
 612                ba.widget(edit=btn,
 613                          right_widget=_ba.get_special_widget('party_button'))
 614            ba.widget(edit=btn, left_widget=bbtn)
 615            v -= game_service_button_space * 0.15
 616        else:
 617            self.game_service_button = None
 618
 619        self._achievements_text: ba.Widget | None
 620        if show_achievements_text:
 621            v -= achievements_text_space * 0.5
 622            self._achievements_text = ba.textwidget(
 623                parent=self._subcontainer,
 624                position=(self._sub_width * 0.5, v),
 625                size=(0, 0),
 626                scale=0.9,
 627                color=(0.75, 0.7, 0.8),
 628                maxwidth=self._sub_width * 0.8,
 629                h_align='center',
 630                v_align='center')
 631            v -= achievements_text_space * 0.5
 632        else:
 633            self._achievements_text = None
 634
 635        self._achievements_button: ba.Widget | None
 636        if show_achievements_button:
 637            button_width = 300
 638            v -= achievements_button_space * 0.85
 639            self._achievements_button = btn = ba.buttonwidget(
 640                parent=self._subcontainer,
 641                position=((self._sub_width - button_width) * 0.5, v),
 642                color=(0.55, 0.5, 0.6),
 643                textcolor=(0.75, 0.7, 0.8),
 644                autoselect=True,
 645                icon=ba.gettexture('googlePlayAchievementsIcon'
 646                                   if is_google else 'achievementsIcon'),
 647                icon_color=(0.8, 0.95, 0.7) if is_google else (0.85, 0.8, 0.9),
 648                on_activate_call=self._on_achievements_press,
 649                size=(button_width, 50),
 650                label='')
 651            if first_selectable is None:
 652                first_selectable = btn
 653            if ba.app.ui.use_toolbars:
 654                ba.widget(edit=btn,
 655                          right_widget=_ba.get_special_widget('party_button'))
 656            ba.widget(edit=btn, left_widget=bbtn)
 657            v -= achievements_button_space * 0.15
 658        else:
 659            self._achievements_button = None
 660
 661        if show_achievements_text or show_achievements_button:
 662            self._refresh_achievements()
 663
 664        self._leaderboards_button: ba.Widget | None
 665        if show_leaderboards_button:
 666            button_width = 300
 667            v -= leaderboards_button_space * 0.85
 668            self._leaderboards_button = btn = ba.buttonwidget(
 669                parent=self._subcontainer,
 670                position=((self._sub_width - button_width) * 0.5, v),
 671                color=(0.55, 0.5, 0.6),
 672                textcolor=(0.75, 0.7, 0.8),
 673                autoselect=True,
 674                icon=ba.gettexture('googlePlayLeaderboardsIcon'),
 675                icon_color=(0.8, 0.95, 0.7),
 676                on_activate_call=self._on_leaderboards_press,
 677                size=(button_width, 50),
 678                label=ba.Lstr(resource='leaderboardsText'))
 679            if first_selectable is None:
 680                first_selectable = btn
 681            if ba.app.ui.use_toolbars:
 682                ba.widget(edit=btn,
 683                          right_widget=_ba.get_special_widget('party_button'))
 684            ba.widget(edit=btn, left_widget=bbtn)
 685            v -= leaderboards_button_space * 0.15
 686        else:
 687            self._leaderboards_button = None
 688
 689        self._campaign_progress_text: ba.Widget | None
 690        if show_campaign_progress:
 691            v -= campaign_progress_space * 0.5
 692            self._campaign_progress_text = ba.textwidget(
 693                parent=self._subcontainer,
 694                position=(self._sub_width * 0.5, v),
 695                size=(0, 0),
 696                scale=0.9,
 697                color=(0.75, 0.7, 0.8),
 698                maxwidth=self._sub_width * 0.8,
 699                h_align='center',
 700                v_align='center')
 701            v -= campaign_progress_space * 0.5
 702            self._refresh_campaign_progress_text()
 703        else:
 704            self._campaign_progress_text = None
 705
 706        self._tickets_text: ba.Widget | None
 707        if show_tickets:
 708            v -= tickets_space * 0.5
 709            self._tickets_text = ba.textwidget(parent=self._subcontainer,
 710                                               position=(self._sub_width * 0.5,
 711                                                         v),
 712                                               size=(0, 0),
 713                                               scale=0.9,
 714                                               color=(0.75, 0.7, 0.8),
 715                                               maxwidth=self._sub_width * 0.8,
 716                                               flatness=1.0,
 717                                               h_align='center',
 718                                               v_align='center')
 719            v -= tickets_space * 0.5
 720            self._refresh_tickets_text()
 721
 722        else:
 723            self._tickets_text = None
 724
 725        # bit of spacing before the reset/sign-out section
 726        v -= 5
 727
 728        button_width = 250
 729        if show_reset_progress_button:
 730            confirm_text = (ba.Lstr(resource=self._r +
 731                                    '.resetProgressConfirmText')
 732                            if self._can_reset_achievements else ba.Lstr(
 733                                resource=self._r +
 734                                '.resetProgressConfirmNoAchievementsText'))
 735            v -= reset_progress_button_space
 736            self._reset_progress_button = btn = ba.buttonwidget(
 737                parent=self._subcontainer,
 738                position=((self._sub_width - button_width) * 0.5, v),
 739                color=(0.55, 0.5, 0.6),
 740                textcolor=(0.75, 0.7, 0.8),
 741                autoselect=True,
 742                size=(button_width, 60),
 743                label=ba.Lstr(resource=self._r + '.resetProgressText'),
 744                on_activate_call=lambda: confirm.ConfirmWindow(
 745                    text=confirm_text,
 746                    width=500,
 747                    height=200,
 748                    action=self._reset_progress))
 749            if first_selectable is None:
 750                first_selectable = btn
 751            if ba.app.ui.use_toolbars:
 752                ba.widget(edit=btn,
 753                          right_widget=_ba.get_special_widget('party_button'))
 754            ba.widget(edit=btn, left_widget=bbtn)
 755
 756        self._linked_accounts_text: ba.Widget | None
 757        if show_linked_accounts_text:
 758            v -= linked_accounts_text_space * 0.8
 759            self._linked_accounts_text = ba.textwidget(
 760                parent=self._subcontainer,
 761                position=(self._sub_width * 0.5, v),
 762                size=(0, 0),
 763                scale=0.9,
 764                color=(0.75, 0.7, 0.8),
 765                maxwidth=self._sub_width * 0.95,
 766                h_align='center',
 767                v_align='center')
 768            v -= linked_accounts_text_space * 0.2
 769            self._update_linked_accounts_text()
 770        else:
 771            self._linked_accounts_text = None
 772
 773        if show_link_accounts_button:
 774            v -= link_accounts_button_space
 775            self._link_accounts_button = btn = ba.buttonwidget(
 776                parent=self._subcontainer,
 777                position=((self._sub_width - button_width) * 0.5, v),
 778                autoselect=True,
 779                size=(button_width, 60),
 780                label='',
 781                color=(0.55, 0.5, 0.6),
 782                on_activate_call=self._link_accounts_press)
 783            ba.textwidget(parent=self._subcontainer,
 784                          draw_controller=btn,
 785                          h_align='center',
 786                          v_align='center',
 787                          size=(0, 0),
 788                          position=(self._sub_width * 0.5, v + 17 + 20),
 789                          text=ba.Lstr(resource=self._r + '.linkAccountsText'),
 790                          maxwidth=button_width * 0.8,
 791                          color=(0.75, 0.7, 0.8))
 792            ba.textwidget(parent=self._subcontainer,
 793                          draw_controller=btn,
 794                          h_align='center',
 795                          v_align='center',
 796                          size=(0, 0),
 797                          position=(self._sub_width * 0.5, v - 4 + 20),
 798                          text=ba.Lstr(resource=self._r +
 799                                       '.linkAccountsInfoText'),
 800                          flatness=1.0,
 801                          scale=0.5,
 802                          maxwidth=button_width * 0.8,
 803                          color=(0.75, 0.7, 0.8))
 804            if first_selectable is None:
 805                first_selectable = btn
 806            if ba.app.ui.use_toolbars:
 807                ba.widget(edit=btn,
 808                          right_widget=_ba.get_special_widget('party_button'))
 809            ba.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=50)
 810
 811        self._unlink_accounts_button: ba.Widget | None
 812        if show_unlink_accounts_button:
 813            v -= unlink_accounts_button_space
 814            self._unlink_accounts_button = btn = ba.buttonwidget(
 815                parent=self._subcontainer,
 816                position=((self._sub_width - button_width) * 0.5, v + 25),
 817                autoselect=True,
 818                size=(button_width, 60),
 819                label='',
 820                color=(0.55, 0.5, 0.6),
 821                on_activate_call=self._unlink_accounts_press)
 822            self._unlink_accounts_button_label = ba.textwidget(
 823                parent=self._subcontainer,
 824                draw_controller=btn,
 825                h_align='center',
 826                v_align='center',
 827                size=(0, 0),
 828                position=(self._sub_width * 0.5, v + 55),
 829                text=ba.Lstr(resource=self._r + '.unlinkAccountsText'),
 830                maxwidth=button_width * 0.8,
 831                color=(0.75, 0.7, 0.8))
 832            if first_selectable is None:
 833                first_selectable = btn
 834            if ba.app.ui.use_toolbars:
 835                ba.widget(edit=btn,
 836                          right_widget=_ba.get_special_widget('party_button'))
 837            ba.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=50)
 838            self._update_unlink_accounts_button()
 839        else:
 840            self._unlink_accounts_button = None
 841
 842        if show_sign_out_button:
 843            v -= sign_out_button_space
 844            self._sign_out_button = btn = ba.buttonwidget(
 845                parent=self._subcontainer,
 846                position=((self._sub_width - button_width) * 0.5, v),
 847                size=(button_width, 60),
 848                label=ba.Lstr(resource=self._r + '.signOutText'),
 849                color=(0.55, 0.5, 0.6),
 850                textcolor=(0.75, 0.7, 0.8),
 851                autoselect=True,
 852                on_activate_call=self._sign_out_press)
 853            if first_selectable is None:
 854                first_selectable = btn
 855            if ba.app.ui.use_toolbars:
 856                ba.widget(edit=btn,
 857                          right_widget=_ba.get_special_widget('party_button'))
 858            ba.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=15)
 859
 860        if show_cancel_v2_sign_in_button:
 861            v -= cancel_v2_sign_in_button_space
 862            self._cancel_v2_sign_in_button = btn = ba.buttonwidget(
 863                parent=self._subcontainer,
 864                position=((self._sub_width - button_width) * 0.5, v),
 865                size=(button_width, 60),
 866                label=ba.Lstr(resource='cancelText'),
 867                color=(0.55, 0.5, 0.6),
 868                textcolor=(0.75, 0.7, 0.8),
 869                autoselect=True,
 870                on_activate_call=self._cancel_v2_sign_in_press)
 871            if first_selectable is None:
 872                first_selectable = btn
 873            if ba.app.ui.use_toolbars:
 874                ba.widget(edit=btn,
 875                          right_widget=_ba.get_special_widget('party_button'))
 876            ba.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=15)
 877
 878        # Whatever the topmost selectable thing is, we want it to scroll all
 879        # the way up when we select it.
 880        if first_selectable is not None:
 881            ba.widget(edit=first_selectable,
 882                      up_widget=bbtn,
 883                      show_buffer_top=400)
 884            # (this should re-scroll us to the top..)
 885            ba.containerwidget(edit=self._subcontainer,
 886                               visible_child=first_selectable)
 887        self._needs_refresh = False
 888
 889    def _on_achievements_press(self) -> None:
 890        # pylint: disable=cyclic-import
 891        from bastd.ui import achievements
 892        account_state = _ba.get_v1_account_state()
 893        account_type = (_ba.get_v1_account_type()
 894                        if account_state == 'signed_in' else 'unknown')
 895        # for google play we use the built-in UI; otherwise pop up our own
 896        if account_type == 'Google Play':
 897            ba.timer(0.15,
 898                     ba.Call(_ba.show_online_score_ui, 'achievements'),
 899                     timetype=ba.TimeType.REAL)
 900        elif account_type != 'unknown':
 901            assert self._achievements_button is not None
 902            achievements.AchievementsWindow(
 903                position=self._achievements_button.get_screen_space_center())
 904        else:
 905            print('ERROR: unknown account type in on_achievements_press:',
 906                  account_type)
 907
 908    def _on_leaderboards_press(self) -> None:
 909        ba.timer(0.15,
 910                 ba.Call(_ba.show_online_score_ui, 'leaderboards'),
 911                 timetype=ba.TimeType.REAL)
 912
 913    def _have_unlinkable_accounts(self) -> bool:
 914        # if this is not present, we haven't had contact from the server so
 915        # let's not proceed..
 916        if _ba.get_public_login_id() is None:
 917            return False
 918        accounts = _ba.get_v1_account_misc_read_val_2('linkedAccounts', [])
 919        return len(accounts) > 1
 920
 921    def _update_unlink_accounts_button(self) -> None:
 922        if self._unlink_accounts_button is None:
 923            return
 924        if self._have_unlinkable_accounts():
 925            clr = (0.75, 0.7, 0.8, 1.0)
 926        else:
 927            clr = (1.0, 1.0, 1.0, 0.25)
 928        ba.textwidget(edit=self._unlink_accounts_button_label, color=clr)
 929
 930    def _update_linked_accounts_text(self) -> None:
 931        if self._linked_accounts_text is None:
 932            return
 933
 934        # if this is not present, we haven't had contact from the server so
 935        # let's not proceed..
 936        if _ba.get_public_login_id() is None:
 937            num = int(time.time()) % 4
 938            accounts_str = num * '.' + (4 - num) * ' '
 939        else:
 940            accounts = _ba.get_v1_account_misc_read_val_2('linkedAccounts', [])
 941            # our_account = _bs.get_v1_account_display_string()
 942            # accounts = [a for a in accounts if a != our_account]
 943            # accounts_str = u', '.join(accounts) if accounts else
 944            # ba.Lstr(translate=('settingNames', 'None'))
 945            # UPDATE - we now just print the number here; not the actual
 946            # accounts
 947            # (they can see that in the unlink section if they're curious)
 948            accounts_str = str(max(0, len(accounts) - 1))
 949        ba.textwidget(edit=self._linked_accounts_text,
 950                      text=ba.Lstr(value='${L} ${A}',
 951                                   subs=[('${L}',
 952                                          ba.Lstr(resource=self._r +
 953                                                  '.linkedAccountsText')),
 954                                         ('${A}', accounts_str)]))
 955
 956    def _refresh_campaign_progress_text(self) -> None:
 957        from ba.internal import getcampaign
 958        if self._campaign_progress_text is None:
 959            return
 960        p_str: str | ba.Lstr
 961        try:
 962            campaign = getcampaign('Default')
 963            levels = campaign.levels
 964            levels_complete = sum((1 if l.complete else 0) for l in levels)
 965
 966            # Last level cant be completed; hence the -1;
 967            progress = min(1.0, float(levels_complete) / (len(levels) - 1))
 968            p_str = ba.Lstr(resource=self._r + '.campaignProgressText',
 969                            subs=[('${PROGRESS}',
 970                                   str(int(progress * 100.0)) + '%')])
 971        except Exception:
 972            p_str = '?'
 973            ba.print_exception('Error calculating co-op campaign progress.')
 974        ba.textwidget(edit=self._campaign_progress_text, text=p_str)
 975
 976    def _refresh_tickets_text(self) -> None:
 977        if self._tickets_text is None:
 978            return
 979        try:
 980            tc_str = str(_ba.get_v1_account_ticket_count())
 981        except Exception:
 982            ba.print_exception()
 983            tc_str = '-'
 984        ba.textwidget(edit=self._tickets_text,
 985                      text=ba.Lstr(resource=self._r + '.ticketsText',
 986                                   subs=[('${COUNT}', tc_str)]))
 987
 988    def _refresh_account_name_text(self) -> None:
 989        if self._account_name_text is None:
 990            return
 991        try:
 992            name_str = _ba.get_v1_account_display_string()
 993        except Exception:
 994            ba.print_exception()
 995            name_str = '??'
 996        ba.textwidget(edit=self._account_name_text, text=name_str)
 997
 998    def _refresh_achievements(self) -> None:
 999        if (self._achievements_text is None
1000                and self._achievements_button is None):
1001            return
1002        complete = sum(1 if a.complete else 0 for a in ba.app.ach.achievements)
1003        total = len(ba.app.ach.achievements)
1004        txt_final = ba.Lstr(resource=self._r + '.achievementProgressText',
1005                            subs=[('${COUNT}', str(complete)),
1006                                  ('${TOTAL}', str(total))])
1007
1008        if self._achievements_text is not None:
1009            ba.textwidget(edit=self._achievements_text, text=txt_final)
1010        if self._achievements_button is not None:
1011            ba.buttonwidget(edit=self._achievements_button, label=txt_final)
1012
1013    def _link_accounts_press(self) -> None:
1014        # pylint: disable=cyclic-import
1015        from bastd.ui.account import link
1016        link.AccountLinkWindow(origin_widget=self._link_accounts_button)
1017
1018    def _unlink_accounts_press(self) -> None:
1019        # pylint: disable=cyclic-import
1020        from bastd.ui.account import unlink
1021        if not self._have_unlinkable_accounts():
1022            ba.playsound(ba.getsound('error'))
1023            return
1024        unlink.AccountUnlinkWindow(origin_widget=self._unlink_accounts_button)
1025
1026    def _player_profiles_press(self) -> None:
1027        # pylint: disable=cyclic-import
1028        from bastd.ui.profile import browser as pbrowser
1029        self._save_state()
1030        ba.containerwidget(edit=self._root_widget, transition='out_left')
1031        pbrowser.ProfileBrowserWindow(
1032            origin_widget=self._player_profiles_button)
1033
1034    def _cancel_v2_sign_in_press(self) -> None:
1035        # Just say we don't wanna be signed in anymore.
1036        ba.app.accounts_v2.set_primary_credentials(None)
1037
1038        # Speed UI updates along.
1039        ba.timer(0.1, ba.WeakCall(self._update), timetype=ba.TimeType.REAL)
1040
1041    def _sign_out_press(self) -> None:
1042
1043        if ba.app.accounts_v2.have_primary_credentials():
1044            ba.app.accounts_v2.set_primary_credentials(None)
1045        else:
1046            _ba.sign_out_v1()
1047
1048        cfg = ba.app.config
1049
1050        # Also take note that its our *explicit* intention to not be
1051        # signed in at this point (affects v1 accounts).
1052        cfg['Auto Account State'] = 'signed_out'
1053        cfg.commit()
1054        ba.buttonwidget(edit=self._sign_out_button,
1055                        label=ba.Lstr(resource=self._r + '.signingOutText'))
1056
1057        # Speed UI updates along.
1058        ba.timer(0.1, ba.WeakCall(self._update), timetype=ba.TimeType.REAL)
1059
1060    def _sign_in_press(self,
1061                       account_type: str,
1062                       show_test_warning: bool = True) -> None:
1063        del show_test_warning  # unused
1064        _ba.sign_in_v1(account_type)
1065
1066        # Make note of the type account we're *wanting* to be signed in with.
1067        cfg = ba.app.config
1068        cfg['Auto Account State'] = account_type
1069        cfg.commit()
1070        self._needs_refresh = True
1071        ba.timer(0.1, ba.WeakCall(self._update), timetype=ba.TimeType.REAL)
1072
1073    def _v2_sign_in_press(self) -> None:
1074        # pylint: disable=cyclic-import
1075        from bastd.ui.account.v2 import V2SignInWindow
1076        assert self._sign_in_v2_button is not None
1077        V2SignInWindow(origin_widget=self._sign_in_v2_button)
1078
1079    def _reset_progress(self) -> None:
1080        try:
1081            from ba.internal import getcampaign
1082            # FIXME: This would need to happen server-side these days.
1083            if self._can_reset_achievements:
1084                ba.app.config['Achievements'] = {}
1085                _ba.reset_achievements()
1086            campaign = getcampaign('Default')
1087            campaign.reset()  # also writes the config..
1088            campaign = getcampaign('Challenges')
1089            campaign.reset()  # also writes the config..
1090        except Exception:
1091            ba.print_exception('Error resetting co-op campaign progress.')
1092
1093        ba.playsound(ba.getsound('shieldDown'))
1094        self._refresh()
1095
1096    def _back(self) -> None:
1097        # pylint: disable=cyclic-import
1098        from bastd.ui.mainmenu import MainMenuWindow
1099        self._save_state()
1100        ba.containerwidget(edit=self._root_widget,
1101                           transition=self._transition_out)
1102
1103        if not self._modal:
1104            ba.app.ui.set_main_menu_window(
1105                MainMenuWindow(transition='in_left').get_root_widget())
1106
1107    def _save_state(self) -> None:
1108        try:
1109            sel = self._root_widget.get_selected_child()
1110            if sel == self._back_button:
1111                sel_name = 'Back'
1112            elif sel == self._scrollwidget:
1113                sel_name = 'Scroll'
1114            else:
1115                raise ValueError('unrecognized selection')
1116            ba.app.ui.window_states[type(self)] = sel_name
1117        except Exception:
1118            ba.print_exception(f'Error saving state for {self}.')
1119
1120    def _restore_state(self) -> None:
1121        try:
1122            sel_name = ba.app.ui.window_states.get(type(self))
1123            if sel_name == 'Back':
1124                sel = self._back_button
1125            elif sel_name == 'Scroll':
1126                sel = self._scrollwidget
1127            else:
1128                sel = self._back_button
1129            ba.containerwidget(edit=self._root_widget, selected_child=sel)
1130        except Exception:
1131            ba.print_exception(f'Error restoring state for {self}.')
class AccountSettingsWindow(ba.ui.Window):
  19class AccountSettingsWindow(ba.Window):
  20    """Window for account related functionality."""
  21
  22    def __init__(self,
  23                 transition: str = 'in_right',
  24                 modal: bool = False,
  25                 origin_widget: ba.Widget | None = None,
  26                 close_once_signed_in: bool = False):
  27        # pylint: disable=too-many-statements
  28
  29        self._sign_in_game_circle_button: ba.Widget | None = None
  30        self._sign_in_v2_button: ba.Widget | None = None
  31        self._sign_in_device_button: ba.Widget | None = None
  32
  33        self._close_once_signed_in = close_once_signed_in
  34        ba.set_analytics_screen('Account Window')
  35
  36        # If they provided an origin-widget, scale up from that.
  37        scale_origin: tuple[float, float] | None
  38        if origin_widget is not None:
  39            self._transition_out = 'out_scale'
  40            scale_origin = origin_widget.get_screen_space_center()
  41            transition = 'in_scale'
  42        else:
  43            self._transition_out = 'out_right'
  44            scale_origin = None
  45
  46        self._r = 'accountSettingsWindow'
  47        self._modal = modal
  48        self._needs_refresh = False
  49        self._signed_in = (_ba.get_v1_account_state() == 'signed_in')
  50        self._account_state_num = _ba.get_v1_account_state_num()
  51        self._show_linked = (self._signed_in
  52                             and _ba.get_v1_account_misc_read_val(
  53                                 'allowAccountLinking2', False))
  54        self._check_sign_in_timer = ba.Timer(1.0,
  55                                             ba.WeakCall(self._update),
  56                                             timetype=ba.TimeType.REAL,
  57                                             repeat=True)
  58
  59        # Currently we can only reset achievements on game-center.
  60        account_type: str | None
  61        if self._signed_in:
  62            account_type = _ba.get_v1_account_type()
  63        else:
  64            account_type = None
  65        self._can_reset_achievements = (account_type == 'Game Center')
  66
  67        app = ba.app
  68        uiscale = app.ui.uiscale
  69
  70        self._width = 760 if uiscale is ba.UIScale.SMALL else 660
  71        x_offs = 50 if uiscale is ba.UIScale.SMALL else 0
  72        self._height = (390 if uiscale is ba.UIScale.SMALL else
  73                        430 if uiscale is ba.UIScale.MEDIUM else 490)
  74
  75        self._sign_in_button = None
  76        self._sign_in_text = None
  77
  78        self._scroll_width = self._width - (100 + x_offs * 2)
  79        self._scroll_height = self._height - 120
  80        self._sub_width = self._scroll_width - 20
  81
  82        # Determine which sign-in/sign-out buttons we should show.
  83        self._show_sign_in_buttons: list[str] = []
  84
  85        if app.platform == 'android' and app.subplatform == 'google':
  86            self._show_sign_in_buttons.append('Google Play')
  87
  88        elif app.platform == 'android' and app.subplatform == 'amazon':
  89            self._show_sign_in_buttons.append('Game Circle')
  90
  91        # Local accounts are generally always available with a few key
  92        # exceptions.
  93        self._show_sign_in_buttons.append('Local')
  94
  95        # Ditto with shiny new V2 ones.
  96        if bool(True):
  97            self._show_sign_in_buttons.append('V2')
  98
  99        top_extra = 15 if uiscale is ba.UIScale.SMALL else 0
 100        super().__init__(root_widget=ba.containerwidget(
 101            size=(self._width, self._height + top_extra),
 102            transition=transition,
 103            toolbar_visibility='menu_minimal',
 104            scale_origin_stack_offset=scale_origin,
 105            scale=(2.09 if uiscale is ba.UIScale.SMALL else
 106                   1.4 if uiscale is ba.UIScale.MEDIUM else 1.0),
 107            stack_offset=(0, -19) if uiscale is ba.UIScale.SMALL else (0, 0)))
 108        if uiscale is ba.UIScale.SMALL and ba.app.ui.use_toolbars:
 109            self._back_button = None
 110            ba.containerwidget(edit=self._root_widget,
 111                               on_cancel_call=self._back)
 112        else:
 113            self._back_button = btn = ba.buttonwidget(
 114                parent=self._root_widget,
 115                position=(51 + x_offs, self._height - 62),
 116                size=(120, 60),
 117                scale=0.8,
 118                text_scale=1.2,
 119                autoselect=True,
 120                label=ba.Lstr(
 121                    resource='doneText' if self._modal else 'backText'),
 122                button_type='regular' if self._modal else 'back',
 123                on_activate_call=self._back)
 124            ba.containerwidget(edit=self._root_widget, cancel_button=btn)
 125            if not self._modal:
 126                ba.buttonwidget(edit=btn,
 127                                button_type='backSmall',
 128                                size=(60, 56),
 129                                label=ba.charstr(ba.SpecialChar.BACK))
 130
 131        ba.textwidget(parent=self._root_widget,
 132                      position=(self._width * 0.5, self._height - 41),
 133                      size=(0, 0),
 134                      text=ba.Lstr(resource=self._r + '.titleText'),
 135                      color=ba.app.ui.title_color,
 136                      maxwidth=self._width - 340,
 137                      h_align='center',
 138                      v_align='center')
 139
 140        self._scrollwidget = ba.scrollwidget(
 141            parent=self._root_widget,
 142            highlight=False,
 143            position=((self._width - self._scroll_width) * 0.5,
 144                      self._height - 65 - self._scroll_height),
 145            size=(self._scroll_width, self._scroll_height),
 146            claims_left_right=True,
 147            claims_tab=True,
 148            selection_loops_to_parent=True)
 149        self._subcontainer: ba.Widget | None = None
 150        self._refresh()
 151        self._restore_state()
 152
 153    def _update(self) -> None:
 154
 155        # If they want us to close once we're signed in, do so.
 156        if self._close_once_signed_in and self._signed_in:
 157            self._back()
 158            return
 159
 160        # Hmm should update this to use get_account_state_num.
 161        # Theoretically if we switch from one signed-in account to another
 162        # in the background this would break.
 163        account_state_num = _ba.get_v1_account_state_num()
 164        account_state = _ba.get_v1_account_state()
 165
 166        show_linked = (self._signed_in and _ba.get_v1_account_misc_read_val(
 167            'allowAccountLinking2', False))
 168
 169        if (account_state_num != self._account_state_num
 170                or self._show_linked != show_linked or self._needs_refresh):
 171            self._show_linked = show_linked
 172            self._account_state_num = account_state_num
 173            self._signed_in = (account_state == 'signed_in')
 174            self._refresh()
 175
 176        # Go ahead and refresh some individual things
 177        # that may change under us.
 178        self._update_linked_accounts_text()
 179        self._update_unlink_accounts_button()
 180        self._refresh_campaign_progress_text()
 181        self._refresh_achievements()
 182        self._refresh_tickets_text()
 183        self._refresh_account_name_text()
 184
 185    def _get_sign_in_text(self) -> ba.Lstr:
 186        return ba.Lstr(resource=self._r + '.signInText')
 187
 188    def _refresh(self) -> None:
 189        # pylint: disable=too-many-statements
 190        # pylint: disable=too-many-branches
 191        # pylint: disable=too-many-locals
 192        # pylint: disable=cyclic-import
 193        from bastd.ui import confirm
 194
 195        account_state = _ba.get_v1_account_state()
 196        account_type = (_ba.get_v1_account_type()
 197                        if account_state == 'signed_in' else 'unknown')
 198
 199        is_google = account_type == 'Google Play'
 200
 201        show_local_signed_in_as = False
 202        local_signed_in_as_space = 50.0
 203
 204        show_signed_in_as = self._signed_in
 205        signed_in_as_space = 95.0
 206
 207        show_sign_in_benefits = not self._signed_in
 208        sign_in_benefits_space = 80.0
 209
 210        show_signing_in_text = account_state == 'signing_in'
 211        signing_in_text_space = 80.0
 212
 213        show_google_play_sign_in_button = (account_state == 'signed_out'
 214                                           and 'Google Play'
 215                                           in self._show_sign_in_buttons)
 216        show_game_circle_sign_in_button = (account_state == 'signed_out'
 217                                           and 'Game Circle'
 218                                           in self._show_sign_in_buttons)
 219        show_device_sign_in_button = (account_state == 'signed_out' and 'Local'
 220                                      in self._show_sign_in_buttons)
 221        show_v2_sign_in_button = (account_state == 'signed_out'
 222                                  and 'V2' in self._show_sign_in_buttons)
 223        sign_in_button_space = 70.0
 224
 225        show_game_service_button = (self._signed_in and account_type
 226                                    in ['Game Center', 'Game Circle'])
 227        game_service_button_space = 60.0
 228
 229        show_linked_accounts_text = (self._signed_in
 230                                     and _ba.get_v1_account_misc_read_val(
 231                                         'allowAccountLinking2', False))
 232        linked_accounts_text_space = 60.0
 233
 234        show_achievements_button = (
 235            self._signed_in
 236            and account_type in ('Google Play', 'Alibaba', 'Local', 'OUYA'))
 237        achievements_button_space = 60.0
 238
 239        show_achievements_text = (self._signed_in
 240                                  and not show_achievements_button)
 241        achievements_text_space = 27.0
 242
 243        show_leaderboards_button = (self._signed_in and is_google)
 244        leaderboards_button_space = 60.0
 245
 246        show_campaign_progress = self._signed_in
 247        campaign_progress_space = 27.0
 248
 249        show_tickets = self._signed_in
 250        tickets_space = 27.0
 251
 252        show_reset_progress_button = False
 253        reset_progress_button_space = 70.0
 254
 255        show_player_profiles_button = self._signed_in
 256        player_profiles_button_space = 100.0
 257
 258        show_link_accounts_button = (self._signed_in
 259                                     and _ba.get_v1_account_misc_read_val(
 260                                         'allowAccountLinking2', False))
 261        link_accounts_button_space = 70.0
 262
 263        show_unlink_accounts_button = show_link_accounts_button
 264        unlink_accounts_button_space = 90.0
 265
 266        show_sign_out_button = (self._signed_in and account_type
 267                                in ['Local', 'Google Play', 'V2'])
 268        sign_out_button_space = 70.0
 269
 270        show_cancel_v2_sign_in_button = (
 271            account_state == 'signing_in'
 272            and ba.app.accounts_v2.have_primary_credentials())
 273        cancel_v2_sign_in_button_space = 70.0
 274
 275        if self._subcontainer is not None:
 276            self._subcontainer.delete()
 277        self._sub_height = 60.0
 278        if show_local_signed_in_as:
 279            self._sub_height += local_signed_in_as_space
 280        if show_signed_in_as:
 281            self._sub_height += signed_in_as_space
 282        if show_signing_in_text:
 283            self._sub_height += signing_in_text_space
 284        if show_google_play_sign_in_button:
 285            self._sub_height += sign_in_button_space
 286        if show_game_circle_sign_in_button:
 287            self._sub_height += sign_in_button_space
 288        if show_device_sign_in_button:
 289            self._sub_height += sign_in_button_space
 290        if show_v2_sign_in_button:
 291            self._sub_height += sign_in_button_space
 292        if show_game_service_button:
 293            self._sub_height += game_service_button_space
 294        if show_linked_accounts_text:
 295            self._sub_height += linked_accounts_text_space
 296        if show_achievements_text:
 297            self._sub_height += achievements_text_space
 298        if show_achievements_button:
 299            self._sub_height += achievements_button_space
 300        if show_leaderboards_button:
 301            self._sub_height += leaderboards_button_space
 302        if show_campaign_progress:
 303            self._sub_height += campaign_progress_space
 304        if show_tickets:
 305            self._sub_height += tickets_space
 306        if show_sign_in_benefits:
 307            self._sub_height += sign_in_benefits_space
 308        if show_reset_progress_button:
 309            self._sub_height += reset_progress_button_space
 310        if show_player_profiles_button:
 311            self._sub_height += player_profiles_button_space
 312        if show_link_accounts_button:
 313            self._sub_height += link_accounts_button_space
 314        if show_unlink_accounts_button:
 315            self._sub_height += unlink_accounts_button_space
 316        if show_sign_out_button:
 317            self._sub_height += sign_out_button_space
 318        if show_cancel_v2_sign_in_button:
 319            self._sub_height += cancel_v2_sign_in_button_space
 320        self._subcontainer = ba.containerwidget(parent=self._scrollwidget,
 321                                                size=(self._sub_width,
 322                                                      self._sub_height),
 323                                                background=False,
 324                                                claims_left_right=True,
 325                                                claims_tab=True,
 326                                                selection_loops_to_parent=True)
 327
 328        first_selectable = None
 329        v = self._sub_height - 10.0
 330
 331        if show_local_signed_in_as:
 332            v -= local_signed_in_as_space * 0.6
 333            ba.textwidget(
 334                parent=self._subcontainer,
 335                position=(self._sub_width * 0.5, v),
 336                size=(0, 0),
 337                text=ba.Lstr(
 338                    resource='accountSettingsWindow.deviceSpecificAccountText',
 339                    subs=[('${NAME}', _ba.get_v1_account_display_string())]),
 340                scale=0.7,
 341                color=(0.5, 0.5, 0.6),
 342                maxwidth=self._sub_width * 0.9,
 343                flatness=1.0,
 344                h_align='center',
 345                v_align='center')
 346            v -= local_signed_in_as_space * 0.4
 347
 348        self._account_name_text: ba.Widget | None
 349        if show_signed_in_as:
 350            v -= signed_in_as_space * 0.2
 351            txt = ba.Lstr(
 352                resource='accountSettingsWindow.youAreSignedInAsText',
 353                fallback_resource='accountSettingsWindow.youAreLoggedInAsText')
 354            ba.textwidget(parent=self._subcontainer,
 355                          position=(self._sub_width * 0.5, v),
 356                          size=(0, 0),
 357                          text=txt,
 358                          scale=0.9,
 359                          color=ba.app.ui.title_color,
 360                          maxwidth=self._sub_width * 0.9,
 361                          h_align='center',
 362                          v_align='center')
 363            v -= signed_in_as_space * 0.4
 364            self._account_name_text = ba.textwidget(
 365                parent=self._subcontainer,
 366                position=(self._sub_width * 0.5, v),
 367                size=(0, 0),
 368                scale=1.5,
 369                maxwidth=self._sub_width * 0.9,
 370                res_scale=1.5,
 371                color=(1, 1, 1, 1),
 372                h_align='center',
 373                v_align='center')
 374            self._refresh_account_name_text()
 375            v -= signed_in_as_space * 0.4
 376        else:
 377            self._account_name_text = None
 378
 379        if self._back_button is None:
 380            bbtn = _ba.get_special_widget('back_button')
 381        else:
 382            bbtn = self._back_button
 383
 384        if show_sign_in_benefits:
 385            v -= sign_in_benefits_space
 386            app = ba.app
 387            extra: str | ba.Lstr | None
 388            if (app.platform in ['mac', 'ios']
 389                    and app.subplatform == 'appstore'):
 390                extra = ba.Lstr(
 391                    value='\n${S}',
 392                    subs=[('${S}',
 393                           ba.Lstr(resource='signInWithGameCenterText'))])
 394            else:
 395                extra = ''
 396
 397            ba.textwidget(parent=self._subcontainer,
 398                          position=(self._sub_width * 0.5,
 399                                    v + sign_in_benefits_space * 0.4),
 400                          size=(0, 0),
 401                          text=ba.Lstr(value='${A}${B}',
 402                                       subs=[('${A}',
 403                                              ba.Lstr(resource=self._r +
 404                                                      '.signInInfoText')),
 405                                             ('${B}', extra)]),
 406                          max_height=sign_in_benefits_space * 0.9,
 407                          scale=0.9,
 408                          color=(0.75, 0.7, 0.8),
 409                          maxwidth=self._sub_width * 0.8,
 410                          h_align='center',
 411                          v_align='center')
 412
 413        if show_signing_in_text:
 414            v -= signing_in_text_space
 415
 416            ba.textwidget(
 417                parent=self._subcontainer,
 418                position=(self._sub_width * 0.5,
 419                          v + signing_in_text_space * 0.5),
 420                size=(0, 0),
 421                text=ba.Lstr(resource='accountSettingsWindow.signingInText'),
 422                scale=0.9,
 423                color=(0, 1, 0),
 424                maxwidth=self._sub_width * 0.8,
 425                h_align='center',
 426                v_align='center')
 427
 428        if show_google_play_sign_in_button:
 429            button_width = 350
 430            v -= sign_in_button_space
 431            self._sign_in_google_play_button = btn = ba.buttonwidget(
 432                parent=self._subcontainer,
 433                position=((self._sub_width - button_width) * 0.5, v - 20),
 434                autoselect=True,
 435                size=(button_width, 60),
 436                label=ba.Lstr(
 437                    value='${A}${B}',
 438                    subs=[('${A}',
 439                           ba.charstr(ba.SpecialChar.GOOGLE_PLAY_GAMES_LOGO)),
 440                          ('${B}',
 441                           ba.Lstr(resource=self._r +
 442                                   '.signInWithGooglePlayText'))]),
 443                on_activate_call=lambda: self._sign_in_press('Google Play'))
 444            if first_selectable is None:
 445                first_selectable = btn
 446            if ba.app.ui.use_toolbars:
 447                ba.widget(edit=btn,
 448                          right_widget=_ba.get_special_widget('party_button'))
 449            ba.widget(edit=btn, left_widget=bbtn)
 450            ba.widget(edit=btn, show_buffer_bottom=40, show_buffer_top=100)
 451            self._sign_in_text = None
 452
 453        if show_game_circle_sign_in_button:
 454            button_width = 350
 455            v -= sign_in_button_space
 456            self._sign_in_game_circle_button = btn = ba.buttonwidget(
 457                parent=self._subcontainer,
 458                position=((self._sub_width - button_width) * 0.5, v - 20),
 459                autoselect=True,
 460                size=(button_width, 60),
 461                label=ba.Lstr(value='${A}${B}',
 462                              subs=[('${A}',
 463                                     ba.charstr(
 464                                         ba.SpecialChar.GAME_CIRCLE_LOGO)),
 465                                    ('${B}',
 466                                     ba.Lstr(resource=self._r +
 467                                             '.signInWithGameCircleText'))]),
 468                on_activate_call=lambda: self._sign_in_press('Game Circle'))
 469            if first_selectable is None:
 470                first_selectable = btn
 471            if ba.app.ui.use_toolbars:
 472                ba.widget(edit=btn,
 473                          right_widget=_ba.get_special_widget('party_button'))
 474            ba.widget(edit=btn, left_widget=bbtn)
 475            ba.widget(edit=btn, show_buffer_bottom=40, show_buffer_top=100)
 476            self._sign_in_text = None
 477
 478        if show_v2_sign_in_button:
 479            button_width = 350
 480            v -= sign_in_button_space
 481            self._sign_in_v2_button = btn = ba.buttonwidget(
 482                parent=self._subcontainer,
 483                position=((self._sub_width - button_width) * 0.5, v - 20),
 484                autoselect=True,
 485                size=(button_width, 60),
 486                label='',
 487                on_activate_call=self._v2_sign_in_press)
 488            ba.textwidget(
 489                parent=self._subcontainer,
 490                draw_controller=btn,
 491                h_align='center',
 492                v_align='center',
 493                size=(0, 0),
 494                position=(self._sub_width * 0.5, v + 17),
 495                text=ba.Lstr(
 496                    value='${A}${B}',
 497                    subs=[('${A}', ba.charstr(ba.SpecialChar.V2_LOGO)),
 498                          ('${B}',
 499                           ba.Lstr(resource=self._r + '.signInWithV2Text'))]),
 500                maxwidth=button_width * 0.8,
 501                color=(0.75, 1.0, 0.7))
 502            ba.textwidget(parent=self._subcontainer,
 503                          draw_controller=btn,
 504                          h_align='center',
 505                          v_align='center',
 506                          size=(0, 0),
 507                          position=(self._sub_width * 0.5, v - 4),
 508                          text=ba.Lstr(resource=self._r +
 509                                       '.signInWithV2InfoText'),
 510                          flatness=1.0,
 511                          scale=0.57,
 512                          maxwidth=button_width * 0.9,
 513                          color=(0.55, 0.8, 0.5))
 514            if first_selectable is None:
 515                first_selectable = btn
 516            if ba.app.ui.use_toolbars:
 517                ba.widget(edit=btn,
 518                          right_widget=_ba.get_special_widget('party_button'))
 519            ba.widget(edit=btn, left_widget=bbtn)
 520            ba.widget(edit=btn, show_buffer_bottom=40, show_buffer_top=100)
 521            self._sign_in_text = None
 522
 523        if show_device_sign_in_button:
 524            button_width = 350
 525            v -= sign_in_button_space
 526            self._sign_in_device_button = btn = ba.buttonwidget(
 527                parent=self._subcontainer,
 528                position=((self._sub_width - button_width) * 0.5, v - 20),
 529                autoselect=True,
 530                size=(button_width, 60),
 531                label='',
 532                on_activate_call=lambda: self._sign_in_press('Local'))
 533            ba.textwidget(parent=self._subcontainer,
 534                          draw_controller=btn,
 535                          h_align='center',
 536                          v_align='center',
 537                          size=(0, 0),
 538                          position=(self._sub_width * 0.5, v + 17),
 539                          text=ba.Lstr(
 540                              value='${A}${B}',
 541                              subs=[('${A}',
 542                                     ba.charstr(ba.SpecialChar.LOCAL_ACCOUNT)),
 543                                    ('${B}',
 544                                     ba.Lstr(resource=self._r +
 545                                             '.signInWithDeviceText'))]),
 546                          maxwidth=button_width * 0.8,
 547                          color=(0.75, 1.0, 0.7))
 548            ba.textwidget(parent=self._subcontainer,
 549                          draw_controller=btn,
 550                          h_align='center',
 551                          v_align='center',
 552                          size=(0, 0),
 553                          position=(self._sub_width * 0.5, v - 4),
 554                          text=ba.Lstr(resource=self._r +
 555                                       '.signInWithDeviceInfoText'),
 556                          flatness=1.0,
 557                          scale=0.57,
 558                          maxwidth=button_width * 0.9,
 559                          color=(0.55, 0.8, 0.5))
 560            if first_selectable is None:
 561                first_selectable = btn
 562            if ba.app.ui.use_toolbars:
 563                ba.widget(edit=btn,
 564                          right_widget=_ba.get_special_widget('party_button'))
 565            ba.widget(edit=btn, left_widget=bbtn)
 566            ba.widget(edit=btn, show_buffer_bottom=40, show_buffer_top=100)
 567            self._sign_in_text = None
 568
 569        if show_player_profiles_button:
 570            button_width = 300
 571            v -= player_profiles_button_space
 572            self._player_profiles_button = btn = ba.buttonwidget(
 573                parent=self._subcontainer,
 574                position=((self._sub_width - button_width) * 0.5, v + 30),
 575                autoselect=True,
 576                size=(button_width, 60),
 577                label=ba.Lstr(resource='playerProfilesWindow.titleText'),
 578                color=(0.55, 0.5, 0.6),
 579                icon=ba.gettexture('cuteSpaz'),
 580                textcolor=(0.75, 0.7, 0.8),
 581                on_activate_call=self._player_profiles_press)
 582            if first_selectable is None:
 583                first_selectable = btn
 584            if ba.app.ui.use_toolbars:
 585                ba.widget(edit=btn,
 586                          right_widget=_ba.get_special_widget('party_button'))
 587            ba.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=0)
 588
 589        # the button to go to OS-Specific leaderboards/high-score-lists/etc.
 590        if show_game_service_button:
 591            button_width = 300
 592            v -= game_service_button_space * 0.85
 593            account_type = _ba.get_v1_account_type()
 594            if account_type == 'Game Center':
 595                account_type_name = ba.Lstr(resource='gameCenterText')
 596            elif account_type == 'Game Circle':
 597                account_type_name = ba.Lstr(resource='gameCircleText')
 598            else:
 599                raise ValueError("unknown account type: '" +
 600                                 str(account_type) + "'")
 601            self._game_service_button = btn = ba.buttonwidget(
 602                parent=self._subcontainer,
 603                position=((self._sub_width - button_width) * 0.5, v),
 604                color=(0.55, 0.5, 0.6),
 605                textcolor=(0.75, 0.7, 0.8),
 606                autoselect=True,
 607                on_activate_call=_ba.show_online_score_ui,
 608                size=(button_width, 50),
 609                label=account_type_name)
 610            if first_selectable is None:
 611                first_selectable = btn
 612            if ba.app.ui.use_toolbars:
 613                ba.widget(edit=btn,
 614                          right_widget=_ba.get_special_widget('party_button'))
 615            ba.widget(edit=btn, left_widget=bbtn)
 616            v -= game_service_button_space * 0.15
 617        else:
 618            self.game_service_button = None
 619
 620        self._achievements_text: ba.Widget | None
 621        if show_achievements_text:
 622            v -= achievements_text_space * 0.5
 623            self._achievements_text = ba.textwidget(
 624                parent=self._subcontainer,
 625                position=(self._sub_width * 0.5, v),
 626                size=(0, 0),
 627                scale=0.9,
 628                color=(0.75, 0.7, 0.8),
 629                maxwidth=self._sub_width * 0.8,
 630                h_align='center',
 631                v_align='center')
 632            v -= achievements_text_space * 0.5
 633        else:
 634            self._achievements_text = None
 635
 636        self._achievements_button: ba.Widget | None
 637        if show_achievements_button:
 638            button_width = 300
 639            v -= achievements_button_space * 0.85
 640            self._achievements_button = btn = ba.buttonwidget(
 641                parent=self._subcontainer,
 642                position=((self._sub_width - button_width) * 0.5, v),
 643                color=(0.55, 0.5, 0.6),
 644                textcolor=(0.75, 0.7, 0.8),
 645                autoselect=True,
 646                icon=ba.gettexture('googlePlayAchievementsIcon'
 647                                   if is_google else 'achievementsIcon'),
 648                icon_color=(0.8, 0.95, 0.7) if is_google else (0.85, 0.8, 0.9),
 649                on_activate_call=self._on_achievements_press,
 650                size=(button_width, 50),
 651                label='')
 652            if first_selectable is None:
 653                first_selectable = btn
 654            if ba.app.ui.use_toolbars:
 655                ba.widget(edit=btn,
 656                          right_widget=_ba.get_special_widget('party_button'))
 657            ba.widget(edit=btn, left_widget=bbtn)
 658            v -= achievements_button_space * 0.15
 659        else:
 660            self._achievements_button = None
 661
 662        if show_achievements_text or show_achievements_button:
 663            self._refresh_achievements()
 664
 665        self._leaderboards_button: ba.Widget | None
 666        if show_leaderboards_button:
 667            button_width = 300
 668            v -= leaderboards_button_space * 0.85
 669            self._leaderboards_button = btn = ba.buttonwidget(
 670                parent=self._subcontainer,
 671                position=((self._sub_width - button_width) * 0.5, v),
 672                color=(0.55, 0.5, 0.6),
 673                textcolor=(0.75, 0.7, 0.8),
 674                autoselect=True,
 675                icon=ba.gettexture('googlePlayLeaderboardsIcon'),
 676                icon_color=(0.8, 0.95, 0.7),
 677                on_activate_call=self._on_leaderboards_press,
 678                size=(button_width, 50),
 679                label=ba.Lstr(resource='leaderboardsText'))
 680            if first_selectable is None:
 681                first_selectable = btn
 682            if ba.app.ui.use_toolbars:
 683                ba.widget(edit=btn,
 684                          right_widget=_ba.get_special_widget('party_button'))
 685            ba.widget(edit=btn, left_widget=bbtn)
 686            v -= leaderboards_button_space * 0.15
 687        else:
 688            self._leaderboards_button = None
 689
 690        self._campaign_progress_text: ba.Widget | None
 691        if show_campaign_progress:
 692            v -= campaign_progress_space * 0.5
 693            self._campaign_progress_text = ba.textwidget(
 694                parent=self._subcontainer,
 695                position=(self._sub_width * 0.5, v),
 696                size=(0, 0),
 697                scale=0.9,
 698                color=(0.75, 0.7, 0.8),
 699                maxwidth=self._sub_width * 0.8,
 700                h_align='center',
 701                v_align='center')
 702            v -= campaign_progress_space * 0.5
 703            self._refresh_campaign_progress_text()
 704        else:
 705            self._campaign_progress_text = None
 706
 707        self._tickets_text: ba.Widget | None
 708        if show_tickets:
 709            v -= tickets_space * 0.5
 710            self._tickets_text = ba.textwidget(parent=self._subcontainer,
 711                                               position=(self._sub_width * 0.5,
 712                                                         v),
 713                                               size=(0, 0),
 714                                               scale=0.9,
 715                                               color=(0.75, 0.7, 0.8),
 716                                               maxwidth=self._sub_width * 0.8,
 717                                               flatness=1.0,
 718                                               h_align='center',
 719                                               v_align='center')
 720            v -= tickets_space * 0.5
 721            self._refresh_tickets_text()
 722
 723        else:
 724            self._tickets_text = None
 725
 726        # bit of spacing before the reset/sign-out section
 727        v -= 5
 728
 729        button_width = 250
 730        if show_reset_progress_button:
 731            confirm_text = (ba.Lstr(resource=self._r +
 732                                    '.resetProgressConfirmText')
 733                            if self._can_reset_achievements else ba.Lstr(
 734                                resource=self._r +
 735                                '.resetProgressConfirmNoAchievementsText'))
 736            v -= reset_progress_button_space
 737            self._reset_progress_button = btn = ba.buttonwidget(
 738                parent=self._subcontainer,
 739                position=((self._sub_width - button_width) * 0.5, v),
 740                color=(0.55, 0.5, 0.6),
 741                textcolor=(0.75, 0.7, 0.8),
 742                autoselect=True,
 743                size=(button_width, 60),
 744                label=ba.Lstr(resource=self._r + '.resetProgressText'),
 745                on_activate_call=lambda: confirm.ConfirmWindow(
 746                    text=confirm_text,
 747                    width=500,
 748                    height=200,
 749                    action=self._reset_progress))
 750            if first_selectable is None:
 751                first_selectable = btn
 752            if ba.app.ui.use_toolbars:
 753                ba.widget(edit=btn,
 754                          right_widget=_ba.get_special_widget('party_button'))
 755            ba.widget(edit=btn, left_widget=bbtn)
 756
 757        self._linked_accounts_text: ba.Widget | None
 758        if show_linked_accounts_text:
 759            v -= linked_accounts_text_space * 0.8
 760            self._linked_accounts_text = ba.textwidget(
 761                parent=self._subcontainer,
 762                position=(self._sub_width * 0.5, v),
 763                size=(0, 0),
 764                scale=0.9,
 765                color=(0.75, 0.7, 0.8),
 766                maxwidth=self._sub_width * 0.95,
 767                h_align='center',
 768                v_align='center')
 769            v -= linked_accounts_text_space * 0.2
 770            self._update_linked_accounts_text()
 771        else:
 772            self._linked_accounts_text = None
 773
 774        if show_link_accounts_button:
 775            v -= link_accounts_button_space
 776            self._link_accounts_button = btn = ba.buttonwidget(
 777                parent=self._subcontainer,
 778                position=((self._sub_width - button_width) * 0.5, v),
 779                autoselect=True,
 780                size=(button_width, 60),
 781                label='',
 782                color=(0.55, 0.5, 0.6),
 783                on_activate_call=self._link_accounts_press)
 784            ba.textwidget(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 + 17 + 20),
 790                          text=ba.Lstr(resource=self._r + '.linkAccountsText'),
 791                          maxwidth=button_width * 0.8,
 792                          color=(0.75, 0.7, 0.8))
 793            ba.textwidget(parent=self._subcontainer,
 794                          draw_controller=btn,
 795                          h_align='center',
 796                          v_align='center',
 797                          size=(0, 0),
 798                          position=(self._sub_width * 0.5, v - 4 + 20),
 799                          text=ba.Lstr(resource=self._r +
 800                                       '.linkAccountsInfoText'),
 801                          flatness=1.0,
 802                          scale=0.5,
 803                          maxwidth=button_width * 0.8,
 804                          color=(0.75, 0.7, 0.8))
 805            if first_selectable is None:
 806                first_selectable = btn
 807            if ba.app.ui.use_toolbars:
 808                ba.widget(edit=btn,
 809                          right_widget=_ba.get_special_widget('party_button'))
 810            ba.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=50)
 811
 812        self._unlink_accounts_button: ba.Widget | None
 813        if show_unlink_accounts_button:
 814            v -= unlink_accounts_button_space
 815            self._unlink_accounts_button = btn = ba.buttonwidget(
 816                parent=self._subcontainer,
 817                position=((self._sub_width - button_width) * 0.5, v + 25),
 818                autoselect=True,
 819                size=(button_width, 60),
 820                label='',
 821                color=(0.55, 0.5, 0.6),
 822                on_activate_call=self._unlink_accounts_press)
 823            self._unlink_accounts_button_label = ba.textwidget(
 824                parent=self._subcontainer,
 825                draw_controller=btn,
 826                h_align='center',
 827                v_align='center',
 828                size=(0, 0),
 829                position=(self._sub_width * 0.5, v + 55),
 830                text=ba.Lstr(resource=self._r + '.unlinkAccountsText'),
 831                maxwidth=button_width * 0.8,
 832                color=(0.75, 0.7, 0.8))
 833            if first_selectable is None:
 834                first_selectable = btn
 835            if ba.app.ui.use_toolbars:
 836                ba.widget(edit=btn,
 837                          right_widget=_ba.get_special_widget('party_button'))
 838            ba.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=50)
 839            self._update_unlink_accounts_button()
 840        else:
 841            self._unlink_accounts_button = None
 842
 843        if show_sign_out_button:
 844            v -= sign_out_button_space
 845            self._sign_out_button = btn = ba.buttonwidget(
 846                parent=self._subcontainer,
 847                position=((self._sub_width - button_width) * 0.5, v),
 848                size=(button_width, 60),
 849                label=ba.Lstr(resource=self._r + '.signOutText'),
 850                color=(0.55, 0.5, 0.6),
 851                textcolor=(0.75, 0.7, 0.8),
 852                autoselect=True,
 853                on_activate_call=self._sign_out_press)
 854            if first_selectable is None:
 855                first_selectable = btn
 856            if ba.app.ui.use_toolbars:
 857                ba.widget(edit=btn,
 858                          right_widget=_ba.get_special_widget('party_button'))
 859            ba.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=15)
 860
 861        if show_cancel_v2_sign_in_button:
 862            v -= cancel_v2_sign_in_button_space
 863            self._cancel_v2_sign_in_button = btn = ba.buttonwidget(
 864                parent=self._subcontainer,
 865                position=((self._sub_width - button_width) * 0.5, v),
 866                size=(button_width, 60),
 867                label=ba.Lstr(resource='cancelText'),
 868                color=(0.55, 0.5, 0.6),
 869                textcolor=(0.75, 0.7, 0.8),
 870                autoselect=True,
 871                on_activate_call=self._cancel_v2_sign_in_press)
 872            if first_selectable is None:
 873                first_selectable = btn
 874            if ba.app.ui.use_toolbars:
 875                ba.widget(edit=btn,
 876                          right_widget=_ba.get_special_widget('party_button'))
 877            ba.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=15)
 878
 879        # Whatever the topmost selectable thing is, we want it to scroll all
 880        # the way up when we select it.
 881        if first_selectable is not None:
 882            ba.widget(edit=first_selectable,
 883                      up_widget=bbtn,
 884                      show_buffer_top=400)
 885            # (this should re-scroll us to the top..)
 886            ba.containerwidget(edit=self._subcontainer,
 887                               visible_child=first_selectable)
 888        self._needs_refresh = False
 889
 890    def _on_achievements_press(self) -> None:
 891        # pylint: disable=cyclic-import
 892        from bastd.ui import achievements
 893        account_state = _ba.get_v1_account_state()
 894        account_type = (_ba.get_v1_account_type()
 895                        if account_state == 'signed_in' else 'unknown')
 896        # for google play we use the built-in UI; otherwise pop up our own
 897        if account_type == 'Google Play':
 898            ba.timer(0.15,
 899                     ba.Call(_ba.show_online_score_ui, 'achievements'),
 900                     timetype=ba.TimeType.REAL)
 901        elif account_type != 'unknown':
 902            assert self._achievements_button is not None
 903            achievements.AchievementsWindow(
 904                position=self._achievements_button.get_screen_space_center())
 905        else:
 906            print('ERROR: unknown account type in on_achievements_press:',
 907                  account_type)
 908
 909    def _on_leaderboards_press(self) -> None:
 910        ba.timer(0.15,
 911                 ba.Call(_ba.show_online_score_ui, 'leaderboards'),
 912                 timetype=ba.TimeType.REAL)
 913
 914    def _have_unlinkable_accounts(self) -> bool:
 915        # if this is not present, we haven't had contact from the server so
 916        # let's not proceed..
 917        if _ba.get_public_login_id() is None:
 918            return False
 919        accounts = _ba.get_v1_account_misc_read_val_2('linkedAccounts', [])
 920        return len(accounts) > 1
 921
 922    def _update_unlink_accounts_button(self) -> None:
 923        if self._unlink_accounts_button is None:
 924            return
 925        if self._have_unlinkable_accounts():
 926            clr = (0.75, 0.7, 0.8, 1.0)
 927        else:
 928            clr = (1.0, 1.0, 1.0, 0.25)
 929        ba.textwidget(edit=self._unlink_accounts_button_label, color=clr)
 930
 931    def _update_linked_accounts_text(self) -> None:
 932        if self._linked_accounts_text is None:
 933            return
 934
 935        # if this is not present, we haven't had contact from the server so
 936        # let's not proceed..
 937        if _ba.get_public_login_id() is None:
 938            num = int(time.time()) % 4
 939            accounts_str = num * '.' + (4 - num) * ' '
 940        else:
 941            accounts = _ba.get_v1_account_misc_read_val_2('linkedAccounts', [])
 942            # our_account = _bs.get_v1_account_display_string()
 943            # accounts = [a for a in accounts if a != our_account]
 944            # accounts_str = u', '.join(accounts) if accounts else
 945            # ba.Lstr(translate=('settingNames', 'None'))
 946            # UPDATE - we now just print the number here; not the actual
 947            # accounts
 948            # (they can see that in the unlink section if they're curious)
 949            accounts_str = str(max(0, len(accounts) - 1))
 950        ba.textwidget(edit=self._linked_accounts_text,
 951                      text=ba.Lstr(value='${L} ${A}',
 952                                   subs=[('${L}',
 953                                          ba.Lstr(resource=self._r +
 954                                                  '.linkedAccountsText')),
 955                                         ('${A}', accounts_str)]))
 956
 957    def _refresh_campaign_progress_text(self) -> None:
 958        from ba.internal import getcampaign
 959        if self._campaign_progress_text is None:
 960            return
 961        p_str: str | ba.Lstr
 962        try:
 963            campaign = getcampaign('Default')
 964            levels = campaign.levels
 965            levels_complete = sum((1 if l.complete else 0) for l in levels)
 966
 967            # Last level cant be completed; hence the -1;
 968            progress = min(1.0, float(levels_complete) / (len(levels) - 1))
 969            p_str = ba.Lstr(resource=self._r + '.campaignProgressText',
 970                            subs=[('${PROGRESS}',
 971                                   str(int(progress * 100.0)) + '%')])
 972        except Exception:
 973            p_str = '?'
 974            ba.print_exception('Error calculating co-op campaign progress.')
 975        ba.textwidget(edit=self._campaign_progress_text, text=p_str)
 976
 977    def _refresh_tickets_text(self) -> None:
 978        if self._tickets_text is None:
 979            return
 980        try:
 981            tc_str = str(_ba.get_v1_account_ticket_count())
 982        except Exception:
 983            ba.print_exception()
 984            tc_str = '-'
 985        ba.textwidget(edit=self._tickets_text,
 986                      text=ba.Lstr(resource=self._r + '.ticketsText',
 987                                   subs=[('${COUNT}', tc_str)]))
 988
 989    def _refresh_account_name_text(self) -> None:
 990        if self._account_name_text is None:
 991            return
 992        try:
 993            name_str = _ba.get_v1_account_display_string()
 994        except Exception:
 995            ba.print_exception()
 996            name_str = '??'
 997        ba.textwidget(edit=self._account_name_text, text=name_str)
 998
 999    def _refresh_achievements(self) -> None:
1000        if (self._achievements_text is None
1001                and self._achievements_button is None):
1002            return
1003        complete = sum(1 if a.complete else 0 for a in ba.app.ach.achievements)
1004        total = len(ba.app.ach.achievements)
1005        txt_final = ba.Lstr(resource=self._r + '.achievementProgressText',
1006                            subs=[('${COUNT}', str(complete)),
1007                                  ('${TOTAL}', str(total))])
1008
1009        if self._achievements_text is not None:
1010            ba.textwidget(edit=self._achievements_text, text=txt_final)
1011        if self._achievements_button is not None:
1012            ba.buttonwidget(edit=self._achievements_button, label=txt_final)
1013
1014    def _link_accounts_press(self) -> None:
1015        # pylint: disable=cyclic-import
1016        from bastd.ui.account import link
1017        link.AccountLinkWindow(origin_widget=self._link_accounts_button)
1018
1019    def _unlink_accounts_press(self) -> None:
1020        # pylint: disable=cyclic-import
1021        from bastd.ui.account import unlink
1022        if not self._have_unlinkable_accounts():
1023            ba.playsound(ba.getsound('error'))
1024            return
1025        unlink.AccountUnlinkWindow(origin_widget=self._unlink_accounts_button)
1026
1027    def _player_profiles_press(self) -> None:
1028        # pylint: disable=cyclic-import
1029        from bastd.ui.profile import browser as pbrowser
1030        self._save_state()
1031        ba.containerwidget(edit=self._root_widget, transition='out_left')
1032        pbrowser.ProfileBrowserWindow(
1033            origin_widget=self._player_profiles_button)
1034
1035    def _cancel_v2_sign_in_press(self) -> None:
1036        # Just say we don't wanna be signed in anymore.
1037        ba.app.accounts_v2.set_primary_credentials(None)
1038
1039        # Speed UI updates along.
1040        ba.timer(0.1, ba.WeakCall(self._update), timetype=ba.TimeType.REAL)
1041
1042    def _sign_out_press(self) -> None:
1043
1044        if ba.app.accounts_v2.have_primary_credentials():
1045            ba.app.accounts_v2.set_primary_credentials(None)
1046        else:
1047            _ba.sign_out_v1()
1048
1049        cfg = ba.app.config
1050
1051        # Also take note that its our *explicit* intention to not be
1052        # signed in at this point (affects v1 accounts).
1053        cfg['Auto Account State'] = 'signed_out'
1054        cfg.commit()
1055        ba.buttonwidget(edit=self._sign_out_button,
1056                        label=ba.Lstr(resource=self._r + '.signingOutText'))
1057
1058        # Speed UI updates along.
1059        ba.timer(0.1, ba.WeakCall(self._update), timetype=ba.TimeType.REAL)
1060
1061    def _sign_in_press(self,
1062                       account_type: str,
1063                       show_test_warning: bool = True) -> None:
1064        del show_test_warning  # unused
1065        _ba.sign_in_v1(account_type)
1066
1067        # Make note of the type account we're *wanting* to be signed in with.
1068        cfg = ba.app.config
1069        cfg['Auto Account State'] = account_type
1070        cfg.commit()
1071        self._needs_refresh = True
1072        ba.timer(0.1, ba.WeakCall(self._update), timetype=ba.TimeType.REAL)
1073
1074    def _v2_sign_in_press(self) -> None:
1075        # pylint: disable=cyclic-import
1076        from bastd.ui.account.v2 import V2SignInWindow
1077        assert self._sign_in_v2_button is not None
1078        V2SignInWindow(origin_widget=self._sign_in_v2_button)
1079
1080    def _reset_progress(self) -> None:
1081        try:
1082            from ba.internal import getcampaign
1083            # FIXME: This would need to happen server-side these days.
1084            if self._can_reset_achievements:
1085                ba.app.config['Achievements'] = {}
1086                _ba.reset_achievements()
1087            campaign = getcampaign('Default')
1088            campaign.reset()  # also writes the config..
1089            campaign = getcampaign('Challenges')
1090            campaign.reset()  # also writes the config..
1091        except Exception:
1092            ba.print_exception('Error resetting co-op campaign progress.')
1093
1094        ba.playsound(ba.getsound('shieldDown'))
1095        self._refresh()
1096
1097    def _back(self) -> None:
1098        # pylint: disable=cyclic-import
1099        from bastd.ui.mainmenu import MainMenuWindow
1100        self._save_state()
1101        ba.containerwidget(edit=self._root_widget,
1102                           transition=self._transition_out)
1103
1104        if not self._modal:
1105            ba.app.ui.set_main_menu_window(
1106                MainMenuWindow(transition='in_left').get_root_widget())
1107
1108    def _save_state(self) -> None:
1109        try:
1110            sel = self._root_widget.get_selected_child()
1111            if sel == self._back_button:
1112                sel_name = 'Back'
1113            elif sel == self._scrollwidget:
1114                sel_name = 'Scroll'
1115            else:
1116                raise ValueError('unrecognized selection')
1117            ba.app.ui.window_states[type(self)] = sel_name
1118        except Exception:
1119            ba.print_exception(f'Error saving state for {self}.')
1120
1121    def _restore_state(self) -> None:
1122        try:
1123            sel_name = ba.app.ui.window_states.get(type(self))
1124            if sel_name == 'Back':
1125                sel = self._back_button
1126            elif sel_name == 'Scroll':
1127                sel = self._scrollwidget
1128            else:
1129                sel = self._back_button
1130            ba.containerwidget(edit=self._root_widget, selected_child=sel)
1131        except Exception:
1132            ba.print_exception(f'Error restoring state for {self}.')

Window for account related functionality.

AccountSettingsWindow( transition: str = 'in_right', modal: bool = False, origin_widget: _ba.Widget | None = None, close_once_signed_in: bool = False)
 22    def __init__(self,
 23                 transition: str = 'in_right',
 24                 modal: bool = False,
 25                 origin_widget: ba.Widget | None = None,
 26                 close_once_signed_in: bool = False):
 27        # pylint: disable=too-many-statements
 28
 29        self._sign_in_game_circle_button: ba.Widget | None = None
 30        self._sign_in_v2_button: ba.Widget | None = None
 31        self._sign_in_device_button: ba.Widget | None = None
 32
 33        self._close_once_signed_in = close_once_signed_in
 34        ba.set_analytics_screen('Account Window')
 35
 36        # If they provided an origin-widget, scale up from that.
 37        scale_origin: tuple[float, float] | None
 38        if origin_widget is not None:
 39            self._transition_out = 'out_scale'
 40            scale_origin = origin_widget.get_screen_space_center()
 41            transition = 'in_scale'
 42        else:
 43            self._transition_out = 'out_right'
 44            scale_origin = None
 45
 46        self._r = 'accountSettingsWindow'
 47        self._modal = modal
 48        self._needs_refresh = False
 49        self._signed_in = (_ba.get_v1_account_state() == 'signed_in')
 50        self._account_state_num = _ba.get_v1_account_state_num()
 51        self._show_linked = (self._signed_in
 52                             and _ba.get_v1_account_misc_read_val(
 53                                 'allowAccountLinking2', False))
 54        self._check_sign_in_timer = ba.Timer(1.0,
 55                                             ba.WeakCall(self._update),
 56                                             timetype=ba.TimeType.REAL,
 57                                             repeat=True)
 58
 59        # Currently we can only reset achievements on game-center.
 60        account_type: str | None
 61        if self._signed_in:
 62            account_type = _ba.get_v1_account_type()
 63        else:
 64            account_type = None
 65        self._can_reset_achievements = (account_type == 'Game Center')
 66
 67        app = ba.app
 68        uiscale = app.ui.uiscale
 69
 70        self._width = 760 if uiscale is ba.UIScale.SMALL else 660
 71        x_offs = 50 if uiscale is ba.UIScale.SMALL else 0
 72        self._height = (390 if uiscale is ba.UIScale.SMALL else
 73                        430 if uiscale is ba.UIScale.MEDIUM else 490)
 74
 75        self._sign_in_button = None
 76        self._sign_in_text = None
 77
 78        self._scroll_width = self._width - (100 + x_offs * 2)
 79        self._scroll_height = self._height - 120
 80        self._sub_width = self._scroll_width - 20
 81
 82        # Determine which sign-in/sign-out buttons we should show.
 83        self._show_sign_in_buttons: list[str] = []
 84
 85        if app.platform == 'android' and app.subplatform == 'google':
 86            self._show_sign_in_buttons.append('Google Play')
 87
 88        elif app.platform == 'android' and app.subplatform == 'amazon':
 89            self._show_sign_in_buttons.append('Game Circle')
 90
 91        # Local accounts are generally always available with a few key
 92        # exceptions.
 93        self._show_sign_in_buttons.append('Local')
 94
 95        # Ditto with shiny new V2 ones.
 96        if bool(True):
 97            self._show_sign_in_buttons.append('V2')
 98
 99        top_extra = 15 if uiscale is ba.UIScale.SMALL else 0
100        super().__init__(root_widget=ba.containerwidget(
101            size=(self._width, self._height + top_extra),
102            transition=transition,
103            toolbar_visibility='menu_minimal',
104            scale_origin_stack_offset=scale_origin,
105            scale=(2.09 if uiscale is ba.UIScale.SMALL else
106                   1.4 if uiscale is ba.UIScale.MEDIUM else 1.0),
107            stack_offset=(0, -19) if uiscale is ba.UIScale.SMALL else (0, 0)))
108        if uiscale is ba.UIScale.SMALL and ba.app.ui.use_toolbars:
109            self._back_button = None
110            ba.containerwidget(edit=self._root_widget,
111                               on_cancel_call=self._back)
112        else:
113            self._back_button = btn = ba.buttonwidget(
114                parent=self._root_widget,
115                position=(51 + x_offs, self._height - 62),
116                size=(120, 60),
117                scale=0.8,
118                text_scale=1.2,
119                autoselect=True,
120                label=ba.Lstr(
121                    resource='doneText' if self._modal else 'backText'),
122                button_type='regular' if self._modal else 'back',
123                on_activate_call=self._back)
124            ba.containerwidget(edit=self._root_widget, cancel_button=btn)
125            if not self._modal:
126                ba.buttonwidget(edit=btn,
127                                button_type='backSmall',
128                                size=(60, 56),
129                                label=ba.charstr(ba.SpecialChar.BACK))
130
131        ba.textwidget(parent=self._root_widget,
132                      position=(self._width * 0.5, self._height - 41),
133                      size=(0, 0),
134                      text=ba.Lstr(resource=self._r + '.titleText'),
135                      color=ba.app.ui.title_color,
136                      maxwidth=self._width - 340,
137                      h_align='center',
138                      v_align='center')
139
140        self._scrollwidget = ba.scrollwidget(
141            parent=self._root_widget,
142            highlight=False,
143            position=((self._width - self._scroll_width) * 0.5,
144                      self._height - 65 - self._scroll_height),
145            size=(self._scroll_width, self._scroll_height),
146            claims_left_right=True,
147            claims_tab=True,
148            selection_loops_to_parent=True)
149        self._subcontainer: ba.Widget | None = None
150        self._refresh()
151        self._restore_state()
Inherited Members
ba.ui.Window
get_root_widget