691 lines
18 KiB
TypeScript
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>
|
|
);
|
|
}
|