mirror of
https://github.com/CharaChorder/DeviceManager.git
synced 2026-01-22 18:02:42 +00:00
feat: layout editing (sorta)
This commit is contained in:
@@ -62,6 +62,11 @@ const config: IconsConfig = {
|
|||||||
"upload_file",
|
"upload_file",
|
||||||
"commit",
|
"commit",
|
||||||
"bug_report",
|
"bug_report",
|
||||||
|
"delete",
|
||||||
|
"remove_selection",
|
||||||
|
"bolt",
|
||||||
|
"undo",
|
||||||
|
"redo",
|
||||||
],
|
],
|
||||||
codePoints: {
|
codePoints: {
|
||||||
speed: "e9e4",
|
speed: "e9e4",
|
||||||
|
|||||||
@@ -2,6 +2,12 @@ import type {Translation} from "../i18n-types"
|
|||||||
|
|
||||||
const de = {
|
const de = {
|
||||||
TITLE: "amaCC1ng",
|
TITLE: "amaCC1ng",
|
||||||
|
saveActions: {
|
||||||
|
UNDO: "Rückgängig",
|
||||||
|
REDO: "Wiederholen",
|
||||||
|
APPLY: "Anwenden",
|
||||||
|
SAVE: "Änderungen auf das Gerät schreiben",
|
||||||
|
},
|
||||||
backup: {
|
backup: {
|
||||||
TITLE: "Sicherungskopie",
|
TITLE: "Sicherungskopie",
|
||||||
DISCLAIMER:
|
DISCLAIMER:
|
||||||
@@ -9,6 +15,14 @@ const de = {
|
|||||||
DOWNLOAD: "Kopie Speichern",
|
DOWNLOAD: "Kopie Speichern",
|
||||||
RESTORE: "Wiederherstellen",
|
RESTORE: "Wiederherstellen",
|
||||||
},
|
},
|
||||||
|
modal: {
|
||||||
|
CLOSE: "Schließen",
|
||||||
|
},
|
||||||
|
actionSearch: {
|
||||||
|
PLACEHOLDER: "Nach Aktionen suchen",
|
||||||
|
CURRENT_ACTION: "Aktuelle Aktion",
|
||||||
|
DELETE: "Entfernen",
|
||||||
|
},
|
||||||
share: {
|
share: {
|
||||||
URL_COPIED: "Teilbare URL kopiert!",
|
URL_COPIED: "Teilbare URL kopiert!",
|
||||||
EXTRA_DOWNLOAD: "Als Datei herunterladen",
|
EXTRA_DOWNLOAD: "Als Datei herunterladen",
|
||||||
|
|||||||
@@ -2,12 +2,26 @@ import type {BaseTranslation} from "../i18n-types"
|
|||||||
|
|
||||||
const en = {
|
const en = {
|
||||||
TITLE: "amaCC1ng",
|
TITLE: "amaCC1ng",
|
||||||
|
saveActions: {
|
||||||
|
UNDO: "Undo",
|
||||||
|
REDO: "Redo",
|
||||||
|
APPLY: "Apply",
|
||||||
|
SAVE: "Write changes to your device",
|
||||||
|
},
|
||||||
backup: {
|
backup: {
|
||||||
TITLE: "Local Backup",
|
TITLE: "Local Backup",
|
||||||
DISCLAIMER: "Backups remain on your computer and are never shared with us or uploaded to our servers.",
|
DISCLAIMER: "Backups remain on your computer and are never shared with us or uploaded to our servers.",
|
||||||
DOWNLOAD: "Download Backup",
|
DOWNLOAD: "Download Backup",
|
||||||
RESTORE: "Restore",
|
RESTORE: "Restore",
|
||||||
},
|
},
|
||||||
|
modal: {
|
||||||
|
CLOSE: "Close",
|
||||||
|
},
|
||||||
|
actionSearch: {
|
||||||
|
PLACEHOLDER: "Search for actions",
|
||||||
|
CURRENT_ACTION: "Current action",
|
||||||
|
DELETE: "Remove",
|
||||||
|
},
|
||||||
share: {
|
share: {
|
||||||
URL_COPIED: "Sharable URL copied!",
|
URL_COPIED: "Sharable URL copied!",
|
||||||
EXTRA_DOWNLOAD: "Download as file",
|
EXTRA_DOWNLOAD: "Download as file",
|
||||||
|
|||||||
@@ -1,59 +0,0 @@
|
|||||||
import type {Action} from "svelte/action"
|
|
||||||
import Index from "flexsearch"
|
|
||||||
import {KEYMAP_CODES} from "$lib/serial/keymap-codes"
|
|
||||||
import tippy from "tippy.js"
|
|
||||||
import ActionAutocomplete from "$lib/components/ActionAutocomplete.svelte"
|
|
||||||
import {browser} from "$app/environment"
|
|
||||||
|
|
||||||
const index = browser ? new Index({tokenize: "full"}) : undefined
|
|
||||||
for (const action of Object.values(KEYMAP_CODES)) {
|
|
||||||
index?.add(
|
|
||||||
action.code,
|
|
||||||
`${action.title || ""} ${action.variant || ""} ${action.category} ${action.id || ""} ${
|
|
||||||
action.description || ""
|
|
||||||
}`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
const exact = Object.fromEntries(
|
|
||||||
Object.values(KEYMAP_CODES)
|
|
||||||
.filter(it => !!it.id)
|
|
||||||
.map(it => [it.id, it] as const),
|
|
||||||
)
|
|
||||||
|
|
||||||
export const actionAutocomplete: Action<HTMLInputElement> = node => {
|
|
||||||
if (!browser) return
|
|
||||||
|
|
||||||
let completionComponent: ActionAutocomplete
|
|
||||||
const completionDialog = tippy(node, {
|
|
||||||
interactive: true,
|
|
||||||
placement: "bottom-start",
|
|
||||||
hideOnClick: false,
|
|
||||||
theme: "surface-variant search-completion",
|
|
||||||
arrow: false,
|
|
||||||
trigger: "focus",
|
|
||||||
offset: [0, 0],
|
|
||||||
onCreate(instance) {
|
|
||||||
const target = instance.popper.querySelector(".tippy-content")!
|
|
||||||
completionComponent = new ActionAutocomplete({target, props: {width: node.clientWidth}})
|
|
||||||
},
|
|
||||||
onDestroy() {
|
|
||||||
completionComponent.$destroy()
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
function input(event: Event) {
|
|
||||||
completionComponent.$set({
|
|
||||||
results: index!.search(node.value),
|
|
||||||
exact: exact[node.value],
|
|
||||||
code: Number(node.value),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
node.addEventListener("input", input)
|
|
||||||
|
|
||||||
return {
|
|
||||||
destroy() {
|
|
||||||
node.removeEventListener("input", input)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import type {Chord} from "$lib/serial/chord"
|
|
||||||
import {KEYMAP_CODES} from "$lib/serial/keymap-codes"
|
|
||||||
|
|
||||||
interface Language {
|
|
||||||
name: string
|
|
||||||
noLazyMode?: boolean
|
|
||||||
orderedByFrequency?: boolean
|
|
||||||
words: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function calculateChordCoverage(chords: Chord[]) {
|
|
||||||
const language: Language = await fetch("/languages/english.json").then(it => it.json())
|
|
||||||
|
|
||||||
const words = new Set(language.words)
|
|
||||||
for (const chord of chords) {
|
|
||||||
words.delete(chord.phrase.map(it => KEYMAP_CODES[it].id!).join(""))
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
coverage: words.size / language.words.length,
|
|
||||||
missing: [...words.values()],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import ActionListItem from "$lib/components/ActionListItem.svelte"
|
|
||||||
|
|
||||||
export let exact: number | undefined = undefined
|
|
||||||
export let code: number = Number.NaN
|
|
||||||
export let results: number[] = []
|
|
||||||
|
|
||||||
export let width: number
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="list" style="width: {width}px">
|
|
||||||
{#if exact !== undefined}
|
|
||||||
<div class="exact">
|
|
||||||
<i>Exact match</i>
|
|
||||||
<ActionListItem id={exact} />
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{#if !exact && code}
|
|
||||||
{#if code >= 2 ** 5 && code < 2 ** 13}
|
|
||||||
<button>USE CODE</button>
|
|
||||||
{:else}
|
|
||||||
<div>Action code is out of range</div>
|
|
||||||
{/if}
|
|
||||||
{/if}
|
|
||||||
{#each results as id (id)}
|
|
||||||
<ActionListItem {id} />
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.list {
|
|
||||||
--scrollbar-color: var(--md-sys-color-on-surface-variant);
|
|
||||||
|
|
||||||
scrollbar-gutter: stable both-edges;
|
|
||||||
|
|
||||||
overflow-x: hidden;
|
|
||||||
overflow-y: auto;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
max-height: 500px;
|
|
||||||
padding-block: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.exact {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
border: 1px solid var(--md-sys-color-primary);
|
|
||||||
border-radius: 8px;
|
|
||||||
|
|
||||||
> i {
|
|
||||||
padding-inline: 8px;
|
|
||||||
color: var(--md-sys-color-on-primary);
|
|
||||||
background: var(--md-sys-color-primary);
|
|
||||||
border-radius: 0 0 8px 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
$: key = (typeof id === "number" ? KEYMAP_CODES[id] ?? id : id) as number | KeyInfo
|
$: key = (typeof id === "number" ? KEYMAP_CODES[id] ?? id : id) as number | KeyInfo
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<button>
|
<button on:click>
|
||||||
{#if typeof key === "object"}
|
{#if typeof key === "object"}
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<b>
|
<b>
|
||||||
@@ -43,6 +43,13 @@
|
|||||||
|
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
color: var(--md-sys-color-on-surface-variant);
|
||||||
|
background: var(--md-sys-color-surface-variant);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
|
|||||||
@@ -1,77 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import LayoutCC1 from "$lib/components/layout/LayoutCC1.svelte"
|
|
||||||
import {chords, highlightActions} from "$lib/serial/connection"
|
|
||||||
import {KEYMAP_CODES} from "$lib/serial/keymap-codes.js"
|
|
||||||
|
|
||||||
$: content = Array.from({length: 10}).map(() => $chords[Math.floor(Math.random() * $chords.length)])
|
|
||||||
|
|
||||||
let cursor = [0, 0]
|
|
||||||
let input = []
|
|
||||||
|
|
||||||
$: {
|
|
||||||
$highlightActions = content[cursor[0]]?.actions ?? []
|
|
||||||
}
|
|
||||||
|
|
||||||
function keypress(event: KeyboardEvent) {
|
|
||||||
cursor++
|
|
||||||
input.push(event.key)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<svelte:window on:keypress={keypress} />
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<section>
|
|
||||||
<!-- <div class="cursor" style="translate: calc({cursor}ch - 50%) -50%" /> -->
|
|
||||||
{#each content as word, i}
|
|
||||||
{#if word}
|
|
||||||
{#each word.phrase as letter, j}
|
|
||||||
<span>{KEYMAP_CODES[letter].id}</span>
|
|
||||||
{/each}
|
|
||||||
|
|
||||||
{/if}
|
|
||||||
{/each}
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<LayoutCC1 />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
div {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-evenly;
|
|
||||||
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
section {
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
|
|
||||||
font-size: 1.3rem;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.letter {
|
|
||||||
position: relative;
|
|
||||||
filter: brightness(50%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cursor {
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 0;
|
|
||||||
translate: -50% -50%;
|
|
||||||
|
|
||||||
width: 2px;
|
|
||||||
height: 1em;
|
|
||||||
|
|
||||||
background: var(--md-sys-color-primary);
|
|
||||||
|
|
||||||
transition: all 250ms ease;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,167 +1,306 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {KEYMAP_CODES} from "$lib/serial/keymap-codes.js"
|
import {KEYMAP_CODES} from "$lib/serial/keymap-codes"
|
||||||
import charaActions from "$lib/assets/keymaps/chara-chorder.yml"
|
import type {KeyInfo} from "$lib/serial/keymap-codes"
|
||||||
import mouseActions from "$lib/assets/keymaps/mouse.yml"
|
import Index from "flexsearch"
|
||||||
import keyboardActions from "$lib/assets/keymaps/keyboard.yml"
|
import {createEventDispatcher} from "svelte"
|
||||||
import asciiActions from "$lib/assets/keymaps/ascii.yml"
|
import ActionListItem from "$lib/components/ActionListItem.svelte"
|
||||||
import cp1252Actions from "$lib/assets/keymaps/cp-1252.yml"
|
import LL from "../../../i18n/i18n-svelte"
|
||||||
import FlexSearch from "flexsearch"
|
|
||||||
|
|
||||||
const index = new FlexSearch({tokenize: "full"})
|
export let currentAction: number
|
||||||
|
|
||||||
for (const code in KEYMAP_CODES) {
|
const index = new Index({tokenize: "full"})
|
||||||
const key = KEYMAP_CODES[code]
|
for (const action of Object.values(KEYMAP_CODES)) {
|
||||||
index.add(
|
index?.add(
|
||||||
code,
|
action.code,
|
||||||
`${key.id || key.code} ${key.title || ""} ${key.variant || ""} ${key.description || ""}`.trim(),
|
`${action.title || ""} ${action.variant || ""} ${action.category} ${action.id || ""} ${
|
||||||
|
action.description || ""
|
||||||
|
}`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
const exactIndex: Record<string, KeyInfo> = Object.fromEntries(
|
||||||
|
Object.values(KEYMAP_CODES)
|
||||||
|
.filter(it => !!it.id)
|
||||||
|
.map(it => [it.id, it] as const),
|
||||||
|
)
|
||||||
|
|
||||||
function search() {
|
function search() {
|
||||||
const query = searchInput.value
|
results = index!.search(searchBox.value)
|
||||||
customValue = query && !Number.isNaN(Number(query)) ? Number(query) : undefined
|
exact = exactIndex[searchBox.value]?.code
|
||||||
results = query ? index.search(searchInput.value) : defaultActions
|
code = Number(searchBox.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
let customValue: number | undefined = undefined
|
function select(id?: number) {
|
||||||
const defaultActions: string[] = [
|
if (id !== undefined) {
|
||||||
charaActions,
|
dispatch("select", id)
|
||||||
mouseActions,
|
}
|
||||||
keyboardActions,
|
}
|
||||||
asciiActions,
|
|
||||||
cp1252Actions,
|
function keyboardNavigation(event: KeyboardEvent) {
|
||||||
].flatMap(it => Object.keys(it.actions))
|
if (event.shiftKey && event.key === "Enter") {
|
||||||
let results: string[] = defaultActions
|
dispatch("select", exact)
|
||||||
let searchInput: HTMLInputElement
|
} else if (event.shiftKey && event.key === "Escape") {
|
||||||
|
dispatch("select", 0)
|
||||||
|
} else if (event.key === "Escape") {
|
||||||
|
dispatch("close")
|
||||||
|
} else if (event.key === "ArrowDown") {
|
||||||
|
const element =
|
||||||
|
resultList.querySelector("li:focus-within")?.nextSibling ?? resultList.querySelector("li:not(.exact)")
|
||||||
|
if (element instanceof HTMLLIElement) {
|
||||||
|
element.querySelector("button")?.focus()
|
||||||
|
}
|
||||||
|
} else if (event.key === "ArrowUp") {
|
||||||
|
const element =
|
||||||
|
resultList.querySelector("li:focus-within")?.previousSibling ??
|
||||||
|
resultList.querySelector("li:not(.exact)")
|
||||||
|
if (element instanceof HTMLLIElement) {
|
||||||
|
element.querySelector("button")?.focus()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
searchBox.focus()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
event.preventDefault()
|
||||||
|
}
|
||||||
|
|
||||||
|
let results: number[] = []
|
||||||
|
let exact: number | undefined = undefined
|
||||||
|
let code: number = Number.NaN
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
let searchBox: HTMLInputElement
|
||||||
|
let resultList: HTMLUListElement
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section>
|
<svelte:window on:keydown={keyboardNavigation} />
|
||||||
<input type="search" on:input={search} placeholder="Search Actions" bind:this={searchInput} />
|
|
||||||
|
|
||||||
<div class="results">
|
<dialog open on:click|self={() => dispatch("close")}>
|
||||||
{#if customValue !== undefined}
|
<div class="content">
|
||||||
<button class="custom">
|
<div class="search-row">
|
||||||
Custom ActionID
|
<input
|
||||||
<span class="key">0x{customValue.toString(16).toUpperCase()}</span>
|
type="search"
|
||||||
</button>
|
bind:this={searchBox}
|
||||||
{/if}
|
autofocus
|
||||||
|
on:input={search}
|
||||||
{#each results as id}
|
on:keypress={event => {
|
||||||
{@const key = KEYMAP_CODES[id]}
|
if (event.key === "Enter") {
|
||||||
<button title={key.description}>
|
select(exact)
|
||||||
<div class="title">
|
}
|
||||||
<b>
|
}}
|
||||||
{key.title || ""}
|
placeholder={$LL.actionSearch.PLACEHOLDER()}
|
||||||
{#if key.variant === "left"}
|
/>
|
||||||
(Left)
|
<button on:click={() => select(0)}
|
||||||
{:else if key.variant === "right"}
|
><div><span class="icon key-hint">shift</span>+<span class="key-hint">ESC</span></div>
|
||||||
(Right)
|
{$LL.actionSearch.DELETE()}</button
|
||||||
{/if}
|
>
|
||||||
</b>
|
<button title={$LL.modal.CLOSE()} class="icon" on:click={() => dispatch("close")}>close</button>
|
||||||
{#if key.description}
|
|
||||||
<i>{key.description}</i>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
<span class:icon={!!key.icon} class="key">{key.icon || key.id || key.code}</span>
|
<aside>
|
||||||
</button>
|
<h3>{$LL.actionSearch.CURRENT_ACTION()}</h3>
|
||||||
|
<ActionListItem id={currentAction} />
|
||||||
|
</aside>
|
||||||
|
<ul bind:this={resultList}>
|
||||||
|
{#if exact !== undefined}
|
||||||
|
<li class="exact">
|
||||||
|
<i
|
||||||
|
>Exact match <span class="icon key-hint">shift</span>+<span class="icon key-hint"
|
||||||
|
>keyboard_return</span
|
||||||
|
></i
|
||||||
|
>
|
||||||
|
<ActionListItem id={exact} on:click={() => select(exact)} />
|
||||||
|
</li>
|
||||||
|
{/if}
|
||||||
|
{#if !exact && code}
|
||||||
|
{#if code >= 2 ** 5 && code < 2 ** 13}
|
||||||
|
<li><button on:click={() => select(code)}>USE CODE</button></li>
|
||||||
|
{:else}
|
||||||
|
<li>Action code is out of range</li>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
{#each results as id (id)}
|
||||||
|
<li><ActionListItem {id} on:click={() => select(id)} /></li>
|
||||||
{/each}
|
{/each}
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</dialog>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
section {
|
dialog {
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
|
|
||||||
width: calc(min(100vw - 10px, 512px));
|
|
||||||
height: calc(min(90vh, 600px));
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="search"] {
|
|
||||||
width: 100%;
|
|
||||||
height: 48px;
|
|
||||||
padding-inline: 16px;
|
|
||||||
|
|
||||||
font-family: "Noto Sans Mono", monospace;
|
|
||||||
font-size: 18px;
|
|
||||||
color: var(--md-sys-color-on-primary);
|
|
||||||
|
|
||||||
background: var(--md-sys-color-primary);
|
|
||||||
border: none;
|
|
||||||
border-radius: 24px;
|
|
||||||
|
|
||||||
&::placeholder {
|
|
||||||
color: inherit;
|
|
||||||
opacity: 0.3;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
content: "plus";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.key {
|
|
||||||
overflow: hidden;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
min-width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
padding: 4px;
|
|
||||||
|
|
||||||
font-size: 18px;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
|
|
||||||
border: 1px solid var(--md-sys-color-outline);
|
|
||||||
border-radius: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 4px;
|
|
||||||
align-items: flex-start;
|
|
||||||
|
|
||||||
text-align: start;
|
|
||||||
|
|
||||||
> b {
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
font-family: "Noto Sans Mono", monospace;
|
background: rgba(0 0 0 / 60%);
|
||||||
font-size: 14px;
|
|
||||||
color: inherit;
|
|
||||||
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom {
|
aside {
|
||||||
padding: 8px;
|
pointer-events: none;
|
||||||
padding-inline-start: 16px;
|
|
||||||
|
margin: 8px;
|
||||||
|
|
||||||
|
opacity: 0.4;
|
||||||
border: 1px dashed var(--md-sys-color-outline);
|
border: 1px dashed var(--md-sys-color-outline);
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
> h3 {
|
||||||
|
width: fit-content;
|
||||||
|
margin-block-start: -13px;
|
||||||
|
margin-block-end: 0;
|
||||||
|
margin-inline-start: 16px;
|
||||||
|
padding-inline: 8px;
|
||||||
|
|
||||||
|
background: var(--md-sys-color-background);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin-inline: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
align-items: center;
|
||||||
|
margin-inline: 16px;
|
||||||
|
|
||||||
|
> button {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
height: fit-content;
|
||||||
|
|
||||||
|
color: currentcolor;
|
||||||
|
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
border-radius: 100%;
|
||||||
|
|
||||||
|
&:not(.icon) {
|
||||||
|
font-family: inherit;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > div {
|
||||||
|
display: flex;
|
||||||
|
gap: 2px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
aspect-ratio: 1;
|
||||||
|
color: var(--md-sys-color-on-surface-variant);
|
||||||
|
background: var(--md-sys-color-surface-variant);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
width: calc(min(30cm, 90%));
|
||||||
|
height: calc(min(100% - 128px, 90%));
|
||||||
|
|
||||||
|
color: var(--md-sys-color-on-background);
|
||||||
|
|
||||||
|
background: var(--md-sys-color-background);
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.results {
|
input[type="search"] {
|
||||||
overflow-y: scroll;
|
width: 100%;
|
||||||
|
height: 64px;
|
||||||
|
margin-block-end: 8px;
|
||||||
|
padding-inline: 16px;
|
||||||
|
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 16px;
|
||||||
|
color: currentcolor;
|
||||||
|
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 1px solid var(--md-sys-color-primary-container);
|
||||||
|
|
||||||
|
transition: all 250ms ease;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-bottom: 1px solid var(--md-sys-color-primary);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
--scrollbar-color: var(--md-sys-color-surface-variant);
|
||||||
|
|
||||||
|
scrollbar-gutter: both-edges stable;
|
||||||
|
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
box-sizing: border-box;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
padding-inline: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exact {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 8px;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
height: 100%;
|
width: 100%;
|
||||||
|
margin-block-start: 8px;
|
||||||
|
|
||||||
|
border: 1px solid var(--md-sys-color-primary);
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
> i {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
padding-inline: 6px;
|
||||||
|
|
||||||
|
color: var(--md-sys-color-on-primary);
|
||||||
|
|
||||||
|
background: var(--md-sys-color-primary);
|
||||||
|
border-radius: 0 0 8px 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.key-hint {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
height: 20px;
|
||||||
|
margin-block: 6px;
|
||||||
|
padding: 2px;
|
||||||
|
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: normal;
|
||||||
|
color: currentcolor;
|
||||||
|
|
||||||
|
border: 1px solid currentcolor;
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
&.icon {
|
||||||
|
padding: 0;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -13,11 +13,6 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<select bind:value={device}>
|
|
||||||
<option value="ONE">CC1</option>
|
|
||||||
<option value="LITE">Lite</option>
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<fieldset>
|
<fieldset>
|
||||||
{#each layers as [title, icon, value]}
|
{#each layers as [title, icon, value]}
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -7,14 +7,14 @@
|
|||||||
<div class="col layout" style="gap: 0">
|
<div class="col layout" style="gap: 0">
|
||||||
<div class="row" style="gap: 156px">
|
<div class="row" style="gap: 156px">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<RingInput {activeLayer} keys={{d: 30, e: 31, n: 32, w: 33, s: 34}} type="tertiary" />
|
<RingInput {activeLayer} keys={{d: 30, e: 31, n: 32, w: 33, s: 34}} />
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<RingInput {activeLayer} keys={{d: 25, e: 26, n: 27, w: 28, s: 29}} />
|
<RingInput {activeLayer} keys={{d: 25, e: 26, n: 27, w: 28, s: 29}} />
|
||||||
<RingInput {activeLayer} keys={{d: 40, e: 41, n: 42, w: 43, s: 44}} type="secondary" />
|
<RingInput {activeLayer} keys={{d: 40, e: 41, n: 42, w: 43, s: 44}} />
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<RingInput {activeLayer} keys={{d: 20, e: 21, n: 22, w: 23, s: 24}} />
|
<RingInput {activeLayer} keys={{d: 20, e: 21, n: 22, w: 23, s: 24}} />
|
||||||
<RingInput {activeLayer} keys={{d: 35, e: 36, n: 37, w: 38, s: 39}} type="secondary" />
|
<RingInput {activeLayer} keys={{d: 35, e: 36, n: 37, w: 38, s: 39}} />
|
||||||
</div>
|
</div>
|
||||||
<RingInput {activeLayer} keys={{d: 15, e: 16, n: 17, w: 18, s: 19}} />
|
<RingInput {activeLayer} keys={{d: 15, e: 16, n: 17, w: 18, s: 19}} />
|
||||||
</div>
|
</div>
|
||||||
@@ -41,8 +41,8 @@
|
|||||||
<RingInput {activeLayer} keys={{d: 50, w: 51, n: 52, e: 53, s: 54}} />
|
<RingInput {activeLayer} keys={{d: 50, w: 51, n: 52, e: 53, s: 54}} />
|
||||||
</div>
|
</div>
|
||||||
<div class="row" style="gap: 320px; margin-top: -12px">
|
<div class="row" style="gap: 320px; margin-top: -12px">
|
||||||
<RingInput {activeLayer} keys={{d: 0, e: 1, n: 2, w: 3, s: 4}} type="secondary" />
|
<RingInput {activeLayer} keys={{d: 0, e: 1, n: 2, w: 3, s: 4}} />
|
||||||
<RingInput {activeLayer} keys={{d: 45, w: 46, n: 47, e: 48, s: 49}} type="secondary" />
|
<RingInput {activeLayer} keys={{d: 45, w: 46, n: 47, e: 48, s: 49}} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {highlightActions, layout} from "$lib/serial/connection"
|
import {changes, highlightActions, layout} from "$lib/serial/connection"
|
||||||
|
import type {Change} from "$lib/serial/connection"
|
||||||
import type {CharaLayout} from "$lib/serialization/layout"
|
import type {CharaLayout} from "$lib/serialization/layout"
|
||||||
import {KEYMAP_CODES} from "$lib/serial/keymap-codes"
|
import {KEYMAP_CODES} from "$lib/serial/keymap-codes"
|
||||||
import type {KeyInfo} from "$lib/serial/keymap-codes"
|
import type {KeyInfo} from "$lib/serial/keymap-codes"
|
||||||
@@ -7,9 +8,6 @@
|
|||||||
|
|
||||||
export let activeLayer = 0
|
export let activeLayer = 0
|
||||||
export let keys: Record<"d" | "s" | "n" | "w" | "e", number>
|
export let keys: Record<"d" | "s" | "n" | "w" | "e", number>
|
||||||
export let type: "primary" | "secondary" | "tertiary" = "primary"
|
|
||||||
|
|
||||||
const layerNames = ["Primary Layer", "Number Layer", "Function Layer"]
|
|
||||||
|
|
||||||
const virtualLayerMap = [1, 0, 2]
|
const virtualLayerMap = [1, 0, 2]
|
||||||
const characterOffset = 8
|
const characterOffset = 8
|
||||||
@@ -20,26 +18,32 @@
|
|||||||
return 25 * quadrant + layerOffsetIndex * layerOffset
|
return 25 * quadrant + layerOffsetIndex * layerOffset
|
||||||
}
|
}
|
||||||
|
|
||||||
function getActions(id: number, layout: CharaLayout): KeyInfo[] {
|
function getActions(id: number, layout: CharaLayout, changes: Change[]): [KeyInfo, KeyInfo | undefined][] {
|
||||||
return Array.from({length: 3}).map((_, i) => {
|
return Array.from({length: 3}).map((_, i) => {
|
||||||
const actionId = layout?.[i][id]
|
const actionId = layout?.[i][id]
|
||||||
return KEYMAP_CODES[actionId]
|
const changedId = changes.findLast(it => it?.layout?.[i]?.[id] !== undefined)?.layout![i]![id]
|
||||||
|
if (changedId !== undefined) {
|
||||||
|
return [KEYMAP_CODES[changedId], KEYMAP_CODES[actionId]]
|
||||||
|
} else {
|
||||||
|
return [KEYMAP_CODES[actionId], undefined]
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="radial {type}">
|
<div class="radial">
|
||||||
{#each [keys.n, keys.e, keys.s, keys.w, keys.d] as id, quadrant}
|
{#each [keys.n, keys.e, keys.s, keys.w, keys.d] as id, quadrant}
|
||||||
{@const actions = getActions(id, $layout)}
|
{@const actions = getActions(id, $layout, $changes)}
|
||||||
<button
|
<button
|
||||||
use:editableLayout={{id, quadrant}}
|
use:editableLayout={{activeLayer, id}}
|
||||||
class:active={actions.some(it => it && $highlightActions?.includes(it.code))}
|
class:active={actions.some(([it]) => it && $highlightActions?.includes(it.code))}
|
||||||
>
|
>
|
||||||
{#each actions as keyInfo, layer}
|
{#each actions as [keyInfo, old], layer}
|
||||||
{#if keyInfo}
|
{#if keyInfo}
|
||||||
<span
|
<span
|
||||||
class:active={virtualLayerMap[activeLayer] === virtualLayerMap[layer]}
|
class:active={virtualLayerMap[activeLayer] === virtualLayerMap[layer]}
|
||||||
class:icon={!!keyInfo.icon}
|
class:icon={!!keyInfo.icon}
|
||||||
|
class:changed={!!old}
|
||||||
style="offset-distance: {offsetDistance(quadrant, layer, activeLayer)}%"
|
style="offset-distance: {offsetDistance(quadrant, layer, activeLayer)}%"
|
||||||
>{keyInfo.icon || keyInfo.id || keyInfo.code}</span
|
>{keyInfo.icon || keyInfo.id || keyInfo.code}</span
|
||||||
>
|
>
|
||||||
@@ -95,7 +99,9 @@
|
|||||||
|
|
||||||
opacity: 0.2;
|
opacity: 0.2;
|
||||||
|
|
||||||
transition: scale $transition-time ease, opacity $transition-time ease,
|
transition:
|
||||||
|
scale $transition-time ease,
|
||||||
|
opacity $transition-time ease,
|
||||||
offset-distance $transition-time ease;
|
offset-distance $transition-time ease;
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
@@ -107,6 +113,11 @@
|
|||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.changed {
|
||||||
|
color: var(--md-sys-color-on-secondary-container);
|
||||||
|
background: var(--md-sys-color-secondary-container);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
@@ -170,12 +181,4 @@
|
|||||||
mask-image: none;
|
mask-image: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.secondary > button {
|
|
||||||
filter: brightness(80%) contrast(120%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tertiary > button {
|
|
||||||
filter: brightness(80%) contrast(110%);
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,32 +1,35 @@
|
|||||||
import tippy from "tippy.js"
|
|
||||||
import InputEdit from "$lib/components/layout/InputEdit.svelte"
|
|
||||||
import type {Action} from "svelte/action"
|
import type {Action} from "svelte/action"
|
||||||
|
import ActionSelector from "$lib/components/layout/ActionSelector.svelte"
|
||||||
|
import {changes, layout} from "$lib/serial/connection"
|
||||||
|
import {get} from "svelte/store"
|
||||||
|
|
||||||
export const editableLayout: Action<HTMLButtonElement, {id: number; quadrant: number}> = (
|
export const editableLayout: Action<HTMLButtonElement, {activeLayer: number; id: number}> = (
|
||||||
node,
|
node,
|
||||||
{id, quadrant},
|
{id, activeLayer},
|
||||||
) => {
|
) => {
|
||||||
let component: InputEdit | undefined
|
let component: ActionSelector | undefined
|
||||||
const edit = tippy(node, {
|
function present() {
|
||||||
interactive: true,
|
|
||||||
appendTo: document.body,
|
|
||||||
trigger: "click",
|
|
||||||
placement: (["top", "right", "bottom", "left"] as const)[quadrant],
|
|
||||||
onShow(instance) {
|
|
||||||
component ??= new InputEdit({
|
|
||||||
target: instance.popper.querySelector(".tippy-content")!,
|
|
||||||
props: {id},
|
|
||||||
})
|
|
||||||
},
|
|
||||||
onHidden() {
|
|
||||||
component?.$destroy()
|
component?.$destroy()
|
||||||
component = undefined
|
component = new ActionSelector({
|
||||||
},
|
target: document.body,
|
||||||
|
props: {currentAction: get(layout)[activeLayer][id]},
|
||||||
})
|
})
|
||||||
|
component.$on("close", () => {
|
||||||
|
component!.$destroy()
|
||||||
|
})
|
||||||
|
component.$on("select", ({detail}) => {
|
||||||
|
changes.update(changes => {
|
||||||
|
changes.push({layout: {[activeLayer]: {[id]: detail}}})
|
||||||
|
return changes
|
||||||
|
})
|
||||||
|
component!.$destroy()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
node.addEventListener("click", present)
|
||||||
return {
|
return {
|
||||||
destroy() {
|
destroy() {
|
||||||
edit.destroy()
|
node.removeEventListener("click", present)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,14 @@ export const layout = persistentWritable<CharaLayout>(
|
|||||||
() => get(userPreferences).backup,
|
() => get(userPreferences).backup,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export interface Change {
|
||||||
|
layout?: Record<number, Record<number, number>>
|
||||||
|
chords?: never
|
||||||
|
settings?: Record<number, number>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const changes = persistentWritable<Change[]>("changes", [])
|
||||||
|
|
||||||
export const settings = writable({})
|
export const settings = writable({})
|
||||||
|
|
||||||
export const unsavedChanges = writable(new Map<number, number>())
|
export const unsavedChanges = writable(new Map<number, number>())
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script>
|
<script>
|
||||||
import {page} from "$app/stores"
|
import {page} from "$app/stores"
|
||||||
import LL from "../../i18n/i18n-svelte"
|
import LL from "../i18n/i18n-svelte"
|
||||||
|
|
||||||
$: paths = [
|
$: paths = [
|
||||||
{href: "/config/chords/", title: $LL.configure.chords.TITLE(), icon: "piano"},
|
{href: "/config/chords/", title: $LL.configure.chords.TITLE(), icon: "piano"},
|
||||||
85
src/routes/EditActions.svelte
Normal file
85
src/routes/EditActions.svelte
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import LL from "../i18n/i18n-svelte"
|
||||||
|
import {changes} from "$lib/serial/connection"
|
||||||
|
import type {Change} from "$lib/serial/connection"
|
||||||
|
import {fly} from "svelte/transition"
|
||||||
|
|
||||||
|
function undo() {
|
||||||
|
redoQueue = [$changes.pop()!, ...redoQueue]
|
||||||
|
changes.update(it => it)
|
||||||
|
}
|
||||||
|
|
||||||
|
function redo() {
|
||||||
|
const [change, ...queue] = redoQueue
|
||||||
|
changes.update(it => {
|
||||||
|
it.push(change)
|
||||||
|
return it
|
||||||
|
})
|
||||||
|
redoQueue = queue
|
||||||
|
}
|
||||||
|
let redoQueue: Change[] = []
|
||||||
|
|
||||||
|
function apply() {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<button title={$LL.saveActions.UNDO()} class="icon" disabled={$changes.length === 0} on:click={undo}
|
||||||
|
>undo</button
|
||||||
|
>
|
||||||
|
<button title={$LL.saveActions.REDO()} class="icon" disabled={redoQueue.length === 0} on:click={redo}
|
||||||
|
>redo</button
|
||||||
|
>
|
||||||
|
<div class="separator" />
|
||||||
|
<button title={$LL.saveActions.SAVE()} class="icon">save</button>
|
||||||
|
{#if $changes.length !== 0}
|
||||||
|
<button class="click-me" transition:fly={{x: 8}}
|
||||||
|
><span class="icon">bolt</span>{$LL.saveActions.APPLY()}</button
|
||||||
|
>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
button {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
color: currentcolor;
|
||||||
|
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
|
||||||
|
transition: all 250ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
:disabled {
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.click-me {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
margin-inline: 8px;
|
||||||
|
padding-block: 2px;
|
||||||
|
padding-inline-start: 4px;
|
||||||
|
padding-inline-end: 8px;
|
||||||
|
|
||||||
|
font-family: inherit;
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--md-sys-color-primary);
|
||||||
|
|
||||||
|
border: 2px solid var(--md-sys-color-primary);
|
||||||
|
border-radius: 18px;
|
||||||
|
outline: 2px dashed var(--md-sys-color-primary);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.separator {
|
||||||
|
width: 1px;
|
||||||
|
height: 24px;
|
||||||
|
background: var(--md-sys-color-outline-variant);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -20,6 +20,7 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
opacity: 0.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {serialPort, syncStatus, unsavedChanges} from "$lib/serial/connection"
|
import {serialPort, syncStatus, unsavedChanges} from "$lib/serial/connection"
|
||||||
import {page} from "$app/stores"
|
|
||||||
import {slide, fly} from "svelte/transition"
|
import {slide, fly} from "svelte/transition"
|
||||||
import {canShare, triggerShare} from "$lib/share"
|
import {canShare, triggerShare} from "$lib/share"
|
||||||
import {popup} from "$lib/popup"
|
import {popup} from "$lib/popup"
|
||||||
@@ -11,17 +10,8 @@
|
|||||||
import {userPreferences} from "$lib/preferences"
|
import {userPreferences} from "$lib/preferences"
|
||||||
import LL from "../i18n/i18n-svelte"
|
import LL from "../i18n/i18n-svelte"
|
||||||
import Profile from "./Profile.svelte"
|
import Profile from "./Profile.svelte"
|
||||||
|
import ConfigTabs from "./ConfigTabs.svelte"
|
||||||
const training = [
|
import EditActions from "./EditActions.svelte"
|
||||||
{slug: "cpm", title: "CPM - Characters Per Minute", icon: "music_note"},
|
|
||||||
{slug: "chords", title: "ChM - Chords Mastered", icon: "piano"},
|
|
||||||
{slug: "avg-wpm", title: "aWPM - Average Words Per Minute", icon: "avg_pace"},
|
|
||||||
{slug: "sentences", title: "StM - Sentences Mastered", icon: "lyrics"},
|
|
||||||
{slug: "top-wpm", title: "tWPM - Top Words Per Minute", icon: "speed"},
|
|
||||||
{slug: "cm", title: "CM - Concepts Mastered", icon: "cognition"},
|
|
||||||
]
|
|
||||||
|
|
||||||
let placeboProgress = false
|
|
||||||
|
|
||||||
async function flashChanges() {
|
async function flashChanges() {
|
||||||
$syncStatus = "uploading"
|
$syncStatus = "uploading"
|
||||||
@@ -49,19 +39,12 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<nav>
|
<nav>
|
||||||
<a href="/" class="title">{$LL.TITLE()}</a>
|
<div class="actions">
|
||||||
|
<EditActions />
|
||||||
<div class="steps">
|
|
||||||
{#each training as {slug, title, icon}}
|
|
||||||
<a
|
|
||||||
href="/train/{slug}/"
|
|
||||||
{title}
|
|
||||||
class="icon train {slug}"
|
|
||||||
class:active={$page.url.pathname === `/train/${slug}/`}>{icon}</a
|
|
||||||
>
|
|
||||||
{/each}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ConfigTabs />
|
||||||
|
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
{#if $canShare}
|
{#if $canShare}
|
||||||
<button transition:fly={{x: -8}} class="icon" on:click={triggerShare}>share</button>
|
<button transition:fly={{x: -8}} class="icon" on:click={triggerShare}>share</button>
|
||||||
@@ -73,8 +56,13 @@
|
|||||||
{/await}
|
{/await}
|
||||||
{/if}
|
{/if}
|
||||||
{#if $unsavedChanges.size > 0}
|
{#if $unsavedChanges.size > 0}
|
||||||
<button disabled={$syncStatus === 'uploading'} on:click={flashChanges} transition:fly={{x: -8}}
|
<button
|
||||||
title={$LL.deviceManager.APPLY_SETTINGS()} class="icon">save
|
disabled={$syncStatus === "uploading"}
|
||||||
|
on:click={flashChanges}
|
||||||
|
transition:fly={{x: -8}}
|
||||||
|
title={$LL.deviceManager.APPLY_SETTINGS()}
|
||||||
|
class="icon"
|
||||||
|
>save
|
||||||
</button>
|
</button>
|
||||||
<div transition:slide class="separator" />
|
<div transition:slide class="separator" />
|
||||||
{/if}
|
{/if}
|
||||||
@@ -167,16 +155,20 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
nav {
|
nav {
|
||||||
display: flex;
|
display: grid;
|
||||||
|
grid-template-columns: 1fr auto 1fr;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
|
|
||||||
|
width: calc(min(100%, 28cm));
|
||||||
margin-block: 8px;
|
margin-block: 8px;
|
||||||
margin-inline: 16px;
|
margin-inline: auto;
|
||||||
|
padding-inline: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
margin-block: 0;
|
margin-block: 0;
|
||||||
|
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
@@ -195,7 +187,7 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
aspect-ratio: 1;
|
aspect-ratio: 1;
|
||||||
padding: 4px;
|
padding: 2px;
|
||||||
|
|
||||||
color: inherit;
|
color: inherit;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
@@ -210,49 +202,16 @@
|
|||||||
color: var(--md-sys-color-on-error);
|
color: var(--md-sys-color-on-error);
|
||||||
background: var(--md-sys-color-error);
|
background: var(--md-sys-color-error);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.active,
|
|
||||||
&:active {
|
|
||||||
color: var(--md-sys-color-on-primary);
|
|
||||||
background: var(--md-sys-color-primary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.steps {
|
|
||||||
position: absolute;
|
|
||||||
left: 50%;
|
|
||||||
translate: -50% 0;
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
> a.icon {
|
|
||||||
aspect-ratio: unset;
|
|
||||||
margin-inline: -4px;
|
|
||||||
padding-inline: 16px;
|
|
||||||
|
|
||||||
font-size: 24px;
|
|
||||||
color: var(--md-sys-on-surface-variant);
|
|
||||||
|
|
||||||
background: var(--md-sys-color-surface-variant);
|
|
||||||
clip-path: polygon(25% 50%, 0% 0%, 75% 0%, 100% 50%, 75% 100%, 0% 100%);
|
|
||||||
border-radius: 0;
|
|
||||||
|
|
||||||
&.active,
|
|
||||||
&:active {
|
|
||||||
color: var(--md-sys-color-on-tertiary);
|
|
||||||
background: var(--md-sys-color-tertiary);
|
|
||||||
|
|
||||||
&,
|
|
||||||
~ * {
|
|
||||||
translate: 8px 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.actions {
|
.actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon.account {
|
.icon.account {
|
||||||
|
|||||||
@@ -2,10 +2,8 @@
|
|||||||
import {chords} from "$lib/serial/connection"
|
import {chords} from "$lib/serial/connection"
|
||||||
import {KEYMAP_CODES} from "$lib/serial/keymap-codes"
|
import {KEYMAP_CODES} from "$lib/serial/keymap-codes"
|
||||||
import Index from "flexsearch"
|
import Index from "flexsearch"
|
||||||
import {tick} from "svelte"
|
|
||||||
import type {Chord} from "$lib/serial/chord"
|
import type {Chord} from "$lib/serial/chord"
|
||||||
import LL from "../../../i18n/i18n-svelte"
|
import LL from "../../../i18n/i18n-svelte"
|
||||||
import {actionAutocomplete} from "$lib/action-autocomplete"
|
|
||||||
|
|
||||||
$: searchIndex = $chords?.length > 0 ? buildIndex($chords) : undefined
|
$: searchIndex = $chords?.length > 0 ? buildIndex($chords) : undefined
|
||||||
|
|
||||||
@@ -20,11 +18,8 @@
|
|||||||
let searchFilter: number[] | undefined
|
let searchFilter: number[] | undefined
|
||||||
|
|
||||||
function search(event: Event) {
|
function search(event: Event) {
|
||||||
document.startViewTransition(async () => {
|
|
||||||
const query = (event.target as HTMLInputElement).value
|
const query = (event.target as HTMLInputElement).value
|
||||||
searchFilter = query && searchIndex ? searchIndex.search(query) : undefined
|
searchFilter = query && searchIndex ? searchIndex.search(query) : undefined
|
||||||
await tick()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$: items = searchFilter?.map(it => [$chords[it], it] as const) ?? $chords.map((it, i) => [it, i] as const)
|
$: items = searchFilter?.map(it => [$chords[it], it] as const) ?? $chords.map((it, i) => [it, i] as const)
|
||||||
@@ -38,7 +33,7 @@
|
|||||||
<input
|
<input
|
||||||
type="search"
|
type="search"
|
||||||
placeholder={$LL.configure.chords.search.PLACEHOLDER($chords.length)}
|
placeholder={$LL.configure.chords.search.PLACEHOLDER($chords.length)}
|
||||||
use:actionAutocomplete
|
on:input={search}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
import {redirect} from "@sveltejs/kit"
|
|
||||||
import type {PageLoad} from "./$types"
|
|
||||||
|
|
||||||
export const load = (() => {
|
|
||||||
throw redirect(302, "/train/cpm/")
|
|
||||||
}) satisfies PageLoad
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<script>
|
|
||||||
import TypingInput from "$lib/components/TypingInput.svelte"
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<TypingInput />
|
|
||||||
Reference in New Issue
Block a user