# used to hide / show the feather def updateFeather(): if pic.winfo_viewable(): pic.grid_remove() namelbl.grid_remove() else: pic.grid() namelbl.grid() # create the root window root = ThemedTk(theme="breeze") root.title("Feather Window") root.option_add('*tearOff', FALSE) # so the menu doesn't look weird # create the main frame of the program and the sub frame content = ttk.Frame(root) subframe = ttk.Frame(content, borderwidth=5, relief='sunken') # create a menu menubar = Menu(root) sub1 = Menu(root) menubar.add_cascade(label="test1", menu=sub1) sub1.add_command(label='a') sub1.add_command(label='b') sub1.add_command(label='c')
def __init__(self, playlists: list[Playlist], defaults: dict): root = ThemedTk(theme='black') root.option_add('*tearOff', tk.FALSE) root.wm_title("btecify") root.wm_iconbitmap('assets\\btecify.ico') root.resizable(width=False, height=False) root.wm_iconify() root.wm_deiconify() for namefont in tkfont.names(root): rootfont = tkfont.nametofont(namefont) rootfont.config(family="Lucida Console") def _onclose(): self.output = ["EXIT"] root.destroy() root.protocol("WM_DELETE_WINDOW", _onclose) self.selectedplaylist = tk.StringVar() self.playlists = playlists self._generateplaylistnames() self.songqueuevar = tk.StringVar(value=[]) self.volume = tk.IntVar(value=50) self.seek = tk.DoubleVar(value=0) self.songlistvar = tk.StringVar(value=[]) self.playlistsongsvar = tk.StringVar(value=[]) self.progressbarvar = tk.IntVar(value=0) self.songsearchqueryvar = tk.StringVar(value="") self.extrainfoplaylistsvar = tk.StringVar(value=[]) self.searchfunc = searchsongname self.discordpresencevar = tk.BooleanVar(value=defaults['discord']) self.keybinds: list[tuple[str, str]] = [] # CONSOLE consolewindow = tk.Toplevel(root) consolewindow.wm_title("Console Logs") consolewindow.wm_protocol("WM_DELETE_WINDOW", lambda: consolewindow.wm_withdraw()) consolewindow.wm_withdraw() consolewindow.wm_resizable(False, False) consolewindowframe = ttk.Frame(consolewindow, padding=5, relief="groove") consolewindowframe.grid() consolewindowtext = tk.Text(consolewindowframe, foreground='white', background='black', state='disabled', width=100, height=40) consolewindowtext.grid(row=0, column=0) consolewindowtextscrollbar = ttk.Scrollbar( consolewindowframe, orient=tk.VERTICAL, command=consolewindowtext.yview) consolewindowtext['yscrollcommand'] = consolewindowtextscrollbar.set consolewindowtextscrollbar.grid(row=0, column=1, sticky='ns') def resetconsolewindow(*args): consolewindowtext.yview_moveto(1.0) consolewindowtext.bind('<Visibility>', resetconsolewindow) consolewindowtext.bind('<FocusIn>', resetconsolewindow) # KEYBINDS keybindwindow = tk.Toplevel(root) keybindwindow.wm_title("Keybindings") keybindwindow.wm_protocol("WM_DELETE_WINDOW", lambda: keybindwindow.wm_withdraw()) keybindwindow.wm_resizable(False, False) keybindwindow.wm_withdraw() keybindwindowframe = ttk.Frame(keybindwindow, padding=5, relief='groove') keybindwindowframe.grid() keybindlistframe = ttk.Frame(keybindwindowframe, padding=3, relief='groove') keybindlistframe.grid(row=0, column=0) keybindings = [i for i in defaults['keybinds']] keybindlist = [] for x in range(len(keybindings)): kbname = str(keybindings[x]) newframe = ttk.Frame(keybindlistframe) newframe.grid(column=0, row=x) newlabel = ttk.Label( newframe, text=kbname + ": ", width=max(map(lambda a: len(a), keybindings)) + 2) newlabel.grid(row=0, column=0) keybindtextvariable = tk.StringVar("") newentry = ttk.Entry(newframe, textvariable=keybindtextvariable) newentry.grid(row=0, column=1) newentry.bind('<FocusIn>', lambda *args: self._addchange('keybinds')) keybindlist.append((kbname, keybindtextvariable)) keybindbuttonsframe = ttk.Frame(keybindwindowframe, padding=3, relief='groove') keybindbuttonsframe.grid(row=1, column=0) keybindbuttondefault = ttk.Button( keybindbuttonsframe, text="RESET TO DEFAULTS", command=lambda: self._setoutput("defaultkeybinds")) keybindbuttondefault.grid(row=0, column=0) keybindbuttonconfirm = ttk.Button( keybindbuttonsframe, text="CONFIRM KEYBINDINGS", command=lambda: self._setoutput( "updatekeybinds", [(i[0], i[1].get()) for i in keybindlist])) keybindbuttonconfirm.grid(row=0, column=1) # MENU menubar = tk.Menu(root) root.configure(menu=menubar) menuplaylist = tk.Menu(menubar) menusong = tk.Menu(menubar) menufile = tk.Menu(menubar) menubar.add_cascade(menu=menuplaylist, label="Playlist") menubar.add_cascade(menu=menusong, label="Song") menubar.add_cascade(menu=menufile, label="File") menubar.add_separator() menubar.add_command(label="Playlist: None", state="disabled") menubarplaylistlabelindex = len(menubar.winfo_children()) + 1 menuplaylist.add_command(label="New...", command=self._newplaylist) menuplaylist.add_command(label="Delete", command=self._deleteplaylist) menuplaylist.add_command(label="Rename...", command=self._renameplaylist) menuplaylist.add_command(label="Copy...", command=self._copyplaylist) menuplaylist.add_separator() menuplaylist.add_command(label="Reset watched", command=self._unwatchplaylist) menuplaylist.add_command(label="Requeue", command=self._requeue) menuplaylist.add_separator() menuplaylist.add_command(label="Reset from Youtube", command=self._resetfromyoutube) menusong.add_command(label="New...", command=self._newsong) menusong.add_command(label="Delete", command=lambda: self._setoutput( "deletesongs", *self._getselectedsongs())) menusong.add_separator() menusong.add_command(label="Add selected songs to selected playlist", command=self._addsongtoplaylist) menusong.add_command( label="Remove selected songs from selected playlist", command=lambda: self._setoutput("removesongsfromplaylist", self._getselectedplaylist(), self._getselectedsongs())) menusong.add_separator() menusong.add_command(label="Play selected song", command=self._playselectedsong) menusong.add_command(label="Play random song", command=lambda: self._setoutput("randomsong")) menufile.add_command(label="View console logs...", command=consolewindow.wm_deiconify) menufile.add_command( label="Open data directory...", command=lambda: self._setoutput("opendatadirectory")) menufile.add_separator() menufile.add_command(label="Change keybinds...", command=keybindwindow.wm_deiconify) menufile.add_separator() menufile.add_checkbutton( label="Discord Presence", command=lambda: self._setoutput('discordpresence', self.discordpresencevar.get()), variable=self.discordpresencevar) menufile.add_separator() menufile.add_command(label="Change API key...", command=lambda: self._setoutput("newapikey")) menufile.add_separator() menufile.add_command(label="Login Details...", command=self._logindetails) menufile.add_command(label="Sync playlist to btecify servers", command=lambda: self._setoutput("syncwithserver")) # PRIMARY FRAME primaryframe = ttk.Frame(root) primaryframe.grid() # QUEUE queuelabelframe = ttk.Labelframe(primaryframe, text="Song queue", relief='groove', borderwidth=5) queuelabelframe.grid(column=0, row=0, columnspan=2, rowspan=2, sticky='nswe') queuelist = mylistbox(queuelabelframe, height=15, listvariable=self.songqueuevar, width=50, exportselection=False, selectmode=tk.MULTIPLE) queuelistscrollbar = ttk.Scrollbar(queuelabelframe, orient=tk.VERTICAL, command=queuelist.yview) queuelist.grid(column=0, row=0, sticky='nswe') queuelistscrollbar.grid(column=1, row=0, sticky='ns') queuelist['yscrollcommand'] = queuelistscrollbar.set # PLAYER INFORMATION playingframe = ttk.Labelframe(primaryframe, text="Playing Song", relief='groove', padding=5) playingframe.grid(column=2, row=0, sticky='new') songinfo = ttk.Label( playingframe, text= f"No playlist\nNo song playing\nNo song author\nNo duration\n{PLAYINGINFOPLACEHOLDER}", justify=tk.CENTER, anchor=tk.CENTER) songinfo.grid(column=0, row=0, sticky='ew') songdesc = ttk.Label(playingframe, text="", justify=tk.CENTER, anchor=tk.CENTER) songdesc.grid(column=0, row=1) songprogress = ttk.Progressbar(playingframe, orient=tk.HORIZONTAL, mode='determinate', variable=self.progressbarvar) songprogress.grid(column=0, row=3, sticky='wes') songseeker = ttk.Scale(playingframe, from_=0, to=1, variable=self.seek) songseeker.grid(column=0, row=4, sticky='wes') songseeker.bind("<ButtonPress-1>", lambda *args: self.changes.update({'seeking': True})) songseeker.bind("<ButtonRelease-1>", lambda *args: self.changes.update({'seeking': False})) playingframe.grid_rowconfigure((0, 1, 2, 3), weight=1) # SONG SELECTION AND SONG VIEWING songselectionandviewingframe = ttk.Frame(primaryframe) songselectionandviewingframe.grid(column=3, row=0, columnspan=2, rowspan=2) songlistnotebook = ttk.Notebook(songselectionandviewingframe) songlistnotebook.grid(column=0, row=0) songlistframe = ttk.Frame(songlistnotebook, padding=1) songlist = mylistbox(songlistframe, height=15, listvariable=self.songlistvar, selectmode=tk.MULTIPLE, bg="#282828", disabledforeground="gray80", fg="white", activestyle='dotbox', selectbackground="#282828", selectforeground="red2", width=50, exportselection=False) songlistscrollbar = ttk.Scrollbar(songlistframe, orient=tk.VERTICAL, command=songlist.yview) ################################################################################################################ playlistsongsframe = ttk.Frame(songlistnotebook, padding=1) playlistsongslist = mylistbox(playlistsongsframe, height=15, listvariable=self.playlistsongsvar, selectmode=tk.MULTIPLE, bg="#282828", disabledforeground="gray80", fg="white", activestyle='dotbox', selectbackground="#282828", selectforeground="red2", width=50, exportselection=False) playlistsongslistscrollbar = ttk.Scrollbar( playlistsongsframe, orient=tk.VERTICAL, command=playlistsongslist.yview) ################################################################################################################ _songlistsearchchangedcommand = root._register( self._songlistsearchchanged) songsearchentry = ttk.Entry( songselectionandviewingframe, validate="all", validatecommand=(_songlistsearchchangedcommand, '%V'), textvariable=self.songsearchqueryvar, ) self.completeselectedsongs: list[Song] = [] resetsonglistselectionbutton = ttk.Button( songselectionandviewingframe, text="RESET SELECTION|SELECTED: 0", command=lambda: self._addchange("resetselectedsongs")) songlist.grid(row=0, column=0, columnspan=2) songlistscrollbar.grid(row=0, column=2, sticky='wns') playlistsongslist.grid(row=0, column=0, columnspan=2) playlistsongslistscrollbar.grid(row=0, column=2, sticky='wns') songsearchentry.grid(row=1, column=0, sticky='ews') resetsonglistselectionbutton.grid(row=2, column=0, sticky='nw') songlist['yscrollcommand'] = songlistscrollbar.set playlistsongslist['yscrollcommand'] = playlistsongslistscrollbar.set songlistnotebook.add(songlistframe, text="Song list") songlistnotebook.add(playlistsongsframe, text="empty") # BOTTOM LEFT LOGO btecifyiconimage = tk.PhotoImage(file="assets/btecify64.png") btecifyiconlabel = ttk.Label(primaryframe, image=btecifyiconimage) btecifyiconlabel.grid(column=0, row=2, sticky='ws') # PLAYLIST SELECT playlistselectframe = ttk.LabelFrame(primaryframe, text="Playlist select", relief='groove', padding=3) playlistselectframe.grid(row=2, column=3, sticky='wn') playlistselectcombobox = ttk.Combobox( playlistselectframe, values=self.playlistnames, textvariable=self.selectedplaylist, width=26, state='readonly') self.selectedplaylist.trace_add( mode="write", callback=self._playlistcomboboxvalueupdated) playlistselectcombobox.set(playlists[0].name) playlistselectcombobox.grid(sticky='ewn') playlistselectbutton = ttk.Button(playlistselectframe, text="SWITCH TO PLAYLIST", command=self._chooseplaylist) playlistselectbutton.grid(row=1, sticky='s') # PLAYER BUTTONS bottommiddleframe = ttk.LabelFrame(primaryframe, text="Player controls", relief='groove', padding=5) bottommiddleframe.grid(column=2, row=1, sticky='wnse') pausebutton = ttk.Button(bottommiddleframe, text="PAUSE", command=self._pause) pausebutton.grid(row=0, column=0, columnspan=3, sticky='ew') skipbutton = ttk.Button(bottommiddleframe, text="SKIP", command=self._skip) skipbutton.grid(row=1, sticky='w') loopbutton = ttk.Button(bottommiddleframe, text="LOOP: DISABLED", command=lambda: self._setoutput("loop")) loopbutton.grid(row=1, column=1, padx=120) removesongbutton = ttk.Button(bottommiddleframe, text="REMOVE SONG", command=self._playerremovesongbutton) removesongbutton.grid(row=1, column=2, sticky='e') volumeslider = ttk.LabeledScale(bottommiddleframe, from_=0, to=100, variable=self.volume, compound='bottom') volumeslider.scale.set(defaults['volume']) volumeslider.scale.configure(command=self._volchange) volumeslider.label.update() volumeslider.grid(row=2, columnspan=3, sticky='ew') bottommiddleframe.grid_rowconfigure((0, 1, 2), weight=1) bottommiddleframe.grid_columnconfigure((0, 1), weight=1) # EXTRA SONG INFORMATION extrasonginfoframe = ttk.Labelframe(primaryframe, text="Song Info", relief="sunken", padding=3) extrasonginfoframe.grid(row=2, column=1, columnspan=2, sticky="nesw") extrasonginfoname = ttk.Label(extrasonginfoframe, text="NO SONG", justify=tk.LEFT, anchor="w") extrasonginfoname.grid(row=0, column=0, sticky="nesw") extrasonginfoplaylistlabelframe = ttk.Labelframe(extrasonginfoframe, text="In Playlists", relief="groove", padding=5) extrasonginfoplaylistlabelframe.grid(row=1, column=0, sticky="w") extrasonginfoplaylists = mylistbox( extrasonginfoplaylistlabelframe, height=5, selectmode="browse", listvariable=self.extrainfoplaylistsvar, exportselection=False) extrasonginfoplaylists.grid(row=0, column=0, sticky="") extrasonginfoplaylistsresetbutton = ttk.Button( extrasonginfoplaylistlabelframe, text="RESET", command=lambda: extrasonginfoplaylists.selection_clear( 0, 100000) or self.extraplaylistselection.clear( )) # Executes two statements in one lambda. extrasonginfoplaylistsresetbutton.grid(row=1, column=0, sticky='nesw') extrasonginfobuttonsframe = ttk.Frame(extrasonginfoplaylistlabelframe, padding=2) extrasonginfobuttonsframe.grid(row=0, column=1, sticky='nesw') extrasonginforemovebutton = ttk.Button( extrasonginfobuttonsframe, text="REMOVE SONG FROM PLAYLISTS", command=self._extrasonginforemovebuttonfunc) extrasonginforemovebutton.grid(row=0, column=0, sticky='') extrasonginfoopensong = ttk.Button( extrasonginfobuttonsframe, text="OPEN IN YOUTUBE", command=lambda: self._setoutput("openinyoutube", [ *self.completeselectedsongs ] or [self._getselectedsong()])) extrasonginfoopensong.grid(row=1, column=0, sticky='') def _updatebasedonvalues(): extrasongselectedplaylistvalues = self._getextrasongselectedplaylists( extrasonginfoplaylists) if self.changes[ 'songinfo'] or extrasongselectedplaylistvalues != self.extraplaylistselection: if self.playingsong is not None: self.progressbarvar.set(value=self.progressbar) playlistofthissong = self.playlistwhichsongisfrom if playlistofthissong is None: playlistofthissong = "Played manually" removesongbutton.configure(state='disabled') else: playlistofthissong = playlistofthissong.name removesongbutton.configure(state='active') songinfo['text'] = ( f"{playlistofthissong}\n{self.playingsong.name[:len(PLAYINGINFOPLACEHOLDER)]}\n{self.playingsong.author}\n{self.playingsong.duration}\n" + PLAYINGINFOPLACEHOLDER) if self.paused: songdesc['text'] = "PAUSED" pausebutton['text'] = "PLAY" else: songdesc['text'] = "PLAYING" pausebutton['text'] = "PAUSE" if self.loop: loopbutton['text'] = "LOOP: ENABLED" else: loopbutton['text'] = "LOOP: DISABLED" targetsong = self._getselectedsong() if targetsong is not None: extrasonginfoname['text'] = targetsong.name[:( queuelist.cget("width") // 3) + len(PLAYINGINFOPLACEHOLDER)] self.playlistswithtargetsong = list( filter(lambda a: targetsong in a.getsongs(), self.playlists)) self.extrainfoplaylistsvar.set( [i.name for i in self.playlistswithtargetsong]) extrasonginfoplaylists.selection_clear(0, 1000000) self.extraplaylistselection.extend([ i for i in extrasongselectedplaylistvalues if i not in self.extraplaylistselection ]) for i, v in enumerate(self.extraplaylistselection): if v in self.playlistswithtargetsong: extrasonginfoplaylists.selection_set( self.playlistswithtargetsong.index(v)) else: self.extraplaylistselection.remove(v) else: extrasonginfoname['text'] = "NO SONG" self.extrainfoplaylistsvar.set([]) extrasonginfoplaylists.selection_clear(0, 10000) self._addchange('songinfo', False) if self.changes['resetselectedsongs']: songlist.selection_clear(0, 100000000) queuelist.selection_clear(0, 100000) playlistsongslist.selection_clear(0, 100000) self.completeselectedsongs = [] resetsonglistselectionbutton.configure( text=f"RESET SELECTION | SELECTED: 0") self._addchange('songinfo') self._addchange('resetselectedsongs', False) currentlyselectedsonglistvalues = self._getselectedvalues( songlist, self.displaysonglist) currentlyselectedqueuevalues = self._getselectedvalues( queuelist, self.songqueue) currentlyselectedplaylistsongsvalues = self._getselectedvalues( playlistsongslist, self.displayplaylistsongs) displayablesongsinsonglist = set([ i for i in self.displaysonglistnew if i in self.completeselectedsongs ]) displayablesongsinqueuelist = set([ i for i in self.songqueuenew if i in self.completeselectedsongs ]) displayablesongsinplaylistsongslist = set([ i for i in self.displayplaylistsongsnew if i in self.completeselectedsongs ]) if self.changes['songlist'] or ( currentlyselectedsonglistvalues != displayablesongsinsonglist ) or (displayablesongsinqueuelist != currentlyselectedqueuevalues ) or (displayablesongsinplaylistsongslist != currentlyselectedplaylistsongsvalues): if self.changes['songlist']: self._songlistsearchchanged() self.songlistvar.set( value=[i.name for i in self.displaysonglistnew]) self.playlistsongsvar.set( value=[i.name for i in self.displayplaylistsongsnew]) self.displaysonglist = self.displaysonglistnew self.displayplaylistsongs = self.displayplaylistsongsnew songlist.selection_clear(0, 1000000) queuelist.selection_clear(0, 1000000) playlistsongslist.selection_clear(0, 10000) self._addchange('songinfo') self._addchange('songlist', False) else: self.completeselectedsongs.extend([ i for i in currentlyselectedsonglistvalues if i not in self.completeselectedsongs ]) self.completeselectedsongs.extend([ i for i in currentlyselectedqueuevalues if i not in self.completeselectedsongs ]) self.completeselectedsongs.extend([ i for i in currentlyselectedplaylistsongsvalues if i not in self.completeselectedsongs ]) for song in self.completeselectedsongs: if song: if song in self.displaysonglistnew: songlist.selection_set( self.displaysonglistnew.index(song)) if song in self.songqueuenew: queuelist.selection_set( self.songqueuenew.index(song)) if song in self.displayplaylistsongsnew: playlistsongslist.selection_set( self.displayplaylistsongsnew.index(song)) self._addchange('songinfo') resetsonglistselectionbutton.configure( text= f"RESET SELECTION | SELECTED: {len(self.completeselectedsongs)}" ) if self.changes['songqueue']: queuelist.selection_clear(0, 100000) self.songqueue = self.songqueuenew self.songqueuevar.set(value=[ f"{i+1:>3}: {v.name}" for i, v in enumerate(self.songqueue) ]) self._addchange('songqueue', False) if self.changes['playlistoptions']: self._generateplaylistnames() playlistselectcombobox['values'] = self.playlistnames self._addchange('songinfo') self._addchange('playlistoptions', False) if self.changes['progressbar']: self.progressbarvar.set(value=self.progressbar) self._addchange('progressbar', False) if self.changes['playlistcomboboxupdate']: playlist = self._getselectedplaylist() label = "Playlist: " if playlist: label += playlist.name else: label += "None" menubar.entryconfigure(menubarplaylistlabelindex, label=label) songlistnotebook.tab(1, text=self._getselectedplaylist().name) self._addchange("songlist") self._addchange('playlistcomboboxupdate', False) if self.changes['updatelogs']: logstoadd = self.newlogs[len(self.logs):] consolewindowtext['state'] = 'normal' for log in logstoadd: logstr = "" timevalues = log[0] logstr += f"{timevalues.tm_hour:0>2}:{timevalues.tm_min:0>2}:{timevalues.tm_sec:0>2}: " for obj in log[1]: objstring = str(obj).replace("\n", "\n\t ") logstr += objstring + " " consolewindowtext.insert('end', logstr + "\n\n") consolewindowtext['state'] = 'disabled' self.logs = self.newlogs self._addchange('updatelogs', False) if self.changes['seeking']: val = self.seek.get() if 0 < val < 1: self._setoutput('seek', self.seek.get()) if self.changes['keybinds']: for kbset in keybindlist: for j in self.keybinds: if j[0] == kbset[0]: kbset[1].set(j[1]) self._addchange("keybinds", False) root.after(10, _updatebasedonvalues) _updatebasedonvalues() G.musicgui = self root.mainloop()