🌌 Migrate app to svelte-5

This commit is contained in:
Rune Harlyk
2025-02-26 22:28:30 +01:00
committed by Rune Harlyk
parent d9285bbdc0
commit 788f4ffea3
51 changed files with 1512 additions and 1348 deletions
+8 -8
View File
@@ -21,8 +21,8 @@
"@playwright/test": "^1.49.1", "@playwright/test": "^1.49.1",
"@sveltejs/adapter-auto": "^3.0.0", "@sveltejs/adapter-auto": "^3.0.0",
"@sveltejs/adapter-static": "^3.0.1", "@sveltejs/adapter-static": "^3.0.1",
"@sveltejs/kit": "^2.5.5", "@sveltejs/kit": "^2.5.27",
"@sveltejs/vite-plugin-svelte": "^3.0.0", "@sveltejs/vite-plugin-svelte": "^4.0.0",
"@types/eslint": "^8.56.0", "@types/eslint": "^8.56.0",
"@types/three": "^0.162.0", "@types/three": "^0.162.0",
"@typescript-eslint/eslint-plugin": "^7.0.0", "@typescript-eslint/eslint-plugin": "^7.0.0",
@@ -30,19 +30,19 @@
"autoprefixer": "^10.4.19", "autoprefixer": "^10.4.19",
"eslint": "^8.56.0", "eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.35.1", "eslint-plugin-svelte": "^2.45.1",
"jsdom": "^24.0.0", "jsdom": "^24.0.0",
"postcss": "^8.4.38", "postcss": "^8.4.38",
"prettier": "^3.1.1", "prettier": "^3.1.1",
"prettier-plugin-svelte": "^3.1.2", "prettier-plugin-svelte": "^3.2.6",
"svelte": "^4.2.7", "svelte": "^5.0.0",
"svelte-check": "^3.6.0", "svelte-check": "^4.0.0",
"svelte-focus-trap": "^1.2.0", "svelte-focus-trap": "^1.2.0",
"tailwindcss": "^3.4.3", "tailwindcss": "^3.4.3",
"tslib": "^2.6.1", "tslib": "^2.6.1",
"typescript": "^5.1.6", "typescript": "^5.5.0",
"unplugin-icons": "^0.18.5", "unplugin-icons": "^0.18.5",
"vite": "^5.0.3", "vite": "^5.4.4",
"vitest": "^1.2.0" "vitest": "^1.2.0"
}, },
"type": "module", "type": "module",
+509 -548
View File
File diff suppressed because it is too large Load Diff
+38 -37
View File
@@ -1,43 +1,44 @@
<script lang="ts"> <script lang="ts">
import { slide } from 'svelte/transition'; import { slide } from 'svelte/transition';
import { cubicOut } from 'svelte/easing'; import { cubicOut } from 'svelte/easing';
import { createEventDispatcher } from 'svelte'; import { Down } from './icons';
import { Down } from './icons';
const dispatch = createEventDispatcher(); function openCollapsible() {
open = !open;
if (open) {
opened();
} else {
closed();
}
}
function openCollapsible() { let { icon, title, children, open, opened, closed, class: klass } = $props();
open = !open;
if (open) {
dispatch('opened');
} else {
dispatch('closed');
}
}
export let open = false;
</script> </script>
<div class="{$$props.class || ''} relative grid w-full max-w-2xl self-center overflow-hidden"> <div class="{klass} relative grid w-full max-w-2xl self-center overflow-hidden">
<div class="min-h-16 flex w-full items-center justify-between space-x-3 p-4 text-xl font-medium"> <div
<span class="inline-flex items-baseline"> class="min-h-16 flex w-full items-center justify-between space-x-3 p-4 text-xl font-medium"
<slot name="icon" /> >
<slot name="title" /> <span class="inline-flex items-baseline">
</span> {@render icon?.()}
<button class="btn btn-circle btn-ghost btn-sm" on:click={() => openCollapsible()}> {@render title?.()}
<Down </span>
class="text-base-content h-auto w-6 transition-transform duration-300 ease-in-out {open <button class="btn btn-circle btn-ghost btn-sm" onclick={() => openCollapsible()}>
? 'rotate-180' <Down
: ''}" class="text-base-content h-auto w-6 transition-transform duration-300 ease-in-out {(
/> open
</button> ) ?
</div> 'rotate-180'
{#if open} : ''}"
<div />
class="flex flex-col gap-2 p-4 pt-0" </button>
transition:slide|local={{ duration: 300, easing: cubicOut }} </div>
> {#if open}
<slot /> <div
</div> class="flex flex-col gap-2 p-4 pt-0"
{/if} transition:slide|local={{ duration: 300, easing: cubicOut }}
>
{@render children?.()}
</div>
{/if}
</div> </div>
+28 -14
View File
@@ -1,47 +1,61 @@
<script lang="ts"> <script lang="ts">
import { createBubbler } from 'svelte/legacy';
const bubble = createBubbler();
import { closeModal } from 'svelte-modals'; import { closeModal } from 'svelte-modals';
import { focusTrap } from 'svelte-focus-trap'; import { focusTrap } from 'svelte-focus-trap';
import { fly } from 'svelte/transition'; import { fly } from 'svelte/transition';
import { Cancel, Check } from '$lib/components/icons'; import { Cancel, Check } from '$lib/components/icons';
// provided by <Modals /> // provided by <Modals />
export let isOpen: boolean;
export let title: string; interface Props {
export let message: string; isOpen: boolean;
export let onConfirm: any; title: string;
export let labels = { message: string;
onConfirm: any;
labels?: any;
}
let {
isOpen,
title,
message,
onConfirm,
labels = {
cancel: { label: 'Cancel', icon: Cancel }, cancel: { label: 'Cancel', icon: Cancel },
confirm: { label: 'OK', icon: Check } confirm: { label: 'OK', icon: Check }
}; }
}: Props = $props();
</script> </script>
{#if isOpen} {#if isOpen}
{@const SvelteComponent = labels?.confirm.icon}
<div <div
role="dialog" role="dialog"
class="pointer-events-none fixed inset-0 z-50 flex items-center justify-center" class="pointer-events-none fixed inset-0 z-50 flex items-center justify-center"
transition:fly={{ y: 50 }} transition:fly={{ y: 50 }}
on:introstart onintrostart={bubble('introstart')}
on:outroend onoutroend={bubble('outroend')}
use:focusTrap use:focusTrap
> >
<div <div
class="rounded-box bg-base-100 pointer-events-auto flex min-w-fit max-w-md flex-col justify-between p-4 shadow-lg" class="rounded-box bg-base-100 pointer-events-auto flex min-w-fit max-w-md flex-col justify-between p-4 shadow-lg"
> >
<h2 class="text-base-content text-start text-2xl font-bold">{title}</h2> <h2 class="text-base-content text-start text-2xl font-bold">{title}</h2>
<div class="divider my-2" /> <div class="divider my-2"></div>
<p class="text-base-content mb-1 text-start">{message}</p> <p class="text-base-content mb-1 text-start">{message}</p>
<div class="divider my-2" /> <div class="divider my-2"></div>
<div class="flex justify-end gap-2"> <div class="flex justify-end gap-2">
<button class="btn btn-primary inline-flex items-center" on:click={closeModal} <button class="btn btn-primary inline-flex items-center" onclick={closeModal}
><svelte:component this={labels.cancel.icon} class="mr-2 h-5 w-5" /><span ><labels.cancel.icon class="mr-2 h-5 w-5" /><span
>{labels?.cancel.label}</span >{labels?.cancel.label}</span
></button ></button
> >
<button <button
class="btn btn-warning text-warning-content inline-flex items-center" class="btn btn-warning text-warning-content inline-flex items-center"
on:click={onConfirm} onclick={onConfirm}
><svelte:component this={labels?.confirm.icon} class="mr-2 h-5 w-5" /><span ><SvelteComponent class="mr-2 h-5 w-5" /><span
>{labels?.confirm.label}</span >{labels?.confirm.label}</span
></button ></button
> >
@@ -1,4 +1,7 @@
<script lang="ts"> <script lang="ts">
import { run, createBubbler } from 'svelte/legacy';
const bubble = createBubbler();
import { closeAllModals, onBeforeClose } from 'svelte-modals'; import { closeAllModals, onBeforeClose } from 'svelte-modals';
import { focusTrap } from 'svelte-focus-trap'; import { focusTrap } from 'svelte-focus-trap';
import { fly } from 'svelte/transition'; import { fly } from 'svelte/transition';
@@ -6,35 +9,45 @@
import { Cancel } from './icons'; import { Cancel } from './icons';
// provided by <Modals /> // provided by <Modals />
export let isOpen: boolean; interface Props {
isOpen: boolean;
let updating = true;
let progress = 0;
$: if ($telemetry.download_ota.status == 'progress') {
progress = $telemetry.download_ota.progress;
} }
$: if ($telemetry.download_ota.status == 'error') { let { isOpen }: Props = $props();
updating = false;
}
let message = 'Preparing ...'; let updating = $state(true);
let timerId: number;
$: if ($telemetry.download_ota.status == 'progress') { let progress = $state(0);
message = 'Downloading ...'; run(() => {
} else if ($telemetry.download_ota.status == 'error') { if ($telemetry.download_ota.status == 'progress') {
message = $telemetry.download_ota.error; progress = $telemetry.download_ota.progress;
} else if ($telemetry.download_ota.status == 'finished') { }
message = 'Restarting ...'; });
progress = 0;
// Reload page after 5 sec run(() => {
timerId = setTimeout(() => { if ($telemetry.download_ota.status == 'error') {
closeAllModals(); updating = false;
location.reload(); }
}, 5000); });
}
let message = $state('Preparing ...');
let timerId: number = $state();
run(() => {
if ($telemetry.download_ota.status == 'progress') {
message = 'Downloading ...';
} else if ($telemetry.download_ota.status == 'error') {
message = $telemetry.download_ota.error;
} else if ($telemetry.download_ota.status == 'finished') {
message = 'Restarting ...';
progress = 0;
// Reload page after 5 sec
timerId = setTimeout(() => {
closeAllModals();
location.reload();
}, 5000);
}
});
onBeforeClose(() => { onBeforeClose(() => {
if (updating) { if (updating) {
@@ -54,32 +67,32 @@
role="dialog" role="dialog"
class="pointer-events-none fixed inset-0 z-50 flex items-center justify-center" class="pointer-events-none fixed inset-0 z-50 flex items-center justify-center"
transition:fly={{ y: 50 }} transition:fly={{ y: 50 }}
on:introstart onintrostart={bubble('introstart')}
on:outroend onoutroend={bubble('outroend')}
use:focusTrap use:focusTrap
> >
<div <div
class="bg-base-100 rounded-box pointer-events-auto flex max-h-full min-w-fit max-w-md flex-col justify-between p-4 shadow-lg" class="bg-base-100 rounded-box pointer-events-auto flex max-h-full min-w-fit max-w-md flex-col justify-between p-4 shadow-lg"
> >
<h2 class="text-base-content text-start text-2xl font-bold">Updating Firmware</h2> <h2 class="text-base-content text-start text-2xl font-bold">Updating Firmware</h2>
<div class="divider my-2" /> <div class="divider my-2"></div>
<div class="overflow-y-auto"> <div class="overflow-y-auto">
<div class="bg-base-100 flex flex-col items-center justify-center p-6"> <div class="bg-base-100 flex flex-col items-center justify-center p-6">
{#if $telemetry.download_ota.status == 'progress'} {#if $telemetry.download_ota.status == 'progress'}
<progress class="progress progress-primary w-56" value={progress} max="100" /> <progress class="progress progress-primary w-56" value={progress} max="100"></progress>
{:else} {:else}
<progress class="progress progress-primary w-56" /> <progress class="progress progress-primary w-56"></progress>
{/if} {/if}
<p class="mt-8 text-2xl">{message}</p> <p class="mt-8 text-2xl">{message}</p>
</div> </div>
</div> </div>
<div class="divider my-2" /> <div class="divider my-2"></div>
<div class="flex flex-wrap justify-end gap-2"> <div class="flex flex-wrap justify-end gap-2">
<div class="flex-grow" /> <div class="flex-grow"></div>
<button <button
class="btn btn-warning text-warning-content inline-flex flex-none items-center" class="btn btn-warning text-warning-content inline-flex flex-none items-center"
disabled={updating} disabled={updating}
on:click={() => { onclick={() => {
closeAllModals(); closeAllModals();
location.reload(); location.reload();
}} }}
+24 -11
View File
@@ -1,15 +1,28 @@
<script lang="ts"> <script lang="ts">
import { createBubbler } from 'svelte/legacy';
const bubble = createBubbler();
import { focusTrap } from 'svelte-focus-trap'; import { focusTrap } from 'svelte-focus-trap';
import { fly } from 'svelte/transition'; import { fly } from 'svelte/transition';
import { Check } from './icons'; import { Check } from './icons';
// provided by <Modals /> // provided by <Modals />
export let isOpen: boolean;
export let title: string; interface Props {
export let message: string; isOpen: boolean;
export let onDismiss: any; title: string;
export let dismiss = { label: 'Dismiss', icon: Check }; message: string;
onDismiss: any;
dismiss?: any;
}
let {
isOpen,
title,
message,
onDismiss,
dismiss = { label: 'Dismiss', icon: Check }
}: Props = $props();
</script> </script>
{#if isOpen} {#if isOpen}
@@ -17,22 +30,22 @@
role="dialog" role="dialog"
class="pointer-events-none fixed inset-0 z-50 flex items-center justify-center" class="pointer-events-none fixed inset-0 z-50 flex items-center justify-center"
transition:fly={{ y: 50 }} transition:fly={{ y: 50 }}
on:introstart onintrostart={bubble('introstart')}
on:outroend onoutroend={bubble('outroend')}
use:focusTrap use:focusTrap
> >
<div <div
class="rounded-box bg-base-100 pointer-events-auto flex min-w-fit max-w-md flex-col justify-between p-4 shadow-lg" class="rounded-box bg-base-100 pointer-events-auto flex min-w-fit max-w-md flex-col justify-between p-4 shadow-lg"
> >
<h2 class="text-base-content text-start text-2xl font-bold">{title}</h2> <h2 class="text-base-content text-start text-2xl font-bold">{title}</h2>
<div class="divider my-2" /> <div class="divider my-2"></div>
<p class="text-base-content mb-1 text-start">{message}</p> <p class="text-base-content mb-1 text-start">{message}</p>
<div class="divider my-2" /> <div class="divider my-2"></div>
<div class="flex justify-end gap-2"> <div class="flex justify-end gap-2">
<button <button
class="btn btn-warning text-warning-content inline-flex items-center" class="btn btn-warning text-warning-content inline-flex items-center"
on:click={onDismiss} onclick={onDismiss}
><svelte:component this={dismiss.icon} class="mr-2 h-5 w-5" /><span>{dismiss.label}</span ><dismiss.icon class="mr-2 h-5 w-5" /><span>{dismiss.label}</span
></button ></button
> >
</div> </div>
+22 -9
View File
@@ -2,8 +2,21 @@
import { slide } from 'svelte/transition'; import { slide } from 'svelte/transition';
import { cubicOut } from 'svelte/easing'; import { cubicOut } from 'svelte/easing';
import { Down } from './icons'; import { Down } from './icons';
export let open = true; interface Props {
export let collapsible = true; open?: boolean;
collapsible?: boolean;
icon?: import('svelte').Snippet;
title?: import('svelte').Snippet;
children?: import('svelte').Snippet;
}
let {
open = $bindable(true),
collapsible = true,
icon,
title,
children
}: Props = $props();
</script> </script>
{#if collapsible} {#if collapsible}
@@ -14,12 +27,12 @@
class="min-h-16 flex w-full items-center justify-between space-x-3 p-4 text-xl font-medium" class="min-h-16 flex w-full items-center justify-between space-x-3 p-4 text-xl font-medium"
> >
<span class="inline-flex items-baseline"> <span class="inline-flex items-baseline">
<slot name="icon" /> {@render icon?.()}
<slot name="title" /> {@render title?.()}
</span> </span>
<button <button
class="btn btn-circle btn-ghost btn-sm" class="btn btn-circle btn-ghost btn-sm"
on:click={() => { onclick={() => {
open = !open; open = !open;
}} }}
> >
@@ -35,7 +48,7 @@
class="flex flex-col gap-2 p-4 pt-0" class="flex flex-col gap-2 p-4 pt-0"
transition:slide|local={{ duration: 300, easing: cubicOut }} transition:slide|local={{ duration: 300, easing: cubicOut }}
> >
<slot /> {@render children?.()}
</div> </div>
{/if} {/if}
</div> </div>
@@ -45,12 +58,12 @@
> >
<div class="min-h-16 w-full p-4 text-xl font-medium"> <div class="min-h-16 w-full p-4 text-xl font-medium">
<span class="inline-flex items-baseline"> <span class="inline-flex items-baseline">
<slot name="icon" /> {@render icon?.()}
<slot name="title" /> {@render title?.()}
</span> </span>
</div> </div>
<div class="flex flex-col gap-2 p-4 pt-0"> <div class="flex flex-col gap-2 p-4 pt-0">
<slot /> {@render children?.()}
</div> </div>
</div> </div>
{/if} {/if}
+1 -1
View File
@@ -2,7 +2,7 @@
import { onDestroy } from 'svelte'; import { onDestroy } from 'svelte';
import { location } from '$lib/stores'; import { location } from '$lib/stores';
let source = `${$location}/api/camera/stream`; let source = $state(`${$location}/api/camera/stream`);
onDestroy(() => (source = '#')); onDestroy(() => (source = '#'));
</script> </script>
+7 -6
View File
@@ -4,30 +4,31 @@
import { notifications } from '$lib/components/toasts/notifications'; import { notifications } from '$lib/components/toasts/notifications';
import { error, info, success, warning } from './icons'; import { error, info, success, warning } from './icons';
export let theme = {
/** @type {{theme?: any, icon?: any}} */
let { theme = {
error: 'alert-error', error: 'alert-error',
success: 'alert-success', success: 'alert-success',
warning: 'alert-warning', warning: 'alert-warning',
info: 'alert-info' info: 'alert-info'
}; }, icon = {
export let icon = {
error: error, error: error,
success: success, success: success,
warning: warning, warning: warning,
info: info info: info
}; } } = $props();
</script> </script>
<div class="toast toast-end mr-4"> <div class="toast toast-end mr-4">
{#each $notifications as notification (notification.id)} {#each $notifications as notification (notification.id)}
{@const SvelteComponent = icon[notification.type]}
<div <div
animate:flip={{ duration: 400 }} animate:flip={{ duration: 400 }}
class="alert animate-none {theme[notification.type]}" class="alert animate-none {theme[notification.type]}"
in:fly={{ y: 100, duration: 400 }} in:fly={{ y: 100, duration: 400 }}
out:fly={{ x: 100, duration: 400 }} out:fly={{ x: 100, duration: 400 }}
> >
<svelte:component this={icon[notification.type]} class="h-6 w-6 flex-shrink-0" /> <SvelteComponent class="h-6 w-6 flex-shrink-0" />
<span>{notification.message}</span> <span>{notification.message}</span>
</div> </div>
{/each} {/each}
+18 -8
View File
@@ -41,14 +41,24 @@
import type { URDFRobot } from 'urdf-loader'; import type { URDFRobot } from 'urdf-loader';
import { get } from 'svelte/store'; import { get } from 'svelte/store';
export let sky = true; interface Props {
export let orbit = false; sky?: boolean;
export let panel = true; orbit?: boolean;
export let debug = false; panel?: boolean;
export let ground = true; debug?: boolean;
ground?: boolean;
}
let sceneManager = new SceneBuilder(); let {
let canvas: HTMLCanvasElement; sky = true,
orbit = false,
panel = true,
debug = false,
ground = true
}: Props = $props();
let sceneManager = $state(new SceneBuilder());
let canvas: HTMLCanvasElement = $state();
let currentModelAngles: number[] = new Array(12).fill(0); let currentModelAngles: number[] = new Array(12).fill(0);
let modelTargetAngles: number[] = new Array(12).fill(0); let modelTargetAngles: number[] = new Array(12).fill(0);
@@ -332,6 +342,6 @@
}; };
</script> </script>
<svelte:window on:resize={sceneManager.fillParent} /> <svelte:window onresize={sceneManager.fillParent} />
<canvas bind:this={canvas}></canvas> <canvas bind:this={canvas}></canvas>
@@ -1,11 +1,15 @@
<script lang="ts"> <script lang="ts">
import { MdiEyeOffOutline, MdiEyeOutline } from "../icons"; import { MdiEyeOffOutline, MdiEyeOutline } from "../icons";
export let show = false; interface Props {
export let value = ''; show?: boolean;
export let id = ''; value?: string;
id?: string;
}
let { show = $bindable(false), value = $bindable(''), id = '' }: Props = $props();
$: type = show ? 'text' : 'password'; let type = $derived(show ? 'text' : 'password');
const handleInput = (e: any) => value = e.target.value const handleInput = (e: any) => value = e.target.value
@@ -13,9 +17,9 @@
</script> </script>
<label class="input input-bordered flex items-center gap-2"> <label class="input input-bordered flex items-center gap-2">
<input {type} class="grow" {value} on:input={handleInput} {id} /> <input {type} class="grow" {value} oninput={handleInput} {id} />
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y_click_events_have_key_events -->
<div on:click={togglePassword} role="button" tabindex="0"> <div onclick={togglePassword} role="button" tabindex="0">
<MdiEyeOffOutline class="text-base-content/50 h-6 {show ? 'block' : 'hidden'}" /> <MdiEyeOffOutline class="text-base-content/50 h-6 {show ? 'block' : 'hidden'}" />
<MdiEyeOutline class="text-base-content/50 h-6 {show ? 'hidden' : 'block'}" /> <MdiEyeOutline class="text-base-content/50 h-6 {show ? 'hidden' : 'block'}" />
</div> </div>
@@ -1,8 +1,20 @@
<script lang="ts"> <script lang="ts">
export let min = 0; import { createBubbler } from 'svelte/legacy';
export let max = 100;
export let step = 1; const bubble = createBubbler();
export let value = (max - min) / 2; interface Props {
min?: number;
max?: number;
step?: number;
value?: any;
}
let {
min = 0,
max = 100,
step = 1,
value = $bindable((max - min) / 2)
}: Props = $props();
</script> </script>
<input <input
@@ -13,8 +25,8 @@
max={max} max={max}
step={step} step={step}
bind:value bind:value
on:input oninput={bubble('input')}
on:change onchange={bubble('change')}
/> />
<style> <style>
+6 -1
View File
@@ -1,6 +1,11 @@
<script lang="ts"> <script lang="ts">
interface Props {
children?: import('svelte').Snippet;
}
let { children }: Props = $props();
</script> </script>
<div class="box-border overflow-hidden flex-1"> <div class="box-border overflow-hidden flex-1">
<slot></slot> {@render children?.()}
</div> </div>
@@ -1,8 +1,13 @@
<script lang="ts"> <script lang="ts">
import WidgetContainer from './WidgetContainer.svelte';
import { WidgetComponents, type WidgetContainerConfig, isWidgetConfig } from '$lib/stores/application'; import { WidgetComponents, type WidgetContainerConfig, isWidgetConfig } from '$lib/stores/application';
import Widget from './Widget.svelte'; import Widget from './Widget.svelte';
export let container: WidgetContainerConfig; interface Props {
container: WidgetContainerConfig;
}
let { container }: Props = $props();
</script> </script>
<div class="w-full h-full flex flex-col overflow-hidden"> <div class="w-full h-full flex flex-col overflow-hidden">
@@ -15,9 +20,10 @@
{#each container.widgets as widget, index (widget.id + '-' + index)} {#each container.widgets as widget, index (widget.id + '-' + index)}
<Widget> <Widget>
{#if isWidgetConfig(widget)} {#if isWidgetConfig(widget)}
<svelte:component this={WidgetComponents[widget.component]} {...widget.props} /> {@const SvelteComponent = WidgetComponents[widget.component]}
<SvelteComponent {...widget.props} />
{:else if widget.widgets} {:else if widget.widgets}
<svelte:self container={widget} /> <WidgetContainer container={widget} />
{/if} {/if}
</Widget> </Widget>
{#if index !== container.widgets.length - 1} {#if index !== container.widgets.length - 1}
@@ -1,7 +1,11 @@
<script lang="ts"> <script lang="ts">
import { Github } from "../icons"; import { Github } from "../icons";
export let github; interface Props {
github: any;
}
let { github }: Props = $props();
</script> </script>
{#if github.active} {#if github.active}
@@ -1,7 +1,8 @@
<script> <script>
import logo from '$lib/assets/logo512.png'; import logo from '$lib/assets/logo512.png';
export let appName; /** @type {{appName: any}} */
let { appName } = $props();
</script> </script>
<a <a
+105 -98
View File
@@ -1,4 +1,6 @@
<script lang="ts"> <script lang="ts">
import { run } from 'svelte/legacy';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
import { useFeatureFlags } from '$lib/stores/featureFlags'; import { useFeatureFlags } from '$lib/stores/featureFlags';
@@ -42,102 +44,105 @@
submenu?: menuItem[]; submenu?: menuItem[];
}; };
$: menuItems = [ let menuItems = $state();
{ run(() => {
title: 'Connection', menuItems = [
icon: WiFi, {
href: '/connection', title: 'Connection',
feature: !appEnv.VITE_USE_HOST_NAME icon: WiFi,
}, href: '/connection',
{ feature: !appEnv.VITE_USE_HOST_NAME
title: 'Controller', },
icon: MdiController, {
href: '/controller', title: 'Controller',
feature: true icon: MdiController,
}, href: '/controller',
{ feature: true
title: 'Peripherals', },
icon: Devices, {
feature: true, title: 'Peripherals',
submenu: [ icon: Devices,
{ feature: true,
title: 'I2C', submenu: [
icon: Connection, {
href: '/peripherals/i2c', title: 'I2C',
feature: true icon: Connection,
}, href: '/peripherals/i2c',
{ feature: true
title: 'Camera', },
icon: Camera, {
href: '/peripherals/camera', title: 'Camera',
feature: $features.camera icon: Camera,
}, href: '/peripherals/camera',
{ feature: $features.camera
title: 'Servo', },
icon: MotorOutline, {
href: '/peripherals/servo', title: 'Servo',
feature: true icon: MotorOutline,
}, href: '/peripherals/servo',
{ feature: true
title: 'IMU', },
icon: Rotate3d, {
href: '/peripherals/imu', title: 'IMU',
feature: $features.imu || $features.mag || $features.bmp icon: Rotate3d,
} href: '/peripherals/imu',
] feature: $features.imu || $features.mag || $features.bmp
}, }
{ ]
title: 'WiFi', },
icon: WiFi, {
feature: true, title: 'WiFi',
submenu: [ icon: WiFi,
{ feature: true,
title: 'WiFi Station', submenu: [
icon: Router, {
href: '/wifi/sta', title: 'WiFi Station',
feature: true icon: Router,
}, href: '/wifi/sta',
{ feature: true
title: 'Access Point', },
icon: AP, {
href: '/wifi/ap', title: 'Access Point',
feature: true icon: AP,
} href: '/wifi/ap',
] feature: true
}, }
{ ]
title: 'System', },
icon: Settings, {
feature: true, title: 'System',
submenu: [ icon: Settings,
{ feature: true,
title: 'System Status', submenu: [
icon: Health, {
href: '/system/status', title: 'System Status',
feature: true icon: Health,
}, href: '/system/status',
{ feature: true
title: 'File System', },
icon: Folder, {
href: '/system/filesystem', title: 'File System',
feature: true icon: Folder,
}, href: '/system/filesystem',
{ feature: true
title: 'System Metrics', },
icon: Metrics, {
href: '/system/metrics', title: 'System Metrics',
feature: $features.analytics icon: Metrics,
}, href: '/system/metrics',
{ feature: $features.analytics
title: 'Firmware Update', },
icon: Update, {
href: '/system/update', title: 'Firmware Update',
feature: icon: Update,
$features.ota || $features.upload_firmware || $features.download_firmware href: '/system/update',
} feature:
] $features.ota || $features.upload_firmware || $features.download_firmware
} }
] as menuItem[]; ]
}
] as menuItem[];
});
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
@@ -152,7 +157,9 @@
dispatch('menuClicked'); dispatch('menuClicked');
} }
$: setActiveMenuItem($page.data.title); run(() => {
setActiveMenuItem($page.data.title);
});
const updateMenu = (event: any) => { const updateMenu = (event: any) => {
setActiveMenuItem(event.details); setActiveMenuItem(event.details);
@@ -164,7 +171,7 @@
<MenuList {menuItems} on:select{updateMenu} class="flex-grow flex-nowrap overflow-y-auto" /> <MenuList {menuItems} on:select{updateMenu} class="flex-grow flex-nowrap overflow-y-auto" />
<div class="divider my-0" /> <div class="divider my-0"></div>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<GithubButton {github} /> <GithubButton {github} />
+11 -16
View File
@@ -1,38 +1,33 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from 'svelte'; import MenuList from './MenuList.svelte';
type MenuItem = {
const dispatch = createEventDispatcher();
type menuItem = {
title: string; title: string;
icon: ConstructorOfATypedSvelteComponent; icon: ConstructorOfATypedSvelteComponent;
href?: string; href?: string;
feature: boolean; feature: boolean;
active?: boolean; active?: boolean;
submenu?: menuItem[]; submenu?: MenuItem[];
}; };
export let menuItems: menuItem[]; let { level, menuItems, select, class: klass } = $props();
export let level = 0;
const selectMenuItem = (title: string) => { const selectMenuItem = (title: string) => {
dispatch('select', title); select(title);
}; };
</script> </script>
<ul class={$$props.class + ' menu'}> <ul class={klass + ' menu'}>
{#each menuItems as menuItem, i (menuItem.title)} {#each menuItems as MenuItem[] as menuItem, i (menuItem.title)}
{#if menuItem.feature} {#if menuItem.feature}
<li> <li>
{#if menuItem.submenu} {#if menuItem.submenu}
<details open={menuItem.submenu.some(subItem => subItem.active)}> <details open={menuItem.submenu.some(subItem => subItem.active)}>
<summary class="text-lg font-bold"> <summary class="text-lg font-bold">
<svelte:component this={menuItem.icon} class="h-6 w-6" /> <menuItem.icon class="h-6 w-6" />
{menuItem.title} {menuItem.title}
</summary> </summary>
<div class="pl-4"> <div class="pl-4">
<svelte:self menuItems={menuItem.submenu} level={level + 1} /> <MenuList menuItems={menuItem.submenu} level={level + 1} />
</div> </div>
</details> </details>
{:else} {:else}
@@ -42,9 +37,9 @@
class:bg-base-100={menuItem.active} class:bg-base-100={menuItem.active}
class:text-lg={level === 0} class:text-lg={level === 0}
class:text-md={level === 1} class:text-md={level === 1}
on:click={() => selectMenuItem(menuItem.title)} onclick={() => selectMenuItem(menuItem.title)}
> >
<svelte:component this={menuItem.icon} class="h-6 w-6" /> <menuItem.icon class="h-6 w-6" />
{menuItem.title} {menuItem.title}
</a> </a>
{/if} {/if}
@@ -5,7 +5,11 @@
const features = useFeatureFlags(); const features = useFeatureFlags();
export let battery:Battery; interface Props {
battery: Battery;
}
let { battery }: Props = $props();
const getBatteryIcon = () => { const getBatteryIcon = () => {
if (battery.voltage === 0) return BatteryCharging; if (battery.voltage === 0) return BatteryCharging;
@@ -18,9 +22,9 @@
</script> </script>
{#if $features.battery} {#if $features.battery}
{@const SvelteComponent = getBatteryIcon()}
<div class="tooltip tooltip-left z-10" data-tip="{battery.voltage}V {Math.floor(battery.current*10)/10} mA"> <div class="tooltip tooltip-left z-10" data-tip="{battery.voltage}V {Math.floor(battery.current*10)/10} mA">
<svelte:component <SvelteComponent
this={getBatteryIcon()}
class="h-7 w-7 -rotate-90 {battery.voltage === 0 || battery.voltage <= 7.6 ? 'animate-pulse' : ''} {battery.voltage <= 7.6 ? 'text-error' : ''}" class="h-7 w-7 -rotate-90 {battery.voltage === 0 || battery.voltage <= 7.6 ? 'animate-pulse' : ''} {battery.voltage <= 7.6 ? 'text-error' : ''}"
/> />
</div> </div>
@@ -1,8 +1,10 @@
<script lang="ts"> <script lang="ts">
import { isFullscreen, toggleFullscreen } from '$lib/stores'; import { isFullscreen, toggleFullscreen } from '$lib/stores';
import { MdiFullscreenExit, MdiFullscreen } from '../icons'; import { MdiFullscreenExit, MdiFullscreen } from '../icons';
const SvelteComponent = $derived($isFullscreen ? MdiFullscreenExit : MdiFullscreen);
</script> </script>
<button on:click={toggleFullscreen}> <button onclick={toggleFullscreen}>
<svelte:component this={$isFullscreen ? MdiFullscreenExit : MdiFullscreen} class="h-7 w-7" /> <SvelteComponent class="h-7 w-7" />
</button> </button>
@@ -1,8 +1,12 @@
<script lang="ts"> <script lang="ts">
import { WiFi, WiFi0, WiFi1, WiFi2, WifiOff } from "../icons"; import { WiFi, WiFi0, WiFi1, WiFi2, WifiOff } from "../icons";
export let showDBm = false; interface Props {
export let rssi = 0; showDBm?: boolean;
rssi?: number;
}
let { showDBm = false, rssi = 0 }: Props = $props();
const getWiFiIcon = () => { const getWiFiIcon = () => {
if (rssi === 0) return WifiOff; if (rssi === 0) return WifiOff;
@@ -11,6 +15,8 @@
if (rssi >= -85) return WiFi1; if (rssi >= -85) return WiFi1;
return WiFi0; return WiFi0;
}; };
const SvelteComponent = $derived(getWiFiIcon());
</script> </script>
<div class="indicator"> <div class="indicator">
@@ -21,7 +27,7 @@
</span> </span>
{/if} {/if}
<div class="h-7 w-7"> <div class="h-7 w-7">
<svelte:component this={getWiFiIcon()} class="absolute inset-0 h-full w-full" /> <SvelteComponent class="absolute inset-0 h-full w-full" />
</div> </div>
</div> </div>
</div> </div>
@@ -27,7 +27,7 @@
{#if $features.sleep} {#if $features.sleep}
<div class="flex-none"> <div class="flex-none">
<button class="btn btn-square btn-ghost h-9 w-10" on:click={confirmSleep}> <button class="btn btn-square btn-ghost h-9 w-10" onclick={confirmSleep}>
<Power class="text-error h-9 w-9" /> <Power class="text-error h-9 w-9" />
</button> </button>
</div> </div>
@@ -7,4 +7,4 @@
</script> </script>
<button on:click={deactivate} class="bg-error text-white btn rounded-none">STOP</button> <button onclick={deactivate} class="bg-error text-white btn rounded-none">STOP</button>
@@ -5,7 +5,7 @@
<div class="topbar absolute left-0 top-0 w-full z-20 flex justify-between bg-zinc-800"> <div class="topbar absolute left-0 top-0 w-full z-20 flex justify-between bg-zinc-800">
<div class="flex gap-2 p-2"> <div class="flex gap-2 p-2">
<a href="/"> <a href="/">
<svelte:component this={Hamburger} class="h-8 w-8"/> <Hamburger class="h-8 w-8"/>
</a> </a>
</div> </div>
</div> </div>
@@ -14,10 +14,14 @@
const features = useFeatureFlags(); const features = useFeatureFlags();
export let update = false; interface Props {
update?: boolean;
}
let firmwareVersion: string; let { update = $bindable(false) }: Props = $props();
let firmwareDownloadLink: string;
let firmwareVersion: string = $state();
let firmwareDownloadLink: string = $state();
async function getGithubAPI() { async function getGithubAPI() {
const headers = { const headers = {
@@ -100,7 +104,7 @@
<div class="indicator flex-none"> <div class="indicator flex-none">
<button <button
class="btn btn-square btn-ghost h-9 w-9" class="btn btn-square btn-ghost h-9 w-9"
on:click={() => confirmGithubUpdate(firmwareDownloadLink)} onclick={() => confirmGithubUpdate(firmwareDownloadLink)}
> >
<span <span
class="indicator-item indicator-top indicator-center badge badge-info badge-xs top-2 scale-75 lg:top-1" class="indicator-item indicator-top indicator-center badge badge-info badge-xs top-2 scale-75 lg:top-1"
+7 -6
View File
@@ -4,30 +4,31 @@
import { notifications } from '$lib/components/toasts/notifications'; import { notifications } from '$lib/components/toasts/notifications';
import { error, info, success, warning } from '../icons'; import { error, info, success, warning } from '../icons';
export let theme = {
/** @type {{theme?: any, icon?: any}} */
let { theme = {
error: 'alert-error', error: 'alert-error',
success: 'alert-success', success: 'alert-success',
warning: 'alert-warning', warning: 'alert-warning',
info: 'alert-info' info: 'alert-info'
}; }, icon = {
export let icon = {
error: error, error: error,
success: success, success: success,
warning: warning, warning: warning,
info: info info: info
}; } } = $props();
</script> </script>
<div class="toast toast-end mr-4 z-20"> <div class="toast toast-end mr-4 z-20">
{#each $notifications as notification (notification.id)} {#each $notifications as notification (notification.id)}
{@const SvelteComponent = icon[notification.type]}
<div <div
animate:flip={{ duration: 400 }} animate:flip={{ duration: 400 }}
class="alert animate-none {theme[notification.type]}" class="alert animate-none {theme[notification.type]}"
in:fly={{ y: 100, duration: 400 }} in:fly={{ y: 100, duration: 400 }}
out:fly={{ x: 100, duration: 400 }} out:fly={{ x: 100, duration: 400 }}
> >
<svelte:component this={icon[notification.type]} class="h-6 w-6 flex-shrink-0" /> <SvelteComponent class="h-6 w-6 flex-shrink-0" />
<span>{notification.message}</span> <span>{notification.message}</span>
</div> </div>
{/each} {/each}
@@ -5,12 +5,16 @@
import { cubicOut } from "svelte/easing"; import { cubicOut } from "svelte/easing";
import { slide } from "svelte/transition"; import { slide } from "svelte/transition";
let chartElement: HTMLCanvasElement; let chartElement: HTMLCanvasElement = $state();
let chart: Chart; let chart: Chart;
export let label interface Props {
export let data:number[] label: any;
export let title data: number[];
title: any;
}
let { label, data, title }: Props = $props();
Chart.register(...registerables); Chart.register(...registerables);
@@ -94,6 +98,6 @@
class="flex w-full flex-col space-y-1 h-60" class="flex w-full flex-col space-y-1 h-60"
transition:slide|local={{ duration: 300, easing: cubicOut }} transition:slide|local={{ duration: 300, easing: cubicOut }}
> >
<canvas bind:this={chartElement} /> <canvas bind:this={chartElement}></canvas>
</div> </div>
</div> </div>
+12 -4
View File
@@ -1,12 +1,20 @@
<script lang="ts"> <script lang="ts">
export let options: string[] = []; import { createBubbler } from 'svelte/legacy';
export let selectedOption: string = '';
const bubble = createBubbler();
interface Props {
options?: string[];
selectedOption?: string;
[key: string]: any
}
let { options = [], selectedOption = $bindable(''), ...rest }: Props = $props();
</script> </script>
<select <select
bind:value={selectedOption} bind:value={selectedOption}
on:change onchange={bubble('change')}
class="select select-bordered select-sm lg:select-md max-w-xs {$$restProps.class || ''}" class="select select-bordered select-sm lg:select-md max-w-xs {rest.class || ''}"
> >
{#each options as option} {#each options as option}
<option value={option}>{option}</option> <option value={option}>{option}</option>
+319 -319
View File
@@ -1,26 +1,26 @@
import { import {
Mesh, Mesh,
PerspectiveCamera, PerspectiveCamera,
PlaneGeometry, PlaneGeometry,
Scene, Scene,
WebGLRenderer, WebGLRenderer,
AmbientLight, AmbientLight,
DirectionalLight, DirectionalLight,
PCFSoftShadowMap, PCFSoftShadowMap,
GridHelper, type GridHelper,
ArrowHelper, ArrowHelper,
Vector3, Vector3,
FogExp2, FogExp2,
CanvasTexture, CanvasTexture,
type ColorRepresentation, type ColorRepresentation,
type WebGLRendererParameters, type WebGLRendererParameters,
MeshPhongMaterial, MeshPhongMaterial,
EquirectangularReflectionMapping, EquirectangularReflectionMapping,
ACESFilmicToneMapping, ACESFilmicToneMapping,
MathUtils, MathUtils,
Group, Group,
MeshBasicMaterial, MeshBasicMaterial,
RepeatWrapping RepeatWrapping
} from 'three'; } from 'three';
import { Sky } from 'three/addons/objects/Sky.js'; import { Sky } from 'three/addons/objects/Sky.js';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'; import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
@@ -33,347 +33,347 @@ import { sunCalculator } from './utilities/position-utilities';
export const addScene = () => new Scene(); export const addScene = () => new Scene();
interface position { interface position {
x?: number; x?: number;
y?: number; y?: number;
z?: number; z?: number;
} }
interface light { interface light {
color?: ColorRepresentation; color?: ColorRepresentation;
intensity?: number; intensity?: number;
} }
interface arrowOptions { interface arrowOptions {
origin: position; origin: position;
direction: position; direction: position;
length?: number; length?: number;
color?: ColorRepresentation; color?: ColorRepresentation;
} }
type directionalLight = position & light; type directionalLight = position & light;
export default class SceneBuilder { export default class SceneBuilder {
public scene: Scene; public scene: Scene;
public camera!: PerspectiveCamera; public camera!: PerspectiveCamera;
public ground!: Mesh; public ground!: Mesh;
public renderer!: WebGLRenderer; public renderer!: WebGLRenderer;
public orbit: OrbitControls; public orbit: OrbitControls;
public callback: Function | undefined; public callback: Function | undefined;
public gridHelper!: GridHelper; public gridHelper!: GridHelper;
public model!: URDFRobot; public model!: URDFRobot;
public liveStreamTexture!: CanvasTexture; public liveStreamTexture!: CanvasTexture;
private fog!: FogExp2; private fog!: FogExp2;
private isLoaded: boolean = false; private isLoaded: boolean = false;
public isDragging: boolean = false; public isDragging: boolean = false;
highlightMaterial: any; highlightMaterial: any;
sky!: Sky; sky!: Sky;
transformControl: TransformControls; transformControl: TransformControls;
public modelGroup!: Group; public modelGroup!: Group;
constructor() { constructor() {
this.scene = new Scene(); this.scene = new Scene();
if (this.scene.environment?.mapping) { if (this.scene.environment?.mapping) {
this.scene.environment.mapping = EquirectangularReflectionMapping; this.scene.environment.mapping = EquirectangularReflectionMapping;
} }
return this; return this;
} }
public addRenderer = (parameters?: WebGLRendererParameters) => { public addRenderer = (parameters?: WebGLRendererParameters) => {
this.renderer = new WebGLRenderer(parameters); this.renderer = new WebGLRenderer(parameters);
this.renderer.outputColorSpace = 'srgb'; this.renderer.outputColorSpace = 'srgb';
this.renderer.shadowMap.enabled = true; this.renderer.shadowMap.enabled = true;
this.renderer.shadowMap.type = PCFSoftShadowMap; this.renderer.shadowMap.type = PCFSoftShadowMap;
this.renderer.toneMapping = ACESFilmicToneMapping; this.renderer.toneMapping = ACESFilmicToneMapping;
this.renderer.toneMappingExposure = 0.85; this.renderer.toneMappingExposure = 0.85;
if (!parameters?.canvas) document.body.appendChild(this.renderer.domElement); if (!parameters?.canvas) document.body.appendChild(this.renderer.domElement);
return this; return this;
}; };
public addSky = () => { public addSky = () => {
this.sky = new Sky(); this.sky = new Sky();
this.sky.scale.setScalar(450000); this.sky.scale.setScalar(450000);
this.scene.add(this.sky); this.scene.add(this.sky);
const effectController = { const effectController = {
turbidity: 10, turbidity: 10,
rayleigh: 3, rayleigh: 3,
mieCoefficient: 0.005, mieCoefficient: 0.005,
mieDirectionalG: 0.7, mieDirectionalG: 0.7,
elevation: sunCalculator.calculateSunElevation(), elevation: sunCalculator.calculateSunElevation(),
azimuth: 200, azimuth: 200,
exposure: this.renderer.toneMappingExposure exposure: this.renderer.toneMappingExposure
}; };
const uniforms = this.sky.material.uniforms; const uniforms = this.sky.material.uniforms;
uniforms['turbidity'].value = effectController.turbidity; uniforms['turbidity'].value = effectController.turbidity;
uniforms['rayleigh'].value = effectController.rayleigh; uniforms['rayleigh'].value = effectController.rayleigh;
uniforms['mieCoefficient'].value = effectController.mieCoefficient; uniforms['mieCoefficient'].value = effectController.mieCoefficient;
uniforms['mieDirectionalG'].value = effectController.mieDirectionalG; uniforms['mieDirectionalG'].value = effectController.mieDirectionalG;
this.renderer.toneMappingExposure = 0.5; this.renderer.toneMappingExposure = 0.5;
const phi = MathUtils.degToRad(90 - effectController.elevation); const phi = MathUtils.degToRad(90 - effectController.elevation);
const theta = MathUtils.degToRad(effectController.azimuth); const theta = MathUtils.degToRad(effectController.azimuth);
const sun = new Vector3(); const sun = new Vector3();
sun.setFromSphericalCoords(1, phi, theta); sun.setFromSphericalCoords(1, phi, theta);
uniforms['sunPosition'].value.copy(sun); uniforms['sunPosition'].value.copy(sun);
return this; return this;
}; };
public addPerspectiveCamera = (options: position) => { public addPerspectiveCamera = (options: position) => {
this.camera = new PerspectiveCamera(); this.camera = new PerspectiveCamera();
this.camera.position.set(options.x ?? 0, options.y ?? 2.7, options.z ?? 0); this.camera.position.set(options.x ?? 0, options.y ?? 2.7, options.z ?? 0);
this.scene.add(this.camera); this.scene.add(this.camera);
return this; return this;
}; };
public addGroundPlane = (options?: position) => { public addGroundPlane = (options?: position) => {
const checkerboardTexture = this.createCheckerboardTexture(1024, 2); const checkerboardTexture = this.createCheckerboardTexture(1024, 2);
checkerboardTexture.wrapS = RepeatWrapping; checkerboardTexture.wrapS = RepeatWrapping;
checkerboardTexture.wrapT = RepeatWrapping; checkerboardTexture.wrapT = RepeatWrapping;
checkerboardTexture.repeat.set(100, 100); checkerboardTexture.repeat.set(100, 100);
const checkerboardMat = new MeshBasicMaterial({ const checkerboardMat = new MeshBasicMaterial({
map: checkerboardTexture, map: checkerboardTexture,
opacity: 0.1, opacity: 0.1,
transparent: true transparent: true
}); });
const plane = new PlaneGeometry(400, 400); const plane = new PlaneGeometry(400, 400);
this.ground = new Mesh(plane, checkerboardMat); this.ground = new Mesh(plane, checkerboardMat);
this.ground.rotation.x = -Math.PI / 2; this.ground.rotation.x = -Math.PI / 2;
this.ground.position.set(options?.x ?? 0, options?.y ?? 0.01, options?.z ?? 0); this.ground.position.set(options?.x ?? 0, options?.y ?? 0.01, options?.z ?? 0);
this.ground.receiveShadow = true; this.ground.receiveShadow = true;
this.scene.add(this.ground); this.scene.add(this.ground);
const mirror = new Reflector(plane, { const mirror = new Reflector(plane, {
clipBias: 0.003, clipBias: 0.003,
textureWidth: window.innerWidth * window.devicePixelRatio, textureWidth: window.innerWidth * window.devicePixelRatio,
textureHeight: window.innerHeight * window.devicePixelRatio, textureHeight: window.innerHeight * window.devicePixelRatio,
color: 0x00bfff color: 0x00bfff
}); });
mirror.rotateX(-Math.PI / 2); mirror.rotateX(-Math.PI / 2);
this.scene.add(mirror); this.scene.add(mirror);
return this; return this;
}; };
public addOrbitControls = (minDistance: number, maxDistance: number, autoRotate = true) => { public addOrbitControls = (minDistance: number, maxDistance: number, autoRotate = true) => {
this.orbit = new OrbitControls(this.camera, this.renderer.domElement); this.orbit = new OrbitControls(this.camera, this.renderer.domElement);
this.orbit.minDistance = minDistance; this.orbit.minDistance = minDistance;
this.orbit.maxDistance = maxDistance; this.orbit.maxDistance = maxDistance;
this.orbit.autoRotate = autoRotate; this.orbit.autoRotate = autoRotate;
this.orbit.update(); this.orbit.update();
return this; return this;
}; };
public addAmbientLight = (options: light) => { public addAmbientLight = (options: light) => {
const ambientLight = new AmbientLight(options.color, options.intensity); const ambientLight = new AmbientLight(options.color, options.intensity);
this.scene.add(ambientLight); this.scene.add(ambientLight);
return this; return this;
}; };
public addDirectionalLight = (options: directionalLight) => { public addDirectionalLight = (options: directionalLight) => {
const directionalLight = new DirectionalLight(options.color, options.intensity); const directionalLight = new DirectionalLight(options.color, options.intensity);
directionalLight.castShadow = true; directionalLight.castShadow = true;
directionalLight.shadow.camera.top = 10; directionalLight.shadow.camera.top = 10;
directionalLight.shadow.camera.bottom = -10; directionalLight.shadow.camera.bottom = -10;
directionalLight.shadow.camera.right = 10; directionalLight.shadow.camera.right = 10;
directionalLight.shadow.camera.left = -10; directionalLight.shadow.camera.left = -10;
directionalLight.shadow.mapSize.set(4096, 4096); directionalLight.shadow.mapSize.set(4096, 4096);
directionalLight.position.set(options.x ?? 0, options.y ?? 0, options.z ?? 0); directionalLight.position.set(options.x ?? 0, options.y ?? 0, options.z ?? 0);
this.scene.add(directionalLight); this.scene.add(directionalLight);
return this; return this;
}; };
private createCheckerboardTexture = (size: number, squares: number) => { private createCheckerboardTexture = (size: number, squares: number) => {
const canvas = document.createElement('canvas'); const canvas = document.createElement('canvas');
canvas.width = size; canvas.width = size;
canvas.height = size; canvas.height = size;
const context = canvas.getContext('2d'); const context = canvas.getContext('2d');
const squareSize = size / squares; const squareSize = size / squares;
for (let y = 0; y < squares; y++) { for (let y = 0; y < squares; y++) {
for (let x = 0; x < squares; x++) { for (let x = 0; x < squares; x++) {
context!.fillStyle = (x + y) % 2 === 0 ? '#ffffff' : '#000000'; context!.fillStyle = (x + y) % 2 === 0 ? '#ffffff' : '#000000';
context!.fillRect(x * squareSize, y * squareSize, squareSize, squareSize); context!.fillRect(x * squareSize, y * squareSize, squareSize, squareSize);
} }
} }
const texture = new CanvasTexture(canvas); const texture = new CanvasTexture(canvas);
texture.wrapS = texture.wrapT = RepeatWrapping; texture.wrapS = texture.wrapT = RepeatWrapping;
texture.anisotropy = 16; texture.anisotropy = 16;
return texture; return texture;
}; };
public addFogExp2 = (color: ColorRepresentation, density?: number) => { public addFogExp2 = (color: ColorRepresentation, density?: number) => {
this.scene.fog = new FogExp2(color, density); this.scene.fog = new FogExp2(color, density);
return this; return this;
}; };
public fillParent = () => { public fillParent = () => {
const parentElement = this.renderer.domElement.parentElement; const parentElement = this.renderer.domElement.parentElement;
if (parentElement) { if (parentElement) {
const width = parentElement.clientWidth; const width = parentElement.clientWidth;
const height = parentElement.clientHeight; const height = parentElement.clientHeight;
this.handleResize(width, height); this.handleResize(width, height);
} }
return this; return this;
}; };
public handleResize = (width = window.innerWidth, height = window.innerHeight) => { public handleResize = (width = window.innerWidth, height = window.innerHeight) => {
this.renderer.setSize(width, height); this.renderer.setSize(width, height);
this.renderer.setPixelRatio(window.devicePixelRatio); this.renderer.setPixelRatio(window.devicePixelRatio);
this.camera.aspect = width / height; this.camera.aspect = width / height;
this.camera.updateProjectionMatrix(); this.camera.updateProjectionMatrix();
return this; return this;
}; };
public addRenderCb = (callback: Function) => { public addRenderCb = (callback: Function) => {
this.callback = callback; this.callback = callback;
return this; return this;
}; };
public startRenderLoop = () => { public startRenderLoop = () => {
this.renderer.setAnimationLoop(() => { this.renderer.setAnimationLoop(() => {
this.renderer.render(this.scene, this.camera); this.renderer.render(this.scene, this.camera);
this.orbit.update(); this.orbit.update();
this.handleRobotShadow(); this.handleRobotShadow();
if (this.callback) this.callback(); if (this.callback) this.callback();
if (!this.liveStreamTexture) return; if (!this.liveStreamTexture) return;
}); });
return this; return this;
}; };
public addArrowHelper = (options?: arrowOptions) => { public addArrowHelper = (options?: arrowOptions) => {
const dir = new Vector3( const dir = new Vector3(
options?.direction.x ?? 0, options?.direction.x ?? 0,
options?.direction.y ?? 0, options?.direction.y ?? 0,
options?.direction.z ?? 0 options?.direction.z ?? 0
); );
const origin = new Vector3( const origin = new Vector3(
options?.origin.x ?? 0, options?.origin.x ?? 0,
options?.origin.y ?? 0, options?.origin.y ?? 0,
options?.origin.z ?? 0 options?.origin.z ?? 0
); );
const arrowHelper = new ArrowHelper( const arrowHelper = new ArrowHelper(
dir, dir,
origin, origin,
options?.length ?? 1.5, options?.length ?? 1.5,
options?.color ?? 0xff0000 options?.color ?? 0xff0000
); );
this.scene.add(arrowHelper); this.scene.add(arrowHelper);
return this; return this;
}; };
private setJointValue(jointName: string, angle: number) { private setJointValue(jointName: string, angle: number) {
if (!this.model) return; if (!this.model) return;
if (!this.model.joints[jointName]) return; if (!this.model.joints[jointName]) return;
this.model.joints[jointName].setJointValue(angle); this.model.joints[jointName].setJointValue(angle);
} }
isJoint = (j: URDFJoint) => j.isURDFJoint && j.jointType !== 'fixed'; isJoint = (j: URDFJoint) => j.isURDFJoint && j.jointType !== 'fixed';
highlightLinkGeometry = (m: URDFMimicJoint, revert: boolean, material: MeshPhongMaterial) => { highlightLinkGeometry = (m: URDFMimicJoint, revert: boolean, material: MeshPhongMaterial) => {
const traverse = (c: any) => { const traverse = (c: any) => {
if (c.type === 'Mesh') { if (c.type === 'Mesh') {
if (revert) { if (revert) {
c.material = c.__origMaterial; c.material = c.__origMaterial;
delete c.__origMaterial; delete c.__origMaterial;
} else { } else {
c.__origMaterial = c.material; c.__origMaterial = c.material;
c.material = material; c.material = material;
} }
} }
if (c === m || !this.isJoint(c)) { if (c === m || !this.isJoint(c)) {
for (let i = 0; i < c.children.length; i++) { for (let i = 0; i < c.children.length; i++) {
const child = c.children[i]; const child = c.children[i];
if (!child.isURDFCollider) { if (!child.isURDFCollider) {
traverse(c.children[i]); traverse(c.children[i]);
} }
} }
} }
}; };
traverse(m); traverse(m);
}; };
public addTransformControls = (model: any) => { public addTransformControls = (model: any) => {
this.transformControl = new TransformControls(this.camera, this.renderer.domElement); this.transformControl = new TransformControls(this.camera, this.renderer.domElement);
this.transformControl.addEventListener('dragging-changed', (event: any) => { this.transformControl.addEventListener('dragging-changed', (event: any) => {
this.orbit.enabled = !event.value; this.orbit.enabled = !event.value;
this.isDragging = !event.value; this.isDragging = !event.value;
}); });
this.transformControl.attach(model); this.transformControl.attach(model);
this.scene.add(this.transformControl); this.scene.add(this.transformControl);
this.transformControl.setMode('rotate'); this.transformControl.setMode('rotate');
return this; return this;
}; };
public addModel = (model: any) => { public addModel = (model: any) => {
this.modelGroup = new Group(); this.modelGroup = new Group();
this.modelGroup.add(model); this.modelGroup.add(model);
this.model = model; this.model = model;
this.scene.add(this.modelGroup); this.scene.add(this.modelGroup);
return this; return this;
}; };
public addDragControl = (updateAngle: any) => { public addDragControl = (updateAngle: any) => {
const highlightColor = '#FFFFFF'; const highlightColor = '#FFFFFF';
const highlightMaterial = new MeshPhongMaterial({ const highlightMaterial = new MeshPhongMaterial({
shininess: 10, shininess: 10,
color: highlightColor, color: highlightColor,
emissive: highlightColor, emissive: highlightColor,
emissiveIntensity: 0.9 emissiveIntensity: 0.9
}); });
const dragControls = new PointerURDFDragControls( const dragControls = new PointerURDFDragControls(
this.scene, this.scene,
this.camera, this.camera,
this.renderer.domElement this.renderer.domElement
); );
dragControls.updateJoint = (joint: URDFMimicJoint, angle: number) => { dragControls.updateJoint = (joint: URDFMimicJoint, angle: number) => {
this.setJointValue(joint.name, angle); this.setJointValue(joint.name, angle);
updateAngle(joint.name, angle); updateAngle(joint.name, angle);
}; };
dragControls.onDragStart = () => { dragControls.onDragStart = () => {
this.orbit.enabled = false; this.orbit.enabled = false;
this.isDragging = true; this.isDragging = true;
}; };
dragControls.onDragEnd = () => { dragControls.onDragEnd = () => {
this.orbit.enabled = true; this.orbit.enabled = true;
this.isDragging = false; this.isDragging = false;
}; };
dragControls.onHover = (joint: URDFMimicJoint) => dragControls.onHover = (joint: URDFMimicJoint) =>
this.highlightLinkGeometry(joint, false, highlightMaterial); this.highlightLinkGeometry(joint, false, highlightMaterial);
dragControls.onUnhover = (joint: URDFMimicJoint) => dragControls.onUnhover = (joint: URDFMimicJoint) =>
this.highlightLinkGeometry(joint, true, highlightMaterial); this.highlightLinkGeometry(joint, true, highlightMaterial);
this.renderer.domElement.addEventListener( this.renderer.domElement.addEventListener(
'touchstart', 'touchstart',
(data) => dragControls._mouseDown(data.touches[0]), data => dragControls._mouseDown(data.touches[0]),
{ passive: true } { passive: true }
); );
this.renderer.domElement.addEventListener( this.renderer.domElement.addEventListener(
'touchmove', 'touchmove',
(data) => dragControls._mouseMove(data.touches[0]), data => dragControls._mouseMove(data.touches[0]),
{ passive: true } { passive: true }
); );
this.renderer.domElement.addEventListener( this.renderer.domElement.addEventListener(
'touchend', 'touchend',
(data) => dragControls._mouseUp(data.touches[0]), data => dragControls._mouseUp(data.touches[0]),
{ passive: true } { passive: true }
); );
return this; return this;
}; };
public toggleFog = () => { public toggleFog = () => {
this.scene.fog = this.scene.fog ? null : this.fog; this.scene.fog = this.scene.fog ? null : this.fog;
}; };
private handleRobotShadow = () => { private handleRobotShadow = () => {
if (this.isLoaded) return; if (this.isLoaded) return;
const intervalId = setInterval(() => this.model?.traverse((c) => (c.castShadow = true)), 10); const intervalId = setInterval(() => this.model?.traverse(c => (c.castShadow = true)), 10);
setTimeout(() => clearInterval(intervalId), 1000); setTimeout(() => clearInterval(intervalId), 1000);
this.isLoaded = true; this.isLoaded = true;
}; };
} }
+17 -11
View File
@@ -22,6 +22,11 @@
useFeatureFlags useFeatureFlags
} from '$lib/stores'; } from '$lib/stores';
import type { Analytics, DownloadOTA } from '$lib/types/models'; import type { Analytics, DownloadOTA } from '$lib/types/models';
interface Props {
children?: import('svelte').Snippet;
}
let { children }: Props = $props();
const features = useFeatureFlags(); const features = useFeatureFlags();
@@ -82,7 +87,7 @@
const handleOAT = (data: DownloadOTA) => telemetry.setDownloadOTA(data); const handleOAT = (data: DownloadOTA) => telemetry.setDownloadOTA(data);
let menuOpen = false; let menuOpen = $state(false);
</script> </script>
<svelte:head> <svelte:head>
@@ -96,24 +101,25 @@
<Statusbar /> <Statusbar />
<!-- Main page content here --> <!-- Main page content here -->
<slot /> {@render children?.()}
</div> </div>
<!-- Side Navigation --> <!-- Side Navigation -->
<div class="drawer-side z-30 shadow-lg"> <div class="drawer-side z-30 shadow-lg">
<label for="main-menu" class="drawer-overlay" /> <label for="main-menu" class="drawer-overlay"></label>
<Menu on:menuClicked={() => (menuOpen = false)} /> <Menu on:menuClicked={() => (menuOpen = false)} />
</div> </div>
</div> </div>
<Modals> <Modals>
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y-no-static-element-interactions --> <!-- svelte-ignore a11y_no_static_element_interactions -->
<div {#snippet backdrop()}
slot="backdrop" <div
class="fixed inset-0 z-40 max-h-full max-w-full bg-black/20 backdrop-blur" class="fixed inset-0 z-40 max-h-full max-w-full bg-black/20 backdrop-blur"
transition:fade transition:fade
on:click={closeModal} onclick={closeModal}
/> ></div>
{/snippet}
</Modals> </Modals>
<Toast /> <Toast />
+6 -2
View File
@@ -3,7 +3,11 @@
import { notifications } from '$lib/components/toasts/notifications'; import { notifications } from '$lib/components/toasts/notifications';
import Visualization from '$lib/components/Visualization.svelte'; import Visualization from '$lib/components/Visualization.svelte';
export let data: PageData; interface Props {
data: PageData;
}
let { data }: Props = $props();
</script> </script>
<div class="hero bg-base-100 h-screen"> <div class="hero bg-base-100 h-screen">
@@ -17,7 +21,7 @@
<a <a
class="btn btn-primary" class="btn btn-primary"
href="/controller" href="/controller"
on:click={() => notifications.success('You did it!', 1000)}>Begin</a onclick={() => notifications.success('You did it!', 1000)}>Begin</a
> >
</div> </div>
</div> </div>
+7 -3
View File
@@ -12,13 +12,17 @@
</script> </script>
<SettingsCard collapsible={false}> <SettingsCard collapsible={false}>
<WiFi slot="icon" class="lex-shrink-0 mr-2 h-6 w-6 self-end" /> {#snippet icon()}
<span slot="title">Connection</span> <WiFi class="lex-shrink-0 mr-2 h-6 w-6 self-end" />
{/snippet}
{#snippet title()}
<span >Connection</span>
{/snippet}
<div class="flex"> <div class="flex">
<label class="label w-32" for="server">Address:</label> <label class="label w-32" for="server">Address:</label>
<input class="input" bind:value={$location} /> <input class="input" bind:value={$location} />
</div> </div>
<button class="btn btn-primary" on:click={update}>Update</button> <button class="btn btn-primary" onclick={update}>Update</button>
</SettingsCard> </SettingsCard>
+1 -1
View File
@@ -7,7 +7,7 @@
import { imu } from '$lib/stores/imu'; import { imu } from '$lib/stores/imu';
import type { IMU } from '$lib/types/models'; import type { IMU } from '$lib/types/models';
$: layout = $views.find(v => v.name === $selectedView)!; let layout = $derived($views.find(v => v.name === $selectedView)!);
onMount(() => { onMount(() => {
socket.on('imu', (data: IMU) => { socket.on('imu', (data: IMU) => {
+7 -7
View File
@@ -86,9 +86,9 @@
<div class="absolute top-0 left-0 w-screen h-screen"> <div class="absolute top-0 left-0 w-screen h-screen">
<div class="absolute top-0 left-0 h-full w-full flex portrait:hidden"> <div class="absolute top-0 left-0 h-full w-full flex portrait:hidden">
<div id="left" class="flex w-60 items-center justify-end" /> <div id="left" class="flex w-60 items-center justify-end"></div>
<div class="flex-1" /> <div class="flex-1"></div>
<div id="right" class="flex w-60 items-center" /> <div id="right" class="flex w-60 items-center"></div>
</div> </div>
<div class="absolute bottom-0 right-0 p-4 z-10 gap-2 flex-col hidden lg:flex"> <div class="absolute bottom-0 right-0 p-4 z-10 gap-2 flex-col hidden lg:flex">
<div class="flex justify-center w-full"> <div class="flex justify-center w-full">
@@ -112,7 +112,7 @@
<button <button
class="btn join-item" class="btn join-item"
class:btn-primary={$mode === modes.indexOf(modeValue)} class:btn-primary={$mode === modes.indexOf(modeValue)}
on:click={() => changeMode(modeValue)} onclick={() => changeMode(modeValue)}
> >
{capitalize(modeValue)} {capitalize(modeValue)}
</button> </button>
@@ -128,13 +128,13 @@
name="s1" name="s1"
min="0" min="0"
max="100" max="100"
on:input={(e) => handleRange(e, 's1')} oninput={(e) => handleRange(e, 's1')}
class="range range-sm range-primary" class="range range-sm range-primary"
/> />
</div> </div>
<div> <div>
<label for="speed">Speed</label> <label for="speed">Speed</label>
<input type="range" name="speed" min="0" max="100" on:input={(e) => handleRange(e, 'speed')} class="range range-sm range-primary" /> <input type="range" name="speed" min="0" max="100" oninput={(e) => handleRange(e, 'speed')} class="range range-sm range-primary" />
</div> </div>
</div> </div>
{/if} {/if}
@@ -142,4 +142,4 @@
</div> </div>
</div> </div>
<svelte:window on:keyup={handleKeyup} on:keydown={handleKeyup} /> <svelte:window onkeyup={handleKeyup} onkeydown={handleKeyup} />
@@ -6,8 +6,12 @@
</script> </script>
<SettingsCard collapsible={false}> <SettingsCard collapsible={false}>
<Camera slot="icon" class="lex-shrink-0 mr-2 h-6 w-6 self-end" /> {#snippet icon()}
<span slot="title">Camera</span> <Camera class="lex-shrink-0 mr-2 h-6 w-6 self-end" />
{/snippet}
{#snippet title()}
<span >Camera</span>
{/snippet}
<Stream /> <Stream />
<CameraSetting /> <CameraSetting />
</SettingsCard> </SettingsCard>
@@ -2,7 +2,7 @@
import { api } from '$lib/api'; import { api } from '$lib/api';
import Spinner from '$lib/components/Spinner.svelte'; import Spinner from '$lib/components/Spinner.svelte';
import type { CameraSettings } from '$lib/types/models'; import type { CameraSettings } from '$lib/types/models';
let settings:CameraSettings let settings:CameraSettings = $state()
const getCameraSettings = async () => { const getCameraSettings = async () => {
const result = await api.get<CameraSettings>('/api/camera/settings') const result = await api.get<CameraSettings>('/api/camera/settings')
@@ -27,7 +27,7 @@
<Spinner /> <Spinner />
{:then _} {:then _}
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<button class="btn btn-primary" type="button" on:click={updateCameraSettings}>Update camera settings</button> <button class="btn btn-primary" type="button" onclick={updateCameraSettings}>Update camera settings</button>
<label for="brightness"> <label for="brightness">
Brightness {settings.brightness} Brightness {settings.brightness}
+7 -3
View File
@@ -17,7 +17,7 @@
{ address: 119, part_number: 'BMP085', name: 'Temp/Barometric' } { address: 119, part_number: 'BMP085', name: 'Temp/Barometric' }
]; ];
let active_devices: I2CDevice[] = []; let active_devices: I2CDevice[] = $state([]);
onMount(() => { onMount(() => {
socket.on('i2cScan', handleScan); socket.on('i2cScan', handleScan);
@@ -38,8 +38,12 @@
</script> </script>
<SettingsCard collapsible={false}> <SettingsCard collapsible={false}>
<Connection slot="icon" class="lex-shrink-0 mr-2 h-6 w-6 self-end" /> {#snippet icon()}
<span slot="title">I<sup>2</sup>C</span> <Connection class="lex-shrink-0 mr-2 h-6 w-6 self-end" />
{/snippet}
{#snippet title()}
<span >I<sup>2</sup>C</span>
{/snippet}
<div class="grid"> <div class="grid">
{#if active_devices.length === 0} {#if active_devices.length === 0}
+12 -8
View File
@@ -15,13 +15,13 @@
Chart.register(...registerables); Chart.register(...registerables);
let angleChartElement: HTMLCanvasElement; let angleChartElement: HTMLCanvasElement = $state();
let angleChart: Chart; let angleChart: Chart;
let tempChartElement: HTMLCanvasElement; let tempChartElement: HTMLCanvasElement = $state();
let tempChart: Chart; let tempChart: Chart;
let altitudeChartElement: HTMLCanvasElement; let altitudeChartElement: HTMLCanvasElement = $state();
let altitudeChart: Chart; let altitudeChart: Chart;
const handleImu = (data: IMU) => { const handleImu = (data: IMU) => {
@@ -272,15 +272,19 @@
</script> </script>
<SettingsCard collapsible={false}> <SettingsCard collapsible={false}>
<Rotate3d slot="icon" class="lex-shrink-0 mr-2 h-6 w-6 self-end" /> {#snippet icon()}
<span slot="title">IMU</span> <Rotate3d class="lex-shrink-0 mr-2 h-6 w-6 self-end" />
{/snippet}
{#snippet title()}
<span >IMU</span>
{/snippet}
{#if $features.imu} {#if $features.imu}
<div class="w-full overflow-x-auto"> <div class="w-full overflow-x-auto">
<div <div
class="flex w-full flex-col space-y-1 h-60" class="flex w-full flex-col space-y-1 h-60"
transition:slide|local={{ duration: 300, easing: cubicOut }} transition:slide|local={{ duration: 300, easing: cubicOut }}
> >
<canvas bind:this={angleChartElement} /> <canvas bind:this={angleChartElement}></canvas>
</div> </div>
</div> </div>
{/if} {/if}
@@ -290,7 +294,7 @@
class="flex w-full flex-col space-y-1 h-60" class="flex w-full flex-col space-y-1 h-60"
transition:slide|local={{ duration: 300, easing: cubicOut }} transition:slide|local={{ duration: 300, easing: cubicOut }}
> >
<canvas bind:this={tempChartElement} /> <canvas bind:this={tempChartElement}></canvas>
</div> </div>
</div> </div>
<div class="w-full overflow-x-auto"> <div class="w-full overflow-x-auto">
@@ -298,7 +302,7 @@
class="flex w-full flex-col space-y-1 h-60" class="flex w-full flex-col space-y-1 h-60"
transition:slide|local={{ duration: 300, easing: cubicOut }} transition:slide|local={{ duration: 300, easing: cubicOut }}
> >
<canvas bind:this={altitudeChartElement} /> <canvas bind:this={altitudeChartElement}></canvas>
</div> </div>
</div> </div>
{/if} {/if}
@@ -1,9 +1,13 @@
<script lang="ts"> <script lang="ts">
import { api } from '$lib/api'; import { api } from '$lib/api';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
export let data = { interface Props {
data?: any;
}
let { data = $bindable({
servos: [] servos: []
}; }) }: Props = $props();
const updateValue = (event, index, key) => { const updateValue = (event, index, key) => {
data.servos[index][key] = event.target.innerText; data.servos[index][key] = event.target.innerText;
@@ -36,29 +40,29 @@
<tr> <tr>
<td <td
contenteditable="true" contenteditable="true"
on:blur={syncConfig} onblur={syncConfig}
on:input={event => updateValue(event, index, 'center_pwm')} oninput={event => updateValue(event, index, 'center_pwm')}
> >
{servo.center_pwm} {servo.center_pwm}
</td> </td>
<td <td
contenteditable="true" contenteditable="true"
on:blur={syncConfig} onblur={syncConfig}
on:input={event => updateValue(event, index, 'center_angle')} oninput={event => updateValue(event, index, 'center_angle')}
> >
{servo.center_angle} {servo.center_angle}
</td> </td>
<td <td
contenteditable="true" contenteditable="true"
on:blur={syncConfig} onblur={syncConfig}
on:input={event => updateValue(event, index, 'direction')} oninput={event => updateValue(event, index, 'direction')}
> >
{servo.direction} {servo.direction}
</td> </td>
<td <td
contenteditable="true" contenteditable="true"
on:blur={syncConfig} onblur={syncConfig}
on:input={event => updateValue(event, index, 'conversion')} oninput={event => updateValue(event, index, 'conversion')}
> >
{servo.conversion} {servo.conversion}
</td> </td>
+11 -7
View File
@@ -10,9 +10,9 @@
let isLoading = false; let isLoading = false;
let active = false; let active = $state(false);
let servoId = 0; let servoId = $state(0);
const throttler = new Throttler(); const throttler = new Throttler();
@@ -29,7 +29,7 @@
socket.sendEvent('servoState', { active: 0 }); socket.sendEvent('servoState', { active: 0 });
}; };
let pwm = 306; let pwm = $state(306);
const updatePWM = () => { const updatePWM = () => {
throttler.throttle(() => { throttler.throttle(() => {
@@ -39,15 +39,19 @@
</script> </script>
<SettingsCard collapsible={false}> <SettingsCard collapsible={false}>
<MotorOutline slot="icon" class="lex-shrink-0 mr-2 h-6 w-6 self-end" /> {#snippet icon()}
<span slot="title">Servo</span> <MotorOutline class="lex-shrink-0 mr-2 h-6 w-6 self-end" />
{/snippet}
{#snippet title()}
<span >Servo</span>
{/snippet}
{pwm} {pwm}
<input <input
type="range" type="range"
min="80" min="80"
max="600" max="600"
bind:value={pwm} bind:value={pwm}
on:input={updatePWM} oninput={updatePWM}
class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
/> />
@@ -63,7 +67,7 @@
type="checkbox" type="checkbox"
class="toggle" class="toggle"
bind:checked={active} bind:checked={active}
on:change={active ? activateServo : deactivateServo} onchange={active ? activateServo : deactivateServo}
/> />
</span> </span>
</div> </div>
+5 -4
View File
@@ -2,7 +2,8 @@
import { FileIcon } from '$lib/components/icons'; import { FileIcon } from '$lib/components/icons';
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
export let name; /** @type {{name: any}} */
let { name } = $props();
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
@@ -11,8 +12,8 @@
} }
</script> </script>
<!-- svelte-ignore a11y-interactive-supports-focus --> <!-- svelte-ignore a11y_interactive_supports_focus -->
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y_click_events_have_key_events -->
<span role="button" class="flex pl-4 gap-2 items-center" on:click={updateSelected}> <span role="button" class="flex pl-4 gap-2 items-center" onclick={updateSelected}>
<FileIcon/>{name} <FileIcon/>{name}
</span> </span>
@@ -6,7 +6,7 @@
import type { Directory } from "$lib/types/models"; import type { Directory } from "$lib/types/models";
import { FolderIcon } from "$lib/components/icons"; import { FolderIcon } from "$lib/components/icons";
let filename = ''; let filename = $state('');
const getFiles = async () => { const getFiles = async () => {
const result = await api.get<Directory>('/api/files') const result = await api.get<Directory>('/api/files')
@@ -38,8 +38,12 @@
} }
</script> </script>
<SettingsCard collapsible={false}> <SettingsCard collapsible={false}>
<FolderIcon slot="icon" class="lex-shrink-0 mr-2 h-6 w-6 self-end" /> {#snippet icon()}
<span slot="title">File System</span> <FolderIcon class="lex-shrink-0 mr-2 h-6 w-6 self-end" />
{/snippet}
{#snippet title()}
<span >File System</span>
{/snippet}
<div class="w-full overflow-x-auto"> <div class="w-full overflow-x-auto">
{#await getFiles()} {#await getFiles()}
<Spinner /> <Spinner />
+10 -5
View File
@@ -1,11 +1,16 @@
<script lang="ts"> <script lang="ts">
import Folder from './Folder.svelte';
import File from './File.svelte'; import File from './File.svelte';
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
import { FolderIcon, FolderOpenOutline } from '$lib/components/icons'; import { FolderIcon, FolderOpenOutline } from '$lib/components/icons';
export let expanded = false; interface Props {
export let name; expanded?: boolean;
export let files; name: any;
files: any;
}
let { expanded = $bindable(false), name, files }: Props = $props();
function toggle() { function toggle() {
expanded = !expanded; expanded = !expanded;
@@ -18,7 +23,7 @@
} }
</script> </script>
<button class="flex pl-2" on:click={toggle}> <button class="flex pl-2" onclick={toggle}>
{#if expanded} {#if expanded}
<FolderOpenOutline class="w-6 h-6" /> <FolderOpenOutline class="w-6 h-6" />
{:else} {:else}
@@ -32,7 +37,7 @@
{#each Object.entries(files) as [name, content]} {#each Object.entries(files) as [name, content]}
<li class="p-1"> <li class="p-1">
{#if typeof content == 'object'} {#if typeof content == 'object'}
<svelte:self {name} files={content} on:selected={updateSelected} /> <Folder {name} files={content} on:selected={updateSelected} />
{:else} {:else}
<File {name} on:selected={updateSelected}/> <File {name} on:selected={updateSelected}/>
{/if} {/if}
@@ -12,16 +12,16 @@
Chart.register(...registerables); Chart.register(...registerables);
let cpuChartElement: HTMLCanvasElement; let cpuChartElement: HTMLCanvasElement = $state();
let cpuChart: Chart; let cpuChart: Chart;
let heapChartElement: HTMLCanvasElement; let heapChartElement: HTMLCanvasElement = $state();
let heapChart: Chart; let heapChart: Chart;
let filesystemChartElement: HTMLCanvasElement; let filesystemChartElement: HTMLCanvasElement = $state();
let filesystemChart: Chart; let filesystemChart: Chart;
let temperatureChartElement: HTMLCanvasElement; let temperatureChartElement: HTMLCanvasElement = $state();
let temperatureChart: Chart; let temperatureChart: Chart;
onMount(() => { onMount(() => {
@@ -330,15 +330,19 @@
</script> </script>
<SettingsCard collapsible={false}> <SettingsCard collapsible={false}>
<Metrics slot="icon" class="lex-shrink-0 mr-2 h-6 w-6 self-end" /> {#snippet icon()}
<span slot="title">System Metrics</span> <Metrics class="lex-shrink-0 mr-2 h-6 w-6 self-end" />
{/snippet}
{#snippet title()}
<span >System Metrics</span>
{/snippet}
<div class="w-full overflow-x-auto"> <div class="w-full overflow-x-auto">
<div <div
class="flex w-full flex-col space-y-1 h-60" class="flex w-full flex-col space-y-1 h-60"
transition:slide|local={{ duration: 300, easing: cubicOut }} transition:slide|local={{ duration: 300, easing: cubicOut }}
> >
<canvas bind:this={cpuChartElement} /> <canvas bind:this={cpuChartElement}></canvas>
</div> </div>
</div> </div>
@@ -347,7 +351,7 @@
class="flex w-full flex-col space-y-1 h-60" class="flex w-full flex-col space-y-1 h-60"
transition:slide|local={{ duration: 300, easing: cubicOut }} transition:slide|local={{ duration: 300, easing: cubicOut }}
> >
<canvas bind:this={heapChartElement} /> <canvas bind:this={heapChartElement}></canvas>
</div> </div>
</div> </div>
<div class="w-full overflow-x-auto"> <div class="w-full overflow-x-auto">
@@ -355,7 +359,7 @@
class="flex w-full flex-col space-y-1 h-52" class="flex w-full flex-col space-y-1 h-52"
transition:slide|local={{ duration: 300, easing: cubicOut }} transition:slide|local={{ duration: 300, easing: cubicOut }}
> >
<canvas bind:this={filesystemChartElement} /> <canvas bind:this={filesystemChartElement}></canvas>
</div> </div>
</div> </div>
<div class="w-full overflow-x-auto"> <div class="w-full overflow-x-auto">
@@ -363,7 +367,7 @@
class="flex w-full flex-col space-y-1 h-52" class="flex w-full flex-col space-y-1 h-52"
transition:slide|local={{ duration: 300, easing: cubicOut }} transition:slide|local={{ duration: 300, easing: cubicOut }}
> >
<canvas bind:this={temperatureChartElement} /> <canvas bind:this={temperatureChartElement}></canvas>
</div> </div>
</div> </div>
</SettingsCard> </SettingsCard>
@@ -34,7 +34,7 @@
const features = useFeatureFlags(); const features = useFeatureFlags();
let systemInformation: SystemInformation; let systemInformation: SystemInformation = $state();
async function getSystemStatus() { async function getSystemStatus() {
const result = await api.get<SystemInformation>('/api/system/status'); const result = await api.get<SystemInformation>('/api/system/status');
@@ -106,8 +106,12 @@
</script> </script>
<SettingsCard collapsible={false}> <SettingsCard collapsible={false}>
<Health slot="icon" class="lex-shrink-0 mr-2 h-6 w-6 self-end" /> {#snippet icon()}
<span slot="title">System Status</span> <Health class="lex-shrink-0 mr-2 h-6 w-6 self-end" />
{/snippet}
{#snippet title()}
<span >System Status</span>
{/snippet}
<div class="w-full overflow-x-auto"> <div class="w-full overflow-x-auto">
{#await getSystemStatus()} {#await getSystemStatus()}
@@ -309,14 +313,14 @@
<div class="mt-4 flex flex-wrap justify-end gap-2"> <div class="mt-4 flex flex-wrap justify-end gap-2">
{#if $features.sleep} {#if $features.sleep}
<button class="btn btn-primary inline-flex items-center" on:click={confirmSleep}> <button class="btn btn-primary inline-flex items-center" onclick={confirmSleep}>
<Sleep class="mr-2 h-5 w-5" /><span>Sleep</span> <Sleep class="mr-2 h-5 w-5" /><span>Sleep</span>
</button> </button>
{/if} {/if}
<button class="btn btn-primary inline-flex items-center" on:click={confirmRestart}> <button class="btn btn-primary inline-flex items-center" onclick={confirmRestart}>
<Power class="mr-2 h-5 w-5" /><span>Restart</span> <Power class="mr-2 h-5 w-5" /><span>Restart</span>
</button> </button>
<button class="btn btn-secondary inline-flex items-center" on:click={confirmReset}> <button class="btn btn-secondary inline-flex items-center" onclick={confirmReset}>
<FactoryReset class="mr-2 h-5 w-5" /><span>Factory Reset</span> <FactoryReset class="mr-2 h-5 w-5" /><span>Factory Reset</span>
</button> </button>
</div> </div>
@@ -81,8 +81,12 @@
</script> </script>
<SettingsCard collapsible={false}> <SettingsCard collapsible={false}>
<Github slot="icon" class="lex-shrink-0 mr-2 h-6 w-6 self-end rounded-full" /> {#snippet icon()}
<span slot="title">Github Firmware Manager</span> <Github class="lex-shrink-0 mr-2 h-6 w-6 self-end rounded-full" />
{/snippet}
{#snippet title()}
<span >Github Firmware Manager</span>
{/snippet}
{#await getGithubAPI()} {#await getGithubAPI()}
<Spinner /> <Spinner />
{:then githubReleases} {:then githubReleases}
@@ -136,7 +140,7 @@
{#if compareVersions($features.firmware_version, release.tag_name) != 0} {#if compareVersions($features.firmware_version, release.tag_name) != 0}
<button <button
class="btn btn-ghost btn-circle btn-sm" class="btn btn-ghost btn-circle btn-sm"
on:click={() => { onclick={() => {
confirmGithubUpdate(release.assets); confirmGithubUpdate(release.assets);
}} }}
> >
@@ -6,7 +6,7 @@
import { api } from '$lib/api'; import { api } from '$lib/api';
import { Cancel, OTA, Warning } from '$lib/components/icons'; import { Cancel, OTA, Warning } from '$lib/components/icons';
let files: FileList; let files: FileList = $state();
async function uploadBIN() { async function uploadBIN() {
const formData = new FormData(); const formData = new FormData();
@@ -32,8 +32,12 @@
</script> </script>
<SettingsCard collapsible={false}> <SettingsCard collapsible={false}>
<OTA slot="icon" class="lex-shrink-0 mr-2 h-6 w-6 self-end rounded-full" /> {#snippet icon()}
<span slot="title">Upload Firmware</span> <OTA class="lex-shrink-0 mr-2 h-6 w-6 self-end rounded-full" />
{/snippet}
{#snippet title()}
<span >Upload Firmware</span>
{/snippet}
<div class="alert alert-warning shadow-lg"> <div class="alert alert-warning shadow-lg">
<Warning class="h-6 w-6 flex-shrink-0" /> <Warning class="h-6 w-6 flex-shrink-0" />
<span <span
@@ -48,6 +52,6 @@
class="file-input file-input-bordered file-input-secondary mt-4 w-full" class="file-input file-input-bordered file-input-secondary mt-4 w-full"
bind:files bind:files
accept=".bin,.md5" accept=".bin,.md5"
on:change={confirmBinUpload} onchange={confirmBinUpload}
/> />
</SettingsCard> </SettingsCard>
+14 -8
View File
@@ -1,4 +1,6 @@
<script lang="ts"> <script lang="ts">
import { preventDefault } from 'svelte/legacy';
import { onMount, onDestroy } from 'svelte'; import { onMount, onDestroy } from 'svelte';
import { slide } from 'svelte/transition'; import { slide } from 'svelte/transition';
import { cubicOut } from 'svelte/easing'; import { cubicOut } from 'svelte/easing';
@@ -13,10 +15,10 @@
const features = useFeatureFlags(); const features = useFeatureFlags();
let apSettings: ApSettings; let apSettings: ApSettings = $state();
let apStatus: ApStatus; let apStatus: ApStatus = $state();
let formField: any; let formField: any = $state();
async function getAPStatus() { async function getAPStatus() {
const result = await api.get<ApStatus>('/api/wifi/ap/status'); const result = await api.get<ApStatus>('/api/wifi/ap/status');
@@ -67,14 +69,14 @@
{ bg_color: 'bg-warning', text_color: 'text-warning-content', description: 'Lingering' } { bg_color: 'bg-warning', text_color: 'text-warning-content', description: 'Lingering' }
]; ];
let formErrors = { let formErrors = $state({
ssid: false, ssid: false,
channel: false, channel: false,
max_clients: false, max_clients: false,
local_ip: false, local_ip: false,
gateway_ip: false, gateway_ip: false,
subnet_mask: false subnet_mask: false
}; });
async function postAPSettings(data: ApSettings) { async function postAPSettings(data: ApSettings) {
const result = await api.post<ApSettings>('/api/wifi/ap/settings', data); const result = await api.post<ApSettings>('/api/wifi/ap/settings', data);
@@ -152,8 +154,12 @@
</script> </script>
<SettingsCard collapsible={false}> <SettingsCard collapsible={false}>
<AP slot="icon" class="lex-shrink-0 mr-2 h-6 w-6 self-end" /> {#snippet icon()}
<span slot="title">Access Point</span> <AP class="lex-shrink-0 mr-2 h-6 w-6 self-end" />
{/snippet}
{#snippet title()}
<span >Access Point</span>
{/snippet}
<div class="w-full overflow-x-auto"> <div class="w-full overflow-x-auto">
{#await getAPStatus()} {#await getAPStatus()}
<Spinner /> <Spinner />
@@ -234,7 +240,7 @@
> >
<form <form
class="grid w-full grid-cols-1 content-center gap-x-4 p-0s sm:grid-cols-2" class="grid w-full grid-cols-1 content-center gap-x-4 p-0s sm:grid-cols-2"
on:submit|preventDefault={handleSubmitAP} onsubmit={preventDefault(handleSubmitAP)}
novalidate novalidate
bind:this={formField} bind:this={formField}
> >
+21 -14
View File
@@ -1,4 +1,7 @@
<script lang="ts"> <script lang="ts">
import { createBubbler } from 'svelte/legacy';
const bubble = createBubbler();
import { closeModal } from 'svelte-modals'; import { closeModal } from 'svelte-modals';
import { focusTrap } from 'svelte-focus-trap'; import { focusTrap } from 'svelte-focus-trap';
import { fly } from 'svelte/transition'; import { fly } from 'svelte/transition';
@@ -10,8 +13,12 @@
import { AP, Network, Reload, Cancel } from '$lib/components/icons'; import { AP, Network, Reload, Cancel } from '$lib/components/icons';
// provided by <Modals /> // provided by <Modals />
export let isOpen: boolean; interface Props {
export let storeNetwork: any; isOpen: boolean;
storeNetwork: any;
}
let { isOpen, storeNetwork }: Props = $props();
const encryptionType = [ const encryptionType = [
'Open', 'Open',
@@ -25,9 +32,9 @@
'WAPI PSK' 'WAPI PSK'
]; ];
let listOfNetworks: NetworkItem[] = []; let listOfNetworks: NetworkItem[] = $state([]);
let scanActive = false; let scanActive = $state(false);
let pollingId: number; let pollingId: number;
@@ -73,15 +80,15 @@
role="dialog" role="dialog"
class="pointer-events-none fixed inset-0 z-50 flex items-center justify-center" class="pointer-events-none fixed inset-0 z-50 flex items-center justify-center"
transition:fly={{ y: 50 }} transition:fly={{ y: 50 }}
on:introstart onintrostart={bubble('introstart')}
on:outroend onoutroend={bubble('outroend')}
use:focusTrap use:focusTrap
> >
<div <div
class="bg-base-100 rounded-box pointer-events-auto flex max-h-full min-w-fit max-w-md flex-col justify-between p-4 shadow-lg" class="bg-base-100 rounded-box pointer-events-auto flex max-h-full min-w-fit max-w-md flex-col justify-between p-4 shadow-lg"
> >
<h2 class="text-base-content text-start text-2xl font-bold">Scan Networks</h2> <h2 class="text-base-content text-start text-2xl font-bold">Scan Networks</h2>
<div class="divider my-2" /> <div class="divider my-2"></div>
<div class="overflow-y-auto"> <div class="overflow-y-auto">
{#if scanActive}<div {#if scanActive}<div
class="bg-base-100 flex flex-col items-center justify-center p-6" class="bg-base-100 flex flex-col items-center justify-center p-6"
@@ -93,10 +100,10 @@
<ul class="menu"> <ul class="menu">
{#each listOfNetworks as network, i} {#each listOfNetworks as network, i}
<li> <li>
<!-- svelte-ignore a11y-click-events-have-key-events --> <!-- svelte-ignore a11y_click_events_have_key_events -->
<div <div
class="bg-base-200 rounded-btn my-1 flex items-center space-x-3 hover:scale-[1.02] active:scale-[0.98]" class="bg-base-200 rounded-btn my-1 flex items-center space-x-3 hover:scale-[1.02] active:scale-[0.98]"
on:click={() => { onclick={() => {
storeNetwork(network.ssid); storeNetwork(network.ssid);
}} }}
role="button" role="button"
@@ -114,7 +121,7 @@
Channel: {network.channel} Channel: {network.channel}
</div> </div>
</div> </div>
<div class="flex-grow" /> <div class="flex-grow"></div>
<RssiIndicator showDBm={true} rssi={network.rssi} /> <RssiIndicator showDBm={true} rssi={network.rssi} />
</div> </div>
</li> </li>
@@ -122,19 +129,19 @@
</ul> </ul>
{/if} {/if}
</div> </div>
<div class="divider my-2" /> <div class="divider my-2"></div>
<div class="flex flex-wrap justify-end gap-2"> <div class="flex flex-wrap justify-end gap-2">
<button <button
class="btn btn-primary inline-flex flex-none items-center" class="btn btn-primary inline-flex flex-none items-center"
disabled={scanActive} disabled={scanActive}
on:click={scanNetworks} onclick={scanNetworks}
><Reload class="mr-2 h-5 w-5" /><span>Scan again</span></button ><Reload class="mr-2 h-5 w-5" /><span>Scan again</span></button
> >
<div class="flex-grow" /> <div class="flex-grow"></div>
<button <button
class="btn btn-warning text-warning-content inline-flex flex-none items-center" class="btn btn-warning text-warning-content inline-flex flex-none items-center"
on:click={closeModal}><Cancel class="mr-2 h-5 w-5" /><span>Cancel</span></button onclick={closeModal}><Cancel class="mr-2 h-5 w-5" /><span>Cancel</span></button
> >
</div> </div>
</div> </div>
+61 -60
View File
@@ -1,5 +1,3 @@
<svelte:options immutable={true} />
<script lang="ts"> <script lang="ts">
import { onMount, onDestroy } from 'svelte'; import { onMount, onDestroy } from 'svelte';
import { openModal, closeModal } from 'svelte-modals'; import { openModal, closeModal } from 'svelte-modals';
@@ -38,7 +36,7 @@
const features = useFeatureFlags(); const features = useFeatureFlags();
let networkEditable: KnownNetworkItem = { let networkEditable: KnownNetworkItem = $state({
ssid: '', ssid: '',
password: '', password: '',
static_ip_config: false, static_ip_config: false,
@@ -47,32 +45,32 @@
gateway_ip: undefined, gateway_ip: undefined,
dns_ip_1: undefined, dns_ip_1: undefined,
dns_ip_2: undefined dns_ip_2: undefined
}; });
let static_ip_config = false; let static_ip_config = $state(false);
let newNetwork: boolean = true; let newNetwork: boolean = $state(true);
let showNetworkEditor: boolean = false; let showNetworkEditor: boolean = $state(false);
let wifiStatus: WifiStatus; let wifiStatus: WifiStatus = $state();
let wifiSettings: WifiSettings; let wifiSettings: WifiSettings = $state();
let dndNetworkList: KnownNetworkItem[] = []; let dndNetworkList: KnownNetworkItem[] = $state([]);
let showWifiDetails = false; let showWifiDetails = $state(false);
let formField: any; let formField: any = $state();
let formErrors = { let formErrors = $state({
ssid: false, ssid: false,
local_ip: false, local_ip: false,
gateway_ip: false, gateway_ip: false,
subnet_mask: false, subnet_mask: false,
dns_1: false, dns_1: false,
dns_2: false dns_2: false
}; });
let formErrorhostname = false; let formErrorhostname = $state(false);
async function getWifiStatus() { async function getWifiStatus() {
const result = await api.get<WifiStatus>('/api/wifi/sta/status'); const result = await api.get<WifiStatus>('/api/wifi/sta/status');
@@ -128,7 +126,8 @@
} }
} }
function validateWiFiForm() { function validateWiFiForm(event: SubmitEvent) {
event.preventDefault();
let valid = true; let valid = true;
// Validate SSID // Validate SSID
@@ -286,8 +285,12 @@
</script> </script>
<SettingsCard collapsible={false}> <SettingsCard collapsible={false}>
<Router slot="icon" class="lex-shrink-0 mr-2 h-6 w-6 self-end" /> {#snippet icon()}
<span slot="title">WiFi Connection</span> <Router class="lex-shrink-0 mr-2 h-6 w-6 self-end" />
{/snippet}
{#snippet title()}
<span>WiFi Connection</span>
{/snippet}
<div class="w-full overflow-x-auto"> <div class="w-full overflow-x-auto">
{#await getWifiStatus()} {#await getWifiStatus()}
<Spinner /> <Spinner />
@@ -350,10 +353,10 @@
{wifiStatus.rssi} dBm {wifiStatus.rssi} dBm
</div> </div>
</div> </div>
<div class="grow" /> <div class="grow"></div>
<button <button
class="btn btn-circle btn-ghost btn-sm modal-button" class="btn btn-circle btn-ghost btn-sm modal-button"
on:click={() => { onclick={() => {
showWifiDetails = !showWifiDetails; showWifiDetails = !showWifiDetails;
}} }}
> >
@@ -451,7 +454,7 @@
<div class="relative w-full overflow-visible"> <div class="relative w-full overflow-visible">
<button <button
class="btn btn-primary text-primary-content btn-md absolute -top-14 right-16" class="btn btn-primary text-primary-content btn-md absolute -top-14 right-16"
on:click={() => { onclick={() => {
if (checkNetworkList()) { if (checkNetworkList()) {
addNetwork(); addNetwork();
showNetworkEditor = true; showNetworkEditor = true;
@@ -462,7 +465,7 @@
> >
<button <button
class="btn btn-primary text-primary-content btn-md absolute -top-14 right-0" class="btn btn-primary text-primary-content btn-md absolute -top-14 right-0"
on:click={() => { onclick={() => {
if (checkNetworkList()) { if (checkNetworkList()) {
scanForNetworks(); scanForNetworks();
showNetworkEditor = true; showNetworkEditor = true;
@@ -482,51 +485,49 @@
itemSize={60} itemSize={60}
itemCount={dndNetworkList.length} itemCount={dndNetworkList.length}
on:drop={onDrop} on:drop={onDrop}
let:index
> >
<!-- svelte-ignore a11y-click-events-have-key-events --> {#snippet children({ index })}
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2"> <!-- svelte-ignore a11y_click_events_have_key_events -->
<div class="mask mask-hexagon bg-primary h-auto w-10 shrink-0"> <div
<Router class="text-primary-content h-auto w-full scale-75" /> class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2"
>
<div class="mask mask-hexagon bg-primary h-auto w-10 shrink-0">
<Router class="text-primary-content h-auto w-full scale-75" />
</div>
<div>
<div class="font-bold">{dndNetworkList[index].ssid}</div>
</div>
<div class="flex-grow"></div>
<div class="space-x-0 px-0 mx-0">
<button
class="btn btn-ghost btn-sm"
onclick={() => {
handleEdit(index);
}}
>
<Edit class="h-6 w-6" /></button
>
<button
class="btn btn-ghost btn-sm"
onclick={() => {
confirmDelete(index);
}}
>
<Delete class="text-error h-6 w-6" />
</button>
</div>
</div> </div>
<div> {/snippet}
<div class="font-bold">{dndNetworkList[index].ssid}</div>
</div>
<div class="flex-grow" />
<div class="space-x-0 px-0 mx-0">
<button
class="btn btn-ghost btn-sm"
on:click={() => {
handleEdit(index);
}}
>
<Edit class="h-6 w-6" /></button
>
<button
class="btn btn-ghost btn-sm"
on:click={() => {
confirmDelete(index);
}}
>
<Delete class="text-error h-6 w-6" />
</button>
</div>
</div>
</DragDropList> </DragDropList>
</div> </div>
</div> </div>
<div class="divider mb-0" /> <div class="divider mb-0"></div>
<div <div
class="flex flex-col gap-2 p-0" class="flex flex-col gap-2 p-0"
transition:slide|local={{ duration: 300, easing: cubicOut }} transition:slide|local={{ duration: 300, easing: cubicOut }}
> >
<form <form class="" onsubmit={validateWiFiForm} novalidate bind:this={formField}>
class=""
on:submit|preventDefault={validateWiFiForm}
novalidate
bind:this={formField}
>
<div class="grid w-full grid-cols-1 content-center gap-x-4 px-4 sm:grid-cols-2"> <div class="grid w-full grid-cols-1 content-center gap-x-4 px-4 sm:grid-cols-2">
<div> <div>
<label class="label" for="channel"> <label class="label" for="channel">
@@ -566,7 +567,7 @@
</div> </div>
{#if showNetworkEditor} {#if showNetworkEditor}
<div class="divider my-0" /> <div class="divider my-0"></div>
<div <div
class="grid w-full grid-cols-1 content-center gap-x-4 px-4 sm:grid-cols-2" class="grid w-full grid-cols-1 content-center gap-x-4 px-4 sm:grid-cols-2"
transition:slide|local={{ duration: 300, easing: cubicOut }} transition:slide|local={{ duration: 300, easing: cubicOut }}
@@ -747,12 +748,12 @@
{/if} {/if}
{/if} {/if}
<div class="divider mb-2 mt-0" /> <div class="divider mb-2 mt-0"></div>
<div class="mx-4 flex flex-wrap justify-end gap-2"> <div class="mx-4 flex flex-wrap justify-end gap-2">
<button class="btn btn-primary" type="submit" disabled={!showNetworkEditor}> <button class="btn btn-primary" type="submit" disabled={!showNetworkEditor}>
{newNetwork ? 'Add Network' : 'Update Network'} {newNetwork ? 'Add Network' : 'Update Network'}
</button> </button>
<button class="btn btn-primary" type="button" on:click={validateHostName}> <button class="btn btn-primary" type="button" onclick={validateHostName}>
Apply Settings Apply Settings
</button> </button>
</div> </div>
+19 -18
View File
@@ -1,20 +1,21 @@
{ {
"extends": "./.svelte-kit/tsconfig.json", "extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": { "compilerOptions": {
"allowJs": true, "allowJs": true,
"checkJs": true, "checkJs": true,
"esModuleInterop": true, "esModuleInterop": true,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"resolveJsonModule": true, "resolveJsonModule": true,
"skipLibCheck": true, "skipLibCheck": true,
"sourceMap": true, "sourceMap": true,
"strict": true, "strict": true,
"moduleResolution": "bundler", "moduleResolution": "bundler",
"types": ["unplugin-icons/types/svelte"] "module": "preserve",
} "types": ["unplugin-icons/types/svelte"]
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias }
// except $lib which is handled by https://kit.svelte.dev/docs/configuration#files // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
// // except $lib which is handled by https://kit.svelte.dev/docs/configuration#files
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes //
// from the referenced tsconfig.json - TypeScript does not merge them in // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
// from the referenced tsconfig.json - TypeScript does not merge them in
} }