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
This commit is contained in:
Yassine Doghri 2020-09-08 11:45:17 +00:00
parent 40a0535fc1
commit 02e4441f98
33 changed files with 1034 additions and 538 deletions

View File

@ -10,8 +10,12 @@ WORKDIR /castopod
RUN apt-get update && apt-get install -y \
libicu-dev \
libpng-dev \
libjpeg-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
@ -19,4 +23,5 @@ RUN echo "file_uploads = On\n" \
"memory_limit = 100M\n" \
"upload_max_filesize = 100M\n" \
"post_max_size = 120M\n" \
"max_execution_time = 300\n" \
> /usr/local/etc/php/conf.d/uploads.ini

View File

@ -30,4 +30,73 @@ class Images extends BaseConfig
'gd' => \CodeIgniter\Images\Handlers\GDHandler::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';
}

View File

@ -17,9 +17,9 @@ class Validation
public $ruleSets = [
\CodeIgniter\Validation\Rules::class,
\CodeIgniter\Validation\FormatRules::class,
\CodeIgniter\Validation\FileRules::class,
\CodeIgniter\Validation\CreditCardRules::class,
\App\Validation\Rules::class,
\App\Validation\FileRules::class,
\Myth\Auth\Authentication\Passwords\ValidationRules::class,
];

View File

@ -85,7 +85,7 @@ class Episode extends BaseController
$rules = [
'enclosure' => 'uploaded[enclosure]|ext_in[enclosure,mp3,m4a]',
'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_time' =>
'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
'enclosure' =>
'uploaded[enclosure]|ext_in[enclosure,mp3,m4a]|permit_empty',
'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_time' =>
'regex_match[/^(0[0-9]|1[0-9]|2[0-3]):[0-5][0-9]$/]|permit_empty',

View File

@ -79,7 +79,8 @@ class Podcast extends BaseController
public function attemptCreate()
{
$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)) {
@ -162,8 +163,9 @@ class Podcast extends BaseController
helper(['media', 'misc']);
$rules = [
'name' => 'required',
'imported_feed_url' => 'required',
'imported_feed_url' => 'required|valid_url',
'season_number' => 'is_natural_no_zero|permit_empty',
'max_episodes' => 'is_natural_no_zero|permit_empty',
];
if (!$this->validate($rules)) {
@ -217,8 +219,6 @@ class Podcast extends BaseController
'complete' => empty($nsItunes->complete)
? false
: $nsItunes->complete == 'yes',
'episode_description_footer' => '',
'custom_html_head' => '',
'created_by' => user(),
'updated_by' => user(),
]);
@ -299,9 +299,10 @@ class Podcast extends BaseController
? null
: download_file($nsItunes->image->attributes()),
'explicit' => $nsItunes->explicit == 'yes',
'number' => $this->request->getPost('force_renumber')
? $itemNumber
: $nsItunes->episode,
'number' =>
$this->request->getPost('force_renumber') == 'yes'
? $itemNumber
: $nsItunes->episode,
'season_number' => empty(
$this->request->getPost('season_number')
)
@ -358,7 +359,7 @@ class Podcast extends BaseController
{
$rules = [
'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)) {

View File

@ -47,6 +47,8 @@ class Analytics extends Controller
// Add one hit to this episode:
public function hit($p_podcastId, $p_episodeId, ...$filename)
{
helper('media');
podcast_hit($p_podcastId, $p_episodeId);
return redirect()->to(media_url(implode('/', $filename)));
}

View File

@ -25,20 +25,10 @@ class Episode extends Entity
protected $link;
/**
* @var \CodeIgniter\Files\File
* @var \App\Entities\Image
*/
protected $image;
/**
* @var string
*/
protected $image_media_path;
/**
* @var string
*/
protected $image_url;
/**
* @var \CodeIgniter\Files\File
*/
@ -98,33 +88,30 @@ class Episode extends Entity
(!($image instanceof \CodeIgniter\HTTP\Files\UploadedFile) ||
$image->isValid())
) {
helper('media');
// check whether the user has inputted an image and store it
$this->attributes['image_uri'] = save_podcast_media(
$image,
$this->getPodcast()->name,
$this->attributes['slug']
);
$this->image = new \App\Entities\Image(
$this->attributes['image_uri']
);
$this->image->saveSizes();
}
return $this;
}
public function getImage(): \CodeIgniter\Files\File
{
return new \CodeIgniter\Files\File($this->getImageMediaPath());
}
public function getImageMediaPath(): string
{
return media_path($this->attributes['image_uri']);
}
public function getImageUrl(): string
public function getImage(): \App\Entities\Image
{
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;
}
/**

151
app/Entities/Image.php Normal file
View File

@ -0,0 +1,151 @@
<?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']);
}
}

View File

@ -23,20 +23,10 @@ class Podcast extends Entity
protected $link;
/**
* @var \CodeIgniter\Files\File
* @var \App\Entities\Image
*/
protected $image;
/**
* @var string
*/
protected $image_media_path;
/**
* @var string
*/
protected $image_url;
/**
* @var \App\Entities\Episode[]
*/
@ -101,24 +91,18 @@ class Podcast extends Entity
$this->attributes['name'],
'cover'
);
return $this;
$this->image = new \App\Entities\Image(
$this->attributes['image_uri']
);
$this->image->saveSizes();
}
return $this;
}
public function getImage()
{
return new \CodeIgniter\Files\File($this->getImageMediaPath());
}
public function getImageMediaPath()
{
return media_path($this->attributes['image_uri']);
}
public function getImageUrl()
{
return media_url($this->attributes['image_uri']);
return new \App\Entities\Image($this->attributes['image_uri']);
}
public function getLink()

