From e462abf6d660e41d2170c52caf45704008de58e9 Mon Sep 17 00:00:00 2001
From: Yassine Doghri <yassine@doghri.fr>
Date: Tue, 7 Dec 2021 16:58:12 +0000
Subject: [PATCH] feat(video-clips): replace hardcoded colors with config's
 theme colors

---
 .../MediaClipper/Config/MediaClipper.php      |  20 ++++++
 app/Libraries/MediaClipper/VideoClip.php      |  61 ++++++++++++------
 app/Libraries/MediaClipper/quotes.png         | Bin 4247 -> 2822 bytes
 app/Resources/icons/clapperboard.svg          |   6 ++
 modules/Admin/Config/Routes.php               |  16 +++--
 ...ontroller.php => VideoClipsController.php} |  30 +++++++--
 .../Admin/Language/en/EpisodeNavigation.php   |   4 +-
 .../Admin/Language/fr/EpisodeNavigation.php   |   3 +
 themes/cp_admin/episode/_sidebar.php          |   6 +-
 themes/cp_admin/episode/video_clips_list.php  |  13 ++++
 .../{video_clips.php => video_clips_new.php}  |  16 +++--
 11 files changed, 140 insertions(+), 35 deletions(-)
 create mode 100644 app/Resources/icons/clapperboard.svg
 rename modules/Admin/Controllers/{ClipsController.php => VideoClipsController.php} (71%)
 create mode 100644 themes/cp_admin/episode/video_clips_list.php
 rename themes/cp_admin/episode/{video_clips.php => video_clips_new.php} (73%)

diff --git a/app/Libraries/MediaClipper/Config/MediaClipper.php b/app/Libraries/MediaClipper/Config/MediaClipper.php
index 52702da64b..c457185199 100644
--- a/app/Libraries/MediaClipper/Config/MediaClipper.php
+++ b/app/Libraries/MediaClipper/Config/MediaClipper.php
@@ -203,4 +203,24 @@ class MediaClipper extends BaseConfig
             ],
         ],
     ];
+
+    /**
+     * @var array<string, array<string, string|int[]>>
+     */
+    public array $themes = [
+        'pine' => [
+            'background' => [0, 86, 74],
+            'text' => [255, 255, 255],
+            // subtitle hex color is BGR (Blue, Green, Red),
+            'subtitles' => 'FFFFFF',
+            // quotes image MUST BE black
+            'quotes' => [0, 148, 134],
+            'episodeNumberingBg' => [0, 61, 11],
+            'episodeNumberingText' => [255, 255, 255],
+            'progressbar' => '009486',
+            'timestampBg' => '00564A',
+            'timestampText' => 'FFFFFF',
+            'soundwaves' => 'F2FAF9',
+        ],
+    ];
 }
diff --git a/app/Libraries/MediaClipper/VideoClip.php b/app/Libraries/MediaClipper/VideoClip.php
index 9be8e42f37..909bd13e82 100644
--- a/app/Libraries/MediaClipper/VideoClip.php
+++ b/app/Libraries/MediaClipper/VideoClip.php
@@ -44,32 +44,46 @@ class VideoClip
 
     protected ?string $episodeNumbering = null;
 
+    /**
+     * @var 'landscape'|'portrait'|'squared'
+     */
+    protected string $format = 'landscape';
+
     /**
      * @var array<string, mixed>
      */
     protected array $dimensions = [];
 
     /**
-     * @var 'landscape'|'portrait'|'squared'
+     * @var 'pine'|'crimson'|'lake'|'amber'|'jacaranda'|'onyx'
      */
