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

Window for viewing a playlist.

PlaylistCustomizeBrowserWindow( sessiontype: type[bascenev1._session.Session], transition: str = 'in_right', select_playlist: str | None = None, origin_widget: _bauiv1.Widget | None = None)
 23    def __init__(
 24        self,
 25        sessiontype: type[bs.Session],
 26        transition: str = 'in_right',
 27        select_playlist: str | None = None,
 28        origin_widget: bui.Widget | None = None,
 29    ):
 30        # Yes this needs tidying.
 31        # pylint: disable=too-many-locals
 32        # pylint: disable=too-many-statements
 33        # pylint: disable=cyclic-import
 34        from bauiv1lib import playlist
 35
 36        scale_origin: tuple[float, float] | None
 37        if origin_widget is not None:
 38            self._transition_out = 'out_scale'
 39            scale_origin = origin_widget.get_screen_space_center()
 40            transition = 'in_scale'
 41        else:
 42            self._transition_out = 'out_right'
 43            scale_origin = None
 44
 45        self._sessiontype = sessiontype
 46        self._pvars = playlist.PlaylistTypeVars(sessiontype)
 47        self._max_playlists = 30
 48        self._r = 'gameListWindow'
 49        assert bui.app.classic is not None
 50        uiscale = bui.app.ui_v1.uiscale
 51        self._width = 850.0 if uiscale is bui.UIScale.SMALL else 650.0
 52        x_inset = 100.0 if uiscale is bui.UIScale.SMALL else 0.0
 53        self._height = (
 54            380.0
 55            if uiscale is bui.UIScale.SMALL
 56            else 420.0 if uiscale is bui.UIScale.MEDIUM else 500.0
 57        )
 58        top_extra = 20.0 if uiscale is bui.UIScale.SMALL else 0.0
 59
 60        super().__init__(
 61            root_widget=bui.containerwidget(
 62                size=(self._width, self._height + top_extra),
 63                transition=transition,
 64                scale_origin_stack_offset=scale_origin,
 65                scale=(
 66                    2.05
 67                    if uiscale is bui.UIScale.SMALL
 68                    else 1.5 if uiscale is bui.UIScale.MEDIUM else 1.0
 69                ),
 70                stack_offset=(
 71                    (0, -10) if uiscale is bui.UIScale.SMALL else (0, 0)
 72                ),
 73            )
 74        )
 75
 76        self._back_button = back_button = btn = bui.buttonwidget(
 77            parent=self._root_widget,
 78            position=(43 + x_inset, self._height - 60),
 79            size=(160, 68),
 80            scale=0.77,
 81            autoselect=True,
 82            text_scale=1.3,
 83            label=bui.Lstr(resource='backText'),
 84            button_type='back',
 85        )
 86
 87        bui.textwidget(
 88            parent=self._root_widget,
 89            position=(0, self._height - 47),
 90            size=(self._width, 25),
 91            text=bui.Lstr(
 92                resource=self._r + '.titleText',
 93                subs=[('${TYPE}', self._pvars.window_title_name)],
 94            ),
 95            color=bui.app.ui_v1.heading_color,
 96            maxwidth=290,
 97            h_align='center',
 98            v_align='center',
 99        )
