mirror of
https://code.castopod.org/adaures/castopod
synced 2025-04-19 13:01:19 +00:00
feat: add audio-clipper webcomponent (wip)
This commit is contained in:
parent
7609bb6033
commit
21d4251b9b
@ -1,5 +1,6 @@
|
||||
import "@github/markdown-toolbar-element";
|
||||
import "@github/time-elements";
|
||||
import "./modules/audio-clipper";
|
||||
import ClientTimezone from "./modules/ClientTimezone";
|
||||
import Clipboard from "./modules/Clipboard";
|
||||
import DateTimePicker from "./modules/DateTimePicker";
|
||||
|
440
app/Resources/js/modules/audio-clipper.ts
Normal file
440
app/Resources/js/modules/audio-clipper.ts
Normal file
@ -0,0 +1,440 @@
|
||||
import { css, html, LitElement, TemplateResult } from "lit";
|
||||
import {
|
||||
customElement,
|
||||
property,
|
||||
query,
|
||||
queryAssignedNodes,
|
||||
state,
|
||||
} from "lit/decorators.js";
|
||||
import WaveSurfer from "wavesurfer.js";
|
||||
|
||||
enum ACTIONS {
|
||||
StretchLeft,
|
||||
StretchRight,
|
||||
Seek,
|
||||
}
|
||||
|
||||
@customElement("audio-clipper")
|
||||
export class AudioClipper extends LitElement {
|
||||
@queryAssignedNodes("audio", true)
|
||||
_audio!: NodeListOf<HTMLAudioElement>;
|
||||
|
||||
@queryAssignedNodes("start_time", true)
|
||||
_startTimeInput!: NodeListOf<HTMLInputElement>;
|
||||
|
||||
@queryAssignedNodes("duration", true)
|
||||
_durationInput!: NodeListOf<HTMLInputElement>;
|
||||
|
||||
@query(".slider")
|
||||
_sliderNode!: HTMLDivElement;
|
||||
|
||||
@query(".slider__segment--wrapper")
|
||||
_segmentNode!: HTMLDivElement;
|
||||
|
||||
@query(".slider__segment-content")
|
||||
_segmentContentNode!: HTMLDivElement;
|
||||
|
||||
@query(".slider__segment-progress-handle")
|
||||
_progressNode!: HTMLDivElement;
|
||||
|
||||
@query("#waveform")
|
||||
_waveformNode!: HTMLDivElement;
|
||||
|
||||
@property({ type: Number, attribute: "start-time" })
|
||||
startTime = 0;
|
||||
|
||||
@property({ type: Number })
|
||||
duration = 10;
|
||||
|
||||
@property({ type: Number, attribute: "min-duration" })
|
||||
minDuration = 5;
|
||||
|
||||
@property({ type: Number, attribute: "volume" })
|
||||
initVolume = 0.5;
|
||||
|
||||
@state()
|
||||
_isPlaying = false;
|
||||
|
||||
@state()
|
||||
_clip = {
|
||||
startTime: 0,
|
||||
endTime: 0,
|
||||
};
|
||||
|
||||
@state()
|
||||
_action: ACTIONS | null = null;
|
||||
|
||||
@state()
|
||||
_audioDuration = 0;
|
||||
|
||||
@state()
|
||||
_sliderWidth = 0;
|
||||
|
||||
@state()
|
||||
_currentTime = 0;
|
||||
|
||||
@state()
|
||||
_volume = 0.5;
|
||||
|
||||
@state()
|
||||
_wavesurfer!: WaveSurfer;
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
|
||||
console.log("connectedCallback_before");
|
||||
this._clip = {
|
||||
startTime: this.startTime,
|
||||
endTime: this.startTime + this.duration,
|
||||
};
|
||||
this._volume = this.initVolume;
|
||||
console.log("connectedCallback_after");
|
||||
}
|
||||
|
||||
protected firstUpdated(): void {
|
||||
console.log("firstUpdate");
|
||||
this._audioDuration = this._audio[0].duration;
|
||||
this._audio[0].volume = this._volume;
|
||||
|
||||
this._wavesurfer = WaveSurfer.create({
|
||||
container: this._waveformNode,
|
||||
interact: false,
|
||||
barWidth: 2,
|
||||
barHeight: 1,
|
||||
responsive: true,
|
||||
});
|
||||
this._wavesurfer.load(this._audio[0].src);
|
||||
|
||||
window.addEventListener("load", () => {
|
||||
this._sliderWidth = this._sliderNode.clientWidth;
|
||||
this.setSegmentPosition();
|
||||
});
|
||||
window.addEventListener("resize", () => {
|
||||
this._sliderWidth = this._sliderNode.clientWidth;
|
||||
this.setSegmentPosition();
|
||||
});
|
||||
|
||||
document.addEventListener("mouseup", () => {
|
||||
if (this._action !== null) {
|
||||
this._action = null;
|
||||
}
|
||||
});
|
||||
document.addEventListener("mousemove", (event: MouseEvent) => {
|
||||
if (this._action !== null) {
|
||||
this.updatePosition(event);
|
||||
}
|
||||
});
|
||||
|
||||
this._audio[0].addEventListener("play", () => {
|
||||
this._isPlaying = true;
|
||||
});
|
||||
this._audio[0].addEventListener("pause", () => {
|
||||
this._isPlaying = false;
|
||||
});
|
||||
// this._audio[0].addEventListener("timeupdate", () => {
|
||||
// this._currentTime = this._audio[0].currentTime;
|
||||
// });
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
console.log("disconnectedCallback");
|
||||
|
||||
window.removeEventListener("load", () => {
|
||||
this._sliderWidth = this._sliderNode.clientWidth;
|
||||
this.setSegmentPosition();
|
||||
});
|
||||
window.removeEventListener("resize", () => {
|
||||
this._sliderWidth = this._sliderNode.clientWidth;
|
||||
this.setSegmentPosition();
|
||||
});
|
||||
|
||||
document.removeEventListener("mouseup", () => {
|
||||
if (this._action !== null) {
|
||||
this._action = null;
|
||||
}
|
||||
});
|
||||
document.removeEventListener("mousemove", (event: MouseEvent) => {
|
||||
if (this._action !== null) {
|
||||
this.updatePosition(event);
|
||||
}
|
||||
});
|
||||
|
||||
this._audio[0].removeEventListener("play", () => {
|
||||
this._isPlaying = true;
|
||||
});
|
||||
this._audio[0].removeEventListener("pause", () => {
|
||||
this._isPlaying = false;
|
||||
});
|
||||
// this._audio[0].removeEventListener("timeupdate", () => {
|
||||
// this._currentTime = this._audio[0].currentTime;
|
||||
// });
|
||||
}
|
||||
|
||||
setSegmentPosition(): void {
|
||||
const startTimePosition = this.getPositionFromSeconds(this._clip.startTime);
|
||||
const endTimePosition = this.getPositionFromSeconds(this._clip.endTime);
|
||||
|
||||
this._segmentNode.style.transform = `translateX(${startTimePosition}px)`;
|
||||
this._segmentContentNode.style.width = `${
|
||||
endTimePosition - startTimePosition
|
||||
}px`;
|
||||
}
|
||||
|
||||
getPositionFromSeconds(seconds: number) {
|
||||
return (seconds * this._sliderWidth) / this._audioDuration;
|
||||
}
|
||||
|
||||
getSecondsFromPosition(position: number) {
|
||||
return (this._audioDuration * position) / this._sliderWidth;
|
||||
}
|
||||
|
||||
protected updated(
|
||||
_changedProperties: Map<string | number | symbol, unknown>
|
||||
): void {
|
||||
// console.log("updated", _changedProperties);
|
||||
|
||||
if (_changedProperties.has("_clip")) {
|
||||
// console.log("CLIP", _changedProperties.get("_clip"));
|
||||
this.pause();
|
||||
this.setSegmentPosition();
|
||||
console.log(this._clip.startTime);
|
||||
this._audio[0].currentTime = 58;
|
||||
console.log(this._audio[0].currentTime);
|
||||
}
|
||||
}
|
||||
|
||||
play(): void {
|
||||
this._audio[0].play();
|
||||
// setTimeout(() => {
|
||||
// this.pause();
|
||||
// this._audio[0].currentTime = this._clip.startTime;
|
||||
// }, (this._clip.endTime - this._clip.startTime) * 1000);
|
||||
}
|
||||
|
||||
pause(): void {
|
||||
this._audio[0].pause();
|
||||
}
|
||||
|
||||
updatePosition(event: MouseEvent): void {
|
||||
const cursorPosition =
|
||||
event.clientX -
|
||||
(this._sliderNode.getBoundingClientRect().left +
|
||||
document.documentElement.scrollLeft);
|
||||
|
||||
const seconds = this.getSecondsFromPosition(cursorPosition);
|
||||
|
||||
switch (this._action) {
|
||||
case ACTIONS.StretchLeft: {
|
||||
let startTime;
|
||||
if (seconds > 0) {
|
||||
if (seconds > this._clip.endTime - this.minDuration) {
|
||||
startTime = this._clip.endTime - this.minDuration;
|
||||
} else {
|
||||
startTime = seconds;
|
||||
}
|
||||
} else {
|
||||
startTime = 0;
|
||||
}
|
||||
this._clip = {
|
||||
startTime,
|
||||
endTime: this._clip.endTime,
|
||||
};
|
||||
break;
|
||||
}
|
||||
case ACTIONS.StretchRight: {
|
||||
let endTime;
|
||||
if (seconds < this._audioDuration) {
|
||||
if (seconds < this._clip.startTime + this.minDuration) {
|
||||
endTime = this._clip.startTime + this.minDuration;
|
||||
} else {
|
||||
endTime = seconds;
|
||||
}
|
||||
} else {
|
||||
endTime = this._audioDuration;
|
||||
}
|
||||
|
||||
this._clip = {
|
||||
startTime: this._clip.startTime,
|
||||
endTime,
|
||||
};
|
||||
break;
|
||||
}
|
||||
case ACTIONS.Seek: {
|
||||
console.log("seeking");
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
setVolume(event: InputEvent): void {
|
||||
this._volume = parseFloat((event.target as HTMLInputElement).value);
|
||||
this._audio[0].volume = this._volume;
|
||||
}
|
||||
|
||||
setCurrentTime(event: MouseEvent): void {
|
||||
const cursorPosition =
|
||||
event.clientX -
|
||||
(this._sliderNode.getBoundingClientRect().left +
|
||||
document.documentElement.scrollLeft);
|
||||
|
||||
const seconds = this.getSecondsFromPosition(cursorPosition);
|
||||
this._audio[0].currentTime = seconds;
|
||||
}
|
||||
|
||||
setAction(action: ACTIONS): void {
|
||||
this._action = action;
|
||||
}
|
||||
|
||||
secondsToHHMMSS(seconds: number): string {
|
||||
return new Date(seconds * 1000).toISOString().substr(11, 8);
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.slider {
|
||||
position: relative;
|
||||
height: 6rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
background-color: #0f172a;
|
||||
}
|
||||
|
||||
.slider__track-placeholder {
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
background-color: #64748b;
|
||||
}
|
||||
|
||||
.slider__segment--wrapper {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.slider__segment {
|
||||
position: relative;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.slider__segment-content {
|
||||
background-color: rgba(255, 255, 255, 0.9);
|
||||
height: 4rem;
|
||||
width: 1px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.slider__segment-progress-handle {
|
||||
position: absolute;
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
margin-top: -9px;
|
||||
margin-left: -4px;
|
||||
background-color: #3b82f6;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.slider__segment .slider__segment-handle {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
width: 1rem;
|
||||
height: 100%;
|
||||
background-color: #b91c1c;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.slider__segment .slider__segment-handle::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
height: 3rem;
|
||||
width: 2px;
|
||||
background-color: #ffffff;
|
||||
margin: auto;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.slider__segment .clipper__handle-left {
|
||||
left: -1rem;
|
||||
border-radius: 0.2rem 0 0 0.2rem;
|
||||
}
|
||||
|
||||
.slider__segment .clipper__handle-right {
|
||||
right: -1rem;
|
||||
border-radius: 0 0.2rem 0.2rem 0;
|
||||
}
|
||||
`;
|
||||
|
||||
render(): TemplateResult<1> {
|
||||
return html`
|
||||
<slot name="audio"></slot>
|
||||
<slot name="start_time"></slot>
|
||||
<slot name="duration"></slot>
|
||||
<div>${this.secondsToHHMMSS(this._clip.startTime)}</div>
|
||||
<div>${this.secondsToHHMMSS(this._currentTime)}</div>
|
||||
<div>${this.secondsToHHMMSS(this._clip.endTime)}</div>
|
||||
<input
|
||||
type="range"
|
||||
id="volume"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.1"
|
||||
value="${this._volume}"
|
||||
@change="${this.setVolume}"
|
||||
/>
|
||||
<div id="waveform"></div>
|
||||
<div class="slider" role="slider">
|
||||
<div class="slider__track-placeholder"></div>
|
||||
<div class="slider__segment--wrapper">
|
||||
<div
|
||||
class="slider__segment-progress-handle"
|
||||
@mousedown="${() => this.setAction(ACTIONS.Seek)}"
|
||||
></div>
|
||||
<!-- <div class="slider__segment-progress-handle-bar"></div> -->
|
||||
<div class="slider__segment">
|
||||
<button
|
||||
class="slider__segment-handle clipper__handle-left"
|
||||
title="${this.secondsToHHMMSS(this._clip.startTime)}"
|
||||
@mousedown="${() => this.setAction(ACTIONS.StretchLeft)}"
|
||||
></button>
|
||||
<div
|
||||
class="slider__segment-content"
|
||||
@click="${this.setCurrentTime}"
|
||||
></div>
|
||||
<button
|
||||
class="slider__segment-handle clipper__handle-right"
|
||||
title="${this.secondsToHHMMSS(this._clip.endTime)}"
|
||||
@mousedown="${() => this.setAction(ACTIONS.StretchRight)}"
|
||||
></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button @click="${this._isPlaying ? this.pause : this.play}">
|
||||
${this._isPlaying
|
||||
? html`<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
width="1em"
|
||||
height="1em"
|
||||
>
|
||||
<g>
|
||||
<path fill="none" d="M0 0h24v24H0z" />
|
||||
<path d="M6 5h2v14H6V5zm10 0h2v14h-2V5z" />
|
||||
</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>
|
||||
`;
|
||||
}
|
||||
}
|
42
package-lock.json
generated
42
package-lock.json
generated
@ -29,6 +29,7 @@
|
||||
"leaflet.markercluster": "^1.5.3",
|
||||
"lit": "^2.0.2",
|
||||
"marked": "^4.0.7",
|
||||
"wavesurfer.js": "^5.2.0",
|
||||
"xml-formatter": "^2.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
@ -43,6 +44,7 @@
|
||||
"@tailwindcss/typography": "^0.5.0-alpha.3",
|
||||
"@types/leaflet": "^1.7.6",
|
||||
"@types/marked": "^4.0.1",
|
||||
"@types/wavesurfer.js": "^5.2.2",
|
||||
"@typescript-eslint/eslint-plugin": "^5.7.0",
|
||||
"@typescript-eslint/parser": "^5.7.0",
|
||||
"cross-env": "^7.0.3",
|
||||
@ -3248,6 +3250,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/debounce": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/debounce/-/debounce-1.2.1.tgz",
|
||||
"integrity": "sha512-epMsEE85fi4lfmJUH/89/iV/LI+F5CvNIvmgs5g5jYFPfhO2S/ae8WSsLOKWdwtoaZw9Q2IhJ4tQ5tFCcS/4HA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "0.0.39",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz",
|
||||
@ -3342,6 +3350,15 @@
|
||||
"version": "2.0.2",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/wavesurfer.js": {
|
||||
"version": "5.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/wavesurfer.js/-/wavesurfer.js-5.2.2.tgz",
|
||||
"integrity": "sha512-/vjpf81co0SK3z4F5V79fZrFPQ8pw9/fEpgkzcgNVkBa9sY0gAaYzKuaQyCX/yjVf6kc73uPtWABQuVgvpguDQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/debounce": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "5.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.8.1.tgz",
|
||||
@ -15895,6 +15912,11 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/wavesurfer.js": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/wavesurfer.js/-/wavesurfer.js-5.2.0.tgz",
|
||||
"integrity": "sha512-SkPlTXfvKy+ZnEA7f7g7jn6iQg5/8mAvWpVV5vRbIS/FF9TB2ak9J7VayQfzfshOLW/CqccTiN6DDR/fZA902g=="
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "6.1.0",
|
||||
"license": "BSD-2-Clause",
|
||||
@ -18897,6 +18919,12 @@
|
||||
"version": "1.1.1",
|
||||
"dev": true
|
||||
},
|
||||
"@types/debounce": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/debounce/-/debounce-1.2.1.tgz",
|
||||
"integrity": "sha512-epMsEE85fi4lfmJUH/89/iV/LI+F5CvNIvmgs5g5jYFPfhO2S/ae8WSsLOKWdwtoaZw9Q2IhJ4tQ5tFCcS/4HA==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/estree": {
|
||||
"version": "0.0.39",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz",
|
||||
@ -18979,6 +19007,15 @@
|
||||
"@types/trusted-types": {
|
||||
"version": "2.0.2"
|
||||
},
|
||||
"@types/wavesurfer.js": {
|
||||
"version": "5.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/wavesurfer.js/-/wavesurfer.js-5.2.2.tgz",
|
||||
"integrity": "sha512-/vjpf81co0SK3z4F5V79fZrFPQ8pw9/fEpgkzcgNVkBa9sY0gAaYzKuaQyCX/yjVf6kc73uPtWABQuVgvpguDQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/debounce": "*"
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/eslint-plugin": {
|
||||
"version": "5.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.8.1.tgz",
|
||||
@ -27308,6 +27345,11 @@
|
||||
"xml-name-validator": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"wavesurfer.js": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/wavesurfer.js/-/wavesurfer.js-5.2.0.tgz",
|
||||
"integrity": "sha512-SkPlTXfvKy+ZnEA7f7g7jn6iQg5/8mAvWpVV5vRbIS/FF9TB2ak9J7VayQfzfshOLW/CqccTiN6DDR/fZA902g=="
|
||||
},
|
||||
"webidl-conversions": {
|
||||
"version": "6.1.0"
|
||||
},
|
||||
|
@ -47,6 +47,7 @@
|
||||
"leaflet.markercluster": "^1.5.3",
|
||||
"lit": "^2.0.2",
|
||||
"marked": "^4.0.7",
|
||||
"wavesurfer.js": "^5.2.0",
|
||||
"xml-formatter": "^2.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
@ -61,6 +62,7 @@
|
||||
"@tailwindcss/typography": "^0.5.0-alpha.3",
|
||||
"@types/leaflet": "^1.7.6",
|
||||
"@types/marked": "^4.0.1",
|
||||
"@types/wavesurfer.js": "^5.2.2",
|
||||
"@typescript-eslint/eslint-plugin": "^5.7.0",
|
||||
"@typescript-eslint/parser": "^5.7.0",
|
||||
"cross-env": "^7.0.3",
|
||||
|
@ -10,9 +10,20 @@
|
||||
|
||||
<?= $this->section('content') ?>
|
||||
|
||||
<form action="<?= route_to('video-clips-create', $podcast->id, $episode->id) ?>" method="POST" class="flex flex-col gap-y-4">
|
||||
<form action="<?= route_to('video-clips-create', $podcast->id, $episode->id) ?>" method="POST" class="flex gap-4">
|
||||
|
||||
<Forms.Section title="<?= lang('VideoClip.form.params_section_title') ?>" >
|
||||
<div class="flex-1 w-full">
|
||||
<!-- <div class="h-full bg-black"></div> -->
|
||||
<audio-clipper start-time="1000" duration="140" min-duration="10" volume=".25">
|
||||
<audio slot="audio" src="<?= $episode->audio->file_url ?>" class="w-full">
|
||||
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>
|
||||
</div>
|
||||
|
||||
<!-- <Forms.Section title="<?= lang('VideoClip.form.params_section_title') ?>" >
|
||||
|
||||
<Forms.Field
|
||||
name="label"
|
||||
@ -49,26 +60,9 @@
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<div class="flex flex-col gap-x-2 gap-y-4 md:flex-row">
|
||||
<Forms.Field
|
||||
type="number"
|
||||
name="start_time"
|
||||
label="<?= lang('VideoClip.form.start_time') ?>"
|
||||
required="true"
|
||||
step="0.001"
|
||||
/>
|
||||
<Forms.Field
|
||||
type="number"
|
||||
name="duration"
|
||||
label="<?= lang('VideoClip.form.duration') ?>"
|
||||
required="true"
|
||||
step="0.001"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button variant="primary" type="submit" iconRight="arrow-right" class="self-end"><?= lang('VideoClip.form.submit') ?></Button>
|
||||
|
||||
</Forms.Section>
|
||||
</Forms.Section> -->
|
||||
|
||||
</form>
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user