Use Zustand store; modularize API & routes

Introduce a centralized Zustand store and refactor app state out of App.tsx into src/store/appStore.ts. Modularize API surface by moving media/cast/settings/converters/types into src/lib/api/* and re-exporting from src/api.ts for backward compatibility. Replace inline route helpers with dedicated route components (MediaDetailRoute, CastDetailRoute, CategoryBrowseRoute) and wire CATEGORY_PATHS/PATH_TO_CATEGORY constants. Update AddMediaView UI (icons, layout) and adjust settings/category handling to use DEFAULT_SETTINGS and the store. Add zustand to package.json/package-lock.json and include a new React SKILL.md. Overall changes improve state management, API organization, and route/component separation for better maintainability and code-splitting.
This commit is contained in:
Lars Behrends
2026-04-16 14:53:46 +02:00
parent a407b57006
commit 432416cfc5
22 changed files with 1843 additions and 1342 deletions

View File

@@ -1,7 +1,7 @@
const BASE_URL = import.meta.env.VITE_API_URL;
// Import the source mapping
import { SOURCE_CATEGORY_MAPPING } from '@/types';
// Import the source mapping and types
import { SOURCE_CATEGORY_MAPPING, Media, Staff } from '@/types';
export interface PlayniteConfig {
ip: string;
@@ -54,6 +54,9 @@ export interface PlayniteGame {
lastPlayed?: string;
source?: string;
isInstalled?: boolean;
coverBase64?: string;
backgroundBase64?: string;
iconBase64?: string;
}
export interface PlayniteGamesResponse {
@@ -65,7 +68,7 @@ export interface PlayniteGamesResponse {
export type LogCallback = (message: string) => void;
export type ProgressCallback = (progress: Partial<ImportProgress>) => void;
/*
async function fetchGameCover(baseUrl: string, headers: Record<string, string>, gameId: string): Promise<string | null> {
try {
const coverResponse = await fetch(`${baseUrl}/api/games/${gameId}/cover`, {
@@ -89,6 +92,50 @@ async function fetchGameCover(baseUrl: string, headers: Record<string, string>,
}
}
async function fetchGameBackground(baseUrl: string, headers: Record<string, string>, gameId: string): Promise<string | null> {
try {
const backgroundResponse = await fetch(`${baseUrl}/api/games/${gameId}/background`, {
method: 'GET',
headers
});
if (!backgroundResponse.ok) {
return null;
}
const blob = await backgroundResponse.blob();
const arrayBuffer = await blob.arrayBuffer();
const base64 = btoa(String.fromCharCode(...new Uint8Array(arrayBuffer)));
const mimeType = blob.type || 'image/jpeg';
return `data:${mimeType};base64,${base64}`;
} catch (error) {
return null;
}
}
async function fetchGameIcon(baseUrl: string, headers: Record<string, string>, gameId: string): Promise<string | null> {
try {
const iconResponse = await fetch(`${baseUrl}/api/games/${gameId}/icon`, {
method: 'GET',
headers
});
if (!iconResponse.ok) {
return null;
}
const blob = await iconResponse.blob();
const arrayBuffer = await blob.arrayBuffer();
const base64 = btoa(String.fromCharCode(...new Uint8Array(arrayBuffer)));
const mimeType = blob.type || 'image/png';
return `data:${mimeType};base64,${base64}`;
} catch (error) {
return null;
}
}
*/
export async function importFromPlaynite(
config: PlayniteConfig,
logCallback: LogCallback,
@@ -117,7 +164,7 @@ export async function importFromPlaynite(
const existingMediaResponse = await fetch(`${BASE_URL}/api/media?limit=1000`);
const existingMediaData = await existingMediaResponse.json();
const existingMedia = new Map(
(existingMediaData.data?.items || []).map((m: any) => [m.title, m])
(existingMediaData.data?.items || []).map((m: Media) => [m.title, m])
);
logCallback(`Found ${existingMedia.size} existing games in database`);
@@ -159,6 +206,18 @@ export async function importFromPlaynite(
if (detailResponse.ok) {
const detailData: PlayniteGame = await detailResponse.json();
/*
// Fetch images
const [cover, background, icon] = await Promise.all([
fetchGameCover(baseUrl, headers, game.id),
fetchGameBackground(baseUrl, headers, game.id),
fetchGameIcon(baseUrl, headers, game.id)
]);
detailData.coverBase64 = cover;
detailData.backgroundBase64 = background;
detailData.iconBase64 = icon;
*/
detailedGames.push(detailData);
logCallback(`✓ Fetched details for: ${game.name}`);
} else {
@@ -231,7 +290,7 @@ export async function importFromPlaynite(
}
// Staff is for actors/performers only - leave empty for games
const staff: any[] = [];
const staff: Staff[] = [];
// Determine type based on genres/features
let type = 'Game';
//if (game.genres?.includes('Visual Novel') || game.genres?.includes('Adventure')) {