diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 91de34a65356f0bcf316a310bca257028622a55d..bd50617eb8ebed69feee710784ca06b9870e3cf4 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -326,6 +326,14 @@ $routes->group( 'filter' => 'permission:podcast_episodes-edit', ] ); + $routes->get( + 'embeddable-player', + 'Episode::embeddablePlayer/$1/$2', + [ + 'as' => 'embeddable-player-add', + 'filter' => 'permission:podcast_episodes-edit', + ] + ); $routes->group('persons', function ($routes) { $routes->get('/', 'EpisodePerson/$1/$2', [ @@ -565,9 +573,19 @@ $routes->group(config('App')->authGateway, function ($routes) { // Public routes $routes->group('@(:podcastName)', function ($routes) { $routes->get('/', 'Podcast/$1', ['as' => 'podcast']); - $routes->get('(:slug)', 'Episode/$1/$2', [ - 'as' => 'episode', - ]); + $routes->group('(:slug)', function ($routes) { + $routes->get('/', 'Episode/$1/$2', [ + 'as' => 'episode', + ]); + $routes->group('embeddable-player', function ($routes) { + $routes->get('/', 'Episode::embeddablePlayer/$1/$2', [ + 'as' => 'embeddable-player', + ]); + $routes->get('(:slug)', 'Episode::embeddablePlayer/$1/$2/$3', [ + 'as' => 'embeddable-player-theme', + ]); + }); + }); $routes->head('feed.xml', 'Feed/$1', ['as' => 'podcast_feed']); $routes->get('feed.xml', 'Feed/$1', ['as' => 'podcast_feed']); }); diff --git a/app/Controllers/Admin/Episode.php b/app/Controllers/Admin/Episode.php index 8ad5ce6f927be192fe74c486ae79fae286f1f455..92f9a8ea722934d7af9ca03648b93ec187a8bfcb 100644 --- a/app/Controllers/Admin/Episode.php +++ b/app/Controllers/Admin/Episode.php @@ -420,4 +420,21 @@ class Episode extends BaseController $this->episode->id, ]); } + + public function embeddablePlayer() + { + helper(['form']); + + $data = [ + 'podcast' => $this->podcast, + 'episode' => $this->episode, + 'themes' => EpisodeModel::$themes, + ]; + + replace_breadcrumb_params([ + 0 => $this->podcast->title, + 1 => $this->episode->title, + ]); + return view('admin/episode/embeddable_player', $data); + } } diff --git a/app/Controllers/Admin/PodcastPlatform.php b/app/Controllers/Admin/PodcastPlatform.php index 9229cbd053e8c87db4394cfaf31f0e61cc2c04a1..6f5fd99fd3821980383cd5ac670daaab2969a1f9 100644 --- a/app/Controllers/Admin/PodcastPlatform.php +++ b/app/Controllers/Admin/PodcastPlatform.php @@ -81,6 +81,12 @@ class PodcastPlatform extends BaseController ) ? $podcastPlatform['visible'] == 'yes' : false, + 'is_on_embeddable_player' => array_key_exists( + 'on_embeddable_player', + $podcastPlatform + ) + ? $podcastPlatform['on_embeddable_player'] == 'yes' + : false, ]); } } diff --git a/app/Controllers/Analytics.php b/app/Controllers/Analytics.php index ccaa92f688a21cf6d451b652eb83e4ebee05756d..c687cb65c1dd908a0e68eb32de8fe7b03a60af90 100644 --- a/app/Controllers/Analytics.php +++ b/app/Controllers/Analytics.php @@ -49,8 +49,16 @@ class Analytics extends Controller public function hit($base64EpisodeData, ...$filename) { helper('media', 'analytics'); - - $serviceName = isset($_GET['_from']) ? $_GET['_from'] : ''; + $session = \Config\Services::session(); + $session->start(); + $serviceName = ''; + if (isset($_GET['_from'])) { + $serviceName = $_GET['_from']; + } elseif (!empty($session->get('embeddable_player_domain'))) { + $serviceName = $session->get('embeddable_player_domain'); + } elseif ($session->get('referer') !== '- Direct -') { + $serviceName = parse_url($session->get('referer'), PHP_URL_HOST); + } $episodeData = unpack( 'IpodcastId/IepisodeId/IbytesThreshold/IfileSize/Iduration/IpublicationDate', diff --git a/app/Controllers/Episode.php b/app/Controllers/Episode.php index 3df89dda870acb533787256340b2b827cf255778..477a5cad979cdf9ea35f05dc5ddf7e2eba722210 100644 --- a/app/Controllers/Episode.php +++ b/app/Controllers/Episode.php @@ -36,8 +36,9 @@ class Episode extends BaseController ) { throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound(); } - - return $this->$method(); + unset($params[1]); + unset($params[0]); + return $this->$method(...$params); } public function index() @@ -54,48 +55,12 @@ class Episode extends BaseController $this->podcast->type ); + helper(['persons']); $persons = []; - foreach ($this->episode->episode_persons as $episodePerson) { - if (array_key_exists($episodePerson->person->id, $persons)) { - $persons[$episodePerson->person->id]['roles'] .= - empty($episodePerson->person_group) || - empty($episodePerson->person_role) - ? '' - : (empty( - $persons[$episodePerson->person->id][ - 'roles' - ] - ) - ? '' - : ', ') . - lang( - 'PersonsTaxonomy.persons.' . - $episodePerson->person_group . - '.roles.' . - $episodePerson->person_role . - '.label' - ); - } else { - $persons[$episodePerson->person->id] = [ - 'full_name' => $episodePerson->person->full_name, - 'information_url' => - $episodePerson->person->information_url, - 'thumbnail_url' => - $episodePerson->person->image->thumbnail_url, - 'roles' => - empty($episodePerson->person_group) || - empty($episodePerson->person_role) - ? '' - : lang( - 'PersonsTaxonomy.persons.' . - $episodePerson->person_group . - '.roles.' . - $episodePerson->person_role . - '.label' - ), - ]; - } - } + construct_episode_person_array( + $this->episode->episode_persons, + $persons + ); $data = [ 'previousEpisode' => $previousNextEpisodes['previous'], @@ -120,4 +85,58 @@ class Episode extends BaseController return $cachedView; } + + public function embeddablePlayer($theme = 'light-transparent') + { + self::triggerWebpageHit($this->episode->podcast_id); + + $session = \Config\Services::session(); + $session->start(); + if (isset($_SERVER['HTTP_REFERER'])) { + $session->set( + 'embeddable_player_domain', + parse_url($_SERVER['HTTP_REFERER'], PHP_URL_HOST) + ); + } + + $locale = service('request')->getLocale(); + + $cacheName = "page_podcast{$this->episode->podcast_id}_episode{$this->episode->id}_embeddable_player_{$theme}_{$locale}"; + + if (!($cachedView = cache($cacheName))) { + $episodeModel = new EpisodeModel(); + $theme = EpisodeModel::$themes[$theme]; + helper(['persons']); + $persons = []; + construct_episode_person_array( + $this->episode->episode_persons, + $persons + ); + constructs_podcast_person_array( + $this->podcast->podcast_persons, + $persons + ); + + $data = [ + 'podcast' => $this->podcast, + 'episode' => $this->episode, + 'persons' => $persons, + 'theme' => $theme, + ]; + + $secondsToNextUnpublishedEpisode = $episodeModel->getSecondsToNextUnpublishedEpisode( + $this->podcast->id + ); + + // The page cache is set to a decade so it is deleted manually upon podcast update + return view('embeddable_player', $data, [ + 'cache' => $secondsToNextUnpublishedEpisode + ? $secondsToNextUnpublishedEpisode + : DECADE, + 'cache_name' => $cacheName, + ]); + } + + return $cachedView; + } } diff --git a/app/Controllers/Podcast.php b/app/Controllers/Podcast.php index 9c6fc6756e19feca366c76f0233790a877ed03e3..d7a80a6ee3730d2079b3e2c7d1b79e2719b75010 100644 --- a/app/Controllers/Podcast.php +++ b/app/Controllers/Podcast.php @@ -109,48 +109,12 @@ class Podcast extends BaseController ]); } + helper(['persons']); $persons = []; - foreach ($this->podcast->podcast_persons as $podcastPerson) { - if (array_key_exists($podcastPerson->person->id, $persons)) { - $persons[$podcastPerson->person->id]['roles'] .= - empty($podcastPerson->person_group) || - empty($podcastPerson->person_role) - ? '' - : (empty( - $persons[$podcastPerson->person->id][ - 'roles' - ] - ) - ? '' - : ', ') . - lang( - 'PersonsTaxonomy.persons.' . - $podcastPerson->person_group . - '.roles.' . - $podcastPerson->person_role . - '.label' - ); - } else { - $persons[$podcastPerson->person->id] = [ - 'full_name' => $podcastPerson->person->full_name, - 'information_url' => - $podcastPerson->person->information_url, - 'thumbnail_url' => - $podcastPerson->person->image->thumbnail_url, - 'roles' => - empty($podcastPerson->person_group) || - empty($podcastPerson->person_role) - ? '' - : lang( - 'PersonsTaxonomy.persons.' . - $podcastPerson->person_group . - '.roles.' . - $podcastPerson->person_role . - '.label' - ), - ]; - } - } + constructs_podcast_person_array( + $this->podcast->podcast_persons, + $persons + ); $data = [ 'podcast' => $this->podcast, diff --git a/app/Database/Migrations/2020-06-08-160000_add_podcasts_platforms.php b/app/Database/Migrations/2020-06-08-160000_add_podcasts_platforms.php index 045add8548aeb5844618560c801d5289f76a0b71..4b2a9f9efd48e1808fd3a3df097527b957222576 100644 --- a/app/Database/Migrations/2020-06-08-160000_add_podcasts_platforms.php +++ b/app/Database/Migrations/2020-06-08-160000_add_podcasts_platforms.php @@ -40,6 +40,11 @@ class AddPodcastsPlatforms extends Migration 'constraint' => 1, 'default' => 0, ], + 'is_on_embeddable_player' => [ + 'type' => 'TINYINT', + 'constraint' => 1, + 'default' => 0, + ], ]); $this->forge->addPrimaryKey(['podcast_id', 'platform_slug']); diff --git a/app/Entities/Episode.php b/app/Entities/Episode.php index 964d51b2c005bb26303b87bc8f3b1fee72d5b6a6..99bc4804e790f44b3d6ed1d732676ec8455fd067 100644 --- a/app/Entities/Episode.php +++ b/app/Entities/Episode.php @@ -94,6 +94,13 @@ class Episode extends Entity */ protected $description; + /** + * The embeddable player URL + * + * @var string + */ + protected $embeddable_player; + /** * @var string */ @@ -421,6 +428,24 @@ class Episode extends Entity ); } + public function getEmbeddablePlayer($theme = null) + { + return base_url( + $theme + ? route_to( + 'embeddable-player-theme', + $this->getPodcast()->name, + $this->attributes['slug'], + $theme + ) + : route_to( + 'embeddable-player', + $this->getPodcast()->name, + $this->attributes['slug'] + ) + ); + } + public function setGuid(string $guid) { return $this->attributes['guid'] = empty($guid) diff --git a/app/Entities/Platform.php b/app/Entities/Platform.php index 904603266341c52302f2032498528d1e8a0a4e0c..a1080da46f287f411ee4b3cfa3b377d732559d6f 100644 --- a/app/Entities/Platform.php +++ b/app/Entities/Platform.php @@ -21,5 +21,6 @@ class Platform extends Entity 'link_url' => '?string', 'link_content' => '?string', 'is_visible' => '?boolean', + 'is_on_embeddable_player' => '?boolean', ]; } diff --git a/app/Entities/Podcast.php b/app/Entities/Podcast.php index f35cd75954a0f626e8a37e7ab52f1fbd1b60e994..aa4a4afe71a10dc6f92686931403ea2c12870b10 100644 --- a/app/Entities/Podcast.php +++ b/app/Entities/Podcast.php @@ -324,6 +324,24 @@ class Podcast extends Entity return $this->podcastingPlatforms; } + /** + * Returns true if the podcast has podcasting platform links + */ + public function getHasPodcastingPlatforms() + { + if (empty($this->id)) { + throw new \RuntimeException( + 'Podcast must be created before getting podcasting platform.' + ); + } + foreach ($this->getPodcastingPlatforms() as $podcastingPlatform) { + if ($podcastingPlatform->is_on_embeddable_player) { + return true; + } + } + return false; + } + /** * Returns the podcast's social platform links * @@ -347,6 +365,24 @@ class Podcast extends Entity return $this->socialPlatforms; } + /** + * Returns true if the podcast has social platform links + */ + public function getHasSocialPlatforms() + { + if (empty($this->id)) { + throw new \RuntimeException( + 'Podcast must be created before getting social platform.' + ); + } + foreach ($this->getSocialPlatforms() as $socialPlatform) { + if ($socialPlatform->is_on_embeddable_player) { + return true; + } + } + return false; + } + /** * Returns the podcast's funding platform links * @@ -370,6 +406,24 @@ class Podcast extends Entity return $this->fundingPlatforms; } + /** + * Returns true if the podcast has social platform links + */ + public function getHasFundingPlatforms() + { + if (empty($this->id)) { + throw new \RuntimeException( + 'Podcast must be created before getting Funding platform.' + ); + } + foreach ($this->getFundingPlatforms() as $fundingPlatform) { + if ($fundingPlatform->is_on_embeddable_player) { + return true; + } + } + return false; + } + public function getOtherCategories() { if (empty($this->id)) { diff --git a/app/Helpers/analytics_helper.php b/app/Helpers/analytics_helper.php index 7439c1e4f0ec84a705fa729fce379375188b9080..3360de83359b92aa2685142c03d269b5d0e762ae 100644 --- a/app/Helpers/analytics_helper.php +++ b/app/Helpers/analytics_helper.php @@ -111,9 +111,6 @@ function set_user_session_player() $session->start(); if (!$session->has('player')) { - $session = \Config\Services::session(); - $session->start(); - $playerFound = null; $userAgent = $_SERVER['HTTP_USER_AGENT']; diff --git a/app/Helpers/components_helper.php b/app/Helpers/components_helper.php index 3d69bab2ccda738debdab168801cc3ea015190fa..78b1516e4218ee01b3e12cd5b2a1e21822187624 100644 --- a/app/Helpers/components_helper.php +++ b/app/Helpers/components_helper.php @@ -384,29 +384,12 @@ if (!function_exists('location_link')) { $locationOsmid, $class = '' ) { - $link = null; + $link = ''; + if (!empty($locationName)) { - $uri = ''; - if (!empty($locationOsmid)) { - $uri = - 'https://www.openstreetmap.org/' . - ['N' => 'node', 'W' => 'way', 'R' => 'relation'][ - substr($locationOsmid, 0, 1) - ] . - '/' . - substr($locationOsmid, 1); - } elseif (!empty($locationGeo)) { - $uri = - 'https://www.openstreetmap.org/#map=17/' . - str_replace(',', '/', substr($locationGeo, 4)); - } else { - $uri = - 'https://www.openstreetmap.org/search?query=' . - urlencode($locationName); - } $link = button( $locationName, - $uri, + location_url($locationName, $locationGeo, $locationOsmid), [ 'variant' => 'default', 'size' => 'small', @@ -421,6 +404,7 @@ if (!function_exists('location_link')) { ] ); } + return $link; } } diff --git a/app/Helpers/persons_helper.php b/app/Helpers/persons_helper.php new file mode 100644 index 0000000000000000000000000000000000000000..5fe79d763b5e39880a2353433ca719281a2f162a --- /dev/null +++ b/app/Helpers/persons_helper.php @@ -0,0 +1,97 @@ +<?php + +/** + * @copyright 2021 Podlibre + * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 + * @link https://castopod.org/ + */ + +/** + * Fetches persons from an episode + * + * @param array $podcast_persons + * @param array &$persons + */ +function constructs_podcast_person_array($podcast_persons, &$persons) +{ + foreach ($podcast_persons as $podcastPerson) { + if (array_key_exists($podcastPerson->person->id, $persons)) { + $persons[$podcastPerson->person->id]['roles'] .= + empty($podcastPerson->person_group) || + empty($podcastPerson->person_role) + ? '' + : (empty($persons[$podcastPerson->person->id]['roles']) + ? '' + : ', ') . + lang( + 'PersonsTaxonomy.persons.' . + $podcastPerson->person_group . + '.roles.' . + $podcastPerson->person_role . + '.label' + ); + } else { + $persons[$podcastPerson->person->id] = [ + 'full_name' => $podcastPerson->person->full_name, + 'information_url' => $podcastPerson->person->information_url, + 'thumbnail_url' => $podcastPerson->person->image->thumbnail_url, + 'roles' => + empty($podcastPerson->person_group) || + empty($podcastPerson->person_role) + ? '' + : lang( + 'PersonsTaxonomy.persons.' . + $podcastPerson->person_group . + '.roles.' . + $podcastPerson->person_role . + '.label' + ), + ]; + } + } +} + +/** + * Fetches persons from an episode + * + * @param array $episode_persons + * @param array &$persons + */ +function construct_episode_person_array($episode_persons, &$persons) +{ + foreach ($episode_persons as $episodePerson) { + if (array_key_exists($episodePerson->person->id, $persons)) { + $persons[$episodePerson->person->id]['roles'] .= + empty($episodePerson->person_group) || + empty($episodePerson->person_role) + ? '' + : (empty($persons[$episodePerson->person->id]['roles']) + ? '' + : ', ') . + lang( + 'PersonsTaxonomy.persons.' . + $episodePerson->person_group . + '.roles.' . + $episodePerson->person_role . + '.label' + ); + } else { + $persons[$episodePerson->person->id] = [ + 'full_name' => $episodePerson->person->full_name, + 'information_url' => $episodePerson->person->information_url, + 'thumbnail_url' => $episodePerson->person->image->thumbnail_url, + 'roles' => + empty($episodePerson->person_group) || + empty($episodePerson->person_role) + ? '' + : lang( + 'PersonsTaxonomy.persons.' . + $episodePerson->person_group . + '.roles.' . + $episodePerson->person_role . + '.label' + ), + ]; + } + } +} diff --git a/app/Helpers/url_helper.php b/app/Helpers/url_helper.php index ec462ca8e46be49f522ca3b6ed0cc99cd5c61f70..6fa434aefd44fb22fb6b544e245b0635416d0acd 100644 --- a/app/Helpers/url_helper.php +++ b/app/Helpers/url_helper.php @@ -38,3 +38,39 @@ if (!function_exists('current_season_url')) { return current_url() . $season_query_string; } } + +if (!function_exists('location_url')) { + /** + * Returns URL to display from location info + * + * @param string $locationName + * @param string $locationGeo + * @param string $locationOsmid + * + * @return string + */ + function location_url($locationName, $locationGeo, $locationOsmid) + { + $uri = ''; + + if (!empty($locationOsmid)) { + $uri = + 'https://www.openstreetmap.org/' . + ['N' => 'node', 'W' => 'way', 'R' => 'relation'][ + substr($locationOsmid, 0, 1) + ] . + '/' . + substr($locationOsmid, 1); + } elseif (!empty($locationGeo)) { + $uri = + 'https://www.openstreetmap.org/#map=17/' . + str_replace(',', '/', substr($locationGeo, 4)); + } elseif (!empty($locationName)) { + $uri = + 'https://www.openstreetmap.org/search?query=' . + urlencode($locationName); + } + + return $uri; + } +} diff --git a/app/Language/en/Breadcrumb.php b/app/Language/en/Breadcrumb.php index 03db0bce7f4e22e1c9257f41788613046373f423..ab1e511c9cbcf1c797cb2a7b42cca87840615a6e 100644 --- a/app/Language/en/Breadcrumb.php +++ b/app/Language/en/Breadcrumb.php @@ -32,4 +32,5 @@ return [ 'listening-time' => 'listening time', 'time-periods' => 'time periods', 'soundbites' => 'soundbites', + 'embeddable-player' => 'embeddable player', ]; diff --git a/app/Language/en/Episode.php b/app/Language/en/Episode.php index 00a487219f4e6e1e953b53e351a4cd7dc3d15525..18df1b40f447732545d440ffd5a6fa6058c5554b 100644 --- a/app/Language/en/Episode.php +++ b/app/Language/en/Episode.php @@ -109,4 +109,16 @@ return [ 'Click while playing to get current position, click again to get duration.', 'submit_edit' => 'Save all soundbites', ], + 'embeddable_player' => [ + 'add' => 'Add embeddable player', + 'title' => 'Embeddable player', + 'label' => + 'Pick a theme color, copy the embeddable player to clipboard, then paste it on your website.', + 'clipboard_iframe' => 'Copy embeddable player to clipboard', + 'clipboard_url' => 'Copy address to clipboard', + 'dark' => 'Dark', + 'dark-transparent' => 'Dark transparent', + 'light' => 'Light', + 'light-transparent' => 'Light transparent', + ], ]; diff --git a/app/Language/en/Platforms.php b/app/Language/en/Platforms.php index da822c972d3abba33e536faaaab00f22ef322a25..c1890482cb63d3b39e30bc15fc1bad415864f04a 100644 --- a/app/Language/en/Platforms.php +++ b/app/Language/en/Platforms.php @@ -11,6 +11,7 @@ return [ 'home_url' => 'Go to {platformName} website', 'submit_url' => 'Submit your podcast on {platformName}', 'visible' => 'Display in podcast homepage?', + 'on_embeddable_player' => 'Display on embeddable player?', 'remove' => 'Remove {platformName}', 'submit' => 'Save', 'messages' => [ diff --git a/app/Language/fr/Breadcrumb.php b/app/Language/fr/Breadcrumb.php index 961d403c081fd1fbf64e571dbaca19a51c692132..179238e9f139ac144f90552bde3defc2e29d5d61 100644 --- a/app/Language/fr/Breadcrumb.php +++ b/app/Language/fr/Breadcrumb.php @@ -32,4 +32,5 @@ return [ 'listening-time' => 'drée d’écoute', 'time-periods' => 'périodes', 'soundbites' => 'extraits sonores', + 'embeddable-player' => 'lecteur intégré', ]; diff --git a/app/Language/fr/Episode.php b/app/Language/fr/Episode.php index e691f9eed7401c2054296790734293b8072d98eb..78f94404c6bffa816e3aa1019085b444190a3bb6 100644 --- a/app/Language/fr/Episode.php +++ b/app/Language/fr/Episode.php @@ -111,4 +111,16 @@ return [ 'Cliquez pour récupérer la position actuelle, cliquez à nouveau pour récupérer la durée.', 'submit_edit' => 'Enregistrer tous les extraits sonores', ], + 'embeddable_player' => [ + 'add' => 'Ajouter un lecteur intégré', + 'title' => 'Lecteur intégré', + 'label' => + 'Sélectionnez une couleur de thème, copiez le code dans le presse-papier, puis collez-le sur votre site internet.', + 'clipboard_iframe' => 'Copier le lecteur dans le presse papier', + 'clipboard_url' => 'Copier l’adresse dans le presse papier', + 'dark' => 'Sombre', + 'dark-transparent' => 'Sombre transparent', + 'light' => 'Clair', + 'light-transparent' => 'Clair transparent', + ], ]; diff --git a/app/Language/fr/Platforms.php b/app/Language/fr/Platforms.php index d7b66d639afa5f699e221ada146cda050f08f7ee..4a4fdc5d8814158d8d56e444812df871437f16c1 100644 --- a/app/Language/fr/Platforms.php +++ b/app/Language/fr/Platforms.php @@ -11,6 +11,7 @@ return [ 'home_url' => 'Aller au site {platformName}', 'submit_url' => 'Soumettez votre podcast sur {platformName}', 'visible' => 'Afficher sur la page d’accueil du podcast ?', + 'on_embeddable_player' => 'Afficher sur le lecteur intégré ?', 'remove' => 'Supprimer {platformName}', 'submit' => 'Enregistrer', 'messages' => [ diff --git a/app/Models/EpisodeModel.php b/app/Models/EpisodeModel.php index eacbac9b3c2e91b4c4614fbde6126beda2d49961..3237daafa1b532b741690e45f5784c553ce2e375 100644 --- a/app/Models/EpisodeModel.php +++ b/app/Models/EpisodeModel.php @@ -69,6 +69,35 @@ class EpisodeModel extends Model protected $afterUpdate = ['writeEnclosureMetadata']; protected $beforeDelete = ['clearCache']; + public static $themes = [ + 'light-transparent' => [ + 'style' => + 'background-color: #fff; background-image: linear-gradient(45deg, #ccc 12.5%, transparent 12.5%, transparent 50%, #ccc 50%, #ccc 62.5%, transparent 62.5%, transparent 100%); background-size: 5.66px 5.66px;', + 'background' => 'transparent', + 'text' => '#000', + 'inverted' => '#fff', + ], + 'light' => [ + 'style' => 'background-color: #fff;', + 'background' => '#fff', + 'text' => '#000', + 'inverted' => '#fff', + ], + 'dark-transparent' => [ + 'style' => + 'background-color: #001f1a; background-image: linear-gradient(45deg, #888 12.5%, transparent 12.5%, transparent 50%, #888 50%, #888 62.5%, transparent 62.5%, transparent 100%); background-size: 5.66px 5.66px;', + 'background' => 'transparent', + 'text' => '#fff', + 'inverted' => '#000', + ], + 'dark' => [ + 'style' => 'background-color: #001f1a;', + 'background' => '#001f1a', + 'text' => '#fff', + 'inverted' => '#000', + ], + ]; + public function getEpisodeBySlug($podcastId, $episodeSlug) { if (!($found = cache("podcast{$podcastId}_episode@{$episodeSlug}"))) { @@ -411,6 +440,14 @@ class EpisodeModel extends Model } } + foreach (array_keys(self::$themes) as $themeKey) { + foreach ($supportedLocales as $locale) { + cache()->delete( + "page_podcast{$episode->podcast_id}_episode{$episode->id}_embeddable_player_{$themeKey}_{$locale}" + ); + } + } + // delete query cache cache()->delete("podcast{$episode->podcast_id}_defaultQuery"); cache()->delete("podcast{$episode->podcast_id}_years"); diff --git a/app/Models/PlatformModel.php b/app/Models/PlatformModel.php index 827c4de1fff0957b1254d7add4127d7357c2c5db..1307d1a42503488a6a07f31a4acb04e0391ce476 100644 --- a/app/Models/PlatformModel.php +++ b/app/Models/PlatformModel.php @@ -75,7 +75,7 @@ class PlatformModel extends Model !($found = cache("podcast{$podcastId}_platforms_{$platformType}")) ) { $found = $this->select( - 'platforms.*, podcasts_platforms.link_url, podcasts_platforms.link_content, podcasts_platforms.is_visible' + 'platforms.*, podcasts_platforms.link_url, podcasts_platforms.link_content, podcasts_platforms.is_visible, podcasts_platforms.is_on_embeddable_player' ) ->join( 'podcasts_platforms', @@ -103,7 +103,7 @@ class PlatformModel extends Model )) ) { $found = $this->select( - 'platforms.*, podcasts_platforms.link_url, podcasts_platforms.link_content, podcasts_platforms.is_visible' + 'platforms.*, podcasts_platforms.link_url, podcasts_platforms.link_content, podcasts_platforms.is_visible, podcasts_platforms.is_on_embeddable_player' ) ->join( 'podcasts_platforms', @@ -168,6 +168,8 @@ EOD; public function clearCache($podcastId) { + $podcast = (new PodcastModel())->getPodcastById($podcastId); + foreach (['podcasting', 'social', 'funding'] as $platformType) { cache()->delete("podcast{$podcastId}_platforms_{$platformType}"); cache()->delete( @@ -195,5 +197,22 @@ EOD; ); } } + + // clear cache for every localized podcast episode page + foreach ($podcast->episodes as $episode) { + foreach ($supportedLocales as $locale) { + cache()->delete( + "page_podcast{$podcast->id}_episode{$episode->id}_{$locale}" + ); + foreach ( + array_keys(\App\Models\EpisodeModel::$themes) + as $themeKey + ) { + cache()->delete( + "page_podcast{$podcast->id}_episode{$episode->id}_embeddable_player_{$themeKey}_{$locale}" + ); + } + } + } } } diff --git a/app/Models/PodcastModel.php b/app/Models/PodcastModel.php index 4bfed81d2cf810f03a177f82ea69a0d885d3ac45..fc23d5dd02a1d312cde6feba11407ad627d18b6b 100644 --- a/app/Models/PodcastModel.php +++ b/app/Models/PodcastModel.php @@ -193,6 +193,14 @@ class PodcastModel extends Model cache()->delete( "page_podcast{$podcast->id}_episode{$episode->id}_{$locale}" ); + foreach ( + array_keys(\App\Models\EpisodeModel::$themes) + as $themeKey + ) { + cache()->delete( + "page_podcast{$podcast->id}_episode{$episode->id}_embeddable_player_{$themeKey}_{$locale}" + ); + } } } // clear cache for every credit page diff --git a/app/Views/_assets/admin.ts b/app/Views/_assets/admin.ts index 1c54fec275396cba346155ede9fe37c95988f5d4..8cb179e0016bd0b033e7a8f76cd693d8865c914a 100644 --- a/app/Views/_assets/admin.ts +++ b/app/Views/_assets/admin.ts @@ -1,4 +1,6 @@ import ClientTimezone from "./modules/ClientTimezone"; +import Clipboard from "./modules/Clipboard"; +import ThemePicker from "./modules/ThemePicker"; import DateTimePicker from "./modules/DateTimePicker"; import Dropdown from "./modules/Dropdown"; import MarkdownEditor from "./modules/MarkdownEditor"; @@ -19,3 +21,5 @@ ClientTimezone(); DateTimePicker(); Time(); Soundbites(); +Clipboard(); +ThemePicker(); diff --git a/app/Views/_assets/icons/file-copy.svg b/app/Views/_assets/icons/file-copy.svg new file mode 100644 index 0000000000000000000000000000000000000000..491df11d372ab1a6668a7193a78734abd4a96181 --- /dev/null +++ b/app/Views/_assets/icons/file-copy.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M7 6V3a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1h-3v3c0 .552-.45 1-1.007 1H4.007A1.001 1.001 0 0 1 3 21l.003-14c0-.552.45-1 1.006-1H7zM5.002 8L5 20h10V8H5.002zM9 6h8v10h2V4H9v2zm-2 5h6v2H7v-2zm0 4h6v2H7v-2z"/></svg> \ No newline at end of file diff --git a/app/Views/_assets/icons/movie.svg b/app/Views/_assets/icons/movie.svg new file mode 100644 index 0000000000000000000000000000000000000000..a3eaa1b73d707fc82251ced6f3ce081acff9c7cd --- /dev/null +++ b/app/Views/_assets/icons/movie.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M2 3.993A1 1 0 0 1 2.992 3h18.016c.548 0 .992.445.992.993v16.014a1 1 0 0 1-.992.993H2.992A.993.993 0 0 1 2 20.007V3.993zM4 5v14h16V5H4zm6.622 3.415l4.879 3.252a.4.4 0 0 1 0 .666l-4.88 3.252a.4.4 0 0 1-.621-.332V8.747a.4.4 0 0 1 .622-.332z"/></svg> \ No newline at end of file diff --git a/app/Views/_assets/modules/Clipboard.ts b/app/Views/_assets/modules/Clipboard.ts new file mode 100644 index 0000000000000000000000000000000000000000..2b5b0a59acb02a5ee84ee0155316f0443e43cd62 --- /dev/null +++ b/app/Views/_assets/modules/Clipboard.ts @@ -0,0 +1,23 @@ +const Clipboard = (): void => { + const buttons: NodeListOf< + HTMLButtonElement + > | null = document.querySelectorAll("button[data-type='clipboard-copy']"); + + if (buttons) { + for (let i = 0; i < buttons.length; i++) { + const button: HTMLButtonElement = buttons[i]; + const textArea: HTMLTextAreaElement | null = document.querySelector( + `textarea[id="${button.dataset.clipboardTarget}"]` + ); + if (textArea) { + button.addEventListener("click", () => { + textArea.select(); + textArea.setSelectionRange(0, textArea.value.length); + document.execCommand("copy"); + }); + } + } + } +}; + +export default Clipboard; diff --git a/app/Views/_assets/modules/ThemePicker.ts b/app/Views/_assets/modules/ThemePicker.ts new file mode 100644 index 0000000000000000000000000000000000000000..167f2600206df9cb590573de5af6578ff5b316ae --- /dev/null +++ b/app/Views/_assets/modules/ThemePicker.ts @@ -0,0 +1,30 @@ +const ThemePicker = (): void => { + const buttons: NodeListOf< + HTMLButtonElement + > | null = document.querySelectorAll("button[data-type='theme-picker']"); + const iframe: HTMLIFrameElement | null = document.querySelector( + `iframe[id="embeddable_player"]` + ); + const iframeTextArea: HTMLTextAreaElement | null = document.querySelector( + `textarea[id="iframe"]` + ); + const urlTextArea: HTMLTextAreaElement | null = document.querySelector( + `textarea[id="url"]` + ); + + if (buttons && iframe && iframeTextArea && urlTextArea) { + for (let i = 0; i < buttons.length; i++) { + const button: HTMLButtonElement = buttons[i]; + const url: string | undefined = button.dataset.url; + if (url) { + button.addEventListener("click", () => { + iframeTextArea.value = `<iframe width="100%" height="280" frameborder="0" scrolling="no" style="width: 100%; height: 280px; overflow: hidden;" src="${url}"></iframe>`; + urlTextArea.value = url; + iframe.src = url; + }); + } + } + } +}; + +export default ThemePicker; diff --git a/app/Views/admin/episode/embeddable_player.php b/app/Views/admin/episode/embeddable_player.php new file mode 100644 index 0000000000000000000000000000000000000000..2144580e1e003acf702f145de1cf68f8a5ae420a --- /dev/null +++ b/app/Views/admin/episode/embeddable_player.php @@ -0,0 +1,66 @@ +<?= $this->extend('admin/_layout') ?> + +<?= $this->section('title') ?> +<?= lang('Episode.embeddable_player.title') ?> +<?= $this->endSection() ?> + +<?= $this->section('pageTitle') ?> +<?= lang('Episode.embeddable_player.title') ?> +<?= $this->endSection() ?> + +<?= $this->section('content') ?> + + <?= form_label(lang('Episode.embeddable_player.label'), 'label') ?> + + <div class="flex w-full mt-6 mb-6"> + <?php foreach ($themes as $themeKey => $theme): ?> + <button style="<?= $theme[ + 'style' + ] ?>" class="w-12 h-12 mr-1 border-2 border-gray-400 rounded-lg hover:border-white" title="<?= lang("Episode.embeddable_player.{$themeKey}") ?>" data-type="theme-picker" data-url="<?= $episode->getEmbeddablePlayer( + $themeKey +) ?>"></button> + <?php endforeach; ?> + </div> + + <iframe name="embeddable_player" id="embeddable_player" width="100%" height="280" frameborder="0" scrolling="no" style="width: 100%; height: 280; overflow: hidden;" src="<?= $episode->embeddable_player ?>"></iframe> + + <div class="flex items-center w-full mt-8"> + <?= form_textarea( + [ + 'id' => 'iframe', + 'name' => 'iframe', + 'class' => 'form-textarea w-full h-20 mr-2', + ], + "<iframe width=\"100%\" height=\"280\" frameborder=\"0\" scrolling=\"no\" style=\"width: 100%; height: 280px; overflow: hidden;\" src=\"{$episode->embeddable_player}\"></iframe>" + ) ?> + <?= icon_button( + 'file-copy', + lang('Episode.embeddable_player.clipboard_iframe'), + null, + ['variant' => 'default'], + [ + 'data-type' => 'clipboard-copy', + 'data-clipboard-target' => 'iframe', + ] + ) ?> + </div> + + <div class="flex items-center w-full mt-4"> + <?= form_textarea( + [ + 'id' => 'url', + 'name' => 'url', + 'class' => 'form-textarea w-full h-10 mr-2', + ], + $episode->embeddable_player + ) ?> + <?= icon_button( + 'file-copy', + lang('Episode.embeddable_player.clipboard_url'), + null, + ['variant' => 'default'], + ['data-type' => 'clipboard-copy', 'data-clipboard-target' => 'url'] + ) ?> + </div> + +<?= $this->endSection() ?> diff --git a/app/Views/admin/episode/list.php b/app/Views/admin/episode/list.php index 6bed3c358aed1680d5bc611ea732bf848eb3e4b2..fc0f61e37348abbffe019f965a91a69c4651e304 100644 --- a/app/Views/admin/episode/list.php +++ b/app/Views/admin/episode/list.php @@ -61,6 +61,13 @@ $podcast->id, $episode->id ) ?>"><?= lang('Episode.edit') ?></a> + <a class="px-4 py-1 hover:bg-gray-100" href="<?= route_to( + 'embeddable-player-add', + $podcast->id, + $episode->id + ) ?>"><?= lang( + 'Episode.embeddable_player.add' +) ?></a> <a class="px-4 py-1 hover:bg-gray-100" href="<?= route_to( 'episode-person-manage', $podcast->id, diff --git a/app/Views/admin/episode/view.php b/app/Views/admin/episode/view.php index 08956ec37c83e7c8be6fb287445d167fa5cdd629..bc2dfd4641148173ef3a8146884949e4969c759a 100644 --- a/app/Views/admin/episode/view.php +++ b/app/Views/admin/episode/view.php @@ -58,6 +58,12 @@ </div> <div class="mb-12"> + <?= button( + lang('Episode.embeddable_player.add'), + route_to('embeddable-player-add', $podcast->id, $episode->id), + ['variant' => 'info', 'iconLeft' => 'movie'], + ['class' => 'mb-4'] + ) ?> <?= button( lang('Episode.soundbites_form.title'), route_to('soundbites-edit', $podcast->id, $episode->id), diff --git a/app/Views/admin/podcast/latest_episodes.php b/app/Views/admin/podcast/latest_episodes.php index 7c6f17dba3a7bb0bc6c463b1c355d8f7a13afa62..71ec4389ba174c73e91791158a0b7944f5ec28d1 100644 --- a/app/Views/admin/podcast/latest_episodes.php +++ b/app/Views/admin/podcast/latest_episodes.php @@ -58,6 +58,13 @@ $podcast->id, $episode->id ) ?>"><?= lang('Episode.edit') ?></a> + <a class="px-4 py-1 hover:bg-gray-100" href="<?= route_to( + 'embeddable-player-add', + $podcast->id, + $episode->id + ) ?>"><?= lang( + 'Episode.embeddable_player.add' +) ?></a> <a class="px-4 py-1 hover:bg-gray-100" href="<?= route_to( 'episode-person-manage', $podcast->id, diff --git a/app/Views/admin/podcast/platforms.php b/app/Views/admin/podcast/platforms.php index 06e3df6fab7f01b51fa53f0d89233cc4418d11f6..88365ad8bd4203a3463da18271c37e626ba5be2c 100644 --- a/app/Views/admin/podcast/platforms.php +++ b/app/Views/admin/podcast/platforms.php @@ -115,6 +115,22 @@ $platform->slug . '_visible', $platform->is_visible ? $platform->is_visible : false ), + 'text-sm mb-1' + ) ?> + <?= form_switch( + lang('Platforms.on_embeddable_player'), + [ + 'id' => $platform->slug . '_on_embeddable_player', + 'name' => + 'platforms[' . $platform->slug . '][on_embeddable_player]', + ], + 'yes', + old( + $platform->slug . '_on_embeddable_player', + $platform->is_on_embeddable_player + ? $platform->is_on_embeddable_player + : false + ), 'text-sm' ) ?> </div> diff --git a/app/Views/embeddable_player.php b/app/Views/embeddable_player.php new file mode 100644 index 0000000000000000000000000000000000000000..a20367325969bed877b8961db872259b8231414a --- /dev/null +++ b/app/Views/embeddable_player.php @@ -0,0 +1,214 @@ +<!DOCTYPE html> +<html lang="<?= service('request')->getLocale() ?>"> +<head> + <meta charset="UTF-8" /> + <title><?= $episode->title ?></title> + <meta name="description" + content="<?= htmlspecialchars($episode->description) ?>"/> + <link rel="shortcut icon" type="image/x-icon" href="/favicon.ico" /> + <link rel="stylesheet" href="/assets/index.css" /> + <link rel="canonical" href="<?= $episode->link ?>" /> +</head> +<body> + <div class="flex w-full p-1 md:p-2"style="background: <?= $theme[ + 'background' + ] ?>; color: <?= $theme['text'] ?>;"> + <img src="<?= $episode->image + ->medium_url ?>" alt="<?= $episode->title ?>" class="w-32 h-32 md:w-64 md:h-64" /> + <div class="flex-grow pl-4"> + <div class="flex"> + <a href="<?= route_to('podcast', $podcast->name) ?>" + style="color: <?= $theme['text'] ?>;" + class="flex flex-col text-base leading-tight opacity-50 md:text-lg hover:opacity-100" target="_blank"> + <?= $podcast->title ?> + </a> + <address class="ml-2 text-xs opacity-50 md:text-sm"> + <?= lang('Podcast.by', [ + 'publisher' => $podcast->publisher, + ]) ?></address> + </div> + + <div class="flex mt-1 space-x-2 md:space-x-4 md:mt-3 md:top-0 md:mr-4 md:right-0 md:absolute "> + <?php if ($podcast->has_social_platforms): ?> + <div class="flex space-x-1"> + <?php foreach ( + $podcast->social_platforms + as $socialPlatform + ): ?> + <?php if ( + $socialPlatform->is_on_embeddable_player + ): ?> + <?= anchor( + $socialPlatform->link_url, + platform_icon( + $socialPlatform->type, + $socialPlatform->slug, + 'h-4 md:h-6' + ), + [ + 'target' => '_blank', + 'rel' => 'noopener noreferrer', + 'title' => $socialPlatform->label, + 'class' => + 'opacity-50 hover:opacity-100', + ] + ) ?> + <?php endif; ?> + <?php endforeach; ?> + </div> + <?php endif; ?> + <?php if ($podcast->has_funding_platforms): ?> + <div class="flex space-x-1"> + <?php foreach ( + $podcast->funding_platforms + as $fundingPlatform + ): ?> + <?php if ( + $fundingPlatform->is_on_embeddable_player + ): ?> + <?= anchor( + $fundingPlatform->link_url, + platform_icon( + $fundingPlatform->type, + $fundingPlatform->slug, + 'h-4 md:h-6' + ), + [ + 'target' => '_blank', + 'rel' => 'noopener noreferrer', + 'title' => $fundingPlatform->label, + 'class' => + 'opacity-50 hover:opacity-100', + ] + ) ?> + <?php endif; ?> + <?php endforeach; ?> + </div> + <?php endif; ?> + <div class="flex space-x-1"> + <?php foreach ( + $podcast->podcasting_platforms + as $podcastingPlatform + ): ?> + <?php if ($podcastingPlatform->is_on_embeddable_player): ?> + <?= anchor( + $podcastingPlatform->link_url, + platform_icon( + $podcastingPlatform->type, + $podcastingPlatform->slug, + 'h-4 md:h-6' + ), + [ + 'target' => '_blank', + 'rel' => 'noopener noreferrer', + 'title' => $podcastingPlatform->label, + 'class' => 'opacity-50 hover:opacity-100', + ] + ) ?> + <?php endif; ?> + <?php endforeach; ?> + <?= anchor( + route_to('podcast_feed', $podcast->name), + icon('rss', 'mr-2') . lang('Podcast.feed'), + [ + 'target' => '_blank', + 'class' => + 'text-white h-4 md:h-6 md:text-sm text-xs bg-gradient-to-r from-orange-400 to-red-500 hover:to-orange-500 hover:bg-orange-500 inline-flex items-center px-2 py-1 font-semibold rounded-md md:rounded-lg shadow-md hover:bg-orange-600', + ] + ) ?> + </div> + </div> + <h1 class="mt-2 text-xl font-semibold opacity-100 md:text-3xl hover:opacity-75"> + <a href="<?= $episode->link ?>" + style="color: <?= $theme['text'] ?>;" + target="_blank"> + <?= $episode->title ?> + </a> + </h1> + <div class="flex w-full"> + <div + style="color: <?= $theme['text'] ?>;" + class="text-sm opacity-50 md:text-base"> + <?= episode_numbering( + $episode->number, + $episode->season_number + ) ?> + <div> + <time + pubdate + datetime="<?= $episode->published_at->format( + DateTime::ATOM + ) ?>" + title="<?= $episode->published_at ?>"> + <?= lang('Common.mediumDate', [ + $episode->published_at, + ]) ?> + </time> + <span>•</span> + <time datetime="PT<?= $episode->enclosure_duration ?>S"> + <?= format_duration($episode->enclosure_duration) ?> + </time> + </div> + </div> + <?php if ($episode->location_name): ?> + <a href="<?= location_url( + $episode->location_name, + $episode->location_geo, + $episode->location_osmid + ) ?>" + style="color: <?= $theme['inverted'] ?>; background: <?= $theme[ + 'text' +] ?>;" class="inline-flex items-center px-3 py-1 mt-1 ml-4 text-xs align-middle rounded-full shadow-xs outline-none opacity-50 md:mt-2 md:text-sm hover:opacity-75 focus:shadow-outline" target="_blank" rel="noreferrer noopener"><?= icon( + 'map-pin' +) ?> + <?= $episode->location_name ?> + </a> + <?php endif; ?> + </div> + + <?php if (!empty($persons)): ?> + <div class="flex my-2 space-x-1 md:my-4 md:space-x-2"> + <?php foreach ($persons as $person): ?> + <?php if (!empty($person['information_url'])): ?> + <a href="<?= $person['information_url'] ?>" + class="hover:opacity-50" + target="_blank" + rel="noreferrer noopener"> + <?php endif; ?> + <img src="<?= $person['thumbnail_url'] ?>" + alt="<?= $person['full_name'] ?>" + title="[<?= $person[ + 'full_name' + ] ?>] <?= $person['roles'] ?>" + class="object-cover h-8 rounded-full md:h-12 md:w-12" /> + <?php if (!empty($person['information_url'])): ?> + </a> + <?php endif; ?> + <?php endforeach; ?> + </div> + <?php endif; ?> + <audio controls preload="none" class="flex w-full mt-2 md:mt-4"> + <source + src="<?= $episode->enclosure_url . + (isset($_SERVER['HTTP_REFERER']) + ? '?_from=' . + parse_url($_SERVER['HTTP_REFERER'], PHP_URL_HOST) + : '') ?>" + type="<?= $episode->enclosure_type ?>" /> + Your browser does not support the audio tag. + </audio> + </div> + + + <a href="https://castopod.org/" + class="absolute bottom-0 right-0 mb-4 mr-4 hover:opacity-75" + title="<?= lang('Common.powered_by', [ + 'castopod' => 'Castopod', + ]) ?>" + target="_blank" + rel="noopener noreferrer"> + <?= platform_icon('podcasting', 'castopod', 'h-6') ?> + </a> + </div> +</body> +</html> \ No newline at end of file diff --git a/app/Views/episode.php b/app/Views/episode.php index 0b116d7bd4bf97c8bd4dbd89dd8e96af283668da..5570d4e7fa93e791eea55056a2c7cbeb720353df 100644 --- a/app/Views/episode.php +++ b/app/Views/episode.php @@ -5,15 +5,16 @@ <head> <meta charset="UTF-8"/> <title><?= $episode->title ?></title> - <meta name="description" content="<?= $episode->description ?>"/> - <meta name="viewport" content="width=device-width, initial-scale=1.0"/> + <meta name="description" + content="<?= htmlspecialchars($episode->description) ?>" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <?php if ( !empty($podcast->payment_pointer) -): ?> <meta name="monetization" content="<?= $podcast->payment_pointer ?>"> +): ?> <meta name="monetization" content="<?= $podcast->payment_pointer ?>" /> <?php endif; ?> <link rel="shortcut icon" type="image/png" href="/favicon.ico" /> <link rel="stylesheet" href="/assets/index.css"/> - <link rel="canonical" href="<?= current_url() ?>" /> + <link rel="canonical" href="<?= $episode->link ?>" /> <script src="/assets/podcast.js" type="module" defer></script> <meta property="og:title" content="<?= $episode->title ?>" /> <meta property="og:locale" content="<?= $podcast->language_code ?>" /> diff --git a/app/Views/podcast.php b/app/Views/podcast.php index 29fed4b48ed4d97e1dd8547bab98ebd8c313c83c..2b42958621db83b0a74be42ae8fa9d2531c9ec85 100644 --- a/app/Views/podcast.php +++ b/app/Views/podcast.php @@ -6,11 +6,12 @@ <head> <meta charset="UTF-8"/> <title><?= $podcast->title ?></title> - <meta name="description" content="<?= $podcast->description ?>"/> - <meta name="viewport" content="width=device-width, initial-scale=1.0"/> + <meta name="description" + content="<?= htmlspecialchars($podcast->description) ?>" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <?php if ( !empty($podcast->payment_pointer) -): ?> <meta name="monetization" content="<?= $podcast->payment_pointer ?>"> +): ?> <meta name="monetization" content="<?= $podcast->payment_pointer ?>" /> <?php endif; ?> <link rel="shortcut icon" type="image/png" href="/favicon.ico" /> <link rel="stylesheet" href="/assets/index.css"/>