aboutsummaryrefslogtreecommitdiff
path: root/src_frontend
diff options
context:
space:
mode:
Diffstat (limited to 'src_frontend')
-rw-r--r--src_frontend/App.svelte110
-rw-r--r--src_frontend/ComponentLib/Button/Button.svelte80
-rw-r--r--src_frontend/ComponentLib/Button/FloatingButton.svelte104
-rw-r--r--src_frontend/ComponentLib/FloatingSelect.svelte45
-rw-r--r--src_frontend/ComponentLib/Input.svelte14
-rw-r--r--src_frontend/ComponentLib/PrettyVar.svelte16
-rw-r--r--src_frontend/ComponentLib/RoundRange.svelte169
-rw-r--r--src_frontend/Components/Dialogs/ConfirmActionDialog.svelte59
-rw-r--r--src_frontend/Components/Editor/Controls.svelte83
-rw-r--r--src_frontend/Components/Editor/Editor.svelte296
-rw-r--r--src_frontend/Components/Editor/Output.svelte54
-rw-r--r--src_frontend/Components/Editor/Pane.svelte41
-rw-r--r--src_frontend/Components/Editor/TopBar.svelte62
-rw-r--r--src_frontend/Components/LEDConfig/LEDConfig.svelte204
-rw-r--r--src_frontend/Components/LEDConfig/MatrixSegment.svelte57
-rw-r--r--src_frontend/Components/LEDConfig/Segment.svelte38
-rw-r--r--src_frontend/Components/Logs/Logs.svelte10
-rw-r--r--src_frontend/Components/MainControls/ControlColors.svelte107
-rw-r--r--src_frontend/Components/MainControls/ControlComponents.svelte139
-rw-r--r--src_frontend/Components/MainControls/ControlOthers.svelte36
-rw-r--r--src_frontend/Components/MainMenu.svelte80
-rw-r--r--src_frontend/Components/ModeList/Mode.svelte57
-rw-r--r--src_frontend/Components/ModeList/ModeList.svelte64
-rw-r--r--src_frontend/Components/ModeList/NewModeDialog.svelte153
-rw-r--r--src_frontend/Components/NotImplemented.svelte7
-rw-r--r--src_frontend/Components/Notifs/Notif.svelte126
-rw-r--r--src_frontend/Components/Notifs/NotifsWrapper.svelte33
-rw-r--r--src_frontend/Components/Settings/CreateEditUser.svelte93
-rw-r--r--src_frontend/Components/Settings/InstanceName.svelte36
-rw-r--r--src_frontend/Components/Settings/SSLCert.svelte57
-rw-r--r--src_frontend/Components/Settings/Settings.svelte25
-rw-r--r--src_frontend/Components/Settings/System.svelte34
-rw-r--r--src_frontend/Components/Settings/Users.svelte93
-rw-r--r--src_frontend/Components/Settings/Version.svelte60
-rw-r--r--src_frontend/layout/Desktop.svelte48
-rw-r--r--src_frontend/layout/Drawer.svelte38
-rw-r--r--src_frontend/layout/Phone.svelte50
-rw-r--r--src_frontend/main.js7
-rw-r--r--src_frontend/routes/EditorRoute.svelte9
-rw-r--r--src_frontend/routes/LoginRoute.svelte106
-rw-r--r--src_frontend/routes/MainRoute.svelte61
-rw-r--r--src_frontend/routes/UnknownRoute.svelte21
-rw-r--r--src_frontend/routes/WidgetRoute.svelte130
-rw-r--r--src_frontend/stores/notifs.js24
-rw-r--r--src_frontend/stores/socketStore.js112
45 files changed, 3248 insertions, 0 deletions
diff --git a/src_frontend/App.svelte b/src_frontend/App.svelte
new file mode 100644
index 0000000..e95f316
--- /dev/null
+++ b/src_frontend/App.svelte
@@ -0,0 +1,110 @@
+<script>
+ import Router from 'svelte-spa-router';
+ import { wrap } from 'svelte-spa-router/wrap';
+ import MainRoute from "./routes/MainRoute.svelte";
+ import EditorRoute from "./routes/EditorRoute.svelte";
+ import LoginRoute from "./routes/LoginRoute.svelte";
+ import WidgetRoute from "./routes/WidgetRoute.svelte";
+ import UnknownRoute from "./routes/UnknownRoute.svelte";
+
+ import { connected, reconnecting } from "./stores/socketStore.js";
+
+ let main_router_routes = new Map();
+ main_router_routes.set(/^\/(schedules|modes|led_config|logs|settings|)(?:\/.*)?$/, wrap({
+ component: MainRoute
+ }));
+ main_router_routes.set("/editor/*", wrap({
+ component: EditorRoute
+ }));
+ main_router_routes.set("/login", wrap({
+ component: LoginRoute
+ }));
+ main_router_routes.set("/widget", wrap({
+ component: WidgetRoute
+ }));
+ main_router_routes.set("*", wrap({
+ component: UnknownRoute
+ }));
+</script>
+
+<style>
+ .no-connection {
+ width: 100%;
+ height: 100%;
+ box-sizing: border-box;
+ padding: 15px;
+ background-color: #3c3b3b;
+ text-align: center;
+ color: white;
+ }
+ .lds-ellipsis {
+ display: inline-block;
+ position: relative;
+ width: 80px;
+ height: 80px;
+ }
+ .lds-ellipsis div {
+ position: absolute;
+ top: 33px;
+ width: 13px;
+ height: 13px;
+ border-radius: 50%;
+ background: #fff;
+ animation-timing-function: cubic-bezier(0, 1, 1, 0);
+ }
+ .lds-ellipsis div:nth-child(1) {
+ left: 8px;
+ animation: lds-ellipsis1 0.6s infinite;
+ }
+ .lds-ellipsis div:nth-child(2) {
+ left: 8px;
+ animation: lds-ellipsis2 0.6s infinite;
+ }
+ .lds-ellipsis div:nth-child(3) {
+ left: 32px;
+ animation: lds-ellipsis2 0.6s infinite;
+ }
+ .lds-ellipsis div:nth-child(4) {
+ left: 56px;
+ animation: lds-ellipsis3 0.6s infinite;
+ }
+ @keyframes lds-ellipsis1 {
+ 0% {
+ transform: scale(0);
+ }
+ 100% {
+ transform: scale(1);
+ }
+ }
+ @keyframes lds-ellipsis3 {
+ 0% {
+ transform: scale(1);
+ }
+ 100% {
+ transform: scale(0);
+ }
+ }
+ @keyframes lds-ellipsis2 {
+ 0% {
+ transform: translate(0, 0);
+ }
+ 100% {
+ transform: translate(24px, 0);
+ }
+ }
+
+</style>
+
+{#if $connected}
+<Router routes={main_router_routes} />
+{:else if $reconnecting}
+<div class="no-connection">
+ <div class="lds-ellipsis"><div></div><div></div><div></div><div></div></div>
+ <div>Lost connection to server, attempting to reconnect...</div>
+</div>
+{:else}
+<div class="no-connection">
+ <div class="lds-ellipsis"><div></div><div></div><div></div><div></div></div>
+ <div>No server connection, attempting to connect...</div>
+</div>
+{/if} \ No newline at end of file
diff --git a/src_frontend/ComponentLib/Button/Button.svelte b/src_frontend/ComponentLib/Button/Button.svelte
new file mode 100644
index 0000000..3943ecf
--- /dev/null
+++ b/src_frontend/ComponentLib/Button/Button.svelte
@@ -0,0 +1,80 @@
+<script>
+ export let faIcon = false;
+ export let fullWidth = false;
+ export let backgroundColor = "var(--theme-primary)";
+ export let color = "var(--theme-on-primary)";
+
+ export let loadingPromise = null;
+ $: listen(loadingPromise);
+ function listen(promise) {
+ if (promise != null) {
+ loading = true;
+ promise.then(res => {
+ loading = false;
+ success = true;
+ }).catch(err => {
+ loading = false;
+ success = false;
+ });
+ } else {
+ loading = false;
+ }
+ }
+ let loading;
+ let success;
+</script>
+
+<style>
+ button {
+ background-color: var(--bg-color);
+ color: var(--color);
+ border: none;
+ text-decoration: none;
+ padding: 5px 15px;
+ font-size: 15px;
+
+ transition: background-color, color 0.1s ease;
+ border-radius: 15px;
+ }
+ button:hover {
+ filter: brightness(0.95);
+ }
+ button:active {
+ filter: brightness(0.90);
+ }
+ .fullWidth {
+ width: 100%;
+ }
+ .iconButton {
+ display: flex;
+ }
+ .iconButton .text {
+ margin: auto;
+ }
+ .active {
+ background-color: var(--active-bg-color);
+ color: var(--active-color);
+ }
+</style>
+
+<button
+ on:click
+ class:fullWidth={fullWidth}
+ class:iconButton={faIcon != false}
+ style="--bg-color: {backgroundColor};
+ --color: {color};">
+
+ {#if faIcon}
+ <div class="icon">
+ <i class={faIcon}></i>
+ </div>
+ {/if}
+
+ {#if loading}
+ <i class="fas fa-spinner fa-pulse"></i>
+ {:else}
+ <div class="text">
+ <slot></slot>
+ </div>
+ {/if}
+</button> \ No newline at end of file
diff --git a/src_frontend/ComponentLib/Button/FloatingButton.svelte b/src_frontend/ComponentLib/Button/FloatingButton.svelte
new file mode 100644
index 0000000..123debc
--- /dev/null
+++ b/src_frontend/ComponentLib/Button/FloatingButton.svelte
@@ -0,0 +1,104 @@
+<script>
+ export let faIcon = false;
+ export let fullWidth = false;
+ export let backgroundColor = "white";
+ export let color = "black";
+ export let activeBackgroundColor = "gray";
+ export let activeColor = "white";
+ export let active = false;
+ export let label = null;
+
+ export let loadingPromise = null;
+ $: listen(loadingPromise);
+ function listen(promise) {
+ if (promise != null) {
+ loading = true;
+ promise.then(res => {
+ loading = false;
+ success = true;
+ }).catch(err => {
+ loading = false;
+ success = false;
+ });
+ } else {
+ loading = false;
+ }
+ }
+ let loading;
+ let success;
+</script>
+
+<style>
+ button {
+ background-color: var(--bg-color);
+ color: var(--color);
+ border: none;
+ text-decoration: none;
+ padding: 15px;
+ font-size: 15px;
+
+ transition: background-color, color 0.1s ease;
+ border-radius: 50px;
+ }
+ button:hover {
+ filter: brightness(0.95);
+ }
+ button:active {
+ filter: brightness(0.90);
+ }
+ .fullWidth {
+ width: 100%;
+ }
+ .iconButton {
+ display: flex;
+ }
+ .iconButton .text {
+ margin: auto;
+ }
+ .active {
+ background-color: var(--active-bg-color);
+ color: var(--active-color);
+ }
+ .wrapper {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ }
+ .label {
+ margin-top: 10px;
+ color: var(--grey-700);
+ font-size: 13px;
+ }
+</style>
+
+<div class="wrapper">
+ <button
+ on:click
+ class:fullWidth={fullWidth}
+ class:iconButton={faIcon != false}
+ class:active={active}
+ style="--bg-color: {backgroundColor};
+ --color: {color};
+ --active-bg-color: {activeBackgroundColor};
+ --active-color: {activeColor};"
+ class="drop-shadow"
+ >
+
+ {#if faIcon}
+ <div class="icon">
+ <i class={faIcon}></i>
+ </div>
+ {/if}
+
+ {#if loading}
+ <i class="fas fa-spinner fa-pulse"></i>
+ {:else}
+ <div class="text">
+ <slot></slot>
+ </div>
+ {/if}
+ </button>
+ {#if label != null}
+ <span class="label">{label}</span>
+ {/if}
+</div> \ No newline at end of file
diff --git a/src_frontend/ComponentLib/FloatingSelect.svelte b/src_frontend/ComponentLib/FloatingSelect.svelte
new file mode 100644
index 0000000..0d71ca7
--- /dev/null
+++ b/src_frontend/ComponentLib/FloatingSelect.svelte
@@ -0,0 +1,45 @@
+<script>
+ import FloatingButton from "./Button/FloatingButton.svelte";
+ export let value;
+
+ export let faIcon = false;
+ export let backgroundColor = "white";
+ export let color = "black";
+ export let activeBackgroundColor = "gray";
+ export let activeColor = "white";
+ export let active = false;
+ export let label = null;
+</script>
+
+<style>
+ select {
+ text-decoration: none;
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+ background-color: transparent;
+ border: none;
+ margin: auto;
+ text-align: center;
+ }
+ select:focus {
+ outline: none;
+ }
+ .active {
+ background-color: var(--active-bg-color);
+ color: var(--active-color);
+ }
+</style>
+<FloatingButton fullWidth=true
+ {faIcon}
+ {label}
+ {backgroundColor}
+ {color}
+ {activeBackgroundColor}
+ {activeColor}
+ {active} >
+ <select on:change
+ bind:value={value}>
+ <slot></slot>
+ </select>
+</FloatingButton> \ No newline at end of file
diff --git a/src_frontend/ComponentLib/Input.svelte b/src_frontend/ComponentLib/Input.svelte
new file mode 100644
index 0000000..ecbaefb
--- /dev/null
+++ b/src_frontend/ComponentLib/Input.svelte
@@ -0,0 +1,14 @@
+<script>
+ export let fullWidth = true;
+ export let type = "text";
+ export let id;
+ export let value
+</script>
+
+<style>
+
+</style>
+
+<div>
+ <input type={type} id={id} bind:value={value} />
+</div> \ No newline at end of file
diff --git a/src_frontend/ComponentLib/PrettyVar.svelte b/src_frontend/ComponentLib/PrettyVar.svelte
new file mode 100644
index 0000000..68f082f
--- /dev/null
+++ b/src_frontend/ComponentLib/PrettyVar.svelte
@@ -0,0 +1,16 @@
+<script>
+ export let varText;
+ let prettyVarText;
+
+ function prettify(text) {
+ try {
+ prettyVarText = text.replace("_", " ");
+ prettyVarText = prettyVarText.charAt(0).toUpperCase() + prettyVarText.slice(1);
+ } catch {
+ prettyVarText = varText;
+ }
+ }
+ $: prettify(varText);
+</script>
+
+{prettyVarText}
diff --git a/src_frontend/ComponentLib/RoundRange.svelte b/src_frontend/ComponentLib/RoundRange.svelte
new file mode 100644
index 0000000..42c4d57
--- /dev/null
+++ b/src_frontend/ComponentLib/RoundRange.svelte
@@ -0,0 +1,169 @@
+<script>
+ import { onMount } from 'svelte';
+ import { createEventDispatcher } from 'svelte';
+ const dispatch = createEventDispatcher();
+
+ export let min = 0;
+ export let max = 100;
+ export let value = 0;
+ export let enabled = true;
+
+ let svg;
+ let indicator;
+ let thumb;
+ let track;
+ let cVal = value;
+ let touchActive = false;
+
+ let inputCoordinates;
+
+ function setValue(_value) {
+ value = _value;
+ let delta = cVal - _value;
+
+ if (!touchActive && (Math.abs(delta) > 10)) {
+ let animInterval = setInterval(() => {
+ cVal = cVal + ((delta < 0) ? 1 : -1);
+ setThumbAndTrack(cVal);
+ if ( ((delta < 0) && (cVal >= value)) || ((delta > 0) && (cVal <= value)) ) {
+ clearInterval(animInterval);
+ }
+ }, 1);
+ } else {
+ setThumbAndTrack(value);
+ cVal = value;
+ }
+ }
+
+ function setThumbAndTrack(val_pos) {
+ if (indicator && thumb) {
+ let indicator_value = (((val_pos - min) * 207) / (max - min));
+ indicator.style.strokeDasharray = `${indicator_value},207`;
+
+ let p = indicator.getPointAtLength(indicator_value);
+ thumb.setAttribute("transform", `translate(${p.x}, ${p.y})`);
+ }
+ }
+
+ onMount(async() => {
+ let lastVal = value;
+ setValue(value);
+ setInterval(() => {
+ if (value != lastVal) {
+ setValue(value);
+ lastVal = value;
+ }
+ }, 500);
+ window.indicator = indicator;
+
+ inputCoordinates = svg.createSVGPoint();
+ addEventListenersToElement(thumb);
+ addEventListenersToElement(track);
+ addEventListenersToElement(indicator);
+ });
+
+ function addEventListenersToElement(el) {
+ el.addEventListener("touchstart", () => {
+ touchActive = true;
+ }, { capture: true, passive: false });
+ el.addEventListener("touchend", () => {
+ touchActive = false;
+ }, { capture: true, passive: true });
+ el.addEventListener("touchmove", updateValueFromTouchEvent, { capture: true, passive: false });
+
+ el.addEventListener("mousedown", (ev) => {
+ touchActive = true;
+ updateValueFromMouseEvent(ev);
+ svg.addEventListener("mousemove", updateValueFromMouseEvent);
+ }, { capture: true, passive: false });
+ el.addEventListener("mouseup", (ev) => {
+ touchActive = false;
+ svg.removeEventListener("mousemove", updateValueFromMouseEvent);
+ }, { capture: true, passive: false });
+ }
+
+ function updateValueFromTouchEvent(ev) {
+ inputCoordinates.x = ev.touches[0].clientX;
+ inputCoordinates.y = ev.touches[0].clientY;
+ updateValueFromPoint(inputCoordinates);
+ ev.preventDefault();
+ }
+
+ function updateValueFromMouseEvent(ev) {
+ inputCoordinates.x = ev.clientX;
+ inputCoordinates.y = ev.clientY;
+ updateValueFromPoint(inputCoordinates);
+ ev.preventDefault();
+ }
+
+ function updateValueFromPoint(point) {
+ inputCoordinates = inputCoordinates.matrixTransform(svg.getScreenCTM().inverse());
+ setValue(getValueAtLength(indicator, getPathLangthOfClosestPoint(indicator, inputCoordinates)));
+ dispatch("change");
+ }
+
+ function getPathLangthOfClosestPoint(path, point) {
+ let pathLength = path.getTotalLength();
+ let shortestDistance = Number.MAX_VALUE;
+ let shortestPathDistance;
+
+ for (let i = 0; i <= pathLength; i += 4) {
+ let p = path.getPointAtLength(i),
+ dx = p.x - point.x,
+ dy = p.y - point.y,
+ cDistance = dx * dx + dy * dy;
+
+ if (cDistance < shortestDistance) {
+ shortestDistance = cDistance;
+ shortestPathDistance = i;
+ }
+ }
+
+ return shortestPathDistance;
+ }
+
+ function getValueAtLength(path, length) {
+ return Math.floor(((length * (max - min)) / path.getTotalLength()) + min);
+ }
+</script>
+
+<style>
+ svg {
+ --primary-color: var(--yellow-500);
+ --muted-color: var(--yellow-100);
+ margin-top: -15px;
+ }
+ path {
+ transition: stroke 0.2s ease;
+ }
+ circle {
+ filter: drop-shadow(0 0 2px #d0d0d0);
+ transition: stroke 0.2s ease;
+ }
+ circle:hover {
+ stroke: #f9f9f9;
+ }
+ circle:active {
+ fill: var(--yellow-600);
+ }
+ .disabled {
+ --primary-color: var(--grey-200);
+ --muted-color: var(--grey-50);
+ }
+</style>
+
+
+<svg bind:this={svg} class:disabled={!enabled} viewbox="0 0 100 100">
+ <path bind:this={track} fill="none" stroke-linecap="round" stroke-width="10" stroke="var(--muted-color)"
+ d="M25 85
+ a 40 40 0 1 1 50 0
+ ">
+ </path>
+ <path bind:this={indicator} fill="none" stroke-linecap="round" stroke-width="10" stroke="var(--primary-color)"
+ stroke-dasharray="207,0"
+ d="M25 85
+ a 40 40 0 1 1 50 0">
+ </path>
+ <circle bind:this={thumb} cx="0" cy="0" r="8" stroke="white" stroke-width="3" fill="var(--primary-color)" />
+ <text id="count" x="50" y="55" text-anchor="middle" dy="7" font-size="20">{value}%</text>
+</svg> \ No newline at end of file
diff --git a/src_frontend/Components/Dialogs/ConfirmActionDialog.svelte b/src_frontend/Components/Dialogs/ConfirmActionDialog.svelte
new file mode 100644
index 0000000..185c743
--- /dev/null
+++ b/src_frontend/Components/Dialogs/ConfirmActionDialog.svelte
@@ -0,0 +1,59 @@
+<script>
+ import { onMount } from "svelte";
+ import dialogPolyfill from 'dialog-polyfill'
+ import Button from "../../ComponentLib/Button/Button.svelte";
+
+ export let title = "Are you sure?";
+ export let text = "Are you sure you want to delete the galaxy?";
+ export let defaultAction = false;
+ export let action = () => console.log("No action specified");
+
+ let modal;
+ let activeTab = 0;
+ let name;
+ let sourceMode;
+
+ function open() {
+ modal.showModal()
+ }
+ function confirm() {
+ modal.close();
+ action();
+ }
+ function register(node) {
+ dialogPolyfill.registerDialog(node);
+ }
+</script>
+
+<style>
+ dialog {
+ padding: 15px;
+ border: none;
+ border-radius: 15px;
+ }
+ h2 {
+ margin: 0;
+ }
+ .buttons {
+ display: flex;
+ }
+ .buttons > * {
+ flex-grow: 1;
+ }
+ .buttons > *:not(:last-child) {
+ margin-right: 5px;
+ }
+ .buttons > *:not(:first-child) {
+ margin-left: 5px;
+ }
+</style>
+
+<slot name="trigger" {open}></slot>
+<dialog bind:this={modal} use:register>
+ <h2>{title}</h2>
+ <p>{text}</p>
+ <div class="buttons">
+ <div><Button fullWidth=true on:click={confirm} color={"var(--theme-primary)"} backgroundColor={"white"}>Yes</Button></div>
+ <div><Button fullWidth=true on:click={() => modal.close() }>No</Button></div>
+ </div>
+</dialog> \ No newline at end of file
diff --git a/src_frontend/Components/Editor/Controls.svelte b/src_frontend/Components/Editor/Controls.svelte
new file mode 100644
index 0000000..302aa7a
--- /dev/null
+++ b/src_frontend/Components/Editor/Controls.svelte
@@ -0,0 +1,83 @@
+<script>
+ import { onMount } from "svelte";
+ import PrettyVar from "../../ComponentLib/PrettyVar.svelte";
+ import { openSocket } from "../../stores/socketStore";
+
+ let brightnessValue = 0;
+ let power_on = false;
+ let variables = {};
+
+ function setBrightness() {
+ if (!power_on) { openSocket.emit("power:set", true); }
+ openSocket.emit("brightness:set", brightnessValue);
+ }
+ function setPower() { openSocket.emit("power:set", power_on); }
+ function setVar(ev) {
+ openSocket.emit("var:set", ev.target.id, ev.target.value);
+ }
+
+ openSocket.on("power", (power) => power_on = power);
+ openSocket.on("brightness", (value) => brightnessValue = value);
+ openSocket.on("vars", (vars) => variables = vars);
+ openSocket.on("var", (name, value) => {
+ name = name.replace("variable/", "");
+ if (value.value == null) {
+ delete variables[name];
+ } else {
+ variables[name] = value;
+ }
+ variables = variables;
+ });
+
+ onMount(() => {
+ openSocket.emit("power:get");
+ openSocket.emit("brightness:get");
+ openSocket.emit("vars:get")
+ });
+</script>
+
+<style>
+ label {
+ width: 100%;
+ font-size: 12px;
+ color: var(--grey-500);
+ }
+ .var-group {
+ display: flex;
+ }
+ input[type=range] {
+ width: 100%;
+ }
+ input[type=text] {
+ margin-top: 5px;
+ display: block;
+ width: 100%;
+ background-color: #737373;
+ padding: 5px;
+ color: white;
+ border: none;
+ box-sizing: border-box;
+ border-radius: 5px;
+ }
+</style>
+
+<div>
+ <div class="var-group">
+ <label for="power">Power</label>
+ <input type="checkbox" id="power" bind:checked={power_on} on:change={setPower} />
+ </div>
+ <div>
+ <label for="brightness">Brightness</label>
+ <div class="var-group">
+ <input id="brightness" type="range" min=0 max=255 bind:value={brightnessValue} on:change={setBrightness} />
+ {brightnessValue}
+ </div>
+ </div>
+
+ {#each Object.entries(variables) as [name, value]}
+ <div>
+ <label for="{name}"><PrettyVar varText={name} /></label>
+ <input type="text" id="{name}" bind:value={value.value} on:blur={setVar} />
+ </div>
+ {/each}
+</div> \ No newline at end of file
diff --git a/src_frontend/Components/Editor/Editor.svelte b/src_frontend/Components/Editor/Editor.svelte
new file mode 100644
index 0000000..b63ee9b
--- /dev/null
+++ b/src_frontend/Components/Editor/Editor.svelte
@@ -0,0 +1,296 @@
+<script context="module">
+ let debuggerInitialised = false;
+</script>
+<script>
+ import { onDestroy } from "svelte";
+ import { pop } from "svelte-spa-router";
+ import { EditorState, EditorView, basicSetup } from "@codemirror/basic-setup"
+ import { python } from "@codemirror/lang-python"
+ import { HighlightStyle, tags as t } from "@codemirror/highlight"
+ import { notif } from "../../stores/notifs";
+ import TopBar from "./TopBar.svelte";
+ import Pane from "./Pane.svelte";
+ import ControlComponents from "../MainControls/ControlComponents.svelte";
+ import Controls from "./Controls.svelte";
+ import Output from "./Output.svelte";
+
+ import { authorizedSocket, authorizedSocketNeeded } from "../../stores/socketStore";
+ authorizedSocketNeeded.set(true);
+
+ export let modeId;
+
+ let codeEditorView;
+ let codeEditorEl;
+ let codeEditorHasChanges = false;
+ let procIsRunning = false;
+
+ function initDebugger() {
+ if (debuggerInitialised) { return; }
+ debuggerInitialised = true;
+ authorizedSocket.emit("editor:open", `user/${modeId}`, (res) => {
+ if (!res.success) { notif({title: res.reason, type: "danger"}); return; }
+ });
+ }
+
+ authorizedSocket.on("editor:code", (modeId, code) => {
+ const chalky = "#e5c07b",
+ coral = "#e06c75",
+ cyan = "#56b6c2",
+ invalid = "#ffffff",
+ ivory = "#abb2bf",
+ stone = "#7d8799",
+ malibu = "#61afef",
+ sage = "#98c379",
+ whiskey = "#d19a66",
+ violet = "#c678dd",
+ darkBackground = "#21252b",
+ highlightBackground = "#2c313a",
+ background = "#282c34",
+ selection = "#3E4451",
+ cursor = "#528bff"
+
+ codeEditorView = new EditorView({
+ state: EditorState.create({
+ extensions: [
+ basicSetup,
+ python(),
+ EditorView.updateListener.of(update => {
+ if (update.docChanged) {
+ codeEditorHasChanges = true;
+ }
+ }),
+ EditorView.theme({
+ "&": {
+ color: ivory,
+ },
+
+ ".cm-content": {
+ caretColor: cursor
+ },
+
+ "&.cm-focused .cm-cursor": {borderLeftColor: cursor},
+ "&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection": {backgroundColor: selection},
+
+ ".cm-panels": {backgroundColor: darkBackground, color: ivory},
+ ".cm-panels.cm-panels-top": {borderBottom: "2px solid black"},
+ ".cm-panels.cm-panels-bottom": {borderTop: "2px solid black"},
+
+ ".cm-searchMatch": {
+ backgroundColor: "#72a1ff59",
+ outline: "1px solid #457dff"
+ },
+ ".cm-searchMatch.cm-searchMatch-selected": {
+ backgroundColor: "#6199ff2f"
+ },
+
+ ".cm-activeLine": {backgroundColor: highlightBackground},
+ ".cm-selectionMatch": {backgroundColor: "#aafe661a"},
+
+ ".cm-matchingBracket, .cm-nonmatchingBracket": {
+ backgroundColor: "#bad0f847",
+ outline: "1px solid #515a6b"
+ },
+
+ ".cm-gutters": {
+ backgroundColor: "transparent",
+ color: stone,
+ border: "none"
+ },
+
+ ".cm-activeLineGutter": {
+ backgroundColor: highlightBackground
+ },
+
+ ".cm-foldPlaceholder": {
+ backgroundColor: "transparent",
+ border: "none",
+ color: "#ddd"
+ },
+
+ ".cm-tooltip": {
+ border: "1px solid #181a1f",
+ backgroundColor: darkBackground
+ },
+ ".cm-tooltip-autocomplete": {
+ "& > ul > li[aria-selected]": {
+ backgroundColor: highlightBackground,
+ color: ivory
+ }
+ }
+ }, {dark:true}),
+ HighlightStyle.define([
+ {tag: t.keyword,
+ color: violet},
+ {tag: [t.name, t.deleted, t.character, t.propertyName, t.macroName],
+ color: coral},
+ {tag: [t.function(t.variableName), t.labelName],
+ color: malibu},
+ {tag: [t.color, t.constant(t.name), t.standard(t.name)],
+ color: whiskey},
+ {tag: [t.definition(t.name), t.separator],
+ color: ivory},
+ {tag: [t.typeName, t.className, t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace],
+ color: chalky},
+ {tag: [t.operator, t.operatorKeyword, t.url, t.escape, t.regexp, t.link, t.special(t.string)],
+ color: cyan},
+ {tag: [t.meta, t.comment],
+ color: stone},
+ {tag: t.strong,
+ fontWeight: "bold"},
+ {tag: t.emphasis,
+ fontStyle: "italic"},
+ {tag: t.strikethrough,
+ textDecoration: "line-through"},
+ {tag: t.link,
+ color: stone,
+ textDecoration: "underline"},
+ {tag: t.heading,
+ fontWeight: "bold",
+ color: coral},
+ {tag: [t.atom, t.bool, t.special(t.variableName)],
+ color: whiskey },
+ {tag: [t.processingInstruction, t.string, t.inserted],
+ color: sage},
+ {tag: t.invalid,
+ color: invalid},
+ ]),
+ ],
+ doc: code
+ }),
+ parent: codeEditorEl
+ })
+ });
+ authorizedSocket.on("editor:proc:start", () => procIsRunning = true);
+ authorizedSocket.on("editor:proc:exit", (code) => {
+ procIsRunning = false;
+ });
+
+ function startProc() {
+ saveCode(() => {
+ authorizedSocket.emit("editor:startmode", (res) => {
+ if (!res.success) { notif({title: res.reason, type: "danger"}); }
+ });
+ });
+ }
+
+ function stopProc() {
+ authorizedSocket.emit("editor:stopmode", (res) => {
+ if (!res.success) { notif({title: res.reason, type: "danger"}); }
+ });
+ }
+
+ function restartProc () {
+ saveCode((res) => {
+ if (!res.success) { notif({title: res.reason, type: "danger"}); }
+ authorizedSocket.emit("editor:restartmode", (res) => {
+ if (!res.success) { notif({title: res.reason, type: "danger"}); }
+ });
+ });
+ }
+
+ function saveCode(fn) {
+ if (codeEditorView == null) { return; }
+ authorizedSocket.emit("editor:save", `user/${modeId}`, codeEditorView.state.doc.toString(), res => {
+ if (!res.success) { notif({title: res.reason, type: "danger"}); }
+ if (fn != null) { fn(res) }
+ });
+ codeEditorHasChanges = false;
+ }
+
+ function closeDebugger() {
+ saveCode((res) => {
+ if (!res.success) { notif({title: res.reason, type: "danger"}); }
+ authorizedSocket.emit("editor:close", res => {
+ if (!res.success) { notif({title: res.reason, type: "danger"}); }
+ debuggerInitialised = false;
+ });
+ });
+ }
+
+ onDestroy(() => {
+ closeDebugger();
+ })
+
+ document.addEventListener("keydown", function(e) {
+ if ((window.navigator.platform.match("Mac") ? e.metaKey : e.ctrlKey) && e.keyCode == 83) {
+ e.preventDefault();
+ saveCode();
+ }
+ }, false);
+
+ setInterval(() => {
+ if (codeEditorHasChanges) {
+ saveCode();
+ }
+ }, 5000);
+</script>
+
+<style>
+ main {
+ display: grid;
+ box-sizing: border-box;
+ padding: 15px;
+ column-gap: 15px;
+ row-gap: 15px;
+ grid-template-columns: 300px 1fr;
+ grid-template-rows: 50% 1fr 33%;
+ grid-template-areas:
+ "simulation editor"
+ "controls editor"
+ "controls output";
+ width: 100%;
+ height: calc(100% - 35px);
+ background-color: #333333;
+ color: white;
+ }
+ .simulation { grid-area: simulation; }
+ .controls { grid-area: controls; }
+ .editor { grid-area: editor; }
+ .output { grid-area: output; }
+
+ .editor {
+ overflow: auto;
+ }
+
+ @media (max-width: 800px) {
+ main {
+ grid-template-columns: auto;
+ grid-template-areas:
+ "editor"
+ "editor"
+ "output";
+ }
+ .controls, .simulation {
+ display: none;
+ }
+ }
+</style>
+
+<TopBar modeId={modeId}
+ hasChange={codeEditorHasChanges}
+ on:closedebugger={pop}
+ on:start={startProc}
+ on:stop={stopProc}
+ on:restart={restartProc}
+ bind:procIsRunning={procIsRunning} />
+<main use:initDebugger>
+ <div class="simulation">
+ <Pane header="simulation">
+ </Pane>
+ </div>
+
+ <div class="controls">
+ <Pane header="Controls">
+ <!-- <ControlComponents /> -->
+ <Controls />
+ </Pane>
+ </div>
+
+ <div class="editor" bind:this={codeEditorEl}></div>
+
+ <div class="output">
+ <Pane header="output" padding={false} scrollable={false}>
+ <Output />
+ </Pane>
+ </div>
+</main>
diff --git a/src_frontend/Components/Editor/Output.svelte b/src_frontend/Components/Editor/Output.svelte
new file mode 100644
index 0000000..dcd5995
--- /dev/null
+++ b/src_frontend/Components/Editor/Output.svelte
@@ -0,0 +1,54 @@
+<script>
+ import { authorizedSocket, authorizedSocketNeeded } from "../../stores/socketStore";
+ authorizedSocketNeeded.set(true);
+
+ let scrollBox;
+ let htmlCode = "";
+
+ function addData(data, classname) {
+ // let styles = "white-space:pre-wrap;margin:0;";
+ // let styles = "overflow-x:auto;";
+ let styles = "";
+ switch (classname) {
+ case "exit":
+ styles += "color: green";
+ break;
+ case"stderr":
+ styles += "color: red";
+ break;
+ }
+ htmlCode += `<span style="${styles}">${data}</span>`;
+ if (scrollBox != null) {
+ scrollBox.scrollTop = scrollBox.scrollHeight + 100;
+ }
+ }
+ authorizedSocket.on("editor:proc:exit", (code) => addData(`\nMode exited with ${code}\n\n`, "exit"));
+ authorizedSocket.on("editor:proc:stdout", (stdout) => addData(stdout, "stdout"));
+ authorizedSocket.on("editor:proc:stderr", (stderr) => addData(stderr, "stderr"));
+</script>
+
+<style>
+ div {
+ height: 100%;
+ width: 100%;
+ }
+ pre {
+ height: 100%;
+ width: calc(100vw - 30px);
+ overflow: auto;
+ padding: 15px;
+ margin: 0;
+ box-sizing: border-box;
+ }
+ @media (min-width: 800px) {
+ pre {
+ width: calc(100vw - 360px);
+ }
+ }
+</style>
+
+<div>
+ <pre bind:this={scrollBox}>
+ {@html htmlCode}
+ </pre>
+</div> \ No newline at end of file
diff --git a/src_frontend/Components/Editor/Pane.svelte b/src_frontend/Components/Editor/Pane.svelte
new file mode 100644
index 0000000..143c569
--- /dev/null
+++ b/src_frontend/Components/Editor/Pane.svelte
@@ -0,0 +1,41 @@
+<script>
+ export let header;
+ export let padding = true;
+ export let scrollable = true;
+</script>
+
+<style>
+ .box {
+ background-color: #444242;
+ border-radius: 5px;
+ height: 100%;
+ }
+ .header {
+ width: 100%;
+ padding: 10px;
+ box-sizing: border-box;
+ border-bottom: 1px solid #333333;
+ }
+ .header h1 {
+ margin: 0;
+ font-size: 12px;
+ font-weight: bold;
+ text-transform: uppercase;
+ }
+ .content {
+ height: calc(100% - 35px);
+ width: 100%;
+ box-sizing: border-box;
+ }
+ .padding { padding: 15px; }
+ .scrollable { overflow: auto; }
+</style>
+
+<div class="box">
+ <div class="header">
+ <h1>{header}</h1>
+ </div>
+ <div class="content" class:padding={padding} class:scrollable={scrollable}>
+ <slot></slot>
+ </div>
+</div> \ No newline at end of file
diff --git a/src_frontend/Components/Editor/TopBar.svelte b/src_frontend/Components/Editor/TopBar.svelte
new file mode 100644
index 0000000..c74adf0
--- /dev/null
+++ b/src_frontend/Components/Editor/TopBar.svelte
@@ -0,0 +1,62 @@
+<script>
+ import { createEventDispatcher } from 'svelte';
+ import { pop } from "svelte-spa-router";
+ import PrettyVar from "../../ComponentLib/PrettyVar.svelte";
+
+ const dispatch = createEventDispatcher();
+
+ export let modeId;
+ export let hasChange = false;
+ export let procIsRunning = false;
+</script>
+
+<style>
+ .topbar {
+ display: flex;
+ background-color: #444242;
+ height: 35px;
+ box-sizing: border-box;
+ padding: 10px;
+ font-size: 12px;
+ color: white;
+ }
+ .topbar .title { margin: auto; }
+ .savestatus {
+ font-size: 10px;
+ color: var(--grey-400);
+ }
+ button {
+ background: #444242;
+ border: none;
+ color: white;
+ }
+ button i {
+ margin-right: 5px;
+ }
+ button:hover {
+ filter: brightness(0.95);
+ }
+ button:active {
+ filter: brightness(0.90);
+ }
+</style>
+
+<div class="topbar">
+ <div><button on:click={() => dispatch("closedebugger")}><i class="fas fa-chevron-left"></i></button></div>
+ <div class="title">
+ <span class="filename"><PrettyVar varText={modeId} /></span>
+ <span class="savestatus">
+ {#if hasChange}
+ (not saved)
+ {/if}
+ </span>
+ </div>
+ <div>
+ {#if procIsRunning}
+ <button on:click={() => dispatch("restart")}><i class="fas fa-sync-alt"></i>Restart</button>
+ <button on:click={() => dispatch("stop")}><i class="fas fa-stop"></i>Stop</button>
+ {:else}
+ <button on:click={() => dispatch("start")}><i class="fas fa-play"></i>Start</button>
+ {/if}
+ </div>
+</div> \ No newline at end of file
diff --git a/src_frontend/Components/LEDConfig/LEDConfig.svelte b/src_frontend/Components/LEDConfig/LEDConfig.svelte
new file mode 100644
index 0000000..2cf9aa2
--- /dev/null
+++ b/src_frontend/Components/LEDConfig/LEDConfig.svelte
@@ -0,0 +1,204 @@
+<script>
+ import Segment from "./Segment.svelte";
+ import MatrixSegment from "./MatrixSegment.svelte";
+ import { onMount } from "svelte";
+
+ import { authorizedSocket, authorizedSocketNeeded } from "../../stores/socketStore.js";
+ authorizedSocketNeeded.set(true);
+
+ let segments = [];
+
+ function addSegment() {
+ console.log(segments);
+ segments.push(null);
+ segments = segments; // This is needed because svelte is weird :)
+ saveConfig();
+ }
+ function removeSegment() {
+ segments.pop();
+ segments = segments; // This is needed because svelte is weird :)
+ saveConfig();
+ }
+ function segmentChange(ev) {
+ saveConfig();
+ }
+
+ let matrix = [[[null, false]]];
+
+ function addRow() {
+ matrix.push([[null, false]]);
+ matrix = matrix; // This is needed because svelte is weird :)
+ saveConfig();
+ }
+ function removeRow() {
+ matrix.pop();
+ matrix = matrix; // This is needed because svelte is weird :)
+ saveConfig();
+ }
+ function addCell(ev) {
+ matrix[ev.target.dataset.id].push([null, false]);
+ matrix = matrix; // This is needed because svelte is weird :)
+ saveConfig();
+ }
+ function removeCell() {
+ matrix[ev.target.dataset.id].pop();
+ matrix = matrix; // This is needed because svelte is weird :)
+ saveConfig();
+ }
+
+ let led_channel;
+ let led_dma;
+ let led_freq_hz;
+ let led_invert;
+ let led_pin;
+
+ authorizedSocket.on("led_config", (data) => {
+ segments = data.segments;
+ matrix = data.matrix;
+ led_channel = data.led_channel.toString();
+ led_dma = data.led_dma.toString();
+ led_freq_hz = data.led_freq_hz.toString();
+ led_invert = data.led_invert;
+ led_pin = data.led_pin.toString();
+ });
+
+ onMount(async () => {
+ authorizedSocket.emit("led_config:get");
+ });
+
+ function saveConfig() {
+ authorizedSocket.emit("led_config:set", {
+ segments: segments,
+ matrix: matrix,
+ led_channel: parseInt(led_channel, 10),
+ led_dma: parseInt(led_dma, 10),
+ led_freq_hz: parseInt(led_freq_hz, 10),
+ led_invert: led_invert,
+ led_pin: parseInt(led_pin, 10)
+ });
+ }
+</script>
+
+<style>
+ .wrapper {
+ padding-bottom: 15px;
+ }
+ h1 {
+ margin-bottom: 0;
+ font-weight: 100;
+ }
+
+ .row {
+ width: 100%;
+ overflow-x: auto;
+ white-space: nowrap;
+ }
+
+ .segment {
+ display: inline-block;
+ margin-bottom: 5px;
+ }
+ .segment:not(:first-child) {
+ margin-left: 5px;
+ }
+
+ button {
+ background-color: var(--grey-200);
+ border: none;
+ border-radius: 50px;
+ padding: 10px;
+ }
+ button:hover { background-color: var(--grey-300); }
+ button:active { background-color: var(--grey-400); }
+ button > * { pointer-events: none; }
+
+ select {
+ width: 100%;
+ }
+
+</style>
+
+<div class="wrapper">
+ <h1>Segments</h1>
+ <p>Here you are defining the "segments" of your light-display. Use this to split the strip in stairs, blocks or any other configuration. Normally you would define a segment, for each cut you have made in the strip. But you could do other things to get fancy results. Each segment will be represented in the "matrix", by the number just below the number. Each segment should have it's own box below.</p>
+
+ <div class="row">
+ {#each segments as segment, i}
+ <div class="segment">
+ <Segment on:change={segmentChange} on:blur={segmentChange} bind:ledCount={segment} id={i} />
+ </div>
+ {/each}
+ <button on:click={addSegment}><i class="fas fa-plus"></i></button>
+ <button on:click={removeSegment}><i class="fas fa-minus"></i></button>
+ </div>
+
+ <h1>Matrix</h1>
+ <p>Here you are defining your matrix. A matrix is really nothing more than a 2-dimentional array, or a list of lists. This is not a mathematical array, meaning each "row" can have different lengths. Use this to stitch your segments together. Each "box" should contain the number of the segment it is representing. By pressing the double-arrows on the box, you can "invert" a segment. This means counting from the other end. Use this if you have the segments in a snake-layout.</p>
+
+ <div class="row">
+ {#each matrix as row, i}
+ <div class="">
+ {#each row as cell}
+ <div class="segment">
+ <MatrixSegment on:change={saveConfig} bind:segmentId={cell[0]} bind:inverted={cell[1]} />
+ </div>
+ {/each}
+ <button on:click={addCell} data-id={i}><i class="fas fa-plus"></i></button>
+ <button on:click={removeCell} data-id={i}><i class="fas fa-minus"></i></button>
+ </div>
+ {/each}
+ <button on:click={addRow}><i class="fas fa-plus"></i></button>
+ <button on:click={removeRow}><i class="fas fa-minus"></i></button>
+ </div>
+
+ <h1>Advanced</h1>
+ <div class="">
+ <label for="gpioPin">GPIO pin</label>
+ <select id="gpioPin" bind:value={led_pin} on:change={saveConfig} >
+ <option value="10">10 (SPI)</option>
+ <option value="12">12 (PWM)</option>
+ <option value="18">18 (PWM) (Luxcena neo hat)</option>
+ <option value="21">21 (PCM)</option>
+ <option value="31">31 (PCM)</option>
+ <option value="38">39 (SPI)</option>
+ <option value="40">40 (PWM)</option>
+ <option value="52">52 (PWM)</option>
+ </select>
+
+ <label for="dmaChannel">DMA channel</label>
+ <select id="dmaChannel" bind:value={led_dma} on:change={saveConfig} >
+ <option value="0">0</option>
+ <option value="1">1</option>
+ <option value="2">2</option>
+ <option value="3">3</option>
+ <option value="4">4</option>
+ <option value="5">5</option>
+ <option value="6">6</option>
+ <option value="7">7</option>
+ <option value="8">8</option>
+ <option value="9">9</option>
+ <option value="10">10 (Recommended)</option>
+ <option value="11">11</option>
+ <option value="12">12</option>
+ <option value="13">13</option>
+ <option value="14">14</option>
+ <option value="15">15</option>
+ </select>
+
+ <label for="frequency">Frequency</label>
+ <select id="frequency" bind:value={led_freq_hz} on:change={saveConfig} >
+ <option value="800000">80k</option>
+ <option value="100000">10k</option>
+ </select>
+
+ <label for="ledChannel">LED Channel</label>
+ <select id="ledChannel" bind:value={led_channel} on:change={saveConfig} >
+ <option value="0">0</option>
+ <option value="1">1</option>
+ </select>
+
+ <input type="checkbox" id="invertSignal" bind:checked={led_invert} on:change={saveConfig} />
+ <label for="invertSignal">Invert signal</label>
+
+ </div>
+</div> \ No newline at end of file
diff --git a/src_frontend/Components/LEDConfig/MatrixSegment.svelte b/src_frontend/Components/LEDConfig/MatrixSegment.svelte
new file mode 100644
index 0000000..c2c1934
--- /dev/null
+++ b/src_frontend/Components/LEDConfig/MatrixSegment.svelte
@@ -0,0 +1,57 @@
+<script>
+ import { createEventDispatcher } from 'svelte';
+ const dispatch = createEventDispatcher();
+
+ export let segmentId;
+ export let inverted;
+
+ function toggleInvert() {
+ inverted = !inverted;
+ dispatch("change");
+ }
+
+ function forwardOnChange() {
+ dispatch("change")
+ }
+</script>
+
+<style>
+ div {
+ display: flex;
+ }
+ input {
+ padding: 10px 0 10px 10px;
+ width: 38px;
+ text-align: center;
+ background-color: var(--grey-300);
+ border: none;
+ border-radius: 5px 0 0 5px;
+ margin-right: -5px;
+ transition: background-color 0.5s ease;
+ }
+ input:focus {
+ outline: none;
+ background-color: var(--grey-400);
+ }
+ button {
+ background-color: var(--grey-300);
+ border: none;
+ border-radius: 0 10px 10px 0;
+ padding: 10 5px;
+ margin-left: 0;
+ }
+ .inverted input, .inverted button{
+ background-color: var(--amber-500);
+ }
+ button:hover {
+ background-color: var(--grey-400);
+ }
+ button:active {
+ background-color: var(--grey-500);
+ }
+</style>
+
+<div class:inverted={inverted}>
+ <input on:change={forwardOnChange} on:blur={forwardOnChange} type="number" step=1 bind:value={segmentId} />
+ <button on:click={toggleInvert}><i class="fas fa-exchange-alt"></i></button>
+</div> \ No newline at end of file
diff --git a/src_frontend/Components/LEDConfig/Segment.svelte b/src_frontend/Components/LEDConfig/Segment.svelte
new file mode 100644
index 0000000..3b71451
--- /dev/null
+++ b/src_frontend/Components/LEDConfig/Segment.svelte
@@ -0,0 +1,38 @@
+<script>
+ import { fade } from 'svelte/transition';
+ export let id;
+ export let ledCount;
+</script>
+
+<style>
+ div {
+ display: flex;
+ flex-direction: column;
+ }
+ input {
+ display: inline-block;
+ padding: 10px 10px 10px 10px;
+ width: 38px;
+ text-align: right;
+ background-color: var(--grey-300);
+ border: none;
+ border-radius: 5px 5px 0 0;
+ margin: 0;
+ }
+ input:focus {
+ outline: none;
+ background-color: var(--grey-400);
+ }
+ span {
+ background-color: var(--grey-500);
+ font-size: 12px;
+ padding: 2px;
+ border-radius: 0 0 5px 5px;
+ text-align: center;
+ }
+</style>
+
+<div transition:fade|local>
+ <input on:change on:blur type="number" data-id={id} step=1 bind:value={ledCount} />
+ <span>{id}</span>
+</div> \ No newline at end of file
diff --git a/src_frontend/Components/Logs/Logs.svelte b/src_frontend/Components/Logs/Logs.svelte
new file mode 100644
index 0000000..a7256ae
--- /dev/null
+++ b/src_frontend/Components/Logs/Logs.svelte
@@ -0,0 +1,10 @@
+
+<style>
+ .wrapper {
+ padding: var(--theme-padding);
+ }
+</style>
+
+<div class="wrapper">
+
+</div> \ No newline at end of file
diff --git a/src_frontend/Components/MainControls/ControlColors.svelte b/src_frontend/Components/MainControls/ControlColors.svelte
new file mode 100644
index 0000000..5d21cc0
--- /dev/null
+++ b/src_frontend/Components/MainControls/ControlColors.svelte
@@ -0,0 +1,107 @@
+<script>
+ import { writable } from 'svelte/store';
+ import { createEventDispatcher } from 'svelte';
+ import { fade } from 'svelte/transition';
+
+ import PrettyVar from "../../ComponentLib/PrettyVar.svelte";
+ import iro from '@jaames/iro';
+
+ const dispatch = createEventDispatcher();
+
+ let wrapperWidth = writable(0);
+
+ // This is a list of color variables that we can change
+ export let colorVariables = {
+ "Main": "#1bcf3f",
+ "Second": "#fafafa",
+ "tert": "#fafafa"
+ };
+ // These are some helper functions for the object
+ const getColorOfVar = function(name) { return colorVariables[name]; }
+ const setColorOfVar = function(name, color) { colorVariables[name] = color; }
+ // This is the identifier of the variable that currently is selected for changing
+ let currentVariable = Object.keys(colorVariables)[0];
+
+ let colorPicker;
+ function picker(node) {
+ colorPicker = new iro.ColorPicker(node, {
+ color: getColorOfVar(currentVariable),
+ width: $wrapperWidth
+ });
+ colorPicker.on('color:change', function(color) {
+ setColorOfVar(currentVariable, color.hexString);
+ dispatch("change", {name: currentVariable, value: color.hexString});
+ });
+
+ wrapperWidth.subscribe((width) => {
+ colorPicker.resize(width - 30);
+ });
+
+ return {
+ destroy() {}
+ };
+ }
+
+ function changeColorVar(ev) {
+ currentVariable = ev.target.dataset.id;
+ colorPicker.color.hexString = getColorOfVar(currentVariable);
+ }
+
+</script>
+
+<style>
+ .wrapper {
+ width: 100%;
+ box-sizing: border-box;
+ border-radius: 15px;
+ }
+
+ .color-picker {
+ padding: 15px;
+ }
+
+ .color-options {
+ margin-top: 15px;
+ display: flex;
+ width: 100%;
+ justify-content: center;
+ align-items: center;
+ }
+ .color-options > * {
+ margin: auto;
+ width: 100%;
+ text-align: center;
+ padding: 15px;
+ box-sizing: border-box;
+ border-top: 2px solid;
+ }
+ .color-options > *:first-child {
+ border-bottom-left-radius: 15px;
+ }
+ .color-options > *:last-child {
+ border-bottom-right-radius: 15px;
+ }
+ .color-options .selected {
+ background-color: #eaeaea;
+ }
+ .color-options > *:hover {
+ background-color: #dcdcdc;
+ }
+ .color-options > *:active {
+ background-color: #d4d4d4;
+ }
+</style>
+
+<div transition:fade|local class="wrapper drop-shadow" bind:clientWidth={$wrapperWidth} >
+ <div class="color-picker" use:picker></div>
+ <div class="color-options">
+ {#each Object.keys(colorVariables) as colorVar}
+ <span style="border-top-color: {getColorOfVar(colorVar)}"
+ data-id={colorVar}
+ on:click={changeColorVar}
+ class:selected={colorVar == currentVariable}>
+ <PrettyVar varText={colorVar} />
+ </span>
+ {/each}
+ </div>
+</div> \ No newline at end of file
diff --git a/src_frontend/Components/MainControls/ControlComponents.svelte b/src_frontend/Components/MainControls/ControlComponents.svelte
new file mode 100644
index 0000000..5f6d165
--- /dev/null
+++ b/src_frontend/Components/MainControls/ControlComponents.svelte
@@ -0,0 +1,139 @@
+<script>
+ import { onMount } from "svelte";
+ import { fade } from 'svelte/transition';
+ import RoundRange from "../../ComponentLib/RoundRange.svelte";
+ import FloatingButton from "../../ComponentLib/Button/FloatingButton.svelte";
+ import FloatingSelect from "../../ComponentLib/FloatingSelect.svelte";
+ import PrettyVar from "../../ComponentLib/PrettyVar.svelte";
+ import ControlColors from "./ControlColors.svelte";
+ import ControlOthers from "./ControlOthers.svelte";
+
+ import { openSocket } from "../../stores/socketStore";
+
+ let name = "-";
+ let brightness = 0;
+ let powerIsOn = false;
+
+ let modeSelect;
+ let allModes = {};
+ let activeMode = "";
+
+ let colorVariables = {};
+
+ function togglePower() {
+ powerIsOn = !powerIsOn;
+ openSocket.emit("power:set", powerIsOn);
+ }
+ function setBrightness() {
+ if (!powerIsOn) {
+ togglePower();
+ }
+ openSocket.emit("brightness:set", Math.floor((brightness * 255) / 100));
+ }
+ function setColor(ev) {
+ openSocket.emit("var:set", ev.detail.name, ev.detail.value);
+ }
+ function setMode(el) {
+ openSocket.emit("mode:set", el.target.value, (res) => {
+ console.log(res);
+ })
+ }
+ function onVarChange(name, value) {
+ if (!name.includes("variable/")) {
+ console.log(`Change on unknown globvar "${name}".`);
+ return;
+ }
+ name = name.replace("variable/", "");
+
+ switch (value.var_type) {
+ case "COLOR":
+ if (value.value == null) {
+ delete colorVariables[name];
+ } else {
+ colorVariables[name] = value.value;
+ }
+ colorVariables = colorVariables;
+ break;
+ }
+ }
+ openSocket.on("modelist", (modelist) => {
+ allModes = [];
+ for (let i = 0; i < modelist.length; i++) {
+ let modePath = modelist[i].split("/", 1)[0];
+ if (!allModes.hasOwnProperty(modePath)) { allModes[modePath] = []; }
+ allModes[modePath].push([modelist[i], modelist[i].replace(modePath + "/", "")])
+ }
+ allModes = allModes;
+ })
+ openSocket.on("name", (_name) => name = _name);
+ openSocket.on("mode", (newMode) => activeMode = newMode);
+ openSocket.on("power", (power) => powerIsOn = power);
+ openSocket.on("brightness", (value) => brightness = Math.floor((value * 100) / 255));
+ openSocket.on("var", onVarChange);
+ openSocket.on("vars", (vars) => {
+ for (const [name, value] of Object.entries(vars)) {
+ onVarChange(`variable/${name}`, value);
+ }
+ });
+
+ onMount(async () => {
+ openSocket.emit("name:get");
+ openSocket.emit("modelist:get");
+ openSocket.emit("mode:get");
+ openSocket.emit("power:get");
+ openSocket.emit("brightness:get");
+ openSocket.emit("vars:get");
+ });
+
+</script>
+
+<style>
+ .wrapper {
+ box-sizing: border-box;
+ padding-bottom: 17px;
+ text-align: center;
+ }
+ h1 {
+ color: var(--grey-600);
+ margin: 0;
+ font-size: 20px;
+ font-weight: 100;;
+ }
+ .row {
+ margin-bottom: 25px;
+ margin-left: 10%;
+ margin-right: 10%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+ .mode_button {
+ margin-left: 10%;
+ flex-grow: 1;
+ }
+</style>
+
+<div class="wrapper">
+ <h1>{name}</h1>
+ <RoundRange bind:enabled={powerIsOn} on:change={setBrightness} bind:value={brightness} />
+
+ <div class="row">
+ <FloatingButton label="POWER" on:click={togglePower} bind:active={powerIsOn} activeBackgroundColor="var(--yellow-500)" activeColor="black" faIcon="fa fa-power-off" />
+ <div class="mode_button">
+ <FloatingSelect label="MODE" faIcon="fas fa-sliders-h" on:change={setMode} bind:this={modeSelect} bind:value={activeMode} >
+ {#each Object.entries(allModes) as [modePath, modes]}
+ <optgroup label={modePath}>
+ {#each modes as mode}
+ <option value={mode[0]}><PrettyVar varText={mode[1]} /></option>
+ {/each}
+ </optgroup>
+ {/each}
+ </FloatingSelect>
+ </div>
+ </div>
+
+ {#if Object.keys(colorVariables).length > 0}
+ <ControlColors on:change={setColor} bind:colorVariables={colorVariables} />
+ {/if}
+ <!-- <ControlOthers /> -->
+</div> \ No newline at end of file
diff --git a/src_frontend/Components/MainControls/ControlOthers.svelte b/src_frontend/Components/MainControls/ControlOthers.svelte
new file mode 100644
index 0000000..862a4f5
--- /dev/null
+++ b/src_frontend/Components/MainControls/ControlOthers.svelte
@@ -0,0 +1,36 @@
+<script>
+
+ // This is a list of variables that we can change
+ let variables = [
+ {id: 1, name: "Speed", type: "range", value: 20, min: 0, max: 100},
+ {id: 2, name: "Tingle intensity", type: "range", value: 40, min: 0, max: 255},
+ {id: 3, name: "Amount of tingle", type: "range", value: 90, min: 0, max: 100},
+ {id: 4, name: "Variable of unknown type", type: "date", value: "January"}
+ ];
+
+</script>
+
+<style>
+ .wrapper {
+ width: 100%;
+ box-sizing: border-box;
+ border-radius: 15px;
+ margin-top: 15px;
+ padding: 15px;
+ text-align: left;
+ }
+ input {
+ width: 100%;
+ }
+</style>
+
+<div class="wrapper drop-shadow">
+ {#each variables as variable}
+ <label for={variable.id}>{variable.name}</label>
+ {#if variable.type == "range"}
+ <input type="range" id={variable.id} min={variable.min} max={variable.max} value={variable.value}>
+ {:else}
+ <input type="text" id={variable.id} value={variable.value} />
+ {/if}
+ {/each}
+</div> \ No newline at end of file
diff --git a/src_frontend/Components/MainMenu.svelte b/src_frontend/Components/MainMenu.svelte
new file mode 100644
index 0000000..7de36a5
--- /dev/null
+++ b/src_frontend/Components/MainMenu.svelte
@@ -0,0 +1,80 @@
+<script>
+ import { link } from 'svelte-spa-router';
+ import active from 'svelte-spa-router/active';
+ import { fade } from 'svelte/transition';
+
+ let menuItems = {
+ 0: {type: "item", name: "Home", href: "/"},
+ // 1: {type: "item", name: "Schedules", href: "/schedules"},
+ 2: {type: "seperator"},
+ 3: {type: "item", name: "Mode editor", href: "/modes"},
+ 4: {type: "item", name: "LED configuration", href: "/led_config"},
+ // 5: {type: "item", name: "Logs", href: "/logs"},
+ 6: {type: "seperator"},
+ 7: {type: "item", name: "Settings", href: "/settings"},
+ 8: {type: "seperator"},
+ 9: {type: "abslink", name: "Docs", href: "/docs"}
+ };
+</script>
+
+<style>
+ .menu {
+ position: absolute;
+ height: calc(100% - var(--theme-phone-header-height));
+ top: var(--theme-phone-header-height);
+ overflow-y: auto;
+ padding: var(--theme-padding);
+ font-size: 20px;
+ width: 100%;
+ box-sizing: border-box;
+ }
+ .menu a {
+ display: block;
+ text-decoration: none;
+ margin-bottom: 10px;
+ color: var(--grey-300);
+ }
+ .menu a:hover {
+ color: var(--grey-400);
+ }
+ .menu a:active {
+ color: var(--grey-600);
+ }
+ .menu hr {
+ border: 0.1px solid var(--grey-700);
+ }
+ @media (min-width: 900px) {
+ .menu {
+ position: fixed;
+ background-color: #f7f7f7;
+ top: 0;
+ right: 0;
+ font-size: 12px;
+ width: 180px;
+ height: 100%;
+ padding-top: 65px;
+ }
+ .menu a {
+ color: var(--grey-600);
+ text-transform: uppercase;
+ font-weight: bold;
+ margin-bottom: 20px;
+ }
+ .menu hr {
+ display: none;
+ border-color: var(--grey-100);
+ }
+ }
+</style>
+
+<div class="menu">
+ {#each Object.entries(menuItems) as [id, menuItem]}
+ {#if menuItem.type == "item"}
+ <a use:link use:active href={menuItem.href}>{menuItem.name}</a>
+ {:else if menuItem.type == "abslink"}
+ <a href={menuItem.href}>{menuItem.name}</a>
+ {:else if menuItem.type == "seperator"}
+ <hr />
+ {/if}
+ {/each}
+</div> \ No newline at end of file
diff --git a/src_frontend/Components/ModeList/Mode.svelte b/src_frontend/Components/ModeList/Mode.svelte
new file mode 100644
index 0000000..67752c2
--- /dev/null
+++ b/src_frontend/Components/ModeList/Mode.svelte
@@ -0,0 +1,57 @@
+<script>
+ import { push } from "svelte-spa-router";
+ import ConfirmActionDialog from "../Dialogs/ConfirmActionDialog.svelte";
+ import { authorizedSocket } from "../../stores/socketStore.js";
+ import { notif } from "../../stores/notifs";
+ export let id;
+
+ function deleteMode() {
+ authorizedSocket.emit("mode:delete", `user/${id}`, (res) => {
+ if (!res.success) {
+ notif({title: "Error", text: "Could not delete mode...", type: "danger"})
+ console.log(res);
+ }
+ });
+ }
+</script>
+
+<style>
+ .wrapper {
+ width: 100%;
+ padding: var(--theme-padding);
+ box-sizing: border-box;
+ border-radius: 15px;
+
+ display: flex;
+ align-items: center;
+ }
+ .right {
+ margin-left: auto;
+ }
+ button {
+ border: none;
+ background-color: white;
+ background-color: transparent;
+ border: none;
+ padding: 10px;
+ border-radius: 15px;
+ }
+ button:hover {
+ background-color: var(--grey-300);
+ }
+ button:active {
+ background-color: var(--grey-400);
+ }
+</style>
+
+<div class="wrapper drop-shadow">
+ {id}
+ <div class="right">
+ <ConfirmActionDialog title="Are you sure?" text="Are you sure you want to delete {id}" action={deleteMode}>
+ <svelte:fragment slot="trigger" let:open>
+ <button on:click={open}><i class="fas fa-trash"></i></button>
+ </svelte:fragment>
+ </ConfirmActionDialog>
+ <button on:click={() => {push(`/editor/${id}`)}}><i class="fas fa-edit"></i></button>
+ </div>
+</div> \ No newline at end of file
diff --git a/src_frontend/Components/ModeList/ModeList.svelte b/src_frontend/Components/ModeList/ModeList.svelte
new file mode 100644
index 0000000..8bac3f9
--- /dev/null
+++ b/src_frontend/Components/ModeList/ModeList.svelte
@@ -0,0 +1,64 @@
+<script>
+ import { onMount } from "svelte";
+ import { fade } from 'svelte/transition';
+ import FloatingButton from "../../ComponentLib/Button/FloatingButton.svelte";
+ import Mode from "./Mode.svelte";
+ import NewModeDialog from "./NewModeDialog.svelte";
+
+ import { openSocket, authorizedSocket, authorizedSocketNeeded } from "../../stores/socketStore.js";
+ authorizedSocketNeeded.set(true);
+
+ let userModes = [];
+ let remotes = [];
+
+ openSocket.on("modelist", (modes) => {
+ userModes = [];
+ remotes = [];
+ for (let i = 0; i < modes.length; i++) {
+ if (modes[i].substr(0, 4) === "user") {
+ userModes.push(modes[i].replace("user/", ""));
+ }
+ if (modes[i].substr(0, 6) === "remote") {
+ remotes.push(modes[i].replace("remote/", ""));
+ }
+ }
+ });
+ onMount(async() => {
+ openSocket.emit("modelist:get");
+ });
+</script>
+
+<style>
+ .wrapper {
+ padding-bottom: var(--theme-padding);
+ }
+ .modes > * {
+ margin-bottom: 10px;
+ }
+ .button_menu {
+ margin-top: 20px;
+ width: 100%;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ }
+</style>
+
+<div class="wrapper">
+ <h1>Modes</h1>
+ <div class="modes">
+ {#each userModes as mode}
+ <div>
+ <Mode id={mode} />
+ </div>
+ {/each}
+ </div>
+
+ <div class="button_menu">
+ <NewModeDialog>
+ <svelte:fragment slot="trigger" let:open>
+ <FloatingButton on:click={open} faIcon="fas fa-plus" label="NEW" />
+ </svelte:fragment>
+ </NewModeDialog>
+ </div>
+</div> \ No newline at end of file
diff --git a/src_frontend/Components/ModeList/NewModeDialog.svelte b/src_frontend/Components/ModeList/NewModeDialog.svelte
new file mode 100644
index 0000000..539e3ef
--- /dev/null
+++ b/src_frontend/Components/ModeList/NewModeDialog.svelte
@@ -0,0 +1,153 @@
+<script>
+ import { onMount } from "svelte";
+ import dialogPolyfill from 'dialog-polyfill'
+ import Button from "../../ComponentLib/Button/Button.svelte";
+ import PrettyVar from "../../ComponentLib/PrettyVar.svelte";
+ import { openSocket, authorizedSocket } from "../../stores/socketStore.js";
+ import { notif } from "../../stores/notifs";
+
+ let modal;
+ let activeTab = 0;
+ let name;
+ let sourceMode;
+
+ function open() {
+ modal.showModal()
+ }
+ function register(node) {
+ dialogPolyfill.registerDialog(node);
+ }
+ function createMode() {
+ let template = "template/base";
+ if (activeTab == 1) {
+ template = sourceMode;
+ }
+ authorizedSocket.emit("mode:create", name, template, (res) => {
+ if (!res.success) {
+ notif({title: "Error", text: "Could not create mode...", type: "danger"})
+ };
+
+ modal.close();
+ });
+ }
+
+ let builtinModes = [];
+ let userModes = [];
+ // let remoteModes = [];
+ openSocket.on("modelist", (modes) => {
+ builtinModes = [];
+ for (let i = 0; i < modes.length; i++) {
+ if (modes[i].substr(0, 8) === "builtin/") {
+ builtinModes.push([modes[i], modes[i].replace("builtin/", "")]);
+ }
+ if (modes[i].substr(0, 5) === "user/") {
+ userModes.push([modes[i], modes[i].replace("user/", "")]);
+ }
+ // if (modes[i].substr(0, 6) === "remote") {
+ // remotes.push(modes[i].replace("", ""));
+ // }
+ }
+ });
+ onMount(async() => {
+ openSocket.emit("modelist:get");
+ })
+</script>
+
+<style>
+ dialog {
+ padding: 0;
+ border: none;
+ border-radius: 15px;
+ }
+ .tabs {
+ display: flex;
+ width: 100%;
+ }
+ .tabs i {
+ position: relative;
+ flex-grow: 1;
+ text-align: center;
+ font-size: 20px;
+ padding: 15px;
+ }
+ .tabs i:hover {
+ color: var(--theme-primary);
+ }
+ .tabs i.active {
+ color: var(--theme-primary);
+ }
+ .tabs > *:not(:last-child):after {
+ content: "";
+ position: absolute;
+ width: 1px;
+ height: 43%;
+ border-left: 0.1px solid var(--grey-400);
+ left: 100%;
+ }
+ .divider-h {
+ margin-left: 15px;
+ margin-right: 15px;
+ border-top: 0.01px solid var(--grey-400);
+ }
+ .content {
+ padding: 15px;
+ box-sizing: border-box;
+ }
+ input, select {
+ display: block;
+ width: 100%;
+ box-sizing: border-box;
+ }
+ .buttons {
+ padding: 0 15px 15px 15px;
+ display: flex;
+ }
+ .buttons > * {
+ flex-grow: 1;
+ }
+ .buttons > *:not(:last-child) {
+ margin-right: 5px;
+ }
+ .buttons > *:not(:first-child) {
+ margin-left: 5px;
+ }
+</style>
+
+<slot name="trigger" {open}></slot>
+<dialog bind:this={modal} use:register>
+
+ <div class="tabs">
+ <i class:active={activeTab == 0} on:click={() => { activeTab = 0; }} class="far fa-file"></i>
+ <i class:active={activeTab == 1} on:click={() => { activeTab = 1; }} class="far fa-clone"></i>
+ </div>
+
+ <div class="divider-h"></div>
+ <div class="content">
+ {#if [0, 1].includes(activeTab)}
+ <div>
+ <label for="fname">Mode name</label>
+ <input type="text" id="fname" placeholder="My_Awesome_New_Mode" bind:value={name} />
+ {#if activeTab == 1}
+ <label for="sourcemode">Source</label>
+ <select bind:value={sourceMode}>
+ <optgroup label="builtin">
+ {#each builtinModes as mode}
+ <option value={mode[0]}><PrettyVar varText={mode[1]} /></option>
+ {/each}
+ </optgroup>
+ <optgroup label="user">
+ {#each userModes as mode}
+ <option value={mode[0]}><PrettyVar varText={mode[1]} /></option>
+ {/each}
+ </optgroup>
+ </select>
+ {/if}
+ </div>
+ {/if}
+ </div>
+ <div class="buttons">
+ <div><Button fullWidth=true on:click={() => modal.close() } color={"var(--theme-primary)"} backgroundColor={"white"}>Cancel</Button></div>
+ <div><Button fullWidth=true on:click={createMode}>Create</Button></div>
+ </div>
+
+</dialog> \ No newline at end of file
diff --git a/src_frontend/Components/NotImplemented.svelte b/src_frontend/Components/NotImplemented.svelte
new file mode 100644
index 0000000..a3f385a
--- /dev/null
+++ b/src_frontend/Components/NotImplemented.svelte
@@ -0,0 +1,7 @@
+<script>
+ export let params;
+</script>
+
+<div>
+ Sorry, this functionality is not implemented yet...
+</div> \ No newline at end of file
diff --git a/src_frontend/Components/Notifs/Notif.svelte b/src_frontend/Components/Notifs/Notif.svelte
new file mode 100644
index 0000000..c8ef807
--- /dev/null
+++ b/src_frontend/Components/Notifs/Notif.svelte
@@ -0,0 +1,126 @@
+<script>
+ import { fade, fly } from "svelte/transition";
+ import { removeNotif } from "../../stores/notifs";
+ import { elasticOut } from "svelte/easing";
+ export let nid;
+ export let text = "";
+ export let title;
+ export let type;
+ export let transitionType = "fly";
+ function customTransition(
+ node,
+ { duration = 400, x = 0, y = 0, opacity = 0 }
+ ) {
+ return {
+ duration,
+ css: (t, u) => {
+ // t = 0 to 1, u = (1 - t)
+ if (transitionType === "fly")
+ return `
+ transform: translate(${x * (1 - t)}px,${y * (1 - t)}px);
+ opacity: ${t};
+ `;
+ else
+ return `
+ opacity: ${t};
+ `;
+ }
+ };
+ }
+ </script>
+
+ <style>
+ .notification {
+ max-width: 300px;
+ padding: 16px;
+ padding-right: 0;
+ margin: 10px 0;
+ box-shadow: 0 3px 6px rgb(0, 0, 0, 0.3);
+ border-radius: 4px;
+ display: flex;
+ justify-content: space-between;
+ z-index: 1000;
+ }
+ .notification.default {
+ background: rgb(194, 194, 194);
+ color: black;
+ border: 1px solid rgb(150, 150, 150);
+ border-left: 4px solid rgb(150, 150, 150);
+ }
+ .notification.danger {
+ background: rgb(255, 72, 72);
+ border: 1px solid rgb(235, 15, 15);
+ border-left: 4px solid rgb(235, 15, 15);
+ color: white;
+ }
+ .notification.success {
+ background: rgb(23, 153, 12);
+ border: 1px solid rgb(15, 116, 6);
+ border-left: 4px solid rgb(15, 116, 6);
+ color: white;
+ }
+ .notification.warning {
+ background: rgb(255, 144, 69);
+ border: 1px solid rgb(238, 103, 12);
+ border-left: 4px solid rgb(238, 103, 12);
+ color: white;
+ }
+ .notification.info {
+ background: rgb(69, 150, 255);
+ border: 1px solid rgb(8, 97, 214);
+ border-left: 4px solid rgb(8, 97, 214);
+ color: white;
+ }
+ .notification .notification__content {
+ padding-right: 16px;
+ }
+ .notification .notification__content--title {
+ font-weight: 500;
+ font-size: 0.9rem;
+ text-transform: uppercase;
+ margin: 0;
+ margin-bottom: 5px;
+ }
+ .notification .notification__content--text {
+ font-size: 0.9rem;
+ margin: 0;
+ word-wrap: break-word;
+ overflow: hidden;
+ }
+ .notification .notification__close {
+ font-size: 1rem;
+ font-weight: bold;
+ margin: 0;
+ text-align: right;
+ text-transform: uppercase;
+ padding: 0 6px;
+ border-left: 1px solid rgba(0, 0, 0, 0.199);
+ display: flex;
+ align-items: center;
+ /* justify-content: center; */
+ }
+ .notification.info .notification__close,
+ .notification.danger .notification__close,
+ .notification.warning .notification__close,
+ .notification.success .notification__close {
+ color: white;
+ border-left-color: rgba(255, 255, 255, 0.438);
+ }
+ .notification .notification__close span {
+ cursor: pointer;
+ }
+ </style>
+
+ <div
+ class="notification {type || 'default'}"
+ transition:customTransition={{ x: 50, duration: 200 }}>
+ <div class="notification__content">
+ {#if title}
+ <h1 class="notification__content--title">{title}</h1>
+ {/if}
+ <p class="notification__content--text">{text}</p>
+ </div>
+ <div class="notification__close">
+ <span on:click={() => removeNotif(nid)}>&times;</span>
+ </div>
+ </div> \ No newline at end of file
diff --git a/src_frontend/Components/Notifs/NotifsWrapper.svelte b/src_frontend/Components/Notifs/NotifsWrapper.svelte
new file mode 100644
index 0000000..e68e2e2
--- /dev/null
+++ b/src_frontend/Components/Notifs/NotifsWrapper.svelte
@@ -0,0 +1,33 @@
+<script>
+ import { notifs } from "../../stores/notifs";
+ import Notif from "./Notif.svelte";
+ export let position = "bottom-center";
+ $: transitionType = position === "bottom-center" ? "fade" : "fly";
+</script>
+
+<style>
+.notifications {
+ position: absolute;
+ z-index: 999;
+}
+.bottom-center {
+ bottom: 20px;
+ left: 5vw;
+ width: 90vw;
+}
+.top-right {
+ top: 20px;
+ right: 20px;
+}
+</style>
+
+<div class="notifications {position}">
+{#each $notifs as notification (notification.id)}
+ <Notif
+ nid={notification.id}
+ text={notification.text}
+ title={notification.title}
+ type={notification.type}
+ {transitionType} />
+{/each}
+</div> \ No newline at end of file
diff --git a/src_frontend/Components/Settings/CreateEditUser.svelte b/src_frontend/Components/Settings/CreateEditUser.svelte
new file mode 100644
index 0000000..ca87336
--- /dev/null
+++ b/src_frontend/Components/Settings/CreateEditUser.svelte
@@ -0,0 +1,93 @@
+<script>
+ import { onMount } from "svelte";
+ import dialogPolyfill from 'dialog-polyfill'
+ import Button from "../../ComponentLib/Button/Button.svelte";
+ import { authorizedSocket } from "../../stores/socketStore.js";
+ import { notif } from "../../stores/notifs";
+
+ export let username = null;
+
+ let modal;
+ let password = "";
+ let newUser = username == null;
+
+ function open() {
+ modal.showModal()
+ }
+ function register(node) {
+ dialogPolyfill.registerDialog(node);
+ }
+
+ function updateUser() {
+ authorizedSocket.emit("user:newpassword", username, password, (res) => {
+ if (!res.success) { notif({title: res.reason}); }
+ modal.close();
+ });
+ }
+
+ function createUser() {
+ authorizedSocket.emit("user:create", username, password, (res) => {
+ if (!res.success) { notif({title: res.reason}); }
+ modal.close();
+ });
+ }
+</script>
+
+<style>
+ dialog {
+ padding: 0;
+ border: none;
+ border-radius: 15px;
+ }
+ h2 {
+ margin: 0;
+ }
+ .content {
+ padding: 15px;
+ box-sizing: border-box;
+ }
+ input {
+ display: block;
+ width: 100%;
+ box-sizing: border-box;
+ }
+ .buttons {
+ padding: 0 15px 15px 15px;
+ display: flex;
+ }
+ .buttons > * {
+ flex-grow: 1;
+ }
+ .buttons > *:not(:last-child) {
+ margin-right: 5px;
+ }
+ .buttons > *:not(:first-child) {
+ margin-left: 5px;
+ }
+</style>
+
+<slot name="trigger" {open}></slot>
+<dialog bind:this={modal} use:register>
+ <div class="content">
+ <h2>
+ {#if newUser}
+ Create user
+ {:else}
+ Edit user
+ {/if}
+ </h2>
+ <label for="username">Username</label>
+ <input type="text" id="username" placeholder="username" bind:value={username} disabled="{!newUser}" />
+ <label for="password">password</label>
+ <input type="password" id="password" bind:value={password} />
+ </div>
+ <div class="buttons">
+ <div><Button fullWidth=true on:click={() => modal.close() } color={"var(--theme-primary)"} backgroundColor={"white"}>Cancel</Button></div>
+ {#if newUser}
+ <div><Button fullWidth=true on:click={createUser}>Create</Button></div>
+ {:else}
+ <div><Button fullWidth=true on:click={updateUser}>Update</Button></div>
+ {/if}
+ </div>
+
+</dialog> \ No newline at end of file
diff --git a/src_frontend/Components/Settings/InstanceName.svelte b/src_frontend/Components/Settings/InstanceName.svelte
new file mode 100644
index 0000000..6d1892a
--- /dev/null
+++ b/src_frontend/Components/Settings/InstanceName.svelte
@@ -0,0 +1,36 @@
+<script>
+ import { onMount } from "svelte";
+ import { openSocket, authorizedSocket } from "../../stores/socketStore.js";
+
+ let name = "-";
+
+ openSocket.on("name", (_name) => name = _name);
+
+ function saveName() {
+ authorizedSocket.emit("name:set", name, (res) => {});
+ }
+
+ onMount(async() => {
+ openSocket.emit("name:get");
+ });
+</script>
+
+<style>
+ div {
+ margin-bottom: 15px;
+ }
+ h1 { margin-bottom: 0; }
+ input {
+ background-color: var(--grey-200);
+ border-radius: 15px;
+ width: 100%;
+ padding: 15px;
+ box-sizing: border-box;
+ border: none;
+ }
+</style>
+
+<div>
+ <h1>Name</h1>
+ <input type="text" bind:value={name} on:change={saveName} />
+</div>
diff --git a/src_frontend/Components/Settings/SSLCert.svelte b/src_frontend/Components/Settings/SSLCert.svelte
new file mode 100644
index 0000000..adb3649
--- /dev/null
+++ b/src_frontend/Components/Settings/SSLCert.svelte
@@ -0,0 +1,57 @@
+<script>
+ import { onMount } from "svelte";
+
+ import FloatingButton from "../../ComponentLib/Button/FloatingButton.svelte";
+ import ConfirmActionDialog from "../Dialogs/ConfirmActionDialog.svelte";
+ import { authorizedSocket } from "../../stores/socketStore.js";
+
+ let isValid = false;
+ let CN = "-";
+ let validTime = "-";
+
+ authorizedSocket.on("sslcert:info", (status) => {
+ isValid = status.isValid;
+ CN = status.CN;
+ validTime = Math.round((status.certExpire-(new Date()).getTime())/86400000);
+ });
+
+ let newCertPromise;
+ function generateNewCert() {
+ newCertPromise = new Promise((resolve, reject) => {
+ authorizedSocket.emit("sslcert:generate_new", () => {
+ resolve();
+ });
+ });
+ }
+
+ onMount(async() => {
+ authorizedSocket.emit("sslcert:info");
+ });
+</script>
+
+<style>
+ h1 { margin-bottom: 0; }
+ p { margin: 0; }
+ .small {
+ font-weight: 100;
+ font-style: italic;
+ font-size: 12px;
+ color: var(--grey-600);
+ }
+ .button {
+ margin-top: 10px;
+ }
+</style>
+
+<div>
+ <h1>SSL Certificate</h1>
+ <p>{isValid ? "VALID" : "INVALID"} <span class="small">(for {validTime} days)</span></p>
+ <p><span class="small">CN</span> {CN}</p>
+
+ <ConfirmActionDialog title="Are you sure?" text="Are you sure you want to generate new self signed SSL Certificate?" action={generateNewCert}>
+ <svelte:fragment slot="trigger" let:open>
+ <div class="button"><FloatingButton on:click={open} bind:loadingPromise={newCertPromise} fullWidth=true>Generate new cerificate</FloatingButton></div>
+ </svelte:fragment>
+ </ConfirmActionDialog>
+ <!-- <div class="button"><Button fullWidth=true round=true>Show details</Button></div> -->
+</div>
diff --git a/src_frontend/Components/Settings/Settings.svelte b/src_frontend/Components/Settings/Settings.svelte
new file mode 100644
index 0000000..b95f1ac
--- /dev/null
+++ b/src_frontend/Components/Settings/Settings.svelte
@@ -0,0 +1,25 @@
+<script>
+ import { fade } from 'svelte/transition';
+ import InstanceName from './InstanceName.svelte';
+ import Version from "./Version.svelte";
+ import SSLCert from "./SSLCert.svelte";
+ import System from "./System.svelte";
+ import Users from "./Users.svelte";
+
+ import { authorizedSocketNeeded } from "../../stores/socketStore.js";
+ authorizedSocketNeeded.set(true);
+</script>
+
+<style>
+ div {
+ padding-bottom: var(--theme-padding);
+ }
+</style>
+
+<div>
+ <InstanceName />
+ <Version />
+ <SSLCert />
+ <Users />
+ <System />
+</div> \ No newline at end of file
diff --git a/src_frontend/Components/Settings/System.svelte b/src_frontend/Components/Settings/System.svelte
new file mode 100644
index 0000000..1af4531
--- /dev/null
+++ b/src_frontend/Components/Settings/System.svelte
@@ -0,0 +1,34 @@
+<script>
+ import FloatingButton from "../../ComponentLib/Button/FloatingButton.svelte";
+ import ConfirmActionDialog from "../Dialogs/ConfirmActionDialog.svelte";
+ import { authorizedSocket } from "../../stores/socketStore.js";
+
+ function restartSystem() {
+ authorizedSocket.emit("restart:system");
+ }
+ function restartService() {
+ authorizedSocket.emit("restart:service");
+ }
+
+</script>
+
+<style>
+ h1, p {
+ margin-bottom: 10px;
+ }
+ .button { margin-bottom: 10px; }
+</style>
+
+<div>
+ <h1>System restart</h1>
+ <ConfirmActionDialog title="Are you sure?" text="Are you sure you want to restart the rPI?" action={restartSystem}>
+ <svelte:fragment slot="trigger" let:open>
+ <div class="button"><FloatingButton on:click={open} fullWidth=true>Restart system</FloatingButton></div>
+ </svelte:fragment>
+ </ConfirmActionDialog>
+ <ConfirmActionDialog title="Are you sure?" text="Are you sure you want to restart the Luxcena NEO service?" action={restartService}>
+ <svelte:fragment slot="trigger" let:open>
+ <div class="button"><FloatingButton on:click={open} fullWidth=true>Restart service</FloatingButton></div>
+ </svelte:fragment>
+ </ConfirmActionDialog>
+</div> \ No newline at end of file
diff --git a/src_frontend/Components/Settings/Users.svelte b/src_frontend/Components/Settings/Users.svelte
new file mode 100644
index 0000000..7e68c3a
--- /dev/null
+++ b/src_frontend/Components/Settings/Users.svelte
@@ -0,0 +1,93 @@
+<script>
+ import { onMount } from "svelte";
+ import { authorizedSocket, authorizedSocketNeeded, user } from "../../stores/socketStore.js";
+ authorizedSocketNeeded.set(true);
+ import CreateEditUser from "./CreateEditUser.svelte";
+ import ConfirmActionDialog from "../Dialogs/ConfirmActionDialog.svelte";
+ import FloatingButton from "../../ComponentLib/Button/FloatingButton.svelte";
+
+
+ let usersList = [];
+
+ function deleteUser(username) {
+ authorizedSocket.emit("user:delete", username, (res) => {
+ if (!res.success) { notif({title: res.reason}); }
+ });
+ };
+
+ authorizedSocket.on("users", (users) => {
+ usersList = users;
+ });
+
+ onMount(() => {
+ authorizedSocket.emit("users:get");
+ });
+</script>
+
+<style>
+ h1 {
+ margin: 0;
+ }
+ ul {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+ }
+ li {
+ width: 100%;
+ display: flex;
+ padding: 10px 10px;
+ border-radius: 5px;
+ align-items:center;
+ box-sizing: border-box;
+ }
+ li:hover {
+ background-color: var(--grey-100);
+ }
+ li:not(:last-child) {
+ border-bottom: 0.5px solid var(--grey-400);
+ }
+ .align-right { margin-left: auto; }
+ button {
+ background-color: transparent;
+ border: none;
+ padding: 10px;
+ border-radius: 50%;
+ }
+ button:hover {
+ background-color: var(--grey-300);
+ }
+ button:active {
+ background-color: var(--grey-400);
+ }
+</style>
+
+<div>
+ <h1>Users</h1>
+ <ul>
+ {#each usersList as _user}
+ <li>
+ {_user}
+ <div class="align-right">
+ {#if $user?.username != _user}
+ <ConfirmActionDialog title="Are you sure?" text="Are you sure you want to delete {_user}" action={() => {deleteUser(_user)}}>
+ <svelte:fragment slot="trigger" let:open>
+ <button on:click={open}><i class="fas fa-trash"></i></button>
+ </svelte:fragment>
+ </ConfirmActionDialog>
+ {/if}
+ <CreateEditUser username={_user}>
+ <svelte:fragment slot="trigger" let:open>
+ <button on:click={open}><i class="fas fa-edit"></i></button>
+ </svelte:fragment>
+ </CreateEditUser>
+ </div>
+ </li>
+ {/each}
+ </ul>
+ <CreateEditUser>
+ <svelte:fragment slot="trigger" let:open>
+ <div class="button"><FloatingButton on:click={open} fullWidth=true>Create new user</FloatingButton></div>
+ </svelte:fragment>
+ </CreateEditUser>
+</div> \ No newline at end of file
diff --git a/src_frontend/Components/Settings/Version.svelte b/src_frontend/Components/Settings/Version.svelte
new file mode 100644
index 0000000..29d04ae
--- /dev/null
+++ b/src_frontend/Components/Settings/Version.svelte
@@ -0,0 +1,60 @@
+<script>
+ import { onMount } from 'svelte';
+ import FloatingButton from "../../ComponentLib/Button/FloatingButton.svelte";
+ import PrettyVar from "../../ComponentLib/PrettyVar.svelte";
+ import { authorizedSocket } from "../../stores/socketStore.js";
+
+ let version = "-";
+ let branch = "-";
+ let newVer = "-";
+
+ authorizedSocket.on("version:branch", _branch => branch = _branch);
+ authorizedSocket.on("version:current_number", _version => version = _version);
+ authorizedSocket.on("version:newest_number", _version => newVer = _version);
+
+ let checkVersionPromise;
+ function checkForUpdate() {
+ checkVersionPromise = new Promise((resolve, reject) => {
+ authorizedSocket.emit("version:check_for_update", () => {
+ resolve();
+ });
+ });
+ }
+
+ onMount(async() => {
+ authorizedSocket.emit("version:branch");
+ authorizedSocket.emit("version:current_number");
+ authorizedSocket.emit("version:newest_number");
+ });
+</script>
+
+<style>
+ h1, p { margin: 0; }
+ .label {
+ font-weight: 100;
+ font-style: italic;
+ color: var(--grey-600);
+ }
+ .button-row {
+ /* display: flex; */
+ margin-top: 20px;
+ width: 100%;
+ /* justify-content: center; */
+ /* align-items: center; */
+ }
+ .update-available {
+ color: var(--green-300);
+ }
+</style>
+
+<div>
+ <h1>Version</h1>
+ <p><span class="label">Current version</span> <PrettyVar varText={version}/></p>
+ <p><span class="label">Current branch</span> <PrettyVar bind:varText={branch}/></p>
+ {#if newVer != version}
+ <p><span class="update-available">Version <PrettyVar bind:varText={newVer} /> available.</span></p>
+ {/if}
+ <div class="button-row">
+ <FloatingButton on:click={checkForUpdate} bind:loadingPromise={checkVersionPromise} fullWidth=true>Check for updates</FloatingButton>
+ </div>
+</div> \ No newline at end of file
diff --git a/src_frontend/layout/Desktop.svelte b/src_frontend/layout/Desktop.svelte
new file mode 100644
index 0000000..e90a4a2
--- /dev/null
+++ b/src_frontend/layout/Desktop.svelte
@@ -0,0 +1,48 @@
+<script>
+ import ControlComponents from "../Components/MainControls/ControlComponents.svelte";
+ import MainMenu from "../Components/MainMenu.svelte";
+ import NotifsWrapper from "../Components/Notifs/NotifsWrapper.svelte";
+</script>
+
+<style>
+ .controls {
+ background-color: white;
+ position: fixed;
+ width: 300px;
+ padding: 15px;
+ left: 50px;
+ top: 50px;
+ bottom: 50px;
+
+ overflow: auto;
+ border-radius: 15px;
+ max-height: 500px;
+ }
+
+ @media (min-height: 600px) {
+ .controls {
+ height: 500px;
+ top: 50%;
+ transform: translateY(-50%);
+ }
+ }
+
+ .content {
+ margin-left: 400px;
+ height: 100%;
+ overflow: auto;
+ margin-top: 35px;
+ margin-bottom: 15px;
+ margin-right: 185px;
+ padding: 15px;
+ }
+</style>
+
+<NotifsWrapper />
+<div>
+ <MainMenu />
+ <div class="controls drop-shadow"><ControlComponents /></div>
+ <div class="content">
+ <slot></slot>
+ </div>
+</div> \ No newline at end of file
diff --git a/src_frontend/layout/Drawer.svelte b/src_frontend/layout/Drawer.svelte
new file mode 100644
index 0000000..abf41fd
--- /dev/null
+++ b/src_frontend/layout/Drawer.svelte
@@ -0,0 +1,38 @@
+<script>
+ import { location } from 'svelte-spa-router'
+ import { slide } from 'svelte/transition';
+
+ export let open = true;
+
+ location.subscribe((value) => {
+ open = true;
+ });
+
+ function drawerInit(node) {
+ node.addEventListener("click", () => {
+ if (!open) { open = true; }
+ });
+ }
+</script>
+
+<style>
+ .drawer {
+ background-color: white;
+ position: absolute;
+ padding: var(--theme-padding);
+ height: calc(100% - var(--theme-phone-header-height));
+ width: 100%;
+ box-sizing: border-box;
+ bottom: 0;
+ border-radius: 30px 30px 0 0;
+ transition: height 1s ease;
+ }
+ .closed {
+ height: 20%;
+ overflow: hidden;
+ }
+</style>
+
+<div class="drawer" class:closed={!open} use:drawerInit transition:slide>
+ <slot></slot>
+</div> \ No newline at end of file
diff --git a/src_frontend/layout/Phone.svelte b/src_frontend/layout/Phone.svelte
new file mode 100644
index 0000000..dc8d69a
--- /dev/null
+++ b/src_frontend/layout/Phone.svelte
@@ -0,0 +1,50 @@
+<script>
+ import NotifsWrapper from "../Components/Notifs/NotifsWrapper.svelte";
+ import MainMenu from "../Components/MainMenu.svelte";
+ import Drawer from "./Drawer.svelte";
+
+ let drawerOpen;
+
+ function toggleDrawer() {
+ drawerOpen = !drawerOpen;
+ }
+</script>
+
+<style>
+ main {
+ background-color: #3c3b3b;
+ height: 100%;
+ width: 100%;
+ }
+ .header {
+ position: fixed;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ width: 100%;
+ height: var(--theme-phone-header-height);
+ color: white;
+ }
+ .header > * {
+ margin: auto;
+ }
+ .header > div:first-child, .header > div:last-child {
+ margin-left: 15px;
+ margin-right: 15px;
+ }
+</style>
+
+<NotifsWrapper />
+<main>
+ <div class="header">
+ <div><!--<i class="fas fa-chevron-left"></i>--></div>
+ <div>Luxcena NEO</div>
+ <div on:click={toggleDrawer}><i class="fas fa-bars"></i></div>
+ </div>
+ <MainMenu />
+
+ <Drawer bind:open={drawerOpen}>
+ <slot></slot>
+ </Drawer>
+</main> \ No newline at end of file
diff --git a/src_frontend/main.js b/src_frontend/main.js
new file mode 100644
index 0000000..4c473fa
--- /dev/null
+++ b/src_frontend/main.js
@@ -0,0 +1,7 @@
+import App from './App.svelte';
+
+const app = new App({
+ target: document.body
+});
+
+export default app; \ No newline at end of file
diff --git a/src_frontend/routes/EditorRoute.svelte b/src_frontend/routes/EditorRoute.svelte
new file mode 100644
index 0000000..283f8db
--- /dev/null
+++ b/src_frontend/routes/EditorRoute.svelte
@@ -0,0 +1,9 @@
+<script>
+ import Editor from "../Components/Editor/Editor.svelte";
+ import NotifsWrapper from "../Components/Notifs/NotifsWrapper.svelte";
+ export let params;
+ let modeId = params.wild;
+</script>
+
+<NotifsWrapper position="top-right" />
+<Editor modeId={modeId} /> \ No newline at end of file
diff --git a/src_frontend/routes/LoginRoute.svelte b/src_frontend/routes/LoginRoute.svelte
new file mode 100644
index 0000000..1f9ef45
--- /dev/null
+++ b/src_frontend/routes/LoginRoute.svelte
@@ -0,0 +1,106 @@
+<script>
+ import Button from "../ComponentLib/Button/Button.svelte";
+ import { authenticate } from "../stores/socketStore";
+
+ let isLoading = false;
+
+ let username;
+ let password;
+
+ let errorMessage = null;
+
+ function tryLogin() {
+ isLoading = true;
+ authenticate(username, password, (success) => {
+ if (!success) {
+ errorMessage = "Username/password incorrect";
+ setTimeout(() => {
+ errorMessage = null;
+ }, 5000);
+ }
+ isLoading = false;
+ });
+ }
+</script>
+
+<style>
+ main {
+ width: 100%;
+ height: 100%;
+ box-sizing: border-box;
+ background-color: #3c3b3b;
+ text-align: center;
+ }
+ .login {
+ position: absolute;
+ left: 50%;
+ top: 50%;
+ transform: translateX(-50%) translateY(-50%);
+ background-color: white;
+ border-radius: 15px;
+ max-width: 200px;
+ box-sizing: border-box;
+ }
+ img {
+ margin-bottom: -18px;
+ margin-top: -5px;
+ }
+ form {
+ padding: 15px;
+ padding-top: 0;
+ text-align: left;
+ }
+ h2 {
+ margin: 0;
+ }
+ input {
+ width: 100%;
+ margin-top: 5px;
+ margin-bottom: 5px;
+ box-sizing: border-box;
+ border: none;
+ padding: 10px 15px;
+ background: var(--grey-200);
+ border-radius: 5px;
+ }
+ label {
+ font-size: 12px;
+ color: var(--grey-600);
+ margin-bottom: 5px;
+ }
+ .error-message {
+ color: var(--red-500);
+ font-size: 12px;
+ margin: 15px;
+ margin-top: 0;
+ }
+</style>
+
+<main>
+ <div class="login">
+ <img src="./assets/img/logo/Icon-192h.png" alt="">
+ <!-- <h2>Luxcena neo Login</h2> -->
+ <form on:submit|preventDefault={tryLogin}>
+ <label for="username">Username</label>
+ <input type="text" id="username" autocomplete="username" minlength="1" required bind:value={username} />
+
+ <label for="password">Password</label>
+ <input type="password" id="password" autocomplete="current-password" minlength="1" required bind:value={password} />
+
+ <Button fullWidth=true>
+ {#if isLoading}
+ <i class="fas fa-spinner fa-pulse"></i>
+ {:else}
+ Login
+ {/if}
+ </Button>
+ </form>
+
+ {#if errorMessage}
+ <div class="error-message">
+ {errorMessage}
+ </div>
+ {/if}
+
+ </div>
+</main> \ No newline at end of file
diff --git a/src_frontend/routes/MainRoute.svelte b/src_frontend/routes/MainRoute.svelte
new file mode 100644
index 0000000..30a689c
--- /dev/null
+++ b/src_frontend/routes/MainRoute.svelte
@@ -0,0 +1,61 @@
+<script>
+ import ControlComponents from "../Components/MainControls/ControlComponents.svelte";
+ import ModeList from "../Components/ModeList/ModeList.svelte";
+ import LEDConfig from "../Components/LEDConfig/LEDConfig.svelte";
+ import Settings from "../Components/Settings/Settings.svelte";
+ import NotImplemented from '../Components/NotImplemented.svelte';
+
+ import Phone from "../layout/Phone.svelte";
+ import Desktop from "../layout/Desktop.svelte";
+
+ export let params;
+ $: updateComponent(params);
+
+ let activeLayout = Phone;
+ let activeComponent = ControlComponents;
+
+ const mql = window.matchMedia('(max-width: 900px)');
+ try {
+ mql.addEventListener('change', () => { updateLayout(); });
+ } catch {
+ mql.addListener(() => { updateLayout(); });
+ }
+
+ function updateLayout() {
+ const mobileView = mql.matches;
+ if (mobileView) {
+ activeLayout = Phone;
+ } else {
+ activeLayout = Desktop;
+ }
+ }
+ function updateComponent(params) {
+ switch (params[0]) {
+ case "/":
+ if (mql.matches) {
+ activeComponent = ControlComponents
+ } else {
+ activeComponent = ModeList;
+ }
+ break;
+ case "/led_config":
+ activeComponent = LEDConfig;
+ break;
+ case "/modes":
+ activeComponent = ModeList;
+ break;
+ case "/settings":
+ activeComponent = Settings;
+ break;
+ default:
+ activeComponent = NotImplemented;
+ break;
+ }
+ }
+
+ updateLayout();
+</script>
+
+<svelte:component this={activeLayout}>
+ <svelte:component this={activeComponent}/>
+</svelte:component>
diff --git a/src_frontend/routes/UnknownRoute.svelte b/src_frontend/routes/UnknownRoute.svelte
new file mode 100644
index 0000000..eb649dd
--- /dev/null
+++ b/src_frontend/routes/UnknownRoute.svelte
@@ -0,0 +1,21 @@
+<script>
+ export let params;
+</script>
+
+<style>
+ main {
+ width: 100%;
+ height: 100%;
+ box-sizing: border-box;
+ padding: 15px;
+ background-color: #3c3b3b;
+ text-align: center;
+ color: white;
+ }
+</style>
+
+<main>
+ <div>
+ Unknown path ({params.wild})...
+ </div>
+</main> \ No newline at end of file
diff --git a/src_frontend/routes/WidgetRoute.svelte b/src_frontend/routes/WidgetRoute.svelte
new file mode 100644
index 0000000..a3bbbe7
--- /dev/null
+++ b/src_frontend/routes/WidgetRoute.svelte
@@ -0,0 +1,130 @@
+<script>
+ import { onMount } from "svelte";
+ import { openSocket } from "../stores/socketStore";
+
+ let instanceName = "-"
+ let brightness = 0;
+ let powerIsOn = false;
+
+ openSocket.on("name", (name) => instanceName = name);
+ openSocket.on("power", (power) => powerIsOn = power);
+ openSocket.on("brightness", (value) => brightness = value);
+ // openSocket.on("mode", (newMode) => activeMode = newMode);
+
+ function setBrightness() {
+ openSocket.emit("brightness:set", brightness);
+ }
+ function setPower() {
+ openSocket.emit("power:set", powerIsOn);
+ }
+
+ onMount(async () => {
+ openSocket.emit("name:get");
+ openSocket.emit("power:get");
+ openSocket.emit("brightness:get");
+ });
+</script>
+
+<style>
+ .wrapper {
+ background-color: #3c3b3b;
+ width: 100%;
+ height: 100%;
+ display: grid;
+ grid-template-columns: 1fr 60px;
+ grid-template-rows: 80% 1fr;
+ grid-template-areas:
+ "name power"
+ "brightness brightness"
+ ;
+ padding: 15px;
+ box-sizing: border-box;
+ }
+ .name {
+ color: white;
+ grid-area: name;
+ font-weight: bold;
+ align-self: center;
+ }
+ .power {
+ grid-area: power;
+ align-self: center;
+ }
+ .brightness { grid-area: brightness; }
+ input[type=range] {
+ width: 100%;
+ }
+
+ /* The switch - the box around the slider */
+ .switch {
+ position: relative;
+ display: inline-block;
+ width: 60px;
+ height: 34px;
+ }
+
+ /* Hide default HTML checkbox */
+ .switch input {
+ opacity: 0;
+ width: 0;
+ height: 0;
+ }
+
+ /* The slider */
+ .slider {
+ position: absolute;
+ cursor: pointer;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background-color: #ccc;
+ -webkit-transition: .4s;
+ transition: .4s;
+ border-radius: 34px;
+ }
+
+ .slider:before {
+ position: absolute;
+ content: "";
+ height: 26px;
+ width: 26px;
+ left: 4px;
+ bottom: 4px;
+ background-color: white;
+ -webkit-transition: .4s;
+ transition: .4s;
+ border-radius: 50%;
+ }
+
+ input:checked + .slider {
+ background-color: #2196F3;
+ }
+
+ input:focus + .slider {
+ box-shadow: 0 0 1px #2196F3;
+ }
+
+ input:checked + .slider:before {
+ -webkit-transform: translateX(26px);
+ -ms-transform: translateX(26px);
+ transform: translateX(26px);
+ }
+ /* .slider.round { border-radius: 34px; }
+ .slider.round:before { border-radius: 50%; } */
+</style>
+
+<div class="wrapper">
+ <div class="name">
+ {instanceName}
+ </div>
+ <div class="power">
+ <label class="switch">
+ <input type="checkbox" bind:checked={powerIsOn} on:change={setPower}/>
+ <span class="slider round"></span>
+ </label>
+ </div>
+ <div class="brightness">
+ <input type="range" min=0 max=255 bind:value={brightness} on:change={setBrightness}/>
+ </div>
+</div> \ No newline at end of file
diff --git a/src_frontend/stores/notifs.js b/src_frontend/stores/notifs.js
new file mode 100644
index 0000000..f117f18
--- /dev/null
+++ b/src_frontend/stores/notifs.js
@@ -0,0 +1,24 @@
+import { writable } from "svelte/store";
+import { nanoid } from 'nanoid'
+
+export const notifs = writable([]);
+
+export function notif(notification) {
+ let _notif = {
+ id: nanoid(),
+ timeout: 10000,
+ ...notification
+ }
+ notifs.update(_notifs => {
+ setTimeout(() => {
+ removeNotif(_notif.id)
+ }, _notif.timeout);
+ return [..._notifs, _notif];
+ });
+}
+
+export function removeNotif(notifId) {
+ notifs.update(_notifs => {
+ return _notifs.filter(n => n.id !== notifId);
+ });
+}
diff --git a/src_frontend/stores/socketStore.js b/src_frontend/stores/socketStore.js
new file mode 100644
index 0000000..d87a075
--- /dev/null
+++ b/src_frontend/stores/socketStore.js
@@ -0,0 +1,112 @@
+import { writable, derived, get } from "svelte/store";
+import { replace } from "svelte-spa-router";
+import { io } from 'socket.io-client';
+import {location, querystring} from 'svelte-spa-router'
+
+export const openSocket = io("/open");
+export let openSocketConnected = writable(false);
+export let openSocketReconnecting = writable(false);
+
+openSocket.on("connect", () => {
+ openSocketConnected.set(true);
+});
+openSocket.on("disconnect", () => {
+ openSocketConnected.set(false);
+});
+openSocket.io.on("reconnect_attempt", () => {
+ openSocketReconnecting.set(true);
+});
+openSocket.io.on("reconnect", () => {
+ openSocketReconnecting.set(false);
+});
+
+export const authorizedSocket = io("/authed", {auth: {token: "te"}});
+export let authorizedSocketConnected = writable(false);
+export let authorizedSocketReconnecting = writable(false);
+export let authorizedSocketConnectError = writable(false);
+export const user = writable({});
+
+authorizedSocket.on("connect_error", (err) => {
+ authorizedSocketConnectError.set(true);
+ authorizedSocketReconnecting.set(false);
+ if ((err.message == "not authorized") &&
+ get(authorizedSocketNeeded) &&
+ (get(location) != "/login")) {
+ replace(`/login?ref=${get(location)}&${get(querystring)}`);
+ authorizedSocketConnected.set(false);
+ }
+});
+authorizedSocket.on("connect", () => {
+ authorizedSocketConnected.set(true);
+ authorizedSocketConnectError.set(false);
+ if (get(location) == "/login") {
+ let searchParams = new URLSearchParams(get(querystring))
+ if (searchParams.has("ref")) {
+ let path = searchParams.get("ref");
+ searchParams.delete("ref");
+ let params = "";
+ if (searchParams.values().length > 0) {
+ params = "?" + searchParams.toString();
+ }
+ replace(`${path}${params}`);
+ } else {
+ replace(`/`);
+ }
+ }
+});
+authorizedSocket.on("disconnect", () => {
+ authorizedSocketConnected.set(false);
+});
+authorizedSocket.io.on("reconnect_attempt", () => {
+ authorizedSocketReconnecting.set(true);
+});
+authorizedSocket.io.on("reconnect", () => {
+ authorizedSocketReconnecting.set(false);
+});
+authorizedSocket.on("user", (userObj) => {
+ user.set(userObj);
+});
+
+
+let storedSessionToken = localStorage.getItem("sessionToken");
+export const isAuthenticating = writable(storedSessionToken != undefined);
+export const sessionToken = writable(storedSessionToken);
+function connectAuthorizedSocket() {
+ authorizedSocket.auth.token = get(sessionToken);
+ authorizedSocket.disconnect().connect();
+}
+sessionToken.subscribe((token) => localStorage.setItem("sessionToken", token));
+sessionToken.subscribe(() => connectAuthorizedSocket());
+
+export const authorizedSocketNeeded = writable(false);
+authorizedSocketNeeded.subscribe((value) => {
+ if (value) { connectAuthorizedSocket(); }
+});
+
+export function authenticate(username, password, callback) {
+ openSocket.emit("authenticate:user", username, password, (res) => {
+ if (res.success) {
+ sessionToken.set(res.token);
+ } else if (res.reason != "Invalid username/password") {
+ console.log(res);
+ }
+ callback(res.success);
+ });
+}
+
+export const connected = writable(false);
+export const reconnecting = writable(false);
+function connectedStateChange() {
+ // console.log(`${get(openSocketConnected)} ${get(authorizedSocketConnectError)} ${get(authorizedSocketReconnecting)}`);
+ connected.set(get(openSocketConnected)
+ && (get(authorizedSocketConnectError)
+ || get(authorizedSocketConnected)));
+ reconnecting.set(get(openSocketReconnecting)
+ && (get(authorizedSocketConnectError)
+ || get(authorizedSocketReconnecting)));
+}
+openSocketConnected.subscribe(connectedStateChange);
+openSocketReconnecting.subscribe(connectedStateChange);
+authorizedSocketConnected.subscribe(connectedStateChange);
+authorizedSocketReconnecting.subscribe(connectedStateChange);
+authorizedSocketConnectError.subscribe(connectedStateChange);