diff --git a/CREDITS b/CREDITS index 4425a0a..5fe8ca3 100644 --- a/CREDITS +++ b/CREDITS @@ -2,6 +2,8 @@ Some icons used in this project are from Font Awesome Free by Fonticons, Inc. Licensed under the Creative Commons Attribution 4.0 International License (CC BY 4.0): https://creativecommons.org/licenses/by/4.0/ Icons were modified (repainted to white and exported to PNG). +The Roboto Black font used in this project is licensed under the Open Font License. Read assets/fonts/OFL.txt for more information. + Huge Thanks to Python for being the programming language used in this application. https://www.python.org/ diff --git a/assets/fonts/OFL.txt b/assets/fonts/OFL.txt new file mode 100644 index 0000000..a417551 --- /dev/null +++ b/assets/fonts/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2011 The Roboto Project Authors (https://github.com/googlefonts/roboto-classic) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/assets/fonts/ProtestStrike-Regular.ttf b/assets/fonts/ProtestStrike-Regular.ttf deleted file mode 100644 index 3a88f0c..0000000 Binary files a/assets/fonts/ProtestStrike-Regular.ttf and /dev/null differ diff --git a/assets/fonts/Roboto-Black.ttf b/assets/fonts/Roboto-Black.ttf new file mode 100644 index 0000000..d51221a Binary files /dev/null and b/assets/fonts/Roboto-Black.ttf differ diff --git a/assets/graphics/music.png b/assets/graphics/music.png index df70310..086a2c6 100644 Binary files a/assets/graphics/music.png and b/assets/graphics/music.png differ diff --git a/menus/add_music.py b/menus/add_music.py index 1682beb..f6256d6 100644 --- a/menus/add_music.py +++ b/menus/add_music.py @@ -1,6 +1,6 @@ import arcade, arcade.gui, os, json -from utils.constants import button_style, dropdown_style +from utils.constants import button_style from utils.preload import button_texture, button_hovered_texture from utils.utils import UIFocusTextureButton @@ -32,10 +32,10 @@ class AddMusic(arcade.gui.UIView): self.anchor = self.add_widget(UIFocusGroup(size_hint=(1, 1))) self.box = self.anchor.add(arcade.gui.UIBoxLayout(space_between=10), anchor_x="center", anchor_y="center") - self.playlist_label = self.box.add(arcade.gui.UILabel(text="Playlist", font_name="Protest Strike", font_size=32)) - self.playlist_option = self.box.add(arcade.gui.UIDropdown(default=list(self.playlists.keys())[0], options=list(self.playlists.keys()), width=self.window.width / 2, height=self.window.height / 15, primary_style=dropdown_style, dropdown_style=dropdown_style, active_style=dropdown_style)) - self.music_label = self.box.add(arcade.gui.UILabel(text="Music File Path", font_name="Protest Strike", font_size=32)) - self.add_music_input = self.box.add(arcade.gui.UIInputText(font_name="Protest Strike", font_size=32, width=self.window.width / 2, height=self.window.height / 10)) + self.playlist_label = self.box.add(arcade.gui.UILabel(text="Playlist", font_name="Roboto", font_size=32)) + self.playlist_option = self.box.add(arcade.gui.UIDropdown(default=list(self.playlists.keys())[0], options=list(self.playlists.keys()), width=self.window.width / 2, height=self.window.height / 15, primary_style=button_style, dropdown_style=button_style, active_style=button_style)) + self.music_label = self.box.add(arcade.gui.UILabel(text="Music File Path", font_name="Roboto", font_size=32)) + self.add_music_input = self.box.add(arcade.gui.UIInputText(font_name="Roboto", font_size=32, width=self.window.width / 2, height=self.window.height / 10)) self.add_music_button = self.box.add(UIFocusTextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text='Add Music', style=button_style, width=self.window.width / 2, height=self.window.height / 10)) self.add_music_button.on_click = lambda event: self.add_music() diff --git a/menus/downloader.py b/menus/downloader.py index 54ffb36..15f82b0 100644 --- a/menus/downloader.py +++ b/menus/downloader.py @@ -6,7 +6,7 @@ import arcade, arcade.gui, os, json, threading, subprocess from arcade.gui.experimental.focus import UIFocusGroup from utils.utils import UIFocusTextureButton, ensure_yt_dlp -from utils.constants import button_style, dropdown_style, yt_dlp_parameters +from utils.constants import button_style from utils.preload import button_texture, button_hovered_texture class Downloader(arcade.gui.UIView): @@ -37,12 +37,12 @@ class Downloader(arcade.gui.UIView): self.download_box = self.anchor.add(arcade.gui.UIBoxLayout(space_between=10), anchor_x="center", anchor_y="center") - self.url_name_label = self.download_box.add(arcade.gui.UILabel(text="URL or Name:", font_name="Protest Strike", font_size=36)) - self.url_name_input = self.download_box.add(arcade.gui.UIInputText(font_name="Protest Strike", width=self.window.width / 2, height=self.window.height / 15, font_size=36)) + self.url_name_label = self.download_box.add(arcade.gui.UILabel(text="URL or Name:", font_name="Roboto", font_size=36)) + self.url_name_input = self.download_box.add(arcade.gui.UIInputText(font_name="Roboto", width=self.window.width / 2, height=self.window.height / 15, font_size=36)) self.url_name_input.activate() - self.tab_label = self.download_box.add(arcade.gui.UILabel(text="Path:", font_name="Protest Strike", font_size=36)) - self.tab_selector = self.download_box.add(arcade.gui.UIDropdown(default=self.tab_options[0], options=self.tab_options, width=self.window.width / 2, height=self.window.height / 15, primary_style=dropdown_style, dropdown_style=dropdown_style, active_style=dropdown_style)) + self.tab_label = self.download_box.add(arcade.gui.UILabel(text="Path:", font_name="Roboto", font_size=36)) + self.tab_selector = self.download_box.add(arcade.gui.UIDropdown(default=self.tab_options[0], options=self.tab_options, width=self.window.width / 2, height=self.window.height / 15, primary_style=button_style, dropdown_style=button_style, active_style=button_style)) self.status_label = self.download_box.add(arcade.gui.UILabel(text="No errors.", font_size=16, text_color=arcade.color.LIGHT_GREEN)) diff --git a/menus/main.py b/menus/main.py index 5aa335f..5a15e34 100644 --- a/menus/main.py +++ b/menus/main.py @@ -3,8 +3,9 @@ import arcade, arcade.gui, pyglet from utils.preload import * from utils.constants import button_style, slider_style, audio_extensions, discord_presence_id -from utils.utils import FakePyPresence, UIFocusTextureButton, extract_metadata, truncate_end +from utils.utils import FakePyPresence, UIFocusTextureButton, Card, extract_metadata, get_audio_thumbnail_texture, truncate_end +from math import ceil from thefuzz import process, fuzz from pydub import AudioSegment @@ -56,6 +57,7 @@ class Main(arcade.gui.UIView): self.tab_options = self.settings_dict.get("tab_options", ["~/Music", "~/Downloads"]) self.tab_content = {} self.playlist_content = {} + self.thumbnails = {} self.tab_buttons = {} self.music_buttons = {} @@ -76,21 +78,26 @@ class Main(arcade.gui.UIView): def on_show_view(self): super().on_show_view() + self.load_content() + + self.create_ui() + + def create_ui(self): self.anchor = self.add_widget(arcade.gui.UIAnchorLayout(size_hint=(1, 1))) self.ui_box = self.anchor.add(arcade.gui.UIBoxLayout(size_hint=(1, 1), space_between=10)) # Tabs + self.tab_box = self.ui_box.add(arcade.gui.UIBoxLayout(size_hint=(0.95, 0.1), space_between=10, vertical=False)) + self.load_tabs() - self.load_content() if self.current_mode == "playlist" and not self.current_playlist: self.current_playlist = list(self.playlist_content.keys())[0] if self.playlist_content else None - self.load_tabs() # Scrollable Sounds self.scroll_box = self.ui_box.add(arcade.gui.UIBoxLayout(size_hint=(0.95, 0.8), space_between=15, vertical=False)) - self.scroll_area = UIScrollArea(size_hint=(0.9, 1)) # center on screen + self.scroll_area = UIScrollArea(size_hint=(0.95, 1)) # center on screen self.scroll_area.scroll_speed = -50 self.scroll_box.add(self.scroll_area) @@ -98,8 +105,8 @@ class Main(arcade.gui.UIView): self.scrollbar.size_hint = (0.02, 1) self.scroll_box.add(self.scrollbar) - self.music_box = arcade.gui.UIBoxLayout(space_between=2) - self.scroll_area.add(self.music_box) + self.music_grid = arcade.gui.UIGridLayout(horizontal_spacing=30, vertical_spacing=30, row_count=100, column_count=5) + self.scroll_area.add(self.music_grid) # Utility @@ -125,8 +132,8 @@ class Main(arcade.gui.UIView): # Controls self.control_box = self.ui_box.add(arcade.gui.UIBoxLayout(size_hint=(0.95, 0.1), space_between=10, vertical=False)) - self.current_music_label = self.control_box.add(arcade.gui.UILabel(text=truncate_end(self.current_music_name, int(self.window.width / 30)) if self.current_music_name else "No songs playing", font_name="Protest Strike", font_size=16)) - self.time_label = self.control_box.add(arcade.gui.UILabel(text="00:00", font_name="Protest Strike", font_size=16)) + self.current_music_label = self.control_box.add(arcade.gui.UILabel(text=truncate_end(self.current_music_name, int(self.window.width / 30)) if self.current_music_name else "No songs playing", font_name="Roboto", font_size=16)) + self.time_label = self.control_box.add(arcade.gui.UILabel(text="00:00", font_name="Roboto", font_size=16)) self.progressbar = self.control_box.add(arcade.gui.UISlider(style=slider_style, width=self.window.width / 3, height=35)) self.progressbar.on_change = self.on_progress_change @@ -147,7 +154,7 @@ class Main(arcade.gui.UIView): self.progressbar.max_value = self.current_length self.volume = int(self.current_music_player.volume * 100) - self.volume_label = self.control_box.add(arcade.gui.UILabel(text=f"{self.volume}%", font_name="Protest Strike", font_size=16)) + self.volume_label = self.control_box.add(arcade.gui.UILabel(text=f"{self.volume}%", font_name="Roboto", font_size=16)) self.volume_slider = self.control_box.add(arcade.gui.UISlider(style=slider_style, width=self.window.width / 10, height=35, value=self.volume, max_value=100)) self.volume_slider.on_change = self.on_volume_slider_change @@ -200,6 +207,8 @@ class Main(arcade.gui.UIView): self.highest_score_file = "" self.search_term = "" + self.load_tabs() + self.reload() def skip_sound(self): @@ -237,7 +246,7 @@ class Main(arcade.gui.UIView): self.update_buttons() def show_content(self, tab): - self.music_box.clear() + self.music_grid.clear() self.music_buttons.clear() if self.current_mode == "files": @@ -245,16 +254,21 @@ class Main(arcade.gui.UIView): if not self.search_term == "": matches = process.extract(self.search_term, self.tab_content[self.current_tab], limit=5, processor=lambda text: text.lower(), scorer=fuzz.partial_token_sort_ratio) self.highest_score_file = f"{self.current_tab}/{matches[0][0]}" - for match in matches: + for n, match in enumerate(matches): music_filename = match[0] - self.music_buttons[music_filename] = self.music_box.add(arcade.gui.UITextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text=music_filename, style=button_style, width=self.window.width * 0.85, height=self.window.height / 30)) - self.music_buttons[music_filename].on_click = lambda event, tab=tab, music_filename=music_filename: self.queue.append(f"{tab}/{music_filename}") - + self.music_buttons[music_filename] = self.music_grid.add(Card(card_texture=self.thumbnails[f"{tab}/{music_filename}"], font_name="Roboto", font_size=13, text=music_filename, width=self.window.width / 6, height=self.window.height / 5), row=0, column=n) + self.music_buttons[music_filename].button.on_click = lambda event, tab=tab, music_filename=music_filename: self.queue.append(f"{tab}/{music_filename}") else: + self.music_grid.row_count = ceil(len(self.tab_content[tab]) / 5) + self.music_grid._update_size_hints() + self.highest_score_file = "" - for music_filename in self.tab_content[tab]: - self.music_buttons[music_filename] = self.music_box.add(arcade.gui.UITextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text=music_filename, style=button_style, width=self.window.width * 0.85, height=self.window.height / 30)) - self.music_buttons[music_filename].on_click = lambda event, tab=tab, music_filename=music_filename: self.queue.append(f"{tab}/{music_filename}") + for n, music_filename in enumerate(self.tab_content[tab]): + row = n // 5 + col = n % 5 + + self.music_buttons[music_filename] = self.music_grid.add(Card(card_texture=self.thumbnails[f"{tab}/{music_filename}"], font_name="Roboto", font_size=13, text=music_filename, width=self.window.width / 6, height=self.window.height / 5), row=row, column=col) + self.music_buttons[music_filename].button.on_click = lambda event, tab=tab, music_filename=music_filename: self.queue.append(f"{tab}/{music_filename}") elif self.current_mode == "playlist": self.current_playlist = tab @@ -263,45 +277,57 @@ class Main(arcade.gui.UIView): if not self.search_term == "": matches = process.extract(self.search_term, self.playlist_content[tab], limit=5, processor=lambda text: text.lower(), scorer=fuzz.partial_token_sort_ratio) self.highest_score_file = matches[0][0] - for match in matches: + for n, match in enumerate(matches): music_filename = match[0] - self.music_buttons[music_filename] = self.music_box.add(arcade.gui.UITextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text=music_filename, style=button_style, width=self.window.width * 0.85, height=self.window.height / 30)) - self.music_buttons[music_filename].on_click = lambda event, tab=tab, music_filename=music_filename: self.queue.append(music_filename) + self.music_buttons[music_filename] = self.music_grid.add(Card(card_texture=self.thumbnails[music_filename], font_name="Roboto", font_size=13, text=music_filename, width=self.window.width / 6, height=self.window.height / 5), row=0, column=n) + self.music_buttons[music_filename].button.on_click = lambda event, tab=tab, music_filename=music_filename: self.queue.append(music_filename) else: - self.highest_score_file = "" - for music_filename in self.playlist_content[tab]: - self.music_buttons[music_filename] = self.music_box.add(arcade.gui.UITextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text=music_filename, style=button_style, width=self.window.width * 0.85, height=self.window.height / 30)) - self.music_buttons[music_filename].on_click = lambda event, tab=tab, music_filename=music_filename: self.queue.append(music_filename) + self.music_grid.row_count = ceil((len(self.playlist_content[tab]) + 1) / 5) + self.music_grid._update_size_hints() - self.music_buttons["add_music"] = self.music_box.add(arcade.gui.UITextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text="Add Music", style=button_style, width=self.window.width * 0.85, height=self.window.height / 30)) - self.music_buttons["add_music"].on_click = lambda event: self.add_music() + self.highest_score_file = "" + + for n, music_filename in enumerate(self.playlist_content[tab]): + row = n // 5 + col = n % 5 + + self.music_buttons[music_filename] = self.music_grid.add(Card(card_texture=self.thumbnails[music_filename], font_name="Roboto", font_size=13, text=music_filename, width=self.window.width / 6, height=self.window.height / 5), row=row, column=col) + self.music_buttons[music_filename].button.on_click = lambda event, tab=tab, music_filename=music_filename: self.queue.append(music_filename) + row = (n + 1) // 5 + col = (n + 1) % 5 + + self.music_buttons["add_music"] = self.music_grid.add(Card(card_texture=music_icon, font_name="Roboto", font_size=13, text="Add Music", width=self.window.width / 6, height=self.window.height / 5), row=row, column=col) + self.music_buttons["add_music"].button.on_click = lambda event: self.add_music() self.update_buttons() def load_content(self): - self.tab_content.clear() - self.playlist_content.clear() - for tab in self.tab_options: self.tab_content[os.path.expanduser(tab)] = [] for filename in os.listdir(os.path.expanduser(tab)): if filename.split(".")[-1] in audio_extensions: + if f"{os.path.expanduser(tab)}/{filename}" not in self.thumbnails: + self.thumbnails[f"{os.path.expanduser(tab)}/{filename}"] = get_audio_thumbnail_texture(f"{os.path.expanduser(tab)}/{filename}", self.window.size) self.tab_content[os.path.expanduser(tab)].append(filename) - for playlist, content in self.settings_dict.get("playlists", {}).items(): - self.playlist_content[playlist] = content + for content in self.settings_dict.get("playlists", {}).values(): + for file in content: + if file not in self.thumbnails: + self.thumbnails[file] = get_audio_thumbnail_texture(file, self.window.size) + + self.playlist_content = self.settings_dict.get("playlists", {}) def load_tabs(self): - self.tab_box = self.ui_box.add(arcade.gui.UIBoxLayout(size_hint=(0.95, 0.1), space_between=10, vertical=False)) + self.tab_box.clear() if self.current_mode == "files": for tab in self.tab_options: - self.tab_buttons[os.path.expanduser(tab)] = self.tab_box.add(UIFocusTextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text=os.path.basename(os.path.normpath(os.path.expanduser(tab))), style=button_style, width=self.window.width / 10, height=self.window.height / 15)) + self.tab_buttons[os.path.expanduser(tab)] = self.tab_box.add(arcade.gui.UITextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text=os.path.basename(os.path.normpath(os.path.expanduser(tab))), style=button_style, width=self.window.width / 10, height=self.window.height / 15)) self.tab_buttons[os.path.expanduser(tab)].on_click = lambda event, tab=os.path.expanduser(tab): self.show_content(tab) elif self.current_mode == "playlist": for playlist in self.playlist_content: - self.tab_buttons[playlist] = self.tab_box.add(UIFocusTextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text=playlist, style=button_style, width=self.window.width / 10, height=self.window.height / 15)) + self.tab_buttons[playlist] = self.tab_box.add(arcade.gui.UITextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text=playlist, style=button_style, width=self.window.width / 10, height=self.window.height / 15)) self.tab_buttons[playlist].on_click = lambda event, playlist=playlist: self.show_content(playlist) def on_progress_change(self, event): @@ -388,7 +414,7 @@ class Main(arcade.gui.UIView): elif symbol == arcade.key.BACKSPACE: self.search_term = self.search_term[:-1] if self.current_mode == "files": - self.show_content(self.current_tab) + self.show_content(os.path.expanduser(self.current_tab)) elif self.current_mode == "playlist": self.show_content(self.current_playlist) elif symbol == arcade.key.ENTER and self.highest_score_file: @@ -396,14 +422,14 @@ class Main(arcade.gui.UIView): self.highest_score_file = "" self.search_term = "" if self.current_mode == "files": - self.show_content(self.current_tab) + self.show_content(os.path.expanduser(self.current_tab)) elif self.current_mode == "playlist": self.show_content(self.current_playlist) elif symbol == arcade.key.ESCAPE: self.highest_score_file = "" self.search_term = "" if self.current_mode == "files": - self.show_content(self.current_tab) + self.show_content(os.path.expanduser(self.current_tab)) elif self.current_mode == "playlist": self.show_content(self.current_playlist) @@ -414,7 +440,7 @@ class Main(arcade.gui.UIView): self.search_term += text if self.current_mode == "files": - self.show_content(self.current_tab) + self.show_content(os.path.expanduser(self.current_tab)) elif self.current_mode == "playlist": self.show_content(self.current_playlist) @@ -443,8 +469,13 @@ class Main(arcade.gui.UIView): self.window.show_view(Downloader(self.pypresence_client, self.current_mode, self.current_music_name, self.current_length, self.current_music_player, self.queue, self.loaded_sounds, self.shuffle)) def reload(self): - self.ui.clear() - self.on_show_view() + self.load_content() + + if self.current_mode == "files": + self.show_content(self.current_tab) + elif self.current_mode == "playlist": + self.show_content(self.current_playlist) + self.update_buttons() def update_presence(self, _): diff --git a/menus/new_tab.py b/menus/new_tab.py index 9d64634..38f5ac2 100644 --- a/menus/new_tab.py +++ b/menus/new_tab.py @@ -2,7 +2,6 @@ import arcade, arcade.gui, os, json from utils.constants import button_style from utils.preload import button_texture, button_hovered_texture -from utils.utils import UIFocusTextureButton from arcade.gui.experimental.focus import UIFocusGroup @@ -34,17 +33,17 @@ class NewTab(arcade.gui.UIView): self.box = self.anchor.add(arcade.gui.UIBoxLayout(space_between=10), anchor_x="center", anchor_y="center") if self.current_mode == "files": - self.new_tab_label = self.box.add(arcade.gui.UILabel(text="New Tab Path:", font_name="Protest Strike", font_size=32)) - self.new_tab_input = self.box.add(arcade.gui.UIInputText(font_name="Protest Strike", font_size=32, width=self.window.width / 2, height=self.window.height / 10)) - self.new_tab_button = self.box.add(UIFocusTextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text='Add new tab', style=button_style, width=self.window.width / 2, height=self.window.height / 10)) + self.new_tab_label = self.box.add(arcade.gui.UILabel(text="New Tab Path:", font_name="Roboto", font_size=32)) + self.new_tab_input = self.box.add(arcade.gui.UIInputText(font_name="Roboto", font_size=32, width=self.window.width / 2, height=self.window.height / 10)) + self.new_tab_button = self.box.add(arcade.gui.UITextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text='Add new tab', style=button_style, width=self.window.width / 2, height=self.window.height / 10)) self.new_tab_button.on_click = lambda event: self.add_tab() elif self.current_mode == "playlist": - self.new_tab_label = self.box.add(arcade.gui.UILabel(text="New Playlist Name:", font_name="Protest Strike", font_size=32)) - self.new_tab_input = self.box.add(arcade.gui.UIInputText(font_name="Protest Strike", font_size=32, width=self.window.width / 2, height=self.window.height / 10)) - self.new_tab_button = self.box.add(UIFocusTextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text='Add new Playlist', style=button_style, width=self.window.width / 2, height=self.window.height / 10)) + self.new_tab_label = self.box.add(arcade.gui.UILabel(text="New Playlist Name:", font_name="Roboto", font_size=32)) + self.new_tab_input = self.box.add(arcade.gui.UIInputText(font_name="Roboto", font_size=32, width=self.window.width / 2, height=self.window.height / 10)) + self.new_tab_button = self.box.add(arcade.gui.UITextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text='Add new Playlist', style=button_style, width=self.window.width / 2, height=self.window.height / 10)) self.new_tab_button.on_click = lambda event: self.add_tab() - self.back_button = self.anchor.add(UIFocusTextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text='<--', style=button_style, width=100, height=50), anchor_x="left", anchor_y="top", align_x=5, align_y=-5) + self.back_button = self.anchor.add(arcade.gui.UITextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text='<--', style=button_style, width=100, height=50), anchor_x="left", anchor_y="top", align_x=5, align_y=-5) self.back_button.on_click = lambda event: self.main_exit() self.anchor.detect_focusable_widgets() diff --git a/menus/settings.py b/menus/settings.py index 5f62cd6..8918907 100644 --- a/menus/settings.py +++ b/menus/settings.py @@ -1,9 +1,10 @@ +from arcade.gui.widgets.buttons import UITextureButton import copy, pypresence, json import arcade, arcade.gui -from utils.constants import button_style, dropdown_style, slider_style, settings, discord_presence_id, settings_start_category -from utils.utils import FakePyPresence, UIFocusTextureButton +from utils.constants import button_style, slider_style, settings, discord_presence_id, settings_start_category +from utils.utils import FakePyPresence from utils.preload import button_texture, button_hovered_texture from arcade.gui.experimental.focus import UIFocusGroup @@ -55,7 +56,7 @@ class Settings(arcade.gui.UIView): self.ui.push_handlers(self) - self.back_button = UIFocusTextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text='<--', style=button_style, width=100, height=50) + self.back_button = arcade.gui.UITextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text='<--', style=button_style, width=100, height=50) self.back_button.on_click = lambda event: self.main_exit() self.top_box.add(self.back_button) @@ -67,7 +68,7 @@ class Settings(arcade.gui.UIView): def display_categories(self): for category in settings: - category_button = UIFocusTextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text=category, style=button_style, width=self.window.width / 10, height=50) + category_button = arcade.gui.UITextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text=category, style=button_style, width=self.window.width / 10, height=50) if not category == "Credits": category_button.on_click = lambda event, category=category: self.display_category(category) @@ -93,27 +94,27 @@ class Settings(arcade.gui.UIView): self.value_layout.clear() for setting in settings[category]: - label = arcade.gui.UILabel(text=setting, font_name="Protest Strike", font_size=28, text_color=arcade.color.WHITE ) + label = arcade.gui.UILabel(text=setting, font_name="Roboto", font_size=28, text_color=arcade.color.WHITE ) self.key_layout.add(label) setting_dict = settings[category][setting] if setting_dict['type'] == "option": - dropdown = arcade.gui.UIDropdown(options=setting_dict['options'], width=200, height=50, default=self.settings_dict.get(setting_dict["config_key"], setting_dict["options"][0]), active_style=dropdown_style, dropdown_style=dropdown_style, primary_style=dropdown_style) + dropdown = arcade.gui.UIDropdown(options=setting_dict['options'], width=200, height=50, default=self.settings_dict.get(setting_dict["config_key"], setting_dict["options"][0]), active_style=button_style, dropdown_style=button_style, primary_style=button_style) dropdown.on_change = lambda _, setting=setting, dropdown=dropdown: self.update(setting, dropdown.value, "option") self.value_layout.add(dropdown) elif setting_dict['type'] == "bool": button_layout = self.value_layout.add(arcade.gui.UIBoxLayout(space_between=50, vertical=False)) - on_radiobutton = UIFocusTextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text="ON", style=button_style, width=150, height=50) + on_radiobutton = arcade.gui.UITextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text="ON", style=button_style, width=150, height=50) self.on_radiobuttons[setting] = on_radiobutton - on_radiobutton.on_click = lambda _, setting=setting: self.update(setting, True, "bool") + on_radiobutton.on_click = lambda event, setting=setting: self.update(setting, True, "bool") button_layout.add(on_radiobutton) - off_radiobutton = UIFocusTextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text="OFF", style=button_style, width=150, height=50) + off_radiobutton = arcade.gui.UITextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text="OFF", style=button_style, width=150, height=50) self.off_radiobuttons[setting] = off_radiobutton - off_radiobutton.on_click = lambda _, setting=setting: self.update(setting, False, "bool") + off_radiobutton.on_click = lambda event, setting=setting: self.update(setting, False, "bool") button_layout.add(off_radiobutton) if self.settings_dict.get(setting_dict["config_key"], setting_dict["default"]): @@ -142,7 +143,7 @@ class Settings(arcade.gui.UIView): self.sliders[setting] = slider self.value_layout.add(slider) - self.apply_button = UIFocusTextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text='Apply', style=button_style, width=200, height=100) + self.apply_button = arcade.gui.UITextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text='Apply', style=button_style, width=200, height=100) self.apply_button.on_click = lambda event: self.apply_settings() self.anchor.add(self.apply_button, anchor_x="right", anchor_y="bottom", align_x=-10, align_y=10) @@ -205,7 +206,7 @@ class Settings(arcade.gui.UIView): self.create_layouts() - self.back_button = UIFocusTextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text='<--', style=button_style, width=100, height=50) + self.back_button = arcade.gui.UITextureButton(texture=button_texture, texture_hovered=button_hovered_texture, text='<--', style=button_style, width=100, height=50) self.back_button.on_click = lambda event: self.main_exit() self.top_box.add(self.back_button) @@ -275,7 +276,7 @@ class Settings(arcade.gui.UIView): else: font_size = 12 - self.credits_label = arcade.gui.UILabel(text=text, text_color=arcade.color.WHITE, font_name="Protest Strike", font_size=font_size, align="center", multiline=True) + self.credits_label = arcade.gui.UILabel(text=text, text_color=arcade.color.WHITE, font_name="Roboto", font_size=font_size, align="center", multiline=True) self.key_layout.add(self.credits_label) diff --git a/run.py b/run.py index d1151bc..e40c68d 100644 --- a/run.py +++ b/run.py @@ -9,6 +9,8 @@ pyglet.options.debug_gl = False import logging, datetime, json, sys, arcade +arcade.ArcadeContext.atlas_size = (16384, 16384) + from utils.utils import get_closest_resolution, print_debug_info, on_exception, ErrorView from utils.constants import log_dir, menu_background_color from menus.main import Main diff --git a/utils/constants.py b/utils/constants.py index 9995237..7380246 100644 --- a/utils/constants.py +++ b/utils/constants.py @@ -7,47 +7,51 @@ menu_background_color = (17, 17, 17) log_dir = 'logs' discord_presence_id = 1368277020332523530 -audio_extensions = [ - "3g2", "3gp", "aac", "ac3", "aiff", "alac", "amr", "ape", "au", "caf", - "dts", "flac", "gsm", "m4a", "mka", "mlp", "mmf", "mp2", "mp3", - "oga", "ogg", "opus", "ra", "rm", "sln", "tta", "vorbis", "voc", "vox", - "wav", "webm", "wma", "wv" -] +audio_extensions = ["mp3", "m4a", "mp4", "aac", "flac", "ogg", "opus", "wav"] -yt_dlp_parameters = { - "final_ext": "mp3", - "format": "bestaudio/best", - "outtmpl": {"pl_thumbnail": "", "default": "downloaded_music.mp3"}, - "postprocessors": [ - { - "key": "FFmpegExtractAudio", - "nopostoverwrites": False, - "preferredcodec": "mp3", - "preferredquality": "5" - }, - { - "add_chapters": True, - "add_infojson": "if_exists", - "add_metadata": True, - "key": "FFmpegMetadata" - }, - { "already_have_thumbnail": False, "key": "EmbedThumbnail" } - ], - "writethumbnail": True +DARK_GRAY = Color(45, 45, 45) +GRAY = Color(70, 70, 70) +LIGHT_GRAY = Color(150, 150, 150) +PRIMARY = Color(0, 189, 126) +PRIMARY_DARK = Color(0, 145, 96) +DISABLED = Color(90, 90, 90) +FONT_COLOR = arcade.color.BLACK +FONT = "Roboto" +FONT_SIZE = 14 +BIG_FONT_SIZE = 22 + +button_style = { + "normal": UIFlatButtonStyle(font_name=FONT, font_size=FONT_SIZE, font_color=FONT_COLOR, bg=GRAY), + "hover": UIFlatButtonStyle(font_name=FONT, font_size=FONT_SIZE, font_color=FONT_COLOR, bg=PRIMARY), + "press": UIFlatButtonStyle(font_name=FONT, font_size=FONT_SIZE, font_color=FONT_COLOR, bg=PRIMARY_DARK), + "disabled": UIFlatButtonStyle(font_name=FONT, font_size=FONT_SIZE, font_color=LIGHT_GRAY, bg=DISABLED), } -button_style = {'normal': UITextureButtonStyle(font_name="Protest Strike", font_color=arcade.color.BLACK), 'hover': UITextureButtonStyle(font_name="Protest Strike", font_color=arcade.color.BLACK), - 'press': UITextureButtonStyle(font_name="Protest Strike", font_color=arcade.color.BLACK), 'disabled': UITextureButtonStyle(font_name="Protest Strike", font_color=arcade.color.BLACK)} -big_button_style = {'normal': UITextureButtonStyle(font_name="Protest Strike", font_color=arcade.color.BLACK, font_size=26), 'hover': UITextureButtonStyle(font_name="Protest Strike", font_color=arcade.color.BLACK, font_size=26), - 'press': UITextureButtonStyle(font_name="Protest Strike", font_color=arcade.color.BLACK, font_size=26), 'disabled': UITextureButtonStyle(font_name="Protest Strike", font_color=arcade.color.BLACK, font_size=26)} +big_button_style = { + "normal": UIFlatButtonStyle(font_name=FONT, font_size=BIG_FONT_SIZE, font_color=FONT_COLOR, bg=GRAY), + "hover": UIFlatButtonStyle(font_name=FONT, font_size=BIG_FONT_SIZE, font_color=FONT_COLOR, bg=PRIMARY), + "press": UIFlatButtonStyle(font_name=FONT, font_size=BIG_FONT_SIZE, font_color=FONT_COLOR, bg=PRIMARY_DARK), + "disabled": UIFlatButtonStyle(font_name=FONT, font_size=BIG_FONT_SIZE, font_color=LIGHT_GRAY, bg=DISABLED), +} -dropdown_style = {'normal': UIFlatButtonStyle(font_name="Protest Strike", font_color=arcade.color.BLACK, bg=Color(128, 128, 128)), 'hover': UIFlatButtonStyle(font_name="Protest Strike", font_color=arcade.color.BLACK, bg=Color(49, 154, 54)), - 'press': UIFlatButtonStyle(font_name="Protest Strike", font_color=arcade.color.BLACK, bg=Color(128, 128, 128)), 'disabled': UIFlatButtonStyle(font_name="Protest Strike", font_color=arcade.color.BLACK, bg=Color(128, 128, 128))} +slider_default_style = UISliderStyle( + bg=GRAY, + unfilled_track=DARK_GRAY, + filled_track=PRIMARY +) -slider_default_style = UISliderStyle(bg=Color(128, 128, 128), unfilled_track=Color(128, 128, 128), filled_track=Color(49, 154, 54)) -slider_hover_style = UISliderStyle(bg=Color(49, 154, 54), unfilled_track=Color(128, 128, 128), filled_track=Color(49, 154, 54)) +slider_hover_style = UISliderStyle( + bg=PRIMARY, + unfilled_track=DARK_GRAY, + filled_track=PRIMARY_DARK +) -slider_style = {'normal': slider_default_style, 'hover': slider_hover_style, 'press': slider_hover_style, 'disabled': slider_default_style} +slider_style = { + "normal": slider_default_style, + "hover": slider_hover_style, + "press": slider_hover_style, + "disabled": slider_default_style, +} settings = { "Music": { diff --git a/utils/preload.py b/utils/preload.py index 08842e0..f3ad475 100644 --- a/utils/preload.py +++ b/utils/preload.py @@ -19,3 +19,5 @@ download_icon = arcade.load_texture("assets/graphics/download.png") plus_icon = arcade.load_texture("assets/graphics/plus.png") playlist_icon = arcade.load_texture("assets/graphics/playlist.png") files_icon = arcade.load_texture("assets/graphics/files.png") + +music_icon = arcade.load_texture("assets/graphics/music.png") diff --git a/utils/utils.py b/utils/utils.py index a8af9d2..1c8c681 100644 --- a/utils/utils.py +++ b/utils/utils.py @@ -1,8 +1,9 @@ -import logging, arcade, arcade.gui, sys, traceback, os, re, platform, urllib.request, zipfile, subprocess +import logging, sys, traceback, os, re, platform, urllib.request, zipfile, subprocess, textwrap, io, base64 from mutagen.easyid3 import EasyID3 - +from mutagen import File +from PIL import Image from utils.constants import menu_background_color -import pyglet +import pyglet, arcade, arcade.gui def dump_platform(): import platform @@ -58,25 +59,6 @@ class ErrorView(arcade.gui.UIView): msgbox.on_action = lambda event: self.exit() self.add_widget(msgbox) -def on_exception(*exc_info): - logging.error(f"Unhandled exception:\n{''.join(traceback.format_exception(exc_info[1], limit=None))}") - -def get_closest_resolution(): - allowed_resolutions = [(1366, 768), (1440, 900), (1600,900), (1920,1080), (2560,1440), (3840,2160)] - screen_width, screen_height = arcade.get_screens()[0].width, arcade.get_screens()[0].height - if (screen_width, screen_height) in allowed_resolutions: - if not allowed_resolutions.index((screen_width, screen_height)) == 0: - closest_resolution = allowed_resolutions[allowed_resolutions.index((screen_width, screen_height))-1] - else: - closest_resolution = (screen_width, screen_height) - else: - target_width, target_height = screen_width // 2, screen_height // 2 - - closest_resolution = min( - allowed_resolutions, - key=lambda res: abs(res[0] - target_width) + abs(res[1] - target_height) - ) - return closest_resolution class FakePyPresence(): def __init__(self): @@ -97,16 +79,60 @@ class UIFocusTextureButton(arcade.gui.UITextureButton): else: self.resize(width=self.width / 1.1, height=self.height / 1.1) +class Card(arcade.gui.UIBoxLayout): + def __init__(self, width: int, height: int, font_name: str, font_size: int, text: str, card_texture: arcade.Texture, padding=10): + super().__init__(width=width, height=height, space_between=padding, align="bottom") + + self.button = self.add(arcade.gui.UITextureButton( + texture=card_texture, + texture_hovered=card_texture, + texture_pressed=card_texture, + texture_disabled=card_texture, + width=width / 2, + height=height * 0.5, + )) + + wrapped_lines = textwrap.wrap(text, width=int(width / (font_size * 0.6))) + wrapped_text = "\n".join(wrapped_lines) + + self.label = self.add(arcade.gui.UILabel( + text=wrapped_text, + font_name=font_name, + font_size=font_size, + width=width, + height=height * 0.5, + multiline=True + )) + +def on_exception(*exc_info): + logging.error(f"Unhandled exception:\n{''.join(traceback.format_exception(exc_info[1], limit=None))}") + +def get_closest_resolution(): + allowed_resolutions = [(1366, 768), (1440, 900), (1600,900), (1920,1080), (2560,1440), (3840,2160)] + screen_width, screen_height = arcade.get_screens()[0].width, arcade.get_screens()[0].height + if (screen_width, screen_height) in allowed_resolutions: + if not allowed_resolutions.index((screen_width, screen_height)) == 0: + closest_resolution = allowed_resolutions[allowed_resolutions.index((screen_width, screen_height))-1] + else: + closest_resolution = (screen_width, screen_height) + else: + target_width, target_height = screen_width // 2, screen_height // 2 + + closest_resolution = min( + allowed_resolutions, + key=lambda res: abs(res[0] - target_width) + abs(res[1] - target_height) + ) + return closest_resolution + def get_yt_dlp_binary_path(): - binary = "yt-dlp" system = platform.system() if system == "Windows": - binary += ".exe" + binary = "yt-dlp.exe" elif system == "Darwin": - binary += "_macos" + binary = "yt-dlp_macos" elif system == "Linux": - binary += "_linux" + binary = "yt-dlp_linux" return os.path.join("bin", binary) @@ -150,10 +176,10 @@ def extract_metadata(filename): name_only = re.sub(r'\s*\[[a-zA-Z0-9\-_]{5,}\]$', '', name_only) try: - audio = EasyID3(filename) + thumb_audio = EasyID3(filename) - artist = str(audio["artist"][0]) - title = str(audio["title"][0]) + artist = str(thumb_audio["artist"][0]) + title = str(thumb_audio["title"][0]) artist_title_match = re.search(r'^.+\s*-\s*.+$', title) # check for Artist - Title titles, so Artist doesnt appear twice @@ -182,3 +208,43 @@ def extract_metadata(filename): title = name_only return artist, title + +def get_audio_thumbnail_texture(audio_path: str, window_resolution: tuple) -> arcade.Texture: + ext = os.path.splitext(audio_path)[1].lower().lstrip('.') + thumb_audio = File(audio_path) + + thumb_image_data = None + + try: + if ext == 'mp3': + for tag in thumb_audio.values(): + if tag.FrameID == "APIC": + thumb_image_data = tag.data + break + + elif ext in ('m4a', 'mp4', 'aac'): + if 'covr' in thumb_audio: + thumb_image_data = thumb_audio['covr'][0] + + elif ext == 'flac': + if thumb_audio.pictures: + thumb_image_data = thumb_audio.pictures[0].data + + elif ext in ('ogg', 'opus'): + if "metadata_block_picture" in thumb_audio: + pic_data = base64.b64decode(thumb_audio["metadata_block_picture"][0]) + import struct + header_len = struct.unpack(">I", pic_data[0:4])[0] + thumb_image_data = pic_data[4 + header_len:] + + if thumb_image_data: + pil_image = Image.open(io.BytesIO(thumb_image_data)).convert("RGBA") + pil_image = pil_image.resize((int(window_resolution[0] / 5), int(window_resolution[1] / 8))) + thumb_texture = arcade.Texture(pil_image) + return thumb_texture + + except Exception as e: + logging.debug(f"[Thumbnail Error] {audio_path}: {e}") + + from utils.preload import music_icon + return music_icon