mirror of
https://github.com/JuLi0n21/pwa-player.git
synced 2026-04-19 23:40:05 +00:00
minor improvements
This commit is contained in:
3
frontend/.prettierrc
Normal file
3
frontend/.prettierrc
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"printWidth": 120
|
||||||
|
}
|
||||||
@@ -9,12 +9,8 @@
|
|||||||
"name": "MusicBackend"
|
"name": "MusicBackend"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"consumes": [
|
"consumes": ["application/json"],
|
||||||
"application/json"
|
"produces": ["application/json"],
|
||||||
],
|
|
||||||
"produces": [
|
|
||||||
"application/json"
|
|
||||||
],
|
|
||||||
"paths": {
|
"paths": {
|
||||||
"/api/v1/artist/{artist}": {
|
"/api/v1/artist/{artist}": {
|
||||||
"get": {
|
"get": {
|
||||||
@@ -41,9 +37,7 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"tags": [
|
"tags": ["MusicBackend"]
|
||||||
"MusicBackend"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/v1/collections": {
|
"/api/v1/collections": {
|
||||||
@@ -92,9 +86,7 @@
|
|||||||
"format": "int32"
|
"format": "int32"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"tags": [
|
"tags": ["MusicBackend"]
|
||||||
"MusicBackend"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/v1/favorites": {
|
"/api/v1/favorites": {
|
||||||
@@ -136,9 +128,7 @@
|
|||||||
"format": "int32"
|
"format": "int32"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"tags": [
|
"tags": ["MusicBackend"]
|
||||||
"MusicBackend"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/v1/recent": {
|
"/api/v1/recent": {
|
||||||
@@ -174,9 +164,7 @@
|
|||||||
"format": "int32"
|
"format": "int32"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"tags": [
|
"tags": ["MusicBackend"]
|
||||||
"MusicBackend"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/v1/search": {
|
"/api/v1/search": {
|
||||||
@@ -218,9 +206,7 @@
|
|||||||
"format": "int32"
|
"format": "int32"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"tags": [
|
"tags": ["MusicBackend"]
|
||||||
"MusicBackend"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/v1/search/artists": {
|
"/api/v1/search/artists": {
|
||||||
@@ -262,9 +248,7 @@
|
|||||||
"format": "int32"
|
"format": "int32"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"tags": [
|
"tags": ["MusicBackend"]
|
||||||
"MusicBackend"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/v1/search/collections": {
|
"/api/v1/search/collections": {
|
||||||
@@ -306,9 +290,7 @@
|
|||||||
"format": "int32"
|
"format": "int32"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"tags": [
|
"tags": ["MusicBackend"]
|
||||||
"MusicBackend"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/v1/song/{hash}": {
|
"/api/v1/song/{hash}": {
|
||||||
@@ -336,9 +318,7 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"tags": [
|
"tags": ["MusicBackend"]
|
||||||
"MusicBackend"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/ping": {
|
"/ping": {
|
||||||
@@ -366,9 +346,7 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"tags": [
|
"tags": ["MusicBackend"]
|
||||||
"MusicBackend"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,21 +1,23 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
<head>
|
||||||
<head>
|
<meta charset="UTF-8" />
|
||||||
<meta charset="UTF-8">
|
<link rel="icon" href="/favicon.ico" />
|
||||||
<link rel="icon" href="/favicon.ico">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
<link rel="manifest" href="/manifest.json">
|
<link rel="manifest" href="/manifest.json" />
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.6.0/css/all.min.css"
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.6.0/css/all.min.css"
|
||||||
integrity="sha512-Kc323vGBEqzTmouAECnVceyQqyqdsSiqLQISBL29aUW4U/M7pSPA/gEUZQqv1cwx4OnYxTxve5UMg5GT6L4JJg=="
|
integrity="sha512-Kc323vGBEqzTmouAECnVceyQqyqdsSiqLQISBL29aUW4U/M7pSPA/gEUZQqv1cwx4OnYxTxve5UMg5GT6L4JJg=="
|
||||||
crossorigin="anonymous" referrerpolicy="no-referrer" />
|
crossorigin="anonymous"
|
||||||
|
referrerpolicy="no-referrer"
|
||||||
|
/>
|
||||||
<title>osu! music player, not affiliated with the osu! trademark</title>
|
<title>osu! music player, not affiliated with the osu! trademark</title>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<script type="module" src="/src/main.ts"></script>
|
<script type="module" src="/src/main.ts"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
15
frontend/package-lock.json
generated
15
frontend/package-lock.json
generated
@@ -10,6 +10,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.9.0",
|
"axios": "^1.9.0",
|
||||||
"pinia": "^2.2.6",
|
"pinia": "^2.2.6",
|
||||||
|
"prettier": "^3.8.1",
|
||||||
"vue": "^3.5.13",
|
"vue": "^3.5.13",
|
||||||
"vue-router": "^4.4.5"
|
"vue-router": "^4.4.5"
|
||||||
},
|
},
|
||||||
@@ -3641,6 +3642,20 @@
|
|||||||
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
|
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/prettier": {
|
||||||
|
"version": "3.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz",
|
||||||
|
"integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==",
|
||||||
|
"bin": {
|
||||||
|
"prettier": "bin/prettier.cjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/proxy-agent": {
|
"node_modules/proxy-agent": {
|
||||||
"version": "6.5.0",
|
"version": "6.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz",
|
||||||
|
|||||||
@@ -9,11 +9,13 @@
|
|||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"build-only": "vite build",
|
"build-only": "vite build",
|
||||||
"type-check": "vue-tsc --build",
|
"type-check": "vue-tsc --build",
|
||||||
|
"format": "prettier --write .",
|
||||||
"generate": "openapi-generator-cli generate -i api-specs/osu_music.swagger.json -g typescript-axios -o ./src/generated"
|
"generate": "openapi-generator-cli generate -i api-specs/osu_music.swagger.json -g typescript-axios -o ./src/generated"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.9.0",
|
"axios": "^1.9.0",
|
||||||
"pinia": "^2.2.6",
|
"pinia": "^2.2.6",
|
||||||
|
"prettier": "^3.8.1",
|
||||||
"vue": "^3.5.13",
|
"vue": "^3.5.13",
|
||||||
"vue-router": "^4.4.5"
|
"vue-router": "^4.4.5"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,43 +1,31 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { RouterLink, RouterView } from 'vue-router'
|
import { RouterLink, RouterView } from "vue-router";
|
||||||
import NowPlaying from '@/components/NowPlaying.vue'
|
import NowPlaying from "@/components/NowPlaying.vue";
|
||||||
import NowPlayingView from '@/views/NowPlayingView.vue'
|
import NowPlayingView from "@/views/NowPlayingView.vue";
|
||||||
import MenuView from '@/views/MenuView.vue'
|
import MenuView from "@/views/MenuView.vue";
|
||||||
import HistoryView from '@/views/HistoryView.vue'
|
import HistoryView from "@/views/HistoryView.vue";
|
||||||
import Footer from '@/components/Footer.vue'
|
import Footer from "@/components/Footer.vue";
|
||||||
import { ref, onMounted, watch, onUnmounted } from 'vue'
|
import { ref, onMounted, watch, onUnmounted } from "vue";
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from "vue-router";
|
||||||
import { isMobile, isPc } from './script/utils.ts'
|
import { isMobile, isPc } from "./script/utils.ts";
|
||||||
import { useAudio } from './composables/useAudio.ts'
|
|
||||||
|
|
||||||
const showNowPlaying = ref(true);
|
const showNowPlaying = ref(true);
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const audioRef = ref<HTMLAudioElement | null>(null)
|
|
||||||
const audio = useAudio(audioRef);
|
|
||||||
|
|
||||||
watch(route, async (to) => {
|
watch(route, async (to) => {
|
||||||
|
|
||||||
if (route.path.startsWith("/nowplaying")) {
|
if (route.path.startsWith("/nowplaying")) {
|
||||||
showNowPlaying.value = false;
|
showNowPlaying.value = false;
|
||||||
} else {
|
} else {
|
||||||
showNowPlaying.value = true;
|
showNowPlaying.value = true;
|
||||||
}
|
}
|
||||||
/*
|
});
|
||||||
if (route.path.startsWith("/menu")) {
|
|
||||||
headerStore.hide();
|
|
||||||
} else {
|
|
||||||
headerStore.show();
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
})
|
|
||||||
|
|
||||||
function loadColors() {
|
function loadColors() {
|
||||||
|
document.documentElement.style.setProperty("--background-color", localStorage.getItem("bgColor") || "#1c1719");
|
||||||
|
document.documentElement.style.setProperty("--action-color", localStorage.getItem("actionColor") || "#eab308");
|
||||||
|
|
||||||
document.documentElement.style.setProperty('--background-color', localStorage.getItem('bgColor') || '#1c1719');
|
document.documentElement.style.setProperty("--information-color", localStorage.getItem("infoColor") || "#ec4899");
|
||||||
document.documentElement.style.setProperty('--action-color', localStorage.getItem('actionColor') || '#eab308');
|
document.documentElement.style.setProperty("--border-color", localStorage.getItem("borderColor") || "#ec4899");
|
||||||
|
|
||||||
document.documentElement.style.setProperty('--information-color', localStorage.getItem('infoColor') || '#ec4899');
|
|
||||||
document.documentElement.style.setProperty('--border-color', localStorage.getItem('borderColor') || '#ec4899');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
loadColors();
|
loadColors();
|
||||||
@@ -50,29 +38,27 @@ const screenInfo = ref({
|
|||||||
const checkScreenSize = () => {
|
const checkScreenSize = () => {
|
||||||
screenInfo.value.isSmall = isMobile();
|
screenInfo.value.isSmall = isMobile();
|
||||||
screenInfo.value.isMedium = isPc();
|
screenInfo.value.isMedium = isPc();
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
checkScreenSize();
|
checkScreenSize();
|
||||||
window.addEventListener('resize', checkScreenSize);
|
window.addEventListener("resize", checkScreenSize);
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
window.removeEventListener('resize', checkScreenSize);
|
window.removeEventListener("resize", checkScreenSize);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div v-if="screenInfo.isSmall" class="flex flex-col h-screen max-h-screen wrapper info text-xl">
|
<div v-if="screenInfo.isSmall" class="flex flex-col h-screen max-h-screen text-xl wrapper info">
|
||||||
<RouterView />
|
<RouterView />
|
||||||
<NowPlaying v-show="showNowPlaying" />
|
<NowPlaying v-show="showNowPlaying" />
|
||||||
<Footer />
|
<Footer />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="flex flex-col h-screen max-h-screen wrapper info text-xl">
|
<div v-else class="flex flex-col h-screen max-h-screen text-xl wrapper info">
|
||||||
<main class="flex flex-1 w-full h-full overflow-y-hidden">
|
<main class="flex flex-1 w-full h-full overflow-y-hidden">
|
||||||
|
<aside class="bg-primary p-4 w-1/12 overflow-y-scroll">
|
||||||
<aside class="w-1/12 bg-primary p-4 overflow-y-scroll">
|
|
||||||
<HistoryView />
|
<HistoryView />
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
@@ -80,7 +66,7 @@ onUnmounted(() => {
|
|||||||
<RouterView />
|
<RouterView />
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="w-1/5 overflow-y-scroll flex flex-col">
|
<section class="flex flex-col w-1/5 overflow-y-scroll">
|
||||||
<NowPlayingView />
|
<NowPlayingView />
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -1,19 +1,27 @@
|
|||||||
.v-enter-from, .v-leave-to
|
.v-enter-from,
|
||||||
{
|
.v-leave-to {
|
||||||
animation: fadeIn 0.5s;
|
animation: fadeIn 0.5s;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.v-enter-to, .v-leave-from {
|
.v-enter-to,
|
||||||
animation: fadeOut 0.5s;
|
.v-leave-from {
|
||||||
|
animation: fadeOut 0.5s;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes fadeIn {
|
@keyframes fadeIn {
|
||||||
0% { opacity: 0; }
|
0% {
|
||||||
100% { opacity: 1; }
|
opacity: 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes fadeOut {
|
@keyframes fadeOut {
|
||||||
0% { opacity: 1; }
|
0% {
|
||||||
100% { opacity: 0; }
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
body {
|
body {
|
||||||
background-color: var(--background-color);
|
background-color: var(--background-color);
|
||||||
margin:0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
* .{
|
* . {
|
||||||
min-height: 0px;
|
min-height: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.backdrop--light {
|
.backdrop--light {
|
||||||
@@ -21,10 +21,10 @@ min-height: 0px;
|
|||||||
}
|
}
|
||||||
|
|
||||||
.backdrop--medium--dark {
|
.backdrop--medium--dark {
|
||||||
background-color: rgba(56, 46, 50, 1)
|
background-color: rgba(56, 46, 50, 1);
|
||||||
}
|
}
|
||||||
.backdrop--dark {
|
.backdrop--dark {
|
||||||
background-color: rgba(28, 23, 25, 1)
|
background-color: rgba(28, 23, 25, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.router-link-active {
|
.router-link-active {
|
||||||
@@ -43,25 +43,25 @@ min-height: 0px;
|
|||||||
}
|
}
|
||||||
|
|
||||||
.bg {
|
.bg {
|
||||||
background-color: var(--background-color)
|
background-color: var(--background-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.action {
|
.action {
|
||||||
color: var(--action-color)
|
color: var(--action-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.info {
|
.info {
|
||||||
color: var(--information-color)
|
color: var(--information-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.info:hover {
|
.info:hover {
|
||||||
color: var(--information-color)
|
color: var(--information-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
searcheven {
|
searcheven {
|
||||||
background-color: lighten(var(--background-color),10%);
|
background-color: lighten(var(--background-color), 10%);
|
||||||
}
|
}
|
||||||
|
|
||||||
searchodd {
|
searchodd {
|
||||||
background-color: lighten(var(--background-color),15%);
|
background-color: lighten(var(--background-color), 15%);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@import './base.css';
|
@import "./base.css";
|
||||||
|
|
||||||
a,
|
a,
|
||||||
.green {
|
.green {
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Song, CollectionPreview } from '../script/types'
|
import type { Song, CollectionPreview } from "../script/types";
|
||||||
import { useAudio } from '@/composables/useAudio';
|
import { useAudio } from "@/composables/useAudio";
|
||||||
import { ref } from 'vue';
|
import { ref } from "vue";
|
||||||
import { RouterLink } from 'vue-router';
|
import { RouterLink } from "vue-router";
|
||||||
|
|
||||||
const audioStore = useAudio()
|
const audioStore = useAudio();
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
songs: Song[];
|
songs: Song[];
|
||||||
@@ -13,20 +13,18 @@ const props = defineProps<{
|
|||||||
}>();
|
}>();
|
||||||
|
|
||||||
function update(hash: string) {
|
function update(hash: string) {
|
||||||
|
audioStore.setSong(props.songs[props.songs.findIndex((s) => s.hash == hash)]);
|
||||||
audioStore.setSong(props.songs.at(props.songs.findIndex(s => s.hash == hash)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function highlightText(text: string, searchterm: string) {
|
function highlightText(text: string, searchterm: string) {
|
||||||
if (!searchterm) return text;
|
if (!searchterm) return text;
|
||||||
const regex = new RegExp(`(${searchterm})`, 'gi');
|
const regex = new RegExp(`(${searchterm})`, "gi");
|
||||||
return text.replace(regex, '<span style="color: yellow;">$1</span>');
|
return text.replace(regex, '<span style="color: yellow;">$1</span>');
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="h-full w-full overflow-scroll overflow-x-hidden border bordercolor rounded-lg text-xs bg">
|
<div class="border rounded-lg w-full h-full overflow-scroll overflow-x-hidden text-xs bordercolor bg">
|
||||||
<div v-if="props.artist && props.artist.length > 0" class="border bordercolor">
|
<div v-if="props.artist && props.artist.length > 0" class="border bordercolor">
|
||||||
<h2 class="text-2xl action">Artists</h2>
|
<h2 class="text-2xl action">Artists</h2>
|
||||||
<ul>
|
<ul>
|
||||||
@@ -41,15 +39,14 @@ function highlightText(text: string, searchterm: string) {
|
|||||||
<ul>
|
<ul>
|
||||||
<li v-for="(song, index) in props.songs" :key="index" class="rounded-lg">
|
<li v-for="(song, index) in props.songs" :key="index" class="rounded-lg">
|
||||||
<button @click="update(song.hash)" class="flex">
|
<button @click="update(song.hash)" class="flex">
|
||||||
<img :src="encodeURI(song.previewimage + '?h=120&w=120')" class="h-12 w-12"></img>
|
<img :src="encodeURI(song.previewimage + '?h=120&w=120')" class="w-12 h-12" />
|
||||||
<p class="text-nowrap text-ellipsis overflow-hidden ml-2">
|
<p class="ml-2 overflow-hidden text-ellipsis text-nowrap">
|
||||||
<span v-html="highlightText(song.name, search)"></span> - <span
|
<span v-html="highlightText(song.name, search)"></span> -
|
||||||
v-html="highlightText(song.artist, props.search)"></span>
|
<span v-html="highlightText(song.artist, props.search)"></span>
|
||||||
</p>
|
</p>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,97 +1,90 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import SongItem from '../components/SongItem.vue'
|
import SongItem from "../components/SongItem.vue";
|
||||||
import { type Song, mapApiToCollection } from '../script/types'
|
import { type Song, mapApiToCollection } from "../script/types";
|
||||||
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
import { ref, onMounted, onBeforeUnmount } from "vue";
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from "vue-router";
|
||||||
import CollectionListItem from '../components/CollectionListItem.vue'
|
import CollectionListItem from "../components/CollectionListItem.vue";
|
||||||
import { useAudio } from '@/composables/useAudio'
|
import { useAudio } from "@/composables/useAudio";
|
||||||
import { useApi } from '@/composables/useApi'
|
import { useApi } from "@/composables/useApi";
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute();
|
||||||
const audioStore = useAudio()
|
const audioStore = useAudio();
|
||||||
const { musicApi } = useApi()
|
const { musicApi } = useApi();
|
||||||
|
|
||||||
const songs = ref<Song[]>([])
|
const songs = ref<Song[]>([]);
|
||||||
const name = ref('name')
|
const name = ref("name");
|
||||||
const limit = 50
|
const limit = 50;
|
||||||
let offset = ref(0)
|
let offset = ref(0);
|
||||||
let loading = ref(false)
|
let loading = ref(false);
|
||||||
let hasMore = ref(true)
|
let hasMore = ref(true);
|
||||||
|
|
||||||
const fetchMoreSongs = async () => {
|
const fetchMoreSongs = async () => {
|
||||||
if (loading.value || !hasMore.value) return
|
if (loading.value || !hasMore.value) return;
|
||||||
|
|
||||||
loading.value = true
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
const response = await musicApi().musicBackendCollections(
|
const response = await musicApi.value.musicBackendCollections("", route.params.id, limit, offset.value);
|
||||||
"",
|
|
||||||
Array.isArray(route.params.id) ? route.params.id[0] : route.params.id,
|
|
||||||
limit,
|
|
||||||
offset.value
|
|
||||||
)
|
|
||||||
|
|
||||||
const col = mapApiToCollection(response.data)
|
const col = mapApiToCollection(response.data);
|
||||||
|
|
||||||
if (offset.value === 0) {
|
if (offset.value === 0) {
|
||||||
name.value = col.name
|
name.value = col.name;
|
||||||
songs.value = col.songs
|
songs.value = col.songs;
|
||||||
} else {
|
} else {
|
||||||
songs.value.push(...col.songs)
|
songs.value.push(...col.songs);
|
||||||
}
|
}
|
||||||
|
|
||||||
audioStore.setCollection(songs.value)
|
audioStore.setCollection(songs.value);
|
||||||
|
|
||||||
if (col.songs.length < limit) {
|
if (col.songs.length < limit) {
|
||||||
hasMore.value = false
|
hasMore.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
offset.value += limit
|
offset.value += limit;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching songs:', error)
|
console.error("Error fetching songs:", error);
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const onScroll = (e: Event) => {
|
const onScroll = (e: Event) => {
|
||||||
const target = e.target as HTMLElement
|
const target = e.target as HTMLElement;
|
||||||
const scrollTop = target.scrollTop
|
const scrollTop = target.scrollTop;
|
||||||
const scrollHeight = target.scrollHeight
|
const scrollHeight = target.scrollHeight;
|
||||||
const clientHeight = target.clientHeight
|
const clientHeight = target.clientHeight;
|
||||||
|
|
||||||
if (scrollTop + clientHeight >= scrollHeight * 0.9) {
|
if (scrollTop + clientHeight >= scrollHeight * 0.9) {
|
||||||
fetchMoreSongs()
|
fetchMoreSongs();
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchMoreSongs()
|
fetchMoreSongs();
|
||||||
const container = document.querySelector('.coll-container')
|
const container = document.querySelector(".coll-container");
|
||||||
container?.addEventListener('scroll', onScroll)
|
container?.addEventListener("scroll", onScroll);
|
||||||
})
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
const container = document.querySelector('.coll-container')
|
const container = document.querySelector(".coll-container");
|
||||||
container?.removeEventListener('scroll', onScroll)
|
container?.removeEventListener("scroll", onScroll);
|
||||||
})
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<main class="flex-1 text-center flex flex-col h-full overflow-y-hidden">
|
<main class="flex flex-col flex-1 h-full overflow-y-hidden text-center">
|
||||||
<header v-show="true">
|
<header v-show="true">
|
||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
<nav class="flex justify-start my-2 mx-1 space-x-1 overflow-y-scroll flex-nowrap text-nowrap">
|
<nav class="flex flex-nowrap justify-start space-x-1 mx-1 my-2 overflow-y-scroll text-nowrap">
|
||||||
<RouterLink class="p-1 rounded-full backdrop--light shadow-xl" to="/menu/collections"><i class="fa-solid fa-arrow-left"></i>
|
<RouterLink class="shadow-xl backdrop--light p-1 rounded-full" to="/menu/collections"
|
||||||
|
><i class="fa-arrow-left fa-solid"></i>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
<p class="p-1 rounded-full backdrop--light shadow-xl">{{ name }}</p>
|
<p class="shadow-xl backdrop--light p-1 rounded-full">{{ name }}</p>
|
||||||
</nav>
|
</nav>
|
||||||
<hr>
|
<hr />
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div
|
<div class="flex-col flex-1 justify-start overflow-y-scroll coll-container">
|
||||||
class="flex-1 flex-col overflow-y-scroll coll-container justify-start"
|
|
||||||
>
|
|
||||||
<SongItem v-for="(song, index) in songs" :key="index" :song="song" />
|
<SongItem v-for="(song, index) in songs" :key="index" :song="song" />
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -1,20 +1,17 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { CollectionPreview } from '@/script/types';
|
import type { CollectionPreview } from "@/script/types";
|
||||||
|
|
||||||
const props = defineProps<{ collection: CollectionPreview }>();
|
const props = defineProps<{ collection: CollectionPreview }>();
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<RouterLink :to="'/collection/' + props.collection.index">
|
||||||
<RouterLink :to="'/collection/' + props.collection.index +1">
|
<div class="flex border rounded-lg bordercolor">
|
||||||
<div class=" border bordercolor rounded-lg flex">
|
<img class="m-2 rounded-lg w-20 h-20" :src="props.collection.previewimage" loading="lazy" />
|
||||||
<img class="h-20 w-20 m-2 rounded-lg" :src="props.collection.previewimage" loading="lazy" />
|
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<h3 class="self-start info">{{ props.collection.name }}</h3>
|
<h3 class="self-start info">{{ props.collection.name }}</h3>
|
||||||
<h5 class="self-start info text-sm">{{ props.collection.length }} Songs </h5>
|
<h5 class="self-start text-sm info">{{ props.collection.length }} Songs</h5>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,26 +1,25 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useUser } from '@/composables/useUser';
|
import { useUser } from "@/composables/useUser";
|
||||||
|
|
||||||
|
|
||||||
const userStore = useUser();
|
const userStore = useUser();
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<hr>
|
<hr />
|
||||||
<nav class="flex justify-around my-2 text-sm md:text-2xl">
|
<nav class="flex justify-around my-2 text-sm md:text-2xl">
|
||||||
<RouterLink class="p-1 rounded-full backdrop--light shadow-xl hover:text-pink-500" to="/me">
|
<RouterLink class="shadow-xl backdrop--light p-1 rounded-full hover:text-pink-500" to="/me">
|
||||||
<img :src="userStore.user.value?.avatar_url || 'https://osu.ppy.sh/images/layout/avatar-guest.png'"
|
<img
|
||||||
class="md:h-12 h-6 rounded-full">
|
:src="userStore.user.value?.avatar_url ?? 'https://osu.ppy.sh/images/layout/avatar-guest.png'"
|
||||||
|
class="rounded-full h-6 md:h-12"
|
||||||
|
/>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
|
|
||||||
<RouterLink class="flex flex-col justify-center p-2 rounded-full backdrop--light shadow-xl info" to="/menu"><i
|
<RouterLink class="flex flex-col justify-center shadow-xl backdrop--light p-2 rounded-full info" to="/menu"
|
||||||
class="fa-solid fa-house"></i>
|
><i class="fa-solid fa-house"></i>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
|
|
||||||
<RouterLink class="flex flex-col justify-center p-2 rounded-full backdrop--light shadow-xl info" to="/search">
|
<RouterLink class="flex flex-col justify-center shadow-xl backdrop--light p-2 rounded-full info" to="/search">
|
||||||
<i class="fa-solid fa-magnifying-glass"></i>
|
<i class="fa-solid fa-magnifying-glass"></i>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
|
|
||||||
</nav>
|
</nav>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,27 +1,30 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useAudio } from '@/composables/useAudio';
|
import { useAudio } from "@/composables/useAudio";
|
||||||
import { onMounted, computed } from 'vue'
|
import { onMounted, computed } from "vue";
|
||||||
|
|
||||||
const audioStore = useAudio();
|
const audioStore = useAudio();
|
||||||
|
|
||||||
const title = computed(() => audioStore.currentSong.value?.name || 'Unknown Title')
|
const title = computed(() => audioStore.currentSong.value?.name || "Unknown Title");
|
||||||
const artist = computed(() => audioStore.currentSong.value?.artist || 'Unknown Artist')
|
const artist = computed(() => audioStore.currentSong.value?.artist || "Unknown Artist");
|
||||||
const bgimg = computed(() => audioStore.currentSong.value?.previewimage || '/default-bg.jpg')
|
const bgimg = computed(() => audioStore.currentSong.value?.previewimage || "/default-bg.jpg");
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
audioStore.init()
|
audioStore.init();
|
||||||
})
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<hr>
|
<hr />
|
||||||
<div class="relative wrapper p-1 action">
|
<div class="relative wrapper p-1 action">
|
||||||
<img :src="encodeURI(bgimg + '?h=150&w=400')" class="w-full absolute top-0 left-0 right-0 h-full"
|
<img
|
||||||
:style="{ 'filter': 'blur(2px)', 'opacity': '0.5' }" alt="Background Image" />
|
:src="encodeURI(bgimg + '?h=150&w=400')"
|
||||||
|
class="w-full absolute top-0 left-0 right-0 h-full"
|
||||||
|
:style="{ filter: 'blur(2px)', opacity: '0.5' }"
|
||||||
|
alt="Background Image"
|
||||||
|
/>
|
||||||
|
|
||||||
<nav class="relative flex-col z-10">
|
<nav class="relative flex-col z-10">
|
||||||
|
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<RouterLink to="/nowplaying" class="grow overflow-hidden">
|
<RouterLink to="/nowplaying" class="grow overflow-hidden">
|
||||||
<p class="relative text-sm text-left font-bold info overflow-hidden text-ellipsis text-nowrap">
|
<p class="relative text-sm text-left font-bold info overflow-hidden text-ellipsis text-nowrap">
|
||||||
@@ -33,17 +36,19 @@ onMounted(() => {
|
|||||||
</p>
|
</p>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
<div class="flex flex-col text-center justify-center px-2" @click="audioStore.togglePlay">
|
<div class="flex flex-col text-center justify-center px-2" @click="audioStore.togglePlay">
|
||||||
<i :class="[audioStore.isPlaying.value ? ' fa-circle-pause' : 'fa-circle-play']" class="text-3xl fa-regular"></i>
|
<i
|
||||||
|
:class="[audioStore.isPlaying.value ? ' fa-circle-pause' : 'fa-circle-play']"
|
||||||
|
class="text-3xl fa-regular"
|
||||||
|
></i>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-full bg-gray-200 rounded-full h-0.5 dark:bg-gray-700">
|
<div class="w-full bg-gray-200 rounded-full h-0.5 dark:bg-gray-700">
|
||||||
<div class="bg-blue-600 h-0.5 rounded-full dark:bg-yellow-500"
|
<div
|
||||||
:style="{ 'width': audioStore.percentDone.value + '%' }">
|
class="bg-blue-600 h-0.5 rounded-full dark:bg-yellow-500"
|
||||||
|
:style="{ width: audioStore.percentDone.value + '%' }"
|
||||||
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,43 +1,44 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useAudio } from '@/composables/useAudio';
|
import { useAudio } from "@/composables/useAudio";
|
||||||
import type { Song } from '@/script/types';
|
import type { Song } from "@/script/types";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
song: Song,
|
song: Song;
|
||||||
action?: string,
|
action?: string;
|
||||||
info?: string,
|
info?: string;
|
||||||
border?: string,
|
border?: string;
|
||||||
}>();
|
}>();
|
||||||
const audioStore = useAudio();
|
const audioStore = useAudio();
|
||||||
|
|
||||||
function updateSong() {
|
function updateSong() {
|
||||||
|
|
||||||
let updated = props.song;
|
let updated = props.song;
|
||||||
audioStore.setSong(updated)
|
audioStore.setSong(updated);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<div @click="updateSong" :style="{ borderColor: border }" class="flex m-1 border rounded-lg md:text-xl bordercolor">
|
||||||
<div @click="updateSong" :style="{ borderColor: border }" class="m-1 md:text-xl border bordercolor rounded-lg flex">
|
<img
|
||||||
<img class="h-14 w-14 md:w-24 md:h-24 m-1 rounded-lg"
|
class="m-1 rounded-lg w-14 md:w-24 h-14 md:h-24"
|
||||||
:src="encodeURI(props.song?.previewimage ? props.song?.previewimage + '?h=56&w=56' : '/default-bg.png')"
|
:src="encodeURI(props.song?.previewimage ? props.song?.previewimage + '?h=56&w=56' : '/default-bg.png')"
|
||||||
loading="lazy" />
|
loading="lazy"
|
||||||
|
/>
|
||||||
<div class="flex flex-col overflow-hidden text-left">
|
<div class="flex flex-col overflow-hidden text-left">
|
||||||
<p :style="{ color: info }" class="text-nowrap text-ellipsis overflow-hidden text-base info">
|
<p :style="{ color: info }" class="overflow-hidden text-base text-ellipsis text-nowrap info">
|
||||||
<slot name="songName">{{ props.song?.name ? props.song?.name : 'Unknown Title' }}</slot>
|
<slot name="songName">{{ props.song?.name ? props.song?.name : "Unknown Title" }}</slot>
|
||||||
</p>
|
</p>
|
||||||
<h5 :style="{ color: action }" class="action text-sm text-nowrap text-ellipsis overflow-hidden text-base">
|
<h5 :style="{ color: action }" class="overflow-hidden text-sm text-base text-ellipsis text-nowrap action">
|
||||||
<slot name="artist">{{ props.song?.artist ? props.song.artist : 'Unknown Artist' }}</slot>
|
<slot name="artist">{{ props.song?.artist ? props.song.artist : "Unknown Artist" }}</slot>
|
||||||
</h5>
|
</h5>
|
||||||
<h5 :style="{ color: action }" class="action text-sm">
|
<h5 :style="{ color: action }" class="text-sm action">
|
||||||
<slot name="length">{{ Math.floor(props.song?.length / 60000 ?? 0) }}:{{ Math.floor((props.song?.length ?? 0 /
|
<slot name="length"
|
||||||
1000)
|
>{{ Math.floor(props.song.length / 60000 || 0) }}:{{
|
||||||
% 60).toString().padStart(2, '0') }}</slot>
|
Math.floor((props.song.length ?? 0 / 1000) % 60)
|
||||||
|
.toString()
|
||||||
|
.padStart(2, "0")
|
||||||
|
}}</slot
|
||||||
|
>
|
||||||
</h5>
|
</h5>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
import { MusicBackendApi, Configuration, } from '@/generated';
|
import { MusicBackendApi, Configuration } from "@/generated";
|
||||||
import type { ConfigurationParameters } from '@/generated';
|
import type { ConfigurationParameters } from "@/generated";
|
||||||
import { ref } from 'vue';
|
import { useUser } from "./useUser";
|
||||||
|
import { computed } from "vue";
|
||||||
|
|
||||||
export function useApi() {
|
export function useApi() {
|
||||||
const basePath = ref(import.meta.env.BACKEND_URL || 'http://localhost:8080');
|
const userStore = useUser();
|
||||||
const musicApi = (): MusicBackendApi => {
|
|
||||||
|
const musicApi = computed(() => {
|
||||||
const configParams: ConfigurationParameters = {
|
const configParams: ConfigurationParameters = {
|
||||||
basePath: basePath.value,
|
basePath: userStore.cloudflareUrl.value,
|
||||||
};
|
};
|
||||||
|
|
||||||
const configuration = new Configuration(configParams);
|
const configuration = new Configuration(configParams);
|
||||||
return new MusicBackendApi(configuration);
|
return new MusicBackendApi(configuration);
|
||||||
};
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
musicApi,
|
musicApi,
|
||||||
|
|||||||
@@ -1,160 +1,155 @@
|
|||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from "vue";
|
||||||
import { mapApiToSongs, type Song } from '@/script/types'
|
import { mapApiToSongs, type Song } from "@/script/types";
|
||||||
import { useApi } from './useApi'
|
import { useApi } from "./useApi";
|
||||||
|
|
||||||
let audioInstance: ReturnType<typeof createAudio> | null = null
|
let audioInstance: ReturnType<typeof createAudio> | null = null;
|
||||||
|
|
||||||
function createAudio() {
|
function createAudio() {
|
||||||
const { musicApi } = useApi()
|
const { musicApi } = useApi();
|
||||||
|
|
||||||
const audioElement = document.createElement('audio')
|
const audioElement = document.createElement("audio");
|
||||||
audioElement.setAttribute('id', 'global-audio')
|
audioElement.setAttribute("id", "global-audio");
|
||||||
audioElement.setAttribute('controls', '')
|
audioElement.setAttribute("controls", "");
|
||||||
audioElement.classList.add('hidden')
|
audioElement.classList.add("hidden");
|
||||||
document.body.appendChild(audioElement)
|
document.body.appendChild(audioElement);
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
isPlaying: ref(false),
|
isPlaying: ref(false),
|
||||||
duration: ref('0:00'),
|
duration: ref("0:00"),
|
||||||
currentTime: ref('0:00'),
|
currentTime: ref("0:00"),
|
||||||
percentDone: ref(0),
|
percentDone: ref(0),
|
||||||
shuffle: ref(false),
|
shuffle: ref(false),
|
||||||
repeat: ref(false),
|
repeat: ref(false),
|
||||||
activeSongs: ref<Song[] | null>([]),
|
activeSongs: ref<Song[] | null>([]),
|
||||||
currentSong: ref<Song | null>(null),
|
currentSong: ref<Song | null>(null),
|
||||||
recentlyPlayed: ref(new Map<string, Song>()),
|
recentlyPlayed: ref(new Map<string, Song>()),
|
||||||
}
|
};
|
||||||
|
|
||||||
function saveToLocalStorage(key: string, data: any) {
|
function saveToLocalStorage(key: string, data: any) {
|
||||||
localStorage.setItem(key, JSON.stringify(data))
|
localStorage.setItem(key, JSON.stringify(data));
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadFromLocalStorage<T>(key: string): T | null {
|
function loadFromLocalStorage<T>(key: string): T | null {
|
||||||
const item = localStorage.getItem(key)
|
const item = localStorage.getItem(key);
|
||||||
return item ? JSON.parse(item) as T : null
|
return item ? (JSON.parse(item) as T) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setSong(song: Song | null) {
|
function setSong(song: Song | null) {
|
||||||
if (!song) return
|
if (!song) return;
|
||||||
state.currentSong.value = song
|
state.currentSong.value = song;
|
||||||
const map = state.recentlyPlayed.value;
|
const map = state.recentlyPlayed.value;
|
||||||
|
|
||||||
|
saveToLocalStorage("lastPlayedSong", song);
|
||||||
saveToLocalStorage('lastPlayedSong', song)
|
|
||||||
if (map.has(song.hash)) {
|
if (map.has(song.hash)) {
|
||||||
map.delete(song.hash);
|
map.delete(song.hash);
|
||||||
}
|
}
|
||||||
map.set(song.hash, song);
|
map.set(song.hash, song);
|
||||||
|
|
||||||
audioElement.pause()
|
audioElement.pause();
|
||||||
audioElement.src = song.url
|
audioElement.src = song.url;
|
||||||
audioElement.addEventListener(
|
audioElement.addEventListener("canplaythrough", () => audioElement.play().catch(console.error), { once: true });
|
||||||
'canplaythrough',
|
|
||||||
() => audioElement.play().catch(console.error),
|
|
||||||
{ once: true }
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setCollection(song: Song[] | null) {
|
function setCollection(song: Song[] | null) {
|
||||||
if (!song) return
|
if (!song) return;
|
||||||
state.activeSongs.value = song
|
state.activeSongs.value = song;
|
||||||
saveToLocalStorage('activeCollection', song)
|
saveToLocalStorage("activeCollection", song);
|
||||||
}
|
}
|
||||||
|
|
||||||
function togglePlay() {
|
function togglePlay() {
|
||||||
if (audioElement.paused) {
|
if (audioElement.paused) {
|
||||||
audioElement.play().catch(console.error)
|
audioElement.play().catch(console.error);
|
||||||
} else {
|
} else {
|
||||||
audioElement.pause()
|
audioElement.pause();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleNext() {
|
function toggleNext() {
|
||||||
if (!state.activeSongs.value || !state.currentSong.value) return;
|
if (!state.activeSongs.value || !state.currentSong.value) return;
|
||||||
|
|
||||||
const songs = state.activeSongs.value;
|
const songs = state.activeSongs.value;
|
||||||
|
|
||||||
if(state.shuffle.value){
|
if (state.shuffle.value) {
|
||||||
setSong(songs[Math.floor(Math.random() * songs.length)]);
|
setSong(songs[Math.floor(Math.random() * songs.length)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentHash = state.currentSong.value.hash;
|
const currentHash = state.currentSong.value.hash;
|
||||||
const currentIndex = songs.findIndex(song => song.hash === currentHash);
|
const currentIndex = songs.findIndex((song) => song.hash === currentHash);
|
||||||
|
|
||||||
if (currentIndex === -1) return;
|
if (currentIndex === -1) return;
|
||||||
|
|
||||||
const nextIndex = (currentIndex + 1) % songs.length;
|
const nextIndex = (currentIndex + 1) % songs.length;
|
||||||
setSong(songs[nextIndex]);
|
setSong(songs[nextIndex]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function togglePrevious() {
|
function togglePrevious() {
|
||||||
if (!state.activeSongs.value || !state.currentSong.value) return;
|
if (!state.activeSongs.value || !state.currentSong.value) return;
|
||||||
|
|
||||||
const songs = state.activeSongs.value;
|
const songs = state.activeSongs.value;
|
||||||
const currentHash = state.currentSong.value.hash;
|
const currentHash = state.currentSong.value.hash;
|
||||||
const currentIndex = songs.findIndex(song => song.hash === currentHash);
|
const currentIndex = songs.findIndex((song) => song.hash === currentHash);
|
||||||
|
|
||||||
if (currentIndex === -1) return;
|
if (currentIndex === -1) return;
|
||||||
|
|
||||||
const prevIndex = (currentIndex - 1 + songs.length) % songs.length;
|
const prevIndex = (currentIndex - 1 + songs.length) % songs.length;
|
||||||
setSong(songs[prevIndex]);
|
setSong(songs[prevIndex]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function update() {
|
function update() {
|
||||||
const { currentTime: ct, duration: dur } = audioElement
|
const { currentTime: ct, duration: dur } = audioElement;
|
||||||
state.isPlaying.value = !audioElement.paused
|
state.isPlaying.value = !audioElement.paused;
|
||||||
state.currentTime.value = formatTime(ct)
|
state.currentTime.value = formatTime(ct);
|
||||||
state.duration.value = formatTime(dur)
|
state.duration.value = formatTime(dur);
|
||||||
state.percentDone.value = isNaN(dur) ? 0 : (ct / dur) * 100
|
state.percentDone.value = isNaN(dur) ? 0 : (ct / dur) * 100;
|
||||||
|
|
||||||
if (audioElement.ended) {
|
if (audioElement.ended) {
|
||||||
if (state.repeat.value) {
|
if (state.repeat.value) {
|
||||||
audioElement.currentTime = 0
|
audioElement.currentTime = 0;
|
||||||
audioElement.play()
|
audioElement.play();
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleNext();
|
toggleNext();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatTime(seconds: number): string {
|
function formatTime(seconds: number): string {
|
||||||
const min = Math.floor(seconds / 60)
|
const min = Math.floor(seconds / 60);
|
||||||
const sec = Math.floor(seconds % 60).toString().padStart(2, '0')
|
const sec = Math.floor(seconds % 60)
|
||||||
return `${min}:${sec}`
|
.toString()
|
||||||
|
.padStart(2, "0");
|
||||||
|
return `${min}:${sec}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateTime(value: number) {
|
function updateTime(value: number) {
|
||||||
if (value) audioElement.currentTime = (Number(value) / 100) * audioElement.duration
|
if (value) audioElement.currentTime = (Number(value) / 100) * audioElement.duration;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadInitialSong() {
|
async function loadInitialSong() {
|
||||||
try {
|
try {
|
||||||
const api = musicApi()
|
const api = musicApi();
|
||||||
const res = await api.musicBackendRecent()
|
const res = await api.musicBackendRecent();
|
||||||
let songs = mapApiToSongs(res.data.songs);
|
let songs = mapApiToSongs(res.data.songs);
|
||||||
if (res.data?.songs?.length) {
|
if (res.data?.songs?.length) {
|
||||||
setSong(songs[0])
|
setSong(songs[0]);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to load song:', err)
|
console.error("Failed to load song:", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
setSong(loadFromLocalStorage<Song>('lastPlayedSong'))
|
setSong(loadFromLocalStorage<Song>("lastPlayedSong"));
|
||||||
setCollection(loadFromLocalStorage<Song[]>('activeCollection'))
|
setCollection(loadFromLocalStorage<Song[]>("activeCollection"));
|
||||||
|
|
||||||
if (!state.currentSong.value) {
|
if (!state.currentSong.value) {
|
||||||
loadInitialSong()
|
loadInitialSong();
|
||||||
}
|
}
|
||||||
|
|
||||||
audioElement.addEventListener('timeupdate', update)
|
audioElement.addEventListener("timeupdate", update);
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(init)
|
onMounted(init);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
@@ -167,12 +162,12 @@ function togglePrevious() {
|
|||||||
setSong,
|
setSong,
|
||||||
setCollection,
|
setCollection,
|
||||||
init,
|
init,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useAudio() {
|
export function useAudio() {
|
||||||
if (!audioInstance) {
|
if (!audioInstance) {
|
||||||
audioInstance = createAudio()
|
audioInstance = createAudio();
|
||||||
}
|
}
|
||||||
return audioInstance
|
return audioInstance;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
import { ref } from 'vue';
|
import { ref } from "vue";
|
||||||
|
|
||||||
export function useThemeColors() {
|
export function useThemeColors() {
|
||||||
const bgColor = ref(localStorage.getItem('bgColor') || '#1c1719');
|
const bgColor = ref(localStorage.getItem("bgColor") || "#1c1719");
|
||||||
const actionColor = ref(localStorage.getItem('actionColor') || '#eab308');
|
const actionColor = ref(localStorage.getItem("actionColor") || "#eab308");
|
||||||
const infoColor = ref(localStorage.getItem('infoColor') || '#ec4899');
|
const infoColor = ref(localStorage.getItem("infoColor") || "#ec4899");
|
||||||
const borderColor = ref(localStorage.getItem('borderColor') || '#ec4899');
|
const borderColor = ref(localStorage.getItem("borderColor") || "#ec4899");
|
||||||
|
|
||||||
function applyColors(bg: string, main: string, info: string, border: string) {
|
function applyColors(bg: string, main: string, info: string, border: string) {
|
||||||
document.documentElement.style.setProperty('--background-color', bg);
|
document.documentElement.style.setProperty("--background-color", bg);
|
||||||
document.documentElement.style.setProperty('--action-color', main);
|
document.documentElement.style.setProperty("--action-color", main);
|
||||||
document.documentElement.style.setProperty('--information-color', info);
|
document.documentElement.style.setProperty("--information-color", info);
|
||||||
document.documentElement.style.setProperty('--border-color', border);
|
document.documentElement.style.setProperty("--border-color", border);
|
||||||
}
|
}
|
||||||
|
|
||||||
function save(
|
function save(
|
||||||
bg: string | null = null,
|
bg: string | null = null,
|
||||||
main: string | null = null,
|
main: string | null = null,
|
||||||
info: string | null = null,
|
info: string | null = null,
|
||||||
border: string | null = null
|
border: string | null = null,
|
||||||
) {
|
) {
|
||||||
bgColor.value = bg ?? bgColor.value;
|
bgColor.value = bg ?? bgColor.value;
|
||||||
actionColor.value = main ?? actionColor.value;
|
actionColor.value = main ?? actionColor.value;
|
||||||
@@ -26,13 +26,12 @@ export function useThemeColors() {
|
|||||||
|
|
||||||
applyColors(bgColor.value, actionColor.value, infoColor.value, borderColor.value);
|
applyColors(bgColor.value, actionColor.value, infoColor.value, borderColor.value);
|
||||||
|
|
||||||
localStorage.setItem('bgColor', bgColor.value);
|
localStorage.setItem("bgColor", bgColor.value);
|
||||||
localStorage.setItem('actionColor', actionColor.value);
|
localStorage.setItem("actionColor", actionColor.value);
|
||||||
localStorage.setItem('infoColor', infoColor.value);
|
localStorage.setItem("infoColor", infoColor.value);
|
||||||
localStorage.setItem('borderColor', borderColor.value);
|
localStorage.setItem("borderColor", borderColor.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize colors on composable use
|
|
||||||
applyColors(bgColor.value, actionColor.value, infoColor.value, borderColor.value);
|
applyColors(bgColor.value, actionColor.value, infoColor.value, borderColor.value);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
import { ref } from 'vue';
|
import { ref } from "vue";
|
||||||
import type { Me } from '@/script/types';
|
import type { Me } from "@/script/types";
|
||||||
|
|
||||||
let userInstance: ReturnType<typeof createUser> | null = null;
|
let userInstance: ReturnType<typeof createUser> | null = null;
|
||||||
|
|
||||||
function createUser() {
|
function createUser() {
|
||||||
const user = ref<Me | null>(null);
|
const user = ref<Me | null>(null);
|
||||||
const baseUrl = ref(import.meta.env.VITE_BACKEND_URL || 'http://localhost:8080');
|
const cloudflareUrl = ref(import.meta.env.VITE_BACKEND_URL || "http://localhost:8080");
|
||||||
const proxyUrl = ref(import.meta.env.VITE_PROXY_URL || 'https://proxy.illegalesachen.download');
|
const proxyUrl = ref(import.meta.env.VITE_PROXY_URL || "https://proxy.illegalesachen.download");
|
||||||
|
|
||||||
function saveUser(u: Me | null) {
|
function saveUser(u: Me | null) {
|
||||||
localStorage.setItem('activeUser', JSON.stringify(u));
|
localStorage.setItem("activeUser", JSON.stringify(u));
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadUser(): Me | null {
|
function loadUser(): Me | null {
|
||||||
const u = localStorage.getItem('activeUser');
|
const u = localStorage.getItem("activeUser");
|
||||||
return u ? JSON.parse(u) : null;
|
return u ? JSON.parse(u) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,12 +25,12 @@ function createUser() {
|
|||||||
async function fetchMe(): Promise<Me | {}> {
|
async function fetchMe(): Promise<Me | {}> {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${proxyUrl.value}/me`, {
|
const response = await fetch(`${proxyUrl.value}/me`, {
|
||||||
method: 'GET',
|
method: "GET",
|
||||||
credentials: 'include',
|
credentials: "include",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.redirected) {
|
if (response.redirected) {
|
||||||
window.open(response.url, '_blank');
|
window.open(response.url, "_blank");
|
||||||
return { redirected: true };
|
return { redirected: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ function createUser() {
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Fetch error:', error);
|
console.error("Fetch error:", error);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -51,7 +51,7 @@ function createUser() {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
user,
|
user,
|
||||||
baseUrl,
|
cloudflareUrl,
|
||||||
proxyUrl,
|
proxyUrl,
|
||||||
setUser,
|
setUser,
|
||||||
fetchMe,
|
fetchMe,
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import './assets/main.css'
|
import "./assets/main.css";
|
||||||
|
|
||||||
import { createApp } from 'vue'
|
import { createApp } from "vue";
|
||||||
|
|
||||||
import App from './App.vue'
|
import App from "./App.vue";
|
||||||
import router from './router'
|
import router from "./router";
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App);
|
||||||
|
|
||||||
app.use(router)
|
app.use(router);
|
||||||
|
|
||||||
app.mount('#app')
|
app.mount("#app");
|
||||||
|
|||||||
@@ -1,32 +1,31 @@
|
|||||||
/* eslint-disable no-console */
|
/* eslint-disable no-console */
|
||||||
|
|
||||||
import { register } from 'register-service-worker'
|
import { register } from "register-service-worker";
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'production') {
|
if (process.env.NODE_ENV === "production") {
|
||||||
register(`${process.env.BASE_URL}service-worker.js`, {
|
register(`${process.env.BASE_URL}service-worker.js`, {
|
||||||
ready () {
|
ready() {
|
||||||
console.log(
|
console.log(
|
||||||
'App is being served from cache by a service worker.\n' +
|
"App is being served from cache by a service worker.\n" + "For more details, visit https://goo.gl/AFskqB",
|
||||||
'For more details, visit https://goo.gl/AFskqB'
|
);
|
||||||
)
|
|
||||||
},
|
},
|
||||||
registered () {
|
registered() {
|
||||||
console.log('Service worker has been registered.')
|
console.log("Service worker has been registered.");
|
||||||
},
|
},
|
||||||
cached () {
|
cached() {
|
||||||
console.log('Content has been cached for offline use.')
|
console.log("Content has been cached for offline use.");
|
||||||
},
|
},
|
||||||
updatefound () {
|
updatefound() {
|
||||||
console.log('New content is downloading.')
|
console.log("New content is downloading.");
|
||||||
},
|
},
|
||||||
updated () {
|
updated() {
|
||||||
console.log('New content is available; please refresh.')
|
console.log("New content is available; please refresh.");
|
||||||
},
|
},
|
||||||
offline () {
|
offline() {
|
||||||
console.log('No internet connection found. App is running in offline mode.')
|
console.log("No internet connection found. App is running in offline mode.");
|
||||||
},
|
},
|
||||||
error (error) {
|
error(error) {
|
||||||
console.error('Error during service worker registration:', error)
|
console.error("Error during service worker registration:", error);
|
||||||
}
|
},
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,69 +1,69 @@
|
|||||||
import { createRouter, createWebHistory } from 'vue-router'
|
import { createRouter, createWebHistory } from "vue-router";
|
||||||
import MenuView from '../views/MenuView.vue'
|
import MenuView from "../views/MenuView.vue";
|
||||||
import RecentView from '../views/RecentView.vue'
|
import RecentView from "../views/RecentView.vue";
|
||||||
import FavouritView from '../views/FavouritView.vue'
|
import FavouritView from "../views/FavouritView.vue";
|
||||||
import CollectionView from '../views/CollectionView.vue'
|
import CollectionView from "../views/CollectionView.vue";
|
||||||
import NowPlayingView from '../views/NowPlayingView.vue'
|
import NowPlayingView from "../views/NowPlayingView.vue";
|
||||||
import MeView from '../views/MeView.vue'
|
import MeView from "../views/MeView.vue";
|
||||||
import SearchView from '../views/SearchView.vue'
|
import SearchView from "../views/SearchView.vue";
|
||||||
import CollectionItem from '../components/CollectionItem.vue'
|
import CollectionItem from "../components/CollectionItem.vue";
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(import.meta.env.BASE_URL),
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
routes: [
|
routes: [
|
||||||
{
|
{
|
||||||
path: '/',
|
path: "/",
|
||||||
name: '',
|
name: "",
|
||||||
component: MeView,
|
component: MeView,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/menu',
|
path: "/menu",
|
||||||
name: 'menu',
|
name: "menu",
|
||||||
component: MenuView,
|
component: MenuView,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: '',
|
path: "",
|
||||||
name: 'default',
|
name: "default",
|
||||||
component: RecentView
|
component: RecentView,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'recent',
|
path: "recent",
|
||||||
name: 'recent',
|
name: "recent",
|
||||||
component: RecentView
|
component: RecentView,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'favourites',
|
path: "favourites",
|
||||||
name: 'favourites',
|
name: "favourites",
|
||||||
component: FavouritView
|
component: FavouritView,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'collections',
|
path: "collections",
|
||||||
name: 'collections',
|
name: "collections",
|
||||||
component: CollectionView
|
component: CollectionView,
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/nowplaying',
|
path: "/nowplaying",
|
||||||
name: 'nowplaying',
|
name: "nowplaying",
|
||||||
component: NowPlayingView
|
component: NowPlayingView,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/me',
|
path: "/me",
|
||||||
name: 'me',
|
name: "me",
|
||||||
component: MeView
|
component: MeView,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/search',
|
path: "/search",
|
||||||
name: 'search',
|
name: "search",
|
||||||
component: SearchView
|
component: SearchView,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/collection/:id',
|
path: "/collection/:id",
|
||||||
name: 'collection',
|
name: "collection",
|
||||||
component: CollectionItem
|
component: CollectionItem,
|
||||||
}
|
},
|
||||||
]
|
],
|
||||||
})
|
});
|
||||||
|
|
||||||
export default router
|
export default router;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { Apiv1Song, v1CollectionPreview, v1Collection } from '@/generated';
|
import type { Apiv1Song, v1CollectionPreview, v1Collection } from "@/generated";
|
||||||
|
|
||||||
export type Song = {
|
export type Song = {
|
||||||
hash: string;
|
hash: string;
|
||||||
@@ -10,7 +10,7 @@ export type Song = {
|
|||||||
mapper: string;
|
mapper: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const basePath = import.meta.env.BACKEND_URL || 'http://localhost:8080';
|
const basePath = import.meta.env.BACKEND_URL || "http://localhost:8080";
|
||||||
|
|
||||||
export function mapToSong(apiSong: Apiv1Song): Song {
|
export function mapToSong(apiSong: Apiv1Song): Song {
|
||||||
const image = apiSong.image;
|
const image = apiSong.image;
|
||||||
@@ -21,10 +21,8 @@ export function mapToSong(apiSong: Apiv1Song): Song {
|
|||||||
name: apiSong.title,
|
name: apiSong.title,
|
||||||
artist: apiSong.artist,
|
artist: apiSong.artist,
|
||||||
length: Number(apiSong.totalTime),
|
length: Number(apiSong.totalTime),
|
||||||
url: `${basePath}/api/v1/audio/${btoa(apiSong.folder + "/" + apiSong.audio).replace(/=+$/, '')}`,
|
url: `${basePath}/api/v1/audio/${btoa(apiSong.folder + "/" + apiSong.audio).replace(/=+$/, "")}`,
|
||||||
previewimage: imageIsMissing
|
previewimage: imageIsMissing ? "/404.gif" : `${basePath}/api/v1/image/${btoa(image).replace(/=+$/, "")}`,
|
||||||
? "/404.gif"
|
|
||||||
: `${basePath}/api/v1/image/${btoa(image).replace(/=+$/, '')}`,
|
|
||||||
mapper: apiSong.creator,
|
mapper: apiSong.creator,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -40,32 +38,25 @@ export type Collection = {
|
|||||||
name: string;
|
name: string;
|
||||||
items: number;
|
items: number;
|
||||||
songs: Song[];
|
songs: Song[];
|
||||||
}
|
};
|
||||||
|
|
||||||
export function mapApiToCollection(coll : v1Collection): Collection {
|
|
||||||
|
|
||||||
|
export function mapApiToCollection(coll: v1Collection): Collection {
|
||||||
return {
|
return {
|
||||||
name: coll.name,
|
name: coll.name,
|
||||||
items: coll.items,
|
items: coll.items,
|
||||||
songs: mapApiToSongs(coll.songs)
|
songs: mapApiToSongs(coll.songs),
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function mapToCollectionPreview(apiCollection: v1CollectionPreview, index: number): CollectionPreview {
|
||||||
export function mapToCollectionPreview(
|
|
||||||
apiCollection: v1CollectionPreview,
|
|
||||||
index: number
|
|
||||||
): CollectionPreview {
|
|
||||||
const image = apiCollection.image;
|
const image = apiCollection.image;
|
||||||
const imageIsMissing = !image || image === "404.png";
|
const imageIsMissing = !image || image === "404.png";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
index,
|
index: index,
|
||||||
name: apiCollection.name,
|
name: apiCollection.name,
|
||||||
length: apiCollection.items,
|
length: apiCollection.items,
|
||||||
previewimage: imageIsMissing
|
previewimage: imageIsMissing ? "/404.gif" : `${basePath}/api/v1/image/${btoa(image).replace(/=+$/, "")}`,
|
||||||
? "/404.gif"
|
|
||||||
: `${basePath}/api/v1/image/${btoa(image).replace(/=+$/, '')}`,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,8 +72,6 @@ export function mapApiToSongs(apiSongs: Apiv1Song[]): Song[] {
|
|||||||
return apiSongs.map(mapToSong);
|
return apiSongs.map(mapToSong);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mapApiToCollectionPreview(
|
export function mapApiToCollectionPreview(apiCollections: v1CollectionPreview[], offset: number): CollectionPreview[] {
|
||||||
apiCollections: v1CollectionPreview[]
|
return apiCollections.map((c, i) => mapToCollectionPreview(c, i + offset));
|
||||||
): CollectionPreview[] {
|
|
||||||
return apiCollections.map((c, i) => mapToCollectionPreview(c, i));
|
|
||||||
}
|
}
|
||||||
@@ -1,13 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { type Song, type CollectionPreview, mapApiToCollectionPreview } from '../script/types'
|
import { type Song, type CollectionPreview, mapApiToCollectionPreview } from "../script/types";
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from "vue";
|
||||||
import CollectionListItem from '../components/CollectionListItem.vue'
|
import CollectionListItem from "../components/CollectionListItem.vue";
|
||||||
import { useUser } from '@/composables/useUser';
|
import { useApi } from "@/composables/useApi";
|
||||||
import { useApi } from '@/composables/useApi';
|
|
||||||
|
|
||||||
const userStore = useUser();
|
const { musicApi } = useApi();
|
||||||
const { musicApi } = useApi()
|
const api = musicApi.value;
|
||||||
const api = musicApi()
|
|
||||||
|
|
||||||
const collections = ref<CollectionPreview[]>([]);
|
const collections = ref<CollectionPreview[]>([]);
|
||||||
const limit = ref(10);
|
const limit = ref(10);
|
||||||
@@ -19,7 +17,7 @@ const fetchCollections = async () => {
|
|||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
|
|
||||||
const response = await api.musicBackendSearchCollections("", limit.value, offset.value);
|
const response = await api.musicBackendSearchCollections("", limit.value, offset.value);
|
||||||
let songs = mapApiToCollectionPreview(response.data.collections)
|
let songs = mapApiToCollectionPreview(response.data.collections || [], offset.value);
|
||||||
collections.value = [...collections.value, ...songs];
|
collections.value = [...collections.value, ...songs];
|
||||||
offset.value += limit.value;
|
offset.value += limit.value;
|
||||||
|
|
||||||
@@ -29,9 +27,9 @@ const fetchCollections = async () => {
|
|||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await fetchCollections();
|
await fetchCollections();
|
||||||
|
|
||||||
const container = document.querySelector('.collection-container');
|
const container = document.querySelector(".collection-container");
|
||||||
if (container) {
|
if (container) {
|
||||||
container.addEventListener('scroll', async () => {
|
container.addEventListener("scroll", async () => {
|
||||||
const scrollTop = container.scrollTop;
|
const scrollTop = container.scrollTop;
|
||||||
const scrollHeight = container.scrollHeight;
|
const scrollHeight = container.scrollHeight;
|
||||||
const clientHeight = container.clientHeight;
|
const clientHeight = container.clientHeight;
|
||||||
@@ -42,11 +40,10 @@ onMounted(async () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<main class="flex-1 text-center flex flex-col h-full overflow-y-hidden">
|
<main class="flex flex-col flex-1 h-full overflow-y-hidden text-center">
|
||||||
<div class="flex flex-col overflow-y-scroll collection-container">
|
<div class="flex flex-col overflow-y-scroll collection-container">
|
||||||
<CollectionListItem v-for="(collection, index) in collections" :key="index" :collection="collection" />
|
<CollectionListItem v-for="(collection, index) in collections" :key="index" :collection="collection" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import SongItem from '../components/SongItem.vue'
|
import SongItem from "../components/SongItem.vue";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<template>
|
<template>
|
||||||
|
|
||||||
<main class="flex-1 flex-col">
|
<main class="flex-1 flex-col">
|
||||||
<div class="flex-1 flex-col h-full overflow-y-hidden song-container">
|
<div class="flex-1 flex-col h-full overflow-y-hidden song-container">
|
||||||
<p class="p-1 rounded-full backdrop--light shadow-xl text-center">History</p>
|
<p class="p-1 rounded-full backdrop--light shadow-xl text-center">History</p>
|
||||||
@@ -10,12 +9,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue';
|
import { computed } from "vue";
|
||||||
import SongItem from '../components/SongItem.vue'
|
import SongItem from "../components/SongItem.vue";
|
||||||
|
|
||||||
import { useAudio } from '@/composables/useAudio';
|
import { useAudio } from "@/composables/useAudio";
|
||||||
const audioStore = useAudio();
|
const audioStore = useAudio();
|
||||||
|
|
||||||
const songs = computed(() => Array.from(audioStore.recentlyPlayed.value?.values()).reverse() || [])
|
const songs = computed(() => Array.from(audioStore.recentlyPlayed.value?.values()).reverse() || []);
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
@@ -1,185 +1,233 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from "vue";
|
||||||
import SongItem from '../components/SongItem.vue'
|
import SongItem from "../components/SongItem.vue";
|
||||||
import { useAudio } from '@/composables/useAudio';
|
import { useAudio } from "@/composables/useAudio";
|
||||||
import { useUser } from '@/composables/useUser';
|
import { useUser } from "@/composables/useUser";
|
||||||
import type { Me } from '@/script/types';
|
import type { Me } from "@/script/types";
|
||||||
|
|
||||||
const audioStore = useAudio();
|
const audioStore = useAudio();
|
||||||
const userStore = useUser();
|
const userStore = useUser();
|
||||||
|
|
||||||
const bgColor = ref('');
|
const bgColor = ref("");
|
||||||
const actionColor = ref('');
|
const actionColor = ref("");
|
||||||
const infoColor = ref('');
|
const infoColor = ref("");
|
||||||
const borderColor = ref('');
|
const borderColor = ref("");
|
||||||
|
|
||||||
const loginStatus = ref('Login');
|
const loginStatus = ref("Login");
|
||||||
|
|
||||||
function update() {
|
function update() {
|
||||||
var input = document.getElementById("url-input") as HTMLInputElement;
|
var input = document.getElementById("url-input") as HTMLInputElement;
|
||||||
userStore.baseUrl.value = input.value;
|
userStore.cloudflareUrl.value = input.value;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function save(bg: string | null, main: string | null, info: string | null, border: string | null) {
|
function save(bg: string | null, main: string | null, info: string | null, border: string | null) {
|
||||||
|
document.documentElement.style.setProperty("--background-color", bg ?? bgColor.value);
|
||||||
|
document.documentElement.style.setProperty("--action-color", main ?? actionColor.value);
|
||||||
|
document.documentElement.style.setProperty("--information-color", info ?? infoColor.value);
|
||||||
|
document.documentElement.style.setProperty("--border-color", border ?? borderColor.value);
|
||||||
|
|
||||||
document.documentElement.style.setProperty('--background-color', bg ?? bgColor.value);
|
localStorage.setItem("bgColor", bg ?? bgColor.value);
|
||||||
document.documentElement.style.setProperty('--action-color', main ?? actionColor.value);
|
localStorage.setItem("actionColor", main ?? actionColor.value);
|
||||||
document.documentElement.style.setProperty('--information-color', info ?? infoColor.value);
|
localStorage.setItem("infoColor", info ?? infoColor.value);
|
||||||
document.documentElement.style.setProperty('--border-color', border ?? borderColor.value);
|
localStorage.setItem("borderColor", border ?? borderColor.value);
|
||||||
|
|
||||||
localStorage.setItem('bgColor', bg ?? bgColor.value);
|
|
||||||
localStorage.setItem('actionColor', main ?? actionColor.value);
|
|
||||||
localStorage.setItem('infoColor', info ?? infoColor.value);
|
|
||||||
localStorage.setItem('borderColor', border ?? borderColor.value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getMe() {
|
async function getMe() {
|
||||||
|
const data = (await userStore.fetchMe()) as Me;
|
||||||
const data = await userStore.fetchMe() as Me;
|
|
||||||
if (data.redirected == true) {
|
if (data.redirected == true) {
|
||||||
loginStatus.value = "waiting for login, click to refresh!"
|
loginStatus.value = "waiting for login, click to refresh!";
|
||||||
console.log("redirect detected");
|
console.log("redirect detected");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.id === null || data.id === undefined || Object.keys(data).length === 0) {
|
if (data.id === null || data.id === undefined || Object.keys(data).length === 0) {
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
userStore.setUser(data);
|
userStore.setUser(data);
|
||||||
userStore.baseUrl.value(data.endpoint);
|
userStore.cloudflareUrl.value(data.endpoint);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
reset();
|
reset();
|
||||||
})
|
});
|
||||||
|
|
||||||
function reset() {
|
function reset() {
|
||||||
|
bgColor.value = localStorage.getItem("bgColor") || "#1c1719";
|
||||||
|
actionColor.value = localStorage.getItem("actionColor") || "#eab308";
|
||||||
|
infoColor.value = localStorage.getItem("infoColor") || "#ec4899";
|
||||||
|
borderColor.value = localStorage.getItem("borderColor") || "#ec4899";
|
||||||
|
|
||||||
bgColor.value = localStorage.getItem('bgColor') || '#1c1719';
|
document.documentElement.style.setProperty("--background-color", bgColor.value);
|
||||||
actionColor.value = localStorage.getItem('actionColor') || '#eab308';
|
document.documentElement.style.setProperty("--action-color", actionColor.value);
|
||||||
infoColor.value = localStorage.getItem('infoColor') || '#ec4899';
|
document.documentElement.style.setProperty("--information-color", infoColor.value);
|
||||||
borderColor.value = localStorage.getItem('borderColor') || '#ec4899';
|
document.documentElement.style.setProperty("--border-color", borderColor.value);
|
||||||
|
|
||||||
document.documentElement.style.setProperty('--background-color', bgColor.value);
|
|
||||||
document.documentElement.style.setProperty('--action-color', actionColor.value);
|
|
||||||
document.documentElement.style.setProperty('--information-color', infoColor.value);
|
|
||||||
document.documentElement.style.setProperty('--border-color', borderColor.value);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<header>
|
<header>
|
||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
<nav class="flex justify-start my-2 mx-1 space-x-1">
|
<nav class="flex justify-start space-x-1 mx-1 my-2">
|
||||||
<RouterLink class="p-1 rounded-full backdrop--light shadow-xl" to="/"><i class="fa-solid fa-arrow-left"></i>
|
<RouterLink class="shadow-xl backdrop--light p-1 rounded-full" to="/"
|
||||||
|
><i class="fa-arrow-left fa-solid"></i>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
</nav>
|
</nav>
|
||||||
<hr>
|
<hr />
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main class="flex-1 flex flex-col h-full overflow-y-scroll">
|
<main class="flex flex-col flex-1 h-full overflow-y-scroll">
|
||||||
<input @change="update" type="text" id="url-input" :value="userStore.user.value?.endpoint" disabled />
|
<input @change="update" type="text" id="url-input" :value="userStore.user.value?.endpoint" disabled />
|
||||||
<br>
|
<br />
|
||||||
<button v-if="!userStore.user.value" @click="getMe" class="border bordercolor rounded-lg p-0.5">{{ loginStatus }}</button>
|
<button v-if="!userStore.user.value" @click="getMe" class="p-0.5 border rounded-lg bordercolor">
|
||||||
<div v-if="userStore.user.value" class="flex p-5 justify-between">
|
{{ loginStatus }}
|
||||||
<img :src="userStore.user.value.avatar_url" class="w-1/3">
|
</button>
|
||||||
|
<div v-if="userStore.user.value" class="flex justify-between p-5">
|
||||||
|
<img :src="userStore.user.value.avatar_url" class="w-1/3" />
|
||||||
<div>
|
<div>
|
||||||
<p>{{ userStore.user.value.name }}</p>
|
<p>{{ userStore.user.value.name }}</p>
|
||||||
<p>{{ userStore.user.value.endpoint == "" ? 'Not Connected' : 'Connected' }}</p>
|
<p>
|
||||||
<p>Sharing: <button @click="userStore.user.value.share" class="border bordercolor rounded-lg p-0.5">{{ userStore.user.value.share
|
{{ userStore.user.value.endpoint == "" ? "Not Connected" : "Connected" }}
|
||||||
}}</button></p>
|
</p>
|
||||||
<button @click="getMe" class="border bordercolor rounded-lg p-0.5"> Refresh
|
<p>
|
||||||
|
Sharing:
|
||||||
|
<button
|
||||||
|
@click="userStore.user.value.share = !userStore.user.value.share"
|
||||||
|
class="p-0.5 border rounded-lg bordercolor"
|
||||||
|
>
|
||||||
|
{{ userStore.user.value.share }}
|
||||||
</button>
|
</button>
|
||||||
|
</p>
|
||||||
|
<button @click="getMe" class="p-0.5 border rounded-lg bordercolor">Refresh</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col w-full justify-around p-10">
|
<div class="flex flex-col justify-around p-10 w-full">
|
||||||
<div class="flex flex-1 justify-between">
|
<div class="flex flex-1 justify-between">
|
||||||
<p>Background:</p>
|
<p>Background:</p>
|
||||||
<input type="color" id="bgPicker" v-model="bgColor" @input="save()"
|
<input
|
||||||
class="appearance-none w-8 h-8 border-2 p-0 overflow-hidden cursor-pointer">
|
type="color"
|
||||||
|
id="bgPicker"
|
||||||
|
v-model="bgColor"
|
||||||
|
@input="save(null, null, null, null)"
|
||||||
|
class="p-0 border-2 w-8 h-8 overflow-hidden appearance-none cursor-pointer"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-1 justify-between">
|
<div class="flex flex-1 justify-between">
|
||||||
<p>Main:</p>
|
<p>Main:</p>
|
||||||
<input type="color" id="actionPicker" v-model="actionColor" @input="save()"
|
<input
|
||||||
class="appearance-none w-8 h-8 border-2 p-0 overflow-hidden cursor-pointer">
|
type="color"
|
||||||
|
id="actionPicker"
|
||||||
|
v-model="actionColor"
|
||||||
|
@input="save(null, null, null, null)"
|
||||||
|
class="p-0 border-2 w-8 h-8 overflow-hidden appearance-none cursor-pointer"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-1 justify-between">
|
<div class="flex flex-1 justify-between">
|
||||||
<p>Secondary:</p>
|
<p>Secondary:</p>
|
||||||
<input type="color" id="infoPicker" v-model="infoColor" @input="save()"
|
<input
|
||||||
class="appearance-none w-8 h-8 border-2 p-0 overflow-hidden cursor-pointer">
|
type="color"
|
||||||
|
id="infoPicker"
|
||||||
|
v-model="infoColor"
|
||||||
|
@input="save(null, null, null, null)"
|
||||||
|
class="p-0 border-2 w-8 h-8 overflow-hidden appearance-none cursor-pointer"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-1 justify-between">
|
<div class="flex flex-1 justify-between">
|
||||||
<p>Border:</p>
|
<p>Border:</p>
|
||||||
<input type="color" id="borderPicker" v-model="borderColor" @input="save()"
|
<input
|
||||||
class="appearance-none w-8 h-8 border-2 p-0 overflow-hidden cursor-pointer">
|
type="color"
|
||||||
|
id="borderPicker"
|
||||||
|
v-model="borderColor"
|
||||||
|
@input="save(null, null, null, null)"
|
||||||
|
class="p-0 border-2 w-8 h-8 overflow-hidden appearance-none cursor-pointer"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full p-2">
|
<div class="p-2 w-full">
|
||||||
<p>Current</p>
|
<p>Current</p>
|
||||||
<SongItem :song="audioStore.currentSong.value" />
|
<SongItem :song="audioStore.currentSong.value" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-full p-2 bg-black">
|
<div class="bg-black p-2 w-full">
|
||||||
<p class="flex-1 flex justify-between" style=" color : #57db5d">StaryNight <button style="border-color : #b3002d"
|
<p class="flex flex-1 justify-between" style="color: #57db5d">
|
||||||
class="border rounded-lg p-0.5" @click="save('#000000', '#5e2d8f', '#57db5d', '#b3002d')">Choose
|
StaryNight
|
||||||
|
<button
|
||||||
|
style="border-color: #b3002d"
|
||||||
|
class="p-0.5 border rounded-lg"
|
||||||
|
@click="save('#000000', '#5e2d8f', '#57db5d', '#b3002d')"
|
||||||
|
>
|
||||||
|
Choose
|
||||||
</button>
|
</button>
|
||||||
</p>
|
</p>
|
||||||
<SongItem :song="audioStore.currentSong.value" :border="'#b3002d'" :action="'#5e2d8f'" :info="'#57db5d'" />
|
<SongItem :song="audioStore.currentSong.value" :border="'#b3002d'" :action="'#5e2d8f'" :info="'#57db5d'" />
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full p-2" style="background-color: #1c1719">
|
<div class="p-2 w-full" style="background-color: #1c1719">
|
||||||
<p class="flex-1 flex justify-between" style=" color : #ec4889">Default<button style="border-color : #ec4889"
|
<p class="flex flex-1 justify-between" style="color: #ec4889">
|
||||||
class="border rounded-lg p-0.5" @click="save('#1c1719', '#eab308', '#ec4889', '#ec4889')">Choose
|
Default<button
|
||||||
|
style="border-color: #ec4889"
|
||||||
|
class="p-0.5 border rounded-lg"
|
||||||
|
@click="save('#1c1719', '#eab308', '#ec4889', '#ec4889')"
|
||||||
|
>
|
||||||
|
Choose
|
||||||
</button>
|
</button>
|
||||||
</p>
|
</p>
|
||||||
<SongItem :song="audioStore.currentSong.value" :border="'#ec4889'" :info="'#ec4889'" :action="'#eab308'" />
|
<SongItem :song="audioStore.currentSong.value" :border="'#ec4889'" :info="'#ec4889'" :action="'#eab308'" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-full p-2" style="background-color: #ff4c4c">
|
<div class="p-2 w-full" style="background-color: #ff4c4c">
|
||||||
<p class="flex-1 flex justify-between" style="color: #ffffff">
|
<p class="flex flex-1 justify-between" style="color: #ffffff">
|
||||||
Bright Sunset
|
Bright Sunset
|
||||||
<button style="border-color: #ffffff" class="border rounded-lg p-0.5"
|
<button
|
||||||
@click="save('#ff4c4c', '#ffcc00', '#ffffff', '#ffffff')">Choose</button>
|
style="border-color: #ffffff"
|
||||||
|
class="p-0.5 border rounded-lg"
|
||||||
|
@click="save('#ff4c4c', '#ffcc00', '#ffffff', '#ffffff')"
|
||||||
|
>
|
||||||
|
Choose
|
||||||
|
</button>
|
||||||
</p>
|
</p>
|
||||||
<SongItem :song="audioStore.currentSong.value" :border="'#ffffff'" :info="'#ffffff'" :action="'#ffcc00'" />
|
<SongItem :song="audioStore.currentSong.value" :border="'#ffffff'" :info="'#ffffff'" :action="'#ffcc00'" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-full p-2" style="background-color: #003d00">
|
<div class="p-2 w-full" style="background-color: #003d00">
|
||||||
<p class="flex-1 flex justify-between" style="color: #e0f8d8">
|
<p class="flex flex-1 justify-between" style="color: #e0f8d8">
|
||||||
Forest Night
|
Forest Night
|
||||||
<button style="border-color: #e0f8d8" class="border rounded-lg p-0.5"
|
<button
|
||||||
@click="save('#003d00', '#a8d5a2', '#e0f8d8', '#e0f8d8')">Choose</button>
|
style="border-color: #e0f8d8"
|
||||||
|
class="p-0.5 border rounded-lg"
|
||||||
|
@click="save('#003d00', '#a8d5a2', '#e0f8d8', '#e0f8d8')"
|
||||||
|
>
|
||||||
|
Choose
|
||||||
|
</button>
|
||||||
</p>
|
</p>
|
||||||
<SongItem :song="audioStore.currentSong.value" :border="'#e0f8d8'" :info="'#e0f8d8'" :action="'#a8d5a2'" />
|
<SongItem :song="audioStore.currentSong.value" :border="'#e0f8d8'" :info="'#e0f8d8'" :action="'#a8d5a2'" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-full p-2" style="background-color: #00274d">
|
<div class="p-2 w-full" style="background-color: #00274d">
|
||||||
<p class="flex-1 flex justify-between" style="color: #00ffff">
|
<p class="flex flex-1 justify-between" style="color: #00ffff">
|
||||||
Electric Blue
|
Electric Blue
|
||||||
<button style="border-color: #00ffff" class="border rounded-lg p-0.5"
|
<button
|
||||||
@click="save('#00274d', '#0099ff', '#00ffff', '#00ffff')">Choose</button>
|
style="border-color: #00ffff"
|
||||||
|
class="p-0.5 border rounded-lg"
|
||||||
|
@click="save('#00274d', '#0099ff', '#00ffff', '#00ffff')"
|
||||||
|
>
|
||||||
|
Choose
|
||||||
|
</button>
|
||||||
</p>
|
</p>
|
||||||
<SongItem :song="audioStore.currentSong.value" :border="'#00ffff'" :info="'#00ffff'" :action="'#0099ff'" />
|
<SongItem :song="audioStore.currentSong.value" :border="'#00ffff'" :info="'#00ffff'" :action="'#0099ff'" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
input[type='color']::-webkit-color-swatch-wrapper {
|
input[type="color"]::-webkit-color-swatch-wrapper {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type='color']::-webkit-color-swatch {
|
input[type="color"]::-webkit-color-swatch {
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from "vue-router";
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
|
||||||
function isActive(path: string) {
|
function isActive(path: string) {
|
||||||
return route.path === path ? 'bg-blue-500 text-white' : '';
|
return route.path === path ? "bg-blue-500 text-white" : "";
|
||||||
};
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -14,16 +13,20 @@ function isActive(path: string) {
|
|||||||
<header v-show="true">
|
<header v-show="true">
|
||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
<nav class="flex justify-start my-2 mx-1 space-x-1 overflow-y-auto overflow-x-auto flex-nowrap text-nowrap">
|
<nav class="flex justify-start my-2 mx-1 space-x-1 overflow-y-auto overflow-x-auto flex-nowrap text-nowrap">
|
||||||
<RouterLink class="p-1 rounded-full backdrop--light shadow-xl" to="/"><i class="fa-solid fa-arrow-left"></i>
|
<RouterLink class="p-1 rounded-full backdrop--light shadow-xl" to="/"
|
||||||
|
><i class="fa-solid fa-arrow-left"></i>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
<RouterLink :class="`p-1 rounded-full backdrop--light shadow-xl ${isActive('/')}`" to="/menu/recent">Recently
|
<RouterLink :class="`p-1 rounded-full backdrop--light shadow-xl ${isActive('/')}`" to="/menu/recent"
|
||||||
added</RouterLink>
|
>Recently added</RouterLink
|
||||||
|
>
|
||||||
<RouterLink :class="`p-1 rounded-full backdrop--light shadow-xl ${isActive('/')}`" to="/menu/favourites">
|
<RouterLink :class="`p-1 rounded-full backdrop--light shadow-xl ${isActive('/')}`" to="/menu/favourites">
|
||||||
Favorites</RouterLink>
|
Favorites</RouterLink
|
||||||
|
>
|
||||||
<RouterLink :class="`p-1 rounded-full backdrop--light shadow-xl ${isActive('/')}`" to="/menu/collections">
|
<RouterLink :class="`p-1 rounded-full backdrop--light shadow-xl ${isActive('/')}`" to="/menu/collections">
|
||||||
Collections</RouterLink>
|
Collections</RouterLink
|
||||||
|
>
|
||||||
</nav>
|
</nav>
|
||||||
<hr>
|
<hr />
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|||||||
@@ -1,62 +1,61 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue';
|
import { computed } from "vue";
|
||||||
import { useAudio } from '@/composables/useAudio';
|
import { useAudio } from "@/composables/useAudio";
|
||||||
|
|
||||||
const audioStore = useAudio();
|
const audioStore = useAudio();
|
||||||
|
|
||||||
const title = computed(() => audioStore.currentSong.value?.name || 'Unknown Title')
|
const title = computed(() => audioStore.currentSong.value?.name || "Unknown Title");
|
||||||
const artist = computed(() => audioStore.currentSong.value?.artist || 'Unknown Artist')
|
const artist = computed(() => audioStore.currentSong.value?.artist || "Unknown Artist");
|
||||||
const bgimg = computed(() => audioStore.currentSong.value?.previewimage || '/default-bg.jpg')
|
const bgimg = computed(() => audioStore.currentSong.value?.previewimage || "/default-bg.jpg");
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<header>
|
<header>
|
||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<nav class="flex flex-1 justify-start my-2 mx-1 space-x-1">
|
<nav class="flex flex-1 justify-start space-x-1 mx-1 my-2">
|
||||||
<RouterLink class="p-1 rounded-full backdrop--light shadow-xl" to="/"><i class="fa-solid fa-arrow-left"></i>
|
<RouterLink class="shadow-xl backdrop--light p-1 rounded-full" to="/"
|
||||||
|
><i class="fa-arrow-left fa-solid"></i>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
<h1 class="absolute left-0 right-0 text-center"> Now Playing </h1>
|
<h1 class="right-0 left-0 absolute text-center">Now Playing</h1>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
<hr>
|
<hr />
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main class="flex-1 flex flex-col items-center justify-center text-center px-4">
|
<main class="flex flex-col flex-1 justify-center items-center px-4 text-center">
|
||||||
<div class="flex flex-col items-center w-full max-w-md space-y-6">
|
<div class="flex flex-col items-center space-y-6 w-full max-w-md">
|
||||||
|
|
||||||
<div class="relative w-full aspect-square">
|
<div class="relative w-full aspect-square">
|
||||||
<img
|
<img
|
||||||
class="absolute inset-0 w-full h-full object-cover rounded-lg shadow-lg"
|
class="absolute inset-0 shadow-lg rounded-lg w-full h-full object-cover"
|
||||||
:src="encodeURI(bgimg + '?h=320&w=320')"
|
:src="encodeURI(bgimg + '?h=320&w=320')"
|
||||||
:key="bgimg"
|
:key="bgimg"
|
||||||
alt="Album Art"
|
alt="Album Art"
|
||||||
/>
|
/>
|
||||||
<i class="absolute inset-0 flex items-center justify-center text-white text-5xl">
|
<i class="absolute inset-0 flex justify-center items-center text-white text-5xl">
|
||||||
<i class="fa-solid fa-play bg-black bg-opacity-50 p-4 rounded-full"></i>
|
<i class="bg-black bg-opacity-50 p-4 rounded-full fa-solid fa-play"></i>
|
||||||
</i>
|
</i>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-between items-center w-full text-3xl space-x-6">
|
<div class="flex justify-between items-center space-x-6 w-full text-3xl">
|
||||||
<i class="fa-solid fa-backward-step" @click="audioStore.togglePrevious"></i>
|
<i class="fa-solid fa-backward-step" @click="audioStore.togglePrevious"></i>
|
||||||
<i
|
<i
|
||||||
:class="[audioStore.isPlaying.value ? 'fa-circle-pause' : 'fa-circle-play']"
|
:class="[audioStore.isPlaying.value ? 'fa-circle-pause' : 'fa-circle-play']"
|
||||||
class="fa-regular text-5xl"
|
class="text-5xl fa-regular"
|
||||||
@click="audioStore.togglePlay"
|
@click="audioStore.togglePlay"
|
||||||
></i>
|
></i>
|
||||||
<i class="fa-solid fa-forward-step" @click="audioStore.toggleNext"></i>
|
<i class="fa-solid fa-forward-step" @click="audioStore.toggleNext"></i>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="text-center w-full px-2">
|
<div class="px-2 w-full text-center">
|
||||||
<p class="truncate text-lg font-semibold">{{ title }}</p>
|
<p class="font-semibold text-lg truncate">{{ title }}</p>
|
||||||
<RouterLink :to="'search?a=' + artist" class="block text-sm text-blue-500 truncate">
|
<RouterLink :to="'search?a=' + artist" class="block text-blue-500 text-sm truncate">
|
||||||
{{ artist }}
|
{{ artist }}
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-between items-center w-full px-4">
|
<div class="flex justify-between items-center px-4 w-full">
|
||||||
<i
|
<i
|
||||||
@click="audioStore.toggleShuffle"
|
@click="audioStore.toggleShuffle"
|
||||||
:class="[audioStore.shuffle.value ? 'text-yellow-500' : '']"
|
:class="[audioStore.shuffle.value ? 'text-yellow-500' : '']"
|
||||||
@@ -70,21 +69,21 @@ const bgimg = computed(() => audioStore.currentSong.value?.previewimage || '/def
|
|||||||
<i @click="$router.go(-1)" class="fa-solid fa-arrow-down"></i>
|
<i @click="$router.go(-1)" class="fa-solid fa-arrow-down"></i>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-full px-4">
|
<div class="px-4 w-full">
|
||||||
<input
|
<input
|
||||||
class="w-full appearance-none h-2 rounded-full bg-yellow-200 bg-opacity-20 accent-yellow-600 outline-none"
|
class="bg-yellow-200 bg-opacity-20 rounded-full outline-none w-full h-2 accent-yellow-600 appearance-none"
|
||||||
type="range"
|
type="range"
|
||||||
@input="event => audioStore.updateTime(Number(event.target.value))"
|
@input="audioStore.updateTime(Number(($event.target as HTMLInputElement).value) || 0)"
|
||||||
:max="100"
|
:max="100"
|
||||||
step="0.001"
|
step="0.001"
|
||||||
:value="audioStore.percentDone.value"
|
:value="audioStore.percentDone.value"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-between text-sm w-full px-4">
|
<div class="flex justify-between px-4 w-full text-sm">
|
||||||
<span>{{ audioStore.currentTime.value }}</span>
|
<span>{{ audioStore.currentTime.value }}</span>
|
||||||
<span>{{ audioStore.duration.value }}</span>
|
<span>{{ audioStore.duration.value }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,21 +1,20 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import SongItem from '../components/SongItem.vue'
|
import SongItem from "../components/SongItem.vue";
|
||||||
|
|
||||||
import { type Song, type CollectionPreview, mapApiToSongs } from '../script/types'
|
import { type Song, type CollectionPreview, mapApiToSongs } from "../script/types";
|
||||||
import { ref, onMounted, nextTick } from 'vue'
|
import { ref, onMounted, nextTick } from "vue";
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from "vue-router";
|
||||||
import { useAudio } from '@/composables/useAudio';
|
import { useAudio } from "@/composables/useAudio";
|
||||||
import { useUser } from '@/composables/useUser';
|
import { useUser } from "@/composables/useUser";
|
||||||
import { useApi } from '@/composables/useApi';
|
import { useApi } from "@/composables/useApi";
|
||||||
|
|
||||||
const userStore = useUser();
|
const userStore = useUser();
|
||||||
const audioStore = useAudio();
|
const audioStore = useAudio();
|
||||||
const { musicApi } = useApi();
|
const { musicApi } = useApi();
|
||||||
const api = musicApi()
|
const api = musicApi.value;
|
||||||
|
|
||||||
|
|
||||||
const songs = ref<Song[]>([]);
|
const songs = ref<Song[]>([]);
|
||||||
const name = ref('name');
|
const name = ref("name");
|
||||||
const containerRef = ref<HTMLElement | null>(null);
|
const containerRef = ref<HTMLElement | null>(null);
|
||||||
|
|
||||||
const limit = ref(100);
|
const limit = ref(100);
|
||||||
@@ -38,11 +37,9 @@ const fetchRecent = async () => {
|
|||||||
audioStore.setCollection(songs.value);
|
audioStore.setCollection(songs.value);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load songs:', error)
|
console.error("Failed to load songs:", error);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await fetchRecent();
|
await fetchRecent();
|
||||||
@@ -51,7 +48,7 @@ onMounted(async () => {
|
|||||||
|
|
||||||
const container = containerRef.value;
|
const container = containerRef.value;
|
||||||
if (container) {
|
if (container) {
|
||||||
container.addEventListener('scroll', async () => {
|
container.addEventListener("scroll", async () => {
|
||||||
const scrollTop = container.scrollTop;
|
const scrollTop = container.scrollTop;
|
||||||
const scrollHeight = container.scrollHeight;
|
const scrollHeight = container.scrollHeight;
|
||||||
const clientHeight = container.clientHeight;
|
const clientHeight = container.clientHeight;
|
||||||
@@ -61,16 +58,11 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div ref="containerRef" class="flex-col flex-1 overflow-y-scroll song-container">
|
||||||
ref="containerRef"
|
|
||||||
class="flex-1 flex-col overflow-y-scroll song-container"
|
|
||||||
>
|
|
||||||
<SongItem v-for="(song, index) in songs" :key="index" :song="song" />
|
<SongItem v-for="(song, index) in songs" :key="index" :song="song" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,103 +1,107 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { mapApiToSongs, mapToSong, type Song } from '../script/types'
|
import { mapApiToSongs, mapToSong, type Song } from "../script/types";
|
||||||
import { ref, onMounted, watch } from 'vue'
|
import { ref, onMounted, watch } from "vue";
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from "vue-router";
|
||||||
import ActiveSearchList from '../components/ActiveSearchList.vue'
|
import ActiveSearchList from "../components/ActiveSearchList.vue";
|
||||||
import SongItem from '../components/SongItem.vue'
|
import SongItem from "../components/SongItem.vue";
|
||||||
import { useAudio } from '@/composables/useAudio'
|
import { useAudio } from "@/composables/useAudio";
|
||||||
import { useUser } from '@/composables/useUser'
|
import { useUser } from "@/composables/useUser";
|
||||||
import { useApi } from '@/composables/useApi'
|
import { useApi } from "@/composables/useApi";
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
const route = useRoute()
|
const route = useRoute();
|
||||||
|
|
||||||
const audioStore = useAudio()
|
const audioStore = useAudio();
|
||||||
const userStore = useUser()
|
const userStore = useUser();
|
||||||
const { musicApi } = useApi()
|
const { musicApi } = useApi();
|
||||||
const api = musicApi()
|
const api = musicApi.value;
|
||||||
|
|
||||||
const activesongs = ref<Song[]>([])
|
const activesongs = ref<Song[]>([]);
|
||||||
const songs = ref<Song[]>([])
|
const songs = ref<Song[]>([]);
|
||||||
const artists = ref<string[]>([])
|
const artists = ref<string[]>([]);
|
||||||
const showSearch = ref(false)
|
const showSearch = ref(false);
|
||||||
const searchTerm = ref('')
|
const searchTerm = ref("");
|
||||||
|
|
||||||
async function fetchActiveSearch(term: string) {
|
async function fetchActiveSearch(term: string) {
|
||||||
const response = await api.musicBackendSearch(term);
|
const response = await api.musicBackendSearch(term);
|
||||||
|
|
||||||
const songData = mapApiToSongs(response.data.songs)
|
const songData = mapApiToSongs(response.data.songs ?? []);
|
||||||
|
|
||||||
activesongs.value = songData
|
activesongs.value = songData;
|
||||||
|
|
||||||
if (response.data.artist) artists.value = [response.data.artist]
|
if (response.data.artist) artists.value = [response.data.artist];
|
||||||
audioStore.setCollection(songData)
|
audioStore.setCollection(songData);
|
||||||
showSearch.value = true
|
showSearch.value = true;
|
||||||
searchTerm.value = term
|
searchTerm.value = term;
|
||||||
router.replace({ query: { s: term } })
|
router.replace({ query: { s: term } });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchSearchArtist(artist: string) {
|
async function fetchSearchArtist(artist: string) {
|
||||||
const response = await api.musicBackendArtist(artist)
|
const response = await api.musicBackendArtist(artist);
|
||||||
|
|
||||||
const data = mapApiToSongs(response.data.songs)
|
const data = mapApiToSongs(response.data.songs || []);
|
||||||
|
|
||||||
data.forEach((song: Song) => {
|
data.forEach((song: Song) => {
|
||||||
song.previewimage = `${userStore.baseUrl.value}/api/v1/images/${song.previewimage}`
|
song.previewimage = `${userStore.cloudflareUrl.value}/api/v1/images/${song.previewimage}`;
|
||||||
song.url = `${userStore.baseUrl.value}/api/v1/audio/${song.url}`
|
song.url = `${userStore.cloudflareUrl.value}/api/v1/audio/${song.url}`;
|
||||||
})
|
});
|
||||||
|
|
||||||
songs.value = data
|
songs.value = data;
|
||||||
showSearch.value = false
|
showSearch.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function emptySearch() {
|
async function emptySearch() {
|
||||||
activesongs.value = []
|
activesongs.value = [];
|
||||||
artists.value = []
|
artists.value = [];
|
||||||
songs.value = []
|
songs.value = [];
|
||||||
showSearch.value = false
|
showSearch.value = false;
|
||||||
searchTerm.value = ''
|
searchTerm.value = "";
|
||||||
router.replace({ query: {} })
|
router.replace({ query: {} });
|
||||||
|
searchInput.value = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (route.query.a) {
|
if (route.query.a) {
|
||||||
await fetchSearchArtist(route.query.a as string)
|
await fetchSearchArtist(route.query.a as string);
|
||||||
}
|
}
|
||||||
if (route.query.s) {
|
if (route.query.s) {
|
||||||
await fetchActiveSearch(route.query.s as string)
|
await fetchActiveSearch(route.query.s as string);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
watch(() => route.query.a, async (newArtist) => {
|
watch(
|
||||||
|
() => route.query.a,
|
||||||
|
async (newArtist) => {
|
||||||
if (newArtist) {
|
if (newArtist) {
|
||||||
await fetchSearchArtist(newArtist as string)
|
await fetchSearchArtist(newArtist as string);
|
||||||
} else {
|
} else {
|
||||||
songs.value = []
|
songs.value = [];
|
||||||
}
|
}
|
||||||
})
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const searchInput = ref(searchTerm.value)
|
const searchInput = ref(searchTerm.value);
|
||||||
|
|
||||||
watch(searchInput, async (val) => {
|
watch(searchInput, async (val) => {
|
||||||
if (val && val.trim() !== '') {
|
if (val && val.trim() !== "") {
|
||||||
await fetchActiveSearch(val)
|
await fetchActiveSearch(val);
|
||||||
} else {
|
} else {
|
||||||
showSearch.value = false
|
showSearch.value = false;
|
||||||
activesongs.value = []
|
activesongs.value = [];
|
||||||
artists.value = []
|
artists.value = [];
|
||||||
router.replace({ query: {} })
|
router.replace({ query: {} });
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<header>
|
<header>
|
||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
<nav class="flex justify-start my-2 mx-1 space-x-1">
|
<nav class="flex justify-start space-x-1 mx-1 my-2">
|
||||||
<RouterLink class="p-1 rounded-full backdrop--light shadow-xl" to="/">
|
<RouterLink class="shadow-xl backdrop--light p-1 rounded-full" to="/">
|
||||||
<i class="fa-solid fa-arrow-left"></i>
|
<i class="fa-arrow-left fa-solid"></i>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
<h1 class="absolute left-0 right-0 text-center">Search</h1>
|
<h1 class="right-0 left-0 absolute text-center">Search</h1>
|
||||||
</nav>
|
</nav>
|
||||||
<hr />
|
<hr />
|
||||||
</div>
|
</div>
|
||||||
@@ -108,15 +112,15 @@ watch(searchInput, async (val) => {
|
|||||||
<input
|
<input
|
||||||
v-model="searchInput"
|
v-model="searchInput"
|
||||||
placeholder="Type to Search..."
|
placeholder="Type to Search..."
|
||||||
class="w-full flex-1 max-h-12 search border bordercolor accent-pink-800 bg-yellow-300 bg-opacity-20 rounded-lg p-2"
|
class="flex-1 bg-yellow-300 bg-opacity-20 p-2 border rounded-lg w-full max-h-12 accent-pink-800 search bordercolor"
|
||||||
/>
|
/>
|
||||||
<div class="absolute top-4 right-4 flex flex-col justify-center cursor-pointer" @click="emptySearch">
|
<div class="top-4 right-4 absolute flex flex-col justify-center cursor-pointer" @click="emptySearch">
|
||||||
<i class="far fa-times-circle opacity-50"></i>
|
<i class="opacity-50 far fa-times-circle"></i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="relative flex flex-col w-full h-full overflow-y-scroll">
|
<div class="relative flex flex-col w-full h-full overflow-y-scroll">
|
||||||
<div v-if="showSearch" class="absolute w-full text-center search-recommendations z-20">
|
<div v-if="showSearch" class="z-20 absolute w-full text-center search-recommendations">
|
||||||
<ActiveSearchList :songs="activesongs" :artist="artists" :search="searchTerm" />
|
<ActiveSearchList :songs="activesongs" :artist="artists" :search="searchTerm" />
|
||||||
</div>
|
</div>
|
||||||
<SongItem v-for="(song, index) in songs" :key="index" :song="song" />
|
<SongItem v-for="(song, index) in songs" :key="index" :song="song" />
|
||||||
|
|||||||
@@ -1,12 +1,6 @@
|
|||||||
{
|
{
|
||||||
"extends": "@tsconfig/node22/tsconfig.json",
|
"extends": "@tsconfig/node22/tsconfig.json",
|
||||||
"include": [
|
"include": ["vite.config.*", "vitest.config.*", "cypress.config.*", "nightwatch.conf.*", "playwright.config.*"],
|
||||||
"vite.config.*",
|
|
||||||
"vitest.config.*",
|
|
||||||
"cypress.config.*",
|
|
||||||
"nightwatch.conf.*",
|
|
||||||
"playwright.config.*"
|
|
||||||
],
|
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"composite": true,
|
"composite": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
|
|||||||
@@ -1,18 +1,15 @@
|
|||||||
import { fileURLToPath, URL } from 'node:url'
|
import { fileURLToPath, URL } from "node:url";
|
||||||
|
|
||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from "vite";
|
||||||
import vue from '@vitejs/plugin-vue'
|
import vue from "@vitejs/plugin-vue";
|
||||||
import vueDevTools from 'vite-plugin-vue-devtools'
|
import vueDevTools from "vite-plugin-vue-devtools";
|
||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [vue(), vueDevTools()],
|
||||||
vue(),
|
|
||||||
vueDevTools(),
|
|
||||||
],
|
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
"@": fileURLToPath(new URL("./src", import.meta.url)),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user