feat(episode): add preview link in admin to view and share episode before publication
This commit is contained in:
parent
7a1eea58d3
commit
7d21b3509e
|
@ -210,6 +210,15 @@ $routes->get('audio/@(:podcastHandle)/(:slug).(:alphanum)', 'EpisodeAudioControl
|
|||
'as' => 'episode-audio',
|
||||
], );
|
||||
|
||||
// episode preview link
|
||||
$routes->get('/p/(:uuid)', 'EpisodePreviewController::index/$1', [
|
||||
'as' => 'episode-preview',
|
||||
]);
|
||||
|
||||
$routes->get('/p/(:uuid)/activity', 'EpisodePreviewController::activity/$1', [
|
||||
'as' => 'episode-preview-activity',
|
||||
]);
|
||||
|
||||
// Other pages
|
||||
$routes->get('/credits', 'CreditsController', [
|
||||
'as' => 'credits',
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @copyright 2023 Ad Aures
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Entities\Episode;
|
||||
use App\Models\EpisodeModel;
|
||||
use CodeIgniter\Exceptions\PageNotFoundException;
|
||||
use CodeIgniter\HTTP\RedirectResponse;
|
||||
|
||||
class EpisodePreviewController extends BaseController
|
||||
{
|
||||
protected Episode $episode;
|
||||
|
||||
public function _remap(string $method, string ...$params): mixed
|
||||
{
|
||||
if (count($params) < 1) {
|
||||
throw PageNotFoundException::forPageNotFound();
|
||||
}
|
||||
|
||||
// find episode by previewUUID
|
||||
$episode = (new EpisodeModel())->getEpisodeByPreviewId($params[0]);
|
||||
|
||||
if (! $episode instanceof Episode) {
|
||||
throw PageNotFoundException::forPageNotFound();
|
||||
}
|
||||
|
||||
$this->episode = $episode;
|
||||
|
||||
if ($episode->publication_status === 'published') {
|
||||
// redirect to episode page
|
||||
return redirect()->route('episode', [$episode->podcast->handle, $episode->slug]);
|
||||
}
|
||||
|
||||
unset($params[0]);
|
||||
|
||||
return $this->{$method}(...$params);
|
||||
}
|
||||
|
||||
public function index(): RedirectResponse | string
|
||||
{
|
||||
helper('form');
|
||||
|
||||
return view('episode/preview-comments', [
|
||||
'podcast' => $this->episode->podcast,
|
||||
'episode' => $this->episode,
|
||||
]);
|
||||
}
|
||||
|
||||
public function activity(): RedirectResponse | string
|
||||
{
|
||||
helper('form');
|
||||
|
||||
return view('episode/preview-activity', [
|
||||
'podcast' => $this->episode->podcast,
|
||||
'episode' => $this->episode,
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
class AddEpisodePreviewId extends BaseMigration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
$fields = [
|
||||
'preview_id' => [
|
||||
'type' => 'BINARY',
|
||||
'constraint' => 16,
|
||||
'after' => 'podcast_id',
|
||||
],
|
||||
];
|
||||
|
||||
$this->forge->addColumn('episodes', $fields);
|
||||
|
||||
// set preview_id as unique key
|
||||
$prefix = $this->db->getPrefix();
|
||||
$uniquePreviewId = <<<CODE_SAMPLE
|
||||
ALTER TABLE `{$prefix}episodes`
|
||||
ADD CONSTRAINT `preview_id` UNIQUE (`preview_id`);
|
||||
CODE_SAMPLE;
|
||||
|
||||
$this->db->query($uniquePreviewId);
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
$fields = ['preview_id'];
|
||||
$this->forge->dropColumn('episodes', $fields);
|
||||
}
|
||||
}
|
|
@ -14,6 +14,7 @@ use App\Entities\Clip\Soundbite;
|
|||
use App\Libraries\SimpleRSSElement;
|
||||
use App\Models\ClipModel;
|
||||
use App\Models\EpisodeCommentModel;
|
||||
use App\Models\EpisodeModel;
|
||||
use App\Models\PersonModel;
|
||||
use App\Models\PodcastModel;
|
||||
use App\Models\PostModel;
|
||||
|
@ -21,6 +22,7 @@ use CodeIgniter\Entity\Entity;
|
|||
use CodeIgniter\Files\File;
|
||||
use CodeIgniter\HTTP\Files\UploadedFile;
|
||||
use CodeIgniter\I18n\Time;
|
||||
use Exception;
|
||||
use League\CommonMark\Environment\Environment;
|
||||
use League\CommonMark\Extension\Autolink\AutolinkExtension;
|
||||
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
|
||||
|
@ -39,6 +41,8 @@ use SimpleXMLElement;
|
|||
* @property int $id
|
||||
* @property int $podcast_id
|
||||
* @property Podcast $podcast
|
||||
* @property ?string $preview_id
|
||||
* @property string $preview_link
|
||||
* @property string $link
|
||||
* @property string $guid
|
||||
* @property string $slug
|
||||
|
@ -150,6 +154,7 @@ class Episode extends Entity
|
|||
protected $casts = [
|
||||
'id' => 'integer',
|
||||
'podcast_id' => 'integer',
|
||||
'preview_id' => '?string',
|
||||
'guid' => 'string',
|
||||
'slug' => 'string',
|
||||
'title' => 'string',
|
||||
|
@ -509,7 +514,7 @@ class Episode extends Entity
|
|||
|
||||
if ($this->getPodcast()->episode_description_footer_html) {
|
||||
$descriptionHtml .= "<footer>{$this->getPodcast()
|
||||
->episode_description_footer_html}</footer>";
|
||||
->episode_description_footer_html}</footer>";
|
||||
}
|
||||
|
||||
return $descriptionHtml;
|
||||
|
@ -667,4 +672,18 @@ class Episode extends Entity
|
|||
urlencode((string) $this->attributes['guid']) .
|
||||
($serviceSlug !== null ? '&_from=' . $serviceSlug : '');
|
||||
}
|
||||
|
||||
public function getPreviewLink(): string
|
||||
{
|
||||
if ($this->preview_id === null) {
|
||||
// generate preview id
|
||||
if (! $previewUUID = (new EpisodeModel())->setEpisodePreviewId($this->id)) {
|
||||
throw new Exception('Could not set episode preview id');
|
||||
}
|
||||
|
||||
$this->preview_id = $previewUUID;
|
||||
}
|
||||
|
||||
return url_to('episode-preview', (string) $this->preview_id);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ declare(strict_types=1);
|
|||
*/
|
||||
|
||||
use App\Entities\Category;
|
||||
use App\Entities\Episode;
|
||||
use App\Entities\Location;
|
||||
use CodeIgniter\I18n\Time;
|
||||
use CodeIgniter\View\Table;
|
||||
|
@ -218,8 +219,8 @@ if (! function_exists('publication_status_banner')) {
|
|||
}
|
||||
|
||||
return <<<HTML
|
||||
<div class="flex items-center px-12 py-2 border-b bg-stripes-gray border-subtle" role="alert">
|
||||
<p class="flex items-center text-gray-900">
|
||||
<div class="flex flex-wrap items-baseline px-4 py-2 border-b md:px-12 bg-stripes-default border-subtle" role="alert">
|
||||
<p class="flex items-baseline text-gray-900">
|
||||
<span class="text-xs font-semibold tracking-wide uppercase">{$bannerDisclaimer}</span>
|
||||
<span class="ml-3 text-sm">{$bannerText}</span>
|
||||
</p>
|
||||
|
@ -231,6 +232,58 @@ if (! function_exists('publication_status_banner')) {
|
|||
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
if (! function_exists('episode_publication_status_banner')) {
|
||||
/**
|
||||
* Publication status banner component for podcasts
|
||||
*
|
||||
* Displays the appropriate banner depending on the podcast's publication status.
|
||||
*/
|
||||
function episode_publication_status_banner(Episode $episode, string $class = ''): string
|
||||
{
|
||||
switch ($episode->publication_status) {
|
||||
case 'not_published':
|
||||
$linkRoute = route_to('episode-publish', $episode->podcast_id, $episode->id);
|
||||
$publishLinkLabel = lang('Episode.publish');
|
||||
break;
|
||||
case 'scheduled':
|
||||
case 'with_podcast':
|
||||
$linkRoute = route_to('episode-publish_edit', $episode->podcast_id, $episode->id);
|
||||
$publishLinkLabel = lang('Episode.publish_edit');
|
||||
break;
|
||||
default:
|
||||
$bannerDisclaimer = '';
|
||||
$linkRoute = '';
|
||||
$publishLinkLabel = '';
|
||||
break;
|
||||
}
|
||||
|
||||
$bannerDisclaimer = lang('Episode.publication_status_banner.draft_mode');
|
||||
$bannerText = lang('Episode.publication_status_banner.text', [
|
||||
'publication_status' => $episode->publication_status,
|
||||
'publication_date' => $episode->published_at instanceof Time ? local_datetime(
|
||||
$episode->published_at
|
||||
) : null,
|
||||
], null, false);
|
||||
$previewLinkLabel = lang('Episode.publication_status_banner.preview');
|
||||
|
||||
return <<<HTML
|
||||
<div class="flex flex-wrap gap-4 items-baseline px-4 md:px-12 py-2 bg-stripes-default border-subtle {$class}" role="alert">
|
||||
<p class="flex items-baseline text-gray-900">
|
||||
<span class="text-xs font-semibold tracking-wide uppercase">{$bannerDisclaimer}</span>
|
||||
<span class="ml-3 text-sm">{$bannerText}</span>
|
||||
</p>
|
||||
<div class="flex items-baseline">
|
||||
<a href="{$episode->preview_link}" class="ml-1 text-sm font-semibold underline shadow-xs text-accent-base hover:text-accent-hover hover:no-underline">{$previewLinkLabel}</a>
|
||||
<span class="mx-1">•</span>
|
||||
<a href="{$linkRoute}" class="ml-1 text-sm font-semibold underline shadow-xs text-accent-base hover:text-accent-hover hover:no-underline">{$publishLinkLabel}</a>
|
||||
</div>
|
||||
</div>
|
||||
HTML;
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
if (! function_exists('episode_numbering')) {
|
||||
/**
|
||||
* Returns relevant translated episode numbering.
|
||||
|
@ -360,7 +413,7 @@ if (! function_exists('relative_time')) {
|
|||
$datetime = $time->format(DateTime::ATOM);
|
||||
|
||||
return <<<HTML
|
||||
<relative-time tense="past" class="{$class}" datetime="{$datetime}">
|
||||
<relative-time tense="auto" class="{$class}" datetime="{$datetime}">
|
||||
<time
|
||||
datetime="{$datetime}"
|
||||
title="{$time}">{$translatedDate}</time>
|
||||
|
@ -378,10 +431,10 @@ if (! function_exists('local_datetime')) {
|
|||
'request'
|
||||
)->getLocale(), IntlDateFormatter::MEDIUM, IntlDateFormatter::LONG);
|
||||
$translatedDate = $time->toLocalizedString($formatter->getPattern());
|
||||
$datetime = $time->format(DateTime::ISO8601);
|
||||
$datetime = $time->format(DateTime::ATOM);
|
||||
|
||||
return <<<HTML
|
||||
<relative-time datetime="{$datetime}"
|
||||
<relative-time datetime="{$datetime}"
|
||||
prefix=""
|
||||
threshold="PT0S"
|
||||
weekday="long"
|
||||
|
|
|
@ -82,14 +82,12 @@ if (! function_exists('write_audio_file_tags')) {
|
|||
|
||||
// write tags
|
||||
if ($tagwriter->WriteTags()) {
|
||||
echo 'Successfully wrote tags<br>';
|
||||
// Successfully wrote tags
|
||||
if ($tagwriter->warnings !== []) {
|
||||
echo 'There were some warnings:<br>' .
|
||||
implode('<br><br>', $tagwriter->warnings);
|
||||
log_message('warning', 'There were some warnings:' . PHP_EOL . implode(PHP_EOL, $tagwriter->warnings));
|
||||
}
|
||||
} else {
|
||||
echo 'Failed to write tags!<br>' .
|
||||
implode('<br><br>', $tagwriter->errors);
|
||||
log_message('critical', 'Failed to write tags!' . PHP_EOL . implode(PHP_EOL, $tagwriter->errors));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,4 +30,16 @@ return [
|
|||
}',
|
||||
'all_podcast_episodes' => 'All podcast episodes',
|
||||
'back_to_podcast' => 'Go back to podcast',
|
||||
'preview' => [
|
||||
'title' => 'Preview',
|
||||
'not_published' => 'Not published',
|
||||
'text' => '{publication_status, select,
|
||||
published {This episode is not yet published.}
|
||||
scheduled {This episode is scheduled for publication on {publication_date}.}
|
||||
with_podcast {This episode will be published at the same time as the podcast.}
|
||||
other {This episode is not yet published.}
|
||||
}',
|
||||
'publish' => 'Publish',
|
||||
'publish_edit' => 'Edit publication',
|
||||
],
|
||||
];
|
||||
|
|
|
@ -14,9 +14,10 @@ use App\Entities\Episode;
|
|||
use CodeIgniter\Database\BaseBuilder;
|
||||
use CodeIgniter\Database\BaseResult;
|
||||
use CodeIgniter\I18n\Time;
|
||||
use CodeIgniter\Model;
|
||||
use Michalsn\Uuid\UuidModel;
|
||||
use Ramsey\Uuid\Lazy\LazyUuidFromString;
|
||||
|
||||
class EpisodeModel extends Model
|
||||
class EpisodeModel extends UuidModel
|
||||
{
|
||||
/**
|
||||
* TODO: remove, shouldn't be here
|
||||
|
@ -50,6 +51,11 @@ class EpisodeModel extends Model
|
|||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
protected $uuidFields = ['preview_id'];
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
|
@ -61,6 +67,7 @@ class EpisodeModel extends Model
|
|||
protected $allowedFields = [
|
||||
'id',
|
||||
'podcast_id',
|
||||
'preview_id',
|
||||
'guid',
|
||||
'title',
|
||||
'slug',
|
||||
|
@ -188,6 +195,38 @@ class EpisodeModel extends Model
|
|||
return $found;
|
||||
}
|
||||
|
||||
public function getEpisodeByPreviewId(string $previewId): ?Episode
|
||||
{
|
||||
$cacheName = "podcast_episode#preview-{$previewId}";
|
||||
if (! ($found = cache($cacheName))) {
|
||||
$builder = $this->where([
|
||||
'preview_id' => $this->uuid->fromString($previewId)
|
||||
->getBytes(),
|
||||
]);
|
||||
|
||||
$found = $builder->first();
|
||||
|
||||
cache()
|
||||
->save($cacheName, $found, DECADE);
|
||||
}
|
||||
|
||||
return $found;
|
||||
}
|
||||
|
||||
public function setEpisodePreviewId(int $episodeId): string|false
|
||||
{
|
||||
/** @var LazyUuidFromString $uuid */
|
||||
$uuid = $this->uuid->{$this->uuidVersion}();
|
||||
|
||||
if (! $this->update($episodeId, [
|
||||
'preview_id' => $uuid,
|
||||
])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (string) $uuid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all episodes for a podcast ordered according to podcast type Filtered depending on year or season
|
||||
*
|
||||
|
|
|
@ -59,7 +59,7 @@
|
|||
);
|
||||
}
|
||||
|
||||
.bg-stripes-gray {
|
||||
.bg-stripes-default {
|
||||
background-image: repeating-linear-gradient(
|
||||
-45deg,
|
||||
#f3f4f6,
|
||||
|
|
|
@ -22,6 +22,7 @@ return [
|
|||
'all_podcast_episodes' => 'All podcast episodes',
|
||||
'back_to_podcast' => 'Go back to podcast',
|
||||
'edit' => 'Edit',
|
||||
'preview' => 'Preview',
|
||||
'publish' => 'Publish',
|
||||
'publish_edit' => 'Edit publication',
|
||||
'publish_date_edit' => 'Edit publication date',
|
||||
|
@ -211,4 +212,14 @@ return [
|
|||
'light' => 'Light',
|
||||
'light-transparent' => 'Light transparent',
|
||||
],
|
||||
'publication_status_banner' => [
|
||||
'draft_mode' => 'draft mode',
|
||||
'text' => '{publication_status, select,
|
||||
published {This episode is not yet published.}
|
||||
scheduled {This episode is scheduled for publication on {publication_date}.}
|
||||
with_podcast {This episode will be published at the same time as the podcast.}
|
||||
other {This episode is not yet published.}
|
||||
}',
|
||||
'preview' => 'Preview',
|
||||
],
|
||||
];
|
||||
|
|
|
@ -67,6 +67,9 @@ $isEpisodeArea = isset($podcast) && isset($episode);
|
|||
<?= publication_status_banner($podcast->published_at, $podcast->id, $podcast->publication_status) ?>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
<?php if ($isEpisodeArea && $episode->publication_status !== 'published'): ?>
|
||||
<?= episode_publication_status_banner($episode, 'border-b') ?>
|
||||
<?php endif; ?>
|
||||
<div class="px-2 py-8 mx-auto md:px-12">
|
||||
<?= view('_message_block') ?>
|
||||
<?= $this->renderSection('content') ?>
|
||||
|
|
|
@ -0,0 +1,199 @@
|
|||
<?= helper('page') ?>
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="<?= service('request')
|
||||
->getLocale() ?>">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<link rel="icon" type="image/x-icon" href="<?= get_site_icon_url('ico') ?>" />
|
||||
<link rel="apple-touch-icon" href="<?= get_site_icon_url('180') ?>">
|
||||
<link rel="manifest" href="<?= route_to('podcast-webmanifest', esc($podcast->handle)) ?>">
|
||||
<meta name="theme-color" content="<?= \App\Controllers\WebmanifestController::THEME_COLORS[service('settings')->get('App.theme')]['theme'] ?>">
|
||||
<script>
|
||||
// Check that service workers are supported
|
||||
if ('serviceWorker' in navigator) {
|
||||
// Use the window load event to keep the page load performant
|
||||
window.addEventListener('load', () => {
|
||||
navigator.serviceWorker.register('/sw.js');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<meta name="robots" content="noindex">
|
||||
|
||||
<title>[<?= lang('Episode.preview.title') ?>] <?= $episode->title ?></title>
|
||||
<meta name="description" content="<?= esc($episode->description) ?>">
|
||||
|
||||
<link rel='stylesheet' type='text/css' href='<?= route_to('themes-colors-css') ?>' />
|
||||
<?= service('vite')
|
||||
->asset('styles/index.css', 'css') ?>
|
||||
<?= service('vite')
|
||||
->asset('js/app.ts', 'js') ?>
|
||||
<?= service('vite')
|
||||
->asset('js/podcast.ts', 'js') ?>
|
||||
<?= service('vite')
|
||||
->asset('js/audio-player.ts', 'js') ?>
|
||||
</head>
|
||||
|
||||
<body class="flex flex-col min-h-screen mx-auto md:min-h-full md:grid md:grid-cols-podcast bg-base theme-<?= service('settings')
|
||||
->get('App.theme') ?>">
|
||||
<?php if (can_user_interact()): ?>
|
||||
<div class="col-span-full">
|
||||
<?= $this->include('_admin_navbar') ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<nav class="flex items-center justify-between h-10 col-start-2 text-white bg-header">
|
||||
<a href="<?= route_to('podcast-episodes', esc($podcast->handle)) ?>" class="flex items-center h-full min-w-0 px-2 gap-x-2 focus:ring-accent focus:ring-inset" title="<?= lang('Episode.back_to_episodes', [
|
||||
'podcast' => esc($podcast->title),
|
||||
]) ?>">
|
||||
<?= icon('arrow-left', 'text-lg flex-shrink-0') ?>
|
||||
<div class="flex items-center min-w-0 gap-x-2">
|
||||
<img class="w-8 h-8 rounded-full" src="<?= $episode->podcast->cover->tiny_url ?>" alt="<?= esc($episode->podcast->title) ?>" loading="lazy" />
|
||||
<div class="flex flex-col overflow-hidden">
|
||||
<span class="text-sm font-semibold leading-none truncate"><?= esc($episode->podcast->title) ?></span>
|
||||
<span class="text-xs"><?= lang('Podcast.followers', [
|
||||
'numberOfFollowers' => $podcast->actor->followers_count,
|
||||
]) ?></span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<div class="inline-flex items-center self-end h-full px-2 gap-x-2">
|
||||
<?php if (in_array(true, array_column($podcast->fundingPlatforms, 'is_visible'), true)): ?>
|
||||
<button class="p-2 text-red-600 bg-white rounded-full shadow hover:text-red-500 focus:ring-accent" data-toggle="funding-links" data-toggle-class="hidden" title="<?= lang('Podcast.sponsor') ?>"><Icon glyph="heart"></Icon></button>
|
||||
<?php endif; ?>
|
||||
<?= anchor_popup(
|
||||
route_to('follow', esc($podcast->handle)),
|
||||
icon(
|
||||
'social/castopod',
|
||||
'mr-2 text-xl text-black/75 group-hover:text-black',
|
||||
) . lang('Podcast.follow'),
|
||||
[
|
||||
'width' => 420,
|
||||
'height' => 620,
|
||||
'class' => 'group inline-flex items-center px-3 leading-8 text-xs tracking-wider font-semibold text-black uppercase rounded-full shadow focus:ring-accent bg-white',
|
||||
],
|
||||
) ?>
|
||||
</div>
|
||||
</nav>
|
||||
<header class="relative z-50 flex flex-col col-start-2 px-8 pt-8 pb-4 overflow-hidden bg-accent-base/75 gap-y-4">
|
||||
<div class="absolute top-0 left-0 w-full h-full bg-center bg-no-repeat bg-cover blur-lg mix-blend-overlay filter grayscale" style="background-image: url('<?= get_podcast_banner_url($episode->podcast, 'small') ?>');"></div>
|
||||
<div class="absolute top-0 left-0 w-full h-full bg-gradient-to-t from-background-header to-transparent"></div>
|
||||
<div class="z-10 flex flex-col items-start gap-y-2 gap-x-4 sm:flex-row">
|
||||
<div class="relative flex-shrink-0">
|
||||
<?= explicit_badge($episode->parental_advisory === 'explicit', 'rounded absolute left-0 bottom-0 ml-2 mb-2 bg-black/75 text-accent-contrast') ?>
|
||||
<?php if ($episode->is_premium): ?>
|
||||
<Icon glyph="exchange-dollar" class="absolute left-0 w-8 pl-2 text-2xl rounded-r-full rounded-tl-lg top-2 text-accent-contrast bg-accent-base" />
|
||||
<?php endif; ?>
|
||||
<img src="<?= $episode->cover->medium_url ?>" alt="<?= esc($episode->title) ?>" class="flex-shrink-0 rounded-md shadow-xl h-36 aspect-square" loading="lazy" />
|
||||
</div>
|
||||
<div class="flex flex-col items-start w-full min-w-0 text-white">
|
||||
<?= episode_numbering($episode->number, $episode->season_number, 'text-sm leading-none font-semibold px-1 py-1 text-white/90 border !no-underline border-subtle', true) ?>
|
||||
<h1 class="inline-flex items-baseline max-w-lg mt-2 text-2xl font-bold sm:leading-none sm:text-3xl font-display line-clamp-2" title="<?= esc($episode->title) ?>"><?= esc($episode->title) ?></h1>
|
||||
<div class="flex items-center w-full mt-4 gap-x-8">
|
||||
<?php if ($episode->persons !== []): ?>
|
||||
<button class="flex items-center flex-shrink-0 text-xs font-semibold gap-x-2 hover:underline focus:ring-accent" data-toggle="persons-list" data-toggle-class="hidden">
|
||||
<span class="inline-flex flex-row-reverse">
|
||||
<?php $i = 0; ?>
|
||||
<?php foreach ($episode->persons as $person): ?>
|
||||
<img src="<?= get_avatar_url($person, 'thumbnail') ?>" alt="<?= esc($person->full_name) ?>" class="object-cover w-8 h-8 -ml-4 border-2 rounded-full aspect-square border-background-header last:ml-0" loading="lazy" />
|
||||
<?php $i++;
|
||||
if ($i === 3) {
|
||||
break;
|
||||
}?>
|
||||
<?php endforeach; ?>
|
||||
</span>
|
||||
<?= lang('Episode.persons', [
|
||||
'personsCount' => count($episode->persons),
|
||||
]) ?>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<?php if ($episode->location): ?>
|
||||
<?= location_link($episode->location, 'text-xs font-semibold p-2') ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="z-10 inline-flex items-center text-white gap-x-4">
|
||||
<play-episode-button
|
||||
id="<?= $episode->id ?>"
|
||||
imageSrc="<?= $episode->cover->thumbnail_url ?>"
|
||||
title="<?= esc($episode->title) ?>"
|
||||
podcast="<?= esc($episode->podcast->title) ?>"
|
||||
src="<?= $episode->audio->file_url ?>"
|
||||
mediaType="<?= $episode->audio->file_mimetype ?>"
|
||||
playLabel="<?= lang('Common.play_episode_button.play') ?>"
|
||||
playingLabel="<?= lang('Common.play_episode_button.playing') ?>"></play-episode-button>
|
||||
<div class="text-xs">
|
||||
<?php if ($episode->published_at): ?>
|
||||
<?= relative_time($episode->published_at) ?>
|
||||
<?php else: ?>
|
||||
<?= lang('Episode.preview.not_published') ?>
|
||||
<?php endif; ?>
|
||||
<span class="mx-1">•</span>
|
||||
<time datetime="PT<?= round($episode->audio->duration, 3) ?>S">
|
||||
<?= format_duration_symbol((int) $episode->audio->duration) ?>
|
||||
</time>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div class="col-start-2 px-8 py-4 text-white bg-header">
|
||||
<h2 class="text-xs font-bold tracking-wider uppercase whitespace-pre-line font-display"><?= lang('Episode.description') ?></h2>
|
||||
<?php if (substr_count($episode->description_markdown, "\n") > 6 || strlen($episode->description) > 500): ?>
|
||||
<SeeMore class="max-w-xl prose-sm text-white"><?= $episode->getDescriptionHtml('-+Website+-') ?></SeeMore>
|
||||
<?php else: ?>
|
||||
<div class="max-w-xl prose-sm text-white"><?= $episode->getDescriptionHtml('-+Website+-') ?></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?= $this->include('episode/_partials/navigation') ?>
|
||||
<?= $this->include('podcast/_partials/premium_banner') ?>
|
||||
<div class="flex flex-wrap items-center min-h-[2.5rem] col-start-2 p-1 mt-2 md:mt-4 rounded-conditional-full gap-y-2 sm:flex-row bg-accent-base text-accent-contrast" role="alert">
|
||||
<div class="flex flex-wrap gap-4 pl-2">
|
||||
<div class="inline-flex items-center gap-2 font-semibold tracking-wide uppercase">
|
||||
<Icon glyph="eye" />
|
||||
<span class="text-xs"><?= lang('Episode.preview.title') ?></span>
|
||||
</div>
|
||||
<p class="text-sm">
|
||||
<?= lang('Episode.preview.text', [
|
||||
'publication_status' => $episode->publication_status,
|
||||
'publication_date' => $episode->published_at ? local_datetime($episode->published_at) : null,
|
||||
], null, false); ?>
|
||||
</p>
|
||||
</div>
|
||||
<?php if (auth()->loggedIn()): ?>
|
||||
<?php if (in_array($episode->publication_status, ['scheduled', 'with_podcast'], true)): ?>
|
||||
<Button
|
||||
iconLeft="upload-cloud"
|
||||
variant="primary"
|
||||
size="small"
|
||||
class="ml-auto"
|
||||
uri="<?= route_to('episode-publish_edit', $episode->podcast_id, $episode->id) ?>"><?= lang('Episode.preview.publish_edit') ?></Button>
|
||||
<?php else: ?>
|
||||
<Button
|
||||
iconLeft="upload-cloud"
|
||||
variant="secondary"
|
||||
size="small"
|
||||
class="ml-auto"
|
||||
uri="<?= route_to('episode-publish', $episode->podcast_id, $episode->id) ?>"><?= lang('Episode.preview.publish') ?></Button>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="relative grid items-start flex-1 col-start-2 grid-cols-podcastMain gap-x-6">
|
||||
<main class="w-full col-start-1 row-start-1 py-6 col-span-full md:col-span-1">
|
||||
<?= $this->renderSection('content') ?>
|
||||
</main>
|
||||
<div data-sidebar-toggler="backdrop" class="absolute top-0 left-0 z-10 hidden w-full h-full bg-backdrop/75 md:hidden" role="button" tabIndex="0" aria-label="<?= lang('Common.close') ?>"></div>
|
||||
<?= $this->include('podcast/_partials/sidebar') ?>
|
||||
</div>
|
||||
<?= view('_persons_modal', [
|
||||
'title' => lang('Episode.persons_list', [
|
||||
'episodeTitle' => esc($episode->title),
|
||||
]),
|
||||
'persons' => $episode->persons,
|
||||
]) ?>
|
||||
<?php if (in_array(true, array_column($podcast->fundingPlatforms, 'is_visible'), true)): ?>
|
||||
<?= $this->include('podcast/_partials/funding_links_modal') ?>
|
||||
<?php endif; ?>
|
||||
</body>
|
|
@ -1,17 +1,32 @@
|
|||
<?php declare(strict_types=1);
|
||||
|
||||
$navigationItems = [
|
||||
[
|
||||
'uri' => route_to('episode', esc($podcast->handle), esc($episode->slug)),
|
||||
'label' => lang('Episode.comments'),
|
||||
'labelInfo' => $episode->comments_count,
|
||||
],
|
||||
[
|
||||
'uri' => route_to('episode-activity', esc($podcast->handle), esc($episode->slug)),
|
||||
'label' => lang('Episode.activity'),
|
||||
'labelInfo' => $episode->posts_count,
|
||||
],
|
||||
]
|
||||
if ($episode->publication_status === 'published') {
|
||||
$navigationItems = [
|
||||
[
|
||||
'uri' => route_to('episode', esc($podcast->handle), esc($episode->slug)),
|
||||
'label' => lang('Episode.comments'),
|
||||
'labelInfo' => $episode->comments_count,
|
||||
],
|
||||
[
|
||||
'uri' => route_to('episode-activity', esc($podcast->handle), esc($episode->slug)),
|
||||
'label' => lang('Episode.activity'),
|
||||
'labelInfo' => $episode->posts_count,
|
||||
],
|
||||
];
|
||||
} else {
|
||||
$navigationItems = [
|
||||
[
|
||||
'uri' => route_to('episode-preview', $episode->preview_id),
|
||||
'label' => lang('Episode.comments'),
|
||||
'labelInfo' => $episode->comments_count,
|
||||
],
|
||||
[
|
||||
'uri' => route_to('episode-preview-activity', $episode->preview_id),
|
||||
'label' => lang('Episode.activity'),
|
||||
'labelInfo' => $episode->posts_count,
|
||||
],
|
||||
];
|
||||
}
|
||||
?>
|
||||
<nav class="sticky z-40 flex col-start-2 pt-4 shadow bg-elevated md:px-8 gap-x-2 md:gap-x-4 -top-4 rounded-conditional-b-xl">
|
||||
<?php foreach ($navigationItems as $item): ?>
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
<?= $this->extend('episode/_layout-preview') ?>
|
||||
|
||||
<?= $this->section('content') ?>
|
||||
|
||||
<div class="flex flex-col gap-y-4">
|
||||
<?php foreach ($episode->posts as $key => $post): ?>
|
||||
<?= view('post/_partials/card', [
|
||||
'index' => $key,
|
||||
'post' => $post,
|
||||
'podcast' => $podcast,
|
||||
]) ?>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<?= $this->endSection() ?>
|
|
@ -0,0 +1,14 @@
|
|||
<?= $this->extend('episode/_layout-preview') ?>
|
||||
|
||||
<?= $this->section('content') ?>
|
||||
|
||||
<div class="flex flex-col gap-y-2">
|
||||
<?php foreach ($episode->comments as $comment): ?>
|
||||
<?= view('episode/_partials/comment', [
|
||||
'comment' => $comment,
|
||||
'podcast' => $podcast,
|
||||
]) ?>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<?= $this->endSection() ?>
|
Loading…
Reference in New Issue