mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-07-28 12:36:42 +00:00
Major rework of how logos are handle. We will no longer load logos on login and they will only be loaded into the store when needed.
This commit is contained in:
parent
85a0b584a5
commit
b92d7c2c21
9 changed files with 409 additions and 187 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
),
|
||||
]
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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()}`
|
||||
|
|
|
|||
|
|
@ -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={
|
||||
<div>
|
||||
<Text size="xs" fw={600}>
|
||||
Used by {count} channel{count !== 1 ? 's' : ''}:
|
||||
Used by {count} item{count !== 1 ? 's' : ''}:
|
||||
</Text>
|
||||
{channelNames.map((name, index) => (
|
||||
<Text key={index} size="xs">
|
||||
|
|
@ -429,7 +429,7 @@ const LogosTable = () => {
|
|||
width={220}
|
||||
>
|
||||
<Badge size="sm" variant="light" color="blue">
|
||||
{count} channel{count !== 1 ? 's' : ''}
|
||||
{count} item{count !== 1 ? 's' : ''}
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
);
|
||||
|
|
@ -689,7 +689,7 @@ const LogosTable = () => {
|
|||
}}
|
||||
>
|
||||
<div>
|
||||
<LoadingOverlay visible={isLoading} />
|
||||
<LoadingOverlay visible={isLoading || storeLoading} />
|
||||
<CustomTable table={table} />
|
||||
</div>
|
||||
</Box>
|
||||
|
|
@ -758,7 +758,8 @@ const LogosTable = () => {
|
|||
Are you sure you want to delete {selectedRows.size} selected
|
||||
logos?
|
||||
<Text size="sm" c="dimmed" mt="xs">
|
||||
Any channels using these logos will have their logo removed.
|
||||
Any channels, movies, or series using these logos will have
|
||||
their logo removed.
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed" mt="xs">
|
||||
This action cannot be undone.
|
||||
|
|
@ -770,8 +771,8 @@ const LogosTable = () => {
|
|||
{logoToDelete.channel_count > 0 && (
|
||||
<Text size="sm" c="orange" mt="xs">
|
||||
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.
|
||||
</Text>
|
||||
)}
|
||||
<Text size="sm" c="dimmed" mt="xs">
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<Box style={{ padding: 10 }}>
|
||||
<LogosTable />
|
||||
</Box>
|
||||
);
|
||||
return (
|
||||
<Box style={{ padding: 10 }}>
|
||||
{isLoading && (
|
||||
<Center style={{ marginBottom: 20 }}>
|
||||
<Stack align="center" spacing="sm">
|
||||
<Loader size="sm" />
|
||||
<Text size="sm" color="dimmed">
|
||||
Loading all logos...
|
||||
</Text>
|
||||
</Stack>
|
||||
</Center>
|
||||
)}
|
||||
<LogosTable />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default LogosPage;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue