Deletes old project
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
<script lang="ts">
|
||||
import Battery0 from '~icons/tabler/battery';
|
||||
import Battery25 from '~icons/tabler/battery-1';
|
||||
import Battery50 from '~icons/tabler/battery-2';
|
||||
import Battery75 from '~icons/tabler/battery-3';
|
||||
import Battery100 from '~icons/tabler/battery-4';
|
||||
import BatteryCharging from '~icons/tabler/battery-charging-2';
|
||||
|
||||
export let charging = false;
|
||||
export let soc = 100;
|
||||
</script>
|
||||
|
||||
<div class="tooltip tooltip-bottom" data-tip="{soc} %">
|
||||
{#if charging}
|
||||
<BatteryCharging class="{$$props.class || ''} -rotate-90 animate-pulse" />
|
||||
{:else if soc > 75}
|
||||
<Battery100 class="{$$props.class || ''} -rotate-90" />
|
||||
{:else if soc > 55}
|
||||
<Battery75 class="{$$props.class || ''} -rotate-90" />
|
||||
{:else if soc > 30}
|
||||
<Battery50 class="{$$props.class || ''} -rotate-90" />
|
||||
{:else if soc > 5}
|
||||
<Battery25 class="{$$props.class || ''} -rotate-90" />
|
||||
{:else}
|
||||
<Battery0 class="{$$props.class || ''} text-error -rotate-90 animate-pulse" />
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,43 @@
|
||||
<script lang="ts">
|
||||
import { slide } from 'svelte/transition';
|
||||
import { cubicOut } from 'svelte/easing';
|
||||
import Down from '~icons/tabler/chevron-down';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
function openCollapsible() {
|
||||
open = !open;
|
||||
if (open) {
|
||||
dispatch('opened');
|
||||
} else {
|
||||
dispatch('closed');
|
||||
}
|
||||
}
|
||||
|
||||
export let open = false;
|
||||
</script>
|
||||
|
||||
<div class="{$$props.class || ''} relative grid w-full max-w-2xl self-center overflow-hidden">
|
||||
<div class="min-h-16 flex w-full items-center justify-between space-x-3 p-4 text-xl font-medium">
|
||||
<span class="inline-flex items-baseline">
|
||||
<slot name="icon" />
|
||||
<slot name="title" />
|
||||
</span>
|
||||
<button class="btn btn-circle btn-ghost btn-sm" on:click={() => openCollapsible()}>
|
||||
<Down
|
||||
class="text-base-content h-auto w-6 transition-transform duration-300 ease-in-out {open
|
||||
? 'rotate-180'
|
||||
: ''}"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
{#if open}
|
||||
<div
|
||||
class="flex flex-col gap-2 p-4 pt-0"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,52 @@
|
||||
<script lang="ts">
|
||||
import { closeModal } from 'svelte-modals';
|
||||
import { focusTrap } from 'svelte-focus-trap';
|
||||
import { fly } from 'svelte/transition';
|
||||
import Cancel from '~icons/tabler/x';
|
||||
import Check from '~icons/tabler/check';
|
||||
|
||||
// provided by <Modals />
|
||||
export let isOpen: boolean;
|
||||
|
||||
export let title: string;
|
||||
export let message: string;
|
||||
export let onConfirm: any;
|
||||
export let labels = {
|
||||
cancel: { label: 'Cancel', icon: Cancel },
|
||||
confirm: { label: 'OK', icon: Check }
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if isOpen}
|
||||
<div
|
||||
role="dialog"
|
||||
class="pointer-events-none fixed inset-0 z-50 flex items-center justify-center"
|
||||
transition:fly={{ y: 50 }}
|
||||
on:introstart
|
||||
on:outroend
|
||||
use:focusTrap
|
||||
>
|
||||
<div
|
||||
class="rounded-box bg-base-100 pointer-events-auto flex min-w-fit max-w-md flex-col justify-between p-4 shadow-lg"
|
||||
>
|
||||
<h2 class="text-base-content text-start text-2xl font-bold">{title}</h2>
|
||||
<div class="divider my-2" />
|
||||
<p class="text-base-content mb-1 text-start">{message}</p>
|
||||
<div class="divider my-2" />
|
||||
<div class="flex justify-end gap-2">
|
||||
<button class="btn btn-primary inline-flex items-center" on:click={closeModal}
|
||||
><svelte:component this={labels.cancel.icon} class="mr-2 h-5 w-5" /><span
|
||||
>{labels?.cancel.label}</span
|
||||
></button
|
||||
>
|
||||
<button
|
||||
class="btn btn-warning text-warning-content inline-flex items-center"
|
||||
on:click={onConfirm}
|
||||
><svelte:component this={labels?.confirm.icon} class="mr-2 h-5 w-5" /><span
|
||||
>{labels?.confirm.label}</span
|
||||
></button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -0,0 +1,92 @@
|
||||
<script lang="ts">
|
||||
import { closeAllModals, onBeforeClose } from 'svelte-modals';
|
||||
import { focusTrap } from 'svelte-focus-trap';
|
||||
import { fly } from 'svelte/transition';
|
||||
import { telemetry } from '$lib/stores/telemetry';
|
||||
import Cancel from '~icons/tabler/x';
|
||||
|
||||
// provided by <Modals />
|
||||
export let 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') {
|
||||
updating = false;
|
||||
}
|
||||
|
||||
let message = 'Preparing ...';
|
||||
let timerId: number;
|
||||
|
||||
$: if ($telemetry.download_ota.status == 'progress') {
|
||||
message = 'Downloading ...';
|
||||
} else if ($telemetry.download_ota.status == 'error') {
|
||||
message = $telemetry.download_ota.error;
|
||||
} else if ($telemetry.download_ota.status == 'finished') {
|
||||
message = 'Restarting ...';
|
||||
progress = 0;
|
||||
// Reload page after 5 sec
|
||||
timerId = setTimeout(() => {
|
||||
closeAllModals();
|
||||
location.reload();
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
onBeforeClose(() => {
|
||||
if (updating) {
|
||||
// prevents modal from closing
|
||||
return false;
|
||||
} else {
|
||||
$telemetry.download_ota.status = 'idle';
|
||||
$telemetry.download_ota.error = '';
|
||||
$telemetry.download_ota.progress = 0;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if isOpen}
|
||||
<div
|
||||
role="dialog"
|
||||
class="pointer-events-none fixed inset-0 z-50 flex items-center justify-center"
|
||||
transition:fly={{ y: 50 }}
|
||||
on:introstart
|
||||
on:outroend
|
||||
use:focusTrap
|
||||
>
|
||||
<div
|
||||
class="bg-base-100 rounded-box pointer-events-auto flex max-h-full min-w-fit max-w-md flex-col justify-between p-4 shadow-lg"
|
||||
>
|
||||
<h2 class="text-base-content text-start text-2xl font-bold">Updating Firmware</h2>
|
||||
<div class="divider my-2" />
|
||||
<div class="overflow-y-auto">
|
||||
<div class="bg-base-100 flex flex-col items-center justify-center p-6">
|
||||
{#if $telemetry.download_ota.status == 'progress'}
|
||||
<progress class="progress progress-primary w-56" value={progress} max="100" />
|
||||
{:else}
|
||||
<progress class="progress progress-primary w-56" />
|
||||
{/if}
|
||||
<p class="mt-8 text-2xl">{message}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="divider my-2" />
|
||||
<div class="flex flex-wrap justify-end gap-2">
|
||||
<div class="flex-grow" />
|
||||
<button
|
||||
class="btn btn-warning text-warning-content inline-flex flex-none items-center"
|
||||
disabled={updating}
|
||||
on:click={() => {
|
||||
closeAllModals();
|
||||
location.reload();
|
||||
}}
|
||||
>
|
||||
<Cancel class="mr-2 h-5 w-5" /><span>Close</span></button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -0,0 +1,42 @@
|
||||
<script lang="ts">
|
||||
import { closeModal } from 'svelte-modals';
|
||||
import { focusTrap } from 'svelte-focus-trap';
|
||||
import { fly } from 'svelte/transition';
|
||||
import Check from '~icons/tabler/check';
|
||||
|
||||
// provided by <Modals />
|
||||
export let isOpen: boolean;
|
||||
|
||||
export let title: string;
|
||||
export let message: string;
|
||||
export let onDismiss: any;
|
||||
export let dismiss = { label: 'Dismiss', icon: Check };
|
||||
</script>
|
||||
|
||||
{#if isOpen}
|
||||
<div
|
||||
role="dialog"
|
||||
class="pointer-events-none fixed inset-0 z-50 flex items-center justify-center"
|
||||
transition:fly={{ y: 50 }}
|
||||
on:introstart
|
||||
on:outroend
|
||||
use:focusTrap
|
||||
>
|
||||
<div
|
||||
class="rounded-box bg-base-100 pointer-events-auto flex min-w-fit max-w-md flex-col justify-between p-4 shadow-lg"
|
||||
>
|
||||
<h2 class="text-base-content text-start text-2xl font-bold">{title}</h2>
|
||||
<div class="divider my-2" />
|
||||
<p class="text-base-content mb-1 text-start">{message}</p>
|
||||
<div class="divider my-2" />
|
||||
<div class="flex justify-end gap-2">
|
||||
<button
|
||||
class="btn btn-warning text-warning-content inline-flex items-center"
|
||||
on:click={onDismiss}
|
||||
><svelte:component this={dismiss.icon} class="mr-2 h-5 w-5" /><span>{dismiss.label}</span
|
||||
></button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -0,0 +1,56 @@
|
||||
<script lang="ts">
|
||||
let show = false;
|
||||
$: type = show ? 'text' : 'password';
|
||||
|
||||
export let value = '';
|
||||
export let id = '';
|
||||
function handleInput(e: any) {
|
||||
value = e.target.value;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="relative">
|
||||
<input {type} class="input input-bordered w-full" {value} on:input={handleInput} {id} />
|
||||
<div class="absolute inset-y-0 right-0 flex items-center pr-1">
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="text-base-content/50 h-6 {show ? 'block' : 'hidden'}"
|
||||
on:click={() => (show = false)}
|
||||
width="40"
|
||||
height="40"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M10.585 10.587a2 2 0 0 0 2.829 2.828" />
|
||||
<path
|
||||
d="M16.681 16.673a8.717 8.717 0 0 1 -4.681 1.327c-3.6 0 -6.6 -2 -9 -6c1.272 -2.12 2.712 -3.678 4.32 -4.674m2.86 -1.146a9.055 9.055 0 0 1 1.82 -.18c3.6 0 6.6 2 9 6c-.666 1.11 -1.379 2.067 -2.138 2.87"
|
||||
/>
|
||||
<path d="M3 3l18 18" />
|
||||
</svg>
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="text-base-content/50 h-6 {show ? 'hidden' : 'block'}"
|
||||
on:click={() => (show = true)}
|
||||
width="40"
|
||||
height="40"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M10 12a2 2 0 1 0 4 0a2 2 0 0 0 -4 0" />
|
||||
<path d="M21 12c-2.4 4 -5.4 6 -9 6c-3.6 0 -6.6 -2 -9 -6c2.4 -4 5.4 -6 9 -6c3.6 0 6.6 2 9 6" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,35 @@
|
||||
<script lang="ts">
|
||||
import WiFi from '~icons/tabler/wifi';
|
||||
import WiFi0 from '~icons/tabler/wifi-0';
|
||||
import WiFi1 from '~icons/tabler/wifi-1';
|
||||
import WiFi2 from '~icons/tabler/wifi-2';
|
||||
|
||||
export let showDBm = false;
|
||||
export let rssi_dbm = 0;
|
||||
</script>
|
||||
|
||||
<div class="indicator">
|
||||
{#if showDBm}
|
||||
<span class="indicator-item indicator-start badge badge-accent badge-outline badge-xs">
|
||||
{rssi_dbm} dBm
|
||||
</span>
|
||||
{/if}
|
||||
{#if rssi_dbm >= -55}
|
||||
<WiFi class={$$props.class || ''} />
|
||||
{:else if rssi_dbm >= -75}
|
||||
<div class="{$$props.class || ''} relative">
|
||||
<WiFi class="absolute inset-0 h-full w-full opacity-30" />
|
||||
<WiFi2 class="absolute inset-0 h-full w-full" />
|
||||
</div>
|
||||
{:else if rssi_dbm >= -85}
|
||||
<div class="{$$props.class || ''} relative">
|
||||
<WiFi class="absolute inset-0 h-full w-full opacity-30" />
|
||||
<WiFi1 class="absolute inset-0 h-full w-full" />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="{$$props.class || ''} relative">
|
||||
<WiFi class="absolute inset-0 h-full w-full opacity-30" />
|
||||
<WiFi0 class="absolute inset-0 h-full w-full" />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,56 @@
|
||||
<script lang="ts">
|
||||
import { slide } from 'svelte/transition';
|
||||
import { cubicOut } from 'svelte/easing';
|
||||
import Down from '~icons/tabler/chevron-down';
|
||||
export let open = true;
|
||||
export let collapsible = true;
|
||||
</script>
|
||||
|
||||
{#if collapsible}
|
||||
<div
|
||||
class="bg-base-200 rounded-box relative grid w-full max-w-2xl self-center overflow-hidden shadow-lg"
|
||||
>
|
||||
<div
|
||||
class="min-h-16 flex w-full items-center justify-between space-x-3 p-4 text-xl font-medium"
|
||||
>
|
||||
<span class="inline-flex items-baseline">
|
||||
<slot name="icon" />
|
||||
<slot name="title" />
|
||||
</span>
|
||||
<button
|
||||
class="btn btn-circle btn-ghost btn-sm"
|
||||
on:click={() => {
|
||||
open = !open;
|
||||
}}
|
||||
>
|
||||
<Down
|
||||
class="text-base-content h-auto w-6 transition-transform duration-300 ease-in-out {open
|
||||
? 'rotate-180'
|
||||
: ''}"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
{#if open}
|
||||
<div
|
||||
class="flex flex-col gap-2 p-4 pt-0"
|
||||
transition:slide|local={{ duration: 300, easing: cubicOut }}
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div
|
||||
class="bg-base-200 rounded-box relative grid w-full max-w-2xl self-center overflow-hidden shadow-lg"
|
||||
>
|
||||
<div class="min-h-16 w-full p-4 text-xl font-medium">
|
||||
<span class="inline-flex items-baseline">
|
||||
<slot name="icon" />
|
||||
<slot name="title" />
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 p-4 pt-0">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -0,0 +1,8 @@
|
||||
<script lang="ts">
|
||||
import Loader from '~icons/tabler/loader-2';
|
||||
</script>
|
||||
|
||||
<div class="flex h-full w-full flex-col items-center justify-center p-6">
|
||||
<Loader class="text-primary h-14 w-auto animate-spin stroke-2" />
|
||||
<p class="text-xl">Loading...</p>
|
||||
</div>
|
||||
@@ -0,0 +1,37 @@
|
||||
<script>
|
||||
import { flip } from 'svelte/animate';
|
||||
import { fly } from 'svelte/transition';
|
||||
import { notifications } from '$lib/components/toasts/notifications';
|
||||
import error from '~icons/tabler/circle-x';
|
||||
import success from '~icons/tabler/circle-check';
|
||||
import warning from '~icons/tabler/alert-triangle';
|
||||
import info from '~icons/tabler/info-circle';
|
||||
|
||||
export let theme = {
|
||||
error: 'alert-error',
|
||||
success: 'alert-success',
|
||||
warning: 'alert-warning',
|
||||
info: 'alert-info'
|
||||
};
|
||||
|
||||
export let icon = {
|
||||
error: error,
|
||||
success: success,
|
||||
warning: warning,
|
||||
info: info
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="toast toast-end mr-4">
|
||||
{#each $notifications as notification (notification.id)}
|
||||
<div
|
||||
animate:flip={{ duration: 400 }}
|
||||
class="alert animate-none {theme[notification.type]}"
|
||||
in:fly={{ y: 100, duration: 400 }}
|
||||
out:fly={{ x: 100, duration: 400 }}
|
||||
>
|
||||
<svelte:component this={icon[notification.type]} class="h-6 w-6 flex-shrink-0" />
|
||||
<span>{notification.message}</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import MdiHamburgerMenu from '~icons/mdi/hamburger-menu';
|
||||
</script>
|
||||
|
||||
<div class="topbar absolute left-0 top-0 w-full z-10 flex justify-between bg-zinc-800">
|
||||
<div class="flex gap-2 p-2">
|
||||
<a href="/">
|
||||
<svelte:component this={MdiHamburgerMenu} class="h-8 w-8"/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.topbar {
|
||||
height: 50px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,116 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { openModal, closeAllModals } from 'svelte-modals';
|
||||
import { user } from '$lib/stores/user';
|
||||
import { notifications } from '$lib/components/toasts/notifications';
|
||||
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
|
||||
import Firmware from '~icons/tabler/refresh-alert';
|
||||
import Cancel from '~icons/tabler/x';
|
||||
import CloudDown from '~icons/tabler/cloud-download';
|
||||
import GithubUpdateDialog from '$lib/components/GithubUpdateDialog.svelte';
|
||||
import { compareVersions } from 'compare-versions';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
export let update = false;
|
||||
|
||||
let firmwareVersion: string;
|
||||
let firmwareDownloadLink: string;
|
||||
|
||||
async function getGithubAPI() {
|
||||
try {
|
||||
const response = await fetch(
|
||||
'https://api.github.com/repos/' + $page.data.github + '/releases/latest',
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
accept: 'application/vnd.github+json',
|
||||
'X-GitHub-Api-Version': '2022-11-28'
|
||||
}
|
||||
}
|
||||
);
|
||||
const results = await response.json();
|
||||
if (results.message == "Not Found") {
|
||||
console.error('Error: Could not find releases in the repository');
|
||||
return;
|
||||
}
|
||||
|
||||
update = false;
|
||||
firmwareVersion = '';
|
||||
|
||||
if (compareVersions(results.tag_name, $page.data.features.firmware_version) === 1) {
|
||||
// iterate over assets and find the correct one
|
||||
for (let i = 0; i < results.assets.length; i++) {
|
||||
// check if the asset is of type *.bin
|
||||
if (
|
||||
results.assets[i].name.includes('.bin') &&
|
||||
results.assets[i].name.includes($page.data.features.firmware_built_target)
|
||||
) {
|
||||
update = true;
|
||||
firmwareVersion = results.tag_name;
|
||||
firmwareDownloadLink = results.assets[i].browser_download_url;
|
||||
notifications.info('Firmware update available.', 5000);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function postGithubDownload(url: string) {
|
||||
try {
|
||||
const apiResponse = await fetch('/api/downloadUpdate', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: $page.data.features.security ? 'Bearer ' + $user.bearer_token : 'Basic',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ download_url: url })
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if ($page.data.features.download_firmware && (!$page.data.features.security || $user.admin)) {
|
||||
getGithubAPI();
|
||||
const interval = setInterval(
|
||||
async () => {
|
||||
getGithubAPI();
|
||||
},
|
||||
60 * 60 * 1000
|
||||
); // once per hour
|
||||
}
|
||||
});
|
||||
|
||||
function confirmGithubUpdate(url: string) {
|
||||
openModal(ConfirmDialog, {
|
||||
title: 'Confirm flashing new firmware to the device',
|
||||
message: 'Are you sure you want to overwrite the existing firmware with a new one?',
|
||||
labels: {
|
||||
cancel: { label: 'Abort', icon: Cancel },
|
||||
confirm: { label: 'Update', icon: CloudDown }
|
||||
},
|
||||
onConfirm: () => {
|
||||
postGithubDownload(url);
|
||||
openModal(GithubUpdateDialog, {
|
||||
onConfirm: () => closeAllModals()
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if update}
|
||||
<button
|
||||
class="btn btn-square btn-ghost h-9 w-9"
|
||||
on:click={() => confirmGithubUpdate(firmwareDownloadLink)}
|
||||
>
|
||||
<span
|
||||
class="indicator-item indicator-top indicator-center badge badge-info badge-xs top-2 scale-75 lg:top-1"
|
||||
>{firmwareVersion}</span
|
||||
>
|
||||
<Firmware class="h-7 w-7" />
|
||||
</button>
|
||||
{/if}
|
||||
@@ -0,0 +1,153 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { BufferGeometry, Line, LineBasicMaterial, Vector3, type NormalBufferAttributes } from 'three';
|
||||
import uzip from 'uzip';
|
||||
import { model, servoAnglesOut } from '$lib/stores';
|
||||
import { footColor, isEmbeddedApp, toeWorldPositions } from '$lib/utilities';
|
||||
import { fileService } from '$lib/services';
|
||||
import { servoAngles, mpu, jointNames } from '$lib/stores';
|
||||
import SceneBuilder from '$lib/sceneBuilder';
|
||||
import { lerp, degToRad } from 'three/src/math/MathUtils';
|
||||
import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
|
||||
|
||||
export let sky = true
|
||||
export let orbit = false
|
||||
export let panel = true
|
||||
export let debug = false
|
||||
export let ground = true
|
||||
|
||||
let sceneManager = new SceneBuilder();
|
||||
let canvas: HTMLCanvasElement
|
||||
|
||||
let currentModelAngles: number[] = new Array(12).fill(0);
|
||||
let modelTargetAngles: number[] = new Array(12).fill(0)
|
||||
let gui_panel: GUI
|
||||
|
||||
let feet_trace = new Array(4).fill([]);
|
||||
let trace_lines: BufferGeometry<NormalBufferAttributes>[] = []
|
||||
|
||||
let settings = {
|
||||
'Trace feet':debug,
|
||||
'Trace points': 30,
|
||||
'Fix camera on robot': true
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
await cacheModelFiles()
|
||||
await createScene();
|
||||
if (!isEmbeddedApp && panel) createPanel();
|
||||
servoAngles.subscribe(updateAnglesFromStore)
|
||||
});
|
||||
|
||||
const updateAnglesFromStore = (angles: number[]) => {
|
||||
if (sceneManager.isDragging) return
|
||||
modelTargetAngles = angles;
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
canvas.remove()
|
||||
gui_panel?.destroy()
|
||||
});
|
||||
|
||||
const createPanel = () => {
|
||||
gui_panel = new GUI({width: 310});
|
||||
gui_panel.close();
|
||||
gui_panel.domElement.id = 'three-gui-panel';
|
||||
|
||||
const visibility = gui_panel.addFolder('Visualization');
|
||||
visibility.add(settings, 'Trace feet')
|
||||
visibility.add(settings, 'Trace points', 1, 1000, 1)
|
||||
}
|
||||
|
||||
const cacheModelFiles = async () => {
|
||||
let data = await fetch('/stl.zip').then((data) => data.arrayBuffer());
|
||||
|
||||
var files = uzip.parse(data);
|
||||
|
||||
for (const [path, data] of Object.entries(files) as [path: string, data: Uint8Array][]) {
|
||||
const url = new URL(path, window.location.href);
|
||||
fileService.saveFile(url.toString(), data);
|
||||
}
|
||||
};
|
||||
|
||||
const updateAngles = (name: string, angle: number) => {
|
||||
modelTargetAngles[$jointNames.indexOf(name)] = angle * (180 / Math.PI);
|
||||
servoAnglesOut.set(modelTargetAngles)
|
||||
};
|
||||
|
||||
const createScene = async () => {
|
||||
sceneManager
|
||||
.addRenderer({ antialias: true, canvas, alpha: true })
|
||||
.addPerspectiveCamera({ x: -0.5, y: 0.5, z: 1 })
|
||||
.addOrbitControls(8, 30, orbit)
|
||||
.addDirectionalLight({ x: 10, y: 20, z: 10, color: 0xffffff, intensity: 0.9 })
|
||||
.addAmbientLight({ color: 0xffffff, intensity: 0.6 })
|
||||
.addFogExp2(0xcccccc, 0.015)
|
||||
.addModel($model)
|
||||
.fillParent()
|
||||
.addRenderCb(render)
|
||||
.startRenderLoop();
|
||||
|
||||
if (ground) sceneManager
|
||||
.addGroundPlane()
|
||||
.addGridHelper({ size: 30, divisions: 25 })
|
||||
|
||||
|
||||
if (sky) sceneManager.addSky()
|
||||
if (debug) sceneManager.addDragControl(updateAngles)
|
||||
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const geometry = new BufferGeometry();
|
||||
const material = new LineBasicMaterial({ color: footColor() });
|
||||
const line = new Line(geometry, material);
|
||||
trace_lines.push(geometry);
|
||||
sceneManager.scene.add(line);
|
||||
}
|
||||
};
|
||||
|
||||
const renderTraceLines = (foot_positions: Vector3[]) => {
|
||||
if (!settings['Trace feet']) {
|
||||
if (!feet_trace.length) return
|
||||
trace_lines.forEach((line, i) => line.setFromPoints(feet_trace[i].slice(-1)))
|
||||
feet_trace = new Array(4).fill([])
|
||||
return
|
||||
}
|
||||
|
||||
trace_lines.forEach((line, i) => {
|
||||
feet_trace[i].push(foot_positions[i])
|
||||
feet_trace[i] = feet_trace[i].slice(-settings['Trace points'])
|
||||
line.setFromPoints(feet_trace[i]);
|
||||
})
|
||||
}
|
||||
|
||||
const render = () => {
|
||||
const robot = sceneManager.model;
|
||||
if (!robot) return;
|
||||
|
||||
const toes = toeWorldPositions(robot)
|
||||
|
||||
renderTraceLines(toes)
|
||||
|
||||
if (settings['Fix camera on robot']) {
|
||||
sceneManager.controls.target = robot.position.clone()
|
||||
}
|
||||
|
||||
robot.position.y = robot.position.y - Math.min(...toes.map(toe => toe.y));
|
||||
robot.rotation.z = lerp(robot.rotation.z, degToRad($mpu.heading + 90), 0.1);
|
||||
|
||||
for (let i = 0; i < $jointNames.length; i++) {
|
||||
currentModelAngles[i] = lerp(
|
||||
(robot.joints[$jointNames[i]].angle as number) * (180 / Math.PI),
|
||||
modelTargetAngles[i],
|
||||
0.1
|
||||
);
|
||||
robot.joints[$jointNames[i]].setJointValue(degToRad(currentModelAngles[i]));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<svelte:window on:resize={sceneManager.fillParent} />
|
||||
|
||||
<canvas bind:this={canvas}></canvas>
|
||||
@@ -0,0 +1,52 @@
|
||||
import { writable, derived } from 'svelte/store';
|
||||
|
||||
type State = {
|
||||
id: string;
|
||||
type: string;
|
||||
message: string;
|
||||
timeout: number;
|
||||
};
|
||||
|
||||
function createNotificationStore() {
|
||||
const state: State[] = [];
|
||||
const _notifications = writable(state);
|
||||
|
||||
function send(message: string, type = 'info', timeout: number) {
|
||||
_notifications.update((state) => {
|
||||
return [...state, { id: id(), type, message, timeout }];
|
||||
});
|
||||
}
|
||||
|
||||
let timers = [];
|
||||
|
||||
const notifications = derived(_notifications, ($_notifications, set) => {
|
||||
set($_notifications);
|
||||
if ($_notifications.length > 0) {
|
||||
const timer = setTimeout(() => {
|
||||
_notifications.update((state) => {
|
||||
state.shift();
|
||||
return state;
|
||||
});
|
||||
}, $_notifications[0].timeout);
|
||||
return () => {
|
||||
clearTimeout(timer);
|
||||
};
|
||||
}
|
||||
});
|
||||
const { subscribe } = notifications;
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
send,
|
||||
error: (msg: string, timeout: number) => send(msg, 'error', timeout),
|
||||
warning: (msg: string, timeout: number) => send(msg, 'warning', timeout),
|
||||
info: (msg: string, timeout: number) => send(msg, 'info', timeout),
|
||||
success: (msg: string, timeout: number) => send(msg, 'success', timeout)
|
||||
};
|
||||
}
|
||||
|
||||
function id() {
|
||||
return '_' + Math.random().toString(36).substr(2, 9);
|
||||
}
|
||||
|
||||
export const notifications = createNotificationStore();
|
||||
@@ -0,0 +1,37 @@
|
||||
<script>
|
||||
import { flip } from 'svelte/animate';
|
||||
import { fly } from 'svelte/transition';
|
||||
import { notifications } from '$lib/components/toasts/notifications';
|
||||
import error from '~icons/tabler/circle-x';
|
||||
import success from '~icons/tabler/circle-check';
|
||||
import warning from '~icons/tabler/alert-triangle';
|
||||
import info from '~icons/tabler/info-circle';
|
||||
|
||||
export let theme = {
|
||||
error: 'alert-error',
|
||||
success: 'alert-success',
|
||||
warning: 'alert-warning',
|
||||
info: 'alert-info'
|
||||
};
|
||||
|
||||
export let icon = {
|
||||
error: error,
|
||||
success: success,
|
||||
warning: warning,
|
||||
info: info
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="toast toast-end mr-4">
|
||||
{#each $notifications as notification (notification.id)}
|
||||
<div
|
||||
animate:flip={{ duration: 400 }}
|
||||
class="alert animate-none {theme[notification.type]}"
|
||||
in:fly={{ y: 100, duration: 400 }}
|
||||
out:fly={{ x: 100, duration: 400 }}
|
||||
>
|
||||
<svelte:component this={icon[notification.type]} class="h-6 w-6 flex-shrink-0" />
|
||||
<span>{notification.message}</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -0,0 +1,52 @@
|
||||
import { writable, derived, type Writable } from 'svelte/store';
|
||||
|
||||
type NotificationType = 'info' | 'error' | 'warning' | 'success';
|
||||
|
||||
type State = {
|
||||
id: string;
|
||||
type: NotificationType;
|
||||
message: string;
|
||||
timeout: number;
|
||||
};
|
||||
|
||||
function createNotificationStore() {
|
||||
const state: State[] = [];
|
||||
const _notifications = writable(state);
|
||||
|
||||
function send(message: string, type: NotificationType = 'info', timeout: number) {
|
||||
_notifications.update((state) => {
|
||||
return [...state, { id: id(), type, message, timeout }];
|
||||
});
|
||||
}
|
||||
|
||||
const notifications = derived(_notifications, ($_notifications, set) => {
|
||||
set($_notifications);
|
||||
if ($_notifications.length > 0) {
|
||||
const timer = setTimeout(() => {
|
||||
_notifications.update((state) => {
|
||||
state.shift();
|
||||
return state;
|
||||
});
|
||||
}, $_notifications[0].timeout);
|
||||
return () => {
|
||||
clearTimeout(timer);
|
||||
};
|
||||
}
|
||||
}) as Writable<State[]>;
|
||||
const { subscribe } = notifications;
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
send,
|
||||
error: (msg: string, timeout: number) => send(msg, 'error', timeout),
|
||||
warning: (msg: string, timeout: number) => send(msg, 'warning', timeout),
|
||||
info: (msg: string, timeout: number) => send(msg, 'info', timeout),
|
||||
success: (msg: string, timeout: number) => send(msg, 'success', timeout)
|
||||
};
|
||||
}
|
||||
|
||||
function id() {
|
||||
return '_' + Math.random().toString(36).substr(2, 9);
|
||||
}
|
||||
|
||||
export const notifications = createNotificationStore();
|
||||
Reference in New Issue
Block a user