mirror of
https://code.castopod.org/adaures/castopod
synced 2025-04-19 13:01:19 +00:00
feat(soundbites): add soundbite list and creation forms with audio-clipper component
This commit is contained in:
parent
602654b99b
commit
de19317138
@ -39,10 +39,9 @@ class AddClips extends Migration
|
||||
'type' => 'DECIMAL(7,3)',
|
||||
'unsigned' => true,
|
||||
],
|
||||
'label' => [
|
||||
'title' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 128,
|
||||
'null' => true,
|
||||
],
|
||||
'type' => [
|
||||
'type' => 'ENUM',
|
||||
|
@ -29,7 +29,7 @@ use Modules\Auth\Entities\User;
|
||||
* @property Podcast $podcast
|
||||
* @property int $episode_id
|
||||
* @property Episode $episode
|
||||
* @property string $label
|
||||
* @property string $title
|
||||
* @property double $start_time
|
||||
* @property double $end_time
|
||||
* @property double $duration
|
||||
@ -68,7 +68,7 @@ class BaseClip extends Entity
|
||||
'id' => 'integer',
|
||||
'podcast_id' => 'integer',
|
||||
'episode_id' => 'integer',
|
||||
'label' => 'string',
|
||||
'title' => 'string',
|
||||
'start_time' => 'double',
|
||||
'duration' => 'double',
|
||||
'type' => 'string',
|
||||
|
@ -255,7 +255,7 @@ if (! function_exists('get_rss_feed')) {
|
||||
$comments->addAttribute('uri', url_to('episode-comments', $podcast->handle, $episode->slug));
|
||||
$comments->addAttribute('contentType', 'application/podcast-activity+json');
|
||||
|
||||
if ($episode->transcript->file_url !== '') {
|
||||
if ($episode->transcript !== null) {
|
||||
$transcriptElement = $item->addChild('transcript', null, $podcastNamespace);
|
||||
$transcriptElement->addAttribute('url', $episode->transcript->file_url);
|
||||
$transcriptElement->addAttribute(
|
||||
@ -275,7 +275,7 @@ if (! function_exists('get_rss_feed')) {
|
||||
|
||||
foreach ($episode->soundbites as $soundbite) {
|
||||
// TODO: differentiate video from soundbites?
|
||||
$soundbiteElement = $item->addChild('soundbite', $soundbite->label, $podcastNamespace);
|
||||
$soundbiteElement = $item->addChild('soundbite', $soundbite->title, $podcastNamespace);
|
||||
$soundbiteElement->addAttribute('start_time', (string) $soundbite->start_time);
|
||||
$soundbiteElement->addAttribute('duration', (string) $soundbite->duration);
|
||||
}
|
||||
|
@ -39,7 +39,7 @@ class ClipModel extends Model
|
||||
'id',
|
||||
'podcast_id',
|
||||
'episode_id',
|
||||
'label',
|
||||
'title',
|
||||
'start_time',
|
||||
'duration',
|
||||
'type',
|
||||
@ -89,33 +89,6 @@ class ClipModel extends Model
|
||||
parent::__construct($db, $validation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all clips for an episode
|
||||
*
|
||||
* @return Soundbite[]
|
||||
*/
|
||||
public function getEpisodeSoundbites(int $podcastId, int $episodeId): array
|
||||
{
|
||||
$cacheName = "podcast#{$podcastId}_episode#{$episodeId}_soundbites";
|
||||
if (! ($found = cache($cacheName))) {
|
||||
$found = $this->where([
|
||||
'episode_id' => $episodeId,
|
||||
'podcast_id' => $podcastId,
|
||||
'type' => 'audio',
|
||||
])
|
||||
->orderBy('start_time')
|
||||
->findAll();
|
||||
|
||||
foreach ($found as $key => $soundbite) {
|
||||
$found[$key] = new Soundbite($soundbite->toArray());
|
||||
}
|
||||
|
||||
cache()
|
||||
->save($cacheName, $found, DECADE);
|
||||
}
|
||||
return $found;
|
||||
}
|
||||
|
||||
public function getVideoClipById(int $videoClipId): ?VideoClip
|
||||
{
|
||||
$cacheName = "video-clip#{$videoClipId}";
|
||||
@ -184,6 +157,53 @@ class ClipModel extends Model
|
||||
return $found;
|
||||
}
|
||||
|
||||
public function getSoundbiteById(int $soundbiteId): ?Soundbite
|
||||
{
|
||||
$cacheName = "soundbite#{$soundbiteId}";
|
||||
if (! ($found = cache($cacheName))) {
|
||||
$clip = $this->find($soundbiteId);
|
||||
|
||||
if ($clip === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// @phpstan-ignore-next-line
|
||||
$found = new Soundbite($clip->toArray());
|
||||
|
||||
cache()
|
||||
->save($cacheName, $found, DECADE);
|
||||
}
|
||||
|
||||
return $found;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all clips for an episode
|
||||
*
|
||||
* @return Soundbite[]
|
||||
*/
|
||||
public function getEpisodeSoundbites(int $podcastId, int $episodeId): array
|
||||
{
|
||||
$cacheName = "podcast#{$podcastId}_episode#{$episodeId}_soundbites";
|
||||
if (! ($found = cache($cacheName))) {
|
||||
$found = $this->where([
|
||||
'episode_id' => $episodeId,
|
||||
'podcast_id' => $podcastId,
|
||||
'type' => 'audio',
|
||||
])
|
||||
->orderBy('start_time')
|
||||
->findAll();
|
||||
|
||||
foreach ($found as $key => $soundbite) {
|
||||
$found[$key] = new Soundbite($soundbite->toArray());
|
||||
}
|
||||
|
||||
cache()
|
||||
->save($cacheName, $found, DECADE);
|
||||
}
|
||||
return $found;
|
||||
}
|
||||
|
||||
public function deleteSoundbite(int $podcastId, int $episodeId, int $clipId): BaseResult | bool
|
||||
{
|
||||
cache()
|
||||
|
@ -10,11 +10,11 @@ import "./modules/markdown-preview";
|
||||
import "./modules/markdown-write-preview";
|
||||
import MultiSelect from "./modules/MultiSelect";
|
||||
import "./modules/permalink-edit";
|
||||
import "./modules/play-soundbite";
|
||||
import PublishMessageWarning from "./modules/PublishMessageWarning";
|
||||
import Select from "./modules/Select";
|
||||
import SidebarToggler from "./modules/SidebarToggler";
|
||||
import Slugify from "./modules/Slugify";
|
||||
import Soundbites from "./modules/Soundbites";
|
||||
import ThemePicker from "./modules/ThemePicker";
|
||||
import Time from "./modules/Time";
|
||||
import Tooltip from "./modules/Tooltip";
|
||||
@ -31,7 +31,6 @@ SidebarToggler();
|
||||
ClientTimezone();
|
||||
DateTimePicker();
|
||||
Time();
|
||||
Soundbites();
|
||||
Clipboard();
|
||||
ThemePicker();
|
||||
PublishMessageWarning();
|
||||
|
@ -1,87 +0,0 @@
|
||||
/**
|
||||
* TODO: refactor file
|
||||
*/
|
||||
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", () => {
|
||||
// get values from inputs to play soundbite
|
||||
const startTime: HTMLInputElement | null | undefined =
|
||||
soundbitePlayButton.parentElement?.parentElement?.querySelector(
|
||||
'input[data-field-type="start_time"]'
|
||||
);
|
||||
const duration: HTMLInputElement | null | undefined =
|
||||
soundbitePlayButton.parentElement?.parentElement?.querySelector(
|
||||
'input[data-field-type="duration"]'
|
||||
);
|
||||
|
||||
if (startTime && duration) {
|
||||
playSoundbite(
|
||||
audioPlayer,
|
||||
parseFloat(startTime.value),
|
||||
parseFloat(duration.value)
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default Soundbites;
|
198
app/Resources/js/modules/play-soundbite.ts
Normal file
198
app/Resources/js/modules/play-soundbite.ts
Normal file
@ -0,0 +1,198 @@
|
||||
import { css, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
|
||||
@customElement("play-soundbite")
|
||||
export class PlaySoundbite extends LitElement {
|
||||
@property({ attribute: "audio-src" })
|
||||
audioSrc!: string;
|
||||
|
||||
@property({ type: Number, attribute: "start-time" })
|
||||
startTime!: number;
|
||||
|
||||
@property({ type: Number })
|
||||
duration!: number;
|
||||
|
||||
@property({ attribute: "play-label" })
|
||||
playLabel!: string;
|
||||
|
||||
@property({ attribute: "playing-label" })
|
||||
playingLabel!: string;
|
||||
|
||||
@state()
|
||||
_audio: HTMLAudioElement | null = null;
|
||||
|
||||
@state()
|
||||
_isPlaying = false;
|
||||
|
||||
@state()
|
||||
_isLoading = false;
|
||||
|
||||
_audioEvents = [
|
||||
{
|
||||
name: "play",
|
||||
onEvent: () => {
|
||||
this._isPlaying = true;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "pause",
|
||||
onEvent: () => {
|
||||
this._isPlaying = false;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "timeupdate",
|
||||
onEvent: () => {
|
||||
if (this._audio) {
|
||||
console.log(
|
||||
this._audio.currentTime,
|
||||
this.startTime,
|
||||
this.startTime + this.duration
|
||||
);
|
||||
if (this._audio.currentTime < this.startTime) {
|
||||
this._isLoading = true;
|
||||
this._audio.currentTime = this.startTime;
|
||||
} else if (this._audio.currentTime > this.startTime + this.duration) {
|
||||
this.stopSoundbite();
|
||||
} else {
|
||||
this._isLoading = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
playSoundbite() {
|
||||
if (this._audio === null) {
|
||||
this._audio = new Audio(this.audioSrc);
|
||||
for (const event of this._audioEvents) {
|
||||
this._audio.addEventListener(event.name, event.onEvent);
|
||||
}
|
||||
}
|
||||
|
||||
this._audio.currentTime = this.startTime;
|
||||
this._audio.play();
|
||||
}
|
||||
|
||||
stopSoundbite() {
|
||||
if (this._audio !== null) {
|
||||
this._audio.pause();
|
||||
this._audio.currentTime = this.startTime;
|
||||
}
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
if (this._audio) {
|
||||
for (const event of this._audioEvents) {
|
||||
this._audio.removeEventListener(event.name, event.onEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
button {
|
||||
background-color: hsl(var(--color-accent-base));
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
border: 2px solid transparent;
|
||||
border-radius: 9999px;
|
||||
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: hsl(var(--color-accent-hover));
|
||||
}
|
||||
|
||||
button:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px hsl(var(--color-background-base)),
|
||||
0 0 0 4px hsl(var(--color-accent-base));
|
||||
}
|
||||
|
||||
button.playing {
|
||||
background-color: hsl(var(--color-background-base));
|
||||
border: 2px solid hsl(var(--color-accent-base));
|
||||
}
|
||||
|
||||
button.playing:hover {
|
||||
background-color: hsl(var(--color-background-elevated));
|
||||
}
|
||||
|
||||
button.playing svg {
|
||||
color: hsl(var(--color-accent-base));
|
||||
}
|
||||
|
||||
svg {
|
||||
color: hsl(var(--color-accent-contrast));
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-spin {
|
||||
animation: spin 3s linear infinite;
|
||||
}
|
||||
`;
|
||||
|
||||
render(): TemplateResult<1> {
|
||||
return html`<button
|
||||
@click="${this._isPlaying ? this.stopSoundbite : this.playSoundbite}"
|
||||
title="${this._isPlaying ? this.playingLabel : this.playLabel}"
|
||||
>
|
||||
${this._isLoading
|
||||
? html`<svg
|
||||
class="animate-spin"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle
|
||||
opacity="0.25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
stroke-width="4"
|
||||
></circle>
|
||||
<path
|
||||
opacity="0.75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
></path>
|
||||
</svg>`
|
||||
: this._isPlaying
|
||||
? html`<svg
|
||||
class="animate-spin"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
width="1em"
|
||||
height="1em"
|
||||
>
|
||||
<g>
|
||||
<path fill="none" d="M0 0h24v24H0z" />
|
||||
<path
|
||||
d="M13 9.17A3 3 0 1 0 15 12V2.458c4.057 1.274 7 5.064 7 9.542 0 5.523-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2c.337 0 .671.017 1 .05v7.12z"
|
||||
/>
|
||||
</g>
|
||||
</svg>`
|
||||
: html`<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
width="1em"
|
||||
height="1em"
|
||||
>
|
||||
<path fill="none" d="M0 0h24v24H0z" />
|
||||
<path
|
||||
d="M7.752 5.439l10.508 6.13a.5.5 0 0 1 0 .863l-10.508 6.13A.5.5 0 0 1 7 18.128V5.871a.5.5 0 0 1 .752-.432z"
|
||||
/>
|
||||
</svg>`}
|
||||
</button>`;
|
||||
}
|
||||
}
|
@ -334,24 +334,33 @@ $routes->group(
|
||||
);
|
||||
$routes->get(
|
||||
'soundbites',
|
||||
'EpisodeController::soundbitesEdit/$1/$2',
|
||||
'SoundbiteController::list/$1/$2',
|
||||
[
|
||||
'as' => 'soundbites-edit',
|
||||
'as' => 'soundbites-list',
|
||||
'filter' => 'permission:podcast_episodes-edit',
|
||||
],
|
||||
);
|
||||
$routes->get(
|
||||
'soundbites/new',
|
||||
'SoundbiteController::create/$1/$2',
|
||||
[
|
||||
'as' => 'soundbites-create',
|
||||
'filter' => 'permission:podcast_episodes-edit',
|
||||
],
|
||||
);
|
||||
$routes->post(
|
||||
'soundbites',
|
||||
'EpisodeController::soundbitesAttemptEdit/$1/$2',
|
||||
'soundbites/new',
|
||||
'SoundbiteController::attemptCreate/$1/$2',
|
||||
[
|
||||
'as' => 'soundbites-create',
|
||||
'filter' => 'permission:podcast_episodes-edit',
|
||||
],
|
||||
);
|
||||
$routes->get(
|
||||
'soundbites/(:num)/delete',
|
||||
'EpisodeController::soundbiteDelete/$1/$2/$3',
|
||||
'SoundbiteController::delete/$1/$2/$3',
|
||||
[
|
||||
'as' => 'soundbite-delete',
|
||||
'as' => 'soundbites-delete',
|
||||
'filter' => 'permission:podcast_episodes-edit',
|
||||
],
|
||||
);
|
||||
|
@ -15,7 +15,6 @@ use App\Entities\EpisodeComment;
|
||||
use App\Entities\Location;
|
||||
use App\Entities\Podcast;
|
||||
use App\Entities\Post;
|
||||
use App\Models\ClipModel;
|
||||
use App\Models\EpisodeCommentModel;
|
||||
use App\Models\EpisodeModel;
|
||||
use App\Models\MediaModel;
|
||||
@ -719,83 +718,6 @@ class EpisodeController extends BaseController
|
||||
return redirect()->route('episode-list', [$this->podcast->id]);
|
||||
}
|
||||
|
||||
public function soundbitesEdit(): string
|
||||
{
|
||||
helper(['form']);
|
||||
|
||||
$data = [
|
||||
'podcast' => $this->podcast,
|
||||
'episode' => $this->episode,
|
||||
];
|
||||
|
||||
replace_breadcrumb_params([
|
||||
0 => $this->podcast->title,
|
||||
1 => $this->episode->title,
|
||||
]);
|
||||
return view('episode/soundbites', $data);
|
||||
}
|
||||
|
||||
public function soundbitesAttemptEdit(): RedirectResponse
|
||||
{
|
||||
$soundbites = $this->request->getPost('soundbites');
|
||||
$rules = [
|
||||
'soundbites.0.start_time' =>
|
||||
'permit_empty|required_with[soundbites.0.duration]|decimal|greater_than_equal_to[0]',
|
||||
'soundbites.0.duration' =>
|
||||
'permit_empty|required_with[soundbites.0.start_time]|decimal|greater_than_equal_to[0]',
|
||||
];
|
||||
foreach (array_keys($soundbites) as $soundbite_id) {
|
||||
$rules += [
|
||||
"soundbites.{$soundbite_id}.start_time" => 'required|decimal|greater_than_equal_to[0]',
|
||||
"soundbites.{$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 as $soundbite_id => $soundbite) {
|
||||
$data = [
|
||||
'podcast_id' => $this->podcast->id,
|
||||
'episode_id' => $this->episode->id,
|
||||
'start_time' => (float) $soundbite['start_time'],
|
||||
'duration' => (float) $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(string $clipId): RedirectResponse
|
||||
{
|
||||
(new ClipModel())->deleteClip($this->podcast->id, $this->episode->id, (int) $clipId);
|
||||
|
||||
return redirect()->route('clips-edit', [$this->podcast->id, $this->episode->id]);
|
||||
}
|
||||
|
||||
public function embed(): string
|
||||
{
|
||||
helper(['form']);
|
||||
|
163
modules/Admin/Controllers/SoundbiteController.php
Normal file
163
modules/Admin/Controllers/SoundbiteController.php
Normal file
@ -0,0 +1,163 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @copyright 2020 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
namespace Modules\Admin\Controllers;
|
||||
|
||||
use App\Entities\Clip\Soundbite;
|
||||
use App\Entities\Episode;
|
||||
use App\Entities\Podcast;
|
||||
use App\Models\ClipModel;
|
||||
use App\Models\EpisodeModel;
|
||||
use App\Models\MediaModel;
|
||||
use App\Models\PodcastModel;
|
||||
use CodeIgniter\Exceptions\PageNotFoundException;
|
||||
use CodeIgniter\HTTP\RedirectResponse;
|
||||
|
||||
class SoundbiteController extends BaseController
|
||||
{
|
||||
protected Podcast $podcast;
|
||||
|
||||
protected Episode $episode;
|
||||
|
||||
public function _remap(string $method, string ...$params): mixed
|
||||
{
|
||||
if (
|
||||
($podcast = (new PodcastModel())->getPodcastById((int) $params[0])) === null
|
||||
) {
|
||||
throw PageNotFoundException::forPageNotFound();
|
||||
}
|
||||
|
||||
$this->podcast = $podcast;
|
||||
|
||||
if (count($params) > 1) {
|
||||
if (
|
||||
! ($episode = (new EpisodeModel())
|
||||
->where([
|
||||
'id' => $params[1],
|
||||
'podcast_id' => $params[0],
|
||||
])
|
||||
->first())
|
||||
) {
|
||||
throw PageNotFoundException::forPageNotFound();
|
||||
}
|
||||
|
||||
$this->episode = $episode;
|
||||
|
||||
unset($params[1]);
|
||||
unset($params[0]);
|
||||
}
|
||||
|
||||
return $this->{$method}(...$params);
|
||||
}
|
||||
|
||||
public function list(): string
|
||||
{
|
||||
$soundbitesBuilder = (new ClipModel('audio'))
|
||||
->where([
|
||||
'podcast_id' => $this->podcast->id,
|
||||
'episode_id' => $this->episode->id,
|
||||
'type' => 'audio',
|
||||
])
|
||||
->orderBy('created_at', 'desc');
|
||||
|
||||
$soundbites = $soundbitesBuilder->paginate(10);
|
||||
|
||||
$data = [
|
||||
'podcast' => $this->podcast,
|
||||
'episode' => $this->episode,
|
||||
'soundbites' => $soundbites,
|
||||
'pager' => $soundbitesBuilder->pager,
|
||||
];
|
||||
|
||||
replace_breadcrumb_params([
|
||||
0 => $this->podcast->title,
|
||||
1 => $this->episode->title,
|
||||
]);
|
||||
return view('episode/soundbites_list', $data);
|
||||
}
|
||||
|
||||
public function create(): string
|
||||
{
|
||||
helper(['form']);
|
||||
|
||||
$data = [
|
||||
'podcast' => $this->podcast,
|
||||
'episode' => $this->episode,
|
||||
];
|
||||
|
||||
replace_breadcrumb_params([
|
||||
0 => $this->podcast->title,
|
||||
1 => $this->episode->title,
|
||||
]);
|
||||
return view('episode/soundbites_new', $data);
|
||||
}
|
||||
|
||||
public function attemptCreate(): RedirectResponse
|
||||
{
|
||||
$rules = [
|
||||
'title' => 'required',
|
||||
'start_time' => 'required|greater_than_equal_to[0]',
|
||||
'duration' => 'required|greater_than[0]',
|
||||
];
|
||||
|
||||
if (! $this->validate($rules)) {
|
||||
return redirect()
|
||||
->back()
|
||||
->withInput()
|
||||
->with('errors', $this->validator->getErrors());
|
||||
}
|
||||
|
||||
$newSoundbite = new Soundbite([
|
||||
'title' => $this->request->getPost('title'),
|
||||
'start_time' => (float) $this->request->getPost('start_time'),
|
||||
'duration' => (float) $this->request->getPost('duration',),
|
||||
'type' => 'audio',
|
||||
'status' => '',
|
||||
'podcast_id' => $this->podcast->id,
|
||||
'episode_id' => $this->episode->id,
|
||||
'created_by' => user_id(),
|
||||
'updated_by' => user_id(),
|
||||
]);
|
||||
|
||||
$clipModel = new ClipModel('audio');
|
||||
if (! $clipModel->save($newSoundbite)) {
|
||||
return redirect()
|
||||
->back()
|
||||
->withInput()
|
||||
->with('errors', $clipModel->errors());
|
||||
}
|
||||
|
||||
return redirect()->route('soundbites-list', [$this->podcast->id, $this->episode->id]);
|
||||
}
|
||||
|
||||
public function delete(string $soundbiteId): RedirectResponse
|
||||
{
|
||||
$soundbite = (new ClipModel())->getSoundbiteById((int) $soundbiteId);
|
||||
|
||||
if ($soundbite === null) {
|
||||
throw PageNotFoundException::forPageNotFound();
|
||||
}
|
||||
|
||||
if ($soundbite->media === null) {
|
||||
// delete Clip directly
|
||||
(new ClipModel())->delete($soundbite->id);
|
||||
} else {
|
||||
$mediaModel = new MediaModel();
|
||||
if (! $mediaModel->deleteMedia($soundbite->media)) {
|
||||
return redirect()
|
||||
->back()
|
||||
->withInput()
|
||||
->with('errors', $mediaModel->errors());
|
||||
}
|
||||
}
|
||||
|
||||
return redirect()->route('soundbites-list', [$this->podcast->id, $this->episode->id]);
|
||||
}
|
||||
}
|
@ -101,7 +101,7 @@ class VideoClipsController extends BaseController
|
||||
replace_breadcrumb_params([
|
||||
0 => $this->podcast->title,
|
||||
1 => $this->episode->title,
|
||||
2 => $videoClip->label,
|
||||
2 => $videoClip->title,
|
||||
]);
|
||||
return view('episode/video_clip', $data);
|
||||
}
|
||||
@ -140,8 +140,8 @@ class VideoClipsController extends BaseController
|
||||
public function attemptCreate(): RedirectResponse
|
||||
{
|
||||
$rules = [
|
||||
'label' => 'required',
|
||||
'start_time' => 'required|numeric',
|
||||
'title' => 'required',
|
||||
'start_time' => 'required|greater_than_equal_to[0]',
|
||||
'duration' => 'required|greater_than[0]',
|
||||
'format' => 'required|in_list[' . implode(',', array_keys(config('MediaClipper')->formats)) . ']',
|
||||
'theme' => 'required|in_list[' . implode(',', array_keys(config('Colors')->themes)) . ']',
|
||||
@ -163,7 +163,7 @@ class VideoClipsController extends BaseController
|
||||
];
|
||||
|
||||
$videoClip = new VideoClip([
|
||||
'label' => $this->request->getPost('label'),
|
||||
'title' => $this->request->getPost('title'),
|
||||
'start_time' => (float) $this->request->getPost('start_time'),
|
||||
'duration' => (float) $this->request->getPost('duration',),
|
||||
'theme' => $theme,
|
||||
|
@ -144,25 +144,6 @@ return [
|
||||
'understand' => 'I understand, I want to delete the episode',
|
||||
'submit' => 'Delete',
|
||||
],
|
||||
'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' => 'Save soundbites',
|
||||
],
|
||||
'embed' => [
|
||||
'title' => 'Embeddable player',
|
||||
'label' =>
|
||||
|
@ -16,7 +16,8 @@ return [
|
||||
'episode-persons-manage' => 'Manage persons',
|
||||
'embed-add' => 'Embeddable player',
|
||||
'clips' => 'Clips',
|
||||
'soundbites-edit' => 'Soundbites',
|
||||
'video-clips-list' => 'Video clips',
|
||||
'video-clips-create' => 'New video clip',
|
||||
'soundbites-list' => 'Soundbites',
|
||||
'soundbites-create' => 'New soundbite',
|
||||
];
|
||||
|
27
modules/Admin/Language/en/Soundbite.php
Normal file
27
modules/Admin/Language/en/Soundbite.php
Normal file
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @copyright 2021 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
return [
|
||||
'list' => [
|
||||
'title' => 'Soundbites',
|
||||
'soundbite' => 'Soundbite',
|
||||
],
|
||||
'form' => [
|
||||
'title' => 'New soundbite',
|
||||
'soundbite_title' => 'Soundbite title',
|
||||
'start_time' => 'Start at',
|
||||
'duration' => 'Duration',
|
||||
'submit' => 'Create soundbite',
|
||||
],
|
||||
'play' => 'Play soundbite',
|
||||
'stop' => 'Stop soundbite',
|
||||
'create' => 'New soundbite',
|
||||
'delete' => 'Delete soundbite',
|
||||
];
|
@ -151,26 +151,6 @@ return [
|
||||
'understand' => 'Je comprends, Je veux supprimer l’épisode',
|
||||
'submit' => 'Supprimer',
|
||||
],
|
||||
'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' => 'Enregistrer les extraits sonnores',
|
||||
],
|
||||
'embed' => [
|
||||
'add' => 'Ajouter un lecteur intégré',
|
||||
'title' => 'Lecteur intégré',
|
||||
|
@ -16,7 +16,8 @@ return [
|
||||
'episode-persons-manage' => 'Gestion des intervenants',
|
||||
'embed' => 'Lecteur intégré',
|
||||
'clips' => 'Extraits',
|
||||
'soundbites-edit' => 'Extraits sonores',
|
||||
'video-clips-list' => 'Extraits video',
|
||||
'video-clips-create' => 'Nouvel extrait video',
|
||||
'soundbites-list' => 'Extraits sonores',
|
||||
'soundbites-create' => 'Nouvel extrait sonore',
|
||||
];
|
||||
|
27
modules/Admin/Language/fr/Soundbite.php
Normal file
27
modules/Admin/Language/fr/Soundbite.php
Normal file
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @copyright 2021 Podlibre
|
||||
* @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3
|
||||
* @link https://castopod.org/
|
||||
*/
|
||||
|
||||
return [
|
||||
'list' => [
|
||||
'title' => 'Extraits sonores',
|
||||
'soundbite' => 'Extrait sonore',
|
||||
],
|
||||
'form' => [
|
||||
'title' => 'Nouvel extrait sonore',
|
||||
'soundbite_title' => 'Titre de l’extrait',
|
||||
'start_time' => 'Début à',
|
||||
'duration' => 'Durée',
|
||||
'submit' => 'Créer l’extrait sonore',
|
||||
],
|
||||
'play' => 'Lancer l’extrait sonore',
|
||||
'stop' => 'Arrêter l’extrait sonore',
|
||||
'create' => 'Nouvel extrait sonore',
|
||||
'delete' => 'Supprimer l’extrait sonore',
|
||||
];
|
@ -7,7 +7,7 @@ $podcastNavigation = [
|
||||
],
|
||||
'clips' => [
|
||||
'icon' => 'clapperboard',
|
||||
'items' => ['video-clips-list', 'video-clips-create', 'soundbites-edit'],
|
||||
'items' => ['video-clips-list', 'video-clips-create', 'soundbites-list', 'soundbites-create'],
|
||||
],
|
||||
]; ?>
|
||||
|
||||
|
@ -1,72 +0,0 @@
|
||||
<?= $this->extend('_layout') ?>
|
||||
|
||||
<?= $this->section('title') ?>
|
||||
<?= lang('Episode.soundbites_form.title') ?>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('pageTitle') ?>
|
||||
<?= lang('Episode.soundbites_form.title') ?>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('headerRight') ?>
|
||||
<Button variant="primary" type="submit" form="soundbites-form"><?= lang('Episode.soundbites_form.submit') ?></Button>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
|
||||
<?= $this->section('content') ?>
|
||||
|
||||
<form id="soundbites-form" action="<?= route_to('episode-soundbites-edit', $podcast->id, $episode->id) ?>" method="POST" class="flex flex-col max-w-xl">
|
||||
<?= csrf_field() ?>
|
||||
|
||||
<Forms.Section
|
||||
title="<?= lang('Episode.soundbites_form.info_section_title') ?>"
|
||||
subtitle="<?= lang('Episode.soundbites_form.info_section_subtitle') ?>" >
|
||||
|
||||
<?php
|
||||
$table = new \CodeIgniter\View\Table();
|
||||
|
||||
$table->setHeading(
|
||||
lang('Episode.soundbites_form.start_time') . hint_tooltip(lang('Episode.soundbites_form.start_time_hint')),
|
||||
lang('Episode.soundbites_form.duration') . hint_tooltip(lang('Episode.soundbites_form.duration_hint')),
|
||||
lang('Episode.soundbites_form.label') . hint_tooltip(lang('Episode.soundbites_form.label_hint')),
|
||||
'',
|
||||
''
|
||||
);
|
||||
|
||||
foreach ($episode->soundbites as $soundbite) {
|
||||
$table->addRow(
|
||||
"<Forms.Input class='w-24' type='number' step='any' min='0' max='{$episode->audio->duration}' name='soundbites[{$soundbite->id}][start_time]' value='{$soundbite->start_time}' data-type='soundbite-field' data-field-type='start_time' required='true' />",
|
||||
"<Forms.Input class='w-24' type='number' step='any' min='0' max='{$episode->audio->duration}' name='soundbites[{$soundbite->id}][duration]' value='{$soundbite->duration}' data-type='soundbite-field' data-field-type='duration' required='true' />",
|
||||
"<Forms.Input class='flex-1' name='soundbites[{$soundbite->id}][label]' value='{$soundbite->label}' />",
|
||||
"<IconButton variant='primary' glyph='play' data-type='play-soundbite' data-soundbite-id='{$soundbite->id}'>" . lang('Episode.soundbites_form.play') . '</IconButton>',
|
||||
'<IconButton uri=' . route_to(
|
||||
'soundbite-delete',
|
||||
$podcast->id,
|
||||
$episode->id,
|
||||
$soundbite->id,
|
||||
) . " variant='danger' glyph='delete-bin'>" . lang('Episode.soundbites_form.delete') . '</IconButton>'
|
||||
);
|
||||
}
|
||||
|
||||
$table->addRow(
|
||||
"<Forms.Input class='w-24' type='number' step='any' min='0' max='{$episode->audio->duration}' name='soundbites[0][start_time]' data-type='soundbite-field' data-field-type='start_time' />",
|
||||
"<Forms.Input class='w-24' type='number' step='any' min='0' max='{$episode->audio->duration}' name='soundbites[0][duration]' data-type='soundbite-field' data-field-type='duration' />",
|
||||
"<Forms.Input class='flex-1' name='soundbites[0][label]' />",
|
||||
"<IconButton variant='primary' glyph='play' data-type='play-soundbite' data-soundbite-id='0'>" . lang('Episode.soundbites_form.play') . '</IconButton>',
|
||||
);
|
||||
|
||||
echo $table->generate();
|
||||
|
||||
?>
|
||||
|
||||
<div class="flex items-center gap-x-2">
|
||||
<audio controls preload="auto" class="flex-1 w-full">
|
||||
<source src="<?= $episode->audio->file_url ?>" type="<?= $episode->audio->file_mimetype ?>">
|
||||
Your browser does not support the audio tag.
|
||||
</audio>
|
||||
<IconButton glyph="timer" variant="info" data-type="get-soundbite" data-start-time-field-name="soundbites[0][start_time]" data-duration-field-name="soundbites[0][duration]" ><?= lang('Episode.soundbites_form.bookmark') ?></IconButton>
|
||||
</div>
|
||||
</Forms.Section>
|
||||
</form>
|
||||
|
||||
<?= $this->endSection() ?>
|
48
themes/cp_admin/episode/soundbites_list.php
Normal file
48
themes/cp_admin/episode/soundbites_list.php
Normal file
@ -0,0 +1,48 @@
|
||||
<?= $this->extend('_layout') ?>
|
||||
|
||||
<?= $this->section('title') ?>
|
||||
<?= lang('Soundbite.list.title') ?>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('pageTitle') ?>
|
||||
<?= lang('Soundbite.list.title') ?>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('headerRight') ?>
|
||||
<Button uri="<?= route_to('soundbites-create', $podcast->id, $episode->id) ?>" variant="primary" iconLeft="add"><?= lang('Soundbite.create') ?></Button>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('content') ?>
|
||||
|
||||
<?= data_table(
|
||||
[
|
||||
[
|
||||
'header' => lang('Soundbite.list.soundbite'),
|
||||
'cell' => function ($soundbite): string {
|
||||
return '<div class="flex gap-x-2"><play-soundbite audio-src="' . $soundbite->episode->audio->file_url . '" start-time="' . $soundbite->start_time . '" duration="' . $soundbite->duration . '" play-label="' . lang('Soundbite.play') . '" playing-label="' . lang('Soundbite.stop') . '"></play-soundbite><div class="flex flex-col"><span class="text-sm font-semibold">' . $soundbite->title . '</span><span class="text-xs">' . format_duration((int) $soundbite->duration) . '</span></div></div>';
|
||||
},
|
||||
],
|
||||
[
|
||||
'header' => lang('Common.actions'),
|
||||
'cell' => function ($soundbite): string {
|
||||
return '<button id="more-dropdown-' . $soundbite->id . '" type="button" class="inline-flex items-center p-1 rounded-full focus:ring-accent" data-dropdown="button" data-dropdown-target="more-dropdown-' . $soundbite->id . '-menu" aria-haspopup="true" aria-expanded="false">' .
|
||||
icon('more') .
|
||||
'</button>' .
|
||||
'<DropdownMenu id="more-dropdown-' . $soundbite->id . '-menu" labelledby="more-dropdown-' . $soundbite->id . '" offsetY="-24" items="' . esc(json_encode([
|
||||
[
|
||||
'type' => 'link',
|
||||
'title' => lang('Soundbite.delete'),
|
||||
'uri' => route_to('soundbites-delete', $soundbite->podcast_id, $soundbite->episode_id, $soundbite->id),
|
||||
'class' => 'font-semibold text-red-600',
|
||||
],
|
||||
])) . '" />';
|
||||
},
|
||||
],
|
||||
],
|
||||
$soundbites,
|
||||
'mb-6',
|
||||
) ?>
|
||||
|
||||
<?= $pager->links() ?>
|
||||
|
||||
<?= $this->endSection() ?>
|
35
themes/cp_admin/episode/soundbites_new.php
Normal file
35
themes/cp_admin/episode/soundbites_new.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?= $this->extend('_layout') ?>
|
||||
|
||||
<?= $this->section('title') ?>
|
||||
<?= lang('Soundbite.form.title') ?>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('pageTitle') ?>
|
||||
<?= lang('Soundbite.form.title') ?>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
|
||||
<?= $this->section('content') ?>
|
||||
|
||||
<form id="soundbites-form" action="<?= route_to('episode-soundbites-edit', $podcast->id, $episode->id) ?>" method="POST" class="flex flex-col">
|
||||
<?= csrf_field() ?>
|
||||
|
||||
<Forms.Field
|
||||
name="title"
|
||||
label="<?= lang('Soundbite.form.soundbite_title') ?>"
|
||||
required="true"
|
||||
class="max-w-sm"
|
||||
/>
|
||||
<audio-clipper start-time="<?= old('start_time', 0) ?>" duration="<?= old('duration', 30) ?>" min-duration="10" volume=".5" height="50" class="mt-8">
|
||||
<audio slot="audio" src="<?= $episode->audio->file_url ?>" preload="auto">
|
||||
Your browser does not support the <code>audio</code> element.
|
||||
</audio>
|
||||
<input slot="start_time" type="number" name="start_time" placeholder="<?= lang('VideoClip.form.start_time') ?>" step="0.001" />
|
||||
<input slot="duration" type="number" name="duration" placeholder="<?= lang('VideoClip.form.duration') ?>" step="0.001" />
|
||||
</audio-clipper>
|
||||
|
||||
<Button variant="primary" type="submit" class="self-end mt-4" iconRight="arrow-right"><?= lang('Soundbite.form.submit') ?></Button>
|
||||
|
||||
</form>
|
||||
|
||||
<?= $this->endSection() ?>
|
@ -2,13 +2,13 @@
|
||||
|
||||
<?= $this->section('title') ?>
|
||||
<?= lang('VideoClip.title', [
|
||||
'videoClipLabel' => $videoClip->label,
|
||||
'videoClipLabel' => $videoClip->title,
|
||||
]) ?>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
<?= $this->section('pageTitle') ?>
|
||||
<?= lang('VideoClip.title', [
|
||||
'videoClipLabel' => $videoClip->label,
|
||||
'videoClipLabel' => $videoClip->title,
|
||||
]) ?>
|
||||
<?= $this->endSection() ?>
|
||||
|
||||
|
@ -62,7 +62,7 @@ use CodeIgniter\I18n\Time;
|
||||
'portrait' => 'aspect-[9/16]',
|
||||
'squared' => 'aspect-square',
|
||||
];
|
||||
return '<a href="' . route_to('video-clip', $videoClip->podcast_id, $videoClip->episode_id, $videoClip->id) . '" class="inline-flex items-center w-full group gap-x-2 focus:ring-accent"><div class="relative"><span class="absolute block w-3 h-3 rounded-full ring-2 ring-white -bottom-1 -left-1" data-tooltip="bottom" title="' . lang('Settings.theme.' . $videoClip->theme['name']) . '" style="background-color:hsl(' . $videoClip->theme['preview'] . ')"></span><div class="flex items-center justify-center h-6 overflow-hidden bg-black rounded-sm aspect-video" data-tooltip="bottom" title="' . $videoClip->format . '"><span class="flex items-center justify-center h-full text-white bg-gray-400 ' . $formatClass[$videoClip->format] . '"><Icon glyph="play"/></span></div></div><div class="flex flex-col"><div class="text-sm">#' . $videoClip->id . ' – <span class="font-semibold group-hover:underline">' . $videoClip->label . '</span><span class="ml-1 text-sm">by ' . $videoClip->user->username . '</span></div><span class="text-xs">' . format_duration((int) $videoClip->duration) . '</span></div></a>';
|
||||
return '<a href="' . route_to('video-clip', $videoClip->podcast_id, $videoClip->episode_id, $videoClip->id) . '" class="inline-flex items-center w-full group gap-x-2 focus:ring-accent"><div class="relative"><span class="absolute block w-3 h-3 rounded-full ring-2 ring-white -bottom-1 -left-1" data-tooltip="bottom" title="' . lang('Settings.theme.' . $videoClip->theme['name']) . '" style="background-color:hsl(' . $videoClip->theme['preview'] . ')"></span><div class="flex items-center justify-center h-6 overflow-hidden bg-black rounded-sm aspect-video" data-tooltip="bottom" title="' . $videoClip->format . '"><span class="flex items-center justify-center h-full text-white bg-gray-400 ' . $formatClass[$videoClip->format] . '"><Icon glyph="play"/></span></div></div><div class="flex flex-col"><div class="text-sm">#' . $videoClip->id . ' – <span class="font-semibold group-hover:underline">' . $videoClip->title . '</span><span class="ml-1 text-sm">by ' . $videoClip->user->username . '</span></div><span class="text-xs">' . format_duration((int) $videoClip->duration) . '</span></div></a>';
|
||||
},
|
||||
],
|
||||
[
|
||||
@ -89,7 +89,7 @@ use CodeIgniter\I18n\Time;
|
||||
$downloadButton = '';
|
||||
if ($videoClip->media) {
|
||||
helper('misc');
|
||||
$filename = 'clip-' . slugify($videoClip->label) . "-{$videoClip->start_time}-{$videoClip->end_time}";
|
||||
$filename = 'clip-' . slugify($videoClip->title) . "-{$videoClip->start_time}-{$videoClip->end_time}";
|
||||
$downloadButton = '<IconButton glyph="download" uri="' . $videoClip->media->file_url . '" download="' . $filename . '">' . lang('VideoClip.download_clip') . '</IconButton>';
|
||||
}
|
||||
|
||||
|
@ -17,7 +17,7 @@
|
||||
<img slot="preview_image" src="<?= $episode->cover->thumbnail_url ?>" alt="<?= $episode->cover->description ?>" />
|
||||
</video-clip-previewer>
|
||||
<audio-clipper start-time="<?= old('start_time', 0) ?>" duration="<?= old('duration', 30) ?>" min-duration="10" volume=".5" height="50">
|
||||
<audio slot="audio" src="<?= $episode->audio->file_url ?>" class="w-full" preload="auto">
|
||||
<audio slot="audio" src="<?= $episode->audio->file_url ?>" preload="auto">
|
||||
Your browser does not support the <code>audio</code> element.
|
||||
</audio>
|
||||
<input slot="start_time" type="number" name="start_time" placeholder="<?= lang('VideoClip.form.start_time') ?>" step="0.001" />
|
||||
@ -28,7 +28,7 @@
|
||||
<div class="flex flex-col items-end w-full max-w-xl xl:max-w-sm 2xl:max-w-xl gap-y-4">
|
||||
<Forms.Section title="<?= lang('VideoClip.form.params_section_title') ?>" >
|
||||
<Forms.Field
|
||||
name="label"
|
||||
name="title"
|
||||
label="<?= lang('VideoClip.form.clip_title') ?>"
|
||||
required="true"
|
||||
/>
|
||||
|
Loading…
x
Reference in New Issue
Block a user