IGDB-Integration für Game Library + Electron Update-Button im Version-Modal

- Neues IGDB-Service-Modul (igdb.ts): Token-Management, Rate-Limiting, Game-Lookup per Steam-AppID/Name, Batch-Enrichment mit In-Memory-Cache
- Server: 2 neue Routes (/igdb/enrich/:steamId, /igdb/game/:appid), Auto-Enrichment bei Steam-Login und Refresh
- Frontend: IGDB-Cover, Genre-Tags, Plattform-Badges, farbcodiertes Rating, IGDB-Anreichern-Button
- Version-Modal: Update-Button für Electron-App (checkForUpdates/installUpdate), Desktop-App-Version anzeigen
- CSS: Styles für IGDB-Elemente und Update-UI

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Daniel 2026-03-08 01:48:15 +01:00
parent aec1142bff
commit b404c20eca
6 changed files with 785 additions and 25 deletions

View file

@ -13,17 +13,31 @@ interface UserSummary {
lastUpdated: string;
}
interface IgdbData {
igdbId: number;
name: string;
coverUrl: string | null;
genres: string[];
platforms: string[];
rating: number | null;
firstReleaseDate: string | null;
summary: string | null;
igdbUrl: string | null;
}
interface SteamGame {
appid: number;
name: string;
playtime_forever: number;
img_icon_url: string;
igdb?: IgdbData;
}
interface CommonGame {
appid: number;
name: string;
img_icon_url: string;
igdb?: IgdbData;
owners: Array<{ steamId: string; personaName: string; playtime_forever: number }>;
}
@ -78,6 +92,7 @@ export default function GameLibraryTab({ data }: { data: any }) {
const [searchQuery, setSearchQuery] = useState('');
const [searchResults, setSearchResults] = useState<SearchResult[] | null>(null);
const [loading, setLoading] = useState(false);
const [enriching, setEnriching] = useState<string | null>(null);
const searchTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const filterInputRef = useRef<HTMLInputElement>(null);
@ -153,6 +168,21 @@ export default function GameLibraryTab({ data }: { data: any }) {
}
}, [fetchUsers, mode, selectedUser, viewUser]);
// ── Enrich user library with IGDB data ──
const enrichUser = useCallback(async (steamId: string) => {
setEnriching(steamId);
try {
const resp = await fetch(`/api/game-library/igdb/enrich/${steamId}`);
if (resp.ok) {
// Reload user's game data to get IGDB info
if (mode === 'user' && selectedUser === steamId) {
viewUser(steamId);
}
}
} catch { /* silent */ }
finally { setEnriching(null); }
}, [mode, selectedUser, viewUser]);
// ── Toggle user selection for common games ──
const toggleCommonUser = useCallback((steamId: string) => {
setSelectedUsers(prev => {
@ -408,6 +438,14 @@ export default function GameLibraryTab({ data }: { data: any }) {
>
&#x21bb;
</button>
<button
className="gl-enrich-btn"
onClick={() => enrichUser(selectedUser!)}
disabled={enriching === selectedUser}
title="Mit IGDB-Daten anreichern"
>
{enriching === selectedUser ? '\u23F3' : '\uD83C\uDF10'} IGDB
</button>
</>
)}
</div>
@ -431,20 +469,44 @@ export default function GameLibraryTab({ data }: { data: any }) {
) : (
<div className="gl-game-list">
{filteredGames.map(g => (
<div key={g.appid} className="gl-game-item">
{g.img_icon_url ? (
<img
className="gl-game-icon"
src={gameIconUrl(g.appid, g.img_icon_url)}
alt=""
/>
) : (
<div className="gl-game-icon" />
)}
<span className="gl-game-name">{g.name}</span>
<span className="gl-game-playtime">
{formatPlaytime(g.playtime_forever)}
</span>
<div key={g.appid} className={`gl-game-item ${g.igdb ? 'enriched' : ''}`}>
{/* Cover/Icon */}
<div className="gl-game-visual">
{g.igdb?.coverUrl ? (
<img className="gl-game-cover" src={g.igdb.coverUrl} alt="" />
) : g.img_icon_url ? (
<img className="gl-game-icon" src={gameIconUrl(g.appid, g.img_icon_url)} alt="" />
) : (
<div className="gl-game-icon" />
)}
</div>
{/* Info */}
<div className="gl-game-info">
<span className="gl-game-name">{g.name}</span>
{g.igdb?.genres && g.igdb.genres.length > 0 && (
<div className="gl-game-genres">
{g.igdb.genres.slice(0, 3).map(genre => (
<span key={genre} className="gl-genre-tag">{genre}</span>
))}
</div>
)}
{g.igdb?.platforms && g.igdb.platforms.filter(p => !p.includes('Windows') && !p.includes('Linux') && !p.includes('Mac')).length > 0 && (
<div className="gl-game-platforms">
Auch auf: {g.igdb.platforms.filter(p => !p.includes('Windows') && !p.includes('Linux') && !p.includes('Mac')).slice(0, 3).map(p => (
<span key={p} className="gl-platform-tag">{p}</span>
))}
</div>
)}
</div>
{/* Right side: rating + playtime */}
<div className="gl-game-meta">
{g.igdb?.rating != null && (
<span className={`gl-game-rating ${g.igdb.rating >= 75 ? 'high' : g.igdb.rating >= 50 ? 'mid' : 'low'}`}>
{Math.round(g.igdb.rating)}
</span>
)}
<span className="gl-game-playtime">{formatPlaytime(g.playtime_forever)}</span>
</div>
</div>
))}
</div>
@ -494,16 +556,16 @@ export default function GameLibraryTab({ data }: { data: any }) {
</p>
<div className="gl-game-list">
{commonGames.map(g => (
<div key={g.appid} className="gl-game-item">
{g.img_icon_url ? (
<img
className="gl-game-icon"
src={gameIconUrl(g.appid, g.img_icon_url)}
alt=""
/>
) : (
<div className="gl-game-icon" />
)}
<div key={g.appid} className={`gl-game-item ${g.igdb ? 'enriched' : ''}`}>
<div className="gl-game-visual">
{g.igdb?.coverUrl ? (
<img className="gl-game-cover" src={g.igdb.coverUrl} alt="" />
) : g.img_icon_url ? (
<img className="gl-game-icon" src={gameIconUrl(g.appid, g.img_icon_url)} alt="" />
) : (
<div className="gl-game-icon" />
)}
</div>
<span className="gl-game-name">{g.name}</span>
<div className="gl-common-playtimes">
{g.owners.map(o => (