View File

@ -47,7 +47,7 @@ function write_enclosure_tags($episode)
$tagwriter->tagformats = ['id3v2.4'];
$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());

View File

@ -19,17 +19,15 @@ function save_podcast_media($file, $podcast_name, $media_name)
{
$file_name = $media_name . '.' . $file->getExtension();
if (!file_exists(config('App')->mediaRoot . '/' . $podcast_name)) {
mkdir(config('App')->mediaRoot . '/' . $podcast_name, 0777, true);
touch(config('App')->mediaRoot . '/' . $podcast_name . '/index.html');
$mediaRoot = config('App')->mediaRoot;
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
$file->move(
config('App')->mediaRoot . '/' . $podcast_name . '/',
$file_name,
true
);
$file->move($mediaRoot . '/' . $podcast_name . '/', $file_name, true);
return $podcast_name . '/' . $file_name;
}
@ -64,3 +62,15 @@ function media_path($uri = ''): string
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);
}

View File

@ -57,7 +57,7 @@ function get_rss_feed($podcast)
$channel->addChild('title', $podcast->title);
$channel->addChildWithCDATA('description', $podcast->description_html);
$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);
$itunes_category = $channel->addChild('category', null, $itunes_namespace);
@ -106,7 +106,7 @@ function get_rss_feed($podcast)
$channel->addChild('complete', 'Yes', $itunes_namespace);
$image = $channel->addChild('image');
$image->addChild('url', $podcast->image_url);
$image->addChild('url', $podcast->image->feed_url);
$image->addChild('title', $podcast->title);
$image->addChild('link', $podcast->link);
@ -136,7 +136,7 @@ function get_rss_feed($podcast)
null,
$itunes_namespace
);
$episode_itunes_image->addAttribute('href', $episode->image_url);
$episode_itunes_image->addAttribute('href', $episode->image->feed_url);
$item->addChild(
'explicit',
$episode->explicit ? 'true' : 'false',

View File

@ -1,19 +0,0 @@
<?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);
}

View File

@ -12,4 +12,8 @@ return [
'mediumDate' => '{0,date,medium}',
'duration' => '{0,duration}',
'powered_by' => 'Powered by {castopod}.',
'forms' => [
'image_size_hint' =>
'Image must be squared with at least 1400px wide and tall.',
],
];

