feat: add publish feature for podcasts and set draft by default

closes #128, #220
This commit is contained in:
Ola Hneini 2022-07-05 16:39:20 +00:00 committed by Yassine Doghri
parent 9843ce3882
commit 3d363f2efe
25 changed files with 909 additions and 109 deletions

View File

@ -177,6 +177,10 @@ class AddPodcasts extends Migration
'type' => 'INT',
'unsigned' => true,
],
'published_at' => [
'type' => 'DATETIME',
'null' => true,
],
'created_at' => [
'type' => 'DATETIME',
],

View File

@ -168,7 +168,7 @@ class AuthSeeder extends Seeder
[
'name' => 'manage_publications',
'description' =>
'Publish / unpublish episodes & posts of a podcast',
'Publish a podcast and publish / unpublish its episodes & posts',
'has_permission' => ['podcast_admin'],
],
[

View File

@ -541,6 +541,8 @@ class Episode extends Entity
if ($this->publication_status === null) {
if ($this->published_at === null) {
$this->publication_status = 'not_published';
} elseif ($this->getPodcast()->publication_status !== 'published') {
$this->publication_status = 'with_podcast';
} elseif ($this->published_at->isBefore(Time::now())) {
$this->publication_status = 'published';
} else {

View File

@ -79,6 +79,8 @@ use RuntimeException;
* @property string|null $partner_image_url
* @property int $created_by
* @property int $updated_by
* @property string $publication_status;
* @property Time|null $published_at;
* @property Time $created_at;
* @property Time $updated_at;
*
@ -147,6 +149,13 @@ class Podcast extends Entity
protected string $custom_rss_string;
protected ?string $publication_status = null;
/**
* @var string[]
*/
protected $dates = ['published_at', 'created_at', 'updated_at'];
/**
* @var array<string, string>
*/
@ -459,6 +468,21 @@ class Podcast extends Entity
return $this->description;
}
public function getPublicationStatus(): string
{
if ($this->publication_status === null) {
if ($this->published_at === null) {
$this->publication_status = 'not_published';
} elseif ($this->published_at->isBefore(Time::now())) {
$this->publication_status = 'published';
} else {
$this->publication_status = 'scheduled';
}
}
return $this->publication_status;
}
/**
* Returns the podcast's podcasting platform links
*

View File

@ -116,18 +116,27 @@ if (! function_exists('publication_pill')) {
$class = match ($publicationStatus) {
'published' => 'text-pine-500 border-pine-500 bg-pine-50',
'scheduled' => 'text-red-600 border-red-600 bg-red-50',
'with_podcast' => 'text-blue-600 border-blue-600 bg-blue-50',
'not_published' => 'text-gray-600 border-gray-600 bg-gray-50',
default => 'text-gray-600 border-gray-600 bg-gray-50',
};
$title = match ($publicationStatus) {
'published', 'scheduled' => (string) $publicationDate,
'with_podcast' => lang('Episode.with_podcast_hint'),
'not_published' => '',
default => '',
};
$label = lang('Episode.publication_status.' . $publicationStatus);
return '<span ' . ($publicationDate === null ? '' : 'title="' . $publicationDate . '"') . ' class="px-1 font-semibold border rounded ' .
return '<span ' . ($title === '' ? '' : 'title="' . $title . '"') . ' class="flex items-center px-1 font-semibold border rounded w-max ' .
$class .
' ' .
$customClass .
'">' .
$label .
($publicationStatus === 'with_podcast' ? '<Icon glyph="warning" class="flex-shrink-0 ml-1 text-lg" />' : '') .
'</span>';
}
}
@ -136,7 +145,7 @@ if (! function_exists('publication_pill')) {
if (! function_exists('publication_button')) {
/**
* Publication button component
* Publication button component for episodes
*
* Displays the appropriate publication button depending on the publication post.
*/
@ -149,6 +158,7 @@ if (! function_exists('publication_button')) {
$variant = 'primary';
$iconLeft = 'upload-cloud';
break;
case 'with_podcast':
case 'scheduled':
$label = lang('Episode.publish_edit');
$route = route_to('episode-publish_edit', $podcastId, $episodeId);
@ -177,6 +187,51 @@ if (! function_exists('publication_button')) {
// ------------------------------------------------------------------------
if (! function_exists('publication_status_banner')) {
/**
* Publication status banner component for podcasts
*
* Displays the appropriate banner depending on the podcast's publication status.
*/
function publication_status_banner(?Time $publicationDate, int $podcastId, string $publicationStatus): string
{
switch ($publicationStatus) {
case 'not_published':
$bannerDisclaimer = lang('Podcast.publication_status_banner.draft_mode');
$bannerText = lang('Podcast.publication_status_banner.not_published');
$linkRoute = route_to('podcast-publish', $podcastId);
$linkLabel = lang('Podcast.publish');
break;
case 'scheduled':
$bannerDisclaimer = lang('Podcast.publication_status_banner.draft_mode');
$bannerText = lang('Podcast.publication_status_banner.scheduled', [
'publication_date' => local_time($publicationDate),
], null, false);
$linkRoute = route_to('podcast-publish_edit', $podcastId);
$linkLabel = lang('Podcast.publish_edit');
break;
default:
$bannerDisclaimer = '';
$bannerText = '';
$linkRoute = '';
$linkLabel = '';
break;
}
return <<<CODE_SAMPLE
<div class="flex items-center px-12 py-1 border-b bg-stripes-gray border-subtle" role="alert">
<p class="text-gray-900">
<span class="text-xs font-semibold tracking-wide uppercase">{$bannerDisclaimer}</span>
<span class="ml-3 text-sm">{$bannerText}</span>
</p>
<a href="{$linkRoute}" class="ml-1 text-sm font-semibold underline shadow-xs text-accent-base hover:text-accent-hover hover:no-underline">{$linkLabel}</a>
</div>
CODE_SAMPLE;
}
}
// ------------------------------------------------------------------------
if (! function_exists('episode_numbering')) {
/**
* Returns relevant translated episode numbering.

View File

@ -8,6 +8,8 @@ declare(strict_types=1);
* @link https://castopod.org/
*/
use CodeIgniter\I18n\Time;
if (! function_exists('get_browser_language')) {
/**
* Gets the browser default language using the request header key `HTTP_ACCEPT_LANGUAGE`
@ -292,3 +294,28 @@ if (! function_exists('format_bytes')) {
return round($bytes, $precision) . $units[$pow];
}
}
if (! function_exists('local_time')) {
function local_time(Time $time): string
{
$formatter = new IntlDateFormatter(service(
'request'
)->getLocale(), IntlDateFormatter::MEDIUM, IntlDateFormatter::LONG);
$translatedDate = $time->toLocalizedString($formatter->getPattern());
$datetime = $time->format(DateTime::ISO8601);
return <<<CODE_SAMPLE
<local-time datetime="{$datetime}"
weekday="long"
month="long"
day="numeric"
year="numeric"
hour="numeric"
minute="numeric">
<time
datetime="{$datetime}"
title="{$time}">{$translatedDate}</time>
</local-time>
CODE_SAMPLE;
}
}

View File

@ -142,7 +142,7 @@ class EpisodeModel extends Model
->join('podcasts', 'podcasts.id = episodes.podcast_id')
->where('slug', $episodeSlug)
->where('podcasts.handle', $podcastHandle)
->where('`published_at` <= UTC_TIMESTAMP()', null, false)
->where('`' . $this->db->getPrefix() . 'episodes`.`published_at` <= UTC_TIMESTAMP()', null, false)
->first();
cache()

View File

@ -64,6 +64,7 @@ class PodcastModel extends Model
'partner_id',
'partner_link_url',
'partner_image_url',
'published_at',
'created_by',
'updated_by',
];
@ -92,6 +93,7 @@ class PodcastModel extends Model
'owner_email' => 'required|valid_email',
'new_feed_url' => 'valid_url_strict|permit_empty',
'type' => 'required',
'published_at' => 'valid_date|permit_empty',
'created_by' => 'required',
'updated_by' => 'required',
];
@ -128,6 +130,7 @@ class PodcastModel extends Model
$cacheName = "podcast-{$podcastHandle}";
if (! ($found = cache($cacheName))) {
$found = $this->where('handle', $podcastHandle)
->where('`published_at` <= UTC_TIMESTAMP()', null, false)
->first();
cache()
->save("podcast-{$podcastHandle}", $found, DECADE);
@ -168,9 +171,9 @@ class PodcastModel extends Model
*/
public function getAllPodcasts(string $orderBy = null): array
{
if ($orderBy === 'activity') {
$prefix = $this->db->getPrefix();
$prefix = $this->db->getPrefix();
if ($orderBy === 'activity') {
$fediverseTablePrefix = $prefix . config('Fediverse')
->tablesPrefix;
$this->builder()
@ -195,7 +198,7 @@ class PodcastModel extends Model
$this->orderBy('created_at', 'ASC');
}
return $this->findAll();
return $this->where('`' . $prefix . 'podcasts`.`published_at` <= UTC_TIMESTAMP()', null, false)->findAll();
}
/**

View 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="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm-1-7v2h2v-2h-2zm0-8v6h2V7h-2z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 254 B

View File

@ -42,4 +42,14 @@
hsla(0 0% 0% / 0.8) 100%
);
}
.bg-stripes-gray {
background-image: repeating-linear-gradient(
-45deg,
#f3f4f6,
#f3f4f6 10px,
#e5e7eb 10px,
#e5e7eb 20px
);
}
}

View File

@ -119,6 +119,49 @@ $routes->group(
$routes->post('edit', 'PodcastController::attemptEdit/$1', [
'filter' => 'permission:podcast-edit',
]);
$routes->get(
'publish',
'PodcastController::publish/$1',
[
'as' => 'podcast-publish',
'filter' =>
'permission:podcast-manage_publications',
],
);
$routes->post(
'publish',
'PodcastController::attemptPublish/$1',
[
'filter' =>
'permission:podcast-manage_publications',
],
);
$routes->get(
'publish-edit',
'PodcastController::publishEdit/$1',
[
'as' => 'podcast-publish_edit',
'filter' =>
'permission:podcast-manage_publications',
],
);
$routes->post(
'publish-edit',
'PodcastController::attemptPublishEdit/$1',
[
'filter' =>
'permission:podcast-manage_publications',
],
);
$routes->get(
'publish-cancel',
'PodcastController::publishCancel/$1',
[
'as' => 'podcast-publish-cancel',
'filter' =>
'permission:podcast-manage_publications',
],
);
$routes->get('edit/delete-banner', 'PodcastController::deleteBanner/$1', [
'as' => 'podcast-banner-delete',
'filter' => 'permission:podcast-edit',

View File

@ -440,17 +440,19 @@ class EpisodeController extends BaseController
public function attemptPublish(): RedirectResponse
{
$rules = [
'publication_method' => 'required',
'scheduled_publication_date' =>
'valid_date[Y-m-d H:i]|permit_empty',
];
if ($this->podcast->publication_status === 'published') {
$rules = [
'publication_method' => 'required',
'scheduled_publication_date' =>
'valid_date[Y-m-d H:i]|permit_empty',
];
if (! $this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $this->validator->getErrors());
if (! $this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $this->validator->getErrors());
}
}
$db = db_connect();
@ -463,22 +465,29 @@ class EpisodeController extends BaseController
'created_by' => user_id(),
]);
$publishMethod = $this->request->getPost('publication_method');
if ($publishMethod === 'schedule') {
$scheduledPublicationDate = $this->request->getPost('scheduled_publication_date');
if ($scheduledPublicationDate) {
$this->episode->published_at = Time::createFromFormat(
'Y-m-d H:i',
$scheduledPublicationDate,
$this->request->getPost('client_timezone'),
)->setTimezone(app_timezone());
if ($this->podcast->publication_status === 'published') {
$publishMethod = $this->request->getPost('publication_method');
if ($publishMethod === 'schedule') {
$scheduledPublicationDate = $this->request->getPost('scheduled_publication_date');
if ($scheduledPublicationDate) {
$this->episode->published_at = Time::createFromFormat(
'Y-m-d H:i',
$scheduledPublicationDate,
$this->request->getPost('client_timezone'),
)->setTimezone(app_timezone());
} else {
$db->transRollback();
return redirect()
->back()
->withInput()
->with('error', lang('Episode.messages.scheduleDateError'));
}
} else {
$db->transRollback();
return redirect()
->back()
->withInput()
->with('error', 'Schedule date must be set!');
$this->episode->published_at = Time::now();
}
} elseif ($this->podcast->publication_status === 'scheduled') {
// podcast publication date has already been set
$this->episode->published_at = $this->podcast->published_at->addSeconds(1);
} else {
$this->episode->published_at = Time::now();
}
@ -505,12 +514,17 @@ class EpisodeController extends BaseController
$db->transComplete();
return redirect()->route('episode-view', [$this->podcast->id, $this->episode->id]);
return redirect()->route('episode-view', [$this->podcast->id, $this->episode->id])->with(
'message',
lang('Episode.messages.publishSuccess', [
'publication_status' => $this->episode->publication_status,
])
);
}
public function publishEdit(): string | RedirectResponse
{
if ($this->episode->publication_status === 'scheduled') {
if (in_array($this->episode->publication_status, ['scheduled', 'with_podcast'], true)) {
helper(['form']);
$data = [
@ -539,39 +553,48 @@ class EpisodeController extends BaseController
public function attemptPublishEdit(): RedirectResponse
{
$rules = [
'post_id' => 'required',
'publication_method' => 'required',
'scheduled_publication_date' =>
'valid_date[Y-m-d H:i]|permit_empty',
];
if ($this->podcast->publication_status === 'published') {
$rules = [
'post_id' => 'required',
'publication_method' => 'required',
'scheduled_publication_date' =>
'valid_date[Y-m-d H:i]|permit_empty',
];
if (! $this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $this->validator->getErrors());
if (! $this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $this->validator->getErrors());
}
}
$db = db_connect();
$db->transStart();
$publishMethod = $this->request->getPost('publication_method');
if ($publishMethod === 'schedule') {
$scheduledPublicationDate = $this->request->getPost('scheduled_publication_date');
if ($scheduledPublicationDate) {
$this->episode->published_at = Time::createFromFormat(
'Y-m-d H:i',
$scheduledPublicationDate,
$this->request->getPost('client_timezone'),
)->setTimezone(app_timezone());
if ($this->podcast->publication_status === 'published') {
$publishMethod = $this->request->getPost('publication_method');
if ($publishMethod === 'schedule') {
$scheduledPublicationDate = $this->request->getPost('scheduled_publication_date');
if ($scheduledPublicationDate) {
$this->episode->published_at = Time::createFromFormat(
'Y-m-d H:i',
$scheduledPublicationDate,
$this->request->getPost('client_timezone'),
)->setTimezone(app_timezone());
} else {
$db->transRollback();
return redirect()
->back()
->withInput()
->with('error', lang('Episode.messages.scheduleDateError'));
}
} else {
$db->transRollback();
return redirect()
->back()
->withInput()
->with('error', 'Schedule date must be set!');
$this->episode->published_at = Time::now();
}
} elseif ($this->podcast->publication_status === 'scheduled') {
// podcast publication date has already been set
$this->episode->published_at = $this->podcast->published_at->addSeconds(1);
} else {
$this->episode->published_at = Time::now();
}
@ -603,12 +626,17 @@ class EpisodeController extends BaseController
$db->transComplete();
return redirect()->route('episode-view', [$this->podcast->id, $this->episode->id]);
return redirect()->route('episode-view', [$this->podcast->id, $this->episode->id])->with(
'message',
lang('Episode.messages.publishSuccess', [
'publication_status' => $this->episode->publication_status,
])
);
}
public function publishCancel(): RedirectResponse
{
if ($this->episode->publication_status === 'scheduled') {
if (in_array($this->episode->publication_status, ['scheduled', 'with_podcast'], true)) {
$db = db_connect();
$db->transStart();
@ -634,13 +662,13 @@ class EpisodeController extends BaseController
$db->transComplete();
return redirect()->route('episode-view', [$this->podcast->id, $this->episode->id]);
return redirect()->route('episode-view', [$this->podcast->id, $this->episode->id])->with(
'message',
lang('Episode.messages.publishCancelSuccess')
);
}
return redirect()->route('episode-view', [$this->podcast->id, $this->episode->id])->with(
'message',
lang('Episode.messages.publishCancelSuccess')
);
return redirect()->route('episode-view', [$this->podcast->id, $this->episode->id]);
}
public function unpublish(): string | RedirectResponse

View File

@ -12,14 +12,17 @@ namespace Modules\Admin\Controllers;
use App\Entities\Location;
use App\Entities\Podcast;
use App\Entities\Post;
use App\Models\ActorModel;
use App\Models\CategoryModel;
use App\Models\EpisodeModel;
use App\Models\LanguageModel;
use App\Models\MediaModel;
use App\Models\PodcastModel;
use App\Models\PostModel;
use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\RedirectResponse;
use CodeIgniter\I18n\Time;
use Config\Services;
use Modules\Analytics\Models\AnalyticsPodcastByCountryModel;
use Modules\Analytics\Models\AnalyticsPodcastByEpisodeModel;
@ -237,6 +240,7 @@ class PodcastController extends BaseController
'is_locked' => $this->request->getPost('lock') === 'yes',
'created_by' => user_id(),
'updated_by' => user_id(),
'published_at' => null,
]);
$podcastModel = new PodcastModel();
@ -604,4 +608,361 @@ class PodcastController extends BaseController
'podcast_handle' => $this->podcast->handle,
]));
}
public function publish(): string | RedirectResponse
{
helper(['form']);
$data = [
'podcast' => $this->podcast,
];
replace_breadcrumb_params([
0 => $this->podcast->title,
]);
return view('podcast/publish', $data);
}
public function attemptPublish(): RedirectResponse
{
if ($this->podcast->publication_status !== 'not_published') {
return redirect()->route('podcast-view', [$this->podcast->id])->with(
'error',
lang('Podcast.messages.publishError')
);
}
$rules = [
'publication_method' => 'required',
'scheduled_publication_date' =>
'valid_date[Y-m-d H:i]|permit_empty',
];
if (! $this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $this->validator->getErrors());
}
$db = db_connect();
$db->transStart();
$publishMethod = $this->request->getPost('publication_method');
if ($publishMethod === 'schedule') {
$scheduledPublicationDate = $this->request->getPost('scheduled_publication_date');
if ($scheduledPublicationDate) {
$this->podcast->published_at = Time::createFromFormat(
'Y-m-d H:i',
$scheduledPublicationDate,
$this->request->getPost('client_timezone'),
)->setTimezone(app_timezone());
} else {
$db->transRollback();
return redirect()
->back()
->withInput()
->with('error', lang('Podcast.messages.scheduleDateError'));
}
} else {
$this->podcast->published_at = Time::now();
}
$message = $this->request->getPost('message');
// only create post if message is not empty
if ($message !== '') {
$newPost = new Post([
'actor_id' => $this->podcast->actor_id,
'message' => $message,
'created_by' => user_id(),
]);
$newPost->published_at = $this->podcast->published_at;
$postModel = new PostModel();
if (! $postModel->addPost($newPost)) {
$db->transRollback();
return redirect()
->back()
->withInput()
->with('errors', $postModel->errors());
}
}
$episodes = (new EpisodeModel())
->where('podcast_id', $this->podcast->id)
->where('published_at !=', null)
->findAll();
foreach ($episodes as $episode) {
$episode->published_at = $this->podcast->published_at->addSeconds(1);
$episodeModel = new EpisodeModel();
if (! $episodeModel->update($episode->id, $episode)) {
$db->transRollback();
return redirect()
->back()
->withInput()
->with('errors', $episodeModel->errors());
}
$post = (new PostModel())->where('episode_id', $episode->id)
->first();
if ($post !== null) {
$post->published_at = $episode->published_at;
$postModel = new PostModel();
if (! $postModel->update($post->id, $post)) {
$db->transRollback();
return redirect()
->back()
->withInput()
->with('errors', $postModel->errors());
}
}
}
$podcastModel = new PodcastModel();
if (! $podcastModel->update($this->podcast->id, $this->podcast)) {
$db->transRollback();
return redirect()
->back()
->withInput()
->with('errors', $podcastModel->errors());
}
$db->transComplete();
return redirect()->route('podcast-view', [$this->podcast->id]);
}
public function publishEdit(): string | RedirectResponse
{
helper(['form']);
$data = [
'podcast' => $this->podcast,
'post' => (new PostModel())
->where([
'actor_id' => $this->podcast->actor_id,
'episode_id' => null,
])
->first(),
];
replace_breadcrumb_params([
0 => $this->podcast->title,
]);
return view('podcast/publish_edit', $data);
}
public function attemptPublishEdit(): RedirectResponse
{
if ($this->podcast->publication_status !== 'scheduled') {
return redirect()->route('podcast-view', [$this->podcast->id])->with(
'error',
lang('Podcast.messages.publishEditError')
);
}
$rules = [
'publication_method' => 'required',
'scheduled_publication_date' =>
'valid_date[Y-m-d H:i]|permit_empty',
];
if (! $this->validate($rules)) {
return redirect()
->back()
->withInput()
->with('errors', $this->validator->getErrors());
}
$db = db_connect();
$db->transStart();
$publishMethod = $this->request->getPost('publication_method');
if ($publishMethod === 'schedule') {
$scheduledPublicationDate = $this->request->getPost('scheduled_publication_date');
if ($scheduledPublicationDate) {
$this->podcast->published_at = Time::createFromFormat(
'Y-m-d H:i',
$scheduledPublicationDate,
$this->request->getPost('client_timezone'),
)->setTimezone(app_timezone());
} else {
$db->transRollback();
return redirect()
->back()
->withInput()
->with('error', lang('Podcast.messages.scheduleDateError'));
}
} else {
$this->podcast->published_at = Time::now();
}
$post = (new PostModel())
->where([
'actor_id' => $this->podcast->actor_id,
'episode_id' => null,
])
->first();
$newPostMessage = $this->request->getPost('message');
if ($post !== null) {
if ($newPostMessage !== '') {
// edit post if post exists and message is not empty
$post->message = $newPostMessage;
$post->published_at = $this->podcast->published_at;
$postModel = new PostModel();
if (! $postModel->editPost($post)) {
$db->transRollback();
return redirect()
->back()
->withInput()
->with('errors', $postModel->errors());
}
} else {
// remove post if post exists and message is empty
$postModel = new PostModel();
$post = $postModel
->where([
'actor_id' => $this->podcast->actor_id,
'episode_id' => null,
])
->first();
$postModel->removePost($post);
}
} elseif ($newPostMessage !== '') {
// create post if there is no post and message is not empty
$newPost = new Post([
'actor_id' => $this->podcast->actor_id,
'message' => $newPostMessage,
'created_by' => user_id(),
]);
$newPost->published_at = $this->podcast->published_at;
$postModel = new PostModel();
if (! $postModel->addPost($newPost)) {
$db->transRollback();
return redirect()
->back()
->withInput()
->with('errors', $postModel->errors());
}
}
$episodes = (new EpisodeModel())
->where('podcast_id', $this->podcast->id)
->where('published_at !=', null)
->findAll();
foreach ($episodes as $episode) {
$episode->published_at = $this->podcast->published_at->addSeconds(1);
$episodeModel = new EpisodeModel();
if (! $episodeModel->update($episode->id, $episode)) {
$db->transRollback();
return redirect()
->back()
->withInput()
->with('errors', $episodeModel->errors());
}
$post = (new PostModel())->where('episode_id', $episode->id)
->first();
if ($post !== null) {
$post->published_at = $episode->published_at;
$postModel = new PostModel();
if (! $postModel->update($post->id, $post)) {
$db->transRollback();
return redirect()
->back()
->withInput()
->with('errors', $postModel->errors());
}
}
}
$podcastModel = new PodcastModel();
if (! $podcastModel->update($this->podcast->id, $this->podcast)) {
$db->transRollback();
return redirect()
->back()
->withInput()
->with('errors', $podcastModel->errors());
}
$db->transComplete();
return redirect()->route('podcast-view', [$this->podcast->id]);
}
public function publishCancel(): RedirectResponse
{
if ($this->podcast->publication_status !== 'scheduled') {
return redirect()->route('podcast-view', [$this->podcast->id]);
}
$db = db_connect();
$db->transStart();
$postModel = new PostModel();
$post = $postModel
->where([
'actor_id' => $this->podcast->actor_id,
'episode_id' => null,
])
->first();
if ($post !== null) {
$postModel->removePost($post);
}
$episodes = (new EpisodeModel())
->where('podcast_id', $this->podcast->id)
->where('published_at !=', null)
->findAll();
foreach ($episodes as $episode) {
$episode->published_at = null;
$episodeModel = new EpisodeModel();
if (! $episodeModel->update($episode->id, $episode)) {
$db->transRollback();
return redirect()
->back()
->withInput()
->with('errors', $episodeModel->errors());
}
$postModel = new PostModel();
$post = $postModel->where('episode_id', $episode->id)
->first();
$postModel->removePost($post);
}
$this->podcast->published_at = null;
$podcastModel = new PodcastModel();
if (! $podcastModel->update($this->podcast->id, $this->podcast)) {
$db->transRollback();
return redirect()
->back()
->withInput()
->with('errors', $podcastModel->errors());
}
$db->transComplete();
return redirect()->route('podcast-view', [$this->podcast->id])->with(
'message',
lang('Podcast.messages.publishCancelSuccess')
);
}
}

View File

@ -450,12 +450,27 @@ class PodcastImportController extends BaseController
->with('errors', $episodePersonModel->errors());
}
}
if ($itemNumber === 1) {
$firstEpisodePublicationDate = strtotime((string) $item->pubDate);
}
}
// set interact as the newly imported podcast actor
$importedPodcast = (new PodcastModel())->getPodcastById($newPodcastId);
set_interact_as_actor($importedPodcast->actor_id);
// set podcast publication date
$importedPodcast->published_at = $firstEpisodePublicationDate ?? $importedPodcast->created_at;
$podcastModel = new PodcastModel();
if (! $podcastModel->update($importedPodcast->id, $importedPodcast)) {
$db->transRollback();
return redirect()
->back()
->withInput()
->with('errors', $podcastModel->errors());
}
$db->transComplete();
return redirect()->route('podcast-view', [$newPodcastId]);

View File

@ -34,9 +34,11 @@ return [
'create' => 'Add an episode',
'publication_status' => [
'published' => 'Published',
'with_podcast' => 'Published',
'scheduled' => 'Scheduled',
'not_published' => 'Not published',
],
'with_podcast_hint' => 'To be published at the same time as the podcast',
'list' => [
'search' => [
'placeholder' => 'Search for an episode',
@ -55,8 +57,15 @@ return [
'messages' => [
'createSuccess' => 'Episode has been successfully created!',
'editSuccess' => 'Episode has been successfully updated!',
'publishSuccess' => '{publication_status, select,
published {Episode successfully published!}
scheduled {Episode publication successfully scheduled!}
with_podcast {This episode will be published at the same time as the podcast.}
other {This episode is not published.}
}',
'publishCancelSuccess' => 'Episode publication successfully cancelled!',
'unpublishBeforeDeleteTip' => 'You must unpublish the episode before deleting it.',
'scheduleDateError' => 'Schedule date must be set!',
'deletePublishedEpisodeError' => 'Please unpublish the episode before deleting it.',
'deleteSuccess' => 'Episode successfully deleted!',
'deleteError' => 'Failed to delete episode {type, select,
@ -138,9 +147,9 @@ return [
'If you need RSS tags that Castopod does not handle, set them here.',
'custom_rss' => 'Custom RSS tags for the episode',
'custom_rss_hint' => 'This will be injected within the ❬item❭ tag.',
'block' => 'Episode should be hidden from all platforms',
'block' => 'Episode should be hidden from public catalogues',
'block_hint' =>
'The episode show or hide post. If you want this episode removed from the Apple directory, toggle this on.',
'The episode show or hide status: toggling this on prevents the episode from appearing in Apple Podcasts, Google Podcasts, and any third party apps that pull shows from these directories. (Not guaranteed)',
'submit_create' => 'Create episode',
'submit_edit' => 'Save episode',
],
@ -154,6 +163,7 @@ return [
'publication_method' => [
'now' => 'Now',
'schedule' => 'Schedule',
'with_podcast' => 'Publish alongside podcast',
],
'scheduled_publication_date' => 'Scheduled publication date',
'scheduled_publication_date_clear' => 'Clear publication date',

View File

@ -16,14 +16,17 @@ return [
'new_episode' => 'New Episode',
'view' => 'View podcast',
'edit' => 'Edit podcast',
'publish' => 'Publish podcast',
'publish_edit' => 'Edit publication',
'delete' => 'Delete podcast',
'see_episodes' => 'See episodes',
'see_contributors' => 'See contributors',
'go_to_page' => 'Go to page',
'latest_episodes' => 'Latest episodes',
'see_all_episodes' => 'See all episodes',
'draft' => 'Draft',
'messages' => [
'createSuccess' => 'Podcast has been successfully created!',
'createSuccess' => 'Podcast successfully created!',
'editSuccess' => 'Podcast has been successfully updated!',
'importSuccess' => 'Podcast has been successfully imported!',
'deleteSuccess' => 'Podcast @{podcast_handle} successfully deleted!',
@ -46,6 +49,10 @@ return [
} added to the podcast!',
'podcastFeedUpToDate' => 'Podcast is already up to date.',
'podcastNotImported' => 'Podcast could not be updated as it was not imported.',
'publishError' => 'This podcast is either already published or scheduled for publication.',
'publishEditError' => 'This podcast is not scheduled for publication.',
'publishCancelSuccess' => 'Podcast publication successfully cancelled!',
'scheduleDateError' => 'Schedule date must be set!',
],
'form' => [
'identity_section_title' => 'Podcast identity',
@ -121,7 +128,9 @@ return [
'partner_link_url_hint' => 'The generic partner link address',
'partner_image_url_hint' => 'The generic partner image address',
'status_section_title' => 'Status',
'block' => 'Podcast should be hidden from all platforms',
'block' => 'Podcast should be hidden from public catalogues',
'block_hint' =>
'The podcast show or hide status: toggling this on prevents the entire podcast from appearing in Apple Podcasts, Google Podcasts, and any third party apps that pull shows from these directories. (Not guaranteed)',
'complete' => 'Podcast will not be having new episodes',
'lock' => 'Prevent podcast from being copied',
'lock_hint' =>
@ -242,6 +251,32 @@ return [
'film_reviews' => 'Film Reviews',
'tv_reviews' => 'TV Reviews',
],
'publish_form' => [
'back_to_podcast_dashboard' => 'Back to podcast dashboard',
'post' => 'Your announcement post',
'post_hint' =>
"Write a message to announce the publication of your podcast. The message will be featured in your podcast's homepage.",
'message_placeholder' => 'Write your message…',
'submit' => 'Publish',
'publication_date' => 'Publication date',
'publication_method' => [
'now' => 'Now',
'schedule' => 'Schedule',
],
'scheduled_publication_date' => 'Scheduled publication date',
'scheduled_publication_date_hint' =>
'You can schedule the podcast release by setting a future publication date. This field must be formatted as YYYY-MM-DD HH:mm',
'submit_edit' => 'Edit publication',
'cancel_publication' => 'Cancel publication',
'message_warning' => 'You did not write a message for your announcement post!',
'message_warning_hint' => 'Having a message increases social engagement, resulting in a better visibility for your podcast.',
'message_warning_submit' => 'Publish anyway',
],
'publication_status_banner' => [
'draft_mode' => 'draft mode',
'not_published' => 'This podcast is not yet published.',
'scheduled' => 'This podcast is scheduled for publication on {publication_date}.',
],
'delete_form' => [
'disclaimer' =>
"Deleting the podcast will delete all episodes, media files, posts and analytics associated with it. This action is irreversible, you will not be able to retrieve them afterwards.",

View File

@ -32,6 +32,7 @@ class WebSubController extends Controller
->select('podcasts.*')
->join('episodes', 'podcasts.id = episodes.podcast_id', 'left outer')
->where('podcasts.is_published_on_hubs', false)
->where('`' . $podcastModel->db->getPrefix() . 'podcasts`.`published_at` <= UTC_TIMESTAMP()', null, false)
->orGroupStart()
->where('episodes.is_published_on_hubs', false)
->where('`' . $podcastModel->db->getPrefix() . 'episodes`.`published_at` <= UTC_TIMESTAMP()', null, false)

View File

@ -39,6 +39,9 @@
</div>
</div>
</header>
<?php if (isset($podcast) && $podcast->publication_status !== 'published'): ?>
<?= publication_status_banner($podcast->published_at, $podcast->id, $podcast->publication_status) ?>
<?php endif ?>
<div class="px-2 py-8 mx-auto md:px-12">
<?= view('_message_block') ?>
<?= $this->renderSection('content') ?>

View File

@ -69,28 +69,30 @@
</footer>
</div>
<fieldset class="flex flex-col">
<legend class="text-lg font-semibold"><?= lang(
<?php if ($podcast->publication_status === 'published'): ?>
<fieldset class="flex flex-col">
<legend class="text-lg font-semibold"><?= lang(
'Episode.publish_form.publication_date',
) ?></legend>
<Forms.Radio value="now" name="publication_method" isChecked="<?= old('publication_method') ? old('publish') === 'now' : true ?>"><?= lang('Episode.publish_form.publication_method.now') ?></Forms.Radio>
<div class="inline-flex flex-wrap items-center radio-toggler">
<input
class="w-6 h-6 border-contrast text-accent-base border-3 focus:ring-accent"
type="radio" id="schedule" name="publication_method" value="schedule" <?= old('publication_method') && old('publication_method') === 'schedule' ? 'checked' : '' ?> />
<Label for="schedule" class="pl-2 leading-8"><?= lang('Episode.publish_form.publication_method.schedule') ?></label>
<div class="w-full mt-2 radio-toggler-element">
<Forms.Field
as="DatetimePicker"
name="scheduled_publication_date"
label="<?= lang('Episode.publish_form.scheduled_publication_date') ?>"
hint="<?= lang('Episode.publish_form.scheduled_publication_date_hint') ?>"
value="<?= $episode->published_at ?>"
/>
<Forms.Radio value="now" name="publication_method" isChecked="<?= old('publication_method') ? old('publish') === 'now' : true ?>"><?= lang('Episode.publish_form.publication_method.now') ?></Forms.Radio>
<div class="inline-flex flex-wrap items-center radio-toggler">
<input
class="w-6 h-6 border-contrast text-accent-base border-3 focus:ring-accent"
type="radio" id="schedule" name="publication_method" value="schedule" <?= old('publication_method') && old('publication_method') === 'schedule' ? 'checked' : '' ?> />
<Label for="schedule" class="pl-2 leading-8"><?= lang('Episode.publish_form.publication_method.schedule') ?></label>
<div class="w-full mt-2 radio-toggler-element">
<Forms.Field
as="DatetimePicker"
name="scheduled_publication_date"
label="<?= lang('Episode.publish_form.scheduled_publication_date') ?>"
hint="<?= lang('Episode.publish_form.scheduled_publication_date_hint') ?>"
value="<?= $episode->published_at ?>"
/>
</div>
</div>
</div>
</fieldset>
</fieldset>
<?php endif ?>
<Alert id="publish-warning" variant="warning" glyph="alert" class="hidden mt-2" title="<?= lang('Episode.publish_form.message_warning') ?>"><?= lang('Episode.publish_form.message_warning_hint') ?></Alert>
<div class="flex items-center justify-between w-full mt-4">

View File

@ -73,27 +73,29 @@
</footer>
</div>
<fieldset class="flex flex-col">
<legend class="text-lg font-semibold"><?= lang(
<?php if ($podcast->publication_status === 'published'): ?>
<fieldset class="flex flex-col">
<legend class="text-lg font-semibold"><?= lang(
'Episode.publish_form.publication_date',
) ?></legend>
<Forms.Radio value="now" name="publication_method" isChecked="<?= old('publication_method') && old('publish') === 'now' ?>"><?= lang('Episode.publish_form.publication_method.now') ?></Forms.Radio>
<div class="inline-flex flex-wrap items-center radio-toggler">
<input
class="w-6 h-6 border-contrast text-accent-base border-3 focus:ring-accent"
type="radio" id="schedule" name="publication_method" value="schedule" <?= old('publication_method') ? old('publication_method') === 'schedule' : 'checked' ?> />
<Label for="schedule" class="pl-2 leading-8"><?= lang('Episode.publish_form.publication_method.schedule') ?></label>
<div class="w-full mt-2 radio-toggler-element">
<Forms.Field
as="DatetimePicker"
name="scheduled_publication_date"
label="<?= lang('Episode.publish_form.scheduled_publication_date') ?>"
hint="<?= lang('Episode.publish_form.scheduled_publication_date_hint') ?>"
value="<?= $episode->published_at ?>"
/>
<Forms.Radio value="now" name="publication_method" isChecked="<?= old('publication_method') && old('publish') === 'now' ?>"><?= lang('Episode.publish_form.publication_method.now') ?></Forms.Radio>
<div class="inline-flex flex-wrap items-center radio-toggler">
<input
class="w-6 h-6 border-contrast text-accent-base border-3 focus:ring-accent"
type="radio" id="schedule" name="publication_method" value="schedule" <?= old('publication_method') ? old('publication_method') === 'schedule' : 'checked' ?> />
<Label for="schedule" class="pl-2 leading-8"><?= lang('Episode.publish_form.publication_method.schedule') ?></label>
<div class="w-full mt-2 radio-toggler-element">
<Forms.Field
as="DatetimePicker"
name="scheduled_publication_date"
label="<?= lang('Episode.publish_form.scheduled_publication_date') ?>"
hint="<?= lang('Episode.publish_form.scheduled_publication_date_hint') ?>"
value="<?= $episode->published_at ?>"
/>
</div>
</div>
</div>
</fieldset>
</fieldset>
<?php endif ?>
<Alert id="publish-warning" variant="warning" glyph="alert" class="hidden mt-2" title="<?= lang('Episode.publish_form.message_warning') ?>"><?= lang('Episode.publish_form.message_warning_hint') ?></Alert>

View File

@ -1,11 +1,19 @@
<article class="relative h-full overflow-hidden transition shadow bg-elevated border-3 border-subtle group rounded-xl hover:shadow-xl focus-within:shadow-xl focus-within:ring-accent">
<a href="<?= route_to('podcast-view', $podcast->id) ?>" class="flex flex-col justify-end w-full h-full text-white group">
<div class="absolute bottom-0 left-0 z-10 w-full h-full backdrop-gradient mix-blend-multiply"></div>
<div class="w-full h-full overflow-hidden bg-header">
<div class="<?= 'w-full h-full overflow-hidden bg-header' . ($podcast->publication_status !== 'published' ? ' grayscale group-hover:grayscale-[60%]' : '') ?>">
<img
alt="<?= esc($podcast->title) ?>"
src="<?= $podcast->cover->medium_url ?>" class="object-cover w-full h-full transition duration-200 ease-in-out transform aspect-square group-focus:scale-105 group-hover:scale-105" loading="lazy" />
</div>
<?php if ($podcast->publication_status !== 'published'): ?>
<span class="absolute top-0 left-0 flex items-center px-1 mt-2 ml-2 text-sm font-semibold text-gray-600 border border-gray-600 rounded bg-gray-50">
<?= lang('Podcast.draft') ?>
<?php if ($podcast->publication_status === 'scheduled'): ?>
<Icon glyph="timer" class="flex-shrink-0 ml-1 text-lg" />
<?php endif ?>
</span>
<?php endif ?>
<div class="absolute z-20 w-full px-4 pb-4 transition duration-75 ease-out translate-y-6 group-focus:translate-y-0 group-hover:translate-y-0">
<h2 class="font-bold leading-none truncate font-display"><?= esc($podcast->title) ?></h2>
<p class="text-sm transition duration-150 opacity-0 group-focus:opacity-100 group-hover:opacity-100">@<?= esc($podcast->handle) ?></p>

View File

@ -204,7 +204,7 @@
<Forms.Toggler class="mb-2" name="lock" value="yes" checked="true" hint="<?= lang('Podcast.form.lock_hint') ?>">
<?= lang('Podcast.form.lock') ?>
</Forms.Toggler>
<Forms.Toggler class="mb-2" name="block" value="yes" checked="false">
<Forms.Toggler class="mb-2" name="block" value="yes" checked="false" hint="<?= lang('Podcast.form.block_hint') ?>">
<?= lang('Podcast.form.block') ?>
</Forms.Toggler>
<Forms.Toggler name="complete" value="yes" checked="false">

View File

@ -244,7 +244,7 @@
<Forms.Toggler class="mb-2" name="lock" value="yes" checked="<?= $podcast->is_locked ? 'true' : 'false' ?>" hint="<?= lang('Podcast.form.lock_hint') ?>">
<?= lang('Podcast.form.lock') ?>
</Forms.Toggler>
<Forms.Toggler class="mb-2" name="block" value="yes" checked="<?= $podcast->is_blocked ? 'true' : 'false' ?>">
<Forms.Toggler class="mb-2" name="block" value="yes" checked="<?= $podcast->is_blocked ? 'true' : 'false' ?>" hint="<?= lang('Podcast.form.block_hint') ?>">
<?= lang('Podcast.form.block') ?>
</Forms.Toggler>
<Forms.Toggler name="complete" value="yes" checked="<?= $podcast->is_completed ? 'true' : 'false' ?>">

View File

@ -0,0 +1,80 @@
<?= $this->extend('_layout') ?>
<?= $this->section('title') ?>
<?= lang('Podcast.publish') ?>
<?= $this->endSection() ?>
<?= $this->section('pageTitle') ?>
<?= lang('Podcast.publish') ?>
<?= $this->endSection() ?>
<?= $this->section('content') ?>
<?= anchor(
route_to('podcast-view', $podcast->id),
icon('arrow-left', 'mr-2 text-lg') . lang('Podcast.publish_form.back_to_podcast_dashboard'),
[
'class' => 'inline-flex items-center font-semibold mr-4 text-sm focus:ring-accent',
],
) ?>
<form action="<?= route_to('podcast-publish', $podcast->id) ?>" method="POST" class="flex flex-col items-start w-full max-w-lg mx-auto mt-4" data-submit="validate-message">
<?= csrf_field() ?>
<input type="hidden" name="client_timezone" value="UTC" />
<label for="message" class="text-lg font-semibold"><?= lang(
'Podcast.publish_form.post',
) ?></label>
<small class="max-w-md mb-2 text-skin-muted"><?= lang('Podcast.publish_form.post_hint') ?></small>
<div class="mb-8 overflow-hidden shadow-md bg-elevated rounded-xl">
<div class="flex px-4 py-3 gap-x-2">
<img src="<?= $podcast->actor->avatar_image_url ?>" alt="<?= esc($podcast->actor->display_name) ?>" class="w-10 h-10 rounded-full aspect-square" loading="lazy" />
<div class="flex flex-col min-w-0">
<p class="flex items-baseline min-w-0">
<span class="mr-2 font-semibold truncate"><?= esc($podcast->actor->display_name) ?></span>
<span class="text-sm truncate text-skin-muted">@<?= esc($podcast->actor->username) ?></span>
</p>
</div>
</div>
<div class="px-4 mb-2">
<Forms.Textarea name="message" placeholder="<?= lang('Podcast.publish_form.message_placeholder') ?>" autofocus="" rows="2" />
</div>
<footer class="flex justify-around px-6 py-3">
<span class="inline-flex items-center"><Icon glyph="chat" class="mr-1 text-xl opacity-40" />0</span>
<span class="inline-flex items-center"><Icon glyph="repeat" class="mr-1 text-xl opacity-40" />0</span>
<span class="inline-flex items-center"><Icon glyph="heart" class="mr-1 text-xl opacity-40" />0</span>
</footer>
</div>
<fieldset class="flex flex-col">
<legend class="text-lg font-semibold"><?= lang(
'Podcast.publish_form.publication_date',
) ?></legend>
<Forms.Radio value="now" name="publication_method" isChecked="<?= old('publication_method') ? old('publish') === 'now' : true ?>"><?= lang('Podcast.publish_form.publication_method.now') ?></Forms.Radio>
<div class="inline-flex flex-wrap items-center radio-toggler">
<input
class="w-6 h-6 border-contrast text-accent-base border-3 focus:ring-accent"
type="radio" id="schedule" name="publication_method" value="schedule" <?= old('publication_method') && old('publication_method') === 'schedule' ? 'checked' : '' ?> />
<Label for="schedule" class="pl-2 leading-8"><?= lang('Podcast.publish_form.publication_method.schedule') ?></label>
<div class="w-full mt-2 radio-toggler-element">
<Forms.Field
as="DatetimePicker"
name="scheduled_publication_date"
label="<?= lang('Podcast.publish_form.scheduled_publication_date') ?>"
hint="<?= lang('Podcast.publish_form.scheduled_publication_date_hint') ?>"
value="<?= $podcast->published_at ?>"
/>
</div>
</div>
</fieldset>
<Alert id="publish-warning" variant="warning" glyph="alert" class="hidden mt-2" title="<?= lang('Podcast.publish_form.message_warning') ?>"><?= lang('Podcast.publish_form.message_warning_hint') ?></Alert>
<div class="flex items-center justify-between w-full mt-4">
<Button uri="<?= route_to('podcast-publish-cancel', $podcast->id) ?>" variant="danger"><?= lang('Podcast.publish_form.cancel_publication') ?></Button>
<Button variant="primary" type="submit" data-btn-text-warning="<?= lang('Podcast.publish_form.message_warning_submit') ?>" data-btn-text="<?= lang('Podcast.publish_form.submit') ?>"><?= lang('Podcast.publish_form.submit') ?></Button>
</div>
</form>
<?= $this->endSection() ?>

View File

@ -0,0 +1,81 @@
<?= $this->extend('_layout') ?>
<?= $this->section('title') ?>
<?= lang('Podcast.publish_edit') ?>
<?= $this->endSection() ?>
<?= $this->section('pageTitle') ?>
<?= lang('Podcast.publish_edit') ?>
<?= $this->endSection() ?>
<?= $this->section('content') ?>
<?= anchor(
route_to('podcast-view', $podcast->id),
icon('arrow-left', 'mr-2 text-lg') . lang('Podcast.publish_form.back_to_podcast_dashboard'),
[
'class' => 'inline-flex items-center font-semibold mr-4 text-sm',
],
) ?>
<form action="<?= route_to('podcast-publish_edit', $podcast->id) ?>" method="POST" class="flex flex-col items-start w-full max-w-lg mx-auto mt-4" data-submit="validate-message">
<?= csrf_field() ?>
<input type="hidden" name="client_timezone" value="UTC" />
<label for="message" class="text-lg font-semibold"><?= lang(
'Podcast.publish_form.post',
) ?></label>
<small class="max-w-md mb-2 text-skin-muted"><?= lang('Podcast.publish_form.post_hint') ?></small>
<div class="mb-8 overflow-hidden shadow-md bg-elevated rounded-xl">
<div class="flex px-4 py-3 gap-x-2">
<img src="<?= $podcast->actor->avatar_image_url ?>" alt="<?= esc($podcast->actor->display_name) ?>" class="w-10 h-10 rounded-full aspect-square" loading="lazy" />
<div class="flex flex-col min-w-0">
<p class="flex items-baseline min-w-0">
<span class="mr-2 font-semibold truncate"><?= esc($podcast->actor->display_name) ?></span>
<span class="text-sm truncate text-skin-muted">@<?= esc($podcast->actor->username) ?></span>
</p>
<?= relative_time($podcast->published_at, 'text-xs text-skin-muted') ?>
</div>
</div>
<div class="px-4 mb-2">
<Forms.Textarea name="message" placeholder="<?= lang('Podcast.publish_form.message_placeholder') ?>" autofocus="" value="<?= $post !== null ? esc($post->message) : '' ?>" rows="2" />
</div>
<footer class="flex justify-around px-6 py-3">
<span class="inline-flex items-center"><Icon glyph="chat" class="mr-1 text-xl opacity-40" />0</span>
<span class="inline-flex items-center"><Icon glyph="repeat" class="mr-1 text-xl opacity-40" />0</span>
<span class="inline-flex items-center"><Icon glyph="heart" class="mr-1 text-xl opacity-40" />0</span>
</footer>
</div>
<fieldset class="flex flex-col">
<legend class="text-lg font-semibold"><?= lang(
'Podcast.publish_form.publication_date',
) ?></legend>
<Forms.Radio value="now" name="publication_method" isChecked="<?= old('publication_method') && old('publish') === 'now' ?>"><?= lang('Podcast.publish_form.publication_method.now') ?></Forms.Radio>
<div class="inline-flex flex-wrap items-center radio-toggler">
<input
class="w-6 h-6 border-contrast text-accent-base border-3 focus:ring-accent"
type="radio" id="schedule" name="publication_method" value="schedule" <?= old('publication_method') ? old('publication_method') === 'schedule' : 'checked' ?> />
<Label for="schedule" class="pl-2 leading-8"><?= lang('Podcast.publish_form.publication_method.schedule') ?></label>
<div class="w-full mt-2 radio-toggler-element">
<Forms.Field
as="DatetimePicker"
name="scheduled_publication_date"
label="<?= lang('Podcast.publish_form.scheduled_publication_date') ?>"
hint="<?= lang('Podcast.publish_form.scheduled_publication_date_hint') ?>"
value="<?= $podcast->published_at ?>"
/>
</div>
</div>
</fieldset>
<Alert id="publish-warning" variant="warning" glyph="alert" class="hidden mt-2" title="<?= lang('Episode.publish_form.message_warning') ?>"><?= lang('Podcast.publish_form.message_warning_hint') ?></Alert>
<div class="flex items-center justify-between w-full mt-4">
<Button uri="<?= route_to('podcast-publish-cancel', $podcast->id) ?>" variant="danger"><?= lang('Podcast.publish_form.cancel_publication') ?></Button>
<Button variant="primary" type="submit" data-btn-text-warning="<?= lang('Podcast.publish_form.message_warning_submit') ?>" data-btn-text="<?= lang('Podcast.publish_form.submit_edit') ?>"><?= lang('Podcast.publish_form.submit_edit') ?></Button>
</div>
</form>
<?= $this->endSection() ?>