chore: add display text

This commit is contained in:
E44
2025-11-21 22:08:27 +01:00
parent 94bad9a72f
commit 5b891ae64c
10 changed files with 1887 additions and 84 deletions
+20
View File
@@ -10,6 +10,7 @@
"npm:@tailwindcss/typography@~0.5.15": "0.5.16_tailwindcss@4.1.11",
"npm:@tailwindcss/vite@4": "4.1.11_vite@7.0.6__picomatch@4.0.3",
"npm:@thisux/sveltednd@^0.0.20": "0.0.20_svelte@5.37.1__acorn@8.15.0",
"npm:dexie@^4.2.1": "4.2.1",
"npm:eslint-config-prettier@^10.0.1": "10.1.8_eslint@9.33.0",
"npm:eslint-plugin-svelte@3": "3.11.0_eslint@9.33.0_svelte@5.37.1__acorn@8.15.0_postcss@8.5.6",
"npm:eslint@^9.18.0": "9.33.0",
@@ -19,6 +20,7 @@
"npm:prettier@^3.4.2": "3.6.2",
"npm:svelte-check@4": "4.3.0_svelte@5.37.1__acorn@8.15.0_typescript@5.8.3",
"npm:svelte-dnd-action@~0.9.65": "0.9.65_svelte@5.37.1__acorn@8.15.0",
"npm:svelte-splitpanes@^8.0.9": "8.0.9_svelte@5.37.1__acorn@8.15.0",
"npm:svelte@5": "5.37.1_acorn@8.15.0",
"npm:tailwindcss@4": "4.1.11",
"npm:typescript-eslint@^8.20.0": "8.40.0_eslint@9.33.0_typescript@5.8.3_@typescript-eslint+parser@8.40.0__eslint@9.33.0__typescript@5.8.3",
@@ -867,6 +869,9 @@
"devalue@5.1.1": {
"integrity": "sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw=="
},
"dexie@4.2.1": {
"integrity": "sha512-Ckej0NS6jxQ4Po3OrSQBFddayRhTCic2DoCAG5zacOfOVB9P2Q5Xc5uL/nVa7ZVs+HdMnvUPzLFCB/JwpB6Csg=="
},
"enhanced-resolve@5.18.2": {
"integrity": "sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==",
"dependencies": [
@@ -991,6 +996,12 @@
],
"bin": true
},
"esm-env-robust@0.0.3": {
"integrity": "sha512-90Gnuw2DALOqlL1581VxP3GHPUNHX9U+fQ+8FNcTTFClhY5gEggAAnJ3q1b2Oq23knRsjv8YpNeMRPaMLUymOA==",
"dependencies": [
"esm-env"
]
},
"esm-env@1.2.2": {
"integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="
},
@@ -1579,6 +1590,13 @@
"svelte"
]
},
"svelte-splitpanes@8.0.9_svelte@5.37.1__acorn@8.15.0": {
"integrity": "sha512-L3oLXTC99M191FInTXJ/f/2i0welRql1QuVbPaU8iy6nvCR6X9VyjHCsCpLqKGWHwqkWo/AM9CQ1c0nzlb+MkA==",
"dependencies": [
"esm-env-robust",
"svelte"
]
},
"svelte@5.37.1_acorn@8.15.0": {
"integrity": "sha512-h8arWpQZ+3z8eahyBT5KkiBOUsG6xvI5Ykg0ozRr9xEdImgSMUPUlOFWRNkUsT7Ti0DSUCTEbPoped0aoxFyWA==",
"dependencies": [
@@ -1729,6 +1747,7 @@
"npm:@tailwindcss/typography@~0.5.15",
"npm:@tailwindcss/vite@4",
"npm:@thisux/sveltednd@^0.0.20",
"npm:dexie@^4.2.1",
"npm:eslint-config-prettier@^10.0.1",
"npm:eslint-plugin-svelte@3",
"npm:eslint@^9.18.0",
@@ -1738,6 +1757,7 @@
"npm:prettier@^3.4.2",
"npm:svelte-check@4",
"npm:svelte-dnd-action@~0.9.65",
"npm:svelte-splitpanes@^8.0.9",
"npm:svelte@5",
"npm:tailwindcss@4",
"npm:typescript-eslint@^8.20.0",
+1528 -32
View File
File diff suppressed because it is too large Load Diff
+8 -1
View File
@@ -20,14 +20,15 @@
"@sveltejs/adapter-static": "^3.0.9",
"@sveltejs/kit": "^2.22.0",
"@sveltejs/vite-plugin-svelte": "^6.0.0",
"@tailwindcss/typography": "^0.5.15",
"@tailwindcss/vite": "^4.0.0",
"@tiptap/extension-placeholder": "^3.11.0",
"eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-svelte": "^3.0.0",
"globals": "^16.0.0",
"prettier": "^3.4.2",
"prettier-plugin-svelte": "^3.3.3",
"sass-embedded": "^1.93.3",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"svelte-dnd-action": "^0.9.65",
@@ -38,6 +39,12 @@
},
"dependencies": {
"@thisux/sveltednd": "^0.0.20",
"@tiptap/core": "^3.11.0",
"@tiptap/extension-highlight": "^3.11.0",
"@tiptap/extension-text-style": "^3.11.0",
"@tiptap/extensions": "^3.11.0",
"@tiptap/pm": "^3.11.0",
"@tiptap/starter-kit": "^3.11.0",
"dexie": "^4.2.1",
"lucide-svelte": "^0.545.0",
"svelte-splitpanes": "^8.0.9"
@@ -15,13 +15,11 @@
import PopUp from './PopUp.svelte';
import type { PopupContent } from '../ts/types';
import KeyInput from './KeyInput.svelte';
import { send_keyboard_input, show_blackscreen } from '../ts/api_handler';
import { send_keyboard_input, show_blackscreen, show_html } from '../ts/api_handler';
import { run_on_all_selected_displays } from '../ts/stores/displays';
import { selected_display_ids } from '../ts/stores/select';
import { onMount } from 'svelte';
let show_text_body: string = `<p>I've been working on this <strong>new feature</strong> for the past few days.
It's been quite <em>challenging</em> but I think I'm making good progress.</p>`;
import TipTapInput from './TipTapInput.svelte';
let popup_content: PopupContent = $state({
open: false,
@@ -50,28 +48,25 @@
snippet: text_popup,
title: 'Text anzeigen',
title_icon: TextAlignStart,
closable: true
closable: true,
window_class: 'size-full'
};
};
onMount(() => {
show_text_popup();
});
</script>
{#snippet send_keys_popup()}
<div>
<KeyInput />
</div>
<div class="flex flex-row justify-end gap-2">
<Button className="px-4 font-bold" click_function={popup_close_function}>Fertig</Button>
<div class="overflow-hidden flex flex-col gap-2">
<div>
<KeyInput />
</div>
<div class="flex flex-row justify-end gap-2">
<Button className="px-4 font-bold" click_function={popup_close_function}>Fertig</Button>
</div>
</div>
{/snippet}
{#snippet text_popup()}
<!-- <div class="flex flex-row justify-end gap-2">
<Button className="px-4 font-bold" click_function={popup_close_function}>Fertig</Button>
</div> -->
<TipTapInput />
{/snippet}
<div class="grid grid-rows-[2.5rem_auto] bg-stone-800 rounded-2xl min-w-0">
@@ -159,7 +154,7 @@
content={popup_content}
close_function={popup_close_function}
className="rounded-b-2xl"
snippet_container_class="overflow-hidden"
snippet_container_class="size-full"
/>
</div>
</div>
@@ -181,7 +181,7 @@
</span>
<div class="flex flex-row gap-1">
<button
class="gap-2 min-w-40 px-4 rounded-xl cursor-pointer duration-200 transition-colors {get_selectable_color_classes(
class="min-w-40 px-4 rounded-xl cursor-pointer duration-200 transition-colors {get_selectable_color_classes(
all_selected($displays, $selected_display_ids),
{
bg: true,
+29 -20
View File
@@ -5,7 +5,12 @@
import type { PopupContent } from '../ts/types';
import { fade } from 'svelte/transition';
let { content, close_function, className = '', snippet_container_class = '' } = $props<{
let {
content,
close_function,
className = '',
snippet_container_class = ''
} = $props<{
content: PopupContent;
close_function: () => void;
className?: string;
@@ -40,29 +45,33 @@
<!-- svelte-ignore a11y_no_static_element_interactions -->
<!-- svelte-ignore a11y_click_events_have_key_events -->
<div
class="bg-stone-800 rounded-2xl min-w-[30%] max-w-[95%] max-h-[90%] flex flex-col shadow-2xl/60 overflow-hidden"
class="bg-stone-800 rounded-2xl min-w-[30%] max-w-[90%] max-h-[85%] flex flex-col shadow-2xl/60 overflow-hidden {content.window_class ??
''}"
onclick={(e) => e.stopPropagation()}
>
<div
class="font-bold bg-stone-700 p-1.5 flex flex-row justify-between gap-8 w-full"
>
<div class="flex flex-row flex-1 gap-3 pl-2 py-1 items-center grow whitespace-nowrap min-w-0 flex-shrink-0 text-lg {content.title_class ?? ''}">
{#if content.title_icon}
{@const Icon = content.title_icon}
<Icon strokeWidth="2" class="flex-shrink-0" />
{/if}
<div class="flex-shrink-0">
{content.title}
{#if content.title}
<div class="font-bold bg-stone-700 p-1.5 flex flex-row justify-between gap-8 w-full">
<div
class="flex flex-row flex-1 gap-3 pl-2 py-1 items-center grow whitespace-nowrap min-w-0 flex-shrink-0 text-lg {content.title_class ??
''}"
>
{#if content.title_icon}
{@const Icon = content.title_icon}
<Icon strokeWidth="2" class="flex-shrink-0" />
{/if}
<div class="flex-shrink-0">
{content.title}
</div>
</div>
<div class="flex aspect-square flex-shrink-0">
{#if content.closable}
<Button className="aspect-square !p-1.5" click_function={try_to_close}>
<X />
</Button>
{/if}
</div>
</div>
<div class="flex aspect-square flex-shrink-0">
{#if content.closable}
<Button className="aspect-square !p-1.5" click_function={try_to_close}>
<X />
</Button>
{/if}
</div>
</div>
{/if}
<div class="p-2 min-h-0 overflow-auto flex flex-col gap-2 {snippet_container_class}">
{@render content.snippet(content.snippet_arg)}
</div>
@@ -0,0 +1,255 @@
<script lang="ts">
import { onMount, onDestroy } from 'svelte';
import { Editor } from '@tiptap/core';
import { StarterKit } from '@tiptap/starter-kit';
import Placeholder from '@tiptap/extension-placeholder';
import {
Baseline,
Bold,
Code,
Highlighter,
Italic,
PaintBucket,
QrCode,
Strikethrough
} from 'lucide-svelte';
import Button from './Button.svelte';
import { run_on_all_selected_displays } from '../ts/stores/displays';
import { show_html } from '../ts/api_handler';
import { get_selectable_color_classes } from '../ts/stores/ui_behavior';
import { TextStyle } from '@tiptap/extension-text-style';
import { Color } from '@tiptap/extension-text-style';
import Highlight from '@tiptap/extension-highlight';
type TextEditOption = {
onclick: () => void;
is_selected: () => boolean;
icon: typeof Bold;
title: string;
color?: ColorElement;
};
type ColorElement = {
color_string: () => string;
color_picker: () => void;
};
type ColorState = {
el: HTMLInputElement | null;
value: string;
};
let element = $state<HTMLElement>();
let editor_state = $state<{ editor: Editor | null }>({ editor: null });
let color_states: { text: ColorState; highlight: ColorState; bg: ColorState } = $state({
text: { el: null, value: '#b91c1c' },
highlight: { el: null, value: '#0c4a6e' },
bg: { el: null, value: '#1c1917' }
});
const text_edit_options: TextEditOption[][] = [
[
{
onclick: () => editor_state.editor?.chain().focus().toggleBold().run(),
is_selected: () => editor_state.editor?.isActive('bold') ?? false,
title: 'Fett (STRG+B)',
icon: Bold
},
{
onclick: () => editor_state.editor?.chain().focus().toggleItalic().run(),
is_selected: () => editor_state.editor?.isActive('italic') ?? false,
title: 'Kursiv (STRG+I)',
icon: Italic
},
{
onclick: () => editor_state.editor?.chain().focus().toggleStrike().run(),
is_selected: () => editor_state.editor?.isActive('strike') ?? false,
title: 'Durchgestrichen',
icon: Strikethrough
},
{
onclick: () => {},
is_selected: () => false,
title: 'QR-Code anfügen',
icon: QrCode
}
],
[
{
onclick: () => editor_state.editor?.chain().focus().setColor(color_states.text.value).run(),
is_selected: () =>
editor_state.editor?.isActive('textStyle', { color: color_states.text.value }) ?? false,
icon: Baseline,
title: 'Textfarbe',
color: get_color_element(color_states.text)
},
{
onclick: () =>
editor_state.editor
?.chain()
.focus()
.toggleHighlight({ color: color_states.highlight.value })
.run(),
is_selected: () =>
editor_state.editor?.isActive('highlight', { color: color_states.highlight.value }) ??
false,
icon: Highlighter,
title: 'Markierungsfarbe',
color: get_color_element(color_states.highlight)
},
{
onclick: () => color_states.bg.el?.click(),
is_selected: () => false,
title: 'Hintergrundfarbe',
icon: PaintBucket
}
]
];
function get_color_element(color_state: ColorState) {
return {
color_string: () => color_state.value,
color_picker: () => color_state.el?.click()
};
}
function show_text() {
const html =
editor_state.editor?.getHTML() +
`<style>:root {--background-color: ${color_states.bg.value} !important;}
</style>`;
run_on_all_selected_displays(show_html, true, html);
}
onMount(() => {
editor_state.editor = new Editor({
element: element,
extensions: [
StarterKit,
Placeholder.configure({
placeholder: 'Text hier eingeben ...'
}),
TextStyle,
Color,
Highlight.configure({
multicolor: true
})
],
content: '',
onTransaction: ({ editor }) => {
// Increment the state signal to force a re-render
editor_state = { editor };
},
autofocus: true
});
});
onDestroy(() => {
editor_state.editor?.destroy();
});
</script>
{#each Object.values(color_states) as color_state}
<input type="color" bind:this={color_state.el} bind:value={color_state.value} class="hidden" />
{/each}
<div class="flex flex-row gap-2 size-full">
<div
class="rounded-xl size-full flex-shrink min-w-0 flex"
style="background-color: {color_states.bg.value};"
>
<div bind:this={element} class="size-full overflow-auto px-3 py-2"></div>
</div>
<div class="flex flex-col gap-2 justify-between">
<div class="flex flex-col gap-2">
{#each text_edit_options as edit_row}
<div class="flex flex-row gap-1">
{#each edit_row as option}
<div class="flex flex-row">
<button
title={option.title}
onclick={option.onclick}
class="p-1 {option.color
? 'rounded-l-xl'
: 'rounded-xl'} cursor-pointer duration-200 transition-colors {get_selectable_color_classes(
option.is_selected(),
{
bg: true,
hover: true,
active: true,
text: true
},
-100,
50
)}"
>
{#if option.icon}
{@const Icon = option.icon}
<Icon class="h-7 w-7" />
{/if}
</button>
{#if option.color}
<button
onclick={option.color.color_picker}
title={option.title}
class="flex p-1 rounded-r-xl cursor-pointer duration-200 transition-colors justify-center items-center {get_selectable_color_classes(
option.is_selected(),
{
bg: true,
hover: true,
active: true,
text: true
},
-100,
50
)}"
>
<span
class="h-7 w-3 rounded-full"
style="background-color: {option.color.color_string()};"
></span>
</button>
{/if}
</div>
{/each}
</div>
{/each}
</div>
<Button click_function={show_text} className="w-full font-bold">Text anzeigen</Button>
</div>
</div>
<style>
:global(.tiptap) {
width: 100%;
height: 100%;
--tw-outline-style: none;
outline-style: none;
}
:global(.tiptap ul),
:global(.tiptap ol) {
padding-left: 1.5rem;
}
:global(.tiptap ul) {
list-style: disc;
}
:global(.tiptap ol) {
list-style: decimal;
}
:global(.tiptap p) {
margin-bottom: 0.5rem;
}
:global(.tiptap p.is-editor-empty:first-child::before) {
content: attr(data-placeholder);
pointer-events: none;
color: oklch(44.4% 0.011 73.639);
float: left;
height: 0;
opacity: 0.7;
}
+12 -1
View File
@@ -25,6 +25,17 @@ export async function send_keyboard_input(ip: string, key: string): Promise<void
await request_display(ip, '/keyboardInput', options);
}
export async function show_html(ip: string, html: string) {
const options = {
method: 'PATCH',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
html: html
})
};
await request_display(ip, '/showHTML', options);
}
export async function get_file_data(ip: string, path: string): Promise<FolderElement[]> {
interface FileInfo {
name: string;
@@ -136,7 +147,7 @@ async function request_control(api_route: string, options: { method: string, hea
async function request(url: string, options: { method: string, headers?: Record<string, string>, body?: any }, supress_error_handling_http_codes: number[] = []): Promise<RequestResponse> {
try {
const cache_buster = `${url.includes('?') ? '&' : '?'}=${Date.now()}`;
console.log(url + cache_buster)
console.log(url + cache_buster, options.body ?? null);
const response = await fetch(url + cache_buster, options);
if (response.ok || supress_error_handling_http_codes.includes(response.status)) {
const contentType = response.headers.get("content-type") || "";
+2 -1
View File
@@ -73,9 +73,10 @@ export type PopupContent = {
open: boolean;
snippet: Snippet<[string]> | null;
snippet_arg?: string;
title: string;
title?: string;
title_class?: string;
title_icon?: typeof X | null;
window_class?: string;
closable?: boolean;
}