diff options
| author | gammelalf <gammelalf@hopfen.space> | 2023-01-23 03:12:21 +0100 |
|---|---|---|
| committer | Florian Fischer <florian.fischer@muhq.space> | 2023-02-24 12:47:40 +0100 |
| commit | 7b9d908d2c1612843990dd39decbdbf40e51dbd7 (patch) | |
| tree | 540f2c6aaf25f9251e4666dc56f0afbf85b2944c /web/src | |
| parent | 95f95783383a58af5250bcb66155a04971385b75 (diff) | |
| download | WRMS-7b9d908d2c1612843990dd39decbdbf40e51dbd7.tar.gz WRMS-7b9d908d2c1612843990dd39decbdbf40e51dbd7.zip | |
Reimplemented current frontend in svelte
Diffstat (limited to 'web/src')
| -rw-r--r-- | web/src/app.d.ts | 12 | ||||
| -rw-r--r-- | web/src/app.html | 13 | ||||
| -rw-r--r-- | web/src/lib/api.ts | 40 | ||||
| -rw-r--r-- | web/src/routes/+page.svelte | 159 | ||||
| -rw-r--r-- | web/src/routes/+page.ts | 1 |
5 files changed, 225 insertions, 0 deletions
diff --git a/web/src/app.d.ts b/web/src/app.d.ts new file mode 100644 index 0000000..f59b884 --- /dev/null +++ b/web/src/app.d.ts @@ -0,0 +1,12 @@ +// See https://kit.svelte.dev/docs/types#app +// for information about these interfaces +declare global { + namespace App { + // interface Error {} + // interface Locals {} + // interface PageData {} + // interface Platform {} + } +} + +export {}; diff --git a/web/src/app.html b/web/src/app.html new file mode 100644 index 0000000..1d67ac6 --- /dev/null +++ b/web/src/app.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <title>WRMS</title> + <meta charset="utf-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + <link rel="stylesheet" href="%sveltekit.assets%/client.css" /> + %sveltekit.head% + </head> + <body> + <div>%sveltekit.body%</div> + </body> +</html> diff --git a/web/src/lib/api.ts b/web/src/lib/api.ts new file mode 100644 index 0000000..032cbd4 --- /dev/null +++ b/web/src/lib/api.ts @@ -0,0 +1,40 @@ +export type Song = { + title: string; + artist: string; + source: string; + uri: string; + weight: number; +}; + +export type WsMessage = { + cmd: 'add' | 'update' | 'pause' | 'play' | 'upvoted' | 'downvoted'; + songs: Array<Song>; +}; +export type SearchResponse = { + cmd: 'search'; + songs: Array<Song>; +}; + +const HEADERS = new Headers([ + ['Accept', 'application/json'], + ['Content-Type', 'application/json'] +]); +const API = { + baseUrl: '', + get(url: string, body?: Record<string, string>) { + if (body !== undefined) { + url += '?' + new URLSearchParams(body); + } + return fetch(this.baseUrl + url, { + method: 'GET' + }); + }, + post(url: string, body?: object) { + return fetch(this.baseUrl + url, { + method: 'POST', + body: body && JSON.stringify(body), + headers: HEADERS + }); + } +}; +export default API; diff --git a/web/src/routes/+page.svelte b/web/src/routes/+page.svelte new file mode 100644 index 0000000..1cb4c71 --- /dev/null +++ b/web/src/routes/+page.svelte @@ -0,0 +1,159 @@ +<script lang="ts"> + import { onDestroy, onMount } from 'svelte'; + import addSVG from '../../static/play.svg'; + import playSVG from '../../static/play.svg'; + import pauseSVG from '../../static/pause.svg'; + import type { WsMessage, Song, SearchResponse } from '../lib/api'; + import API from '../lib/api'; + API.baseUrl = '/'; + + let status: 'paused' | 'playing' = 'paused'; + let currentSong: Song | null = null; + let playlist: Array<Song> = []; + let votes: Map<string, 'up' | 'down'> = new Map(); + + let search = ''; + let searchResults: Array<Song> = []; + + let socket: WebSocket; + onMount(function () { + socket = new WebSocket('ws://localhost:8080/ws'); + console.info('Attempting Connection...'); + + socket.onopen = () => { + console.info('Successfully Connected'); + }; + + socket.onclose = (event) => { + console.info('Socket Closed Connection: ', event); + }; + + socket.onerror = (error) => { + console.error('Socket Error: ', error); + }; + + socket.onmessage = ({ data }) => { + const { cmd, songs }: WsMessage = JSON.parse(data); + console.debug('Ws Message', cmd, songs); + switch (cmd) { + case 'add': + playlist = [...playlist, ...songs]; + break; + case 'update': + for (const song of songs) { + playlist[ + playlist.findIndex(({ uri }) => { + return uri === song.uri; + }) + ] = song; + } + break; + case 'pause': + status = 'paused'; + break; + case 'play': + status = 'playing'; + currentSong = songs[0]; + playlist = playlist.filter(({ uri }) => uri !== currentSong?.uri); + break; + case 'upvoted': + for (const { uri } of songs) { + votes.set(uri, 'up'); + } + votes = votes; + break; + case 'downvoted': + for (const { uri } of songs) { + votes.set(uri, 'down'); + } + votes = votes; + break; + } + }; + }); + onDestroy(function () { + socket?.close(); + }); + + $: playlist = playlist.sort(function (a, b) { + return b.weight - a.weight; + }); + + async function submitSearch() { + const response = await API.get('search', { pattern: search }); + const { songs }: SearchResponse = await response.json(); + searchResults = songs; + } + + function vote(direction: 'up' | 'down', uri: string) { + return async function () { + let action; + if (votes.get(uri) === direction) { + votes.delete(uri); + action = 'unvote'; + } else { + votes.set(uri, direction); + action = direction; + } + const response = await API.get(action, { song: uri }); + console.debug(response); + }; + } +</script> + +<div class="flex-column"> + <h2>Currently Playing</h2> + <p>{currentSong?.title || 'Nothing'}</p> + <div id="controls"> + <button class="icon-button play" on:click={() => API.get('playpause').then(console.debug)}> + {#if status === 'playing'} + <img src={pauseSVG} alt="Pause" /> + {:else} + <img src={playSVG} alt="Play" /> + {/if} + </button> + </div> + <div class="flex-row"> + <div class="flex-column"> + <h1>Add your song</h1> + <form on:submit|preventDefault={submitSearch}> + <label><input bind:value={search} placeholder="Title/Artist/Album/..." /></label> + <button formaction="submit">Search</button> + {#each searchResults as { title, artist, source, uri, weight } (uri)} + <div class="flex-row search-results"> + <button + class="icon-button" + on:click={() => + API.post('add', { title, artist, source, uri, weight }).then(console.debug)} + > + <img src={addSVG} alt="Add" /> + </button> + {artist + ' - ' + title} + <small>{source}</small> + </div> + {/each} + </form> + </div> + <div class="flex-column"> + <h1>Playlist</h1> + {#each playlist as { title, artist, source, uri, weight } (uri)} + <div> + <button class="vote" on:click={vote('up', uri)}>Up</button> + <button class="vote" on:click={vote('down', uri)}>Down</button> + {`${weight} ${artist} - ${title}`} + <small>{`(${source})`}</small> + </div> + {/each} + </div> + </div> +</div> + +<style> + .search-results > button { + --icon-size: 2em; + } + .search-results > small { + color: var(--secondary); + margin-left: 0.5em; + } +</style> diff --git a/web/src/routes/+page.ts b/web/src/routes/+page.ts new file mode 100644 index 0000000..189f71e --- /dev/null +++ b/web/src/routes/+page.ts @@ -0,0 +1 @@ +export const prerender = true; |
