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 (
+
{
+ 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 }) => {
-
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 }) => {
-
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 (
-
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;