Commit 4a8147bf authored by Yassine Doghri's avatar Yassine Doghri
Browse files

feat: add podcast banner field for each podcast + refactor images configuration

- rename image fields on podcast, episode and persons for better clarity
- set different sizes
config for podcast cover, banner and persons avatars
- add tiny size for covers
- fix responsive
on admin forms
parent 5c56f3e6
......@@ -32,42 +32,63 @@ class Images extends BaseConfig
/*
|--------------------------------------------------------------------------
| Uploaded images resizing sizes (in px)
| Uploaded images sizes (in px)
|--------------------------------------------------------------------------
| The sizes listed below determine the resizing of images when uploaded.
| All uploaded images are of 1:1 ratio (width and height are the same).
*/
public int $thumbnailSize = 150;
public int $mediumSize = 320;
public int $largeSize = 1024;
/**
* Size of images linked in the rss feed (should be between 1400 and 3000)
* Podcast cover image sizes
*
* Uploaded podcast covers are of 1:1 ratio (width and height are the same).
*
* Size of images linked in the rss feed (should be between 1400 and 3000). Size for ID3 tag cover art (should be
* between 300 and 800)
*
* Array values are as follows: 'name' => [width, height]
*
* @var array<string, int[]>
*/
public int $feedSize = 1400;
public array $podcastCoverSizes = [
'tiny' => [40, 40],
'thumbnail' => [150, 150],
'medium' => [320, 320],
'large' => [1024, 1024],
'feed' => [1400, 1400],
'id3' => [500, 500],
];
/**
* Size for ID3 tag cover art (should be between 300 and 800)
* Podcast header cover image
*
* Uploaded podcast header covers are of 3:1 ratio
*
* Array values are as follows: 'name' => [width, height]
*
* @var array<string, int[]>
*/
public int $id3Size = 500;
/*
|--------------------------------------------------------------------------
| Uploaded images naming extensions
|--------------------------------------------------------------------------
| The properties listed below set the name extensions for the resized images
*/
public string $thumbnailSuffix = '_thumbnail';
public string $mediumSuffix = '_medium';
public array $podcastBannerSizes = [
'small' => [320, 128],
'medium' => [960, 320],
'large' => [1500, 500],
];
public string $largeSuffix = '_large';
public string $podcastBannerDefaultPath = 'castopod-banner-default.jpg';
public string $feedSuffix = '_feed';
public string $podcastBannerDefaultMimeType = 'image/jpeg';
public string $id3Suffix = '_id3';
/**
* Person image
*
* Uploaded person images are of 1:1 ratio (width and height are the same).
*
* Array values are as follows: 'name' => [width, height]
*
* @var array<string, int[]>
*/
public array $personAvatarSizes = [
'tiny' => [40, 40],
'thumbnail' => [150, 150],
'medium' => [320, 320],
];
}
......@@ -48,7 +48,7 @@ class CreditsController extends BaseController
$personId => [
'full_name' => $credit->person->full_name,
'thumbnail_url' =>
$credit->person->image->thumbnail_url,
$credit->person->avatar->thumbnail_url,
'information_url' =>
$credit->person->information_url,
'roles' => [
......@@ -87,7 +87,7 @@ class CreditsController extends BaseController
$credits[$personGroup]['persons'][$personId] = [
'full_name' => $credit->person->full_name,
'thumbnail_url' =>
$credit->person->image->thumbnail_url,
$credit->person->avatar->thumbnail_url,
'information_url' => $credit->person->information_url,
'roles' => [
$personRole => [
......
......@@ -200,11 +200,11 @@ class EpisodeController extends BaseController
'" width="100%" height="144" frameborder="0" scrolling="no"></iframe>',
'width' => 600,
'height' => 144,
'thumbnail_url' => $this->episode->image->large_url,
'thumbnail_url' => $this->episode->cover->large_url,
'thumbnail_width' => config('Images')
->largeSize,
->podcastCoverSizes['large'][0],
'thumbnail_height' => config('Images')
->largeSize,
->podcastCoverSizes['large'][1],
]);
}
......@@ -219,9 +219,9 @@ class EpisodeController extends BaseController
$oembed->addChild('provider_url', $this->podcast->link);
$oembed->addChild('author_name', $this->podcast->title);
$oembed->addChild('author_url', $this->podcast->link);
$oembed->addChild('thumbnail', $this->episode->image->large_url);
$oembed->addChild('thumbnail_width', config('Images')->largeSize);
$oembed->addChild('thumbnail_height', config('Images')->largeSize);
$oembed->addChild('thumbnail', $this->episode->cover->large_url);
$oembed->addChild('thumbnail_width', config('Images')->podcastCoverSizes['large'][0]);
$oembed->addChild('thumbnail_height', config('Images')->podcastCoverSizes['large'][1]);
$oembed->addChild(
'html',
htmlentities(
......
......@@ -46,7 +46,7 @@ class MapMarkerController extends BaseController
'location_url' => $episode->location->url,
'episode_link' => $episode->link,
'podcast_link' => $episode->podcast->link,
'image_path' => $episode->image->thumbnail_url,
'cover_path' => $episode->cover->thumbnail_url,
'podcast_title' => $episode->podcast->title,
'episode_title' => $episode->title,
];
......
......@@ -46,16 +46,28 @@ class AddPodcasts extends Migration
'description_html' => [
'type' => 'TEXT',
],
'image_path' => [
'cover_path' => [
'type' => 'VARCHAR',
'constraint' => 255,
],
// constraint is 13 because the longest safe mimetype for images is image/svg+xml,
// see https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types#image_types
'image_mimetype' => [
'cover_mimetype' => [
'type' => 'VARCHAR',
'constraint' => 13,
],
'banner_path' => [
'type' => 'VARCHAR',
'constraint' => 255,
'null' => true,
'default' => null,
],
'banner_mimetype' => [
'type' => 'VARCHAR',
'constraint' => 13,
'null' => true,
'default' => null,
],
'language_code' => [
'type' => 'VARCHAR',
'constraint' => 2,
......
......@@ -70,14 +70,14 @@ class AddEpisodes extends Migration
'description_html' => [
'type' => 'TEXT',
],
'image_path' => [
'cover_path' => [
'type' => 'VARCHAR',
'constraint' => 255,
'null' => true,
],
// constraint is 13 because the longest safe mimetype for images is image/svg+xml,
// see https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types#image_types
'image_mimetype' => [
'cover_mimetype' => [
'type' => 'VARCHAR',
'constraint' => 13,
'null' => true,
......
......@@ -42,14 +42,14 @@ class AddPersons extends Migration
'The url to a relevant resource of information about the person, such as a homepage or third-party profile platform.',
'null' => true,
],
'image_path' => [
'avatar_path' => [
'type' => 'VARCHAR',
'constraint' => 255,
'null' => true,
],
// constraint is 13 because the longest safe mimetype for images is image/svg+xml,
// see https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types#image_types
'image_mimetype' => [
'avatar_mimetype' => [
'type' => 'VARCHAR',
'constraint' => 13,
'null' => true,
......
......@@ -44,9 +44,9 @@ use RuntimeException;
* @property string|null $description Holds text only description, striped of any markdown or html special characters
* @property string $description_markdown
* @property string $description_html
* @property Image $image
* @property string|null $image_path
* @property string|null $image_mimetype
* @property Image $cover
* @property string|null $cover_path
* @property string|null $cover_mimetype
* @property File|null $transcript_file
* @property string|null $transcript_file_url
* @property string|null $transcript_file_path
......@@ -98,7 +98,7 @@ class Episode extends Entity
protected string $embed_url;
protected Image $image;
protected Image $cover;
protected ?string $description = null;
......@@ -153,8 +153,8 @@ class Episode extends Entity
'audio_file_header_size' => 'integer',
'description_markdown' => 'string',
'description_html' => 'string',
'image_path' => '?string',
'image_mimetype' => '?string',
'cover_path' => '?string',
'cover_mimetype' => '?string',
'transcript_file_path' => '?string',
'transcript_file_remote_url' => '?string',
'chapters_file_path' => '?string',
......@@ -175,31 +175,36 @@ class Episode extends Entity
];
/**
* Saves an episode image
* Saves an episode cover
*/
public function setImage(?Image $image = null): static
public function setCover(?Image $cover = null): static
{
if ($image === null) {
if ($cover === null) {
return $this;
}
// Save image
$image->saveImage('podcasts/' . $this->getPodcast()->handle, $this->attributes['slug']);
$cover->saveImage(
config('Images')
->podcastCoverSizes,
'podcasts/' . $this->getPodcast()->handle,
$this->attributes['slug']
);
$this->attributes['image_mimetype'] = $image->mimetype;
$this->attributes['image_path'] = $image->path;
$this->attributes['cover_mimetype'] = $cover->mimetype;
$this->attributes['cover_path'] = $cover->path;
return $this;
}
public function getImage(): Image
public function getCover(): Image
{
if ($imagePath = $this->attributes['image_path']) {
return new Image(null, $imagePath, $this->attributes['image_mimetype']);
if ($coverPath = $this->attributes['cover_path']) {
return new Image(null, $coverPath, $this->attributes['cover_mimetype']);
}
return $this->getPodcast()
->image;
->cover;
}
/**
......
......@@ -13,7 +13,6 @@ namespace App\Entities;
use CodeIgniter\Entity\Entity;
use CodeIgniter\Files\File;
use Config\Images;
use Config\Services;
use RuntimeException;
/**
......@@ -24,16 +23,6 @@ use RuntimeException;
* @property string $mimetype
* @property string $path
* @property string $url
* @property string $thumbnail_path
* @property string $thumbnail_url
* @property string $medium_path
* @property string $medium_url
* @property string $large_path
* @property string $large_url
* @property string $feed_path
* @property string $feed_url
* @property string $id3_path
* @property string $id3_url
*/
class Image extends Entity
{
......@@ -47,14 +36,14 @@ class Image extends Entity
protected string $extension;
protected string $mimetype;
public function __construct(?File $file, string $path = '', string $mimetype = '')
{
if ($file === null && $path === '') {
throw new RuntimeException('File or path must be set to create an Image.');
}
$this->config = config('Images');
$dirname = '';
$filename = '';
$extension = '';
......@@ -81,152 +70,87 @@ class Image extends Entity
$this->mimetype = $mimetype;
}
public function getFile(): File
public function __get($property)
{
if ($this->file === null) {
$this->file = new File($this->path);
// Convert to CamelCase for the method
$method = 'get' . str_replace(' ', '', ucwords(str_replace(['-', '_'], ' ', $property)));
// if a get* method exists for this property,
// call that method to get this value.
// @phpstan-ignore-next-line
if (method_exists($this, $method)) {
return $this->{$method}();
}
return $this->file;
}
public function getPath(): string
{
return $this->dirname . '/' . $this->filename . '.' . $this->extension;
}
public function getUrl(): string
{
helper('media');
return media_base_url($this->path);
}
public function getThumbnailPath(): string
{
return $this->dirname .
'/' .
$this->filename .
$this->config->thumbnailSuffix .
'.' .
$this->extension;
}
public function getThumbnailUrl(): string
{
helper('media');
$fileSuffix = '';
if ($lastUnderscorePosition = strrpos($property, '_')) {
$fileSuffix = '_' . substr($property, 0, $lastUnderscorePosition);
}
return media_base_url($this->thumbnail_path);
}
$path = '';
if ($this->dirname !== '.') {
$path .= $this->dirname . '/';
}
$path .= $this->filename . $fileSuffix . '.' . $this->extension;
public function getMediumPath(): string
{
return $this->dirname .
'/' .
$this->filename .
$this->config->mediumSuffix .
'.' .
$this->extension;
}
if (str_ends_with($property, 'url')) {
helper('media');
public function getMediumUrl(): string
{
helper('media');
return media_base_url($path);
}
return media_base_url($this->medium_path);
if (str_ends_with($property, 'path')) {
return $path;
}
}
public function getLargePath(): string
public function getMimetype(): string
{
return $this->dirname .
'/' .
$this->filename .
$this->config->largeSuffix .
'.' .
$this->extension;
return $this->mimetype;
}
public function getLargeUrl(): string
public function getFile(): File
{
helper('media');
return media_base_url($this->large_path);
}
if ($this->file === null) {
$this->file = new File($this->path);
}
public function getFeedPath(): string
{
return $this->dirname .
'/' .
$this->filename .
$this->config->feedSuffix .
'.' .
$this->extension;
return $this->file;
}
public function getFeedUrl(): string
/**
* @param array<string, int[]> $sizes
*/
public function saveImage(array $sizes, string $dirname, string $filename): void
{
helper('media');
return media_base_url($this->feed_path);
}
$this->dirname = $dirname;
$this->filename = $filename;
public function getId3Path(): string
{
return $this->dirname .
'/' .
$this->filename .
$this->config->id3Suffix .
'.' .
$this->extension;
}
save_media($this->file, $this->dirname, $this->filename);
public function getId3Url(): string
{
helper('media');
$imageService = service('image');
return media_base_url($this->id3_path);
foreach ($sizes as $name => $size) {
$pathProperty = $name . '_path';
$imageService
->withFile(media_path($this->path))
->resize($size[0], $size[1])
->save(media_path($this->{$pathProperty}));
}
}
public function saveImage(string $dirname, string $filename): void
/**
* @param array<string, int[]> $sizes
*/
public function delete(array $sizes): void
{
helper('media');
$this->dirname = $dirname;
$this->filename = $filename;
save_media($this->file, $this->dirname, $this->filename);
$imageService = Services::image();
$thumbnailSize = $this->config->thumbnailSize;
$mediumSize = $this->config->mediumSize;
$largeSize = $this->config->largeSize;
$feedSize = $this->config->feedSize;
$id3Size = $this->config->id3Size;
$imageService
->withFile(media_path($this->path))
->resize($thumbnailSize, $thumbnailSize)
->save(media_path($this->thumbnail_path));
$imageService
->withFile(media_path($this->path))
->resize($mediumSize, $mediumSize)
->save(media_path($this->medium_path));
$imageService
->withFile(media_path($this->path))
->resize($largeSize, $largeSize)
->save(media_path($this->large_path));
$imageService
->withFile(media_path($this->path))
->resize($feedSize, $feedSize)
->save(media_path($this->feed_path));
$imageService
->withFile(media_path($this->path))
->resize($id3Size, $id3Size)
->save(media_path($this->id3_path));
foreach (array_keys($sizes) as $name) {
$pathProperty = $name . '_path';
unlink(media_path($this->{$pathProperty}));
}
}
}
......@@ -19,16 +19,16 @@ use RuntimeException;
* @property string $full_name
* @property string $unique_name
* @property string|null $information_url
* @property Image $image
* @property string $image_path
* @property string $image_mimetype
* @property Image $avatar
* @property string $avatar_path
* @property string $avatar_mimetype
* @property int $created_by
* @property int $updated_by
* @property object[]|null $roles
*/
class Person extends Entity
{
protected Image $image;
protected Image $avatar;
protected ?int $podcast_id = null;
......@@ -47,8 +47,8 @@ class Person extends Entity
'full_name' => 'string',
'unique_name' => 'string',
'information_url' => '?string',
'image_path' => '?string',
'image_mimetype' => '?string',
'avatar_path' => '?string',
'avatar_mimetype' => '?string',
'podcast_id' => '?integer',
'episode_id' => '?integer',
'created_by' => 'integer',
......@@ -56,32 +56,31 @@ class Person extends Entity
];
/**
* Saves a picture in `public/media/persons/`
* Saves the person avatar in `public/media/persons/`
*/
public function setImage(?Image $image = null): static
public function setAvatar(?Image $avatar = null): static
{
if ($image === null) {
if ($avatar === null) {
return $this;
}
helper('media');
// Save image
$image->saveImage('persons', $this->attributes['unique_name']);
$avatar->saveImage(config('Images')->personAvatarSizes, 'persons', $this->attributes['unique_name']);
$this->attributes['image_mimetype'] = $image->mimetype;
$this->attributes['image_path'] = $image->path;
$this->attributes['avatar_mimetype'] = $avatar->mimetype;
$this->attributes['avatar_path'] = $avatar->path;
return $this;
}
public function getImage(): Image
public function getAvatar(): Image
{
if ($this->attributes['image_path'] === null) {
if ($this->attributes['avatar_path'] === null) {
return new Image(null, '/castopod-avatar-default.jpg', 'image/jpeg');
}
return new Image(null, $this->attributes['image_path'], $this->attributes['image_mimetype']);