From e95eed11dda203d978a7baf6633fe34beb6b018e Mon Sep 17 00:00:00 2001 From: Timothy Date: Tue, 7 Jul 2026 14:33:01 +0200 Subject: [PATCH] feat(web): FFmpeg profile, filler preset, watermark editors (#143) Three new SPA list+editor surfaces, closing the Blazor-only gap found in the #91 phase-(b) recon: - FFmpeg Profiles (System group): list with resolution/video/audio summary + add/copy/delete; full General/Video/Audio editor. Per-hwaccel video-format and profile rules ported from FFmpegProfileEditViewModelValidator; video preset list ported from FFmpegLibraryHelper.PresetsForFFmpegProfile / AvailablePresets.ForAccelAndFormat (cited in code). Resolutions from /api/settings/resolutions, hwaccel kinds from the new lookup endpoint. Copy = client-side clone into the create form (no server copy endpoint). - Filler Presets (Media group): list + editor with the Blazor VM interlocks (Fallback/Tail force mode None; non-MidRoll clears expression; Fallback disables chapters; Playlist collection type forces Count mode; duration/ count/pad fields gated by mode). Collection pickers use the existing /api/library/browse client for all seven collection types incl. Playlist. - Watermarks (System group): list + editor with mode-driven field enabling, ChannelLogo vs Custom image source, upload via /api/artwork/uploads (target=watermark) and preview via /artwork/watermarks/{path}. - App.tsx: ScreenIds/routes (sub-paths /add + /{id}) and nav registration; api modules ffmpegProfiles/fillerPresets/watermarks with URL-assert tests; SettingsScreen Classic-UI help text updated (these are no longer Blazor-only) and the FFmpeg-profiles card now links to the new screen. - Regenerated web/src/api/generated/v1.d.ts from the #159 OpenAPI changes. web verification: typecheck clean; vitest 188/188 (14 files); vite build OK; eslint clean on touched files. Co-Authored-By: Claude Fable 5 --- web/src/App.test.tsx | 4 +- web/src/App.tsx | 60 +- web/src/api/ffmpegProfiles.test.ts | 105 +++ web/src/api/ffmpegProfiles.ts | 43 ++ web/src/api/fillerPresets.test.ts | 79 +++ web/src/api/fillerPresets.ts | 38 + web/src/api/generated/v1.d.ts | 131 +++- web/src/api/index.ts | 3 + web/src/api/watermarks.test.ts | 86 +++ web/src/api/watermarks.ts | 47 ++ web/src/screens/FFmpegProfilesScreen.tsx | 846 +++++++++++++++++++++++ web/src/screens/FillerPresetsScreen.tsx | 738 ++++++++++++++++++++ web/src/screens/SettingsScreen.tsx | 13 +- web/src/screens/WatermarksScreen.tsx | 628 +++++++++++++++++ 14 files changed, 2805 insertions(+), 16 deletions(-) create mode 100644 web/src/api/ffmpegProfiles.test.ts create mode 100644 web/src/api/ffmpegProfiles.ts create mode 100644 web/src/api/fillerPresets.test.ts create mode 100644 web/src/api/fillerPresets.ts create mode 100644 web/src/api/watermarks.test.ts create mode 100644 web/src/api/watermarks.ts create mode 100644 web/src/screens/FFmpegProfilesScreen.tsx create mode 100644 web/src/screens/FillerPresetsScreen.tsx create mode 100644 web/src/screens/WatermarksScreen.tsx diff --git a/web/src/App.test.tsx b/web/src/App.test.tsx index 01ed499d0..98019317f 100644 --- a/web/src/App.test.tsx +++ b/web/src/App.test.tsx @@ -2712,7 +2712,7 @@ describe('Settings screen (#93)', () => { expect(screen.queryByRole('button', { name: 'Delete 1920×1080' })).not.toBeInTheDocument(); }); - it('renders FFmpeg profiles and media sources as read-only cards with a Legacy UI callout', async () => { + it('renders FFmpeg profiles read-only with a link to the FFmpeg Profiles editor, and media sources', async () => { mockDashboardApi({ ffmpegProfiles: [ffmpegProfile({ id: 1, name: '1080p H.264' }), ffmpegProfile({ id: 2, name: '720p H.264' })], mediaSources: [mediaSource({ id: 30, kind: 'Local', name: 'Local' })] @@ -2722,7 +2722,7 @@ describe('Settings screen (#93)', () => { expect((await screen.findAllByText('1080p H.264')).length).toBeGreaterThan(0); expect(screen.getAllByText('720p H.264').length).toBeGreaterThan(0); - expect(screen.getAllByText('Legacy UI').length).toBeGreaterThan(0); + expect(screen.getByRole('button', { name: /Manage FFmpeg profiles/ })).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: /^System/ })); expect(await screen.findByText('Local')).toBeInTheDocument(); diff --git a/web/src/App.tsx b/web/src/App.tsx index 29f9a517c..8f2dc601e 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -45,7 +45,9 @@ import { Server, Settings, Shuffle, + SlidersHorizontal, Sparkles, + Stamp, Stethoscope, Timer, Trash2, @@ -56,7 +58,10 @@ import chicoryMarkUrl from '../../design-system/assets/chicory-mark.svg'; import { ChannelBuilderScreen } from './builder/ChannelBuilder'; import { ChannelEditScreen } from './screens/ChannelEditScreen'; import { CollectionsScreen } from './screens/CollectionsScreen'; +import { FFmpegProfilesScreen } from './screens/FFmpegProfilesScreen'; +import { FillerPresetsScreen } from './screens/FillerPresetsScreen'; import { SettingsScreen } from './screens/SettingsScreen'; +import { WatermarksScreen } from './screens/WatermarksScreen'; import { navigateToPath } from './routing'; import { Badge, @@ -131,7 +136,10 @@ type ScreenId = | 'schedules' | 'playouts' | 'collections' + | 'fillerPresets' | 'libraries' + | 'ffmpegProfiles' + | 'watermarks' | 'settings'; interface ScreenRoute { @@ -244,6 +252,18 @@ const routes: ScreenRoute[] = [ primaryAction: 'Add Collection', placeholder: 'Collections workspace' }, + { + id: 'fillerPresets', + path: '/app/filler-presets', + label: 'Filler Presets', + title: 'Filler Presets', + kicker: 'Media', + description: 'Pre/mid/post-roll, tail and fallback filler presets used by schedules.', + icon: