feat: support sending modifier keys (#18)

Co-authored-by: E44 <129310925+programmer-44@users.noreply.github.com>
This commit is contained in:
2026-01-19 18:47:53 +01:00
committed by GitHub
parent c8e21a64bf
commit 4d5d9849da
7 changed files with 339 additions and 388 deletions
+33 -33
View File
@@ -57,6 +57,7 @@
snippet: send_keys_popup,
title: 'Tastatur-Eingaben Senden',
title_icon: Keyboard,
window_class: 'h-full'
};
};
@@ -123,17 +124,8 @@
);
}
async function send_single_key_press(key: string) {
await run_on_all_selected_displays((d) =>
send_keyboard_input(d.ip, [{ key, action: 'press' }])
);
setTimeout(
async () =>
await run_on_all_selected_displays((d) =>
send_keyboard_input(d.ip, [{ key, action: 'release' }])
),
10
);
async function send_single_key_press(key: string, action: 'press' | 'release') {
await run_on_all_selected_displays((d) => send_keyboard_input(d.ip, [{ key, action }]));
}
let website_url = $state('');
let website_url_valid = $state(false);
@@ -192,14 +184,7 @@
{/snippet}
{#snippet send_keys_popup()}
<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>
<KeyInput {popup_close_function}/>
{/snippet}
{#snippet text_popup()}
@@ -214,22 +199,37 @@
<div class="flex flex-row justify-between gap-2">
<div class="flex flex-col gap-2">
<div class="flex flex-row gap-2 w-75 justify-normal">
<Button
title="Vorherige Folie (Pfeil nach Links)"
className="px-9"
disabled={no_active_display_selected($selected_display_ids, $online_displays)}
click_function={async () => {
await send_single_key_press('VK_LEFT');
}}><ArrowBigLeft /></Button
<button
title="Vorherige Folie (Pfeil nach Links) [gedrückt halten möglich]"
class="px-9 bg-stone-700 {$selected_display_ids.length === 0
? 'text-stone-500 cursor-not-allowed'
: 'hover:bg-stone-600 active:bg-stone-500 cursor-pointer'} py-2 rounded-xl flex justify-center items-center transition-colors duration-200"
disabled={$selected_display_ids.length === 0}
onmousedown={async () => {
await send_single_key_press('ArrowLeft', 'press');
}}
onmouseup={async () => {
await send_single_key_press('ArrowLeft', 'release');
}}
>
<Button
title="Nächste Folie (Pfeil nach Rechts)"
className="px-9"
disabled={no_active_display_selected($selected_display_ids, $online_displays)}
click_function={async () => {
await send_single_key_press('VK_RIGHT');
}}><ArrowBigRight /></Button
<ArrowBigLeft />
</button>
<button
title="Vorherige Folie (Pfeil nach Links) [gedrückt halten möglich]"
class="px-9 bg-stone-700 {$selected_display_ids.length === 0
? 'text-stone-500 cursor-not-allowed'
: 'hover:bg-stone-600 active:bg-stone-500 cursor-pointer'} py-2 rounded-xl flex justify-center items-center transition-colors duration-200"
disabled={$selected_display_ids.length === 0}
onmousedown={async () => {
await send_single_key_press('ArrowRight', 'press');
}}
onmouseup={async () => {
await send_single_key_press('ArrowRight', 'release');
}}
>
<ArrowBigRight />
</button>
</div>
<Button
+127 -63
View File
@@ -1,106 +1,170 @@
<script lang="ts">
import { flip } from 'svelte/animate';
import { get_selectable_color_classes } from '$lib/ts/stores/ui_behavior';
import key_map_json from './../../../../shared/keys.json';
import { fade } from 'svelte/transition';
import { run_on_all_selected_displays } from '$lib/ts/stores/displays';
import { send_keyboard_input } from '$lib/ts/api_handler';
import { ArrowDownToLine, ArrowUpFromLine, Grid2x2, Grid2X2, Option } from 'lucide-svelte';
import Button from '$lib/components/Button.svelte';
import { onDestroy } from 'svelte';
const key_map: Record<string, string> = key_map_json as Record<string, string>;
let {
popup_close_function
}: {
popup_close_function: () => void;
} = $props();
let active = $state(false);
const current_keys: string[] = $state([]);
let last_keys: { id: number; key: string }[] = $state([]);
let seq = 0;
let el: HTMLDivElement;
function add_to_last_keys(name: string) {
const id = Date.now();
last_keys.push({ id, key: name });
const id = ++seq;
// Neueste oben
last_keys = [{ id, key: name }, ...last_keys].slice(0, 6);
setTimeout(() => {
last_keys = last_keys.filter((e) => e.id !== id);
}, 1500);
}
async function on_key(e: KeyboardEvent, key_down: boolean) {
async function on_keyboard_input(e: KeyboardEvent, key_down: boolean) {
if (!active) return;
const id = key_map[e.code];
const id = e.code;
if (!id) return;
if (key_down) {
if (current_keys.includes(e.code)) return;
current_keys.push(e.code);
} else {
const index = current_keys.indexOf(e.code);
if (index === -1) return;
current_keys.splice(index, 1);
}
if (e.repeat) return;
e.preventDefault();
e.stopPropagation();
if (e.repeat) return;
await on_key(id, key_down);
}
async function on_key(key: string, key_down: boolean) {
if (key_down) {
if (current_keys.includes(key)) return;
current_keys.push(key);
} else {
const index = current_keys.indexOf(key);
if (index === -1) return;
current_keys.splice(index, 1);
}
const action: 'press' | 'release' = key_down ? 'press' : 'release';
add_to_last_keys(action.toUpperCase() + ' ' + e.code);
await run_on_all_selected_displays(
(d) => send_keyboard_input(d.ip, [{ key: id, action }]),
true
);
add_to_last_keys(action.toUpperCase() + ' ' + key);
await run_on_all_selected_displays((d) => send_keyboard_input(d.ip, [{ key, action }]), true);
}
async function release_all_pressed_keys() {
const inputs: {key: string; action: 'press' | 'release' }[] = [];
const inputs: { key: string; action: 'press' | 'release' }[] = [];
for (let i = current_keys.length - 1; i >= 0; i--) {
inputs.push({key: current_keys[i], action: 'release'})
inputs.push({ key: current_keys[i], action: 'release' });
current_keys.splice(i, 1);
}
await run_on_all_selected_displays(
(d) => send_keyboard_input(d.ip, inputs),
true
);
await run_on_all_selected_displays((d) => send_keyboard_input(d.ip, inputs), true);
}
onDestroy(() => {
release_all_pressed_keys();
});
</script>
<div
role="textbox"
tabindex="0"
bind:this={el}
onclick={() => {
if (active) {
el.blur();
} else {
el.focus();
active = true;
}
}}
onblur={async () => {
active = false;
await release_all_pressed_keys();
}}
onkeydown={(e) => on_key(e, true)}
onkeyup={(e) => on_key(e, false)}
class="relative flex justify-center items-center h-15 w-full cursor-pointer rounded-xl transition-colors duration-200 select-none {get_selectable_color_classes(
active,
{
bg: true,
hover: true,
active: true,
text: true
}
)}"
>
{active ? 'Erfassung aktiv' : 'Hier für Erfassung klicken'}
<div class="absolute top-full left-0 ml-1 mt-0.5 flex flex-col-reverse text-sm text-stone-400">
{#each last_keys as key (key.id)}
<div class="flex flex-row gap-2 overflow-hidden h-full">
<div
role="textbox"
tabindex="0"
bind:this={el}
onclick={() => {
if (active) {
el.blur();
} else {
el.focus();
active = true;
}
}}
onblur={async () => {
active = false;
await release_all_pressed_keys();
}}
onkeydown={(e) => on_keyboard_input(e, true)}
onkeyup={(e) => on_keyboard_input(e, false)}
class="flex justify-center items-center text-center grow py-2 px-8 w-70 h-full cursor-pointer rounded-xl transition-colors duration-200 select-none {get_selectable_color_classes(
active,
{
bg: true,
hover: true,
active: true,
text: true
}
)}"
>
{active ? 'Erfassung aktiv' : 'Hier für Erfassung klicken'}
</div>
<div
class="relative flex flex-col gap-1 text-sm w-40 p-2 bg-stone-750 rounded-xl overflow-hidden h-full"
>
{#each last_keys as item (item.id)}
<span
animate:flip={{ duration: 200 }}
in:fade={{ duration: 200 }}
out:fade={{ duration: 500 }}>{key.key}</span
class="flex flex-row gap-2 {item.key.split(' ')[0] === 'PRESS'
? 'text-sky-600'
: 'text-lime-600'}"
animate:flip={{ duration: 120 }}
in:fade={{ duration: 120 }}
out:fade={{ duration: 200 }}
>
{#if item.key.split(' ')[0] === 'PRESS'}
<ArrowDownToLine />
{:else}
<ArrowUpFromLine />
{/if}
{item.key.split(' ').at(-1)}
</span>
{/each}
<div
class="absolute bottom-0 right-0 left-0 h-5 bg-linear-to-b from-transparent to-stone-750"
></div>
</div>
<div class="flex flex-col gap-2 justify-between">
<div class="flex flex-col gap-2">
<button
title="Windows-/Meta-Taste [gedrückt halten möglich]"
class="px-3 bg-stone-700 py-2 gap-2 rounded-xl flex items-center transition-colors duration-200 hover:bg-stone-600 active:bg-stone-500 cursor-pointer"
onmousedown={async (e) => {
e.preventDefault();
await on_key('MetaLeft', true);
}}
onmouseup={async () => {
await on_key('MetaLeft', false);
}}
>
<Grid2x2 /> Meta
</button>
<button
title="Alt-Taste [gedrückt halten möglich]"
class="px-3 bg-stone-700 py-2 gap-2 rounded-xl flex items-center transition-colors duration-200 hover:bg-stone-600 active:bg-stone-500 cursor-pointer"
onmousedown={async (e) => {
e.preventDefault();
await on_key('AltLeft', true);
}}
onmouseup={async () => {
await on_key('AltLeft', false);
}}
>
<Option /> Alt
</button>
</div>
<Button
div_class="mt-2 justify-end"
className="px-4 font-bold"
click_function={popup_close_function}>Fertig</Button
>
</div>
</div>