Files
whattoplay/src/pages/Settings/SettingsDetailPage.tsx

691 lines
18 KiB
TypeScript

import React, { useEffect, useMemo, useState } from "react";
import {
IonAlert,
IonBackButton,
IonButton,
IonButtons,
IonContent,
IonHeader,
IonIcon,
IonInput,
IonItem,
IonLabel,
IonList,
IonPage,
IonSelect,
IonSelectOption,
IonText,
IonTitle,
IonToolbar,
} from "@ionic/react";
import {
informationCircleOutline,
refreshOutline,
saveOutline,
settingsOutline,
shareOutline,
timeOutline,
trashOutline,
} from "ionicons/icons";
import { useParams } from "react-router-dom";
import {
ConfigService,
type ServiceConfig,
} from "../../services/ConfigService";
import { db } from "../../services/Database";
import "./SettingsDetailPage.css";
interface SettingsRouteParams {
serviceId: string;
}
const SERVICE_META = {
steam: {
title: "Steam",
description: "Deine Steam-Bibliothek",
tutorialKey: "steam",
},
gog: {
title: "GOG",
description: "GOG Galaxy Bibliothek",
tutorialKey: "gog",
},
epic: {
title: "Epic Games",
description: "Epic Games Launcher",
tutorialKey: "epic",
},
amazon: {
title: "Amazon Games",
description: "Prime Gaming / Luna",
tutorialKey: "amazon",
},
blizzard: {
title: "Blizzard",
description: "Battle.net / WoW / Diablo",
tutorialKey: "blizzard",
},
data: {
title: "Datenverwaltung",
description: "Export, Import und Reset",
tutorialKey: null,
},
} as const;
type ServiceId = keyof typeof SERVICE_META;
const PROVIDER_IDS = ["steam", "gog", "epic", "amazon", "blizzard"] as const;
export default function SettingsDetailPage() {
const { serviceId } = useParams<SettingsRouteParams>();
const [config, setConfig] = useState<ServiceConfig>({});
const [showAlert, setShowAlert] = useState(false);
const [alertMessage, setAlertMessage] = useState("");
const [apiOutput, setApiOutput] = useState<string>("");
const meta = useMemo(() => SERVICE_META[serviceId as ServiceId], [serviceId]);
useEffect(() => {
const loadConfig = async () => {
let loadedConfig = await ConfigService.loadConfig();
// Test-Modus: Lade config.local.json wenn --test Parameter gesetzt
const isTestMode = new URLSearchParams(window.location.search).has(
"test",
);
if (isTestMode) {
try {
const response = await fetch("/api/config/load");
const responseText = await response.text();
console.log("API Response Status:", response.status);
console.log("API Response Text:", responseText);
if (response.ok) {
const testConfig = JSON.parse(responseText);
loadedConfig = { ...loadedConfig, ...testConfig };
console.log("Test-Modus: Geladene Config:", loadedConfig);
setAlertMessage("🛠️ Test-Modus: config.local.json geladen");
setShowAlert(true);
} else {
console.error(
"API /api/config/load fehlgeschlagen:",
response.status,
responseText,
);
}
} catch (error) {
console.error(
"Test-Modus aktiv, aber config.local.json nicht ladbar",
error,
);
}
}
// Query-Parameter für Steam-Seite übernehmen
if (serviceId === "steam") {
const query = new URLSearchParams(window.location.search);
const steamIdParam = query.get("steamid") ?? query.get("steamId") ?? "";
const apiKeyParam = query.get("apikey") ?? query.get("apiKey") ?? "";
if (steamIdParam || apiKeyParam) {
loadedConfig = {
...loadedConfig,
steam: {
...loadedConfig.steam,
...(steamIdParam ? { steamId: steamIdParam } : {}),
...(apiKeyParam ? { apiKey: apiKeyParam } : {}),
},
};
}
}
setConfig(loadedConfig);
};
loadConfig();
}, [serviceId]);
const handleDraftChange = (service: keyof ServiceConfig, data: any) => {
setConfig((prev) => ({
...prev,
[service]: { ...prev[service], ...data },
}));
};
const handleSaveService = async (service: keyof ServiceConfig) => {
await ConfigService.saveConfig(config);
setAlertMessage(`${service.toUpperCase()} Einstellungen gespeichert`);
setShowAlert(true);
// Automatisch Daten abrufen nach dem Speichern
if (service === "steam") {
await handleManualRefresh(service);
}
};
const handleManualRefresh = async (service: keyof ServiceConfig) => {
setApiOutput("Rufe API auf...");
try {
if (service === "steam") {
const steamConfig = config.steam;
if (!steamConfig?.apiKey || !steamConfig?.steamId) {
setApiOutput("❌ Fehler: Steam API Key und Steam ID erforderlich");
setAlertMessage("Bitte zuerst Steam-Zugangsdaten eingeben");
setShowAlert(true);
return;
}
// Rufe Backend-Endpoint auf (statt direkt Steam API wegen CORS)
const response = await fetch("/api/steam/refresh", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
apiKey: steamConfig.apiKey,
steamId: steamConfig.steamId,
}),
});
if (!response.ok) {
const errorText = await response.text();
setApiOutput(`❌ API Fehler: ${response.status}\n${errorText}`);
setAlertMessage("Steam Refresh fehlgeschlagen");
setShowAlert(true);
return;
}
const result = await response.json();
// Spiele in Database speichern
await db.saveGames(result.games);
setApiOutput(
`${result.games.length} Spiele geladen und gespeichert\n\nBeispiel:\n${JSON.stringify(result.games.slice(0, 2), null, 2)}`,
);
}
const updatedConfig = {
...config,
[service]: {
...config[service],
lastRefresh: new Date().toISOString(),
},
};
setConfig(updatedConfig);
await ConfigService.saveConfig(updatedConfig);
setAlertMessage(`${service.toUpperCase()} aktualisiert`);
setShowAlert(true);
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error);
setApiOutput(
`❌ Fehler: ${errorMsg}\n\n💡 Tipp: Führe stattdessen im Terminal aus:\nnpm run fetch:steam`,
);
setAlertMessage("Aktualisierung fehlgeschlagen");
setShowAlert(true);
}
};
const formatLastRefresh = (value?: string) => {
if (!value) return "Nie";
const date = new Date(value);
if (Number.isNaN(date.getTime())) return "Unbekannt";
return new Intl.DateTimeFormat("de-DE", {
dateStyle: "medium",
timeStyle: "short",
}).format(date);
};
const handleExportConfig = () => {
const validation = ConfigService.validateConfig(config);
if (!validation.valid) {
setAlertMessage(
`⚠️ Config unvollständig:\n${validation.errors.join("\n")}`,
);
setShowAlert(true);
return;
}
ConfigService.exportConfig(config);
setAlertMessage("✓ Config exportiert");
setShowAlert(true);
};
const handleImportConfig = async (
event: React.ChangeEvent<HTMLInputElement>,
) => {
const file = event.target.files?.[0];
if (!file) return;
const imported = await ConfigService.importConfig(file);
if (imported) {
setConfig(imported);
setAlertMessage("✓ Config importiert");
} else {
setAlertMessage("❌ Import fehlgeschlagen");
}
setShowAlert(true);
};
const handleClearConfig = async () => {
await ConfigService.clearConfig();
await db.clear();
setConfig({});
setApiOutput("");
setAlertMessage("✓ Alle Einstellungen und Spiele gelöscht");
setShowAlert(true);
};
if (!meta) {
return (
<IonPage>
<IonHeader>
<IonToolbar>
<IonButtons slot="start">
<IonBackButton defaultHref="/settings" />
</IonButtons>
<IonTitle>Einstellungen</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent fullscreen>
<div className="settings-detail-empty">
<IonText color="medium">Unbekannter Bereich.</IonText>
</div>
</IonContent>
</IonPage>
);
}
const isProvider = (PROVIDER_IDS as readonly string[]).includes(serviceId);
const providerKey = isProvider ? (serviceId as keyof ServiceConfig) : null;
const lastRefresh = providerKey
? config[providerKey]?.lastRefresh
: undefined;
return (
<IonPage>
<IonHeader>
<IonToolbar>
<IonButtons slot="start">
<IonBackButton defaultHref="/settings" />
</IonButtons>
<IonTitle>{meta.title}</IonTitle>
{isProvider && providerKey && (
<IonButtons slot="end">
<IonButton
fill="clear"
aria-label="Manuell aktualisieren"
onClick={() => handleManualRefresh(providerKey)}
>
<IonIcon icon={refreshOutline} />
</IonButton>
</IonButtons>
)}
</IonToolbar>
</IonHeader>
<IonContent fullscreen>
<div className="settings-detail-header">
<IonIcon icon={settingsOutline} />
<div>
<h2>{meta.title}</h2>
<p>{meta.description}</p>
</div>
</div>
{serviceId === "steam" && (
<>
<IonList inset>
<IonItem>
<IonLabel position="stacked">Steam API Key</IonLabel>
<IonInput
type="text"
placeholder="Dein Steam Web API Key"
value={config.steam?.apiKey || ""}
onIonChange={(e) =>
handleDraftChange("steam", {
apiKey: e.detail.value || "",
})
}
/>
</IonItem>
<IonItem>
<IonLabel position="stacked">Steam Profil URL oder ID</IonLabel>
<IonInput
placeholder="steamcommunity.com/id/deinname oder Steam ID"
value={config.steam?.steamId || ""}
onIonChange={(e) => {
const input = e.detail.value || "";
// Extract Steam ID from URL if provided
const idMatch = input.match(/\/(id|profiles)\/(\w+)/);
const extractedId = idMatch ? idMatch[2] : input;
handleDraftChange("steam", {
steamId: extractedId,
});
}}
/>
</IonItem>
</IonList>
<IonList inset>
<IonItem
button
detail
routerLink={`/settings/${serviceId}/tutorial`}
routerDirection="forward"
>
<IonLabel>Anleitung anzeigen</IonLabel>
</IonItem>
</IonList>
<div className="settings-detail-actions">
<IonButton
expand="block"
onClick={() => handleSaveService("steam")}
>
<IonIcon slot="start" icon={saveOutline} />
<IonLabel>Speichern</IonLabel>
</IonButton>
</div>
</>
)}
{serviceId === "gog" && (
<>
<IonList inset>
<IonItem>
<IonLabel position="stacked">GOG User ID</IonLabel>
<IonInput
type="text"
placeholder="galaxyUserId"
value={config.gog?.userId || ""}
onIonChange={(e) =>
handleDraftChange("gog", {
userId: e.detail.value || "",
})
}
/>
</IonItem>
<IonItem>
<IonLabel position="stacked">Access Token</IonLabel>
<IonInput
type="text"
placeholder="Bearer token"
value={config.gog?.accessToken || ""}
onIonChange={(e) =>
handleDraftChange("gog", {
accessToken: e.detail.value || "",
})
}
/>
</IonItem>
</IonList>
<IonList inset>
<IonItem
button
detail
routerLink={`/settings/${serviceId}/tutorial`}
routerDirection="forward"
>
<IonLabel>Anleitung anzeigen</IonLabel>
</IonItem>
</IonList>
<div className="settings-detail-actions">
<IonButton
expand="block"
onClick={() => handleSaveService("gog")}
>
<IonIcon slot="start" icon={saveOutline} />
<IonLabel>Speichern</IonLabel>
</IonButton>
</div>
</>
)}
{serviceId === "epic" && (
<>
<IonList inset>
<IonItem>
<IonLabel position="stacked">Account E-Mail</IonLabel>
<IonInput
type="email"
placeholder="dein@email.com"
value={config.epic?.email || ""}
onIonChange={(e) =>
handleDraftChange("epic", {
email: e.detail.value || "",
})
}
/>
</IonItem>
<IonItem>
<IonLabel>Import-Methode</IonLabel>
<IonSelect
value={config.epic?.method || "manual"}
onIonChange={(e) =>
handleDraftChange("epic", {
method: e.detail.value || "manual",
})
}
>
<IonSelectOption value="manual">
Manuelle JSON-Upload
</IonSelectOption>
<IonSelectOption value="oauth">
OAuth (benötigt Backend)
</IonSelectOption>
</IonSelect>
</IonItem>
</IonList>
<IonItem lines="none" className="settings-detail-note">
<IonIcon icon={informationCircleOutline} />
<IonText>
Epic hat keine öffentliche API. Nutze manuellen Import oder
Backend OAuth.
</IonText>
</IonItem>
<IonList inset>
<IonItem
button
detail
routerLink={`/settings/${serviceId}/tutorial`}
routerDirection="forward"
>
<IonLabel>Anleitung anzeigen</IonLabel>
</IonItem>
</IonList>
<div className="settings-detail-actions">
<IonButton
expand="block"
onClick={() => handleSaveService("epic")}
>
<IonIcon slot="start" icon={saveOutline} />
<IonLabel>Speichern</IonLabel>
</IonButton>
</div>
</>
)}
{serviceId === "amazon" && (
<>
<IonList inset>
<IonItem>
<IonLabel position="stacked">Account E-Mail</IonLabel>
<IonInput
type="email"
placeholder="dein@amazon.com"
value={config.amazon?.email || ""}
onIonChange={(e) =>
handleDraftChange("amazon", {
email: e.detail.value || "",
})
}
/>
</IonItem>
<IonItem>
<IonLabel>Import-Methode</IonLabel>
<IonSelect
value={config.amazon?.method || "manual"}
onIonChange={(e) =>
handleDraftChange("amazon", {
method: e.detail.value || "manual",
})
}
>
<IonSelectOption value="manual">
Manuelle JSON-Upload
</IonSelectOption>
<IonSelectOption value="oauth">
OAuth (benötigt Backend)
</IonSelectOption>
</IonSelect>
</IonItem>
</IonList>
<IonList inset>
<IonItem
button
detail
routerLink={`/settings/${serviceId}/tutorial`}
routerDirection="forward"
>
<IonLabel>Anleitung anzeigen</IonLabel>
</IonItem>
</IonList>
<div className="settings-detail-actions">
<IonButton
expand="block"
onClick={() => handleSaveService("amazon")}
>
<IonIcon slot="start" icon={saveOutline} />
<IonLabel>Speichern</IonLabel>
</IonButton>
</div>
</>
)}
{serviceId === "blizzard" && (
<>
<IonList inset>
<IonItem>
<IonLabel position="stacked">Client ID</IonLabel>
<IonInput
type="text"
placeholder="your_client_id"
value={config.blizzard?.clientId || ""}
onIonChange={(e) =>
handleDraftChange("blizzard", {
clientId: e.detail.value || "",
})
}
/>
</IonItem>
<IonItem>
<IonLabel position="stacked">Client Secret</IonLabel>
<IonInput
type="text"
placeholder="your_client_secret"
value={config.blizzard?.clientSecret || ""}
onIonChange={(e) =>
handleDraftChange("blizzard", {
clientSecret: e.detail.value || "",
})
}
/>
</IonItem>
<IonItem>
<IonLabel>Region</IonLabel>
<IonSelect
value={config.blizzard?.region || "eu"}
onIonChange={(e) =>
handleDraftChange("blizzard", {
region: e.detail.value || "eu",
})
}
>
<IonSelectOption value="us">🇺🇸 North America</IonSelectOption>
<IonSelectOption value="eu">🇪🇺 Europe</IonSelectOption>
<IonSelectOption value="kr">🇰🇷 Korea</IonSelectOption>
<IonSelectOption value="tw">🇹🇼 Taiwan</IonSelectOption>
</IonSelect>
</IonItem>
</IonList>
<IonList inset>
<IonItem
button
detail
routerLink={`/settings/${serviceId}/tutorial`}
routerDirection="forward"
>
<IonLabel>Anleitung anzeigen</IonLabel>
</IonItem>
</IonList>
<div className="settings-detail-actions">
<IonButton
expand="block"
onClick={() => handleSaveService("blizzard")}
>
<IonIcon slot="start" icon={saveOutline} />
<IonLabel>Speichern</IonLabel>
</IonButton>
</div>
</>
)}
{serviceId === "data" && (
<>
<IonList inset>
<IonItem button onClick={handleExportConfig}>
<IonLabel>Config exportieren</IonLabel>
<IonIcon slot="end" icon={shareOutline} />
</IonItem>
<IonItem className="settings-detail-file-item">
<IonLabel>Config importieren</IonLabel>
<input
type="file"
accept=".json"
onChange={handleImportConfig}
className="settings-detail-file-input"
/>
</IonItem>
</IonList>
<div className="settings-detail-actions">
<IonButton
expand="block"
color="danger"
onClick={() => handleClearConfig()}
>
<IonIcon slot="start" icon={trashOutline} />
Alle Einstellungen löschen
</IonButton>
</div>
</>
)}
{isProvider && (
<>
<div className="settings-detail-last-refresh">
<IonText color="medium">
<IonIcon icon={timeOutline} /> Letzter Abruf:{" "}
{formatLastRefresh(lastRefresh)}
</IonText>
</div>
{apiOutput && (
<div className="settings-detail-api-output">
<IonText color="medium">
<strong>API Response:</strong>
</IonText>
<pre>{apiOutput}</pre>
</div>
)}
</>
)}
<div style={{ paddingBottom: "24px" }} />
</IonContent>
<IonAlert
isOpen={showAlert}
onDidDismiss={() => setShowAlert(false)}
message={alertMessage}
buttons={["OK"]}
/>
</IonPage>
);
}