mirror of
https://github.com/CharaChorder/DeviceManager.git
synced 2025-12-11 05:16:16 +00:00
feat: multi-purpose site
feat: editor feat: plugin editor
This commit is contained in:
@@ -94,6 +94,12 @@ const config = {
|
||||
"description",
|
||||
"add_circle",
|
||||
"refresh",
|
||||
"tune",
|
||||
"edit_document",
|
||||
"chat",
|
||||
"account_circle",
|
||||
"experiment",
|
||||
"code",
|
||||
],
|
||||
codePoints: {
|
||||
speed: "e9e4",
|
||||
@@ -113,6 +119,7 @@ const config = {
|
||||
stat_minus_2: "e69c",
|
||||
stat_2: "e699",
|
||||
routine: "e20c",
|
||||
experiment: "e686",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -5,21 +5,21 @@
|
||||
import { ChordsReplayPlugin } from "./core/plugins/chords.js";
|
||||
import type { ReplayPlayer } from "./core/player.js";
|
||||
|
||||
const player: ReplayPlayer | undefined = getContext("replay");
|
||||
const player: { player: ReplayPlayer | undefined } = getContext("replay");
|
||||
|
||||
let {
|
||||
chords = $bindable([]),
|
||||
count = 1,
|
||||
}: {
|
||||
chords: InferredChord[];
|
||||
count: number;
|
||||
count?: number;
|
||||
} = $props();
|
||||
|
||||
if (browser) {
|
||||
$effect(() => {
|
||||
if (!player) return;
|
||||
if (!player.player) return;
|
||||
const tracker = new ChordsReplayPlugin();
|
||||
tracker.register(player);
|
||||
tracker.register(player.player);
|
||||
const unsubscribe = tracker.subscribe((value) => {
|
||||
chords = value.slice(-count);
|
||||
});
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
import { RollingWpmReplayPlugin } from "./core/plugins/rolling-wpm";
|
||||
import type { ReplayPlayer } from "./core/player";
|
||||
|
||||
const player: ReplayPlayer | undefined = getContext("replay");
|
||||
const player: { player: ReplayPlayer | undefined } = getContext("replay");
|
||||
|
||||
let { wpm = $bindable(0) } = $props();
|
||||
|
||||
$effect(() => {
|
||||
if (!player) return;
|
||||
if (!player.player) return;
|
||||
const tracker = new RollingWpmReplayPlugin();
|
||||
tracker.register(player);
|
||||
tracker.register(player.player);
|
||||
const unsubscribe = tracker.subscribe((value) => {
|
||||
wpm = value;
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ export const popup: Action<HTMLButtonElement, Component> = (
|
||||
let target: HTMLElement | undefined;
|
||||
const edit = tippy(node, {
|
||||
interactive: true,
|
||||
placement: "right",
|
||||
trigger: "click",
|
||||
onShow(instance) {
|
||||
target = instance.popper.querySelector(".tippy-content") as HTMLElement;
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
import "$lib/style/scrollbar.scss";
|
||||
import "$lib/style/tippy.scss";
|
||||
import "$lib/style/theme.scss";
|
||||
import Sidebar from "./Sidebar.svelte";
|
||||
import { onDestroy, onMount, type Snippet } from "svelte";
|
||||
import {
|
||||
applyTheme,
|
||||
argbFromHex,
|
||||
themeFromSourceColor,
|
||||
} from "@material/material-color-utilities";
|
||||
import Navigation from "./Navigation.svelte";
|
||||
import { canAutoConnect } from "$lib/serial/device";
|
||||
import { initSerial } from "$lib/serial/connection";
|
||||
import type { LayoutData } from "./$types";
|
||||
@@ -116,7 +116,9 @@
|
||||
|
||||
<svelte:window on:keydown={handleHotkey} />
|
||||
|
||||
<Navigation />
|
||||
<div class="layout">
|
||||
|
||||
<Sidebar />
|
||||
|
||||
<!-- <PickChangesDialog /> -->
|
||||
|
||||
@@ -132,5 +134,18 @@
|
||||
<BrowserWarning />
|
||||
{/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>
|
||||
|
||||
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">
|
||||
import { serialPort, syncStatus } from "$lib/serial/connection";
|
||||
import { slide, fly } from "svelte/transition";
|
||||
import { fly } from "svelte/transition";
|
||||
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 LL from "$i18n/i18n-svelte";
|
||||
import ConfigTabs from "./ConfigTabs.svelte";
|
||||
import EditActions from "./EditActions.svelte";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
onMount(async () => {
|
||||
if (browser && !$userPreferences.autoConnect) {
|
||||
connectButton.click();
|
||||
}
|
||||
});
|
||||
|
||||
let connectButton: HTMLButtonElement;
|
||||
</script>
|
||||
|
||||
<nav>
|
||||
@@ -43,44 +28,16 @@
|
||||
class="icon"
|
||||
onclick={() => print()}>print</button
|
||||
>
|
||||
<div transition:slide class="separator"></div>
|
||||
{/if}
|
||||
{#if import.meta.env.TAURI_FAMILY === undefined}
|
||||
{#await import("$lib/components/PwaStatus.svelte") then { default: PwaStatus }}
|
||||
<PwaStatus />
|
||||
{/await}
|
||||
{/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>
|
||||
</nav>
|
||||
|
||||
<style lang="scss">
|
||||
.separator {
|
||||
width: 1px;
|
||||
height: 24px;
|
||||
margin-inline: 4px;
|
||||
background: var(--md-sys-color-outline-variant);
|
||||
}
|
||||
|
||||
nav {
|
||||
display: grid;
|
||||
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} />
|
||||
<section>
|
||||
<h3>Plugin</h3>
|
||||
<button onclick={runPlugin}
|
||||
><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