From dfb7888aeb689b4066abc37084e08cd7f1d0f15d Mon Sep 17 00:00:00 2001 From: Yassine Doghri Date: Thu, 9 May 2024 17:55:41 +0000 Subject: [PATCH] feat(plugins): add aside with plugin metadata next to plugin's readme - enhance plugin card ui - refactor components to be more consistent - invert toggler label for better UX - edit view components regex --- app/Helpers/breadcrumb_helper.php | 2 +- app/Helpers/components_helper.php | 53 ++----- app/Helpers/page_helper.php | 12 +- app/Libraries/Breadcrumb.php | 21 ++- app/Libraries/ViewComponents/Component.php | 50 +++++- .../ViewComponents/ComponentRenderer.php | 29 ++-- .../Helpers/viewcomponents_helper.php | 33 ---- app/Resources/styles/breadcrumb.css | 4 - app/Resources/styles/choices.css | 8 - app/Resources/styles/custom.css | 9 ++ app/Resources/styles/readMore.css | 2 +- app/Views/Components/Alert.php | 35 ++-- app/Views/Components/Button.php | 104 ++++++------ .../Components/Charts/ChartsComponent.php | 10 +- app/Views/Components/DashboardCard.php | 8 +- app/Views/Components/DropdownMenu.php | 38 +++-- app/Views/Components/Forms/Checkbox.php | 36 +++-- .../Components/Forms/ColorRadioButton.php | 17 +- app/Views/Components/Forms/DatetimePicker.php | 17 +- app/Views/Components/Forms/Field.php | 71 ++++++--- app/Views/Components/Forms/FormComponent.php | 60 ++++--- app/Views/Components/Forms/Helper.php | 13 +- app/Views/Components/Forms/Input.php | 19 ++- app/Views/Components/Forms/Label.php | 31 ++-- app/Views/Components/Forms/MarkdownEditor.php | 26 +-- app/Views/Components/Forms/MultiSelect.php | 22 ++- app/Views/Components/Forms/Radio.php | 17 +- app/Views/Components/Forms/RadioButton.php | 24 +-- app/Views/Components/Forms/Section.php | 14 +- app/Views/Components/Forms/Select.php | 19 +-- app/Views/Components/Forms/Textarea.php | 4 +- app/Views/Components/Forms/Toggler.php | 47 +++--- app/Views/Components/Forms/XMLEditor.php | 2 + app/Views/Components/Heading.php | 13 +- app/Views/Components/Hint.php | 28 ++++ app/Views/Components/IconButton.php | 18 ++- app/Views/Components/Pill.php | 33 ++-- app/Views/Components/ReadMore.php | 14 +- app/Views/Components/SeeMore.php | 10 +- app/Views/_message_block.php | 6 +- app/Views/errors/html/error_403.php | 2 +- app/Views/errors/html/error_404.php | 2 +- app/Views/errors/html/production.php | 6 +- .../Plugins/Controllers/PluginController.php | 21 ++- modules/Plugins/Core/BasePlugin.php | 42 ++++- modules/Plugins/Core/Plugins.php | 26 +++ modules/Plugins/ExternalImageProcessor.php | 9 -- modules/Plugins/ExternalLinkProcessor.php | 9 -- modules/Plugins/Language/en/Plugins.php | 12 +- modules/Plugins/Manifest/Manifest.php | 27 ++-- modules/Plugins/Manifest/ManifestObject.php | 2 +- .../Manifest/{Author.php => Person.php} | 2 +- modules/Plugins/Manifest/Repository.php | 34 ++++ modules/Plugins/Manifest/SettingsField.php | 4 +- modules/Plugins/Manifest/schema.json | 3 - tailwind.config.cjs | 3 + themes/cp_admin/_layout.php | 8 +- themes/cp_admin/_message_block.php | 14 +- themes/cp_admin/_partials/_nav_aside.php | 2 +- themes/cp_admin/_partials/_nav_header.php | 14 +- themes/cp_admin/_partials/_nav_menu.php | 13 +- themes/cp_admin/_sidebar.php | 17 +- themes/cp_admin/contributor/create.php | 10 +- themes/cp_admin/contributor/delete.php | 12 +- themes/cp_admin/contributor/edit.php | 6 +- themes/cp_admin/contributor/list.php | 8 +- themes/cp_admin/dashboard.php | 16 +- themes/cp_admin/episode/_card.php | 4 +- themes/cp_admin/episode/_sidebar.php | 6 +- themes/cp_admin/episode/create.php | 111 +++++++------ themes/cp_admin/episode/delete.php | 8 +- themes/cp_admin/episode/edit.php | 117 +++++++------- themes/cp_admin/episode/embed.php | 8 +- themes/cp_admin/episode/list.php | 14 +- themes/cp_admin/episode/persons.php | 16 +- themes/cp_admin/episode/publish.php | 18 +-- themes/cp_admin/episode/publish_date_edit.php | 6 +- themes/cp_admin/episode/publish_edit.php | 16 +- themes/cp_admin/episode/soundbites_list.php | 6 +- themes/cp_admin/episode/soundbites_new.php | 6 +- themes/cp_admin/episode/unpublish.php | 8 +- themes/cp_admin/episode/video_clips_list.php | 12 +- themes/cp_admin/episode/video_clips_new.php | 34 ++-- .../episode/video_clips_requirements.php | 4 +- themes/cp_admin/episode/view.php | 10 +- themes/cp_admin/fediverse/blocked_actors.php | 8 +- themes/cp_admin/fediverse/blocked_domains.php | 8 +- themes/cp_admin/import/_queue_table.php | 12 +- themes/cp_admin/import/add_to_queue.php | 28 ++-- themes/cp_admin/import/podcast_queue.php | 2 +- themes/cp_admin/import/podcast_sync.php | 6 +- themes/cp_admin/import/queue.php | 2 +- .../cp_admin/my_account/change_password.php | 10 +- themes/cp_admin/page/create.php | 14 +- themes/cp_admin/page/edit.php | 14 +- themes/cp_admin/page/list.php | 8 +- themes/cp_admin/page/view.php | 2 +- themes/cp_admin/person/_card.php | 4 +- themes/cp_admin/person/create.php | 14 +- themes/cp_admin/person/edit.php | 14 +- themes/cp_admin/person/list.php | 2 +- themes/cp_admin/person/view.php | 2 +- themes/cp_admin/plugins/_plugin.php | 88 +++++++---- themes/cp_admin/plugins/_settings.php | 4 +- themes/cp_admin/plugins/installed.php | 8 +- themes/cp_admin/plugins/view.php | 79 +++++++++- themes/cp_admin/podcast/_card.php | 4 +- themes/cp_admin/podcast/_platform.php | 10 +- themes/cp_admin/podcast/_sidebar.php | 8 +- themes/cp_admin/podcast/analytics/index.php | 6 +- .../podcast/analytics/listening_time.php | 4 +- .../cp_admin/podcast/analytics/locations.php | 6 +- themes/cp_admin/podcast/analytics/players.php | 10 +- .../podcast/analytics/time_periods.php | 4 +- .../podcast/analytics/unique_listeners.php | 4 +- .../cp_admin/podcast/analytics/webpages.php | 8 +- themes/cp_admin/podcast/create.php | 139 ++++++++-------- themes/cp_admin/podcast/delete.php | 8 +- themes/cp_admin/podcast/edit.php | 149 +++++++++--------- themes/cp_admin/podcast/latest_episodes.php | 4 +- themes/cp_admin/podcast/list.php | 4 +- .../cp_admin/podcast/monetization_other.php | 22 +-- themes/cp_admin/podcast/notifications.php | 2 +- themes/cp_admin/podcast/persons.php | 16 +- themes/cp_admin/podcast/platforms.php | 2 +- themes/cp_admin/podcast/publish.php | 18 +-- themes/cp_admin/podcast/publish_edit.php | 16 +- themes/cp_admin/podcast/view.php | 4 +- themes/cp_admin/settings/about.php | 2 +- themes/cp_admin/settings/general.php | 36 ++--- themes/cp_admin/settings/theme.php | 10 +- themes/cp_admin/subscription/create.php | 8 +- themes/cp_admin/subscription/delete.php | 10 +- themes/cp_admin/subscription/edit.php | 4 +- themes/cp_admin/subscription/list.php | 12 +- themes/cp_admin/subscription/suspend.php | 10 +- themes/cp_admin/user/create.php | 14 +- themes/cp_admin/user/delete.php | 12 +- themes/cp_admin/user/edit.php | 6 +- themes/cp_admin/user/list.php | 12 +- themes/cp_app/_admin_navbar.php | 12 +- themes/cp_app/_message_block.php | 8 +- themes/cp_app/_persons_modal.php | 2 +- themes/cp_app/embed.php | 2 +- themes/cp_app/episode/_layout-preview.php | 18 +-- themes/cp_app/episode/_layout.php | 10 +- themes/cp_app/episode/_partials/card.php | 2 +- .../episode/_partials/comment_actions.php | 2 +- .../_partials/comment_actions_from_post.php | 2 +- .../_partials/comment_reply_actions.php | 2 +- .../_partials/comment_with_replies.php | 6 +- .../cp_app/episode/_partials/navigation.php | 4 +- .../cp_app/episode/_partials/preview_card.php | 2 +- themes/cp_app/episode/activity.php | 6 +- themes/cp_app/episode/comment.php | 2 +- themes/cp_app/episode/comments.php | 6 +- themes/cp_app/episode/preview-transcript.php | 4 +- themes/cp_app/episode/transcript.php | 4 +- themes/cp_app/home.php | 12 +- themes/cp_app/pages/_layout.php | 6 +- themes/cp_app/pages/map.php | 6 +- themes/cp_app/podcast/_layout.php | 4 +- .../podcast/_partials/funding_links_modal.php | 4 +- .../cp_app/podcast/_partials/navigation.php | 4 +- .../podcast/_partials/premium_banner.php | 12 +- themes/cp_app/podcast/_partials/sidebar.php | 4 +- themes/cp_app/podcast/about.php | 2 +- themes/cp_app/podcast/activity.php | 8 +- themes/cp_app/podcast/episodes.php | 2 +- themes/cp_app/podcast/follow.php | 8 +- themes/cp_app/podcast/links.php | 10 +- themes/cp_app/podcast/unlock.php | 10 +- themes/cp_app/post/_partials/actions.php | 2 +- themes/cp_app/post/_partials/card.php | 2 +- .../post/_partials/post_with_replies.php | 6 +- .../cp_app/post/_partials/reply_actions.php | 2 +- themes/cp_app/post/post.php | 2 +- themes/cp_app/post/remote_action.php | 8 +- themes/cp_auth/_layout.php | 8 +- themes/cp_auth/_message_block.php | 8 +- themes/cp_auth/email_2fa_show.php | 6 +- themes/cp_auth/email_2fa_verify.php | 6 +- themes/cp_auth/email_activate_show.php | 6 +- themes/cp_auth/login.php | 12 +- themes/cp_auth/magic_link_form.php | 6 +- themes/cp_auth/magic_link_set_password.php | 6 +- themes/cp_auth/register.php | 18 +-- themes/cp_install/_layout.php | 2 +- themes/cp_install/_message_block.php | 8 +- themes/cp_install/cache_config.php | 8 +- themes/cp_install/create_superadmin.php | 16 +- themes/cp_install/database_config.php | 24 +-- themes/cp_install/instance_config.php | 18 +-- 193 files changed, 1632 insertions(+), 1348 deletions(-) delete mode 100644 app/Libraries/ViewComponents/Helpers/viewcomponents_helper.php create mode 100644 app/Views/Components/Hint.php rename modules/Plugins/Manifest/{Author.php => Person.php} (97%) create mode 100644 modules/Plugins/Manifest/Repository.php diff --git a/app/Helpers/breadcrumb_helper.php b/app/Helpers/breadcrumb_helper.php index c600d2b3..e18b904a 100644 --- a/app/Helpers/breadcrumb_helper.php +++ b/app/Helpers/breadcrumb_helper.php @@ -31,6 +31,6 @@ if (! function_exists('replace_breadcrumb_params')) { function replace_breadcrumb_params(array $newParams): void { $breadcrumb = Services::breadcrumb(); - $breadcrumb->replaceParams(esc($newParams)); + $breadcrumb->replaceParams($newParams); } } diff --git a/app/Helpers/components_helper.php b/app/Helpers/components_helper.php index d3db3ac8..1b68b74e 100644 --- a/app/Helpers/components_helper.php +++ b/app/Helpers/components_helper.php @@ -16,31 +16,6 @@ use CodeIgniter\View\Table; // ------------------------------------------------------------------------ -if (! function_exists('hint_tooltip')) { - /** - * Hint component - * - * Used to produce tooltip with a question mark icon for hint texts - * - * @param string $hintText The hint text - */ - function hint_tooltip(string $hintText = '', string $class = ''): string - { - $tooltip = - '' . icon('question-fill') . ''; - } -} - -// ------------------------------------------------------------------------ - if (! function_exists('data_table')) { /** * Data table component @@ -113,12 +88,12 @@ if (! function_exists('publication_pill')) { */ function publication_pill(?Time $publicationDate, string $publicationStatus, string $customClass = ''): string { - $class = match ($publicationStatus) { - 'published' => 'text-pine-500 border-pine-500 bg-pine-50', - 'scheduled' => 'text-red-600 border-red-600 bg-red-50', - 'with_podcast' => 'text-blue-600 border-blue-600 bg-blue-50', - 'not_published' => 'text-gray-600 border-gray-600 bg-gray-50', - default => 'text-gray-600 border-gray-600 bg-gray-50', + $variant = match ($publicationStatus) { + 'published' => 'success', + 'scheduled' => 'warning', + 'with_podcast' => 'info', + 'not_published' => 'default', + default => 'default', }; $title = match ($publicationStatus) { @@ -130,16 +105,12 @@ if (! function_exists('publication_pill')) { $label = lang('Episode.publication_status.' . $publicationStatus); - return '' . - $label . - ($publicationStatus === 'with_podcast' ? icon('error-warning-fill', [ + // @icon('error-warning-fill') + return '' . $label . ($publicationStatus === 'with_podcast' ? icon('error-warning-fill', [ 'class' => 'flex-shrink-0 ml-1 text-lg', ]) : '') . - ''; + ''; } } @@ -182,7 +153,7 @@ if (! function_exists('publication_button')) { } return <<{$label} + {$label} HTML; } } @@ -356,7 +327,7 @@ if (! function_exists('location_link')) { 'class' => 'mr-2 flex-shrink-0', ]) . '' . esc($location->name) . '', [ - 'class' => 'w-full overflow-hidden inline-flex items-baseline hover:underline focus:ring-accent' . + 'class' => 'w-full overflow-hidden inline-flex items-baseline hover:underline' . ($class === '' ? '' : " {$class}"), 'target' => '_blank', 'rel' => 'noreferrer noopener', diff --git a/app/Helpers/page_helper.php b/app/Helpers/page_helper.php index 253b0135..835162f1 100644 --- a/app/Helpers/page_helper.php +++ b/app/Helpers/page_helper.php @@ -20,30 +20,30 @@ if (! function_exists('render_page_links')) { { $pages = (new PageModel())->findAll(); $links = anchor(route_to('home'), lang('Common.home'), [ - 'class' => 'px-2 py-1 underline hover:no-underline focus:ring-accent', + 'class' => 'px-2 py-1 underline hover:no-underline', ]); if ($podcastHandle !== null) { $links .= anchor(route_to('podcast-links', $podcastHandle), lang('Podcast.links'), [ - 'class' => 'px-2 py-1 underline hover:no-underline focus:ring-accent', + 'class' => 'px-2 py-1 underline hover:no-underline', ]); } $links .= anchor(route_to('credits'), lang('Person.credits'), [ - 'class' => 'px-2 py-1 underline hover:no-underline focus:ring-accent', + 'class' => 'px-2 py-1 underline hover:no-underline', ]); $links .= anchor(route_to('map'), lang('Page.map.title'), [ - 'class' => 'px-2 py-1 underline hover:no-underline focus:ring-accent', + 'class' => 'px-2 py-1 underline hover:no-underline', ]); foreach ($pages as $page) { $links .= anchor($page->link, esc($page->title), [ - 'class' => 'px-2 py-1 underline hover:no-underline focus:ring-accent', + 'class' => 'px-2 py-1 underline hover:no-underline', ]); } // if set in .env, add legal notice link at the end of page links if (config('App')->legalNoticeURL !== null) { $links .= anchor(config('App')->legalNoticeURL, lang('Common.legal_notice'), [ - 'class' => 'px-2 py-1 underline hover:no-underline focus:ring-accent', + 'class' => 'px-2 py-1 underline hover:no-underline', 'target' => '_blank', 'rel' => 'noopener noreferrer', ]); diff --git a/app/Libraries/Breadcrumb.php b/app/Libraries/Breadcrumb.php index 2c46bd82..529d98d4 100644 --- a/app/Libraries/Breadcrumb.php +++ b/app/Libraries/Breadcrumb.php @@ -32,12 +32,18 @@ class Breadcrumb $uri = ''; foreach (current_url(true)->getSegments() as $segment) { $uri .= '/' . $segment; - $this->links[] = [ + $link = [ 'text' => is_numeric($segment) ? $segment : lang('Breadcrumb.' . $segment), 'href' => base_url($uri), ]; + + if (is_numeric($segment)) { + $this->links[] = $link; + } else { + $this->links[$segment] = $link; + } } } @@ -46,20 +52,19 @@ class Breadcrumb * * Given a breadcrumb with numeric params, this function replaces them with the values provided in $newParams * - * Example with `Home / podcasts / 1 / episodes / 1` + * Example with `Home / podcasts / 1 / episodes / 1 / foo` * - * $newParams = [ 0 => 'foo', 1 => 'bar' ] replaceParams($newParams); + * $newParams = [ 0 => 'bar', 1 => 'baz', 'foo' => 'I Pity The Foo' ] replaceParams($newParams); * - * The breadcrumb is now `Home / podcasts / foo / episodes / bar` + * The breadcrumb is now `Home / podcasts / foo / episodes / bar / I Pity The Foo` * * @param string[] $newParams */ public function replaceParams(array $newParams): void { - foreach ($this->links as $key => $link) { - if (is_numeric($link['text'])) { - $this->links[$key]['text'] = $newParams[0]; - array_shift($newParams); + foreach ($newParams as $key => $newValue) { + if (array_key_exists($key, $this->links)) { + $this->links[$key]['text'] = $newValue; } } } diff --git a/app/Libraries/ViewComponents/Component.php b/app/Libraries/ViewComponents/Component.php index b86e4a85..6d45923c 100644 --- a/app/Libraries/ViewComponents/Component.php +++ b/app/Libraries/ViewComponents/Component.php @@ -4,26 +4,30 @@ declare(strict_types=1); namespace ViewComponents; -class Component implements ComponentInterface +abstract class Component implements ComponentInterface { - protected string $slot = ''; + /** + * @var list + */ + protected array $props = []; - protected string $class = ''; + /** + * @var array + */ + protected array $casts = []; + + protected ?string $slot = null; /** * @var array */ - protected array $attributes = [ - 'class' => '', - ]; + protected array $attributes = []; /** * @param array $attributes */ public function __construct(array $attributes) { - helper('viewcomponents'); - // overwrite default attributes if set $this->attributes = [...$this->attributes, ...$attributes]; @@ -42,9 +46,39 @@ class Component implements ComponentInterface if (is_callable([$this, $method])) { $this->{$method}($value); } else { + if (array_key_exists($name, $this->casts)) { + $value = match ($this->casts[$name]) { + 'boolean' => $value === 'true', + 'number' => (int) $value, + 'array' => json_decode(htmlspecialchars_decode($value), true), + default => $value + }; + } + $this->{$name} = $value; } + + // remove from attributes + if (in_array($name, $this->props, true)) { + unset($this->attributes[$name]); + } } + + unset($this->attributes['slot']); + } + + public function mergeClass(string $class): void + { + if (! array_key_exists('class', $this->attributes)) { + $this->attributes['class'] = $class; + } else { + $this->attributes['class'] .= ' ' . $class; + } + } + + public function getStringifiedAttributes(): string + { + return stringify_attributes($this->attributes); } public function render(): string diff --git a/app/Libraries/ViewComponents/ComponentRenderer.php b/app/Libraries/ViewComponents/ComponentRenderer.php index f9e916c0..e9a68172 100644 --- a/app/Libraries/ViewComponents/ComponentRenderer.php +++ b/app/Libraries/ViewComponents/ComponentRenderer.php @@ -43,38 +43,38 @@ class ComponentRenderer private function renderSelfClosingTags(string $output): string { // Pattern borrowed and adapted from Laravel's ComponentTagCompiler - // Should match any Component tags + // Should match any Component tags $pattern = "/ < - \s* - (?[A-Z][A-Za-z0-9\.]*?) - \s* + \\s* + x[-\\:](?[\\w\\-\\:\\.]*) + \\s* (? (?: - \s+ + \\s+ (?: (?: - \{\{\s*\\\$attributes(?:[^}]+?)?\s*\}\} + \\{\\{\\s*\\\$attributes(?:[^}]+?)?\\s*\\}\\} ) | (?: - [\w\-:.@]+ + [\\w\\-:.@]+ ( = (?: \\\"[^\\\"]*\\\" | - \'[^\']*\' + \\'[^\\']*\\' | - [^\'\\\"=<>]+ + [^\\'\\\"=<>]+ ) )? ) ) )* - \s* + \\s* ) - \/> + \\/> /x"; /* @@ -96,8 +96,9 @@ class ComponentRenderer private function renderPairedTags(string $output): string { - $pattern = '/<\s*(?[A-Z][A-Za-z0-9\.]*?)(?(\s*[\w\-]+\s*=\s*(\'[^\']*\'|\"[^\"]*\"))+\s*)>(?.*)<\/\s*\1\s*>/uUsm'; - ini_set('pcre.backtrack_limit', '-1'); + // ini_set('pcre.backtrack_limit', '-1'); + $pattern = '/<\s*x[-\:](?[\w\-\:\.]*?)(?(\s*[\w\-]+\s*=\s*(\'[^\']*\'|\"[^\"]*\"))+\s*)>(?.*)<\/\s*x-\1\s*>/uiUsm'; + /* $matches[0] = full tags matched and all of its content $matches[name] = pascal cased tag name @@ -167,8 +168,6 @@ class ComponentRenderer ( \"[^\"]+\" | - \'[^\']+\' - | \\\'[^\\\']+\\\' | [^\s>]+ diff --git a/app/Libraries/ViewComponents/Helpers/viewcomponents_helper.php b/app/Libraries/ViewComponents/Helpers/viewcomponents_helper.php deleted file mode 100644 index 9c7caf7d..00000000 --- a/app/Libraries/ViewComponents/Helpers/viewcomponents_helper.php +++ /dev/null @@ -1,33 +0,0 @@ - $val) { - $atts .= ($js) ? $key . '=' . esc($val, 'js') . ',' : ' ' . $key . '="' . $val . '"'; - } - - return rtrim($atts, ','); - } -} diff --git a/app/Resources/styles/breadcrumb.css b/app/Resources/styles/breadcrumb.css index 5b73b775..b1ef8a30 100644 --- a/app/Resources/styles/breadcrumb.css +++ b/app/Resources/styles/breadcrumb.css @@ -15,10 +15,6 @@ &:hover { @apply underline; } - - &:focus { - @apply ring-accent; - } } .breadcrumb-item.active { diff --git a/app/Resources/styles/choices.css b/app/Resources/styles/choices.css index 84423cb8..f9f59a34 100644 --- a/app/Resources/styles/choices.css +++ b/app/Resources/styles/choices.css @@ -9,10 +9,6 @@ font-size: 16px; } - .choices:focus { - outline: none; - } - .choices:last-child { margin-bottom: 0; } @@ -327,10 +323,6 @@ cursor: pointer; } - .choices__button:focus { - outline: none; - } - .choices__input { @apply mb-1 align-middle bg-elevated; diff --git a/app/Resources/styles/custom.css b/app/Resources/styles/custom.css index adeb38e7..6dfa93b1 100644 --- a/app/Resources/styles/custom.css +++ b/app/Resources/styles/custom.css @@ -5,6 +5,15 @@ } } + .ring-accent { + @apply outline-none ring-2 ring-offset-2; + + /* FIXME: why doesn't ring-accent-base work? */ + --tw-ring-opacity: 1; + --tw-ring-color: hsl(var(--color-accent-base) / var(--tw-ring-opacity)); + --tw-ring-offset-color: hsl(var(--color-background-base)); + } + .rounded-conditional-b-xl { border-bottom-right-radius: max( 0px, diff --git a/app/Resources/styles/readMore.css b/app/Resources/styles/readMore.css index a084575a..27241de4 100644 --- a/app/Resources/styles/readMore.css +++ b/app/Resources/styles/readMore.css @@ -34,7 +34,7 @@ Read more component (basic unstyled component) /* Don't forget focus and hover styles for accessibility! */ .read-more__checkbox:focus ~ .read-more__label { - @apply ring; + @apply ring-accent; } .read-more__checkbox:hover ~ .read-more__label { diff --git a/app/Views/Components/Alert.php b/app/Views/Components/Alert.php index f328e6c8..b4043637 100644 --- a/app/Views/Components/Alert.php +++ b/app/Views/Components/Alert.php @@ -8,9 +8,15 @@ use ViewComponents\Component; class Alert extends Component { - protected ?string $glyph = null; + protected array $props = ['glyph', 'title']; - protected ?string $title = null; + protected string $glyph = ''; + + protected ?string $title = ''; + + protected array $attributes = [ + 'role' => 'alert', + ]; /** * @var 'default'|'success'|'danger'|'warning' @@ -19,7 +25,7 @@ class Alert extends Component public function render(): string { - $variants = [ + $variantData = match ($this->variant) { 'success' => [ 'class' => 'text-pine-900 bg-pine-100 border-pine-300', 'glyph' => 'check-fill', // @icon('check-fill') @@ -32,30 +38,21 @@ class Alert extends Component 'class' => 'text-yellow-900 bg-yellow-100 border-yellow-300', 'glyph' => 'alert-fill', // @icon('alert-fill') ], - 'default' => [ + default => [ 'class' => 'text-blue-900 bg-blue-100 border-blue-300', 'glyph' => 'error-warning-fill', // @icon('error-warning-fill') ], - ]; + }; - if (! array_key_exists($this->variant, $variants)) { - $this->variant = 'default'; - } - - $glyph = icon(($this->glyph ?? $variants[$this->variant]['glyph']), [ + $glyph = icon(($this->glyph === '' ? $variantData['glyph'] : $this->glyph), [ 'class' => 'flex-shrink-0 mr-2 text-lg', ]); - $title = $this->title === null ? '' : '
' . $this->title . '
'; - $class = 'inline-flex w-full p-2 text-sm border rounded ' . $variants[$this->variant]['class'] . ' ' . $this->class; - - unset($this->attributes['slot']); - unset($this->attributes['variant']); - unset($this->attributes['class']); - unset($this->attributes['glyph']); - $attributes = stringify_attributes($this->attributes); + $title = $this->title === '' ? '' : '
' . $this->title . '
'; + $this->mergeClass('inline-flex w-full p-2 text-sm border rounded '); + $this->mergeClass($variantData['class']); return <<{$glyph}
{$title}

