Introduce a full user settings feature: add a SettingsView component and UserSettings type, plus API helpers to fetch, create, and update settings (convertors between API and app shapes). App now loads settings on mount, persists category toggles to the API, exposes a /settings route, and passes itemsPerPage into BrowseView and CastView. Header gains a settings icon/link and BrowseView/CastView update pagination option defaults. This enables centralized library/display/content preferences and syncs them with the backend.
769 lines
23 KiB
TypeScript
769 lines
23 KiB
TypeScript
import { Media, Staff, UserSettings, MediaCategory } from './types';
|
|
|
|
const BASE_URL = 'http://192.168.1.102:6400';
|
|
|
|
function normalizeUrl(url: string | null): string {
|
|
if (!url) return '';
|
|
if (url.startsWith('http://') || url.startsWith('https://')) {
|
|
return url;
|
|
}
|
|
// Remove leading slash if present and add base URL
|
|
const cleanPath = url.startsWith('/') ? url.slice(1) : url;
|
|
return `${BASE_URL}/${cleanPath}`;
|
|
}
|
|
|
|
// API Response Types
|
|
export interface ApiResponse<T> {
|
|
success: boolean;
|
|
data: T;
|
|
}
|
|
|
|
export interface PaginatedResponse<T> {
|
|
items: T[];
|
|
total: number;
|
|
page: number;
|
|
limit: number;
|
|
totalPages?: number;
|
|
}
|
|
|
|
// Media Types
|
|
export interface ApiMediaItem {
|
|
id: number;
|
|
title: string;
|
|
year: number;
|
|
poster: string | null;
|
|
banner: string | null;
|
|
description: string | null;
|
|
rating: number | null;
|
|
category: string | null;
|
|
type: string;
|
|
status: string;
|
|
aspectRatio: string | null;
|
|
runtime: number | null;
|
|
director: string | null;
|
|
writer: string | null;
|
|
releaseDate: string | null;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
genres?: string[];
|
|
tags?: string[];
|
|
studios?: string[];
|
|
staff?: ApiStaff[];
|
|
categories?: string[];
|
|
platforms?: string[];
|
|
developers?: string[];
|
|
completionStatus?: string;
|
|
source?: string;
|
|
playCount?: number;
|
|
lastActivity?: string | null;
|
|
playtime?: number;
|
|
}
|
|
|
|
export interface ApiStaff {
|
|
id: number;
|
|
name: string;
|
|
photo: string | null;
|
|
bio: string | null;
|
|
birthDate: string | null;
|
|
birthPlace: string | null;
|
|
role: string;
|
|
characterName: string | null;
|
|
characterImage: string | null;
|
|
occupations?: string[];
|
|
}
|
|
|
|
export interface CreateMediaInput {
|
|
title: string;
|
|
year: number;
|
|
poster?: string | null;
|
|
banner?: string | null;
|
|
description?: string | null;
|
|
rating?: number | null;
|
|
category?: string | null;
|
|
type?: string;
|
|
status?: string;
|
|
aspectRatio?: string | null;
|
|
runtime?: number | null;
|
|
director?: string | null;
|
|
writer?: string | null;
|
|
releaseDate?: string | null;
|
|
genres?: string[];
|
|
tags?: string[];
|
|
studios?: string[];
|
|
staff?: CreateStaffInput[];
|
|
}
|
|
|
|
export interface UpdateMediaInput extends Partial<CreateMediaInput> {}
|
|
|
|
export interface CreateStaffInput {
|
|
name: string;
|
|
photo?: string | null;
|
|
bio?: string | null;
|
|
birthDate?: string | null;
|
|
birthPlace?: string | null;
|
|
role: string;
|
|
characterName?: string | null;
|
|
characterImage?: string | null;
|
|
occupations?: string[];
|
|
}
|
|
|
|
// Cast Types
|
|
export interface ApiCastItem {
|
|
id: number;
|
|
name: string;
|
|
cleanname?: string;
|
|
photo: string | null;
|
|
bio: string | null;
|
|
birthDate: string | null;
|
|
birthPlace: string | null;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
occupations?: string[];
|
|
filmography?: ApiCastMediaItem[];
|
|
media_types?: string[];
|
|
bust_size?: number | null;
|
|
cup_size?: string | null;
|
|
waist_size?: number | null;
|
|
hip_size?: number | null;
|
|
height?: number | null;
|
|
weight?: number | null;
|
|
hair_color?: string | null;
|
|
eye_color?: string | null;
|
|
ethnicity?: string | null;
|
|
adult_specifics?: {
|
|
id: number;
|
|
cast_id: number;
|
|
bust_size?: number | null;
|
|
cup_size?: string | null;
|
|
waist_size?: number | null;
|
|
hip_size?: number | null;
|
|
height?: number | null;
|
|
weight?: number | null;
|
|
hair_color?: string | null;
|
|
eye_color?: string | null;
|
|
ethnicity?: string | null;
|
|
tattoos?: string | null;
|
|
piercings?: string | null;
|
|
measurements?: string | null;
|
|
shoe_size?: number | null;
|
|
};
|
|
}
|
|
|
|
export interface ApiCastMediaItem {
|
|
id: number;
|
|
title: string;
|
|
year: number;
|
|
poster: string | null;
|
|
category: string | null;
|
|
type: string;
|
|
role: string;
|
|
characterName?: string | null;
|
|
}
|
|
|
|
export interface CreateCastInput {
|
|
name: string;
|
|
photo?: string | null;
|
|
bio?: string | null;
|
|
birthDate?: string | null;
|
|
birthPlace?: string | null;
|
|
occupations?: string[];
|
|
}
|
|
|
|
export interface UpdateCastInput extends Partial<CreateCastInput> {}
|
|
|
|
|
|
export function convertApiCastToStaff(apiItem: ApiCastItem): Staff {
|
|
return {
|
|
id: apiItem.id.toString(),
|
|
name: apiItem.name,
|
|
cleanname: apiItem.cleanname,
|
|
role: apiItem.occupations?.[0] || 'Actor',
|
|
photo: normalizeUrl(apiItem.photo) || `https://picsum.photos/seed/cast-${apiItem.id}/200/200`,
|
|
bio: apiItem.bio || undefined,
|
|
birthDate: apiItem.birthDate || undefined,
|
|
birthPlace: apiItem.birthPlace || undefined,
|
|
occupations: apiItem.occupations || ['Actor'],
|
|
createdAt: apiItem.createdAt,
|
|
updatedAt: apiItem.updatedAt,
|
|
bust_size: apiItem.bust_size,
|
|
cup_size: apiItem.cup_size,
|
|
waist_size: apiItem.waist_size,
|
|
hip_size: apiItem.hip_size,
|
|
height: apiItem.height,
|
|
weight: apiItem.weight,
|
|
hair_color: apiItem.hair_color,
|
|
eye_color: apiItem.eye_color,
|
|
ethnicity: apiItem.ethnicity,
|
|
filmography: apiItem.filmography?.map(item => ({
|
|
id: item.id,
|
|
title: item.title,
|
|
year: item.year,
|
|
poster: normalizeUrl(item.poster) || `https://picsum.photos/seed/${item.id}/400/600`,
|
|
category: item.category,
|
|
type: item.type,
|
|
role: item.role,
|
|
characterName: item.characterName
|
|
})),
|
|
media_types: apiItem.media_types,
|
|
adult_specifics: apiItem.adult_specifics
|
|
};
|
|
}
|
|
|
|
export function convertApiToMedia(apiItem: ApiMediaItem): Media {
|
|
// Convert staff from API to Media staff format
|
|
const staff: Staff[] = (apiItem.staff || []).map((staffMember) => ({
|
|
id: staffMember.id.toString(),
|
|
name: staffMember.name,
|
|
role: staffMember.role,
|
|
photo: normalizeUrl(staffMember.photo) || `https://picsum.photos/seed/staff-${staffMember.id}/200/200`,
|
|
characterName: staffMember.characterName || staffMember.name,
|
|
characterImage: normalizeUrl(staffMember.characterImage) || normalizeUrl(staffMember.photo) || `https://picsum.photos/seed/staff-${staffMember.id}/200/200`,
|
|
}));
|
|
|
|
// Determine aspect ratio from API format
|
|
let aspectRatio: '2/3' | '16/9' | '1/1' = '2/3';
|
|
if (apiItem.aspectRatio) {
|
|
const ratio = apiItem.aspectRatio.toLowerCase();
|
|
if (ratio.includes('16:9') || ratio.includes('16/9') || ratio.includes('1.78') || ratio.includes('2.39')) {
|
|
aspectRatio = '16/9';
|
|
} else if (ratio.includes('1:1') || ratio.includes('1/1') || ratio.includes('1.00')) {
|
|
aspectRatio = '1/1';
|
|
} else if (ratio.includes('2/3')) {
|
|
aspectRatio = '2/3';
|
|
}
|
|
}
|
|
|
|
// Map API type to Media type allowed values
|
|
let mediaType: 'TV' | 'Movie' | 'OVA' | 'ONA' | 'Album' | 'Single' | 'Hardcover' | 'E-book' | 'Console' | 'Game' = 'Movie';
|
|
const apiType = apiItem.type?.toLowerCase();
|
|
if (apiType === 'tv' || apiType === 'episode') {
|
|
mediaType = 'TV';
|
|
} else if (apiType === 'album' || apiType === 'single') {
|
|
mediaType = apiType === 'album' ? 'Album' : 'Single';
|
|
} else if (apiType === 'game' || apiType === 'console') {
|
|
mediaType = apiType === 'game' ? 'Game' : 'Console';
|
|
} else if (apiType === 'ova') {
|
|
mediaType = 'OVA';
|
|
} else if (apiType === 'ona') {
|
|
mediaType = 'ONA';
|
|
} else if (apiType === 'hardcover' || apiType === 'e-book') {
|
|
mediaType = apiType === 'hardcover' ? 'Hardcover' : 'E-book';
|
|
}
|
|
|
|
// Map API category to MediaCategory
|
|
let mediaCategory: 'Anime' | 'Movies' | 'TV Series' | 'Music' | 'Books' | 'Adult' | 'Consoles' | 'Games' = 'Movies';
|
|
const apiCategory = apiItem.category?.toLowerCase();
|
|
|
|
if (apiCategory === 'anime') {
|
|
mediaCategory = 'Anime';
|
|
} else if (apiCategory === 'movie' || apiCategory === 'movies') {
|
|
mediaCategory = 'Movies';
|
|
} else if (apiCategory === 'tv' || apiCategory === 'series' || apiCategory === 'tv series' || apiType === 'tv' || apiType === 'episode') {
|
|
mediaCategory = 'TV Series';
|
|
} else if (apiCategory === 'music' || apiType === 'album' || apiType === 'single') {
|
|
mediaCategory = 'Music';
|
|
} else if (apiCategory === 'book' || apiCategory === 'books' || apiType === 'hardcover' || apiType === 'e-book') {
|
|
mediaCategory = 'Books';
|
|
} else if (apiCategory === 'adult') {
|
|
mediaCategory = 'Adult';
|
|
} else if (apiCategory === 'console' || apiCategory === 'consoles' || apiType === 'console') {
|
|
mediaCategory = 'Consoles';
|
|
} else if (apiCategory === 'game' || apiCategory === 'games' || apiType === 'game') {
|
|
mediaCategory = 'Games';
|
|
} else {
|
|
// If category doesn't match any known category, use the original value capitalized
|
|
// This handles cases where the API returns unexpected category values
|
|
console.warn('Unknown category:', apiItem.category, 'defaulting to Movies');
|
|
mediaCategory = 'Movies';
|
|
}
|
|
|
|
// Map API status to Media status allowed values
|
|
let mediaStatus: 'watching' | 'completed' | 'planned' | 'dropped' | 'reading' | 'listening' | 'playing' | 'on-hold' = 'completed';
|
|
const apiStatus = apiItem.status?.toLowerCase();
|
|
if (apiStatus === 'ongoing' || apiStatus === 'watching') {
|
|
mediaStatus = 'watching';
|
|
} else if (apiStatus === 'upcoming' || apiStatus === 'planned') {
|
|
mediaStatus = 'planned';
|
|
} else if (apiStatus === 'dropped') {
|
|
mediaStatus = 'dropped';
|
|
} else if (apiStatus === 'reading') {
|
|
mediaStatus = 'reading';
|
|
} else if (apiStatus === 'listening') {
|
|
mediaStatus = 'listening';
|
|
} else if (apiStatus === 'playing') {
|
|
mediaStatus = 'playing';
|
|
} else if (apiStatus === 'on-hold') {
|
|
mediaStatus = 'on-hold';
|
|
}
|
|
|
|
return {
|
|
id: apiItem.id.toString(),
|
|
title: apiItem.title,
|
|
year: apiItem.year?.toString() || 'Unknown',
|
|
poster: normalizeUrl(apiItem.poster) || `https://picsum.photos/seed/${apiItem.id}/400/600`,
|
|
category: mediaCategory,
|
|
banner: normalizeUrl(apiItem.banner) || undefined,
|
|
description: apiItem.description || undefined,
|
|
rating: apiItem.rating || undefined,
|
|
genres: apiItem.genres || [],
|
|
tags: apiItem.tags || [],
|
|
studios: apiItem.studios,
|
|
type: mediaType,
|
|
status: mediaStatus,
|
|
staff: staff.length > 0 ? staff : undefined,
|
|
aspectRatio: aspectRatio,
|
|
categories: apiItem.categories,
|
|
platforms: apiItem.platforms,
|
|
developers: apiItem.developers,
|
|
completionStatus: apiItem.completionStatus,
|
|
source: apiItem.source,
|
|
playCount: apiItem.playCount,
|
|
lastActivity: apiItem.lastActivity,
|
|
playtime: apiItem.playtime
|
|
};
|
|
}
|
|
|
|
// Media API Functions
|
|
export async function fetchAllMedia(page: number = 1, limit: number = 10000): Promise<Media[]> {
|
|
try {
|
|
const response = await fetch(`${BASE_URL}/api/media?page=${page}&limit=${limit}`);
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
const data: ApiResponse<PaginatedResponse<ApiMediaItem>> = await response.json();
|
|
|
|
if (data.success && data.data.items) {
|
|
return data.data.items.map(convertApiToMedia);
|
|
}
|
|
return [];
|
|
} catch (error) {
|
|
console.error('Error fetching media from API:', error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
export async function fetchMediaById(id: number | string): Promise<Media | null> {
|
|
try {
|
|
const response = await fetch(`${BASE_URL}/api/media/${id}`);
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
const data: ApiResponse<ApiMediaItem> = await response.json();
|
|
|
|
if (data.success && data.data) {
|
|
return convertApiToMedia(data.data);
|
|
}
|
|
return null;
|
|
} catch (error) {
|
|
console.error('Error fetching media by ID:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export async function createMedia(media: CreateMediaInput): Promise<Media | null> {
|
|
try {
|
|
const response = await fetch(`${BASE_URL}/api/media`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify(media),
|
|
});
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
const data: ApiResponse<ApiMediaItem> = await response.json();
|
|
|
|
if (data.success && data.data) {
|
|
return convertApiToMedia(data.data);
|
|
}
|
|
return null;
|
|
} catch (error) {
|
|
console.error('Error creating media:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export async function updateMedia(id: number | string, media: UpdateMediaInput): Promise<Media | null> {
|
|
try {
|
|
const response = await fetch(`${BASE_URL}/api/media/${id}`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify(media),
|
|
});
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
const data: ApiResponse<ApiMediaItem> = await response.json();
|
|
|
|
if (data.success && data.data) {
|
|
return convertApiToMedia(data.data);
|
|
}
|
|
return null;
|
|
} catch (error) {
|
|
console.error('Error updating media:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export async function deleteMedia(id: number | string): Promise<boolean> {
|
|
try {
|
|
const response = await fetch(`${BASE_URL}/api/media/${id}`, {
|
|
method: 'DELETE',
|
|
});
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
const data: ApiResponse<{ message: string }> = await response.json();
|
|
return data.success;
|
|
} catch (error) {
|
|
console.error('Error deleting media:', error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Cast API Functions
|
|
export async function fetchAllCast(page: number = 1, limit: number = 100000): Promise<Staff[]> {
|
|
try {
|
|
const response = await fetch(`${BASE_URL}/api/cast?page=${page}&limit=${limit}`);
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
const data: ApiResponse<PaginatedResponse<ApiCastItem>> = await response.json();
|
|
|
|
if (data.success && data.data.items) {
|
|
return data.data.items.map(convertApiCastToStaff);
|
|
}
|
|
return [];
|
|
} catch (error) {
|
|
console.error('Error fetching cast from API:', error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
export async function fetchCastById(id: number | string): Promise<ApiCastItem | null> {
|
|
try {
|
|
const response = await fetch(`${BASE_URL}/api/cast/${id}`);
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
const data: ApiResponse<ApiCastItem> = await response.json();
|
|
|
|
if (data.success && data.data) {
|
|
return data.data;
|
|
}
|
|
return null;
|
|
} catch (error) {
|
|
console.error('Error fetching cast by ID:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export async function fetchCastMedia(castId: number | string): Promise<Media[]> {
|
|
try {
|
|
const response = await fetch(`${BASE_URL}/api/cast/${castId}/media`);
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
const data: ApiResponse<PaginatedResponse<ApiMediaItem>> = await response.json();
|
|
|
|
if (data.success && data.data.items) {
|
|
return data.data.items.map(convertApiToMedia);
|
|
}
|
|
return [];
|
|
} catch (error) {
|
|
console.error('Error fetching cast media:', error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
export async function createCast(cast: CreateCastInput): Promise<ApiCastItem | null> {
|
|
try {
|
|
const response = await fetch(`${BASE_URL}/api/cast`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify(cast),
|
|
});
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
const data: ApiResponse<ApiCastItem> = await response.json();
|
|
|
|
if (data.success && data.data) {
|
|
return data.data;
|
|
}
|
|
return null;
|
|
} catch (error) {
|
|
console.error('Error creating cast:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export async function updateCast(id: number | string, cast: UpdateCastInput): Promise<ApiCastItem | null> {
|
|
try {
|
|
const response = await fetch(`${BASE_URL}/api/cast/${id}`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify(cast),
|
|
});
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
const data: ApiResponse<ApiCastItem> = await response.json();
|
|
|
|
if (data.success && data.data) {
|
|
return data.data;
|
|
}
|
|
return null;
|
|
} catch (error) {
|
|
console.error('Error updating cast:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export async function deleteCast(id: number | string): Promise<boolean> {
|
|
try {
|
|
const response = await fetch(`${BASE_URL}/api/cast/${id}`, {
|
|
method: 'DELETE',
|
|
});
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
const data: ApiResponse<{ message: string }> = await response.json();
|
|
return data.success;
|
|
} catch (error) {
|
|
console.error('Error deleting cast:', error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Legacy function for compatibility - fetches all unique staff members from media
|
|
export async function fetchAllActors(): Promise<Array<{id: number, name: string, photo: string | null}>> {
|
|
try {
|
|
const media = await fetchAllMedia(1, 1000);
|
|
const actorMap = new Map<number, {id: number, name: string, photo: string | null}>();
|
|
|
|
media.forEach(item => {
|
|
item.staff?.forEach(staffMember => {
|
|
const id = parseInt(staffMember.id);
|
|
if (!actorMap.has(id)) {
|
|
actorMap.set(id, {
|
|
id: id,
|
|
name: staffMember.name,
|
|
photo: staffMember.photo
|
|
});
|
|
}
|
|
});
|
|
});
|
|
|
|
return Array.from(actorMap.values());
|
|
} catch (error) {
|
|
console.error('Error fetching all actors:', error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
// Legacy function for compatibility - fetches all unique tags from media
|
|
export async function fetchAllTags(): Promise<string[]> {
|
|
try {
|
|
const media = await fetchAllMedia(1, 1000);
|
|
const tagSet = new Set<string>();
|
|
|
|
media.forEach(item => {
|
|
item.tags?.forEach(tag => tagSet.add(tag));
|
|
item.genres?.forEach(genre => tagSet.add(genre));
|
|
});
|
|
|
|
return Array.from(tagSet).sort();
|
|
} catch (error) {
|
|
console.error('Error fetching all tags:', error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
// Legacy function for compatibility - fetches media by actor name
|
|
export async function fetchMediaByActor(actorName: string): Promise<Media[]> {
|
|
try {
|
|
const media = await fetchAllMedia(1, 1000);
|
|
return media.filter(item =>
|
|
item.staff?.some(staffMember =>
|
|
staffMember.name.toLowerCase().includes(actorName.toLowerCase())
|
|
)
|
|
);
|
|
} catch (error) {
|
|
console.error('Error fetching media by actor:', error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
// Legacy function for compatibility - fetches media by tag
|
|
export async function fetchMediaByTag(tag: string): Promise<Media[]> {
|
|
try {
|
|
const media = await fetchAllMedia(1, 1000);
|
|
return media.filter(item =>
|
|
item.tags?.some(t => t.toLowerCase().includes(tag.toLowerCase())) ||
|
|
item.genres?.some(g => g.toLowerCase().includes(tag.toLowerCase()))
|
|
);
|
|
} catch (error) {
|
|
console.error('Error fetching media by tag:', error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
// Convenience function - fetch media from API (legacy compatibility)
|
|
export async function fetchMediaFromApi(apiUrl?: string): Promise<Media[]> {
|
|
return fetchAllMedia();
|
|
}
|
|
|
|
// Convenience function - fetch media from local JSON (legacy compatibility)
|
|
export async function fetchMediaFromLocalJson(): Promise<Media[]> {
|
|
return fetchAllMedia();
|
|
}
|
|
|
|
// Settings API Types
|
|
export interface ApiSettingsItem {
|
|
id?: number;
|
|
enabled_categories: string[];
|
|
items_per_page: number;
|
|
default_view: string;
|
|
show_adult_content: boolean;
|
|
auto_play_trailers: boolean;
|
|
language: string;
|
|
theme: string;
|
|
created_at?: string;
|
|
updated_at?: string;
|
|
}
|
|
|
|
export interface CreateSettingsInput {
|
|
enabled_categories: string[];
|
|
items_per_page?: number;
|
|
default_view?: string;
|
|
show_adult_content?: boolean;
|
|
auto_play_trailers?: boolean;
|
|
language?: string;
|
|
theme?: string;
|
|
}
|
|
|
|
export interface UpdateSettingsInput extends Partial<CreateSettingsInput> {}
|
|
|
|
export function convertApiToSettings(apiItem: ApiSettingsItem): UserSettings {
|
|
return {
|
|
id: apiItem.id,
|
|
enabledCategories: apiItem.enabled_categories as MediaCategory[],
|
|
itemsPerPage: apiItem.items_per_page || 20,
|
|
defaultView: (apiItem.default_view as 'grid' | 'list') || 'grid',
|
|
showAdultContent: apiItem.show_adult_content || false,
|
|
autoPlayTrailers: apiItem.auto_play_trailers || false,
|
|
language: apiItem.language || 'en',
|
|
theme: (apiItem.theme as 'light' | 'dark' | 'system') || 'system',
|
|
createdAt: apiItem.created_at,
|
|
updatedAt: apiItem.updated_at,
|
|
};
|
|
}
|
|
|
|
export function convertSettingsToApi(settings: UserSettings): CreateSettingsInput {
|
|
return {
|
|
enabled_categories: settings.enabledCategories,
|
|
items_per_page: settings.itemsPerPage,
|
|
default_view: settings.defaultView,
|
|
show_adult_content: settings.showAdultContent,
|
|
auto_play_trailers: settings.autoPlayTrailers,
|
|
language: settings.language,
|
|
theme: settings.theme,
|
|
};
|
|
}
|
|
|
|
// Settings API Functions
|
|
export async function fetchSettings(): Promise<UserSettings | null> {
|
|
try {
|
|
const response = await fetch(`${BASE_URL}/api/settings`);
|
|
if (!response.ok) {
|
|
// If settings don't exist (404), return null to use defaults
|
|
if (response.status === 404) {
|
|
return null;
|
|
}
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
const data: ApiResponse<ApiSettingsItem> = await response.json();
|
|
|
|
if (data.success && data.data) {
|
|
return convertApiToSettings(data.data);
|
|
}
|
|
return null;
|
|
} catch (error) {
|
|
console.error('Error fetching settings:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export async function createSettings(settings: UserSettings): Promise<UserSettings | null> {
|
|
try {
|
|
const apiSettings = convertSettingsToApi(settings);
|
|
console.log('Creating settings:', apiSettings);
|
|
const response = await fetch(`${BASE_URL}/api/settings`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify(apiSettings),
|
|
});
|
|
console.log('Create settings response status:', response.status);
|
|
if (!response.ok) {
|
|
const errorText = await response.text();
|
|
console.error('Create settings error response:', errorText);
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
const data: ApiResponse<ApiSettingsItem> = await response.json();
|
|
console.log('Create settings response:', data);
|
|
|
|
if (data.success && data.data) {
|
|
return convertApiToSettings(data.data);
|
|
}
|
|
return null;
|
|
} catch (error) {
|
|
console.error('Error creating settings:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export async function updateSettings(settings: UserSettings): Promise<UserSettings | null> {
|
|
try {
|
|
const apiSettings = convertSettingsToApi(settings);
|
|
console.log('Updating settings:', apiSettings);
|
|
const response = await fetch(`${BASE_URL}/api/settings`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify(apiSettings),
|
|
});
|
|
console.log('Update settings response status:', response.status);
|
|
if (!response.ok) {
|
|
// If settings don't exist (404), try creating them instead
|
|
if (response.status === 404) {
|
|
console.log('Settings not found, attempting to create...');
|
|
return createSettings(settings);
|
|
}
|
|
const errorText = await response.text();
|
|
console.error('Update settings error response:', errorText);
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
const data: ApiResponse<ApiSettingsItem> = await response.json();
|
|
console.log('Update settings response:', data);
|
|
|
|
if (data.success && data.data) {
|
|
return convertApiToSettings(data.data);
|
|
}
|
|
return null;
|
|
} catch (error) {
|
|
console.error('Error updating settings:', error);
|
|
return null;
|
|
}
|
|
}
|