mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-07-29 04:50:20 +00:00
Enhance Logo management with filtering and usage details in API and UI
This commit is contained in:
parent
5f08d0fbbf
commit
cd30f6da66
4 changed files with 155 additions and 7 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue