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

@@ -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",
},
};

View File

@@ -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);
});

View File

@@ -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;
});

View File

@@ -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;

View File

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

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

View File

@@ -0,0 +1 @@
<h2>WIP</h2>

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

@@ -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;

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

View File

@@ -0,0 +1 @@
<h2>WIP</h2>

View File

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

View File

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