-    protected string $format = 'landscape';
+    protected string $theme = 'pine';
+
+    /**
+     * @var array<string, mixed>
+     */
+    protected array $colors = [];
 
     /**
      * @param 'landscape'|'portrait'|'squared' $format
+     * @param 'pine'|'crimson'|'lake'|'amber'|'jacaranda'|'onyx' $theme
      */
     public function __construct(
         protected Episode $episode,
         protected float $start,
         protected float $end,
         string $format,
+        string $theme,
     ) {
         $this->duration = $end - $start;
         $this->format = $format;
         $this->episodeNumbering = $this->episodeNumbering($this->episode->number, $this->episode->season_number);
         $this->dimensions = config('MediaClipper')
             ->formats[$format];
+        $this->colors = config('MediaClipper')
+            ->themes[$theme];
 
-        helper('media');
+        helper(['media']);
 
         $this->audioInput = media_path($this->episode->audio_file_path);
         $this->episodeCoverPath = media_path($this->episode->cover->path);
@@ -118,7 +132,7 @@ 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=white,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=0xFFFFFF,format=yuva420p[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]',
@@ -128,11 +142,11 @@ class VideoClip
             "[waves_t3]scale={$this->dimensions['soundwaves']['rescaleWidth']}:{$this->dimensions['soundwaves']['rescaleHeight']}[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=white:box=1:boxcolor=0x00564A:boxborderw={$this->dimensions['timestamp']['padding']}[v3]",
-            "color=c=0x009486:s={$this->dimensions['width']}x{$this->dimensions['progressbar']['height']}[progressbar]",
+            ) . ":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]",
+            "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']},BorderStyle=1,Outline=0,Shadow=0,MarginL={$this->dimensions['subtitles']['marginL']},MarginR={$this->dimensions['subtitles']['marginR']},MarginV={$this->dimensions['subtitles']['marginV']}'[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']}',format=yuv420p,colormatrix=bt601:bt2020[outv]",
         ];
 
         $videoClipCmd = [
@@ -142,12 +156,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=white:{$this->dimensions['width']}x{$this->dimensions['height']}",
+            "-f lavfi -i color=0x{$this->colors['soundwaves']}:{$this->dimensions['width']}x{$this->dimensions['height']}",
             '-filter_complex "' . implode(';', $filters) . '"',
             '-map "[outv]"',
             '-map 0:a',
             '-acodec copy',
             '-vcodec libx264',
+            '-pix_fmt yuv420p',
             "{$this->videoClipOutput}",
         ];
 
@@ -184,7 +199,7 @@ class VideoClip
 
     private function generateVideoClipBg(): bool
     {
-        $background = $this->generateColouredBg($this->dimensions['width'], $this->dimensions['height']);
+        $background = $this->generateBackground($this->dimensions['width'], $this->dimensions['height']);
 
         if ($background === null) {
             return false;
@@ -265,6 +280,8 @@ class VideoClip
                 $this->episodeNumbering,
                 $this->getFont('episodeNumbering'),
                 $this->dimensions['episodeNumbering']['fontsize'],
+                $this->colors['episodeNumberingText'],
+                $this->colors['episodeNumberingBg'],
                 $this->dimensions['episodeNumbering']['paddingX'],
                 $this->dimensions['episodeNumbering']['paddingY'],
             );
@@ -289,6 +306,8 @@ class VideoClip
             return false;
         }
 
+        imagefilter($quotes, IMG_FILTER_COLORIZE, ...$this->colors['quotes']);
+
         $scaledQuotes = $this->scaleImage(
             $quotes,
             $this->dimensions['quotes']['width'],
@@ -319,7 +338,7 @@ class VideoClip
         return config('MediaClipper')->fontsFolder . self::FONTS[$name];
     }
 
-    private function generateColouredBg(int $width, int $height): ?GdImage
+    private function generateBackground(int $width, int $height): ?GdImage
     {
         $background = imagecreatetruecolor($width, $height);
 
@@ -327,7 +346,7 @@ class VideoClip
             return null;
         }
 
