Initial interface
This commit is contained in:
parent
5d0a210e6d
commit
e2013351d1
13
ui/.eslintignore
Normal file
13
ui/.eslintignore
Normal file
@ -0,0 +1,13 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Ignore files for PNPM, NPM and YARN
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
yarn.lock
|
14
ui/.eslintrc.cjs
Normal file
14
ui/.eslintrc.cjs
Normal file
@ -0,0 +1,14 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ['eslint:recommended', 'plugin:svelte/recommended', 'prettier'],
|
||||
parserOptions: {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 2020,
|
||||
extraFileExtensions: ['.svelte']
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
es2017: true,
|
||||
node: true
|
||||
}
|
||||
};
|
10
ui/.gitignore
vendored
Normal file
10
ui/.gitignore
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
13
ui/.prettierignore
Normal file
13
ui/.prettierignore
Normal file
@ -0,0 +1,13 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Ignore files for PNPM, NPM and YARN
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
yarn.lock
|
9
ui/.prettierrc
Normal file
9
ui/.prettierrc
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"useTabs": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 100,
|
||||
"plugins": ["prettier-plugin-svelte"],
|
||||
"pluginSearchDirs": ["."],
|
||||
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
||||
}
|
32
ui/assets-dev.go
Normal file
32
ui/assets-dev.go
Normal file
@ -0,0 +1,32 @@
|
||||
//go:build dev
|
||||
// +build dev
|
||||
|
||||
package ui
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
var (
|
||||
Assets http.FileSystem
|
||||
StaticDir string = "ui/"
|
||||
)
|
||||
|
||||
func init() {
|
||||
flag.StringVar(&StaticDir, "static", StaticDir, "Directory containing static files")
|
||||
}
|
||||
|
||||
func sanitizeStaticOptions() error {
|
||||
StaticDir, _ = filepath.Abs(StaticDir)
|
||||
if _, err := os.Stat(StaticDir); os.IsNotExist(err) {
|
||||
StaticDir, _ = filepath.Abs(filepath.Join(filepath.Dir(os.Args[0]), "ui"))
|
||||
if _, err := os.Stat(StaticDir); os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
Assets = http.Dir(StaticDir)
|
||||
return nil
|
||||
}
|
28
ui/assets.go
Normal file
28
ui/assets.go
Normal file
@ -0,0 +1,28 @@
|
||||
//go:build !dev
|
||||
// +build !dev
|
||||
|
||||
package ui
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
//go:embed all:build
|
||||
var _assets embed.FS
|
||||
|
||||
var Assets http.FileSystem
|
||||
|
||||
func init() {
|
||||
sub, err := fs.Sub(_assets, "build")
|
||||
if err != nil {
|
||||
log.Fatal("Unable to cd to ui/build directory:", err)
|
||||
}
|
||||
Assets = http.FS(sub)
|
||||
}
|
||||
|
||||
func sanitizeStaticOptions() error {
|
||||
return nil
|
||||
}
|
2556
ui/package-lock.json
generated
Normal file
2556
ui/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
30
ui/package.json
Normal file
30
ui/package.json
Normal file
@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "ui",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"lint": "prettier --plugin-search-dir . --check . && eslint .",
|
||||
"format": "prettier --plugin-search-dir . --write ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-auto": "^2.0.0",
|
||||
"@sveltejs/kit": "^1.20.4",
|
||||
"eslint": "^8.28.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-svelte": "^2.30.0",
|
||||
"prettier": "^2.8.0",
|
||||
"prettier-plugin-svelte": "^2.10.1",
|
||||
"svelte": "^4.0.5",
|
||||
"vite": "^4.4.2"
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@sveltejs/adapter-static": "^2.0.3",
|
||||
"bootstrap": "^5.3.2",
|
||||
"bootstrap-icons": "^1.11.1",
|
||||
"sass": "^1.69.5"
|
||||
}
|
||||
}
|
71
ui/routes.go
Normal file
71
ui/routes.go
Normal file
@ -0,0 +1,71 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"git.nemunai.re/nemunaire/hathoris/config"
|
||||
)
|
||||
|
||||
func serveOrReverse(forced_url string, cfg *config.Config) gin.HandlerFunc {
|
||||
if cfg.DevProxy != "" {
|
||||
// Forward to the Vue dev proxy
|
||||
return func(c *gin.Context) {
|
||||
if u, err := url.Parse(cfg.DevProxy); err != nil {
|
||||
http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
|
||||
} else {
|
||||
if forced_url != "" {
|
||||
u.Path = path.Join(u.Path, forced_url)
|
||||
} else {
|
||||
u.Path = path.Join(u.Path, c.Request.URL.Path)
|
||||
}
|
||||
|
||||
if r, err := http.NewRequest(c.Request.Method, u.String(), c.Request.Body); err != nil {
|
||||
http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
|
||||
} else if resp, err := http.DefaultClient.Do(r); err != nil {
|
||||
http.Error(c.Writer, err.Error(), http.StatusBadGateway)
|
||||
} else {
|
||||
defer resp.Body.Close()
|
||||
|
||||
for key := range resp.Header {
|
||||
c.Writer.Header().Add(key, resp.Header.Get(key))
|
||||
}
|
||||
c.Writer.WriteHeader(resp.StatusCode)
|
||||
|
||||
io.Copy(c.Writer, resp.Body)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if forced_url != "" {
|
||||
// Serve forced_url
|
||||
return func(c *gin.Context) {
|
||||
c.FileFromFS(forced_url, Assets)
|
||||
}
|
||||
} else {
|
||||
// Serve requested file
|
||||
return func(c *gin.Context) {
|
||||
c.FileFromFS(c.Request.URL.Path, Assets)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func DeclareRoutes(router *gin.Engine, cfg *config.Config) {
|
||||
if cfg.DevProxy != "" {
|
||||
router.GET("/.svelte-kit/*_", serveOrReverse("", cfg))
|
||||
router.GET("/node_modules/*_", serveOrReverse("", cfg))
|
||||
router.GET("/@vite/*_", serveOrReverse("", cfg))
|
||||
router.GET("/@id/*_", serveOrReverse("", cfg))
|
||||
router.GET("/@fs/*_", serveOrReverse("", cfg))
|
||||
router.GET("/src/*_", serveOrReverse("", cfg))
|
||||
}
|
||||
|
||||
router.GET("/", serveOrReverse("", cfg))
|
||||
|
||||
router.GET("/_app/*_", serveOrReverse("", cfg))
|
||||
router.GET("/img/*_", serveOrReverse("", cfg))
|
||||
router.GET("/favicon.ico", serveOrReverse("", cfg))
|
||||
}
|
17
ui/src/app.html
Normal file
17
ui/src/app.html
Normal file
@ -0,0 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr" class="d-flex flex-column mh-100 h-100">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="description" content="" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#ffffff"/>
|
||||
<meta name="author" content="nemucorp">
|
||||
<meta name="robots" content="none">
|
||||
<base href="/">
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover" class="flex-fill d-flex flex-column">
|
||||
<noscript>Si la page ne charge pas, essayez <a href="/nojs.html">la version sans JavaScript</a>.</noscript>
|
||||
<div class="flex-fill d-flex flex-column justify-content-between" style="min-height: 100%">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
75
ui/src/hathoris.scss
Normal file
75
ui/src/hathoris.scss
Normal file
@ -0,0 +1,75 @@
|
||||
// Your variable overrides can go here, e.g.:
|
||||
// $h1-font-size: 3rem;
|
||||
//$primary: #ff485a;
|
||||
//$secondary: #ff7b88;
|
||||
|
||||
$blue: #2a9fd6;
|
||||
$indigo: #6610f2;
|
||||
$purple: #6f42c1;
|
||||
$pink: #e83e8c;
|
||||
$red: #c00;
|
||||
$orange: #fd7e14;
|
||||
$yellow: #f80;
|
||||
$green: #77b300;
|
||||
$teal: #20c997;
|
||||
$cyan: #93c;
|
||||
|
||||
$primary: $purple;
|
||||
$success: $green;
|
||||
$info: $cyan;
|
||||
$warning: $yellow;
|
||||
$danger: $red;
|
||||
|
||||
$min-contrast-ratio: 2.25;
|
||||
|
||||
$enable-shadows: true;
|
||||
$enable-gradients: true;
|
||||
$enable-responsive-font-sizes: true;
|
||||
|
||||
$link-color: $primary;
|
||||
|
||||
$navbar-padding-y: 0;
|
||||
$nav-link-padding-y: 0.2rem;
|
||||
|
||||
@import "bootstrap/scss/bootstrap";
|
||||
|
||||
a.btn, button.btn {
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
.fixed-bottom .nav-item a {
|
||||
border-top: 1px solid $pink;
|
||||
}
|
||||
.fixed-bottom .nav-item a.active {
|
||||
background: white;
|
||||
color: $pink;
|
||||
}
|
||||
|
||||
.marquee {
|
||||
height: 1.5em;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
.marquee p {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
transform: translateX(100%);
|
||||
animation: scroll-left 10s linear infinite;
|
||||
}
|
||||
@include media-breakpoint-up(md) {
|
||||
.marquee p {
|
||||
animation: scroll-left 25s linear infinite;
|
||||
}
|
||||
}
|
||||
@include media-breakpoint-up(xl) {
|
||||
.marquee p {
|
||||
animation: scroll-left 45s linear infinite;
|
||||
}
|
||||
}
|
||||
@keyframes scroll-left {
|
||||
0% { transform: translateX(100%); }
|
||||
100% { transform: translateX(-100%); }
|
||||
}
|
110
ui/src/lib/components/Mixer.svelte
Normal file
110
ui/src/lib/components/Mixer.svelte
Normal file
@ -0,0 +1,110 @@
|
||||
<script>
|
||||
function refreshMixers() {
|
||||
const mxrs = fetch('api/mixer', {headers: {'Accept': 'application/json'}}).then((res) => res.json());
|
||||
mxrs.then((m) => {
|
||||
mixers = m;
|
||||
altering_mixer = null;
|
||||
})
|
||||
}
|
||||
|
||||
export let showReadOnly = false;
|
||||
export let advanced = false;
|
||||
|
||||
let mixers = null;
|
||||
refreshMixers();
|
||||
setInterval(refreshMixers, 5000);
|
||||
|
||||
let altering_mixer = null;
|
||||
async function alterMixer(mixer, values) {
|
||||
if (altering_mixer) altering_mixer.abort();
|
||||
altering_mixer = setTimeout(() => {
|
||||
fetch(`api/mixer/${mixer.NumID}/values`, {headers: {'Accept': 'application/json'}, method: 'POST', body: JSON.stringify(values ? values : (advanced ? mixer.values : [mixer.values[0]]))}).then(refreshMixers);
|
||||
altering_mixer = null;
|
||||
}, 450);
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if !mixers}
|
||||
<div class="card-body d-flex flex-fill justify-content-center">
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<ul class="list-group list-group-flush">
|
||||
{#each mixers as mixer (mixer.NumID)}
|
||||
{#if showReadOnly || mixer.RW}
|
||||
<li class="list-group-item py-3">
|
||||
{#if mixer.items}
|
||||
<label for={mixer.Name + '0'} class="form-label">{mixer.Name}</label>
|
||||
{#if mixer.values}
|
||||
{#each mixer.values as cur, idx}
|
||||
<select
|
||||
class="form-select"
|
||||
disabled={!mixer.RW}
|
||||
id={mixer.Name + idx}
|
||||
bind:value={cur}
|
||||
on:change={() => alterMixer(mixer)}
|
||||
>
|
||||
{#each mixer.items as opt, idx}
|
||||
<option value={idx}>{opt}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{/each}
|
||||
{/if}
|
||||
{:else if mixer.Type === "INTEGER"}
|
||||
<label for={mixer.Name + '0'} class="form-label">{mixer.Name}</label>
|
||||
{#if mixer.values}
|
||||
<div class="badge bg-primary float-end">
|
||||
{mixer.DBScale ? ((mixer.DBScale.Min + mixer.DBScale.Step * mixer.values[0]) + ' dB') : mixer.values[0]}
|
||||
</div>
|
||||
{#each mixer.values as cur, idx}
|
||||
{#if advanced || idx === 0}
|
||||
<input
|
||||
type="range"
|
||||
class="form-range"
|
||||
disabled={!mixer.RW}
|
||||
id={mixer.Name + idx}
|
||||
min={mixer.Min}
|
||||
max={mixer.Max}
|
||||
step={mixer.Step}
|
||||
title={mixer.DBScale ? ((mixer.DBScale.Min + mixer.DBScale.Step * cur) + ' dB') : cur}
|
||||
bind:value={cur}
|
||||
on:change={() => alterMixer(mixer)}
|
||||
>
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
||||
{:else if mixer.Type === "BOOLEAN"}
|
||||
{#if mixer.RW}
|
||||
<div class="btn-group" role="group" aria-label="Basic example">
|
||||
<button
|
||||
class="btn"
|
||||
class:btn-secondary={!mixer.values.reduce((a,b) => a || b)}
|
||||
class:btn-primary={mixer.values.reduce((a,b) => a || b)}
|
||||
on:click={() => alterMixer(mixer, [!mixer.values.reduce((a,b) => a || b)])}
|
||||
>
|
||||
{mixer.Name}
|
||||
</button>
|
||||
{#if advanced && mixer.values.length > 1}
|
||||
{#each mixer.values as cur, ichan}
|
||||
<button
|
||||
class="btn btn-sm"
|
||||
class:btn-secondary={!cur}
|
||||
class:btn-primary={cur}
|
||||
on:click={() => alterMixer(mixer, mixer.values.map((v, i) => i == ichan ? !v : v))}
|
||||
>
|
||||
{ichan+1}
|
||||
</button>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<label class="form-label">{mixer.Name}</label>
|
||||
{/if}
|
||||
{/if}
|
||||
</li>
|
||||
{/if}
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
45
ui/src/lib/components/SourceSelection.svelte
Normal file
45
ui/src/lib/components/SourceSelection.svelte
Normal file
@ -0,0 +1,45 @@
|
||||
<script>
|
||||
import { sources } from '$lib/stores/sources';
|
||||
|
||||
let activating_source = null;
|
||||
async function clickSource(src) {
|
||||
activating_source = src.id;
|
||||
if (src.enabled) {
|
||||
await src.deactivate();
|
||||
await sources.refresh();
|
||||
} else {
|
||||
await src.activate();
|
||||
await sources.refresh();
|
||||
}
|
||||
activating_source = null;
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if !$sources}
|
||||
<div class="d-flex flex-fill justify-content-center">
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="row row-cols-2 row-cols-sm-3 row-cols-md-4 row-cols-lg-6 row-cols-xl-auto justify-content-center g-2">
|
||||
{#each Object.keys($sources) as source}
|
||||
<div class="col d-flex flex-column justify-content-center align-items-center">
|
||||
<button
|
||||
class="btn btn-lg"
|
||||
class:btn-primary={$sources[source].enabled}
|
||||
class:btn-secondary={!$sources[source].enabled}
|
||||
disabled={activating_source !== null}
|
||||
on:click={() => clickSource($sources[source])}
|
||||
>
|
||||
{#if activating_source && activating_source === source}
|
||||
<div class="spinner-grow spinner-grow-sm" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
{/if}
|
||||
{$sources[source].name}
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
1
ui/src/lib/index.js
Normal file
1
ui/src/lib/index.js
Normal file
@ -0,0 +1 @@
|
||||
// place files you want to import through the `$lib` alias in this folder.
|
57
ui/src/lib/source.js
Normal file
57
ui/src/lib/source.js
Normal file
@ -0,0 +1,57 @@
|
||||
export class Source {
|
||||
constructor(id, res) {
|
||||
this.id = id;
|
||||
if (res) {
|
||||
this.update(res);
|
||||
}
|
||||
}
|
||||
|
||||
update({ name, enabled, active }) {
|
||||
this.name = name;
|
||||
this.enabled = enabled;
|
||||
this.active = active;
|
||||
}
|
||||
|
||||
async activate() {
|
||||
await fetch(`api/sources/${this.id}/enable`, {headers: {'Accept': 'application/json'}, method: 'POST'});
|
||||
}
|
||||
|
||||
async deactivate() {
|
||||
await fetch(`api/sources/${this.id}/disable`, {headers: {'Accept': 'application/json'}, method: 'POST'});
|
||||
}
|
||||
|
||||
async currently() {
|
||||
const data = await fetch(`api/sources/${this.id}/currently`, {headers: {'Accept': 'application/json'}});
|
||||
if (data.status == 200) {
|
||||
return await data.json();
|
||||
} else {
|
||||
throw new Error((await res.json()).errmsg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function getSources() {
|
||||
const res = await fetch(`api/sources`, {headers: {'Accept': 'application/json'}})
|
||||
if (res.status == 200) {
|
||||
const data = await res.json();
|
||||
if (data == null) {
|
||||
return {}
|
||||
} else {
|
||||
Object.keys(data).forEach((k) => {
|
||||
data[k] = new Source(k, data[k]);
|
||||
});
|
||||
return data;
|
||||
}
|
||||
} else {
|
||||
throw new Error((await res.json()).errmsg);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getSource(sid) {
|
||||
const res = await fetch(`api/sources/${sid}`, {headers: {'Accept': 'application/json'}})
|
||||
if (res.status == 200) {
|
||||
return new Source(sid, await res.json());
|
||||
} else {
|
||||
throw new Error((await res.json()).errmsg);
|
||||
}
|
||||
}
|
41
ui/src/lib/stores/sources.js
Normal file
41
ui/src/lib/stores/sources.js
Normal file
@ -0,0 +1,41 @@
|
||||
import { derived, writable } from 'svelte/store';
|
||||
|
||||
import { getSources } from '$lib/source'
|
||||
|
||||
function createSourcesStore() {
|
||||
const { subscribe, set, update } = writable(null);
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
|
||||
set: (v) => {
|
||||
update((m) => v);
|
||||
},
|
||||
|
||||
refresh: async () => {
|
||||
const list = await getSources();
|
||||
update((m) => list);
|
||||
return list;
|
||||
},
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
export const sources = createSourcesStore();
|
||||
|
||||
export const sourcesList = derived(
|
||||
sources,
|
||||
($sources) => {
|
||||
if (!$sources) {
|
||||
return [];
|
||||
}
|
||||
return Object.keys($sources).map((k) => $sources[k]);
|
||||
},
|
||||
);
|
||||
|
||||
export const activeSources = derived(
|
||||
sourcesList,
|
||||
($sourcesList) => {
|
||||
return $sourcesList.filter((s) => s.active);
|
||||
},
|
||||
);
|
1
ui/src/routes/+layout.js
Normal file
1
ui/src/routes/+layout.js
Normal file
@ -0,0 +1 @@
|
||||
export const ssr = false;
|
20
ui/src/routes/+layout.svelte
Normal file
20
ui/src/routes/+layout.svelte
Normal file
@ -0,0 +1,20 @@
|
||||
<script>
|
||||
import '../hathoris.scss'
|
||||
import "bootstrap-icons/font/bootstrap-icons.css";
|
||||
|
||||
import { sources } from '$lib/stores/sources';
|
||||
sources.refresh();
|
||||
setInterval(sources.refresh, 5000);
|
||||
|
||||
const version = fetch('api/version', {headers: {'Accept': 'application/json'}}).then((res) => res.json())
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Hathoris</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="flex-fill d-flex flex-column bg-light">
|
||||
<div class="container-fluid flex-fill d-flex flex-column justify-content-center">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
75
ui/src/routes/+page.svelte
Normal file
75
ui/src/routes/+page.svelte
Normal file
@ -0,0 +1,75 @@
|
||||
<script>
|
||||
import Mixer from '$lib/components/Mixer.svelte';
|
||||
import SourceSelection from '$lib/components/SourceSelection.svelte';
|
||||
import { activeSources } from '$lib/stores/sources';
|
||||
|
||||
let mixerAdvanced = false;
|
||||
</script>
|
||||
|
||||
<div class="my-2">
|
||||
<SourceSelection />
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
{#if $activeSources.length === 0}
|
||||
<div class="text-muted text-center mt-1 mb-1">
|
||||
Aucune source active pour l'instant.
|
||||
</div>
|
||||
{:else}
|
||||
<marquee>
|
||||
{#each $activeSources as source}
|
||||
<div class="d-inline-block me-3">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<strong>{source.name} :</strong>
|
||||
{#await source.currently()}
|
||||
<div class="spinner-border spinner-border-sm" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
{:then title}
|
||||
{title}
|
||||
{:catch error}
|
||||
activée
|
||||
{/await}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</marquee>
|
||||
{/if}
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="card my-2">
|
||||
<h4 class="card-header">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div>
|
||||
<i class="bi bi-sliders"></i>
|
||||
Mixer
|
||||
</div>
|
||||
<button
|
||||
class="btn btn-sm"
|
||||
class:btn-info={mixerAdvanced}
|
||||
class:btn-secondary={!mixerAdvanced}
|
||||
on:click={() => { mixerAdvanced = !mixerAdvanced; }}
|
||||
>
|
||||
<i class="bi bi-alt"></i>
|
||||
</button>
|
||||
</div>
|
||||
</h4>
|
||||
<Mixer advanced={mixerAdvanced} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col">
|
||||
<div class="card my-2">
|
||||
<h4 class="card-header">
|
||||
<i class="bi bi-speaker"></i>
|
||||
Sources
|
||||
</h4>
|
||||
<div class="card-body">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
12
ui/svelte.config.js
Normal file
12
ui/svelte.config.js
Normal file
@ -0,0 +1,12 @@
|
||||
import adapter from '@sveltejs/adapter-static';
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
kit: {
|
||||
adapter: adapter({
|
||||
fallback: 'index.html'
|
||||
}),
|
||||
}
|
||||
};
|
||||
|
||||
export default config;
|
6
ui/vite.config.js
Normal file
6
ui/vite.config.js
Normal file
@ -0,0 +1,6 @@
|
||||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [sveltekit()]
|
||||
});
|
Loading…
Reference in New Issue
Block a user