mirror of
https://github.com/CharaChorder/DeviceManager.git
synced 2026-02-28 03:52:05 +00:00
feat: tauri serial polyfill
This commit is contained in:
@@ -7,8 +7,6 @@
|
||||
export let results: number[] = []
|
||||
|
||||
export let width: number
|
||||
|
||||
console.log(width)
|
||||
</script>
|
||||
|
||||
<div class="list" style="width: {width}px">
|
||||
|
||||
@@ -8,7 +8,6 @@ export const popup: Action<HTMLButtonElement, ComponentType> = (node, Component)
|
||||
const edit = tippy(node, {
|
||||
interactive: true,
|
||||
trigger: "click",
|
||||
sticky: true,
|
||||
onShow(instance) {
|
||||
target = instance.popper.querySelector(".tippy-content") as HTMLElement
|
||||
target.classList.add("active")
|
||||
|
||||
23
src/lib/serial/TauriSerialDialog.svelte
Normal file
23
src/lib/serial/TauriSerialDialog.svelte
Normal file
@@ -0,0 +1,23 @@
|
||||
<script lang="ts">
|
||||
import {createEventDispatcher} from "svelte"
|
||||
|
||||
export let ports: SerialPort[]
|
||||
const dispatch = createEventDispatcher<{confirm: SerialPort | undefined}>()
|
||||
let selected = ports[0].getInfo().name
|
||||
</script>
|
||||
|
||||
<dialog>
|
||||
{#each ports as port}
|
||||
{@const info = port.getInfo()}
|
||||
<label>{info.product}<input type="radio" name="port" value={info.name} bind:group={selected} /></label>
|
||||
{/each}
|
||||
|
||||
<button on:click={() => dispatch("confirm", undefined)}>Cancel</button>
|
||||
<button
|
||||
on:click={() =>
|
||||
dispatch(
|
||||
"confirm",
|
||||
ports.find(it => it.getInfo().name === selected),
|
||||
)}>Ok</button
|
||||
>
|
||||
</dialog>
|
||||
@@ -6,7 +6,7 @@ import type {CharaLayout} from "$lib/serialization/layout"
|
||||
import {persistentWritable} from "$lib/storage"
|
||||
import {userPreferences} from "$lib/preferences"
|
||||
|
||||
export const serialPort = writable<CharaDevice>()
|
||||
export const serialPort = writable<CharaDevice | undefined>()
|
||||
|
||||
export interface SerialLogEntry {
|
||||
type: "input" | "output" | "system"
|
||||
|
||||
@@ -11,7 +11,6 @@ if (browser && import.meta.env.TAURI_FAMILY !== undefined) {
|
||||
}
|
||||
|
||||
export async function getViablePorts(): Promise<SerialPort[]> {
|
||||
console.log(await navigator.serial.getPorts().then(it => it.map(it => it.getInfo())))
|
||||
return navigator.serial.getPorts().then(ports => ports.filter(it => it.getInfo().usbVendorId === VENDOR_ID))
|
||||
}
|
||||
|
||||
|
||||
8
src/lib/serial/tauri-serial-extension.d.ts
vendored
Normal file
8
src/lib/serial/tauri-serial-extension.d.ts
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
/// <references types="@types/w3c-web-serial" />
|
||||
|
||||
interface SerialPortInfo {
|
||||
name?: string
|
||||
serialNumber?: string
|
||||
manufacturer?: string
|
||||
product?: string
|
||||
}
|
||||
@@ -1,22 +1,65 @@
|
||||
import {invoke} from "@tauri-apps/api"
|
||||
import TauriSerialDialog from "$lib/serial/TauriSerialDialog.svelte"
|
||||
|
||||
export type TauriSerialPort = Pick<
|
||||
SerialPort,
|
||||
"getInfo" | "open" | "close" | "readable" | "writable" | "forget"
|
||||
>
|
||||
|
||||
function NativeSerialPort(info: SerialPortInfo): TauriSerialPort {
|
||||
return {
|
||||
getInfo() {
|
||||
return info
|
||||
},
|
||||
async open({baudRate}: SerialOptions) {
|
||||
await invoke("plugin:serial|open", {path: info.name, baudRate})
|
||||
},
|
||||
async close() {
|
||||
await invoke("plugin:serial|close", {path: info.name})
|
||||
},
|
||||
async forget() {
|
||||
// noop
|
||||
},
|
||||
readable: new ReadableStream({
|
||||
async pull(controller) {
|
||||
const result = await invoke<number[]>("plugin:serial|read", {path: info.name})
|
||||
controller.enqueue(new Uint8Array(result))
|
||||
},
|
||||
}),
|
||||
writable: new WritableStream({
|
||||
async write(chunk) {
|
||||
await invoke("plugin:serial|write", {path: info.name, chunk: Array.from(chunk)})
|
||||
},
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-expect-error polyfill
|
||||
// noinspection JSConstantReassignment
|
||||
navigator.serial = {
|
||||
getPorts(): Promise<SerialPort[]> {
|
||||
async getPorts(): Promise<SerialPort[]> {
|
||||
return invoke<any[]>("plugin:serial|get_serial_ports").then(ports =>
|
||||
ports.map<Partial<SerialPort>>(port => ({
|
||||
getInfo() {
|
||||
return {
|
||||
name: port["name"],
|
||||
usbVendorId: port["vendor_id"],
|
||||
usbProductId: port["product_id"],
|
||||
serialNumber: port["serial_number"],
|
||||
manufacturer: port["manufacturer"],
|
||||
product: port["product"],
|
||||
} as SerialPortInfo
|
||||
},
|
||||
})),
|
||||
ports.map(NativeSerialPort),
|
||||
) as Promise<SerialPort[]>
|
||||
},
|
||||
async requestPort(options?: SerialPortRequestOptions): Promise<SerialPort> {
|
||||
const ports = await navigator.serial.getPorts().then(ports =>
|
||||
options?.filters !== undefined
|
||||
? ports.filter(port =>
|
||||
options.filters!.some(({usbVendorId, usbProductId}) => {
|
||||
const info = port.getInfo()
|
||||
return (
|
||||
(usbVendorId === undefined || info.usbVendorId === usbVendorId) &&
|
||||
(usbProductId === undefined || info.usbProductId === usbProductId)
|
||||
)
|
||||
}),
|
||||
)
|
||||
: ports,
|
||||
)
|
||||
|
||||
const dialog = new TauriSerialDialog({target: document.body, props: {ports}})
|
||||
const port = await new Promise<SerialPort>(resolve => dialog.$on("confirm", resolve))
|
||||
dialog.$destroy()
|
||||
return port
|
||||
},
|
||||
}
|
||||
|
||||
@@ -9,9 +9,7 @@
|
||||
import Navigation from "./Navigation.svelte"
|
||||
import {canAutoConnect} from "$lib/serial/device"
|
||||
import {initSerial} from "$lib/serial/connection"
|
||||
import {pwaInfo} from "virtual:pwa-info"
|
||||
import type {LayoutServerData} from "./$types"
|
||||
import type {RegisterSWOptions} from "vite-plugin-pwa/types"
|
||||
import {browser} from "$app/environment"
|
||||
import BrowserWarning from "./BrowserWarning.svelte"
|
||||
import "tippy.js/animations/shift-away.css"
|
||||
@@ -46,21 +44,15 @@
|
||||
const dark = it.mode === "dark" // window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
applyTheme(theme, {target: document.body, dark})
|
||||
})
|
||||
|
||||
if (pwaInfo) {
|
||||
const {registerSW} = await import("virtual:pwa-register")
|
||||
registerSW({
|
||||
immediate: true,
|
||||
onRegisterError(error) {
|
||||
console.log("ServiceWorker Registration Error", error)
|
||||
},
|
||||
} satisfies RegisterSWOptions)
|
||||
if (import.meta.env.TAURI_FAMILY === undefined) {
|
||||
const {initPwa} = await import("./pwa-setup")
|
||||
await initPwa()
|
||||
}
|
||||
|
||||
if (browser && $userPreferences.autoConnect && (await canAutoConnect())) await initSerial()
|
||||
})
|
||||
|
||||
$: webManifestLink = pwaInfo ? pwaInfo.webManifest.linkTag : ""
|
||||
let webManifestLink = ""
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -76,7 +68,7 @@
|
||||
<slot />
|
||||
</main>
|
||||
|
||||
{#if !import.meta.env.TAURI_FAMILY && browser && !("serial" in navigator)}
|
||||
{#if import.meta.env.TAURI_FAMILY === undefined && browser && !("serial" in navigator)}
|
||||
<BrowserWarning />
|
||||
{/if}
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
<button
|
||||
class="secondary"
|
||||
on:click={() => {
|
||||
$serialPort.forget()
|
||||
$serialPort?.forget()
|
||||
$serialPort = undefined
|
||||
}}><span class="icon">usb_off</span>{$LL.deviceManager.DISCONNECT()}</button
|
||||
>
|
||||
@@ -45,7 +45,7 @@
|
||||
href="/terminal"
|
||||
title={$LL.deviceManager.TERMINAL()}
|
||||
class="icon"
|
||||
disabled={$serialPort === undefined}
|
||||
class:disabled={$serialPort === undefined}
|
||||
on:click={() => (terminal = !terminal)}>terminal</a
|
||||
>
|
||||
<button
|
||||
@@ -62,13 +62,13 @@
|
||||
<h3>{$LL.deviceManager.bootMenu.TITLE()}</h3>
|
||||
<button
|
||||
on:click={() => {
|
||||
$serialPort.reboot()
|
||||
$serialPort?.reboot()
|
||||
$serialPort = undefined
|
||||
}}><span class="icon">restart_alt</span>{$LL.deviceManager.bootMenu.REBOOT()}</button
|
||||
>
|
||||
<button
|
||||
on:click={() => {
|
||||
$serialPort.bootloader()
|
||||
$serialPort?.bootloader()
|
||||
$serialPort = undefined
|
||||
}}><span class="icon">rule_settings</span>{$LL.deviceManager.bootMenu.BOOTLOADER()}</button
|
||||
>
|
||||
@@ -176,31 +176,22 @@
|
||||
|
||||
transition: all 250ms ease;
|
||||
|
||||
&:disabled {
|
||||
cursor: default;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&.icon {
|
||||
aspect-ratio: 1;
|
||||
padding-inline-end: 8px;
|
||||
font-size: 24px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
&.secondary {
|
||||
color: var(--md-sys-color-on-secondary);
|
||||
background: var(--md-sys-color-secondary);
|
||||
}
|
||||
a.disabled,
|
||||
button:disabled {
|
||||
cursor: default;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&.error {
|
||||
color: var(--md-sys-color-on-error);
|
||||
background: var(--md-sys-color-error);
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
background: var(--md-sys-color-surface-variant);
|
||||
}
|
||||
button:active:not(:disabled) {
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
background: var(--md-sys-color-surface-variant);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -47,9 +47,11 @@
|
||||
<button transition:fly={{x: -8}} class="icon" on:click={triggerShare}>share</button>
|
||||
<div transition:slide class="separator" />
|
||||
{/if}
|
||||
{#await import("$lib/components/PwaStatus.svelte") then { default: PwaStatus }}
|
||||
<PwaStatus />
|
||||
{/await}
|
||||
{#if import.meta.env.TAURI_FAMILY === undefined}
|
||||
{#await import("$lib/components/PwaStatus.svelte") then { default: PwaStatus }}
|
||||
<PwaStatus />
|
||||
{/await}
|
||||
{/if}
|
||||
{#if $serialPort}
|
||||
<button title={$LL.backup.TITLE()} use:popup={BackupPopup} class="icon {$syncStatus}">
|
||||
{#if $syncStatus === "downloading"}
|
||||
|
||||
16
src/routes/pwa-setup.ts
Normal file
16
src/routes/pwa-setup.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import type {RegisterSWOptions} from "vite-plugin-pwa/types"
|
||||
|
||||
export async function initPwa(): Promise<string> {
|
||||
// @ts-expect-error confused TS
|
||||
const {pwaInfo} = await import("virtual:pwa-info")
|
||||
// @ts-expect-error confused TS
|
||||
const {registerSW} = await import("virtual:pwa-register")
|
||||
registerSW({
|
||||
immediate: true,
|
||||
onRegisterError(error) {
|
||||
console.log("ServiceWorker Registration Error", error)
|
||||
},
|
||||
} satisfies RegisterSWOptions)
|
||||
|
||||
return pwaInfo ? pwaInfo.webManifest.linkTag : ""
|
||||
}
|
||||
Reference in New Issue
Block a user