mirror of
https://github.com/CharaChorder/DeviceManager.git
synced 2026-01-21 17:32:41 +00:00
stuff
This commit is contained in:
@@ -6,9 +6,7 @@
|
|||||||
overlays = [
|
overlays = [
|
||||||
(final: prev: rec {
|
(final: prev: rec {
|
||||||
nodejs = prev.nodejs-18_x;
|
nodejs = prev.nodejs-18_x;
|
||||||
chrome = prev.google-chrome;
|
chrome = prev.chromium;
|
||||||
firefox = prev.firefox;
|
|
||||||
webkit = prev.epiphany; # Safari-ish browser
|
|
||||||
})
|
})
|
||||||
];
|
];
|
||||||
supportedSystems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ];
|
supportedSystems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ];
|
||||||
@@ -27,12 +25,9 @@
|
|||||||
{
|
{
|
||||||
default = pkgs.mkShell {
|
default = pkgs.mkShell {
|
||||||
packages = with pkgs; [
|
packages = with pkgs; [
|
||||||
node2nix
|
|
||||||
nodejs
|
nodejs
|
||||||
python
|
python
|
||||||
firefox
|
|
||||||
chrome
|
chrome
|
||||||
webkit
|
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
28
src/lib/popup.ts
Normal file
28
src/lib/popup.ts
Normal file
@@ -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<HTMLButtonElement, ComponentType> = (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()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
32
src/lib/preferences.ts
Normal file
32
src/lib/preferences.ts
Normal file
@@ -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<UserPreferences>({
|
||||||
|
backup: false,
|
||||||
|
autoSync: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const preference: Action<HTMLInputElement, keyof UserPreferences> = (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)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import {writable} from "svelte/store"
|
import {get, writable} from "svelte/store"
|
||||||
import {CharaDevice} from "$lib/serial/device"
|
import {CharaDevice} from "$lib/serial/device"
|
||||||
import type {Chord} from "$lib/serial/chord"
|
import type {Chord} from "$lib/serial/chord"
|
||||||
import type {Writable} from "svelte/store"
|
import type {Writable} from "svelte/store"
|
||||||
@@ -26,10 +26,11 @@ export const syncStatus: Writable<"done" | "error" | "downloading" | "uploading"
|
|||||||
let device: CharaDevice // @hmr:keep
|
let device: CharaDevice // @hmr:keep
|
||||||
|
|
||||||
export async function initSerial() {
|
export async function initSerial() {
|
||||||
syncStatus.set("downloading")
|
const device = get(serialPort) ?? new CharaDevice()
|
||||||
device ??= new CharaDevice()
|
await device.ready()
|
||||||
serialPort.set(device)
|
serialPort.set(device)
|
||||||
|
|
||||||
|
syncStatus.set("downloading")
|
||||||
const parsedLayout: CharaLayout = [[], [], []]
|
const parsedLayout: CharaLayout = [[], [], []]
|
||||||
for (let layer = 1; layer <= 3; layer++) {
|
for (let layer = 1; layer <= 3; layer++) {
|
||||||
for (let i = 0; i < 90; i++) {
|
for (let i = 0; i < 90; i++) {
|
||||||
|
|||||||
@@ -5,8 +5,12 @@ import {chordFromCommandCompatible} from "$lib/serial/chord"
|
|||||||
|
|
||||||
export const VENDOR_ID = 0x239a
|
export const VENDOR_ID = 0x239a
|
||||||
|
|
||||||
export async function hasSerialPermission() {
|
export async function getViablePorts(): Promise<SerialPort[]> {
|
||||||
return navigator.serial.getPorts().then(it => it.length > 0)
|
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 {
|
export class CharaDevice {
|
||||||
@@ -22,10 +26,11 @@ export class CharaDevice {
|
|||||||
deviceId: Promise<string>
|
deviceId: Promise<string>
|
||||||
|
|
||||||
constructor(baudRate = 115200) {
|
constructor(baudRate = 115200) {
|
||||||
this.port = navigator.serial.getPorts().then(async ports => {
|
this.port = getViablePorts().then(async ports => {
|
||||||
const port =
|
const port =
|
||||||
ports.find(it => it.getInfo().usbVendorId === VENDOR_ID) ??
|
ports.length === 1
|
||||||
(await navigator.serial.requestPort({filters: [{usbVendorId: VENDOR_ID}]}))
|
? ports[0]
|
||||||
|
: await navigator.serial.requestPort({filters: [{usbVendorId: VENDOR_ID}]})
|
||||||
await port.open({baudRate})
|
await port.open({baudRate})
|
||||||
const info = port.getInfo()
|
const info = port.getInfo()
|
||||||
serialLog.update(it => {
|
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
|
* Read/write to serial port
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,10 +1,20 @@
|
|||||||
import {chords, layout} from "$lib/serial/connection"
|
import {chords, layout} from "$lib/serial/connection"
|
||||||
|
import {userPreferences} from "$lib/preferences"
|
||||||
|
|
||||||
const PROFILE_KEY = "profiles"
|
const PROFILE_KEY = "profiles"
|
||||||
const CHORD_LIBRARY_STORAGE_KEY = "chord-library"
|
const CHORD_LIBRARY_STORAGE_KEY = "chord-library"
|
||||||
const LAYOUT_STORAGE_KEY = "layouts"
|
const LAYOUT_STORAGE_KEY = "layouts"
|
||||||
|
const PREFERENCES = "user-preferences"
|
||||||
|
|
||||||
export function initLocalStorage() {
|
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)
|
const storedLayout = localStorage.getItem(LAYOUT_STORAGE_KEY)
|
||||||
if (storedLayout) {
|
if (storedLayout) {
|
||||||
layout.set(JSON.parse(storedLayout))
|
layout.set(JSON.parse(storedLayout))
|
||||||
|
|||||||
@@ -1,8 +1,21 @@
|
|||||||
|
$padding: 16px;
|
||||||
|
|
||||||
.tippy-box[data-theme~="surface-variant"] {
|
.tippy-box[data-theme~="surface-variant"] {
|
||||||
color: var(--md-sys-color-on-surface-variant);
|
color: var(--md-sys-color-on-surface-variant);
|
||||||
background-color: var(--md-sys-color-surface-variant);
|
background-color: var(--md-sys-color-surface-variant);
|
||||||
|
|
||||||
filter: drop-shadow(0 0 12px #000a);
|
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 {
|
@each $placement in top, bottom, right, left {
|
||||||
&[data-placement^="#{$placement}"] > .tippy-arrow::before {
|
&[data-placement^="#{$placement}"] > .tippy-arrow::before {
|
||||||
|
|||||||
64
src/lib/style/toggle.scss
Normal file
64
src/lib/style/toggle.scss
Normal file
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,12 +3,12 @@
|
|||||||
import "$lib/fonts/material-symbols-rounded.scss"
|
import "$lib/fonts/material-symbols-rounded.scss"
|
||||||
import "$lib/style/scrollbar.scss"
|
import "$lib/style/scrollbar.scss"
|
||||||
import "$lib/style/tippy.scss"
|
import "$lib/style/tippy.scss"
|
||||||
|
import "$lib/style/toggle.scss"
|
||||||
import {onMount} from "svelte"
|
import {onMount} from "svelte"
|
||||||
import {applyTheme, argbFromHex, themeFromSourceColor} from "@material/material-color-utilities"
|
import {applyTheme, argbFromHex, themeFromSourceColor} from "@material/material-color-utilities"
|
||||||
import Navigation from "$lib/components/Navigation.svelte"
|
import Navigation from "./Navigation.svelte"
|
||||||
import {hasSerialPermission} from "$lib/serial/device"
|
import {canAutoConnect} from "$lib/serial/device"
|
||||||
import {initSerial} from "$lib/serial/connection"
|
import {initSerial} from "$lib/serial/connection"
|
||||||
// noinspection TypeScriptCheckImport
|
|
||||||
import {pwaInfo} from "virtual:pwa-info"
|
import {pwaInfo} from "virtual:pwa-info"
|
||||||
import type {LayoutServerData} from "./$types"
|
import type {LayoutServerData} from "./$types"
|
||||||
import type {RegisterSWOptions} from "vite-plugin-pwa/types"
|
import type {RegisterSWOptions} from "vite-plugin-pwa/types"
|
||||||
@@ -18,6 +18,7 @@
|
|||||||
import "tippy.js/animations/shift-away.css"
|
import "tippy.js/animations/shift-away.css"
|
||||||
import "tippy.js/dist/tippy.css"
|
import "tippy.js/dist/tippy.css"
|
||||||
import tippy from "tippy.js"
|
import tippy from "tippy.js"
|
||||||
|
import {userPreferences} from "$lib/preferences.js"
|
||||||
|
|
||||||
if (browser) {
|
if (browser) {
|
||||||
tippy.setDefaultProps({
|
tippy.setDefaultProps({
|
||||||
@@ -50,7 +51,7 @@
|
|||||||
} satisfies RegisterSWOptions)
|
} satisfies RegisterSWOptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (await hasSerialPermission()) await initSerial()
|
if ($userPreferences.autoSync && (await canAutoConnect())) await initSerial()
|
||||||
})
|
})
|
||||||
|
|
||||||
$: webManifestLink = pwaInfo ? pwaInfo.webManifest.linkTag : ""
|
$: webManifestLink = pwaInfo ? pwaInfo.webManifest.linkTag : ""
|
||||||
@@ -69,7 +70,7 @@
|
|||||||
<slot />
|
<slot />
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
{#if browser && !/Chrome\/[\d.]+(\s(?!Mobile)|$)/.test(navigator.userAgent)}
|
{#if browser && !("serial" in navigator)}
|
||||||
<BrowserWarning />
|
<BrowserWarning />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
@@ -83,14 +84,23 @@
|
|||||||
color: var(--md-sys-color-tertiary);
|
color: var(--md-sys-color-tertiary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
label:has(input):hover,
|
||||||
.button:hover:not(:active),
|
.button:hover:not(:active),
|
||||||
a:hover:not(:active),
|
a:hover:not(:active),
|
||||||
button:hover:not(:active) {
|
button:hover:not(:active) {
|
||||||
filter: brightness(70%);
|
filter: brightness(70%);
|
||||||
|
transition: filter 250ms ease;
|
||||||
|
|
||||||
|
&:has(:checked),
|
||||||
&.active {
|
&.active {
|
||||||
filter: brightness(120%);
|
filter: brightness(120%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:disabled,
|
||||||
|
&.disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
filter: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
|
|||||||
105
src/routes/BackupPopup.svelte
Normal file
105
src/routes/BackupPopup.svelte
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {getSharableUrl, parseCompressed, stringifyCompressed} from "$lib/serial/serialization"
|
||||||
|
import {chords, layout} from "$lib/serial/connection"
|
||||||
|
import {preference} from "$lib/preferences"
|
||||||
|
|
||||||
|
async function downloadBackup() {
|
||||||
|
const downloadUrl = URL.createObjectURL(
|
||||||
|
await stringifyCompressed({
|
||||||
|
isCharaBackup: "v1.0",
|
||||||
|
chords: $chords,
|
||||||
|
layout: $layout,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
const element = document.createElement("a")
|
||||||
|
element.setAttribute("download", "chords.chb")
|
||||||
|
element.href = downloadUrl
|
||||||
|
element.setAttribute("target", "_blank")
|
||||||
|
element.click()
|
||||||
|
URL.revokeObjectURL(downloadUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function restoreBackup(event: InputEvent) {
|
||||||
|
const input = (event.target as HTMLInputElement).files![0]
|
||||||
|
if (!input) return
|
||||||
|
const backup = await parseCompressed(input)
|
||||||
|
if (backup.isCharaBackup !== "v1.0") throw new Error("Invalid Backup")
|
||||||
|
if (backup.chords) {
|
||||||
|
$chords = backup.chords
|
||||||
|
}
|
||||||
|
if (backup.layout) {
|
||||||
|
$layout = backup.layout
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createShareUrl() {
|
||||||
|
console.log(await getSharableUrl("chords", $chords))
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Backup & Restore</h2>
|
||||||
|
<label><input type="checkbox" use:preference={"backup"} />Local backups</label>
|
||||||
|
<p class="disclaimer">
|
||||||
|
<i>Backups remain on your computer and are never shared or uploaded to our servers.</i>
|
||||||
|
</p>
|
||||||
|
<div class="save">
|
||||||
|
<button class="primary" on:click={downloadBackup}><span class="icon">save</span> Download Backup</button>
|
||||||
|
<label class="button"
|
||||||
|
><input on:input={restoreBackup} type="file" /><span class="icon">settings_backup_restore</span> Restore</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.disclaimer {
|
||||||
|
max-width: 16cm;
|
||||||
|
font-size: 12px;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="file"] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button,
|
||||||
|
button {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
height: 48px;
|
||||||
|
padding-block: 8px;
|
||||||
|
padding-inline: 16px;
|
||||||
|
|
||||||
|
font-family: "Noto Sans Mono", monospace;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--md-sys-color-on-background);
|
||||||
|
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-radius: 32px;
|
||||||
|
|
||||||
|
transition: all 250ms ease;
|
||||||
|
|
||||||
|
&.primary {
|
||||||
|
color: var(--md-sys-color-on-primary);
|
||||||
|
background: var(--md-sys-color-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,16 +1,25 @@
|
|||||||
<dialog open>
|
<dialog open>
|
||||||
<h1>Warning</h1>
|
<h1>Warning</h1>
|
||||||
<p>
|
<p>
|
||||||
Your current browser is not supported. Due to this site's unique requirement for serial connections, we
|
Your current browser is not supported due to this site's unique requirement for <a
|
||||||
require the use of <b>desktop</b> versions of <b>Chromium-based</b> browsers.
|
class="normal"
|
||||||
|
target="_blank"
|
||||||
|
href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Serial_API#browser_compatibility"
|
||||||
|
>serial connections</a
|
||||||
|
>. Though all <b>chromium-based desktop</b> browsers fulfill this requirement, some derivations such as
|
||||||
|
Brave
|
||||||
|
<a href="https://github.com/brave/brave-browser/issues/13902" target="_blank"
|
||||||
|
>have been known to disable the API intentionally</a
|
||||||
|
>.
|
||||||
</p>
|
</p>
|
||||||
<p>Popular options include</p>
|
|
||||||
<div>
|
<div>
|
||||||
<a href="https://www.chromium.org/getting-involved/download-chromium/" target="_blank" class="chrome"
|
<a href="https://www.chromium.org/getting-involved/download-chromium/" target="_blank" class="chrome"
|
||||||
>Chromium</a
|
>Download Chromium</a
|
||||||
>
|
>
|
||||||
|
</div>
|
||||||
|
<h5>Other popular options include</h5>
|
||||||
|
<div>
|
||||||
<a href="https://www.google.com/chrome/" target="_blank" class="chrome">Chrome</a>
|
<a href="https://www.google.com/chrome/" target="_blank" class="chrome">Chrome</a>
|
||||||
<a href="https://brave.com/" target="_blank" class="brave">Brave</a>
|
|
||||||
<a href="https://www.microsoft.com/en-us/edge/download?form=MA13FJ" target="_blank" class="edge"
|
<a href="https://www.microsoft.com/en-us/edge/download?form=MA13FJ" target="_blank" class="edge"
|
||||||
>Microsoft Edge</a
|
>Microsoft Edge</a
|
||||||
>
|
>
|
||||||
@@ -45,11 +54,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
|
color: var(--md-sys-color-on-error);
|
||||||
|
}
|
||||||
|
|
||||||
|
div > a {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
color: var(--md-sys-color-on-error);
|
|
||||||
list-style: none;
|
list-style: none;
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
|
|||||||
102
src/routes/ConnectionPopup.svelte
Normal file
102
src/routes/ConnectionPopup.svelte
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
<script>
|
||||||
|
import {initSerial, serialPort} from "$lib/serial/connection"
|
||||||
|
import {browser} from "$app/environment"
|
||||||
|
import {getViablePorts} from "$lib/serial/device"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<h2>Devices</h2>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<button disabled={$serialPort === undefined}><span class="icon">restart_alt</span>Reboot</button>
|
||||||
|
<button disabled={$serialPort === undefined}><span class="icon">rule_settings</span>Bootloader</button>
|
||||||
|
</div>
|
||||||
|
{#if browser}
|
||||||
|
{#await ($serialPort, getViablePorts()) then ports}
|
||||||
|
<div class="row">
|
||||||
|
{#if ports.length === 0}
|
||||||
|
<button class="secondary" on:click={initSerial}>
|
||||||
|
<span class="icon">usb</span>Pair
|
||||||
|
</button>
|
||||||
|
{:else if $serialPort}
|
||||||
|
<button
|
||||||
|
class="secondary"
|
||||||
|
on:click={() => {
|
||||||
|
$serialPort.forget()
|
||||||
|
$serialPort = undefined
|
||||||
|
}}><span class="icon">usb</span>Unpair</button
|
||||||
|
>
|
||||||
|
{:else}
|
||||||
|
<button class="secondary" on:click={initSerial}><span class="icon">usb</span>Connect</button>
|
||||||
|
{/if}
|
||||||
|
{#if $serialPort}
|
||||||
|
<button
|
||||||
|
on:click={() => {
|
||||||
|
$serialPort.disconnect()
|
||||||
|
$serialPort = undefined
|
||||||
|
}}><span class="icon">usb</span>Disconnect</button
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/await}
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
height: fit-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
height: 48px;
|
||||||
|
padding: 8px;
|
||||||
|
padding-inline-end: 16px;
|
||||||
|
|
||||||
|
font-size: 1rem;
|
||||||
|
color: var(--md-sys-color-on-background);
|
||||||
|
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-radius: 32px;
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active:not(:disabled) {
|
||||||
|
color: var(--md-sys-color-on-surface-variant);
|
||||||
|
background: var(--md-sys-color-surface-variant);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-grid {
|
||||||
|
contain: size;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-grow: 1;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,8 +1,13 @@
|
|||||||
<script>
|
<script lang="ts">
|
||||||
import {serialPort, syncStatus} from "$lib/serial/connection"
|
import {serialPort, syncStatus} from "$lib/serial/connection"
|
||||||
import {page} from "$app/stores"
|
import {page} from "$app/stores"
|
||||||
import {slide, fly} from "svelte/transition"
|
import {slide, fly} from "svelte/transition"
|
||||||
import {canShare, triggerShare} from "$lib/share"
|
import {canShare, triggerShare} from "$lib/share"
|
||||||
|
import {popup} from "$lib/popup"
|
||||||
|
import BackupPopup from "./BackupPopup.svelte"
|
||||||
|
import ConnectionPopup from "./ConnectionPopup.svelte"
|
||||||
|
import {canAutoConnect} from "$lib/serial/device"
|
||||||
|
import {browser} from "$app/environment"
|
||||||
|
|
||||||
const training = [
|
const training = [
|
||||||
{slug: "cpm", title: "CPM - Characters Per Minute", icon: "music_note"},
|
{slug: "cpm", title: "CPM - Characters Per Minute", icon: "music_note"},
|
||||||
@@ -12,6 +17,12 @@
|
|||||||
{slug: "top-wpm", title: "tWPM - Top Words Per Minute", icon: "speed"},
|
{slug: "top-wpm", title: "tWPM - Top Words Per Minute", icon: "speed"},
|
||||||
{slug: "cm", title: "CM - Concepts Mastered", icon: "cognition"},
|
{slug: "cm", title: "CM - Concepts Mastered", icon: "cognition"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
$: if (browser && !canAutoConnect()) {
|
||||||
|
connectButton?.click()
|
||||||
|
}
|
||||||
|
|
||||||
|
let connectButton: HTMLButtonElement
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<nav>
|
<nav>
|
||||||
@@ -36,29 +47,26 @@
|
|||||||
{#await import("$lib/components/PwaStatus.svelte") then { default: PwaStatus }}
|
{#await import("$lib/components/PwaStatus.svelte") then { default: PwaStatus }}
|
||||||
<PwaStatus />
|
<PwaStatus />
|
||||||
{/await}
|
{/await}
|
||||||
<a
|
{#if $serialPort}
|
||||||
title="Backup & Restore"
|
<button title="Backup & Restore" use:popup={BackupPopup} class="icon {$syncStatus}">
|
||||||
href="/backup/"
|
{#if $syncStatus === "downloading"}
|
||||||
class="icon {$syncStatus}"
|
backup
|
||||||
class:active={$page.url.pathname.startsWith("/backup/")}
|
{:else if $syncStatus === "uploading"}
|
||||||
>
|
cloud_download
|
||||||
{#if $syncStatus === "downloading"}
|
{:else}
|
||||||
backup
|
cloud_done
|
||||||
{:else if $syncStatus === "uploading"}
|
{/if}
|
||||||
cloud_download
|
</button>
|
||||||
{:else}
|
{/if}
|
||||||
cloud_done
|
<button
|
||||||
{/if}
|
bind:this={connectButton}
|
||||||
</a>
|
title="Devices"
|
||||||
<a
|
use:popup={ConnectionPopup}
|
||||||
href="/config/"
|
|
||||||
title="Device Manager"
|
|
||||||
class="icon connect"
|
class="icon connect"
|
||||||
class:active={$page.url.pathname.startsWith("/config/")}
|
|
||||||
class:error={$serialPort === undefined}
|
class:error={$serialPort === undefined}
|
||||||
>
|
>
|
||||||
cable
|
cable
|
||||||
</a>
|
</button>
|
||||||
<a href="/stats/" title="Statistics" class="icon account">person</a>
|
<a href="/stats/" title="Statistics" class="icon account">person</a>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
Reference in New Issue
Block a user