bauiv1lib.playlist.customizebrowser
Provides UI for viewing/creating/editing playlists.
1# Released under the MIT License. See LICENSE for details. 2# 3"""Provides UI for viewing/creating/editing playlists.""" 4 5from __future__ import annotations 6 7import copy 8import time 9import logging 10from typing import TYPE_CHECKING, override 11 12import bascenev1 as bs 13import bauiv1 as bui 14 15if TYPE_CHECKING: 16 from typing import Any 17 18REQUIRE_PRO = False 19 20# TEMP 21UNDER_CONSTRUCTION = True 22 23 24class PlaylistCustomizeBrowserWindow(bui.MainWindow): 25 """Window for viewing a playlist.""" 26 27 def __init__( 28 self, 29 sessiontype: type[bs.Session], 30 transition: str | None = 'in_right', 31 origin_widget: bui.Widget | None = None, 32 select_playlist: str | None = None, 33 ): 34 # Yes this needs tidying. 35 # pylint: disable=too-many-locals 36 # pylint: disable=too-many-statements 37 # pylint: disable=cyclic-import 38 from bauiv1lib import playlist 39 40 self._sessiontype = sessiontype 41 self._pvars = playlist.PlaylistTypeVars(sessiontype) 42 self._max_playlists = 30 43 self._r = 'gameListWindow' 44 assert bui.app.classic is not None 45 uiscale = bui.app.ui_v1.uiscale 46 self._width = 970.0 if uiscale is bui.UIScale.SMALL else 650.0 47 x_inset = 100.0 if uiscale is bui.UIScale.SMALL else 0.0 48 self._height = ( 49 380.0 50 if uiscale is bui.UIScale.SMALL 51 else 420.0 if uiscale is bui.UIScale.MEDIUM else 500.0 52 ) 53 top_extra = 20.0 if uiscale is bui.UIScale.SMALL else 0.0 54 55 super().__init__( 56 root_widget=bui.containerwidget( 57 size=(self._width, self._height + top_extra), 58 scale=( 59 1.83 60 if uiscale is bui.UIScale.SMALL 61 else 1.5 if uiscale is bui.UIScale.MEDIUM else 1.0 62 ), 63 toolbar_visibility=( 64 'menu_minimal' 65 if uiscale is bui.UIScale.SMALL 66 else 'menu_full' 67 ), 68 stack_offset=( 69 (0, -10) if uiscale is bui.UIScale.SMALL else (0, 0) 70 ), 71 ), 72 transition=transition, 73 origin_widget=origin_widget, 74 ) 75 76 self._back_button: bui.Widget | None 77 if uiscale is bui.UIScale.SMALL: 78 self._back_button = None 79 bui.containerwidget( 80 edit=self._root_widget, on_cancel_call=self._back 81 ) 82 else: 83 self._back_button = bui.buttonwidget( 84 parent=self._root_widget, 85 position=(43 + x_inset, self._height - 60), 86 size=(160, 68), 87 scale=0.77, 88 autoselect=True, 89 text_scale=1.3, 90 label=bui.Lstr(resource='backText'), 91 button_type='back', 92 ) 93 bui.buttonwidget( 94 edit=self._back_button, 95 button_type='backSmall', 96 size=(60, 60), 97 label=bui.charstr(bui.SpecialChar.BACK), 98 ) 99 100 bui.textwidget( 101 parent=self._root_widget, 102 position=(0, self._height - 47), 103 size=(self._width, 25), 104 text=bui.Lstr( 105 resource=f'{self._r}.titleText', 106 subs=[('${TYPE}', self._pvars.window_title_name)], 107 ), 108 color=bui.app.ui_v1.heading_color, 109 maxwidth=290, 110 h_align='center', 111 v_align='center', 112 ) 113 114 v = self._height - 59.0 115 h = 41 + x_inset 116 b_color = (0.6, 0.53, 0.63) 117 b_textcolor = (0.75, 0.7, 0.8) 118 self._lock_images: list[bui.Widget] = [] 119 lock_tex = bui.gettexture('lock') 120 121 scl = ( 122 1.1 123 if uiscale is bui.UIScale.SMALL 124 else 1.27 if uiscale is bui.UIScale.MEDIUM else 1.57 125 ) 126 scl *= 0.63 127 v -= 65.0 * scl 128 new_button = btn = bui.buttonwidget( 129 parent=self._root_widget, 130 position=(h, v), 131 size=(90, 58.0 * scl), 132 on_activate_call=self._new_playlist, 133 color=b_color, 134 autoselect=True, 135 button_type='square', 136 textcolor=b_textcolor, 137 text_scale=0.7, 138 label=bui.Lstr( 139 resource='newText', fallback_resource=f'{self._r}.newText' 140 ), 141 ) 142 self._lock_images.append( 143 bui.imagewidget( 144 parent=self._root_widget, 145 size=(30, 30), 146 draw_controller=btn, 147 position=(h - 10, v + 58.0 * scl - 28), 148 texture=lock_tex, 149 ) 150 ) 151 152 v -= 65.0 * scl 153 self._edit_button = edit_button = btn = bui.buttonwidget( 154 parent=self._root_widget, 155 position=(h, v), 156 size=(90, 58.0 * scl), 157 on_activate_call=self._edit_playlist, 158 color=b_color, 159 autoselect=True, 160 textcolor=b_textcolor, 161 button_type='square', 162 text_scale=0.7, 163 label=bui.Lstr( 164 resource='editText', fallback_resource=f'{self._r}.editText' 165 ), 166 ) 167 self._lock_images.append( 168 bui.imagewidget( 169 parent=self._root_widget, 170 size=(30, 30), 171 draw_controller=btn, 172 position=(h - 10, v + 58.0 * scl - 28), 173 texture=lock_tex, 174 ) 175 ) 176 177 v -= 65.0 * scl 178 duplicate_button = btn = bui.buttonwidget( 179 parent=self._root_widget, 180 position=(h, v), 181 size=(90, 58.0 * scl), 182 on_activate_call=self._duplicate_playlist, 183 color=b_color, 184 autoselect=True, 185 textcolor=b_textcolor, 186 button_type='square', 187 text_scale=0.7, 188 label=bui.Lstr( 189 resource='duplicateText', 190 fallback_resource=f'{self._r}.duplicateText', 191 ), 192 ) 193 self._lock_images.append( 194 bui.imagewidget( 195 parent=self._root_widget, 196 size=(30, 30), 197 draw_controller=btn, 198 position=(h - 10, v + 58.0 * scl - 28), 199 texture=lock_tex, 200 ) 201 ) 202 203 v -= 65.0 * scl 204 delete_button = btn = bui.buttonwidget( 205 parent=self._root_widget, 206 position=(h, v), 207 size=(90, 58.0 * scl), 208 on_activate_call=self._delete_playlist, 209 color=b_color, 210 autoselect=True, 211 textcolor=b_textcolor, 212 button_type='square', 213 text_scale=0.7, 214 label=bui.Lstr( 215 resource='deleteText', fallback_resource=f'{self._r}.deleteText' 216 ), 217 ) 218 self._lock_images.append( 219 bui.imagewidget( 220 parent=self._root_widget, 221 size=(30, 30), 222 draw_controller=btn, 223 position=(h - 10, v + 58.0 * scl - 28), 224 texture=lock_tex, 225 ) 226 ) 227 v -= 65.0 * scl 228 self._import_button = bui.buttonwidget( 229 parent=self._root_widget, 230 position=(h, v), 231 size=(90, 58.0 * scl), 232 on_activate_call=self._import_playlist, 233 color=b_color, 234 autoselect=True, 235 textcolor=b_textcolor, 236 button_type='square', 237 text_scale=0.7, 238 label=bui.Lstr(resource='importText'), 239 ) 240 v -= 65.0 * scl 241 btn = bui.buttonwidget( 242 parent=self._root_widget, 243 position=(h, v), 244 size=(90, 58.0 * scl), 245 on_activate_call=self._share_playlist, 246 color=b_color, 247 autoselect=True, 248 textcolor=b_textcolor, 249 button_type='square', 250 text_scale=0.7, 251 label=bui.Lstr(resource='shareText'), 252 ) 253 self._lock_images.append( 254 bui.imagewidget( 255 parent=self._root_widget, 256 size=(30, 30), 257 draw_controller=btn, 258 position=(h - 10, v + 58.0 * scl - 28), 259 texture=lock_tex, 260 ) 261 ) 262 263 v = self._height - 75 264 self._scroll_height = self._height - 119 265 scrollwidget = bui.scrollwidget( 266 parent=self._root_widget, 267 position=(140 + x_inset, v - self._scroll_height), 268 size=(self._width - (180 + 2 * x_inset), self._scroll_height + 10), 269 highlight=False, 270 ) 271 if self._back_button is not None: 272 bui.widget(edit=self._back_button, right_widget=scrollwidget) 273 274 self._columnwidget = bui.columnwidget( 275 parent=scrollwidget, border=2, margin=0 276 ) 277 278 h = 145 279 280 self._do_randomize_val = bui.app.config.get( 281 self._pvars.config_name + ' Playlist Randomize', 0 282 ) 283 284 h += 210 285 286 for btn in [new_button, delete_button, edit_button, duplicate_button]: 287 bui.widget(edit=btn, right_widget=scrollwidget) 288 bui.widget( 289 edit=scrollwidget, 290 left_widget=new_button, 291 right_widget=bui.get_special_widget('squad_button'), 292 ) 293 294 # make sure config exists 295 self._config_name_full = self._pvars.config_name + ' Playlists' 296 297 if self._config_name_full not in bui.app.config: 298 bui.app.config[self._config_name_full] = {} 299 300 self._selected_playlist_name: str | None = None 301 self._selected_playlist_index: int | None = None 302 self._playlist_widgets: list[bui.Widget] = [] 303 304 self._refresh(select_playlist=select_playlist) 305 306 if self._back_button is not None: 307 bui.buttonwidget( 308 edit=self._back_button, on_activate_call=self._back 309 ) 310 bui.containerwidget( 311 edit=self._root_widget, cancel_button=self._back_button 312 ) 313 314 bui.containerwidget(edit=self._root_widget, selected_child=scrollwidget) 315 316 # Keep our lock images up to date/etc. 317 self._update_timer = bui.AppTimer( 318 1.0, bui.WeakCall(self._update), repeat=True 319 ) 320 self._update() 321 322 @override 323 def get_main_window_state(self) -> bui.MainWindowState: 324 # Support recreating our window for back/refresh purposes. 325 cls = type(self) 326 327 # Avoid dereferencing self within the lambda or we'll keep 328 # ourself alive indefinitely. 329 stype = self._sessiontype 330 331 return bui.BasicMainWindowState( 332 create_call=lambda transition, origin_widget: cls( 333 transition=transition, 334 origin_widget=origin_widget, 335 sessiontype=stype, 336 ) 337 ) 338 339 # @override 340 # def on_main_window_close(self) -> None: 341 # self._save_state() 342 343 def _update(self) -> None: 344 assert bui.app.classic is not None 345 have = bui.app.classic.accounts.have_pro_options() 346 for lock in self._lock_images: 347 bui.imagewidget( 348 edit=lock, opacity=0.0 if (have or not REQUIRE_PRO) else 1.0 349 ) 350 351 def _back(self) -> None: 352 # pylint: disable=cyclic-import 353 # from bauiv1lib.playlist import browser 354 355 # no-op if our underlying widget is dead or on its way out. 356 if not self._root_widget or self._root_widget.transitioning_out: 357 return 358 359 if self._selected_playlist_name is not None: 360 cfg = bui.app.config 361 cfg[self._pvars.config_name + ' Playlist Selection'] = ( 362 self._selected_playlist_name 363 ) 364 cfg.commit() 365 366 self.main_window_back() 367 # bui.containerwidget( 368 # edit=self._root_widget, transition=self._transition_out 369 # ) 370 # assert bui.app.classic is not None 371 # bui.app.ui_v1.set_main_window( 372 # browser.PlaylistBrowserWindow( 373 # transition='in_left', sessiontype=self._sessiontype 374 # ), 375 # from_window=self, 376 # is_back=True, 377 # ) 378 379 def _select(self, name: str, index: int) -> None: 380 self._selected_playlist_name = name 381 self._selected_playlist_index = index 382 383 def _run_selected_playlist(self) -> None: 384 # pylint: disable=cyclic-import 385 bui.unlock_all_input() 386 try: 387 bs.new_host_session(self._sessiontype) 388 except Exception: 389 from bascenev1lib import mainmenu 390 391 logging.exception('Error running session %s.', self._sessiontype) 392 393 # Drop back into a main menu session. 394 bs.new_host_session(mainmenu.MainMenuSession) 395 396 def _choose_playlist(self) -> None: 397 if self._selected_playlist_name is None: 398 return 399 self._save_playlist_selection() 400 bui.containerwidget(edit=self._root_widget, transition='out_left') 401 bui.fade_screen(False, endcall=self._run_selected_playlist) 402 bui.lock_all_input() 403 404 def _refresh(self, select_playlist: str | None = None) -> None: 405 from efro.util import asserttype 406 407 old_selection = self._selected_playlist_name 408 409 # If there was no prev selection, look in prefs. 410 if old_selection is None: 411 old_selection = bui.app.config.get( 412 self._pvars.config_name + ' Playlist Selection' 413 ) 414 415 old_selection_index = self._selected_playlist_index 416 417 # Delete old. 418 while self._playlist_widgets: 419 self._playlist_widgets.pop().delete() 420 421 items = list(bui.app.config[self._config_name_full].items()) 422 423 # Make sure everything is unicode now. 424 items = [ 425 (i[0].decode(), i[1]) if not isinstance(i[0], str) else i 426 for i in items 427 ] 428 429 items.sort(key=lambda x: asserttype(x[0], str).lower()) 430 431 items = [['__default__', None]] + items # Default is always first. 432 index = 0 433 for pname, _ in items: 434 assert pname is not None 435 txtw = bui.textwidget( 436 parent=self._columnwidget, 437 size=(self._width - 40, 30), 438 maxwidth=440, 439 text=self._get_playlist_display_name(pname), 440 h_align='left', 441 v_align='center', 442 color=( 443 (0.6, 0.6, 0.7, 1.0) 444 if pname == '__default__' 445 else (0.85, 0.85, 0.85, 1) 446 ), 447 always_highlight=True, 448 on_select_call=bui.Call(self._select, pname, index), 449 on_activate_call=bui.Call(self._edit_button.activate), 450 selectable=True, 451 ) 452 bui.widget(edit=txtw, show_buffer_top=50, show_buffer_bottom=50) 453 454 # Hitting up from top widget should jump to 'back' 455 if index == 0: 456 bui.widget( 457 edit=txtw, 458 up_widget=( 459 self._back_button 460 if self._back_button is not None 461 else bui.get_special_widget('back_button') 462 ), 463 ) 464 465 self._playlist_widgets.append(txtw) 466 467 # Select this one if the user requested it. 468 if select_playlist is not None: 469 if pname == select_playlist: 470 bui.columnwidget( 471 edit=self._columnwidget, 472 selected_child=txtw, 473 visible_child=txtw, 474 ) 475 else: 476 # Select this one if it was previously selected. 477 # Go by index if there's one. 478 if old_selection_index is not None: 479 if index == old_selection_index: 480 bui.columnwidget( 481 edit=self._columnwidget, 482 selected_child=txtw, 483 visible_child=txtw, 484 ) 485 else: # Otherwise look by name. 486 if pname == old_selection: 487 bui.columnwidget( 488 edit=self._columnwidget, 489 selected_child=txtw, 490 visible_child=txtw, 491 ) 492 493 index += 1 494 495 def _save_playlist_selection(self) -> None: 496 # Store the selected playlist in prefs. 497 # This serves dual purposes of letting us re-select it next time 498 # if we want and also lets us pass it to the game (since we reset 499 # the whole python environment that's not actually easy). 500 cfg = bui.app.config 501 cfg[self._pvars.config_name + ' Playlist Selection'] = ( 502 self._selected_playlist_name 503 ) 504 cfg[self._pvars.config_name + ' Playlist Randomize'] = ( 505 self._do_randomize_val 506 ) 507 cfg.commit() 508 509 def _new_playlist(self) -> None: 510 # pylint: disable=cyclic-import 511 from bauiv1lib.playlist.editcontroller import PlaylistEditController 512 from bauiv1lib.purchase import PurchaseWindow 513 514 if UNDER_CONSTRUCTION: 515 bui.screenmessage('UNDER CONSTRUCTION') 516 return 517 518 assert bui.app.classic is not None 519 if REQUIRE_PRO and not bui.app.classic.accounts.have_pro_options(): 520 PurchaseWindow(items=['pro']) 521 return 522 523 # Clamp at our max playlist number. 524 if len(bui.app.config[self._config_name_full]) > self._max_playlists: 525 bui.screenmessage( 526 bui.Lstr( 527 translate=( 528 'serverResponses', 529 'Max number of playlists reached.', 530 ) 531 ), 532 color=(1, 0, 0), 533 ) 534 bui.getsound('error').play() 535 return 536 537 # In case they cancel so we can return to this state. 538 self._save_playlist_selection() 539 540 # Kick off the edit UI. 541 PlaylistEditController(sessiontype=self._sessiontype) 542 bui.containerwidget(edit=self._root_widget, transition='out_left') 543 544 def _edit_playlist(self) -> None: 545 # pylint: disable=cyclic-import 546 from bauiv1lib.playlist.editcontroller import PlaylistEditController 547 from bauiv1lib.purchase import PurchaseWindow 548 549 if UNDER_CONSTRUCTION: 550 bui.screenmessage('UNDER CONSTRUCTION') 551 return 552 553 assert bui.app.classic is not None 554 if REQUIRE_PRO and not bui.app.classic.accounts.have_pro_options(): 555 PurchaseWindow(items=['pro']) 556 return 557 if self._selected_playlist_name is None: 558 return 559 if self._selected_playlist_name == '__default__': 560 bui.getsound('error').play() 561 bui.screenmessage( 562 bui.Lstr(resource=f'{self._r}.cantEditDefaultText') 563 ) 564 return 565 self._save_playlist_selection() 566 PlaylistEditController( 567 existing_playlist_name=self._selected_playlist_name, 568 sessiontype=self._sessiontype, 569 ) 570 bui.containerwidget(edit=self._root_widget, transition='out_left') 571 572 def _do_delete_playlist(self) -> None: 573 plus = bui.app.plus 574 assert plus is not None 575 plus.add_v1_account_transaction( 576 { 577 'type': 'REMOVE_PLAYLIST', 578 'playlistType': self._pvars.config_name, 579 'playlistName': self._selected_playlist_name, 580 } 581 ) 582 plus.run_v1_account_transactions() 583 bui.getsound('shieldDown').play() 584 585 # (we don't use len()-1 here because the default list adds one) 586 assert self._selected_playlist_index is not None 587 self._selected_playlist_index = min( 588 self._selected_playlist_index, 589 len(bui.app.config[self._pvars.config_name + ' Playlists']), 590 ) 591 self._refresh() 592 593 def _import_playlist(self) -> None: 594 # pylint: disable=cyclic-import 595 from bauiv1lib.playlist import share 596 597 plus = bui.app.plus 598 assert plus is not None 599 600 # Gotta be signed in for this to work. 601 if plus.get_v1_account_state() != 'signed_in': 602 bui.screenmessage( 603 bui.Lstr(resource='notSignedInErrorText'), color=(1, 0, 0) 604 ) 605 bui.getsound('error').play() 606 return 607 608 share.SharePlaylistImportWindow( 609 origin_widget=self._import_button, 610 on_success_callback=bui.WeakCall(self._on_playlist_import_success), 611 ) 612 613 def _on_playlist_import_success(self) -> None: 614 self._refresh() 615 616 def _on_share_playlist_response(self, name: str, response: Any) -> None: 617 # pylint: disable=cyclic-import 618 from bauiv1lib.playlist import share 619 620 if response is None: 621 bui.screenmessage( 622 bui.Lstr(resource='internal.unavailableNoConnectionText'), 623 color=(1, 0, 0), 624 ) 625 bui.getsound('error').play() 626 return 627 share.SharePlaylistResultsWindow(name, response) 628 629 def _share_playlist(self) -> None: 630 # pylint: disable=cyclic-import 631 from bauiv1lib.purchase import PurchaseWindow 632 633 plus = bui.app.plus 634 assert plus is not None 635 636 assert bui.app.classic is not None 637 if REQUIRE_PRO and not bui.app.classic.accounts.have_pro_options(): 638 PurchaseWindow(items=['pro']) 639 return 640 641 # Gotta be signed in for this to work. 642 if plus.get_v1_account_state() != 'signed_in': 643 bui.screenmessage( 644 bui.Lstr(resource='notSignedInErrorText'), color=(1, 0, 0) 645 ) 646 bui.getsound('error').play() 647 return 648 if self._selected_playlist_name == '__default__': 649 bui.getsound('error').play() 650 bui.screenmessage( 651 bui.Lstr(resource=f'{self._r}.cantShareDefaultText'), 652 color=(1, 0, 0), 653 ) 654 return 655 656 if self._selected_playlist_name is None: 657 return 658 659 plus.add_v1_account_transaction( 660 { 661 'type': 'SHARE_PLAYLIST', 662 'expire_time': time.time() + 5, 663 'playlistType': self._pvars.config_name, 664 'playlistName': self._selected_playlist_name, 665 }, 666 callback=bui.WeakCall( 667 self._on_share_playlist_response, self._selected_playlist_name 668 ), 669 ) 670 plus.run_v1_account_transactions() 671 bui.screenmessage(bui.Lstr(resource='sharingText')) 672 673 def _delete_playlist(self) -> None: 674 # pylint: disable=cyclic-import 675 from bauiv1lib.purchase import PurchaseWindow 676 from bauiv1lib.confirm import ConfirmWindow 677 678 assert bui.app.classic is not None 679 if REQUIRE_PRO and not bui.app.classic.accounts.have_pro_options(): 680 PurchaseWindow(items=['pro']) 681 return 682 683 if self._selected_playlist_name is None: 684 return 685 if self._selected_playlist_name == '__default__': 686 bui.getsound('error').play() 687 bui.screenmessage( 688 bui.Lstr(resource=f'{self._r}.cantDeleteDefaultText') 689 ) 690 else: 691 ConfirmWindow( 692 bui.Lstr( 693 resource=f'{self._r}.deleteConfirmText', 694 subs=[('${LIST}', self._selected_playlist_name)], 695 ), 696 self._do_delete_playlist, 697 450, 698 150, 699 ) 700 701 def _get_playlist_display_name(self, playlist: str) -> bui.Lstr: 702 if playlist == '__default__': 703 return self._pvars.default_list_name 704 return ( 705 playlist 706 if isinstance(playlist, bui.Lstr) 707 else bui.Lstr(value=playlist) 708 ) 709 710 def _duplicate_playlist(self) -> None: 711 # pylint: disable=too-many-branches 712 # pylint: disable=cyclic-import 713 from bauiv1lib.purchase import PurchaseWindow 714 715 plus = bui.app.plus 716 assert plus is not None 717 718 assert bui.app.classic is not None 719 if REQUIRE_PRO and not bui.app.classic.accounts.have_pro_options(): 720 PurchaseWindow(items=['pro']) 721 return 722 if self._selected_playlist_name is None: 723 return 724 plst: list[dict[str, Any]] | None 725 if self._selected_playlist_name == '__default__': 726 plst = self._pvars.get_default_list_call() 727 else: 728 plst = bui.app.config[self._config_name_full].get( 729 self._selected_playlist_name 730 ) 731 if plst is None: 732 bui.getsound('error').play() 733 return 734 735 # clamp at our max playlist number 736 if len(bui.app.config[self._config_name_full]) > self._max_playlists: 737 bui.screenmessage( 738 bui.Lstr( 739 translate=( 740 'serverResponses', 741 'Max number of playlists reached.', 742 ) 743 ), 744 color=(1, 0, 0), 745 ) 746 bui.getsound('error').play() 747 return 748 749 copy_text = bui.Lstr(resource='copyOfText').evaluate() 750 # get just 'Copy' or whatnot 751 copy_word = copy_text.replace('${NAME}', '').strip() 752 # find a valid dup name that doesn't exist 753 754 test_index = 1 755 base_name = self._get_playlist_display_name( 756 self._selected_playlist_name 757 ).evaluate() 758 759 # If it looks like a copy, strip digits and spaces off the end. 760 if copy_word in base_name: 761 while base_name[-1].isdigit() or base_name[-1] == ' ': 762 base_name = base_name[:-1] 763 while True: 764 if copy_word in base_name: 765 test_name = base_name 766 else: 767 test_name = copy_text.replace('${NAME}', base_name) 768 if test_index > 1: 769 test_name += ' ' + str(test_index) 770 if test_name not in bui.app.config[self._config_name_full]: 771 break 772 test_index += 1 773 774 plus.add_v1_account_transaction( 775 { 776 'type': 'ADD_PLAYLIST', 777 'playlistType': self._pvars.config_name, 778 'playlistName': test_name, 779 'playlist': copy.deepcopy(plst), 780 } 781 ) 782 plus.run_v1_account_transactions() 783 784 bui.getsound('gunCocking').play() 785 self._refresh(select_playlist=test_name)
REQUIRE_PRO =
False
UNDER_CONSTRUCTION =
True
class
PlaylistCustomizeBrowserWindow(bauiv1._uitypes.MainWindow):
25class PlaylistCustomizeBrowserWindow(bui.MainWindow): 26 """Window for viewing a playlist.""" 27 28 def __init__( 29 self, 30 sessiontype: type[bs.Session], 31 transition: str | None = 'in_right', 32 origin_widget: bui.Widget | None = None, 33 select_playlist: str | None = None, 34 ): 35 # Yes this needs tidying. 36 # pylint: disable=too-many-locals 37 # pylint: disable=too-many-statements 38 # pylint: disable=cyclic-import 39 from bauiv1lib import playlist 40 41 self._sessiontype = sessiontype 42 self._pvars = playlist.PlaylistTypeVars(sessiontype) 43 self._max_playlists = 30 44 self._r = 'gameListWindow' 45 assert bui.app.classic is not None 46 uiscale = bui.app.ui_v1.uiscale 47 self._width = 970.0 if uiscale is bui.UIScale.SMALL else 650.0 48 x_inset = 100.0 if uiscale is bui.UIScale.SMALL else 0.0 49 self._height = ( 50 380.0 51 if uiscale is bui.UIScale.SMALL 52 else 420.0 if uiscale is bui.UIScale.MEDIUM else 500.0 53 ) 54 top_extra = 20.0 if uiscale is bui.UIScale.SMALL else 0.0 55 56 super().__init__( 57 root_widget=bui.containerwidget( 58 size=(self._width, self._height + top_extra), 59 scale=( 60 1.83 61 if uiscale is bui.UIScale.SMALL 62 else 1.5 if uiscale is bui.UIScale.MEDIUM else 1.0 63 ), 64 toolbar_visibility=( 65 'menu_minimal' 66 if uiscale is bui.UIScale.SMALL 67 else 'menu_full' 68 ), 69 stack_offset=( 70 (0, -10) if uiscale is bui.UIScale.SMALL else (0, 0) 71 ), 72 ), 73 transition=transition, 74 origin_widget=origin_widget, 75 ) 76 77 self._back_button: bui.Widget | None 78 if uiscale is bui.UIScale.SMALL: 79 self._back_button = None 80 bui.containerwidget( 81 edit=self._root_widget, on_cancel_call=self._back 82 ) 83 else: 84 self._back_button = bui.buttonwidget( 85 parent=self._root_widget, 86 position=(43 + x_inset, self._height - 60), 87 size=(160, 68), 88 scale=0.77, 89 autoselect=True, 90 text_scale=1.3, 91 label=bui.Lstr(resource='backText'), 92 button_type='back', 93 ) 94 bui.buttonwidget( 95 edit=self._back_button, 96 button_type='backSmall', 97 size=(60, 60), 98 label=bui.charstr(bui.SpecialChar.BACK), 99 ) 100 101 bui.textwidget( 102 parent=self._root_widget, 103 position=(0, self._height - 47), 104 size=(self._width, 25), 105 text=bui.Lstr( 106 resource=f'{self._r}.titleText', 107 subs=[('${TYPE}', self._pvars.window_title_name)], 108 ), 109 color=bui.app.ui_v1.heading_color, 110 maxwidth=290, 111 h_align='center', 112 v_align='center', 113 ) 114 115 v = self._height - 59.0 116 h = 41 + x_inset 117 b_color = (0.6, 0.53, 0.63) 118 b_textcolor = (0.75, 0.7, 0.8) 119 self._lock_images: list[bui.Widget] = [] 120 lock_tex = bui.gettexture('lock') 121 122 scl = ( 123 1.1 124 if uiscale is bui.UIScale.SMALL 125 else 1.27 if uiscale is bui.UIScale.MEDIUM else 1.57 126 ) 127 scl *= 0.63 128 v -= 65.0 * scl 129 new_button = btn = bui.buttonwidget( 130 parent=self._root_widget, 131 position=(h, v), 132 size=(90, 58.0 * scl), 133 on_activate_call=self._new_playlist, 134 color=b_color, 135 autoselect=True, 136 button_type='square', 137 textcolor=b_textcolor, 138 text_scale=0.7, 139 label=bui.Lstr( 140 resource='newText', fallback_resource=f'{self._r}.newText' 141 ), 142 ) 143 self._lock_images.append( 144 bui.imagewidget( 145 parent=self._root_widget, 146 size=(30, 30), 147 draw_controller=btn, 148 position=(h - 10, v + 58.0 * scl - 28), 149 texture=lock_tex, 150 ) 151 ) 152 153 v -= 65.0 * scl 154 self._edit_button = edit_button = btn = bui.buttonwidget( 155 parent=self._root_widget, 156 position=(h, v), 157 size=(90, 58.0 * scl), 158 on_activate_call=self._edit_playlist, 159 color=b_color, 160 autoselect=True, 161 textcolor=b_textcolor, 162 button_type='square', 163 text_scale=0.7, 164 label=bui.Lstr( 165 resource='editText', fallback_resource=f'{self._r}.editText' 166 ), 167 ) 168 self._lock_images.append( 169 bui.imagewidget( 170 parent=self._root_widget, 171 size=(30, 30), 172 draw_controller=btn, 173 position=(h - 10, v + 58.0 * scl - 28), 174 texture=lock_tex, 175 ) 176 ) 177 178 v -= 65.0 * scl 179 duplicate_button = btn = bui.buttonwidget( 180 parent=self._root_widget, 181 position=(h, v), 182 size=(90, 58.0 * scl), 183 on_activate_call=self._duplicate_playlist, 184 color=b_color, 185 autoselect=True, 186 textcolor=b_textcolor, 187 button_type='square', 188 text_scale=0.7, 189 label=bui.Lstr( 190 resource='duplicateText', 191 fallback_resource=f'{self._r}.duplicateText', 192 ), 193 ) 194 self._lock_images.append( 195 bui.imagewidget( 196 parent=self._root_widget, 197 size=(30, 30), 198 draw_controller=btn, 199 position=(h - 10, v + 58.0 * scl - 28), 200 texture=lock_tex, 201 ) 202 ) 203 204 v -= 65.0 * scl 205 delete_button = btn = bui.buttonwidget( 206 parent=self._root_widget, 207 position=(h, v), 208 size=(90, 58.0 * scl), 209 on_activate_call=self._delete_playlist, 210 color=b_color, 211 autoselect=True, 212 textcolor=b_textcolor, 213 button_type='square', 214 text_scale=0.7, 215 label=bui.Lstr( 216 resource='deleteText', fallback_resource=f'{self._r}.deleteText' 217 ), 218 ) 219 self._lock_images.append( 220 bui.imagewidget( 221 parent=self._root_widget, 222 size=(30, 30), 223 draw_controller=btn, 224 position=(h - 10, v + 58.0 * scl - 28), 225 texture=lock_tex, 226 ) 227 ) 228 v -= 65.0 * scl 229 self._import_button = bui.buttonwidget( 230 parent=self._root_widget, 231 position=(h, v), 232 size=(90, 58.0 * scl), 233 on_activate_call=self._import_playlist, 234 color=b_color, 235 autoselect=True, 236 textcolor=b_textcolor, 237 button_type='square', 238 text_scale=0.7, 239 label=bui.Lstr(resource='importText'), 240 ) 241 v -= 65.0 * scl 242 btn = bui.buttonwidget( 243 parent=self._root_widget, 244 position=(h, v), 245 size=(90, 58.0 * scl), 246 on_activate_call=self._share_playlist, 247 color=b_color, 248 autoselect=True, 249 textcolor=b_textcolor, 250 button_type='square', 251 text_scale=0.7, 252 label=bui.Lstr(resource='shareText'), 253 ) 254 self._lock_images.append( 255 bui.imagewidget( 256 parent=self._root_widget, 257 size=(30, 30), 258 draw_controller=btn, 259 position=(h - 10, v + 58.0 * scl - 28), 260 texture=lock_tex, 261 ) 262 ) 263 264 v = self._height - 75 265 self._scroll_height = self._height - 119 266 scrollwidget = bui.scrollwidget( 267 parent=self._root_widget, 268 position=(140 + x_inset, v - self._scroll_height), 269 size=(self._width - (180 + 2 * x_inset), self._scroll_height + 10), 270 highlight=False, 271 ) 272 if self._back_button is not None: 273 bui.widget(edit=self._back_button, right_widget=scrollwidget) 274 275 self._columnwidget = bui.columnwidget( 276 parent=scrollwidget, border=2, margin=0 277 ) 278 279 h = 145 280 281 self._do_randomize_val = bui.app.config.get( 282 self._pvars.config_name + ' Playlist Randomize', 0 283 ) 284 285 h += 210 286 287 for btn in [new_button, delete_button, edit_button, duplicate_button]: 288 bui.widget(edit=btn, right_widget=scrollwidget) 289 bui.widget( 290 edit=scrollwidget, 291 left_widget=new_button, 292 right_widget=bui.get_special_widget('squad_button'), 293 ) 294 295 # make sure config exists 296 self._config_name_full = self._pvars.config_name + ' Playlists' 297 298 if self._config_name_full not in bui.app.config: 299 bui.app.config[self._config_name_full] = {} 300 301 self._selected_playlist_name: str | None = None 302 self._selected_playlist_index: int | None = None 303 self._playlist_widgets: list[bui.Widget] = [] 304 305 self._refresh(select_playlist=select_playlist) 306 307 if self._back_button is not None: 308 bui.buttonwidget( 309 edit=self._back_button, on_activate_call=self._back 310 ) 311 bui.containerwidget( 312 edit=self._root_widget, cancel_button=self._back_button 313 ) 314 315 bui.containerwidget(edit=self._root_widget, selected_child=scrollwidget) 316 317 # Keep our lock images up to date/etc. 318 self._update_timer = bui.AppTimer( 319 1.0, bui.WeakCall(self._update), repeat=True 320 ) 321 self._update() 322 323 @override 324 def get_main_window_state(self) -> bui.MainWindowState: 325 # Support recreating our window for back/refresh purposes. 326 cls = type(self) 327 328 # Avoid dereferencing self within the lambda or we'll keep 329 # ourself alive indefinitely. 330 stype = self._sessiontype 331 332 return bui.BasicMainWindowState( 333 create_call=lambda transition, origin_widget: cls( 334 transition=transition, 335 origin_widget=origin_widget, 336 sessiontype=stype, 337 ) 338 ) 339 340 # @override 341 # def on_main_window_close(self) -> None: 342 # self._save_state() 343 344 def _update(self) -> None: 345 assert bui.app.classic is not None 346 have = bui.app.classic.accounts.have_pro_options() 347 for lock in self._lock_images: 348 bui.imagewidget( 349 edit=lock, opacity=0.0 if (have or not REQUIRE_PRO) else 1.0 350 ) 351 352 def _back(self) -> None: 353 # pylint: disable=cyclic-import 354 # from bauiv1lib.playlist import browser 355 356 # no-op if our underlying widget is dead or on its way out. 357 if not self._root_widget or self._root_widget.transitioning_out: 358 return 359 360 if self._selected_playlist_name is not None: 361 cfg = bui.app.config 362 cfg[self._pvars.config_name + ' Playlist Selection'] = ( 363 self._selected_playlist_name 364 ) 365 cfg.commit() 366 367 self.main_window_back() 368 # bui.containerwidget( 369 # edit=self._root_widget, transition=self._transition_out 370 # ) 371 # assert bui.app.classic is not None 372 # bui.app.ui_v1.set_main_window( 373 # browser.PlaylistBrowserWindow( 374 # transition='in_left', sessiontype=self._sessiontype 375 # ), 376 # from_window=self, 377 # is_back=True, 378 # ) 379 380 def _select(self, name: str, index: int) -> None: 381 self._selected_playlist_name = name 382 self._selected_playlist_index = index 383 384 def _run_selected_playlist(self) -> None: 385 # pylint: disable=cyclic-import 386 bui.unlock_all_input() 387 try: 388 bs.new_host_session(self._sessiontype) 389 except Exception: 390 from bascenev1lib import mainmenu 391 392 logging.exception('Error running session %s.', self._sessiontype) 393 394 # Drop back into a main menu session. 395 bs.new_host_session(mainmenu.MainMenuSession) 396 397 def _choose_playlist(self) -> None: 398 if self._selected_playlist_name is None: 399 return 400 self._save_playlist_selection() 401 bui.containerwidget(edit=self._root_widget, transition='out_left') 402 bui.fade_screen(False, endcall=self._run_selected_playlist) 403 bui.lock_all_input() 404 405 def _refresh(self, select_playlist: str | None = None) -> None: 406 from efro.util import asserttype 407 408 old_selection = self._selected_playlist_name 409 410 # If there was no prev selection, look in prefs. 411 if old_selection is None: 412 old_selection = bui.app.config.get( 413 self._pvars.config_name + ' Playlist Selection' 414 ) 415 416 old_selection_index = self._selected_playlist_index 417 418 # Delete old. 419 while self._playlist_widgets: 420 self._playlist_widgets.pop().delete() 421 422 items = list(bui.app.config[self._config_name_full].items()) 423 424 # Make sure everything is unicode now. 425 items = [ 426 (i[0].decode(), i[1]) if not isinstance(i[0], str) else i 427 for i in items 428 ] 429 430 items.sort(key=lambda x: asserttype(x[0], str).lower()) 431 432 items = [['__default__', None]] + items # Default is always first. 433 index = 0 434 for pname, _ in items: 435 assert pname is not None 436 txtw = bui.textwidget( 437 parent=self._columnwidget, 438 size=(self._width - 40, 30), 439 maxwidth=440, 440 text=self._get_playlist_display_name(pname), 441 h_align='left', 442 v_align='center', 443 color=( 444 (0.6, 0.6, 0.7, 1.0) 445 if pname == '__default__' 446 else (0.85, 0.85, 0.85, 1) 447 ), 448 always_highlight=True, 449 on_select_call=bui.Call(self._select, pname, index), 450 on_activate_call=bui.Call(self._edit_button.activate), 451 selectable=True, 452 ) 453 bui.widget(edit=txtw, show_buffer_top=50, show_buffer_bottom=50) 454 455 # Hitting up from top widget should jump to 'back' 456 if index == 0: 457 bui.widget( 458 edit=txtw, 459 up_widget=( 460 self._back_button 461 if self._back_button is not None 462 else bui.get_special_widget('back_button') 463 ), 464 ) 465 466 self._playlist_widgets.append(txtw) 467 468 # Select this one if the user requested it. 469 if select_playlist is not None: 470 if pname == select_playlist: 471 bui.columnwidget( 472 edit=self._columnwidget, 473 selected_child=txtw, 474 visible_child=txtw, 475 ) 476 else: 477 # Select this one if it was previously selected. 478 # Go by index if there's one. 479 if old_selection_index is not None: 480 if index == old_selection_index: 481 bui.columnwidget( 482 edit=self._columnwidget, 483 selected_child=txtw, 484 visible_child=txtw, 485 ) 486 else: # Otherwise look by name. 487 if pname == old_selection: 488 bui.columnwidget( 489 edit=self._columnwidget, 490 selected_child=txtw, 491 visible_child=txtw, 492 ) 493 494 index += 1 495 496 def _save_playlist_selection(self) -> None: 497 # Store the selected playlist in prefs. 498 # This serves dual purposes of letting us re-select it next time 499 # if we want and also lets us pass it to the game (since we reset 500 # the whole python environment that's not actually easy). 501 cfg = bui.app.config 502 cfg[self._pvars.config_name + ' Playlist Selection'] = ( 503 self._selected_playlist_name 504 ) 505 cfg[self._pvars.config_name + ' Playlist Randomize'] = ( 506 self._do_randomize_val 507 ) 508 cfg.commit() 509 510 def _new_playlist(self) -> None: 511 # pylint: disable=cyclic-import 512 from bauiv1lib.playlist.editcontroller import PlaylistEditController 513 from bauiv1lib.purchase import PurchaseWindow 514 515 if UNDER_CONSTRUCTION: 516 bui.screenmessage('UNDER CONSTRUCTION') 517 return 518 519 assert bui.app.classic is not None 520 if REQUIRE_PRO and not bui.app.classic.accounts.have_pro_options(): 521 PurchaseWindow(items=['pro']) 522 return 523 524 # Clamp at our max playlist number. 525 if len(bui.app.config[self._config_name_full]) > self._max_playlists: 526 bui.screenmessage( 527 bui.Lstr( 528 translate=( 529 'serverResponses', 530 'Max number of playlists reached.', 531 ) 532 ), 533 color=(1, 0, 0), 534 ) 535 bui.getsound('error').play() 536 return 537 538 # In case they cancel so we can return to this state. 539 self._save_playlist_selection() 540 541 # Kick off the edit UI. 542 PlaylistEditController(sessiontype=self._sessiontype) 543 bui.containerwidget(edit=self._root_widget, transition='out_left') 544 545 def _edit_playlist(self) -> None: 546 # pylint: disable=cyclic-import 547 from bauiv1lib.playlist.editcontroller import PlaylistEditController 548 from bauiv1lib.purchase import PurchaseWindow 549 550 if UNDER_CONSTRUCTION: 551 bui.screenmessage('UNDER CONSTRUCTION') 552 return 553 554 assert bui.app.classic is not None 555 if REQUIRE_PRO and not bui.app.classic.accounts.have_pro_options(): 556 PurchaseWindow(items=['pro']) 557 return 558 if self._selected_playlist_name is None: 559 return 560 if self._selected_playlist_name == '__default__': 561 bui.getsound('error').play() 562 bui.screenmessage( 563 bui.Lstr(resource=f'{self._r}.cantEditDefaultText') 564 ) 565 return 566 self._save_playlist_selection() 567 PlaylistEditController( 568 existing_playlist_name=self._selected_playlist_name, 569 sessiontype=self._sessiontype, 570 ) 571 bui.containerwidget(edit=self._root_widget, transition='out_left') 572 573 def _do_delete_playlist(self) -> None: 574 plus = bui.app.plus 575 assert plus is not None 576 plus.add_v1_account_transaction( 577 { 578 'type': 'REMOVE_PLAYLIST', 579 'playlistType': self._pvars.config_name, 580 'playlistName': self._selected_playlist_name, 581 } 582 ) 583 plus.run_v1_account_transactions() 584 bui.getsound('shieldDown').play() 585 586 # (we don't use len()-1 here because the default list adds one) 587 assert self._selected_playlist_index is not None 588 self._selected_playlist_index = min( 589 self._selected_playlist_index, 590 len(bui.app.config[self._pvars.config_name + ' Playlists']), 591 ) 592 self._refresh() 593 594 def _import_playlist(self) -> None: 595 # pylint: disable=cyclic-import 596 from bauiv1lib.playlist import share 597 598 plus = bui.app.plus 599 assert plus is not None 600 601 # Gotta be signed in for this to work. 602 if plus.get_v1_account_state() != 'signed_in': 603 bui.screenmessage( 604 bui.Lstr(resource='notSignedInErrorText'), color=(1, 0, 0) 605 ) 606 bui.getsound('error').play() 607 return 608 609 share.SharePlaylistImportWindow( 610 origin_widget=self._import_button, 611 on_success_callback=bui.WeakCall(self._on_playlist_import_success), 612 ) 613 614 def _on_playlist_import_success(self) -> None: 615 self._refresh() 616 617 def _on_share_playlist_response(self, name: str, response: Any) -> None: 618 # pylint: disable=cyclic-import 619 from bauiv1lib.playlist import share 620 621 if response is None: 622 bui.screenmessage( 623 bui.Lstr(resource='internal.unavailableNoConnectionText'), 624 color=(1, 0, 0), 625 ) 626 bui.getsound('error').play() 627 return 628 share.SharePlaylistResultsWindow(name, response) 629 630 def _share_playlist(self) -> None: 631 # pylint: disable=cyclic-import 632 from bauiv1lib.purchase import PurchaseWindow 633 634 plus = bui.app.plus 635 assert plus is not None 636 637 assert bui.app.classic is not None 638 if REQUIRE_PRO and not bui.app.classic.accounts.have_pro_options(): 639 PurchaseWindow(items=['pro']) 640 return 641 642 # Gotta be signed in for this to work. 643 if plus.get_v1_account_state() != 'signed_in': 644 bui.screenmessage( 645 bui.Lstr(resource='notSignedInErrorText'), color=(1, 0, 0) 646 ) 647 bui.getsound('error').play() 648 return 649 if self._selected_playlist_name == '__default__': 650 bui.getsound('error').play() 651 bui.screenmessage( 652 bui.Lstr(resource=f'{self._r}.cantShareDefaultText'), 653 color=(1, 0, 0), 654 ) 655 return 656 657 if self._selected_playlist_name is None: 658 return 659 660 plus.add_v1_account_transaction( 661 { 662 'type': 'SHARE_PLAYLIST', 663 'expire_time': time.time() + 5, 664 'playlistType': self._pvars.config_name, 665 'playlistName': self._selected_playlist_name, 666 }, 667 callback=bui.WeakCall( 668 self._on_share_playlist_response, self._selected_playlist_name 669 ), 670 ) 671 plus.run_v1_account_transactions() 672 bui.screenmessage(bui.Lstr(resource='sharingText')) 673 674 def _delete_playlist(self) -> None: 675 # pylint: disable=cyclic-import 676 from bauiv1lib.purchase import PurchaseWindow 677 from bauiv1lib.confirm import ConfirmWindow 678 679 assert bui.app.classic is not None 680 if REQUIRE_PRO and not bui.app.classic.accounts.have_pro_options(): 681 PurchaseWindow(items=['pro']) 682 return 683 684 if self._selected_playlist_name is None: 685 return 686 if self._selected_playlist_name == '__default__': 687 bui.getsound('error').play() 688 bui.screenmessage( 689 bui.Lstr(resource=f'{self._r}.cantDeleteDefaultText') 690 ) 691 else: 692 ConfirmWindow( 693 bui.Lstr( 694 resource=f'{self._r}.deleteConfirmText', 695 subs=[('${LIST}', self._selected_playlist_name)], 696 ), 697 self._do_delete_playlist, 698 450, 699 150, 700 ) 701 702 def _get_playlist_display_name(self, playlist: str) -> bui.Lstr: 703 if playlist == '__default__': 704 return self._pvars.default_list_name 705 return ( 706 playlist 707 if isinstance(playlist, bui.Lstr) 708 else bui.Lstr(value=playlist) 709 ) 710 711 def _duplicate_playlist(self) -> None: 712 # pylint: disable=too-many-branches 713 # pylint: disable=cyclic-import 714 from bauiv1lib.purchase import PurchaseWindow 715 716 plus = bui.app.plus 717 assert plus is not None 718 719 assert bui.app.classic is not None 720 if REQUIRE_PRO and not bui.app.classic.accounts.have_pro_options(): 721 PurchaseWindow(items=['pro']) 722 return 723 if self._selected_playlist_name is None: 724 return 725 plst: list[dict[str, Any]] | None 726 if self._selected_playlist_name == '__default__': 727 plst = self._pvars.get_default_list_call() 728 else: 729 plst = bui.app.config[self._config_name_full].get( 730 self._selected_playlist_name 731 ) 732 if plst is None: 733 bui.getsound('error').play() 734 return 735 736 # clamp at our max playlist number 737 if len(bui.app.config[self._config_name_full]) > self._max_playlists: 738 bui.screenmessage( 739 bui.Lstr( 740 translate=( 741 'serverResponses', 742 'Max number of playlists reached.', 743 ) 744 ), 745 color=(1, 0, 0), 746 ) 747 bui.getsound('error').play() 748 return 749 750 copy_text = bui.Lstr(resource='copyOfText').evaluate() 751 # get just 'Copy' or whatnot 752 copy_word = copy_text.replace('${NAME}', '').strip() 753 # find a valid dup name that doesn't exist 754 755 test_index = 1 756 base_name = self._get_playlist_display_name( 757 self._selected_playlist_name 758 ).evaluate() 759 760 # If it looks like a copy, strip digits and spaces off the end. 761 if copy_word in base_name: 762 while base_name[-1].isdigit() or base_name[-1] == ' ': 763 base_name = base_name[:-1] 764 while True: 765 if copy_word in base_name: 766 test_name = base_name 767 else: 768 test_name = copy_text.replace('${NAME}', base_name) 769 if test_index > 1: 770 test_name += ' ' + str(test_index) 771 if test_name not in bui.app.config[self._config_name_full]: 772 break 773 test_index += 1 774 775 plus.add_v1_account_transaction( 776 { 777 'type': 'ADD_PLAYLIST', 778 'playlistType': self._pvars.config_name, 779 'playlistName': test_name, 780 'playlist': copy.deepcopy(plst), 781 } 782 ) 783 plus.run_v1_account_transactions() 784 785 bui.getsound('gunCocking').play() 786 self._refresh(select_playlist=test_name)
Window for viewing a playlist.
PlaylistCustomizeBrowserWindow( sessiontype: type[bascenev1.Session], transition: str | None = 'in_right', origin_widget: _bauiv1.Widget | None = None, select_playlist: str | None = None)
28 def __init__( 29 self, 30 sessiontype: type[bs.Session], 31 transition: str | None = 'in_right', 32 origin_widget: bui.Widget | None = None, 33 select_playlist: str | None = None, 34 ): 35 # Yes this needs tidying. 36 # pylint: disable=too-many-locals 37 # pylint: disable=too-many-statements 38 # pylint: disable=cyclic-import 39 from bauiv1lib import playlist 40 41 self._sessiontype = sessiontype 42 self._pvars = playlist.PlaylistTypeVars(sessiontype) 43 self._max_playlists = 30 44 self._r = 'gameListWindow' 45 assert bui.app.classic is not None 46 uiscale = bui.app.ui_v1.uiscale 47 self._width = 970.0 if uiscale is bui.UIScale.SMALL else 650.0 48 x_inset = 100.0 if uiscale is bui.UIScale.SMALL else 0.0 49 self._height = ( 50 380.0 51 if uiscale is bui.UIScale.SMALL 52 else 420.0 if uiscale is bui.UIScale.MEDIUM else 500.0 53 ) 54 top_extra = 20.0 if uiscale is bui.UIScale.SMALL else 0.0 55 56 super().__init__( 57 root_widget=bui.containerwidget( 58 size=(self._width, self._height + top_extra), 59 scale=( 60 1.83 61 if uiscale is bui.UIScale.SMALL 62 else 1.5 if uiscale is bui.UIScale.MEDIUM else 1.0 63 ), 64 toolbar_visibility=( 65 'menu_minimal' 66 if uiscale is bui.UIScale.SMALL 67 else 'menu_full' 68 ), 69 stack_offset=( 70 (0, -10) if uiscale is bui.UIScale.SMALL else (0, 0) 71 ), 72 ), 73 transition=transition, 74 origin_widget=origin_widget, 75 ) 76 77 self._back_button: bui.Widget | None 78 if uiscale is bui.UIScale.SMALL: 79 self._back_button = None 80 bui.containerwidget( 81 edit=self._root_widget, on_cancel_call=self._back 82 ) 83 else: 84 self._back_button = bui.buttonwidget( 85 parent=self._root_widget, 86 position=(43 + x_inset, self._height - 60), 87 size=(160, 68), 88 scale=0.77, 89 autoselect=True, 90 text_scale=1.3, 91 label=bui.Lstr(resource='backText'), 92 button_type='back', 93 ) 94 bui.buttonwidget( 95 edit=self._back_button, 96 button_type='backSmall', 97 size=(60, 60), 98 label=bui.charstr(bui.SpecialChar.BACK), 99 ) 100 101 bui.textwidget( 102 parent=self._root_widget, 103 position=(0, self._height - 47), 104 size=(self._width, 25), 105 text=bui.Lstr( 106 resource=f'{self._r}.titleText', 107 subs=[('${TYPE}', self._pvars.window_title_name)], 108 ), 109 color=bui.app.ui_v1.heading_color, 110 maxwidth=290, 111 h_align='center', 112 v_align='center', 113 ) 114 115 v = self._height - 59.0 116 h = 41 + x_inset 117 b_color = (0.6, 0.53, 0.63) 118 b_textcolor = (0.75, 0.7, 0.8) 119 self._lock_images: list[bui.Widget] = [] 120 lock_tex = bui.gettexture('lock') 121 122 scl = ( 123 1.1 124 if uiscale is bui.UIScale.SMALL 125 else 1.27 if uiscale is bui.UIScale.MEDIUM else 1.57 126 ) 127 scl *= 0.63 128 v -= 65.0 * scl 129 new_button = btn = bui.buttonwidget( 130 parent=self._root_widget, 131 position=(h, v), 132 size=(90, 58.0 * scl), 133 on_activate_call=self._new_playlist, 134 color=b_color, 135 autoselect=True, 136 button_type='square', 137 textcolor=b_textcolor, 138 text_scale=0.7, 139 label=bui.Lstr( 140 resource='newText', fallback_resource=f'{self._r}.newText' 141 ), 142 ) 143 self._lock_images.append( 144 bui.imagewidget( 145 parent=self._root_widget, 146 size=(30, 30), 147 draw_controller=btn, 148 position=(h - 10, v + 58.0 * scl - 28), 149 texture=lock_tex, 150 ) 151 ) 152 153 v -= 65.0 * scl 154 self._edit_button = edit_button = btn = bui.buttonwidget( 155 parent=self._root_widget, 156 position=(h, v), 157 size=(90, 58.0 * scl), 158 on_activate_call=self._edit_playlist, 159 color=b_color, 160 autoselect=True, 161 textcolor=b_textcolor, 162 button_type='square', 163 text_scale=0.7, 164 label=bui.Lstr( 165 resource='editText', fallback_resource=f'{self._r}.editText' 166 ), 167 ) 168 self._lock_images.append( 169 bui.imagewidget( 170 parent=self._root_widget, 171 size=(30, 30), 172 draw_controller=btn, 173 position=(h - 10, v + 58.0 * scl - 28), 174 texture=lock_tex, 175 ) 176 ) 177 178 v -= 65.0 * scl 179 duplicate_button = btn = bui.buttonwidget( 180 parent=self._root_widget, 181 position=(h, v), 182 size=(90, 58.0 * scl), 183 on_activate_call=self._duplicate_playlist, 184 color=b_color, 185 autoselect=True, 186 textcolor=b_textcolor, 187 button_type='square', 188 text_scale=0.7, 189 label=bui.Lstr( 190 resource='duplicateText', 191 fallback_resource=f'{self._r}.duplicateText', 192 ), 193 ) 194 self._lock_images.append( 195 bui.imagewidget( 196 parent=self._root_widget, 197 size=(30, 30), 198 draw_controller=btn, 199 position=(h - 10, v + 58.0 * scl - 28), 200 texture=lock_tex, 201 ) 202 ) 203 204 v -= 65.0 * scl 205 delete_button = btn = bui.buttonwidget( 206 parent=self._root_widget, 207 position=(h, v), 208 size=(90, 58.0 * scl), 209 on_activate_call=self._delete_playlist, 210 color=b_color, 211 autoselect=True, 212 textcolor=b_textcolor, 213 button_type='square', 214 text_scale=0.7, 215 label=bui.Lstr( 216 resource='deleteText', fallback_resource=f'{self._r}.deleteText' 217 ), 218 ) 219 self._lock_images.append( 220 bui.imagewidget( 221 parent=self._root_widget, 222 size=(30, 30), 223 draw_controller=btn, 224 position=(h - 10, v + 58.0 * scl - 28), 225 texture=lock_tex, 226 ) 227 ) 228 v -= 65.0 * scl 229 self._import_button = bui.buttonwidget( 230 parent=self._root_widget, 231 position=(h, v), 232 size=(90, 58.0 * scl), 233 on_activate_call=self._import_playlist, 234 color=b_color, 235 autoselect=True, 236 textcolor=b_textcolor, 237 button_type='square', 238 text_scale=0.7, 239 label=bui.Lstr(resource='importText'), 240 ) 241 v -= 65.0 * scl 242 btn = bui.buttonwidget( 243 parent=self._root_widget, 244 position=(h, v), 245 size=(90, 58.0 * scl), 246 on_activate_call=self._share_playlist, 247 color=b_color, 248 autoselect=True, 249 textcolor=b_textcolor, 250 button_type='square', 251 text_scale=0.7, 252 label=bui.Lstr(resource='shareText'), 253 ) 254 self._lock_images.append( 255 bui.imagewidget( 256 parent=self._root_widget, 257 size=(30, 30), 258 draw_controller=btn, 259 position=(h - 10, v + 58.0 * scl - 28), 260 texture=lock_tex, 261 ) 262 ) 263 264 v = self._height - 75 265 self._scroll_height = self._height - 119 266 scrollwidget = bui.scrollwidget( 267 parent=self._root_widget, 268 position=(140 + x_inset, v - self._scroll_height), 269 size=(self._width - (180 + 2 * x_inset), self._scroll_height + 10), 270 highlight=False, 271 ) 272 if self._back_button is not None: 273 bui.widget(edit=self._back_button, right_widget=scrollwidget) 274 275 self._columnwidget = bui.columnwidget( 276 parent=scrollwidget, border=2, margin=0 277 ) 278 279 h = 145 280 281 self._do_randomize_val = bui.app.config.get( 282 self._pvars.config_name + ' Playlist Randomize', 0 283 ) 284 285 h += 210 286 287 for btn in [new_button, delete_button, edit_button, duplicate_button]: 288 bui.widget(edit=btn, right_widget=scrollwidget) 289 bui.widget( 290 edit=scrollwidget, 291 left_widget=new_button, 292 right_widget=bui.get_special_widget('squad_button'), 293 ) 294 295 # make sure config exists 296 self._config_name_full = self._pvars.config_name + ' Playlists' 297 298 if self._config_name_full not in bui.app.config: 299 bui.app.config[self._config_name_full] = {} 300 301 self._selected_playlist_name: str | None = None 302 self._selected_playlist_index: int | None = None 303 self._playlist_widgets: list[bui.Widget] = [] 304 305 self._refresh(select_playlist=select_playlist) 306 307 if self._back_button is not None: 308 bui.buttonwidget( 309 edit=self._back_button, on_activate_call=self._back 310 ) 311 bui.containerwidget( 312 edit=self._root_widget, cancel_button=self._back_button 313 ) 314 315 bui.containerwidget(edit=self._root_widget, selected_child=scrollwidget) 316 317 # Keep our lock images up to date/etc. 318 self._update_timer = bui.AppTimer( 319 1.0, bui.WeakCall(self._update), repeat=True 320 ) 321 self._update()
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.
323 @override 324 def get_main_window_state(self) -> bui.MainWindowState: 325 # Support recreating our window for back/refresh purposes. 326 cls = type(self) 327 328 # Avoid dereferencing self within the lambda or we'll keep 329 # ourself alive indefinitely. 330 stype = self._sessiontype 331 332 return bui.BasicMainWindowState( 333 create_call=lambda transition, origin_widget: cls( 334 transition=transition, 335 origin_widget=origin_widget, 336 sessiontype=stype, 337 ) 338 )
Return a WindowState to recreate this window, if supported.
Inherited Members
- bauiv1._uitypes.MainWindow
- main_window_back_state
- main_window_close
- can_change_main_window
- main_window_back
- main_window_replace
- on_main_window_close
- bauiv1._uitypes.Window
- get_root_widget