mirror of
https://code.castopod.org/adaures/castopod
synced 2025-04-19 13:01:19 +00:00
feat(video-clips): add route for scheduled video clips + list video clips with status
This commit is contained in:
parent
2f6fdf9091
commit
2065ebbee5
@ -85,7 +85,6 @@ class AddClips extends Migration
|
||||
]);
|
||||
|
||||
$this->forge->addKey('id', true);
|
||||
$this->forge->addUniqueKey(['episode_id', 'start_time', 'duration', 'type']);
|
||||
$this->forge->addForeignKey('podcast_id', 'podcasts', 'id', '', 'CASCADE');
|
||||
$this->forge->addForeignKey('episode_id', 'episodes', 'id', '', 'CASCADE');
|
||||
$this->forge->addForeignKey('media_id', 'media', 'id', '', 'CASCADE');
|
||||
|
@ -17,8 +17,10 @@ use App\Entities\Podcast;
|
||||
use App\Models\EpisodeModel;
|
||||
use App\Models\MediaModel;
|
||||
use App\Models\PodcastModel;
|
||||
use App\Models\UserModel;
|
||||
use CodeIgniter\Entity\Entity;
|
||||
use CodeIgniter\Files\File;
|
||||
use Modules\Auth\Entities\User;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
@ -33,8 +35,10 @@ use CodeIgniter\Files\File;
|
||||
* @property string $type
|
||||
* @property int $media_id
|
||||
* @property Video|Audio $media
|
||||
* @property array|null $metadata
|
||||
* @property string $status
|
||||
* @property string $logs
|
||||
* @property User $user
|
||||
* @property int $created_by
|
||||
* @property int $updated_by
|
||||
*/
|
||||
@ -52,14 +56,17 @@ class BaseClip extends Entity
|
||||
'duration' => 'double',
|
||||
'type' => 'string',
|
||||
'media_id' => '?integer',
|
||||
'metadata' => 'json-array',
|
||||
'metadata' => '?json-array',
|
||||
'status' => 'string',
|
||||
'logs' => 'string',
|
||||
'created_by' => 'integer',
|
||||
'updated_by' => 'integer',
|
||||
];
|
||||
|
||||
public function __construct($data)
|
||||
/**
|
||||
* @param array<string, mixed>|null $data
|
||||
*/
|
||||
public function __construct(array $data = null)
|
||||
{
|
||||
parent::__construct($data);
|
||||
|
||||
@ -80,13 +87,20 @@ class BaseClip extends Entity
|
||||
return (new EpisodeModel())->getEpisodeById($this->episode_id);
|
||||
}
|
||||
|
||||
public function getUser(): ?User
|
||||
{
|
||||
return (new UserModel())->find($this->created_by);
|
||||
}
|
||||
|
||||
public function setMedia(string $filePath = null): static
|
||||
{
|
||||
if ($filePath === null || ($file = new File($filePath)) === null) {
|
||||
if ($filePath === null) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
if ($this->media_id !== 0) {
|
||||
$file = new File($filePath);
|
||||
|
||||
if ($this->media_id !== null) {
|
||||
$this->getMedia()
|
||||
->setFile($file);
|
||||
$this->getMedia()
|
||||
@ -97,8 +111,8 @@ class BaseClip extends Entity
|
||||
'file_path' => $filePath,
|
||||
'language_code' => $this->getPodcast()
|
||||
->language_code,
|
||||
'uploaded_by' => user_id(),
|
||||
'updated_by' => user_id(),
|
||||
'uploaded_by' => $this->attributes['created_by'],
|
||||
'updated_by' => $this->attributes['created_by'],
|
||||
]);
|
||||
$media->setFile($file);
|
||||
|
||||
|
@ -10,20 +10,78 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Entities\Clip;
|
||||
|
||||
use App\Entities\Media\Video;
|
||||
use App\Models\MediaModel;
|
||||
use CodeIgniter\Files\File;
|
||||
|
||||
/**
|
||||
* @property string $theme
|
||||
* @property array $theme
|
||||
* @property string $format
|
||||
*/
|
||||
class VideoClip extends BaseClip
|
||||
{
|
||||
protected string $type = 'video';
|
||||
|
||||
/**
|
||||
* @param array<string, mixed>|null $data
|
||||
*/
|
||||
public function __construct(array $data = null)
|
||||
{
|
||||
parent::__construct($data);
|
||||
|
||||
if ($this->metadata !== null) {
|
||||
if ($this->metadata !== null && $this->metadata !== []) {
|
||||
$this->theme = $this->metadata['theme'];
|
||||
$this->format = $this->metadata['format'];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $theme
|
||||
*/
|
||||
public function setTheme(array $theme): self
|
||||
{
|
||||
// TODO: change?
|
||||
$this->attributes['metadata'] = json_decode($this->attributes['metadata'] ?? '[]', true);
|
||||
|
||||
$this->attributes['theme'] = $theme;
|
||||
$this->attributes['metadata']['theme'] = $theme;
|
||||
|
||||
$this->attributes['metadata'] = json_encode($this->attributes['metadata']);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setFormat(string $format): self
|
||||
{
|
||||
$this->attributes['metadata'] = json_decode($this->attributes['metadata'], true);
|
||||
|
||||
$this->attributes['format'] = $format;
|
||||
$this->attributes['metadata']['format'] = $format;
|
||||
|
||||
$this->attributes['metadata'] = json_encode($this->attributes['metadata']);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setMedia(string $filePath = null): static
|
||||
{
|
||||
if ($filePath === null) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$file = new File($filePath);
|
||||
|
||||
$video = new Video([
|
||||
'file_path' => $filePath,
|
||||
'language_code' => $this->getPodcast()
|
||||
->language_code,
|
||||
'uploaded_by' => $this->attributes['created_by'],
|
||||
'updated_by' => $this->attributes['created_by'],
|
||||
]);
|
||||
$video->setFile($file);
|
||||
|
||||
$this->attributes['media_id'] = (new MediaModel())->saveMedia($video);
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Entities;
|
||||
|
||||
use App\Entities\Clip\BaseClip;
|
||||
use App\Entities\Clip\Soundbite;
|
||||
use App\Entities\Media\Audio;
|
||||
use App\Entities\Media\Chapters;
|
||||
use App\Entities\Media\Image;
|
||||
@ -75,7 +75,7 @@ use RuntimeException;
|
||||
* @property Time|null $deleted_at;
|
||||
*
|
||||
* @property Person[] $persons;
|
||||
* @property Soundbites[] $soundbites;
|
||||
* @property Soundbite[] $soundbites;
|
||||
* @property string $embed_url;
|
||||
*/
|
||||
class Episode extends Entity
|
||||
@ -110,7 +110,7 @@ class Episode extends Entity
|
||||
protected ?array $persons = null;
|
||||
|
||||
/**
|
||||
* @var Soundbites[]|null
|
||||
* @var Soundbite[]|null
|
||||
*/
|
||||
protected ?array $soundbites = null;
|
||||
|
||||
@ -407,7 +407,7 @@ class Episode extends Entity
|
||||
/**
|
||||
* Returns the episode’s clips
|
||||
*
|
||||
* @return BaseClip[]|\App\Entities\Soundbites[]
|
||||
* @return Soundbite[]
|
||||
*/
|
||||
public function getSoundbites(): array
|
||||
{
|
||||
|
@ -31,6 +31,12 @@ class VideoClipper
|
||||
'timestamp' => 'NotoSansMono-Regular.ttf',
|
||||
];
|
||||
|
||||
public ?string $logs = null;
|
||||
|
||||
public bool $error = false;
|
||||
|
||||
public string $videoClipOutput;
|
||||
|
||||
protected float $duration;
|
||||
|
||||
protected string $audioInput;
|
||||
@ -45,8 +51,6 @@ class VideoClipper
|
||||
|
||||
protected string $videoClipBgOutput;
|
||||
|
||||
protected string $videoClipOutput;
|
||||
|
||||
protected ?string $episodeNumbering = null;
|
||||
|
||||
/**
|
||||
@ -107,7 +111,10 @@ class VideoClipper
|
||||
}
|
||||
}
|
||||
|
||||
public function generate(): string
|
||||
/**
|
||||
* @return int 0 for success, else error
|
||||
*/
|
||||
public function generate(): int
|
||||
{
|
||||
$this->soundbite();
|
||||
$this->subtitlesClip();
|
||||
@ -119,7 +126,7 @@ class VideoClipper
|
||||
|
||||
$generateCmd = $this->getCmd();
|
||||
|
||||
return shell_exec($generateCmd . ' 2>&1');
|
||||
return $this->cmd_exec($generateCmd);
|
||||
}
|
||||
|
||||
public function getCmd(): string
|
||||
@ -205,7 +212,7 @@ class VideoClipper
|
||||
return false;
|
||||
}
|
||||
|
||||
$episodeCover = imagecreatefromjpeg($this->episodeCoverPath);
|
||||
$episodeCover = $this->createCoverImage();
|
||||
if (! $episodeCover) {
|
||||
return false;
|
||||
}
|
||||
@ -340,6 +347,41 @@ class VideoClipper
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int 0 (success), 1 - 2 - 254 - 255 (error)
|
||||
*/
|
||||
private function cmd_exec(string $cmd): int
|
||||
{
|
||||
$outFile = tempnam(WRITEPATH . 'logs', 'cmd-out-');
|
||||
|
||||
if (! $outFile) {
|
||||
return 254;
|
||||
}
|
||||
|
||||
$descriptorSpec = [
|
||||
0 => ['pipe', 'r'],
|
||||
1 => ['pipe', 'w'],
|
||||
2 => ['file', $outFile, 'w'],
|
||||
// FFmpeg outputs to stderr by default
|
||||
];
|
||||
$proc = proc_open($cmd, $descriptorSpec, $pipes);
|
||||
|
||||
if (! is_resource($proc)) {
|
||||
return 255;
|
||||
}
|
||||
|
||||
fclose($pipes[0]); //Don't really want to give any input
|
||||
|
||||
$exit = proc_close($proc);
|
||||
|
||||
$this->logs = (string) file_get_contents($outFile);
|
||||
|
||||
// remove temporary files
|
||||
unlink($outFile);
|
||||
|
||||
return $exit;
|
||||
}
|
||||
|
||||
private function getFont(string $name): string
|
||||
{
|
||||
return config('MediaClipper')->fontsFolder . self::FONTS[$name];
|
||||
@ -364,6 +406,15 @@ class VideoClipper
|
||||
return $background;
|
||||
}
|
||||
|
||||
private function createCoverImage(): GdImage | false
|
||||
{
|
||||
return match ($this->episode->cover->file_mimetype) {
|
||||
'image/jpeg' => imagecreatefromjpeg($this->episodeCoverPath),
|
||||
'image/png' => imagecreatefrompng($this->episodeCoverPath),
|
||||
default => imagecreate(1400, 1400),
|
||||
};
|
||||
}
|
||||
|
||||
private function scaleImage(GdImage $image, int $width, int $height): GdImage | false
|
||||
{
|
||||
return imagescale($image, $width, $height);
|
||||
|
@ -44,6 +44,7 @@ class ClipModel extends Model
|
||||
'duration',
|
||||
'type',
|
||||
'media_id',
|
||||
'metadata',
|
||||
'status',
|
||||
'logs',
|
||||
'created_by',
|
||||
@ -65,21 +66,6 @@ class ClipModel extends Model
|
||||
*/
|
||||
protected $useTimestamps = true;
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
protected $afterInsert = ['clearCache'];
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
protected $afterUpdate = ['clearCache'];
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
protected $beforeDelete = ['clearCache'];
|
||||
|
||||
public function __construct(
|
||||
protected string $type = 'audio',
|
||||
ConnectionInterface &$db = null,
|
||||
@ -104,7 +90,7 @@ class ClipModel extends Model
|
||||
/**
|
||||
* Gets all clips for an episode
|
||||
*
|
||||
* @return BaseClip[]
|
||||
* @return Soundbite[]
|
||||
*/
|
||||
public function getEpisodeSoundbites(int $podcastId, int $episodeId): array
|
||||
{
|
||||
@ -155,6 +141,27 @@ class ClipModel extends Model
|
||||
return $found;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets scheduled video clips for an episode
|
||||
*
|
||||
* @return VideoClip[]
|
||||
*/
|
||||
public function getScheduledVideoClips(): array
|
||||
{
|
||||
$found = $this->where([
|
||||
'type' => 'video',
|
||||
'status' => 'queued',
|
||||
])
|
||||
->orderBy('created_at')
|
||||
->findAll();
|
||||
|
||||
foreach ($found as $key => $videoClip) {
|
||||
$found[$key] = new VideoClip($videoClip->toArray());
|
||||
}
|
||||
|
||||
return $found;
|
||||
}
|
||||
|
||||
public function deleteSoundbite(int $podcastId, int $episodeId, int $clipId): BaseResult | bool
|
||||
{
|
||||
cache()
|
||||
@ -167,25 +174,6 @@ class ClipModel extends Model
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, array<string|int, mixed>> $data
|
||||
* @return array<string, array<string|int, mixed>>
|
||||
*/
|
||||
public function clearCache(array $data): array
|
||||
{
|
||||
$episode = (new EpisodeModel())->find(
|
||||
isset($data['data'])
|
||||
? $data['data']['episode_id']
|
||||
: $data['id']['episode_id'],
|
||||
);
|
||||
|
||||
// delete cache for rss feed
|
||||
cache()
|
||||
->deleteMatching("podcast#{$episode->podcast_id}_feed*");
|
||||
|
||||
cache()
|
||||
->deleteMatching("page_podcast#{$episode->podcast_id}_episode#{$episode->id}_*");
|
||||
|
||||
return $data;
|
||||
}
|
||||
// cache()
|
||||
// ->deleteMatching("page_podcast#{$clip->podcast_id}_episode#{$clip->episode_id}_*");
|
||||
}
|
||||
|
6
app/Resources/icons/forbid.svg
Normal file
6
app/Resources/icons/forbid.svg
Normal file
@ -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="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zM8.523 7.109A6.04 6.04 0 0 0 7.11 8.523l8.368 8.368a6.04 6.04 0 0 0 1.414-1.414L8.523 7.109z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 317 B |
36
app/Views/Components/Pill.php
Normal file
36
app/Views/Components/Pill.php
Normal file
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Views\Components;
|
||||
|
||||
use ViewComponents\Component;
|
||||
|
||||
class Pill extends Component
|
||||
{
|
||||
/**
|
||||
* @var 'small'|'base'
|
||||
*/
|
||||
public string $size = 'base';
|
||||
|
||||
public string $variant = 'default';
|
||||
|
||||
public ?string $icon = null;
|
||||
|
||||
public function render(): string
|
||||
{
|
||||
$variantClasses = [
|
||||
'default' => 'text-gray-800 bg-gray-100 border-gray-300',
|
||||
'primary' => 'text-accent-contrast bg-accent-base border-accent-base',
|
||||
'success' => 'text-pine-900 bg-pine-100 border-castopod-300',
|
||||
'danger' => 'text-red-900 bg-red-100 border-red-300',
|
||||
'warning' => 'text-yellow-900 bg-yellow-100 border-yellow-300',
|
||||
];
|
||||
|
||||
$icon = $this->icon ? icon($this->icon) : '';
|
||||
|
||||
return <<<HTML
|
||||
<span class="inline-flex items-center gap-x-1 px-1 font-semibold border rounded {$variantClasses[$this->variant]}">{$icon}{$this->slot}</span>
|
||||
HTML;
|
||||
}
|
||||
}
|
@ -6,6 +6,11 @@ namespace Modules\Admin\Config;
|
||||
|
||||
$routes = service('routes');
|
||||
|
||||
// video-clips scheduler
|
||||
$routes->add('scheduled-video-clips', 'SchedulerController::generateVideoClips', [
|
||||
'namespace' => 'Modules\Admin\Controllers',
|
||||
]);
|
||||
|
||||
// Admin area routes
|
||||
$routes->group(
|
||||
config('Admin')
|
||||
|
73
modules/Admin/Controllers/SchedulerController.php
Normal file
73
modules/Admin/Controllers/SchedulerController.php
Normal file
@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @copyright 2021 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
namespace Modules\Admin\Controllers;
|
||||
|
||||
use App\Models\ClipModel;
|
||||
use CodeIgniter\Controller;
|
||||
use MediaClipper\VideoClipper;
|
||||
|
||||
class SchedulerController extends Controller
|
||||
{
|
||||
public function generateVideoClips(): bool
|
||||
{
|
||||
// get all clips that haven't been processed yet
|
||||
$scheduledClips = (new ClipModel())->getScheduledVideoClips();
|
||||
|
||||
if ($scheduledClips === []) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$data = [];
|
||||
foreach ($scheduledClips as $scheduledClip) {
|
||||
$data[] = [
|
||||
'id' => $scheduledClip->id,
|
||||
'status' => 'pending',
|
||||
];
|
||||
}
|
||||
|
||||
(new ClipModel())->updateBatch($data, 'id');
|
||||
|
||||
// Loop through clips to generate them
|
||||
foreach ($scheduledClips as $scheduledClip) {
|
||||
// set clip to pending
|
||||
(new ClipModel())
|
||||
->update($scheduledClip->id, [
|
||||
'status' => 'running',
|
||||
]);
|
||||
$clipper = new VideoClipper(
|
||||
$scheduledClip->episode,
|
||||
$scheduledClip->start_time,
|
||||
$scheduledClip->end_time,
|
||||
$scheduledClip->format,
|
||||
$scheduledClip->theme['name'],
|
||||
);
|
||||
$exitCode = $clipper->generate();
|
||||
|
||||
if ($exitCode === 0) {
|
||||
// success, video was generated
|
||||
$scheduledClip->setMedia($clipper->videoClipOutput);
|
||||
(new ClipModel())->update($scheduledClip->id, [
|
||||
'media_id' => $scheduledClip->media_id,
|
||||
'status' => 'passed',
|
||||
'logs' => $clipper->logs,
|
||||
]);
|
||||
} else {
|
||||
// error
|
||||
(new ClipModel())->update($scheduledClip->id, [
|
||||
'status' => 'failed',
|
||||
'logs' => $clipper->logs,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
@ -10,7 +10,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace Modules\Admin\Controllers;
|
||||
|
||||
use App\Entities\Clip;
|
||||
use App\Entities\Clip\VideoClip;
|
||||
use App\Entities\Episode;
|
||||
use App\Entities\Podcast;
|
||||
@ -19,7 +18,6 @@ use App\Models\EpisodeModel;
|
||||
use App\Models\PodcastModel;
|
||||
use CodeIgniter\Exceptions\PageNotFoundException;
|
||||
use CodeIgniter\HTTP\RedirectResponse;
|
||||
use MediaClipper\VideoClipper;
|
||||
|
||||
class VideoClipsController extends BaseController
|
||||
{
|
||||
@ -60,7 +58,7 @@ class VideoClipsController extends BaseController
|
||||
|
||||
public function list(): string
|
||||
{
|
||||
$videoClips = (new ClipModel('video'))
|
||||
$videoClipsBuilder = (new ClipModel('video'))
|
||||
->where([
|
||||
'podcast_id' => $this->podcast->id,
|
||||
'episode_id' => $this->episode->id,
|
||||
@ -68,11 +66,18 @@ class VideoClipsController extends BaseController
|
||||
])
|
||||
->orderBy('created_at', 'desc');
|
||||
|
||||
$clips = $videoClipsBuilder->paginate(10);
|
||||
|
||||
$videoClips = [];
|
||||
foreach ($clips as $clip) {
|
||||
$videoClips[] = new VideoClip($clip->toArray());
|
||||
}
|
||||
|
||||
$data = [
|
||||
'podcast' => $this->podcast,
|
||||
'episode' => $this->episode,
|
||||
'videoClips' => $videoClips->paginate(10),
|
||||
'pager' => $videoClips->pager,
|
||||
'videoClips' => $videoClips,
|
||||
'pager' => $videoClipsBuilder->pager,
|
||||
];
|
||||
|
||||
replace_breadcrumb_params([
|
||||
@ -115,10 +120,20 @@ class VideoClipsController extends BaseController
|
||||
->with('errors', $this->validator->getErrors());
|
||||
}
|
||||
|
||||
$themeName = $this->request->getPost('theme');
|
||||
$themeColors = config('MediaClipper')
|
||||
->themes[$themeName];
|
||||
$theme = [
|
||||
'name' => $themeName,
|
||||
'preview' => $themeColors['preview'],
|
||||
];
|
||||
|
||||
$videoClip = new VideoClip([
|
||||
'label' => 'NEW CLIP',
|
||||
'start_time' => (float) $this->request->getPost('start_time'),
|
||||
'end_time' => (float) $this->request->getPost('end_time',),
|
||||
'theme' => $theme,
|
||||
'format' => $this->request->getPost('format'),
|
||||
'type' => 'video',
|
||||
'status' => 'queued',
|
||||
'podcast_id' => $this->podcast->id,
|
||||
@ -134,39 +149,4 @@ class VideoClipsController extends BaseController
|
||||
lang('Settings.images.regenerationSuccess')
|
||||
);
|
||||
}
|
||||
|
||||
public function scheduleClips(): void
|
||||
{
|
||||
// get all clips that haven't been generated
|
||||
$scheduledClips = (new ClipModel())->getScheduledVideoClips();
|
||||
|
||||
foreach ($scheduledClips as $scheduledClip) {
|
||||
$scheduledClip->status = 'pending';
|
||||
}
|
||||
|
||||
(new ClipModel())->updateBatch($scheduledClips);
|
||||
|
||||
// Loop through clips to generate them
|
||||
foreach ($scheduledClips as $scheduledClip) {
|
||||
// set clip to pending
|
||||
(new ClipModel())
|
||||
->update($scheduledClip->id, [
|
||||
'status' => 'running',
|
||||
]);
|
||||
$clipper = new VideoClipper(
|
||||
$scheduledClip->episode,
|
||||
$scheduledClip->start_time,
|
||||
$scheduledClip->end_time,
|
||||
$scheduledClip->format,
|
||||
$scheduledClip->theme,
|
||||
);
|
||||
$output = $clipper->generate();
|
||||
$scheduledClip->setMedia($clipper->videoClipOutput);
|
||||
|
||||
(new ClipModel())->update($scheduledClip->id, [
|
||||
'status' => 'passed',
|
||||
'logs' => $output,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -12,15 +12,50 @@
|
||||
<?= data_table(
|
||||
[
|
||||
[
|
||||
'header' => lang('Episode.list.episode'),
|
||||
'header' => lang('VideoClip.list.status'),
|
||||
'cell' => function ($videoClip): string {
|
||||
return $videoClip->label;
|
||||
$pillVariantMap = [
|
||||
'queued' => 'default',
|
||||
'pending' => 'warning',
|
||||
'running' => 'primary',
|
||||
'canceled' => 'default',
|
||||
'failed' => 'danger',
|
||||
'passed' => 'success',
|
||||
];
|
||||
|
||||
$pillIconMap = [
|
||||
'queued' => 'timer',
|
||||
'pending' => 'pause',
|
||||
'running' => 'play',
|
||||
'canceled' => 'forbid',
|
||||
'failed' => 'close',
|
||||
'passed' => 'check',
|
||||
];
|
||||
|
||||
return '<Pill variant="' . $pillVariantMap[$videoClip->status] . '" icon="' . $pillIconMap[$videoClip->status] . '">' . $videoClip->status . '</Pill>';
|
||||
},
|
||||
],
|
||||
[
|
||||
'header' => lang('Episode.list.visibility'),
|
||||
'header' => lang('VideoClip.list.label'),
|
||||
'cell' => function ($videoClip): string {
|
||||
return $videoClip->status;
|
||||
$formatClass = [
|
||||
'landscape' => 'aspect-video h-4',
|
||||
'portrait' => 'aspect-[9/16] w-4',
|
||||
'squared' => 'aspect-square h-6',
|
||||
];
|
||||
return '<a href="#" class="inline-flex items-center w-full hover:underline gap-x-2"><span class="block w-3 h-3 rounded-full" data-tooltip="bottom" title="' . $videoClip->theme['name'] . '" style="background-color:hsl(' . $videoClip->theme['preview'] . ')"></span><span class="flex items-center justify-center text-white bg-gray-400 rounded-sm ' . $formatClass[$videoClip->format] . '" data-tooltip="bottom" title="' . $videoClip->format . '"><Icon glyph="play"/></span>' . $videoClip->label . '</a>';
|
||||
},
|
||||
],
|
||||
[
|
||||
'header' => lang('VideoClip.list.clip_id'),
|
||||
'cell' => function ($videoClip): string {
|
||||
return '#' . $videoClip->id . ' by ' . $videoClip->user->username;
|
||||
},
|
||||
],
|
||||
[
|
||||
'header' => lang('Common.actions'),
|
||||
'cell' => function ($videoClip): string {
|
||||
return '…';
|
||||
},
|
||||
],
|
||||
],
|
||||
|
Loading…
x
Reference in New Issue
Block a user