Enhance channel and settings functionalities: introduce viewer settings in ChannelEndpoints, update SiteSettings to include channel number toggling, and refactor related data structures. Implement new endpoints for validating templates and diffing scheduling changes, improving overall user experience and configuration management.
This commit is contained in:
@@ -18,7 +18,24 @@ export type AuthResponse = {
|
||||
|
||||
export type RegistrationStatus = { enabled: boolean }
|
||||
|
||||
export type SiteSettings = { registrationEnabled: boolean; preferredAudioLanguages: string }
|
||||
export type SiteSettings = {
|
||||
registrationEnabled: boolean
|
||||
preferredAudioLanguages: string
|
||||
/** Переключение каналов по номерам у зрителя; сетка каналов остаётся всегда. */
|
||||
channelNumbersEnabled: boolean
|
||||
}
|
||||
|
||||
/** Угол экрана для логотипа-оверлея. */
|
||||
export type LogoCorner = 'TopLeft' | 'TopRight' | 'BottomLeft' | 'BottomRight'
|
||||
|
||||
/** Как канал выглядит у зрителя — всё опционально и по умолчанию выключено (см. 6.8). */
|
||||
export type ViewerSettings = {
|
||||
logoImageId: string | null
|
||||
logoCorner: LogoCorner
|
||||
logoOpacity: number
|
||||
showClock: boolean
|
||||
analogFilterStrength: number
|
||||
}
|
||||
|
||||
export type RoleDto = {
|
||||
id: string
|
||||
@@ -360,6 +377,7 @@ export type ChannelDto = {
|
||||
bumper: BumperSettings
|
||||
bumperTemplates: BumperTemplateDto[]
|
||||
fillerAssetId: string | null
|
||||
viewer: ViewerSettings
|
||||
}
|
||||
|
||||
// ── Сетка канала (шаблон → слои → слоты) ──────────────────────────────────
|
||||
@@ -466,6 +484,10 @@ export type AudienceWindow = { from: string; to: string; maxAudience: ShowAudien
|
||||
export type PlanningRules = {
|
||||
maxAudienceByTime?: AudienceWindow[] | null
|
||||
maxRepeatsInWindow?: { windowDays: number; max: number } | null
|
||||
/** Пост-проверки: считаются по готовой ленте и только предупреждают. */
|
||||
maxBreakMinutesPerHour?: number | null
|
||||
maxGenreSharePercent?: number | null
|
||||
maxFallbackSharePercent?: number | null
|
||||
}
|
||||
|
||||
export type ScheduleTemplateDto = {
|
||||
@@ -493,6 +515,9 @@ export type PlanningWarningKind =
|
||||
| 'RepeatSourceEmpty'
|
||||
| 'FallbackEmpty'
|
||||
| 'CandidatesFiltered'
|
||||
| 'BreakLimitExceeded'
|
||||
| 'GenreShareExceeded'
|
||||
| 'FallbackShareExceeded'
|
||||
|
||||
export type PlanningWarningDto = {
|
||||
kind: PlanningWarningKind
|
||||
@@ -502,6 +527,72 @@ export type PlanningWarningDto = {
|
||||
|
||||
export type ApplyResultDto = { added: number; warnings: PlanningWarningDto[] }
|
||||
|
||||
/** Что изменится в эфире, если применить правила сейчас (см. 6.6). */
|
||||
export type ScheduleChangeDto = {
|
||||
startsAtUtc: string
|
||||
before: string | null
|
||||
after: string | null
|
||||
/** Попадает в ближайшие сутки — подсвечивается отдельно. */
|
||||
soon: boolean
|
||||
}
|
||||
|
||||
export type ScheduleDiffDto = {
|
||||
total: number
|
||||
changed: number
|
||||
changedSoon: number
|
||||
changes: ScheduleChangeDto[]
|
||||
}
|
||||
|
||||
/** Цепочка происхождения записи — «почему это здесь» (см. 6.5). */
|
||||
export type EntryTraceDto = {
|
||||
entryId: string
|
||||
startsAtUtc: string
|
||||
endsAtUtc: string
|
||||
showName: string | null
|
||||
episodeIndex: number | null
|
||||
layerName: string | null
|
||||
layerPriority: number | null
|
||||
slotTitle: string | null
|
||||
slotKind: SlotKind | null
|
||||
slotWeekday: number | null
|
||||
slotTargetStart: string | null
|
||||
slotDurationMinutes: number | null
|
||||
groupName: string | null
|
||||
groupItemCount: number | null
|
||||
strategy: SlotStrategyType | null
|
||||
cooldownDays: number | null
|
||||
candidatesAfterCooldown: number | null
|
||||
driftMinutes: number
|
||||
snapped: boolean
|
||||
junctionName: string | null
|
||||
}
|
||||
|
||||
export type CopyTemplateResultDto = {
|
||||
layers: number
|
||||
slots: number
|
||||
junctions: number
|
||||
/** Врезки-заставки, для которых на канале-приёмнике не нашлось блока с таким же именем. */
|
||||
droppedBumperRefs: number
|
||||
}
|
||||
|
||||
/** Проверки сетки по правилам, до генерации (см. 5.1). */
|
||||
export type TemplateIssueKind =
|
||||
| 'GroupEmpty'
|
||||
| 'GroupTooSmall'
|
||||
| 'GridGap'
|
||||
| 'SlotOverlap'
|
||||
| 'CooldownUnreachable'
|
||||
| 'AudienceConflict'
|
||||
| 'GroupMissing'
|
||||
|
||||
export type TemplateIssueDto = {
|
||||
kind: TemplateIssueKind
|
||||
severity: 'Warning' | 'Error'
|
||||
layerId: string | null
|
||||
slotId: string | null
|
||||
details: string
|
||||
}
|
||||
|
||||
/** Что попало в ленту предпросмотра. `Bumper` приходит без ассета — он рендерится при применении. */
|
||||
export type PlannedItemKind = 'Program' | 'Fallback' | 'SignOff' | 'Ad' | 'Promo' | 'Bumper'
|
||||
|
||||
@@ -543,9 +634,16 @@ export type PublicChannelDto = {
|
||||
id: string
|
||||
slug: string
|
||||
name: string
|
||||
/** Номер канала для переключения по номерам или null. */
|
||||
number: number | null
|
||||
currentShowId: string | null
|
||||
currentShowName: string | null
|
||||
currentShowPosterImageId: string | null
|
||||
logoImageId: string | null
|
||||
logoCorner: LogoCorner
|
||||
logoOpacity: number
|
||||
showClock: boolean
|
||||
analogFilterStrength: number
|
||||
}
|
||||
|
||||
export type PublicEpgEntryDto = {
|
||||
|
||||
@@ -66,6 +66,7 @@ const resources = {
|
||||
noChannels: 'Пока нет доступных каналов. Загляните позже.',
|
||||
offline: 'Канал сейчас не в эфире',
|
||||
offlineHint: 'Нет расписания или контента. Загляните позже.',
|
||||
numbersHint: '↑ / ↓ — переключение каналов по номерам',
|
||||
retry: 'Повторить',
|
||||
},
|
||||
settings: {
|
||||
@@ -378,7 +379,65 @@ const resources = {
|
||||
previewDays_one: '{{count}} сутки',
|
||||
previewDays_few: '{{count}} суток',
|
||||
previewDays_many: '{{count}} суток',
|
||||
previewTabs: { programme: 'Программа', tape: 'Лента' },
|
||||
previewTabs: { programme: 'Программа', tape: 'Лента', problems: 'Проблемы' },
|
||||
noProblems: 'Проблем нет',
|
||||
andMore: 'и ещё {{count}}',
|
||||
heatmap: 'Повторы: шоу × сутки',
|
||||
heatmapTotal: 'всего',
|
||||
issues: 'Проверки: ошибок {{errors}}, предупреждений {{warnings}}',
|
||||
goToSlot: 'к слоту',
|
||||
issueKinds: {
|
||||
GroupEmpty: 'Пустая группа',
|
||||
GroupTooSmall: 'Мало контента',
|
||||
GridGap: 'Дыра в сетке',
|
||||
SlotOverlap: 'Слоты пересекаются',
|
||||
CooldownUnreachable: 'Недостижимое остывание',
|
||||
AudienceConflict: 'Возрастной конфликт',
|
||||
GroupMissing: 'Группа не выбрана',
|
||||
},
|
||||
viewer: 'Как выглядит у зрителя',
|
||||
viewerHint:
|
||||
'Оверлеи рисуются поверх картинки на клиенте — видео не перекодируется. Всё по умолчанию выключено.',
|
||||
logo: 'Логотип',
|
||||
noLogo: 'нет',
|
||||
pickLogo: 'Выбрать логотип',
|
||||
logoCorner: 'Угол',
|
||||
logoOpacity: 'Прозрачность',
|
||||
corners: {
|
||||
TopLeft: 'Слева вверху',
|
||||
TopRight: 'Справа вверху',
|
||||
BottomLeft: 'Слева внизу',
|
||||
BottomRight: 'Справа внизу',
|
||||
},
|
||||
showClock: 'Показывать часы',
|
||||
analogFilter: 'Аналоговый фильтр',
|
||||
analogFilterHint: 'Сила 0..1. Ноль — выключен; переборщить очень легко.',
|
||||
whyHere: 'Почему это здесь',
|
||||
priority: 'приоритет',
|
||||
traceLayer: 'Слой',
|
||||
traceSlot: 'Слот',
|
||||
traceGroup: 'Группа',
|
||||
traceStrategy: 'Стратегия',
|
||||
traceJunction: 'Врезки',
|
||||
traceDrift: 'дрейф {{minutes}} мин',
|
||||
traceSnapped: 'старт округлён',
|
||||
traceCooldown: 'остывание {{days}} дн.',
|
||||
traceCandidates: 'кандидатов после остывания: {{count}}',
|
||||
diffSummary: 'Затронет {{total}} записей, изменятся {{changed}}',
|
||||
diffSoon: 'В ближайшие сутки изменится записей: {{count}}',
|
||||
diffNoChanges: 'Эфир не изменится',
|
||||
copyTemplate: 'Копировать сетку',
|
||||
pickTargetChannel: 'Выберите канал',
|
||||
copyTemplateHint:
|
||||
'Слои, слоты, стыки и правила уедут на выбранный канал, его прежняя сетка заменится. Группы общие и не копируются.',
|
||||
templateCopied: 'Скопировано: слоёв {{layers}}, слотов {{slots}}',
|
||||
copyDroppedBumpers: 'Врезок без блока заставки: {{count}} — донастройте руками',
|
||||
postChecks: 'Пост-проверки',
|
||||
breakLimit: 'Потолок врезок в час, мин',
|
||||
genreShare: 'Потолок доли жанра за сутки, %',
|
||||
fallbackShare: 'Потолок доли фона, %',
|
||||
postChecksHint:
|
||||
'Пост-проверки считаются по готовой ленте и только предупреждают — ничего не переигрывается.',
|
||||
previewKinds: {
|
||||
Program: 'Программа',
|
||||
Fallback: 'Фон',
|
||||
@@ -454,6 +513,9 @@ const resources = {
|
||||
RepeatSourceEmpty: 'Нечего повторять',
|
||||
FallbackEmpty: 'Нечем закрыть паузы',
|
||||
CandidatesFiltered: 'Возрастной потолок отсёк всех',
|
||||
BreakLimitExceeded: 'Врезок в часе больше потолка',
|
||||
GenreShareExceeded: 'Доля жанра выше нормы',
|
||||
FallbackShareExceeded: 'Фона в эфире больше нормы',
|
||||
},
|
||||
|
||||
title: 'Каналы',
|
||||
@@ -565,6 +627,9 @@ const resources = {
|
||||
'Когда выключено — новые пользователи не могут регистрироваться сами, учётки заводит только администратор.',
|
||||
registrationLabel: 'Разрешить регистрацию на сайте',
|
||||
preferredAudio: 'Предпочитаемые озвучки',
|
||||
channelNumbers: 'Переключение каналов по номерам',
|
||||
channelNumbersHint:
|
||||
'Зритель переключает каналы стрелками, как на телевизоре. Сетка каналов остаётся всегда.',
|
||||
preferredAudioHint:
|
||||
'Коды языков через запятую в порядке приоритета (напр. «rus, eng»). Если в файле есть дорожка с таким языком — при обработке выбирается она (по порядку); иначе — выбор ffmpeg по умолчанию. Применяется к новым обработкам.',
|
||||
},
|
||||
@@ -974,7 +1039,65 @@ const resources = {
|
||||
previewHint: 'A run against the current rules: nothing is written, slot cursors do not move.',
|
||||
previewDays_one: '{{count}} day',
|
||||
previewDays_other: '{{count}} days',
|
||||
previewTabs: { programme: 'Programme', tape: 'Tape' },
|
||||
previewTabs: { programme: 'Programme', tape: 'Tape', problems: 'Problems' },
|
||||
noProblems: 'No problems',
|
||||
andMore: 'and {{count}} more',
|
||||
heatmap: 'Repeats: show × day',
|
||||
heatmapTotal: 'total',
|
||||
issues: 'Checks: {{errors}} errors, {{warnings}} warnings',
|
||||
goToSlot: 'to slot',
|
||||
issueKinds: {
|
||||
GroupEmpty: 'Empty group',
|
||||
GroupTooSmall: 'Too little content',
|
||||
GridGap: 'Gap in the grid',
|
||||
SlotOverlap: 'Slots overlap',
|
||||
CooldownUnreachable: 'Unreachable cooldown',
|
||||
AudienceConflict: 'Age conflict',
|
||||
GroupMissing: 'No group selected',
|
||||
},
|
||||
viewer: 'How viewers see it',
|
||||
viewerHint:
|
||||
'Overlays are drawn on the client on top of the picture — the video is not re-encoded. Everything is off by default.',
|
||||
logo: 'Logo',
|
||||
noLogo: 'none',
|
||||
pickLogo: 'Pick a logo',
|
||||
logoCorner: 'Corner',
|
||||
logoOpacity: 'Opacity',
|
||||
corners: {
|
||||
TopLeft: 'Top left',
|
||||
TopRight: 'Top right',
|
||||
BottomLeft: 'Bottom left',
|
||||
BottomRight: 'Bottom right',
|
||||
},
|
||||
showClock: 'Show a clock',
|
||||
analogFilter: 'Analog filter',
|
||||
analogFilterHint: 'Strength 0..1. Zero is off; it is very easy to overdo.',
|
||||
whyHere: 'Why is this here',
|
||||
priority: 'priority',
|
||||
traceLayer: 'Layer',
|
||||
traceSlot: 'Slot',
|
||||
traceGroup: 'Group',
|
||||
traceStrategy: 'Strategy',
|
||||
traceJunction: 'Breaks',
|
||||
traceDrift: 'drift {{minutes}} min',
|
||||
traceSnapped: 'start snapped',
|
||||
traceCooldown: 'cooldown {{days}} d.',
|
||||
traceCandidates: 'candidates after cooldown: {{count}}',
|
||||
diffSummary: 'Affects {{total}} entries, {{changed}} will change',
|
||||
diffSoon: 'Entries changing within 24 hours: {{count}}',
|
||||
diffNoChanges: 'The air will not change',
|
||||
copyTemplate: 'Copy grid',
|
||||
pickTargetChannel: 'Pick a channel',
|
||||
copyTemplateHint:
|
||||
'Layers, slots, junctions and rules move to the chosen channel, replacing its grid. Groups are shared and not copied.',
|
||||
templateCopied: 'Copied: {{layers}} layers, {{slots}} slots',
|
||||
copyDroppedBumpers: 'Breaks left without a bumper block: {{count}} — set them up by hand',
|
||||
postChecks: 'Post-checks',
|
||||
breakLimit: 'Breaks per hour cap, min',
|
||||
genreShare: 'Genre share per day cap, %',
|
||||
fallbackShare: 'Background share cap, %',
|
||||
postChecksHint:
|
||||
'Post-checks run against the finished tape and only warn — nothing is replanned.',
|
||||
previewKinds: {
|
||||
Program: 'Programme',
|
||||
Fallback: 'Background',
|
||||
@@ -1045,6 +1168,9 @@ const resources = {
|
||||
RepeatSourceEmpty: 'Nothing to repeat',
|
||||
FallbackEmpty: 'Nothing to fill pauses with',
|
||||
CandidatesFiltered: 'The age cap ruled out every candidate',
|
||||
BreakLimitExceeded: 'Breaks in an hour exceed the cap',
|
||||
GenreShareExceeded: 'Genre share above the norm',
|
||||
FallbackShareExceeded: 'Background share above the norm',
|
||||
},
|
||||
|
||||
title: 'Channels',
|
||||
@@ -1155,6 +1281,9 @@ const resources = {
|
||||
registrationHint:
|
||||
'When off, new users cannot sign up themselves — only an administrator can create accounts.',
|
||||
registrationLabel: 'Allow public registration',
|
||||
channelNumbers: 'Switch channels by number',
|
||||
channelNumbersHint:
|
||||
'Viewers switch channels with the arrow keys, like on a TV set. The channel grid stays available regardless.',
|
||||
preferredAudio: 'Preferred audio tracks',
|
||||
preferredAudioHint:
|
||||
'Comma-separated language codes in priority order (e.g. "rus, eng"). If a file has a track in one of these languages, it is picked during processing (by order); otherwise ffmpeg default. Applies to new processing.',
|
||||
|
||||
Reference in New Issue
Block a user