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