Move from list to card grid view, only support popular file extensions,

Use Roboto Black as a font, dont recreate ui on refresh, update styles
This commit is contained in:
csd4ni3l
2025-05-25 19:09:59 +02:00
parent 31bcf82534
commit caf567b003
14 changed files with 335 additions and 135 deletions

View File

@@ -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": {

View File

@@ -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")

View File

@@ -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