feat(embeddable-player): add embeddable player widget

This commit is contained in:
Benjamin Bellamy 2021-02-27 21:21:26 +00:00
parent 526809ef28
commit 141788fa08
36 changed files with 821 additions and 121 deletions

View File

@ -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']);
});

View File

@ -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);
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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']);

View File

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

View File

@ -21,5 +21,6 @@ class Platform extends Entity
'link_url' => '?string',
'link_content' => '?string',
'is_visible' => '?boolean',
'is_on_embeddable_player' => '?boolean',
];
}

View File

@ -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)) {

View File

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

View File

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

View File

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

View File

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

View File

@ -32,4 +32,5 @@ return [
'listening-time' => 'listening time',
'time-periods' => 'time periods',
'soundbites' => 'soundbites',
'embeddable-player' => 'embeddable player',
];

View File

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

View File

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

View File

@ -32,4 +32,5 @@ return [
'listening-time' => 'drée découte',
'time-periods' => 'périodes',
'soundbites' => 'extraits sonores',
'embeddable-player' => 'lecteur intégré',
];

View File

@ -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 ladresse dans le presse papier',
'dark' => 'Sombre',
'dark-transparent' => 'Sombre transparent',
'light' => 'Clair',
'light-transparent' => 'Clair transparent',
],
];

View File

@ -11,6 +11,7 @@ return [
'home_url' => 'Aller au site {platformName}',
'submit_url' => 'Soumettez votre podcast sur {platformName}',
'visible' => 'Afficher sur la page daccueil du podcast?',
'on_embeddable_player' => 'Afficher sur le lecteur intégré?',
'remove' => 'Supprimer {platformName}',
'submit' => 'Enregistrer',
'messages' => [

View File

@ -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");

View File

@ -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}"
);
}
}
}
}
}

View File

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

View File

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

View File

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

After

Width:  |  Height:  |  Size: 350 B

View File

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

After

Width:  |  Height:  |  Size: 376 B

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 ?>" />

View File

@ -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"/>