refactor: use standard prettier formatting

This commit is contained in:
2024-04-06 13:15:35 +02:00
parent 86ec8651b6
commit 854ab6d3be
106 changed files with 2703 additions and 2046 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
<script>
import {page} from "$app/stores"
import { page } from "$app/stores";
</script>
<h1>{$page.status}</h1>
+7 -3
View File
@@ -1,8 +1,12 @@
import {themeBase, themeColor, themeSuccessBase} from "$lib/style/theme.server"
import type {LayoutServerLoad} from "./$types"
import {
themeBase,
themeColor,
themeSuccessBase,
} from "$lib/style/theme.server";
import type { LayoutServerLoad } from "./$types";
export const load = (async () => ({
themeSuccessBase,
themeBase,
themeColor,
})) satisfies LayoutServerLoad
})) satisfies LayoutServerLoad;
+54 -49
View File
@@ -1,38 +1,43 @@
<script lang="ts">
import "$lib/fonts/noto-sans-mono.scss"
import "$lib/fonts/material-symbols-rounded.scss"
import "$lib/style/scrollbar.scss"
import "$lib/style/tippy.scss"
import "$lib/style/theme.scss"
import {onDestroy, onMount} from "svelte"
import {applyTheme, argbFromHex, themeFromSourceColor} from "@material/material-color-utilities"
import Navigation from "./Navigation.svelte"
import {canAutoConnect} from "$lib/serial/device"
import {initSerial} from "$lib/serial/connection"
import type {LayoutData} from "./$types"
import {browser} from "$app/environment"
import BrowserWarning from "./BrowserWarning.svelte"
import "tippy.js/animations/shift-away.css"
import "tippy.js/dist/tippy.css"
import tippy from "tippy.js"
import {theme, userPreferences} from "$lib/preferences.js"
import {LL, setLocale} from "../i18n/i18n-svelte"
import {loadLocale} from "../i18n/i18n-util.sync"
import {detectLocale} from "../i18n/i18n-util"
import type {Locales} from "../i18n/i18n-types"
import Footer from "./Footer.svelte"
import {runLayoutDetection} from "$lib/os-layout.js"
import PageTransition from "./PageTransition.svelte"
import {restoreFromFile} from "$lib/backup/backup"
import {goto} from "$app/navigation"
import "$lib/fonts/noto-sans-mono.scss";
import "$lib/fonts/material-symbols-rounded.scss";
import "$lib/style/scrollbar.scss";
import "$lib/style/tippy.scss";
import "$lib/style/theme.scss";
import { onDestroy, onMount } from "svelte";
import {
applyTheme,
argbFromHex,
themeFromSourceColor,
} from "@material/material-color-utilities";
import Navigation from "./Navigation.svelte";
import { canAutoConnect } from "$lib/serial/device";
import { initSerial } from "$lib/serial/connection";
import type { LayoutData } from "./$types";
import { browser } from "$app/environment";
import BrowserWarning from "./BrowserWarning.svelte";
import "tippy.js/animations/shift-away.css";
import "tippy.js/dist/tippy.css";
import tippy from "tippy.js";
import { theme, userPreferences } from "$lib/preferences.js";
import { LL, setLocale } from "../i18n/i18n-svelte";
import { loadLocale } from "../i18n/i18n-util.sync";
import { detectLocale } from "../i18n/i18n-util";
import type { Locales } from "../i18n/i18n-types";
import Footer from "./Footer.svelte";
import { runLayoutDetection } from "$lib/os-layout.js";
import PageTransition from "./PageTransition.svelte";
import { restoreFromFile } from "$lib/backup/backup";
import { goto } from "$app/navigation";
const locale = ((browser && localStorage.getItem("locale")) as Locales) || detectLocale()
loadLocale(locale)
setLocale(locale)
let stopLayoutDetection: () => void
const locale =
((browser && localStorage.getItem("locale")) as Locales) || detectLocale();
loadLocale(locale);
setLocale(locale);
let stopLayoutDetection: () => void;
if (browser) {
stopLayoutDetection = runLayoutDetection()
stopLayoutDetection = runLayoutDetection();
tippy.setDefaultProps({
animation: "shift-away",
theme: "surface-variant",
@@ -40,38 +45,38 @@
duration: 250,
maxWidth: "none",
arrow: true,
})
});
}
export let data: LayoutData
export let data: LayoutData;
onMount(async () => {
theme.subscribe(it => {
const theme = themeFromSourceColor(argbFromHex(it.color))
const dark = it.mode === "dark" // window.matchMedia("(prefers-color-scheme: dark)").matches
applyTheme(theme, {target: document.body, dark})
})
theme.subscribe((it) => {
const theme = themeFromSourceColor(argbFromHex(it.color));
const dark = it.mode === "dark"; // window.matchMedia("(prefers-color-scheme: dark)").matches
applyTheme(theme, { target: document.body, dark });
});
if (import.meta.env.TAURI_FAMILY === undefined) {
const {initPwa} = await import("./pwa-setup")
webManifestLink = await initPwa()
const { initPwa } = await import("./pwa-setup");
webManifestLink = await initPwa();
}
if (browser && $userPreferences.autoConnect && (await canAutoConnect())) {
await initSerial()
await initSerial();
}
if (data.importFile) {
restoreFromFile(data.importFile)
const url = new URL(location.href)
url.searchParams.delete("import")
await goto(url.href, {replaceState: true})
restoreFromFile(data.importFile);
const url = new URL(location.href);
url.searchParams.delete("import");
await goto(url.href, { replaceState: true });
}
})
});
onDestroy(() => {
stopLayoutDetection?.()
})
stopLayoutDetection?.();
});
let webManifestLink = ""
let webManifestLink = "";
</script>
<svelte:head>
+12 -10
View File
@@ -1,14 +1,16 @@
import type {LayoutLoad} from "./$types"
import {browser} from "$app/environment"
import {charaFileFromUriComponent} from "$lib/share/share-url"
import type { LayoutLoad } from "./$types";
import { browser } from "$app/environment";
import { charaFileFromUriComponent } from "$lib/share/share-url";
export const prerender = true
export const trailingSlash = "always"
export const prerender = true;
export const trailingSlash = "always";
export const load = (async ({url, data, fetch}) => {
const importFile = browser && new URLSearchParams(url.search).get("import")
export const load = (async ({ url, data, fetch }) => {
const importFile = browser && new URLSearchParams(url.search).get("import");
return {
...data,
importFile: importFile ? await charaFileFromUriComponent(importFile, fetch) : undefined,
}
}) satisfies LayoutLoad
importFile: importFile
? await charaFileFromUriComponent(importFile, fetch)
: undefined,
};
}) satisfies LayoutLoad;
+4 -4
View File
@@ -1,6 +1,6 @@
import {redirect} from "@sveltejs/kit"
import type {PageLoad} from "./$types"
import { redirect } from "@sveltejs/kit";
import type { PageLoad } from "./$types";
export const load = (() => {
throw redirect(302, "/config/")
}) satisfies PageLoad
throw redirect(302, "/config/");
}) satisfies PageLoad;
+13 -5
View File
@@ -1,6 +1,6 @@
<script lang="ts">
import {preference} from "$lib/preferences"
import LL from "../i18n/i18n-svelte"
import { preference } from "$lib/preferences";
import LL from "../i18n/i18n-svelte";
import {
createChordBackup,
createLayoutBackup,
@@ -8,11 +8,18 @@
downloadBackup,
downloadFile,
restoreBackup,
} from "$lib/backup/backup"
} from "$lib/backup/backup";
</script>
<section>
<h2><label><input type="checkbox" use:preference={"backup"} />{$LL.backup.TITLE()}</label></h2>
<h2>
<label
><input
type="checkbox"
use:preference={"backup"}
/>{$LL.backup.TITLE()}</label
>
</h2>
<p class="disclaimer">
<i>{$LL.backup.DISCLAIMER()}</i>
</p>
@@ -36,7 +43,8 @@
><span class="icon">download</span>{$LL.backup.DOWNLOAD()}</button
>
<label class="button"
><input on:input={restoreBackup} type="file" /><span class="icon">settings_backup_restore</span
><input on:input={restoreBackup} type="file" /><span class="icon"
>settings_backup_restore</span
>{$LL.backup.RESTORE()}</label
>
</div>
+4 -3
View File
@@ -1,5 +1,5 @@
<script>
import LL from "../i18n/i18n-svelte"
import LL from "../i18n/i18n-svelte";
</script>
<dialog open>
@@ -12,8 +12,9 @@
>{$LL.browserWarning.INFO_SERIAL_INFIX()}</a
>{$LL.browserWarning.INFO_SERIAL_SUFFIX()}
{$LL.browserWarning.INFO_BROWSER_PREFIX()}
<a href="https://github.com/brave/brave-browser/issues/13902" target="_blank"
>{$LL.browserWarning.INFO_BROWSER_INFIX()}</a
<a
href="https://github.com/brave/brave-browser/issues/13902"
target="_blank">{$LL.browserWarning.INFO_BROWSER_INFIX()}</a
>{$LL.browserWarning.INFO_BROWSER_SUFFIX()}
</p>
<div>
+24 -8
View File
@@ -1,18 +1,34 @@
<script>
import {page} from "$app/stores"
import {action} from "$lib/title"
import LL from "../i18n/i18n-svelte"
import { page } from "$app/stores";
import { action } from "$lib/title";
import LL from "../i18n/i18n-svelte";
$: paths = [
{href: "/config/chords/", title: $LL.configure.chords.TITLE(), icon: "piano"},
{href: "/config/layout/", title: $LL.configure.layout.TITLE(), icon: "keyboard"},
{href: "/config/settings/", title: $LL.configure.settings.TITLE(), icon: "settings"},
]
{
href: "/config/chords/",
title: $LL.configure.chords.TITLE(),
icon: "piano",
},
{
href: "/config/layout/",
title: $LL.configure.layout.TITLE(),
icon: "keyboard",
},
{
href: "/config/settings/",
title: $LL.configure.settings.TITLE(),
icon: "settings",
},
];
</script>
<nav>
{#each paths as { href, title, icon }, i}
<a {href} class:active={$page.url.pathname.startsWith(href)} use:action={{shortcut: `shift+${i + 1}`}}>
<a
{href}
class:active={$page.url.pathname.startsWith(href)}
use:action={{ shortcut: `shift+${i + 1}` }}
>
<span class="icon">{icon}</span>
{title}
</a>
+60 -39
View File
@@ -1,48 +1,56 @@
<script lang="ts">
import {initSerial, serialPort} from "$lib/serial/connection"
import {browser} from "$app/environment"
import {slide, fade} from "svelte/transition"
import {preference} from "$lib/preferences"
import LL from "../i18n/i18n-svelte"
import {downloadBackup} from "$lib/backup/backup"
import { initSerial, serialPort } from "$lib/serial/connection";
import { browser } from "$app/environment";
import { slide, fade } from "svelte/transition";
import { preference } from "$lib/preferences";
import LL from "../i18n/i18n-svelte";
import { downloadBackup } from "$lib/backup/backup";
function reboot() {
$serialPort?.reboot()
$serialPort = undefined
powerDialog = false
$serialPort?.reboot();
$serialPort = undefined;
powerDialog = false;
setTimeout(() => {
initSerial()
}, 1000)
initSerial();
}, 1000);
}
function bootloader() {
downloadBackup()
$serialPort?.bootloader()
$serialPort = undefined
rebootInfo = true
powerDialog = false
downloadBackup();
$serialPort?.bootloader();
$serialPort = undefined;
rebootInfo = true;
powerDialog = false;
}
async function updateFirmware() {
const {usbVendorId: vendorId, usbProductId: productId} = $serialPort!.portInfo
$serialPort!.bootloader()
await new Promise(resolve => setTimeout(resolve, 1000))
console.log(await navigator.usb.requestDevice({filters: [{vendorId, productId}]}))
const { usbVendorId: vendorId, usbProductId: productId } =
$serialPort!.portInfo;
$serialPort!.bootloader();
await new Promise((resolve) => setTimeout(resolve, 1000));
console.log(
await navigator.usb.requestDevice({ filters: [{ vendorId, productId }] }),
);
}
let rebootInfo = false
let terminal = false
let powerDialog = false
let rebootInfo = false;
let terminal = false;
let powerDialog = false;
$: if ($serialPort) {
rebootInfo = false
rebootInfo = false;
}
</script>
<section>
<div class="row">
<h2>{$LL.deviceManager.TITLE()}</h2>
<label>{$LL.deviceManager.AUTO_CONNECT()}<input type="checkbox" use:preference={"autoConnect"} /></label>
<label
>{$LL.deviceManager.AUTO_CONNECT()}<input
type="checkbox"
use:preference={"autoConnect"}
/></label
>
</div>
{#if $serialPort}
@@ -54,7 +62,8 @@
Version {$serialPort.version}
</p>
{#if $serialPort.version.toString() !== import.meta.env.VITE_LATEST_FIRMWARE}
<a href="https://docs.charachorder.com/CharaChorder%20One.html#updating-the-firmware"
<a
href="https://docs.charachorder.com/CharaChorder%20One.html#updating-the-firmware"
>Firmware Update Instructions</a
>
{/if}
@@ -66,14 +75,18 @@
<details class="linux-info" transition:slide>
<summary>{@html $LL.deviceManager.LINUX_PERMISSIONS()}</summary>
In most cases you can simply follow the
<a target="_blank" href="https://docs.arduino.cc/software/ide-v1/tutorials/Linux#please-read"
<a
target="_blank"
href="https://docs.arduino.cc/software/ide-v1/tutorials/Linux#please-read"
>Arduino Guide</a
>
on serial port permissions.
<p>Special systems:</p>
<ul>
<li>
<a target="_blank" href="https://wiki.archlinux.org/title/Arduino#Accessing_serial"
<a
target="_blank"
href="https://wiki.archlinux.org/title/Arduino#Accessing_serial"
>Arch and Arch-based like Manjaro or EndeavourOS</a
>
</li>
@@ -85,7 +98,9 @@
>
</li>
<li>
<a target="_blank" href="https://wiki.gentoo.org/wiki/Arduino#Grant_access_to_non-root_users"
<a
target="_blank"
href="https://wiki.gentoo.org/wiki/Arduino#Grant_access_to_non-root_users"
>Gentoo</a
>
</li>
@@ -93,16 +108,20 @@
</details>
{/if}
{#if rebootInfo}
<p transition:slide><b>{$LL.deviceManager.bootMenu.POWER_WARNING()}</b></p>
<p transition:slide>
<b>{$LL.deviceManager.bootMenu.POWER_WARNING()}</b>
</p>
{/if}
<div class="row">
{#if $serialPort}
<button
class="secondary"
on:click={() => {
$serialPort?.forget()
$serialPort = undefined
}}><span class="icon">usb_off</span>{$LL.deviceManager.DISCONNECT()}</button
$serialPort?.forget();
$serialPort = undefined;
}}
><span class="icon">usb_off</span
>{$LL.deviceManager.DISCONNECT()}</button
>
{:else}
<button class="error" on:click={() => initSerial(true)}
@@ -130,19 +149,21 @@
class="backdrop"
role="button"
tabindex="-1"
transition:fade={{duration: 250}}
transition:fade={{ duration: 250 }}
on:click={() => (powerDialog = !powerDialog)}
on:keypress={event => {
if (event.key === "Enter") powerDialog = !powerDialog
on:keypress={(event) => {
if (event.key === "Enter") powerDialog = !powerDialog;
}}
/>
<dialog open transition:slide={{duration: 250}}>
<dialog open transition:slide={{ duration: 250 }}>
<h3>{$LL.deviceManager.bootMenu.TITLE()}</h3>
<button on:click={reboot}
><span class="icon">restart_alt</span>{$LL.deviceManager.bootMenu.REBOOT()}</button
><span class="icon">restart_alt</span
>{$LL.deviceManager.bootMenu.REBOOT()}</button
>
<button on:click={bootloader}
><span class="icon">rule_settings</span>{$LL.deviceManager.bootMenu.BOOTLOADER()}</button
><span class="icon">rule_settings</span
>{$LL.deviceManager.bootMenu.BOOTLOADER()}</button
>
</dialog>
{/if}
+75 -59
View File
@@ -1,9 +1,16 @@
<script lang="ts">
import LL from "../i18n/i18n-svelte"
import {changes, ChangeType, chords, layout, overlay, settings} from "$lib/undo-redo"
import type {Change} from "$lib/undo-redo"
import {fly} from "svelte/transition"
import {action} from "$lib/title"
import LL from "../i18n/i18n-svelte";
import {
changes,
ChangeType,
chords,
layout,
overlay,
settings,
} from "$lib/undo-redo";
import type { Change } from "$lib/undo-redo";
import { fly } from "svelte/transition";
import { action } from "$lib/title";
import {
deviceChords,
deviceLayout,
@@ -11,72 +18,80 @@
serialPort,
syncProgress,
syncStatus,
} from "$lib/serial/connection"
import {askForConfirmation} from "$lib/dialogs/confirm-dialog"
import {KEYMAP_CODES} from "$lib/serial/keymap-codes"
} from "$lib/serial/connection";
import { askForConfirmation } from "$lib/dialogs/confirm-dialog";
import { KEYMAP_CODES } from "$lib/serial/keymap-codes";
function undo(event: MouseEvent) {
if (event.shiftKey) {
changes.set([])
changes.set([]);
} else {
redoQueue = [$changes.pop()!, ...redoQueue]
changes.update(it => it)
redoQueue = [$changes.pop()!, ...redoQueue];
changes.update((it) => it);
}
}
function redo() {
const [change, ...queue] = redoQueue
changes.update(it => {
it.push(change)
return it
})
redoQueue = queue
const [change, ...queue] = redoQueue;
changes.update((it) => {
it.push(change);
return it;
});
redoQueue = queue;
}
let redoQueue: Change[] = []
let redoQueue: Change[] = [];
async function save() {
try {
const port = $serialPort
if (!port) return
$syncStatus = "uploading"
const port = $serialPort;
if (!port) return;
$syncStatus = "uploading";
for (const [id, {actions, phrase, deleted}] of $overlay.chords) {
for (const [id, { actions, phrase, deleted }] of $overlay.chords) {
if (!deleted) {
if (id !== JSON.stringify(actions)) {
const existingChord = await port.getChordPhrase(actions)
const existingChord = await port.getChordPhrase(actions);
if (
existingChord !== undefined &&
!(await askForConfirmation(
$LL.configure.chords.conflict.TITLE(),
$LL.configure.chords.conflict.DESCRIPTION(
actions.map(it => `<kbd>${KEYMAP_CODES[it].id}</kbd>`).join(" "),
actions
.map((it) => `<kbd>${KEYMAP_CODES[it].id}</kbd>`)
.join(" "),
),
$LL.configure.chords.conflict.CONFIRM(),
$LL.configure.chords.conflict.ABORT(),
))
) {
changes.update(changes =>
changes.filter(it => !(it.type === ChangeType.Chord && JSON.stringify(it.id) === id)),
)
continue
changes.update((changes) =>
changes.filter(
(it) =>
!(
it.type === ChangeType.Chord &&
JSON.stringify(it.id) === id
),
),
);
continue;
}
await port.deleteChord({actions: JSON.parse(id)})
await port.deleteChord({ actions: JSON.parse(id) });
}
await port.setChord({actions, phrase})
await port.setChord({ actions, phrase });
} else {
await port.deleteChord({actions})
await port.deleteChord({ actions });
}
}
for (const [layer, actions] of $overlay.layout.entries()) {
for (const [id, action] of actions) {
await port.setLayoutKey(layer + 1, id, action)
await port.setLayoutKey(layer + 1, id, action);
}
}
for (const [id, setting] of $overlay.settings) {
await port.setSetting(id, setting)
await port.setSetting(id, setting);
}
// Yes, this is a completely arbitrary and unnecessary delay.
@@ -86,60 +101,61 @@
// would be if they click it every time they change a setting.
// Because of that, we don't need to show a fearmongering message such as
// "Your device will break after you click this 10,000 times!"
const virtualWriteTime = 1000
const startStamp = performance.now()
await new Promise<void>(resolve => {
const virtualWriteTime = 1000;
const startStamp = performance.now();
await new Promise<void>((resolve) => {
function animate() {
const delta = performance.now() - startStamp
const delta = performance.now() - startStamp;
syncProgress.set({
max: virtualWriteTime,
current: delta,
})
});
if (delta >= virtualWriteTime) {
resolve()
resolve();
} else {
requestAnimationFrame(animate)
requestAnimationFrame(animate);
}
}
requestAnimationFrame(animate)
})
await port.commit()
requestAnimationFrame(animate);
});
await port.commit();
$deviceLayout = $layout.map(layer => layer.map<number>(({action}) => action)) as [
number[],
number[],
number[],
]
$deviceChords = $chords.filter(({deleted}) => !deleted).map(({actions, phrase}) => ({actions, phrase}))
$deviceSettings = $settings.map(({value}) => value)
$changes = []
$deviceLayout = $layout.map((layer) =>
layer.map<number>(({ action }) => action),
) as [number[], number[], number[]];
$deviceChords = $chords
.filter(({ deleted }) => !deleted)
.map(({ actions, phrase }) => ({ actions, phrase }));
$deviceSettings = $settings.map(({ value }) => value);
$changes = [];
} catch (e) {
alert(e)
console.error(e)
alert(e);
console.error(e);
} finally {
$syncStatus = "done"
$syncStatus = "done";
}
}
</script>
<button
use:action={{title: $LL.saveActions.UNDO(), shortcut: "ctrl+z"}}
use:action={{ title: $LL.saveActions.UNDO(), shortcut: "ctrl+z" }}
class="icon"
disabled={$changes.length === 0}
on:click={undo}>undo</button
>
<button
use:action={{title: $LL.saveActions.REDO(), shortcut: "ctrl+y"}}
use:action={{ title: $LL.saveActions.REDO(), shortcut: "ctrl+y" }}
class="icon"
disabled={redoQueue.length === 0}
on:click={redo}>redo</button
>
{#if $changes.length !== 0}
<button
transition:fly={{x: 10}}
use:action={{title: $LL.saveActions.SAVE(), shortcut: "ctrl+shift+s"}}
transition:fly={{ x: 10 }}
use:action={{ title: $LL.saveActions.SAVE(), shortcut: "ctrl+shift+s" }}
on:click={save}
class="click-me"><span class="icon">save</span>{$LL.saveActions.SAVE()}</button
class="click-me"
><span class="icon">save</span>{$LL.saveActions.SAVE()}</button
>
{/if}
+42 -27
View File
@@ -1,44 +1,47 @@
<script lang="ts">
import {browser, version} from "$app/environment"
import {action} from "$lib/title"
import LL, {setLocale} from "../i18n/i18n-svelte"
import {theme} from "$lib/preferences.js"
import type {Locales} from "../i18n/i18n-types"
import {detectLocale, locales} from "../i18n/i18n-util"
import {loadLocaleAsync} from "../i18n/i18n-util.async"
import {tick} from "svelte"
import SyncOverlay from "./SyncOverlay.svelte"
import {serialPort} from "$lib/serial/connection"
import { browser, version } from "$app/environment";
import { action } from "$lib/title";
import LL, { setLocale } from "../i18n/i18n-svelte";
import { theme } from "$lib/preferences.js";
import type { Locales } from "../i18n/i18n-types";
import { detectLocale, locales } from "../i18n/i18n-util";
import { loadLocaleAsync } from "../i18n/i18n-util.async";
import { tick } from "svelte";
import SyncOverlay from "./SyncOverlay.svelte";
import { serialPort } from "$lib/serial/connection";
let locale = (browser && (localStorage.getItem("locale") as Locales)) || detectLocale()
let locale =
(browser && (localStorage.getItem("locale") as Locales)) || detectLocale();
$: if (browser)
(async () => {
localStorage.setItem("locale", locale)
await loadLocaleAsync(locale)
setLocale(locale)
})()
localStorage.setItem("locale", locale);
await loadLocaleAsync(locale);
setLocale(locale);
})();
function switchTheme() {
const mode = $theme.mode === "light" ? "dark" : "light"
const mode = $theme.mode === "light" ? "dark" : "light";
if (document.startViewTransition) {
document.startViewTransition(async () => {
$theme.mode = mode
await tick()
})
$theme.mode = mode;
await tick();
});
} else {
$theme.mode = mode
$theme.mode = mode;
}
}
let languageSelect: HTMLSelectElement
let languageSelect: HTMLSelectElement;
</script>
<footer>
<ul>
<li>
<!-- svelte-ignore not-defined -->
<a href={import.meta.env.VITE_HOMEPAGE_URL} rel="noreferrer" target="_blank"
><span class="icon">commit</span> v{version}</a
<a
href={import.meta.env.VITE_HOMEPAGE_URL}
rel="noreferrer"
target="_blank"><span class="icon">commit</span> v{version}</a
>
</li>
<li>
@@ -67,15 +70,27 @@
</div>
<ul>
<li class="hide-forced-colors">
<input use:action={{title: $LL.profile.theme.COLOR_SCHEME()}} type="color" bind:value={$theme.color} />
<input
use:action={{ title: $LL.profile.theme.COLOR_SCHEME() }}
type="color"
bind:value={$theme.color}
/>
</li>
<li class="hide-forced-colors">
{#if $theme.mode === "light"}
<button use:action={{title: $LL.profile.theme.DARK_MODE()}} class="icon" on:click={switchTheme}>
<button
use:action={{ title: $LL.profile.theme.DARK_MODE() }}
class="icon"
on:click={switchTheme}
>
dark_mode
</button>
{:else if $theme.mode === "dark"}
<button use:action={{title: $LL.profile.theme.LIGHT_MODE()}} class="icon" on:click={switchTheme}>
<button
use:action={{ title: $LL.profile.theme.LIGHT_MODE() }}
class="icon"
on:click={switchTheme}
>
light_mode
</button>
{/if}
@@ -83,7 +98,7 @@
<li>
<button
class="icon"
use:action={{title: $LL.profile.LANGUAGE()}}
use:action={{ title: $LL.profile.LANGUAGE() }}
on:click={() => languageSelect.click()}
>translate
+26 -22
View File
@@ -1,25 +1,25 @@
<script lang="ts">
import {serialPort, syncStatus} from "$lib/serial/connection"
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 {browser} from "$app/environment"
import {userPreferences} from "$lib/preferences"
import {action} from "$lib/title"
import LL from "../i18n/i18n-svelte"
import ConfigTabs from "./ConfigTabs.svelte"
import EditActions from "./EditActions.svelte"
import {onMount} from "svelte"
import { serialPort, syncStatus } from "$lib/serial/connection";
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 { browser } from "$app/environment";
import { userPreferences } from "$lib/preferences";
import { action } from "$lib/title";
import LL from "../i18n/i18n-svelte";
import ConfigTabs from "./ConfigTabs.svelte";
import EditActions from "./EditActions.svelte";
import { onMount } from "svelte";
onMount(async () => {
if (browser && !$userPreferences.autoConnect) {
connectButton.click()
connectButton.click();
}
})
});
let connectButton: HTMLButtonElement
let connectButton: HTMLButtonElement;
</script>
<nav>
@@ -32,14 +32,14 @@
<div class="actions">
{#if $canShare}
<button
use:action={{title: $LL.share.TITLE()}}
transition:fly={{x: -8}}
use:action={{ title: $LL.share.TITLE() }}
transition:fly={{ x: -8 }}
class="icon"
on:click={triggerShare}>share</button
>
<button
use:action={{title: $LL.print.TITLE()}}
transition:fly={{x: -8}}
use:action={{ title: $LL.print.TITLE() }}
transition:fly={{ x: -8 }}
class="icon"
on:click={() => print()}>print</button
>
@@ -50,7 +50,11 @@
<PwaStatus />
{/await}
{/if}
<button use:action={{title: $LL.backup.TITLE()}} use:popup={BackupPopup} class="icon {$syncStatus}">
<button
use:action={{ title: $LL.backup.TITLE() }}
use:popup={BackupPopup}
class="icon {$syncStatus}"
>
{#if $userPreferences.backup}
history
{:else}
@@ -59,7 +63,7 @@
</button>
<button
bind:this={connectButton}
use:action={{title: $LL.deviceManager.TITLE()}}
use:action={{ title: $LL.deviceManager.TITLE() }}
use:popup={ConnectionPopup}
class="icon connect"
class:error={$serialPort === undefined}
+33 -29
View File
@@ -1,49 +1,53 @@
<script lang="ts">
import {fly} from "svelte/transition"
import {afterNavigate, beforeNavigate} from "$app/navigation"
import {expoIn, expoOut} from "svelte/easing"
import { fly } from "svelte/transition";
import { afterNavigate, beforeNavigate } from "$app/navigation";
import { expoIn, expoOut } from "svelte/easing";
let inDirection = 0
let outDirection = 0
let outroEnd: undefined | (() => void) = undefined
let animationDone: Promise<void>
let inDirection = 0;
let outDirection = 0;
let outroEnd: undefined | (() => void) = undefined;
let animationDone: Promise<void>;
let isNavigating = false
let isNavigating = false;
const routeOrder = ["/config/chords/", "/config/layout/", "/config/settings/"]
const routeOrder = [
"/config/chords/",
"/config/layout/",
"/config/settings/",
];
beforeNavigate(navigation => {
const from = navigation.from?.url.pathname
const to = navigation.to?.url.pathname
if (from === to) return
isNavigating = true
beforeNavigate((navigation) => {
const from = navigation.from?.url.pathname;
const to = navigation.to?.url.pathname;
if (from === to) return;
isNavigating = true;
if (!(from && to && routeOrder.includes(from) && routeOrder.includes(to))) {
inDirection = 0
outDirection = 0
inDirection = 0;
outDirection = 0;
} else {
const fromIndex = routeOrder.indexOf(from)
const toIndex = routeOrder.indexOf(to)
const fromIndex = routeOrder.indexOf(from);
const toIndex = routeOrder.indexOf(to);
inDirection = fromIndex > toIndex ? -1 : 1
outDirection = fromIndex > toIndex ? 1 : -1
inDirection = fromIndex > toIndex ? -1 : 1;
outDirection = fromIndex > toIndex ? 1 : -1;
}
animationDone = new Promise(resolve => {
outroEnd = resolve
})
})
animationDone = new Promise((resolve) => {
outroEnd = resolve;
});
});
afterNavigate(async () => {
await animationDone
isNavigating = false
})
await animationDone;
isNavigating = false;
});
</script>
{#if !isNavigating}
<main
in:fly={{x: inDirection * 24, duration: 150, easing: expoOut}}
out:fly={{x: outDirection * 24, duration: 150, easing: expoIn}}
in:fly={{ x: inDirection * 24, duration: 150, easing: expoOut }}
out:fly={{ x: outDirection * 24, duration: 150, easing: expoIn }}
on:outroend={outroEnd}
>
<slot />
+15 -5
View File
@@ -1,13 +1,21 @@
<script lang="ts">
import {serialPort, syncProgress, syncStatus, sync} from "$lib/serial/connection"
import LL from "../i18n/i18n-svelte"
import {slide} from "svelte/transition"
import {
serialPort,
syncProgress,
syncStatus,
sync,
} from "$lib/serial/connection";
import LL from "../i18n/i18n-svelte";
import { slide } from "svelte/transition";
</script>
<div class="container">
{#if $syncStatus !== "done"}
<div transition:slide>
<progress max={$syncProgress?.max ?? 1} value={$syncProgress?.current ?? 1}></progress>
<progress
max={$syncProgress?.max ?? 1}
value={$syncProgress?.current ?? 1}
></progress>
{#if $syncStatus === "downloading"}
<div>{$LL.sync.TITLE_READ()}</div>
{:else}
@@ -15,7 +23,9 @@
{/if}
</div>
{:else if $serialPort}
<button transition:slide on:click={sync}><span class="icon">refresh</span>{$LL.sync.RELOAD()}</button>
<button transition:slide on:click={sync}
><span class="icon">refresh</span>{$LL.sync.RELOAD()}</button
>
{/if}
</div>
+4 -4
View File
@@ -1,6 +1,6 @@
import {redirect} from "@sveltejs/kit"
import type {PageLoad} from "./$types"
import { redirect } from "@sveltejs/kit";
import type { PageLoad } from "./$types";
export const load = (() => {
throw redirect(302, "/config/layout/")
}) satisfies PageLoad
throw redirect(302, "/config/layout/");
}) satisfies PageLoad;
+1 -1
View File
@@ -1,5 +1,5 @@
<script>
import LL from "../../i18n/i18n-svelte"
import LL from "../../i18n/i18n-svelte";
</script>
{$LL.share.URL_COPIED()}
+59 -51
View File
@@ -1,89 +1,92 @@
<script lang="ts">
import {KEYMAP_CODES} from "$lib/serial/keymap-codes"
import Index from "flexsearch"
import LL from "../../../i18n/i18n-svelte"
import {action} from "$lib/title"
import {onDestroy, onMount, setContext} from "svelte"
import {changes, ChangeType, chords} from "$lib/undo-redo"
import type {ChordInfo} from "$lib/undo-redo"
import {derived, writable} from "svelte/store"
import ChordEdit from "./ChordEdit.svelte"
import {crossfade} from "svelte/transition"
import ChordActionEdit from "./ChordActionEdit.svelte"
import { KEYMAP_CODES } from "$lib/serial/keymap-codes";
import Index from "flexsearch";
import LL from "../../../i18n/i18n-svelte";
import { action } from "$lib/title";
import { onDestroy, onMount, setContext } from "svelte";
import { changes, ChangeType, chords } from "$lib/undo-redo";
import type { ChordInfo } from "$lib/undo-redo";
import { derived, writable } from "svelte/store";
import ChordEdit from "./ChordEdit.svelte";
import { crossfade } from "svelte/transition";
import ChordActionEdit from "./ChordActionEdit.svelte";
const resultSize = 38
let results: HTMLElement
const pageSize = writable(0)
let resizeObserver: ResizeObserver
const resultSize = 38;
let results: HTMLElement;
const pageSize = writable(0);
let resizeObserver: ResizeObserver;
onMount(() => {
resizeObserver = new ResizeObserver(() => {
pageSize.set(Math.floor(results.clientHeight / resultSize))
})
pageSize.set(Math.floor(results.clientHeight / resultSize))
resizeObserver.observe(results)
})
pageSize.set(Math.floor(results.clientHeight / resultSize));
});
pageSize.set(Math.floor(results.clientHeight / resultSize));
resizeObserver.observe(results);
});
onDestroy(() => {
resizeObserver?.disconnect()
})
resizeObserver?.disconnect();
});
$: searchIndex = $chords?.length > 0 ? buildIndex($chords) : undefined
$: searchIndex = $chords?.length > 0 ? buildIndex($chords) : undefined;
function buildIndex(chords: ChordInfo[]): Index {
const index = new Index({tokenize: "full"})
const index = new Index({ tokenize: "full" });
chords.forEach((chord, i) => {
if ("phrase" in chord) {
index.add(
i,
chord.phrase
.map(it => KEYMAP_CODES[it]?.id)
.filter(it => !!it)
.map((it) => KEYMAP_CODES[it]?.id)
.filter((it) => !!it)
.join(""),
)
);
}
})
return index
});
return index;
}
const searchFilter = writable<number[] | undefined>(undefined)
const searchFilter = writable<number[] | undefined>(undefined);
function search(event: Event) {
const query = (event.target as HTMLInputElement).value
searchFilter.set(query && searchIndex ? searchIndex.search(query) : undefined)
page = 0
const query = (event.target as HTMLInputElement).value;
searchFilter.set(
query && searchIndex ? searchIndex.search(query) : undefined,
);
page = 0;
}
function insertChord(actions: number[]) {
const id = JSON.stringify(actions)
if ($chords.some(it => JSON.stringify(it.actions) === id)) {
alert($LL.configure.chords.DUPLICATE())
return
const id = JSON.stringify(actions);
if ($chords.some((it) => JSON.stringify(it.actions) === id)) {
alert($LL.configure.chords.DUPLICATE());
return;
}
changes.update(changes => {
changes.update((changes) => {
changes.push({
type: ChangeType.Chord,
id: actions,
actions,
phrase: [],
})
return changes
})
});
return changes;
});
}
const items = derived(
[searchFilter, chords],
([filter, chords]) =>
filter?.map(it => [chords[it], it] as const) ?? chords.map((it, i) => [it, i] as const),
)
filter?.map((it) => [chords[it], it] as const) ??
chords.map((it, i) => [it, i] as const),
);
const lastPage = derived(
[items, pageSize],
([items, pageSize]) => Math.ceil((items.length + 1) / pageSize) - 1,
)
);
setContext("cursor-crossfade", crossfade({}))
setContext("cursor-crossfade", crossfade({}));
let page = 0
let page = 0;
</script>
<svelte:head>
@@ -104,13 +107,15 @@
- / -
{/if}
</div>
<button class="icon" on:click={() => (page = Math.max(page - 1, 0))} use:action={{shortcut: "ctrl+left"}}
>navigate_before</button
<button
class="icon"
on:click={() => (page = Math.max(page - 1, 0))}
use:action={{ shortcut: "ctrl+left" }}>navigate_before</button
>
<button
class="icon"
on:click={() => (page = Math.min(page + 1, $lastPage))}
use:action={{shortcut: "ctrl+right"}}>navigate_next</button
use:action={{ shortcut: "ctrl+right" }}>navigate_next</button
>
</div>
@@ -118,8 +123,11 @@
<table>
{#if page === 0}
<tr
><th class="new-chord"><ChordActionEdit on:submit={({detail}) => insertChord(detail)} /></th><td /><td
/></tr
><th class="new-chord"
><ChordActionEdit
on:submit={({ detail }) => insertChord(detail)}
/></th
><td /><td /></tr
>
{/if}
{#if $lastPage !== -1}
+50 -42
View File
@@ -1,80 +1,83 @@
<script lang="ts">
import type {ChordInfo} from "$lib/undo-redo"
import {changes, ChangeType} from "$lib/undo-redo"
import {createEventDispatcher} from "svelte"
import LL from "../../../i18n/i18n-svelte"
import ActionString from "$lib/components/ActionString.svelte"
import {selectAction} from "./action-selector"
import {serialPort} from "$lib/serial/connection"
import {get} from "svelte/store"
import {inputToAction} from "./input-converter"
import type { ChordInfo } from "$lib/undo-redo";
import { changes, ChangeType } from "$lib/undo-redo";
import { createEventDispatcher } from "svelte";
import LL from "../../../i18n/i18n-svelte";
import ActionString from "$lib/components/ActionString.svelte";
import { selectAction } from "./action-selector";
import { serialPort } from "$lib/serial/connection";
import { get } from "svelte/store";
import { inputToAction } from "./input-converter";
export let chord: ChordInfo | undefined = undefined
export let chord: ChordInfo | undefined = undefined;
const dispatch = createEventDispatcher()
const dispatch = createEventDispatcher();
let pressedKeys = new Set<number>()
let editing = false
let pressedKeys = new Set<number>();
let editing = false;
function compare(a: number, b: number) {
return a - b
return a - b;
}
function edit() {
pressedKeys = new Set()
editing = true
pressedKeys = new Set();
editing = true;
}
function keydown(event: KeyboardEvent) {
if (!editing) return
event.preventDefault()
const input = inputToAction(event, get(serialPort)?.device === "X")
if (!editing) return;
event.preventDefault();
const input = inputToAction(event, get(serialPort)?.device === "X");
if (input == undefined) {
alert("Invalid key")
return
alert("Invalid key");
return;
}
pressedKeys.add(input)
pressedKeys = pressedKeys
pressedKeys.add(input);
pressedKeys = pressedKeys;
}
function keyup() {
if (!editing) return
editing = false
if (pressedKeys.size < 2) return
if (!chord) return dispatch("submit", [...pressedKeys].sort(compare))
changes.update(changes => {
if (!editing) return;
editing = false;
if (pressedKeys.size < 2) return;
if (!chord) return dispatch("submit", [...pressedKeys].sort(compare));
changes.update((changes) => {
changes.push({
type: ChangeType.Chord,
id: chord!.id,
actions: [...pressedKeys].sort(compare),
phrase: chord!.phrase,
})
return changes
})
});
return changes;
});
}
function addSpecial(event: MouseEvent) {
selectAction(event, action => {
changes.update(changes => {
selectAction(event, (action) => {
changes.update((changes) => {
changes.push({
type: ChangeType.Chord,
id: chord!.id,
actions: [...chord!.actions, action].sort(compare),
phrase: chord!.phrase,
})
return changes
})
})
});
return changes;
});
});
}
$: chordActions = chord?.actions.slice(chord.actions.lastIndexOf(0) + 1).toSorted(compare)
$: compoundIndices = chord?.actions.slice(0, chord.actions.indexOf(0))
$: chordActions = chord?.actions
.slice(chord.actions.lastIndexOf(0) + 1)
.toSorted(compare);
$: compoundIndices = chord?.actions.slice(0, chord.actions.indexOf(0));
</script>
<button
class:deleted={chord && chord.deleted}
class:edited={chord && chord.actionsChanged}
class:invalid={chord && chordActions?.some((it, i) => chordActions?.[i] !== it)}
class:invalid={chord &&
chordActions?.some((it, i) => chordActions?.[i] !== it)}
class="chord"
on:click={edit}
on:keydown={keydown}
@@ -93,8 +96,13 @@
<span>&rarr;</span>
{/if}
{/if}
<ActionString display="keys" actions={editing ? [...pressedKeys].sort(compare) : chordActions ?? []} />
<button class="icon add" on:click|stopPropagation={addSpecial}>add_circle</button>
<ActionString
display="keys"
actions={editing ? [...pressedKeys].sort(compare) : chordActions ?? []}
/>
<button class="icon add" on:click|stopPropagation={addSpecial}
>add_circle</button
>
<sup></sup>
</button>
+43 -28
View File
@@ -1,39 +1,46 @@
<script lang="ts">
import {changes, ChangeType} from "$lib/undo-redo.js"
import type {ChordInfo} from "$lib/undo-redo.js"
import ChordPhraseEdit from "./ChordPhraseEdit.svelte"
import ChordActionEdit from "./ChordActionEdit.svelte"
import type {Chord} from "$lib/serial/chord"
import {slide} from "svelte/transition"
import {charaFileToUriComponent} from "$lib/share/share-url"
import SharePopup from "../SharePopup.svelte"
import tippy from "tippy.js"
import { changes, ChangeType } from "$lib/undo-redo.js";
import type { ChordInfo } from "$lib/undo-redo.js";
import ChordPhraseEdit from "./ChordPhraseEdit.svelte";
import ChordActionEdit from "./ChordActionEdit.svelte";
import type { Chord } from "$lib/serial/chord";
import { slide } from "svelte/transition";
import { charaFileToUriComponent } from "$lib/share/share-url";
import SharePopup from "../SharePopup.svelte";
import tippy from "tippy.js";
export let chord: ChordInfo
export let chord: ChordInfo;
function remove() {
changes.update(changes => {
changes.update((changes) => {
changes.push({
type: ChangeType.Chord,
id: chord.id,
actions: chord.actions,
phrase: chord.phrase,
deleted: true,
})
return changes
})
});
return changes;
});
}
function isSameChord(a: Chord, b: Chord) {
return a.actions.length === b.actions.length && a.actions.every((it, i) => it === b.actions[i])
return (
a.actions.length === b.actions.length &&
a.actions.every((it, i) => it === b.actions[i])
);
}
function restore() {
changes.update(changes => changes.filter(it => !(it.type === ChangeType.Chord && isSameChord(it, chord))))
changes.update((changes) =>
changes.filter(
(it) => !(it.type === ChangeType.Chord && isSameChord(it, chord)),
),
);
}
async function share(event: Event) {
const url = new URL(window.location.href)
const url = new URL(window.location.href);
url.searchParams.set(
"import",
await charaFileToUriComponent({
@@ -41,21 +48,21 @@
type: "chords",
chords: [[chord.actions, chord.phrase]],
}),
)
await navigator.clipboard.writeText(url.toString())
let shareComponent: SharePopup
);
await navigator.clipboard.writeText(url.toString());
let shareComponent: SharePopup;
tippy(event.target as HTMLElement, {
onCreate(instance) {
const target = instance.popper.querySelector(".tippy-content")!
shareComponent = new SharePopup({target})
const target = instance.popper.querySelector(".tippy-content")!;
shareComponent = new SharePopup({ target });
},
onHidden(instance) {
instance.destroy()
instance.destroy();
},
onDestroy(instance) {
shareComponent.$destroy()
shareComponent.$destroy();
},
}).show()
}).show();
}
</script>
@@ -67,11 +74,19 @@
</td>
<td class="table-buttons">
{#if !chord.deleted}
<button transition:slide class="icon compact" on:click={remove}>delete</button>
<button transition:slide class="icon compact" on:click={remove}
>delete</button
>
{:else}
<button transition:slide class="icon compact" on:click={restore}>restore_from_trash</button>
<button transition:slide class="icon compact" on:click={restore}
>restore_from_trash</button
>
{/if}
<button class="icon compact" class:disabled={chord.isApplied} on:click={restore}>undo</button>
<button
class="icon compact"
class:disabled={chord.isApplied}
on:click={restore}>undo</button
>
<div class="separator" />
<button class="icon compact" on:click={share}>share</button>
</td>
+51 -51
View File
@@ -1,102 +1,102 @@
<script lang="ts">
import {tick} from "svelte"
import {changes, ChangeType} from "$lib/undo-redo"
import type {ChordInfo} from "$lib/undo-redo"
import {scale} from "svelte/transition"
import ActionString from "$lib/components/ActionString.svelte"
import {selectAction} from "./action-selector"
import {inputToAction} from "./input-converter"
import {serialPort} from "$lib/serial/connection"
import {get} from "svelte/store"
import { tick } from "svelte";
import { changes, ChangeType } from "$lib/undo-redo";
import type { ChordInfo } from "$lib/undo-redo";
import { scale } from "svelte/transition";
import ActionString from "$lib/components/ActionString.svelte";
import { selectAction } from "./action-selector";
import { inputToAction } from "./input-converter";
import { serialPort } from "$lib/serial/connection";
import { get } from "svelte/store";
export let chord: ChordInfo
export let chord: ChordInfo;
function keypress(event: KeyboardEvent) {
if (event.key === "ArrowUp") {
addSpecial(event)
addSpecial(event);
} else if (event.key === "ArrowLeft") {
moveCursor(cursorPosition - 1)
moveCursor(cursorPosition - 1);
} else if (event.key === "ArrowRight") {
moveCursor(cursorPosition + 1)
moveCursor(cursorPosition + 1);
} else if (event.key === "Backspace") {
deleteAction(cursorPosition - 1)
moveCursor(cursorPosition - 1)
deleteAction(cursorPosition - 1);
moveCursor(cursorPosition - 1);
} else if (event.key === "Delete") {
deleteAction(cursorPosition)
deleteAction(cursorPosition);
} else {
if (event.key === "Shift") return
const action = inputToAction(event, get(serialPort)?.device === "X")
if (event.key === "Shift") return;
const action = inputToAction(event, get(serialPort)?.device === "X");
if (action !== undefined) {
insertAction(cursorPosition, action)
tick().then(() => moveCursor(cursorPosition + 1))
insertAction(cursorPosition, action);
tick().then(() => moveCursor(cursorPosition + 1));
}
}
}
function moveCursor(to: number) {
cursorPosition = Math.max(0, Math.min(to, chord.phrase.length))
const item = box.children.item(cursorPosition) as HTMLElement
cursorOffset = item.offsetLeft + item.offsetWidth
cursorPosition = Math.max(0, Math.min(to, chord.phrase.length));
const item = box.children.item(cursorPosition) as HTMLElement;
cursorOffset = item.offsetLeft + item.offsetWidth;
}
function deleteAction(at: number, count = 1) {
if (!(at in chord.phrase)) return
changes.update(changes => {
if (!(at in chord.phrase)) return;
changes.update((changes) => {
changes.push({
type: ChangeType.Chord,
id: chord.id,
actions: chord.actions,
phrase: chord.phrase.toSpliced(at, count),
})
return changes
})
});
return changes;
});
}
function insertAction(at: number, action: number) {
changes.update(changes => {
changes.update((changes) => {
changes.push({
type: ChangeType.Chord,
id: chord.id,
actions: chord.actions,
phrase: chord.phrase.toSpliced(at, 0, action),
})
return changes
})
});
return changes;
});
}
function clickCursor(event: MouseEvent) {
if (event.target === button) return
const distance = (event as unknown as {layerX: number}).layerX
if (event.target === button) return;
const distance = (event as unknown as { layerX: number }).layerX;
let i = 0
let i = 0;
for (const child of box.children) {
const {offsetLeft, offsetWidth} = child as HTMLElement
const { offsetLeft, offsetWidth } = child as HTMLElement;
if (distance < offsetLeft + offsetWidth / 2) {
moveCursor(i - 1)
return
moveCursor(i - 1);
return;
}
i++
i++;
}
moveCursor(i - 1)
moveCursor(i - 1);
}
function addSpecial(event: MouseEvent | KeyboardEvent) {
selectAction(
event,
action => {
insertAction(cursorPosition, action)
tick().then(() => moveCursor(cursorPosition + 1))
(action) => {
insertAction(cursorPosition, action);
tick().then(() => moveCursor(cursorPosition + 1));
},
() => box.focus(),
)
);
}
let button: HTMLButtonElement
let box: HTMLDivElement
let cursorPosition = 0
let cursorOffset = 0
let button: HTMLButtonElement;
let box: HTMLDivElement;
let cursorPosition = 0;
let cursorOffset = 0;
let hasFocus = false
let hasFocus = false;
</script>
<div
@@ -107,8 +107,8 @@
bind:this={box}
class:edited={!chord.deleted && chord.phraseChanged}
on:focusin={() => (hasFocus = true)}
on:focusout={event => {
if (event.relatedTarget !== button) hasFocus = false
on:focusout={(event) => {
if (event.relatedTarget !== button) hasFocus = false;
}}
>
{#if hasFocus}
+32 -29
View File
@@ -1,50 +1,53 @@
import ActionSelector from "$lib/components/layout/ActionSelector.svelte"
import {tick} from "svelte"
import ActionSelector from "$lib/components/layout/ActionSelector.svelte";
import { tick } from "svelte";
export function selectAction(
event: MouseEvent | KeyboardEvent,
select: (action: number) => void,
dismissed?: () => void,
) {
const component = new ActionSelector({target: document.body})
const dialog = document.querySelector("dialog > div") as HTMLDivElement
const backdrop = document.querySelector("dialog") as HTMLDialogElement
const dialogRect = dialog.getBoundingClientRect()
const groupRect = (event.target as HTMLElement).getBoundingClientRect()
const component = new ActionSelector({ target: document.body });
const dialog = document.querySelector("dialog > div") as HTMLDivElement;
const backdrop = document.querySelector("dialog") as HTMLDialogElement;
const dialogRect = dialog.getBoundingClientRect();
const groupRect = (event.target as HTMLElement).getBoundingClientRect();
const scale = 0.5
const dialogScale = `${1 - scale * (1 - groupRect.width / dialogRect.width)} ${
1 - scale * (1 - groupRect.height / dialogRect.height)
}`
const scale = 0.5;
const dialogScale = `${
1 - scale * (1 - groupRect.width / dialogRect.width)
} ${1 - scale * (1 - groupRect.height / dialogRect.height)}`;
const dialogTranslate = `${scale * (groupRect.x - dialogRect.x)}px ${
scale * (groupRect.y - dialogRect.y)
}px`
}px`;
const duration = 150
const options = {duration, easing: "ease"}
const duration = 150;
const options = { duration, easing: "ease" };
const dialogAnimation = dialog.animate(
[
{scale: dialogScale, translate: dialogTranslate},
{translate: "0 0", scale: "1"},
{ scale: dialogScale, translate: dialogTranslate },
{ translate: "0 0", scale: "1" },
],
options,
)
const backdropAnimation = backdrop.animate([{opacity: 0}, {opacity: 1}], options)
);
const backdropAnimation = backdrop.animate(
[{ opacity: 0 }, { opacity: 1 }],
options,
);
async function closed() {
dialogAnimation.reverse()
backdropAnimation.reverse()
dialogAnimation.reverse();
backdropAnimation.reverse();
await dialogAnimation.finished
await dialogAnimation.finished;
component.$destroy()
await tick()
dismissed?.()
component.$destroy();
await tick();
dismissed?.();
}
component.$on("close", closed)
component.$on("select", ({detail}) => {
select(detail)
closed()
})
component.$on("close", closed);
component.$on("select", ({ detail }) => {
select(detail);
closed();
});
}
+7 -4
View File
@@ -1,9 +1,12 @@
import {KEYMAP_IDS, KEYMAP_KEYCODES} from "$lib/serial/keymap-codes"
import { KEYMAP_IDS, KEYMAP_KEYCODES } from "$lib/serial/keymap-codes";
export function inputToAction(event: KeyboardEvent, useKeycodes?: boolean): number | undefined {
export function inputToAction(
event: KeyboardEvent,
useKeycodes?: boolean,
): number | undefined {
if (useKeycodes) {
return KEYMAP_KEYCODES.get(event.code)
return KEYMAP_KEYCODES.get(event.code);
} else {
return KEYMAP_IDS.get(event.key)?.code ?? KEYMAP_KEYCODES.get(event.code)
return KEYMAP_IDS.get(event.key)?.code ?? KEYMAP_KEYCODES.get(event.code);
}
}
+25 -21
View File
@@ -1,39 +1,43 @@
<script lang="ts">
import {share} from "$lib/share"
import tippy from "tippy.js"
import {setContext} from "svelte"
import Layout from "$lib/components/layout/Layout.svelte"
import {charaFileToUriComponent} from "$lib/share/share-url"
import SharePopup from "../SharePopup.svelte"
import type {VisualLayoutConfig} from "$lib/components/layout/visual-layout"
import {writable} from "svelte/store"
import {layout} from "$lib/undo-redo"
import { share } from "$lib/share";
import tippy from "tippy.js";
import { setContext } from "svelte";
import Layout from "$lib/components/layout/Layout.svelte";
import { charaFileToUriComponent } from "$lib/share/share-url";
import SharePopup from "../SharePopup.svelte";
import type { VisualLayoutConfig } from "$lib/components/layout/visual-layout";
import { writable } from "svelte/store";
import { layout } from "$lib/undo-redo";
async function shareLayout(event: Event) {
const url = new URL(window.location.href)
const url = new URL(window.location.href);
url.searchParams.set(
"import",
await charaFileToUriComponent({
charaVersion: 1,
type: "layout",
device: "one",
layout: $layout.map(it => it.map(it => it.action)) as [number[], number[], number[]],
layout: $layout.map((it) => it.map((it) => it.action)) as [
number[],
number[],
number[],
],
}),
)
await navigator.clipboard.writeText(url.toString())
let shareComponent: SharePopup
);
await navigator.clipboard.writeText(url.toString());
let shareComponent: SharePopup;
tippy(event.target as HTMLElement, {
onCreate(instance) {
const target = instance.popper.querySelector(".tippy-content")!
shareComponent = new SharePopup({target})
const target = instance.popper.querySelector(".tippy-content")!;
shareComponent = new SharePopup({ target });
},
onHidden(instance) {
instance.destroy()
instance.destroy();
},
onDestroy() {
shareComponent.$destroy()
shareComponent.$destroy();
},
}).show()
}).show();
}
setContext<VisualLayoutConfig>("visual-layout-config", {
@@ -44,9 +48,9 @@
margin: 5,
fontSize: 9,
iconFontSize: 14,
})
});
setContext("active-layer", writable(0))
setContext("active-layer", writable(0));
</script>
<svelte:head>
+147 -59
View File
@@ -1,9 +1,9 @@
<script>
import Action from "$lib/components/Action.svelte"
import {popup} from "$lib/popup"
import {serialPort} from "$lib/serial/connection"
import {setting} from "$lib/setting"
import ResetPopup from "./ResetPopup.svelte"
import Action from "$lib/components/Action.svelte";
import { popup } from "$lib/popup";
import { serialPort } from "$lib/serial/connection";
import { setting } from "$lib/setting";
import ResetPopup from "./ResetPopup.svelte";
</script>
<svelte:head>
@@ -14,19 +14,26 @@
{#if $serialPort}
<form>
<fieldset>
<legend><label><input type="checkbox" use:setting={{id: 0x41}} />Spurring</label></legend>
<legend
><label
><input type="checkbox" use:setting={{ id: 0x41 }} />Spurring</label
></legend
>
<p>
"Chording only" mode which tells your device to output chords on a press rather than a press &
release. It also enables you to jump from one chord to another without releasing everything and can be
activated in GTM or by chording both mirror keys. It can provide significant speed gains with
chording, but also takes away the flexibility of character entry.
"Chording only" mode which tells your device to output chords on a press
rather than a press & release. It also enables you to jump from one
chord to another without releasing everything and can be activated in
GTM or by chording both mirror keys. It can provide significant speed
gains with chording, but also takes away the flexibility of character
entry.
</p>
<p>Spurring also helps new users learn how to chord by eliminating the need to focus on timing.</p>
<p>
Spurring is toggled by chording <Action display="keys" action={540} /> and <Action
display="keys"
action={542}
/> together.
Spurring also helps new users learn how to chord by eliminating the need
to focus on timing.
</p>
<p>
Spurring is toggled by chording <Action display="keys" action={540} /> and
<Action display="keys" action={542} /> together.
</p>
<label
>Character Counter Timeout<span class="unit"
@@ -35,40 +42,53 @@
step="0.001"
min="0"
max="240"
use:setting={{id: 0x43, scale: 0.001}}
use:setting={{ id: 0x43, scale: 0.001 }}
/>s</span
></label
>
</fieldset>
<fieldset>
<legend><label><input type="checkbox" use:setting={{id: 0x51}} />Arpeggiates</label></legend>
<legend
><label
><input
type="checkbox"
use:setting={{ id: 0x51 }}
/>Arpeggiates</label
></legend
>
<p>
A quick, single key press and release used to indicate a suffix, prefix, or modifier to be associated
with a chord.
A quick, single key press and release used to indicate a suffix, prefix,
or modifier to be associated with a chord.
</p>
<p>
The following keys have special behavior when arpeggiates are enabled:
</p>
<p>The following keys have special behavior when arpeggiates are enabled:</p>
<ul>
<li>
<Action display="keys" action={44} />, <Action display="keys" action={59} /> and <Action
<Action display="keys" action={44} />, <Action
display="keys"
action={58}
/> will be placed before the auto-inserted space
action={59}
/> and <Action display="keys" action={58} /> will be placed before the
auto-inserted space
</li>
<li>
<Action display="keys" action={46} />, <Action display="keys" action={63} /> and <Action
<Action display="keys" action={46} />, <Action
display="keys"
action={33}
/> will be placed before the auto-inserted space and capitalize the next word
action={63}
/> and <Action display="keys" action={33} /> will be placed before the
auto-inserted space and capitalize the next word
</li>
<li>
<Action display="keys" action={45} /> and <Action display="keys" action={47} /> will replace the auto-inserted
space
<Action display="keys" action={45} /> and <Action
display="keys"
action={47}
/> will replace the auto-inserted space
</li>
</ul>
<label
>Timeout After Chord<span class="unit"
><input type="number" step="1" use:setting={{id: 0x54}} />ms</span
><input type="number" step="1" use:setting={{ id: 0x54 }} />ms</span
></label
>
</fieldset>
@@ -76,98 +96,166 @@
<fieldset>
<legend>Chord Modifiers</legend>
<p>
Chord modifiers change a chord when held with the chord or when pressed after (arpeggiated), <b
>provided that arpeggiates are enabled.</b
>
Chord modifiers change a chord when held with the chord or when pressed
after (arpeggiated), <b>provided that arpeggiates are enabled.</b>
</p>
<ul>
<li><Action display="keys" action={513} /> Capitalizes the first letter of a chord</li>
<li><Action display="keys" action={540} /> Present Tense (supported words only)</li>
<li><Action display="keys" action={542} /> Plural (supported words only)</li>
<li><Action display="keys" action={550} /> Past Tense (supported words only)</li>
<li><Action display="keys" action={551} /> Comparative (supported words only)</li>
<li>
<Action display="keys" action={513} /> Capitalizes the first letter of
a chord
</li>
<li>
<Action display="keys" action={540} /> Present Tense (supported words only)
</li>
<li>
<Action display="keys" action={542} /> Plural (supported words only)
</li>
<li>
<Action display="keys" action={550} /> Past Tense (supported words only)
</li>
<li>
<Action display="keys" action={551} /> Comparative (supported words only)
</li>
</ul>
</fieldset>
<fieldset>
<legend>Character Entry</legend>
{#if $serialPort.device === "LITE"}
<label>Swap Keymap 0 and 1<input type="checkbox" use:setting={{id: 0x13}} /></label>
<label
>Swap Keymap 0 and 1<input
type="checkbox"
use:setting={{ id: 0x13 }}
/></label
>
{/if}
<label>
Character Entry (chentry)
<input type="checkbox" use:setting={{id: 0x12}} />
<input type="checkbox" use:setting={{ id: 0x12 }} />
</label>
<label>
Key Scan Rate
<span class="unit"><input type="number" use:setting={{id: 0x14, inverse: 1000}} />Hz</span></label
<span class="unit"
><input
type="number"
use:setting={{ id: 0x14, inverse: 1000 }}
/>Hz</span
></label
>
<label>
Key Debounce Press<span class="unit"><input type="number" use:setting={{id: 0x15}} />ms</span></label
>
<label
>Key Debounce Release<span class="unit"><input type="number" use:setting={{id: 0x16}} />ms</span
Key Debounce Press<span class="unit"
><input type="number" use:setting={{ id: 0x15 }} />ms</span
></label
>
<label
>Output Character Delay<span class="unit"><input type="number" use:setting={{id: 0x17}} />µs</span
>Key Debounce Release<span class="unit"
><input type="number" use:setting={{ id: 0x16 }} />ms</span
></label
>
<label
>Output Character Delay<span class="unit"
><input type="number" use:setting={{ id: 0x17 }} />µs</span
></label
>
</fieldset>
<fieldset>
<legend><label><input type="checkbox" use:setting={{id: 0x21}} />Mouse</label></legend>
<legend
><label><input type="checkbox" use:setting={{ id: 0x21 }} />Mouse</label
></legend
>
<label
>Mouse Speed<input type="number" use:setting={{id: 0x22}} /><input
>Mouse Speed<input type="number" use:setting={{ id: 0x22 }} /><input
type="number"
use:setting={{id: 0x23}}
use:setting={{ id: 0x23 }}
/></label
>
<label>Scroll Speed<input type="number" use:setting={{id: 0x25}} /></label>
<label
>Scroll Speed<input type="number" use:setting={{ id: 0x25 }} /></label
>
<label>
<span>
Active Mouse
<p>Bounces mouse by 1px every 60s if enabled</p></span
>
<input type="checkbox" use:setting={{id: 0x24}} /></label
<input type="checkbox" use:setting={{ id: 0x24 }} /></label
>
<label
>Poll Rate<span class="unit"><input type="number" use:setting={{id: 0x26, inverse: 1000}} />Hz</span
>Poll Rate<span class="unit"
><input
type="number"
use:setting={{ id: 0x26, inverse: 1000 }}
/>Hz</span
></label
>
</fieldset>
<fieldset>
<legend><label><input type="checkbox" use:setting={{id: 0x31}} />Chording</label></legend>
<legend
><label
><input type="checkbox" use:setting={{ id: 0x31 }} />Chording</label
></legend
>
<label
>Auto-delete Timeout <span class="unit"
><input type="number" min="0" max="25500" step="10" use:setting={{id: 0x33}} />ms</span
><input
type="number"
min="0"
max="25500"
step="10"
use:setting={{ id: 0x33 }}
/>ms</span
></label
>
<label
>Press Tolerance<span class="unit"
><input type="number" min="1" max="50" step="1" use:setting={{id: 0x34}} />ms</span
><input
type="number"
min="1"
max="50"
step="1"
use:setting={{ id: 0x34 }}
/>ms</span
></label
>
<label
>Release Tolerance<span class="unit"
><input type="number" min="1" max="50" step="1" use:setting={{id: 0x35}} />ms</span
><input
type="number"
min="1"
max="50"
step="1"
use:setting={{ id: 0x35 }}
/>ms</span
></label
>
<label>Compound Chording<input type="checkbox" use:setting={{id: 0x61}} /></label>
<label
>Compound Chording<input
type="checkbox"
use:setting={{ id: 0x61 }}
/></label
>
</fieldset>
<fieldset>
<legend>Device</legend>
<label>Boot message<input type="checkbox" use:setting={{id: 0x93}} /></label>
<label>GTM Realtime Feedback<input type="checkbox" use:setting={{id: 0x92}} /></label>
<label
>Boot message<input type="checkbox" use:setting={{ id: 0x93 }} /></label
>
<label
>GTM Realtime Feedback<input
type="checkbox"
use:setting={{ id: 0x92 }}
/></label
>
<button class="outline" use:popup={ResetPopup}>Reset...</button>
</fieldset>
{#if $serialPort.device === "LITE"}
<fieldset>
<legend><label><input type="checkbox" />RGB</label></legend>
<label>Brightness<input type="range" min="0" max="50" step="1" /></label>
<label>Brightness<input type="range" min="0" max="50" step="1" /></label
>
Color
<label>Reactive Keys<input type="checkbox" /></label>
</fieldset>
@@ -1,14 +1,14 @@
<script lang="ts">
import {serialPort} from "$lib/serial/connection"
import {createEventDispatcher} from "svelte"
import { serialPort } from "$lib/serial/connection";
import { createEventDispatcher } from "svelte";
export let challenge: string
export let challenge: string;
let challengeInput = ""
$: challengeString = `${challenge} ${$serialPort!.device}`
$: isValid = challengeInput === challengeString
let challengeInput = "";
$: challengeString = `${challenge} ${$serialPort!.device}`;
$: isValid = challengeInput === challengeString;
const dispatch = createEventDispatcher()
const dispatch = createEventDispatcher();
</script>
<h3>Type the following to confirm the action</h3>
@@ -16,7 +16,9 @@
<p>{challengeString}</p>
<input type="text" bind:value={challengeInput} placeholder={challengeString} />
<button disabled={!isValid} on:click={() => dispatch("confirm")}>Confirm {challenge}</button>
<button disabled={!isValid} on:click={() => dispatch("confirm")}
>Confirm {challenge}</button
>
<style lang="scss">
input[type="text"] {
+5 -5
View File
@@ -1,6 +1,6 @@
<script lang="ts">
import {confirmChallenge} from "./confirm-challenge"
import {serialPort} from "$lib/serial/connection"
import { confirmChallenge } from "./confirm-challenge";
import { serialPort } from "$lib/serial/connection";
const options = [
[["FACTORY", "Factory Reset"]],
@@ -13,7 +13,7 @@
["STARTER", "Add starter chords"],
["FUNC", "Add functional chords"],
],
] as const
] as const;
</script>
<h3>Reset Device</h3>
@@ -26,8 +26,8 @@
class="error"
use:confirmChallenge={{
onConfirm() {
$serialPort?.reset(command)
$serialPort = undefined
$serialPort?.reset(command);
$serialPort = undefined;
},
challenge: description,
}}>{description}...</button
+22 -22
View File
@@ -1,37 +1,37 @@
import type {Action} from "svelte/action"
import ConfirmChallenge from "./ConfirmChallenge.svelte"
import tippy from "tippy.js"
import type { Action } from "svelte/action";
import ConfirmChallenge from "./ConfirmChallenge.svelte";
import tippy from "tippy.js";
export const confirmChallenge: Action<HTMLElement, {onConfirm: () => void; challenge: string}> = (
node,
{onConfirm, challenge},
) => {
let component: ConfirmChallenge | undefined
let target: HTMLElement | undefined
export const confirmChallenge: Action<
HTMLElement,
{ onConfirm: () => void; challenge: string }
> = (node, { onConfirm, challenge }) => {
let component: ConfirmChallenge | 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")
target = instance.popper.querySelector(".tippy-content") as HTMLElement;
target.classList.add("active");
if (component === undefined) {
component = new ConfirmChallenge({target, props: {challenge}})
component = new ConfirmChallenge({ target, props: { challenge } });
component.$on("confirm", () => {
edit.hide()
onConfirm()
})
edit.hide();
onConfirm();
});
}
},
onHidden() {
component?.$destroy()
target?.classList.remove("active")
component = undefined
component?.$destroy();
target?.classList.remove("active");
component = undefined;
},
})
});
return {
destroy() {
edit.destroy()
edit.destroy();
},
}
}
};
};
+53 -37
View File
@@ -1,17 +1,17 @@
<script lang="ts">
import {KEYMAP_CODES} from "$lib/serial/keymap-codes"
import {onMount} from "svelte"
import {basicSetup, EditorView} from "codemirror"
import {javascript, javascriptLanguage} from "@codemirror/lang-javascript"
import {defaultKeymap} from "@codemirror/commands"
import {keymap} from "@codemirror/view"
import {HighlightStyle, syntaxHighlighting} from "@codemirror/language"
import {tags} from "@lezer/highlight"
import LL from "../../i18n/i18n-svelte"
import type {CompletionContext} from "@codemirror/autocomplete"
import {serialPort} from "$lib/serial/connection"
import type {CharaDevice} from "$lib/serial/device"
import examplePlugin from "./example-plugin.js?raw"
import { KEYMAP_CODES } from "$lib/serial/keymap-codes";
import { onMount } from "svelte";
import { basicSetup, EditorView } from "codemirror";
import { javascript, javascriptLanguage } from "@codemirror/lang-javascript";
import { defaultKeymap } from "@codemirror/commands";
import { keymap } from "@codemirror/view";
import { HighlightStyle, syntaxHighlighting } from "@codemirror/language";
import { tags } from "@lezer/highlight";
import LL from "../../i18n/i18n-svelte";
import type { CompletionContext } from "@codemirror/autocomplete";
import { serialPort } from "$lib/serial/connection";
import type { CharaDevice } from "$lib/serial/device";
import examplePlugin from "./example-plugin.js?raw";
let theme = EditorView.baseTheme({
".cm-editor .cm-content": {
@@ -40,25 +40,29 @@
background: "transparent !important",
backdropFilter: "invert(0.3)",
},
})
});
const highlightStyle = HighlightStyle.define(
[
{tag: tags.keyword, color: "var(--md-sys-color-primary)"},
{tag: tags.number, color: "var(--md-sys-color-secondary)"},
{tag: tags.string, color: "var(--md-sys-color-tertiary)"},
{tag: tags.comment, color: "var(--md-sys-color-on-background)", opacity: 0.6},
{ tag: tags.keyword, color: "var(--md-sys-color-primary)" },
{ tag: tags.number, color: "var(--md-sys-color-secondary)" },
{ tag: tags.string, color: "var(--md-sys-color-tertiary)" },
{
tag: tags.comment,
color: "var(--md-sys-color-on-background)",
opacity: 0.6,
},
],
{
all: {fontFamily: '"Noto Sans Mono", monospace', fontSize: "14px"},
all: { fontFamily: '"Noto Sans Mono", monospace', fontSize: "14px" },
},
)
);
const completion = javascriptLanguage.data.of({
autocomplete: function completeGlobals(context: CompletionContext) {
if (context.matchBefore(/Chara\./)) {
// TODO
}
},
})
});
onMount(() => {
editorView = new EditorView({
@@ -72,8 +76,8 @@
],
parent: editor,
doc: examplePlugin,
})
})
});
});
const charaMethods = [
"reboot",
@@ -89,7 +93,7 @@
"getChordCount",
"getChord",
"send",
] satisfies Array<keyof CharaDevice>
] satisfies Array<keyof CharaDevice>;
$: channels = $serialPort
? ({
getVersion: async (..._args: unknown[]) => $serialPort.version,
@@ -102,19 +106,23 @@
"Click OK to perform the commit anyways.",
)
) {
return $serialPort.commit()
return $serialPort.commit();
}
},
...Object.fromEntries(charaMethods.map(it => [it, $serialPort[it].bind($serialPort)] as const)),
...Object.fromEntries(
charaMethods.map(
(it) => [it, $serialPort[it].bind($serialPort)] as const,
),
),
} satisfies Record<string, Function>)
: ({} as any)
: ({} as any);
async function onMessage(event: MessageEvent) {
if (event.origin !== "null" || event.source !== frame.contentWindow) return
if (event.origin !== "null" || event.source !== frame.contentWindow) return;
const [channel, params] = event.data
const response = channels[channel as keyof typeof channels](...params)
frame.contentWindow!.postMessage({response: await response}, "*")
const [channel, params] = event.data;
const response = channels[channel as keyof typeof channels](...params);
frame.contentWindow!.postMessage({ response: await response }, "*");
}
function runPlugin() {
@@ -125,21 +133,29 @@
charaChannels: Object.keys(channels),
},
"*",
)
);
}
let frame: HTMLIFrameElement
let editor: HTMLDivElement
let editorView: EditorView
let frame: HTMLIFrameElement;
let editor: HTMLDivElement;
let editorView: EditorView;
</script>
<svelte:window on:message={onMessage} />
<section>
<button on:click={runPlugin}><span class="icon">play_arrow</span>{$LL.plugin.editor.RUN()}</button>
<button on:click={runPlugin}
><span class="icon">play_arrow</span>{$LL.plugin.editor.RUN()}</button
>
<div class="editor-root" bind:this={editor} />
</section>
<iframe aria-hidden="true" title="code sandbox" bind:this={frame} src="/sandbox/" sandbox="allow-scripts" />
<iframe
aria-hidden="true"
title="code sandbox"
bind:this={frame}
src="/sandbox/"
sandbox="allow-scripts"
/>
<style lang="scss">
section {
+9 -9
View File
@@ -11,21 +11,21 @@
*
*/
const count = await Chara.getChordCount() // => 499
const chord = await Chara.getChord(2) // => {actions: [1, 2, 3], phrase: [4, 5, 6]}
const count = await Chara.getChordCount(); // => 499
const chord = await Chara.getChord(2); // => {actions: [1, 2, 3], phrase: [4, 5, 6]}
const setting = await Chara.getSetting(5) // => 0
const setting = await Chara.getSetting(5); // => 0
// This, for example, would return all chords
const chords = []
const chords = [];
for (let i = 0; i < count; i++) {
chords.push(await Chara.getChord(i))
chords.push(await Chara.getChord(i));
}
// You can also print values to the browser console (F12)
console.log("Chords:", chords)
console.log("Chords:", chords);
// You can access the actions by ID!
Actions.SPACE // => {id: "SPACE", code: 32, icon: "space_bar", description: ...}
Actions[32] // This also works
Actions[0x20] // Or this!
Actions.SPACE; // => {id: "SPACE", code: 32, icon: "space_bar", description: ...}
Actions[32]; // This also works
Actions[0x20]; // Or this!
+6 -6
View File
@@ -1,16 +1,16 @@
import type {RegisterSWOptions} from "vite-plugin-pwa/types"
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")
const { pwaInfo } = await import("virtual:pwa-info");
// @ts-expect-error confused TS
const {registerSW} = await import("virtual:pwa-register")
const { registerSW } = await import("virtual:pwa-register");
registerSW({
immediate: true,
onRegisterError(error) {
console.log("ServiceWorker Registration Error", error)
console.log("ServiceWorker Registration Error", error);
},
} satisfies RegisterSWOptions)
} satisfies RegisterSWOptions);
return pwaInfo ? pwaInfo.webManifest.linkTag : ""
return pwaInfo ? pwaInfo.webManifest.linkTag : "";
}
+22 -22
View File
@@ -1,43 +1,43 @@
<script>
let ongoingRequest
let resolveRequest
let source
let ongoingRequest;
let resolveRequest;
let source;
async function post(channel, args) {
while (ongoingRequest) {
await ongoingRequest
await ongoingRequest;
}
ongoingRequest = new Promise(resolve => {
resolveRequest = resolve
source.postMessage([channel, args], "*")
})
ongoingRequest = new Promise((resolve) => {
resolveRequest = resolve;
source.postMessage([channel, args], "*");
});
ongoingRequest.then(() => {
ongoingRequest = undefined
})
return ongoingRequest
ongoingRequest = undefined;
});
return ongoingRequest;
}
window.addEventListener("message", event => {
window.addEventListener("message", (event) => {
if ("response" in event.data) {
resolveRequest(event.data.response)
resolveRequest(event.data.response);
} else {
source = event.source
source = event.source;
var Action = event.data.actionCodes
var Action = event.data.actionCodes;
Object.assign(
Action,
Object.fromEntries(
Object.values(event.data.actionCodes)
.filter(it => !!it.id)
.map(it => [it.id, it]),
.filter((it) => !!it.id)
.map((it) => [it.id, it]),
),
)
);
var Chara = {}
var Chara = {};
for (const fn of event.data.charaChannels) {
Chara[fn] = (...args) => post(fn, args)
Chara[fn] = (...args) => post(fn, args);
}
eval(`(async function(){${event.data.script}})()`)
eval(`(async function(){${event.data.script}})()`);
}
})
});
</script>
+1 -1
View File
@@ -1,5 +1,5 @@
<script>
import Terminal from "$lib/components/Terminal.svelte"
import Terminal from "$lib/components/Terminal.svelte";
</script>
<section class="terminal">
+5 -2
View File
@@ -1,7 +1,10 @@
<script lang="ts">
import {LL} from "../../i18n/i18n-svelte"
import { LL } from "../../i18n/i18n-svelte";
</script>
<h1>{$LL.update.TITLE()}</h1>
<a href="https://github.com/CharaChorder/CCOS-firmware/blob/main/CHANGELOG.md" target="_blank">Changelog</a>
<a
href="https://github.com/CharaChorder/CCOS-firmware/blob/main/CHANGELOG.md"
target="_blank">Changelog</a
>
+65 -46
View File
@@ -1,10 +1,10 @@
<script lang="ts">
import {chords} from "$lib/undo-redo"
import Action from "$lib/components/Action.svelte"
import {onDestroy, onMount} from "svelte"
import {KEYMAP_CODES} from "$lib/serial/keymap-codes"
import {fly} from "svelte/transition"
import type {Chord} from "$lib/serial/chord"
import { chords } from "$lib/undo-redo";
import Action from "$lib/components/Action.svelte";
import { onDestroy, onMount } from "svelte";
import { KEYMAP_CODES } from "$lib/serial/keymap-codes";
import { fly } from "svelte/transition";
import type { Chord } from "$lib/serial/chord";
const speedRating = [
[400, "+100", "excited", true],
@@ -12,7 +12,7 @@
[1400, "+25", "neutral", true],
[3000, "0", "dissatisfied", false],
[Infinity, "-50", "sad", false],
] as const
] as const;
const accuracyRating = [
[2, "+100", "calm", true],
[3, "+50", "content", false],
@@ -20,73 +20,82 @@
[7, "0", "frustrated", false],
[14, "-25", "very_dissatisfied", false],
[Infinity, "-50", "extremely_dissatisfied", false],
] as const
] as const;
let next: Chord[] = []
let nextHandle: number
let took: number | undefined
let delta = 0
let next: Chord[] = [];
let nextHandle: number;
let took: number | undefined;
let delta = 0;
let speed: readonly [number, string, string, boolean] | undefined
let accuracy: readonly [number, string, string, boolean] | undefined
let progress = 0
let speed: readonly [number, string, string, boolean] | undefined;
let accuracy: readonly [number, string, string, boolean] | undefined;
let progress = 0;
let attempts = 0
let attempts = 0;
let userInput = ""
let userInput = "";
onMount(() => {
runTest()
})
runTest();
});
function runTest() {
if (took === undefined) {
took = performance.now()
delta = 0
attempts = 0
userInput = ""
took = performance.now();
delta = 0;
attempts = 0;
userInput = "";
if (next.length === 0) {
next = Array.from({length: 5}, () => $chords[Math.floor(Math.random() * $chords.length)])
next = Array.from(
{ length: 5 },
() => $chords[Math.floor(Math.random() * $chords.length)],
);
} else {
next.shift()
next.push($chords[Math.floor(Math.random() * $chords.length)])
next = next
next.shift();
next.push($chords[Math.floor(Math.random() * $chords.length)]);
next = next;
}
}
if (userInput === next[0].phrase.map(it => (it === 32 ? " " : KEYMAP_CODES[it]!.id)).join("") + " ") {
took = undefined
speed = speedRating.find(([max]) => delta <= max)
accuracy = accuracyRating.find(([max]) => attempts <= max)
progress++
if (
userInput ===
next[0].phrase
.map((it) => (it === 32 ? " " : KEYMAP_CODES[it]!.id))
.join("") +
" "
) {
took = undefined;
speed = speedRating.find(([max]) => delta <= max);
accuracy = accuracyRating.find(([max]) => attempts <= max);
progress++;
} else {
delta = performance.now() - took
delta = performance.now() - took;
}
nextHandle = requestAnimationFrame(runTest)
nextHandle = requestAnimationFrame(runTest);
}
let debounceTimer = 0
let debounceTimer = 0;
function backspace(event: KeyboardEvent) {
if (event.code === "Backspace") {
userInput = userInput.slice(0, -1)
userInput = userInput.slice(0, -1);
}
}
function input(event: KeyboardEvent) {
const stamp = performance.now()
const stamp = performance.now();
if (stamp - debounceTimer > 50) {
attempts++
attempts++;
}
debounceTimer = stamp
userInput += event.key
debounceTimer = stamp;
userInput += event.key;
}
onDestroy(() => {
if (nextHandle) {
cancelAnimationFrame(nextHandle)
cancelAnimationFrame(nextHandle);
}
})
});
</script>
<svelte:window on:keydown={backspace} on:keypress={input} />
@@ -96,18 +105,28 @@
{#if next[0]}
<div class="row">
{#key progress}
<div in:fly={{duration: 300, x: -48}} out:fly={{duration: 1000, x: 128}} class="rating">
<div
in:fly={{ duration: 300, x: -48 }}
out:fly={{ duration: 1000, x: 128 }}
class="rating"
>
{#if speed}
<span class="rating-item">
<span style:color="var(--md-sys-color-{speed[3] ? `primary` : `error`})" class="icon">timer</span>
<span
style:color="var(--md-sys-color-{speed[3] ? `primary` : `error`})"
class="icon">timer</span
>
{speed[1]}
<span class="icon">sentiment_{speed[2]}</span>
</span>
{/if}
{#if accuracy}
<span class="rating-item">
<span style:color="var(--md-sys-color-{accuracy[3] ? `primary` : `error`})" class="icon"
>target</span
<span
style:color="var(--md-sys-color-{accuracy[3]
? `primary`
: `error`})"
class="icon">target</span
>
{accuracy[1]}
<span class="icon">sentiment_{accuracy[2]}</span>