From 1d1490b06a1f5ecb10b3b98a72efc55d09c10944 Mon Sep 17 00:00:00 2001
From: Yassine Doghri <yassine@doghri.fr>
Date: Wed, 8 Dec 2021 17:05:34 +0000
Subject: [PATCH] feat(video-clips): add new themes + add castopod logo as a
 watermark

fix video colors to portray exact rgb values using libx264rgb encoding + image transparency issue
when overlaying images with php gd
---
 .../MediaClipper/Config/MediaClipper.php      | 116 +++++++++++++++++-
 app/Libraries/MediaClipper/VideoClip.php      |  84 +++++++++----
 app/Libraries/MediaClipper/castopod-logo.png  | Bin 0 -> 2803 bytes
 .../Components/Forms/ColorRadioButton.php     |   4 +-
 themes/cp_admin/episode/video_clips_new.php   |   7 +-
 5 files changed, 178 insertions(+), 33 deletions(-)
 create mode 100644 app/Libraries/MediaClipper/castopod-logo.png

diff --git a/app/Libraries/MediaClipper/Config/MediaClipper.php b/app/Libraries/MediaClipper/Config/MediaClipper.php
index c457185199..f4274d6de2 100644
--- a/app/Libraries/MediaClipper/Config/MediaClipper.php
+++ b/app/Libraries/MediaClipper/Config/MediaClipper.php
@@ -14,6 +14,8 @@ class MediaClipper extends BaseConfig
 
     public string $wavesMask = APPPATH . 'Libraries/MediaClipper/waves-mask.png';
 
+    public string $watermark = APPPATH . 'Libraries/MediaClipper/castopod-logo.png';
+
     /**
      * @var array<string, array<string, int|array<string, float|int|string>>>
      */
@@ -57,9 +59,15 @@ class MediaClipper extends BaseConfig
             'timestamp' => [
                 'fontsize' => 32,
                 'padding' => 10,
-                'x' => 1680,
+                'x' => 1620,
                 'y' => 985,
             ],
+            'watermark' => [
+                'width' => 90,
+                'height' => 72,
+                'x' => 140,
+                'y' => 960,
+            ],
             'progressbar' => [
                 'height' => 10,
             ],
@@ -117,10 +125,16 @@ class MediaClipper extends BaseConfig
             ],
             'timestamp' => [
                 'fontsize' => 48,
-                'padding' => 10,
-                'x' => 735,
+                'padding' => 14,
+                'x' => 734,
                 'y' => 1800,
             ],
+            'watermark' => [
+                'width' => 120,
+                'height' => 96,
+                'x' => 130,
+                'y' => 1770,
+            ],
             'progressbar' => [
                 'height' => 10,
             ],
@@ -183,6 +197,12 @@ class MediaClipper extends BaseConfig
                 'x' => 855,
                 'y' => 1070,
             ],
+            'watermark' => [
+                'width' => 120,
+                'height' => 96,
+                'x' => 130,
+                'y' => 1040,
+            ],
             'progressbar' => [
                 'height' => 10,
             ],
