diff --git a/app/Config/Routes.php b/app/Config/Routes.php index d46a8855..ae3c9365 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -210,6 +210,15 @@ $routes->get('audio/@(:podcastHandle)/(:slug).(:alphanum)', 'EpisodeAudioControl 'as' => 'episode-audio', ], ); +// episode preview link +$routes->get('/p/(:uuid)', 'EpisodePreviewController::index/$1', [ + 'as' => 'episode-preview', +]); + +$routes->get('/p/(:uuid)/activity', 'EpisodePreviewController::activity/$1', [ + 'as' => 'episode-preview-activity', +]); + // Other pages $routes->get('/credits', 'CreditsController', [ 'as' => 'credits', diff --git a/app/Controllers/EpisodePreviewController.php b/app/Controllers/EpisodePreviewController.php new file mode 100644 index 00000000..6c03eef2 --- /dev/null +++ b/app/Controllers/EpisodePreviewController.php @@ -0,0 +1,66 @@ +getEpisodeByPreviewId($params[0]); + + if (! $episode instanceof Episode) { + throw PageNotFoundException::forPageNotFound(); + } + + $this->episode = $episode; + + if ($episode->publication_status === 'published') { + // redirect to episode page + return redirect()->route('episode', [$episode->podcast->handle, $episode->slug]); + } + + unset($params[0]); + + return $this->{$method}(...$params); + } + + public function index(): RedirectResponse | string + { + helper('form'); + + return view('episode/preview-comments', [ + 'podcast' => $this->episode->podcast, + 'episode' => $this->episode, + ]); + } + + public function activity(): RedirectResponse | string + { + helper('form'); + + return view('episode/preview-activity', [ + 'podcast' => $this->episode->podcast, + 'episode' => $this->episode, + ]); + } +} diff --git a/app/Database/Migrations/2023-08-22-120000_add_episode_preview_id.php b/app/Database/Migrations/2023-08-22-120000_add_episode_preview_id.php new file mode 100644 index 00000000..2bd026e2 --- /dev/null +++ b/app/Database/Migrations/2023-08-22-120000_add_episode_preview_id.php @@ -0,0 +1,36 @@ + [ + 'type' => 'BINARY', + 'constraint' => 16, + 'after' => 'podcast_id', + ], + ]; + + $this->forge->addColumn('episodes', $fields); + + // set preview_id as unique key + $prefix = $this->db->getPrefix(); + $uniquePreviewId = <<db->query($uniquePreviewId); + } + + public function down(): void + { + $fields = ['preview_id']; + $this->forge->dropColumn('episodes', $fields); + } +} diff --git a/app/Entities/Episode.php b/app/Entities/Episode.php index 41c54a5b..c7af02d3 100644 --- a/app/Entities/Episode.php +++ b/app/Entities/Episode.php @@ -14,6 +14,7 @@ use App\Entities\Clip\Soundbite; use App\Libraries\SimpleRSSElement; use App\Models\ClipModel; use App\Models\EpisodeCommentModel; +use App\Models\EpisodeModel; use App\Models\PersonModel; use App\Models\PodcastModel; use App\Models\PostModel; @@ -21,6 +22,7 @@ use CodeIgniter\Entity\Entity; use CodeIgniter\Files\File; use CodeIgniter\HTTP\Files\UploadedFile; use CodeIgniter\I18n\Time; +use Exception; use League\CommonMark\Environment\Environment; use League\CommonMark\Extension\Autolink\AutolinkExtension; use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension; @@ -39,6 +41,8 @@ use SimpleXMLElement; * @property int $id * @property int $podcast_id * @property Podcast $podcast + * @property ?string $preview_id + * @property string $preview_link * @property string $link * @property string $guid * @property string $slug @@ -150,6 +154,7 @@ class Episode extends Entity protected $casts = [ 'id' => 'integer', 'podcast_id' => 'integer', + 'preview_id' => '?string', 'guid' => 'string', 'slug' => 'string', 'title' => 'string', @@ -509,7 +514,7 @@ class Episode extends Entity if ($this->getPodcast()->episode_description_footer_html) { $descriptionHtml .= ""; +->episode_description_footer_html}"; } return $descriptionHtml; @@ -667,4 +672,18 @@ class Episode extends Entity urlencode((string) $this->attributes['guid']) . ($serviceSlug !== null ? '&_from=' . $serviceSlug : ''); } + + public function getPreviewLink(): string + { + if ($this->preview_id === null) { + // generate preview id + if (! $previewUUID = (new EpisodeModel())->setEpisodePreviewId($this->id)) { + throw new Exception('Could not set episode preview id'); + } + + $this->preview_id = $previewUUID; + } + + return url_to('episode-preview', (string) $this->preview_id); + } } diff --git a/app/Helpers/components_helper.php b/app/Helpers/components_helper.php index 351d2ec0..176e75c7 100644 --- a/app/Helpers/components_helper.php +++ b/app/Helpers/components_helper.php @@ -9,6 +9,7 @@ declare(strict_types=1); */ use App\Entities\Category; +use App\Entities\Episode; use App\Entities\Location; use CodeIgniter\I18n\Time; use CodeIgniter\View\Table; @@ -218,8 +219,8 @@ if (! function_exists('publication_status_banner')) { } return << -

+

+ HTML; + } +} + +// ------------------------------------------------------------------------ + if (! function_exists('episode_numbering')) { /** * Returns relevant translated episode numbering. @@ -360,7 +413,7 @@ if (! function_exists('relative_time')) { $datetime = $time->format(DateTime::ATOM); return << + @@ -378,10 +431,10 @@ if (! function_exists('local_datetime')) { 'request' )->getLocale(), IntlDateFormatter::MEDIUM, IntlDateFormatter::LONG); $translatedDate = $time->toLocalizedString($formatter->getPattern()); - $datetime = $time->format(DateTime::ISO8601); + $datetime = $time->format(DateTime::ATOM); return <<WriteTags()) { - echo 'Successfully wrote tags
'; + // Successfully wrote tags if ($tagwriter->warnings !== []) { - echo 'There were some warnings:
' . - implode('

