mirror of
https://code.castopod.org/adaures/castopod
synced 2025-06-05 08:52:00 +00:00
parent
98c6658840
commit
87cc437e1e
@ -125,6 +125,9 @@ $routes->group('@(:podcastHandle)', static function ($routes): void {
|
|||||||
$routes->get('activity', 'EpisodeController::activity/$1/$2', [
|
$routes->get('activity', 'EpisodeController::activity/$1/$2', [
|
||||||
'as' => 'episode-activity',
|
'as' => 'episode-activity',
|
||||||
]);
|
]);
|
||||||
|
$routes->get('chapters', 'EpisodeController::chapters/$1/$2', [
|
||||||
|
'as' => 'episode-chapters',
|
||||||
|
]);
|
||||||
$routes->options('comments', 'ActivityPubController::preflight');
|
$routes->options('comments', 'ActivityPubController::preflight');
|
||||||
$routes->get('comments', 'EpisodeController::comments/$1/$2', [
|
$routes->get('comments', 'EpisodeController::comments/$1/$2', [
|
||||||
'as' => 'episode-comments',
|
'as' => 'episode-comments',
|
||||||
@ -196,10 +199,12 @@ $routes->get('/audio/@(:podcastHandle)/(:slug).(:alphanum)', 'EpisodeAudioContro
|
|||||||
$routes->get('/p/(:uuid)', 'EpisodePreviewController::index/$1', [
|
$routes->get('/p/(:uuid)', 'EpisodePreviewController::index/$1', [
|
||||||
'as' => 'episode-preview',
|
'as' => 'episode-preview',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$routes->get('/p/(:uuid)/activity', 'EpisodePreviewController::activity/$1', [
|
$routes->get('/p/(:uuid)/activity', 'EpisodePreviewController::activity/$1', [
|
||||||
'as' => 'episode-preview-activity',
|
'as' => 'episode-preview-activity',
|
||||||
]);
|
]);
|
||||||
|
$routes->get('/p/(:uuid)/chapters', 'EpisodePreviewController::chapters/$1', [
|
||||||
|
'as' => 'episode-preview-chapters',
|
||||||
|
]);
|
||||||
|
|
||||||
// Other pages
|
// Other pages
|
||||||
$routes->get('/credits', 'CreditsController', [
|
$routes->get('/credits', 'CreditsController', [
|
||||||
|
@ -27,6 +27,7 @@ use Config\Services;
|
|||||||
use Modules\Analytics\AnalyticsTrait;
|
use Modules\Analytics\AnalyticsTrait;
|
||||||
use Modules\Fediverse\Objects\OrderedCollectionObject;
|
use Modules\Fediverse\Objects\OrderedCollectionObject;
|
||||||
use Modules\Fediverse\Objects\OrderedCollectionPage;
|
use Modules\Fediverse\Objects\OrderedCollectionPage;
|
||||||
|
use Modules\Media\FileManagers\FileManagerInterface;
|
||||||
use SimpleXMLElement;
|
use SimpleXMLElement;
|
||||||
|
|
||||||
class EpisodeController extends BaseController
|
class EpisodeController extends BaseController
|
||||||
@ -166,6 +167,67 @@ class EpisodeController extends BaseController
|
|||||||
return $cachedView;
|
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
|
public function embed(string $theme = 'light-transparent'): string
|
||||||
{
|
{
|
||||||
header('Content-Security-Policy: frame-ancestors http://*:* https://*:*');
|
header('Content-Security-Policy: frame-ancestors http://*:* https://*:*');
|
||||||
|
@ -63,4 +63,12 @@ class EpisodePreviewController extends BaseController
|
|||||||
'episode' => $this->episode,
|
'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\Entity\Entity;
|
||||||
use CodeIgniter\Files\File;
|
use CodeIgniter\Files\File;
|
||||||
use CodeIgniter\HTTP\Files\UploadedFile;
|
use CodeIgniter\HTTP\Files\UploadedFile;
|
||||||
use CodeIgniter\HTTP\URI;
|
|
||||||
use CodeIgniter\I18n\Time;
|
use CodeIgniter\I18n\Time;
|
||||||
use Config\Images;
|
use Config\Images;
|
||||||
use Exception;
|
use Exception;
|
||||||
|
@ -23,6 +23,7 @@ return [
|
|||||||
'back_to_episodes' => 'Back to episodes of {podcast}',
|
'back_to_episodes' => 'Back to episodes of {podcast}',
|
||||||
'comments' => 'Comments',
|
'comments' => 'Comments',
|
||||||
'activity' => 'Activity',
|
'activity' => 'Activity',
|
||||||
|
'chapters' => 'Chapters',
|
||||||
'description' => 'Episode description',
|
'description' => 'Episode description',
|
||||||
'number_of_comments' => '{numberOfComments, plural,
|
'number_of_comments' => '{numberOfComments, plural,
|
||||||
one {# comment}
|
one {# comment}
|
||||||
@ -42,4 +43,5 @@ return [
|
|||||||
'publish' => 'Publish',
|
'publish' => 'Publish',
|
||||||
'publish_edit' => 'Edit publication',
|
'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;
|
namespace Modules\Media\Entities;
|
||||||
|
|
||||||
|
use CodeIgniter\Files\File;
|
||||||
|
use Exception;
|
||||||
|
|
||||||
class Chapters extends BaseMedia
|
class Chapters extends BaseMedia
|
||||||
{
|
{
|
||||||
protected string $type = 'chapters';
|
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: {
|
backgroundColor: {
|
||||||
base: "hsl(var(--color-background-base) / <alpha-value>)",
|
base: "hsl(var(--color-background-base) / <alpha-value>)",
|
||||||
elevated: "hsl(var(--color-background-elevated) / <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: "hsl(var(--color-background-navigation) / <alpha-value>)",
|
||||||
"navigation-active":
|
"navigation-active":
|
||||||
"hsl(var(--color-background-navigation-active) / <alpha-value>)",
|
"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'),
|
'label' => lang('Episode.activity'),
|
||||||
'labelInfo' => $episode->posts_count,
|
'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 {
|
} else {
|
||||||
$navigationItems = [
|
$navigationItems = [
|
||||||
@ -25,8 +30,15 @@ if ($episode->publication_status === 'published') {
|
|||||||
'label' => lang('Episode.activity'),
|
'label' => lang('Episode.activity'),
|
||||||
'labelInfo' => $episode->posts_count,
|
'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">
|
<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): ?>
|
<?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