@@ -209,6 +229,8 @@ class MediaClipper extends BaseConfig
      */
     public array $themes = [
         'pine' => [
+            // Preview must be a HSL colorscheme string
+            'preview' => '174 100% 29%',
             'background' => [0, 86, 74],
             'text' => [255, 255, 255],
             // subtitle hex color is BGR (Blue, Green, Red),
@@ -220,7 +242,93 @@ class MediaClipper extends BaseConfig
             'progressbar' => '009486',
             'timestampBg' => '00564A',
             'timestampText' => 'FFFFFF',
-            'soundwaves' => 'F2FAF9',
+            'watermarkBg' => '00564A',
+            'soundwaves' => [231, 249, 228],
+        ],
+        'crimson' => [
+            // Preview must be a HSL colorscheme string
+            'preview' => '350 87% 61%',
+            'background' => [179, 31, 57],
+            'text' => [255, 255, 255],
+            // subtitle hex color is BGR (Blue, Green, Red),
+            'subtitles' => 'FFFFFF',
+            // quotes image MUST BE black
+            'quotes' => [242, 70, 100],
+            'episodeNumberingBg' => [152, 16, 43],
+            'episodeNumberingText' => [255, 255, 255],
+            'progressbar' => 'F24664',
+            'timestampBg' => 'B31F39',
+            'timestampText' => 'FFFFFF',
+            'watermarkBg' => 'B31F39',
+            'soundwaves' => [253, 206, 215],
+        ],
+        'lake' => [
+            // Preview must be a HSL colorscheme string
+            'preview' => '194 100% 44%',
+            'background' => [0, 86, 113],
+            'text' => [255, 255, 255],
+            // subtitle hex color is BGR (Blue, Green, Red),
+            'subtitles' => 'FFFFFF',
+            // quotes image MUST BE black
+            'quotes' => [0, 171, 225],
+            'episodeNumberingBg' => [0, 43, 57],
+            'episodeNumberingText' => [255, 255, 255],
+            'progressbar' => '00ABE1',
+            'timestampBg' => '005671',
+            'timestampText' => 'FFFFFF',
+            'watermarkBg' => '005671',
+            'soundwaves' => [214, 245, 255],
+        ],
+        'amber' => [
+            // Preview must be a HSL colorscheme string
+            'preview' => '17 100% 57%',
+            'background' => [177, 50, 0],
+            'text' => [255, 255, 255],
+            // subtitle hex color is BGR (Blue, Green, Red),
+            'subtitles' => 'FFFFFF',
+            // quotes image MUST BE black
+            'quotes' => [255, 96, 34],
+            'episodeNumberingBg' => [121, 34, 0],
+            'episodeNumberingText' => [255, 255, 255],
+            'progressbar' => 'FF6022',
+            'timestampBg' => 'B13200',
+            'timestampText' => 'FFFFFF',
+            'watermarkBg' => 'B13200',
+            'soundwaves' => [255, 213, 197],
+        ],
+        'jacaranda' => [
+            // Preview must be a HSL colorscheme string
+            'preview' => '254 72% 52%',
+            'background' => [47, 21, 132],
+            'text' => [255, 255, 255],
+            // subtitle hex color is BGR (Blue, Green, Red),
+            'subtitles' => 'FFFFFF',
+            // quotes image MUST BE black
+            'quotes' => [86, 45, 221],
+            'episodeNumberingBg' => [30, 14, 84],
+            'episodeNumberingText' => [255, 255, 255],
+            'progressbar' => '562DDD',
+            'timestampBg' => '2F1584',
+            'timestampText' => 'FFFFFF',
+            'watermarkBg' => '2F1584',
+            'soundwaves' => [199, 185, 244],
+        ],
+        'onyx' => [
+            // Preview must be a HSL colorscheme string
+            'preview' => '240 17% 2%',
+            'background' => [5, 5, 7],
+            'text' => [255, 255, 255],
+            // subtitle hex color is BGR (Blue, Green, Red),
+            'subtitles' => 'FFFFFF',
+            // quotes image MUST BE black
+            'quotes' => [38, 38, 49],
+            'episodeNumberingBg' => [0, 0, 0],
+            'episodeNumberingText' => [255, 255, 255],
+            'progressbar' => 'D5D5E1',
+            'timestampBg' => '050507',
+            'timestampText' => 'FFFFFF',
+            'watermarkBg' => '050507',
+            'soundwaves' => [213, 213, 225],
         ],
     ];
 }
diff --git a/app/Libraries/MediaClipper/VideoClip.php b/app/Libraries/MediaClipper/VideoClip.php
index 909bd13e82..d1f11de5b9 100644
--- a/app/Libraries/MediaClipper/VideoClip.php
+++ b/app/Libraries/MediaClipper/VideoClip.php
@@ -13,6 +13,11 @@ namespace MediaClipper;
 use App\Entities\Episode;
 use GdImage;
 
