mirror of
https://github.com/CharaChorder/DeviceManager.git
synced 2026-07-26 22:44:45 +00:00
refactor: use standard prettier formatting
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<script>
|
||||
import {page} from "$app/stores"
|
||||
import { page } from "$app/stores";
|
||||
</script>
|
||||
|
||||
<h1>{$page.status}</h1>
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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,5 +1,5 @@
|
||||
<script>
|
||||
import LL from "../../i18n/i18n-svelte"
|
||||
import LL from "../../i18n/i18n-svelte";
|
||||
</script>
|
||||
|
||||
{$LL.share.URL_COPIED()}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>→</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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"] {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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!
|
||||
|
||||
@@ -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 : "";
|
||||
}
|
||||
|
||||
@@ -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,5 +1,5 @@
|
||||
<script>
|
||||
import Terminal from "$lib/components/Terminal.svelte"
|
||||
import Terminal from "$lib/components/Terminal.svelte";
|
||||
</script>
|
||||
|
||||
<section class="terminal">
|
||||
|
||||
@@ -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
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user