feat: editing

This commit is contained in:
2023-11-02 00:16:18 +01:00
parent fade2f978e
commit ef309d603e
22 changed files with 409 additions and 517 deletions

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import {parseCompressed, stringifyCompressed} from "$lib/serial/serialization"
import {chords, layout} from "$lib/serial/connection"
import {deviceChords, deviceLayout} from "$lib/serial/connection"
import {preference} from "$lib/preferences"
import type {Chord} from "$lib/serial/chord"
import type {CharaLayout} from "$lib/serialization/layout"
@@ -16,8 +16,8 @@
const downloadUrl = URL.createObjectURL(
await stringifyCompressed({
isCharaBackup: "v1.0",
chords: $chords,
layout: $layout,
chords: $deviceChords,
layout: $deviceLayout,
}),
)
const element = document.createElement("a")
@@ -34,10 +34,10 @@
const backup = await parseCompressed<Backup>(input)
if (backup.isCharaBackup !== "v1.0") throw new Error("Invalid Backup")
if (backup.chords) {
$chords = backup.chords
$deviceChords = backup.chords
}
if (backup.layout) {
$layout = backup.layout
$deviceLayout = backup.layout
}
}
</script>

View File

@@ -1,9 +1,10 @@
<script lang="ts">
import LL from "../i18n/i18n-svelte"
import {changes} from "$lib/serial/connection"
import type {Change} from "$lib/serial/connection"
import {changes, ChangeType, chords, layout, 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, deviceSettings, serialPort, syncStatus} from "$lib/serial/connection"
function undo() {
redoQueue = [$changes.pop()!, ...redoQueue]
@@ -20,8 +21,54 @@
}
let redoQueue: Change[] = []
function apply() {
// TODO
async function apply() {
const port = $serialPort
if (!port) return
$syncStatus = "uploading"
for (const change of $changes) {
switch (change.type) {
case ChangeType.Layout:
await port.setLayoutKey(change.layer + 1, change.id, change.action)
break
case ChangeType.Chord:
if (change.phrase) {
await port.setChord({actions: change.actions, phrase: change.phrase})
} else {
await port.deleteChord({actions: change.actions})
}
break
case ChangeType.Setting:
await port.setSetting(change.id, change.setting)
break
}
}
$deviceLayout = $layout.map(layer => layer.map<number>(({action}) => action)) as [
number[],
number[],
number[],
]
$deviceChords = $chords.map(({actions, phrase}) => ({actions, phrase}))
$deviceSettings = $settings.map(({value}) => value)
$changes = []
$syncStatus = "done"
}
async function flashChanges() {
$syncStatus = "uploading"
// Yes, this is a completely arbitrary and unnecessary delay.
// The only purpose of it is to create a sense of weight,
// aka make it more "energy intensive" to click.
// The only conceivable way users could reach the commit limit in this case
// 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!"
await new Promise(resolve => setTimeout(resolve, 6000))
if ($serialPort) {
await $serialPort.commit()
$changes = []
}
$syncStatus = "done"
}
</script>
@@ -107,7 +154,11 @@
on:click={redo}>redo</button
>
<div class="separator" />
<button use:action={{title: $LL.saveActions.SAVE(), shortcut: "ctrl+shift+s"}} class="icon">save</button>
<button
use:action={{title: $LL.saveActions.SAVE(), shortcut: "ctrl+shift+s"}}
on:click={flashChanges}
class="icon">save</button
>
{#if $changes.length !== 0}
<button
class="click-me"
@@ -121,73 +172,6 @@
{/if}
<style lang="scss">
.pacman {
position: relative;
aspect-ratio: 1;
height: 32px;
background: currentcolor;
border: 8px solid currentcolor;
border-radius: 100%;
outline: 6px solid currentcolor;
outline-offset: 2px;
animation: pacman 0.2s linear infinite alternate-reverse;
&::before {
content: "";
position: absolute;
top: 0;
left: 25%;
width: 200%;
height: 100%;
background: var(--md-sys-color-background);
animation: squish 0.2s linear infinite alternate-reverse;
border-radius: 16px;
}
&::after {
content: "c c o s";
position: absolute;
display: flex;
width: 500%;
animation: go 1s linear infinite;
}
}
@keyframes go {
from {
translate: 0 0;
}
to {
translate: -100% 0;
}
}
@keyframes squish {
from {
scale: 1;
}
to {
scale: 1 0.5;
}
}
@keyframes pacman {
to {
scale: 1;
}
to {
scale: 0.9;
}
}
.click-me {
display: flex;
align-items: center;

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import {serialPort, syncStatus, unsavedChanges} from "$lib/serial/connection"
import {serialPort, syncStatus} from "$lib/serial/connection"
import {slide, fly} from "svelte/transition"
import {canShare, triggerShare} from "$lib/share"
import {popup} from "$lib/popup"
@@ -13,24 +13,6 @@
import ConfigTabs from "./ConfigTabs.svelte"
import EditActions from "./EditActions.svelte"
async function flashChanges() {
$syncStatus = "uploading"
// Yes, this is a completely arbitrary and unnecessary delay.
// The only purpose of it is to create a sense of weight,
// aka make it more "energy intensive" to click.
// The only conceivable way users could reach the commit limit in this case
// 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!"
await new Promise(resolve => setTimeout(resolve, 6000))
$serialPort.commit()
unsavedChanges.update(it => {
it.clear()
return it
})
$syncStatus = "done"
}
$: if (browser && !canAutoConnect()) {
connectButton?.click()
}
@@ -55,17 +37,6 @@
<PwaStatus />
{/await}
{/if}
{#if $unsavedChanges.size > 0}
<button
disabled={$syncStatus === "uploading"}
on:click={flashChanges}
transition:fly={{x: -8}}
title={$LL.deviceManager.APPLY_SETTINGS()}
class="icon"
>save
</button>
<div transition:slide class="separator" />
{/if}
{#if $serialPort}
<button title={$LL.backup.TITLE()} use:popup={BackupPopup} class="icon {$syncStatus}">
{#if $syncStatus === "downloading"}

View File

@@ -1,23 +1,24 @@
<script lang="ts">
import {changes, chords} from "$lib/serial/connection"
import {KEYMAP_CODES} from "$lib/serial/keymap-codes"
import Index from "flexsearch"
import type {Chord} from "$lib/serial/chord"
import LL from "../../../i18n/i18n-svelte"
import {action} from "$lib/title"
import {onDestroy, onMount} from "svelte"
import ActionStringEdit from "$lib/components/ActionStringEdit.svelte"
import {changes, ChangeType, chords} from "$lib/undo-redo"
import type {ChordInfo} from "$lib/undo-redo"
import {derived, writable} from "svelte/store"
const resultSize = 38
let results: HTMLElement
let pageSize: number
const pageSize = writable(0)
let resizeObserver: ResizeObserver
onMount(() => {
resizeObserver = new ResizeObserver(() => {
pageSize = Math.floor(results.clientHeight / resultSize)
pageSize.set(Math.floor(results.clientHeight / resultSize))
})
pageSize = Math.floor(results.clientHeight / resultSize)
pageSize.set(Math.floor(results.clientHeight / resultSize))
resizeObserver.observe(results)
})
@@ -27,27 +28,33 @@
$: searchIndex = $chords?.length > 0 ? buildIndex($chords) : undefined
function buildIndex(chords: Chord[]): Index {
function buildIndex(chords: ChordInfo[]): Index {
const index = new Index({tokenize: "full"})
chords.forEach((chord, i) => {
index.add(i, chord.phrase.map(it => KEYMAP_CODES[it].id).join(""))
if ("phrase" in chord) {
index.add(i, chord.phrase.map(it => KEYMAP_CODES[it].id).join(""))
}
})
return index
}
let searchFilter: number[] | undefined
const searchFilter = writable<number[] | undefined>(undefined)
function search(event: Event) {
const query = (event.target as HTMLInputElement).value
searchFilter = query && searchIndex ? searchIndex.search(query) : undefined
searchFilter.set(query && searchIndex ? searchIndex.search(query) : undefined)
}
$: items = searchFilter?.map(it => [$chords[it], it] as const) ?? $chords.map((it, i) => [it, i] as const)
$: lastPage = Math.ceil(items.length / pageSize) - 1
const items = derived([searchFilter, chords], ([filter, chords]) =>
(filter?.map(it => [chords[it], it] as const) ?? chords.map((it, i) => [it, i] as const)).filter(
([{phrase}]) => phrase.length > 0,
),
)
const lastPage = derived([items, pageSize], ([items, pageSize]) => Math.ceil(items.length / pageSize) - 1)
let page = 0
$: {
items
$items
page = 0
}
</script>
@@ -63,8 +70,8 @@
on:input={search}
/>
<div class="paginator">
{#if lastPage !== -1}
{page + 1} / {lastPage + 1}
{#if $lastPage !== -1}
{page + 1} / {$lastPage + 1}
{:else}
- / -
{/if}
@@ -74,26 +81,31 @@
>
<button
class="icon"
on:click={() => (page = Math.min(page + 1, lastPage))}
on:click={() => (page = Math.min(page + 1, $lastPage))}
use:action={{shortcut: "ctrl+right"}}>navigate_next</button
>
</div>
<section bind:this={results}>
<table>
{#if lastPage !== -1}
{#each items.slice(page * pageSize, (page + 1) * pageSize) as [chord]}
<tr>
{#if $lastPage !== -1}
{#each $items.slice(page * $pageSize, (page + 1) * $pageSize) as [{ actions, phrase, isApplied }]}
<tr style:color={isApplied ? "" : "var(--md-sys-color-secondary"}>
<th>
<ActionStringEdit actions={chord.phrase} />
<ActionStringEdit {actions} />
</th>
<td>
<ActionStringEdit actions={chord.actions} />
<ActionStringEdit actions={phrase} />
</td>
<td class="table-buttons">
<button class="icon compact">share</button>
<button class="icon compact" on:click={() => $changes.push({chords: [{delete: chord}]})}
>delete</button
<button
class="icon compact"
on:click={() =>
changes.update(changes => {
changes.push({type: ChangeType.Chord, actions, phrase: []})
return changes
})}>delete</button
>
</td>
</tr>

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import {share} from "$lib/share"
import {layout} from "$lib/serial/connection"
import {deviceLayout} from "$lib/serial/connection"
import tippy from "tippy.js"
import {onMount, setContext} from "svelte"
import Layout from "$lib/components/layout/Layout.svelte"
@@ -16,7 +16,7 @@
if (url.searchParams.has("import")) {
const file = await charaFileFromUriComponent(url.searchParams.get("import")!)
if (file.type === "layout") {
$layout = file.layout
$deviceLayout = file.layout
}
}
})
@@ -29,7 +29,7 @@
charaVersion: 1,
type: "layout",
device: "one",
layout: $layout,
layout: $deviceLayout,
}),
)
await navigator.clipboard.writeText(url.toString())
@@ -52,7 +52,8 @@
const file = await fileInput.files?.item(0)?.text()
if (!file) return
const importedLayout = isCsvLayout(file) ? csvLayoutToJson(file) : (JSON.parse(file) as CharaLayoutFile)
if (importedLayout.type === "layout" && importedLayout.charaVersion === 1) $layout = importedLayout.layout
if (importedLayout.type === "layout" && importedLayout.charaVersion === 1)
$deviceLayout = importedLayout.layout
}
setContext<VisualLayoutConfig>("visual-layout-config", {

View File

@@ -225,4 +225,8 @@
font-size: 10px;
}
}
form label:has(:global(.pending-changes)) {
color: var(--md-sys-color-tertiary);
}
</style>