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