mirror of
https://code.castopod.org/adaures/castopod
synced 2025-04-19 13:01:19 +00:00
feat(plugins): add new field types + validate & cast user data before storing settings
+ refactor form fields components
This commit is contained in:
parent
da4c1e2cfd
commit
8917bdb655
@ -65,12 +65,17 @@ class CategoryModel extends Model
|
||||
$options = array_reduce(
|
||||
$categories,
|
||||
static function (array $result, Category $category): array {
|
||||
$result[$category->id] = '';
|
||||
$label = '';
|
||||
if ($category->parent instanceof Category) {
|
||||
$result[$category->id] = lang('Podcast.category_options.' . $category->parent->code) . ' › ';
|
||||
$label = lang('Podcast.category_options.' . $category->parent->code) . ' › ';
|
||||
}
|
||||
|
||||
$result[$category->id] .= lang('Podcast.category_options.' . $category->code);
|
||||
$label .= lang('Podcast.category_options.' . $category->code);
|
||||
|
||||
$result[] = [
|
||||
'value' => $category->id,
|
||||
'label' => $label,
|
||||
];
|
||||
return $result;
|
||||
},
|
||||
[],
|
||||
|
@ -56,7 +56,10 @@ class LanguageModel extends Model
|
||||
$options = array_reduce(
|
||||
$languages,
|
||||
static function (array $result, Language $language): array {
|
||||
$result[$language->code] = $language->native_name;
|
||||
$result[] = [
|
||||
'value' => $language->code,
|
||||
'label' => $language->native_name,
|
||||
];
|
||||
return $result;
|
||||
},
|
||||
[],
|
||||
|
@ -8,7 +8,7 @@ import Dropdown from "./modules/Dropdown";
|
||||
import HotKeys from "./modules/HotKeys";
|
||||
import "./modules/markdown-preview";
|
||||
import "./modules/markdown-write-preview";
|
||||
import MultiSelect from "./modules/MultiSelect";
|
||||
import SelectMulti from "./modules/SelectMulti";
|
||||
import "./modules/permalink-edit";
|
||||
import "./modules/play-soundbite";
|
||||
import PublishMessageWarning from "./modules/PublishMessageWarning";
|
||||
@ -26,7 +26,7 @@ import "./modules/xml-editor";
|
||||
Dropdown();
|
||||
Tooltip();
|
||||
Select();
|
||||
MultiSelect();
|
||||
SelectMulti();
|
||||
Slugify();
|
||||
SidebarToggler();
|
||||
ClientTimezone();
|
||||
|
@ -1,6 +1,6 @@
|
||||
import Choices from "choices.js";
|
||||
|
||||
const MultiSelect = (): void => {
|
||||
const SelectMulti = (): void => {
|
||||
// Pass single element
|
||||
const multiSelects: NodeListOf<HTMLSelectElement> =
|
||||
document.querySelectorAll("select[multiple]");
|
||||
@ -49,4 +49,4 @@ const MultiSelect = (): void => {
|
||||
}
|
||||
};
|
||||
|
||||
export default MultiSelect;
|
||||
export default SelectMulti;
|
@ -25,7 +25,10 @@
|
||||
}
|
||||
|
||||
.choices [hidden] {
|
||||
display: none !important;
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
z-index: -9999;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.choices[data-type*="select-one"] {
|
||||
|
@ -11,13 +11,13 @@
|
||||
}
|
||||
|
||||
&:checked + .form-switch-slider::before {
|
||||
@apply transform translate-x-8;
|
||||
@apply transform translate-x-6;
|
||||
}
|
||||
|
||||
&:checked + .form-switch-slider::after {
|
||||
@apply transform translate-x-0 left-2;
|
||||
@apply transform translate-x-0 left-1.5;
|
||||
|
||||
content: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23ffffff'%3E%3Cpath fill='none' d='M0 0h24v24H0z'/%3E%3Cpath d='m10 15.172 9.192-9.193 1.415 1.414L10 18l-6.364-6.364 1.414-1.414z'/%3E%3C/svg%3E%0A");
|
||||
content: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23ffffff'%3E%3Cpath d='m10 15.172 9.192-9.193 1.415 1.414L10 18l-6.364-6.364 1.414-1.414z'/%3E%3C/svg%3E%0A");
|
||||
}
|
||||
|
||||
&:checked + .form-switch-slider.form-switch-slider--small::before {
|
||||
@ -30,7 +30,7 @@
|
||||
}
|
||||
|
||||
.form-switch-slider {
|
||||
@apply relative inset-0 flex-shrink-0 w-16 h-8 transition duration-200 rounded-full cursor-pointer bg-highlight border-contrast border-3;
|
||||
@apply relative inset-0 flex-shrink-0 h-8 transition duration-200 rounded-full cursor-pointer w-14 bg-highlight border-contrast border-3;
|
||||
|
||||
&.form-switch-slider--small {
|
||||
@apply w-12 h-6;
|
||||
@ -56,10 +56,11 @@
|
||||
}
|
||||
|
||||
&::after {
|
||||
@apply absolute w-5 h-5 transition duration-150 transform translate-x-5;
|
||||
@apply absolute w-4 h-4 transition duration-150 transform top-1;
|
||||
|
||||
content: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' %3E%3Cpath fill='none' d='M0 0h24v24H0z'/%3E%3Cpath d='m12 10.586 4.95-4.95 1.414 1.414-4.95 4.95 4.95 4.95-1.414 1.414-4.95-4.95-4.95 4.95-1.414-1.414 4.95-4.95-4.95-4.95L7.05 5.636z'/%3E%3C/svg%3E%0A");
|
||||
top: 3px;
|
||||
--tw-translate-x: 1.125rem;
|
||||
|
||||
content: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' %3E%3Cpath d='m12 10.586 4.95-4.95 1.414 1.414-4.95 4.95 4.95 4.95-1.414 1.414-4.95-4.95-4.95 4.95-1.414-1.414 4.95-4.95-4.95-4.95L7.05 5.636z'/%3E%3C/svg%3E%0A");
|
||||
left: 10px;
|
||||
}
|
||||
}
|
||||
|
@ -22,12 +22,12 @@ class Checkbox extends FormComponent
|
||||
{
|
||||
$checkboxInput = form_checkbox(
|
||||
[
|
||||
'id' => $this->value,
|
||||
'id' => $this->id,
|
||||
'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,
|
||||
old($this->name) ? old($this->name) === 'yes' : $this->isChecked,
|
||||
);
|
||||
|
||||
$hint = $this->hint === '' ? '' : (new Hint([
|
||||
|
@ -6,13 +6,13 @@ namespace App\Views\Components\Forms;
|
||||
|
||||
class ColorRadioButton extends FormComponent
|
||||
{
|
||||
protected array $props = ['isChecked'];
|
||||
protected array $props = ['isSelected'];
|
||||
|
||||
protected array $casts = [
|
||||
'isChecked' => 'boolean',
|
||||
'isSelected' => 'boolean',
|
||||
];
|
||||
|
||||
protected bool $isChecked = false;
|
||||
protected bool $isSelected = false;
|
||||
|
||||
public function render(): string
|
||||
{
|
||||
@ -29,7 +29,7 @@ class ColorRadioButton extends FormComponent
|
||||
$radioInput = form_radio(
|
||||
$data,
|
||||
$this->value,
|
||||
old($this->name) ? old($this->name) === $this->value : $this->isChecked,
|
||||
old($this->name) ? old($this->name) === $this->value : $this->isSelected,
|
||||
);
|
||||
|
||||
return <<<HTML
|
||||
|
@ -13,6 +13,7 @@ class DatetimePicker extends FormComponent
|
||||
public function render(): string
|
||||
{
|
||||
$dateInput = form_input([
|
||||
'name' => $this->name,
|
||||
'class' => 'rounded-l-lg border-0 border-rounded-r-none flex-1 focus:ring-0',
|
||||
'data-input' => '',
|
||||
], old($this->name, $this->value));
|
||||
|
@ -66,8 +66,8 @@ class Field extends Component
|
||||
unset($this->attributes['class']);
|
||||
|
||||
$this->attributes['name'] = $this->name;
|
||||
$this->attributes['isRequired'] = $this->isRequired ? 'true' : 'false';
|
||||
$this->attributes['isReadonly'] = $this->isReadonly ? 'true' : 'false';
|
||||
$this->attributes['isRequired'] = var_export($this->isRequired, true);
|
||||
$this->attributes['isReadonly'] = var_export($this->isReadonly, true);
|
||||
$element = __NAMESPACE__ . '\\' . $this->as;
|
||||
$fieldElement = new $element($this->attributes);
|
||||
|
||||
@ -75,7 +75,7 @@ class Field extends Component
|
||||
<div class="{$fieldClass}">
|
||||
{$label->render()}
|
||||
{$helperText}
|
||||
<div class="w-full mt-1">
|
||||
<div class="relative w-full mt-1">
|
||||
{$fieldElement->render()}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -25,7 +25,10 @@ abstract class FormComponent extends Component
|
||||
|
||||
protected string $name;
|
||||
|
||||
protected string $value = '';
|
||||
/**
|
||||
* @var null|string|list<string>
|
||||
*/
|
||||
protected null|string|array $value = null;
|
||||
|
||||
protected bool $isRequired = false;
|
||||
|
||||
@ -57,9 +60,4 @@ abstract class FormComponent extends Component
|
||||
$this->attributes['readonly'] = 'readonly';
|
||||
}
|
||||
}
|
||||
|
||||
public function setValue(string $value): void
|
||||
{
|
||||
$this->value = htmlspecialchars_decode($value, ENT_QUOTES);
|
||||
}
|
||||
}
|
||||
|
@ -29,6 +29,6 @@ class Input extends FormComponent
|
||||
$this->attributes['type'] = $this->type;
|
||||
$this->attributes['value'] = $this->value;
|
||||
|
||||
return form_input($this->attributes, old($this->name, $this->value));
|
||||
return form_input($this->attributes, old($this->name, (string) $this->value));
|
||||
}
|
||||
}
|
||||
|
@ -34,6 +34,8 @@ class Label extends Component
|
||||
'slot' => $this->hint,
|
||||
]))->render();
|
||||
|
||||
$this->attributes['for'] = $this->for;
|
||||
|
||||
return <<<HTML
|
||||
<label {$this->getStringifiedAttributes()}>{$this->slot}{$optionalText}{$hint}</label>
|
||||
HTML;
|
||||
|
@ -28,7 +28,7 @@ class MarkdownEditor extends FormComponent
|
||||
|
||||
$textarea = form_textarea(
|
||||
$this->attributes,
|
||||
old($this->name, $this->value)
|
||||
old($this->name, (string) $this->value)
|
||||
);
|
||||
$markdownIcon = icon('markdown-fill', [
|
||||
'class' => 'mr-1 text-lg opacity-40',
|
||||
|
@ -1,43 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Views\Components\Forms;
|
||||
|
||||
class MultiSelect extends FormComponent
|
||||
{
|
||||
protected array $props = ['options', 'selected'];
|
||||
|
||||
protected array $casts = [
|
||||
'options' => 'array',
|
||||
'selected' => 'array',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var array<string, string>
|
||||
*/
|
||||
protected array $options = [];
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
protected array $selected = [];
|
||||
|
||||
public function render(): string
|
||||
{
|
||||
$this->mergeClass('w-full bg-elevated border-3 border-contrast rounded-lg');
|
||||
|
||||
$defaultAttributes = [
|
||||
'data-class' => $this->attributes['class'],
|
||||
'multiple' => 'multiple',
|
||||
'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'),
|
||||
];
|
||||
$extra = [...$defaultAttributes, ...$this->attributes];
|
||||
|
||||
return form_dropdown($this->name, $this->options, $this->selected, $extra);
|
||||
}
|
||||
}
|
@ -11,10 +11,10 @@ class RadioButton extends FormComponent
|
||||
protected array $props = ['isChecked', 'hint'];
|
||||
|
||||
protected array $casts = [
|
||||
'isChecked' => 'boolean',
|
||||
'isSelected' => 'boolean',
|
||||
];
|
||||
|
||||
protected bool $isChecked = false;
|
||||
protected bool $isSelected = false;
|
||||
|
||||
protected string $hint = '';
|
||||
|
||||
@ -33,7 +33,7 @@ class RadioButton extends FormComponent
|
||||
$radioInput = form_radio(
|
||||
$data,
|
||||
$this->value,
|
||||
old($this->name) ? old($this->name) === $this->value : $this->isChecked,
|
||||
old($this->name) ? old($this->name) === $this->value : $this->isSelected,
|
||||
);
|
||||
|
||||
$hint = $this->hint === '' ? '' : (new Hint([
|
||||
|
67
app/Views/Components/Forms/RadioGroup.php
Normal file
67
app/Views/Components/Forms/RadioGroup.php
Normal file
@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Views\Components\Forms;
|
||||
|
||||
use App\Views\Components\Hint;
|
||||
|
||||
class RadioGroup extends FormComponent
|
||||
{
|
||||
protected array $props = ['label', 'options', 'hint', 'helper'];
|
||||
|
||||
protected array $casts = [
|
||||
'options' => 'array',
|
||||
];
|
||||
|
||||
protected string $label;
|
||||
|
||||
/**
|
||||
* @var array{value:string,label:string,hint?:string}
|
||||
*/
|
||||
protected array $options = [];
|
||||
|
||||
protected string $helper = '';
|
||||
|
||||
protected string $hint = '';
|
||||
|
||||
public function render(): string
|
||||
{
|
||||
$this->mergeClass('flex flex-col');
|
||||
|
||||
$options = '';
|
||||
foreach ($this->options as $option) {
|
||||
$options .= (new RadioButton([
|
||||
'value' => $option['value'],
|
||||
'name' => $this->name,
|
||||
'slot' => $option['label'],
|
||||
'hint' => $option['hint'] ?? '',
|
||||
'isSelected' => var_export($this->value === null ? ($option['value'] === $this->options[array_key_first($this->options)]['value']) : ($this->value === $option['value']), true),
|
||||
'isRequired' => var_export($this->isRequired, true),
|
||||
]))->render();
|
||||
}
|
||||
|
||||
$helperText = '';
|
||||
if ($this->helper !== '') {
|
||||
$helperId = $this->name . 'Help';
|
||||
$helperText = (new Helper([
|
||||
'id' => $helperId,
|
||||
'slot' => $this->helper,
|
||||
]))->render();
|
||||
$this->attributes['aria-describedby'] = $helperId;
|
||||
}
|
||||
|
||||
$hint = $this->hint === '' ? '' : (new Hint([
|
||||
'class' => 'ml-1',
|
||||
'slot' => $this->hint,
|
||||
]))->render();
|
||||
|
||||
return <<<HTML
|
||||
<fieldset {$this->getStringifiedAttributes()}>
|
||||
<legend class="-mb-1 text-sm font-semibold">{$this->label}{$hint}</legend>
|
||||
{$helperText}
|
||||
<div class="flex gap-1 mt-1">{$options}</div>
|
||||
</fieldset>
|
||||
HTML;
|
||||
}
|
||||
}
|
@ -18,7 +18,7 @@ class Section extends Component
|
||||
{
|
||||
$subtitle = $this->subtitle === '' ? '' : '<p class="text-sm text-skin-muted">' . $this->subtitle . '</p>';
|
||||
|
||||
$this->mergeClass('w-full p-8 bg-elevated border-3 flex flex-col items-start border-subtle rounded-xl');
|
||||
$this->mergeClass('w-full p-4 sm:p-6 md:p-8 bg-elevated border-3 flex flex-col items-start border-subtle rounded-xl');
|
||||
|
||||
return <<<HTML
|
||||
<fieldset {$this->getStringifiedAttributes()}>
|
||||
|
@ -6,7 +6,7 @@ namespace App\Views\Components\Forms;
|
||||
|
||||
class Select extends FormComponent
|
||||
{
|
||||
protected array $props = ['options', 'selected'];
|
||||
protected array $props = ['options', 'defaultValue'];
|
||||
|
||||
protected array $casts = [
|
||||
'options' => 'array',
|
||||
@ -17,7 +17,7 @@ class Select extends FormComponent
|
||||
*/
|
||||
protected array $options = [];
|
||||
|
||||
protected string $selected = '';
|
||||
protected string $defaultValue = '';
|
||||
|
||||
public function render(): string
|
||||
{
|
||||
@ -29,8 +29,18 @@ class Select extends FormComponent
|
||||
'data-no-choices-text' => lang('Common.forms.multiSelect.noChoicesText'),
|
||||
'data-max-item-text' => lang('Common.forms.multiSelect.maxItemText'),
|
||||
];
|
||||
$extra = [...$defaultAttributes, ...$this->attributes];
|
||||
$this->attributes = [...$defaultAttributes, ...$this->attributes];
|
||||
|
||||
return form_dropdown($this->name, $this->options, old($this->name, $this->selected !== '' ? [$this->selected] : []), $extra);
|
||||
$options = '';
|
||||
$selected = $this->value ?? $this->defaultValue;
|
||||
foreach ($this->options as $option) {
|
||||
$options .= '<option value="' . $option['value'] . '"' . ($option['value'] === $selected ? ' selected' : '') . '>' . $option['label'] . '</option>';
|
||||
}
|
||||
|
||||
$this->attributes['name'] = $this->name;
|
||||
|
||||
return <<<HTML
|
||||
<select {$this->getStringifiedAttributes()}>{$options}</select>
|
||||
HTML;
|
||||
}
|
||||
}
|
||||
|
53
app/Views/Components/Forms/SelectMulti.php
Normal file
53
app/Views/Components/Forms/SelectMulti.php
Normal file
@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Views\Components\Forms;
|
||||
|
||||
class SelectMulti extends FormComponent
|
||||
{
|
||||
protected array $props = ['options', 'defaultValue'];
|
||||
|
||||
protected array $casts = [
|
||||
'value' => 'array',
|
||||
'options' => 'array',
|
||||
'defaultValue' => 'array',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var array<string, string>
|
||||
*/
|
||||
protected array $options = [];
|
||||
|
||||
/**
|
||||
* @var list<string>
|
||||
*/
|
||||
protected array $defaultValue = [];
|
||||
|
||||
public function render(): string
|
||||
{
|
||||
$this->mergeClass('w-full bg-elevated border-3 border-contrast rounded-lg relative');
|
||||
|
||||
$defaultAttributes = [
|
||||
'multiple' => 'multiple',
|
||||
'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'),
|
||||
];
|
||||
$this->attributes = [...$defaultAttributes, ...$this->attributes];
|
||||
|
||||
$options = '';
|
||||
$selected = $this->value ?? $this->defaultValue;
|
||||
foreach ($this->options as $option) {
|
||||
$options .= '<option value="' . $option['value'] . '"' . (in_array($option['value'], $selected, true) ? ' selected' : '') . '>' . $option['label'] . '</option>';
|
||||
}
|
||||
|
||||
$this->attributes['name'] = $this->name . '[]';
|
||||
|
||||
return <<<HTML
|
||||
<select {$this->getStringifiedAttributes()}>{$options}</select>
|
||||
HTML;
|
||||
}
|
||||
}
|
@ -14,27 +14,23 @@ class Toggler extends FormComponent
|
||||
'isChecked' => 'boolean',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var 'base'|'small
|
||||
*/
|
||||
protected string $size = 'base';
|
||||
|
||||
protected string $hint = '';
|
||||
|
||||
protected bool $isChecked = false;
|
||||
|
||||
public function render(): string
|
||||
{
|
||||
$sizeClass = match ($this->size) {
|
||||
'small' => 'form-switch-slider form-switch-slider--small',
|
||||
default => 'form-switch-slider',
|
||||
};
|
||||
|
||||
$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);
|
||||
$checkbox = form_checkbox(
|
||||
[
|
||||
'id' => $this->id,
|
||||
'name' => $this->name,
|
||||
'class' => 'form-switch',
|
||||
],
|
||||
'yes',
|
||||
old($this->name) ? old($this->name) === 'yes' : $this->isChecked
|
||||
);
|
||||
|
||||
$hint = $this->hint === '' ? '' : (new Hint([
|
||||
'class' => 'ml-1',
|
||||
@ -43,9 +39,9 @@ class Toggler extends FormComponent
|
||||
|
||||
return <<<HTML
|
||||
<label {$this->getStringifiedAttributes()}>
|
||||
<span class="">{$this->slot}{$hint}</span>
|
||||
<span>{$this->slot}{$hint}</span>
|
||||
{$checkbox}
|
||||
<span class="{$sizeClass}"></span>
|
||||
<span class="form-switch-slider"></span>
|
||||
</label>
|
||||
HTML;
|
||||
}
|
||||
|
@ -10,7 +10,10 @@ use App\Models\EpisodeModel;
|
||||
use App\Models\PodcastModel;
|
||||
use CodeIgniter\Exceptions\PageNotFoundException;
|
||||
use CodeIgniter\HTTP\RedirectResponse;
|
||||
use CodeIgniter\HTTP\URI;
|
||||
use CodeIgniter\I18n\Time;
|
||||
use Modules\Admin\Controllers\BaseController;
|
||||
use Modules\Plugins\Core\Markdown;
|
||||
use Modules\Plugins\Core\Plugins;
|
||||
|
||||
class PluginController extends BaseController
|
||||
@ -103,9 +106,41 @@ class PluginController extends BaseController
|
||||
throw PageNotFoundException::forPageNotFound();
|
||||
}
|
||||
|
||||
// construct validation rules first
|
||||
$rules = [];
|
||||
foreach ($plugin->getSettingsFields('general') as $field) {
|
||||
$optionValue = $this->request->getPost($field->key);
|
||||
$plugins->setOption($plugin, $field->key, $optionValue);
|
||||
$typeRules = $plugins::FIELDS_VALIDATIONS[$field->type];
|
||||
if (! in_array('permit_empty', $typeRules, true) && ! $field->optional) {
|
||||
$typeRules[] = 'required';
|
||||
}
|
||||
|
||||
$rules[$field->key] = $typeRules;
|
||||
}
|
||||
|
||||
if (! $this->validate($rules)) {
|
||||
return redirect()
|
||||
->back()
|
||||
->withInput()
|
||||
->with('errors', $this->validator->getErrors());
|
||||
}
|
||||
|
||||
$validatedData = $this->validator->getValidated();
|
||||
|
||||
foreach ($plugin->getSettingsFields('general') as $field) {
|
||||
$value = $validatedData[$field->key] ?? null;
|
||||
$fieldValue = match ($plugins::FIELDS_CASTS[$field->type] ?? 'text') {
|
||||
'bool' => $value === 'yes',
|
||||
'int' => (int) $value,
|
||||
'uri' => new URI($value),
|
||||
'datetime' => Time::createFromFormat(
|
||||
'Y-m-d H:i',
|
||||
$value,
|
||||
$this->request->getPost('client_timezone')
|
||||
)->setTimezone(app_timezone()),
|
||||
'markdown' => new Markdown($value),
|
||||
default => $value === '' ? null : $value,
|
||||
};
|
||||
$plugins->setOption($plugin, $field->key, $fieldValue);
|
||||
}
|
||||
|
||||
return redirect()->back()
|
||||
|
@ -16,11 +16,11 @@ use League\CommonMark\Extension\SmartPunct\SmartPunctExtension;
|
||||
use League\CommonMark\MarkdownConverter;
|
||||
use Modules\Plugins\ExternalImageProcessor;
|
||||
use Modules\Plugins\ExternalLinkProcessor;
|
||||
use Modules\Plugins\Manifest\Field;
|
||||
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;
|
||||
|
||||
/**
|
||||
@ -163,7 +163,7 @@ abstract class BasePlugin implements PluginInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* @return SettingsField[]
|
||||
* @return Field[]
|
||||
*/
|
||||
final public function getSettingsFields(string $type): array
|
||||
{
|
||||
|
44
modules/Plugins/Core/Markdown.php
Normal file
44
modules/Plugins/Core/Markdown.php
Normal file
@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Modules\Plugins\Core;
|
||||
|
||||
use League\CommonMark\Environment\Environment;
|
||||
use League\CommonMark\Extension\Autolink\AutolinkExtension;
|
||||
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
|
||||
use League\CommonMark\Extension\DisallowedRawHtml\DisallowedRawHtmlExtension;
|
||||
use League\CommonMark\Extension\SmartPunct\SmartPunctExtension;
|
||||
use League\CommonMark\MarkdownConverter;
|
||||
use Stringable;
|
||||
|
||||
class Markdown implements Stringable
|
||||
{
|
||||
public function __construct(
|
||||
protected string $markdown
|
||||
) {
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->markdown;
|
||||
}
|
||||
|
||||
public function renderHTML(): string
|
||||
{
|
||||
$config = [
|
||||
'html_input' => 'escape',
|
||||
'allow_unsafe_links' => false,
|
||||
];
|
||||
|
||||
$environment = new Environment($config);
|
||||
$environment->addExtension(new CommonMarkCoreExtension());
|
||||
$environment->addExtension(new AutolinkExtension());
|
||||
$environment->addExtension(new SmartPunctExtension());
|
||||
$environment->addExtension(new DisallowedRawHtmlExtension());
|
||||
|
||||
$converter = new MarkdownConverter($environment);
|
||||
|
||||
return (string) $converter->convert($this->markdown);
|
||||
}
|
||||
}
|
@ -23,6 +23,30 @@ class Plugins
|
||||
*/
|
||||
public const HOOKS = ['channelTag', 'itemTag', 'siteHead'];
|
||||
|
||||
public const FIELDS_VALIDATIONS = [
|
||||
'checkbox' => ['permit_empty'],
|
||||
'datetime' => ['valid_date[Y-m-d H:i]'],
|
||||
'email' => ['valid_email'],
|
||||
'markdown' => ['string'],
|
||||
'number' => ['integer'],
|
||||
'radio-group' => ['string'],
|
||||
'select' => ['string'],
|
||||
'select-multiple' => ['permit_empty', 'is_list'],
|
||||
'text' => ['string'],
|
||||
'textarea' => ['string'],
|
||||
'toggler' => ['permit_empty'],
|
||||
'url' => ['valid_url_strict'],
|
||||
];
|
||||
|
||||
public const FIELDS_CASTS = [
|
||||
'checkbox' => 'bool',
|
||||
'datetime' => 'datetime',
|
||||
'number' => 'int',
|
||||
'toggler' => 'bool',
|
||||
'url' => 'uri',
|
||||
'markdown' => 'markdown',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var array<BasePlugin>
|
||||
*/
|
||||
|
64
modules/Plugins/Manifest/Field.php
Normal file
64
modules/Plugins/Manifest/Field.php
Normal file
@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Modules\Plugins\Manifest;
|
||||
|
||||
/**
|
||||
* @property 'text'|'email'|'url'|'markdown'|'number'|'switch' $type
|
||||
* @property string $key
|
||||
* @property string $label
|
||||
* @property string $hint
|
||||
* @property string $helper
|
||||
* @property bool $optional
|
||||
*/
|
||||
class Field extends ManifestObject
|
||||
{
|
||||
protected const VALIDATION_RULES = [
|
||||
'type' => 'permit_empty|in_list[checkbox,datetime,email,markdown,number,radio-group,select-multiple,select,text,textarea,toggler,url]',
|
||||
'key' => 'required|alpha_dash',
|
||||
'label' => 'required|string',
|
||||
'hint' => 'permit_empty|string',
|
||||
'helper' => 'permit_empty|string',
|
||||
'optional' => 'permit_empty|is_boolean',
|
||||
'options' => 'permit_empty|is_list',
|
||||
];
|
||||
|
||||
protected const CASTS = [
|
||||
'options' => [Option::class],
|
||||
];
|
||||
|
||||
protected string $type = 'text';
|
||||
|
||||
protected string $key;
|
||||
|
||||
protected string $label;
|
||||
|
||||
protected string $hint = '';
|
||||
|
||||
protected string $helper = '';
|
||||
|
||||
protected bool $optional = false;
|
||||
|
||||
/**
|
||||
* @var Option[]
|
||||
*/
|
||||
protected array $options = [];
|
||||
|
||||
/**
|
||||
* @return array{label:string,value:string,hint:string}[]
|
||||
*/
|
||||
public function getOptionsArray(): array
|
||||
{
|
||||
$optionsArray = [];
|
||||
foreach ($this->options as $option) {
|
||||
$optionsArray[] = [
|
||||
'label' => $option->label,
|
||||
'value' => $option->value,
|
||||
'hint' => (string) $option->hint,
|
||||
];
|
||||
}
|
||||
|
||||
return $optionsArray;
|
||||
}
|
||||
}
|
@ -39,9 +39,6 @@ class Manifest extends ManifestObject
|
||||
'repository' => 'permit_empty|is_list',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var array<string,array{string}|string>
|
||||
*/
|
||||
protected const CASTS = [
|
||||
'authors' => [Person::class],
|
||||
'homepage' => URI::class,
|
||||
|
@ -39,6 +39,11 @@ abstract class ManifestObject
|
||||
throw new Exception('Undefined object property ' . static::class . '::' . $name);
|
||||
}
|
||||
|
||||
public function __isset(string $property): bool
|
||||
{
|
||||
return property_exists($this, $property);
|
||||
}
|
||||
|
||||
public function load(): void
|
||||
{
|
||||
/** @var Validation $validation */
|
||||
|
25
modules/Plugins/Manifest/Option.php
Normal file
25
modules/Plugins/Manifest/Option.php
Normal file
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Modules\Plugins\Manifest;
|
||||
|
||||
/**
|
||||
* @property string $label
|
||||
* @property string $value
|
||||
* @property ?string $hint
|
||||
*/
|
||||
class Option extends ManifestObject
|
||||
{
|
||||
protected const VALIDATION_RULES = [
|
||||
'label' => 'required|string',
|
||||
'value' => 'required|alpha_dash',
|
||||
'hint' => 'permit_empty|string',
|
||||
];
|
||||
|
||||
protected string $label;
|
||||
|
||||
protected string $value;
|
||||
|
||||
protected ?string $hint = null;
|
||||
}
|
@ -5,9 +5,9 @@ declare(strict_types=1);
|
||||
namespace Modules\Plugins\Manifest;
|
||||
|
||||
/**
|
||||
* @property SettingsField[] $general
|
||||
* @property SettingsField[] $podcast
|
||||
* @property SettingsField[] $episode
|
||||
* @property Field[] $general
|
||||
* @property Field[] $podcast
|
||||
* @property Field[] $episode
|
||||
*/
|
||||
class Settings extends ManifestObject
|
||||
{
|
||||
@ -21,23 +21,23 @@ class Settings extends ManifestObject
|
||||
* @var array<string,array{string}|string>
|
||||
*/
|
||||
protected const CASTS = [
|
||||
'general' => [SettingsField::class],
|
||||
'podcast' => [SettingsField::class],
|
||||
'episode' => [SettingsField::class],
|
||||
'general' => [Field::class],
|
||||
'podcast' => [Field::class],
|
||||
'episode' => [Field::class],
|
||||
];
|
||||
|
||||
/**
|
||||
* @var SettingsField[]
|
||||
* @var Field[]
|
||||
*/
|
||||
protected array $general = [];
|
||||
|
||||
/**
|
||||
* @var SettingsField[]
|
||||
* @var Field[]
|
||||
*/
|
||||
protected array $podcast = [];
|
||||
|
||||
/**
|
||||
* @var SettingsField[]
|
||||
* @var Field[]
|
||||
*/
|
||||
protected array $episode = [];
|
||||
}
|
||||
|
@ -1,37 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Modules\Plugins\Manifest;
|
||||
|
||||
/**
|
||||
* @property 'text'|'email'|'url'|'markdown'|'number'|'switch' $type
|
||||
* @property string $key
|
||||
* @property string $label
|
||||
* @property string $hint
|
||||
* @property string $helper
|
||||
* @property bool $optional
|
||||
*/
|
||||
class SettingsField extends ManifestObject
|
||||
{
|
||||
protected const VALIDATION_RULES = [
|
||||
'type' => 'permit_empty|in_list[text,email,url,markdown,number,switch]',
|
||||
'key' => 'required|alpha_dash',
|
||||
'label' => 'required|string',
|
||||
'hint' => 'permit_empty|string',
|
||||
'helper' => 'permit_empty|string',
|
||||
'optional' => 'permit_empty|is_boolean',
|
||||
];
|
||||
|
||||
protected string $type = 'text';
|
||||
|
||||
protected string $key;
|
||||
|
||||
protected string $label;
|
||||
|
||||
protected string $hint = '';
|
||||
|
||||
protected string $helper = '';
|
||||
|
||||
protected bool $optional = false;
|
||||
}
|
@ -97,20 +97,26 @@
|
||||
"general": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/$defs/settings-field"
|
||||
}
|
||||
"$ref": "#/$defs/field"
|
||||
},
|
||||
"minItems": 1,
|
||||
"uniqueItems": true
|
||||
},
|
||||
"podcast": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/$defs/settings-field"
|
||||
}
|
||||
"$ref": "#/$defs/field"
|
||||
},
|
||||
"minItems": 1,
|
||||
"uniqueItems": true
|
||||
},
|
||||
"episode": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/$defs/settings-field"
|
||||
}
|
||||
"$ref": "#/$defs/field"
|
||||
},
|
||||
"minItems": 1,
|
||||
"uniqueItems": true
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -158,15 +164,48 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings-field": {
|
||||
"section": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"fields": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/$defs/field"
|
||||
},
|
||||
"minItems": 1,
|
||||
"uniqueItems": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"field": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum": ["text", "email", "url", "markdown", "number", "switch"],
|
||||
"enum": [
|
||||
"checkbox",
|
||||
"datetime",
|
||||
"email",
|
||||
"markdown",
|
||||
"number",
|
||||
"radio-group",
|
||||
"select-multiple",
|
||||
"select",
|
||||
"text",
|
||||
"textarea",
|
||||
"toggler",
|
||||
"url"
|
||||
],
|
||||
"default": "text"
|
||||
},
|
||||
"key": {
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"pattern": "^[A-Za-z]+[\\w\\-\\:\\.]*$"
|
||||
},
|
||||
"label": {
|
||||
"type": "string"
|
||||
@ -179,10 +218,56 @@
|
||||
},
|
||||
"optional": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"options": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/$defs/option"
|
||||
},
|
||||
"minItems": 1,
|
||||
"uniqueItems": true
|
||||
}
|
||||
},
|
||||
"required": ["key", "label"],
|
||||
"additionalProperties": false,
|
||||
"allOf": [
|
||||
{ "$ref": "#/$defs/field-multiple-implies-options-is-required" }
|
||||
]
|
||||
},
|
||||
"option": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"label": {
|
||||
"type": "string"
|
||||
},
|
||||
"value": {
|
||||
"type": "string"
|
||||
},
|
||||
"hint": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["label", "value"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"field-multiple-implies-options-is-required": {
|
||||
"anyOf": [
|
||||
{
|
||||
"not": {
|
||||
"properties": {
|
||||
"type": {
|
||||
"anyOf": [
|
||||
{ "const": "radio-group" },
|
||||
{ "const": "select" },
|
||||
{ "const": "select-multiple" }
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": ["type"]
|
||||
}
|
||||
},
|
||||
{ "required": ["options"] }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -69,42 +69,49 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<fieldset class="flex gap-1">
|
||||
<legend><?= lang('Episode.form.type.label') ?></legend>
|
||||
<x-Forms.RadioButton
|
||||
value="full"
|
||||
<x-Forms.RadioGroup
|
||||
label="<?= lang('Episode.form.type.label') ?>"
|
||||
name="type"
|
||||
hint="<?= esc(lang('Episode.form.type.full_hint')) ?>"
|
||||
isChecked="true" ><?= lang('Episode.form.type.full') ?></x-Forms.RadioButton>
|
||||
<x-Forms.RadioButton
|
||||
value="trailer"
|
||||
name="type"
|
||||
hint="<?= esc(lang('Episode.form.type.trailer_hint')) ?>"
|
||||
isChecked="false" ><?= lang('Episode.form.type.trailer') ?></x-Forms.RadioButton>
|
||||
<x-Forms.RadioButton
|
||||
value="bonus"
|
||||
name="type"
|
||||
hint="<?= esc(lang('Episode.form.type.bonus_hint')) ?>"
|
||||
isChecked="false" ><?= lang('Episode.form.type.bonus') ?></x-Forms.RadioButton>
|
||||
</fieldset>
|
||||
options="<?= esc(json_encode([
|
||||
[
|
||||
'label' => lang('Episode.form.type.full'),
|
||||
'value' => 'full',
|
||||
'hint' => lang('Episode.form.type.full_hint'),
|
||||
],
|
||||
[
|
||||
'label' => lang('Episode.form.type.trailer'),
|
||||
'value' => 'trailer',
|
||||
'hint' => lang('Episode.form.type.trailer_hint'),
|
||||
],
|
||||
[
|
||||
'label' => lang('Episode.form.type.bonus'),
|
||||
'value' => 'bonus',
|
||||
'hint' => lang('Episode.form.type.bonus_hint'),
|
||||
],
|
||||
])) ?>"
|
||||
isRequired="true"
|
||||
/>
|
||||
|
||||
<fieldset class="flex gap-1">
|
||||
<legend><?= lang('Episode.form.parental_advisory.label') ?><x-Hint class="ml-1"><?= lang('Episode.form.parental_advisory.hint') ?></x-Hint></legend>
|
||||
<x-Forms.RadioButton
|
||||
value="undefined"
|
||||
<x-Forms.RadioGroup
|
||||
label="<?= lang('Episode.form.parental_advisory.label') ?>"
|
||||
hint="<?= lang('Episode.form.parental_advisory.hint') ?>"
|
||||
name="parental_advisory"
|
||||
isChecked="true" ><?= lang('Episode.form.parental_advisory.undefined') ?></x-Forms.RadioButton>
|
||||
<x-Forms.RadioButton
|
||||
value="clean"
|
||||
name="parental_advisory"
|
||||
isChecked="false" ><?= lang('Episode.form.parental_advisory.clean') ?></x-Forms.RadioButton>
|
||||
<x-Forms.RadioButton
|
||||
value="explicit"
|
||||
name="parental_advisory"
|
||||
isChecked="false" ><?= lang('Episode.form.parental_advisory.explicit') ?></x-Forms.RadioButton>
|
||||
</fieldset>
|
||||
|
||||
|
||||
options="<?= esc(json_encode([
|
||||
[
|
||||
'label' => lang('Episode.form.parental_advisory.undefined'),
|
||||
'value' => 'undefined',
|
||||
],
|
||||
[
|
||||
'label' => lang('Episode.form.parental_advisory.clean'),
|
||||
'value' => 'clean',
|
||||
],
|
||||
[
|
||||
'label' => lang('Episode.form.parental_advisory.explicit'),
|
||||
'value' => 'explicit',
|
||||
],
|
||||
])) ?>"
|
||||
isRequired="true"
|
||||
/>
|
||||
|
||||
</x-Forms.Section>
|
||||
|
||||
|
@ -73,40 +73,51 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<fieldset class="flex gap-1">
|
||||
<legend><?= lang('Episode.form.type.label') ?></legend>
|
||||
<x-Forms.RadioButton
|
||||
value="full"
|
||||
<x-Forms.RadioGroup
|
||||
label="<?= lang('Episode.form.type.label') ?>"
|
||||
name="type"
|
||||
hint="<?= esc(lang('Episode.form.type.full_hint')) ?>"
|
||||
isChecked="<?= $episode->type === 'full' ? 'true' : 'false' ?>" ><?= lang('Episode.form.type.full') ?></x-Forms.RadioButton>
|
||||
<x-Forms.RadioButton
|
||||
value="trailer"
|
||||
name="type"
|
||||
hint="<?= esc(lang('Episode.form.type.trailer_hint')) ?>"
|
||||
isChecked="<?= $episode->type === 'trailer' ? 'true' : 'false' ?>" ><?= lang('Episode.form.type.trailer') ?></x-Forms.RadioButton>
|
||||
<x-Forms.RadioButton
|
||||
value="bonus"
|
||||
name="type"
|
||||
hint="<?= esc(lang('Episode.form.type.bonus_hint')) ?>"
|
||||
isChecked="<?= $episode->type === 'bonus' ? 'true' : 'false' ?>" ><?= lang('Episode.form.type.bonus') ?></x-Forms.RadioButton>
|
||||
</fieldset>
|
||||
value="<?= $episode->type ?>"
|
||||
options="<?= esc(json_encode([
|
||||
[
|
||||
'label' => lang('Episode.form.type.full'),
|
||||
'value' => 'full',
|
||||
'hint' => lang('Episode.form.type.full_hint'),
|
||||
],
|
||||
[
|
||||
'label' => lang('Episode.form.type.trailer'),
|
||||
'value' => 'trailer',
|
||||
'hint' => lang('Episode.form.type.trailer_hint'),
|
||||
],
|
||||
[
|
||||
'label' => lang('Episode.form.type.bonus'),
|
||||
'value' => 'bonus',
|
||||
'hint' => lang('Episode.form.type.bonus_hint'),
|
||||
],
|
||||
])) ?>"
|
||||
isRequired="true"
|
||||
/>
|
||||
|
||||
<fieldset class="flex gap-1">
|
||||
<legend><?= lang('Episode.form.parental_advisory.label') ?><x-Hint class="ml-1"><?= lang('Episode.form.parental_advisory.hint') ?></x-Hint></legend>
|
||||
<x-Forms.RadioButton
|
||||
value="undefined"
|
||||
<x-Forms.RadioGroup
|
||||
label="<?= lang('Episode.form.parental_advisory.label') ?>"
|
||||
hint="<?= lang('Episode.form.parental_advisory.hint') ?>"
|
||||
name="parental_advisory"
|
||||
isChecked="<?= $episode->parental_advisory === null ? 'true' : 'false' ?>" ><?= lang('Episode.form.parental_advisory.undefined') ?></x-Forms.RadioButton>
|
||||
<x-Forms.RadioButton
|
||||
value="clean"
|
||||
name="parental_advisory"
|
||||
isChecked="<?= $episode->parental_advisory === 'clean' ? 'true' : 'false' ?>" ><?= lang('Episode.form.parental_advisory.clean') ?></x-Forms.RadioButton>
|
||||
<x-Forms.RadioButton
|
||||
value="explicit"
|
||||
name="parental_advisory"
|
||||
isChecked="<?= $episode->parental_advisory === 'explicit' ? 'true' : 'false' ?>" ><?= lang('Episode.form.parental_advisory.explicit') ?></x-Forms.RadioButton>
|
||||
</fieldset>
|
||||
value="<?= $episode->parental_advisory ?>"
|
||||
options="<?= esc(json_encode([
|
||||
[
|
||||
'label' => lang('Episode.form.parental_advisory.undefined'),
|
||||
'value' => 'undefined',
|
||||
],
|
||||
[
|
||||
'label' => lang('Episode.form.parental_advisory.clean'),
|
||||
'value' => 'clean',
|
||||
],
|
||||
[
|
||||
'label' => lang('Episode.form.parental_advisory.explicit'),
|
||||
'value' => 'explicit',
|
||||
],
|
||||
])) ?>"
|
||||
isRequired="true"
|
||||
/>
|
||||
|
||||
</x-Forms.Section>
|
||||
|
||||
|
@ -24,7 +24,7 @@
|
||||
>
|
||||
|
||||
<x-Forms.Field
|
||||
as="MultiSelect"
|
||||
as="SelectMulti"
|
||||
id="persons"
|
||||
name="persons[]"
|
||||
label="<?= esc(lang('Person.episode_form.persons')) ?>"
|
||||
@ -35,7 +35,7 @@
|
||||
/>
|
||||
|
||||
<x-Forms.Field
|
||||
as="MultiSelect"
|
||||
as="SelectMulti"
|
||||
id="roles"
|
||||
name="roles[]"
|
||||
label="<?= esc(lang('Person.episode_form.roles')) ?>"
|
||||
|
@ -42,7 +42,7 @@
|
||||
<x-Forms.RadioButton
|
||||
value="landscape"
|
||||
name="format"
|
||||
isChecked="true"
|
||||
isSelected="true"
|
||||
isRequired="true"
|
||||
hint="<?= esc(lang('VideoClip.form.format.landscape_hint')) ?>"><?= lang('VideoClip.format.landscape') ?></x-Forms.RadioButton>
|
||||
<x-Forms.RadioButton
|
||||
@ -65,7 +65,7 @@
|
||||
value="<?= esc($themeName) ?>"
|
||||
name="theme"
|
||||
isRequired="true"
|
||||
isChecked="<?= $themeName === 'pine' ? 'true' : 'false' ?>"
|
||||
isSelected="<?= $themeName === 'pine' ? 'true' : 'false' ?>"
|
||||
style="--color-accent-base: <?= $colors['preview']?>; --color-background-preview: <?= $colors['preview-background'] ?>"><?= lang('Settings.theme.' . $themeName) ?></x-Forms.ColorRadioButton>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
@ -1,14 +1,144 @@
|
||||
<form method="POST" action="<?= $action ?>" class="flex flex-col max-w-sm gap-4" >
|
||||
<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): ?>
|
||||
<x-Forms.Field
|
||||
name="<?= esc($field->key) ?>"
|
||||
label="<?= esc($field->label) ?>"
|
||||
hint="<?= esc($field->hint) ?>"
|
||||
helper="<?= esc($field->helper) ?>"
|
||||
required="<?= $field->optional ? 'false' : 'true' ?>"
|
||||
value="<?= get_plugin_option($plugin->getKey(), $field->key, $context) ?>"
|
||||
/>
|
||||
<?php switch ($field->type): case 'checkbox': ?>
|
||||
<x-Forms.Checkbox
|
||||
name="<?= $field->key ?>"
|
||||
hint="<?= esc($field->hint) ?>"
|
||||
isChecked="<?= get_plugin_option($plugin->getKey(), $field->key, $context) ? 'true' : 'false' ?>"
|
||||
><?= $field->label ?></x-Forms.Checkbox>
|
||||
<?php break;
|
||||
case 'toggler': ?>
|
||||
<x-Forms.Toggler
|
||||
name="<?= $field->key ?>"
|
||||
hint="<?= esc($field->hint) ?>"
|
||||
isChecked="<?= get_plugin_option($plugin->getKey(), $field->key, $context) ? 'true' : 'false' ?>"
|
||||
><?= $field->label ?></x-Forms.Toggler>
|
||||
<?php break;
|
||||
case 'radio-group': ?>
|
||||
<x-Forms.RadioGroup
|
||||
label="<?= $field->label ?>"
|
||||
name="<?= $field->key ?>"
|
||||
options="<?= esc(json_encode($field->getOptionsArray())) ?>"
|
||||
hint="<?= esc($field->hint) ?>"
|
||||
helper="<?= esc($field->helper) ?>"
|
||||
isRequired="<?= $field->optional ? 'false' : 'true' ?>"
|
||||
value="<?= get_plugin_option($plugin->getKey(), $field->key, $context) ?>"
|
||||
/>
|
||||
<?php break;
|
||||
case 'select': ?>
|
||||
<x-Forms.Field
|
||||
as="Select"
|
||||
name="<?= $field->key ?>"
|
||||
label="<?= $field->label ?>"
|
||||
options="<?= esc(json_encode($field->getOptionsArray())) ?>"
|
||||
hint="<?= esc($field->hint) ?>"
|
||||
helper="<?= esc($field->helper) ?>"
|
||||
isRequired="<?= $field->optional ? 'false' : 'true' ?>"
|
||||
value="<?= get_plugin_option($plugin->getKey(), $field->key, $context) ?>"
|
||||
/>
|
||||
<?php break;
|
||||
case 'select-multiple': ?>
|
||||
<x-Forms.Field
|
||||
as="SelectMulti"
|
||||
name="<?= $field->key ?>"
|
||||
label="<?= $field->label ?>"
|
||||
hint="<?= esc($field->hint) ?>"
|
||||
helper="<?= esc($field->helper) ?>"
|
||||
options="<?= esc(json_encode($field->getOptionsArray())) ?>"
|
||||
isRequired="<?= $field->optional ? 'false' : 'true' ?>"
|
||||
value="<?= esc(json_encode(get_plugin_option($plugin->getKey(), $field->key, $context))) ?>"
|
||||
/>
|
||||
<?php break;
|
||||
case 'email': ?>
|
||||
<x-Forms.Field
|
||||
as="Input"
|
||||
type="email"
|
||||
name="<?= $field->key ?>"
|
||||
label="<?= esc($field->label) ?>"
|
||||
hint="<?= esc($field->hint) ?>"
|
||||
helper="<?= esc($field->helper) ?>"
|
||||
isRequired="<?= $field->optional ? 'false' : 'true' ?>"
|
||||
value="<?= get_plugin_option($plugin->getKey(), $field->key, $context) ?>"
|
||||
/>
|
||||
<?php break;
|
||||
case 'url': ?>
|
||||
<x-Forms.Field
|
||||
as="Input"
|
||||
type="url"
|
||||
placeholder="https://…"
|
||||
name="<?= $field->key ?>"
|
||||
label="<?= esc($field->label) ?>"
|
||||
hint="<?= esc($field->hint) ?>"
|
||||
helper="<?= esc($field->helper) ?>"
|
||||
isRequired="<?= $field->optional ? 'false' : 'true' ?>"
|
||||
value="<?= get_plugin_option($plugin->getKey(), $field->key, $context) ?>"
|
||||
/>
|
||||
<?php break;
|
||||
case 'number': ?>
|
||||
<x-Forms.Field
|
||||
as="Input"
|
||||
type="number"
|
||||
name="<?= $field->key ?>"
|
||||
label="<?= esc($field->label) ?>"
|
||||
hint="<?= esc($field->hint) ?>"
|
||||
helper="<?= esc($field->helper) ?>"
|
||||
isRequired="<?= $field->optional ? 'false' : 'true' ?>"
|
||||
value="<?= get_plugin_option($plugin->getKey(), $field->key, $context) ?>"
|
||||
/>
|
||||
<?php break;
|
||||
case 'textarea': ?>
|
||||
<x-Forms.Field
|
||||
as="Textarea"
|
||||
name="<?= $field->key ?>"
|
||||
label="<?= esc($field->label) ?>"
|
||||
hint="<?= esc($field->hint) ?>"
|
||||
helper="<?= esc($field->helper) ?>"
|
||||
isRequired="<?= $field->optional ? 'false' : 'true' ?>"
|
||||
value="<?= get_plugin_option($plugin->getKey(), $field->key, $context) ?>"
|
||||
/>
|
||||
<?php break;
|
||||
case 'markdown': ?>
|
||||
<x-Forms.Field
|
||||
as="MarkdownEditor"
|
||||
name="<?= $field->key ?>"
|
||||
label="<?= esc($field->label) ?>"
|
||||
hint="<?= esc($field->hint) ?>"
|
||||
helper="<?= esc($field->helper) ?>"
|
||||
isRequired="<?= $field->optional ? 'false' : 'true' ?>"
|
||||
value="<?= get_plugin_option($plugin->getKey(), $field->key, $context) ?>"
|
||||
/>
|
||||
<?php break;
|
||||
case 'datetime': ?>
|
||||
<?php $hasDatetime = true ?>
|
||||
<x-Forms.Field
|
||||
as="DatetimePicker"
|
||||
name="<?= $field->key ?>"
|
||||
label="<?= esc($field->label) ?>"
|
||||
hint="<?= esc($field->hint) ?>"
|
||||
helper="<?= esc($field->helper) ?>"
|
||||
isRequired="<?= $field->optional ? 'false' : 'true' ?>"
|
||||
value="<?= get_plugin_option($plugin->getKey(), $field->key, $context) ?>"
|
||||
/>
|
||||
<?php break;
|
||||
default: ?>
|
||||
<x-Forms.Field
|
||||
as="Input"
|
||||
name="<?= $field->key ?>"
|
||||
label="<?= esc($field->label) ?>"
|
||||
hint="<?= esc($field->hint) ?>"
|
||||
helper="<?= esc($field->helper) ?>"
|
||||
isRequired="<?= $field->optional ? 'false' : 'true' ?>"
|
||||
value="<?= get_plugin_option($plugin->getKey(), $field->key, $context) ?>"
|
||||
/>
|
||||
<?php endswitch; ?>
|
||||
|
||||
<?php endforeach; ?>
|
||||
|
||||
<?php if ($hasDatetime): ?>
|
||||
<input type="hidden" name="client_timezone" value="UTC" />
|
||||
<?php endif; ?>
|
||||
|
||||
<x-Button class="self-end mt-4" variant="primary" type="submit"><?= lang('Common.forms.save') ?></x-Button>
|
||||
</form>
|
@ -42,7 +42,7 @@
|
||||
]) ?>" data-tooltip="bottom"><?= lang('Platforms.register') ?></a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<fieldset>
|
||||
<fieldset class="flex flex-col">
|
||||
<x-Forms.Field
|
||||
label="<?= esc(lang('Platforms.your_link')) ?>"
|
||||
class="w-full mt-4"
|
||||
|
@ -41,41 +41,46 @@
|
||||
isRequired="true"
|
||||
disallowList="header,quote" />
|
||||
|
||||
<fieldset>
|
||||
<legend><?= lang('Podcast.form.type.label') ?></legend>
|
||||
<div class="flex gap-2">
|
||||
<x-Forms.RadioButton
|
||||
value="episodic"
|
||||
name="type"
|
||||
hint="<?= esc(lang('Podcast.form.type.episodic_hint')) ?>"
|
||||
isChecked="true'" ><?= lang('Podcast.form.type.episodic') ?></x-Forms.RadioButton>
|
||||
<x-Forms.RadioButton
|
||||
value="serial"
|
||||
name="type"
|
||||
hint="<?= esc(lang('Podcast.form.type.serial_hint')) ?>"
|
||||
isChecked="false" ><?= lang('Podcast.form.type.serial') ?></x-Forms.RadioButton>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend><?= lang('Podcast.form.medium.label') ?></legend>
|
||||
<div class="flex gap-2">
|
||||
<x-Forms.RadioButton
|
||||
value="podcast"
|
||||
name="medium"
|
||||
hint="<?= esc(lang('Podcast.form.medium.podcast_hint')) ?>"
|
||||
isChecked="true" ><?= lang('Podcast.form.medium.podcast') ?></x-Forms.RadioButton>
|
||||
<x-Forms.RadioButton
|
||||
value="music"
|
||||
name="medium"
|
||||
hint="<?= esc(lang('Podcast.form.medium.music_hint')) ?>"
|
||||
isChecked="false" ><?= lang('Podcast.form.medium.music') ?></x-Forms.RadioButton>
|
||||
<x-Forms.RadioButton
|
||||
value="audiobook"
|
||||
name="medium"
|
||||
hint="<?= esc(lang('Podcast.form.medium.audiobook_hint')) ?>"
|
||||
isChecked="false" ><?= lang('Podcast.form.medium.audiobook') ?></x-Forms.RadioButton>
|
||||
</div>
|
||||
</fieldset>
|
||||
<x-Forms.RadioGroup
|
||||
label="<?= lang('Podcast.form.type.label') ?>"
|
||||
name="type"
|
||||
options="<?= esc(json_encode([
|
||||
[
|
||||
'label' => lang('Podcast.form.type.episodic'),
|
||||
'value' => 'episodic',
|
||||
'hint' => lang('Podcast.form.type.episodic_hint'),
|
||||
],
|
||||
[
|
||||
'label' => lang('Podcast.form.type.serial'),
|
||||
'value' => 'serial',
|
||||
'hint' => lang('Podcast.form.type.serial_hint'),
|
||||
],
|
||||
])) ?>"
|
||||
isRequired="true"
|
||||
/>
|
||||
|
||||
<x-Forms.RadioGroup
|
||||
label="<?= lang('Podcast.form.medium.label') ?>"
|
||||
name="medium"
|
||||
options="<?= esc(json_encode([
|
||||
[
|
||||
'label' => lang('Podcast.form.medium.podcast'),
|
||||
'value' => 'podcast',
|
||||
'hint' => lang('Podcast.form.medium.podcast_hint'),
|
||||
],
|
||||
[
|
||||
'label' => lang('Podcast.form.medium.music'),
|
||||
'value' => 'music',
|
||||
'hint' => lang('Podcast.form.medium.music_hint'),
|
||||
],
|
||||
[
|
||||
'label' => lang('Podcast.form.medium.audiobook'),
|
||||
'value' => 'audiobook',
|
||||
'hint' => lang('Podcast.form.medium.audiobook_hint'),
|
||||
],
|
||||
])) ?>"
|
||||
isRequired="true"
|
||||
/>
|
||||
</x-Forms.Section>
|
||||
|
||||
<x-Forms.Section
|
||||
@ -86,7 +91,7 @@
|
||||
as="Select"
|
||||
name="language"
|
||||
label="<?= esc(lang('Podcast.form.language')) ?>"
|
||||
selected="<?= $browserLang ?>"
|
||||
defaultValue="<?= $browserLang ?>"
|
||||
isRequired="true"
|
||||
options="<?= esc(json_encode($languageOptions)) ?>" />
|
||||
|
||||
@ -98,29 +103,32 @@
|
||||
options="<?= esc(json_encode($categoryOptions)) ?>" />
|
||||
|
||||
<x-Forms.Field
|
||||
as="MultiSelect"
|
||||
as="SelectMulti"
|
||||
name="other_categories[]"
|
||||
label="<?= esc(lang('Podcast.form.other_categories')) ?>"
|
||||
data-max-item-count="2"
|
||||
options="<?= esc(json_encode($categoryOptions)) ?>" />
|
||||
|
||||
<fieldset class="mb-4">
|
||||
<legend><?= lang('Podcast.form.parental_advisory.label') ?><x-Hint class="ml-1"><?= lang('Podcast.form.parental_advisory.hint') ?></x-Hint></legend>
|
||||
<div class="flex gap-2">
|
||||
<x-Forms.RadioButton
|
||||
value="undefined"
|
||||
name="parental_advisory"
|
||||
isChecked="true" ><?= lang('Podcast.form.parental_advisory.undefined') ?></x-Forms.RadioButton>
|
||||
<x-Forms.RadioButton
|
||||
value="clean"
|
||||
name="parental_advisory"
|
||||
isChecked="false" ><?= lang('Podcast.form.parental_advisory.clean') ?></x-Forms.RadioButton>
|
||||
<x-Forms.RadioButton
|
||||
value="explicit"
|
||||
name="parental_advisory"
|
||||
isChecked="false" ><?= lang('Podcast.form.parental_advisory.explicit') ?></x-Forms.RadioButton>
|
||||
</div>
|
||||
</fieldset>
|
||||
<x-Forms.RadioGroup
|
||||
label="<?= lang('Podcast.form.parental_advisory.label') ?>"
|
||||
hint="<?= lang('Podcast.form.parental_advisory.hint') ?>"
|
||||
name="parental_advisory"
|
||||
options="<?= esc(json_encode([
|
||||
[
|
||||
'label' => lang('Podcast.form.parental_advisory.undefined'),
|
||||
'value' => 'undefined',
|
||||
],
|
||||
[
|
||||
'label' => lang('Podcast.form.parental_advisory.clean'),
|
||||
'value' => 'clean',
|
||||
],
|
||||
[
|
||||
'label' => lang('Podcast.form.parental_advisory.explicit'),
|
||||
'value' => 'explicit',
|
||||
],
|
||||
])) ?>"
|
||||
isRequired="true"
|
||||
/>
|
||||
</x-Forms.Section>
|
||||
|
||||
<x-Forms.Section
|
||||
|
@ -64,41 +64,48 @@
|
||||
isRequired="true"
|
||||
disallowList="header,quote" />
|
||||
|
||||
<fieldset>
|
||||
<legend><?= lang('Podcast.form.type.label') ?></legend>
|
||||
<div class="flex gap-2">
|
||||
<x-Forms.RadioButton
|
||||
value="episodic"
|
||||
name="type"
|
||||
hint="<?= esc(lang('Podcast.form.type.episodic_hint')) ?>"
|
||||
isChecked="<?= $podcast->type === 'episodic' ? 'true' : 'false' ?>" ><?= lang('Podcast.form.type.episodic') ?></x-Forms.RadioButton>
|
||||
<x-Forms.RadioButton
|
||||
value="serial"
|
||||
name="type"
|
||||
hint="<?= esc(lang('Podcast.form.type.serial_hint')) ?>"
|
||||
isChecked="<?= $podcast->type === 'serial' ? 'true' : 'false' ?>" ><?= lang('Podcast.form.type.serial') ?></x-Forms.RadioButton>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend><?= lang('Podcast.form.medium.label') ?><x-Hint class="ml-1"><?= lang('Podcast.form.medium.hint') ?></x-Hint></legend>
|
||||
<div class="flex gap-2">
|
||||
<x-Forms.RadioButton
|
||||
value="podcast"
|
||||
name="medium"
|
||||
hint="<?= esc(lang('Podcast.form.medium.podcast_hint')) ?>"
|
||||
isChecked="<?= $podcast->medium === 'podcast' ? 'true' : 'false' ?>" ><?= lang('Podcast.form.medium.podcast') ?></x-Forms.RadioButton>
|
||||
<x-Forms.RadioButton
|
||||
value="music"
|
||||
name="medium"
|
||||
hint="<?= esc(lang('Podcast.form.medium.music_hint')) ?>"
|
||||
isChecked="<?= $podcast->medium === 'music' ? 'true' : 'false' ?>" ><?= lang('Podcast.form.medium.music') ?></x-Forms.RadioButton>
|
||||
<x-Forms.RadioButton
|
||||
value="audiobook"
|
||||
name="medium"
|
||||
hint="<?= esc(lang('Podcast.form.medium.audiobook_hint')) ?>"
|
||||
isChecked="<?= $podcast->medium === 'audiobook' ? 'true' : 'false' ?>" ><?= lang('Podcast.form.medium.audiobook') ?></x-Forms.RadioButton>
|
||||
</div>
|
||||
</fieldset>
|
||||
<x-Forms.RadioGroup
|
||||
label="<?= lang('Podcast.form.type.label') ?>"
|
||||
name="type"
|
||||
value="<?= $podcast->type ?>"
|
||||
options="<?= esc(json_encode([
|
||||
[
|
||||
'label' => lang('Podcast.form.type.episodic'),
|
||||
'value' => 'episodic',
|
||||
'hint' => lang('Podcast.form.type.episodic_hint'),
|
||||
],
|
||||
[
|
||||
'label' => lang('Podcast.form.type.serial'),
|
||||
'value' => 'serial',
|
||||
'hint' => lang('Podcast.form.type.serial_hint'),
|
||||
],
|
||||
])) ?>"
|
||||
isRequired="true"
|
||||
/>
|
||||
|
||||
<x-Forms.RadioGroup
|
||||
label="<?= lang('Podcast.form.medium.label') ?>"
|
||||
name="medium"
|
||||
value="<?= $podcast->medium ?>"
|
||||
options="<?= esc(json_encode([
|
||||
[
|
||||
'label' => lang('Podcast.form.medium.podcast'),
|
||||
'value' => 'podcast',
|
||||
'hint' => lang('Podcast.form.medium.podcast_hint'),
|
||||
],
|
||||
[
|
||||
'label' => lang('Podcast.form.medium.music'),
|
||||
'value' => 'music',
|
||||
'hint' => lang('Podcast.form.medium.music_hint'),
|
||||
],
|
||||
[
|
||||
'label' => lang('Podcast.form.medium.audiobook'),
|
||||
'value' => 'audiobook',
|
||||
'hint' => lang('Podcast.form.medium.audiobook_hint'),
|
||||
],
|
||||
])) ?>"
|
||||
isRequired="true"
|
||||
/>
|
||||
</x-Forms.Section>
|
||||
|
||||
<x-Forms.Section
|
||||
@ -109,7 +116,7 @@
|
||||
as="Select"
|
||||
name="language"
|
||||
label="<?= esc(lang('Podcast.form.language')) ?>"
|
||||
selected="<?= $podcast->language_code ?>"
|
||||
value="<?= $podcast->language_code ?>"
|
||||
options="<?= esc(json_encode($languageOptions)) ?>"
|
||||
isRequired="true" />
|
||||
|
||||
@ -117,35 +124,39 @@
|
||||
as="Select"
|
||||
name="category"
|
||||
label="<?= esc(lang('Podcast.form.category')) ?>"
|
||||
selected="<?= $podcast->category_id ?>"
|
||||
value="<?= $podcast->category_id ?>"
|
||||
options="<?= esc(json_encode($categoryOptions)) ?>"
|
||||
isRequired="true" />
|
||||
|
||||
<x-Forms.Field
|
||||
as="MultiSelect"
|
||||
as="SelectMulti"
|
||||
name="other_categories[]"
|
||||
label="<?= esc(lang('Podcast.form.other_categories')) ?>"
|
||||
data-max-item-count="2"
|
||||
selected="<?= esc(json_encode($podcast->other_categories_ids)) ?>"
|
||||
value="<?= esc(json_encode($podcast->other_categories_ids)) ?>"
|
||||
options="<?= esc(json_encode($categoryOptions)) ?>" />
|
||||
|
||||
<fieldset class="mb-4">
|
||||
<legend><?= lang('Podcast.form.parental_advisory.label') ?><x-Hint class="ml-1"><?= lang('Podcast.form.parental_advisory.hint') ?></x-Hint></legend>
|
||||
<div class="flex gap-2">
|
||||
<x-Forms.RadioButton
|
||||
value="undefined"
|
||||
name="parental_advisory"
|
||||
isChecked="<?= $podcast->parental_advisory === null ? 'true' : 'false' ?>" ><?= lang('Podcast.form.parental_advisory.undefined') ?></x-Forms.RadioButton>
|
||||
<x-Forms.RadioButton
|
||||
value="clean"
|
||||
name="parental_advisory"
|
||||
isChecked="<?= $podcast->parental_advisory === 'clean' ? 'true' : 'false' ?>" ><?= lang('Podcast.form.parental_advisory.clean') ?></x-Forms.RadioButton>
|
||||
<x-Forms.RadioButton
|
||||
value="explicit"
|
||||
name="parental_advisory"
|
||||
isChecked="<?= $podcast->parental_advisory === 'explicit' ? 'true' : 'false' ?>" ><?= lang('Podcast.form.parental_advisory.explicit') ?></x-Forms.RadioButton>
|
||||
</div>
|
||||
</fieldset>
|
||||
<x-Forms.RadioGroup
|
||||
label="<?= lang('Podcast.form.parental_advisory.label') ?>"
|
||||
hint="<?= lang('Podcast.form.parental_advisory.hint') ?>"
|
||||
name="parental_advisory"
|
||||
value="<?= $podcast->parental_advisory ?>"
|
||||
options="<?= esc(json_encode([
|
||||
[
|
||||
'label' => lang('Podcast.form.parental_advisory.undefined'),
|
||||
'value' => 'undefined',
|
||||
],
|
||||
[
|
||||
'label' => lang('Podcast.form.parental_advisory.clean'),
|
||||
'value' => 'clean',
|
||||
],
|
||||
[
|
||||
'label' => lang('Podcast.form.parental_advisory.explicit'),
|
||||
'value' => 'explicit',
|
||||
],
|
||||
])) ?>"
|
||||
isRequired="true"
|
||||
/>
|
||||
</x-Forms.Section>
|
||||
|
||||
<x-Forms.Section
|
||||
|
@ -24,7 +24,7 @@
|
||||
>
|
||||
|
||||
<x-Forms.Field
|
||||
as="MultiSelect"
|
||||
as="SelectMulti"
|
||||
id="persons"
|
||||
name="persons[]"
|
||||
label="<?= esc(lang('Person.podcast_form.persons')) ?>"
|
||||
@ -34,7 +34,7 @@
|
||||
isRequired="true" />
|
||||
|
||||
<x-Forms.Field
|
||||
as="MultiSelect"
|
||||
as="SelectMulti"
|
||||
id="roles"
|
||||
name="roles[]"
|
||||
label="<?= esc(lang('Person.podcast_form.roles')) ?>"
|
||||
|
@ -26,7 +26,7 @@
|
||||
class="theme-<?= $themeName ?> mx-auto"
|
||||
value="<?= esc($themeName) ?>"
|
||||
name="theme"
|
||||
isChecked="<?= $themeName === service('settings')
|
||||
isSelected="<?= $themeName === service('settings')
|
||||
->get('App.theme') ? 'true' : 'false' ?>" ><?= lang('Settings.theme.' . $themeName) ?></x-Forms.ColorRadioButton>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
Loading…
x
Reference in New Issue
Block a user