diff --git a/apps/channels/api_views.py b/apps/channels/api_views.py index 0221a266..5457c3ba 100644 --- a/apps/channels/api_views.py +++ b/apps/channels/api_views.py @@ -1280,6 +1280,18 @@ class LogoViewSet(viewsets.ModelViewSet): """Optimize queryset with prefetch and add filtering""" queryset = Logo.objects.prefetch_related('channels').order_by('name') + # Filter by specific IDs + ids = self.request.query_params.getlist('ids') + if ids: + try: + # Convert string IDs to integers and filter + id_list = [int(id_str) for id_str in ids if id_str.isdigit()] + if id_list: + queryset = queryset.filter(id__in=id_list) + except (ValueError, TypeError): + pass # Invalid IDs, return empty queryset + queryset = Logo.objects.none() + # Filter by usage used_filter = self.request.query_params.get('used', None) if used_filter == 'true': diff --git a/frontend/src/WebSocket.jsx b/frontend/src/WebSocket.jsx index 599b55d5..9ba62273 100644 --- a/frontend/src/WebSocket.jsx +++ b/frontend/src/WebSocket.jsx @@ -9,6 +9,7 @@ import React, { } from 'react'; import { notifications } from '@mantine/notifications'; import useChannelsStore from './store/channels'; +import useLogosStore from './store/logos'; import usePlaylistsStore from './store/playlists'; import useEPGsStore from './store/epgs'; import { Box, Button, Stack, Alert, Group } from '@mantine/core'; @@ -499,7 +500,7 @@ export const WebsocketProvider = ({ children }) => { const setProfilePreview = usePlaylistsStore((s) => s.setProfilePreview); const fetchEPGData = useEPGsStore((s) => s.fetchEPGData); const fetchEPGs = useEPGsStore((s) => s.fetchEPGs); - const fetchLogos = useChannelsStore((s) => s.fetchLogos); + const fetchLogos = useLogosStore((s) => s.fetchLogos); const fetchChannelProfiles = useChannelsStore((s) => s.fetchChannelProfiles); const ret = useMemo(() => { diff --git a/frontend/src/api.js b/frontend/src/api.js index 28a4d36e..22d37ce6 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -1,6 +1,7 @@ // src/api.js (updated) import useAuthStore from './store/auth'; import useChannelsStore from './store/channels'; +import useLogosStore from './store/logos'; import useUserAgentsStore from './store/userAgents'; import usePlaylistsStore from './store/playlists'; import useEPGsStore from './store/epgs'; @@ -1293,16 +1294,52 @@ export default class API { } } + static async getLogosByIds(logoIds) { + try { + if (!logoIds || logoIds.length === 0) return []; + + const params = new URLSearchParams(); + logoIds.forEach(id => params.append('ids', id)); + + const response = await request( + `${host}/api/channels/logos/?${params.toString()}` + ); + + return response; + } catch (e) { + errorNotification('Failed to retrieve logos by IDs', e); + return []; + } + } + static async fetchLogos() { try { const response = await this.getLogos(); - useChannelsStore.getState().setLogos(response); + useLogosStore.getState().setLogos(response); return response; } catch (e) { errorNotification('Failed to fetch logos', e); } } + static async fetchUsedLogos() { + try { + const response = await useLogosStore.getState().fetchUsedLogos(); + return response; + } catch (e) { + errorNotification('Failed to fetch used logos', e); + } + } + + static async fetchLogosByIds(logoIds) { + try { + const response = await useLogosStore.getState().fetchLogosByIds(logoIds); + return response; + } catch (e) { + errorNotification('Failed to fetch logos by IDs', e); + } + } + static async uploadLogo(file) { try { const formData = new FormData(); @@ -1340,7 +1377,7 @@ export default class API { } const result = await response.json(); - useChannelsStore.getState().addLogo(result); + useLogosStore.getState().addLogo(result); return result; } catch (e) { if (e.name === 'AbortError') { @@ -1368,7 +1405,7 @@ export default class API { body: formData, }); - useChannelsStore.getState().addLogo(response); + useLogosStore.getState().addLogo(response); return response; } catch (e) { @@ -1383,7 +1420,7 @@ export default class API { body: values, // This will be converted to JSON in the request function }); - useChannelsStore.getState().updateLogo(response); + useLogosStore.getState().updateLogo(response); return response; } catch (e) { @@ -1403,7 +1440,7 @@ export default class API { method: 'DELETE', }); - useChannelsStore.getState().removeLogo(id); + useLogosStore.getState().removeLogo(id); return true; } catch (e) { @@ -1425,7 +1462,7 @@ export default class API { // Remove multiple logos from store ids.forEach((id) => { - useChannelsStore.getState().removeLogo(id); + useLogosStore.getState().removeLogo(id); }); return true; diff --git a/frontend/src/components/LazyLogo.jsx b/frontend/src/components/LazyLogo.jsx new file mode 100644 index 00000000..6ba15f7c --- /dev/null +++ b/frontend/src/components/LazyLogo.jsx @@ -0,0 +1,66 @@ +import React, { useState, useEffect } from 'react'; +import { Skeleton } from '@mantine/core'; +import useLogosStore from '../store/logos'; +import logo from '../images/logo.png'; // Default logo + +const LazyLogo = ({ + logoId, + alt = 'logo', + style = { maxHeight: 18, maxWidth: 55 }, + fallbackSrc = logo, + ...props +}) => { + const [isLoading, setIsLoading] = useState(false); + const [hasError, setHasError] = useState(false); + const logos = useLogosStore((s) => s.logos); + const fetchLogosByIds = useLogosStore((s) => s.fetchLogosByIds); + + // Determine the logo source + const logoData = logoId && logos[logoId]; + const logoSrc = logoData?.cache_url || (logoId ? `/api/channels/logos/${logoId}/cache/` : fallbackSrc); + + useEffect(() => { + // If we have a logoId but no logo data, try to fetch it + if (logoId && !logoData && !isLoading && !hasError) { + setIsLoading(true); + fetchLogosByIds([logoId]) + .then(() => { + setIsLoading(false); + }) + .catch((error) => { + console.warn(`Failed to load logo ${logoId}:`, error); + setIsLoading(false); + setHasError(true); + }); + } + }, [logoId, logoData, fetchLogosByIds, isLoading, hasError]); + + // Show skeleton while loading + if (isLoading) { + return ( + + ); + } + + // Show image (will use fallback if logo fails to load) + return ( + {alt} { + if (!hasError) { + setHasError(true); + e.target.src = fallbackSrc; + } + }} + {...props} + /> + ); +}; + +export default LazyLogo; diff --git a/frontend/src/components/forms/Channel.jsx b/frontend/src/components/forms/Channel.jsx index c8a1f60d..3e7bf574 100644 --- a/frontend/src/components/forms/Channel.jsx +++ b/frontend/src/components/forms/Channel.jsx @@ -2,12 +2,15 @@ import React, { useState, useEffect, useRef } from 'react'; import { useFormik } from 'formik'; import * as Yup from 'yup'; import useChannelsStore from '../../store/channels'; +import useLogosStore from '../../store/logos'; import API from '../../api'; import useStreamProfilesStore from '../../store/streamProfiles'; import useStreamsStore from '../../store/streams'; import ChannelGroupForm from './ChannelGroup'; import usePlaylistsStore from '../../store/playlists'; import logo from '../../images/logo.png'; +import { useLogoSelection } from '../../hooks/useSmartLogos'; +import LazyLogo from '../LazyLogo'; import { Box, Button, @@ -48,8 +51,8 @@ const ChannelForm = ({ channel = null, isOpen, onClose }) => { const channelGroups = useChannelsStore((s) => s.channelGroups); const canEditChannelGroup = useChannelsStore((s) => s.canEditChannelGroup); - const logos = useChannelsStore((s) => s.logos); - const fetchLogos = useChannelsStore((s) => s.fetchLogos); + const { logos, ensureLogosLoaded, isLoading: logosLoading } = useLogoSelection(); + const fetchLogos = useLogosStore((s) => s.fetchLogos); const streams = useStreamsStore((state) => state.streams); const streamProfiles = useStreamProfilesStore((s) => s.profiles); const playlists = usePlaylistsStore((s) => s.playlists); @@ -193,10 +196,10 @@ const ChannelForm = ({ channel = null, isOpen, onClose }) => { formik.resetForm(); API.requeryChannels(); - + // Refresh channel profiles to update the membership information useChannelsStore.getState().fetchChannelProfiles(); - + setSubmitting(false); setTvgFilter(''); setLogoFilter(''); @@ -471,7 +474,13 @@ const ChannelForm = ({ channel = null, isOpen, onClose }) => { { + setLogoPopoverOpened(opened); + // Load all logos when popover is opened + if (opened) { + ensureLogosLoaded(); + } + }} // position="bottom-start" withArrow > @@ -530,13 +539,10 @@ const ChannelForm = ({ channel = null, isOpen, onClose }) => { - channel logo diff --git a/frontend/src/components/forms/Channels.jsx b/frontend/src/components/forms/Channels.jsx index 2d35f3fa..16acc8ee 100644 --- a/frontend/src/components/forms/Channels.jsx +++ b/frontend/src/components/forms/Channels.jsx @@ -2,9 +2,13 @@ import React, { useState, useEffect, useRef } from 'react'; import { useFormik } from 'formik'; import * as Yup from 'yup'; import useChannelsStore from '../../store/channels'; +import useLogosStore from '../../store/logos'; import API from '../../api'; import useStreamProfilesStore from '../../store/streamProfiles'; +import useStreamProfilesStore from '../../store/streamProfiles'; import useStreamsStore from '../../store/streams'; +import { useLogoSelection } from '../../hooks/useSmartLogos'; +import LazyLogo from '../LazyLogo'; import ChannelGroupForm from './ChannelGroup'; import usePlaylistsStore from '../../store/playlists'; import logo from '../../images/logo.png'; @@ -45,8 +49,8 @@ const ChannelsForm = ({ channel = null, isOpen, onClose }) => { const groupListRef = useRef(null); const channelGroups = useChannelsStore((s) => s.channelGroups); - const logos = useChannelsStore((s) => s.logos); - const fetchLogos = useChannelsStore((s) => s.fetchLogos); + const { logos, ensureLogosLoaded } = useLogoSelection(); + const fetchLogos = useLogosStore((s) => s.fetchLogos); const streams = useStreamsStore((state) => state.streams); const streamProfiles = useStreamProfilesStore((s) => s.profiles); const playlists = usePlaylistsStore((s) => s.playlists); @@ -189,10 +193,10 @@ const ChannelsForm = ({ channel = null, isOpen, onClose }) => { formik.resetForm(); API.requeryChannels(); - + // Refresh channel profiles to update the membership information useChannelsStore.getState().fetchChannelProfiles(); - + setSubmitting(false); setTvgFilter(''); setLogoFilter(''); @@ -448,7 +452,12 @@ const ChannelsForm = ({ channel = null, isOpen, onClose }) => { { + setLogoPopoverOpened(opened); + if (opened) { + ensureLogosLoaded(); + } + }} // position="bottom-start" withArrow > @@ -507,13 +516,10 @@ const ChannelsForm = ({ channel = null, isOpen, onClose }) => { - channel logo diff --git a/frontend/src/components/tables/ChannelsTable.jsx b/frontend/src/components/tables/ChannelsTable.jsx index 3380fbd2..2601520e 100644 --- a/frontend/src/components/tables/ChannelsTable.jsx +++ b/frontend/src/components/tables/ChannelsTable.jsx @@ -1,5 +1,6 @@ import React, { useEffect, useMemo, useState, useCallback } from 'react'; import useChannelsStore from '../../store/channels'; +import useLogosStore from '../../store/logos'; import { notifications } from '@mantine/notifications'; import API from '../../api'; import ChannelForm from '../forms/Channel'; @@ -49,6 +50,7 @@ import { getCoreRowModel, flexRender } from '@tanstack/react-table'; import './table.css'; import useChannelsTableStore from '../../store/channelsTable'; import ChannelTableStreams from './ChannelTableStreams'; +import LazyLogo from '../LazyLogo'; import useLocalStorage from '../../hooks/useLocalStorage'; import { CustomTable, useTable } from './CustomTable'; import ChannelsTableOnboarding from './ChannelsTable/ChannelsTableOnboarding'; @@ -244,7 +246,7 @@ const ChannelsTable = ({ }) => { const channels = useChannelsStore((s) => s.channels); const profiles = useChannelsStore((s) => s.profiles); const selectedProfileId = useChannelsStore((s) => s.selectedProfileId); - const logos = useChannelsStore((s) => s.logos); + const logos = useLogosStore((s) => s.logos); const [tablePrefs, setTablePrefs] = useLocalStorage('channel-table-prefs', { pageSize: 50, }); @@ -717,18 +719,11 @@ const ChannelsTable = ({ }) => { header: '', cell: ({ getValue }) => { const logoId = getValue(); - let src = logo; // Default fallback - - if (logoId && logos[logoId]) { - // Try to use cache_url if available, otherwise construct it from the ID - src = - logos[logoId].cache_url || `/api/channels/logos/${logoId}/cache/`; - } return (
- logo diff --git a/frontend/src/components/tables/LogosTable.jsx b/frontend/src/components/tables/LogosTable.jsx index 09c8d38c..a01ddaaa 100644 --- a/frontend/src/components/tables/LogosTable.jsx +++ b/frontend/src/components/tables/LogosTable.jsx @@ -1,7 +1,7 @@ import React, { useMemo, useCallback, useState, useEffect } from 'react'; import API from '../../api'; import LogoForm from '../forms/Logo'; -import useChannelsStore from '../../store/channels'; +import useLogosStore from '../../store/logos'; import useLocalStorage from '../../hooks/useLocalStorage'; import { SquarePlus, @@ -83,7 +83,7 @@ const LogosTable = () => { /** * STORES */ - const { logos, fetchLogos } = useChannelsStore(); + const { logos, fetchLogos } = useLogosStore(); /** * useState diff --git a/frontend/src/components/tables/StreamsTable.jsx b/frontend/src/components/tables/StreamsTable.jsx index 4731c019..c4dea34a 100644 --- a/frontend/src/components/tables/StreamsTable.jsx +++ b/frontend/src/components/tables/StreamsTable.jsx @@ -3,6 +3,7 @@ import API from '../../api'; import StreamForm from '../forms/Stream'; import usePlaylistsStore from '../../store/playlists'; import useChannelsStore from '../../store/channels'; +import useLogosStore from '../../store/logos'; import { copyToClipboard, useDebounce } from '../../utils'; import { SquarePlus, @@ -59,7 +60,7 @@ const StreamRowActions = ({ (state) => state.channels.find((chan) => chan.id === selectedChannelIds[0])?.streams ); - const fetchLogos = useChannelsStore((s) => s.fetchLogos); + const fetchLogos = useLogosStore((s) => s.fetchLogos); const createChannelFromStream = async () => { const selectedChannelProfileId = useChannelsStore.getState().selectedProfileId; diff --git a/frontend/src/hooks/useSmartLogos.jsx b/frontend/src/hooks/useSmartLogos.jsx new file mode 100644 index 00000000..730b9937 --- /dev/null +++ b/frontend/src/hooks/useSmartLogos.jsx @@ -0,0 +1,67 @@ +import { useState, useEffect, useCallback } from 'react'; +import useLogosStore from '../store/logos'; + +/** + * Hook for components that need to display all logos (like logo selection popovers) + * Loads logos on-demand when the component is opened + */ +export const useLogoSelection = () => { + const [isLoading, setIsLoading] = useState(false); + const [isInitialized, setIsInitialized] = useState(false); + + const logos = useLogosStore((s) => s.logos); + const fetchLogos = useLogosStore((s) => s.fetchLogos); // Check if we have a reasonable number of logos loaded + const hasEnoughLogos = Object.keys(logos).length > 0; + + const ensureLogosLoaded = useCallback(async () => { + if (isLoading || (hasEnoughLogos && isInitialized)) { + return; + } + + setIsLoading(true); + try { + await fetchLogos(); + setIsInitialized(true); + } catch (error) { + console.error('Failed to load logos for selection:', error); + } finally { + setIsLoading(false); + } + }, [isLoading, hasEnoughLogos, isInitialized, fetchLogos]); + + return { + logos, + isLoading, + ensureLogosLoaded, + hasLogos: hasEnoughLogos, + }; +}; + +/** + * Hook for components that need specific logos by IDs + */ +export const useLogosById = (logoIds = []) => { + const [isLoading, setIsLoading] = useState(false); + + const logos = useLogosStore((s) => s.logos); + const fetchLogosByIds = useLogosStore((s) => s.fetchLogosByIds); // Find missing logos + const missingIds = logoIds.filter(id => id && !logos[id]); + + useEffect(() => { + if (missingIds.length > 0 && !isLoading) { + setIsLoading(true); + fetchLogosByIds(missingIds) + .then(() => setIsLoading(false)) + .catch((error) => { + console.error('Failed to load logos by IDs:', error); + setIsLoading(false); + }); + } + }, [missingIds.length, isLoading, fetchLogosByIds]); + + return { + logos, + isLoading, + missingLogos: missingIds.length, + }; +}; diff --git a/frontend/src/pages/Guide.jsx b/frontend/src/pages/Guide.jsx index 91e558ee..d35bb7a3 100644 --- a/frontend/src/pages/Guide.jsx +++ b/frontend/src/pages/Guide.jsx @@ -3,6 +3,7 @@ import React, { useMemo, useState, useEffect, useRef } from 'react'; import dayjs from 'dayjs'; import API from '../api'; import useChannelsStore from '../store/channels'; +import useLogosStore from '../store/logos'; import logo from '../images/logo.png'; import useVideoStore from '../store/useVideoStore'; // NEW import import { notifications } from '@mantine/notifications'; @@ -39,7 +40,7 @@ export default function TVChannelGuide({ startDate, endDate }) { const recordings = useChannelsStore((s) => s.recordings); const channelGroups = useChannelsStore((s) => s.channelGroups); const profiles = useChannelsStore((s) => s.profiles); - const logos = useChannelsStore((s) => s.logos); + const logos = useLogosStore((s) => s.logos); const tvgsById = useEPGsStore((s) => s.tvgsById); diff --git a/frontend/src/pages/Logos.jsx b/frontend/src/pages/Logos.jsx index ee26c51e..220e9bab 100644 --- a/frontend/src/pages/Logos.jsx +++ b/frontend/src/pages/Logos.jsx @@ -1,27 +1,31 @@ -import React, { useEffect } from 'react'; +import React, { useEffect, useCallback } from 'react'; import { Box } from '@mantine/core'; import { notifications } from '@mantine/notifications'; -import useChannelsStore from '../store/channels'; +import useLogosStore from '../store/logos'; import LogosTable from '../components/tables/LogosTable'; const LogosPage = () => { - const { fetchLogos } = useChannelsStore(); + const { fetchLogos, logos } = useLogosStore(); - useEffect(() => { - loadLogos(); - }, []); - - const loadLogos = async () => { + const loadLogos = useCallback(async () => { try { - await fetchLogos(); - } catch (error) { + // Only fetch all logos if we don't have any yet + if (Object.keys(logos).length === 0) { + await fetchLogos(); + } + } catch (err) { notifications.show({ title: 'Error', message: 'Failed to load logos', color: 'red', }); + console.error('Failed to load logos:', err); } - }; + }, [fetchLogos, logos]); + + useEffect(() => { + loadLogos(); + }, [loadLogos]); return ( diff --git a/frontend/src/pages/Stats.jsx b/frontend/src/pages/Stats.jsx index d6d4eb89..6e169997 100644 --- a/frontend/src/pages/Stats.jsx +++ b/frontend/src/pages/Stats.jsx @@ -19,6 +19,7 @@ import { import { TableHelper } from '../helpers'; import API from '../api'; import useChannelsStore from '../store/channels'; +import useLogosStore from '../store/logos'; import logo from '../images/logo.png'; import { Gauge, @@ -699,7 +700,7 @@ const ChannelsPage = () => { const channels = useChannelsStore((s) => s.channels); const channelsByUUID = useChannelsStore((s) => s.channelsByUUID); const channelStats = useChannelsStore((s) => s.stats); - const logos = useChannelsStore((s) => s.logos); // Add logos from the store + const logos = useLogosStore((s) => s.logos); // Add logos from the store const streamProfiles = useStreamProfilesStore((s) => s.profiles); const [activeChannels, setActiveChannels] = useState({}); diff --git a/frontend/src/store/auth.jsx b/frontend/src/store/auth.jsx index 80d5e7c3..70bf929c 100644 --- a/frontend/src/store/auth.jsx +++ b/frontend/src/store/auth.jsx @@ -2,6 +2,7 @@ import { create } from 'zustand'; import api from '../api'; import useSettingsStore from './settings'; import useChannelsStore from './channels'; +import useLogosStore from './logos'; import usePlaylistsStore from './playlists'; import useEPGsStore from './epgs'; import useStreamProfilesStore from './streamProfiles'; @@ -47,7 +48,7 @@ const useAuthStore = create((set, get) => ({ await useSettingsStore.getState().fetchSettings(); try { - // Only after settings are loaded, fetch the dependent data + // Load essential data first (without all logos) await Promise.all([ useChannelsStore.getState().fetchChannels(), useChannelsStore.getState().fetchChannelGroups(), @@ -55,17 +56,25 @@ const useAuthStore = create((set, get) => ({ usePlaylistsStore.getState().fetchPlaylists(), useEPGsStore.getState().fetchEPGs(), useEPGsStore.getState().fetchEPGData(), - useChannelsStore.getState().fetchLogos(), useStreamProfilesStore.getState().fetchProfiles(), useUserAgentsStore.getState().fetchUserAgents(), useVODStore.getState().fetchCategories(), // Add VOD categories ]); + // Load only logos that are currently used by channels (much faster) + await useLogosStore.getState().fetchUsedLogos(); + if (user.user_level >= USER_LEVELS.ADMIN) { await Promise.all([useUsersStore.getState().fetchUsers()]); } set({ user, isAuthenticated: true }); + + // Start background loading of remaining logos after login is complete + setTimeout(() => { + useLogosStore.getState().fetchLogosInBackground(); + }, 2000); // 2 second delay to let UI settle + } catch (error) { console.error('Error initializing data:', error); } diff --git a/frontend/src/store/channels.jsx b/frontend/src/store/channels.jsx index b32975c5..ca2d0af9 100644 --- a/frontend/src/store/channels.jsx +++ b/frontend/src/store/channels.jsx @@ -14,7 +14,6 @@ const useChannelsStore = create((set, get) => ({ stats: {}, activeChannels: {}, activeClients: {}, - logos: {}, recordings: [], isLoading: false, error: null, @@ -215,52 +214,6 @@ const useChannelsStore = create((set, get) => ({ return { channelGroups: remainingGroups }; }), - fetchLogos: async () => { - set({ isLoading: true, error: null }); - try { - const logos = await api.getLogos(); - set({ - logos: logos.reduce((acc, logo) => { - acc[logo.id] = { - ...logo, - }; - return acc; - }, {}), - isLoading: false, - }); - } catch (error) { - console.error('Failed to fetch logos:', error); - set({ error: 'Failed to load logos.', isLoading: false }); - } - }, - - addLogo: (newLogo) => - set((state) => ({ - logos: { - ...state.logos, - [newLogo.id]: { - ...newLogo, - }, - }, - })), - - updateLogo: (logo) => - set((state) => ({ - logos: { - ...state.logos, - [logo.id]: { - ...logo, - }, - }, - })), - - removeLogo: (logoId) => - set((state) => { - const newLogos = { ...state.logos }; - delete newLogos[logoId]; - return { logos: newLogos }; - }), - addProfile: (profile) => set((state) => ({ profiles: { @@ -348,10 +301,10 @@ const useChannelsStore = create((set, get) => ({ }), setChannelsPageSelection: (channelsPageSelection) => - set((state) => ({ channelsPageSelection })), + set(() => ({ channelsPageSelection })), setSelectedProfileId: (id) => - set((state) => ({ + set(() => ({ selectedProfileId: id, })), diff --git a/frontend/src/store/logos.jsx b/frontend/src/store/logos.jsx new file mode 100644 index 00000000..04e56099 --- /dev/null +++ b/frontend/src/store/logos.jsx @@ -0,0 +1,142 @@ +import { create } from 'zustand'; +import api from '../api'; + +const useLogosStore = create((set, get) => ({ + logos: {}, + isLoading: false, + error: null, + + // Basic CRUD operations + setLogos: (logos) => { + set({ + logos: logos.reduce((acc, logo) => { + acc[logo.id] = { ...logo }; + return acc; + }, {}), + }); + }, + + addLogo: (newLogo) => + set((state) => ({ + logos: { + ...state.logos, + [newLogo.id]: { ...newLogo }, + }, + })), + + updateLogo: (logo) => + set((state) => ({ + logos: { + ...state.logos, + [logo.id]: { ...logo }, + }, + })), + + removeLogo: (logoId) => + set((state) => { + const newLogos = { ...state.logos }; + delete newLogos[logoId]; + return { logos: newLogos }; + }), + + // Smart loading methods + fetchLogos: async () => { + set({ isLoading: true, error: null }); + try { + const logos = await api.getLogos(); + set({ + logos: logos.reduce((acc, logo) => { + acc[logo.id] = { ...logo }; + return acc; + }, {}), + isLoading: false, + }); + return logos; + } catch (error) { + console.error('Failed to fetch logos:', error); + set({ error: 'Failed to load logos.', isLoading: false }); + throw error; + } + }, + + fetchUsedLogos: async () => { + set({ isLoading: true, error: null }); + try { + const logos = await api.getLogos({ used: 'true' }); + set((state) => ({ + logos: { + ...state.logos, + ...logos.reduce((acc, logo) => { + acc[logo.id] = { ...logo }; + return acc; + }, {}), + }, + isLoading: false, + })); + return logos; + } catch (error) { + console.error('Failed to fetch used logos:', error); + set({ error: 'Failed to load used logos.', isLoading: false }); + throw error; + } + }, + + fetchLogosByIds: async (logoIds) => { + if (!logoIds || logoIds.length === 0) return []; + + try { + // Filter out logos we already have + const missingIds = logoIds.filter(id => !get().logos[id]); + if (missingIds.length === 0) return []; + + const logos = await api.getLogosByIds(missingIds); + set((state) => ({ + logos: { + ...state.logos, + ...logos.reduce((acc, logo) => { + acc[logo.id] = { ...logo }; + return acc; + }, {}), + }, + })); + return logos; + } catch (error) { + console.error('Failed to fetch logos by IDs:', error); + throw error; + } + }, + + fetchLogosInBackground: async () => { + try { + // Load all remaining logos in background + const allLogos = await api.getLogos(); + set((state) => ({ + logos: { + ...state.logos, + ...allLogos.reduce((acc, logo) => { + acc[logo.id] = { ...logo }; + return acc; + }, {}), + }, + })); + } catch (error) { + console.error('Background logo loading failed:', error); + // Don't throw error for background loading + } + }, + + // Helper methods + getLogoById: (logoId) => { + return get().logos[logoId] || null; + }, + + hasLogo: (logoId) => { + return !!get().logos[logoId]; + }, + + getLogosCount: () => { + return Object.keys(get().logos).length; + }, +})); + +export default useLogosStore;