start using claude code
This commit is contained in:
1462
package-lock.json
generated
1462
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
12
package.json
12
package.json
@@ -8,7 +8,14 @@
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"test": "node --test server/**/*.test.mjs",
|
||||
"test:steam": "node scripts/steam-cli.mjs"
|
||||
"oauth": "node workers/oauth-proxy.mjs",
|
||||
"worker:dev": "wrangler dev --config workers/wrangler.toml",
|
||||
"worker:deploy": "wrangler deploy --config workers/wrangler.toml",
|
||||
"fetch:steam": "node scripts/fetch-steam.mjs",
|
||||
"fetch:gog": "node scripts/fetch-gog.mjs",
|
||||
"fetch:epic": "node scripts/fetch-epic.mjs",
|
||||
"fetch:amazon": "node scripts/fetch-amazon.mjs",
|
||||
"fetch:all": "node scripts/fetch-all.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ionic/react": "^8.0.0",
|
||||
@@ -26,6 +33,7 @@
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^5.0.0"
|
||||
"vite": "^5.0.0",
|
||||
"wrangler": "^4.63.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname, join } from "node:path";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
const runScript = (scriptName) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const scriptPath = join(__dirname, scriptName);
|
||||
const child = spawn("node", [scriptPath], {
|
||||
stdio: "inherit",
|
||||
cwd: __dirname,
|
||||
});
|
||||
|
||||
child.on("close", (code) => {
|
||||
if (code === 0) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(`${scriptName} exited with code ${code}`));
|
||||
}
|
||||
});
|
||||
|
||||
child.on("error", reject);
|
||||
});
|
||||
};
|
||||
|
||||
const run = async () => {
|
||||
console.log("Starte alle API-Importer...\n");
|
||||
|
||||
try {
|
||||
await runScript("fetch-steam.mjs");
|
||||
await runScript("fetch-epic.mjs");
|
||||
await runScript("fetch-gog.mjs");
|
||||
await runScript("fetch-blizzard.mjs");
|
||||
console.log("\n✓ Alle Importer erfolgreich ausgeführt.");
|
||||
} catch (error) {
|
||||
console.error("\n✗ Fehler beim Ausführen der Importer:", error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
run();
|
||||
@@ -1,183 +0,0 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
/**
|
||||
* Blizzard Account Library Importer
|
||||
* Nutzt OAuth 2.0 für Authentifizierung
|
||||
*
|
||||
* Unterstützt:
|
||||
* - World of Warcraft
|
||||
* - Diablo
|
||||
* - Overwatch
|
||||
* - StarCraft
|
||||
* - Warcraft III
|
||||
* - Heroes of the Storm
|
||||
* - Hearthstone
|
||||
*/
|
||||
|
||||
const loadConfig = () => {
|
||||
const configPath = path.join(process.cwd(), "config.local.json");
|
||||
try {
|
||||
if (fs.existsSync(configPath)) {
|
||||
return JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("⚠️ Config nicht lesbar, nutze Defaults");
|
||||
}
|
||||
return {
|
||||
blizzard: {
|
||||
clientId: "",
|
||||
clientSecret: "",
|
||||
accountName: "",
|
||||
region: "eu",
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const fetchBlizzardGames = async ({ clientId, clientSecret, region }) => {
|
||||
// OAuth 2.0 Token Endpoint
|
||||
const tokenUrl = `https://${region}.battle.net/oauth/token`;
|
||||
const libraryUrl = `https://${region}.api.blizzard.com/d3/profile/${clientId}/hero`;
|
||||
|
||||
try {
|
||||
// Schritt 1: Bearer Token holen (Client Credentials Flow)
|
||||
const tokenResponse = await fetch(tokenUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
Authorization: `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString("base64")}`,
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
grant_type: "client_credentials",
|
||||
scope: "d3.profile.us",
|
||||
}),
|
||||
});
|
||||
|
||||
if (!tokenResponse.ok) {
|
||||
throw new Error(
|
||||
`Token-Fehler: ${tokenResponse.status} - ${await tokenResponse.text()}`,
|
||||
);
|
||||
}
|
||||
|
||||
const { access_token } = await tokenResponse.json();
|
||||
|
||||
// Schritt 2: Games/Accountinfo laden
|
||||
const gamesResponse = await fetch(libraryUrl, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${access_token}`,
|
||||
"User-Agent": "WhatToPlay/1.0",
|
||||
},
|
||||
});
|
||||
|
||||
if (!gamesResponse.ok) {
|
||||
console.warn(
|
||||
`⚠️ Blizzard API: ${gamesResponse.status} - Möglicherweise falscher Region oder Credentials`,
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
const data = await gamesResponse.json();
|
||||
|
||||
// Blizzard gibt Heros statt Games zurück
|
||||
// Wir extrahieren Informationen über verfügbare Spiele
|
||||
return data.heroes || [];
|
||||
} catch (error) {
|
||||
console.error(`❌ Blizzard Fehler: ${error.message}`);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const buildBlizzardEntry = (hero, gameType = "Diablo III") => ({
|
||||
id: `blizzard-${hero.id}`,
|
||||
title: `${gameType} - ${hero.name}`,
|
||||
platform: "Blizzard",
|
||||
class: hero.class,
|
||||
level: hero.level,
|
||||
experience: hero.experience,
|
||||
killed: hero.kills?.elites || 0,
|
||||
hardcore: hero.hardcore || false,
|
||||
lastPlayed: hero.lastUpdated
|
||||
? new Date(hero.lastUpdated).toISOString()
|
||||
: null,
|
||||
url: `https://www.diablo3.com/en/profile/${hero.id}/`,
|
||||
});
|
||||
|
||||
const buildTextFile = (game) => {
|
||||
const lines = [
|
||||
`# ${game.title}`,
|
||||
"",
|
||||
`**Plattform**: ${game.platform}`,
|
||||
`**Charaktertyp**: ${game.class || "Unbekannt"}`,
|
||||
`**Level**: ${game.level || "N/A"}`,
|
||||
game.hardcore ? `**Hardcore**: Ja ⚔️` : "",
|
||||
`**Elite-Kills**: ${game.killed || 0}`,
|
||||
`**Erfahrung**: ${game.experience || 0}`,
|
||||
game.lastPlayed
|
||||
? `**Zuletzt gespielt**: ${new Date(game.lastPlayed).toLocaleDateString("de-DE")}`
|
||||
: "",
|
||||
"",
|
||||
`[Im Profil anschauen](${game.url})`,
|
||||
];
|
||||
|
||||
return lines.filter(Boolean).join("\n");
|
||||
};
|
||||
|
||||
const writeBlizzardData = async (games) => {
|
||||
const dataDir = path.join(process.cwd(), "public/data");
|
||||
const textDir = path.join(dataDir, "blizzard-text");
|
||||
|
||||
// Stelle sicher dass Verzeichnisse existieren
|
||||
if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true });
|
||||
if (!fs.existsSync(textDir)) fs.mkdirSync(textDir, { recursive: true });
|
||||
|
||||
// Schreibe JSON-Datei
|
||||
fs.writeFileSync(
|
||||
path.join(dataDir, "blizzard.json"),
|
||||
JSON.stringify(games, null, 2),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
// Schreibe Text-Dateien für jeden Hero
|
||||
games.forEach((game) => {
|
||||
const textFile = `${game.id}.txt`;
|
||||
const filePath = path.join(textDir, textFile);
|
||||
const content = buildTextFile(game);
|
||||
fs.writeFileSync(filePath, content, "utf-8");
|
||||
});
|
||||
|
||||
return games.length;
|
||||
};
|
||||
|
||||
const main = async () => {
|
||||
const config = loadConfig();
|
||||
const { clientId, clientSecret, region } = config.blizzard || {};
|
||||
|
||||
if (!clientId || !clientSecret) {
|
||||
console.log(
|
||||
"⚠️ Blizzard: Keine Credentials - Überspringe\n → Für iOS/Web: Backend mit OAuth benötigt\n → Siehe docs/BLIZZARD-SETUP.md für Development-Setup",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("⏳ Blizzard-Games laden...");
|
||||
const games = await fetchBlizzardGames({
|
||||
clientId,
|
||||
clientSecret,
|
||||
region: region || "eu",
|
||||
});
|
||||
|
||||
if (games.length === 0) {
|
||||
console.log(
|
||||
"⚠️ Keine Blizzard-Games gefunden\n → Stelle sicher dass der Account mit Heros in Diablo III hat",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Verarbeite jeden Hero
|
||||
const processedGames = games.map((hero) => buildBlizzardEntry(hero));
|
||||
|
||||
const count = await writeBlizzardData(processedGames);
|
||||
console.log(`✓ Blizzard-Export fertig: ${count} Charaktere`);
|
||||
};
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -1,96 +0,0 @@
|
||||
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
|
||||
const loadConfig = async () => {
|
||||
const configUrl = new URL("../config.local.json", import.meta.url);
|
||||
try {
|
||||
const raw = await readFile(configUrl, "utf-8");
|
||||
return JSON.parse(raw);
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
const sanitizeFileName = (value) => {
|
||||
const normalized = value
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/^-+|-+$/g, "");
|
||||
return normalized || "spiel";
|
||||
};
|
||||
|
||||
const fetchEpicGames = async ({ accountId, accessToken }) => {
|
||||
// ⚠️ Epic Games Store hat KEINE öffentliche API!
|
||||
// Legendary (Python CLI) funktioniert nicht auf iOS/Web
|
||||
// Lösung: Backend mit Epic OAuth oder manuelle Import-Funktion
|
||||
console.warn("⚠️ Epic Games: Keine öffentliche API verfügbar");
|
||||
console.log(" → Für iOS/Web: Backend mit Epic OAuth benötigt");
|
||||
console.log(" → Alternative: Manuelle Library-Import-Funktion\n");
|
||||
return [];
|
||||
};
|
||||
|
||||
const buildEpicEntry = (game) => ({
|
||||
id: game.id || game.catalogItemId,
|
||||
title: game.title || game.displayName,
|
||||
platform: "PC",
|
||||
lastPlayed: game.lastPlayed || null,
|
||||
playtimeHours: game.playtimeMinutes
|
||||
? Math.round((game.playtimeMinutes / 60) * 10) / 10
|
||||
: 0,
|
||||
tags: game.categories || [],
|
||||
url: game.productSlug
|
||||
? `https://store.epicgames.com/en-US/p/${game.productSlug}`
|
||||
: null,
|
||||
});
|
||||
|
||||
const buildTextFile = (entry) => {
|
||||
const lines = [
|
||||
`Titel: ${entry.title}`,
|
||||
`Epic ID: ${entry.id}`,
|
||||
`Zuletzt gespielt: ${entry.lastPlayed ?? "-"}`,
|
||||
`Spielzeit (h): ${entry.playtimeHours ?? 0}`,
|
||||
`Store: ${entry.url ?? "-"}`,
|
||||
"Quelle: epic",
|
||||
];
|
||||
return lines.join("\n") + "\n";
|
||||
};
|
||||
|
||||
const writeOutputs = async (entries) => {
|
||||
const dataDir = new URL("../public/data/", import.meta.url);
|
||||
const textDir = new URL("../public/data/epic-text/", import.meta.url);
|
||||
|
||||
await mkdir(dataDir, { recursive: true });
|
||||
await mkdir(textDir, { recursive: true });
|
||||
|
||||
const jsonPath = new URL("epic.json", dataDir);
|
||||
await writeFile(jsonPath, JSON.stringify(entries, null, 2) + "\n", "utf-8");
|
||||
|
||||
await Promise.all(
|
||||
entries.map(async (entry) => {
|
||||
const fileName = `${sanitizeFileName(entry.title)}__${entry.id}.txt`;
|
||||
const filePath = new URL(fileName, textDir);
|
||||
await writeFile(filePath, buildTextFile(entry), "utf-8");
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const run = async () => {
|
||||
const config = await loadConfig();
|
||||
const accountId = config.epic?.accountId || process.env.EPIC_ACCOUNT_ID;
|
||||
const accessToken = config.epic?.accessToken || process.env.EPIC_ACCESS_TOKEN;
|
||||
|
||||
if (!accountId || !accessToken) {
|
||||
console.warn(
|
||||
"Epic-Zugangsdaten nicht gesetzt. Erstelle leere Datei als Platzhalter.",
|
||||
);
|
||||
}
|
||||
|
||||
const games = await fetchEpicGames({ accountId, accessToken });
|
||||
const entries = games.map(buildEpicEntry);
|
||||
await writeOutputs(entries);
|
||||
console.log(`Epic-Export fertig: ${entries.length} Spiele.`);
|
||||
};
|
||||
|
||||
run().catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,112 +0,0 @@
|
||||
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
|
||||
const loadConfig = async () => {
|
||||
const configUrl = new URL("../config.local.json", import.meta.url);
|
||||
try {
|
||||
const raw = await readFile(configUrl, "utf-8");
|
||||
return JSON.parse(raw);
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
const sanitizeFileName = (value) => {
|
||||
const normalized = value
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/^-+|-+$/g, "");
|
||||
return normalized || "spiel";
|
||||
};
|
||||
|
||||
const fetchGogGames = async ({ userId, accessToken }) => {
|
||||
if (!userId || !accessToken) {
|
||||
console.warn("⚠️ GOG: Keine Credentials - Überspringe");
|
||||
console.log(" → Für iOS/Web: Backend mit OAuth benötigt");
|
||||
console.log(" → Development: Token aus Browser DevTools kopieren\n");
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
// GOG Galaxy Library API (wie Heroic Launcher)
|
||||
const url = `https://galaxy-library.gog.com/users/${userId}/releases`;
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"User-Agent": "WhatToPlay/1.0",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`GOG API Fehler: ${response.status}`);
|
||||
}
|
||||
|
||||
const payload = await response.json();
|
||||
|
||||
// Galaxy API gibt items zurück, nicht owned
|
||||
return payload.items || [];
|
||||
} catch (error) {
|
||||
console.error("GOG API-Aufruf fehlgeschlagen:", error.message);
|
||||
console.log("💡 Tipp: Token abgelaufen? Neu aus gog.com holen\n");
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const buildGogEntry = (game) => ({
|
||||
// Galaxy Library API gibt external_id (GOG Product ID)
|
||||
id: String(game.external_id || game.id),
|
||||
title: game.title || `GOG Game ${game.external_id}`,
|
||||
platform: "PC",
|
||||
lastPlayed: game.date_created
|
||||
? new Date(game.date_created * 1000).toISOString()
|
||||
: null,
|
||||
playtimeHours: 0, // Galaxy API hat keine Spielzeit in /releases endpoint
|
||||
tags: [],
|
||||
url: `https://www.gog.com/game/${game.external_id}`,
|
||||
});
|
||||
|
||||
const buildTextFile = (entry) => {
|
||||
const lines = [
|
||||
`Titel: ${entry.title}`,
|
||||
`GOG ID: ${entry.id}`,
|
||||
`Zuletzt gespielt: ${entry.lastPlayed ?? "-"}`,
|
||||
`Spielzeit (h): ${entry.playtimeHours ?? 0}`,
|
||||
`Store: ${entry.url}`,
|
||||
"Quelle: gog",
|
||||
];
|
||||
return lines.join("\n") + "\n";
|
||||
};
|
||||
|
||||
const writeOutputs = async (entries) => {
|
||||
const dataDir = new URL("../public/data/", import.meta.url);
|
||||
const textDir = new URL("../public/data/gog-text/", import.meta.url);
|
||||
|
||||
await mkdir(dataDir, { recursive: true });
|
||||
await mkdir(textDir, { recursive: true });
|
||||
|
||||
const jsonPath = new URL("gog.json", dataDir);
|
||||
await writeFile(jsonPath, JSON.stringify(entries, null, 2) + "\n", "utf-8");
|
||||
|
||||
await Promise.all(
|
||||
entries.map(async (entry) => {
|
||||
const fileName = `${sanitizeFileName(entry.title)}__${entry.id}.txt`;
|
||||
const filePath = new URL(fileName, textDir);
|
||||
await writeFile(filePath, buildTextFile(entry), "utf-8");
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const run = async () => {
|
||||
const config = await loadConfig();
|
||||
const userId = config.gog?.userId || process.env.GOG_USER_ID;
|
||||
const accessToken = config.gog?.accessToken || process.env.GOG_ACCESS_TOKEN;
|
||||
|
||||
const games = await fetchGogGames({ userId, accessToken });
|
||||
const entries = games.map(buildGogEntry);
|
||||
await writeOutputs(entries);
|
||||
console.log(`GOG-Export fertig: ${entries.length} Spiele.`);
|
||||
};
|
||||
|
||||
run().catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -23,7 +23,6 @@ import LibraryPage from "./pages/Library/LibraryPage";
|
||||
import PlaylistsPage from "./pages/Playlists/PlaylistsPage";
|
||||
import SettingsPage from "./pages/Settings/SettingsPage";
|
||||
import SettingsDetailPage from "./pages/Settings/SettingsDetailPage";
|
||||
import SettingsTutorialPage from "./pages/Settings/SettingsTutorialPage";
|
||||
|
||||
import "./App.css";
|
||||
|
||||
@@ -43,11 +42,6 @@ export default function App() {
|
||||
path="/settings/:serviceId"
|
||||
component={SettingsDetailPage}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/settings/:serviceId/tutorial"
|
||||
component={SettingsTutorialPage}
|
||||
/>
|
||||
<Route exact path="/">
|
||||
<Redirect to="/home" />
|
||||
</Route>
|
||||
|
||||
@@ -1,141 +0,0 @@
|
||||
import React from "react";
|
||||
import {
|
||||
IonPopover,
|
||||
IonHeader,
|
||||
IonToolbar,
|
||||
IonTitle,
|
||||
IonContent,
|
||||
IonButtons,
|
||||
IonButton,
|
||||
IonIcon,
|
||||
IonCard,
|
||||
IonCardContent,
|
||||
IonCardHeader,
|
||||
IonCardTitle,
|
||||
IonText,
|
||||
} from "@ionic/react";
|
||||
import { closeOutline } from "ionicons/icons";
|
||||
|
||||
import { TUTORIALS, type TutorialStep } from "../data/tutorials";
|
||||
|
||||
interface TutorialModalProps {
|
||||
service: string | null;
|
||||
onClose: () => void;
|
||||
anchorEvent?: Event;
|
||||
}
|
||||
|
||||
export default function TutorialModal({
|
||||
service,
|
||||
onClose,
|
||||
anchorEvent,
|
||||
}: TutorialModalProps) {
|
||||
const tutorial = service ? TUTORIALS[service] : null;
|
||||
|
||||
return (
|
||||
<IonPopover
|
||||
isOpen={!!service}
|
||||
event={anchorEvent}
|
||||
onDidDismiss={onClose}
|
||||
showBackdrop
|
||||
>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonTitle>
|
||||
<IonIcon icon={tutorial?.icon} /> {tutorial?.title}
|
||||
</IonTitle>
|
||||
<IonButtons slot="end">
|
||||
<IonButton onClick={onClose}>
|
||||
<IonIcon icon={closeOutline} />
|
||||
</IonButton>
|
||||
</IonButtons>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonContent fullscreen>
|
||||
{tutorial && (
|
||||
<>
|
||||
<div style={{ paddingTop: "12px" }}>
|
||||
{tutorial.steps.map((step: TutorialStep) => (
|
||||
<IonCard key={step.title}>
|
||||
<IonCardHeader>
|
||||
<IonCardTitle
|
||||
style={{
|
||||
fontSize: "16px",
|
||||
}}
|
||||
>
|
||||
{step.title}
|
||||
</IonCardTitle>
|
||||
</IonCardHeader>
|
||||
<IonCardContent>
|
||||
<p>{step.description}</p>
|
||||
{step.code && (
|
||||
<div
|
||||
className="code-block"
|
||||
style={{
|
||||
backgroundColor: "#222",
|
||||
color: "#0f0",
|
||||
padding: "12px",
|
||||
borderRadius: "4px",
|
||||
fontFamily: "monospace",
|
||||
fontSize: "12px",
|
||||
marginTop: "8px",
|
||||
overflowX: "auto",
|
||||
}}
|
||||
>
|
||||
<code>{step.code}</code>
|
||||
</div>
|
||||
)}
|
||||
{step.hint && (
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: "rgba(255, 193, 7, 0.1)",
|
||||
borderLeft: "3px solid #ffc107",
|
||||
padding: "8px 12px",
|
||||
marginTop: "8px",
|
||||
borderRadius: "3px",
|
||||
fontSize: "13px",
|
||||
}}
|
||||
>
|
||||
💡 {step.hint}
|
||||
</div>
|
||||
)}
|
||||
</IonCardContent>
|
||||
</IonCard>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<IonCard
|
||||
style={{
|
||||
margin: "12px",
|
||||
backgroundColor: "rgba(102, 126, 234, 0.1)",
|
||||
}}
|
||||
>
|
||||
<IonCardHeader>
|
||||
<IonCardTitle style={{ fontSize: "16px" }}>
|
||||
💡 Tipps
|
||||
</IonCardTitle>
|
||||
</IonCardHeader>
|
||||
<IonCardContent>
|
||||
<ul style={{ marginLeft: "20px" }}>
|
||||
{tutorial.tips.map((tip: string) => (
|
||||
<li
|
||||
key={tip}
|
||||
style={{
|
||||
marginBottom: "8px",
|
||||
fontSize: "14px",
|
||||
}}
|
||||
>
|
||||
{tip}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</IonCardContent>
|
||||
</IonCard>
|
||||
|
||||
<div style={{ paddingBottom: "40px" }} />
|
||||
</>
|
||||
)}
|
||||
</IonContent>
|
||||
</IonPopover>
|
||||
);
|
||||
}
|
||||
@@ -237,8 +237,7 @@ export default function LibraryPage() {
|
||||
<div>
|
||||
<h1>Spielebibliothek</h1>
|
||||
<p>
|
||||
Konsolidierte Übersicht aus Steam, Epic Games und GOG. Duplikate
|
||||
werden automatisch zusammengeführt.
|
||||
Deine Spiele aus Steam.
|
||||
</p>
|
||||
</div>
|
||||
<div className="hero-stats">
|
||||
|
||||
@@ -12,14 +12,11 @@ import {
|
||||
IonLabel,
|
||||
IonList,
|
||||
IonPage,
|
||||
IonSelect,
|
||||
IonSelectOption,
|
||||
IonText,
|
||||
IonTitle,
|
||||
IonToolbar,
|
||||
} from "@ionic/react";
|
||||
import {
|
||||
informationCircleOutline,
|
||||
refreshOutline,
|
||||
saveOutline,
|
||||
settingsOutline,
|
||||
@@ -45,37 +42,15 @@ 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;
|
||||
const PROVIDER_IDS = ["steam"] as const;
|
||||
|
||||
export default function SettingsDetailPage() {
|
||||
const { serviceId } = useParams<SettingsRouteParams>();
|
||||
@@ -97,51 +72,19 @@ export default function SettingsDetailPage() {
|
||||
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);
|
||||
const testConfig = await response.json();
|
||||
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,
|
||||
);
|
||||
console.log("✓ Test-Modus: config.local.json geladen", testConfig);
|
||||
}
|
||||
} 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 } : {}),
|
||||
},
|
||||
};
|
||||
console.warn("config.local.json konnte nicht geladen werden", error);
|
||||
}
|
||||
}
|
||||
|
||||
setConfig(loadedConfig);
|
||||
};
|
||||
|
||||
loadConfig();
|
||||
}, [serviceId]);
|
||||
|
||||
@@ -167,6 +110,8 @@ export default function SettingsDetailPage() {
|
||||
setApiOutput("Rufe API auf...");
|
||||
|
||||
try {
|
||||
let result: { games: any[]; count: number } | null = null;
|
||||
|
||||
if (service === "steam") {
|
||||
const steamConfig = config.steam;
|
||||
if (!steamConfig?.apiKey || !steamConfig?.steamId) {
|
||||
@@ -176,7 +121,6 @@ export default function SettingsDetailPage() {
|
||||
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" },
|
||||
@@ -194,32 +138,31 @@ export default function SettingsDetailPage() {
|
||||
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)}`,
|
||||
);
|
||||
result = await response.json();
|
||||
}
|
||||
|
||||
const updatedConfig = {
|
||||
...config,
|
||||
[service]: {
|
||||
...config[service],
|
||||
lastRefresh: new Date().toISOString(),
|
||||
},
|
||||
};
|
||||
setConfig(updatedConfig);
|
||||
await ConfigService.saveConfig(updatedConfig);
|
||||
setAlertMessage(`✓ ${service.toUpperCase()} aktualisiert`);
|
||||
setShowAlert(true);
|
||||
if (result) {
|
||||
await db.saveGames(result.games);
|
||||
|
||||
const updatedConfig = {
|
||||
...config,
|
||||
[service]: {
|
||||
...config[service],
|
||||
lastRefresh: new Date().toISOString(),
|
||||
},
|
||||
};
|
||||
setConfig(updatedConfig);
|
||||
await ConfigService.saveConfig(updatedConfig);
|
||||
|
||||
setApiOutput(
|
||||
`✓ ${result.count} Spiele abgerufen\n\nBeispiel:\n${JSON.stringify(result.games.slice(0, 2), null, 2)}`,
|
||||
);
|
||||
setAlertMessage(`✓ ${result.count} Spiele 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`,
|
||||
);
|
||||
setApiOutput(`❌ Fehler: ${errorMsg}`);
|
||||
setAlertMessage("Aktualisierung fehlgeschlagen");
|
||||
setShowAlert(true);
|
||||
}
|
||||
@@ -364,16 +307,6 @@ export default function SettingsDetailPage() {
|
||||
/>
|
||||
</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"
|
||||
@@ -386,247 +319,6 @@ export default function SettingsDetailPage() {
|
||||
</>
|
||||
)}
|
||||
|
||||
{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>
|
||||
|
||||
@@ -16,9 +16,6 @@ import {
|
||||
cloudOutline,
|
||||
cogOutline,
|
||||
gameControllerOutline,
|
||||
globeOutline,
|
||||
shieldOutline,
|
||||
storefrontOutline,
|
||||
} from "ionicons/icons";
|
||||
|
||||
import "./SettingsPage.css";
|
||||
@@ -46,34 +43,6 @@ export default function SettingsPage() {
|
||||
<IonLabel>Steam</IonLabel>
|
||||
<IonNote slot="end">API Key · Steam ID</IonNote>
|
||||
</IonItem>
|
||||
<IonItem routerLink="/settings/gog" routerDirection="forward" detail>
|
||||
<IonIcon slot="start" icon={globeOutline} />
|
||||
<IonLabel>GOG</IonLabel>
|
||||
<IonNote slot="end">Token</IonNote>
|
||||
</IonItem>
|
||||
<IonItem routerLink="/settings/epic" routerDirection="forward" detail>
|
||||
<IonIcon slot="start" icon={shieldOutline} />
|
||||
<IonLabel>Epic Games</IonLabel>
|
||||
<IonNote slot="end">Import</IonNote>
|
||||
</IonItem>
|
||||
<IonItem
|
||||
routerLink="/settings/amazon"
|
||||
routerDirection="forward"
|
||||
detail
|
||||
>
|
||||
<IonIcon slot="start" icon={storefrontOutline} />
|
||||
<IonLabel>Amazon Games</IonLabel>
|
||||
<IonNote slot="end">Import</IonNote>
|
||||
</IonItem>
|
||||
<IonItem
|
||||
routerLink="/settings/blizzard"
|
||||
routerDirection="forward"
|
||||
detail
|
||||
>
|
||||
<IonIcon slot="start" icon={cloudOutline} />
|
||||
<IonLabel>Blizzard</IonLabel>
|
||||
<IonNote slot="end">OAuth</IonNote>
|
||||
</IonItem>
|
||||
</IonList>
|
||||
|
||||
<IonList inset>
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
.settings-tutorial-section {
|
||||
padding: 8px 16px 16px;
|
||||
}
|
||||
|
||||
.settings-tutorial-step {
|
||||
margin-bottom: 20px;
|
||||
padding: 16px;
|
||||
background: #ffffff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.settings-tutorial-step h3 {
|
||||
margin: 0 0 12px 0;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.settings-tutorial-step p {
|
||||
margin: 0 0 12px 0;
|
||||
line-height: 1.6;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.settings-tutorial-code {
|
||||
background: #f9fafb;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
margin: 12px 0;
|
||||
white-space: pre-wrap;
|
||||
font-family: "Monaco", "Courier New", monospace;
|
||||
font-size: 0.85rem;
|
||||
color: #374151;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.settings-tutorial-hint {
|
||||
margin-top: 12px;
|
||||
padding: 12px;
|
||||
background: #fef3c7;
|
||||
border-left: 4px solid #f59e0b;
|
||||
border-radius: 6px;
|
||||
font-size: 0.9rem;
|
||||
color: #92400e;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.settings-tutorial-hint::before {
|
||||
content: "💡 ";
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.settings-tutorial-empty {
|
||||
padding: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
import React, { useMemo } from "react";
|
||||
import {
|
||||
IonBackButton,
|
||||
IonButtons,
|
||||
IonContent,
|
||||
IonHeader,
|
||||
IonIcon,
|
||||
IonPage,
|
||||
IonText,
|
||||
IonTitle,
|
||||
IonToolbar,
|
||||
IonCard,
|
||||
IonCardContent,
|
||||
IonCardHeader,
|
||||
IonCardTitle,
|
||||
} from "@ionic/react";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
import { TUTORIALS } from "../../data/tutorials";
|
||||
|
||||
import "./SettingsTutorialPage.css";
|
||||
|
||||
interface TutorialRouteParams {
|
||||
serviceId: string;
|
||||
}
|
||||
|
||||
export default function SettingsTutorialPage() {
|
||||
const { serviceId } = useParams<TutorialRouteParams>();
|
||||
const tutorial = useMemo(() => TUTORIALS[serviceId], [serviceId]);
|
||||
|
||||
if (!tutorial) {
|
||||
return (
|
||||
<IonPage>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonButtons slot="start">
|
||||
<IonBackButton defaultHref="/settings" />
|
||||
</IonButtons>
|
||||
<IonTitle>Anleitung</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
<IonContent fullscreen>
|
||||
<div className="settings-tutorial-empty">
|
||||
<IonText color="medium">Keine Anleitung verfügbar.</IonText>
|
||||
</div>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<IonPage>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonButtons slot="start">
|
||||
<IonBackButton defaultHref={`/settings/${serviceId}`} />
|
||||
</IonButtons>
|
||||
<IonTitle>
|
||||
<IonIcon icon={tutorial.icon} /> {tutorial.title}
|
||||
</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonContent fullscreen>
|
||||
<div className="settings-tutorial-section">
|
||||
{tutorial.steps.map((step, index) => (
|
||||
<IonCard key={`${step.title}-${index}`}>
|
||||
<IonCardHeader>
|
||||
<IonCardTitle>{step.title}</IonCardTitle>
|
||||
</IonCardHeader>
|
||||
<IonCardContent>
|
||||
<p>{step.description}</p>
|
||||
{step.code && (
|
||||
<pre className="settings-tutorial-code">{step.code}</pre>
|
||||
)}
|
||||
{step.hint && (
|
||||
<div className="settings-tutorial-hint">{step.hint}</div>
|
||||
)}
|
||||
</IonCardContent>
|
||||
</IonCard>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="settings-tutorial-section">
|
||||
<IonCard>
|
||||
<IonCardHeader>
|
||||
<IonCardTitle>Tipps</IonCardTitle>
|
||||
</IonCardHeader>
|
||||
<IonCardContent>
|
||||
<ul>
|
||||
{tutorial.tips.map((tip) => (
|
||||
<li key={tip}>{tip}</li>
|
||||
))}
|
||||
</ul>
|
||||
</IonCardContent>
|
||||
</IonCard>
|
||||
</div>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
}
|
||||
@@ -6,29 +6,8 @@ import { db } from "./Database";
|
||||
|
||||
export interface ServiceConfig {
|
||||
steam?: {
|
||||
apiKey: string;
|
||||
steamId: string;
|
||||
lastRefresh?: string;
|
||||
};
|
||||
gog?: {
|
||||
userId: string;
|
||||
accessToken: string;
|
||||
lastRefresh?: string;
|
||||
};
|
||||
epic?: {
|
||||
email?: string;
|
||||
method?: "oauth" | "manual";
|
||||
lastRefresh?: string;
|
||||
};
|
||||
amazon?: {
|
||||
email?: string;
|
||||
method?: "oauth" | "manual";
|
||||
lastRefresh?: string;
|
||||
};
|
||||
blizzard?: {
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
region: "us" | "eu" | "kr" | "tw";
|
||||
apiKey?: string;
|
||||
steamId?: string;
|
||||
lastRefresh?: string;
|
||||
};
|
||||
}
|
||||
@@ -142,17 +121,6 @@ export class ConfigService {
|
||||
if (!config.steam.steamId) errors.push("Steam: Steam ID fehlt");
|
||||
}
|
||||
|
||||
if (config.gog) {
|
||||
if (!config.gog.userId) errors.push("GOG: User ID fehlt");
|
||||
if (!config.gog.accessToken) errors.push("GOG: Access Token fehlt");
|
||||
}
|
||||
|
||||
if (config.blizzard) {
|
||||
if (!config.blizzard.clientId) errors.push("Blizzard: Client ID fehlt");
|
||||
if (!config.blizzard.clientSecret)
|
||||
errors.push("Blizzard: Client Secret fehlt");
|
||||
}
|
||||
|
||||
return {
|
||||
valid: errors.length === 0,
|
||||
errors,
|
||||
|
||||
@@ -5,29 +5,33 @@
|
||||
|
||||
export interface DbConfig {
|
||||
steam?: {
|
||||
apiKey: string;
|
||||
steamId: string;
|
||||
apiKey?: string;
|
||||
steamId?: string;
|
||||
lastRefresh?: string;
|
||||
};
|
||||
gog?: {
|
||||
userId: string;
|
||||
accessToken: string;
|
||||
userId?: string;
|
||||
accessToken?: string;
|
||||
refreshToken?: string;
|
||||
lastRefresh?: string;
|
||||
};
|
||||
epic?: {
|
||||
email?: string;
|
||||
accessToken?: string;
|
||||
method?: "oauth" | "manual";
|
||||
lastRefresh?: string;
|
||||
};
|
||||
amazon?: {
|
||||
email?: string;
|
||||
manualGames?: Array<{ name: string; gameId?: string; source?: string }>;
|
||||
method?: "oauth" | "manual";
|
||||
lastRefresh?: string;
|
||||
};
|
||||
blizzard?: {
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
region: "us" | "eu" | "kr" | "tw";
|
||||
clientId?: string;
|
||||
clientSecret?: string;
|
||||
accessToken?: string;
|
||||
region?: "us" | "eu" | "kr" | "tw";
|
||||
lastRefresh?: string;
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user