From 998a4003958dd2e35ec803d73a1929c5f313ae56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thea=20Sch=C3=B6bl?= Date: Sun, 23 Jul 2023 00:43:54 +0200 Subject: [PATCH] stuff --- flake.nix | 7 +- src/lib/popup.ts | 28 +++++ src/lib/preferences.ts | 32 ++++++ src/lib/serial/connection.ts | 7 +- src/lib/serial/device.ts | 30 ++++- src/lib/serial/storage.ts | 10 ++ src/lib/style/tippy.scss | 15 ++- src/lib/style/toggle.scss | 64 +++++++++++ src/routes/+layout.svelte | 20 +++- src/routes/BackupPopup.svelte | 105 ++++++++++++++++++ src/routes/BrowserWarning.svelte | 25 +++-- src/routes/ConnectionPopup.svelte | 102 +++++++++++++++++ .../components => routes}/Navigation.svelte | 48 ++++---- 13 files changed, 446 insertions(+), 47 deletions(-) create mode 100644 src/lib/popup.ts create mode 100644 src/lib/preferences.ts create mode 100644 src/lib/style/toggle.scss create mode 100644 src/routes/BackupPopup.svelte create mode 100644 src/routes/ConnectionPopup.svelte rename src/{lib/components => routes}/Navigation.svelte (83%) diff --git a/flake.nix b/flake.nix index 8f793005..f4589c97 100644 --- a/flake.nix +++ b/flake.nix @@ -6,9 +6,7 @@ overlays = [ (final: prev: rec { nodejs = prev.nodejs-18_x; - chrome = prev.google-chrome; - firefox = prev.firefox; - webkit = prev.epiphany; # Safari-ish browser + chrome = prev.chromium; }) ]; supportedSystems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ]; @@ -27,12 +25,9 @@ { default = pkgs.mkShell { packages = with pkgs; [ - node2nix nodejs python - firefox chrome - webkit ]; }; }); diff --git a/src/lib/popup.ts b/src/lib/popup.ts new file mode 100644 index 00000000..5272976b --- /dev/null +++ b/src/lib/popup.ts @@ -0,0 +1,28 @@ +import tippy from "tippy.js" +import type {Action} from "svelte/action" +import type {ComponentType, SvelteComponent} from "svelte" + +export const popup: Action = (node, Component) => { + let component: SvelteComponent | undefined + let target: HTMLElement | undefined + const edit = tippy(node, { + interactive: true, + trigger: "click", + onShow(instance) { + target = instance.popper.querySelector(".tippy-content") as HTMLElement + target.classList.add("active") + component ??= new Component({target}) + }, + onHidden() { + component?.$destroy() + target?.classList.remove("active") + component = undefined + }, + }) + + return { + destroy() { + edit.destroy() + }, + } +} diff --git a/src/lib/preferences.ts b/src/lib/preferences.ts new file mode 100644 index 00000000..8a516a6e --- /dev/null +++ b/src/lib/preferences.ts @@ -0,0 +1,32 @@ +import {writable} from "svelte/store" +import type {Action} from "svelte/action" + +export interface UserPreferences { + backup: boolean + autoSync: boolean +} + +export const userPreferences = writable({ + backup: false, + autoSync: true, +}) + +export const preference: Action = (node, key) => { + const unsubscribe = userPreferences.subscribe(it => { + node.checked = it[key] + }) + function update() { + userPreferences.update(value => { + value[key] = node.checked + return value + }) + } + node.addEventListener("input", update) + + return { + destroy() { + unsubscribe() + node.removeEventListener("input", update) + }, + } +} diff --git a/src/lib/serial/connection.ts b/src/lib/serial/connection.ts index 8cc17e4c..40cfb311 100644 --- a/src/lib/serial/connection.ts +++ b/src/lib/serial/connection.ts @@ -1,4 +1,4 @@ -import {writable} from "svelte/store" +import {get, writable} from "svelte/store" import {CharaDevice} from "$lib/serial/device" import type {Chord} from "$lib/serial/chord" import type {Writable} from "svelte/store" @@ -26,10 +26,11 @@ export const syncStatus: Writable<"done" | "error" | "downloading" | "uploading" let device: CharaDevice // @hmr:keep export async function initSerial() { - syncStatus.set("downloading") - device ??= new CharaDevice() + const device = get(serialPort) ?? new CharaDevice() + await device.ready() serialPort.set(device) + syncStatus.set("downloading") const parsedLayout: CharaLayout = [[], [], []] for (let layer = 1; layer <= 3; layer++) { for (let i = 0; i < 90; i++) { diff --git a/src/lib/serial/device.ts b/src/lib/serial/device.ts index 5accc921..10ec1aa8 100644 --- a/src/lib/serial/device.ts +++ b/src/lib/serial/device.ts @@ -5,8 +5,12 @@ import {chordFromCommandCompatible} from "$lib/serial/chord" export const VENDOR_ID = 0x239a -export async function hasSerialPermission() { - return navigator.serial.getPorts().then(it => it.length > 0) +export async function getViablePorts(): Promise { + return navigator.serial.getPorts().then(ports => ports.filter(it => it.getInfo().usbVendorId === VENDOR_ID)) +} + +export async function canAutoConnect() { + return getViablePorts().then(it => it.length === 1) } export class CharaDevice { @@ -22,10 +26,11 @@ export class CharaDevice { deviceId: Promise constructor(baudRate = 115200) { - this.port = navigator.serial.getPorts().then(async ports => { + this.port = getViablePorts().then(async ports => { const port = - ports.find(it => it.getInfo().usbVendorId === VENDOR_ID) ?? - (await navigator.serial.requestPort({filters: [{usbVendorId: VENDOR_ID}]})) + ports.length === 1 + ? ports[0] + : await navigator.serial.requestPort({filters: [{usbVendorId: VENDOR_ID}]}) await port.open({baudRate}) const info = port.getInfo() serialLog.update(it => { @@ -91,6 +96,21 @@ export class CharaDevice { } } + async ready() { + await this.port + } + + async forget() { + await (await this.port).forget() + } + + async disconnect() { + this.abortController1.abort() + this.abortController2.abort() + ;(await this.reader).releaseLock() + await (await this.port).close() + } + /** * Read/write to serial port */ diff --git a/src/lib/serial/storage.ts b/src/lib/serial/storage.ts index e8592e9c..68d86e20 100644 --- a/src/lib/serial/storage.ts +++ b/src/lib/serial/storage.ts @@ -1,10 +1,20 @@ import {chords, layout} from "$lib/serial/connection" +import {userPreferences} from "$lib/preferences" const PROFILE_KEY = "profiles" const CHORD_LIBRARY_STORAGE_KEY = "chord-library" const LAYOUT_STORAGE_KEY = "layouts" +const PREFERENCES = "user-preferences" export function initLocalStorage() { + const storedPreferences = localStorage.getItem(PREFERENCES) + if (storedPreferences) { + userPreferences.set(JSON.parse(storedPreferences)) + } + userPreferences.subscribe(preferences => { + localStorage.setItem(PREFERENCES, JSON.stringify(preferences)) + }) + const storedLayout = localStorage.getItem(LAYOUT_STORAGE_KEY) if (storedLayout) { layout.set(JSON.parse(storedLayout)) diff --git a/src/lib/style/tippy.scss b/src/lib/style/tippy.scss index 4a9facde..b1b3d8bd 100644 --- a/src/lib/style/tippy.scss +++ b/src/lib/style/tippy.scss @@ -1,8 +1,21 @@ +$padding: 16px; + .tippy-box[data-theme~="surface-variant"] { color: var(--md-sys-color-on-surface-variant); background-color: var(--md-sys-color-surface-variant); - filter: drop-shadow(0 0 12px #000a); + border-radius: calc(24px + $padding); + + .tippy-content { + padding: $padding; + } + + h2 { + margin-block-start: 8px; + margin-block-end: calc(8px + $padding); + display: flex; + justify-content: center; + } @each $placement in top, bottom, right, left { &[data-placement^="#{$placement}"] > .tippy-arrow::before { diff --git a/src/lib/style/toggle.scss b/src/lib/style/toggle.scss new file mode 100644 index 00000000..53db84b4 --- /dev/null +++ b/src/lib/style/toggle.scss @@ -0,0 +1,64 @@ +$padding: 3px; +$border: 2px; +$height: 1.5em; + +label:has(input[type="checkbox"]) { + cursor: pointer; + + display: flex; + gap: $padding; + align-items: center; + justify-content: center; + + font-size: 12px; + + input { + $width: calc($height * (5 / 3)); + $diameter: calc($height - ((2 * $padding) + (2 * $border))); + $radius: calc($diameter / 2); + + cursor: pointer; + + position: relative; + + overflow: hidden; + display: flex; + + width: $width; + height: $height; + + font-size: inherit; + color: inherit; + + border-radius: calc($height / 2); + outline: $border solid currentcolor; + outline-offset: calc(-1 * $border); + + &::after { + content: ""; + + position: absolute; + top: calc($padding + $border); + left: calc($padding + $border); + + display: block; + + width: $diameter; + height: $diameter; + + border-radius: calc($radius); + outline-color: inherit; + outline-style: solid; + outline-width: $radius; + outline-offset: calc(-1 * $radius); + + transition: all 250ms ease; + } + + &:checked::after { + translate: calc($width - 2 * $diameter - $padding / 2) 0; + outline-width: calc($width - ($height - $border) + $padding); + outline-offset: calc($padding / 2); + } + } +} diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index d2663437..8c9f08cc 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -3,12 +3,12 @@ import "$lib/fonts/material-symbols-rounded.scss" import "$lib/style/scrollbar.scss" import "$lib/style/tippy.scss" + import "$lib/style/toggle.scss" import {onMount} from "svelte" import {applyTheme, argbFromHex, themeFromSourceColor} from "@material/material-color-utilities" - import Navigation from "$lib/components/Navigation.svelte" - import {hasSerialPermission} from "$lib/serial/device" + import Navigation from "./Navigation.svelte" + import {canAutoConnect} from "$lib/serial/device" import {initSerial} from "$lib/serial/connection" - // noinspection TypeScriptCheckImport import {pwaInfo} from "virtual:pwa-info" import type {LayoutServerData} from "./$types" import type {RegisterSWOptions} from "vite-plugin-pwa/types" @@ -18,6 +18,7 @@ import "tippy.js/animations/shift-away.css" import "tippy.js/dist/tippy.css" import tippy from "tippy.js" + import {userPreferences} from "$lib/preferences.js" if (browser) { tippy.setDefaultProps({ @@ -50,7 +51,7 @@ } satisfies RegisterSWOptions) } - if (await hasSerialPermission()) await initSerial() + if ($userPreferences.autoSync && (await canAutoConnect())) await initSerial() }) $: webManifestLink = pwaInfo ? pwaInfo.webManifest.linkTag : "" @@ -69,7 +70,7 @@ -{#if browser && !/Chrome\/[\d.]+(\s(?!Mobile)|$)/.test(navigator.userAgent)} +{#if browser && !("serial" in navigator)} {/if} @@ -83,14 +84,23 @@ color: var(--md-sys-color-tertiary); } + label:has(input):hover, .button:hover:not(:active), a:hover:not(:active), button:hover:not(:active) { filter: brightness(70%); + transition: filter 250ms ease; + &:has(:checked), &.active { filter: brightness(120%); } + + &:disabled, + &.disabled { + opacity: 0.5; + filter: none; + } } body { diff --git a/src/routes/BackupPopup.svelte b/src/routes/BackupPopup.svelte new file mode 100644 index 00000000..ccc0d439 --- /dev/null +++ b/src/routes/BackupPopup.svelte @@ -0,0 +1,105 @@ + + +
+

Backup & Restore

+ +

+ Backups remain on your computer and are never shared or uploaded to our servers. +

+
+ + +
+
+ + diff --git a/src/routes/BrowserWarning.svelte b/src/routes/BrowserWarning.svelte index ca342064..4d6dd147 100644 --- a/src/routes/BrowserWarning.svelte +++ b/src/routes/BrowserWarning.svelte @@ -1,16 +1,25 @@

Warning

- Your current browser is not supported. Due to this site's unique requirement for serial connections, we - require the use of desktop versions of Chromium-based browsers. + Your current browser is not supported due to this site's unique requirement for serial connections. Though all chromium-based desktop browsers fulfill this requirement, some derivations such as + Brave + have been known to disable the API intentionally.

-

Popular options include

ChromiumDownload Chromium +
+
Other popular options include
+
Chrome - Brave Microsoft Edge @@ -45,11 +54,13 @@ } a { + color: var(--md-sys-color-on-error); + } + + div > a { display: flex; gap: 8px; align-items: center; - - color: var(--md-sys-color-on-error); list-style: none; &::before { diff --git a/src/routes/ConnectionPopup.svelte b/src/routes/ConnectionPopup.svelte new file mode 100644 index 00000000..2eda08d1 --- /dev/null +++ b/src/routes/ConnectionPopup.svelte @@ -0,0 +1,102 @@ + + +

Devices

+ +
+ + +
+{#if browser} + {#await ($serialPort, getViablePorts()) then ports} +
+ {#if ports.length === 0} + + {:else if $serialPort} + + {:else} + + {/if} + {#if $serialPort} + + {/if} +
+ {/await} +{/if} + + diff --git a/src/lib/components/Navigation.svelte b/src/routes/Navigation.svelte similarity index 83% rename from src/lib/components/Navigation.svelte rename to src/routes/Navigation.svelte index 007ff382..453b99a7 100644 --- a/src/lib/components/Navigation.svelte +++ b/src/routes/Navigation.svelte @@ -1,8 +1,13 @@ -