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
This commit is contained in:
Yassine Doghri 2021-11-01 17:12:03 +00:00
parent 5c56f3e6f0
commit 4a8147bfbb
79 changed files with 515 additions and 428 deletions

View File

@ -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;
/**
* 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 array $podcastCoverSizes = [
'tiny' => [40, 40],
'thumbnail' => [150, 150],
'medium' => [320, 320],
'large' => [1024, 1024],
'feed' => [1400, 1400],
'id3' => [500, 500],
];
/**
* Size of images linked in the rss feed (should be between 1400 and 3000)
* 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 $feedSize = 1400;
public array $podcastBannerSizes = [
'small' => [320, 128],
'medium' => [960, 320],
'large' => [1500, 500],
];
public string $podcastBannerDefaultPath = 'castopod-banner-default.jpg';
public string $podcastBannerDefaultMimeType = 'image/jpeg';
/**
* Size for ID3 tag cover art (should be between 300 and 800)
* 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 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 string $largeSuffix = '_large';
public string $feedSuffix = '_feed';
public string $id3Suffix = '_id3';
public array $personAvatarSizes = [
'tiny' => [40, 40],
'thumbnail' => [150, 150],
'medium' => [320, 320],
];
}

View File

@ -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 => [

View File

@ -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(

View File

@ -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,
];

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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;
}
/**

View File

@ -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,6 +70,45 @@ class Image extends Entity
$this->mimetype = $mimetype;
}
public function __get($property)
{
// 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}();
}
$fileSuffix = '';
if ($lastUnderscorePosition = strrpos($property, '_')) {
$fileSuffix = '_' . substr($property, 0, $lastUnderscorePosition);
}
$path = '';
if ($this->dirname !== '.') {
$path .= $this->dirname . '/';
}
$path .= $this->filename . $fileSuffix . '.' . $this->extension;
if (str_ends_with($property, 'url')) {
helper('media');
return media_base_url($path);
}
if (str_ends_with($property, 'path')) {
return $path;
}
}
public function getMimetype(): string
{
return $this->mimetype;
}
public function getFile(): File
{
if ($this->file === null) {
@ -90,104 +118,10 @@ class Image extends Entity
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');
return media_base_url($this->thumbnail_path);
}
public function getMediumPath(): string
{
return $this->dirname .
'/' .
$this->filename .
$this->config->mediumSuffix .
'.' .
$this->extension;
}
public function getMediumUrl(): string
{
helper('media');
return media_base_url($this->medium_path);
}
public function getLargePath(): string
{
return $this->dirname .
'/' .
$this->filename .
$this->config->largeSuffix .
'.' .
$this->extension;
}
public function getLargeUrl(): string
{
helper('media');
return media_base_url($this->large_path);
}
public function getFeedPath(): string
{
return $this->dirname .
'/' .
$this->filename .
$this->config->feedSuffix .
'.' .
$this->extension;
}
public function getFeedUrl(): string
{
helper('media');
return media_base_url($this->feed_path);
}
public function getId3Path(): string
{
return $this->dirname .
'/' .
$this->filename .
$this->config->id3Suffix .
'.' .
$this->extension;
}
public function getId3Url(): string
{
helper('media');
return media_base_url($this->id3_path);
}
public function saveImage(string $dirname, string $filename): void
/**
* @param array<string, int[]> $sizes
*/
public function saveImage(array $sizes, string $dirname, string $filename): void
{
helper('media');
@ -196,37 +130,27 @@ class Image extends Entity
save_media($this->file, $this->dirname, $this->filename);
$imageService = Services::image();
$imageService = service('image');
$thumbnailSize = $this->config->thumbnailSize;
$mediumSize = $this->config->mediumSize;
$largeSize = $this->config->largeSize;
$feedSize = $this->config->feedSize;
$id3Size = $this->config->id3Size;
foreach ($sizes as $name => $size) {
$pathProperty = $name . '_path';
$imageService
->withFile(media_path($this->path))
->resize($size[0], $size[1])
->save(media_path($this->{$pathProperty}));
}
}
$imageService
->withFile(media_path($this->path))
->resize($thumbnailSize, $thumbnailSize)
->save(media_path($this->thumbnail_path));
/**
* @param array<string, int[]> $sizes
*/
public function delete(array $sizes): void
{
helper('media');
$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}));
}
}
}

View File

@ -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']);
return new Image(null, $this->attributes['avatar_path'], $this->attributes['avatar_mimetype']);
}
/**

View File

@ -34,9 +34,12 @@ 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 $image_path
* @property string $image_mimetype
* @property Image $cover
* @property string $cover_path
* @property string $cover_mimetype
* @property Image|null $banner
* @property string|null $banner_path
* @property string|null $banner_mimetype
* @property string $language_code
* @property int $category_id
* @property Category|null $category
@ -84,7 +87,9 @@ class Podcast extends Entity
protected ?Actor $actor = null;
protected Image $image;
protected Image $cover;
protected ?Image $banner;
protected ?string $description = null;
@ -145,8 +150,10 @@ class Podcast extends Entity
'title' => 'string',
'description_markdown' => 'string',
'description_html' => 'string',
'image_path' => 'string',
'image_mimetype' => 'string',
'cover_path' => 'string',
'cover_mimetype' => 'string',
'banner_path' => '?string',
'banner_mimetype' => '?string',
'language_code' => 'string',
'category_id' => 'integer',
'parental_advisory' => '?string',
@ -189,22 +196,63 @@ class Podcast extends Entity
}
/**
* Saves a cover image to the corresponding podcast folder in `public/media/podcast_name/`
* Saves a podcast cover to the corresponding podcast folder in `public/media/podcast_name/`
*/
public function setImage(Image $image): static
public function setCover(Image $cover): static
{
// Save image
$image->saveImage('podcasts/' . $this->attributes['handle'], 'cover');
$cover->saveImage(config('Images')->podcastCoverSizes, 'podcasts/' . $this->attributes['handle'], 'cover');
$this->attributes['image_mimetype'] = $image->mimetype;
$this->attributes['image_path'] = $image->path;
$this->attributes['cover_path'] = $cover->path;
$this->attributes['cover_mimetype'] = $cover->mimetype;
return $this;
}
public function getImage(): Image
public function getCover(): Image
{
return new Image(null, $this->image_path, $this->image_mimetype);
return new Image(null, $this->cover_path, $this->cover_mimetype);
}
/**
* Saves a podcast cover to the corresponding podcast folder in `public/media/podcast_name/`
*/
public function setBanner(?Image $banner): static
{
if ($banner === null) {
$this->attributes['banner_path'] = null;
$this->attributes['banner_mimetype'] = null;
return $this;
}
// Save image
$banner->saveImage(
config('Images')
->podcastBannerSizes,
'podcasts/' . $this->attributes['handle'],
'banner'
);
$this->attributes['banner_path'] = $banner->path;
$this->attributes['banner_mimetype'] = $banner->mimetype;
return $this;
}
public function getBanner(): Image
{
if ($this->attributes['banner_path'] === null) {
return new Image(
null,
config('Images')
->podcastBannerDefaultPath,
config('Images')
->podcastBannerDefaultMimeType
);
}
return new Image(null, $this->banner_path, $this->banner_mimetype);
}
public function getLink(): string

View File

