mirror of
https://github.com/CharaChorder/DeviceManager.git
synced 2026-01-05 09:32:53 +00:00
218 lines
5.1 KiB
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>
|