mirror of
https://github.com/CharaChorder/DeviceManager.git
synced 2026-02-24 10:02:04 +00:00
feat: profile support
This commit is contained in:
@@ -53,6 +53,7 @@
|
|||||||
"@tauri-apps/api": "^1.6.0",
|
"@tauri-apps/api": "^1.6.0",
|
||||||
"@tauri-apps/cli": "^1.6.0",
|
"@tauri-apps/cli": "^1.6.0",
|
||||||
"@types/dom-view-transitions": "^1.0.6",
|
"@types/dom-view-transitions": "^1.0.6",
|
||||||
|
"@types/semver": "^7.7.0",
|
||||||
"@types/w3c-web-serial": "^1.0.8",
|
"@types/w3c-web-serial": "^1.0.8",
|
||||||
"@types/w3c-web-usb": "^1.0.10",
|
"@types/w3c-web-usb": "^1.0.10",
|
||||||
"@types/wicg-file-system-access": "^2023.10.5",
|
"@types/wicg-file-system-access": "^2023.10.5",
|
||||||
|
|||||||
8
pnpm-lock.yaml
generated
8
pnpm-lock.yaml
generated
@@ -65,6 +65,9 @@ importers:
|
|||||||
'@types/dom-view-transitions':
|
'@types/dom-view-transitions':
|
||||||
specifier: ^1.0.6
|
specifier: ^1.0.6
|
||||||
version: 1.0.6
|
version: 1.0.6
|
||||||
|
'@types/semver':
|
||||||
|
specifier: ^7.7.0
|
||||||
|
version: 7.7.0
|
||||||
'@types/w3c-web-serial':
|
'@types/w3c-web-serial':
|
||||||
specifier: ^1.0.8
|
specifier: ^1.0.8
|
||||||
version: 1.0.8
|
version: 1.0.8
|
||||||
@@ -1488,6 +1491,9 @@ packages:
|
|||||||
'@types/retry@0.12.0':
|
'@types/retry@0.12.0':
|
||||||
resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==}
|
resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==}
|
||||||
|
|
||||||
|
'@types/semver@7.7.0':
|
||||||
|
resolution: {integrity: sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==}
|
||||||
|
|
||||||
'@types/sinonjs__fake-timers@8.1.1':
|
'@types/sinonjs__fake-timers@8.1.1':
|
||||||
resolution: {integrity: sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g==}
|
resolution: {integrity: sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g==}
|
||||||
|
|
||||||
@@ -5669,6 +5675,8 @@ snapshots:
|
|||||||
|
|
||||||
'@types/retry@0.12.0': {}
|
'@types/retry@0.12.0': {}
|
||||||
|
|
||||||
|
'@types/semver@7.7.0': {}
|
||||||
|
|
||||||
'@types/sinonjs__fake-timers@8.1.1': {}
|
'@types/sinonjs__fake-timers@8.1.1': {}
|
||||||
|
|
||||||
'@types/sizzle@2.3.8': {}
|
'@types/sizzle@2.3.8': {}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ const de = {
|
|||||||
AUTO_BACKUP: "Auto-backup",
|
AUTO_BACKUP: "Auto-backup",
|
||||||
DISCLAIMER:
|
DISCLAIMER:
|
||||||
"Das Backup in diesem Browser gespeichert und bleibt nur auf diesem Computer.",
|
"Das Backup in diesem Browser gespeichert und bleibt nur auf diesem Computer.",
|
||||||
DOWNLOAD: "Alles",
|
DOWNLOAD: "Komplettes Profil",
|
||||||
RESTORE: "Wiederherstellen",
|
RESTORE: "Wiederherstellen",
|
||||||
},
|
},
|
||||||
modal: {
|
modal: {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ const en = {
|
|||||||
AUTO_BACKUP: "Auto-backup",
|
AUTO_BACKUP: "Auto-backup",
|
||||||
DISCLAIMER:
|
DISCLAIMER:
|
||||||
"Whenever you connect this device to browser, a backup is made locally and kept only on your computer.",
|
"Whenever you connect this device to browser, a backup is made locally and kept only on your computer.",
|
||||||
DOWNLOAD: "Everything",
|
DOWNLOAD: "Full profile",
|
||||||
RESTORE: "Restore",
|
RESTORE: "Restore",
|
||||||
},
|
},
|
||||||
sync: {
|
sync: {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
settings,
|
settings,
|
||||||
} from "$lib/undo-redo.js";
|
} from "$lib/undo-redo.js";
|
||||||
import { get } from "svelte/store";
|
import { get } from "svelte/store";
|
||||||
import { serialPort } from "../serial/connection";
|
import { activeProfile, serialPort } from "../serial/connection";
|
||||||
import { csvLayoutToJson, isCsvLayout } from "$lib/backup/compat/legacy-layout";
|
import { csvLayoutToJson, isCsvLayout } from "$lib/backup/compat/legacy-layout";
|
||||||
import { isCsvChords, csvChordsToJson } from "./compat/legacy-chords";
|
import { isCsvChords, csvChordsToJson } from "./compat/legacy-chords";
|
||||||
|
|
||||||
@@ -50,11 +50,9 @@ export function createLayoutBackup(): CharaLayoutFile {
|
|||||||
charaVersion: 1,
|
charaVersion: 1,
|
||||||
type: "layout",
|
type: "layout",
|
||||||
device: get(serialPort)?.device,
|
device: get(serialPort)?.device,
|
||||||
layout: get(layout).map((it) => it.map((it) => it.action)) as [
|
layout: (get(layout)[get(activeProfile)]?.map((it) =>
|
||||||
number[],
|
it.map((it) => it.action),
|
||||||
number[],
|
) ?? []) as [number[], number[], number[]],
|
||||||
number[],
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,7 +68,7 @@ export function createSettingsBackup(): CharaSettingsFile {
|
|||||||
return {
|
return {
|
||||||
charaVersion: 1,
|
charaVersion: 1,
|
||||||
type: "settings",
|
type: "settings",
|
||||||
settings: get(settings).map((it) => it.value),
|
settings: get(settings)[get(activeProfile)]?.map((it) => it.value) ?? [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,9 +95,11 @@ export function restoreFromFile(
|
|||||||
const recent = file.history[0];
|
const recent = file.history[0];
|
||||||
if (!recent) return;
|
if (!recent) return;
|
||||||
let backupDevice = recent[1].device;
|
let backupDevice = recent[1].device;
|
||||||
if (backupDevice === "TWO") backupDevice = "ONE";
|
if (backupDevice === "TWO" || backupDevice === "M4G")
|
||||||
|
backupDevice = "ONE";
|
||||||
let currentDevice = get(serialPort)?.device;
|
let currentDevice = get(serialPort)?.device;
|
||||||
if (currentDevice === "TWO") currentDevice = "ONE";
|
if (currentDevice === "TWO" || backupDevice === "M4G")
|
||||||
|
currentDevice = "ONE";
|
||||||
|
|
||||||
if (backupDevice !== currentDevice) {
|
if (backupDevice !== currentDevice) {
|
||||||
alert("Backup is incompatible with this device");
|
alert("Backup is incompatible with this device");
|
||||||
@@ -167,12 +167,13 @@ export function getChangesFromChordFile(file: CharaChordFile) {
|
|||||||
export function getChangesFromSettingsFile(file: CharaSettingsFile) {
|
export function getChangesFromSettingsFile(file: CharaSettingsFile) {
|
||||||
const changes: Change[] = [];
|
const changes: Change[] = [];
|
||||||
for (const [id, value] of file.settings.entries()) {
|
for (const [id, value] of file.settings.entries()) {
|
||||||
const setting = get(settings)[id];
|
const setting = get(settings)[get(activeProfile)]?.[id];
|
||||||
if (setting !== undefined && setting.value !== value) {
|
if (setting !== undefined && setting.value !== value) {
|
||||||
changes.push({
|
changes.push({
|
||||||
type: ChangeType.Setting,
|
type: ChangeType.Setting,
|
||||||
id,
|
id,
|
||||||
setting: value,
|
setting: value,
|
||||||
|
profile: get(activeProfile),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -183,12 +184,13 @@ export function getChangesFromLayoutFile(file: CharaLayoutFile) {
|
|||||||
const changes: Change[] = [];
|
const changes: Change[] = [];
|
||||||
for (const [layer, keys] of file.layout.entries()) {
|
for (const [layer, keys] of file.layout.entries()) {
|
||||||
for (const [id, action] of keys.entries()) {
|
for (const [id, action] of keys.entries()) {
|
||||||
if (get(layout)[layer]?.[id]?.action !== action) {
|
if (get(layout)[get(activeProfile)]?.[layer]?.[id]?.action !== action) {
|
||||||
changes.push({
|
changes.push({
|
||||||
type: ChangeType.Layout,
|
type: ChangeType.Layout,
|
||||||
layer,
|
layer,
|
||||||
id,
|
id,
|
||||||
action,
|
action,
|
||||||
|
profile: get(activeProfile),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,17 +8,16 @@
|
|||||||
import { dev } from "$app/environment";
|
import { dev } from "$app/environment";
|
||||||
import ActionSelector from "$lib/components/layout/ActionSelector.svelte";
|
import ActionSelector from "$lib/components/layout/ActionSelector.svelte";
|
||||||
import { get } from "svelte/store";
|
import { get } from "svelte/store";
|
||||||
import type { Writable } from "svelte/store";
|
|
||||||
import KeyboardKey from "$lib/components/layout/KeyboardKey.svelte";
|
import KeyboardKey from "$lib/components/layout/KeyboardKey.svelte";
|
||||||
import { getContext, mount, unmount } from "svelte";
|
import { getContext, mount, unmount } from "svelte";
|
||||||
import type { VisualLayoutConfig } from "./visual-layout.js";
|
import type { VisualLayoutConfig } from "./visual-layout.js";
|
||||||
import { changes, ChangeType, layout } from "$lib/undo-redo";
|
import { changes, ChangeType, layout } from "$lib/undo-redo";
|
||||||
import { fly } from "svelte/transition";
|
import { fly } from "svelte/transition";
|
||||||
import { expoOut } from "svelte/easing";
|
import { expoOut } from "svelte/easing";
|
||||||
|
import { activeLayer, activeProfile } from "$lib/serial/connection";
|
||||||
|
|
||||||
const { scale, margin, strokeWidth, fontSize, iconFontSize } =
|
const { scale, margin, strokeWidth, fontSize, iconFontSize } =
|
||||||
getContext<VisualLayoutConfig>("visual-layout-config");
|
getContext<VisualLayoutConfig>("visual-layout-config");
|
||||||
const activeLayer = getContext<Writable<number>>("active-layer");
|
|
||||||
|
|
||||||
if (dev) {
|
if (dev) {
|
||||||
// you have absolutely no idea what a difference this makes for performance
|
// you have absolutely no idea what a difference this makes for performance
|
||||||
@@ -125,8 +124,10 @@
|
|||||||
const keyInfo = layoutInfo.keys[index];
|
const keyInfo = layoutInfo.keys[index];
|
||||||
if (!keyInfo) return;
|
if (!keyInfo) return;
|
||||||
const clickedGroup = groupParent.children.item(index) as SVGGElement;
|
const clickedGroup = groupParent.children.item(index) as SVGGElement;
|
||||||
const nextAction = get(layout)[get(activeLayer)]?.[keyInfo.id];
|
const nextAction =
|
||||||
const currentAction = get(deviceLayout)[get(activeLayer)]?.[keyInfo.id];
|
get(layout)[get(activeProfile)][get(activeLayer)]?.[keyInfo.id];
|
||||||
|
const currentAction =
|
||||||
|
get(deviceLayout)[get(activeProfile)][get(activeLayer)]?.[keyInfo.id];
|
||||||
const component = mount(ActionSelector, {
|
const component = mount(ActionSelector, {
|
||||||
target: document.body,
|
target: document.body,
|
||||||
props: {
|
props: {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { getContext } from "svelte";
|
|
||||||
import type { Writable } from "svelte/store";
|
import type { Writable } from "svelte/store";
|
||||||
import type { VisualLayoutConfig } from "$lib/components/layout/visual-layout";
|
import type { VisualLayoutConfig } from "$lib/components/layout/visual-layout";
|
||||||
import type { CompiledLayoutKey } from "$lib/serialization/visual-layout";
|
import type { CompiledLayoutKey } from "$lib/serialization/visual-layout";
|
||||||
@@ -7,10 +6,11 @@
|
|||||||
import { osLayout } from "$lib/os-layout.js";
|
import { osLayout } from "$lib/os-layout.js";
|
||||||
import { KEYMAP_CODES } from "$lib/serial/keymap-codes";
|
import { KEYMAP_CODES } from "$lib/serial/keymap-codes";
|
||||||
import { action } from "$lib/title";
|
import { action } from "$lib/title";
|
||||||
|
import { activeProfile, activeLayer } from "$lib/serial/connection";
|
||||||
|
import { getContext } from "svelte";
|
||||||
|
|
||||||
const { fontSize, margin, inactiveOpacity, inactiveScale, iconFontSize } =
|
const { fontSize, margin, inactiveOpacity, inactiveScale, iconFontSize } =
|
||||||
getContext<VisualLayoutConfig>("visual-layout-config");
|
getContext<VisualLayoutConfig>("visual-layout-config");
|
||||||
const activeLayer = getContext<Writable<number>>("active-layer");
|
|
||||||
const currentAction = getContext<Writable<Set<number>> | undefined>(
|
const currentAction = getContext<Writable<Set<number>> | undefined>(
|
||||||
"highlight-action",
|
"highlight-action",
|
||||||
);
|
);
|
||||||
@@ -33,7 +33,9 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#each positions as position, layer}
|
{#each positions as position, layer}
|
||||||
{@const { action: actionId, isApplied } = $layout[layer]?.[key.id] ?? {
|
{@const { action: actionId, isApplied } = $layout[$activeProfile]?.[layer]?.[
|
||||||
|
key.id
|
||||||
|
] ?? {
|
||||||
action: 0,
|
action: 0,
|
||||||
isApplied: true,
|
isApplied: true,
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -2,21 +2,11 @@
|
|||||||
import { deviceMeta, serialPort } from "$lib/serial/connection";
|
import { deviceMeta, serialPort } from "$lib/serial/connection";
|
||||||
import { action } from "$lib/title";
|
import { action } from "$lib/title";
|
||||||
import GenericLayout from "$lib/components/layout/GenericLayout.svelte";
|
import GenericLayout from "$lib/components/layout/GenericLayout.svelte";
|
||||||
import { getContext } from "svelte";
|
import { activeProfile, activeLayer } from "$lib/serial/connection";
|
||||||
import type { Writable } from "svelte/store";
|
|
||||||
import type { VisualLayout } from "$lib/serialization/visual-layout";
|
import type { VisualLayout } from "$lib/serialization/visual-layout";
|
||||||
import { fade, fly } from "svelte/transition";
|
import { fade, fly } from "svelte/transition";
|
||||||
import { restoreFromFile } from "$lib/backup/backup";
|
import { restoreFromFile } from "$lib/backup/backup";
|
||||||
|
|
||||||
let device = $derived($serialPort?.device);
|
|
||||||
const activeLayer = getContext<Writable<number>>("active-layer");
|
|
||||||
|
|
||||||
const layers = [
|
|
||||||
["Numeric Layer", "123", 1],
|
|
||||||
["Primary Layer", "abc", 0],
|
|
||||||
["Function Layer", "function", 2],
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
const layouts = {
|
const layouts = {
|
||||||
ONE: () =>
|
ONE: () =>
|
||||||
import("$lib/assets/layouts/one.yml").then(
|
import("$lib/assets/layouts/one.yml").then(
|
||||||
@@ -46,19 +36,25 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
{#if device}
|
{#if $serialPort}
|
||||||
{#await layouts[device]() then visualLayout}
|
{#await layouts[$serialPort.device]() then visualLayout}
|
||||||
<fieldset transition:fade>
|
<fieldset transition:fade>
|
||||||
{#each layers as [title, icon, value]}
|
<div class="layers">
|
||||||
<button
|
{#each Array.from({ length: $serialPort.layerCount }, (_, i) => i) as layer}
|
||||||
class="icon"
|
<label>
|
||||||
use:action={{ title, shortcut: `alt+${value + 1}` }}
|
<input
|
||||||
onclick={() => ($activeLayer = value)}
|
type="radio"
|
||||||
class:active={$activeLayer === value}
|
onclick={() => ($activeLayer = layer)}
|
||||||
>
|
name="layer"
|
||||||
{icon}
|
value={layer}
|
||||||
</button>
|
checked={$activeLayer === layer}
|
||||||
|
/>
|
||||||
|
{String.fromCodePoint(
|
||||||
|
"A".codePointAt(0)! + $activeProfile,
|
||||||
|
)}{layer + 1}
|
||||||
|
</label>
|
||||||
{/each}
|
{/each}
|
||||||
|
</div>
|
||||||
{#if $deviceMeta?.factoryDefaults?.layout}
|
{#if $deviceMeta?.factoryDefaults?.layout}
|
||||||
<button
|
<button
|
||||||
use:action={{ title: "Reset Layout" }}
|
use:action={{ title: "Reset Layout" }}
|
||||||
@@ -100,60 +96,13 @@
|
|||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
button.icon {
|
.layers {
|
||||||
cursor: pointer;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
z-index: 1;
|
margin-inline: auto;
|
||||||
|
|
||||||
font-size: 24px;
|
gap: 2px;
|
||||||
color: var(--md-sys-color-on-surface-variant);
|
|
||||||
|
|
||||||
background: var(--md-sys-color-surface-variant);
|
|
||||||
border: none;
|
|
||||||
|
|
||||||
transition: all 250ms ease;
|
|
||||||
|
|
||||||
&:nth-child(2) {
|
|
||||||
z-index: 2;
|
|
||||||
|
|
||||||
aspect-ratio: 1;
|
|
||||||
|
|
||||||
font-size: 32px;
|
|
||||||
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:first-child,
|
|
||||||
&:nth-child(3) {
|
|
||||||
aspect-ratio: unset;
|
|
||||||
height: unset;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
margin-inline-end: -8px;
|
|
||||||
padding-inline: 4px 24px;
|
|
||||||
border-radius: 16px 0 0 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:nth-child(3) {
|
|
||||||
margin-inline-start: -8px;
|
|
||||||
padding-inline: 24px 4px;
|
|
||||||
border-radius: 0 16px 16px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.reset-layout {
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
right: 0;
|
|
||||||
transform: translate(100%, -50%);
|
|
||||||
background: none;
|
|
||||||
font-size: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
font-weight: 900;
|
|
||||||
color: var(--md-sys-color-on-tertiary);
|
|
||||||
background: var(--md-sys-color-tertiary);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -29,21 +29,24 @@ export const deviceChords = persistentWritable<Chord[]>(
|
|||||||
/**
|
/**
|
||||||
* Layout as read from the device
|
* Layout as read from the device
|
||||||
*/
|
*/
|
||||||
export const deviceLayout = persistentWritable<CharaLayout>(
|
export const deviceLayout = persistentWritable<CharaLayout[]>(
|
||||||
"layout",
|
"layout-profiles",
|
||||||
[[], [], []],
|
[],
|
||||||
() => get(userPreferences).backup,
|
() => get(userPreferences).backup,
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Settings as read from the device
|
* Settings as read from the device
|
||||||
*/
|
*/
|
||||||
export const deviceSettings = persistentWritable<number[]>(
|
export const deviceSettings = persistentWritable<number[][]>(
|
||||||
"device-settings",
|
"settings-profiles",
|
||||||
[],
|
[],
|
||||||
() => get(userPreferences).backup,
|
() => get(userPreferences).backup,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const activeProfile = persistentWritable<number>("active-profile", 0);
|
||||||
|
export const activeLayer = persistentWritable<number>("active-profile", 0);
|
||||||
|
|
||||||
export const syncStatus: Writable<
|
export const syncStatus: Writable<
|
||||||
"done" | "error" | "downloading" | "uploading"
|
"done" | "error" | "downloading" | "uploading"
|
||||||
> = writable("done");
|
> = writable("done");
|
||||||
@@ -80,32 +83,51 @@ export async function sync() {
|
|||||||
.map((it) => it.items.length)
|
.map((it) => it.items.length)
|
||||||
.reduce((a, b) => a + b, 0);
|
.reduce((a, b) => a + b, 0);
|
||||||
|
|
||||||
const max = maxSettings + device.keyCount * 3 + chordCount;
|
const max =
|
||||||
|
(maxSettings + device.keyCount * device.layerCount) * device.profileCount +
|
||||||
|
chordCount;
|
||||||
let current = 0;
|
let current = 0;
|
||||||
|
activeProfile.update((it) => Math.min(it, device.profileCount - 1));
|
||||||
|
activeLayer.update((it) => Math.min(it, device.layerCount - 1));
|
||||||
syncProgress.set({ max, current });
|
syncProgress.set({ max, current });
|
||||||
function progressTick() {
|
function progressTick() {
|
||||||
current++;
|
current++;
|
||||||
syncProgress.set({ max, current });
|
syncProgress.set({ max, current });
|
||||||
}
|
}
|
||||||
|
|
||||||
const parsedSettings: number[] = [];
|
const parsedSettings: number[][] = Array.from(
|
||||||
|
{ length: device.profileCount },
|
||||||
|
() => [],
|
||||||
|
);
|
||||||
|
for (const [profile, settings] of parsedSettings.entries()) {
|
||||||
for (const category of meta.settings) {
|
for (const category of meta.settings) {
|
||||||
for (const setting of category.items) {
|
for (const setting of category.items) {
|
||||||
try {
|
try {
|
||||||
parsedSettings[setting.id] = await device.getSetting(setting.id);
|
settings[setting.id] = await device.getSetting(profile, setting.id);
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
progressTick();
|
progressTick();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
deviceSettings.set(parsedSettings);
|
deviceSettings.set(parsedSettings);
|
||||||
|
|
||||||
const parsedLayout: CharaLayout = [[], [], []];
|
const parsedLayout: CharaLayout[] = Array.from(
|
||||||
for (let layer = 1; layer <= 3; layer++) {
|
{ length: device.profileCount },
|
||||||
for (let i = 0; i < device.keyCount; i++) {
|
() =>
|
||||||
parsedLayout[layer - 1]![i] = await device.getLayoutKey(layer, i);
|
Array.from({ length: device.layerCount }, () =>
|
||||||
|
Array.from({ length: device.keyCount }, () => 0),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
for (const [profile, layout] of parsedLayout.entries()) {
|
||||||
|
for (const [layer, keys] of layout.entries()) {
|
||||||
|
for (let i = 0; i < keys.length; i++) {
|
||||||
|
try {
|
||||||
|
keys[i] = await device.getLayoutKey(profile, layer + 1, i);
|
||||||
|
} catch {}
|
||||||
progressTick();
|
progressTick();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
deviceLayout.set(parsedLayout);
|
deviceLayout.set(parsedLayout);
|
||||||
|
|
||||||
const chordInfo = [];
|
const chordInfo = [];
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { LineBreakTransformer } from "$lib/serial/line-break-transformer";
|
import { LineBreakTransformer } from "$lib/serial/line-break-transformer";
|
||||||
import { serialLog } from "$lib/serial/connection";
|
import { serialLog } from "$lib/serial/connection";
|
||||||
import type { Chord } from "$lib/serial/chord";
|
import type { Chord } from "$lib/serial/chord";
|
||||||
import { SemVer } from "$lib/serial/sem-ver";
|
|
||||||
import {
|
import {
|
||||||
parseChordActions,
|
parseChordActions,
|
||||||
parsePhrase,
|
parsePhrase,
|
||||||
@@ -10,6 +9,7 @@ import {
|
|||||||
} from "$lib/serial/chord";
|
} from "$lib/serial/chord";
|
||||||
import { browser } from "$app/environment";
|
import { browser } from "$app/environment";
|
||||||
import { showConnectionFailedDialog } from "$lib/dialogs/connection-failed-dialog";
|
import { showConnectionFailedDialog } from "$lib/dialogs/connection-failed-dialog";
|
||||||
|
import semverGte from "semver/functions/gte";
|
||||||
|
|
||||||
const PORT_FILTERS: Map<string, SerialPortFilter> = new Map([
|
const PORT_FILTERS: Map<string, SerialPortFilter> = new Map([
|
||||||
["ONE M0", { usbProductId: 32783, usbVendorId: 9114 }],
|
["ONE M0", { usbProductId: 32783, usbVendorId: 9114 }],
|
||||||
@@ -100,11 +100,13 @@ export class CharaDevice {
|
|||||||
private readonly suspendDebounce = 100;
|
private readonly suspendDebounce = 100;
|
||||||
private suspendDebounceId?: number;
|
private suspendDebounceId?: number;
|
||||||
|
|
||||||
version!: SemVer;
|
version!: string;
|
||||||
company!: "CHARACHORDER" | "FORGE";
|
company!: "CHARACHORDER" | "FORGE";
|
||||||
device!: "ONE" | "TWO" | "LITE" | "X" | "M4G";
|
device!: "ONE" | "TWO" | "LITE" | "X" | "M4G";
|
||||||
chipset!: "M0" | "S2" | "S3";
|
chipset!: "M0" | "S2" | "S3";
|
||||||
keyCount!: 90 | 67 | 256;
|
keyCount!: 90 | 67 | 256;
|
||||||
|
layerCount = 3;
|
||||||
|
profileCount = 1;
|
||||||
|
|
||||||
get portInfo() {
|
get portInfo() {
|
||||||
return this.port.getInfo();
|
return this.port.getInfo();
|
||||||
@@ -135,9 +137,13 @@ export class CharaDevice {
|
|||||||
});
|
});
|
||||||
await this.port.close();
|
await this.port.close();
|
||||||
|
|
||||||
this.version = new SemVer(
|
this.version = await this.send(1, ["VERSION"]).then(
|
||||||
await this.send(1, ["VERSION"]).then(([version]) => version),
|
([version]) => version,
|
||||||
);
|
);
|
||||||
|
// TODO: beta.3
|
||||||
|
if (semverGte(this.version, "2.2.0-beta.3")) {
|
||||||
|
this.profileCount = 3;
|
||||||
|
}
|
||||||
const [company, device, chipset] = await this.send(3, ["ID"]);
|
const [company, device, chipset] = await this.send(3, ["ID"]);
|
||||||
this.company = company as typeof this.company;
|
this.company = company as typeof this.company;
|
||||||
this.device = device as typeof this.device;
|
this.device = device as typeof this.device;
|
||||||
@@ -369,11 +375,16 @@ export class CharaDevice {
|
|||||||
* @param id id of the key, refer to the individual device for where each key is
|
* @param id id of the key, refer to the individual device for where each key is
|
||||||
* @param action the assigned action id
|
* @param action the assigned action id
|
||||||
*/
|
*/
|
||||||
async setLayoutKey(layer: number, id: number, action: number) {
|
async setLayoutKey(
|
||||||
|
profile: number,
|
||||||
|
layer: number,
|
||||||
|
id: number,
|
||||||
|
action: number,
|
||||||
|
) {
|
||||||
const [status] = await this.send(1, [
|
const [status] = await this.send(1, [
|
||||||
"VAR",
|
"VAR",
|
||||||
"B4",
|
"B4",
|
||||||
`A${layer}`,
|
`${String.fromCodePoint("A".codePointAt(0)! + profile)}${layer}`,
|
||||||
id.toString(),
|
id.toString(),
|
||||||
action.toString(),
|
action.toString(),
|
||||||
]);
|
]);
|
||||||
@@ -386,11 +397,11 @@ export class CharaDevice {
|
|||||||
* @param id id of the key, refer to the individual device for where each key is
|
* @param id id of the key, refer to the individual device for where each key is
|
||||||
* @returns the assigned action id
|
* @returns the assigned action id
|
||||||
*/
|
*/
|
||||||
async getLayoutKey(layer: number, id: number) {
|
async getLayoutKey(profile: number, layer: number, id: number) {
|
||||||
const [position, status] = await this.send(2, [
|
const [position, status] = await this.send(2, [
|
||||||
"VAR",
|
"VAR",
|
||||||
"B3",
|
"B3",
|
||||||
`A${layer}`,
|
`${String.fromCodePoint("A".codePointAt(0)! + profile)}${layer}`,
|
||||||
id.toString(),
|
id.toString(),
|
||||||
]);
|
]);
|
||||||
if (status !== "0") throw new Error(`Failed with status ${status}`);
|
if (status !== "0") throw new Error(`Failed with status ${status}`);
|
||||||
@@ -415,11 +426,11 @@ export class CharaDevice {
|
|||||||
* Settings are applied until the next reboot or loss of power.
|
* Settings are applied until the next reboot or loss of power.
|
||||||
* To permanently store the settings, you *must* call commit.
|
* To permanently store the settings, you *must* call commit.
|
||||||
*/
|
*/
|
||||||
async setSetting(id: number, value: number) {
|
async setSetting(profile: number, id: number, value: number) {
|
||||||
const [status] = await this.send(1, [
|
const [status] = await this.send(1, [
|
||||||
"VAR",
|
"VAR",
|
||||||
"B2",
|
"B2",
|
||||||
id.toString(16).toUpperCase(),
|
(id + profile * 0x100).toString(16).toUpperCase(),
|
||||||
value.toString(),
|
value.toString(),
|
||||||
]);
|
]);
|
||||||
if (status !== "0") throw new Error(`Failed with status ${status}`);
|
if (status !== "0") throw new Error(`Failed with status ${status}`);
|
||||||
@@ -428,11 +439,11 @@ export class CharaDevice {
|
|||||||
/**
|
/**
|
||||||
* Retrieves a setting from the device
|
* Retrieves a setting from the device
|
||||||
*/
|
*/
|
||||||
async getSetting(id: number): Promise<number> {
|
async getSetting(profile: number, id: number): Promise<number> {
|
||||||
const [value, status] = await this.send(2, [
|
const [value, status] = await this.send(2, [
|
||||||
"VAR",
|
"VAR",
|
||||||
"B1",
|
"B1",
|
||||||
id.toString(16).toUpperCase(),
|
(id + profile * 0x100).toString(16).toUpperCase(),
|
||||||
]);
|
]);
|
||||||
if (status !== "0")
|
if (status !== "0")
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
export class SemVer {
|
|
||||||
major = 0;
|
|
||||||
minor = 0;
|
|
||||||
patch = 0;
|
|
||||||
preRelease?: string;
|
|
||||||
meta?: string;
|
|
||||||
|
|
||||||
constructor(versionString: string) {
|
|
||||||
const result =
|
|
||||||
/^([0-9]+)\.([0-9]+)\.([0-9]+)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+([0-9A-Za-z-]+))?$/.exec(
|
|
||||||
versionString,
|
|
||||||
);
|
|
||||||
if (!result) {
|
|
||||||
console.error("Invalid version string:", versionString);
|
|
||||||
} else {
|
|
||||||
const [, major, minor, patch, preRelease, meta] = result;
|
|
||||||
this.major = Number.parseInt(major ?? "NaN");
|
|
||||||
this.minor = Number.parseInt(minor ?? "NaN");
|
|
||||||
this.patch = Number.parseInt(patch ?? "NaN");
|
|
||||||
if (preRelease) this.preRelease = preRelease;
|
|
||||||
if (meta) this.meta = meta;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
toString() {
|
|
||||||
return (
|
|
||||||
`${this.major}.${this.minor}.${this.patch}` +
|
|
||||||
(this.preRelease ? `-${this.preRelease}` : "") +
|
|
||||||
(this.meta ? `+${this.meta}` : "")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,13 +4,10 @@ import { fromBase64, toBase64 } from "$lib/serialization/base64";
|
|||||||
export interface NewCharaLayout {
|
export interface NewCharaLayout {
|
||||||
charaLayoutVersion: 1;
|
charaLayoutVersion: 1;
|
||||||
device: "one" | "lite" | string;
|
device: "one" | "lite" | string;
|
||||||
/**
|
layers: number[][];
|
||||||
* Layers A1-A3, with numeric action codes on each
|
|
||||||
*/
|
|
||||||
layers: [number[], number[], number[]];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CharaLayout = [number[], number[], number[]];
|
export type CharaLayout = number[][];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Serialize a layout into a micro package
|
* Serialize a layout into a micro package
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import type { Action } from "svelte/action";
|
import type { Action } from "svelte/action";
|
||||||
import { changes, ChangeType, settings } from "$lib/undo-redo";
|
import { changes, ChangeType, settings } from "$lib/undo-redo";
|
||||||
|
import { activeProfile } from "./serial/connection";
|
||||||
|
import { combineLatest, map } from "rxjs";
|
||||||
|
import { fromReadable } from "./util/from-readable";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* https://gist.github.com/mjackson/5311256
|
* https://gist.github.com/mjackson/5311256
|
||||||
@@ -103,7 +106,12 @@ export const setting: Action<
|
|||||||
? Number(node.getAttribute("max"))
|
? Number(node.getAttribute("max"))
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const unsubscribe = settings.subscribe(async (settings) => {
|
const subscription = combineLatest([
|
||||||
|
fromReadable(settings),
|
||||||
|
fromReadable(activeProfile),
|
||||||
|
])
|
||||||
|
.pipe(map(([settings, profile]) => settings[profile]!))
|
||||||
|
.subscribe(async (settings) => {
|
||||||
if (id in settings) {
|
if (id in settings) {
|
||||||
const { value, isApplied } = settings[id]!;
|
const { value, isApplied } = settings[id]!;
|
||||||
if (isNumeric) {
|
if (isNumeric) {
|
||||||
@@ -186,7 +194,7 @@ export const setting: Action<
|
|||||||
return {
|
return {
|
||||||
destroy() {
|
destroy() {
|
||||||
node.removeEventListener("change", listener);
|
node.removeEventListener("change", listener);
|
||||||
unsubscribe();
|
subscription.unsubscribe();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
36
src/lib/style/form/_radio.scss
Normal file
36
src/lib/style/form/_radio.scss
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
label:has(input[type="radio"]) {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
aspect-ratio: unset;
|
||||||
|
height: 1.5em;
|
||||||
|
padding-inline: 12px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0;
|
||||||
|
|
||||||
|
font-size: 16px;
|
||||||
|
color: var(--md-sys-color-on-surface-variant);
|
||||||
|
|
||||||
|
background: var(--md-sys-color-surface-variant);
|
||||||
|
|
||||||
|
transition: all 250ms ease;
|
||||||
|
|
||||||
|
> input[type="radio"] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
border-radius: 16px 0 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-radius: 0 16px 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:has(:checked) {
|
||||||
|
font-weight: 900;
|
||||||
|
color: var(--md-sys-color-on-tertiary);
|
||||||
|
background: var(--md-sys-color-tertiary);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
@use "form/button";
|
@use "form/button";
|
||||||
@use "form/toggle";
|
@use "form/toggle";
|
||||||
|
@use "form/radio";
|
||||||
|
|
||||||
@use "kbd";
|
@use "kbd";
|
||||||
@use "print";
|
@use "print";
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export interface LayoutChange {
|
|||||||
id: number;
|
id: number;
|
||||||
layer: number;
|
layer: number;
|
||||||
action: number;
|
action: number;
|
||||||
|
profile?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ChordChange {
|
export interface ChordChange {
|
||||||
@@ -33,6 +34,7 @@ export interface SettingChange {
|
|||||||
type: ChangeType.Setting;
|
type: ChangeType.Setting;
|
||||||
id: number;
|
id: number;
|
||||||
setting: number;
|
setting: number;
|
||||||
|
profile?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ChangeInfo {
|
export interface ChangeInfo {
|
||||||
@@ -45,23 +47,29 @@ export type Change = LayoutChange | ChordChange | SettingChange;
|
|||||||
export const changes = persistentWritable<Change[][]>("changes", []);
|
export const changes = persistentWritable<Change[][]>("changes", []);
|
||||||
|
|
||||||
export interface Overlay {
|
export interface Overlay {
|
||||||
layout: [Map<number, number>, Map<number, number>, Map<number, number>];
|
layout: Array<Array<Map<number, number> | undefined> | undefined>;
|
||||||
chords: Map<string, Chord & { deleted: boolean }>;
|
chords: Map<string, Chord & { deleted: boolean }>;
|
||||||
settings: Map<number, number>;
|
settings: Array<Map<number, number> | undefined>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const overlay = derived(changes, (changes) => {
|
export const overlay = derived(changes, (changes) => {
|
||||||
const overlay: Overlay = {
|
const overlay: Overlay = {
|
||||||
layout: [new Map(), new Map(), new Map()],
|
layout: [],
|
||||||
chords: new Map(),
|
chords: new Map(),
|
||||||
settings: new Map(),
|
settings: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const changeset of changes) {
|
for (const changeset of changes) {
|
||||||
for (const change of changeset) {
|
for (const change of changeset) {
|
||||||
switch (change.type) {
|
switch (change.type) {
|
||||||
case ChangeType.Layout:
|
case ChangeType.Layout:
|
||||||
overlay.layout[change.layer]?.set(change.id, change.action);
|
change.profile ??= 0;
|
||||||
|
overlay.layout[change.profile] ??= [];
|
||||||
|
overlay.layout[change.profile]![change.layer] ??= new Map();
|
||||||
|
overlay.layout[change.profile]![change.layer]!.set(
|
||||||
|
change.id,
|
||||||
|
change.action,
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
case ChangeType.Chord:
|
case ChangeType.Chord:
|
||||||
overlay.chords.set(JSON.stringify(change.id), {
|
overlay.chords.set(JSON.stringify(change.id), {
|
||||||
@@ -71,7 +79,9 @@ export const overlay = derived(changes, (changes) => {
|
|||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case ChangeType.Setting:
|
case ChangeType.Setting:
|
||||||
overlay.settings.set(change.id, change.setting);
|
change.profile ??= 0;
|
||||||
|
overlay.settings[change.profile] ??= new Map();
|
||||||
|
overlay.settings[change.profile]!.set(change.id, change.setting);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -82,22 +92,26 @@ export const overlay = derived(changes, (changes) => {
|
|||||||
|
|
||||||
export const settings = derived(
|
export const settings = derived(
|
||||||
[overlay, deviceSettings],
|
[overlay, deviceSettings],
|
||||||
([overlay, settings]) =>
|
([overlay, profiles]) =>
|
||||||
|
profiles.map((settings, profile) =>
|
||||||
settings.map<{ value: number } & ChangeInfo>((value, id) => ({
|
settings.map<{ value: number } & ChangeInfo>((value, id) => ({
|
||||||
value: overlay.settings.get(id) ?? value,
|
value: overlay.settings[profile]?.get(id) ?? value,
|
||||||
isApplied: !overlay.settings.has(id),
|
isApplied: !overlay.settings[profile]?.has(id),
|
||||||
})),
|
})),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
export type KeyInfo = { action: number } & ChangeInfo;
|
export type KeyInfo = { action: number } & ChangeInfo;
|
||||||
export const layout = derived([overlay, deviceLayout], ([overlay, layout]) =>
|
export const layout = derived([overlay, deviceLayout], ([overlay, profiles]) =>
|
||||||
|
profiles.map((layout, profile) =>
|
||||||
layout.map(
|
layout.map(
|
||||||
(actions, layer) =>
|
(actions, layer) =>
|
||||||
actions.map<KeyInfo>((action, id) => ({
|
actions.map<KeyInfo>((action, id) => ({
|
||||||
action: overlay.layout[layer]?.get(id) ?? action,
|
action: overlay.layout[profile]?.[layer]?.get(id) ?? action,
|
||||||
isApplied: !overlay.layout[layer]?.has(id),
|
isApplied: !overlay.layout[profile]?.[layer]?.has(id),
|
||||||
})) as [KeyInfo, KeyInfo, KeyInfo],
|
})) as [KeyInfo, KeyInfo, KeyInfo],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
export type ChordInfo = Chord &
|
export type ChordInfo = Chord &
|
||||||
|
|||||||
10
src/lib/util/from-readable.ts
Normal file
10
src/lib/util/from-readable.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { Observable } from "rxjs";
|
||||||
|
import type { Readable } from "svelte/store";
|
||||||
|
|
||||||
|
export function fromReadable<T>(store: Readable<T>): Observable<T> {
|
||||||
|
return new Observable((subscriber) =>
|
||||||
|
store.subscribe((value) => {
|
||||||
|
subscriber.next(value);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -48,10 +48,14 @@
|
|||||||
$syncStatus = "uploading";
|
$syncStatus = "uploading";
|
||||||
|
|
||||||
const layoutChanges = $overlay.layout.reduce(
|
const layoutChanges = $overlay.layout.reduce(
|
||||||
(acc, layer) => acc + layer.size,
|
(acc, profile) =>
|
||||||
|
acc + profile.reduce((acc, layer) => acc + layer.size, 0),
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
const settingChanges = $overlay.settings.reduce(
|
||||||
|
(acc, profile) => acc + profile.size,
|
||||||
0,
|
0,
|
||||||
);
|
);
|
||||||
const settingChanges = $overlay.settings.size;
|
|
||||||
const chordChanges = $overlay.chords.size;
|
const chordChanges = $overlay.chords.size;
|
||||||
const needsCommit = settingChanges > 0 || layoutChanges > 0;
|
const needsCommit = settingChanges > 0 || layoutChanges > 0;
|
||||||
const progressMax = layoutChanges + settingChanges + chordChanges;
|
const progressMax = layoutChanges + settingChanges + chordChanges;
|
||||||
@@ -63,6 +67,8 @@
|
|||||||
current: progressCurrent,
|
current: progressCurrent,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log($overlay);
|
||||||
|
|
||||||
for (const [id, chord] of $overlay.chords) {
|
for (const [id, chord] of $overlay.chords) {
|
||||||
if (!chord.deleted) {
|
if (!chord.deleted) {
|
||||||
if (id !== JSON.stringify(chord.actions)) {
|
if (id !== JSON.stringify(chord.actions)) {
|
||||||
@@ -105,23 +111,32 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [layer, actions] of $overlay.layout.entries()) {
|
for (const [profile, layout] of $overlay.layout.entries()) {
|
||||||
|
if (layout === undefined) continue;
|
||||||
|
for (const [layer, actions] of layout.entries()) {
|
||||||
|
if (actions === undefined) continue;
|
||||||
for (const [id, action] of actions) {
|
for (const [id, action] of actions) {
|
||||||
await port.setLayoutKey(layer + 1, id, action);
|
if (action === undefined) continue;
|
||||||
|
await port.setLayoutKey(profile, layer + 1, id, action);
|
||||||
syncProgress.set({
|
syncProgress.set({
|
||||||
max: progressMax,
|
max: progressMax,
|
||||||
current: progressCurrent++,
|
current: progressCurrent++,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (const [id, setting] of $overlay.settings) {
|
for (const [profile, settings] of $overlay.settings.entries()) {
|
||||||
await port.setSetting(id, setting);
|
if (settings === undefined) continue;
|
||||||
|
for (const [id, setting] of settings.entries()) {
|
||||||
|
if (setting === undefined) continue;
|
||||||
|
await port.setSetting(profile, id, setting);
|
||||||
syncProgress.set({
|
syncProgress.set({
|
||||||
max: progressMax,
|
max: progressMax,
|
||||||
current: progressCurrent++,
|
current: progressCurrent++,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Yes, this is a completely arbitrary and unnecessary delay.
|
// Yes, this is a completely arbitrary and unnecessary delay.
|
||||||
// The only purpose of it is to create a sense of weight,
|
// The only purpose of it is to create a sense of weight,
|
||||||
@@ -134,13 +149,15 @@
|
|||||||
await port.commit();
|
await port.commit();
|
||||||
}
|
}
|
||||||
|
|
||||||
$deviceLayout = $layout.map((layer) =>
|
$deviceLayout = $layout.map((profile) =>
|
||||||
layer.map<number>(({ action }) => action),
|
profile.map((layer) => layer.map<number>(({ action }) => action)),
|
||||||
) as [number[], number[], number[]];
|
);
|
||||||
$deviceChords = $chords
|
$deviceChords = $chords
|
||||||
.filter(({ deleted }) => !deleted)
|
.filter(({ deleted }) => !deleted)
|
||||||
.map(({ actions, phrase }) => ({ actions, phrase }));
|
.map(({ actions, phrase }) => ({ actions, phrase }));
|
||||||
$deviceSettings = $settings.map(({ value }) => value);
|
$deviceSettings = $settings.map((profile) =>
|
||||||
|
profile.map(({ value }) => value),
|
||||||
|
);
|
||||||
$changes = [];
|
$changes = [];
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert(e);
|
alert(e);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { fly } from "svelte/transition";
|
import { fly } from "svelte/transition";
|
||||||
import { canShare, triggerShare } from "$lib/share";
|
import { canShare, triggerShare } from "$lib/share";
|
||||||
import { action } from "$lib/title";
|
import { action } from "$lib/title";
|
||||||
|
import { activeProfile, serialPort } from "$lib/serial/connection";
|
||||||
import LL from "$i18n/i18n-svelte";
|
import LL from "$i18n/i18n-svelte";
|
||||||
import EditActions from "./EditActions.svelte";
|
import EditActions from "./EditActions.svelte";
|
||||||
</script>
|
</script>
|
||||||
@@ -11,6 +12,23 @@
|
|||||||
<EditActions />
|
<EditActions />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="profiles">
|
||||||
|
{#if $serialPort}
|
||||||
|
{#if $serialPort.profileCount > 1}
|
||||||
|
{#each Array.from({ length: $serialPort.profileCount }, (_, i) => i) as profile}
|
||||||
|
<label
|
||||||
|
><input
|
||||||
|
type="radio"
|
||||||
|
name="profile"
|
||||||
|
checked={profile == $activeProfile}
|
||||||
|
onclick={() => ($activeProfile = profile)}
|
||||||
|
/>{String.fromCodePoint("A".codePointAt(0)! + profile)}</label
|
||||||
|
>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
{#if $canShare}
|
{#if $canShare}
|
||||||
<button
|
<button
|
||||||
@@ -37,7 +55,7 @@
|
|||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
nav {
|
nav {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr auto 1fr;
|
||||||
|
|
||||||
width: calc(min(100%, 28cm));
|
width: calc(min(100%, 28cm));
|
||||||
margin-block: 8px;
|
margin-block: 8px;
|
||||||
@@ -76,6 +94,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.profiles {
|
||||||
|
display: flex;
|
||||||
|
gap: 2px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
:disabled {
|
:disabled {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
import { charaFileToUriComponent } from "$lib/share/share-url";
|
import { charaFileToUriComponent } from "$lib/share/share-url";
|
||||||
import SharePopup from "../SharePopup.svelte";
|
import SharePopup from "../SharePopup.svelte";
|
||||||
import type { VisualLayoutConfig } from "$lib/components/layout/visual-layout";
|
import type { VisualLayoutConfig } from "$lib/components/layout/visual-layout";
|
||||||
import { writable } from "svelte/store";
|
|
||||||
import { layout } from "$lib/undo-redo";
|
import { layout } from "$lib/undo-redo";
|
||||||
|
|
||||||
async function shareLayout(event: Event) {
|
async function shareLayout(event: Event) {
|
||||||
@@ -49,8 +48,6 @@
|
|||||||
fontSize: 9,
|
fontSize: 9,
|
||||||
iconFontSize: 14,
|
iconFontSize: 14,
|
||||||
});
|
});
|
||||||
|
|
||||||
setContext("active-layer", writable(0));
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
|
|||||||
Reference in New Issue
Block a user