Merge pull request #30 from geoffrey45/integrate-topbar

-  Adds back and forward buttons to topbar
- Adds square images in playlists page
- Adds thumbnail creation to playlists
- Other minor refactors
This commit is contained in:
Mungai Geoffrey 2022-04-14 11:41:47 +03:00 committed by GitHub
commit 68b474bbba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 311 additions and 156 deletions

View File

@ -40,6 +40,7 @@ def create_playlist():
"pre_tracks": [],
"lastUpdated": data["lastUpdated"],
"image": "",
"thumb": "",
}
try:
@ -100,15 +101,20 @@ def update_playlist(playlistid: str):
"description": str(data.get("description").strip()),
"lastUpdated": str(data.get("lastUpdated")),
"image": None,
"thumb": None,
}
for p in api.PLAYLISTS:
if p.playlistid == playlistid:
if image:
playlist["image"] = playlistlib.save_p_image(image, playlistid)
image_, thumb_ = playlistlib.save_p_image(image, playlistid)
playlist["image"] = image_
playlist["thumb"] = thumb_
else:
playlist["image"] = p.image.split("/")[-1]
playlist["thumb"] = p.thumb.split("/")[-1]
p.update_playlist(playlist)
instances.playlist_instance.update_playlist(playlistid, playlist)

View File

@ -19,7 +19,7 @@ def send_track_file(trackid):
file["filepath"] for file in api.PRE_TRACKS
if file["_id"]["$oid"] == trackid
][0]
except (FileNotFoundError, IndexError):
except (FileNotFoundError, IndexError) as e:
return "File not found", 404
return send_file(filepath, mimetype="audio/mp3")

View File

