diff --git a/README.md b/README.md index 358d3759..03eaee3b 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,50 @@ ### 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 + ![](./images/1.png) -`installation instructions coming soon ...` \ No newline at end of file +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 + +![](./images/3.png) + +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 + +![](./images/4.png) +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 + +![](./images/2.png) +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. diff --git a/images/1.png b/images/1.png index d017d02a..980a32a1 100644 Binary files a/images/1.png and b/images/1.png differ diff --git a/images/2.png b/images/2.png new file mode 100644 index 00000000..49b69844 Binary files /dev/null and b/images/2.png differ diff --git a/images/3.png b/images/3.png new file mode 100644 index 00000000..04972d63 Binary files /dev/null and b/images/3.png differ diff --git a/images/4.png b/images/4.png new file mode 100644 index 00000000..2489fb04 Binary files /dev/null and b/images/4.png differ diff --git a/server/app/api/playlist.py b/server/app/api/playlist.py index 41cf8b73..ad568df7 100644 --- a/server/app/api/playlist.py +++ b/server/app/api/playlist.py @@ -1,13 +1,16 @@ """ Contains all the playlist routes. """ -from copy import deepcopy -from typing import List -from flask import Blueprint, request -from app import instances, api -from app.lib import playlistlib -from app import models +from datetime import datetime + +from app import api 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="/") @@ -17,14 +20,14 @@ TrackExistsInPlaylist = exceptions.TrackExistsInPlaylist @playlist_bp.route("/playlists", methods=["GET"]) def get_all_playlists(): - ppp = deepcopy(api.PLAYLISTS) - playlists = [] - - for pl in ppp: - pl.count = len(pl.tracks) - pl.tracks = [] - playlists.append(pl) - + playlists = [ + serializer.Playlist(p, construct_last_updated=False) + for p in api.PLAYLISTS + ] + playlists.sort( + key=lambda p: datetime.strptime(p.lastUpdated, "%Y-%m-%d %H:%M:%S"), + reverse=True, + ) return {"data": playlists} @@ -35,16 +38,16 @@ def create_playlist(): playlist = { "name": data["name"], "description": "", - "tracks": [], - "count": 0, - "lastUpdated": 0, + "pre_tracks": [], + "lastUpdated": data["lastUpdated"], + "image": "", } 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: 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 -@playlist_bp.route("/playlist/") -def get_single_p_info(playlist_id: str): +@playlist_bp.route("/playlist/") +def get_single_p_info(playlistid: str): for p in api.PLAYLISTS: - if p.playlistid == playlist_id: - p.count = len(p.tracks) - return {"data": p} + if p.playlistid == playlistid: + tracks = p.get_tracks() + return { + "info": serializer.Playlist(p), + "tracks": tracks, + } + + return {"info": {}, "tracks": []} + + +@playlist_bp.route("/playlist//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//info") diff --git a/server/app/db/playlists.py b/server/app/db/playlists.py index 4c411107..ca081062 100644 --- a/server/app/db/playlists.py +++ b/server/app/db/playlists.py @@ -1,15 +1,14 @@ """ This file contains the Playlists class for interacting with the playlist documents in MongoDB. """ - -from app import db, models +from app import db +from app import models from bson import ObjectId convert_many = db.convert_many convert_one = db.convert_one - class Playlists(db.Mongo): """ The class for all playlist-related database operations. @@ -24,8 +23,12 @@ class Playlists(db.Mongo): Inserts a new playlist object into the database. """ return self.collection.update_one( - {"name": playlist["name"]}, - {"$set": playlist}, + { + "name": playlist["name"] + }, + { + "$set": playlist + }, upsert=True, ).upserted_id @@ -43,20 +46,17 @@ class Playlists(db.Mongo): playlist = self.collection.find_one({"_id": ObjectId(id)}) 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. """ - track = { - "title": track.title, - "artists": track.artists, - "album": track.album, - } return self.collection.update_one( {"_id": ObjectId(playlistid)}, - {"$push": {"tracks": track}}, - ).modified_count + {"$push": { + "pre_tracks": track + }}, + ) def get_playlist_by_name(self, name: str) -> dict: """ @@ -64,3 +64,12 @@ class Playlists(db.Mongo): """ playlist = self.collection.find_one({"name": name}) 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}, + ) diff --git a/server/app/functions.py b/server/app/functions.py index 046a3489..c8ac77f9 100644 --- a/server/app/functions.py +++ b/server/app/functions.py @@ -81,7 +81,6 @@ def populate(): albumslib.create_everything() folderslib.run_scandir() - playlistlib.create_all_playlists() end = time.time() diff --git a/server/app/lib/playlistlib.py b/server/app/lib/playlistlib.py index c5b5d262..aa2b9fa9 100644 --- a/server/app/lib/playlistlib.py +++ b/server/app/lib/playlistlib.py @@ -1,10 +1,20 @@ """ This library contains all the functions related to playlists. """ -from progress.bar import Bar -from app import api, instances, models, exceptions, helpers +import os +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 PIL import Image +from PIL import ImageSequence +from progress.bar import Bar +from werkzeug import datastructures TrackExistsInPlaylist = exceptions.TrackExistsInPlaylist @@ -15,14 +25,21 @@ def add_track(playlistid: str, trackid: str): """ for playlist in api.PLAYLISTS: if playlist.playlistid == playlistid: - track = trackslib.get_track_by_id(trackid) + tt = trackslib.get_track_by_id(trackid) - if track not in playlist.tracks: - playlist.tracks.append(track) - instances.playlist_instance.add_track_to_playlist(playlistid, track) + track = { + "title": tt.title, + "artists": tt.artists, + "album": tt.album, + } + + try: + playlist.add_track(track) + instances.playlist_instance.add_track_to_playlist( + playlistid, track) return - else: - raise TrackExistsInPlaylist("Track already in playlist.") + except TrackExistsInPlaylist as e: + return {"error": str(e)}, 409 def get_playlist_tracks(pid: str): @@ -31,7 +48,6 @@ def get_playlist_tracks(pid: str): return p.tracks - def create_all_playlists(): """ Gets all playlists from the database. @@ -41,5 +57,32 @@ def create_all_playlists(): _bar = Bar("Creating playlists", max=len(playlists)) for playlist in playlists: api.PLAYLISTS.append(models.Playlist(playlist)) + _bar.next() _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 diff --git a/server/app/models.py b/server/app/models.py index d00f4689..3eeb3ca5 100644 --- a/server/app/models.py +++ b/server/app/models.py @@ -1,12 +1,14 @@ """ Contains all the models for objects generation and typing. """ - from dataclasses import dataclass +from dataclasses import field from datetime import date from typing import List + from app import api from app import settings +from app.exceptions import TrackExistsInPlaylist @dataclass @@ -71,11 +73,9 @@ class Album: def get_p_track(ptrack): for track in api.TRACKS: - if ( - track.title == ptrack["title"] - and track.artists == ptrack["artists"] - and ptrack["album"] == track.album - ): + if (track.title == ptrack["title"] + and track.artists == ptrack["artists"] + and ptrack["album"] == track.album): return track @@ -99,10 +99,11 @@ class Playlist: playlistid: str name: str - description: str - image: str tracks: List[Track] + _pre_tracks: list = field(init=False, repr=False) lastUpdated: int + image: str + description: str = "" count: int = 0 """A list of track objects in the playlist""" @@ -110,9 +111,44 @@ class Playlist: self.playlistid = data["_id"]["$oid"] self.name = data["name"] self.description = data["description"] - self.image = "" - self.tracks = create_playlist_tracks(data["tracks"]) + self.image = self.create_img_link(data["image"]) + self._pre_tracks = data["pre_tracks"] + self.tracks = [] 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 diff --git a/server/app/prep.py b/server/app/prep.py index 9fdfa920..db1d3b5d 100644 --- a/server/app/prep.py +++ b/server/app/prep.py @@ -13,27 +13,20 @@ def create_config_dir() -> None: _home_dir = os.path.expanduser("~") config_folder = os.path.join(_home_dir, settings.CONFIG_FOLDER) + print(config_folder) dirs = [ "", "images", os.path.join("images", "artists"), os.path.join("images", "thumbnails"), + os.path.join("images", "playlists"), ] for _dir in dirs: path = os.path.join(config_folder, _dir) + exists = os.path.exists(path) - try: - os.path.exists(path) - except FileNotFoundError: + if not exists: os.makedirs(path) 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 diff --git a/server/app/serializer.py b/server/app/serializer.py new file mode 100644 index 00000000..37874fad --- /dev/null +++ b/server/app/serializer.py @@ -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) diff --git a/server/app/settings.py b/server/app/settings.py index eea4b28f..2ad51f17 100644 --- a/server/app/settings.py +++ b/server/app/settings.py @@ -13,6 +13,7 @@ THUMBS_PATH = os.path.join(APP_DIR, "images", "thumbnails") IMG_BASE_URI = "http://127.0.0.1:8900/images/" IMG_ARTIST_URI = IMG_BASE_URI + "artists/" IMG_THUMB_URI = IMG_BASE_URI + "thumbnails/" +IMG_PLAYLIST_URI = IMG_BASE_URI + "playlists/" # defaults DEFAULT_ARTIST_IMG = IMG_ARTIST_URI + "0.webp" diff --git a/server/roadmap.md b/server/roadmap.md index 82626e18..895a6410 100644 --- a/server/roadmap.md +++ b/server/roadmap.md @@ -1,63 +1,39 @@ # 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 ⚠ - [ ] 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 -- [ ] Different songs having same link -- [ ] ConnectionError -- [ ] Move thumbnails to .config -- [ ] Write a multithreaded file server - [ ] Add support for WAV files -- [ ] Support multiple folders - [ ] Compress thumbnails # Features + + ## Needed features -- [ ] Seeking current song + - [ ] 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 -- [ ] Right click on song to do stuff - [ ] Adjust volume - [ ] Add listening statistics for all songs - [ ] Extract color from artist image [for use with artist card gradient] - [ ] Adding songs to favorites -- [ ] Adding songs to playlist - [ ] Playing song radio ## Future features + - [ ] Toggle shuffle - [ ] Toggle repeat -- [ ] Display artist albums - [ ] Suggest similar artists - [ ] Getting artist info -- [ ] Getting album info - [ ] Create a Python script to build, bundle and serve the app - [ ] Getting extra song info (probably from genius) - [ ] Getting lyrics -- [ ] Notifications - [ ] Sorting songs - [ ] Suggest undiscorvered artists, albums and songs - [ ] Remember last played song - [ ] Add next and previous song transition and progress bar reset animations -- [ ] Hover animations for list items -- [ ] Highlight currently playing song in playlist +- [ ] Add playlist to folder - [ ] Add functionality to 'Listen now' button -- [ ] Add a 'Scan' button to the sidebar - [ ] Paginated requests for songs - [ ] Package app as installable PWA - -## Finished ✅ \ No newline at end of file diff --git a/src/assets/css/global.scss b/src/assets/css/global.scss index 79e3b95a..ec154e4b 100644 --- a/src/assets/css/global.scss +++ b/src/assets/css/global.scss @@ -38,11 +38,7 @@ a { } .border { - border: solid 1px $gray; -} - -.border-sm { - border: solid 1px #27262654; + border: solid 1px $gray3; } .separator { @@ -63,21 +59,6 @@ a { 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 { display: grid; diff --git a/src/components/AlbumView/Header.vue b/src/components/AlbumView/Header.vue index 96694747..ab5c9b53 100644 --- a/src/components/AlbumView/Header.vue +++ b/src/components/AlbumView/Header.vue @@ -1,104 +1,89 @@ - - \ No newline at end of file + diff --git a/src/components/RightSideBar/Queue.vue b/src/components/RightSideBar/Queue.vue index 226662b1..ea324983 100644 --- a/src/components/RightSideBar/Queue.vue +++ b/src/components/RightSideBar/Queue.vue @@ -1,28 +1,7 @@ + + diff --git a/src/components/RightSideBar/queue/upNext.vue b/src/components/RightSideBar/queue/upNext.vue new file mode 100644 index 00000000..d9d0c0b8 --- /dev/null +++ b/src/components/RightSideBar/queue/upNext.vue @@ -0,0 +1,90 @@ + + + + + diff --git a/src/components/contextMenu.vue b/src/components/contextMenu.vue index 9d43f423..65927b36 100644 --- a/src/components/contextMenu.vue +++ b/src/components/contextMenu.vue @@ -12,6 +12,7 @@ 'context-many-kids': context.hasManyChildren(), }, ]" + id="context-menu" :style="{ left: context.x + 'px', top: context.y + 'px', @@ -56,7 +57,6 @@ const context = useContextStore(); top: 0; left: 0; width: 12rem; - height: min-content; z-index: 10; transform: scale(0); @@ -89,6 +89,7 @@ const context = useContextStore(); position: absolute; right: -13rem; width: 13rem; + top: -0.5rem; max-height: 21.25rem; padding: $small !important; diff --git a/src/components/modal.vue b/src/components/modal.vue index 0aa74ffb..39bb197c 100644 --- a/src/components/modal.vue +++ b/src/components/modal.vue @@ -10,6 +10,12 @@ @hideModal="hideModal" @title="title" /> + @@ -17,6 +23,7 @@ + + diff --git a/src/components/playlists/NewPlaylistCard.vue b/src/components/playlists/NewPlaylistCard.vue new file mode 100644 index 00000000..502b4cd6 --- /dev/null +++ b/src/components/playlists/NewPlaylistCard.vue @@ -0,0 +1,43 @@ + + + diff --git a/src/components/playlists/PlaylistCard.vue b/src/components/playlists/PlaylistCard.vue index b0b468b7..931868e2 100644 --- a/src/components/playlists/PlaylistCard.vue +++ b/src/components/playlists/PlaylistCard.vue @@ -2,48 +2,88 @@ -
+
+
+
+
+ +
{{ props.playlist.name }}
-
{{ props.playlist.count }} Tracks
+
+ No Tracks + {{ props.playlist.count }} Track + {{ props.playlist.count }} Tracks +
diff --git a/src/components/shared/PlayBtn.vue b/src/components/shared/PlayBtn.vue index faf18ca2..917f222c 100644 --- a/src/components/shared/PlayBtn.vue +++ b/src/components/shared/PlayBtn.vue @@ -1,23 +1,25 @@ - + diff --git a/src/components/shared/PlayBtnRect.vue b/src/components/shared/PlayBtnRect.vue index 9599123b..90857423 100644 --- a/src/components/shared/PlayBtnRect.vue +++ b/src/components/shared/PlayBtnRect.vue @@ -35,11 +35,11 @@ function play() { break; case playSources.playlist: queue.playFromPlaylist( - playlist.playlist.name, - playlist.playlist.playlistid, - playlist.playlist.tracks + playlist.info.name, + playlist.info.playlistid, + playlist.tracks ); - queue.play(playlist.playlist.tracks[0]); + queue.play(playlist.tracks[0]); break; } } @@ -53,13 +53,9 @@ function play() { height: 2.5rem; padding-left: 0.75rem; cursor: pointer; - background: linear-gradient( - 34deg, - rgba(255, 166, 0, 0.644) 30%, - rgb(214, 188, 38) - ); + background: linear-gradient(34deg, $accent, $red); user-select: none; - transition: all 0.5s ease; + transition: all 0.5s ease-in-out; .icon { height: 2rem; diff --git a/src/components/shared/SongItem.vue b/src/components/shared/SongItem.vue index 883ae3a1..6879ee6d 100644 --- a/src/components/shared/SongItem.vue +++ b/src/components/shared/SongItem.vue @@ -67,6 +67,7 @@ import perks from "../../composables/perks.js"; import useContextStore from "../../stores/context"; import useModalStore from "../../stores/modal"; +import { ContextSrc } from "../../composables/enums"; import { ref } from "vue"; import trackContext from "../../contexts/track_context"; @@ -80,7 +81,9 @@ const showContextMenu = (e: Event) => { e.preventDefault(); 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; contextStore.$subscribe((mutation, state) => { diff --git a/src/components/shared/TrackItem.vue b/src/components/shared/TrackItem.vue index 011a2806..87958570 100644 --- a/src/components/shared/TrackItem.vue +++ b/src/components/shared/TrackItem.vue @@ -39,6 +39,7 @@ import { ref } from "vue"; import perks from "../../composables/perks"; import trackContext from "../../contexts/track_context"; import { Track } from "../../interfaces"; +import { ContextSrc } from "../../composables/enums"; import useContextStore from "../../stores/context"; import useModalStore from "../../stores/modal"; @@ -58,7 +59,9 @@ const showContextMenu = (e: Event) => { e.preventDefault(); 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; contextStore.$subscribe((mutation, state) => { diff --git a/src/composables/enums.ts b/src/composables/enums.ts index 0ae05f6c..a69ebdae 100644 --- a/src/composables/enums.ts +++ b/src/composables/enums.ts @@ -10,3 +10,16 @@ export enum NotifType { Info, Error, } + +export enum FromOptions { + playlist = "playlist", + folder = "folder", + album = "album", + search = "search", +} + +export enum ContextSrc { + PHeader = "PHeader", + Track = "Track", + AHeader = "AHeader", +} diff --git a/src/composables/perks.js b/src/composables/perks.js index 5159a8b1..8ab43192 100644 --- a/src/composables/perks.js +++ b/src/composables/perks.js @@ -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 { putCommas, focusCurrent, formatSeconds, getElem, + getCurrentDate, }; diff --git a/src/composables/playlists.ts b/src/composables/playlists.ts index 74f4c839..a88202d2 100644 --- a/src/composables/playlists.ts +++ b/src/composables/playlists.ts @@ -2,7 +2,7 @@ import axios from "axios"; import { Playlist, Track } from "../interfaces"; import { Notification, NotifType } from "../stores/notification"; import state from "./state"; - +import { getCurrentDate } from "../composables/perks"; /** * Creates a new playlist on the server. * @param playlist_name The name of the playlist to create. @@ -13,6 +13,7 @@ async function createNewPlaylist(playlist_name: string, track?: Track) { await axios .post(state.settings.uri + "/playlist/new", { name: playlist_name, + lastUpdated: getCurrentDate(), }) .then((res) => { new Notification("✅ Playlist created successfullly!"); @@ -60,7 +61,6 @@ async function getAllPlaylists(): Promise { async function addTrackToPlaylist(playlist: Playlist, track: Track) { const uri = `${state.settings.uri}/playlist/${playlist.playlistid}/add`; - console.log(track.trackid, playlist.playlistid); await axios .post(uri, { track: track.trackid }) @@ -95,12 +95,16 @@ async function getPTracks(playlistid: string) { async function getPlaylist(pid: string) { const uri = state.settings.uri + "/playlist/" + pid; - let playlist: Playlist; + let playlist = { + info: {}, + tracks: [], + }; await axios .get(uri) .then((res) => { - playlist = res.data.data; + playlist.info = res.data.info; + playlist.tracks = res.data.tracks; }) .catch((err) => { new Notification("Something funny happened!", NotifType.Error); @@ -110,4 +114,30 @@ async function getPlaylist(pid: string) { 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, +}; diff --git a/src/contexts/playlist.ts b/src/contexts/playlist.ts new file mode 100644 index 00000000..f06eecd7 --- /dev/null +++ b/src/contexts/playlist.ts @@ -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]; +}; diff --git a/src/interfaces.ts b/src/interfaces.ts index 254208b2..a4e1dad7 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -1,4 +1,5 @@ -import { NotifType } from "./stores/enums"; +import { FromOptions } from "./composables/enums"; +import { NotifType } from "./composables/enums"; interface Track { trackid: string; @@ -51,10 +52,11 @@ interface Playlist { playlistid: string; name: string; description?: string; - image?: string; + image?: string | FormData; tracks?: Track[]; count?: number; - lastUpdated?: number; + lastUpdated?: string; + color?: string; } interface Notif { @@ -62,4 +64,31 @@ interface Notif { 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, +}; diff --git a/src/stores/context.ts b/src/stores/context.ts index 7eb34e38..2c46203b 100644 --- a/src/stores/context.ts +++ b/src/stores/context.ts @@ -1,23 +1,30 @@ import { defineStore } from "pinia"; import normalize from "../composables/normalizeContextMenu"; import { Option } from "../interfaces"; +import { ContextSrc } from "../composables/enums"; export default defineStore("context-menu", { state: () => ({ visible: false, - options: Array