mirror of
https://github.com/Dvorinka/swingmusic-extended.git
synced 2026-06-05 04:53:01 +00:00
Merge pull request #25 from geoffrey45/add-edit-playlist-modal
Add edit playlist modal
This commit is contained in:
@@ -1,5 +1,50 @@
|
|||||||
### Alice Music
|
### Alice Music
|
||||||
|
|
||||||
|
Alice is a web-based music manager (or basically a music player) that will make it easier to find and enjoy your music. Currently in the early stages of development.
|
||||||
|
|
||||||
|
Although it's quite usable, it's not quite ready for use yet. I'm working on getting done with some dev setup instructions. So, check back soon!
|
||||||
|
|
||||||
|
Assuming you won't look at those broken buttons twice, here are some screenshots of the current state of the app:
|
||||||
|
|
||||||
|
### 1. The folder page
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
`installation instructions coming soon ...`
|
This pages allows you to navigate through your music folders the same way you would on your computer. It doesn't show the directories without playable files. At this point, only FLAC and MP3 files are supported. (For experimenting purposes only, other formats will be added before the stable release)
|
||||||
|
|
||||||
|
### 2. The Album page
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
This page shows the album details including tracks, album artist, featured artists and Last FM album bio. The UI may look a bit rough but it will do for now.
|
||||||
|
|
||||||
|
### 3. The Playlists list page
|
||||||
|
|
||||||
|

|
||||||
|
This page lists all the playlists you have created. More features will be added to this page in the future. These "might" include Folders, Search, etc.
|
||||||
|
|
||||||
|
### 4. The Playlist page
|
||||||
|
|
||||||
|

