# Released under the MIT License. See LICENSE for details.
#
"""Provides a popup for displaying info about any account."""
from __future__ import annotations
from typing import TYPE_CHECKING, override
import logging
import bauiv1 as bui
from bauiv1lib.popup import PopupWindow, PopupMenuWindow
if TYPE_CHECKING:
from typing import Any
from bauiv1lib.popup import PopupMenu
[docs]
class AccountViewerWindow(PopupWindow):
"""Popup window that displays info for an account."""
def __init__(
self,
account_id: str,
*,
profile_id: str | None = None,
position: tuple[float, float] = (0.0, 0.0),
scale: float | None = None,
offset: tuple[float, float] = (0.0, 0.0),
):
if bui.app.classic is None:
raise RuntimeError('This requires classic support.')
plus = bui.app.plus
assert plus is not None
self._account_id = account_id
self._profile_id = profile_id
uiscale = bui.app.ui_v1.uiscale
if scale is None:
scale = (
2.6
if uiscale is bui.UIScale.SMALL
else 1.8 if uiscale is bui.UIScale.MEDIUM else 1.4
)
self._transitioning_out = False
self._width = 400
self._height = (
300
if uiscale is bui.UIScale.SMALL
else 400 if uiscale is bui.UIScale.MEDIUM else 450
)
self._subcontainer: bui.Widget | None = None
bg_color = (0.5, 0.4, 0.6)
# Creates our _root_widget.
super().__init__(
position=position,
size=(self._width, self._height),
scale=scale,
bg_color=bg_color,
offset=offset,
)
self._cancel_button = bui.buttonwidget(
parent=self.root_widget,
position=(50, self._height - 30),
size=(50, 50),
scale=0.5,
label='',
color=bg_color,
on_activate_call=self._on_cancel_press,
autoselect=True,
icon=bui.gettexture('crossOut'),
iconscale=1.2,
)
self._title_text = bui.textwidget(
parent=self.root_widget,
position=(self._width * 0.5, self._height - 20),
size=(0, 0),
h_align='center',
v_align='center',
scale=0.6,
text=bui.Lstr(resource='playerInfoText'),
maxwidth=200,
color=bui.app.ui_v1.title_color,
)
self._scrollwidget = bui.scrollwidget(
parent=self.root_widget,
size=(self._width - 60, self._height - 70),
position=(30, 30),
capture_arrows=True,
simple_culling_v=10,
border_opacity=0.4,
)
bui.widget(edit=self._scrollwidget, autoselect=True)
# Note to self: Make sure to always update loading text and
# spinner visibility together.
self._loading_text = bui.textwidget(
parent=self._scrollwidget,
scale=0.5,
text='',
size=(self._width - 60, 100),
h_align='center',
v_align='center',
)
self._loading_spinner = bui.spinnerwidget(
parent=self.root_widget,
position=(self._width * 0.5, self._height * 0.5),
style='bomb',
size=48,
)
# In cases where the user most likely has a browser/email, lets
# offer a 'report this user' button.
if (
bui.is_browser_likely_available()
and plus.get_v1_account_misc_read_val(
'showAccountExtrasMenu', False
)
):
self._extras_menu_button = bui.buttonwidget(
parent=self.root_widget,
size=(20, 20),
position=(self._width - 60, self._height - 30),
autoselect=True,
label='...',
button_type='square',
color=(0.64, 0.52, 0.69),
textcolor=(0.57, 0.47, 0.57),
on_activate_call=self._on_extras_menu_press,
)
bui.containerwidget(
edit=self.root_widget, cancel_button=self._cancel_button
)
bui.app.classic.master_server_v1_get(
'bsAccountInfo',
{
'buildNumber': bui.app.env.engine_build_number,
'accountID': self._account_id,
'profileID': self._profile_id,
},
callback=bui.WeakCall(self._on_query_response),
)
def _on_extras_menu_press(self) -> None:
choices = ['more', 'report']
choices_display = [
bui.Lstr(resource='coopSelectWindow.seeMoreText'),
bui.Lstr(resource='reportThisPlayerText'),
]
is_admin = False
if is_admin:
bui.screenmessage('TEMP FORCING ADMIN ON')
choices.append('ban')
choices_display.append(bui.Lstr(resource='banThisPlayerText'))
assert bui.app.classic is not None
uiscale = bui.app.ui_v1.uiscale
PopupMenuWindow(
position=self._extras_menu_button.get_screen_space_center(),
scale=(
2.3
if uiscale is bui.UIScale.SMALL
else 1.65 if uiscale is bui.UIScale.MEDIUM else 1.23
),
choices=choices,
choices_display=choices_display,
current_choice='more',
delegate=self,
)
def _on_ban_press(self) -> None:
plus = bui.app.plus
assert plus is not None
plus.add_v1_account_transaction(
{'type': 'BAN_ACCOUNT', 'account': self._account_id}
)
plus.run_v1_account_transactions()
def _on_report_press(self) -> None:
from bauiv1lib import report
report.ReportPlayerWindow(
self._account_id, origin_widget=self._extras_menu_button
)
def _on_more_press(self) -> None:
plus = bui.app.plus
assert plus is not None
bui.open_url(
plus.get_master_server_address()
+ '/highscores?profile='
+ self._account_id
)
def _on_query_response(self, data: dict[str, Any] | None) -> None:
# FIXME: Tidy this up.
# pylint: disable=too-many-locals
# pylint: disable=too-many-branches
# pylint: disable=too-many-statements
# pylint: disable=too-many-nested-blocks
assert bui.app.classic is not None
if data is None:
bui.textwidget(
edit=self._loading_text,
text=bui.Lstr(resource='internal.unavailableNoConnectionText'),
)
bui.spinnerwidget(edit=self._loading_spinner, visible=False)
else:
try:
self._loading_text.delete()
self._loading_spinner.delete()
trophystr = ''
try:
trophystr = data['trophies']
num = 10
chunks = [
trophystr[i : i + num]
for i in range(0, len(trophystr), num)
]
trophystr = '\n\n'.join(chunks)
if trophystr == '':
trophystr = '-'
except Exception:
logging.exception('Error displaying trophies.')
account_name_spacing = 15
tscale = 0.65
ts_height = bui.get_string_height(
trophystr, suppress_warning=True
)
sub_width = self._width - 80
sub_height = (
200
+ ts_height * tscale
+ account_name_spacing * len(data['accountDisplayStrings'])
)
self._subcontainer = bui.containerwidget(
parent=self._scrollwidget,
size=(sub_width, sub_height),
background=False,
)
v = sub_height - 20
title_scale = 0.37
center = 0.3
maxwidth_scale = 0.45
showing_character = False
if data['profileDisplayString'] is not None:
tint_color = (1, 1, 1)
try:
if data['profile'] is not None:
profile = data['profile']
assert bui.app.classic is not None
character = bui.app.classic.spaz_appearances.get(
profile['character'], None
)
if character is not None:
tint_color = (
profile['color']
if 'color' in profile
else (1, 1, 1)
)
tint2_color = (
profile['highlight']
if 'highlight' in profile
else (1, 1, 1)
)
icon_tex = character.icon_texture
tint_tex = character.icon_mask_texture
mask_texture = bui.gettexture(
'characterIconMask'
)
bui.imagewidget(
parent=self._subcontainer,
position=(sub_width * center - 40, v - 80),
size=(80, 80),
color=(1, 1, 1),
mask_texture=mask_texture,
texture=bui.gettexture(icon_tex),
tint_texture=bui.gettexture(tint_tex),
tint_color=tint_color,
tint2_color=tint2_color,
)
v -= 95
except Exception:
logging.exception('Error displaying character.')
bui.textwidget(
parent=self._subcontainer,
size=(0, 0),
position=(sub_width * center, v),
h_align='center',
v_align='center',
scale=0.9,
color=bui.safecolor(tint_color, 0.7),
shadow=1.0,
text=bui.Lstr(value=data['profileDisplayString']),
maxwidth=sub_width * maxwidth_scale * 0.75,
)
showing_character = True
v -= 33
center = 0.75 if showing_character else 0.5
maxwidth_scale = 0.45 if showing_character else 0.9
v = sub_height - 20
if len(data['accountDisplayStrings']) <= 1:
account_title = bui.Lstr(
resource='settingsWindow.accountText'
)
else:
account_title = bui.Lstr(
resource='accountSettingsWindow.accountsText',
fallback_resource='settingsWindow.accountText',
)
bui.textwidget(
parent=self._subcontainer,
size=(0, 0),
position=(sub_width * center, v),
flatness=1.0,
h_align='center',
v_align='center',
scale=title_scale,
color=bui.app.ui_v1.infotextcolor,
text=account_title,
maxwidth=sub_width * maxwidth_scale,
)
draw_small = (
showing_character or len(data['accountDisplayStrings']) > 1
)
v -= 14 if draw_small else 20
for account_string in data['accountDisplayStrings']:
bui.textwidget(
parent=self._subcontainer,
size=(0, 0),
position=(sub_width * center, v),
h_align='center',
v_align='center',
scale=0.55 if draw_small else 0.8,
text=account_string,
maxwidth=sub_width * maxwidth_scale,
)
v -= account_name_spacing
v += account_name_spacing
v -= 25 if showing_character else 29
bui.textwidget(
parent=self._subcontainer,
size=(0, 0),
position=(sub_width * center, v),
flatness=1.0,
h_align='center',
v_align='center',
scale=title_scale,
color=bui.app.ui_v1.infotextcolor,
text=bui.Lstr(resource='rankText'),
maxwidth=sub_width * maxwidth_scale,
)
v -= 14
if data['rank'] is None:
rank_str = '-'
suffix_offset = None
else:
str_raw = bui.Lstr(
resource='league.rankInLeagueText'
).evaluate()
# FIXME: Would be nice to not have to eval this.
rank_str = bui.Lstr(
resource='league.rankInLeagueText',
subs=[
('${RANK}', str(data['rank'][2])),
(
'${NAME}',
bui.Lstr(
translate=('leagueNames', data['rank'][0])
),
),
('${SUFFIX}', ''),
],
).evaluate()
rank_str_width = min(
sub_width * maxwidth_scale,
bui.get_string_width(rank_str, suppress_warning=True)
* 0.55,
)
# Only tack our suffix on if its at the end and only for
# non-diamond leagues.
if (
str_raw.endswith('${SUFFIX}')
and data['rank'][0] != 'Diamond'
):
suffix_offset = rank_str_width * 0.5 + 2
else:
suffix_offset = None
bui.textwidget(
parent=self._subcontainer,
size=(0, 0),
position=(sub_width * center, v),
h_align='center',
v_align='center',
scale=0.55,
text=rank_str,
maxwidth=sub_width * maxwidth_scale,
)
if suffix_offset is not None:
assert data['rank'] is not None
bui.textwidget(
parent=self._subcontainer,
size=(0, 0),
position=(sub_width * center + suffix_offset, v + 3),
h_align='left',
v_align='center',
scale=0.29,
flatness=1.0,
text='[' + str(data['rank'][1]) + ']',
)
v -= 14
str_raw = bui.Lstr(
resource='league.rankInLeagueText'
).evaluate()
old_offs = -50
prev_ranks_shown = 0
for prev_rank in data['prevRanks']:
rank_str = bui.Lstr(
value='${S}: ${I}',
subs=[
(
'${S}',
bui.Lstr(
resource='league.seasonText',
subs=[('${NUMBER}', str(prev_rank[0]))],
),
),
(
'${I}',
bui.Lstr(
resource='league.rankInLeagueText',
subs=[
('${RANK}', str(prev_rank[3])),
(
'${NAME}',
bui.Lstr(
translate=(
'leagueNames',
prev_rank[1],
)
),
),
('${SUFFIX}', ''),
],
),
),
],
).evaluate()
rank_str_width = min(
sub_width * maxwidth_scale,
bui.get_string_width(rank_str, suppress_warning=True)
* 0.3,
)
# Only tack our suffix on if its at the end and only for
# non-diamond leagues.
if (
str_raw.endswith('${SUFFIX}')
and prev_rank[1] != 'Diamond'
):
suffix_offset = rank_str_width + 2
else:
suffix_offset = None
bui.textwidget(
parent=self._subcontainer,
size=(0, 0),
position=(sub_width * center + old_offs, v),
h_align='left',
v_align='center',
scale=0.3,
text=rank_str,
flatness=1.0,
maxwidth=sub_width * maxwidth_scale,
)
if suffix_offset is not None:
bui.textwidget(
parent=self._subcontainer,
size=(0, 0),
position=(
sub_width * center + old_offs + suffix_offset,
v + 1,
),
h_align='left',
v_align='center',
scale=0.20,
flatness=1.0,
text='[' + str(prev_rank[2]) + ']',
)
prev_ranks_shown += 1
v -= 10
v -= 13
bui.textwidget(
parent=self._subcontainer,
size=(0, 0),
position=(sub_width * center, v),
flatness=1.0,
h_align='center',
v_align='center',
scale=title_scale,
color=bui.app.ui_v1.infotextcolor,
text=bui.Lstr(resource='achievementsText'),
maxwidth=sub_width * maxwidth_scale,
)
v -= 14
bui.textwidget(
parent=self._subcontainer,
size=(0, 0),
position=(sub_width * center, v),
h_align='center',
v_align='center',
scale=0.55,
text=str(data['achievementsCompleted'])
+ ' / '
+ str(len(bui.app.classic.ach.achievements)),
maxwidth=sub_width * maxwidth_scale,
)
v -= 25
if prev_ranks_shown == 0 and showing_character:
v -= 20
elif prev_ranks_shown == 1 and showing_character:
v -= 10
center = 0.5
maxwidth_scale = 0.9
bui.textwidget(
parent=self._subcontainer,
size=(0, 0),
position=(sub_width * center, v),
h_align='center',
v_align='center',
scale=title_scale,
color=bui.app.ui_v1.infotextcolor,
flatness=1.0,
text=bui.Lstr(
resource='trophiesThisSeasonText',
fallback_resource='trophiesText',
),
maxwidth=sub_width * maxwidth_scale,
)
v -= 19
bui.textwidget(
parent=self._subcontainer,
size=(0, ts_height),
position=(sub_width * 0.5, v - ts_height * tscale),
h_align='center',
v_align='top',
corner_scale=tscale,
text=trophystr,
)
except Exception:
logging.exception('Error displaying account info.')
def _on_cancel_press(self) -> None:
self._transition_out()
def _transition_out(self) -> None:
if not self._transitioning_out:
self._transitioning_out = True
bui.containerwidget(edit=self.root_widget, transition='out_scale')
# 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