diff --git a/apps/channels/api_views.py b/apps/channels/api_views.py
index 849a0c9b..0a4ae81e 100644
--- a/apps/channels/api_views.py
+++ b/apps/channels/api_views.py
@@ -44,6 +44,7 @@ import django_filters
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.filters import SearchFilter, OrderingFilter
from apps.epg.models import EPGData
+from apps.vod.models import Movie, Series
from django.db.models import Q
from django.http import StreamingHttpResponse, FileResponse, Http404
import mimetypes
@@ -1206,7 +1207,7 @@ class CleanupUnusedLogosAPIView(APIView):
return [Authenticated()]
@swagger_auto_schema(
- operation_description="Delete all logos that are not used by any channels",
+ operation_description="Delete all logos that are not used by any channels, movies, or series",
request_body=openapi.Schema(
type=openapi.TYPE_OBJECT,
properties={
@@ -1220,10 +1221,24 @@ class CleanupUnusedLogosAPIView(APIView):
responses={200: "Cleanup completed"},
)
def post(self, request):
- """Delete all logos with no channel associations"""
+ """Delete all logos with no channel, movie, or series associations"""
delete_files = request.data.get("delete_files", False)
- unused_logos = Logo.objects.filter(channels__isnull=True)
+ # Find logos that are not used by channels, movies, or series
+ filter_conditions = Q(channels__isnull=True)
+
+ # Add VOD conditions if models are available
+ try:
+ filter_conditions &= Q(movie_set__isnull=True)
+ except:
+ pass
+
+ try:
+ filter_conditions &= Q(series_set__isnull=True)
+ except:
+ pass
+
+ unused_logos = Logo.objects.filter(filter_conditions)
deleted_count = unused_logos.count()
logo_names = list(unused_logos.values_list('name', flat=True))
local_files_deleted = 0
@@ -1259,9 +1274,23 @@ class CleanupUnusedLogosAPIView(APIView):
})
+class LogoPagination(PageNumberPagination):
+ page_size = 50 # Default page size to match frontend default
+ page_size_query_param = "page_size" # Allow clients to specify page size
+ max_page_size = 1000 # Prevent excessive page sizes
+
+ def paginate_queryset(self, queryset, request, view=None):
+ # Check if pagination should be disabled for specific requests
+ if request.query_params.get('no_pagination') == 'true':
+ return None # disables pagination, returns full queryset
+
+ return super().paginate_queryset(queryset, request, view)
+
+
class LogoViewSet(viewsets.ModelViewSet):
queryset = Logo.objects.all()
serializer_class = LogoSerializer
+ pagination_class = LogoPagination
parser_classes = (MultiPartParser, FormParser, JSONParser)
def get_permissions(self):
@@ -1278,8 +1307,16 @@ class LogoViewSet(viewsets.ModelViewSet):
def get_queryset(self):
"""Optimize queryset with prefetch and add filtering"""
+ # Start with basic prefetch for channels
queryset = Logo.objects.prefetch_related('channels').order_by('name')
+ # Try to prefetch VOD relations if available
+ try:
+ queryset = queryset.prefetch_related('movie', 'series')
+ except:
+ # VOD app might not be available, continue without VOD prefetch
+ pass
+
# Filter by specific IDs
ids = self.request.query_params.getlist('ids')
if ids:
@@ -1292,12 +1329,41 @@ class LogoViewSet(viewsets.ModelViewSet):
pass # Invalid IDs, return empty queryset
queryset = Logo.objects.none()
- # Filter by usage
+ # Filter by usage - now includes VOD content
used_filter = self.request.query_params.get('used', None)
if used_filter == 'true':
- queryset = queryset.filter(channels__isnull=False).distinct()
+ # Logo is used if it has any channels, movies, or series
+ filter_conditions = Q(channels__isnull=False)
+
+ # Add VOD conditions if models are available
+ try:
+ filter_conditions |= Q(movie__isnull=False)
+ except:
+ pass
+
+ try:
+ filter_conditions |= Q(series__isnull=False)
+ except:
+ pass
+
+ queryset = queryset.filter(filter_conditions).distinct()
+
elif used_filter == 'false':
- queryset = queryset.filter(channels__isnull=True)
+ # Logo is unused if it has no channels, movies, or series
+ filter_conditions = Q(channels__isnull=True)
+
+ # Add VOD conditions if models are available
+ try:
+ filter_conditions &= Q(movie__isnull=True)
+ except:
+ pass
+
+ try:
+ filter_conditions &= Q(series__isnull=True)
+ except:
+ pass
+
+ queryset = queryset.filter(filter_conditions)
# Filter by name
name_filter = self.request.query_params.get('name', None)
diff --git a/apps/channels/serializers.py b/apps/channels/serializers.py
index 72320840..0b29353e 100644
--- a/apps/channels/serializers.py
+++ b/apps/channels/serializers.py
@@ -61,19 +61,81 @@ class LogoSerializer(serializers.ModelSerializer):
return reverse("api:channels:logo-cache", args=[obj.id])
def get_channel_count(self, obj):
- """Get the number of channels using this logo"""
- return obj.channels.count()
+ """Get the number of channels, movies, and series using this logo"""
+ channel_count = obj.channels.count()
+
+ # Safely get movie count
+ try:
+ movie_count = obj.movie.count() if hasattr(obj, 'movie') else 0
+ except AttributeError:
+ movie_count = 0
+
+ # Safely get series count
+ try:
+ series_count = obj.series.count() if hasattr(obj, 'series') else 0
+ except AttributeError:
+ series_count = 0
+
+ return channel_count + movie_count + series_count
def get_is_used(self, obj):
- """Check if this logo is used by any channels"""
- return obj.channels.exists()
+ """Check if this logo is used by any channels, movies, or series"""
+ # Check if used by channels
+ if obj.channels.exists():
+ return True
+
+ # Check if used by movies (handle case where VOD app might not be available)
+ try:
+ if hasattr(obj, 'movie') and obj.movie.exists():
+ return True
+ except AttributeError:
+ pass
+
+ # Check if used by series (handle case where VOD app might not be available)
+ try:
+ if hasattr(obj, 'series') and obj.series.exists():
+ return True
+ except AttributeError:
+ pass
+
+ return False
def get_channel_names(self, obj):
- """Get the names of channels using this logo (limited to first 5)"""
+ """Get the names of channels, movies, and series using this logo (limited to first 5)"""
+ names = []
+
+ # Get channel names
channels = obj.channels.all()[:5]
- names = [channel.name for channel in channels]
- if obj.channels.count() > 5:
- names.append(f"...and {obj.channels.count() - 5} more")
+ for channel in channels:
+ names.append(f"Channel: {channel.name}")
+
+ # Get movie names (only if we haven't reached limit)
+ if len(names) < 5:
+ try:
+ if hasattr(obj, 'movie'):
+ remaining_slots = 5 - len(names)
+ movies = obj.movie.all()[:remaining_slots]
+ for movie in movies:
+ names.append(f"Movie: {movie.name}")
+ except AttributeError:
+ pass
+
+ # Get series names (only if we haven't reached limit)
+ if len(names) < 5:
+ try:
+ if hasattr(obj, 'series'):
+ remaining_slots = 5 - len(names)
+ series = obj.series.all()[:remaining_slots]
+ for series_item in series:
+ names.append(f"Series: {series_item.name}")
+ except AttributeError:
+ pass
+
+ # Calculate total count for "more" message
+ total_count = self.get_channel_count(obj)
+ if total_count > 5:
+ names.append(f"...and {total_count - 5} more")
+
return names
diff --git a/apps/vod/migrations/0002_alter_movie_logo_alter_series_logo.py b/apps/vod/migrations/0002_alter_movie_logo_alter_series_logo.py
new file mode 100644
index 00000000..f3dbf7ea
--- /dev/null
+++ b/apps/vod/migrations/0002_alter_movie_logo_alter_series_logo.py
@@ -0,0 +1,25 @@
+# Generated by Django 5.2.4 on 2025-08-26 16:52
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('dispatcharr_channels', '0024_alter_channelgroupm3uaccount_channel_group'),
+ ('vod', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='movie',
+ name='logo',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='movie', to='dispatcharr_channels.logo'),
+ ),
+ migrations.AlterField(
+ model_name='series',
+ name='logo',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='series', to='dispatcharr_channels.logo'),
+ ),
+ ]
diff --git a/apps/vod/models.py b/apps/vod/models.py
index eccef94f..0e799144 100644
--- a/apps/vod/models.py
+++ b/apps/vod/models.py
@@ -69,7 +69,7 @@ class Series(models.Model):
year = models.IntegerField(blank=True, null=True)
rating = models.CharField(max_length=10, blank=True, null=True)
genre = models.CharField(max_length=255, blank=True, null=True)
- logo = models.ForeignKey(Logo, on_delete=models.SET_NULL, null=True, blank=True)
+ logo = models.ForeignKey(Logo, on_delete=models.SET_NULL, null=True, blank=True, related_name='series')
# Metadata IDs for deduplication - these should be globally unique when present
tmdb_id = models.CharField(max_length=50, blank=True, null=True, unique=True, help_text="TMDB ID for metadata")
@@ -108,7 +108,7 @@ class Movie(models.Model):
rating = models.CharField(max_length=10, blank=True, null=True)
genre = models.CharField(max_length=255, blank=True, null=True)
duration_secs = models.IntegerField(blank=True, null=True, help_text="Duration in seconds")
- logo = models.ForeignKey(Logo, on_delete=models.SET_NULL, null=True, blank=True)
+ logo = models.ForeignKey(Logo, on_delete=models.SET_NULL, null=True, blank=True, related_name='movie')
# Metadata IDs for deduplication - these should be globally unique when present
tmdb_id = models.CharField(max_length=50, blank=True, null=True, unique=True, help_text="TMDB ID for metadata")
diff --git a/frontend/src/api.js b/frontend/src/api.js
index a197ae4e..982eae78 100644
--- a/frontend/src/api.js
+++ b/frontend/src/api.js
@@ -1310,6 +1310,8 @@ export default class API {
const params = new URLSearchParams();
logoIds.forEach(id => params.append('ids', id));
+ // Disable pagination for ID-based queries to get all matching logos
+ params.append('no_pagination', 'true');
const response = await request(
`${host}/api/channels/logos/?${params.toString()}`
diff --git a/frontend/src/components/tables/LogosTable.jsx b/frontend/src/components/tables/LogosTable.jsx
index ae48af1f..70cbaeb6 100644
--- a/frontend/src/components/tables/LogosTable.jsx
+++ b/frontend/src/components/tables/LogosTable.jsx
@@ -83,7 +83,7 @@ const LogosTable = () => {
/**
* STORES
*/
- const { logos, fetchLogos } = useLogosStore();
+ const { logos, fetchLogos, isLoading: storeLoading } = useLogosStore();
/**
* useState
@@ -416,7 +416,7 @@ const LogosTable = () => {
label={
- Used by {count} channel{count !== 1 ? 's' : ''}:
+ Used by {count} item{count !== 1 ? 's' : ''}:
{channelNames.map((name, index) => (
@@ -429,7 +429,7 @@ const LogosTable = () => {
width={220}
>
- {count} channel{count !== 1 ? 's' : ''}
+ {count} item{count !== 1 ? 's' : ''}
);
@@ -689,7 +689,7 @@ const LogosTable = () => {
}}
>
-
+
@@ -758,7 +758,8 @@ const LogosTable = () => {
Are you sure you want to delete {selectedRows.size} selected
logos?
- Any channels using these logos will have their logo removed.
+ Any channels, movies, or series using these logos will have
+ their logo removed.
This action cannot be undone.
@@ -770,8 +771,8 @@ const LogosTable = () => {
{logoToDelete.channel_count > 0 && (
This logo is currently used by {logoToDelete.channel_count}{' '}
- channel{logoToDelete.channel_count !== 1 ? 's' : ''}. They
- will have their logo removed.
+ item{logoToDelete.channel_count !== 1 ? 's' : ''}. They will
+ have their logo removed.
)}
diff --git a/frontend/src/pages/Logos.jsx b/frontend/src/pages/Logos.jsx
index 220e9bab..92a4aab8 100644
--- a/frontend/src/pages/Logos.jsx
+++ b/frontend/src/pages/Logos.jsx
@@ -1,37 +1,47 @@
import React, { useEffect, useCallback } from 'react';
-import { Box } from '@mantine/core';
+import { Box, Loader, Center, Text, Stack } from '@mantine/core';
import { notifications } from '@mantine/notifications';
import useLogosStore from '../store/logos';
import LogosTable from '../components/tables/LogosTable';
const LogosPage = () => {
- const { fetchLogos, logos } = useLogosStore();
+ const { fetchAllLogos, isLoading, needsAllLogos } = useLogosStore();
- const loadLogos = useCallback(async () => {
- try {
- // 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]);
+ const loadLogos = useCallback(async () => {
+ try {
+ // Only fetch all logos if we haven't loaded them yet
+ if (needsAllLogos()) {
+ await fetchAllLogos();
+ }
+ } catch (err) {
+ notifications.show({
+ title: 'Error',
+ message: 'Failed to load logos',
+ color: 'red',
+ });
+ console.error('Failed to load logos:', err);
+ }
+ }, [fetchAllLogos, needsAllLogos]);
- useEffect(() => {
- loadLogos();
- }, [loadLogos]);
+ useEffect(() => {
+ loadLogos();
+ }, [loadLogos]);
- return (
-
-
-
- );
+ return (
+
+ {isLoading && (
+
+
+
+
+ Loading all logos...
+
+
+
+ )}
+
+
+ );
};
export default LogosPage;
diff --git a/frontend/src/store/auth.jsx b/frontend/src/store/auth.jsx
index 70bf929c..e3fc7f57 100644
--- a/frontend/src/store/auth.jsx
+++ b/frontend/src/store/auth.jsx
@@ -2,7 +2,6 @@ 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';
@@ -10,7 +9,6 @@ import useUserAgentsStore from './userAgents';
import useUsersStore from './users';
import API from '../api';
import { USER_LEVELS } from '../constants';
-import useVODStore from './useVODStore';
const decodeToken = (token) => {
if (!token) return null;
@@ -48,7 +46,7 @@ const useAuthStore = create((set, get) => ({
await useSettingsStore.getState().fetchSettings();
try {
- // Load essential data first (without all logos)
+ // Only after settings are loaded, fetch the essential data
await Promise.all([
useChannelsStore.getState().fetchChannels(),
useChannelsStore.getState().fetchChannelGroups(),
@@ -58,23 +56,13 @@ const useAuthStore = create((set, get) => ({
useEPGsStore.getState().fetchEPGData(),
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/logos.jsx b/frontend/src/store/logos.jsx
index 04e56099..441997d6 100644
--- a/frontend/src/store/logos.jsx
+++ b/frontend/src/store/logos.jsx
@@ -2,141 +2,209 @@ import { create } from 'zustand';
import api from '../api';
const useLogosStore = create((set, get) => ({
- logos: {},
- isLoading: false,
- error: null,
+ logos: {},
+ isLoading: false,
+ backgroundLoading: false,
+ hasLoadedAll: false, // Track if we've loaded all logos
+ error: null,
- // Basic CRUD operations
- setLogos: (logos) => {
- set({
- logos: logos.reduce((acc, logo) => {
- acc[logo.id] = { ...logo };
- return acc;
+ // 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 (pageSize = 100) => {
+ set({ isLoading: true, error: null });
+ try {
+ const response = await api.getLogos({ page_size: pageSize });
+
+ // Handle both paginated and non-paginated responses
+ const logos = Array.isArray(response) ? response : response.results || [];
+
+ set({
+ logos: logos.reduce((acc, logo) => {
+ acc[logo.id] = { ...logo };
+ return acc;
+ }, {}),
+ isLoading: false,
+ });
+ return response;
+ } catch (error) {
+ console.error('Failed to fetch logos:', error);
+ set({ error: 'Failed to load logos.', isLoading: false });
+ throw error;
+ }
+ },
+
+ fetchAllLogos: async () => {
+ set({ isLoading: true, error: null });
+ try {
+ // Disable pagination to get all logos for management interface
+ const response = await api.getLogos({ no_pagination: 'true' });
+
+ // Handle both paginated and non-paginated responses
+ const logos = Array.isArray(response) ? response : response.results || [];
+
+ set({
+ logos: logos.reduce((acc, logo) => {
+ acc[logo.id] = { ...logo };
+ return acc;
+ }, {}),
+ hasLoadedAll: true, // Mark that we've loaded all logos
+ isLoading: false,
+ });
+ return logos;
+ } catch (error) {
+ console.error('Failed to fetch all logos:', error);
+ set({ error: 'Failed to load all logos.', isLoading: false });
+ throw error;
+ }
+ },
+
+ fetchUsedLogos: async (pageSize = 100) => {
+ set({ isLoading: true, error: null });
+ try {
+ // Load used logos with pagination for better performance
+ const response = await api.getLogos({
+ used: 'true',
+ page_size: pageSize,
+ });
+
+ // Handle both paginated and non-paginated responses
+ const logos = Array.isArray(response) ? response : response.results || [];
+
+ set((state) => ({
+ logos: {
+ ...state.logos,
+ ...logos.reduce((acc, logo) => {
+ acc[logo.id] = { ...logo };
+ return acc;
+ }, {}),
+ },
+ isLoading: false,
+ }));
+ return response;
+ } 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 response = await api.getLogosByIds(missingIds);
+
+ // Handle both paginated and non-paginated responses
+ const logos = Array.isArray(response) ? response : response.results || [];
+
+ 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 () => {
+ set({ backgroundLoading: true });
+ try {
+ // Load logos in chunks using pagination for better performance
+ let page = 1;
+ const pageSize = 200;
+ let hasMore = true;
+
+ while (hasMore) {
+ const response = await api.getLogos({ page, page_size: pageSize });
+
+ set((state) => ({
+ logos: {
+ ...state.logos,
+ ...response.results.reduce((acc, logo) => {
+ acc[logo.id] = { ...logo };
+ return acc;
}, {}),
- });
- },
+ },
+ }));
- addLogo: (newLogo) =>
- set((state) => ({
- logos: {
- ...state.logos,
- [newLogo.id]: { ...newLogo },
- },
- })),
+ // Check if there are more pages
+ hasMore = !!response.next;
+ page++;
- 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;
+ // Add a small delay between chunks to avoid overwhelming the server
+ if (hasMore) {
+ await new Promise((resolve) => setTimeout(resolve, 100));
}
- },
+ }
+ } catch (error) {
+ console.error('Background logo loading failed:', error);
+ // Don't throw error for background loading
+ } finally {
+ set({ backgroundLoading: false });
+ }
+ },
- 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;
- }
- },
+ // Helper methods
+ getLogoById: (logoId) => {
+ return get().logos[logoId] || null;
+ },
- fetchLogosByIds: async (logoIds) => {
- if (!logoIds || logoIds.length === 0) return [];
+ hasLogo: (logoId) => {
+ return !!get().logos[logoId];
+ },
- try {
- // Filter out logos we already have
- const missingIds = logoIds.filter(id => !get().logos[id]);
- if (missingIds.length === 0) return [];
+ getLogosCount: () => {
+ return Object.keys(get().logos).length;
+ },
- 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;
- },
+ // Check if we need to fetch all logos (haven't loaded them yet or store is empty)
+ needsAllLogos: () => {
+ const state = get();
+ return !state.hasLoadedAll || Object.keys(state.logos).length === 0;
+ },
}));
export default useLogosStore;