forked from Mirrors/swingmusic-webclient
Compare commits
43 Commits
handle-alb
...
next
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e88e6dcc2d | ||
|
|
00ffbdbc42 | ||
|
|
97f348daf2 | ||
|
|
d0f47b4504 | ||
|
|
2db6bfebcf | ||
|
|
286003cf27 | ||
|
|
d27c61c7ce | ||
|
|
c56ee65a73 | ||
|
|
d3c0c7c596 | ||
|
|
e7fec30b7c | ||
|
|
da36f8d7dd | ||
|
|
63de7a6613 | ||
|
|
b14c814c55 | ||
|
|
7f8293e691 | ||
|
|
59a27d4489 | ||
|
|
4e59e73ec5 | ||
|
|
df1c909fce | ||
|
|
3aa0aebfc6 | ||
|
|
afdbb0dbb5 | ||
|
|
9fc37034a6 | ||
|
|
53fc0c6656 | ||
|
|
cfe57b788b | ||
|
|
3b55cc1c2c | ||
|
|
47c41be79a | ||
|
|
cfc9c2632b | ||
|
|
c0cb2791d0 | ||
|
|
6520b686a3 | ||
|
|
4041c8f588 | ||
|
|
da63f481c6 | ||
|
|
a5bcaadafb | ||
|
|
19142b284a | ||
|
|
511fa58d66 | ||
|
|
2fbac120b2 | ||
|
|
dea36af5cc | ||
|
|
81d28461f6 | ||
|
|
92302e87e5 | ||
|
|
1fd30b4ac3 | ||
|
|
f751bbac97 | ||
|
|
feb99103b8 | ||
|
|
0851c76e65 | ||
|
|
bf1f3bf00a | ||
|
|
c0dd04bc94 | ||
|
|
c2aba79db7 |
@@ -21,9 +21,7 @@
|
||||
"motion": "^10.15.5",
|
||||
"node-vibrant": "3.1.6",
|
||||
"pinia": "^2.0.17",
|
||||
"pinia-plugin-persistedstate": "^2.1.1",
|
||||
"sass": "^1.56.1",
|
||||
"sass-loader": "^13.2.0",
|
||||
"pinia-plugin-persistedstate": "^3.2.0",
|
||||
"v-wave": "^1.5.0",
|
||||
"vue": "^v3.2.45",
|
||||
"vue-debounce": "^3.0.2",
|
||||
@@ -40,6 +38,8 @@
|
||||
"eslint": "^8.46.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-plugin-vue": "^9.17.0",
|
||||
"sass": "^1.56.1",
|
||||
"sass-loader": "^13.2.0",
|
||||
"typescript": "^5.0.4",
|
||||
"vite": "^3.0.4",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
|
||||
15
public/workers/logtrack.js
Normal file
15
public/workers/logtrack.js
Normal file
@@ -0,0 +1,15 @@
|
||||
onmessage = (e) => {
|
||||
const { trackhash, duration, source, timestamp } = e.data;
|
||||
|
||||
const is_dev = location.port === "5173";
|
||||
const base_url = is_dev ? "http://localhost:1980" : location.origin;
|
||||
const url = base_url + "/logger/track/log";
|
||||
|
||||
fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ trackhash, duration, source, timestamp }),
|
||||
});
|
||||
};
|
||||
72
src/App.vue
72
src/App.vue
@@ -10,11 +10,11 @@
|
||||
NoSideBorders: !xxl,
|
||||
extendWidth: settings.extend_width && settings.can_extend_width,
|
||||
}"
|
||||
:style="{ maxWidth: `${content_height > 1080 ? '2220px' : '1720px'}` }"
|
||||
:style="{ maxWidth: `${content_height > 1080 ? '2220px' : '1760px'}` }"
|
||||
>
|
||||
<LeftSidebar v-if="!isMobile" />
|
||||
<NavBar />
|
||||
<div id="acontent" v-element-size="updateContentElemSize">
|
||||
<div id="acontent" ref="appcontent" v-element-size="updateContentElemSize">
|
||||
<BalancerProvider>
|
||||
<router-view />
|
||||
</BalancerProvider>
|
||||
@@ -27,10 +27,10 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
// @libraries
|
||||
import { onMounted } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { onStartTyping } from "@vueuse/core";
|
||||
import { vElementSize } from "@vueuse/components";
|
||||
import { onStartTyping } from "@vueuse/core";
|
||||
import { onMounted, Ref, ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { BalancerProvider } from "vue-wrap-balancer";
|
||||
|
||||
// @stores
|
||||
@@ -38,11 +38,13 @@ import {
|
||||
content_height,
|
||||
content_width,
|
||||
isMobile,
|
||||
updateCardWidth,
|
||||
} from "@/stores/content-width";
|
||||
import useModalStore from "@/stores/modal";
|
||||
import useQStore from "@/stores/queue";
|
||||
import useSettingsStore from "@/stores/settings";
|
||||
import useQueueStore from "@/stores/queue";
|
||||
import useLyrics from "@/stores/lyrics";
|
||||
import useModal from "@/stores/modal";
|
||||
import useQueue from "@/stores/queue";
|
||||
import useSettings from "@/stores/settings";
|
||||
import useTracker from "@/stores/tracker";
|
||||
|
||||
// @utils
|
||||
import handleShortcuts from "@/helpers/useKeyboard";
|
||||
@@ -56,20 +58,23 @@ import Notification from "@/components/Notification.vue";
|
||||
|
||||
// @app-grid-components
|
||||
import BottomBar from "@/components/BottomBar/BottomBar.vue";
|
||||
import LeftSidebar from "@/components/LeftSidebar/index.vue";
|
||||
import NavBar from "@/components/nav/NavBar.vue";
|
||||
import RightSideBar from "@/components/RightSideBar/Main.vue";
|
||||
import LeftSidebar from "@/components/LeftSidebar/index.vue";
|
||||
import { getAllSettings } from "@/requests/settings";
|
||||
|
||||
import { getRootDirs } from "@/requests/settings/rootdirs";
|
||||
import { baseApiUrl } from "@/config";
|
||||
// import BubbleManager from "./components/bubbles/BinManager.vue";
|
||||
|
||||
const appcontent: Ref<HTMLLegendElement | null> = ref(null);
|
||||
const queue = useQueue();
|
||||
const modal = useModal();
|
||||
const lyrics = useLyrics();
|
||||
const router = useRouter();
|
||||
const queue = useQueueStore();
|
||||
const modal = useModalStore();
|
||||
const settings = useSettingsStore();
|
||||
const settings = useSettings();
|
||||
useTracker();
|
||||
|
||||
handleShortcuts(useQStore, useModalStore);
|
||||
handleShortcuts(useQueue, useModal);
|
||||
|
||||
router.afterEach(() => {
|
||||
(document.getElementById("acontent") as HTMLElement).scrollTo(0, 0);
|
||||
@@ -83,6 +88,14 @@ onStartTyping(() => {
|
||||
elem.value = "";
|
||||
});
|
||||
|
||||
function getContentSize() {
|
||||
const elem = document.getElementById("acontent") as HTMLElement;
|
||||
return {
|
||||
width: elem.offsetWidth,
|
||||
height: elem.offsetHeight,
|
||||
};
|
||||
}
|
||||
|
||||
function updateContentElemSize({
|
||||
width,
|
||||
height,
|
||||
@@ -90,8 +103,11 @@ function updateContentElemSize({
|
||||
width: number;
|
||||
height: number;
|
||||
}) {
|
||||
content_width.value = width;
|
||||
// 1372 is the maxwidth of the #acontent. see app-grid.scss > $maxwidth
|
||||
const elem_width = Math.min(1372, appcontent.value?.offsetWidth || 0);
|
||||
content_width.value = elem_width;
|
||||
content_height.value = height;
|
||||
updateCardWidth();
|
||||
}
|
||||
|
||||
function handleWelcomeModal() {
|
||||
@@ -118,16 +134,28 @@ function handleRootDirsPrompt() {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
queue.startBufferingStatusWatcher();
|
||||
const { width, height } = getContentSize();
|
||||
updateContentElemSize({ width, height });
|
||||
|
||||
handleWelcomeModal();
|
||||
settings.initializeVolume();
|
||||
|
||||
if (baseApiUrl.value === null) {
|
||||
modal.showSetIPModal();
|
||||
return;
|
||||
}
|
||||
|
||||
handleRootDirsPrompt();
|
||||
|
||||
getAllSettings()
|
||||
.then(({ settings: data }) => {
|
||||
settings.mapDbSettings(data);
|
||||
})
|
||||
.then(() => {
|
||||
if (settings.use_lyrics_plugin) return;
|
||||
});
|
||||
|
||||
if (queue.currenttrack) {
|
||||
lyrics.checkExists(
|
||||
queue.currenttrack.filepath,
|
||||
queue.currenttrack.trackhash
|
||||
);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
4
src/assets/icons/lyrics.svg
Normal file
4
src/assets/icons/lyrics.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.39687 27.1131C9.04913 27.1131 9.53405 26.7937 10.3459 26.0807L14.3572 22.5294H21.4504C24.9784 22.5294 26.9547 20.503 26.9547 17.0251V7.93679C26.9547 4.45891 24.9784 2.4325 21.4504 2.4325H6.50218C2.97625 2.4325 1 4.4493 1 7.93679V17.0251C1 20.5126 3.02476 22.5294 6.42836 22.5294H6.9107V25.4181C6.9107 26.4533 7.45422 27.1131 8.39687 27.1131ZM8.96593 24.532V21.1863C8.96593 20.4986 8.67132 20.2335 8.01319 20.2335H6.56757C4.35225 20.2335 3.29592 19.1066 3.29592 16.9523V8.00007C3.29592 5.84569 4.35225 4.72842 6.56757 4.72842H21.3871C23.5928 4.72842 24.6588 5.84569 24.6588 8.00007V16.9523C24.6588 19.1066 23.5928 20.2335 21.3871 20.2335H14.2321C13.521 20.2335 13.1823 20.3484 12.6864 20.8497L8.96593 24.532Z" fill="currentColor"/>
|
||||
<path d="M8.2771 11.2555C8.2771 12.6189 9.13022 13.6523 10.4502 13.6523C10.9762 13.6523 11.4768 13.547 11.7878 13.1582H11.9174C11.5281 14.0908 10.6426 14.7372 9.78389 14.9491C9.37561 15.0578 9.23194 15.2495 9.23194 15.5329C9.23194 15.8427 9.4928 16.0804 9.83147 16.0804C11.0784 16.0804 13.4024 14.5982 13.4024 11.7334C13.4024 10.0933 12.3606 8.83142 10.776 8.83142C9.34444 8.83142 8.2771 9.8315 8.2771 11.2555ZM14.6327 11.2555C14.6327 12.6189 15.4837 13.6523 16.8058 13.6523C17.3317 13.6523 17.8302 13.547 18.1412 13.1582H18.273C17.8837 14.0908 16.9982 14.7372 16.1394 14.9491C15.7387 15.0578 15.5875 15.2495 15.5875 15.5329C15.5875 15.8427 15.8462 16.0804 16.1849 16.0804C17.4318 16.0804 19.7559 14.5982 19.7559 11.7334C19.7559 10.0933 18.7141 8.83142 17.1316 8.83142C15.6979 8.83142 14.6327 9.8315 14.6327 11.2555Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
4
src/assets/icons/lyrics2.svg
Normal file
4
src/assets/icons/lyrics2.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.39687 27.1131C9.04913 27.1131 9.53405 26.7937 10.3459 26.0807L14.3572 22.5294H21.4504C24.9784 22.5294 26.9547 20.503 26.9547 17.0251V7.93679C26.9547 4.45891 24.9784 2.4325 21.4504 2.4325H6.50218C2.97625 2.4325 1 4.4493 1 7.93679V17.0251C1 20.5126 3.02476 22.5294 6.42836 22.5294H6.9107V25.4181C6.9107 26.4533 7.45422 27.1131 8.39687 27.1131ZM8.96593 24.532V21.1863C8.96593 20.4986 8.67132 20.2335 8.01319 20.2335H6.56757C4.35225 20.2335 3.29592 19.1066 3.29592 16.9523V8.00007C3.29592 5.84569 4.35225 4.72842 6.56757 4.72842H21.3871C23.5928 4.72842 24.6588 5.84569 24.6588 8.00007V16.9523C24.6588 19.1066 23.5928 20.2335 21.3871 20.2335H14.2321C13.521 20.2335 13.1823 20.3484 12.6864 20.8497L8.96593 24.532Z" fill="currentColor"/>
|
||||
<path d="M8.2771 11.2555C8.2771 12.6189 9.13022 13.6523 10.4502 13.6523C10.9762 13.6523 11.4768 13.547 11.7878 13.1582H11.9174C11.5281 14.0908 10.6426 14.7372 9.78389 14.9491C9.37561 15.0578 9.23194 15.2495 9.23194 15.5329C9.23194 15.8427 9.4928 16.0804 9.83147 16.0804C11.0784 16.0804 13.4024 14.5982 13.4024 11.7334C13.4024 10.0933 12.3606 8.83142 10.776 8.83142C9.34444 8.83142 8.2771 9.8315 8.2771 11.2555ZM14.6327 11.2555C14.6327 12.6189 15.4837 13.6523 16.8058 13.6523C17.3317 13.6523 17.8302 13.547 18.1412 13.1582H18.273C17.8837 14.0908 16.9982 14.7372 16.1394 14.9491C15.7387 15.0578 15.5875 15.2495 15.5875 15.5329C15.5875 15.8427 15.8462 16.0804 16.1849 16.0804C17.4318 16.0804 19.7559 14.5982 19.7559 11.7334C19.7559 10.0933 18.7141 8.83142 17.1316 8.83142C15.6979 8.83142 14.6327 9.8315 14.6327 11.2555Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -1,4 +0,0 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M15.6328 35L8.625 10.3203H8.41406C8.74219 14.5234 8.90625 17.7891 8.90625 20.1172V35H0.703125V0.734375H13.0312L20.1797 25.0625H20.3672L27.375 0.734375H39.7266V35H31.2188V19.9766C31.2188 19.1953 31.2266 18.3281 31.2422 17.375C31.2734 16.4219 31.3828 14.0859 31.5703 10.3672H31.3594L24.4453 35H15.6328ZM76.2422 0.734375V21.3594C76.2422 25.8438 74.9688 29.3203 72.4219 31.7891C69.8906 34.2422 66.2344 35.4688 61.4531 35.4688C56.7812 35.4688 53.1875 34.2734 50.6719 31.8828C48.1719 29.4922 46.9219 26.0547 46.9219 21.5703V0.734375H56.2266V20.8438C56.2266 23.2656 56.6797 25.0234 57.5859 26.1172C58.4922 27.2109 59.8281 27.7578 61.5938 27.7578C63.4844 27.7578 64.8516 27.2188 65.6953 26.1406C66.5547 25.0469 66.9844 23.2656 66.9844 20.7969V0.734375H76.2422ZM105.844 24.5938C105.844 26.7188 105.305 28.6094 104.227 30.2656C103.148 31.9062 101.594 33.1875 99.5625 34.1094C97.5312 35.0156 95.1484 35.4688 92.4141 35.4688C90.1328 35.4688 88.2188 35.3125 86.6719 35C85.125 34.6719 83.5156 34.1094 81.8438 33.3125V25.0625C83.6094 25.9688 85.4453 26.6797 87.3516 27.1953C89.2578 27.6953 91.0078 27.9453 92.6016 27.9453C93.9766 27.9453 94.9844 27.7109 95.625 27.2422C96.2656 26.7578 96.5859 26.1406 96.5859 25.3906C96.5859 24.9219 96.4531 24.5156 96.1875 24.1719C95.9375 23.8125 95.5234 23.4531 94.9453 23.0938C94.3828 22.7344 92.8672 22 90.3984 20.8906C88.1641 19.875 86.4844 18.8906 85.3594 17.9375C84.25 16.9844 83.4219 15.8906 82.875 14.6562C82.3438 13.4219 82.0781 11.9609 82.0781 10.2734C82.0781 7.11719 83.2266 4.65625 85.5234 2.89062C87.8203 1.125 90.9766 0.242188 94.9922 0.242188C98.5391 0.242188 102.156 1.0625 105.844 2.70312L103.008 9.85156C99.8047 8.38281 97.0391 7.64844 94.7109 7.64844C93.5078 7.64844 92.6328 7.85938 92.0859 8.28125C91.5391 8.70312 91.2656 9.22656 91.2656 9.85156C91.2656 10.5234 91.6094 11.125 92.2969 11.6562C93 12.1875 94.8906 13.1562 97.9688 14.5625C100.922 15.8906 102.969 17.3203 104.109 18.8516C105.266 20.3672 105.844 22.2812 105.844 24.5938ZM111.141 35V0.734375H120.445V35H111.141ZM143.133 7.83594C140.93 7.83594 139.211 8.74219 137.977 10.5547C136.742 12.3516 136.125 14.8359 136.125 18.0078C136.125 24.6016 138.633 27.8984 143.648 27.8984C145.164 27.8984 146.633 27.6875 148.055 27.2656C149.477 26.8438 150.906 26.3359 152.344 25.7422V33.5703C149.484 34.8359 146.25 35.4688 142.641 35.4688C137.469 35.4688 133.5 33.9688 130.734 30.9688C127.984 27.9688 126.609 23.6328 126.609 17.9609C126.609 14.4141 127.273 11.2969 128.602 8.60938C129.945 5.92188 131.867 3.85938 134.367 2.42188C136.883 0.96875 139.836 0.242188 143.227 0.242188C146.93 0.242188 150.469 1.04688 153.844 2.65625L151.008 9.94531C149.742 9.35156 148.477 8.85156 147.211 8.44531C145.945 8.03906 144.586 7.83594 143.133 7.83594Z" fill="white"/>
|
||||
<path d="M160.289 14.4453C159.008 12.5859 158.367 9.9375 158.367 6.5V3.10156H167.602L185.039 22.2031C186.023 23.2969 186.68 24.2188 187.008 24.9688C187.336 25.7188 187.578 26.4062 187.734 27.0312C188.078 28.2812 188.25 29.8203 188.25 31.6484V35H179.273L160.359 14.4453H160.289ZM178.102 13.1094C178.102 8.8125 178.766 6.01562 180.094 4.71875C180.797 4.03125 181.711 3.59375 182.836 3.40625C183.977 3.20312 185.359 3.10156 186.984 3.10156H188.25V6.80469C188.25 10.1328 187.375 12.3594 185.625 13.4844C184.375 14.2969 182.273 14.7031 179.32 14.7031H178.102V13.1094ZM158.367 31.3438C158.367 28.0156 159.242 25.7812 160.992 24.6406C162.242 23.8438 164.344 23.4453 167.297 23.4453H168.516V25.0391C168.516 29.3828 167.852 32.1719 166.523 33.4062C165.617 34.25 164.297 34.75 162.562 34.9062C161.688 34.9688 160.711 35 159.633 35H158.367V31.3438Z" fill="#4AD168"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 3.6 KiB |
@@ -5,7 +5,6 @@
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr));
|
||||
padding: 0 1rem;
|
||||
padding-bottom: 4rem;
|
||||
overflow: auto;
|
||||
max-height: 100%;
|
||||
gap: 2rem 1rem;
|
||||
|
||||
@@ -23,9 +23,15 @@ $g-border: solid 1px $gray5;
|
||||
#acontent {
|
||||
width: 100%;
|
||||
grid-area: content;
|
||||
padding-right: calc($medium);
|
||||
|
||||
overflow: hidden;
|
||||
margin-right: $margright;
|
||||
}
|
||||
|
||||
.content-page {
|
||||
scrollbar-gutter: stable;
|
||||
padding-left: $padleft;
|
||||
padding-right: $padright;
|
||||
padding-bottom: $padbottom;
|
||||
}
|
||||
|
||||
.vue-recycle-scroller__item-wrapper {
|
||||
@@ -33,18 +39,23 @@ $g-border: solid 1px $gray5;
|
||||
}
|
||||
|
||||
.vue-recycle-scroller {
|
||||
padding-left: 1.25rem;
|
||||
scrollbar-gutter: stable;
|
||||
padding-left: $padleft;
|
||||
}
|
||||
|
||||
.r-sidebar {
|
||||
grid-area: r-sidebar;
|
||||
border-left: $g-border;
|
||||
|
||||
.vue-recycle-scroller {
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.topnav {
|
||||
grid-area: nav;
|
||||
margin: 1rem 0;
|
||||
padding: 1rem $padleft;
|
||||
padding-right: $padright;
|
||||
}
|
||||
|
||||
.b-bar {
|
||||
@@ -52,13 +63,6 @@ $g-border: solid 1px $gray5;
|
||||
border-top: $g-border;
|
||||
}
|
||||
|
||||
.content-page {
|
||||
margin-left: 1.25rem;
|
||||
margin-right: -$medium;
|
||||
padding-right: $medium;
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
|
||||
// ====== MODIFIERS =======
|
||||
|
||||
#app-grid.extendWidth {
|
||||
@@ -77,13 +81,7 @@ $g-border: solid 1px $gray5;
|
||||
|
||||
#acontent {
|
||||
margin-right: 0 !important;
|
||||
padding-right: $medium !important;
|
||||
}
|
||||
|
||||
.topnav {
|
||||
//reduce width to match #acontent
|
||||
width: calc(100% - 1rem);
|
||||
padding-right: 0;
|
||||
// padding-right: $medium !important;
|
||||
}
|
||||
|
||||
@include allPhones {
|
||||
@@ -102,19 +100,18 @@ $g-border: solid 1px $gray5;
|
||||
}
|
||||
|
||||
.v-scroll-page {
|
||||
width: calc(100% + $medium) !important;
|
||||
|
||||
.scroller {
|
||||
padding-right: $padright;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding-right: 1.25rem;
|
||||
padding-bottom: $content-padding-bottom;
|
||||
padding-bottom: $padbottom;
|
||||
}
|
||||
}
|
||||
|
||||
.isSmall {
|
||||
.songlist-item {
|
||||
grid-template-columns: 2fr 5.5rem;
|
||||
grid-template-columns: 2fr 5.5rem !important;
|
||||
}
|
||||
|
||||
.song-artists,
|
||||
|
||||
@@ -140,7 +140,6 @@ button {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
// NO THIS, NO THAT (OVERRIDES)
|
||||
.no-border {
|
||||
border: none;
|
||||
}
|
||||
@@ -170,3 +169,75 @@ button {
|
||||
position: absolute;
|
||||
left: -20rem;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
border: solid 3px rgb(0, 0, 0);
|
||||
border-top: solid 3px transparent;
|
||||
border-left: solid 3px transparent;
|
||||
border-radius: 50%;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
animation: spin 0.45s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.card-list-scroll-x {
|
||||
overflow: hidden;
|
||||
|
||||
h3 {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr max-content;
|
||||
align-items: baseline;
|
||||
padding: 0 $medium;
|
||||
margin-bottom: $medium;
|
||||
}
|
||||
|
||||
.cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(10.1rem, 1fr));
|
||||
overflow-x: auto;
|
||||
scroll-snap-type: x mandatory;
|
||||
flex-direction: row;
|
||||
padding-bottom: 2rem;
|
||||
|
||||
@include hideScrollbars;
|
||||
}
|
||||
|
||||
.album-card {
|
||||
&:hover {
|
||||
background-color: $gray;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.rhelp {
|
||||
text-transform: uppercase;
|
||||
font-size: 11px;
|
||||
color: $purple;
|
||||
font-weight: bold;
|
||||
margin: $smaller 0;
|
||||
|
||||
&.album {
|
||||
color: $orange;
|
||||
}
|
||||
|
||||
&.track {
|
||||
color: $pink;
|
||||
}
|
||||
|
||||
&.folder {
|
||||
color: $teal;
|
||||
}
|
||||
|
||||
&.playlist {
|
||||
color: $green;
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,7 @@
|
||||
}
|
||||
|
||||
@mixin tablet-portrait {
|
||||
@media (max-width: 810) {
|
||||
@media (max-width: 810px) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,14 +26,14 @@ $gray2: rgb(99, 99, 102);
|
||||
$gray3: rgb(72, 72, 74);
|
||||
$gray4: rgb(58, 58, 60);
|
||||
$gray5: rgb(44, 44, 46);
|
||||
$body: rgba(0, 0, 0, 0.95);
|
||||
$body: #111111;
|
||||
|
||||
$red: #ff453a;
|
||||
$blue: #0a84ff;
|
||||
$darkblue: #055ee2;
|
||||
$green: rgb(20, 160, 55);
|
||||
$green: rgb(94, 247, 132);
|
||||
$yellow: rgb(255, 214, 10);
|
||||
$orange: rgb(255, 159, 10);
|
||||
$orange: #ff9f0a;
|
||||
$pink: rgb(255, 55, 95);
|
||||
$purple: #bf5af2;
|
||||
$brown: rgb(172, 142, 104);
|
||||
@@ -55,3 +55,16 @@ $side-nav-svg: $red;
|
||||
$overshoot: cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
||||
|
||||
$separator: $gray4;
|
||||
|
||||
$maxwidth: 1372px;
|
||||
$maxpadleft: 5rem;
|
||||
$maxpadright: calc(100% - $maxwidth);
|
||||
|
||||
$padbottom: 4rem;
|
||||
$padleft: clamp(2rem, $maxpadright, $maxpadleft);
|
||||
$padright: clamp(
|
||||
2rem,
|
||||
max($maxpadright, 5rem),
|
||||
calc($maxpadright + $maxpadleft)
|
||||
);
|
||||
$margright: calc(0rem - $padright);
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
"./variables",
|
||||
"./ProgressBar.scss",
|
||||
"./BottomBar/BottomBar.scss",
|
||||
"./Global",
|
||||
"./moz.scss"
|
||||
"./Global"
|
||||
;
|
||||
|
||||
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
// Styles that only apply to our dear Firefox
|
||||
|
||||
@-moz-document url-prefix() {
|
||||
#acontent {
|
||||
margin-right: calc(-1rem + 1px);
|
||||
padding-right: 1rem;
|
||||
}
|
||||
|
||||
// applies to playlist list page
|
||||
.content-page {
|
||||
margin-right: calc(0rem - ($medium + 4px));
|
||||
}
|
||||
|
||||
// virtual scroller pages: folder, playlist, album
|
||||
.header-list-layout {
|
||||
margin-right: calc(0rem - ($medium + 4px)) !important;
|
||||
|
||||
.scrollable {
|
||||
padding-right: calc(1rem - 3px) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.v-scroll-page {
|
||||
width: calc(100% + 1rem) !important;
|
||||
|
||||
.scroller {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
// padding-right: 1.25rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
#app-grid.noSidebar > #acontent {
|
||||
padding-right: 1rem !important;
|
||||
}
|
||||
|
||||
.search-view {
|
||||
margin-right: -1rem !important;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div
|
||||
class="album_disc_header no-select"
|
||||
v-if="album_disc.is_album_disc_number"
|
||||
class="album_disc_header no-select"
|
||||
>
|
||||
<div class="disc_number">Disc {{ album_disc.album_page_disc_number }}</div>
|
||||
<div></div>
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
<template>
|
||||
<div class="card-list-scroll-x">
|
||||
<h3>
|
||||
<span>{{ title }}</span>
|
||||
<SeeAll
|
||||
v-if="route && maxAbumCards - 1 <= albums.length"
|
||||
:route="route"
|
||||
@click="
|
||||
!favorites ? useArtistDiscographyStore().setPage(albumType) : null
|
||||
"
|
||||
/>
|
||||
</h3>
|
||||
<div ref="artistItemsWrappers" class="cards">
|
||||
<AlbumCard
|
||||
v-for="a in albums"
|
||||
:key="a.albumhash"
|
||||
:album="a"
|
||||
:show_date="show_date"
|
||||
:artist_page="artist_page"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
|
||||
import { Album } from "@/interfaces";
|
||||
import { discographyAlbumTypes } from "@/enums";
|
||||
import { maxAbumCards } from "@/stores/content-width";
|
||||
import useArtistDiscographyStore from "@/stores/pages/artistDiscog";
|
||||
|
||||
import AlbumCard from "../shared/AlbumCard.vue";
|
||||
import SeeAll from "../shared/SeeAll.vue";
|
||||
|
||||
defineProps<{
|
||||
title: string;
|
||||
albums: Album[];
|
||||
albumType?: discographyAlbumTypes;
|
||||
favorites?: boolean;
|
||||
route?: string;
|
||||
show_date?: boolean;
|
||||
artist_page?: boolean;
|
||||
}>();
|
||||
|
||||
const artistItemsWrappers = ref<HTMLElement | null>(null);
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.card-list-scroll-x {
|
||||
overflow: hidden;
|
||||
|
||||
h3 {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr max-content;
|
||||
align-items: baseline;
|
||||
padding: 0 $medium;
|
||||
margin-bottom: $medium;
|
||||
}
|
||||
|
||||
.cards {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
overflow-x: auto;
|
||||
scroll-snap-type: x mandatory;
|
||||
flex-direction: row;
|
||||
padding-bottom: 2rem;
|
||||
|
||||
@include hideScrollbars;
|
||||
}
|
||||
|
||||
.album-card {
|
||||
&:hover {
|
||||
background-color: $gray;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -4,8 +4,8 @@
|
||||
|
||||
<HeartSvg
|
||||
:state="album.is_favorite"
|
||||
@handleFav="handleFav"
|
||||
:color="colors.bg ? colors.bg : ''"
|
||||
@handleFav="handleFav"
|
||||
/>
|
||||
<button
|
||||
class="options"
|
||||
|
||||
@@ -16,12 +16,13 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
|
||||
import { Album } from "@/interfaces";
|
||||
import { formatSeconds } from "@/utils";
|
||||
import { isSmallPhone } from "@/stores/content-width";
|
||||
|
||||
import ArtistName from "@/components/shared/ArtistName.vue";
|
||||
import { computed } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
album: Album;
|
||||
@@ -33,7 +34,9 @@ const statsText = computed(() => {
|
||||
// hide track count if it's a single, also add an s to track if it's plural
|
||||
return `• ${props.album.date} ${
|
||||
!is_single
|
||||
? `• ${props.album.count} Track${props.album.count > 1 ? "s" : ""}`
|
||||
? `• ${props.album.count.toLocaleString()} Track${
|
||||
props.album.count > 1 ? "s" : ""
|
||||
}`
|
||||
: ""
|
||||
} • ${formatSeconds(props.album.duration, true)}`;
|
||||
});
|
||||
|
||||
@@ -24,8 +24,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from "pinia";
|
||||
import { ref } from "vue";
|
||||
import { storeToRefs } from "pinia";
|
||||
|
||||
import { paths } from "@/config";
|
||||
import { isHeaderSmall } from "@/stores/content-width";
|
||||
|
||||
@@ -1,21 +1,33 @@
|
||||
<template>
|
||||
<div style="height: 1px"></div>
|
||||
<div style="height: 1px">
|
||||
<button v-if="show_text" @click="fetch_callback">Load More</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from "vue";
|
||||
import { nextTick, onMounted } from "vue";
|
||||
import { onBeforeRouteUpdate } from "vue-router";
|
||||
|
||||
import { updateCardWidth } from "@/stores/content-width";
|
||||
|
||||
const props = defineProps<{
|
||||
fetch_callback: () => void;
|
||||
reset_callback: () => void;
|
||||
show_text?: boolean;
|
||||
fetch_callback: () => Promise<void>;
|
||||
reset_callback?: () => Promise<void>;
|
||||
}>();
|
||||
|
||||
const update = async () => {
|
||||
await nextTick();
|
||||
|
||||
updateCardWidth();
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
props.fetch_callback();
|
||||
props.fetch_callback().then(update);
|
||||
});
|
||||
|
||||
onBeforeRouteUpdate(() => {
|
||||
props.reset_callback();
|
||||
if (!props.reset_callback) return;
|
||||
props.reset_callback().then(update);
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -19,13 +19,13 @@
|
||||
</div>
|
||||
<div class="stats">
|
||||
<span v-if="artist.trackcount">
|
||||
{{ artist.trackcount }} Track{{
|
||||
{{ artist.trackcount.toLocaleString() }} Track{{
|
||||
`${artist.trackcount == 1 ? "" : "s"}`
|
||||
}}
|
||||
</span>
|
||||
{{ artist.albumcount && artist.trackcount ? "•" : "" }}
|
||||
{{ artist.albumcount && artist.trackcount.toLocaleString() ? "•" : "" }}
|
||||
<span v-if="artist.albumcount">
|
||||
{{ artist.albumcount }} Album{{
|
||||
{{ artist.albumcount.toLocaleString() }} Album{{
|
||||
`${artist.albumcount == 1 ? "" : "s"}`
|
||||
}}
|
||||
</span>
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
<template>
|
||||
<div class="artists-view rounded">
|
||||
<div class="al-header">
|
||||
<div class="heading">ALL ARTISTS</div>
|
||||
<div class="search">
|
||||
<input type="search" placeholder="Search artists" />
|
||||
</div>
|
||||
</div>
|
||||
<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 t-center ellip">{{ artist.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
setup() {
|
||||
const artists = [
|
||||
{
|
||||
name: "Juice Wrld",
|
||||
},
|
||||
{
|
||||
name: "Eminem",
|
||||
},
|
||||
{
|
||||
name: "Sting",
|
||||
},
|
||||
{
|
||||
name: "Juice Wrld",
|
||||
},
|
||||
{
|
||||
name: "Eminem",
|
||||
},
|
||||
{
|
||||
name: "Sting",
|
||||
},
|
||||
{
|
||||
name: "Juice Wrld",
|
||||
},
|
||||
];
|
||||
|
||||
return {
|
||||
artists,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.artists-view {
|
||||
height: calc(100% - 14.5rem);
|
||||
padding: $small;
|
||||
margin-top: $small;
|
||||
overflow: hidden;
|
||||
|
||||
.all-albums {
|
||||
height: calc(100% - 4rem);
|
||||
border-top: 1px solid $separator;
|
||||
padding: $small 0 0 0;
|
||||
overflow-y: auto;
|
||||
|
||||
.item {
|
||||
position: relative;
|
||||
width: 10.9rem;
|
||||
height: 12rem;
|
||||
padding: $small 0.95rem $small 0.95rem;
|
||||
margin: $smaller;
|
||||
transition: all 0.2s ease-in-out;
|
||||
cursor: default;
|
||||
float: left;
|
||||
|
||||
&:hover {
|
||||
background-color: rgb(74, 84, 87);
|
||||
// border: solid
|
||||
}
|
||||
|
||||
.album-art {
|
||||
height: 9em;
|
||||
width: 9em;
|
||||
border-radius: 50%;
|
||||
background-image: url(../../assets/images/null.webp);
|
||||
}
|
||||
|
||||
.name {
|
||||
margin-top: $small;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.al-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
height: 4em;
|
||||
padding: 0 $small 0 $small;
|
||||
|
||||
.search {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
padding-right: $small;
|
||||
|
||||
input {
|
||||
width: 30rem;
|
||||
border: none;
|
||||
border-radius: 2rem;
|
||||
padding-left: 1rem;
|
||||
background-color: #4645456c;
|
||||
color: rgba(255, 255, 255, 0.521);
|
||||
font-size: 1rem;
|
||||
line-height: 3rem;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
input::-webkit-search-cancel-button {
|
||||
position: relative;
|
||||
right: 20px;
|
||||
cursor: default;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,160 +0,0 @@
|
||||
<template>
|
||||
<div class="top-artists">
|
||||
<div class="heading">TOP ARTISTS</div>
|
||||
<div class="items">
|
||||
<div class="item rounded" v-for="artist in artists" :key="artist">
|
||||
<div class="image"></div>
|
||||
<div class="info">
|
||||
<div class="name ellip">{{ artist.name }}</div>
|
||||
<div class="artist ellip">{{ artist.album_count }} Albums</div>
|
||||
<div class="separator"></div>
|
||||
<div class="top">
|
||||
<div class="play-icon"></div>
|
||||
<div class="text ellip">{{ artist.top_track }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script >
|
||||
export default {
|
||||
setup() {
|
||||
const artists = [
|
||||
{
|
||||
name: "Sting",
|
||||
album_count: "12",
|
||||
top_track: "Alien in Newyork",
|
||||
},
|
||||
{
|
||||
name: "Juice Wrld",
|
||||
album_count: "4",
|
||||
top_track: "Girl Of My Dreams",
|
||||
},
|
||||
{
|
||||
name: "Lil Peep",
|
||||
album_count: "6",
|
||||
top_track: "Haunt U",
|
||||
},
|
||||
];
|
||||
|
||||
return {
|
||||
artists,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.top-artists {
|
||||
height: 14rem;
|
||||
border-radius: $small;
|
||||
padding: $small;
|
||||
|
||||
.heading {
|
||||
margin: $small 0 1.5em $small;
|
||||
}
|
||||
|
||||
.items {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
grid-gap: $small;
|
||||
|
||||
.item {
|
||||
height: 10rem;
|
||||
width: 100%;
|
||||
max-width: 25rem;
|
||||
background-color: rgb(7, 6, 6);
|
||||
display: grid;
|
||||
align-items: center;
|
||||
grid-template-columns: 7.5rem 1fr;
|
||||
padding: $small;
|
||||
cursor: default;
|
||||
transition: all 0.2s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-.5em);
|
||||
}
|
||||
|
||||
.image {
|
||||
height: 7rem;
|
||||
width: 7rem;
|
||||
// background-image: url("../../assets/images/null.webp");
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.info .name {
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.info .artist {
|
||||
font-size: small;
|
||||
color: rgba(255, 255, 255, 0.699);
|
||||
}
|
||||
|
||||
.info .top {
|
||||
height: 2.5rem;
|
||||
background-color: rgb(51, 129, 20);
|
||||
border-radius: $small;
|
||||
margin-left: auto;
|
||||
display: grid;
|
||||
grid-template-columns: 2.5rem 1fr;
|
||||
align-items: center;
|
||||
transition: all 0.2s ease-in-out;
|
||||
user-select: none;
|
||||
|
||||
.play-icon {
|
||||
margin: 0 0 0 $small;
|
||||
height: 1.2rem;
|
||||
width: 1.2rem;
|
||||
background-image: url(../../assets/icons/play.svg);
|
||||
background-size: 96%;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.text {
|
||||
color: white;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: rgb(0, 134, 89);
|
||||
transition: all 0.2s ease-in-out;
|
||||
|
||||
.play-icon {
|
||||
transform: scale(1.2);
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
transition: all 0.1s ease-in-out;
|
||||
}
|
||||
}
|
||||
&:first-child {
|
||||
background-color: rgb(177, 116, 2);
|
||||
|
||||
.image {
|
||||
background-image: url("../../assets/images/null.webp");
|
||||
}
|
||||
}
|
||||
|
||||
&:nth-child(2){
|
||||
background-color: rgb(0, 74, 117);
|
||||
|
||||
.image {
|
||||
background-image: url("../../assets/images/null.webp");
|
||||
}
|
||||
}
|
||||
|
||||
&:nth-child(3){
|
||||
background-color: rgb(161, 106, 106);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -10,9 +10,10 @@
|
||||
title="Go to Now Playing"
|
||||
:to="{
|
||||
name: Routes.nowPlaying,
|
||||
query: {
|
||||
tab: 'queue',
|
||||
params: {
|
||||
tab: 'home',
|
||||
},
|
||||
replace: true,
|
||||
}"
|
||||
>
|
||||
<img
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<div class="right-group">
|
||||
<LyricsButton v-if="settings.use_lyrics_plugin || lyrics.exists" />
|
||||
<Volume />
|
||||
<button
|
||||
class="repeat"
|
||||
@@ -25,16 +26,19 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import useQStore from "@/stores/queue";
|
||||
import useSettingsStore from "@/stores/settings";
|
||||
import useQueue from "@/stores/queue";
|
||||
import useSettings from "@/stores/settings";
|
||||
import useLyrics from "@/stores/lyrics";
|
||||
|
||||
import HeartSvg from "../shared/HeartSvg.vue";
|
||||
import RepeatAllSvg from "@/assets/icons/repeat.svg";
|
||||
import RepeatOneSvg from "@/assets/icons/repeat-one.svg";
|
||||
import Volume from "./Volume.vue";
|
||||
import LyricsButton from "../shared/LyricsButton.vue";
|
||||
|
||||
const queue = useQStore();
|
||||
const settings = useSettingsStore();
|
||||
const queue = useQueue();
|
||||
const lyrics = useLyrics();
|
||||
const settings = useSettings();
|
||||
|
||||
defineProps<{
|
||||
hideHeart?: boolean;
|
||||
@@ -49,7 +53,7 @@ defineEmits<{
|
||||
.right-group {
|
||||
display: grid;
|
||||
justify-content: flex-end;
|
||||
grid-template-columns: repeat(3, max-content);
|
||||
grid-template-columns: repeat(4, max-content);
|
||||
align-items: center;
|
||||
height: 4rem;
|
||||
|
||||
@@ -69,9 +73,8 @@ defineEmits<{
|
||||
}
|
||||
}
|
||||
|
||||
button.repeat {
|
||||
background-color: transparent;
|
||||
|
||||
.lyrics,
|
||||
.repeat {
|
||||
svg {
|
||||
transform: scale(0.75);
|
||||
}
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
<template>
|
||||
<div class="card-list-scroll-x">
|
||||
<h3>Recent</h3>
|
||||
<div ref="recentitemswrappers" class="cards">
|
||||
<Recentsitemcard
|
||||
v-for="fav in favs.slice(0, maxAbumCards)"
|
||||
:key="JSON.stringify(fav)"
|
||||
:fav="fav"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
|
||||
import Recentsitemcard from "@/components/Favorites/RecentsItemCard.vue";
|
||||
import { RecentFavResult } from "@/interfaces";
|
||||
import { maxAbumCards } from "@/stores/content-width";
|
||||
|
||||
defineProps<{
|
||||
favs: RecentFavResult[];
|
||||
}>();
|
||||
|
||||
const recentitemswrappers = ref<HTMLElement | null>(null);
|
||||
</script>
|
||||
@@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<div class="breadcrumb-nav">
|
||||
<div
|
||||
class="path"
|
||||
v-for="path in subPaths"
|
||||
:key="path.path"
|
||||
class="path"
|
||||
:class="{ inthisfolder: path.active }"
|
||||
@click.prevent="$emit('navigate', path.path)"
|
||||
>
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
<template>
|
||||
<router-link :to="{ name: Routes.folder, params: { path: folder.path } }">
|
||||
<div
|
||||
v-auto-animate
|
||||
class="f-item"
|
||||
@click="(e) => (folder_page ? null : handleClick(e))"
|
||||
@mouseover="mouse_over = true"
|
||||
@mouseleave="mouse_over = false"
|
||||
:style="{
|
||||
backgroundColor: is_checked ? '#234ece' : '',
|
||||
}"
|
||||
:class="{ context_menu_showing }"
|
||||
@click="(e) => (folder_page ? null : handleClick(e))"
|
||||
@mouseover="mouse_over = true"
|
||||
@mouseleave="mouse_over = false"
|
||||
@contextmenu.prevent="(e) => (!folder_page ? null : showContextMenu(e))"
|
||||
v-auto-animate
|
||||
>
|
||||
<SymLinkSvg v-if="folder.is_sym" />
|
||||
<FolderSvg v-else />
|
||||
<div class="info">
|
||||
<div class="f-item-text ellip">{{ folder.name }}</div>
|
||||
<div class="f-count" v-if="folder.count">
|
||||
<div v-if="folder.count" class="f-count">
|
||||
{{ folder.count + ` File${folder.count == 1 ? "" : "s"}` }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="check" v-if="!folder_page">
|
||||
<div v-if="!folder_page" class="check">
|
||||
<CheckSvg v-if="!is_checked && mouse_over" />
|
||||
<CheckFilledSvg v-if="is_checked" />
|
||||
</div>
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
<template>
|
||||
<h1>This is your Homepage</h1>
|
||||
</template>
|
||||
68
src/components/HomeView/Browse.vue
Normal file
68
src/components/HomeView/Browse.vue
Normal file
@@ -0,0 +1,68 @@
|
||||
<template>
|
||||
<div class="homebrowse">
|
||||
<div class="btitle"><b>Browse Library</b></div>
|
||||
<div class="browselist">
|
||||
<RouterLink
|
||||
v-for="i in browselist"
|
||||
:key="i.title"
|
||||
class="browseitem rounded-sm t-center"
|
||||
:to="{ name: i.route, params: i.params }"
|
||||
:style="{ width: `${album_card_with - 24}px` }"
|
||||
>
|
||||
{{ i.title }}
|
||||
</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Routes } from "@/router";
|
||||
import { album_card_with } from "@/stores/content-width";
|
||||
|
||||
const browselist = [
|
||||
{
|
||||
title: "Folders",
|
||||
route: Routes.folder,
|
||||
params: {
|
||||
path: "$home",
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Albums",
|
||||
route: Routes.AlbumList,
|
||||
},
|
||||
{
|
||||
title: "Artists",
|
||||
route: Routes.ArtistList,
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.homebrowse {
|
||||
padding: 1rem 0;
|
||||
padding-left: $medium;
|
||||
|
||||
.btitle {
|
||||
font-size: 1.15rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.browselist {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1.5rem;
|
||||
margin-top: $small;
|
||||
}
|
||||
|
||||
.browseitem {
|
||||
padding: 1.5rem 0;
|
||||
background-color: $gray;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
.browseitem:hover {
|
||||
background-color: $gray5;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -17,10 +17,10 @@
|
||||
}"
|
||||
></div>
|
||||
<div
|
||||
v-else
|
||||
id="home-button"
|
||||
class="nav-button"
|
||||
:class="{ active: $route.name === menu.route_name }"
|
||||
id="home-button"
|
||||
v-else
|
||||
>
|
||||
<div class="in">
|
||||
<component :is="menu.icon"></component>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div
|
||||
class="bitrate"
|
||||
v-if="q.currenttrack?.bitrate"
|
||||
class="bitrate"
|
||||
title="file type • bitrate"
|
||||
>
|
||||
{{ q.currenttrack.filepath?.split('.').pop() }} • {{ q.currenttrack.bitrate }}
|
||||
|
||||
@@ -1,31 +1,33 @@
|
||||
<template>
|
||||
<div class="hotkeys no-scroll">
|
||||
<button @click.prevent="q.playPrev">
|
||||
<button @click.prevent="queue.playPrev">
|
||||
<PrevSvg />
|
||||
</button>
|
||||
<button @click.prevent="q.playPause">
|
||||
<Spinner v-if="q.buffering && q.playing" />
|
||||
<PauseSvg v-else-if="q.playing" />
|
||||
<PlaySvg v-else/>
|
||||
<button @click.prevent="queue.playPause">
|
||||
<Spinner v-if="buffering && queue.playing" />
|
||||
<PauseSvg v-else-if="queue.playing" />
|
||||
<PlaySvg v-else />
|
||||
</button>
|
||||
<button @click.prevent="q.playNext">
|
||||
<button @click.prevent="queue.playNext">
|
||||
<NextSvg />
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { usePlayer } from "@/stores/player";
|
||||
import useQStore from "@/stores/queue";
|
||||
|
||||
import {
|
||||
default as NextSvg,
|
||||
default as PrevSvg,
|
||||
default as NextSvg,
|
||||
default as PrevSvg,
|
||||
} from "@/assets/icons/next.svg";
|
||||
import PauseSvg from "@/assets/icons/pause.svg";
|
||||
import PlaySvg from "@/assets/icons/play.svg";
|
||||
import Spinner from "@/components/shared/Spinner.vue";
|
||||
|
||||
const q = useQStore();
|
||||
const queue = useQStore();
|
||||
const { buffering } = usePlayer();
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@@ -68,4 +70,4 @@ const q = useQStore();
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
@@ -13,6 +13,9 @@
|
||||
<router-link
|
||||
:to="{
|
||||
name: Routes.nowPlaying,
|
||||
params: {
|
||||
tab: 'home',
|
||||
},
|
||||
}"
|
||||
>
|
||||
<img
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
<template>
|
||||
<div class="side-nav-container">
|
||||
<router-link
|
||||
v-for="(menu, index) in menus.filter((i) =>
|
||||
i.unlock_key ? i.unlock_key() : true
|
||||
)"
|
||||
v-for="(menu, index) in menus"
|
||||
:key="index"
|
||||
v-wave
|
||||
:to="{
|
||||
@@ -18,7 +16,7 @@
|
||||
}"
|
||||
>
|
||||
<div v-if="!menu.separator">
|
||||
<component :is="menu.icon" :class="menu.class" />
|
||||
<component :is="menu.icon" />
|
||||
<span>{{ menu.name }}</span>
|
||||
</div>
|
||||
</router-link>
|
||||
@@ -86,7 +84,7 @@ import { menus } from "./navitems";
|
||||
transform: scale(0.9);
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
|
||||
svg.radiosvg {
|
||||
transform: scale(0.7);
|
||||
}
|
||||
|
||||
@@ -6,8 +6,14 @@ import HeartSvg from "@/assets/icons/heart.svg";
|
||||
import PlaylistSvg from "@/assets/icons/playlist-1.svg";
|
||||
import SearchSvg from "@/assets/icons/search.svg";
|
||||
import SettingsSvg from "@/assets/icons/settings.svg";
|
||||
import HomeSvg from "@/assets/icons/home.svg";
|
||||
|
||||
export const menus = [
|
||||
{
|
||||
name: "home",
|
||||
route_name: Routes.Home,
|
||||
icon: HomeSvg,
|
||||
},
|
||||
{
|
||||
name: "folders",
|
||||
route_name: Routes.folder,
|
||||
@@ -40,6 +46,7 @@ export const menus = [
|
||||
{
|
||||
name: "settings",
|
||||
route_name: Routes.settings,
|
||||
params: { tab: "general" },
|
||||
icon: SettingsSvg,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
:class="notif.type"
|
||||
>
|
||||
<component :is="getSvg(notif.type)" class="notif-icon" />
|
||||
<div class="notif-text ellip">{{ notif.text }}</div>
|
||||
<div class="notif-text">{{ notif.text }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
/>
|
||||
</RouterLink>
|
||||
<NowPlayingInfo @handle-fav="handleFav" />
|
||||
<Progress v-if="isSmallPhone"/>
|
||||
<Progress v-if="isSmallPhone" />
|
||||
<div v-if="isSmallPhone" class="below-progress">
|
||||
<div class="time">
|
||||
{{ formatSeconds(queue.duration.current) }}
|
||||
@@ -30,17 +30,13 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h3 v-if="queue.currenttrack">Now Playing</h3>
|
||||
<h3 v-if="queue.next">Up Next</h3>
|
||||
<SongItem
|
||||
v-if="queue.currenttrack"
|
||||
:track="queue.currenttrack"
|
||||
:index="queue.currentindex + 1"
|
||||
v-if="queue.next"
|
||||
:track="queue.next"
|
||||
:index="queue.nextindex + 1"
|
||||
:source="dropSources.folder"
|
||||
:style="{
|
||||
backgroundColor: colors.theme1,
|
||||
color: getTextColor(colors.theme1),
|
||||
}"
|
||||
@play-this="() => {}"
|
||||
@play-this="queue.playNext"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -49,21 +45,18 @@
|
||||
import { paths } from "@/config";
|
||||
import { Routes } from "@/router";
|
||||
import useQueueStore from "@/stores/queue";
|
||||
|
||||
import Progress from "@/components/LeftSidebar/NP/Progress.vue";
|
||||
import { formatSeconds } from "@/utils";
|
||||
import { dropSources, favType } from "@/enums";
|
||||
import { isSmallPhone } from "@/stores/content-width";
|
||||
import favoriteHandler from "@/helpers/favoriteHandler";
|
||||
import useColorStore from "@/stores/colors";
|
||||
import { getTextColor } from "@/utils/colortools/shift";
|
||||
import SongItem from "../shared/SongItem.vue";
|
||||
import NowPlayingInfo from "./NowPlayingInfo.vue";
|
||||
|
||||
import PlayingFrom from "./PlayingFrom.vue";
|
||||
import Buttons from "../BottomBar/Right.vue";
|
||||
import { isSmallPhone } from "@/stores/content-width";
|
||||
import { formatSeconds } from "@/utils";
|
||||
import SongItem from "../shared/SongItem.vue";
|
||||
import NowPlayingInfo from "./NowPlayingInfo.vue";
|
||||
import Progress from "@/components/LeftSidebar/NP/Progress.vue";
|
||||
|
||||
const queue = useQueueStore();
|
||||
const colors = useColorStore();
|
||||
|
||||
function handleFav() {
|
||||
favoriteHandler(
|
||||
|
||||
@@ -26,22 +26,19 @@
|
||||
</div>
|
||||
</div>
|
||||
</router-link>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from "vue";
|
||||
import { computed } from "vue";
|
||||
import { RouteLocationRaw } from "vue-router";
|
||||
|
||||
import useTracklist from "@/stores/queue/tracklist";
|
||||
|
||||
import { FromOptions } from "@/enums";
|
||||
import useQueueStore from "@/stores/queue";
|
||||
import playingFrom from "@/utils/playingFrom";
|
||||
|
||||
const queue = useQueueStore();
|
||||
|
||||
|
||||
|
||||
const queue = useTracklist();
|
||||
|
||||
const data = computed(() => {
|
||||
const { name, location, icon, image } = playingFrom(queue.from);
|
||||
@@ -55,8 +52,6 @@ const data = computed(() => {
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
|
||||
}
|
||||
|
||||
.now-playling-from-link {
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
<template>
|
||||
<div class="card-list-scroll-x">
|
||||
<h3>{{ title }} <SeeAll v-if="route" :route="route" /></h3>
|
||||
<div ref="artistItemswrappers" class="cards">
|
||||
<ArtistCard
|
||||
v-for="artist in artists.slice(0, maxAbumCards)"
|
||||
:key="artist.image"
|
||||
:artist="artist"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
|
||||
import { Artist } from "@/interfaces";
|
||||
import { maxAbumCards } from "@/stores/content-width";
|
||||
|
||||
import ArtistCard from "@/components/shared/ArtistCard.vue";
|
||||
import SeeAll from "../shared/SeeAll.vue";
|
||||
|
||||
defineProps<{
|
||||
artists: Artist[];
|
||||
title: string;
|
||||
route?: string;
|
||||
}>();
|
||||
|
||||
const artistItemswrappers = ref<HTMLElement | null>(null);
|
||||
</script>
|
||||
@@ -11,6 +11,7 @@
|
||||
:class="{ 'use-sqr_img': useSqrImg }"
|
||||
>
|
||||
<div
|
||||
v-if="Number.isNaN"
|
||||
class="float"
|
||||
:style="{
|
||||
color: textColor,
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
</div>
|
||||
<div class="duration">
|
||||
{{
|
||||
playlist.info.count +
|
||||
playlist.info.count.toLocaleString() +
|
||||
` ${playlist.info.count == 1 ? "Track" : "Tracks"}`
|
||||
}}
|
||||
•
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="last-updated">
|
||||
<span class="status" v-if="!isHeaderSmall"
|
||||
<span v-if="!isHeaderSmall" class="status"
|
||||
>Last updated {{ playlist.info.last_updated }}  |  </span
|
||||
>
|
||||
<div class="edit" @click="editPlaylist">Edit  </div>
|
||||
|
||||
@@ -20,9 +20,13 @@
|
||||
:class="{ border: !playlist.thumb }"
|
||||
/>
|
||||
<div class="overlay rounded">
|
||||
<div v-if="playlist.help_text" class="rhelp playlist">{{ playlist.help_text }}</div>
|
||||
<div class="p-name ellip">{{ playlist.name }}</div>
|
||||
<div class="p-count">
|
||||
{{ playlist.count + ` ${playlist.count === 1 ? "Track" : "Tracks"}` }}
|
||||
<b>{{
|
||||
playlist.count.toLocaleString() +
|
||||
` Track${playlist.count === 1 ? "" : "s"}`
|
||||
}}</b>
|
||||
</div>
|
||||
</div>
|
||||
</router-link>
|
||||
@@ -33,8 +37,7 @@ import { paths } from "../../config";
|
||||
import { Playlist } from "../../interfaces";
|
||||
|
||||
const imguri = paths.images.playlist;
|
||||
|
||||
const props = defineProps<{
|
||||
defineProps<{
|
||||
playlist: Playlist;
|
||||
}>();
|
||||
</script>
|
||||
@@ -44,9 +47,10 @@ const props = defineProps<{
|
||||
background-color: #2c2c2e45;
|
||||
display: grid;
|
||||
grid-template-rows: 1fr max-content;
|
||||
padding: 1rem;
|
||||
padding: $medium;
|
||||
gap: $small;
|
||||
user-select: none;
|
||||
height: max-content;
|
||||
|
||||
.image-grid {
|
||||
display: grid;
|
||||
@@ -55,7 +59,7 @@ const props = defineProps<{
|
||||
|
||||
&:hover {
|
||||
transition: all 0.25s ease;
|
||||
background-color: $gray3;
|
||||
background-color: $gray4 !important;
|
||||
background-blend-mode: screen;
|
||||
}
|
||||
|
||||
@@ -65,6 +69,7 @@ const props = defineProps<{
|
||||
object-fit: cover;
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -74,25 +79,7 @@ const props = defineProps<{
|
||||
.p-count {
|
||||
opacity: 0.75;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.p-name {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.bottom {
|
||||
margin-top: $smaller;
|
||||
|
||||
.name {
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.count {
|
||||
font-size: $medium;
|
||||
opacity: 0.5;
|
||||
margin-top: $smaller;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
<template>
|
||||
<div class="r-home">
|
||||
<UpNext :track="queue.tracklist[queue.next]" :playNext="queue.playNext" />
|
||||
<UpNext :track="queue.tracklist[queue.next]" :play-next="queue.playNext" />
|
||||
<!-- <Recommendations /> -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.r-home {
|
||||
height: calc(100% - 1rem);
|
||||
}
|
||||
</style>
|
||||
|
||||
<script setup lang="ts">
|
||||
import useQStore from "../../../stores/queue";
|
||||
import UpNext from "../Queue/upNext.vue";
|
||||
const queue = useQStore();
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.r-home {
|
||||
height: calc(100% - 1rem);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="r-tracks rounded bg-primary">
|
||||
<div class="heading">Similar tracks</div>
|
||||
<div class="tracks">
|
||||
<div class="song-item" v-for="song in songs" :key="song.artist">
|
||||
<div v-for="song in songs" :key="song.artist" class="song-item">
|
||||
<img src="" class="rounded" loading="lazy"/>
|
||||
<div class="tags">
|
||||
<div class="title">{{ song.title }}</div>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<template>
|
||||
<div class="r-sidebar">
|
||||
<SearchInput />
|
||||
<div v-auto-animate class="r-content no-scroll" >
|
||||
<div class="r-dash" v-if="tabs.current === tabs.tabs.home">
|
||||
<div v-auto-animate class="r-content no-scroll">
|
||||
<div v-if="tabs.current === tabs.tabs.home" class="r-dash">
|
||||
<DashBoard />
|
||||
</div>
|
||||
<div class="r-search" v-if="tabs.current === tabs.tabs.search">
|
||||
<div v-if="tabs.current === tabs.tabs.search" class="r-search">
|
||||
<Search />
|
||||
</div>
|
||||
<div class="r-queue" v-if="tabs.current === tabs.tabs.queue">
|
||||
<div v-if="tabs.current === tabs.tabs.queue" class="r-queue">
|
||||
<Queue />
|
||||
</div>
|
||||
</div>
|
||||
@@ -16,10 +16,11 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import useTabStore from "../../stores/tabs";
|
||||
import DashBoard from "./Home/Main.vue";
|
||||
import useTabStore from "@/stores/tabs";
|
||||
|
||||
import Queue from "./Queue.vue";
|
||||
import Search from "./Search/Main.vue";
|
||||
import DashBoard from "./Home/Main.vue";
|
||||
import SearchInput from "./SearchInput.vue";
|
||||
|
||||
const tabs = useTabStore();
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
@mouseout="mouseover = false"
|
||||
>
|
||||
<NoItems
|
||||
:flag="!queue.tracklist.length"
|
||||
:flag="!store.tracklist.length"
|
||||
:title="'No songs in queue'"
|
||||
:description="'When you start playing songs, they will appear here.'"
|
||||
:icon="QueueSvg"
|
||||
@@ -36,18 +36,23 @@
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from "vue";
|
||||
|
||||
import useQStore from "@/stores/queue";
|
||||
import useInterface from "@/stores/interface";
|
||||
import useTracklist from "@/stores/queue/tracklist";
|
||||
|
||||
import TrackItem from "@/components/shared/TrackItem.vue";
|
||||
import QueueActions from "./Queue/QueueActions.vue";
|
||||
import NoItems from "../shared/NoItems.vue";
|
||||
import QueueActions from "./Queue/QueueActions.vue";
|
||||
import TrackItem from "@/components/shared/TrackItem.vue";
|
||||
import QueueSvg from "@/assets/icons/queue.svg";
|
||||
|
||||
const itemHeight = 64;
|
||||
const queue = useQStore();
|
||||
const store = useTracklist();
|
||||
const mouseover = ref(false);
|
||||
|
||||
const { focusCurrentInSidebar, setScrollFunction } = useInterface();
|
||||
|
||||
const scrollerItems = computed(() => {
|
||||
return queue.tracklist.map((track, index) => ({
|
||||
return store.tracklist.map((track, index) => ({
|
||||
track,
|
||||
id: index,
|
||||
}));
|
||||
@@ -70,12 +75,12 @@ function scrollToCurrent() {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
queue.setScrollFunction(scrollToCurrent, mouseover);
|
||||
queue.focusCurrentInSidebar();
|
||||
setScrollFunction(scrollToCurrent, mouseover);
|
||||
focusCurrentInSidebar();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
queue.setScrollFunction(() => {}, null);
|
||||
setScrollFunction(() => {}, null);
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -83,9 +88,5 @@ onBeforeUnmount(() => {
|
||||
.queue-virtual-scroller {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
.vue-recycle-scroller {
|
||||
padding: 0 !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
</div>
|
||||
<div class="right">
|
||||
<button
|
||||
class="menu"
|
||||
:class="{ 'btn-active': context_showing }"
|
||||
@click="showContextMenu"
|
||||
>
|
||||
@@ -19,20 +20,28 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import useQueueStore from "@/stores/queue";
|
||||
import useQueue from "@/stores/queue";
|
||||
import useTracklist from "@/stores/queue/tracklist";
|
||||
|
||||
import { showQueueContextMenu } from "@/helpers/contextMenuHandler";
|
||||
|
||||
import OptionsSvg from "@/assets/icons/more.svg";
|
||||
import ShuffleSvg from "@/assets/icons/shuffle.svg";
|
||||
import { showQueueContextMenu } from "@/helpers/contextMenuHandler";
|
||||
|
||||
const queue = useQueueStore();
|
||||
const queue = useQueue();
|
||||
const { tracklist } = useTracklist();
|
||||
|
||||
const context_showing = ref(false);
|
||||
|
||||
function showContextMenu(e: MouseEvent) {
|
||||
if (!queue.tracklist.length) return;
|
||||
if (!tracklist.length) return;
|
||||
|
||||
showQueueContextMenu(e, context_showing);
|
||||
}
|
||||
|
||||
defineProps<{
|
||||
onNowPlaying?: boolean;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@@ -43,6 +52,20 @@ function showContextMenu(e: MouseEvent) {
|
||||
margin: 1rem;
|
||||
margin-bottom: 0;
|
||||
|
||||
.lyricsversion {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
|
||||
.save {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
// hide on screens less than 600px
|
||||
@media screen and (max-width: 600px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.left {
|
||||
display: flex;
|
||||
gap: $small;
|
||||
@@ -56,11 +79,16 @@ function showContextMenu(e: MouseEvent) {
|
||||
}
|
||||
}
|
||||
|
||||
.right > button {
|
||||
padding: 0 $smaller;
|
||||
.right {
|
||||
display: flex;
|
||||
gap: $medium;
|
||||
|
||||
svg {
|
||||
transform: scale(1.2) rotate(90deg);
|
||||
.menu {
|
||||
padding: 0 $smaller;
|
||||
|
||||
svg {
|
||||
transform: scale(1.2) rotate(90deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<div class="morexx">
|
||||
<button
|
||||
@click.prevent="loader()"
|
||||
:class="{
|
||||
load_disabled: !can_load_more,
|
||||
}"
|
||||
@click.prevent="loader()"
|
||||
>
|
||||
<div class="text">Load More</div>
|
||||
</button>
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
<div class="right-search">
|
||||
<TabsWrapper
|
||||
:tabs="tabs"
|
||||
:current-tab="currentTab"
|
||||
:tab-content="true"
|
||||
@switchTab="switchTab"
|
||||
:currentTab="currentTab"
|
||||
:tabContent="true"
|
||||
>
|
||||
<Tab :name="currentTab" />
|
||||
</TabsWrapper>
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
<template>
|
||||
<div class="right-search-top-albums-or-artists">
|
||||
<AlbumCard
|
||||
v-for="album in search.top_results.albums.slice(
|
||||
0,
|
||||
!onSearchPage ? 3 : search.top_results.albums.length
|
||||
)"
|
||||
v-for="album in search.top_results.albums.slice(0, 3)"
|
||||
:key="album.albumhash"
|
||||
:album="album"
|
||||
/>
|
||||
@@ -16,10 +13,6 @@ import useSearchstore from "@/stores/search";
|
||||
import AlbumCard from "@/components/shared/AlbumCard.vue";
|
||||
|
||||
const search = useSearchstore();
|
||||
|
||||
defineProps<{
|
||||
onSearchPage?: boolean;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
<template>
|
||||
<div class="right-search-top-albums-or-artists">
|
||||
<ArtistCard
|
||||
v-for="artist in search.top_results.artists.slice(
|
||||
0,
|
||||
!onSearchPage ? 3 : search.top_results.artists.length
|
||||
)"
|
||||
v-for="artist in search.top_results.artists.slice(0, 3)"
|
||||
:key="artist.artisthash"
|
||||
:artist="artist"
|
||||
/>
|
||||
@@ -12,12 +9,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ArtistCard from "@/components/shared/ArtistCard.vue";
|
||||
import useSearchStore from "@/stores/search";
|
||||
import ArtistCard from "@/components/shared/ArtistCard.vue";
|
||||
|
||||
const search = useSearchStore();
|
||||
|
||||
defineProps<{
|
||||
onSearchPage?: boolean;
|
||||
}>();
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -17,15 +17,17 @@ import { Track } from "@/interfaces";
|
||||
|
||||
import useQueueStore from "@/stores/queue";
|
||||
import useSearchStore from "@/stores/search";
|
||||
import useTracklist from "@/stores/queue/tracklist";
|
||||
|
||||
import TrackItem from "@/components/shared/TrackItem.vue";
|
||||
|
||||
const search = useSearchStore();
|
||||
const queue = useQueueStore();
|
||||
const tracklist = useTracklist();
|
||||
|
||||
function handlePlay(track: Track) {
|
||||
queue.clearQueue();
|
||||
queue.playFromSearch(search.query, [track]);
|
||||
tracklist.setFromSearch(search.query, [track]);
|
||||
queue.play(0);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -4,21 +4,21 @@
|
||||
<TrackItem
|
||||
v-for="(track, index) in search.tracks.value"
|
||||
:key="track.id"
|
||||
:isCurrent="queue.currenttrackhash === track.trackhash"
|
||||
:isHighlighted="false"
|
||||
:isCurrentPlaying="
|
||||
:is-current="queue.currenttrackhash === track.trackhash"
|
||||
:is-highlighted="false"
|
||||
:is-current-playing="
|
||||
queue.currenttrackhash === track.trackhash && queue.playing
|
||||
"
|
||||
:track="track"
|
||||
@playThis="updateQueue(index)"
|
||||
:index="index + 1"
|
||||
@playThis="updateQueue(index)"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="t-center"><h5>No tracks</h5></div>
|
||||
<LoadMore
|
||||
v-if="search.tracks.value.length"
|
||||
:loader="search.loadTracks"
|
||||
:can_load_more="search.tracks.more"
|
||||
v-if="search.tracks.value.length"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -26,16 +26,19 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from "vue";
|
||||
|
||||
import TrackItem from "@/components/shared/TrackItem.vue";
|
||||
import useQStore from "@/stores/queue";
|
||||
import useSearchStore from "@/stores/search";
|
||||
import LoadMore from "./LoadMore.vue";
|
||||
import useQueue from "@/stores/queue";
|
||||
import useSearch from "@/stores/search";
|
||||
import useTracklist from "@/stores/queue/tracklist";
|
||||
|
||||
const queue = useQStore();
|
||||
const search = useSearchStore();
|
||||
import LoadMore from "./LoadMore.vue";
|
||||
import TrackItem from "@/components/shared/TrackItem.vue";
|
||||
|
||||
const queue = useQueue();
|
||||
const search = useSearch();
|
||||
const tracklist = useTracklist();
|
||||
|
||||
function updateQueue(index: number) {
|
||||
queue.playFromSearch(search.query, search.tracks.value);
|
||||
tracklist.setFromSearch(search.query, search.tracks.value);
|
||||
queue.play(index);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
@click.prevent="handleButton"
|
||||
>
|
||||
<SearchSvg v-if="on_nav || tabs.current === tabs.tabs.queue" />
|
||||
<BackSvg v-else-if="tabs.current === tabs.tabs.search" />
|
||||
<BackSvg v-else />
|
||||
</button>
|
||||
<input
|
||||
id="globalsearch"
|
||||
@@ -56,7 +56,7 @@ function removeFocusedClass() {
|
||||
function handleButton() {
|
||||
if (props.on_nav) return;
|
||||
|
||||
if (tabs.current === tabs.tabs.search) {
|
||||
if (tabs.current === tabs.tabs.search || tabs.current === tabs.tabs.lyrics) {
|
||||
tabs.switchToQueue();
|
||||
} else {
|
||||
tabs.switchToSearch();
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
<template></template>
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from "vue";
|
||||
|
||||
|
||||
57
src/components/SettingsView/About.vue
Normal file
57
src/components/SettingsView/About.vue
Normal file
@@ -0,0 +1,57 @@
|
||||
<template>
|
||||
<div class="aboutswingmusic">
|
||||
Swing Music is a labor of love developed by
|
||||
<a href="https://github.com/cwilvx" target="_blank">@<u>cwilvx</u></a> on
|
||||
GitHub. If you like this software, please consider donating to support
|
||||
development and giving it a star on GitHub. <br /><br /><br />
|
||||
<div class="flex">
|
||||
<a href="https://swingmusic.vercel.app/support-us.html" target="_blank">
|
||||
<button>Donate</button></a
|
||||
><a href="https://github.com/cwilvx/swingmusic" target="_blank"
|
||||
><button>Star on Github</button></a
|
||||
>
|
||||
<a href="https://github.com/cwilvx" target="_blank"
|
||||
><button>Follow @cwilvx on Github</button></a
|
||||
>
|
||||
</div>
|
||||
|
||||
<br /><br />
|
||||
If you encounter any bugs, please open an issue on GitHub. If you would like
|
||||
to get involved in development, start with the
|
||||
<a
|
||||
href="https://github.com/cwilvx/swingmusic/blob/master/.github/contributing.md"
|
||||
target="_blank"
|
||||
><u>contribution guidelines</u></a
|
||||
>.
|
||||
|
||||
<br /><br /><br />
|
||||
<div class="flex">
|
||||
<a
|
||||
href="https://github.com/cwilvx/swingmusic/issues/new/choose"
|
||||
target="_blank"
|
||||
>
|
||||
<button>Open an Issue</button>
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/cwilvx/swingmusic/blob/master/.github/contributing.md"
|
||||
target="_blank"
|
||||
><button>Contribute</button></a
|
||||
>
|
||||
</div>
|
||||
<br /><br />
|
||||
Let there be music 💃🕺!
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<style lang="scss">
|
||||
.aboutswingmusic {
|
||||
padding: $small;
|
||||
margin-top: 2rem;
|
||||
|
||||
.flex {
|
||||
gap: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<div class="setting-select rounded-sm no-scroll">
|
||||
<div
|
||||
class="option"
|
||||
v-for="option in optionsWithActive"
|
||||
:key="option.title"
|
||||
class="option"
|
||||
:class="{ active: option.active }"
|
||||
@click="setterFn(option.value)"
|
||||
>
|
||||
|
||||
@@ -24,13 +24,13 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Ref, computed, ref, watch } from "vue";
|
||||
import { Ref, computed, onMounted, ref } from "vue";
|
||||
|
||||
const separatorinput: Ref<HTMLInputElement | null> = ref(null);
|
||||
const input = ref("");
|
||||
|
||||
const props = defineProps<{
|
||||
default: () => string[];
|
||||
default: string[];
|
||||
submit: (input: string) => void;
|
||||
}>();
|
||||
|
||||
@@ -69,14 +69,16 @@ function submitInput() {
|
||||
|
||||
const preview_items = computed(() => splitInput(input.value));
|
||||
const default_input = computed(() =>
|
||||
props.default() ? props.default().join(", ") : ""
|
||||
props.default ? props.default.join(", ") : ""
|
||||
);
|
||||
|
||||
watch(props.default, (newval, _) => {
|
||||
const text = newval.join(", ");
|
||||
|
||||
if (separatorinput.value) separatorinput.value.value = text;
|
||||
onMounted(() => {
|
||||
const text = props.default.join(", ");
|
||||
input.value = text;
|
||||
|
||||
if (separatorinput.value) {
|
||||
separatorinput.value.value = text;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
state: boolean | null;
|
||||
state: undefined;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
|
||||
@@ -6,44 +6,62 @@
|
||||
Customize your settings and preferences
|
||||
</template>
|
||||
</GenericHeader>
|
||||
<GenericTabs
|
||||
:items="
|
||||
settingGroups.map((g) => ({
|
||||
title: g.title,
|
||||
params: {
|
||||
tab: g.title.toLowerCase(),
|
||||
},
|
||||
}))
|
||||
"
|
||||
:active="(item) => item.title === currentTab?.title"
|
||||
:route="Routes.settings"
|
||||
/>
|
||||
<Group
|
||||
v-for="(group, index) in settingGroups[current].groups"
|
||||
v-for="(group, index) in currentTab?.groups"
|
||||
:key="index"
|
||||
:group="group"
|
||||
/>
|
||||
<About v-if="currentTab?.title === 'About'" />
|
||||
<div class="version t-center">
|
||||
<LogoSvg /> <span>Swing Music - v{{ VERSION }}</span>
|
||||
<b>Swing Music - v{{ settings.version }}</b>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { VERSION } from "@/config";
|
||||
import { computed } from "vue";
|
||||
import { Routes } from "@/router";
|
||||
import { useRoute } from "vue-router";
|
||||
|
||||
import settingGroups from "@/settings";
|
||||
import useSettings from "@/stores/settings";
|
||||
|
||||
import Group from "./Group.vue";
|
||||
import About from "./About.vue";
|
||||
import GenericTabs from "@/components/shared/GenericTabs.vue";
|
||||
import GenericHeader from "@/components/shared/GenericHeader.vue";
|
||||
import LogoSvg from "@/assets/icons/logos/logo-light.svg";
|
||||
|
||||
defineProps<{
|
||||
current: number;
|
||||
}>();
|
||||
const route = useRoute();
|
||||
const settings = useSettings();
|
||||
|
||||
const currentTab = computed(() => {
|
||||
const tab = route.params.tab;
|
||||
return settingGroups.find((group) => group.title.toLowerCase() === tab);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.settingscontent {
|
||||
width: 35rem;
|
||||
max-width: 100%;
|
||||
padding-bottom: 2rem;
|
||||
|
||||
.version {
|
||||
margin: 2rem auto;
|
||||
color: $gray1;
|
||||
height: 3rem;
|
||||
width: max-content;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
opacity: 0.5;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
<template>
|
||||
<div v-if="group.show_if ? group.show_if() : true" class="settingsgroup">
|
||||
<div v-if="group.title || group.desc" class="info">
|
||||
<h4 v-if="group.title">{{ group.title }}</h4>
|
||||
<h4 v-if="group.title">
|
||||
{{ group.title
|
||||
}}<span v-if="group.experimental" class="experimental circular">
|
||||
{{ group.experimental ? "experimental" : "" }}
|
||||
</span>
|
||||
</h4>
|
||||
<div v-if="group.desc" class="desc">{{ group.desc }}</div>
|
||||
</div>
|
||||
<div class="setting rounded pad-lg">
|
||||
@@ -65,7 +70,7 @@
|
||||
<SeparatorsInput
|
||||
v-if="setting.type === SettingType.separators_input && setting.action"
|
||||
:submit="setting.action"
|
||||
:default="setting.state ? setting.state : () => []"
|
||||
:default="setting.state ? setting.state() : []"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -96,6 +101,15 @@ defineProps<{
|
||||
border-bottom: solid 1px $gray;
|
||||
padding-bottom: 2rem;
|
||||
|
||||
.experimental {
|
||||
font-size: 12px;
|
||||
margin-left: $small;
|
||||
opacity: 0.5;
|
||||
border: solid 1px $yellow;
|
||||
color: $yellow;
|
||||
padding: 0 $smaller;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<template>
|
||||
<div class="modal" v-if="modal.visible">
|
||||
<div v-if="modal.visible" class="modal">
|
||||
<div class="bg" @click="modal.hideModal"></div>
|
||||
<div
|
||||
v-motion-slide-top
|
||||
class="m-content rounded"
|
||||
:style="{
|
||||
maxWidth:
|
||||
modal.component == modal.options.setRootDirs ? '56rem' : '30rem',
|
||||
}"
|
||||
v-motion-slide-top
|
||||
>
|
||||
<div class="heading">{{ modal.title }}</div>
|
||||
<div class="close circular" @click="modal.hideModal">
|
||||
@@ -20,8 +20,8 @@
|
||||
@setTitle="setTitle"
|
||||
/>
|
||||
<UpdatePlaylist
|
||||
v-bind="modal.props"
|
||||
v-if="modal.component == modal.options.updatePlaylist"
|
||||
v-if="modal.component == modal.options.updatePlaylist"
|
||||
v-bind="modal.props"
|
||||
@hideModal="hideModal"
|
||||
@setTitle="setTitle"
|
||||
/>
|
||||
@@ -29,14 +29,10 @@
|
||||
<div v-if="modal.component == modal.options.deletePlaylist">
|
||||
<ConfirmModal
|
||||
:text="'Are you sure you want to permanently delete this playlist?'"
|
||||
:cancelAction="modal.hideModal"
|
||||
:confirmAction="deletePlaylist"
|
||||
:cancel-action="modal.hideModal"
|
||||
:confirm-action="deletePlaylist"
|
||||
/>
|
||||
</div>
|
||||
<SetIP
|
||||
v-if="modal.component == modal.options.SetIP"
|
||||
@setTitle="setTitle"
|
||||
/>
|
||||
<SetRootDirs
|
||||
v-if="modal.component == modal.options.setRootDirs"
|
||||
@hideModal="hideModal"
|
||||
@@ -59,7 +55,6 @@ import WelcomeModal from "./WelcomeModal.vue";
|
||||
import ConfirmModal from "./modals/ConfirmModal.vue";
|
||||
import NewPlaylist from "./modals/NewPlaylist.vue";
|
||||
import RootDirsPrompt from "./modals/RootDirsPrompt.vue";
|
||||
import SetIP from "./modals/SetIP.vue";
|
||||
import SetRootDirs from "./modals/SetRootDirs.vue";
|
||||
import UpdatePlaylist from "./modals/updatePlaylist.vue";
|
||||
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
<template>
|
||||
<div class="set-ip-modal">
|
||||
<label for="">Change the </label>
|
||||
<input
|
||||
id="modal-input"
|
||||
type="text"
|
||||
class="rounded-sm"
|
||||
v-model="ip_address"
|
||||
/>
|
||||
<button
|
||||
class="circular btn-active"
|
||||
@click.prevent="setBaseApiUrl(ip_address)"
|
||||
>
|
||||
Set address
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from "vue";
|
||||
import { setBaseApiUrl, baseApiUrl } from "@/config";
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "setTitle", title: string): void;
|
||||
}>();
|
||||
const title = "Set backend address";
|
||||
|
||||
const ip_address = ref("http://localhost:1970");
|
||||
|
||||
onMounted(() => {
|
||||
emit("setTitle", title);
|
||||
document.getElementById("modal-input")?.focus();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.set-ip-modal {
|
||||
#modal-input {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
button {
|
||||
margin: 0 auto;
|
||||
margin-top: $small;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<br /><br />
|
||||
<div style="position: relative">
|
||||
<div class="bread-nav rounded-sm" id="bread-nav">
|
||||
<div id="bread-nav" class="bread-nav rounded-sm">
|
||||
<span @click="fetchDirs('$root')">📁</span
|
||||
> <BreadCrumbNav :subPaths="subPaths" @navigate="fetchDirs" />
|
||||
> <BreadCrumbNav :sub-paths="subPaths" @navigate="fetchDirs" />
|
||||
</div>
|
||||
<div class="set-root-dirs-browser">
|
||||
<h4 v-if="no_more_dirs">
|
||||
@@ -16,11 +16,11 @@
|
||||
v-for="dir in dirs"
|
||||
:key="dir.name"
|
||||
:folder="dir"
|
||||
@navigate="fetchDirs(dir.path)"
|
||||
@check="handleCheck(dir.path)"
|
||||
:is_checked="
|
||||
selected.filter((p) => p == dir.path).length > 0 ? true : false
|
||||
"
|
||||
@navigate="fetchDirs(dir.path)"
|
||||
@check="handleCheck(dir.path)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -16,28 +16,21 @@
|
||||
<SearchTitle v-if="$route.name == Routes.search" />
|
||||
<PlaylistsTitle v-if="$route.name == Routes.playlists" />
|
||||
<QueueTitle v-if="$route.name == Routes.nowPlaying" />
|
||||
<ArtistDiscographyTitle
|
||||
v-if="$route.name == Routes.artistDiscography"
|
||||
/>
|
||||
<SimpleNav
|
||||
v-if="$route.name == Routes.artistTracks"
|
||||
:text="$route.query.artist as string || 'Artist Tracks'"
|
||||
/>
|
||||
<SimpleNav
|
||||
v-if="$route.name === Routes.favorites"
|
||||
:text="'Favorites ❤️'"
|
||||
/>
|
||||
<SimpleNav
|
||||
v-if="$route.name === Routes.favoriteAlbums"
|
||||
:text="'Favorite Albums ❤️'"
|
||||
:text="'Favorite Albums'"
|
||||
/>
|
||||
<SimpleNav
|
||||
v-if="$route.name === Routes.favoriteArtists"
|
||||
:text="'Favorite Artists ❤️'"
|
||||
:text="'Favorite Artists'"
|
||||
/>
|
||||
<SimpleNav
|
||||
v-if="$route.name === Routes.favoriteTracks"
|
||||
:text="'Favorite Tracks ❤️'"
|
||||
:text="'Favorite Tracks'"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -54,7 +47,6 @@ import { createSubPaths } from "@/utils";
|
||||
|
||||
import NavButtons from "./NavButtons.vue";
|
||||
|
||||
import ArtistDiscographyTitle from "./Titles/ArtistDiscographyTitle.vue";
|
||||
import FolderTitle from "./Titles/Folder.vue";
|
||||
import PlaylistsTitle from "./Titles/PlaylistsTitle.vue";
|
||||
import QueueTitle from "./Titles/QueueTitle.vue";
|
||||
@@ -98,8 +90,6 @@ watch(
|
||||
.topnav {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr min-content;
|
||||
width: 100%;
|
||||
padding: 0 1.25rem;
|
||||
|
||||
.left {
|
||||
display: grid;
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
<template>
|
||||
<div class="artist-discography-nav">
|
||||
<h1 class="ellip">{{ store.artistname }}</h1>
|
||||
<div class="buttons">
|
||||
<div class="select rounded-sm" v-auto-animate="{ duration: 10 }">
|
||||
<button class="selected" @click.prevent="showDropDown = !showDropDown">
|
||||
<span class="ellip">{{ store.page }}</span>
|
||||
<ArrowSvg />
|
||||
</button>
|
||||
<div
|
||||
ref="dropOptionsRef"
|
||||
class="options rounded-sm shadow-lg"
|
||||
v-if="showDropDown"
|
||||
>
|
||||
<div
|
||||
class="option"
|
||||
v-for="a in albums"
|
||||
@click.prevent="switchView(a)"
|
||||
>
|
||||
{{ a }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onClickOutside } from "@vueuse/core";
|
||||
|
||||
import ArrowSvg from "@/assets/icons/expand.svg";
|
||||
import { discographyAlbumTypes as albums } from "@/enums";
|
||||
import { Ref, ref } from "vue";
|
||||
|
||||
import useArtistDiscogStore from "@/stores/pages/artistDiscog";
|
||||
|
||||
const store = useArtistDiscogStore();
|
||||
|
||||
const showDropDown = ref(false);
|
||||
const dropOptionsRef: Ref<HTMLElement | undefined> = ref();
|
||||
|
||||
function hideDropDown() {
|
||||
showDropDown.value = false;
|
||||
}
|
||||
|
||||
function switchView(album: albums) {
|
||||
store.setAlbums(album);
|
||||
hideDropDown();
|
||||
}
|
||||
|
||||
onClickOutside(dropOptionsRef, (e) => {
|
||||
// @ts-ignore
|
||||
e.stopImmediatePropagation();
|
||||
hideDropDown();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.artist-discography-nav {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.selected {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 2rem;
|
||||
gap: $smaller;
|
||||
width: 100%;
|
||||
padding-right: 0;
|
||||
|
||||
svg {
|
||||
transform: rotate(90deg) scale(0.9);
|
||||
}
|
||||
}
|
||||
|
||||
.select {
|
||||
position: relative;
|
||||
width: 8rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: calc($medium + 2px);
|
||||
z-index: 10;
|
||||
|
||||
.options {
|
||||
background-color: $gray;
|
||||
position: absolute;
|
||||
top: 120%;
|
||||
padding: $small $smaller;
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.option {
|
||||
padding: $small;
|
||||
border-bottom: 1px solid $gray4;
|
||||
width: 7.5rem;
|
||||
|
||||
&:hover {
|
||||
border-radius: $smaller;
|
||||
border-bottom: 1px solid transparent;
|
||||
background-color: $darkestblue;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="nav-queue-title">
|
||||
<QueueActions />
|
||||
<QueueActions :on-now-playing="true" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -17,7 +17,6 @@ import QueueActions from "@/components/RightSideBar/Queue/QueueActions.vue";
|
||||
.queue-actions {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
<template>
|
||||
<div class="nav-search-input">
|
||||
<SearchInput :on_nav="true" />
|
||||
<div v-if="!isMobile" class="buttons-area">
|
||||
<Tabs
|
||||
:tabs="tabs"
|
||||
:current-tab="($route.params.page as string)"
|
||||
@switchTab="(tab: string) => {
|
||||
<Tabs
|
||||
v-if="!isMobile"
|
||||
:tabs="tabs"
|
||||
:current-tab="($route.params.page as string)"
|
||||
@switchTab="(tab: string) => {
|
||||
$router.replace({ name: Routes.search, params: { page: tab }, query: {
|
||||
q: search.query,
|
||||
} });
|
||||
search.switchTab(tab);
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -32,33 +31,17 @@ const tabs = ["top", "tracks", "albums", "artists"];
|
||||
.nav-search-input {
|
||||
align-items: center;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(14rem, 20rem) max-content;
|
||||
grid-template-columns: minmax(10rem, 20rem) max-content;
|
||||
justify-content: space-between;
|
||||
gap: 2rem;
|
||||
gap: 1rem;
|
||||
|
||||
@include allPhones {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.buttons-area {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 18rem;
|
||||
}
|
||||
|
||||
#right-tabs {
|
||||
width: max-content;
|
||||
height: max-content;
|
||||
|
||||
.tabheaders {
|
||||
height: 38px;
|
||||
}
|
||||
}
|
||||
|
||||
.tabheaders {
|
||||
height: 2.25rem;
|
||||
margin: 0;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -22,6 +22,9 @@
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div v-if="album.help_text" class="rhelp album">
|
||||
{{ album.help_text }}
|
||||
</div>
|
||||
<h4 v-tooltip class="title ellip">
|
||||
{{ album.title }}
|
||||
</h4>
|
||||
@@ -38,30 +41,6 @@
|
||||
{{ `${artists[0].name}` }}
|
||||
</RouterLink>
|
||||
</div>
|
||||
|
||||
<!-- when showing other versions -->
|
||||
<!-- <div
|
||||
v-if="
|
||||
album.versions.length === 0 &&
|
||||
hide_artists &&
|
||||
$route.name === Routes.album
|
||||
"
|
||||
class="artist ellip"
|
||||
>
|
||||
{{ album.date }}
|
||||
{{
|
||||
album.albumartists.length > 1
|
||||
? ` • ${album.albumartists[1].name}`
|
||||
: ""
|
||||
}}
|
||||
</div> -->
|
||||
<!-- <div v-else>
|
||||
<div class="artist ellip">
|
||||
{{ album.date }}
|
||||
</div>
|
||||
</div> -->
|
||||
<!-- end -->
|
||||
|
||||
<div v-if="album.versions.length" class="versions">
|
||||
<MasterFlag
|
||||
v-for="v in getVersions(
|
||||
@@ -121,8 +100,6 @@ const artists = computed(() => {
|
||||
|
||||
<style lang="scss">
|
||||
.album-card {
|
||||
flex: 0 0 10.1rem;
|
||||
|
||||
display: grid;
|
||||
gap: $small;
|
||||
padding: $medium;
|
||||
|
||||
@@ -8,17 +8,39 @@
|
||||
}"
|
||||
class="artist-card"
|
||||
>
|
||||
<img class="artist-image circular" :src="imguri + artist.image" />
|
||||
<div class="image circular">
|
||||
<img class="artist-image circular" :src="imguri + artist.image" />
|
||||
<div
|
||||
class="overlay circular"
|
||||
:style="{
|
||||
background: `linear-gradient(to top, ${artist.colors[0]} 20%, transparent)`,
|
||||
}"
|
||||
></div>
|
||||
<PlayBtn
|
||||
:artisthash="artist.artisthash"
|
||||
:artistname="artist.name"
|
||||
:source="playSources.artist"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="artist.help_text" class="rhelp t-center">
|
||||
{{ artist.help_text }}
|
||||
</div>
|
||||
<div class="artist-name t-center">
|
||||
{{ artist.name }}
|
||||
</div>
|
||||
<div v-if="artist.help_text && artist.trackcount" class="racount t-center">
|
||||
{{ artist.trackcount }} Track{{ artist.trackcount == 1 ? "" : "s" }}
|
||||
</div>
|
||||
</RouterLink>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Artist } from "@/interfaces";
|
||||
import { Routes } from "@/router";
|
||||
import { paths } from "../../config";
|
||||
import { paths } from "@/config";
|
||||
|
||||
import PlayBtn from "./PlayBtn.vue";
|
||||
import { playSources } from "@/enums";
|
||||
|
||||
const imguri = paths.images.artist.large;
|
||||
|
||||
@@ -29,32 +51,67 @@ defineProps<{
|
||||
|
||||
<style lang="scss">
|
||||
.artist-card {
|
||||
flex: 0 0 10.1rem;
|
||||
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
||||
border-radius: $medium;
|
||||
display: grid;
|
||||
gap: $small;
|
||||
justify-content: center;
|
||||
padding: 1.2rem 1rem !important;
|
||||
font-size: 0.9rem;
|
||||
font-weight: bolder;
|
||||
height: max-content;
|
||||
|
||||
.image {
|
||||
position: relative;
|
||||
|
||||
.overlay {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: calc(100% - $small + 1px);
|
||||
top: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
$btnwidth: 4rem;
|
||||
|
||||
.play-btn {
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
width: 4rem;
|
||||
bottom: 0;
|
||||
left: calc(50% - ($btnwidth / 2));
|
||||
transition: all 0.25s;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: $gray4;
|
||||
|
||||
.play-btn {
|
||||
opacity: 1;
|
||||
transform: translateY(-1.25rem);
|
||||
}
|
||||
|
||||
.overlay {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.artist-image {
|
||||
width: 100%;
|
||||
transition: all 0.5s ease-in-out;
|
||||
object-fit: cover;
|
||||
margin-bottom: $smaller;
|
||||
}
|
||||
|
||||
.artist-name {
|
||||
margin-bottom: $smaller;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.racount {
|
||||
font-size: 12px;
|
||||
color: #ffffffbf;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -8,10 +8,10 @@
|
||||
}"
|
||||
@click.stop="() => {}"
|
||||
>
|
||||
<div v-tooltip v-if="artists === null || artists.length === 0">
|
||||
<div v-if="artists === null || artists.length === 0" v-tooltip>
|
||||
<span class="artist">{{ albumartists }}</span>
|
||||
</div>
|
||||
<div v-tooltip v-else>
|
||||
<div v-else v-tooltip>
|
||||
<template v-for="(artist, index) in artists" :key="index">
|
||||
<RouterLink
|
||||
class="artist"
|
||||
@@ -21,7 +21,7 @@
|
||||
}"
|
||||
>{{ `${artist.name}` }}</RouterLink
|
||||
>
|
||||
<span class="artist" v-if="index < artists.length - 1"
|
||||
<span v-if="index < artists.length - 1" class="artist"
|
||||
>,
|
||||
</span> </template
|
||||
>
|
||||
|
||||
38
src/components/shared/CardRow.vue
Normal file
38
src/components/shared/CardRow.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<div v-if="type == 'album'" class="cardlistrow">
|
||||
<AlbumCard
|
||||
v-for="item in items"
|
||||
:key="item.albumhash"
|
||||
class="hlistitem"
|
||||
:album="(item as Album)"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="type == 'artist'" class="cardlistrow">
|
||||
<ArtistCard
|
||||
v-for="item in items"
|
||||
:key="item.artisthash"
|
||||
class="hlistitem"
|
||||
:artist="(item as Artist)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Album, Artist } from "@/interfaces";
|
||||
import AlbumCard from "./AlbumCard.vue";
|
||||
import ArtistCard from "./ArtistCard.vue";
|
||||
|
||||
defineProps<{
|
||||
type: "album" | "artist";
|
||||
items: Album[] | Artist[];
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.cardlistrow {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(10.1rem, 1fr));
|
||||
padding-bottom: 2rem;
|
||||
z-index: -1;
|
||||
}
|
||||
</style>
|
||||
139
src/components/shared/CardScroller.vue
Normal file
139
src/components/shared/CardScroller.vue
Normal file
@@ -0,0 +1,139 @@
|
||||
<template>
|
||||
<div class="cardscroller">
|
||||
<div class="rinfo">
|
||||
<div class="rtitle">
|
||||
<b>{{ title }}</b>
|
||||
<SeeAll v-if="route && items.length >= maxAbumCards" :route="route" />
|
||||
</div>
|
||||
<div v-if="description" class="rdesc">{{ description }}</div>
|
||||
</div>
|
||||
<div class="recentitems">
|
||||
<component
|
||||
:is="getComponent(i.type)"
|
||||
v-for="(i, index) in items.slice(0, maxAbumCards)"
|
||||
:key="index"
|
||||
class="hlistitem"
|
||||
v-bind="getProps(i)"
|
||||
@playThis="() => $emit('playThis', index)"
|
||||
></component>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { playSources } from "@/enums";
|
||||
import { maxAbumCards } from "@/stores/content-width";
|
||||
|
||||
import TrackCard from "./TrackCard.vue";
|
||||
import SeeAll from "../shared/SeeAll.vue";
|
||||
import FolderCard from "./FolderCard.vue";
|
||||
import AlbumCard from "./AlbumCard.vue";
|
||||
import ArtistCard from "./ArtistCard.vue";
|
||||
import PlaylistCard from "../PlaylistsList/PlaylistCard.vue";
|
||||
import FavoritesCard from "./FavoritesCard.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
title: string;
|
||||
description?: string;
|
||||
items: {
|
||||
type: string;
|
||||
item: any;
|
||||
}[];
|
||||
playSource?: playSources;
|
||||
child_props?: any;
|
||||
route?: string;
|
||||
}>();
|
||||
|
||||
defineEmits<{
|
||||
playThis: (index: number) => void;
|
||||
}>();
|
||||
|
||||
function getComponent(type: string) {
|
||||
switch (type) {
|
||||
case "album":
|
||||
return AlbumCard;
|
||||
case "track":
|
||||
return TrackCard;
|
||||
case "artist":
|
||||
return ArtistCard;
|
||||
case "folder":
|
||||
return FolderCard;
|
||||
case "playlist":
|
||||
return PlaylistCard;
|
||||
case "favorite_tracks":
|
||||
return FavoritesCard;
|
||||
}
|
||||
}
|
||||
|
||||
function getProps(item: { type: string; item: any }) {
|
||||
switch (item.type) {
|
||||
case "album":
|
||||
return {
|
||||
album: item.item,
|
||||
...props.child_props,
|
||||
};
|
||||
case "track":
|
||||
return {
|
||||
track: item.item,
|
||||
playSource: props.playSource,
|
||||
};
|
||||
case "artist":
|
||||
return {
|
||||
artist: item.item,
|
||||
};
|
||||
case "folder":
|
||||
return {
|
||||
folder: item.item,
|
||||
};
|
||||
case "playlist":
|
||||
return {
|
||||
playlist: item.item,
|
||||
};
|
||||
case "favorite_tracks":
|
||||
return {
|
||||
item: item.item,
|
||||
};
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.cardscroller {
|
||||
padding: 2rem 0;
|
||||
|
||||
.recentitems {
|
||||
gap: 1.5rem 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(10.1rem, 1fr));
|
||||
}
|
||||
|
||||
.p-card {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.rinfo {
|
||||
padding: 0 $medium;
|
||||
margin-bottom: $medium;
|
||||
|
||||
.rtitle {
|
||||
font-size: 1.15rem;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.rdesc {
|
||||
font-size: 0.9rem;
|
||||
color: rgba(255, 255, 255, 0.747);
|
||||
}
|
||||
}
|
||||
|
||||
.to_playlist {
|
||||
background-color: #fff;
|
||||
color: #000;
|
||||
border: 1px solid #fff;
|
||||
padding: 1.25rem 2rem;
|
||||
margin: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
111
src/components/shared/DropDown.vue
Normal file
111
src/components/shared/DropDown.vue
Normal file
@@ -0,0 +1,111 @@
|
||||
<template>
|
||||
<div class="smdropdown buttons" :class="component_key">
|
||||
<div v-auto-animate="{ duration: 10 }" class="select rounded-sm">
|
||||
<button class="selected" @click.prevent="handleOpener">
|
||||
<span class="ellip">{{ current }}</span>
|
||||
<ArrowSvg />
|
||||
</button>
|
||||
<div
|
||||
v-if="showDropDown"
|
||||
ref="dropOptionsRef"
|
||||
class="options rounded-sm shadow-lg"
|
||||
>
|
||||
<div
|
||||
v-for="a in items"
|
||||
:key="a"
|
||||
class="option"
|
||||
:class="{ current: current == a }"
|
||||
@click.prevent="handleClick(a)"
|
||||
>
|
||||
{{ a }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { Ref, ref } from "vue";
|
||||
import { onClickOutside } from "@vueuse/core";
|
||||
|
||||
import ArrowSvg from "@/assets/icons/expand.svg";
|
||||
|
||||
const showDropDown = ref(false);
|
||||
const dropOptionsRef: Ref<HTMLElement | undefined> = ref();
|
||||
|
||||
defineProps<{
|
||||
items: any[];
|
||||
current: any;
|
||||
component_key: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: "itemClicked", item: any): void;
|
||||
}>();
|
||||
|
||||
const handleOpener = () => {
|
||||
showDropDown.value = !showDropDown.value;
|
||||
};
|
||||
|
||||
const handleClick = (item: any) => {
|
||||
emit("itemClicked", item);
|
||||
showDropDown.value = false;
|
||||
};
|
||||
|
||||
onClickOutside(dropOptionsRef, (e) => {
|
||||
// @ts-ignore
|
||||
e.stopImmediatePropagation();
|
||||
showDropDown.value = false;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.smdropdown {
|
||||
.selected {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 2rem;
|
||||
gap: $smaller;
|
||||
width: 100%;
|
||||
padding-right: 0;
|
||||
|
||||
svg {
|
||||
transform: rotate(90deg) scale(0.9);
|
||||
}
|
||||
}
|
||||
|
||||
.select {
|
||||
position: relative;
|
||||
width: 8rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: calc($medium + 2px);
|
||||
z-index: 10;
|
||||
|
||||
.options {
|
||||
background-color: $gray;
|
||||
position: absolute;
|
||||
top: 120%;
|
||||
padding: $small $smaller;
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.option {
|
||||
padding: $small;
|
||||
width: 7.5rem;
|
||||
border-radius: $smaller;
|
||||
margin: 0 $smaller;
|
||||
|
||||
&:hover {
|
||||
background-color: $darkestblue;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.current {
|
||||
background-color: $gray5;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
79
src/components/shared/FavoritesCard.vue
Normal file
79
src/components/shared/FavoritesCard.vue
Normal file
@@ -0,0 +1,79 @@
|
||||
<template>
|
||||
<RouterLink
|
||||
:to="{ name: Routes.favoriteTracks }"
|
||||
class="favoritescard rounded"
|
||||
>
|
||||
<div class="img">
|
||||
<svg
|
||||
width="100"
|
||||
height="100"
|
||||
viewBox="0 0 28 28"
|
||||
fill="#ff453a"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M13.9912 22.1445C14.2197 22.1445 14.5449 21.9775 14.8086 21.8105C19.7217 18.6465 22.8682 14.9375 22.8682 11.1758C22.8682 7.9502 20.6445 5.7002 17.8408 5.7002C16.0918 5.7002 14.7822 6.66699 13.9912 8.11719C13.2178 6.67578 11.8994 5.7002 10.1504 5.7002C7.34668 5.7002 5.11426 7.9502 5.11426 11.1758C5.11426 14.9375 8.26074 18.6465 13.1738 21.8105C13.4463 21.9775 13.7715 22.1445 13.9912 22.1445Z"
|
||||
/>
|
||||
</svg>
|
||||
<PlayBtn :source="playSources.favorite" />
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="rhelp playlist">PLAYLIST</div>
|
||||
<div class="title">Favorite Tracks</div>
|
||||
<div class="fcount">
|
||||
<b>{{ item.count + ` Track${item.count == 1 ? "" : "s"}` }}</b>
|
||||
</div>
|
||||
</div>
|
||||
</RouterLink>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Routes } from "@/router";
|
||||
import PlayBtn from "../shared/PlayBtn.vue";
|
||||
import { playSources } from "@/enums";
|
||||
defineProps<{
|
||||
item: any;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.favoritescard {
|
||||
padding: $medium;
|
||||
|
||||
.img {
|
||||
width: 100%;
|
||||
aspect-ratio: 1/1;
|
||||
background-color: $gray5;
|
||||
border-radius: $small;
|
||||
margin-bottom: $medium;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-image: linear-gradient(37deg, $gray5, $gray, $gray);
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.play-btn {
|
||||
position: absolute;
|
||||
width: 4rem;
|
||||
bottom: 0;
|
||||
opacity: 0;
|
||||
transition: all 0.25s;
|
||||
}
|
||||
|
||||
.fcount {
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.75;
|
||||
padding-top: 2px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: $gray4;
|
||||
|
||||
.play-btn {
|
||||
opacity: 1;
|
||||
transform: translateY(-1rem);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
108
src/components/shared/FolderCard.vue
Normal file
108
src/components/shared/FolderCard.vue
Normal file
@@ -0,0 +1,108 @@
|
||||
<template>
|
||||
<RouterLink
|
||||
:to="{
|
||||
name: Routes.folder,
|
||||
params: {
|
||||
path: folder.path,
|
||||
},
|
||||
}"
|
||||
class="foldercard rounded"
|
||||
>
|
||||
<div class="rimg rounded-sm">
|
||||
<svg
|
||||
width="30"
|
||||
height="30"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 28 28"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="bg"
|
||||
>
|
||||
<path
|
||||
d="M7 5.6875C5.55231 5.6875 4.375 6.86481 4.375 8.3125V19.6875C4.375 21.1352 5.55231 22.3125 7 22.3125H21C22.4477 22.3125 23.625 21.1352 23.625 19.6875V10.0625C23.625 8.61481 22.4477 7.4375 21 7.4375H12.7328C12.3369 7.4375 11.9492 7.30146 11.6407 7.05383L10.8914 6.45483C10.2732 5.96046 9.49659 5.6875 8.70471 5.6875H7ZM7 7.4375H8.70471C9.10065 7.4375 9.48873 7.57354 9.79761 7.82117L10.5461 8.42017C11.1643 8.91454 11.9409 9.1875 12.7328 9.1875H21C21.4826 9.1875 21.875 9.57994 21.875 10.0625V10.5H6.125V8.3125C6.125 7.82994 6.51744 7.4375 7 7.4375ZM6.125 12.25H21.875V19.6875C21.875 20.1701 21.4826 20.5625 21 20.5625H7C6.51744 20.5625 6.125 20.1701 6.125 19.6875V12.25ZM15.8705 13.3634L13.8214 13.7787C13.6705 13.8093 13.5625 13.9347 13.5625 14.0795V17.1086C13.5625 17.2556 13.4371 17.3717 13.025 17.4513C12.3867 17.5751 11.8125 17.8221 11.8125 18.5903C11.8125 18.9701 12.1575 19.4551 13.025 19.4551C13.7806 19.4551 14.4375 18.8381 14.4375 17.9631V15.6073C14.4375 15.5106 14.509 15.4271 14.6101 15.4065L15.9286 15.1382C16.0795 15.1076 16.1875 14.9822 16.1875 14.8374V13.6035C16.1875 13.4469 16.0337 13.3302 15.8705 13.3634Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
<PlayBtn :source="playSources.folder" :folderpath="folder.path" />
|
||||
</div>
|
||||
|
||||
<div v-if="folder.help_text" class="rhelp folder">
|
||||
{{ folder.help_text }}
|
||||
</div>
|
||||
<div class="ellip" :title="name(folder.path)">
|
||||
{{ name(folder.path) }}
|
||||
</div>
|
||||
<div class="rtcount">
|
||||
<b>{{ folder.count }} Track{{ folder.count == 1 ? "" : "s" }}</b>
|
||||
</div>
|
||||
</RouterLink>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Routes } from "@/router";
|
||||
import PlayBtn from "../shared/PlayBtn.vue";
|
||||
import { playSources } from "@/enums";
|
||||
|
||||
defineProps<{
|
||||
folder: {
|
||||
path: string;
|
||||
count: number;
|
||||
help_text: string;
|
||||
};
|
||||
}>();
|
||||
|
||||
const name = (path: string) => {
|
||||
const splits = path.split("/");
|
||||
|
||||
return splits[splits.length - 1];
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.foldercard {
|
||||
padding: $medium;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: max-content;
|
||||
|
||||
.play-btn {
|
||||
position: absolute;
|
||||
width: 4rem;
|
||||
bottom: 0;
|
||||
opacity: 0;
|
||||
transition: all 0.25s;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: $gray4;
|
||||
|
||||
.play-btn {
|
||||
opacity: 1;
|
||||
transform: translateY(-1rem);
|
||||
}
|
||||
}
|
||||
|
||||
svg.bg {
|
||||
transform: scale(2);
|
||||
color: $gray2;
|
||||
}
|
||||
|
||||
.rimg {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
aspect-ratio: 1;
|
||||
background-color: $gray;
|
||||
background-image: linear-gradient(37deg, $gray5, $gray, $gray);
|
||||
margin-bottom: $small;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.rtcount {
|
||||
font-size: 12px;
|
||||
color: #ffffffbf;
|
||||
margin-top: $smaller;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,11 +1,9 @@
|
||||
<template>
|
||||
<div class="generichead">
|
||||
<div class="left">
|
||||
<h1><slot name="name">Playlists</slot></h1>
|
||||
<h1><slot name="name"></slot></h1>
|
||||
<div class="desc">
|
||||
<slot name="description">
|
||||
<div>All your playlists, all in one place.</div>
|
||||
</slot>
|
||||
<slot name="description"></slot>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right">
|
||||
@@ -18,8 +16,7 @@
|
||||
|
||||
<style lang="scss">
|
||||
.generichead {
|
||||
margin: 3rem 0;
|
||||
padding: 0 $smaller;
|
||||
padding: 2rem 0 1rem $small;
|
||||
height: max-content;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr max-content;
|
||||
|
||||
60
src/components/shared/GenericTabs.vue
Normal file
60
src/components/shared/GenericTabs.vue
Normal file
@@ -0,0 +1,60 @@
|
||||
<template>
|
||||
<div class="generictabs">
|
||||
<RouterLink
|
||||
v-for="(item, index) in items"
|
||||
:key="index"
|
||||
class="tab"
|
||||
:class="{ active: active(item) }"
|
||||
:to="{
|
||||
name: route,
|
||||
params: item.params,
|
||||
replace: true,
|
||||
query: item.query,
|
||||
}"
|
||||
>
|
||||
{{ item.title }}
|
||||
<div class="indicator"></div>
|
||||
</RouterLink>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
items: { title: string; params: any, query?: any }[];
|
||||
active: (item: any) => boolean;
|
||||
route: string;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.generictabs {
|
||||
display: flex;
|
||||
border-bottom: solid 1px $gray;
|
||||
|
||||
.tab {
|
||||
padding: $medium;
|
||||
position: relative;
|
||||
color: $gray1;
|
||||
}
|
||||
|
||||
.indicator {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background-color: $white;
|
||||
height: 3px;
|
||||
border-radius: 1rem;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
color: $white;
|
||||
|
||||
.indicator {
|
||||
width: 3rem;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,42 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
class="loaderx"
|
||||
:class="{ loader: loader.loading, not_loader: !loader.loading }"
|
||||
>
|
||||
<div v-if="!loader.loading">🦋</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import useLoaderStore from "@/stores/loader";
|
||||
|
||||
const loader = useLoaderStore();
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.loaderx {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
border-radius: 50%;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.loader {
|
||||
border: dotted $blue;
|
||||
animation: spin 0.25s linear infinite;
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.not_loader {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
</style>
|
||||
41
src/components/shared/LyricsButton.vue
Normal file
41
src/components/shared/LyricsButton.vue
Normal file
@@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<button
|
||||
class="lyrics"
|
||||
:class="{ showStatus: lyrics.exists }"
|
||||
@click="handleClick"
|
||||
>
|
||||
<LyricsSvg /> {{ showText ? "Lyrics" : "" }}
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
|
||||
import { Routes } from "@/router";
|
||||
import useLyrics from "@/stores/lyrics";
|
||||
import LyricsSvg from "@/assets/icons/lyrics.svg";
|
||||
|
||||
defineProps<{
|
||||
showText?: boolean;
|
||||
}>();
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const lyrics = useLyrics();
|
||||
let prevRoute = ref(route.name);
|
||||
|
||||
function handleClick() {
|
||||
if (route.name === Routes.nowPlaying && route.params.tab === "lyrics") {
|
||||
return router.back();
|
||||
}
|
||||
|
||||
router.push({
|
||||
name: Routes.nowPlaying,
|
||||
params: { tab: "lyrics" },
|
||||
});
|
||||
|
||||
prevRoute.value = route.name;
|
||||
}
|
||||
</script>
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<button id="option-drop" @click.stop.prevent="showDropdown" class="btn-more">
|
||||
<button id="option-drop" class="btn-more" @click.stop.prevent="showDropdown">
|
||||
<MoreSvg />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
@@ -7,10 +7,17 @@
|
||||
<script setup lang="ts">
|
||||
import { Track } from "@/interfaces";
|
||||
import { playSources } from "@/enums";
|
||||
import { playFromAlbumCard, playFromArtistCard } from "@/helpers/usePlayFrom";
|
||||
import {
|
||||
playFromAlbumCard,
|
||||
playFromArtistCard,
|
||||
playFromFavorites,
|
||||
playFromFolderCard,
|
||||
playFromPlaylist,
|
||||
} from "@/helpers/usePlayFrom";
|
||||
|
||||
import PlaySvg from "@/assets/icons/play.svg";
|
||||
import useQueueStore from "@/stores/queue";
|
||||
import useQueue from "@/stores/queue";
|
||||
import useTracklist from "@/stores/queue/tracklist";
|
||||
import useSearchStore from "@/stores/search";
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -19,6 +26,7 @@ const props = defineProps<{
|
||||
albumName?: string;
|
||||
artisthash?: string;
|
||||
artistname?: string;
|
||||
folderpath?: string;
|
||||
track?: Track;
|
||||
}>();
|
||||
|
||||
@@ -31,19 +39,28 @@ function handlePlay() {
|
||||
case playSources.artist:
|
||||
playFromArtistCard(props.artisthash || "", props.artistname || "");
|
||||
break;
|
||||
|
||||
case playSources.folder:
|
||||
playFromFolderCard(props.folderpath || "");
|
||||
break;
|
||||
case playSources.recentlyAdded:
|
||||
playFromPlaylist("recentlyadded", props.track);
|
||||
break;
|
||||
case playSources.track: {
|
||||
// insert after current and play
|
||||
if (!props.track) break;
|
||||
|
||||
const queue = useQueueStore();
|
||||
const queue = useQueue();
|
||||
const search = useSearchStore();
|
||||
const tracklist = useTracklist();
|
||||
|
||||
queue.clearQueue();
|
||||
queue.playFromSearch(search.query, [props.track]);
|
||||
tracklist.setFromSearch(search.query, [props.track]);
|
||||
queue.play();
|
||||
break;
|
||||
}
|
||||
case playSources.favorite:
|
||||
playFromFavorites(props.track);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
<button
|
||||
v-wave
|
||||
class="playbtnrect shadow-sm circular btn-active"
|
||||
@click="playFrom(source)"
|
||||
:style="{
|
||||
backgroundColor: bg_color ? bg_color : '',
|
||||
borderColor: bg_color ? bg_color : '',
|
||||
color: bg_color ? getShift(bg_color, [100, 100]) : '',
|
||||
}"
|
||||
@click="playFrom(source)"
|
||||
>
|
||||
<playBtnSvg />
|
||||
<div class="text">Play</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<span class="see-all">
|
||||
<RouterLink :to="route"> SEE ALL </RouterLink>
|
||||
<RouterLink :to="route"> <b>SEE ALL</b> </RouterLink>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<router-link
|
||||
v-if="!hide_album"
|
||||
class="song-album ellip"
|
||||
v-tooltip
|
||||
class="song-album ellip"
|
||||
:to="{
|
||||
name: 'AlbumView',
|
||||
params: {
|
||||
|
||||
@@ -5,23 +5,23 @@
|
||||
style="height: 100%"
|
||||
>
|
||||
<RecycleScroller
|
||||
class="scroller"
|
||||
id="songlist-scroller"
|
||||
v-slot="{ item, index }"
|
||||
class="scroller"
|
||||
style="height: 100%"
|
||||
:items="tracks.map((track, index) => ({ track, id: index }))"
|
||||
:item-size="itemHeight"
|
||||
key-field="id"
|
||||
v-slot="{ item, index }"
|
||||
>
|
||||
<SongItem
|
||||
:track="item.track"
|
||||
:index="index + 1"
|
||||
:is_queue_track="is_queue"
|
||||
@playThis="handlePlay(index)"
|
||||
:is_last="index == tracks.length - 1"
|
||||
:droppable="false"
|
||||
@trackDropped="dropHandler"
|
||||
:source="source"
|
||||
@playThis="handlePlay(index)"
|
||||
@trackDropped="dropHandler"
|
||||
/>
|
||||
</RecycleScroller>
|
||||
</div>
|
||||
|
||||
81
src/components/shared/TrackCard.vue
Normal file
81
src/components/shared/TrackCard.vue
Normal file
@@ -0,0 +1,81 @@
|
||||
<template>
|
||||
<div class="trackcard rounded">
|
||||
<div class="image">
|
||||
<img class="rounded-sm" :src="paths.images.thumb.large + track.image" />
|
||||
<PlayBtn :source="playSource" :track="track" />
|
||||
</div>
|
||||
<div class="tinfo">
|
||||
<div v-if="track.help_text" class="rhelp track">
|
||||
{{ track.help_text }}
|
||||
</div>
|
||||
<div class="ttitle ellip">{{ track.title }}</div>
|
||||
<ArtistName :albumartists="track.albumartists" :artists="track.artists" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { paths } from "@/config";
|
||||
import { Track } from "@/interfaces";
|
||||
import { playSources } from "@/enums";
|
||||
|
||||
import PlayBtn from "../shared/PlayBtn.vue";
|
||||
import ArtistName from "../shared/ArtistName.vue";
|
||||
|
||||
defineProps<{
|
||||
track: Track;
|
||||
playSource: playSources;
|
||||
}>();
|
||||
|
||||
defineEmits<{
|
||||
playThis: (index: number) => void;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.trackcard {
|
||||
padding: $medium;
|
||||
cursor: pointer;
|
||||
height: max-content;
|
||||
|
||||
.image {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
$btnwidth: 4rem;
|
||||
|
||||
.play-btn {
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
width: 4rem;
|
||||
bottom: $small;
|
||||
left: calc(50% - ($btnwidth / 2));
|
||||
transition: all 0.25s;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: $gray4;
|
||||
|
||||
.play-btn {
|
||||
opacity: 1;
|
||||
transform: translateY(-0.75rem);
|
||||
}
|
||||
}
|
||||
|
||||
.ttitle {
|
||||
font-size: 0.9rem;
|
||||
margin-top: $small;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.artist {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 700;
|
||||
opacity: 0.86;
|
||||
opacity: 0.75;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,31 +1,18 @@
|
||||
import { useStorage } from "@vueuse/core";
|
||||
|
||||
const development = import.meta.env.DEV;
|
||||
|
||||
function getBaseUrl() {
|
||||
const base_url = window.location.origin;
|
||||
|
||||
if (!development) {
|
||||
return base_url;
|
||||
}
|
||||
|
||||
const splits = base_url.split(":");
|
||||
return base_url.replace(splits[splits.length - 1], "1980");
|
||||
}
|
||||
|
||||
const dev_url = getBaseUrl();
|
||||
const url = development ? dev_url : "";
|
||||
|
||||
export const baseApiUrl = useStorage("baseApiUrl", url, sessionStorage);
|
||||
|
||||
const hostname = "swingmusic.netlify.app";
|
||||
|
||||
if (window.location.hostname === hostname && baseApiUrl.value === "") {
|
||||
// is running on netlify and baseApiUrl is not set
|
||||
baseApiUrl.value = null;
|
||||
}
|
||||
|
||||
const baseImgUrl = baseApiUrl.value + "/img";
|
||||
|
||||
export function setBaseApiUrl(url: string) {
|
||||
baseApiUrl.value = url;
|
||||
location.reload();
|
||||
}
|
||||
const base_url = getBaseUrl();
|
||||
const baseImgUrl = base_url + "/img";
|
||||
|
||||
const imageRoutes = {
|
||||
thumb: {
|
||||
@@ -43,14 +30,16 @@ const imageRoutes = {
|
||||
|
||||
export const paths = {
|
||||
api: {
|
||||
album: baseApiUrl.value + "/album",
|
||||
favorite: baseApiUrl.value + "/favorite",
|
||||
favorites: baseApiUrl.value + "/favorites",
|
||||
favAlbums: baseApiUrl.value + "/albums/favorite",
|
||||
favTracks: baseApiUrl.value + "/tracks/favorite",
|
||||
favArtists: baseApiUrl.value + "/artists/favorite",
|
||||
isFavorite: baseApiUrl.value + "/favorites/check",
|
||||
artist: baseApiUrl.value + "/artist",
|
||||
album: base_url + "/album",
|
||||
favorite: base_url + "/favorite",
|
||||
favorites: base_url + "/favorites",
|
||||
favAlbums: base_url + "/albums/favorite",
|
||||
favTracks: base_url + "/tracks/favorite",
|
||||
favArtists: base_url + "/artists/favorite",
|
||||
isFavorite: base_url + "/favorites/check",
|
||||
artist: base_url + "/artist",
|
||||
lyrics: base_url + "/lyrics",
|
||||
plugins: base_url + "/plugins",
|
||||
get addFavorite() {
|
||||
return this.favorite + "/add";
|
||||
},
|
||||
@@ -70,12 +59,12 @@ export const paths = {
|
||||
return this.album + "/versions";
|
||||
},
|
||||
folder: {
|
||||
base: baseApiUrl.value + "/folder",
|
||||
showInFiles: baseApiUrl.value + "/folder/show-in-files",
|
||||
base: base_url + "/folder",
|
||||
showInFiles: base_url + "/folder/show-in-files",
|
||||
},
|
||||
dir_browser: baseApiUrl.value + "/folder/dir-browser",
|
||||
dir_browser: base_url + "/folder/dir-browser",
|
||||
playlist: {
|
||||
base: baseApiUrl.value + "/playlist",
|
||||
base: base_url + "/playlist",
|
||||
get new() {
|
||||
return this.base + "/new";
|
||||
},
|
||||
@@ -87,7 +76,7 @@ export const paths = {
|
||||
},
|
||||
},
|
||||
search: {
|
||||
base: baseApiUrl.value + "/search",
|
||||
base: base_url + "/search",
|
||||
get top() {
|
||||
return this.base + "/top?q=";
|
||||
},
|
||||
@@ -104,14 +93,29 @@ export const paths = {
|
||||
return this.base + "/loadmore";
|
||||
},
|
||||
},
|
||||
logger: {
|
||||
base: base_url + "/logger",
|
||||
get logTrack() {
|
||||
return this.base + "/track/log";
|
||||
},
|
||||
},
|
||||
getall: {
|
||||
base: base_url + "/getall",
|
||||
get albums() {
|
||||
return this.base + "/albums";
|
||||
},
|
||||
get artists() {
|
||||
return this.base + "/artists";
|
||||
},
|
||||
},
|
||||
colors: {
|
||||
base: baseApiUrl.value + "/colors",
|
||||
base: base_url + "/colors",
|
||||
get album() {
|
||||
return this.base + "/album";
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
base: baseApiUrl.value + "/settings",
|
||||
base: base_url + "/settings",
|
||||
get get_root_dirs() {
|
||||
return this.base + "/get-root-dirs";
|
||||
},
|
||||
@@ -125,7 +129,16 @@ export const paths = {
|
||||
return this.base + "/trigger-scan";
|
||||
},
|
||||
},
|
||||
files: baseApiUrl.value + "/file",
|
||||
files: base_url + "/file",
|
||||
home: {
|
||||
base: base_url + "/home",
|
||||
get recentlyAdded() {
|
||||
return this.base + "/recents/added";
|
||||
},
|
||||
get recentlyPlayed() {
|
||||
return this.base + "/recents/played";
|
||||
},
|
||||
},
|
||||
},
|
||||
images: {
|
||||
thumb: {
|
||||
@@ -141,5 +154,3 @@ export const paths = {
|
||||
raw: baseImgUrl + imageRoutes.raw,
|
||||
},
|
||||
};
|
||||
|
||||
export const VERSION = "1.3.0";
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import modal from "@/stores/modal";
|
||||
import queue from "@/stores/queue";
|
||||
import album from "@/stores/pages/album";
|
||||
import useModal from "@/stores/modal";
|
||||
import useAlbum from "@/stores/pages/album";
|
||||
import useTracklist from "@/stores/queue/tracklist";
|
||||
|
||||
import { Option, Playlist } from "@/interfaces";
|
||||
import { addAlbumToPlaylist } from "@/requests/playlists";
|
||||
import { getAddToPlaylistOptions, separator } from "./utils";
|
||||
import { getAddToPlaylistOptions, get_find_on_social } from "./utils";
|
||||
import { AddToQueueIcon, PlayNextIcon, PlaylistIcon, PlusIcon } from "@/icons";
|
||||
|
||||
export default async () => {
|
||||
const play_next = <Option>{
|
||||
label: "Play next",
|
||||
action: () => {
|
||||
const tracks = album().tracks.filter(
|
||||
const tracks = useAlbum().tracks.filter(
|
||||
(track) => !track.is_album_disc_number
|
||||
);
|
||||
queue().insertAfterCurrent(tracks);
|
||||
useTracklist().insertAfterCurrent(tracks);
|
||||
},
|
||||
icon: PlayNextIcon,
|
||||
};
|
||||
@@ -22,24 +22,24 @@ export default async () => {
|
||||
const add_to_queue = <Option>{
|
||||
label: "Add to queue",
|
||||
action: () => {
|
||||
const tracks = album().tracks.filter(
|
||||
const tracks = useAlbum().tracks.filter(
|
||||
(track) => !track.is_album_disc_number
|
||||
);
|
||||
queue().addTracksToQueue(tracks);
|
||||
useTracklist().addTracks(tracks);
|
||||
},
|
||||
icon: AddToQueueIcon,
|
||||
};
|
||||
|
||||
// Action for each playlist option
|
||||
const AddToPlaylistAction = (playlist: Playlist) => {
|
||||
const store = album();
|
||||
const store = useAlbum();
|
||||
addAlbumToPlaylist(playlist, store.info.albumhash);
|
||||
};
|
||||
|
||||
const add_to_playlist: Option = {
|
||||
label: "Add to Playlist",
|
||||
children: await getAddToPlaylistOptions(AddToPlaylistAction, {
|
||||
albumhash: album().info.albumhash,
|
||||
albumhash: useAlbum().info.albumhash,
|
||||
}),
|
||||
icon: PlusIcon,
|
||||
};
|
||||
@@ -47,11 +47,12 @@ export default async () => {
|
||||
const save_as_playlist: Option = {
|
||||
label: "Save as Playlist",
|
||||
action: () => {
|
||||
const modal_s = modal();
|
||||
const album_s = album();
|
||||
modal_s.showSaveAlbumAsPlaylistModal(
|
||||
album_s.info.title,
|
||||
album_s.info.albumhash
|
||||
const modal = useModal();
|
||||
const album = useAlbum();
|
||||
|
||||
modal.showSaveAlbumAsPlaylistModal(
|
||||
album.info.title,
|
||||
album.info.albumhash
|
||||
);
|
||||
},
|
||||
icon: PlaylistIcon,
|
||||
@@ -60,8 +61,8 @@ export default async () => {
|
||||
return [
|
||||
play_next,
|
||||
add_to_queue,
|
||||
separator,
|
||||
add_to_playlist,
|
||||
save_as_playlist,
|
||||
get_find_on_social(),
|
||||
];
|
||||
};
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import modal from "@/stores/modal";
|
||||
import queue from "@/stores/queue";
|
||||
import useTracklist from "@/stores/queue/tracklist";
|
||||
|
||||
import { getArtistTracks } from "@/requests/artists";
|
||||
import { addArtistToPlaylist } from "@/requests/playlists";
|
||||
|
||||
import { Option, Playlist } from "@/interfaces";
|
||||
import { getAddToPlaylistOptions, separator } from "./utils";
|
||||
import { getAddToPlaylistOptions, get_find_on_social } from "./utils";
|
||||
import { AddToQueueIcon, PlayNextIcon, PlaylistIcon, PlusIcon } from "@/icons";
|
||||
|
||||
export default async (artisthash: string, artistname: string) => {
|
||||
@@ -13,7 +13,7 @@ export default async (artisthash: string, artistname: string) => {
|
||||
label: "Play next",
|
||||
action: () => {
|
||||
getArtistTracks(artisthash).then((tracks) => {
|
||||
const store = queue();
|
||||
const store = useTracklist();
|
||||
store.insertAfterCurrent(tracks);
|
||||
});
|
||||
},
|
||||
@@ -24,8 +24,8 @@ export default async (artisthash: string, artistname: string) => {
|
||||
label: "Add to queue",
|
||||
action: () => {
|
||||
getArtistTracks(artisthash).then((tracks) => {
|
||||
const store = queue();
|
||||
store.addTracksToQueue(tracks);
|
||||
const store = useTracklist();
|
||||
store.addTracks(tracks);
|
||||
});
|
||||
},
|
||||
icon: AddToQueueIcon,
|
||||
@@ -56,8 +56,8 @@ export default async (artisthash: string, artistname: string) => {
|
||||
return [
|
||||
play_next,
|
||||
add_to_queue,
|
||||
separator,
|
||||
add_to_playlist,
|
||||
save_as_playlist,
|
||||
get_find_on_social("artist"),
|
||||
];
|
||||
};
|
||||
|
||||
@@ -3,16 +3,16 @@ import { ContextSrc } from "@/enums";
|
||||
import { Option, Playlist } from "@/interfaces";
|
||||
import { getTracksInPath } from "@/requests/folders";
|
||||
|
||||
import useQueueStore from "@/stores/queue";
|
||||
import useModalStore from "@/stores/modal";
|
||||
import useSettingsStore from "@/stores/settings";
|
||||
import useModal from "@/stores/modal";
|
||||
import useSettings from "@/stores/settings";
|
||||
import useTracklist from "@/stores/queue/tracklist";
|
||||
|
||||
import { addFolderToPlaylist } from "@/requests/playlists";
|
||||
import { getAddToPlaylistOptions, separator } from "./utils";
|
||||
import { getAddToPlaylistOptions } from "./utils";
|
||||
|
||||
export default async (trigger_src: ContextSrc, path: string) => {
|
||||
const settings = useSettingsStore();
|
||||
const modal = useModalStore();
|
||||
const settings = useSettings();
|
||||
const modal = useModal();
|
||||
|
||||
const getListModeOption = () =>
|
||||
<Option>{
|
||||
@@ -22,16 +22,13 @@ export default async (trigger_src: ContextSrc, path: string) => {
|
||||
};
|
||||
|
||||
// if trigger source is folder nav, show list mode option
|
||||
let items =
|
||||
trigger_src === ContextSrc.FolderNav
|
||||
? [separator, getListModeOption()]
|
||||
: [];
|
||||
let items = trigger_src === ContextSrc.FolderNav ? [getListModeOption()] : [];
|
||||
|
||||
const play_next = <Option>{
|
||||
label: "Play next",
|
||||
action: () => {
|
||||
getTracksInPath(path).then((tracks) => {
|
||||
const store = useQueueStore();
|
||||
const store = useTracklist();
|
||||
store.insertAfterCurrent(tracks);
|
||||
});
|
||||
},
|
||||
@@ -42,8 +39,8 @@ export default async (trigger_src: ContextSrc, path: string) => {
|
||||
label: "Add to Queue",
|
||||
action: () => {
|
||||
getTracksInPath(path).then((tracks) => {
|
||||
const store = useQueueStore();
|
||||
store.addTracksToQueue(tracks);
|
||||
const store = useTracklist();
|
||||
store.addTracks(tracks);
|
||||
});
|
||||
},
|
||||
icon: icons.AddToQueueIcon,
|
||||
@@ -68,12 +65,5 @@ export default async (trigger_src: ContextSrc, path: string) => {
|
||||
icon: icons.PlaylistIcon,
|
||||
};
|
||||
|
||||
return [
|
||||
play_next,
|
||||
add_to_queue,
|
||||
separator,
|
||||
add_to_playlist,
|
||||
save_as_playlist,
|
||||
...items,
|
||||
];
|
||||
return [play_next, add_to_queue, add_to_playlist, save_as_playlist, ...items];
|
||||
};
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { DeleteIcon, PlaylistIcon, PlusIcon } from "@/icons";
|
||||
import { Option, Playlist } from "@/interfaces";
|
||||
import { addTracksToPlaylist } from "@/requests/playlists";
|
||||
import useQueue from "@/stores/queue";
|
||||
import useModalStore from "@/stores/modal";
|
||||
import useQueueStore, { From } from "@/stores/queue";
|
||||
import { getAddToPlaylistOptions, separator } from "./utils";
|
||||
import useTracklist, { From } from "@/stores/queue/tracklist";
|
||||
|
||||
import { FromOptions } from "@/enums";
|
||||
import { Option, Playlist } from "@/interfaces";
|
||||
import { getAddToPlaylistOptions } from "./utils";
|
||||
import { addTracksToPlaylist } from "@/requests/playlists";
|
||||
import { DeleteIcon, PlaylistIcon, PlusIcon } from "@/icons";
|
||||
|
||||
function getQueueName(from: From) {
|
||||
switch (from.type) {
|
||||
@@ -26,12 +28,12 @@ function getQueueName(from: From) {
|
||||
}
|
||||
|
||||
export default async () => {
|
||||
const queue = useQueueStore();
|
||||
const store = useTracklist();
|
||||
|
||||
const clearQueue: Option = {
|
||||
label: "Clear queue",
|
||||
action: () => {
|
||||
useQueueStore().clearQueue();
|
||||
useQueue().clearQueue();
|
||||
},
|
||||
icon: DeleteIcon,
|
||||
};
|
||||
@@ -39,22 +41,22 @@ export default async () => {
|
||||
const saveAsPlaylist: Option = {
|
||||
label: "Save as playlist",
|
||||
action: () => {
|
||||
useModalStore().showSaveQueueAsPlaylistModal(getQueueName(queue.from));
|
||||
useModalStore().showSaveQueueAsPlaylistModal(getQueueName(store.from));
|
||||
},
|
||||
icon: PlaylistIcon,
|
||||
};
|
||||
|
||||
const AddToPlaylistAction = (playlist: Playlist) => {
|
||||
addTracksToPlaylist(playlist, queue.tracklist);
|
||||
addTracksToPlaylist(playlist, store.tracklist);
|
||||
};
|
||||
|
||||
const addToPlaylist: Option = {
|
||||
label: "Add to Playlist",
|
||||
children: await getAddToPlaylistOptions(AddToPlaylistAction, {
|
||||
trackhash: queue.tracklist.map((t) => t.trackhash).join(","),
|
||||
trackhash: store.tracklist.map((t) => t.trackhash).join(","),
|
||||
}),
|
||||
icon: PlusIcon,
|
||||
};
|
||||
|
||||
return [clearQueue, addToPlaylist, separator, saveAsPlaylist];
|
||||
return [clearQueue, addToPlaylist, saveAsPlaylist];
|
||||
};
|
||||
|
||||
@@ -18,7 +18,8 @@ import {
|
||||
} from "@/icons";
|
||||
import usePlaylistStore from "@/stores/pages/playlist";
|
||||
import useQueueStore from "@/stores/queue";
|
||||
import { getAddToPlaylistOptions, separator } from "./utils";
|
||||
import useTracklist from "@/stores/queue/tracklist";
|
||||
import { getAddToPlaylistOptions } from "./utils";
|
||||
|
||||
/**
|
||||
* Returns a list of context menu items for a track.
|
||||
@@ -74,7 +75,7 @@ export default async (
|
||||
label: "Add to Playlist",
|
||||
children: await getAddToPlaylistOptions(AddToPlaylistAction, {
|
||||
trackhash: track.trackhash,
|
||||
playlist_name: track.title + ' Radio',
|
||||
playlist_name: track.title + " Radio",
|
||||
}),
|
||||
icon: PlusIcon,
|
||||
};
|
||||
@@ -82,7 +83,7 @@ export default async (
|
||||
const add_to_q: Option = {
|
||||
label: "Add to Queue",
|
||||
action: () => {
|
||||
useQueueStore().addTrackToQueue(track);
|
||||
useTracklist().addTrack(track);
|
||||
},
|
||||
icon: AddToQueueIcon,
|
||||
};
|
||||
@@ -183,17 +184,12 @@ export default async (
|
||||
const options: Option[] = [
|
||||
play_next,
|
||||
add_to_q,
|
||||
separator,
|
||||
add_to_playlist,
|
||||
separator,
|
||||
go_to_album,
|
||||
go_to_folder,
|
||||
separator,
|
||||
go_to_artist,
|
||||
go_to_alb_artist,
|
||||
separator,
|
||||
open_in_explorer,
|
||||
// separator,
|
||||
// del_track,
|
||||
];
|
||||
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
import modal from "@/stores/modal";
|
||||
import useAlbum from "@/stores/pages/album";
|
||||
import useArtist from "@/stores/pages/artist";
|
||||
|
||||
import { Option, Playlist, Track } from "@/interfaces";
|
||||
import { Option, Playlist } from "@/interfaces";
|
||||
import { getAllPlaylists } from "@/requests/playlists";
|
||||
import { SearchIcon } from "@/icons";
|
||||
|
||||
export const separator: Option = {
|
||||
type: "separator",
|
||||
};
|
||||
|
||||
export function get_new_playlist_option(new_playlist_modal_props: any = {}): Option {
|
||||
export function get_new_playlist_option(
|
||||
new_playlist_modal_props: any = {}
|
||||
): Option {
|
||||
return {
|
||||
label: "New playlist",
|
||||
action: () => {
|
||||
@@ -50,3 +55,86 @@ export async function getAddToPlaylistOptions(
|
||||
|
||||
return [...items, separator, ...playlists];
|
||||
}
|
||||
|
||||
export const get_find_on_social = (page = "album") => {
|
||||
const is_album = page === "album";
|
||||
const getAlbumSearchTerm = () => {
|
||||
const store = useAlbum();
|
||||
|
||||
return `${store.info.title} - ${store.info.albumartists
|
||||
.map((a) => a.name)
|
||||
.join(", ")}`;
|
||||
};
|
||||
const search_term = is_album ? getAlbumSearchTerm() : useArtist().info.name;
|
||||
|
||||
return <Option>{
|
||||
label: "Search on",
|
||||
icon: SearchIcon,
|
||||
children: [
|
||||
{
|
||||
label: "Google",
|
||||
action: () =>
|
||||
window.open(
|
||||
`https://www.google.com/search?q=${search_term}`,
|
||||
"_blank"
|
||||
),
|
||||
},
|
||||
{
|
||||
label: "YouTube",
|
||||
action: () =>
|
||||
window.open(
|
||||
`https://www.youtube.com/results?search_query=${search_term}`,
|
||||
"_blank"
|
||||
),
|
||||
},
|
||||
{
|
||||
label: "Spotify",
|
||||
action: () =>
|
||||
window.open(
|
||||
`https://open.spotify.com/search/${search_term}/${page}s`,
|
||||
"_blank"
|
||||
),
|
||||
},
|
||||
{
|
||||
label: "Tidal",
|
||||
action: () =>
|
||||
window.open(
|
||||
`https://listen.tidal.com/search/${page}s?q=${search_term}`,
|
||||
"_blank"
|
||||
),
|
||||
},
|
||||
{
|
||||
label: "Apple Music",
|
||||
action: () =>
|
||||
window.open(
|
||||
`https://music.apple.com/search?term=${search_term}`,
|
||||
"_blank"
|
||||
),
|
||||
},
|
||||
{
|
||||
label: "Deezer",
|
||||
action: () =>
|
||||
window.open(
|
||||
`https://www.deezer.com/search/${search_term}/${page}`,
|
||||
"_blank"
|
||||
),
|
||||
},
|
||||
{
|
||||
label: "Wikipedia",
|
||||
action: () =>
|
||||
window.open(
|
||||
`https://en.wikipedia.org/wiki/Special:Search?search=${search_term}`,
|
||||
"_blank"
|
||||
),
|
||||
},
|
||||
{
|
||||
label: "Last.fm",
|
||||
action: () =>
|
||||
window.open(
|
||||
`https://www.last.fm/search/${page}s?q=${search_term}`,
|
||||
"_blank"
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
27
src/enums.ts
27
src/enums.ts
@@ -6,6 +6,8 @@ export enum playSources {
|
||||
artist,
|
||||
favorite,
|
||||
track,
|
||||
recentlyAdded,
|
||||
recentlyPlayed,
|
||||
}
|
||||
|
||||
export enum NotifType {
|
||||
@@ -50,14 +52,14 @@ export enum contextChildrenShowMode {
|
||||
hover = "hover",
|
||||
}
|
||||
|
||||
export enum discographyAlbumTypes {
|
||||
all = "All",
|
||||
albums = "Albums",
|
||||
singles = "Singles",
|
||||
eps = "EPs",
|
||||
appearances = "Appearances",
|
||||
compilations = "Compilations",
|
||||
}
|
||||
// rewrite the above as an object
|
||||
export const discographyAlbumTypes = {
|
||||
all: "all",
|
||||
albums: "albums",
|
||||
EPs_and_singles: "EP & singles",
|
||||
appearances: "appearances",
|
||||
compilations: "compilations",
|
||||
};
|
||||
|
||||
export enum favType {
|
||||
artist = "artist",
|
||||
@@ -88,6 +90,13 @@ export enum DbSettingKeys {
|
||||
show_albums_as_singles = "show_albums_as_singles",
|
||||
}
|
||||
|
||||
interface Plugin {
|
||||
active: boolean;
|
||||
description: string;
|
||||
name: string;
|
||||
settings: any;
|
||||
}
|
||||
|
||||
export interface DBSettings {
|
||||
root_dirs: string[];
|
||||
exclude_dirs: string[];
|
||||
@@ -99,4 +108,6 @@ export interface DBSettings {
|
||||
remove_remaster: boolean;
|
||||
merge_albums: boolean;
|
||||
show_albums_as_singles: boolean;
|
||||
plugins: Plugin[];
|
||||
version: string;
|
||||
}
|
||||
|
||||
@@ -12,12 +12,15 @@ import folderContextItems from "@/context_menus/folder";
|
||||
import trackContextItems from "@/context_menus/track";
|
||||
import queueContextItems from "@/context_menus/queue";
|
||||
|
||||
let prev_track = "";
|
||||
let stop_prev_watcher = () => {};
|
||||
|
||||
function flagWatcher(menu: Store, flag: Ref<boolean>) {
|
||||
stop_prev_watcher();
|
||||
|
||||
if (flag.value) {
|
||||
return (flag.value = false);
|
||||
}
|
||||
|
||||
// watch for context menu visibility and reset flag
|
||||
stop_prev_watcher = menu.$subscribe((mutation, state) => {
|
||||
//@ts-ignore
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { addFavorite, removeFavorite } from "@/requests/favorite";
|
||||
import useQueueStore from "@/stores/queue";
|
||||
import useQueue from "@/stores/queue";
|
||||
import useTracklist from "@/stores/queue/tracklist";
|
||||
|
||||
import { favType } from "../enums";
|
||||
import { addFavorite, removeFavorite } from "@/requests/favorite";
|
||||
import tracklist from "@/stores/queue/tracklist";
|
||||
/**
|
||||
* Handles the favorite state of an item.
|
||||
* @param setter The ref to track the is_favorite state
|
||||
@@ -16,7 +19,9 @@ export default async function favoriteHandler(
|
||||
) {
|
||||
if (itemhash == "") return;
|
||||
|
||||
const queue = useQueueStore();
|
||||
const queue = useQueue();
|
||||
const tracklist = useTracklist();
|
||||
|
||||
const is_current =
|
||||
type === favType.track && itemhash === queue.currenttrackhash;
|
||||
if (flag) {
|
||||
@@ -28,6 +33,6 @@ export default async function favoriteHandler(
|
||||
}
|
||||
|
||||
if (is_current) {
|
||||
queue.toggleFav(queue.currentindex);
|
||||
tracklist.toggleFav(queue.currentindex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,41 +1,48 @@
|
||||
import { NotifType, playSources } from "@/enums";
|
||||
import { FromOptions, NotifType, playSources } from "@/enums";
|
||||
|
||||
import { useNotifStore } from "@/stores/notification";
|
||||
import useAStore from "@/stores/pages/album";
|
||||
import useArtistPageStore from "@/stores/pages/artist";
|
||||
import useFStore from "@/stores/pages/folder";
|
||||
import usePStore from "@/stores/pages/playlist";
|
||||
import useQStore from "@/stores/queue";
|
||||
import useQueue from "@/stores/queue";
|
||||
import useAlbum from "@/stores/pages/album";
|
||||
import useArtist from "@/stores/pages/artist";
|
||||
import usePlaylist from "@/stores/pages/playlist";
|
||||
import useSettingsStore from "@/stores/settings";
|
||||
import useTracklist from "@/stores/queue/tracklist";
|
||||
import { useNotifStore } from "@/stores/notification";
|
||||
|
||||
import { getAlbumTracks } from "@/requests/album";
|
||||
import { getArtistTracks } from "@/requests/artists";
|
||||
import { getFiles } from "@/requests/folders";
|
||||
import { getPlaylist } from "@/requests/playlists";
|
||||
import { Track } from "@/interfaces";
|
||||
import { getFavTracks } from "@/requests/favorite";
|
||||
|
||||
export async function utilPlayFromArtist(index: number = 0) {
|
||||
const queue = useQueue();
|
||||
const artist = useArtist();
|
||||
const tracklist = useTracklist();
|
||||
|
||||
export async function utilPlayFromArtist(
|
||||
queue: typeof useQStore,
|
||||
artist: typeof useArtistPageStore,
|
||||
index: number = 0
|
||||
) {
|
||||
const qu = queue();
|
||||
const ar = artist();
|
||||
const settings = useSettingsStore();
|
||||
|
||||
if (ar.tracks.length === 0) return;
|
||||
if (artist.tracks.length === 0) return;
|
||||
|
||||
if (ar.info.trackcount <= settings.artist_top_tracks_count) {
|
||||
qu.playFromArtist(ar.info.artisthash, ar.info.name, ar.tracks);
|
||||
qu.play();
|
||||
if (artist.info.trackcount <= settings.artist_top_tracks_count) {
|
||||
tracklist.setFromArtist(
|
||||
artist.info.artisthash,
|
||||
artist.info.name,
|
||||
artist.tracks
|
||||
);
|
||||
queue.play();
|
||||
return;
|
||||
}
|
||||
|
||||
const tracks = await getArtistTracks(ar.info.artisthash);
|
||||
const tracks = await getArtistTracks(artist.info.artisthash);
|
||||
|
||||
qu.playFromArtist(ar.info.artisthash, ar.info.name, tracks);
|
||||
qu.play(index);
|
||||
tracklist.setFromArtist(artist.info.artisthash, artist.info.name, tracks);
|
||||
queue.play(index);
|
||||
}
|
||||
|
||||
export async function playFromAlbumCard(albumhash: string, albumname: string) {
|
||||
const qu = useQStore();
|
||||
const queue = useQueue();
|
||||
const tracklist = useTracklist();
|
||||
|
||||
const tracks = await getAlbumTracks(albumhash);
|
||||
|
||||
@@ -44,15 +51,16 @@ export async function playFromAlbumCard(albumhash: string, albumname: string) {
|
||||
return;
|
||||
}
|
||||
|
||||
qu.playFromAlbum(albumname, albumhash, tracks);
|
||||
qu.play();
|
||||
tracklist.setFromAlbum(albumname, albumhash, tracks);
|
||||
queue.play();
|
||||
}
|
||||
|
||||
export async function playFromArtistCard(
|
||||
artisthash: string,
|
||||
artistname: string
|
||||
) {
|
||||
const queue = useQStore();
|
||||
const queue = useQueue();
|
||||
const tracklist = useTracklist();
|
||||
const tracks = await getArtistTracks(artisthash);
|
||||
|
||||
if (tracks.length === 0) {
|
||||
@@ -63,34 +71,100 @@ export async function playFromArtistCard(
|
||||
return;
|
||||
}
|
||||
|
||||
queue.playFromArtist(artisthash, artistname, tracks);
|
||||
tracklist.setFromArtist(artisthash, artistname, tracks);
|
||||
queue.play();
|
||||
}
|
||||
|
||||
export async function playFromFolderCard(folderpath: string) {
|
||||
const queue = useQueue();
|
||||
const tracklist = useTracklist();
|
||||
|
||||
const data = await getFiles(folderpath, true);
|
||||
const tracks = data.tracks;
|
||||
|
||||
if (tracks.length === 0) {
|
||||
useNotifStore().showNotification(
|
||||
"Folder tracks not found",
|
||||
NotifType.Error
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
tracklist.setFromFolder(folderpath, tracks);
|
||||
queue.play();
|
||||
}
|
||||
|
||||
export async function playFromFavorites(track: Track | undefined) {
|
||||
const queue = useQueue();
|
||||
const tracklist = useTracklist();
|
||||
|
||||
if (tracklist.from.type !== FromOptions.favorite) {
|
||||
const tracks = await getFavTracks(0);
|
||||
tracklist.setFromFav(tracks);
|
||||
}
|
||||
|
||||
let index = 0;
|
||||
|
||||
if (track) {
|
||||
index = tracklist.tracklist.findIndex(
|
||||
(t) => t.trackhash === track?.trackhash
|
||||
);
|
||||
}
|
||||
|
||||
queue.play(index);
|
||||
}
|
||||
|
||||
export async function playFromPlaylist(id: string, track?: Track) {
|
||||
const queue = useQueue();
|
||||
const tracklist = useTracklist();
|
||||
const data = await getPlaylist(id);
|
||||
|
||||
if (!data) return;
|
||||
|
||||
const { tracks, info } = data;
|
||||
|
||||
tracklist.setFromPlaylist(info.name, info.id, tracks);
|
||||
|
||||
if (track) {
|
||||
const index = tracks.findIndex((t) => t.trackhash === track.trackhash);
|
||||
queue.play(index);
|
||||
} else {
|
||||
queue.play();
|
||||
}
|
||||
}
|
||||
|
||||
export const playFrom = async (source: playSources) => {
|
||||
const useQueue = useQStore();
|
||||
const queue = useQueue();
|
||||
const tracklist = useTracklist();
|
||||
|
||||
switch (source) {
|
||||
case playSources.album:
|
||||
const a_store = useAStore();
|
||||
case playSources.album: {
|
||||
const album = useAlbum();
|
||||
|
||||
useQueue.playFromAlbum(
|
||||
a_store.info.title,
|
||||
a_store.info.albumhash,
|
||||
a_store.srcTracks
|
||||
tracklist.setFromAlbum(
|
||||
album.info.title,
|
||||
album.info.albumhash,
|
||||
album.srcTracks
|
||||
);
|
||||
useQueue.play();
|
||||
queue.play();
|
||||
break;
|
||||
case playSources.playlist:
|
||||
const p = usePStore();
|
||||
}
|
||||
|
||||
if (p.tracks.length === 0) return;
|
||||
case playSources.playlist: {
|
||||
const playlist = usePlaylist();
|
||||
|
||||
useQueue.playFromPlaylist(p.info.name, p.info.id, p.tracks);
|
||||
useQueue.play();
|
||||
if (playlist.tracks.length === 0) return;
|
||||
|
||||
tracklist.setFromPlaylist(
|
||||
playlist.info.name,
|
||||
playlist.info.id,
|
||||
playlist.tracks
|
||||
);
|
||||
queue.play();
|
||||
break;
|
||||
}
|
||||
|
||||
case playSources.artist:
|
||||
utilPlayFromArtist(useQStore, useArtistPageStore, 0);
|
||||
utilPlayFromArtist(0);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -8,12 +8,12 @@ export interface AlbumDisc {
|
||||
export interface Track extends AlbumDisc {
|
||||
id: string;
|
||||
title: string;
|
||||
album?: string;
|
||||
album: string;
|
||||
artists: Artist[];
|
||||
albumartists: Artist[];
|
||||
albumhash?: string;
|
||||
folder?: string;
|
||||
filepath?: string;
|
||||
filepath: string;
|
||||
duration?: number;
|
||||
bitrate: number;
|
||||
image: string;
|
||||
@@ -29,6 +29,7 @@ export interface Track extends AlbumDisc {
|
||||
genre?: string;
|
||||
copyright?: string;
|
||||
master_index?: number;
|
||||
help_text?: string;
|
||||
}
|
||||
|
||||
export interface Folder {
|
||||
@@ -54,6 +55,7 @@ export interface Album {
|
||||
colors: string[];
|
||||
copyright?: string;
|
||||
|
||||
help_text?: string;
|
||||
is_live: boolean;
|
||||
is_compilation: boolean;
|
||||
is_soundtrack: boolean;
|
||||
@@ -73,6 +75,7 @@ export interface Artist {
|
||||
duration: number;
|
||||
colors: string[];
|
||||
is_favorite?: boolean;
|
||||
help_text?: string;
|
||||
}
|
||||
|
||||
export interface Option {
|
||||
@@ -103,6 +106,7 @@ export interface Playlist {
|
||||
duration: number;
|
||||
settings: PlaylistSettings;
|
||||
pinned: boolean;
|
||||
help_text?: string;
|
||||
images:
|
||||
| {
|
||||
image: string;
|
||||
@@ -216,3 +220,8 @@ export interface ScrollerItem {
|
||||
component: any;
|
||||
props?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface LyricsLine {
|
||||
time: number;
|
||||
text: string;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user