🌌 Migrate app to svelte-5
This commit is contained in:
+8
-8
@@ -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",
|
||||||
|
|||||||
Generated
+509
-548
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||||
|
|||||||
@@ -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();
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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} />
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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
@@ -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;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 />
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 />
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user