Enhance channel settings to include icon image support for IPTV
ci / build-backend (push) Successful in 1m18s
ci / build-frontend (push) Successful in 39s
ci / tests (push) Successful in 1m22s
ci / sonar (push) Successful in 3m41s

Updated the Channel and related DTOs to incorporate an optional IconImageId property, allowing for the specification of a channel icon for external IPTV players. Modified the UpdateChannelSettings command and its handler to accommodate this new property. Adjusted frontend components to manage the icon image, including selection and display functionality. Updated localization files to support new UI elements related to the IPTV icon. This enhancement improves the user experience by providing a visual representation of channels in IPTV applications.
This commit is contained in:
Leonid Pershin
2026-08-02 17:07:24 +03:00
parent bed6d4fcfc
commit 7a164dbd90
27 changed files with 1758 additions and 28 deletions
@@ -43,6 +43,7 @@ type ChannelSettingsBody = {
name: string
isEnabled: boolean
fillerAssetId: string | null
iconImageId: string | null
}
export function updateChannelSettings(id: string, body: ChannelSettingsBody) {
@@ -1,6 +1,8 @@
import { useMutation } from '@tanstack/react-query'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { imageUrl } from '@/features/admin/images/api'
import { ImageGallery } from '@/features/admin/images/ImageGallery'
import type { ChannelDto } from '@/shared/api/types'
import { Button } from '@/shared/ui/button'
import { Input } from '@/shared/ui/input'
@@ -27,6 +29,8 @@ export function SettingsCard({
const [name, setName] = useState(channel.name)
const [isEnabled, setIsEnabled] = useState(channel.isEnabled)
const [fillerAssetId, setFillerAssetId] = useState(channel.fillerAssetId ?? '')
const [iconImageId, setIconImageId] = useState(channel.iconImageId ?? '')
const [iconGalleryOpen, setIconGalleryOpen] = useState(false)
const [number, setNumber] = useState(channel.number?.toString() ?? '')
const [offsetHours, setOffsetHours] = useState(channel.utcOffsetMinutes / 60)
// Начало вещательных суток приходит как «06:00:00» — в поле нужен формат «06:00».
@@ -36,6 +40,7 @@ export function SettingsCard({
setName(channel.name)
setIsEnabled(channel.isEnabled)
setFillerAssetId(channel.fillerAssetId ?? '')
setIconImageId(channel.iconImageId ?? '')
setNumber(channel.number?.toString() ?? '')
setOffsetHours(channel.utcOffsetMinutes / 60)
setDayStart(channel.dayStartTime.slice(0, 5))
@@ -48,6 +53,7 @@ export function SettingsCard({
name: name.trim(),
isEnabled,
fillerAssetId: fillerAssetId || null,
iconImageId: iconImageId || null,
})
await updateChannelTime(channel.id, {
number: number.trim() === '' ? null : Number(number),
@@ -119,6 +125,35 @@ export function SettingsCard({
</SelectContent>
</Select>
</div>
<div className="flex flex-col gap-1.5">
<Label>{t('admin.channels.iptvIcon')}</Label>
<div className="flex items-center gap-3">
<div className="flex h-12 w-12 shrink-0 items-center justify-center rounded-md border border-border bg-muted/30">
{iconImageId ? (
<img src={imageUrl(iconImageId)} alt="" className="h-full w-full object-contain" />
) : (
<span className="text-center text-[10px] leading-tight text-muted-foreground">
{t('admin.channels.noIptvIcon')}
</span>
)}
</div>
<Button size="sm" variant="outline" onClick={() => setIconGalleryOpen(true)}>
{t('admin.channels.pickIptvIcon')}
</Button>
{iconImageId && (
<Button size="sm" variant="ghost" onClick={() => setIconImageId('')}>
{t('common.delete')}
</Button>
)}
</div>
<p className="text-xs text-muted-foreground">{t('admin.channels.iptvIconHint')}</p>
<ImageGallery
open={iconGalleryOpen}
onOpenChange={setIconGalleryOpen}
category="Library"
onSelect={(image) => setIconImageId(image.id)}
/>
</div>
<label className="flex items-center gap-2 text-sm">
<input
type="checkbox"
+2
View File
@@ -651,6 +651,8 @@ export type ChannelDto = {
dayStartTime: string
templateId: string | null
fillerAssetId: string | null
/** Иконка канала для внешних IPTV-плееров (M3U/XMLTV); null — отдаётся фавикон-заглушка. */
iconImageId: string | null
viewer: ViewerSettings
}
+5
View File
@@ -785,6 +785,11 @@ export const en = {
settings: 'Settings',
filler: 'Filler',
noFiller: 'No filler',
iptvIcon: 'IPTV icon',
noIptvIcon: 'none',
pickIptvIcon: 'Pick an icon',
iptvIconHint:
'Channel logo in M3U/EPG for external players. If unset, the site favicon is served.',
noSchedule: 'Schedule not built yet',
airToday: 'Today',
airCount: 'Entries for the day: {{count}}',
+5
View File
@@ -780,6 +780,11 @@ export const ru = {
settings: 'Настройки',
filler: 'Заглушка',
noFiller: 'Без заглушки',
iptvIcon: 'Иконка для IPTV',
noIptvIcon: 'нет',
pickIptvIcon: 'Выбрать иконку',
iptvIconHint:
'Логотип канала в M3U/EPG для внешних плееров. Если не задан — отдаётся фавикон сайта.',
noSchedule: 'Расписание ещё не построено',
airToday: 'Сегодня',
airCount: 'Записей за сутки: {{count}}',