{$this->slot}

+
getStringifiedAttributes()}>{$glyph}
{$title}

{$this->slot}

HTML; } } diff --git a/app/Views/Components/Button.php b/app/Views/Components/Button.php index 28674dfb..b63c9750 100644 --- a/app/Views/Components/Button.php +++ b/app/Views/Components/Button.php @@ -8,10 +8,20 @@ use ViewComponents\Component; class Button extends Component { + protected array $props = ['uri', 'variant', 'size', 'iconLeft', 'iconRight', 'isSquared', 'isExternal']; + + protected array $casts = [ + 'isSquared' => 'boolean', + 'isExternal' => 'boolean', + ]; + protected string $uri = ''; protected string $variant = 'default'; + /** + * @var 'small'|'base'|'large' + */ protected string $size = 'base'; protected string $iconLeft = ''; @@ -20,65 +30,54 @@ class Button extends Component protected bool $isSquared = false; - public function setIsSquared(string $value): void - { - $this->isSquared = $value === 'true'; - } + protected bool $isExternal = false; public function render(): string { - $baseClass = - 'gap-x-2 flex-shrink-0 inline-flex items-center justify-center font-semibold rounded-full'; + $this->mergeClass('gap-x-2 flex-shrink-0 inline-flex items-center justify-center font-semibold rounded-full'); - $variantClass = [ - 'default' => 'shadow-sm text-black bg-gray-300 hover:bg-gray-400', + $variantClass = match ($this->variant) { 'primary' => 'shadow-sm text-accent-contrast bg-accent-base hover:bg-accent-hover', 'secondary' => 'shadow-sm ring-2 ring-accent ring-inset text-accent-base bg-white hover:border-accent-hover hover:text-accent-hover', - 'success' => 'shadow-sm text-white bg-pine-500 hover:bg-pine-800', - 'danger' => 'shadow-sm text-white bg-red-600 hover:bg-red-700', - 'warning' => 'shadow-sm text-black bg-yellow-500 hover:bg-yellow-600', - 'info' => 'shadow-sm text-white bg-blue-500 hover:bg-blue-600', + 'success' => 'shadow-sm ring-2 ring-pine-700 ring-inset text-pine-700 hover:ring-pine-800 hover:text-pine-800', + 'danger' => 'shadow-sm ring-2 ring-red-700 ring-inset text-red-700 hover:ring-red-800 hover:text-red-800', + 'warning' => 'shadow-sm ring-2 ring-yellow-700 ring-inset text-yellow-700 hover:ring-yellow-800 hover:text-yellow-800', + 'info' => 'shadow-sm ring-2 ring-blue-700 ring-inset text-blue-700 hover:ring-blue-800 hover:text-blue-800', 'disabled' => 'shadow-sm text-black bg-gray-300 cursor-not-allowed', - ]; + default => 'shadow-sm text-black bg-gray-100 hover:bg-gray-300', + }; - $sizeClass = [ + $sizeClass = match ($this->size) { 'small' => 'text-xs leading-6', - 'base' => 'text-sm leading-5', 'large' => 'text-base leading-6', - ]; + default => 'text-sm leading-5', + }; - $iconSize = [ + $iconSizeClass = match ($this->size) { 'small' => 'text-sm', - 'base' => 'text-lg', 'large' => 'text-2xl', - ]; + default => 'text-lg', + }; - $basePaddings = [ + $basePaddings = match ($this->size) { 'small' => 'px-3 py-1', - 'base' => 'px-3 py-2', 'large' => 'px-4 py-2', - ]; + default => 'px-3 py-2', + }; - $squaredPaddings = [ + $squaredPaddings = match ($this->size) { 'small' => 'p-1', - 'base' => 'p-2', 'large' => 'p-3', - ]; + default => 'p-2', + }; - $buttonClass = - $baseClass . - ' ' . - ($this->isSquared - ? $squaredPaddings[$this->size] - : $basePaddings[$this->size]) . - ' ' . - $sizeClass[$this->size] . - ' ' . - $variantClass[$this->variant]; + $this->mergeClass($variantClass); + $this->mergeClass($sizeClass); - if (array_key_exists('class', $this->attributes)) { - $buttonClass .= ' ' . $this->attributes['class']; - unset($this->attributes['class']); + if ($this->isSquared) { + $this->mergeClass($squaredPaddings); + } else { + $this->mergeClass($basePaddings); } if ($this->iconLeft !== '' || $this->iconRight !== '') { @@ -87,41 +86,30 @@ class Button extends Component if ($this->iconLeft !== '') { $this->slot = icon($this->iconLeft, [ - 'class' => 'opacity-75 ' . $iconSize[$this->size], + 'class' => 'opacity-75 ' . $iconSizeClass, ]) . $this->slot; } if ($this->iconRight !== '') { $this->slot .= icon($this->iconRight, [ - 'class' => 'opacity-75 ' . $iconSize[$this->size], + 'class' => 'opacity-75 ' . $iconSizeClass, ]); } - unset($this->attributes['slot']); - unset($this->attributes['variant']); - unset($this->attributes['size']); - unset($this->attributes['iconLeft']); - unset($this->attributes['iconRight']); - unset($this->attributes['isSquared']); - unset($this->attributes['uri']); - unset($this->attributes['label']); - if ($this->uri !== '') { $tagName = 'a'; - $defaultButtonAttributes = [ - 'href' => $this->uri, - ]; + $this->attributes['href'] = $this->uri; + if ($this->isExternal) { + $this->attributes['target'] = '_blank'; + $this->attributes['rel'] = 'noopener noreferrer'; + } } else { $tagName = 'button'; - $defaultButtonAttributes = [ - 'type' => 'button', - ]; + $this->attributes['type'] ??= 'button'; } - $attributes = stringify_attributes(array_merge($defaultButtonAttributes, $this->attributes)); - return <<{$this->slot} + <{$tagName} {$this->getStringifiedAttributes()}>{$this->slot} HTML; } } diff --git a/app/Views/Components/Charts/ChartsComponent.php b/app/Views/Components/Charts/ChartsComponent.php index a4c4a575..c912734c 100644 --- a/app/Views/Components/Charts/ChartsComponent.php +++ b/app/Views/Components/Charts/ChartsComponent.php @@ -8,13 +8,13 @@ use ViewComponents\Component; class ChartsComponent extends Component { - protected string $title = ''; + protected string $title; protected string $subtitle = ''; - protected string $dataUrl = ''; + protected string $dataUrl; - protected string $type = ''; + protected string $type; public function render(): string { @@ -23,8 +23,10 @@ class ChartsComponent extends Component $subtitleBlock = '

' . $this->subtitle . '

'; } + $this->mergeClass('bg-elevated border-3 rounded-xl border-subtle'); + return << +
getStringifiedAttributes()}>

{$this->title}

{$subtitleBlock}
diff --git a/app/Views/Components/DashboardCard.php b/app/Views/Components/DashboardCard.php index 6510fe4b..e951f251 100644 --- a/app/Views/Components/DashboardCard.php +++ b/app/Views/Components/DashboardCard.php @@ -8,7 +8,9 @@ use ViewComponents\Component; class DashboardCard extends Component { - protected ?string $href = null; + protected array $props = ['href', 'glyph', 'title', 'subtitle']; + + protected string $href = ''; protected string $glyph; @@ -27,11 +29,11 @@ class DashboardCard extends Component 'class' => 'flex-shrink-0 bg-base rounded-full w-8 h-8 p-2 text-accent-base', ]); - if ($this->href !== null && $this->href !== '') { + if ($this->href !== '') { $chevronRight = icon('arrow-right-s-fill'); $viewLang = lang('Common.view'); return << +
{$glyph}
{$this->title}
{$viewLang}{$chevronRight}

{$this->subtitle}

{$this->slot}
diff --git a/app/Views/Components/DropdownMenu.php b/app/Views/Components/DropdownMenu.php index 2a50ea71..bed6e299 100644 --- a/app/Views/Components/DropdownMenu.php +++ b/app/Views/Components/DropdownMenu.php @@ -9,17 +9,25 @@ use ViewComponents\Component; class DropdownMenu extends Component { - public string $id = ''; + protected array $props = ['id', 'labelledby', 'placement', 'offsetX', 'offsetY', 'items']; - public string $labelledby; + protected array $casts = [ + 'offsetX' => 'number', + 'offsetY' => 'number', + 'items' => 'array', + ]; - public string $placement = 'bottom-end'; + protected string $id; - public string $offsetX = '0'; + protected string $labelledby; - public string $offsetY = '0'; + protected string $placement = 'bottom-end'; - public array $items = []; + protected int $offsetX = 0; + + protected int $offsetY = 0; + + protected array $items = []; public function setItems(string $value): void { @@ -37,7 +45,7 @@ class DropdownMenu extends Component switch ($item['type']) { case 'link': $menuItems .= anchor($item['uri'], $item['title'], [ - 'class' => 'inline-flex gap-x-1 items-center px-4 py-1 hover:bg-highlight focus:ring-accent focus:ring-inset' . (array_key_exists('class', $item) ? ' ' . $item['class'] : ''), + 'class' => 'inline-flex gap-x-1 items-center px-4 py-1 hover:bg-highlight' . (array_key_exists('class', $item) ? ' ' . $item['class'] : ''), ]); break; case 'html': @@ -51,14 +59,16 @@ class DropdownMenu extends Component } } + $this->mergeClass('absolute flex flex-col py-2 rounded-lg z-60 whitespace-nowrap text-skin-base border-contrast bg-elevated border-3'); + $this->attributes['id'] = $this->id; + $this->attributes['aria-labelledby'] = $this->labelledby; + $this->attributes['data-dropdown'] = 'menu'; + $this->attributes['data-dropdown-placement'] = $this->placement; + $this->attributes['data-dropdown-offset-x'] = $this->offsetX; + $this->attributes['data-dropdown-offset-y'] = $this->offsetY; + return <<{$menuItems} + HTML; } } diff --git a/app/Views/Components/Forms/Checkbox.php b/app/Views/Components/Forms/Checkbox.php index 1d81acb8..9ffafc8b 100644 --- a/app/Views/Components/Forms/Checkbox.php +++ b/app/Views/Components/Forms/Checkbox.php @@ -4,35 +4,41 @@ declare(strict_types=1); namespace App\Views\Components\Forms; +use App\Views\Components\Hint; + class Checkbox extends FormComponent { - protected ?string $hint = null; + protected array $props = ['hint', 'isChecked']; + + protected array $casts = [ + 'isChecked' => 'boolean', + ]; + + protected string $hint = ''; protected bool $isChecked = false; - public function setIsChecked(string $value): void - { - $this->isChecked = $value === 'true'; - } - public function render(): string { - $attributes = [ - 'id' => $this->value, - 'name' => $this->name, - 'class' => 'form-checkbox bg-elevated text-accent-base border-contrast border-3 focus:ring-accent w-6 h-6', - ]; - $checkboxInput = form_checkbox( - $attributes, + [ + 'id' => $this->value, + 'name' => $this->name, + 'class' => 'form-checkbox bg-elevated text-accent-base border-contrast border-3 w-6 h-6', + ], 'yes', old($this->name) ? old($this->name) === $this->value : $this->isChecked, ); - $hint = $this->hint === null ? '' : hint_tooltip($this->hint, 'ml-1'); + $hint = $this->hint === '' ? '' : (new Hint([ + 'class' => 'ml-1', + 'slot' => $this->hint, + ]))->render(); + + $this->mergeClass('inline-flex items-center'); return <<{$checkboxInput}{$this->slot}{$hint} + HTML; } } diff --git a/app/Views/Components/Forms/ColorRadioButton.php b/app/Views/Components/Forms/ColorRadioButton.php index ab2fb16f..b562d71e 100644 --- a/app/Views/Components/Forms/ColorRadioButton.php +++ b/app/Views/Components/Forms/ColorRadioButton.php @@ -6,15 +6,14 @@ namespace App\Views\Components\Forms; class ColorRadioButton extends FormComponent { + protected array $props = ['isChecked']; + + protected array $casts = [ + 'isChecked' => 'boolean', + ]; + protected bool $isChecked = false; - protected string $style = ''; - - public function setIsChecked(string $value): void - { - $this->isChecked = $value === 'true'; - } - public function render(): string { $data = [ @@ -23,7 +22,7 @@ class ColorRadioButton extends FormComponent 'class' => 'color-radio-btn', ]; - if ($this->required) { + if ($this->isRequired) { $data['required'] = 'required'; } @@ -34,7 +33,7 @@ class ColorRadioButton extends FormComponent ); return << +
getStringifiedAttributes()}> {$radioInput}
diff --git a/app/Views/Components/Forms/DatetimePicker.php b/app/Views/Components/Forms/DatetimePicker.php index f2d33332..54bc4fa4 100644 --- a/app/Views/Components/Forms/DatetimePicker.php +++ b/app/Views/Components/Forms/DatetimePicker.php @@ -6,21 +6,28 @@ namespace App\Views\Components\Forms; class DatetimePicker extends FormComponent { + protected array $attributes = [ + 'data-picker' => 'datetime', + ]; + public function render(): string { - $this->attributes['class'] = 'rounded-l-lg border-0 border-rounded-r-none flex-1 focus:ring-0'; - $this->attributes['data-input'] = ''; - $dateInput = form_input($this->attributes, old($this->name, $this->value)); + $dateInput = form_input([ + 'class' => 'rounded-l-lg border-0 border-rounded-r-none flex-1 focus:ring-0', + 'data-input' => '', + ], old($this->name, $this->value)); $clearLabel = lang( 'Episode.publish_form.scheduled_publication_date_clear', ); $closeIcon = icon('close-fill'); + $this->mergeClass('flex border-3 rounded-lg border-contrast focus-within:ring-accent'); + return << +
getStringifiedAttributes()}> {$dateInput} -
diff --git a/app/Views/Components/Forms/Field.php b/app/Views/Components/Forms/Field.php index 40d219cb..784b3a91 100644 --- a/app/Views/Components/Forms/Field.php +++ b/app/Views/Components/Forms/Field.php @@ -4,49 +4,76 @@ declare(strict_types=1); namespace App\Views\Components\Forms; -class Field extends FormComponent +use ViewComponents\Component; + +class Field extends Component { + protected array $props = [ + 'name', + 'label', + 'isRequired', + 'isReadonly', + 'as', + 'helper', + 'hint', + ]; + + protected array $casts = [ + 'isRequired' => 'boolean', + 'isReadonly' => 'boolean', + ]; + + protected string $name; + + protected string $label; + + protected bool $isRequired = false; + + protected bool $isReadonly = false; + protected string $as = 'Input'; - protected string $label = ''; + protected string $helper = ''; - protected ?string $helper = null; - - protected ?string $hint = null; + protected string $hint = ''; public function render(): string { $helperText = ''; - if ($this->helper !== null) { - $helperId = $this->id . 'Help'; - $helperText = '' . $this->helper . ''; + if ($this->helper !== '') { + $helperId = $this->name . 'Help'; + $helperText = (new Helper([ + 'id' => $helperId, + 'slot' => $this->helper, + ]))->render(); $this->attributes['aria-describedby'] = $helperId; } $labelAttributes = [ - 'for' => $this->id, - 'isOptional' => $this->required ? 'false' : 'true', + 'for' => $this->name, + 'isOptional' => $this->isRequired ? 'false' : 'true', 'class' => '-mb-1', + 'slot' => $this->label, ]; - if ($this->hint) { + if ($this->hint !== '') { $labelAttributes['hint'] = $this->hint; } - $labelAttributes = stringify_attributes($labelAttributes); + $label = new Label($labelAttributes); - // remove field specific attributes to inject the rest to Form Component - $fieldComponentAttributes = $this->attributes; - unset($fieldComponentAttributes['as']); - unset($fieldComponentAttributes['label']); - unset($fieldComponentAttributes['class']); - unset($fieldComponentAttributes['helper']); - unset($fieldComponentAttributes['hint']); + $this->mergeClass('flex flex-col'); + $fieldClass = $this->attributes['class']; + unset($this->attributes['class']); + + $this->attributes['name'] = $this->name; + $this->attributes['isRequired'] = $this->isRequired ? 'true' : 'false'; + $this->attributes['isReadonly'] = $this->isReadonly ? 'true' : 'false'; $element = __NAMESPACE__ . '\\' . $this->as; - $fieldElement = new $element($fieldComponentAttributes); + $fieldElement = new $element($this->attributes); return << - {$this->label} +
+ {$label->render()} {$helperText}
{$fieldElement->render()} diff --git a/app/Views/Components/Forms/FormComponent.php b/app/Views/Components/Forms/FormComponent.php index f854fb64..2217b2af 100644 --- a/app/Views/Components/Forms/FormComponent.php +++ b/app/Views/Components/Forms/FormComponent.php @@ -6,28 +6,55 @@ namespace App\Views\Components\Forms; use ViewComponents\Component; -class FormComponent extends Component +abstract class FormComponent extends Component { - protected ?string $id = null; + protected array $props = [ + 'id', + 'name', + 'value', + 'isRequired', + 'isReadonly', + ]; - protected string $name = ''; + protected array $casts = [ + 'isRequired' => 'boolean', + 'isReadonly' => 'boolean', + ]; + + protected string $id; + + protected string $name; protected string $value = ''; - protected bool $required = false; + protected bool $isRequired = false; - protected bool $readonly = false; + protected bool $isReadonly = false; /** * @param array $attributes */ public function __construct(array $attributes) { + $parentVars = get_class_vars(self::class); + $this->casts = [...$parentVars['casts'], ...$this->casts]; + $this->props = [...$parentVars['props'], $this->props]; + parent::__construct($attributes); - if ($this->id === null) { + if (! isset($this->id)) { $this->id = $this->name; - $this->attributes['id'] = $this->id; + } + + $this->attributes['id'] = $this->id; + $this->attributes['name'] = $this->name; + + if ($this->isRequired) { + $this->attributes['required'] = 'required'; + } + + if ($this->isReadonly) { + $this->attributes['readonly'] = 'readonly'; } } @@ -35,23 +62,4 @@ class FormComponent extends Component { $this->value = htmlspecialchars_decode($value, ENT_QUOTES); } - - public function setRequired(string $value): void - { - $this->required = $value === 'true'; - unset($this->attributes['required']); - if ($this->required) { - $this->attributes['required'] = 'required'; - } - } - - public function setReadonly(string $value): void - { - $this->readonly = $value === 'true'; - if ($this->readonly) { - $this->attributes['readonly'] = 'readonly'; - } else { - unset($this->attributes['readonly']); - } - } } diff --git a/app/Views/Components/Forms/Helper.php b/app/Views/Components/Forms/Helper.php index f1702573..331c14a9 100644 --- a/app/Views/Components/Forms/Helper.php +++ b/app/Views/Components/Forms/Helper.php @@ -4,19 +4,18 @@ declare(strict_types=1); namespace App\Views\Components\Forms; -class Helper extends FormComponent +use ViewComponents\Component; + +class Helper extends Component { - /** - * @var 'default'|'error' - */ - protected string $type = 'default'; + // TODO: add type with error and show errors inline public function render(): string { - $class = 'text-skin-muted'; + $this->mergeClass('text-skin-muted'); return <<{$this->slot} + getStringifiedAttributes()}>{$this->slot} HTML; } } diff --git a/app/Views/Components/Forms/Input.php b/app/Views/Components/Forms/Input.php index 6ba0ffdb..5ca7dfb9 100644 --- a/app/Views/Components/Forms/Input.php +++ b/app/Views/Components/Forms/Input.php @@ -6,26 +6,29 @@ namespace App\Views\Components\Forms; class Input extends FormComponent { + protected array $props = ['type']; + protected string $type = 'text'; public function render(): string { - $baseClass = 'w-full border-contrast rounded-lg focus:border-contrast border-3 focus:ring-accent focus-within:ring-accent ' . $this->class; - - $this->attributes['class'] = $baseClass; + $this->mergeClass('w-full border-contrast rounded-lg focus:border-contrast border-3 focus-within:ring-accent'); if ($this->type === 'file') { - $this->attributes['class'] .= ' file:px-3 file:py-2 file:h-[40px] file:font-semibold file:text-skin-muted file:text-sm file:rounded-none file:border-none file:bg-highlight file:cursor-pointer'; + $this->mergeClass('file:px-3 file:py-2 file:h-[40px] file:font-semibold file:text-skin-muted file:text-sm file:rounded-none file:border-none file:bg-highlight file:cursor-pointer'); } else { - $this->attributes['class'] .= ' px-3 py-2'; + $this->mergeClass('px-3 py-2'); } - if ($this->readonly) { - $this->attributes['class'] .= ' bg-base'; + if ($this->isReadonly) { + $this->mergeClass('bg-base'); } else { - $this->attributes['class'] .= ' bg-elevated'; + $this->mergeClass('bg-elevated'); } + $this->attributes['type'] = $this->type; + $this->attributes['value'] = $this->value; + return form_input($this->attributes, old($this->name, $this->value)); } } diff --git a/app/Views/Components/Forms/Label.php b/app/Views/Components/Forms/Label.php index 1c7ef093..808458ff 100644 --- a/app/Views/Components/Forms/Label.php +++ b/app/Views/Components/Forms/Label.php @@ -4,39 +4,38 @@ declare(strict_types=1); namespace App\Views\Components\Forms; +use App\Views\Components\Hint; use ViewComponents\Component; class Label extends Component { - protected ?string $for = null; + protected array $props = ['for', 'hint', 'isOptional']; - protected ?string $hint = null; + protected array $casts = [ + 'isOptional' => 'boolean', + ]; + + protected string $for; + + protected string $hint = ''; protected bool $isOptional = false; - public function setIsOptional(string $value): void - { - $this->isOptional = $value === 'true'; - } - public function render(): string { - $labelClass = 'text-sm font-semibold ' . $this->attributes['class']; - unset($this->attributes['class']); + $this->mergeClass('text-sm font-semibold'); $optionalText = $this->isOptional ? '(' . lang('Common.optional') . ')' : ''; - $hint = $this->hint === null ? '' : hint_tooltip($this->hint, 'ml-1'); - unset($this->attributes['isOptional']); - unset($this->attributes['hint']); - unset($this->attributes['slot']); - - $attributes = stringify_attributes($this->attributes); + $hint = $this->hint === '' ? '' : (new Hint([ + 'class' => 'ml-1', + 'slot' => $this->hint, + ]))->render(); return <<{$this->slot}{$optionalText}{$hint} + HTML; } } diff --git a/app/Views/Components/Forms/MarkdownEditor.php b/app/Views/Components/Forms/MarkdownEditor.php index 5c6bda64..b4c253ba 100644 --- a/app/Views/Components/Forms/MarkdownEditor.php +++ b/app/Views/Components/Forms/MarkdownEditor.php @@ -6,6 +6,8 @@ namespace App\Views\Components\Forms; class MarkdownEditor extends FormComponent { + protected array $props = ['disallowList']; + /** * @var string[] */ @@ -18,18 +20,20 @@ class MarkdownEditor extends FormComponent public function render(): string { - $editorClass = 'w-full flex flex-col bg-elevated border-3 border-contrast rounded-lg overflow-hidden focus-within:ring-accent ' . $this->class; + $this->mergeClass('w-full flex flex-col bg-elevated border-3 border-contrast rounded-lg overflow-hidden focus-within:ring-accent'); + $wrapperClass = $this->attributes['class']; $this->attributes['class'] = 'bg-elevated border-none focus:border-none focus:outline-none focus:ring-0 w-full h-full'; $this->attributes['rows'] = 6; - $textarea = form_textarea($this->attributes, old($this->name, $this->value)); - $markdownIcon = icon( - 'markdown-fill', - [ - 'class' => 'mr-1 text-lg opacity-40', - ] + $textarea = form_textarea( + $this->attributes, + old($this->name, $this->value) ); + $markdownIcon = icon('markdown-fill', [ + 'class' => 'mr-1 text-lg opacity-40', + ]); + $translations = [ 'write' => lang('Common.forms.editor.write'), 'preview' => lang('Common.forms.editor.preview'), @@ -85,19 +89,19 @@ class MarkdownEditor extends FormComponent $toolbarContent .= '
'; foreach ($buttonsGroup as $button) { if (! in_array($button['name'], $this->disallowList, true)) { - $toolbarContent .= '<' . $button['tag'] . ' class="opacity-50 hover:opacity-100 focus:ring-accent focus:opacity-100">' . $button['icon'] . ''; + $toolbarContent .= '<' . $button['tag'] . ' class="opacity-50 hover:opacity-100 focus:opacity-100">' . $button['icon'] . ''; } } $toolbarContent .= '
'; } return << +
- - + + {$toolbarContent}
diff --git a/app/Views/Components/Forms/MultiSelect.php b/app/Views/Components/Forms/MultiSelect.php index 81450a32..327c0bff 100644 --- a/app/Views/Components/Forms/MultiSelect.php +++ b/app/Views/Components/Forms/MultiSelect.php @@ -6,6 +6,13 @@ namespace App\Views\Components\Forms; class MultiSelect extends FormComponent { + protected array $props = ['options', 'selected']; + + protected array $casts = [ + 'options' => 'array', + 'selected' => 'array', + ]; + /** * @var array */ @@ -16,18 +23,10 @@ class MultiSelect extends FormComponent */ protected array $selected = []; - public function setOptions(string $value): void - { - $this->options = json_decode(htmlspecialchars_decode($value), true); - } - - public function setSelected(string $selected): void - { - $this->selected = json_decode(htmlspecialchars_decode($selected), true); - } - public function render(): string { + $this->mergeClass('w-full bg-elevated border-3 border-contrast rounded-lg'); + $defaultAttributes = [ 'data-class' => $this->attributes['class'], 'multiple' => 'multiple', @@ -37,8 +36,7 @@ class MultiSelect extends FormComponent 'data-no-choices-text' => lang('Common.forms.multiSelect.noChoicesText'), 'data-max-item-text' => lang('Common.forms.multiSelect.maxItemText'), ]; - $this->attributes['class'] .= ' w-full bg-elevated border-3 border-contrast rounded-lg'; - $extra = array_merge($defaultAttributes, $this->attributes); + $extra = [...$defaultAttributes, ...$this->attributes]; return form_dropdown($this->name, $this->options, $this->selected, $extra); } diff --git a/app/Views/Components/Forms/Radio.php b/app/Views/Components/Forms/Radio.php index 72bd6273..fe0a6789 100644 --- a/app/Views/Components/Forms/Radio.php +++ b/app/Views/Components/Forms/Radio.php @@ -6,12 +6,13 @@ namespace App\Views\Components\Forms; class Radio extends FormComponent { - protected bool $isChecked = false; + protected array $props = ['isChecked']; - public function setIsChecked(string $value): void - { - $this->isChecked = $value === 'true'; - } + protected array $casts = [ + 'isChecked' => 'boolean', + ]; + + protected bool $isChecked = false; public function render(): string { @@ -19,14 +20,16 @@ class Radio extends FormComponent [ 'id' => $this->value, 'name' => $this->name, - 'class' => 'text-accent-base bg-elevated border-contrast border-3 focus:ring-accent w-6 h-6', + 'class' => 'text-accent-base bg-elevated border-contrast border-3 w-6 h-6', ], $this->value, old($this->name) ? old($this->name) === $this->value : $this->isChecked, ); + $this->mergeClass('inline-flex items-center'); + return <<{$radioInput}{$this->slot} + HTML; } } diff --git a/app/Views/Components/Forms/RadioButton.php b/app/Views/Components/Forms/RadioButton.php index 550cb823..c36931db 100644 --- a/app/Views/Components/Forms/RadioButton.php +++ b/app/Views/Components/Forms/RadioButton.php @@ -4,16 +4,19 @@ declare(strict_types=1); namespace App\Views\Components\Forms; +use App\Views\Components\Hint; + class RadioButton extends FormComponent { + protected array $props = ['isChecked', 'hint']; + + protected array $casts = [ + 'isChecked' => 'boolean', + ]; + protected bool $isChecked = false; - protected ?string $hint = null; - - public function setIsChecked(string $value): void - { - $this->isChecked = $value === 'true'; - } + protected string $hint = ''; public function render(): string { @@ -23,7 +26,7 @@ class RadioButton extends FormComponent 'class' => 'form-radio-btn bg-elevated', ]; - if ($this->required) { + if ($this->isRequired) { $data['required'] = 'required'; } @@ -33,10 +36,13 @@ class RadioButton extends FormComponent old($this->name) ? old($this->name) === $this->value : $this->isChecked, ); - $hint = $this->hint ? hint_tooltip($this->hint, 'ml-1 text-base') : ''; + $hint = $this->hint === '' ? '' : (new Hint([ + 'class' => 'ml-1 text-base', + 'slot' => $this->hint, + ]))->render(); return << +
getStringifiedAttributes()}"> {$radioInput}
diff --git a/app/Views/Components/Forms/Section.php b/app/Views/Components/Forms/Section.php index f28237d1..0b8bd2ab 100644 --- a/app/Views/Components/Forms/Section.php +++ b/app/Views/Components/Forms/Section.php @@ -8,17 +8,21 @@ use ViewComponents\Component; class Section extends Component { - protected string $title = ''; + protected array $props = ['title', 'subtitle']; - protected ?string $subtitle = null; + protected string $title; + + protected string $subtitle = ''; public function render(): string { - $subtitle = $this->subtitle === null ? '' : '

' . $this->subtitle . '

'; + $subtitle = $this->subtitle === '' ? '' : '

' . $this->subtitle . '

'; + + $this->mergeClass('w-full p-8 bg-elevated border-3 flex flex-col items-start border-subtle rounded-xl'); return << - {$this->title} +
getStringifiedAttributes()}> + {$this->title} {$subtitle}
{$this->slot}
diff --git a/app/Views/Components/Forms/Select.php b/app/Views/Components/Forms/Select.php index 8f112c90..e1af514e 100644 --- a/app/Views/Components/Forms/Select.php +++ b/app/Views/Components/Forms/Select.php @@ -6,6 +6,12 @@ namespace App\Views\Components\Forms; class Select extends FormComponent { + protected array $props = ['options', 'selected']; + + protected array $casts = [ + 'options' => 'array', + ]; + /** * @var array */ @@ -13,26 +19,17 @@ class Select extends FormComponent protected string $selected = ''; - public function setOptions(string $value): void - { - $this->options = json_decode(htmlspecialchars_decode($value), true); - } - public function render(): string { + $this->mergeClass('w-full focus:border-contrast border-3 rounded-lg bg-elevated border-contrast'); $defaultAttributes = [ - 'class' => 'w-full focus:border-contrast focus:ring-accent border-3 rounded-lg bg-elevated border-contrast ' . $this->class, - 'data-class' => $this->class, 'data-select-text' => lang('Common.forms.multiSelect.selectText'), 'data-loading-text' => lang('Common.forms.multiSelect.loadingText'), 'data-no-results-text' => lang('Common.forms.multiSelect.noResultsText'), 'data-no-choices-text' => lang('Common.forms.multiSelect.noChoicesText'), 'data-max-item-text' => lang('Common.forms.multiSelect.maxItemText'), ]; - unset($this->attributes['name']); - unset($this->attributes['options']); - unset($this->attributes['selected']); - $extra = [...$this->attributes, ...$defaultAttributes]; + $extra = [...$defaultAttributes, ...$this->attributes]; return form_dropdown($this->name, $this->options, old($this->name, $this->selected !== '' ? [$this->selected] : []), $extra); } diff --git a/app/Views/Components/Forms/Textarea.php b/app/Views/Components/Forms/Textarea.php index 705ec0f3..d3976565 100644 --- a/app/Views/Components/Forms/Textarea.php +++ b/app/Views/Components/Forms/Textarea.php @@ -15,9 +15,9 @@ class Textarea extends FormComponent public function render(): string { - unset($this->attributes['value']); + $this->mergeClass('bg-elevated w-full rounded-lg border-3 border-contrast focus:border-contrast focus-within:ring-accent'); - $this->attributes['class'] = 'bg-elevated w-full focus:border-contrast focus:ring-accent rounded-lg border-3 border-contrast ' . $this->class; + $this->attributes['id'] = $this->id; $textarea = form_textarea( $this->attributes, diff --git a/app/Views/Components/Forms/Toggler.php b/app/Views/Components/Forms/Toggler.php index bfb0d659..07e8df7e 100644 --- a/app/Views/Components/Forms/Toggler.php +++ b/app/Views/Components/Forms/Toggler.php @@ -4,45 +4,48 @@ declare(strict_types=1); namespace App\Views\Components\Forms; +use App\Views\Components\Hint; + class Toggler extends FormComponent { + protected array $props = ['size', 'hint', 'isChecked']; + + protected array $casts = [ + 'isChecked' => 'boolean', + ]; + /** * @var 'base'|'small */ protected string $size = 'base'; - protected string $label = ''; - protected string $hint = ''; - protected bool $checked = false; - - public function setChecked(string $value): void - { - $this->checked = $value === 'true'; - } + protected bool $isChecked = false; public function render(): string { - unset($this->attributes['checked']); - - $wrapperClass = $this->class; - unset($this->attributes['class']); - - $sizeClass = [ - 'base' => 'form-switch-slider', + $sizeClass = match ($this->size) { 'small' => 'form-switch-slider form-switch-slider--small', - ]; + default => 'form-switch-slider', + }; - $this->attributes['class'] = 'form-switch'; + $this->mergeClass('relative justify-between inline-flex items-center gap-x-2'); + + $checkbox = form_checkbox([ + 'class' => 'form-switch', + ], 'yes', old($this->name) === 'yes' ? true : $this->isChecked); + + $hint = $this->hint === '' ? '' : (new Hint([ + 'class' => 'ml-1', + 'slot' => $this->hint, + ]))->render(); - $checkbox = form_checkbox($this->attributes, $this->value, old($this->name) === 'yes' ? true : $this->checked); - $hint = $this->hint === '' ? '' : hint_tooltip($this->hint, 'ml-1'); return << + HTML; } diff --git a/app/Views/Components/Forms/XMLEditor.php b/app/Views/Components/Forms/XMLEditor.php index 1c23eddb..1a6a0880 100644 --- a/app/Views/Components/Forms/XMLEditor.php +++ b/app/Views/Components/Forms/XMLEditor.php @@ -6,6 +6,8 @@ namespace App\Views\Components\Forms; class XMLEditor extends FormComponent { + protected array $props = ['content']; + /** * @var array */ diff --git a/app/Views/Components/Heading.php b/app/Views/Components/Heading.php index 001679cf..f5f8ed0c 100644 --- a/app/Views/Components/Heading.php +++ b/app/Views/Components/Heading.php @@ -8,6 +8,8 @@ use ViewComponents\Component; class Heading extends Component { + protected array $props = ['tagName', 'size']; + protected string $tagName = 'div'; /** @@ -17,16 +19,17 @@ class Heading extends Component public function render(): string { - $sizeClasses = [ + $sizeClass = match ($this->size) { 'small' => 'tracking-wide text-base', - 'base' => 'text-xl', 'large' => 'text-3xl', - ]; + default => 'text-xl', + }; - $class = $this->class . ' relative z-10 font-bold text-heading-foreground font-display before:w-full before:absolute before:h-1/2 before:left-0 before:bottom-0 before:rounded-full before:bg-heading-background before:z-[-10] ' . $sizeClasses[$this->size]; + $this->mergeClass('relative z-10 font-bold text-heading-foreground font-display before:w-full before:absolute before:h-1/2 before:left-0 before:bottom-0 before:rounded-full before:bg-heading-background before:z-[-10]'); + $this->mergeClass($sizeClass); return <<tagName} class="{$class}">{$this->slot}tagName}> + <{$this->tagName} {$this->getStringifiedAttributes()}>{$this->slot}tagName}> HTML; } } diff --git a/app/Views/Components/Hint.php b/app/Views/Components/Hint.php new file mode 100644 index 00000000..51eb080a --- /dev/null +++ b/app/Views/Components/Hint.php @@ -0,0 +1,28 @@ + 'bottom', + 'tabindex' => '0', + ]; + + public function render(): string + { + $this->attributes['title'] = $this->slot; + + $this->mergeClass('inline-block align-middle opacity-75'); + + $icon = icon('question-fill'); + + return <<getStringifiedAttributes()}>{$icon} + HTML; + } +} diff --git a/app/Views/Components/IconButton.php b/app/Views/Components/IconButton.php index b2c9f263..5f14cb26 100644 --- a/app/Views/Components/IconButton.php +++ b/app/Views/Components/IconButton.php @@ -6,7 +6,9 @@ namespace App\Views\Components; class IconButton extends Button { - public string $glyph = ''; + public string $glyph; + + protected array $props = ['glyph']; public function __construct(array $attributes) { @@ -16,18 +18,18 @@ class IconButton extends Button 'data-tooltip' => 'bottom', ]; - $glyphSize = [ - 'small' => 'text-sm', - 'base' => 'text-lg', - 'large' => 'text-2xl', - ]; - $allAttributes = [...$attributes, ...$iconButtonAttributes]; parent::__construct($allAttributes); + $glyphSizeClass = match ($this->size) { + 'small' => 'text-sm', + 'large' => 'text-2xl', + default => 'text-lg', + }; + $this->slot = icon($this->glyph, [ - 'class' => $glyphSize[$this->size], + 'class' => $glyphSizeClass, ]); } } diff --git a/app/Views/Components/Pill.php b/app/Views/Components/Pill.php index b3ec02a3..3fd4944c 100644 --- a/app/Views/Components/Pill.php +++ b/app/Views/Components/Pill.php @@ -15,29 +15,44 @@ class Pill extends Component public string $variant = 'default'; - public ?string $icon = null; + public string $icon = ''; - public ?string $iconClass = ''; + public string $iconClass = ''; - protected ?string $hint = null; + protected array $props = ['size', 'variant', 'icon', 'iconClass', 'hint']; + + protected string $hint = ''; public function render(): string { - $variantClasses = [ - 'default' => 'text-gray-800 bg-gray-100 border-gray-300', + $variantClass = match ($this->variant) { 'primary' => 'text-accent-contrast bg-accent-base border-accent-base', 'success' => 'text-pine-900 bg-pine-100 border-pine-300', 'danger' => 'text-red-900 bg-red-100 border-red-300', 'warning' => 'text-yellow-900 bg-yellow-100 border-yellow-300', - ]; + default => 'text-gray-800 bg-gray-100 border-gray-300', + }; - $icon = $this->icon ? icon($this->icon, [ + $sizeClass = match ($this->size) { + 'small' => 'text-xs tracking-wide', + default => 'text-sm', + }; + + $icon = $this->icon !== '' ? icon($this->icon, [ 'class' => $this->iconClass, ]) : ''; - $hint = $this->hint ? 'data-tooltip="bottom" title="' . $this->hint . '"' : ''; + + if ($this->hint !== '') { + $this->attributes['data-tooltip'] = 'bottom'; + $this->attributes['title'] = $this->hint; + } + + $this->mergeClass('inline-flex lowercase items-center gap-x-1 px-1 font-semibold border rounded'); + $this->mergeClass($variantClass); + $this->mergeClass($sizeClass); return <<{$icon}{$this->slot} + getStringifiedAttributes()}>{$icon}{$this->slot} HTML; } } diff --git a/app/Views/Components/ReadMore.php b/app/Views/Components/ReadMore.php index 016a60b3..3739cbcc 100644 --- a/app/Views/Components/ReadMore.php +++ b/app/Views/Components/ReadMore.php @@ -8,17 +8,23 @@ use ViewComponents\Component; class ReadMore extends Component { - public string $id; + protected array $props = ['id']; + + protected string $id; public function render(): string { $readMoreLabel = lang('Common.read_more'); $readLessLabel = lang('Common.read_less'); + + $this->mergeClass('read-more'); + $this->attributes['style'] = '--line-clamp: 3'; + return << +
getStringifiedAttributes()}> -
{$this->slot}
- +
{$this->slot}
+
HTML; } diff --git a/app/Views/Components/SeeMore.php b/app/Views/Components/SeeMore.php index 19f247f2..db30ab94 100644 --- a/app/Views/Components/SeeMore.php +++ b/app/Views/Components/SeeMore.php @@ -12,11 +12,15 @@ class SeeMore extends Component { $seeMoreLabel = lang('Common.see_more'); $seeLessLabel = lang('Common.see_less'); + + $this->mergeClass('see-more'); + $this->attributes['styles'] = '--content-height: 10rem'; + return << +
getStringifiedAttributes()}> -
{$this->slot}
- +
{$this->slot}
+
HTML; } diff --git a/app/Views/_message_block.php b/app/Views/_message_block.php index 1504aa37..4f40c4e5 100644 --- a/app/Views/_message_block.php +++ b/app/Views/_message_block.php @@ -1,18 +1,18 @@ has('message')): ?> - + has('error')): ?> - + has('errors')): ?>
    -
  • +
diff --git a/app/Views/errors/html/error_403.php b/app/Views/errors/html/error_403.php index 0a8d31b4..55b37f89 100644 --- a/app/Views/errors/html/error_403.php +++ b/app/Views/errors/html/error_403.php @@ -23,7 +23,7 @@ You do not have sufficient permissions to access that page.

- + diff --git a/app/Views/errors/html/error_404.php b/app/Views/errors/html/error_404.php index 092217de..79575fb1 100644 --- a/app/Views/errors/html/error_404.php +++ b/app/Views/errors/html/error_404.php @@ -23,7 +23,7 @@

- + diff --git a/app/Views/errors/html/production.php b/app/Views/errors/html/production.php index fb7b11ec..77445e16 100644 --- a/app/Views/errors/html/production.php +++ b/app/Views/errors/html/production.php @@ -28,7 +28,7 @@

getCode() ? ' #' . $exception->getCode() : '') ?>

getMessage())) ?>
at getFile())) ?>:getLine()) ?>

- + 'mr-2', ]) ?>Copy stack trace @@ -41,11 +41,11 @@

Found a bug?

-

You can help get it fixed by creating an issue on the Castopod issue tracker. Please check that the issue does not already exist beforehand.

+

You can help get it fixed by creating an issue on the Castopod issue tracker. Please check that the issue does not already exist beforehand.

Not sure what's happening?

-

You can ask for help in the Castopod community chat!

+

You can ask for help in the Castopod community chat!

diff --git a/modules/Plugins/Controllers/PluginController.php b/modules/Plugins/Controllers/PluginController.php index 908c1801..6dc6ed11 100644 --- a/modules/Plugins/Controllers/PluginController.php +++ b/modules/Plugins/Controllers/PluginController.php @@ -41,6 +41,9 @@ class PluginController extends BaseController $plugins = service('plugins'); $vendorPlugins = $plugins->getVendorPlugins($vendor); + replace_breadcrumb_params([ + $vendor => $vendor, + ]); return view('plugins/installed', [ 'total' => count($vendorPlugins), 'plugins' => $vendorPlugins, @@ -59,6 +62,10 @@ class PluginController extends BaseController throw PageNotFoundException::forPageNotFound(); } + replace_breadcrumb_params([ + $vendor => $vendor, + $package => $package, + ]); return view('plugins/view', [ 'plugin' => $plugin, ]); @@ -76,6 +83,10 @@ class PluginController extends BaseController } helper('form'); + replace_breadcrumb_params([ + $vendor => $vendor, + $package => $package, + ]); return view('plugins/settings_general', [ 'plugin' => $plugin, ]); @@ -122,7 +133,9 @@ class PluginController extends BaseController helper('form'); replace_breadcrumb_params([ - 0 => $podcast->handle, + 0 => $podcast->handle, + $vendor => $vendor, + $package => $package, ]); return view('plugins/settings_podcast', [ 'podcast' => $podcast, @@ -171,8 +184,10 @@ class PluginController extends BaseController helper('form'); replace_breadcrumb_params([ - 0 => $episode->podcast->handle, - 1 => $episode->title, + 0 => $episode->podcast->handle, + 1 => $episode->title, + $vendor => $vendor, + $package => $package, ]); return view('plugins/settings_episode', [ 'podcast' => $episode->podcast, diff --git a/modules/Plugins/Core/BasePlugin.php b/modules/Plugins/Core/BasePlugin.php index 1ccbb3c5..16524e34 100644 --- a/modules/Plugins/Core/BasePlugin.php +++ b/modules/Plugins/Core/BasePlugin.php @@ -17,6 +17,8 @@ use League\CommonMark\MarkdownConverter; use Modules\Plugins\ExternalImageProcessor; use Modules\Plugins\ExternalLinkProcessor; use Modules\Plugins\Manifest\Manifest; +use Modules\Plugins\Manifest\Person; +use Modules\Plugins\Manifest\Repository; use Modules\Plugins\Manifest\Settings; use Modules\Plugins\Manifest\SettingsField; use RuntimeException; @@ -35,7 +37,7 @@ abstract class BasePlugin implements PluginInterface protected Manifest $manifest; - protected string $readmeHTML; + protected ?string $readmeHTML; public function __construct( protected string $vendor, @@ -121,6 +123,27 @@ abstract class BasePlugin implements PluginInterface return $this->manifest->homepage; } + final public function getRepository(): ?Repository + { + return $this->manifest->repository; + } + + /** + * @return list + */ + final public function getKeywords(): array + { + return $this->manifest->keywords; + } + + /** + * @return Person[] + */ + final public function getAuthors(): array + { + return $this->manifest->authors; + } + final public function getIconSrc(): string { return $this->iconSrc; @@ -194,11 +217,16 @@ abstract class BasePlugin implements PluginInterface return $description; } - final public function getReadmeHTML(): string + final public function getReadmeHTML(): ?string { return $this->readmeHTML; } + final public function getLicense(): string + { + return $this->manifest->license ?? 'UNLICENSED'; + } + final protected function getOption(string $option): mixed { return get_plugin_option($this->key, $option); @@ -238,7 +266,7 @@ abstract class BasePlugin implements PluginInterface $environment = new Environment([ 'html_input' => 'escape', 'allow_unsafe_links' => false, - 'host' => 'hello', + 'host' => (new URI(base_url()))->getHost(), ]); $environment->addExtension(new CommonMarkCoreExtension()); @@ -247,11 +275,15 @@ abstract class BasePlugin implements PluginInterface $environment->addEventListener( DocumentParsedEvent::class, - [new ExternalLinkProcessor($environment), 'onDocumentParsed'] + static function (DocumentParsedEvent $event): void { + (new ExternalLinkProcessor())->onDocumentParsed($event); + } ); $environment->addEventListener( DocumentParsedEvent::class, - [new ExternalImageProcessor($environment), 'onDocumentParsed'] + static function (DocumentParsedEvent $event): void { + (new ExternalImageProcessor())->onDocumentParsed($event); + } ); $converter = new MarkdownConverter($environment); diff --git a/modules/Plugins/Core/Plugins.php b/modules/Plugins/Core/Plugins.php index ec3968aa..a4a623ef 100644 --- a/modules/Plugins/Core/Plugins.php +++ b/modules/Plugins/Core/Plugins.php @@ -35,6 +35,8 @@ class Plugins protected static int $installedCount = 0; + protected static int $activeCount = 0; + public function __construct() { helper('plugins'); @@ -63,6 +65,21 @@ class Plugins return array_slice(static::$plugins, (($page - 1) * $perPage), $perPage); } + /** + * @return array + */ + public function getActivePlugins(): array + { + $activePlugins = []; + foreach (static::$plugins as $plugin) { + if ($plugin->isActive()) { + $activePlugins[] = $plugin; + } + } + + return $activePlugins; + } + /** * @return array */ @@ -177,6 +194,11 @@ class Plugins return static::$installedCount; } + public function getActiveCount(): int + { + return static::$activeCount; + } + public function uninstall(BasePlugin $plugin): bool { // remove all settings data @@ -235,6 +257,10 @@ class Plugins static::$plugins[] = $plugin; static::$pluginsByVendor[$vendor][] = $plugin; ++static::$installedCount; + + if ($plugin->isActive()) { + ++static::$activeCount; + } } } diff --git a/modules/Plugins/ExternalImageProcessor.php b/modules/Plugins/ExternalImageProcessor.php index 946afb65..1e53837f 100644 --- a/modules/Plugins/ExternalImageProcessor.php +++ b/modules/Plugins/ExternalImageProcessor.php @@ -5,19 +5,11 @@ declare(strict_types=1); namespace Modules\Plugins; use CodeIgniter\HTTP\URI; -use League\CommonMark\Environment\EnvironmentInterface; use League\CommonMark\Event\DocumentParsedEvent; use League\CommonMark\Extension\CommonMark\Node\Inline\Image; class ExternalImageProcessor { - private EnvironmentInterface $environment; - - public function __construct(EnvironmentInterface $environment) - { - $this->environment = $environment; - } - public function onDocumentParsed(DocumentParsedEvent $event): void { $document = $event->getDocument(); @@ -47,7 +39,6 @@ class ExternalImageProcessor $host = parse_url($url, PHP_URL_HOST); // TODO: load from environment's config - // return $host != $this->environment->getConfiguration()->get('host'); return $host !== (new URI(base_url()))->getHost(); } } diff --git a/modules/Plugins/ExternalLinkProcessor.php b/modules/Plugins/ExternalLinkProcessor.php index 8b7c1ea4..c7f944ce 100644 --- a/modules/Plugins/ExternalLinkProcessor.php +++ b/modules/Plugins/ExternalLinkProcessor.php @@ -5,19 +5,11 @@ declare(strict_types=1); namespace Modules\Plugins; use CodeIgniter\HTTP\URI; -use League\CommonMark\Environment\EnvironmentInterface; use League\CommonMark\Event\DocumentParsedEvent; use League\CommonMark\Extension\CommonMark\Node\Inline\Link; class ExternalLinkProcessor { - private EnvironmentInterface $environment; - - public function __construct(EnvironmentInterface $environment) - { - $this->environment = $environment; - } - public function onDocumentParsed(DocumentParsedEvent $event): void { $document = $event->getDocument(); @@ -48,7 +40,6 @@ class ExternalLinkProcessor $host = parse_url($url, PHP_URL_HOST); // TODO: load from environment's config - // return $host != $this->environment->getConfiguration()->get('host'); return $host !== (new URI(base_url()))->getHost(); } } diff --git a/modules/Plugins/Language/en/Plugins.php b/modules/Plugins/Language/en/Plugins.php index 49a611b4..a41c91db 100644 --- a/modules/Plugins/Language/en/Plugins.php +++ b/modules/Plugins/Language/en/Plugins.php @@ -9,11 +9,19 @@ declare(strict_types=1); */ return [ - 'installed' => 'Installed plugins ({count})', + 'installed' => 'Plugins installed', + 'about' => 'About', 'website' => 'Website', - 'settings' => '{pluginName} settings', + 'repository' => 'Code repository', + 'authors' => 'Authors', + 'author_email' => 'Email {authorName}', + 'author_homepage' => '{authorName} homepage', + 'settings' => 'Settings', + 'view' => 'View', 'activate' => 'Activate', 'deactivate' => 'Deactivate', + 'active' => 'Active', + 'inactive' => 'Inactive', 'uninstall' => 'Uninstall', 'keywords' => [ 'podcasting20' => 'Podcasting 2.0', diff --git a/modules/Plugins/Manifest/Manifest.php b/modules/Plugins/Manifest/Manifest.php index 6e701da5..11275bdd 100644 --- a/modules/Plugins/Manifest/Manifest.php +++ b/modules/Plugins/Manifest/Manifest.php @@ -10,14 +10,15 @@ use CodeIgniter\HTTP\URI; * @property string $name * @property string $version * @property ?string $description - * @property ?Author $author - * @property Author[] $authors + * @property Person[] $authors + * @property Person[] $contributors * @property ?URI $homepage * @property ?string $license * @property bool $private * @property list $keywords * @property list $hooks * @property ?Settings $settings + * @property ?Repository $repository */ class Manifest extends ManifestObject { @@ -25,27 +26,27 @@ class Manifest extends ManifestObject * @var array */ protected const VALIDATION_RULES = [ - 'name' => 'required|max_length[32]', + 'name' => 'required|max_length[128]', 'version' => 'required|regex_match[/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/]', - 'description' => 'permit_empty|max_length[128]', - 'author' => 'permit_empty', + 'description' => 'permit_empty|max_length[256]', 'authors' => 'permit_empty|is_list', 'homepage' => 'permit_empty|valid_url_strict', 'license' => 'permit_empty|string', 'private' => 'permit_empty|is_boolean', 'keywords.*' => 'permit_empty', 'hooks.*' => 'permit_empty|in_list[channelTag,itemTag,siteHead]', - 'settings' => 'permit_empty', + 'settings' => 'permit_empty|is_list', + 'repository' => 'permit_empty|is_list', ]; /** * @var array */ protected const CASTS = [ - 'author' => Author::class, - 'authors' => [Author::class], - 'homepage' => URI::class, - 'settings' => Settings::class, + 'authors' => [Person::class], + 'homepage' => URI::class, + 'settings' => Settings::class, + 'repository' => Repository::class, ]; protected string $name; @@ -54,10 +55,8 @@ class Manifest extends ManifestObject protected ?string $description = null; - protected ?Author $author = null; - /** - * @var Author[] + * @var Person[] */ protected array $authors = []; @@ -78,4 +77,6 @@ class Manifest extends ManifestObject protected array $hooks = []; protected ?Settings $settings = null; + + protected ?Repository $repository = null; } diff --git a/modules/Plugins/Manifest/ManifestObject.php b/modules/Plugins/Manifest/ManifestObject.php index 16cf701c..0569fb3d 100644 --- a/modules/Plugins/Manifest/ManifestObject.php +++ b/modules/Plugins/Manifest/ManifestObject.php @@ -32,7 +32,7 @@ abstract class ManifestObject public function __get(string $name): mixed { - if (isset($this->{$name})) { + if (property_exists($this, $name)) { return $this->{$name}; } diff --git a/modules/Plugins/Manifest/Author.php b/modules/Plugins/Manifest/Person.php similarity index 97% rename from modules/Plugins/Manifest/Author.php rename to modules/Plugins/Manifest/Person.php index 97e8d9ac..1a5b4289 100644 --- a/modules/Plugins/Manifest/Author.php +++ b/modules/Plugins/Manifest/Person.php @@ -12,7 +12,7 @@ use Exception; * @property ?string $email * @property ?URI $url */ -class Author extends ManifestObject +class Person extends ManifestObject { protected const VALIDATION_RULES = [ 'name' => 'required', diff --git a/modules/Plugins/Manifest/Repository.php b/modules/Plugins/Manifest/Repository.php new file mode 100644 index 00000000..266125b6 --- /dev/null +++ b/modules/Plugins/Manifest/Repository.php @@ -0,0 +1,34 @@ + 'required|in_list[git]', + 'url' => 'required|valid_url_strict', + 'directory' => 'permit_empty', + ]; + + /** + * @var array + */ + protected const CASTS = [ + 'url' => URI::class, + ]; + + protected string $type; + + protected URI $url; + + protected ?string $directory = null; +} diff --git a/modules/Plugins/Manifest/SettingsField.php b/modules/Plugins/Manifest/SettingsField.php index d011b579..f303bcdd 100644 --- a/modules/Plugins/Manifest/SettingsField.php +++ b/modules/Plugins/Manifest/SettingsField.php @@ -29,9 +29,9 @@ class SettingsField extends ManifestObject protected string $label; - protected ?string $hint = ''; + protected string $hint = ''; - protected ?string $helper = ''; + protected string $helper = ''; protected bool $optional = false; } diff --git a/modules/Plugins/Manifest/schema.json b/modules/Plugins/Manifest/schema.json index 58528a1f..80283589 100644 --- a/modules/Plugins/Manifest/schema.json +++ b/modules/Plugins/Manifest/schema.json @@ -21,9 +21,6 @@ "description": "This helps people discover your plugin as it's listed in repositories", "type": "string" }, - "author": { - "$ref": "#/$defs/person" - }, "authors": { "type": "array", "items": { diff --git a/tailwind.config.cjs b/tailwind.config.cjs index 2e7248f1..50cea743 100644 --- a/tailwind.config.cjs +++ b/tailwind.config.cjs @@ -139,6 +139,9 @@ module.exports = { textDecoration: "none", }, }, + input: { + margin: 0, + }, }, }, sm: { diff --git a/themes/cp_admin/_layout.php b/themes/cp_admin/_layout.php index 5bc67710..a52a5441 100644 --- a/themes/cp_admin/_layout.php +++ b/themes/cp_admin/_layout.php @@ -36,15 +36,15 @@ $isEpisodeArea = isset($podcast) && isset($episode);
-
+
is_premium) || ($isPodcastArea && $podcast->is_premium)): ?>
- is_premium) ? lang('PremiumPodcasts.episode_is_premium') : lang('PremiumPodcasts.podcast_is_premium') ?> - renderSection('pageTitle') ?> + is_premium) ? lang('PremiumPodcasts.episode_is_premium') : lang('PremiumPodcasts.podcast_is_premium') ?> + renderSection('pageTitle') ?>
- renderSection('pageTitle') ?> + renderSection('pageTitle') ?> renderSection('headerLeft') ?>
diff --git a/themes/cp_admin/_message_block.php b/themes/cp_admin/_message_block.php index 5f1ba623..42a34097 100644 --- a/themes/cp_admin/_message_block.php +++ b/themes/cp_admin/_message_block.php @@ -1,33 +1,33 @@ has('message')): ?> - + has('error')): ?> - + has('errors')): ?> - +
-
+ has('warning')): ?> - + has('warnings')): ?> - +
-
+ diff --git a/themes/cp_admin/_partials/_nav_aside.php b/themes/cp_admin/_partials/_nav_aside.php index 88773cae..0f2867b1 100644 --- a/themes/cp_admin/_partials/_nav_aside.php +++ b/themes/cp_admin/_partials/_nav_aside.php @@ -15,7 +15,7 @@ $isEpisodeArea = isset($podcast) && isset($episode);
- + - - - + - + published_at === null): ?> - + - + diff --git a/themes/cp_admin/episode/embed.php b/themes/cp_admin/episode/embed.php index 58af05b8..47c2d1e5 100644 --- a/themes/cp_admin/episode/embed.php +++ b/themes/cp_admin/episode/embed.php @@ -33,15 +33,15 @@ $embedHeight = config('Embed')->height;
- embed_url}\">") ?>" /> + embed_url}\">") ?>" /> - +
- + - +
endSection() ?> diff --git a/themes/cp_admin/episode/list.php b/themes/cp_admin/episode/list.php index a3d3fde0..3358eeb8 100644 --- a/themes/cp_admin/episode/list.php +++ b/themes/cp_admin/episode/list.php @@ -10,7 +10,7 @@ section('headerRight') ?> - + endSection() ?> @@ -28,16 +28,16 @@

- + ]) ?>
@@ -161,10 +161,10 @@ data_table( HTML), ]; } - return '' . - ''; + ''; }, ], ], diff --git a/themes/cp_admin/episode/persons.php b/themes/cp_admin/episode/persons.php index a66a1520..3f79a0ba 100644 --- a/themes/cp_admin/episode/persons.php +++ b/themes/cp_admin/episode/persons.php @@ -10,7 +10,7 @@ section('headerRight') ?> - + endSection() ?> section('content') ?> @@ -18,12 +18,12 @@
- - - - + - +
@@ -87,7 +87,7 @@ 'header' => lang('Common.actions'), 'cell' => function ($person): string { // @icon('delete-bin-fill') - return ''; + return '' . lang('Person.episode_form.remove') . ''; }, ], ], diff --git a/themes/cp_admin/episode/publish.php b/themes/cp_admin/episode/publish.php index 1564462e..8e853dff 100644 --- a/themes/cp_admin/episode/publish.php +++ b/themes/cp_admin/episode/publish.php @@ -16,7 +16,7 @@ 'class' => 'mr-2 text-lg', ]) . lang('Episode.publish_form.back_to_episode_dashboard'), [ - 'class' => 'inline-flex items-center font-semibold mr-4 text-sm focus:ring-accent', + 'class' => 'inline-flex items-center font-semibold mr-4 text-sm', ], ) ?> @@ -39,7 +39,7 @@
- +
- +
/> - +
- - +
- - + +
diff --git a/themes/cp_admin/episode/publish_date_edit.php b/themes/cp_admin/episode/publish_date_edit.php index 83493448..c142d058 100644 --- a/themes/cp_admin/episode/publish_date_edit.php +++ b/themes/cp_admin/episode/publish_date_edit.php @@ -24,16 +24,16 @@ - - + diff --git a/themes/cp_admin/episode/publish_edit.php b/themes/cp_admin/episode/publish_edit.php index 31bbf3d4..12cc3efc 100644 --- a/themes/cp_admin/episode/publish_edit.php +++ b/themes/cp_admin/episode/publish_edit.php @@ -41,7 +41,7 @@
- +
- +
/> - +
- - +
- - + +
diff --git a/themes/cp_admin/episode/soundbites_list.php b/themes/cp_admin/episode/soundbites_list.php index 1955bfce..d64a8ef6 100644 --- a/themes/cp_admin/episode/soundbites_list.php +++ b/themes/cp_admin/episode/soundbites_list.php @@ -10,7 +10,7 @@ section('headerRight') ?> - + endSection() ?> section('content') ?> @@ -26,10 +26,10 @@ [ 'header' => lang('Common.actions'), 'cell' => function ($soundbite): string { - return '' . - 'id . '-menu" labelledby="more-dropdown-' . $soundbite->id . '" offsetY="-24" items="' . esc(json_encode([ [ 'type' => 'link', 'title' => lang('Soundbite.delete'), diff --git a/themes/cp_admin/episode/soundbites_new.php b/themes/cp_admin/episode/soundbites_new.php index c6ba10fb..bbcd952e 100644 --- a/themes/cp_admin/episode/soundbites_new.php +++ b/themes/cp_admin/episode/soundbites_new.php @@ -14,10 +14,10 @@
- @@ -29,7 +29,7 @@ - + diff --git a/themes/cp_admin/episode/unpublish.php b/themes/cp_admin/episode/unpublish.php index 50b8fb14..26281a53 100644 --- a/themes/cp_admin/episode/unpublish.php +++ b/themes/cp_admin/episode/unpublish.php @@ -13,13 +13,13 @@
- + - +
- - + +
diff --git a/themes/cp_admin/episode/video_clips_list.php b/themes/cp_admin/episode/video_clips_list.php index 2a0e075c..5a70ac4d 100644 --- a/themes/cp_admin/episode/video_clips_list.php +++ b/themes/cp_admin/episode/video_clips_list.php @@ -16,7 +16,7 @@ use CodeIgniter\I18n\Time; section('headerRight') ?> - + endSection() ?> section('content') ?> @@ -52,7 +52,7 @@ use CodeIgniter\I18n\Time; 'passed' => '', ]; - return '' . lang('VideoClip.list.status.' . $videoClip->status) . ''; + return '' . lang('VideoClip.list.status.' . $videoClip->status) . ''; }, ], [ @@ -63,7 +63,7 @@ use CodeIgniter\I18n\Time; 'portrait' => 'aspect-[9/16]', 'squared' => 'aspect-square', ]; - return '
' . icon('play-fill') . '
#' . $videoClip->id . ' – ' . esc($videoClip->title) . 'by ' . esc($videoClip->user->username) . '
' . format_duration((int) $videoClip->duration) . '
'; + return '
' . icon('play-fill') . '
#' . $videoClip->id . ' – ' . esc($videoClip->title) . 'by ' . esc($videoClip->user->username) . '
' . format_duration((int) $videoClip->duration) . '
'; }, ], [ @@ -98,14 +98,14 @@ use CodeIgniter\I18n\Time; helper('misc'); $filename = 'clip-' . slugify($videoClip->title) . "-{$videoClip->start_time}-{$videoClip->end_time}"; // @icon('import-fill') - $downloadButton = '' . lang('VideoClip.download_clip') . ''; + $downloadButton = '' . lang('VideoClip.download_clip') . ''; } return '
' . $downloadButton . - '' . - 'id . '-menu" labelledby="more-dropdown-' . $videoClip->id . '" offsetY="-24" items="' . esc(json_encode([ [ 'type' => 'link', 'title' => lang('VideoClip.go_to_page'), diff --git a/themes/cp_admin/episode/video_clips_new.php b/themes/cp_admin/episode/video_clips_new.php index fdcd72b0..dbd56409 100644 --- a/themes/cp_admin/episode/video_clips_new.php +++ b/themes/cp_admin/episode/video_clips_new.php @@ -31,48 +31,48 @@
- - +
- - + - + + isRequired="true" + hint="">
themes as $themeName => $colors): ?> - + style="--color-accent-base: ; --color-background-preview: ">
-
+ - +
diff --git a/themes/cp_admin/episode/video_clips_requirements.php b/themes/cp_admin/episode/video_clips_requirements.php index 73d8d1a2..d7b7e3fb 100644 --- a/themes/cp_admin/episode/video_clips_requirements.php +++ b/themes/cp_admin/episode/video_clips_requirements.php @@ -12,9 +12,9 @@
- 'flex-shrink-0 text-xl text-orange-600', - ]) ?> + ]) ?>

