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