mirror of
https://github.com/CharaChorder/DeviceManager.git
synced 2026-01-20 17:03:42 +00:00
feat: profile support
This commit is contained in:
@@ -14,7 +14,7 @@ import {
|
||||
settings,
|
||||
} from "$lib/undo-redo.js";
|
||||
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 { isCsvChords, csvChordsToJson } from "./compat/legacy-chords";
|
||||
|
||||
@@ -50,11 +50,9 @@ export function createLayoutBackup(): CharaLayoutFile {
|
||||
charaVersion: 1,
|
||||
type: "layout",
|
||||
device: get(serialPort)?.device,
|
||||
layout: get(layout).map((it) => it.map((it) => it.action)) as [
|
||||
number[],
|
||||
number[],
|
||||
number[],
|
||||
],
|
||||
layout: (get(layout)[get(activeProfile)]?.map((it) =>
|
||||
it.map((it) => it.action),
|
||||
) ?? []) as [number[], number[], number[]],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -70,7 +68,7 @@ export function createSettingsBackup(): CharaSettingsFile {
|
||||
return {
|
||||
charaVersion: 1,
|
||||
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];
|
||||
if (!recent) return;
|
||||
let backupDevice = recent[1].device;
|
||||
if (backupDevice === "TWO") backupDevice = "ONE";
|
||||
if (backupDevice === "TWO" || backupDevice === "M4G")
|
||||
backupDevice = "ONE";
|
||||
let currentDevice = get(serialPort)?.device;
|
||||
if (currentDevice === "TWO") currentDevice = "ONE";
|
||||
if (currentDevice === "TWO" || backupDevice === "M4G")
|
||||
currentDevice = "ONE";
|
||||
|
||||
if (backupDevice !== currentDevice) {
|
||||
alert("Backup is incompatible with this device");
|
||||
@@ -167,12 +167,13 @@ export function getChangesFromChordFile(file: CharaChordFile) {
|
||||
export function getChangesFromSettingsFile(file: CharaSettingsFile) {
|
||||
const changes: Change[] = [];
|
||||
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) {
|
||||
changes.push({
|
||||
type: ChangeType.Setting,
|
||||
id,
|
||||
setting: value,
|
||||
profile: get(activeProfile),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -183,12 +184,13 @@ export function getChangesFromLayoutFile(file: CharaLayoutFile) {
|
||||
const changes: Change[] = [];
|
||||
for (const [layer, keys] of file.layout.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({
|
||||
type: ChangeType.Layout,
|
||||
layer,
|
||||
id,
|
||||
action,
|
||||
profile: get(activeProfile),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,17 +8,16 @@
|
||||
import { dev } from "$app/environment";
|
||||
import ActionSelector from "$lib/components/layout/ActionSelector.svelte";
|
||||
import { get } from "svelte/store";
|
||||
import type { Writable } from "svelte/store";
|
||||
import KeyboardKey from "$lib/components/layout/KeyboardKey.svelte";
|
||||
import { getContext, mount, unmount } from "svelte";
|
||||
import type { VisualLayoutConfig } from "./visual-layout.js";
|
||||
import { changes, ChangeType, layout } from "$lib/undo-redo";
|
||||
import { fly } from "svelte/transition";
|
||||
import { expoOut } from "svelte/easing";
|
||||
import { activeLayer, activeProfile } from "$lib/serial/connection";
|
||||
|
||||
const { scale, margin, strokeWidth, fontSize, iconFontSize } =
|
||||
getContext<VisualLayoutConfig>("visual-layout-config");
|
||||
const activeLayer = getContext<Writable<number>>("active-layer");
|
||||
|
||||
if (dev) {
|
||||
// you have absolutely no idea what a difference this makes for performance
|
||||
@@ -125,8 +124,10 @@
|
||||
const keyInfo = layoutInfo.keys[index];
|
||||
if (!keyInfo) return;
|
||||
const clickedGroup = groupParent.children.item(index) as SVGGElement;
|
||||
const nextAction = get(layout)[get(activeLayer)]?.[keyInfo.id];
|
||||
const currentAction = get(deviceLayout)[get(activeLayer)]?.[keyInfo.id];
|
||||
const nextAction =
|
||||
get(layout)[get(activeProfile)][get(activeLayer)]?.[keyInfo.id];
|
||||
const currentAction =
|
||||
get(deviceLayout)[get(activeProfile)][get(activeLayer)]?.[keyInfo.id];
|
||||
const component = mount(ActionSelector, {
|
||||
target: document.body,
|
||||
props: {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from "svelte";
|
||||
import type { Writable } from "svelte/store";
|
||||
import type { VisualLayoutConfig } from "$lib/components/layout/visual-layout";
|
||||
import type { CompiledLayoutKey } from "$lib/serialization/visual-layout";
|
||||
@@ -7,10 +6,11 @@
|
||||
import { osLayout } from "$lib/os-layout.js";
|
||||
import { KEYMAP_CODES } from "$lib/serial/keymap-codes";
|
||||
import { action } from "$lib/title";
|
||||
import { activeProfile, activeLayer } from "$lib/serial/connection";
|
||||
import { getContext } from "svelte";
|
||||
|
||||
const { fontSize, margin, inactiveOpacity, inactiveScale, iconFontSize } =
|
||||
getContext<VisualLayoutConfig>("visual-layout-config");
|
||||
const activeLayer = getContext<Writable<number>>("active-layer");
|
||||
const currentAction = getContext<Writable<Set<number>> | undefined>(
|
||||
"highlight-action",
|
||||
);
|
||||
@@ -33,7 +33,9 @@
|
||||
</script>
|
||||
|
||||
{#each positions as position, layer}
|
||||
{@const { action: actionId, isApplied } = $layout[layer]?.[key.id] ?? {
|
||||
{@const { action: actionId, isApplied } = $layout[$activeProfile]?.[layer]?.[
|
||||
key.id
|
||||
] ?? {
|
||||
action: 0,
|
||||
isApplied: true,
|
||||
}}
|
||||
|
||||
@@ -2,21 +2,11 @@
|
||||
import { deviceMeta, serialPort } from "$lib/serial/connection";
|
||||
import { action } from "$lib/title";
|
||||
import GenericLayout from "$lib/components/layout/GenericLayout.svelte";
|
||||
import { getContext } from "svelte";
|
||||
import type { Writable } from "svelte/store";
|
||||
import { activeProfile, activeLayer } from "$lib/serial/connection";
|
||||
import type { VisualLayout } from "$lib/serialization/visual-layout";
|
||||
import { fade, fly } from "svelte/transition";
|
||||
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 = {
|
||||
ONE: () =>
|
||||
import("$lib/assets/layouts/one.yml").then(
|
||||
@@ -46,19 +36,25 @@
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
{#if device}
|
||||
{#await layouts[device]() then visualLayout}
|
||||
{#if $serialPort}
|
||||
{#await layouts[$serialPort.device]() then visualLayout}
|
||||
<fieldset transition:fade>
|
||||
{#each layers as [title, icon, value]}
|
||||
<button
|
||||
class="icon"
|
||||
use:action={{ title, shortcut: `alt+${value + 1}` }}
|
||||
onclick={() => ($activeLayer = value)}
|
||||
class:active={$activeLayer === value}
|
||||
>
|
||||
{icon}
|
||||
</button>
|
||||
{/each}
|
||||
<div class="layers">
|
||||
{#each Array.from({ length: $serialPort.layerCount }, (_, i) => i) as layer}
|
||||
<label>
|
||||
<input
|
||||
type="radio"
|
||||
onclick={() => ($activeLayer = layer)}
|
||||
name="layer"
|
||||
value={layer}
|
||||
checked={$activeLayer === layer}
|
||||
/>
|
||||
{String.fromCodePoint(
|
||||
"A".codePointAt(0)! + $activeProfile,
|
||||
)}{layer + 1}
|
||||
</label>
|
||||
{/each}
|
||||
</div>
|
||||
{#if $deviceMeta?.factoryDefaults?.layout}
|
||||
<button
|
||||
use:action={{ title: "Reset Layout" }}
|
||||
@@ -100,60 +96,13 @@
|
||||
border: none;
|
||||
}
|
||||
|
||||
button.icon {
|
||||
cursor: pointer;
|
||||
.layers {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
z-index: 1;
|
||||
margin-inline: auto;
|
||||
|
||||
font-size: 24px;
|
||||
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);
|
||||
}
|
||||
gap: 2px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -29,21 +29,24 @@ export const deviceChords = persistentWritable<Chord[]>(
|
||||
/**
|
||||
* Layout as read from the device
|
||||
*/
|
||||
export const deviceLayout = persistentWritable<CharaLayout>(
|
||||
"layout",
|
||||
[[], [], []],
|
||||
export const deviceLayout = persistentWritable<CharaLayout[]>(
|
||||
"layout-profiles",
|
||||
[],
|
||||
() => get(userPreferences).backup,
|
||||
);
|
||||
|
||||
/**
|
||||
* Settings as read from the device
|
||||
*/
|
||||
export const deviceSettings = persistentWritable<number[]>(
|
||||
"device-settings",
|
||||
export const deviceSettings = persistentWritable<number[][]>(
|
||||
"settings-profiles",
|
||||
[],
|
||||
() => get(userPreferences).backup,
|
||||
);
|
||||
|
||||
export const activeProfile = persistentWritable<number>("active-profile", 0);
|
||||
export const activeLayer = persistentWritable<number>("active-profile", 0);
|
||||
|
||||
export const syncStatus: Writable<
|
||||
"done" | "error" | "downloading" | "uploading"
|
||||
> = writable("done");
|
||||
@@ -80,30 +83,49 @@ export async function sync() {
|
||||
.map((it) => it.items.length)
|
||||
.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;
|
||||
activeProfile.update((it) => Math.min(it, device.profileCount - 1));
|
||||
activeLayer.update((it) => Math.min(it, device.layerCount - 1));
|
||||
syncProgress.set({ max, current });
|
||||
function progressTick() {
|
||||
current++;
|
||||
syncProgress.set({ max, current });
|
||||
}
|
||||
|
||||
const parsedSettings: number[] = [];
|
||||
for (const category of meta.settings) {
|
||||
for (const setting of category.items) {
|
||||
try {
|
||||
parsedSettings[setting.id] = await device.getSetting(setting.id);
|
||||
} catch {}
|
||||
const parsedSettings: number[][] = Array.from(
|
||||
{ length: device.profileCount },
|
||||
() => [],
|
||||
);
|
||||
for (const [profile, settings] of parsedSettings.entries()) {
|
||||
for (const category of meta.settings) {
|
||||
for (const setting of category.items) {
|
||||
try {
|
||||
settings[setting.id] = await device.getSetting(profile, setting.id);
|
||||
} catch {}
|
||||
}
|
||||
progressTick();
|
||||
}
|
||||
progressTick();
|
||||
}
|
||||
deviceSettings.set(parsedSettings);
|
||||
|
||||
const parsedLayout: CharaLayout = [[], [], []];
|
||||
for (let layer = 1; layer <= 3; layer++) {
|
||||
for (let i = 0; i < device.keyCount; i++) {
|
||||
parsedLayout[layer - 1]![i] = await device.getLayoutKey(layer, i);
|
||||
progressTick();
|
||||
const parsedLayout: CharaLayout[] = Array.from(
|
||||
{ length: device.profileCount },
|
||||
() =>
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
deviceLayout.set(parsedLayout);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { LineBreakTransformer } from "$lib/serial/line-break-transformer";
|
||||
import { serialLog } from "$lib/serial/connection";
|
||||
import type { Chord } from "$lib/serial/chord";
|
||||
import { SemVer } from "$lib/serial/sem-ver";
|
||||
import {
|
||||
parseChordActions,
|
||||
parsePhrase,
|
||||
@@ -10,6 +9,7 @@ import {
|
||||
} from "$lib/serial/chord";
|
||||
import { browser } from "$app/environment";
|
||||
import { showConnectionFailedDialog } from "$lib/dialogs/connection-failed-dialog";
|
||||
import semverGte from "semver/functions/gte";
|
||||
|
||||
const PORT_FILTERS: Map<string, SerialPortFilter> = new Map([
|
||||
["ONE M0", { usbProductId: 32783, usbVendorId: 9114 }],
|
||||
@@ -100,11 +100,13 @@ export class CharaDevice {
|
||||
private readonly suspendDebounce = 100;
|
||||
private suspendDebounceId?: number;
|
||||
|
||||
version!: SemVer;
|
||||
version!: string;
|
||||
company!: "CHARACHORDER" | "FORGE";
|
||||
device!: "ONE" | "TWO" | "LITE" | "X" | "M4G";
|
||||
chipset!: "M0" | "S2" | "S3";
|
||||
keyCount!: 90 | 67 | 256;
|
||||
layerCount = 3;
|
||||
profileCount = 1;
|
||||
|
||||
get portInfo() {
|
||||
return this.port.getInfo();
|
||||
@@ -135,9 +137,13 @@ export class CharaDevice {
|
||||
});
|
||||
await this.port.close();
|
||||
|
||||
this.version = new SemVer(
|
||||
await this.send(1, ["VERSION"]).then(([version]) => version),
|
||||
this.version = await this.send(1, ["VERSION"]).then(
|
||||
([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"]);
|
||||
this.company = company as typeof this.company;
|
||||
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 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, [
|
||||
"VAR",
|
||||
"B4",
|
||||
`A${layer}`,
|
||||
`${String.fromCodePoint("A".codePointAt(0)! + profile)}${layer}`,
|
||||
id.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
|
||||
* @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, [
|
||||
"VAR",
|
||||
"B3",
|
||||
`A${layer}`,
|
||||
`${String.fromCodePoint("A".codePointAt(0)! + profile)}${layer}`,
|
||||
id.toString(),
|
||||
]);
|
||||
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.
|
||||
* 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, [
|
||||
"VAR",
|
||||
"B2",
|
||||
id.toString(16).toUpperCase(),
|
||||
(id + profile * 0x100).toString(16).toUpperCase(),
|
||||
value.toString(),
|
||||
]);
|
||||
if (status !== "0") throw new Error(`Failed with status ${status}`);
|
||||
@@ -428,11 +439,11 @@ export class CharaDevice {
|
||||
/**
|
||||
* 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, [
|
||||
"VAR",
|
||||
"B1",
|
||||
id.toString(16).toUpperCase(),
|
||||
(id + profile * 0x100).toString(16).toUpperCase(),
|
||||
]);
|
||||
if (status !== "0")
|
||||
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 {
|
||||
charaLayoutVersion: 1;
|
||||
device: "one" | "lite" | string;
|
||||
/**
|
||||
* Layers A1-A3, with numeric action codes on each
|
||||
*/
|
||||
layers: [number[], number[], number[]];
|
||||
layers: number[][];
|
||||
}
|
||||
|
||||
export type CharaLayout = [number[], number[], number[]];
|
||||
export type CharaLayout = number[][];
|
||||
|
||||
/**
|
||||
* Serialize a layout into a micro package
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import type { Action } from "svelte/action";
|
||||
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
|
||||
@@ -103,37 +106,42 @@ export const setting: Action<
|
||||
? Number(node.getAttribute("max"))
|
||||
: undefined;
|
||||
|
||||
const unsubscribe = settings.subscribe(async (settings) => {
|
||||
if (id in settings) {
|
||||
const { value, isApplied } = settings[id]!;
|
||||
if (isNumeric) {
|
||||
node.value = (
|
||||
inverse !== undefined
|
||||
? inverse / value
|
||||
: scale !== undefined
|
||||
? scale * value
|
||||
: value
|
||||
).toString();
|
||||
} else if (isColor) {
|
||||
const rgb = hsvToRgb(
|
||||
settings[id]!.value,
|
||||
settings[id + 1]!.value,
|
||||
settings[id + 2]!.value,
|
||||
);
|
||||
node.value = `#${rgb.map((c) => c.toString(16).padStart(2, "0")).join("")}`;
|
||||
const subscription = combineLatest([
|
||||
fromReadable(settings),
|
||||
fromReadable(activeProfile),
|
||||
])
|
||||
.pipe(map(([settings, profile]) => settings[profile]!))
|
||||
.subscribe(async (settings) => {
|
||||
if (id in settings) {
|
||||
const { value, isApplied } = settings[id]!;
|
||||
if (isNumeric) {
|
||||
node.value = (
|
||||
inverse !== undefined
|
||||
? inverse / value
|
||||
: scale !== undefined
|
||||
? scale * value
|
||||
: value
|
||||
).toString();
|
||||
} else if (isColor) {
|
||||
const rgb = hsvToRgb(
|
||||
settings[id]!.value,
|
||||
settings[id + 1]!.value,
|
||||
settings[id + 2]!.value,
|
||||
);
|
||||
node.value = `#${rgb.map((c) => c.toString(16).padStart(2, "0")).join("")}`;
|
||||
} else {
|
||||
node.checked = value !== 0;
|
||||
}
|
||||
if (isApplied) {
|
||||
node.classList.remove("pending-changes");
|
||||
} else {
|
||||
node.classList.add("pending-changes");
|
||||
}
|
||||
node.removeAttribute("disabled");
|
||||
} else {
|
||||
node.checked = value !== 0;
|
||||
node.setAttribute("disabled", "");
|
||||
}
|
||||
if (isApplied) {
|
||||
node.classList.remove("pending-changes");
|
||||
} else {
|
||||
node.classList.add("pending-changes");
|
||||
}
|
||||
node.removeAttribute("disabled");
|
||||
} else {
|
||||
node.setAttribute("disabled", "");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
async function listener() {
|
||||
let value: number;
|
||||
@@ -186,7 +194,7 @@ export const setting: Action<
|
||||
return {
|
||||
destroy() {
|
||||
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/toggle";
|
||||
@use "form/radio";
|
||||
|
||||
@use "kbd";
|
||||
@use "print";
|
||||
|
||||
@@ -19,6 +19,7 @@ export interface LayoutChange {
|
||||
id: number;
|
||||
layer: number;
|
||||
action: number;
|
||||
profile?: number;
|
||||
}
|
||||
|
||||
export interface ChordChange {
|
||||
@@ -33,6 +34,7 @@ export interface SettingChange {
|
||||
type: ChangeType.Setting;
|
||||
id: number;
|
||||
setting: number;
|
||||
profile?: number;
|
||||
}
|
||||
|
||||
export interface ChangeInfo {
|
||||
@@ -45,23 +47,29 @@ export type Change = LayoutChange | ChordChange | SettingChange;
|
||||
export const changes = persistentWritable<Change[][]>("changes", []);
|
||||
|
||||
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 }>;
|
||||
settings: Map<number, number>;
|
||||
settings: Array<Map<number, number> | undefined>;
|
||||
}
|
||||
|
||||
export const overlay = derived(changes, (changes) => {
|
||||
const overlay: Overlay = {
|
||||
layout: [new Map(), new Map(), new Map()],
|
||||
layout: [],
|
||||
chords: new Map(),
|
||||
settings: new Map(),
|
||||
settings: [],
|
||||
};
|
||||
|
||||
for (const changeset of changes) {
|
||||
for (const change of changeset) {
|
||||
switch (change.type) {
|
||||
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;
|
||||
case ChangeType.Chord:
|
||||
overlay.chords.set(JSON.stringify(change.id), {
|
||||
@@ -71,7 +79,9 @@ export const overlay = derived(changes, (changes) => {
|
||||
});
|
||||
break;
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -82,21 +92,25 @@ export const overlay = derived(changes, (changes) => {
|
||||
|
||||
export const settings = derived(
|
||||
[overlay, deviceSettings],
|
||||
([overlay, settings]) =>
|
||||
settings.map<{ value: number } & ChangeInfo>((value, id) => ({
|
||||
value: overlay.settings.get(id) ?? value,
|
||||
isApplied: !overlay.settings.has(id),
|
||||
})),
|
||||
([overlay, profiles]) =>
|
||||
profiles.map((settings, profile) =>
|
||||
settings.map<{ value: number } & ChangeInfo>((value, id) => ({
|
||||
value: overlay.settings[profile]?.get(id) ?? value,
|
||||
isApplied: !overlay.settings[profile]?.has(id),
|
||||
})),
|
||||
),
|
||||
);
|
||||
|
||||
export type KeyInfo = { action: number } & ChangeInfo;
|
||||
export const layout = derived([overlay, deviceLayout], ([overlay, layout]) =>
|
||||
layout.map(
|
||||
(actions, layer) =>
|
||||
actions.map<KeyInfo>((action, id) => ({
|
||||
action: overlay.layout[layer]?.get(id) ?? action,
|
||||
isApplied: !overlay.layout[layer]?.has(id),
|
||||
})) as [KeyInfo, KeyInfo, KeyInfo],
|
||||
export const layout = derived([overlay, deviceLayout], ([overlay, profiles]) =>
|
||||
profiles.map((layout, profile) =>
|
||||
layout.map(
|
||||
(actions, layer) =>
|
||||
actions.map<KeyInfo>((action, id) => ({
|
||||
action: overlay.layout[profile]?.[layer]?.get(id) ?? action,
|
||||
isApplied: !overlay.layout[profile]?.[layer]?.has(id),
|
||||
})) as [KeyInfo, KeyInfo, KeyInfo],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
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);
|
||||
}),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user