mirror of
https://codeberg.org/PLG-Development/PLG-MuDiCS
synced 2026-07-05 16:37:09 +00:00
425 lines
13 KiB
Svelte
425 lines
13 KiB
Svelte
<script lang="ts">
|
|
import {
|
|
Monitor,
|
|
Plus,
|
|
Radio,
|
|
Settings,
|
|
Trash2,
|
|
Menu,
|
|
ChevronDown,
|
|
SquareCheckBig,
|
|
Square,
|
|
Info
|
|
} from 'lucide-svelte';
|
|
import Button from '$lib/components/Button.svelte';
|
|
import FileView from './FileView.svelte';
|
|
import ControlView from './ControlView.svelte';
|
|
import DisplayView from './DisplayView.svelte';
|
|
import PopUp from '$lib/components/PopUp.svelte';
|
|
import { type Display, type PopupContent } from '$lib/ts/types';
|
|
import TextInput from '$lib/components/TextInput.svelte';
|
|
import {
|
|
add_display,
|
|
edit_display_data,
|
|
get_display_by_id,
|
|
is_display_name_taken,
|
|
remove_display,
|
|
screenshot_loop
|
|
} from '$lib/ts/stores/displays';
|
|
import { notifications } from '$lib/ts/stores/notification';
|
|
import { ping_ip } from '$lib/ts/api_handler';
|
|
import { onMount } from 'svelte';
|
|
import { on_app_start, update_display_status } from '$lib/ts/main';
|
|
import { display_status_to_info } from '$lib/ts/utils';
|
|
import HighlightedText from '$lib/components/HighlightedText.svelte';
|
|
import { preview_settings } from '$lib/ts/stores/ui_behavior';
|
|
import NumberSettingInput from '$lib/components/NumberSettingInput.svelte';
|
|
import { db } from '$lib/ts/database';
|
|
import version from './../../../../shared/version.txt?raw';
|
|
|
|
const ip_regex =
|
|
/^(?:(?:10|127)\.(?:25[0-5]|2[0-4]\d|1?\d?\d)\.(?:25[0-5]|2[0-4]\d|1?\d?\d)\.(?:25[0-5]|2[0-4]\d|1?\d?\d)|192\.168\.(?:25[0-5]|2[0-4]\d|1?\d?\d)\.(?:25[0-5]|2[0-4]\d|1?\d?\d)|172\.(?:1[6-9]|2\d|3[0-1])\.(?:25[0-5]|2[0-4]\d|1?\d?\d)\.(?:25[0-5]|2[0-4]\d|1?\d?\d))$/;
|
|
const mac_regex = /^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/;
|
|
|
|
let popup_content: PopupContent = $state({
|
|
open: false,
|
|
snippet: null,
|
|
title: '',
|
|
title_class: '!text-xl'
|
|
});
|
|
let remove_display_name = $state('');
|
|
|
|
const text_inputs_valid_null_values = {
|
|
name: { valid: false, value: '' },
|
|
ip: { valid: false, value: '' },
|
|
mac: { valid: false, value: '' }
|
|
};
|
|
let text_inputs_valid = $state(text_inputs_valid_null_values);
|
|
|
|
function all_text_inputs_valid(): boolean {
|
|
for (const entry of Object.values(text_inputs_valid)) {
|
|
if (!entry.valid) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
async function finalize_add_edit_display(existing_display_id: string | null) {
|
|
popup_close_function();
|
|
const ip = text_inputs_valid.ip.value;
|
|
const mac = text_inputs_valid.mac.value === '' ? null : text_inputs_valid.mac.value;
|
|
const name = text_inputs_valid.name.value;
|
|
let display: Display | null = null;
|
|
if (existing_display_id) {
|
|
display = await edit_display_data(existing_display_id, ip, mac, name);
|
|
} else {
|
|
const resp = await ping_ip(text_inputs_valid.ip.value);
|
|
display = await add_display(ip, mac, name, resp.status);
|
|
}
|
|
if (display) {
|
|
await update_display_status(display);
|
|
}
|
|
}
|
|
|
|
function get_display_preview_mode(mode: 'never' | 'normal' | 'always') {
|
|
switch (mode) {
|
|
case 'never':
|
|
return 'Nie';
|
|
case 'normal':
|
|
return 'Normal';
|
|
case 'always':
|
|
return 'Dauerhaft';
|
|
}
|
|
}
|
|
|
|
async function change_preview_mode(mode: 'never' | 'normal' | 'always') {
|
|
$preview_settings.mode = mode;
|
|
if (mode === 'never') {
|
|
await db.displays
|
|
.toCollection()
|
|
.modify({ preview: { currently_updating: false, url: null } });
|
|
} else {
|
|
const display_ids = (await db.displays.toArray()).map((d) => d.id);
|
|
for (const display_id of display_ids) {
|
|
screenshot_loop(display_id);
|
|
}
|
|
}
|
|
}
|
|
|
|
function popup_close_function() {
|
|
popup_content.open = false;
|
|
}
|
|
|
|
const show_new_display_popup = () => {
|
|
text_inputs_valid = text_inputs_valid_null_values;
|
|
popup_content = {
|
|
open: true,
|
|
snippet: display_popup,
|
|
title: 'Neuen Bildschirm Hinzufügen',
|
|
title_icon: Monitor,
|
|
title_class: '!text-xl',
|
|
window_class: 'w-3xl'
|
|
};
|
|
};
|
|
|
|
const show_settings_popup = () => {
|
|
popup_content = {
|
|
open: true,
|
|
snippet: settings_popup,
|
|
title: 'Einstellungen',
|
|
title_icon: Settings,
|
|
title_class: '!text-xl',
|
|
window_class: 'w-3xl'
|
|
};
|
|
};
|
|
|
|
const show_remove_display_popup = async (display_id: string) => {
|
|
remove_display_name = (await get_display_by_id(display_id))?.name || '?';
|
|
popup_content = {
|
|
open: true,
|
|
snippet: remove_display_popup,
|
|
snippet_arg: display_id,
|
|
title: 'Bildschirm wirklich löschen?',
|
|
title_class: 'text-red-400 !text-xl',
|
|
title_icon: Trash2
|
|
};
|
|
};
|
|
|
|
const show_edit_display_popup = async (display_id: string) => {
|
|
const display = await get_display_by_id(display_id);
|
|
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: display_popup,
|
|
snippet_arg: display_id,
|
|
title: 'Bildschirm bearbeiten',
|
|
title_icon: Monitor,
|
|
title_class: '!text-xl'
|
|
};
|
|
};
|
|
|
|
onMount(on_app_start);
|
|
|
|
const show_about_popup = () => {
|
|
popup_content = {
|
|
open: true,
|
|
snippet: about_popup,
|
|
title: 'Über PLG MuDiCS',
|
|
title_icon: Info,
|
|
title_class: '!text-xl'
|
|
};
|
|
};
|
|
</script>
|
|
|
|
<!-- eslint-disable-next-line @typescript-eslint/no-unused-vars -->
|
|
{#snippet about_popup(_: string)}
|
|
<div class="px-2">
|
|
<p>
|
|
{version}
|
|
</p>
|
|
<h3 class="text-lg font-bold mt-4">Entwickler</h3>
|
|
<p>
|
|
<a target="_blank" class="link" href="https://github.com/programmer-44">E44</a>,
|
|
<a target="_blank" class="link" href="https://codeberg.org/2mal3">2mal3</a>
|
|
</p>
|
|
|
|
<h3 class="text-lg font-bold mt-4">Lizenz</h3>
|
|
<a
|
|
target="_blank"
|
|
class="link"
|
|
href="https://github.com/PLG-Development/PLG-MuDiCS/blob/main/LICENSE.txt"
|
|
>
|
|
GNU Affero General Public License v3 (AGPL-3.0)
|
|
</a>
|
|
|
|
<h3 class="text-lg font-bold mt-4">Verwendete Bibliotheken</h3>
|
|
<ul class="list-disc list-inside">
|
|
<li><a target="_blank" href="https://svelte.dev/" class="link">Svelte & SvelteKit</a></li>
|
|
<li><a target="_blank" href="https://tailwindcss.com/" class="link">TailwindCSS</a></li>
|
|
<li><a target="_blank" href="https://lucide.dev/" class="link">Lucide Icons</a></li>
|
|
<li><a target="_blank" href="https://tiptap.dev/" class="link">Tiptap</a></li>
|
|
<li><a target="_blank" href="https://dexie.org/" class="link">Dexie.js</a></li>
|
|
<li>
|
|
<a target="_blank" href="https://github.com/orefalo/svelte-splitpanes" class="link"
|
|
>svelte-splitpanes</a
|
|
>
|
|
</li>
|
|
<li>
|
|
<a target="_blank" href="https://github.com/thisux/sveltednd" class="link"
|
|
>@thisux/sveltednd</a
|
|
>
|
|
</li>
|
|
<li><a target="_blank" href="https://echo.labstack.com/" class="link">Echo</a></li>
|
|
<li><a target="_blank" href="https://github.com/mdlayher/wol" class="link">wol</a></li>
|
|
</ul>
|
|
</div>
|
|
<div class="flex justify-end pt-2">
|
|
<Button click_function={popup_close_function} className="px-4">Schließen</Button>
|
|
</div>
|
|
{/snippet}
|
|
|
|
{#snippet remove_display_popup(display_id: string)}
|
|
<div class="max-w-prose px-2">
|
|
Soll der Bildschirm <HighlightedText>{remove_display_name}</HighlightedText> 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="button space font-bold" click_function={popup_close_function}>
|
|
Abbrechen
|
|
</Button>
|
|
<Button
|
|
className="error button space"
|
|
click_function={async () => {
|
|
popup_close_function();
|
|
await remove_display(display_id);
|
|
}}>Löschen</Button
|
|
>
|
|
</div>
|
|
{/snippet}
|
|
|
|
{#snippet display_popup(existing_display_id: string | null = null)}
|
|
<TextInput
|
|
focused_on_start
|
|
bind:current_value={text_inputs_valid.name.value}
|
|
bind:current_valid={text_inputs_valid.name.valid}
|
|
title="Anzeigename"
|
|
placeholder="z.B. Beamer vorne links"
|
|
is_valid_function={async (input: string) => {
|
|
if (existing_display_id) {
|
|
if (input === (await get_display_by_id(existing_display_id))?.name)
|
|
return [true, 'Gültiger Name'];
|
|
}
|
|
if (input.length === 0 || input.length > 50) return [false, 'Ungültige Länge'];
|
|
if (await is_display_name_taken(input)) return [false, 'Name bereits verwendet'];
|
|
return [true, 'Gültiger Name'];
|
|
}}
|
|
enter_mode="focus_next"
|
|
/>
|
|
<div class="flex flex-row gap-2">
|
|
<TextInput
|
|
bind:current_value={text_inputs_valid.ip.value}
|
|
bind:current_valid={text_inputs_valid.ip.valid}
|
|
title="IP-Adresse"
|
|
placeholder="z.B. 192.168.176.111"
|
|
is_valid_function={(input: string) => {
|
|
return ip_regex.test(input)
|
|
? [true, 'Gültige IP-Adresse']
|
|
: [false, 'Ungültige IP-Adresse'];
|
|
}}
|
|
className="grow"
|
|
enter_mode="focus_next"
|
|
/>
|
|
<div class="flex items-end shrink-0">
|
|
<Button
|
|
disabled={!text_inputs_valid.ip.valid}
|
|
className="px-4 gap-2"
|
|
bg="bg-stone-750"
|
|
click_function={async () => {
|
|
const resp = await ping_ip(text_inputs_valid.ip.value);
|
|
notifications.push(
|
|
'info',
|
|
`Ping '${text_inputs_valid.ip.value}'`,
|
|
`Aktueller Zustand: ${display_status_to_info(resp.status)}`
|
|
);
|
|
}}><Radio /> Ping</Button
|
|
>
|
|
</div>
|
|
</div>
|
|
<TextInput
|
|
bind:current_value={text_inputs_valid.mac.value}
|
|
bind:current_valid={text_inputs_valid.mac.valid}
|
|
title="MAC-Adresse (optional, wird zum aufwecken des Displays benötigt)"
|
|
placeholder="z.B. D4:81:A6:C4:BF:3F"
|
|
is_valid_function={(input: string) => {
|
|
return input === ''
|
|
? [true, 'Keine MAC-Adresse (WOL deaktiviert)']
|
|
: mac_regex.test(input)
|
|
? [true, 'Gültige MAC-Adresse']
|
|
: [false, 'Ungültige MAC-Adresse'];
|
|
}}
|
|
enter_mode="submit"
|
|
enter_function={async () => {
|
|
await finalize_add_edit_display(existing_display_id);
|
|
}}
|
|
/>
|
|
<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
|
|
disabled={!all_text_inputs_valid()}
|
|
className="{existing_display_id ? 'px-4' : 'pl-3 pr-4 gap-2'} font-bold"
|
|
bg="bg-stone-650"
|
|
click_function={async () => {
|
|
await finalize_add_edit_display(existing_display_id);
|
|
}}
|
|
>{#if !!existing_display_id}
|
|
Speichern
|
|
{:else}
|
|
<Plus /> Bildschirm hinzufügen
|
|
{/if}
|
|
</Button>
|
|
</div>
|
|
{/snippet}
|
|
|
|
{#snippet settings_popup()}
|
|
<div class="flex flex-col gap-2 pl-1">
|
|
<span class="font-bold text-lg">Vorschau-Verhalten</span>
|
|
<div class="flex flex-col gap-2 ml-2">
|
|
<span class="text-stone-400 text-sm max-w-prose"
|
|
>Die Vorschau eines Bildschirms ist das Bild, welches links neben dem Display-Namen zu sehen
|
|
ist. Es zeigt relativ aktuell das an, was auf dem jeweiligen Bildschirm zu sehen ist.</span
|
|
>
|
|
<div class="flex flex-row justify-between items-center">
|
|
<span>Aktualisierungs-Verhalten</span>
|
|
<Button
|
|
className="gap-3 pl-4 pr-3 w-35"
|
|
menu_options={(['never', 'normal', 'always'] as const).map((mode) => ({
|
|
icon: mode === $preview_settings.mode ? SquareCheckBig : Square,
|
|
name: get_display_preview_mode(mode),
|
|
on_select: async () => await change_preview_mode(mode)
|
|
}))}>{get_display_preview_mode($preview_settings.mode)} <ChevronDown /></Button
|
|
>
|
|
</div>
|
|
<div class="flex flex-row justify-between items-center">
|
|
<span>Intervall zwischen den Aktualisierungs-Anfragen</span>
|
|
<NumberSettingInput
|
|
disabled={$preview_settings.mode === 'never'}
|
|
number_setting={$preview_settings.retry_seconds}
|
|
on_change={(new_value: number) => {
|
|
$preview_settings.retry_seconds.now = new_value;
|
|
}}
|
|
/>
|
|
</div>
|
|
<div class="flex flex-row justify-between items-center max-w-full gap-8">
|
|
<span class="">Anzahl der änderungslosen Aktualisierungen bis pausiert wird</span>
|
|
<NumberSettingInput
|
|
disabled={$preview_settings.mode !== 'normal'}
|
|
number_setting={$preview_settings.retry_count}
|
|
on_change={(new_value: number) => {
|
|
$preview_settings.retry_count.now = new_value;
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="flex justify-end pt-4">
|
|
<Button click_function={popup_close_function} className="px-4">Schließen</Button>
|
|
</div>
|
|
{/snippet}
|
|
|
|
<main class="bg-stone-900 h-dvh w-dvw text-stone-200 px-4 py-2 gap-2 grid grid-rows-[3rem_auto]">
|
|
<div class="w-[calc(100dvw-(8*var(--spacing)))] flex justify-between">
|
|
<span class="text-4xl font-bold content-center pl-1"> PLG MuDiCS </span>
|
|
<Button
|
|
className="aspect-square"
|
|
div_class="aspect-square"
|
|
menu_options={[
|
|
{
|
|
icon: Plus,
|
|
name: 'Neuen Bildschirm Hinzufügen',
|
|
on_select: show_new_display_popup
|
|
},
|
|
{
|
|
icon: Settings,
|
|
name: 'Einstellungen',
|
|
on_select: show_settings_popup
|
|
},
|
|
{
|
|
icon: Info,
|
|
name: 'Über',
|
|
on_select: show_about_popup
|
|
}
|
|
]}
|
|
>
|
|
<Menu />
|
|
</Button>
|
|
</div>
|
|
<div class="w-[calc(100dvw-(8*var(--spacing)))] grid grid-cols-2 gap-2">
|
|
<DisplayView
|
|
handle_display_deletion={show_remove_display_popup}
|
|
handle_display_editing={show_edit_display_popup}
|
|
/>
|
|
<div
|
|
class="col-start-2 h-[calc(100dvh-3rem-(6*var(--spacing)))] rounded-2xl flex flex-col gap-2"
|
|
>
|
|
<ControlView />
|
|
<FileView />
|
|
</div>
|
|
</div>
|
|
<PopUp
|
|
content={popup_content}
|
|
close_function={popup_close_function}
|
|
className="bg-white/10"
|
|
snippet_container_class="min-w-115"
|
|
/>
|
|
</main>
|