', $tagwriter->warnings); + log_message('warning', 'There were some warnings:' . PHP_EOL . implode(PHP_EOL, $tagwriter->warnings)); } } else { - echo 'Failed to write tags!
' . - implode('

', $tagwriter->errors); + log_message('critical', 'Failed to write tags!' . PHP_EOL . implode(PHP_EOL, $tagwriter->errors)); } } } diff --git a/app/Language/en/Episode.php b/app/Language/en/Episode.php index ebe39336..44be8e38 100644 --- a/app/Language/en/Episode.php +++ b/app/Language/en/Episode.php @@ -30,4 +30,16 @@ return [ }', 'all_podcast_episodes' => 'All podcast episodes', 'back_to_podcast' => 'Go back to podcast', + 'preview' => [ + 'title' => 'Preview', + 'not_published' => 'Not published', + 'text' => '{publication_status, select, + published {This episode is not yet published.} + scheduled {This episode is scheduled for publication on {publication_date}.} + with_podcast {This episode will be published at the same time as the podcast.} + other {This episode is not yet published.} + }', + 'publish' => 'Publish', + 'publish_edit' => 'Edit publication', + ], ]; diff --git a/app/Models/EpisodeModel.php b/app/Models/EpisodeModel.php index bedb1949..bde68905 100644 --- a/app/Models/EpisodeModel.php +++ b/app/Models/EpisodeModel.php @@ -14,9 +14,10 @@ use App\Entities\Episode; use CodeIgniter\Database\BaseBuilder; use CodeIgniter\Database\BaseResult; use CodeIgniter\I18n\Time; -use CodeIgniter\Model; +use Michalsn\Uuid\UuidModel; +use Ramsey\Uuid\Lazy\LazyUuidFromString; -class EpisodeModel extends Model +class EpisodeModel extends UuidModel { /** * TODO: remove, shouldn't be here @@ -50,6 +51,11 @@ class EpisodeModel extends Model ], ]; + /** + * @var string[] + */ + protected $uuidFields = ['preview_id']; + /** * @var string */ @@ -61,6 +67,7 @@ class EpisodeModel extends Model protected $allowedFields = [ 'id', 'podcast_id', + 'preview_id', 'guid', 'title', 'slug', @@ -188,6 +195,38 @@ class EpisodeModel extends Model return $found; } + public function getEpisodeByPreviewId(string $previewId): ?Episode + { + $cacheName = "podcast_episode#preview-{$previewId}"; + if (! ($found = cache($cacheName))) { + $builder = $this->where([ + 'preview_id' => $this->uuid->fromString($previewId) + ->getBytes(), + ]); + + $found = $builder->first(); + + cache() + ->save($cacheName, $found, DECADE); + } + + return $found; + } + + public function setEpisodePreviewId(int $episodeId): string|false + { + /** @var LazyUuidFromString $uuid */ + $uuid = $this->uuid->{$this->uuidVersion}(); + + if (! $this->update($episodeId, [ + 'preview_id' => $uuid, + ])) { + return false; + } + + return (string) $uuid; + } + /** * Gets all episodes for a podcast ordered according to podcast type Filtered depending on year or season * diff --git a/app/Resources/styles/custom.css b/app/Resources/styles/custom.css index 25883b74..6dfa93b1 100644 --- a/app/Resources/styles/custom.css +++ b/app/Resources/styles/custom.css @@ -59,7 +59,7 @@ ); } - .bg-stripes-gray { + .bg-stripes-default { background-image: repeating-linear-gradient( -45deg, #f3f4f6, diff --git a/modules/Admin/Language/en/Episode.php b/modules/Admin/Language/en/Episode.php index 5ed5e3ac..1a5e57a4 100644 --- a/modules/Admin/Language/en/Episode.php +++ b/modules/Admin/Language/en/Episode.php @@ -22,6 +22,7 @@ return [ 'all_podcast_episodes' => 'All podcast episodes', 'back_to_podcast' => 'Go back to podcast', 'edit' => 'Edit', + 'preview' => 'Preview', 'publish' => 'Publish', 'publish_edit' => 'Edit publication', 'publish_date_edit' => 'Edit publication date', @@ -211,4 +212,14 @@ return [ 'light' => 'Light', 'light-transparent' => 'Light transparent', ], + 'publication_status_banner' => [ + 'draft_mode' => 'draft mode', + 'text' => '{publication_status, select, + published {This episode is not yet published.} + scheduled {This episode is scheduled for publication on {publication_date}.} + with_podcast {This episode will be published at the same time as the podcast.} + other {This episode is not yet published.} + }', + 'preview' => 'Preview', + ], ]; diff --git a/themes/cp_admin/_layout.php b/themes/cp_admin/_layout.php index bd006e54..865465fc 100644 --- a/themes/cp_admin/_layout.php +++ b/themes/cp_admin/_layout.php @@ -67,6 +67,9 @@ $isEpisodeArea = isset($podcast) && isset($episode); published_at, $podcast->id, $podcast->publication_status) ?> + publication_status !== 'published'): ?> + +
renderSection('content') ?> diff --git a/themes/cp_app/episode/_layout-preview.php b/themes/cp_app/episode/_layout-preview.php new file mode 100644 index 00000000..c8caa036 --- /dev/null +++ b/themes/cp_app/episode/_layout-preview.php @@ -0,0 +1,199 @@ + + + + + + + + + + + + + + + + + [<?= lang('Episode.preview.title') ?>] <?= $episode->title ?> + + + ' /> + asset('styles/index.css', 'css') ?> + asset('js/app.ts', 'js') ?> + asset('js/podcast.ts', 'js') ?> + asset('js/audio-player.ts', 'js') ?> + + + + +
+ include('_admin_navbar') ?> +
+ + + +
+
+
+
+
+ parental_advisory === 'explicit', 'rounded absolute left-0 bottom-0 ml-2 mb-2 bg-black/75 text-accent-contrast') ?> + is_premium): ?> + + + <?= esc($episode->title) ?> +
+
+ number, $episode->season_number, 'text-sm leading-none font-semibold px-1 py-1 text-white/90 border !no-underline border-subtle', true) ?> +

title) ?>

+
+ persons !== []): ?> + + + location): ?> + location, 'text-xs font-semibold p-2') ?> + +
+
+
+
+ +
+ published_at): ?> + published_at) ?> + + + + + +
+
+
+
+

+ description_markdown, "\n") > 6 || strlen($episode->description) > 500): ?> + getDescriptionHtml('-+Website+-') ?> + +
getDescriptionHtml('-+Website+-') ?>
+ +
+ include('episode/_partials/navigation') ?> + include('podcast/_partials/premium_banner') ?> + +
+
+ renderSection('content') ?> +
+ + include('podcast/_partials/sidebar') ?> +
+ lang('Episode.persons_list', [ + 'episodeTitle' => esc($episode->title), + ]), + 'persons' => $episode->persons, + ]) ?> + fundingPlatforms, 'is_visible'), true)): ?> + include('podcast/_partials/funding_links_modal') ?> + + diff --git a/themes/cp_app/episode/_partials/navigation.php b/themes/cp_app/episode/_partials/navigation.php index ddeedac4..73fdcf5b 100644 --- a/themes/cp_app/episode/_partials/navigation.php +++ b/themes/cp_app/episode/_partials/navigation.php @@ -1,17 +1,32 @@ route_to('episode', esc($podcast->handle), esc($episode->slug)), - 'label' => lang('Episode.comments'), - 'labelInfo' => $episode->comments_count, - ], - [ - 'uri' => route_to('episode-activity', esc($podcast->handle), esc($episode->slug)), - 'label' => lang('Episode.activity'), - 'labelInfo' => $episode->posts_count, - ], -] +if ($episode->publication_status === 'published') { + $navigationItems = [ + [ + 'uri' => route_to('episode', esc($podcast->handle), esc($episode->slug)), + 'label' => lang('Episode.comments'), + 'labelInfo' => $episode->comments_count, + ], + [ + 'uri' => route_to('episode-activity', esc($podcast->handle), esc($episode->slug)), + 'label' => lang('Episode.activity'), + 'labelInfo' => $episode->posts_count, + ], + ]; +} else { + $navigationItems = [ + [ + 'uri' => route_to('episode-preview', $episode->preview_id), + 'label' => lang('Episode.comments'), + 'labelInfo' => $episode->comments_count, + ], + [ + 'uri' => route_to('episode-preview-activity', $episode->preview_id), + 'label' => lang('Episode.activity'), + 'labelInfo' => $episode->posts_count, + ], + ]; +} ?>