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