@ -1,8 +1,7 @@
"""
This file contains the Album class for interacting with
This file contains the Album class for interacting with
album documents in MongoDB.
"""
from app import db
from bson import ObjectId
@ -16,16 +15,21 @@ class Albums(db.Mongo):
"""
def __init__(self):
super(Albums, self).__init__("ALBUMS")
self.collection = self.db["ALBUMS"]
super(Albums, self).__init__("ALICE_ALBUMS")
self.collection = self.db["ALL_ALBUMS"]
def insert_album(self, album: dict) -> None:
"""
Inserts a new album object into the database.
"""
return self.collection.update_one(
{"album": album["album"], "artist": album["artist"]},
{"$set": album},
{
"album": album["album"],
"artist": album["artist"]
},
{
"$set": album
},
upsert=True,
).upserted_id

View File

@ -1,7 +1,6 @@
"""
This file contains the Artists class for interacting with artist documents in MongoDB.
"""
from app import db
from bson import ObjectId
@ -12,16 +11,17 @@ class Artists(db.Mongo):
"""
def __init__(self):
super(Artists, self).__init__("ALL_ARTISTS")
self.collection = self.db["THEM_ARTISTS"]
super(Artists, self).__init__("ALICE_ARTISTS")
self.collection = self.db["ALL_ARTISTS"]
def insert_artist(self, artist_obj: dict) -> None:
"""
Inserts an artist into the database.
"""
self.collection.update_one(
artist_obj, {"$set": artist_obj}, upsert=True
).upserted_id
self.collection.update_one(artist_obj, {
"$set": artist_obj
},
upsert=True).upserted_id
def get_all_artists(self) -> list:
"""

View File

@ -15,8 +15,8 @@ class Playlists(db.Mongo):
"""
def __init__(self):
super(Playlists, self).__init__("PLAYLISTS")
self.collection = self.db["PLAYLISTS"]
super(Playlists, self).__init__("ALICE_PLAYLISTS")
self.collection = self.db["ALL_PLAYLISTS"]
def insert_playlist(self, playlist: dict) -> None:
"""

View File

@ -1,7 +1,6 @@
"""
This file contains the TrackColors class for interacting with Track colors documents in MongoDB.
"""
from app import db
@ -11,7 +10,7 @@ class TrackColors(db.Mongo):
"""
def __init__(self):
super(TrackColors, self).__init__("TRACK_COLORS")
super(TrackColors, self).__init__("ALICE_TRACK_COLORS")
self.collection = self.db["TRACK_COLORS"]
def insert_track_color(self, track_color: dict) -> None:
@ -19,8 +18,12 @@ class TrackColors(db.Mongo):
Inserts a new track object into the database.
"""
return self.collection.update_one(
{"filepath": track_color["filepath"]},
{"$set": track_color},
{
"filepath": track_color["filepath"]
},
{
"$set": track_color
},
upsert=True,
).upserted_id

View File

@ -1,7 +1,6 @@
"""
This file contains the AllSongs class for interacting with track documents in MongoDB.
"""
from app import db
from bson import ObjectId
@ -15,8 +14,8 @@ class AllSongs(db.Mongo):
"""
def __init__(self):
super(AllSongs, self).__init__("ALL_SONGS")
self.collection = self.db["ALL_SONGS"]
super(AllSongs, self).__init__("ALICE_MUSIC_TRACKS")
self.collection = self.db["ALL_TRACKS"]
# def drop_db(self):
# self.collection.drop()
@ -25,9 +24,12 @@ class AllSongs(db.Mongo):
"""
Inserts a new track object into the database.
"""
return self.collection.update_one(
{"filepath": song_obj["filepath"]}, {"$set": song_obj}, upsert=True
).upserted_id
return self.collection.update_one({
"filepath": song_obj["filepath"]
}, {
"$set": song_obj
},
upsert=True).upserted_id
def get_all_songs(self) -> list:
"""
@ -53,22 +55,33 @@ class AllSongs(db.Mongo):
"""
Returns all the songs matching the albums in the query params (using regex).
"""
songs = self.collection.find({"album": {"$regex": query, "$options": "i"}})
songs = self.collection.find(
{"album": {
"$regex": query,
"$options": "i"
}})
return convert_many(songs)
def search_songs_by_artist(self, query: str) -> list:
"""
Returns all the songs matching the artists in the query params.
"""
songs = self.collection.find({"artists": {"$regex": query, "$options": "i"}})
songs = self.collection.find(
{"artists": {
"$regex": query,
"$options": "i"
}})
return convert_many(songs)
def find_song_by_title(self, query: str) -> list:
"""
Finds all the tracks matching the title in the query params.
"""
self.collection.create_index([("title", db.pymongo.TEXT)])
song = self.collection.find({"title": {"$regex": query, "$options": "i"}})
song = self.collection.find(
{"title": {
"$regex": query,
"$options": "i"
}})
return convert_many(song)
def find_songs_by_album(self, name: str, artist: str) -> list:
@ -82,7 +95,9 @@ class AllSongs(db.Mongo):
"""
Returns a sorted list of all the tracks exactly matching the folder in the query params
"""
songs = self.collection.find({"folder": query}).sort("title", db.pymongo.ASCENDING)
songs = self.collection.find({
"folder": query
}).sort("title", db.pymongo.ASCENDING)
return convert_many(songs)
def find_songs_by_folder_og(self, query: str) -> list:
@ -104,8 +119,10 @@ class AllSongs(db.Mongo):
Returns a list of all the tracks containing the albumartist in the query params.
"""
songs = self.collection.find(
{"albumartist": {"$regex": query, "$options": "i"}}
)
{"albumartist": {
"$regex": query,
"$options": "i"
}})
return convert_many(songs)
def get_song_by_path(self, path: str) -> dict:

View File

@ -1,6 +1,9 @@
import time
from typing import List
from app import api, helpers, models
from app import api
from app import helpers
from app import models
from progress.bar import Bar
@ -58,7 +61,6 @@ def get_subdirs(foldername: str) -> List[models.Folder]:
if str1 is not None:
subdirs.add(foldername + "/" + str1)
return [create_folder(dir) for dir in subdirs]

View File

