Inbäddad mini-spelare: ▶ på korten spelar direkt i appen
All checks were successful
build-and-push / build (push) Successful in 14s

- GET /api/play?q=: slår upp spelbart videoId via YouTube Data API
  (kategori musik, YOUTUBE_API_KEY i stackens .env)
- MiniPlayer: fast spelare nere till höger (YouTube-embed, autoplay),
  ligger kvar medan man bläddrar; stängs med ✕
- MusicCard: ▶-knapp på artist/album/låt/sångare (genrer behåller sina
  två knappar); YT-länken heter nu 'YT ↗'
- 2 nya tester, totalt 31

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01824ZrvG2mDYYrmwqypLNup
This commit is contained in:
2026-08-01 04:38:33 +02:00
parent 4e89bd1986
commit 9874d383ea
7 changed files with 174 additions and 8 deletions

View File

@@ -1,6 +1,7 @@
<script setup>
import { ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import MiniPlayer from './components/MiniPlayer.vue'
const route = useRoute()
const router = useRouter()
@@ -40,6 +41,7 @@ async function logout() {
<main>
<router-view />
</main>
<MiniPlayer />
</template>
<style scoped>

View File

@@ -0,0 +1,64 @@
<script setup>
import { nowPlaying } from '../player.js'
</script>
<template>
<div v-if="nowPlaying" class="player">
<div class="bar">
<span class="title" :title="nowPlaying.title"> {{ nowPlaying.title }}</span>
<button class="close" @click="nowPlaying = null"></button>
</div>
<iframe
:src="`https://www.youtube.com/embed/${nowPlaying.videoId}?autoplay=1`"
title="Spelare"
frameborder="0"
allow="autoplay; encrypted-media; picture-in-picture"
allowfullscreen
></iframe>
</div>
</template>
<style scoped>
.player {
position: fixed;
right: 1rem;
bottom: 1rem;
z-index: 100;
width: min(24rem, calc(100vw - 2rem));
background: var(--bg-deep);
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 0.8rem;
overflow: hidden;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
}
.bar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.6rem;
padding: 0.4rem 0.4rem 0.4rem 0.8rem;
}
.title {
font-size: 0.85rem;
color: var(--text-dim);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.close {
background: transparent;
color: var(--text-dim);
padding: 0.15rem 0.5rem;
flex: 0 0 auto;
}
iframe {
display: block;
width: 100%;
aspect-ratio: 16 / 9;
border: 0;
}
</style>

View File

@@ -1,17 +1,32 @@
<script setup>
import { computed, ref } from 'vue'
import { nowPlaying } from '../player.js'
const props = defineProps({ item: { type: Object, required: true } })
const emit = defineEmits(['detail'])
const state = ref('') // '' | 'busy' | 'done' | 'fanns' | 'fel'
const playBusy = ref(false)
const ytUrl = computed(() => {
const q =
props.item.kind === 'artist'
? props.item.name
: `${props.item.secondary ?? ''} ${props.item.name}`.trim()
return `https://music.youtube.com/search?q=${encodeURIComponent(q)}`
})
const ytQuery = computed(() =>
props.item.kind === 'artist'
? props.item.name
: `${props.item.secondary ?? ''} ${props.item.name}`.trim(),
)
const ytUrl = computed(() => `https://music.youtube.com/search?q=${encodeURIComponent(ytQuery.value)}`)
async function play() {
playBusy.value = true
try {
const res = await fetch(`/api/play?q=${encodeURIComponent(ytQuery.value)}`)
const body = await res.json()
if (res.ok && body.videoId) {
nowPlaying.value = { videoId: body.videoId, title: body.title || props.item.name }
}
} finally {
playBusy.value = false
}
}
async function grab() {
state.value = 'busy'
@@ -64,7 +79,16 @@ async function grab() {
{{ state === 'busy' ? 'Lägger till' : state === 'fel' ? 'Försök igen' : ' Hämta' }}
</button>
</template>
<a class="yt" :href="ytUrl" target="_blank" rel="noopener" title="Öppna i YouTube Music"> YT</a>
<button
v-if="item.kind !== 'genre'"
class="play"
:disabled="playBusy"
title="Spela direkt i mini-spelaren"
@click="play"
>
{{ playBusy ? '…' : '▶' }}
</button>
<a class="yt" :href="ytUrl" target="_blank" rel="noopener" title="Öppna i YouTube Music">YT </a>
</div>
<button v-if="item.kind === 'singer'" class="more" @click="emit('detail', item)">
Topplåtar & liknande
@@ -184,6 +208,16 @@ async function grab() {
background: rgba(255, 60, 60, 0.12);
}
.play {
flex: 0 0 auto;
padding: 0.55rem 0.75rem;
background: rgba(255, 60, 60, 0.85);
}
.play:hover {
background: rgba(255, 60, 60, 1);
}
.more {
background: rgba(99, 102, 241, 0.15);
color: var(--accent-hover);

4
frontend/src/player.js Normal file
View File

@@ -0,0 +1,4 @@
import { ref } from 'vue'
// Delat mini-spelar-tillstånd: {videoId, title} eller null
export const nowPlaying = ref(null)