@ -51,7 +51,7 @@ if (! function_exists('write_audio_file_tags')) {
$tagwriter->tagformats = ['id3v2.4'];
$tagwriter->tag_encoding = $TextEncoding;
$cover = new File(media_path($episode->image->id3_path));
$cover = new File(media_path($episode->cover->id3_path));
$APICdata = file_get_contents($cover->getRealPath());

View File

@ -56,8 +56,7 @@ if (! function_exists('get_rss_feed')) {
$itunesImage = $channel->addChild('image', null, $itunesNamespace);
// FIXME: This should be downsized to 1400x1400
$itunesImage->addAttribute('href', $podcast->image->url);
$itunesImage->addAttribute('href', $podcast->cover->feed_url);
$channel->addChild('language', $podcast->language_code);
if ($podcast->location !== null) {
@ -134,7 +133,7 @@ if (! function_exists('get_rss_feed')) {
$podcastNamespace,
);
$personElement->addAttribute('img', $person->image->medium_url);
$personElement->addAttribute('img', $person->avatar->medium_url);
if ($person->information_url !== null) {
$personElement->addAttribute('href', $person->information_url);
@ -190,7 +189,7 @@ if (! function_exists('get_rss_feed')) {
}
$image = $channel->addChild('image');
$image->addChild('url', $podcast->image->feed_url);
$image->addChild('url', $podcast->cover->feed_url);
$image->addChild('title', $podcast->title, null, false);
$image->addChild('link', $podcast->link);
@ -234,7 +233,7 @@ if (! function_exists('get_rss_feed')) {
$item->addChild('duration', (string) $episode->audio_file_duration, $itunesNamespace);
$item->addChild('link', $episode->link);
$episodeItunesImage = $item->addChild('image', null, $itunesNamespace);
$episodeItunesImage->addAttribute('href', $episode->image->feed_url);
$episodeItunesImage->addAttribute('href', $episode->cover->feed_url);
$episode->parental_advisory &&
$item->addChild(
@ -300,7 +299,7 @@ if (! function_exists('get_rss_feed')) {
htmlspecialchars(lang("PersonsTaxonomy.persons.{$role->group}.label", [], 'en')),
);
$personElement->addAttribute('img', $person->image->medium_url);
$personElement->addAttribute('img', $person->avatar->medium_url);
if ($person->information_url !== null) {
$personElement->addAttribute('href', $person->information_url);

View File

@ -52,8 +52,8 @@ class PodcastEpisode extends ObjectType
$this->image = [
'type' => 'Image',
'mediaType' => $episode->image_mimetype,
'url' => $episode->image->url,
'mediaType' => $episode->cover_mimetype,
'url' => $episode->cover->feed_url,
];
// add audio file

View File

@ -75,8 +75,8 @@ class EpisodeModel extends Model
'audio_file_header_size',
'description_markdown',
'description_html',
'image_path',
'image_mimetype',
'cover_path',
'cover_mimetype',
'transcript_file_path',
'transcript_file_remote_url',
'chapters_file_path',

View File

@ -35,8 +35,8 @@ class PersonModel extends Model
'full_name',
'unique_name',
'information_url',
'image_path',
'image_mimetype',
'avatar_path',
'avatar_mimetype',
'created_by',
'updated_by',
];

View File

@ -10,7 +10,6 @@ declare(strict_types=1);
namespace App\Models;
use App\Entities\Actor;
use App\Entities\Podcast;
use CodeIgniter\Database\Query;
use CodeIgniter\HTTP\URI;
@ -41,8 +40,10 @@ class PodcastModel extends Model
'description_html',
'episode_description_footer_markdown',
'episode_description_footer_html',
'image_path',
'image_mimetype',
'cover_path',
'cover_mimetype',
'banner_path',
'banner_mimetype',
'language_code',
'category_id',
'parental_advisory',
@ -91,7 +92,7 @@ class PodcastModel extends Model
'handle' =>
'required|regex_match[/^[a-zA-Z0-9\_]{1,32}$/]|is_unique[podcasts.handle,id,{id}]',
'description_markdown' => 'required',
'image_path' => 'required',
'cover_path' => 'required',
'language_code' => 'required',
'category_id' => 'required',
'owner_email' => 'required|valid_email',
@ -454,13 +455,16 @@ class PodcastModel extends Model
{
$podcast = (new self())->getPodcastById(is_array($data['id']) ? $data['id'][0] : $data['id']);
$podcastActor = (new ActorModel())->find($podcast->actor_id);
if ($podcast !== null) {
$podcastActor = (new ActorModel())->find($podcast->actor_id);
$podcastActor->avatar_image_url = $podcast->image->thumbnail_url;
$podcastActor->avatar_image_mimetype = $podcast->image_mimetype;
(new ActorModel())->update($podcast->actor_id, $podcastActor);
if ($podcastActor) {
$podcastActor->avatar_image_url = $podcast->cover->thumbnail_url;
$podcastActor->avatar_image_mimetype = $podcast->cover_mimetype;
(new ActorModel())->update($podcast->actor_id, $podcastActor);
}
}
return $data;
}
@ -477,14 +481,18 @@ class PodcastModel extends Model
$actorModel = new ActorModel();
$actor = $actorModel->getActorById($podcast->actor_id);
// update values
$actor->display_name = $podcast->title;
$actor->summary = $podcast->description_html;
$actor->avatar_image_url = $podcast->image->thumbnail_url;
$actor->avatar_image_mimetype = $podcast->image_mimetype;
if ($actor !== null) {
// update values
$actor->display_name = $podcast->title;
$actor->summary = $podcast->description_html;
$actor->avatar_image_url = $podcast->cover->thumbnail_url;
$actor->avatar_image_mimetype = $podcast->cover->mimetype;
$actor->cover_image_url = $podcast->banner->large_url;
$actor->cover_image_mimetype = $podcast->banner->mimetype;
if ($actor->hasChanged()) {
$actorModel->update($actor->id, $actor);
if ($actor->hasChanged()) {
$actorModel->update($actor->id, $actor);
}
}
}

View File

@ -47,7 +47,7 @@ const drawEpisodesMap = async (mapDivId: string, dataUrl: string) => {
data[i].longitude,
]).bindPopup(
'<div class="flex min-w-max"><img src="' +
data[i].image_path +
data[i].cover_path +
'" alt="' +
data[i].episode_title +
'" class="mr-2 rounded w-16 h-16" /><div class="flex flex-col"><h2 class="lg:text-base text-sm ! font-bold"><a href="' +

View File

@ -38,4 +38,19 @@
hsla(0, 0%, 0%, 0.8) 100%
);
}
.backdrop-gradient-pine {
background-image: linear-gradient(
180deg,
hsla(172, 100%, 17%, 0) 0%,
hsla(172, 100%, 17%, 0.034375) 16.36%,
hsla(172, 100%, 17%, 0.125) 33.34%,
hsla(172, 100%, 17%, 0.253125) 50.1%,
hsla(172, 100%, 17%, 0.4) 65.75%,
hsla(172, 100%, 17%, 0.546875) 79.43%,
hsla(172, 100%, 17%, 0.675) 90.28%,
hsla(172, 100%, 17%, 0.765625) 97.43%,
hsla(172, 100%, 17%, 0.8) 100%
);
}
}

View File

@ -57,9 +57,9 @@ class FileRules extends ValidationFileRules
//--------------------------------------------------------------------
/**
* Checks an uploaded file to verify that the image ratio is of 1:1
* Checks an uploaded image to verify that the ratio corresponds to the params
*/
public function is_image_squared(string $blank = null, string $params): bool
public function is_image_ratio(string $blank = null, string $params): bool
{
// Grab the file name off the top of the $params
// after we split it.
@ -79,12 +79,14 @@ class FileRules extends ValidationFileRules
return true;
}
// Get uploaded image size
$info = getimagesize($file->getTempName());
$fileWidth = $info[0];
$fileHeight = $info[1];
// Get Parameter sizes
$x = $params[0] ?? 1;
$y = $params[1] ?? 1;
if ($fileWidth !== $fileHeight) {
// Get uploaded image size
[0 => $fileWidth, 1 => $fileHeight] = getimagesize($file->getTempName());
if (($x / $y) !== ($fileWidth / $fileHeight)) {
return false;
}
}

View File

@ -10,7 +10,7 @@ class Input extends FormComponent
public function render(): string
{
$baseClass = 'bg-white border-black rounded-lg focus:border-black border-3 focus:ring-castopod focus-within:ring-castopod ' . $this->class;
$baseClass = 'w-full bg-white border-black rounded-lg focus:border-black border-3 focus:ring-castopod focus-within:ring-castopod ' . $this->class;
$this->attributes['class'] = $baseClass;

View File

@ -13,7 +13,7 @@ class MarkdownEditor extends FormComponent
$this->attributes['class'] = 'border-none focus:border-none focus:outline-none focus:ring-0 w-full h-full';
$this->attributes['rows'] = 6;
$textarea = form_textarea($this->attributes, old($this->name, html_entity_decode($this->value), null));
$textarea = form_textarea($this->attributes, old($this->name, html_entity_decode($this->value), false));
$icons = [
'heading' => icon('heading'),
'bold' => icon('bold'),

View File

@ -17,7 +17,7 @@ class Textarea extends FormComponent
{
unset($this->attributes['value']);
$this->attributes['class'] = 'focus:border-black focus:ring-castopod rounded-lg border-3 border-black ' . $this->class;
$this->attributes['class'] = 'w-full focus:border-black focus:ring-castopod rounded-lg border-3 border-black ' . $this->class;
$textarea = form_textarea(
$this->attributes,

View File

@ -36,7 +36,7 @@ class Toggler extends FormComponent
$this->attributes['class'] = 'form-switch';
$checkbox = form_checkbox($this->attributes, $this->value, old($this->name, $this->checked));
$checkbox = form_checkbox($this->attributes, $this->value, old($this->name) === 'yes' ? true : $this->checked);
$hint = $this->hint === '' ? '' : hint_tooltip($this->hint, 'ml-1');
return <<<HTML
<label class="relative inline-flex items-center {$wrapperClass}">

View File

@ -98,6 +98,10 @@ $routes->group(
$routes->post('edit', 'PodcastController::attemptEdit/$1', [
'filter' => 'permission:podcast-edit',
]);
$routes->get('edit/delete-banner', 'PodcastController::deleteBanner/$1', [
'as' => 'podcast-banner-delete',
'filter' => 'permission:podcast-edit',
]);
$routes->get('delete', 'PodcastController::delete/$1', [
'as' => 'podcast-delete',
'filter' => 'permission:podcasts-delete',

View File

@ -112,8 +112,8 @@ class EpisodeController extends BaseController
{
$rules = [
'audio_file' => 'uploaded[audio_file]|ext_in[audio_file,mp3,m4a]',
'image' =>
'is_image[image]|ext_in[image,jpg,png]|min_dims[image,1400,1400]|is_image_squared[image]',
'cover' =>
'is_image[cover]|ext_in[cover,jpg,png]|min_dims[cover,1400,1400]|is_image_ratio[cover,1,1]',
'transcript_file' =>
'ext_in[transcript,txt,html,srt,json]|permit_empty',
'chapters_file' => 'ext_in[chapters,json]|permit_empty',
@ -126,12 +126,6 @@ class EpisodeController extends BaseController
->with('errors', $this->validator->getErrors());
}
$image = null;
$imageFile = $this->request->getFile('image');
if ($imageFile !== null && $imageFile->isValid()) {
$image = new Image($imageFile);
}
$newEpisode = new Episode([
'podcast_id' => $this->podcast->id,
'title' => $this->request->getPost('title'),
@ -139,7 +133,6 @@ class EpisodeController extends BaseController
'guid' => null,
'audio_file' => $this->request->getFile('audio_file'),
'description_markdown' => $this->request->getPost('description'),
'image' => $image,
'location' => $this->request->getPost('location_name') === '' ? null : new Location($this->request->getPost(
'location_name'
)),
@ -163,6 +156,11 @@ class EpisodeController extends BaseController
'published_at' => null,
]);
$coverFile = $this->request->getFile('cover');
if ($coverFile !== null && $coverFile->isValid()) {
$newEpisode->cover = new Image($coverFile);
}
$transcriptChoice = $this->request->getPost('transcript-choice');
if (
$transcriptChoice === 'upload-file'
@ -238,8 +236,8 @@ class EpisodeController extends BaseController
$rules = [
'audio_file' =>
'uploaded[audio_file]|ext_in[audio_file,mp3,m4a]|permit_empty',
'image' =>
'is_image[image]|ext_in[image,jpg,png]|min_dims[image,1400,1400]|is_image_squared[image]',
'cover' =>
'is_image[cover]|ext_in[cover,jpg,png]|min_dims[cover,1400,1400]|is_image_ratio[cover,1,1]',
'transcript_file' =>
'ext_in[transcript_file,txt,html,srt,json]|permit_empty',
'chapters_file' => 'ext_in[chapters_file,json]|permit_empty',
@ -279,9 +277,9 @@ class EpisodeController extends BaseController
$this->episode->audio_file = $audioFile;
}
$imageFile = $this->request->getFile('image');
if ($imageFile !== null && $imageFile->isValid()) {
$this->episode->image = new Image($imageFile);
$coverFile = $this->request->getFile('cover');
if ($coverFile !== null && $coverFile->isValid()) {
$this->episode->cover = new Image($coverFile);
}
$transcriptChoice = $this->request->getPost('transcript-choice');

View File

@ -66,8 +66,8 @@ class PersonController extends BaseController
public function attemptCreate(): RedirectResponse
{
$rules = [
'image' =>
'is_image[image]|ext_in[image,jpg,jpeg,png]|min_dims[image,400,400]|is_image_squared[image]',
'avatar' =>
'is_image[avatar]|ext_in[avatar,jpg,jpeg,png]|min_dims[avatar,400,400]|is_image_ratio[avatar,1,1]',
];
if (! $this->validate($rules)) {
@ -85,9 +85,9 @@ class PersonController extends BaseController
'updated_by' => user_id(),
]);
$imageFile = $this->request->getFile('image');
if ($imageFile !== null && $imageFile->isValid()) {
$person->image = new Image($imageFile);
$avatarFile = $this->request->getFile('avatar');
if ($avatarFile !== null && $avatarFile->isValid()) {
$person->avatar = new Image($avatarFile);
}
$personModel = new PersonModel();
@ -119,8 +119,8 @@ class PersonController extends BaseController
public function attemptEdit(): RedirectResponse
{
$rules = [
'image' =>
'is_image[image]|ext_in[image,jpg,jpeg,png]|min_dims[image,400,400]|is_image_squared[image]',
'avatar' =>
'is_image[avatar]|ext_in[avatar,jpg,jpeg,png]|min_dims[avatar,400,400]|is_image_ratio[avatar,1,1]',
];
if (! $this->validate($rules)) {
@ -134,9 +134,9 @@ class PersonController extends BaseController
$this->person->unique_name = $this->request->getPost('unique_name');
$this->person->information_url = $this->request->getPost('information_url');
$imageFile = $this->request->getFile('image');
if ($imageFile !== null && $imageFile->isValid()) {
$this->person->image = new Image($imageFile);
$avatarFile = $this->request->getFile('avatar');
if ($avatarFile !== null && $avatarFile->isValid()) {
$this->person->avatar = new Image($avatarFile);
}
$this->person->updated_by = user_id();

View File

@ -171,8 +171,9 @@ class PodcastController extends BaseController
public function attemptCreate(): RedirectResponse
{
$rules = [
'image' =>
'uploaded[image]|is_image[image]|ext_in[image,jpg,png]|min_dims[image,1400,1400]|is_image_squared[image]',
'cover' =>
'uploaded[cover]|is_image[cover]|ext_in[cover,jpg,png]|min_dims[cover,1400,1400]|is_image_ratio[cover,1,1]',
'banner' => 'is_image[banner]|ext_in[banner,jpg,png]|min_dims[banner,1500,500]|is_image_ratio[banner,3,1]',
];
if (! $this->validate($rules)) {
@ -195,7 +196,7 @@ class PodcastController extends BaseController
'title' => $this->request->getPost('title'),
'handle' => $this->request->getPost('handle'),
'description_markdown' => $this->request->getPost('description'),
'image' => new Image($this->request->getFile('image')),
'cover' => new Image($this->request->getFile('cover')),
'language_code' => $this->request->getPost('language'),
'category_id' => $this->request->getPost('category'),
'parental_advisory' =>
@ -224,6 +225,11 @@ class PodcastController extends BaseController
'updated_by' => user_id(),
]);
$bannerFile = $this->request->getFile('banner');
if ($bannerFile !== null && $bannerFile->isValid()) {
$podcast->banner = new Image($bannerFile);
}
$podcastModel = new PodcastModel();
$db = db_connect();
@ -279,8 +285,9 @@ class PodcastController extends BaseController
public function attemptEdit(): RedirectResponse
{
$rules = [
'image' =>
'is_image[image]|ext_in[image,jpg,png]|min_dims[image,1400,1400]|is_image_squared[image]',
'cover' =>
'is_image[cover]|ext_in[cover,jpg,png]|min_dims[cover,1400,1400]|is_image_ratio[cover,1,1]',
'banner' => 'is_image[banner]|ext_in[banner,jpg,png]|min_dims[banner,1500,500]|is_image_ratio[banner,3,1]',
];
if (! $this->validate($rules)) {
@ -302,9 +309,13 @@ class PodcastController extends BaseController
$this->podcast->title = $this->request->getPost('title');
$this->podcast->description_markdown = $this->request->getPost('description');
$image = $this->request->getFile('image');
if ($image !== null && $image->isValid()) {
$this->podcast->image = new Image($image);
$coverFile = $this->request->getFile('cover');
if ($coverFile !== null && $coverFile->isValid()) {
$this->podcast->cover = new Image($coverFile);
}
$bannerFile = $this->request->getFile('banner');
if ($bannerFile !== null && $bannerFile->isValid()) {
$this->podcast->banner = new Image($bannerFile);
}
$this->podcast->language_code = $this->request->getPost('language');
$this->podcast->category_id = $this->request->getPost('category');
@ -356,6 +367,28 @@ class PodcastController extends BaseController
return redirect()->route('podcast-view', [$this->podcast->id]);
}
public function deleteBanner(): RedirectResponse
{
if ($this->podcast->banner === null) {
return redirect()->back();
}
$this->podcast->banner->delete(config('Images')->podcastBannerSizes);
$this->podcast->banner_path = null;
$this->podcast->banner_mimetype = null;
$podcastModel = new PodcastModel();
if (! $podcastModel->update($this->podcast->id, $this->podcast)) {
return redirect()
->back()
->withInput()
->with('errors', $podcastModel->errors());
}
return redirect()->back();
}
public function latestEpisodes(int $limit, int $podcastId): string
{
$episodes = (new EpisodeModel())

View File

@ -115,9 +115,9 @@ class PodcastImportController extends BaseController
property_exists($nsItunes, 'image') && $nsItunes->image !== null &&
$nsItunes->image->attributes()['href'] !== null
) {
$imageFile = download_file((string) $nsItunes->image->attributes()['href']);
$coverFile = download_file((string) $nsItunes->image->attributes()['href']);
} else {
$imageFile = download_file((string) $feed->channel[0]->image->url);
$coverFile = download_file((string) $feed->channel[0]->image->url);
}
$location = null;
@ -140,7 +140,8 @@ class PodcastImportController extends BaseController
'title' => (string) $feed->channel[0]->title,
'description_markdown' => $converter->convert($channelDescriptionHtml),
'description_html' => $channelDescriptionHtml,
'image' => new Image($imageFile),
'cover' => new Image($coverFile),
'banner' => null,
'language_code' => $this->request->getPost('language'),
'category_id' => $this->request->getPost('category'),
'parental_advisory' =>
@ -249,7 +250,7 @@ class PodcastImportController extends BaseController
'full_name' => $fullName,
'unique_name' => slugify($fullName),
'information_url' => $podcastPerson->attributes()['href'],
'image' => new Image(download_file((string) $podcastPerson->attributes()['img'])),
'avatar' => new Image(download_file((string) $podcastPerson->attributes()['img'])),
'created_by' => user_id(),
'updated_by' => user_id(),
]);
@ -326,9 +327,9 @@ class PodcastImportController extends BaseController
property_exists($nsItunes, 'image') && $nsItunes->image !== null &&
$nsItunes->image->attributes()['href'] !== null
) {
$episodeImage = new Image(download_file((string) $nsItunes->image->attributes()['href']));
$episodeCover = new Image(download_file((string) $nsItunes->image->attributes()['href']));
} else {
$episodeImage = null;
$episodeCover = null;
}
$location = null;
@ -351,7 +352,7 @@ class PodcastImportController extends BaseController
),
'description_markdown' => $converter->convert($itemDescriptionHtml),
'description_html' => $itemDescriptionHtml,
'image' => $episodeImage,
'cover' => $episodeCover,
'parental_advisory' =>
property_exists($nsItunes, 'explicit') && $nsItunes->explicit !== null
? (in_array((string) $nsItunes->explicit, ['yes', 'true'], true)
@ -402,7 +403,7 @@ class PodcastImportController extends BaseController
'full_name' => $fullName,
'unique_name' => slugify($fullName),
'information_url' => $episodePerson->attributes()['href'],
'image' => new Image(download_file((string) $episodePerson->attributes()['img'])),
'avatar' => new Image(download_file((string) $episodePerson->attributes()['img'])),
'created_by' => user_id(),
'updated_by' => user_id(),
]);

View File

@ -25,7 +25,7 @@ class SettingsController extends BaseController
{
$rules = [
'site_icon' =>
'is_image[site_icon]|ext_in[site_icon,png,jpeg]|is_image_squared[site_icon]|min_dims[image,512,512]|permit_empty',
'is_image[site_icon]|ext_in[site_icon,png,jpeg]|is_image_ratio[site_icon,1,1]|min_dims[image,512,512]|permit_empty',
];
if (! $this->validate($rules)) {

View File

@ -38,8 +38,6 @@ return [
'noChoicesText' => 'No choices to choose from',
'maxItemText' => 'Cannot add more items',
],
'image_size_hint' =>
'Image must be squared with at least 1400px wide and tall.',
'upload_file' => 'Upload a file',
'remote_url' => 'Remote URL',
],

View File

@ -49,10 +49,10 @@ return [
'audio_file' => 'Audio file',
'audio_file_hint' => 'Choose an .mp3 or .m4a audio file.',
'info_section_title' => 'Episode info',
'info_section_subtitle' => '',
'image' => 'Cover image',
'image_hint' =>
'If you do not set an image, the podcast cover will be used instead.',
'cover' => 'Episode cover',
'cover_hint' =>
'If you do not set a cover, the podcast cover will be used instead.',
'cover_size_hint' => 'Cover must be squared with at least 1400px wide and tall.',
'title' => 'Title',
'title_hint' =>
'Should contain a clear and concise episode name. Do not specify the episode or season numbers here.',

View File

@ -17,9 +17,9 @@ return [
'edit' => 'Edit person',
'delete' => 'Delete person',
'form' => [
'image' => 'Picture',
'image_size_hint' =>
'Image must be squared with at least 400px wide and tall.',
'avatar' => 'Avatar',
'avatar_size_hint' =>
'Avatar must be squared with at least 400px wide and tall.',
'full_name' => 'Full name',
'full_name_hint' => 'This is the full name or alias of the person.',
'unique_name' => 'Unique name',

View File

@ -25,7 +25,11 @@ return [
'form' => [
'identity_section_title' => 'Podcast identity',
'identity_section_subtitle' => 'These fields allow you to get noticed.',
'image' => 'Cover image',
'cover' => 'Podcast cover',
'cover_size_hint' => 'Cover must be squared with at least 1400px wide and tall.',
'banner' => 'Podcast banner',
'banner_size_hint' => 'Banner must have a 3:1 ratio with at least 1500px wide.',
'banner_delete' => 'Delete podcast banner',
'title' => 'Title',
'handle' => 'Handle',
'handle_hint' =>

View File

@ -11,8 +11,8 @@ declare(strict_types=1);
return [
'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).',
'is_image_ratio' =>
'{field} is either not an image or not of the right ratio.',
'validate_url' =>
'The {field} field must be a valid URL (eg. https://example.com/).',
];

View File

@ -38,8 +38,6 @@ return [
'noChoicesText' => 'Aucune sélection possible',
'maxItemText' => 'Impossible de rajouter un élément',
],
'image_size_hint' =>
'Limage doit être carrée, avec au minimum 1400px de long et de large.',
'upload_file' => 'Téléversez un fichier',
'remote_url' => 'URL distante',
],

View File

@ -50,10 +50,10 @@ return [
'audio_file' => 'Fichier audio',
'audio_file_hint' => 'Sélectionnez un fichier audio .mp3 ou .m4a.',
'info_section_title' => 'Informations épisode',
'info_section_subtitle' => '',
'image' => 'Image de couverture',
'image_hint' =>
'cover' => 'Image de couverture',
'cover_hint' =>
'Si vous ne définissez pas dimage, celle du podcast sera utilisée à la place.',
'cover_size_hint' => 'La couverture de lépisode doit être carrée, avec au minimum 1400px de largeur et de hauteur.',
'title' => 'Titre',
'title_hint' =>
'Doit contenir un titre dépisode clair et concis. Ne précisez ici aucun numéro de saison ou dépisode.',

View File

@ -17,8 +17,8 @@ return [
'edit' => 'Modifier lintervenant',
'delete' => 'Supprimer lintervenant',
'form' => [
'image' => 'Photo',
'image_size_hint' =>
'avatar' => 'Avatar',
'avatar_size_hint' =>
'Limage doit être carrée et avoir au moins 400px de largeur et de hauteur.',
'full_name' => 'Nom complet',
'full_name_hint' => 'Le nom complet ou le pseudonyme de lintervenant',

View File

@ -26,7 +26,11 @@ return [
'identity_section_title' => 'Informations sur le Podcast',
'identity_section_subtitle' =>
'Ces champs vous permettent de vous faire remarquer.',
'image' => 'Image de couverture',
'cover' => 'Couverture du podcast',
'cover_size_hint' => 'La couverture du podcast doit être carrée, avec au minimum 1400px de largeur et de hauteur.',
'banner' => 'Bannière du podcast',
'banner_size_hint' => 'La bannière doit être au format 3/1, avec au minimum 1500px de largeur.',
'banner_delete' => 'Supprimer la bannière du podcast',
'title' => 'Titre',
'handle' => 'Identifiant',
'handle_hint' =>

View File

@ -16,7 +16,7 @@ return [
'site_icon' => 'Favicon du site',
'site_icon_delete' => 'Supprimer la favicon du site',
'site_icon_hint' => 'Les favicons sont ce que vous voyez sur les onglets de votre navigateur, dans votre barre de favoris, et lorsque vous ajoutez un site web en raccourci sur des appareils mobiles.',
'site_icon_helper' => 'La favicon doit être carrée et avoir au moins 512px de largeur et de hauteur.',
'site_icon_helper' => 'La favicon doit être carrée, avec au minimum 512px de largeur et de hauteur.',
'site_name' => 'Titre du site',
'site_description' => 'Description du site',
'submit' => 'Save',

View File

@ -11,8 +11,8 @@ declare(strict_types=1);
return [
'min_dims' =>
'{field} nest pas une image ou na pas la taille minimale requise.',
'is_image_squared' =>
'{field} nest pas une image ou nest pas carré (largeur et hauteur différentes).',
'is_image_ratio' =>
'{field} nest pas une image ou nest pas au bon format.',
'validate_url' =>
'Le champs {field} doit être une adresse valide (par exemple https://exemple.com/).',
];

View File

@ -28,6 +28,7 @@ parameters:
- '#Function \"preg_.*\(\)\" cannot be used/left in the code#'
- '#Function "property_exists\(\)" cannot be used/left in the code#'
- '#Instead of "instanceof/is_a\(\)" use ReflectionProvider service or "\(new ObjectType\(<desired_type\>\)\)\-\>isSuperTypeOf\(<element_type\>\)" for static reflection to work#'
- '#^Access to an undefined property App\\Entities\\Image#'
-
message: '#Function "function_exists\(\)" cannot be used/left in the code#'
paths:

View File

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -36,7 +36,7 @@
$interactButtons .= <<<CODE_SAMPLE
<button class="inline-flex items-center w-full px-4 py-1 hover:bg-gray-100" id="interact-as-actor-{$userPodcast->id}" name="actor_id" value="{$userPodcast->actor_id}">
<span class="inline-flex items-center flex-1 text-sm"><img src="{$userPodcast->image->thumbnail_url}" class="w-6 h-6 mr-2 rounded-full" />{$userPodcast->title}{$checkMark}</span>
<span class="inline-flex items-center flex-1 text-sm"><img src="{$userPodcast->cover->tiny_url}" class="w-6 h-6 mr-2 rounded-full" />{$userPodcast->title}{$checkMark}</span>
</button>
CODE_SAMPLE;
}

View File

@ -2,7 +2,7 @@
<a href="<?= route_to('episode-view', $episode->podcast->id, $episode->id) ?>" class="flex flex-col justify-end w-full h-full text-white group">
<div class="absolute bottom-0 left-0 z-10 w-full h-full backdrop-gradient"></div>
<div class="w-full h-full overflow-hidden">
<img src="<?= $episode->image->medium_url ?>" alt="<?= $episode->title ?>" class="object-cover w-full h-full transition duration-200 ease-in-out transform group-focus:scale-105 group-hover:scale-105" />
<img src="<?= $episode->cover->medium_url ?>" alt="<?= $episode->title ?>" class="object-cover w-full h-full transition duration-200 ease-in-out transform group-focus:scale-105 group-hover:scale-105" />
</div>
<?= publication_pill($episode->published_at, $episode->publication_status, 'absolute top-0 left-0 ml-2 mt-2 text-sm'); ?>
<div class="absolute z-20 flex flex-col items-start px-4 py-2">

View File

@ -10,7 +10,7 @@ $podcastNavigation = [
<a href="<?= route_to('podcast-view', $podcast->id) ?>" class="flex items-center px-4 py-2 border-b border-pine-900 focus:ring-inset focus:ring-castopod">
<?= icon('arrow-left', 'mr-2') ?>
<img
src="<?= $podcast->image->thumbnail_url ?>"
src="<?= $podcast->cover->tiny_url ?>"
alt="<?= $podcast->title ?>"
class="object-cover w-6 h-6 rounded"
/>
@ -18,7 +18,7 @@ $podcastNavigation = [
</a>
<div class="flex items-center px-4 py-2 border-b border-pine-900">
<img
src="<?= $episode->image->thumbnail_url ?>"
src="<?= $episode->cover->thumbnail_url ?>"
alt="<?= $episode->title ?>"
class="object-cover w-16 h-16 rounded"
/>

View File

@ -28,10 +28,10 @@
required="true" />
<Forms.Field
name="image"
label="<?= lang('Episode.form.image') ?>"
hint="<?= lang('Episode.form.image_hint') ?>"
helper="<?= lang('Common.forms.image_size_hint', ) ?>"
name="cover"
label="<?= lang('Episode.form.cover') ?>"
hint="<?= lang('Episode.form.cover_hint') ?>"
helper="<?= lang('Episode.form.cover_size_hint', ) ?>"
type="file"
accept=".jpg,.jpeg,.png" />

View File

@ -31,10 +31,10 @@
accept=".mp3,.m4a" />
<Forms.Field
name="image"
label="<?= lang('Episode.form.image') ?>"
hint="<?= lang('Episode.form.image_hint') ?>"
helper="<?= lang('Common.forms.image_size_hint', ) ?>"
name="cover"
label="<?= lang('Episode.form.cover') ?>"
hint="<?= lang('Episode.form.cover_hint') ?>"
helper="<?= lang('Episode.form.cover_size_hint', ) ?>"
type="file"
accept=".jpg,.jpeg,.png" />

View File

@ -34,7 +34,7 @@
$episode->audio_file_duration,
) .
'</time>' .
'<img loading="lazy" src="' . $episode->image->thumbnail_url . '" alt="' . $episode->title . '" class="object-cover w-20 h-20 rounded-lg" />' .
'<img loading="lazy" src="' . $episode->cover->thumbnail_url . '" alt="' . $episode->title . '" class="object-cover w-20 h-20 rounded-lg" />' .
'</div>' .
'<a class="overflow-x-hidden text-sm hover:underline" href="' . route_to(
'episode-view',

View File

@ -58,7 +58,7 @@
return '<div class="flex">' .
'<a href="' .
route_to('person-view', $person->id) .
"\"><img src=\"{$person->image->thumbnail_url}\" alt=\"{$person->full_name}\" class=\"object-cover w-16 h-16 rounded-full\" /></a>" .
"\"><img src=\"{$person->avatar->thumbnail_url}\" alt=\"{$person->full_name}\" class=\"object-cover w-16 h-16 rounded-full\" /></a>" .
'<div class="flex flex-col ml-3">' .
$person->full_name .
implode(

View File

@ -40,7 +40,7 @@
<Forms.Textarea name="message" placeholder="<?= lang('Episode.publish_form.message_placeholder') ?>" autofocus="" rows="2" />
</div>
<div class="flex border-t border-b">
<img src="<?= $episode->image
<img src="<?= $episode->cover
->thumbnail_url ?>" alt="<?= $episode->title ?>" class="w-24 h-24" />
<div class="flex flex-col flex-1">
<a href="<?= $episode->link ?>" class="flex-1 px-4 py-2">

View File

@ -42,7 +42,7 @@
<Forms.Textarea name="message" placeholder="<?= lang('Episode.publish_form.message_placeholder') ?>" autofocus="" value="<?= $post->message ?>" rows="2" />
</div>
<div class="flex border-t border-b">
<img src="<?= $episode->image
<img src="<?= $episode->cover
->thumbnail_url ?>" alt="<?= $episode->title ?>" class="w-24 h-24" />
<div class="flex flex-col flex-1">
<a href="<?= $episode->link ?>" class="flex-1 px-4 py-2">

View File

@ -2,7 +2,7 @@
<a href="<?= route_to('person-view', $person->id) ?>" class="flex flex-col justify-end w-full h-full text-white group">
<div class="absolute bottom-0 left-0 z-10 w-full h-full backdrop-gradient"></div>
<div class="w-full h-full overflow-hidden">
<img alt="<?= $person->full_name ?>" src="<?= $person->image->medium_url ?>" class="object-cover w-full h-full transition duration-200 ease-in-out transform group-focus:scale-105 group-hover:scale-105" />
<img alt="<?= $person->full_name ?>" src="<?= $person->avatar->medium_url ?>" class="object-cover w-full h-full transition duration-200 ease-in-out transform group-focus:scale-105 group-hover:scale-105" />
</div>
<div class="absolute z-20">
<h2 class="px-4 py-2 font-semibold leading-tight"><?= $person->full_name ?></h2>

View File

@ -15,9 +15,9 @@
<?= csrf_field() ?>
<Forms.Field
name="image"
label="<?= lang('Person.form.image') ?>"
helper="<?= lang('Person.form.image_size_hint') ?>"
name="avatar"
label="<?= lang('Person.form.avatar') ?>"
helper="<?= lang('Person.form.avatar_size_hint') ?>"
type="file"
accept=".jpg,.jpeg,.png" />

View File

@ -15,9 +15,9 @@
<?= csrf_field() ?>
<Forms.Field
name="image"
label="<?= lang('Person.form.image') ?>"
helper="<?= lang('Person.form.image_size_hint') ?>"
name="avatar"
label="<?= lang('Person.form.avatar') ?>"
helper="<?= lang('Person.form.avatar_size_hint') ?>"
type="file"
accept=".jpg,.jpeg,.png" />

View File

@ -18,7 +18,7 @@
<div class="flex flex-wrap">
<div class="w-full max-w-sm mb-6 md:mr-4">
<img
src="<?= $person->image->medium_url ?>"
src="<?= $person->avatar->medium_url ?>"
alt="$person->full_name"
class="object-cover w-full rounded"
/>

View File

@ -4,7 +4,7 @@
<div class="w-full h-full overflow-hidden">
<img
alt="<?= $podcast->title ?>"
src="<?= $podcast->image->medium_url ?>" class="object-cover w-full h-full transition duration-200 ease-in-out transform group-focus:scale-105 group-hover:scale-105" />
src="<?= $podcast->cover->medium_url ?>" class="object-cover w-full h-full transition duration-200 ease-in-out transform group-focus:scale-105 group-hover:scale-105" />
</div>
<div class="absolute z-20 px-4 pb-4 transition duration-75 ease-out translate-y-6 group-focus:translate-y-0 group-hover:translate-y-0">
<h2 class="font-bold leading-none truncate font-display"><?= $podcast->title ?></h2>

View File

@ -37,7 +37,7 @@ $podcastNavigation = [
<div class="flex items-center px-4 py-2 border-b border-pine-900">
<img
src="<?= $podcast->image->thumbnail_url ?>"
src="<?= $podcast->cover->thumbnail_url ?>"
alt="<?= $podcast->title ?>"
class="object-cover w-16 h-16 rounded"
/>

View File

@ -14,22 +14,28 @@
<?= $this->section('content') ?>
<form action="<?= route_to('podcast-create') ?>" method="POST" enctype='multipart/form-data' class="flex flex-col">
<form action="<?= route_to('podcast-create') ?>" method="POST" enctype='multipart/form-data' class="flex flex-col gap-y-6">
<?= csrf_field() ?>
<Forms.Section
class="mb-8"
title="<?= lang('Podcast.form.identity_section_title') ?>"
subtitle="<?= lang('Podcast.form.identity_section_subtitle') ?>" >
<Forms.Field
name="image"
label="<?= lang('Podcast.form.image') ?>"
helper="<?= lang('Common.forms.image_size_hint') ?>"
name="cover"
label="<?= lang('Podcast.form.cover') ?>"
helper="<?= lang('Podcast.form.cover_size_hint') ?>"
type="file"
required="true"
accept=".jpg,.jpeg,.png" />
<Forms.Field
name="banner"
label="<?= lang('Podcast.form.banner') ?>"
helper="<?= lang('Podcast.form.banner_size_hint') ?>"
type="file"
accept=".jpg,.jpeg,.png" />
<Forms.Field
name="title"
label="<?= lang('Podcast.form.title') ?>"
@ -67,7 +73,6 @@
</Forms.Section>
<Forms.Section
class="mb-8"
title="<?= lang('Podcast.form.classification_section_title') ?>"
subtitle="<?= lang('Podcast.form.classification_section_subtitle') ?>" >
@ -114,7 +119,6 @@
</Forms.Section>
<Forms.Section
class="mb-8"
title="<?= lang('Podcast.form.author_section_title') ?>"
subtitle="<?= lang('Podcast.form.author_section_subtitle') ?>" >
@ -143,7 +147,6 @@
</Forms.Section>
<Forms.Section
class="mb-8"
title="<?= lang('Podcast.form.location_section_title') ?>"
subtitle="<?= lang('Podcast.form.location_section_subtitle') ?>" >
@ -155,7 +158,6 @@
</Forms.Section>
<Forms.Section
class="mb-8"
title="<?= lang('Podcast.form.monetization_section_title') ?>"
subtitle="<?= lang('Podcast.form.monetization_section_subtitle') ?>" >
@ -184,7 +186,6 @@
</Forms.Section>
<Forms.Section
class="mb-8"
title="<?= lang('Podcast.form.advanced_section_title') ?>"
subtitle="<?= lang('Podcast.form.advanced_section_subtitle') ?>" >
@ -197,7 +198,6 @@
</Forms.Section>
<Forms.Section
class="mb-8"
title="<?= lang('Podcast.form.status_section_title') ?>" >
<Forms.Toggler class="mb-2" name="lock" value="yes" checked="true" hint="<?= lang('Podcast.form.lock_hint') ?>">
<?= lang('Podcast.form.lock') ?>

View File

@ -18,18 +18,41 @@
<?= $this->section('content') ?>
<form id="podcast-edit-form" action="<?= route_to('podcast-edit', $podcast->id) ?>" method="POST" enctype='multipart/form-data' class="flex flex-col">
<form id="podcast-edit-form" action="<?= route_to('podcast-edit', $podcast->id) ?>" method="POST" enctype='multipart/form-data' class="flex flex-row-reverse flex-wrap items-start justify-end gap-4">
<?= csrf_field() ?>
<div class="sticky z-40 flex flex-col w-full max-w-xs overflow-hidden bg-white shadow-sm border-3 border-pine-100 top-24 rounded-xl">
<?php if ($podcast->banner_path !== null): ?>
<a href="<?= route_to('podcast-banner-delete', $podcast->id) ?>" class="absolute p-1 text-white bg-red-600 border-2 border-black rounded-full hover:bg-red-800 focus:ring-castopod top-2 right-2" title="<?= lang('Podcast.form.banner_delete') ?>"><?= icon('delete-bin') ?></a>
<?php endif; ?>
<img src="<?= $podcast->banner->small_url ?>" alt="" class="object-cover w-full aspect-[3/1] bg-pine-800" />
<div class="flex px-4 py-2">
<img src="<?= $podcast->cover->thumbnail_url ?>" alt="<?= $podcast->title ?>"
class="w-16 h-16 mr-4 -mt-8 rounded-full ring-2 ring-white" />
<div class="flex flex-col">
<p class="font-semibold leading-none"><?= $podcast->title ?></p>
<p class="text-sm text-gray-500">@<?= $podcast->handle ?></p>
</div>
</div>
</div>
<div class="flex flex-col gap-y-6">
<Forms.Section
class="mb-8"
title="<?= lang('Podcast.form.identity_section_title') ?>"
subtitle="<?= lang('Podcast.form.identity_section_subtitle') ?>" >
<Forms.Field
name="image"
label="<?= lang('Podcast.form.image') ?>"
helper="<?= lang('Common.forms.image_size_hint') ?>"
name="cover"
label="<?= lang('Podcast.form.cover') ?>"
helper="<?= lang('Podcast.form.cover_size_hint') ?>"
type="file"
accept=".jpg,.jpeg,.png" />
<Forms.Field
name="banner"
label="<?= lang('Podcast.form.banner') ?>"
helper="<?= lang('Podcast.form.banner_size_hint') ?>"
type="file"
accept=".jpg,.jpeg,.png" />
@ -65,7 +88,6 @@
</Forms.Section>
<Forms.Section
class="mb-8"
title="<?= lang('Podcast.form.classification_section_title') ?>"
subtitle="<?= lang('Podcast.form.classification_section_subtitle') ?>" >
@ -114,7 +136,6 @@
</Forms.Section>
<Forms.Section
class="mb-8"
title="<?= lang('Podcast.form.author_section_title') ?>"
subtitle="<?= lang('Podcast.form.author_section_subtitle') ?>" >
@ -147,7 +168,6 @@
</Forms.Section>
<Forms.Section
class="mb-8"
title="<?= lang('Podcast.form.location_section_title') ?>"
subtitle="<?= lang('Podcast.form.location_section_subtitle') ?>" >
@ -160,7 +180,6 @@
</Forms.Section>
<Forms.Section
class="mb-8"
title="<?= lang('Podcast.form.monetization_section_title') ?>"
subtitle="<?= lang('Podcast.form.monetization_section_subtitle') ?>" >
@ -190,7 +209,6 @@
</Forms.Section>
<Forms.Section
class="mb-8"
title="<?= lang('Podcast.form.advanced_section_title') ?>"
subtitle="<?= lang('Podcast.form.advanced_section_subtitle') ?>" >
@ -204,7 +222,6 @@
</Forms.Section>
<Forms.Section
class="mb-8"
title="<?= lang('Podcast.form.status_section_title') ?>" >
<Forms.Toggler class="mb-2" name="lock" value="yes" checked="<?= $podcast->is_locked ? 'true' : 'false' ?>" hint="<?= lang('Podcast.form.lock_hint') ?>">
<?= lang('Podcast.form.lock') ?>
@ -217,6 +234,9 @@
</Forms.Toggler>
</Forms.Section>
<Button variant="primary" type="submit" class="self-end"><?= lang('Podcast.form.submit_edit') ?></Button>
</div>
</form>
<?= $this->endSection() ?>

View File

@ -1,5 +1,5 @@
<section class="flex flex-col">
<header class="flex justify-between py-2">
<header class="flex justify-between">
<Heading tagName="h2"><?= lang('Podcast.latest_episodes') ?></Heading>
<a href="<?= route_to(
'episode-list',
@ -10,7 +10,7 @@
</a>
</header>
<?php if ($episodes): ?>
<div class="grid px-4 py-2 -mx-2 overflow-x-auto grid-cols-latestEpisodes gap-x-4 snap snap-x snap-proximity">
<div class="grid px-4 pt-2 pb-5 -mx-2 overflow-x-auto grid-cols-latestEpisodes gap-x-4 snap snap-x snap-proximity">
<?php foreach ($episodes as $episode): ?>
<?= view('episode/_card', [
'episode' => $episode,

View File

@ -55,7 +55,7 @@
return '<div class="flex">' .
'<a href="' .
route_to('person-view', $person->id) .
"\"><img src=\"{$person->image->thumbnail_url}\" alt=\"{$person->full_name}\" class=\"object-cover w-16 h-16 rounded-full\" /></a>" .
"\"><img src=\"{$person->avatar->thumbnail_url}\" alt=\"{$person->full_name}\" class=\"object-cover w-16 h-16 rounded-full\" /></a>" .
'<div class="flex flex-col ml-3">' .
$person->full_name .
implode(

View File

@ -10,7 +10,7 @@
<?= $this->section('content') ?>
<form action="<?= route_to('settings-instance') ?>" method="POST" class="flex flex-col max-w-sm gap-y-4" enctype="multipart/form-data">
<form action="<?= route_to('settings-instance') ?>" method="POST" class="flex flex-col gap-y-4" enctype="multipart/form-data">
<?= csrf_field() ?>
<Forms.Section

View File

@ -31,7 +31,7 @@
$interactButtons .= <<<CODE_SAMPLE
<button class="inline-flex items-center w-full px-4 py-1 hover:bg-gray-100" id="interact-as-actor-{$userPodcast->id}" name="actor_id" value="{$userPodcast->actor_id}">
<span class="inline-flex items-center flex-1 text-sm"><img src="{$userPodcast->image->thumbnail_url}" class="w-6 h-6 mr-2 rounded-full" />{$userPodcast->title}{$checkMark}</span>
<span class="inline-flex items-center flex-1 text-sm"><img src="{$userPodcast->cover->tiny_url}" class="w-6 h-6 mr-2 rounded-full" />{$userPodcast->title}{$checkMark}</span>
</button>
CODE_SAMPLE;
}

View File

@ -17,7 +17,7 @@
<div class="flex flex-col items-start p-4 gap-y-4">
<?php foreach ($persons as $person): ?>
<div class="flex gap-x-2">
<img src="<?= $person->image->thumbnail_url ?>" alt="<?= $person->full_name ?>" class="object-cover w-10 h-10 rounded-full" />
<img src="<?= $person->avatar->thumbnail_url ?>" alt="<?= $person->full_name ?>" class="object-cover w-10 h-10 rounded-full" />
<div class="flex flex-col">
<h4 class="text-sm font-semibold">
<?php if ($person->information_url): ?>

View File

@ -20,7 +20,7 @@
</head>
<body class="flex" style="background: <?= $themeData['background'] ?>; color: <?= $themeData['text'] ?>;">
<img src="<?= $episode->image->thumbnail_url ?>" alt="<?= $episode->title ?>" class="flex-shrink w-36 h-36" />
<img src="<?= $episode->cover->thumbnail_url ?>" alt="<?= $episode->title ?>" class="flex-shrink w-36 h-36" />
<div class="flex flex-col items-start flex-1 min-w-0 px-4 pt-4 h-36">
<a href="https://castopod.org/" class="absolute top-0 right-0 mt-1 mr-2 text-2xl text-pine-500 hover:opacity-75" title="<?= lang('Common.powered_by', [
'castopod' => 'Castopod',

View File

@ -40,7 +40,7 @@
]) ?>">
<?= icon('arrow-left', 'mr-2 text-lg') ?>
<div class="inline-flex items-center gap-x-2">
<img class="w-8 h-8 rounded-full" src="<?= $episode->podcast->image->thumbnail_url ?>" alt="<?= $episode->podcast->title ?>" />
<img class="w-8 h-8 rounded-full" src="<?= $episode->podcast->cover->tiny_url ?>" alt="<?= $episode->podcast->title ?>" />
<div class="flex flex-col">
<span class="text-sm font-semibold leading-none"><?= $episode->podcast->title ?></span>
<span class="text-xs"><?= lang('Podcast.followers', [
@ -69,10 +69,10 @@
</div>
</nav>
<header class="relative z-50 flex flex-col col-start-2 px-8 pt-8 pb-4 overflow-hidden bg-pine-500 gap-y-4">
<div class="absolute top-0 left-0 w-full h-full bg-center bg-no-repeat bg-cover blur-lg mix-blend-luminosity" style="background-image: url('<?= $episode->podcast->image->thumbnail_url ?>');"></div>
<div class="absolute top-0 left-0 w-full h-full bg-center bg-no-repeat bg-cover blur-lg mix-blend-soft-light" style="background-image: url('<?= $episode->podcast->banner->small_url ?>');"></div>
<div class="absolute top-0 left-0 w-full h-full bg-gradient-to-t from-pine-800 to-transparent"></div>
<div class="z-10 flex flex-col items-start gap-y-2 gap-x-4 sm:flex-row">
<img src="<?= $episode->image->medium_url ?>" alt="<?= $episode->title ?>" loading="lazy" class="rounded-md h-36" />
<img src="<?= $episode->cover->medium_url ?>" alt="<?= $episode->title ?>" loading="lazy" class="rounded-md h-36" />
<div class="flex flex-col items-start text-white">
<?= episode_numbering($episode->number, $episode->season_number, 'bg-pine-50 text-sm leading-none font-semibold text-gray-700 border !no-underline border-pine-100', true) ?>
<h1 class="inline-flex items-baseline max-w-md mt-2 text-2xl font-bold leading-none sm:text-3xl font-display line-clamp-2"><?= $episode->title ?></h1>
@ -82,7 +82,7 @@
<div class="inline-flex flex-row-reverse">
<?php $i = 0; ?>
<?php foreach ($episode->persons as $person): ?>
<img src="<?= $person->image->thumbnail_url ?>" alt="<?= $person->full_name ?>" class="object-cover w-8 h-8 -ml-5 border-2 rounded-full border-pine-100 last:ml-0" />
<img src="<?= $person->avatar->thumbnail_url ?>" alt="<?= $person->full_name ?>" class="object-cover w-8 h-8 -ml-5 border-2 rounded-full border-pine-100 last:ml-0" />
<?php $i++; if ($i === 3) {
break;
}?>
@ -102,7 +102,7 @@
<div class="z-10 inline-flex items-center text-white gap-x-4">
<play-episode-button
id="<?= $episode->id ?>"
imageSrc="<?= $episode->image->thumbnail_url ?>"
imageSrc="<?= $episode->cover->thumbnail_url ?>"
title="<?= $episode->title ?>"
podcast="<?= $episode->podcast->title ?>"
src="<?= $episode->audio_file_web_url ?>"

View File

@ -3,7 +3,7 @@
<time class="absolute px-1 text-xs font-semibold text-white rounded bottom-2 right-2 bg-black/50" datetime="PT<?= $episode->audio_file_duration ?>S">
<?= format_duration($episode->audio_file_duration) ?>
</time>
<img loading="lazy" src="<?= $episode->image
<img loading="lazy" src="<?= $episode->cover
->thumbnail_url ?>" alt="<?= $episode->title ?>" class="object-cover w-20 h-20 rounded-lg" />
</div>
<div class="flex items-center flex-1 gap-x-4">
@ -16,7 +16,7 @@
</div>
<play-episode-button
id="<?= $episode->id ?>"
imageSrc="<?= $episode->image->thumbnail_url ?>"
imageSrc="<?= $episode->cover->thumbnail_url ?>"
title="<?= $episode->title ?>"
podcast="<?= $episode->podcast->title ?>"
src="<?= $episode->audio_file_web_url ?>"

View File

@ -4,7 +4,7 @@
<?= format_duration($episode->audio_file_duration) ?>
</time>
<img
src="<?= $episode->image->thumbnail_url ?>"
src="<?= $episode->cover->thumbnail_url ?>"
alt="<?= $episode->title ?>" class="w-24 h-24"/>
</div>
<div class="flex flex-col flex-1 px-4 py-2">
@ -17,7 +17,7 @@
<play-episode-button
class="mr-4"
id="<?= $index . '_' . $episode->id ?>"
imageSrc="<?= $episode->image->thumbnail_url ?>"
imageSrc="<?= $episode->cover->thumbnail_url ?>"
title="<?= $episode->title ?>"
podcast="<?= $episode->podcast->title ?>"
src="<?= $episode->audio_file_web_url ?>"

View File

@ -9,11 +9,10 @@
<meta property="og:locale" content="<?= $podcast->language_code ?>" />
<meta property="og:site_name" content="<?= $podcast->title ?>" />
<meta property="og:url" content="<?= current_url() ?>" />
<meta property="og:image" content="<?= $episode->image->large_url ?>" />
<meta property="og:image" content="<?= $episode->cover->large_url ?>" />
<meta property="og:image:width" content="<?= config('Images')
->largeSize ?>" />
<meta property="og:image:height" content="<?= config('Images')
->largeSize ?>" />
->podcastCoverSizes['large'][0] ?>" />
<meta property="og:image:height" content="<?= config('Images')->podcastCoverSizes['large'][1] ?>" />
<meta property="og:description" content="$description" />
<meta property="article:published_time" content="<?= $episode->published_at ?>" />
<meta property="article:modified_time" content="<?= $episode->updated_at ?>" />
@ -23,7 +22,7 @@
<link rel="alternate" type="text/xml+oembed" href="<?= base_url(route_to('episode-oembed-xml', $podcast->handle, $episode->slug)) ?>" title="<?= $episode->title ?> oEmbed xml" />
<meta name="twitter:title" content="<?= $episode->title ?>" />
<meta name="twitter:description" content="<?= $episode->description ?>" />
<meta name="twitter:image" content="<?= $episode->image->large_url ?>" />
<meta name="twitter:image" content="<?= $episode->cover->large_url ?>" />
<meta name="twitter:card" content="player" />
<meta property="twitter:audio:partner" content="<?= $podcast->publisher ?>" />
<meta property="twitter:audio:artist_name" content="<?= $podcast->owner_name ?>" />

View File

@ -11,11 +11,10 @@
<meta property="og:locale" content="<?= $podcast->language_code ?>" />
<meta property="og:site_name" content="<?= $podcast->title ?>" />
<meta property="og:url" content="<?= current_url() ?>" />
<meta property="og:image" content="<?= $episode->image->large_url ?>" />
<meta property="og:image" content="<?= $episode->cover->large_url ?>" />
<meta property="og:image:width" content="<?= config('Images')
->largeSize ?>" />
<meta property="og:image:height" content="<?= config('Images')
->largeSize ?>" />
->podcastCoverSizes['large'][0] ?>" />
<meta property="og:image:height" content="<?= config('Images')->podcastCoverSizes['large'][1] ?>" />
<meta property="og:description" content="$description" />
<meta property="article:published_time" content="<?= $episode->published_at ?>" />
<meta property="article:modified_time" content="<?= $episode->updated_at ?>" />
@ -25,7 +24,7 @@
<link rel="alternate" type="text/xml+oembed" href="<?= base_url(route_to('episode-oembed-xml', $podcast->handle, $episode->slug)) ?>" title="<?= $episode->title ?> oEmbed xml" />
<meta name="twitter:title" content="<?= $episode->title ?>" />
<meta name="twitter:description" content="<?= $episode->description ?>" />
<meta name="twitter:image" content="<?= $episode->image->large_url ?>" />
<meta name="twitter:image" content="<?= $episode->cover->large_url ?>" />
<meta name="twitter:card" content="player" />
<meta property="twitter:audio:partner" content="<?= $podcast->publisher ?>" />
<meta property="twitter:audio:artist_name" content="<?= $podcast->owner_name ?>" />

View File

@ -52,7 +52,7 @@
<article class="text-white">
<div class="absolute bottom-0 left-0 z-10 w-full h-full backdrop-gradient"></div>
<div class="w-full h-full overflow-hidden">
<img alt="<?= $podcast->title ?>" src="<?= $podcast->image->medium_url ?>" class="object-cover w-full h-full transition duration-200 ease-in-out transform group-focus:scale-105 group-hover:scale-105" />
<img alt="<?= $podcast->title ?>" src="<?= $podcast->cover->medium_url ?>" class="object-cover w-full h-full transition duration-200 ease-in-out transform group-focus:scale-105 group-hover:scale-105" />
</div>
<div class="absolute bottom-0 left-0 z-20 px-4 pb-2">
<h2 class="font-bold leading-none truncate font-display"><?= $podcast->title ?></h2>

View File

@ -34,9 +34,10 @@
</div>
<?php endif; ?>
<header class="z-50 flex flex-col-reverse justify-between w-full col-start-2 bg-top bg-no-repeat bg-cover sm:flex-row sm:items-end bg-pine-800 aspect-[3/1]" style="background-image: url('<?= $podcast->actor->cover_image_url ?>');">
<div class="flex items-center pl-4 -mb-6 md:pl-8 md:-mb-8 gap-x-4">
<img src="<?= $podcast->image->thumbnail_url ?>" alt="<?= $podcast->title ?>" loading="lazy" class="h-24 rounded-full md:h-28 ring-4 ring-white" />
<header class="relative z-50 flex flex-col-reverse justify-between w-full col-start-2 bg-top bg-no-repeat bg-cover sm:flex-row sm:items-end bg-pine-800 aspect-[3/1]" style="background-image: url('<?= $podcast->banner->medium_url ?>');">
<div class="absolute bottom-0 left-0 w-full h-full backdrop-gradient-pine"></div>
<div class="z-10 flex items-center pl-4 -mb-6 md:pl-8 md:-mb-8 gap-x-4">
<img src="<?= $podcast->cover->thumbnail_url ?>" alt="<?= $podcast->title ?>" loading="lazy" class="h-24 rounded-full md:h-28 ring-4 ring-white" />
<div class="relative flex flex-col text-white -top-2">
<h1 class="text-lg font-bold leading-none line-clamp-2 md:leading-none md:text-2xl font-display"><?= $podcast->title ?><span class="ml-1 font-sans text-base font-normal">@<?= $podcast->handle ?></span></h1>
<span class="text-xs"><?= lang('Podcast.followers', [
@ -44,7 +45,7 @@
]) ?></span>
</div>
</div>
<div class="inline-flex items-center self-end mt-2 mb-2 mr-2 gap-x-2">
<div class="z-10 inline-flex items-center self-end mt-2 mr-2 sm:mb-4 sm:mr-4 gap-x-2">
<?php if (in_array(true, array_column($podcast->fundingPlatforms, 'is_visible'), true)): ?>
<IconButton glyph="heart" variant="accent" data-toggle="funding-links" data-toggle-class="hidden"><?= lang('Podcast.sponsor') . lang('Podcast.sponsor_title') ?></IconButton>
<?php endif; ?>

View File

@ -19,11 +19,9 @@
<meta property="og:locale" content="<?= $podcast->language_code ?>" />
<meta property="og:site_name" content="<?= $podcast->title ?>" />
<meta property="og:url" content="<?= current_url() ?>" />
<meta property="og:image" content="<?= $podcast->image->large_url ?>" />
<meta property="og:image:width" content="<?= config('Images')
->largeSize ?>" />
<meta property="og:image:height" content="<?= config('Images')
->largeSize ?>" />
<meta property="og:image" content="<?= $podcast->cover->large_url ?>" />
<meta property="og:image:width" content="<?= config('Images')->podcastCoverSizes['large'][0] ?>" />
<meta property="og:image:height" content="<?= config('Images')->podcastCoverSizes['large'][1] ?>" />
<meta name="twitter:card" content="summary_large_image" />
<?= service('vite')
@ -55,7 +53,7 @@
<div class="inline-flex flex-row-reverse">
<?php $i = 0; ?>
<?php foreach ($podcast->persons as $person): ?>
<img src="<?= $person->image->thumbnail_url ?>" alt="<?= $person->full_name ?>" class="object-cover w-8 h-8 -ml-5 border-2 rounded-full border-pine-100 last:ml-0" />
<img src="<?= $person->avatar->thumbnail_url ?>" alt="<?= $person->full_name ?>" class="object-cover w-8 h-8 -ml-5 border-2 rounded-full border-pine-100 last:ml-0" />
<?php $i++; if ($i === 3) {
break;
}?>

View File

@ -17,11 +17,9 @@
<meta property="og:locale" content="<?= $podcast->language_code ?>" />
<meta property="og:site_name" content="<?= $podcast->title ?>" />
<meta property="og:url" content="<?= current_url() ?>" />
<meta property="og:image" content="<?= $podcast->image->large_url ?>" />
<meta property="og:image:width" content="<?= config('Images')
->largeSize ?>" />
<meta property="og:image:height" content="<?= config('Images')
->largeSize ?>" />
<meta property="og:image" content="<?= $podcast->cover->large_url ?>" />
<meta property="og:image:width" content="<?= config('Images')->podcastCoverSizes['large'][0] ?>" />
<meta property="og:image:height" content="<?= config('Images')->podcastCoverSizes['large'][1] ?>" />
<meta name="twitter:card" content="summary_large_image" />
<?= service('vite')

View File

@ -17,11 +17,9 @@
<meta property="og:locale" content="<?= $podcast->language_code ?>" />
<meta property="og:site_name" content="<?= $podcast->title ?>" />
<meta property="og:url" content="<?= current_url() ?>" />
<meta property="og:image" content="<?= $podcast->image->large_url ?>" />
<meta property="og:image:width" content="<?= config('Images')
->largeSize ?>" />
<meta property="og:image:height" content="<?= config('Images')
->largeSize ?>" />
<meta property="og:image" content="<?= $podcast->cover->large_url ?>" />
<meta property="og:image:width" content="<?= config('Images')->podcastCoverSizes['large'][0] ?>" />
<meta property="og:image:height" content="<?= config('Images')->podcastCoverSizes['large'][1] ?>" />
<meta name="twitter:card" content="summary_large_image" />
<?= service('vite')