mirror of
https://github.com/CharaChorder/DeviceManager.git
synced 2026-01-21 09:23:00 +00:00
stuff
This commit is contained in:
@@ -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 @@
|
||||
<slot />
|
||||
</main>
|
||||
|
||||
{#if browser && !/Chrome\/[\d.]+(\s(?!Mobile)|$)/.test(navigator.userAgent)}
|
||||
{#if browser && !("serial" in navigator)}
|
||||
<BrowserWarning />
|
||||
{/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 {
|
||||
|
||||
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>
|
||||
<h1>Warning</h1>
|
||||
<p>
|
||||
Your current browser is not supported. Due to this site's unique requirement for serial connections, we
|
||||
require the use of <b>desktop</b> versions of <b>Chromium-based</b> browsers.
|
||||
Your current browser is not supported due to this site's unique requirement for <a
|
||||
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>Popular options include</p>
|
||||
<div>
|
||||
<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://brave.com/" target="_blank" class="brave">Brave</a>
|
||||
<a href="https://www.microsoft.com/en-us/edge/download?form=MA13FJ" target="_blank" class="edge"
|
||||
>Microsoft Edge</a
|
||||
>
|
||||
@@ -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 {
|
||||
|
||||
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>
|
||||
226
src/routes/Navigation.svelte
Normal file
226
src/routes/Navigation.svelte
Normal file
@@ -0,0 +1,226 @@
|
||||
<script lang="ts">
|
||||
import {serialPort, syncStatus} from "$lib/serial/connection"
|
||||
import {page} from "$app/stores"
|
||||
import {slide, fly} from "svelte/transition"
|
||||
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 = [
|
||||
{slug: "cpm", title: "CPM - Characters Per Minute", icon: "music_note"},
|
||||
{slug: "chords", title: "ChM - Chords Mastered", icon: "piano"},
|
||||
{slug: "avg-wpm", title: "aWPM - Average Words Per Minute", icon: "avg_pace"},
|
||||
{slug: "sentences", title: "StM - Sentences Mastered", icon: "lyrics"},
|
||||
{slug: "top-wpm", title: "tWPM - Top Words Per Minute", icon: "speed"},
|
||||
{slug: "cm", title: "CM - Concepts Mastered", icon: "cognition"},
|
||||
]
|
||||
|
||||
$: if (browser && !canAutoConnect()) {
|
||||
connectButton?.click()
|
||||
}
|
||||
|
||||
let connectButton: HTMLButtonElement
|
||||
</script>
|
||||
|
||||
<nav>
|
||||
<a href="/" class="title">amaCC1ng</a>
|
||||
|
||||
<div class="steps">
|
||||
{#each training as { slug, title, icon }}
|
||||
<a
|
||||
href="/train/{slug}/"
|
||||
{title}
|
||||
class="icon train {slug}"
|
||||
class:active={$page.url.pathname === `/train/${slug}/`}>{icon}</a
|
||||
>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
{#if $canShare}
|
||||
<a transition:fly={{x: -8}} class="icon" on:click={triggerShare}>share</a>
|
||||
<div transition:slide class="separator" />
|
||||
{/if}
|
||||
{#await import("$lib/components/PwaStatus.svelte") then { default: PwaStatus }}
|
||||
<PwaStatus />
|
||||
{/await}
|
||||
{#if $serialPort}
|
||||
<button title="Backup & Restore" use:popup={BackupPopup} class="icon {$syncStatus}">
|
||||
{#if $syncStatus === "downloading"}
|
||||
backup
|
||||
{:else if $syncStatus === "uploading"}
|
||||
cloud_download
|
||||
{:else}
|
||||
cloud_done
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
<button
|
||||
bind:this={connectButton}
|
||||
title="Devices"
|
||||
use:popup={ConnectionPopup}
|
||||
class="icon connect"
|
||||
class:error={$serialPort === undefined}
|
||||
>
|
||||
cable
|
||||
</button>
|
||||
<a href="/stats/" title="Statistics" class="icon account">person</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<style lang="scss">
|
||||
@keyframes sync {
|
||||
0% {
|
||||
scale: 1 1;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
85% {
|
||||
scale: 1 0;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
86% {
|
||||
scale: 1 1;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
scale: 1 1;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.uploading::after,
|
||||
.downloading::after {
|
||||
content: "";
|
||||
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
left: 50%;
|
||||
transform-origin: top;
|
||||
translate: -50% 0;
|
||||
|
||||
width: 8px;
|
||||
height: 10px;
|
||||
|
||||
background: var(--md-sys-color-background);
|
||||
|
||||
animation: sync 1s linear infinite;
|
||||
}
|
||||
|
||||
.downloading.active::after,
|
||||
.uploading.active::after {
|
||||
background: var(--md-sys-color-primary);
|
||||
}
|
||||
|
||||
.sync.downloading::after {
|
||||
top: 10px;
|
||||
transform-origin: bottom;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.separator {
|
||||
width: 1px;
|
||||
height: 24px;
|
||||
background: var(--md-sys-color-outline-variant);
|
||||
}
|
||||
|
||||
nav {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
margin-block: 8px;
|
||||
margin-inline: 16px;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-block: 0;
|
||||
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
color: var(--md-sys-color-primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.icon {
|
||||
cursor: pointer;
|
||||
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
aspect-ratio: 1;
|
||||
padding: 4px;
|
||||
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
|
||||
transition: all 250ms ease;
|
||||
|
||||
&.error {
|
||||
color: var(--md-sys-color-on-error);
|
||||
background: var(--md-sys-color-error);
|
||||
}
|
||||
|
||||
&.active,
|
||||
&:active {
|
||||
color: var(--md-sys-color-on-primary);
|
||||
background: var(--md-sys-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.steps {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
translate: -50% 0;
|
||||
display: flex;
|
||||
|
||||
> a.icon {
|
||||
aspect-ratio: unset;
|
||||
margin-inline: -4px;
|
||||
padding-inline: 16px;
|
||||
|
||||
font-size: 24px;
|
||||
color: var(--md-sys-on-surface-variant);
|
||||
|
||||
background: var(--md-sys-color-surface-variant);
|
||||
clip-path: polygon(25% 50%, 0% 0%, 75% 0%, 100% 50%, 75% 100%, 0% 100%);
|
||||
border-radius: 0;
|
||||
|
||||
&.active,
|
||||
&:active {
|
||||
color: var(--md-sys-color-on-tertiary);
|
||||
background: var(--md-sys-color-tertiary);
|
||||
|
||||
&,
|
||||
~ * {
|
||||
translate: 8px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.icon.account {
|
||||
font-size: 32px;
|
||||
color: var(--md-sys-color-on-secondary-container);
|
||||
background: var(--md-sys-color-secondary-container);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user