edit and delete display added

This commit is contained in:
E44
2025-11-04 20:24:33 +01:00
parent d355891517
commit 9f8bf01aba
8 changed files with 185 additions and 71 deletions
@@ -1,9 +1,17 @@
<script lang="ts"> <script lang="ts">
import { ArrowBigLeft, ArrowBigRight, ChevronDown, Keyboard, Power, PowerOff, Presentation, SquareTerminal, TextAlignStart, TrafficCone } from "lucide-svelte"; import {
import { selected_display_ids } from "../ts/stores/select"; ArrowBigLeft,
import Button from "./Button.svelte"; ArrowBigRight,
ChevronDown,
Keyboard,
Power,
PowerOff,
Presentation,
SquareTerminal,
TextAlignStart,
TrafficCone
} from 'lucide-svelte';
import Button from './Button.svelte';
</script> </script>
<div class="grid grid-rows-[2.5rem_auto] bg-stone-800 rounded-2xl min-w-0"> <div class="grid grid-rows-[2.5rem_auto] bg-stone-800 rounded-2xl min-w-0">
@@ -13,11 +13,12 @@
} from '../ts/stores/displays'; } from '../ts/stores/displays';
import DNDGrip from './DNDGrip.svelte'; import DNDGrip from './DNDGrip.svelte';
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
import type { DisplayGroup } from '../ts/types'; import type { DisplayGroup, MenuOption } from '../ts/types';
import { selected_display_ids } from '../ts/stores/select'; import { selected_display_ids } from '../ts/stores/select';
let { display_group } = $props<{ let { display_group, get_display_menu_options } = $props<{
display_group: DisplayGroup; display_group: DisplayGroup;
get_display_menu_options: (display_id: string) => MenuOption[]
}>(); }>();
let hovering_selectable = $state(false); let hovering_selectable = $state(false);
@@ -76,7 +77,7 @@
class="outline-none" class="outline-none"
role="figure" role="figure"
> >
<DisplayObject {display} /> <DisplayObject {display} {get_display_menu_options} />
</section> </section>
{/each} {/each}
@@ -8,11 +8,12 @@
import DNDGrip from './DNDGrip.svelte'; import DNDGrip from './DNDGrip.svelte';
import { Menu, Pencil, Pin, PinOff, Trash2, VideoOff, X } from 'lucide-svelte'; import { Menu, Pencil, Pin, PinOff, Trash2, VideoOff, X } from 'lucide-svelte';
import OnlineState from './OnlineState.svelte'; import OnlineState from './OnlineState.svelte';
import type { Display } from '../ts/types'; import type { Display, MenuOption } from '../ts/types';
import { is_selected, select, selected_display_ids } from '../ts/stores/select'; import { is_selected, select, selected_display_ids } from '../ts/stores/select';
let { display } = $props<{ let { display, get_display_menu_options } = $props<{
display: Display; display: Display;
get_display_menu_options: (display_id: string) => MenuOption[];
}>(); }>();
let hovering_unselectable = $state(false); let hovering_unselectable = $state(false);
@@ -121,17 +122,7 @@
click_function={(e) => { click_function={(e) => {
e.stopPropagation(); e.stopPropagation();
}} }}
menu_options={[ menu_options={get_display_menu_options(display.id)}
{
icon: Pencil,
name: 'Bildschirm bearbeiten'
},
{
icon: Trash2,
name: 'Bildschirm löschen',
class: 'text-red-400 hover:text-stone-200 hover:!bg-red-400 active:!bg-red-500'
}
]}
> >
<Menu /> <Menu />
</Button> </Button>
@@ -16,7 +16,7 @@
next_height_step_size, next_height_step_size,
pinned_display_id pinned_display_id
} from '../ts/stores/ui_behavior'; } from '../ts/stores/ui_behavior';
import { type Display, type DisplayGroup } from '../ts/types'; import { type Display, type DisplayGroup, type MenuOption } from '../ts/types';
import Button from './Button.svelte'; import Button from './Button.svelte';
import OnlineState from './OnlineState.svelte'; import OnlineState from './OnlineState.svelte';
import { cubicOut } from 'svelte/easing'; import { cubicOut } from 'svelte/easing';
@@ -26,11 +26,33 @@
import { flip } from 'svelte/animate'; import { flip } from 'svelte/animate';
import DisplayGroupObject from './DisplayGroupObject.svelte'; import DisplayGroupObject from './DisplayGroupObject.svelte';
let { handle_display_deletion, handle_display_editing } = $props<{
handle_display_deletion: (display_id: string) => void;
handle_display_editing: (display_id: string) => void;
}>();
let displays_scroll_box: HTMLElement; let displays_scroll_box: HTMLElement;
let pinned_display: Display | null = $derived( let pinned_display: Display | null = $derived(
get_display_by_id($pinned_display_id || '', $displays) get_display_by_id($pinned_display_id || '', $displays)
); );
function get_display_menu_options(display_id: string): MenuOption[] {
return [
{
icon: Pencil,
name: 'Bildschirm bearbeiten',
on_select: () => {handle_display_editing(display_id)},
},
{
icon: Trash2,
name: 'Bildschirm löschen',
class: 'text-red-400 hover:text-stone-200 hover:!bg-red-400 active:!bg-red-500',
on_select: () => {handle_display_deletion(display_id)},
}
];
}
function select_all(current_displays: DisplayGroup[], current_selected_display_ids: string[]) { function select_all(current_displays: DisplayGroup[], current_selected_display_ids: string[]) {
const new_value = !all_selected(current_displays, current_selected_display_ids); const new_value = !all_selected(current_displays, current_selected_display_ids);
for (const display_group of current_displays) { for (const display_group of current_displays) {
@@ -89,17 +111,7 @@
click_function={(e) => { click_function={(e) => {
e.stopPropagation(); e.stopPropagation();
}} }}
menu_options={[ menu_options={get_display_menu_options($pinned_display_id)}
{
icon: Pencil,
name: 'Bildschirm bearbeiten'
},
{
icon: Trash2,
name: 'Bildschirm löschen',
class: 'text-red-400 hover:text-stone-200 hover:!bg-red-400 active:!bg-red-500'
}
]}
> >
<Menu /> <Menu />
</Button> </Button>
@@ -159,7 +171,7 @@
: 'Alle auswählen'}</span : 'Alle auswählen'}</span
> >
</button> </button>
<div class="flex flex-ro"> <div class="flex flex-row">
<Button <Button
title="Bildschirme größer darstellen" title="Bildschirme größer darstellen"
className="aspect-square !p-1 rounded-r-none" className="aspect-square !p-1 rounded-r-none"
@@ -206,8 +218,9 @@
> >
{#if $displays.length === 1 && $displays[0].data.length === 0} {#if $displays.length === 1 && $displays[0].data.length === 0}
<div class="text-stone-500 px-10 py-6 leading-relaxed text-center"> <div class="text-stone-500 px-10 py-6 leading-relaxed text-center">
Es wurden noch keine Bildschirme hinzugefügt. Klicke oben rechts auf <Settings class="inline pb-1"/> und "Neuen Es wurden noch keine Bildschirme hinzugefügt. Klicke oben rechts auf <Settings
Bildschirm hinzufügen". class="inline pb-1"
/> und "Neuen Bildschirm hinzufügen".
</div> </div>
{:else} {:else}
{#each $displays as display_group (display_group.id)} {#each $displays as display_group (display_group.id)}
@@ -217,7 +230,7 @@
animate:flip={{ duration: dnd_flip_duration_ms, easing: cubicOut }} animate:flip={{ duration: dnd_flip_duration_ms, easing: cubicOut }}
class="outline-none" class="outline-none"
> >
<DisplayGroupObject {display_group} /> <DisplayGroupObject {display_group} {get_display_menu_options} />
</section> </section>
{/each} {/each}
{/if} {/if}
+2 -2
View File
@@ -38,7 +38,7 @@
<!-- svelte-ignore a11y_no_static_element_interactions --> <!-- svelte-ignore a11y_no_static_element_interactions -->
<!-- svelte-ignore a11y_click_events_have_key_events --> <!-- svelte-ignore a11y_click_events_have_key_events -->
<div <div
class="bg-stone-800 rounded-2xl min-w-[30%] min-h-[30%] max-w-[80%] max-h-[80%] flex flex-col shadow-2xl/60 overflow-hidden" class="bg-stone-800 rounded-2xl min-w-[30%] max-w-[80%] max-h-[80%] flex flex-col shadow-2xl/60 overflow-hidden"
onclick={(e) => e.stopPropagation()} onclick={(e) => e.stopPropagation()}
> >
<div <div
@@ -62,7 +62,7 @@
</div> </div>
</div> </div>
<div class="p-2 min-h-0 overflow-auto flex flex-col gap-2"> <div class="p-2 min-h-0 overflow-auto flex flex-col gap-2">
{@render content.snippet()} {@render content.snippet(content.snippet_arg)}
</div> </div>
</div> </div>
</div> </div>
+98 -24
View File
@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { Monitor, Plus, Radio, Settings, X } from 'lucide-svelte'; import { Monitor, Plus, Radio, Settings, Trash2, X } from 'lucide-svelte';
import Button from '../components/Button.svelte'; import Button from '../components/Button.svelte';
import FileView from '../components/FileView.svelte'; import FileView from '../components/FileView.svelte';
import ControlView from '../components/ControlView.svelte'; import ControlView from '../components/ControlView.svelte';
@@ -8,7 +8,14 @@
import PopUp from '../components/PopUp.svelte'; import PopUp from '../components/PopUp.svelte';
import type { PopupContent } from '../ts/types'; import type { PopupContent } from '../ts/types';
import TextInput from '../components/TextInput.svelte'; import TextInput from '../components/TextInput.svelte';
import { add_display, is_display_name_taken } from '../ts/stores/displays'; import {
add_display,
displays,
edit_display_data,
get_display_by_id,
is_display_name_taken,
remove_display
} from '../ts/stores/displays';
import { text } from '@sveltejs/kit'; import { text } from '@sveltejs/kit';
const ip_regex = const ip_regex =
@@ -22,11 +29,12 @@
closable: true closable: true
}); });
let text_inputs_valid = $state({ const text_inputs_valid_null_values = {
name: { valid: false, value: '' }, name: { valid: false, value: '' },
ip: { valid: false, value: '' }, ip: { valid: false, value: '' },
mac: { valid: false, value: '' } mac: { valid: false, value: '' }
}); };
let text_inputs_valid = $state(text_inputs_valid_null_values);
function all_text_inputs_valid(): boolean { function all_text_inputs_valid(): boolean {
for (const entry of Object.values(text_inputs_valid)) { for (const entry of Object.values(text_inputs_valid)) {
@@ -37,17 +45,16 @@
return true; return true;
} }
function finalize_add_new_display() { function finalize_add_edit_display(existing_display_id: string|null) {
const ip = text_inputs_valid.ip.value; const ip = text_inputs_valid.ip.value;
const mac = text_inputs_valid.mac.value === '' ? null : text_inputs_valid.mac.value; const mac = text_inputs_valid.mac.value === '' ? null : text_inputs_valid.mac.value;
const name = text_inputs_valid.name.value; const name = text_inputs_valid.name.value;
add_display(ip, mac, name, 'Online'); if (!!existing_display_id) {
edit_display_data(existing_display_id, ip, mac, name);
} else {
add_display(ip, mac, name, 'Online');
}
popup_close_function(); popup_close_function();
text_inputs_valid = {
name: { valid: false, value: '' },
ip: { valid: false, value: '' },
mac: { valid: false, value: '' }
};
} }
function popup_close_function() { function popup_close_function() {
@@ -55,28 +62,81 @@
} }
const show_new_display_popup = () => { const show_new_display_popup = () => {
text_inputs_valid = text_inputs_valid_null_values;
popup_content = { popup_content = {
open: true, open: true,
snippet: add_new_display, snippet: add_new_display_popup,
title: 'Neuen Bildschirm hinzufügen', title: 'Neuen Bildschirm hinzufügen',
title_icon: Monitor, title_icon: Monitor,
closable: true closable: true
}; };
}; };
const show_remove_display_popup = (display_id: string) => {
popup_content = {
open: true,
snippet: remove_display_popup,
snippet_arg: display_id,
title: 'Bildschirm wirklich löschen?',
title_class: 'text-red-400',
title_icon: Trash2,
closable: true
};
};
const show_edit_display_popup = (display_id: string) => {
const display = get_display_by_id(display_id, $displays);
if (!display) return;
// insert existing values in text_inputs_valid
for (const key of Object.keys(text_inputs_valid) as (keyof typeof text_inputs_valid)[]) {
text_inputs_valid[key].valid = true;
text_inputs_valid[key].value = display[key] || '';
}
popup_content = {
open: true,
snippet: add_new_display_popup,
snippet_arg: display_id,
title: 'Bildschirm bearbeiten',
title_icon: Monitor,
closable: true
};
};
</script> </script>
{#snippet add_new_display()} {#snippet remove_display_popup(display_id: string)}
<div class="max-w-prose px-2">
Soll der Bildschirm "{get_display_by_id(display_id, $displays)?.name || '?'}" wirklich gelöscht
werden? Dadurch wird es von diesem Controller nicht mehr erreichbar. Die Installation auf dem
Gerät bleibt bestehen. Mit dem erneuten Hinzufügen des Bildschirms wird er wieder steuerbar.
</div>
<div class="flex flex-row justify-end gap-2">
<Button className="px-4 font-bold" click_function={popup_close_function}>Abbrechen</Button>
<Button
hover_bg="bg-red-400"
active_bg="bg-red-500"
className="px-4 flex text-red-400 hover:text-stone-100"
click_function={() => {
remove_display(display_id);
popup_close_function();
}}>Löschen</Button
>
</div>
{/snippet}
{#snippet add_new_display_popup(existing_display_id: string | null = null)}
<TextInput <TextInput
bind:current_value={text_inputs_valid.name.value} bind:current_value={text_inputs_valid.name.value}
bind:current_valid={text_inputs_valid.name.valid} bind:current_valid={text_inputs_valid.name.valid}
title="Anzeigename" title="Anzeigename"
placeholder="z.B. Beamer vorne links" placeholder="z.B. Beamer vorne links"
is_valid_function={(input: string) => { is_valid_function={(input: string) => {
return input.length === 0 || input.length > 50 if (!!existing_display_id) {
? [false, 'Ungültige Länge'] if (input === get_display_by_id(existing_display_id, $displays)?.name)
: is_display_name_taken(input) return [true, 'Gültiger Name'];
? [false, 'Name bereits verwendet'] }
: [true, 'Gültiger Name']; if (input.length === 0 || input.length > 50) return [false, 'Ungültige Länge'];
if (is_display_name_taken(input)) return [false, 'Name bereits verwendet'];
return [true, 'Gültiger Name'];
}} }}
/> />
<div class="flex flex-row gap-2"> <div class="flex flex-row gap-2">
@@ -111,18 +171,29 @@
: [false, 'Ungültige MAC-Adresse']; : [false, 'Ungültige MAC-Adresse'];
}} }}
/> />
<div class="flex justify-end pt-2"> <div class="flex flex-row gap-2 justify-end pt-2">
{#if !!existing_display_id}
<!-- TODO: Ping mit existing_display_id -->
<Button className="px-4" click_function={popup_close_function}>Abbrechen</Button>
{/if}
<Button <Button
disabled={!all_text_inputs_valid()} disabled={!all_text_inputs_valid()}
className="pl-3 pr-4 gap-2 font-bold" className="{!!existing_display_id ? 'px-4' : 'pl-3 pr-4 gap-2'} font-bold"
bg="bg-stone-650" bg="bg-stone-650"
click_function={finalize_add_new_display}><Plus /> Bildschirm hinzufügen</Button click_function={() => {
> finalize_add_edit_display(existing_display_id);
}}
>{#if !!existing_display_id}
Speichern
{:else}
<Plus /> Bildschirm hinzufügen
{/if}
</Button>
</div> </div>
{/snippet} {/snippet}
<main class="bg-stone-900 h-dvh w-dvw text-stone-200 px-4 py-2 gap-2 grid grid-rows-[3rem_auto]"> <main class="bg-stone-900 h-dvh w-dvw text-stone-200 px-4 py-2 gap-2 grid grid-rows-[3rem_auto]">
{@html SplashScreen} <!-- {@html SplashScreen} -->
<div class="w-[calc(100dvw-(8*var(--spacing)))] flex justify-between"> <div class="w-[calc(100dvw-(8*var(--spacing)))] flex justify-between">
<span class="text-4xl font-bold content-center pl-1"> PLG MuDiCS </span> <span class="text-4xl font-bold content-center pl-1"> PLG MuDiCS </span>
@@ -146,7 +217,10 @@
</Button> </Button>
</div> </div>
<div class="w-[calc(100dvw-(8*var(--spacing)))] grid grid-cols-2 gap-2"> <div class="w-[calc(100dvw-(8*var(--spacing)))] grid grid-cols-2 gap-2">
<DisplayView /> <DisplayView
handle_display_deletion={show_remove_display_popup}
handle_display_editing={show_edit_display_popup}
/>
<div <div
class="col-start-2 h-[calc(100dvh-3rem-(6*var(--spacing)))] rounded-2xl flex flex-col gap-2" class="col-start-2 h-[calc(100dvh-3rem-(6*var(--spacing)))] rounded-2xl flex flex-col gap-2"
> >
+31 -6
View File
@@ -11,19 +11,44 @@ export const displays: Writable<DisplayGroup[]> = writable<DisplayGroup[]>([{
export function is_display_name_taken(name: string): boolean { export function is_display_name_taken(name: string): boolean {
const display_groups = get(displays); const display_groups = get(displays);
return display_groups.some(group => return display_groups.some(group =>
group.data.some(display => display.name.trim().toLowerCase() === name.trim().toLowerCase()) group.data.some(display => display.name.trim().toLowerCase() === name.trim().toLowerCase())
); );
} }
export function add_display(ip: string, mac: string|null, name: string, status: string) { export function add_display(ip: string, mac: string | null, name: string, status: string) {
displays.update((displays: DisplayGroup[]) => { displays.update((displays: DisplayGroup[]) => {
displays[0].data.push({ id: get_uuid(), ip, preview_url: null, preview_timeout_id: null, mac, name, status }); displays[0].data.push({ id: get_uuid(), ip, preview_url: null, preview_timeout_id: null, mac, name, status });
return displays; return displays;
}); });
} }
export function edit_display_data(display_id: string, ip: string, mac: string | null, name: string) {
displays.update((display_groups) =>
display_groups.map((group) => ({
...group,
data: group.data.map((display) => {
if (display.id !== display_id) return display;
return { ...display, ip: ip, mac: mac, name: name };
}),
}))
);
}
export function remove_display(display_id: string) {
console.log(display_id);
displays.update((displays: DisplayGroup[]) => {
displays = displays.map(display_group => ({
...display_group,
data: display_group.data.filter(display => display.id !== display_id)
}));
return displays;
});
// TODO remove ID from Files usw.
}
export function all_displays_of_group_selected(display_group: DisplayGroup, current_selected_displays: string[]) { export function all_displays_of_group_selected(display_group: DisplayGroup, current_selected_displays: string[]) {
if (display_group.data.length === 0) return false; if (display_group.data.length === 0) return false;
for (const display of display_group.data) { for (const display of display_group.data) {
@@ -139,6 +164,6 @@ function add_testing_displays() {
// add_display("127.0.0.1", "00:1A:2B:3C:4D:5E", name, "Offline"); // add_display("127.0.0.1", "00:1A:2B:3C:4D:5E", name, "Offline");
// } // }
// add_display("127.0.0.1", "00:1A:2B:3C:4D:5E", "PC", "Offline"); add_display("127.0.0.1", "00:1A:2B:3C:4D:5E", "PC", "Offline");
// add_display("192.168.178.111", "D4:81:D7:C0:DF:3C", "Laptop", "Online"); // add_display("192.168.178.111", "D4:81:D7:C0:DF:3C", "Laptop", "Online");
} }
+4 -2
View File
@@ -1,4 +1,5 @@
import { FileBox, FileImage, FileVideoCamera, ImagePlay, type X } from "lucide-svelte"; import { FileBox, FileImage, FileVideoCamera, ImagePlay, type X } from "lucide-svelte";
import type { Snippet } from "svelte";
export type SupportedFileType = { export type SupportedFileType = {
display_name: display_name:
@@ -60,7 +61,7 @@ export type Display = {
ip: string; ip: string;
preview_url: string | null; preview_url: string | null;
preview_timeout_id: number | null; preview_timeout_id: number | null;
mac: string|null; mac: string | null;
name: string; name: string;
status: string; status: string;
} }
@@ -81,7 +82,8 @@ export type MenuOption = {
export type PopupContent = { export type PopupContent = {
open: boolean; open: boolean;
snippet: any; snippet: Snippet<[string]> | null;
snippet_arg?: string;
title: string; title: string;
title_class?: string; title_class?: string;
title_icon?: typeof X | null; title_icon?: typeof X | null;