generated from Dicken/dickendock
round19: Startseite aufgeraeumt, Favicon-Fallback ueber Ports, Favicon-Picker, Live-Status per Ping
This commit is contained in:
@@ -6,6 +6,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { Button, Favicon } from "@launchpad/ui";
|
||||
import type { Service } from "@launchpad/shared";
|
||||
import { useServices } from "../../hooks/useServices.js";
|
||||
import { useBookmarks } from "../../hooks/useBookmarks.js";
|
||||
import { useCategories } from "../../hooks/useCategories.js";
|
||||
import { useDevices } from "../../hooks/useDevices.js";
|
||||
import { AdminPageHeader } from "./AdminPageHeader.js";
|
||||
@@ -110,6 +111,8 @@ const EDIT_FORM_COLSPAN = 12;
|
||||
|
||||
function EditForm({ service, onDone }: { service: Service; onDone: () => void }) {
|
||||
const queryClient = useQueryClient();
|
||||
const { data: allServices } = useServices();
|
||||
const { data: allBookmarks } = useBookmarks();
|
||||
const [displayName, setDisplayName] = useState(service.displayName);
|
||||
const [category, setCategory] = useState(service.category ?? "");
|
||||
const [alias, setAlias] = useState(service.alias.join(", "));
|
||||
@@ -122,6 +125,22 @@ function EditForm({ service, onDone }: { service: Service; onDone: () => void })
|
||||
// string = neu hochgeladenes Favicon als data:-URL.
|
||||
const [favicon, setFavicon] = useState<string | null | undefined>(undefined);
|
||||
const [faviconError, setFaviconError] = useState<string | null>(null);
|
||||
const [pickerOpen, setPickerOpen] = useState(false);
|
||||
|
||||
// Bereits verwendete Favicons (Dienste + Lesezeichen), zur Auswahl im
|
||||
// "Vorhandenes Favicon wählen"-Picker - z. B. wenn mehrere Dienste
|
||||
// desselben Geräts eigentlich dasselbe Icon zeigen sollten, der Scan aber
|
||||
// nur bei einem davon eins gefunden hat.
|
||||
const existingFavicons = useMemo(() => {
|
||||
const seen = new Map<string, string>(); // favicon-URL -> Anzeigename für Tooltip
|
||||
for (const s of allServices ?? []) {
|
||||
if (s.favicon && s.id !== service.id && !seen.has(s.favicon)) seen.set(s.favicon, s.displayName);
|
||||
}
|
||||
for (const b of allBookmarks ?? []) {
|
||||
if (b.favicon && !seen.has(b.favicon)) seen.set(b.favicon, b.displayName);
|
||||
}
|
||||
return Array.from(seen.entries());
|
||||
}, [allServices, allBookmarks, service.id]);
|
||||
|
||||
const FAVICON_MAX_BYTES = 300 * 1024;
|
||||
|
||||
@@ -192,6 +211,16 @@ function EditForm({ service, onDone }: { service: Service; onDone: () => void })
|
||||
onChange={(e) => handleFaviconFile(e.target.files?.[0])}
|
||||
/>
|
||||
</label>
|
||||
{existingFavicons.length > 0 ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setPickerOpen((v) => !v)}
|
||||
className="rounded-lg border border-black/10 px-2 py-1 text-xs text-black/70
|
||||
hover:bg-black/5 dark:border-white/10 dark:text-white/70 dark:hover:bg-white/10"
|
||||
>
|
||||
Vorhandenes wählen
|
||||
</button>
|
||||
) : null}
|
||||
{faviconPreview ? (
|
||||
<button
|
||||
type="button"
|
||||
@@ -203,6 +232,25 @@ function EditForm({ service, onDone }: { service: Service; onDone: () => void })
|
||||
) : null}
|
||||
</div>
|
||||
{faviconError ? <p className="mt-1 text-xs text-red-500">{faviconError}</p> : null}
|
||||
{pickerOpen ? (
|
||||
<div className="mt-2 flex max-w-xs flex-wrap gap-1.5 rounded-lg border border-black/10 p-2
|
||||
dark:border-white/10">
|
||||
{existingFavicons.map(([iconUrl, name]) => (
|
||||
<button
|
||||
key={iconUrl}
|
||||
type="button"
|
||||
title={name}
|
||||
onClick={() => {
|
||||
setFavicon(iconUrl);
|
||||
setPickerOpen(false);
|
||||
}}
|
||||
className="rounded p-0.5 hover:bg-black/5 dark:hover:bg-white/10"
|
||||
>
|
||||
<Favicon src={iconUrl} fallbackLetter={name} size="sm" />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div>
|
||||
<label className="mb-1 block text-xs text-black/50 dark:text-white/50">Name</label>
|
||||
|
||||
Reference in New Issue
Block a user