-        $coloredBackground = imagecolorallocate($background, 0, 86, 74);
+        $coloredBackground = imagecolorallocate($background, ...$this->colors['background']);
 
         if ($coloredBackground === false) {
             return null;
@@ -450,9 +469,9 @@ class VideoClip
         int $paragraphIndent = 0,
     ): bool {
         // Allocate A Color For The Text
-        $white = imagecolorallocate($image, 255, 255, 255);
+        $textColor = imagecolorallocate($image, ...$this->colors['text']);
 
-        if ($white === false) {
+        if ($textColor === false) {
             return false;
         }
 
@@ -470,7 +489,7 @@ class VideoClip
                 0,
                 $x + ($paragraphIndent * ($i === 0 ? 1 : 0)),
                 $y + $fontsize + ($leading * $i),
-                $white,
+                $textColor,
                 $fontPath,
                 $line
             );
@@ -510,6 +529,10 @@ class VideoClip
         ];
     }
 
+    /**
+     * @param int[] $boxTextColor
+     * @param int[] $boxBgColor
+     */
     private function addTextWithBox(
         GdImage $image,
         int $x,
@@ -517,14 +540,16 @@ class VideoClip
         string $text,
         string $fontPath,
         int $fontsize,
+        array $boxTextColor,
+        array $boxBgColor,
         int $paddingX = 0,
         int $paddingY = 0,
     ): bool {
         // Create some colors
-        $white = imagecolorallocate($image, 255, 255, 255);
-        $bgColor = imagecolorallocate($image, 0, 61, 11);
+        $textColor = imagecolorallocate($image, ...$boxTextColor);
+        $bgColor = imagecolorallocate($image, ...$boxBgColor);
 
-        if ($white === false || $bgColor === false) {
+        if ($textColor === false || $bgColor === false) {
             return false;
         }
 
@@ -540,7 +565,7 @@ class VideoClip
         $y2 = $y + $bbox['height'] + ($paddingY * 2);
 
         imagefilledrectangle($image, $x, $y, $x2, $y2, $bgColor);
-        imagettftext($image, $fontsize, 0, $x1, $y1, $white, $fontPath, $text);
+        imagettftext($image, $fontsize, 0, $x1, $y1, $textColor, $fontPath, $text);
 
         return true;
     }
diff --git a/app/Libraries/MediaClipper/quotes.png b/app/Libraries/MediaClipper/quotes.png
index 2c65242817398e386b3c512453968dc34e2db5ec..41f22cacf9d154f9629ab61c9f60fe516fcd4367 100644
GIT binary patch
delta 2615
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

delta 4050
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
W`BrwH$`J+z1_n=8KbLh*2~7Y}yYdSF

