mirror of
https://code.castopod.org/adaures/castopod
synced 2025-04-18 20:41:18 +00:00
feat(plugins): add html field type + CodeEditor component + rework html head generation
update php and js packages to latest
This commit is contained in:
parent
b869acb3a9
commit
8cf9c6dc83
@ -37,7 +37,7 @@ if (! function_exists('view')) {
|
||||
$renderer = single_service('renderer', $path);
|
||||
|
||||
$saveData = config('View')
|
||||
->saveData;
|
||||
->saveData;
|
||||
|
||||
if (array_key_exists('saveData', $options)) {
|
||||
$saveData = (bool) $options['saveData'];
|
||||
|
@ -67,7 +67,7 @@ class Filters extends BaseConfig
|
||||
/**
|
||||
* List of filter aliases that are always applied before and after every request.
|
||||
*
|
||||
* @var array<string, array<string, array<string, string|array<string>>>>>|array<string, list<string>>
|
||||
* @var array<string, array<string, array<string, string|array<string>>>>|array<string, list<string>>
|
||||
*/
|
||||
public array $globals = [
|
||||
'before' => [
|
||||
|
@ -25,7 +25,7 @@ class Generators extends BaseConfig
|
||||
*
|
||||
* YOU HAVE BEEN WARNED!
|
||||
*
|
||||
* @var array<string, string>
|
||||
* @var array<string, string|array<string,string>>
|
||||
*/
|
||||
public array $views = [
|
||||
'make:cell' => [
|
||||
|
@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace Config;
|
||||
|
||||
use App\Libraries\Breadcrumb;
|
||||
use App\Libraries\HtmlHead;
|
||||
use App\Libraries\Negotiate;
|
||||
use App\Libraries\Router;
|
||||
use CodeIgniter\Config\BaseService;
|
||||
@ -66,4 +67,13 @@ class Services extends BaseService
|
||||
|
||||
return new Breadcrumb();
|
||||
}
|
||||
|
||||
public static function html_head(bool $getShared = true): HtmlHead
|
||||
{
|
||||
if ($getShared) {
|
||||
return self::getSharedInstance('html_head');
|
||||
}
|
||||
|
||||
return new HtmlHead();
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace Config;
|
||||
|
||||
use App\Views\Decorators\SiteHead;
|
||||
use CodeIgniter\Config\View as BaseView;
|
||||
use CodeIgniter\View\ViewDecoratorInterface;
|
||||
use ViewComponents\Decorator;
|
||||
@ -54,5 +53,5 @@ class View extends BaseView
|
||||
*
|
||||
* @var list<class-string<ViewDecoratorInterface>>
|
||||
*/
|
||||
public array $decorators = [Decorator::class, SiteHead::class];
|
||||
public array $decorators = [Decorator::class];
|
||||
}
|
||||
|
@ -31,10 +31,10 @@ class ActorController extends FediverseActorController
|
||||
}
|
||||
|
||||
helper(['form', 'components', 'svg']);
|
||||
// @phpstan-ignore-next-line
|
||||
set_follow_metatags($this->actor);
|
||||
$data = [
|
||||
// @phpstan-ignore-next-line
|
||||
'metatags' => get_follow_metatags($this->actor),
|
||||
'actor' => $this->actor,
|
||||
'actor' => $this->actor,
|
||||
];
|
||||
|
||||
return view('podcast/follow', $data);
|
||||
|
@ -164,10 +164,10 @@ class CreditsController extends BaseController
|
||||
}
|
||||
}
|
||||
|
||||
set_page_metatags($page);
|
||||
$data = [
|
||||
'metatags' => get_page_metatags($page),
|
||||
'page' => $page,
|
||||
'credits' => $credits,
|
||||
'page' => $page,
|
||||
'credits' => $credits,
|
||||
];
|
||||
|
||||
$found = view('pages/credits', $data);
|
||||
|
@ -96,12 +96,12 @@ class EpisodeCommentController extends BaseController
|
||||
);
|
||||
|
||||
if (! ($cachedView = cache($cacheName))) {
|
||||
set_episode_comment_metatags($this->comment);
|
||||
$data = [
|
||||
'metatags' => get_episode_comment_metatags($this->comment),
|
||||
'podcast' => $this->podcast,
|
||||
'actor' => $this->actor,
|
||||
'episode' => $this->episode,
|
||||
'comment' => $this->comment,
|
||||
'podcast' => $this->podcast,
|
||||
'actor' => $this->actor,
|
||||
'episode' => $this->episode,
|
||||
'comment' => $this->comment,
|
||||
];
|
||||
|
||||
// if user is logged in then send to the authenticated activity view
|
||||
|
@ -86,10 +86,10 @@ class EpisodeController extends BaseController
|
||||
);
|
||||
|
||||
if (! ($cachedView = cache($cacheName))) {
|
||||
set_episode_metatags($this->episode);
|
||||
$data = [
|
||||
'metatags' => get_episode_metatags($this->episode),
|
||||
'podcast' => $this->podcast,
|
||||
'episode' => $this->episode,
|
||||
'podcast' => $this->podcast,
|
||||
'episode' => $this->episode,
|
||||
];
|
||||
|
||||
$secondsToNextUnpublishedEpisode = (new EpisodeModel())->getSecondsToNextUnpublishedEpisode(
|
||||
@ -135,10 +135,10 @@ class EpisodeController extends BaseController
|
||||
);
|
||||
|
||||
if (! ($cachedView = cache($cacheName))) {
|
||||
set_episode_metatags($this->episode);
|
||||
$data = [
|
||||
'metatags' => get_episode_metatags($this->episode),
|
||||
'podcast' => $this->podcast,
|
||||
'episode' => $this->episode,
|
||||
'podcast' => $this->podcast,
|
||||
'episode' => $this->episode,
|
||||
];
|
||||
|
||||
$secondsToNextUnpublishedEpisode = (new EpisodeModel())->getSecondsToNextUnpublishedEpisode(
|
||||
@ -184,13 +184,13 @@ class EpisodeController extends BaseController
|
||||
);
|
||||
|
||||
if (! ($cachedView = cache($cacheName))) {
|
||||
// get chapters from json file
|
||||
set_episode_metatags($this->episode);
|
||||
$data = [
|
||||
'metatags' => get_episode_metatags($this->episode),
|
||||
'podcast' => $this->podcast,
|
||||
'episode' => $this->episode,
|
||||
'podcast' => $this->podcast,
|
||||
'episode' => $this->episode,
|
||||
];
|
||||
|
||||
// get chapters from json file
|
||||
if (isset($this->episode->chapters->file_key)) {
|
||||
/** @var FileManagerInterface $fileManager */
|
||||
$fileManager = service('file_manager');
|
||||
@ -243,13 +243,13 @@ class EpisodeController extends BaseController
|
||||
);
|
||||
|
||||
if (! ($cachedView = cache($cacheName))) {
|
||||
// get transcript from json file
|
||||
set_episode_metatags($this->episode);
|
||||
$data = [
|
||||
'metatags' => get_episode_metatags($this->episode),
|
||||
'podcast' => $this->podcast,
|
||||
'episode' => $this->episode,
|
||||
'podcast' => $this->podcast,
|
||||
'episode' => $this->episode,
|
||||
];
|
||||
|
||||
// get transcript from json file
|
||||
if ($this->episode->transcript !== null) {
|
||||
$data['transcript'] = $this->episode->transcript;
|
||||
|
||||
|
@ -32,9 +32,9 @@ class HomeController extends BaseController
|
||||
return redirect()->route('podcast-activity', [$allPodcasts[0]->handle]);
|
||||
}
|
||||
|
||||
set_home_metatags();
|
||||
// default behavior: list all podcasts on home page
|
||||
$data = [
|
||||
'metatags' => get_home_metatags(),
|
||||
'podcasts' => $allPodcasts,
|
||||
'sortBy' => $sortBy,
|
||||
];
|
||||
|
@ -49,9 +49,9 @@ class PageController extends BaseController
|
||||
);
|
||||
|
||||
if (! ($found = cache($cacheName))) {
|
||||
set_page_metatags($this->page);
|
||||
$data = [
|
||||
'metatags' => get_page_metatags($this->page),
|
||||
'page' => $this->page,
|
||||
'page' => $this->page,
|
||||
];
|
||||
|
||||
$found = view('pages/page', $data);
|
||||
|
@ -78,10 +78,10 @@ class PodcastController extends BaseController
|
||||
);
|
||||
|
||||
if (! ($cachedView = cache($cacheName))) {
|
||||
set_podcast_metatags($this->podcast, 'activity');
|
||||
$data = [
|
||||
'metatags' => get_podcast_metatags($this->podcast, 'activity'),
|
||||
'podcast' => $this->podcast,
|
||||
'posts' => (new PostModel())->getActorPublishedPosts($this->podcast->actor_id),
|
||||
'podcast' => $this->podcast,
|
||||
'posts' => (new PostModel())->getActorPublishedPosts($this->podcast->actor_id),
|
||||
];
|
||||
|
||||
// if user is logged in then send to the authenticated activity view
|
||||
@ -128,10 +128,10 @@ class PodcastController extends BaseController
|
||||
if (! ($cachedView = cache($cacheName))) {
|
||||
$stats = (new EpisodeModel())->getPodcastStats($this->podcast->id);
|
||||
|
||||
set_podcast_metatags($this->podcast, 'about');
|
||||
$data = [
|
||||
'metatags' => get_podcast_metatags($this->podcast, 'about'),
|
||||
'podcast' => $this->podcast,
|
||||
'stats' => $stats,
|
||||
'podcast' => $this->podcast,
|
||||
'stats' => $stats,
|
||||
];
|
||||
|
||||
// // if user is logged in then send to the authenticated activity view
|
||||
@ -245,8 +245,8 @@ class PodcastController extends BaseController
|
||||
];
|
||||
}
|
||||
|
||||
set_podcast_metatags($this->podcast, 'episodes');
|
||||
$data = [
|
||||
'metatags' => get_podcast_metatags($this->podcast, 'episodes'),
|
||||
'podcast' => $this->podcast,
|
||||
'episodesNav' => $episodesNavigation,
|
||||
'activeQuery' => $activeQuery,
|
||||
@ -315,9 +315,9 @@ class PodcastController extends BaseController
|
||||
|
||||
public function links(): string
|
||||
{
|
||||
set_podcast_metatags($this->podcast, 'links');
|
||||
return view('podcast/links', [
|
||||
'metatags' => get_podcast_metatags($this->podcast, 'links'),
|
||||
'podcast' => $this->podcast,
|
||||
'podcast' => $this->podcast,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
@ -45,6 +45,7 @@ class PostController extends FediversePostController
|
||||
#[Override]
|
||||
public function _remap(string $method, string ...$params): mixed
|
||||
{
|
||||
|
||||
if (
|
||||
! ($podcast = (new PodcastModel())->getPodcastByHandle($params[0])) instanceof Podcast
|
||||
) {
|
||||
@ -54,16 +55,21 @@ class PostController extends FediversePostController
|
||||
$this->podcast = $podcast;
|
||||
$this->actor = $this->podcast->actor;
|
||||
|
||||
if (
|
||||
count($params) > 1 &&
|
||||
($post = (new PostModel())->getPostById($params[1])) instanceof CastopodPost
|
||||
) {
|
||||
$this->post = $post;
|
||||
|
||||
unset($params[0]);
|
||||
unset($params[1]);
|
||||
if (count($params) <= 1) {
|
||||
throw PageNotFoundException::forPageNotFound();
|
||||
}
|
||||
|
||||
if (
|
||||
! ($post = (new PostModel())->getPostById($params[1])) instanceof CastopodPost
|
||||
) {
|
||||
throw PageNotFoundException::forPageNotFound();
|
||||
}
|
||||
|
||||
$this->post = $post;
|
||||
|
||||
unset($params[0]);
|
||||
unset($params[1]);
|
||||
|
||||
return $this->{$method}(...$params);
|
||||
}
|
||||
|
||||
@ -74,10 +80,6 @@ class PostController extends FediversePostController
|
||||
$this->registerPodcastWebpageHit($this->podcast->id);
|
||||
}
|
||||
|
||||
if (! $this->post instanceof CastopodPost) {
|
||||
throw PageNotFoundException::forPageNotFound();
|
||||
}
|
||||
|
||||
$cacheName = implode(
|
||||
'_',
|
||||
array_filter([
|
||||
@ -91,10 +93,10 @@ class PostController extends FediversePostController
|
||||
);
|
||||
|
||||
if (! ($cachedView = cache($cacheName))) {
|
||||
set_post_metatags($this->post);
|
||||
$data = [
|
||||
'metatags' => get_post_metatags($this->post),
|
||||
'post' => $this->post,
|
||||
'podcast' => $this->podcast,
|
||||
'post' => $this->post,
|
||||
'podcast' => $this->podcast,
|
||||
];
|
||||
|
||||
// if user is logged in then send to the authenticated activity view
|
||||
@ -254,12 +256,12 @@ class PostController extends FediversePostController
|
||||
$this->registerPodcastWebpageHit($this->podcast->id);
|
||||
}
|
||||
|
||||
set_remote_actions_metatags($this->post, $action);
|
||||
$data = [
|
||||
'metatags' => get_remote_actions_metatags($this->post, $action),
|
||||
'podcast' => $this->podcast,
|
||||
'actor' => $this->actor,
|
||||
'post' => $this->post,
|
||||
'action' => $action,
|
||||
'podcast' => $this->podcast,
|
||||
'actor' => $this->actor,
|
||||
'post' => $this->post,
|
||||
'action' => $action,
|
||||
];
|
||||
|
||||
helper('form');
|
||||
|
@ -36,7 +36,7 @@ use Modules\Media\Models\MediaModel;
|
||||
* @property string $type
|
||||
* @property int|null $media_id
|
||||
* @property Video|Audio|null $media
|
||||
* @property array|null $metadata
|
||||
* @property array<mixed>|null $metadata
|
||||
* @property string $status
|
||||
* @property string $logs
|
||||
* @property User $user
|
||||
@ -136,7 +136,7 @@ class BaseClip extends Entity
|
||||
$media = new Audio([
|
||||
'file_key' => $fileKey,
|
||||
'language_code' => $this->getPodcast()
|
||||
->language_code,
|
||||
->language_code,
|
||||
'uploaded_by' => $this->attributes['updated_by'],
|
||||
'updated_by' => $this->attributes['updated_by'],
|
||||
]);
|
||||
|
@ -16,7 +16,7 @@ use Modules\Media\Models\MediaModel;
|
||||
use Override;
|
||||
|
||||
/**
|
||||
* @property array $theme
|
||||
* @property array{name:string,preview:string} $theme
|
||||
* @property string $format
|
||||
*/
|
||||
class VideoClip extends BaseClip
|
||||
@ -37,7 +37,7 @@ class VideoClip extends BaseClip
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $theme
|
||||
* @param array{name:string,preview:string} $theme
|
||||
*/
|
||||
public function setTheme(array $theme): self
|
||||
{
|
||||
@ -75,7 +75,7 @@ class VideoClip extends BaseClip
|
||||
$video = new Video([
|
||||
'file_key' => $fileKey,
|
||||
'language_code' => $this->getPodcast()
|
||||
->language_code,
|
||||
->language_code,
|
||||
'uploaded_by' => $this->attributes['created_by'],
|
||||
'updated_by' => $this->attributes['created_by'],
|
||||
]);
|
||||
|
@ -191,10 +191,10 @@ class Episode extends Entity
|
||||
$this->audio_url = url_to(
|
||||
'episode-audio',
|
||||
$this->getPodcast()
|
||||
->handle,
|
||||
->handle,
|
||||
$this->slug,
|
||||
$this->getAudio()
|
||||
->file_extension
|
||||
->file_extension
|
||||
);
|
||||
|
||||
$this->audio_opengraph_url = $this->audio_url . '?_from=-+Open+Graph+-';
|
||||
@ -219,7 +219,7 @@ class Episode extends Entity
|
||||
$cover = new Image([
|
||||
'file_key' => 'podcasts/' . $this->getPodcast()->handle . '/' . $this->attributes['slug'] . '.' . $file->getExtension(),
|
||||
'sizes' => config('Images')
|
||||
->podcastCoverSizes,
|
||||
->podcastCoverSizes,
|
||||
'uploaded_by' => $this->attributes['updated_by'],
|
||||
'updated_by' => $this->attributes['updated_by'],
|
||||
]);
|
||||
@ -302,7 +302,7 @@ class Episode extends Entity
|
||||
$transcript = new Transcript([
|
||||
'file_key' => 'podcasts/' . $this->getPodcast()->handle . '/' . $this->attributes['slug'] . '-transcript.' . $file->getExtension(),
|
||||
'language_code' => $this->getPodcast()
|
||||
->language_code,
|
||||
->language_code,
|
||||
'uploaded_by' => $this->attributes['updated_by'],
|
||||
'updated_by' => $this->attributes['updated_by'],
|
||||
]);
|
||||
@ -339,7 +339,7 @@ class Episode extends Entity
|
||||
$chapters = new Chapters([
|
||||
'file_key' => 'podcasts/' . $this->getPodcast()->handle . '/' . $this->attributes['slug'] . '-chapters' . '.' . $file->getExtension(),
|
||||
'language_code' => $this->getPodcast()
|
||||
->language_code,
|
||||
->language_code,
|
||||
'uploaded_by' => $this->attributes['updated_by'],
|
||||
'updated_by' => $this->attributes['updated_by'],
|
||||
]);
|
||||
|
@ -72,7 +72,7 @@ class Person extends Entity
|
||||
$avatar = new Image([
|
||||
'file_key' => 'persons/' . $this->attributes['unique_name'] . '.' . $file->getExtension(),
|
||||
'sizes' => config('Images')
|
||||
->personAvatarSizes,
|
||||
->personAvatarSizes,
|
||||
'uploaded_by' => $this->attributes['updated_by'],
|
||||
'updated_by' => $this->attributes['updated_by'],
|
||||
]);
|
||||
|
@ -231,7 +231,7 @@ class Podcast extends Entity
|
||||
$cover = new Image([
|
||||
'file_key' => 'podcasts/' . $this->attributes['handle'] . '/cover.' . $file->getExtension(),
|
||||
'sizes' => config('Images')
|
||||
->podcastCoverSizes,
|
||||
->podcastCoverSizes,
|
||||
'uploaded_by' => $this->attributes['updated_by'],
|
||||
'updated_by' => $this->attributes['updated_by'],
|
||||
]);
|
||||
@ -274,7 +274,7 @@ class Podcast extends Entity
|
||||
$banner = new Image([
|
||||
'file_key' => 'podcasts/' . $this->attributes['handle'] . '/banner.' . $file->getExtension(),
|
||||
'sizes' => config('Images')
|
||||
->podcastBannerSizes,
|
||||
->podcastBannerSizes,
|
||||
'uploaded_by' => $this->attributes['updated_by'],
|
||||
'updated_by' => $this->attributes['updated_by'],
|
||||
]);
|
||||
|
@ -105,7 +105,7 @@ if (! function_exists('publication_pill')) {
|
||||
|
||||
$label = lang('Episode.publication_status.' . $publicationStatus);
|
||||
|
||||
// @icon('error-warning-fill')
|
||||
// @icon("error-warning-fill")
|
||||
return '<x-Pill ' . ($title === '' ? '' : 'title="' . $title . '"') . ' variant="' . $variant . '" class="' . $customClass .
|
||||
'">' . $label . ($publicationStatus === 'with_podcast' ? icon('error-warning-fill', [
|
||||
'class' => 'flex-shrink-0 ml-1 text-lg',
|
||||
@ -129,20 +129,20 @@ if (! function_exists('publication_button')) {
|
||||
$label = lang('Episode.publish');
|
||||
$route = route_to('episode-publish', $podcastId, $episodeId);
|
||||
$variant = 'primary';
|
||||
$iconLeft = 'upload-cloud-fill'; // @icon('upload-cloud-fill')
|
||||
$iconLeft = 'upload-cloud-fill'; // @icon("upload-cloud-fill")
|
||||
break;
|
||||
case 'with_podcast':
|
||||
case 'scheduled':
|
||||
$label = lang('Episode.publish_edit');
|
||||
$route = route_to('episode-publish_edit', $podcastId, $episodeId);
|
||||
$variant = 'warning';
|
||||
$iconLeft = 'upload-cloud-fill'; // @icon('upload-cloud-fill')
|
||||
$iconLeft = 'upload-cloud-fill'; // @icon("upload-cloud-fill")
|
||||
break;
|
||||
case 'published':
|
||||
$label = lang('Episode.unpublish');
|
||||
$route = route_to('episode-unpublish', $podcastId, $episodeId);
|
||||
$variant = 'danger';
|
||||
$iconLeft = 'cloud-off-fill'; // @icon('cloud-off-fill')
|
||||
$iconLeft = 'cloud-off-fill'; // @icon("cloud-off-fill")
|
||||
break;
|
||||
default:
|
||||
$label = '';
|
||||
|
@ -210,7 +210,7 @@ if (! function_exists('get_podcast_banner')) {
|
||||
)->podcastBannerDefaultPaths['default'];
|
||||
|
||||
$sizes = config('Images')
|
||||
->podcastBannerSizes;
|
||||
->podcastBannerSizes;
|
||||
|
||||
$sizeConfig = $sizes[$size];
|
||||
helper('filesystem');
|
||||
@ -231,7 +231,7 @@ if (! function_exists('get_podcast_banner_mimetype')) {
|
||||
{
|
||||
if (! $podcast->banner instanceof Image) {
|
||||
$sizes = config('Images')
|
||||
->podcastBannerSizes;
|
||||
->podcastBannerSizes;
|
||||
|
||||
$sizeConfig = $sizes[$size];
|
||||
helper('filesystem');
|
||||
@ -252,10 +252,10 @@ if (! function_exists('get_avatar_url')) {
|
||||
{
|
||||
if (! $person->avatar instanceof Image) {
|
||||
$defaultAvatarPath = config('Images')
|
||||
->avatarDefaultPath;
|
||||
->avatarDefaultPath;
|
||||
|
||||
$sizes = config('Images')
|
||||
->personAvatarSizes;
|
||||
->personAvatarSizes;
|
||||
|
||||
$sizeConfig = $sizes[$size];
|
||||
|
||||
|
@ -422,7 +422,7 @@ if (! function_exists('add_category_tag')) {
|
||||
/**
|
||||
* Adds <itunes:category> and <category> tags to node for a given category
|
||||
*/
|
||||
function add_category_tag(SimpleXMLElement $node, Category $category): void
|
||||
function add_category_tag(RssFeed $node, Category $category): void
|
||||
{
|
||||
$itunesCategory = $node->addChild('category', null, RssFeed::ITUNES_NAMESPACE);
|
||||
$itunesCategory->addAttribute(
|
||||
@ -441,70 +441,3 @@ if (! function_exists('add_category_tag')) {
|
||||
$node->addChild('category', $category->apple_category);
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('rss_to_array')) {
|
||||
/**
|
||||
* Converts XML to array
|
||||
*
|
||||
* FIXME: param should be SimpleRSSElement
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
function rss_to_array(SimpleXMLElement $rssNode): array
|
||||
{
|
||||
$nameSpaces = ['', 'http://www.itunes.com/dtds/podcast-1.0.dtd', 'https://podcastindex.org/namespace/1.0'];
|
||||
$arrayNode = [];
|
||||
$arrayNode['name'] = $rssNode->getName();
|
||||
$arrayNode['namespace'] = $rssNode->getNamespaces(false);
|
||||
foreach ($rssNode->attributes() as $key => $value) {
|
||||
$arrayNode['attributes'][$key] = (string) $value;
|
||||
}
|
||||
|
||||
$textcontent = trim((string) $rssNode);
|
||||
if (strlen($textcontent) > 0) {
|
||||
$arrayNode['content'] = $textcontent;
|
||||
}
|
||||
|
||||
foreach ($nameSpaces as $currentNameSpace) {
|
||||
foreach ($rssNode->children($currentNameSpace) as $childXmlNode) {
|
||||
$arrayNode['elements'][] = rss_to_array($childXmlNode);
|
||||
}
|
||||
}
|
||||
|
||||
return $arrayNode;
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('array_to_rss')) {
|
||||
/**
|
||||
* Inserts array (converted to XML node) in XML node
|
||||
*
|
||||
* @param array<string, mixed> $arrayNode
|
||||
* @param RssFeed $xmlNode The XML parent node where this arrayNode should be attached
|
||||
*/
|
||||
function array_to_rss(array $arrayNode, RssFeed &$xmlNode): RssFeed
|
||||
{
|
||||
if (array_key_exists('elements', $arrayNode)) {
|
||||
foreach ($arrayNode['elements'] as $childArrayNode) {
|
||||
$childXmlNode = $xmlNode->addChild(
|
||||
$childArrayNode['name'],
|
||||
$childArrayNode['content'] ?? null,
|
||||
$childArrayNode['namespace'] === []
|
||||
? null
|
||||
: current($childArrayNode['namespace'])
|
||||
);
|
||||
if (array_key_exists('attributes', $childArrayNode)) {
|
||||
foreach (
|
||||
$childArrayNode['attributes'] as $attributeKey => $attributeValue
|
||||
) {
|
||||
$childXmlNode->addAttribute($attributeKey, $attributeValue);
|
||||
}
|
||||
}
|
||||
|
||||
array_to_rss($childArrayNode, $childXmlNode);
|
||||
}
|
||||
}
|
||||
|
||||
return $xmlNode;
|
||||
}
|
||||
}
|
||||
|
@ -8,19 +8,19 @@ use App\Entities\EpisodeComment;
|
||||
use App\Entities\Page;
|
||||
use App\Entities\Podcast;
|
||||
use App\Entities\Post;
|
||||
use Melbahja\Seo\MetaTags;
|
||||
use App\Libraries\HtmlHead;
|
||||
use Melbahja\Seo\Schema;
|
||||
use Melbahja\Seo\Schema\Thing;
|
||||
use Modules\Fediverse\Entities\PreviewCard;
|
||||
|
||||
/**
|
||||
* @copyright 2021 Ad Aures
|
||||
* @copyright 2024 Ad Aures
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
if (! function_exists('get_podcast_metatags')) {
|
||||
function get_podcast_metatags(Podcast $podcast, string $page): string
|
||||
if (! function_exists('set_podcast_metatags')) {
|
||||
function set_podcast_metatags(Podcast $podcast, string $page): void
|
||||
{
|
||||
$category = '';
|
||||
if ($podcast->category->parent_id !== null) {
|
||||
@ -48,10 +48,11 @@ if (! function_exists('get_podcast_metatags')) {
|
||||
])
|
||||
);
|
||||
|
||||
$metatags = new MetaTags();
|
||||
/** @var HtmlHead $head */
|
||||
$head = service('html_head');
|
||||
|
||||
$metatags
|
||||
->title($podcast->title . ' (@' . $podcast->handle . ') • ' . lang('Podcast.' . $page))
|
||||
$head
|
||||
->title(sprintf('%s (@%s) • %s', $podcast->title, $podcast->handle, lang('Podcast.' . $page)))
|
||||
->description(esc($podcast->description))
|
||||
->image((string) $podcast->cover->og_url)
|
||||
->canonical((string) current_url())
|
||||
@ -59,20 +60,18 @@ if (! function_exists('get_podcast_metatags')) {
|
||||
->og('image:height', (string) config('Images')->podcastCoverSizes['og']['height'])
|
||||
->og('locale', $podcast->language_code)
|
||||
->og('site_name', esc(service('settings')->get('App.siteName')))
|
||||
->push('link', [
|
||||
->tag('link', null, [
|
||||
'rel' => 'alternate',
|
||||
'type' => 'application/activity+json',
|
||||
'href' => url_to('podcast-activity', esc($podcast->handle)),
|
||||
]);
|
||||
|
||||
return '<link type="application/rss+xml" rel="alternate" title="' . esc(
|
||||
$podcast->title
|
||||
) . '" href="' . $podcast->feed_url . '" />' . PHP_EOL . $metatags->__toString() . PHP_EOL . $schema->__toString();
|
||||
])->appendRawContent('<link type="application/rss+xml" rel="alternate" title="' . esc(
|
||||
$podcast->title
|
||||
) . '" href="' . $podcast->feed_url . '" />' . $schema);
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('get_episode_metatags')) {
|
||||
function get_episode_metatags(Episode $episode): string
|
||||
if (! function_exists('set_episode_metatags')) {
|
||||
function set_episode_metatags(Episode $episode): void
|
||||
{
|
||||
$schema = new Schema(
|
||||
new Thing('PodcastEpisode', [
|
||||
@ -80,7 +79,7 @@ if (! function_exists('get_episode_metatags')) {
|
||||
'name' => $episode->title,
|
||||
'image' => $episode->cover->feed_url,
|
||||
'description' => $episode->description,
|
||||
'datePublished' => $episode->published_at->format(DATE_ISO8601),
|
||||
'datePublished' => $episode->published_at->format(DATE_ATOM),
|
||||
'timeRequired' => iso8601_duration($episode->audio->duration),
|
||||
'duration' => iso8601_duration($episode->audio->duration),
|
||||
'associatedMedia' => new Thing('MediaObject', [
|
||||
@ -93,9 +92,10 @@ if (! function_exists('get_episode_metatags')) {
|
||||
])
|
||||
);
|
||||
|
||||
$metatags = new MetaTags();
|
||||
/** @var HtmlHead $head */
|
||||
$head = service('html_head');
|
||||
|
||||
$metatags
|
||||
$head
|
||||
->title($episode->title)
|
||||
->description(esc($episode->description))
|
||||
->image((string) $episode->cover->og_url, 'player')
|
||||
@ -106,35 +106,34 @@ if (! function_exists('get_episode_metatags')) {
|
||||
->og('locale', $episode->podcast->language_code)
|
||||
->og('audio', $episode->audio_opengraph_url)
|
||||
->og('audio:type', $episode->audio->file_mimetype)
|
||||
->meta('article:published_time', $episode->published_at->format(DATE_ISO8601))
|
||||
->meta('article:modified_time', $episode->updated_at->format(DATE_ISO8601))
|
||||
->meta('article:published_time', $episode->published_at->format(DATE_ATOM))
|
||||
->meta('article:modified_time', $episode->updated_at->format(DATE_ATOM))
|
||||
->twitter('audio:partner', $episode->podcast->publisher ?? '')
|
||||
->twitter('audio:artist_name', esc($episode->podcast->owner_name))
|
||||
->twitter('player', $episode->getEmbedUrl('light'))
|
||||
->twitter('player:width', (string) config('Embed')->width)
|
||||
->twitter('player:height', (string) config('Embed')->height)
|
||||
->push('link', [
|
||||
->tag('link', null, [
|
||||
'rel' => 'alternate',
|
||||
'type' => 'application/activity+json',
|
||||
'href' => url_to('episode', $episode->podcast->handle, $episode->slug),
|
||||
]);
|
||||
|
||||
return $metatags->__toString() . PHP_EOL . '<link rel="alternate" type="application/json+oembed" href="' . base_url(
|
||||
route_to('episode-oembed-json', $episode->podcast->handle, $episode->slug)
|
||||
) . '" title="' . esc(
|
||||
$episode->title
|
||||
) . ' oEmbed json" />' . PHP_EOL . '<link rel="alternate" type="text/xml+oembed" href="' . base_url(
|
||||
route_to('episode-oembed-xml', $episode->podcast->handle, $episode->slug)
|
||||
) . '" title="' . esc($episode->title) . ' oEmbed xml" />' . PHP_EOL . $schema->__toString();
|
||||
'href' => $episode->link,
|
||||
])
|
||||
->appendRawContent('<link rel="alternate" type="application/json+oembed" href="' . base_url(
|
||||
route_to('episode-oembed-json', $episode->podcast->handle, $episode->slug)
|
||||
) . '" title="' . esc(
|
||||
$episode->title
|
||||
) . ' oEmbed json" />' . '<link rel="alternate" type="text/xml+oembed" href="' . base_url(
|
||||
route_to('episode-oembed-xml', $episode->podcast->handle, $episode->slug)
|
||||
) . '" title="' . esc($episode->title) . ' oEmbed xml" />' . $schema);
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('get_post_metatags')) {
|
||||
function get_post_metatags(Post $post): string
|
||||
if (! function_exists('set_post_metatags')) {
|
||||
function set_post_metatags(Post $post): void
|
||||
{
|
||||
$socialMediaPosting = new Thing('SocialMediaPosting', [
|
||||
'@id' => url_to('post', esc($post->actor->username), $post->id),
|
||||
'datePublished' => $post->published_at->format(DATE_ISO8601),
|
||||
'datePublished' => $post->published_at->format(DATE_ATOM),
|
||||
'author' => new Thing('Person', [
|
||||
'name' => $post->actor->display_name,
|
||||
'url' => $post->actor->uri,
|
||||
@ -162,8 +161,10 @@ if (! function_exists('get_post_metatags')) {
|
||||
|
||||
$schema = new Schema($socialMediaPosting);
|
||||
|
||||
$metatags = new MetaTags();
|
||||
$metatags
|
||||
/** @var HtmlHead $head */
|
||||
$head = service('html_head');
|
||||
|
||||
$head
|
||||
->title(lang('Post.title', [
|
||||
'actorDisplayName' => $post->actor->display_name,
|
||||
]))
|
||||
@ -171,18 +172,16 @@ if (! function_exists('get_post_metatags')) {
|
||||
->image($post->actor->avatar_image_url)
|
||||
->canonical((string) current_url())
|
||||
->og('site_name', esc(service('settings')->get('App.siteName')))
|
||||
->push('link', [
|
||||
->tag('link', null, [
|
||||
'rel' => 'alternate',
|
||||
'type' => 'application/activity+json',
|
||||
'href' => url_to('post', esc($post->actor->username), $post->id),
|
||||
]);
|
||||
|
||||
return $metatags->__toString() . PHP_EOL . $schema->__toString();
|
||||
])->appendRawContent((string) $schema);
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('get_episode_comment_metatags')) {
|
||||
function get_episode_comment_metatags(EpisodeComment $episodeComment): string
|
||||
if (! function_exists('set_episode_comment_metatags')) {
|
||||
function set_episode_comment_metatags(EpisodeComment $episodeComment): void
|
||||
{
|
||||
$schema = new Schema(new Thing('SocialMediaPosting', [
|
||||
'@id' => url_to(
|
||||
@ -191,7 +190,7 @@ if (! function_exists('get_episode_comment_metatags')) {
|
||||
$episodeComment->episode->slug,
|
||||
$episodeComment->id
|
||||
),
|
||||
'datePublished' => $episodeComment->created_at->format(DATE_ISO8601),
|
||||
'datePublished' => $episodeComment->created_at->format(DATE_ATOM),
|
||||
'author' => new Thing('Person', [
|
||||
'name' => $episodeComment->actor->display_name,
|
||||
'url' => $episodeComment->actor->uri,
|
||||
@ -200,8 +199,10 @@ if (! function_exists('get_episode_comment_metatags')) {
|
||||
'upvoteCount' => $episodeComment->likes_count,
|
||||
]));
|
||||
|
||||
$metatags = new MetaTags();
|
||||
$metatags
|
||||
/** @var HtmlHead $head */
|
||||
$head = service('html_head');
|
||||
|
||||
$head
|
||||
->title(lang('Comment.title', [
|
||||
'actorDisplayName' => $episodeComment->actor->display_name,
|
||||
'episodeTitle' => $episodeComment->episode->title,
|
||||
@ -210,7 +211,7 @@ if (! function_exists('get_episode_comment_metatags')) {
|
||||
->image($episodeComment->actor->avatar_image_url)
|
||||
->canonical((string) current_url())
|
||||
->og('site_name', esc(service('settings')->get('App.siteName')))
|
||||
->push('link', [
|
||||
->tag('link', null, [
|
||||
'rel' => 'alternate',
|
||||
'type' => 'application/activity+json',
|
||||
'href' => url_to(
|
||||
@ -219,17 +220,16 @@ if (! function_exists('get_episode_comment_metatags')) {
|
||||
$episodeComment->episode->slug,
|
||||
$episodeComment->id
|
||||
),
|
||||
]);
|
||||
|
||||
return $metatags->__toString() . PHP_EOL . $schema->__toString();
|
||||
])->appendRawContent((string) $schema);
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('get_follow_metatags')) {
|
||||
function get_follow_metatags(Actor $actor): string
|
||||
if (! function_exists('set_follow_metatags')) {
|
||||
function set_follow_metatags(Actor $actor): void
|
||||
{
|
||||
$metatags = new MetaTags();
|
||||
$metatags
|
||||
/** @var HtmlHead $head */
|
||||
$head = service('html_head');
|
||||
$head
|
||||
->title(lang('Podcast.followTitle', [
|
||||
'actorDisplayName' => $actor->display_name,
|
||||
]))
|
||||
@ -237,16 +237,15 @@ if (! function_exists('get_follow_metatags')) {
|
||||
->image($actor->avatar_image_url)
|
||||
->canonical((string) current_url())
|
||||
->og('site_name', esc(service('settings')->get('App.siteName')));
|
||||
|
||||
return $metatags->__toString();
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('get_remote_actions_metatags')) {
|
||||
function get_remote_actions_metatags(Post $post, string $action): string
|
||||
if (! function_exists('set_remote_actions_metatags')) {
|
||||
function set_remote_actions_metatags(Post $post, string $action): void
|
||||
{
|
||||
$metatags = new MetaTags();
|
||||
$metatags
|
||||
/** @var HtmlHead $head */
|
||||
$head = service('html_head');
|
||||
$head
|
||||
->title(lang('Fediverse.' . $action . '.title', [
|
||||
'actorDisplayName' => $post->actor->display_name,
|
||||
],))
|
||||
@ -254,31 +253,30 @@ if (! function_exists('get_remote_actions_metatags')) {
|
||||
->image($post->actor->avatar_image_url)
|
||||
->canonical((string) current_url())
|
||||
->og('site_name', esc(service('settings')->get('App.siteName')));
|
||||
|
||||
return $metatags->__toString();
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('get_home_metatags')) {
|
||||
function get_home_metatags(): string
|
||||
if (! function_exists('set_home_metatags')) {
|
||||
function set_home_metatags(): void
|
||||
{
|
||||
$metatags = new MetaTags();
|
||||
$metatags
|
||||
/** @var HtmlHead $head */
|
||||
$head = service('html_head');
|
||||
$head
|
||||
->title(service('settings')->get('App.siteName'))
|
||||
->description(esc(service('settings')->get('App.siteDescription')))
|
||||
->image(get_site_icon_url('512'))
|
||||
->canonical((string) current_url())
|
||||
->og('site_name', esc(service('settings')->get('App.siteName')));
|
||||
|
||||
return $metatags->__toString();
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('get_page_metatags')) {
|
||||
function get_page_metatags(Page $page): string
|
||||
if (! function_exists('set_page_metatags')) {
|
||||
function set_page_metatags(Page $page): void
|
||||
{
|
||||
$metatags = new MetaTags();
|
||||
$metatags
|
||||
/** @var HtmlHead $head */
|
||||
$head = service('html_head');
|
||||
$head
|
||||
->title(
|
||||
$page->title . service('settings')->get('App.siteTitleSeparator') . service(
|
||||
'settings'
|
||||
@ -289,7 +287,6 @@ if (! function_exists('get_page_metatags')) {
|
||||
->canonical((string) current_url())
|
||||
->og('site_name', esc(service('settings')->get('App.siteName')));
|
||||
|
||||
return $metatags->__toString();
|
||||
}
|
||||
}
|
||||
|
||||
|
188
app/Libraries/HtmlHead.php
Normal file
188
app/Libraries/HtmlHead.php
Normal file
@ -0,0 +1,188 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Libraries;
|
||||
|
||||
use App\Controllers\WebmanifestController;
|
||||
use Override;
|
||||
use Stringable;
|
||||
|
||||
/**
|
||||
* Inspired by https://github.com/melbahja/seo
|
||||
*/
|
||||
class HtmlHead implements Stringable
|
||||
{
|
||||
protected ?string $title = null;
|
||||
|
||||
/**
|
||||
* @var array{name:string,value:string|null,attributes:array<string,string|null>}[]
|
||||
*/
|
||||
protected array $tags = [];
|
||||
|
||||
protected string $rawContent = '';
|
||||
|
||||
#[Override]
|
||||
public function __toString(): string
|
||||
{
|
||||
helper('misc');
|
||||
$this
|
||||
->tag('meta', null, [
|
||||
'charset' => 'UTF-8',
|
||||
])
|
||||
->meta('viewport', 'width=device-width, initial-scale=1.0')
|
||||
->tag('link', null, [
|
||||
'rel' => 'icon',
|
||||
'type' => 'image/x-icon',
|
||||
'href' => get_site_icon_url('ico'),
|
||||
])
|
||||
->tag('link', null, [
|
||||
'rel' => 'apple-touch-icon',
|
||||
'href' => get_site_icon_url('180'),
|
||||
])
|
||||
->tag('link', null, [
|
||||
'rel' => 'manifest',
|
||||
// @phpstan-ignore-next-line
|
||||
'href' => isset($podcast) ? route_to('podcast-webmanifest', esc($podcast->handle)) : route_to(
|
||||
'webmanifest'
|
||||
),
|
||||
])
|
||||
->meta(
|
||||
'theme-color',
|
||||
WebmanifestController::THEME_COLORS[service('settings')->get('App.theme')]['theme']
|
||||
)
|
||||
->tag('link', null, [
|
||||
'rel' => 'stylesheet',
|
||||
'type' => 'text/css',
|
||||
'href' => route_to('themes-colors-css'),
|
||||
])
|
||||
->appendRawContent(<<<HTML
|
||||
<script>
|
||||
// Check that service workers are supported
|
||||
if ('serviceWorker' in navigator) {
|
||||
// Use the window load event to keep the page load performant
|
||||
window.addEventListener('load', () => {
|
||||
navigator.serviceWorker.register('/sw.js');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
HTML);
|
||||
|
||||
if ($this->title) {
|
||||
$this->tag('title', esc($this->title));
|
||||
}
|
||||
|
||||
if (url_is(route_to('admin') . '*') || url_is(base_url(config('Auth')->gateway) . '*')) {
|
||||
// restricted admin and auth areas, do not index
|
||||
$this->meta('robots', 'noindex');
|
||||
} else {
|
||||
// public website, set siteHead hook only there
|
||||
service('plugins')
|
||||
->siteHead($this);
|
||||
}
|
||||
|
||||
$head = '<head>';
|
||||
foreach ($this->tags as $tag) {
|
||||
if ($tag['value'] === null) {
|
||||
$head .= <<<HTML
|
||||
<{$tag['name']}{$this->stringify_attributes($tag['attributes'])}/>
|
||||
HTML;
|
||||
} else {
|
||||
$head .= <<<HTML
|
||||
<{$tag['name']} {$this->stringify_attributes($tag['attributes'])}>{$tag['value']}</{$tag['name']}>
|
||||
HTML;
|
||||
}
|
||||
}
|
||||
|
||||
$head .= $this->rawContent . '</head>';
|
||||
|
||||
// reset head for next render
|
||||
$this->title = null;
|
||||
$this->tags = [];
|
||||
$this->rawContent = '';
|
||||
|
||||
return $head;
|
||||
}
|
||||
|
||||
public function title(string $title): self
|
||||
{
|
||||
$this->title = $title;
|
||||
return $this->meta('title', $title)
|
||||
->og('title', $title)
|
||||
->twitter('title', $title);
|
||||
}
|
||||
|
||||
public function description(string $desc): self
|
||||
{
|
||||
return $this->meta('description', $desc)
|
||||
->og('description', $desc)
|
||||
->twitter('description', $desc);
|
||||
}
|
||||
|
||||
public function image(string $url, string $card = 'summary_large_image'): self
|
||||
{
|
||||
return $this->og('image', $url)
|
||||
->twitter('card', $card)
|
||||
->twitter('image', $url);
|
||||
}
|
||||
|
||||
public function canonical(string $url): self
|
||||
{
|
||||
return $this->tag('link', null, [
|
||||
'rel' => 'canonical',
|
||||
'href' => $url,
|
||||
]);
|
||||
}
|
||||
|
||||
public function twitter(string $name, string $value): self
|
||||
{
|
||||
$this->meta("twitter:{$name}", $value);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,string|null> $attributes
|
||||
*/
|
||||
public function tag(string $name, ?string $value = null, array $attributes = []): self
|
||||
{
|
||||
$this->tags[] = [
|
||||
'name' => $name,
|
||||
'value' => $value,
|
||||
'attributes' => $attributes,
|
||||
];
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function meta(string $name, string $content): self
|
||||
{
|
||||
$this->tag('meta', null, [
|
||||
'name' => $name,
|
||||
'content' => $content,
|
||||
]);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function og(string $name, string $content): self
|
||||
{
|
||||
$this->meta('og:' . $name, $content);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function appendRawContent(string $content): self
|
||||
{
|
||||
$this->rawContent .= $content;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string|null> $attributes
|
||||
*/
|
||||
private function stringify_attributes(array $attributes): string
|
||||
{
|
||||
return stringify_attributes($attributes);
|
||||
}
|
||||
}
|
@ -229,8 +229,6 @@ class EpisodeCommentModel extends UuidModel
|
||||
$episodeComments . ' UNION ' . $episodePostsReplies . ' ORDER BY created_at ASC'
|
||||
);
|
||||
|
||||
// FIXME:?
|
||||
// @phpstan-ignore-next-line
|
||||
return $this->convertUuidFieldsToStrings(
|
||||
$allEpisodeComments->getCustomResultObject($this->tempReturnType),
|
||||
$this->tempReturnType
|
||||
|
@ -3,17 +3,17 @@
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @icon('funding:buymeacoffee')
|
||||
* @icon('funding:donorbox')
|
||||
* @icon('funding:gofundme')
|
||||
* @icon('funding:helloasso')
|
||||
* @icon('funding:indiegogo')
|
||||
* @icon('funding:kickstarter')
|
||||
* @icon('funding:kisskissbankbank')
|
||||
* @icon('funding:kofi')
|
||||
* @icon('funding:liberapay')
|
||||
* @icon('funding:patreon')
|
||||
* @icon('funding:paypal')
|
||||
* @icon('funding:tipeee')
|
||||
* @icon('funding:ulule')
|
||||
* @icon("funding:buymeacoffee")
|
||||
* @icon("funding:donorbox")
|
||||
* @icon("funding:gofundme")
|
||||
* @icon("funding:helloasso")
|
||||
* @icon("funding:indiegogo")
|
||||
* @icon("funding:kickstarter")
|
||||
* @icon("funding:kisskissbankbank")
|
||||
* @icon("funding:kofi")
|
||||
* @icon("funding:liberapay")
|
||||
* @icon("funding:patreon")
|
||||
* @icon("funding:paypal")
|
||||
* @icon("funding:tipeee")
|
||||
* @icon("funding:ulule")
|
||||
*/
|
||||
|
@ -3,46 +3,46 @@
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @icon('podcasting:amazon')
|
||||
* @icon('podcasting:antennapod')
|
||||
* @icon('podcasting:anytime')
|
||||
* @icon('podcasting:apple')
|
||||
* @icon('podcasting:blubrry')
|
||||
* @icon('podcasting:breez')
|
||||
* @icon('podcasting:castamatic')
|
||||
* @icon('podcasting:castbox')
|
||||
* @icon('podcasting:castopod')
|
||||
* @icon('podcasting:castro')
|
||||
* @icon('podcasting:deezer')
|
||||
* @icon('podcasting:episodes-fm')
|
||||
* @icon('podcasting:fountain')
|
||||
* @icon('podcasting:fyyd')
|
||||
* @icon('podcasting:gpodder')
|
||||
* @icon('podcasting:ivoox')
|
||||
* @icon('podcasting:listennotes')
|
||||
* @icon('podcasting:overcast')
|
||||
* @icon('podcasting:playerfm')
|
||||
* @icon('podcasting:plink')
|
||||
* @icon('podcasting:pocketcasts')
|
||||
* @icon('podcasting:podbean')
|
||||
* @icon('podcasting:podcastaddict')
|
||||
* @icon('podcasting:podcastguru')
|
||||
* @icon('podcasting:podcastindex')
|
||||
* @icon('podcasting:podchaser')
|
||||
* @icon('podcasting:podcloud')
|
||||
* @icon('podcasting:podfriend')
|
||||
* @icon('podcasting:podinstall')
|
||||
* @icon('podcasting:podlink')
|
||||
* @icon('podcasting:podlp')
|
||||
* @icon('podcasting:podnews')
|
||||
* @icon('podcasting:podtail')
|
||||
* @icon('podcasting:podverse')
|
||||
* @icon('podcasting:radiopublic')
|
||||
* @icon('podcasting:sphinxchat')
|
||||
* @icon('podcasting:spotify')
|
||||
* @icon('podcasting:spreaker')
|
||||
* @icon('podcasting:truefans')
|
||||
* @icon('podcasting:tsacdop')
|
||||
* @icon('podcasting:tunein')
|
||||
* @icon('podcasting:youtube-music')
|
||||
* @icon("podcasting:amazon")
|
||||
* @icon("podcasting:antennapod")
|
||||
* @icon("podcasting:anytime")
|
||||
* @icon("podcasting:apple")
|
||||
* @icon("podcasting:blubrry")
|
||||
* @icon("podcasting:breez")
|
||||
* @icon("podcasting:castamatic")
|
||||
* @icon("podcasting:castbox")
|
||||
* @icon("podcasting:castopod")
|
||||
* @icon("podcasting:castro")
|
||||
* @icon("podcasting:deezer")
|
||||
* @icon("podcasting:episodes-fm")
|
||||
* @icon("podcasting:fountain")
|
||||
* @icon("podcasting:fyyd")
|
||||
* @icon("podcasting:gpodder")
|
||||
* @icon("podcasting:ivoox")
|
||||
* @icon("podcasting:listennotes")
|
||||
* @icon("podcasting:overcast")
|
||||
* @icon("podcasting:playerfm")
|
||||
* @icon("podcasting:plink")
|
||||
* @icon("podcasting:pocketcasts")
|
||||
* @icon("podcasting:podbean")
|
||||
* @icon("podcasting:podcastaddict")
|
||||
* @icon("podcasting:podcastguru")
|
||||
* @icon("podcasting:podcastindex")
|
||||
* @icon("podcasting:podchaser")
|
||||
* @icon("podcasting:podcloud")
|
||||
* @icon("podcasting:podfriend")
|
||||
* @icon("podcasting:podinstall")
|
||||
* @icon("podcasting:podlink")
|
||||
* @icon("podcasting:podlp")
|
||||
* @icon("podcasting:podnews")
|
||||
* @icon("podcasting:podtail")
|
||||
* @icon("podcasting:podverse")
|
||||
* @icon("podcasting:radiopublic")
|
||||
* @icon("podcasting:sphinxchat")
|
||||
* @icon("podcasting:spotify")
|
||||
* @icon("podcasting:spreaker")
|
||||
* @icon("podcasting:truefans")
|
||||
* @icon("podcasting:tsacdop")
|
||||
* @icon("podcasting:tunein")
|
||||
* @icon("podcasting:youtube-music")
|
||||
*/
|
||||
|
@ -3,26 +3,26 @@
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @icon('social:bluesky')
|
||||
* @icon('social:discord')
|
||||
* @icon('social:facebook')
|
||||
* @icon('social:funkwhale')
|
||||
* @icon('social:instagram')
|
||||
* @icon('social:linkedin')
|
||||
* @icon('social:mastodon')
|
||||
* @icon('social:matrix')
|
||||
* @icon('social:misskey')
|
||||
* @icon('social:mobilizon')
|
||||
* @icon('social:peertube')
|
||||
* @icon('social:pixelfed')
|
||||
* @icon('social:pleroma')
|
||||
* @icon('social:plume')
|
||||
* @icon('social:slack')
|
||||
* @icon('social:telegram')
|
||||
* @icon('social:threads')
|
||||
* @icon('social:tiktok')
|
||||
* @icon('social:twitch')
|
||||
* @icon('social:writefreely')
|
||||
* @icon('social:x')
|
||||
* @icon('social:youtube')
|
||||
* @icon("social:bluesky")
|
||||
* @icon("social:discord")
|
||||
* @icon("social:facebook")
|
||||
* @icon("social:funkwhale")
|
||||
* @icon("social:instagram")
|
||||
* @icon("social:linkedin")
|
||||
* @icon("social:mastodon")
|
||||
* @icon("social:matrix")
|
||||
* @icon("social:misskey")
|
||||
* @icon("social:mobilizon")
|
||||
* @icon("social:peertube")
|
||||
* @icon("social:pixelfed")
|
||||
* @icon("social:pleroma")
|
||||
* @icon("social:plume")
|
||||
* @icon("social:slack")
|
||||
* @icon("social:telegram")
|
||||
* @icon("social:threads")
|
||||
* @icon("social:tiktok")
|
||||
* @icon("social:twitch")
|
||||
* @icon("social:writefreely")
|
||||
* @icon("social:x")
|
||||
* @icon("social:youtube")
|
||||
*/
|
||||
|
@ -21,7 +21,7 @@ import Tooltip from "./modules/Tooltip";
|
||||
import ValidateFileSize from "./modules/ValidateFileSize";
|
||||
import "./modules/video-clip-previewer";
|
||||
import VideoClipBuilder from "./modules/VideoClipBuilder";
|
||||
import "./modules/xml-editor";
|
||||
import "./modules/code-editor";
|
||||
import "@patternfly/elements/pf-tabs/pf-tabs.js";
|
||||
import FieldArray from "./modules/FieldArray";
|
||||
|
||||
|
222
app/Resources/js/modules/code-editor.ts
Normal file
222
app/Resources/js/modules/code-editor.ts
Normal file
@ -0,0 +1,222 @@
|
||||
import { indentWithTab } from "@codemirror/commands";
|
||||
import { html as htmlLang } from "@codemirror/lang-html";
|
||||
import { xml } from "@codemirror/lang-xml";
|
||||
import {
|
||||
defaultHighlightStyle,
|
||||
syntaxHighlighting,
|
||||
} from "@codemirror/language";
|
||||
import { Compartment, EditorState, Extension } from "@codemirror/state";
|
||||
import { keymap, ViewUpdate } from "@codemirror/view";
|
||||
import { basicSetup, EditorView } from "codemirror";
|
||||
import { prettify as prettifyHTML, minify as minifyHTML } from "htmlfy";
|
||||
import { css, html, LitElement, TemplateResult } from "lit";
|
||||
import {
|
||||
customElement,
|
||||
property,
|
||||
queryAssignedNodes,
|
||||
state,
|
||||
} from "lit/decorators.js";
|
||||
import xmlFormat from "xml-formatter";
|
||||
|
||||
const language = new Compartment();
|
||||
|
||||
@customElement("code-editor")
|
||||
export class XMLEditor extends LitElement {
|
||||
@queryAssignedNodes({ slot: "textarea" })
|
||||
_textarea!: NodeListOf<HTMLTextAreaElement>;
|
||||
|
||||
@property()
|
||||
lang = "html";
|
||||
|
||||
@state()
|
||||
editorState!: EditorState;
|
||||
|
||||
@state()
|
||||
editorView!: EditorView;
|
||||
|
||||
_textareaEvents = [
|
||||
{
|
||||
events: ["focus", "invalid"],
|
||||
onEvent: (_: Event, editor: EditorView) => {
|
||||
// focus editor when textarea is focused or invalid
|
||||
editor.focus();
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
firstUpdated(): void {
|
||||
const minHeightEditor = EditorView.baseTheme({
|
||||
".cm-content, .cm-gutter": {
|
||||
minHeight: this._textarea[0].clientHeight + "px",
|
||||
},
|
||||
});
|
||||
|
||||
const extensions: Extension[] = [
|
||||
basicSetup,
|
||||
keymap.of([indentWithTab]),
|
||||
minHeightEditor,
|
||||
syntaxHighlighting(defaultHighlightStyle),
|
||||
EditorView.updateListener.of((viewUpdate: ViewUpdate) => {
|
||||
if (viewUpdate.docChanged) {
|
||||
// Document changed, minify and update textarea value
|
||||
switch (this.lang) {
|
||||
case "xml":
|
||||
this._textarea[0].value = minifyXML(
|
||||
viewUpdate.state.doc.toString()
|
||||
);
|
||||
break;
|
||||
case "html":
|
||||
this._textarea[0].value = minifyHTML(
|
||||
viewUpdate.state.doc.toString()
|
||||
);
|
||||
break;
|
||||
default:
|
||||
this._textarea[0].value = viewUpdate.state.doc.toString();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}),
|
||||
];
|
||||
|
||||
let editorContents = "";
|
||||
switch (this.lang) {
|
||||
case "xml":
|
||||
editorContents = formatXML(this._textarea[0].value);
|
||||
extensions.push(language.of(xml()));
|
||||
break;
|
||||
case "html":
|
||||
editorContents = prettifyHTML(this._textarea[0].value);
|
||||
extensions.push(language.of(htmlLang()));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
this.editorState = EditorState.create({
|
||||
doc: editorContents,
|
||||
extensions: extensions,
|
||||
});
|
||||
|
||||
this.editorView = new EditorView({
|
||||
state: this.editorState,
|
||||
root: this.shadowRoot as ShadowRoot,
|
||||
parent: this.shadowRoot as ShadowRoot,
|
||||
});
|
||||
|
||||
// hide textarea
|
||||
this._textarea[0].style.position = "absolute";
|
||||
this._textarea[0].style.opacity = "0";
|
||||
this._textarea[0].style.zIndex = "-9999";
|
||||
this._textarea[0].style.pointerEvents = "none";
|
||||
|
||||
for (const event of this._textareaEvents) {
|
||||
event.events.forEach((name) => {
|
||||
this._textarea[0].addEventListener(name, (e) =>
|
||||
event.onEvent(e, this.editorView)
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
|
||||
for (const event of this._textareaEvents) {
|
||||
event.events.forEach((name) => {
|
||||
this._textarea[0].removeEventListener(name, (e) =>
|
||||
event.onEvent(e, this.editorView)
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.cm-editor {
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
border: 3px solid hsl(var(--color-border-contrast));
|
||||
background-color: hsl(var(--color-background-elevated));
|
||||
}
|
||||
.cm-editor.cm-focused {
|
||||
outline: 2px solid transparent;
|
||||
box-shadow:
|
||||
0 0 0 2px hsl(var(--color-background-elevated)),
|
||||
0 0 0 calc(4px) hsl(var(--color-accent-base));
|
||||
}
|
||||
.cm-gutters {
|
||||
background-color: hsl(var(--color-background-elevated)) !important;
|
||||
}
|
||||
|
||||
.cm-activeLine {
|
||||
background-color: hsla(
|
||||
var(--color-background-highlight) / 0.25
|
||||
) !important;
|
||||
}
|
||||
|
||||
.cm-activeLineGutter {
|
||||
background-color: hsl(var(--color-background-highlight)) !important;
|
||||
}
|
||||
|
||||
.ͼ4 .cm-line {
|
||||
caret-color: hsl(var(--color-text-base)) !important;
|
||||
}
|
||||
|
||||
.ͼ1 .cm-cursor {
|
||||
border: none;
|
||||
}
|
||||
`;
|
||||
|
||||
render(): TemplateResult<1> {
|
||||
return html`<slot name="textarea"></slot>`;
|
||||
}
|
||||
}
|
||||
|
||||
function formatXML(contents: string) {
|
||||
if (contents === "") {
|
||||
return contents;
|
||||
}
|
||||
|
||||
let editorContents = "";
|
||||
try {
|
||||
editorContents = xmlFormat(contents, {
|
||||
indentation: " ",
|
||||
});
|
||||
} catch {
|
||||
// xml doesn't have a root node
|
||||
editorContents = xmlFormat("<root>" + contents + "</root>", {
|
||||
indentation: " ",
|
||||
});
|
||||
// remove root, unnecessary lines and indents
|
||||
editorContents = editorContents
|
||||
.replace(/^<root>/, "")
|
||||
.replace(/<\/root>$/, "")
|
||||
.replace(/^\s*[\r\n]/gm, "")
|
||||
.replace(/[\r\n] {2}/gm, "\r\n")
|
||||
.trim();
|
||||
}
|
||||
|
||||
return editorContents;
|
||||
}
|
||||
|
||||
function minifyXML(contents: string) {
|
||||
if (contents === "") {
|
||||
return contents;
|
||||
}
|
||||
|
||||
let minifiedContent = "";
|
||||
try {
|
||||
minifiedContent = xmlFormat.minify(contents, {
|
||||
collapseContent: true,
|
||||
});
|
||||
} catch {
|
||||
minifiedContent = xmlFormat.minify(`<root>${contents}</root>`, {
|
||||
collapseContent: true,
|
||||
});
|
||||
// remove root
|
||||
minifiedContent = minifiedContent
|
||||
.replace(/^<root>/, "")
|
||||
.replace(/<\/root>$/, "");
|
||||
}
|
||||
|
||||
return minifiedContent;
|
||||
}
|
@ -1,125 +0,0 @@
|
||||
import { indentWithTab } from "@codemirror/commands";
|
||||
import { xml } from "@codemirror/lang-xml";
|
||||
import {
|
||||
defaultHighlightStyle,
|
||||
syntaxHighlighting,
|
||||
} from "@codemirror/language";
|
||||
import { Compartment, EditorState } from "@codemirror/state";
|
||||
import { keymap, ViewUpdate } from "@codemirror/view";
|
||||
import { basicSetup, EditorView } from "codemirror";
|
||||
import { css, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, queryAssignedNodes, state } from "lit/decorators.js";
|
||||
import prettifyXML from "xml-formatter";
|
||||
|
||||
const language = new Compartment();
|
||||
|
||||
@customElement("xml-editor")
|
||||
export class XMLEditor extends LitElement {
|
||||
@queryAssignedNodes({ slot: "textarea" })
|
||||
_textarea!: NodeListOf<HTMLTextAreaElement>;
|
||||
|
||||
@state()
|
||||
editorState!: EditorState;
|
||||
|
||||
@state()
|
||||
editorView!: EditorView;
|
||||
|
||||
firstUpdated(): void {
|
||||
const minHeightEditor = EditorView.theme({
|
||||
".cm-content, .cm-gutter": {
|
||||
minHeight: this._textarea[0].clientHeight + "px",
|
||||
},
|
||||
});
|
||||
|
||||
let editorContents = "";
|
||||
if (this._textarea[0].value) {
|
||||
try {
|
||||
editorContents = prettifyXML(this._textarea[0].value, {
|
||||
indentation: " ",
|
||||
});
|
||||
} catch {
|
||||
// xml doesn't have a root node
|
||||
editorContents = prettifyXML(
|
||||
"<root>" + this._textarea[0].value + "</root>",
|
||||
{
|
||||
indentation: " ",
|
||||
}
|
||||
);
|
||||
// remove root, unnecessary lines and indents
|
||||
editorContents = editorContents
|
||||
.replace(/^<root>/, "")
|
||||
.replace(/<\/root>$/, "")
|
||||
.replace(/^\s*[\r\n]/gm, "")
|
||||
.replace(/[\r\n] {2}/gm, "\r\n")
|
||||
.trim();
|
||||
}
|
||||
}
|
||||
|
||||
this.editorState = EditorState.create({
|
||||
doc: editorContents,
|
||||
extensions: [
|
||||
basicSetup,
|
||||
keymap.of([indentWithTab]),
|
||||
language.of(xml()),
|
||||
minHeightEditor,
|
||||
syntaxHighlighting(defaultHighlightStyle),
|
||||
EditorView.updateListener.of((viewUpdate: ViewUpdate) => {
|
||||
if (viewUpdate.docChanged) {
|
||||
// Document changed, update textarea value
|
||||
this._textarea[0].value = viewUpdate.state.doc.toString();
|
||||
}
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
this.editorView = new EditorView({
|
||||
state: this.editorState,
|
||||
root: this.shadowRoot as ShadowRoot,
|
||||
parent: this.shadowRoot as ShadowRoot,
|
||||
});
|
||||
|
||||
// hide textarea
|
||||
this._textarea[0].style.position = "absolute";
|
||||
this._textarea[0].style.opacity = "0";
|
||||
this._textarea[0].style.zIndex = "-9999";
|
||||
this._textarea[0].style.pointerEvents = "none";
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.cm-editor {
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
border: 3px solid hsl(var(--color-border-contrast));
|
||||
background-color: hsl(var(--color-background-elevated));
|
||||
}
|
||||
.cm-editor.cm-focused {
|
||||
outline: 2px solid transparent;
|
||||
box-shadow:
|
||||
0 0 0 2px hsl(var(--color-background-elevated)),
|
||||
0 0 0 calc(4px) hsl(var(--color-accent-base));
|
||||
}
|
||||
.cm-gutters {
|
||||
background-color: hsl(var(--color-background-elevated)) !important;
|
||||
}
|
||||
|
||||
.cm-activeLine {
|
||||
background-color: hsl(var(--color-background-highlight)) !important;
|
||||
}
|
||||
|
||||
.cm-activeLineGutter {
|
||||
background-color: hsl(var(--color-background-highlight)) !important;
|
||||
}
|
||||
|
||||
.ͼ4 .cm-line {
|
||||
caret-color: hsl(var(--color-text-base)) !important;
|
||||
}
|
||||
|
||||
.ͼ1 .cm-cursor {
|
||||
border: none;
|
||||
}
|
||||
`;
|
||||
|
||||
render(): TemplateResult<1> {
|
||||
return html`<slot name="textarea"></slot>`;
|
||||
}
|
||||
}
|
@ -30,19 +30,19 @@ class Alert extends Component
|
||||
$variantData = match ($this->variant) {
|
||||
'success' => [
|
||||
'class' => 'text-pine-900 bg-pine-100 border-pine-300',
|
||||
'glyph' => 'check-fill', // @icon('check-fill')
|
||||
'glyph' => 'check-fill', // @icon("check-fill")
|
||||
],
|
||||
'danger' => [
|
||||
'class' => 'text-red-900 bg-red-100 border-red-300',
|
||||
'glyph' => 'close-fill', // @icon('close-fill')
|
||||
'glyph' => 'close-fill', // @icon("close-fill")
|
||||
],
|
||||
'warning' => [
|
||||
'class' => 'text-yellow-900 bg-yellow-100 border-yellow-300',
|
||||
'glyph' => 'alert-fill', // @icon('alert-fill')
|
||||
'glyph' => 'alert-fill', // @icon("alert-fill")
|
||||
],
|
||||
default => [
|
||||
'class' => 'text-blue-900 bg-blue-100 border-blue-300',
|
||||
'glyph' => 'error-warning-fill', // @icon('error-warning-fill')
|
||||
'glyph' => 'error-warning-fill', // @icon("error-warning-fill")
|
||||
],
|
||||
};
|
||||
|
||||
|
@ -6,20 +6,22 @@ namespace App\Views\Components\Forms;
|
||||
|
||||
use Override;
|
||||
|
||||
class XMLEditor extends FormComponent
|
||||
class CodeEditor extends FormComponent
|
||||
{
|
||||
protected array $props = ['content'];
|
||||
protected array $props = ['content', 'lang'];
|
||||
|
||||
/**
|
||||
* @var array<string, string>
|
||||
*/
|
||||
protected array $attributes = [
|
||||
'rows' => '5',
|
||||
'rows' => '6',
|
||||
'class' => 'textarea',
|
||||
];
|
||||
|
||||
protected string $content = '';
|
||||
|
||||
protected string $lang = '';
|
||||
|
||||
public function setContent(string $value): void
|
||||
{
|
||||
$this->content = htmlspecialchars_decode($value);
|
||||
@ -32,7 +34,7 @@ class XMLEditor extends FormComponent
|
||||
$textarea = form_textarea($this->attributes, $this->content);
|
||||
|
||||
return <<<HTML
|
||||
<xml-editor>{$textarea}</xml-editor>
|
||||
<code-editor lang="{$this->lang}">{$textarea}</code-editor>
|
||||
HTML;
|
||||
}
|
||||
}
|
@ -1,38 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Views\Decorators;
|
||||
|
||||
use CodeIgniter\View\ViewDecoratorInterface;
|
||||
use Override;
|
||||
|
||||
class SiteHead implements ViewDecoratorInterface
|
||||
{
|
||||
private static int $renderedCount = 0;
|
||||
|
||||
#[Override]
|
||||
public static function decorate(string $html): string
|
||||
{
|
||||
if (url_is(config('Admin')->gateway . '*') || url_is(config('Install')->gateway)) {
|
||||
return $html;
|
||||
}
|
||||
|
||||
if (static::$renderedCount > 0) {
|
||||
return $html;
|
||||
}
|
||||
|
||||
ob_start(); // Start output buffering
|
||||
// run hook to add tags to <head>
|
||||
service('plugins')->siteHead();
|
||||
$metaTags = ob_get_contents(); // Store buffer in variable
|
||||
ob_end_clean();
|
||||
|
||||
if (str_contains($html, '</head>')) {
|
||||
$html = str_replace('</head>', "\n\t{$metaTags}\n</head>", $html);
|
||||
++static::$renderedCount;
|
||||
}
|
||||
|
||||
return $html;
|
||||
}
|
||||
}
|
@ -9,37 +9,37 @@
|
||||
"php": "^8.3",
|
||||
"adaures/ipcat-php": "^v1.0.0",
|
||||
"adaures/podcast-persons-taxonomy": "^v1.0.1",
|
||||
"aws/aws-sdk-php": "^3.325.2",
|
||||
"aws/aws-sdk-php": "^3.334.7",
|
||||
"chrisjean/php-ico": "^1.0.4",
|
||||
"cocur/slugify": "^v4.6.0",
|
||||
"codeigniter4/framework": "v4.5.5",
|
||||
"codeigniter4/settings": "v2.2.0",
|
||||
"codeigniter4/shield": "v1.1.0",
|
||||
"codeigniter4/tasks": "dev-develop",
|
||||
"geoip2/geoip2": "v3.0.0",
|
||||
"geoip2/geoip2": "v3.1.0",
|
||||
"james-heinrich/getid3": "^2.0.0-beta6",
|
||||
"league/commonmark": "^2.5.3",
|
||||
"league/commonmark": "^2.6.0",
|
||||
"league/html-to-markdown": "5.1.1",
|
||||
"melbahja/seo": "^v2.1.1",
|
||||
"michalsn/codeigniter4-uuid": "v1.1.0",
|
||||
"mpratt/embera": "^2.0.41",
|
||||
"opawg/user-agents-v2-php": "dev-main",
|
||||
"phpseclib/phpseclib": "~2.0.47",
|
||||
"phpseclib/phpseclib": "~2.0.48",
|
||||
"vlucas/phpdotenv": "v5.6.1",
|
||||
"whichbrowser/parser": "^v2.1.8",
|
||||
"yassinedoghri/php-icons": "^v1.2.0",
|
||||
"yassinedoghri/podcast-feed": "dev-main"
|
||||
},
|
||||
"require-dev": {
|
||||
"captainhook/captainhook": "^5.23.6",
|
||||
"codeigniter/phpstan-codeigniter": "v1.4.3",
|
||||
"captainhook/captainhook": "^5.24.1",
|
||||
"codeigniter/phpstan-codeigniter": "v1.5.1",
|
||||
"mikey179/vfsstream": "^v1.6.12",
|
||||
"phpstan/extension-installer": "^1.4.3",
|
||||
"phpstan/phpstan": "^1.12.7",
|
||||
"phpunit/phpunit": "^10.5.38",
|
||||
"rector/rector": "^1.2.9",
|
||||
"phpstan/phpstan": "^2.0.3",
|
||||
"phpunit/phpunit": "^11.5.1",
|
||||
"rector/rector": "^2.0.3",
|
||||
"symplify/coding-standard": "^12.2.3",
|
||||
"symplify/easy-coding-standard": "^12.3.6"
|
||||
"symplify/easy-coding-standard": "^12.5.4"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
|
961
composer.lock
generated
961
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@ -37,7 +37,7 @@ RSS feed.
|
||||
Here is a good place to add new tags to the generated channel.
|
||||
|
||||
```php
|
||||
public function rssAfterChannel(Podcast $podcast, SimpleRSSElement $channel): void
|
||||
public function rssAfterChannel(Podcast $podcast, RssFeed $channel): void
|
||||
{
|
||||
// …
|
||||
}
|
||||
@ -65,7 +65,7 @@ feed.
|
||||
Here is a good place to add new tags to the generated item.
|
||||
|
||||
```php
|
||||
public function rssAfterItem(Epsiode $episode, SimpleRSSElement $item): void
|
||||
public function rssAfterItem(Epsiode $episode, RssFeed $item): void
|
||||
{
|
||||
// …
|
||||
}
|
||||
@ -75,11 +75,11 @@ public function rssAfterItem(Epsiode $episode, SimpleRSSElement $item): void
|
||||
|
||||
This hook is executed in the public pages' `<head>` tag.
|
||||
|
||||
This is a good place to add meta tags and third-party scripts to Castopod's
|
||||
public pages.
|
||||
This is a good place to add meta tags, custom styles, and third-party scripts to
|
||||
Castopod's public pages.
|
||||
|
||||
```php
|
||||
public function siteHead(): void
|
||||
public function siteHead(HtmlHead $head): void
|
||||
{
|
||||
// …
|
||||
}
|
||||
|
@ -101,16 +101,16 @@ each property being a field key and the value being a `Field` object.
|
||||
|
||||
A field is a form element:
|
||||
|
||||
| Property | Type | Note |
|
||||
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------ |
|
||||
| `type` | `checkbox` \| `datetime` \| `email` \| `group` \| `markdown` \| `number` \| `radio-group` \| `select-multiple` \| `select` \| `text` \| `textarea` \| `toggler` \| `url` | Default is `text` |
|
||||
| `label` (required) | `string` | Can be translated (see i18n) |
|
||||
| `hint` | `string` | Can be translated (see i18n) |
|
||||
| `helper` | `string` | Can be translated (see i18n) |
|
||||
| `optional` | `boolean` | Default is `false` |
|
||||
| `options` | `Options` | Required for `radio-group`, `select-multiple`, and `select` types. |
|
||||
| `multiple` | `boolean` | Default is `false` |
|
||||
| `fields` | `Array<string, Field>` | Required for `group` type |
|
||||
| Property | Type | Note |
|
||||
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
|
||||
| `type` | `checkbox` \| `datetime` \| `email` \| `group` \| `html` \| `markdown` \| `number` \| `radio-group` \| `rss` \| `select-multiple` \| `select` \| `text` \| `textarea` \| `toggler` \| `url` | Default is `text` |
|
||||
| `label` (required) | `string` | Can be translated (see i18n) |
|
||||
| `hint` | `string` | Can be translated (see i18n) |
|
||||
| `helper` | `string` | Can be translated (see i18n) |
|
||||
| `optional` | `boolean` | Default is `false` |
|
||||
| `options` | `Options` | Required for `radio-group`, `select-multiple`, and `select` types. |
|
||||
| `multiple` | `boolean` | Default is `false` |
|
||||
| `fields` | `Array<string, Field>` | Required for `group` type |
|
||||
|
||||
#### Options object
|
||||
|
||||
|
@ -2,8 +2,9 @@
|
||||
title: 验证 & 授权
|
||||
---
|
||||
|
||||
Castopod 使用 `codeigniter/shield` 处理身份验证和授权 与自定义规则。 角色和权限
|
||||
在两个级别上定义: Roles and permissions are defined at two levels:
|
||||
Castopod 使用 `codeigniter/shield`
|
||||
处理身份验证和授权 与自定义规则。 角色和权限在两个级别上定义: Roles and
|
||||
permissions are defined at two levels:
|
||||
|
||||
1. [实例范围](#1-instance-wide-roles-and-permissions)
|
||||
2. [每个播客](#2-per-podcast-roles-and-permissions)
|
||||
|
@ -4,15 +4,12 @@ title: 官方 Docker 镜像
|
||||
|
||||
Castopod 在其自动构建期间会将 3 个 Docker 映像推送到 Docker Hub :
|
||||
|
||||
- [**`castopod/castopod`**](https://hub.docker.com/r/castopod/castopod);一个使
|
||||
用 nginx 单元的整合 Castopod 镜像
|
||||
- [**`castopod/app`**](https://hub.docker.com/r/castopod/app):应用程序包,包含
|
||||
所有 Castopod 依赖关系
|
||||
- [**`castopod/web-server`**](https://hub.docker.com/r/castopod/web-server):Castopod
|
||||
的 Nginx 配置
|
||||
- [**`castopod/castopod`**](https://hub.docker.com/r/castopod/castopod);一个使用 nginx 单元的整合 Castopod 镜像
|
||||
- [**`castopod/app`**](https://hub.docker.com/r/castopod/app):应用程序包,包含所有 Castopod 依赖关系
|
||||
- [**`castopod/web-server`**](https://hub.docker.com/r/castopod/web-server):Castopod 的 Nginx 配置
|
||||
|
||||
此外,Castopod 需要一个与 MySQL 兼容的数据库。 Redis 数据库 可以添加为缓存处理
|
||||
器。 A Redis database can be added as a cache handler.
|
||||
此外,Castopod 需要一个与 MySQL 兼容的数据库。 Redis 数据库 可以添加为缓存处理器。 A
|
||||
Redis database can be added as a cache handler.
|
||||
|
||||
## 目前支持的标签
|
||||
|
||||
|
@ -35,9 +35,8 @@ PHP version 8.3 or higher is required, with the following extensions installed:
|
||||
|
||||
> 我们建议使用 [MariaDB](https://mariadb.org)。
|
||||
|
||||
你需要填写服务器主机名、数据库名称、用户名和密码才能完成安装过程。 如果没有这
|
||||
些,请与你的服务器管理员联系。 If you do not have these, please contact your
|
||||
server administrator.
|
||||
你需要填写服务器主机名、数据库名称、用户名和密码才能完成安装过程。 如果没有这些,请与你的服务器管理员联系。 If
|
||||
you do not have these, please contact your server administrator.
|
||||
|
||||
#### 权限
|
||||
|
||||
@ -46,8 +45,8 @@ server administrator.
|
||||
|
||||
### (可选)FFmpeg v4.1.8 或更高版本,用于视频素材
|
||||
|
||||
如果你需要视频素材,则需要 [FFFmpeg](https://www.ffmpeg.org/) 4.1.8 或更高版本。
|
||||
必须安装以下扩展: The following extensions must be installed:
|
||||
如果你需要视频素材,则需要 [FFFmpeg](https://www.ffmpeg.org/)
|
||||
4.1.8 或更高版本。必须安装以下扩展: The following extensions must be installed:
|
||||
|
||||
- **FreeType 2** 来自库
|
||||
[gd](https://www.php.net/manual/en/image.installation.php)
|
||||
@ -63,11 +62,11 @@ server administrator.
|
||||
### Pre-requisites
|
||||
|
||||
0. 需要一台已经实现 [环境要求](#requirements)的 Web 服务器
|
||||
1. 为 Castopod 创建一个 MySQL 数据库,其中用户具有访问和修改权限(有关详细信息,
|
||||
请参阅 [MySQL 兼容数据库](#mysql-compatible-database))。
|
||||
1. 为 Castopod 创建一个 MySQL 数据库,其中用户具有访问和修改权限(有关详细信息,请参阅
|
||||
[MySQL 兼容数据库](#mysql-compatible-database))。
|
||||
2. 使用 _SSL 证书_ 在您的域激活 HTTPS。
|
||||
3. 下载最新的 [Castopod](https://castopod.org/) 到 web 服务器并解压(如果尚未下
|
||||
载)。
|
||||
3. 下载最新的 [Castopod](https://castopod.org/)
|
||||
到 web 服务器并解压(如果尚未下载)。
|
||||
- ⚠️ 将 web 服务器根目录设置为 `castopod` 文件夹中的 `public/` 子文件夹。
|
||||
4. 在 Web 服务器上为各种后台进程添加 **cron 任务** (相应地替换路径):
|
||||
|
||||
@ -85,16 +84,16 @@ server administrator.
|
||||
|
||||
### (推荐) 安装向导
|
||||
|
||||
1. 前往你最喜欢的浏览器并跳转至安装向导页面
|
||||
(`https://your_domain_name.com/cp-install`)运行 Castopod 安装脚本。
|
||||
1. 前往你最喜欢的浏览器并跳转至安装向导页面 (`https://your_domain_name.com/cp-install`)运行 Castopod 安装脚本。
|
||||
2. 请按照屏幕上的说明进行操作。
|
||||
3. 开始播客!
|
||||
|
||||
<Aside>
|
||||
|
||||
The install script writes a `.env` file in the package root. 安装脚本将会在根目
|
||||
录中创建一个 `.env` 文件并写入数据。 如果你不能执行安装向导,那么可以基于
|
||||
`.env.example` 文件手动创建和编辑 `.env` 文件。
|
||||
The install script writes a `.env` file in the package
|
||||
root. 安装脚本将会在根目录中创建一个 `.env`
|
||||
文件并写入数据。 如果你不能执行安装向导,那么可以基于 `.env.example`
|
||||
文件手动创建和编辑 `.env` 文件。
|
||||
|
||||
</Aside>
|
||||
|
||||
@ -147,9 +146,9 @@ email.SMTPPass="你的邮件密码"
|
||||
|
||||
### 媒体存储
|
||||
|
||||
By default, files are saved to the `public/media` folder using the file system.
|
||||
默认情况下,文件使用文件系统保存到 `公共/媒体` 文件夹中。 如果您需要将 `media`
|
||||
文件夹重新定位到其他位置,您可以在您的 `.env` 文件中指定它,如下所示:
|
||||
By default, files are saved to the `public/media` folder using the file
|
||||
system. 默认情况下,文件使用文件系统保存到 `公共/媒体` 文件夹中。 如果您需要将
|
||||
`media` 文件夹重新定位到其他位置,您可以在您的 `.env` 文件中指定它,如下所示:
|
||||
|
||||
```ini
|
||||
# […]
|
||||
@ -194,9 +193,9 @@ media.s3.region="your_s3_region"
|
||||
|
||||
### 使用 YunoHost 安装
|
||||
|
||||
[YunoHost](https://yunohost.org/) 是一个基于 Debian GNU/Linux 的发行版,由免费和
|
||||
开源软件包组成。 它可以为你解决自托管的困难。 It manages the hardships of
|
||||
self-hosting for you.
|
||||
[YunoHost](https://yunohost.org/) 是一个基于 Debian
|
||||
GNU/Linux 的发行版,由免费和开源软件包组成。 它可以为你解决自托管的困难。 It
|
||||
manages the hardships of self-hosting for you.
|
||||
|
||||
<div class="flex flex-wrap items-center gap-4">
|
||||
|
||||
|
@ -5,8 +5,7 @@ title: 安全问题
|
||||
Castopod 构建于 [CodeIgniter4](https://codeigniter.com/), PHP 框架上,鼓励
|
||||
[更好的安全实践](https://codeigniter.com/user_guide/concepts/security.html)。
|
||||
|
||||
为了最大限度地提高你实例的安全性并防止任何恶意攻击。 我们 建议你在安装或更新后检
|
||||
查所有的 Castopod 文件权限(避免任何之前的权限错误):
|
||||
为了最大限度地提高你实例的安全性并防止任何恶意攻击。 我们 建议你在安装或更新后检查所有的 Castopod 文件权限(避免任何之前的权限错误):
|
||||
|
||||
- `writable/` 文件夹权限为 **可读** 和 **可写**。
|
||||
- `public/media/` 文件夹权限为 **可读** 和 **可写**。
|
||||
|
@ -4,8 +4,7 @@ title: 如何更新 Castopod ?
|
||||
|
||||
import { Aside } from "@astrojs/starlight/components";
|
||||
|
||||
安装 Castopod 后,你可能希望将实例更新到最新版本 版本以享受最新功能 ✨, 修复错误
|
||||
🐛 和性能提升 ⚡。
|
||||
安装 Castopod 后,你可能希望将实例更新到最新版本 版本以享受最新功能 ✨, 修复错误 🐛 和性能提升 ⚡。
|
||||
|
||||
## 更新说明
|
||||
|
||||
@ -13,14 +12,14 @@ import { Aside } from "@astrojs/starlight/components";
|
||||
|
||||
- 参看. [我应该在更新前进行备份吗?](#should-i-make-a-backup-before-updating)
|
||||
|
||||
1. 前往 [发布页面](https://code.castopod.org/adaures/castopod/-/releases) 和 查
|
||||
看您的实例是否是最新的 Castopod 版本
|
||||
1. 前往 [发布页面](https://code.castopod.org/adaures/castopod/-/releases)
|
||||
和 查看您的实例是否是最新的 Castopod 版本
|
||||
|
||||
- 参看
|
||||
[我在哪里可以找到我的 Castopod 版本?](#where-can-i-find-my-castopod-version)
|
||||
|
||||
2. 下载名为`Castopod Package`的最新发布包,你可以在 `zip` 或 `tar.gz` 压缩包之间
|
||||
选择
|
||||
2. 下载名为`Castopod Package`的最新发布包,你可以在 `zip` 或 `tar.gz`
|
||||
压缩包之间选择
|
||||
|
||||
- ⚠️ 请确保你下载的是 Castopod 软件包而 **不是** 源代码
|
||||
- 请注意,你还可以从 [castopod.org](https://castopod.org/)
|
||||
@ -83,8 +82,9 @@ them sequentially, from the oldest to the newest.
|
||||
|
||||
1. 下载最新版本,覆盖您的文件,同时保留 `.env` 文件和 `public/media` 文件夹。
|
||||
|
||||
2. 从 `v1.0.0-alpha.43` 开始,按顺序执行每个版本更新指令(从老版本到 最新版本),
|
||||
然后是 `v1.0.0-alpha.44`,`v1.0.0-alpha.45`,…,直到 `v1.0.0-beta.1`。
|
||||
2. 从 `v1.0.0-alpha.43`
|
||||
开始,按顺序执行每个版本更新指令(从老版本到 最新版本),然后是
|
||||
`v1.0.0-alpha.44`,`v1.0.0-alpha.45`,…,直到 `v1.0.0-beta.1`。
|
||||
|
||||
3. ✨ 享受你的新实例, 你已经更新完毕!
|
||||
|
||||
|
@ -4,11 +4,10 @@ title: 欢迎 👋
|
||||
|
||||
import { LinkCard } from "@astrojs/starlight/components";
|
||||
|
||||
Castopod 是一个免费的开源播客托管平台,为那些想要和听众接触与互动的播客们制作
|
||||
的。
|
||||
Castopod 是一个免费的开源播客托管平台,为那些想要和听众接触与互动的播客们制作的。
|
||||
|
||||
Castopod 易于安装,并使用 [CodeIgniter4](https://codeigniter.com/) 构建, 这是一
|
||||
个强大的 PHP 框架,并且占用极小。
|
||||
Castopod 易于安装,并使用 [CodeIgniter4](https://codeigniter.com/)
|
||||
构建, 这是一个强大的 PHP 框架,并且占用极小。
|
||||
|
||||
<LinkCard title="安装" href="./getting-started/install" />
|
||||
|
||||
@ -44,28 +43,24 @@ Castopod 易于安装,并使用 [CodeIgniter4](https://codeigniter.com/) 构
|
||||
- 📤 也支持将播客移出 Castopod
|
||||
- 🔀 多租户:根据需要托管任意数量的播客
|
||||
- 👥 多用户:添加贡献者并设置角色
|
||||
- 🌎 i18n 支持:翻译成英语,法语,波兰语,德语,巴西葡萄牙语和西班牙语
|
||||
...[还有更多](https://translate.castopod.org)! and
|
||||
- 🌎
|
||||
i18n 支持:翻译成英语,法语,波兰语,德语,巴西葡萄牙语和西班牙语 ...[还有更多](https://translate.castopod.org)! and
|
||||
[many more](https://translate.castopod.org)!
|
||||
|
||||
## 创作动机
|
||||
|
||||
播客生态系统本质上是去中心化的:你可以创建自己的播客 RSS 文件,将其发布到网络上
|
||||
并在线共享。
|
||||
播客生态系统本质上是去中心化的:你可以创建自己的播客 RSS 文件,将其发布到网络上并在线共享。
|
||||
|
||||
事实上,它是唯一长期保持这种状态的媒体之一。
|
||||
|
||||
随着习惯的发展,越来越多的人开始接触播客:允许创作者寻找新的方式来分享他们的想
|
||||
法,或是让听众获得更好的内容。
|
||||
随着习惯的发展,越来越多的人开始接触播客:允许创作者寻找新的方式来分享他们的想法,或是让听众获得更好的内容。
|
||||
|
||||
随着播客的使用越来越广泛,一些公司正试图控制播客与集中化。
|
||||
|
||||
Castopod 的创建旨在提供一种开放且可持续的替代方案来托管你的播客,促进权力下放,
|
||||
确保播客可以用创造力表达自己。
|
||||
Castopod 的创建旨在提供一种开放且可持续的替代方案来托管你的播客,促进权力下放,确保播客可以用创造力表达自己。
|
||||
|
||||
此项目由开源社区推动的,特别是
|
||||
由[联邦宇宙](https://fediverse.party/en/fediverse/) 和
|
||||
[播客 2.0](https://podcastindex.org/) 推动。
|
||||
此项目由开源社区推动的,特别是由[联邦宇宙](https://fediverse.party/en/fediverse/)
|
||||
和 [播客 2.0](https://podcastindex.org/) 推动。
|
||||
|
||||
## 与其他解决方案的对比
|
||||
|
||||
@ -75,51 +70,42 @@ gauge whether Castopod is the right fit for you.
|
||||
|
||||
### Castopod 对比 Wordpress
|
||||
|
||||
Castopod 经常被称为 “播客中的 Wordpress”,因为两者有很多相似之处。 在某些方面,
|
||||
确实如此。 实际上,Castopod 受到 WordPress 生态的极大启发,看到了采用社区的易用
|
||||
性以及运行它的网站数量。 In some ways this is true. And actually, Castopod was
|
||||
greatly inspired by the Wordpress ecosystem, seeing the ease of adoption from
|
||||
the community and the number of websites running it.
|
||||
Castopod 经常被称为 “播客中的 Wordpress”,因为两者有很多相似之处。 在某些方面,确实如此。 实际上,Castopod 受到 WordPress 生态的极大启发,看到了采用社区的易用性以及运行它的网站数量。 In
|
||||
some ways this is true. And actually, Castopod was greatly inspired by the
|
||||
Wordpress ecosystem, seeing the ease of adoption from the community and the
|
||||
number of websites running it.
|
||||
|
||||
就像 Wordpress 一样,Castopod 是免费 & 开源的,PHP 构建并使用 MySQL 数据库,可以
|
||||
在大多数 Web 服务器上轻松安装。
|
||||
就像 Wordpress 一样,Castopod 是免费 & 开源的,PHP 构建并使用 MySQL 数据库,可以在大多数 Web 服务器上轻松安装。
|
||||
|
||||
Wordpress 是创建你的网站,并使用插件扩展以获得想要内容的好办法。 这是一个成熟的
|
||||
CMS,可以帮助你在线访问任何类型的网站。 It is a full fledged CMS that helps you
|
||||
get any type of website online.
|
||||
Wordpress 是创建你的网站,并使用插件扩展以获得想要内容的好办法。 这是一个成熟的 CMS,可以帮助你在线访问任何类型的网站。 It
|
||||
is a full fledged CMS that helps you get any type of website online.
|
||||
|
||||
另一方面,Castopod 旨在专门满足播客的需求,专注于播客,而不是其他。 你不需要任何
|
||||
插件即可轻松开始播客之旅。 You don't need any plugin to get you started on your
|
||||
podcasting journey.
|
||||
另一方面,Castopod 旨在专门满足播客的需求,专注于播客,而不是其他。 你不需要任何插件即可轻松开始播客之旅。 You
|
||||
don't need any plugin to get you started on your podcasting journey.
|
||||
|
||||
还拥有对播客的独特优化:从播客的创建和新剧集的发布一直到广播,营销和分析。
|
||||
|
||||
最后,根据你的需要,Wordpress 和 Castopod 甚至可以共存,因为他们有相同的配置环
|
||||
境!
|
||||
最后,根据你的需要,Wordpress 和 Castopod 甚至可以共存,因为他们有相同的配置环境!
|
||||
|
||||
### Castopod 对比 Funkwhale
|
||||
|
||||
Funkwhale 是一个自托管、现代界面、免费开源的音乐服务器。 就像 Castopod 一
|
||||
样,Funkwhale 也位于联邦宇宙中,这是一个去中心化的社交网络,允许两者的互联。Just
|
||||
Funkwhale 是一个自托管、现代界面、免费开源的音乐服务器。 就像 Castopod 一样,Funkwhale 也位于联邦宇宙中,这是一个去中心化的社交网络,允许两者的互联。Just
|
||||
as Castopod, Funkwhale is on the fediverse, a decentralized social network
|
||||
allowing interoperability between the two.
|
||||
|
||||
Funkwhale 最初是围绕音乐制作的。 后来,随着项目的发展,引入了托管播客的能力。And
|
||||
later on, as the project evolved, the ability to host podcasts was introduced.
|
||||
|
||||
与 Funkwhale 不同,Castopod 是只围绕播客设计和构建的。 这样可以更简单地实现与播
|
||||
客相关的生态系统,例如播客 2.0 功能(报表、 章节、地点、人员…)。 This allows
|
||||
easier implementation for features related to the podcasting ecosystem, such as
|
||||
the podcasting 2.0 features (transcripts, chapters, locations, persons, …).
|
||||
与 Funkwhale 不同,Castopod 是只围绕播客设计和构建的。 这样可以更简单地实现与播客相关的生态系统,例如播客 2.0 功能(报表、 章节、地点、人员…)。 This
|
||||
allows easier implementation for features related to the podcasting ecosystem,
|
||||
such as the podcasting 2.0 features (transcripts, chapters, locations, persons,
|
||||
…).
|
||||
|
||||
因此,如果你想托管你的音乐库,你可能应该使用 Funkwhale,如果您想主持一个播客,请
|
||||
使用 Castopod。
|
||||
因此,如果你想托管你的音乐库,你可能应该使用 Funkwhale,如果您想主持一个播客,请使用 Castopod。
|
||||
|
||||
### Castopod 与其他播客
|
||||
|
||||
有许多非常棒地解决方案可供你托管播客,并
|
||||
且[很多](https://podcastindex.org/apps)正在搭上播客 2.0 的便车,就像 Castopod 一
|
||||
样!
|
||||
有许多非常棒地解决方案可供你托管播客,并且[很多](https://podcastindex.org/apps)正在搭上播客 2.0 的便车,就像 Castopod 一样!
|
||||
|
||||
这些解决方案各不相同,你可以对比 [功能列表](#features)。
|
||||
|
||||
@ -129,34 +115,30 @@ the podcasting 2.0 features (transcripts, chapters, locations, persons, …).
|
||||
full control over what you produce. Also, as it is open-source, you can even
|
||||
customize it as you wish.
|
||||
|
||||
- Castopod 是目前唯一一个同时集成去中心化的,带有 ActivePub 的社交网络以及很多播
|
||||
客 2.0 功能集成的解决方案,希望弥合两者之间的差距。
|
||||
- Castopod 是目前唯一一个同时集成去中心化的,带有 ActivePub 的社交网络以及很多播客 2.0 功能集成的解决方案,希望弥合两者之间的差距。
|
||||
|
||||
## 贡献
|
||||
|
||||
喜欢 Castopod 并且想帮忙吗? 请查看以下文档以帮助你入门。 请查看以下文档以帮助你
|
||||
入门。
|
||||
喜欢 Castopod 并且想帮忙吗? 请查看以下文档以帮助你入门。 请查看以下文档以帮助你入门。
|
||||
|
||||
### 行为准则
|
||||
|
||||
Castopod has adopted a Code of Conduct that we expect project participants to
|
||||
adhere to. Castopod 已经通过了一项行为准则,并希望所有的参与者都能够遵循本行为准
|
||||
则。 请阅
|
||||
读[行为准则](https://code.castopod.org/adaures/castopod/-/blob/beta/CODE_OF_CONDUCT.md)
|
||||
adhere to.
|
||||
Castopod 已经通过了一项行为准则,并希望所有的参与者都能够遵循本行为准则。 请阅读[行为准则](https://code.castopod.org/adaures/castopod/-/blob/beta/CODE_OF_CONDUCT.md)
|
||||
以便了解哪些行为被允许,哪些行为不会被容忍。
|
||||
|
||||
### 贡献指南
|
||||
|
||||
阅读我们的 [贡献指南](../contributing/guidelines.md) ,了解我们的开发过程。 提出
|
||||
错 误修正和改进想法,以及如何构建和测试 Castopod 。
|
||||
阅读我们的 [贡献指南](../contributing/guidelines.md)
|
||||
,了解我们的开发过程。 提出错 误修正和改进想法,以及如何构建和测试 Castopod 。
|
||||
|
||||
## 联系
|
||||
|
||||
你可以联系我们寻求帮助或提出任何问题:
|
||||
|
||||
- [Discord](https://castopod.org/discord) (用于与开发人员和社区直接互动)
|
||||
- [问题跟踪器](https://code.castopod.org/adaures/castopod/-/issues)(用于功能请
|
||||
求和错误报告)
|
||||
- [问题跟踪器](https://code.castopod.org/adaures/castopod/-/issues)(用于功能请求和错误报告)
|
||||
|
||||
或者,你可以在社交媒体上关注我们,以获取有关 Castopod 的新闻:
|
||||
|
||||
@ -168,8 +150,8 @@ adhere to. Castopod 已经通过了一项行为准则,并希望所有的参与
|
||||
## 赞助商
|
||||
|
||||
The ongoing development of Castopod is made possible with the support of its
|
||||
backers. Castopod 的发展离不开赞助商的支持。 如果你想要帮助我们,请考
|
||||
虑[赞助 Castopod 的开发](https://opencollective.com/castopod/contribute).
|
||||
backers.
|
||||
Castopod 的发展离不开赞助商的支持。 如果你想要帮助我们,请考虑[赞助 Castopod 的开发](https://opencollective.com/castopod/contribute).
|
||||
|
||||
[](https://adaures.com/)
|
||||
|
||||
|
@ -2,8 +2,9 @@
|
||||
title: 認證 & 授權
|
||||
---
|
||||
|
||||
Castopod 使用 `codeigniter/shield` 處理身分認證和授權 與自定義規則。 腳色和權限
|
||||
在定義為兩個層級: Roles and permissions are defined at two levels:
|
||||
Castopod 使用 `codeigniter/shield`
|
||||
處理身分認證和授權 與自定義規則。 腳色和權限在定義為兩個層級: Roles and
|
||||
permissions are defined at two levels:
|
||||
|
||||
1. [實例範圍](#1-instance-wide-roles-and-permissions)
|
||||
2. [每個播客](#2-per-podcast-roles-and-permissions)
|
||||
|
@ -16,7 +16,7 @@ $routes->add('scheduled-video-clips', 'SchedulerController::generateVideoClips',
|
||||
// Admin area routes
|
||||
$routes->group(
|
||||
config('Admin')
|
||||
->gateway,
|
||||
->gateway,
|
||||
[
|
||||
'namespace' => 'Modules\Admin\Controllers',
|
||||
],
|
||||
|
@ -25,6 +25,7 @@ class AboutController extends BaseController
|
||||
'languages' => implode(', ', config('App')->supportedLocales),
|
||||
];
|
||||
|
||||
$this->setHtmlHead(lang('AboutCastopod.title'));
|
||||
return view('settings/about', [
|
||||
'info' => $instanceInfo,
|
||||
]);
|
||||
|
@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Modules\Admin\Controllers;
|
||||
|
||||
use App\Libraries\HtmlHead;
|
||||
use CodeIgniter\Controller;
|
||||
use CodeIgniter\HTTP\IncomingRequest;
|
||||
use CodeIgniter\HTTP\RequestInterface;
|
||||
@ -41,4 +42,16 @@ abstract class BaseController extends Controller
|
||||
|
||||
Theme::setTheme('admin');
|
||||
}
|
||||
|
||||
protected function setHtmlHead(string $title): void
|
||||
{
|
||||
/** @var HtmlHead $head */
|
||||
$head = service('html_head');
|
||||
|
||||
$head
|
||||
->title($title . ' | Castopod Admin')
|
||||
->description(
|
||||
'Castopod is an open-source hosting platform made for podcasters who want engage and interact with their audience.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -81,6 +81,7 @@ class DashboardController extends BaseController
|
||||
'onlyPodcastId' => $onlyPodcastId,
|
||||
];
|
||||
|
||||
$this->setHtmlHead(lang('Dashboard.home'));
|
||||
return view('dashboard', $data);
|
||||
}
|
||||
}
|
||||
|
@ -115,6 +115,7 @@ class EpisodeController extends BaseController
|
||||
'query' => $query,
|
||||
];
|
||||
|
||||
$this->setHtmlHead(lang('Episode.all_podcast_episodes'));
|
||||
replace_breadcrumb_params([
|
||||
0 => $this->podcast->at_handle,
|
||||
]);
|
||||
@ -128,6 +129,7 @@ class EpisodeController extends BaseController
|
||||
'episode' => $this->episode,
|
||||
];
|
||||
|
||||
$this->setHtmlHead($this->episode->title);
|
||||
replace_breadcrumb_params([
|
||||
0 => $this->podcast->at_handle,
|
||||
1 => $this->episode->title,
|
||||
@ -148,6 +150,8 @@ class EpisodeController extends BaseController
|
||||
$currentSeasonNumber
|
||||
),
|
||||
];
|
||||
|
||||
$this->setHtmlHead(lang('Episode.create'));
|
||||
replace_breadcrumb_params([
|
||||
0 => $this->podcast->at_handle,
|
||||
]);
|
||||
@ -261,6 +265,7 @@ class EpisodeController extends BaseController
|
||||
'episode' => $this->episode,
|
||||
];
|
||||
|
||||
$this->setHtmlHead(lang('Episode.edit'));
|
||||
replace_breadcrumb_params([
|
||||
0 => $this->podcast->at_handle,
|
||||
1 => $this->episode->title,
|
||||
@ -409,6 +414,7 @@ class EpisodeController extends BaseController
|
||||
'episode' => $this->episode,
|
||||
];
|
||||
|
||||
$this->setHtmlHead(lang('Episode.publish'));
|
||||
replace_breadcrumb_params([
|
||||
0 => $this->podcast->at_handle,
|
||||
1 => $this->episode->title,
|
||||
@ -521,6 +527,7 @@ class EpisodeController extends BaseController
|
||||
->first(),
|
||||
];
|
||||
|
||||
$this->setHtmlHead(lang('Episode.publish_edit'));
|
||||
replace_breadcrumb_params([
|
||||
0 => $this->podcast->at_handle,
|
||||
1 => $this->episode->title,
|
||||
@ -670,11 +677,11 @@ class EpisodeController extends BaseController
|
||||
'episode' => $this->episode,
|
||||
];
|
||||
|
||||
$this->setHtmlHead(lang('Episode.publish_date_edit'));
|
||||
replace_breadcrumb_params([
|
||||
0 => $this->podcast->title,
|
||||
1 => $this->episode->title,
|
||||
]);
|
||||
|
||||
return view('episode/publish_date_edit', $data);
|
||||
}
|
||||
|
||||
@ -746,6 +753,7 @@ class EpisodeController extends BaseController
|
||||
'episode' => $this->episode,
|
||||
];
|
||||
|
||||
$this->setHtmlHead(lang('Episode.unpublish'));
|
||||
replace_breadcrumb_params([
|
||||
0 => $this->podcast->title,
|
||||
1 => $this->episode->title,
|
||||
@ -822,6 +830,7 @@ class EpisodeController extends BaseController
|
||||
'episode' => $this->episode,
|
||||
];
|
||||
|
||||
$this->setHtmlHead(lang('Episode.delete'));
|
||||
replace_breadcrumb_params([
|
||||
0 => $this->podcast->at_handle,
|
||||
1 => $this->episode->title,
|
||||
@ -928,6 +937,7 @@ class EpisodeController extends BaseController
|
||||
'themes' => EpisodeModel::$themes,
|
||||
];
|
||||
|
||||
$this->setHtmlHead(lang('Episode.embed.title'));
|
||||
replace_breadcrumb_params([
|
||||
0 => $this->podcast->at_handle,
|
||||
1 => $this->episode->title,
|
||||
|
@ -58,6 +58,8 @@ class EpisodePersonController extends BaseController
|
||||
'personOptions' => (new PersonModel())->getPersonOptions(),
|
||||
'taxonomyOptions' => (new PersonModel())->getTaxonomyOptions(),
|
||||
];
|
||||
|
||||
$this->setHtmlHead(lang('Person.episode_form.title'));
|
||||
replace_breadcrumb_params([
|
||||
0 => $this->podcast->at_handle,
|
||||
1 => $this->episode->title,
|
||||
|
@ -26,6 +26,7 @@ class FediverseController extends BaseController
|
||||
$blockedActors = model('ActorModel', false)
|
||||
->getBlockedActors();
|
||||
|
||||
$this->setHtmlHead(lang('Fediverse.blocked_actors'));
|
||||
return view('fediverse/blocked_actors', [
|
||||
'blockedActors' => $blockedActors,
|
||||
]);
|
||||
@ -38,6 +39,7 @@ class FediverseController extends BaseController
|
||||
$blockedDomains = model('BlockedDomainModel', false)
|
||||
->getBlockedDomains();
|
||||
|
||||
$this->setHtmlHead(lang('Fediverse.blocked_domains'));
|
||||
return view('fediverse/blocked_domains', [
|
||||
'blockedDomains' => $blockedDomains,
|
||||
]);
|
||||
|
@ -66,10 +66,10 @@ class NotificationController extends BaseController
|
||||
'pager' => $notifications->pager,
|
||||
];
|
||||
|
||||
$this->setHtmlHead(lang('Notifications.title'));
|
||||
replace_breadcrumb_params([
|
||||
0 => $this->podcast->at_handle,
|
||||
]);
|
||||
|
||||
return view('podcast/notifications', $data);
|
||||
}
|
||||
|
||||
|
@ -34,6 +34,7 @@ class PageController extends BaseController
|
||||
|
||||
public function list(): string
|
||||
{
|
||||
$this->setHtmlHead(lang('Page.all_pages'));
|
||||
$data = [
|
||||
'pages' => (new PageModel())->findAll(),
|
||||
];
|
||||
@ -43,6 +44,7 @@ class PageController extends BaseController
|
||||
|
||||
public function view(): string
|
||||
{
|
||||
$this->setHtmlHead($this->page->title);
|
||||
return view('page/view', [
|
||||
'page' => $this->page,
|
||||
]);
|
||||
@ -52,6 +54,7 @@ class PageController extends BaseController
|
||||
{
|
||||
helper('form');
|
||||
|
||||
$this->setHtmlHead(lang('Page.create'));
|
||||
return view('page/create');
|
||||
}
|
||||
|
||||
@ -83,6 +86,7 @@ class PageController extends BaseController
|
||||
{
|
||||
helper('form');
|
||||
|
||||
$this->setHtmlHead(lang('Page.edit'));
|
||||
replace_breadcrumb_params([
|
||||
0 => $this->page->title,
|
||||
]);
|
||||
|
@ -42,6 +42,7 @@ class PersonController extends BaseController
|
||||
->findAll(),
|
||||
];
|
||||
|
||||
$this->setHtmlHead(lang('Person.all_persons'));
|
||||
return view('person/list', $data);
|
||||
}
|
||||
|
||||
@ -51,6 +52,7 @@ class PersonController extends BaseController
|
||||
'person' => $this->person,
|
||||
];
|
||||
|
||||
$this->setHtmlHead($this->person->full_name);
|
||||
replace_breadcrumb_params([
|
||||
0 => $this->person->full_name,
|
||||
]);
|
||||
@ -61,6 +63,7 @@ class PersonController extends BaseController
|
||||
{
|
||||
helper(['form']);
|
||||
|
||||
$this->setHtmlHead(lang('Person.create'));
|
||||
return view('person/create');
|
||||
}
|
||||
|
||||
@ -112,6 +115,7 @@ class PersonController extends BaseController
|
||||
'person' => $this->person,
|
||||
];
|
||||
|
||||
$this->setHtmlHead(lang('Person.edit'));
|
||||
replace_breadcrumb_params([
|
||||
0 => $this->person->full_name,
|
||||
]);
|
||||
|
@ -68,6 +68,7 @@ class PodcastController extends BaseController
|
||||
];
|
||||
}
|
||||
|
||||
$this->setHtmlHead(lang('Podcast.all_podcasts'));
|
||||
return view('podcast/list', $data);
|
||||
}
|
||||
|
||||
@ -77,6 +78,7 @@ class PodcastController extends BaseController
|
||||
'podcast' => $this->podcast,
|
||||
];
|
||||
|
||||
$this->setHtmlHead($this->podcast->title);
|
||||
replace_breadcrumb_params([
|
||||
0 => $this->podcast->at_handle,
|
||||
]);
|
||||
@ -89,6 +91,7 @@ class PodcastController extends BaseController
|
||||
'podcast' => $this->podcast,
|
||||
];
|
||||
|
||||
$this->setHtmlHead($this->podcast->title);
|
||||
replace_breadcrumb_params([
|
||||
0 => $this->podcast->at_handle,
|
||||
]);
|
||||
@ -101,6 +104,7 @@ class PodcastController extends BaseController
|
||||
'podcast' => $this->podcast,
|
||||
];
|
||||
|
||||
$this->setHtmlHead($this->podcast->title);
|
||||
replace_breadcrumb_params([
|
||||
0 => $this->podcast->at_handle,
|
||||
]);
|
||||
@ -113,6 +117,7 @@ class PodcastController extends BaseController
|
||||
'podcast' => $this->podcast,
|
||||
];
|
||||
|
||||
$this->setHtmlHead($this->podcast->title);
|
||||
replace_breadcrumb_params([
|
||||
0 => $this->podcast->at_handle,
|
||||
]);
|
||||
@ -125,6 +130,7 @@ class PodcastController extends BaseController
|
||||
'podcast' => $this->podcast,
|
||||
];
|
||||
|
||||
$this->setHtmlHead($this->podcast->title);
|
||||
replace_breadcrumb_params([
|
||||
0 => $this->podcast->at_handle,
|
||||
]);
|
||||
@ -137,6 +143,7 @@ class PodcastController extends BaseController
|
||||
'podcast' => $this->podcast,
|
||||
];
|
||||
|
||||
$this->setHtmlHead($this->podcast->title);
|
||||
replace_breadcrumb_params([
|
||||
0 => $this->podcast->at_handle,
|
||||
]);
|
||||
@ -149,6 +156,7 @@ class PodcastController extends BaseController
|
||||
'podcast' => $this->podcast,
|
||||
];
|
||||
|
||||
$this->setHtmlHead($this->podcast->title);
|
||||
replace_breadcrumb_params([
|
||||
0 => $this->podcast->at_handle,
|
||||
]);
|
||||
@ -161,6 +169,7 @@ class PodcastController extends BaseController
|
||||
'podcast' => $this->podcast,
|
||||
];
|
||||
|
||||
$this->setHtmlHead($this->podcast->title);
|
||||
replace_breadcrumb_params([
|
||||
0 => $this->podcast->at_handle,
|
||||
]);
|
||||
@ -180,6 +189,7 @@ class PodcastController extends BaseController
|
||||
'browserLang' => get_browser_language($this->request->getServer('HTTP_ACCEPT_LANGUAGE')),
|
||||
];
|
||||
|
||||
$this->setHtmlHead(lang('Podcast.create'));
|
||||
return view('podcast/create', $data);
|
||||
}
|
||||
|
||||
@ -270,6 +280,7 @@ class PodcastController extends BaseController
|
||||
'categoryOptions' => $categoryOptions,
|
||||
];
|
||||
|
||||
$this->setHtmlHead(lang('Podcast.edit'));
|
||||
replace_breadcrumb_params([
|
||||
0 => $this->podcast->at_handle,
|
||||
]);
|
||||
@ -410,6 +421,7 @@ class PodcastController extends BaseController
|
||||
'podcast' => $this->podcast,
|
||||
];
|
||||
|
||||
$this->setHtmlHead(lang('Podcast.delete'));
|
||||
replace_breadcrumb_params([
|
||||
0 => $this->podcast->at_handle,
|
||||
]);
|
||||
@ -578,10 +590,10 @@ class PodcastController extends BaseController
|
||||
'podcast' => $this->podcast,
|
||||
];
|
||||
|
||||
$this->setHtmlHead(lang('Podcast.publish'));
|
||||
replace_breadcrumb_params([
|
||||
0 => $this->podcast->at_handle,
|
||||
]);
|
||||
|
||||
return view('podcast/publish', $data);
|
||||
}
|
||||
|
||||
@ -713,10 +725,10 @@ class PodcastController extends BaseController
|
||||
->first(),
|
||||
];
|
||||
|
||||
$this->setHtmlHead(lang('Podcast.publish_edit'));
|
||||
replace_breadcrumb_params([
|
||||
0 => $this->podcast->at_handle,
|
||||
]);
|
||||
|
||||
return view('podcast/publish_edit', $data);
|
||||
}
|
||||
|
||||
|
@ -46,6 +46,8 @@ class PodcastPersonController extends BaseController
|
||||
'personOptions' => (new PersonModel())->getPersonOptions(),
|
||||
'taxonomyOptions' => (new PersonModel())->getTaxonomyOptions(),
|
||||
];
|
||||
|
||||
$this->setHtmlHead(lang('Person.podcast_form.title'));
|
||||
replace_breadcrumb_params([
|
||||
0 => $this->podcast->at_handle,
|
||||
]);
|
||||
|
@ -30,6 +30,7 @@ class SettingsController extends BaseController
|
||||
public function index(): string
|
||||
{
|
||||
helper('form');
|
||||
$this->setHtmlHead(lang('Settings.title'));
|
||||
return view('settings/general');
|
||||
}
|
||||
|
||||
@ -202,6 +203,7 @@ class SettingsController extends BaseController
|
||||
public function theme(): string
|
||||
{
|
||||
helper('form');
|
||||
$this->setHtmlHead(lang('Settings.theme.title'));
|
||||
return view('settings/theme');
|
||||
}
|
||||
|
||||
|
@ -76,6 +76,7 @@ class SoundbiteController extends BaseController
|
||||
'pager' => $soundbitesBuilder->pager,
|
||||
];
|
||||
|
||||
$this->setHtmlHead(lang('Soundbite.list.title'));
|
||||
replace_breadcrumb_params([
|
||||
0 => $this->podcast->at_handle,
|
||||
1 => $this->episode->title,
|
||||
@ -92,6 +93,7 @@ class SoundbiteController extends BaseController
|
||||
'episode' => $this->episode,
|
||||
];
|
||||
|
||||
$this->setHtmlHead(lang('Soundbite.form.title'));
|
||||
replace_breadcrumb_params([
|
||||
0 => $this->podcast->at_handle,
|
||||
1 => $this->episode->title,
|
||||
|
@ -82,6 +82,7 @@ class VideoClipsController extends BaseController
|
||||
'pager' => $videoClipsBuilder->pager,
|
||||
];
|
||||
|
||||
$this->setHtmlHead(lang('VideoClip.list.title'));
|
||||
replace_breadcrumb_params([
|
||||
0 => $this->podcast->at_handle,
|
||||
1 => $this->episode->title,
|
||||
@ -99,6 +100,9 @@ class VideoClipsController extends BaseController
|
||||
'videoClip' => $videoClip,
|
||||
];
|
||||
|
||||
$this->setHtmlHead(lang('VideoClip.title', [
|
||||
'videoClipLabel' => esc($videoClip->title),
|
||||
]));
|
||||
replace_breadcrumb_params([
|
||||
0 => $this->podcast->at_handle,
|
||||
1 => $this->episode->title,
|
||||
@ -128,9 +132,10 @@ class VideoClipsController extends BaseController
|
||||
'transcript' => $this->episode->transcript instanceof Transcript,
|
||||
];
|
||||
|
||||
$this->setHtmlHead(lang('VideoClip.form.title'));
|
||||
|
||||
if (in_array(false, $checks, true)) {
|
||||
$data['checks'] = $checks;
|
||||
|
||||
return view('episode/video_clips_requirements', $data);
|
||||
}
|
||||
|
||||
|
@ -2,15 +2,13 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use CodeIgniter\Router\RouteCollection;
|
||||
|
||||
/**
|
||||
* @copyright 2021 Ad Aures
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
/** @var RouteCollection $routes */
|
||||
/** @var \CodeIgniter\Router\RouteCollection $routes */
|
||||
|
||||
/**
|
||||
* Analytics routes file
|
||||
|
@ -13,6 +13,7 @@ declare(strict_types=1);
|
||||
namespace Modules\Analytics\Entities;
|
||||
|
||||
use CodeIgniter\Entity\Entity;
|
||||
use CodeIgniter\I18n\Time;
|
||||
|
||||
/**
|
||||
* @property int $podcast_id
|
||||
|
@ -13,6 +13,7 @@ declare(strict_types=1);
|
||||
namespace Modules\Analytics\Entities;
|
||||
|
||||
use CodeIgniter\Entity\Entity;
|
||||
use CodeIgniter\I18n\Time;
|
||||
|
||||
/**
|
||||
* @property int $podcast_id
|
||||
|
@ -13,6 +13,7 @@ declare(strict_types=1);
|
||||
namespace Modules\Analytics\Entities;
|
||||
|
||||
use CodeIgniter\Entity\Entity;
|
||||
use CodeIgniter\I18n\Time;
|
||||
|
||||
/**
|
||||
* @property int $podcast_id
|
||||
|
@ -13,6 +13,7 @@ declare(strict_types=1);
|
||||
namespace Modules\Analytics\Entities;
|
||||
|
||||
use CodeIgniter\Entity\Entity;
|
||||
use CodeIgniter\I18n\Time;
|
||||
|
||||
/**
|
||||
* @property int $podcast_id
|
||||
|
@ -13,6 +13,7 @@ declare(strict_types=1);
|
||||
namespace Modules\Analytics\Entities;
|
||||
|
||||
use CodeIgniter\Entity\Entity;
|
||||
use CodeIgniter\I18n\Time;
|
||||
|
||||
/**
|
||||
* @property int $podcast_id
|
||||
|
@ -13,6 +13,7 @@ declare(strict_types=1);
|
||||
namespace Modules\Analytics\Entities;
|
||||
|
||||
use CodeIgniter\Entity\Entity;
|
||||
use CodeIgniter\I18n\Time;
|
||||
|
||||
/**
|
||||
* @property int $podcast_id
|
||||
|
@ -13,6 +13,7 @@ declare(strict_types=1);
|
||||
namespace Modules\Analytics\Entities;
|
||||
|
||||
use CodeIgniter\Entity\Entity;
|
||||
use CodeIgniter\I18n\Time;
|
||||
use Opawg\UserAgentsV2Php\UserAgentsRSS;
|
||||
|
||||
/**
|
||||
|
@ -11,6 +11,7 @@ declare(strict_types=1);
|
||||
namespace Modules\Analytics\Entities;
|
||||
|
||||
use CodeIgniter\Entity\Entity;
|
||||
use CodeIgniter\I18n\Time;
|
||||
|
||||
/**
|
||||
* @property int $podcast_id
|
||||
|
@ -13,6 +13,7 @@ declare(strict_types=1);
|
||||
namespace Modules\Analytics\Entities;
|
||||
|
||||
use CodeIgniter\Entity\Entity;
|
||||
use CodeIgniter\I18n\Time;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
|
@ -13,6 +13,7 @@ declare(strict_types=1);
|
||||
namespace Modules\Analytics\Entities;
|
||||
|
||||
use CodeIgniter\Entity\Entity;
|
||||
use CodeIgniter\I18n\Time;
|
||||
|
||||
/**
|
||||
* @property int $podcast_id
|
||||
|
@ -13,6 +13,7 @@ declare(strict_types=1);
|
||||
namespace Modules\Analytics\Entities;
|
||||
|
||||
use CodeIgniter\Entity\Entity;
|
||||
use CodeIgniter\I18n\Time;
|
||||
|
||||
/**
|
||||
* @property int $podcast_id
|
||||
|
@ -13,6 +13,7 @@ declare(strict_types=1);
|
||||
namespace Modules\Analytics\Entities;
|
||||
|
||||
use CodeIgniter\Entity\Entity;
|
||||
use CodeIgniter\I18n\Time;
|
||||
|
||||
/**
|
||||
* @property int $podcast_id
|
||||
|
@ -286,8 +286,7 @@ if (! function_exists('podcast_hit')) {
|
||||
$parts = explode('-', $range);
|
||||
$downloadedBytes += array_key_exists(1, $parts)
|
||||
? $fileSize
|
||||
: (int) $parts[1] -
|
||||
(array_key_exists(0, $parts) ? 0 : (int) $parts[0]);
|
||||
: (int) $parts[1] - (int) $parts[0];
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -71,7 +71,7 @@ class EpisodeController extends Controller
|
||||
protected static function mapEpisode(Episode $episode): Episode
|
||||
{
|
||||
$episode->cover_url = $episode->getCover()
|
||||
->file_url;
|
||||
->file_url;
|
||||
$episode->duration = round($episode->audio->duration);
|
||||
|
||||
return $episode;
|
||||
|
@ -44,9 +44,9 @@ class PodcastController extends Controller
|
||||
{
|
||||
$podcast->feed_url = $podcast->getFeedUrl();
|
||||
$podcast->actor_display_name = $podcast->getActor()
|
||||
->display_name;
|
||||
->display_name;
|
||||
$podcast->cover_url = $podcast->getCover()
|
||||
->file_url;
|
||||
->file_url;
|
||||
|
||||
$categories = [$podcast->getCategory(), ...$podcast->getOtherCategories()];
|
||||
|
||||
|
@ -56,6 +56,7 @@ class ContributorController extends BaseController
|
||||
'podcast' => $this->podcast,
|
||||
];
|
||||
|
||||
$this->setHtmlHead(lang('Contributor.podcast_contributors'));
|
||||
replace_breadcrumb_params([
|
||||
0 => $this->podcast->at_handle,
|
||||
]);
|
||||
@ -69,6 +70,10 @@ class ContributorController extends BaseController
|
||||
'contributor' => (new UserModel())->getPodcastContributor($this->contributor->id, $this->podcast->id),
|
||||
];
|
||||
|
||||
$this->setHtmlHead(lang('Contributor.view', [
|
||||
'username' => esc($this->contributor->username),
|
||||
'podcastTitle' => esc($this->podcast->title),
|
||||
]));
|
||||
replace_breadcrumb_params([
|
||||
0 => $this->podcast->at_handle,
|
||||
1 => $this->contributor->username,
|
||||
@ -113,6 +118,7 @@ class ContributorController extends BaseController
|
||||
'roleOptions' => $roleOptions,
|
||||
];
|
||||
|
||||
$this->setHtmlHead(lang('Contributor.add_contributor', [esc($this->podcast->title)]));
|
||||
replace_breadcrumb_params([
|
||||
0 => $this->podcast->at_handle,
|
||||
]);
|
||||
@ -170,6 +176,7 @@ class ContributorController extends BaseController
|
||||
'roleOptions' => $roleOptions,
|
||||
];
|
||||
|
||||
$this->setHtmlHead(lang('Contributor.edit_role', [esc($this->contributor->username)]));
|
||||
replace_breadcrumb_params([
|
||||
0 => $this->podcast->at_handle,
|
||||
1 => $this->contributor->username,
|
||||
@ -208,6 +215,9 @@ class ContributorController extends BaseController
|
||||
'contributor' => $this->contributor,
|
||||
];
|
||||
|
||||
$this->setHtmlHead(lang('Contributor.delete_form.title', [
|
||||
'contributor' => $this->contributor->username,
|
||||
]));
|
||||
replace_breadcrumb_params([
|
||||
0 => $this->podcast->at_handle,
|
||||
1 => $this->contributor->username,
|
||||
|
@ -18,6 +18,7 @@ class MyAccountController extends BaseController
|
||||
{
|
||||
public function index(): string
|
||||
{
|
||||
$this->setHtmlHead(lang('MyAccount.info'));
|
||||
return view('my_account/view');
|
||||
}
|
||||
|
||||
@ -25,6 +26,7 @@ class MyAccountController extends BaseController
|
||||
{
|
||||
helper('form');
|
||||
|
||||
$this->setHtmlHead(lang('MyAccount.changePassword'));
|
||||
return view('my_account/change_password');
|
||||
}
|
||||
|
||||
|
@ -42,6 +42,7 @@ class UserController extends BaseController
|
||||
'users' => (new UserModel())->findAll(),
|
||||
];
|
||||
|
||||
$this->setHtmlHead(lang('User.all_users'));
|
||||
return view('user/list', $data);
|
||||
}
|
||||
|
||||
@ -51,6 +52,9 @@ class UserController extends BaseController
|
||||
'user' => $this->user,
|
||||
];
|
||||
|
||||
$this->setHtmlHead(lang('User.view', [
|
||||
'username' => esc($this->user->username),
|
||||
]));
|
||||
replace_breadcrumb_params([
|
||||
0 => $this->user->username,
|
||||
]);
|
||||
@ -76,6 +80,7 @@ class UserController extends BaseController
|
||||
'roleOptions' => $roleOptions,
|
||||
];
|
||||
|
||||
$this->setHtmlHead(lang('User.create'));
|
||||
return view('user/create', $data);
|
||||
}
|
||||
|
||||
@ -182,6 +187,9 @@ class UserController extends BaseController
|
||||
'roleOptions' => $roleOptions,
|
||||
];
|
||||
|
||||
$this->setHtmlHead(lang('User.edit_role', [
|
||||
'username' => esc($this->user->username),
|
||||
]));
|
||||
replace_breadcrumb_params([
|
||||
0 => $this->user->username,
|
||||
]);
|
||||
@ -221,6 +229,9 @@ class UserController extends BaseController
|
||||
'user' => $this->user,
|
||||
];
|
||||
|
||||
$this->setHtmlHead(lang('User.delete_form.title', [
|
||||
'user' => $this->user->username,
|
||||
]));
|
||||
replace_breadcrumb_params([
|
||||
0 => $this->user->username,
|
||||
]);
|
||||
|
@ -10,6 +10,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Modules\Fediverse\Entities;
|
||||
|
||||
use CodeIgniter\I18n\Time;
|
||||
use Michalsn\Uuid\UuidEntity;
|
||||
use RuntimeException;
|
||||
|
||||
|
@ -41,7 +41,7 @@ if (! function_exists('split_handle')) {
|
||||
/**
|
||||
* Splits handle into its parts (username, host and port)
|
||||
*
|
||||
* @return array<string, string>|false
|
||||
* @return array{0:string,username:non-empty-string,1:non-empty-string,domain:non-empty-string,2:non-empty-string,port?:non-falsy-string,3?:non-falsy-string}
|
||||
*/
|
||||
function split_handle(string $handle): array | false
|
||||
{
|
||||
|
@ -101,7 +101,7 @@ class WebFinger
|
||||
/**
|
||||
* Split resource into its parts (username, domain)
|
||||
*
|
||||
* @return array<string, string>|false
|
||||
* @return array{0:string,username:non-empty-string,1:non-empty-string,2:non-empty-string,domain:non-falsy-string,3:non-falsy-string,4:non-falsy-string,5?:non-falsy-string}
|
||||
*/
|
||||
private function splitResource(string $resource): bool|array
|
||||
{
|
||||
|
@ -25,7 +25,7 @@ use RuntimeException;
|
||||
* @property string $file_extension
|
||||
* @property int $file_size
|
||||
* @property string $file_mimetype
|
||||
* @property array|null $file_metadata
|
||||
* @property array<mixed>|null $file_metadata
|
||||
* @property 'image'|'audio'|'video'|'document' $type
|
||||
* @property string|null $description
|
||||
* @property string|null $language_code
|
||||
|
@ -16,7 +16,7 @@ use GdImage;
|
||||
use Override;
|
||||
|
||||
/**
|
||||
* @property array $sizes
|
||||
* @property array<string, array<string, int|string>> $sizes
|
||||
*/
|
||||
class Image extends BaseMedia
|
||||
{
|
||||
|
@ -34,7 +34,7 @@ if (! function_exists('download_file')) {
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, ['User-Agent: Castopod/' . CP_VERSION]);
|
||||
|
||||
// follow redirects up to 20, like Apple Podcasts
|
||||
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
|
||||
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
|
||||
curl_setopt($ch, CURLOPT_MAXREDIRS, 20);
|
||||
|
||||
curl_exec($ch);
|
||||
|
@ -55,10 +55,10 @@ class PlatformController extends BaseController
|
||||
'platforms' => (new PlatformModel())->getPlatformsWithData($this->podcast->id, $platformType),
|
||||
];
|
||||
|
||||
$this->setHtmlHead(lang("Platforms.title.{$platformType}"));
|
||||
replace_breadcrumb_params([
|
||||
0 => $this->podcast->at_handle,
|
||||
]);
|
||||
|
||||
return view('podcast/platforms', $data);
|
||||
}
|
||||
|
||||
|
@ -127,7 +127,7 @@ class PlatformModel extends Model
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<array<string, string|int>> $data
|
||||
* @param array<array<string, bool|int|string|null>> $data
|
||||
*
|
||||
* @return int|false Number of rows inserted or FALSE on failure
|
||||
*/
|
||||
|
@ -458,7 +458,7 @@ class Platforms
|
||||
}
|
||||
|
||||
/**
|
||||
* @return null|array{label:string,home_url:string,submit_url:?string}>
|
||||
* @return null|array{label:string,home_url:string,submit_url:?string}
|
||||
*/
|
||||
public function findPlatformBySlug(string $type, string $slug): ?array
|
||||
{
|
||||
|
@ -16,10 +16,10 @@ class CreatePlugin extends BaseCommand
|
||||
{
|
||||
protected const HOOKS_IMPORTS = [
|
||||
'rssBeforeChannel' => ['use App\Entities\Podcast;'],
|
||||
'rssAfterChannel' => ['use App\Entities\Podcast;', 'use App\Libraries\SimpleRSSElement;'],
|
||||
'rssAfterChannel' => ['use App\Entities\Podcast;', 'use App\Libraries\RssFeed;'],
|
||||
'rssBeforeItem' => ['use App\Entities\Episode;'],
|
||||
'rssAfterItem' => ['use App\Entities\Episode;', 'use App\Libraries\SimpleRSSElement;'],
|
||||
'siteHead' => [],
|
||||
'rssAfterItem' => ['use App\Entities\Episode;', 'use App\Libraries\RssFeed;'],
|
||||
'siteHead' => ['use use App\Libraries\HtmlHead'],
|
||||
];
|
||||
|
||||
protected const HOOKS_METHODS = [
|
||||
@ -27,7 +27,7 @@ class CreatePlugin extends BaseCommand
|
||||
{
|
||||
// YOUR CODE HERE
|
||||
}',
|
||||
'rssAfterChannel' => ' public function rssAfterChannel(Podcast $podcast, SimpleRSSElement $channel): void
|
||||
'rssAfterChannel' => ' public function rssAfterChannel(Podcast $podcast, RssFeed $channel): void
|
||||
{
|
||||
// YOUR CODE HERE
|
||||
}',
|
||||
@ -35,11 +35,11 @@ class CreatePlugin extends BaseCommand
|
||||
{
|
||||
// YOUR CODE HERE
|
||||
}',
|
||||
'rssAfterItem' => ' public function rssAfterItem(Episode $episode, SimpleRSSElement $item): void
|
||||
'rssAfterItem' => ' public function rssAfterItem(Episode $episode, RssFeed $item): void
|
||||
{
|
||||
// YOUR CODE HERE
|
||||
}',
|
||||
'siteHead' => ' public function siteHead(): void
|
||||
'siteHead' => ' public function siteHead(HtmlHead $head): void
|
||||
{
|
||||
// YOUR CODE HERE
|
||||
}',
|
||||
|
@ -8,7 +8,7 @@ use CodeIgniter\Router\RouteCollection;
|
||||
|
||||
$routes->group(
|
||||
config('Admin')
|
||||
->gateway,
|
||||
->gateway,
|
||||
[
|
||||
'namespace' => 'Modules\Plugins\Controllers',
|
||||
],
|
||||
|
@ -38,6 +38,7 @@ class PluginController extends BaseController
|
||||
|
||||
$pager_links = $pager->makeLinks($page, $perPage, $total);
|
||||
|
||||
$this->setHtmlHead(lang('Plugins.installed'));
|
||||
return view('plugins/installed', [
|
||||
'total' => $total,
|
||||
'plugins' => $this->plugins->getPlugins($page, $perPage),
|
||||
@ -47,8 +48,9 @@ class PluginController extends BaseController
|
||||
|
||||
public function vendor(string $vendor): string
|
||||
{
|
||||
|
||||
$vendorPlugins = $this->plugins->getVendorPlugins($vendor);
|
||||
|
||||
$this->setHtmlHead(lang('Plugins.installed'));
|
||||
replace_breadcrumb_params([
|
||||
$vendor => $vendor,
|
||||
]);
|
||||
@ -68,6 +70,7 @@ class PluginController extends BaseController
|
||||
throw PageNotFoundException::forPageNotFound();
|
||||
}
|
||||
|
||||
$this->setHtmlHead($plugin->getTitle());
|
||||
replace_breadcrumb_params([
|
||||
$vendor => $vendor,
|
||||
$package => $package,
|
||||
@ -137,6 +140,10 @@ class PluginController extends BaseController
|
||||
$data['fields'] = $fields;
|
||||
|
||||
helper('form');
|
||||
$this->setHtmlHead(lang('Plugins.settingsTitle', [
|
||||
'pluginTitle' => $plugin->getTitle(),
|
||||
'type' => $type,
|
||||
]));
|
||||
replace_breadcrumb_params($breadcrumbReplacements);
|
||||
return view('plugins/settings', $data);
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ namespace Modules\Plugins\Core;
|
||||
|
||||
use App\Entities\Episode;
|
||||
use App\Entities\Podcast;
|
||||
use App\Libraries\HtmlHead;
|
||||
use App\Libraries\RssFeed;
|
||||
use CodeIgniter\HTTP\URI;
|
||||
use League\CommonMark\Environment\Environment;
|
||||
@ -101,7 +102,7 @@ abstract class BasePlugin implements PluginInterface
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function siteHead(): void
|
||||
public function siteHead(HtmlHead $head): void
|
||||
{
|
||||
}
|
||||
|
||||
@ -264,7 +265,7 @@ abstract class BasePlugin implements PluginInterface
|
||||
return $title;
|
||||
}
|
||||
|
||||
final public function getDescription(): ?string
|
||||
final public function getDescription(): string
|
||||
{
|
||||
$key = sprintf('Plugin.%s.description', $this->key);
|
||||
|
||||
|
@ -6,6 +6,7 @@ namespace Modules\Plugins\Core;
|
||||
|
||||
use App\Entities\Episode;
|
||||
use App\Entities\Podcast;
|
||||
use App\Libraries\HtmlHead;
|
||||
use App\Libraries\RssFeed;
|
||||
|
||||
interface PluginInterface
|
||||
@ -18,5 +19,5 @@ interface PluginInterface
|
||||
|
||||
public function rssAfterItem(Episode $episode, RssFeed $item): void;
|
||||
|
||||
public function siteHead(): void;
|
||||
public function siteHead(HtmlHead $head): void;
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ namespace Modules\Plugins\Core;
|
||||
|
||||
use App\Entities\Episode;
|
||||
use App\Entities\Podcast;
|
||||
use App\Libraries\HtmlHead;
|
||||
use App\Libraries\RssFeed;
|
||||
use Config\Database;
|
||||
use Modules\Plugins\Config\Plugins as PluginsConfig;
|
||||
@ -15,7 +16,7 @@ use Modules\Plugins\Config\Plugins as PluginsConfig;
|
||||
* @method void rssAfterChannel(Podcast $podcast, RssFeed $channel)
|
||||
* @method void rssBeforeItem(Episode $episode)
|
||||
* @method void rssAfterItem(Episode $episode, RssFeed $item)
|
||||
* @method void siteHead()
|
||||
* @method void siteHead(HtmlHead $head)
|
||||
*/
|
||||
class Plugins
|
||||
{
|
||||
@ -29,6 +30,7 @@ class Plugins
|
||||
'datetime' => ['valid_date[Y-m-d H:i]'],
|
||||
'email' => ['valid_email'],
|
||||
'group' => ['permit_empty', 'is_list'],
|
||||
'html' => ['string'],
|
||||
'markdown' => ['string'],
|
||||
'number' => ['integer'],
|
||||
'radio-group' => ['string'],
|
||||
|
@ -7,7 +7,7 @@ namespace Modules\Plugins\Manifest;
|
||||
use Override;
|
||||
|
||||
/**
|
||||
* @property 'checkbox'|'datetime'|'email'|'group'|'markdown'|'number'|'radio-group'|'rss'|'select-multiple'|'select'|'text'|'textarea'|'toggler'|'url' $type
|
||||
* @property 'checkbox'|'datetime'|'email'|'group'|'html'|'markdown'|'number'|'radio-group'|'rss'|'select-multiple'|'select'|'text'|'textarea'|'toggler'|'url' $type
|
||||
* @property string $key
|
||||
* @property string $label
|
||||
* @property string $hint
|
||||
@ -20,7 +20,7 @@ use Override;
|
||||
class Field extends ManifestObject
|
||||
{
|
||||
protected const VALIDATION_RULES = [
|
||||
'type' => 'permit_empty|in_list[checkbox,datetime,email,group,markdown,number,radio-group,rss,select-multiple,select,text,textarea,toggler,url]',
|
||||
'type' => 'permit_empty|in_list[checkbox,datetime,email,group,html,markdown,number,radio-group,rss,select-multiple,select,text,textarea,toggler,url]',
|
||||
'key' => 'required|alpha_dash',
|
||||
'label' => 'required|string',
|
||||
'hint' => 'permit_empty|string',
|
||||
|
@ -178,6 +178,7 @@
|
||||
"datetime",
|
||||
"email",
|
||||
"group",
|
||||
"html",
|
||||
"markdown",
|
||||
"number",
|
||||
"radio-group",
|
||||
|
@ -160,7 +160,7 @@ class PodcastImport extends BaseCommand
|
||||
|
||||
$podcastModel = new PodcastModel();
|
||||
if (! $podcastModel->update($this->podcast->id, $this->podcast)) {
|
||||
throw new Exception((string) print_r($podcastModel->errors()));
|
||||
throw new Exception(print_r($podcastModel->errors(), true));
|
||||
}
|
||||
|
||||
CLI::showProgress(false);
|
||||
@ -260,7 +260,7 @@ class PodcastImport extends BaseCommand
|
||||
$podcastModel = new PodcastModel();
|
||||
if (! ($podcastId = $podcastModel->insert($podcast, true))) {
|
||||
$db->transRollback();
|
||||
throw new Exception((string) print_r($podcastModel->errors()));
|
||||
throw new Exception(print_r($podcastModel->errors(), true));
|
||||
}
|
||||
|
||||
$podcast->id = $podcastId;
|
||||
@ -326,7 +326,7 @@ class PodcastImport extends BaseCommand
|
||||
]);
|
||||
|
||||
if (! $newPersonId = $personModel->insert($newPodcastPerson)) {
|
||||
throw new Exception((string) print_r($personModel->errors()));
|
||||
throw new Exception(print_r($personModel->errors(), true));
|
||||
}
|
||||
}
|
||||
|
||||
@ -353,7 +353,7 @@ class PodcastImport extends BaseCommand
|
||||
$personGroupSlug,
|
||||
$personRoleSlug
|
||||
)) {
|
||||
throw new Exception((string) print_r($podcastPersonModel->errors()));
|
||||
throw new Exception(print_r($podcastPersonModel->errors(), true));
|
||||
}
|
||||
}
|
||||
|
||||
@ -498,7 +498,7 @@ class PodcastImport extends BaseCommand
|
||||
|
||||
if (! ($episodeId = $episodeModel->insert($episode, true))) {
|
||||
$db->transRollback();
|
||||
throw new Exception((string) print_r($episodeModel->errors()));
|
||||
throw new Exception(print_r($episodeModel->errors(), true));
|
||||
}
|
||||
|
||||
$this->importEpisodePersons($episodeId, $item->podcast_persons);
|
||||
@ -546,7 +546,7 @@ class PodcastImport extends BaseCommand
|
||||
]);
|
||||
|
||||
if (! ($newPersonId = $personModel->insert($newPerson))) {
|
||||
throw new Exception((string) print_r($personModel->errors()));
|
||||
throw new Exception(print_r($personModel->errors(), true));
|
||||
}
|
||||
}
|
||||
|
||||
@ -574,7 +574,7 @@ class PodcastImport extends BaseCommand
|
||||
$personGroupSlug,
|
||||
$personRoleSlug
|
||||
)) {
|
||||
throw new Exception((string) print_r($episodePersonModel->errors()));
|
||||
throw new Exception(print_r($episodePersonModel->errors(), true));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -28,6 +28,7 @@ class PodcastImportController extends BaseController
|
||||
{
|
||||
helper('podcast_import');
|
||||
|
||||
$this->setHtmlHead(lang('Podcast.all_imports'));
|
||||
return view('import/queue', [
|
||||
'podcastImportsQueue' => get_import_tasks(),
|
||||
]);
|
||||
@ -41,6 +42,7 @@ class PodcastImportController extends BaseController
|
||||
|
||||
helper('podcast_import');
|
||||
|
||||
$this->setHtmlHead(lang('Podcast.all_imports'));
|
||||
replace_breadcrumb_params([
|
||||
0 => $podcast->at_handle,
|
||||
]);
|
||||
@ -63,6 +65,7 @@ class PodcastImportController extends BaseController
|
||||
'browserLang' => get_browser_language($this->request->getServer('HTTP_ACCEPT_LANGUAGE')),
|
||||
];
|
||||
|
||||
$this->setHtmlHead(lang('Podcast.import'));
|
||||
return view('import/add_to_queue', $data);
|
||||
}
|
||||
|
||||
@ -112,6 +115,7 @@ class PodcastImportController extends BaseController
|
||||
|
||||
helper('form');
|
||||
|
||||
$this->setHtmlHead(lang('PodcastImport.syncForm.title'));
|
||||
replace_breadcrumb_params([
|
||||
0 => $podcast->at_handle,
|
||||
]);
|
||||
|
@ -21,10 +21,6 @@ if (! function_exists('get_import_tasks')) {
|
||||
$podcastImportsQueue = service('settings')
|
||||
->get('Import.queue') ?? [];
|
||||
|
||||
if (! is_array($podcastImportsQueue)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if ($podcastHandle !== null) {
|
||||
$podcastImportsQueue = array_filter(
|
||||
$podcastImportsQueue,
|
||||
@ -48,6 +44,6 @@ if (! function_exists('get_import_tasks')) {
|
||||
return $a->created_at->isAfter($b->created_at) ? -1 : 1;
|
||||
});
|
||||
|
||||
return array_values($podcastImportsQueue);
|
||||
return $podcastImportsQueue;
|
||||
}
|
||||
}
|
||||
|
@ -59,6 +59,7 @@ class SubscriptionController extends BaseController
|
||||
|
||||
helper('form');
|
||||
|
||||
$this->setHtmlHead(lang('Subscription.podcast_subscriptions'));
|
||||
replace_breadcrumb_params([
|
||||
0 => $this->podcast->at_handle,
|
||||
]);
|
||||
@ -109,6 +110,7 @@ class SubscriptionController extends BaseController
|
||||
'subscription' => $this->subscription,
|
||||
];
|
||||
|
||||
$this->setHtmlHead(lang('Subscription.view', [$this->subscription->id]));
|
||||
replace_breadcrumb_params([
|
||||
0 => $this->podcast->at_handle,
|
||||
1 => '#' . $this->subscription->id,
|
||||
@ -124,6 +126,7 @@ class SubscriptionController extends BaseController
|
||||
'podcast' => $this->podcast,
|
||||
];
|
||||
|
||||
$this->setHtmlHead(lang('Subscription.add', [esc($this->podcast->title)]));
|
||||
replace_breadcrumb_params([
|
||||
0 => $this->podcast->at_handle,
|
||||
]);
|
||||
@ -250,6 +253,7 @@ class SubscriptionController extends BaseController
|
||||
'subscription' => $this->subscription,
|
||||
];
|
||||
|
||||
$this->setHtmlHead(lang('Subscription.edit', [esc($this->podcast->title)]));
|
||||
replace_breadcrumb_params([
|
||||
0 => $this->podcast->at_handle,
|
||||
1 => '#' . $this->subscription->id,
|
||||
@ -318,6 +322,7 @@ class SubscriptionController extends BaseController
|
||||
'subscription' => $this->subscription,
|
||||
];
|
||||
|
||||
$this->setHtmlHead(lang('Subscription.suspend'));
|
||||
replace_breadcrumb_params([
|
||||
0 => $this->podcast->at_handle,
|
||||
1 => '#' . $this->subscription->id,
|
||||
@ -413,6 +418,7 @@ class SubscriptionController extends BaseController
|
||||
'subscription' => $this->subscription,
|
||||
];
|
||||
|
||||
$this->setHtmlHead(lang('Subscription.delete'));
|
||||
replace_breadcrumb_params([
|
||||
0 => $this->podcast->at_handle,
|
||||
1 => '#' . $this->subscription->id,
|
||||
|
@ -58,9 +58,7 @@ class Subscription extends Entity
|
||||
|
||||
public function getStatus(): string
|
||||
{
|
||||
return ($this->expires_at instanceof Time && $this->expires_at->isBefore(
|
||||
Time::now()
|
||||
)) ? 'expired' : $this->attributes['status'];
|
||||
return $this->expires_at->isBefore(Time::now()) ? 'expired' : $this->attributes['status'];
|
||||
}
|
||||
|
||||
/**
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user