Commit 0e14eb4d authored by Yassine Doghri's avatar Yassine Doghri
Browse files

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
parent b72e7c86
Loading
Loading
Loading
Loading
+7 −6
Original line number Diff line number Diff line
@@ -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' =>
                '<iframe src="' .
                $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,
            '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(
                '<iframe src="' .
                    $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('height', '200');
        $oembed->addChild('height', '144');

        return $this->response->setXML((string) $oembed);
    }
+88 −25
Original line number Diff line number Diff line
@@ -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' => '<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_alt_start' => '<tr class="hover:bg-pine-100">',
            'row_start' => '<tr class="bg-gray-50 hover:bg-pine-50">',
            'row_alt_start' => '<tr class="hover:bg-pine-50">',
        ];

        $table->setTemplate($template);
@@ -225,7 +225,7 @@ if (! function_exists('data_table')) {
            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() .
            '</div>';
    }
@@ -241,28 +241,16 @@ if (! function_exists('publication_pill')) {
     */
    function publication_pill(?Time $publicationDate, string $publicationStatus, string $customClass = ''): string
    {
        if ($publicationDate === null) {
            return '';
        }

        $class =
            $publicationStatus === 'published'
                ? 'text-pine-500 border-pine-500'
                : 'text-red-600 border-red-600';

        $langOptions = [
            '<time pubdate datetime="' .
            $publicationDate->format(DateTime::ATOM) .
            '" title="' .
            $publicationDate .
            '">' .
            lang('Common.mediumDate', [$publicationDate]) .
            '</time>',
        ];
        $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',
        };

        $label = lang('Episode.publication_status.' . $publicationStatus, $langOptions);
        $label = lang('Episode.publication_status.' . $publicationStatus);

        return '<span class="px-1 font-semibold border ' .
        return '<span ' . ($publicationDate === null ? '' : 'title="' . $publicationDate . '"') . ' class="px-1 font-semibold border rounded ' .
            $class .
            ' ' .
            $customClass .
@@ -354,7 +342,7 @@ if (! function_exists('episode_numbering')) {
        }

        if ($isAbbr) {
            return '<abbr class="' .
            return '<abbr class="tracking-wider ' .
                $class .
                '" title="' .
                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;
    }
}
+16 −10
Original line number Diff line number Diff line
@@ -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);
    }
}

+4 −0
Original line number Diff line number Diff line
@@ -41,4 +41,8 @@ return [
        'upload_file' => 'Upload a file',
        'remote_url' => 'Remote URL',
    ],
    'play_episode_button' => [
        'play' => 'Play',
        'playing' => 'Playing',
    ],
];
+8 −2
Original line number Diff line number Diff line
@@ -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.<br />These values must be higher than the audio file you wish to upload.',
Loading