@ -61,6 +61,26 @@ def create_all_playlists():
_bar.next()
_bar.finish()
validate_images()
def create_thumbnail(image: any, img_path: str) -> str:
"""
Creates a 250 x 250 thumbnail from a playlist image
"""
thumb_path = "thumb_" + img_path
full_thumb_path = os.path.join(settings.APP_DIR, "images", "playlists",
thumb_path)
aspect_ratio = image.width / image.height
new_w = round(250 * aspect_ratio)
thumb = image.resize((new_w, 250), Image.ANTIALIAS)
thumb.save(full_thumb_path, "webp")
return thumb_path
def save_p_image(file: datastructures.FileStorage, pid: str):
"""
@ -72,7 +92,9 @@ def save_p_image(file: datastructures.FileStorage, pid: str):
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)
full_img_path = os.path.join(settings.APP_DIR, "images", "playlists",
img_path)
if file.content_type == "image/gif":
frames = []
@ -80,9 +102,33 @@ def save_p_image(file: datastructures.FileStorage, pid: str):
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
frames[0].save(full_img_path, save_all=True, append_images=frames[1:])
thumb_path = create_thumbnail(img, img_path=img_path)
img.save(full_path, "webp")
return img_path, thumb_path
return img_path
img.save(full_img_path, "webp")
thumb_path = create_thumbnail(img, img_path=img_path)
return img_path, thumb_path
def validate_images():
"""
Removes all unused images in the images/playlists folder.
"""
images = []
for playlist in api.PLAYLISTS:
if playlist.image:
img_path = playlist.image.split("/")[-1]
thumb_path = playlist.thumb.split("/")[-1]
images.append(img_path)
images.append(thumb_path)
p_path = os.path.join(settings.APP_DIR, "images", "playlists")
for image in os.listdir(p_path):
if image not in images:
os.remove(os.path.join(p_path, image))

View File

@ -103,6 +103,7 @@ class Playlist:
_pre_tracks: list = field(init=False, repr=False)
lastUpdated: int
image: str
thumb: str
description: str = ""
count: int = 0
"""A list of track objects in the playlist"""
@ -112,6 +113,7 @@ class Playlist:
self.name = data["name"]
self.description = data["description"]
self.image = self.create_img_link(data["image"])
self.thumb = self.create_img_link(data["thumb"])
self._pre_tracks = data["pre_tracks"]
self.tracks = []
self.lastUpdated = data["lastUpdated"]
@ -149,6 +151,7 @@ class Playlist:
if data["image"]:
self.image = self.create_img_link(data["image"])
self.thumb = self.create_img_link(data["thumb"])
@dataclass

3
server/app/patches.py Normal file
View File

@ -0,0 +1,3 @@
"""
This module contains patch functions to modify existing data in the database.
"""

View File

@ -58,6 +58,7 @@ class Playlist:
playlistid: str
name: str
image: str
thumb: str
lastUpdated: int
description: str
count: int = 0
@ -68,6 +69,7 @@ class Playlist:
self.playlistid = p.playlistid
self.name = p.name
self.image = p.image
self.thumb = p.thumb
self.lastUpdated = p.lastUpdated
self.description = p.description
self.count = p.count

View File

@ -103,5 +103,6 @@ app_dom.addEventListener("click", (e) => {
padding: 0 $small;
display: grid;
grid-template-rows: 1fr;
margin-top: $small;
}
</style>

View File

@ -44,7 +44,7 @@ const props = defineProps<{
.a-header {
display: grid;
grid-template-columns: 13rem 1fr;
grid-template-columns: 15rem 1fr;
padding: 1rem;
height: 100%;
background-color: $gray4;
@ -58,8 +58,8 @@ const props = defineProps<{
align-items: flex-end;
.image {
width: 12rem;
height: 12rem;
width: 14rem;
height: 14rem;
}
}

View File

@ -1,59 +0,0 @@
<template>
<div class="folder-top flex">
<div class="fname">
<PlayBtnRect />
<div class="ftext">
<div class="icon image"></div>
<div class="ellip">
{{ folder.path.split("/").splice(-1).join("") }}
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import useFStore from "../../stores/folder";
import PlayBtnRect from "../shared/PlayBtnRect.vue";
const folder = useFStore();
</script>
<style lang="scss">
.folder-top {
border-bottom: 1px solid $separator;
width: calc(100% - 0.5rem);
padding-bottom: $small;
height: 3rem;
}
.folder-top .fname {
width: 100%;
display: flex;
gap: $small;
align-items: center;
.ftext {
position: relative;
display: flex;
align-items: center;
height: 2.5rem;
border-radius: $small;
background-color: $primary;
padding: $small $small $small 2.25rem;
.icon {
position: absolute;
left: $small;
height: 1.5rem;
width: 1.5rem;
background-image: url(../../assets/icons/folder.fill.svg);
margin-right: $small;
}
@include phone-only {
display: none;
}
}
}
</style>

View File

@ -1,9 +1,8 @@
<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 }}
Playing from
</div>
<div class="name">
<div id="to">
@ -100,27 +99,17 @@ function goTo() {
<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;
background-color: $accent;
&: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;
@ -133,7 +122,7 @@ function goTo() {
align-items: center;
gap: $small;
text-transform: capitalize;
color: rgba(255, 255, 255, 0.664);
color: rgba(255, 255, 255, 0.849);
.icon {
height: 1.25rem;

View File

@ -1,31 +1,50 @@
<template>
<div class="topnav rounded">
<div class="topnav">
<div class="left">
<div class="btn">
<PlayBtn />
<NavButtons />
</div>
<div class="info">
<div class="title">Playlists</div>
<div class="title" v-if="$route.name == 'Playlists'">Playlists</div>
<div class="folder" v-else-if="$route.name == 'FolderView'">
<div class="play">
<PlayBtnRect />
</div>
<div class="fname">
<div class="icon image"></div>
<div class="ellip">
{{ $route.params.path.split("/").splice(-1)[0] }}
</div>
</div>
</div>
</div>
</div>
<div class="center rounded">
<Loader />
</div>
<div class="right"></div>
<div class="right">
<Search />
</div>
</div>
</template>
<script setup>
import PlayBtn from "../shared/PlayBtn.vue";
import NavButtons from "./NavButtons.vue";
import Loader from "../shared/Loader.vue";
import PlayBtnRect from "../shared/PlayBtnRect.vue";
import Search from "./Search.vue";
</script>
<style lang="scss">
.topnav {
display: grid;
grid-template-columns: repeat(3, 1fr);
padding: 0 $small;
grid-template-columns: 1fr max-content max-content;
padding-bottom: 1rem;
margin: $small $small 0 $small;
border-bottom: 1px solid $gray3;
height: 3rem;
.left {
display: flex;
@ -37,12 +56,46 @@ import Loader from "../shared/Loader.vue";
font-size: 1.5rem;
font-weight: bold;
}
.folder {
display: flex;
gap: 1rem;
.playbtnrect {
height: 2.25rem;
}
.fname {
position: relative;
padding-left: 2.25rem;
background-color: $gray4;
border-radius: $small;
height: 2.25rem;
display: flex;
align-items: center;
padding-right: $small;
.icon {
position: absolute;
left: $small;
top: $small;
width: 1.5rem;
height: 1.5rem;
background-image: url("../../assets/icons/folder.fill.svg");
}
}
}
}
}
.center {
display: grid;
place-items: center;
margin-right: 1rem;
}
.right {
width: 100%;
}
}
</style>

View File

@ -0,0 +1,44 @@
<template>
<div id="back-forward">
<div class="back image" @click="$router.back()"></div>
<div class="forward image" @click="$router.forward()"></div>
</div>
</template>
<script setup>
console.log();
</script>
<style lang="scss">
#back-forward {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
padding-right: 1rem;
margin-right: $small;
border-right: 1px solid $gray3;
& > div {
background-color: $gray4;
border-radius: $small;
height: 2.25rem;
width: 2.25rem;
cursor: pointer;
background-size: 2rem;
transition: all .25s ease-in-out;
&:hover {
background-color: $accent;
}
}
.back {
background-image: url("../../assets/icons/right-arrow.svg");
transform: rotate(180deg);
}
.forward {
background-image: url("../../assets/icons/right-arrow.svg");
}
}
</style>

View File

@ -0,0 +1,36 @@
<template>
<div id="nav-search">
<form>
<input
type="search"
name=""
id=""
placeholder="Search this playlist"
class="rounded"
/>
</form>
</div>
</template>
<style lang="scss">
#nav-search {
form {
display: flex;
gap: $small;
input[type="search"] {
background-color: $gray5;
border: none;
padding: $small;
width: 100%;
min-width: 24rem;
color: $white;
font-size: 1rem;
&:focus {
outline: solid $accent;
}
}
}
}
</style>

View File

@ -3,6 +3,7 @@
<div class="gradient rounded"></div>
<div class="plus image p-image"></div>
<div>New Playlist</div>
<div></div>
</div>
</template>
@ -10,30 +11,31 @@
#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;
width: calc(100% - 1.5rem);
top: 0.75rem;
background-image: linear-gradient(37deg, $red, $blue);
background-size: 100%;
transition: all .5s ease-in-out;
transition: all 0.5s ease-in-out;
aspect-ratio: 1;
}
.image {
background-image: url("../../assets/icons/plus.svg");
background-size: 5rem;
z-index: 1;
transition: all .5s ease-in-out;
transition: all 0.5s ease-in-out;
background-color: transparent;
margin-bottom: $small;
}
&:hover {
.gradient {
background-size: 30rem;
background-size: 300rem;
}
.image {
transform: rotate(270deg);

View File

@ -10,7 +10,7 @@
<div
class="image p-image rounded shadow-sm"
:style="{
backgroundImage: `url(${props.playlist.image})`,
backgroundImage: `url(${props.playlist.thumb})`,
}"
></div>
<div class="pbtn">
@ -37,21 +37,21 @@ import Option from "../shared/Option.vue";
const props = defineProps<{
playlist: Playlist;
}>();
</script>
<style lang="scss">
.p-card {
width: 100%;
padding: 0.75rem;
transition: all 0.2s ease;
background-image: linear-gradient(37deg, #000000e8, $gray);
transition: all 0.25s ease;
background-position: -10rem;
position: relative;
.p-image {
min-width: 100%;
height: 10rem;
transition: all 0.2s ease;
background-color: $gray4;
aspect-ratio: 1;
}
.drop {
@ -60,6 +60,7 @@ const props = defineProps<{
right: 1.25rem;
opacity: 0;
transition: all 0.25s ease-in-out;
display: none;
.drop-btn {
background-color: $gray3;
@ -67,6 +68,7 @@ const props = defineProps<{
}
.pbtn {
display: none;
position: absolute;
bottom: 4.5rem;
left: 1.25rem;
@ -75,10 +77,12 @@ const props = defineProps<{
}
&:hover {
background-color: $gray5;
.drop {
transition-delay: .75s;
transition-delay: 0.75s;
opacity: 1;
transform: translate(0, -.5rem);
transform: translate(0, -0.5rem);
}
}

View File

@ -1,20 +1,21 @@
<template>
<div class="loaderx" :class="{ loader: loading, not_loader: !loading }">
<div v-if="!loading">😹</div>
<div v-if="!loading">🦋</div>
</div>
</template>
<script setup>
import state from "@/composables/state";
const loading = state.loading
const loading = state.loading;
</script>
<style lang="scss">
.loaderx {
width: 1.5rem;
height:1.5rem;
height: 1.5rem;
border-radius: 50%;
user-select: none;
}
.loader {

View File

@ -56,7 +56,7 @@ interface Playlist {
tracks?: Track[];
count?: number;
lastUpdated?: string;
color?: string;
thumb?: string;
}
interface Notif {

View File

@ -21,6 +21,7 @@ export default defineStore("album", {
this.tracks = tracks.tracks;
this.info = tracks.info;
this.artists = artists;
this.bio = null;
},
fetchBio(title: string, albumartist: string) {
getAlbumBio(title, albumartist).then((bio) => {

View File

@ -46,7 +46,7 @@ onBeforeRouteUpdate(async (to) => {
.songs {
padding: $small;
min-height: calc(100% - 30rem);
min-height: calc(100% - 32rem);
}
&::-webkit-scrollbar {

View File

@ -1,8 +1,5 @@
<template>
<div id="f-view-parent" class="rounded">
<div class="fixed">
<Header :path="FStore.path" :first_song="FStore.tracks[0]" />
</div>
<div id="scrollable" ref="scrollable">
<FolderList :folders="FStore.dirs" />
<div
@ -20,7 +17,6 @@ import { onBeforeRouteUpdate } from "vue-router";
import SongList from "@/components/FolderView/SongList.vue";
import FolderList from "@/components/FolderView/FolderList.vue";
import Header from "@/components/FolderView/Header.vue";
import useFStore from "../stores/folder";
import state from "../composables/state";
@ -44,9 +40,10 @@ onBeforeRouteUpdate((to) => {
<style lang="scss">
#f-view-parent {
position: relative;
padding: 4rem $small 0 $small;
padding: 0 $small 0 $small;
overflow: hidden;
margin: $small;
margin-top: $small;
.h {
font-size: 2rem;
@ -54,12 +51,12 @@ onBeforeRouteUpdate((to) => {
}
}
#f-view-parent .fixed {
position: absolute;
height: min-content;
width: calc(100% - 1rem);
top: 0.5rem;
}
// #f-view-parent .fixed {
// position: absolute;
// height: min-content;
// width: calc(100% - 1rem);
// top: 0.5rem;
// }
#scrollable {
overflow-y: auto;

View File

@ -49,7 +49,7 @@ const playlist = usePTrackStore();
}
.songlist {
padding: $small;
min-height: calc(100% - 30rem);
min-height: calc(100% - 32rem);
}
}
</style>

View File

@ -22,15 +22,15 @@ const pStore = usePStore();
<style lang="scss">
#p-view {
margin: $small;
margin-top: 0;
padding: $small;
overflow: auto;
scrollbar-color: $gray2 transparent;
border-top: 1px solid $gray3;
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
gap: $small;
gap: 1rem;
}
}
</style>