diff --git a/app/Resources/icons/clapperboard.svg b/app/Resources/icons/clapperboard.svg
new file mode 100644
index 0000000000..c5d7d1215e
--- /dev/null
+++ b/app/Resources/icons/clapperboard.svg
@@ -0,0 +1,6 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
+    <g>
+        <path fill="none" d="M0 0h24v24H0z"/>
+        <path d="M17.998 7l2.31-4h.7c.548 0 .992.445.992.993v16.014a1 1 0 0 1-.992.993H2.992A.993.993 0 0 1 2 20.007V3.993A1 1 0 0 1 2.992 3h3.006l-2.31 4h2.31l2.31-4h3.69l-2.31 4h2.31l2.31-4h3.69l-2.31 4h2.31z"/>
+    </g>
+</svg>
diff --git a/modules/Admin/Config/Routes.php b/modules/Admin/Config/Routes.php
index 469859db1a..59233f6198 100644
--- a/modules/Admin/Config/Routes.php
+++ b/modules/Admin/Config/Routes.php
@@ -352,15 +352,23 @@ $routes->group(
                         );
                         $routes->get(
                             'video-clips',
-                            'ClipsController::videoClips/$1/$2',
+                            'VideoClipsController::list/$1/$2',
                             [
-                                'as' => 'video-clips',
+                                'as' => 'video-clips-list',
+                                'filter' => 'permission:podcast_episodes-edit',
+                            ],
+                        );
+                        $routes->get(
+                            'video-clips/new',
+                            'VideoClipsController::generate/$1/$2',
+                            [
+                                'as' => 'video-clips-generate',
                                 'filter' => 'permission:podcast_episodes-edit',
                             ],
                         );
                         $routes->post(
-                            'video-clips',
-                            'ClipsController::generateVideoClip/$1/$2',
+                            'video-clips/new',
+                            'VideoClipsController::attemptGenerate/$1/$2',
                             [
                                 'as' => 'video-clips-generate',
                                 'filter' => 'permission:podcast_episodes-edit',
diff --git a/modules/Admin/Controllers/ClipsController.php b/modules/Admin/Controllers/VideoClipsController.php
similarity index 71%
rename from modules/Admin/Controllers/ClipsController.php
rename to modules/Admin/Controllers/VideoClipsController.php
index cfb9eb4d2d..3520e2862d 100644
--- a/modules/Admin/Controllers/ClipsController.php
+++ b/modules/Admin/Controllers/VideoClipsController.php
@@ -18,7 +18,7 @@ use CodeIgniter\Exceptions\PageNotFoundException;
 use CodeIgniter\HTTP\RedirectResponse;
 use MediaClipper\VideoClip;
 
-class ClipsController extends BaseController
+class VideoClipsController extends BaseController
 {
     protected Podcast $podcast;
 
@@ -55,7 +55,21 @@ class ClipsController extends BaseController
         return $this->{$method}(...$params);
     }
 
-    public function videoClips(): string
+    public function list(): string
+    {
+        $data = [
+            'podcast' => $this->podcast,
+            'episode' => $this->episode,
+        ];
+
+        replace_breadcrumb_params([
+            0 => $this->podcast->title,
+            1 => $this->episode->title,
+        ]);
+        return view('episode/video_clips_list', $data);
+    }
+
+    public function generate(): string
     {
         helper('form');
 
@@ -66,18 +80,19 @@ class ClipsController extends BaseController
 
         replace_breadcrumb_params([
             0 => $this->podcast->title,
-            1 => $this->episode->slug,
+            1 => $this->episode->title,
         ]);
-        return view('episode/video_clips', $data);
+        return view('episode/video_clips_new', $data);
     }
 
-    public function generateVideoClip(): RedirectResponse
+    public function attemptGenerate(): RedirectResponse
     {
         // TODO: add end_time greater than start_time, with minimum ?
         $rules = [
-            'format' => 'required|in_list[landscape,portrait,squared]',
             'start_time' => 'required|numeric',
             'end_time' => 'required|numeric|differs[start_time]',
+            'format' => 'required|in_list[' . implode(',', array_keys(config('MediaClipper')->formats)) . ']',
+            'theme' => 'required|in_list[' . implode(',', array_keys(config('Colors')->themes)) . ']',
         ];
 
         if (! $this->validate($rules)) {
@@ -92,10 +107,11 @@ class ClipsController extends BaseController
             (float) $this->request->getPost('start_time'),
             (float) $this->request->getPost('end_time',),
             $this->request->getPost('format'),
+            $this->request->getPost('theme'),
         );
         $clipper->generate();
 
-        return redirect()->route('video-clips', [$this->podcast->id, $this->episode->id])->with(
+        return redirect()->route('video-clips-generate', [$this->podcast->id, $this->episode->id])->with(
             'message',
             lang('Settings.images.regenerationSuccess')
         );
diff --git a/modules/Admin/Language/en/EpisodeNavigation.php b/modules/Admin/Language/en/EpisodeNavigation.php
index c7f7dba3ea..8e2df5beb4 100644
--- a/modules/Admin/Language/en/EpisodeNavigation.php
+++ b/modules/Admin/Language/en/EpisodeNavigation.php
@@ -15,6 +15,8 @@ return [
     'episode-edit' => 'Edit episode',
     'episode-persons-manage' => 'Manage persons',
     'embed-add' => 'Embeddable player',
+    'clips' => 'Clips',
     'soundbites-edit' => 'Soundbites',
-    'video-clips' => 'Video clips',
+    'video-clips-list' => 'Video clips',
+    'video-clips-generate' => 'New video clip',
 ];
diff --git a/modules/Admin/Language/fr/EpisodeNavigation.php b/modules/Admin/Language/fr/EpisodeNavigation.php
index d85c74d2c1..288b9203dd 100644
--- a/modules/Admin/Language/fr/EpisodeNavigation.php
+++ b/modules/Admin/Language/fr/EpisodeNavigation.php
@@ -15,5 +15,8 @@ return [
     'episode-edit' => 'Modifier l’épisode',
     'episode-persons-manage' => 'Gestion des intervenants',
     'embed' => 'Lecteur intégré',
+    'clips' => 'Extraits',
     'soundbites-edit' => 'Extraits sonores',
+    'video-clips-list' => 'Extraits video',
+    'video-clips-generate' => 'Nouvel extrait video',
 ];
diff --git a/themes/cp_admin/episode/_sidebar.php b/themes/cp_admin/episode/_sidebar.php
index 32332db025..9fbcf49333 100644
--- a/themes/cp_admin/episode/_sidebar.php
+++ b/themes/cp_admin/episode/_sidebar.php
@@ -3,7 +3,11 @@
 $podcastNavigation = [
     'dashboard' => [
         'icon' => 'dashboard',
-        'items' => ['episode-view', 'episode-edit', 'episode-persons-manage', 'embed-add', 'soundbites-edit', 'video-clips'],
+        'items' => ['episode-view', 'episode-edit', 'episode-persons-manage', 'embed-add'],
+    ],
+    'clips' => [
+        'icon' => 'clapperboard',
+        'items' => ['video-clips-list', 'video-clips-generate', 'soundbites-edit'],
     ],
 ]; ?>
 
diff --git a/themes/cp_admin/episode/video_clips_list.php b/themes/cp_admin/episode/video_clips_list.php
new file mode 100644
index 0000000000..f357a62267
--- /dev/null
+++ b/themes/cp_admin/episode/video_clips_list.php
@@ -0,0 +1,13 @@
+<?= $this->extend('_layout') ?>
+
+<?= $this->section('title') ?>
+<?= lang('Episode.video_clips.title') ?>
+<?= $this->endSection() ?>
+
+<?= $this->section('pageTitle') ?>
+<?= lang('Episode.video_clips.title') ?>
+<?= $this->endSection() ?>
+
+<?= $this->section('content') ?>
+
+<?= $this->endSection() ?>
diff --git a/themes/cp_admin/episode/video_clips.php b/themes/cp_admin/episode/video_clips_new.php
similarity index 73%
rename from themes/cp_admin/episode/video_clips.php
rename to themes/cp_admin/episode/video_clips_new.php
index 314b060cbe..abcb48b22a 100644
--- a/themes/cp_admin/episode/video_clips.php
+++ b/themes/cp_admin/episode/video_clips_new.php
@@ -28,23 +28,31 @@
 </div>
 </fieldset>
 
+<div class="grid gap-4 grid-cols-colorButtons">
+    <?php foreach (config('Colors')->themes as $themeName => $color): ?>
+        <Forms.ColorRadioButton
+        class="theme-<?= $themeName ?> mx-auto"
+        value="<?= $themeName ?>"
+        name="theme"
+        isChecked="<?= $themeName === 'pine' ? 'true' : 'false' ?>" ><?= lang('Settings.theme.' . $themeName) ?></Forms.ColorRadioButton>
+    <?php endforeach; ?>
+</div>
+
 <Forms.Field
     type="number"
     name="start_time"
     label="START"
     required="true"
-    value="0"
+    value="5"
 />
 <Forms.Field
     type="number"
     name="end_time"
     label="END"
     required="true"
-    value="15"
+    value="10"
 />
 
-<audio></audio>
-
 <Button variant="primary" type="submit"><?= lang('Episode.video_clips.submit') ?></Button>
 
 </form>
-- 
GitLab