mirror of
https://code.castopod.org/adaures/castopod
synced 2025-04-19 13:01:19 +00:00
feat: add publish feature for podcasts and set draft by default
closes #128, #220
This commit is contained in:
parent
9843ce3882
commit
3d363f2efe
@ -177,6 +177,10 @@ class AddPodcasts extends Migration
|
||||
'type' => 'INT',
|
||||
'unsigned' => true,
|
||||
],
|
||||
'published_at' => [
|
||||
'type' => 'DATETIME',
|
||||
'null' => true,
|
||||
],
|
||||
'created_at' => [
|
||||
'type' => 'DATETIME',
|
||||
],
|
||||
|
@ -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'],
|
||||
],
|
||||
[
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
*
|
||||
|
@ -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.
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
6
app/Resources/icons/warning.svg
Normal file
6
app/Resources/icons/warning.svg
Normal 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 |
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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',
|
||||
|
@ -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
|
||||
|
@ -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')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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]);
|
||||
|
@ -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',
|
||||
|
@ -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.",
|
||||
|
@ -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)
|
||||
|
@ -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') ?>
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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">
|
||||
|
@ -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' ?>">
|
||||
|
80
themes/cp_admin/podcast/publish.php
Normal file
80
themes/cp_admin/podcast/publish.php
Normal 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() ?>
|
81
themes/cp_admin/podcast/publish_edit.php
Normal file
81
themes/cp_admin/podcast/publish_edit.php
Normal 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() ?>
|
Loading…
x
Reference in New Issue
Block a user