# Released under the MIT License. See LICENSE for details. # """UI functionality for entering promo codes.""" from __future__ import annotations import time import logging from typing import TYPE_CHECKING, override import bauiv1 as bui if TYPE_CHECKING: from typing import Any class SendInfoWindow(bui.MainWindow): """Window for sending info to the developer.""" def __init__( self, transition: str | None = 'in_scale', origin_widget: bui.Widget | None = None, ): # Need to wrangle our own transition-out in modal mode. if origin_widget is not None: self._transition_out = 'out_scale' else: self._transition_out = 'out_right' uiscale = bui.app.ui_v1.uiscale width = 1200 if uiscale is bui.UIScale.SMALL else 600 height = 600 if uiscale is bui.UIScale.SMALL else 300 self._r = 'promoCodeWindow' # 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 = ( 2.0 if uiscale is bui.UIScale.SMALL else 1.5 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(width - 80, screensize[0] / scale) target_height = min(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 * height + 0.5 * target_height + 20.0 assert bui.app.classic is not None super().__init__( root_widget=bui.containerwidget( size=(width, height), toolbar_visibility=( 'menu_full' if bui.in_main_menu() else 'menu_minimal' ), 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 not bui.UIScale.SMALL: close_button = bui.buttonwidget( parent=self._root_widget, id=f'{self.main_window_id_prefix}|close', position=(25, yoffs - 35), size=(60, 60), scale=0.7, autoselect=True, label=bui.charstr(bui.SpecialChar.BACK), button_type='backSmall', on_activate_call=self.main_window_back, ) else: close_button = None v = yoffs - 74 v += -30 if uiscale is bui.UIScale.SMALL else 10 bui.textwidget( parent=self._root_widget, text=bui.Lstr(resource='sendInfoDescriptionText'), maxwidth=width * 0.9, position=(width * 0.5, v), color=(0.7, 0.7, 0.7, 1.0), size=(0, 0), scale=0.8, h_align='center', v_align='center', ) v -= 100 txoffs = -270 bui.textwidget( parent=self._root_widget, text=bui.Lstr(resource='descriptionText'), position=(width * 0.5 + txoffs + 22, v), color=(0.8, 0.8, 0.8, 1.0), size=(90, 30), h_align='right', maxwidth=100, ) v -= 8 self._text_field = bui.textwidget( parent=self._root_widget, id=f'{self.main_window_id_prefix}|desc', position=(width * 0.5 + txoffs + 125, v), size=(380, 46), text='', h_align='left', v_align='center', max_chars=64, color=(0.9, 0.9, 0.9, 1.0), description=bui.Lstr(resource='descriptionText'), editable=True, autoselect=True, padding=4, on_return_press_call=self._activate_enter_button, ) if close_button is not None: bui.widget(edit=close_button, down_widget=self._text_field) v -= 79 b_width = 200 self._enter_button = btn2 = bui.buttonwidget( parent=self._root_widget, id=f'{self.main_window_id_prefix}|enter', position=(width * 0.5 - b_width * 0.5, v), size=(b_width, 60), scale=1.0, label=bui.Lstr( resource='submitText', fallback_resource=f'{self._r}.enterText' ), on_activate_call=self._do_enter, autoselect=True, ) bui.containerwidget( edit=self._root_widget, start_button=btn2, selected_child=self._text_field, ) if close_button is not None: bui.containerwidget( edit=self._root_widget, cancel_button=close_button, ) else: bui.containerwidget( edit=self._root_widget, on_cancel_call=self.main_window_back ) @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, ) ) @override def main_window_should_preserve_selection(self) -> bool: return True def _activate_enter_button(self) -> None: self._enter_button.activate() def _do_enter(self) -> None: plus = bui.app.plus assert plus is not None description: Any = bui.textwidget(query=self._text_field) assert isinstance(description, str) # no-op if we're not in control. if not self.main_window_has_control(): return self.main_window_back() # Used for things like unlocking shared playlists or linking # accounts: talk directly to V1 server via transactions. bui.app.create_async_task(_send_info(description)) class SendInfoWindowLegacyModal(bui.Window): """Window for sending info to the developer.""" def __init__( self, transition: str | None = 'in_scale', origin_widget: bui.Widget | None = None, ): # pylint: disable=too-many-locals # Need to wrangle our own transition-out in modal mode. if origin_widget is not None: self._transition_out = 'out_scale' else: self._transition_out = 'out_right' uiscale = bui.app.ui_v1.uiscale width = 450 height = 200 self._r = 'promoCodeWindow' # 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 = ( 2.0 if uiscale is bui.UIScale.SMALL else 1.5 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(width - 80, screensize[0] / scale) target_height = min(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 * height + 0.5 * target_height + 20.0 scale_origin = ( None if origin_widget is None else origin_widget.get_screen_space_center() ) assert bui.app.classic is not None super().__init__( root_widget=bui.containerwidget( size=(width, height), toolbar_visibility=('menu_minimal_no_back'), transition=transition, scale_origin_stack_offset=scale_origin, scale=scale, darken_behind=True, ), ) close_button = bui.buttonwidget( parent=self._root_widget, scale=0.5, position=(30, yoffs - 30), size=(60, 60), on_activate_call=self._do_back, autoselect=True, color=(0.55, 0.5, 0.6), label=bui.charstr(bui.SpecialChar.CLOSE), textcolor=(1, 1, 1), ) v = yoffs - 74 txoffs = -200 bui.textwidget( parent=self._root_widget, text=bui.Lstr(resource=f'{self._r}.codeText'), position=(width * 0.5 + txoffs + 22, v), color=(0.8, 0.8, 0.8, 1.0), size=(90, 30), h_align='right', maxwidth=100, ) v -= 8 self._text_field = bui.textwidget( parent=self._root_widget, position=(width * 0.5 + txoffs + 125, v), size=(280, 46), text='', h_align='left', v_align='center', max_chars=64, color=(0.9, 0.9, 0.9, 1.0), description=bui.Lstr(resource=f'{self._r}.codeText'), editable=True, autoselect=True, padding=4, on_return_press_call=self._activate_enter_button, ) if close_button is not None: bui.widget(edit=close_button, down_widget=self._text_field) v -= 79 b_width = 200 self._enter_button = btn2 = bui.buttonwidget( parent=self._root_widget, position=(width * 0.5 - b_width * 0.5, v), size=(b_width, 60), scale=1.0, label=bui.Lstr( resource='submitText', fallback_resource=f'{self._r}.enterText' ), on_activate_call=self._do_enter, autoselect=True, ) bui.containerwidget( edit=self._root_widget, start_button=btn2, selected_child=self._text_field, ) if close_button is not None: bui.containerwidget( edit=self._root_widget, cancel_button=close_button, ) def _do_back(self) -> None: # pylint: disable=cyclic-import # Handle modal case: # no-op if our underlying widget is dead or on its way out. if not self._root_widget or self._root_widget.transitioning_out: return bui.containerwidget( edit=self._root_widget, transition=self._transition_out ) def _activate_enter_button(self) -> None: self._enter_button.activate() def _do_enter(self) -> None: # pylint: disable=cyclic-import # from bauiv1lib.settings.advanced import AdvancedSettingsWindow plus = bui.app.plus assert plus is not None description: Any = bui.textwidget(query=self._text_field) assert isinstance(description, str) # no-op if our underlying widget is dead or on its way out. if not self._root_widget or self._root_widget.transitioning_out: return bui.containerwidget( edit=self._root_widget, transition=self._transition_out ) # Used for things like unlocking shared playlists or linking # accounts: talk directly to V1 server via transactions. if plus.get_v1_account_state() != 'signed_in': bui.screenmessage( bui.Lstr(resource='notSignedInErrorText'), color=(1, 0, 0) ) bui.getsound('error').play() else: plus.add_v1_account_transaction( { 'type': 'PROMO_CODE', 'expire_time': time.time() + 5, 'code': description, } ) plus.run_v1_account_transactions() async def _send_info(description: str) -> None: from bacommon.classic import SendInfoMessage plus = bui.app.plus assert plus is not None classic = bui.app.classic assert classic is not None ui_pause: bui.RootUIUpdatePause | None = None # pylint: disable=W0612 try: # Don't allow *anything* if our V2 transport connection isn't up. if not plus.cloud.connected: bui.screenmessage( bui.Lstr(resource='internal.unavailableNoConnectionText'), color=(1, 0, 0), ) bui.getsound('error').play() return # Pause root ui updates so stuff like token counts don't change # automatically until we've run any client-effect animations # resulting from this message. ui_pause = bui.RootUIUpdatePause() # Ship to V2 server, with or without account info. if plus.accounts.primary is not None: with plus.accounts.primary: response = await plus.cloud.send_message_async( SendInfoMessage(description) ) else: response = await plus.cloud.send_message_async( SendInfoMessage(description) ) # Support simple message printing from v2 server. if response.message is not None: bui.screenmessage( bui.Lstr(translate=('serverResponses', response.message)), color=(0, 1, 0), ) # As of newer builds we support client-effects too. if response.effects: classic.run_bs_client_effects(response.effects) # If V2 handled it, we're done. if response.handled: return # Ok; V2 didn't handle it. Try V1 if we're signed in there. if plus.get_v1_account_state() != 'signed_in': bui.screenmessage( bui.Lstr(resource='notSignedInErrorText'), color=(1, 0, 0) ) bui.getsound('error').play() return # Push it along to v1 as an old style code. Allow v2 response to # sub in its own code. plus.add_v1_account_transaction( { 'type': 'PROMO_CODE', 'expire_time': time.time() + 5, 'code': ( description if response.legacy_code is None else response.legacy_code ), } ) plus.run_v1_account_transactions() except Exception: logging.exception('Error sending promo code.') bui.screenmessage('Error sending code (see log).', color=(1, 0, 0)) bui.getsound('error').play() finally: # Make sure ui-pause is dead even if something is holding # on to this stack frame. ui_pause = None