mirror of
https://github.com/CharaChorder/DeviceManager.git
synced 2026-01-21 17:32:41 +00:00
feat: cv2
This commit is contained in:
@@ -1,8 +1,5 @@
|
|||||||
import { type KeyInfo } from "$lib/serial/keymap-codes";
|
|
||||||
import { syntaxTree } from "@codemirror/language";
|
|
||||||
import { linter, type Diagnostic } from "@codemirror/lint";
|
import { linter, type Diagnostic } from "@codemirror/lint";
|
||||||
import { parsedChordsField } from "./parsed-chords-plugin";
|
import { parsedChordsField } from "./parsed-chords-plugin";
|
||||||
import { actionMetaPlugin } from "./action-meta-plugin";
|
|
||||||
|
|
||||||
export function actionLinter(config?: Parameters<typeof linter>[1]) {
|
export function actionLinter(config?: Parameters<typeof linter>[1]) {
|
||||||
const finalConfig: Parameters<typeof linter>[1] = {
|
const finalConfig: Parameters<typeof linter>[1] = {
|
||||||
@@ -113,186 +110,26 @@ export function actionLinter(config?: Parameters<typeof linter>[1]) {
|
|||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
if (chord.phrase) {
|
||||||
return diagnostics;
|
if (!chord.phrase.originalValue) {
|
||||||
|
|
||||||
syntaxTree(view.state)
|
|
||||||
.cursor()
|
|
||||||
.iterate((node) => {
|
|
||||||
let action: KeyInfo | undefined = undefined;
|
|
||||||
switch (node.name) {
|
|
||||||
case "SingleLetter": {
|
|
||||||
action = ids.get(view.state.doc.sliceString(node.from, node.to));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "ActionId": {
|
|
||||||
action = ids.get(view.state.doc.sliceString(node.from, node.to));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "HexNumber": {
|
|
||||||
const hexString = view.state.doc.sliceString(node.from, node.to);
|
|
||||||
const code = Number.parseInt(hexString, 16);
|
|
||||||
if (hexString.length === 10) {
|
|
||||||
if (compoundInputs.has(code)) {
|
|
||||||
diagnostics.push({
|
diagnostics.push({
|
||||||
from: node.from,
|
from: chord.range[0],
|
||||||
to: node.to,
|
to: chord.range[1],
|
||||||
severity: "info",
|
severity: "info",
|
||||||
message: "Compound hash literal can be expanded",
|
markClass: "chord-new",
|
||||||
actions: [
|
message: `New Chord`,
|
||||||
{
|
|
||||||
name: "Expand",
|
|
||||||
apply(view, from, to) {
|
|
||||||
view.dispatch({
|
|
||||||
changes: {
|
|
||||||
from: from - 1,
|
|
||||||
to: to + 1,
|
|
||||||
insert: compoundInputs.get(code)! + "|",
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
},
|
} else if (chord.phrase.originalValue !== chord.phrase.value) {
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(code >= 0 && code <= 1023)) {
|
|
||||||
diagnostics.push({
|
diagnostics.push({
|
||||||
from: node.from,
|
from: chord.range[0],
|
||||||
to: node.to,
|
to: chord.range[1],
|
||||||
severity: "error",
|
|
||||||
message: "Hex code invalid (out of range)",
|
|
||||||
actions: [
|
|
||||||
{
|
|
||||||
name: "Remove",
|
|
||||||
apply(view, from, to) {
|
|
||||||
view.dispatch({ changes: { from, to } });
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
action = codes.get(code);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!action) {
|
|
||||||
const action = view.state.doc.sliceString(node.from, node.to);
|
|
||||||
diagnostics.push({
|
|
||||||
from: node.from,
|
|
||||||
to: node.to,
|
|
||||||
severity: node.name === "HexNumber" ? "warning" : "error",
|
|
||||||
message: `Unknown action: ${action}`,
|
|
||||||
actions: [
|
|
||||||
...(node.name === "SingleLetter"
|
|
||||||
? ([
|
|
||||||
{
|
|
||||||
name: "Generate Windows Hex Numpad Code",
|
|
||||||
apply(view, from, to) {
|
|
||||||
view.dispatch({
|
|
||||||
changes: {
|
|
||||||
from,
|
|
||||||
to,
|
|
||||||
insert:
|
|
||||||
"<PRESS_NEXT><LEFT_ALT><KP_PLUS>" +
|
|
||||||
action
|
|
||||||
.codePointAt(0)!
|
|
||||||
.toString(16)
|
|
||||||
.split("")
|
|
||||||
.map((c) =>
|
|
||||||
/^\d$/.test(c)
|
|
||||||
? `<KP_${c}>`
|
|
||||||
: c.toLowerCase(),
|
|
||||||
)
|
|
||||||
.join("") +
|
|
||||||
"<RELEASE_NEXT><LEFT_ALT>",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
] satisfies Diagnostic["actions"])
|
|
||||||
: []),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const m of meta) {
|
|
||||||
if (m.invalidActions) {
|
|
||||||
diagnostics.push({
|
|
||||||
from: m.from,
|
|
||||||
to: m.to,
|
|
||||||
severity: "error",
|
|
||||||
markClass: "chord-invalid",
|
|
||||||
message: `Chord contains invalid actions`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (m.invalidInput) {
|
|
||||||
diagnostics.push({
|
|
||||||
from: m.from,
|
|
||||||
to: m.to,
|
|
||||||
severity: "error",
|
|
||||||
markClass: "chord-invalid",
|
|
||||||
message: `Chord input is invalid`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (m.emptyPhrase) {
|
|
||||||
diagnostics.push({
|
|
||||||
from: m.from,
|
|
||||||
to: m.from,
|
|
||||||
severity: "warning",
|
|
||||||
message: `Chord phrase is empty`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (m.overriddenBy) {
|
|
||||||
diagnostics.push({
|
|
||||||
from: m.from,
|
|
||||||
to: m.from,
|
|
||||||
severity: "warning",
|
|
||||||
message: `Chord overridden by previous chord`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (m.orphan) {
|
|
||||||
diagnostics.push({
|
|
||||||
from: m.from,
|
|
||||||
to: m.from,
|
|
||||||
severity: "warning",
|
|
||||||
message: `Orphan compound chord`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (m.disabled) {
|
|
||||||
diagnostics.push({
|
|
||||||
from: m.from,
|
|
||||||
to: m.to,
|
|
||||||
severity: "info",
|
severity: "info",
|
||||||
markClass: "chord-ignored",
|
markClass: "chord-unchanged",
|
||||||
message: `Chord disabled`,
|
message: `Phrase changed`,
|
||||||
});
|
|
||||||
}
|
|
||||||
if ((m.overrides?.length ?? 0) > 0) {
|
|
||||||
diagnostics.push({
|
|
||||||
from: m.from,
|
|
||||||
to: m.from,
|
|
||||||
severity: "info",
|
|
||||||
message: `Chord overrides other chords`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (m.originalPhrase) {
|
|
||||||
diagnostics.push({
|
|
||||||
from: m.from,
|
|
||||||
to: m.to,
|
|
||||||
severity: "info",
|
|
||||||
message: `Chord phrase changed from "${m.originalPhrase}"`,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return diagnostics;
|
return diagnostics;
|
||||||
}, finalConfig);
|
}, finalConfig);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import type {
|
|||||||
} from "./parse-meta";
|
} from "./parse-meta";
|
||||||
|
|
||||||
export function canUseIdAsString(info: KeyInfo): boolean {
|
export function canUseIdAsString(info: KeyInfo): boolean {
|
||||||
return !!info.id && /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(info.id);
|
return !!info.id && /^[^>\n]+$/.test(info.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function actionToValue(action: number | KeyInfo) {
|
export function actionToValue(action: number | KeyInfo) {
|
||||||
@@ -224,6 +224,40 @@ function resolveCompoundParents(chords: ChordMeta[]) {
|
|||||||
console.timeEnd("resolveCompoundParents");
|
console.timeEnd("resolveCompoundParents");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function resolveChanges(
|
||||||
|
chords: ChordMeta[],
|
||||||
|
deviceChords: CharaChordFile["chords"],
|
||||||
|
): CharaChordFile["chords"] {
|
||||||
|
console.time("resolveChanges");
|
||||||
|
const removed: CharaChordFile["chords"] = [];
|
||||||
|
const info = new Map<string, ChordMeta>();
|
||||||
|
for (const chord of chords) {
|
||||||
|
if (chord.input && chord.phrase && !chord.disabled) {
|
||||||
|
info.set(
|
||||||
|
JSON.stringify([chord.input.value, chord.phrase?.value ?? []]),
|
||||||
|
chord,
|
||||||
|
);
|
||||||
|
info.set(JSON.stringify(chord.input.value), chord);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const deviceChord of deviceChords) {
|
||||||
|
const exact = info.get(JSON.stringify(deviceChord));
|
||||||
|
if (exact) {
|
||||||
|
exact.phrase!.originalValue = exact.phrase!.value;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const byInput = info.get(JSON.stringify(deviceChord[0]));
|
||||||
|
if (byInput) {
|
||||||
|
byInput.phrase!.originalValue = deviceChord[1];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
removed.push(deviceChord);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.timeEnd("resolveChanges");
|
||||||
|
return removed;
|
||||||
|
}
|
||||||
|
|
||||||
export function parseCharaChords(
|
export function parseCharaChords(
|
||||||
data: EditorState,
|
data: EditorState,
|
||||||
ids: Map<string, KeyInfo>,
|
ids: Map<string, KeyInfo>,
|
||||||
@@ -236,6 +270,7 @@ export function parseCharaChords(
|
|||||||
resolveChordOverrides(chords);
|
resolveChordOverrides(chords);
|
||||||
resolveChordAliases(chords);
|
resolveChordAliases(chords);
|
||||||
resolveCompoundParents(chords);
|
resolveCompoundParents(chords);
|
||||||
|
const removed = resolveChanges(chords, deviceChords);
|
||||||
|
|
||||||
/*for (let i = 0; i < metas.length; i++) {
|
/*for (let i = 0; i < metas.length; i++) {
|
||||||
const [, compound] = splitCompound(chords[i]![0]);
|
const [, compound] = splitCompound(chords[i]![0]);
|
||||||
@@ -269,5 +304,5 @@ export function parseCharaChords(
|
|||||||
console.timeEnd("parseTotal");
|
console.timeEnd("parseTotal");
|
||||||
|
|
||||||
console.log(chords);
|
console.log(chords);
|
||||||
return { chords, removed: [] };
|
return { chords, removed };
|
||||||
}
|
}
|
||||||
|
|||||||
39
src/lib/chord-editor/changes-panel.ts
Normal file
39
src/lib/chord-editor/changes-panel.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import type { EditorState } from "@codemirror/state";
|
||||||
|
import { EditorView, showPanel, type Panel } from "@codemirror/view";
|
||||||
|
import { parsedChordsField } from "./parsed-chords-plugin";
|
||||||
|
|
||||||
|
function getChanges(state: EditorState): string {
|
||||||
|
const parsed = state.field(parsedChordsField);
|
||||||
|
const added = parsed.chords.reduce(
|
||||||
|
(acc, chord) =>
|
||||||
|
acc + (chord.phrase && chord.phrase.originalValue === undefined ? 1 : 0),
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
const changed = parsed.chords.reduce(
|
||||||
|
(acc, chord) =>
|
||||||
|
acc +
|
||||||
|
(chord.phrase &&
|
||||||
|
chord.phrase.originalValue &&
|
||||||
|
chord.phrase.originalValue !== chord.phrase.value
|
||||||
|
? 1
|
||||||
|
: 0),
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
const removed = parsed.removed.length;
|
||||||
|
return `+${added} ~${changed} -${removed} (${parsed.chords.length} total)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function wordCountPanel(view: EditorView): Panel {
|
||||||
|
let dom = document.createElement("div");
|
||||||
|
dom.textContent = getChanges(view.state);
|
||||||
|
return {
|
||||||
|
dom,
|
||||||
|
update(update) {
|
||||||
|
dom.textContent = getChanges(update.state);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function changesPanel() {
|
||||||
|
return showPanel.of(wordCountPanel);
|
||||||
|
}
|
||||||
@@ -77,6 +77,7 @@ function mapActionStringMeta<T extends ActionStringMeta<unknown>>(
|
|||||||
|
|
||||||
export interface PhraseMeta extends ActionStringMeta<number[]> {
|
export interface PhraseMeta extends ActionStringMeta<number[]> {
|
||||||
hasConcatenator: boolean;
|
hasConcatenator: boolean;
|
||||||
|
originalValue?: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CompoundMeta extends ActionStringMeta<number> {
|
export interface CompoundMeta extends ActionStringMeta<number> {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
historyKeymap,
|
historyKeymap,
|
||||||
standardKeymap,
|
standardKeymap,
|
||||||
} from "@codemirror/commands";
|
} from "@codemirror/commands";
|
||||||
import { debounceTime, Subject } from "rxjs";
|
import { debounceTime, mergeMap, Subject } from "rxjs";
|
||||||
import { EditorState, type EditorStateConfig } from "@codemirror/state";
|
import { EditorState, type EditorStateConfig } from "@codemirror/state";
|
||||||
import { lintGutter } from "@codemirror/lint";
|
import { lintGutter } from "@codemirror/lint";
|
||||||
import {
|
import {
|
||||||
@@ -27,6 +27,11 @@ import { syntaxHighlighting } from "@codemirror/language";
|
|||||||
import { deviceChordField } from "./chord-sync-plugin";
|
import { deviceChordField } from "./chord-sync-plugin";
|
||||||
import { actionMetaPlugin } from "./action-meta-plugin";
|
import { actionMetaPlugin } from "./action-meta-plugin";
|
||||||
import { parsedChordsField } from "./parsed-chords-plugin";
|
import { parsedChordsField } from "./parsed-chords-plugin";
|
||||||
|
import { changesPanel } from "./changes-panel";
|
||||||
|
import {
|
||||||
|
parseCompressed,
|
||||||
|
stringifyCompressed,
|
||||||
|
} from "$lib/serial/serialization";
|
||||||
|
|
||||||
const serializedFields = {
|
const serializedFields = {
|
||||||
history: historyField,
|
history: historyField,
|
||||||
@@ -39,13 +44,16 @@ export interface EditorConfig {
|
|||||||
autocomplete(query: string | undefined): void;
|
autocomplete(query: string | undefined): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function loadPersistentState(params: EditorConfig): EditorState {
|
export async function loadPersistentState(
|
||||||
|
params: EditorConfig,
|
||||||
|
): Promise<EditorState> {
|
||||||
const stored = localStorage.getItem(params.storeName);
|
const stored = localStorage.getItem(params.storeName);
|
||||||
const config = {
|
const config = {
|
||||||
extensions: [
|
extensions: [
|
||||||
actionMetaPlugin.plugin,
|
actionMetaPlugin.plugin,
|
||||||
deviceChordField,
|
deviceChordField,
|
||||||
parsedChordsField,
|
parsedChordsField,
|
||||||
|
changesPanel(),
|
||||||
lintGutter(),
|
lintGutter(),
|
||||||
params.rawCode ? [lineNumbers()] : [delimPlugin, actionPlugin],
|
params.rawCode ? [lineNumbers()] : [delimPlugin, actionPlugin],
|
||||||
chordLanguageSupport(),
|
chordLanguageSupport(),
|
||||||
@@ -84,7 +92,7 @@ export function loadPersistentState(params: EditorConfig): EditorState {
|
|||||||
|
|
||||||
if (stored) {
|
if (stored) {
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(stored);
|
const parsed = await parseCompressed(new Blob([stored]));
|
||||||
return EditorState.fromJSON(parsed, config, serializedFields);
|
return EditorState.fromJSON(parsed, config, serializedFields);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to parse persistent state:", e);
|
console.error("Failed to parse persistent state:", e);
|
||||||
@@ -98,12 +106,15 @@ export function persistentStatePlugin(storeName: string) {
|
|||||||
class {
|
class {
|
||||||
updateSubject = new Subject<void>();
|
updateSubject = new Subject<void>();
|
||||||
subscription = this.updateSubject
|
subscription = this.updateSubject
|
||||||
.pipe(debounceTime(500))
|
.pipe(
|
||||||
.subscribe(() => {
|
debounceTime(500),
|
||||||
localStorage.setItem(
|
mergeMap(() =>
|
||||||
storeName,
|
stringifyCompressed(this.view.state.toJSON(serializedFields)),
|
||||||
JSON.stringify(this.view.state.toJSON(serializedFields)),
|
),
|
||||||
);
|
mergeMap((blob) => blob.text()),
|
||||||
|
)
|
||||||
|
.subscribe((value) => {
|
||||||
|
localStorage.setItem(storeName, value);
|
||||||
});
|
});
|
||||||
|
|
||||||
constructor(readonly view: EditorView) {}
|
constructor(readonly view: EditorView) {}
|
||||||
|
|||||||
@@ -5,11 +5,16 @@
|
|||||||
import "$lib/chord-editor/chords.grammar";
|
import "$lib/chord-editor/chords.grammar";
|
||||||
import { persistentWritable } from "$lib/storage";
|
import { persistentWritable } from "$lib/storage";
|
||||||
import ActionList from "$lib/components/layout/ActionList.svelte";
|
import ActionList from "$lib/components/layout/ActionList.svelte";
|
||||||
import { splitCompound } from "$lib/serial/chord";
|
import {
|
||||||
|
composeChordInput,
|
||||||
|
hashChord,
|
||||||
|
splitCompound,
|
||||||
|
} from "$lib/serial/chord";
|
||||||
import { loadPersistentState } from "$lib/chord-editor/persistent-state-plugin";
|
import { loadPersistentState } from "$lib/chord-editor/persistent-state-plugin";
|
||||||
import { parsedChordsField } from "$lib/chord-editor/parsed-chords-plugin";
|
import { parsedChordsField } from "$lib/chord-editor/parsed-chords-plugin";
|
||||||
import type { CharaChordFile } from "$lib/share/chara-file";
|
import type { CharaChordFile } from "$lib/share/chara-file";
|
||||||
import { chordSyncEffect } from "$lib/chord-editor/chord-sync-plugin";
|
import { chordSyncEffect } from "$lib/chord-editor/chord-sync-plugin";
|
||||||
|
import { KEYMAP_IDS, type KeyInfo } from "$lib/serial/keymap-codes";
|
||||||
|
|
||||||
let queryFilter: string | undefined = $state(undefined);
|
let queryFilter: string | undefined = $state(undefined);
|
||||||
|
|
||||||
@@ -22,17 +27,21 @@
|
|||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (!editor) return;
|
if (!editor) return;
|
||||||
view = new EditorView({
|
const viewPromise = loadPersistentState({
|
||||||
parent: editor,
|
|
||||||
state: loadPersistentState({
|
|
||||||
rawCode: $rawCode,
|
rawCode: $rawCode,
|
||||||
storeName: "chord-editor-state-storage",
|
storeName: "chord-editor-state-storage",
|
||||||
autocomplete(query) {
|
autocomplete(query) {
|
||||||
queryFilter = query;
|
queryFilter = query;
|
||||||
},
|
},
|
||||||
|
}).then(
|
||||||
|
(state) =>
|
||||||
|
new EditorView({
|
||||||
|
parent: editor,
|
||||||
|
state,
|
||||||
}),
|
}),
|
||||||
});
|
);
|
||||||
return () => view.destroy();
|
viewPromise.then((it) => (view = it));
|
||||||
|
return () => viewPromise.then((it) => it.destroy());
|
||||||
});
|
});
|
||||||
|
|
||||||
function regenerate() {
|
function regenerate() {
|
||||||
@@ -55,6 +64,63 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function largeFile() {
|
||||||
|
const chordCount = 100000;
|
||||||
|
const maxPhraseLength = 100;
|
||||||
|
const maxInputLength = 8;
|
||||||
|
const compoundChance = 0.05;
|
||||||
|
|
||||||
|
const actions = [...$KEYMAP_IDS.values()];
|
||||||
|
function randomAction(): KeyInfo {
|
||||||
|
return actions[Math.floor(actions.length * Math.random())]!;
|
||||||
|
}
|
||||||
|
|
||||||
|
const backup: [KeyInfo[][], KeyInfo[]][] = Array.from(
|
||||||
|
{ length: chordCount },
|
||||||
|
() =>
|
||||||
|
[
|
||||||
|
[
|
||||||
|
Array.from(
|
||||||
|
{ length: Math.floor(Math.random() * maxInputLength) + 1 },
|
||||||
|
randomAction,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
Array.from(
|
||||||
|
{
|
||||||
|
length: Math.floor(Math.log(Math.random() * maxPhraseLength)) + 1,
|
||||||
|
},
|
||||||
|
randomAction,
|
||||||
|
),
|
||||||
|
] as const,
|
||||||
|
);
|
||||||
|
for (const chord of backup) {
|
||||||
|
if (Math.random() < compoundChance) {
|
||||||
|
chord[0] = [
|
||||||
|
...backup[Math.floor(backup.length * Math.random())]![0],
|
||||||
|
...chord[0],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const doc = backup
|
||||||
|
.map(([inputs, phrase]) => {
|
||||||
|
return (
|
||||||
|
inputs
|
||||||
|
.map((input) => input.map((it) => actionToValue(it)).join(""))
|
||||||
|
.join("|") +
|
||||||
|
"=>" +
|
||||||
|
phrase.map((it) => actionToValue(it)).join("")
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.join("\n");
|
||||||
|
view.dispatch({
|
||||||
|
changes: { from: 0, to: view.state.doc.length, insert: doc },
|
||||||
|
effects: chordSyncEffect.of(
|
||||||
|
$chords.map((chord) => [chord.actions, chord.phrase] as const),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function loadBackup(event: Event) {
|
function loadBackup(event: Event) {
|
||||||
const input = event.target as HTMLInputElement;
|
const input = event.target as HTMLInputElement;
|
||||||
if (!input.files || input.files.length === 0) return;
|
if (!input.files || input.files.length === 0) return;
|
||||||
@@ -106,6 +172,7 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<div class="vertical">
|
||||||
<div style:display="flex">
|
<div style:display="flex">
|
||||||
<label><input type="checkbox" bind:checked={$rawCode} />Edit as code</label>
|
<label><input type="checkbox" bind:checked={$rawCode} />Edit as code</label>
|
||||||
<!--<label><input type="checkbox" bind:checked={$showEdits} />Show edits</label>-->
|
<!--<label><input type="checkbox" bind:checked={$showEdits} />Show edits</label>-->
|
||||||
@@ -113,6 +180,7 @@
|
|||||||
><input type="checkbox" bind:checked={$denseSpacing} />Dense Spacing</label
|
><input type="checkbox" bind:checked={$denseSpacing} />Dense Spacing</label
|
||||||
>
|
>
|
||||||
<button onclick={regenerate}>Regenerate from current chords</button>
|
<button onclick={regenerate}>Regenerate from current chords</button>
|
||||||
|
<!--<button onclick={largeFile}>Create Huge File</button>-->
|
||||||
<button onclick={downloadBackup}>Download Backup</button>
|
<button onclick={downloadBackup}>Download Backup</button>
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
@@ -132,13 +200,22 @@
|
|||||||
></div>
|
></div>
|
||||||
<ActionList {queryFilter} ignoreIcon={$rawCode} />
|
<ActionList {queryFilter} ignoreIcon={$rawCode} />
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
.vertical {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.split {
|
.split {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 1rem;
|
flex-grow: 1;
|
||||||
|
flex-shrink: 1;
|
||||||
width: calc(min(100%, 1400px));
|
width: calc(min(100%, 1400px));
|
||||||
height: 100%;
|
min-height: 0;
|
||||||
|
|
||||||
> :global(*) {
|
> :global(*) {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|||||||
Reference in New Issue
Block a user