$value): ?> diff --git a/themes/cp_admin/episode/view.php b/themes/cp_admin/episode/view.php index a5cc18c6..bb2281a6 100644 --- a/themes/cp_admin/episode/view.php +++ b/themes/cp_admin/episode/view.php @@ -12,19 +12,19 @@ published_at, $episode->publication_status, - 'text-sm ml-2 align-middle', + 'text-sm align-middle', ) ?> endSection() ?> section('headerRight') ?> publication_status === 'published'): ?> - +> id, @@ -41,7 +41,7 @@
- " dataUrl="id, 'PodcastByEpisode', @@ -49,7 +49,7 @@ $episode->id, ) ?>"/> - " dataUrl="id, 'PodcastByEpisode', diff --git a/themes/cp_admin/fediverse/blocked_actors.php b/themes/cp_admin/fediverse/blocked_actors.php index 683ba5c7..9240a5d1 100644 --- a/themes/cp_admin/fediverse/blocked_actors.php +++ b/themes/cp_admin/fediverse/blocked_actors.php @@ -14,12 +14,12 @@
- - + isRequired="true" /> + id . '" />' . csrf_field() . - '' . + '' . lang('Fediverse.list.unblock') . '' . ''; }, ], diff --git a/themes/cp_admin/fediverse/blocked_domains.php b/themes/cp_admin/fediverse/blocked_domains.php index ec26fda7..3d34a233 100644 --- a/themes/cp_admin/fediverse/blocked_domains.php +++ b/themes/cp_admin/fediverse/blocked_domains.php @@ -14,11 +14,11 @@
- - + isRequired="true" /> + name) . '" />' . csrf_field() . - '' . + '' . lang('Fediverse.list.unblock') . '' . ''; }, ], diff --git a/themes/cp_admin/import/_queue_table.php b/themes/cp_admin/import/_queue_table.php index c6ac64eb..b607b9b1 100644 --- a/themes/cp_admin/import/_queue_table.php +++ b/themes/cp_admin/import/_queue_table.php @@ -38,9 +38,9 @@ use Modules\PodcastImport\Entities\TaskStatus; 'passed' => '', ]; - $errorHint = $importTask->status === TaskStatus::Failed ? hint_tooltip(esc($importTask->error), 'ml-1') : ''; + $errorHint = $importTask->status === TaskStatus::Failed ? '' . esc($importTask->error) . '' : ''; - return '
' . lang('PodcastImport.queue.status.' . $importTask->status->value) . '' . $errorHint . '
'; + return '
' . lang('PodcastImport.queue.status.' . $importTask->status->value) . '' . $errorHint . '
'; }, ], [ @@ -86,10 +86,10 @@ use Modules\PodcastImport\Entities\TaskStatus; 'cell' => function (PodcastImportTask $importTask) { if ($importTask->episodes_count) { $progressPercentage = (int) ($importTask->getProgress() * 100) . '%'; - $moreInfoHelper = hint_tooltip(lang('PodcastImport.queue.imported_episodes_hint', [ + $moreInfoHelper = '' . lang('PodcastImport.queue.imported_episodes_hint', [ 'newlyImportedCount' => $importTask->episodes_newly_imported, 'alreadyImportedCount' => $importTask->episodes_already_imported, - ]), 'ml-1'); + ]) . ''; return << {$progressPercentage} @@ -134,10 +134,10 @@ use Modules\PodcastImport\Entities\TaskStatus; } return '
' . - '' . - '' . + '' . '
'; }, ], diff --git a/themes/cp_admin/import/add_to_queue.php b/themes/cp_admin/import/add_to_queue.php index 75046a27..931ed1cb 100644 --- a/themes/cp_admin/import/add_to_queue.php +++ b/themes/cp_admin/import/add_to_queue.php @@ -13,51 +13,51 @@
- - - + - + isRequired="true" /> + -
- +
'absolute inset-0 h-full text-xl opacity-40 left-3', ]) ?> - +
- - -
+ - +
diff --git a/themes/cp_admin/import/podcast_queue.php b/themes/cp_admin/import/podcast_queue.php index 9deec3a8..0a3b7858 100644 --- a/themes/cp_admin/import/podcast_queue.php +++ b/themes/cp_admin/import/podcast_queue.php @@ -10,7 +10,7 @@ section('headerRight') ?> - + endSection() ?> section('content') ?> diff --git a/themes/cp_admin/import/podcast_sync.php b/themes/cp_admin/import/podcast_sync.php index d71ad77d..702c0680 100644 --- a/themes/cp_admin/import/podcast_sync.php +++ b/themes/cp_admin/import/podcast_sync.php @@ -11,14 +11,14 @@ section('content') ?>
- - + endSection() ?> diff --git a/themes/cp_admin/import/queue.php b/themes/cp_admin/import/queue.php index faedb76f..8253a9ab 100644 --- a/themes/cp_admin/import/queue.php +++ b/themes/cp_admin/import/queue.php @@ -13,7 +13,7 @@ section('headerRight') ?> - + endSection() ?> diff --git a/themes/cp_admin/my_account/change_password.php b/themes/cp_admin/my_account/change_password.php index f0731a9e..93df4baf 100644 --- a/themes/cp_admin/my_account/change_password.php +++ b/themes/cp_admin/my_account/change_password.php @@ -13,18 +13,18 @@
- - - + endSection() ?> diff --git a/themes/cp_admin/page/create.php b/themes/cp_admin/page/create.php index f1e32d23..2ab5c74c 100644 --- a/themes/cp_admin/page/create.php +++ b/themes/cp_admin/page/create.php @@ -14,29 +14,29 @@
-
- + …/pages/ - +
- - + diff --git a/themes/cp_admin/page/edit.php b/themes/cp_admin/page/edit.php index 95c5941e..614d50c4 100644 --- a/themes/cp_admin/page/edit.php +++ b/themes/cp_admin/page/edit.php @@ -14,32 +14,32 @@
-
- + …/pages/ - +
- - + diff --git a/themes/cp_admin/page/list.php b/themes/cp_admin/page/list.php index 4b08a44c..e64deb01 100644 --- a/themes/cp_admin/page/list.php +++ b/themes/cp_admin/page/list.php @@ -10,7 +10,7 @@ section('headerRight') ?> - + endSection() ?> @@ -31,9 +31,9 @@ [ 'header' => lang('Common.actions'), 'cell' => function ($page) { - return '' . - '' . - ''; + return '' . lang('Page.go_to_page') . '' . + '' . lang('Page.edit') . '' . + '' . lang('Page.delete') . ''; }, ], ], diff --git a/themes/cp_admin/page/view.php b/themes/cp_admin/page/view.php index 609fa4ad..8d26eb31 100644 --- a/themes/cp_admin/page/view.php +++ b/themes/cp_admin/page/view.php @@ -10,7 +10,7 @@ section('headerRight') ?> - + endSection() ?> section('content') ?> diff --git a/themes/cp_admin/person/_card.php b/themes/cp_admin/person/_card.php index 7a73e7d4..b4506c8c 100644 --- a/themes/cp_admin/person/_card.php +++ b/themes/cp_admin/person/_card.php @@ -8,8 +8,8 @@

