Frontend
This commit is contained in:
parent
59e243472b
commit
9f9b911ca0
|
@ -2,12 +2,7 @@ from flask import Flask, render_template, request, jsonify
|
||||||
from .controller import Controller
|
from .controller import Controller
|
||||||
|
|
||||||
|
|
||||||
app = Flask(
|
app = Flask(__name__, static_folder="./client/build", static_url_path="/assets")
|
||||||
__name__,
|
|
||||||
static_folder="./frontend/dist",
|
|
||||||
static_url_path="/",
|
|
||||||
template_folder="./frontend/dist",
|
|
||||||
)
|
|
||||||
controller = Controller()
|
controller = Controller()
|
||||||
|
|
||||||
|
|
||||||
|
@ -46,13 +41,12 @@ def gain():
|
||||||
return jsonify(controller.set_gain(amount))
|
return jsonify(controller.set_gain(amount))
|
||||||
|
|
||||||
|
|
||||||
@app.route("/")
|
@app.route("/", defaults={"path": ""})
|
||||||
@app.route("/<path:path>")
|
@app.route("/<path:path>")
|
||||||
def index(path: str = ""):
|
def index(path: str):
|
||||||
print(f"PATH: {path}")
|
return render_template("index.html.j2")
|
||||||
return render_template("index.html")
|
|
||||||
|
|
||||||
|
|
||||||
@app.errorhandler(404)
|
# @app.errorhandler(404)
|
||||||
def not_found(*args, **kwargs):
|
# def not_found(*args, **kwargs):
|
||||||
return render_template("index.html")
|
# return render_template("index.html.j2")
|
||||||
|
|
|
@ -2234,6 +2234,11 @@
|
||||||
"integrity": "sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg==",
|
"integrity": "sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"regexparam": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/regexparam/-/regexparam-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-6IQpFBv6e5vz1QAqI+V4k8P2e/3gRrqfCJ9FI+O1FLQTO+Uz6RXZEZOPmTJ6hlGj7gkERzY5BRCv09whKP96/g=="
|
||||||
|
},
|
||||||
"regexpu-core": {
|
"regexpu-core": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-1.0.0.tgz",
|
||||||
|
@ -2518,6 +2523,14 @@
|
||||||
"resolved": "https://registry.npmjs.org/svelte/-/svelte-3.16.7.tgz",
|
"resolved": "https://registry.npmjs.org/svelte/-/svelte-3.16.7.tgz",
|
||||||
"integrity": "sha512-egrva1UklB1n7KAv179IhDpQzMGAvubJUlOQ9PitmmZmAfrCUEgrQnx2vPxn2s+mGV3aYegXvJ/yQ35N2SfnYQ=="
|
"integrity": "sha512-egrva1UklB1n7KAv179IhDpQzMGAvubJUlOQ9PitmmZmAfrCUEgrQnx2vPxn2s+mGV3aYegXvJ/yQ35N2SfnYQ=="
|
||||||
},
|
},
|
||||||
|
"svelte-spa-router": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/svelte-spa-router/-/svelte-spa-router-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-cIzRfHisJ87IzI9gyufkEXwaXT5hJrejMa3ryeClIzGLC2LzmKECfBhbitmYJ8gDfkto9tfZVXGvgQ9i7lmxXA==",
|
||||||
|
"requires": {
|
||||||
|
"regexparam": "^1.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"svgo": {
|
"svgo": {
|
||||||
"version": "1.3.2",
|
"version": "1.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/svgo/-/svgo-1.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/svgo/-/svgo-1.3.2.tgz",
|
||||||
|
|
|
@ -15,8 +15,16 @@
|
||||||
"> 0.5%",
|
"> 0.5%",
|
||||||
"last 2 versions",
|
"last 2 versions",
|
||||||
"Firefox ESR",
|
"Firefox ESR",
|
||||||
"not dead"
|
"not dead",
|
||||||
|
"iOS > 9.3"
|
||||||
],
|
],
|
||||||
|
"prettier": {
|
||||||
|
"tabWidth": 2,
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": true,
|
||||||
|
"bracketSpacing": false,
|
||||||
|
"arrowParens": "always"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"postcss": "^7.0.24",
|
"postcss": "^7.0.24",
|
||||||
"postcss-import": "^12.0.1",
|
"postcss-import": "^12.0.1",
|
||||||
|
@ -29,6 +37,7 @@
|
||||||
"tailwindcss": "^1.1.4"
|
"tailwindcss": "^1.1.4"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"svelte": "^3.16.7"
|
"svelte": "^3.16.7",
|
||||||
|
"svelte-spa-router": "^2.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
<script>
|
||||||
|
import Router from 'svelte-spa-router'
|
||||||
|
import routes from '../routes'
|
||||||
|
import Header from './Header.svelte'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="container py-4">
|
||||||
|
<Header />
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<Router {routes} />
|
||||||
|
</main>
|
||||||
|
</div>
|
|
@ -0,0 +1,60 @@
|
||||||
|
<script>
|
||||||
|
import {fade} from 'svelte/transition'
|
||||||
|
import {push} from 'svelte-spa-router'
|
||||||
|
import controller from '../controller'
|
||||||
|
import {connected} from '../stores'
|
||||||
|
|
||||||
|
const defaults = {
|
||||||
|
host: '0.0.0.0',
|
||||||
|
port: 9800
|
||||||
|
}
|
||||||
|
|
||||||
|
let host = defaults.host
|
||||||
|
let port = defaults.port
|
||||||
|
let submitButton
|
||||||
|
let error = ''
|
||||||
|
|
||||||
|
function connect() {
|
||||||
|
if (!host || !port) {
|
||||||
|
error = 'You must supply a host and a port number to connect'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
error = ''
|
||||||
|
const buttonValue = submitButton.value
|
||||||
|
submitButton.value = 'Connecting...'
|
||||||
|
|
||||||
|
controller
|
||||||
|
.connect(host, port)
|
||||||
|
.then((success) => {
|
||||||
|
connected.set(success)
|
||||||
|
push('/fonts')
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
error = 'Unable to connect. Try again or check the device.'
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
submitButton.value = buttonValue
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if error}
|
||||||
|
<p class="error" transition:fade>{error}</p>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<form on:submit|preventDefault={connect}>
|
||||||
|
<p>
|
||||||
|
<label for="host">Host</label>
|
||||||
|
<input type="text" name="host" id="host" bind:value={host} />
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<label for="port">Port</label>
|
||||||
|
<input type="number" name="port" id="port" min="0" bind:value={port} />
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p class="actions">
|
||||||
|
<button type="submit" bind:this={submitButton}>Connect</button>
|
||||||
|
</p>
|
||||||
|
</form>
|
|
@ -0,0 +1,51 @@
|
||||||
|
<script>
|
||||||
|
import {onMount} from 'svelte'
|
||||||
|
import {fade} from 'svelte/transition'
|
||||||
|
import {link} from 'svelte-spa-router'
|
||||||
|
import controller from '../controller'
|
||||||
|
|
||||||
|
let fonts = []
|
||||||
|
let loading = false
|
||||||
|
let error = ''
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
refresh()
|
||||||
|
})
|
||||||
|
|
||||||
|
function refresh() {
|
||||||
|
error = ''
|
||||||
|
loading = true
|
||||||
|
|
||||||
|
controller
|
||||||
|
.getSoundfonts()
|
||||||
|
.then((fetched) => {
|
||||||
|
fonts = fetched
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
error = 'Could not fetch list of fonts'
|
||||||
|
})
|
||||||
|
.finally(() => (loading = false))
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if error}
|
||||||
|
<p class="error" transition:fade>{error}</p>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<ul class="mb-4">
|
||||||
|
{#each fonts as font}
|
||||||
|
<li>
|
||||||
|
<a href="/fonts/{font.id}" use:link>{font.name}</a>
|
||||||
|
</li>
|
||||||
|
{:else}
|
||||||
|
{#if loading}
|
||||||
|
<p class="text-gray-500">Loading...</p>
|
||||||
|
{:else if !error}
|
||||||
|
<p class="text-red-500">No fonts!</p>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
{#if !loading}
|
||||||
|
<button on:click|preventDefault={refresh}>Refresh</button>
|
||||||
|
{/if}
|
|
@ -0,0 +1,75 @@
|
||||||
|
<script>
|
||||||
|
import {onMount} from 'svelte'
|
||||||
|
import {fade} from 'svelte/transition'
|
||||||
|
import controller from '../../controller'
|
||||||
|
|
||||||
|
export let params = {}
|
||||||
|
|
||||||
|
let instruments = []
|
||||||
|
let loading = false
|
||||||
|
let error = ''
|
||||||
|
let selected = undefined
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
refresh()
|
||||||
|
})
|
||||||
|
|
||||||
|
function refresh() {
|
||||||
|
error = ''
|
||||||
|
loading = true
|
||||||
|
|
||||||
|
controller
|
||||||
|
.getInstruments(params.id)
|
||||||
|
.then((fetched) => {
|
||||||
|
instruments = fetched
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
error = 'Could not fetch list of instruments'
|
||||||
|
})
|
||||||
|
.finally(() => (loading = false))
|
||||||
|
}
|
||||||
|
|
||||||
|
function select(instrument) {
|
||||||
|
error = ''
|
||||||
|
|
||||||
|
controller
|
||||||
|
.select({
|
||||||
|
channel: 0,
|
||||||
|
instrument: params.id,
|
||||||
|
bank: instrument.bank,
|
||||||
|
program: instrument.program
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
selected = instrument
|
||||||
|
})
|
||||||
|
.catch((err) => (error = 'Could not select instrument'))
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if error}
|
||||||
|
<p class="error" transition:fade>{error}</p>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<ul class="mb-4">
|
||||||
|
{#each instruments as instrument}
|
||||||
|
<li
|
||||||
|
class="bg-gray-200 dark:bg-gray-800 p-2 border-b border-gray-400
|
||||||
|
dark:border-gray-600 cursor-pointer flex items-center"
|
||||||
|
on:click|preventDefault={() => select(instrument)}>
|
||||||
|
<span class="flex-auto">{instrument.name}</span>
|
||||||
|
<span>
|
||||||
|
{#if selected === instrument}✅{/if}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
{:else}
|
||||||
|
{#if loading}
|
||||||
|
<p class="text-gray-500">Loading...</p>
|
||||||
|
{:else if !error}
|
||||||
|
<p>No instruments!</p>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
{#if !loading}
|
||||||
|
<button on:click|preventDefault={refresh}>Refresh</button>
|
||||||
|
{/if}
|
|
@ -0,0 +1,22 @@
|
||||||
|
<script>
|
||||||
|
import {link} from 'svelte-spa-router'
|
||||||
|
import {connected} from '../stores'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<header class="flex items-center mb-4">
|
||||||
|
<nav class="flex-auto horizontal">
|
||||||
|
<ul>
|
||||||
|
{#if $connected}
|
||||||
|
<li>
|
||||||
|
<a href="/fonts" use:link>Fonts</a>
|
||||||
|
</li>
|
||||||
|
{:else}
|
||||||
|
<li>
|
||||||
|
<a href="/connect" use:link>Connect</a>
|
||||||
|
</li>
|
||||||
|
{/if}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="flex-none">{$connected ? '✅' : '❌'}</div>
|
||||||
|
</header>
|
|
@ -0,0 +1,61 @@
|
||||||
|
class Controller {
|
||||||
|
constructor() {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
async connect(host = '0.0.0.0', port = 9800) {
|
||||||
|
const url = this.apiURL('/api/connect', {
|
||||||
|
host,
|
||||||
|
port
|
||||||
|
})
|
||||||
|
|
||||||
|
const response = await fetch(url)
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
return data.success
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSoundfonts() {
|
||||||
|
const url = this.apiURL('/api/soundfonts')
|
||||||
|
|
||||||
|
const response = await fetch(url)
|
||||||
|
|
||||||
|
return response.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
async getInstruments(font) {
|
||||||
|
const url = this.apiURL('/api/instruments', {
|
||||||
|
font
|
||||||
|
})
|
||||||
|
|
||||||
|
const response = await fetch(url)
|
||||||
|
|
||||||
|
return response.json()
|
||||||
|
}
|
||||||
|
|
||||||
|
async select({channel, instrument, bank, program}) {
|
||||||
|
const url = this.apiURL('/api/select', {channel, instrument, bank, program})
|
||||||
|
const response = await fetch(url)
|
||||||
|
const data = response.json()
|
||||||
|
|
||||||
|
return data.success
|
||||||
|
}
|
||||||
|
|
||||||
|
// Private
|
||||||
|
|
||||||
|
apiURL(path, params = {}) {
|
||||||
|
const url = new URL(document.location)
|
||||||
|
url.pathname = path
|
||||||
|
url.hash = ''
|
||||||
|
|
||||||
|
for (let [key, value] of Object.entries(params)) {
|
||||||
|
url.searchParams.append(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const controller = new Controller()
|
||||||
|
|
||||||
|
export default controller
|
|
@ -0,0 +1,73 @@
|
||||||
|
form {
|
||||||
|
& > p {
|
||||||
|
@apply mb-4;
|
||||||
|
|
||||||
|
@screen md {
|
||||||
|
@apply flex items-center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& label {
|
||||||
|
@apply font-semibold block w-full mb-2;
|
||||||
|
|
||||||
|
@screen md {
|
||||||
|
@apply w-1/6 inline-block mr-4 mb-0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& input:not([type="submit"]), & textarea, & select {
|
||||||
|
@apply w-full;
|
||||||
|
|
||||||
|
@screen md {
|
||||||
|
@apply flex-auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .actions {
|
||||||
|
@screen md {
|
||||||
|
@apply w-5/6 ml-auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input, textarea {
|
||||||
|
@apply bg-white text-gray-900 px-3 py-2 rounded border border-gray-300;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
@apply bg-gray-100;
|
||||||
|
}
|
||||||
|
|
||||||
|
@screen dark {
|
||||||
|
@apply bg-black text-gray-100 border border-gray-600;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
@apply bg-gray-900;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="submit"], button, .button {
|
||||||
|
@apply bg-gray-400 text-gray-900 px-4 py-2 rounded;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
@apply bg-red-100 text-red-900 p-2 mb-4 rounded;
|
||||||
|
|
||||||
|
@screen dark {
|
||||||
|
@apply bg-red-700 text-red-100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nav.horizontal {
|
||||||
|
& li {
|
||||||
|
@apply inline-block mr-4;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
@apply mr-0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& :any-link {
|
||||||
|
@apply underline;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
@import "tailwindcss/base";
|
||||||
|
@import "tailwindcss/components";
|
||||||
|
|
||||||
|
@import "./custom.css";
|
||||||
|
|
||||||
|
@import "tailwindcss/utilities";
|
|
@ -0,0 +1,10 @@
|
||||||
|
import './css/main.css'
|
||||||
|
import App from './components/App.svelte'
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
document.querySelector('#loading').remove()
|
||||||
|
|
||||||
|
const app = new App({
|
||||||
|
target: document.querySelector('#app')
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,9 @@
|
||||||
|
import Connect from './components/Connect.svelte'
|
||||||
|
import Fonts from './components/Fonts.svelte'
|
||||||
|
import Font from './components/Fonts/Font.svelte'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
'/connect': Connect,
|
||||||
|
'/fonts': Fonts,
|
||||||
|
'/fonts/:id': Font
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
import {writable} from 'svelte/store'
|
||||||
|
|
||||||
|
export const connected = writable(false)
|
||||||
|
export const fonts = writable([])
|
||||||
|
export const instruments = writable([])
|
|
@ -0,0 +1,17 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||||
|
<title>FluidControl</title>
|
||||||
|
<link rel="stylesheet" href="{{ url_for("static", filename="main.css") }}">
|
||||||
|
<script src="{{ url_for("static", filename="main.js") }}" type="text/javascript"></script>
|
||||||
|
</head>
|
||||||
|
<body class="bg-gray-100 dark:bg-gray-900 text-gray-900 dark:text-gray-100">
|
||||||
|
<div id="app">
|
||||||
|
<div id="loading" class="p-4">Loading…</div>
|
||||||
|
</div>
|
||||||
|
<noscript>You must have JavaScript enabled for FluidControl to work.</noscript>
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
Reference in New Issue