View File

@ -9,4 +9,8 @@
return [
'not_in_protected_slugs' =>
'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).',
];

View File

@ -0,0 +1,105 @@
<?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;
}
//--------------------------------------------------------------------
}

View File

@ -27,4 +27,6 @@ class Rules
];
return !in_array($value, $protectedSlugs, true);
}
//--------------------------------------------------------------------
}

View File

@ -1,5 +1,8 @@
<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">
<a href="<?= route_to(
'episode-view',

View File

@ -1,5 +1,8 @@
<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">
<a href="<?= route_to(
'podcast-view',

View File

@ -23,6 +23,18 @@
'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_input([
'id' => 'title',
@ -87,16 +99,6 @@
</div>
<?= 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_input([
'id' => 'season_number',

View File

@ -22,6 +22,23 @@
'accept' => '.mp3,.m4a',
]) ?>
<?= form_label(lang('Episode.form.image'), 'image') ?>
<img
src="<?= $episode->image->thumbnail_url ?>"
alt="<?= $episode->title ?>"
class="object-cover w-32 h-32"
/>
<?= 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_input([
'id' => 'title',
@ -94,16 +111,6 @@
</div>
<?= form_fieldset_close() ?>
<?= form_label(lang('Episode.form.image'), 'image') ?>
<img src="<?= $episode->image_url ?>" alt="<?= $episode->title ?>" class="object-cover w-32 h-32" />
<?= 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_input([
'id' => 'season_number',

View File

@ -6,7 +6,11 @@
<?= $this->section('content') ?>
<img src="<?= $episode->image_url ?>" alt="Episode cover" class="object-cover w-40 h-40 mb-6" />
<img
src="<?= $episode->image->medium_url ?>"
alt="Episode cover"
class="object-cover w-40 h-40 mb-6"
/>
<audio controls preload="none" class="mb-12">
<source src="<?= $episode->enclosure_url ?>" type="<?= $episode->enclosure_type ?>">
Your browser does not support the audio tag.

View File

@ -13,6 +13,19 @@
]) ?>
<?= csrf_field() ?>
<?= form_label(lang('Podcast.form.image'), 'image') ?>
<?= form_input([
'id' => 'image',
'name' => 'image',
'class' => 'form-input',
'required' => 'required',
'type' => 'file',
'accept' => '.jpg,.jpeg,.png',
]) ?>
<small class="mb-4 text-gray-600"><?= lang(
'Common.forms.image_size_hint'
) ?></small>
<?= form_label(lang('Podcast.form.title'), 'title') ?>
<?= form_input([
'id' => 'title',
@ -54,7 +67,6 @@
[
'id' => 'episode_description_footer',
'name' => 'episode_description_footer',
'class' => 'form-textarea',
],
old('episode_description_footer', '', false),
@ -62,16 +74,6 @@
) ?>
</div>
<?= form_label(lang('Podcast.form.image'), 'image') ?>
<?= form_input([
'id' => 'image',
'name' => 'image',
'class' => 'form-input mb-4',
'required' => 'required',
'type' => 'file',
'accept' => '.jpg,.jpeg,.png',
]) ?>
<?= form_label(lang('Podcast.form.language'), 'language') ?>
<?= form_dropdown('language', $languageOptions, old('language', $browserLang), [
'id' => 'language',
@ -122,7 +124,9 @@
'value' => old('author'),
]) ?>
<?= form_fieldset('', ['class' => 'flex flex-col mb-4']) ?>
<?= form_fieldset('', [
'class' => 'flex flex-col mb-4',
]) ?>
<legend><?= lang('Podcast.form.type.label') ?></legend>
<label for="episodic" class="inline-flex items-center">
<?= form_radio(

View File

@ -13,6 +13,22 @@
]) ?>
<?= csrf_field() ?>
<?= form_label(lang('Podcast.form.image'), 'image') ?>
<img
src="<?= $podcast->image->thumbnail_url ?>"
alt="<?= $podcast->title ?>"
class="object-cover w-32 h-32"
/>
<?= 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('Podcast.form.title'), 'title') ?>
<?= form_input([
@ -66,16 +82,6 @@
) ?>
</div>
<?= form_label(lang('Podcast.form.image'), 'image') ?>
<img src="<?= $podcast->image_url ?>" alt="<?= $podcast->title ?>" class="object-cover w-32 h-32" />
<?= form_input([
'id' => 'image',
'name' => 'image',
'class' => 'form-input mb-4',
'type' => 'file',
'accept' => '.jpg,.jpeg,.png',
]) ?>
<?= form_label(lang('Podcast.form.language'), 'language') ?>
<?= form_dropdown(
'language',

View File

@ -7,28 +7,33 @@
<?= $this->section('content') ?>
<?= form_open_multipart(route_to('podcast_import'), [
<?= form_open_multipart(route_to('rzqr'), [
'method' => 'post',
'class' => 'flex flex-col max-w-md',
]) ?>
<?= csrf_field() ?>
<?= form_label(lang('Podcast.form_import.name'), 'name') ?>
<?= form_input([
'id' => 'name',
'name' => 'name',
'class' => 'form-input mb-4',
'value' => old('name'),
'required' => 'required',
]) ?>
<div class="flex flex-col mb-4">
<label for="name"><?= lang('Podcast.form_import.name') ?></label>
<input type="text" class="form-input" id="name" name="name" value="<?= old(
'name'
) ?>" required />
</div>
<div class="flex flex-col mb-4">
<label for="name"><?= lang(
'Podcast.form_import.imported_feed_url'
) ?></label>
<input type="text" class="form-input" id="imported_feed_url" name="imported_feed_url" value="<?= old(
'imported_feed_url'
) ?>" required />
</div>
<?= form_label(
lang('Podcast.form_import.imported_feed_url'),
'imported_feed_url'
) ?>
<?= form_input([
'id' => 'imported_feed_url',
'name' => 'imported_feed_url',
'class' => 'form-input mb-4',
'value' => old('imported_feed_url'),
'type' => 'url',
'required' => 'required',
]) ?>
<?= form_label(lang('Podcast.form.language'), 'language') ?>
<?= form_dropdown('language', $languageOptions, old('language', $browserLang), [
@ -44,9 +49,8 @@
'required' => 'required',
]) ?>
<?= form_fieldset(lang('Podcast.form_import.slug_field.label'), [
'class' => 'flex flex-col mb-4',
]) ?>
<?= form_fieldset('', ['class' => 'flex flex-col mb-4']) ?>
<legend><?= lang('Podcast.form_import.slug_field.label') ?></legend>
<label for="link" class="inline-flex items-center">
<?= form_radio(
['id' => 'link', 'name' => 'slug_field', 'class' => 'form-radio'],
@ -69,9 +73,8 @@
</label>
<?= form_fieldset_close() ?>
<?= form_fieldset(lang('Podcast.form_import.description_field.label'), [
'class' => 'flex flex-col mb-4',
]) ?>
<?= form_fieldset('', ['class' => 'flex flex-col mb-4']) ?>
<legend><?= lang('Podcast.form_import.description_field.label') ?></legend>
<label for="description" class="inline-flex items-center">
<?= form_radio(
[
@ -88,7 +91,7 @@
'Podcast.form_import.description_field.description'
) ?></span>
</label>
<label for="subtitle_summary" class="inline-flex items-center">
<label for="summary" class="inline-flex items-center">
<?= form_radio(
[
'id' => 'summary',
@ -136,27 +139,30 @@
<span class="ml-2"><?= lang('Podcast.form_import.force_renumber') ?></span>
</label>
<div class="flex flex-col mb-4">
<label for="name"><?= lang('Podcast.form_import.season_number') ?></label>
<input type="text" class="form-input" id="season_number" name="season_number" value="<?= old(
'season_number'
) ?>" />
</div>
<?= form_label(lang('Podcast.form_import.season_number'), 'season_number') ?>
<?= form_input([
'id' => 'season_number',
'name' => 'season_number',
'class' => 'form-input mb-4',
'value' => old('season_number'),
'type' => 'number',
]) ?>
<div class="flex flex-col mb-4">
<label for="max_episodes"><?= lang(
'Podcast.form_import.max_episodes'
) ?></label>
<input type="text" class="form-input" id="max_episodes" name="max_episodes" value="<?= old(
'max_episodes'
) ?>" />
</div>
<?= form_label(lang('Podcast.form_import.max_episodes'), 'max_episodes') ?>
<?= form_input([
'id' => 'max_episodes',
'name' => 'max_episodes',
'class' => 'form-input mb-4',
'value' => old('max_episodes'),
'type' => 'number',
]) ?>
<?= form_button([
'content' => lang('Podcast.form_import.submit_import'),
'type' => 'submit',
'class' => 'self-end px-4 py-2 bg-gray-200',
]) ?>
<button type="submit" name="submit" onsubmit="this.disabled=true; this.value='<?= lang(
'Podcast.form_import.submit_importing'
) ?>';" class="self-end px-4 py-2 bg-gray-200"><?= lang(
'Podcast.form_import.submit_import'
) ?></button>
<?= form_close() ?>

View File

@ -18,7 +18,11 @@
<?= $this->endSection() ?>
<?= $this->section('content') ?>
<img class="w-64 mb-4" src="<?= $podcast->image_url ?>" alt="<?= $podcast->title ?>" />
<img
class="w-64 mb-4"
src="<?= $podcast->image->medium_url ?>"
alt="<?= $podcast->title ?>"
/>
<a class="inline-flex px-2 py-1 mb-2 text-white bg-yellow-700 hover:bg-yellow-800" href="<?= route_to(
'contributor-list',
$podcast->id

View File

@ -1,6 +1,6 @@
<?= helper('page') ?>
<!DOCTYPE html>
<html lang="en">
<html lang="<?= $episode->podcast->language ?>">
<head>
<meta charset="UTF-8"/>
@ -14,14 +14,15 @@
<body class="flex flex-col min-h-screen mx-auto">
<header class="border-b bg-gradient-to-tr from-gray-900 to-gray-800">
<div class="container flex items-start px-2 py-2 mx-auto">
<img class="w-12 h-12 mr-2 rounded cover" src="<?= $episode->podcast
->image_url ?>" alt="<?= $episode->podcast->title ?>" />
<a href="<?= route_to(
'podcast',
$episode->podcast->name
) ?>" class="flex flex-col text-lg leading-tight text-white" title="<?= lang(
'Episode.back_to_podcast'
) ?>">
<img
class="w-12 h-12 mr-2 rounded cover"
src="<?= $episode->podcast->image->thumbnail_url ?>"
alt="<?= $episode->podcast->title ?>"
/>
<a
href="<?= route_to('podcast', $episode->podcast->name) ?>"
class="flex flex-col text-lg leading-tight text-white"
title="<?= lang('Episode.back_to_podcast') ?>">
<?= $episode->podcast->title ?>
<span class="text-sm text-gray-300">
@<?= $episode->podcast->name ?>
@ -57,7 +58,8 @@
<?php endif; ?>
</nav>
<header class="flex flex-col items-center px-4 md:items-stretch md:justify-center md:flex-row">
<img src="<?= $episode->image_url ?>" alt="<?= $episode->title ?>" class="object-cover w-full max-w-xs mb-2 rounded-lg md:mb-0 md:mr-4" />
<img src="<?= $episode->image->medium_url ?>"
alt="<?= $episode->title ?>" class="object-cover w-full max-w-xs mb-2 rounded-lg md:mb-0 md:mr-4" />
<div class="flex flex-col w-full max-w-sm">
<h1 class="text-lg font-semibold md:text-2xl"><?= $episode->title ?></h1>
<?php if ($episode->number): ?>

View File

@ -12,7 +12,9 @@
<?php foreach ($podcasts as $podcast): ?>
<a href="<?= route_to('podcast', $podcast->name) ?>">
<article class="w-48 h-full p-2 mb-4 mr-4 border shadow-sm hover:bg-gray-100 hover:shadow">
<img alt="<?= $podcast->title ?>" src="<?= $podcast->image_url ?>" class="object-cover w-full h-40 mb-2" />
<img alt="<?= $podcast->title ?>"
src="<?= $podcast->image->thumbnail_url ?>"
class="object-cover w-full h-40 mb-2" />
<h2 class="font-semibold leading-tight"><?= $podcast->title ?></h2>
<p class="text-gray-600">@<?= $podcast->name ?></p>
</article>

View File

@ -17,7 +17,8 @@
<main class="flex-1 bg-gray-200">
<header class="border-b bg-gradient-to-tr from-gray-900 to-gray-800">
<div class="flex flex-col items-center justify-center md:items-stretch md:mx-auto md:container md:py-12 md:flex-row ">
<img src="<?= $podcast->image_url ?>" alt="Podcast cover" class="object-cover w-full max-w-xs m-4 rounded-lg shadow-xl" />
<img src="<?= $podcast->image->medium_url ?>"
alt="<?= $podcast->title ?>" class="object-cover w-full max-w-xs m-4 rounded-lg shadow-xl" />
<div class="w-full p-4 bg-white md:w-auto md:text-white md:bg-transparent">
<h1 class="text-2xl font-semibold leading-tight"><?= $podcast->title ?> <span class="text-lg font-normal opacity-75">@<?= $podcast->name ?></span></h1>
<div class="flex items-center mb-4">
@ -99,7 +100,10 @@
</h1>
<?php foreach ($episodes as $episode): ?>
<article class="flex w-full max-w-lg p-4 mx-auto">
<img loading="lazy" src="<?= $episode->image_url ?>" alt="<?= $episode->title ?>" class="object-cover w-20 h-20 mr-2 rounded-lg" />
<img
loading="lazy"
src="<?= $episode->image->thumbnail_url ?>"
alt="<?= $episode->title ?>" class="object-cover w-20 h-20 mr-2 rounded-lg" />
<div class="flex flex-col flex-1">
<a class="text-sm hover:underline" href="<?= $episode->link ?>">
<h2 class="inline-flex justify-between w-full font-bold leading-none group">

56
composer.lock generated
View File

@ -12,12 +12,12 @@
"source": {
"type": "git",
"url": "https://github.com/codeigniter4/CodeIgniter4.git",
"reference": "9a7e826138bf8940ef8c7a25d59d67b1aebfe0ee"
"reference": "9b6eda2729d4a8912ccfe8f8c20587b21ff92ac4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/codeigniter4/CodeIgniter4/zipball/9a7e826138bf8940ef8c7a25d59d67b1aebfe0ee",
"reference": "9a7e826138bf8940ef8c7a25d59d67b1aebfe0ee",
"url": "https://api.github.com/repos/codeigniter4/CodeIgniter4/zipball/9b6eda2729d4a8912ccfe8f8c20587b21ff92ac4",
"reference": "9b6eda2729d4a8912ccfe8f8c20587b21ff92ac4",
"shasum": ""
},
"require": {
@ -34,7 +34,7 @@
"codeigniter4/codeigniter4-standard": "^1.0",
"fzaninotto/faker": "^1.9@dev",
"mikey179/vfsstream": "1.6.*",
"phpstan/phpstan": "^0.12.37",
"phpstan/phpstan": "^0.12",
"phpunit/phpunit": "^8.5",
"predis/predis": "^1.1",
"squizlabs/php_codesniffer": "^3.3"
@ -66,20 +66,20 @@
"slack": "https://codeigniterchat.slack.com",
"issues": "https://github.com/codeigniter4/CodeIgniter4/issues"
},
"time": "2020-08-17T14:11:23+00:00"
"time": "2020-09-07T16:29:38+00:00"
},
{
"name": "composer/ca-bundle",
"version": "1.2.7",
"version": "1.2.8",
"source": {
"type": "git",
"url": "https://github.com/composer/ca-bundle.git",
"reference": "95c63ab2117a72f48f5a55da9740a3273d45b7fd"
"reference": "8a7ecad675253e4654ea05505233285377405215"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/composer/ca-bundle/zipball/95c63ab2117a72f48f5a55da9740a3273d45b7fd",
"reference": "95c63ab2117a72f48f5a55da9740a3273d45b7fd",
"url": "https://api.github.com/repos/composer/ca-bundle/zipball/8a7ecad675253e4654ea05505233285377405215",
"reference": "8a7ecad675253e4654ea05505233285377405215",
"shasum": ""
},
"require": {
@ -127,12 +127,16 @@
"url": "https://packagist.com",
"type": "custom"
},
{
"url": "https://github.com/composer",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/composer/composer",
"type": "tidelift"
}
],
"time": "2020-04-08T08:27:21+00:00"
"time": "2020-08-23T12:54:47+00:00"
},
{
"name": "geoip2/geoip2",
@ -451,23 +455,23 @@
},
{
"name": "laminas/laminas-zendframework-bridge",
"version": "1.0.4",
"version": "1.1.0",
"source": {
"type": "git",
"url": "https://github.com/laminas/laminas-zendframework-bridge.git",
"reference": "fcd87520e4943d968557803919523772475e8ea3"
"reference": "4939c81f63a8a4968c108c440275c94955753b19"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laminas/laminas-zendframework-bridge/zipball/fcd87520e4943d968557803919523772475e8ea3",
"reference": "fcd87520e4943d968557803919523772475e8ea3",
"url": "https://api.github.com/repos/laminas/laminas-zendframework-bridge/zipball/4939c81f63a8a4968c108c440275c94955753b19",
"reference": "4939c81f63a8a4968c108c440275c94955753b19",
"shasum": ""
},
"require": {
"php": "^5.6 || ^7.0"
"php": "^5.6 || ^7.0 || ^8.0"
},
"require-dev": {
"phpunit/phpunit": "^5.7 || ^6.5 || ^7.5 || ^8.1",
"phpunit/phpunit": "^5.7 || ^6.5 || ^7.5 || ^8.1 || ^9.3",
"squizlabs/php_codesniffer": "^3.5"
},
"type": "library",
@ -505,20 +509,20 @@
"type": "community_bridge"
}
],
"time": "2020-05-20T16:45:56+00:00"
"time": "2020-08-18T16:34:51+00:00"
},
{
"name": "league/commonmark",
"version": "1.5.3",
"version": "1.5.4",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/commonmark.git",
"reference": "2574454b97e4103dc4e36917bd783b25624aefcd"
"reference": "21819c989e69bab07e933866ad30c7e3f32984ba"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/commonmark/zipball/2574454b97e4103dc4e36917bd783b25624aefcd",
"reference": "2574454b97e4103dc4e36917bd783b25624aefcd",
"url": "https://api.github.com/repos/thephpleague/commonmark/zipball/21819c989e69bab07e933866ad30c7e3f32984ba",
"reference": "21819c989e69bab07e933866ad30c7e3f32984ba",
"shasum": ""
},
"require": {
@ -600,7 +604,7 @@
"type": "tidelift"
}
],
"time": "2020-07-19T22:47:30+00:00"
"time": "2020-08-18T01:19:12+00:00"
},
{
"name": "league/html-to-markdown",
@ -796,12 +800,12 @@
"source": {
"type": "git",
"url": "https://github.com/lonnieezell/myth-auth.git",
"reference": "d9c9b0e4a8bea9ba6c847dcfefc6645bf1e1d694"
"reference": "e838cb8de6ffa118caf2b9909e71776a866c8973"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/lonnieezell/myth-auth/zipball/d9c9b0e4a8bea9ba6c847dcfefc6645bf1e1d694",
"reference": "d9c9b0e4a8bea9ba6c847dcfefc6645bf1e1d694",
"url": "https://api.github.com/repos/lonnieezell/myth-auth/zipball/e838cb8de6ffa118caf2b9909e71776a866c8973",
"reference": "e838cb8de6ffa118caf2b9909e71776a866c8973",
"shasum": ""
},
"require": {
@ -848,7 +852,7 @@
"type": "patreon"
}
],
"time": "2020-07-16T14:00:14+00:00"
"time": "2020-09-07T03:37:26+00:00"
},
{
"name": "phpoption/phpoption",

745
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -29,12 +29,12 @@
"prosemirror-example-setup": "^1.1.2",
"prosemirror-markdown": "^1.5.0",
"prosemirror-state": "^1.3.3",
"prosemirror-view": "^1.15.5"
"prosemirror-view": "^1.15.6"
},
"devDependencies": {
"@babel/core": "^7.11.4",
"@babel/core": "^7.11.6",
"@babel/plugin-proposal-class-properties": "^7.10.4",
"@babel/preset-env": "^7.11.0",
"@babel/preset-env": "^7.11.5",
"@babel/preset-typescript": "^7.10.4",
"@commitlint/cli": "^9.1.2",
"@commitlint/config-conventional": "^9.1.2",
@ -49,30 +49,30 @@
"@types/codemirror": "0.0.97",
"@types/prosemirror-markdown": "^1.0.3",
"@types/prosemirror-view": "^1.15.1",
"@typescript-eslint/eslint-plugin": "^3.10.1",
"@typescript-eslint/parser": "^3.10.1",
"@typescript-eslint/eslint-plugin": "^4.0.1",
"@typescript-eslint/parser": "^4.0.1",
"cross-env": "^7.0.2",
"cssnano": "^4.1.10",
"cz-conventional-changelog": "^3.3.0",
"eslint": "^7.7.0",
"eslint": "^7.8.1",
"eslint-config-prettier": "^6.11.0",
"eslint-plugin-prettier": "^3.1.4",
"husky": "^4.2.5",
"lint-staged": "^10.2.13",
"postcss-cli": "^7.1.1",
"lint-staged": "^10.3.0",
"postcss-cli": "^7.1.2",
"postcss-import": "^12.0.1",
"postcss-preset-env": "^6.7.0",
"prettier": "2.1.1",
"prettier-plugin-organize-imports": "^1.1.1",
"rollup": "^2.26.6",
"rollup": "^2.26.10",
"rollup-plugin-multi-input": "^1.1.1",
"rollup-plugin-node-polyfills": "^0.2.1",
"rollup-plugin-postcss": "^3.1.6",
"rollup-plugin-terser": "^7.0.0",
"stylelint": "^13.6.1",
"rollup-plugin-postcss": "^3.1.8",
"rollup-plugin-terser": "^7.0.1",
"stylelint": "^13.7.0",
"stylelint-config-standard": "^20.0.0",
"svgo": "^1.3.2",
"tailwindcss": "^1.7.5",
"tailwindcss": "^1.7.6",
"typescript": "^4.0.2"
},
"husky": {

View File

@ -18,7 +18,8 @@ Options All -Indexes
# Redirect Trailing Slashes...
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)/$ /$1 [L,R=301]
RewriteCond %{REQUEST_URI} (.+)/$
RewriteRule ^ %1 [L,R=301]
# Rewrite "www.example.com -> example.com"
RewriteCond %{HTTPS} !=on
@ -30,7 +31,7 @@ Options All -Indexes
# request to the front controller, index.php
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php/$1 [L]
RewriteRule ^([\s\S]*)$ index.php/$1 [L,NC,QSA]
# Ensure Authorization header is passed along
RewriteCond %{HTTP:Authorization} .
@ -45,4 +46,4 @@ Options All -Indexes
# Disable server signature start
ServerSignature Off
# Disable server signature end
# Disable server signature end