full_name) ?>

- -
- -
diff --git a/themes/cp_admin/podcast/_sidebar.php b/themes/cp_admin/podcast/_sidebar.php index 4f53c3a6..c0adf96a 100644 --- a/themes/cp_admin/podcast/_sidebar.php +++ b/themes/cp_admin/podcast/_sidebar.php @@ -92,7 +92,7 @@ $podcastNavigation = [ foreach (plugins()->getPluginsWithPodcastSettings() as $plugin) { $route = route_to('plugins-podcast-settings', $podcast->id, $plugin->getKey()); $podcastNavigation['plugins']['items'][] = $route; - $podcastNavigation['plugins']['items-labels'][] = $plugin->getName(); + $podcastNavigation['plugins']['items-labels'][$route] = $plugin->getName(); $podcastNavigation['plugins']['items-permissions'][$route] = 'edit'; } @@ -118,7 +118,7 @@ foreach (plugins()->getPluginsWithPodcastSettings() as $plugin) { " class="inline-flex items-center text-sm hover:underline" data-tooltip="bottom" title="">@handle) ?> @@ -128,7 +128,7 @@ foreach (plugins()->getPluginsWithPodcastSettings() as $plugin) {
- + 'text-xl text-orange-400 inline-flex items-center justify-center rounded', ]) . 'RSS Feed' . icon('external-link-fill', [ @@ -136,7 +136,7 @@ foreach (plugins()->getPluginsWithPodcastSettings() as $plugin) { ]) ?> is_op3_enabled): ?> - + 'text-xl text-white inline-flex items-center justify-center rounded', ]) . 'OP3' . icon('external-link-fill', [ diff --git a/themes/cp_admin/podcast/analytics/index.php b/themes/cp_admin/podcast/analytics/index.php index c625586d..0275ddea 100644 --- a/themes/cp_admin/podcast/analytics/index.php +++ b/themes/cp_admin/podcast/analytics/index.php @@ -11,21 +11,21 @@ section('content') ?>
- - - - - - " dataUrl="id, 'PodcastByCountry', 'Weekly', ) ?>" /> - " dataUrl="id, 'PodcastByCountry', 'Yearly', ) ?>" /> - - " dataUrl="id, 'PodcastByPlayer', 'ByAppWeekly', ) ?>" /> - " dataUrl="id, 'PodcastByService', 'ByServiceWeekly', ) ?>" /> - " dataUrl="id, 'PodcastByPlayer', 'ByDeviceWeekly', ) ?>" /> - " dataUrl="id, 'PodcastByPlayer', 'ByOsWeekly', ) ?>" /> - - " dataUrl="id, 'Podcast', 'ByWeekday', ) ?>" /> - " dataUrl="id, 'PodcastByHour', diff --git a/themes/cp_admin/podcast/analytics/unique_listeners.php b/themes/cp_admin/podcast/analytics/unique_listeners.php index 89522300..3fff9be4 100644 --- a/themes/cp_admin/podcast/analytics/unique_listeners.php +++ b/themes/cp_admin/podcast/analytics/unique_listeners.php @@ -11,14 +11,14 @@ section('content') ?>
- - - " dataUrl="id, 'WebsiteByReferer', 'ByDomainWeekly', ) ?>" /> - " dataUrl="id, 'WebsiteByReferer', 'ByDomainYearly', ) ?>" /> - " dataUrl="id, 'WebsiteByEntryPage', ) ?>" /> - " dataUrl="id, 'WebsiteByBrowser', diff --git a/themes/cp_admin/podcast/create.php b/themes/cp_admin/podcast/create.php index 3e33bc5a..c5d17353 100644 --- a/themes/cp_admin/podcast/create.php +++ b/themes/cp_admin/podcast/create.php @@ -17,87 +17,87 @@
- - - + isRequired="true" /> -
- - + + isChecked="false" >
- - + - + + isChecked="false" >
-
+ - - - -
- +
- - + - + + isChecked="false" >
-
+ - - + isRequired="true" /> - + isRequired="true" /> - - + + - - - + -
- +
'absolute inset-0 h-full text-xl opacity-40 left-3', ]) ?> - +
- -
+ - - - - + + + + - -
'text-sm', ]) ?>op3.dev - - + + - - - + - - - - + - - + + - - + + - + - + - +
diff --git a/themes/cp_admin/podcast/delete.php b/themes/cp_admin/podcast/delete.php index 83576b90..d2f5217d 100644 --- a/themes/cp_admin/podcast/delete.php +++ b/themes/cp_admin/podcast/delete.php @@ -13,13 +13,13 @@
- + - +
- - + +
diff --git a/themes/cp_admin/podcast/edit.php b/themes/cp_admin/podcast/edit.php index d38bc5a8..d9f0b10c 100644 --- a/themes/cp_admin/podcast/edit.php +++ b/themes/cp_admin/podcast/edit.php @@ -13,7 +13,7 @@ endSection() ?> section('headerRight') ?> - + endSection() ?> section('content') ?> @@ -21,9 +21,10 @@
+
banner_id !== null): ?> - +
@@ -38,90 +39,89 @@
- - - + isRequired="true" /> -
- - + + isChecked="type === 'serial' ? 'true' : 'false' ?>" >
- +
- - + - + + isChecked="medium === 'audiobook' ? 'true' : 'false' ?>" >
-
+ - - + isRequired="true" /> - + isRequired="true" /> -
- +
- - + - + + isChecked="parental_advisory === 'explicit' ? 'true' : 'false' ?>" >
-
+ - - + isRequired="true" /> - + isRequired="true" /> - - + + - - - + -
- +
'absolute inset-0 h-full text-xl opacity-40 left-3', ]) ?> - +
- -
+ - - - - + + + + - - 'text-sm', ]) ?>op3.dev - - + + - - - + - - - - - + - - + + - - + + - + - +
- + endSection() ?> diff --git a/themes/cp_admin/podcast/latest_episodes.php b/themes/cp_admin/podcast/latest_episodes.php index 47bdd5c7..9835e997 100644 --- a/themes/cp_admin/podcast/latest_episodes.php +++ b/themes/cp_admin/podcast/latest_episodes.php @@ -1,10 +1,10 @@
- + + ) ?>" class="inline-flex items-center text-sm underline hover:no-underline"> 'ml-2', diff --git a/themes/cp_admin/podcast/list.php b/themes/cp_admin/podcast/list.php index 5ecb7976..08758797 100644 --- a/themes/cp_admin/podcast/list.php +++ b/themes/cp_admin/podcast/list.php @@ -13,8 +13,8 @@ // @icon('import-fill') // @icon('add-fill') ?> - - + + endSection() ?> diff --git a/themes/cp_admin/podcast/monetization_other.php b/themes/cp_admin/podcast/monetization_other.php index 68704a13..fd470696 100644 --- a/themes/cp_admin/podcast/monetization_other.php +++ b/themes/cp_admin/podcast/monetization_other.php @@ -12,36 +12,36 @@
- -
- +
- - + +
- - + +
- - + +
-
+ - +
endSection() ?> diff --git a/themes/cp_admin/podcast/notifications.php b/themes/cp_admin/podcast/notifications.php index dfa887b5..8551ced5 100644 --- a/themes/cp_admin/podcast/notifications.php +++ b/themes/cp_admin/podcast/notifications.php @@ -9,7 +9,7 @@ endSection() ?> section('headerRight') ?> - + endSection() ?> section('content') ?> diff --git a/themes/cp_admin/podcast/persons.php b/themes/cp_admin/podcast/persons.php index 697d3ea2..cab12b6d 100644 --- a/themes/cp_admin/podcast/persons.php +++ b/themes/cp_admin/podcast/persons.php @@ -10,7 +10,7 @@ section('headerRight') ?> - + endSection() ?> section('content') ?> @@ -18,12 +18,12 @@
- - + isRequired="true" /> - - + - +
lang('Common.actions'), 'cell' => function ($person): string { // @icon('delete-bin-fill') - return ''; + return '' . lang('Person.podcast_form.remove') . ''; }, ], ], diff --git a/themes/cp_admin/podcast/platforms.php b/themes/cp_admin/podcast/platforms.php index e65facde..1f994bf1 100644 --- a/themes/cp_admin/podcast/platforms.php +++ b/themes/cp_admin/podcast/platforms.php @@ -9,7 +9,7 @@ endSection() ?> section('headerRight') ?> - + endSection() ?> section('content') ?> diff --git a/themes/cp_admin/podcast/publish.php b/themes/cp_admin/podcast/publish.php index b8979193..25f724a3 100644 --- a/themes/cp_admin/podcast/publish.php +++ b/themes/cp_admin/podcast/publish.php @@ -16,7 +16,7 @@ 'class' => 'mr-2 text-lg', ]) . lang('Podcast.publish_form.back_to_podcast_dashboard'), [ - 'class' => 'inline-flex items-center font-semibold mr-4 text-sm focus:ring-accent', + 'class' => 'inline-flex items-center font-semibold mr-4 text-sm', ], ) ?> @@ -39,7 +39,7 @@
- +