+/**
+ * TODO: refactor this by splitting image manipulations + video generation
+ *
+ * @phpstan-ignore-next-line
+ */
 class VideoClip
 {
     /**
@@ -44,21 +49,11 @@ class VideoClip
 
     protected ?string $episodeNumbering = null;
 
-    /**
-     * @var 'landscape'|'portrait'|'squared'
-     */
-    protected string $format = 'landscape';
-
     /**
      * @var array<string, mixed>
      */
     protected array $dimensions = [];
 
-    /**
-     * @var 'pine'|'crimson'|'lake'|'amber'|'jacaranda'|'onyx'
-     */
-    protected string $theme = 'pine';
-
     /**
      * @var array<string, mixed>
      */
@@ -72,11 +67,10 @@ class VideoClip
         protected Episode $episode,
         protected float $start,
         protected float $end,
-        string $format,
-        string $theme,
+        protected string $format = 'landscape',
+        protected string $theme = 'pine',
     ) {
         $this->duration = $end - $start;
-        $this->format = $format;
         $this->episodeNumbering = $this->episodeNumbering($this->episode->number, $this->episode->season_number);
         $this->dimensions = config('MediaClipper')
             ->formats[$format];
@@ -95,8 +89,8 @@ class VideoClip
 
         $this->soundbiteOutput = $podcastFolder . "/{$this->episode->slug}-soundbite-{$this->start}-to-{$this->end}.mp3";
         $this->subtitlesClipOutput = $podcastFolder . "/{$this->episode->slug}-subtitles-clip-{$this->start}-to-{$this->end}.srt";
-        $this->videoClipBgOutput = $podcastFolder . "/{$this->episode->slug}-clip-bg-{$this->format}.png";
-        $this->videoClipOutput = $podcastFolder . "/{$this->episode->slug}-clip-{$this->start}-to-{$this->end}-{$this->format}.mp4";
+        $this->videoClipBgOutput = $podcastFolder . "/{$this->episode->slug}-clip-bg-{$this->format}-{$this->theme}.png";
+        $this->videoClipOutput = $podcastFolder . "/{$this->episode->slug}-clip-{$this->start}-to-{$this->end}-{$this->format}-{$this->theme}.mp4";
     }
 
     public function soundbite(): void
