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', 'type' => 'INT',
'unsigned' => true, 'unsigned' => true,
], ],
'published_at' => [
'type' => 'DATETIME',
'null' => true,
],
'created_at' => [ 'created_at' => [
'type' => 'DATETIME', 'type' => 'DATETIME',
], ],

View File

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

View File

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

View File

@ -79,6 +79,8 @@ use RuntimeException;
* @property string|null $partner_image_url * @property string|null $partner_image_url
* @property int $created_by * @property int $created_by
* @property int $updated_by * @property int $updated_by
* @property string $publication_status;
* @property Time|null $published_at;
* @property Time $created_at; * @property Time $created_at;
* @property Time $updated_at; * @property Time $updated_at;
* *
@ -147,6 +149,13 @@ class Podcast extends Entity
protected string $custom_rss_string; protected string $custom_rss_string;
protected ?string $publication_status = null;
/**
* @var string[]
*/
protected $dates = ['published_at', 'created_at', 'updated_at'];
/** /**
* @var array<string, string> * @var array<string, string>
*/ */
@ -459,6 +468,21 @@ class Podcast extends Entity
return $this->description; 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 * Returns the podcast's podcasting platform links
* *

View File

@ -116,18 +116,27 @@ if (! function_exists('publication_pill')) {
$class = match ($publicationStatus) { $class = match ($publicationStatus) {
'published' => 'text-pine-500 border-pine-500 bg-pine-50', 'published' => 'text-pine-500 border-pine-500 bg-pine-50',
'scheduled' => 'text-red-600 border-red-600 bg-red-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', 'not_published' => 'text-gray-600 border-gray-600 bg-gray-50',
default => '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); $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 . $class .
' ' . ' ' .
$customClass . $customClass .
'">' . '">' .
$label . $label .
($publicationStatus === 'with_podcast' ? '<Icon glyph="warning" class="flex-shrink-0 ml-1 text-lg" />' : '') .
'</span>'; '</span>';
} }
} }
@ -136,7 +145,7 @@ if (! function_exists('publication_pill')) {
if (! function_exists('publication_button')) { if (! function_exists('publication_button')) {
/** /**
* Publication button component * Publication button component for episodes
* *
* Displays the appropriate publication button depending on the publication post. * Displays the appropriate publication button depending on the publication post.
*/ */
@ -149,6 +158,7 @@ if (! function_exists('publication_button')) {
$variant = 'primary'; $variant = 'primary';
$iconLeft = 'upload-cloud'; $iconLeft = 'upload-cloud';
break; break;
case 'with_podcast':
case 'scheduled': case 'scheduled':
$label = lang('Episode.publish_edit'); $label = lang('Episode.publish_edit');
$route = route_to('episode-publish_edit', $podcastId, $episodeId); $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')) { if (! function_exists('episode_numbering')) {
/** /**
* Returns relevant translated episode numbering. * Returns relevant translated episode numbering.

View File

@ -8,6 +8,8 @@ declare(strict_types=1);
* @link https://castopod.org/ * @link https://castopod.org/
*/ */
use CodeIgniter\I18n\Time;
if (! function_exists('get_browser_language')) { if (! function_exists('get_browser_language')) {
/** /**
* Gets the browser default language using the request header key `HTTP_ACCEPT_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]; 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') ->join('podcasts', 'podcasts.id = episodes.podcast_id')
->where('slug', $episodeSlug) ->where('slug', $episodeSlug)
->where('podcasts.handle', $podcastHandle) ->where('podcasts.handle', $podcastHandle)
->where('`published_at` <= UTC_TIMESTAMP()', null, false) ->where('`' . $this->db->getPrefix() . 'episodes`.`published_at` <= UTC_TIMESTAMP()', null, false)
->first(); ->first();
cache() cache()

View File

@ -64,6 +64,7 @@ class PodcastModel extends Model
'partner_id', 'partner_id',
'partner_link_url', 'partner_link_url',
'partner_image_url', 'partner_image_url',
'published_at',
'created_by', 'created_by',
'updated_by', 'updated_by',
]; ];
@ -92,6 +93,7 @@ class PodcastModel extends Model
'owner_email' => 'required|valid_email', 'owner_email' => 'required|valid_email',
'new_feed_url' => 'valid_url_strict|permit_empty', 'new_feed_url' => 'valid_url_strict|permit_empty',
'type' => 'required', 'type' => 'required',
'published_at' => 'valid_date|permit_empty',
'created_by' => 'required', 'created_by' => 'required',
'updated_by' => 'required', 'updated_by' => 'required',
]; ];
@ -128,6 +130,7 @@ class PodcastModel extends Model
$cacheName = "podcast-{$podcastHandle}"; $cacheName = "podcast-{$podcastHandle}";
if (! ($found = cache($cacheName))) { if (! ($found = cache($cacheName))) {
$found = $this->where('handle', $podcastHandle) $found = $this->where('handle', $podcastHandle)
->where('`published_at` <= UTC_TIMESTAMP()', null, false)
->first(); ->first();
cache() cache()
->save("podcast-{$podcastHandle}", $found, DECADE); ->save("podcast-{$podcastHandle}", $found, DECADE);
@ -168,9 +171,9 @@ class PodcastModel extends Model
*/ */
public function getAllPodcasts(string $orderBy = null): array 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') $fediverseTablePrefix = $prefix . config('Fediverse')
->tablesPrefix; ->tablesPrefix;
$this->builder() $this->builder()
@ -195,7 +198,7 @@ class PodcastModel extends Model
$this->orderBy('created_at', 'ASC'); $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% 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', [ $routes->post('edit', 'PodcastController::attemptEdit/$1', [
'filter' => 'permission:podcast-edit', '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', [ $routes->get('edit/delete-banner', 'PodcastController::deleteBanner/$1', [
'as' => 'podcast-banner-delete', 'as' => 'podcast-banner-delete',
'filter' => 'permission:podcast-edit', 'filter' => 'permission:podcast-edit',

View File

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

View File

@ -12,14 +12,17 @@ namespace Modules\Admin\Controllers;
use App\Entities\Location; use App\Entities\Location;
use App\Entities\Podcast; use App\Entities\Podcast;
use App\Entities\Post;
use App\Models\ActorModel; use App\Models\ActorModel;
use App\Models\CategoryModel; use App\Models\CategoryModel;
use App\Models\EpisodeModel; use App\Models\EpisodeModel;
use App\Models\LanguageModel; use App\Models\LanguageModel;
use App\Models\MediaModel; use App\Models\MediaModel;
use App\Models\PodcastModel; use App\Models\PodcastModel;
use App\Models\PostModel;
use CodeIgniter\Exceptions\PageNotFoundException; use CodeIgniter\Exceptions\PageNotFoundException;
use CodeIgniter\HTTP\RedirectResponse; use CodeIgniter\HTTP\RedirectResponse;
use CodeIgniter\I18n\Time;
use Config\Services; use Config\Services;
use Modules\Analytics\Models\AnalyticsPodcastByCountryModel; use Modules\Analytics\Models\AnalyticsPodcastByCountryModel;
use Modules\Analytics\Models\AnalyticsPodcastByEpisodeModel; use Modules\Analytics\Models\AnalyticsPodcastByEpisodeModel;
@ -237,6 +240,7 @@ class PodcastController extends BaseController
'is_locked' => $this->request->getPost('lock') === 'yes', 'is_locked' => $this->request->getPost('lock') === 'yes',
'created_by' => user_id(), 'created_by' => user_id(),
'updated_by' => user_id(), 'updated_by' => user_id(),
'published_at' => null,
]); ]);
$podcastModel = new PodcastModel(); $podcastModel = new PodcastModel();
@ -604,4 +608,361 @@ class PodcastController extends BaseController
'podcast_handle' => $this->podcast->handle, '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()); ->with('errors', $episodePersonModel->errors());
} }
} }
if ($itemNumber === 1) {
$firstEpisodePublicationDate = strtotime((string) $item->pubDate);
}
} }
// set interact as the newly imported podcast actor // set interact as the newly imported podcast actor
$importedPodcast = (new PodcastModel())->getPodcastById($newPodcastId); $importedPodcast = (new PodcastModel())->getPodcastById($newPodcastId);
set_interact_as_actor($importedPodcast->actor_id); 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(); $db->transComplete();
return redirect()->route('podcast-view', [$newPodcastId]); return redirect()->route('podcast-view', [$newPodcastId]);

