mirror of
https://github.com/CharaChorder/DeviceManager.git
synced 2026-01-20 08:52:59 +00:00
feat: editing
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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", {
|
||||
|
||||
@@ -225,4 +225,8 @@
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
form label:has(:global(.pending-changes)) {
|
||||
color: var(--md-sys-color-tertiary);
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user