mirror of
https://github.com/CharaChorder/DeviceManager.git
synced 2026-01-29 21:32:40 +00:00
Compare commits
11 Commits
24fc861ef4
...
v2.3.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
bd1c6147fd
|
|||
|
891abda0fb
|
|||
|
3611f65e24
|
|||
|
f76882a09c
|
|||
|
ff7e4f7b2e
|
|||
|
1c1c86241f
|
|||
|
dc8b3c3d66
|
|||
|
|
65911419b0 | ||
|
|
ccfb09e261 | ||
|
b841469505
|
|||
|
bc06e8ee80
|
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@@ -4,6 +4,8 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
tags:
|
||||
- v*
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
|
||||
@@ -7,6 +7,8 @@ node_modules
|
||||
.env.*
|
||||
!.env.example
|
||||
/src-tauri/target
|
||||
/openssl*
|
||||
/src/i18n/i18n*
|
||||
|
||||
# Ignore files for PNPM, NPM and YARN
|
||||
pnpm-lock.yaml
|
||||
|
||||
@@ -40,4 +40,4 @@ To generate the icons use the following command:
|
||||
|
||||
```shell
|
||||
npm run minify-icons
|
||||
```
|
||||
```
|
||||
|
||||
@@ -80,6 +80,9 @@ const config = {
|
||||
"delete",
|
||||
"remove_selection",
|
||||
"bolt",
|
||||
"thunderstorm",
|
||||
"join_inner",
|
||||
"uppercase",
|
||||
"undo",
|
||||
"redo",
|
||||
"replay",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "charachorder-device-manager",
|
||||
"version": "2.2.3",
|
||||
"version": "2.3.0",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"private": true,
|
||||
"engines": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "app"
|
||||
version = "2.2.3"
|
||||
version = "2.3.0"
|
||||
description = "A Tauri App"
|
||||
authors = ["Thea Schöbl <dev@theaninova.de>"]
|
||||
license = "AGPL-3"
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"devPath": "http://localhost:5173",
|
||||
"distDir": "../build"
|
||||
},
|
||||
"package": { "productName": "amacc1ng", "version": "2.2.3" },
|
||||
"package": { "productName": "amacc1ng", "version": "2.3.0" },
|
||||
"tauri": {
|
||||
"allowlist": { "all": false },
|
||||
"bundle": {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import { expoIn, expoOut } from "svelte/easing";
|
||||
import type { Snippet } from "svelte";
|
||||
|
||||
let { children, routeOrder }: { children: Snippet; routeOrder: string[]; direction: } =
|
||||
let { children, routeOrder }: { children: Snippet; routeOrder: string[] } =
|
||||
$props();
|
||||
|
||||
let inDirection = $state(0);
|
||||
|
||||
@@ -177,7 +177,6 @@
|
||||
<style lang="scss">
|
||||
$border-radius: 16px;
|
||||
|
||||
|
||||
.input {
|
||||
border: 1px solid var(--md-sys-color-outline);
|
||||
flex-grow: 1;
|
||||
@@ -220,7 +219,6 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -84,7 +84,6 @@
|
||||
border-right-width: 3px;
|
||||
}
|
||||
|
||||
|
||||
.inline-kbd {
|
||||
margin-inline-end: 2px;
|
||||
}
|
||||
|
||||
@@ -73,8 +73,9 @@
|
||||
font-stretch: 62.5% 100%;
|
||||
src: url("@fontsource-variable/noto-sans-mono/files/noto-sans-mono-latin-ext-wght-normal.woff2")
|
||||
format("woff2-variations");
|
||||
unicode-range: U+0100-02AF, U+0300-0301, U+0303-0304, U+0308-0309, U+0323,
|
||||
U+0329, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F,
|
||||
unicode-range:
|
||||
U+0100-02AF, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329,
|
||||
U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F,
|
||||
U+A720-A7FF;
|
||||
}
|
||||
|
||||
@@ -87,7 +88,8 @@
|
||||
font-stretch: 62.5% 100%;
|
||||
src: url("@fontsource-variable/noto-sans-mono/files/noto-sans-mono-latin-wght-normal.woff2")
|
||||
format("woff2-variations");
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
|
||||
U+02DC, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+2000-206F,
|
||||
U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
unicode-range:
|
||||
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC,
|
||||
U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+2000-206F, U+2074,
|
||||
U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ export async function getMeta(
|
||||
try {
|
||||
if (!browser) return fetchMeta(device, version, fetch);
|
||||
|
||||
const dbRequest = indexedDB.open("version-meta", 3);
|
||||
const dbRequest = indexedDB.open("version-meta", 4);
|
||||
const db = await new Promise<IDBDatabase>((resolve, reject) => {
|
||||
dbRequest.onsuccess = () => resolve(dbRequest.result);
|
||||
dbRequest.onerror = () => reject(dbRequest.error);
|
||||
@@ -120,6 +120,9 @@ async function fetchMeta(
|
||||
}
|
||||
return settings;
|
||||
})),
|
||||
changelog: await (meta?.changelog
|
||||
? fetch(`${path}/${meta.changelog}`).then((it) => it.json())
|
||||
: {}),
|
||||
actions: await (meta?.actions
|
||||
? fetch(`${path}/${meta.actions}`).then((it) => it.json())
|
||||
: Promise.all<KeymapCategory[]>(
|
||||
|
||||
@@ -22,6 +22,16 @@ export interface SettingsItemMeta {
|
||||
scale?: number;
|
||||
}
|
||||
|
||||
export interface ChangelogEntry {
|
||||
summary: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface Changelog {
|
||||
features: ChangelogEntry[];
|
||||
fixes: ChangelogEntry[];
|
||||
}
|
||||
|
||||
export interface RawVersionMeta {
|
||||
version: string;
|
||||
target: string;
|
||||
@@ -32,6 +42,7 @@ export interface RawVersionMeta {
|
||||
development_mode: number;
|
||||
actions: string;
|
||||
settings: string;
|
||||
changelog: string;
|
||||
factory_defaults: {
|
||||
layout: string;
|
||||
settings: string;
|
||||
@@ -57,6 +68,7 @@ export interface VersionMeta {
|
||||
developmentBuild: boolean;
|
||||
actions: KeymapCategory[];
|
||||
settings: SettingsMeta[];
|
||||
changelog: Changelog;
|
||||
factoryDefaults?: {
|
||||
layout: CharaLayoutFile;
|
||||
settings: CharaSettingsFile;
|
||||
|
||||
@@ -13,12 +13,13 @@ import { showConnectionFailedDialog } from "$lib/dialogs/connection-failed-dialo
|
||||
|
||||
const PORT_FILTERS: Map<string, SerialPortFilter> = new Map([
|
||||
["ONE M0", { usbProductId: 32783, usbVendorId: 9114 }],
|
||||
["TWO S3 (pre-production)", { usbProductId: 0x8252, usbVendorId: 0x303a }], // TODO: remove this after everyone has migrated
|
||||
["TWO S3 (pre-production)", { usbProductId: 0x8252, usbVendorId: 0x303a }],
|
||||
["TWO S3", { usbProductId: 0x8253, usbVendorId: 0x303a }],
|
||||
["LITE S2", { usbProductId: 33070, usbVendorId: 12346 }],
|
||||
["LITE S2", { usbProductId: 0x812e, usbVendorId: 0x303a }],
|
||||
["LITE M0", { usbProductId: 32796, usbVendorId: 9114 }],
|
||||
["X", { usbProductId: 33163, usbVendorId: 12346 }],
|
||||
["M4G S3", { usbProductId: 4097, usbVendorId: 12346 }],
|
||||
["X", { usbProductId: 0x818b, usbVendorId: 0x303a }],
|
||||
["M4G S3 (pre-production)", { usbProductId: 0x1001, usbVendorId: 0x303a }],
|
||||
["M4G S3", { usbProductId: 0x829a, usbVendorId: 0x303a }],
|
||||
]);
|
||||
|
||||
const KEY_COUNTS = {
|
||||
|
||||
@@ -1,6 +1,85 @@
|
||||
import type { Action } from "svelte/action";
|
||||
import { changes, ChangeType, settings } from "$lib/undo-redo";
|
||||
|
||||
/**
|
||||
* https://gist.github.com/mjackson/5311256
|
||||
*/
|
||||
function rgbToHsv(r: number, g: number, b: number): [number, number, number] {
|
||||
r /= 255;
|
||||
g /= 255;
|
||||
b /= 255;
|
||||
|
||||
const max = Math.max(r, g, b);
|
||||
const min = Math.min(r, g, b);
|
||||
let h = 0;
|
||||
const v = max;
|
||||
|
||||
const d = max - min;
|
||||
const s = max == 0 ? 0 : d / max;
|
||||
|
||||
if (max == min) {
|
||||
h = 0; // achromatic
|
||||
} else {
|
||||
switch (max) {
|
||||
case r:
|
||||
h = (g - b) / d + (g < b ? 6 : 0);
|
||||
break;
|
||||
case g:
|
||||
h = (b - r) / d + 2;
|
||||
break;
|
||||
case b:
|
||||
h = (r - g) / d + 4;
|
||||
break;
|
||||
}
|
||||
|
||||
h /= 6;
|
||||
}
|
||||
|
||||
return [Math.floor(h * 0xffff), Math.floor(s * 0xff), Math.floor(v * 0xff)];
|
||||
}
|
||||
|
||||
/**
|
||||
* https://gist.github.com/mjackson/5311256
|
||||
*/
|
||||
function hsvToRgb(h: number, s: number, v: number): [number, number, number] {
|
||||
h /= 0xffff;
|
||||
s /= 0xff;
|
||||
v /= 0xff;
|
||||
|
||||
let r = 0;
|
||||
let g = 0;
|
||||
let b = 0;
|
||||
|
||||
const i = Math.floor(h * 6);
|
||||
const f = h * 6 - i;
|
||||
const p = v * (1 - s);
|
||||
const q = v * (1 - f * s);
|
||||
const t = v * (1 - (1 - f) * s);
|
||||
|
||||
switch (i % 6) {
|
||||
case 0:
|
||||
(r = v), (g = t), (b = p);
|
||||
break;
|
||||
case 1:
|
||||
(r = q), (g = v), (b = p);
|
||||
break;
|
||||
case 2:
|
||||
(r = p), (g = v), (b = t);
|
||||
break;
|
||||
case 3:
|
||||
(r = p), (g = q), (b = v);
|
||||
break;
|
||||
case 4:
|
||||
(r = t), (g = p), (b = v);
|
||||
break;
|
||||
case 5:
|
||||
(r = v), (g = p), (b = q);
|
||||
break;
|
||||
}
|
||||
|
||||
return [Math.floor(r * 0xff), Math.floor(g * 0xff), Math.floor(b * 0xff)];
|
||||
}
|
||||
|
||||
export const setting: Action<
|
||||
HTMLInputElement | HTMLSelectElement,
|
||||
{ id: number; inverse?: number; scale?: number }
|
||||
@@ -9,7 +88,12 @@ export const setting: Action<
|
||||
{ id, inverse, scale },
|
||||
) {
|
||||
node.setAttribute("disabled", "");
|
||||
const type = node.getAttribute("type") as "number" | "checkbox" | "range";
|
||||
const type = node.getAttribute("type") as
|
||||
| "number"
|
||||
| "checkbox"
|
||||
| "range"
|
||||
| "color";
|
||||
const isColor = type === "color";
|
||||
const isNumeric =
|
||||
type === "number" || type === "range" || node instanceof HTMLSelectElement;
|
||||
const min = node.hasAttribute("min")
|
||||
@@ -30,6 +114,13 @@ export const setting: Action<
|
||||
? 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;
|
||||
}
|
||||
@@ -58,6 +149,22 @@ export const setting: Action<
|
||||
? value / scale
|
||||
: value,
|
||||
);
|
||||
} else if (isColor) {
|
||||
const r = parseInt(node.value.slice(1, 3), 16);
|
||||
const g = parseInt(node.value.slice(3, 5), 16);
|
||||
const b = parseInt(node.value.slice(5, 7), 16);
|
||||
const hsv = rgbToHsv(r, g, b);
|
||||
changes.update((changes) => {
|
||||
changes.push(
|
||||
hsv.map((value, i) => ({
|
||||
type: ChangeType.Setting,
|
||||
id: id + i,
|
||||
setting: value,
|
||||
})),
|
||||
);
|
||||
return changes;
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
value = node.checked ? 1 : 0;
|
||||
}
|
||||
|
||||
@@ -5,23 +5,24 @@
|
||||
*
|
||||
* I could use _.debounce(), but bringing dependency on lodash didn't feel
|
||||
* justified yet.
|
||||
*
|
||||
*
|
||||
* @param func The function to debounce
|
||||
* @param wait The number of milliseconds to delay execution
|
||||
* @returns A debounced version of the provided function
|
||||
*/
|
||||
function debounce<T extends (...args: any[]) => void>(
|
||||
func: T,
|
||||
wait: number
|
||||
wait: number,
|
||||
): T & { cancel: () => void } {
|
||||
let timeout: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
const debounced = function(
|
||||
this: ThisParameterType<T>, ...args: Parameters<T>
|
||||
const debounced = function (
|
||||
this: ThisParameterType<T>,
|
||||
...args: Parameters<T>
|
||||
): void {
|
||||
const context = this;
|
||||
|
||||
const later = function() {
|
||||
const later = function () {
|
||||
timeout = null;
|
||||
func.apply(context, args);
|
||||
};
|
||||
@@ -33,7 +34,7 @@ function debounce<T extends (...args: any[]) => void>(
|
||||
timeout = setTimeout(later, wait);
|
||||
};
|
||||
|
||||
debounced.cancel = function() {
|
||||
debounced.cancel = function () {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
timeout = null;
|
||||
@@ -43,4 +44,4 @@ function debounce<T extends (...args: any[]) => void>(
|
||||
return debounced as T & { cancel: () => void };
|
||||
}
|
||||
|
||||
export default debounce;
|
||||
export default debounce;
|
||||
|
||||
@@ -173,7 +173,6 @@
|
||||
</footer>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
.sync-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -287,7 +287,7 @@
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
{#if data.meta.update.esptool}
|
||||
{#if false && data.meta.update.esptool}
|
||||
<section>
|
||||
<h3>Factory Flash (WIP)</h3>
|
||||
<p>
|
||||
@@ -319,11 +319,56 @@
|
||||
</section>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<section class="changelog">
|
||||
<h2>Changelog</h2>
|
||||
{#if data.meta.changelog.features}
|
||||
<h3>Features</h3>
|
||||
<ul>
|
||||
{#each data.meta.changelog.features as feature}
|
||||
<li>
|
||||
<b>{@html feature.summary}</b>
|
||||
{@html feature.description}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
{#if data.meta.changelog.fixes}
|
||||
<h3>Fixes</h3>
|
||||
<ul>
|
||||
{#each data.meta.changelog.fixes as fix}
|
||||
<li>
|
||||
<b>{@html fix.summary}</b>
|
||||
{@html fix.description}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
h3 {
|
||||
margin-block-start: 4em;
|
||||
.changelog:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.changelog ul {
|
||||
list-style: none;
|
||||
padding-inline-start: 0em;
|
||||
}
|
||||
|
||||
.changelog li {
|
||||
margin-block: 0.2em;
|
||||
padding: 0.5em 1em;
|
||||
}
|
||||
|
||||
.changelog b {
|
||||
display: inline-block;
|
||||
color: var(--md-sys-color-on-tertiary-container);
|
||||
background: var(--md-sys-color-tertiary-container);
|
||||
padding: 0.2em 0.5em;
|
||||
border-radius: 8px;
|
||||
translate: -0.5em -0.2em;
|
||||
}
|
||||
|
||||
pre {
|
||||
|
||||
@@ -165,7 +165,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.timeline {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
@@ -45,7 +45,6 @@
|
||||
padding-inline: 16px;
|
||||
}
|
||||
|
||||
|
||||
.icon {
|
||||
cursor: pointer;
|
||||
|
||||
|
||||
@@ -131,7 +131,7 @@
|
||||
codes: Map<number, KeyInfo>,
|
||||
): Promise<FlexSearch.Index> {
|
||||
if (chords.length === 0 || !browser) return index;
|
||||
|
||||
|
||||
index = new FlexSearch.Index({
|
||||
tokenize: "full",
|
||||
encode(phrase: string) {
|
||||
@@ -149,36 +149,36 @@
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
let abort = false;
|
||||
abortIndexing = () => {
|
||||
abort = true;
|
||||
};
|
||||
|
||||
|
||||
const batchSize = 200;
|
||||
const batches = Math.ceil(chords.length / batchSize);
|
||||
|
||||
|
||||
for (let b = 0; b < batches; b++) {
|
||||
if (abort) return index;
|
||||
|
||||
|
||||
const start = b * batchSize;
|
||||
const end = Math.min((b + 1) * batchSize, chords.length);
|
||||
const batch = chords.slice(start, end);
|
||||
|
||||
|
||||
const promises = batch.map((chord, i) => {
|
||||
const chordIndex = start + i;
|
||||
progress = chordIndex + 1;
|
||||
|
||||
|
||||
if ("phrase" in chord) {
|
||||
const encodedChord = encodeChord(chord, osLayout, codes);
|
||||
return index.addAsync(chordIndex, encodedChord);
|
||||
}
|
||||
return Promise.resolve();
|
||||
});
|
||||
|
||||
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
|
||||
@@ -40,163 +40,155 @@
|
||||
</svelte:head>
|
||||
|
||||
<section>
|
||||
<fieldset>
|
||||
<legend>{$LL.backup.TITLE()}</legend>
|
||||
<label
|
||||
><input
|
||||
type="checkbox"
|
||||
use:preference={"backup"}
|
||||
/>{$LL.backup.AUTO_BACKUP()}</label
|
||||
>
|
||||
<p class="disclaimer">
|
||||
{$LL.backup.DISCLAIMER()}
|
||||
</p>
|
||||
<div class="row" style="margin-top: auto">
|
||||
<button onclick={() => downloadFile(createChordBackup())}>
|
||||
<span class="icon">piano</span>
|
||||
{$LL.configure.chords.TITLE()}
|
||||
</button>
|
||||
<button onclick={() => downloadFile(createLayoutBackup())}>
|
||||
<span class="icon">keyboard</span>
|
||||
{$LL.configure.layout.TITLE()}
|
||||
</button>
|
||||
<button onclick={() => downloadFile(createSettingsBackup())}>
|
||||
<span class="icon">settings</span>
|
||||
Settings
|
||||
</button>
|
||||
</div>
|
||||
<div class="row">
|
||||
<button class="primary" onclick={downloadBackup}
|
||||
><span class="icon">download</span>{$LL.backup.DOWNLOAD()}</button
|
||||
>
|
||||
<label class="button"
|
||||
><input oninput={restoreBackup} type="file" /><span class="icon"
|
||||
>settings_backup_restore</span
|
||||
>{$LL.backup.RESTORE()}</label
|
||||
>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>Device</legend>
|
||||
<nav>
|
||||
{#if $deviceMeta}
|
||||
{#each $deviceMeta?.settings as category}
|
||||
<a href={`#${category.name}`}>{titlecase(category.name)}</a>
|
||||
{/each}
|
||||
{/if}
|
||||
<a href="#backup">Backup</a>
|
||||
</nav>
|
||||
<div class="content">
|
||||
<label
|
||||
>{$LL.deviceManager.AUTO_CONNECT()}<input
|
||||
type="checkbox"
|
||||
use:preference={"autoConnect"}
|
||||
/></label
|
||||
>
|
||||
{#if $serialPort}
|
||||
{#if $deviceMeta?.factoryDefaults?.settings}
|
||||
<button
|
||||
use:action={{ title: "Reset Settings" }}
|
||||
transition:fly={{ x: -8 }}
|
||||
onclick={() => restoreFromFile($deviceMeta.factoryDefaults!.settings)}
|
||||
><span class="icon">reset_settings</span>Reset Settings</button
|
||||
>
|
||||
{/if}
|
||||
<button class="outline" use:popup={ResetPopup}>Recovery...</button>
|
||||
{/if}
|
||||
</fieldset>
|
||||
{#if $deviceMeta}
|
||||
{#each $deviceMeta.settings as category}
|
||||
<fieldset>
|
||||
<legend>
|
||||
{#if category.items[0]?.name === "enable"}
|
||||
<label
|
||||
><input
|
||||
type="checkbox"
|
||||
use:setting={{ id: category.items[0].id }}
|
||||
/>{titlecase(category.name)}</label
|
||||
>
|
||||
{:else}
|
||||
{#if $deviceMeta}
|
||||
{#each $deviceMeta.settings as category}
|
||||
<fieldset id={category.name}>
|
||||
<legend>
|
||||
{titlecase(category.name)}
|
||||
</legend>
|
||||
{#if category.description}
|
||||
<p>{category.description}</p>
|
||||
{/if}
|
||||
</legend>
|
||||
{#if category.description}
|
||||
<p>{category.description}</p>
|
||||
{#each category.items as item}
|
||||
{#if item.unit === "H"}
|
||||
<label
|
||||
><input type="color" use:setting={{ id: item.id }} /> Color</label
|
||||
>
|
||||
{:else if item.unit !== "S" && item.unit !== "B"}
|
||||
<label class:enable-item={item.name === "enable"}
|
||||
>{#if item.enum}
|
||||
<select class="value" use:setting={{ id: item.id }}>
|
||||
{#each item.enum as name, value}
|
||||
<option {value}>{titlecase(name)}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{:else if item.range[0] === 0 && item.range[1] === 1}
|
||||
<input
|
||||
class="value"
|
||||
type="checkbox"
|
||||
use:setting={{ id: item.id }}
|
||||
/>
|
||||
{:else}
|
||||
<div class="value unit">
|
||||
<input
|
||||
type="number"
|
||||
min={settingValue(item.range[0], item)}
|
||||
max={settingValue(item.range[1], item)}
|
||||
step={item.inverse !== undefined ||
|
||||
item.scale !== undefined ||
|
||||
item.step === undefined
|
||||
? undefined
|
||||
: settingValue(item.step, item)}
|
||||
use:setting={{
|
||||
id: item.id,
|
||||
inverse: item.inverse,
|
||||
scale: item.scale,
|
||||
}}
|
||||
/>{item.unit}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="title">{titlecase(item.name)}</div>
|
||||
{#if item.description}
|
||||
<div class="description">{item.description}</div>
|
||||
{/if}
|
||||
</label>
|
||||
{/if}
|
||||
{/each}
|
||||
</fieldset>
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
<fieldset id="backup">
|
||||
<legend>{$LL.backup.TITLE()}</legend>
|
||||
<label
|
||||
><input
|
||||
type="checkbox"
|
||||
use:preference={"backup"}
|
||||
/>{$LL.backup.AUTO_BACKUP()}</label
|
||||
>
|
||||
<p class="disclaimer">
|
||||
{$LL.backup.DISCLAIMER()}
|
||||
</p>
|
||||
<div class="row" style="margin-top: auto">
|
||||
<button onclick={() => downloadFile(createChordBackup())}>
|
||||
<span class="icon">piano</span>
|
||||
{$LL.configure.chords.TITLE()}
|
||||
</button>
|
||||
<button onclick={() => downloadFile(createLayoutBackup())}>
|
||||
<span class="icon">keyboard</span>
|
||||
{$LL.configure.layout.TITLE()}
|
||||
</button>
|
||||
<button onclick={() => downloadFile(createSettingsBackup())}>
|
||||
<span class="icon">settings</span>
|
||||
Settings
|
||||
</button>
|
||||
</div>
|
||||
<div class="row">
|
||||
<button class="primary" onclick={downloadBackup}
|
||||
><span class="icon">download</span>{$LL.backup.DOWNLOAD()}</button
|
||||
>
|
||||
<label class="button"
|
||||
><input oninput={restoreBackup} type="file" /><span class="icon"
|
||||
>settings_backup_restore</span
|
||||
>{$LL.backup.RESTORE()}</label
|
||||
>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<div class="footer">
|
||||
{#if $serialPort}
|
||||
{#if $deviceMeta?.factoryDefaults?.settings}
|
||||
<button
|
||||
use:action={{ title: "Reset Settings" }}
|
||||
transition:fly={{ x: -8 }}
|
||||
onclick={() =>
|
||||
restoreFromFile($deviceMeta.factoryDefaults!.settings)}
|
||||
><span class="icon">reset_settings</span>Reset Settings</button
|
||||
>
|
||||
{/if}
|
||||
{#each category.items as item}
|
||||
{#if item.name !== "enable"}
|
||||
<label
|
||||
>{#if item.enum}
|
||||
<select use:setting={{ id: item.id }}>
|
||||
{#each item.enum as name, value}
|
||||
<option {value}>{titlecase(name)}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{:else if item.range[0] === 0 && item.range[1] === 1}
|
||||
<input type="checkbox" use:setting={{ id: item.id }} />
|
||||
{:else}
|
||||
<span class="unit"
|
||||
><input
|
||||
type="number"
|
||||
min={settingValue(item.range[0], item)}
|
||||
max={settingValue(item.range[1], item)}
|
||||
step={item.inverse !== undefined ||
|
||||
item.scale !== undefined ||
|
||||
item.step === undefined
|
||||
? undefined
|
||||
: settingValue(item.step, item)}
|
||||
use:setting={{
|
||||
id: item.id,
|
||||
inverse: item.inverse,
|
||||
scale: item.scale,
|
||||
}}
|
||||
/>{item.unit}</span
|
||||
>
|
||||
{/if}
|
||||
{#if item.description}
|
||||
<span
|
||||
>{titlecase(item.name)}
|
||||
<p>{item.description}</p></span
|
||||
>
|
||||
{:else}
|
||||
{titlecase(item.name)}
|
||||
{/if}
|
||||
</label>
|
||||
{/if}
|
||||
{/each}
|
||||
</fieldset>
|
||||
{/each}
|
||||
{/if}
|
||||
<button use:popup={ResetPopup}>Recovery...</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style lang="scss">
|
||||
section {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.content {
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
gap: 16px;
|
||||
justify-content: center;
|
||||
|
||||
margin-block: auto;
|
||||
padding-block-end: 48px;
|
||||
scroll-behavior: smooth;
|
||||
max-width: 20cm;
|
||||
}
|
||||
|
||||
button.outline {
|
||||
border: 1px solid currentcolor;
|
||||
border-radius: 8px;
|
||||
height: 2em;
|
||||
margin-block: 2em;
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
legend,
|
||||
legend > label {
|
||||
font-size: 24px;
|
||||
legend {
|
||||
color: var(--md-sys-color-primary);
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
position: relative;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
legend:has(label) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
legend:not(:has(label)) {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
font-size: 12px !important;
|
||||
}
|
||||
@@ -204,22 +196,28 @@
|
||||
fieldset {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
|
||||
max-width: 400px;
|
||||
border: 1px solid var(--md-sys-color-outline);
|
||||
border-radius: 24px;
|
||||
width: 100%;
|
||||
margin-inline: 0;
|
||||
border: none;
|
||||
margin-block-end: 32px;
|
||||
|
||||
/*&:has(> legend input:not(:checked)) > :not(legend) {
|
||||
pointer-events: none;
|
||||
opacity: 0.7;
|
||||
}*/
|
||||
> p {
|
||||
padding-inline-start: 16px;
|
||||
}
|
||||
|
||||
> label {
|
||||
appearance: none;
|
||||
position: relative;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
justify-content: flex-start;
|
||||
height: auto;
|
||||
font-weight: normal;
|
||||
padding: 8px;
|
||||
width: fit-content;
|
||||
|
||||
margin-block: 4px;
|
||||
|
||||
@@ -232,6 +230,26 @@
|
||||
filter: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.enable-item {
|
||||
background: var(--md-sys-color-surface-variant);
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
margin-inline-start: 8px;
|
||||
padding-inline-end: 16px;
|
||||
padding-inline-start: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-inline-start: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.description {
|
||||
width: 100%;
|
||||
font-size: 12px;
|
||||
white-space: normal;
|
||||
text-wrap: wrap;
|
||||
}
|
||||
|
||||
.unit {
|
||||
@@ -279,6 +297,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
appearance: none;
|
||||
background: var(--md-sys-color-secondary);
|
||||
border: none;
|
||||
padding: 4px 8px;
|
||||
border-radius: 8px;
|
||||
font: inherit;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
// stylelint-disable-next-line
|
||||
label:global(:has(.pending-changes)) {
|
||||
color: var(--md-sys-color-primary);
|
||||
@@ -296,9 +324,15 @@
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
margin-block: 8px;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -114,7 +114,6 @@
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
|
||||
section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -10,7 +10,10 @@ export const SENTENCE_TRAINER_PAGE_PARAMS: {
|
||||
showDevTools: PageParam<boolean>;
|
||||
textAreaDebounceInMillis: PageParam<number>;
|
||||
} = {
|
||||
sentence: { key: "sentence", default: "This text has been typed at the speed of thought" },
|
||||
sentence: {
|
||||
key: "sentence",
|
||||
default: "This text has been typed at the speed of thought",
|
||||
},
|
||||
wpm: {
|
||||
key: "wpm",
|
||||
default: 250,
|
||||
|
||||
Reference in New Issue
Block a user