mirror of
https://code.castopod.org/adaures/castopod.git
synced 2024-09-08 03:39:20 +02:00
docs(plugins): add experimental plugins section + plugins:create command to create plugin via CLI
This commit is contained in:
parent
91dc8c8325
commit
8f8c61eaae
@ -2,7 +2,7 @@
|
||||
"trailingComma": "es5",
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.md",
|
||||
"files": ["*.md", "*.mdx"],
|
||||
"options": {
|
||||
"proseWrap": "always"
|
||||
}
|
||||
|
@ -183,6 +183,35 @@ export default defineConfig({
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Plugins",
|
||||
badge: {
|
||||
text: "Experimental",
|
||||
},
|
||||
items: [
|
||||
{
|
||||
label: "Introduction",
|
||||
link: "/plugins/",
|
||||
},
|
||||
{
|
||||
label: "Creating a plugin",
|
||||
link: "/plugins/create",
|
||||
},
|
||||
{
|
||||
label: "Reference",
|
||||
items: [
|
||||
{
|
||||
label: "manifest.json",
|
||||
link: "/plugins/manifest",
|
||||
},
|
||||
{
|
||||
label: "hooks",
|
||||
link: "/plugins/hooks",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
editLink: {
|
||||
baseUrl:
|
||||
|
56
docs/src/content/docs/en/plugins/create.mdx
Normal file
56
docs/src/content/docs/en/plugins/create.mdx
Normal file
@ -0,0 +1,56 @@
|
||||
---
|
||||
title: Creating a Plugin
|
||||
---
|
||||
|
||||
import { FileTree, Steps } from "@astrojs/starlight/components";
|
||||
|
||||
In order to get started, you first need to
|
||||
[setup your Castopod dev environment](https://code.castopod.org/adaures/castopod/-/blob/develop/CONTRIBUTING-DEV.md).
|
||||
|
||||
## Using the create command
|
||||
|
||||
To quickly get you started, you can create a plugin using the following CLI
|
||||
command:
|
||||
|
||||
```sh
|
||||
php spark plugins:create
|
||||
```
|
||||
|
||||
👉 Follow the CLI instructions: you will be prompted for metadata and hooks
|
||||
definitions to generate the [plugin folder](./#plugin-folder-structure) for you.
|
||||
|
||||
## Manual setup
|
||||
|
||||
<Steps>
|
||||
1. create a plugin folder inside a vendor directory
|
||||
|
||||
<FileTree>
|
||||
- plugins
|
||||
- acme
|
||||
- **hello-world/**
|
||||
- …
|
||||
|
||||
</FileTree>
|
||||
|
||||
2. add a manifest.json file
|
||||
|
||||
<FileTree>
|
||||
|
||||
- hello-world
|
||||
- **manifest.json**
|
||||
|
||||
</FileTree>
|
||||
|
||||
See the [manifest reference](./manifest).
|
||||
|
||||
3. add the Plugin.php class
|
||||
|
||||
<FileTree>
|
||||
|
||||
- hello-world
|
||||
- manifest.json
|
||||
- **Plugin.php**
|
||||
|
||||
</FileTree>
|
||||
|
||||
</Steps>
|
3
docs/src/content/docs/en/plugins/helpers.mdx
Normal file
3
docs/src/content/docs/en/plugins/helpers.mdx
Normal file
@ -0,0 +1,3 @@
|
||||
---
|
||||
title: BasePlugin
|
||||
---
|
61
docs/src/content/docs/en/plugins/hooks.mdx
Normal file
61
docs/src/content/docs/en/plugins/hooks.mdx
Normal file
@ -0,0 +1,61 @@
|
||||
---
|
||||
title: Hooks reference
|
||||
---
|
||||
|
||||
Hooks are methods that live in the Plugin class, they are executed in parts of
|
||||
the Castopod codebase.
|
||||
|
||||
## List
|
||||
|
||||
| Hooks | Executes in |
|
||||
| ---------------- | ----------- |
|
||||
| rssBeforeChannel | RSS Feed |
|
||||
| rssAfterChannel | RSS Feed |
|
||||
| rssBeforeItem | RSS Feed |
|
||||
| rssAfterItem | RSS Feed |
|
||||
| siteHead | Website |
|
||||
|
||||
### rssBeforeChannel
|
||||
|
||||
```php
|
||||
public function rssBeforeChannel(Podcast $podcast): void
|
||||
{
|
||||
// …
|
||||
}
|
||||
```
|
||||
|
||||
### rssAfterChannel
|
||||
|
||||
```php
|
||||
public function rssAfterChannel(Podcast $podcast, SimpleRSSElement $rss): void
|
||||
{
|
||||
// …
|
||||
}
|
||||
```
|
||||
|
||||
### rssBeforeItem
|
||||
|
||||
```php
|
||||
public function rssBeforeItem(Episode $episode): void
|
||||
{
|
||||
// …
|
||||
}
|
||||
```
|
||||
|
||||
### rssAfterItem
|
||||
|
||||
```php
|
||||
public function rssAfterItem(Epsiode $episode, SimpleRSSElement $rss): void
|
||||
{
|
||||
// …
|
||||
}
|
||||
```
|
||||
|
||||
### siteHead
|
||||
|
||||
```php
|
||||
public function siteHead(): void
|
||||
{
|
||||
// …
|
||||
}
|
||||
```
|
133
docs/src/content/docs/en/plugins/index.mdx
Normal file
133
docs/src/content/docs/en/plugins/index.mdx
Normal file
@ -0,0 +1,133 @@
|
||||
---
|
||||
title: Castopod Plugins
|
||||
---
|
||||
|
||||
import { FileTree, Aside } from "@astrojs/starlight/components";
|
||||
|
||||
Plugins are ways to extend Castopod's core features.
|
||||
|
||||
## Plugin folder structure
|
||||
|
||||
<FileTree>
|
||||
|
||||
- hello-world
|
||||
- i18n
|
||||
- en.json
|
||||
- fr.json
|
||||
- …
|
||||
- icon.svg
|
||||
- [manifest.json](./manifest) // required
|
||||
- [Plugin.php](#plugin-class) // required
|
||||
- README.md
|
||||
|
||||
</FileTree>
|
||||
|
||||
Plugins reside in the `plugins` folder under a **vendor** folder, ie. the
|
||||
organisation or person who authored the plugin.
|
||||
|
||||
<FileTree>
|
||||
|
||||
- **plugins**
|
||||
- acme
|
||||
- hello-world/
|
||||
- …
|
||||
- atlantis/
|
||||
|
||||
</FileTree>
|
||||
|
||||
### manifest.json (required)
|
||||
|
||||
The plugin manifest is a JSON file containing your plugin's metadata and
|
||||
permissions.
|
||||
|
||||
This file will determine whether a plugin is valid or not. The minimal required
|
||||
data being:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "acme/hello-world",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
```
|
||||
|
||||
Checkout the [manifest.json reference](./manifest).
|
||||
|
||||
<h3 id="plugin-class">Plugin class (required)</h3>
|
||||
|
||||
This is where your plugin's logic will live.
|
||||
|
||||
The Plugin class must extend Castopod's BasePlugin class and implement one or
|
||||
multiple [Hooks](./hooks) (methods).
|
||||
|
||||
```php
|
||||
// Plugin.php
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Modules\Plugins\Core\BasePlugin;
|
||||
|
||||
class AcmeHelloWorldPlugin extends BasePlugin
|
||||
{
|
||||
// …
|
||||
}
|
||||
```
|
||||
|
||||
<Aside type="note">
|
||||
|
||||
The Plugin class name is determined by its `vendor/name` pair.
|
||||
For example, a plugin living under the `acme/hello-world` folder must be named
|
||||
`AcmeHelloWorldPlugin`:
|
||||
|
||||
- the first letter of every word is capitalized (ie. PascalCase)
|
||||
- any special caracter is removed
|
||||
- the `Plugin` suffix is added
|
||||
|
||||
</Aside>
|
||||
|
||||
### README.md
|
||||
|
||||
The `README.md` file is loaded into the plugin's view page for the user to
|
||||
read.
|
||||
It should be used for any additional information to help guide the user in using
|
||||
the plugin.
|
||||
|
||||
### icon.svg
|
||||
|
||||
The plugin icon is displayed next to its title, it is an SVG file intended to
|
||||
give a graphical representation of the plugin.
|
||||
|
||||
The icon should be squared, and be legible in a 64px by 64px circle.
|
||||
|
||||
### Internationalization (i18n)
|
||||
|
||||
Translation strings live under the `i18n` folder. Translation files are JSON
|
||||
files named as locale keys:
|
||||
|
||||
<FileTree>
|
||||
|
||||
- **i18n**
|
||||
- en.json // default locale
|
||||
- fr.json
|
||||
- de.json
|
||||
- …
|
||||
|
||||
</FileTree>
|
||||
|
||||
Supported locales are:
|
||||
`br`,`ca`,`de`,`en`,`es`,`fr`,`nn-no`,`pl`,`pt-br`,`sr-latn`,`zh-hans`.
|
||||
|
||||
The translation strings allow you to translate the title, description and
|
||||
settings keys.
|
||||
|
||||
```json
|
||||
{
|
||||
"title": "Hello, World!",
|
||||
"description": "A Castopod plugin to greet the world!",
|
||||
"settings": {
|
||||
"general": {},
|
||||
"podcast": {},
|
||||
"episode": {}
|
||||
}
|
||||
}
|
||||
```
|
59
docs/src/content/docs/en/plugins/manifest.mdx
Normal file
59
docs/src/content/docs/en/plugins/manifest.mdx
Normal file
@ -0,0 +1,59 @@
|
||||
---
|
||||
title: manifest.json reference
|
||||
---
|
||||
|
||||
This page details the attributes of a Castopod Plugin's manifest, which must be
|
||||
a JSON file.
|
||||
|
||||
### name (required)
|
||||
|
||||
The plugin name, including 'vendor-name/' prefix.
|
||||
|
||||
### version (required)
|
||||
|
||||
The plugin's semantic version (eg. 1.0.0) - see https://semver.org/
|
||||
|
||||
### description
|
||||
|
||||
The plugin's description. This helps people discover your plugin as it's listed
|
||||
in repositories
|
||||
|
||||
### authors
|
||||
|
||||
Array one or more persons having authored the plugin. A person is an object with
|
||||
a required "name" field and optional "email" and "url" fields:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Jean D'eau",
|
||||
"email": "jean.deau@example.com",
|
||||
"url": "https://example.com/"
|
||||
}
|
||||
```
|
||||
|
||||
Or you can shorten the object into a single string:
|
||||
|
||||
```json
|
||||
"Jean D'eau <jean.deau@example.com> (https://example.com/)"
|
||||
```
|
||||
|
||||
### homepage
|
||||
|
||||
The URL to the project homepage.
|
||||
|
||||
### license
|
||||
|
||||
You should specify a license for your plugin so that people know how they are
|
||||
permitted to use it, and any restrictions you're placing on it.
|
||||
|
||||
### private
|
||||
|
||||
### keywords
|
||||
|
||||
### hooks
|
||||
|
||||
### settings
|
||||
|
||||
### files
|
||||
|
||||
### repository
|
159
modules/Plugins/Commands/CreatePlugin.php
Normal file
159
modules/Plugins/Commands/CreatePlugin.php
Normal file
@ -0,0 +1,159 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Modules\Plugins\Commands;
|
||||
|
||||
use CodeIgniter\CLI\BaseCommand;
|
||||
use CodeIgniter\CLI\CLI;
|
||||
use Exception;
|
||||
use Modules\Plugins\Config\Plugins as PluginsConfig;
|
||||
use Modules\Plugins\Core\Plugins;
|
||||
use Modules\Plugins\Manifest\Manifest;
|
||||
use Override;
|
||||
|
||||
class CreatePlugin extends BaseCommand
|
||||
{
|
||||
protected const HOOKS_IMPORTS = [
|
||||
'rssBeforeChannel' => ['use App\Entities\Podcast;'],
|
||||
'rssAfterChannel' => ['use App\Entities\Podcast;', 'use App\Libraries\SimpleRSSElement;'],
|
||||
'rssBeforeItem' => ['use App\Entities\Episode;'],
|
||||
'rssAfterItem' => ['use App\Entities\Episode;', 'use App\Libraries\SimpleRSSElement;'],
|
||||
'siteHead' => [],
|
||||
];
|
||||
|
||||
protected const HOOKS_METHODS = [
|
||||
'rssBeforeChannel' => ' public function rssBeforeChannel(Podcast $podcast): void
|
||||
{
|
||||
// YOUR CODE HERE
|
||||
}',
|
||||
'rssAfterChannel' => ' public function rssAfterChannel(Podcast $podcast, SimpleRSSElement $channel): void
|
||||
{
|
||||
// YOUR CODE HERE
|
||||
}',
|
||||
'rssBeforeItem' => ' public function rssBeforeItem(Episode $episode): void
|
||||
{
|
||||
// YOUR CODE HERE
|
||||
}',
|
||||
'rssAfterItem' => ' public function rssAfterItem(Episode $episode, SimpleRSSElement $item): void
|
||||
{
|
||||
// YOUR CODE HERE
|
||||
}',
|
||||
'siteHead' => ' public function siteHead(): void
|
||||
{
|
||||
// YOUR CODE HERE
|
||||
}',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $group = 'Plugins';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $name = 'plugins:create';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Generates a new plugin folder based on a template.';
|
||||
|
||||
/**
|
||||
* Actually execute a command.
|
||||
*
|
||||
* @param list<string> $params
|
||||
*/
|
||||
#[Override]
|
||||
public function run(array $params): void
|
||||
{
|
||||
$pluginName = CLI::prompt(
|
||||
'Plugin name (<vendor>/<name>)',
|
||||
'acme/hello-world',
|
||||
Manifest::VALIDATION_RULES['name']
|
||||
);
|
||||
CLI::newLine();
|
||||
$description = CLI::prompt('Description', '', Manifest::VALIDATION_RULES['description']);
|
||||
CLI::newLine();
|
||||
$license = CLI::prompt('License', 'UNLICENSED', Manifest::VALIDATION_RULES['license']);
|
||||
CLI::newLine();
|
||||
$hooks = CLI::promptByMultipleKeys('Which hooks do you want to implement?', Plugins::HOOKS);
|
||||
|
||||
$nameParts = explode('/', $pluginName);
|
||||
$vendor = $nameParts[0];
|
||||
$name = $nameParts[1];
|
||||
|
||||
/** @var PluginsConfig $pluginsConfig */
|
||||
$pluginsConfig = config('Plugins');
|
||||
|
||||
// 1. create plugin directory if not existent
|
||||
$pluginDirectory = $pluginsConfig->folder . $vendor . DIRECTORY_SEPARATOR . $name;
|
||||
if (! file_exists($pluginDirectory)) {
|
||||
mkdir($pluginDirectory, 0755, true);
|
||||
}
|
||||
|
||||
// 2. get contents of templates
|
||||
$manifestTemplate = file_get_contents(__DIR__ . '/plugin-template/manifest.tpl.json');
|
||||
|
||||
if (! $manifestTemplate) {
|
||||
throw new Exception('Failed to get manifest template.');
|
||||
}
|
||||
|
||||
$pluginClassTemplate = file_get_contents(__DIR__ . '/plugin-template/Plugin.tpl.php');
|
||||
|
||||
if (! $pluginClassTemplate) {
|
||||
throw new Exception('Failed to get Plugin class template.');
|
||||
}
|
||||
|
||||
// 3. edit templates' contents
|
||||
$manifestContents = str_replace('"name": ""', '"name": "' . $pluginName . '"', $manifestTemplate);
|
||||
$manifestContents = str_replace(
|
||||
'"description": ""',
|
||||
'"description": "' . $description . '"',
|
||||
$manifestContents
|
||||
);
|
||||
$manifestContents = str_replace('"license": ""', '"license": "' . $license . '"', $manifestContents);
|
||||
$manifestContents = str_replace(
|
||||
'"hooks": []',
|
||||
'"hooks": ["' . implode('", "', $hooks) . '"]',
|
||||
$manifestContents
|
||||
);
|
||||
|
||||
$pluginClassName = str_replace(
|
||||
' ',
|
||||
'',
|
||||
ucwords(str_replace(['-', '_', '.'], ' ', $vendor . ' ' . $name)) . 'Plugin'
|
||||
);
|
||||
$pluginClassContents = str_replace('class Plugin', 'class ' . $pluginClassName, $pluginClassTemplate);
|
||||
|
||||
$allImports = [];
|
||||
$allMethods = [];
|
||||
foreach ($hooks as $hook) {
|
||||
$allImports = [...$allImports, ...self::HOOKS_IMPORTS[$hook]];
|
||||
$allMethods = [...$allMethods, self::HOOKS_METHODS[$hook]];
|
||||
}
|
||||
|
||||
$imports = implode(PHP_EOL, array_unique($allImports));
|
||||
$methods = implode(PHP_EOL . PHP_EOL, $allMethods);
|
||||
$pluginClassContents = str_replace('// IMPORTS_HERE', $imports, $pluginClassContents);
|
||||
$pluginClassContents = str_replace(' // HOOKS_HERE', $methods, $pluginClassContents);
|
||||
|
||||
$manifest = $pluginDirectory . '/manifest.json';
|
||||
$pluginClass = $pluginDirectory . '/Plugin.php';
|
||||
|
||||
if (! file_put_contents($manifest, $manifestContents)) {
|
||||
throw new Exception('Failed to create manifest.json file.');
|
||||
}
|
||||
|
||||
if (! file_put_contents($pluginClass, $pluginClassContents)) {
|
||||
throw new Exception('Failed to create Plugin class file.');
|
||||
}
|
||||
|
||||
CLI::newLine(1);
|
||||
CLI::write(
|
||||
sprintf('Plugin %s created in %s', CLI::color($pluginName, 'white'), CLI::color($pluginDirectory, 'white')),
|
||||
'green'
|
||||
);
|
||||
}
|
||||
}
|
@ -30,7 +30,7 @@ class UninstallPlugin extends BaseCommand
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = '';
|
||||
protected $description = 'Removes a plugin from the plugins directory.';
|
||||
|
||||
/**
|
||||
* The Command's Usage
|
||||
|
11
modules/Plugins/Commands/plugin-template/Plugin.tpl.php
Normal file
11
modules/Plugins/Commands/plugin-template/Plugin.tpl.php
Normal file
@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
// IMPORTS_HERE
|
||||
use Modules\Plugins\Core\BasePlugin;
|
||||
|
||||
class Plugin extends BasePlugin
|
||||
{
|
||||
// HOOKS_HERE
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "",
|
||||
"version": "0.1.0",
|
||||
"description": "",
|
||||
"license": "",
|
||||
"hooks": []
|
||||
}
|
@ -206,7 +206,7 @@ class PluginController extends BaseController
|
||||
|
||||
return redirect()->back()
|
||||
->with('message', lang('Plugins.messages.saveSettingsSuccess', [
|
||||
'pluginName' => $plugin->getName(),
|
||||
'pluginTitle' => $plugin->getTitle(),
|
||||
]));
|
||||
}
|
||||
|
||||
|
@ -233,17 +233,17 @@ abstract class BasePlugin implements PluginInterface
|
||||
return $this->package;
|
||||
}
|
||||
|
||||
final public function getName(): string
|
||||
final public function getTitle(): string
|
||||
{
|
||||
$key = sprintf('Plugin.%s.name', $this->key);
|
||||
/** @var string $name */
|
||||
$name = lang($key);
|
||||
$key = sprintf('Plugin.%s.title', $this->key);
|
||||
/** @var string $title */
|
||||
$title = lang($key);
|
||||
|
||||
if ($name === $key) {
|
||||
if ($title === $key) {
|
||||
return $this->manifest->name;
|
||||
}
|
||||
|
||||
return $name;
|
||||
return $title;
|
||||
}
|
||||
|
||||
final public function getDescription(): ?string
|
||||
|
@ -19,9 +19,9 @@ return [
|
||||
'declaredHooks' => 'Declared hooks',
|
||||
'settings' => 'Settings',
|
||||
'settingsTitle' => '{type, select,
|
||||
podcast {{pluginName} podcast settings}
|
||||
episode {{pluginName} episode settings}
|
||||
other {{pluginName} general settings}
|
||||
podcast {{pluginTitle} podcast settings}
|
||||
episode {{pluginTitle} episode settings}
|
||||
other {{pluginTitle} general settings}
|
||||
}',
|
||||
'view' => 'View',
|
||||
'activate' => 'Activate',
|
||||
@ -39,7 +39,7 @@ return [
|
||||
'noDescription' => 'No description',
|
||||
'noReadme' => 'No README file found.',
|
||||
'messages' => [
|
||||
'saveSettingsSuccess' => '{pluginName} settings were successfully saved!',
|
||||
'saveSettingsSuccess' => '{pluginTitle} settings were successfully saved!',
|
||||
],
|
||||
'errors' => [
|
||||
'manifestError' => 'Plugin manifest has errors',
|
||||
|
@ -25,7 +25,7 @@ class Manifest extends ManifestObject
|
||||
/**
|
||||
* @var array<string,string>
|
||||
*/
|
||||
protected const VALIDATION_RULES = [
|
||||
public const VALIDATION_RULES = [
|
||||
'name' => 'required|max_length[128]|regex_match[/^[a-z0-9]([_.-]?[a-z0-9]+)*\/[a-z0-9]([_.-]?[a-z0-9]+)*$/]',
|
||||
'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[256]',
|
||||
|
@ -88,7 +88,7 @@ $navigation = [
|
||||
foreach (plugins()->getActivePlugins() as $plugin) {
|
||||
$route = route_to('plugins-view', $plugin->getVendor(), $plugin->getPackage());
|
||||
$navigation['plugins']['items'][] = $route;
|
||||
$navigation['plugins']['items-labels'][$route] = $plugin->getName();
|
||||
$navigation['plugins']['items-labels'][$route] = $plugin->getTitle();
|
||||
$navigation['plugins']['items-permissions'][$route] = 'plugins.manage';
|
||||
}
|
||||
|
||||
|
@ -35,7 +35,7 @@ $episodeNavigation = [
|
||||
foreach (plugins()->getPluginsWithEpisodeSettings() as $plugin) {
|
||||
$route = route_to('plugins-settings-episode', $plugin->getVendor(), $plugin->getPackage(), $podcast->id, $episode->id);
|
||||
$episodeNavigation['plugins']['items'][] = $route;
|
||||
$episodeNavigation['plugins']['items-labels'][$route] = $plugin->getName();
|
||||
$episodeNavigation['plugins']['items-labels'][$route] = $plugin->getTitle();
|
||||
$episodeNavigation['plugins']['items-permissions'][$route] = 'episodes.edit';
|
||||
}
|
||||
|
||||
|
@ -18,7 +18,7 @@ use Modules\Plugins\Core\PluginStatus;
|
||||
</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 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>
|
||||
<h2 class="flex items-center text-xl font-bold font-display gap-x-2" title="<?= $plugin->getTitle() ?>"><a class="line-clamp-1" href="<?= route_to('plugins-view', $plugin->getVendor(), $plugin->getPackage()) ?>" class="hover:underline decoration-accent"><?= $plugin->getTitle() ?></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>
|
||||
|
@ -2,15 +2,15 @@
|
||||
|
||||
<?= $this->section('title') ?>
|
||||
<?= lang('Plugins.settingsTitle', [
|
||||
'pluginName' => $plugin->getName(),
|
||||
'type' => $type,
|
||||
'pluginTitle' => $plugin->getTitle(),
|
||||
'type' => $type,
|
||||
]) ?>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('pageTitle') ?>
|
||||
<?= lang('Plugins.settingsTitle', [
|
||||
'pluginName' => $plugin->getName(),
|
||||
'type' => $type,
|
||||
'pluginTitle' => $plugin->getTitle(),
|
||||
'type' => $type,
|
||||
]) ?>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
|
@ -5,11 +5,11 @@
|
||||
<?= $this->extend('_layout') ?>
|
||||
|
||||
<?= $this->section('title') ?>
|
||||
<?= $plugin->getName() ?>
|
||||
<?= $plugin->getTitle() ?>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('pageTitle') ?>
|
||||
<?= $plugin->getName() ?>
|
||||
<?= $plugin->getTitle() ?>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('headerLeft') ?>
|
||||
|
@ -92,7 +92,7 @@ $podcastNavigation = [
|
||||
foreach (plugins()->getPluginsWithPodcastSettings() as $plugin) {
|
||||
$route = route_to('plugins-settings-podcast', $plugin->getVendor(), $plugin->getPackage(), $podcast->id);
|
||||
$podcastNavigation['plugins']['items'][] = $route;
|
||||
$podcastNavigation['plugins']['items-labels'][$route] = $plugin->getName();
|
||||
$podcastNavigation['plugins']['items-labels'][$route] = $plugin->getTitle();
|
||||
$podcastNavigation['plugins']['items-permissions'][$route] = 'edit';
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user