Add Prettier to the frontend and gate formatting in CI
Форматтера у фронтенда не было: стиль держался вручную и успел разъехаться в 50 файлах. Ставим Prettier с настройками под уже сложившийся стиль (без точек с запятой, одинарные кавычки, ширина 100 — подобрана замером: при 100 расходится меньше файлов, чем при 96 или 110) и прогоняем его по коду. `src/routeTree.gen.ts` исключён — его переписывает плагин роутера. Чтобы форматирование больше не расходилось незаметно, добавлены проверки в CI: `csharpier check` для бэкенда (его отсутствие и позволило накопиться 79 неотформатированным файлам) и `prettier --check` для фронтенда. Версии форматтеров прибиты точно, без кареток: минорка меняет вывод и красит CI на файлах, которых никто не трогал. `.editorconfig` задаёт редакторам те же отступы и LF ещё до форматтера; значения совпадают с настройками csharpier и Prettier намеренно — оба его читают. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
0442056367
commit
0606ea3e6e
@@ -1,371 +1,375 @@
|
||||
import { apiRequest, getAccessToken, HttpError } from '@/shared/api/client'
|
||||
import type {
|
||||
ApplyResultDto,
|
||||
BumperSettings,
|
||||
BumperTextKind,
|
||||
BumperTrigger,
|
||||
ChannelDto,
|
||||
ChannelSummaryDto,
|
||||
CopyTemplateResultDto,
|
||||
CreatedIdResponse,
|
||||
EntryTraceDto,
|
||||
JunctionAmountMode,
|
||||
JunctionConditions,
|
||||
JunctionElementKind,
|
||||
JunctionTemplateDto,
|
||||
LayerApplicability,
|
||||
PlanningRules,
|
||||
ScheduleDiffDto,
|
||||
ScheduleEntryDto,
|
||||
SchedulePreviewDto,
|
||||
ScheduleTemplateDto,
|
||||
SlotDto,
|
||||
TemplateIssueDto,
|
||||
ViewerSettings,
|
||||
} from '@/shared/api/types'
|
||||
|
||||
export function listChannels() {
|
||||
return apiRequest<ChannelSummaryDto[]>('/admin/channels')
|
||||
}
|
||||
|
||||
export function getChannel(id: string) {
|
||||
return apiRequest<ChannelDto>(`/admin/channels/${id}`)
|
||||
}
|
||||
|
||||
export function createChannel(body: { name: string; slug: string }) {
|
||||
return apiRequest<CreatedIdResponse>('/admin/channels', { method: 'POST', body })
|
||||
}
|
||||
|
||||
type ChannelSettingsBody = {
|
||||
name: string
|
||||
isEnabled: boolean
|
||||
bumpersEnabled: boolean
|
||||
bumper: BumperSettings
|
||||
fillerAssetId: string | null
|
||||
}
|
||||
|
||||
export function updateChannelSettings(id: string, body: ChannelSettingsBody) {
|
||||
return apiRequest<void>(`/admin/channels/${id}/settings`, { method: 'PUT', body })
|
||||
}
|
||||
|
||||
/** Оверлеи и аналоговый фильтр канала — как он выглядит у зрителя. */
|
||||
export function updateViewerSettings(id: string, body: ViewerSettings) {
|
||||
return apiRequest<void>(`/admin/channels/${id}/viewer`, { method: 'PUT', body })
|
||||
}
|
||||
|
||||
/** Номер канала и его время: смещение от UTC и начало вещательных суток. */
|
||||
export function updateChannelTime(
|
||||
id: string,
|
||||
body: { number: number | null; utcOffsetMinutes: number; dayStartTime: string },
|
||||
) {
|
||||
return apiRequest<void>(`/admin/channels/${id}/time`, { method: 'PUT', body })
|
||||
}
|
||||
|
||||
// ── Сетка канала ──────────────────────────────────────────────────────────
|
||||
|
||||
export function getChannelTemplate(channelId: string) {
|
||||
return apiRequest<ScheduleTemplateDto>(`/admin/channels/${channelId}/template`)
|
||||
}
|
||||
|
||||
/** Заводит каналу сетку, если её нет: у каналов из старой ротации шаблона может не быть. */
|
||||
export function createChannelTemplate(channelId: string) {
|
||||
return apiRequest<CreatedIdResponse>(`/admin/channels/${channelId}/template`, { method: 'POST' })
|
||||
}
|
||||
|
||||
/** Применяет правила к эфиру: пересобирает будущий хвост. Правка слотов эфир не двигает. */
|
||||
export function applyChannelTemplate(channelId: string) {
|
||||
return apiRequest<ApplyResultDto>(`/admin/channels/${channelId}/template/apply`, {
|
||||
method: 'POST',
|
||||
})
|
||||
}
|
||||
|
||||
/** Проверки сетки по правилам — считаются по шаблону, без прогона генератора. */
|
||||
export function getTemplateIssues(channelId: string) {
|
||||
return apiRequest<TemplateIssueDto[]>(`/admin/channels/${channelId}/template/issues`)
|
||||
}
|
||||
|
||||
/** Что изменится в эфире, если применить сейчас. Прогон сухой — лента не трогается. */
|
||||
export function getApplyDiff(channelId: string) {
|
||||
return apiRequest<ScheduleDiffDto>(`/admin/channels/${channelId}/template/diff`)
|
||||
}
|
||||
|
||||
/** Копия сетки на другой канал: слои, слоты, стыки и правила. Группы общие и не копируются. */
|
||||
export function copyTemplateTo(channelId: string, targetChannelId: string) {
|
||||
return apiRequest<CopyTemplateResultDto>(
|
||||
`/admin/channels/${channelId}/template/copy-to/${targetChannelId}`,
|
||||
{ method: 'POST' },
|
||||
)
|
||||
}
|
||||
|
||||
/** Цепочка происхождения записи, записанная в момент генерации. */
|
||||
export function getEntryTrace(entryId: string) {
|
||||
return apiRequest<EntryTraceDto>(`/admin/channels/entries/${entryId}/trace`)
|
||||
}
|
||||
|
||||
/** Сухой прогон по текущим правилам: ничего не пишет и не двигает курсоры слотов. */
|
||||
export function previewTemplate(channelId: string, days: number) {
|
||||
const query = new URLSearchParams({ days: String(days) })
|
||||
return apiRequest<SchedulePreviewDto>(
|
||||
`/admin/channels/${channelId}/template/preview?${query.toString()}`,
|
||||
)
|
||||
}
|
||||
|
||||
export function updateTemplate(
|
||||
templateId: string,
|
||||
body: {
|
||||
name: string
|
||||
fallbackGroupId: string | null
|
||||
defaultJunctionId: string | null
|
||||
rules: PlanningRules | null
|
||||
},
|
||||
) {
|
||||
return apiRequest<void>(`/admin/templates/${templateId}`, { method: 'PUT', body })
|
||||
}
|
||||
|
||||
export function createLayer(templateId: string, body: { name: string; priority: number }) {
|
||||
return apiRequest<CreatedIdResponse>(`/admin/templates/${templateId}/layers`, {
|
||||
method: 'POST',
|
||||
body,
|
||||
})
|
||||
}
|
||||
|
||||
export function updateLayer(
|
||||
layerId: string,
|
||||
body: {
|
||||
name: string
|
||||
priority: number
|
||||
applicability: LayerApplicability | null
|
||||
isEnabled: boolean
|
||||
},
|
||||
) {
|
||||
return apiRequest<void>(`/admin/layers/${layerId}`, { method: 'PUT', body })
|
||||
}
|
||||
|
||||
export function deleteLayer(layerId: string) {
|
||||
return apiRequest<void>(`/admin/layers/${layerId}`, { method: 'DELETE' })
|
||||
}
|
||||
|
||||
/** Тело слота: то же для создания и правки (см. SlotInput на сервере). */
|
||||
export type SlotBody = Omit<SlotDto, 'id' | 'layerId' | 'groupName'>
|
||||
|
||||
/** Слот из ответа сервера → тело запроса: отбрасываем то, что сервер проставляет сам. */
|
||||
export function toSlotBody(slot: SlotDto): SlotBody {
|
||||
const { id: _id, layerId: _layerId, groupName: _groupName, ...body } = slot
|
||||
return body
|
||||
}
|
||||
|
||||
export function createSlot(layerId: string, body: SlotBody) {
|
||||
return apiRequest<CreatedIdResponse>(`/admin/layers/${layerId}/slots`, { method: 'POST', body })
|
||||
}
|
||||
|
||||
export function updateSlot(slotId: string, body: SlotBody) {
|
||||
return apiRequest<void>(`/admin/slots/${slotId}`, { method: 'PUT', body })
|
||||
}
|
||||
|
||||
export function deleteSlot(slotId: string) {
|
||||
return apiRequest<void>(`/admin/slots/${slotId}`, { method: 'DELETE' })
|
||||
}
|
||||
|
||||
// ── Стыки канала ──────────────────────────────────────────────────────────
|
||||
|
||||
export function listJunctions(channelId: string) {
|
||||
return apiRequest<JunctionTemplateDto[]>(`/admin/channels/${channelId}/junctions`)
|
||||
}
|
||||
|
||||
export function createJunction(channelId: string, name: string) {
|
||||
return apiRequest<CreatedIdResponse>(`/admin/channels/${channelId}/junctions`, {
|
||||
method: 'POST',
|
||||
body: { name },
|
||||
})
|
||||
}
|
||||
|
||||
export function renameJunction(junctionId: string, name: string) {
|
||||
return apiRequest<void>(`/admin/junctions/${junctionId}`, { method: 'PUT', body: { name } })
|
||||
}
|
||||
|
||||
export function deleteJunction(junctionId: string) {
|
||||
return apiRequest<void>(`/admin/junctions/${junctionId}`, { method: 'DELETE' })
|
||||
}
|
||||
|
||||
export function addJunctionElement(junctionId: string, kind: JunctionElementKind) {
|
||||
return apiRequest<CreatedIdResponse>(`/admin/junctions/${junctionId}/elements`, {
|
||||
method: 'POST',
|
||||
body: { kind },
|
||||
})
|
||||
}
|
||||
|
||||
/** Тело врезки: то же для любого типа — лишние поля сервер обнуляет сам (см. JunctionElement.Update). */
|
||||
export type JunctionElementBody = {
|
||||
kind: JunctionElementKind
|
||||
groupId: string | null
|
||||
bumperTemplateId: string | null
|
||||
amountMode: JunctionAmountMode
|
||||
amountValue: number
|
||||
isRequired: boolean
|
||||
conditions: JunctionConditions | null
|
||||
}
|
||||
|
||||
export function updateJunctionElement(
|
||||
junctionId: string,
|
||||
elementId: string,
|
||||
body: JunctionElementBody,
|
||||
) {
|
||||
return apiRequest<void>(`/admin/junctions/${junctionId}/elements/${elementId}`, {
|
||||
method: 'PUT',
|
||||
body,
|
||||
})
|
||||
}
|
||||
|
||||
export function removeJunctionElement(junctionId: string, elementId: string) {
|
||||
return apiRequest<void>(`/admin/junctions/${junctionId}/elements/${elementId}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
}
|
||||
|
||||
/** Порядок врезок: не упомянутые остаются после перечисленных. */
|
||||
export function reorderJunction(junctionId: string, elementIdsInOrder: string[]) {
|
||||
return apiRequest<void>(`/admin/junctions/${junctionId}/order`, {
|
||||
method: 'PUT',
|
||||
body: { elementIdsInOrder },
|
||||
})
|
||||
}
|
||||
|
||||
type BumperTemplateStyleBody = {
|
||||
name: string
|
||||
backgroundColor: string
|
||||
backgroundColor2: string
|
||||
accentColor: string
|
||||
textColor: string
|
||||
}
|
||||
|
||||
export function addBumperTemplate(id: string, name: string) {
|
||||
return apiRequest<CreatedIdResponse>(`/admin/channels/${id}/bumper/templates`, {
|
||||
method: 'POST',
|
||||
body: { name },
|
||||
})
|
||||
}
|
||||
|
||||
export function updateBumperTemplate(id: string, templateId: string, body: BumperTemplateStyleBody) {
|
||||
return apiRequest<void>(`/admin/channels/${id}/bumper/templates/${templateId}`, {
|
||||
method: 'PUT',
|
||||
body,
|
||||
})
|
||||
}
|
||||
|
||||
export function removeBumperTemplate(id: string, templateId: string) {
|
||||
return apiRequest<void>(`/admin/channels/${id}/bumper/templates/${templateId}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
}
|
||||
|
||||
/** Загрузка сырого файла блока (звук/фон): тело — файл, имя — в query (как в uploadMedia). */
|
||||
function uploadBumperTemplateFile(
|
||||
id: string,
|
||||
templateId: string,
|
||||
kind: 'audio' | 'background',
|
||||
file: File,
|
||||
): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest()
|
||||
const query = new URLSearchParams({ fileName: file.name })
|
||||
xhr.open(
|
||||
'PUT',
|
||||
`/api/admin/channels/${id}/bumper/templates/${templateId}/${kind}?${query.toString()}`,
|
||||
)
|
||||
const token = getAccessToken()
|
||||
if (token) xhr.setRequestHeader('Authorization', `Bearer ${token}`)
|
||||
xhr.onload = () => {
|
||||
if (xhr.status >= 200 && xhr.status < 300) {
|
||||
resolve()
|
||||
} else {
|
||||
let detail = `HTTP ${xhr.status}`
|
||||
try {
|
||||
const problem = JSON.parse(xhr.responseText) as { detail?: string; title?: string }
|
||||
detail = problem.detail ?? problem.title ?? detail
|
||||
} catch {
|
||||
/* пусто */
|
||||
}
|
||||
reject(new HttpError({ detail }, xhr.status))
|
||||
}
|
||||
}
|
||||
xhr.onerror = () => reject(new HttpError({ title: 'Network error' }, 0))
|
||||
xhr.send(file)
|
||||
})
|
||||
}
|
||||
|
||||
export function uploadBumperTemplateAudio(id: string, templateId: string, file: File) {
|
||||
return uploadBumperTemplateFile(id, templateId, 'audio', file)
|
||||
}
|
||||
|
||||
type BumperVariantBody = {
|
||||
name: string
|
||||
kind: BumperTextKind
|
||||
nowLabel: string
|
||||
nextLabel: string
|
||||
line1: string
|
||||
line2: string
|
||||
trigger: BumperTrigger
|
||||
weight: number
|
||||
}
|
||||
|
||||
export function addBumperVariant(id: string, templateId: string, name: string) {
|
||||
return apiRequest<CreatedIdResponse>(
|
||||
`/admin/channels/${id}/bumper/templates/${templateId}/variants`,
|
||||
{ method: 'POST', body: { name } },
|
||||
)
|
||||
}
|
||||
|
||||
export function updateBumperVariant(
|
||||
id: string,
|
||||
templateId: string,
|
||||
variantId: string,
|
||||
body: BumperVariantBody,
|
||||
) {
|
||||
return apiRequest<void>(
|
||||
`/admin/channels/${id}/bumper/templates/${templateId}/variants/${variantId}`,
|
||||
{ method: 'PUT', body },
|
||||
)
|
||||
}
|
||||
|
||||
export function removeBumperVariant(id: string, templateId: string, variantId: string) {
|
||||
return apiRequest<void>(
|
||||
`/admin/channels/${id}/bumper/templates/${templateId}/variants/${variantId}`,
|
||||
{ method: 'DELETE' },
|
||||
)
|
||||
}
|
||||
|
||||
/** Привязать фон-картинку блока по ссылке на изображение из реестра (галерея). */
|
||||
export function setBumperTemplateBackground(id: string, templateId: string, imageId: string) {
|
||||
return apiRequest<void>(`/admin/channels/${id}/bumper/templates/${templateId}/background`, {
|
||||
method: 'PUT',
|
||||
body: { imageId },
|
||||
})
|
||||
}
|
||||
|
||||
export function clearBumperTemplateAudio(id: string, templateId: string) {
|
||||
return apiRequest<void>(`/admin/channels/${id}/bumper/templates/${templateId}/audio`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
}
|
||||
|
||||
export function clearBumperTemplateBackground(id: string, templateId: string) {
|
||||
return apiRequest<void>(`/admin/channels/${id}/bumper/templates/${templateId}/background`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
}
|
||||
|
||||
/** Синхронно рендерит примеры всех подблоков блока (сервер собирает ffmpeg-клипы). */
|
||||
export function renderBumperPreviews(id: string, templateId: string) {
|
||||
return apiRequest<void>(`/admin/channels/${id}/bumper/templates/${templateId}/preview`, {
|
||||
method: 'POST',
|
||||
})
|
||||
}
|
||||
|
||||
export function bumperPreviewPlaylistUrl(id: string, templateId: string, variantId: string) {
|
||||
return `/api/admin/channels/${id}/bumper/templates/${templateId}/preview/${variantId}/index.m3u8`
|
||||
}
|
||||
|
||||
export function getSchedule(id: string, from: Date, to: Date) {
|
||||
const query = new URLSearchParams({ from: from.toISOString(), to: to.toISOString() })
|
||||
return apiRequest<ScheduleEntryDto[]>(`/admin/channels/${id}/schedule?${query.toString()}`)
|
||||
}
|
||||
import { apiRequest, getAccessToken, HttpError } from '@/shared/api/client'
|
||||
import type {
|
||||
ApplyResultDto,
|
||||
BumperSettings,
|
||||
BumperTextKind,
|
||||
BumperTrigger,
|
||||
ChannelDto,
|
||||
ChannelSummaryDto,
|
||||
CopyTemplateResultDto,
|
||||
CreatedIdResponse,
|
||||
EntryTraceDto,
|
||||
JunctionAmountMode,
|
||||
JunctionConditions,
|
||||
JunctionElementKind,
|
||||
JunctionTemplateDto,
|
||||
LayerApplicability,
|
||||
PlanningRules,
|
||||
ScheduleDiffDto,
|
||||
ScheduleEntryDto,
|
||||
SchedulePreviewDto,
|
||||
ScheduleTemplateDto,
|
||||
SlotDto,
|
||||
TemplateIssueDto,
|
||||
ViewerSettings,
|
||||
} from '@/shared/api/types'
|
||||
|
||||
export function listChannels() {
|
||||
return apiRequest<ChannelSummaryDto[]>('/admin/channels')
|
||||
}
|
||||
|
||||
export function getChannel(id: string) {
|
||||
return apiRequest<ChannelDto>(`/admin/channels/${id}`)
|
||||
}
|
||||
|
||||
export function createChannel(body: { name: string; slug: string }) {
|
||||
return apiRequest<CreatedIdResponse>('/admin/channels', { method: 'POST', body })
|
||||
}
|
||||
|
||||
type ChannelSettingsBody = {
|
||||
name: string
|
||||
isEnabled: boolean
|
||||
bumpersEnabled: boolean
|
||||
bumper: BumperSettings
|
||||
fillerAssetId: string | null
|
||||
}
|
||||
|
||||
export function updateChannelSettings(id: string, body: ChannelSettingsBody) {
|
||||
return apiRequest<void>(`/admin/channels/${id}/settings`, { method: 'PUT', body })
|
||||
}
|
||||
|
||||
/** Оверлеи и аналоговый фильтр канала — как он выглядит у зрителя. */
|
||||
export function updateViewerSettings(id: string, body: ViewerSettings) {
|
||||
return apiRequest<void>(`/admin/channels/${id}/viewer`, { method: 'PUT', body })
|
||||
}
|
||||
|
||||
/** Номер канала и его время: смещение от UTC и начало вещательных суток. */
|
||||
export function updateChannelTime(
|
||||
id: string,
|
||||
body: { number: number | null; utcOffsetMinutes: number; dayStartTime: string },
|
||||
) {
|
||||
return apiRequest<void>(`/admin/channels/${id}/time`, { method: 'PUT', body })
|
||||
}
|
||||
|
||||
// ── Сетка канала ──────────────────────────────────────────────────────────
|
||||
|
||||
export function getChannelTemplate(channelId: string) {
|
||||
return apiRequest<ScheduleTemplateDto>(`/admin/channels/${channelId}/template`)
|
||||
}
|
||||
|
||||
/** Заводит каналу сетку, если её нет: у каналов из старой ротации шаблона может не быть. */
|
||||
export function createChannelTemplate(channelId: string) {
|
||||
return apiRequest<CreatedIdResponse>(`/admin/channels/${channelId}/template`, { method: 'POST' })
|
||||
}
|
||||
|
||||
/** Применяет правила к эфиру: пересобирает будущий хвост. Правка слотов эфир не двигает. */
|
||||
export function applyChannelTemplate(channelId: string) {
|
||||
return apiRequest<ApplyResultDto>(`/admin/channels/${channelId}/template/apply`, {
|
||||
method: 'POST',
|
||||
})
|
||||
}
|
||||
|
||||
/** Проверки сетки по правилам — считаются по шаблону, без прогона генератора. */
|
||||
export function getTemplateIssues(channelId: string) {
|
||||
return apiRequest<TemplateIssueDto[]>(`/admin/channels/${channelId}/template/issues`)
|
||||
}
|
||||
|
||||
/** Что изменится в эфире, если применить сейчас. Прогон сухой — лента не трогается. */
|
||||
export function getApplyDiff(channelId: string) {
|
||||
return apiRequest<ScheduleDiffDto>(`/admin/channels/${channelId}/template/diff`)
|
||||
}
|
||||
|
||||
/** Копия сетки на другой канал: слои, слоты, стыки и правила. Группы общие и не копируются. */
|
||||
export function copyTemplateTo(channelId: string, targetChannelId: string) {
|
||||
return apiRequest<CopyTemplateResultDto>(
|
||||
`/admin/channels/${channelId}/template/copy-to/${targetChannelId}`,
|
||||
{ method: 'POST' },
|
||||
)
|
||||
}
|
||||
|
||||
/** Цепочка происхождения записи, записанная в момент генерации. */
|
||||
export function getEntryTrace(entryId: string) {
|
||||
return apiRequest<EntryTraceDto>(`/admin/channels/entries/${entryId}/trace`)
|
||||
}
|
||||
|
||||
/** Сухой прогон по текущим правилам: ничего не пишет и не двигает курсоры слотов. */
|
||||
export function previewTemplate(channelId: string, days: number) {
|
||||
const query = new URLSearchParams({ days: String(days) })
|
||||
return apiRequest<SchedulePreviewDto>(
|
||||
`/admin/channels/${channelId}/template/preview?${query.toString()}`,
|
||||
)
|
||||
}
|
||||
|
||||
export function updateTemplate(
|
||||
templateId: string,
|
||||
body: {
|
||||
name: string
|
||||
fallbackGroupId: string | null
|
||||
defaultJunctionId: string | null
|
||||
rules: PlanningRules | null
|
||||
},
|
||||
) {
|
||||
return apiRequest<void>(`/admin/templates/${templateId}`, { method: 'PUT', body })
|
||||
}
|
||||
|
||||
export function createLayer(templateId: string, body: { name: string; priority: number }) {
|
||||
return apiRequest<CreatedIdResponse>(`/admin/templates/${templateId}/layers`, {
|
||||
method: 'POST',
|
||||
body,
|
||||
})
|
||||
}
|
||||
|
||||
export function updateLayer(
|
||||
layerId: string,
|
||||
body: {
|
||||
name: string
|
||||
priority: number
|
||||
applicability: LayerApplicability | null
|
||||
isEnabled: boolean
|
||||
},
|
||||
) {
|
||||
return apiRequest<void>(`/admin/layers/${layerId}`, { method: 'PUT', body })
|
||||
}
|
||||
|
||||
export function deleteLayer(layerId: string) {
|
||||
return apiRequest<void>(`/admin/layers/${layerId}`, { method: 'DELETE' })
|
||||
}
|
||||
|
||||
/** Тело слота: то же для создания и правки (см. SlotInput на сервере). */
|
||||
export type SlotBody = Omit<SlotDto, 'id' | 'layerId' | 'groupName'>
|
||||
|
||||
/** Слот из ответа сервера → тело запроса: отбрасываем то, что сервер проставляет сам. */
|
||||
export function toSlotBody(slot: SlotDto): SlotBody {
|
||||
const { id: _id, layerId: _layerId, groupName: _groupName, ...body } = slot
|
||||
return body
|
||||
}
|
||||
|
||||
export function createSlot(layerId: string, body: SlotBody) {
|
||||
return apiRequest<CreatedIdResponse>(`/admin/layers/${layerId}/slots`, { method: 'POST', body })
|
||||
}
|
||||
|
||||
export function updateSlot(slotId: string, body: SlotBody) {
|
||||
return apiRequest<void>(`/admin/slots/${slotId}`, { method: 'PUT', body })
|
||||
}
|
||||
|
||||
export function deleteSlot(slotId: string) {
|
||||
return apiRequest<void>(`/admin/slots/${slotId}`, { method: 'DELETE' })
|
||||
}
|
||||
|
||||
// ── Стыки канала ──────────────────────────────────────────────────────────
|
||||
|
||||
export function listJunctions(channelId: string) {
|
||||
return apiRequest<JunctionTemplateDto[]>(`/admin/channels/${channelId}/junctions`)
|
||||
}
|
||||
|
||||
export function createJunction(channelId: string, name: string) {
|
||||
return apiRequest<CreatedIdResponse>(`/admin/channels/${channelId}/junctions`, {
|
||||
method: 'POST',
|
||||
body: { name },
|
||||
})
|
||||
}
|
||||
|
||||
export function renameJunction(junctionId: string, name: string) {
|
||||
return apiRequest<void>(`/admin/junctions/${junctionId}`, { method: 'PUT', body: { name } })
|
||||
}
|
||||
|
||||
export function deleteJunction(junctionId: string) {
|
||||
return apiRequest<void>(`/admin/junctions/${junctionId}`, { method: 'DELETE' })
|
||||
}
|
||||
|
||||
export function addJunctionElement(junctionId: string, kind: JunctionElementKind) {
|
||||
return apiRequest<CreatedIdResponse>(`/admin/junctions/${junctionId}/elements`, {
|
||||
method: 'POST',
|
||||
body: { kind },
|
||||
})
|
||||
}
|
||||
|
||||
/** Тело врезки: то же для любого типа — лишние поля сервер обнуляет сам (см. JunctionElement.Update). */
|
||||
export type JunctionElementBody = {
|
||||
kind: JunctionElementKind
|
||||
groupId: string | null
|
||||
bumperTemplateId: string | null
|
||||
amountMode: JunctionAmountMode
|
||||
amountValue: number
|
||||
isRequired: boolean
|
||||
conditions: JunctionConditions | null
|
||||
}
|
||||
|
||||
export function updateJunctionElement(
|
||||
junctionId: string,
|
||||
elementId: string,
|
||||
body: JunctionElementBody,
|
||||
) {
|
||||
return apiRequest<void>(`/admin/junctions/${junctionId}/elements/${elementId}`, {
|
||||
method: 'PUT',
|
||||
body,
|
||||
})
|
||||
}
|
||||
|
||||
export function removeJunctionElement(junctionId: string, elementId: string) {
|
||||
return apiRequest<void>(`/admin/junctions/${junctionId}/elements/${elementId}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
}
|
||||
|
||||
/** Порядок врезок: не упомянутые остаются после перечисленных. */
|
||||
export function reorderJunction(junctionId: string, elementIdsInOrder: string[]) {
|
||||
return apiRequest<void>(`/admin/junctions/${junctionId}/order`, {
|
||||
method: 'PUT',
|
||||
body: { elementIdsInOrder },
|
||||
})
|
||||
}
|
||||
|
||||
type BumperTemplateStyleBody = {
|
||||
name: string
|
||||
backgroundColor: string
|
||||
backgroundColor2: string
|
||||
accentColor: string
|
||||
textColor: string
|
||||
}
|
||||
|
||||
export function addBumperTemplate(id: string, name: string) {
|
||||
return apiRequest<CreatedIdResponse>(`/admin/channels/${id}/bumper/templates`, {
|
||||
method: 'POST',
|
||||
body: { name },
|
||||
})
|
||||
}
|
||||
|
||||
export function updateBumperTemplate(
|
||||
id: string,
|
||||
templateId: string,
|
||||
body: BumperTemplateStyleBody,
|
||||
) {
|
||||
return apiRequest<void>(`/admin/channels/${id}/bumper/templates/${templateId}`, {
|
||||
method: 'PUT',
|
||||
body,
|
||||
})
|
||||
}
|
||||
|
||||
export function removeBumperTemplate(id: string, templateId: string) {
|
||||
return apiRequest<void>(`/admin/channels/${id}/bumper/templates/${templateId}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
}
|
||||
|
||||
/** Загрузка сырого файла блока (звук/фон): тело — файл, имя — в query (как в uploadMedia). */
|
||||
function uploadBumperTemplateFile(
|
||||
id: string,
|
||||
templateId: string,
|
||||
kind: 'audio' | 'background',
|
||||
file: File,
|
||||
): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest()
|
||||
const query = new URLSearchParams({ fileName: file.name })
|
||||
xhr.open(
|
||||
'PUT',
|
||||
`/api/admin/channels/${id}/bumper/templates/${templateId}/${kind}?${query.toString()}`,
|
||||
)
|
||||
const token = getAccessToken()
|
||||
if (token) xhr.setRequestHeader('Authorization', `Bearer ${token}`)
|
||||
xhr.onload = () => {
|
||||
if (xhr.status >= 200 && xhr.status < 300) {
|
||||
resolve()
|
||||
} else {
|
||||
let detail = `HTTP ${xhr.status}`
|
||||
try {
|
||||
const problem = JSON.parse(xhr.responseText) as { detail?: string; title?: string }
|
||||
detail = problem.detail ?? problem.title ?? detail
|
||||
} catch {
|
||||
/* пусто */
|
||||
}
|
||||
reject(new HttpError({ detail }, xhr.status))
|
||||
}
|
||||
}
|
||||
xhr.onerror = () => reject(new HttpError({ title: 'Network error' }, 0))
|
||||
xhr.send(file)
|
||||
})
|
||||
}
|
||||
|
||||
export function uploadBumperTemplateAudio(id: string, templateId: string, file: File) {
|
||||
return uploadBumperTemplateFile(id, templateId, 'audio', file)
|
||||
}
|
||||
|
||||
type BumperVariantBody = {
|
||||
name: string
|
||||
kind: BumperTextKind
|
||||
nowLabel: string
|
||||
nextLabel: string
|
||||
line1: string
|
||||
line2: string
|
||||
trigger: BumperTrigger
|
||||
weight: number
|
||||
}
|
||||
|
||||
export function addBumperVariant(id: string, templateId: string, name: string) {
|
||||
return apiRequest<CreatedIdResponse>(
|
||||
`/admin/channels/${id}/bumper/templates/${templateId}/variants`,
|
||||
{ method: 'POST', body: { name } },
|
||||
)
|
||||
}
|
||||
|
||||
export function updateBumperVariant(
|
||||
id: string,
|
||||
templateId: string,
|
||||
variantId: string,
|
||||
body: BumperVariantBody,
|
||||
) {
|
||||
return apiRequest<void>(
|
||||
`/admin/channels/${id}/bumper/templates/${templateId}/variants/${variantId}`,
|
||||
{ method: 'PUT', body },
|
||||
)
|
||||
}
|
||||
|
||||
export function removeBumperVariant(id: string, templateId: string, variantId: string) {
|
||||
return apiRequest<void>(
|
||||
`/admin/channels/${id}/bumper/templates/${templateId}/variants/${variantId}`,
|
||||
{ method: 'DELETE' },
|
||||
)
|
||||
}
|
||||
|
||||
/** Привязать фон-картинку блока по ссылке на изображение из реестра (галерея). */
|
||||
export function setBumperTemplateBackground(id: string, templateId: string, imageId: string) {
|
||||
return apiRequest<void>(`/admin/channels/${id}/bumper/templates/${templateId}/background`, {
|
||||
method: 'PUT',
|
||||
body: { imageId },
|
||||
})
|
||||
}
|
||||
|
||||
export function clearBumperTemplateAudio(id: string, templateId: string) {
|
||||
return apiRequest<void>(`/admin/channels/${id}/bumper/templates/${templateId}/audio`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
}
|
||||
|
||||
export function clearBumperTemplateBackground(id: string, templateId: string) {
|
||||
return apiRequest<void>(`/admin/channels/${id}/bumper/templates/${templateId}/background`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
}
|
||||
|
||||
/** Синхронно рендерит примеры всех подблоков блока (сервер собирает ffmpeg-клипы). */
|
||||
export function renderBumperPreviews(id: string, templateId: string) {
|
||||
return apiRequest<void>(`/admin/channels/${id}/bumper/templates/${templateId}/preview`, {
|
||||
method: 'POST',
|
||||
})
|
||||
}
|
||||
|
||||
export function bumperPreviewPlaylistUrl(id: string, templateId: string, variantId: string) {
|
||||
return `/api/admin/channels/${id}/bumper/templates/${templateId}/preview/${variantId}/index.m3u8`
|
||||
}
|
||||
|
||||
export function getSchedule(id: string, from: Date, to: Date) {
|
||||
const query = new URLSearchParams({ from: from.toISOString(), to: to.toISOString() })
|
||||
return apiRequest<ScheduleEntryDto[]>(`/admin/channels/${id}/schedule?${query.toString()}`)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user