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",
|
"build": "vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"test": "node --test server/**/*.test.mjs",
|
"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": {
|
"dependencies": {
|
||||||
"@ionic/react": "^8.0.0",
|
"@ionic/react": "^8.0.0",
|
||||||
@@ -26,6 +33,7 @@
|
|||||||
"@types/react-router-dom": "^5.3.3",
|
"@types/react-router-dom": "^5.3.3",
|
||||||
"@vitejs/plugin-react": "^4.2.1",
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
"typescript": "^5.3.3",
|
"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 PlaylistsPage from "./pages/Playlists/PlaylistsPage";
|
||||||
import SettingsPage from "./pages/Settings/SettingsPage";
|
import SettingsPage from "./pages/Settings/SettingsPage";
|
||||||
import SettingsDetailPage from "./pages/Settings/SettingsDetailPage";
|
import SettingsDetailPage from "./pages/Settings/SettingsDetailPage";
|
||||||
import SettingsTutorialPage from "./pages/Settings/SettingsTutorialPage";
|
|
||||||
|
|
||||||
import "./App.css";
|
import "./App.css";
|
||||||
|
|
||||||
@@ -43,11 +42,6 @@ export default function App() {
|
|||||||
path="/settings/:serviceId"
|
path="/settings/:serviceId"
|
||||||
component={SettingsDetailPage}
|
component={SettingsDetailPage}
|
||||||
/>
|
/>
|
||||||
<Route
|
|
||||||
exact
|
|
||||||
path="/settings/:serviceId/tutorial"
|
|
||||||
component={SettingsTutorialPage}
|
|
||||||
/>
|
|
||||||
<Route exact path="/">
|
<Route exact path="/">
|
||||||
<Redirect to="/home" />
|
<Redirect to="/home" />
|
||||||
</Route>
|
</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>
|
<div>
|
||||||
<h1>Spielebibliothek</h1>
|
<h1>Spielebibliothek</h1>
|
||||||
<p>
|
<p>
|
||||||
Konsolidierte Übersicht aus Steam, Epic Games und GOG. Duplikate
|
Deine Spiele aus Steam.
|
||||||
werden automatisch zusammengeführt.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="hero-stats">
|
<div className="hero-stats">
|
||||||
|
|||||||
@@ -12,14 +12,11 @@ import {
|
|||||||
IonLabel,
|
IonLabel,
|
||||||
IonList,
|
IonList,
|
||||||
IonPage,
|
IonPage,
|
||||||
IonSelect,
|
|
||||||
IonSelectOption,
|
|
||||||
IonText,
|
IonText,
|
||||||
IonTitle,
|
IonTitle,
|
||||||
IonToolbar,
|
IonToolbar,
|
||||||
} from "@ionic/react";
|
} from "@ionic/react";
|
||||||
import {
|
import {
|
||||||
informationCircleOutline,
|
|
||||||
refreshOutline,
|
refreshOutline,
|
||||||
saveOutline,
|
saveOutline,
|
||||||
settingsOutline,
|
settingsOutline,
|
||||||
@@ -45,37 +42,15 @@ const SERVICE_META = {
|
|||||||
steam: {
|
steam: {
|
||||||
title: "Steam",
|
title: "Steam",
|
||||||
description: "Deine Steam-Bibliothek",
|
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: {
|
data: {
|
||||||
title: "Datenverwaltung",
|
title: "Datenverwaltung",
|
||||||
description: "Export, Import und Reset",
|
description: "Export, Import und Reset",
|
||||||
tutorialKey: null,
|
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
type ServiceId = keyof typeof SERVICE_META;
|
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() {
|
export default function SettingsDetailPage() {
|
||||||
const { serviceId } = useParams<SettingsRouteParams>();
|
const { serviceId } = useParams<SettingsRouteParams>();
|
||||||
@@ -97,51 +72,19 @@ export default function SettingsDetailPage() {
|
|||||||
if (isTestMode) {
|
if (isTestMode) {
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/api/config/load");
|
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) {
|
if (response.ok) {
|
||||||
const testConfig = JSON.parse(responseText);
|
const testConfig = await response.json();
|
||||||
loadedConfig = { ...loadedConfig, ...testConfig };
|
loadedConfig = { ...loadedConfig, ...testConfig };
|
||||||
console.log("Test-Modus: Geladene Config:", loadedConfig);
|
console.log("✓ Test-Modus: config.local.json geladen", testConfig);
|
||||||
setAlertMessage("🛠️ Test-Modus: config.local.json geladen");
|
|
||||||
setShowAlert(true);
|
|
||||||
} else {
|
|
||||||
console.error(
|
|
||||||
"API /api/config/load fehlgeschlagen:",
|
|
||||||
response.status,
|
|
||||||
responseText,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(
|
console.warn("config.local.json konnte nicht geladen werden", 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);
|
setConfig(loadedConfig);
|
||||||
};
|
};
|
||||||
|
|
||||||
loadConfig();
|
loadConfig();
|
||||||
}, [serviceId]);
|
}, [serviceId]);
|
||||||
|
|
||||||
@@ -167,6 +110,8 @@ export default function SettingsDetailPage() {
|
|||||||
setApiOutput("Rufe API auf...");
|
setApiOutput("Rufe API auf...");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
let result: { games: any[]; count: number } | null = null;
|
||||||
|
|
||||||
if (service === "steam") {
|
if (service === "steam") {
|
||||||
const steamConfig = config.steam;
|
const steamConfig = config.steam;
|
||||||
if (!steamConfig?.apiKey || !steamConfig?.steamId) {
|
if (!steamConfig?.apiKey || !steamConfig?.steamId) {
|
||||||
@@ -176,7 +121,6 @@ export default function SettingsDetailPage() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rufe Backend-Endpoint auf (statt direkt Steam API wegen CORS)
|
|
||||||
const response = await fetch("/api/steam/refresh", {
|
const response = await fetch("/api/steam/refresh", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
@@ -194,32 +138,31 @@ export default function SettingsDetailPage() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await response.json();
|
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 = {
|
if (result) {
|
||||||
...config,
|
await db.saveGames(result.games);
|
||||||
[service]: {
|
|
||||||
...config[service],
|
const updatedConfig = {
|
||||||
lastRefresh: new Date().toISOString(),
|
...config,
|
||||||
},
|
[service]: {
|
||||||
};
|
...config[service],
|
||||||
setConfig(updatedConfig);
|
lastRefresh: new Date().toISOString(),
|
||||||
await ConfigService.saveConfig(updatedConfig);
|
},
|
||||||
setAlertMessage(`✓ ${service.toUpperCase()} aktualisiert`);
|
};
|
||||||
setShowAlert(true);
|
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) {
|
} catch (error) {
|
||||||
const errorMsg = error instanceof Error ? error.message : String(error);
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
||||||
setApiOutput(
|
setApiOutput(`❌ Fehler: ${errorMsg}`);
|
||||||
`❌ Fehler: ${errorMsg}\n\n💡 Tipp: Führe stattdessen im Terminal aus:\nnpm run fetch:steam`,
|
|
||||||
);
|
|
||||||
setAlertMessage("Aktualisierung fehlgeschlagen");
|
setAlertMessage("Aktualisierung fehlgeschlagen");
|
||||||
setShowAlert(true);
|
setShowAlert(true);
|
||||||
}
|
}
|
||||||
@@ -364,16 +307,6 @@ export default function SettingsDetailPage() {
|
|||||||
/>
|
/>
|
||||||
</IonItem>
|
</IonItem>
|
||||||
</IonList>
|
</IonList>
|
||||||
<IonList inset>
|
|
||||||
<IonItem
|
|
||||||
button
|
|
||||||
detail
|
|
||||||
routerLink={`/settings/${serviceId}/tutorial`}
|
|
||||||
routerDirection="forward"
|
|
||||||
>
|
|
||||||
<IonLabel>Anleitung anzeigen</IonLabel>
|
|
||||||
</IonItem>
|
|
||||||
</IonList>
|
|
||||||
<div className="settings-detail-actions">
|
<div className="settings-detail-actions">
|
||||||
<IonButton
|
<IonButton
|
||||||
expand="block"
|
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" && (
|
{serviceId === "data" && (
|
||||||
<>
|
<>
|
||||||
<IonList inset>
|
<IonList inset>
|
||||||
|
|||||||
@@ -16,9 +16,6 @@ import {
|
|||||||
cloudOutline,
|
cloudOutline,
|
||||||
cogOutline,
|
cogOutline,
|
||||||
gameControllerOutline,
|
gameControllerOutline,
|
||||||
globeOutline,
|
|
||||||
shieldOutline,
|
|
||||||
storefrontOutline,
|
|
||||||
} from "ionicons/icons";
|
} from "ionicons/icons";
|
||||||
|
|
||||||
import "./SettingsPage.css";
|
import "./SettingsPage.css";
|
||||||
@@ -46,34 +43,6 @@ export default function SettingsPage() {
|
|||||||
<IonLabel>Steam</IonLabel>
|
<IonLabel>Steam</IonLabel>
|
||||||
<IonNote slot="end">API Key · Steam ID</IonNote>
|
<IonNote slot="end">API Key · Steam ID</IonNote>
|
||||||
</IonItem>
|
</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>
|
||||||
|
|
||||||
<IonList inset>
|
<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 {
|
export interface ServiceConfig {
|
||||||
steam?: {
|
steam?: {
|
||||||
apiKey: string;
|
apiKey?: string;
|
||||||
steamId: 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";
|
|
||||||
lastRefresh?: string;
|
lastRefresh?: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -142,17 +121,6 @@ export class ConfigService {
|
|||||||
if (!config.steam.steamId) errors.push("Steam: Steam ID fehlt");
|
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 {
|
return {
|
||||||
valid: errors.length === 0,
|
valid: errors.length === 0,
|
||||||
errors,
|
errors,
|
||||||
|
|||||||
@@ -5,29 +5,33 @@
|
|||||||
|
|
||||||
export interface DbConfig {
|
export interface DbConfig {
|
||||||
steam?: {
|
steam?: {
|
||||||
apiKey: string;
|
apiKey?: string;
|
||||||
steamId: string;
|
steamId?: string;
|
||||||
lastRefresh?: string;
|
lastRefresh?: string;
|
||||||
};
|
};
|
||||||
gog?: {
|
gog?: {
|
||||||
userId: string;
|
userId?: string;
|
||||||
accessToken: string;
|
accessToken?: string;
|
||||||
|
refreshToken?: string;
|
||||||
lastRefresh?: string;
|
lastRefresh?: string;
|
||||||
};
|
};
|
||||||
epic?: {
|
epic?: {
|
||||||
email?: string;
|
email?: string;
|
||||||
|
accessToken?: string;
|
||||||
method?: "oauth" | "manual";
|
method?: "oauth" | "manual";
|
||||||
lastRefresh?: string;
|
lastRefresh?: string;
|
||||||
};
|
};
|
||||||
amazon?: {
|
amazon?: {
|
||||||
email?: string;
|
email?: string;
|
||||||
|
manualGames?: Array<{ name: string; gameId?: string; source?: string }>;
|
||||||
method?: "oauth" | "manual";
|
method?: "oauth" | "manual";
|
||||||
lastRefresh?: string;
|
lastRefresh?: string;
|
||||||
};
|
};
|
||||||
blizzard?: {
|
blizzard?: {
|
||||||
clientId: string;
|
clientId?: string;
|
||||||
clientSecret: string;
|
clientSecret?: string;
|
||||||
region: "us" | "eu" | "kr" | "tw";
|
accessToken?: string;
|
||||||
|
region?: "us" | "eu" | "kr" | "tw";
|
||||||
lastRefresh?: string;
|
lastRefresh?: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user