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

from typing import TYPE_CHECKING, override

import bauiv1 as bui

if TYPE_CHECKING:
    from typing import Any, Callable

    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, ): # 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 = 1200.0 if uiscale is bui.UIScale.SMALL else 650.0 self._height = ( 800.0 if uiscale is bui.UIScale.SMALL else 420.0 if uiscale is bui.UIScale.MEDIUM else 500.0 ) # Do some fancy math to fill all available screen area up to the # size of our backing container. This lets us fit to the exact # screen shape at small ui scale. screensize = bui.get_virtual_screen_size() scale = ( 1.8 if uiscale is bui.UIScale.SMALL else 1.4 if uiscale is bui.UIScale.MEDIUM else 1.0 ) # Calc screen size in our local container space and clamp to a # bit smaller than our container size. target_width = min(self._width - 70, screensize[0] / scale) target_height = min(self._height - 40, screensize[1] / scale) # To get top/left coords, go to the center of our window and # offset by half the width/height of our target area. yoffs = ( 0.5 * self._height + 0.5 * target_height + (30.0 if uiscale is bui.UIScale.SMALL else 50) ) self._button_width = 90 self._x_inset = 10 self._scroll_width = ( target_width - self._button_width - 2.0 * self._x_inset ) self._scroll_height = target_height - 75 self._scroll_bottom = yoffs - 98 - self._scroll_height self._button_height = self._scroll_height / 6.0 super().__init__( root_widget=bui.containerwidget( size=(self._width, self._height), scale=scale, toolbar_visibility=( 'menu_minimal' if uiscale is bui.UIScale.SMALL else 'menu_full' ), ), transition=transition, origin_widget=origin_widget, # We're affected by screen size only at small ui-scale. refresh_on_screen_size_changes=uiscale is bui.UIScale.SMALL, ) 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, yoffs - 87), size=(60, 60), scale=0.77, autoselect=True, text_scale=1.3, label=bui.charstr(bui.SpecialChar.BACK), button_type='backSmall', ) bui.textwidget( parent=self._root_widget, position=(0, yoffs - (77 if uiscale is bui.UIScale.SMALL else 77)), 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', ) h = self._width * 0.5 - (self._scroll_width + self._button_width) * 0.5 b_color = (0.6, 0.53, 0.63) b_textcolor = (0.75, 0.7, 0.8) self._lock_images: list[bui.Widget] = [] xmargin = 0.06 ymargin = 0.05 def _make_button( i: int, label: bui.Lstr, call: Callable[[], None] ) -> bui.Widget: v = self._scroll_bottom + self._button_height * i return bui.buttonwidget( parent=self._root_widget, position=( h + xmargin * self._button_width, v + ymargin * self._button_height, ), size=( self._button_width * (1.0 - 2.0 * xmargin), self._button_height * (1.0 - 2.0 * ymargin), ), on_activate_call=call, color=b_color, autoselect=True, button_type='square', textcolor=b_textcolor, text_scale=0.7, label=label, ) new_button = _make_button( 5, bui.Lstr( resource='newText', fallback_resource=f'{self._r}.newText' ), self._new_playlist, ) self._edit_button = _make_button( 4, bui.Lstr( resource='editText', fallback_resource=f'{self._r}.editText', ), self._edit_playlist, ) duplicate_button = _make_button( 3, bui.Lstr( resource='duplicateText', fallback_resource=f'{self._r}.duplicateText', ), self._duplicate_playlist, ) delete_button = _make_button( 2, bui.Lstr( resource='deleteText', fallback_resource=f'{self._r}.deleteText' ), self._delete_playlist, ) self._import_button = _make_button( 1, bui.Lstr(resource='importText'), self._import_playlist ) share_button = _make_button( 0, bui.Lstr(resource='shareText'), self._share_playlist ) scrollwidget = bui.scrollwidget( parent=self._root_widget, size=(self._scroll_width, self._scroll_height), position=( self._width * 0.5 - (self._scroll_width + self._button_width) * 0.5 + self._button_width, self._scroll_bottom, ), highlight=False, border_opacity=0.4, ) 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, self._edit_button, duplicate_button, self._import_button, share_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)
# Docs-generation hack; import some stuff that we likely only forward-declared # in our actual source code so that docs tools can find it. from typing import (Coroutine, Any, Literal, Callable, Generator, Awaitable, Sequence, Self) import asyncio from concurrent.futures import Future from pathlib import Path from enum import Enum