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
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 |
@@ -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 |
|
Before Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 81 KiB |
@@ -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 |
|
Before Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 628 KiB |
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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");
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
// Styles that only apply on our dear Firefox
|
||||
|
||||
// @-moz-document url-prefix() {
|
||||
|
||||
// }
|
||||
@@ -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,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 {
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,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()"
|
||||
/>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,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;
|
||||
|
||||
@@ -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 }}  |  </span
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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("${current.image}")`,
|
||||
}"
|
||||
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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,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,7 +1,6 @@
|
||||
<template>
|
||||
<div
|
||||
class="title"
|
||||
v-motion-slide-from-left-100
|
||||
>
|
||||
Playlists
|
||||
</div>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ defineProps<{
|
||||
background-color: $gray4;
|
||||
color: #ffffffde !important;
|
||||
transition: all 0.5s ease;
|
||||
min-width: 8.5rem;
|
||||
|
||||
.img {
|
||||
position: relative;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) => {
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,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");
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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,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: () => ({
|
||||
|
||||
@@ -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,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: () => ({
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -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,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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||