mirror of
https://github.com/Dvorinka/swingmusic-extended.git
synced 2026-06-03 20:13:02 +00:00
fix merge conflicts
This commit is contained in:
@@ -3,7 +3,6 @@ This module contains all the Flask Blueprints and API routes. It also contains a
|
||||
that are used through-out the app. It handles the initialization of the watchdog,
|
||||
checking and creating config dirs and starting the re-indexing process using a background thread.
|
||||
"""
|
||||
|
||||
from app import functions
|
||||
from app import helpers
|
||||
from app import prep
|
||||
|
||||
+20
-11
@@ -6,12 +6,12 @@ from typing import List
|
||||
|
||||
from app import api
|
||||
from app import helpers
|
||||
from app import instances
|
||||
from app import models
|
||||
from app.functions import FetchAlbumBio
|
||||
from app.lib import albumslib
|
||||
from flask import Blueprint
|
||||
from flask import request
|
||||
from app.functions import FetchAlbumBio
|
||||
from app import instances
|
||||
|
||||
album_bp = Blueprint("album", __name__, url_prefix="")
|
||||
|
||||
@@ -41,27 +41,31 @@ def get_album():
|
||||
"""Returns all the tracks in the given album."""
|
||||
data = request.get_json()
|
||||
albumhash = data["hash"]
|
||||
error_msg = {"error": "Album not created yet."}
|
||||
|
||||
tracks = instances.tracks_instance.find_tracks_by_hash(albumhash)
|
||||
|
||||
if len(tracks) == 0:
|
||||
return error_msg, 204
|
||||
|
||||
tracks = [models.Track(t) for t in tracks]
|
||||
tracks = helpers.RemoveDuplicates(tracks)()
|
||||
|
||||
album = instances.album_instance.find_album_by_hash(albumhash)
|
||||
|
||||
if not album:
|
||||
return {"error": "Album not created yet."}, 204
|
||||
return error_msg, 204
|
||||
|
||||
album = models.Album(album)
|
||||
|
||||
album.count = len(tracks)
|
||||
album.duration = albumslib.get_album_duration(tracks)
|
||||
try:
|
||||
album.duration = sum([t.length for t in tracks])
|
||||
except AttributeError:
|
||||
album.duration = 0
|
||||
|
||||
if (
|
||||
album.count == 1
|
||||
and tracks[0].title == album.title
|
||||
and tracks[0].tracknumber == 1
|
||||
and tracks[0].disknumber == 1
|
||||
):
|
||||
if (album.count == 1 and tracks[0].title == album.title
|
||||
and tracks[0].tracknumber == 1 and tracks[0].disknumber == 1):
|
||||
album.is_single = True
|
||||
|
||||
return {"tracks": tracks, "info": album}
|
||||
@@ -72,12 +76,17 @@ def get_album_bio():
|
||||
"""Returns the album bio for the given album."""
|
||||
data = request.get_json()
|
||||
album_hash = data["hash"]
|
||||
err_msg = {"bio": "No bio found"}
|
||||
|
||||
album = instances.album_instance.find_album_by_hash(album_hash)
|
||||
|
||||
if album is None:
|
||||
return err_msg, 404
|
||||
|
||||
bio = FetchAlbumBio(album["title"], album["artist"])()
|
||||
|
||||
if bio is None:
|
||||
return {"bio": "No bio found."}, 404
|
||||
return err_msg, 404
|
||||
|
||||
return {"bio": bio}
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ from flask import Blueprint
|
||||
|
||||
artist_bp = Blueprint("artist", __name__, url_prefix="/")
|
||||
|
||||
|
||||
# @artist_bp.route("/artist/<artist>")
|
||||
# @cache.cached()
|
||||
# def get_artist_data(artist: str):
|
||||
|
||||
@@ -8,12 +8,13 @@ from app import exceptions
|
||||
from app import instances
|
||||
from app import models
|
||||
from app import serializer
|
||||
from app.helpers import create_new_date
|
||||
from app.helpers import Get
|
||||
from app.helpers import UseBisection
|
||||
from app.lib import playlistlib
|
||||
from flask import Blueprint
|
||||
from flask import request
|
||||
|
||||
from app.helpers import Get, UseBisection, create_new_date
|
||||
|
||||
playlist_bp = Blueprint("playlist", __name__, url_prefix="/")
|
||||
|
||||
PlaylistExists = exceptions.PlaylistExists
|
||||
@@ -27,7 +28,8 @@ def get_all_playlists():
|
||||
dbplaylists = [models.Playlist(p) for p in dbplaylists]
|
||||
|
||||
playlists = [
|
||||
serializer.Playlist(p, construct_last_updated=False) for p in dbplaylists
|
||||
serializer.Playlist(p, construct_last_updated=False)
|
||||
for p in dbplaylists
|
||||
]
|
||||
playlists.sort(
|
||||
key=lambda p: datetime.strptime(p.lastUpdated, "%Y-%m-%d %H:%M:%S"),
|
||||
@@ -84,7 +86,12 @@ def get_playlist(playlistid: str):
|
||||
playlist = models.Playlist(p)
|
||||
|
||||
tracks = playlistlib.create_playlist_tracks(playlist.pretracks)
|
||||
return {"info": serializer.Playlist(playlist), "tracks": tracks}
|
||||
|
||||
duration = sum([t.length for t in tracks])
|
||||
playlist = serializer.Playlist(playlist)
|
||||
playlist.duration = duration
|
||||
|
||||
return {"info": playlist, "tracks": tracks}
|
||||
|
||||
|
||||
@playlist_bp.route("/playlist/<playlistid>/update", methods=["PUT"])
|
||||
|
||||
@@ -3,14 +3,14 @@ Contains all the search routes.
|
||||
"""
|
||||
from pprint import pprint
|
||||
from typing import List
|
||||
|
||||
from app import helpers
|
||||
from app import models
|
||||
from app import serializer
|
||||
from app.lib import searchlib
|
||||
from flask import Blueprint
|
||||
from flask import request
|
||||
|
||||
from app import models
|
||||
from app import serializer
|
||||
|
||||
search_bp = Blueprint("search", __name__, url_prefix="/")
|
||||
|
||||
SEARCH_RESULTS = {
|
||||
@@ -197,20 +197,20 @@ def search_load_more():
|
||||
if type == "tracks":
|
||||
t = SearchResults.tracks
|
||||
return {
|
||||
"tracks": t[index : index + 5],
|
||||
"tracks": t[index:index + 5],
|
||||
"more": len(t) > index + 5,
|
||||
}
|
||||
|
||||
elif type == "albums":
|
||||
a = SearchResults.albums
|
||||
return {
|
||||
"albums": a[index : index + 6],
|
||||
"albums": a[index:index + 6],
|
||||
"more": len(a) > index + 6,
|
||||
}
|
||||
|
||||
elif type == "artists":
|
||||
a = SearchResults.artists
|
||||
return {
|
||||
"artists": a[index : index + 6],
|
||||
"artists": a[index:index + 6],
|
||||
"more": len(a) > index + 6,
|
||||
}
|
||||
|
||||
@@ -3,11 +3,10 @@ Contains all the track routes.
|
||||
"""
|
||||
from app import api
|
||||
from app import instances
|
||||
from app import models
|
||||
from flask import Blueprint
|
||||
from flask import send_file
|
||||
|
||||
from app import models
|
||||
|
||||
track_bp = Blueprint("track", __name__, url_prefix="/")
|
||||
|
||||
|
||||
@@ -17,14 +16,18 @@ def send_track_file(trackid):
|
||||
Returns an audio file that matches the passed id to the client.
|
||||
"""
|
||||
track = instances.tracks_instance.get_track_by_id(trackid)
|
||||
msg = {"msg": "File Not Found"}
|
||||
|
||||
if track is None:
|
||||
return "File not found", 404
|
||||
return msg, 404
|
||||
|
||||
track = models.Track(track)
|
||||
type = track.filepath.split(".")[-1]
|
||||
|
||||
return send_file(track.filepath, mimetype=f"audio/{type}")
|
||||
try:
|
||||
return send_file(track.filepath, mimetype=f"audio/{type}")
|
||||
except FileNotFoundError:
|
||||
return msg, 404
|
||||
|
||||
|
||||
@track_bp.route("/sample")
|
||||
@@ -33,6 +36,5 @@ def get_sample_track():
|
||||
Returns a sample track object.
|
||||
"""
|
||||
|
||||
return instances.tracks_instance.get_song_by_album(
|
||||
"Legends Never Die", "Juice WRLD"
|
||||
)
|
||||
return instances.tracks_instance.get_song_by_album("Legends Never Die",
|
||||
"Juice WRLD")
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
This file contains the Album class for interacting with
|
||||
album documents in MongoDB.
|
||||
"""
|
||||
from typing import List
|
||||
|
||||
from app.db.mongodb import convert_many
|
||||
from app.db.mongodb import convert_one
|
||||
from app.db.mongodb import MongoAlbums
|
||||
@@ -20,8 +22,13 @@ class Albums(MongoAlbums):
|
||||
"""
|
||||
album = album.__dict__
|
||||
return self.collection.update_one(
|
||||
{"album": album["title"], "artist": album["artist"]},
|
||||
{"$set": album},
|
||||
{
|
||||
"album": album["title"],
|
||||
"artist": album["artist"]
|
||||
},
|
||||
{
|
||||
"$set": album
|
||||
},
|
||||
upsert=True,
|
||||
).upserted_id
|
||||
|
||||
@@ -59,3 +66,14 @@ class Albums(MongoAlbums):
|
||||
"""
|
||||
album = self.collection.find_one({"hash": hash})
|
||||
return convert_one(album)
|
||||
|
||||
def set_album_colors(self, colors: List[str], hash: str) -> None:
|
||||
"""
|
||||
Sets the colors for an album.
|
||||
"""
|
||||
self.collection.update_one(
|
||||
{"hash": hash},
|
||||
{"$set": {
|
||||
"colors": colors
|
||||
}},
|
||||
)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"""
|
||||
This file contains the Playlists class for interacting with the playlist documents in MongoDB.
|
||||
"""
|
||||
from app import helpers
|
||||
from app.db.mongodb import convert_many
|
||||
from app.db.mongodb import convert_one
|
||||
from app.db.mongodb import MongoPlaylists
|
||||
from app import helpers
|
||||
from bson import ObjectId
|
||||
|
||||
|
||||
@@ -18,8 +18,12 @@ class Playlists(MongoPlaylists):
|
||||
Inserts a new playlist object into the database.
|
||||
"""
|
||||
return self.collection.update_one(
|
||||
{"name": playlist["name"]},
|
||||
{"$set": playlist},
|
||||
{
|
||||
"name": playlist["name"]
|
||||
},
|
||||
{
|
||||
"$set": playlist
|
||||
},
|
||||
upsert=True,
|
||||
).upserted_id
|
||||
|
||||
@@ -45,7 +49,9 @@ class Playlists(MongoPlaylists):
|
||||
|
||||
return self.collection.update_one(
|
||||
{"_id": ObjectId(playlistid)},
|
||||
{"$set": {"lastUpdated": date}},
|
||||
{"$set": {
|
||||
"lastUpdated": date
|
||||
}},
|
||||
)
|
||||
|
||||
def add_track_to_playlist(self, playlistid: str, track: dict) -> None:
|
||||
@@ -56,7 +62,9 @@ class Playlists(MongoPlaylists):
|
||||
{
|
||||
"_id": ObjectId(playlistid),
|
||||
},
|
||||
{"$push": {"pre_tracks": track}},
|
||||
{"$push": {
|
||||
"pre_tracks": track
|
||||
}},
|
||||
)
|
||||
self.set_last_updated(playlistid)
|
||||
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
This file contains the AllSongs class for interacting with track documents in MongoDB.
|
||||
"""
|
||||
import pymongo
|
||||
from app.db.mongodb import MongoTracks, convert_many, convert_one
|
||||
from app.db.mongodb import convert_many
|
||||
from app.db.mongodb import convert_one
|
||||
from app.db.mongodb import MongoTracks
|
||||
from bson import ObjectId
|
||||
|
||||
|
||||
@@ -18,9 +20,12 @@ class Tracks(MongoTracks):
|
||||
"""
|
||||
Inserts a new track object into the database.
|
||||
"""
|
||||
return self.collection.update_one(
|
||||
{"filepath": song_obj["filepath"]}, {"$set": song_obj}, upsert=True
|
||||
).upserted_id
|
||||
return self.collection.update_one({
|
||||
"filepath": song_obj["filepath"]
|
||||
}, {
|
||||
"$set": song_obj
|
||||
},
|
||||
upsert=True).upserted_id
|
||||
|
||||
def insert_many(self, songs: list):
|
||||
"""
|
||||
@@ -52,21 +57,33 @@ class Tracks(MongoTracks):
|
||||
"""
|
||||
Returns all the songs matching the albums in the query params (using regex).
|
||||
"""
|
||||
songs = self.collection.find({"album": {"$regex": query, "$options": "i"}})
|
||||
songs = self.collection.find(
|
||||
{"album": {
|
||||
"$regex": query,
|
||||
"$options": "i"
|
||||
}})
|
||||
return convert_many(songs)
|
||||
|
||||
def search_songs_by_artist(self, query: str) -> list:
|
||||
"""
|
||||
Returns all the songs matching the artists in the query params.
|
||||
"""
|
||||
songs = self.collection.find({"artists": {"$regex": query, "$options": "i"}})
|
||||
songs = self.collection.find(
|
||||
{"artists": {
|
||||
"$regex": query,
|
||||
"$options": "i"
|
||||
}})
|
||||
return convert_many(songs)
|
||||
|
||||
def find_song_by_title(self, query: str) -> list:
|
||||
"""
|
||||
Finds all the tracks matching the title in the query params.
|
||||
"""
|
||||
song = self.collection.find({"title": {"$regex": query, "$options": "i"}})
|
||||
song = self.collection.find(
|
||||
{"title": {
|
||||
"$regex": query,
|
||||
"$options": "i"
|
||||
}})
|
||||
return convert_many(song)
|
||||
|
||||
def find_songs_by_album(self, name: str, artist: str) -> list:
|
||||
@@ -80,7 +97,9 @@ class Tracks(MongoTracks):
|
||||
"""
|
||||
Returns a sorted list of all the tracks exactly matching the folder in the query params
|
||||
"""
|
||||
songs = self.collection.find({"folder": query}).sort("title", pymongo.ASCENDING)
|
||||
songs = self.collection.find({
|
||||
"folder": query
|
||||
}).sort("title", pymongo.ASCENDING)
|
||||
return convert_many(songs)
|
||||
|
||||
def find_songs_by_filenames(self, filenames: list) -> list:
|
||||
@@ -102,8 +121,10 @@ class Tracks(MongoTracks):
|
||||
Returns a list of all the tracks matching the path in the query params.
|
||||
"""
|
||||
return self.collection.count_documents(
|
||||
{"filepath": {"$regex": f"^{path}", "$options": "i"}}
|
||||
)
|
||||
{"filepath": {
|
||||
"$regex": f"^{path}",
|
||||
"$options": "i"
|
||||
}})
|
||||
|
||||
def find_songs_by_artist(self, query: str) -> list:
|
||||
"""
|
||||
@@ -117,8 +138,10 @@ class Tracks(MongoTracks):
|
||||
Returns a list of all the tracks containing the albumartist in the query params.
|
||||
"""
|
||||
songs = self.collection.find(
|
||||
{"albumartist": {"$regex": query, "$options": "i"}}
|
||||
)
|
||||
{"albumartist": {
|
||||
"$regex": query,
|
||||
"$options": "i"
|
||||
}})
|
||||
return convert_many(songs)
|
||||
|
||||
def get_song_by_path(self, path: str) -> dict:
|
||||
@@ -155,13 +178,14 @@ class Tracks(MongoTracks):
|
||||
songs = self.collection.find({"albumhash": hash})
|
||||
return convert_many(songs)
|
||||
|
||||
def find_track_by_title_artists_album(
|
||||
self, title: str, artist: str, album: str
|
||||
) -> dict:
|
||||
def find_track_by_title_artists_album(self, title: str, artist: str,
|
||||
album: str) -> dict:
|
||||
"""
|
||||
Returns a single track matching the title, artist, and album in the query params.
|
||||
"""
|
||||
song = self.collection.find_one(
|
||||
{"title": title, "artists": artist, "album": album}
|
||||
)
|
||||
song = self.collection.find_one({
|
||||
"title": title,
|
||||
"artists": artist,
|
||||
"album": album
|
||||
})
|
||||
return convert_one(song)
|
||||
|
||||
+43
-30
@@ -7,14 +7,19 @@ from concurrent.futures import ThreadPoolExecutor
|
||||
from io import BytesIO
|
||||
|
||||
import requests
|
||||
from PIL import Image
|
||||
|
||||
from app import helpers, settings
|
||||
from app import helpers
|
||||
from app import settings
|
||||
from app.lib import trackslib
|
||||
from app.lib import watchdoge
|
||||
from app.lib.albumslib import ValidateAlbumThumbs
|
||||
from app.lib import trackslib
|
||||
from app.lib.populate import CreateAlbums, Populate
|
||||
from app.lib.colorlib import ProcessAlbumColors
|
||||
from app.lib.playlistlib import ValidatePlaylistThumbs
|
||||
from app.lib.populate import CreateAlbums
|
||||
from app.lib.populate import Populate
|
||||
from app.logger import get_logger
|
||||
from PIL import Image
|
||||
|
||||
log = get_logger()
|
||||
|
||||
|
||||
@helpers.background
|
||||
@@ -22,18 +27,25 @@ def run_checks():
|
||||
"""
|
||||
Checks for new songs every 5 minutes.
|
||||
"""
|
||||
|
||||
# while True:
|
||||
trackslib.validate_tracks()
|
||||
|
||||
Populate()
|
||||
CreateAlbums()
|
||||
|
||||
if helpers.Ping()():
|
||||
CheckArtistImages()()
|
||||
|
||||
ValidateAlbumThumbs()
|
||||
ValidatePlaylistThumbs()
|
||||
|
||||
while True:
|
||||
trackslib.validate_tracks()
|
||||
|
||||
Populate()
|
||||
CreateAlbums()
|
||||
|
||||
if helpers.Ping()():
|
||||
CheckArtistImages()()
|
||||
|
||||
@helpers.background
|
||||
def process_album_colors():
|
||||
ProcessAlbumColors()
|
||||
|
||||
ValidatePlaylistThumbs()
|
||||
process_album_colors()
|
||||
|
||||
time.sleep(300)
|
||||
|
||||
|
||||
@helpers.background
|
||||
@@ -67,6 +79,7 @@ class getArtistImage:
|
||||
|
||||
|
||||
class useImageDownloader:
|
||||
|
||||
def __init__(self, url: str, dest: str) -> None:
|
||||
self.url = url
|
||||
self.dest = dest
|
||||
@@ -76,14 +89,18 @@ class useImageDownloader:
|
||||
img = Image.open(BytesIO(requests.get(self.url).content))
|
||||
img.save(self.dest, format="webp")
|
||||
img.close()
|
||||
return "fetched image"
|
||||
except requests.exceptions.ConnectionError:
|
||||
time.sleep(5)
|
||||
return "connection error"
|
||||
|
||||
|
||||
class CheckArtistImages:
|
||||
|
||||
def __init__(self):
|
||||
self.artists: list[str] = []
|
||||
print("Checking for artist images")
|
||||
log.info("Checking artist images")
|
||||
|
||||
@staticmethod
|
||||
def check_if_exists(img_path: str):
|
||||
@@ -104,29 +121,25 @@ class CheckArtistImages:
|
||||
:param artistname: The artist name
|
||||
"""
|
||||
|
||||
img_path = (
|
||||
settings.APP_DIR
|
||||
+ "/images/artists/"
|
||||
+ helpers.create_safe_name(artistname)
|
||||
+ ".webp"
|
||||
)
|
||||
img_path = (settings.APP_DIR + "/images/artists/" +
|
||||
helpers.create_safe_name(artistname) + ".webp")
|
||||
|
||||
if cls.check_if_exists(img_path):
|
||||
return
|
||||
return "exists"
|
||||
|
||||
url = getArtistImage(artistname)()
|
||||
|
||||
if url is None:
|
||||
return
|
||||
useImageDownloader(url, img_path)()
|
||||
return "url is none"
|
||||
|
||||
return useImageDownloader(url, img_path)()
|
||||
|
||||
def __call__(self):
|
||||
self.artists = helpers.Get.get_all_artists()
|
||||
|
||||
with ThreadPoolExecutor() as pool:
|
||||
iter = pool.map(self.download_image, self.artists)
|
||||
for i in iter:
|
||||
pass
|
||||
[i for i in iter]
|
||||
|
||||
print("Done fetching images")
|
||||
|
||||
@@ -136,8 +149,7 @@ def fetch_album_bio(title: str, albumartist: str) -> str | None:
|
||||
Returns the album bio for a given album.
|
||||
"""
|
||||
last_fm_url = "http://ws.audioscrobbler.com/2.0/?method=album.getinfo&api_key={}&artist={}&album={}&format=json".format(
|
||||
settings.LAST_FM_API_KEY, albumartist, title
|
||||
)
|
||||
settings.LAST_FM_API_KEY, albumartist, title)
|
||||
|
||||
try:
|
||||
response = requests.get(last_fm_url)
|
||||
@@ -146,7 +158,8 @@ def fetch_album_bio(title: str, albumartist: str) -> str | None:
|
||||
return None
|
||||
|
||||
try:
|
||||
bio = data["album"]["wiki"]["summary"].split('<a href="https://www.last.fm/')[0]
|
||||
bio = data["album"]["wiki"]["summary"].split(
|
||||
'<a href="https://www.last.fm/')[0]
|
||||
except KeyError:
|
||||
bio = None
|
||||
|
||||
|
||||
+9
-14
@@ -1,19 +1,16 @@
|
||||
"""
|
||||
This module contains mini functions for the server.
|
||||
"""
|
||||
from dataclasses import dataclass
|
||||
import os
|
||||
from pprint import pprint
|
||||
import threading
|
||||
from datetime import datetime
|
||||
from typing import Dict, Set
|
||||
from typing import Dict
|
||||
from typing import List
|
||||
from typing import Set
|
||||
|
||||
import requests
|
||||
|
||||
from app import models
|
||||
from app import instances
|
||||
from app.lib.albumslib import Thumbnail
|
||||
from app import models
|
||||
|
||||
|
||||
def background(func):
|
||||
@@ -53,9 +50,8 @@ def run_fast_scandir(__dir: str, full=False) -> Dict[List[str], List[str]]:
|
||||
return subfolders, files
|
||||
|
||||
|
||||
|
||||
|
||||
class RemoveDuplicates:
|
||||
|
||||
def __init__(self, tracklist: List[models.Track]) -> None:
|
||||
self.tracklist = tracklist
|
||||
|
||||
@@ -77,15 +73,12 @@ def is_valid_file(filename: str) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
ill_chars = '/\\:*?"<>|#&'
|
||||
|
||||
|
||||
def create_album_hash(title: str, artist: str) -> str:
|
||||
"""
|
||||
Creates a simple hash for an album
|
||||
"""
|
||||
lower = (title + artist).replace(" ", "").lower()
|
||||
hash = "".join([i for i in lower if i not in ill_chars])
|
||||
hash = "".join([i for i in lower if i.isalnum()])
|
||||
return hash
|
||||
|
||||
|
||||
@@ -99,7 +92,7 @@ def create_safe_name(name: str) -> str:
|
||||
"""
|
||||
Creates a url-safe name from a name.
|
||||
"""
|
||||
return "".join([i for i in name if i not in ill_chars])
|
||||
return "".join([i for i in name if i.isalnum()])
|
||||
|
||||
|
||||
class UseBisection:
|
||||
@@ -110,7 +103,8 @@ class UseBisection:
|
||||
items.
|
||||
"""
|
||||
|
||||
def __init__(self, source: List, search_from: str, queries: List[str]) -> None:
|
||||
def __init__(self, source: List, search_from: str,
|
||||
queries: List[str]) -> None:
|
||||
self.source_list = source
|
||||
self.queries_list = queries
|
||||
self.attr = search_from
|
||||
@@ -140,6 +134,7 @@ class UseBisection:
|
||||
|
||||
|
||||
class Get:
|
||||
|
||||
@staticmethod
|
||||
def get_all_tracks() -> List[models.Track]:
|
||||
"""
|
||||
|
||||
@@ -6,7 +6,9 @@ import random
|
||||
from dataclasses import dataclass
|
||||
from typing import List
|
||||
|
||||
from app import helpers, instances, models
|
||||
from app import helpers
|
||||
from app import instances
|
||||
from app import models
|
||||
from app.lib import taglib
|
||||
from app.logger import logg
|
||||
from app.settings import THUMBS_PATH
|
||||
@@ -35,6 +37,7 @@ class RipAlbumImage:
|
||||
|
||||
|
||||
class ValidateAlbumThumbs:
|
||||
|
||||
@staticmethod
|
||||
def remove_obsolete():
|
||||
"""
|
||||
@@ -58,7 +61,9 @@ class ValidateAlbumThumbs:
|
||||
Re-rip lost album thumbnails
|
||||
"""
|
||||
entries = os.scandir(THUMBS_PATH)
|
||||
entries = [Thumbnail(entry.name) for entry in entries if entry.is_file()]
|
||||
entries = [
|
||||
Thumbnail(entry.name) for entry in entries if entry.is_file()
|
||||
]
|
||||
|
||||
albums = helpers.Get.get_all_albums()
|
||||
thumbs = [(album.hash + ".webp") for album in albums]
|
||||
@@ -85,19 +90,6 @@ class ValidateAlbumThumbs:
|
||||
self.find_lost_thumbnails()
|
||||
|
||||
|
||||
def get_album_duration(album: List[models.Track]) -> int:
|
||||
"""
|
||||
Gets the duration of an album.
|
||||
"""
|
||||
|
||||
album_duration = 0
|
||||
|
||||
for track in album:
|
||||
album_duration += track.length
|
||||
|
||||
return album_duration
|
||||
|
||||
|
||||
def use_defaults() -> str:
|
||||
"""
|
||||
Returns a path to a random image in the defaults directory.
|
||||
|
||||
+23
-27
@@ -1,17 +1,17 @@
|
||||
from io import BytesIO
|
||||
|
||||
import colorgram
|
||||
from app import api
|
||||
from app import instances
|
||||
from app.lib.taglib import return_album_art
|
||||
from PIL import Image
|
||||
from progress.bar import Bar
|
||||
from app import settings
|
||||
from app.helpers import Get
|
||||
from app.logger import get_logger
|
||||
from app.models import Album
|
||||
|
||||
log = get_logger()
|
||||
|
||||
|
||||
def get_image_colors(image) -> list:
|
||||
def get_image_colors(image: str) -> list:
|
||||
"""Extracts 2 of the most dominant colors from an image."""
|
||||
try:
|
||||
colors = sorted(colorgram.extract(image, 2), key=lambda c: c.hsl.h)
|
||||
colors = sorted(colorgram.extract(image, 4), key=lambda c: c.hsl.h)
|
||||
except OSError:
|
||||
return []
|
||||
|
||||
@@ -24,30 +24,26 @@ def get_image_colors(image) -> list:
|
||||
return formatted_colors
|
||||
|
||||
|
||||
def save_track_colors(img, filepath) -> None:
|
||||
"""Saves the track colors to the database"""
|
||||
class ProcessAlbumColors:
|
||||
|
||||
track_colors = get_image_colors(img)
|
||||
def __init__(self) -> None:
|
||||
log.info("Processing album colors")
|
||||
all_albums = Get.get_all_albums()
|
||||
|
||||
tc_dict = {
|
||||
"filepath": filepath,
|
||||
"colors": track_colors,
|
||||
}
|
||||
all_albums = [a for a in all_albums if len(a.colors) == 0]
|
||||
|
||||
instances.track_color_instance.insert_track_color(tc_dict)
|
||||
for a in all_albums:
|
||||
self.process_color(a)
|
||||
|
||||
log.info("Processing album colors ... ✅")
|
||||
|
||||
def save_t_colors():
|
||||
_bar = Bar("Processing image colors", max=len(api.DB_TRACKS))
|
||||
@staticmethod
|
||||
def process_color(album: Album):
|
||||
img = settings.THUMBS_PATH + "/" + album.image
|
||||
|
||||
for track in api.DB_TRACKS:
|
||||
filepath = track["filepath"]
|
||||
album_art = return_album_art(filepath)
|
||||
colors = get_image_colors(img)
|
||||
|
||||
if album_art is not None:
|
||||
img = Image.open(BytesIO(album_art))
|
||||
save_track_colors(img, filepath)
|
||||
if len(colors) > 0:
|
||||
instances.album_instance.set_album_colors(colors, album.hash)
|
||||
|
||||
_bar.next()
|
||||
|
||||
_bar.finish()
|
||||
return colors
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import time
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from dataclasses import dataclass
|
||||
from os import scandir
|
||||
import time
|
||||
from typing import Tuple
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
from app.models import Folder
|
||||
from app.models import Track
|
||||
|
||||
from app import instances
|
||||
from app.models import Folder
|
||||
from app.models import Track
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -27,10 +26,14 @@ def get_folder_track_count(path: str) -> int:
|
||||
def create_folder(dir: Dir) -> Folder:
|
||||
"""Create a single Folder object"""
|
||||
folder = {
|
||||
"name": dir.path.split("/")[-1],
|
||||
"path": dir.path,
|
||||
"is_sym": dir.is_sym,
|
||||
"trackcount": instances.tracks_instance.find_tracks_inside_path_regex(dir.path),
|
||||
"name":
|
||||
dir.path.split("/")[-1],
|
||||
"path":
|
||||
dir.path,
|
||||
"is_sym":
|
||||
dir.is_sym,
|
||||
"trackcount":
|
||||
instances.tracks_instance.find_tracks_inside_path_regex(dir.path),
|
||||
}
|
||||
|
||||
return Folder(folder)
|
||||
|
||||
@@ -11,14 +11,13 @@ from app import exceptions
|
||||
from app import instances
|
||||
from app import models
|
||||
from app import settings
|
||||
from app.helpers import Get
|
||||
from app.lib import trackslib
|
||||
from app.logger import get_logger
|
||||
from PIL import Image
|
||||
from PIL import ImageSequence
|
||||
from werkzeug import datastructures
|
||||
|
||||
from app.lib import trackslib
|
||||
from app.helpers import Get
|
||||
from app.logger import get_logger
|
||||
|
||||
TrackExistsInPlaylist = exceptions.TrackExistsInPlaylist
|
||||
|
||||
logg = get_logger()
|
||||
@@ -53,7 +52,8 @@ def create_thumbnail(image: any, img_path: str) -> str:
|
||||
Creates a 250 x 250 thumbnail from a playlist image
|
||||
"""
|
||||
thumb_path = "thumb_" + img_path
|
||||
full_thumb_path = os.path.join(settings.APP_DIR, "images", "playlists", thumb_path)
|
||||
full_thumb_path = os.path.join(settings.APP_DIR, "images", "playlists",
|
||||
thumb_path)
|
||||
|
||||
aspect_ratio = image.width / image.height
|
||||
|
||||
@@ -71,11 +71,13 @@ def save_p_image(file: datastructures.FileStorage, pid: str):
|
||||
"""
|
||||
img = Image.open(file)
|
||||
|
||||
random_str = "".join(random.choices(string.ascii_letters + string.digits, k=5))
|
||||
random_str = "".join(
|
||||
random.choices(string.ascii_letters + string.digits, k=5))
|
||||
|
||||
img_path = pid + str(random_str) + ".webp"
|
||||
|
||||
full_img_path = os.path.join(settings.APP_DIR, "images", "playlists", img_path)
|
||||
full_img_path = os.path.join(settings.APP_DIR, "images", "playlists",
|
||||
img_path)
|
||||
|
||||
if file.content_type == "image/gif":
|
||||
frames = []
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
from dataclasses import dataclass
|
||||
import time
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from dataclasses import dataclass
|
||||
from typing import List
|
||||
|
||||
from app import instances
|
||||
from app import settings
|
||||
from app.logger import logg
|
||||
from app.helpers import Get, UseBisection, create_album_hash
|
||||
from app.helpers import create_album_hash
|
||||
from app.helpers import Get
|
||||
from app.helpers import run_fast_scandir
|
||||
from app.helpers import UseBisection
|
||||
from app.instances import tracks_instance
|
||||
from app.lib.albumslib import create_album
|
||||
from app.lib.taglib import get_tags
|
||||
from app.models import Album, Track
|
||||
from app.logger import logg
|
||||
from app.models import Album
|
||||
from app.models import Track
|
||||
from tqdm import tqdm
|
||||
|
||||
from app import instances
|
||||
|
||||
|
||||
class Populate:
|
||||
"""
|
||||
@@ -76,6 +78,7 @@ class PreAlbum:
|
||||
|
||||
|
||||
class CreateAlbums:
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.db_tracks = Get.get_all_tracks()
|
||||
self.db_albums = Get.get_all_albums()
|
||||
@@ -119,7 +122,8 @@ class CreateAlbums:
|
||||
return prealbums
|
||||
|
||||
@staticmethod
|
||||
def filter_processed(albums: List[Album], prealbums: List[PreAlbum]) -> List[dict]:
|
||||
def filter_processed(albums: List[Album],
|
||||
prealbums: List[PreAlbum]) -> List[dict]:
|
||||
to_process = []
|
||||
|
||||
for p in tqdm(prealbums, desc="Filtering processed albums"):
|
||||
@@ -144,7 +148,7 @@ class CreateAlbums:
|
||||
album = create_album(track)
|
||||
self.db_tracks.remove(track)
|
||||
else:
|
||||
album["image"] = hash
|
||||
album["image"] = hash + ".webp"
|
||||
try:
|
||||
album = Album(album)
|
||||
return album
|
||||
|
||||
@@ -37,6 +37,7 @@ class Limit:
|
||||
|
||||
|
||||
class SearchTracks:
|
||||
|
||||
def __init__(self, tracks: List[models.Track], query: str) -> None:
|
||||
self.query = query
|
||||
self.tracks = tracks
|
||||
@@ -59,6 +60,7 @@ class SearchTracks:
|
||||
|
||||
|
||||
class SearchArtists:
|
||||
|
||||
def __init__(self, artists: set[str], query: str) -> None:
|
||||
self.query = query
|
||||
self.artists = artists
|
||||
@@ -88,6 +90,7 @@ class SearchArtists:
|
||||
|
||||
|
||||
class SearchAlbums:
|
||||
|
||||
def __init__(self, albums: List[models.Album], query: str) -> None:
|
||||
self.query = query
|
||||
self.albums = albums
|
||||
@@ -118,6 +121,7 @@ class SearchAlbums:
|
||||
|
||||
|
||||
class SearchPlaylists:
|
||||
|
||||
def __init__(self, playlists: List[models.Playlist], query: str) -> None:
|
||||
self.playlists = playlists
|
||||
self.query = query
|
||||
|
||||
@@ -33,6 +33,7 @@ def extract_thumb(filepath: str, webp_path: str) -> bool:
|
||||
Extracts the thumbnail from an audio file. Returns the path to the thumbnail.
|
||||
"""
|
||||
img_path = os.path.join(settings.THUMBS_PATH, webp_path)
|
||||
tsize = settings.THUMB_SIZE
|
||||
|
||||
if os.path.exists(img_path):
|
||||
return True
|
||||
@@ -43,12 +44,12 @@ def extract_thumb(filepath: str, webp_path: str) -> bool:
|
||||
img = Image.open(BytesIO(album_art))
|
||||
|
||||
try:
|
||||
small_img = img.resize((250, 250), Image.ANTIALIAS)
|
||||
small_img = img.resize((tsize, tsize), Image.ANTIALIAS)
|
||||
small_img.save(img_path, format="webp")
|
||||
except OSError:
|
||||
try:
|
||||
png = img.convert("RGB")
|
||||
small_img = png.resize((250, 250), Image.ANTIALIAS)
|
||||
small_img = png.resize((tsize, tsize), Image.ANTIALIAS)
|
||||
small_img.save(webp_path, format="webp")
|
||||
except:
|
||||
return False
|
||||
|
||||
@@ -22,5 +22,4 @@ def validate_tracks() -> None:
|
||||
|
||||
def get_p_track(ptrack):
|
||||
return instances.tracks_instance.find_track_by_title_artists_album(
|
||||
ptrack["title"], ptrack["artists"], ptrack["album"]
|
||||
)
|
||||
ptrack["title"], ptrack["artists"], ptrack["album"])
|
||||
|
||||
@@ -4,15 +4,15 @@ This library contains the classes and functions related to the watchdog file wat
|
||||
import os
|
||||
import time
|
||||
|
||||
from app.logger import logg
|
||||
from app import instances
|
||||
from app import models
|
||||
from app.helpers import Get, create_album_hash
|
||||
from app.lib.albumslib import create_album
|
||||
from app.helpers import create_album_hash
|
||||
from app.lib.taglib import get_tags
|
||||
from app.logger import get_logger
|
||||
from watchdog.events import PatternMatchingEventHandler
|
||||
from watchdog.observers import Observer
|
||||
|
||||
log = get_logger()
|
||||
|
||||
|
||||
class OnMyWatch:
|
||||
"""
|
||||
@@ -31,7 +31,7 @@ class OnMyWatch:
|
||||
try:
|
||||
self.observer.start()
|
||||
except OSError:
|
||||
logg.error("Could not start watchdog.")
|
||||
log.error("Could not start watchdog.")
|
||||
return
|
||||
|
||||
try:
|
||||
@@ -55,18 +55,6 @@ def add_track(filepath: str) -> None:
|
||||
if tags is not None:
|
||||
hash = create_album_hash(tags["album"], tags["albumartist"])
|
||||
tags["albumhash"] = hash
|
||||
|
||||
album = instances.album_instance.find_album_by_hash(hash)
|
||||
all_tracks = Get.get_all_tracks()
|
||||
all_tracks.append(models.Track(tags))
|
||||
|
||||
if album is None:
|
||||
album_data = create_album(tags, all_tracks)
|
||||
album = models.Album(album_data)
|
||||
|
||||
instances.album_instance.insert_album(album)
|
||||
|
||||
tags["image"] = album.image
|
||||
instances.tracks_instance.insert_song(tags)
|
||||
|
||||
|
||||
@@ -75,8 +63,6 @@ def remove_track(filepath: str) -> None:
|
||||
Removes a track from the music dict.
|
||||
"""
|
||||
|
||||
filepath = filepath + "k"
|
||||
|
||||
instances.tracks_instance.remove_song_by_filepath(filepath)
|
||||
|
||||
|
||||
@@ -140,7 +126,3 @@ class Handler(PatternMatchingEventHandler):
|
||||
|
||||
|
||||
watch = OnMyWatch()
|
||||
|
||||
# TODO
|
||||
# When removing a track, check if there are other tracks in the same album,
|
||||
# if it was the last one, remove the album.
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import logging
|
||||
|
||||
from app.settings import logger
|
||||
|
||||
|
||||
class CustomFormatter(logging.Formatter):
|
||||
|
||||
|
||||
+11
-7
@@ -1,8 +1,9 @@
|
||||
"""
|
||||
Contains all the models for objects generation and typing.
|
||||
"""
|
||||
from dataclasses import dataclass, field
|
||||
import random
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import field
|
||||
from typing import List
|
||||
|
||||
from app import helpers
|
||||
@@ -48,16 +49,13 @@ class Track:
|
||||
self.image = tags["albumhash"] + ".webp"
|
||||
self.tracknumber = int(tags["tracknumber"])
|
||||
|
||||
self.uniq_hash = self.create_unique_hash(
|
||||
"".join(self.artists), self.album, self.title
|
||||
)
|
||||
self.uniq_hash = self.create_unique_hash("".join(self.artists),
|
||||
self.album, self.title)
|
||||
|
||||
@staticmethod
|
||||
def create_unique_hash(*args):
|
||||
ill_chars = '/\\:*?"<>|#&'
|
||||
|
||||
string = "".join(str(a) for a in args).replace(" ", "")
|
||||
return "".join(string).strip(ill_chars).lower()
|
||||
return "".join([i for i in string if i.isalnum()]).lower()
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
@@ -92,6 +90,7 @@ class Album:
|
||||
is_soundtrack: bool = False
|
||||
is_compilation: bool = False
|
||||
is_single: bool = False
|
||||
colors: List[str] = field(default_factory=list)
|
||||
|
||||
def __init__(self, tags):
|
||||
self.title = tags["title"]
|
||||
@@ -100,6 +99,11 @@ class Album:
|
||||
self.image = tags["image"]
|
||||
self.hash = tags["hash"]
|
||||
|
||||
try:
|
||||
self.colors = tags["colors"]
|
||||
except KeyError:
|
||||
self.colors = []
|
||||
|
||||
@property
|
||||
def is_soundtrack(self) -> bool:
|
||||
keywords = ["motion picture", "soundtrack"]
|
||||
|
||||
+6
-10
@@ -11,13 +11,11 @@ class CopyFiles:
|
||||
"""Copies assets to the app directory."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
files = [
|
||||
{
|
||||
"src": "assets",
|
||||
"dest": os.path.join(settings.APP_DIR, "assets"),
|
||||
"is_dir": True,
|
||||
}
|
||||
]
|
||||
files = [{
|
||||
"src": "assets",
|
||||
"dest": os.path.join(settings.APP_DIR, "assets"),
|
||||
"is_dir": True,
|
||||
}]
|
||||
|
||||
for entry in files:
|
||||
src = os.path.join(os.getcwd(), entry["src"])
|
||||
@@ -26,9 +24,7 @@ class CopyFiles:
|
||||
shutil.copytree(
|
||||
src,
|
||||
entry["dest"],
|
||||
ignore=shutil.ignore_patterns(
|
||||
"*.pyc",
|
||||
),
|
||||
ignore=shutil.ignore_patterns("*.pyc", ),
|
||||
copy_function=shutil.copy2,
|
||||
dirs_exist_ok=True,
|
||||
)
|
||||
|
||||
@@ -59,6 +59,7 @@ class Playlist:
|
||||
lastUpdated: int
|
||||
description: str
|
||||
count: int = 0
|
||||
duration: int = 0
|
||||
|
||||
def __init__(self,
|
||||
p: models.Playlist,
|
||||
@@ -72,7 +73,8 @@ class Playlist:
|
||||
self.count = p.count
|
||||
|
||||
if construct_last_updated:
|
||||
self.lastUpdated = self.l_updated(p.lastUpdated)
|
||||
self.lastUpdated = self.get_l_updated(p.lastUpdated)
|
||||
|
||||
def l_updated(self, date: str) -> str:
|
||||
@staticmethod
|
||||
def get_l_updated(date: str) -> str:
|
||||
return date_string_to_time_passed(date)
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
"""
|
||||
Contains default configs
|
||||
"""
|
||||
import os
|
||||
import multiprocessing
|
||||
|
||||
import os
|
||||
|
||||
# paths
|
||||
CONFIG_FOLDER = ".alice"
|
||||
@@ -28,6 +27,9 @@ LAST_FM_API_KEY = "762db7a44a9e6fb5585661f5f2bdf23a"
|
||||
|
||||
CPU_COUNT = multiprocessing.cpu_count()
|
||||
|
||||
THUMB_SIZE: int = 400
|
||||
"""
|
||||
The size of extracted in pixels
|
||||
"""
|
||||
|
||||
class logger:
|
||||
enable = True
|
||||
LOGGER_ENABLE: bool = True
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 1.2 KiB |
@@ -39,7 +39,7 @@ $teal: rgb(64, 200, 224);
|
||||
|
||||
|
||||
$primary: $gray4;
|
||||
$accent: $darkblue;
|
||||
$accent: $red;
|
||||
$secondary: $gray5;
|
||||
$cta: $blue;
|
||||
$danger: $red;
|
||||
|
||||
@@ -8,10 +8,10 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: ['bio'],
|
||||
};
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
bio: string;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@@ -48,7 +48,6 @@ export default {
|
||||
width: 10rem;
|
||||
}
|
||||
|
||||
|
||||
.rect {
|
||||
width: 10rem;
|
||||
height: 10rem;
|
||||
@@ -59,10 +58,10 @@ export default {
|
||||
left: 7rem;
|
||||
transform: rotate(45deg) translate(-1rem, -9rem);
|
||||
z-index: 1;
|
||||
transition: all .5s ease-in-out;
|
||||
transition: all 0.5s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
transition: all .5s ease-in-out;
|
||||
transition: all 0.5s ease-in-out;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,4 +85,4 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
<template>
|
||||
<div class="album-h" ref="albumheaderthing">
|
||||
<div class="a-header rounded">
|
||||
<div
|
||||
class="a-header rounded"
|
||||
:style="{
|
||||
backgroundImage: `linear-gradient(
|
||||
37deg, ${props.album.colors[0]}, ${props.album.colors[3]}
|
||||
)`,
|
||||
}"
|
||||
>
|
||||
<div class="art">
|
||||
<div
|
||||
class="image shadow-lg rounded"
|
||||
:style="{
|
||||
backgroundImage: `url("${imguri + album.image}")`,
|
||||
}"
|
||||
<img
|
||||
:src="imguri + album.image"
|
||||
alt=""
|
||||
v-motion-slide-from-left
|
||||
></div>
|
||||
class="rounded shadow-lg"
|
||||
/>
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="info" :class="{ nocontrast: isLight() }">
|
||||
<div class="top" v-motion-slide-from-top>
|
||||
<div class="h">
|
||||
<span v-if="album.is_soundtrack">Soundtrack</span>
|
||||
@@ -18,7 +24,9 @@
|
||||
<span v-else-if="album.is_single">Single</span>
|
||||
<span v-else>Album</span>
|
||||
</div>
|
||||
<div class="title ellip">{{ album.title }}</div>
|
||||
<div class="title ellip">
|
||||
{{ album.title }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="bottom">
|
||||
<div class="stats">
|
||||
@@ -26,7 +34,11 @@
|
||||
{{ formatSeconds(album.duration, true) }} • {{ album.date }} •
|
||||
{{ album.artist }}
|
||||
</div>
|
||||
<PlayBtnRect :source="playSources.album" :store="useAlbumStore" />
|
||||
<PlayBtnRect
|
||||
:source="playSources.album"
|
||||
:store="useAlbumStore"
|
||||
:background="getButtonColor()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -44,31 +56,133 @@ import { paths } from "../../config";
|
||||
import { AlbumInfo } from "../../interfaces";
|
||||
import PlayBtnRect from "../shared/PlayBtnRect.vue";
|
||||
|
||||
defineProps<{
|
||||
const props = defineProps<{
|
||||
album: AlbumInfo;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: "resetBottomPadding"): void;
|
||||
}>();
|
||||
|
||||
const albumheaderthing = ref<HTMLElement>(null);
|
||||
const imguri = paths.images.thumb;
|
||||
const nav = useNavStore();
|
||||
|
||||
useVisibility(albumheaderthing, nav.toggleShowPlay);
|
||||
/**
|
||||
* Calls the `toggleShowPlay` method which toggles the play button in the nav.
|
||||
* Emits the `resetBottomPadding` event to reset the album page content bottom padding.
|
||||
*
|
||||
* @param {boolean} state the new visibility state of the album page header.
|
||||
*/
|
||||
function handleVisibilityState(state: boolean) {
|
||||
if (state) {
|
||||
emit("resetBottomPadding");
|
||||
}
|
||||
|
||||
nav.toggleShowPlay(state);
|
||||
}
|
||||
|
||||
useVisibility(albumheaderthing, handleVisibilityState);
|
||||
|
||||
/**
|
||||
* Returns `true` if the rgb color passed is light.
|
||||
*
|
||||
* @param {string} rgb The color to check whether it's light or dark.
|
||||
* @returns {boolean} true if color is light, false if color is dark.
|
||||
*/
|
||||
function isLight(rgb: string = props.album.colors[0]): boolean {
|
||||
if (rgb == null || undefined) return false;
|
||||
|
||||
const [r, g, b] = rgb.match(/\d+/g)!.map(Number);
|
||||
const brightness = (r * 299 + g * 587 + b * 114) / 1000;
|
||||
|
||||
return brightness > 170;
|
||||
}
|
||||
|
||||
interface BtnColor {
|
||||
color: string;
|
||||
isDark: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first contrasting color in the album colors.
|
||||
*
|
||||
* @param {string[]} colors The album colors to choose from.
|
||||
* @returns {BtnColor} A color to use as the play button background
|
||||
*/
|
||||
function getButtonColor(colors: string[] = props.album.colors): BtnColor {
|
||||
const base_color = colors[0];
|
||||
if (colors.length === 0) return { color: "#fff", isDark: true };
|
||||
|
||||
for (let i = 0; i < colors.length; i++) {
|
||||
if (theyContrast(base_color, colors[i])) {
|
||||
return {
|
||||
color: colors[i],
|
||||
isDark: isLight(colors[i]),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
color: "#fff",
|
||||
isDark: true,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the luminance of a color.
|
||||
* @param r The red value of the color.
|
||||
* @param g The green value of the color.
|
||||
* @param b The blue value of the color.
|
||||
*/
|
||||
function luminance(r: any, g: any, b: any) {
|
||||
let a = [r, g, b].map(function (v) {
|
||||
v /= 255;
|
||||
return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
|
||||
});
|
||||
return a[0] * 0.2126 + a[1] * 0.7152 + a[2] * 0.0722;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a contrast ratio of `color1`:`color2`
|
||||
* @param {string} color1 The first color
|
||||
* @param {string} color2 The second color
|
||||
*/
|
||||
function contrast(color1: number[], color2: number[]): number {
|
||||
let lum1 = luminance(color1[0], color1[1], color1[2]);
|
||||
let lum2 = luminance(color2[0], color2[1], color2[2]);
|
||||
let brightest = Math.max(lum1, lum2);
|
||||
let darkest = Math.min(lum1, lum2);
|
||||
return (brightest + 0.05) / (darkest + 0.05);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a rgb color string to an array of the form: `[r, g, b]`
|
||||
* @param rgb The color to convert
|
||||
* @returns {number[]} The array representation of the color
|
||||
*/
|
||||
function rgbToArray(rgb: string): number[] {
|
||||
return rgb.match(/\d+/g)!.map(Number);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the `color2` contrast with `color1`.
|
||||
* @param color1 The first color
|
||||
* @param color2 The second color
|
||||
*/
|
||||
function theyContrast(color1: string, color2: string) {
|
||||
return contrast(rgbToArray(color1), rgbToArray(color2)) > 3;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.album-h {
|
||||
height: 16rem;
|
||||
}
|
||||
|
||||
.a-header {
|
||||
display: grid;
|
||||
grid-template-columns: 15rem 1fr;
|
||||
grid-template-columns: max-content 1fr;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
height: 100%;
|
||||
background-color: $black;
|
||||
background-color: #000000;
|
||||
background-image: linear-gradient(37deg, $black 20%, $gray, $black 90%);
|
||||
|
||||
.art {
|
||||
@@ -78,12 +192,17 @@ useVisibility(albumheaderthing, nav.toggleShowPlay);
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
|
||||
.image {
|
||||
width: 14rem;
|
||||
height: 14rem;
|
||||
img {
|
||||
width: 15rem;
|
||||
height: 15rem;
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
}
|
||||
|
||||
.nocontrast {
|
||||
color: $black;
|
||||
}
|
||||
|
||||
.info {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
@@ -91,20 +210,14 @@ useVisibility(albumheaderthing, nav.toggleShowPlay);
|
||||
justify-content: flex-end;
|
||||
|
||||
.top {
|
||||
.h {
|
||||
color: #ffffffcb;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.artist {
|
||||
font-size: 1.15rem;
|
||||
color: #ffffffe0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,23 +234,6 @@ useVisibility(albumheaderthing, nav.toggleShowPlay);
|
||||
font-size: 0.8rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.play {
|
||||
height: 2.5rem;
|
||||
width: 6rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: $blue;
|
||||
padding: $small;
|
||||
cursor: pointer;
|
||||
|
||||
.icon {
|
||||
height: 1.5rem;
|
||||
width: 1.5rem;
|
||||
margin-right: $small;
|
||||
background: url(../../assets/icons/play.svg) no-repeat center/cover;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
<template>
|
||||
<div class="folder">
|
||||
<div class="table rounded" v-if="tracks.length">
|
||||
<div class="thead">
|
||||
<div class="index"></div>
|
||||
<div class="track-header">Track</div>
|
||||
<div class="artists-header">Artist</div>
|
||||
<div class="album-header">Album</div>
|
||||
<div class="duration-header">Duration</div>
|
||||
</div>
|
||||
<div class="songlist">
|
||||
<SongItem
|
||||
v-for="track in getTracks()"
|
||||
@@ -16,7 +9,7 @@
|
||||
:index="track.index"
|
||||
@updateQueue="updateQueue"
|
||||
:isPlaying="queue.playing"
|
||||
:isCurrent="queue.current.trackid == track.trackid"
|
||||
:isCurrent="queue.currentid == track.trackid"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -54,18 +47,22 @@ let route = useRoute().name;
|
||||
* @param track Track object
|
||||
*/
|
||||
function updateQueue(track: Track) {
|
||||
const index = props.tracks.findIndex(
|
||||
(t: Track) => t.trackid === track.trackid
|
||||
);
|
||||
|
||||
switch (route) {
|
||||
case "FolderView":
|
||||
queue.playFromFolder(props.path, props.tracks);
|
||||
queue.play(track);
|
||||
queue.play(index);
|
||||
break;
|
||||
case "AlbumView":
|
||||
queue.playFromAlbum(track.album, track.albumartist, props.tracks);
|
||||
queue.play(track);
|
||||
queue.play(index);
|
||||
break;
|
||||
case "PlaylistView":
|
||||
queue.playFromPlaylist(props.pname, props.playlistid, props.tracks);
|
||||
queue.play(track);
|
||||
queue.play(index);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -116,47 +113,6 @@ function getTracks() {
|
||||
}
|
||||
}
|
||||
|
||||
.thead {
|
||||
display: grid;
|
||||
grid-template-columns: 1.5rem 1.5fr 1fr 1.5fr 0.25fr;
|
||||
height: 2.5rem;
|
||||
align-items: center;
|
||||
text-transform: uppercase;
|
||||
font-weight: bold;
|
||||
color: $gray1;
|
||||
gap: $small;
|
||||
|
||||
@include tablet-landscape {
|
||||
grid-template-columns: 1.5rem 1.5fr 1fr 1.5fr;
|
||||
}
|
||||
|
||||
@include tablet-portrait {
|
||||
grid-template-columns: 1.5rem 1.5fr 1fr;
|
||||
}
|
||||
|
||||
@include phone-only {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.duration-header {
|
||||
@include tablet-landscape {
|
||||
display: none;
|
||||
}
|
||||
|
||||
width: 6rem;
|
||||
}
|
||||
|
||||
.album-header {
|
||||
@include tablet-portrait {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.songlist {
|
||||
scrollbar-width: none;
|
||||
&::-webkit-scrollbar {
|
||||
|
||||
@@ -77,7 +77,7 @@ const menus = [
|
||||
}
|
||||
|
||||
svg > path {
|
||||
fill: $side-nav-svg;
|
||||
fill: $accent;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,14 +2,19 @@
|
||||
<div class="info">
|
||||
<div class="desc">
|
||||
<div>
|
||||
<div class="art">
|
||||
<div
|
||||
class="l-image image rounded"
|
||||
:style="{
|
||||
backgroundImage: `url("${imguri + track.image}")`,
|
||||
}"
|
||||
></div>
|
||||
</div>
|
||||
<router-link
|
||||
:to="{
|
||||
name: 'AlbumView',
|
||||
params: {
|
||||
hash: track.albumhash,
|
||||
},
|
||||
}"
|
||||
>
|
||||
<div class="art">
|
||||
<img :src="imguri + track.image" alt="" class="l-image rounded" />
|
||||
</div>
|
||||
</router-link>
|
||||
|
||||
<div id="bitrate">
|
||||
<span v-if="track.bitrate > 1500">MASTER</span>
|
||||
<span v-else-if="track.bitrate > 330">FLAC</span>
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
<template>
|
||||
<div class="l_ rounded">
|
||||
<div class="headin">Now Playing</div>
|
||||
<div class="button menu image rounded"></div>
|
||||
<div
|
||||
class="button menu rounded"
|
||||
@click="showContextMenu"
|
||||
:class="{ context_on: context_on }"
|
||||
>
|
||||
<MenuSvg />
|
||||
</div>
|
||||
<div class="separator no-border"></div>
|
||||
<div>
|
||||
<SongCard :track="queue.current" />
|
||||
<SongCard :track="queue.tracks[queue.current]" />
|
||||
<Progress />
|
||||
<HotKeys />
|
||||
</div>
|
||||
@@ -16,8 +22,38 @@ import SongCard from "./SongCard.vue";
|
||||
import HotKeys from "./NP/HotKeys.vue";
|
||||
import Progress from "./NP/Progress.vue";
|
||||
import useQStore from "../../stores/queue";
|
||||
import MenuSvg from "../../assets/icons/more.svg";
|
||||
import trackContext from "@/contexts/track_context";
|
||||
import useContextStore from "@/stores/context";
|
||||
import useModalStore from "@/stores/modal";
|
||||
import useQueueStore from "@/stores/queue";
|
||||
import { ContextSrc } from "@/composables/enums";
|
||||
|
||||
import { ref } from "vue";
|
||||
|
||||
const queue = useQStore();
|
||||
const contextStore = useContextStore();
|
||||
const context_on = ref(false);
|
||||
|
||||
const showContextMenu = (e: Event) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const menus = trackContext(
|
||||
queue.tracks[queue.current],
|
||||
useModalStore,
|
||||
useQueueStore
|
||||
);
|
||||
|
||||
contextStore.showContextMenu(e, menus, ContextSrc.Track);
|
||||
context_on.value = true;
|
||||
|
||||
contextStore.$subscribe((mutation, state) => {
|
||||
if (!state.visible) {
|
||||
context_on.value = false;
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.l_ {
|
||||
@@ -49,31 +85,26 @@ const queue = useQStore();
|
||||
}
|
||||
|
||||
.button {
|
||||
height: 2rem;
|
||||
width: 2rem;
|
||||
position: absolute;
|
||||
background-size: 1.5rem;
|
||||
top: $small;
|
||||
cursor: pointer;
|
||||
transition: all 200ms;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: $smaller;
|
||||
|
||||
&:hover {
|
||||
background-color: $gray2;
|
||||
background-color: $accent;
|
||||
}
|
||||
}
|
||||
|
||||
.context_on {
|
||||
background-color: $accent;
|
||||
}
|
||||
|
||||
.menu {
|
||||
right: $small;
|
||||
background-image: url("../../assets/icons/right-arrow.svg");
|
||||
transform: rotate(90deg);
|
||||
|
||||
&:hover {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
}
|
||||
|
||||
br {
|
||||
height: 0rem;
|
||||
}
|
||||
|
||||
.art {
|
||||
|
||||
@@ -3,81 +3,66 @@
|
||||
<div class="header">
|
||||
<div class="headin">Featured Artists</div>
|
||||
<div class="xcontrols">
|
||||
<div class="prev" @click="scrollLeft"></div>
|
||||
<div class="next" @click="scrollRight"></div>
|
||||
<div class="prev icon" @click="scrollLeft()"><ArrowSvg /></div>
|
||||
<div class="next icon" @click="scrollRight()"><ArrowSvg /></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="separator no-border"></div>
|
||||
<div class="artists" ref="artists_dom">
|
||||
<ArtistCard
|
||||
v-for="artist in artists"
|
||||
:key="artist"
|
||||
:key="artist.image"
|
||||
:artist="artist"
|
||||
:color="'ffffff00'"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
<script setup lang="ts">
|
||||
import { ref } from "@vue/reactivity";
|
||||
import ArtistCard from "@/components/shared/ArtistCard.vue";
|
||||
import { computed, reactive } from "vue";
|
||||
import { Artist } from "@/interfaces";
|
||||
import ArrowSvg from "../../assets/icons/right-arrow.svg";
|
||||
|
||||
export default {
|
||||
props: ["artists"],
|
||||
components: {
|
||||
ArtistCard,
|
||||
},
|
||||
setup() {
|
||||
const artists_dom = ref(null);
|
||||
defineProps<{
|
||||
artists: Artist[];
|
||||
}>();
|
||||
|
||||
const scrollLeft = () => {
|
||||
const dom = artists_dom.value;
|
||||
dom.scrollBy({
|
||||
left: -700,
|
||||
behavior: "smooth",
|
||||
});
|
||||
};
|
||||
const artists_dom = ref(null);
|
||||
|
||||
const scrollRight = () => {
|
||||
const dom = artists_dom.value;
|
||||
dom.scrollBy({
|
||||
left: 700,
|
||||
behavior: "smooth",
|
||||
});
|
||||
};
|
||||
const scrollLeft = () => {
|
||||
const dom = artists_dom.value;
|
||||
dom.scrollBy({
|
||||
left: -700,
|
||||
behavior: "smooth",
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
artists_dom,
|
||||
scrollLeft,
|
||||
scrollRight,
|
||||
};
|
||||
},
|
||||
const scrollRight = () => {
|
||||
const dom = artists_dom.value;
|
||||
dom.scrollBy({
|
||||
left: 700,
|
||||
behavior: "smooth",
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.f-artists {
|
||||
height: 14.5em;
|
||||
width: calc(100%);
|
||||
padding: $small;
|
||||
padding-bottom: 0;
|
||||
width: 100%;
|
||||
padding: 0 $small;
|
||||
border-radius: $small;
|
||||
user-select: none;
|
||||
background: linear-gradient(0deg, transparent, $black);
|
||||
position: relative;
|
||||
background-color: #ffffff00;
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
height: 2.5rem;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
|
||||
.headin {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 900;
|
||||
// border: solid;
|
||||
margin-left: $small;
|
||||
}
|
||||
}
|
||||
@@ -85,40 +70,31 @@ export default {
|
||||
|
||||
.f-artists .xcontrols {
|
||||
z-index: 1;
|
||||
width: 5rem;
|
||||
height: 2rem;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
&:hover {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.next {
|
||||
background: url(../../assets/icons/right-arrow.svg) no-repeat center;
|
||||
}
|
||||
gap: 1rem;
|
||||
|
||||
.prev {
|
||||
background: url(../../assets/icons/right-arrow.svg) no-repeat center;
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
.next,
|
||||
.prev {
|
||||
width: 2em;
|
||||
height: 2em;
|
||||
|
||||
.icon {
|
||||
border-radius: $small;
|
||||
cursor: pointer;
|
||||
transition: all 0.5s ease;
|
||||
background-color: rgb(51, 51, 51);
|
||||
}
|
||||
padding: $smaller;
|
||||
|
||||
.next:hover,
|
||||
.prev:hover {
|
||||
background-color: $blue;
|
||||
transition: all 0.5s ease;
|
||||
svg {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: $accent;
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,14 @@
|
||||
<div class="carddd">
|
||||
<div class="info">
|
||||
<div class="btns">
|
||||
<PlayBtnRect :source="playSources.playlist" :store="usePStore" />
|
||||
<PlayBtnRect
|
||||
:source="playSources.playlist"
|
||||
:store="usePStore"
|
||||
:background="{
|
||||
color: '#fff',
|
||||
isDark: true,
|
||||
}"
|
||||
/>
|
||||
<Option @showDropdown="showDropdown" :src="context.src" />
|
||||
</div>
|
||||
<div class="duration">
|
||||
@@ -20,7 +27,8 @@
|
||||
<span v-else-if="props.info.count == 1"
|
||||
>{{ props.info.count }} Track</span
|
||||
>
|
||||
<span v-else>{{ props.info.count }} Tracks</span> • 3 Hours
|
||||
<span v-else>{{ props.info.count }} Tracks</span> •
|
||||
{{ formatSeconds(props.info.duration, true) }}
|
||||
</div>
|
||||
<div class="desc">
|
||||
{{ props.info.description }}
|
||||
@@ -51,6 +59,7 @@ import useContextStore from "../../stores/context";
|
||||
import useModalStore from "../../stores/modal";
|
||||
import Option from "../shared/Option.vue";
|
||||
import PlayBtnRect from "../shared/PlayBtnRect.vue";
|
||||
import { formatSeconds } from "@/composables/perks";
|
||||
|
||||
const imguri = paths.images.playlist;
|
||||
const context = useContextStore();
|
||||
@@ -77,7 +86,7 @@ function showDropdown(e: any) {
|
||||
.p-header {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
height: 16rem;
|
||||
height: 17rem;
|
||||
position: relative;
|
||||
border-radius: 0.75rem;
|
||||
color: $white;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="r-home">
|
||||
<UpNext :next="queue.next" :playNext="queue.playNext" />
|
||||
<UpNext :next="queue.tracks[queue.next]" :playNext="queue.playNext" />
|
||||
<Recommendations />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -8,9 +8,8 @@
|
||||
<div class="r-search" v-show="tabs.current === tabs.tabs.search">
|
||||
<Search />
|
||||
</div>
|
||||
|
||||
<div class="r-queue" v-show="tabs.current === tabs.tabs.queue">
|
||||
<UpNext />
|
||||
<Queue />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -19,7 +18,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import Search from "./Search/Main.vue";
|
||||
import UpNext from "./Queue.vue";
|
||||
import Queue from "./Queue.vue";
|
||||
import DashBoard from "./Home/Main.vue";
|
||||
import useTabStore from "../../stores/tabs";
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="up-next">
|
||||
<div class="r-grid">
|
||||
<UpNext :next="queue.next" :playNext="queue.playNext" />
|
||||
<UpNext :next="queue.tracks[queue.next]" :playNext="queue.playNext" />
|
||||
<div class="scrollable-r border rounded">
|
||||
<div
|
||||
class="inner"
|
||||
@@ -9,11 +9,11 @@
|
||||
@mouseleave="setMouseOver(false)"
|
||||
>
|
||||
<TrackItem
|
||||
v-for="t in queue.tracks"
|
||||
v-for="(t, index) in queue.tracks"
|
||||
:key="t.trackid"
|
||||
:track="t"
|
||||
@playThis="queue.play(t)"
|
||||
:isCurrent="t.trackid === queue.current.trackid"
|
||||
@playThis="queue.play(index)"
|
||||
:isCurrent="index === queue.current"
|
||||
:isPlaying="queue.playing"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
<div id="tracks-results" v-if="search.tracks.value">
|
||||
<TransitionGroup name="list">
|
||||
<TrackItem
|
||||
v-for="track in search.tracks.value"
|
||||
v-for="(track, index) in search.tracks.value"
|
||||
:key="track.trackid"
|
||||
:track="track"
|
||||
:isPlaying="queue.playing"
|
||||
:isCurrent="queue.current.trackid == track.trackid"
|
||||
:isCurrent="queue.currentid == track.trackid"
|
||||
:isSearchTrack="true"
|
||||
@PlayThis="updateQueue"
|
||||
@PlayThis="updateQueue(index)"
|
||||
/>
|
||||
</TransitionGroup>
|
||||
<LoadMore v-if="search.tracks.more" @loadMore="loadMore" />
|
||||
@@ -30,9 +30,9 @@ function loadMore() {
|
||||
search.loadTracks(search.loadCounter.tracks);
|
||||
}
|
||||
|
||||
function updateQueue(track: Track) {
|
||||
function updateQueue(index: number) {
|
||||
queue.playFromSearch(search.query, search.tracks.value);
|
||||
queue.play(track);
|
||||
queue.play(index);
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ const context = useContextStore();
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 12rem;
|
||||
z-index: 10;
|
||||
z-index: 10000 !important;
|
||||
transform: scale(0);
|
||||
|
||||
padding: $small;
|
||||
@@ -68,12 +68,10 @@ const context = useContextStore();
|
||||
.context-item {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
cursor: default;
|
||||
padding: $small;
|
||||
border-radius: $small;
|
||||
color: rgb(255, 255, 255);
|
||||
position: relative;
|
||||
text-transform: capitalize;
|
||||
|
||||
@@ -141,7 +139,7 @@ const context = useContextStore();
|
||||
|
||||
.children {
|
||||
transform: scale(1);
|
||||
transition: transform 0.2s ease-in-out;
|
||||
transition: transform 0.1s ease-in-out;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -160,7 +158,6 @@ const context = useContextStore();
|
||||
|
||||
.context-menu-visible {
|
||||
transform: scale(1);
|
||||
transition: transform 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.context-normalizedX {
|
||||
|
||||
@@ -38,11 +38,11 @@ defineProps<{
|
||||
cursor: pointer;
|
||||
|
||||
.artist-image {
|
||||
width: 7em;
|
||||
height: 7em;
|
||||
width: 8em;
|
||||
height: 8em;
|
||||
border-radius: 60%;
|
||||
margin-bottom: $small;
|
||||
background-size: 7rem 7rem;
|
||||
background-size: 8rem 8rem;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
transition: all 0.5s ease-in-out;
|
||||
|
||||
@@ -2,8 +2,12 @@
|
||||
<div
|
||||
class="playbtnrect rounded"
|
||||
@click="usePlayFrom(source, useQStore, store)"
|
||||
:style="{
|
||||
backgroundColor: background.color,
|
||||
}"
|
||||
:class="{ playbtnrectdark: background.isDark }"
|
||||
>
|
||||
<div class="icon image"></div>
|
||||
<playBtnSvg />
|
||||
<div class="text">Play</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -11,13 +15,18 @@
|
||||
<script setup lang="ts">
|
||||
import { playSources } from "@/composables/enums";
|
||||
import usePlayFrom from "@/composables/usePlayFrom";
|
||||
import useFStore from "@/stores/folder";
|
||||
import useFStore from "@/stores/pages/folder";
|
||||
import useAStore from "@/stores/pages/album";
|
||||
import usePStore from "@/stores/pages/playlist";
|
||||
import useQStore from "@/stores/queue";
|
||||
import playBtnSvg from "@/assets/icons/play.svg";
|
||||
|
||||
defineProps<{
|
||||
source: playSources;
|
||||
background?: {
|
||||
color: string;
|
||||
isDark?: boolean;
|
||||
};
|
||||
store:
|
||||
| typeof useQStore
|
||||
| typeof useFStore
|
||||
@@ -34,8 +43,8 @@ defineProps<{
|
||||
height: 2.5rem;
|
||||
padding-left: 0.75rem;
|
||||
cursor: pointer;
|
||||
background: linear-gradient(34deg, $accent, $red);
|
||||
user-select: none;
|
||||
color: $white;
|
||||
transition: all 0.5s ease-in-out;
|
||||
|
||||
.icon {
|
||||
@@ -50,4 +59,12 @@ defineProps<{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.playbtnrectdark {
|
||||
color: $black !important;
|
||||
|
||||
svg > path {
|
||||
fill: $accent !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -7,13 +7,12 @@
|
||||
>
|
||||
<div class="index">{{ props.index }}</div>
|
||||
<div class="flex">
|
||||
<div
|
||||
class="album-art image rounded"
|
||||
:style="{
|
||||
backgroundImage: `url("${imguri + props.song.image}"`,
|
||||
}"
|
||||
@click="emitUpdate(props.song)"
|
||||
>
|
||||
<div @click="emitUpdate(props.song)" class="thumbnail">
|
||||
<img
|
||||
:src="imguri + props.song.image"
|
||||
alt=""
|
||||
class="album-art image rounded"
|
||||
/>
|
||||
<div
|
||||
class="now-playing-track image"
|
||||
v-if="props.isPlaying && props.isCurrent"
|
||||
@@ -67,17 +66,17 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { putCommas, formatSeconds } from "../../composables/perks";
|
||||
import useContextStore from "../../stores/context";
|
||||
import useModalStore from "../../stores/modal";
|
||||
import useQueueStore from "../../stores/queue";
|
||||
import { ContextSrc } from "../../composables/enums";
|
||||
import OptionSvg from "../../assets/icons/more.svg";
|
||||
import { putCommas, formatSeconds } from "@/composables/perks";
|
||||
import useContextStore from "@/stores/context";
|
||||
import useModalStore from "@/stores/modal";
|
||||
import useQueueStore from "@/stores/queue";
|
||||
import { ContextSrc } from "@/composables/enums";
|
||||
import OptionSvg from "@/assets/icons/more.svg";
|
||||
|
||||
import { ref } from "vue";
|
||||
import trackContext from "../../contexts/track_context";
|
||||
import { Track } from "../../interfaces";
|
||||
import { paths } from "../../config";
|
||||
import trackContext from "@/contexts/track_context";
|
||||
import { Track } from "@/interfaces";
|
||||
import { paths } from "@/config";
|
||||
|
||||
const contextStore = useContextStore();
|
||||
|
||||
@@ -122,15 +121,14 @@ function emitUpdate(track: Track) {
|
||||
.songlist-item {
|
||||
display: grid;
|
||||
align-items: center;
|
||||
grid-template-columns: 1.5rem 1.5fr 1fr 1.5fr 0.25fr 2.5rem;
|
||||
grid-template-columns: 1.5rem 1.5fr 1fr 1.5fr 2rem 2.5rem;
|
||||
height: 3.75rem;
|
||||
text-align: left;
|
||||
gap: $small;
|
||||
user-select: none;
|
||||
-moz-user-select: none;
|
||||
|
||||
@include tablet-landscape {
|
||||
grid-template-columns: 1.5rem 1.5fr 1fr 1.5fr 2.5rem;
|
||||
grid-template-columns: 1.5rem 1.5fr 1fr 1fr 2.5rem;
|
||||
}
|
||||
|
||||
@include tablet-portrait {
|
||||
@@ -157,6 +155,10 @@ function emitUpdate(track: Track) {
|
||||
max-width: max-content;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@include tablet-portrait {
|
||||
display: none;
|
||||
}
|
||||
@@ -189,7 +191,7 @@ function emitUpdate(track: Track) {
|
||||
font-size: 0.9rem;
|
||||
width: 5rem !important;
|
||||
|
||||
text-align: right;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.options-icon {
|
||||
@@ -223,20 +225,25 @@ function emitUpdate(track: Track) {
|
||||
|
||||
.flex {
|
||||
position: relative;
|
||||
padding-left: 4rem;
|
||||
align-items: center;
|
||||
|
||||
.thumbnail {
|
||||
margin-right: $small;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.album-art {
|
||||
position: absolute;
|
||||
left: $small;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
margin-right: 1rem;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.now-playing-track {
|
||||
position: absolute;
|
||||
left: $small;
|
||||
top: $small;
|
||||
}
|
||||
|
||||
.title {
|
||||
cursor: pointer;
|
||||
word-break: break-all;
|
||||
|
||||
@@ -74,11 +74,11 @@ const showContextMenu = (e: Event) => {
|
||||
};
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "PlayThis", track: Track): void;
|
||||
(e: "PlayThis"): void;
|
||||
}>();
|
||||
|
||||
const playThis = (track: Track) => {
|
||||
emit("PlayThis", track);
|
||||
emit("PlayThis");
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ const getAlbumData = async (hash: string, ToastStore: typeof useNotifStore) => {
|
||||
info: {
|
||||
album: "",
|
||||
artist: "",
|
||||
colors: []
|
||||
},
|
||||
tracks: [],
|
||||
};
|
||||
|
||||
@@ -37,9 +37,12 @@ function getElem(id: string, type: string) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts seconds into minutes and hours.
|
||||
* @param seconds The seconds to convert
|
||||
* @param long Whether to provide the time in the long format
|
||||
*/
|
||||
function formatSeconds(seconds: number, long?: boolean) {
|
||||
// check if there are arguments
|
||||
|
||||
const date = new Date(seconds * 1000);
|
||||
|
||||
const hh = date.getUTCHours();
|
||||
|
||||
@@ -9,29 +9,6 @@ const uris = {
|
||||
artists: `${base_url}/artists?q=`,
|
||||
};
|
||||
|
||||
async function search(query: string) {
|
||||
state.loading.value = true;
|
||||
|
||||
const url = base_url + encodeURIComponent(query.trim());
|
||||
|
||||
const res = await fetch(url);
|
||||
|
||||
if (!res.ok) {
|
||||
const message = `An error has occured: ${res.status}`;
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
state.loading.value = false;
|
||||
|
||||
return {
|
||||
tracks: data.data[0],
|
||||
albums: data.data[1],
|
||||
artists: data.data[2],
|
||||
};
|
||||
}
|
||||
|
||||
async function searchTracks(query: string) {
|
||||
const url = uris.tracks + encodeURIComponent(query.trim());
|
||||
|
||||
@@ -43,7 +20,6 @@ async function searchTracks(query: string) {
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
console.log(data);
|
||||
|
||||
return data;
|
||||
}
|
||||
@@ -107,4 +83,4 @@ export {
|
||||
};
|
||||
|
||||
// TODO:
|
||||
// Rewrite this module using `useAxios` hook
|
||||
// Rewrite this module using `useAxios` hook
|
||||
|
||||
@@ -2,56 +2,12 @@ import { Track, AlbumInfo, Artist } from "./../interfaces";
|
||||
import { ref } from "@vue/reactivity";
|
||||
import { reactive } from "vue";
|
||||
|
||||
const search_query = ref("");
|
||||
|
||||
const queue = ref(
|
||||
Array<Track>({
|
||||
title: "Nothing played yet",
|
||||
artists: ["... blah blah blah"],
|
||||
image: "http://127.0.0.1:8900/images/thumbnails/4.webp",
|
||||
trackid: "",
|
||||
})
|
||||
);
|
||||
|
||||
const folder_song_list = ref([]);
|
||||
const folder_list = ref([]);
|
||||
|
||||
const current = ref(<Track>{
|
||||
title: "Nothing played yet",
|
||||
artists: ["... blah blah blah"],
|
||||
image: "http://127.0.0.1:8900/images/thumbnails/4.webp",
|
||||
trackid: "",
|
||||
});
|
||||
|
||||
const prev = ref(<Track>{
|
||||
title: "Nothing played yet",
|
||||
artists: ["... blah blah blah"],
|
||||
image: "http://127.0.0.1:8900/images/thumbnails/4.webp",
|
||||
trackid: "",
|
||||
});
|
||||
|
||||
const album = reactive({
|
||||
tracklist: Array<Track>(),
|
||||
info: <AlbumInfo>{},
|
||||
artists: Array<Artist>(),
|
||||
bio: "",
|
||||
});
|
||||
|
||||
const loading = ref(false);
|
||||
const is_playing = ref(false);
|
||||
const settings = reactive({
|
||||
uri: "http://127.0.0.1:9876",
|
||||
});
|
||||
|
||||
export default {
|
||||
search_query,
|
||||
queue,
|
||||
folder_song_list,
|
||||
folder_list,
|
||||
current,
|
||||
prev,
|
||||
loading,
|
||||
is_playing,
|
||||
album,
|
||||
settings,
|
||||
};
|
||||
|
||||
@@ -25,14 +25,14 @@ export default function play(
|
||||
const f = store();
|
||||
|
||||
useQueue.playFromFolder(f.path, f.tracks);
|
||||
useQueue.play(f.tracks[0]);
|
||||
useQueue.play();
|
||||
break;
|
||||
case playSources.album:
|
||||
store = store as typeof album;
|
||||
const a = store();
|
||||
|
||||
useQueue.playFromAlbum(a.info.title, a.info.artist, a.tracks);
|
||||
useQueue.play(store().tracks[0]);
|
||||
useQueue.play();
|
||||
break;
|
||||
case playSources.playlist:
|
||||
store = store as typeof playlist;
|
||||
@@ -41,7 +41,7 @@ export default function play(
|
||||
if (p.tracks.length === 0) return;
|
||||
|
||||
useQueue.playFromPlaylist(p.info.name, p.info.playlistid, p.tracks);
|
||||
useQueue.play(store().tracks[0]);
|
||||
useQueue.play();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
+5
-2
@@ -15,7 +15,7 @@ export interface Track {
|
||||
image: string;
|
||||
tracknumber?: number;
|
||||
disknumber?: number;
|
||||
index?: number
|
||||
index?: number;
|
||||
}
|
||||
|
||||
export interface Folder {
|
||||
@@ -27,6 +27,7 @@ export interface Folder {
|
||||
}
|
||||
|
||||
export interface AlbumInfo {
|
||||
albumid: string;
|
||||
title: string;
|
||||
artist: string;
|
||||
count: number;
|
||||
@@ -36,7 +37,8 @@ export interface AlbumInfo {
|
||||
is_compilation: boolean;
|
||||
is_soundtrack: boolean;
|
||||
is_single: boolean;
|
||||
hash: string
|
||||
hash: string;
|
||||
colors: string[];
|
||||
}
|
||||
|
||||
export interface Artist {
|
||||
@@ -62,6 +64,7 @@ export interface Playlist {
|
||||
count?: number;
|
||||
lastUpdated?: string;
|
||||
thumb?: string;
|
||||
duration?: number
|
||||
}
|
||||
|
||||
export interface Notif {
|
||||
|
||||
+10
-13
@@ -1,20 +1,17 @@
|
||||
import { createRouter, createWebHashHistory } from "vue-router";
|
||||
import Home from "@/views/Home.vue";
|
||||
import FolderView from "@/views/FolderView.vue";
|
||||
import PlaylistView from "@/views/PlaylistView.vue";
|
||||
import Playlists from "@/views/Playlists.vue";
|
||||
|
||||
import state from "@/composables/state";
|
||||
import useAStore from "@/stores/pages/album";
|
||||
import useFStore from "@/stores/pages/folder";
|
||||
import usePTrackStore from "@/stores/pages/playlist";
|
||||
import usePStore from "@/stores/pages/playlists";
|
||||
import AlbumsExplorer from "@/views/AlbumsExplorer.vue";
|
||||
import AlbumView from "@/views/AlbumView.vue";
|
||||
|
||||
import ArtistsExplorer from "@/views/ArtistsExplorer.vue";
|
||||
import FolderView from "@/views/FolderView.vue";
|
||||
import Home from "@/views/Home.vue";
|
||||
import Playlists from "@/views/Playlists.vue";
|
||||
import PlaylistView from "@/views/PlaylistView.vue";
|
||||
import SettingsView from "@/views/SettingsView.vue";
|
||||
|
||||
import usePStore from "@/stores/pages/playlists";
|
||||
import usePTrackStore from "@/stores/pages/playlist";
|
||||
import useFStore from "@/stores/pages/folder";
|
||||
import useAStore from "@/stores/pages/album";
|
||||
import state from "@/composables/state";
|
||||
import { createRouter, createWebHashHistory } from "vue-router";
|
||||
|
||||
const routes = [
|
||||
{
|
||||
|
||||
+33
-42
@@ -25,20 +25,22 @@ function writeQueue(
|
||||
);
|
||||
}
|
||||
|
||||
function writeCurrent(track: Track) {
|
||||
localStorage.setItem("current", JSON.stringify(track));
|
||||
function writeCurrent(index: number) {
|
||||
localStorage.setItem("current", JSON.stringify(index));
|
||||
}
|
||||
|
||||
function readCurrent(): Track {
|
||||
function readCurrent(): number {
|
||||
const current = localStorage.getItem("current");
|
||||
|
||||
if (current) {
|
||||
return JSON.parse(current);
|
||||
}
|
||||
return defaultTrack;
|
||||
return 0;
|
||||
}
|
||||
|
||||
const defaultTrack = <Track>{
|
||||
title: "Nothing played yet",
|
||||
albumhash: " ",
|
||||
artists: ["Alice"],
|
||||
trackid: "",
|
||||
image: "",
|
||||
@@ -52,18 +54,22 @@ export default defineStore("Queue", {
|
||||
current_time: 0,
|
||||
duration: 0,
|
||||
},
|
||||
current: <Track>{},
|
||||
next: <Track>{},
|
||||
prev: <Track>{},
|
||||
current: 0,
|
||||
next: 0,
|
||||
prev: 0,
|
||||
currentid: "",
|
||||
playing: false,
|
||||
from: <fromFolder>{} || <fromAlbum>{} || <fromPlaylist>{},
|
||||
tracks: <Track[]>[defaultTrack],
|
||||
}),
|
||||
actions: {
|
||||
play(track: Track) {
|
||||
play(index: number = 0) {
|
||||
const track = this.tracks[index];
|
||||
this.current = index;
|
||||
this.currentid = track.trackid;
|
||||
const uri = state.settings.uri + "/file/" + track.trackid;
|
||||
const elem = document.getElementById("progress");
|
||||
this.updateCurrent(track);
|
||||
this.updateCurrent(index);
|
||||
|
||||
new Promise((resolve, reject) => {
|
||||
this.audio.autoplay = true;
|
||||
@@ -130,39 +136,27 @@ export default defineStore("Queue", {
|
||||
|
||||
this.updateCurrent(readCurrent());
|
||||
},
|
||||
updateCurrent(track: Track) {
|
||||
this.current = track;
|
||||
updateCurrent(index: number) {
|
||||
this.updateNext(index);
|
||||
this.updatePrev(index);
|
||||
|
||||
this.updateNext(this.current);
|
||||
this.updatePrev(this.current);
|
||||
|
||||
writeCurrent(track);
|
||||
writeCurrent(index);
|
||||
},
|
||||
updateNext(track: Track) {
|
||||
const index = this.tracks.findIndex(
|
||||
(t: Track) => t.trackid == track.trackid
|
||||
);
|
||||
|
||||
updateNext(index: number) {
|
||||
if (index == this.tracks.length - 1) {
|
||||
this.next = this.tracks[0];
|
||||
} else if (index == 0) {
|
||||
this.next = this.tracks[1];
|
||||
} else {
|
||||
this.next = this.tracks[index + 1];
|
||||
this.next = 0;
|
||||
return;
|
||||
}
|
||||
},
|
||||
updatePrev(track: Track) {
|
||||
const index = this.tracks.findIndex(
|
||||
(t: Track) => t.trackid === track.trackid
|
||||
);
|
||||
|
||||
this.next = index + 1;
|
||||
},
|
||||
updatePrev(index: number) {
|
||||
if (index === 0) {
|
||||
this.prev = this.tracks[this.tracks.length - 1];
|
||||
} else if (index === this.tracks.length - 1) {
|
||||
this.prev = this.tracks[index - 1];
|
||||
} else {
|
||||
this.prev = this.tracks[index - 1];
|
||||
this.prev = this.tracks.length - 1;
|
||||
return;
|
||||
}
|
||||
|
||||
this.prev = index - 1;
|
||||
},
|
||||
setNewQueue(tracklist: Track[]) {
|
||||
if (this.tracks !== tracklist) {
|
||||
@@ -212,14 +206,11 @@ export default defineStore("Queue", {
|
||||
},
|
||||
playTrackNext(track: Track) {
|
||||
const Toast = useNotifStore();
|
||||
const currentid = this.tracks.findIndex(
|
||||
(t: Track) => t.trackid === this.current.trackid
|
||||
);
|
||||
|
||||
if (currentid == this.tracks.length - 1) {
|
||||
if (this.current == this.tracks.length - 1) {
|
||||
this.tracks.push(track);
|
||||
} else {
|
||||
const next: Track = this.tracks[currentid + 1];
|
||||
const nextindex = this.current + 1;
|
||||
const next: Track = this.tracks[nextindex];
|
||||
|
||||
if (next.trackid === track.trackid) {
|
||||
Toast.showNotification("Track is already queued", NotifType.Info);
|
||||
@@ -227,7 +218,7 @@ export default defineStore("Queue", {
|
||||
}
|
||||
}
|
||||
|
||||
this.tracks.splice(currentid + 1, 0, track);
|
||||
this.tracks.splice(this.current + 1, 0, track);
|
||||
this.updateNext(this.current);
|
||||
Toast.showNotification(
|
||||
`Added ${track.title} to queue`,
|
||||
|
||||
+1
-1
@@ -3,8 +3,8 @@ import { focusElem } from "../composables/perks";
|
||||
|
||||
const tablist = {
|
||||
home: "home",
|
||||
search: "search",
|
||||
queue: "queue",
|
||||
search: "search",
|
||||
};
|
||||
|
||||
export default defineStore("tabs", {
|
||||
|
||||
+143
-14
@@ -1,17 +1,35 @@
|
||||
<template>
|
||||
<div class="al-view rounded">
|
||||
<div>
|
||||
<Header :album="album.info" />
|
||||
</div>
|
||||
<div class="separator" id="av-sep"></div>
|
||||
<div class="songs rounded">
|
||||
<SongList :tracks="album.tracks" :on_album_page="true"/>
|
||||
</div>
|
||||
<div class="separator" id="av-sep"></div>
|
||||
<FeaturedArtists :artists="album.artists" />
|
||||
<div v-if="album.bio">
|
||||
<div class="separator" id="av-sep"></div>
|
||||
<AlbumBio :bio="album.bio" />
|
||||
<div class="al-content" id="albumcontent">
|
||||
<div>
|
||||
<Header :album="album.info" @resetBottomPadding="resetBottomPadding" />
|
||||
</div>
|
||||
<div class="songs rounded">
|
||||
<SongList :tracks="album.tracks" :on_album_page="true" />
|
||||
</div>
|
||||
<div
|
||||
id="bottom-items"
|
||||
class="rounded"
|
||||
ref="albumbottomcards"
|
||||
:class="{
|
||||
bottomexpanded: bottomContainerRaised,
|
||||
}"
|
||||
>
|
||||
<div class="click-to-expand" @click="toggleBottom">
|
||||
<div>
|
||||
<div class="arrow">↑</div>
|
||||
<span>tap here</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bottom-content">
|
||||
<FeaturedArtists :artists="album.artists" />
|
||||
<div v-if="album.bio">
|
||||
<div class="separator" id="av-sep"></div>
|
||||
<AlbumBio :bio="album.bio" />
|
||||
</div>
|
||||
<div class="dummy"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -22,25 +40,76 @@ import AlbumBio from "../components/AlbumView/AlbumBio.vue";
|
||||
|
||||
import SongList from "../components/FolderView/SongList.vue";
|
||||
import FeaturedArtists from "../components/PlaylistView/FeaturedArtists.vue";
|
||||
|
||||
import useAStore from "../stores/pages/album";
|
||||
import { onBeforeRouteUpdate } from "vue-router";
|
||||
import { onMounted, ref } from "vue";
|
||||
|
||||
const album = useAStore();
|
||||
const albumbottomcards = ref<HTMLElement>(null);
|
||||
const bottomContainerRaised = ref(false);
|
||||
let elem: HTMLElement = null;
|
||||
let classlist: DOMTokenList = null;
|
||||
|
||||
onMounted(() => {
|
||||
elem = document.getElementById("albumcontent");
|
||||
classlist = elem.classList;
|
||||
});
|
||||
|
||||
onBeforeRouteUpdate(async (to) => {
|
||||
await album.fetchTracksAndArtists(to.params.hash.toString());
|
||||
album.fetchBio(to.params.hash.toString());
|
||||
});
|
||||
|
||||
/**
|
||||
* Toggles the state of the bottom container. Adds the `addbottompadding` class that adds a bottom padding to the album content div.
|
||||
*/
|
||||
function toggleBottom() {
|
||||
bottomContainerRaised.value = !bottomContainerRaised.value;
|
||||
|
||||
if (bottomContainerRaised.value) {
|
||||
classlist.add("addbottompadding");
|
||||
return;
|
||||
}
|
||||
|
||||
if (elem.scrollTop == 0) {
|
||||
classlist.remove("addbottompadding");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the album page header gets into the viewport.
|
||||
* Removes the bottom padding which was added when you expand the bottom container.
|
||||
*/
|
||||
function resetBottomPadding() {
|
||||
if (bottomContainerRaised.value) return;
|
||||
|
||||
classlist.remove("addbottompadding");
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.al-view {
|
||||
scrollbar-width: none;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
margin-right: -$small;
|
||||
overflow: hidden;
|
||||
|
||||
.al-content {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
padding-bottom: 17rem;
|
||||
padding-right: $small;
|
||||
transition: all 0.5s;
|
||||
z-index: -1 !important;
|
||||
}
|
||||
|
||||
.addbottompadding {
|
||||
padding-bottom: 37rem;
|
||||
}
|
||||
|
||||
.songs {
|
||||
min-height: calc(100% - 31.5rem);
|
||||
margin-top: $small;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
@@ -50,5 +119,65 @@ onBeforeRouteUpdate(async (to) => {
|
||||
#av-sep {
|
||||
border: none;
|
||||
}
|
||||
|
||||
#bottom-items {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: calc(100% - $small);
|
||||
height: 15rem;
|
||||
background-color: $gray;
|
||||
transition: all 0.5s ease;
|
||||
overscroll-behavior: contain;
|
||||
display: grid;
|
||||
grid-template-rows: 2rem 1fr;
|
||||
|
||||
.bottom-content {
|
||||
overflow: hidden;
|
||||
scroll-behavior: contain;
|
||||
}
|
||||
|
||||
.click-to-expand {
|
||||
height: 1.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: $gray1;
|
||||
|
||||
div {
|
||||
margin: 0 auto;
|
||||
font-size: small;
|
||||
cursor: default;
|
||||
user-select: none;
|
||||
display: flex;
|
||||
gap: $small;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
max-width: min-content;
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: $accent !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bottomexpanded {
|
||||
height: 32rem !important;
|
||||
scroll-behavior: contain;
|
||||
|
||||
.arrow {
|
||||
transform: rotate(180deg) !important;
|
||||
}
|
||||
|
||||
.bottom-content {
|
||||
overflow: auto !important;
|
||||
scrollbar-width: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
- [ ] Add and deploy demo branch
|
||||
|
||||
### Client
|
||||
- [ ] Add processes tab to show running tasks, eg. when tagging files. I have no idea on how to go about it so far. Web sockets?
|
||||
- [ ] Add processes tab to show running tasks, eg. when tagging files. I have no idea on how to go about it so far. Web sockets?
|
||||
- [ ] Responsiveness, especially the track list.
|
||||
- [ ] Make dummy buttons functional.
|
||||
- [ ] Add settings page (or modal)
|
||||
@@ -21,4 +21,4 @@
|
||||
|
||||
|
||||
|
||||
- Resolve album page using albumhash instead of album title and artist
|
||||
- Resolve album page using albumhash instead of album title and artist
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
import { defineConfig } from "vite";
|
||||
import svgLoader from 'vite-svg-loader'
|
||||
import vue from "@vitejs/plugin-vue";
|
||||
import { defineConfig } from "vite";
|
||||
import svgLoader from "vite-svg-loader";
|
||||
|
||||
const path = require("path");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user