From 6b34617d07c70522cb941e96d91d9987493413eb Mon Sep 17 00:00:00 2001 From: Benjamin Bellamy Date: Mon, 7 Dec 2020 20:13:46 +0000 Subject: [PATCH] feat(rss): add soundbites according to the podcastindex specs Closes #83 --- app/Config/Routes.php | 23 ++ app/Controllers/Admin/Episode.php | 96 ++++++++- .../2020-06-05-180000_add_soundbites.php | 77 +++++++ app/Entities/Category.php | 1 + app/Entities/Episode.php | 30 +++ app/Entities/Soundbite.php | 39 ++++ app/Entities/User.php | 1 + app/Helpers/rss_helper.php | 13 ++ app/Language/en/Breadcrumb.php | 1 + app/Language/en/Episode.php | 19 ++ app/Language/fr/Breadcrumb.php | 1 + app/Language/fr/Episode.php | 20 ++ app/Models/SoundbiteModel.php | 97 +++++++++ app/Views/_assets/icons/bookmark.svg | 1 + app/Views/_assets/icons/play.svg | 1 + app/Views/_assets/icons/timer.svg | 1 + app/Views/_assets/modules/Charts.ts | 9 +- app/Views/_assets/modules/Soundbites.ts | 95 +++++++++ app/Views/_assets/soundbites.ts | 3 + app/Views/admin/_layout.php | 1 + app/Views/admin/episode/list.php | 17 +- app/Views/admin/episode/soundbites.php | 198 ++++++++++++++++++ app/Views/admin/episode/view.php | 53 ++++- app/Views/episode.php | 47 +++++ 24 files changed, 834 insertions(+), 10 deletions(-) create mode 100644 app/Database/Migrations/2020-06-05-180000_add_soundbites.php create mode 100644 app/Entities/Soundbite.php create mode 100644 app/Models/SoundbiteModel.php create mode 100644 app/Views/_assets/icons/bookmark.svg create mode 100644 app/Views/_assets/icons/play.svg create mode 100644 app/Views/_assets/icons/timer.svg create mode 100644 app/Views/_assets/modules/Soundbites.ts create mode 100644 app/Views/_assets/soundbites.ts create mode 100644 app/Views/admin/episode/soundbites.php diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 23c5e951..cfbc02c9 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -253,6 +253,29 @@ $routes->group( 'filter' => 'permission:podcast_episodes-edit', ] ); + $routes->get( + 'soundbites', + 'Episode::soundbitesEdit/$1/$2', + [ + 'as' => 'soundbites-edit', + 'filter' => 'permission:podcast_episodes-edit', + ] + ); + $routes->post( + 'soundbites', + 'Episode::soundbitesAttemptEdit/$1/$2', + [ + 'filter' => 'permission:podcast_episodes-edit', + ] + ); + $routes->add( + 'soundbites/(:num)/delete', + 'Episode::soundbiteDelete/$1/$2/$3', + [ + 'as' => 'soundbite-delete', + 'filter' => 'permission:podcast_episodes-edit', + ] + ); }); }); diff --git a/app/Controllers/Admin/Episode.php b/app/Controllers/Admin/Episode.php index 4f2531c8..f88c49b2 100644 --- a/app/Controllers/Admin/Episode.php +++ b/app/Controllers/Admin/Episode.php @@ -10,6 +10,7 @@ namespace App\Controllers\Admin; use App\Models\EpisodeModel; use App\Models\PodcastModel; +use App\Models\SoundbiteModel; use CodeIgniter\I18n\Time; class Episode extends BaseController @@ -24,6 +25,11 @@ class Episode extends BaseController */ protected $episode; + /** + * @var \App\Entities\Soundbite|null + */ + protected $soundbites; + public function _remap($method, ...$params) { $this->podcast = (new PodcastModel())->getPodcastById($params[0]); @@ -39,9 +45,12 @@ class Episode extends BaseController ) { throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound(); } + + unset($params[1]); + unset($params[0]); } - return $this->$method(); + return $this->$method(...$params); } public function list() @@ -316,4 +325,89 @@ class Episode extends BaseController return redirect()->route('episode-list', [$this->podcast->id]); } + + public function soundbitesEdit() + { + helper(['form']); + + $data = [ + 'podcast' => $this->podcast, + 'episode' => $this->episode, + ]; + + replace_breadcrumb_params([ + 0 => $this->podcast->title, + 1 => $this->episode->title, + ]); + return view('admin/episode/soundbites', $data); + } + + public function soundbitesAttemptEdit() + { + $soundbites_array = $this->request->getPost('soundbites_array'); + $rules = [ + 'soundbites_array.0.start_time' => + 'permit_empty|required_with[soundbites_array.0.duration]|decimal|greater_than_equal_to[0]', + 'soundbites_array.0.duration' => + 'permit_empty|required_with[soundbites_array.0.start_time]|decimal|greater_than_equal_to[0]', + ]; + foreach ($soundbites_array as $soundbite_id => $soundbite) { + $rules += [ + "soundbites_array.{$soundbite_id}.start_time" => 'required|decimal|greater_than_equal_to[0]', + "soundbites_array.{$soundbite_id}.duration" => 'required|decimal|greater_than_equal_to[0]', + ]; + } + if (!$this->validate($rules)) { + return redirect() + ->back() + ->withInput() + ->with('errors', $this->validator->getErrors()); + } + + foreach ($soundbites_array as $soundbite_id => $soundbite) { + if ( + !empty($soundbite['start_time']) && + !empty($soundbite['duration']) + ) { + $data = [ + 'podcast_id' => $this->podcast->id, + 'episode_id' => $this->episode->id, + 'start_time' => $soundbite['start_time'], + 'duration' => $soundbite['duration'], + 'label' => $soundbite['label'], + 'updated_by' => user()->id, + ]; + if ($soundbite_id == 0) { + $data += ['created_by' => user()->id]; + } else { + $data += ['id' => $soundbite_id]; + } + $soundbiteModel = new SoundbiteModel(); + if (!$soundbiteModel->save($data)) { + return redirect() + ->back() + ->withInput() + ->with('errors', $soundbiteModel->errors()); + } + } + } + return redirect()->route('soundbites-edit', [ + $this->podcast->id, + $this->episode->id, + ]); + } + + public function soundbiteDelete($soundbiteId) + { + (new SoundbiteModel())->deleteSoundbite( + $this->podcast->id, + $this->episode->id, + $soundbiteId + ); + + return redirect()->route('soundbites-edit', [ + $this->podcast->id, + $this->episode->id, + ]); + } } diff --git a/app/Database/Migrations/2020-06-05-180000_add_soundbites.php b/app/Database/Migrations/2020-06-05-180000_add_soundbites.php new file mode 100644 index 00000000..57af8f31 --- /dev/null +++ b/app/Database/Migrations/2020-06-05-180000_add_soundbites.php @@ -0,0 +1,77 @@ +forge->addField([ + 'id' => [ + 'type' => 'INT', + 'unsigned' => true, + 'auto_increment' => true, + ], + 'podcast_id' => [ + 'type' => 'INT', + 'unsigned' => true, + ], + 'episode_id' => [ + 'type' => 'INT', + 'unsigned' => true, + ], + 'start_time' => [ + 'type' => 'FLOAT', + ], + 'duration' => [ + 'type' => 'FLOAT', + ], + 'label' => [ + 'type' => 'VARCHAR', + 'constraint' => 128, + 'null' => true, + ], + 'created_by' => [ + 'type' => 'INT', + 'unsigned' => true, + ], + 'updated_by' => [ + 'type' => 'INT', + 'unsigned' => true, + ], + 'created_at' => [ + 'type' => 'DATETIME', + ], + 'updated_at' => [ + 'type' => 'DATETIME', + ], + 'deleted_at' => [ + 'type' => 'DATETIME', + 'null' => true, + ], + ]); + $this->forge->addKey('id', true); + $this->forge->addUniqueKey(['episode_id', 'start_time', 'duration']); + $this->forge->addForeignKey('podcast_id', 'podcasts', 'id'); + $this->forge->addForeignKey('episode_id', 'episodes', 'id'); + $this->forge->addForeignKey('created_by', 'users', 'id'); + $this->forge->addForeignKey('updated_by', 'users', 'id'); + $this->forge->createTable('soundbites'); + } + + public function down() + { + $this->forge->dropTable('soundbites'); + } +} diff --git a/app/Entities/Category.php b/app/Entities/Category.php index aa1e32d9..e233f5a5 100644 --- a/app/Entities/Category.php +++ b/app/Entities/Category.php @@ -19,6 +19,7 @@ class Category extends Entity protected $parent; protected $casts = [ + 'id' => 'integer', 'parent_id' => 'integer', 'code' => 'string', 'apple_category' => 'string', diff --git a/app/Entities/Episode.php b/app/Entities/Episode.php index b316e97e..5d240f9c 100644 --- a/app/Entities/Episode.php +++ b/app/Entities/Episode.php @@ -9,6 +9,7 @@ namespace App\Entities; use App\Models\PodcastModel; +use App\Models\SoundbiteModel; use CodeIgniter\Entity; use CodeIgniter\I18n\Time; use League\CommonMark\CommonMarkConverter; @@ -75,6 +76,11 @@ class Episode extends Entity */ protected $chapters_url; + /** + * @var \App\Entities\Soundbite[] + */ + protected $soundbites; + /** * Holds text only description, striped of any markdown or html special characters * @@ -95,6 +101,7 @@ class Episode extends Entity ]; protected $casts = [ + 'id' => 'integer', 'guid' => 'string', 'slug' => 'string', 'title' => 'string', @@ -348,6 +355,29 @@ class Episode extends Entity : null; } + /** + * Returns the episode’s soundbites + * + * @return \App\Entities\Episode[] + */ + public function getSoundbites() + { + if (empty($this->id)) { + throw new \RuntimeException( + 'Episode must be created before getting soundbites.' + ); + } + + if (empty($this->soundbites)) { + $this->soundbites = (new SoundbiteModel())->getEpisodeSoundbites( + $this->getPodcast()->id, + $this->id + ); + } + + return $this->soundbites; + } + public function getLink() { return base_url( diff --git a/app/Entities/Soundbite.php b/app/Entities/Soundbite.php new file mode 100644 index 00000000..33373c8b --- /dev/null +++ b/app/Entities/Soundbite.php @@ -0,0 +1,39 @@ + 'integer', + 'podcast_id' => 'integer', + 'episode_id' => 'integer', + 'start_time' => 'float', + 'duration' => 'float', + 'label' => '?string', + 'created_by' => 'integer', + 'updated_by' => 'integer', + ]; + + public function setCreatedBy(\App\Entities\User $user) + { + $this->attributes['created_by'] = $user->id; + + return $this; + } + + public function setUpdatedBy(\App\Entities\User $user) + { + $this->attributes['updated_by'] = $user->id; + + return $this; + } +} diff --git a/app/Entities/User.php b/app/Entities/User.php index 6a3e7a1f..2c94df54 100644 --- a/app/Entities/User.php +++ b/app/Entities/User.php @@ -29,6 +29,7 @@ class User extends \Myth\Auth\Entities\User * when they are accessed. */ protected $casts = [ + 'id' => 'integer', 'active' => 'boolean', 'force_pass_reset' => 'boolean', 'podcast_role' => '?string', diff --git a/app/Helpers/rss_helper.php b/app/Helpers/rss_helper.php index 88baff71..a00a2b5e 100644 --- a/app/Helpers/rss_helper.php +++ b/app/Helpers/rss_helper.php @@ -255,6 +255,19 @@ function get_rss_feed($podcast, $serviceSlug = '') $chaptersElement->addAttribute('type', 'application/json+chapters'); } + foreach ($episode->soundbites as $soundbite) { + $soundbiteElement = $item->addChild( + 'soundbite', + empty($soundbite->label) ? null : $soundbite->label, + $podcast_namespace + ); + $soundbiteElement->addAttribute( + 'start_time', + $soundbite->start_time + ); + $soundbiteElement->addAttribute('duration', $soundbite->duration); + } + $episode->is_blocked && $item->addChild('block', 'Yes', $itunes_namespace); } diff --git a/app/Language/en/Breadcrumb.php b/app/Language/en/Breadcrumb.php index 8fcfcbeb..27301ab7 100644 --- a/app/Language/en/Breadcrumb.php +++ b/app/Language/en/Breadcrumb.php @@ -30,4 +30,5 @@ return [ 'players' => 'players', 'listening-time' => 'listening time', 'time-periods' => 'time periods', + 'soundbites' => 'soundbites', ]; diff --git a/app/Language/en/Episode.php b/app/Language/en/Episode.php index ea045e49..94d175c9 100644 --- a/app/Language/en/Episode.php +++ b/app/Language/en/Episode.php @@ -82,4 +82,23 @@ return [ 'submit_create' => 'Create episode', 'submit_edit' => 'Save episode', ], + 'soundbites' => 'Soundbites', + 'soundbites_form' => [ + 'title' => 'Edit soundbites', + 'info_section_title' => 'Episode soundbites', + 'info_section_subtitle' => 'Add, edit or delete soundbites', + 'start_time' => 'Start', + 'start_time_hint' => + 'The first second of the soundbite, it can be a decimal number.', + 'duration' => 'Duration', + 'duration_hint' => + 'The duration of the soundbite (in seconds), it can be a decimal number.', + 'label' => 'Label', + 'label_hint' => 'Text that will be displayed.', + 'play' => 'Play soundbite', + 'delete' => 'Delete soundbite', + 'bookmark' => + 'Click while playing to get current position, click again to get duration.', + 'submit_edit' => 'Save all soundbites', + ], ]; diff --git a/app/Language/fr/Breadcrumb.php b/app/Language/fr/Breadcrumb.php index de8d5f80..71d8c331 100644 --- a/app/Language/fr/Breadcrumb.php +++ b/app/Language/fr/Breadcrumb.php @@ -30,4 +30,5 @@ return [ 'players' => 'lecteurs', 'listening-time' => 'drée d’écoute', 'time-periods' => 'périodes', + 'soundbites' => 'extraits sonores', ]; diff --git a/app/Language/fr/Episode.php b/app/Language/fr/Episode.php index 25911e2a..36d1daac 100644 --- a/app/Language/fr/Episode.php +++ b/app/Language/fr/Episode.php @@ -83,4 +83,24 @@ return [ 'submit_create' => 'Créer l’épisode', 'submit_edit' => 'Enregistrer l’épisode', ], + 'soundbites' => 'Extraits sonores', + 'soundbites_form' => [ + 'title' => 'Modifier les extraits sonores', + 'info_section_title' => 'Extraits sonores de l’épisode', + 'info_section_subtitle' => + 'Ajouter, modifier ou supprimer des extraits sonores', + 'start_time' => 'Début', + 'start_time_hint' => + 'La première seconde de l’extrait sonore, cela peut être un nombre décimal.', + 'duration' => 'Durée', + 'duration_hint' => + 'La durée de l’extrait sonore (en secondes), cela peut être un nombre décimal.', + 'label' => 'Libellé', + 'label_hint' => 'Texte qui sera affiché.', + 'play' => 'Écouter l’extrait sonore', + 'delete' => 'Supprimer l’extrait sonore', + 'bookmark' => + 'Cliquez pour récupérer la position actuelle, cliquez à nouveau pour récupérer la durée.', + 'submit_edit' => 'Enregistrer tous les extraits sonores', + ], ]; diff --git a/app/Models/SoundbiteModel.php b/app/Models/SoundbiteModel.php new file mode 100644 index 00000000..fa55d592 --- /dev/null +++ b/app/Models/SoundbiteModel.php @@ -0,0 +1,97 @@ +delete([ + 'podcast_id' => $podcastId, + 'episode_id' => $episodeId, + 'id' => $soundbiteId, + ]); + } + + /** + * Gets all soundbites for an episode + * + * @param int $podcastId + * @param int $episodeId + * + * @return \App\Entities\Soundbite[] + */ + public function getEpisodeSoundbites(int $podcastId, int $episodeId): array + { + if (!($found = cache("episode{$episodeId}_soundbites"))) { + $found = $this->where([ + 'episode_id' => $episodeId, + 'podcast_id' => $podcastId, + ]) + ->orderBy('start_time') + ->findAll(); + cache()->save("episode{$episodeId}_soundbites", $found, DECADE); + } + return $found; + } + + public function clearCache(array $data) + { + $episode = (new EpisodeModel())->find( + isset($data['data']) + ? $data['data']['episode_id'] + : $data['id']['episode_id'] + ); + + cache()->delete("episode{$episode->id}_soundbites"); + + // delete cache for rss feed + cache()->delete("podcast{$episode->id}_feed"); + foreach (\Opawg\UserAgentsPhp\UserAgentsRSS::$db as $service) { + cache()->delete( + "podcast{$episode->podcast->id}_feed_{$service['slug']}" + ); + } + + $supportedLocales = config('App')->supportedLocales; + foreach ($supportedLocales as $locale) { + cache()->delete( + "page_podcast{$episode->podcast->id}_episode{$episode->id}_{$locale}" + ); + } + return $data; + } +} diff --git a/app/Views/_assets/icons/bookmark.svg b/app/Views/_assets/icons/bookmark.svg new file mode 100644 index 00000000..f340d6ed --- /dev/null +++ b/app/Views/_assets/icons/bookmark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/Views/_assets/icons/play.svg b/app/Views/_assets/icons/play.svg new file mode 100644 index 00000000..4978d3d5 --- /dev/null +++ b/app/Views/_assets/icons/play.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/Views/_assets/icons/timer.svg b/app/Views/_assets/icons/timer.svg new file mode 100644 index 00000000..4f2136e6 --- /dev/null +++ b/app/Views/_assets/icons/timer.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/Views/_assets/modules/Charts.ts b/app/Views/_assets/modules/Charts.ts index 777a0d8e..08014db1 100644 --- a/app/Views/_assets/modules/Charts.ts +++ b/app/Views/_assets/modules/Charts.ts @@ -3,7 +3,7 @@ import am4geodata_worldLow from "@amcharts/amcharts4-geodata/worldLow"; import * as am4charts from "@amcharts/amcharts4/charts"; import * as am4core from "@amcharts/amcharts4/core"; import * as am4maps from "@amcharts/amcharts4/maps"; -import * as am4plugins_sliceGrouper from "@amcharts/amcharts4/plugins/sliceGrouper"; +import * as am4plugins_sliceGrouper from "@amcharts/amcharts4/plugins/sliceGrouper"; import am4themes_material from "@amcharts/amcharts4/themes/material"; const drawPieChart = (chartDivId: string, dataUrl: string | null): void => { @@ -21,7 +21,9 @@ const drawPieChart = (chartDivId: string, dataUrl: string | null): void => { chart.dataSource.parser.options.emptyAs = 0; // Add and configure Series const pieSeries = chart.series.push(new am4charts.PieSeries()); - const grouper = pieSeries.plugins.push(new am4plugins_sliceGrouper.SliceGrouper()); + const grouper = pieSeries.plugins.push( + new am4plugins_sliceGrouper.SliceGrouper() + ); grouper.limit = 9; grouper.groupName = "- Other -"; grouper.clickBehavior = "break"; @@ -95,13 +97,12 @@ const drawBarChart = (chartDivId: string, dataUrl: string | null): void => { series.dataFields.categoryX = "labels"; series.name = "Hits"; series.columns.template.tooltipText = "{valueY} hits"; - series.columns.template.fillOpacity = .8; + series.columns.template.fillOpacity = 0.8; const columnTemplate = series.columns.template; columnTemplate.strokeWidth = 2; columnTemplate.strokeOpacity = 1; }; - const drawXYDurationChart = ( chartDivId: string, dataUrl: string | null diff --git a/app/Views/_assets/modules/Soundbites.ts b/app/Views/_assets/modules/Soundbites.ts new file mode 100644 index 00000000..b8f7ffbf --- /dev/null +++ b/app/Views/_assets/modules/Soundbites.ts @@ -0,0 +1,95 @@ +let timeout: number | null = null; + +const playSoundbite = ( + audioPlayer: HTMLAudioElement, + startTime: number, + duration: number +): void => { + audioPlayer.currentTime = startTime; + if (duration > 0) { + audioPlayer.play(); + if (timeout) { + clearTimeout(timeout); + timeout = null; + } + timeout = window.setTimeout(() => { + audioPlayer.pause(); + timeout = null; + }, duration * 1000); + } +}; + +const Soundbites = (): void => { + const audioPlayer: HTMLAudioElement | null = document.querySelector("audio"); + + if (audioPlayer) { + const soundbiteButton: HTMLButtonElement | null = document.querySelector( + "button[data-type='get-soundbite']" + ); + if (soundbiteButton) { + const startTimeField: HTMLInputElement | null = document.querySelector( + `input[name="${soundbiteButton.dataset.startTimeFieldName}"]` + ); + const durationField: HTMLInputElement | null = document.querySelector( + `input[name="${soundbiteButton.dataset.durationFieldName}"]` + ); + + if (startTimeField && durationField) { + soundbiteButton.addEventListener("click", () => { + if (startTimeField.value === "") { + startTimeField.value = ( + Math.round(audioPlayer.currentTime * 100) / 100 + ).toString(); + } else { + durationField.value = ( + Math.round( + (audioPlayer.currentTime - Number(startTimeField.value)) * 100 + ) / 100 + ).toString(); + } + }); + } + } + + const soundbitePlayButtons: NodeListOf< + HTMLButtonElement + > | null = document.querySelectorAll("button[data-type='play-soundbite']"); + if (soundbitePlayButtons) { + for (let i = 0; i < soundbitePlayButtons.length; i++) { + const soundbitePlayButton: HTMLButtonElement = soundbitePlayButtons[i]; + soundbitePlayButton.addEventListener("click", () => { + playSoundbite( + audioPlayer, + Number(soundbitePlayButton.dataset.soundbiteStartTime), + Number(soundbitePlayButton.dataset.soundbiteDuration) + ); + }); + } + } + + const inputFields: NodeListOf< + HTMLInputElement + > | null = document.querySelectorAll("input[data-type='soundbite-field']"); + if (inputFields) { + for (let i = 0; i < inputFields.length; i++) { + const inputField: HTMLInputElement = inputFields[i]; + const soundbitePlayButton: HTMLButtonElement | null = document.querySelector( + `button[data-type="play-soundbite"][data-soundbite-id="${inputField.dataset.soundbiteId}"]` + ); + if (soundbitePlayButton) { + if (inputField.dataset.fieldType == "start-time") { + inputField.addEventListener("input", () => { + soundbitePlayButton.dataset.soundbiteStartTime = inputField.value; + }); + } else if (inputField.dataset.fieldType == "duration") { + inputField.addEventListener("input", () => { + soundbitePlayButton.dataset.soundbiteDuration = inputField.value; + }); + } + } + } + } + } +}; + +export default Soundbites; diff --git a/app/Views/_assets/soundbites.ts b/app/Views/_assets/soundbites.ts new file mode 100644 index 00000000..fa8b6be1 --- /dev/null +++ b/app/Views/_assets/soundbites.ts @@ -0,0 +1,3 @@ +import Soundbites from "./modules/Soundbites"; + +Soundbites(); diff --git a/app/Views/admin/_layout.php b/app/Views/admin/_layout.php index 6a8d1706..3f1289cc 100644 --- a/app/Views/admin/_layout.php +++ b/app/Views/admin/_layout.php @@ -10,6 +10,7 @@ + diff --git a/app/Views/admin/episode/list.php b/app/Views/admin/episode/list.php index dff7ceff..da3170e9 100644 --- a/app/Views/admin/episode/list.php +++ b/app/Views/admin/episode/list.php @@ -11,10 +11,12 @@ endSection() ?> section('headerRight') ?> -id), [ - 'variant' => 'primary', - 'iconLeft' => 'add', -]) ?> +id), + + ['variant' => 'primary', 'iconLeft' => 'add'] +) ?> endSection() ?> @@ -59,6 +61,13 @@ $podcast->id, $episode->id ) ?>"> + + + + + + + + + + + + soundbites as $soundbite): ?> + + "soundbites_array[{$soundbite->id}][start_time]", + 'name' => "soundbites_array[{$soundbite->id}][start_time]", + 'class' => 'form-input w-full border-none text-center', + 'value' => $soundbite->start_time, + 'data-type' => 'soundbite-field', + 'data-field-type' => 'start-time', + 'data-soundbite-id' => $soundbite->id, + 'required' => 'required', + 'min' => '0', + ] + ) ?> + "soundbites_array[{$soundbite->id}][duration]", + 'name' => "soundbites_array[{$soundbite->id}][duration]", + 'class' => 'form-input w-full border-none text-center', + 'value' => $soundbite->duration, + 'data-type' => 'soundbite-field', + 'data-field-type' => 'duration', + 'data-soundbite-id' => $soundbite->id, + 'required' => 'required', + 'min' => '0', + ] + ) ?> + "soundbites_array[{$soundbite->id}][label]", + 'name' => "soundbites_array[{$soundbite->id}][label]", + 'class' => 'form-input w-full border-none', + 'value' => $soundbite->label, + ] + ) ?> + 'primary'], + [ + 'class' => 'mb-1 mr-1', + 'data-type' => 'play-soundbite', + 'data-soundbite-id' => $soundbite->id, + 'data-soundbite-start-time' => $soundbite->start_time, + 'data-soundbite-duration' => $soundbite->duration, + ] + ) ?> + id, + $episode->id, + $soundbite->id + ), + ['variant' => 'danger'], + [] + ) ?> + + + + + 'soundbites_array[0][start_time]', + 'name' => 'soundbites_array[0][start_time]', + 'class' => 'form-input w-full border-none text-center', + 'value' => old('start_time'), + 'data-soundbite-id' => '0', + 'data-type' => 'soundbite-field', + 'data-field-type' => 'start-time', + 'min' => '0', + ] + ) ?> + 'soundbites_array[0][duration]', + 'name' => 'soundbites_array[0][duration]', + 'class' => 'form-input w-full border-none text-center', + 'value' => old('duration'), + 'data-soundbite-id' => '0', + 'data-type' => 'soundbite-field', + 'data-field-type' => 'duration', + 'min' => '0', + ] + ) ?> + 'soundbites_array[0][label]', + 'name' => 'soundbites_array[0][label]', + 'class' => 'form-input w-full border-none', + 'value' => old('label'), + ] + ) ?> + 'primary'], + [ + 'data-type' => 'play-soundbite', + 'data-soundbite-id' => 0, + 'data-soundbite-start-time' => 0, + 'data-soundbite-duration' => 0, + ] + ) ?> + + + + + + + 'info'], + [ + 'data-type' => 'get-soundbite', + 'data-start-time-field-name' => + 'soundbites_array[0][start_time]', + 'data-duration-field-name' => 'soundbites_array[0][duration]', + ] + ) ?> + + + + + + + 'primary'], + ['type' => 'submit', 'class' => 'self-end'] +) ?> + + + + +endSection() ?> diff --git a/app/Views/admin/episode/view.php b/app/Views/admin/episode/view.php index d36151f1..74744e47 100644 --- a/app/Views/admin/episode/view.php +++ b/app/Views/admin/episode/view.php @@ -22,7 +22,7 @@ alt="Episode cover" class="object-cover w-full" /> -