Enhance Logo management with filtering and usage details in API and UI

This commit is contained in:
SergeantPanda 2025-07-15 20:26:02 -05:00
parent 5f08d0fbbf
commit cd30f6da66
4 changed files with 155 additions and 7 deletions

View file

@ -1040,6 +1040,24 @@ class LogoViewSet(viewsets.ModelViewSet):
except KeyError:
return [Authenticated()]
def get_queryset(self):
"""Optimize queryset with prefetch and add filtering"""
queryset = Logo.objects.prefetch_related('channels').order_by('name')
# Filter by usage
used_filter = self.request.query_params.get('used', None)
if used_filter == 'true':
queryset = queryset.filter(channels__isnull=False).distinct()
elif used_filter == 'false':
queryset = queryset.filter(channels__isnull=True)
# Filter by name
name_filter = self.request.query_params.get('name', None)
if name_filter:
queryset = queryset.filter(name__icontains=name_filter)
return queryset
def create(self, request, *args, **kwargs):
"""Create a new logo entry"""
serializer = self.get_serializer(data=request.data)

View file

@ -20,10 +20,13 @@ from django.utils import timezone
class LogoSerializer(serializers.ModelSerializer):
cache_url = serializers.SerializerMethodField()
channel_count = serializers.SerializerMethodField()
is_used = serializers.SerializerMethodField()
channel_names = serializers.SerializerMethodField()
class Meta:
model = Logo
fields = ["id", "name", "url", "cache_url"]
fields = ["id", "name", "url", "cache_url", "channel_count", "is_used", "channel_names"]
def get_cache_url(self, obj):
# return f"/api/channels/logos/{obj.id}/cache/"
@ -34,6 +37,22 @@ 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()
def get_is_used(self, obj):
"""Check if this logo is used by any channels"""
return obj.channels.exists()
def get_channel_names(self, obj):
"""Get the names of channels using this logo (limited to first 5)"""
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")
return names
#
# Stream

View file

@ -1225,9 +1225,10 @@ export default class API {
}
}
static async getLogos() {
static async getLogos(params = {}) {
try {
const response = await request(`${host}/api/channels/logos/`);
const queryParams = new URLSearchParams(params);
const response = await request(`${host}/api/channels/logos/?${queryParams.toString()}`);
return response;
} catch (e) {
@ -1235,6 +1236,16 @@ export default class API {
}
}
static async fetchLogos() {
try {
const response = await this.getLogos();
useChannelsStore.getState().setLogos(response);
return response;
} catch (e) {
errorNotification('Failed to fetch logos', e);
}
}
static async uploadLogo(file) {
try {
const formData = new FormData();

View file

@ -8,6 +8,7 @@ import {
SquareMinus,
SquarePen,
ExternalLink,
Filter,
} from 'lucide-react';
import {
ActionIcon,
@ -22,6 +23,11 @@ import {
Stack,
Image,
Center,
Badge,
Tooltip,
Select,
TextInput,
Menu,
} from '@mantine/core';
import { CustomTable, useTable } from './CustomTable';
import ConfirmationDialog from '../ConfirmationDialog';
@ -83,6 +89,10 @@ const LogosTable = () => {
const [deleteTarget, setDeleteTarget] = useState(null);
const [logoToDelete, setLogoToDelete] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const [filters, setFilters] = useState({
name: '',
used: 'all'
});
/**
* Functions
@ -155,6 +165,42 @@ const LogosTable = () => {
</Text>
),
},
{
header: 'Usage',
accessorKey: 'channel_count',
size: 120,
cell: ({ getValue, row }) => {
const count = getValue();
const channelNames = row.original.channel_names || [];
if (count === 0) {
return (
<Badge size="sm" variant="light" color="gray">
Unused
</Badge>
);
}
return (
<Tooltip
label={
<div>
<Text size="xs" fw={600}>Used by {count} channel{count !== 1 ? 's' : ''}:</Text>
{channelNames.map((name, index) => (
<Text key={index} size="xs"> {name}</Text>
))}
</div>
}
multiline
width={220}
>
<Badge size="sm" variant="light" color="blue">
{count} channel{count !== 1 ? 's' : ''}
</Badge>
</Tooltip>
);
},
},
{
header: 'URL',
accessorKey: 'url',
@ -211,8 +257,24 @@ const LogosTable = () => {
const data = useMemo(() => {
const logosArray = Object.values(logos || {});
return logosArray.sort((a, b) => a.id - b.id);
}, [logos]);
// Apply filters
let filteredLogos = logosArray;
if (filters.name) {
filteredLogos = filteredLogos.filter(logo =>
logo.name.toLowerCase().includes(filters.name.toLowerCase())
);
}
if (filters.used === 'used') {
filteredLogos = filteredLogos.filter(logo => logo.is_used);
} else if (filters.used === 'unused') {
filteredLogos = filteredLogos.filter(logo => !logo.is_used);
}
return filteredLogos.sort((a, b) => a.id - b.id);
}, [logos, filters]);
const renderHeaderCell = (header) => {
return (
@ -238,6 +300,7 @@ const LogosTable = () => {
cache_url: renderHeaderCell,
name: renderHeaderCell,
url: renderHeaderCell,
channel_count: renderHeaderCell,
},
});
@ -282,11 +345,44 @@ const LogosTable = () => {
<Box
style={{
display: 'flex',
justifyContent: 'flex-end',
justifyContent: 'space-between',
alignItems: 'center',
padding: '16px',
borderBottom: '1px solid #3f3f46',
}}
>
<Group gap="sm">
<TextInput
placeholder="Filter by name..."
value={filters.name}
onChange={(event) =>
setFilters(prev => ({
...prev,
name: event.currentTarget.value
}))
}
size="xs"
style={{ width: 200 }}
/>
<Select
placeholder="Usage filter"
value={filters.used}
onChange={(value) =>
setFilters(prev => ({
...prev,
used: value
}))
}
data={[
{ value: 'all', label: 'All logos' },
{ value: 'used', label: 'Used only' },
{ value: 'unused', label: 'Unused only' },
]}
size="xs"
style={{ width: 140 }}
/>
</Group>
<Button
leftSection={<SquarePlus size={18} />}
variant="light"
@ -336,7 +432,11 @@ const LogosTable = () => {
logoToDelete ? (
<div>
Are you sure you want to delete the logo "{logoToDelete.name}"?
<br />
{logoToDelete.channel_count > 0 && (
<Text size="sm" c="orange" mt="xs">
Warning: This logo is currently used by {logoToDelete.channel_count} channel{logoToDelete.channel_count !== 1 ? 's' : ''}.
</Text>
)}
<Text size="sm" c="dimmed" mt="xs">
This action cannot be undone.
</Text>