diff --git a/app/Controllers/EpisodeController.php b/app/Controllers/EpisodeController.php index c484ca14..e51ba7b7 100644 --- a/app/Controllers/EpisodeController.php +++ b/app/Controllers/EpisodeController.php @@ -103,7 +103,7 @@ class EpisodeController extends BaseController 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 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}"; if (! ($cachedView = cache($cacheName))) { - $theme = EpisodeModel::$themes[$theme]; + $themeData = EpisodeModel::$themes[$theme]; $data = [ 'podcast' => $this->podcast, 'episode' => $this->episode, 'theme' => $theme, + 'themeData' => $themeData, ]; $secondsToNextUnpublishedEpisode = (new EpisodeModel())->getSecondsToNextUnpublishedEpisode( @@ -159,9 +160,9 @@ class EpisodeController extends BaseController 'html' => '', + '" width="100%" height="144" frameborder="0" scrolling="no">', 'width' => 600, - 'height' => 200, + 'height' => 144, 'thumbnail_url' => $this->episode->image->large_url, 'thumbnail_width' => config('Images') ->largeSize, @@ -189,11 +190,11 @@ class EpisodeController extends BaseController htmlentities( '', + '" width="100%" height="144" frameborder="0" scrolling="no">', ), ); $oembed->addChild('width', '600'); - $oembed->addChild('height', '200'); + $oembed->addChild('height', '144'); return $this->response->setXML((string) $oembed); } diff --git a/app/Helpers/components_helper.php b/app/Helpers/components_helper.php index 90e33641..58f80a3a 100644 --- a/app/Helpers/components_helper.php +++ b/app/Helpers/components_helper.php @@ -185,7 +185,7 @@ if (! function_exists('data_table')) { * @param mixed[] $data data to loop through and display in rows * @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(); @@ -199,8 +199,8 @@ if (! function_exists('data_table')) { 'cell_start' => '', 'cell_alt_start' => '', - 'row_start' => '', - 'row_alt_start' => '', + 'row_start' => '', + 'row_alt_start' => '', ]; $table->setTemplate($template); @@ -225,7 +225,7 @@ if (! function_exists('data_table')) { return lang('Common.no_data'); } - return '
' . + return '
' . $table->generate() . '
'; } @@ -241,28 +241,16 @@ if (! function_exists('publication_pill')) { */ function publication_pill(?Time $publicationDate, string $publicationStatus, string $customClass = ''): string { - if ($publicationDate === null) { - return ''; - } + $class = match ($publicationStatus) { + '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 = - $publicationStatus === 'published' - ? 'text-pine-500 border-pine-500' - : 'text-red-600 border-red-600'; + $label = lang('Episode.publication_status.' . $publicationStatus); - $langOptions = [ - '', - ]; - - $label = lang('Episode.publication_status.' . $publicationStatus, $langOptions); - - return ' + 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; + } +} diff --git a/app/Helpers/misc_helper.php b/app/Helpers/misc_helper.php index f45726b0..21a7ae5c 100644 --- a/app/Helpers/misc_helper.php +++ b/app/Helpers/misc_helper.php @@ -136,20 +136,26 @@ if (! function_exists('slugify')) { 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 */ - function format_duration(int $seconds, string $separator = ':'): string + function format_duration(int $seconds): string { - return sprintf( - '%02d%s%02d%s%02d', - floor($seconds / 3600), - $separator, - ($seconds / 60) % 60, - $separator, - $seconds % 60, - ); + if ($seconds < 60) { + return '0:' . $seconds; + } + if ($seconds < 3600) { + // < 1 hour: returns MM:SS + return ltrim(gmdate('i:s', $seconds), '0'); + } + if ($seconds < 36000) { + // < 10 hours: returns H:MM:SS + return ltrim(gmdate('h:i:s', $seconds), '0'); + } + return gmdate('h:i:s', $seconds); } } diff --git a/app/Language/en/Common.php b/app/Language/en/Common.php index 0bf26a68..28afa799 100644 --- a/app/Language/en/Common.php +++ b/app/Language/en/Common.php @@ -41,4 +41,8 @@ return [ 'upload_file' => 'Upload a file', 'remote_url' => 'Remote URL', ], + 'play_episode_button' => [ + 'play' => 'Play', + 'playing' => 'Playing', + ], ]; diff --git a/app/Language/en/Episode.php b/app/Language/en/Episode.php index 4cd45688..94f5fc16 100644 --- a/app/Language/en/Episode.php +++ b/app/Language/en/Episode.php @@ -44,10 +44,16 @@ return [ 'go_to_page' => 'Go to page', 'create' => 'Add an episode', 'publication_status' => [ - 'published' => 'Published on {0}', - 'scheduled' => 'Scheduled for {0}', + 'published' => 'Published', + 'scheduled' => 'Scheduled', 'not_published' => 'Not published', ], + 'list' => [ + 'episode' => 'Episode', + 'visibility' => 'Visibility', + 'comments' => 'Comments', + 'actions' => 'Actions', + ], 'form' => [ '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.
These values must be higher than the audio file you wish to upload.', diff --git a/app/Language/fr/Common.php b/app/Language/fr/Common.php index a3415467..64997463 100644 --- a/app/Language/fr/Common.php +++ b/app/Language/fr/Common.php @@ -41,4 +41,8 @@ return [ 'upload_file' => 'Téléversez un fichier', 'remote_url' => 'URL distante', ], + 'play_episode_button' => [ + 'play' => 'Lire', + 'playing' => 'En cours', + ], ]; diff --git a/app/Language/fr/Episode.php b/app/Language/fr/Episode.php index 5f75b506..3a655787 100644 --- a/app/Language/fr/Episode.php +++ b/app/Language/fr/Episode.php @@ -44,10 +44,16 @@ return [ 'go_to_page' => 'Voir', 'create' => 'Ajouter un épisode', 'publication_status' => [ - 'published' => 'Publié le {0}', - 'scheduled' => 'Planifié pour le {0}', + 'published' => 'Publié', + 'scheduled' => 'Planifié', 'not_published' => 'Non publié', ], + 'list' => [ + 'episode' => 'Épisode', + 'visibility' => 'Visibilité', + 'comments' => 'Commentaires', + 'actions' => 'Actions', + ], 'form' => [ '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.
Les valeurs doivent être plus grandes que le fichier audio que vous souhaitez téléverser.', diff --git a/app/Models/EpisodeModel.php b/app/Models/EpisodeModel.php index 78bebf53..d5540624 100644 --- a/app/Models/EpisodeModel.php +++ b/app/Models/EpisodeModel.php @@ -43,7 +43,7 @@ class EpisodeModel extends Model ], 'dark' => [ 'style' => 'background-color: #001f1a;', - 'background' => '#001f1a', + 'background' => '#313131', 'text' => '#fff', 'inverted' => '#000', ], diff --git a/app/Resources/icons/disc.svg b/app/Resources/icons/disc.svg new file mode 100644 index 00000000..095d2cda --- /dev/null +++ b/app/Resources/icons/disc.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/Resources/icons/pause.svg b/app/Resources/icons/pause.svg new file mode 100644 index 00000000..81cffce1 --- /dev/null +++ b/app/Resources/icons/pause.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/Resources/icons/volume-high.svg b/app/Resources/icons/volume-high.svg new file mode 100644 index 00000000..0aa5be36 --- /dev/null +++ b/app/Resources/icons/volume-high.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/Resources/icons/volume-low.svg b/app/Resources/icons/volume-low.svg new file mode 100644 index 00000000..6acfade5 --- /dev/null +++ b/app/Resources/icons/volume-low.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/Resources/icons/volume-mute.svg b/app/Resources/icons/volume-mute.svg new file mode 100644 index 00000000..79bd55ac --- /dev/null +++ b/app/Resources/icons/volume-mute.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/Resources/js/audio-player.ts b/app/Resources/js/audio-player.ts new file mode 100644 index 00000000..0a120376 --- /dev/null +++ b/app/Resources/js/audio-player.ts @@ -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``; + +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); diff --git a/app/Resources/js/embed.ts b/app/Resources/js/embed.ts new file mode 100644 index 00000000..4689cf43 --- /dev/null +++ b/app/Resources/js/embed.ts @@ -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`; +} diff --git a/app/Resources/js/modules/play-episode-button.ts b/app/Resources/js/modules/play-episode-button.ts new file mode 100644 index 00000000..7ec0d417 --- /dev/null +++ b/app/Resources/js/modules/play-episode-button.ts @@ -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 { + 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``; + } +} diff --git a/app/Resources/styles/inputRange.css b/app/Resources/styles/inputRange.css new file mode 100644 index 00000000..053df137 --- /dev/null +++ b/app/Resources/styles/inputRange.css @@ -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; +} diff --git a/app/Views/_layout.php b/app/Views/_layout.php index bf9bcded..5782c039 100644 --- a/app/Views/_layout.php +++ b/app/Views/_layout.php @@ -8,7 +8,9 @@ + asset('styles/index.css', 'css') ?> + asset('js/audio-player.ts', 'js') ?> diff --git a/app/Views/admin/_layout.php b/app/Views/admin/_layout.php index c5375af6..16bcf983 100644 --- a/app/Views/admin/_layout.php +++ b/app/Views/admin/_layout.php @@ -7,8 +7,10 @@ + asset('styles/index.css', 'css') ?> asset('js/admin.ts', 'js') ?> + asset('js/audio-player.ts', 'js') ?> diff --git a/app/Views/admin/contributor/list.php b/app/Views/admin/contributor/list.php index f83064d9..d390902f 100644 --- a/app/Views/admin/contributor/list.php +++ b/app/Views/admin/contributor/list.php @@ -65,6 +65,7 @@ ], ], $podcast->contributors, + '', $podcast, ) ?> diff --git a/app/Views/admin/episode/embeddable_player.php b/app/Views/admin/episode/embeddable_player.php index bb42a4a3..b15060b7 100644 --- a/app/Views/admin/episode/embeddable_player.php +++ b/app/Views/admin/episode/embeddable_player.php @@ -24,7 +24,7 @@
- +
endSection() ?> section('pageTitle') ?> - (getDetails()[ - 'total' - ] ?>) + (getDetails()['total'] ?>) endSection() ?> section('headerRight') ?> @@ -21,110 +19,108 @@ section('content') ?>

