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