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

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