$pager->getDetails()['currentPage'], - 'pageCount' => $pager->getDetails()['pageCount'], -]) ?>

-
- - -
- <?= $episode->title ?> -
- -
- published_at, - $episode->publication_status, - ) ?> - -
- - -

- -
+ ) . + '' . + '' . $episode->title . '' . + '
' . + '' . + '

' . + episode_numbering( + $episode->number, + $episode->season_number, + 'text-xs font-semibold text-gray-600', + true, + ) . + '-' . + '' . $episode->title . '' . + '

' . + '

' . $episode->description . '

' . + '
' . + ''; + }, + ], + [ + 'header' => lang('Episode.list.visibility'), + 'cell' => function ($episode) { + return publication_pill( + $episode->published_at, + $episode->publication_status, + ); + }, + ], + [ + 'header' => lang('Episode.list.comments'), + 'cell' => function ($episode) { + return count($episode->comments); + }, + ], + [ + 'header' => lang('Episode.list.actions'), + 'cell' => function ($episode, $podcast) { + return '' . + '' . + ''; + }, + ], + ], + $episodes, + 'mb-6', + $podcast +) ?> links() ?> -endSection() ?> +endSection() ?> \ No newline at end of file diff --git a/app/Views/admin/episode/publish.php b/app/Views/admin/episode/publish.php index e1ed34bc..a3c192a2 100644 --- a/app/Views/admin/episode/publish.php +++ b/app/Views/admin/episode/publish.php @@ -73,10 +73,7 @@ - + audio_file_url, $episode->audio_file_mimetype, 'mt-auto') ?>