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