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