diff --git a/modules/Plugins/Controllers/PluginController.php b/modules/Plugins/Controllers/PluginController.php index c9e4edbb5578b12de132d641d02577475c1ae308..788e5a78b7cf48c247ff01e6a1adddddabdb2bba 100644 --- a/modules/Plugins/Controllers/PluginController.php +++ b/modules/Plugins/Controllers/PluginController.php @@ -125,8 +125,15 @@ class PluginController extends BaseController $data['episode'] = $episode; } + $fields = $plugin->getSettingsFields($type); + + if ($fields === []) { + throw PageNotFoundException::forPageNotFound(); + } + $data['type'] = $type; $data['context'] = $context; + $data['fields'] = $fields; helper('form'); replace_breadcrumb_params($breadcrumbReplacements); @@ -164,8 +171,8 @@ class PluginController extends BaseController $rules = []; foreach ($plugin->getSettingsFields($type) as $field) { $typeRules = $plugins::FIELDS_VALIDATIONS[$field->type]; - if (! in_array('permit_empty', $typeRules, true) && ! $field->optional) { - $typeRules[] = 'required'; + if (! in_array('permit_empty', $typeRules, true)) { + $typeRules[] = $field->optional ? 'permit_empty' : 'required'; } $rules[$field->key] = $typeRules; @@ -182,7 +189,7 @@ class PluginController extends BaseController foreach ($plugin->getSettingsFields('general') as $field) { $value = $validatedData[$field->key] ?? null; - $fieldValue = match ($plugins::FIELDS_CASTS[$field->type] ?? 'text') { + $fieldValue = $value === '' ? null : match ($plugins::FIELDS_CASTS[$field->type] ?? 'text') { 'bool' => $value === 'yes', 'int' => (int) $value, 'uri' => new URI($value), @@ -192,7 +199,7 @@ class PluginController extends BaseController $this->request->getPost('client_timezone') )->setTimezone(app_timezone()), 'markdown' => new Markdown($value), - default => $value === '' ? null : $value, + default => $value, }; $plugins->setOption($plugin, $field->key, $fieldValue, $context); } diff --git a/modules/Plugins/Core/BasePlugin.php b/modules/Plugins/Core/BasePlugin.php index b9fe8674d77a2bbb17303d3e64e7bcbfe920cef2..7eafb3ed005ac5dc5ca490926caffb074cc49c42 100644 --- a/modules/Plugins/Core/BasePlugin.php +++ b/modules/Plugins/Core/BasePlugin.php @@ -108,11 +108,6 @@ abstract class BasePlugin implements PluginInterface return in_array($name, $this->manifest->hooks, true); } - final public function getSettings(): ?Settings - { - return $this->manifest->settings; - } - final public function getVersion(): string { return $this->manifest->version; @@ -167,7 +162,7 @@ abstract class BasePlugin implements PluginInterface */ final public function getSettingsFields(string $type): array { - $settings = $this->getSettings(); + $settings = $this->manifest->settings; if (! $settings instanceof Settings) { return []; } @@ -175,6 +170,14 @@ abstract class BasePlugin implements PluginInterface return $settings->{$type}; } + /** + * @return list<string> + */ + final public function getHooks(): array + { + return $this->manifest->hooks; + } + final public function getKey(): string { return $this->key; diff --git a/modules/Plugins/Language/en/Plugins.php b/modules/Plugins/Language/en/Plugins.php index a41c91db2d00b92e41b52132d9751ea367432191..b5e4dbd1bbff4981bbeb88db44814e1a9ad7fcf3 100644 --- a/modules/Plugins/Language/en/Plugins.php +++ b/modules/Plugins/Language/en/Plugins.php @@ -16,7 +16,13 @@ return [ 'authors' => 'Authors', 'author_email' => 'Email {authorName}', 'author_homepage' => '{authorName} homepage', + 'declaredHooks' => 'Declared hooks', 'settings' => 'Settings', + 'settingsTitle' => '{type, select, + podcast {{pluginName} podcast settings} + episode {{pluginName} episode settings} + other {{pluginName} general settings} + }', 'view' => 'View', 'activate' => 'Activate', 'deactivate' => 'Deactivate', @@ -29,6 +35,8 @@ return [ 'analytics' => 'Analytics', 'accessibility' => 'Accessibility', ], + 'noDescription' => 'No description', + 'noReadme' => 'No README file found.', 'messages' => [ 'saveSettingsSuccess' => '{pluginName} settings were successfully saved!', ], diff --git a/modules/Plugins/Manifest/Person.php b/modules/Plugins/Manifest/Person.php index 1a5b4289a7437fcf242dbe73c4b8f578303926c2..2c2cd74c158e9dcdb7dd4cb9a6c995663e026852 100644 --- a/modules/Plugins/Manifest/Person.php +++ b/modules/Plugins/Manifest/Person.php @@ -46,8 +46,8 @@ class Person extends ManifestObject $data = [ 'name' => $matches['name'], - 'email' => $matches['email'], - 'url' => $matches['url'], + 'email' => $matches['email'] ?? null, + 'url' => $matches['url'] ?? null, ]; } diff --git a/themes/cp_admin/_layout.php b/themes/cp_admin/_layout.php index a52a54410c4055ab5595263a398af534f00a8332..c68b89891f175b5be9b4af61d8882280ee6693f7 100644 --- a/themes/cp_admin/_layout.php +++ b/themes/cp_admin/_layout.php @@ -41,10 +41,10 @@ $isEpisodeArea = isset($podcast) && isset($episode); <div class="inline-flex items-center"> <?php // @icon('exchange-dollar-fill')?> <x-IconButton uri="<?= route_to('subscription-list', $podcast->id) ?>" glyph="exchange-dollar-fill" variant="secondary" size="large" class="p-0 mr-2 border-0"><?= ($isEpisodeArea && $episode->is_premium) ? lang('PremiumPodcasts.episode_is_premium') : lang('PremiumPodcasts.podcast_is_premium') ?></x-IconButton> - <x-Heading tagName="h1" size="large" class="truncate"><?= $this->renderSection('pageTitle') ?></x-Heading> + <x-Heading tagName="h1" size="large" class="max-w-sm truncate"><?= $this->renderSection('pageTitle') ?></x-Heading> </div> <?php else: ?> - <x-Heading tagName="h1" size="large" class="truncate"><?= $this->renderSection('pageTitle') ?></x-Heading> + <x-Heading tagName="h1" size="large" class="max-w-lg truncate"><?= $this->renderSection('pageTitle') ?></x-Heading> <?php endif; ?> <?= $this->renderSection('headerLeft') ?> </div> diff --git a/themes/cp_admin/_partials/_nav_menu.php b/themes/cp_admin/_partials/_nav_menu.php index ecf844cb9b77a8926c83b71aa5708e5ade0033c2..6e0f16fab588661f965eff1b9ae92fdb9754fd9e 100644 --- a/themes/cp_admin/_partials/_nav_menu.php +++ b/themes/cp_admin/_partials/_nav_menu.php @@ -49,11 +49,11 @@ ?> <li class="inline-flex"> <?php if ($isAllowed): ?> - <a class="relative w-full py-3 pl-14 pr-2 text-sm hover:opacity-100 before:content-chevronRightIcon before:absolute before:-ml-5 before:opacity-0 before:w-5 before:h-5 hover:bg-navigation-active<?= $isActive - ? ' before:opacity-100 font-semibold inline-flex items-center' + <a class="line-clamp-1 leading-9 relative w-full py-1 pl-14 pr-2 text-sm hover:opacity-100 before:content-chevronRightIcon before:absolute before:top-2 before:-ml-5 before:opacity-0 before:w-5 before:h-5 hover:bg-navigation-active<?= $isActive + ? ' before:opacity-100 font-semibold' : ' hover:before:opacity-60 focus:before:opacity-60' ?>" href="<?= $href ?>"><?= $label ?></a> <?php else: ?> - <span data-tooltip="right" title="<?= lang('Navigation.not-authorized') ?>" class="relative w-full py-3 pr-2 text-sm cursor-not-allowed before:inset-y-0 before:my-auto pl-14 hover:opacity-100 before:absolute before:content-prohibitedIcon before:-ml-5 before:opacity-60 before:w-4 before:h-4 hover:bg-navigation-active"><?= $label ?></span> + <span data-tooltip="right" title="<?= lang('Navigation.not-authorized') ?>" class="relative w-full py-3 pr-2 text-sm cursor-not-allowed line-clamp-2 before:inset-y-0 before:my-auto pl-14 hover:opacity-100 before:absolute before:content-prohibitedIcon before:-ml-5 before:opacity-60 before:w-4 before:h-4 hover:bg-navigation-active"><?= $label ?></span> <?php endif; ?> </li> <?php endforeach; ?> diff --git a/themes/cp_admin/plugins/_plugin.php b/themes/cp_admin/plugins/_plugin.php index 1c48a318c2f65c21aa76a8ade9e98fe801b0f3ec..0fd1f6c2bbc645c1ada710dace085c6ac78874e2 100644 --- a/themes/cp_admin/plugins/_plugin.php +++ b/themes/cp_admin/plugins/_plugin.php @@ -7,8 +7,8 @@ <?php endif; ?> </div> <img class="rounded-full min-w-16 max-w-16 aspect-square" src="<?= $plugin->getIconSrc() ?>"> - <div class="flex flex-col items-start mt-2"> - <h2 class="flex items-center text-xl font-bold font-display gap-x-2"><a href="<?= route_to('plugins-view', $plugin->getVendor(), $plugin->getPackage()) ?>" class="hover:underline decoration-accent"><?= $plugin->getName() ?></a></h2> + <div class="flex flex-col items-start mt-2 mb-6"> + <h2 class="flex items-center text-xl font-bold font-display gap-x-2" title="<?= $plugin->getName() ?>"><a class="line-clamp-1" href="<?= route_to('plugins-view', $plugin->getVendor(), $plugin->getPackage()) ?>" class="hover:underline decoration-accent"><?= $plugin->getName() ?></a></h2> <p class="inline-flex font-mono text-xs"> <span class="inline-flex tracking-wide bg-gray-100"> <a href="<?= route_to('plugins-vendor', $plugin->getVendor()) ?>" class="underline underline-offset-2 decoration-2 decoration-dotted hover:decoration-solid decoration-accent"><?= $plugin->getVendor() ?></a> @@ -16,9 +16,9 @@ <a class="underline underline-offset-2 decoration-2 decoration-dotted hover:decoration-solid decoration-accent" href="<?= route_to('plugins-view', $plugin->getVendor(), $plugin->getPackage()) ?>"><?= $plugin->getPackage() ?></a></span> <span class="mx-1">•</span><span class="px-1 font-mono text-xs"><?= $plugin->getVersion() ?></span> </p> - <p class="mt-2 text-gray-600"><?= $plugin->getDescription() ?></p> + <p class="relative w-full max-w-sm mt-2 text-skin-muted line-clamp-3"><?= $plugin->getDescription() ?? '<span class="absolute inset-0 px-2 m-auto text-sm lowercase shadow-sm w-fit h-fit bg-elevated">' . lang('Plugins.noDescription') . '</span><span class="block w-full h-4 mt-1 bg-gray-100"></span><span class="block w-full h-4 mt-1 bg-gray-100"></span><span class="block w-4/5 h-4 mt-1 bg-gray-100"></span>' ?></p> </div> - <footer class="flex items-center justify-between mt-6"> + <footer class="flex items-center justify-between mt-auto"> <div class="flex gap-x-2"> <?php if ($plugin->getHomepage()): ?> <?php // @icon('earth-fill')?> @@ -41,9 +41,9 @@ <x-Button type="submit" variant="secondary" size="small"><?= lang('Plugins.activate') ?></x-Button> </form> <?php endif; ?> - <?php if ($plugin->getSettings() !== []): ?> + <?php if ($plugin->getSettingsFields('general') !== []): ?> <?php // @icon('equalizer-fill')?> - <x-IconButton uri="<?= route_to('plugins-general-settings', $plugin->getVendor(), $plugin->getPackage()) ?>" glyph="equalizer-fill"><?= lang('Plugins.settings') ?></x-IconButton> + <x-IconButton uri="<?= route_to('plugins-settings-general', $plugin->getVendor(), $plugin->getPackage()) ?>" glyph="equalizer-fill"><?= lang('Plugins.settings') ?></x-IconButton> <?php endif; ?> <button class="p-2 rounded-full" id="more-dropdown-<?= $plugin->getKey() ?>" data-dropdown="button" data-dropdown-target="more-dropdown-<?= $plugin->getKey() ?>-menu" aria-haspopup="true" aria-expanded="false" title="<?= lang('Common.more') ?>"><?= icon('more-2-fill') ?></button> <?php $items = [ diff --git a/themes/cp_admin/plugins/_settings.php b/themes/cp_admin/plugins/_settings_form.php similarity index 98% rename from themes/cp_admin/plugins/_settings.php rename to themes/cp_admin/plugins/_settings_form.php index a81579118d7d584e9bc52ad72b22f40d20401e3f..c7797ad30f673612333956c1062a2eb7768c01de 100644 --- a/themes/cp_admin/plugins/_settings.php +++ b/themes/cp_admin/plugins/_settings_form.php @@ -1,7 +1,7 @@ <form method="POST" action="<?= $action ?>" class="flex flex-col max-w-xl gap-4 p-4 sm:p-6 md:p-8 bg-elevated border-3 border-subtle rounded-xl" > <?= csrf_field() ?> <?php $hasDatetime = false; ?> -<?php foreach ($plugin->getSettingsFields($type) as $field): ?> +<?php foreach ($fields as $field): ?> <?php switch ($field->type): case 'checkbox': ?> <x-Forms.Checkbox name="<?= $field->key ?>" diff --git a/themes/cp_admin/plugins/settings.php b/themes/cp_admin/plugins/settings.php index afbd1c9eda415c39032b898a054b69bbbfb28e3d..10d0e16ea914aefd0b9ba96808201ce75a437e9c 100644 --- a/themes/cp_admin/plugins/settings.php +++ b/themes/cp_admin/plugins/settings.php @@ -1,14 +1,14 @@ <?= $this->extend('_layout') ?> <?= $this->section('title') ?> -<?= lang('Plugins.settings', [ +<?= lang('Plugins.settingsTitle', [ 'pluginName' => $plugin->getName(), 'type' => $type, ]) ?> <?= $this->endSection() ?> <?= $this->section('pageTitle') ?> -<?= lang('Plugins.settings', [ +<?= lang('Plugins.settingsTitle', [ 'pluginName' => $plugin->getName(), 'type' => $type, ]) ?> @@ -30,10 +30,10 @@ if (isset($episode)) { ?> <?= $this->section('content') ?> -<?= view('plugins/_settings', [ +<?= view('plugins/_settings_form', [ 'plugin' => $plugin, 'action' => route_to(sprintf('plugins-settings-%s-action', $type), ...$params), - 'type' => $type, + 'fields' => $fields, 'context' => $context, ]) ?> <?= $this->endSection() ?> diff --git a/themes/cp_admin/plugins/view.php b/themes/cp_admin/plugins/view.php index 8bb8462b271f1f79121419d40fdb03c30ce2f51d..68e94e411472a4917b574f82ca55110031b307fa 100644 --- a/themes/cp_admin/plugins/view.php +++ b/themes/cp_admin/plugins/view.php @@ -18,14 +18,22 @@ <?= $this->section('headerRight') ?> <?php if($plugin->isActive()): ?> - <form class="flex justify-end" method="POST" action="<?= route_to('plugins-deactivate', $plugin->getVendor(), $plugin->getPackage()) ?>"> + <form class="flex justify-end gap-x-2" method="POST" action="<?= route_to('plugins-deactivate', $plugin->getVendor(), $plugin->getPackage()) ?>"> <?= csrf_field() ?> <x-Button type="submit" variant="danger"><?= lang('Plugins.deactivate') ?></x-Button> + <?php if ($plugin->getSettingsFields('general') !== []): ?> + <?php // @icon('equalizer-fill')?> + <x-Button class="ring-2 ring-inset ring-gray-600" iconLeft="equalizer-fill" uri="<?= route_to('plugins-settings-general', $plugin->getVendor(), $plugin->getPackage()) ?>"><?= lang('Plugins.settings') ?></x-Button> + <?php endif; ?> </form> <?php else: ?> - <form class="flex justify-end" method="POST" action="<?= route_to('plugins-activate', $plugin->getVendor(), $plugin->getPackage()) ?>"> + <form class="flex justify-end gap-x-2" method="POST" action="<?= route_to('plugins-activate', $plugin->getVendor(), $plugin->getPackage()) ?>"> <?= csrf_field() ?> <x-Button type="submit" variant="secondary"><?= lang('Plugins.activate') ?></x-Button> + <?php if ($plugin->getSettingsFields('general') !== []): ?> + <?php // @icon('equalizer-fill')?> + <x-Button class="ring-2 ring-inset ring-gray-600" iconLeft="equalizer-fill" uri="<?= route_to('plugins-settings-general', $plugin->getVendor(), $plugin->getPackage()) ?>"><?= lang('Plugins.settings') ?></x-Button> + <?php endif; ?> </form> <?php endif; ?> <?= $this->endSection() ?> @@ -34,8 +42,10 @@ <div class="flex flex-col items-start justify-center gap-8 mx-auto xl:flex-row-reverse"> <aside class="w-full pb-8 border-b xl:sticky xl:max-w-xs top-28 border-subtle xl:border-none"> <h2 class="mb-2 text-2xl font-bold font-display"><?= lang('Plugins.about') ?></h2> - <p><?= $plugin->getDescription() ?></p> - <a href="<?= $plugin->getHomepage() ?>" class="inline-flex items-center mt-2 font-semibold hover:underline gap-x-2"><?= icon('link') . $plugin->getHomepage() ?></a> + <p class="relative max-w-sm text-skin-muted"><?= $plugin->getDescription() ?? '<span class="absolute inset-0 px-2 m-auto text-sm lowercase shadow-sm w-fit h-fit bg-base">' . lang('Plugins.noDescription') . '</span><span class="block w-full h-4 mt-1 bg-subtle"></span><span class="block w-full h-4 mt-1 bg-subtle"></span><span class="block w-4/5 h-4 mt-1 bg-subtle"></span>' ?></p> + <?php if ($plugin->getHomepage()): ?> + <a href="<?= $plugin->getHomepage() ?>" class="inline-flex items-center mt-2 font-semibold hover:underline gap-x-2"><?= icon('link') . $plugin->getHomepage() ?></a> + <?php endif; ?> <?php if ($plugin->getKeywords() !== []): ?> <div class="mt-2"> <?php foreach ($plugin->getKeywords() as $keyword): ?> @@ -75,9 +85,26 @@ <?php endforeach; ?> </ul> <?php endif; ?> + <?php if ($plugin->getHooks() !== []): ?> + <h3 class="mt-6 text-lg font-bold font-display"><?= lang('Plugins.declaredHooks') ?></h3> + <ul> + <?php foreach ($plugin->getHooks() as $hook): ?> + <li><?= $hook ?></li> + <?php endforeach; ?> + </ul> + <?php endif; ?> </aside> - <section class="max-w-2xl prose prose-headings:font-display"> - <?= $plugin->getReadmeHTML() ?> - </section> + <?php if($plugin->getReadmeHTML()): ?> + <section class="w-full max-w-3xl p-4 prose border rounded-t-lg rounded-b-lg xl:rounded-t-none xl:-mt-12 md:p-6 xl:p-12 prose-headings:font-display bg-elevated border-subtle"> + <?= $plugin->getReadmeHTML() ?> + </section> + <?php else: ?> + <section class="flex flex-col items-center justify-center w-full max-w-3xl p-4 border rounded-t-lg rounded-b-lg xl:rounded-t-none xl:-mt-12 md:p-6 xl:p-12 bg-elevated border-subtle min-h-96"> + <?= icon('article-line', [ + 'class' => 'text-gray-300 text-6xl', + ]) ?> + <p class="mt-2 font-semibold text-skin-muted"><?= lang('Plugins.noReadme') ?></p> + </section> + <?php endif; ?> </div> <?= $this->endSection() ?> \ No newline at end of file