# Released under the MIT License. See LICENSE for details.
#
"""Provides UI for account functionality."""
# pylint: disable=too-many-lines
from __future__ import annotations
import time
import logging
from typing import override
from bacommon.cloud import WebLocation
from bacommon.login import LoginType
import bacommon.cloud
import bauiv1 as bui
# These days we're directing people to the web based account settings
# for V2 account linking and trying to get them to disconnect remaining
# V1 links, but leaving this escape hatch here in case needed.
FORCE_ENABLE_V1_LINKING = False
[docs]
class AccountSettingsWindow(bui.MainWindow):
"""Window for account related functionality."""
def __init__(
self,
transition: str | None = 'in_right',
modal: bool = False,
origin_widget: bui.Widget | None = None,
close_once_signed_in: bool = False,
):
# pylint: disable=too-many-statements
plus = bui.app.plus
assert plus is not None
self._sign_in_v2_proxy_button: bui.Widget | None = None
self._sign_in_device_button: bui.Widget | None = None
self._show_legacy_unlink_button = False
self._signing_in_adapter: bui.LoginAdapter | None = None
self._close_once_signed_in = close_once_signed_in
bui.set_analytics_screen('Account Window')
self._explicitly_signed_out_of_gpgs = False
self._r = 'accountSettingsWindow'
self._modal = modal
self._needs_refresh = False
self._v1_signed_in = plus.get_v1_account_state() == 'signed_in'
self._v1_account_state_num = plus.get_v1_account_state_num()
self._check_sign_in_timer = bui.AppTimer(
1.0, bui.WeakCall(self._update), repeat=True
)
self._can_reset_achievements = False
app = bui.app
assert app.classic is not None
uiscale = app.ui_v1.uiscale
self._width = 850 if uiscale is bui.UIScale.SMALL else 660
x_offs = 70 if uiscale is bui.UIScale.SMALL else 0
self._height = (
380
if uiscale is bui.UIScale.SMALL
else 430 if uiscale is bui.UIScale.MEDIUM else 490
)
self._sign_in_button = None
self._sign_in_text = None
self._scroll_width = self._width - (100 + x_offs * 2)
self._scroll_height = self._height - 120
self._sub_width = self._scroll_width - 20
# Determine which sign-in/sign-out buttons we should show.
self._show_sign_in_buttons: list[str] = []
if LoginType.GPGS in plus.accounts.login_adapters:
self._show_sign_in_buttons.append('Google Play')
if LoginType.GAME_CENTER in plus.accounts.login_adapters:
self._show_sign_in_buttons.append('Game Center')
# Always want to show our web-based v2 login option.
self._show_sign_in_buttons.append('V2Proxy')
# Legacy v1 device accounts available only if the user
# has explicitly enabled deprecated login types.
if bui.app.config.resolve('Show Deprecated Login Types'):
self._show_sign_in_buttons.append('Device')
top_extra = 15 if uiscale is bui.UIScale.SMALL else 0
super().__init__(
root_widget=bui.containerwidget(
size=(self._width, self._height + top_extra),
# transition=transition,
toolbar_visibility=(
'menu_minimal'
if uiscale is bui.UIScale.SMALL
else 'menu_full'
),
scale=(
2.07
if uiscale is bui.UIScale.SMALL
else 1.4 if uiscale is bui.UIScale.MEDIUM else 1.0
),
stack_offset=(
(0, 8) if uiscale is bui.UIScale.SMALL else (0, 0)
),
),
transition=transition,
origin_widget=origin_widget,
)
if uiscale is bui.UIScale.SMALL:
self._back_button = None
bui.containerwidget(
edit=self._root_widget, on_cancel_call=self.main_window_back
)
else:
self._back_button = btn = bui.buttonwidget(
parent=self._root_widget,
position=(51 + x_offs, self._height - 62),
size=(120, 60),
scale=0.8,
text_scale=1.2,
autoselect=True,
label=bui.Lstr(
resource='doneText' if self._modal else 'backText'
),
button_type='regular' if self._modal else 'back',
on_activate_call=self.main_window_back,
)
bui.containerwidget(edit=self._root_widget, cancel_button=btn)
if not self._modal:
bui.buttonwidget(
edit=btn,
button_type='backSmall',
size=(60, 56),
label=bui.charstr(bui.SpecialChar.BACK),
)
titleyoffs = -12 if uiscale is bui.UIScale.SMALL else 0
titlescale = 0.6 if uiscale is bui.UIScale.SMALL else 1.0
bui.textwidget(
parent=self._root_widget,
position=(self._width * 0.5, self._height - 41 + titleyoffs),
size=(0, 0),
text=bui.Lstr(resource=f'{self._r}.titleText'),
color=app.ui_v1.title_color,
scale=titlescale,
maxwidth=self._width - 340,
h_align='center',
v_align='center',
)
self._scrollwidget = bui.scrollwidget(
parent=self._root_widget,
highlight=False,
position=(
(self._width - self._scroll_width) * 0.5,
self._height - 65 - self._scroll_height,
),
size=(self._scroll_width, self._scroll_height),
claims_left_right=True,
claims_tab=True,
selection_loops_to_parent=True,
)
self._subcontainer: bui.Widget | None = None
self._refresh()
self._restore_state()
[docs]
@override
def get_main_window_state(self) -> bui.MainWindowState:
# Support recreating our window for back/refresh purposes.
cls = type(self)
return bui.BasicMainWindowState(
create_call=lambda transition, origin_widget: cls(
transition=transition, origin_widget=origin_widget
)
)
[docs]
@override
def on_main_window_close(self) -> None:
self._save_state()
def _update(self) -> None:
plus = bui.app.plus
assert plus is not None
# If they want us to close once we're signed in, do so.
if self._close_once_signed_in and self._v1_signed_in:
self.main_window_back()
return
# Hmm should update this to use get_account_state_num.
# Theoretically if we switch from one signed-in account to another
# in the background this would break.
v1_account_state_num = plus.get_v1_account_state_num()
v1_account_state = plus.get_v1_account_state()
show_legacy_unlink_button = self._should_show_legacy_unlink_button()
if (
v1_account_state_num != self._v1_account_state_num
or show_legacy_unlink_button != self._show_legacy_unlink_button
or self._needs_refresh
):
self._v1_account_state_num = v1_account_state_num
self._v1_signed_in = v1_account_state == 'signed_in'
self._show_legacy_unlink_button = show_legacy_unlink_button
self._refresh()
# Go ahead and refresh some individual things
# that may change under us.
self._update_linked_accounts_text()
self._update_unlink_accounts_button()
self._refresh_campaign_progress_text()
self._refresh_achievements()
self._refresh_tickets_text()
self._refresh_account_name_text()
def _refresh(self) -> None:
# pylint: disable=too-many-statements
# pylint: disable=too-many-branches
# pylint: disable=too-many-locals
# pylint: disable=cyclic-import
plus = bui.app.plus
assert plus is not None
via_lines: list[str] = []
primary_v2_account = plus.accounts.primary
v1_state = plus.get_v1_account_state()
v1_account_type = (
plus.get_v1_account_type() if v1_state == 'signed_in' else 'unknown'
)
# We expose GPGS-specific functionality only if it is 'active'
# (meaning the current GPGS player matches one of our account's
# logins).
adapter = plus.accounts.login_adapters.get(LoginType.GPGS)
gpgs_active = adapter is not None and adapter.is_back_end_active()
# Ditto for Game Center.
adapter = plus.accounts.login_adapters.get(LoginType.GAME_CENTER)
game_center_active = (
adapter is not None and adapter.is_back_end_active()
)
show_signed_in_as = self._v1_signed_in
signed_in_as_space = 95.0
# To reduce confusion about the whole V2 account situation for
# people used to seeing their Google Play Games or Game Center
# account name and icon and whatnot, let's show those underneath
# the V2 tag to help communicate that they are in fact logged in
# through that account.
via_space = 25.0
if show_signed_in_as and bui.app.plus is not None:
accounts = bui.app.plus.accounts
if accounts.primary is not None:
# For these login types, we show 'via' IF there is a
# login of that type attached to our account AND it is
# currently active (We don't want to show 'via Game
# Center' if we're signed out of Game Center or
# currently running on Steam, even if there is a Game
# Center login attached to our account).
for ltype, lchar in [
(LoginType.GPGS, bui.SpecialChar.GOOGLE_PLAY_GAMES_LOGO),
(LoginType.GAME_CENTER, bui.SpecialChar.GAME_CENTER_LOGO),
]:
linfo = accounts.primary.logins.get(ltype)
ladapter = accounts.login_adapters.get(ltype)
if (
linfo is not None
and ladapter is not None
and ladapter.is_back_end_active()
):
via_lines.append(f'{bui.charstr(lchar)}{linfo.name}')
# TEMP TESTING
if bool(False):
icontxt = bui.charstr(bui.SpecialChar.GAME_CENTER_LOGO)
via_lines.append(f'{icontxt}FloofDibble')
icontxt = bui.charstr(
bui.SpecialChar.GOOGLE_PLAY_GAMES_LOGO
)
via_lines.append(f'{icontxt}StinkBobble')
show_sign_in_benefits = not self._v1_signed_in
sign_in_benefits_space = 80.0
show_signing_in_text = (
v1_state == 'signing_in' or self._signing_in_adapter is not None
)
signing_in_text_space = 80.0
show_google_play_sign_in_button = (
v1_state == 'signed_out'
and self._signing_in_adapter is None
and 'Google Play' in self._show_sign_in_buttons
)
show_game_center_sign_in_button = (
v1_state == 'signed_out'
and self._signing_in_adapter is None
and 'Game Center' in self._show_sign_in_buttons
)
show_v2_proxy_sign_in_button = (
v1_state == 'signed_out'
and self._signing_in_adapter is None
and 'V2Proxy' in self._show_sign_in_buttons
)
show_device_sign_in_button = (
v1_state == 'signed_out'
and self._signing_in_adapter is None
and 'Device' in self._show_sign_in_buttons
)
sign_in_button_space = 70.0
deprecated_space = 60
# Game Center currently has a single UI for everything.
show_game_service_button = game_center_active
game_service_button_space = 60.0
# Phasing this out (for V2 accounts at least).
show_linked_accounts_text = (
self._v1_signed_in and v1_account_type != 'V2'
)
linked_accounts_text_space = 60.0
# Update: No longer showing this since its visible on main
# toolbar.
show_achievements_text = False
achievements_text_space = 27.0
show_leaderboards_button = self._v1_signed_in and gpgs_active
leaderboards_button_space = 60.0
# Update: No longer showing this; trying to get progress type
# stuff out of the account panel.
# show_campaign_progress = self._v1_signed_in
show_campaign_progress = False
campaign_progress_space = 27.0
# show_tickets = self._v1_signed_in
show_tickets = False
tickets_space = 27.0
show_manage_account_button = primary_v2_account is not None
manage_account_button_space = 70.0
show_delete_account_button = primary_v2_account is not None
delete_account_button_space = 70.0
show_link_accounts_button = self._v1_signed_in and (
primary_v2_account is None or FORCE_ENABLE_V1_LINKING
)
link_accounts_button_space = 70.0
show_unlink_accounts_button = show_link_accounts_button
unlink_accounts_button_space = 90.0
# Phasing this out.
show_v2_link_info = False
v2_link_info_space = 70.0
legacy_unlink_button_space = 120.0
show_sign_out_button = primary_v2_account is not None or (
self._v1_signed_in and v1_account_type == 'Local'
)
sign_out_button_space = 70.0
# We can show cancel if we're either waiting on an adapter to
# provide us with v2 credentials or waiting for those
# credentials to be verified.
show_cancel_sign_in_button = self._signing_in_adapter is not None or (
plus.accounts.have_primary_credentials()
and primary_v2_account is None
)
cancel_sign_in_button_space = 70.0
if self._subcontainer is not None:
self._subcontainer.delete()
self._sub_height = 60.0
if show_signed_in_as:
self._sub_height += signed_in_as_space
self._sub_height += via_space * len(via_lines)
if show_signing_in_text:
self._sub_height += signing_in_text_space
if show_google_play_sign_in_button:
self._sub_height += sign_in_button_space
if show_game_center_sign_in_button:
self._sub_height += sign_in_button_space
if show_v2_proxy_sign_in_button:
self._sub_height += sign_in_button_space
if show_device_sign_in_button:
self._sub_height += sign_in_button_space + deprecated_space
if show_game_service_button:
self._sub_height += game_service_button_space
if show_linked_accounts_text:
self._sub_height += linked_accounts_text_space
if show_achievements_text:
self._sub_height += achievements_text_space
if show_leaderboards_button:
self._sub_height += leaderboards_button_space
if show_campaign_progress:
self._sub_height += campaign_progress_space
if show_tickets:
self._sub_height += tickets_space
if show_sign_in_benefits:
self._sub_height += sign_in_benefits_space
if show_manage_account_button:
self._sub_height += manage_account_button_space
if show_link_accounts_button:
self._sub_height += link_accounts_button_space
if show_unlink_accounts_button:
self._sub_height += unlink_accounts_button_space
if show_v2_link_info:
self._sub_height += v2_link_info_space
if self._show_legacy_unlink_button:
self._sub_height += legacy_unlink_button_space
if show_sign_out_button:
self._sub_height += sign_out_button_space
if show_delete_account_button:
self._sub_height += delete_account_button_space
if show_cancel_sign_in_button:
self._sub_height += cancel_sign_in_button_space
self._subcontainer = bui.containerwidget(
parent=self._scrollwidget,
size=(self._sub_width, self._sub_height),
background=False,
claims_left_right=True,
claims_tab=True,
selection_loops_to_parent=True,
)
first_selectable = None
v = self._sub_height - 10.0
assert bui.app.classic is not None
self._account_name_text: bui.Widget | None
if show_signed_in_as:
v -= signed_in_as_space * 0.2
txt = bui.Lstr(
resource='accountSettingsWindow.youAreSignedInAsText',
fallback_resource='accountSettingsWindow.youAreLoggedInAsText',
)
bui.textwidget(
parent=self._subcontainer,
position=(self._sub_width * 0.5, v),
size=(0, 0),
text=txt,
scale=0.9,
color=bui.app.ui_v1.title_color,
maxwidth=self._sub_width * 0.9,
h_align='center',
v_align='center',
)
v -= signed_in_as_space * 0.5
self._account_name_text = bui.textwidget(
parent=self._subcontainer,
position=(self._sub_width * 0.5, v),
size=(0, 0),
scale=1.5,
maxwidth=self._sub_width * 0.9,
res_scale=1.5,
color=(1, 1, 1, 1),
h_align='center',
v_align='center',
)
self._refresh_account_name_text()
v -= signed_in_as_space * 0.4
for via in via_lines:
v -= via_space * 0.1
sscale = 0.7
swidth = (
bui.get_string_width(via, suppress_warning=True) * sscale
)
bui.textwidget(
parent=self._subcontainer,
position=(self._sub_width * 0.5, v),
size=(0, 0),
text=via,
scale=sscale,
color=(0.6, 0.6, 0.6),
flatness=1.0,
shadow=0.0,
h_align='center',
v_align='center',
)
bui.textwidget(
parent=self._subcontainer,
position=(self._sub_width * 0.5 - swidth * 0.5 - 5, v),
size=(0, 0),
text=bui.Lstr(
value='(${VIA}',
subs=[('${VIA}', bui.Lstr(resource='viaText'))],
),
scale=0.5,
color=(0.4, 0.6, 0.4, 0.5),
flatness=1.0,
shadow=0.0,
h_align='right',
v_align='center',
)
bui.textwidget(
parent=self._subcontainer,
position=(self._sub_width * 0.5 + swidth * 0.5 + 10, v),
size=(0, 0),
text=')',
scale=0.5,
color=(0.4, 0.6, 0.4, 0.5),
flatness=1.0,
shadow=0.0,
h_align='right',
v_align='center',
)
v -= via_space * 0.9
else:
self._account_name_text = None
if self._back_button is None:
bbtn = bui.get_special_widget('back_button')
else:
bbtn = self._back_button
if show_sign_in_benefits:
v -= sign_in_benefits_space
bui.textwidget(
parent=self._subcontainer,
position=(
self._sub_width * 0.5,
v + sign_in_benefits_space * 0.4,
),
size=(0, 0),
text=bui.Lstr(resource=f'{self._r}.signInInfoText'),
max_height=sign_in_benefits_space * 0.9,
scale=0.9,
color=(0.75, 0.7, 0.8),
maxwidth=self._sub_width * 0.8,
h_align='center',
v_align='center',
)
if show_signing_in_text:
v -= signing_in_text_space
bui.textwidget(
parent=self._subcontainer,
position=(
self._sub_width * 0.5,
v + signing_in_text_space * 0.5,
),
size=(0, 0),
text=bui.Lstr(resource='accountSettingsWindow.signingInText'),
scale=0.9,
color=(0, 1, 0),
maxwidth=self._sub_width * 0.8,
h_align='center',
v_align='center',
)
if show_google_play_sign_in_button:
button_width = 350
v -= sign_in_button_space
self._sign_in_google_play_button = btn = bui.buttonwidget(
parent=self._subcontainer,
position=((self._sub_width - button_width) * 0.5, v - 20),
autoselect=True,
size=(button_width, 60),
label=bui.Lstr(
value='${A} ${B}',
subs=[
(
'${A}',
bui.charstr(bui.SpecialChar.GOOGLE_PLAY_GAMES_LOGO),
),
(
'${B}',
bui.Lstr(
resource=f'{self._r}.signInWithText',
subs=[
(
'${SERVICE}',
bui.Lstr(resource='googlePlayText'),
)
],
),
),
],
),
on_activate_call=lambda: self._sign_in_press(LoginType.GPGS),
)
if first_selectable is None:
first_selectable = btn
bui.widget(
edit=btn, right_widget=bui.get_special_widget('squad_button')
)
bui.widget(edit=btn, left_widget=bbtn)
bui.widget(edit=btn, show_buffer_bottom=40, show_buffer_top=100)
self._sign_in_text = None
if show_game_center_sign_in_button:
button_width = 350
v -= sign_in_button_space
self._sign_in_google_play_button = btn = bui.buttonwidget(
parent=self._subcontainer,
position=((self._sub_width - button_width) * 0.5, v - 20),
autoselect=True,
size=(button_width, 60),
# Note: Apparently Game Center is just called 'Game Center'
# in all languages. Can revisit if not true.
# https://developer.apple.com/forums/thread/725779
label=bui.Lstr(
value='${A} ${B}',
subs=[
(
'${A}',
bui.charstr(bui.SpecialChar.GAME_CENTER_LOGO),
),
(
'${B}',
bui.Lstr(
resource=f'{self._r}.signInWithText',
subs=[('${SERVICE}', 'Game Center')],
),
),
],
),
on_activate_call=lambda: self._sign_in_press(
LoginType.GAME_CENTER
),
)
if first_selectable is None:
first_selectable = btn
bui.widget(
edit=btn, right_widget=bui.get_special_widget('squad_button')
)
bui.widget(edit=btn, left_widget=bbtn)
bui.widget(edit=btn, show_buffer_bottom=40, show_buffer_top=100)
self._sign_in_text = None
if show_v2_proxy_sign_in_button:
button_width = 350
v -= sign_in_button_space
self._sign_in_v2_proxy_button = btn = bui.buttonwidget(
parent=self._subcontainer,
position=((self._sub_width - button_width) * 0.5, v - 20),
autoselect=True,
size=(button_width, 60),
label='',
on_activate_call=self._v2_proxy_sign_in_press,
)
v2labeltext: bui.Lstr | str = (
bui.Lstr(resource=f'{self._r}.signInWithAnEmailAddressText')
if show_game_center_sign_in_button
or show_google_play_sign_in_button
or show_device_sign_in_button
else bui.Lstr(resource=f'{self._r}.signInText')
)
v2infotext: bui.Lstr | str | None = None
bui.textwidget(
parent=self._subcontainer,
draw_controller=btn,
h_align='center',
v_align='center',
size=(0, 0),
position=(
self._sub_width * 0.5,
v + (17 if v2infotext is not None else 10),
),
text=bui.Lstr(
value='${A} ${B}',
subs=[
('${A}', bui.charstr(bui.SpecialChar.V2_LOGO)),
(
'${B}',
v2labeltext,
),
],
),
maxwidth=button_width * 0.8,
color=(0.75, 1.0, 0.7),
)
if v2infotext is not None:
bui.textwidget(
parent=self._subcontainer,
draw_controller=btn,
h_align='center',
v_align='center',
size=(0, 0),
position=(self._sub_width * 0.5, v - 4),
text=v2infotext,
flatness=1.0,
scale=0.57,
maxwidth=button_width * 0.9,
color=(0.55, 0.8, 0.5),
)
if first_selectable is None:
first_selectable = btn
bui.widget(
edit=btn, right_widget=bui.get_special_widget('squad_button')
)
bui.widget(edit=btn, left_widget=bbtn)
bui.widget(edit=btn, show_buffer_bottom=40, show_buffer_top=100)
self._sign_in_text = None
if show_device_sign_in_button:
button_width = 350
v -= sign_in_button_space + deprecated_space
self._sign_in_device_button = btn = bui.buttonwidget(
parent=self._subcontainer,
position=((self._sub_width - button_width) * 0.5, v - 20),
autoselect=True,
size=(button_width, 60),
label='',
on_activate_call=lambda: self._sign_in_press('Local'),
)
bui.textwidget(
parent=self._subcontainer,
h_align='center',
v_align='center',
size=(0, 0),
position=(self._sub_width * 0.5, v + 60),
text=bui.Lstr(resource='deprecatedText'),
scale=0.8,
maxwidth=300,
color=(0.6, 0.55, 0.45),
)
bui.textwidget(
parent=self._subcontainer,
draw_controller=btn,
h_align='center',
v_align='center',
size=(0, 0),
position=(self._sub_width * 0.5, v + 17),
text=bui.Lstr(
value='${A} ${B}',
subs=[
('${A}', bui.charstr(bui.SpecialChar.LOCAL_ACCOUNT)),
(
'${B}',
bui.Lstr(
resource=f'{self._r}.signInWithDeviceText'
),
),
],
),
maxwidth=button_width * 0.8,
color=(0.75, 1.0, 0.7),
)
bui.textwidget(
parent=self._subcontainer,
draw_controller=btn,
h_align='center',
v_align='center',
size=(0, 0),
position=(self._sub_width * 0.5, v - 4),
text=bui.Lstr(resource=f'{self._r}.signInWithDeviceInfoText'),
flatness=1.0,
scale=0.57,
maxwidth=button_width * 0.9,
color=(0.55, 0.8, 0.5),
)
if first_selectable is None:
first_selectable = btn
bui.widget(
edit=btn, right_widget=bui.get_special_widget('squad_button')
)
bui.widget(edit=btn, left_widget=bbtn)
bui.widget(edit=btn, show_buffer_bottom=40, show_buffer_top=100)
self._sign_in_text = None
if show_manage_account_button:
button_width = 300
v -= manage_account_button_space
self._manage_button = btn = bui.buttonwidget(
parent=self._subcontainer,
position=((self._sub_width - button_width) * 0.5, v),
autoselect=True,
size=(button_width, 60),
label=bui.Lstr(resource=f'{self._r}.manageAccountText'),
color=(0.55, 0.5, 0.6),
icon=bui.gettexture('settingsIcon'),
textcolor=(0.75, 0.7, 0.8),
on_activate_call=bui.WeakCall(self._on_manage_account_press),
)
if first_selectable is None:
first_selectable = btn
bui.widget(
edit=btn, right_widget=bui.get_special_widget('squad_button')
)
bui.widget(edit=btn, left_widget=bbtn)
# the button to go to OS-Specific leaderboards/high-score-lists/etc.
if show_game_service_button:
button_width = 300
v -= game_service_button_space * 0.6
if game_center_active:
# Note: Apparently Game Center is just called 'Game Center'
# in all languages. Can revisit if not true.
# https://developer.apple.com/forums/thread/725779
game_service_button_label = bui.Lstr(
value=bui.charstr(bui.SpecialChar.GAME_CENTER_LOGO)
+ 'Game Center'
)
else:
raise ValueError(
"unknown account type: '" + str(v1_account_type) + "'"
)
self._game_service_button = btn = bui.buttonwidget(
parent=self._subcontainer,
position=((self._sub_width - button_width) * 0.5, v),
color=(0.55, 0.5, 0.6),
textcolor=(0.75, 0.7, 0.8),
autoselect=True,
on_activate_call=self._on_game_service_button_press,
size=(button_width, 50),
label=game_service_button_label,
)
if first_selectable is None:
first_selectable = btn
bui.widget(
edit=btn, right_widget=bui.get_special_widget('squad_button')
)
bui.widget(edit=btn, left_widget=bbtn)
v -= game_service_button_space * 0.4
else:
self.game_service_button = None
self._achievements_text: bui.Widget | None
if show_achievements_text:
v -= achievements_text_space * 0.5
self._achievements_text = bui.textwidget(
parent=self._subcontainer,
position=(self._sub_width * 0.5, v),
size=(0, 0),
scale=0.9,
color=(0.75, 0.7, 0.8),
maxwidth=self._sub_width * 0.8,
h_align='center',
v_align='center',
)
v -= achievements_text_space * 0.5
else:
self._achievements_text = None
if show_achievements_text:
self._refresh_achievements()
self._leaderboards_button: bui.Widget | None
if show_leaderboards_button:
button_width = 300
v -= leaderboards_button_space * 0.85
self._leaderboards_button = btn = bui.buttonwidget(
parent=self._subcontainer,
position=((self._sub_width - button_width) * 0.5, v),
color=(0.55, 0.5, 0.6),
textcolor=(0.75, 0.7, 0.8),
autoselect=True,
icon=bui.gettexture('googlePlayLeaderboardsIcon'),
icon_color=(0.8, 0.95, 0.7),
on_activate_call=self._on_leaderboards_press,
size=(button_width, 50),
label=bui.Lstr(resource='leaderboardsText'),
)
if first_selectable is None:
first_selectable = btn
bui.widget(
edit=btn, right_widget=bui.get_special_widget('squad_button')
)
bui.widget(edit=btn, left_widget=bbtn)
v -= leaderboards_button_space * 0.15
else:
self._leaderboards_button = None
self._campaign_progress_text: bui.Widget | None
if show_campaign_progress:
v -= campaign_progress_space * 0.5
self._campaign_progress_text = bui.textwidget(
parent=self._subcontainer,
position=(self._sub_width * 0.5, v),
size=(0, 0),
scale=0.9,
color=(0.75, 0.7, 0.8),
maxwidth=self._sub_width * 0.8,
h_align='center',
v_align='center',
)
v -= campaign_progress_space * 0.5
self._refresh_campaign_progress_text()
else:
self._campaign_progress_text = None
self._tickets_text: bui.Widget | None
if show_tickets:
v -= tickets_space * 0.5
self._tickets_text = bui.textwidget(
parent=self._subcontainer,
position=(self._sub_width * 0.5, v),
size=(0, 0),
scale=0.9,
color=(0.75, 0.7, 0.8),
maxwidth=self._sub_width * 0.8,
flatness=1.0,
h_align='center',
v_align='center',
)
v -= tickets_space * 0.5
self._refresh_tickets_text()
else:
self._tickets_text = None
# bit of spacing before the reset/sign-out section
# v -= 5
button_width = 300
self._linked_accounts_text: bui.Widget | None
if show_linked_accounts_text:
v -= linked_accounts_text_space * 0.8
self._linked_accounts_text = bui.textwidget(
parent=self._subcontainer,
position=(self._sub_width * 0.5, v),
size=(0, 0),
scale=0.9,
color=(0.75, 0.7, 0.8),
maxwidth=self._sub_width * 0.95,
text=bui.Lstr(resource=f'{self._r}.linkedAccountsText'),
h_align='center',
v_align='center',
)
v -= linked_accounts_text_space * 0.2
self._update_linked_accounts_text()
else:
self._linked_accounts_text = None
# Show link/unlink buttons only for V1 accounts.
if show_link_accounts_button:
v -= link_accounts_button_space
self._link_accounts_button = btn = bui.buttonwidget(
parent=self._subcontainer,
position=((self._sub_width - button_width) * 0.5, v),
autoselect=True,
size=(button_width, 60),
label='',
color=(0.55, 0.5, 0.6),
on_activate_call=self._link_accounts_press,
)
bui.textwidget(
parent=self._subcontainer,
draw_controller=btn,
h_align='center',
v_align='center',
size=(0, 0),
position=(self._sub_width * 0.5, v + 17 + 20),
text=bui.Lstr(resource=f'{self._r}.linkAccountsText'),
maxwidth=button_width * 0.8,
color=(0.75, 0.7, 0.8),
)
bui.textwidget(
parent=self._subcontainer,
draw_controller=btn,
h_align='center',
v_align='center',
size=(0, 0),
position=(self._sub_width * 0.5, v - 4 + 20),
text=bui.Lstr(resource=f'{self._r}.linkAccountsInfoText'),
flatness=1.0,
scale=0.5,
maxwidth=button_width * 0.8,
color=(0.75, 0.7, 0.8),
)
if first_selectable is None:
first_selectable = btn
bui.widget(
edit=btn, right_widget=bui.get_special_widget('squad_button')
)
bui.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=50)
self._unlink_accounts_button: bui.Widget | None
if show_unlink_accounts_button:
v -= unlink_accounts_button_space
self._unlink_accounts_button = btn = bui.buttonwidget(
parent=self._subcontainer,
position=((self._sub_width - button_width) * 0.5, v + 25),
autoselect=True,
size=(button_width, 60),
label='',
color=(0.55, 0.5, 0.6),
on_activate_call=self._unlink_accounts_press,
)
self._unlink_accounts_button_label = bui.textwidget(
parent=self._subcontainer,
draw_controller=btn,
h_align='center',
v_align='center',
size=(0, 0),
position=(self._sub_width * 0.5, v + 55),
text=bui.Lstr(resource=f'{self._r}.unlinkAccountsText'),
maxwidth=button_width * 0.8,
color=(0.75, 0.7, 0.8),
)
if first_selectable is None:
first_selectable = btn
bui.widget(
edit=btn, right_widget=bui.get_special_widget('squad_button')
)
bui.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=50)
self._update_unlink_accounts_button()
else:
self._unlink_accounts_button = None
if show_v2_link_info:
v -= v2_link_info_space
bui.textwidget(
parent=self._subcontainer,
h_align='center',
v_align='center',
size=(0, 0),
position=(self._sub_width * 0.5, v + v2_link_info_space - 20),
text=bui.Lstr(resource='v2AccountLinkingInfoText'),
flatness=1.0,
scale=0.8,
maxwidth=450,
color=(0.5, 0.45, 0.55),
)
if self._show_legacy_unlink_button:
v -= legacy_unlink_button_space
button_width_w = button_width * 1.5
bui.textwidget(
parent=self._subcontainer,
position=(self._sub_width * 0.5 - 150.0, v + 75),
size=(300.0, 60),
text=bui.Lstr(resource='whatIsThisText'),
scale=0.8,
color=(0.3, 0.7, 0.05),
maxwidth=200.0,
h_align='center',
v_align='center',
autoselect=True,
selectable=True,
on_activate_call=show_what_is_legacy_unlinking_page,
click_activate=True,
)
btn = bui.buttonwidget(
parent=self._subcontainer,
position=((self._sub_width - button_width_w) * 0.5, v + 25),
autoselect=True,
size=(button_width_w, 60),
label=bui.Lstr(
resource=f'{self._r}.unlinkLegacyV1AccountsText'
),
textcolor=(0.8, 0.4, 0),
color=(0.55, 0.5, 0.6),
on_activate_call=self._unlink_accounts_press,
)
if show_sign_out_button:
v -= sign_out_button_space
self._sign_out_button = btn = bui.buttonwidget(
parent=self._subcontainer,
position=((self._sub_width - button_width) * 0.5, v),
size=(button_width, 60),
label=bui.Lstr(resource=f'{self._r}.signOutText'),
color=(0.55, 0.5, 0.6),
textcolor=(0.75, 0.7, 0.8),
autoselect=True,
on_activate_call=self._sign_out_press,
)
if first_selectable is None:
first_selectable = btn
bui.widget(
edit=btn, right_widget=bui.get_special_widget('squad_button')
)
bui.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=15)
if show_cancel_sign_in_button:
v -= cancel_sign_in_button_space
self._cancel_sign_in_button = btn = bui.buttonwidget(
parent=self._subcontainer,
position=((self._sub_width - button_width) * 0.5, v),
size=(button_width, 60),
label=bui.Lstr(resource='cancelText'),
color=(0.55, 0.5, 0.6),
textcolor=(0.75, 0.7, 0.8),
autoselect=True,
on_activate_call=self._cancel_sign_in_press,
)
if first_selectable is None:
first_selectable = btn
bui.widget(
edit=btn, right_widget=bui.get_special_widget('squad_button')
)
bui.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=15)
if show_delete_account_button:
v -= delete_account_button_space
self._delete_account_button = btn = bui.buttonwidget(
parent=self._subcontainer,
position=((self._sub_width - button_width) * 0.5, v),
size=(button_width, 60),
label=bui.Lstr(resource=f'{self._r}.deleteAccountText'),
color=(0.85, 0.5, 0.6),
textcolor=(0.9, 0.7, 0.8),
autoselect=True,
on_activate_call=self._on_delete_account_press,
)
if first_selectable is None:
first_selectable = btn
bui.widget(
edit=btn, right_widget=bui.get_special_widget('squad_button')
)
bui.widget(edit=btn, left_widget=bbtn, show_buffer_bottom=15)
# Whatever the topmost selectable thing is, we want it to scroll all
# the way up when we select it.
if first_selectable is not None:
bui.widget(
edit=first_selectable, up_widget=bbtn, show_buffer_top=400
)
# (this should re-scroll us to the top..)
bui.containerwidget(
edit=self._subcontainer, visible_child=first_selectable
)
self._needs_refresh = False
def _on_game_service_button_press(self) -> None:
if bui.app.plus is not None:
bui.app.plus.show_game_service_ui()
else:
logging.warning(
'game-service-ui not available without plus feature-set.'
)
def _on_custom_achievements_press(self) -> None:
if bui.app.plus is not None:
bui.apptimer(
0.15,
bui.Call(bui.app.plus.show_game_service_ui, 'achievements'),
)
else:
logging.warning('show_game_service_ui requires plus feature-set.')
def _on_manage_account_press(self) -> None:
self._do_manage_account_press(WebLocation.ACCOUNT_EDITOR)
def _on_delete_account_press(self) -> None:
self._do_manage_account_press(WebLocation.ACCOUNT_DELETE_SECTION)
def _do_manage_account_press(self, weblocation: WebLocation) -> None:
plus = bui.app.plus
assert plus is not None
# Preemptively fail if it looks like we won't be able to talk to
# the server anyway.
if not plus.cloud.connected:
bui.screenmessage(
bui.Lstr(resource='internal.unavailableNoConnectionText'),
color=(1, 0, 0),
)
bui.getsound('error').play()
return
bui.screenmessage(bui.Lstr(resource='oneMomentText'))
# We expect to have a v2 account signed in if we get here.
if plus.accounts.primary is None:
logging.exception(
'got manage-account press without v2 account present'
)
return
with plus.accounts.primary:
plus.cloud.send_message_cb(
bacommon.cloud.ManageAccountMessage(weblocation=weblocation),
on_response=bui.WeakCall(self._on_manage_account_response),
)
def _on_manage_account_response(
self, response: bacommon.cloud.ManageAccountResponse | Exception
) -> None:
if isinstance(response, Exception) or response.url is None:
logging.warning(
'Got error in manage-account-response: %s.', response
)
bui.screenmessage(bui.Lstr(resource='errorText'), color=(1, 0, 0))
bui.getsound('error').play()
return
bui.open_url(response.url)
def _on_leaderboards_press(self) -> None:
if bui.app.plus is not None:
bui.apptimer(
0.15,
bui.Call(bui.app.plus.show_game_service_ui, 'leaderboards'),
)
else:
logging.warning('show_game_service_ui requires classic')
def _have_unlinkable_v1_accounts(self) -> bool:
plus = bui.app.plus
assert plus is not None
# if this is not present, we haven't had contact from the server so
# let's not proceed..
if plus.get_v1_account_public_login_id() is None:
return False
accounts = plus.get_v1_account_misc_read_val_2('linkedAccounts', [])
return len(accounts) > 1
def _update_unlink_accounts_button(self) -> None:
if self._unlink_accounts_button is None:
return
if self._have_unlinkable_v1_accounts():
clr = (0.75, 0.7, 0.8, 1.0)
else:
clr = (1.0, 1.0, 1.0, 0.25)
bui.textwidget(edit=self._unlink_accounts_button_label, color=clr)
def _should_show_legacy_unlink_button(self) -> bool:
plus = bui.app.plus
assert plus is not None
# Only show this when fully signed in to a v2 account.
if not self._v1_signed_in or plus.accounts.primary is None:
return False
out = self._have_unlinkable_v1_accounts()
return out
def _update_linked_accounts_text(self) -> None:
plus = bui.app.plus
assert plus is not None
if self._linked_accounts_text is None:
return
# Disable this by default when signed in to a V2 account
# (since this shows V1 links which we should no longer care about).
if plus.accounts.primary is not None and not FORCE_ENABLE_V1_LINKING:
return
# if this is not present, we haven't had contact from the server so
# let's not proceed..
if plus.get_v1_account_public_login_id() is None:
num = int(time.time()) % 4
accounts_str = num * '.' + (4 - num) * ' '
else:
accounts = plus.get_v1_account_misc_read_val_2('linkedAccounts', [])
# UPDATE - we now just print the number here; not the actual
# accounts (they can see that in the unlink section if they're
# curious)
accounts_str = str(max(0, len(accounts) - 1))
bui.textwidget(
edit=self._linked_accounts_text,
text=bui.Lstr(
value='${L} ${A}',
subs=[
(
'${L}',
bui.Lstr(resource=f'{self._r}.linkedAccountsText'),
),
('${A}', accounts_str),
],
),
)
def _refresh_campaign_progress_text(self) -> None:
if self._campaign_progress_text is None:
return
p_str: str | bui.Lstr
try:
assert bui.app.classic is not None
campaign = bui.app.classic.getcampaign('Default')
levels = campaign.levels
levels_complete = sum((1 if l.complete else 0) for l in levels)
# Last level cant be completed; hence the -1;
progress = min(1.0, float(levels_complete) / (len(levels) - 1))
p_str = bui.Lstr(
resource=f'{self._r}.campaignProgressText',
subs=[('${PROGRESS}', str(int(progress * 100.0)) + '%')],
)
except Exception:
p_str = '?'
logging.exception('Error calculating co-op campaign progress.')
bui.textwidget(edit=self._campaign_progress_text, text=p_str)
def _refresh_tickets_text(self) -> None:
plus = bui.app.plus
assert plus is not None
if self._tickets_text is None:
return
try:
tc_str = str(plus.get_v1_account_ticket_count())
except Exception:
logging.exception('error refreshing tickets text')
tc_str = '-'
bui.textwidget(
edit=self._tickets_text,
text=bui.Lstr(
resource=f'{self._r}.ticketsText', subs=[('${COUNT}', tc_str)]
),
)
def _refresh_account_name_text(self) -> None:
plus = bui.app.plus
assert plus is not None
if self._account_name_text is None:
return
try:
name_str = plus.get_v1_account_display_string()
except Exception:
logging.exception('error refreshing tickets text')
name_str = '??'
bui.textwidget(edit=self._account_name_text, text=name_str)
def _refresh_achievements(self) -> None:
assert bui.app.classic is not None
if self._achievements_text is None:
return
complete = sum(
1 if a.complete else 0 for a in bui.app.classic.ach.achievements
)
total = len(bui.app.classic.ach.achievements)
txt_final = bui.Lstr(
resource=f'{self._r}.achievementProgressText',
subs=[('${COUNT}', str(complete)), ('${TOTAL}', str(total))],
)
if self._achievements_text is not None:
bui.textwidget(edit=self._achievements_text, text=txt_final)
def _link_accounts_press(self) -> None:
# pylint: disable=cyclic-import
from bauiv1lib.account.link import AccountLinkWindow
AccountLinkWindow(origin_widget=self._link_accounts_button)
def _unlink_accounts_press(self) -> None:
# pylint: disable=cyclic-import
from bauiv1lib.account.unlink import AccountUnlinkWindow
if not self._have_unlinkable_v1_accounts():
bui.getsound('error').play()
return
AccountUnlinkWindow(origin_widget=self._unlink_accounts_button)
def _cancel_sign_in_press(self) -> None:
# If we're waiting on an adapter to give us credentials, abort.
self._signing_in_adapter = None
plus = bui.app.plus
assert plus is not None
# Say we don't wanna be signed in anymore if we are.
plus.accounts.set_primary_credentials(None)
self._needs_refresh = True
# Speed UI updates along.
bui.apptimer(0.1, bui.WeakCall(self._update))
def _sign_out_press(self) -> None:
plus = bui.app.plus
assert plus is not None
if plus.accounts.have_primary_credentials():
if (
plus.accounts.primary is not None
and LoginType.GPGS in plus.accounts.primary.logins
):
self._explicitly_signed_out_of_gpgs = True
plus.accounts.set_primary_credentials(None)
else:
plus.sign_out_v1()
cfg = bui.app.config
# Also take note that its our *explicit* intention to not be
# signed in at this point (affects v1 accounts).
cfg['Auto Account State'] = 'signed_out'
cfg.commit()
bui.buttonwidget(
edit=self._sign_out_button,
label=bui.Lstr(resource=f'{self._r}.signingOutText'),
)
# Speed UI updates along.
bui.apptimer(0.1, bui.WeakCall(self._update))
def _sign_in_press(self, login_type: str | LoginType) -> None:
from bauiv1lib.connectivity import wait_for_connectivity
# If we're still waiting for our master-server connection,
# keep the user informed of this instead of rushing in and
# failing immediately.
wait_for_connectivity(on_connected=lambda: self._sign_in(login_type))
def _sign_in(self, login_type: str | LoginType) -> None:
plus = bui.app.plus
assert plus is not None
# V1 login types are strings.
if isinstance(login_type, str):
plus.sign_in_v1(login_type)
# Make note of the type account we're *wanting*
# to be signed in with.
cfg = bui.app.config
cfg['Auto Account State'] = login_type
cfg.commit()
self._needs_refresh = True
bui.apptimer(0.1, bui.WeakCall(self._update))
return
# V2 login sign-in buttons generally go through adapters.
adapter = plus.accounts.login_adapters.get(login_type)
if adapter is not None:
self._signing_in_adapter = adapter
adapter.sign_in(
result_cb=bui.WeakCall(self._on_adapter_sign_in_result),
description='account settings button',
)
# Will get 'Signing in...' to show.
self._needs_refresh = True
bui.apptimer(0.1, bui.WeakCall(self._update))
else:
bui.screenmessage(f'Unsupported login_type: {login_type.name}')
def _on_adapter_sign_in_result(
self,
adapter: bui.LoginAdapter,
result: bui.LoginAdapter.SignInResult | Exception,
) -> None:
is_us = self._signing_in_adapter is adapter
# If this isn't our current one we don't care.
if not is_us:
return
# If it is us, note that we're done.
self._signing_in_adapter = None
if isinstance(result, Exception):
# For now just make a bit of noise if anything went wrong;
# can get more specific as needed later.
logging.warning('Got error in v2 sign-in result: %s', result)
bui.screenmessage(
bui.Lstr(resource='internal.signInNoConnectionText'),
color=(1, 0, 0),
)
bui.getsound('error').play()
else:
# Success! Plug in these credentials which will begin
# verifying them and set our primary account-handle when
# finished.
plus = bui.app.plus
assert plus is not None
plus.accounts.set_primary_credentials(result.credentials)
# Special case - if the user has explicitly signed out and
# signed in again with GPGS via this button, warn them that
# they need to use the app if they want to switch to a
# different GPGS account.
if (
self._explicitly_signed_out_of_gpgs
and adapter.login_type is LoginType.GPGS
):
# Delay this slightly so it hopefully pops up after
# credentials go through and the account name shows up.
bui.apptimer(
1.5,
bui.Call(
bui.screenmessage,
bui.Lstr(
resource=self._r
+ '.googlePlayGamesAccountSwitchText'
),
),
)
# Speed any UI updates along.
self._needs_refresh = True
bui.apptimer(0.1, bui.WeakCall(self._update))
def _v2_proxy_sign_in_press(self) -> None:
# pylint: disable=cyclic-import
from bauiv1lib.connectivity import wait_for_connectivity
# If we're still waiting for our master-server connection, keep
# the user informed of this instead of rushing in and failing
# immediately.
wait_for_connectivity(on_connected=self._v2_proxy_sign_in)
def _v2_proxy_sign_in(self) -> None:
# pylint: disable=cyclic-import
from bauiv1lib.account.v2proxy import V2ProxySignInWindow
assert self._sign_in_v2_proxy_button is not None
V2ProxySignInWindow(origin_widget=self._sign_in_v2_proxy_button)
def _save_state(self) -> None:
try:
sel = self._root_widget.get_selected_child()
if sel == self._back_button:
sel_name = 'Back'
elif sel == self._scrollwidget:
sel_name = 'Scroll'
else:
raise ValueError('unrecognized selection')
assert bui.app.classic is not None
bui.app.ui_v1.window_states[type(self)] = sel_name
except Exception:
logging.exception('Error saving state for %s.', self)
def _restore_state(self) -> None:
try:
assert bui.app.classic is not None
sel_name = bui.app.ui_v1.window_states.get(type(self))
if sel_name == 'Back':
sel = self._back_button
elif sel_name == 'Scroll':
sel = self._scrollwidget
else:
sel = self._back_button
bui.containerwidget(edit=self._root_widget, selected_child=sel)
except Exception:
logging.exception('Error restoring state for %s.', self)
[docs]
def show_what_is_legacy_unlinking_page() -> None:
"""Show the webpage describing legacy unlinking."""
plus = bui.app.plus
assert plus is not None
bamasteraddr = plus.get_master_server_address(version=2)
bui.open_url(f'{bamasteraddr}/whatarev1links')