feat: qol improvements

This commit is contained in:
2025-12-11 20:51:32 +01:00
parent 7beab5ac07
commit 2893afa2ba
17 changed files with 632 additions and 304 deletions

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import LL from "$i18n/i18n-svelte";
import { preference } from "$lib/preferences";
import { preference, userPreferences } from "$lib/preferences";
import { initSerial } from "$lib/serial/connection";
import {
getPortName,
@@ -9,13 +9,26 @@
} from "$lib/serial/device";
import { showConnectionFailedDialog } from "$lib/dialogs/connection-failed-dialog";
import { onMount } from "svelte";
import { persistentWritable } from "$lib/storage";
let ports = $state<SerialPort[]>([]);
let element: HTMLDivElement | undefined = $state();
onMount(() => {
refreshPorts();
});
let hasDiscoveredAutoConnect = persistentWritable(
"hasDiscoveredAutoConnect",
false,
);
$effect(() => {
if ($userPreferences.backup || $userPreferences.autoConnect) {
$hasDiscoveredAutoConnect = true;
}
});
async function refreshPorts() {
ports = await navigator.serial.getPorts();
}
@@ -28,62 +41,79 @@
await showConnectionFailedDialog(String(error));
}
}
function closePopover() {
element?.closest<HTMLElement>("[popover]")?.hidePopover();
}
async function connectCC0(event: MouseEvent) {
const { fetchCCOS } = await import("$lib/ccos/ccos");
closePopover();
const ccos = await fetchCCOS();
if (ccos) {
connect(ccos, !event.shiftKey);
}
}
async function connectDevice(event: MouseEvent) {
const port = await navigator.serial.requestPort({
filters: event.shiftKey ? [] : [...PORT_FILTERS.values()],
});
if (!port) return;
closePopover();
refreshPorts();
connect(port, true);
}
</script>
<div class="device-list">
<fieldset>
<label
><input type="checkbox" use:preference={"autoConnect"} />
<div class="title">{$LL.deviceManager.AUTO_CONNECT()}</div>
</label>
<div
bind:this={element}
class="device-list"
onmouseenter={() => refreshPorts()}
role="region"
>
{#if ports.length === 1}
<fieldset class:promote={!$hasDiscoveredAutoConnect}>
<label
><input type="checkbox" use:preference={"autoConnect"} />
<div class="title">{$LL.deviceManager.AUTO_CONNECT()}</div>
</label>
<label
><input type="checkbox" use:preference={"backup"} />
<div class="title">{@html $LL.backup.AUTO_BACKUP()}</div>
</label>
</fieldset>
<button
onclick={async (event) => {
const { fetchCCOS } = await import("$lib/ccos/ccos");
const ccos = await fetchCCOS();
if (ccos) {
connect(ccos, !event.shiftKey);
}
}}
>
<span class="icon">history</span>
CC0</button
>
{#each ports as port}
<div class="device">
<button
onclick={(event) => {
connect(port, !event.shiftKey);
}}
>
<span class="icon">history</span>
{getPortName(port)}</button
>
<button
class="error"
onclick={() => {
port.forget();
refreshPorts();
}}><span class="icon">link_off</span></button
>
<label
><input type="checkbox" use:preference={"backup"} />
<div class="title">{@html $LL.backup.AUTO_BACKUP()}</div>
</label>
</fieldset>
{/if}
{#if ports.length !== 0}
<h4>Recent Devices</h4>
<div class="devices">
<div class="device">
<button onclick={connectCC0}> CC0</button>
</div>
{#each ports as port}
<div class="device">
<button
onclick={(event) => {
connect(port, !event.shiftKey);
}}
>
{getPortName(port)}</button
>
<button
class="error"
onclick={() => {
port.forget();
refreshPorts();
}}><span class="icon">visibility_off</span> Hide</button
>
</div>
{/each}
</div>
{/each}
{/if}
<div class="pair">
<button
onclick={async (event) => {
const port = await navigator.serial.requestPort({
filters: event.shiftKey ? [] : [...PORT_FILTERS.values()],
});
if (!port) return;
refreshPorts();
connect(port, true);
}}
class="primary"><span class="icon">add</span>Pair</button
<button onclick={connectDevice} class="primary"
><span class="icon">add</span>Connect</button
>
<a href="/ccos/zero_wasm/"><span class="icon">add</span>Virtual Device</a>
</div>
@@ -93,7 +123,8 @@
button,
a {
padding: 10px;
height: 32px;
padding-inline-end: 16px;
height: 38px;
font-size: 12px;
.icon {
@@ -101,18 +132,33 @@
}
}
h4 {
margin-block-start: 16px;
margin-block-end: 8px;
font-weight: 600;
}
.device-list {
margin: 8px;
}
.pair {
display: flex;
}
.devices {
margin-bottom: 16px;
}
.device {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
button {
flex: 1;
justify-content: flex-start;
font-size: 14px;
}
}
@@ -129,12 +175,57 @@
gap: 8px;
appearance: none;
padding: 0;
height: auto;
overflow: hidden;
.title {
font-weight: 600;
}
}
@keyframes attention {
0%,
100% {
filter: brightness(1);
}
50% {
filter: brightness(0.6);
}
}
@keyframes swoosh {
0% {
transform: translateX(-200%) skewX(-20deg);
opacity: 0;
}
50% {
opacity: 1;
}
100% {
transform: translateX(200%) skewX(-20deg);
opacity: 0;
}
}
.promote {
label:not(:has(input:checked)) {
animation: attention 1s ease;
&::after {
position: absolute;
z-index: -1;
animation: swoosh 1s ease forwards;
background-color: var(--md-sys-color-surface-variant);
width: 25%;
height: 200%;
content: "";
}
}
}
fieldset {
display: flex;
gap: 16px;