mirror of
https://code.castopod.org/adaures/castopod
synced 2025-04-19 04:51:17 +00:00
parent
98c6658840
commit
87cc437e1e
@ -125,6 +125,9 @@ $routes->group('@(:podcastHandle)', static function ($routes): void {
|
||||
$routes->get('activity', 'EpisodeController::activity/$1/$2', [
|
||||
'as' => 'episode-activity',
|
||||
]);
|
||||
$routes->get('chapters', 'EpisodeController::chapters/$1/$2', [
|
||||
'as' => 'episode-chapters',
|
||||
]);
|
||||
$routes->options('comments', 'ActivityPubController::preflight');
|
||||
$routes->get('comments', 'EpisodeController::comments/$1/$2', [
|
||||
'as' => 'episode-comments',
|
||||
@ -196,10 +199,12 @@ $routes->get('/audio/@(:podcastHandle)/(:slug).(:alphanum)', 'EpisodeAudioContro
|
||||
$routes->get('/p/(:uuid)', 'EpisodePreviewController::index/$1', [
|
||||
'as' => 'episode-preview',
|
||||
]);
|
||||
|
||||
$routes->get('/p/(:uuid)/activity', 'EpisodePreviewController::activity/$1', [
|
||||
'as' => 'episode-preview-activity',
|
||||
]);
|
||||
$routes->get('/p/(:uuid)/chapters', 'EpisodePreviewController::chapters/$1', [
|
||||
'as' => 'episode-preview-chapters',
|
||||
]);
|
||||
|
||||
// Other pages
|
||||
$routes->get('/credits', 'CreditsController', [
|
||||
|
@ -27,6 +27,7 @@ use Config\Services;
|
||||
use Modules\Analytics\AnalyticsTrait;
|
||||
use Modules\Fediverse\Objects\OrderedCollectionObject;
|
||||
use Modules\Fediverse\Objects\OrderedCollectionPage;
|
||||
use Modules\Media\FileManagers\FileManagerInterface;
|
||||
use SimpleXMLElement;
|
||||
|
||||
class EpisodeController extends BaseController
|
||||
@ -166,6 +167,67 @@ class EpisodeController extends BaseController
|
||||
return $cachedView;
|
||||
}
|
||||
|
||||
public function chapters(): String
|
||||
{
|
||||
// Prevent analytics hit when authenticated
|
||||
if (! auth()->loggedIn()) {
|
||||
$this->registerPodcastWebpageHit($this->episode->podcast_id);
|
||||
}
|
||||
|
||||
$cacheName = implode(
|
||||
'_',
|
||||
array_filter([
|
||||
'page',
|
||||
"podcast#{$this->podcast->id}",
|
||||
"episode#{$this->episode->id}",
|
||||
'chapters',
|
||||
service('request')
|
||||
->getLocale(),
|
||||
is_unlocked($this->podcast->handle) ? 'unlocked' : null,
|
||||
auth()
|
||||
->loggedIn() ? 'authenticated' : null,
|
||||
]),
|
||||
);
|
||||
|
||||
if (! ($cachedView = cache($cacheName))) {
|
||||
// get chapters from json file
|
||||
$data = [
|
||||
'metatags' => get_episode_metatags($this->episode),
|
||||
'podcast' => $this->podcast,
|
||||
'episode' => $this->episode,
|
||||
];
|
||||
|
||||
if (isset($this->episode->chapters->file_key)) {
|
||||
/** @var FileManagerInterface $fileManager */
|
||||
$fileManager = service('file_manager');
|
||||
$episodeChaptersJsonString = (string) $fileManager->getFileContents($this->episode->chapters->file_key);
|
||||
|
||||
$chapters = json_decode($episodeChaptersJsonString, true);
|
||||
$data['chapters'] = $chapters;
|
||||
}
|
||||
|
||||
$secondsToNextUnpublishedEpisode = (new EpisodeModel())->getSecondsToNextUnpublishedEpisode(
|
||||
$this->podcast->id,
|
||||
);
|
||||
|
||||
if (auth()->loggedIn()) {
|
||||
helper('form');
|
||||
|
||||
return view('episode/chapters', $data);
|
||||
}
|
||||
|
||||
// The page cache is set to a decade so it is deleted manually upon podcast update
|
||||
return view('episode/chapters', $data, [
|
||||
'cache' => $secondsToNextUnpublishedEpisode
|
||||
? $secondsToNextUnpublishedEpisode
|
||||
: DECADE,
|
||||
'cache_name' => $cacheName,
|
||||
]);
|
||||
}
|
||||
|
||||
return $cachedView;
|
||||
}
|
||||
|
||||
public function embed(string $theme = 'light-transparent'): string
|
||||
{
|
||||
header('Content-Security-Policy: frame-ancestors http://*:* https://*:*');
|
||||
|
@ -63,4 +63,12 @@ class EpisodePreviewController extends BaseController
|
||||
'episode' => $this->episode,
|
||||
]);
|
||||
}
|
||||
|
||||
public function chapters(): RedirectResponse | string
|
||||
{
|
||||
return view('episode/preview-chapters', [
|
||||
'podcast' => $this->episode->podcast,
|
||||
'episode' => $this->episode,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
@ -21,7 +21,6 @@ use App\Models\PostModel;
|
||||
use CodeIgniter\Entity\Entity;
|
||||
use CodeIgniter\Files\File;
|
||||
use CodeIgniter\HTTP\Files\UploadedFile;
|
||||
use CodeIgniter\HTTP\URI;
|
||||
use CodeIgniter\I18n\Time;
|
||||
use Config\Images;
|
||||
use Exception;
|
||||
|
@ -23,6 +23,7 @@ return [
|
||||
'back_to_episodes' => 'Back to episodes of {podcast}',
|
||||
'comments' => 'Comments',
|
||||
'activity' => 'Activity',
|
||||
'chapters' => 'Chapters',
|
||||
'description' => 'Episode description',
|
||||
'number_of_comments' => '{numberOfComments, plural,
|
||||
one {# comment}
|
||||
@ -42,4 +43,5 @@ return [
|
||||
'publish' => 'Publish',
|
||||
'publish_edit' => 'Edit publication',
|
||||
],
|
||||
'no_chapters' => 'No chapters are available for this episode.',
|
||||
];
|
||||
|
@ -10,7 +10,49 @@ declare(strict_types=1);
|
||||
|
||||
namespace Modules\Media\Entities;
|
||||
|
||||
use CodeIgniter\Files\File;
|
||||
use Exception;
|
||||
|
||||
class Chapters extends BaseMedia
|
||||
{
|
||||
protected string $type = 'chapters';
|
||||
|
||||
public function initFileProperties(): void
|
||||
{
|
||||
parent::initFileProperties();
|
||||
|
||||
if ($this->file_metadata !== null && array_key_exists('chapter_count', $this->file_metadata)) {
|
||||
helper('media');
|
||||
|
||||
$this->chapter_count = $this->file_metadata['chapter_count'];
|
||||
}
|
||||
}
|
||||
|
||||
public function setFile(File $file): self
|
||||
{
|
||||
parent::setFile($file);
|
||||
|
||||
$metadata = lstat((string) $file) ?? [];
|
||||
|
||||
helper('filesystem');
|
||||
|
||||
$metadata['chapter_count'] = $this->countChaptersInJson($file);
|
||||
|
||||
$this->attributes['file_metadata'] = json_encode($metadata, JSON_INVALID_UTF8_IGNORE);
|
||||
|
||||
$this->file = $file;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
private function countChaptersInJson(File $file): Int
|
||||
{
|
||||
$chapterContent = file_get_contents($file->getRealPath());
|
||||
|
||||
if ($chapterContent === false) {
|
||||
throw new Exception('Could not read chapter file at ' . $this->file->getRealPath());
|
||||
}
|
||||
|
||||
return substr_count($chapterContent, 'startTime');
|
||||
}
|
||||
}
|
||||
|
@ -41,6 +41,7 @@ module.exports = {
|
||||
backgroundColor: {
|
||||
base: "hsl(var(--color-background-base) / <alpha-value>)",
|
||||
elevated: "hsl(var(--color-background-elevated) / <alpha-value>)",
|
||||
subtle: "hsl(var(--color-border-subtle) / <alpha-value>)",
|
||||
navigation: "hsl(var(--color-background-navigation) / <alpha-value>)",
|
||||
"navigation-active":
|
||||
"hsl(var(--color-background-navigation-active) / <alpha-value>)",
|
||||
|
11
themes/cp_app/episode/_partials/chapter.php
Normal file
11
themes/cp_app/episode/_partials/chapter.php
Normal file
@ -0,0 +1,11 @@
|
||||
<article class="flex p-2 gap-x-2">
|
||||
<img src="<?= $chapterImgUrl ?>" class="w-20 h-20 rounded-lg aspect-square" loading="lazy" />
|
||||
<div class="flex flex-col">
|
||||
<div class="flex items-baseline gap-x-2">
|
||||
<span class="px-1 text-sm font-semibold rounded bg-subtle"><?= $startTime ?></span><?= $title ?>
|
||||
</div>
|
||||
<?php if ($chapterUrl !== ''): ?>
|
||||
<a class="inline-flex items-baseline mt-1 text-sm underline text-skin-muted hover:no-underline" href='<?= $chapterUrl ?>' target='_blank' rel="noopener noreferrer"><?= $chapterUrl ?><?= icon('external-link', 'sm:ml-1 sm:text-base sm:opacity-60') ?></a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</article>
|
@ -12,6 +12,11 @@ if ($episode->publication_status === 'published') {
|
||||
'label' => lang('Episode.activity'),
|
||||
'labelInfo' => $episode->posts_count,
|
||||
],
|
||||
[
|
||||
'uri' => route_to('episode-chapters', esc($podcast->handle), esc($episode->slug)),
|
||||
'label' => lang('Episode.chapters'),
|
||||
'labelInfo' => $episode->chapters === null ? 0 : $episode->chapters->chapter_count,
|
||||
],
|
||||
];
|
||||
} else {
|
||||
$navigationItems = [
|
||||
@ -25,8 +30,15 @@ if ($episode->publication_status === 'published') {
|
||||
'label' => lang('Episode.activity'),
|
||||
'labelInfo' => $episode->posts_count,
|
||||
],
|
||||
[
|
||||
'uri' => route_to('episode-preview-chapters', $episode->preview_id),
|
||||
'label' => lang('Episode.chapters'),
|
||||
'labelInfo' => $episode->chapters === null ? 0 : $episode->chapters->chapter_count,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
?>
|
||||
<nav class="sticky z-40 flex col-start-2 pt-4 shadow bg-elevated md:px-8 gap-x-2 md:gap-x-4 -top-4 rounded-conditional-b-xl">
|
||||
<?php foreach ($navigationItems as $item): ?>
|
||||
|
25
themes/cp_app/episode/chapters.php
Normal file
25
themes/cp_app/episode/chapters.php
Normal file
@ -0,0 +1,25 @@
|
||||
<?= $this->extend('episode/_layout') ?>
|
||||
|
||||
<?= $this->section('content') ?>
|
||||
|
||||
<?php if (isset($chapters)): ?>
|
||||
<div class="flex flex-col gap-2">
|
||||
<?php foreach ($chapters['chapters'] as $chapter) {
|
||||
if (isset($chapter['toc'])) {
|
||||
if ($chapter['toc'] !== true) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
echo view('episode/_partials/chapter', [
|
||||
'title' => array_key_exists('title', $chapter) ? $chapter['title'] : '',
|
||||
'startTime' => format_duration($chapter['startTime']),
|
||||
'chapterImgUrl' => array_key_exists('img', $chapter) ? $chapter['img'] : $episode->cover->thumbnail_url,
|
||||
'chapterUrl' => array_key_exists('url', $chapter) ? $chapter['url'] : '',
|
||||
]);
|
||||
} ?>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="text-center"><?= lang('Episode.no_chapters') ?></div>
|
||||
<?php endif; ?>
|
||||
<?= $this->endSection() ?>
|
25
themes/cp_app/episode/preview-chapters.php
Normal file
25
themes/cp_app/episode/preview-chapters.php
Normal file
@ -0,0 +1,25 @@
|
||||
<?= $this->extend('episode/_layout-preview') ?>
|
||||
|
||||
<?= $this->section('content') ?>
|
||||
|
||||
<?php if (isset($chapters)): ?>
|
||||
<div class="flex flex-col gap-2">
|
||||
<?php foreach ($chapters['chapters'] as $chapter) {
|
||||
if (isset($chapter['toc'])) {
|
||||
if ($chapter['toc'] !== true) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
echo view('episode/_partials/chapter', [
|
||||
'title' => array_key_exists('title', $chapter) ? $chapter['title'] : '',
|
||||
'startTime' => format_duration($chapter['startTime']),
|
||||
'chapterImgUrl' => array_key_exists('img', $chapter) ? $chapter['img'] : $episode->cover->thumbnail_url,
|
||||
'chapterUrl' => array_key_exists('url', $chapter) ? $chapter['url'] : '',
|
||||
]);
|
||||
} ?>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="text-center"><?= lang('Episode.no_chapters') ?></div>
|
||||
<?php endif; ?>
|
||||
<?= $this->endSection() ?>
|
Loading…
x
Reference in New Issue
Block a user