diff --git a/app/Resources/js/admin-audio-player.ts b/app/Resources/js/admin-audio-player.ts new file mode 100644 index 0000000000000000000000000000000000000000..28460b891c38571d13026cc06adb844e9a839759 --- /dev/null +++ b/app/Resources/js/admin-audio-player.ts @@ -0,0 +1,79 @@ +import { + VmAudio, + VmCaptions, + VmClickToPlay, + VmControl, + VmControls, + VmCurrentTime, + VmDefaultControls, + VmDefaultSettings, + VmDefaultUi, + VmEndTime, + VmFile, + VmIcon, + VmIconLibrary, + VmLoadingScreen, + VmMenu, + VmMenuItem, + VmMenuRadio, + VmMenuRadioGroup, + VmMuteControl, + VmPlaybackControl, + VmPlayer, + VmScrubberControl, + VmSettings, + VmSettingsControl, + VmSkeleton, + VmSlider, + VmSubmenu, + VmTime, + VmTimeProgress, + VmTooltip, + VmUi, + VmVolumeControl, +} from "@vime/core"; +import "@vime/core/themes/default.css"; +import "@vime/core/themes/light.css"; +import "./modules/play-episode-button"; + +// Register Castopod's icons library +const library: HTMLVmIconLibraryElement | null = document.querySelector( + 'vm-icon-library[name="castopod-icons"]' +); +if (library) { + library.resolver = (iconName) => `/assets/icons/${iconName}.svg`; +} + +// Vime elements for audio player +customElements.define("vm-player", VmPlayer); +customElements.define("vm-file", VmFile); +customElements.define("vm-audio", VmAudio); +customElements.define("vm-ui", VmUi); +customElements.define("vm-default-ui", VmDefaultUi); +customElements.define("vm-click-to-play", VmClickToPlay); +customElements.define("vm-captions", VmCaptions); +customElements.define("vm-loading-screen", VmLoadingScreen); +customElements.define("vm-default-controls", VmDefaultControls); +customElements.define("vm-default-settings", VmDefaultSettings); +customElements.define("vm-controls", VmControls); +customElements.define("vm-playback-control", VmPlaybackControl); +customElements.define("vm-volume-control", VmVolumeControl); +customElements.define("vm-scrubber-control", VmScrubberControl); +customElements.define("vm-current-time", VmCurrentTime); +customElements.define("vm-end-time", VmEndTime); +customElements.define("vm-settings-control", VmSettingsControl); +customElements.define("vm-time-progress", VmTimeProgress); +customElements.define("vm-control", VmControl); +customElements.define("vm-icon", VmIcon); +customElements.define("vm-icon-library", VmIconLibrary); +customElements.define("vm-tooltip", VmTooltip); +customElements.define("vm-mute-control", VmMuteControl); +customElements.define("vm-slider", VmSlider); +customElements.define("vm-time", VmTime); +customElements.define("vm-menu", VmMenu); +customElements.define("vm-menu-item", VmMenuItem); +customElements.define("vm-submenu", VmSubmenu); +customElements.define("vm-menu-radio-group", VmMenuRadioGroup); +customElements.define("vm-menu-radio", VmMenuRadio); +customElements.define("vm-settings", VmSettings); +customElements.define("vm-skeleton", VmSkeleton); diff --git a/app/Views/Components/DropdownMenu.php b/app/Views/Components/DropdownMenu.php new file mode 100644 index 0000000000000000000000000000000000000000..f83712c217d6668593cc5f6127b748d709c4ec86 --- /dev/null +++ b/app/Views/Components/DropdownMenu.php @@ -0,0 +1,51 @@ +<?php + +declare(strict_types=1); + +namespace App\Views\Components; + +use Exception; +use ViewComponents\Component; + +class DropdownMenu extends Component +{ + public string $id = ''; + + public array $items = []; + + public function setItems(string $value): void + { + $this->items = json_decode(html_entity_decode($value), true); + } + + public function render(): string + { + if ($this->items === []) { + throw new Exception('Dropdown menu has no items'); + } + + $menuItems = ''; + foreach ($this->items as $item) { + switch ($item['type']) { + case 'link': + $menuItems .= anchor($item['uri'], $item['title'], [ + 'class' => 'px-4 py-1 hover:bg-gray-100' . (array_key_exists('class', $item) ? ' ' . $item['class'] : ''), + ]); + break; + case 'separator': + $menuItems .= '<hr class="my-2 border border-gray-100">'; + break; + default: + break; + } + } + + return <<<HTML + <nav id="{$this->id}" + class="absolute z-50 flex flex-col py-2 text-black whitespace-no-wrap bg-white border-black rounded-lg border-3" + aria-labelledby="{$this->labeledBy}" + data-dropdown="menu" + data-dropdown-placement="bottom-end">{$menuItems}</nav> + HTML; + } +} diff --git a/themes/cp_admin/_layout.php b/themes/cp_admin/_layout.php index 1950c5ded5cb090e080dedfb7451e02177e661ad..f6b47bd4242d331c654f9b498542cba5a45265a8 100644 --- a/themes/cp_admin/_layout.php +++ b/themes/cp_admin/_layout.php @@ -14,7 +14,7 @@ <?= service('vite') ->asset('js/admin.ts', 'js') ?> <?= service('vite') - ->asset('js/audio-player.ts', 'js') ?> + ->asset('js/admin-audio-player.ts', 'js') ?> </head> <body class="relative bg-pine-50 holy-grail-grid"> @@ -40,28 +40,26 @@ data-dropdown="button" data-dropdown-target="my-account-dropdown-menu" aria-haspopup="true" - aria-expanded="false"> - <?= icon('account-circle', 'text-2xl opacity-60 mr-2') ?> - <?= user() - ->username ?> - <?= icon('caret-down', 'ml-auto text-2xl') ?> - </button> - <nav - id="my-account-dropdown-menu" - class="absolute z-50 flex flex-col py-2 text-black whitespace-no-wrap bg-white border-black rounded border-[3px]" - aria-labelledby="my-accountDropdown" - data-dropdown="menu" - data-dropdown-placement="bottom-end"> - <a class="px-4 py-1 hover:bg-gray-100" href="<?= route_to( - 'my-account', - ) ?>"><?= lang('AdminNavigation.account.my-account') ?></a> - <a class="px-4 py-1 hover:bg-gray-100" href="<?= route_to( - 'change-password', - ) ?>"><?= lang('AdminNavigation.account.change-password') ?></a> - <a class="px-4 py-1 hover:bg-gray-100" href="<?= route_to( - 'logout', - ) ?>"><?= lang('AdminNavigation.account.logout') ?></a> - </nav> + aria-expanded="false"><?= icon('account-circle', 'text-2xl opacity-60 mr-2') . user()->username . icon('caret-down', 'ml-auto text-2xl') ?></button> + <DropdownMenu id="my-account-dropdown-menu" labeledBy="my-account-dropdown" items="<?= esc(json_encode([ + [ + 'type' => 'link', + 'title' => lang('AdminNavigation.account.my-account'), + 'uri' => route_to('my-account'), + ], + [ + 'type' => 'link', + 'title' => lang('AdminNavigation.account.change-password'), + 'uri' => route_to('change-password'), + ], + [ + 'type' => 'separator', + ], + [ + 'type' => 'link', + 'title' => lang('AdminNavigation.account.logout'), + 'uri' => route_to('logout'), + ], ])) ?>" /> </header> <aside id="admin-sidebar" class="sticky z-50 flex flex-col text-white transition duration-200 ease-in-out transform -translate-x-full border-r top-10 border-pine-900 bg-pine-800 holy-grail__sidebar md:translate-x-0"> <?php if (isset($podcast) && isset($episode)): ?> @@ -80,7 +78,7 @@ </footer> </aside> <main class="relative holy-grail__main"> - <header class="z-40 flex items-center bg-white border-b sticky-header-outer border-pine-100"> + <header class="z-40 flex items-center px-4 bg-white border-b md:px-12 sticky-header-outer border-pine-100"> <div class="container flex flex-col justify-end mx-auto -mt-4 sticky-header-inner"> <?= render_breadcrumb('text-gray-800 text-xs items-center flex') ?> <div class="flex justify-between py-1"> diff --git a/themes/cp_admin/episode/list.php b/themes/cp_admin/episode/list.php index ea736f12662032c4ba16d1ad27e54f0e0a1b6d63..42577e05e4990498ff1bf3e7ea1c54278a593496 100644 --- a/themes/cp_admin/episode/list.php +++ b/themes/cp_admin/episode/list.php @@ -74,44 +74,45 @@ [ 'header' => lang('Episode.list.actions'), 'cell' => function ($episode, $podcast) { - return '<button id="more-dropdown-<?= $episode->id ?>" type="button" class="inline-flex items-center p-1 outline-none focus:ring" data-dropdown="button" data-dropdown-target="more-dropdown-<?= $episode->id ?>-menu" aria-haspopup="true" aria-expanded="false">' . + return '<button id="more-dropdown-' . $episode->id . '" type="button" class="inline-flex items-center p-1 outline-none focus:ring" data-dropdown="button" data-dropdown-target="more-dropdown-' . $episode->id . '-menu" aria-haspopup="true" aria-expanded="false">' . icon('more') . '</button>' . - '<nav id="more-dropdown-<?= $episode->id ?>-menu" class="flex flex-col py-2 text-black whitespace-no-wrap bg-white border rounded shadow" aria-labelledby="more-dropdown-<?= $episode->id ?>" data-dropdown="menu" data-dropdown-placement="bottom-start" data-dropdown-offset-x="0" data-dropdown-offset-y="-24">' . - '<a class="px-4 py-1 hover:bg-gray-100" href="' . route_to( - 'episode-edit', - $podcast->id, - $episode->id, - ) . '">' . lang('Episode.edit') . '</a>' . - '<a class="px-4 py-1 hover:bg-gray-100" href="' . route_to( - 'embeddable-player-add', - $podcast->id, - $episode->id, - ) . '">' . lang( - 'Episode.embeddable_player.title', - ) . '</a>' . - '<a class="px-4 py-1 hover:bg-gray-100" href="' . route_to( - 'episode-persons-manage', - $podcast->id, - $episode->id, - ) . '">' . lang('Person.persons') . '</a>' . - '<a class="px-4 py-1 hover:bg-gray-100" href="' . route_to( - 'soundbites-edit', - $podcast->id, - $episode->id, - ) . '">' . lang('Episode.soundbites') . '</a>' . - '<a class="px-4 py-1 hover:bg-gray-100" href="' . route_to( - 'episode', - $podcast->handle, - $episode->slug, - ) . '">' . lang('Episode.go_to_page') . '</a>' . - '<a class="px-4 py-1 hover:bg-gray-100" href="' . route_to( - 'episode-delete', - $podcast->id, - $episode->id, - ) . '">' . lang('Episode.delete') . '</a>' . - '</nav>' . - '</div>'; + '<DropdownMenu id="more-dropdown-' . $episode->id . '-menu" labeledBy="more-dropdown-' . $episode->id . '" items="' . esc(json_encode([ + [ + 'type' => 'link', + 'title' => lang('Episode.edit'), + 'uri' => route_to('episode-edit', $podcast->id, $episode->id), + ], + [ + 'type' => 'link', + 'title' => lang('Episode.embeddable_player.title'), + 'uri' => route_to('embeddable-player-add', $podcast->id, $episode->id), + ], + [ + 'type' => 'link', + 'title' => lang('Person.persons'), + 'uri' => route_to('episode-persons-manage', $podcast->id, $episode->id), + ], + [ + 'type' => 'link', + 'title' => lang('Episode.soundbites'), + 'uri' => route_to('soundbites-edit', $podcast->id, $episode->id), + ], + [ + 'type' => 'link', + 'title' => lang('Episode.go_to_page'), + 'uri' => route_to('episode', $podcast->handle, $episode->slug), + ], + [ + 'type' => 'separator', + ], + [ + 'type' => 'link', + 'title' => lang('Episode.delete'), + 'uri' => route_to('episode-delete', $podcast->id, $episode->id), + 'class' => 'font-semibold text-red-600', + ], + ])) . '" />'; }, ], ], diff --git a/themes/cp_admin/podcast/latest_episodes.php b/themes/cp_admin/podcast/latest_episodes.php index 3a0cb19c46ecbbc3b350219e2ae2baea13623fce..2d795036e80f9e7c9a0c434634d3e0c87dae59dd 100644 --- a/themes/cp_admin/podcast/latest_episodes.php +++ b/themes/cp_admin/podcast/latest_episodes.php @@ -52,41 +52,42 @@ aria-haspopup="true" aria-expanded="false" ><?= icon('more') ?></button> - <nav - id="more-dropdown-<?= $episode->id ?>-menu" - class="z-50 flex flex-col py-2 text-black whitespace-no-wrap bg-white border rounded shadow" - aria-labelledby="more-dropdown-<?= $episode->id ?>" - data-dropdown="menu" - data-dropdown-placement="bottom"> - <a class="px-4 py-1 hover:bg-gray-100" href="<?= route_to( - 'episode-edit', - $podcast->id, - $episode->id, -) ?>"><?= lang('Episode.edit') ?></a> - <a class="px-4 py-1 hover:bg-gray-100" href="<?= route_to( - 'embeddable-player-add', - $podcast->id, - $episode->id, -) ?>"><?= lang( - 'Episode.embeddable_player.title', -) ?></a> - <a class="px-4 py-1 hover:bg-gray-100" href="<?= route_to( - 'episode-persons-manage', - $podcast->id, - $episode->id, -) ?>"><?= lang('Person.persons') ?></a> - <a class="px-4 py-1 hover:bg-gray-100" href="<?= route_to( - 'episode', - $podcast->handle, - $episode->slug, -) ?>"><?= lang('Episode.go_to_page') ?></a> - <hr class="my-2 border border-gray-100"> - <a class="px-4 py-1 font-semibold text-red-600 hover:bg-gray-100" href="<?= route_to( - 'episode-delete', - $podcast->id, - $episode->id, -) ?>"><?= lang('Episode.delete') ?></a> - </nav> + <DropdownMenu id="more-dropdown-<?= $episode->id ?>-menu" labeledBy="more-dropdown-<?= $episode->id ?>" items="<?= esc(json_encode([ + [ + 'type' => 'link', + 'title' => lang('Episode.edit'), + 'uri' => route_to('episode-edit', $podcast->id, $episode->id), + ], + [ + 'type' => 'link', + 'title' => lang('Episode.embeddable_player.title'), + 'uri' => route_to('embeddable-player-add', $podcast->id, $episode->id), + ], + [ + 'type' => 'link', + 'title' => lang('Person.persons'), + 'uri' => route_to('episode-persons-manage', $podcast->id, $episode->id), + ], + [ + 'type' => 'link', + 'title' => lang('Episode.soundbites'), + 'uri' => route_to('soundbites-edit', $podcast->id, $episode->id), + ], + [ + 'type' => 'link', + 'title' => lang('Episode.go_to_page'), + 'uri' => route_to('episode', $podcast->handle, $episode->slug), + ], + [ + 'type' => 'separator', + ], + [ + 'type' => 'link', + 'title' => lang('Episode.delete'), + 'uri' => route_to('episode-delete', $podcast->id, $episode->id), + 'class' => 'font-semibold text-red-600', + ], + ])) ?>" /> </div> </article> <?php endforeach; ?>