@@ -132,23 +126,30 @@ class VideoClip
     {
         // @phpstan-ignore
         $filters = [
-            "[0:a]aformat=channel_layouts=mono,showwaves=s={$this->dimensions['soundwaves']['width']}x{$this->dimensions['soundwaves']['height']}:mode=cline:rate=10:colors=0xFFFFFF,format=yuva420p[waves]",
+            "[0:a]aformat=channel_layouts=mono,showwaves=s={$this->dimensions['soundwaves']['width']}x{$this->dimensions['soundwaves']['height']}:mode=cline:rate=10:colors=white,format=rgb24[waves]",
             "[waves]scale={$this->dimensions['width']}:{$this->dimensions['height']}:flags=neighbor[resizedwaves]",
             '[resizedwaves][3:v][4:v][5:v]threshold[cleanwaves]',
             '[cleanwaves][2:v]alphamerge[waves_t]',
             '[4:v][waves_t]overlay=x=0:y=0:shortest=1[waves_t2]',
             '[waves_t2]split[m][a]',
             '[m][a]alphamerge[waves_t3]',
-            "[waves_t3]scale={$this->dimensions['soundwaves']['rescaleWidth']}:{$this->dimensions['soundwaves']['rescaleHeight']}[waves_final]",
+            "[waves_t3]scale={$this->dimensions['soundwaves']['rescaleWidth']}:{$this->dimensions['soundwaves']['rescaleHeight']},lutrgb=r='if(gt(val,100),{$this->colors['soundwaves'][0]},val)':g='if(gt(val,100),{$this->colors['soundwaves'][1]},val)':b='if(gt(val,100),{$this->colors['soundwaves'][2]},val)'[waves_final]",
             "[1:v][waves_final]overlay=x={$this->dimensions['soundwaves']['x']}:y={$this->dimensions['soundwaves']['y']}:shortest=1,drawtext=fontfile=" . $this->getFont(
                 'timestamp'
-            ) . ":text='%{pts\:gmtime\:{$this->start}\:%H\\\\\\\\\\:%M\\\\\\\\\\:%S\}':x={$this->dimensions['timestamp']['x']}:y={$this->dimensions['timestamp']['y']}:fontsize={$this->dimensions['timestamp']['fontsize']}:fontcolor=0x{$this->colors['timestampText']}:box=1:boxcolor=0x{$this->colors['timestampBg']}:boxborderw={$this->dimensions['timestamp']['padding']},format=yuv420p,colormatrix=bt601:bt2020[v3]",
+            ) . ":text='%{pts\:gmtime\:{$this->start}\:%H\\\\\\\\\\:%M\\\\\\\\\\:%S\}':x={$this->dimensions['timestamp']['x']}:y={$this->dimensions['timestamp']['y']}:fontsize={$this->dimensions['timestamp']['fontsize']}:fontcolor=0x{$this->colors['timestampText']}:box=1:boxcolor=0x{$this->colors['timestampBg']}:boxborderw={$this->dimensions['timestamp']['padding']}[v3]",
             "color=c=0x{$this->colors['progressbar']}:s={$this->dimensions['width']}x{$this->dimensions['progressbar']['height']}[progressbar]",
             "[v3][progressbar]overlay=-w+(w/{$this->duration})*t:0:shortest=1:format=rgb,subtitles={$this->subtitlesClipOutput}:fontsdir=" . config(
                 'MediaClipper'
-            )->fontsFolder . ":force_style='Fontname=" . self::FONTS['subtitles'] . ",Alignment=5,Fontsize={$this->dimensions['subtitles']['fontsize']},PrimaryColour=&H{$this->colors['subtitles']}&,BorderStyle=1,Outline=0,Shadow=0,MarginL={$this->dimensions['subtitles']['marginL']},MarginR={$this->dimensions['subtitles']['marginR']},MarginV={$this->dimensions['subtitles']['marginV']}',format=yuv420p,colormatrix=bt601:bt2020[outv]",
+            )->fontsFolder . ":force_style='Fontname=" . self::FONTS['subtitles'] . ",Alignment=5,Fontsize={$this->dimensions['subtitles']['fontsize']},PrimaryColour=&H{$this->colors['subtitles']}&,BorderStyle=1,Outline=0,Shadow=0,MarginL={$this->dimensions['subtitles']['marginL']},MarginR={$this->dimensions['subtitles']['marginR']},MarginV={$this->dimensions['subtitles']['marginV']}'[outv]",
+            "[6:v]scale={$this->dimensions['watermark']['width']}:{$this->dimensions['watermark']['height']}[watermark]",
+            "color=0x{$this->colors['watermarkBg']}:{$this->dimensions['watermark']['width']}x{$this->dimensions['watermark']['height']}[over]",
+            '[over][watermark]overlay=x=0:y=0:shortest=1[watermark_box]',
+            "[outv][watermark_box]overlay=x={$this->dimensions['watermark']['x']}:y={$this->dimensions['watermark']['y']}:shortest=1[watermarked]",
         ];
 
+        $watermark = config('MediaClipper')
+            ->watermark;
+
         $videoClipCmd = [
             'ffmpeg -y',
             "-i {$this->soundbiteOutput}",
@@ -156,13 +157,13 @@ class VideoClip
             "-loop 1 -framerate 30 -i {$this->dimensions['soundwaves']['mask']}",
             "-f lavfi -i color=gray:{$this->dimensions['width']}x{$this->dimensions['height']}",
             "-f lavfi -i color=black:{$this->dimensions['width']}x{$this->dimensions['height']}",
-            "-f lavfi -i color=0x{$this->colors['soundwaves']}:{$this->dimensions['width']}x{$this->dimensions['height']}",
+            "-f lavfi -i color=white:{$this->dimensions['width']}x{$this->dimensions['height']}",
+            "-loop 1 -framerate 1 -i {$watermark}",
             '-filter_complex "' . implode(';', $filters) . '"',
-            '-map "[outv]"',
+            '-map "[watermarked]"',
             '-map 0:a',
             '-acodec copy',
-            '-vcodec libx264',
-            '-pix_fmt yuv420p',
+            '-vcodec libx264rgb',
             "{$this->videoClipOutput}",
         ];
 
@@ -306,10 +307,16 @@ class VideoClip
             return false;
         }
 
-        imagefilter($quotes, IMG_FILTER_COLORIZE, ...$this->colors['quotes']);
+        $cleanedQuotes = $this->cleanTransparency($quotes);
+
+        if (! $cleanedQuotes) {
+            return false;
+        }
+        imagefilter($cleanedQuotes, IMG_FILTER_CONTRAST, 255);
+        imagefilter($cleanedQuotes, IMG_FILTER_COLORIZE, ...$this->colors['quotes']);
 
         $scaledQuotes = $this->scaleImage(
-            $quotes,
+            $cleanedQuotes,
             $this->dimensions['quotes']['width'],
             $this->dimensions['quotes']['height']
         );
