feat: add basic media player
This commit is contained in:
parent
653dff41dc
commit
6cb1c9d29c
19 changed files with 344 additions and 154 deletions
|
@ -15,7 +15,7 @@
|
||||||
<Dashboard />
|
<Dashboard />
|
||||||
</div>
|
</div>
|
||||||
<!-- the infotainment system -->
|
<!-- the infotainment system -->
|
||||||
<div class="h-screen w-[65vw] right-0 fixed infotainment-container">
|
<div class="min-h-screen w-[65vw] right-0 absolute infotainment-container">
|
||||||
<!-- dynamic app system (edit appList.ts to add new apps) -->
|
<!-- dynamic app system (edit appList.ts to add new apps) -->
|
||||||
<div class="mx-10 mt-10">
|
<div class="mx-10 mt-10">
|
||||||
<svelte:component this={appList[activeApp].component} />
|
<svelte:component this={appList[activeApp].component} />
|
||||||
|
|
1
client/src/globals.d.ts
vendored
1
client/src/globals.d.ts
vendored
|
@ -9,7 +9,6 @@ interface SongData {
|
||||||
artist: string
|
artist: string
|
||||||
src: string
|
src: string
|
||||||
coverImg: string
|
coverImg: string
|
||||||
slug: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AppData {
|
interface AppData {
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
<!-- Genius codeium engineering automatically loads apps -->
|
<!-- Genius codeium engineering automatically loads apps -->
|
||||||
<div
|
<div
|
||||||
class="flex gap-4 justify-between h-20 bg-slate-300 backdrop-blur-sm rounded-xl px-6 shadow-md"
|
class="app-bar backdrop-blur-xl bg-slate-300 bg-opacity-20 flex gap-4 justify-between h-20 rounded-xl px-6 shadow-md"
|
||||||
>
|
>
|
||||||
{#each Object.entries(appList) as [appName, appData]}
|
{#each Object.entries(appList) as [appName, appData]}
|
||||||
<button
|
<button
|
||||||
|
@ -28,9 +28,9 @@
|
||||||
|
|
||||||
<style lang="postcss">
|
<style lang="postcss">
|
||||||
.app-icon {
|
.app-icon {
|
||||||
@apply my-auto hover:brightness-50 h-16 rounded-2xl shadow-md transition-all duration-150;
|
@apply my-auto hover:brightness-75 h-16 rounded-2xl shadow-md transition-all duration-150;
|
||||||
}
|
}
|
||||||
.selected {
|
.selected {
|
||||||
@apply brightness-50;
|
@apply scale-110;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="flex gap-4 w-full py-40 px-10 backdrop-blur-lg justify-center h-full camera-background rounded-3xl"
|
class="flex gap-4 w-full py-40 px-10 backdrop-blur-lg justify-center h-full rounded-3xl shadow-md bg-slate-300 bg-opacity-30"
|
||||||
>
|
>
|
||||||
<div class="my-auto">
|
<div class="my-auto">
|
||||||
<CameraContainer
|
<CameraContainer
|
||||||
|
@ -18,17 +18,4 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="postcss">
|
<style lang="postcss">
|
||||||
.camera-background {
|
|
||||||
background: #4b79a1; /* fallback for old browsers */
|
|
||||||
background: -webkit-linear-gradient(
|
|
||||||
to right,
|
|
||||||
#4b79a1,
|
|
||||||
#283e51
|
|
||||||
); /* Chrome 10-25, Safari 5.1-6 */
|
|
||||||
background: linear-gradient(
|
|
||||||
to right,
|
|
||||||
#4b79a1,
|
|
||||||
#283e51
|
|
||||||
); /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
12
client/src/lib/Apps/MusicBrowser/MusicBrowser.svelte
Normal file
12
client/src/lib/Apps/MusicBrowser/MusicBrowser.svelte
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import Song from './Song.svelte'
|
||||||
|
import { songList } from '../../Dashboard/MediaPlayer/songList'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="flex gap-4 w-full py-10 px-10 bg-blue-200 bg-opacity-25 backdrop-blur-xl h-full media-background rounded-3xl flex-wrap"
|
||||||
|
>
|
||||||
|
{#each Object.entries(songList) as [slug, song]}
|
||||||
|
<Song {song} {slug} />
|
||||||
|
{/each}
|
||||||
|
</div>
|
50
client/src/lib/Apps/MusicBrowser/Song.svelte
Normal file
50
client/src/lib/Apps/MusicBrowser/Song.svelte
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { musicStore } from '../../stores/musicStore'
|
||||||
|
|
||||||
|
export let song: SongData
|
||||||
|
export let slug: string
|
||||||
|
|
||||||
|
let { title, artist, coverImg } = song
|
||||||
|
|
||||||
|
const handlePlay = () => {
|
||||||
|
musicStore.setCurrent(slug)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleQueueNext = () => {
|
||||||
|
musicStore.queueNext(slug)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleQueueLast = () => {
|
||||||
|
musicStore.push(slug)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="flex gap-1 flex-col rounded-lg p-4 bg-slate-800 backdrop-blur-xl shadow-md w-60 flex-grow basis-1/5"
|
||||||
|
>
|
||||||
|
<img src={coverImg} alt="album cover" class="shadow-md rounded-lg w-full" />
|
||||||
|
<p class="mt-2 text-2xl font-medium">{title}</p>
|
||||||
|
<p class="text-xl text-slate-400">{artist}</p>
|
||||||
|
<div class="flex justify-center">
|
||||||
|
<div class="my-auto flex gap-4">
|
||||||
|
<button class="mt-2 hover:brightness-75" on:click={handlePlay}>
|
||||||
|
<span class="material-symbols-outlined icon fill">play_arrow</span>
|
||||||
|
</button>
|
||||||
|
<button class="mt-2 hover:brightness-75" on:click={handleQueueNext}>
|
||||||
|
<span class="material-symbols-outlined icon fill">next_plan</span>
|
||||||
|
</button>
|
||||||
|
<button class="mt-2 hover:brightness-75" on:click={handleQueueLast}>
|
||||||
|
<span class="material-symbols-outlined icon">queue_music</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="postcss">
|
||||||
|
.icon {
|
||||||
|
font-size: 28px;
|
||||||
|
}
|
||||||
|
.fill {
|
||||||
|
font-variation-settings: 'FILL' 1;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,22 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import Song from './Song.svelte'
|
|
||||||
import { songList } from './songList'
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="flex gap-4 w-full py-10 px-10 backdrop-blur-xl h-full media-background rounded-3xl flex-wrap"
|
|
||||||
>
|
|
||||||
{#each songList as song}
|
|
||||||
<Song {song} />
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style lang="postcss">
|
|
||||||
.media-background {
|
|
||||||
background: radial-gradient(
|
|
||||||
circle,
|
|
||||||
rgba(238, 174, 202, 1) 0%,
|
|
||||||
rgba(148, 187, 233, 1) 100%
|
|
||||||
);
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,33 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
export let song: SongData
|
|
||||||
|
|
||||||
let { title, artist, coverImg } = song
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="flex gap-1 flex-col rounded-lg p-4 bg-slate-800 backdrop-blur-xl shadow-md w-60"
|
|
||||||
>
|
|
||||||
<img src={coverImg} alt="album cover" class="shadow-md rounded-lg w-full" />
|
|
||||||
<p class="mt-2 text-3xl font-medium">{title}</p>
|
|
||||||
<p class="text-xl text-slate-400">{artist}</p>
|
|
||||||
<div class="flex justify-center">
|
|
||||||
<div class="my-auto flex gap-4">
|
|
||||||
<button class="mt-2">
|
|
||||||
<span class="material-symbols-outlined icon">skip_previous</span>
|
|
||||||
</button>
|
|
||||||
<button class="mt-2">
|
|
||||||
<span class="material-symbols-outlined icon">play_arrow</span>
|
|
||||||
</button>
|
|
||||||
<button class="mt-2">
|
|
||||||
<span class="material-symbols-outlined icon">skip_next</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style lang="postcss">
|
|
||||||
.icon {
|
|
||||||
font-size: 28px;
|
|
||||||
font-variation-settings: 'FILL' 1;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,30 +0,0 @@
|
||||||
export const songList = [
|
|
||||||
{
|
|
||||||
title: 'Danger Zone',
|
|
||||||
artist: 'Kenny Loggins',
|
|
||||||
src: '/static/songs/danger-zone/audio.mp3',
|
|
||||||
coverImg: '/static/songs/danger-zone/cover.png',
|
|
||||||
slug: 'danger-zone',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Danger Zone',
|
|
||||||
artist: 'Kenny Loggins',
|
|
||||||
src: '/static/songs/danger-zone/audio.mp3',
|
|
||||||
coverImg: '/static/songs/danger-zone/cover.png',
|
|
||||||
slug: 'danger-zone',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Danger Zone',
|
|
||||||
artist: 'Kenny Loggins',
|
|
||||||
src: '/static/songs/danger-zone/audio.mp3',
|
|
||||||
coverImg: '/static/songs/danger-zone/cover.png',
|
|
||||||
slug: 'danger-zone',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Danger Zone',
|
|
||||||
artist: 'Kenny Loggins',
|
|
||||||
src: '/static/songs/danger-zone/audio.mp3',
|
|
||||||
coverImg: '/static/songs/danger-zone/cover.png',
|
|
||||||
slug: 'danger-zone',
|
|
||||||
},
|
|
||||||
]
|
|
|
@ -1,5 +1,5 @@
|
||||||
import Camera from './Camera/Camera.svelte'
|
import Camera from './Camera/Camera.svelte'
|
||||||
import MusicBrowser from './MusicPlayer/MusicBrowser.svelte'
|
import MusicBrowser from './MusicBrowser/MusicBrowser.svelte'
|
||||||
|
|
||||||
export const appList = {
|
export const appList = {
|
||||||
'camera': {
|
'camera': {
|
||||||
|
|
|
@ -2,9 +2,12 @@
|
||||||
import TopBar from './TopBar/TopBar.svelte'
|
import TopBar from './TopBar/TopBar.svelte'
|
||||||
import Speedometer from './Speedometer.svelte'
|
import Speedometer from './Speedometer.svelte'
|
||||||
import SpeedLimit from './SpeedLimit.svelte'
|
import SpeedLimit from './SpeedLimit.svelte'
|
||||||
import MediaDisplay from './MediaDisplay/MediaDisplay.svelte'
|
import MediaDisplay from './MediaPlayer/MediaDisplay.svelte'
|
||||||
|
import Player from './MediaPlayer/Player.svelte'
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<Player />
|
||||||
|
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
<div class="px-5">
|
<div class="px-5">
|
||||||
<TopBar />
|
<TopBar />
|
||||||
|
|
|
@ -1,28 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
export let playing = false
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="my-auto flex gap-4">
|
|
||||||
<button class="mt-2">
|
|
||||||
<span class="material-symbols-outlined icon">skip_previous</span>
|
|
||||||
</button>
|
|
||||||
{#if playing}
|
|
||||||
<button class="mt-2">
|
|
||||||
<span class="material-symbols-outlined icon"> pause </span>
|
|
||||||
</button>
|
|
||||||
{:else}
|
|
||||||
<button class="mt-2">
|
|
||||||
<span class="material-symbols-outlined icon">play_arrow</span>
|
|
||||||
</button>
|
|
||||||
{/if}
|
|
||||||
<button class="mt-2">
|
|
||||||
<span class="material-symbols-outlined icon">skip_next</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style lang="postcss">
|
|
||||||
.icon {
|
|
||||||
font-size: 34px;
|
|
||||||
font-variation-settings: 'FILL' 1;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,20 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import Controls from './Controls.svelte'
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="rounded-t-lg bg-neutral-800 px-4 py-2 h-24 flex justify-between">
|
|
||||||
<div class="flex gap-6">
|
|
||||||
<div class="aspect-square">
|
|
||||||
<img
|
|
||||||
src="https://upload.wikimedia.org/wikipedia/en/thumb/2/2c/Loggins_-_Danger_Zone_single_cover.png/220px-Loggins_-_Danger_Zone_single_cover.png"
|
|
||||||
alt="album cover"
|
|
||||||
class="w-full h-full object-cover rounded-lg shadow-sm shadow-neutral-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="my-auto">
|
|
||||||
<p class="text-xl font-medium">Danger Zone</p>
|
|
||||||
<p class="text-lg text-slate-400">Kenny Loggins</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Controls playing={false} />
|
|
||||||
</div>
|
|
33
client/src/lib/Dashboard/MediaPlayer/Controls.svelte
Normal file
33
client/src/lib/Dashboard/MediaPlayer/Controls.svelte
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { createEventDispatcher } from 'svelte'
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
export let playing = false
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="my-auto flex gap-4 mr-4">
|
||||||
|
<button
|
||||||
|
class="mt-2 hover:brightness-75"
|
||||||
|
on:click={() => dispatch('previous')}
|
||||||
|
>
|
||||||
|
<span class="material-symbols-outlined icon">skip_previous</span>
|
||||||
|
</button>
|
||||||
|
<button class="mt-2 hover:brightness-75" on:click={() => dispatch('toggle')}>
|
||||||
|
{#if playing}
|
||||||
|
<span class="material-symbols-outlined icon">pause</span>
|
||||||
|
{:else}
|
||||||
|
<span class="material-symbols-outlined icon">play_arrow</span>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
<button class="mt-2 hover:brightness-75" on:click={() => dispatch('skip')}>
|
||||||
|
<span class="material-symbols-outlined icon">skip_next</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="postcss">
|
||||||
|
.icon {
|
||||||
|
font-size: 34px;
|
||||||
|
font-variation-settings: 'FILL' 1;
|
||||||
|
}
|
||||||
|
</style>
|
37
client/src/lib/Dashboard/MediaPlayer/MediaDisplay.svelte
Normal file
37
client/src/lib/Dashboard/MediaPlayer/MediaDisplay.svelte
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import Controls from './Controls.svelte'
|
||||||
|
import { musicStore } from '../../stores/musicStore'
|
||||||
|
import { songList } from './songList'
|
||||||
|
|
||||||
|
$: currentSong = $musicStore.queue[$musicStore.currentIndex]
|
||||||
|
$: songData = songList[currentSong]
|
||||||
|
|
||||||
|
const skip = () => {
|
||||||
|
musicStore.skip()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRewind = () => {}
|
||||||
|
|
||||||
|
const toggle = () => {
|
||||||
|
musicStore.toggle()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if songData}
|
||||||
|
<div class="rounded-t-lg bg-neutral-800 px-4 py-2 h-24 flex justify-between">
|
||||||
|
<div class="flex gap-6">
|
||||||
|
<div class="aspect-square">
|
||||||
|
<img
|
||||||
|
src={songData.coverImg}
|
||||||
|
alt="album cover"
|
||||||
|
class="w-full h-full object-cover rounded-lg shadow-sm shadow-neutral-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="my-auto">
|
||||||
|
<p class="text-xl font-medium">{songData.title}</p>
|
||||||
|
<p class="text-lg text-slate-400">{songData.artist}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Controls on:skip={skip} on:toggle={toggle} playing={$musicStore.playing} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
26
client/src/lib/Dashboard/MediaPlayer/Player.svelte
Normal file
26
client/src/lib/Dashboard/MediaPlayer/Player.svelte
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { AudioManager } from './audioManager'
|
||||||
|
import { musicStore } from '../../stores/musicStore'
|
||||||
|
import { songList } from './songList'
|
||||||
|
|
||||||
|
const audioManager = new AudioManager()
|
||||||
|
$: currentSong = songList[$musicStore.queue[$musicStore.currentIndex]]
|
||||||
|
|
||||||
|
let src: string = ''
|
||||||
|
$: {
|
||||||
|
if (currentSong) src = currentSong.src
|
||||||
|
console.log(currentSong)
|
||||||
|
}
|
||||||
|
|
||||||
|
$: {
|
||||||
|
if (src !== '' && $musicStore.playing) {
|
||||||
|
audioManager.playAudio(src)
|
||||||
|
console.log(src)
|
||||||
|
} else if (!$musicStore.playing) {
|
||||||
|
console.log('stopping')
|
||||||
|
audioManager.stopAudio()
|
||||||
|
}
|
||||||
|
console.log($musicStore.queue)
|
||||||
|
console.log($musicStore.currentIndex)
|
||||||
|
}
|
||||||
|
</script>
|
102
client/src/lib/Dashboard/MediaPlayer/audioManager.ts
Normal file
102
client/src/lib/Dashboard/MediaPlayer/audioManager.ts
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
export class AudioManager {
|
||||||
|
private audioContext: AudioContext | null = null
|
||||||
|
private currentSource: AudioBufferSourceNode | null = null
|
||||||
|
private currentBuffer: AudioBuffer | null = null // Stores the current audio buffer
|
||||||
|
private startTime: number = 0 // When the current playback started
|
||||||
|
private pauseTime: number = 0 // Track where we paused
|
||||||
|
private currentToken: number = 0 // Unique token for each play request
|
||||||
|
private isPaused: boolean = false
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.initAudioContext()
|
||||||
|
}
|
||||||
|
|
||||||
|
private async initAudioContext() {
|
||||||
|
if (!this.audioContext) {
|
||||||
|
this.audioContext = new AudioContext()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async playAudio(url: string): Promise<void> {
|
||||||
|
if (this.isPaused && this.currentBuffer) {
|
||||||
|
// If paused, resume instead of reloading
|
||||||
|
this.resume()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pauseTime = 0 // Reset pause time for a new track
|
||||||
|
const playToken = ++this.currentToken // Update the token for this request
|
||||||
|
await this.initAudioContext()
|
||||||
|
|
||||||
|
if (this.audioContext) {
|
||||||
|
this.stopAudio() // Stop any currently playing audio
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(url)
|
||||||
|
const arrayBuffer = await response.arrayBuffer()
|
||||||
|
if (this.currentToken !== playToken) {
|
||||||
|
return // Abort if a newer request has been made
|
||||||
|
}
|
||||||
|
const audioBuffer = await this.audioContext.decodeAudioData(arrayBuffer)
|
||||||
|
this.currentBuffer = audioBuffer // Save the buffer for potential pausing/resuming
|
||||||
|
|
||||||
|
this.startPlayback(audioBuffer, 0) // Start playback from the beginning
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error playing audio:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private startPlayback(buffer: AudioBuffer, offset: number) {
|
||||||
|
const source = this.audioContext!.createBufferSource()
|
||||||
|
source.buffer = buffer
|
||||||
|
source.connect(this.audioContext!.destination)
|
||||||
|
source.start(0, offset)
|
||||||
|
this.startTime = this.audioContext!.currentTime - offset
|
||||||
|
this.currentSource = source
|
||||||
|
this.isPaused = false
|
||||||
|
|
||||||
|
source.onended = () => {
|
||||||
|
if (!this.isPaused) {
|
||||||
|
this.currentBuffer = null // Clear the buffer if playback finishes normally
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public pause() {
|
||||||
|
if (!this.isPaused && this.currentSource && this.audioContext) {
|
||||||
|
this.pauseTime = this.audioContext.currentTime - this.startTime
|
||||||
|
this.currentSource.stop()
|
||||||
|
this.isPaused = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public resume() {
|
||||||
|
if (this.isPaused && this.currentBuffer) {
|
||||||
|
this.startPlayback(this.currentBuffer, this.pauseTime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public stopAudio() {
|
||||||
|
if (this.currentSource) {
|
||||||
|
this.currentSource.stop()
|
||||||
|
this.currentSource = null
|
||||||
|
this.currentBuffer = null // Clear the current buffer
|
||||||
|
this.isPaused = false
|
||||||
|
this.pauseTime = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage example:
|
||||||
|
const audioManager = new AudioManager()
|
||||||
|
const audioUrl = 'https://example.com/path/to/your/audio/file.mp3'
|
||||||
|
|
||||||
|
// To play the audio
|
||||||
|
audioManager.playAudio(audioUrl)
|
||||||
|
|
||||||
|
// To pause the audio
|
||||||
|
audioManager.pause()
|
||||||
|
|
||||||
|
// To resume the audio
|
||||||
|
audioManager.resume()
|
14
client/src/lib/Dashboard/MediaPlayer/songList.ts
Normal file
14
client/src/lib/Dashboard/MediaPlayer/songList.ts
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
export const songList: { [key: string]: SongData } = {
|
||||||
|
'danger-zone': {
|
||||||
|
title: 'Danger Zone',
|
||||||
|
artist: 'Kenny Loggins',
|
||||||
|
src: '/static/songs/danger-zone/audio.mp3',
|
||||||
|
coverImg: '/static/songs/danger-zone/cover.png',
|
||||||
|
},
|
||||||
|
'monko-zone': {
|
||||||
|
title: 'Danger Zone',
|
||||||
|
artist: 'Kenny Loggins',
|
||||||
|
src: '/static/songs/danger-zone/audio.mp3',
|
||||||
|
coverImg: '/static/songs/danger-zone/cover.png',
|
||||||
|
},
|
||||||
|
}
|
60
client/src/lib/stores/musicStore.ts
Normal file
60
client/src/lib/stores/musicStore.ts
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
import { writable } from 'svelte/store'
|
||||||
|
|
||||||
|
interface MusicQueue {
|
||||||
|
queue: string[]
|
||||||
|
currentIndex: number
|
||||||
|
playing: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMusicStore() {
|
||||||
|
const { subscribe, set, update } = writable<MusicQueue>({
|
||||||
|
queue: [],
|
||||||
|
currentIndex: 0,
|
||||||
|
playing: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe,
|
||||||
|
push: (songSlug: string) =>
|
||||||
|
update(store => {
|
||||||
|
store.queue.push(songSlug)
|
||||||
|
return store
|
||||||
|
}),
|
||||||
|
skip: () =>
|
||||||
|
update(store => {
|
||||||
|
let next = store.queue[store.currentIndex + 1]
|
||||||
|
|
||||||
|
if (next !== undefined) {
|
||||||
|
store.currentIndex++
|
||||||
|
}
|
||||||
|
return store
|
||||||
|
}),
|
||||||
|
setCurrent: (songSlug: string) =>
|
||||||
|
update(store => {
|
||||||
|
store.currentIndex = store.queue.length
|
||||||
|
store.queue.push(songSlug)
|
||||||
|
return store
|
||||||
|
}),
|
||||||
|
queueNext: (songSlug: string) =>
|
||||||
|
update(store => {
|
||||||
|
store.queue.splice(store.currentIndex + 1, 0, songSlug)
|
||||||
|
return store
|
||||||
|
}),
|
||||||
|
pause: update(store => {
|
||||||
|
store.playing = false
|
||||||
|
return store
|
||||||
|
}),
|
||||||
|
play: update(store => {
|
||||||
|
store.playing = true
|
||||||
|
return store
|
||||||
|
}),
|
||||||
|
toggle: () =>
|
||||||
|
update(store => {
|
||||||
|
store.playing = !store.playing
|
||||||
|
return store
|
||||||
|
}),
|
||||||
|
reset: () => set({ queue: [], currentIndex: 0, playing: false }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const musicStore = createMusicStore()
|
Loading…
Reference in a new issue