100
101        bui.buttonwidget(
102            edit=btn,
103            button_type='backSmall',
104            size=(60, 60),
105            label=bui.charstr(bui.SpecialChar.BACK),
106        )
107
108        v = self._height - 59.0
109        h = 41 + x_inset
110        b_color = (0.6, 0.53, 0.63)
111        b_textcolor = (0.75, 0.7, 0.8)
112        self._lock_images: list[bui.Widget] = []
113        lock_tex = bui.gettexture('lock')
114
115        scl = (
116            1.1
117            if uiscale is bui.UIScale.SMALL
118            else 1.27 if uiscale is bui.UIScale.MEDIUM else 1.57
119        )
120        scl *= 0.63
121        v -= 65.0 * scl
122        new_button = btn = bui.buttonwidget(
123            parent=self._root_widget,
124            position=(h, v),
125            size=(90, 58.0 * scl),
126            on_activate_call=self._new_playlist,
127            color=b_color,
128            autoselect=True,
129            button_type='square',
130            textcolor=b_textcolor,
131            text_scale=0.7,
132            label=bui.Lstr(
133                resource='newText', fallback_resource=self._r + '.newText'
134            ),
135        )
136        self._lock_images.append(
137            bui.imagewidget(
138                parent=self._root_widget,
139                size=(30, 30),
140                draw_controller=btn,
141                position=(h - 10, v + 58.0 * scl - 28),
142                texture=lock_tex,
143            )
144        )
145
146        v -= 65.0 * scl
147        self._edit_button = edit_button = btn = bui.buttonwidget(
148            parent=self._root_widget,
149            position=(h, v),
150            size=(90, 58.0 * scl),
151            on_activate_call=self._edit_playlist,
152            color=b_color,
153            autoselect=True,
154            textcolor=b_textcolor,
155            button_type='square',
156            text_scale=0.7,
157            label=bui.Lstr(
158                resource='editText', fallback_resource=self._r + '.editText'
159            ),
160        )
161        self._lock_images.append(
162            bui.imagewidget(
163                parent=self._root_widget,
164                size=(30, 30),
165                draw_controller=btn,
166                position=(h - 10, v + 58.0 * scl - 28),
167                texture=lock_tex,
168            )
169        )
170
171        v -= 65.0 * scl
172        duplicate_button = btn = bui.buttonwidget(
173            parent=self._root_widget,
174            position=(h, v),
175            size=(90, 58.0 * scl),
176            on_activate_call=self._duplicate_playlist,
177            color=b_color,
178            autoselect=True,
179            textcolor=b_textcolor,
180            button_type='square',
181            text_scale=0.7,
182            label=bui.Lstr(
183                resource='duplicateText',
184                fallback_resource=self._r + '.duplicateText',
185            ),
186        )
187        self._lock_images.append(
188            bui.imagewidget(
189                parent=self._root_widget,
190                size=(30, 30),
191                draw_controller=btn,
192                position=(h - 10, v + 58.0 * scl - 28),
193                texture=lock_tex,
194            )
195        )
196
197        v -= 65.0 * scl
198        delete_button = btn = bui.buttonwidget(
199            parent=self._root_widget,
200            position=(h, v),
201            size=(90, 58.0 * scl),
202            on_activate_call=self._delete_playlist,
203            color=b_color,
204            autoselect=True,
205            textcolor=b_textcolor,
206            button_type='square',
207            text_scale=0.7,
208            label=bui.Lstr(
209                resource='deleteText', fallback_resource=self._r + '.deleteText'
210            ),
211        )
212        self._lock_images.append(
213            bui.imagewidget(
214                parent=self._root_widget,
215                size=(30, 30),
216                draw_controller=btn,
217                position=(h - 10, v + 58.0 * scl - 28),
218                texture=lock_tex,
219            )
220        )
221        v -= 65.0 * scl
222        self._import_button = bui.buttonwidget(
223            parent=self._root_widget,
224            position=(h, v),
225            size=(90, 58.0 * scl),
226            on_activate_call=self._import_playlist,
227            color=b_color,
228            autoselect=True,
229            textcolor=b_textcolor,
230            button_type='square',
231            text_scale=0.7,
232            label=bui.Lstr(resource='importText'),
233        )
234        v -= 65.0 * scl
235        btn = bui.buttonwidget(
236            parent=self._root_widget,
237            position=(h, v),
238            size=(90, 58.0 * scl),
239            on_activate_call=self._share_playlist,
240            color=b_color,
241            autoselect=True,
242            textcolor=b_textcolor,
243            button_type='square',
244            text_scale=0.7,
245            label=bui.Lstr(resource='shareText'),
246        )
247        self._lock_images.append(
248            bui.imagewidget(
249                parent=self._root_widget,
250                size=(30, 30),
251                draw_controller=btn,
252                position=(h - 10, v + 58.0 * scl - 28),
253                texture=lock_tex,
254            )
255        )
256
257        v = self._height - 75
258        self._scroll_height = self._height - 119
259        scrollwidget = bui.scrollwidget(
260            parent=self._root_widget,
261            position=(140 + x_inset, v - self._scroll_height),
262            size=(self._width - (180 + 2 * x_inset), self._scroll_height + 10),
263            highlight=False,
264        )
265        bui.widget(edit=back_button, right_widget=scrollwidget)
266        self._columnwidget = bui.columnwidget(
267            parent=scrollwidget, border=2, margin=0
268        )
269
270        h = 145
271
272        self._do_randomize_val = bui.app.config.get(
273            self._pvars.config_name + ' Playlist Randomize', 0
274        )
275
276        h += 210
277
278        for btn in [new_button, delete_button, edit_button, duplicate_button]:
279            bui.widget(edit=btn, right_widget=scrollwidget)
280        bui.widget(
281            edit=scrollwidget,
282            left_widget=new_button,
283            right_widget=(
284                bui.get_special_widget('party_button')
285                if bui.app.ui_v1.use_toolbars
286                else None
287            ),
288        )
289
290        # make sure config exists
291        self._config_name_full = self._pvars.config_name + ' Playlists'
292
293        if self._config_name_full not in bui.app.config:
294            bui.app.config[self._config_name_full] = {}
295
296        self._selected_playlist_name: str | None = None
297        self._selected_playlist_index: int | None = None
298        self._playlist_widgets: list[bui.Widget] = []
299
300        self._refresh(select_playlist=select_playlist)
301
302        bui.buttonwidget(edit=back_button, on_activate_call=self._back)
303        bui.containerwidget(edit=self._root_widget, cancel_button=back_button)
304
305        bui.containerwidget(edit=self._root_widget, selected_child=scrollwidget)
306
307        # Keep our lock images up to date/etc.
308        self._update_timer = bui.AppTimer(
309            1.0, bui.WeakCall(self._update), repeat=True
310        )
311        self._update()
Inherited Members
bauiv1._uitypes.Window
get_root_widget