|
||||||
|
This page shows the details of the playlist. It includes the playlist name, description, and the songs in the playlist. You can update your playlist details from this page.
|
||||||
|
|
||||||
|
### A little narration
|
||||||
|
|
||||||
|
The app features two sidebars. The one on the left and one on the right. The left sidebar is the classic navigation bar while the right sidebar acts as a quick access menu. The queue and the global search components are fixed here. Although they might switch to other locations in the future, the current position will work for now.
|
||||||
|
|
||||||
|
Here are some other functional features already implemented:
|
||||||
|
|
||||||
|
- Track context menu
|
||||||
|
- Global search (😅 buggy as fuck)
|
||||||
|
- Basic playback controls
|
||||||
|
- Queue saving on browser or page reload.
|
||||||
|
|
||||||
|
There may be a few more, but I can't remember them at the moment.
|
||||||
|
|
||||||
|
### Dev Setup
|
||||||
|
|
||||||
|
I'm working on this section. I'll be adding instructions soon. Please check back later!
|
||||||
|
|
||||||
|
### Contributing
|
||||||
|
|
||||||
|
The app runs on Python, Vue, Flask, MongoDB and Nginx. If you want to contribute, please open an issue or pull request. Your contribution is highly valued.
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 553 KiB After Width: | Height: | Size: 331 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 860 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 616 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 587 KiB |
+64
-25
@@ -1,13 +1,16 @@
|
|||||||
"""
|
"""
|
||||||
Contains all the playlist routes.
|
Contains all the playlist routes.
|
||||||
"""
|
"""
|
||||||
from copy import deepcopy
|
from datetime import datetime
|
||||||
from typing import List
|
|
||||||
from flask import Blueprint, request
|
from app import api
|
||||||
from app import instances, api
|
|
||||||
from app.lib import playlistlib
|
|
||||||
from app import models
|
|
||||||
from app import exceptions
|
from app import exceptions
|
||||||
|
from app import instances
|
||||||
|
from app import models
|
||||||
|
from app import serializer
|
||||||
|
from app.lib import playlistlib
|
||||||
|
from flask import Blueprint
|
||||||
|
from flask import request
|
||||||
|
|
||||||
playlist_bp = Blueprint("playlist", __name__, url_prefix="/")
|
playlist_bp = Blueprint("playlist", __name__, url_prefix="/")
|
||||||
|
|
||||||
@@ -17,14 +20,14 @@ TrackExistsInPlaylist = exceptions.TrackExistsInPlaylist
|
|||||||
|
|
||||||
@playlist_bp.route("/playlists", methods=["GET"])
|
@playlist_bp.route("/playlists", methods=["GET"])
|
||||||
def get_all_playlists():
|
def get_all_playlists():
|
||||||
ppp = deepcopy(api.PLAYLISTS)
|
playlists = [
|
||||||
playlists = []
|
serializer.Playlist(p, construct_last_updated=False)
|
||||||
|
for p in api.PLAYLISTS
|
||||||
for pl in ppp:
|
]
|
||||||
pl.count = len(pl.tracks)
|
playlists.sort(
|
||||||
pl.tracks = []
|
key=lambda p: datetime.strptime(p.lastUpdated, "%Y-%m-%d %H:%M:%S"),
|
||||||
playlists.append(pl)
|
reverse=True,
|
||||||
|
)
|
||||||
return {"data": playlists}
|
return {"data": playlists}
|
||||||
|
|
||||||
|
|
||||||
@@ -35,16 +38,16 @@ def create_playlist():
|
|||||||
playlist = {
|
playlist = {
|
||||||
"name": data["name"],
|
"name": data["name"],
|
||||||
"description": "",
|
"description": "",
|
||||||
"tracks": [],
|
"pre_tracks": [],
|
||||||
"count": 0,
|
"lastUpdated": data["lastUpdated"],
|
||||||
"lastUpdated": 0,
|
"image": "",
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
p_in_db = instances.playlist_instance.get_playlist_by_name(playlist["name"])
|
for pl in api.PLAYLISTS:
|
||||||
|
if pl.name == playlist["name"]:
|
||||||
|
raise PlaylistExists("Playlist already exists.")
|
||||||
|
|
||||||
if p_in_db:
|
|
||||||
raise PlaylistExists("Playlist already exists.")
|
|
||||||
except PlaylistExists as e:
|
except PlaylistExists as e:
|
||||||
return {"error": str(e)}, 409
|
return {"error": str(e)}, 409
|
||||||
|
|
||||||
@@ -71,12 +74,48 @@ def add_track_to_playlist(playlist_id: str):
|
|||||||
return {"msg": "I think It's done"}, 200
|
return {"msg": "I think It's done"}, 200
|
||||||
|
|
||||||
|
|
||||||
@playlist_bp.route("/playlist/<playlist_id>")
|
@playlist_bp.route("/playlist/<playlistid>")
|
||||||
def get_single_p_info(playlist_id: str):
|
def get_single_p_info(playlistid: str):
|
||||||
for p in api.PLAYLISTS:
|
for p in api.PLAYLISTS:
|
||||||
if p.playlistid == playlist_id:
|
if p.playlistid == playlistid:
|
||||||
p.count = len(p.tracks)
|
tracks = p.get_tracks()
|
||||||
return {"data": p}
|
return {
|
||||||
|
"info": serializer.Playlist(p),
|
||||||
|
"tracks": tracks,
|
||||||
|
}
|
||||||
|
|
||||||
|
return {"info": {}, "tracks": []}
|
||||||
|
|
||||||
|
|
||||||
|
@playlist_bp.route("/playlist/<playlistid>/update", methods=["PUT"])
|
||||||
|
def update_playlist(playlistid: str):
|
||||||
|
image = None
|
||||||
|
|
||||||
|
if "image" in request.files:
|
||||||
|
image = request.files["image"]
|
||||||
|
|
||||||
|
data = request.form
|
||||||
|
|
||||||
|
playlist = {
|
||||||
|
"name": str(data.get("name")).strip(),
|
||||||
|
"description": str(data.get("description").strip()),
|
||||||
|
"lastUpdated": str(data.get("lastUpdated")),
|
||||||
|
"image": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
if image:
|
||||||
|
playlist["image"] = playlistlib.save_p_image(image, playlistid)
|
||||||
|
|
||||||
|
for p in api.PLAYLISTS:
|
||||||
|
if p.playlistid == playlistid:
|
||||||
|
p.update_playlist(playlist)
|
||||||
|
instances.playlist_instance.update_playlist(playlistid, playlist)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"data": serializer.Playlist(p),
|
||||||
|
}
|
||||||
|
|
||||||
|
return {"msg": "Something shady happened"}, 500
|
||||||
|
|
||||||
|
|
||||||
# @playlist_bp.route("/playlist/<playlist_id>/info")
|
# @playlist_bp.route("/playlist/<playlist_id>/info")
|
||||||
|
|||||||
+22
-13
@@ -1,15 +1,14 @@
|
|||||||
"""
|
"""
|
||||||
This file contains the Playlists class for interacting with the playlist documents in MongoDB.
|
This file contains the Playlists class for interacting with the playlist documents in MongoDB.
|
||||||
"""
|
"""
|
||||||
|
from app import db
|
||||||
from app import db, models
|
from app import models
|
||||||
from bson import ObjectId
|
from bson import ObjectId
|
||||||
|
|
||||||
convert_many = db.convert_many
|
convert_many = db.convert_many
|
||||||
convert_one = db.convert_one
|
convert_one = db.convert_one
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Playlists(db.Mongo):
|
class Playlists(db.Mongo):
|
||||||
"""
|
"""
|
||||||
The class for all playlist-related database operations.
|
The class for all playlist-related database operations.
|
||||||
@@ -24,8 +23,12 @@ class Playlists(db.Mongo):
|
|||||||
Inserts a new playlist object into the database.
|
Inserts a new playlist object into the database.
|
||||||
"""
|
"""
|
||||||
return self.collection.update_one(
|
return self.collection.update_one(
|
||||||
{"name": playlist["name"]},
|
{
|
||||||
{"$set": playlist},
|
"name": playlist["name"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$set": playlist
|
||||||
|
},
|
||||||
upsert=True,
|
upsert=True,
|
||||||
).upserted_id
|
).upserted_id
|
||||||
|
|
||||||
@@ -43,20 +46,17 @@ class Playlists(db.Mongo):
|
|||||||
playlist = self.collection.find_one({"_id": ObjectId(id)})
|
playlist = self.collection.find_one({"_id": ObjectId(id)})
|
||||||
return convert_one(playlist)
|
return convert_one(playlist)
|
||||||
|
|
||||||
def add_track_to_playlist(self, playlistid: str, track: models.Track):
|
def add_track_to_playlist(self, playlistid: str, track: dict) -> None:
|
||||||
"""
|
"""
|
||||||
Adds a track to a playlist.
|
Adds a track to a playlist.
|
||||||
"""
|
"""
|
||||||
track = {
|
|
||||||
"title": track.title,
|
|
||||||
"artists": track.artists,
|
|
||||||
"album": track.album,
|
|
||||||
}
|
|
||||||
|
|
||||||
return self.collection.update_one(
|
return self.collection.update_one(
|
||||||
{"_id": ObjectId(playlistid)},
|
{"_id": ObjectId(playlistid)},
|
||||||
{"$push": {"tracks": track}},
|
{"$push": {
|
||||||
).modified_count
|
"pre_tracks": track
|
||||||
|
}},
|
||||||
|
)
|
||||||
|
|
||||||
def get_playlist_by_name(self, name: str) -> dict:
|
def get_playlist_by_name(self, name: str) -> dict:
|
||||||
"""
|
"""
|
||||||
@@ -64,3 +64,12 @@ class Playlists(db.Mongo):
|
|||||||
"""
|
"""
|
||||||
playlist = self.collection.find_one({"name": name})
|
playlist = self.collection.find_one({"name": name})
|
||||||
return convert_one(playlist)
|
return convert_one(playlist)
|
||||||
|
|
||||||
|
def update_playlist(self, playlistid: str, playlist: dict) -> None:
|
||||||
|
"""
|
||||||
|
Updates a playlist.
|
||||||
|
"""
|
||||||
|
return self.collection.update_one(
|
||||||
|
{"_id": ObjectId(playlistid)},
|
||||||
|
{"$set": playlist},
|
||||||
|
)
|
||||||
|
|||||||
@@ -81,7 +81,6 @@ def populate():
|
|||||||
|
|
||||||
albumslib.create_everything()
|
albumslib.create_everything()
|
||||||
folderslib.run_scandir()
|
folderslib.run_scandir()
|
||||||
playlistlib.create_all_playlists()
|
|
||||||
|
|
||||||
end = time.time()
|
end = time.time()
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,20 @@
|
|||||||
"""
|
"""
|
||||||
This library contains all the functions related to playlists.
|
This library contains all the functions related to playlists.
|
||||||
"""
|
"""
|
||||||
from progress.bar import Bar
|
import os
|
||||||
from app import api, instances, models, exceptions, helpers
|
import random
|
||||||
|
import string
|
||||||
|
|
||||||
|
from app import api
|
||||||
|
from app import exceptions
|
||||||
|
from app import instances
|
||||||
|
from app import models
|
||||||
|
from app import settings
|
||||||
from app.lib import trackslib
|
from app.lib import trackslib
|
||||||
|
from PIL import Image
|
||||||
|
from PIL import ImageSequence
|
||||||
|
from progress.bar import Bar
|
||||||
|
from werkzeug import datastructures
|
||||||
|
|
||||||
TrackExistsInPlaylist = exceptions.TrackExistsInPlaylist
|
TrackExistsInPlaylist = exceptions.TrackExistsInPlaylist
|
||||||
|
|
||||||
@@ -15,14 +25,21 @@ def add_track(playlistid: str, trackid: str):
|
|||||||
"""
|
"""
|
||||||
for playlist in api.PLAYLISTS:
|
for playlist in api.PLAYLISTS:
|
||||||
if playlist.playlistid == playlistid:
|
if playlist.playlistid == playlistid:
|
||||||
track = trackslib.get_track_by_id(trackid)
|
tt = trackslib.get_track_by_id(trackid)
|
||||||
|
|
||||||
if track not in playlist.tracks:
|
track = {
|
||||||
playlist.tracks.append(track)
|
"title": tt.title,
|
||||||
instances.playlist_instance.add_track_to_playlist(playlistid, track)
|
"artists": tt.artists,
|
||||||
|
"album": tt.album,
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
playlist.add_track(track)
|
||||||
|
instances.playlist_instance.add_track_to_playlist(
|
||||||
|
playlistid, track)
|
||||||
return
|
return
|
||||||
else:
|
except TrackExistsInPlaylist as e:
|
||||||
raise TrackExistsInPlaylist("Track already in playlist.")
|
return {"error": str(e)}, 409
|
||||||
|
|
||||||
|
|
||||||
def get_playlist_tracks(pid: str):
|
def get_playlist_tracks(pid: str):
|
||||||
@@ -31,7 +48,6 @@ def get_playlist_tracks(pid: str):
|
|||||||
return p.tracks
|
return p.tracks
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def create_all_playlists():
|
def create_all_playlists():
|
||||||
"""
|
"""
|
||||||
Gets all playlists from the database.
|
Gets all playlists from the database.
|
||||||
@@ -41,5 +57,32 @@ def create_all_playlists():
|
|||||||
_bar = Bar("Creating playlists", max=len(playlists))
|
_bar = Bar("Creating playlists", max=len(playlists))
|
||||||
for playlist in playlists:
|
for playlist in playlists:
|
||||||
api.PLAYLISTS.append(models.Playlist(playlist))
|
api.PLAYLISTS.append(models.Playlist(playlist))
|
||||||
|
|
||||||
_bar.next()
|
_bar.next()
|
||||||
_bar.finish()
|
_bar.finish()
|
||||||
|
|
||||||
|
|
||||||
|
def save_p_image(file: datastructures.FileStorage, pid: str):
|
||||||
|
"""
|
||||||
|
Saves the image of a playlist to the database.
|
||||||
|
"""
|
||||||
|
img = Image.open(file)
|
||||||
|
|
||||||
|
random_str = "".join(
|
||||||
|
random.choices(string.ascii_letters + string.digits, k=5))
|
||||||
|
|
||||||
|
img_path = pid + str(random_str) + ".webp"
|
||||||
|
full_path = os.path.join(settings.APP_DIR, "images", "playlists", img_path)
|
||||||
|
|
||||||
|
if file.content_type == "image/gif":
|
||||||
|
frames = []
|
||||||
|
|
||||||
|
for frame in ImageSequence.Iterator(img):
|
||||||
|
frames.append(frame.copy())
|
||||||
|
|
||||||
|
frames[0].save(full_path, save_all=True, append_images=frames[1:])
|
||||||
|
return img_path
|
||||||
|
|
||||||
|
img.save(full_path, "webp")
|
||||||
|
|
||||||
|
return img_path
|
||||||
|
|||||||
+46
-10
@@ -1,12 +1,14 @@
|
|||||||
"""
|
"""
|
||||||
Contains all the models for objects generation and typing.
|
Contains all the models for objects generation and typing.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from dataclasses import field
|
||||||
from datetime import date
|
from datetime import date
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from app import api
|
from app import api
|
||||||
from app import settings
|
from app import settings
|
||||||
|
from app.exceptions import TrackExistsInPlaylist
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -71,11 +73,9 @@ class Album:
|
|||||||
|
|
||||||
def get_p_track(ptrack):
|
def get_p_track(ptrack):
|
||||||
for track in api.TRACKS:
|
for track in api.TRACKS:
|
||||||
if (
|
if (track.title == ptrack["title"]
|
||||||
track.title == ptrack["title"]
|
and track.artists == ptrack["artists"]
|
||||||
and track.artists == ptrack["artists"]
|
and ptrack["album"] == track.album):
|
||||||
and ptrack["album"] == track.album
|
|
||||||
):
|
|
||||||
return track
|
return track
|
||||||
|
|
||||||
|
|
||||||
@@ -99,10 +99,11 @@ class Playlist:
|
|||||||
|
|
||||||
playlistid: str
|
playlistid: str
|
||||||
name: str
|
name: str
|
||||||
description: str
|
|
||||||
image: str
|
|
||||||
tracks: List[Track]
|
tracks: List[Track]
|
||||||
|
_pre_tracks: list = field(init=False, repr=False)
|
||||||
lastUpdated: int
|
lastUpdated: int
|
||||||
|
image: str
|
||||||
|
description: str = ""
|
||||||
count: int = 0
|
count: int = 0
|
||||||
"""A list of track objects in the playlist"""
|
"""A list of track objects in the playlist"""
|
||||||
|
|
||||||
@@ -110,9 +111,44 @@ class Playlist:
|
|||||||
self.playlistid = data["_id"]["$oid"]
|
self.playlistid = data["_id"]["$oid"]
|
||||||
self.name = data["name"]
|
self.name = data["name"]
|
||||||
self.description = data["description"]
|
self.description = data["description"]
|
||||||
self.image = ""
|
self.image = self.create_img_link(data["image"])
|
||||||
self.tracks = create_playlist_tracks(data["tracks"])
|
self._pre_tracks = data["pre_tracks"]
|
||||||
|
self.tracks = []
|
||||||
self.lastUpdated = data["lastUpdated"]
|
self.lastUpdated = data["lastUpdated"]
|
||||||
|
self.count = len(self._pre_tracks)
|
||||||
|
|
||||||
|
def get_tracks(self) -> List[Track]:
|
||||||
|
"""
|
||||||
|
Generates and returns Track objects from pre_tracks
|
||||||
|
"""
|
||||||
|
return create_playlist_tracks(self._pre_tracks)
|
||||||
|
|
||||||
|
def create_img_link(self, image: str):
|
||||||
|
if image:
|
||||||
|
return settings.IMG_PLAYLIST_URI + image
|
||||||
|
|
||||||
|
return settings.IMG_PLAYLIST_URI + ""
|
||||||
|
|
||||||
|
def update_count(self):
|
||||||
|
self.count = len(self._pre_tracks)
|
||||||
|
|
||||||
|
def add_track(self, track):
|
||||||
|
if track not in self._pre_tracks:
|
||||||
|
self._pre_tracks.append(track)
|
||||||
|
self.update_count()
|
||||||
|
else:
|
||||||
|
raise TrackExistsInPlaylist("Track already exists in playlist")
|
||||||
|
|
||||||
|
def update_desc(self, desc):
|
||||||
|
self.description = desc
|
||||||
|
|
||||||
|
def update_playlist(self, data: dict):
|
||||||
|
self.name = data["name"]
|
||||||
|
self.description = data["description"]
|
||||||
|
self.lastUpdated = data["lastUpdated"]
|
||||||
|
|
||||||
|
if data["image"]:
|
||||||
|
self.image = self.create_img_link(data["image"])
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|||||||
+4
-11
@@ -13,27 +13,20 @@ def create_config_dir() -> None:
|
|||||||
|
|
||||||
_home_dir = os.path.expanduser("~")
|
_home_dir = os.path.expanduser("~")
|
||||||
config_folder = os.path.join(_home_dir, settings.CONFIG_FOLDER)
|
config_folder = os.path.join(_home_dir, settings.CONFIG_FOLDER)
|
||||||
|
print(config_folder)
|
||||||
|
|
||||||
dirs = [
|
dirs = [
|
||||||
"",
|
"",
|
||||||
"images",
|
"images",
|
||||||
os.path.join("images", "artists"),
|
os.path.join("images", "artists"),
|
||||||
os.path.join("images", "thumbnails"),
|
os.path.join("images", "thumbnails"),
|
||||||
|
os.path.join("images", "playlists"),
|
||||||
]
|
]
|
||||||
|
|
||||||
for _dir in dirs:
|
for _dir in dirs:
|
||||||
path = os.path.join(config_folder, _dir)
|
path = os.path.join(config_folder, _dir)
|
||||||
|
exists = os.path.exists(path)
|
||||||
|
|
||||||
try:
|
if not exists:
|
||||||
os.path.exists(path)
|
|
||||||
except FileNotFoundError:
|
|
||||||
os.makedirs(path)
|
os.makedirs(path)
|
||||||
os.chmod(path, 0o755)
|
os.chmod(path, 0o755)
|
||||||
|
|
||||||
if _dir == dirs[3]:
|
|
||||||
default_thumbnails_path = "../setup/default-images/thumbnails"
|
|
||||||
|
|
||||||
try:
|
|
||||||
os.path.exists(os.path.join(path, "defaults"))
|
|
||||||
except FileNotFoundError:
|
|
||||||
pass
|
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from app import models
|
||||||
|
|
||||||
|
|
||||||
|
def date_string_to_time_passed(dstring: str) -> str:
|
||||||
|
"""
|
||||||
|
Converts a date string to time passed. eg. 2 minutes ago, 1 hour ago, yesterday, 2 days ago, 2 weeks ago, etc.
|
||||||
|
"""
|
||||||
|
|
||||||
|
now = datetime.now()
|
||||||
|
then = datetime.strptime(dstring, "%Y-%m-%d %H:%M:%S")
|
||||||
|
|
||||||
|
diff = now - then
|
||||||
|
days = diff.days
|
||||||
|
|
||||||
|
if days < 0:
|
||||||
|
return "in the future"
|
||||||
|
|
||||||
|
elif days == 0:
|
||||||
|
seconds = diff.seconds
|
||||||
|
if seconds < 15:
|
||||||
|
return "now"
|
||||||
|
elif seconds < 60:
|
||||||
|
return str(seconds) + " seconds ago"
|
||||||
|
elif seconds < 3600:
|
||||||
|
return str(seconds // 60) + " minutes ago"
|
||||||
|
else:
|
||||||
|
return str(seconds // 3600) + " hours ago"
|
||||||
|
|
||||||
|
elif days == 1:
|
||||||
|
return "yesterday"
|
||||||
|
elif days < 7:
|
||||||
|
if days == 1:
|
||||||
|
return "1 day ago"
|
||||||
|
|
||||||
|
return str(days) + " days ago"
|
||||||
|
elif days < 30:
|
||||||
|
if days == 7:
|
||||||
|
return "1 week ago"
|
||||||
|
|
||||||
|
return str(days // 7) + " weeks ago"
|
||||||
|
elif days < 365:
|
||||||
|
if days == 30:
|
||||||
|
return "1 month ago"
|
||||||
|
|
||||||
|
return str(days // 30) + " months ago"
|
||||||
|
elif days > 365:
|
||||||
|
if days == 365:
|
||||||
|
return "1 year ago"
|
||||||
|
|
||||||
|
return str(days // 365) + " years ago"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Playlist:
|
||||||
|
playlistid: str
|
||||||
|
name: str
|
||||||
|
image: str
|
||||||
|
lastUpdated: int
|
||||||
|
description: str
|
||||||
|
count: int = 0
|
||||||
|
|
||||||
|
def __init__(self,
|
||||||
|
p: models.Playlist,
|
||||||
|
construct_last_updated: bool = True) -> None:
|
||||||
|
self.playlistid = p.playlistid
|
||||||
|
self.name = p.name
|
||||||
|
self.image = p.image
|
||||||
|
self.lastUpdated = p.lastUpdated
|
||||||
|
self.description = p.description
|
||||||
|
self.count = p.count
|
||||||
|
|
||||||
|
if construct_last_updated:
|
||||||
|
self.lastUpdated = self.l_updated(p.lastUpdated)
|
||||||
|
|
||||||
|
def l_updated(self, date: str) -> str:
|
||||||
|
return date_string_to_time_passed(date)
|
||||||
@@ -13,6 +13,7 @@ THUMBS_PATH = os.path.join(APP_DIR, "images", "thumbnails")
|
|||||||
IMG_BASE_URI = "http://127.0.0.1:8900/images/"
|
IMG_BASE_URI = "http://127.0.0.1:8900/images/"
|
||||||
IMG_ARTIST_URI = IMG_BASE_URI + "artists/"
|
IMG_ARTIST_URI = IMG_BASE_URI + "artists/"
|
||||||
IMG_THUMB_URI = IMG_BASE_URI + "thumbnails/"
|
IMG_THUMB_URI = IMG_BASE_URI + "thumbnails/"
|
||||||
|
IMG_PLAYLIST_URI = IMG_BASE_URI + "playlists/"
|
||||||
|
|
||||||
# defaults
|
# defaults
|
||||||
DEFAULT_ARTIST_IMG = IMG_ARTIST_URI + "0.webp"
|
DEFAULT_ARTIST_IMG = IMG_ARTIST_URI + "0.webp"
|
||||||
|
|||||||
+4
-28
@@ -1,63 +1,39 @@
|
|||||||
# Fixes !
|
# Fixes !
|
||||||
|
|
||||||
- [ ] Use click event to play song instead of url ⚠
|
|
||||||
- [ ] Show play/pause button correctly according to state ⚠
|
|
||||||
- [ ] Click on artist image to go to artist page ⚠
|
- [ ] Click on artist image to go to artist page ⚠
|
||||||
- [ ] Play next song if current song can't be loaded ⚠
|
- [ ] Play next song if current song can't be loaded ⚠
|
||||||
- [ ] List item song icon for long song titles ⚠
|
|
||||||
<!-- -->
|
<!-- -->
|
||||||
- [ ] Broken CSS
|
|
||||||
- [ ] Prevent scanning unchanged folders
|
|
||||||
- [ ] Handle '/' and '&' characters in song artists
|
|
||||||
- [ ] Nginx not serving all files in a folder
|
|
||||||
- [ ] Removing song duplicates from queries
|
- [ ] Removing song duplicates from queries
|
||||||
- [ ] Different songs having same link
|
|
||||||
- [ ] ConnectionError
|
|
||||||
- [ ] Move thumbnails to .config
|
|
||||||
- [ ] Write a multithreaded file server
|
|
||||||
- [ ] Add support for WAV files
|
- [ ] Add support for WAV files
|
||||||
- [ ] Support multiple folders
|
|
||||||
- [ ] Compress thumbnails
|
- [ ] Compress thumbnails
|
||||||
|
|
||||||
# Features +
|
# Features +
|
||||||
|
|
||||||
## Needed features
|
## Needed features
|
||||||
- [ ] Seeking current song
|
|
||||||
- [ ] Adding songs to queue
|
- [ ] Adding songs to queue
|
||||||
- [ ] Implement search on frontend
|
|
||||||
<!-- -->
|
<!-- -->
|
||||||
- [ ] Watching for changes in folders and updating them instantly ⚠
|
|
||||||
- [ ] Display folders and files in a tree view. ⚠ 🔵
|
|
||||||
<!-- -->
|
|
||||||
- [ ] Add favicon
|
|
||||||
- [ ] Add keyboard shortcuts
|
- [ ] Add keyboard shortcuts
|
||||||
- [ ] Right click on song to do stuff
|
|
||||||
- [ ] Adjust volume
|
- [ ] Adjust volume
|
||||||
- [ ] Add listening statistics for all songs
|
- [ ] Add listening statistics for all songs
|
||||||
- [ ] Extract color from artist image [for use with artist card gradient]
|
- [ ] Extract color from artist image [for use with artist card gradient]
|
||||||
- [ ] Adding songs to favorites
|
- [ ] Adding songs to favorites
|
||||||
- [ ] Adding songs to playlist
|
|
||||||
- [ ] Playing song radio
|
- [ ] Playing song radio
|
||||||
|
|
||||||
## Future features
|
## Future features
|
||||||
|
|
||||||
- [ ] Toggle shuffle
|
- [ ] Toggle shuffle
|
||||||
- [ ] Toggle repeat
|
- [ ] Toggle repeat
|
||||||
- [ ] Display artist albums
|
|
||||||
- [ ] Suggest similar artists
|
- [ ] Suggest similar artists
|
||||||
- [ ] Getting artist info
|
- [ ] Getting artist info
|
||||||
- [ ] Getting album info
|
|
||||||
- [ ] Create a Python script to build, bundle and serve the app
|
- [ ] Create a Python script to build, bundle and serve the app
|
||||||
- [ ] Getting extra song info (probably from genius)
|
- [ ] Getting extra song info (probably from genius)
|
||||||
- [ ] Getting lyrics
|
- [ ] Getting lyrics
|
||||||
- [ ] Notifications
|
|
||||||
- [ ] Sorting songs
|
- [ ] Sorting songs
|
||||||
- [ ] Suggest undiscorvered artists, albums and songs
|
- [ ] Suggest undiscorvered artists, albums and songs
|
||||||
- [ ] Remember last played song
|
- [ ] Remember last played song
|
||||||
- [ ] Add next and previous song transition and progress bar reset animations
|
- [ ] Add next and previous song transition and progress bar reset animations
|
||||||
- [ ] Hover animations for list items
|
- [ ] Add playlist to folder
|
||||||
- [ ] Highlight currently playing song in playlist
|
|
||||||
- [ ] Add functionality to 'Listen now' button
|
- [ ] Add functionality to 'Listen now' button
|
||||||
- [ ] Add a 'Scan' button to the sidebar
|
|
||||||
- [ ] Paginated requests for songs
|
- [ ] Paginated requests for songs
|
||||||
- [ ] Package app as installable PWA
|
- [ ] Package app as installable PWA
|
||||||
|
|
||||||
## Finished ✅
|
|
||||||
@@ -38,11 +38,7 @@ a {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.border {
|
.border {
|
||||||
border: solid 1px $gray;
|
border: solid 1px $gray3;
|
||||||
}
|
|
||||||
|
|
||||||
.border-sm {
|
|
||||||
border: solid 1px #27262654;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.separator {
|
.separator {
|
||||||
@@ -63,21 +59,6 @@ a {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
|
||||||
border: none;
|
|
||||||
outline: none;
|
|
||||||
color: inherit;
|
|
||||||
font-size: 1rem;
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
background-color: $blue;
|
|
||||||
border-radius: $small;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: $red !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.l-container {
|
.l-container {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|||||||
@@ -1,104 +1,89 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="album-h">
|
<div class="album-h">
|
||||||
<div class="a-header rounded">
|
<div class="a-header rounded">
|
||||||
<div
|
<div class="art">
|
||||||
class="image art shadow-lg rounded"
|
<div
|
||||||
:style="{
|
class="image shadow-lg"
|
||||||
backgroundImage: `url("${props.album_info.image}")`,
|
:style="{
|
||||||
}"
|
backgroundImage: `url("${props.album.image}")`,
|
||||||
></div>
|
}"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
<div class="info">
|
<div class="info">
|
||||||
<div class="top">
|
<div class="top">
|
||||||
<div class="h">Album</div>
|
<div class="h">Album</div>
|
||||||
<div class="separator no-border"></div>
|
<div class="title ellip">{{ props.album.album }}</div>
|
||||||
<div class="title">{{ props.album_info.album }}</div>
|
|
||||||
<div class="artist">{{ props.album_info.artist }}</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="separator no-border"></div>
|
|
||||||
<div class="bottom">
|
<div class="bottom">
|
||||||
<div class="stats shadow-sm">
|
<div class="stats">
|
||||||
{{ props.album_info.count }} Tracks •
|
{{ props.album.count }} Tracks •
|
||||||
{{ perks.formatSeconds(props.album_info.duration, "long") }} •
|
{{ perks.formatSeconds(props.album.duration, "long") }} •
|
||||||
{{ props.album_info.date }}
|
{{ props.album.date }} • {{ props.album.artist }}
|
||||||
</div>
|
|
||||||
<div class="play rounded" @click="playAlbum">
|
|
||||||
<div class="icon"></div>
|
|
||||||
<div>Play</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<PlayBtnRect :source="playSources.album" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup lang="ts">
|
||||||
import state from "@/composables/state";
|
import perks from "../../composables/perks.js";
|
||||||
import perks from "@/composables/perks.js";
|
import { AlbumInfo } from "../../interfaces.js";
|
||||||
|
import PlayBtnRect from "../shared/PlayBtnRect.vue";
|
||||||
const props = defineProps({
|
import { playSources } from "../../composables/enums";
|
||||||
album_info: {
|
const props = defineProps<{
|
||||||
type: Object,
|
album: AlbumInfo;
|
||||||
default: () => ({}),
|
}>();
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
function playAlbum() {
|
|
||||||
perks.updateQueue(state.album.tracklist[0], "album");
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.album-h {
|
.album-h {
|
||||||
height: 14rem;
|
height: 16rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.a-header {
|
.a-header {
|
||||||
position: relative;
|
display: grid;
|
||||||
display: flex;
|
grid-template-columns: 13rem 1fr;
|
||||||
align-items: center;
|
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-color: $gray4;
|
background-color: $gray4;
|
||||||
background-image: linear-gradient(
|
background-image: linear-gradient(37deg, $black 20%, $gray5, $black 90%);
|
||||||
to bottom,
|
|
||||||
$gray3 0%,
|
|
||||||
$gray3 25%,
|
|
||||||
$gray3 35%,
|
|
||||||
$gray4 50%,
|
|
||||||
$gray 75%,
|
|
||||||
$black 100%
|
|
||||||
);
|
|
||||||
|
|
||||||
.art {
|
.art {
|
||||||
position: absolute;
|
width: 100%;
|
||||||
width: 12rem;
|
height: 100%;
|
||||||
height: 12rem;
|
|
||||||
left: 1rem;
|
left: 1rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
|
||||||
|
.image {
|
||||||
|
width: 12rem;
|
||||||
|
height: 12rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.info {
|
.info {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: calc(100%);
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
margin-left: 13rem;
|
|
||||||
|
|
||||||
.top {
|
.top {
|
||||||
.h {
|
.h {
|
||||||
color: #ffffffcb;
|
color: #ffffffcb;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
font-size: 2rem;
|
font-size: 2.5rem;
|
||||||
font-weight: 1000;
|
font-weight: 1000;
|
||||||
color: white;
|
color: white;
|
||||||
text-transform: capitalize;
|
text-transform: capitalize;
|
||||||
}
|
}
|
||||||
|
|
||||||
.artist {
|
.artist {
|
||||||
margin-top: $small;
|
font-size: 1.15rem;
|
||||||
font-size: 1.5rem;
|
color: #ffffffe0;
|
||||||
color: #fffffff1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,16 +92,13 @@ function playAlbum() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.bottom {
|
.bottom {
|
||||||
position: relative;
|
margin-top: $smaller;
|
||||||
|
|
||||||
.stats {
|
.stats {
|
||||||
background-color: $red;
|
|
||||||
padding: $small;
|
|
||||||
border-radius: $small;
|
border-radius: $small;
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.play {
|
.play {
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="folder-top flex">
|
<div class="folder-top flex">
|
||||||
<div class="fname">
|
<div class="fname">
|
||||||
<button class="play image">
|
<PlayBtnRect />
|
||||||
<div class="icon"></div>
|
<div class="ftext">
|
||||||
Play
|
|
||||||
</button>
|
|
||||||
<div class="text">
|
|
||||||
<div class="icon image"></div>
|
<div class="icon image"></div>
|
||||||
<div class="ellip">
|
<div class="ellip">
|
||||||
{{ folder.path.split("/").splice(-1).join("") }}
|
{{ folder.path.split("/").splice(-1).join("") }}
|
||||||
@@ -17,6 +14,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import useFStore from "../../stores/folder";
|
import useFStore from "../../stores/folder";
|
||||||
|
import PlayBtnRect from "../shared/PlayBtnRect.vue";
|
||||||
|
|
||||||
const folder = useFStore();
|
const folder = useFStore();
|
||||||
</script>
|
</script>
|
||||||
@@ -32,30 +30,14 @@ const folder = useFStore();
|
|||||||
.folder-top .fname {
|
.folder-top .fname {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
gap: $small;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
.play {
|
.ftext {
|
||||||
height: 100%;
|
|
||||||
width: 5em;
|
|
||||||
background-color: $blue;
|
|
||||||
padding-left: $small;
|
|
||||||
margin-right: $small;
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
height: 1.5rem;
|
|
||||||
width: 1.5rem;
|
|
||||||
background-image: url(../../assets/icons/play.svg);
|
|
||||||
background-size: 1.5rem;
|
|
||||||
background-position: 10%;
|
|
||||||
margin-right: $small;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.text {
|
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
height: 2.5rem;
|
||||||
border-radius: $small;
|
border-radius: $small;
|
||||||
background-color: $primary;
|
background-color: $primary;
|
||||||
padding: $small $small $small 2.25rem;
|
padding: $small $small $small 2.25rem;
|
||||||
|
|||||||
@@ -25,7 +25,6 @@
|
|||||||
<div class="text">Nothing down here 😑</div>
|
<div class="text">Nothing down here 😑</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else ref="songtitle"></div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -84,6 +83,9 @@ function updateQueue(track: Track) {
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|
||||||
.current {
|
.current {
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
color: $red;
|
color: $red;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
v-for="artist in artists"
|
v-for="artist in artists"
|
||||||
:key="artist"
|
:key="artist"
|
||||||
:artist="artist"
|
:artist="artist"
|
||||||
:color="ffffff00"
|
:color="'ffffff00'"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,14 +1,28 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="p-header">
|
<div
|
||||||
|
class="p-header image"
|
||||||
|
:style="[
|
||||||
|
{
|
||||||
|
backgroundImage: `url(${props.info.image})`,
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<div class="gradient"></div>
|
||||||
<div class="carddd">
|
<div class="carddd">
|
||||||
<div class="art image"></div>
|
|
||||||
<div class="info">
|
<div class="info">
|
||||||
<div class="btns">
|
<div class="btns">
|
||||||
<PlayBtnRect :source="playSources.playlist" />
|
<PlayBtnRect :source="playSources.playlist" />
|
||||||
|
<Option @showDropdown="showDropdown" :src="context.src" />
|
||||||
|
</div>
|
||||||
|
<div class="duration">
|
||||||
|
<span v-if="props.info.count == 0">No Tracks</span>
|
||||||
|
<span v-else-if="props.info.count == 1"
|
||||||
|
>{{ props.info.count }} Track</span
|
||||||
|
>
|
||||||
|
<span v-else>{{ props.info.count }} Tracks</span> • 3 Hours
|
||||||
</div>
|
</div>
|
||||||
<div class="duration">4 Tracks • 3 Hours</div>
|
|
||||||
<div class="desc">
|
<div class="desc">
|
||||||
{{ props.info.desc[0] }}
|
{{ props.info.description }}
|
||||||
</div>
|
</div>
|
||||||
<div class="title ellip">{{ props.info.name }}</div>
|
<div class="title ellip">{{ props.info.name }}</div>
|
||||||
<div class="type">Playlist</div>
|
<div class="type">Playlist</div>
|
||||||
@@ -18,38 +32,62 @@
|
|||||||
<span class="status"
|
<span class="status"
|
||||||
>Last updated {{ props.info.lastUpdated }} | </span
|
>Last updated {{ props.info.lastUpdated }} | </span
|
||||||
>
|
>
|
||||||
<span class="edit">Edit</span>
|
<span class="edit" @click="editPlaylist">Edit</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { playSources } from "../../composables/enums";
|
import { playSources, ContextSrc } from "../../composables/enums";
|
||||||
|
import { Playlist } from "../../interfaces";
|
||||||
import PlayBtnRect from "../shared/PlayBtnRect.vue";
|
import PlayBtnRect from "../shared/PlayBtnRect.vue";
|
||||||
|
import useModalStore from "../../stores/modal";
|
||||||
|
import Option from "../shared/Option.vue";
|
||||||
|
import pContext from "../../contexts/playlist";
|
||||||
|
import useContextStore from "../../stores/context";
|
||||||
|
|
||||||
|
const context = useContextStore();
|
||||||
|
const modal = useModalStore();
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
info: {
|
info: Playlist;
|
||||||
name: string;
|
|
||||||
count: number;
|
|
||||||
duration: string;
|
|
||||||
desc: string;
|
|
||||||
lastUpdated: string;
|
|
||||||
};
|
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
function editPlaylist() {
|
||||||
|
modal.showEditPlaylistModal(props.info);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showDropdown(e: any) {
|
||||||
|
context.showContextMenu(e, pContext(), ContextSrc.PHeader);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.p-header {
|
.p-header {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
height: 14rem;
|
height: 16rem;
|
||||||
// background-image: url("../../assets/images/eggs.jpg");
|
|
||||||
|
|
||||||
background-image: linear-gradient(23deg, $black 40%, rgb(141, 11, 2), $black);
|
|
||||||
position: relative;
|
position: relative;
|
||||||
margin-top: $small;
|
margin-top: $small;
|
||||||
border-radius: 0.75rem;
|
border-radius: 0.75rem;
|
||||||
color: $white;
|
color: $white;
|
||||||
|
background-color: transparent;
|
||||||
|
z-index: 0;
|
||||||
|
|
||||||
|
.gradient {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-image: linear-gradient(
|
||||||
|
37deg,
|
||||||
|
$black 20%,
|
||||||
|
transparent,
|
||||||
|
$black 90%
|
||||||
|
);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.last-updated {
|
.last-updated {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -61,6 +99,7 @@ const props = defineProps<{
|
|||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
border-radius: $smaller;
|
border-radius: $smaller;
|
||||||
box-shadow: 0 0 1rem rgba(0, 0, 0, 0.479);
|
box-shadow: 0 0 1rem rgba(0, 0, 0, 0.479);
|
||||||
|
z-index: 12;
|
||||||
|
|
||||||
@include phone-only {
|
@include phone-only {
|
||||||
bottom: 1rem;
|
bottom: 1rem;
|
||||||
@@ -81,13 +120,19 @@ const props = defineProps<{
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 13rem 1fr;
|
z-index: 10;
|
||||||
|
|
||||||
.art {
|
.art {
|
||||||
width: 12rem;
|
width: 100%;
|
||||||
height: 12rem;
|
height: 100%;
|
||||||
background-color: red;
|
display: flex;
|
||||||
background-image: url("../../assets/images/eggs.jpg");
|
align-items: flex-end;
|
||||||
|
|
||||||
|
.image {
|
||||||
|
width: 12rem;
|
||||||
|
height: 12rem;
|
||||||
|
background-image: url("../../assets/images/eggs.jpg");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.info {
|
.info {
|
||||||
@@ -112,6 +157,7 @@ const props = defineProps<{
|
|||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
-webkit-line-clamp: 2;
|
-webkit-line-clamp: 2;
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
|
max-width: 50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.duration {
|
.duration {
|
||||||
@@ -124,6 +170,8 @@ const props = defineProps<{
|
|||||||
|
|
||||||
.btns {
|
.btns {
|
||||||
margin-top: $small;
|
margin-top: $small;
|
||||||
|
display: flex;
|
||||||
|
gap: $small;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,21 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="r-home">
|
<div class="r-home">
|
||||||
<Recommendations />
|
<UpNext :next="queue.next" :playNext="queue.playNext" />
|
||||||
|
<Recommendations />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.r-home {
|
.r-home {
|
||||||
height: calc(100% - 1rem);
|
height: calc(100% - 1rem);
|
||||||
// padding: 0 $small $small 0;
|
padding: 0 $small $small 0;
|
||||||
|
margin-top: $small;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script setup>
|
<script setup lang="ts">
|
||||||
import Recommendations from '../Recommendation.vue';
|
import Recommendations from "../Recommendation.vue";
|
||||||
|
import UpNext from "../queue/upNext.vue";
|
||||||
|
import useQStore from "../../../stores/queue";
|
||||||
|
const queue = useQStore();
|
||||||
</script>
|
</script>
|
||||||
@@ -1,28 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="up-next">
|
<div class="up-next">
|
||||||
<div class="r-grid">
|
<div class="r-grid">
|
||||||
<div class="main-item border">
|
<UpNext :next="queue.next" :playNext="queue.playNext" />
|
||||||
<p class="heading">COMING UP NEXT</p>
|
|
||||||
<div class="itemx" @click="queue.playNext">
|
|
||||||
<div
|
|
||||||
class="album-art image"
|
|
||||||
:style="{
|
|
||||||
backgroundImage: `url("${queue.next.image}")`,
|
|
||||||
}"
|
|
||||||
></div>
|
|
||||||
<div class="tags">
|
|
||||||
<p class="title ellip">{{ queue.next.title }}</p>
|
|
||||||
<hr />
|
|
||||||
<p class="artist ellip">
|
|
||||||
<span
|
|
||||||
v-for="artist in putCommas(queue.next.artists)"
|
|
||||||
:key="artist"
|
|
||||||
>{{ artist }}</span
|
|
||||||
>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="scrollable-r border rounded">
|
<div class="scrollable-r border rounded">
|
||||||
<TrackItem
|
<TrackItem
|
||||||
v-for="t in queue.tracks"
|
v-for="t in queue.tracks"
|
||||||
@@ -33,21 +12,20 @@
|
|||||||
:isPlaying="queue.playing"
|
:isPlaying="queue.playing"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<PlayingFrom :from="queue.from" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import perks from "../../composables/perks.js";
|
|
||||||
import TrackItem from "../shared/TrackItem.vue";
|
import TrackItem from "../shared/TrackItem.vue";
|
||||||
import useQStore from "../../stores/queue";
|
import useQStore from "../../stores/queue";
|
||||||
import { Track } from "../../interfaces.js";
|
import { Track } from "../../interfaces.js";
|
||||||
import { onBeforeMount } from "vue";
|
import PlayingFrom from "./queue/playingFrom.vue";
|
||||||
|
import UpNext from "./queue/upNext.vue";
|
||||||
|
|
||||||
const queue = useQStore();
|
const queue = useQStore();
|
||||||
|
|
||||||
const putCommas = perks.putCommas;
|
|
||||||
|
|
||||||
function playThis(track: Track) {
|
function playThis(track: Track) {
|
||||||
queue.play(track);
|
queue.play(track);
|
||||||
}
|
}
|
||||||
@@ -55,7 +33,7 @@ function playThis(track: Track) {
|
|||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.up-next {
|
.up-next {
|
||||||
padding: $small $small $small 0;
|
padding: $small $small 0 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
@@ -64,59 +42,17 @@ function playThis(track: Track) {
|
|||||||
margin: 0.5rem 0 1rem 0;
|
margin: 0.5rem 0 1rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-item {
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
padding: 0.5rem;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
|
|
||||||
.itemx {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0.5rem;
|
|
||||||
cursor: pointer;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: $gray;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.album-art {
|
|
||||||
width: 4.5rem;
|
|
||||||
height: 4.5rem;
|
|
||||||
background-image: url(../../assets/images/null.webp);
|
|
||||||
margin: 0 0.5rem 0 0;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tags {
|
|
||||||
hr {
|
|
||||||
border: none;
|
|
||||||
margin: 0.3rem;
|
|
||||||
}
|
|
||||||
.title {
|
|
||||||
width: 20rem;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
.artist {
|
|
||||||
width: 20rem;
|
|
||||||
margin: 0;
|
|
||||||
font-size: small;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.r-grid {
|
.r-grid {
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: min-content;
|
grid-template-rows: max-content 1fr max-content;
|
||||||
|
gap: $small;
|
||||||
|
|
||||||
.scrollable-r {
|
.scrollable-r {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: $small;
|
padding: $small;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
// background-color: $card-dark;
|
|
||||||
scrollbar-color: grey transparent;
|
scrollbar-color: grey transparent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="r-tracks rounded border">
|
<div class="r-tracks rounded border">
|
||||||
<p class="heading">SIMILAR TRACKS</p>
|
<div class="heading">Similar Tracks</div>
|
||||||
<div class="tracks">
|
<div class="tracks">
|
||||||
<div class="song-item" v-for="song in songs" :key="song">
|
<div class="song-item" v-for="song in songs" :key="song">
|
||||||
<div class="album-art image"></div>
|
<div class="album-art image"></div>
|
||||||
@@ -37,8 +37,13 @@ export default {
|
|||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.r-tracks {
|
.r-tracks {
|
||||||
margin: 0.5rem 0.5rem 0.5rem 0;
|
margin: 0.5rem 0 0.5rem 0;
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
|
|
||||||
|
.heading {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
margin-bottom: 0.5rem !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.r-tracks .tracks .song-item {
|
.r-tracks .tracks .song-item {
|
||||||
|
|||||||
@@ -170,7 +170,6 @@ search.$subscribe((mutation, state) => {
|
|||||||
padding: $medium;
|
padding: $medium;
|
||||||
border-radius: $small;
|
border-radius: $small;
|
||||||
margin-bottom: $small;
|
margin-bottom: $small;
|
||||||
text-align: center !important;
|
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
color: $white;
|
color: $white;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,156 @@
|
|||||||
|
<template>
|
||||||
|
<div id="playing-from" class="rounded" @click="goTo">
|
||||||
|
<div class="abs shadow-sm">Playing From</div>
|
||||||
|
<div class="h">
|
||||||
|
<div class="icon image" :class="from.type"></div>
|
||||||
|
{{ from.type }}
|
||||||
|
</div>
|
||||||
|
<div class="name">
|
||||||
|
<div id="to">
|
||||||
|
{{ from.name }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { fromFolder, fromAlbum, fromPlaylist } from "../../../interfaces";
|
||||||
|
import { FromOptions } from "../../../composables/enums";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
import { computed } from "@vue/reactivity";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
from: fromFolder | fromAlbum | fromPlaylist;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
interface from {
|
||||||
|
type: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const from = computed((): from => {
|
||||||
|
switch (props.from.type) {
|
||||||
|
case undefined:
|
||||||
|
return {
|
||||||
|
type: "album",
|
||||||
|
name: "Welcome to Alice",
|
||||||
|
};
|
||||||
|
case FromOptions.folder:
|
||||||
|
return {
|
||||||
|
type: "folder",
|
||||||
|
name: props.from.name,
|
||||||
|
};
|
||||||
|
case FromOptions.album:
|
||||||
|
return {
|
||||||
|
type: "album",
|
||||||
|
name: props.from.name,
|
||||||
|
};
|
||||||
|
case FromOptions.playlist:
|
||||||
|
return {
|
||||||
|
type: "playlist",
|
||||||
|
name: props.from.name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
function goToAlbum() {
|
||||||
|
router.push({
|
||||||
|
name: "AlbumView",
|
||||||
|
params: {
|
||||||
|
album: props.from.name,
|
||||||
|
artist: props.from.albumartist,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function goToFolder() {
|
||||||
|
router.push({
|
||||||
|
name: "FolderView",
|
||||||
|
params: {
|
||||||
|
path: props.from.path,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function goToPlaylist() {
|
||||||
|
router.push({
|
||||||
|
name: "PlaylistView",
|
||||||
|
params: {
|
||||||
|
pid: props.from.playlistid,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function goTo() {
|
||||||
|
switch (props.from.type) {
|
||||||
|
case FromOptions.folder:
|
||||||
|
goToFolder();
|
||||||
|
break;
|
||||||
|
case FromOptions.album:
|
||||||
|
goToAlbum();
|
||||||
|
break;
|
||||||
|
case FromOptions.playlist:
|
||||||
|
goToPlaylist();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
#playing-from {
|
||||||
|
background: linear-gradient(-200deg, $gray4 40%, $red, $gray4);
|
||||||
|
background-size: 120%;
|
||||||
|
padding: 0.75rem;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
transition: all .2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-position: -4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.abs {
|
||||||
|
position: absolute;
|
||||||
|
right: $small;
|
||||||
|
bottom: $small;
|
||||||
|
font-size: .9rem;
|
||||||
|
background-color: $gray;
|
||||||
|
padding: $smaller;
|
||||||
|
border-radius: .25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
text-transform: capitalize;
|
||||||
|
font-weight: bolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h {
|
||||||
|
font-size: .9rem;
|
||||||
|
margin-bottom: $small;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: $small;
|
||||||
|
text-transform: capitalize;
|
||||||
|
color: rgba(255, 255, 255, 0.664);
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
height: 1.25rem;
|
||||||
|
width: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder {
|
||||||
|
background-image: url("../../../assets/icons/folder.fill.svg") !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.album {
|
||||||
|
background-image: url("../../../assets/icons/album.svg") !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playlist {
|
||||||
|
background-image: url("../../../assets/icons/playlist.svg") !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
<template>
|
||||||
|
<div class="main-item border" @click="playNext">
|
||||||
|
<div class="h">Up Next</div>
|
||||||
|
<div class="itemx shadow">
|
||||||
|
<div
|
||||||
|
class="album-art image"
|
||||||
|
:style="{
|
||||||
|
backgroundImage: `url("${next.image}")`,
|
||||||
|
}"
|
||||||
|
></div>
|
||||||
|
<div class="tags">
|
||||||
|
<p class="title ellip">{{ next.title }}</p>
|
||||||
|
<hr />
|
||||||
|
<p class="artist ellip">
|
||||||
|
<span v-for="artist in perks.putCommas(next.artists)" :key="artist">{{
|
||||||
|
artist
|
||||||
|
}}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Track } from "../../../interfaces";
|
||||||
|
import perks from "../../../composables/perks";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
next: Track;
|
||||||
|
playNext: () => void;
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.main-item {
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $accent;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
|
||||||
|
.h {
|
||||||
|
background-color: $black;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.h {
|
||||||
|
position: absolute;
|
||||||
|
right: $small;
|
||||||
|
bottom: $small;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
background-color: $accent;
|
||||||
|
padding: $smaller;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.itemx {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 0.75rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.album-art {
|
||||||
|
width: 4.5rem;
|
||||||
|
height: 4.5rem;
|
||||||
|
background-image: url(../../assets/images/null.webp);
|
||||||
|
margin: 0 0.5rem 0 0;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags {
|
||||||
|
hr {
|
||||||
|
border: none;
|
||||||
|
margin: 0.3rem;
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
width: 20rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.artist {
|
||||||
|
width: 20rem;
|
||||||
|
margin: 0;
|
||||||
|
font-size: small;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
'context-many-kids': context.hasManyChildren(),
|
'context-many-kids': context.hasManyChildren(),
|
||||||
},
|
},
|
||||||
]"
|
]"
|
||||||
|
id="context-menu"
|
||||||
:style="{
|
:style="{
|
||||||
left: context.x + 'px',
|
left: context.x + 'px',
|
||||||
top: context.y + 'px',
|
top: context.y + 'px',
|
||||||
@@ -56,7 +57,6 @@ const context = useContextStore();
|
|||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 12rem;
|
width: 12rem;
|
||||||
height: min-content;
|
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
transform: scale(0);
|
transform: scale(0);
|
||||||
|
|
||||||
@@ -89,6 +89,7 @@ const context = useContextStore();
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
right: -13rem;
|
right: -13rem;
|
||||||
width: 13rem;
|
width: 13rem;
|
||||||
|
top: -0.5rem;
|
||||||
max-height: 21.25rem;
|
max-height: 21.25rem;
|
||||||
|
|
||||||
padding: $small !important;
|
padding: $small !important;
|
||||||
|
|||||||
@@ -10,6 +10,12 @@
|
|||||||
@hideModal="hideModal"
|
@hideModal="hideModal"
|
||||||
@title="title"
|
@title="title"
|
||||||
/>
|
/>
|
||||||
|
<UpdatePlaylist
|
||||||
|
:playlist="modal.props"
|
||||||
|
v-if="modal.component == modal.options.updatePlaylist"
|
||||||
|
@hideModal="hideModal"
|
||||||
|
@title="title"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -17,6 +23,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import useModalStore from "../stores/modal";
|
import useModalStore from "../stores/modal";
|
||||||
import NewPlaylist from "./modals/NewPlaylist.vue";
|
import NewPlaylist from "./modals/NewPlaylist.vue";
|
||||||
|
import UpdatePlaylist from "./modals/updatePlaylist.vue";
|
||||||
|
|
||||||
const modal = useModalStore();
|
const modal = useModalStore();
|
||||||
|
|
||||||
@@ -25,6 +32,7 @@ const modal = useModalStore();
|
|||||||
* @param title
|
* @param title
|
||||||
*/
|
*/
|
||||||
function title(title: string) {
|
function title(title: string) {
|
||||||
|
console.log(title);
|
||||||
modal.setTitle(title);
|
modal.setTitle(title);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,8 +82,6 @@ function hideModal() {
|
|||||||
transform: rotate(135deg);
|
transform: rotate(135deg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,207 @@
|
|||||||
|
<template>
|
||||||
|
<form
|
||||||
|
@submit="update_playlist"
|
||||||
|
class="new-p-form"
|
||||||
|
enctype="multipart/form-data"
|
||||||
|
>
|
||||||
|
<label for="name">Playlist name</label>
|
||||||
|
<br />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="rounded"
|
||||||
|
name="name"
|
||||||
|
id="modal-playlist-name-input"
|
||||||
|
:value="props.playlist.name"
|
||||||
|
/>
|
||||||
|
<label for="name">Description</label>
|
||||||
|
<br />
|
||||||
|
<textarea
|
||||||
|
name="description"
|
||||||
|
id=""
|
||||||
|
class="rounded"
|
||||||
|
:value="props.playlist.description"
|
||||||
|
></textarea>
|
||||||
|
<br />
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
name="image"
|
||||||
|
id="update-pl-image-upload"
|
||||||
|
style="display: none"
|
||||||
|
@change="handleUpload"
|
||||||
|
/>
|
||||||
|
<div id="upload" class="rounded" @click="selectFiles">
|
||||||
|
<div>Click to upload image</div>
|
||||||
|
<div
|
||||||
|
id="update-pl-img-preview"
|
||||||
|
class="image"
|
||||||
|
:style="{
|
||||||
|
backgroundImage: `url(${props.playlist.image})`,
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<input type="submit" class="rounded" value="Update" />
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, ref } from "vue";
|
||||||
|
import { Playlist } from "../../interfaces";
|
||||||
|
import { updatePlaylist } from "../../composables/playlists";
|
||||||
|
import usePStore from "../../stores/p.ptracks";
|
||||||
|
import { getCurrentDate } from "../../composables/perks";
|
||||||
|
|
||||||
|
const pStore = usePStore();
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
playlist: Playlist;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
document.getElementById("modal-playlist-name-input").focus();
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "title", title: string): void;
|
||||||
|
(e: "hideModal"): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
emit("title", "Update Playlist");
|
||||||
|
|
||||||
|
function selectFiles() {
|
||||||
|
const input = document.getElementById(
|
||||||
|
"update-pl-image-upload"
|
||||||
|
) as HTMLInputElement;
|
||||||
|
input.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
let image: File;
|
||||||
|
|
||||||
|
function handleUpload() {
|
||||||
|
const input = document.getElementById(
|
||||||
|
"update-pl-image-upload"
|
||||||
|
) as HTMLInputElement;
|
||||||
|
|
||||||
|
handleFile(input.files[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleFile(file: File) {
|
||||||
|
if (!file || !file.type.startsWith("image/")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const preview = document.getElementById("update-pl-img-preview");
|
||||||
|
const obj_url = URL.createObjectURL(file);
|
||||||
|
preview.style.backgroundImage = `url(${obj_url})`;
|
||||||
|
|
||||||
|
image = file;
|
||||||
|
}
|
||||||
|
|
||||||
|
function update_playlist(e: Event) {
|
||||||
|
e.preventDefault();
|
||||||
|
const form = e.target as HTMLFormElement;
|
||||||
|
const formData = new FormData(form);
|
||||||
|
|
||||||
|
formData.append("image", image);
|
||||||
|
formData.append("lastUpdated", getCurrentDate());
|
||||||
|
|
||||||
|
if (formData.get("name").toString().trim() !== "") {
|
||||||
|
updatePlaylist(props.playlist.playlistid, formData, pStore).then(() => {
|
||||||
|
emit("hideModal");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.new-p-form {
|
||||||
|
grid-gap: 1rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
|
||||||
|
label {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: $gray1;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"] {
|
||||||
|
margin: $small 0;
|
||||||
|
border: 2px solid $gray3;
|
||||||
|
background-color: transparent;
|
||||||
|
color: #fff;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.5rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border: 2px solid transparent;
|
||||||
|
outline: solid 2px $gray1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="submit"] {
|
||||||
|
margin: $small 0;
|
||||||
|
background-color: $accent;
|
||||||
|
width: 7rem;
|
||||||
|
padding: 0.75rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#upload {
|
||||||
|
width: 100%;
|
||||||
|
padding: $small;
|
||||||
|
border: solid 2px $gray3;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr max-content;
|
||||||
|
place-items: center;
|
||||||
|
color: $gray1;
|
||||||
|
margin: $small 0;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
#update-pl-img-preview {
|
||||||
|
width: 4.5rem;
|
||||||
|
height: 4.5rem;
|
||||||
|
border-radius: $small;
|
||||||
|
object-fit: cover;
|
||||||
|
background-color: $gray4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 28rem;
|
||||||
|
max-height: 5rem;
|
||||||
|
color: $white;
|
||||||
|
background-color: transparent;
|
||||||
|
border: solid 2px $gray3;
|
||||||
|
font-family: inherit;
|
||||||
|
padding: $small;
|
||||||
|
outline: none;
|
||||||
|
margin: $small 0;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border: 2px solid transparent;
|
||||||
|
outline: solid 2px $gray1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.colors {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(2rem, 1fr));
|
||||||
|
grid-gap: 0.5rem;
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
|
||||||
|
.color {
|
||||||
|
height: 2.5rem;
|
||||||
|
width: 2.5rem;
|
||||||
|
border-radius: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected {
|
||||||
|
border: 4px solid $white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<template>
|
||||||
|
<div class="p-card rounded" id="new-playlist-card">
|
||||||
|
<div class="gradient rounded"></div>
|
||||||
|
<div class="plus image p-image"></div>
|
||||||
|
<div>New Playlist</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
#new-playlist-card {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
background-color: $black;
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
.gradient {
|
||||||
|
position: absolute;
|
||||||
|
width: calc(100% - 2rem);
|
||||||
|
height: 10rem;
|
||||||
|
top: 1rem;
|
||||||
|
background-image: linear-gradient(37deg, $red, $blue);
|
||||||
|
background-size: 100%;
|
||||||
|
transition: all .5s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image {
|
||||||
|
background-image: url("../../assets/icons/plus.svg");
|
||||||
|
background-size: 5rem;
|
||||||
|
z-index: 1;
|
||||||
|
transition: all .5s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.gradient {
|
||||||
|
background-size: 30rem;
|
||||||
|
}
|
||||||
|
.image {
|
||||||
|
transform: rotate(270deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -2,48 +2,88 @@
|
|||||||
<router-link
|
<router-link
|
||||||
:to="{ name: 'PlaylistView', params: { pid: props.playlist.playlistid } }"
|
:to="{ name: 'PlaylistView', params: { pid: props.playlist.playlistid } }"
|
||||||
:playlist="props.playlist"
|
:playlist="props.playlist"
|
||||||
class="p-card rounded shadow-sm"
|
class="p-card rounded"
|
||||||
>
|
>
|
||||||
<div class="image rounded"></div>
|
<div class="drop">
|
||||||
|
<Option :color="'#48484a'" />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="image p-image rounded shadow-sm"
|
||||||
|
:style="{
|
||||||
|
backgroundImage: `url(${props.playlist.image})`,
|
||||||
|
}"
|
||||||
|
></div>
|
||||||
|
<div class="pbtn">
|
||||||
|
<PlayBtn />
|
||||||
|
</div>
|
||||||
<div class="bottom">
|
<div class="bottom">
|
||||||
<div class="name ellip">{{ props.playlist.name }}</div>
|
<div class="name ellip">{{ props.playlist.name }}</div>
|
||||||
<div class="count">{{ props.playlist.count }} Tracks</div>
|
<div class="count">
|
||||||
|
<span v-if="props.playlist.count == 0">No Tracks</span>
|
||||||
|
<span v-else-if="props.playlist.count == 1"
|
||||||
|
>{{ props.playlist.count }} Track</span
|
||||||
|
>
|
||||||
|
<span v-else>{{ props.playlist.count }} Tracks</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</router-link>
|
</router-link>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Playlist } from "../../interfaces";
|
import { Playlist } from "../../interfaces";
|
||||||
|
import PlayBtn from "../shared/PlayBtn.vue";
|
||||||
|
import Option from "../shared/Option.vue";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
playlist: Playlist;
|
playlist: Playlist;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.p-card {
|
.p-card {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background: $gray;
|
padding: 0.75rem;
|
||||||
padding: $small;
|
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
|
background-image: linear-gradient(37deg, #000000e8, $gray);
|
||||||
|
position: relative;
|
||||||
|
|
||||||
&:hover {
|
.p-image {
|
||||||
background-color: $accent;
|
|
||||||
.bottom > .count {
|
|
||||||
color: $white;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.image {
|
|
||||||
min-width: 100%;
|
min-width: 100%;
|
||||||
height: 10rem;
|
height: 10rem;
|
||||||
background-image: url("../../assets/images/eggs.jpg");
|
|
||||||
background-size: auto 10rem;
|
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.drop {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 4rem;
|
||||||
|
right: 1.25rem;
|
||||||
|
opacity: 0;
|
||||||
|
transition: all 0.25s ease-in-out;
|
||||||
|
|
||||||
|
.drop-btn {
|
||||||
|
background-color: $gray3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pbtn {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 4.5rem;
|
||||||
|
left: 1.25rem;
|
||||||
|
transition: all 0.25s ease-in-out;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.drop {
|
||||||
|
transition-delay: .75s;
|
||||||
|
opacity: 1;
|
||||||
|
transform: translate(0, -.5rem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.bottom {
|
.bottom {
|
||||||
margin-top: $small;
|
margin-top: 1rem;
|
||||||
|
|
||||||
.name {
|
.name {
|
||||||
text-transform: capitalize;
|
text-transform: capitalize;
|
||||||
@@ -51,7 +91,7 @@ const props = defineProps<{
|
|||||||
|
|
||||||
.count {
|
.count {
|
||||||
font-size: $medium;
|
font-size: $medium;
|
||||||
color: $indigo;
|
color: $gray1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="drop-btn rounded shadow-sm"
|
||||||
|
id="option-drop"
|
||||||
|
@click="showDropdown"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="image drop-icon"
|
||||||
|
:class="{ clicked: clicked && src == ContextSrc.PHeader }"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, ref } from "vue";
|
||||||
|
import { ContextSrc } from "../../composables/enums";
|
||||||
|
|
||||||
|
let elem: DOMRect;
|
||||||
|
const clicked = ref(false);
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
src?: string;
|
||||||
|
color?: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "showDropdown", event: any): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
elem = document.getElementById("option-drop").getBoundingClientRect();
|
||||||
|
});
|
||||||
|
|
||||||
|
function showDropdown(e: Event) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopImmediatePropagation();
|
||||||
|
|
||||||
|
emit("showDropdown", {
|
||||||
|
clientX: elem.left + 45,
|
||||||
|
clientY: elem.top,
|
||||||
|
});
|
||||||
|
|
||||||
|
clicked.value = true;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style lang="scss">
|
||||||
|
.drop-btn {
|
||||||
|
width: 2.5rem;
|
||||||
|
height: 2.5rem;
|
||||||
|
background-color: $accent;
|
||||||
|
transition: all 0.5s ease-in-out;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
.drop-icon {
|
||||||
|
transition: all 0.25s;
|
||||||
|
padding: $small;
|
||||||
|
height: 2.5rem;
|
||||||
|
width: 2.5rem;
|
||||||
|
background-image: url("../../assets/icons/right-arrow.svg");
|
||||||
|
background-size: 1.75rem;
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.clicked {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $green;
|
||||||
|
.image {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,23 +1,25 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="play-btn circular">
|
<div class="play-btn rounded shadow-sm" @click="playThis"></div>
|
||||||
<div class="icon"></div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
function playThis(e: Event) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopImmediatePropagation();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.play-btn {
|
.play-btn {
|
||||||
width: 2rem;
|
width: 2.5rem;
|
||||||
height: 2rem;
|
height: 2.5rem;
|
||||||
background-color: $accent;
|
background-color: $gray3;
|
||||||
background-image: url("../../assets/icons/play.svg");
|
background-image: url("../../assets/icons/play.svg");
|
||||||
background-size: 1.25rem;
|
background-size: 1.75rem;
|
||||||
background-position: 60%;
|
background-position: 50% 50%;
|
||||||
background-repeat: no-repeat;
|
transition: all 0.25s ease-in-out;
|
||||||
transition: all .25s;
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: rgb(58, 197, 58);
|
background-color: $accent;
|
||||||
transform: scale(1.1);
|
transform: scale(1.1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -35,11 +35,11 @@ function play() {
|
|||||||
break;
|
break;
|
||||||
case playSources.playlist:
|
case playSources.playlist:
|
||||||
queue.playFromPlaylist(
|
queue.playFromPlaylist(
|
||||||
playlist.playlist.name,
|
playlist.info.name,
|
||||||
playlist.playlist.playlistid,
|
playlist.info.playlistid,
|
||||||
playlist.playlist.tracks
|
playlist.tracks
|
||||||
);
|
);
|
||||||
queue.play(playlist.playlist.tracks[0]);
|
queue.play(playlist.tracks[0]);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -53,13 +53,9 @@ function play() {
|
|||||||
height: 2.5rem;
|
height: 2.5rem;
|
||||||
padding-left: 0.75rem;
|
padding-left: 0.75rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background: linear-gradient(
|
background: linear-gradient(34deg, $accent, $red);
|
||||||
34deg,
|
|
||||||
rgba(255, 166, 0, 0.644) 30%,
|
|
||||||
rgb(214, 188, 38)
|
|
||||||
);
|
|
||||||
user-select: none;
|
user-select: none;
|
||||||
transition: all 0.5s ease;
|
transition: all 0.5s ease-in-out;
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
height: 2rem;
|
height: 2rem;
|
||||||
|
|||||||
@@ -67,6 +67,7 @@
|
|||||||
import perks from "../../composables/perks.js";
|
import perks from "../../composables/perks.js";
|
||||||
import useContextStore from "../../stores/context";
|
import useContextStore from "../../stores/context";
|
||||||
import useModalStore from "../../stores/modal";
|
import useModalStore from "../../stores/modal";
|
||||||
|
import { ContextSrc } from "../../composables/enums";
|
||||||
|
|
||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import trackContext from "../../contexts/track_context";
|
import trackContext from "../../contexts/track_context";
|
||||||
@@ -80,7 +81,9 @@ const showContextMenu = (e: Event) => {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
contextStore.showContextMenu(e, trackContext(props.song, modalStore));
|
const menus = trackContext(props.song, modalStore);
|
||||||
|
|
||||||
|
contextStore.showContextMenu(e, menus, ContextSrc.Track);
|
||||||
context_on.value = true;
|
context_on.value = true;
|
||||||
|
|
||||||
contextStore.$subscribe((mutation, state) => {
|
contextStore.$subscribe((mutation, state) => {
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import { ref } from "vue";
|
|||||||
import perks from "../../composables/perks";
|
import perks from "../../composables/perks";
|
||||||
import trackContext from "../../contexts/track_context";
|
import trackContext from "../../contexts/track_context";
|
||||||
import { Track } from "../../interfaces";
|
import { Track } from "../../interfaces";
|
||||||
|
import { ContextSrc } from "../../composables/enums";
|
||||||
|
|
||||||
import useContextStore from "../../stores/context";
|
import useContextStore from "../../stores/context";
|
||||||
import useModalStore from "../../stores/modal";
|
import useModalStore from "../../stores/modal";
|
||||||
@@ -58,7 +59,9 @@ const showContextMenu = (e: Event) => {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
contextStore.showContextMenu(e, trackContext(props.track, modalStore));
|
const menus = trackContext(props.track, modalStore);
|
||||||
|
|
||||||
|
contextStore.showContextMenu(e, menus, ContextSrc.Track);
|
||||||
context_on.value = true;
|
context_on.value = true;
|
||||||
|
|
||||||
contextStore.$subscribe((mutation, state) => {
|
contextStore.$subscribe((mutation, state) => {
|
||||||
|
|||||||
@@ -10,3 +10,16 @@ export enum NotifType {
|
|||||||
Info,
|
Info,
|
||||||
Error,
|
Error,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum FromOptions {
|
||||||
|
playlist = "playlist",
|
||||||
|
folder = "folder",
|
||||||
|
album = "album",
|
||||||
|
search = "search",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ContextSrc {
|
||||||
|
PHeader = "PHeader",
|
||||||
|
Track = "Track",
|
||||||
|
AHeader = "AHeader",
|
||||||
|
}
|
||||||
|
|||||||
@@ -150,9 +150,24 @@ function formatSeconds(seconds) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getCurrentDate() {
|
||||||
|
const date = new Date();
|
||||||
|
|
||||||
|
const yyyy = date.getFullYear();
|
||||||
|
const mm = date.getMonth() + 1;
|
||||||
|
const dd = date.getDate();
|
||||||
|
|
||||||
|
const hh = date.getHours();
|
||||||
|
const min = date.getMinutes();
|
||||||
|
const sec = date.getSeconds();
|
||||||
|
|
||||||
|
return `${yyyy}-${mm}-${dd} ${hh}:${min}:${sec}`;
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
putCommas,
|
putCommas,
|
||||||
focusCurrent,
|
focusCurrent,
|
||||||
formatSeconds,
|
formatSeconds,
|
||||||
getElem,
|
getElem,
|
||||||
|
getCurrentDate,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import axios from "axios";
|
|||||||
import { Playlist, Track } from "../interfaces";
|
import { Playlist, Track } from "../interfaces";
|
||||||
import { Notification, NotifType } from "../stores/notification";
|
import { Notification, NotifType } from "../stores/notification";
|
||||||
import state from "./state";
|
import state from "./state";
|
||||||
|
import { getCurrentDate } from "../composables/perks";
|
||||||
/**
|
/**
|
||||||
* Creates a new playlist on the server.
|
* Creates a new playlist on the server.
|
||||||
* @param playlist_name The name of the playlist to create.
|
* @param playlist_name The name of the playlist to create.
|
||||||
@@ -13,6 +13,7 @@ async function createNewPlaylist(playlist_name: string, track?: Track) {
|
|||||||
await axios
|
await axios
|
||||||
.post(state.settings.uri + "/playlist/new", {
|
.post(state.settings.uri + "/playlist/new", {
|
||||||
name: playlist_name,
|
name: playlist_name,
|
||||||
|
lastUpdated: getCurrentDate(),
|
||||||
})
|
})
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
new Notification("✅ Playlist created successfullly!");
|
new Notification("✅ Playlist created successfullly!");
|
||||||
@@ -60,7 +61,6 @@ async function getAllPlaylists(): Promise<Playlist[]> {
|
|||||||
|
|
||||||
async function addTrackToPlaylist(playlist: Playlist, track: Track) {
|
async function addTrackToPlaylist(playlist: Playlist, track: Track) {
|
||||||
const uri = `${state.settings.uri}/playlist/${playlist.playlistid}/add`;
|
const uri = `${state.settings.uri}/playlist/${playlist.playlistid}/add`;
|
||||||
console.log(track.trackid, playlist.playlistid);
|
|
||||||
|
|
||||||
await axios
|
await axios
|
||||||
.post(uri, { track: track.trackid })
|
.post(uri, { track: track.trackid })
|
||||||
@@ -95,12 +95,16 @@ async function getPTracks(playlistid: string) {
|
|||||||
async function getPlaylist(pid: string) {
|
async function getPlaylist(pid: string) {
|
||||||
const uri = state.settings.uri + "/playlist/" + pid;
|
const uri = state.settings.uri + "/playlist/" + pid;
|
||||||
|
|
||||||
let playlist: Playlist;
|
let playlist = {
|
||||||
|
info: {},
|
||||||
|
tracks: <Track[]>[],
|
||||||
|
};
|
||||||
|
|
||||||
await axios
|
await axios
|
||||||
.get(uri)
|
.get(uri)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
playlist = res.data.data;
|
playlist.info = res.data.info;
|
||||||
|
playlist.tracks = res.data.tracks;
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
new Notification("Something funny happened!", NotifType.Error);
|
new Notification("Something funny happened!", NotifType.Error);
|
||||||
@@ -110,4 +114,30 @@ async function getPlaylist(pid: string) {
|
|||||||
return playlist;
|
return playlist;
|
||||||
}
|
}
|
||||||
|
|
||||||
export { createNewPlaylist, getAllPlaylists, addTrackToPlaylist, getPTracks, getPlaylist };
|
async function updatePlaylist(pid: string, playlist: FormData, pStore: any) {
|
||||||
|
const uri = state.settings.uri + "/playlist/" + pid + "/update";
|
||||||
|
|
||||||
|
await axios
|
||||||
|
.put(uri, playlist, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "multipart/form-data",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
pStore.updatePInfo(res.data.data);
|
||||||
|
new Notification("Playlist updated!");
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
new Notification("Something funny happened!", NotifType.Error);
|
||||||
|
throw new Error(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
createNewPlaylist,
|
||||||
|
getAllPlaylists,
|
||||||
|
addTrackToPlaylist,
|
||||||
|
getPTracks,
|
||||||
|
getPlaylist,
|
||||||
|
updatePlaylist,
|
||||||
|
};
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { Option } from "../interfaces";
|
||||||
|
|
||||||
|
export default async () => {
|
||||||
|
const deletePlaylist: Option = {
|
||||||
|
label: "Delete playlist",
|
||||||
|
critical: true,
|
||||||
|
action: () => {
|
||||||
|
console.log("delete playlist");
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const playNext: Option = {
|
||||||
|
label: "Play next",
|
||||||
|
action: () => {
|
||||||
|
console.log("play next");
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const addToQueue: Option = {
|
||||||
|
label: "Add to queue",
|
||||||
|
action: () => {
|
||||||
|
console.log("add to queue");
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
return [playNext, addToQueue, deletePlaylist];
|
||||||
|
};
|
||||||
+33
-4
@@ -1,4 +1,5 @@
|
|||||||
import { NotifType } from "./stores/enums";
|
import { FromOptions } from "./composables/enums";
|
||||||
|
import { NotifType } from "./composables/enums";
|
||||||
|
|
||||||
interface Track {
|
interface Track {
|
||||||
trackid: string;
|
trackid: string;
|
||||||
@@ -51,10 +52,11 @@ interface Playlist {
|
|||||||
playlistid: string;
|
playlistid: string;
|
||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
image?: string;
|
image?: string | FormData;
|
||||||
tracks?: Track[];
|
tracks?: Track[];
|
||||||
count?: number;
|
count?: number;
|
||||||
lastUpdated?: number;
|
lastUpdated?: string;
|
||||||
|
color?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Notif {
|
interface Notif {
|
||||||
@@ -62,4 +64,31 @@ interface Notif {
|
|||||||
type: NotifType;
|
type: NotifType;
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Track, Folder, AlbumInfo, Artist, Option, Playlist, Notif };
|
interface fromFolder {
|
||||||
|
type: FromOptions;
|
||||||
|
path: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
interface fromAlbum {
|
||||||
|
type: FromOptions;
|
||||||
|
name: string;
|
||||||
|
albumartist: string;
|
||||||
|
}
|
||||||
|
interface fromPlaylist {
|
||||||
|
type: FromOptions;
|
||||||
|
name: string;
|
||||||
|
playlistid: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Track,
|
||||||
|
Folder,
|
||||||
|
AlbumInfo,
|
||||||
|
Artist,
|
||||||
|
Option,
|
||||||
|
Playlist,
|
||||||
|
Notif,
|
||||||
|
fromFolder,
|
||||||
|
fromAlbum,
|
||||||
|
fromPlaylist,
|
||||||
|
};
|
||||||
|
|||||||
+11
-4
@@ -1,23 +1,30 @@
|
|||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
import normalize from "../composables/normalizeContextMenu";
|
import normalize from "../composables/normalizeContextMenu";
|
||||||
import { Option } from "../interfaces";
|
import { Option } from "../interfaces";
|
||||||
|
import { ContextSrc } from "../composables/enums";
|
||||||
|
|
||||||
export default defineStore("context-menu", {
|
export default defineStore("context-menu", {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
visible: false,
|
visible: false,
|
||||||
options: Array<Option>(),
|
options: <Option[]>[],
|
||||||
x: 500,
|
x: 500,
|
||||||
y: 500,
|
y: 500,
|
||||||
normalizedX: false,
|
normalizedX: false,
|
||||||
normalizedY: false,
|
normalizedY: false,
|
||||||
|
src: "",
|
||||||
}),
|
}),
|
||||||
actions: {
|
actions: {
|
||||||
showContextMenu(e: any, context_options: Promise<Option[]>) {
|
showContextMenu(
|
||||||
|
e: any,
|
||||||
|
context_options: Promise<Option[]>,
|
||||||
|
src: ContextSrc
|
||||||
|
) {
|
||||||
if (this.visible) {
|
if (this.visible) {
|
||||||
this.visible = false;
|
this.visible = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.visible = true;
|
||||||
context_options.then((options) => {
|
context_options.then((options) => {
|
||||||
this.options = options;
|
this.options = options;
|
||||||
});
|
});
|
||||||
@@ -29,11 +36,11 @@ export default defineStore("context-menu", {
|
|||||||
|
|
||||||
this.normalizedX = yo.normalizedX;
|
this.normalizedX = yo.normalizedX;
|
||||||
this.normalizedY = yo.normalizedY;
|
this.normalizedY = yo.normalizedY;
|
||||||
|
this.src = src;
|
||||||
this.visible = true;
|
|
||||||
},
|
},
|
||||||
hideContextMenu() {
|
hideContextMenu() {
|
||||||
this.visible = false;
|
this.visible = false;
|
||||||
|
this.src = null;
|
||||||
},
|
},
|
||||||
hasManyChildren() {
|
hasManyChildren() {
|
||||||
let result = false;
|
let result = false;
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
+10
-4
@@ -1,8 +1,9 @@
|
|||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
import { Track } from "../interfaces";
|
import { Playlist, Track } from "../interfaces";
|
||||||
|
|
||||||
enum ModalOptions {
|
enum ModalOptions {
|
||||||
newPlaylist,
|
newPlaylist = "newPlaylist",
|
||||||
editPlaylist,
|
updatePlaylist = "editPlaylist",
|
||||||
}
|
}
|
||||||
|
|
||||||
export default defineStore("newModal", {
|
export default defineStore("newModal", {
|
||||||
@@ -10,7 +11,7 @@ export default defineStore("newModal", {
|
|||||||
title: "",
|
title: "",
|
||||||
options: ModalOptions,
|
options: ModalOptions,
|
||||||
component: "",
|
component: "",
|
||||||
props: {},
|
props: <any>{},
|
||||||
visible: false,
|
visible: false,
|
||||||
}),
|
}),
|
||||||
actions: {
|
actions: {
|
||||||
@@ -23,6 +24,11 @@ export default defineStore("newModal", {
|
|||||||
this.props.track = track;
|
this.props.track = track;
|
||||||
this.visible = true;
|
this.visible = true;
|
||||||
},
|
},
|
||||||
|
showEditPlaylistModal(playlist: Playlist) {
|
||||||
|
this.component = ModalOptions.updatePlaylist;
|
||||||
|
this.props = playlist;
|
||||||
|
this.visible = true;
|
||||||
|
},
|
||||||
hideModal() {
|
hideModal() {
|
||||||
this.visible = false;
|
this.visible = false;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,15 +1,20 @@
|
|||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
import { getPlaylist, getPTracks } from "../composables/playlists";
|
import { getPlaylist } from "../composables/playlists";
|
||||||
import { Track, Playlist } from "../interfaces";
|
import { Track, Playlist } from "../interfaces";
|
||||||
|
|
||||||
export default defineStore("playlist-tracks", {
|
export default defineStore("playlist-tracks", {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
playlist: <Playlist>{},
|
info: <Playlist>{},
|
||||||
|
tracks: <Track[]>[],
|
||||||
}),
|
}),
|
||||||
actions: {
|
actions: {
|
||||||
async fetchAll(playlistid: string) {
|
async fetchAll(playlistid: string) {
|
||||||
const playlist = await getPlaylist(playlistid);
|
const playlist = await getPlaylist(playlistid);
|
||||||
this.playlist = playlist;
|
this.info = playlist.info;
|
||||||
|
this.tracks = playlist.tracks;
|
||||||
|
},
|
||||||
|
updatePInfo(info: Playlist) {
|
||||||
|
this.info = info;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
+10
-32
@@ -1,31 +1,8 @@
|
|||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
import state from "../composables/state";
|
import state from "../composables/state";
|
||||||
import { Track } from "../interfaces";
|
import { Track, fromFolder, fromAlbum, fromPlaylist } from "../interfaces";
|
||||||
import notif from "../composables/mediaNotification";
|
import notif from "../composables/mediaNotification";
|
||||||
|
import { FromOptions } from "../composables/enums";
|
||||||
enum FromOptions {
|
|
||||||
playlist = "Playlist",
|
|
||||||
folder = "Folder",
|
|
||||||
album = "Album",
|
|
||||||
search = "Search",
|
|
||||||
}
|
|
||||||
|
|
||||||
interface fromFolder {
|
|
||||||
type: FromOptions.folder;
|
|
||||||
path: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface fromAlbum {
|
|
||||||
type: FromOptions.album;
|
|
||||||
name: string;
|
|
||||||
albumartist: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface fromPlaylist {
|
|
||||||
type: FromOptions.playlist;
|
|
||||||
name: string;
|
|
||||||
playlistid: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function addQToLocalStorage(
|
function addQToLocalStorage(
|
||||||
from: fromFolder | fromAlbum | fromPlaylist,
|
from: fromFolder | fromAlbum | fromPlaylist,
|
||||||
@@ -84,7 +61,6 @@ export default defineStore("Queue", {
|
|||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.audio.play().then(() => {
|
this.audio.play().then(() => {
|
||||||
|
|
||||||
this.playing = true;
|
this.playing = true;
|
||||||
notif(track, this.playPause, this.playNext, this.playPrev);
|
notif(track, this.playPause, this.playNext, this.playPrev);
|
||||||
|
|
||||||
@@ -108,6 +84,7 @@ export default defineStore("Queue", {
|
|||||||
this.play(this.current);
|
this.play(this.current);
|
||||||
} else if (this.audio.paused) {
|
} else if (this.audio.paused) {
|
||||||
this.audio.play();
|
this.audio.play();
|
||||||
|
this.playing = true;
|
||||||
} else {
|
} else {
|
||||||
this.audio.pause();
|
this.audio.pause();
|
||||||
this.playing = false;
|
this.playing = false;
|
||||||
@@ -131,6 +108,7 @@ export default defineStore("Queue", {
|
|||||||
},
|
},
|
||||||
readQueueFromLocalStorage() {
|
readQueueFromLocalStorage() {
|
||||||
const queue = localStorage.getItem("queue");
|
const queue = localStorage.getItem("queue");
|
||||||
|
|
||||||
if (queue) {
|
if (queue) {
|
||||||
const parsed = JSON.parse(queue);
|
const parsed = JSON.parse(queue);
|
||||||
this.from = parsed.from;
|
this.from = parsed.from;
|
||||||
@@ -180,30 +158,30 @@ export default defineStore("Queue", {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
playFromFolder(fpath: string, tracks: Track[]) {
|
playFromFolder(fpath: string, tracks: Track[]) {
|
||||||
this.setNewQueue(tracks);
|
|
||||||
|
|
||||||
this.from = <fromFolder>{
|
this.from = <fromFolder>{
|
||||||
type: FromOptions.folder,
|
type: FromOptions.folder,
|
||||||
path: fpath,
|
path: fpath,
|
||||||
|
name: fpath.split("/").splice(-1).join(""),
|
||||||
};
|
};
|
||||||
|
this.setNewQueue(tracks);
|
||||||
},
|
},
|
||||||
playFromAlbum(aname: string, albumartist: string, tracks: Track[]) {
|
playFromAlbum(aname: string, albumartist: string, tracks: Track[]) {
|
||||||
this.setNewQueue(tracks);
|
|
||||||
|
|
||||||
this.from = <fromAlbum>{
|
this.from = <fromAlbum>{
|
||||||
type: FromOptions.album,
|
type: FromOptions.album,
|
||||||
name: aname,
|
name: aname,
|
||||||
albumartist: albumartist,
|
albumartist: albumartist,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.setNewQueue(tracks);
|
||||||
},
|
},
|
||||||
playFromPlaylist(pname: string, pid: string, tracks: Track[]) {
|
playFromPlaylist(pname: string, pid: string, tracks: Track[]) {
|
||||||
this.setNewQueue(tracks);
|
|
||||||
|
|
||||||
this.from = <fromPlaylist>{
|
this.from = <fromPlaylist>{
|
||||||
type: FromOptions.playlist,
|
type: FromOptions.playlist,
|
||||||
name: pname,
|
name: pname,
|
||||||
playlistid: pid,
|
playlistid: pid,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.setNewQueue(tracks);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="al-view rounded">
|
<div class="al-view rounded">
|
||||||
<div>
|
<div>
|
||||||
<Header :album_info="album.info" />
|
<Header :album="album.info" />
|
||||||
</div>
|
</div>
|
||||||
<div class="separator" id="av-sep"></div>
|
<div class="separator" id="av-sep"></div>
|
||||||
<div class="songs rounded">
|
<div class="songs rounded">
|
||||||
|
|||||||
+19
-15
@@ -1,14 +1,26 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="playlist-view">
|
<div class="playlist-view">
|
||||||
<Header :info="info" />
|
<Header :info="playlist.info" />
|
||||||
<div class="separator no-border"></div>
|
<div class="separator no-border"></div>
|
||||||
|
|
||||||
<div class="songlist rounded">
|
<div class="songlist rounded">
|
||||||
<SongList
|
<div v-if="playlist.tracks.length">
|
||||||
:tracks="playlist.tracks"
|
<SongList
|
||||||
:pname="info.name"
|
:tracks="playlist.tracks"
|
||||||
:playlistid="playlist.playlistid"
|
:pname="playlist.info.name"
|
||||||
/>
|
:playlistid="playlist.info.playlistid"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="playlist.tracks.length === 0 && playlist.info.count > 0">
|
||||||
|
<div class="no-results">
|
||||||
|
<div class="text">We can't find your music 🦋</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="playlist.tracks.length === 0 && playlist.info.count == 0">
|
||||||
|
<div class="no-results">
|
||||||
|
<div class="text">Nothing here</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="separator no-border"></div>
|
<div class="separator no-border"></div>
|
||||||
<FeaturedArtists />
|
<FeaturedArtists />
|
||||||
@@ -21,15 +33,7 @@ import SongList from "../components/FolderView/SongList.vue";
|
|||||||
import FeaturedArtists from "../components/PlaylistView/FeaturedArtists.vue";
|
import FeaturedArtists from "../components/PlaylistView/FeaturedArtists.vue";
|
||||||
import usePTrackStore from "../stores/p.ptracks";
|
import usePTrackStore from "../stores/p.ptracks";
|
||||||
|
|
||||||
const playlist = usePTrackStore().playlist;
|
const playlist = usePTrackStore();
|
||||||
|
|
||||||
const info = {
|
|
||||||
name: playlist.name,
|
|
||||||
count: playlist.tracks.length,
|
|
||||||
desc: playlist.description,
|
|
||||||
duration: "3 hours, 4 minutes",
|
|
||||||
lastUpdated: "yesterday",
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="p-view">
|
<div id="p-view">
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
|
<NewPlaylistCard />
|
||||||
<PlaylistCard
|
<PlaylistCard
|
||||||
v-for="p in pStore.playlists"
|
v-for="p in pStore.playlists"
|
||||||
:key="p.playlistid"
|
:key="p.playlistid"
|
||||||
@@ -14,6 +15,7 @@
|
|||||||
import PlaylistCard from "../components/playlists/PlaylistCard.vue";
|
import PlaylistCard from "../components/playlists/PlaylistCard.vue";
|
||||||
|
|
||||||
import usePStore from "../stores/playlists";
|
import usePStore from "../stores/playlists";
|
||||||
|
import NewPlaylistCard from "../components/playlists/NewPlaylistCard.vue";
|
||||||
const pStore = usePStore();
|
const pStore = usePStore();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -22,11 +24,13 @@ const pStore = usePStore();
|
|||||||
margin: $small;
|
margin: $small;
|
||||||
padding: $small;
|
padding: $small;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
scrollbar-color: $gray2 transparent;
|
||||||
|
border-top: 1px solid $gray3;
|
||||||
|
|
||||||
.grid {
|
.grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
|
||||||
gap: 1rem;
|
gap: $small;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user