mirror of
https://github.com/CharaChorder/DeviceManager.git
synced 2025-12-12 22:06:18 +00:00
feat: multi-purpose site
feat: editor feat: plugin editor
This commit is contained in:
@@ -94,6 +94,12 @@ const config = {
|
|||||||
"description",
|
"description",
|
||||||
"add_circle",
|
"add_circle",
|
||||||
"refresh",
|
"refresh",
|
||||||
|
"tune",
|
||||||
|
"edit_document",
|
||||||
|
"chat",
|
||||||
|
"account_circle",
|
||||||
|
"experiment",
|
||||||
|
"code",
|
||||||
],
|
],
|
||||||
codePoints: {
|
codePoints: {
|
||||||
speed: "e9e4",
|
speed: "e9e4",
|
||||||
@@ -113,6 +119,7 @@ const config = {
|
|||||||
stat_minus_2: "e69c",
|
stat_minus_2: "e69c",
|
||||||
stat_2: "e699",
|
stat_2: "e699",
|
||||||
routine: "e20c",
|
routine: "e20c",
|
||||||
|
experiment: "e686",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -5,21 +5,21 @@
|
|||||||
import { ChordsReplayPlugin } from "./core/plugins/chords.js";
|
import { ChordsReplayPlugin } from "./core/plugins/chords.js";
|
||||||
import type { ReplayPlayer } from "./core/player.js";
|
import type { ReplayPlayer } from "./core/player.js";
|
||||||
|
|
||||||
const player: ReplayPlayer | undefined = getContext("replay");
|
const player: { player: ReplayPlayer | undefined } = getContext("replay");
|
||||||
|
|
||||||
let {
|
let {
|
||||||
chords = $bindable([]),
|
chords = $bindable([]),
|
||||||
count = 1,
|
count = 1,
|
||||||
}: {
|
}: {
|
||||||
chords: InferredChord[];
|
chords: InferredChord[];
|
||||||
count: number;
|
count?: number;
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
if (browser) {
|
if (browser) {
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (!player) return;
|
if (!player.player) return;
|
||||||
const tracker = new ChordsReplayPlugin();
|
const tracker = new ChordsReplayPlugin();
|
||||||
tracker.register(player);
|
tracker.register(player.player);
|
||||||
const unsubscribe = tracker.subscribe((value) => {
|
const unsubscribe = tracker.subscribe((value) => {
|
||||||
chords = value.slice(-count);
|
chords = value.slice(-count);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,14 +3,14 @@
|
|||||||
import { RollingWpmReplayPlugin } from "./core/plugins/rolling-wpm";
|
import { RollingWpmReplayPlugin } from "./core/plugins/rolling-wpm";
|
||||||
import type { ReplayPlayer } from "./core/player";
|
import type { ReplayPlayer } from "./core/player";
|
||||||
|
|
||||||
const player: ReplayPlayer | undefined = getContext("replay");
|
const player: { player: ReplayPlayer | undefined } = getContext("replay");
|
||||||
|
|
||||||
let { wpm = $bindable(0) } = $props();
|
let { wpm = $bindable(0) } = $props();
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (!player) return;
|
if (!player.player) return;
|
||||||
const tracker = new RollingWpmReplayPlugin();
|
const tracker = new RollingWpmReplayPlugin();
|
||||||
tracker.register(player);
|
tracker.register(player.player);
|
||||||
const unsubscribe = tracker.subscribe((value) => {
|
const unsubscribe = tracker.subscribe((value) => {
|
||||||
wpm = value;
|
wpm = value;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export const popup: Action<HTMLButtonElement, Component> = (
|
|||||||
let target: HTMLElement | undefined;
|
let target: HTMLElement | undefined;
|
||||||
const edit = tippy(node, {
|
const edit = tippy(node, {
|
||||||
interactive: true,
|
interactive: true,
|
||||||
|
placement: "right",
|
||||||
trigger: "click",
|
trigger: "click",
|
||||||
onShow(instance) {
|
onShow(instance) {
|
||||||
target = instance.popper.querySelector(".tippy-content") as HTMLElement;
|
target = instance.popper.querySelector(".tippy-content") as HTMLElement;
|
||||||
|
|||||||
@@ -4,13 +4,13 @@
|
|||||||
import "$lib/style/scrollbar.scss";
|
import "$lib/style/scrollbar.scss";
|
||||||
import "$lib/style/tippy.scss";
|
import "$lib/style/tippy.scss";
|
||||||
import "$lib/style/theme.scss";
|
import "$lib/style/theme.scss";
|
||||||
|
import Sidebar from "./Sidebar.svelte";
|
||||||
import { onDestroy, onMount, type Snippet } from "svelte";
|
import { onDestroy, onMount, type Snippet } from "svelte";
|
||||||
import {
|
import {
|
||||||
applyTheme,
|
applyTheme,
|
||||||
argbFromHex,
|
argbFromHex,
|
||||||
themeFromSourceColor,
|
themeFromSourceColor,
|
||||||
} from "@material/material-color-utilities";
|
} from "@material/material-color-utilities";
|
||||||
import Navigation from "./Navigation.svelte";
|
|
||||||
import { canAutoConnect } from "$lib/serial/device";
|
import { canAutoConnect } from "$lib/serial/device";
|
||||||
import { initSerial } from "$lib/serial/connection";
|
import { initSerial } from "$lib/serial/connection";
|
||||||
import type { LayoutData } from "./$types";
|
import type { LayoutData } from "./$types";
|
||||||
@@ -116,7 +116,9 @@
|
|||||||
|
|
||||||
<svelte:window on:keydown={handleHotkey} />
|
<svelte:window on:keydown={handleHotkey} />
|
||||||
|
|
||||||
<Navigation />
|
<div class="layout">
|
||||||
|
|
||||||
|
<Sidebar />
|
||||||
|
|
||||||
<!-- <PickChangesDialog /> -->
|
<!-- <PickChangesDialog /> -->
|
||||||
|
|
||||||
@@ -132,5 +134,18 @@
|
|||||||
<BrowserWarning />
|
<BrowserWarning />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style lang="scss" global>
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.layout {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
|
||||||
|
display: grid;
|
||||||
|
grid-template-areas:
|
||||||
|
"sidebar main"
|
||||||
|
"sidebar footer";
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
grid-template-rows: 1fr ;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
88
src/routes/(app)/Sidebar.svelte
Normal file
88
src/routes/(app)/Sidebar.svelte
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { browser } from "$app/environment";
|
||||||
|
import { LL } from "$i18n/i18n-svelte";
|
||||||
|
import { popup } from "$lib/popup";
|
||||||
|
import { userPreferences } from "$lib/preferences";
|
||||||
|
import { serialPort, syncStatus } from "$lib/serial/connection";
|
||||||
|
import { action } from "$lib/title";
|
||||||
|
import BackupPopup from "./BackupPopup.svelte";
|
||||||
|
import ConnectionPopup from "./ConnectionPopup.svelte";
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
if (browser && !$userPreferences.autoConnect) {
|
||||||
|
connectButton.click();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let connectButton: HTMLButtonElement;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="sidebar">
|
||||||
|
<nav>
|
||||||
|
<ul>
|
||||||
|
<li><a href="/config/layout/" class="icon">tune</a></li>
|
||||||
|
<li><a href="/learn" class="wip icon">school</a></li>
|
||||||
|
<li><a href="/editor" class="wip icon">edit_document</a></li>
|
||||||
|
<li><a href="/chat" class="wip icon">chat</a></li>
|
||||||
|
<li><a href="/plugin" class="wip icon">code</a></li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
<ul class="sidebar-footer">
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
bind:this={connectButton}
|
||||||
|
use:action={{ title: $LL.deviceManager.TITLE() }}
|
||||||
|
use:popup={ConnectionPopup}
|
||||||
|
class="icon connect"
|
||||||
|
class:error={$serialPort === undefined}
|
||||||
|
>
|
||||||
|
cable
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
use:action={{ title: $LL.backup.TITLE() }}
|
||||||
|
use:popup={BackupPopup}
|
||||||
|
class="icon {$syncStatus}"
|
||||||
|
>
|
||||||
|
account_circle
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.sidebar {
|
||||||
|
margin: 8px;
|
||||||
|
padding-inline-end: 8px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
grid-area: sidebar;
|
||||||
|
border-right: 1px solid var(--md-sys-color-outline);
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wip {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wip::after {
|
||||||
|
content: "experiment";
|
||||||
|
font-size: 16px;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 4px;
|
||||||
|
right: 4px;
|
||||||
|
background: var(--md-sys-color-tertiary);
|
||||||
|
color: var(--md-sys-color-on-tertiary);
|
||||||
|
border-radius: 100%;
|
||||||
|
padding: 2px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
1
src/routes/(app)/chat/+page.svelte
Normal file
1
src/routes/(app)/chat/+page.svelte
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<h2>WIP</h2>
|
||||||
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}
|
||||||
@@ -1,25 +1,10 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { serialPort, syncStatus } from "$lib/serial/connection";
|
import { fly } from "svelte/transition";
|
||||||
import { slide, fly } from "svelte/transition";
|
|
||||||
import { canShare, triggerShare } from "$lib/share";
|
import { canShare, triggerShare } from "$lib/share";
|
||||||
import { popup } from "$lib/popup";
|
|
||||||
import BackupPopup from "./BackupPopup.svelte";
|
|
||||||
import ConnectionPopup from "./ConnectionPopup.svelte";
|
|
||||||
import { browser } from "$app/environment";
|
|
||||||
import { userPreferences } from "$lib/preferences";
|
|
||||||
import { action } from "$lib/title";
|
import { action } from "$lib/title";
|
||||||
import LL from "$i18n/i18n-svelte";
|
import LL from "$i18n/i18n-svelte";
|
||||||
import ConfigTabs from "./ConfigTabs.svelte";
|
import ConfigTabs from "./ConfigTabs.svelte";
|
||||||
import EditActions from "./EditActions.svelte";
|
import EditActions from "./EditActions.svelte";
|
||||||
import { onMount } from "svelte";
|
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
if (browser && !$userPreferences.autoConnect) {
|
|
||||||
connectButton.click();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let connectButton: HTMLButtonElement;
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<nav>
|
<nav>
|
||||||
@@ -43,44 +28,16 @@
|
|||||||
class="icon"
|
class="icon"
|
||||||
onclick={() => print()}>print</button
|
onclick={() => print()}>print</button
|
||||||
>
|
>
|
||||||
<div transition:slide class="separator"></div>
|
|
||||||
{/if}
|
{/if}
|
||||||
{#if import.meta.env.TAURI_FAMILY === undefined}
|
{#if import.meta.env.TAURI_FAMILY === undefined}
|
||||||
{#await import("$lib/components/PwaStatus.svelte") then { default: PwaStatus }}
|
{#await import("$lib/components/PwaStatus.svelte") then { default: PwaStatus }}
|
||||||
<PwaStatus />
|
<PwaStatus />
|
||||||
{/await}
|
{/await}
|
||||||
{/if}
|
{/if}
|
||||||
<button
|
|
||||||
use:action={{ title: $LL.backup.TITLE() }}
|
|
||||||
use:popup={BackupPopup}
|
|
||||||
class="icon {$syncStatus}"
|
|
||||||
>
|
|
||||||
{#if $userPreferences.backup}
|
|
||||||
history
|
|
||||||
{:else}
|
|
||||||
history_toggle_off
|
|
||||||
{/if}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
bind:this={connectButton}
|
|
||||||
use:action={{ title: $LL.deviceManager.TITLE() }}
|
|
||||||
use:popup={ConnectionPopup}
|
|
||||||
class="icon connect"
|
|
||||||
class:error={$serialPort === undefined}
|
|
||||||
>
|
|
||||||
cable
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.separator {
|
|
||||||
width: 1px;
|
|
||||||
height: 24px;
|
|
||||||
margin-inline: 4px;
|
|
||||||
background: var(--md-sys-color-outline-variant);
|
|
||||||
}
|
|
||||||
|
|
||||||
nav {
|
nav {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr auto 1fr;
|
grid-template-columns: 1fr auto 1fr;
|
||||||
125
src/routes/(app)/editor/+page.svelte
Normal file
125
src/routes/(app)/editor/+page.svelte
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { ReplayRecorder } from "$lib/charrecorder/core/recorder";
|
||||||
|
import type { InferredChord, Replay } from "$lib/charrecorder/core/types";
|
||||||
|
import CharRecorder from "$lib/charrecorder/CharRecorder.svelte";
|
||||||
|
import TrackChords from "$lib/charrecorder/TrackChords.svelte";
|
||||||
|
import TrackRollingWpm from "$lib/charrecorder/TrackRollingWpm.svelte";
|
||||||
|
import { fade } from "svelte/transition";
|
||||||
|
|
||||||
|
let recorder: ReplayRecorder = $state(new ReplayRecorder());
|
||||||
|
let replay: Replay | undefined = $state();
|
||||||
|
|
||||||
|
let wpm = $state(0);
|
||||||
|
let chords: InferredChord[] = $state([]);
|
||||||
|
|
||||||
|
function handleRawKey(event: KeyboardEvent) {
|
||||||
|
event.preventDefault();
|
||||||
|
keyEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
function keyEvent(event: KeyboardEvent) {
|
||||||
|
if (event.key === "Tab") {
|
||||||
|
clear();
|
||||||
|
} else {
|
||||||
|
if (replay) {
|
||||||
|
replay = undefined;
|
||||||
|
}
|
||||||
|
recorder.next(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clear() {
|
||||||
|
recorder = new ReplayRecorder();
|
||||||
|
}
|
||||||
|
|
||||||
|
function runReplay() {
|
||||||
|
replay = recorder.finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
function save() {
|
||||||
|
const replay = recorder.finish();
|
||||||
|
const blob = new Blob([JSON.stringify(replay)], {
|
||||||
|
type: "application/json",
|
||||||
|
});
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = url;
|
||||||
|
a.download = "replay.json";
|
||||||
|
a.click();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>Editor</title>
|
||||||
|
</svelte:head>
|
||||||
|
<svelte:window onkeydown={handleRawKey} onkeyup={handleRawKey} />
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Editor</h2>
|
||||||
|
|
||||||
|
{#if replay}
|
||||||
|
<div class="replay" transition:fade={{ duration: 100 }}>
|
||||||
|
<CharRecorder {replay} cursor={true} keys={true} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#key recorder}
|
||||||
|
<div
|
||||||
|
class="editor"
|
||||||
|
out:fade={{ duration: 100 }}
|
||||||
|
style:opacity={replay ? 0 : undefined}
|
||||||
|
>
|
||||||
|
<CharRecorder replay={recorder.player} cursor={true} keys={true}>
|
||||||
|
<TrackRollingWpm bind:wpm />
|
||||||
|
<TrackChords bind:chords />
|
||||||
|
</CharRecorder>
|
||||||
|
</div>
|
||||||
|
{/key}
|
||||||
|
|
||||||
|
<div class="toolbar">
|
||||||
|
<div>
|
||||||
|
<button onclick={clear}>Clear <kbd>TAB</kbd></button>
|
||||||
|
<button onclick={runReplay}>Replay</button>
|
||||||
|
<button onclick={save}>Export</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div><b>{Math.round(wpm)}</b><sub>WPM</sub></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
section {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.replay,
|
||||||
|
.editor {
|
||||||
|
position: absolute;
|
||||||
|
top: 3em;
|
||||||
|
left: 0;
|
||||||
|
transition: opacity 0.1s;
|
||||||
|
padding: 16px;
|
||||||
|
padding-left: 0;
|
||||||
|
padding-bottom: 5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
padding-right: 16px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
|
||||||
|
> div {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
1
src/routes/(app)/learn/+page.svelte
Normal file
1
src/routes/(app)/learn/+page.svelte
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<h2>WIP</h2>
|
||||||
@@ -209,6 +209,7 @@
|
|||||||
|
|
||||||
<svelte:window onmessage={onMessage} />
|
<svelte:window onmessage={onMessage} />
|
||||||
<section>
|
<section>
|
||||||
|
<h3>Plugin</h3>
|
||||||
<button onclick={runPlugin}
|
<button onclick={runPlugin}
|
||||||
><span class="icon">play_arrow</span>{$LL.plugin.editor.RUN()}</button
|
><span class="icon">play_arrow</span>{$LL.plugin.editor.RUN()}</button
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { LL } from "$i18n/i18n-svelte";
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<h1>{$LL.update.TITLE()}</h1>
|
|
||||||
|
|
||||||
<a
|
|
||||||
href="https://github.com/CharaChorder/CCOS-firmware/blob/main/CHANGELOG.md"
|
|
||||||
target="_blank">Changelog</a
|
|
||||||
>
|
|
||||||
Reference in New Issue
Block a user