aboutsummaryrefslogtreecommitdiff
path: root/web/src
diff options
context:
space:
mode:
authorgammelalf <gammelalf@hopfen.space>2023-01-23 03:12:21 +0100
committerFlorian Fischer <florian.fischer@muhq.space>2023-02-24 12:47:40 +0100
commit7b9d908d2c1612843990dd39decbdbf40e51dbd7 (patch)
tree540f2c6aaf25f9251e4666dc56f0afbf85b2944c /web/src
parent95f95783383a58af5250bcb66155a04971385b75 (diff)
downloadWRMS-7b9d908d2c1612843990dd39decbdbf40e51dbd7.tar.gz
WRMS-7b9d908d2c1612843990dd39decbdbf40e51dbd7.zip
Reimplemented current frontend in svelte
Diffstat (limited to 'web/src')
-rw-r--r--web/src/app.d.ts12
-rw-r--r--web/src/app.html13
-rw-r--r--web/src/lib/api.ts40
-rw-r--r--web/src/routes/+page.svelte159
-rw-r--r--web/src/routes/+page.ts1
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;