# 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
from bauiv1lib.connectivity import wait_for_connectivity
# 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',
origin_widget: bui.Widget | None = None,
close_once_signed_in: bool = False,
):
# pylint: disable=too-many-statements
# pylint: disable=too-many-locals
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._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
self._recreate_suppress: bui.MainWindowAutoRecreateSuppress | None = (
None
)
app = bui.app
assert app.classic is not None
uiscale = app.ui_v1.uiscale
self._width = 980 if uiscale is bui.UIScale.SMALL else 660
self._height = (
600
if uiscale is bui.UIScale.SMALL
else 430 if uiscale is bui.UIScale.MEDIUM else 490
)
# Do some fancy math to fill all available screen area up to the
# size of our backing container. This lets us fit to the exact
# screen shape at small ui scale.
screensize = bui.get_virtual_screen_size()
scale = (
1.9
if uiscale is bui.UIScale.SMALL
else 1.4 if uiscale is bui.UIScale.MEDIUM else 1.0
)
# Calc screen size in our local container space and clamp to a
# bit smaller than our container size.
target_width = min(self._width - 80, screensize[0] / scale)
target_height = min(self._height - 80, screensize[1] / scale)
# To get top/left coords, go to the center of our window and
# offset by half the width/height of our target area.
yoffs = 0.5 * self._height + 0.5 * target_height + 30.0
self._scroll_width = target_width
self._scroll_height = target_height - 33
scroll_bottom = yoffs - 61 - self._scroll_height
self._sign_in_button = None
self._sign_in_text = None
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')
super().__init__(
root_widget=bui.containerwidget(
size=(self._width, self._height),
toolbar_visibility=(
'menu_minimal'
if uiscale is bui.UIScale.SMALL
else 'menu_full'
),
scale=scale,
),
transition=transition,
origin_widget=origin_widget,
# We're affected by screen size only at small ui-scale.
refresh_on_screen_size_changes=uiscale is bui.UIScale.SMALL,
)
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, yoffs - 52.0),
size=(120, 60),
scale=0.8,
text_scale=1.2,
autoselect=True,
label=bui.Lstr(resource='backText'),
button_type='back',
on_activate_call=self.main_window_back,
)
bui.containerwidget(edit=self._root_widget, cancel_button=btn)
bui.buttonwidget(
edit=btn,
button_type='backSmall',
size=(60, 56),
label=bui.charstr(bui.SpecialChar.BACK),
)
titleyoffs = -45.0 if uiscale is bui.UIScale.SMALL else -28.0
titlescale = 0.7 if uiscale is bui.UIScale.SMALL else 1.0
bui.textwidget(
parent=self._root_widget,
position=(
self._width * 0.5,
yoffs + 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,
size=(self._scroll_width, self._scroll_height),
position=(
self._width * 0.5 - self._scroll_width * 0.5,
scroll_bottom,
),
claims_left_right=True,
selection_loops_to_parent=True,
border_opacity=0.4,
)
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_center_button = game_center_active
game_center_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_create_account_button = show_v2_proxy_sign_in_button
create_account_button_space = 70.0
# Apple asks us to make a delete-account button directly
# available in the UI. Currently disabling this elsewhere
# however as I feel that poking 'Manage Account' and scrolling
# down to 'Delete Account' is not hard to find.
show_delete_account_button = primary_v2_account is not None and (
bui.app.classic is not None
and bui.app.classic.platform == 'mac'
and bui.app.classic.subplatform == 'appstore'
)
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_v1_obsolete_note = self._v1_signed_in and (
primary_v2_account is None
)
v1_obsolete_note_space = 80.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_center_button:
self._sub_height += game_center_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_create_account_button:
self._sub_height += create_account_button_space
if show_link_accounts_button:
self._sub_height += link_accounts_button_space
if show_v1_obsolete_note:
self._sub_height += v1_obsolete_note_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,
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_v1_obsolete_note:
v -= v1_obsolete_note_space
bui.textwidget(
parent=self._subcontainer,
h_align='center',
v_align='center',
size=(0, 0),
position=(self._sub_width * 0.5, v + 35.0),
text=(
'YOU ARE SIGNED IN WITH A V1 ACCOUNT.\n'
'THESE ARE NO LONGER SUPPORTED AND MANY\n'
'FEATURES WILL NOT WORK. PLEASE SWITCH TO\n'
'A V2 ACCOUNT OR UPGRADE THIS ONE.'
),
maxwidth=self._sub_width * 0.8,
color=(1, 0, 0),
shadow=1.0,
flatness=1.0,
)
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)
if show_create_account_button:
button_width = 300
v -= create_account_button_space
self._create_button = btn = bui.buttonwidget(
parent=self._subcontainer,
position=((self._sub_width - button_width) * 0.5, v - 30),
autoselect=True,
size=(button_width, 60),
label=bui.Lstr(resource=f'{self._r}.createAnAccountText'),
color=(0.55, 0.5, 0.6),
textcolor=(0.75, 0.7, 0.8),
on_activate_call=bui.WeakCall(self._on_create_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_center_button:
button_width = 300
v -= game_center_button_space * 1.0
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_center_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_center_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_center_button_press,
size=(button_width, 50),
label=game_center_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_center_button_space * 0.4
else:
self.game_center_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
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)
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_center_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_create_account_press(self) -> None:
bui.open_url('https://ballistica.net/createaccount')
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:
# 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._do_manage_account(weblocation)
)
def _do_manage_account(self, weblocation: WebLocation) -> None:
plus = bui.app.plus
assert plus is not None
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
if plus is None:
return False
# 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:
# Any time we initiate a sign in, turn off auto-recreates for
# the remainder of our existence. We want to make sure we stick
# around to handle the entire sign-in flow, and things like
# system sign-in dialogs could potentially cause toolbars to
# show/hide or other things that could affect window dimensions
# and thus trigger window recreates which could interrupt our
# process.
self._recreate_suppress = bui.MainWindowAutoRecreateSuppress()
# 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:
# Any time we initiate a sign in, turn off auto-recreates for
# the remainder of our existence. We want to make sure we stick
# around to handle the entire sign-in flow, and things like
# system sign-in dialogs could potentially cause toolbars to
# show/hide or other things that could affect window dimensions
# and thus trigger window recreates which could interrupt our
# process.
self._recreate_suppress = bui.MainWindowAutoRecreateSuppress()
# 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')
# Docs-generation hack; import some stuff that we likely only forward-declared
# in our actual source code so that docs tools can find it.
from typing import (Coroutine, Any, Literal, Callable,
Generator, Awaitable, Sequence, Self)
import asyncio
from concurrent.futures import Future
from pathlib import Path
from enum import Enum