View File

@ -34,9 +34,11 @@ return [
'create' => 'Add an episode', 'create' => 'Add an episode',
'publication_status' => [ 'publication_status' => [
'published' => 'Published', 'published' => 'Published',
'with_podcast' => 'Published',
'scheduled' => 'Scheduled', 'scheduled' => 'Scheduled',
'not_published' => 'Not published', 'not_published' => 'Not published',
], ],
'with_podcast_hint' => 'To be published at the same time as the podcast',
'list' => [ 'list' => [
'search' => [ 'search' => [
'placeholder' => 'Search for an episode', 'placeholder' => 'Search for an episode',
@ -55,8 +57,15 @@ return [
'messages' => [ 'messages' => [
'createSuccess' => 'Episode has been successfully created!', 'createSuccess' => 'Episode has been successfully created!',
'editSuccess' => 'Episode has been successfully updated!', '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!', 'publishCancelSuccess' => 'Episode publication successfully cancelled!',
'unpublishBeforeDeleteTip' => 'You must unpublish the episode before deleting it.', 'unpublishBeforeDeleteTip' => 'You must unpublish the episode before deleting it.',
'scheduleDateError' => 'Schedule date must be set!',
'deletePublishedEpisodeError' => 'Please unpublish the episode before deleting it.', 'deletePublishedEpisodeError' => 'Please unpublish the episode before deleting it.',
'deleteSuccess' => 'Episode successfully deleted!', 'deleteSuccess' => 'Episode successfully deleted!',
'deleteError' => 'Failed to delete episode {type, select, '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.', 'If you need RSS tags that Castopod does not handle, set them here.',
'custom_rss' => 'Custom RSS tags for the episode', 'custom_rss' => 'Custom RSS tags for the episode',
'custom_rss_hint' => 'This will be injected within the ❬item❭ tag.', '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' => '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_create' => 'Create episode',
'submit_edit' => 'Save episode', 'submit_edit' => 'Save episode',
], ],
@ -154,6 +163,7 @@ return [
'publication_method' => [ 'publication_method' => [
'now' => 'Now', 'now' => 'Now',
'schedule' => 'Schedule', 'schedule' => 'Schedule',
'with_podcast' => 'Publish alongside podcast',
], ],
'scheduled_publication_date' => 'Scheduled publication date', 'scheduled_publication_date' => 'Scheduled publication date',
'scheduled_publication_date_clear' => 'Clear publication date', 'scheduled_publication_date_clear' => 'Clear publication date',

View File

@ -16,14 +16,17 @@ return [
'new_episode' => 'New Episode', 'new_episode' => 'New Episode',
'view' => 'View podcast', 'view' => 'View podcast',
'edit' => 'Edit podcast', 'edit' => 'Edit podcast',
'publish' => 'Publish podcast',
'publish_edit' => 'Edit publication',
'delete' => 'Delete podcast', 'delete' => 'Delete podcast',
'see_episodes' => 'See episodes', 'see_episodes' => 'See episodes',
'see_contributors' => 'See contributors', 'see_contributors' => 'See contributors',
'go_to_page' => 'Go to page', 'go_to_page' => 'Go to page',
'latest_episodes' => 'Latest episodes', 'latest_episodes' => 'Latest episodes',
'see_all_episodes' => 'See all episodes', 'see_all_episodes' => 'See all episodes',
'draft' => 'Draft',
'messages' => [ 'messages' => [
'createSuccess' => 'Podcast has been successfully created!', 'createSuccess' => 'Podcast successfully created!',
'editSuccess' => 'Podcast has been successfully updated!', 'editSuccess' => 'Podcast has been successfully updated!',
'importSuccess' => 'Podcast has been successfully imported!', 'importSuccess' => 'Podcast has been successfully imported!',
'deleteSuccess' => 'Podcast @{podcast_handle} successfully deleted!', 'deleteSuccess' => 'Podcast @{podcast_handle} successfully deleted!',
@ -46,6 +49,10 @@ return [
} added to the podcast!', } added to the podcast!',
'podcastFeedUpToDate' => 'Podcast is already up to date.', 'podcastFeedUpToDate' => 'Podcast is already up to date.',
'podcastNotImported' => 'Podcast could not be updated as it was not imported.', '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' => [ 'form' => [
'identity_section_title' => 'Podcast identity', 'identity_section_title' => 'Podcast identity',
@ -121,7 +128,9 @@ return [
'partner_link_url_hint' => 'The generic partner link address', 'partner_link_url_hint' => 'The generic partner link address',
'partner_image_url_hint' => 'The generic partner image address', 'partner_image_url_hint' => 'The generic partner image address',
'status_section_title' => 'Status', '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', 'complete' => 'Podcast will not be having new episodes',
'lock' => 'Prevent podcast from being copied', 'lock' => 'Prevent podcast from being copied',
'lock_hint' => 'lock_hint' =>
@ -242,6 +251,32 @@ return [
'film_reviews' => 'Film Reviews', 'film_reviews' => 'Film Reviews',
'tv_reviews' => 'TV 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' => [ 'delete_form' => [
'disclaimer' => '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.", "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.*') ->select('podcasts.*')
->join('episodes', 'podcasts.id = episodes.podcast_id', 'left outer') ->join('episodes', 'podcasts.id = episodes.podcast_id', 'left outer')
->where('podcasts.is_published_on_hubs', false) ->where('podcasts.is_published_on_hubs', false)
->where('`' . $podcastModel->db->getPrefix() . 'podcasts`.`published_at` <= UTC_TIMESTAMP()', null, false)
->orGroupStart() ->orGroupStart()
->where('episodes.is_published_on_hubs', false) ->where('episodes.is_published_on_hubs', false)
->where('`' . $podcastModel->db->getPrefix() . 'episodes`.`published_at` <= UTC_TIMESTAMP()', null, false) ->where('`' . $podcastModel->db->getPrefix() . 'episodes`.`published_at` <= UTC_TIMESTAMP()', null, false)

View File

@ -39,6 +39,9 @@
</div> </div>
</div> </div>
</header> </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"> <div class="px-2 py-8 mx-auto md:px-12">
<?= view('_message_block') ?> <?= view('_message_block') ?>
<?= $this->renderSection('content') ?> <?= $this->renderSection('content') ?>

View File

@ -69,28 +69,30 @@
</footer> </footer>
</div> </div>
<fieldset class="flex flex-col"> <?php if ($podcast->publication_status === 'published'): ?>
<legend class="text-lg font-semibold"><?= lang( <fieldset class="flex flex-col">
<legend class="text-lg font-semibold"><?= lang(
'Episode.publish_form.publication_date', 'Episode.publish_form.publication_date',
) ?></legend> ) ?></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> <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"> <div class="inline-flex flex-wrap items-center radio-toggler">
<input <input
class="w-6 h-6 border-contrast text-accent-base border-3 focus:ring-accent" 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' : '' ?> /> 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> <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"> <div class="w-full mt-2 radio-toggler-element">
<Forms.Field <Forms.Field
as="DatetimePicker" as="DatetimePicker"
name="scheduled_publication_date" name="scheduled_publication_date"
label="<?= lang('Episode.publish_form.scheduled_publication_date') ?>" label="<?= lang('Episode.publish_form.scheduled_publication_date') ?>"
hint="<?= lang('Episode.publish_form.scheduled_publication_date_hint') ?>" hint="<?= lang('Episode.publish_form.scheduled_publication_date_hint') ?>"
value="<?= $episode->published_at ?>" value="<?= $episode->published_at ?>"
/> />
</div>
</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> <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"> <div class="flex items-center justify-between w-full mt-4">

View File

@ -73,27 +73,29 @@
</footer> </footer>
</div> </div>
<fieldset class="flex flex-col"> <?php if ($podcast->publication_status === 'published'): ?>
<legend class="text-lg font-semibold"><?= lang( <fieldset class="flex flex-col">
<legend class="text-lg font-semibold"><?= lang(
'Episode.publish_form.publication_date', 'Episode.publish_form.publication_date',
) ?></legend> ) ?></legend>
<Forms.Radio value="now" name="publication_method" isChecked="<?= old('publication_method') && old('publish') === 'now' ?>"><?= lang('Episode.publish_form.publication_method.now') ?></Forms.Radio> <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"> <div class="inline-flex flex-wrap items-center radio-toggler">
<input <input
class="w-6 h-6 border-contrast text-accent-base border-3 focus:ring-accent" 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' ?> /> 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> <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"> <div class="w-full mt-2 radio-toggler-element">
<Forms.Field <Forms.Field
as="DatetimePicker" as="DatetimePicker"
name="scheduled_publication_date" name="scheduled_publication_date"
label="<?= lang('Episode.publish_form.scheduled_publication_date') ?>" label="<?= lang('Episode.publish_form.scheduled_publication_date') ?>"
hint="<?= lang('Episode.publish_form.scheduled_publication_date_hint') ?>" hint="<?= lang('Episode.publish_form.scheduled_publication_date_hint') ?>"
value="<?= $episode->published_at ?>" value="<?= $episode->published_at ?>"
/> />
</div>
</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> <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"> <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"> <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="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 <img
alt="<?= esc($podcast->title) ?>" 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" /> 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> </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"> <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> <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> <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') ?>"> <Forms.Toggler class="mb-2" name="lock" value="yes" checked="true" hint="<?= lang('Podcast.form.lock_hint') ?>">
<?= lang('Podcast.form.lock') ?> <?= lang('Podcast.form.lock') ?>
</Forms.Toggler> </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') ?> <?= lang('Podcast.form.block') ?>
</Forms.Toggler> </Forms.Toggler>
<Forms.Toggler name="complete" value="yes" checked="false"> <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') ?>"> <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') ?> <?= lang('Podcast.form.lock') ?>
</Forms.Toggler> </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') ?> <?= lang('Podcast.form.block') ?>
</Forms.Toggler> </Forms.Toggler>
<Forms.Toggler name="complete" value="yes" checked="<?= $podcast->is_completed ? 'true' : 'false' ?>"> <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() ?>