@@ -318,6 +325,7 @@ class VideoClip
             return false;
         }
 
+        imagesavealpha($scaledQuotes, true);
         $this->overlayImages(
             $background,
             $scaledQuotes,
@@ -570,6 +578,32 @@ class VideoClip
         return true;
     }
 
+    /**
+     * This helps getting a truly transparent background for images with transparency:
+     * https://stackoverflow.com/a/2611911
+     */
+    private function cleanTransparency(GdImage $image): GdImage | false
+    {
+        $imageBg = imagecolorallocate($image, 0, 0, 0);
+        if ($imageBg === false) {
+            return false;
+        }
+
+        // removing the black from the image
+        imagecolortransparent($image, $imageBg);
+
+        // turning off alpha blending (to ensure alpha channel information
+        // is preserved, rather than removed (blending with the rest of the
+        // image in the form of black))
+        imagealphablending($image, false);
+
+        // turning on alpha channel information saving (to ensure the full range
+        // of transparency is preserved)
+        imagesavealpha($image, true);
+
+        return $image;
+    }
+
     /**
      * @return array<int, string>|false
      */
diff --git a/app/Libraries/MediaClipper/castopod-logo.png b/app/Libraries/MediaClipper/castopod-logo.png
new file mode 100644
index 0000000000000000000000000000000000000000..fa6544aaa2087553e12aa7f77e084429f7d3abbb
GIT binary patch
literal 2803
zcmeAS@N?(olHy`uVBq!ia0y~yVEDzrz;J?tje&vTVBDYC3=9mM1s;*b3=G`DAk4@x
zYmNj10|R4mkh>GZx^prw85kH?(j9#r85lP9bN@+XWnkdy@N{tusbGA2*E>h_y6o|f
z|L-J}Pn?pja%n}w;w>IWmN+P_auQPvTGSw-$*;0twUg?mg(?a+L_(5;rsj3==-!>c
zdzW=d(4wH$1581hYF?MFt=JY2_v)xyqepM0C3nt;FMrzaTw8Qw<LjBlY3Kg`o~U<&
zDW~}PpJz7DY+M+~03n`Asb6kipVoP0Ua|dMCeO*O@4l~Oni8p1>Un9`lC8=`kAKbH
zv~#VVYS6j`*#{=y(tdY5O3h)ZDtpR@U*|Tl?2~cK&!4U3dsgqkCeMZ$Dwouxx4oTm
z;1K)YuoM2f`o*{e7Q6JFIB>u!e0Tgb2Q|-(7MYFSPc1X^uJzBG?vN#PX<zJHrxS^d
zZMFB;CB$AX@cn-OZ_z^O!2i1$rm4JCy18L*unl+k;^mk08cZ8c)y|OGd-v)y-z(1y
zvI-Jry>HxUVERiXQoAA4_iXmj%?6IwCG#6XJzwqp!~SZqde4@_>#o%Q<TsT#ey8-t
z)L;7l6X!2x{?N35nN6VN07Da>gu?|!W)7<a21j-tg%=HsEV2d-7?KV@-p<p`y0-oO
zw3BDg-sJu_=l%QX_p;+TkF_u8KltRCTBKe2Hl4lo%XI#ptJQn*o#W7}Uz_}Q-Ch0o
zka*Fr&)t`9`~PrvzTm##VVv#v>P>r6!^HBYoc<$udGqu*<*S7c7s>qjVeDkj)Kn+U
z`{P%;{3P+{=O>@!U({L8wO8`>>sMbHD__U-zxnrClF6ROQ9kYTVs4I#qc68!j;eD#
zopbYksTsG8MCti6^X+{7_I&8csZC*2>~G<l^y+I({aJ3K*YkECwfgKBZ(l#}&%>F^
zH~*7+w3vDM`~MY7U-z9AKJ8<EH$HZg1-I3s@2~pB(zoupx@^x)soL|8`vr@Vw`lHv
z{&8v2#FGoMPlTVZ4M^zcyAsK>d*_1u!tzq5yxDEB^EX5!X}ys8u=bdq!8@PXA5T9&
zBcy$XC02gpiw2$x8_I7cr^)S|$@xpJJh0NO?_r5Z-04<UwWf8xWzSYW_RI5~J6Cg~
zv0#a81G8IFVuIeU1=+dhHU;$@b6P7`d%^kYv%S}UZ4SS_|JSDbh3UI)<^1@)WPZCq
z^;)^FA~#gu>AhWTcAe?hG4=T;Z-140`D*tUd)6>^rqYjRw7&C{uRK0~b&r+ZevRYj
zzU~S?FI(O{(>{6DmUA=oR_}X%EbQHP<87C21gKn_U3A6qyP=^Q^WUoLkvH$xmR-5!
z@!zN1`Qmi36<5~@M9<!`kn#TC?Z!8&4Q8#=*H6{I>*18&Wj$Tz+ol&8Q?>Oso@?#j
z^)l{@$L_rJFHhwAPpyxADC<8tIGgvcxqZ#!4KvgJ_Fa}gamrr)|FH@8{r>+8Ztt(+
zOgQuROGa~C$>zTkR_AVc;HR?USN{Bx)`)zGjhCb!oLZcpzII;c1?k7T9;-in<xp5#
zu~s7K?i=G<3&d|;xty@?&$-<@d1NvV##Vl**pgWPQ~!P6ZZY}JZ?Cte1}url{<<Rj
zi&BQ}pUc9_Rs5yO{?2l}7N(d{|0iN^_@^a*zg_=txbfP-j3il=Ub7d$Tv<(-!kaqk
zO0VVjXDF;V@&A0^cRP)ooW-V>=C(b`-F1KVf7Z)~mezh=lwof+?V#meMX{AD)b_}o
z5nP<n9Cu`a*TvRw-;xWfc)Io}YQ0@Ae(~zeLkTi|&rMdC%xY$3Khn;}|9-O7kHCNL
zMHjz2_VS~>-K?uo-l2Pc)~&gxY**s`C&DP7rM9PL-mizN-E>Y>`o9l7e)P}%<u~Tl
zm<W6^e3h7ZYR1p1_3K17Fg)XOTt98k?EQA(p{-E|r;5jKO%lBS{MCU4M>K9Ncy4?7
z%yXBnRr!@qU+>j=^!r88O$k@&FQ2b}daLgDQDWb!{D>E?pa0Keysv(ZabHIByzi`^
zE!lT<oV2t1IV(2oNLAarecnQgGn)Th+gbBG;`QfH`NZp%ze4M$?cQ_reBH~+_owgO
z5Y=$Im}j$)*>u-M$9dLMZ0;IWA90VLJbQM`!P&ZNzxz$Tx$gZ9tH1lV`sUaFzixkC
z{N{~=8N&HV7Ou-S-YH&kaFfkF?YDAI_Wt&{Dxck__DVMG%b}-{64z?>GJ7s&$Zov)
zxONMhbnfKtV+$sV<t^RncD%Lw=5?umHF4bQ>-I|I)P7;#^!Q8r|DQFB-cLOzAtU!o
zsqDGsmygSZR=YUe*sd+gza{MF3-xRjfA-kK3r@>T-*bgas9QO$i(fBTVf#ma+Je=~
z`TlEGFWdL;Vf&M#*D8hU)=OD`o>Qi4YWvy7|JT;i<MR4JD^DNG<?nD0WRDGf`XprS
z)+Fl`r`h**c`V_|-czbP(RtrBNfwT(FUF5oU6Seks3IvVy58wT?V4@ey1y1=+uOE@
zCTjHcKRv#1=8V_tqNR@RieMB@UT7As)+Be^%=bk9yC>O=Gp}avD(<!mXO}7y-||0R
za%R-BUGI))o^y0|D__3#$|+;fFFE|B*FzTX&zpVkP3ith?=!hJ3&kV-`4r!nrCyKE
z|0Wv$**;FKe8xrQKVLLdW_{fDf@NN(+~UyCwC$6!?%d?u&3j9Id(B?o_mbBwo@_Xw
zTqiJ3?%RfUM?8zqetozoZm*fxt?=v0b-RB5N;$2}y~wuuv{LPyU#F_&-D;!WM$F`|
zYWACa^?I22x6opFl@FrVBA%ut9=jbO_~mv6<IVfEVPR)xy7I;wMm4`avLe~?y`Bhj
zQyu%Q>DgC4?5dsePG|F%eKV)tKdoz4HCJl)1(`ojg2N~OwoX}`#qrDh!qxm<-L{g)
zEk7?WPn|9AckWu`@ybtWGc#wL-@R;S-^wpv>Qk#4a&0(%S!67flB-#uWig2pOVxz8
z-lAM`vP#eCcc!7M^h*EtM^(ABWzC($7c2Ngf2FUWWYvM%4{KB|DgD^r!nuo0Wo5;M
z;FYI)8`V>aqhlW&R{ME&o$or{TY90>my~|g3Ay}NcBPS%+%fOl-%d=(nYPONX-|ci
z#tU<PkEyDgrcYpBlz3e_cPX!|;Q2!=e~+E@+j-T0y_{}}eQ_)A!zC~MR%+?mOV<~A
z*>DSgIh#BE?$KM?p;J6&E3BEmO@2krTDcW}TB`SKE3BUVdu#BjCt<n&svRq)uDN<S
zc;PAk8_!=fpZpxORLy2<Wo5?NxTTvGPd)PBffc7p&IR-Td)~>J+9-zZNVxLsY4Fr{
z@tpH6wak~?o>;wn^-b}q`_8-O#@U&(l?H6NGu6dY?&!2+t~J-*n6rsZ{l@2;8|RlQ
zcjc;QqE+95r@L7ClY}*Ae~aIGV#&g-lT^;{IxpsBxkmMc>eYK@p|fWF*L*rN=<Z9V
ze`(ilU62m+oTC5Y@JrV@CDYq#S1s9EApYsu15LFPQzCw6pMJ6~s=H+pPwcd)wR+6W
hUyWL&NpIG<yuSY+h3Cr0@WNn_W=~f?mvv4FO#ne#L+=0p

literal 0
HcmV?d00001

diff --git a/app/Views/Components/Forms/ColorRadioButton.php b/app/Views/Components/Forms/ColorRadioButton.php
index 906eb69329..a3454f54ba 100644
--- a/app/Views/Components/Forms/ColorRadioButton.php
+++ b/app/Views/Components/Forms/ColorRadioButton.php
@@ -8,6 +8,8 @@ class ColorRadioButton extends FormComponent
 {
     protected bool $isChecked = false;
 
+    protected string $style = '';
+
     public function setIsChecked(string $value): void
     {
         $this->isChecked = $value === 'true';
@@ -26,7 +28,7 @@ class ColorRadioButton extends FormComponent
         );
 
         return <<<HTML
-            <div class="{$this->class}">
+            <div class="{$this->class}" style="{$this->style}">
                 {$radioInput}
                 <label for="{$this->value}" title="{$this->slot}" data-tooltip="bottom"></label>
             </div>
diff --git a/themes/cp_admin/episode/video_clips_new.php b/themes/cp_admin/episode/video_clips_new.php
index abcb48b22a..93ce42a0f2 100644
--- a/themes/cp_admin/episode/video_clips_new.php
+++ b/themes/cp_admin/episode/video_clips_new.php
@@ -29,12 +29,13 @@
 </fieldset>
 
 <div class="grid gap-4 grid-cols-colorButtons">
-    <?php foreach (config('Colors')->themes as $themeName => $color): ?>
+    <?php foreach (config('MediaClipper')->themes as $themeName => $colors): ?>
         <Forms.ColorRadioButton
-        class="theme-<?= $themeName ?> mx-auto"
+        class="mx-auto"
         value="<?= $themeName ?>"
         name="theme"
-        isChecked="<?= $themeName === 'pine' ? 'true' : 'false' ?>" ><?= lang('Settings.theme.' . $themeName) ?></Forms.ColorRadioButton>
+        isChecked="<?= $themeName === 'pine' ? 'true' : 'false' ?>"
+        style="--color-accent-base: <?= $colors['preview']?>"><?= lang('Settings.theme.' . $themeName) ?></Forms.ColorRadioButton>
     <?php endforeach; ?>
 </div>
 
-- 
GitLab