minor improvements

This commit is contained in:
2026-03-12 19:20:46 +01:00
parent 7c61a3ca57
commit 8a2ec44a32
34 changed files with 787 additions and 785 deletions

3
frontend/.prettierrc Normal file
View File

@@ -0,0 +1,3 @@
{
"printWidth": 120
}

View File

@@ -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"]
}
}
},

View File

@@ -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>

View File

@@ -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",

View File

@@ -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"
},

View File

@@ -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,7 +66,7 @@ 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>

View File

@@ -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;
}
}

View File

@@ -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%);
}

View File

@@ -1,4 +1,4 @@
@import './base.css';
@import "./base.css";
a,
.green {

View File

@@ -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>

View File

@@ -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>
</main>
</template>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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,
};
}

View File

@@ -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;
}

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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");

View File

@@ -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);
},
});
}

View File

@@ -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;

View File

@@ -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));
}

View File

@@ -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;
};

View File

@@ -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>

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import SongItem from '../components/SongItem.vue'
import SongItem from "../components/SongItem.vue";
</script>
<template>

View File

@@ -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() || [])
const songs = computed(() => Array.from(audioStore.recentlyPlayed.value?.values()).reverse() || []);
</script>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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,22 +26,20 @@ 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();
@@ -51,7 +48,7 @@ onMounted(async () => {
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>

View File

@@ -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)
const songData = mapApiToSongs(response.data.songs ?? []);
activesongs.value = songData
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 } })
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 response = await api.musicBackendArtist(artist);
const data = mapApiToSongs(response.data.songs)
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}`
})
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
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" />

View File

@@ -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,

View File

@@ -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)),
},
},
})
});