Skip to content
Snippets Groups Projects
Commit 02e4441f authored by Yassine Doghri's avatar Yassine Doghri
Browse files

feat: create optimized & resized images upon upload

- resize uploaded image to thumbnail, medium, large, feed, and id3 formats
- set image url formats where adapted in views
- set format sizes and extensions in Images config file for customization
- add validation for image uploads: `min_dims` and `is_image_squared`
- update codeigniter4 and myth-auth php packages to latest develop versions
- update npm packages to latest versions
- update public/.htaccess

closes #6
parent 40a0535f
No related branches found
No related tags found
No related merge requests found
Showing
with 417 additions and 104 deletions
...@@ -10,8 +10,12 @@ WORKDIR /castopod ...@@ -10,8 +10,12 @@ WORKDIR /castopod
RUN apt-get update && apt-get install -y \ RUN apt-get update && apt-get install -y \
libicu-dev \ libicu-dev \
libpng-dev \ libpng-dev \
libjpeg-dev \
zlib1g-dev \ zlib1g-dev \
&& docker-php-ext-install intl gd && docker-php-ext-install intl
RUN docker-php-ext-configure gd --with-jpeg-dir=/usr/include/ \
&& docker-php-ext-install gd
RUN docker-php-ext-install mysqli && docker-php-ext-enable mysqli RUN docker-php-ext-install mysqli && docker-php-ext-enable mysqli
...@@ -19,4 +23,5 @@ RUN echo "file_uploads = On\n" \ ...@@ -19,4 +23,5 @@ RUN echo "file_uploads = On\n" \
"memory_limit = 100M\n" \ "memory_limit = 100M\n" \
"upload_max_filesize = 100M\n" \ "upload_max_filesize = 100M\n" \
"post_max_size = 120M\n" \ "post_max_size = 120M\n" \
"max_execution_time = 300\n" \
> /usr/local/etc/php/conf.d/uploads.ini > /usr/local/etc/php/conf.d/uploads.ini
...@@ -30,4 +30,73 @@ class Images extends BaseConfig ...@@ -30,4 +30,73 @@ class Images extends BaseConfig
'gd' => \CodeIgniter\Images\Handlers\GDHandler::class, 'gd' => \CodeIgniter\Images\Handlers\GDHandler::class,
'imagick' => \CodeIgniter\Images\Handlers\ImageMagickHandler::class, 'imagick' => \CodeIgniter\Images\Handlers\ImageMagickHandler::class,
]; ];
/**
* --------------------------------------------------------------------------
* Uploaded images resizing 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).
*/
/**
* @var integer
*/
public $thumbnailSize = 150;
/**
* @var integer
*/
public $mediumSize = 320;
/**
* @var integer
*/
public $largeSize = 1024;
/**
* Size of images linked in the rss feed (should be between 1400 and 3000)
*
* @var integer
*/
public $feedSize = 1400;
/**
* Size for ID3 tag cover art (should be between 300 and 800)
*
* @var integer
*/
public $id3Size = 500;
/**
* --------------------------------------------------------------------------
* Uploaded images naming extensions
* --------------------------------------------------------------------------
* The properties listed below set the name extensions for the resized images
*/
/**
* @var string
*/
public $thumbnailExtension = '_thumbnail';
/**
* @var string
*/
public $mediumExtension = '_medium';
/**
* @var string
*/
public $largeExtension = '_large';
/**
* @var string
*/
public $feedExtension = '_feed';
/**
* @var string
*/
public $id3Extension = '_id3';
} }
...@@ -17,9 +17,9 @@ class Validation ...@@ -17,9 +17,9 @@ class Validation
public $ruleSets = [ public $ruleSets = [
\CodeIgniter\Validation\Rules::class, \CodeIgniter\Validation\Rules::class,
\CodeIgniter\Validation\FormatRules::class, \CodeIgniter\Validation\FormatRules::class,
\CodeIgniter\Validation\FileRules::class,
\CodeIgniter\Validation\CreditCardRules::class, \CodeIgniter\Validation\CreditCardRules::class,
\App\Validation\Rules::class, \App\Validation\Rules::class,
\App\Validation\FileRules::class,
\Myth\Auth\Authentication\Passwords\ValidationRules::class, \Myth\Auth\Authentication\Passwords\ValidationRules::class,
]; ];
......
...@@ -85,7 +85,7 @@ class Episode extends BaseController ...@@ -85,7 +85,7 @@ class Episode extends BaseController
$rules = [ $rules = [
'enclosure' => 'uploaded[enclosure]|ext_in[enclosure,mp3,m4a]', 'enclosure' => 'uploaded[enclosure]|ext_in[enclosure,mp3,m4a]',
'image' => 'image' =>
'uploaded[image]|is_image[image]|ext_in[image,jpg,png]|permit_empty', 'is_image[image]|ext_in[image,jpg,png]|min_dims[image,1400,1400]|is_image_squared[image]',
'publication_date' => 'valid_date[Y-m-d]|permit_empty', 'publication_date' => 'valid_date[Y-m-d]|permit_empty',
'publication_time' => 'publication_time' =>
'regex_match[/^(0[0-9]|1[0-9]|2[0-3]):[0-5][0-9]$/]|permit_empty', 'regex_match[/^(0[0-9]|1[0-9]|2[0-3]):[0-5][0-9]$/]|permit_empty',
...@@ -151,7 +151,7 @@ class Episode extends BaseController ...@@ -151,7 +151,7 @@ class Episode extends BaseController
'enclosure' => 'enclosure' =>
'uploaded[enclosure]|ext_in[enclosure,mp3,m4a]|permit_empty', 'uploaded[enclosure]|ext_in[enclosure,mp3,m4a]|permit_empty',
'image' => 'image' =>
'uploaded[image]|is_image[image]|ext_in[image,jpg,png]|permit_empty', 'is_image[image]|ext_in[image,jpg,png]|min_dims[image,1400,1400]|is_image_squared[image]',
'publication_date' => 'valid_date[Y-m-d]|permit_empty', 'publication_date' => 'valid_date[Y-m-d]|permit_empty',
'publication_time' => 'publication_time' =>
'regex_match[/^(0[0-9]|1[0-9]|2[0-3]):[0-5][0-9]$/]|permit_empty', 'regex_match[/^(0[0-9]|1[0-9]|2[0-3]):[0-5][0-9]$/]|permit_empty',
......
...@@ -79,7 +79,8 @@ class Podcast extends BaseController ...@@ -79,7 +79,8 @@ class Podcast extends BaseController
public function attemptCreate() public function attemptCreate()
{ {
$rules = [ $rules = [
'image' => 'uploaded[image]|is_image[image]|ext_in[image,jpg,png]', 'image' =>
'uploaded[image]|is_image[image]|ext_in[image,jpg,png]|min_dims[image,1400,1400]|is_image_squared[image]',
]; ];
if (!$this->validate($rules)) { if (!$this->validate($rules)) {
...@@ -162,8 +163,9 @@ class Podcast extends BaseController ...@@ -162,8 +163,9 @@ class Podcast extends BaseController
helper(['media', 'misc']); helper(['media', 'misc']);
$rules = [ $rules = [
'name' => 'required', 'imported_feed_url' => 'required|valid_url',
'imported_feed_url' => 'required', 'season_number' => 'is_natural_no_zero|permit_empty',
'max_episodes' => 'is_natural_no_zero|permit_empty',
]; ];
if (!$this->validate($rules)) { if (!$this->validate($rules)) {
...@@ -217,8 +219,6 @@ class Podcast extends BaseController ...@@ -217,8 +219,6 @@ class Podcast extends BaseController
'complete' => empty($nsItunes->complete) 'complete' => empty($nsItunes->complete)
? false ? false
: $nsItunes->complete == 'yes', : $nsItunes->complete == 'yes',
'episode_description_footer' => '',
'custom_html_head' => '',
'created_by' => user(), 'created_by' => user(),
'updated_by' => user(), 'updated_by' => user(),
]); ]);
...@@ -299,9 +299,10 @@ class Podcast extends BaseController ...@@ -299,9 +299,10 @@ class Podcast extends BaseController
? null ? null
: download_file($nsItunes->image->attributes()), : download_file($nsItunes->image->attributes()),
'explicit' => $nsItunes->explicit == 'yes', 'explicit' => $nsItunes->explicit == 'yes',
'number' => $this->request->getPost('force_renumber') 'number' =>
? $itemNumber $this->request->getPost('force_renumber') == 'yes'
: $nsItunes->episode, ? $itemNumber
: $nsItunes->episode,
'season_number' => empty( 'season_number' => empty(
$this->request->getPost('season_number') $this->request->getPost('season_number')
) )
...@@ -358,7 +359,7 @@ class Podcast extends BaseController ...@@ -358,7 +359,7 @@ class Podcast extends BaseController
{ {
$rules = [ $rules = [
'image' => 'image' =>
'uploaded[image]|is_image[image]|ext_in[image,jpg,png]|permit_empty', 'is_image[image]|ext_in[image,jpg,png]|min_dims[image,1400,1400]|is_image_squared[image]',
]; ];
if (!$this->validate($rules)) { if (!$this->validate($rules)) {
......
...@@ -47,6 +47,8 @@ class Analytics extends Controller ...@@ -47,6 +47,8 @@ class Analytics extends Controller
// Add one hit to this episode: // Add one hit to this episode:
public function hit($p_podcastId, $p_episodeId, ...$filename) public function hit($p_podcastId, $p_episodeId, ...$filename)
{ {
helper('media');
podcast_hit($p_podcastId, $p_episodeId); podcast_hit($p_podcastId, $p_episodeId);
return redirect()->to(media_url(implode('/', $filename))); return redirect()->to(media_url(implode('/', $filename)));
} }
......
...@@ -25,20 +25,10 @@ class Episode extends Entity ...@@ -25,20 +25,10 @@ class Episode extends Entity
protected $link; protected $link;
/** /**
* @var \CodeIgniter\Files\File * @var \App\Entities\Image
*/ */
protected $image; protected $image;
/**
* @var string
*/
protected $image_media_path;
/**
* @var string
*/
protected $image_url;
/** /**
* @var \CodeIgniter\Files\File * @var \CodeIgniter\Files\File
*/ */
...@@ -98,33 +88,30 @@ class Episode extends Entity ...@@ -98,33 +88,30 @@ class Episode extends Entity
(!($image instanceof \CodeIgniter\HTTP\Files\UploadedFile) || (!($image instanceof \CodeIgniter\HTTP\Files\UploadedFile) ||
$image->isValid()) $image->isValid())
) { ) {
helper('media');
// check whether the user has inputted an image and store it // check whether the user has inputted an image and store it
$this->attributes['image_uri'] = save_podcast_media( $this->attributes['image_uri'] = save_podcast_media(
$image, $image,
$this->getPodcast()->name, $this->getPodcast()->name,
$this->attributes['slug'] $this->attributes['slug']
); );
$this->image = new \App\Entities\Image(
$this->attributes['image_uri']
);
$this->image->saveSizes();
} }
return $this; return $this;
} }
public function getImage(): \CodeIgniter\Files\File public function getImage(): \App\Entities\Image
{
return new \CodeIgniter\Files\File($this->getImageMediaPath());
}
public function getImageMediaPath(): string
{
return media_path($this->attributes['image_uri']);
}
public function getImageUrl(): string
{ {
if ($image_uri = $this->attributes['image_uri']) { if ($image_uri = $this->attributes['image_uri']) {
return media_url($image_uri); return new \App\Entities\Image($image_uri);
} }
return $this->getPodcast()->image_url; return $this->getPodcast()->image;
} }
/** /**
......
<?php
/**
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Entities;
use CodeIgniter\Entity;
class Image extends Entity
{
/**
* @var string
*/
protected $original_path;
/**
* @var string
*/
protected $original_url;
/**
* @var string
*/
protected $thumbnail_path;
/**
* @var string
*/
protected $thumbnail_url;
/**
* @var string
*/
protected $medium_path;
/**
* @var string
*/
protected $medium_url;
/**
* @var string
*/
protected $large_path;
/**
* @var string
*/
protected $large_url;
/**
* @var string
*/
protected $feed_path;
/**
* @var string
*/
protected $feed_url;
/**
* @var string
*/
protected $id3_path;
public function __construct($originalUri)
{
helper('media');
$originalPath = media_path($originalUri);
[
'filename' => $filename,
'dirname' => $dirname,
'extension' => $extension,
] = pathinfo($originalPath);
// load images extensions from config
$imageConfig = config('Images');
$thumbnailExtension = $imageConfig->thumbnailExtension;
$mediumExtension = $imageConfig->mediumExtension;
$largeExtension = $imageConfig->largeExtension;
$feedExtension = $imageConfig->feedExtension;
$id3Extension = $imageConfig->id3Extension;
$thumbnail =
$dirname . '/' . $filename . $thumbnailExtension . '.' . $extension;
$medium =
$dirname . '/' . $filename . $mediumExtension . '.' . $extension;
$large =
$dirname . '/' . $filename . $largeExtension . '.' . $extension;
$feed = $dirname . '/' . $filename . $feedExtension . '.' . $extension;
$id3 = $dirname . '/' . $filename . $id3Extension . '.' . $extension;
parent::__construct([
'original_path' => $originalPath,
'original_url' => media_url($originalUri),
'thumbnail_path' => $thumbnail,
'thumbnail_url' => base_url($thumbnail),
'medium_path' => $medium,
'medium_url' => base_url($medium),
'large_path' => $large,
'large_url' => base_url($large),
'feed_path' => $feed,
'feed_url' => base_url($feed),
'id3_path' => $id3,
]);
}
public function saveSizes()
{
// load images sizes from config
$imageConfig = config('Images');
$thumbnailSize = $imageConfig->thumbnailSize;
$mediumSize = $imageConfig->mediumSize;
$largeSize = $imageConfig->largeSize;
$feedSize = $imageConfig->feedSize;
$id3Size = $imageConfig->id3Size;
$imageService = \Config\Services::image();
$imageService
->withFile($this->attributes['original_path'])
->resize($thumbnailSize, $thumbnailSize)
->save($this->attributes['thumbnail_path']);
$imageService
->withFile($this->attributes['original_path'])
->resize($mediumSize, $mediumSize)
->save($this->attributes['medium_path']);
$imageService
->withFile($this->attributes['original_path'])
->resize($largeSize, $largeSize)
->save($this->attributes['large_path']);
$imageService
->withFile($this->attributes['original_path'])
->resize($feedSize, $feedSize)
->save($this->attributes['feed_path']);
$imageService
->withFile($this->attributes['original_path'])
->resize($id3Size, $id3Size)
->save($this->attributes['id3_path']);
}
}
...@@ -23,20 +23,10 @@ class Podcast extends Entity ...@@ -23,20 +23,10 @@ class Podcast extends Entity
protected $link; protected $link;
/** /**
* @var \CodeIgniter\Files\File * @var \App\Entities\Image
*/ */
protected $image; protected $image;
/**
* @var string
*/
protected $image_media_path;
/**
* @var string
*/
protected $image_url;
/** /**
* @var \App\Entities\Episode[] * @var \App\Entities\Episode[]
*/ */
...@@ -101,24 +91,18 @@ class Podcast extends Entity ...@@ -101,24 +91,18 @@ class Podcast extends Entity
$this->attributes['name'], $this->attributes['name'],
'cover' 'cover'
); );
$this->image = new \App\Entities\Image(
return $this; $this->attributes['image_uri']
);
$this->image->saveSizes();
} }
}
public function getImage() return $this;
{
return new \CodeIgniter\Files\File($this->getImageMediaPath());
}
public function getImageMediaPath()
{
return media_path($this->attributes['image_uri']);
} }
public function getImageUrl() public function getImage()
{ {
return media_url($this->attributes['image_uri']); return new \App\Entities\Image($this->attributes['image_uri']);
} }
public function getLink() public function getLink()
......
...@@ -47,7 +47,7 @@ function write_enclosure_tags($episode) ...@@ -47,7 +47,7 @@ function write_enclosure_tags($episode)
$tagwriter->tagformats = ['id3v2.4']; $tagwriter->tagformats = ['id3v2.4'];
$tagwriter->tag_encoding = $TextEncoding; $tagwriter->tag_encoding = $TextEncoding;
$cover = new \CodeIgniter\Files\File($episode->image_media_path); $cover = new \CodeIgniter\Files\File($episode->image->id3_path);
$APICdata = file_get_contents($cover->getRealPath()); $APICdata = file_get_contents($cover->getRealPath());
......
...@@ -19,17 +19,15 @@ function save_podcast_media($file, $podcast_name, $media_name) ...@@ -19,17 +19,15 @@ function save_podcast_media($file, $podcast_name, $media_name)
{ {
$file_name = $media_name . '.' . $file->getExtension(); $file_name = $media_name . '.' . $file->getExtension();
if (!file_exists(config('App')->mediaRoot . '/' . $podcast_name)) { $mediaRoot = config('App')->mediaRoot;
mkdir(config('App')->mediaRoot . '/' . $podcast_name, 0777, true);
touch(config('App')->mediaRoot . '/' . $podcast_name . '/index.html'); if (!file_exists($mediaRoot . '/' . $podcast_name)) {
mkdir($mediaRoot . '/' . $podcast_name, 0777, true);
touch($mediaRoot . '/' . $podcast_name . '/index.html');
} }
// move to media folder and overwrite file if already existing // move to media folder and overwrite file if already existing
$file->move( $file->move($mediaRoot . '/' . $podcast_name . '/', $file_name, true);
config('App')->mediaRoot . '/' . $podcast_name . '/',
$file_name,
true
);
return $podcast_name . '/' . $file_name; return $podcast_name . '/' . $file_name;
} }
...@@ -64,3 +62,15 @@ function media_path($uri = ''): string ...@@ -64,3 +62,15 @@ function media_path($uri = ''): string
return config('App')->mediaRoot . '/' . $uri; return config('App')->mediaRoot . '/' . $uri;
} }
/**
* Return the media base URL to use in views
*
* @param mixed $uri URI string or array of URI segments
* @param string $protocol
* @return string
*/
function media_url($uri = '', string $protocol = null): string
{
return base_url(config('App')->mediaRoot . '/' . $uri, $protocol);
}
...@@ -57,7 +57,7 @@ function get_rss_feed($podcast) ...@@ -57,7 +57,7 @@ function get_rss_feed($podcast)
$channel->addChild('title', $podcast->title); $channel->addChild('title', $podcast->title);
$channel->addChildWithCDATA('description', $podcast->description_html); $channel->addChildWithCDATA('description', $podcast->description_html);
$itunes_image = $channel->addChild('image', null, $itunes_namespace); $itunes_image = $channel->addChild('image', null, $itunes_namespace);
$itunes_image->addAttribute('href', $podcast->image_url); $itunes_image->addAttribute('href', $podcast->image->url);
$channel->addChild('language', $podcast->language); $channel->addChild('language', $podcast->language);
$itunes_category = $channel->addChild('category', null, $itunes_namespace); $itunes_category = $channel->addChild('category', null, $itunes_namespace);
...@@ -106,7 +106,7 @@ function get_rss_feed($podcast) ...@@ -106,7 +106,7 @@ function get_rss_feed($podcast)
$channel->addChild('complete', 'Yes', $itunes_namespace); $channel->addChild('complete', 'Yes', $itunes_namespace);
$image = $channel->addChild('image'); $image = $channel->addChild('image');
$image->addChild('url', $podcast->image_url); $image->addChild('url', $podcast->image->feed_url);
$image->addChild('title', $podcast->title); $image->addChild('title', $podcast->title);
$image->addChild('link', $podcast->link); $image->addChild('link', $podcast->link);
...@@ -136,7 +136,7 @@ function get_rss_feed($podcast) ...@@ -136,7 +136,7 @@ function get_rss_feed($podcast)
null, null,
$itunes_namespace $itunes_namespace
); );
$episode_itunes_image->addAttribute('href', $episode->image_url); $episode_itunes_image->addAttribute('href', $episode->image->feed_url);
$item->addChild( $item->addChild(
'explicit', 'explicit',
$episode->explicit ? 'true' : 'false', $episode->explicit ? 'true' : 'false',
......
<?php
/**
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
/**
* Return the media base URL to use in views
*
* @param mixed $uri URI string or array of URI segments
* @param string $protocol
* @return string
*/
function media_url($uri = '', string $protocol = null): string
{
return base_url(config('App')->mediaRoot . '/' . $uri, $protocol);
}
...@@ -12,4 +12,8 @@ return [ ...@@ -12,4 +12,8 @@ return [
'mediumDate' => '{0,date,medium}', 'mediumDate' => '{0,date,medium}',
'duration' => '{0,duration}', 'duration' => '{0,duration}',
'powered_by' => 'Powered by {castopod}.', 'powered_by' => 'Powered by {castopod}.',
'forms' => [
'image_size_hint' =>
'Image must be squared with at least 1400px wide and tall.',
],
]; ];
...@@ -9,4 +9,8 @@ ...@@ -9,4 +9,8 @@
return [ return [
'not_in_protected_slugs' => 'not_in_protected_slugs' =>
'The {field} field conflicts with one of the gateway routes (admin, auth or install).', 'The {field} field conflicts with one of the gateway routes (admin, auth or install).',
'min_dims' =>
'{field} is either not an image, or it is not wide or tall enough.',
'is_image_squared' =>
'{field} is either not an image, or it is not squared (width and height differ).',
]; ];
<?php
/**
* @copyright 2020 Podlibre
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
* @link https://castopod.org/
*/
namespace App\Validation;
use CodeIgniter\Validation\FileRules as ValidationFileRules;
class FileRules extends ValidationFileRules
{
/**
* Checks an uploaded file to verify that the dimensions are within
* a specified allowable dimension.
*
* @param string|null $blank
* @param string $params
*
* @return boolean
*/
public function min_dims(string $blank = null, string $params): bool
{
// Grab the file name off the top of the $params
// after we split it.
$params = explode(',', $params);
$name = array_shift($params);
if (!($files = $this->request->getFileMultiple($name))) {
$files = [$this->request->getFile($name)];
}
foreach ($files as $file) {
if (is_null($file)) {
return false;
}
if ($file->getError() === UPLOAD_ERR_NO_FILE) {
return true;
}
// Get Parameter sizes
$minWidth = $params[0] ?? 0;
$minHeight = $params[1] ?? 0;
// Get uploaded image size
$info = getimagesize($file->getTempName());
$fileWidth = $info[0];
$fileHeight = $info[1];
if ($fileWidth < $minWidth || $fileHeight < $minHeight) {
return false;
}
}
return true;
}
//--------------------------------------------------------------------
/**
* Checks an uploaded file to verify that the image ratio is of 1:1
*
* @param string|null $blank
* @param string $params
*
* @return boolean
*/
public function is_image_squared(string $blank = null, string $params): bool
{
// Grab the file name off the top of the $params
// after we split it.
$params = explode(',', $params);
$name = array_shift($params);
if (!($files = $this->request->getFileMultiple($name))) {
$files = [$this->request->getFile($name)];
}
foreach ($files as $file) {
if (is_null($file)) {
return false;
}
if ($file->getError() === UPLOAD_ERR_NO_FILE) {
return true;
}
// Get uploaded image size
$info = getimagesize($file->getTempName());
$fileWidth = $info[0];
$fileHeight = $info[1];
if ($fileWidth != $fileHeight) {
return false;
}
}
return true;
}
//--------------------------------------------------------------------
}
...@@ -27,4 +27,6 @@ class Rules ...@@ -27,4 +27,6 @@ class Rules
]; ];
return !in_array($value, $protectedSlugs, true); return !in_array($value, $protectedSlugs, true);
} }
//--------------------------------------------------------------------
} }
<article class="flex w-full max-w-lg mb-4 bg-white border rounded shadow"> <article class="flex w-full max-w-lg mb-4 bg-white border rounded shadow">
<img src="<?= $episode->image_url ?>" alt="<?= $episode->title ?>" class="object-cover w-32 h-32 rounded-l" /> <img
loading="lazy"
src="<?= $episode->image->thumbnail_url ?>"
alt="<?= $episode->title ?>" class="object-cover w-32 h-32 rounded-l" />
<div class="flex flex-col flex-1 px-4 py-2"> <div class="flex flex-col flex-1 px-4 py-2">
<a href="<?= route_to( <a href="<?= route_to(
'episode-view', 'episode-view',
......
<article class="w-48 h-full mb-4 mr-4 overflow-hidden bg-white border rounded shadow"> <article class="w-48 h-full mb-4 mr-4 overflow-hidden bg-white border rounded shadow">
<img alt="<?= $podcast->title ?>" src="<?= $podcast->image_url ?>" class="object-cover w-full h-40" /> <img
alt="<?= $podcast->title ?>"
src="<?= $podcast->image
->thumbnail_url ?>" class="object-cover w-full h-40" />
<div class="p-2"> <div class="p-2">
<a href="<?= route_to( <a href="<?= route_to(
'podcast-view', 'podcast-view',
......
...@@ -23,6 +23,18 @@ ...@@ -23,6 +23,18 @@
'accept' => '.mp3,.m4a', 'accept' => '.mp3,.m4a',
]) ?> ]) ?>
<?= form_label(lang('Episode.form.image'), 'image') ?>
<?= form_input([
'id' => 'image',
'name' => 'image',
'class' => 'form-input',
'type' => 'file',
'accept' => '.jpg,.jpeg,.png',
]) ?>
<small class="mb-4 text-gray-600"><?= lang(
'Common.forms.image_size_hint'
) ?></small>
<?= form_label(lang('Episode.form.title'), 'title') ?> <?= form_label(lang('Episode.form.title'), 'title') ?>
<?= form_input([ <?= form_input([
'id' => 'title', 'id' => 'title',
...@@ -87,16 +99,6 @@ ...@@ -87,16 +99,6 @@
</div> </div>
<?= form_fieldset_close() ?> <?= form_fieldset_close() ?>
<?= form_label(lang('Episode.form.image'), 'image') ?>
<?= form_input([
'id' => 'image',
'name' => 'image',
'class' => 'form-input mb-4',
'type' => 'file',
'accept' => '.jpg,.jpeg,.png',
]) ?>
<?= form_label(lang('Episode.form.season_number'), 'season_number') ?> <?= form_label(lang('Episode.form.season_number'), 'season_number') ?>
<?= form_input([ <?= form_input([
'id' => 'season_number', 'id' => 'season_number',
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment