mirror of
https://github.com/CharaChorder/DeviceManager.git
synced 2026-01-22 09:52:50 +00:00
feat: layout action search prototype
[deploy]
This commit is contained in:
167
src/lib/components/layout/ActionSelector.svelte
Normal file
167
src/lib/components/layout/ActionSelector.svelte
Normal file
@@ -0,0 +1,167 @@
|
||||
<script lang="ts">
|
||||
import {KEYMAP_CODES} from "$lib/serial/keymap-codes.js"
|
||||
import charaActions from "$lib/assets/keymaps/chara-chorder.yml"
|
||||
import mouseActions from "$lib/assets/keymaps/mouse.yml"
|
||||
import keyboardActions from "$lib/assets/keymaps/keyboard.yml"
|
||||
import asciiActions from "$lib/assets/keymaps/ascii.yml"
|
||||
import cp1252Actions from "$lib/assets/keymaps/cp-1252.yml"
|
||||
import FlexSearch from "flexsearch"
|
||||
|
||||
const index = new FlexSearch({tokenize: "full"})
|
||||
|
||||
for (const code in KEYMAP_CODES) {
|
||||
const key = KEYMAP_CODES[code]
|
||||
index.add(
|
||||
code,
|
||||
`${key.id || key.code} ${key.title || ""} ${key.variant || ""} ${key.description || ""}`.trim(),
|
||||
)
|
||||
}
|
||||
|
||||
function search() {
|
||||
const query = searchInput.value
|
||||
customValue = query && !Number.isNaN(Number(query)) ? Number(query) : undefined
|
||||
results = query ? index.search(searchInput.value) : defaultActions
|
||||
}
|
||||
|
||||
let customValue = undefined
|
||||
const defaultActions: string[] = [
|
||||
charaActions,
|
||||
mouseActions,
|
||||
keyboardActions,
|
||||
asciiActions,
|
||||
cp1252Actions,
|
||||
].flatMap(it => Object.keys(it.actions))
|
||||
let results: string[] = defaultActions
|
||||
let searchInput: HTMLInputElement
|
||||
</script>
|
||||
|
||||
<section>
|
||||
<input type="search" on:input={search} placeholder="Search Actions" bind:this={searchInput} />
|
||||
|
||||
<div class="results">
|
||||
{#if customValue !== undefined}
|
||||
<button class="custom">
|
||||
Custom ActionID
|
||||
<span class="key">0x{customValue.toString(16).toUpperCase()}</span>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
{#each results as id}
|
||||
{@const key = KEYMAP_CODES[id]}
|
||||
<button title={key.description}>
|
||||
<div class="title">
|
||||
<b>
|
||||
{key.title || ""}
|
||||
{#if key.variant === "left"}
|
||||
(Left)
|
||||
{:else if key.variant === "right"}
|
||||
(Right)
|
||||
{/if}
|
||||
</b>
|
||||
{#if key.description}
|
||||
<i>{key.description}</i>
|
||||
{/if}
|
||||
</div>
|
||||
<span class:icon={!!key.icon} class="key">{key.icon || key.id || key.code}</span>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style lang="scss">
|
||||
section {
|
||||
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;
|
||||
align-items: 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%;
|
||||
|
||||
font-family: "Noto Sans Mono", monospace;
|
||||
font-size: 14px;
|
||||
color: inherit;
|
||||
|
||||
background: transparent;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.custom {
|
||||
padding: 8px;
|
||||
padding-inline-start: 16px;
|
||||
border: 1px dashed var(--md-sys-color-outline);
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.results {
|
||||
overflow-y: scroll;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
54
src/lib/components/layout/InputEdit.svelte
Normal file
54
src/lib/components/layout/InputEdit.svelte
Normal file
@@ -0,0 +1,54 @@
|
||||
<script lang="ts">
|
||||
import {layout} from "$lib/serial/connection"
|
||||
import {KEYMAP_CODES} from "$lib/serial/keymap-codes"
|
||||
import ActionSelector from "$lib/components/layout/ActionSelector.svelte"
|
||||
import {popup} from "$lib/popup"
|
||||
|
||||
export let id: number = 0
|
||||
</script>
|
||||
|
||||
<table>
|
||||
{#each $layout as layer, i}
|
||||
{@const action = KEYMAP_CODES[layer[id]]}
|
||||
<tr>
|
||||
<th class="icon">counter_{i + 1}</th>
|
||||
<td
|
||||
><button use:popup={ActionSelector}
|
||||
>{action?.title || action?.id} <span class="icon">edit</span></button
|
||||
></td
|
||||
>
|
||||
</tr>
|
||||
{/each}
|
||||
</table>
|
||||
|
||||
<style lang="scss">
|
||||
span.icon {
|
||||
opacity: 0;
|
||||
transition: opacity 250ms ease;
|
||||
}
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
width: 100%;
|
||||
|
||||
font-family: "Noto Sans Mono", monospace;
|
||||
font-weight: 600;
|
||||
color: var(--md-sys-color-on-surface);
|
||||
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
|
||||
transition: all 250ms ease;
|
||||
}
|
||||
|
||||
button:hover > span.icon {
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
134
src/lib/components/layout/LayoutCC1.svelte
Normal file
134
src/lib/components/layout/LayoutCC1.svelte
Normal file
@@ -0,0 +1,134 @@
|
||||
<script>
|
||||
import RingInput from "$lib/components/layout/RingInput.svelte"
|
||||
|
||||
let activeLayer = 0
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<fieldset>
|
||||
{#each [["Numeric Layer", "123", 1], ["Primary Layer", "abc", 0], ["Function Layer", "function", 2]] as [title, icon, value]}
|
||||
<button
|
||||
{title}
|
||||
class="icon"
|
||||
on:click={() => (activeLayer = value)}
|
||||
class:active={activeLayer === value}
|
||||
>
|
||||
{icon}
|
||||
</button>
|
||||
{/each}
|
||||
</fieldset>
|
||||
|
||||
<div class="col layout" style="gap: 0">
|
||||
<div class="row" style="gap: 156px">
|
||||
<div class="row">
|
||||
<RingInput {activeLayer} keys={{d: 30, e: 31, n: 32, w: 33, s: 34}} type="tertiary" />
|
||||
<div class="col">
|
||||
<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" />
|
||||
</div>
|
||||
<div class="col">
|
||||
<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" />
|
||||
</div>
|
||||
<RingInput {activeLayer} keys={{d: 15, e: 16, n: 17, w: 18, s: 19}} />
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<RingInput {activeLayer} keys={{d: 60, w: 61, n: 62, e: 63, s: 64}} />
|
||||
<div class="col">
|
||||
<RingInput {activeLayer} keys={{d: 65, w: 66, n: 67, e: 68, s: 69}} />
|
||||
<RingInput {activeLayer} keys={{d: 80, w: 81, n: 82, e: 83, s: 84}} />
|
||||
</div>
|
||||
<div class="col">
|
||||
<RingInput {activeLayer} keys={{d: 70, w: 71, n: 72, e: 73, s: 74}} />
|
||||
<RingInput {activeLayer} keys={{d: 85, w: 86, n: 87, e: 88, s: 89}} />
|
||||
</div>
|
||||
<RingInput {activeLayer} keys={{d: 75, w: 76, n: 77, e: 78, s: 79}} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="row" style="gap: 48px; margin-top: -32px">
|
||||
<RingInput {activeLayer} keys={{d: 10, e: 11, n: 12, w: 13, s: 14}} />
|
||||
<RingInput {activeLayer} keys={{d: 55, w: 56, n: 57, e: 58, s: 59}} />
|
||||
</div>
|
||||
<div class="row" style="gap: 160px">
|
||||
<RingInput {activeLayer} keys={{d: 5, e: 6, n: 7, w: 8, s: 9}} />
|
||||
<RingInput {activeLayer} keys={{d: 50, w: 51, n: 52, e: 53, s: 54}} />
|
||||
</div>
|
||||
<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: 45, w: 46, n: 47, e: 48, s: 49}} type="secondary" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
fieldset {
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
margin-block-end: -36px;
|
||||
padding: 0;
|
||||
|
||||
border: none;
|
||||
}
|
||||
|
||||
button.icon {
|
||||
cursor: pointer;
|
||||
|
||||
z-index: 1;
|
||||
|
||||
font-size: 24px;
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
|
||||
background: var(--md-sys-color-surface-variant);
|
||||
border: none;
|
||||
|
||||
transition: all 250ms ease;
|
||||
|
||||
&:nth-child(2) {
|
||||
z-index: 2;
|
||||
|
||||
aspect-ratio: 1;
|
||||
|
||||
font-size: 32px;
|
||||
|
||||
border-radius: 50%;
|
||||
outline: 8px solid var(--md-sys-color-background);
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
padding-inline-end: 16px;
|
||||
border-radius: 16px 0 0 16px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
padding-inline-start: 16px;
|
||||
border-radius: 0 16px 16px 0;
|
||||
}
|
||||
|
||||
&.active {
|
||||
font-weight: 900;
|
||||
color: var(--md-sys-color-on-tertiary);
|
||||
background: var(--md-sys-color-tertiary);
|
||||
}
|
||||
}
|
||||
|
||||
.row,
|
||||
.col {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.row {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.col {
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
166
src/lib/components/layout/RingInput.svelte
Normal file
166
src/lib/components/layout/RingInput.svelte
Normal file
@@ -0,0 +1,166 @@
|
||||
<script lang="ts">
|
||||
import {highlightActions, layout} from "$lib/serial/connection"
|
||||
import type {CharaLayout} from "$lib/serialization/layout"
|
||||
import {KEYMAP_CODES} from "$lib/serial/keymap-codes"
|
||||
import type {KeyInfo} from "$lib/serial/keymap-codes"
|
||||
import {editableLayout} from "$lib/editable-layout"
|
||||
|
||||
export let activeLayer = 0
|
||||
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 characterOffset = 8
|
||||
|
||||
function offsetDistance(quadrant: number, layer: number, activeLayer: number): number {
|
||||
const layerOffsetIndex = virtualLayerMap[layer] - virtualLayerMap[activeLayer]
|
||||
const layerOffset = quadrant > 2 ? -characterOffset : characterOffset
|
||||
return 25 * quadrant + layerOffsetIndex * layerOffset
|
||||
}
|
||||
|
||||
function getActions(id: number, layout: CharaLayout): KeyInfo[] {
|
||||
return Array.from({length: 3}).map((_, i) => {
|
||||
const actionId = layout?.[i][id]
|
||||
return KEYMAP_CODES[actionId]
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="radial {type}">
|
||||
{#each [keys.n, keys.e, keys.s, keys.w] as id, quadrant}
|
||||
{@const actions = getActions(id, $layout)}
|
||||
<button
|
||||
use:editableLayout={{id, quadrant}}
|
||||
class:active={actions.some(it => it && $highlightActions?.includes(it.code))}
|
||||
>
|
||||
{#each actions as keyInfo, layer}
|
||||
{#if keyInfo}
|
||||
<span
|
||||
class:active={virtualLayerMap[activeLayer] === virtualLayerMap[layer]}
|
||||
class:icon={!!keyInfo.icon}
|
||||
style="offset-distance: {offsetDistance(quadrant, layer, activeLayer)}%"
|
||||
>{keyInfo.icon || keyInfo.id || keyInfo.code}</span
|
||||
>
|
||||
{/if}
|
||||
{/each}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@use "sass:math";
|
||||
|
||||
$border-width: 18px;
|
||||
$gap: 6px;
|
||||
$size: 96;
|
||||
$offset: 14;
|
||||
$scale-difference: 0.2;
|
||||
$transition-time: 750ms;
|
||||
|
||||
.radial {
|
||||
position: relative;
|
||||
|
||||
container: radial / size;
|
||||
|
||||
width: #{$size * 1px};
|
||||
height: #{$size * 1px};
|
||||
|
||||
transition: all 250ms ease;
|
||||
}
|
||||
|
||||
span {
|
||||
$cr: math.div($size, 2) - 2 * $offset;
|
||||
|
||||
will-change: scale, offset-distance;
|
||||
user-select: none;
|
||||
|
||||
scale: 0.9;
|
||||
offset-path: path(
|
||||
"M#{math.div($size, 2)} #{$offset}A#{$cr} #{$cr} 0 1 1 #{math.div($size, 2)} #{$size - $offset}A#{$cr} #{$cr} 0 1 1 #{math.div($size, 2)} #{$offset}Z"
|
||||
);
|
||||
offset-rotate: 0deg;
|
||||
|
||||
display: flex;
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
font-size: 16px;
|
||||
|
||||
opacity: 0.2;
|
||||
|
||||
transition: scale $transition-time ease, opacity $transition-time ease,
|
||||
offset-distance $transition-time ease;
|
||||
|
||||
&.active {
|
||||
scale: 1;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.icon {
|
||||
font-size: 20px;
|
||||
font-weight: 800;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
|
||||
position: absolute;
|
||||
|
||||
display: grid;
|
||||
|
||||
width: 100cqw;
|
||||
height: 100cqh;
|
||||
padding: 0;
|
||||
|
||||
font-family: "Noto Sans Mono", monospace;
|
||||
font-size: 16px;
|
||||
font-weight: 900;
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
|
||||
background: var(--md-sys-color-surface-variant);
|
||||
border: none;
|
||||
|
||||
transition: all 250ms ease;
|
||||
|
||||
mask-image: url("$lib/assets/quater-ring.svg");
|
||||
mask-size: 100% 100%;
|
||||
|
||||
&.active,
|
||||
&:active {
|
||||
color: var(--md-sys-color-on-tertiary);
|
||||
background: var(--md-sys-color-tertiary);
|
||||
}
|
||||
|
||||
&:nth-child(1) {
|
||||
clip-path: polygon(50% 50%, 0 0, 100% 0);
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
clip-path: polygon(50% 50%, 100% 0, 100% 100%);
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
clip-path: polygon(50% 50%, 0 100%, 100% 100%);
|
||||
}
|
||||
|
||||
&:nth-child(4) {
|
||||
clip-path: polygon(50% 50%, 0 0, 0 100%);
|
||||
}
|
||||
}
|
||||
|
||||
.secondary > button {
|
||||
filter: brightness(80%) contrast(120%);
|
||||
}
|
||||
|
||||
.tertiary > button {
|
||||
filter: brightness(80%) contrast(110%);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user