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;
|
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 */
|
/** Messung zum Protokoll hinzufügen */
|
||||||
export function addMeasurement(
|
export function addMeasurement(
|
||||||
protocol: Protocol,
|
protocol: Protocol,
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,10 @@
|
||||||
import ToolDialog from '$lib/components/ToolDialog.svelte';
|
import ToolDialog from '$lib/components/ToolDialog.svelte';
|
||||||
import MeasurementResult from '$lib/components/MeasurementResult.svelte';
|
import MeasurementResult from '$lib/components/MeasurementResult.svelte';
|
||||||
import DeviceCard from '$lib/components/DeviceCard.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 { 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 { sync } from '$lib/sync.svelte';
|
||||||
import { toast } from '$lib/toast.svelte';
|
import { toast } from '$lib/toast.svelte';
|
||||||
import { pushOverlay } from '$lib/overlay.svelte';
|
import { pushOverlay } from '$lib/overlay.svelte';
|
||||||
|
|
@ -22,12 +24,21 @@
|
||||||
let activeTool = $state<Tool | null>(null);
|
let activeTool = $state<Tool | null>(null);
|
||||||
let activeDevice = $state<Device | undefined>(undefined);
|
let activeDevice = $state<Device | undefined>(undefined);
|
||||||
let saving = $state(false);
|
let saving = $state(false);
|
||||||
|
let renameTarget = $state<Device | null>(null);
|
||||||
|
let confirmDelete = $state(false);
|
||||||
|
|
||||||
let appStateListener: PluginListenerHandle | null = null;
|
let appStateListener: PluginListenerHandle | null = null;
|
||||||
|
|
||||||
const protocolTools = TOOLS.filter((t) => t.scope === 'protocol');
|
const protocolTools = TOOLS.filter((t) => t.scope === 'protocol');
|
||||||
const deviceTools = TOOLS.filter((t) => t.scope === 'device');
|
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 ampel = ['ampel-ok', 'ampel-warn', 'ampel-fail'];
|
||||||
const ampelDot = ['bg-emerald-500', 'bg-amber-400', 'bg-red-500'];
|
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 (!protocol) return;
|
||||||
if (!confirm('Dieses Protokoll wirklich löschen?')) return;
|
|
||||||
await deleteProtocol(protocol.clientUuid);
|
await deleteProtocol(protocol.clientUuid);
|
||||||
await sync.refreshPending();
|
await sync.refreshPending();
|
||||||
goto('/auftraege/');
|
goto('/auftraege/');
|
||||||
|
|
@ -227,18 +254,20 @@
|
||||||
Noch keine Geräte — IP-Scanner ausführen, um das Netz zu erfassen.
|
Noch keine Geräte — IP-Scanner ausführen, um das Netz zu erfassen.
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
{#each protocol.devices as device (device.clientId)}
|
{#each sortedDevices as device (device.clientId)}
|
||||||
<DeviceCard
|
<DeviceCard
|
||||||
{device}
|
{device}
|
||||||
measurements={measurementsFor(device.clientId)}
|
measurements={measurementsFor(device.clientId)}
|
||||||
tools={deviceTools}
|
tools={deviceTools}
|
||||||
onrun={(tool) => openTool(tool, device)}
|
onrun={(tool) => openTool(tool, device)}
|
||||||
|
onfavorite={() => doToggleFav(device)}
|
||||||
|
onrename={() => (renameTarget = device)}
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div class="px-3">
|
<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
|
Protokoll löschen
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -264,6 +293,28 @@
|
||||||
onrun={runTool}
|
onrun={runTool}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/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}
|
{:else}
|
||||||
<div class="flex min-h-screen items-center justify-center text-zinc-500">Lädt …</div>
|
<div class="flex min-h-screen items-center justify-center text-zinc-500">Lädt …</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue