diff --git a/src/App.tsx b/src/App.tsx index f543d0f..52b65c8 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -13,9 +13,10 @@ import CastView from './components/CastView'; import CastDetailView from './components/CastDetailView'; import AddMediaView from './components/AddMediaView'; import ImporterView from './components/ImporterView'; +import SettingsView from './components/SettingsView'; import { MOCK_MEDIA, DETAIL_MEDIA } from './data'; -import { Media, Staff, MediaCategory } from './types'; -import { fetchAllMedia, fetchMediaById, fetchCastById, convertApiCastToStaff } from './api'; +import { Media, Staff, MediaCategory, UserSettings } from './types'; +import { fetchAllMedia, fetchMediaById, fetchCastById, convertApiCastToStaff, fetchSettings, updateSettings } from './api'; function AppContent() { const navigate = useNavigate(); @@ -28,12 +29,41 @@ function AppContent() { const [selectedPerson, setSelectedPerson] = useState(null); const [searchQuery, setSearchQuery] = useState(searchParams.get('search') || ''); const [enabledCategories, setEnabledCategories] = useState(['Anime', 'Movies', 'TV Series', 'Music', 'Books', 'Consoles', 'Games', 'Adult']); + const [settings, setSettings] = useState(null); const [customMedia, setCustomMedia] = useState([]); const [adultMedia, setAdultMedia] = useState([]); // Load media from API on component mount (only when not on cast routes) const [apiMedia, setApiMedia] = useState([]); + useEffect(() => { + const loadSettingsFromApi = async () => { + try { + const loadedSettings = await fetchSettings(); + if (loadedSettings) { + setSettings(loadedSettings); + setEnabledCategories(loadedSettings.enabledCategories); + } + } catch (error) { + console.error('Failed to load settings from API:', error); + } + }; + + loadSettingsFromApi(); + }, []); + + const reloadSettings = async () => { + try { + const loadedSettings = await fetchSettings(); + if (loadedSettings) { + setSettings(loadedSettings); + setEnabledCategories(loadedSettings.enabledCategories); + } + } catch (error) { + console.error('Failed to reload settings from API:', error); + } + }; + useEffect(() => { const loadMediaFromApi = async () => { try { @@ -50,7 +80,7 @@ function AppContent() { } }, [location.pathname]); - const toggleCategory = (category: MediaCategory) => { + const toggleCategory = async (category: MediaCategory) => { setEnabledCategories(prev => { const isEnabling = !prev.includes(category); const newList = isEnabling @@ -62,6 +92,27 @@ function AppContent() { const nextCategory = newList.find(c => c !== category) || 'Anime'; setActiveCategory(nextCategory as MediaCategory); } + + // Save to API + const baseSettings = settings || { + enabledCategories: prev, + itemsPerPage: 20, + defaultView: 'grid', + showAdultContent: false, + autoPlayTrailers: false, + language: 'en', + theme: 'system', + }; + const updatedSettings: UserSettings = { + ...baseSettings, + enabledCategories: newList, + }; + updateSettings(updatedSettings).then(saved => { + if (saved) { + setSettings(saved); + } + }); + return newList; }); }; @@ -237,6 +288,7 @@ function AppContent() { mediaList={filteredMedia} onMediaClick={handleMediaClick} activeCategory={activeCategory} + itemsPerPage={settings?.itemsPerPage} /> } /> } /> } /> + + } /> diff --git a/src/api.ts b/src/api.ts index 296d2f9..6574171 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1,4 +1,4 @@ -import { Media, Staff } from './types'; +import { Media, Staff, UserSettings, MediaCategory } from './types'; const BASE_URL = 'http://192.168.1.102:6400'; @@ -625,3 +625,144 @@ export async function fetchMediaFromApi(apiUrl?: string): Promise { export async function fetchMediaFromLocalJson(): Promise { 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 {} + +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 { + 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 = 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 { + 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 = 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 { + 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 = 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; + } +} diff --git a/src/components/BrowseView.tsx b/src/components/BrowseView.tsx index a835fe3..78d1d11 100644 --- a/src/components/BrowseView.tsx +++ b/src/components/BrowseView.tsx @@ -17,12 +17,13 @@ interface BrowseViewProps { mediaList: Media[]; onMediaClick: (media: Media) => void; activeCategory: MediaCategory; + itemsPerPage?: number; } -export default function BrowseView({ mediaList, onMediaClick, activeCategory }: BrowseViewProps) { +export default function BrowseView({ mediaList, onMediaClick, activeCategory, itemsPerPage: initialItemsPerPage = 12 }: BrowseViewProps) { const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid'); const [currentPage, setCurrentPage] = useState(1); - const [itemsPerPage, setItemsPerPage] = useState(12); + const [itemsPerPage, setItemsPerPage] = useState(initialItemsPerPage); const [sortBy, setSortBy] = useState('default'); // Filter states @@ -281,7 +282,7 @@ export default function BrowseView({ mediaList, onMediaClick, activeCategory }: }} className="bg-zinc-100 border-none rounded-md px-2 py-1 text-sm font-bold text-zinc-700 focus:ring-2 focus:ring-[#6d28d9] outline-none" > - {[8, 12, 16, 24, 48].map(size => ( + {[12, 20, 36, 48, 60].map(size => ( ))} diff --git a/src/components/CastView.tsx b/src/components/CastView.tsx index 39a44cc..1662de6 100644 --- a/src/components/CastView.tsx +++ b/src/components/CastView.tsx @@ -12,9 +12,10 @@ import { fetchAllCast } from '@/api'; interface CastViewProps { onPersonClick: (person: Staff) => void; enabledCategories: MediaCategory[]; + itemsPerPage?: number; } -export default function CastView({ onPersonClick, enabledCategories }: CastViewProps) { +export default function CastView({ onPersonClick, enabledCategories, itemsPerPage: initialItemsPerPage = 12 }: CastViewProps) { const navigate = useNavigate(); const [staffList, setStaffList] = useState([]); const [loading, setLoading] = useState(true); @@ -34,7 +35,7 @@ export default function CastView({ onPersonClick, enabledCategories }: CastViewP return localStorage.getItem('castFilterMediaType') || ''; }); const [currentPage, setCurrentPage] = useState(1); - const [itemsPerPage, setItemsPerPage] = useState(12); + const [itemsPerPage, setItemsPerPage] = useState(initialItemsPerPage); const [showFilters, setShowFilters] = useState(false); // Persist filters and sorts @@ -382,7 +383,7 @@ export default function CastView({ onPersonClick, enabledCategories }: CastViewP }} className="bg-zinc-100 border-none rounded-md px-2 py-1 text-sm font-bold text-zinc-700 focus:ring-2 focus:ring-[#6d28d9] outline-none" > - {[8, 12, 16, 24, 48].map(size => ( + {[12, 20, 36, 48, 60].map(size => ( ))} diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 15c6bd2..bdca39b 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -1,9 +1,8 @@ -import { Search, User, X, Plus, Download } from 'lucide-react'; +import { Search, User, X, Plus, Download, Settings } from 'lucide-react'; import { cn } from '@/lib/utils'; import React, { useState } from 'react'; import { Link, NavLink } from 'react-router-dom'; import { MediaCategory } from '@/types'; -import LibrarySettings from './LibrarySettings'; interface HeaderProps { onSearch: (query: string) => void; @@ -113,10 +112,12 @@ export default function Header({ > - + + + + + + {saveStatus === 'success' && ( +
+ Settings saved successfully! +
+ )} + {saveStatus === 'error' && ( +
+ Failed to save settings. Please try again. +
+ )} + +
+ {/* Library Settings */} +
+

Library Settings

+
+

+ Toggle which media areas you want to see in your library. +

+
+ {(['Anime', 'Movies', 'Music', 'Books', 'Consoles', 'Games', 'Adult'] as MediaCategory[]).map((category) => ( +
+
+
+ {CATEGORY_ICONS[category]} +
+
+ +

+ {settings.enabledCategories.includes(category) ? 'Enabled' : 'Disabled'} +

+
+
+ toggleCategory(category)} + /> +
+ ))} +
+
+
+ + {/* Display Settings */} +
+

Display Settings

+
+ {/* Items per page */} +
+ +
+ {ITEMS_PER_PAGE_OPTIONS.map((option) => ( + + ))} +
+
+ + {/* Default view */} +
+ +
+ + +
+
+ + {/* Theme */} +
+ +
+ {(['light', 'dark', 'system'] as const).map((theme) => ( + + ))} +
+
+
+
+ + {/* Content Settings */} +
+

Content Settings

+
+ {/* Show adult content */} +
+
+ +

+ Display adult media in your library +

+
+ setSettings(prev => ({ ...prev, showAdultContent: checked }))} + /> +
+ + {/* Auto-play trailers */} +
+
+ +

+ Automatically play trailers when viewing media +

+
+ setSettings(prev => ({ ...prev, autoPlayTrailers: checked }))} + /> +
+
+
+ + {/* Language Settings */} +
+

Language

+
+
+ + +
+
+ {LANGUAGE_OPTIONS.map((option) => ( + + ))} +
+
+
+
+ + + ); +} diff --git a/src/types.ts b/src/types.ts index 305a44d..7a7b646 100644 --- a/src/types.ts +++ b/src/types.ts @@ -95,3 +95,16 @@ export interface AdultSpecifics { measurements?: string | null; shoe_size?: number | null; } + +export interface UserSettings { + id?: number; + enabledCategories: MediaCategory[]; + itemsPerPage: number; + defaultView: 'grid' | 'list'; + showAdultContent: boolean; + autoPlayTrailers: boolean; + language: string; + theme: 'light' | 'dark' | 'system'; + createdAt?: string; + updatedAt?: string; +}