mirror of
https://github.com/CharaChorder/DeviceManager.git
synced 2026-02-14 13:12:41 +00:00
feat: multi-purpose site
feat: editor feat: plugin editor
This commit is contained in:
12
src/routes/(app)/config/+layout.svelte
Normal file
12
src/routes/(app)/config/+layout.svelte
Normal file
@@ -0,0 +1,12 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from "svelte";
|
||||
import Navigation from "./Navigation.svelte";
|
||||
|
||||
let { children }: { children?: Snippet } = $props();
|
||||
</script>
|
||||
|
||||
<Navigation />
|
||||
|
||||
{#if children}
|
||||
{@render children()}
|
||||
{/if}
|
||||
60
src/routes/(app)/config/ConfigTabs.svelte
Normal file
60
src/routes/(app)/config/ConfigTabs.svelte
Normal file
@@ -0,0 +1,60 @@
|
||||
<script lang="ts">
|
||||
import { page } from "$app/stores";
|
||||
import LL from "$i18n/i18n-svelte";
|
||||
import type { Snippet } from "svelte";
|
||||
|
||||
let { children }: { children?: Snippet } = $props();
|
||||
|
||||
let paths = $derived([
|
||||
{
|
||||
href: "/config/chords/",
|
||||
title: $LL.configure.chords.TITLE(),
|
||||
icon: "piano",
|
||||
},
|
||||
{
|
||||
href: "/config/layout/",
|
||||
title: $LL.configure.layout.TITLE(),
|
||||
icon: "keyboard",
|
||||
},
|
||||
{
|
||||
href: "/config/settings/",
|
||||
title: $LL.configure.settings.TITLE(),
|
||||
icon: "settings",
|
||||
},
|
||||
]);
|
||||
</script>
|
||||
|
||||
<nav>
|
||||
{#each paths as { href, title, icon }}
|
||||
<a {href} class:active={$page.url.pathname.startsWith(href)}>
|
||||
<span class="icon">{icon}</span>
|
||||
{title}
|
||||
</a>
|
||||
{/each}
|
||||
</nav>
|
||||
|
||||
{#if children}
|
||||
{@render children()}
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
nav {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
|
||||
padding: 8px;
|
||||
|
||||
color: var(--md-sys-color-on-surface-variant);
|
||||
|
||||
background: var(--md-sys-color-surface-variant);
|
||||
border: none;
|
||||
border-radius: 32px;
|
||||
}
|
||||
|
||||
a.active {
|
||||
--icon-fill: 1;
|
||||
|
||||
color: var(--md-sys-color-on-primary);
|
||||
background: var(--md-sys-color-primary);
|
||||
}
|
||||
</style>
|
||||
177
src/routes/(app)/config/EditActions.svelte
Normal file
177
src/routes/(app)/config/EditActions.svelte
Normal file
@@ -0,0 +1,177 @@
|
||||
<script lang="ts">
|
||||
import LL from "$i18n/i18n-svelte";
|
||||
import {
|
||||
changes,
|
||||
ChangeType,
|
||||
chords,
|
||||
layout,
|
||||
overlay,
|
||||
settings,
|
||||
} from "$lib/undo-redo";
|
||||
import type { Change } from "$lib/undo-redo";
|
||||
import { fly } from "svelte/transition";
|
||||
import { action } from "$lib/title";
|
||||
import {
|
||||
deviceChords,
|
||||
deviceLayout,
|
||||
deviceSettings,
|
||||
serialPort,
|
||||
syncProgress,
|
||||
syncStatus,
|
||||
} from "$lib/serial/connection";
|
||||
import { askForConfirmation } from "$lib/dialogs/confirm-dialog";
|
||||
|
||||
function undo(event: MouseEvent) {
|
||||
if (event.shiftKey) {
|
||||
changes.set([]);
|
||||
} else {
|
||||
redoQueue.unshift($changes.pop()!);
|
||||
changes.update((it) => it);
|
||||
}
|
||||
}
|
||||
|
||||
function redo() {
|
||||
const change = redoQueue.shift();
|
||||
if (change) {
|
||||
changes.update((it) => {
|
||||
it.push(change);
|
||||
return it;
|
||||
});
|
||||
}
|
||||
}
|
||||
let redoQueue: Change[] = $state([]);
|
||||
|
||||
async function save() {
|
||||
try {
|
||||
const port = $serialPort;
|
||||
if (!port) return;
|
||||
$syncStatus = "uploading";
|
||||
|
||||
for (const [id, { actions, phrase, deleted }] of $overlay.chords) {
|
||||
if (!deleted) {
|
||||
if (id !== JSON.stringify(actions)) {
|
||||
const existingChord = await port.getChordPhrase(actions);
|
||||
if (
|
||||
existingChord !== undefined &&
|
||||
!(await askForConfirmation(
|
||||
$LL.configure.chords.conflict.TITLE(),
|
||||
$LL.configure.chords.conflict.DESCRIPTION(),
|
||||
$LL.configure.chords.conflict.CONFIRM(),
|
||||
$LL.configure.chords.conflict.ABORT(),
|
||||
actions.slice(0, actions.lastIndexOf(0)),
|
||||
))
|
||||
) {
|
||||
changes.update((changes) =>
|
||||
changes.filter(
|
||||
(it) =>
|
||||
!(
|
||||
it.type === ChangeType.Chord &&
|
||||
JSON.stringify(it.id) === id
|
||||
),
|
||||
),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
await port.deleteChord({ actions: JSON.parse(id) });
|
||||
}
|
||||
await port.setChord({ actions, phrase });
|
||||
} else {
|
||||
await port.deleteChord({ actions });
|
||||
}
|
||||
}
|
||||
|
||||
for (const [layer, actions] of $overlay.layout.entries()) {
|
||||
for (const [id, action] of actions) {
|
||||
await port.setLayoutKey(layer + 1, id, action);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [id, setting] of $overlay.settings) {
|
||||
await port.setSetting(id, setting);
|
||||
}
|
||||
|
||||
// Yes, this is a completely arbitrary and unnecessary delay.
|
||||
// The only purpose of it is to create a sense of weight,
|
||||
// aka make it more "energy intensive" to click.
|
||||
// The only conceivable way users could reach the commit limit in this case
|
||||
// would be if they click it every time they change a setting.
|
||||
// Because of that, we don't need to show a fearmongering message such as
|
||||
// "Your device will break after you click this 10,000 times!"
|
||||
const virtualWriteTime = 1000;
|
||||
const startStamp = performance.now();
|
||||
await new Promise<void>((resolve) => {
|
||||
function animate() {
|
||||
const delta = performance.now() - startStamp;
|
||||
syncProgress.set({
|
||||
max: virtualWriteTime,
|
||||
current: delta,
|
||||
});
|
||||
if (delta >= virtualWriteTime) {
|
||||
resolve();
|
||||
} else {
|
||||
requestAnimationFrame(animate);
|
||||
}
|
||||
}
|
||||
requestAnimationFrame(animate);
|
||||
});
|
||||
await port.commit();
|
||||
|
||||
$deviceLayout = $layout.map((layer) =>
|
||||
layer.map<number>(({ action }) => action),
|
||||
) as [number[], number[], number[]];
|
||||
$deviceChords = $chords
|
||||
.filter(({ deleted }) => !deleted)
|
||||
.map(({ actions, phrase }) => ({ actions, phrase }));
|
||||
$deviceSettings = $settings.map(({ value }) => value);
|
||||
$changes = [];
|
||||
} catch (e) {
|
||||
alert(e);
|
||||
console.error(e);
|
||||
} finally {
|
||||
$syncStatus = "done";
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<button
|
||||
use:action={{ title: $LL.saveActions.UNDO(), shortcut: "ctrl+z" }}
|
||||
class="icon"
|
||||
disabled={$changes.length === 0}
|
||||
onclick={undo}>undo</button
|
||||
>
|
||||
<button
|
||||
use:action={{ title: $LL.saveActions.REDO(), shortcut: "ctrl+y" }}
|
||||
class="icon"
|
||||
disabled={redoQueue.length === 0}
|
||||
onclick={redo}>redo</button
|
||||
>
|
||||
{#if $changes.length !== 0}
|
||||
<button
|
||||
transition:fly={{ x: 10 }}
|
||||
use:action={{ title: $LL.saveActions.SAVE(), shortcut: "ctrl+shift+s" }}
|
||||
onclick={save}
|
||||
class="click-me"
|
||||
><span class="icon">save</span>{$LL.saveActions.SAVE()}</button
|
||||
>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.click-me {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: fit-content;
|
||||
margin-inline: 8px;
|
||||
padding-block: 2px;
|
||||
padding-inline-start: 8px;
|
||||
padding-inline-end: 12px;
|
||||
font-family: inherit;
|
||||
font-weight: bold;
|
||||
color: var(--md-sys-color-primary);
|
||||
border: 2px solid var(--md-sys-color-primary);
|
||||
border-radius: 18px;
|
||||
outline: 2px dashed var(--md-sys-color-primary);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
</style>
|
||||
103
src/routes/(app)/config/Navigation.svelte
Normal file
103
src/routes/(app)/config/Navigation.svelte
Normal file
@@ -0,0 +1,103 @@
|
||||
<script lang="ts">
|
||||
import { fly } from "svelte/transition";
|
||||
import { canShare, triggerShare } from "$lib/share";
|
||||
import { action } from "$lib/title";
|
||||
import LL from "$i18n/i18n-svelte";
|
||||
import ConfigTabs from "./ConfigTabs.svelte";
|
||||
import EditActions from "./EditActions.svelte";
|
||||
</script>
|
||||
|
||||
<nav>
|
||||
<div class="actions">
|
||||
<EditActions />
|
||||
</div>
|
||||
|
||||
<ConfigTabs />
|
||||
|
||||
<div class="actions">
|
||||
{#if $canShare}
|
||||
<button
|
||||
use:action={{ title: $LL.share.TITLE() }}
|
||||
transition:fly={{ x: -8 }}
|
||||
class="icon"
|
||||
onclick={triggerShare}>share</button
|
||||
>
|
||||
<button
|
||||
use:action={{ title: $LL.print.TITLE() }}
|
||||
transition:fly={{ x: -8 }}
|
||||
class="icon"
|
||||
onclick={() => print()}>print</button
|
||||
>
|
||||
{/if}
|
||||
{#if import.meta.env.TAURI_FAMILY === undefined}
|
||||
{#await import("$lib/components/PwaStatus.svelte") then { default: PwaStatus }}
|
||||
<PwaStatus />
|
||||
{/await}
|
||||
{/if}
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<style lang="scss">
|
||||
nav {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
|
||||
width: calc(min(100%, 28cm));
|
||||
margin-block: 8px;
|
||||
margin-inline: auto;
|
||||
padding-inline: 16px;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
margin-block: 0;
|
||||
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
color: var(--md-sys-color-primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.icon {
|
||||
cursor: pointer;
|
||||
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
aspect-ratio: 1;
|
||||
padding: 0;
|
||||
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
|
||||
transition: all 250ms ease;
|
||||
|
||||
&.error {
|
||||
color: var(--md-sys-color-on-error);
|
||||
background: var(--md-sys-color-error);
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&:last-child {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
:disabled {
|
||||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user