bauiv1lib.store.browser
UI for browsing the store.
1# Released under the MIT License. See LICENSE for details. 2# 3"""UI for browsing the store.""" 4# pylint: disable=too-many-lines 5from __future__ import annotations 6 7import os 8import time 9import copy 10import math 11import logging 12import weakref 13import datetime 14from enum import Enum 15from threading import Thread 16from typing import TYPE_CHECKING, override 17 18from efro.util import utc_now 19from efro.error import CommunicationError 20import bacommon.cloud 21import bauiv1 as bui 22 23if TYPE_CHECKING: 24 from typing import Any, Callable, Sequence 25 26MERCH_LINK_KEY = 'Merch Link' 27 28 29class StoreBrowserWindow(bui.MainWindow): 30 """Window for browsing the store.""" 31 32 class TabID(Enum): 33 """Our available tab types.""" 34 35 EXTRAS = 'extras' 36 MAPS = 'maps' 37 MINIGAMES = 'minigames' 38 CHARACTERS = 'characters' 39 ICONS = 'icons' 40 41 def __init__( 42 self, 43 transition: str | None = 'in_right', 44 modal: bool = False, 45 show_tab: StoreBrowserWindow.TabID | None = None, 46 on_close_call: Callable[[], Any] | None = None, 47 back_location: str | None = None, 48 origin_widget: bui.Widget | None = None, 49 ): 50 # pylint: disable=too-many-statements 51 # pylint: disable=too-many-locals 52 from bauiv1lib.tabs import TabRow 53 from bauiv1 import SpecialChar 54 55 app = bui.app 56 assert app.classic is not None 57 uiscale = app.ui_v1.uiscale 58 59 bui.set_analytics_screen('Store Window') 60 61 # Need to store this ourself for modal mode. 62 if origin_widget is not None: 63 self._transition_out = 'out_scale' 64 else: 65 self._transition_out = 'out_right' 66 67 self.button_infos: dict[str, dict[str, Any]] | None = None 68 self.update_buttons_timer: bui.AppTimer | None = None 69 self._status_textwidget_update_timer = None 70 71 self._back_location = back_location 72 self._on_close_call = on_close_call 73 self._show_tab = show_tab 74 self._modal = modal 75 self._width = 1670 if uiscale is bui.UIScale.SMALL else 1040 76 self._x_inset = x_inset = 310 if uiscale is bui.UIScale.SMALL else 0 77 self._height = ( 78 538 79 if uiscale is bui.UIScale.SMALL 80 else 645 if uiscale is bui.UIScale.MEDIUM else 800 81 ) 82 self._current_tab: StoreBrowserWindow.TabID | None = None 83 extra_top = 30 if uiscale is bui.UIScale.SMALL else 0 84 85 self.request: Any = None 86 self._r = 'store' 87 self._last_buy_time: float | None = None 88 89 super().__init__( 90 root_widget=bui.containerwidget( 91 size=(self._width, self._height + extra_top), 92 toolbar_visibility=( 93 'menu_store' 94 if uiscale is bui.UIScale.SMALL 95 else 'menu_full' 96 ), 97 scale=( 98 1.3 99 if uiscale is bui.UIScale.SMALL 100 else 0.9 if uiscale is bui.UIScale.MEDIUM else 0.8 101 ), 102 stack_offset=( 103 (0, 10) 104 if uiscale is bui.UIScale.SMALL 105 else (0, 0) if uiscale is bui.UIScale.MEDIUM else (0, 0) 106 ), 107 ), 108 transition=transition, 109 origin_widget=origin_widget, 110 ) 111 112 self._back_button = btn = bui.buttonwidget( 113 parent=self._root_widget, 114 position=(70 + x_inset, self._height - 74), 115 size=(140, 60), 116 scale=1.1, 117 autoselect=True, 118 label=bui.Lstr(resource='doneText' if self._modal else 'backText'), 119 button_type=None if self._modal else 'back', 120 on_activate_call=self._back, 121 ) 122 123 if uiscale is bui.UIScale.SMALL: 124 self._back_button.delete() 125 bui.containerwidget( 126 edit=self._root_widget, on_cancel_call=self._back 127 ) 128 # backbutton = bui.get_special_widget('back_button') 129 backbuttonspecial = True 130 else: 131 bui.containerwidget(edit=self._root_widget, cancel_button=btn) 132 # backbutton = self._back_button 133 backbuttonspecial = False 134 135 # self._ticket_count_text: bui.Widget | None = None 136 # self._get_tickets_button: bui.Widget | None = None 137 138 # if bool(False): 139 # if app.classic.allow_ticket_purchases: 140 # self._get_tickets_button = bui.buttonwidget( 141 # parent=self._root_widget, 142 # size=(210, 65), 143 # on_activate_call=self._on_get_more_tickets_press, 144 # autoselect=True, 145 # scale=0.9, 146 # text_scale=1.4, 147 # left_widget=backbutton, 148 # color=(0.7, 0.5, 0.85), 149 # textcolor=(0.2, 1.0, 0.2), 150 # label=bui.Lstr(resource='getTicketsWindow.titleText'), 151 # ) 152 # else: 153 # self._ticket_count_text = bui.textwidget( 154 # parent=self._root_widget, 155 # size=(210, 64), 156 # color=(0.2, 1.0, 0.2), 157 # h_align='center', 158 # v_align='center', 159 # ) 160 161 # Move this dynamically to keep it out of the way of the party icon. 162 # self._update_get_tickets_button_pos() 163 # self._get_ticket_pos_update_timer = bui.AppTimer( 164 # 1.0, 165 # bui.WeakCall(self._update_get_tickets_button_pos), 166 # repeat=True, 167 # ) 168 # if self._get_tickets_button and not backbuttonspecial: 169 # bui.widget( 170 # edit=self._back_button, right_widget=self._get_tickets_button 171 # ) 172 # self._ticket_text_update_timer = bui.AppTimer( 173 # 1.0, bui.WeakCall(self._update_tickets_text), repeat=True 174 # ) 175 # self._update_tickets_text() 176 177 if ( 178 app.classic.platform in ['mac', 'ios'] 179 and app.classic.subplatform == 'appstore' 180 ): 181 bui.buttonwidget( 182 parent=self._root_widget, 183 position=(self._width * 0.5 - 70, 16), 184 size=(230, 50), 185 scale=0.65, 186 on_activate_call=bui.WeakCall(self._restore_purchases), 187 color=(0.35, 0.3, 0.4), 188 selectable=False, 189 textcolor=(0.55, 0.5, 0.6), 190 label=bui.Lstr( 191 resource='getTicketsWindow.restorePurchasesText' 192 ), 193 ) 194 195 bui.textwidget( 196 parent=self._root_widget, 197 position=( 198 self._width * 0.5, 199 self._height - (53 if uiscale is bui.UIScale.SMALL else 44), 200 ), 201 size=(0, 0), 202 color=app.ui_v1.title_color, 203 scale=1.5, 204 h_align='center', 205 v_align='center', 206 text=bui.Lstr(resource='storeText'), 207 maxwidth=290, 208 ) 209 210 if not self._modal and not backbuttonspecial: 211 bui.buttonwidget( 212 edit=self._back_button, 213 button_type='backSmall', 214 size=(60, 60), 215 label=bui.charstr(SpecialChar.BACK), 216 ) 217 218 scroll_buffer_h = 130 + 2 * x_inset 219 tab_buffer_h = 250 + 2 * x_inset 220 221 tabs_def = [ 222 (self.TabID.EXTRAS, bui.Lstr(resource=f'{self._r}.extrasText')), 223 (self.TabID.MAPS, bui.Lstr(resource=f'{self._r}.mapsText')), 224 ( 225 self.TabID.MINIGAMES, 226 bui.Lstr(resource=f'{self._r}.miniGamesText'), 227 ), 228 ( 229 self.TabID.CHARACTERS, 230 bui.Lstr(resource=f'{self._r}.charactersText'), 231 ), 232 (self.TabID.ICONS, bui.Lstr(resource=f'{self._r}.iconsText')), 233 ] 234 235 self._tab_row = TabRow( 236 self._root_widget, 237 tabs_def, 238 pos=(tab_buffer_h * 0.5, self._height - 130), 239 size=(self._width - tab_buffer_h, 50), 240 on_select_call=self._set_tab, 241 ) 242 243 self._purchasable_count_widgets: dict[ 244 StoreBrowserWindow.TabID, dict[str, Any] 245 ] = {} 246 247 # Create our purchasable-items tags and have them update over time. 248 for tab_id, tab in self._tab_row.tabs.items(): 249 pos = tab.position 250 size = tab.size 251 button = tab.button 252 rad = 10 253 center = (pos[0] + 0.1 * size[0], pos[1] + 0.9 * size[1]) 254 img = bui.imagewidget( 255 parent=self._root_widget, 256 position=(center[0] - rad * 1.04, center[1] - rad * 1.15), 257 size=(rad * 2.2, rad * 2.2), 258 texture=bui.gettexture('circleShadow'), 259 color=(1, 0, 0), 260 ) 261 txt = bui.textwidget( 262 parent=self._root_widget, 263 position=center, 264 size=(0, 0), 265 h_align='center', 266 v_align='center', 267 maxwidth=1.4 * rad, 268 scale=0.6, 269 shadow=1.0, 270 flatness=1.0, 271 ) 272 rad = 20 273 sale_img = bui.imagewidget( 274 parent=self._root_widget, 275 position=(center[0] - rad, center[1] - rad), 276 size=(rad * 2, rad * 2), 277 draw_controller=button, 278 texture=bui.gettexture('circleZigZag'), 279 color=(0.5, 0, 1.0), 280 ) 281 sale_title_text = bui.textwidget( 282 parent=self._root_widget, 283 position=(center[0], center[1] + 0.24 * rad), 284 size=(0, 0), 285 h_align='center', 286 v_align='center', 287 draw_controller=button, 288 maxwidth=1.4 * rad, 289 scale=0.6, 290 shadow=0.0, 291 flatness=1.0, 292 color=(0, 1, 0), 293 ) 294 sale_time_text = bui.textwidget( 295 parent=self._root_widget, 296 position=(center[0], center[1] - 0.29 * rad), 297 size=(0, 0), 298 h_align='center', 299 v_align='center', 300 draw_controller=button, 301 maxwidth=1.4 * rad, 302 scale=0.4, 303 shadow=0.0, 304 flatness=1.0, 305 color=(0, 1, 0), 306 ) 307 self._purchasable_count_widgets[tab_id] = { 308 'img': img, 309 'text': txt, 310 'sale_img': sale_img, 311 'sale_title_text': sale_title_text, 312 'sale_time_text': sale_time_text, 313 } 314 self._tab_update_timer = bui.AppTimer( 315 1.0, bui.WeakCall(self._update_tabs), repeat=True 316 ) 317 self._update_tabs() 318 319 # if self._get_tickets_button: 320 # last_tab_button = self._tab_row.tabs[tabs_def[-1][0]].button 321 # bui.widget( 322 # edit=self._get_tickets_button, down_widget=last_tab_button 323 # ) 324 # bui.widget( 325 # edit=last_tab_button, 326 # up_widget=self._get_tickets_button, 327 # right_widget=self._get_tickets_button, 328 # ) 329 330 if uiscale is bui.UIScale.SMALL: 331 first_tab_button = self._tab_row.tabs[tabs_def[0][0]].button 332 last_tab_button = self._tab_row.tabs[tabs_def[-1][0]].button 333 bui.widget( 334 edit=first_tab_button, 335 left_widget=bui.get_special_widget('back_button'), 336 ) 337 bui.widget( 338 edit=last_tab_button, 339 right_widget=bui.get_special_widget('squad_button'), 340 ) 341 342 self._scroll_width = self._width - scroll_buffer_h 343 self._scroll_height = self._height - 180 344 345 self._scrollwidget: bui.Widget | None = None 346 self._status_textwidget: bui.Widget | None = None 347 self._restore_state() 348 349 # def _update_get_tickets_button_pos(self) -> None: 350 # assert bui.app.classic is not None 351 # uiscale = bui.app.ui_v1.uiscale 352 # pos = ( 353 # self._width 354 # - 252 355 # - ( 356 # self._x_inset 357 # + ( 358 # 47 359 # if uiscale is bui.UIScale.SMALL 360 # and bui.is_party_icon_visible() 361 # else 0 362 # ) 363 # ), 364 # self._height - 70, 365 # ) 366 # if self._get_tickets_button: 367 # bui.buttonwidget(edit=self._get_tickets_button, position=pos) 368 # if self._ticket_count_text: 369 # bui.textwidget(edit=self._ticket_count_text, position=pos) 370 371 def _restore_purchases(self) -> None: 372 from bauiv1lib import account 373 374 plus = bui.app.plus 375 assert plus is not None 376 if plus.accounts.primary is None: 377 account.show_sign_in_prompt() 378 else: 379 plus.restore_purchases() 380 381 def _update_tabs(self) -> None: 382 assert bui.app.classic is not None 383 store = bui.app.classic.store 384 385 if not self._root_widget: 386 return 387 for tab_id, tab_data in list(self._purchasable_count_widgets.items()): 388 sale_time = store.get_available_sale_time(tab_id.value) 389 390 if sale_time is not None: 391 bui.textwidget( 392 edit=tab_data['sale_title_text'], 393 text=bui.Lstr(resource='store.saleText'), 394 ) 395 bui.textwidget( 396 edit=tab_data['sale_time_text'], 397 text=bui.timestring(sale_time / 1000.0, centi=False), 398 ) 399 bui.imagewidget(edit=tab_data['sale_img'], opacity=1.0) 400 count = 0 401 else: 402 bui.textwidget(edit=tab_data['sale_title_text'], text='') 403 bui.textwidget(edit=tab_data['sale_time_text'], text='') 404 bui.imagewidget(edit=tab_data['sale_img'], opacity=0.0) 405 count = store.get_available_purchase_count(tab_id.value) 406 407 if count > 0: 408 bui.textwidget(edit=tab_data['text'], text=str(count)) 409 bui.imagewidget(edit=tab_data['img'], opacity=1.0) 410 else: 411 bui.textwidget(edit=tab_data['text'], text='') 412 bui.imagewidget(edit=tab_data['img'], opacity=0.0) 413 414 # def _update_tickets_text(self) -> None: 415 # from bauiv1 import SpecialChar 416 417 # if not self._root_widget: 418 # return 419 # plus = bui.app.plus 420 # assert plus is not None 421 # sval: str | bui.Lstr 422 # if plus.get_v1_account_state() == 'signed_in': 423 # sval = bui.charstr(SpecialChar.TICKET) + str( 424 # plus.get_v1_account_ticket_count() 425 # ) 426 # else: 427 # sval = bui.Lstr(resource='getTicketsWindow.titleText') 428 # if self._get_tickets_button: 429 # bui.buttonwidget(edit=self._get_tickets_button, label=sval) 430 # if self._ticket_count_text: 431 # bui.textwidget(edit=self._ticket_count_text, text=sval) 432 433 def _set_tab(self, tab_id: TabID) -> None: 434 if self._current_tab is tab_id: 435 return 436 self._current_tab = tab_id 437 438 # We wanna preserve our current tab between runs. 439 cfg = bui.app.config 440 cfg['Store Tab'] = tab_id.value 441 cfg.commit() 442 443 # Update tab colors based on which is selected. 444 self._tab_row.update_appearance(tab_id) 445 446 # (Re)create scroll widget. 447 if self._scrollwidget: 448 self._scrollwidget.delete() 449 450 self._scrollwidget = bui.scrollwidget( 451 parent=self._root_widget, 452 highlight=False, 453 position=( 454 (self._width - self._scroll_width) * 0.5, 455 self._height - self._scroll_height - 79 - 48, 456 ), 457 size=(self._scroll_width, self._scroll_height), 458 claims_left_right=True, 459 claims_tab=True, 460 selection_loops_to_parent=True, 461 ) 462 463 # NOTE: this stuff is modified by the _Store class. 464 # Should maybe clean that up. 465 self.button_infos = {} 466 self.update_buttons_timer = None 467 468 # Show status over top. 469 if self._status_textwidget: 470 self._status_textwidget.delete() 471 self._status_textwidget = bui.textwidget( 472 parent=self._root_widget, 473 position=(self._width * 0.5, self._height * 0.5), 474 size=(0, 0), 475 color=(1, 0.7, 1, 0.5), 476 h_align='center', 477 v_align='center', 478 text=bui.Lstr(resource=f'{self._r}.loadingText'), 479 maxwidth=self._scroll_width * 0.9, 480 ) 481 482 class _Request: 483 def __init__(self, window: StoreBrowserWindow): 484 self._window = weakref.ref(window) 485 data = {'tab': tab_id.value} 486 bui.apptimer(0.1, bui.WeakCall(self._on_response, data)) 487 488 def _on_response(self, data: dict[str, Any] | None) -> None: 489 # FIXME: clean this up. 490 # pylint: disable=protected-access 491 window = self._window() 492 if window is not None and (window.request is self): 493 window.request = None 494 # noinspection PyProtectedMember 495 window._on_response(data) 496 497 # Kick off a server request. 498 self.request = _Request(self) 499 500 # Actually start the purchase locally. 501 def _purchase_check_result( 502 self, item: str, is_ticket_purchase: bool, result: dict[str, Any] | None 503 ) -> None: 504 plus = bui.app.plus 505 assert plus is not None 506 if result is None: 507 bui.getsound('error').play() 508 bui.screenmessage( 509 bui.Lstr(resource='internal.unavailableNoConnectionText'), 510 color=(1, 0, 0), 511 ) 512 else: 513 if is_ticket_purchase: 514 if result['allow']: 515 price = plus.get_v1_account_misc_read_val( 516 'price.' + item, None 517 ) 518 if ( 519 price is None 520 or not isinstance(price, int) 521 or price <= 0 522 ): 523 print( 524 'Error; got invalid local price of', 525 price, 526 'for item', 527 item, 528 ) 529 bui.getsound('error').play() 530 else: 531 bui.getsound('click01').play() 532 plus.in_game_purchase(item, price) 533 else: 534 if result['reason'] == 'versionTooOld': 535 bui.getsound('error').play() 536 bui.screenmessage( 537 bui.Lstr( 538 resource='getTicketsWindow.versionTooOldText' 539 ), 540 color=(1, 0, 0), 541 ) 542 else: 543 bui.getsound('error').play() 544 bui.screenmessage( 545 bui.Lstr( 546 resource='getTicketsWindow.unavailableText' 547 ), 548 color=(1, 0, 0), 549 ) 550 # Real in-app purchase. 551 else: 552 if result['allow']: 553 plus.purchase(item) 554 else: 555 if result['reason'] == 'versionTooOld': 556 bui.getsound('error').play() 557 bui.screenmessage( 558 bui.Lstr( 559 resource='getTicketsWindow.versionTooOldText' 560 ), 561 color=(1, 0, 0), 562 ) 563 else: 564 bui.getsound('error').play() 565 bui.screenmessage( 566 bui.Lstr( 567 resource='getTicketsWindow.unavailableText' 568 ), 569 color=(1, 0, 0), 570 ) 571 572 def _do_purchase_check( 573 self, item: str, is_ticket_purchase: bool = False 574 ) -> None: 575 app = bui.app 576 if app.classic is None: 577 logging.warning('_do_purchase_check() requires classic.') 578 return 579 580 # Here we ping the server to ask if it's valid for us to 581 # purchase this. Better to fail now than after we've 582 # paid locally. 583 584 app.classic.master_server_v1_get( 585 'bsAccountPurchaseCheck', 586 { 587 'item': item, 588 'platform': app.classic.platform, 589 'subplatform': app.classic.subplatform, 590 'version': app.env.engine_version, 591 'buildNumber': app.env.engine_build_number, 592 'purchaseType': 'ticket' if is_ticket_purchase else 'real', 593 }, 594 callback=bui.WeakCall( 595 self._purchase_check_result, item, is_ticket_purchase 596 ), 597 ) 598 599 def buy(self, item: str) -> None: 600 """Attempt to purchase the provided item.""" 601 from bauiv1lib import account 602 from bauiv1lib.confirm import ConfirmWindow 603 604 # from bauiv1lib import gettickets 605 606 assert bui.app.classic is not None 607 store = bui.app.classic.store 608 609 plus = bui.app.plus 610 assert plus is not None 611 612 # Prevent pressing buy within a few seconds of the last press 613 # (gives the buttons time to disable themselves and whatnot). 614 curtime = bui.apptime() 615 if ( 616 self._last_buy_time is not None 617 and (curtime - self._last_buy_time) < 2.0 618 ): 619 bui.getsound('error').play() 620 else: 621 if plus.get_v1_account_state() != 'signed_in': 622 account.show_sign_in_prompt() 623 else: 624 self._last_buy_time = curtime 625 626 # Merch is a special case - just a link. 627 if item == 'merch': 628 url = bui.app.config.get('Merch Link') 629 if isinstance(url, str): 630 bui.open_url(url) 631 632 # Pro is an actual IAP, and the rest are ticket purchases. 633 elif item == 'pro': 634 bui.getsound('click01').play() 635 636 # Purchase either pro or pro_sale depending on whether 637 # there is a sale going on. 638 self._do_purchase_check( 639 'pro' 640 if store.get_available_sale_time('extras') is None 641 else 'pro_sale' 642 ) 643 else: 644 price = plus.get_v1_account_misc_read_val( 645 'price.' + item, None 646 ) 647 our_tickets = plus.get_v1_account_ticket_count() 648 if price is not None and our_tickets < price: 649 bui.getsound('error').play() 650 print('FIXME - show not-enough-tickets info.') 651 # gettickets.show_get_tickets_prompt() 652 else: 653 654 def do_it() -> None: 655 self._do_purchase_check( 656 item, is_ticket_purchase=True 657 ) 658 659 bui.getsound('swish').play() 660 ConfirmWindow( 661 bui.Lstr( 662 resource='store.purchaseConfirmText', 663 subs=[ 664 ( 665 '${ITEM}', 666 store.get_store_item_name_translated( 667 item 668 ), 669 ) 670 ], 671 ), 672 width=400, 673 height=120, 674 action=do_it, 675 ok_text=bui.Lstr( 676 resource='store.purchaseText', 677 fallback_resource='okText', 678 ), 679 ) 680 681 def _print_already_own(self, charname: str) -> None: 682 bui.screenmessage( 683 bui.Lstr( 684 resource=f'{self._r}.alreadyOwnText', 685 subs=[('${NAME}', charname)], 686 ), 687 color=(1, 0, 0), 688 ) 689 bui.getsound('error').play() 690 691 def update_buttons(self) -> None: 692 """Update our buttons.""" 693 # pylint: disable=too-many-statements 694 # pylint: disable=too-many-branches 695 # pylint: disable=too-many-locals 696 from bauiv1 import SpecialChar 697 698 assert bui.app.classic is not None 699 store = bui.app.classic.store 700 701 plus = bui.app.plus 702 assert plus is not None 703 704 if not self._root_widget: 705 return 706 707 sales_raw = plus.get_v1_account_misc_read_val('sales', {}) 708 sales = {} 709 try: 710 # Look at the current set of sales; filter any with time remaining. 711 for sale_item, sale_info in list(sales_raw.items()): 712 to_end = ( 713 datetime.datetime.fromtimestamp( 714 sale_info['e'], datetime.UTC 715 ) 716 - utc_now() 717 ).total_seconds() 718 if to_end > 0: 719 sales[sale_item] = { 720 'to_end': to_end, 721 'original_price': sale_info['op'], 722 } 723 except Exception: 724 logging.exception('Error parsing sales.') 725 726 assert self.button_infos is not None 727 for b_type, b_info in self.button_infos.items(): 728 if b_type == 'merch': 729 purchased = False 730 elif b_type in ['upgrades.pro', 'pro']: 731 assert bui.app.classic is not None 732 purchased = bui.app.classic.accounts.have_pro() 733 else: 734 purchased = plus.get_purchased(b_type) 735 736 sale_opacity = 0.0 737 sale_title_text: str | bui.Lstr = '' 738 sale_time_text: str | bui.Lstr = '' 739 740 if purchased: 741 title_color = (0.8, 0.7, 0.9, 1.0) 742 color = (0.63, 0.55, 0.78) 743 extra_image_opacity = 0.5 744 call = bui.WeakCall(self._print_already_own, b_info['name']) 745 price_text = '' 746 price_text_left = '' 747 price_text_right = '' 748 show_purchase_check = True 749 description_color: Sequence[float] = (0.4, 1.0, 0.4, 0.4) 750 description_color2: Sequence[float] = (0.0, 0.0, 0.0, 0.0) 751 price_color = (0.5, 1, 0.5, 0.3) 752 else: 753 title_color = (0.7, 0.9, 0.7, 1.0) 754 color = (0.4, 0.8, 0.1) 755 extra_image_opacity = 1.0 756 call = b_info['call'] if 'call' in b_info else None 757 if b_type == 'merch': 758 price_text = '' 759 price_text_left = '' 760 price_text_right = '' 761 elif b_type in ['upgrades.pro', 'pro']: 762 sale_time = store.get_available_sale_time('extras') 763 if sale_time is not None: 764 priceraw = plus.get_price('pro') 765 price_text_left = ( 766 priceraw if priceraw is not None else '?' 767 ) 768 priceraw = plus.get_price('pro_sale') 769 price_text_right = ( 770 priceraw if priceraw is not None else '?' 771 ) 772 sale_opacity = 1.0 773 price_text = '' 774 sale_title_text = bui.Lstr(resource='store.saleText') 775 sale_time_text = bui.timestring( 776 sale_time / 1000.0, centi=False 777 ) 778 else: 779 priceraw = plus.get_price('pro') 780 price_text = priceraw if priceraw is not None else '?' 781 price_text_left = '' 782 price_text_right = '' 783 else: 784 price = plus.get_v1_account_misc_read_val( 785 'price.' + b_type, 0 786 ) 787 788 # Color the button differently if we cant afford this. 789 if plus.get_v1_account_state() == 'signed_in': 790 if plus.get_v1_account_ticket_count() < price: 791 color = (0.6, 0.61, 0.6) 792 price_text = bui.charstr(bui.SpecialChar.TICKET) + str( 793 plus.get_v1_account_misc_read_val( 794 'price.' + b_type, '?' 795 ) 796 ) 797 price_text_left = '' 798 price_text_right = '' 799 800 # TESTING: 801 if b_type in sales: 802 sale_opacity = 1.0 803 price_text_left = bui.charstr(SpecialChar.TICKET) + str( 804 sales[b_type]['original_price'] 805 ) 806 price_text_right = price_text 807 price_text = '' 808 sale_title_text = bui.Lstr(resource='store.saleText') 809 sale_time_text = bui.timestring( 810 sales[b_type]['to_end'], centi=False 811 ) 812 813 description_color = (0.5, 1.0, 0.5) 814 description_color2 = (0.3, 1.0, 1.0) 815 price_color = (0.2, 1, 0.2, 1.0) 816 show_purchase_check = False 817 818 if 'title_text' in b_info: 819 bui.textwidget(edit=b_info['title_text'], color=title_color) 820 if 'purchase_check' in b_info: 821 bui.imagewidget( 822 edit=b_info['purchase_check'], 823 opacity=1.0 if show_purchase_check else 0.0, 824 ) 825 if 'price_widget' in b_info: 826 bui.textwidget( 827 edit=b_info['price_widget'], 828 text=price_text, 829 color=price_color, 830 ) 831 if 'price_widget_left' in b_info: 832 bui.textwidget( 833 edit=b_info['price_widget_left'], text=price_text_left 834 ) 835 if 'price_widget_right' in b_info: 836 bui.textwidget( 837 edit=b_info['price_widget_right'], text=price_text_right 838 ) 839 if 'price_slash_widget' in b_info: 840 bui.imagewidget( 841 edit=b_info['price_slash_widget'], opacity=sale_opacity 842 ) 843 if 'sale_bg_widget' in b_info: 844 bui.imagewidget( 845 edit=b_info['sale_bg_widget'], opacity=sale_opacity 846 ) 847 if 'sale_title_widget' in b_info: 848 bui.textwidget( 849 edit=b_info['sale_title_widget'], text=sale_title_text 850 ) 851 if 'sale_time_widget' in b_info: 852 bui.textwidget( 853 edit=b_info['sale_time_widget'], text=sale_time_text 854 ) 855 if 'button' in b_info: 856 bui.buttonwidget( 857 edit=b_info['button'], color=color, on_activate_call=call 858 ) 859 if 'extra_backings' in b_info: 860 for bck in b_info['extra_backings']: 861 bui.imagewidget( 862 edit=bck, color=color, opacity=extra_image_opacity 863 ) 864 if 'extra_images' in b_info: 865 for img in b_info['extra_images']: 866 bui.imagewidget(edit=img, opacity=extra_image_opacity) 867 if 'extra_texts' in b_info: 868 for etxt in b_info['extra_texts']: 869 bui.textwidget(edit=etxt, color=description_color) 870 if 'extra_texts_2' in b_info: 871 for etxt in b_info['extra_texts_2']: 872 bui.textwidget(edit=etxt, color=description_color2) 873 if 'descriptionText' in b_info: 874 bui.textwidget( 875 edit=b_info['descriptionText'], color=description_color 876 ) 877 878 def _on_response(self, data: dict[str, Any] | None) -> None: 879 # pylint: disable=too-many-statements 880 881 assert bui.app.classic is not None 882 cstore = bui.app.classic.store 883 884 # clear status text.. 885 if self._status_textwidget: 886 self._status_textwidget.delete() 887 self._status_textwidget_update_timer = None 888 889 if data is None: 890 self._status_textwidget = bui.textwidget( 891 parent=self._root_widget, 892 position=(self._width * 0.5, self._height * 0.5), 893 size=(0, 0), 894 scale=1.3, 895 transition_delay=0.1, 896 color=(1, 0.3, 0.3, 1.0), 897 h_align='center', 898 v_align='center', 899 text=bui.Lstr(resource=f'{self._r}.loadErrorText'), 900 maxwidth=self._scroll_width * 0.9, 901 ) 902 else: 903 904 class _Store: 905 def __init__( 906 self, 907 store_window: StoreBrowserWindow, 908 sdata: dict[str, Any], 909 width: float, 910 ): 911 self._store_window = store_window 912 self._width = width 913 store_data = cstore.get_store_layout() 914 self._tab = sdata['tab'] 915 self._sections = copy.deepcopy(store_data[sdata['tab']]) 916 self._height: float | None = None 917 918 assert bui.app.classic is not None 919 uiscale = bui.app.ui_v1.uiscale 920 921 # Pre-calc a few things and add them to store-data. 922 for section in self._sections: 923 if self._tab == 'characters': 924 dummy_name = 'characters.foo' 925 elif self._tab == 'extras': 926 dummy_name = 'pro' 927 elif self._tab == 'maps': 928 dummy_name = 'maps.foo' 929 elif self._tab == 'icons': 930 dummy_name = 'icons.foo' 931 else: 932 dummy_name = '' 933 section['button_size'] = ( 934 cstore.get_store_item_display_size(dummy_name) 935 ) 936 section['v_spacing'] = ( 937 -25 938 if ( 939 self._tab == 'extras' 940 and uiscale is bui.UIScale.SMALL 941 ) 942 else -17 if self._tab == 'characters' else 0 943 ) 944 if 'title' not in section: 945 section['title'] = '' 946 section['x_offs'] = ( 947 130 948 if self._tab == 'extras' 949 else 270 if self._tab == 'maps' else 0 950 ) 951 section['y_offs'] = ( 952 20 953 if ( 954 self._tab == 'extras' 955 and uiscale is bui.UIScale.SMALL 956 and bui.app.config.get('Merch Link') 957 ) 958 else ( 959 55 960 if ( 961 self._tab == 'extras' 962 and uiscale is bui.UIScale.SMALL 963 ) 964 else -20 if self._tab == 'icons' else 0 965 ) 966 ) 967 968 def instantiate( 969 self, scrollwidget: bui.Widget, tab_button: bui.Widget 970 ) -> None: 971 """Create the store.""" 972 # pylint: disable=too-many-locals 973 # pylint: disable=too-many-branches 974 # pylint: disable=too-many-nested-blocks 975 from bauiv1lib.store.item import ( 976 instantiate_store_item_display, 977 ) 978 979 title_spacing = 40 980 button_border = 20 981 button_spacing = 4 982 boffs_h = 40 983 self._height = 80.0 984 985 # Calc total height. 986 for i, section in enumerate(self._sections): 987 if section['title'] != '': 988 assert self._height is not None 989 self._height += title_spacing 990 b_width, b_height = section['button_size'] 991 b_column_count = int( 992 math.floor( 993 (self._width - boffs_h - 20) 994 / (b_width + button_spacing) 995 ) 996 ) 997 b_row_count = int( 998 math.ceil( 999 float(len(section['items'])) / b_column_count 1000 ) 1001 ) 1002 b_height_total = ( 1003 2 * button_border 1004 + b_row_count * b_height 1005 + (b_row_count - 1) * section['v_spacing'] 1006 ) 1007 self._height += b_height_total 1008 1009 assert self._height is not None 1010 cnt2 = bui.containerwidget( 1011 parent=scrollwidget, 1012 scale=1.0, 1013 size=(self._width, self._height), 1014 background=False, 1015 claims_left_right=True, 1016 claims_tab=True, 1017 selection_loops_to_parent=True, 1018 ) 1019 v = self._height - 20 1020 1021 if self._tab == 'characters': 1022 txt = bui.Lstr( 1023 resource='store.howToSwitchCharactersText', 1024 subs=[ 1025 ( 1026 '${SETTINGS}', 1027 bui.Lstr( 1028 resource=( 1029 'accountSettingsWindow.titleText' 1030 ) 1031 ), 1032 ), 1033 ( 1034 '${PLAYER_PROFILES}', 1035 bui.Lstr( 1036 resource=( 1037 'playerProfilesWindow.titleText' 1038 ) 1039 ), 1040 ), 1041 ], 1042 ) 1043 bui.textwidget( 1044 parent=cnt2, 1045 text=txt, 1046 size=(0, 0), 1047 position=(self._width * 0.5, self._height - 28), 1048 h_align='center', 1049 v_align='center', 1050 color=(0.7, 1, 0.7, 0.4), 1051 scale=0.7, 1052 shadow=0, 1053 flatness=1.0, 1054 maxwidth=700, 1055 transition_delay=0.4, 1056 ) 1057 elif self._tab == 'icons': 1058 txt = bui.Lstr( 1059 resource='store.howToUseIconsText', 1060 subs=[ 1061 ( 1062 '${SETTINGS}', 1063 bui.Lstr(resource='mainMenu.settingsText'), 1064 ), 1065 ( 1066 '${PLAYER_PROFILES}', 1067 bui.Lstr( 1068 resource=( 1069 'playerProfilesWindow.titleText' 1070 ) 1071 ), 1072 ), 1073 ], 1074 ) 1075 bui.textwidget( 1076 parent=cnt2, 1077 text=txt, 1078 size=(0, 0), 1079 position=(self._width * 0.5, self._height - 28), 1080 h_align='center', 1081 v_align='center', 1082 color=(0.7, 1, 0.7, 0.4), 1083 scale=0.7, 1084 shadow=0, 1085 flatness=1.0, 1086 maxwidth=700, 1087 transition_delay=0.4, 1088 ) 1089 elif self._tab == 'maps': 1090 assert self._width is not None 1091 assert self._height is not None 1092 txt = bui.Lstr(resource='store.howToUseMapsText') 1093 bui.textwidget( 1094 parent=cnt2, 1095 text=txt, 1096 size=(0, 0), 1097 position=(self._width * 0.5, self._height - 28), 1098 h_align='center', 1099 v_align='center', 1100 color=(0.7, 1, 0.7, 0.4), 1101 scale=0.7, 1102 shadow=0, 1103 flatness=1.0, 1104 maxwidth=700, 1105 transition_delay=0.4, 1106 ) 1107 1108 prev_row_buttons: list | None = None 1109 this_row_buttons = [] 1110 1111 delay = 0.3 1112 for section in self._sections: 1113 if section['title'] != '': 1114 bui.textwidget( 1115 parent=cnt2, 1116 position=(60, v - title_spacing * 0.8), 1117 size=(0, 0), 1118 scale=1.0, 1119 transition_delay=delay, 1120 color=(0.7, 0.9, 0.7, 1), 1121 h_align='left', 1122 v_align='center', 1123 text=bui.Lstr(resource=section['title']), 1124 maxwidth=self._width * 0.7, 1125 ) 1126 v -= title_spacing 1127 delay = max(0.100, delay - 0.100) 1128 v -= button_border 1129 b_width, b_height = section['button_size'] 1130 b_count = len(section['items']) 1131 b_column_count = int( 1132 math.floor( 1133 (self._width - boffs_h - 20) 1134 / (b_width + button_spacing) 1135 ) 1136 ) 1137 col = 0 1138 item: dict[str, Any] 1139 assert self._store_window.button_infos is not None 1140 for i, item_name in enumerate(section['items']): 1141 item = self._store_window.button_infos[ 1142 item_name 1143 ] = {} 1144 item['call'] = bui.WeakCall( 1145 self._store_window.buy, item_name 1146 ) 1147 if 'x_offs' in section: 1148 boffs_h2 = section['x_offs'] 1149 else: 1150 boffs_h2 = 0 1151 1152 if 'y_offs' in section: 1153 boffs_v2 = section['y_offs'] 1154 else: 1155 boffs_v2 = 0 1156 b_pos = ( 1157 boffs_h 1158 + boffs_h2 1159 + (b_width + button_spacing) * col, 1160 v - b_height + boffs_v2, 1161 ) 1162 instantiate_store_item_display( 1163 item_name, 1164 item, 1165 parent_widget=cnt2, 1166 b_pos=b_pos, 1167 boffs_h=boffs_h, 1168 b_width=b_width, 1169 b_height=b_height, 1170 boffs_h2=boffs_h2, 1171 boffs_v2=boffs_v2, 1172 delay=delay, 1173 ) 1174 btn = item['button'] 1175 delay = max(0.1, delay - 0.1) 1176 this_row_buttons.append(btn) 1177 1178 # Wire this button to the equivalent in the 1179 # previous row. 1180 if prev_row_buttons is not None: 1181 if len(prev_row_buttons) > col: 1182 bui.widget( 1183 edit=btn, 1184 up_widget=prev_row_buttons[col], 1185 ) 1186 bui.widget( 1187 edit=prev_row_buttons[col], 1188 down_widget=btn, 1189 ) 1190 1191 # If we're the last button in our row, 1192 # wire any in the previous row past 1193 # our position to go to us if down is 1194 # pressed. 1195 if ( 1196 col + 1 == b_column_count 1197 or i == b_count - 1 1198 ): 1199 for b_prev in prev_row_buttons[ 1200 col + 1 : 1201 ]: 1202 bui.widget( 1203 edit=b_prev, down_widget=btn 1204 ) 1205 else: 1206 bui.widget( 1207 edit=btn, up_widget=prev_row_buttons[-1] 1208 ) 1209 else: 1210 bui.widget(edit=btn, up_widget=tab_button) 1211 1212 col += 1 1213 if col == b_column_count or i == b_count - 1: 1214 prev_row_buttons = this_row_buttons 1215 this_row_buttons = [] 1216 col = 0 1217 v -= b_height 1218 if i < b_count - 1: 1219 v -= section['v_spacing'] 1220 1221 v -= button_border 1222 1223 # Set a timer to update these buttons periodically as long 1224 # as we're alive (so if we buy one it will grey out, etc). 1225 self._store_window.update_buttons_timer = bui.AppTimer( 1226 0.5, 1227 bui.WeakCall(self._store_window.update_buttons), 1228 repeat=True, 1229 ) 1230 1231 # Also update them immediately. 1232 self._store_window.update_buttons() 1233 1234 if self._current_tab in ( 1235 self.TabID.EXTRAS, 1236 self.TabID.MINIGAMES, 1237 self.TabID.CHARACTERS, 1238 self.TabID.MAPS, 1239 self.TabID.ICONS, 1240 ): 1241 store = _Store(self, data, self._scroll_width) 1242 assert self._scrollwidget is not None 1243 store.instantiate( 1244 scrollwidget=self._scrollwidget, 1245 tab_button=self._tab_row.tabs[self._current_tab].button, 1246 ) 1247 else: 1248 cnt = bui.containerwidget( 1249 parent=self._scrollwidget, 1250 scale=1.0, 1251 size=(self._scroll_width, self._scroll_height * 0.95), 1252 background=False, 1253 claims_left_right=True, 1254 claims_tab=True, 1255 selection_loops_to_parent=True, 1256 ) 1257 self._status_textwidget = bui.textwidget( 1258 parent=cnt, 1259 position=( 1260 self._scroll_width * 0.5, 1261 self._scroll_height * 0.5, 1262 ), 1263 size=(0, 0), 1264 scale=1.3, 1265 transition_delay=0.1, 1266 color=(1, 1, 0.3, 1.0), 1267 h_align='center', 1268 v_align='center', 1269 text=bui.Lstr(resource=f'{self._r}.comingSoonText'), 1270 maxwidth=self._scroll_width * 0.9, 1271 ) 1272 1273 @override 1274 def get_main_window_state(self) -> bui.MainWindowState: 1275 # Support recreating our window for back/refresh purposes. 1276 cls = type(self) 1277 return bui.BasicMainWindowState( 1278 create_call=lambda transition, origin_widget: cls( 1279 transition=transition, origin_widget=origin_widget 1280 ) 1281 ) 1282 1283 @override 1284 def on_main_window_close(self) -> None: 1285 self._save_state() 1286 1287 def _save_state(self) -> None: 1288 try: 1289 sel = self._root_widget.get_selected_child() 1290 selected_tab_ids = [ 1291 tab_id 1292 for tab_id, tab in self._tab_row.tabs.items() 1293 if sel == tab.button 1294 ] 1295 # if sel == self._get_tickets_button: 1296 # sel_name = 'GetTickets' 1297 if sel == self._scrollwidget: 1298 sel_name = 'Scroll' 1299 elif sel == self._back_button: 1300 sel_name = 'Back' 1301 elif selected_tab_ids: 1302 assert len(selected_tab_ids) == 1 1303 sel_name = f'Tab:{selected_tab_ids[0].value}' 1304 else: 1305 raise ValueError(f'unrecognized selection \'{sel}\'') 1306 assert bui.app.classic is not None 1307 bui.app.ui_v1.window_states[type(self)] = { 1308 'sel_name': sel_name, 1309 } 1310 except Exception: 1311 logging.exception('Error saving state for %s.', self) 1312 1313 def _restore_state(self) -> None: 1314 1315 try: 1316 sel: bui.Widget | None 1317 assert bui.app.classic is not None 1318 sel_name = bui.app.ui_v1.window_states.get(type(self), {}).get( 1319 'sel_name' 1320 ) 1321 assert isinstance(sel_name, (str, type(None))) 1322 1323 try: 1324 current_tab = self.TabID(bui.app.config.get('Store Tab')) 1325 except ValueError: 1326 current_tab = self.TabID.CHARACTERS 1327 1328 if self._show_tab is not None: 1329 current_tab = self._show_tab 1330 # if sel_name == 'GetTickets' and self._get_tickets_button: 1331 # sel = self._get_tickets_button 1332 if sel_name == 'Back': 1333 sel = self._back_button 1334 elif sel_name == 'Scroll': 1335 sel = self._scrollwidget 1336 elif isinstance(sel_name, str) and sel_name.startswith('Tab:'): 1337 try: 1338 sel_tab_id = self.TabID(sel_name.split(':')[-1]) 1339 except ValueError: 1340 sel_tab_id = self.TabID.CHARACTERS 1341 sel = self._tab_row.tabs[sel_tab_id].button 1342 else: 1343 sel = self._tab_row.tabs[current_tab].button 1344 1345 # If we were requested to show a tab, select it too.. 1346 if ( 1347 self._show_tab is not None 1348 and self._show_tab in self._tab_row.tabs 1349 ): 1350 sel = self._tab_row.tabs[self._show_tab].button 1351 self._set_tab(current_tab) 1352 if sel is not None: 1353 bui.containerwidget(edit=self._root_widget, selected_child=sel) 1354 except Exception: 1355 logging.exception('Error restoring state for %s.', self) 1356 1357 # def _on_get_more_tickets_press(self) -> None: 1358 # # pylint: disable=cyclic-import 1359 # from bauiv1lib.account import show_sign_in_prompt 1360 # from bauiv1lib.gettickets import GetTicketsWindow 1361 1362 # # no-op if our underlying widget is dead or on its way out. 1363 # if not self._root_widget or self._root_widget.transitioning_out: 1364 # return 1365 1366 # plus = bui.app.plus 1367 # assert plus is not None 1368 1369 # if plus.get_v1_account_state() != 'signed_in': 1370 # show_sign_in_prompt() 1371 # return 1372 # self._save_state() 1373 # bui.containerwidget(edit=self._root_widget, transition='out_left') 1374 # window = GetTicketsWindow( 1375 # from_modal_store=self._modal, 1376 # store_back_location=self._back_location, 1377 # ) 1378 # if not self._modal: 1379 # assert bui.app.classic is not None 1380 # bui.app.ui_v1.set_main_window(window, from_window=self) 1381 1382 def _back(self) -> None: 1383 # pylint: disable=cyclic-import 1384 # from bauiv1lib.coop.browser import CoopBrowserWindow 1385 # from bauiv1lib.mainmenu import MainMenuWindow 1386 1387 # no-op if our underlying widget is dead or on its way out. 1388 if not self._root_widget or self._root_widget.transitioning_out: 1389 return 1390 1391 self._save_state() 1392 1393 if self._modal: 1394 bui.containerwidget( 1395 edit=self._root_widget, transition=self._transition_out 1396 ) 1397 else: 1398 self.main_window_back() 1399 # if not self._modal: 1400 # assert bui.app.classic is not None 1401 # if self._back_location == 'CoopBrowserWindow': 1402 # bui.app.ui_v1.set_main_window( 1403 # CoopBrowserWindow(transition='in_left'), 1404 # from_window=self, 1405 # is_back=True, 1406 # ) 1407 # else: 1408 # bui.app.ui_v1.set_main_window( 1409 # MainMenuWindow(transition='in_left'), 1410 # from_window=self, 1411 # is_back=True, 1412 # is_top_level=True, 1413 # ) 1414 if self._on_close_call is not None: 1415 self._on_close_call() 1416 1417 1418def _check_merch_availability_in_bg_thread() -> None: 1419 # pylint: disable=cell-var-from-loop 1420 1421 # Merch is available from some countries only. 1422 # Make a reasonable check to ask the master-server about this at 1423 # launch and store the results. 1424 plus = bui.app.plus 1425 assert plus is not None 1426 1427 for _i in range(15): 1428 try: 1429 if plus.cloud.is_connected(): 1430 response = plus.cloud.send_message( 1431 bacommon.cloud.MerchAvailabilityMessage() 1432 ) 1433 1434 def _store_in_logic_thread() -> None: 1435 cfg = bui.app.config 1436 current = cfg.get(MERCH_LINK_KEY) 1437 if not isinstance(current, str | None): 1438 current = None 1439 if current != response.url: 1440 cfg[MERCH_LINK_KEY] = response.url 1441 cfg.commit() 1442 1443 # If we successfully get a response, kick it over to the 1444 # logic thread to store and we're done. 1445 bui.pushcall(_store_in_logic_thread, from_other_thread=True) 1446 return 1447 except CommunicationError: 1448 pass 1449 except Exception: 1450 logging.warning( 1451 'Unexpected error in merch-availability-check.', exc_info=True 1452 ) 1453 time.sleep(1.1934) # A bit randomized to avoid aliasing. 1454 1455 1456# Slight hack; start checking merch availability in the bg (but only if 1457# it looks like we've been imported for use in a running app; don't want 1458# to do this during docs generation/etc.) 1459 1460# TODO: Should wire this up explicitly to app bootstrapping; not good to 1461# be kicking off work at module import time. 1462if ( 1463 os.environ.get('BA_RUNNING_WITH_DUMMY_MODULES') != '1' 1464 and bui.app.state is not bui.app.State.NOT_STARTED 1465): 1466 Thread(target=_check_merch_availability_in_bg_thread, daemon=True).start()
MERCH_LINK_KEY =
'Merch Link'
class
StoreBrowserWindow(bauiv1._uitypes.MainWindow):
30class StoreBrowserWindow(bui.MainWindow): 31 """Window for browsing the store.""" 32 33 class TabID(Enum): 34 """Our available tab types.""" 35 36 EXTRAS = 'extras' 37 MAPS = 'maps' 38 MINIGAMES = 'minigames' 39 CHARACTERS = 'characters' 40 ICONS = 'icons' 41 42 def __init__( 43 self, 44 transition: str | None = 'in_right', 45 modal: bool = False, 46 show_tab: StoreBrowserWindow.TabID | None = None, 47 on_close_call: Callable[[], Any] | None = None, 48 back_location: str | None = None, 49 origin_widget: bui.Widget | None = None, 50 ): 51 # pylint: disable=too-many-statements 52 # pylint: disable=too-many-locals 53 from bauiv1lib.tabs import TabRow 54 from bauiv1 import SpecialChar 55 56 app = bui.app 57 assert app.classic is not None 58 uiscale = app.ui_v1.uiscale 59 60 bui.set_analytics_screen('Store Window') 61 62 # Need to store this ourself for modal mode. 63 if origin_widget is not None: 64 self._transition_out = 'out_scale' 65 else: 66 self._transition_out = 'out_right' 67 68 self.button_infos: dict[str, dict[str, Any]] | None = None 69 self.update_buttons_timer: bui.AppTimer | None = None 70 self._status_textwidget_update_timer = None 71 72 self._back_location = back_location 73 self._on_close_call = on_close_call 74 self._show_tab = show_tab 75 self._modal = modal 76 self._width = 1670 if uiscale is bui.UIScale.SMALL else 1040 77 self._x_inset = x_inset = 310 if uiscale is bui.UIScale.SMALL else 0 78 self._height = ( 79 538 80 if uiscale is bui.UIScale.SMALL 81 else 645 if uiscale is bui.UIScale.MEDIUM else 800 82 ) 83 self._current_tab: StoreBrowserWindow.TabID | None = None 84 extra_top = 30 if uiscale is bui.UIScale.SMALL else 0 85 86 self.request: Any = None 87 self._r = 'store' 88 self._last_buy_time: float | None = None 89 90 super().__init__( 91 root_widget=bui.containerwidget( 92 size=(self._width, self._height + extra_top), 93 toolbar_visibility=( 94 'menu_store' 95 if uiscale is bui.UIScale.SMALL 96 else 'menu_full' 97 ), 98 scale=( 99 1.3 100 if uiscale is bui.UIScale.SMALL 101 else 0.9 if uiscale is bui.UIScale.MEDIUM else 0.8 102 ), 103 stack_offset=( 104 (0, 10) 105 if uiscale is bui.UIScale.SMALL 106 else (0, 0) if uiscale is bui.UIScale.MEDIUM else (0, 0) 107 ), 108 ), 109 transition=transition, 110 origin_widget=origin_widget, 111 ) 112 113 self._back_button = btn = bui.buttonwidget( 114 parent=self._root_widget, 115 position=(70 + x_inset, self._height - 74), 116 size=(140, 60), 117 scale=1.1, 118 autoselect=True, 119 label=bui.Lstr(resource='doneText' if self._modal else 'backText'), 120 button_type=None if self._modal else 'back', 121 on_activate_call=self._back, 122 ) 123 124 if uiscale is bui.UIScale.SMALL: 125 self._back_button.delete() 126 bui.containerwidget( 127 edit=self._root_widget, on_cancel_call=self._back 128 ) 129 # backbutton = bui.get_special_widget('back_button') 130 backbuttonspecial = True 131 else: 132 bui.containerwidget(edit=self._root_widget, cancel_button=btn) 133 # backbutton = self._back_button 134 backbuttonspecial = False 135 136 # self._ticket_count_text: bui.Widget | None = None 137 # self._get_tickets_button: bui.Widget | None = None 138 139 # if bool(False): 140 # if app.classic.allow_ticket_purchases: 141 # self._get_tickets_button = bui.buttonwidget( 142 # parent=self._root_widget, 143 # size=(210, 65), 144 # on_activate_call=self._on_get_more_tickets_press, 145 # autoselect=True, 146 # scale=0.9, 147 # text_scale=1.4, 148 # left_widget=backbutton, 149 # color=(0.7, 0.5, 0.85), 150 # textcolor=(0.2, 1.0, 0.2), 151 # label=bui.Lstr(resource='getTicketsWindow.titleText'), 152 # ) 153 # else: 154 # self._ticket_count_text = bui.textwidget( 155 # parent=self._root_widget, 156 # size=(210, 64), 157 # color=(0.2, 1.0, 0.2), 158 # h_align='center', 159 # v_align='center', 160 # ) 161 162 # Move this dynamically to keep it out of the way of the party icon. 163 # self._update_get_tickets_button_pos() 164 # self._get_ticket_pos_update_timer = bui.AppTimer( 165 # 1.0, 166 # bui.WeakCall(self._update_get_tickets_button_pos), 167 # repeat=True, 168 # ) 169 # if self._get_tickets_button and not backbuttonspecial: 170 # bui.widget( 171 # edit=self._back_button, right_widget=self._get_tickets_button 172 # ) 173 # self._ticket_text_update_timer = bui.AppTimer( 174 # 1.0, bui.WeakCall(self._update_tickets_text), repeat=True 175 # ) 176 # self._update_tickets_text() 177 178 if ( 179 app.classic.platform in ['mac', 'ios'] 180 and app.classic.subplatform == 'appstore' 181 ): 182 bui.buttonwidget( 183 parent=self._root_widget, 184 position=(self._width * 0.5 - 70, 16), 185 size=(230, 50), 186 scale=0.65, 187 on_activate_call=bui.WeakCall(self._restore_purchases), 188 color=(0.35, 0.3, 0.4), 189 selectable=False, 190 textcolor=(0.55, 0.5, 0.6), 191 label=bui.Lstr( 192 resource='getTicketsWindow.restorePurchasesText' 193 ), 194 ) 195 196 bui.textwidget( 197 parent=self._root_widget, 198 position=( 199 self._width * 0.5, 200 self._height - (53 if uiscale is bui.UIScale.SMALL else 44), 201 ), 202 size=(0, 0), 203 color=app.ui_v1.title_color, 204 scale=1.5, 205 h_align='center', 206 v_align='center', 207 text=bui.Lstr(resource='storeText'), 208 maxwidth=290, 209 ) 210 211 if not self._modal and not backbuttonspecial: 212 bui.buttonwidget( 213 edit=self._back_button, 214 button_type='backSmall', 215 size=(60, 60), 216 label=bui.charstr(SpecialChar.BACK), 217 ) 218 219 scroll_buffer_h = 130 + 2 * x_inset 220 tab_buffer_h = 250 + 2 * x_inset 221 222 tabs_def = [ 223 (self.TabID.EXTRAS, bui.Lstr(resource=f'{self._r}.extrasText')), 224 (self.TabID.MAPS, bui.Lstr(resource=f'{self._r}.mapsText')), 225 ( 226 self.TabID.MINIGAMES, 227 bui.Lstr(resource=f'{self._r}.miniGamesText'), 228 ), 229 ( 230 self.TabID.CHARACTERS, 231 bui.Lstr(resource=f'{self._r}.charactersText'), 232 ), 233 (self.TabID.ICONS, bui.Lstr(resource=f'{self._r}.iconsText')), 234 ] 235 236 self._tab_row = TabRow( 237 self._root_widget, 238 tabs_def, 239 pos=(tab_buffer_h * 0.5, self._height - 130), 240 size=(self._width - tab_buffer_h, 50), 241 on_select_call=self._set_tab, 242 ) 243 244 self._purchasable_count_widgets: dict[ 245 StoreBrowserWindow.TabID, dict[str, Any] 246 ] = {} 247 248 # Create our purchasable-items tags and have them update over time. 249 for tab_id, tab in self._tab_row.tabs.items(): 250 pos = tab.position 251 size = tab.size 252 button = tab.button 253 rad = 10 254 center = (pos[0] + 0.1 * size[0], pos[1] + 0.9 * size[1]) 255 img = bui.imagewidget( 256 parent=self._root_widget, 257 position=(center[0] - rad * 1.04, center[1] - rad * 1.15), 258 size=(rad * 2.2, rad * 2.2), 259 texture=bui.gettexture('circleShadow'), 260 color=(1, 0, 0), 261 ) 262 txt = bui.textwidget( 263 parent=self._root_widget, 264 position=center, 265 size=(0, 0), 266 h_align='center', 267 v_align='center', 268 maxwidth=1.4 * rad, 269 scale=0.6, 270 shadow=1.0, 271 flatness=1.0, 272 ) 273 rad = 20 274 sale_img = bui.imagewidget( 275 parent=self._root_widget, 276 position=(center[0] - rad, center[1] - rad), 277 size=(rad * 2, rad * 2), 278 draw_controller=button, 279 texture=bui.gettexture('circleZigZag'), 280 color=(0.5, 0, 1.0), 281 ) 282 sale_title_text = bui.textwidget( 283 parent=self._root_widget, 284 position=(center[0], center[1] + 0.24 * rad), 285 size=(0, 0), 286 h_align='center', 287 v_align='center', 288 draw_controller=button, 289 maxwidth=1.4 * rad, 290 scale=0.6, 291 shadow=0.0, 292 flatness=1.0, 293 color=(0, 1, 0), 294 ) 295 sale_time_text = bui.textwidget( 296 parent=self._root_widget, 297 position=(center[0], center[1] - 0.29 * rad), 298 size=(0, 0), 299 h_align='center', 300 v_align='center', 301 draw_controller=button, 302 maxwidth=1.4 * rad, 303 scale=0.4, 304 shadow=0.0, 305 flatness=1.0, 306 color=(0, 1, 0), 307 ) 308 self._purchasable_count_widgets[tab_id] = { 309 'img': img, 310 'text': txt, 311 'sale_img': sale_img, 312 'sale_title_text': sale_title_text, 313 'sale_time_text': sale_time_text, 314 } 315 self._tab_update_timer = bui.AppTimer( 316 1.0, bui.WeakCall(self._update_tabs), repeat=True 317 ) 318 self._update_tabs() 319 320 # if self._get_tickets_button: 321 # last_tab_button = self._tab_row.tabs[tabs_def[-1][0]].button 322 # bui.widget( 323 # edit=self._get_tickets_button, down_widget=last_tab_button 324 # ) 325 # bui.widget( 326 # edit=last_tab_button, 327 # up_widget=self._get_tickets_button, 328 # right_widget=self._get_tickets_button, 329 # ) 330 331 if uiscale is bui.UIScale.SMALL: 332 first_tab_button = self._tab_row.tabs[tabs_def[0][0]].button 333 last_tab_button = self._tab_row.tabs[tabs_def[-1][0]].button 334 bui.widget( 335 edit=first_tab_button, 336 left_widget=bui.get_special_widget('back_button'), 337 ) 338 bui.widget( 339 edit=last_tab_button, 340 right_widget=bui.get_special_widget('squad_button'), 341 ) 342 343 self._scroll_width = self._width - scroll_buffer_h 344 self._scroll_height = self._height - 180 345 346 self._scrollwidget: bui.Widget | None = None 347 self._status_textwidget: bui.Widget | None = None 348 self._restore_state() 349 350 # def _update_get_tickets_button_pos(self) -> None: 351 # assert bui.app.classic is not None 352 # uiscale = bui.app.ui_v1.uiscale 353 # pos = ( 354 # self._width 355 # - 252 356 # - ( 357 # self._x_inset 358 # + ( 359 # 47 360 # if uiscale is bui.UIScale.SMALL 361 # and bui.is_party_icon_visible() 362 # else 0 363 # ) 364 # ), 365 # self._height - 70, 366 # ) 367 # if self._get_tickets_button: 368 # bui.buttonwidget(edit=self._get_tickets_button, position=pos) 369 # if self._ticket_count_text: 370 # bui.textwidget(edit=self._ticket_count_text, position=pos) 371 372 def _restore_purchases(self) -> None: 373 from bauiv1lib import account 374 375 plus = bui.app.plus 376 assert plus is not None 377 if plus.accounts.primary is None: 378 account.show_sign_in_prompt() 379 else: 380 plus.restore_purchases() 381 382 def _update_tabs(self) -> None: 383 assert bui.app.classic is not None 384 store = bui.app.classic.store 385 386 if not self._root_widget: 387 return 388 for tab_id, tab_data in list(self._purchasable_count_widgets.items()): 389 sale_time = store.get_available_sale_time(tab_id.value) 390 391 if sale_time is not None: 392 bui.textwidget( 393 edit=tab_data['sale_title_text'], 394 text=bui.Lstr(resource='store.saleText'), 395 ) 396 bui.textwidget( 397 edit=tab_data['sale_time_text'], 398 text=bui.timestring(sale_time / 1000.0, centi=False), 399 ) 400 bui.imagewidget(edit=tab_data['sale_img'], opacity=1.0) 401 count = 0 402 else: 403 bui.textwidget(edit=tab_data['sale_title_text'], text='') 404 bui.textwidget(edit=tab_data['sale_time_text'], text='') 405 bui.imagewidget(edit=tab_data['sale_img'], opacity=0.0) 406 count = store.get_available_purchase_count(tab_id.value) 407 408 if count > 0: 409 bui.textwidget(edit=tab_data['text'], text=str(count)) 410 bui.imagewidget(edit=tab_data['img'], opacity=1.0) 411 else: 412 bui.textwidget(edit=tab_data['text'], text='') 413 bui.imagewidget(edit=tab_data['img'], opacity=0.0) 414 415 # def _update_tickets_text(self) -> None: 416 # from bauiv1 import SpecialChar 417 418 # if not self._root_widget: 419 # return 420 # plus = bui.app.plus 421 # assert plus is not None 422 # sval: str | bui.Lstr 423 # if plus.get_v1_account_state() == 'signed_in': 424 # sval = bui.charstr(SpecialChar.TICKET) + str( 425 # plus.get_v1_account_ticket_count() 426 # ) 427 # else: 428 # sval = bui.Lstr(resource='getTicketsWindow.titleText') 429 # if self._get_tickets_button: 430 # bui.buttonwidget(edit=self._get_tickets_button, label=sval) 431 # if self._ticket_count_text: 432 # bui.textwidget(edit=self._ticket_count_text, text=sval) 433 434 def _set_tab(self, tab_id: TabID) -> None: 435 if self._current_tab is tab_id: 436 return 437 self._current_tab = tab_id 438 439 # We wanna preserve our current tab between runs. 440 cfg = bui.app.config 441 cfg['Store Tab'] = tab_id.value 442 cfg.commit() 443 444 # Update tab colors based on which is selected. 445 self._tab_row.update_appearance(tab_id) 446 447 # (Re)create scroll widget. 448 if self._scrollwidget: 449 self._scrollwidget.delete() 450 451 self._scrollwidget = bui.scrollwidget( 452 parent=self._root_widget, 453 highlight=False, 454 position=( 455 (self._width - self._scroll_width) * 0.5, 456 self._height - self._scroll_height - 79 - 48, 457 ), 458 size=(self._scroll_width, self._scroll_height), 459 claims_left_right=True, 460 claims_tab=True, 461 selection_loops_to_parent=True, 462 ) 463 464 # NOTE: this stuff is modified by the _Store class. 465 # Should maybe clean that up. 466 self.button_infos = {} 467 self.update_buttons_timer = None 468 469 # Show status over top. 470 if self._status_textwidget: 471 self._status_textwidget.delete() 472 self._status_textwidget = bui.textwidget( 473 parent=self._root_widget, 474 position=(self._width * 0.5, self._height * 0.5), 475 size=(0, 0), 476 color=(1, 0.7, 1, 0.5), 477 h_align='center', 478 v_align='center', 479 text=bui.Lstr(resource=f'{self._r}.loadingText'), 480 maxwidth=self._scroll_width * 0.9, 481 ) 482 483 class _Request: 484 def __init__(self, window: StoreBrowserWindow): 485 self._window = weakref.ref(window) 486 data = {'tab': tab_id.value} 487 bui.apptimer(0.1, bui.WeakCall(self._on_response, data)) 488 489 def _on_response(self, data: dict[str, Any] | None) -> None: 490 # FIXME: clean this up. 491 # pylint: disable=protected-access 492 window = self._window() 493 if window is not None and (window.request is self): 494 window.request = None 495 # noinspection PyProtectedMember 496 window._on_response(data) 497 498 # Kick off a server request. 499 self.request = _Request(self) 500 501 # Actually start the purchase locally. 502 def _purchase_check_result( 503 self, item: str, is_ticket_purchase: bool, result: dict[str, Any] | None 504 ) -> None: 505 plus = bui.app.plus 506 assert plus is not None 507 if result is None: 508 bui.getsound('error').play() 509 bui.screenmessage( 510 bui.Lstr(resource='internal.unavailableNoConnectionText'), 511 color=(1, 0, 0), 512 ) 513 else: 514 if is_ticket_purchase: 515 if result['allow']: 516 price = plus.get_v1_account_misc_read_val( 517 'price.' + item, None 518 ) 519 if ( 520 price is None 521 or not isinstance(price, int) 522 or price <= 0 523 ): 524 print( 525 'Error; got invalid local price of', 526 price, 527 'for item', 528 item, 529 ) 530 bui.getsound('error').play() 531 else: 532 bui.getsound('click01').play() 533 plus.in_game_purchase(item, price) 534 else: 535 if result['reason'] == 'versionTooOld': 536 bui.getsound('error').play() 537 bui.screenmessage( 538 bui.Lstr( 539 resource='getTicketsWindow.versionTooOldText' 540 ), 541 color=(1, 0, 0), 542 ) 543 else: 544 bui.getsound('error').play() 545 bui.screenmessage( 546 bui.Lstr( 547 resource='getTicketsWindow.unavailableText' 548 ), 549 color=(1, 0, 0), 550 ) 551 # Real in-app purchase. 552 else: 553 if result['allow']: 554 plus.purchase(item) 555 else: 556 if result['reason'] == 'versionTooOld': 557 bui.getsound('error').play() 558 bui.screenmessage( 559 bui.Lstr( 560 resource='getTicketsWindow.versionTooOldText' 561 ), 562 color=(1, 0, 0), 563 ) 564 else: 565 bui.getsound('error').play() 566 bui.screenmessage( 567 bui.Lstr( 568 resource='getTicketsWindow.unavailableText' 569 ), 570 color=(1, 0, 0), 571 ) 572 573 def _do_purchase_check( 574 self, item: str, is_ticket_purchase: bool = False 575 ) -> None: 576 app = bui.app 577 if app.classic is None: 578 logging.warning('_do_purchase_check() requires classic.') 579 return 580 581 # Here we ping the server to ask if it's valid for us to 582 # purchase this. Better to fail now than after we've 583 # paid locally. 584 585 app.classic.master_server_v1_get( 586 'bsAccountPurchaseCheck', 587 { 588 'item': item, 589 'platform': app.classic.platform, 590 'subplatform': app.classic.subplatform, 591 'version': app.env.engine_version, 592 'buildNumber': app.env.engine_build_number, 593 'purchaseType': 'ticket' if is_ticket_purchase else 'real', 594 }, 595 callback=bui.WeakCall( 596 self._purchase_check_result, item, is_ticket_purchase 597 ), 598 ) 599 600 def buy(self, item: str) -> None: 601 """Attempt to purchase the provided item.""" 602 from bauiv1lib import account 603 from bauiv1lib.confirm import ConfirmWindow 604 605 # from bauiv1lib import gettickets 606 607 assert bui.app.classic is not None 608 store = bui.app.classic.store 609 610 plus = bui.app.plus 611 assert plus is not None 612 613 # Prevent pressing buy within a few seconds of the last press 614 # (gives the buttons time to disable themselves and whatnot). 615 curtime = bui.apptime() 616 if ( 617 self._last_buy_time is not None 618 and (curtime - self._last_buy_time) < 2.0 619 ): 620 bui.getsound('error').play() 621 else: 622 if plus.get_v1_account_state() != 'signed_in': 623 account.show_sign_in_prompt() 624 else: 625 self._last_buy_time = curtime 626 627 # Merch is a special case - just a link. 628 if item == 'merch': 629 url = bui.app.config.get('Merch Link') 630 if isinstance(url, str): 631 bui.open_url(url) 632 633 # Pro is an actual IAP, and the rest are ticket purchases. 634 elif item == 'pro': 635 bui.getsound('click01').play() 636 637 # Purchase either pro or pro_sale depending on whether 638 # there is a sale going on. 639 self._do_purchase_check( 640 'pro' 641 if store.get_available_sale_time('extras') is None 642 else 'pro_sale' 643 ) 644 else: 645 price = plus.get_v1_account_misc_read_val( 646 'price.' + item, None 647 ) 648 our_tickets = plus.get_v1_account_ticket_count() 649 if price is not None and our_tickets < price: 650 bui.getsound('error').play() 651 print('FIXME - show not-enough-tickets info.') 652 # gettickets.show_get_tickets_prompt() 653 else: 654 655 def do_it() -> None: 656 self._do_purchase_check( 657 item, is_ticket_purchase=True 658 ) 659 660 bui.getsound('swish').play() 661 ConfirmWindow( 662 bui.Lstr( 663 resource='store.purchaseConfirmText', 664 subs=[ 665 ( 666 '${ITEM}', 667 store.get_store_item_name_translated( 668 item 669 ), 670 ) 671 ], 672 ), 673 width=400, 674 height=120, 675 action=do_it, 676 ok_text=bui.Lstr( 677 resource='store.purchaseText', 678 fallback_resource='okText', 679 ), 680 ) 681 682 def _print_already_own(self, charname: str) -> None: 683 bui.screenmessage( 684 bui.Lstr( 685 resource=f'{self._r}.alreadyOwnText', 686 subs=[('${NAME}', charname)], 687 ), 688 color=(1, 0, 0), 689 ) 690 bui.getsound('error').play() 691 692 def update_buttons(self) -> None: 693 """Update our buttons.""" 694 # pylint: disable=too-many-statements 695 # pylint: disable=too-many-branches 696 # pylint: disable=too-many-locals 697 from bauiv1 import SpecialChar 698 699 assert bui.app.classic is not None 700 store = bui.app.classic.store 701 702 plus = bui.app.plus 703 assert plus is not None 704 705 if not self._root_widget: 706 return 707 708 sales_raw = plus.get_v1_account_misc_read_val('sales', {}) 709 sales = {} 710 try: 711 # Look at the current set of sales; filter any with time remaining. 712 for sale_item, sale_info in list(sales_raw.items()): 713 to_end = ( 714 datetime.datetime.fromtimestamp( 715 sale_info['e'], datetime.UTC 716 ) 717 - utc_now() 718 ).total_seconds() 719 if to_end > 0: 720 sales[sale_item] = { 721 'to_end': to_end, 722 'original_price': sale_info['op'], 723 } 724 except Exception: 725 logging.exception('Error parsing sales.') 726 727 assert self.button_infos is not None 728 for b_type, b_info in self.button_infos.items(): 729 if b_type == 'merch': 730 purchased = False 731 elif b_type in ['upgrades.pro', 'pro']: 732 assert bui.app.classic is not None 733 purchased = bui.app.classic.accounts.have_pro() 734 else: 735 purchased = plus.get_purchased(b_type) 736 737 sale_opacity = 0.0 738 sale_title_text: str | bui.Lstr = '' 739 sale_time_text: str | bui.Lstr = '' 740 741 if purchased: 742 title_color = (0.8, 0.7, 0.9, 1.0) 743 color = (0.63, 0.55, 0.78) 744 extra_image_opacity = 0.5 745 call = bui.WeakCall(self._print_already_own, b_info['name']) 746 price_text = '' 747 price_text_left = '' 748 price_text_right = '' 749 show_purchase_check = True 750 description_color: Sequence[float] = (0.4, 1.0, 0.4, 0.4) 751 description_color2: Sequence[float] = (0.0, 0.0, 0.0, 0.0) 752 price_color = (0.5, 1, 0.5, 0.3) 753 else: 754 title_color = (0.7, 0.9, 0.7, 1.0) 755 color = (0.4, 0.8, 0.1) 756 extra_image_opacity = 1.0 757 call = b_info['call'] if 'call' in b_info else None 758 if b_type == 'merch': 759 price_text = '' 760 price_text_left = '' 761 price_text_right = '' 762 elif b_type in ['upgrades.pro', 'pro']: 763 sale_time = store.get_available_sale_time('extras') 764 if sale_time is not None: 765 priceraw = plus.get_price('pro') 766 price_text_left = ( 767 priceraw if priceraw is not None else '?' 768 ) 769 priceraw = plus.get_price('pro_sale') 770 price_text_right = ( 771 priceraw if priceraw is not None else '?' 772 ) 773 sale_opacity = 1.0 774 price_text = '' 775 sale_title_text = bui.Lstr(resource='store.saleText') 776 sale_time_text = bui.timestring( 777 sale_time / 1000.0, centi=False 778 ) 779 else: 780 priceraw = plus.get_price('pro') 781 price_text = priceraw if priceraw is not None else '?' 782 price_text_left = '' 783 price_text_right = '' 784 else: 785 price = plus.get_v1_account_misc_read_val( 786 'price.' + b_type, 0 787 ) 788 789 # Color the button differently if we cant afford this. 790 if plus.get_v1_account_state() == 'signed_in': 791 if plus.get_v1_account_ticket_count() < price: 792 color = (0.6, 0.61, 0.6) 793 price_text = bui.charstr(bui.SpecialChar.TICKET) + str( 794 plus.get_v1_account_misc_read_val( 795 'price.' + b_type, '?' 796 ) 797 ) 798 price_text_left = '' 799 price_text_right = '' 800 801 # TESTING: 802 if b_type in sales: 803 sale_opacity = 1.0 804 price_text_left = bui.charstr(SpecialChar.TICKET) + str( 805 sales[b_type]['original_price'] 806 ) 807 price_text_right = price_text 808 price_text = '' 809 sale_title_text = bui.Lstr(resource='store.saleText') 810 sale_time_text = bui.timestring( 811 sales[b_type]['to_end'], centi=False 812 ) 813 814 description_color = (0.5, 1.0, 0.5) 815 description_color2 = (0.3, 1.0, 1.0) 816 price_color = (0.2, 1, 0.2, 1.0) 817 show_purchase_check = False 818 819 if 'title_text' in b_info: 820 bui.textwidget(edit=b_info['title_text'], color=title_color) 821 if 'purchase_check' in b_info: 822 bui.imagewidget( 823 edit=b_info['purchase_check'], 824 opacity=1.0 if show_purchase_check else 0.0, 825 ) 826 if 'price_widget' in b_info: 827 bui.textwidget( 828 edit=b_info['price_widget'], 829 text=price_text, 830 color=price_color, 831 ) 832 if 'price_widget_left' in b_info: 833 bui.textwidget( 834 edit=b_info['price_widget_left'], text=price_text_left 835 ) 836 if 'price_widget_right' in b_info: 837 bui.textwidget( 838 edit=b_info['price_widget_right'], text=price_text_right 839 ) 840 if 'price_slash_widget' in b_info: 841 bui.imagewidget( 842 edit=b_info['price_slash_widget'], opacity=sale_opacity 843 ) 844 if 'sale_bg_widget' in b_info: 845 bui.imagewidget( 846 edit=b_info['sale_bg_widget'], opacity=sale_opacity 847 ) 848 if 'sale_title_widget' in b_info: 849 bui.textwidget( 850 edit=b_info['sale_title_widget'], text=sale_title_text 851 ) 852 if 'sale_time_widget' in b_info: 853 bui.textwidget( 854 edit=b_info['sale_time_widget'], text=sale_time_text 855 ) 856 if 'button' in b_info: 857 bui.buttonwidget( 858 edit=b_info['button'], color=color, on_activate_call=call 859 ) 860 if 'extra_backings' in b_info: 861 for bck in b_info['extra_backings']: 862 bui.imagewidget( 863 edit=bck, color=color, opacity=extra_image_opacity 864 ) 865 if 'extra_images' in b_info: 866 for img in b_info['extra_images']: 867 bui.imagewidget(edit=img, opacity=extra_image_opacity) 868 if 'extra_texts' in b_info: 869 for etxt in b_info['extra_texts']: 870 bui.textwidget(edit=etxt, color=description_color) 871 if 'extra_texts_2' in b_info: 872 for etxt in b_info['extra_texts_2']: 873 bui.textwidget(edit=etxt, color=description_color2) 874 if 'descriptionText' in b_info: 875 bui.textwidget( 876 edit=b_info['descriptionText'], color=description_color 877 ) 878 879 def _on_response(self, data: dict[str, Any] | None) -> None: 880 # pylint: disable=too-many-statements 881 882 assert bui.app.classic is not None 883 cstore = bui.app.classic.store 884 885 # clear status text.. 886 if self._status_textwidget: 887 self._status_textwidget.delete() 888 self._status_textwidget_update_timer = None 889 890 if data is None: 891 self._status_textwidget = bui.textwidget( 892 parent=self._root_widget, 893 position=(self._width * 0.5, self._height * 0.5), 894 size=(0, 0), 895 scale=1.3, 896 transition_delay=0.1, 897 color=(1, 0.3, 0.3, 1.0), 898 h_align='center', 899 v_align='center', 900 text=bui.Lstr(resource=f'{self._r}.loadErrorText'), 901 maxwidth=self._scroll_width * 0.9, 902 ) 903 else: 904 905 class _Store: 906 def __init__( 907 self, 908 store_window: StoreBrowserWindow, 909 sdata: dict[str, Any], 910 width: float, 911 ): 912 self._store_window = store_window 913 self._width = width 914 store_data = cstore.get_store_layout() 915 self._tab = sdata['tab'] 916 self._sections = copy.deepcopy(store_data[sdata['tab']]) 917 self._height: float | None = None 918 919 assert bui.app.classic is not None 920 uiscale = bui.app.ui_v1.uiscale 921 922 # Pre-calc a few things and add them to store-data. 923 for section in self._sections: 924 if self._tab == 'characters': 925 dummy_name = 'characters.foo' 926 elif self._tab == 'extras': 927 dummy_name = 'pro' 928 elif self._tab == 'maps': 929 dummy_name = 'maps.foo' 930 elif self._tab == 'icons': 931 dummy_name = 'icons.foo' 932 else: 933 dummy_name = '' 934 section['button_size'] = ( 935 cstore.get_store_item_display_size(dummy_name) 936 ) 937 section['v_spacing'] = ( 938 -25 939 if ( 940 self._tab == 'extras' 941 and uiscale is bui.UIScale.SMALL 942 ) 943 else -17 if self._tab == 'characters' else 0 944 ) 945 if 'title' not in section: 946 section['title'] = '' 947 section['x_offs'] = ( 948 130 949 if self._tab == 'extras' 950 else 270 if self._tab == 'maps' else 0 951 ) 952 section['y_offs'] = ( 953 20 954 if ( 955 self._tab == 'extras' 956 and uiscale is bui.UIScale.SMALL 957 and bui.app.config.get('Merch Link') 958 ) 959 else ( 960 55 961 if ( 962 self._tab == 'extras' 963 and uiscale is bui.UIScale.SMALL 964 ) 965 else -20 if self._tab == 'icons' else 0 966 ) 967 ) 968 969 def instantiate( 970 self, scrollwidget: bui.Widget, tab_button: bui.Widget 971 ) -> None: 972 """Create the store.""" 973 # pylint: disable=too-many-locals 974 # pylint: disable=too-many-branches 975 # pylint: disable=too-many-nested-blocks 976 from bauiv1lib.store.item import ( 977 instantiate_store_item_display, 978 ) 979 980 title_spacing = 40 981 button_border = 20 982 button_spacing = 4 983 boffs_h = 40 984 self._height = 80.0 985 986 # Calc total height. 987 for i, section in enumerate(self._sections): 988 if section['title'] != '': 989 assert self._height is not None 990 self._height += title_spacing 991 b_width, b_height = section['button_size'] 992 b_column_count = int( 993 math.floor( 994 (self._width - boffs_h - 20) 995 / (b_width + button_spacing) 996 ) 997 ) 998 b_row_count = int( 999 math.ceil( 1000 float(len(section['items'])) / b_column_count 1001 ) 1002 ) 1003 b_height_total = ( 1004 2 * button_border 1005 + b_row_count * b_height 1006 + (b_row_count - 1) * section['v_spacing'] 1007 ) 1008 self._height += b_height_total 1009 1010 assert self._height is not None 1011 cnt2 = bui.containerwidget( 1012 parent=scrollwidget, 1013 scale=1.0, 1014 size=(self._width, self._height), 1015 background=False, 1016 claims_left_right=True, 1017 claims_tab=True, 1018 selection_loops_to_parent=True, 1019 ) 1020 v = self._height - 20 1021 1022 if self._tab == 'characters': 1023 txt = bui.Lstr( 1024 resource='store.howToSwitchCharactersText', 1025 subs=[ 1026 ( 1027 '${SETTINGS}', 1028 bui.Lstr( 1029 resource=( 1030 'accountSettingsWindow.titleText' 1031 ) 1032 ), 1033 ), 1034 ( 1035 '${PLAYER_PROFILES}', 1036 bui.Lstr( 1037 resource=( 1038 'playerProfilesWindow.titleText' 1039 ) 1040 ), 1041 ), 1042 ], 1043 ) 1044 bui.textwidget( 1045 parent=cnt2, 1046 text=txt, 1047 size=(0, 0), 1048 position=(self._width * 0.5, self._height - 28), 1049 h_align='center', 1050 v_align='center', 1051 color=(0.7, 1, 0.7, 0.4), 1052 scale=0.7, 1053 shadow=0, 1054 flatness=1.0, 1055 maxwidth=700, 1056 transition_delay=0.4, 1057 ) 1058 elif self._tab == 'icons': 1059 txt = bui.Lstr( 1060 resource='store.howToUseIconsText', 1061 subs=[ 1062 ( 1063 '${SETTINGS}', 1064 bui.Lstr(resource='mainMenu.settingsText'), 1065 ), 1066 ( 1067 '${PLAYER_PROFILES}', 1068 bui.Lstr( 1069 resource=( 1070 'playerProfilesWindow.titleText' 1071 ) 1072 ), 1073 ), 1074 ], 1075 ) 1076 bui.textwidget( 1077 parent=cnt2, 1078 text=txt, 1079 size=(0, 0), 1080 position=(self._width * 0.5, self._height - 28), 1081 h_align='center', 1082 v_align='center', 1083 color=(0.7, 1, 0.7, 0.4), 1084 scale=0.7, 1085 shadow=0, 1086 flatness=1.0, 1087 maxwidth=700, 1088 transition_delay=0.4, 1089 ) 1090 elif self._tab == 'maps': 1091 assert self._width is not None 1092 assert self._height is not None 1093 txt = bui.Lstr(resource='store.howToUseMapsText') 1094 bui.textwidget( 1095 parent=cnt2, 1096 text=txt, 1097 size=(0, 0), 1098 position=(self._width * 0.5, self._height - 28), 1099 h_align='center', 1100 v_align='center', 1101 color=(0.7, 1, 0.7, 0.4), 1102 scale=0.7, 1103 shadow=0, 1104 flatness=1.0, 1105 maxwidth=700, 1106 transition_delay=0.4, 1107 ) 1108 1109 prev_row_buttons: list | None = None 1110 this_row_buttons = [] 1111 1112 delay = 0.3 1113 for section in self._sections: 1114 if section['title'] != '': 1115 bui.textwidget( 1116 parent=cnt2, 1117 position=(60, v - title_spacing * 0.8), 1118 size=(0, 0), 1119 scale=1.0, 1120 transition_delay=delay, 1121 color=(0.7, 0.9, 0.7, 1), 1122 h_align='left', 1123 v_align='center', 1124 text=bui.Lstr(resource=section['title']), 1125 maxwidth=self._width * 0.7, 1126 ) 1127 v -= title_spacing 1128 delay = max(0.100, delay - 0.100) 1129 v -= button_border 1130 b_width, b_height = section['button_size'] 1131 b_count = len(section['items']) 1132 b_column_count = int( 1133 math.floor( 1134 (self._width - boffs_h - 20) 1135 / (b_width + button_spacing) 1136 ) 1137 ) 1138 col = 0 1139 item: dict[str, Any] 1140 assert self._store_window.button_infos is not None 1141 for i, item_name in enumerate(section['items']): 1142 item = self._store_window.button_infos[ 1143 item_name 1144 ] = {} 1145 item['call'] = bui.WeakCall( 1146 self._store_window.buy, item_name 1147 ) 1148 if 'x_offs' in section: 1149 boffs_h2 = section['x_offs'] 1150 else: 1151 boffs_h2 = 0 1152 1153 if 'y_offs' in section: 1154 boffs_v2 = section['y_offs'] 1155 else: 1156 boffs_v2 = 0 1157 b_pos = ( 1158 boffs_h 1159 + boffs_h2 1160 + (b_width + button_spacing) * col, 1161 v - b_height + boffs_v2, 1162 ) 1163 instantiate_store_item_display( 1164 item_name, 1165 item, 1166 parent_widget=cnt2, 1167 b_pos=b_pos, 1168 boffs_h=boffs_h, 1169 b_width=b_width, 1170 b_height=b_height, 1171 boffs_h2=boffs_h2, 1172 boffs_v2=boffs_v2, 1173 delay=delay, 1174 ) 1175 btn = item['button'] 1176 delay = max(0.1, delay - 0.1) 1177 this_row_buttons.append(btn) 1178 1179 # Wire this button to the equivalent in the 1180 # previous row. 1181 if prev_row_buttons is not None: 1182 if len(prev_row_buttons) > col: 1183 bui.widget( 1184 edit=btn, 1185 up_widget=prev_row_buttons[col], 1186 ) 1187 bui.widget( 1188 edit=prev_row_buttons[col], 1189 down_widget=btn, 1190 ) 1191 1192 # If we're the last button in our row, 1193 # wire any in the previous row past 1194 # our position to go to us if down is 1195 # pressed. 1196 if ( 1197 col + 1 == b_column_count 1198 or i == b_count - 1 1199 ): 1200 for b_prev in prev_row_buttons[ 1201 col + 1 : 1202 ]: 1203 bui.widget( 1204 edit=b_prev, down_widget=btn 1205 ) 1206 else: 1207 bui.widget( 1208 edit=btn, up_widget=prev_row_buttons[-1] 1209 ) 1210 else: 1211 bui.widget(edit=btn, up_widget=tab_button) 1212 1213 col += 1 1214 if col == b_column_count or i == b_count - 1: 1215 prev_row_buttons = this_row_buttons 1216 this_row_buttons = [] 1217 col = 0 1218 v -= b_height 1219 if i < b_count - 1: 1220 v -= section['v_spacing'] 1221 1222 v -= button_border 1223 1224 # Set a timer to update these buttons periodically as long 1225 # as we're alive (so if we buy one it will grey out, etc). 1226 self._store_window.update_buttons_timer = bui.AppTimer( 1227 0.5, 1228 bui.WeakCall(self._store_window.update_buttons), 1229 repeat=True, 1230 ) 1231 1232 # Also update them immediately. 1233 self._store_window.update_buttons() 1234 1235 if self._current_tab in ( 1236 self.TabID.EXTRAS, 1237 self.TabID.MINIGAMES, 1238 self.TabID.CHARACTERS, 1239 self.TabID.MAPS, 1240 self.TabID.ICONS, 1241 ): 1242 store = _Store(self, data, self._scroll_width) 1243 assert self._scrollwidget is not None 1244 store.instantiate( 1245 scrollwidget=self._scrollwidget, 1246 tab_button=self._tab_row.tabs[self._current_tab].button, 1247 ) 1248 else: 1249 cnt = bui.containerwidget( 1250 parent=self._scrollwidget, 1251 scale=1.0, 1252 size=(self._scroll_width, self._scroll_height * 0.95), 1253 background=False, 1254 claims_left_right=True, 1255 claims_tab=True, 1256 selection_loops_to_parent=True, 1257 ) 1258 self._status_textwidget = bui.textwidget( 1259 parent=cnt, 1260 position=( 1261 self._scroll_width * 0.5, 1262 self._scroll_height * 0.5, 1263 ), 1264 size=(0, 0), 1265 scale=1.3, 1266 transition_delay=0.1, 1267 color=(1, 1, 0.3, 1.0), 1268 h_align='center', 1269 v_align='center', 1270 text=bui.Lstr(resource=f'{self._r}.comingSoonText'), 1271 maxwidth=self._scroll_width * 0.9, 1272 ) 1273 1274 @override 1275 def get_main_window_state(self) -> bui.MainWindowState: 1276 # Support recreating our window for back/refresh purposes. 1277 cls = type(self) 1278 return bui.BasicMainWindowState( 1279 create_call=lambda transition, origin_widget: cls( 1280 transition=transition, origin_widget=origin_widget 1281 ) 1282 ) 1283 1284 @override 1285 def on_main_window_close(self) -> None: 1286 self._save_state() 1287 1288 def _save_state(self) -> None: 1289 try: 1290 sel = self._root_widget.get_selected_child() 1291 selected_tab_ids = [ 1292 tab_id 1293 for tab_id, tab in self._tab_row.tabs.items() 1294 if sel == tab.button 1295 ] 1296 # if sel == self._get_tickets_button: 1297 # sel_name = 'GetTickets' 1298 if sel == self._scrollwidget: 1299 sel_name = 'Scroll' 1300 elif sel == self._back_button: 1301 sel_name = 'Back' 1302 elif selected_tab_ids: 1303 assert len(selected_tab_ids) == 1 1304 sel_name = f'Tab:{selected_tab_ids[0].value}' 1305 else: 1306 raise ValueError(f'unrecognized selection \'{sel}\'') 1307 assert bui.app.classic is not None 1308 bui.app.ui_v1.window_states[type(self)] = { 1309 'sel_name': sel_name, 1310 } 1311 except Exception: 1312 logging.exception('Error saving state for %s.', self) 1313 1314 def _restore_state(self) -> None: 1315 1316 try: 1317 sel: bui.Widget | None 1318 assert bui.app.classic is not None 1319 sel_name = bui.app.ui_v1.window_states.get(type(self), {}).get( 1320 'sel_name' 1321 ) 1322 assert isinstance(sel_name, (str, type(None))) 1323 1324 try: 1325 current_tab = self.TabID(bui.app.config.get('Store Tab')) 1326 except ValueError: 1327 current_tab = self.TabID.CHARACTERS 1328 1329 if self._show_tab is not None: 1330 current_tab = self._show_tab 1331 # if sel_name == 'GetTickets' and self._get_tickets_button: 1332 # sel = self._get_tickets_button 1333 if sel_name == 'Back': 1334 sel = self._back_button 1335 elif sel_name == 'Scroll': 1336 sel = self._scrollwidget 1337 elif isinstance(sel_name, str) and sel_name.startswith('Tab:'): 1338 try: 1339 sel_tab_id = self.TabID(sel_name.split(':')[-1]) 1340 except ValueError: 1341 sel_tab_id = self.TabID.CHARACTERS 1342 sel = self._tab_row.tabs[sel_tab_id].button 1343 else: 1344 sel = self._tab_row.tabs[current_tab].button 1345 1346 # If we were requested to show a tab, select it too.. 1347 if ( 1348 self._show_tab is not None 1349 and self._show_tab in self._tab_row.tabs 1350 ): 1351 sel = self._tab_row.tabs[self._show_tab].button 1352 self._set_tab(current_tab) 1353 if sel is not None: 1354 bui.containerwidget(edit=self._root_widget, selected_child=sel) 1355 except Exception: 1356 logging.exception('Error restoring state for %s.', self) 1357 1358 # def _on_get_more_tickets_press(self) -> None: 1359 # # pylint: disable=cyclic-import 1360 # from bauiv1lib.account import show_sign_in_prompt 1361 # from bauiv1lib.gettickets import GetTicketsWindow 1362 1363 # # no-op if our underlying widget is dead or on its way out. 1364 # if not self._root_widget or self._root_widget.transitioning_out: 1365 # return 1366 1367 # plus = bui.app.plus 1368 # assert plus is not None 1369 1370 # if plus.get_v1_account_state() != 'signed_in': 1371 # show_sign_in_prompt() 1372 # return 1373 # self._save_state() 1374 # bui.containerwidget(edit=self._root_widget, transition='out_left') 1375 # window = GetTicketsWindow( 1376 # from_modal_store=self._modal, 1377 # store_back_location=self._back_location, 1378 # ) 1379 # if not self._modal: 1380 # assert bui.app.classic is not None 1381 # bui.app.ui_v1.set_main_window(window, from_window=self) 1382 1383 def _back(self) -> None: 1384 # pylint: disable=cyclic-import 1385 # from bauiv1lib.coop.browser import CoopBrowserWindow 1386 # from bauiv1lib.mainmenu import MainMenuWindow 1387 1388 # no-op if our underlying widget is dead or on its way out. 1389 if not self._root_widget or self._root_widget.transitioning_out: 1390 return 1391 1392 self._save_state() 1393 1394 if self._modal: 1395 bui.containerwidget( 1396 edit=self._root_widget, transition=self._transition_out 1397 ) 1398 else: 1399 self.main_window_back() 1400 # if not self._modal: 1401 # assert bui.app.classic is not None 1402 # if self._back_location == 'CoopBrowserWindow': 1403 # bui.app.ui_v1.set_main_window( 1404 # CoopBrowserWindow(transition='in_left'), 1405 # from_window=self, 1406 # is_back=True, 1407 # ) 1408 # else: 1409 # bui.app.ui_v1.set_main_window( 1410 # MainMenuWindow(transition='in_left'), 1411 # from_window=self, 1412 # is_back=True, 1413 # is_top_level=True, 1414 # ) 1415 if self._on_close_call is not None: 1416 self._on_close_call()
Window for browsing the store.
StoreBrowserWindow( transition: str | None = 'in_right', modal: bool = False, show_tab: StoreBrowserWindow.TabID | None = None, on_close_call: Optional[Callable[[], Any]] = None, back_location: str | None = None, origin_widget: _bauiv1.Widget | None = None)
42 def __init__( 43 self, 44 transition: str | None = 'in_right', 45 modal: bool = False, 46 show_tab: StoreBrowserWindow.TabID | None = None, 47 on_close_call: Callable[[], Any] | None = None, 48 back_location: str | None = None, 49 origin_widget: bui.Widget | None = None, 50 ): 51 # pylint: disable=too-many-statements 52 # pylint: disable=too-many-locals 53 from bauiv1lib.tabs import TabRow 54 from bauiv1 import SpecialChar 55 56 app = bui.app 57 assert app.classic is not None 58 uiscale = app.ui_v1.uiscale 59 60 bui.set_analytics_screen('Store Window') 61 62 # Need to store this ourself for modal mode. 63 if origin_widget is not None: 64 self._transition_out = 'out_scale' 65 else: 66 self._transition_out = 'out_right' 67 68 self.button_infos: dict[str, dict[str, Any]] | None = None 69 self.update_buttons_timer: bui.AppTimer | None = None 70 self._status_textwidget_update_timer = None 71 72 self._back_location = back_location 73 self._on_close_call = on_close_call 74 self._show_tab = show_tab 75 self._modal = modal 76 self._width = 1670 if uiscale is bui.UIScale.SMALL else 1040 77 self._x_inset = x_inset = 310 if uiscale is bui.UIScale.SMALL else 0 78 self._height = ( 79 538 80 if uiscale is bui.UIScale.SMALL 81 else 645 if uiscale is bui.UIScale.MEDIUM else 800 82 ) 83 self._current_tab: StoreBrowserWindow.TabID | None = None 84 extra_top = 30 if uiscale is bui.UIScale.SMALL else 0 85 86 self.request: Any = None 87 self._r = 'store' 88 self._last_buy_time: float | None = None 89 90 super().__init__( 91 root_widget=bui.containerwidget( 92 size=(self._width, self._height + extra_top), 93 toolbar_visibility=( 94 'menu_store' 95 if uiscale is bui.UIScale.SMALL 96 else 'menu_full' 97 ), 98 scale=( 99 1.3 100 if uiscale is bui.UIScale.SMALL 101 else 0.9 if uiscale is bui.UIScale.MEDIUM else 0.8 102 ), 103 stack_offset=( 104 (0, 10) 105 if uiscale is bui.UIScale.SMALL 106 else (0, 0) if uiscale is bui.UIScale.MEDIUM else (0, 0) 107 ), 108 ), 109 transition=transition, 110 origin_widget=origin_widget, 111 ) 112 113 self._back_button = btn = bui.buttonwidget( 114 parent=self._root_widget, 115 position=(70 + x_inset, self._height - 74), 116 size=(140, 60), 117 scale=1.1, 118 autoselect=True, 119 label=bui.Lstr(resource='doneText' if self._modal else 'backText'), 120 button_type=None if self._modal else 'back', 121 on_activate_call=self._back, 122 ) 123 124 if uiscale is bui.UIScale.SMALL: 125 self._back_button.delete() 126 bui.containerwidget( 127 edit=self._root_widget, on_cancel_call=self._back 128 ) 129 # backbutton = bui.get_special_widget('back_button') 130 backbuttonspecial = True 131 else: 132 bui.containerwidget(edit=self._root_widget, cancel_button=btn) 133 # backbutton = self._back_button 134 backbuttonspecial = False 135 136 # self._ticket_count_text: bui.Widget | None = None 137 # self._get_tickets_button: bui.Widget | None = None 138 139 # if bool(False): 140 # if app.classic.allow_ticket_purchases: 141 # self._get_tickets_button = bui.buttonwidget( 142 # parent=self._root_widget, 143 # size=(210, 65), 144 # on_activate_call=self._on_get_more_tickets_press, 145 # autoselect=True, 146 # scale=0.9, 147 # text_scale=1.4, 148 # left_widget=backbutton, 149 # color=(0.7, 0.5, 0.85), 150 # textcolor=(0.2, 1.0, 0.2), 151 # label=bui.Lstr(resource='getTicketsWindow.titleText'), 152 # ) 153 # else: 154 # self._ticket_count_text = bui.textwidget( 155 # parent=self._root_widget, 156 # size=(210, 64), 157 # color=(0.2, 1.0, 0.2), 158 # h_align='center', 159 # v_align='center', 160 # ) 161 162 # Move this dynamically to keep it out of the way of the party icon. 163 # self._update_get_tickets_button_pos() 164 # self._get_ticket_pos_update_timer = bui.AppTimer( 165 # 1.0, 166 # bui.WeakCall(self._update_get_tickets_button_pos), 167 # repeat=True, 168 # ) 169 # if self._get_tickets_button and not backbuttonspecial: 170 # bui.widget( 171 # edit=self._back_button, right_widget=self._get_tickets_button 172 # ) 173 # self._ticket_text_update_timer = bui.AppTimer( 174 # 1.0, bui.WeakCall(self._update_tickets_text), repeat=True 175 # ) 176 # self._update_tickets_text() 177 178 if ( 179 app.classic.platform in ['mac', 'ios'] 180 and app.classic.subplatform == 'appstore' 181 ): 182 bui.buttonwidget( 183 parent=self._root_widget, 184 position=(self._width * 0.5 - 70, 16), 185 size=(230, 50), 186 scale=0.65, 187 on_activate_call=bui.WeakCall(self._restore_purchases), 188 color=(0.35, 0.3, 0.4), 189 selectable=False, 190 textcolor=(0.55, 0.5, 0.6), 191 label=bui.Lstr( 192 resource='getTicketsWindow.restorePurchasesText' 193 ), 194 ) 195 196 bui.textwidget( 197 parent=self._root_widget, 198 position=( 199 self._width * 0.5, 200 self._height - (53 if uiscale is bui.UIScale.SMALL else 44), 201 ), 202 size=(0, 0), 203 color=app.ui_v1.title_color, 204 scale=1.5, 205 h_align='center', 206 v_align='center', 207 text=bui.Lstr(resource='storeText'), 208 maxwidth=290, 209 ) 210 211 if not self._modal and not backbuttonspecial: 212 bui.buttonwidget( 213 edit=self._back_button, 214 button_type='backSmall', 215 size=(60, 60), 216 label=bui.charstr(SpecialChar.BACK), 217 ) 218 219 scroll_buffer_h = 130 + 2 * x_inset 220 tab_buffer_h = 250 + 2 * x_inset 221 222 tabs_def = [ 223 (self.TabID.EXTRAS, bui.Lstr(resource=f'{self._r}.extrasText')), 224 (self.TabID.MAPS, bui.Lstr(resource=f'{self._r}.mapsText')), 225 ( 226 self.TabID.MINIGAMES, 227 bui.Lstr(resource=f'{self._r}.miniGamesText'), 228 ), 229 ( 230 self.TabID.CHARACTERS, 231 bui.Lstr(resource=f'{self._r}.charactersText'), 232 ), 233 (self.TabID.ICONS, bui.Lstr(resource=f'{self._r}.iconsText')), 234 ] 235 236 self._tab_row = TabRow( 237 self._root_widget, 238 tabs_def, 239 pos=(tab_buffer_h * 0.5, self._height - 130), 240 size=(self._width - tab_buffer_h, 50), 241 on_select_call=self._set_tab, 242 ) 243 244 self._purchasable_count_widgets: dict[ 245 StoreBrowserWindow.TabID, dict[str, Any] 246 ] = {} 247 248 # Create our purchasable-items tags and have them update over time. 249 for tab_id, tab in self._tab_row.tabs.items(): 250 pos = tab.position 251 size = tab.size 252 button = tab.button 253 rad = 10 254 center = (pos[0] + 0.1 * size[0], pos[1] + 0.9 * size[1]) 255 img = bui.imagewidget( 256 parent=self._root_widget, 257 position=(center[0] - rad * 1.04, center[1] - rad * 1.15), 258 size=(rad * 2.2, rad * 2.2), 259 texture=bui.gettexture('circleShadow'), 260 color=(1, 0, 0), 261 ) 262 txt = bui.textwidget( 263 parent=self._root_widget, 264 position=center, 265 size=(0, 0), 266 h_align='center', 267 v_align='center', 268 maxwidth=1.4 * rad, 269 scale=0.6, 270 shadow=1.0, 271 flatness=1.0, 272 ) 273 rad = 20 274 sale_img = bui.imagewidget( 275 parent=self._root_widget, 276 position=(center[0] - rad, center[1] - rad), 277 size=(rad * 2, rad * 2), 278 draw_controller=button, 279 texture=bui.gettexture('circleZigZag'), 280 color=(0.5, 0, 1.0), 281 ) 282 sale_title_text = bui.textwidget( 283 parent=self._root_widget, 284 position=(center[0], center[1] + 0.24 * rad), 285 size=(0, 0), 286 h_align='center', 287 v_align='center', 288 draw_controller=button, 289 maxwidth=1.4 * rad, 290 scale=0.6, 291 shadow=0.0, 292 flatness=1.0, 293 color=(0, 1, 0), 294 ) 295 sale_time_text = bui.textwidget( 296 parent=self._root_widget, 297 position=(center[0], center[1] - 0.29 * rad), 298 size=(0, 0), 299 h_align='center', 300 v_align='center', 301 draw_controller=button, 302 maxwidth=1.4 * rad, 303 scale=0.4, 304 shadow=0.0, 305 flatness=1.0, 306 color=(0, 1, 0), 307 ) 308 self._purchasable_count_widgets[tab_id] = { 309 'img': img, 310 'text': txt, 311 'sale_img': sale_img, 312 'sale_title_text': sale_title_text, 313 'sale_time_text': sale_time_text, 314 } 315 self._tab_update_timer = bui.AppTimer( 316 1.0, bui.WeakCall(self._update_tabs), repeat=True 317 ) 318 self._update_tabs() 319 320 # if self._get_tickets_button: 321 # last_tab_button = self._tab_row.tabs[tabs_def[-1][0]].button 322 # bui.widget( 323 # edit=self._get_tickets_button, down_widget=last_tab_button 324 # ) 325 # bui.widget( 326 # edit=last_tab_button, 327 # up_widget=self._get_tickets_button, 328 # right_widget=self._get_tickets_button, 329 # ) 330 331 if uiscale is bui.UIScale.SMALL: 332 first_tab_button = self._tab_row.tabs[tabs_def[0][0]].button 333 last_tab_button = self._tab_row.tabs[tabs_def[-1][0]].button 334 bui.widget( 335 edit=first_tab_button, 336 left_widget=bui.get_special_widget('back_button'), 337 ) 338 bui.widget( 339 edit=last_tab_button, 340 right_widget=bui.get_special_widget('squad_button'), 341 ) 342 343 self._scroll_width = self._width - scroll_buffer_h 344 self._scroll_height = self._height - 180 345 346 self._scrollwidget: bui.Widget | None = None 347 self._status_textwidget: bui.Widget | None = None 348 self._restore_state()
Create a MainWindow given a root widget and transition info.
Automatically handles in and out transitions on the provided widget, so there is no need to set transitions when creating it.
def
buy(self, item: str) -> None:
600 def buy(self, item: str) -> None: 601 """Attempt to purchase the provided item.""" 602 from bauiv1lib import account 603 from bauiv1lib.confirm import ConfirmWindow 604 605 # from bauiv1lib import gettickets 606 607 assert bui.app.classic is not None 608 store = bui.app.classic.store 609 610 plus = bui.app.plus 611 assert plus is not None 612 613 # Prevent pressing buy within a few seconds of the last press 614 # (gives the buttons time to disable themselves and whatnot). 615 curtime = bui.apptime() 616 if ( 617 self._last_buy_time is not None 618 and (curtime - self._last_buy_time) < 2.0 619 ): 620 bui.getsound('error').play() 621 else: 622 if plus.get_v1_account_state() != 'signed_in': 623 account.show_sign_in_prompt() 624 else: 625 self._last_buy_time = curtime 626 627 # Merch is a special case - just a link. 628 if item == 'merch': 629 url = bui.app.config.get('Merch Link') 630 if isinstance(url, str): 631 bui.open_url(url) 632 633 # Pro is an actual IAP, and the rest are ticket purchases. 634 elif item == 'pro': 635 bui.getsound('click01').play() 636 637 # Purchase either pro or pro_sale depending on whether 638 # there is a sale going on. 639 self._do_purchase_check( 640 'pro' 641 if store.get_available_sale_time('extras') is None 642 else 'pro_sale' 643 ) 644 else: 645 price = plus.get_v1_account_misc_read_val( 646 'price.' + item, None 647 ) 648 our_tickets = plus.get_v1_account_ticket_count() 649 if price is not None and our_tickets < price: 650 bui.getsound('error').play() 651 print('FIXME - show not-enough-tickets info.') 652 # gettickets.show_get_tickets_prompt() 653 else: 654 655 def do_it() -> None: 656 self._do_purchase_check( 657 item, is_ticket_purchase=True 658 ) 659 660 bui.getsound('swish').play() 661 ConfirmWindow( 662 bui.Lstr( 663 resource='store.purchaseConfirmText', 664 subs=[ 665 ( 666 '${ITEM}', 667 store.get_store_item_name_translated( 668 item 669 ), 670 ) 671 ], 672 ), 673 width=400, 674 height=120, 675 action=do_it, 676 ok_text=bui.Lstr( 677 resource='store.purchaseText', 678 fallback_resource='okText', 679 ), 680 )
Attempt to purchase the provided item.
1274 @override 1275 def get_main_window_state(self) -> bui.MainWindowState: 1276 # Support recreating our window for back/refresh purposes. 1277 cls = type(self) 1278 return bui.BasicMainWindowState( 1279 create_call=lambda transition, origin_widget: cls( 1280 transition=transition, origin_widget=origin_widget 1281 ) 1282 )
Return a WindowState to recreate this window, if supported.
@override
def
on_main_window_close(self) -> None:
Called before transitioning out a main window.
A good opportunity to save window state/etc.
Inherited Members
- bauiv1._uitypes.MainWindow
- main_window_back_state
- main_window_close
- can_change_main_window
- main_window_back
- main_window_replace
- bauiv1._uitypes.Window
- get_root_widget
class
StoreBrowserWindow.TabID(enum.Enum):
33 class TabID(Enum): 34 """Our available tab types.""" 35 36 EXTRAS = 'extras' 37 MAPS = 'maps' 38 MINIGAMES = 'minigames' 39 CHARACTERS = 'characters' 40 ICONS = 'icons'
Our available tab types.
Inherited Members
- enum.Enum
- name
- value