bauiv1lib.playlist.customizebrowser

Provides UI for viewing/creating/editing playlists.

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

Window for viewing a playlist.

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

Create a MainWindow given a root widget and transition info.

Automatically handles in and out transitions on the provided widget, so there is no need to set transitions when creating it.

@override
def get_main_window_state(self) -> bauiv1.MainWindowState:
323    @override
324    def get_main_window_state(self) -> bui.MainWindowState:
325        # Support recreating our window for back/refresh purposes.
326        cls = type(self)
327
328        # Avoid dereferencing self within the lambda or we'll keep
329        # ourself alive indefinitely.
330        stype = self._sessiontype
331
332        return bui.BasicMainWindowState(
333            create_call=lambda transition, origin_widget: cls(
334                transition=transition,
335                origin_widget=origin_widget,
336                sessiontype=stype,
337            )
338        )

Return a WindowState to recreate this window, if supported.

Inherited Members
bauiv1._uitypes.MainWindow
main_window_back_state
main_window_close
can_change_main_window
main_window_back
main_window_replace
on_main_window_close
bauiv1._uitypes.Window
get_root_widget