Add folder banner

Add folder page banner

+ Group tracks by discs in album page
+ Show copyright info on album page (if any)
+ implement better spacing throughout the app
+ group global scss into files
+ other enhancements
This commit is contained in:
Mungai Geoffrey
2022-08-03 15:19:17 +03:00
committed by GitHub
102 changed files with 1198 additions and 1505 deletions
+5 -10
View File
@@ -10,26 +10,21 @@
},
"dependencies": {
"@vueuse/core": "^8.5.0",
"@vueuse/motion": "^2.0.0-beta.18",
"axios": "^0.26.1",
"defu": "^6.0.0",
"mitt": "^3.0.0",
"pinia": "^2.0.11",
"register-service-worker": "^1.7.1",
"sass": "^1.49.0",
"sass-loader": "^10",
"vite-svg-loader": "^3.4.0",
"vue": "^3.0.0",
"vue": "^3.2.37",
"vue-debounce": "^3.0.2",
"vue-router": "^4.0.0-0",
"webpack": "^5.64.4"
"vue-router": "^4.1.3",
"webpack": "^5.74.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^1.6.1",
"@vue/compiler-sfc": "^3.0.0",
"@vitejs/plugin-vue": "^3.0.1",
"eslint": "^8.7.0",
"eslint-plugin-vue": "^8.3.0",
"vite": "^2.5.4",
"vite": "^3.0.4",
"vue-svg-loader": "^0.16.0"
},
"packageManager": "yarn@3.1.1"
+1 -1
View File
@@ -68,7 +68,7 @@ def get_album():
album.count == 1
and tracks[0].title == album.title
and tracks[0].tracknumber == 1
and tracks[0].disknumber == 1
and tracks[0].discnumber == 1
):
album.is_single = True
-2
View File
@@ -1,8 +1,6 @@
"""
Contains all the folder routes.
"""
from app import api
from app import helpers
from app import settings
from app.lib.folderslib import getFnF
from flask import Blueprint
+1 -1
View File
@@ -97,7 +97,7 @@ class CheckArtistImages:
@staticmethod
def check_if_exists(img_path: str):
"""
Checks if an image exists on disk.
Checks if an image exists on c.
"""
if os.path.exists(img_path):
+1
View File
@@ -143,6 +143,7 @@ def create_album(track: models.Track) -> dict:
"title": track.album,
"artist": track.albumartist,
"hash": track.albumhash,
"copyright": track.copyright,
}
album["date"] = track.date
+17 -8
View File
@@ -3,8 +3,7 @@ from io import BytesIO
import mutagen
from app import settings
from mutagen.flac import FLAC
from mutagen.flac import MutagenError
from mutagen.flac import FLAC, MutagenError
from mutagen.id3 import ID3
from PIL import Image
@@ -146,16 +145,25 @@ def parse_track_number(tags):
return track_number
def parse_disk_number(tags):
def parse_disc_number(tags):
"""
Parses the disk number from an audio file.
Parses the disc number from an audio file.
"""
try:
disk_number = int(tags["disknumber"][0])
disc_number = int(tags["discnumber"][0])
except (KeyError, IndexError, ValueError):
disk_number = 1
disc_number = 1
return disk_number
return disc_number
def parse_copyright(tags):
try:
copyright = str(tags["copyright"][0])
except (KeyError, IndexError, ValueError):
copyright = None
return copyright
def get_tags(fullpath: str) -> dict | None:
@@ -175,7 +183,8 @@ def get_tags(fullpath: str) -> dict | None:
"genre": parse_genre_tag(tags),
"date": parse_date_tag(tags)[:4],
"tracknumber": parse_track_number(tags),
"disknumber": parse_disk_number(tags),
"discnumber": parse_disc_number(tags),
"copyright": parse_copyright(tags),
"length": round(tags.info.length),
"bitrate": round(int(tags.info.bitrate) / 1000),
"filepath": fullpath,
+36 -15
View File
@@ -3,6 +3,7 @@ Contains all the models for objects generation and typing.
"""
from dataclasses import dataclass
from dataclasses import field
from operator import itemgetter
from typing import List
from app import helpers
@@ -25,26 +26,42 @@ class Track:
genre: str
bitrate: int
tracknumber: int
disknumber: int
discnumber: int
albumhash: str
date: str
image: str
uniq_hash: str
copyright: str
def __init__(self, tags):
(
self.title,
self.album,
self.albumartist,
self.genre,
self.albumhash,
self.date,
self.folder,
self.filepath,
self.copyright,
) = itemgetter(
"title",
"album",
"albumartist",
"genre",
"albumhash",
"date",
"folder",
"filepath",
"copyright",
)(
tags
)
self.trackid = tags["_id"]["$oid"]
self.title = tags["title"]
self.artists = tags["artists"].split(", ")
self.albumartist = tags["albumartist"]
self.album = tags["album"]
self.folder = tags["folder"]
self.filepath = tags["filepath"]
self.genre = tags["genre"]
self.bitrate = int(tags["bitrate"])
self.length = int(tags["length"])
self.disknumber = int(tags["disknumber"])
self.albumhash = tags["albumhash"]
self.date = tags["date"]
self.discnumber = int(tags["discnumber"])
self.image = tags["albumhash"] + ".webp"
self.tracknumber = int(tags["tracknumber"])
@@ -85,17 +102,21 @@ class Album:
image: str
count: int = 0
duration: int = 0
copyright: str = field(default="")
is_soundtrack: bool = False
is_compilation: bool = False
is_single: bool = False
colors: List[str] = field(default_factory=list)
def __init__(self, tags):
self.title = tags["title"]
self.artist = tags["artist"]
self.date = tags["date"]
self.image = tags["image"]
self.hash = tags["hash"]
(
self.title,
self.artist,
self.date,
self.image,
self.hash,
self.copyright,
) = itemgetter("title", "artist", "date", "image", "hash", "copyright")(tags)
try:
self.colors = tags["colors"]
+18 -29
View File
@@ -2,7 +2,7 @@
<ContextMenu />
<Modal />
<Notification />
<div class="l-container">
<div id="app-grid">
<div class="l-sidebar rounded">
<Logo />
<Navigation />
@@ -11,7 +11,7 @@
</div>
</div>
<NavBar />
<div id="acontent">
<div id="acontent" class="rounded">
<router-view />
</div>
<SearchInput />
@@ -21,29 +21,27 @@
</template>
<script setup lang="ts">
import { useRouter, useRoute, RouteLocationNormalized } from "vue-router";
import { onStartTyping } from "@vueuse/core";
import { RouteLocationNormalized, useRoute, useRouter } from "vue-router";
import Navigation from "@/components/LeftSidebar/Navigation.vue";
import RightSideBar from "@/components/RightSideBar/Main.vue";
import nowPlaying from "@/components/LeftSidebar/nowPlaying.vue";
import NavBar from "@/components/nav/NavBar.vue";
import Tabs from "@/components/RightSideBar/Tabs.vue";
import SearchInput from "@/components/RightSideBar/SearchInput.vue";
import useContextStore from "@/stores/context";
import ContextMenu from "@/components/contextMenu.vue";
import Modal from "@/components/modal.vue";
import Notification from "@/components/Notification.vue";
import useQStore from "@/stores/queue";
import Navigation from "@/components/LeftSidebar/Navigation.vue";
import nowPlaying from "@/components/LeftSidebar/nowPlaying.vue";
import Logo from "@/components/Logo.vue";
import Modal from "@/components/modal.vue";
import NavBar from "@/components/nav/NavBar.vue";
import Notification from "@/components/Notification.vue";
import RightSideBar from "@/components/RightSideBar/Main.vue";
import SearchInput from "@/components/RightSideBar/SearchInput.vue";
import Tabs from "@/components/RightSideBar/Tabs.vue";
import useContextStore from "@/stores/context";
import useQStore from "@/stores/queue";
import useShortcuts from "@/composables/useKeyboard";
import { isSameRoute } from "@/composables/perks";
const context_store = useContextStore();
const queue = useQStore();
const app_dom = document.getElementById("app");
const route = useRoute();
const router = useRouter();
queue.readQueue();
@@ -55,28 +53,19 @@ app_dom.addEventListener("click", (e) => {
}
});
function removeHighlight(route: RouteLocationNormalized) {
setTimeout(() => {
router.push({ name: route.name, params: route.params });
}, 5000);
}
router.afterEach((to, from) => {
const h_hash = to.query.highlight as string;
if (h_hash) removeHighlight(to);
if (isSameRoute(to, from)) return;
router.afterEach(() => {
document.getElementById("acontent")?.scrollTo(0, 0);
});
onStartTyping(() => {
document.getElementById("globalsearch").focus();
const elem = document.getElementById("globalsearch") as HTMLInputElement;
elem.focus();
elem.value = "";
});
</script>
<style lang="scss">
@import "./assets/css/mixins.scss";
@import "./assets/scss/mixins.scss";
.l-sidebar {
position: relative;
View File
-365
View File
@@ -1,365 +0,0 @@
@import "../css/ProgressBar.scss";
@import "mixins.scss";
// @import "./moz.scss";
:root {
--separator: #ffffff46;
--green: #4ad168;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
background-color: $body;
color: #fff;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica,
Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
font-size: 1rem;
overflow: hidden;
image-rendering: -webkit-optimize-contrast;
}
.heading {
font-size: 2rem;
font-weight: bold;
}
.t-center {
text-align: center;
}
.h-1:hover {
background-color: #3a39393d;
}
a {
text-decoration: none;
color: #fff;
}
.border {
background-color: $black;
box-shadow: 0 0 1rem rgba(0, 0, 0, 0.1);
}
.separator {
border-top: 0.1px $separator solid;
color: transparent;
margin: $small 0 $small 0;
}
.no-border {
border: none;
}
.noscroll {
overflow: hidden;
}
.grid {
display: grid;
}
.card-dark {
background-color: #fff;
}
.hidden {
display: none;
}
input[type="search"] {
height: 2.25rem !important;
}
.l-container {
display: grid;
grid-template-columns: min-content 1fr min-content;
grid-template-rows: max-content 1fr max-content;
grid-template-areas:
"l-sidebar nav search-input"
"l-sidebar content r-sidebar"
"l-sidebar content r-sidebar"
"l-sidebar content tabs";
align-content: center;
max-width: 2720px;
height: calc(100vh - 1rem);
margin: 0 auto;
gap: 1rem;
margin: $small;
}
#tabs {
grid-area: tabs;
height: 3.5rem;
margin-top: -$small;
}
#acontent {
grid-area: content;
max-width: 1955px;
overflow: hidden auto;
margin-top: -$small;
.nav {
margin: $small;
width: calc(100% - 1rem);
}
}
.tabs {
grid-area: tabs;
@include tablet-landscape {
display: none;
}
}
#gsearch-input {
grid-area: search-input;
}
.topnav {
grid-area: nav;
}
.l-sidebar {
width: 17rem;
grid-area: l-sidebar;
background-color: $black;
padding: 1rem;
}
.bottom-bar {
grid-area: bottom-bar;
height: 4rem;
}
.ellip {
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
.rounded {
border-radius: $small;
}
.circular {
border-radius: 20px;
}
.flex {
display: flex;
}
.r-sidebar {
grid-area: r-sidebar;
margin-top: -$small;
width: 29rem;
}
.image {
background-position: center;
background-repeat: no-repeat;
background-size: cover;
transition: transform 0.3s ease-in-out;
}
.ellipsis {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.shadow-sm {
box-shadow: 0 0 0.5rem rgba(0, 0, 0, 0.452);
}
.shadow-md {
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.075);
}
.shadow-lg {
box-shadow: 0 1rem 2rem rgba(0, 0, 0, 0.175);
}
/* scrollbars */
/* width */
::-webkit-scrollbar {
width: 3px;
}
/* Track */
::-webkit-scrollbar-track {
background: transparent;
border-radius: 1rem;
}
/* Handle */
::-webkit-scrollbar-thumb {
background: rgba(128, 128, 128, 0.322);
border-radius: 1rem;
}
/* Handle on hover */
::-webkit-scrollbar-thumb:hover {
background: $blue;
}
@-webkit-keyframes similarAlbums {
0% {
background-position: 0 38%;
}
50% {
background-position: 100% 63%;
}
100% {
background-position: 0 38%;
}
}
@-moz-keyframes similarAlbums {
0% {
background-position: 0 38%;
}
50% {
background-position: 100% 63%;
}
100% {
background-position: 0 38%;
}
}
@-o-keyframes similarAlbums {
0% {
background-position: 0 38%;
}
50% {
background-position: 100% 63%;
}
100% {
background-position: 0 38%;
}
}
@keyframes similarAlbums {
0% {
background-position: 0 38%;
}
50% {
background-position: 100% 63%;
}
100% {
background-position: 0 38%;
}
}
.now-playing-track {
height: 2rem;
width: 2rem;
border-radius: 50%;
background-color: #fff;
margin-top: 0;
background-size: 60%;
}
.active {
background-image: url(../../assets/icons/playing.gif);
transition: all 0.3s ease-in-out;
}
.not_active {
background-image: url(../../assets/icons/playing.webp);
transition: all 0.3s ease-in-out;
}
.item {
position: relative;
background-color: rgba(34, 33, 33, 0.637);
padding: 0.5rem;
border-radius: 0.5rem;
cursor: pointer;
margin: 0 $small 0 0;
display: flex;
align-items: center;
font-size: 0.9rem;
color: rgb(250, 250, 250);
&:hover {
background-color: $pink;
}
}
input[type="number"] {
width: 40px;
padding: 4px 5px;
border: 1px solid #bbb;
border-radius: 3px;
}
.controls {
display: grid;
grid-template-columns: repeat(3, 1fr);
.shuffle {
width: 100%;
display: flex;
align-items: center;
& * {
height: 2rem;
width: 2rem;
background-size: 70%;
cursor: pointer;
border-radius: 0.5rem;
&:hover {
background-color: rgb(170, 50, 50);
}
}
& :first-child {
background-image: url(../../assets/icons/repeat.svg);
}
& :last-child {
background-image: url(../../assets/icons/shuffle.svg);
}
}
.fav {
width: 100%;
display: flex;
align-items: center;
justify-content: flex-end;
& * {
height: 2rem;
width: 2rem;
background-size: 70%;
border-radius: 0.5rem;
cursor: pointer;
&:hover {
background-color: rgb(170, 50, 50);
}
}
& :first-child {
background-image: url(../../assets/icons/plus.svg);
}
& :last-child {
background-image: url(../../assets/icons/heart.svg);
}
}
}
-16
View File
@@ -1,16 +0,0 @@
// Styles that only apply on our dear Firefox
@-moz-document url-prefix() {
#acontent {
padding-right: 1rem !important;
}
#ap-page {
width: 100% !important;
padding-right: 1rem !important;
}
.ap-page-bottom-container {
width: calc(100% - 1rem) !important;
}
}
+3
View File
@@ -0,0 +1,3 @@
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.31543 23.1816H18.6758C20.5566 23.1816 21.5322 22.1973 21.5322 20.2988V7.03613C21.5322 5.14648 20.5566 4.15332 18.6758 4.15332H12.9453C12.04 4.15332 11.416 4.39062 10.8447 4.9707L7.21484 8.60938C6.66992 9.1543 6.45898 9.70801 6.45898 10.6396V20.2988C6.45898 22.1885 7.43457 23.1816 9.31543 23.1816ZM9.45605 21.459C8.60352 21.459 8.18164 21.0107 8.18164 20.1934V10.71C8.18164 10.1387 8.2959 9.81348 8.59473 9.50586L11.6709 6.41211C12.0312 6.04297 12.4268 5.87598 12.9805 5.87598H18.5264C19.3789 5.87598 19.8096 6.33301 19.8096 7.1416V20.1934C19.8096 21.0107 19.3789 21.459 18.5352 21.459H9.45605ZM11.9082 8.06445V10.7627C11.9082 11.0879 12.1719 11.3516 12.4971 11.3516C12.8223 11.3516 13.0859 11.0879 13.0859 10.7627V8.06445C13.0859 7.73047 12.8311 7.47559 12.4971 7.47559C12.1719 7.47559 11.9082 7.73926 11.9082 8.06445ZM13.7012 8.06445V10.7627C13.7012 11.0879 13.9736 11.3516 14.3076 11.3516C14.624 11.3516 14.8877 11.0879 14.8877 10.7627V8.06445C14.8877 7.73926 14.6328 7.47559 14.3076 7.47559C13.9736 7.47559 13.7012 7.73926 13.7012 8.06445ZM15.5205 8.06445V10.7627C15.5205 11.0879 15.7842 11.3516 16.1094 11.3516C16.4258 11.3516 16.6895 11.0879 16.6895 10.7627V8.06445C16.6895 7.73047 16.4346 7.47559 16.1094 7.47559C15.7754 7.47559 15.5205 7.73926 15.5205 8.06445ZM17.3311 8.06445V10.7539C17.3311 11.0879 17.5947 11.3516 17.9199 11.3516C18.2451 11.3516 18.5 11.0879 18.5 10.7539V8.06445C18.5 7.73047 18.2451 7.47559 17.9199 7.47559C17.5859 7.47559 17.3311 7.73926 17.3311 8.06445Z" fill="#F2F2F2"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

+2 -3
View File
@@ -1,4 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14.1666 14.1667H12.8675L4.78413 5.2725C4.70599 5.18663 4.61077 5.11803 4.50458 5.0711C4.39839 5.02418 4.28356 4.99996 4.16746 5H1.66663V6.66667H3.79913L7.20746 10.4167L3.79913 14.1675H1.66663V15.8342H4.16746C4.28356 15.8342 4.39839 15.81 4.50458 15.7631C4.61077 15.7161 4.70599 15.6475 4.78413 15.5617L8.33329 11.6558L11.8825 15.5608C11.9606 15.6467 12.0558 15.7153 12.162 15.7622C12.2682 15.8092 12.383 15.8334 12.4991 15.8333H14.1666V18.3333L18.3333 15L14.1666 11.6667V14.1667V14.1667Z" fill="white"/>
<path d="M12.8675 6.66667H14.1666V9.16667L18.3333 5.885L14.1666 2.5V5H12.4991C12.383 4.99996 12.2682 5.02418 12.162 5.0711C12.0558 5.11803 11.9606 5.18663 11.8825 5.2725L9.07581 8.36167L10.3091 9.48333L12.8675 6.66667Z" fill="white"/>
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.5498 18.3389C3.5498 18.8486 3.94531 19.209 4.49023 19.209H6.55566C8.06738 19.209 8.95508 18.7695 9.99219 17.5479L12.1191 15.0166L14.2197 17.5039C15.2832 18.7695 16.2852 19.209 17.7969 19.209H19.5547V21.3271C19.5547 21.749 19.8096 21.9951 20.2314 21.9951C20.4248 21.9951 20.6006 21.9248 20.75 21.8105L24.1953 18.9365C24.5293 18.6641 24.5293 18.2422 24.1953 17.9521L20.75 15.0781C20.6006 14.9551 20.4248 14.8936 20.2314 14.8936C19.8096 14.8936 19.5547 15.1309 19.5547 15.5615V17.46H17.8496C16.8125 17.46 16.1357 17.1172 15.3887 16.2207L13.2441 13.6807L15.3975 11.1318C16.1621 10.2266 16.7773 9.90137 17.7969 9.90137H19.5547V11.835C19.5547 12.2568 19.8096 12.5029 20.2314 12.5029C20.4248 12.5029 20.6006 12.4326 20.75 12.3184L24.1953 9.44434C24.5293 9.17188 24.5293 8.75 24.1953 8.45996L20.75 5.58594C20.6006 5.46289 20.4248 5.40137 20.2314 5.40137C19.8096 5.40137 19.5547 5.63867 19.5547 6.06934V8.14355H17.8057C16.2412 8.14355 15.2832 8.57422 14.167 9.91016L12.1191 12.3447L9.99219 9.81348C8.95508 8.5918 8.00586 8.15234 6.50293 8.15234H4.49023C3.94531 8.15234 3.5498 8.5127 3.5498 9.02246C3.5498 9.53223 3.94531 9.90137 4.49023 9.90137H6.43262C7.41699 9.90137 8.10254 10.2441 8.8584 11.1406L10.9941 13.6807L8.8584 16.2207C8.10254 17.1172 7.46973 17.46 6.49414 17.46H4.49023C3.94531 17.46 3.5498 17.8291 3.5498 18.3389Z" fill="#F2F2F2"/>
</svg>

Before

Width:  |  Height:  |  Size: 852 B

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

-78
View File
@@ -1,78 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="1600"
height="900"
viewBox="0 0 1600 900"
version="1.1"
id="svg13"
sodipodi:docname="noise-texture.svg"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)">
<metadata
id="metadata17">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1366"
inkscape:window-height="714"
id="namedview15"
showgrid="false"
inkscape:zoom="0.26222222"
inkscape:cx="28.104465"
inkscape:cy="117.7106"
inkscape:window-x="0"
inkscape:window-y="28"
inkscape:window-maximized="1"
inkscape:current-layer="svg13" />
<defs
id="defs9">
<radialGradient
id="a"
gradientTransform="matrix(1 1 -1 1 0.5 -0.5)">
<stop
stop-color="#455A64"
stop-opacity="0"
offset="0.25"
id="stop2"
style="stop-color:#222222;stop-opacity:0" />
<stop
stop-color="#455A64"
stop-opacity="0.5"
offset="0.75"
id="stop4"
style="stop-color:#222222;stop-opacity:0.50196081" />
<stop
stop-color="#455A64"
stop-opacity="1"
offset="1"
id="stop6"
style="stop-color:#222222;stop-opacity:1" />
</radialGradient>
</defs>
<rect
width="1600"
height="900"
fill="url(#a)"
id="rect11" />
</svg>

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 628 KiB

+64
View File
@@ -0,0 +1,64 @@
#app-grid {
display: grid;
grid-template-columns: min-content 1fr min-content;
grid-template-rows: max-content 1fr max-content;
grid-template-areas:
"l-sidebar nav search-input"
"l-sidebar content r-sidebar"
"l-sidebar content r-sidebar"
"l-sidebar content tabs";
align-content: center;
max-width: 2720px;
height: calc(100vh - 1rem);
margin: 0 auto;
gap: 1rem;
margin: $small;
}
#acontent {
grid-area: content;
max-width: 1955px;
overflow: hidden scroll;
margin-top: -$small;
.nav {
margin: $small;
width: calc(100% - 1rem);
}
}
#tabs {
grid-area: tabs;
height: 3.5rem;
margin-top: -$small;
@include tablet-landscape {
display: none;
}
}
.r-sidebar {
grid-area: r-sidebar;
margin-top: -$small;
width: 29rem;
}
#gsearch-input {
grid-area: search-input;
}
.topnav {
grid-area: nav;
}
.l-sidebar {
width: 17rem;
grid-area: l-sidebar;
background-color: $black;
padding: 1rem;
}
.bottom-bar {
grid-area: bottom-bar;
height: 4rem;
}
+104
View File
@@ -0,0 +1,104 @@
.t-center {
text-align: center;
}
.image {
background-position: center;
background-repeat: no-repeat;
background-size: cover;
transition: transform 0.3s ease-in-out;
}
.ellipsis {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.ellip {
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
.cap-first {
display: inline-block;
&::first-letter {
text-transform: uppercase;
}
}
.rounded {
border-radius: $small;
}
.circular {
border-radius: 20px;
}
.flex {
display: flex;
}
.bg-black {
background-color: $black;
box-shadow: 0 0 1rem rgba(0, 0, 0, 0.1);
}
.heading {
font-size: 2rem;
font-weight: bold;
}
a {
text-decoration: none;
color: #fff;
}
button {
border: none;
font-size: 0.9rem !important;
color: inherit;
border-radius: $small;
display: flex;
align-items: center;
justify-content: center;
height: 2rem;
background-image: linear-gradient(70deg, $gray3, $gray2);
&:hover {
background-image: linear-gradient(70deg, #234ece, $darkblue);
}
}
.btn-active {
background-image: linear-gradient(70deg, #234ece, $darkblue);
}
.separator {
border-top: 0.1px $separator solid;
color: transparent;
margin: $small 0 $small 0;
}
.no-border {
border: none;
}
.noscroll {
overflow: hidden;
}
.abs {
position: absolute;
}
.grid {
display: grid;
}
.card-dark {
background-color: #fff;
}
+57
View File
@@ -0,0 +1,57 @@
.controls {
display: grid;
grid-template-columns: repeat(3, 1fr);
.shuffle {
width: 100%;
display: flex;
align-items: center;
& * {
height: 2rem;
width: 2rem;
background-size: 70%;
cursor: pointer;
border-radius: 0.5rem;
&:hover {
background-color: rgb(170, 50, 50);
}
}
& :first-child {
background-image: url(../../assets/icons/repeat.svg);
}
& :last-child {
background-image: url(../../assets/icons/shuffle.svg);
}
}
.fav {
width: 100%;
display: flex;
align-items: center;
justify-content: flex-end;
& * {
height: 2rem;
width: 2rem;
background-size: 70%;
border-radius: 0.5rem;
cursor: pointer;
&:hover {
background-color: rgb(170, 50, 50);
}
}
& :first-child {
background-image: url(../../assets/icons/plus.svg);
}
& :last-child {
background-image: url(../../assets/icons/heart.svg);
}
}
}
+23
View File
@@ -0,0 +1,23 @@
@import "./app-grid.scss", "./controls.scss", "./inputs.scss",
"./scrollbars.scss", "./state.scss", "./variants.scss", "./basic.scss";
:root {
--separator: #ffffff46;
--green: #4ad168;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
background-color: $body;
color: #fff;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica,
Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
font-family: "SF Compact Display" !important;
font-size: 1rem;
overflow: hidden;
image-rendering: -webkit-optimize-contrast;
}
+10
View File
@@ -0,0 +1,10 @@
input[type="number"] {
width: 40px;
padding: 4px 5px;
border: 1px solid #bbb;
border-radius: 3px;
}
input[type="search"] {
height: 2.25rem !important;
}
+23
View File
@@ -0,0 +1,23 @@
::-webkit-scrollbar {
width: 3px;
}
/* Track */
::-webkit-scrollbar-track {
background: transparent;
border-radius: 1rem;
}
/* Handle */
::-webkit-scrollbar-thumb {
background: rgba(128, 128, 128, 0.322);
border-radius: 1rem;
}
/* Handle on hover */
::-webkit-scrollbar-thumb:hover {
background: $blue;
}
+19
View File
@@ -0,0 +1,19 @@
.now-playing-track-indicator {
background-image: url(../../assets/icons/playing.gif);
transition: all 0.3s ease-in-out;
height: 2rem;
width: 2rem;
border-radius: 50%;
background-color: $white;
background-size: 1.5rem !important;
}
.last_played {
background-image: url(../../assets/icons/playing.webp);
transition: all 0.3s ease-in-out;
}
.hidden {
display: none;
}
+25
View File
@@ -0,0 +1,25 @@
// paddings
.pad-small {
padding: $small;
}
.pad-medium {
padding: $medium;
}
.pad-large {
padding: 1rem;
}
// shadows
.shadow-sm {
box-shadow: 0 0 0.5rem rgba(0, 0, 0, 0.452);
}
.shadow-md {
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.075);
}
.shadow-lg {
box-shadow: 0 1rem 2rem rgba(0, 0, 0, 0.175);
}
@@ -13,6 +13,8 @@ $medium: 0.75rem;
$large: 1.5rem;
$larger: 2rem;
$banner-height: 18rem;
// apple human design guideline colors
$black: #181a1c;
$white: #ffffffde;
@@ -24,9 +26,8 @@ $gray3: rgb(72, 72, 74);
$gray4: rgb(58, 58, 60);
$gray5: rgb(44, 44, 46);
$red: #FF453A;
$blue: #0A84FF;
$red: #ff453a;
$blue: #0a84ff;
$darkblue: #055ee2;
$green: rgb(20, 160, 55);
$yellow: rgb(255, 214, 10);
@@ -34,10 +35,9 @@ $orange: rgb(255, 159, 10);
$pink: rgb(255, 55, 95);
$purple: #bf5af2;
$brown: rgb(172, 142, 104);
$indigo: #5E5CE6;
$indigo: #5e5ce6;
$teal: rgb(64, 200, 224);
$primary: $gray4;
$accent: $red;
$secondary: $gray5;
+14
View File
@@ -0,0 +1,14 @@
@import
"./mixins.scss",
"./variables",
"./moz.scss",
"./ProgressBar.scss",
"./BottomBar/BottomBar.scss",
"./Global"
;
@font-face {
font-family: "SF Compact Display";
src: url("../sf-compact.woff") format("woff");
}
+5
View File
@@ -0,0 +1,5 @@
// Styles that only apply on our dear Firefox
// @-moz-document url-prefix() {
// }
Binary file not shown.
+5 -5
View File
@@ -11,8 +11,8 @@
<div class="circle"></div>
<img class="circle" :src="paths.images.artist + images.artist" alt="" />
</div>
<div class="bio rounded border" v-html="bio" v-if="bio"></div>
<div class="bio rounded border" v-else>No bio found</div>
<div class="bio rounded bg-black" v-html="bio" v-if="bio"></div>
<div class="bio rounded bg-black" v-else>No bio found</div>
</div>
</div>
</template>
@@ -54,7 +54,7 @@ defineProps<{
width: 100%;
margin-right: $small;
overflow: hidden;
border: solid 1px $gray5;
bg-black: solid 1px $gray5;
background-image: linear-gradient(37deg, $gray5 20%, $gray4);
@include tablet-portrait {
@@ -89,7 +89,7 @@ defineProps<{
height: 7rem;
bottom: 0;
left: calc($rectpos + 7rem);
border-radius: 50%;
bg-black-radius: 50%;
box-shadow: 0 0 2rem rgb(0, 0, 0);
transition: all 0.25s ease-in-out;
@@ -99,7 +99,7 @@ defineProps<{
}
}
.bio {
border: solid 1px $gray5;
bg-black: solid 1px $gray5;
padding: $small;
line-height: 1.5rem;
+8 -108
View File
@@ -8,24 +8,18 @@
)`,
}"
>
<div class="art">
<img
:src="imguri + album.image"
alt=""
v-motion-slide-from-left
class="rounded shadow-lg"
/>
<img class="filter rounded" src="../../assets/images/noise-texture.svg" alt="" />
<div class="art rounded">
<img :src="imguri + album.image" alt="" class="rounded shadow-lg" />
</div>
<div class="info" :class="{ nocontrast: isLight() }">
<div class="top" v-motion-slide-from-top>
<div class="info" :class="{ nocontrast: isLight(album.colors[0]) }">
<div class="top">
<div class="h">
<span v-if="album.is_soundtrack">Soundtrack</span>
<span v-else-if="album.is_compilation">Compilation</span>
<span v-else-if="album.is_single">Single</span>
<span v-else>Album</span>
</div>
<div class="title ellip">
<div class="title ellip cap-first">
{{ album.title }}
</div>
</div>
@@ -38,7 +32,7 @@
<PlayBtnRect
:source="playSources.album"
:store="useAlbumStore"
:background="getButtonColor()"
:background="getButtonColor(album.colors)"
/>
</div>
</div>
@@ -55,6 +49,7 @@ import { formatSeconds } from "../../composables/perks";
import { paths } from "@/config";
import { AlbumInfo } from "../../interfaces";
import PlayBtnRect from "../shared/PlayBtnRect.vue";
import { getButtonColor, isLight } from "../../composables/colors/album";
const props = defineProps<{
album: AlbumInfo;
@@ -83,96 +78,6 @@ function handleVisibilityState(state: boolean) {
}
useVisibility(albumheaderthing, handleVisibilityState);
/**
* Returns `true` if the rgb color passed is light.
*
* @param {string} rgb The color to check whether it's light or dark.
* @returns {boolean} true if color is light, false if color is dark.
*/
function isLight(rgb: string = props.album.colors[0]): boolean {
if (rgb == null || undefined) return false;
const [r, g, b] = rgb.match(/\d+/g)!.map(Number);
const brightness = (r * 299 + g * 587 + b * 114) / 1000;
return brightness > 170;
}
interface BtnColor {
color: string;
isDark: boolean;
}
/**
* Returns the first contrasting color in the album colors.
*
* @param {string[]} colors The album colors to choose from.
* @returns {BtnColor} A color to use as the play button background
*/
function getButtonColor(colors: string[] = props.album.colors): BtnColor {
const base_color = colors[0];
if (colors.length === 0) return { color: "#fff", isDark: true };
for (let i = 0; i < colors.length; i++) {
if (theyContrast(base_color, colors[i])) {
return {
color: colors[i],
isDark: isLight(colors[i]),
};
}
}
return {
color: "#fff",
isDark: true,
};
}
/**
* Returns the luminance of a color.
* @param r The red value of the color.
* @param g The green value of the color.
* @param b The blue value of the color.
*/
function luminance(r: any, g: any, b: any) {
let a = [r, g, b].map(function (v) {
v /= 255;
return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
});
return a[0] * 0.2126 + a[1] * 0.7152 + a[2] * 0.0722;
}
/**
* Returns a contrast ratio of `color1`:`color2`
* @param {string} color1 The first color
* @param {string} color2 The second color
*/
function contrast(color1: number[], color2: number[]): number {
let lum1 = luminance(color1[0], color1[1], color1[2]);
let lum2 = luminance(color2[0], color2[1], color2[2]);
let brightest = Math.max(lum1, lum2);
let darkest = Math.min(lum1, lum2);
return (brightest + 0.05) / (darkest + 0.05);
}
/**
* Converts a rgb color string to an array of the form: `[r, g, b]`
* @param rgb The color to convert
* @returns {number[]} The array representation of the color
*/
function rgbToArray(rgb: string): number[] {
return rgb.match(/\d+/g)!.map(Number);
}
/**
* Returns true if the `color2` contrast with `color1`.
* @param color1 The first color
* @param color2 The second color
*/
function theyContrast(color1: string, color2: string) {
return contrast(rgbToArray(color1), rgbToArray(color2)) > 3;
}
</script>
<style lang="scss">
@@ -195,11 +100,7 @@ function theyContrast(color1: string, color2: string) {
aspect-ratio: 1;
object-fit: cover;
transition: all 0.2s ease-in-out;
}
.filter {
position: absolute;
// display: none;
user-select: none;
}
}
@@ -217,7 +118,6 @@ function theyContrast(color1: string, color2: string) {
.title {
font-size: 2.5rem;
font-weight: 600;
text-transform: capitalize;
}
.artist {
+2 -10
View File
@@ -9,7 +9,7 @@
<div class="all-albums">
<div class="item rounded" v-for="artist in artists" :key="artist">
<div class="album-art image rounded"></div>
<div class="name ellip">{{ artist.name }}</div>
<div class="name t-center ellip">{{ artist.name }}</div>
</div>
</div>
</div>
@@ -84,16 +84,8 @@ export default {
}
.name {
text-align: center;
margin-top: $small;
}
.artist {
font-size: small;
font-weight: lighter;
text-align: center;
color: rgba(255, 255, 255, 0.699);
}
}
}
@@ -131,4 +123,4 @@ export default {
}
}
}
</style>
</style>
@@ -18,7 +18,7 @@
</div>
</template>
<script>
<script >
export default {
setup() {
const artists = [
+3 -3
View File
@@ -22,10 +22,10 @@
</template>
<script setup lang="ts">
import "@/assets/css/BottomBar/BottomBar.scss";
import Progress from "../LeftSidebar/NP/Progress.vue";
import HotKeys from "../LeftSidebar/NP/HotKeys.vue";
import "@/assets/scss/BottomBar/BottomBar.scss";
import { formatSeconds } from "@/composables/perks";
import HotKeys from "../LeftSidebar/NP/HotKeys.vue";
import Progress from "../LeftSidebar/NP/Progress.vue";
import useQStore from "@/stores/queue";
-1
View File
@@ -25,7 +25,6 @@ defineProps<{
grid-template-columns: repeat(auto-fill, minmax(13rem, 1fr));
gap: $medium;
padding: $small;
margin-bottom: 1rem;
background-color: $gray5;
}
</style>
+38 -5
View File
@@ -1,9 +1,12 @@
<template>
<div class="folder">
<div class="table rounded" v-if="tracks.length">
<div class="header" v-if="disc && !album.info.is_single">
<div class="disc-number">Disc {{ disc }}</div>
</div>
<div class="songlist">
<SongItem
v-for="track in getTracks()"
v-for="track in getTrackList()"
:key="track.trackid"
:track="track"
:index="track.index"
@@ -19,6 +22,9 @@
<div class="text">No tracks here</div>
</div>
</div>
<div class="copyright" v-if="copyright">
{{ copyright() }}
</div>
</div>
</template>
@@ -31,8 +37,10 @@ import { focusElem } from "@/composables/perks";
import { onMounted, onUpdated, ref } from "vue";
import { Track } from "@/interfaces";
import useQStore from "@/stores/queue";
import useAlbumStore from "@/stores/pages/album";
const queue = useQStore();
const album = useAlbumStore();
const props = defineProps<{
tracks: Track[];
@@ -40,6 +48,8 @@ const props = defineProps<{
pname?: string;
playlistid?: string;
on_album_page?: boolean;
disc?: string | number;
copyright?: () => string;
}>();
const route = useRoute();
@@ -86,13 +96,15 @@ function updateQueue(track: Track) {
queue.play(index);
break;
case "AlbumView":
const tindex = album.tracks.findIndex((t) => t.trackid === track.trackid);
queue.playFromAlbum(
track.album,
track.albumartist,
track.albumhash,
props.tracks
album.tracks
);
queue.play(index);
queue.play(tindex);
break;
case "PlaylistView":
queue.playFromPlaylist(props.pname, props.playlistid, props.tracks);
@@ -101,7 +113,10 @@ function updateQueue(track: Track) {
}
}
function getTracks() {
/**
* Used to show track numbers as indexes in the album page.
*/
function getTrackList() {
if (props.on_album_page) {
let tracks = props.tracks.map((track) => {
track.index = track.tracknumber;
@@ -111,7 +126,7 @@ function getTracks() {
return tracks;
}
let tracks = props.tracks.map((track, index) => {
const tracks = props.tracks.map((track, index) => {
track.index = index + 1;
return track;
});
@@ -129,12 +144,30 @@ function getTracks() {
padding: 1rem;
}
.copyright {
font-size: 0.8rem;
margin-top: 1rem;
text-align: center;
opacity: 0.5;
}
.table {
height: 100%;
overflow-y: hidden;
background-color: $gray5;
padding: $small 0;
.header {
margin: $small;
.disc-number {
font-size: small;
font-weight: bold;
margin: $small 1.5rem;
color: $gray1;
}
}
.current {
a {
color: inherit;
@@ -1,4 +1,5 @@
<template>
<div class="hotkeys">
<div class="image ctrl-btn" id="previous" @click="q.playPrev"></div>
<div
@@ -17,6 +18,7 @@ const q = useQStore();
</script>
<style lang="scss">
.hotkeys {
display: grid;
grid-template-columns: repeat(3, 1fr);
+2 -2
View File
@@ -2,9 +2,9 @@
<input
id="progress"
type="range"
:value="q.track.current_time"
:value="q.duration.current"
min="0"
max="100"
:max="q.duration.full"
step="0.1"
@change="seek()"
/>
+1 -3
View File
@@ -5,7 +5,7 @@
:key="menu.name"
:to="{ name: menu.route_name, params: menu.params }"
>
<div class="nav-button" id="home-button" v-motion-slide-from-left-100>
<div class="nav-button" id="home-button">
<div class="in">
<component :is="menu.icon"></component>
<span>{{ menu.name }}</span>
@@ -70,8 +70,6 @@ const menus = [
}
svg {
width: 2rem;
height: 2rem;
margin: 0 $small 0 $small;
border-radius: $small;
}
+2 -2
View File
@@ -21,9 +21,9 @@
<span v-else>MP3</span>
{{ track.bitrate }}
</div>
<div class="title ellip">{{ props.track.title }}</div>
<div class="title ellip cap-first">{{ props.track.title }}</div>
<div class="separator no-border"></div>
<div class="artists ellip" v-if="props.track.artists[0] !== ''">
<div class="artists ellip cap-first" v-if="props.track.artists[0] !== ''">
<span
v-for="artist in putCommas(props.track.artists)"
:key="artist"
+26 -11
View File
@@ -1,6 +1,6 @@
<template>
<div class="l_ rounded">
<div class="headin">Now Playing</div>
<div class="now-playing-card t-center rounded">
<div class="headin">Now playing</div>
<div
class="button menu rounded"
@click="showContextMenu"
@@ -11,6 +11,10 @@
<div class="separator no-border"></div>
<div>
<SongCard :track="queue.tracks[queue.current]" />
<div class="l-track-time">
<span class="rounded">{{ formatSeconds(queue.duration.current) }}</span
><span class="rounded">{{ formatSeconds(queue.duration.full) }}</span>
</div>
<Progress />
<HotKeys />
</div>
@@ -18,16 +22,17 @@
</template>
<script setup lang="ts">
import SongCard from "./SongCard.vue";
import HotKeys from "./NP/HotKeys.vue";
import Progress from "./NP/Progress.vue";
import useQStore from "../../stores/queue";
import MenuSvg from "../../assets/icons/more.svg";
import { ContextSrc } from "@/composables/enums";
import trackContext from "@/contexts/track_context";
import useContextStore from "@/stores/context";
import useModalStore from "@/stores/modal";
import useQueueStore from "@/stores/queue";
import { ContextSrc } from "@/composables/enums";
import MenuSvg from "../../assets/icons/more.svg";
import useQStore from "../../stores/queue";
import HotKeys from "./NP/HotKeys.vue";
import Progress from "./NP/Progress.vue";
import SongCard from "./SongCard.vue";
import { formatSeconds } from "@/composables/perks";
import { ref } from "vue";
@@ -56,14 +61,24 @@ const showContextMenu = (e: Event) => {
};
</script>
<style lang="scss">
.l_ {
.now-playing-card {
padding: 1rem;
background-color: $primary;
text-align: center;
width: 100%;
display: grid;
position: relative;
text-transform: capitalize;
.l-track-time {
display: flex;
justify-content: space-between;
opacity: 0.8;
margin-top: $small;
span {
font-size: small;
padding: $smaller;
}
}
&:hover {
::-moz-range-thumb {
+1 -2
View File
@@ -1,6 +1,5 @@
<template>
<div id="logo-container"
v-motion-slide-from-top
>
<router-link :to="{ name: 'Home' }">
<div id="logo" class="rounded"></div
@@ -9,7 +8,7 @@
</template>
<style lang="scss">
@import "../assets/css/mixins.scss";
@import "../assets/scss/mixins.scss";
#logo-container {
overflow: hidden;
+1 -1
View File
@@ -37,7 +37,7 @@
<div class="type">Playlist</div>
</div>
</div>
<div class="last-updated" v-motion-slide-from-right>
<div class="last-updated">
<span class="status"
>Last updated {{ props.info.lastUpdated }} &#160;|&#160;&#160;</span
>
+1 -1
View File
@@ -13,7 +13,7 @@
<script setup lang="ts">
import Recommendations from "./Recommendation.vue";
import UpNext from "../queue/upNext.vue";
import UpNext from "../Queue/upNext.vue";
import useQStore from "../../../stores/queue";
const queue = useQStore();
</script>
@@ -1,9 +1,9 @@
<template>
<div class="r-tracks rounded border">
<div class="r-tracks rounded bg-black">
<div class="heading">Similar tracks</div>
<div class="tracks">
<div class="song-item" v-for="song in songs" :key="song.artist">
<img src="../../../assets/images/null.webp" class="rounded" />
<img src="" class="rounded" />
<div class="tags">
<div class="title">{{ song.title }}</div>
<div class="artist">{{ song.artist }}</div>
+3 -3
View File
@@ -1,16 +1,16 @@
<template>
<div class="now-playing border shadow-lg">
<div class="now-playing bg-black shadow-lg">
<div class="art-tags">
<div class="duration">{{ formatSeconds(current.length) }}</div>
<div
:style="{
backgroundImage: `url(&quot;${current.image}&quot;)`,
}"
class="album-art image border"
class="album-art image bg-black"
></div>
<div class="t-a">
<p id="title" class="ellipsis">{{ current.title }}</p>
<div class="separator no-border"></div>
<div class="separator no-bg-black"></div>
<div v-if="current.artists[0] !== ''" id="artist" class="ellip">
<span v-for="artist in putCommas(current.artists)" :key="artist">{{
artist
+14 -8
View File
@@ -2,7 +2,8 @@
<div class="up-next">
<div class="r-grid">
<UpNext :next="queue.tracks[queue.next]" :playNext="queue.playNext" />
<div class="scrollable-r border rounded">
<div class="scrollable-r bg-black rounded">
<QueueActions />
<div
class="inner"
@mouseenter="setMouseOver(true)"
@@ -25,11 +26,12 @@
<script setup lang="ts">
import TrackItem from "../shared/TrackItem.vue";
import useQStore from "../../stores/queue";
import PlayingFrom from "./queue/playingFrom.vue";
import UpNext from "./queue/upNext.vue";
import useQStore from "@/stores/queue";
import PlayingFrom from "./Queue/playingFrom.vue";
import UpNext from "./Queue/upNext.vue";
import { onUpdated, ref } from "vue";
import { focusElem } from "@/composables/perks";
import QueueActions from "./Queue/QueueActions.vue";
const queue = useQStore();
const mouseover = ref(false);
@@ -62,18 +64,22 @@ onUpdated(() => {
grid-template-rows: max-content 1fr max-content;
gap: $small;
.left {
display: flex;
gap: $small;
}
.scrollable-r {
height: 100%;
padding: $small 0 $small $small;
overflow: hidden;
display: grid;
grid-template-rows: max-content 1fr;
.inner {
height: 100%;
overflow: scroll;
margin-top: 1rem;
padding-right: $small;
overflow-x: hidden;
scrollbar-color: grey transparent;
margin: 1rem 0;
}
}
}
@@ -0,0 +1,55 @@
<template>
<div class="queue-actions">
<div class="left">
<button class="clear-queue action" @click="queue.clearQueue">
<ClearSvg />
<span>Clear</span>
</button>
<button class="shuffle-queue action">
<SaveAsPlaylistSvg />
<span> Save as Playlist </span>
</button>
</div>
<div class="right">
<button class="more-action action">
<MoreSvg />
</button>
</div>
</div>
</template>
<script setup lang="ts">
import useQueueStore from "../../../stores/queue";
import ClearSvg from "@/assets/icons/delete.svg";
import SaveAsPlaylistSvg from "@/assets/icons/sdcard.svg";
import MoreSvg from "@/assets/icons/more.svg";
const queue = useQueueStore();
</script>
<style lang="scss">
.queue-actions {
display: flex;
justify-content: space-between;
gap: $small;
margin: 1rem;
margin-bottom: 0;
.action {
padding: $smaller;
padding-right: $small;
svg {
transform: scale(0.8);
}
}
.more-action {
padding-right: $smaller;
svg {
transform: scale(1.25);
}
}
}
</style>
@@ -1,10 +1,10 @@
<template>
<div id="playing-from" class="rounded" @click="goTo">
<div id="playing-from" class="bg-black rounded" @click="goTo">
<div class="h">
<div class="icon image" :class="from.icon"></div>
Playing from
</div>
<div class="name">
<div class="name cap-first">
<div id="to">
{{ from.text }}
</div>
@@ -18,8 +18,8 @@ import {
fromAlbum,
fromPlaylist,
fromSearch,
} from "../../../interfaces";
import { FromOptions } from "../../../composables/enums";
} from "@/interfaces";
import { FromOptions } from "@/composables/enums";
import { useRouter } from "vue-router";
import { computed } from "@vue/reactivity";
@@ -120,7 +120,6 @@ function goTo() {
}
.name {
text-transform: capitalize;
font-weight: bolder;
}
@@ -130,7 +129,6 @@ function goTo() {
display: flex;
align-items: center;
gap: $small;
text-transform: capitalize;
color: rgba(255, 255, 255, 0.849);
.icon {
@@ -1,5 +1,5 @@
<template>
<div class="main-item border" @click="playNext">
<div class="main-item bg-black" @click="playNext">
<div class="h">Up Next</div>
<div class="itemx shadow">
<div
@@ -22,12 +22,12 @@
</template>
<script setup lang="ts">
import { Track } from "../../../interfaces";
import {putCommas} from "../../../composables/perks";
import { paths } from "../../../config";
const imguri = paths.images.thumb;
import { putCommas } from "@/composables/perks";
import { paths } from "@/config";
import { Track } from "@/interfaces";
const props = defineProps<{
const imguri = paths.images.thumb;
defineProps<{
next: Track;
playNext: () => void;
}>();
@@ -1,6 +1,6 @@
<template>
<div class="albums-results border">
<div class="grid">
<div class="artists-results">
<div class="search-results-grid">
<AlbumCard
v-for="album in search.albums.value"
:key="`${album.artist}-${album.title}`"
@@ -23,22 +23,3 @@ function loadMore() {
search.loadAlbums(search.loadCounter.albums);
}
</script>
<style lang="scss">
.right-search .albums-results {
border-radius: 0.5rem;
margin-top: $small;
padding: $small;
overflow-x: hidden;
.result-item:hover {
background-color: $gray4;
}
.grid {
grid-template-columns: repeat(auto-fill, minmax(8rem, 1fr));
flex-wrap: wrap;
gap: 0.75rem;
}
}
</style>
@@ -1,6 +1,6 @@
<template>
<div class="artists-results border">
<div class="grid">
<div class="artists-results" v-if="search.artists.value.length">
<div class="search-results-grid">
<ArtistCard
v-for="artist in search.artists.value"
:key="artist.image"
@@ -13,9 +13,9 @@
</template>
<script setup lang="ts">
import useSearchStore from "../../../stores/search";
import ArtistCard from "../../shared/ArtistCard.vue";
import LoadMore from "./LoadMore.vue";
import useSearchStore from "../../../stores/search";
const search = useSearchStore();
function loadMore() {
@@ -26,17 +26,17 @@ function loadMore() {
<style lang="scss">
.right-search .artists-results {
border-radius: 0.5rem;
padding: $small;
margin-bottom: $small;
display: grid;
margin: 0 1rem;
.xartist {
background-color: $gray;
}
.grid {
grid-template-columns: repeat(3, 1fr);
gap: 0.75rem;
.artist-image {
width: 7rem;
height: 7rem;
object-fit: cover;
}
}
}
</style>
@@ -1,8 +1,8 @@
<template>
<div class="morexx">
<div @click="loadMore" class="btn circular">
<button @click="loadMore" class="btn">
<div class="text">Load More</div>
</div>
</button>
</div>
</template>
@@ -22,19 +22,8 @@ function loadMore() {
place-items: center;
margin-top: $small;
.btn {
height: 2.5rem;
width: 15rem;
display: grid;
place-items: center;
transition: all 0.5s ease;
background-image: linear-gradient(37deg, $red, $blue);
cursor: pointer;
&:hover {
background-color: $blue !important;
width: 12rem;
}
button {
padding: 0 1rem !important;
}
}
</style>
+14 -29
View File
@@ -1,6 +1,9 @@
<template>
<div class="right-search">
<TabsWrapper>
<Tab name="Top Results">
<ArtistGrid />
</Tab>
<Tab name="tracks">
<TracksGrid />
</Tab>
@@ -10,18 +13,19 @@
<Tab name="artists">
<ArtistGrid />
</Tab>
<Tab name="Playlists">
<ArtistGrid />
</Tab>
</TabsWrapper>
</div>
</template>
<script setup lang="ts">
import TabsWrapper from "./TabsWrapper.vue";
import Tab from "./Tab.vue";
import TracksGrid from "./TracksGrid.vue";
import AlbumGrid from "./AlbumGrid.vue";
import ArtistGrid from "./ArtistGrid.vue";
import "@/assets/css/Search/Search.scss";
import Tab from "./Tab.vue";
import TabsWrapper from "./TabsWrapper.vue";
import TracksGrid from "./TracksGrid.vue";
</script>
<style lang="scss">
@@ -31,21 +35,6 @@ import "@/assets/css/Search/Search.scss";
width: auto;
height: 100%;
.no-res {
text-align: center;
display: grid;
height: calc(100% - $small);
place-items: center;
font-size: 2rem;
transition: all 0.3s ease;
line-height: 4rem !important;
.highlight {
padding: $small;
background-color: rgb(29, 26, 26);
}
}
.heading {
padding: $medium;
border-radius: $small;
@@ -53,6 +42,11 @@ import "@/assets/css/Search/Search.scss";
font-size: 2rem;
color: $white;
}
.search-results-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.75rem;
}
.input {
display: flex;
@@ -60,13 +54,4 @@ import "@/assets/css/Search/Search.scss";
position: relative;
}
}
.right-search .scrollable {
overflow-y: auto;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
}
</style>
@@ -1,5 +1,5 @@
<template>
<div class="albums-results border">
<div class="albums-results bg-black">
<div class="grid">
<PCard
v-for="album in search.albums.value"
@@ -12,9 +12,9 @@
</template>
<script setup lang="ts">
import useSearchStore from "../../../stores/search";
import PCard from "../../playlists/PlaylistCard.vue";
import LoadMore from "./LoadMore.vue";
import useSearchStore from "../../../stores/search";
const search = useSearchStore();
+1 -1
View File
@@ -1,5 +1,5 @@
<template>
<div v-show="name == s.currentTab" v-motion-slide-visible-top>
<div v-show="name == s.currentTab">
<slot />
</div>
</template>
@@ -1,16 +1,19 @@
<template>
<div id="right-tabs">
<div id="tabheaders">
<div
class="tab rounded"
v-for="slot in $slots.default()"
:key="slot.key"
@click="s.changeTab(slot.props.name)"
:class="{ activetab: slot.props.name === s.currentTab }"
>
{{ slot.props.name }}
<div id="right-tabs" class="bg-black rounded">
<div class="tab-buttons-wrapper">
<div id="tabheaders" class="rounded noscroll">
<div
class="tab cap-first"
v-for="slot in $slots.default()"
:key="slot.key"
@click="s.changeTab(slot.props.name)"
:class="{ activetab: slot.props.name === s.currentTab }"
>
{{ slot.props.name }}
</div>
</div>
</div>
<div id="tab-content">
<slot />
</div>
@@ -26,30 +29,46 @@ const s = useSearchStore();
<style lang="scss">
#right-tabs {
height: 100%;
margin-right: $small;
display: grid;
grid-template-rows: min-content 1fr;
#tabheaders {
.tab-buttons-wrapper {
display: flex;
gap: $small;
margin: $small 0;
justify-content: center;
align-items: center;
}
#tabheaders {
display: grid;
grid-template-columns: repeat(5, max-content);
justify-content: space-around;
margin: 1rem;
width: max-content;
background: linear-gradient(37deg, $gray3, $gray4, $gray3);
height: 2rem;
& > * {
border-left: solid 1px $gray3;
}
.tab {
background-color: $gray3;
padding: $small;
text-transform: capitalize;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
width: 4rem;
padding: 0 $small;
&:first-child {
border-left: solid 1px transparent;
}
}
.activetab {
background-color: $accent;
width: 6rem;
background-color: $darkblue;
transition: all 0.3s ease;
border-left: solid 1px transparent;
}
}
@@ -57,9 +76,7 @@ const s = useSearchStore();
height: 100%;
overflow: auto;
overflow-x: hidden;
border-radius: $small;
background-color: $gray;
// overflow: hidden;
padding-bottom: 1rem;
}
}
</style>
@@ -19,7 +19,6 @@
import LoadMore from "./LoadMore.vue";
import TrackItem from "../../shared/TrackItem.vue";
import useQStore from "../../../stores/queue";
import { Track } from "../../../interfaces";
import useSearchStore from "../../../stores/search";
const queue = useQStore();
@@ -38,8 +37,6 @@ function updateQueue(index: number) {
<style lang="scss">
.right-search #tracks-results {
border-radius: 0.5rem;
padding: $small;
height: 100% !important;
overflow: hidden;
+4 -8
View File
@@ -1,5 +1,4 @@
<template>
<!-- v-show="context.visible" -->
<div
class="context-menu rounded shadow-lg"
:class="[
@@ -26,7 +25,7 @@
@click="option.action()"
>
<div class="icon image" :class="option.icon"></div>
<div class="label ellip">{{ option.label }}</div>
<div class="label ellip cap-first">{{ option.label }}</div>
<div class="more image" v-if="option.children"></div>
<div class="children rounded shadow-sm" v-if="option.children">
<div
@@ -36,7 +35,7 @@
:class="[{ critical: child.critical }, child.type]"
@click="child.action()"
>
<div class="label ellip">
<div class="label ellip cap-first">
{{ child.label }}
</div>
</div>
@@ -60,7 +59,7 @@ const context = useContextStore();
z-index: 10000 !important;
transform: scale(0);
padding: $small;
padding: $small 0;
background: $context;
transform-origin: top left;
font-size: 0.875rem;
@@ -71,9 +70,7 @@ const context = useContextStore();
align-items: center;
cursor: default;
padding: $small;
border-radius: $small;
position: relative;
text-transform: capitalize;
.more {
height: 1.5rem;
@@ -90,7 +87,7 @@ const context = useContextStore();
top: -0.5rem;
max-height: 23.5rem;
padding: $small !important;
padding: $small 0 !important;
background-color: $context;
transform: scale(0);
transform-origin: top left;
@@ -180,7 +177,6 @@ const context = useContextStore();
.context-many-kids {
.context-item > .children {
// top: -0.5rem;
overflow-y: auto;
}
}
+1 -1
View File
@@ -18,7 +18,7 @@
<script setup lang="ts">
import { onMounted } from "vue";
import { useRoute } from "vue-router";
import { createNewPlaylist } from "../../composables/pages/playlists";
import { createNewPlaylist } from "../../composables/fetch/playlists";
import { Track } from "../../interfaces";
import { Notification, NotifType } from "../../stores/notification";
import usePlaylistStore from "@/stores/pages/playlists";
+2 -2
View File
@@ -55,8 +55,8 @@
<script setup lang="ts">
import usePStore from "@/stores/pages/playlist";
import { onMounted } from "vue";
import { updatePlaylist } from "../../composables/pages/playlists";
import { Playlist } from "../../interfaces";
import { updatePlaylist } from "@/composables/fetch/playlists";
import { Playlist } from "@/interfaces";
const pStore = usePStore();
-3
View File
@@ -81,8 +81,6 @@ watch(
.topnav {
display: grid;
grid-template-columns: 1fr min-content max-content;
// border-bottom: 1px solid $gray3;
// padding-bottom: $small;
.left {
display: grid;
@@ -94,7 +92,6 @@ watch(
.title {
font-size: 1.5rem;
font-weight: bold;
margin-top: $smaller;
display: flex;
}
}
+4 -10
View File
@@ -1,5 +1,5 @@
<template>
<div class="title albumnavtitle" v-motion-slide-from-left-100>
<div class="title albumnavtitle">
<PlayBtn :source="things.source" :store="things.store" />
<div class="ellip">
{{ things.text }}
@@ -44,14 +44,8 @@ const things = computed(() => {
<style lang="scss">
.albumnavtitle {
padding-left: 3rem;
.play-btn {
position: absolute;
left: $smaller;
top: -$smaller;
height: 2.25rem;
aspect-ratio: 1;
}
display: flex;
align-items: center;
gap: $small;
}
</style>
+1 -2
View File
@@ -1,6 +1,6 @@
<template>
<div id="folder-nav-title">
<div class="folder" v-motion-slide-from-left-100>
<div class="folder">
<div class="fname">
<div class="icon image"></div>
<div class="paths">
@@ -9,7 +9,6 @@
v-for="path in subPaths"
:key="path.path"
:class="{ inthisfolder: path.active }"
v-motion-slide-from-left-100
@click="
$router.push({ name: 'FolderView', params: { path: path.path } })
"
-1
View File
@@ -1,7 +1,6 @@
<template>
<div
class="title"
v-motion-slide-from-left-100
>
Playlists
</div>
+1 -2
View File
@@ -11,7 +11,7 @@
}"
></div>
<div class="bottom">
<div class="name ellip">{{ props.playlist.name }}</div>
<div class="name ellip cap-first">{{ props.playlist.name }}</div>
<div class="count">
<span v-if="props.playlist.count == 0">No Tracks</span>
<span v-else-if="props.playlist.count == 1"
@@ -85,7 +85,6 @@ const props = defineProps<{
margin-top: 1rem;
.name {
text-transform: capitalize;
font-weight: 900;
}
+1
View File
@@ -41,6 +41,7 @@ defineProps<{
background-color: $gray4;
color: #ffffffde !important;
transition: all 0.5s ease;
min-width: 8.5rem;
.img {
position: relative;
+6 -14
View File
@@ -1,11 +1,8 @@
<template>
<div class="xartist" :class="{ _is_on_sidebar: alt }">
<div
class="artist-image image border-sm"
:style="{ backgroundImage: `url('${imguri + artist.image}')` }"
></div>
<img class="artist-image shadow-sm" :src="imguri + artist.image" alt="" />
<div>
<p class="artist-name ellipsis">{{ artist.name }}</p>
<p class="artist-name t-center ellipsis">{{ artist.name }}</p>
</div>
</div>
</template>
@@ -28,7 +25,7 @@ defineProps<{
overflow: hidden;
position: relative;
min-width: 8.25em;
min-width: 8.5em;
height: 11em;
border-radius: 0.75rem;
display: flex;
@@ -38,28 +35,23 @@ defineProps<{
cursor: pointer;
.artist-image {
width: 8em;
height: 8em;
width: 8rem;
height: 8rem;
border-radius: 60%;
margin-bottom: $small;
background-size: 8rem 8rem;
background-position: center;
background-repeat: no-repeat;
transition: all 0.5s ease-in-out;
transition-delay: 0.25s;
object-fit: cover;
}
&:hover {
.artist-image {
background-position: 50% 20%;
border-radius: 20%;
background-size: 10rem 10rem;
}
}
.artist-name {
margin: 0;
text-align: center;
font-size: 0.9rem;
font-weight: 510;
max-width: 7rem;
+8 -9
View File
@@ -12,7 +12,7 @@
@dblclick="emitUpdate(track)"
@contextmenu="showContextMenu"
>
<div class="index">{{ index }}</div>
<div class="index t-center">{{ index }}</div>
<div class="flex">
<div @click="emitUpdate(track)" class="thumbnail">
<img
@@ -21,16 +21,16 @@
class="album-art image rounded"
/>
<div
class="now-playing-track image"
v-if="isPlaying && isCurrent"
:class="{ active: isPlaying, not_active: !isPlaying }"
class="now-playing-track-indicator image"
v-if="isCurrent"
:class="{ last_played: !isPlaying }"
></div>
</div>
<div @click="emitUpdate(track)">
<span class="ellip title">{{ track.title }}</span>
<span class="ellip title cap-first">{{ track.title }}</span>
</div>
</div>
<div class="song-artists">
<div class="song-artists cap-first">
<div class="ellip" v-if="track.artists[0] !== ''">
<span
class="artist"
@@ -44,7 +44,7 @@
</div>
</div>
<router-link
class="song-album ellip"
class="song-album ellip cap-first"
:to="{
name: 'AlbumView',
params: {
@@ -186,7 +186,6 @@ function emitUpdate(track: Track) {
.index {
color: grey;
font-size: 0.8rem;
text-align: center;
width: 2rem;
@include phone-only {
@@ -245,7 +244,7 @@ function emitUpdate(track: Track) {
cursor: pointer;
}
.now-playing-track {
.now-playing-track-indicator {
position: absolute;
left: $small;
top: $small;
+24 -23
View File
@@ -1,6 +1,6 @@
<template>
<div
class="track-item h-1"
class="track-item"
@click="playThis(props.track)"
:class="[
{
@@ -17,15 +17,17 @@
}"
>
<div
class="now-playing-track image"
class="now-playing-track-indicator image"
v-if="props.isCurrent"
:class="{ active: props.isPlaying, not_active: !props.isPlaying }"
:class="{ last_played: !props.isPlaying }"
></div>
</div>
<div class="tags">
<div class="title ellip">{{ props.track.title }}</div>
<div class="title ellip cap-first">
{{ props.track.title }}
</div>
<hr />
<div class="artist ellip">
<div class="artist ellip cap-first">
<span v-for="artist in putCommas(props.track.artists)" :key="artist">{{
artist
}}</span>
@@ -36,15 +38,15 @@
<script setup lang="ts">
import { ref } from "vue";
import { putCommas } from "../../composables/perks";
import trackContext from "../../contexts/track_context";
import { Track } from "../../interfaces";
import { ContextSrc } from "../../composables/enums";
import { ContextSrc } from "@/composables/enums";
import { putCommas } from "@/composables/perks";
import trackContext from "@/contexts/track_context";
import { Track } from "@/interfaces";
import useContextStore from "../../stores/context";
import useModalStore from "../../stores/modal";
import useQueueStore from "../../stores/queue";
import { paths } from "../../config";
import { paths } from "@/config";
import useContextStore from "@/stores/context";
import useModalStore from "@/stores/modal";
import useQueueStore from "@/stores/queue";
const contextStore = useContextStore();
const imguri = paths.images.thumb;
@@ -84,7 +86,7 @@ const playThis = (track: Track) => {
<style lang="scss">
.currentInQueue {
background-color: $gray3;
background: linear-gradient(37deg, $gray4, $gray3, $gray3);
}
.contexton {
@@ -93,17 +95,14 @@ const playThis = (track: Track) => {
}
.track-item {
display: flex;
display: grid;
grid-template-columns: min-content 1fr;
align-items: center;
border-radius: 0.5rem;
position: relative;
height: 4rem;
padding: 0.5rem 0.5rem 0.5rem 4rem;
text-transform: capitalize;
padding: $small 1rem;
&:hover {
cursor: pointer;
background-color: $gray4 !important;
background: linear-gradient(37deg, $gray4, $gray3, $gray3);
}
hr {
@@ -111,9 +110,11 @@ const playThis = (track: Track) => {
margin: 0.1rem;
}
// .tags {
// border: solid 1px;
// }
.album-art {
position: absolute;
left: $small;
display: flex;
align-items: center;
justify-content: center;
+89
View File
@@ -0,0 +1,89 @@
/**
* Returns `true` if the rgb color passed is light.
*
* @param {string} rgb The color to check whether it's light or dark.
* @returns {boolean} true if color is light, false if color is dark.
*/
export function isLight(rgb: string): boolean {
if (rgb == null || undefined) return false;
const [r, g, b] = rgb.match(/\d+/g)!.map(Number);
const brightness = (r * 299 + g * 587 + b * 114) / 1000;
return brightness > 170;
}
interface BtnColor {
color: string;
isDark: boolean;
}
/**
* Returns the first contrasting color in the album colors.
*
* @param {string[]} colors The album colors to choose from.
* @returns {BtnColor} A color to use as the play button background
*/
export function getButtonColor(colors: string[]): BtnColor {
const base_color = colors[0];
if (colors.length === 0) return { color: "#fff", isDark: true };
for (let i = 0; i < colors.length; i++) {
if (theyContrast(base_color, colors[i])) {
return {
color: colors[i],
isDark: isLight(colors[i]),
};
}
}
return {
color: "#fff",
isDark: true,
};
}
/**
* Returns the luminance of a color.
* @param r The red value of the color.
* @param g The green value of the color.
* @param b The blue value of the color.
*/
export function luminance(r: any, g: any, b: any) {
let a = [r, g, b].map(function (v) {
v /= 255;
return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
});
return a[0] * 0.2126 + a[1] * 0.7152 + a[2] * 0.0722;
}
/**
* Returns a contrast ratio of `color1`:`color2`
* @param {string} color1 The first color
* @param {string} color2 The second color
*/
export function contrast(color1: number[], color2: number[]): number {
let lum1 = luminance(color1[0], color1[1], color1[2]);
let lum2 = luminance(color2[0], color2[1], color2[2]);
let brightest = Math.max(lum1, lum2);
let darkest = Math.min(lum1, lum2);
return (brightest + 0.05) / (darkest + 0.05);
}
/**
* Converts a rgb color string to an array of the form: `[r, g, b]`
* @param rgb The color to convert
* @returns {number[]} The array representation of the color
*/
export function rgbToArray(rgb: string): number[] {
return rgb.match(/\d+/g)!.map(Number);
}
/**
* Returns true if the `color2` contrast with `color1`.
* @param color1 The first color
* @param color2 The second color
*/
export function theyContrast(color1: string, color2: string) {
return contrast(rgbToArray(color1), rgbToArray(color2)) > 3;
}
@@ -1,6 +1,6 @@
import state from "../state";
import { AlbumInfo, Track } from "../../interfaces";
import useAxios from "../useAxios";
import useAxios from "./useAxios";
import { NotifType, useNotifStore } from "@/stores/notification";
const getAlbumData = async (hash: string, ToastStore: typeof useNotifStore) => {
@@ -1,6 +1,6 @@
import { Folder, Track } from "@/interfaces";
import state from "../state";
import useAxios from "../useAxios";
import useAxios from "./useAxios";
export default async function (path: string) {
interface FolderData {
@@ -1,8 +1,8 @@
import { Artist } from "./../../interfaces";
import { Artist } from "../../interfaces";
import { Playlist, Track } from "../../interfaces";
import { Notification, NotifType } from "../../stores/notification";
import state from "../state";
import useAxios from "../useAxios";
import useAxios from "./useAxios";
/**
* Creates a new playlist on the server.
* @param playlist_name The name of the playlist to create.
@@ -1,4 +1,4 @@
import state from "./state";
import state from "../state";
import axios from "axios";
const base_url = `${state.settings.uri}/search`;
@@ -1,4 +1,4 @@
import { FetchProps } from "../interfaces";
import { FetchProps } from "../../interfaces";
import axios, { AxiosError, AxiosResponse } from "axios";
export default async (args: FetchProps) => {
+1 -1
View File
@@ -3,7 +3,7 @@ import useQStore from "@/stores/queue";
let key_down_fired = false;
function focusSearchBox() {
const elem = document.getElementById("search");
const elem = document.getElementById("globalsearch");
elem.focus();
}
+1 -1
View File
@@ -4,7 +4,7 @@ import { Option } from "../interfaces";
import {
getAllPlaylists,
addTrackToPlaylist,
} from "../composables/pages/playlists";
} from "../composables/fetch/playlists";
import useQueueStore from "../stores/queue";
import useModalStore from "../stores/modal";
-92
View File
@@ -1,92 +0,0 @@
const folders = [
{
name: "Jim Reeves - 14 number 1s",
},
{
name: "Nadina",
},
{
name: "Lil Peep - Carlifornia girls",
},
{
name: "Legends never die",
},
{
name: "Flashback party",
},
{
name: "HiFi Gold by Elton John",
}, {
name: "Jim Reeves - 14 number 1s",
},
{
name: "Nadina",
},
{
name: "Lil Peep - Carlifornia girls",
},
{
name: "Legends never die",
},
{
name: "Flashback party",
},
{
name: "HiFi Gold by Elton John",
}, {
name: "Jim Reeves - 14 number 1s",
},
{
name: "Nadina",
},
{
name: "Lil Peep - Carlifornia girls",
},
{
name: "Legends never die",
},
{
name: "Flashback party",
},
{
name: "HiFi Gold by Elton John",
}, {
name: "Jim Reeves - 14 number 1s",
},
{
name: "Nadina",
},
{
name: "Lil Peep - Carlifornia girls",
},
{
name: "Legends never die",
},
{
name: "Flashback party",
},
{
name: "HiFi Gold by Elton John",
}, {
name: "Jim Reeves - 14 number 1s",
},
{
name: "Nadina",
},
{
name: "Lil Peep - Carlifornia girls",
},
{
name: "Legends never die",
},
{
name: "Flashback party",
},
{
name: "HiFi Gold by Elton John",
},
];
export default {
folders,
};
-58
View File
@@ -1,58 +0,0 @@
const songs = [
{
title: "Doom",
album: "Fighting Demons",
artists: ["Juice Wrld"],
duration: "03:14",
},
{
title: "Girl Of My Dreams",
album: "Fighting Demons",
artists: ["Juice Wrld, ", "Suga [BTS]"],
duration: "03:14",
},
{
title: "Feline",
album: "Fighting Demons",
artists: ["Juice Wrld, ", "Polo G, ", "Lil Yachty"],
duration: "03:14",
},
{
title: "Rockstar In His Prime",
album: "Fighting Demons",
artists: ["Juice Wrld"],
duration: "03:14",
},{
title: "Because I got high",
album: "Fighting Demons",
artists: ["Juice Wrld"],
duration: "03:14",
},
{
title: "Is this love",
album: "Fighting Demons",
artists: ["Bob Marley"],
duration: "03:14",
},
{
title: "I'm a little teapot",
album: "Fighting Demons",
artists: ["Juice Wrld"],
duration: "03:14",
},
{
title: "Don't stop me now",
album: "Fighting Demons",
artists: ["Juice Wrld"],
duration: "03:14",
},{
title: "Because I got high",
album: "Fighting Demons",
artists: ["Juice Wrld"],
duration: "03:14",
},
];
export default {
songs,
};
+3 -1
View File
@@ -14,9 +14,10 @@ export interface Track {
genre?: string;
image: string;
tracknumber?: number;
disknumber?: number;
discnumber?: number;
index?: number;
uniq_hash: string;
copyright?: string;
}
export interface Folder {
@@ -40,6 +41,7 @@ export interface AlbumInfo {
is_single: boolean;
hash: string;
colors: string[];
copyright?: string;
}
export interface Artist {
+1 -4
View File
@@ -101,7 +101,6 @@ function toggleBottom() {
.ap-container {
height: 100%;
position: relative;
width: calc(100% + 1rem);
#ap-page {
overflow: auto;
@@ -110,8 +109,6 @@ function toggleBottom() {
display: grid;
grid-template-rows: 18rem 1fr;
gap: 1rem;
padding-right: $small;
width: calc(100% - $small);
.ap-page-content {
padding-bottom: 16rem;
@@ -122,7 +119,7 @@ function toggleBottom() {
position: absolute;
bottom: 0;
height: 15rem;
width: calc(100% - 1.25rem);
width: 100%;
background-color: $gray;
transition: all 0.5s ease !important;
overscroll-behavior: contain;
+1 -5
View File
@@ -1,18 +1,14 @@
import "./registerServiceWorker";
import "../src/assets/css/global.scss";
import "./assets/scss/index.scss";
import { MotionPlugin } from "@vueuse/motion";
import { createPinia } from "pinia";
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import CustomTransitions from "./transitions";
const app = createApp(App);
app.use(createPinia());
app.use(router);
app.use(MotionPlugin, CustomTransitions);
app.mount("#app");
-32
View File
@@ -1,32 +0,0 @@
/* eslint-disable no-console */
import { register } from 'register-service-worker'
if (process.env.NODE_ENV === 'production') {
register(`${process.env.BASE_URL}service-worker.js`, {
ready () {
console.log(
'App is being served from cache by a service worker.\n' +
'For more details, visit https://goo.gl/AFskqB'
)
},
registered () {
console.log('Service worker has been registered.')
},
cached () {
console.log('Content has been cached for offline use.')
},
updatefound () {
console.log('New content is downloading.')
},
updated () {
console.log('New content is available; please refresh.')
},
offline () {
console.log('No internet connection found. App is running in offline mode.')
},
error (error) {
console.error('Error during service worker registration:', error)
}
})
}
+23 -4
View File
@@ -5,7 +5,7 @@ import {
getAlbumTracks,
getAlbumArtists,
getAlbumBio,
} from "../../composables/pages/album";
} from "../../composables/fetch/album";
function sortTracks(tracks: Track[]) {
return tracks.sort((a, b) => {
@@ -17,10 +17,24 @@ function sortTracks(tracks: Track[]) {
});
}
interface Discs {
[key: string]: Track[];
}
function createDiscs(tracks: Track[]): Discs {
return tracks.reduce((group, track) => {
const { discnumber } = track;
group[discnumber] = group[discnumber] ?? [];
group[discnumber].push(track);
return group;
}, {} as Discs);
}
export default defineStore("album", {
state: () => ({
info: <AlbumInfo>{},
tracks: <Track[]>[],
discs: <Discs>{},
artists: <Artist[]>[],
bio: null,
}),
@@ -31,11 +45,16 @@ export default defineStore("album", {
* @param hash title of the album
*/
async fetchTracksAndArtists(hash: string) {
const tracks = await getAlbumTracks(hash, useNotifStore);
this.tracks = [];
const album = await getAlbumTracks(hash, useNotifStore);
const artists = await getAlbumArtists(hash);
this.tracks = sortTracks(tracks.tracks);
this.info = tracks.info;
this.discs = createDiscs(sortTracks(album.tracks));
Object.keys(this.discs).forEach((disc) => {
this.tracks.push(...this.discs[disc]);
});
this.info = album.info;
this.artists = artists;
},
/**
+1 -1
View File
@@ -1,7 +1,7 @@
import { defineStore } from "pinia";
import { Folder, Track } from "../../interfaces";
import fetchThem from "../../composables/pages/folders";
import fetchThem from "../../composables/fetch/folders";
export default defineStore("FolderDirs&Tracks", {
state: () => ({
+1 -1
View File
@@ -3,7 +3,7 @@ import { defineStore } from "pinia";
import {
getPlaylist,
getPlaylistArtists,
} from "../../composables/pages/playlists";
} from "../../composables/fetch/playlists";
import { Track, Playlist } from "../../interfaces";
export default defineStore("playlist-tracks", {
+1 -1
View File
@@ -1,6 +1,6 @@
import { defineStore } from "pinia";
import { Playlist } from "../../interfaces";
import { getAllPlaylists } from "../../composables/pages/playlists";
import { getAllPlaylists } from "../../composables/fetch/playlists";
export default defineStore("playlists", {
state: () => ({
+20 -7
View File
@@ -52,9 +52,9 @@ export default defineStore("Queue", {
state: () => ({
progressElem: HTMLElement,
audio: new Audio(),
track: {
current_time: 0,
duration: 0,
duration: {
current: 0,
full: 0,
},
current: 0,
next: 0,
@@ -80,15 +80,16 @@ export default defineStore("Queue", {
this.audio.onerror = reject;
})
.then(() => {
this.track.duration = this.audio.duration;
this.duration.full = this.audio.duration;
this.audio.play().then(() => {
this.playing = true;
notif(track, this.playPause, this.playNext, this.playPrev);
this.audio.ontimeupdate = () => {
this.track.current_time =
this.duration.current = this.audio.currentTime;
const bg_size =
(this.audio.currentTime / this.audio.duration) * 100;
elem.style.backgroundSize = `${this.track.current_time}% 100%`;
elem.style.backgroundSize = `${bg_size}% 100%`;
};
this.audio.onended = () => {
@@ -162,8 +163,11 @@ export default defineStore("Queue", {
this.prev = index - 1;
},
setCurrent(index: number) {
const track = this.tracks[index];
this.current = index;
this.currentid = this.tracks[index].trackid;
this.currentid = track.trackid;
this.duration.full = track.length;
},
setNewQueue(tracklist: Track[]) {
if (this.tracks !== tracklist) {
@@ -239,5 +243,14 @@ export default defineStore("Queue", {
);
writeQueue(this.from, this.tracks);
},
clearQueue() {
this.tracks = [defaultTrack] as Track[];
this.current = 0;
this.currentid = "";
this.next = 0;
this.prev = 0;
this.from = <From>{};
console.log(this.current);
},
},
});
+1 -1
View File
@@ -8,7 +8,7 @@ import {
loadMoreTracks,
loadMoreAlbums,
loadMoreArtists,
} from "../composables/searchMusic";
} from "../composables/fetch/searchMusic";
import { watch } from "vue";
import useDebouncedRef from "../composables/useDebouncedRef";
import useTabStore from "./tabs";
-59
View File
@@ -1,59 +0,0 @@
export default {
directives: {
"slide-from-left": {
initial: {
opacity: 0,
x: 0,
y: 20,
},
enter: {
opacity: 1,
x: 0,
y: 0,
transition: {
duration: 100,
ease: "circInOut",
},
},
},
"slide-from-left-100": {
initial: {
opacity: 0,
x: -20,
},
visible: {
opacity: 1,
x: 0,
transition: {
delay: 100,
},
},
},
"slide-from-top": {
initial: {
y: -20,
opacity: 0,
},
enter: {
y: 0,
opacity: 1,
transition: {
delay: 200,
},
},
},
"slide-from-right": {
initial: {
x: 20,
opacity: 0,
},
enter: {
x: 0,
opacity: 1,
transition: {
delay: 200,
},
},
},
},
};
+47 -2
View File
@@ -1,6 +1,16 @@
<template>
<div id="f-view-parent">
<div id="scrollable" ref="scrollable">
<div class="banner shadow-lg">
<div class="text abs rounded pad-medium">
<h3><FolderSvg /> {{ getFolderName($route) }}</h3>
</div>
<img
src="../assets/images/one.jpg"
alt="folder page banner"
class="rounded"
/>
</div>
<FolderList :folders="FStore.dirs" v-if="FStore.dirs.length" />
<SongList :tracks="FStore.tracks" :path="FStore.path" />
</div>
@@ -9,10 +19,11 @@
<script setup lang="ts">
import { ref } from "@vue/reactivity";
import { onBeforeRouteUpdate } from "vue-router";
import { onBeforeRouteUpdate, RouteLocationNormalized } from "vue-router";
import SongList from "@/components/FolderView/SongList.vue";
import FolderList from "@/components/FolderView/FolderList.vue";
import FolderSvg from "@/assets/icons/folder.svg";
import useFStore from "../stores/pages/folder";
import useLoaderStore from "../stores/loader";
@@ -23,6 +34,11 @@ const FStore = useFStore();
const scrollable = ref(null);
function getFolderName(route: RouteLocationNormalized) {
const path = route.params.path as string;
return path.split("/").pop();
}
onBeforeRouteUpdate((to, from) => {
if (isSameRoute(to, from)) return;
@@ -42,6 +58,7 @@ onBeforeRouteUpdate((to, from) => {
<style lang="scss">
#f-view-parent {
position: relative;
overflow: hidden;
.h {
font-size: 2rem;
@@ -50,8 +67,36 @@ onBeforeRouteUpdate((to, from) => {
}
#scrollable {
overflow-y: auto;
scrollbar-color: grey transparent;
display: flex;
flex-direction: column;
gap: 1rem;
.banner {
position: relative;
height: max-content;
height: $banner-height;
.text {
bottom: 1rem;
left: 1rem;
background-color: $black;
h3 {
margin: $small;
display: flex;
align-items: center;
justify-content: center;
gap: $small;
}
}
img {
height: 100%;
width: 100%;
object-fit: cover;
}
}
@include phone-only {
padding-right: 0;
+1 -2
View File
@@ -1,5 +1,5 @@
<template>
<div class="home">
<div class="home t-center">
<home />
</div>
</template>
@@ -10,6 +10,5 @@ import Home from "@/components/Home.vue";
<style>
.home {
padding-left: 20px;
text-align: center;
}
</style>
+34 -4
View File
@@ -1,6 +1,20 @@
<template>
<div class="songs rounded">
<SongList :tracks="tracks" :on_album_page="true" />
<div class="album-tracks rounded">
<div v-for="(disc, key) in discs" class="album-disc">
<SongList
:key="key"
:tracks="disc"
:on_album_page="true"
:disc="key"
:copyright="
() => {
if (isLastDisc(key)) {
return copyright;
}
}
"
/>
</div>
</div>
</template>
@@ -8,7 +22,23 @@
import { Track } from "@/interfaces";
import SongList from "@/components/FolderView/SongList.vue";
defineProps<{
tracks: Track[];
const props = defineProps<{
discs: {
[key: string]: Track[];
};
copyright: string;
}>();
// check if the disc is the last disc
const isLastDisc = (disc: string | number) => {
const discs = Object.keys(props.discs);
return discs[discs.length - 1] === disc;
};
</script>
<style lang="scss">
.album-tracks {
display: grid;
gap: 1rem;
}
</style>
+1 -1
View File
@@ -4,7 +4,7 @@
<Header :album="album.info" />
</template>
<template #content>
<Content :tracks="album.tracks" />
<Content :discs="album.discs" :copyright="album.info.copyright" />
</template>
<template #bottom>
<Bottom

Some files were not shown because too many files have changed in this diff Show More