feat: multi-purpose site

feat: editor
feat: plugin editor
This commit is contained in:
2024-08-01 01:31:04 +02:00
parent b8b903c5e1
commit 8b2bfee099
15 changed files with 262 additions and 64 deletions

View 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}

View 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>

View 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>

View 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>