Files
DeviceManager/src/routes/config/chords/+page.svelte

218 lines
5.1 KiB
Svelte

<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"
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)
})
onDestroy(() => {
resizeObserver?.disconnect()
})
$: searchIndex = $chords?.length > 0 ? buildIndex($chords) : undefined
function buildIndex(chords: ChordInfo[]): Index {
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)
.join(""),
)
}
})
return index
}
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
}
function insertChord(actions: number[]) {
const id = JSON.stringify(actions)
if ($chords.some(it => JSON.stringify(it.actions) === id)) {
alert($LL.configure.chords.DUPLICATE())
return
}
changes.update(changes => {
changes.push({
type: ChangeType.Chord,
id: actions,
actions,
phrase: [],
})
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),
)
const lastPage = derived(
[items, pageSize],
([items, pageSize]) => Math.ceil((items.length + 1) / pageSize) - 1,
)
setContext("cursor-crossfade", crossfade({}))
let page = 0
</script>
<svelte:head>
<title>Chord Manager - CharaChorder Device Manager</title>
<meta name="description" content="Manage your chords" />
</svelte:head>
<div class="search-container">
<input
type="search"
placeholder={$LL.configure.chords.search.PLACEHOLDER($chords.length)}
on:input={search}
/>
<div class="paginator">
{#if $lastPage !== -1}
{page + 1} / {$lastPage + 1}
{:else}
- / -
{/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.min(page + 1, $lastPage))}
use:action={{shortcut: "ctrl+right"}}>navigate_next</button
>
</div>
<section bind:this={results}>
<table>
{#if page === 0}
<tr
><th class="new-chord"><ChordActionEdit on:submit={({detail}) => insertChord(detail)} /></th><td /><td
/></tr
>
{/if}
{#if $lastPage !== -1}
{#each $items.slice(page * $pageSize - (page === 0 ? 0 : 1), (page + 1) * $pageSize - 1) as [chord] (JSON.stringify(chord.id))}
<tr>
<ChordEdit {chord} />
</tr>
{/each}
{:else}
<caption>{$LL.configure.chords.search.NO_RESULTS()}</caption>
{/if}
</table>
<textarea placeholder={$LL.configure.chords.TRY_TYPING()}></textarea>
</section>
<style lang="scss">
.search-container {
display: flex;
align-items: center;
justify-content: center;
}
.paginator {
display: flex;
justify-content: flex-end;
min-width: 8ch;
}
.new-chord :global(.add) {
visibility: hidden;
}
textarea {
transition: border-color 250ms ease;
background: none;
color: inherit;
border: 1px dashed var(--md-sys-color-surface-variant);
padding: 8px;
border-radius: 4px;
&:focus {
outline: none;
border-color: var(--md-sys-color-primary);
}
}
caption {
margin-top: 156px;
}
input[type="search"] {
width: 512px;
margin-block-start: 16px;
padding-block: 8px;
padding-inline: 16px;
font-size: 16px;
color: inherit;
background: none;
border: 0 solid var(--md-sys-color-surface-variant);
border-bottom-width: 1px;
transition: all 250ms ease;
&::placeholder {
color: var(--md-sys-color-on-surface-variant);
opacity: 0.2;
}
&:focus {
border-color: var(--md-sys-color-primary);
outline: none;
}
}
section {
position: relative;
display: flex;
overflow: hidden;
height: 100%;
padding-inline: 8px;
border-radius: 16px;
}
table {
height: fit-content;
overflow: hidden;
min-width: min(90vw, 16.5cm);
transition: all 1s ease;
}
</style>