Source code for bauiv1lib.playlist.customizebrowser

# Released under the MIT License. See LICENSE for details.
#
"""Provides UI for viewing/creating/editing playlists."""

from __future__ import annotations

import copy
import time

# import logging
from typing import TYPE_CHECKING, override

# import bascenev1 as bs
import bauiv1 as bui

if TYPE_CHECKING:
    from typing import Any

    import bascenev1 as bs

REQUIRE_PRO = False


[docs] class PlaylistCustomizeBrowserWindow(bui.MainWindow): """Window for viewing a playlist.""" def __init__( self, sessiontype: type[bs.Session], transition: str | None = 'in_right', origin_widget: bui.Widget | None = None, select_playlist: str | None = None, ): # Yes this needs tidying. # pylint: disable=too-many-locals # pylint: disable=too-many-statements # pylint: disable=cyclic-import from bauiv1lib import playlist self._sessiontype = sessiontype self._pvars = playlist.PlaylistTypeVars(sessiontype) self._max_playlists = 30 self._r = 'gameListWindow' assert bui.app.classic is not None uiscale = bui.app.ui_v1.uiscale self._width = 970.0 if uiscale is bui.UIScale.SMALL else 650.0 x_inset = 100.0 if uiscale is bui.UIScale.SMALL else 0.0 yoffs = -51 if uiscale is bui.UIScale.SMALL else 0.0 self._height = ( 440.0 if uiscale is bui.UIScale.SMALL else 420.0 if uiscale is bui.UIScale.MEDIUM else 500.0 ) super().__init__( root_widget=bui.containerwidget( size=(self._width, self._height), scale=( 1.8 if uiscale is bui.UIScale.SMALL else 1.4 if uiscale is bui.UIScale.MEDIUM else 1.0 ), toolbar_visibility=( 'menu_minimal' if uiscale is bui.UIScale.SMALL else 'menu_full' ), stack_offset=( (0, 0) if uiscale is bui.UIScale.SMALL else (0, 0) ), ), transition=transition, origin_widget=origin_widget, ) self._back_button: bui.Widget | None if uiscale is bui.UIScale.SMALL: self._back_button = None bui.containerwidget( edit=self._root_widget, on_cancel_call=self.main_window_back ) else: self._back_button = bui.buttonwidget( parent=self._root_widget, position=(43 + x_inset, self._height - 60 + yoffs), size=(160, 68), scale=0.77, autoselect=True, text_scale=1.3, label=bui.Lstr(resource='backText'), button_type='back', ) bui.buttonwidget( edit=self._back_button, button_type='backSmall', size=(60, 60), label=bui.charstr(bui.SpecialChar.BACK), ) bui.textwidget( parent=self._root_widget, position=( 0, self._height - (47 if uiscale is bui.UIScale.SMALL else 47) + yoffs, ), size=(self._width, 25), text=bui.Lstr( resource=f'{self._r}.titleText', subs=[('${TYPE}', self._pvars.window_title_name)], ), color=bui.app.ui_v1.heading_color, maxwidth=290, h_align='center', v_align='center', ) v = self._height - 59.0 + yoffs h = 41 + x_inset b_color = (0.6, 0.53, 0.63) b_textcolor = (0.75, 0.7, 0.8) self._lock_images: list[bui.Widget] = [] lock_tex = bui.gettexture('lock') scl = ( 1.1 if uiscale is bui.UIScale.SMALL else 1.27 if uiscale is bui.UIScale.MEDIUM else 1.57 ) scl *= 0.63 v -= 65.0 * scl new_button = btn = bui.buttonwidget( parent=self._root_widget, position=(h, v), size=(90, 58.0 * scl), on_activate_call=self._new_playlist, color=b_color, autoselect=True, button_type='square', textcolor=b_textcolor, text_scale=0.7, label=bui.Lstr( resource='newText', fallback_resource=f'{self._r}.newText' ), ) self._lock_images.append( bui.imagewidget( parent=self._root_widget, size=(30, 30), draw_controller=btn, position=(h - 10, v + 58.0 * scl - 28), texture=lock_tex, ) ) v -= 65.0 * scl self._edit_button = edit_button = btn = bui.buttonwidget( parent=self._root_widget, position=(h, v), size=(90, 58.0 * scl), on_activate_call=self._edit_playlist, color=b_color, autoselect=True, textcolor=b_textcolor, button_type='square', text_scale=0.7, label=bui.Lstr( resource='editText', fallback_resource=f'{self._r}.editText' ), ) self._lock_images.append( bui.imagewidget( parent=self._root_widget, size=(30, 30), draw_controller=btn, position=(h - 10, v + 58.0 * scl - 28), texture=lock_tex, ) ) v -= 65.0 * scl duplicate_button = btn = bui.buttonwidget( parent=self._root_widget, position=(h, v), size=(90, 58.0 * scl), on_activate_call=self._duplicate_playlist, color=b_color, autoselect=True, textcolor=b_textcolor, button_type='square', text_scale=0.7, label=bui.Lstr( resource='duplicateText', fallback_resource=f'{self._r}.duplicateText', ), ) self._lock_images.append( bui.imagewidget( parent=self._root_widget, size=(30, 30), draw_controller=btn, position=(h - 10, v + 58.0 * scl - 28), texture=lock_tex, ) ) v -= 65.0 * scl delete_button = btn = bui.buttonwidget( parent=self._root_widget, position=(h, v), size=(90, 58.0 * scl), on_activate_call=self._delete_playlist, color=b_color, autoselect=True, textcolor=b_textcolor, button_type='square', text_scale=0.7, label=bui.Lstr( resource='deleteText', fallback_resource=f'{self._r}.deleteText' ), ) self._lock_images.append( bui.imagewidget( parent=self._root_widget, size=(30, 30), draw_controller=btn, position=(h - 10, v + 58.0 * scl - 28), texture=lock_tex, ) ) v -= 65.0 * scl self._import_button = bui.buttonwidget( parent=self._root_widget, position=(h, v), size=(90, 58.0 * scl), on_activate_call=self._import_playlist, color=b_color, autoselect=True, textcolor=b_textcolor, button_type='square', text_scale=0.7, label=bui.Lstr(resource='importText'), ) v -= 65.0 * scl btn = bui.buttonwidget( parent=self._root_widget, position=(h, v), size=(90, 58.0 * scl), on_activate_call=self._share_playlist, color=b_color, autoselect=True, textcolor=b_textcolor, button_type='square', text_scale=0.7, label=bui.Lstr(resource='shareText'), ) self._lock_images.append( bui.imagewidget( parent=self._root_widget, size=(30, 30), draw_controller=btn, position=(h - 10, v + 58.0 * scl - 28), texture=lock_tex, ) ) v = self._height - 75 + yoffs self._scroll_height = self._height - ( 180 if uiscale is bui.UIScale.SMALL else 119 ) scrollwidget = bui.scrollwidget( parent=self._root_widget, position=(140 + x_inset, v - self._scroll_height), size=(self._width - (180 + 2 * x_inset), self._scroll_height + 10), highlight=False, ) if self._back_button is not None: bui.widget(edit=self._back_button, right_widget=scrollwidget) self._columnwidget = bui.columnwidget( parent=scrollwidget, border=2, margin=0 ) h = 145 self._do_randomize_val = bui.app.config.get( self._pvars.config_name + ' Playlist Randomize', 0 ) h += 210 for btn in [new_button, delete_button, edit_button, duplicate_button]: bui.widget(edit=btn, right_widget=scrollwidget) bui.widget( edit=scrollwidget, left_widget=new_button, right_widget=bui.get_special_widget('squad_button'), ) # Make sure config exists. self._config_name_full = f'{self._pvars.config_name} Playlists' if self._config_name_full not in bui.app.config: bui.app.config[self._config_name_full] = {} self._selected_playlist_name: str | None = None self._selected_playlist_index: int | None = None self._playlist_widgets: list[bui.Widget] = [] self._refresh(select_playlist=select_playlist) if self._back_button is not None: bui.buttonwidget( edit=self._back_button, on_activate_call=self.main_window_back ) bui.containerwidget( edit=self._root_widget, cancel_button=self._back_button ) bui.containerwidget(edit=self._root_widget, selected_child=scrollwidget) # Keep our lock images up to date/etc. self._update_timer = bui.AppTimer( 1.0, bui.WeakCall(self._update), repeat=True ) self._update()
[docs] @override def get_main_window_state(self) -> bui.MainWindowState: # Support recreating our window for back/refresh purposes. cls = type(self) # Avoid dereferencing self within the lambda or we'll keep # ourself alive indefinitely. stype = self._sessiontype return bui.BasicMainWindowState( create_call=lambda transition, origin_widget: cls( transition=transition, origin_widget=origin_widget, sessiontype=stype, ) )
[docs] @override def on_main_window_close(self) -> None: if self._selected_playlist_name is not None: cfg = bui.app.config cfg[f'{self._pvars.config_name} Playlist Selection'] = ( self._selected_playlist_name ) cfg.commit()
def _update(self) -> None: assert bui.app.classic is not None have = bui.app.classic.accounts.have_pro_options() for lock in self._lock_images: bui.imagewidget( edit=lock, opacity=0.0 if (have or not REQUIRE_PRO) else 1.0 ) def _select(self, name: str, index: int) -> None: self._selected_playlist_name = name self._selected_playlist_index = index def _refresh(self, select_playlist: str | None = None) -> None: from efro.util import asserttype old_selection = self._selected_playlist_name # If there was no prev selection, look in prefs. if old_selection is None: old_selection = bui.app.config.get( self._pvars.config_name + ' Playlist Selection' ) old_selection_index = self._selected_playlist_index # Delete old. while self._playlist_widgets: self._playlist_widgets.pop().delete() items = list(bui.app.config[self._config_name_full].items()) # Make sure everything is unicode now. items = [ (i[0].decode(), i[1]) if not isinstance(i[0], str) else i for i in items ] items.sort(key=lambda x: asserttype(x[0], str).lower()) items = [['__default__', None]] + items # Default is always first. index = 0 for pname, _ in items: assert pname is not None txtw = bui.textwidget( parent=self._columnwidget, size=(self._width - 40, 30), maxwidth=440, text=self._get_playlist_display_name(pname), h_align='left', v_align='center', color=( (0.6, 0.6, 0.7, 1.0) if pname == '__default__' else (0.85, 0.85, 0.85, 1) ), always_highlight=True, on_select_call=bui.Call(self._select, pname, index), on_activate_call=bui.Call(self._edit_button.activate), selectable=True, ) bui.widget(edit=txtw, show_buffer_top=50, show_buffer_bottom=50) # Hitting up from top widget should jump to 'back'. if index == 0: bui.widget( edit=txtw, up_widget=( self._back_button if self._back_button is not None else bui.get_special_widget('back_button') ), ) self._playlist_widgets.append(txtw) # Select this one if the user requested it. if select_playlist is not None: if pname == select_playlist: bui.columnwidget( edit=self._columnwidget, selected_child=txtw, visible_child=txtw, ) else: # Select this one if it was previously selected. Go by # index if there's one. if old_selection_index is not None: if index == old_selection_index: bui.columnwidget( edit=self._columnwidget, selected_child=txtw, visible_child=txtw, ) else: # Otherwise look by name. if pname == old_selection: bui.columnwidget( edit=self._columnwidget, selected_child=txtw, visible_child=txtw, ) index += 1 def _save_playlist_selection(self) -> None: # Store the selected playlist in prefs. This serves dual # purposes of letting us re-select it next time if we want and # also lets us pass it to the game (since we reset the whole # python environment that's not actually easy). cfg = bui.app.config cfg[self._pvars.config_name + ' Playlist Selection'] = ( self._selected_playlist_name ) cfg[self._pvars.config_name + ' Playlist Randomize'] = ( self._do_randomize_val ) cfg.commit() def _new_playlist(self) -> None: # pylint: disable=cyclic-import from bauiv1lib.playlist.editcontroller import PlaylistEditController from bauiv1lib.purchase import PurchaseWindow # No-op if we're not in control. if not self.main_window_has_control(): return assert bui.app.classic is not None if REQUIRE_PRO and not bui.app.classic.accounts.have_pro_options(): PurchaseWindow(items=['pro']) return # Clamp at our max playlist number. if len(bui.app.config[self._config_name_full]) > self._max_playlists: bui.screenmessage( bui.Lstr( translate=( 'serverResponses', 'Max number of playlists reached.', ) ), color=(1, 0, 0), ) bui.getsound('error').play() return # In case they cancel so we can return to this state. self._save_playlist_selection() # Kick off the edit UI. PlaylistEditController(sessiontype=self._sessiontype, from_window=self) def _edit_playlist(self) -> None: # pylint: disable=cyclic-import from bauiv1lib.playlist.editcontroller import PlaylistEditController from bauiv1lib.purchase import PurchaseWindow assert bui.app.classic is not None if REQUIRE_PRO and not bui.app.classic.accounts.have_pro_options(): PurchaseWindow(items=['pro']) return if self._selected_playlist_name is None: return if self._selected_playlist_name == '__default__': bui.getsound('error').play() bui.screenmessage( bui.Lstr(resource=f'{self._r}.cantEditDefaultText') ) return self._save_playlist_selection() PlaylistEditController( existing_playlist_name=self._selected_playlist_name, sessiontype=self._sessiontype, from_window=self, ) def _do_delete_playlist(self) -> None: plus = bui.app.plus assert plus is not None plus.add_v1_account_transaction( { 'type': 'REMOVE_PLAYLIST', 'playlistType': self._pvars.config_name, 'playlistName': self._selected_playlist_name, } ) plus.run_v1_account_transactions() bui.getsound('shieldDown').play() # (we don't use len()-1 here because the default list adds one) assert self._selected_playlist_index is not None self._selected_playlist_index = min( self._selected_playlist_index, len(bui.app.config[self._pvars.config_name + ' Playlists']), ) self._refresh() def _import_playlist(self) -> None: # pylint: disable=cyclic-import from bauiv1lib.playlist import share plus = bui.app.plus assert plus is not None # Gotta be signed in for this to work. if plus.get_v1_account_state() != 'signed_in': bui.screenmessage( bui.Lstr(resource='notSignedInErrorText'), color=(1, 0, 0) ) bui.getsound('error').play() return share.SharePlaylistImportWindow( origin_widget=self._import_button, on_success_callback=bui.WeakCall(self._on_playlist_import_success), ) def _on_playlist_import_success(self) -> None: self._refresh() def _on_share_playlist_response(self, name: str, response: Any) -> None: # pylint: disable=cyclic-import from bauiv1lib.playlist import share if response is None: bui.screenmessage( bui.Lstr(resource='internal.unavailableNoConnectionText'), color=(1, 0, 0), ) bui.getsound('error').play() return share.SharePlaylistResultsWindow(name, response) def _share_playlist(self) -> None: # pylint: disable=cyclic-import from bauiv1lib.purchase import PurchaseWindow plus = bui.app.plus assert plus is not None assert bui.app.classic is not None if REQUIRE_PRO and not bui.app.classic.accounts.have_pro_options(): PurchaseWindow(items=['pro']) return # Gotta be signed in for this to work. if plus.get_v1_account_state() != 'signed_in': bui.screenmessage( bui.Lstr(resource='notSignedInErrorText'), color=(1, 0, 0) ) bui.getsound('error').play() return if self._selected_playlist_name == '__default__': bui.getsound('error').play() bui.screenmessage( bui.Lstr(resource=f'{self._r}.cantShareDefaultText'), color=(1, 0, 0), ) return if self._selected_playlist_name is None: return plus.add_v1_account_transaction( { 'type': 'SHARE_PLAYLIST', 'expire_time': time.time() + 5, 'playlistType': self._pvars.config_name, 'playlistName': self._selected_playlist_name, }, callback=bui.WeakCall( self._on_share_playlist_response, self._selected_playlist_name ), ) plus.run_v1_account_transactions() bui.screenmessage(bui.Lstr(resource='sharingText')) def _delete_playlist(self) -> None: # pylint: disable=cyclic-import from bauiv1lib.purchase import PurchaseWindow from bauiv1lib.confirm import ConfirmWindow assert bui.app.classic is not None if REQUIRE_PRO and not bui.app.classic.accounts.have_pro_options(): PurchaseWindow(items=['pro']) return if self._selected_playlist_name is None: return if self._selected_playlist_name == '__default__': bui.getsound('error').play() bui.screenmessage( bui.Lstr(resource=f'{self._r}.cantDeleteDefaultText') ) else: ConfirmWindow( bui.Lstr( resource=f'{self._r}.deleteConfirmText', subs=[('${LIST}', self._selected_playlist_name)], ), self._do_delete_playlist, 450, 150, ) def _get_playlist_display_name(self, playlist: str) -> bui.Lstr: if playlist == '__default__': return self._pvars.default_list_name return ( playlist if isinstance(playlist, bui.Lstr) else bui.Lstr(value=playlist) ) def _duplicate_playlist(self) -> None: # pylint: disable=too-many-branches # pylint: disable=cyclic-import from bauiv1lib.purchase import PurchaseWindow plus = bui.app.plus assert plus is not None assert bui.app.classic is not None if REQUIRE_PRO and not bui.app.classic.accounts.have_pro_options(): PurchaseWindow(items=['pro']) return if self._selected_playlist_name is None: return plst: list[dict[str, Any]] | None if self._selected_playlist_name == '__default__': plst = self._pvars.get_default_list_call() else: plst = bui.app.config[self._config_name_full].get( self._selected_playlist_name ) if plst is None: bui.getsound('error').play() return # Clamp at our max playlist number. if len(bui.app.config[self._config_name_full]) > self._max_playlists: bui.screenmessage( bui.Lstr( translate=( 'serverResponses', 'Max number of playlists reached.', ) ), color=(1, 0, 0), ) bui.getsound('error').play() return copy_text = bui.Lstr(resource='copyOfText').evaluate() # Get just 'Copy' or whatnot. copy_word = copy_text.replace('${NAME}', '').strip() # Find a valid dup name that doesn't exist. test_index = 1 base_name = self._get_playlist_display_name( self._selected_playlist_name ).evaluate() # If it looks like a copy, strip digits and spaces off the end. if copy_word in base_name: while base_name[-1].isdigit() or base_name[-1] == ' ': base_name = base_name[:-1] while True: if copy_word in base_name: test_name = base_name else: test_name = copy_text.replace('${NAME}', base_name) if test_index > 1: test_name += ' ' + str(test_index) if test_name not in bui.app.config[self._config_name_full]: break test_index += 1 plus.add_v1_account_transaction( { 'type': 'ADD_PLAYLIST', 'playlistType': self._pvars.config_name, 'playlistName': test_name, 'playlist': copy.deepcopy(plst), } ) plus.run_v1_account_transactions() bui.getsound('gunCocking').play() self._refresh(select_playlist=test_name)