Geräte als Favoriten markieren und benennen
- Favoriten-Stern je Gerät; favorisierte Geräte werden in der Liste oben einsortiert. Favorit + eigener Name bleiben im Protokoll erhalten, auch ohne gespeicherten Scan-Snapshot - Gerät umbenennen (customName) über neuen TextPromptDialog - toggleFavorite / renameDevice in protocols.ts - TextPromptDialog + ConfirmDialog: schlanke Modal-Komponenten als Ersatz für die verbotenen Browser-prompt()/confirm(); melden sich als Overlay an (Hardware-Backbutton schließt sie) - Protokoll-Löschen nutzt jetzt ConfirmDialog statt confirm() Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
50793e4e5d
commit
484b5f96fa
4 changed files with 197 additions and 5 deletions
56
src/lib/components/ConfirmDialog.svelte
Normal file
56
src/lib/components/ConfirmDialog.svelte
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
<script lang="ts">
|
||||
/**
|
||||
* Bestätigungs-Modal — ersetzt das im Projekt verbotene `confirm()`.
|
||||
* Meldet sich als Overlay an, damit der Hardware-Backbutton es schließt.
|
||||
*/
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { pushOverlay } from '$lib/overlay.svelte';
|
||||
|
||||
let {
|
||||
title,
|
||||
message = '',
|
||||
confirmLabel = 'OK',
|
||||
danger = false,
|
||||
onconfirm,
|
||||
oncancel,
|
||||
}: {
|
||||
title: string;
|
||||
message?: string;
|
||||
confirmLabel?: string;
|
||||
danger?: boolean;
|
||||
onconfirm: () => void;
|
||||
oncancel: () => void;
|
||||
} = $props();
|
||||
|
||||
let off: (() => void) | undefined;
|
||||
onMount(() => {
|
||||
off = pushOverlay(oncancel);
|
||||
});
|
||||
onDestroy(() => off?.());
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-6"
|
||||
role="presentation"
|
||||
onclick={(e) => {
|
||||
if (e.target === e.currentTarget) oncancel();
|
||||
}}
|
||||
>
|
||||
<div class="w-full max-w-sm rounded-lg bg-zinc-900 p-4">
|
||||
<h2 class="text-sm font-semibold text-zinc-200">{title}</h2>
|
||||
{#if message}<p class="mt-1 text-xs text-zinc-400">{message}</p>{/if}
|
||||
<div class="mt-3 flex justify-end gap-2">
|
||||
<button class="rounded px-3 py-1.5 text-sm text-zinc-400" onclick={oncancel}>
|
||||
Abbrechen
|
||||
</button>
|
||||
<button
|
||||
class="rounded px-3 py-1.5 text-sm font-medium text-white {danger
|
||||
? 'bg-red-600'
|
||||
: 'bg-sky-600'}"
|
||||
onclick={onconfirm}
|
||||
>
|
||||
{confirmLabel}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
73
src/lib/components/TextPromptDialog.svelte
Normal file
73
src/lib/components/TextPromptDialog.svelte
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
<script lang="ts">
|
||||
/**
|
||||
* Schlankes Eingabe-Modal für eine einzelne Textzeile (Gerät benennen,
|
||||
* Scan benennen …). Ersetzt das im Projekt verbotene `prompt()`.
|
||||
*
|
||||
* Meldet sich als Overlay an, damit der Hardware-Backbutton es schließt.
|
||||
*/
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { pushOverlay } from '$lib/overlay.svelte';
|
||||
|
||||
let {
|
||||
title,
|
||||
label = '',
|
||||
value = '',
|
||||
placeholder = '',
|
||||
submitLabel = 'Speichern',
|
||||
onsubmit,
|
||||
oncancel,
|
||||
}: {
|
||||
title: string;
|
||||
label?: string;
|
||||
value?: string;
|
||||
placeholder?: string;
|
||||
submitLabel?: string;
|
||||
onsubmit: (value: string) => void;
|
||||
oncancel: () => void;
|
||||
} = $props();
|
||||
|
||||
let text = $state(value);
|
||||
let off: (() => void) | undefined;
|
||||
|
||||
onMount(() => {
|
||||
off = pushOverlay(oncancel);
|
||||
});
|
||||
onDestroy(() => off?.());
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-6"
|
||||
role="presentation"
|
||||
onclick={(e) => {
|
||||
if (e.target === e.currentTarget) oncancel();
|
||||
}}
|
||||
>
|
||||
<div class="w-full max-w-sm rounded-lg bg-zinc-900 p-4">
|
||||
<h2 class="mb-2 text-sm font-semibold text-zinc-200">{title}</h2>
|
||||
{#if label}
|
||||
<label class="mb-1 block text-xs text-zinc-400" for="tp-input">{label}</label>
|
||||
{/if}
|
||||
<!-- svelte-ignore a11y_autofocus -->
|
||||
<input
|
||||
id="tp-input"
|
||||
class="w-full rounded border border-zinc-700 bg-zinc-800 px-2 py-1.5 text-sm text-zinc-100"
|
||||
bind:value={text}
|
||||
{placeholder}
|
||||
autofocus
|
||||
onkeydown={(e) => {
|
||||
if (e.key === 'Enter') onsubmit(text.trim());
|
||||
}}
|
||||
/>
|
||||
<div class="mt-3 flex justify-end gap-2">
|
||||
<button class="rounded px-3 py-1.5 text-sm text-zinc-400" onclick={oncancel}>
|
||||
Abbrechen
|
||||
</button>
|
||||
<button
|
||||
class="rounded bg-sky-600 px-3 py-1.5 text-sm font-medium text-white"
|
||||
onclick={() => onsubmit(text.trim())}
|
||||
>
|
||||
{submitLabel}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -61,6 +61,18 @@ export function upsertDevice(
|
|||
return created;
|
||||
}
|
||||
|
||||
/** Favoriten-Markierung eines Geräts umschalten */
|
||||
export function toggleFavorite(protocol: Protocol, clientId: string): void {
|
||||
const d = protocol.devices.find((x) => x.clientId === clientId);
|
||||
if (d) d.isFavorite = !d.isFavorite;
|
||||
}
|
||||
|
||||
/** Einem Gerät einen eigenen Namen geben (leerer Name = zurücksetzen) */
|
||||
export function renameDevice(protocol: Protocol, clientId: string, name: string): void {
|
||||
const d = protocol.devices.find((x) => x.clientId === clientId);
|
||||
if (d) d.customName = name.trim() || undefined;
|
||||
}
|
||||
|
||||
/** Messung zum Protokoll hinzufügen */
|
||||
export function addMeasurement(
|
||||
protocol: Protocol,
|
||||
|
|
|
|||
|
|
@ -8,8 +8,10 @@
|
|||
import ToolDialog from '$lib/components/ToolDialog.svelte';
|
||||
import MeasurementResult from '$lib/components/MeasurementResult.svelte';
|
||||
import DeviceCard from '$lib/components/DeviceCard.svelte';
|
||||
import TextPromptDialog from '$lib/components/TextPromptDialog.svelte';
|
||||
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
|
||||
import { getProtocol, saveProtocol, deleteProtocol } from '$lib/db';
|
||||
import { addMeasurement, upsertDevice } from '$lib/protocols';
|
||||
import { addMeasurement, upsertDevice, toggleFavorite, renameDevice } from '$lib/protocols';
|
||||
import { sync } from '$lib/sync.svelte';
|
||||
import { toast } from '$lib/toast.svelte';
|
||||
import { pushOverlay } from '$lib/overlay.svelte';
|
||||
|
|
@ -22,12 +24,21 @@
|
|||
let activeTool = $state<Tool | null>(null);
|
||||
let activeDevice = $state<Device | undefined>(undefined);
|
||||
let saving = $state(false);
|
||||
let renameTarget = $state<Device | null>(null);
|
||||
let confirmDelete = $state(false);
|
||||
|
||||
let appStateListener: PluginListenerHandle | null = null;
|
||||
|
||||
const protocolTools = TOOLS.filter((t) => t.scope === 'protocol');
|
||||
const deviceTools = TOOLS.filter((t) => t.scope === 'device');
|
||||
|
||||
/** Geräte mit Favoriten zuerst */
|
||||
const sortedDevices = $derived(
|
||||
[...(protocol?.devices ?? [])].sort(
|
||||
(a, b) => Number(b.isFavorite ?? false) - Number(a.isFavorite ?? false),
|
||||
),
|
||||
);
|
||||
|
||||
const ampel = ['ampel-ok', 'ampel-warn', 'ampel-fail'];
|
||||
const ampelDot = ['bg-emerald-500', 'bg-amber-400', 'bg-red-500'];
|
||||
|
||||
|
|
@ -127,9 +138,25 @@
|
|||
);
|
||||
}
|
||||
|
||||
async function removeProtocol() {
|
||||
/** Favoriten-Stern eines Geräts umschalten */
|
||||
function doToggleFav(device: Device) {
|
||||
if (!protocol) return;
|
||||
toggleFavorite(protocol, device.clientId);
|
||||
void persist();
|
||||
}
|
||||
|
||||
/** Gerät umbenennen (aus dem Namens-Dialog) */
|
||||
function doRename(name: string) {
|
||||
if (protocol && renameTarget) {
|
||||
renameDevice(protocol, renameTarget.clientId, name);
|
||||
void persist();
|
||||
}
|
||||
renameTarget = null;
|
||||
}
|
||||
|
||||
async function doDelete() {
|
||||
confirmDelete = false;
|
||||
if (!protocol) return;
|
||||
if (!confirm('Dieses Protokoll wirklich löschen?')) return;
|
||||
await deleteProtocol(protocol.clientUuid);
|
||||
await sync.refreshPending();
|
||||
goto('/auftraege/');
|
||||
|
|
@ -227,18 +254,20 @@
|
|||
Noch keine Geräte — IP-Scanner ausführen, um das Netz zu erfassen.
|
||||
</p>
|
||||
{/if}
|
||||
{#each protocol.devices as device (device.clientId)}
|
||||
{#each sortedDevices as device (device.clientId)}
|
||||
<DeviceCard
|
||||
{device}
|
||||
measurements={measurementsFor(device.clientId)}
|
||||
tools={deviceTools}
|
||||
onrun={(tool) => openTool(tool, device)}
|
||||
onfavorite={() => doToggleFav(device)}
|
||||
onrename={() => (renameTarget = device)}
|
||||
/>
|
||||
{/each}
|
||||
</section>
|
||||
|
||||
<div class="px-3">
|
||||
<button class="text-xs text-red-400 underline" onclick={removeProtocol}>
|
||||
<button class="text-xs text-red-400 underline" onclick={() => (confirmDelete = true)}>
|
||||
Protokoll löschen
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -264,6 +293,28 @@
|
|||
onrun={runTool}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if renameTarget}
|
||||
<TextPromptDialog
|
||||
title="Gerät benennen"
|
||||
label="Eigener Name"
|
||||
value={renameTarget.customName ?? ''}
|
||||
placeholder={renameTarget.hostname ?? renameTarget.ip}
|
||||
onsubmit={doRename}
|
||||
oncancel={() => (renameTarget = null)}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if confirmDelete}
|
||||
<ConfirmDialog
|
||||
title="Protokoll löschen?"
|
||||
message="Das Protokoll und alle Messungen werden lokal entfernt."
|
||||
confirmLabel="Löschen"
|
||||
danger
|
||||
onconfirm={doDelete}
|
||||
oncancel={() => (confirmDelete = false)}
|
||||
/>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="flex min-h-screen items-center justify-center text-zinc-500">Lädt …</div>
|
||||
{/if}
|
||||
|
|
|
|||
Loading…
Reference in a new issue