mirror of
https://code.castopod.org/adaures/castopod.git
synced 2024-09-27 20:21:59 +02:00
feat: add js audio player on podcast, admin and embeddable player pages + fix admon episodes ux
- use vimejs as audio player - add global audio player + play episode buttons on public pages - refactor admin episodes list from a grid to a data table - arrange episode cards to be more readable closes #131
This commit is contained in:
parent
b72e7c8691
commit
0e14eb4d3f
@ -103,7 +103,7 @@ class EpisodeController extends BaseController
|
|||||||
|
|
||||||
public function embeddablePlayer(string $theme = 'light-transparent'): string
|
public function embeddablePlayer(string $theme = 'light-transparent'): string
|
||||||
{
|
{
|
||||||
header('Content-Security-Policy: frame-ancestors https://* http://*');
|
header('Content-Security-Policy: frame-ancestors http://*:* https://*:*');
|
||||||
|
|
||||||
// Prevent analytics hit when authenticated
|
// Prevent analytics hit when authenticated
|
||||||
if (! can_user_interact()) {
|
if (! can_user_interact()) {
|
||||||
@ -122,12 +122,13 @@ class EpisodeController extends BaseController
|
|||||||
$cacheName = "page_podcast#{$this->podcast->id}_episode#{$this->episode->id}_embeddable_player_{$theme}_{$locale}";
|
$cacheName = "page_podcast#{$this->podcast->id}_episode#{$this->episode->id}_embeddable_player_{$theme}_{$locale}";
|
||||||
|
|
||||||
if (! ($cachedView = cache($cacheName))) {
|
if (! ($cachedView = cache($cacheName))) {
|
||||||
$theme = EpisodeModel::$themes[$theme];
|
$themeData = EpisodeModel::$themes[$theme];
|
||||||
|
|
||||||
$data = [
|
$data = [
|
||||||
'podcast' => $this->podcast,
|
'podcast' => $this->podcast,
|
||||||
'episode' => $this->episode,
|
'episode' => $this->episode,
|
||||||
'theme' => $theme,
|
'theme' => $theme,
|
||||||
|
'themeData' => $themeData,
|
||||||
];
|
];
|
||||||
|
|
||||||
$secondsToNextUnpublishedEpisode = (new EpisodeModel())->getSecondsToNextUnpublishedEpisode(
|
$secondsToNextUnpublishedEpisode = (new EpisodeModel())->getSecondsToNextUnpublishedEpisode(
|
||||||
@ -159,9 +160,9 @@ class EpisodeController extends BaseController
|
|||||||
'html' =>
|
'html' =>
|
||||||
'<iframe src="' .
|
'<iframe src="' .
|
||||||
$this->episode->embeddable_player_url .
|
$this->episode->embeddable_player_url .
|
||||||
'" width="100%" height="200" frameborder="0" scrolling="no"></iframe>',
|
'" width="100%" height="144" frameborder="0" scrolling="no"></iframe>',
|
||||||
'width' => 600,
|
'width' => 600,
|
||||||
'height' => 200,
|
'height' => 144,
|
||||||
'thumbnail_url' => $this->episode->image->large_url,
|
'thumbnail_url' => $this->episode->image->large_url,
|
||||||
'thumbnail_width' => config('Images')
|
'thumbnail_width' => config('Images')
|
||||||
->largeSize,
|
->largeSize,
|
||||||
@ -189,11 +190,11 @@ class EpisodeController extends BaseController
|
|||||||
htmlentities(
|
htmlentities(
|
||||||
'<iframe src="' .
|
'<iframe src="' .
|
||||||
$this->episode->embeddable_player_url .
|
$this->episode->embeddable_player_url .
|
||||||
'" width="100%" height="200" frameborder="0" scrolling="no"></iframe>',
|
'" width="100%" height="144" frameborder="0" scrolling="no"></iframe>',
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
$oembed->addChild('width', '600');
|
$oembed->addChild('width', '600');
|
||||||
$oembed->addChild('height', '200');
|
$oembed->addChild('height', '144');
|
||||||
|
|
||||||
return $this->response->setXML((string) $oembed);
|
return $this->response->setXML((string) $oembed);
|
||||||
}
|
}
|
||||||
|
@ -185,7 +185,7 @@ if (! function_exists('data_table')) {
|
|||||||
* @param mixed[] $data data to loop through and display in rows
|
* @param mixed[] $data data to loop through and display in rows
|
||||||
* @param mixed ...$rest Any other argument to pass to the `cell` function
|
* @param mixed ...$rest Any other argument to pass to the `cell` function
|
||||||
*/
|
*/
|
||||||
function data_table(array $columns, array $data = [], ...$rest): string
|
function data_table(array $columns, array $data = [], string $class = '', ...$rest): string
|
||||||
{
|
{
|
||||||
$table = new Table();
|
$table = new Table();
|
||||||
|
|
||||||
@ -199,8 +199,8 @@ if (! function_exists('data_table')) {
|
|||||||
'cell_start' => '<td class="px-4 py-2">',
|
'cell_start' => '<td class="px-4 py-2">',
|
||||||
'cell_alt_start' => '<td class="px-4 py-2">',
|
'cell_alt_start' => '<td class="px-4 py-2">',
|
||||||
|
|
||||||
'row_start' => '<tr class="bg-gray-100 hover:bg-pine-100">',
|
'row_start' => '<tr class="bg-gray-50 hover:bg-pine-50">',
|
||||||
'row_alt_start' => '<tr class="hover:bg-pine-100">',
|
'row_alt_start' => '<tr class="hover:bg-pine-50">',
|
||||||
];
|
];
|
||||||
|
|
||||||
$table->setTemplate($template);
|
$table->setTemplate($template);
|
||||||
@ -225,7 +225,7 @@ if (! function_exists('data_table')) {
|
|||||||
return lang('Common.no_data');
|
return lang('Common.no_data');
|
||||||
}
|
}
|
||||||
|
|
||||||
return '<div class="overflow-x-auto bg-white rounded-lg shadow" >' .
|
return '<div class="overflow-x-auto bg-white rounded-lg shadow ' . $class . '" >' .
|
||||||
$table->generate() .
|
$table->generate() .
|
||||||
'</div>';
|
'</div>';
|
||||||
}
|
}
|
||||||
@ -241,28 +241,16 @@ if (! function_exists('publication_pill')) {
|
|||||||
*/
|
*/
|
||||||
function publication_pill(?Time $publicationDate, string $publicationStatus, string $customClass = ''): string
|
function publication_pill(?Time $publicationDate, string $publicationStatus, string $customClass = ''): string
|
||||||
{
|
{
|
||||||
if ($publicationDate === null) {
|
$class = match ($publicationStatus) {
|
||||||
return '';
|
'published' => 'text-pine-600 border-pine-600 bg-pine-50',
|
||||||
}
|
'scheduled' => 'text-red-600 border-red-600 bg-red-50',
|
||||||
|
'not_published' => 'text-gray-600 border-gray-600 bg-gray-50',
|
||||||
|
default => 'text-gray-600 border-gray-600 bg-gray-50',
|
||||||
|
};
|
||||||
|
|
||||||
$class =
|
$label = lang('Episode.publication_status.' . $publicationStatus);
|
||||||
$publicationStatus === 'published'
|
|
||||||
? 'text-pine-500 border-pine-500'
|
|
||||||
: 'text-red-600 border-red-600';
|
|
||||||
|
|
||||||
$langOptions = [
|
return '<span ' . ($publicationDate === null ? '' : 'title="' . $publicationDate . '"') . ' class="px-1 font-semibold border rounded ' .
|
||||||
'<time pubdate datetime="' .
|
|
||||||
$publicationDate->format(DateTime::ATOM) .
|
|
||||||
'" title="' .
|
|
||||||
$publicationDate .
|
|
||||||
'">' .
|
|
||||||
lang('Common.mediumDate', [$publicationDate]) .
|
|
||||||
'</time>',
|
|
||||||
];
|
|
||||||
|
|
||||||
$label = lang('Episode.publication_status.' . $publicationStatus, $langOptions);
|
|
||||||
|
|
||||||
return '<span class="px-1 font-semibold border ' .
|
|
||||||
$class .
|
$class .
|
||||||
' ' .
|
' ' .
|
||||||
$customClass .
|
$customClass .
|
||||||
@ -354,7 +342,7 @@ if (! function_exists('episode_numbering')) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($isAbbr) {
|
if ($isAbbr) {
|
||||||
return '<abbr class="' .
|
return '<abbr class="tracking-wider ' .
|
||||||
$class .
|
$class .
|
||||||
'" title="' .
|
'" title="' .
|
||||||
lang($transKey, $args) .
|
lang($transKey, $args) .
|
||||||
@ -450,4 +438,79 @@ if (! function_exists('person_list')) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// ------------------------------------------------------------------------
|
// ------------------------------------------------------------------------
|
||||||
|
|
||||||
|
if (! function_exists('play_episode_button')) {
|
||||||
|
/**
|
||||||
|
* Returns play episode button
|
||||||
|
*/
|
||||||
|
function play_episode_button(
|
||||||
|
string $episodeId,
|
||||||
|
string $episodeThumbnail,
|
||||||
|
string $episodeTitle,
|
||||||
|
string $podcastTitle,
|
||||||
|
string $source,
|
||||||
|
string $mediaType,
|
||||||
|
string $class = ''
|
||||||
|
): string {
|
||||||
|
$playLabel = lang('Common.play_episode_button.play');
|
||||||
|
$playingLabel = lang('Common.play_episode_button.playing');
|
||||||
|
|
||||||
|
return <<<CODE_SAMPLE
|
||||||
|
<play-episode-button
|
||||||
|
class="${class}"
|
||||||
|
id="${episodeId}"
|
||||||
|
imageSrc=${episodeThumbnail}
|
||||||
|
title="${episodeTitle}"
|
||||||
|
podcast="${podcastTitle}"
|
||||||
|
src="${source}"
|
||||||
|
mediaType="${mediaType}"
|
||||||
|
playLabel="Play"
|
||||||
|
playingLabel="Playing"
|
||||||
|
></play-episode-button>
|
||||||
|
CODE_SAMPLE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
if (! function_exists('audio_player')) {
|
||||||
|
/**
|
||||||
|
* Returns audio player
|
||||||
|
*/
|
||||||
|
function audio_player(string $source, string $mediaType, string $class = ''): string
|
||||||
|
{
|
||||||
|
$language = service('request')
|
||||||
|
->getLocale();
|
||||||
|
|
||||||
|
return <<<CODE_SAMPLE
|
||||||
|
<vm-player
|
||||||
|
id="castopod-vm-player"
|
||||||
|
theme="light"
|
||||||
|
language="${language}"
|
||||||
|
icons="castopod-icons"
|
||||||
|
class="${class}"
|
||||||
|
style="--vm-player-box-shadow:0; --vm-player-theme: #009486; --vm-control-spacing: 4px;"
|
||||||
|
>
|
||||||
|
<vm-audio preload="none">
|
||||||
|
<source src="${source}" type="${mediaType}" />
|
||||||
|
</vm-audio>
|
||||||
|
<vm-ui>
|
||||||
|
<vm-icon-library name="castopod-icons"></vm-icon-library>
|
||||||
|
<vm-controls full-width>
|
||||||
|
<vm-playback-control></vm-playback-control>
|
||||||
|
<vm-volume-control></vm-volume-control>
|
||||||
|
<vm-current-time></vm-current-time>
|
||||||
|
<vm-scrubber-control></vm-scrubber-control>
|
||||||
|
<vm-end-time></vm-end-time>
|
||||||
|
<vm-settings-control></vm-settings-control>
|
||||||
|
<vm-default-settings></vm-default-settings>
|
||||||
|
</vm-controls>
|
||||||
|
</vm-ui>
|
||||||
|
</vm-player>
|
||||||
|
CODE_SAMPLE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -136,20 +136,26 @@ if (! function_exists('slugify')) {
|
|||||||
|
|
||||||
if (! function_exists('format_duration')) {
|
if (! function_exists('format_duration')) {
|
||||||
/**
|
/**
|
||||||
* Formats duration in seconds to an hh:mm:ss string
|
* Formats duration in seconds to an hh:mm:ss string. Doesn't show leading zeros if any.
|
||||||
|
*
|
||||||
|
* ⚠️ This uses php's gmdate function so any duration > 86000 seconds (24 hours) will not be formatted properly.
|
||||||
*
|
*
|
||||||
* @param int $seconds seconds to format
|
* @param int $seconds seconds to format
|
||||||
*/
|
*/
|
||||||
function format_duration(int $seconds, string $separator = ':'): string
|
function format_duration(int $seconds): string
|
||||||
{
|
{
|
||||||
return sprintf(
|
if ($seconds < 60) {
|
||||||
'%02d%s%02d%s%02d',
|
return '0:' . $seconds;
|
||||||
floor($seconds / 3600),
|
}
|
||||||
$separator,
|
if ($seconds < 3600) {
|
||||||
($seconds / 60) % 60,
|
// < 1 hour: returns MM:SS
|
||||||
$separator,
|
return ltrim(gmdate('i:s', $seconds), '0');
|
||||||
$seconds % 60,
|
}
|
||||||
);
|
if ($seconds < 36000) {
|
||||||
|
// < 10 hours: returns H:MM:SS
|
||||||
|
return ltrim(gmdate('h:i:s', $seconds), '0');
|
||||||
|
}
|
||||||
|
return gmdate('h:i:s', $seconds);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,4 +41,8 @@ return [
|
|||||||
'upload_file' => 'Upload a file',
|
'upload_file' => 'Upload a file',
|
||||||
'remote_url' => 'Remote URL',
|
'remote_url' => 'Remote URL',
|
||||||
],
|
],
|
||||||
|
'play_episode_button' => [
|
||||||
|
'play' => 'Play',
|
||||||
|
'playing' => 'Playing',
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
@ -44,10 +44,16 @@ return [
|
|||||||
'go_to_page' => 'Go to page',
|
'go_to_page' => 'Go to page',
|
||||||
'create' => 'Add an episode',
|
'create' => 'Add an episode',
|
||||||
'publication_status' => [
|
'publication_status' => [
|
||||||
'published' => 'Published on {0}',
|
'published' => 'Published',
|
||||||
'scheduled' => 'Scheduled for {0}',
|
'scheduled' => 'Scheduled',
|
||||||
'not_published' => 'Not published',
|
'not_published' => 'Not published',
|
||||||
],
|
],
|
||||||
|
'list' => [
|
||||||
|
'episode' => 'Episode',
|
||||||
|
'visibility' => 'Visibility',
|
||||||
|
'comments' => 'Comments',
|
||||||
|
'actions' => 'Actions',
|
||||||
|
],
|
||||||
'form' => [
|
'form' => [
|
||||||
'warning' =>
|
'warning' =>
|
||||||
'In case of fatal error, try increasing the `memory_limit`, `upload_max_filesize` and `post_max_size` values in your php configuration file then restart your web server.<br />These values must be higher than the audio file you wish to upload.',
|
'In case of fatal error, try increasing the `memory_limit`, `upload_max_filesize` and `post_max_size` values in your php configuration file then restart your web server.<br />These values must be higher than the audio file you wish to upload.',
|
||||||
|
@ -41,4 +41,8 @@ return [
|
|||||||
'upload_file' => 'Téléversez un fichier',
|
'upload_file' => 'Téléversez un fichier',
|
||||||
'remote_url' => 'URL distante',
|
'remote_url' => 'URL distante',
|
||||||
],
|
],
|
||||||
|
'play_episode_button' => [
|
||||||
|
'play' => 'Lire',
|
||||||
|
'playing' => 'En cours',
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
@ -44,10 +44,16 @@ return [
|
|||||||
'go_to_page' => 'Voir',
|
'go_to_page' => 'Voir',
|
||||||
'create' => 'Ajouter un épisode',
|
'create' => 'Ajouter un épisode',
|
||||||
'publication_status' => [
|
'publication_status' => [
|
||||||
'published' => 'Publié le {0}',
|
'published' => 'Publié',
|
||||||
'scheduled' => 'Planifié pour le {0}',
|
'scheduled' => 'Planifié',
|
||||||
'not_published' => 'Non publié',
|
'not_published' => 'Non publié',
|
||||||
],
|
],
|
||||||
|
'list' => [
|
||||||
|
'episode' => 'Épisode',
|
||||||
|
'visibility' => 'Visibilité',
|
||||||
|
'comments' => 'Commentaires',
|
||||||
|
'actions' => 'Actions',
|
||||||
|
],
|
||||||
'form' => [
|
'form' => [
|
||||||
'warning' =>
|
'warning' =>
|
||||||
'En cas d’erreur fatale, essayez d’augmenter les valeurs de `memory_limit`, `upload_max_filesize` et `post_max_size` dans votre fichier de configuration php puis redémarrez votre serveur web.<br />Les valeurs doivent être plus grandes que le fichier audio que vous souhaitez téléverser.',
|
'En cas d’erreur fatale, essayez d’augmenter les valeurs de `memory_limit`, `upload_max_filesize` et `post_max_size` dans votre fichier de configuration php puis redémarrez votre serveur web.<br />Les valeurs doivent être plus grandes que le fichier audio que vous souhaitez téléverser.',
|
||||||
|
@ -43,7 +43,7 @@ class EpisodeModel extends Model
|
|||||||
],
|
],
|
||||||
'dark' => [
|
'dark' => [
|
||||||
'style' => 'background-color: #001f1a;',
|
'style' => 'background-color: #001f1a;',
|
||||||
'background' => '#001f1a',
|
'background' => '#313131',
|
||||||
'text' => '#fff',
|
'text' => '#fff',
|
||||||
'inverted' => '#000',
|
'inverted' => '#000',
|
||||||
],
|
],
|
||||||
|
6
app/Resources/icons/disc.svg
Normal file
6
app/Resources/icons/disc.svg
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
<g>
|
||||||
|
<path fill="none" d="M0 0h24v24H0z"/>
|
||||||
|
<path d="M13 9.17A3 3 0 1 0 15 12V2.458c4.057 1.274 7 5.064 7 9.542 0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2c.337 0 .671.017 1 .05v7.12z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 288 B |
6
app/Resources/icons/pause.svg
Normal file
6
app/Resources/icons/pause.svg
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
<g>
|
||||||
|
<path fill="none" d="M0 0h24v24H0z"/>
|
||||||
|
<path d="M6 5h2v14H6V5zm10 0h2v14h-2V5z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 182 B |
6
app/Resources/icons/volume-high.svg
Normal file
6
app/Resources/icons/volume-high.svg
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
<g>
|
||||||
|
<path fill="none" d="M0 0h24v24H0z"/>
|
||||||
|
<path d="M5.889 16H2a1 1 0 0 1-1-1V9a1 1 0 0 1 1-1h3.889l5.294-4.332a.5.5 0 0 1 .817.387v15.89a.5.5 0 0 1-.817.387L5.89 16zm13.517 4.134l-1.416-1.416A8.978 8.978 0 0 0 21 12a8.982 8.982 0 0 0-3.304-6.968l1.42-1.42A10.976 10.976 0 0 1 23 12c0 3.223-1.386 6.122-3.594 8.134zm-3.543-3.543l-1.422-1.422A3.993 3.993 0 0 0 16 12c0-1.43-.75-2.685-1.88-3.392l1.439-1.439A5.991 5.991 0 0 1 18 12c0 1.842-.83 3.49-2.137 4.591z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 559 B |
6
app/Resources/icons/volume-low.svg
Normal file
6
app/Resources/icons/volume-low.svg
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
<g>
|
||||||
|
<path fill="none" d="M0 0h24v24H0z"/>
|
||||||
|
<path d="M8.889 16H5a1 1 0 0 1-1-1V9a1 1 0 0 1 1-1h3.889l5.294-4.332a.5.5 0 0 1 .817.387v15.89a.5.5 0 0 1-.817.387L8.89 16zm9.974.591l-1.422-1.422A3.993 3.993 0 0 0 19 12c0-1.43-.75-2.685-1.88-3.392l1.439-1.439A5.991 5.991 0 0 1 21 12c0 1.842-.83 3.49-2.137 4.591z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 407 B |
6
app/Resources/icons/volume-mute.svg
Normal file
6
app/Resources/icons/volume-mute.svg
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
<g>
|
||||||
|
<path fill="none" d="M0 0h24v24H0z"/>
|
||||||
|
<path d="M5.889 16H2a1 1 0 0 1-1-1V9a1 1 0 0 1 1-1h3.889l5.294-4.332a.5.5 0 0 1 .817.387v15.89a.5.5 0 0 1-.817.387L5.89 16zm14.525-4l3.536 3.536-1.414 1.414L19 13.414l-3.536 3.536-1.414-1.414L17.586 12 14.05 8.464l1.414-1.414L19 10.586l3.536-3.536 1.414 1.414L20.414 12z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 413 B |
125
app/Resources/js/audio-player.ts
Normal file
125
app/Resources/js/audio-player.ts
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
import {
|
||||||
|
VmAudio,
|
||||||
|
VmCaptions,
|
||||||
|
VmClickToPlay,
|
||||||
|
VmControl,
|
||||||
|
VmControls,
|
||||||
|
VmCurrentTime,
|
||||||
|
VmDefaultControls,
|
||||||
|
VmDefaultSettings,
|
||||||
|
VmDefaultUi,
|
||||||
|
VmEndTime,
|
||||||
|
VmFile,
|
||||||
|
VmIcon,
|
||||||
|
VmIconLibrary,
|
||||||
|
VmLoadingScreen,
|
||||||
|
VmMenu,
|
||||||
|
VmMenuItem,
|
||||||
|
VmMenuRadio,
|
||||||
|
VmMenuRadioGroup,
|
||||||
|
VmMuteControl,
|
||||||
|
VmPlaybackControl,
|
||||||
|
VmPlayer,
|
||||||
|
VmScrubberControl,
|
||||||
|
VmSettings,
|
||||||
|
VmSettingsControl,
|
||||||
|
VmSkeleton,
|
||||||
|
VmSlider,
|
||||||
|
VmSubmenu,
|
||||||
|
VmTime,
|
||||||
|
VmTimeProgress,
|
||||||
|
VmTooltip,
|
||||||
|
VmUi,
|
||||||
|
VmVolumeControl,
|
||||||
|
} from "@vime/core";
|
||||||
|
import "@vime/core/themes/default.css";
|
||||||
|
import "@vime/core/themes/light.css";
|
||||||
|
import { html, render } from "lit";
|
||||||
|
import "./modules/play-episode-button";
|
||||||
|
|
||||||
|
const player = html`<div
|
||||||
|
id="castopod-audio-player"
|
||||||
|
class="fixed bottom-0 left-0 flex flex-col w-full bg-white border-t sm:flex-row"
|
||||||
|
data-episode="-1"
|
||||||
|
style="display: none;"
|
||||||
|
>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<img src="" alt="" class="h-[52px] w-[52px]" />
|
||||||
|
<div class="flex flex-col px-2">
|
||||||
|
<p class="text-sm w-48 truncate" title="" id="castopod-player-title"></p>
|
||||||
|
<p
|
||||||
|
class="text-xs w-48 truncate"
|
||||||
|
title=""
|
||||||
|
id="castopod-player-podcast"
|
||||||
|
></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<vm-player
|
||||||
|
id="castopod-vm-player"
|
||||||
|
theme="light"
|
||||||
|
language="en"
|
||||||
|
icons="castopod-icons"
|
||||||
|
class="flex-1"
|
||||||
|
style="--vm-player-box-shadow:0; --vm-player-theme: #009486;"
|
||||||
|
>
|
||||||
|
<vm-audio preload="none" id="testing-audio">
|
||||||
|
<source src="" type="" />
|
||||||
|
</vm-audio>
|
||||||
|
<vm-ui>
|
||||||
|
<vm-icon-library name="castopod-icons"></vm-icon-library>
|
||||||
|
<vm-controls full-width>
|
||||||
|
<vm-playback-control></vm-playback-control>
|
||||||
|
<vm-volume-control></vm-volume-control>
|
||||||
|
<vm-current-time></vm-current-time>
|
||||||
|
<vm-scrubber-control></vm-scrubber-control>
|
||||||
|
<vm-end-time></vm-end-time>
|
||||||
|
<vm-settings-control></vm-settings-control>
|
||||||
|
<vm-default-settings></vm-default-settings>
|
||||||
|
</vm-controls>
|
||||||
|
</vm-ui>
|
||||||
|
</vm-player>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
render(player, document.body);
|
||||||
|
|
||||||
|
// Register Castopod's icons library
|
||||||
|
const library: HTMLVmIconLibraryElement | null = document.querySelector(
|
||||||
|
'vm-icon-library[name="castopod-icons"]'
|
||||||
|
);
|
||||||
|
if (library) {
|
||||||
|
library.resolver = (iconName) => `/assets/icons/${iconName}.svg`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vime elements for audio player
|
||||||
|
customElements.define("vm-player", VmPlayer);
|
||||||
|
customElements.define("vm-file", VmFile);
|
||||||
|
customElements.define("vm-audio", VmAudio);
|
||||||
|
customElements.define("vm-ui", VmUi);
|
||||||
|
customElements.define("vm-default-ui", VmDefaultUi);
|
||||||
|
customElements.define("vm-click-to-play", VmClickToPlay);
|
||||||
|
customElements.define("vm-captions", VmCaptions);
|
||||||
|
customElements.define("vm-loading-screen", VmLoadingScreen);
|
||||||
|
customElements.define("vm-default-controls", VmDefaultControls);
|
||||||
|
customElements.define("vm-default-settings", VmDefaultSettings);
|
||||||
|
customElements.define("vm-controls", VmControls);
|
||||||
|
customElements.define("vm-playback-control", VmPlaybackControl);
|
||||||
|
customElements.define("vm-volume-control", VmVolumeControl);
|
||||||
|
customElements.define("vm-scrubber-control", VmScrubberControl);
|
||||||
|
customElements.define("vm-current-time", VmCurrentTime);
|
||||||
|
customElements.define("vm-end-time", VmEndTime);
|
||||||
|
customElements.define("vm-settings-control", VmSettingsControl);
|
||||||
|
customElements.define("vm-time-progress", VmTimeProgress);
|
||||||
|
customElements.define("vm-control", VmControl);
|
||||||
|
customElements.define("vm-icon", VmIcon);
|
||||||
|
customElements.define("vm-icon-library", VmIconLibrary);
|
||||||
|
customElements.define("vm-tooltip", VmTooltip);
|
||||||
|
customElements.define("vm-mute-control", VmMuteControl);
|
||||||
|
customElements.define("vm-slider", VmSlider);
|
||||||
|
customElements.define("vm-time", VmTime);
|
||||||
|
customElements.define("vm-menu", VmMenu);
|
||||||
|
customElements.define("vm-menu-item", VmMenuItem);
|
||||||
|
customElements.define("vm-submenu", VmSubmenu);
|
||||||
|
customElements.define("vm-menu-radio-group", VmMenuRadioGroup);
|
||||||
|
customElements.define("vm-menu-radio", VmMenuRadio);
|
||||||
|
customElements.define("vm-settings", VmSettings);
|
||||||
|
customElements.define("vm-skeleton", VmSkeleton);
|
78
app/Resources/js/embed.ts
Normal file
78
app/Resources/js/embed.ts
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import {
|
||||||
|
VmAudio,
|
||||||
|
VmCaptions,
|
||||||
|
VmClickToPlay,
|
||||||
|
VmControl,
|
||||||
|
VmControls,
|
||||||
|
VmCurrentTime,
|
||||||
|
VmDefaultControls,
|
||||||
|
VmDefaultSettings,
|
||||||
|
VmDefaultUi,
|
||||||
|
VmEndTime,
|
||||||
|
VmFile,
|
||||||
|
VmIcon,
|
||||||
|
VmIconLibrary,
|
||||||
|
VmLoadingScreen,
|
||||||
|
VmMenu,
|
||||||
|
VmMenuItem,
|
||||||
|
VmMenuRadio,
|
||||||
|
VmMenuRadioGroup,
|
||||||
|
VmMuteControl,
|
||||||
|
VmPlaybackControl,
|
||||||
|
VmPlayer,
|
||||||
|
VmScrubberControl,
|
||||||
|
VmSettings,
|
||||||
|
VmSettingsControl,
|
||||||
|
VmSkeleton,
|
||||||
|
VmSlider,
|
||||||
|
VmSubmenu,
|
||||||
|
VmTime,
|
||||||
|
VmTimeProgress,
|
||||||
|
VmTooltip,
|
||||||
|
VmUi,
|
||||||
|
VmVolumeControl,
|
||||||
|
} from "@vime/core";
|
||||||
|
import "@vime/core/themes/default.css";
|
||||||
|
import "@vime/core/themes/light.css";
|
||||||
|
|
||||||
|
// Vime elements for audio player
|
||||||
|
customElements.define("vm-player", VmPlayer);
|
||||||
|
customElements.define("vm-file", VmFile);
|
||||||
|
customElements.define("vm-audio", VmAudio);
|
||||||
|
customElements.define("vm-ui", VmUi);
|
||||||
|
customElements.define("vm-default-ui", VmDefaultUi);
|
||||||
|
customElements.define("vm-click-to-play", VmClickToPlay);
|
||||||
|
customElements.define("vm-captions", VmCaptions);
|
||||||
|
customElements.define("vm-loading-screen", VmLoadingScreen);
|
||||||
|
customElements.define("vm-default-controls", VmDefaultControls);
|
||||||
|
customElements.define("vm-default-settings", VmDefaultSettings);
|
||||||
|
customElements.define("vm-controls", VmControls);
|
||||||
|
customElements.define("vm-playback-control", VmPlaybackControl);
|
||||||
|
customElements.define("vm-volume-control", VmVolumeControl);
|
||||||
|
customElements.define("vm-scrubber-control", VmScrubberControl);
|
||||||
|
customElements.define("vm-current-time", VmCurrentTime);
|
||||||
|
customElements.define("vm-end-time", VmEndTime);
|
||||||
|
customElements.define("vm-settings-control", VmSettingsControl);
|
||||||
|
customElements.define("vm-time-progress", VmTimeProgress);
|
||||||
|
customElements.define("vm-control", VmControl);
|
||||||
|
customElements.define("vm-icon", VmIcon);
|
||||||
|
customElements.define("vm-icon-library", VmIconLibrary);
|
||||||
|
customElements.define("vm-tooltip", VmTooltip);
|
||||||
|
customElements.define("vm-mute-control", VmMuteControl);
|
||||||
|
customElements.define("vm-slider", VmSlider);
|
||||||
|
customElements.define("vm-time", VmTime);
|
||||||
|
customElements.define("vm-menu", VmMenu);
|
||||||
|
customElements.define("vm-menu-item", VmMenuItem);
|
||||||
|
customElements.define("vm-submenu", VmSubmenu);
|
||||||
|
customElements.define("vm-menu-radio-group", VmMenuRadioGroup);
|
||||||
|
customElements.define("vm-menu-radio", VmMenuRadio);
|
||||||
|
customElements.define("vm-settings", VmSettings);
|
||||||
|
customElements.define("vm-skeleton", VmSkeleton);
|
||||||
|
|
||||||
|
// Register Castopod's icons library
|
||||||
|
const library: HTMLVmIconLibraryElement | null = document.querySelector(
|
||||||
|
'vm-icon-library[name="castopod-icons"]'
|
||||||
|
);
|
||||||
|
if (library) {
|
||||||
|
library.resolver = (iconName) => `/assets/icons/${iconName}.svg`;
|
||||||
|
}
|
263
app/Resources/js/modules/play-episode-button.ts
Normal file
263
app/Resources/js/modules/play-episode-button.ts
Normal file
@ -0,0 +1,263 @@
|
|||||||
|
import { css, html, LitElement, TemplateResult } from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators.js";
|
||||||
|
|
||||||
|
@customElement("play-episode-button")
|
||||||
|
export class PlayEpisodeButton extends LitElement {
|
||||||
|
@property()
|
||||||
|
id = "0";
|
||||||
|
|
||||||
|
@property()
|
||||||
|
src = "";
|
||||||
|
|
||||||
|
@property()
|
||||||
|
mediaType = "";
|
||||||
|
|
||||||
|
@property()
|
||||||
|
title!: string;
|
||||||
|
|
||||||
|
@property()
|
||||||
|
podcast!: string;
|
||||||
|
|
||||||
|
@property()
|
||||||
|
imageSrc!: string;
|
||||||
|
|
||||||
|
@property()
|
||||||
|
playLabel!: string;
|
||||||
|
|
||||||
|
@property()
|
||||||
|
playingLabel!: string;
|
||||||
|
|
||||||
|
@property()
|
||||||
|
isPlaying!: boolean;
|
||||||
|
|
||||||
|
@property()
|
||||||
|
_castopodAudioPlayer!: HTMLDivElement;
|
||||||
|
|
||||||
|
@property()
|
||||||
|
_audio!: HTMLAudioElement;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
_playbackSpeed = 1;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
_events = [
|
||||||
|
{
|
||||||
|
name: "canplay",
|
||||||
|
onEvent: (event: Event): void => {
|
||||||
|
(event.target as HTMLAudioElement)?.play();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "play",
|
||||||
|
onEvent: (): void => {
|
||||||
|
this.isPlaying = true;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "pause",
|
||||||
|
onEvent: (): void => {
|
||||||
|
this.isPlaying = false;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ratechange",
|
||||||
|
onEvent: (event: Event): void => {
|
||||||
|
this._playbackSpeed = (event.target as HTMLAudioElement)?.playbackRate;
|
||||||
|
console.log(this._playbackSpeed);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
async connectedCallback(): Promise<void> {
|
||||||
|
super.connectedCallback();
|
||||||
|
|
||||||
|
await this._elementReady("div[id=castopod-audio-player]");
|
||||||
|
await this._elementReady("div[id=castopod-audio-player] audio");
|
||||||
|
|
||||||
|
this._castopodAudioPlayer = document.body.querySelector(
|
||||||
|
"div[id=castopod-audio-player]"
|
||||||
|
) as HTMLDivElement;
|
||||||
|
|
||||||
|
this._audio = this._castopodAudioPlayer.querySelector(
|
||||||
|
"audio"
|
||||||
|
) as HTMLAudioElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _elementReady(selector: string) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const element = document.querySelector(selector);
|
||||||
|
if (element) {
|
||||||
|
resolve(element);
|
||||||
|
}
|
||||||
|
new MutationObserver((_, observer) => {
|
||||||
|
// Query for elements matching the specified selector
|
||||||
|
Array.from(document.querySelectorAll(selector)).forEach((element) => {
|
||||||
|
resolve(element);
|
||||||
|
//Once we have resolved we don't need the observer anymore.
|
||||||
|
observer.disconnect();
|
||||||
|
});
|
||||||
|
}).observe(document.documentElement, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
play(): void {
|
||||||
|
const currentlyPlayingEpisode = this._castopodAudioPlayer.dataset.episode;
|
||||||
|
|
||||||
|
const isCurrentEpisode = currentlyPlayingEpisode === this.id;
|
||||||
|
|
||||||
|
if (currentlyPlayingEpisode === "-1") {
|
||||||
|
this._showPlayer();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCurrentEpisode) {
|
||||||
|
this._audio.play();
|
||||||
|
} else {
|
||||||
|
const playingEpisodeButton = document.querySelector(
|
||||||
|
`play-episode-button[id="${currentlyPlayingEpisode}"]`
|
||||||
|
) as PlayEpisodeButton;
|
||||||
|
if (playingEpisodeButton) {
|
||||||
|
this._flushLastPlayButton(playingEpisodeButton);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._loadEpisode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pause(): void {
|
||||||
|
this._audio.pause();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _showPlayer(): void {
|
||||||
|
this._castopodAudioPlayer.style.display = "";
|
||||||
|
document.body.style.paddingBottom = "52px";
|
||||||
|
}
|
||||||
|
|
||||||
|
private _flushLastPlayButton(playingEpisodeButton: PlayEpisodeButton): void {
|
||||||
|
playingEpisodeButton.isPlaying = false;
|
||||||
|
|
||||||
|
for (const event of playingEpisodeButton._events) {
|
||||||
|
playingEpisodeButton._audio.removeEventListener(
|
||||||
|
event.name,
|
||||||
|
event.onEvent,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._playbackSpeed = playingEpisodeButton._playbackSpeed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _loadEpisode(): void {
|
||||||
|
this._castopodAudioPlayer.dataset.episode = this.id;
|
||||||
|
|
||||||
|
this._audio.src = this.src;
|
||||||
|
this._audio.load();
|
||||||
|
this._audio.playbackRate = this._playbackSpeed;
|
||||||
|
for (const event of this._events) {
|
||||||
|
this._audio.addEventListener(event.name, event.onEvent, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
const img: HTMLImageElement | null =
|
||||||
|
this._castopodAudioPlayer.querySelector("img");
|
||||||
|
|
||||||
|
if (img) {
|
||||||
|
img.src = this.imageSrc;
|
||||||
|
img.alt = this.title;
|
||||||
|
}
|
||||||
|
|
||||||
|
const episodeTitle: HTMLParagraphElement | null =
|
||||||
|
this._castopodAudioPlayer.querySelector('p[id="castopod-player-title"]');
|
||||||
|
|
||||||
|
if (episodeTitle) {
|
||||||
|
episodeTitle.title = this.title;
|
||||||
|
episodeTitle.innerHTML = this.title;
|
||||||
|
}
|
||||||
|
|
||||||
|
const podcastTitle: HTMLParagraphElement | null =
|
||||||
|
this._castopodAudioPlayer.querySelector(
|
||||||
|
'p[id="castopod-player-podcast"]'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (podcastTitle) {
|
||||||
|
podcastTitle.title = this.podcast;
|
||||||
|
podcastTitle.innerHTML = this.podcast;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = css`
|
||||||
|
button {
|
||||||
|
background-color: #ffffff;
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
border-width: 2px;
|
||||||
|
border-style: solid;
|
||||||
|
border-radius: 9999px;
|
||||||
|
border-color: rgba(207, 247, 243, 1);
|
||||||
|
|
||||||
|
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
border-color: #009486;
|
||||||
|
background-color: #ebf8f8;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:focus {
|
||||||
|
background-color: #ebf8f8;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
margin-right: 0.25rem;
|
||||||
|
color: #009486;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-spin {
|
||||||
|
animation: spin 3s linear infinite;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
render(): TemplateResult<1> {
|
||||||
|
return html`<button @click="${this.isPlaying ? this.pause : this.play}">
|
||||||
|
${this.isPlaying
|
||||||
|
? html`<svg
|
||||||
|
class="animate-spin"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
>
|
||||||
|
<g>
|
||||||
|
<path fill="none" d="M0 0h24v24H0z" />
|
||||||
|
<path
|
||||||
|
d="M13 9.17A3 3 0 1 0 15 12V2.458c4.057 1.274 7 5.064 7 9.542 0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2c.337 0 .671.017 1 .05v7.12z"
|
||||||
|
/>
|
||||||
|
</g></svg
|
||||||
|
>${this.playingLabel}`
|
||||||
|
: html`<svg
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
>
|
||||||
|
<path fill="none" d="M0 0h24v24H0z" />
|
||||||
|
<path
|
||||||
|
d="M7.752 5.439l10.508 6.13a.5.5 0 0 1 0 .863l-10.508 6.13A.5.5 0 0 1 7 18.128V5.871a.5.5 0 0 1 .752-.432z"
|
||||||
|
/></svg
|
||||||
|
>${this.playLabel}`}
|
||||||
|
</button>`;
|
||||||
|
}
|
||||||
|
}
|
86
app/Resources/styles/inputRange.css
Normal file
86
app/Resources/styles/inputRange.css
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
.wrap {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
width: 12.5em;
|
||||||
|
height: 5.25em;
|
||||||
|
font: 1em/1 arial, sans-serif;
|
||||||
|
}
|
||||||
|
[type="range"] {
|
||||||
|
flex: 1;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
min-height: 1.5em;
|
||||||
|
background: transparent;
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
[type="range"],
|
||||||
|
[type="range"]::-webkit-slider-thumb {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
[type="range"]::-webkit-slider-runnable-track {
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: none;
|
||||||
|
width: 12.5em;
|
||||||
|
height: 0.25em;
|
||||||
|
background: #ccc;
|
||||||
|
}
|
||||||
|
[type="range"]::-moz-range-track {
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: none;
|
||||||
|
width: 12.5em;
|
||||||
|
height: 0.25em;
|
||||||
|
background: #ccc;
|
||||||
|
}
|
||||||
|
[type="range"]::-ms-track {
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: none;
|
||||||
|
width: 12.5em;
|
||||||
|
height: 0.25em;
|
||||||
|
background: #ccc;
|
||||||
|
}
|
||||||
|
[type="range"]::-webkit-slider-thumb {
|
||||||
|
margin-top: -0.625em;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: none;
|
||||||
|
width: 1.5em;
|
||||||
|
height: 1.5em;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #f90;
|
||||||
|
}
|
||||||
|
[type="range"]::-moz-range-thumb {
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: none;
|
||||||
|
width: 1.5em;
|
||||||
|
height: 1.5em;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #f90;
|
||||||
|
}
|
||||||
|
[type="range"]::-ms-thumb {
|
||||||
|
margin-top: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: none;
|
||||||
|
width: 1.5em;
|
||||||
|
height: 1.5em;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #f90;
|
||||||
|
}
|
||||||
|
[type="range"]::-ms-tooltip {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
[type="range"] ~ output {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.js [type="range"] ~ output {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
left: 0.75em;
|
||||||
|
top: 0;
|
||||||
|
padding: 0.25em 0.5em;
|
||||||
|
border-radius: 3px;
|
||||||
|
transform: translate(
|
||||||
|
calc((var(--val) - var(--min)) / (var(--max) - var(--min)) * 11em - 50%)
|
||||||
|
);
|
||||||
|
background: #95a;
|
||||||
|
color: #eee;
|
||||||
|
}
|
@ -8,7 +8,9 @@
|
|||||||
<meta name="description" content="Castopod is an open-source hosting platform made for podcasters who want engage and interact with their audience."/>
|
<meta name="description" content="Castopod is an open-source hosting platform made for podcasters who want engage and interact with their audience."/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
<link rel="shortcut icon" type="image/png" href="/favicon.ico" />
|
<link rel="shortcut icon" type="image/png" href="/favicon.ico" />
|
||||||
|
|
||||||
<?= service('vite')->asset('styles/index.css', 'css') ?>
|
<?= service('vite')->asset('styles/index.css', 'css') ?>
|
||||||
|
<?= service('vite')->asset('js/audio-player.ts', 'js') ?>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="flex flex-col min-h-screen mx-auto bg-gray-100">
|
<body class="flex flex-col min-h-screen mx-auto bg-gray-100">
|
||||||
|
@ -7,8 +7,10 @@
|
|||||||
<meta name="description" content="Castopod is an open-source hosting platform made for podcasters who want engage and interact with their audience."/>
|
<meta name="description" content="Castopod is an open-source hosting platform made for podcasters who want engage and interact with their audience."/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
<link rel="shortcut icon" type="image/png" href="/favicon.ico" />
|
<link rel="shortcut icon" type="image/png" href="/favicon.ico" />
|
||||||
|
|
||||||
<?= service('vite')->asset('styles/index.css', 'css') ?>
|
<?= service('vite')->asset('styles/index.css', 'css') ?>
|
||||||
<?= service('vite')->asset('js/admin.ts', 'js') ?>
|
<?= service('vite')->asset('js/admin.ts', 'js') ?>
|
||||||
|
<?= service('vite')->asset('js/audio-player.ts', 'js') ?>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="relative bg-gray-100 holy-grail-grid">
|
<body class="relative bg-gray-100 holy-grail-grid">
|
||||||
|
@ -65,6 +65,7 @@
|
|||||||
],
|
],
|
||||||
],
|
],
|
||||||
$podcast->contributors,
|
$podcast->contributors,
|
||||||
|
'',
|
||||||
$podcast,
|
$podcast,
|
||||||
) ?>
|
) ?>
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@
|
|||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<iframe name="embeddable_player" id="embeddable_player" class="w-full h-48 max-w-xl" frameborder="0" scrolling="no" style="width: 100%; overflow: hidden;" src="<?= $episode->embeddable_player_url ?>"></iframe>
|
<iframe name="embeddable_player" id="embeddable_player" class="w-full max-w-xl h-36" frameborder="0" scrolling="no" style="width: 100%; overflow: hidden;" src="<?= $episode->embeddable_player_url ?>"></iframe>
|
||||||
|
|
||||||
<div class="flex items-center w-full mt-8">
|
<div class="flex items-center w-full mt-8">
|
||||||
<?= form_textarea(
|
<?= form_textarea(
|
||||||
|
@ -5,9 +5,7 @@
|
|||||||
<?= $this->endSection() ?>
|
<?= $this->endSection() ?>
|
||||||
|
|
||||||
<?= $this->section('pageTitle') ?>
|
<?= $this->section('pageTitle') ?>
|
||||||
<?= lang('Episode.all_podcast_episodes') ?> (<?= $pager->getDetails()[
|
<?= lang('Episode.all_podcast_episodes') ?> (<?= $pager->getDetails()['total'] ?>)
|
||||||
'total'
|
|
||||||
] ?>)
|
|
||||||
<?= $this->endSection() ?>
|
<?= $this->endSection() ?>
|
||||||
|
|
||||||
<?= $this->section('headerRight') ?>
|
<?= $this->section('headerRight') ?>
|
||||||
@ -23,107 +21,105 @@
|
|||||||
<p class="mb-4 text-sm italic text-gray-700"><?= lang('Common.pageInfo', [
|
<p class="mb-4 text-sm italic text-gray-700"><?= lang('Common.pageInfo', [
|
||||||
'currentPage' => $pager->getDetails()['currentPage'],
|
'currentPage' => $pager->getDetails()['currentPage'],
|
||||||
'pageCount' => $pager->getDetails()['pageCount'],
|
'pageCount' => $pager->getDetails()['pageCount'],
|
||||||
]) ?></p>
|
]) ?></p>
|
||||||
<div class="flex flex-wrap mb-6">
|
|
||||||
<?php if ($episodes): ?>
|
<?= data_table(
|
||||||
<?php foreach ($episodes as $episode): ?>
|
[
|
||||||
<article class="flex w-full max-w-lg p-4 mx-auto">
|
[
|
||||||
<img
|
'header' => lang('Episode.list.episode'),
|
||||||
loading="lazy"
|
'cell' => function ($episode, $podcast) {
|
||||||
src="<?= $episode->image->thumbnail_url ?>"
|
return '<div class="flex">' .
|
||||||
alt="<?= $episode->title ?>" class="object-cover w-20 h-20 mr-2 rounded-lg" />
|
'<div class="relative flex-shrink-0 mr-2">'.
|
||||||
<div class="flex flex-col flex-1">
|
'<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">' .
|
||||||
<div class="flex">
|
format_duration(
|
||||||
<a class="flex-1 text-sm hover:underline" href="<?= route_to(
|
$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" />' .
|
||||||
|
'</div>' .
|
||||||
|
'<a class="text-sm hover:underline" href="' . route_to(
|
||||||
'episode-view',
|
'episode-view',
|
||||||
$podcast->id,
|
$podcast->id,
|
||||||
$episode->id,
|
$episode->id,
|
||||||
) ?>">
|
) . '">' .
|
||||||
<h2 class="inline-flex justify-between w-full font-semibold leading-none group">
|
'<h2 class="inline-flex w-full font-semibold leading-none group">' .
|
||||||
<span class="mr-1 group-hover:underline"><?= $episode->title ?></span>
|
episode_numbering(
|
||||||
<?= episode_numbering(
|
|
||||||
$episode->number,
|
$episode->number,
|
||||||
$episode->season_number,
|
$episode->season_number,
|
||||||
'text-xs font-semibold text-gray-600',
|
'text-xs font-semibold text-gray-600',
|
||||||
true,
|
true,
|
||||||
) ?>
|
) .
|
||||||
</h2>
|
'<span class="mx-1">-</span>' .
|
||||||
</a>
|
'<span class="mr-1 group-hover:underline">' . $episode->title . '</span>' .
|
||||||
<button
|
'</h2>' .
|
||||||
id="more-dropdown-<?= $episode->id ?>"
|
'<p class="max-w-sm text-xs text-gray-600 line-clamp-2">' . $episode->description . '</p>' .
|
||||||
type="button"
|
'</a>' .
|
||||||
class="inline-flex items-center p-1 outline-none focus:ring"
|
'</div>';
|
||||||
data-dropdown="button"
|
},
|
||||||
data-dropdown-target="more-dropdown-<?= $episode->id ?>-menu"
|
],
|
||||||
aria-haspopup="true"
|
[
|
||||||
aria-expanded="false">
|
'header' => lang('Episode.list.visibility'),
|
||||||
<?= icon('more') ?>
|
'cell' => function ($episode) {
|
||||||
</button>
|
return publication_pill(
|
||||||
<nav
|
$episode->published_at,
|
||||||
id="more-dropdown-<?= $episode->id ?>-menu"
|
$episode->publication_status,
|
||||||
class="flex flex-col py-2 text-black whitespace-no-wrap bg-white border rounded shadow"
|
);
|
||||||
aria-labelledby="more-dropdown-<?= $episode->id ?>"
|
},
|
||||||
data-dropdown="menu"
|
],
|
||||||
data-dropdown-placement="bottom-start"
|
[
|
||||||
data-dropdown-offset-x="0"
|
'header' => lang('Episode.list.comments'),
|
||||||
data-dropdown-offset-y="-24">
|
'cell' => function ($episode) {
|
||||||
<a class="px-4 py-1 hover:bg-gray-100" href="<?= route_to(
|
return count($episode->comments);
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'header' => lang('Episode.list.actions'),
|
||||||
|
'cell' => function ($episode, $podcast) {
|
||||||
|
return '<button id="more-dropdown-<?= $episode->id ?>" type="button" class="inline-flex items-center p-1 outline-none focus:ring" data-dropdown="button" data-dropdown-target="more-dropdown-<?= $episode->id ?>-menu" aria-haspopup="true" aria-expanded="false">' .
|
||||||
|
icon('more') .
|
||||||
|
'</button>' .
|
||||||
|
'<nav id="more-dropdown-<?= $episode->id ?>-menu" class="flex flex-col py-2 text-black whitespace-no-wrap bg-white border rounded shadow" aria-labelledby="more-dropdown-<?= $episode->id ?>" data-dropdown="menu" data-dropdown-placement="bottom-start" data-dropdown-offset-x="0" data-dropdown-offset-y="-24">' .
|
||||||
|
'<a class="px-4 py-1 hover:bg-gray-100" href="' . route_to(
|
||||||
'episode-edit',
|
'episode-edit',
|
||||||
$podcast->id,
|
$podcast->id,
|
||||||
$episode->id,
|
$episode->id,
|
||||||
) ?>"><?= lang('Episode.edit') ?></a>
|
) . '">' . lang('Episode.edit') . '</a>' .
|
||||||
<a class="px-4 py-1 hover:bg-gray-100" href="<?= route_to(
|
'<a class="px-4 py-1 hover:bg-gray-100" href="' . route_to(
|
||||||
'embeddable-player-add',
|
'embeddable-player-add',
|
||||||
$podcast->id,
|
$podcast->id,
|
||||||
$episode->id,
|
$episode->id,
|
||||||
) ?>"><?= lang(
|
) . '">' . lang(
|
||||||
'Episode.embeddable_player.add',
|
'Episode.embeddable_player.add',
|
||||||
) ?></a>
|
) . '</a>' .
|
||||||
<a class="px-4 py-1 hover:bg-gray-100" href="<?= route_to(
|
'<a class="px-4 py-1 hover:bg-gray-100" href="' . route_to(
|
||||||
'episode-person-manage',
|
'episode-person-manage',
|
||||||
$podcast->id,
|
$podcast->id,
|
||||||
$episode->id,
|
$episode->id,
|
||||||
) ?>"><?= lang('Person.persons') ?></a>
|
) . '">' . lang('Person.persons') . '</a>' .
|
||||||
<a class="px-4 py-1 hover:bg-gray-100" href="<?= route_to(
|
'<a class="px-4 py-1 hover:bg-gray-100" href="' . route_to(
|
||||||
'soundbites-edit',
|
'soundbites-edit',
|
||||||
$podcast->id,
|
$podcast->id,
|
||||||
$episode->id,
|
$episode->id,
|
||||||
) ?>"><?= lang('Episode.soundbites') ?></a>
|
) . '">' . lang('Episode.soundbites') . '</a>' .
|
||||||
<a class="px-4 py-1 hover:bg-gray-100" href="<?= route_to(
|
'<a class="px-4 py-1 hover:bg-gray-100" href="' . route_to(
|
||||||
'episode',
|
'episode',
|
||||||
$podcast->handle,
|
$podcast->handle,
|
||||||
$episode->slug,
|
$episode->slug,
|
||||||
) ?>"><?= lang('Episode.go_to_page') ?></a>
|
) . '">' . lang('Episode.go_to_page') . '</a>' .
|
||||||
<a class="px-4 py-1 hover:bg-gray-100" href="<?= route_to(
|
'<a class="px-4 py-1 hover:bg-gray-100" href="' . route_to(
|
||||||
'episode-delete',
|
'episode-delete',
|
||||||
$podcast->id,
|
$podcast->id,
|
||||||
$episode->id,
|
$episode->id,
|
||||||
) ?>"><?= lang('Episode.delete') ?></a>
|
) . '">' . lang('Episode.delete') . '</a>' .
|
||||||
</nav>
|
'</nav>' .
|
||||||
</div>
|
'</div>';
|
||||||
<div class="mb-2 text-xs">
|
},
|
||||||
<?= publication_pill(
|
],
|
||||||
$episode->published_at,
|
],
|
||||||
$episode->publication_status,
|
$episodes,
|
||||||
) ?>
|
'mb-6',
|
||||||
<span class="mx-1">•</span>
|
$podcast
|
||||||
<time datetime="PT<?= $episode->audio_file_duration ?>S">
|
) ?>
|
||||||
<?= format_duration(
|
|
||||||
$episode->audio_file_duration,
|
|
||||||
) ?>
|
|
||||||
</time>
|
|
||||||
</div>
|
|
||||||
<audio controls preload="none" class="w-full mt-auto">
|
|
||||||
<source src="<?= $episode->audio_file_url ?>" type="<?= $episode->audio_file_mimetype ?>">
|
|
||||||
Your browser does not support the audio tag.
|
|
||||||
</audio>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
<?php else: ?>
|
|
||||||
<p class="italic"><?= lang('Podcast.no_episode') ?></p>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<?= $pager->links() ?>
|
<?= $pager->links() ?>
|
||||||
|
|
||||||
|
@ -73,10 +73,7 @@
|
|||||||
</time>
|
</time>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<audio controls preload="none" class="w-full mt-auto">
|
<?= audio_player($episode->audio_file_url, $episode->audio_file_mimetype, 'mt-auto') ?>
|
||||||
<source src="<?= $episode->audio_file_url ?>" type="<?= $episode->audio_file_mimetype ?>">
|
|
||||||
Your browser does not support the audio tag.
|
|
||||||
</audio>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<footer class="flex justify-around px-6 py-3">
|
<footer class="flex justify-around px-6 py-3">
|
||||||
|
@ -91,10 +91,7 @@
|
|||||||
</time>
|
</time>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<audio controls preload="none" class="w-full mt-auto">
|
<?= audio_player($episode->audio_file_url, $episode->audio_file_mimetype, 'mt-auto') ?>
|
||||||
<source src="<?= $episode->audio_file_url ?>" type="<?= $episode->audio_file_mimetype ?>">
|
|
||||||
Your browser does not support the audio tag.
|
|
||||||
</audio>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<footer class="flex justify-around px-6 py-3">
|
<footer class="flex justify-around px-6 py-3">
|
||||||
|
@ -29,15 +29,14 @@
|
|||||||
|
|
||||||
<div class="flex flex-wrap">
|
<div class="flex flex-wrap">
|
||||||
<div class="w-full max-w-sm mb-6 md:mr-4">
|
<div class="w-full max-w-sm mb-6 md:mr-4">
|
||||||
|
<div class="mb-6">
|
||||||
<img
|
<img
|
||||||
src="<?= $episode->image->medium_url ?>"
|
src="<?= $episode->image->medium_url ?>"
|
||||||
alt="Episode cover"
|
alt="Episode cover"
|
||||||
class="object-cover w-full"
|
class="object-cover w-full"
|
||||||
/>
|
/>
|
||||||
<audio controls preload="auto" class="w-full mb-6">
|
<?= audio_player($episode->audio_file_url, $episode->audio_file_mimetype) ?>
|
||||||
<source src="<?= $episode->audio_file_url ?>" type="<?= $episode->audio_file_mimetype ?>">
|
</div>
|
||||||
Your browser does not support the audio tag.
|
|
||||||
</audio>
|
|
||||||
|
|
||||||
<div class="flex justify-around">
|
<div class="flex justify-around">
|
||||||
<?= button(
|
<?= button(
|
||||||
@ -113,7 +112,7 @@
|
|||||||
[
|
[
|
||||||
'header' => lang('Episode.soundbites_form.duration'),
|
'header' => lang('Episode.soundbites_form.duration'),
|
||||||
'cell' => function ($soundbite): string {
|
'cell' => function ($soundbite): string {
|
||||||
return format_duration($soundbite->duration);
|
return $soundbite->duration . 's';
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
|
@ -12,10 +12,18 @@
|
|||||||
<?php if ($episodes): ?>
|
<?php if ($episodes): ?>
|
||||||
<div class="flex p-2 overflow-x-auto gap-x-6">
|
<div class="flex p-2 overflow-x-auto gap-x-6">
|
||||||
<?php foreach ($episodes as $episode): ?>
|
<?php foreach ($episodes as $episode): ?>
|
||||||
<article class="flex flex-col flex-shrink-0 w-56 overflow-hidden bg-white border shadow rounded-xl">
|
<article class="flex flex-col flex-shrink-0 flex-1 w-full min-w-[12rem] overflow-hidden bg-white border shadow rounded-xl">
|
||||||
|
<div class="relative">
|
||||||
|
<div class=""></div>
|
||||||
|
<?= publication_pill(
|
||||||
|
$episode->published_at,
|
||||||
|
$episode->publication_status,
|
||||||
|
'absolute top-2 right-2 text-sm'
|
||||||
|
); ?>
|
||||||
<img
|
<img
|
||||||
src="<?= $episode->image->thumbnail_url ?>"
|
src="<?= $episode->image->thumbnail_url ?>"
|
||||||
alt="<?= $episode->title ?>" class="object-cover" />
|
alt="<?= $episode->title ?>" class="object-cover w-full" />
|
||||||
|
</div>
|
||||||
<div class="flex items-start justify-between p-2">
|
<div class="flex items-start justify-between p-2">
|
||||||
<div class="flex flex-col min-w-0">
|
<div class="flex flex-col min-w-0">
|
||||||
<a href="<?= route_to(
|
<a href="<?= route_to(
|
||||||
@ -24,29 +32,16 @@
|
|||||||
$episode->id,
|
$episode->id,
|
||||||
) ?>"
|
) ?>"
|
||||||
class="text-sm font-semibold truncate hover:underline"
|
class="text-sm font-semibold truncate hover:underline"
|
||||||
><?= $episode->title ?>
|
>
|
||||||
</a>
|
|
||||||
<div class="text-xs">
|
|
||||||
<?= episode_numbering(
|
<?= episode_numbering(
|
||||||
$episode->number,
|
$episode->number,
|
||||||
$episode->season_number,
|
$episode->season_number,
|
||||||
'font-semibold text-gray-600',
|
'font-semibold text-gray-600',
|
||||||
true,
|
true,
|
||||||
) ?>
|
) ?>
|
||||||
<?php if ($episode->published_at): ?>
|
<span class="mx-1">-</span>
|
||||||
<span class="mx-1">•</span>
|
<?= $episode->title ?>
|
||||||
<time
|
</a>
|
||||||
pubdate
|
|
||||||
datetime="<?= $episode->published_at->format(
|
|
||||||
DateTime::ATOM,
|
|
||||||
) ?>"
|
|
||||||
title="<?= $episode->published_at ?>">
|
|
||||||
<?= lang('Common.mediumDate', [
|
|
||||||
$episode->published_at,
|
|
||||||
]) ?>
|
|
||||||
</time>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -10,24 +10,25 @@
|
|||||||
<link rel="shortcut icon" type="image/x-icon" href="/favicon.ico" />
|
<link rel="shortcut icon" type="image/x-icon" href="/favicon.ico" />
|
||||||
<link rel="canonical" href="<?= $episode->link ?>" />
|
<link rel="canonical" href="<?= $episode->link ?>" />
|
||||||
<?= service('vite')->asset('styles/index.css', 'css') ?>
|
<?= service('vite')->asset('styles/index.css', 'css') ?>
|
||||||
|
<?= service('vite')->asset('js/embed.ts', 'js') ?>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="flex w-full h-screen" style="background: <?= $theme[
|
<body class="flex w-full h-screen" style="background: <?= $themeData[
|
||||||
'background'
|
'background'
|
||||||
] ?>; color: <?= $theme['text'] ?>;">
|
] ?>; color: <?= $themeData['text'] ?>;">
|
||||||
<img src="<?= $episode->image
|
<img src="<?= $episode->image
|
||||||
->medium_url ?>" alt="<?= $episode->title ?>" class="flex-shrink h-full" />
|
->thumbnail_url ?>" alt="<?= $episode->title ?>" class="flex-shrink w-36 h-36" />
|
||||||
<div class="flex flex-col flex-1 min-w-0 p-4">
|
<div class="flex flex-col flex-1 min-w-0 px-4 py-2 h-36">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<a href="<?= route_to(
|
<a href="<?= route_to(
|
||||||
'podcast',
|
'podcast-activity',
|
||||||
$podcast->handle,
|
$podcast->handle,
|
||||||
) ?>" style="color: <?= $theme[
|
) ?>" style="color: <?= $themeData[
|
||||||
'text'
|
'text'
|
||||||
] ?>;" class="mr-2 text-xs tracking-wider uppercase truncate opacity-75 hover:opacity-100" target="_blank">
|
] ?>;" class="mr-2 text-xs tracking-wider uppercase truncate opacity-75 hover:opacity-100" target="_blank">
|
||||||
<?= $podcast->title ?>
|
<?= $podcast->title ?>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://castopod.org/" class="ml-auto text-xl text-pine-700 hover:opacity-75" title="<?= lang(
|
<a href="https://castopod.org/" class="ml-auto text-3xl text-pine-700 hover:opacity-75" title="<?= lang(
|
||||||
'Common.powered_by',
|
'Common.powered_by',
|
||||||
[
|
[
|
||||||
'castopod' => 'Castopod',
|
'castopod' => 'Castopod',
|
||||||
@ -36,7 +37,7 @@
|
|||||||
<?= icon('podcasting/castopod') ?>
|
<?= icon('podcasting/castopod') ?>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<a href="<?= $episode->link ?>" class="flex items-center mb-2" style="color: <?= $theme[
|
<a href="<?= $episode->link ?>" class="flex items-center mb-2" style="color: <?= $themeData[
|
||||||
'text'
|
'text'
|
||||||
] ?>;" target="_blank">
|
] ?>;" target="_blank">
|
||||||
<h1 class="mr-2 text-lg font-semibold truncate opacity-100 hover:opacity-75">
|
<h1 class="mr-2 text-lg font-semibold truncate opacity-100 hover:opacity-75">
|
||||||
@ -49,14 +50,33 @@
|
|||||||
true,
|
true,
|
||||||
) ?>
|
) ?>
|
||||||
</a>
|
</a>
|
||||||
<audio controls preload="none" class="flex w-full mt-auto">
|
<vm-player
|
||||||
<source src="<?= $episode->audio_file_analytics_url .
|
id="castopod-vm-player"
|
||||||
|
theme="<?= str_starts_with($theme, 'dark') ? 'dark' : 'light' ?>"
|
||||||
|
language="${language}"
|
||||||
|
icons="castopod-icons"
|
||||||
|
class="w-full mt-auto"
|
||||||
|
style="--vm-player-box-shadow:0; --vm-player-theme: #009486; --vm-control-spacing: 4px; --vm-control-icon-size: 24px; <?= str_ends_with($theme, 'transparent') ? '--vm-controls-bg: transparent;' : '' ?>"
|
||||||
|
>
|
||||||
|
<vm-audio preload="none">
|
||||||
|
<?php $source = logged_in() ? $episode->audio_file_url : $episode->audio_file_analytics_url .
|
||||||
(isset($_SERVER['HTTP_REFERER'])
|
(isset($_SERVER['HTTP_REFERER'])
|
||||||
? '?_from=' .
|
? '?_from=' .
|
||||||
parse_url($_SERVER['HTTP_REFERER'], PHP_URL_HOST)
|
parse_url($_SERVER['HTTP_REFERER'], PHP_URL_HOST)
|
||||||
: '') ?>" type="<?= $episode->audio_file_mimetype ?>" />
|
: '') ?>
|
||||||
Your browser does not support the audio tag.
|
<source src="<?= $source ?>" type="<?= $episode->audio_file_mimetype ?>" />
|
||||||
</audio>
|
</vm-audio>
|
||||||
|
<vm-ui>
|
||||||
|
<vm-icon-library name="castopod-icons"></vm-icon-library>
|
||||||
|
<vm-controls full-width>
|
||||||
|
<vm-playback-control></vm-playback-control>
|
||||||
|
<vm-volume-control></vm-volume-control>
|
||||||
|
<vm-current-time></vm-current-time>
|
||||||
|
<vm-scrubber-control></vm-scrubber-control>
|
||||||
|
<vm-end-time></vm-end-time>
|
||||||
|
</vm-controls>
|
||||||
|
</vm-ui>
|
||||||
|
</vm-player>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
@ -15,24 +15,20 @@
|
|||||||
|
|
||||||
<?= service('vite')->asset('styles/index.css', 'css') ?>
|
<?= service('vite')->asset('styles/index.css', 'css') ?>
|
||||||
<?= service('vite')->asset('js/podcast.ts', 'js') ?>
|
<?= service('vite')->asset('js/podcast.ts', 'js') ?>
|
||||||
|
<?= service('vite')->asset('js/audio-player.ts', 'js') ?>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="flex w-full min-h-screen pb-20 overflow-x-hidden lg:mx-auto lg:container bg-pine-50 sm:pb-0">
|
<body class="flex w-full min-h-screen pb-20 overflow-x-hidden lg:mx-auto lg:container bg-pine-50 sm:pb-0">
|
||||||
<?= $this->include('podcast/_partials/header') ?>
|
<?= $this->include('podcast/_partials/header') ?>
|
||||||
|
|
||||||
<main class="flex-shrink-0 w-full min-w-0 sm:w-auto sm:flex-1 sm:flex-shrink">
|
<main class="flex-shrink-0 w-full min-w-0 sm:w-auto sm:flex-1 sm:flex-shrink">
|
||||||
<?= $this->renderSection('content') ?>
|
<nav class="sticky top-0 left-0 z-50 flex items-center w-full h-12 px-2 py-1 sm:hidden bg-pine-900">
|
||||||
</main>
|
|
||||||
|
|
||||||
<?= $this->include('podcast/_partials/sidebar') ?>
|
|
||||||
<nav class="fixed bottom-0 left-0 z-50 flex items-center w-full px-4 py-4 sm:hidden">
|
|
||||||
<div class="flex items-center w-full p-2 rounded-full shadow-2xl bg-pine-900">
|
|
||||||
<button
|
<button
|
||||||
data-toggle="main-header"
|
data-toggle="main-header"
|
||||||
data-toggle-class="sticky -translate-x-full"
|
data-toggle-class="sticky -translate-x-full"
|
||||||
class="flex-shrink-0 mr-3 overflow-hidden rounded-full focus:ring-2 focus:outline-none focus:ring-pine-50">
|
class="flex-shrink-0 mr-3 overflow-hidden rounded-full focus:ring-2 focus:outline-none focus:ring-pine-50">
|
||||||
<img src="<?= $podcast->image
|
<img src="<?= $podcast->image
|
||||||
->thumbnail_url ?>" alt="<?= $podcast->title ?>" class="h-14"/>
|
->thumbnail_url ?>" alt="<?= $podcast->title ?>" class="h-10"/>
|
||||||
</button>
|
</button>
|
||||||
<p class="flex flex-col flex-1 min-w-0 mr-2 text-white">
|
<p class="flex flex-col flex-1 min-w-0 mr-2 text-white">
|
||||||
<span class="text-sm font-semibold truncate"><?= $podcast->title ?></span>
|
<span class="text-sm font-semibold truncate"><?= $podcast->title ?></span>
|
||||||
@ -48,20 +44,21 @@
|
|||||||
'width' => 420,
|
'width' => 420,
|
||||||
'height' => 620,
|
'height' => 620,
|
||||||
'class' =>
|
'class' =>
|
||||||
'group inline-flex items-center px-4 py-2 text-xs tracking-wider font-semibold text-white uppercase rounded-full shadow focus:outline-none focus:ring bg-rose-600',
|
'group inline-flex mr-2 items-center px-3 py-1 text-xs tracking-wider font-semibold text-white uppercase rounded-full shadow focus:outline-none focus:ring bg-rose-600',
|
||||||
],
|
],
|
||||||
) ?>
|
) ?>
|
||||||
<button
|
<button
|
||||||
data-toggle="main-sidebar"
|
data-toggle="main-sidebar"
|
||||||
data-toggle-class="translate-x-full"
|
data-toggle-class="translate-x-full"
|
||||||
data-toggle-body-class="-ml-64"
|
data-toggle-body-class="-ml-64"
|
||||||
class="p-4 text-xl rounded-full focus:outline-none focus:ring-2 focus:ring-pine-600 text-pine-200 hover:text-pine-50"><?= icon(
|
class="p-2 text-xl rounded-full focus:outline-none focus:ring-2 focus:ring-pine-600 text-pine-200 hover:text-pine-50"><?= icon(
|
||||||
'menu',
|
'menu',
|
||||||
) ?><span class="sr-only"><?= lang(
|
) ?><span class="sr-only"><?= lang('Podcast.toggle_podcast_sidebar') ?></span></button>
|
||||||
'Podcast.toggle_podcast_sidebar',
|
|
||||||
) ?></span></button>
|
|
||||||
</div>
|
|
||||||
</nav>
|
</nav>
|
||||||
|
<?= $this->renderSection('content') ?>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<?= $this->include('podcast/_partials/sidebar') ?>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
data-toggle="main-sidebar"
|
data-toggle="main-sidebar"
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
|
|
||||||
<?= service('vite')->asset('styles/index.css', 'css') ?>
|
<?= service('vite')->asset('styles/index.css', 'css') ?>
|
||||||
<?= service('vite')->asset('js/podcast.ts', 'js') ?>
|
<?= service('vite')->asset('js/podcast.ts', 'js') ?>
|
||||||
|
<?= service('vite')->asset('js/audio-player.ts', 'js') ?>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="flex w-full min-h-screen pt-12 pb-20 overflow-x-hidden bg-pine-50 lg:mx-auto lg:container sm:pb-0">
|
<body class="flex w-full min-h-screen pt-12 pb-20 overflow-x-hidden bg-pine-50 lg:mx-auto lg:container sm:pb-0">
|
||||||
@ -66,16 +67,13 @@
|
|||||||
<?= $this->include('podcast/_partials/header') ?>
|
<?= $this->include('podcast/_partials/header') ?>
|
||||||
|
|
||||||
<main class="flex-shrink-0 w-full min-w-0 sm:w-auto sm:flex-1 sm:flex-shrink">
|
<main class="flex-shrink-0 w-full min-w-0 sm:w-auto sm:flex-1 sm:flex-shrink">
|
||||||
<?= $this->renderSection('content') ?>
|
<nav class="sticky top-0 left-0 z-50 flex items-center w-full h-12 px-2 py-1 sm:hidden bg-pine-900">
|
||||||
</main>
|
<button
|
||||||
|
data-toggle="main-header"
|
||||||
<?= $this->include('podcast/_partials/sidebar') ?>
|
data-toggle-class="sticky -translate-x-full"
|
||||||
|
class="flex-shrink-0 mr-3 overflow-hidden rounded-full focus:ring-2 focus:outline-none focus:ring-pine-50">
|
||||||
<nav class="fixed bottom-0 left-0 z-50 flex items-center w-full px-4 py-4 sm:hidden">
|
|
||||||
<div class="flex items-center w-full p-2 rounded-full shadow-2xl bg-pine-900">
|
|
||||||
<button data-toggle="main-header" data-toggle-class="sticky -translate-x-full" class="flex-shrink-0 mr-3 overflow-hidden rounded-full focus:ring-2 focus:outline-none focus:ring-pine-50">
|
|
||||||
<img src="<?= $podcast->image
|
<img src="<?= $podcast->image
|
||||||
->thumbnail_url ?>" alt="<?= $podcast->title ?>" class="h-14" />
|
->thumbnail_url ?>" alt="<?= $podcast->title ?>" class="h-10"/>
|
||||||
</button>
|
</button>
|
||||||
<p class="flex flex-col flex-1 min-w-0 mr-2 text-white">
|
<p class="flex flex-col flex-1 min-w-0 mr-2 text-white">
|
||||||
<span class="text-sm font-semibold truncate"><?= $podcast->title ?></span>
|
<span class="text-sm font-semibold truncate"><?= $podcast->title ?></span>
|
||||||
@ -91,16 +89,21 @@
|
|||||||
'width' => 420,
|
'width' => 420,
|
||||||
'height' => 620,
|
'height' => 620,
|
||||||
'class' =>
|
'class' =>
|
||||||
'group inline-flex items-center px-4 py-2 text-xs tracking-wider font-semibold text-white uppercase rounded-full shadow focus:outline-none focus:ring bg-rose-600',
|
'group inline-flex mr-2 items-center px-3 py-1 text-xs tracking-wider font-semibold text-white uppercase rounded-full shadow focus:outline-none focus:ring bg-rose-600',
|
||||||
],
|
],
|
||||||
) ?>
|
) ?>
|
||||||
<button data-toggle="main-sidebar" data-toggle-class="translate-x-full" data-toggle-body-class="-ml-64" class="p-4 text-xl rounded-full focus:outline-none focus:ring-2 focus:ring-pine-600 text-pine-200 hover:text-pine-50"><?= icon(
|
<button
|
||||||
|
data-toggle="main-sidebar"
|
||||||
|
data-toggle-class="translate-x-full"
|
||||||
|
data-toggle-body-class="-ml-64"
|
||||||
|
class="p-2 text-xl rounded-full focus:outline-none focus:ring-2 focus:ring-pine-600 text-pine-200 hover:text-pine-50"><?= icon(
|
||||||
'menu',
|
'menu',
|
||||||
) ?><span class="sr-only"><?= lang(
|
) ?><span class="sr-only"><?= lang('Podcast.toggle_podcast_sidebar') ?></span></button>
|
||||||
'Podcast.toggle_podcast_sidebar',
|
|
||||||
) ?></span></button>
|
|
||||||
</div>
|
|
||||||
</nav>
|
</nav>
|
||||||
|
<?= $this->renderSection('content') ?>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<?= $this->include('podcast/_partials/sidebar') ?>
|
||||||
|
|
||||||
<button data-toggle="main-sidebar" data-toggle-class="translate-x-full" data-toggle-body-class="-ml-64" class="fixed z-40 hidden p-4 text-xl rounded-full shadow-2xl sm:block lg:hidden bottom-4 left-4 bg-pine-900 focus:outline-none focus:ring-2 focus:ring-pine-600 text-pine-200 hover:text-pine-50"><?= icon(
|
<button data-toggle="main-sidebar" data-toggle-class="translate-x-full" data-toggle-body-class="-ml-64" class="fixed z-40 hidden p-4 text-xl rounded-full shadow-2xl sm:block lg:hidden bottom-4 left-4 bg-pine-900 focus:outline-none focus:ring-2 focus:ring-pine-600 text-pine-200 hover:text-pine-50"><?= icon(
|
||||||
'menu',
|
'menu',
|
||||||
|
@ -1,36 +1,47 @@
|
|||||||
<div class="flex">
|
<article class="w-full mb-4 bg-white rounded-lg shadow">
|
||||||
<img
|
<div class="flex p-4">
|
||||||
src="<?= $episode->image->thumbnail_url ?>"
|
<div class="relative mr-2">
|
||||||
alt="<?= $episode->title ?>" class="w-24 h-24"/>
|
<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
|
||||||
|
->thumbnail_url ?>" alt="<?= $episode->title ?>" class="object-cover w-20 h-20 rounded-lg" />
|
||||||
|
</div>
|
||||||
<div class="flex flex-col flex-1">
|
<div class="flex flex-col flex-1">
|
||||||
<a href="<?= $episode->link ?>" class="flex-1 px-4 py-2 bg-gray-100">
|
<a class="flex justify-between text-sm" href="<?= $episode->link ?>">
|
||||||
<div class="flex items-baseline">
|
<h2 class="flex-1 font-semibold hover:underline">
|
||||||
<span class="flex-1 w-0 mr-2 font-semibold leading-none truncate"><?= $episode->title ?></span>
|
|
||||||
<?= episode_numbering(
|
<?= episode_numbering(
|
||||||
$episode->number,
|
$episode->number,
|
||||||
$episode->season_number,
|
$episode->season_number,
|
||||||
'text-xs font-semibold text-gray-600',
|
'text-xs font-semibold text-gray-600',
|
||||||
true,
|
true,
|
||||||
) ?>
|
) ?>
|
||||||
</div>
|
<span class="mx-1">-</span>
|
||||||
<div class="text-xs text-gray-800">
|
<?= $episode->title ?>
|
||||||
<time
|
</h2>
|
||||||
itemprop="published"
|
<time class="text-xs whitespace-nowrap" itemprop="published" datetime="<?= $episode->published_at->format(DateTime::ATOM,) ?>" title="<?= $episode->published_at ?>">
|
||||||
datetime="<?= $episode->published_at->format(DateTime::ATOM) ?>"
|
<?= lang('Common.mediumDate', [
|
||||||
title="<?= $episode->published_at ?>">
|
$episode->published_at,
|
||||||
<?= lang('Common.mediumDate', [$episode->published_at]) ?>
|
]) ?>
|
||||||
</time>
|
</time>
|
||||||
<span class="mx-1">•</span>
|
|
||||||
<time datetime="PT<?= $episode->audio_file_duration ?>S">
|
|
||||||
<?= format_duration($episode->audio_file_duration) ?>
|
|
||||||
</time>
|
|
||||||
</div>
|
|
||||||
</a>
|
</a>
|
||||||
<audio controls preload="none" class="w-full mt-auto">
|
<div class="flex mt-auto gap-x-4">
|
||||||
<source
|
<?= play_episode_button($episode->id, $episode->image->thumbnail_url, $episode->title, $podcast->title, $episode->audio_file_web_url, $episode->audio_file_mimetype, 'mt-auto') ?>
|
||||||
src="<?= $episode->audio_file_web_url ?>"
|
<?= anchor(
|
||||||
type="<?= $episode->audio_file_mimetype ?>">
|
route_to('episode', $podcast->handle, $episode->slug),
|
||||||
Your browser does not support the audio tag.
|
icon('chat', 'text-xl mr-1 text-gray-400') .
|
||||||
</audio>
|
$episode->statuses_total,
|
||||||
|
[
|
||||||
|
'class' =>
|
||||||
|
'inline-flex items-center hover:underline',
|
||||||
|
'title' => lang('Episode.total_statuses', [
|
||||||
|
'numberOfTotalStatuses' => $episode->statuses_total,
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
) ?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
32
app/Views/podcast/_partials/episode_preview_card.php
Normal file
32
app/Views/podcast/_partials/episode_preview_card.php
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<div class="flex">
|
||||||
|
<div class="relative">
|
||||||
|
<time class="absolute px-1 text-sm font-semibold text-white bg-black/50 bottom-2 right-2" datetime="PT<?= $episode->audio_file_duration ?>S">
|
||||||
|
<?= format_duration($episode->audio_file_duration) ?>
|
||||||
|
</time>
|
||||||
|
<img
|
||||||
|
src="<?= $episode->image->thumbnail_url ?>"
|
||||||
|
alt="<?= $episode->title ?>" class="w-24 h-24"/>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col flex-1 px-4 py-2 border-t border-b">
|
||||||
|
<a href="<?= $episode->link ?>" class="flex justify-between flex-1">
|
||||||
|
<div class="flex items-baseline font-semibold">
|
||||||
|
<?= episode_numbering(
|
||||||
|
$episode->number,
|
||||||
|
$episode->season_number,
|
||||||
|
'text-xs font-semibold text-gray-600',
|
||||||
|
true,
|
||||||
|
) ?>
|
||||||
|
<span class="mx-1">-</span>
|
||||||
|
<?= $episode->title ?>
|
||||||
|
</div>
|
||||||
|
<time
|
||||||
|
class="text-xs"
|
||||||
|
itemprop="published"
|
||||||
|
datetime="<?= $episode->published_at->format(DateTime::ATOM) ?>"
|
||||||
|
title="<?= $episode->published_at ?>">
|
||||||
|
<?= lang('Common.mediumDate', [$episode->published_at]) ?>
|
||||||
|
</time>
|
||||||
|
</a>
|
||||||
|
<?= play_episode_button($episode->id, $episode->image->thumbnail_url, $episode->title, $podcast->title, $episode->audio_file_web_url, $episode->audio_file_mimetype, 'mt-auto') ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -35,7 +35,7 @@
|
|||||||
</header>
|
</header>
|
||||||
<div class="px-6 mb-4 status-content"><?= $status->message_html ?></div>
|
<div class="px-6 mb-4 status-content"><?= $status->message_html ?></div>
|
||||||
<?php if ($status->episode_id): ?>
|
<?php if ($status->episode_id): ?>
|
||||||
<?= view('podcast/_partials/episode_card', [
|
<?= view('podcast/_partials/episode_preview_card', [
|
||||||
'episode' => $status->episode,
|
'episode' => $status->episode,
|
||||||
]) ?>
|
]) ?>
|
||||||
<?php elseif ($status->has_preview_card): ?>
|
<?php elseif ($status->has_preview_card): ?>
|
||||||
|
@ -35,7 +35,7 @@
|
|||||||
</header>
|
</header>
|
||||||
<div class="px-6 mb-4 status-content"><?= $status->message_html ?></div>
|
<div class="px-6 mb-4 status-content"><?= $status->message_html ?></div>
|
||||||
<?php if ($status->episode_id): ?>
|
<?php if ($status->episode_id): ?>
|
||||||
<?= view('podcast/_partials/episode_card', [
|
<?= view('podcast/_partials/episode_preview_card', [
|
||||||
'episode' => $status->episode,
|
'episode' => $status->episode,
|
||||||
]) ?>
|
]) ?>
|
||||||
<?php elseif ($status->has_preview_card): ?>
|
<?php elseif ($status->has_preview_card): ?>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<article class="relative z-10 w-full bg-white shadow-md rounded-2xl">
|
<article class="relative z-10 w-full bg-white shadow rounded-2xl">
|
||||||
<header class="flex px-6 py-4">
|
<header class="flex px-6 py-4">
|
||||||
<img src="<?= $status->actor
|
<img src="<?= $status->actor
|
||||||
->avatar_image_url ?>" alt="<?= $status->display_name ?>" class="w-12 h-12 mr-4 rounded-full" />
|
->avatar_image_url ?>" alt="<?= $status->display_name ?>" class="w-12 h-12 mr-4 rounded-full" />
|
||||||
@ -28,7 +28,7 @@
|
|||||||
</header>
|
</header>
|
||||||
<div class="px-6 mb-4 status-content"><?= $status->message_html ?></div>
|
<div class="px-6 mb-4 status-content"><?= $status->message_html ?></div>
|
||||||
<?php if ($status->episode_id): ?>
|
<?php if ($status->episode_id): ?>
|
||||||
<?= view('podcast/_partials/episode_card', [
|
<?= view('podcast/_partials/episode_preview_card', [
|
||||||
'episode' => $status->episode,
|
'episode' => $status->episode,
|
||||||
]) ?>
|
]) ?>
|
||||||
<?php elseif ($status->has_preview_card): ?>
|
<?php elseif ($status->has_preview_card): ?>
|
||||||
|
@ -28,7 +28,7 @@
|
|||||||
</header>
|
</header>
|
||||||
<div class="px-6 mb-4 status-content"><?= $status->message_html ?></div>
|
<div class="px-6 mb-4 status-content"><?= $status->message_html ?></div>
|
||||||
<?php if ($status->episode_id): ?>
|
<?php if ($status->episode_id): ?>
|
||||||
<?= view('podcast/_partials/episode_card', [
|
<?= view('podcast/_partials/episode_preview_card', [
|
||||||
'episode' => $status->episode,
|
'episode' => $status->episode,
|
||||||
]) ?>
|
]) ?>
|
||||||
<?php elseif ($status->has_preview_card): ?>
|
<?php elseif ($status->has_preview_card): ?>
|
||||||
|
@ -18,11 +18,13 @@
|
|||||||
<meta property="og:image:width" content="<?= config('Images')->largeSize ?>" />
|
<meta property="og:image:width" content="<?= config('Images')->largeSize ?>" />
|
||||||
<meta property="og:image:height" content="<?= config('Images')->largeSize ?>" />
|
<meta property="og:image:height" content="<?= config('Images')->largeSize ?>" />
|
||||||
<meta name="twitter:card" content="summary_large_image" />
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
|
|
||||||
|
<?= service('vite')->asset('styles/index.css', 'css') ?>
|
||||||
<?= $this->endSection() ?>
|
<?= $this->endSection() ?>
|
||||||
|
|
||||||
<?= $this->section('content') ?>
|
<?= $this->section('content') ?>
|
||||||
|
|
||||||
<nav class="sticky top-0 z-20 flex justify-center pt-2 text-lg bg-pine-50">
|
<nav class="sticky z-20 flex justify-center pt-2 text-lg sm:top-0 top-12 bg-pine-50">
|
||||||
<a href="<?= route_to(
|
<a href="<?= route_to(
|
||||||
'podcast-activity',
|
'podcast-activity',
|
||||||
$podcast->handle,
|
$podcast->handle,
|
||||||
|
@ -18,11 +18,13 @@
|
|||||||
<meta property="og:image:width" content="<?= config('Images')->largeSize ?>" />
|
<meta property="og:image:width" content="<?= config('Images')->largeSize ?>" />
|
||||||
<meta property="og:image:height" content="<?= config('Images')->largeSize ?>" />
|
<meta property="og:image:height" content="<?= config('Images')->largeSize ?>" />
|
||||||
<meta name="twitter:card" content="summary_large_image" />
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
|
|
||||||
|
<?= service('vite')->asset('styles/index.css', 'css') ?>
|
||||||
<?= $this->endSection() ?>
|
<?= $this->endSection() ?>
|
||||||
|
|
||||||
<?= $this->section('content') ?>
|
<?= $this->section('content') ?>
|
||||||
|
|
||||||
<nav class="sticky top-0 z-20 flex justify-center pt-2 text-lg bg-pine-50">
|
<nav class="sticky z-20 flex justify-center pt-2 text-lg top-12 sm:top-0 bg-pine-50">
|
||||||
<a href="<?= route_to(
|
<a href="<?= route_to(
|
||||||
'podcast-activity',
|
'podcast-activity',
|
||||||
$podcast->handle,
|
$podcast->handle,
|
||||||
|
@ -113,12 +113,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<?= location_link($episode->location, 'text-sm mb-4') ?>
|
<?= location_link($episode->location, 'text-sm mb-4') ?>
|
||||||
<?= person_list($episode->persons) ?>
|
<?= person_list($episode->persons) ?>
|
||||||
|
<?= play_episode_button($episode->id, $episode->image->thumbnail_url, $episode->title, $podcast->title, $episode->audio_file_web_url, $episode->audio_file_mimetype) ?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<audio controls preload="none" class="w-full mt-auto">
|
|
||||||
<source src="<?= $episode->audio_file_web_url ?>" type="<?= $episode->audio_file_mimetype ?>">
|
|
||||||
Your browser does not support the audio tag.
|
|
||||||
</audio>
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="tabset">
|
<div class="tabset">
|
||||||
|
@ -113,12 +113,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<?= location_link($episode->location, 'text-sm mb-4') ?>
|
<?= location_link($episode->location, 'text-sm mb-4') ?>
|
||||||
<?= person_list($episode->persons) ?>
|
<?= person_list($episode->persons) ?>
|
||||||
|
<?= play_episode_button($episode->id, $episode->image->thumbnail_url, $episode->title, $podcast->title, $episode->audio_file_web_url, $episode->audio_file_mimetype) ?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<audio controls preload="none" class="w-full mt-auto">
|
|
||||||
<source src="<?= $episode->audio_file_web_url ?>" type="<?= $episode->audio_file_mimetype ?>">
|
|
||||||
Your browser does not support the audio tag.
|
|
||||||
</audio>
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="tabset">
|
<div class="tabset">
|
||||||
|
@ -18,10 +18,12 @@
|
|||||||
<meta property="og:image:width" content="<?= config('Images')->largeSize ?>" />
|
<meta property="og:image:width" content="<?= config('Images')->largeSize ?>" />
|
||||||
<meta property="og:image:height" content="<?= config('Images')->largeSize ?>" />
|
<meta property="og:image:height" content="<?= config('Images')->largeSize ?>" />
|
||||||
<meta name="twitter:card" content="summary_large_image" />
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
|
|
||||||
|
<?= service('vite')->asset('styles/index.css', 'css') ?>
|
||||||
<?= $this->endSection() ?>
|
<?= $this->endSection() ?>
|
||||||
|
|
||||||
<?= $this->section('content') ?>
|
<?= $this->section('content') ?>
|
||||||
<nav class="sticky top-0 flex items-center justify-center pt-2 text-lg bg-pine-50">
|
<nav class="sticky z-20 flex items-center justify-center pt-2 text-lg top-12 sm:top-0 bg-pine-50">
|
||||||
<a href="<?= route_to(
|
<a href="<?= route_to(
|
||||||
'podcast-activity',
|
'podcast-activity',
|
||||||
$podcast->handle,
|
$podcast->handle,
|
||||||
@ -79,85 +81,9 @@
|
|||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</h1>
|
</h1>
|
||||||
<?php foreach ($episodes as $episode): ?>
|
<?php foreach ($episodes as $episode): ?>
|
||||||
<article class="w-full mb-4 bg-white rounded-lg shadow">
|
<?= view('podcast/_partials/episode_card', [
|
||||||
<div class="flex px-4 pt-4 pb-2">
|
'episode' => $episode,
|
||||||
<img loading="lazy" src="<?= $episode->image
|
|
||||||
->thumbnail_url ?>" alt="<?= $episode->title ?>" class="object-cover w-20 h-20 mr-2 rounded-lg" />
|
|
||||||
<div class="flex flex-col flex-1">
|
|
||||||
<a class="text-sm" href="<?= $episode->link ?>">
|
|
||||||
<h2 class="inline-flex justify-between w-full font-semibold leading-none group">
|
|
||||||
<span class="mr-1 group-hover:underline"><?= $episode->title ?></span>
|
|
||||||
<?= episode_numbering(
|
|
||||||
$episode->number,
|
|
||||||
$episode->season_number,
|
|
||||||
'text-xs font-semibold text-gray-600',
|
|
||||||
true,
|
|
||||||
) ?>
|
|
||||||
</h2>
|
|
||||||
</a>
|
|
||||||
<div class="mb-2 text-xs">
|
|
||||||
<time itemprop="published" datetime="<?= $episode->published_at->format(
|
|
||||||
DateTime::ATOM,
|
|
||||||
) ?>" title="<?= $episode->published_at ?>">
|
|
||||||
<?= lang('Common.mediumDate', [
|
|
||||||
$episode->published_at,
|
|
||||||
]) ?>
|
]) ?>
|
||||||
</time>
|
|
||||||
<span class="mx-1">•</span>
|
|
||||||
<time datetime="PT<?= $episode->audio_file_duration ?>S">
|
|
||||||
<?= format_duration(
|
|
||||||
$episode->audio_file_duration,
|
|
||||||
) ?>
|
|
||||||
</time>
|
|
||||||
</div>
|
|
||||||
<audio controls preload="none" class="w-full mt-auto">
|
|
||||||
<source src="<?= $episode->audio_file_web_url ?>" type="<?= $episode->audio_file_mimetype ?>">
|
|
||||||
Your browser does not support the audio tag.
|
|
||||||
</audio>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="px-4 py-2 space-x-4 text-sm">
|
|
||||||
<?= anchor(
|
|
||||||
route_to('episode', $podcast->handle, $episode->slug),
|
|
||||||
icon('chat', 'text-xl mr-1 text-gray-400') .
|
|
||||||
$episode->statuses_total,
|
|
||||||
[
|
|
||||||
'class' =>
|
|
||||||
'inline-flex items-center hover:underline',
|
|
||||||
'title' => lang('Episode.total_statuses', [
|
|
||||||
'numberOfTotalStatuses' => $episode->statuses_total,
|
|
||||||
]),
|
|
||||||
],
|
|
||||||
) ?>
|
|
||||||
<?= anchor(
|
|
||||||
route_to('episode', $podcast->handle, $episode->slug),
|
|
||||||
icon('repeat', 'text-xl mr-1 text-gray-400') .
|
|
||||||
$episode->reblogs_total,
|
|
||||||
[
|
|
||||||
'class' =>
|
|
||||||
'inline-flex items-center hover:underline',
|
|
||||||
'title' => lang('Episode.total_reblogs', [
|
|
||||||
'numberOfTotalReblogs' =>
|
|
||||||
$episode->reblogs_total,
|
|
||||||
]),
|
|
||||||
],
|
|
||||||
) ?>
|
|
||||||
|
|
||||||
<?= anchor(
|
|
||||||
route_to('episode', $podcast->handle, $episode->slug),
|
|
||||||
icon('heart', 'text-xl mr-1 text-gray-400') .
|
|
||||||
$episode->favourites_total,
|
|
||||||
[
|
|
||||||
'class' =>
|
|
||||||
'inline-flex items-center hover:underline',
|
|
||||||
'title' => lang('Episode.total_favourites', [
|
|
||||||
'numberOfTotalFavourites' =>
|
|
||||||
$episode->favourites_total,
|
|
||||||
]),
|
|
||||||
],
|
|
||||||
) ?>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<h1 class="px-4 mb-2 text-xl text-center"><?= lang(
|
<h1 class="px-4 mb-2 text-xl text-center"><?= lang(
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
<title><?= $podcast->title ?></title>
|
<title><?= $podcast->title ?></title>
|
||||||
<meta name="description" content="<?= htmlspecialchars(
|
<meta name="description" content="<?= htmlspecialchars(
|
||||||
$podcast->description,
|
$podcast->description,
|
||||||
) ?>" />
|
) ?>" />
|
||||||
<link rel="shortcut icon" type="image/png" href="/favicon.ico" />
|
<link rel="shortcut icon" type="image/png" href="/favicon.ico" />
|
||||||
<link rel="canonical" href="<?= current_url() ?>" />
|
<link rel="canonical" href="<?= current_url() ?>" />
|
||||||
<meta property="og:title" content="<?= $podcast->title ?>" />
|
<meta property="og:title" content="<?= $podcast->title ?>" />
|
||||||
@ -18,23 +18,25 @@
|
|||||||
<meta property="og:image:width" content="<?= config('Images')->largeSize ?>" />
|
<meta property="og:image:width" content="<?= config('Images')->largeSize ?>" />
|
||||||
<meta property="og:image:height" content="<?= config('Images')->largeSize ?>" />
|
<meta property="og:image:height" content="<?= config('Images')->largeSize ?>" />
|
||||||
<meta name="twitter:card" content="summary_large_image" />
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
|
|
||||||
|
<?= service('vite')->asset('styles/index.css', 'css') ?>
|
||||||
<?= $this->endSection() ?>
|
<?= $this->endSection() ?>
|
||||||
|
|
||||||
<?= $this->section('content') ?>
|
<?= $this->section('content') ?>
|
||||||
<nav class="sticky top-0 flex items-center justify-center pt-2 text-lg bg-pine-50">
|
<nav class="sticky z-20 flex items-center justify-center pt-2 text-lg top-12 sm:top-0 bg-pine-50">
|
||||||
<a href="<?= route_to(
|
<a href="<?= route_to(
|
||||||
'podcast-activity',
|
'podcast-activity',
|
||||||
$podcast->handle,
|
$podcast->handle,
|
||||||
) ?>" class="px-4 py-1 mr-8 rounded-full hover:bg-pine-100"><?= lang(
|
) ?>" class="px-4 py-1 mr-8 rounded-full hover:bg-pine-100"><?= lang(
|
||||||
'Podcast.activity',
|
'Podcast.activity',
|
||||||
) ?></a>
|
) ?></a>
|
||||||
<a href="<?= route_to(
|
<a href="<?= route_to(
|
||||||
'podcast-episodes',
|
'podcast-episodes',
|
||||||
$podcast->handle,
|
$podcast->handle,
|
||||||
) ?>" class="px-4 py-1 font-semibold border-b-4 text-pine-800 border-pine-800"><?= lang(
|
) ?>" class="px-4 py-1 font-semibold border-b-4 text-pine-800 border-pine-800"><?= lang(
|
||||||
'Podcast.episodes',
|
'Podcast.episodes',
|
||||||
) ?></a>
|
) ?></a>
|
||||||
<?php if ($activeQuery): ?>
|
<?php if ($activeQuery) : ?>
|
||||||
<button id="episode-lists-dropdown" type="button" class="inline-flex items-center px-2 py-1 text-sm font-semibold outline-none focus:ring" data-dropdown="button" data-dropdown-target="episode-lists-dropdown-menu" aria-label="<?= lang(
|
<button id="episode-lists-dropdown" type="button" class="inline-flex items-center px-2 py-1 text-sm font-semibold outline-none focus:ring" data-dropdown="button" data-dropdown-target="episode-lists-dropdown-menu" aria-label="<?= lang(
|
||||||
'Common.more',
|
'Common.more',
|
||||||
) ?>" aria-haspopup="true" aria-expanded="false">
|
) ?>" aria-haspopup="true" aria-expanded="false">
|
||||||
@ -45,7 +47,7 @@
|
|||||||
icon('caret-down', 'ml-2 text-xl') ?>
|
icon('caret-down', 'ml-2 text-xl') ?>
|
||||||
</button>
|
</button>
|
||||||
<nav id="episode-lists-dropdown-menu" class="flex flex-col py-2 text-black bg-white border rounded shadow" aria-labelledby="episode-lists-dropdown" data-dropdown="menu" data-dropdown-placement="bottom-end">
|
<nav id="episode-lists-dropdown-menu" class="flex flex-col py-2 text-black bg-white border rounded shadow" aria-labelledby="episode-lists-dropdown" data-dropdown="menu" data-dropdown-placement="bottom-end">
|
||||||
<?php foreach ($episodesNav as $link): ?>
|
<?php foreach ($episodesNav as $link) : ?>
|
||||||
<?= anchor(
|
<?= anchor(
|
||||||
$link['route'],
|
$link['route'],
|
||||||
$link['label'] . ' (' . $link['number_of_episodes'] . ')',
|
$link['label'] . ' (' . $link['number_of_episodes'] . ')',
|
||||||
@ -64,101 +66,26 @@
|
|||||||
|
|
||||||
<section class="flex flex-col max-w-2xl px-6 py-8 mx-auto">
|
<section class="flex flex-col max-w-2xl px-6 py-8 mx-auto">
|
||||||
|
|
||||||
<?php if ($episodes): ?>
|
<?php if ($episodes) : ?>
|
||||||
<h1 class="mb-4 text-xl font-semibold">
|
<h1 class="mb-4 text-xl font-semibold">
|
||||||
<?php if ($activeQuery['type'] == 'year'): ?>
|
<?php if ($activeQuery['type'] == 'year') : ?>
|
||||||
<?= lang('Podcast.list_of_episodes_year', [
|
<?= lang('Podcast.list_of_episodes_year', [
|
||||||
'year' => $activeQuery['value'],
|
'year' => $activeQuery['value'],
|
||||||
'episodeCount' => count($episodes),
|
'episodeCount' => count($episodes),
|
||||||
]) ?>
|
]) ?>
|
||||||
<?php elseif ($activeQuery['type'] == 'season'): ?>
|
<?php elseif ($activeQuery['type'] == 'season') : ?>
|
||||||
<?= lang('Podcast.list_of_episodes_season', [
|
<?= lang('Podcast.list_of_episodes_season', [
|
||||||
'seasonNumber' => $activeQuery['value'],
|
'seasonNumber' => $activeQuery['value'],
|
||||||
'episodeCount' => count($episodes),
|
'episodeCount' => count($episodes),
|
||||||
]) ?>
|
]) ?>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</h1>
|
</h1>
|
||||||
<?php foreach ($episodes as $episode): ?>
|
<?php foreach ($episodes as $episode) : ?>
|
||||||
<article class="w-full mb-4 bg-white rounded-lg shadow">
|
<?= view('podcast/_partials/episode_card', [
|
||||||
<div class="flex px-4 pt-4 pb-2">
|
'episode' => $episode,
|
||||||
<img loading="lazy" src="<?= $episode->image
|
|
||||||
->thumbnail_url ?>" alt="<?= $episode->title ?>" class="object-cover w-20 h-20 mr-2 rounded-lg" />
|
|
||||||
<div class="flex flex-col flex-1">
|
|
||||||
<a class="text-sm" href="<?= $episode->link ?>">
|
|
||||||
<h2 class="inline-flex justify-between w-full font-semibold leading-none group">
|
|
||||||
<span class="mr-1 group-hover:underline"><?= $episode->title ?></span>
|
|
||||||
<?= episode_numbering(
|
|
||||||
$episode->number,
|
|
||||||
$episode->season_number,
|
|
||||||
'text-xs font-semibold text-gray-600',
|
|
||||||
true,
|
|
||||||
) ?>
|
|
||||||
</h2>
|
|
||||||
</a>
|
|
||||||
<div class="mb-2 text-xs">
|
|
||||||
<time itemprop="published" datetime="<?= $episode->published_at->format(
|
|
||||||
DateTime::ATOM,
|
|
||||||
) ?>" title="<?= $episode->published_at ?>">
|
|
||||||
<?= lang('Common.mediumDate', [
|
|
||||||
$episode->published_at,
|
|
||||||
]) ?>
|
]) ?>
|
||||||
</time>
|
|
||||||
<span class="mx-1">•</span>
|
|
||||||
<time datetime="PT<?= $episode->audio_file_duration ?>S">
|
|
||||||
<?= format_duration(
|
|
||||||
$episode->audio_file_duration,
|
|
||||||
) ?>
|
|
||||||
</time>
|
|
||||||
</div>
|
|
||||||
<audio controls preload="none" class="w-full mt-auto">
|
|
||||||
<source src="<?= $episode->audio_file_web_url ?>" type="<?= $episode->audio_file_mimetype ?>">
|
|
||||||
Your browser does not support the audio tag.
|
|
||||||
</audio>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="px-4 py-2 space-x-4 text-sm">
|
|
||||||
<?= anchor(
|
|
||||||
route_to('episode', $podcast->handle, $episode->slug),
|
|
||||||
icon('chat', 'text-xl mr-1 text-gray-400') .
|
|
||||||
$episode->statuses_total,
|
|
||||||
[
|
|
||||||
'class' =>
|
|
||||||
'inline-flex items-center hover:underline',
|
|
||||||
'title' => lang('Episode.total_statuses', [
|
|
||||||
'numberOfTotalStatuses' => $episode->statuses_total,
|
|
||||||
]),
|
|
||||||
],
|
|
||||||
) ?>
|
|
||||||
<?= anchor(
|
|
||||||
route_to('episode', $podcast->handle, $episode->slug),
|
|
||||||
icon('repeat', 'text-xl mr-1 text-gray-400') .
|
|
||||||
$episode->reblogs_total,
|
|
||||||
[
|
|
||||||
'class' =>
|
|
||||||
'inline-flex items-center hover:underline',
|
|
||||||
'title' => lang('Episode.total_reblogs', [
|
|
||||||
'numberOfTotalReblogs' =>
|
|
||||||
$episode->reblogs_total,
|
|
||||||
]),
|
|
||||||
],
|
|
||||||
) ?>
|
|
||||||
<?= anchor(
|
|
||||||
route_to('episode', $podcast->handle, $episode->slug),
|
|
||||||
icon('heart', 'text-xl mr-1 text-gray-400') .
|
|
||||||
$episode->favourites_total,
|
|
||||||
[
|
|
||||||
'class' =>
|
|
||||||
'inline-flex items-center hover:underline',
|
|
||||||
'title' => lang('Episode.total_favourites', [
|
|
||||||
'numberOfTotalFavourites' =>
|
|
||||||
$episode->favourites_total,
|
|
||||||
]),
|
|
||||||
],
|
|
||||||
) ?>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
<?php else: ?>
|
<?php else : ?>
|
||||||
<h1 class="px-4 mb-2 text-xl text-center"><?= lang(
|
<h1 class="px-4 mb-2 text-xl text-center"><?= lang(
|
||||||
'Podcast.no_episode',
|
'Podcast.no_episode',
|
||||||
) ?></h1>
|
) ?></h1>
|
||||||
|
@ -22,6 +22,8 @@
|
|||||||
<meta property="og:url" content="<?= current_url() ?>" />
|
<meta property="og:url" content="<?= current_url() ?>" />
|
||||||
<meta property="og:image" content="<?= $actor->avatar_image_url ?>" />
|
<meta property="og:image" content="<?= $actor->avatar_image_url ?>" />
|
||||||
<meta property="og:description" content="<?= $actor->summary ?>" />
|
<meta property="og:description" content="<?= $actor->summary ?>" />
|
||||||
|
|
||||||
|
<?= service('vite')->asset('styles/index.css', 'css') ?>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|
||||||
|
2086
package-lock.json
generated
2086
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -31,6 +31,7 @@
|
|||||||
"@amcharts/amcharts4": "^4.10.20",
|
"@amcharts/amcharts4": "^4.10.20",
|
||||||
"@github/markdown-toolbar-element": "^1.5.1",
|
"@github/markdown-toolbar-element": "^1.5.1",
|
||||||
"@popperjs/core": "^2.9.2",
|
"@popperjs/core": "^2.9.2",
|
||||||
|
"@vime/core": "^5.0.33",
|
||||||
"choices.js": "^9.0.1",
|
"choices.js": "^9.0.1",
|
||||||
"flatpickr": "^4.6.9",
|
"flatpickr": "^4.6.9",
|
||||||
"leaflet.markercluster": "^1.5.1",
|
"leaflet.markercluster": "^1.5.1",
|
||||||
|
@ -17,6 +17,8 @@ export default defineConfig({
|
|||||||
"admin.ts": "app/Resources/js/admin.ts",
|
"admin.ts": "app/Resources/js/admin.ts",
|
||||||
"charts.ts": "app/Resources/js/charts.ts",
|
"charts.ts": "app/Resources/js/charts.ts",
|
||||||
"map.ts": "app/Resources/js/map.ts",
|
"map.ts": "app/Resources/js/map.ts",
|
||||||
|
"audio-player.ts": "app/Resources/js/audio-player.ts",
|
||||||
|
"embed.ts": "app/Resources/js/embed.ts",
|
||||||
"styles/index.css": "app/Resources/styles/index.css",
|
"styles/index.css": "app/Resources/styles/index.css",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Loading…
Reference in New Issue
Block a user