Move playback and queue to state files; Move various components to layout; Begin moving view to state file;
This commit is contained in:
parent
65f0e6776e
commit
99b534e9b5
12 changed files with 422 additions and 284 deletions
|
@ -3,10 +3,43 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {Button} from "$lib/components/ui/button";
|
import {Button} from "$lib/components/ui/button";
|
||||||
import {onMount} from "svelte";
|
import {onMount} from "svelte";
|
||||||
let { audio, pause, play } = $props();
|
import {playbackState} from "$lib/states/playback-state.svelte";
|
||||||
|
import {toFixedNumber} from "$lib/formatting";
|
||||||
let mounted = $state(false);
|
let mounted = $state(false);
|
||||||
|
|
||||||
$inspect(audio);
|
function changeVolume(change: number) {
|
||||||
|
if (playbackState.volume + change > 1) {
|
||||||
|
playbackState.volume = 1;
|
||||||
|
}
|
||||||
|
else if (playbackState.volume + change < 0) {
|
||||||
|
playbackState.volume = 0;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
playbackState.volume = toFixedNumber(playbackState.volume + change, 2, 10) ;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (playbackState.song) {
|
||||||
|
playbackState.song.volume = playbackState.volume;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function progressPercent() {
|
||||||
|
return playbackState.progress / playbackState.duration * 100 || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayTime(rawSeconds: number) {
|
||||||
|
const intSeconds = Math.round(rawSeconds);
|
||||||
|
const seconds = intSeconds % 60;
|
||||||
|
const minutes = Math.floor((intSeconds / 60)) % 60;
|
||||||
|
const hours = Math.floor((intSeconds / 3600));
|
||||||
|
|
||||||
|
if (hours == 0) {
|
||||||
|
return `${minutes.toString()}:${seconds.toString().padStart(2, 0)}`
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return `${hours}:${minutes.toString().padStart(2, 0)}:${seconds.toString().padStart(2, 0)}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
mounted = true;
|
mounted = true;
|
||||||
|
@ -15,14 +48,22 @@
|
||||||
|
|
||||||
<div class="flex flex-row gap-2">
|
<div class="flex flex-row gap-2">
|
||||||
{#if mounted}
|
{#if mounted}
|
||||||
<p>Paused: {audio.paused}</p>
|
<div class="flex flex-row gap-2">
|
||||||
<p>Volume: {audio.volume}</p>
|
<p class="border p-2">Song: {playbackState.metaData.title}</p>
|
||||||
<p>Duration: {audio.duration}</p>
|
<p class="border p-2">Volume: {playbackState.volume}</p>
|
||||||
{#if audio.paused}
|
<p class="border p-2">{displayTime(playbackState.progress)}/{displayTime(playbackState.duration)}</p>
|
||||||
<Button onclick={play}>Play</Button>
|
<p class="border p-2">{progressPercent().toFixed(2)}%</p>
|
||||||
{:else}
|
<Button class="border p-2" onclick={() => changeVolume(-0.05)}>-</Button>
|
||||||
<Button onclick={pause}>Pause</Button>
|
<Button class="border p-2" onclick={() => changeVolume(0.05)}>+</Button>
|
||||||
{/if}
|
<Button class="border p-2" onclick={() => playbackState.song.currentTime -= 5}>{"<<"}</Button>
|
||||||
|
{#if playbackState.paused}
|
||||||
|
<Button onclick={() => playbackState.play()}>Play</Button>
|
||||||
|
{:else}
|
||||||
|
<Button onclick={() => playbackState.pause()}>Pause</Button>
|
||||||
|
{/if}
|
||||||
|
<Button class="border p-2" onclick={() => playbackState.song.currentTime += 5}>{">>"}</Button>
|
||||||
|
<Button class="border p-2" onclick={() => playbackState.mode.next()}>{playbackState.mode.get()}</Button>
|
||||||
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<p>Nothing going on here</p>
|
<p>Nothing going on here</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -3,28 +3,28 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {Separator} from "$lib/components/ui/separator";
|
import {Separator} from "$lib/components/ui/separator";
|
||||||
import {Button} from "$lib/components/ui/button";
|
import {Button} from "$lib/components/ui/button";
|
||||||
import {Views} from "$lib/components/custom/Views/views.svelte";
|
import {View} from "$lib/components/custom/Views/views.svelte";
|
||||||
import {AlbumViews} from "$lib/components/custom/Views/views.svelte.js";
|
import {AlbumView} from "$lib/components/custom/Views/views.svelte.js";
|
||||||
|
import {viewState} from "$lib/states/view-state.svelte";
|
||||||
|
|
||||||
let { view = $bindable(), subView = $bindable() } = $props();
|
|
||||||
let playlists = $state([]);
|
let playlists = $state([]);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="border border-2 col-span-1 flex flex-col gap-1 p-2">
|
<div class="border border-2 col-span-1 flex flex-col gap-1 p-2">
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<Button onclick={() => view=Views.Albums} class="flex flex-col h-fit">Albums
|
<Button onclick={() => viewState.setMode(View.Albums)} class="flex flex-col h-fit">Albums
|
||||||
<Button variant="secondary" onclick={() => subView=AlbumViews.All}>All</Button>
|
<Button variant="secondary" onclick={() => viewState.setSubMode(AlbumView.All)}>All</Button>
|
||||||
<Button variant="secondary" onclick={() => subView=AlbumViews.Random}>Random</Button>
|
<Button variant="secondary" onclick={() => viewState.setSubMode(AlbumView.Random)}>Random</Button>
|
||||||
<Button variant="secondary" onclick={() => subView=AlbumViews.Favourites}>Favourites</Button>
|
<Button variant="secondary" onclick={() => viewState.setSubMode(AlbumView.Favourites)}>Favourites</Button>
|
||||||
<Button variant="secondary" onclick={() => subView=AlbumViews.TopRated}>Top Rated</Button>
|
<Button variant="secondary" onclick={() => viewState.setSubMode(AlbumView.TopRated)}>Top Rated</Button>
|
||||||
<Button variant="secondary" onclick={() => subView=AlbumViews.RecentlyAdded}>Recently Added</Button>
|
<Button variant="secondary" onclick={() => viewState.setSubMode(AlbumView.RecentlyAdded)}>Recently Added</Button>
|
||||||
<Button variant="secondary" onclick={() => subView=AlbumViews.RecentlyPlayed}>Recently Played</Button>
|
<Button variant="secondary" onclick={() => viewState.setSubMode(AlbumView.RecentlyPlayed)}>Recently Played</Button>
|
||||||
<Button variant="secondary" onclick={() => subView=AlbumViews.MostPlayed}>Most Played</Button>
|
<Button variant="secondary" onclick={() => viewState.setSubMode(AlbumView.MostPlayed)}>Most Played</Button>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<Separator />
|
<Separator />
|
||||||
<Button onclick={() => view=Views.Artists}>Artists</Button>
|
<Button onclick={() => viewState.setMode(View.Artists)}>Artists</Button>
|
||||||
<h1>Songs</h1>
|
<h1>Songs</h1>
|
||||||
<h1>Radios</h1>
|
<h1>Radios</h1>
|
||||||
<h1>Shares</h1>
|
<h1>Shares</h1>
|
||||||
|
|
|
@ -5,28 +5,54 @@
|
||||||
import {Button} from "$lib/components/ui/button";
|
import {Button} from "$lib/components/ui/button";
|
||||||
import {timeFormat} from "$lib/time-format";
|
import {timeFormat} from "$lib/time-format";
|
||||||
import LoadingSpinner from "$lib/components/custom/LoadingSpinner.svelte";
|
import LoadingSpinner from "$lib/components/custom/LoadingSpinner.svelte";
|
||||||
|
import {queueState} from "$lib/states/play-queue.svelte";
|
||||||
|
import {playbackState} from "$lib/states/playback-state.svelte";
|
||||||
|
|
||||||
let { queue = [], fetchQueue, saveQueue, removeSongFromQueue, playSong, currentIndex } = $props();
|
|
||||||
let loading = $state(false);
|
let loading = $state(false);
|
||||||
|
|
||||||
async function innerFetchQueue() {
|
async function innerFetchQueue() {
|
||||||
loading = true;
|
loading = true;
|
||||||
await fetchQueue()
|
await queueState.getPlayQueue()
|
||||||
await new Promise(resolve => setTimeout(resolve, 100));
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
loading = false;
|
loading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function removeSongFromQueue(idx: number) {
|
||||||
|
if (idx > -1) {
|
||||||
|
queueState.queue.splice(idx, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function playSong( songIndex: number) {
|
||||||
|
queueState.setSong(songIndex);
|
||||||
|
playbackState.pause();
|
||||||
|
playbackState.newSong(queueState.getSong());
|
||||||
|
playbackState.play();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
{#snippet songEntry(song, idx)}
|
||||||
|
<p onclick={() => playSong(idx)}>{song.artist} - {song.title} ({timeFormat(song.duration)})</p>
|
||||||
|
<Button onclick={() => removeSongFromQueue(idx)}>X</Button>
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
{#snippet playerQueue()}
|
{#snippet playerQueue()}
|
||||||
{#each queue as song, idx}
|
{#each queueState.queue as song, idx}
|
||||||
{#if currentIndex == idx}
|
{#if idx < queueState.currentIndex}
|
||||||
<p class="bg-secondary" onclick={() => playSong(song, idx)}>{song.artist} - {song.title} ({timeFormat(song.duration)})<Button onclick={() => removeSongFromQueue(idx)}>X</Button></p>
|
<div class="flex flex-row gap-2 text-muted">
|
||||||
|
{@render songEntry(song, idx)}
|
||||||
|
</div>
|
||||||
|
{:else if idx === queueState.currentIndex}
|
||||||
|
<div class="flex flex-row gap-2 bg-secondary">
|
||||||
|
{@render songEntry(song, idx)}
|
||||||
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<p onclick={() => playSong(song, idx)}>{song.artist} - {song.title} ({timeFormat(song.duration)})<Button onclick={() => removeSongFromQueue(idx)}>X</Button></p>
|
<div class="flex flex-row gap-2">
|
||||||
|
{@render songEntry(song, idx)}
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
|
<Button onclick={() => queueState.queue.push(queueState.queue[0])}>Add</Button>
|
||||||
<Button onclick={() => queue.push(queue[0])}>Add</Button>
|
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
<div class="border border-2 col-span-1 flex flex-col gap-1 p-2">
|
<div class="border border-2 col-span-1 flex flex-col gap-1 p-2">
|
||||||
|
@ -34,7 +60,7 @@
|
||||||
<h1>Current Queue</h1>
|
<h1>Current Queue</h1>
|
||||||
<div class="flex-grow"></div>
|
<div class="flex-grow"></div>
|
||||||
<Button variant="outline" class="h-8 w-12" onclick={innerFetchQueue}>Fetch</Button>
|
<Button variant="outline" class="h-8 w-12" onclick={innerFetchQueue}>Fetch</Button>
|
||||||
<Button variant="outline" class="h-8 w-12" onclick={saveQueue}>Upload</Button>
|
<Button variant="outline" class="h-8 w-12" onclick={() => queueState.saveQueue()}>Upload</Button>
|
||||||
</div>
|
</div>
|
||||||
<Separator />
|
<Separator />
|
||||||
{#if loading}
|
{#if loading}
|
||||||
|
|
|
@ -1,16 +1,14 @@
|
||||||
<svelte:options runes={true} />
|
<svelte:options runes={true} />
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {AlbumViews} from "$lib/components/custom/Views/views.svelte";
|
import {AlbumView} from "$lib/components/custom/Views/views.svelte";
|
||||||
|
import {viewState} from "$lib/states/view-state.svelte";
|
||||||
import {onMount} from "svelte";
|
import {onMount} from "svelte";
|
||||||
import LoadingSpinner from "$lib/components/custom/LoadingSpinner.svelte";
|
|
||||||
import ViewCard from "$lib/components/custom/Views/ViewCard.svelte";
|
import ViewCard from "$lib/components/custom/Views/ViewCard.svelte";
|
||||||
import {type AlbumID3, type GetAlbumList2Response, OpenSubsonic, type Parameter} from "$lib/opensubsonic";
|
import {type AlbumID3, type GetAlbumList2Response, OpenSubsonic, type Parameter} from "$lib/opensubsonic";
|
||||||
import {Skeleton} from "$lib/components/ui/skeleton";
|
import {Skeleton} from "$lib/components/ui/skeleton";
|
||||||
import {goto} from "$app/navigation";
|
import {goto} from "$app/navigation";
|
||||||
|
|
||||||
let { viewMode = $bindable() }: { viewMode: AlbumViews } = $props();
|
|
||||||
let previousView = $state(viewMode);
|
|
||||||
let albums: Array<AlbumID3> = $state([]);
|
let albums: Array<AlbumID3> = $state([]);
|
||||||
let loading = $state(true);
|
let loading = $state(true);
|
||||||
let paginating = $state(false);
|
let paginating = $state(false);
|
||||||
|
@ -22,7 +20,7 @@
|
||||||
|
|
||||||
async function fetchAlbums() {
|
async function fetchAlbums() {
|
||||||
let parameters: Array<Parameter> = [
|
let parameters: Array<Parameter> = [
|
||||||
{key: "type", value: viewMode},
|
{key: "type", value: viewState.current.subMode},
|
||||||
{key: "size", value: paginationIncrement},
|
{key: "size", value: paginationIncrement},
|
||||||
{key: "offset", value: pagination},
|
{key: "offset", value: pagination},
|
||||||
];
|
];
|
||||||
|
@ -50,14 +48,17 @@
|
||||||
})
|
})
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
console.log(viewMode);
|
console.log(viewState.current.subMode);
|
||||||
if (viewMode !== previousView) {
|
if (viewState.previous) {
|
||||||
loading = true;
|
if (viewState.current.subMode !== viewState.previous.subMode) {
|
||||||
albums = [];
|
loading = true;
|
||||||
pagination = 0;
|
albums = [];
|
||||||
fetchAlbums();
|
pagination = 0;
|
||||||
previousView = viewMode
|
fetchAlbums();
|
||||||
|
viewState.previous.mode = viewState.current.mode
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
function selectAlbum(albumId) {
|
function selectAlbum(albumId) {
|
||||||
|
@ -67,7 +68,7 @@
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
fetchAlbums();
|
fetchAlbums();
|
||||||
previousView = viewMode;
|
viewState.previous = viewState.current;
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -3,9 +3,9 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {Separator} from "$lib/components/ui/separator";
|
import {Separator} from "$lib/components/ui/separator";
|
||||||
import {Button} from "$lib/components/ui/button";
|
import {Button} from "$lib/components/ui/button";
|
||||||
import {AlbumViews} from "$lib/components/custom/Views/views.svelte";
|
import {AlbumView} from "$lib/components/custom/Views/views.svelte";
|
||||||
|
|
||||||
let { viewMode = $bindable() }: { viewMode: AlbumViews } = $props();
|
let { viewMode = $bindable() }: { viewMode: AlbumView } = $props();
|
||||||
let range = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,]
|
let range = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,]
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
export enum AlbumViews {
|
export enum AlbumView {
|
||||||
All = "alphabeticalByName",
|
All = "alphabeticalByName",
|
||||||
Random = "random",
|
Random = "random",
|
||||||
Favourites = "starred",
|
Favourites = "starred",
|
||||||
|
@ -8,10 +8,13 @@ export enum AlbumViews {
|
||||||
MostPlayed = "frequent"
|
MostPlayed = "frequent"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum Views {
|
export enum View {
|
||||||
Albums = "Albums",
|
Albums = "Albums",
|
||||||
Artists = "Artists",
|
Artists = "Artists",
|
||||||
Songs = "Songs",
|
Songs = "Songs",
|
||||||
Radios = "Radios",
|
Radios = "Radios",
|
||||||
Shares = "Shares"
|
Shares = "Shares",
|
||||||
|
Playlist = "Playlist",
|
||||||
|
Artist = "Artist",
|
||||||
|
Album = "Album",
|
||||||
}
|
}
|
4
src/lib/formatting.ts
Normal file
4
src/lib/formatting.ts
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
export function toFixedNumber(num: number, digits: number, base: number): number{
|
||||||
|
const pow: number = Math.pow(base ?? 10, digits);
|
||||||
|
return Math.round(num*pow) / pow;
|
||||||
|
}
|
134
src/lib/states/play-queue.svelte.ts
Normal file
134
src/lib/states/play-queue.svelte.ts
Normal file
|
@ -0,0 +1,134 @@
|
||||||
|
import {
|
||||||
|
type GetPlayQueueResponse, type NowPlayingEntry,
|
||||||
|
type NowPlayingResponse,
|
||||||
|
OpenSubsonic,
|
||||||
|
type Parameter,
|
||||||
|
type Song
|
||||||
|
} from "$lib/opensubsonic";
|
||||||
|
import {playbackState} from "$lib/states/playback-state.svelte";
|
||||||
|
|
||||||
|
interface QueueState {
|
||||||
|
queue: Array<Song>,
|
||||||
|
currentIndex: number,
|
||||||
|
nextSong: () => Song,
|
||||||
|
setSong: (index: number) => void,
|
||||||
|
getSong: () => Song,
|
||||||
|
firstSong: () => Song,
|
||||||
|
findSong: (song: Song) => {found: boolean, index: number},
|
||||||
|
addSong: (song: Song) => void,
|
||||||
|
getPlayQueue: (addNowPlaying: boolean) => Promise<void>,
|
||||||
|
saveQueue: () => Promise<void>,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const queueState: QueueState = $state({
|
||||||
|
queue: new Array<Song>(),
|
||||||
|
currentIndex: 0,
|
||||||
|
nextSong(): Song {
|
||||||
|
this.currentIndex += 1;
|
||||||
|
return this.queue[this.currentIndex];
|
||||||
|
},
|
||||||
|
setSong(index: number): void {
|
||||||
|
this.currentIndex = index;
|
||||||
|
},
|
||||||
|
getSong(): Song {
|
||||||
|
return this.queue[this.currentIndex];
|
||||||
|
},
|
||||||
|
firstSong(): Song {
|
||||||
|
this.currentIndex = 0;
|
||||||
|
return this.queue[this.currentIndex];
|
||||||
|
},
|
||||||
|
findSong(searchSong: Song): {found: boolean, index: number} {
|
||||||
|
const data = {
|
||||||
|
found: false,
|
||||||
|
index: 0,
|
||||||
|
}
|
||||||
|
for (const [index, song] of this.queue.entries()) {
|
||||||
|
if (song.id === searchSong.id) {
|
||||||
|
data.found = true;
|
||||||
|
data.index = index;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
addSong(newSong: Song): void {
|
||||||
|
this.queue.push(newSong);
|
||||||
|
this.currentIndex = this.queue.length - 1;
|
||||||
|
},
|
||||||
|
async getPlayQueue(addNowPlaying: boolean = false): Promise<void> {
|
||||||
|
const queueData: GetPlayQueueResponse = await OpenSubsonic.get("getPlayQueue")
|
||||||
|
if (queueData && queueData.playQueue.entry) {
|
||||||
|
this.queue = queueData.playQueue.entry;
|
||||||
|
}
|
||||||
|
const nowPlayingData: NowPlayingResponse = await OpenSubsonic.get("getNowPlaying");
|
||||||
|
|
||||||
|
if (nowPlayingData && nowPlayingData.nowPlaying.entry) {
|
||||||
|
const userEntries: Array<NowPlayingEntry> = [];
|
||||||
|
nowPlayingData.nowPlaying.entry.forEach((entry) => {
|
||||||
|
if (entry.username === OpenSubsonic.username) {
|
||||||
|
userEntries.push(entry);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log(userEntries);
|
||||||
|
let localClientEntry = undefined;
|
||||||
|
userEntries.forEach((entry) => {
|
||||||
|
if (entry.playerName === OpenSubsonic.clientName) {
|
||||||
|
localClientEntry = entry;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (typeof localClientEntry !== "undefined") {
|
||||||
|
if (this.findSong(localClientEntry).found) {
|
||||||
|
this.setSong(this.findSong(localClientEntry).index);
|
||||||
|
playbackState.newSong(this.getSong());
|
||||||
|
}
|
||||||
|
else if (addNowPlaying) {
|
||||||
|
this.addSong(localClientEntry);
|
||||||
|
playbackState.newSong(this.getSong());
|
||||||
|
}
|
||||||
|
else if (this.queue.length != 0) {
|
||||||
|
playbackState.newSong(this.firstSong());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
for (const entry of userEntries) {
|
||||||
|
if (this.findSong(entry).found) {
|
||||||
|
this.setSong(this.findSong(entry).index);
|
||||||
|
playbackState.newSong(this.getSong());
|
||||||
|
}
|
||||||
|
else if (addNowPlaying) {
|
||||||
|
this.addSong(entry);
|
||||||
|
playbackState.newSong(this.getSong());
|
||||||
|
}
|
||||||
|
else if (this.queue.length != 0) {
|
||||||
|
playbackState.newSong(this.firstSong());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async saveQueue(): Promise<void> {
|
||||||
|
const songs: Array<Parameter> = [];
|
||||||
|
|
||||||
|
console.log(this.queue);
|
||||||
|
|
||||||
|
this.queue.forEach((song: Song, idx: number): void => {
|
||||||
|
if (idx === 0) {
|
||||||
|
songs.push({key: "current", value: song.id})
|
||||||
|
songs.push({key: "id", value: song.id})
|
||||||
|
// Add Progress within current song
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
songs.push({key: "id", value: song.id})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const data = await OpenSubsonic.get("savePlayQueue", songs);
|
||||||
|
if (data) {
|
||||||
|
await this.getPlayQueue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
98
src/lib/states/playback-state.svelte.ts
Normal file
98
src/lib/states/playback-state.svelte.ts
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
import {PlaybackMode, PlaybackStateSvelte} from "$lib/player.svelte";
|
||||||
|
import {OpenSubsonic, type Parameter, type Song} from "$lib/opensubsonic";
|
||||||
|
import {queueState} from "$lib/states/play-queue.svelte";
|
||||||
|
|
||||||
|
interface PlaybackState {
|
||||||
|
mode: PlaybackStateSvelte,
|
||||||
|
song: HTMLAudioElement,
|
||||||
|
metaData: Song,
|
||||||
|
duration: number,
|
||||||
|
volume: number,
|
||||||
|
paused: boolean,
|
||||||
|
progress: number,
|
||||||
|
newSong: (song: Song) => void,
|
||||||
|
play: () => void,
|
||||||
|
pause: () => void,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const playbackState: PlaybackState = $state({
|
||||||
|
mode: new PlaybackStateSvelte(),
|
||||||
|
song: {},
|
||||||
|
metaData: {},
|
||||||
|
duration: 0,
|
||||||
|
volume: 0.05,
|
||||||
|
paused: true,
|
||||||
|
progress: 0,
|
||||||
|
newSong(song: Song): void {
|
||||||
|
const parameters: Array<Parameter> = [
|
||||||
|
{ key: "id", value: song.id },
|
||||||
|
//{ key: "maxBitRate", value: } // TODO
|
||||||
|
//{ key: "format", value: } // TODO
|
||||||
|
//{ key: "timeOffset", value: } // TODO? Only Video related
|
||||||
|
//{ key: "size", value: } // TODO? Only Video related
|
||||||
|
{ key: "estimateContentLength", value: "true" },
|
||||||
|
//{ key: "converted", value: } // TODO? Only Video related
|
||||||
|
];
|
||||||
|
const url = OpenSubsonic.getApiUrl("stream", parameters);
|
||||||
|
this.song = new Audio(url); // Assign new URL
|
||||||
|
this.metaData = song;
|
||||||
|
|
||||||
|
// Reassign Event Handlers
|
||||||
|
this.song.onloadedmetadata = () => {
|
||||||
|
this.duration = this.song.duration;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.song.onplay = () => {
|
||||||
|
this.song.volume = this.volume;
|
||||||
|
this.paused = this.song.paused;
|
||||||
|
const time: number = Date.now();
|
||||||
|
const parameters: Array<Parameter> = [
|
||||||
|
{ key: "id", value: song.id },
|
||||||
|
{ key: "time", value: time.toString()},
|
||||||
|
{ key: "submission", value: `${false}`}
|
||||||
|
];
|
||||||
|
OpenSubsonic.get("scrobble", parameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.song.onpause = () => {
|
||||||
|
this.paused = this.song.paused;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.song.ontimeupdate = () => {
|
||||||
|
this.progress = this.song.currentTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.song.onended = () => {
|
||||||
|
switch (this.mode.current) {
|
||||||
|
case PlaybackMode.Linear: {
|
||||||
|
this.newSong(queueState.nextSong());
|
||||||
|
this.play();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case PlaybackMode.LoopOne: {
|
||||||
|
this.play();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case PlaybackMode.LoopQueue: {
|
||||||
|
if (queueState.currentIndex === queueState.queue.length -1) {
|
||||||
|
this.newSong(queueState.firstSong());
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.newSong(queueState.nextSong());
|
||||||
|
}
|
||||||
|
this.play();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.song.load();
|
||||||
|
},
|
||||||
|
play() {
|
||||||
|
this.song.play().catch((): void => {});
|
||||||
|
},
|
||||||
|
pause() {
|
||||||
|
console.log(this.song);
|
||||||
|
this.song.pause();
|
||||||
|
}
|
||||||
|
})
|
30
src/lib/states/view-state.svelte.ts
Normal file
30
src/lib/states/view-state.svelte.ts
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import {AlbumView, View} from "$lib/components/custom/Views/views.svelte";
|
||||||
|
|
||||||
|
interface ViewState {
|
||||||
|
current: ViewDetails,
|
||||||
|
previous?: ViewState,
|
||||||
|
parent?: ViewState,
|
||||||
|
setMode: (newMode: View) => void,
|
||||||
|
setSubMode: (newSubMode: AlbumView) => void,
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ViewDetails {
|
||||||
|
// sort
|
||||||
|
mode: View,
|
||||||
|
subMode: AlbumView,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const viewState: ViewState = $state<ViewState>({
|
||||||
|
current: {
|
||||||
|
mode: View.Albums,
|
||||||
|
subMode: AlbumView.All,
|
||||||
|
},
|
||||||
|
setSubMode(newSubMode: AlbumView): void {
|
||||||
|
this.previous = this.current;
|
||||||
|
this.current.subMode = newSubMode;
|
||||||
|
},
|
||||||
|
setMode(newMode: View): void {
|
||||||
|
this.previous = this.current;
|
||||||
|
this.current.mode = newMode;
|
||||||
|
}
|
||||||
|
})
|
|
@ -2,14 +2,34 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import "../app.pcss";
|
import "../app.pcss";
|
||||||
|
import QueueFrame from "$lib/components/custom/QueueFrame.svelte";
|
||||||
|
import {onMount} from "svelte";
|
||||||
|
import PlayerControls from "$lib/components/custom/PlayerControls.svelte";
|
||||||
|
import {AlbumView, View} from "$lib/components/custom/Views/views.svelte";
|
||||||
|
import PlayerSidebar from "$lib/components/custom/PlayerSidebar.svelte";
|
||||||
|
|
||||||
let { children } = $props();
|
let { children } = $props();
|
||||||
|
|
||||||
|
let viewMode = $state(View.Albums);
|
||||||
|
let subViewMode = $state(AlbumView.All);
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="h-full flex flex-col">
|
<div class="h-full flex flex-col">
|
||||||
<div class="border border-2 p-1">
|
<div class="border border-2 p-1">
|
||||||
<h1>Navbar</h1>
|
<h1>Navbar</h1>
|
||||||
</div>
|
</div>
|
||||||
|
<!--<QueueFrame queue={queueState} {fetchQueue} {saveQueue} {removeSongFromQueue} {playSong} currentIndex={currentSong.queueIndex} />-->
|
||||||
|
<div class="border border-2 flex-1 grid grid-cols-5 h-1 min-h-fit">
|
||||||
|
<PlayerSidebar />
|
||||||
|
{@render children()}
|
||||||
|
<QueueFrame />
|
||||||
|
</div>
|
||||||
|
|
||||||
{@render children()}
|
<div class="border border-2 min-h-24 h-24">
|
||||||
|
<PlayerControls />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
|
@ -1,249 +1,30 @@
|
||||||
<svelte:options runes={true} />
|
<svelte:options runes={true} />
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {
|
|
||||||
type Parameter, type Song, type NowPlayingResponse, type GetPlayQueueResponse,
|
|
||||||
OpenSubsonic
|
|
||||||
} from "$lib/opensubsonic";
|
|
||||||
import {onMount} from "svelte";
|
import {onMount} from "svelte";
|
||||||
import QueueFrame from "$lib/components/custom/QueueFrame.svelte";
|
import AlbumsView from "$lib/components/custom/Views/AlbumsView.svelte";
|
||||||
import {Button} from "$lib/components/ui/button";
|
import {View} from "$lib/components/custom/Views/views.svelte";
|
||||||
import {PlaybackMode, PlaybackStateSvelte} from "$lib/player.svelte.js";
|
import {queueState} from "$lib/states/play-queue.svelte";
|
||||||
import PlayerSidebar from "$lib/components/custom/PlayerSidebar.svelte";
|
import {playbackState} from "$lib/states/playback-state.svelte";
|
||||||
import AlbumView from "$lib/components/custom/Views/AlbumView.svelte";
|
import {viewState} from "$lib/states/view-state.svelte";
|
||||||
import {AlbumViews, Views} from "$lib/components/custom/Views/views.svelte";
|
|
||||||
|
|
||||||
function toFixedNumber(num, digits, base){
|
|
||||||
const pow = Math.pow(base ?? 10, digits);
|
|
||||||
return Math.round(num*pow) / pow;
|
|
||||||
}
|
|
||||||
|
|
||||||
let source: HTMLAudioElement = $state();
|
|
||||||
let mode = $state(new PlaybackStateSvelte());
|
|
||||||
let title = $state("");
|
|
||||||
let artist = $state("");
|
|
||||||
let queueIndex = $state(0);
|
|
||||||
let currentSong = $state({
|
|
||||||
data: {
|
|
||||||
title,
|
|
||||||
artist
|
|
||||||
},
|
|
||||||
source,
|
|
||||||
queueIndex
|
|
||||||
});
|
|
||||||
let isPaused = $state(true);
|
|
||||||
let volume = $state(0.2);
|
|
||||||
let progress = $state(0);
|
|
||||||
let duration = $state(0);
|
|
||||||
let viewMode = $state(Views.Albums);
|
|
||||||
let subViewMode = $state(AlbumViews.All);
|
|
||||||
|
|
||||||
let queue: Array<Song> = $state([]);
|
|
||||||
|
|
||||||
async function fetchQueue() {
|
|
||||||
const data: GetPlayQueueResponse = await OpenSubsonic.get("getPlayQueue");
|
|
||||||
if (data && data.playQueue.entry) {
|
|
||||||
queue = [];
|
|
||||||
queue = queue.concat(data.playQueue.entry);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchNowPlaying() {
|
|
||||||
const data: NowPlayingResponse = await OpenSubsonic.get("getNowPlaying");
|
|
||||||
let foundInNowPlaying = false;
|
|
||||||
if (data && data.nowPlaying.entry) {
|
|
||||||
data.nowPlaying.entry.forEach((entry) => {
|
|
||||||
if (entry.username == OpenSubsonic.username) {
|
|
||||||
newSong(entry);
|
|
||||||
foundInNowPlaying = true;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (!foundInNowPlaying && queue.length != 0) {
|
|
||||||
newSong(queue[0])
|
|
||||||
currentSong.queueIndex = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveQueue() {
|
|
||||||
let songs: Array<Parameter> = [];
|
|
||||||
|
|
||||||
if (queue.length === 0) {
|
|
||||||
songs.push({key: "id", value: ""})
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
queue.forEach((song, idx) => {
|
|
||||||
if (idx === 0) {
|
|
||||||
songs.push({key: "current", value: song.id})
|
|
||||||
songs.push({key: "id", value: song.id})
|
|
||||||
// Add Progress within current song
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
songs.push({key: "id", value: song.id})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const data = await OpenSubsonic.get("savePlayQueue", songs);
|
|
||||||
if (data) {
|
|
||||||
await fetchQueue();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeSongFromQueue(idx: number) {
|
|
||||||
if (idx > -1) {
|
|
||||||
queue.splice(idx, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function playSong(song: Song, songIndex: number) {
|
|
||||||
pause();
|
|
||||||
newSong(song);
|
|
||||||
currentSong.queueIndex = songIndex;
|
|
||||||
play();
|
|
||||||
}
|
|
||||||
|
|
||||||
function newSong(song: Song) {
|
|
||||||
let parameters: Array<Parameter> = [
|
|
||||||
{ key: "id", value: song.id },
|
|
||||||
//{ key: "maxBitRate", value: } // TODO
|
|
||||||
//{ key: "format", value: } // TODO
|
|
||||||
//{ key: "timeOffset", value: } // TODO? Only Video related
|
|
||||||
//{ key: "size", value: } // TODO? Only Video related
|
|
||||||
{ key: "estimateContentLength", value: "true" },
|
|
||||||
//{ key: "converted", value: } // TODO? Only Video related
|
|
||||||
];
|
|
||||||
let url = OpenSubsonic.getApiUrl("stream", parameters);
|
|
||||||
currentSong.source = new Audio(url); // Assign new URL
|
|
||||||
currentSong.data = song;
|
|
||||||
|
|
||||||
// Reassign Event Handlers
|
|
||||||
currentSong.source.onloadedmetadata = () => {
|
|
||||||
duration = currentSong.source.duration;
|
|
||||||
};
|
|
||||||
|
|
||||||
currentSong.source.onplay = () => {
|
|
||||||
currentSong.source.volume = volume;
|
|
||||||
isPaused = currentSong.source.paused;
|
|
||||||
let time: number = Date.now();
|
|
||||||
let parameters: Array<Parameter> = [
|
|
||||||
{ key: "id", value: song.id },
|
|
||||||
{ key: "time", value: time},
|
|
||||||
{ key: "submission", value: false}
|
|
||||||
];
|
|
||||||
OpenSubsonic.get("scrobble", parameters);
|
|
||||||
}
|
|
||||||
|
|
||||||
currentSong.source.onpause = () => {
|
|
||||||
isPaused = currentSong.source.paused;
|
|
||||||
}
|
|
||||||
|
|
||||||
currentSong.source.ontimeupdate = () => {
|
|
||||||
progress = currentSong.source.currentTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
currentSong.source.onended = () => {
|
|
||||||
if (mode.current == PlaybackMode.LoopOne) {
|
|
||||||
currentSong.source.play();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
currentSong.source.load();
|
|
||||||
}
|
|
||||||
|
|
||||||
function play() {
|
|
||||||
currentSong.source.play().catch(() => {});
|
|
||||||
}
|
|
||||||
function pause() {
|
|
||||||
currentSong.source.pause();
|
|
||||||
}
|
|
||||||
|
|
||||||
function changeVolume(change: number) {
|
|
||||||
if (volume + change > 1) {
|
|
||||||
volume = 1;
|
|
||||||
}
|
|
||||||
else if (volume + change < 0) {
|
|
||||||
volume = 0;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
volume = toFixedNumber(volume + change, 2, 10) ;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentSong.source) {
|
|
||||||
currentSong.source.volume = volume;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function progressPercent() {
|
|
||||||
return progress / duration * 100 || 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
function displayTime(rawSeconds: number) {
|
|
||||||
const intSeconds = Math.round(rawSeconds);
|
|
||||||
const seconds = intSeconds % 60;
|
|
||||||
const minutes = Math.floor((intSeconds / 60)) % 60;
|
|
||||||
const hours = Math.floor((intSeconds / 3600));
|
|
||||||
|
|
||||||
if (hours == 0) {
|
|
||||||
return `${minutes.toString()}:${seconds.toString().padStart(2, 0)}`
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return `${hours}:${minutes.toString().padStart(2, 0)}:${seconds.toString().padStart(2, 0)}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
fetchQueue().then(() => {
|
queueState.getPlayQueue(true);
|
||||||
fetchNowPlaying();
|
playbackState.song = new Audio(); // needed so source is not undefined
|
||||||
});
|
|
||||||
currentSong.source = new Audio(); // needed so source is not undefined
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>Lydstyrke - {currentSong.data.title}({currentSong.data.artist})</title>
|
<title>Lydstyrke - {playbackState.metaData.title}({playbackState.metaData.artist})</title>
|
||||||
<meta name="robots" content="noindex nofollow" />
|
<meta name="robots" content="noindex nofollow" />
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
{#snippet centerModule()}
|
<div class="border border-2 col-span-3 overflow-hidden">
|
||||||
<div class="border border-2 col-span-3 overflow-hidden">
|
{#if viewState.current.mode === View.Albums}es
|
||||||
{#if viewMode === Views.Albums}
|
<AlbumsView />
|
||||||
<AlbumView viewMode={subViewMode}/>
|
{:else if viewState.current.mode === View.Artists}
|
||||||
{:else if viewMode === Views.Artists}
|
<p>Get Fucked</p>
|
||||||
<p>Get Fucked</p>
|
{/if}
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/snippet}
|
|
||||||
|
|
||||||
{#snippet leftModule()}
|
|
||||||
<PlayerSidebar bind:view={viewMode} bind:subView={subViewMode} />
|
|
||||||
{/snippet}
|
|
||||||
|
|
||||||
{#snippet rightModule()}
|
|
||||||
<QueueFrame {queue} {fetchQueue} {saveQueue} {removeSongFromQueue} {playSong} currentIndex={currentSong.queueIndex} />
|
|
||||||
{/snippet}
|
|
||||||
|
|
||||||
<div class="border border-2 flex-1 grid grid-cols-5 h-1 min-h-fit">
|
|
||||||
{@render leftModule()}
|
|
||||||
{@render centerModule()}
|
|
||||||
{@render rightModule()}
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<div class="border border-2 min-h-24 h-24">
|
|
||||||
<div class="flex flex-row gap-2">
|
|
||||||
<p class="border p-2">Song: {currentSong.data.title}</p>
|
|
||||||
<p class="border p-2">Volume: {volume}</p>
|
|
||||||
<p class="border p-2">{displayTime(progress)}/{displayTime(duration)}</p>
|
|
||||||
<p class="border p-2">{progressPercent().toFixed(2)}%</p>
|
|
||||||
<Button class="border p-2" onclick={() => changeVolume(-0.05)}>-</Button>
|
|
||||||
<Button class="border p-2" onclick={() => changeVolume(0.05)}>+</Button>
|
|
||||||
<Button class="border p-2" onclick={() => currentSong.source.currentTime -= 5}>{"<<"}</Button>
|
|
||||||
{#if isPaused}
|
|
||||||
<Button onclick={play}>Play</Button>
|
|
||||||
{:else}
|
|
||||||
<Button onclick={pause}>Pause</Button>
|
|
||||||
{/if}
|
|
||||||
<Button class="border p-2" onclick={() => currentSong.source.currentTime += 5}>{">>"}</Button>
|
|
||||||
<Button class="border p-2" onclick={() => mode.next()}>{mode.get()}</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue