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">
|
||||
import {Button} from "$lib/components/ui/button";
|
||||
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);
|
||||
|
||||
$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(() => {
|
||||
mounted = true;
|
||||
|
@ -15,14 +48,22 @@
|
|||
|
||||
<div class="flex flex-row gap-2">
|
||||
{#if mounted}
|
||||
<p>Paused: {audio.paused}</p>
|
||||
<p>Volume: {audio.volume}</p>
|
||||
<p>Duration: {audio.duration}</p>
|
||||
{#if audio.paused}
|
||||
<Button onclick={play}>Play</Button>
|
||||
{:else}
|
||||
<Button onclick={pause}>Pause</Button>
|
||||
{/if}
|
||||
<div class="flex flex-row gap-2">
|
||||
<p class="border p-2">Song: {playbackState.metaData.title}</p>
|
||||
<p class="border p-2">Volume: {playbackState.volume}</p>
|
||||
<p class="border p-2">{displayTime(playbackState.progress)}/{displayTime(playbackState.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={() => 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}
|
||||
<p>Nothing going on here</p>
|
||||
{/if}
|
||||
|
|
|
@ -3,28 +3,28 @@
|
|||
<script lang="ts">
|
||||
import {Separator} from "$lib/components/ui/separator";
|
||||
import {Button} from "$lib/components/ui/button";
|
||||
import {Views} from "$lib/components/custom/Views/views.svelte";
|
||||
import {AlbumViews} from "$lib/components/custom/Views/views.svelte.js";
|
||||
import {View} from "$lib/components/custom/Views/views.svelte";
|
||||
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([]);
|
||||
</script>
|
||||
|
||||
<div class="border border-2 col-span-1 flex flex-col gap-1 p-2">
|
||||
<div class="flex flex-col gap-2">
|
||||
<Button onclick={() => view=Views.Albums} class="flex flex-col h-fit">Albums
|
||||
<Button variant="secondary" onclick={() => subView=AlbumViews.All}>All</Button>
|
||||
<Button variant="secondary" onclick={() => subView=AlbumViews.Random}>Random</Button>
|
||||
<Button variant="secondary" onclick={() => subView=AlbumViews.Favourites}>Favourites</Button>
|
||||
<Button variant="secondary" onclick={() => subView=AlbumViews.TopRated}>Top Rated</Button>
|
||||
<Button variant="secondary" onclick={() => subView=AlbumViews.RecentlyAdded}>Recently Added</Button>
|
||||
<Button variant="secondary" onclick={() => subView=AlbumViews.RecentlyPlayed}>Recently Played</Button>
|
||||
<Button variant="secondary" onclick={() => subView=AlbumViews.MostPlayed}>Most Played</Button>
|
||||
<Button onclick={() => viewState.setMode(View.Albums)} class="flex flex-col h-fit">Albums
|
||||
<Button variant="secondary" onclick={() => viewState.setSubMode(AlbumView.All)}>All</Button>
|
||||
<Button variant="secondary" onclick={() => viewState.setSubMode(AlbumView.Random)}>Random</Button>
|
||||
<Button variant="secondary" onclick={() => viewState.setSubMode(AlbumView.Favourites)}>Favourites</Button>
|
||||
<Button variant="secondary" onclick={() => viewState.setSubMode(AlbumView.TopRated)}>Top Rated</Button>
|
||||
<Button variant="secondary" onclick={() => viewState.setSubMode(AlbumView.RecentlyAdded)}>Recently Added</Button>
|
||||
<Button variant="secondary" onclick={() => viewState.setSubMode(AlbumView.RecentlyPlayed)}>Recently Played</Button>
|
||||
<Button variant="secondary" onclick={() => viewState.setSubMode(AlbumView.MostPlayed)}>Most Played</Button>
|
||||
</Button>
|
||||
|
||||
</div>
|
||||
<Separator />
|
||||
<Button onclick={() => view=Views.Artists}>Artists</Button>
|
||||
<Button onclick={() => viewState.setMode(View.Artists)}>Artists</Button>
|
||||
<h1>Songs</h1>
|
||||
<h1>Radios</h1>
|
||||
<h1>Shares</h1>
|
||||
|
|
|
@ -5,28 +5,54 @@
|
|||
import {Button} from "$lib/components/ui/button";
|
||||
import {timeFormat} from "$lib/time-format";
|
||||
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);
|
||||
|
||||
async function innerFetchQueue() {
|
||||
loading = true;
|
||||
await fetchQueue()
|
||||
await queueState.getPlayQueue()
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
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>
|
||||
|
||||
{#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()}
|
||||
{#each queue as song, idx}
|
||||
{#if currentIndex == idx}
|
||||
<p class="bg-secondary" onclick={() => playSong(song, idx)}>{song.artist} - {song.title} ({timeFormat(song.duration)})<Button onclick={() => removeSongFromQueue(idx)}>X</Button></p>
|
||||
{#each queueState.queue as song, idx}
|
||||
{#if idx < queueState.currentIndex}
|
||||
<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}
|
||||
<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}
|
||||
{/each}
|
||||
|
||||
<Button onclick={() => queue.push(queue[0])}>Add</Button>
|
||||
<Button onclick={() => queueState.queue.push(queueState.queue[0])}>Add</Button>
|
||||
{/snippet}
|
||||
|
||||
<div class="border border-2 col-span-1 flex flex-col gap-1 p-2">
|
||||
|
@ -34,7 +60,7 @@
|
|||
<h1>Current Queue</h1>
|
||||
<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={saveQueue}>Upload</Button>
|
||||
<Button variant="outline" class="h-8 w-12" onclick={() => queueState.saveQueue()}>Upload</Button>
|
||||
</div>
|
||||
<Separator />
|
||||
{#if loading}
|
||||
|
|
|
@ -1,16 +1,14 @@
|
|||
<svelte:options runes={true} />
|
||||
|
||||
<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 LoadingSpinner from "$lib/components/custom/LoadingSpinner.svelte";
|
||||
import ViewCard from "$lib/components/custom/Views/ViewCard.svelte";
|
||||
import {type AlbumID3, type GetAlbumList2Response, OpenSubsonic, type Parameter} from "$lib/opensubsonic";
|
||||
import {Skeleton} from "$lib/components/ui/skeleton";
|
||||
import {goto} from "$app/navigation";
|
||||
|
||||
let { viewMode = $bindable() }: { viewMode: AlbumViews } = $props();
|
||||
let previousView = $state(viewMode);
|
||||
let albums: Array<AlbumID3> = $state([]);
|
||||
let loading = $state(true);
|
||||
let paginating = $state(false);
|
||||
|
@ -22,7 +20,7 @@
|
|||
|
||||
async function fetchAlbums() {
|
||||
let parameters: Array<Parameter> = [
|
||||
{key: "type", value: viewMode},
|
||||
{key: "type", value: viewState.current.subMode},
|
||||
{key: "size", value: paginationIncrement},
|
||||
{key: "offset", value: pagination},
|
||||
];
|
||||
|
@ -50,14 +48,17 @@
|
|||
})
|
||||
|
||||
$effect(() => {
|
||||
console.log(viewMode);
|
||||
if (viewMode !== previousView) {
|
||||
loading = true;
|
||||
albums = [];
|
||||
pagination = 0;
|
||||
fetchAlbums();
|
||||
previousView = viewMode
|
||||
console.log(viewState.current.subMode);
|
||||
if (viewState.previous) {
|
||||
if (viewState.current.subMode !== viewState.previous.subMode) {
|
||||
loading = true;
|
||||
albums = [];
|
||||
pagination = 0;
|
||||
fetchAlbums();
|
||||
viewState.previous.mode = viewState.current.mode
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
function selectAlbum(albumId) {
|
||||
|
@ -67,7 +68,7 @@
|
|||
|
||||
onMount(() => {
|
||||
fetchAlbums();
|
||||
previousView = viewMode;
|
||||
viewState.previous = viewState.current;
|
||||
})
|
||||
</script>
|
||||
|
|
@ -3,9 +3,9 @@
|
|||
<script lang="ts">
|
||||
import {Separator} from "$lib/components/ui/separator";
|
||||
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,]
|
||||
</script>
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
export enum AlbumViews {
|
||||
export enum AlbumView {
|
||||
All = "alphabeticalByName",
|
||||
Random = "random",
|
||||
Favourites = "starred",
|
||||
|
@ -8,10 +8,13 @@ export enum AlbumViews {
|
|||
MostPlayed = "frequent"
|
||||
}
|
||||
|
||||
export enum Views {
|
||||
export enum View {
|
||||
Albums = "Albums",
|
||||
Artists = "Artists",
|
||||
Songs = "Songs",
|
||||
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>
|
||||
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 viewMode = $state(View.Albums);
|
||||
let subViewMode = $state(AlbumView.All);
|
||||
|
||||
onMount(() => {
|
||||
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="h-full flex flex-col">
|
||||
<div class="border border-2 p-1">
|
||||
<h1>Navbar</h1>
|
||||
</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>
|
|
@ -1,249 +1,30 @@
|
|||
<svelte:options runes={true} />
|
||||
|
||||
<script lang="ts">
|
||||
import {
|
||||
type Parameter, type Song, type NowPlayingResponse, type GetPlayQueueResponse,
|
||||
OpenSubsonic
|
||||
} from "$lib/opensubsonic";
|
||||
import {onMount} from "svelte";
|
||||
import QueueFrame from "$lib/components/custom/QueueFrame.svelte";
|
||||
import {Button} from "$lib/components/ui/button";
|
||||
import {PlaybackMode, PlaybackStateSvelte} from "$lib/player.svelte.js";
|
||||
import PlayerSidebar from "$lib/components/custom/PlayerSidebar.svelte";
|
||||
import AlbumView from "$lib/components/custom/Views/AlbumView.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)}`
|
||||
}
|
||||
}
|
||||
import AlbumsView from "$lib/components/custom/Views/AlbumsView.svelte";
|
||||
import {View} from "$lib/components/custom/Views/views.svelte";
|
||||
import {queueState} from "$lib/states/play-queue.svelte";
|
||||
import {playbackState} from "$lib/states/playback-state.svelte";
|
||||
import {viewState} from "$lib/states/view-state.svelte";
|
||||
|
||||
onMount(() => {
|
||||
fetchQueue().then(() => {
|
||||
fetchNowPlaying();
|
||||
});
|
||||
currentSong.source = new Audio(); // needed so source is not undefined
|
||||
queueState.getPlayQueue(true);
|
||||
playbackState.song = new Audio(); // needed so source is not undefined
|
||||
})
|
||||
</script>
|
||||
|
||||
<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" />
|
||||
</svelte:head>
|
||||
|
||||
{#snippet centerModule()}
|
||||
<div class="border border-2 col-span-3 overflow-hidden">
|
||||
{#if viewMode === Views.Albums}
|
||||
<AlbumView viewMode={subViewMode}/>
|
||||
{:else if viewMode === Views.Artists}
|
||||
<p>Get Fucked</p>
|
||||
{/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 class="border border-2 col-span-3 overflow-hidden">
|
||||
{#if viewState.current.mode === View.Albums}es
|
||||
<AlbumsView />
|
||||
{:else if viewState.current.mode === View.Artists}
|
||||
<p>Get Fucked</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
||||
|
|
Loading…
Reference in a new issue