Loading app/Resources/js/admin.ts +1 −0 Original line number Diff line number Diff line Loading @@ -18,6 +18,7 @@ import Soundbites from "./modules/Soundbites"; import ThemePicker from "./modules/ThemePicker"; import Time from "./modules/Time"; import Tooltip from "./modules/Tooltip"; import "./modules/video-clip-previewer"; import "./modules/xml-editor"; Dropdown(); Loading app/Resources/js/modules/audio-clipper.ts +226 −62 Original line number Diff line number Diff line Loading @@ -48,6 +48,9 @@ export class AudioClipper extends LitElement { @query("#waveform") _waveformNode!: HTMLDivElement; @query(".buffering-bar") _bufferingBarNode!: HTMLCanvasElement; @property({ type: Number, attribute: "start-time" }) initStartTime = 0; Loading Loading @@ -87,15 +90,15 @@ export class AudioClipper extends LitElement { @state() _volume = 0.5; @state() _isLoading = false; @state() _seekingTime: number | null = null; @state() _wavesurfer!: WaveSurfer; @state() _isBuffering = false; _windowEvents: EventElement[] = [ { events: ["load", "resize"], Loading Loading @@ -144,21 +147,46 @@ export class AudioClipper extends LitElement { }, }, { events: ["complete"], events: ["progress"], onEvent: () => { this._isLoading = false; const context = this._bufferingBarNode.getContext("2d"); if (context) { context.fillStyle = "lightgray"; context.fillRect( 0, 0, this._bufferingBarNode.width, this._bufferingBarNode.height ); context.fillStyle = "#04AC64"; const inc = this._bufferingBarNode.width / this._audio[0].duration; for (let i = 0; i < this._audio[0].buffered.length; i++) { const startX = this._audio[0].buffered.start(i) * inc; const endX = this._audio[0].buffered.end(i) * inc; const width = endX - startX; context.fillRect(startX, 0, width, this._bufferingBarNode.height); context.rect(startX, 0, width, this._bufferingBarNode.height); } } }, }, { events: ["timeupdate"], onEvent: () => { // TODO: change this this._currentTime = this._audio[0].currentTime; // TODO: change this? this._currentTime = parseFloat(this._audio[0].currentTime.toFixed(3)); if (this._currentTime > this._clip.endTime) { this.pause(); this._audio[0].currentTime = this._clip.endTime; } else if (this._currentTime < this._clip.startTime) { this._isBuffering = true; this._audio[0].currentTime = this._clip.startTime; } else { this._isBuffering = false; this.setCurrentTime(this._currentTime); } }, Loading @@ -178,17 +206,18 @@ export class AudioClipper extends LitElement { protected firstUpdated(): void { this._audioDuration = this._audio[0].duration; this._audio[0].volume = this._volume; this._audio[0].currentTime = this._clip.startTime; this._isLoading = true; this._startTimeInput[0].hidden = true; this._durationInput[0].hidden = true; this._wavesurfer = WaveSurfer.create({ container: this._waveformNode, height: this.height, interact: false, barWidth: 4, barWidth: 2, barHeight: 1, barGap: 4, // barGap: 4, responsive: true, waveColor: "hsl(0 5% 85%)", cursorColor: "transparent", }); this._wavesurfer.load(this._audio[0].src); Loading Loading @@ -266,6 +295,11 @@ export class AudioClipper extends LitElement { if (_changedProperties.has("_clip")) { this.pause(); this.setSegmentPosition(); this._startTimeInput[0].value = this._clip.startTime.toString(); this._durationInput[0].value = ( this._clip.endTime - this._clip.startTime ).toFixed(3); this._audio[0].currentTime = this._clip.startTime; } if (_changedProperties.has("_seekingTime")) { Loading Loading @@ -293,18 +327,16 @@ export class AudioClipper extends LitElement { switch (this._action) { case ACTIONS.StretchLeft: { let startTime; let startTime = 0; 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, startTime: parseFloat(startTime.toFixed(3)), endTime: this._clip.endTime, }; break; Loading @@ -323,7 +355,7 @@ export class AudioClipper extends LitElement { this._clip = { startTime: this._clip.startTime, endTime, endTime: parseFloat(endTime.toFixed(3)), }; break; } Loading @@ -333,7 +365,7 @@ export class AudioClipper extends LitElement { } else if (seconds > this._clip.endTime) { this._seekingTime = this._clip.endTime; } else { this._seekingTime = seconds; this._seekingTime = parseFloat(seconds.toFixed(3)); } break; } Loading Loading @@ -386,6 +418,20 @@ export class AudioClipper extends LitElement { return new Date(seconds * 1000).toISOString().substr(11, 8); } trim(side: "start" | "end") { if (side === "start") { this._clip = { startTime: this._audio[0].currentTime, endTime: this._clip.endTime, }; } else { this._clip = { startTime: this._clip.startTime, endTime: this._currentTime, }; } } static styles = css` .slider-wrapper { position: relative; Loading @@ -393,6 +439,15 @@ export class AudioClipper extends LitElement { background-color: #0f172a; } .buffering-bar { position: absolute; width: 100%; height: 4px; background-color: gray; bottom: -4px; left: 0; } .slider { position: absolute; z-index: 10; Loading @@ -404,12 +459,6 @@ export class AudioClipper extends LitElement { width: 100%; } .slider__track-placeholder { width: 100%; height: 8px; background-color: #64748b; } .slider__segment--wrapper { position: absolute; height: 100%; Loading @@ -418,14 +467,17 @@ export class AudioClipper extends LitElement { .slider__segment { position: relative; display: flex; height: 100%; height: 120%; top: -10%; } .slider__segment-content { box-sizing: border-box; background-color: rgba(255, 255, 255, 0.5); height: 100%; width: 1px; border: none; border-top: 2px dashed #b91c1c; border-bottom: 2px dashed #b91c1c; } .slider__seeking-placeholder { Loading @@ -441,8 +493,9 @@ export class AudioClipper extends LitElement { position: absolute; width: 20px; height: 20px; top: -23px; top: -50%; left: -10px; margin-top: -2px; background-color: #3b82f6; border-radius: 50%; } Loading @@ -453,7 +506,7 @@ export class AudioClipper extends LitElement { width: 0px; height: 0px; bottom: -12px; left: 1px; left: 0; border: 10px solid transparent; border-top-color: transparent; border-top-style: solid; Loading @@ -464,7 +517,7 @@ export class AudioClipper extends LitElement { .slider__segment .slider__segment-handle { position: absolute; width: 1rem; height: 120%; height: 100%; background-color: #b91c1c; border: none; margin: auto 0; Loading @@ -475,7 +528,7 @@ export class AudioClipper extends LitElement { .slider__segment .slider__segment-handle::before { content: ""; position: absolute; height: 3rem; height: 50%; width: 2px; background-color: #ffffff; margin: auto; Loading @@ -487,12 +540,79 @@ export class AudioClipper extends LitElement { .slider__segment .clipper__handle-left { left: -1rem; border-radius: 0.2rem 9999px 9999px 0.2rem; border-radius: 0.2rem 0 0 0.2rem; } .slider__segment .clipper__handle-right { right: -1rem; border-radius: 9999px 0.2rem 0.2rem 9999px; border-radius: 0 0.2rem 0.2rem 0; } .toolbar { display: flex; align-items: center; padding: 0.5rem 0.5rem 0.25rem 0.5rem; justify-content: space-between; background-color: hsl(var(--color-background-elevated)); box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); border-radius: 0 0 0.25rem 0.25rem; flex-wrap: wrap; gap: 0.5rem; } .toolbar__audio-controls { display: flex; align-items: center; gap: 0.5rem; } .toolbar .toolbar__play-button { padding: 0.5rem; height: 32px; width: 32px; font-size: 1em; } .toolbar__trim-controls { display: flex; align-items: center; gap: 0.5rem; flex-shrink: 0; } .toolbar button { cursor: pointer; background-color: hsl(var(--color-accent-base)); color: hsl(var(--color-accent-contrast)); border-radius: 9999px; border: none; padding: 0.25rem 0.5rem; } .animate-spin { animation: spin 1s linear infinite; } @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } .volume { display: flex; font-size: 1.2rem; color: hsl(var(--color-accent-base)); align-items: center; gap: 0.25rem; } .range-slider { accent-color: hsl(var(--color-accent-base)); width: 100px; } `; Loading @@ -501,23 +621,9 @@ export class AudioClipper extends LitElement { <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> <div>${this._isLoading ? "loading..." : "not loading"}</div> <input type="range" id="volume" min="0" max="1" step="0.1" value="${this._volume}" @change="${this.setVolume}" /> <div class="slider-wrapper" style="height:${this.height}"> <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" Loading @@ -543,9 +649,36 @@ export class AudioClipper extends LitElement { </div> </div> </div> <canvas class="buffering-bar"></canvas> </div> <button @click="${this._isPlaying ? this.pause : this.play}"> ${this._isPlaying <div class="toolbar"> <div class="toolbar__audio-controls"> <button class="toolbar__play-button" @click="${this._isPlaying ? this.pause : this.play}" > ${this._isBuffering ? 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 viewBox="0 0 24 24" fill="currentColor" Loading @@ -569,6 +702,37 @@ export class AudioClipper extends LitElement { /> </svg>`} </button> <div class="volume"> <svg viewBox="0 0 24 24" fill="currentColor" width="1em" height="1em" > <g> <path fill="none" d="M0 0h24v24H0z" /> <path d="M8.889 16H5a1 1 0 0 1-1-1V9a1 1 0 0 1 1-1h3.889l5.294-4.332a.5.5 0 0 1 .817.387v15.89a.5.5 0 0 1-.817.387L8.89 16zm9.974.591l-1.422-1.422A3.993 3.993 0 0 0 19 12c0-1.43-.75-2.685-1.88-3.392l1.439-1.439A5.991 5.991 0 0 1 21 12c0 1.842-.83 3.49-2.137 4.591z" /> </g> </svg> <input class="range-slider" type="range" id="volume" min="0" max="1" step="0.1" value="${this._volume}" @change="${this.setVolume}" /> </div> </div> <div class="toolbar__trim-controls"> <button @click="${() => this.trim("start")}">Trim start</button> <button @click="${() => this.trim("end")}">Trim end</button> </div> </div> `; } } app/Resources/js/modules/video-clip-previewer.ts 0 → 100644 +58 −0 Original line number Diff line number Diff line import { css, html, LitElement, TemplateResult } from "lit"; import { customElement, property, queryAssignedNodes } from "lit/decorators.js"; import { styleMap } from "lit/directives/style-map.js"; enum VideoFormats { Landscape = "landscape", Portrait = "portrait", Squared = "squared", } const formatMap = { [VideoFormats.Landscape]: "16/9", [VideoFormats.Portrait]: "9/16", [VideoFormats.Squared]: "1/1", }; @customElement("video-clip-previewer") export class VideoClipPreviewer extends LitElement { @queryAssignedNodes("preview_image", true) _previewImage!: NodeListOf<HTMLImageElement>; @property() format: VideoFormats = VideoFormats.Landscape; @property() theme = "#009486"; static styles = css` .video-background { display: grid; justify-items: center; align-items: center; background-color: black; width: 100%; aspect-ratio: 16 / 9; } .video-format { display: grid; align-items: center; justify-items: center; height: 100%; } `; render(): TemplateResult<1> { const styles = { aspectRatio: formatMap[this.format], backgroundColor: this.theme, }; return html`<div class="video-background"> <div class="video-format" style=${styleMap(styles)}> <slot name="preview_image"></slot> </div> </div>`; } } themes/cp_admin/episode/video_clips_new.php +8 −6 Original line number Diff line number Diff line Loading @@ -10,12 +10,14 @@ <?= $this->section('content') ?> <form action="<?= route_to('video-clips-create', $podcast->id, $episode->id) ?>" method="POST" class="flex gap-4"> <form action="<?= route_to('video-clips-create', $podcast->id, $episode->id) ?>" method="POST" class="flex flex-col items-center gap-4 xl:items-start xl:flex-row"> <div class="flex-1 w-full"> <!-- <div class="h-full bg-black"></div> --> <video-clip-previewer format="portrait"> <img slot="preview_image" src="<?= $episode->cover->thumbnail_url ?>" alt="<?= $episode->cover->description ?>" /> </video-clip-previewer> <audio-clipper start-time="15" duration="10" min-duration="10" volume=".25" height="50"> <audio slot="audio" src="<?= $episode->audio->file_url ?>" class="w-full"> <audio slot="audio" src="<?= $episode->audio->file_url ?>" class="w-full" 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" /> Loading @@ -23,7 +25,7 @@ </audio-clipper> </div> <!-- <Forms.Section title="<?= lang('VideoClip.form.params_section_title') ?>" > <Forms.Section title="<?= lang('VideoClip.form.params_section_title') ?>" > <Forms.Field name="label" Loading Loading @@ -62,7 +64,7 @@ <Button variant="primary" type="submit" iconRight="arrow-right" class="self-end"><?= lang('VideoClip.form.submit') ?></Button> </Forms.Section> --> </Forms.Section> </form> Loading Loading
app/Resources/js/admin.ts +1 −0 Original line number Diff line number Diff line Loading @@ -18,6 +18,7 @@ import Soundbites from "./modules/Soundbites"; import ThemePicker from "./modules/ThemePicker"; import Time from "./modules/Time"; import Tooltip from "./modules/Tooltip"; import "./modules/video-clip-previewer"; import "./modules/xml-editor"; Dropdown(); Loading
app/Resources/js/modules/audio-clipper.ts +226 −62 Original line number Diff line number Diff line Loading @@ -48,6 +48,9 @@ export class AudioClipper extends LitElement { @query("#waveform") _waveformNode!: HTMLDivElement; @query(".buffering-bar") _bufferingBarNode!: HTMLCanvasElement; @property({ type: Number, attribute: "start-time" }) initStartTime = 0; Loading Loading @@ -87,15 +90,15 @@ export class AudioClipper extends LitElement { @state() _volume = 0.5; @state() _isLoading = false; @state() _seekingTime: number | null = null; @state() _wavesurfer!: WaveSurfer; @state() _isBuffering = false; _windowEvents: EventElement[] = [ { events: ["load", "resize"], Loading Loading @@ -144,21 +147,46 @@ export class AudioClipper extends LitElement { }, }, { events: ["complete"], events: ["progress"], onEvent: () => { this._isLoading = false; const context = this._bufferingBarNode.getContext("2d"); if (context) { context.fillStyle = "lightgray"; context.fillRect( 0, 0, this._bufferingBarNode.width, this._bufferingBarNode.height ); context.fillStyle = "#04AC64"; const inc = this._bufferingBarNode.width / this._audio[0].duration; for (let i = 0; i < this._audio[0].buffered.length; i++) { const startX = this._audio[0].buffered.start(i) * inc; const endX = this._audio[0].buffered.end(i) * inc; const width = endX - startX; context.fillRect(startX, 0, width, this._bufferingBarNode.height); context.rect(startX, 0, width, this._bufferingBarNode.height); } } }, }, { events: ["timeupdate"], onEvent: () => { // TODO: change this this._currentTime = this._audio[0].currentTime; // TODO: change this? this._currentTime = parseFloat(this._audio[0].currentTime.toFixed(3)); if (this._currentTime > this._clip.endTime) { this.pause(); this._audio[0].currentTime = this._clip.endTime; } else if (this._currentTime < this._clip.startTime) { this._isBuffering = true; this._audio[0].currentTime = this._clip.startTime; } else { this._isBuffering = false; this.setCurrentTime(this._currentTime); } }, Loading @@ -178,17 +206,18 @@ export class AudioClipper extends LitElement { protected firstUpdated(): void { this._audioDuration = this._audio[0].duration; this._audio[0].volume = this._volume; this._audio[0].currentTime = this._clip.startTime; this._isLoading = true; this._startTimeInput[0].hidden = true; this._durationInput[0].hidden = true; this._wavesurfer = WaveSurfer.create({ container: this._waveformNode, height: this.height, interact: false, barWidth: 4, barWidth: 2, barHeight: 1, barGap: 4, // barGap: 4, responsive: true, waveColor: "hsl(0 5% 85%)", cursorColor: "transparent", }); this._wavesurfer.load(this._audio[0].src); Loading Loading @@ -266,6 +295,11 @@ export class AudioClipper extends LitElement { if (_changedProperties.has("_clip")) { this.pause(); this.setSegmentPosition(); this._startTimeInput[0].value = this._clip.startTime.toString(); this._durationInput[0].value = ( this._clip.endTime - this._clip.startTime ).toFixed(3); this._audio[0].currentTime = this._clip.startTime; } if (_changedProperties.has("_seekingTime")) { Loading Loading @@ -293,18 +327,16 @@ export class AudioClipper extends LitElement { switch (this._action) { case ACTIONS.StretchLeft: { let startTime; let startTime = 0; 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, startTime: parseFloat(startTime.toFixed(3)), endTime: this._clip.endTime, }; break; Loading @@ -323,7 +355,7 @@ export class AudioClipper extends LitElement { this._clip = { startTime: this._clip.startTime, endTime, endTime: parseFloat(endTime.toFixed(3)), }; break; } Loading @@ -333,7 +365,7 @@ export class AudioClipper extends LitElement { } else if (seconds > this._clip.endTime) { this._seekingTime = this._clip.endTime; } else { this._seekingTime = seconds; this._seekingTime = parseFloat(seconds.toFixed(3)); } break; } Loading Loading @@ -386,6 +418,20 @@ export class AudioClipper extends LitElement { return new Date(seconds * 1000).toISOString().substr(11, 8); } trim(side: "start" | "end") { if (side === "start") { this._clip = { startTime: this._audio[0].currentTime, endTime: this._clip.endTime, }; } else { this._clip = { startTime: this._clip.startTime, endTime: this._currentTime, }; } } static styles = css` .slider-wrapper { position: relative; Loading @@ -393,6 +439,15 @@ export class AudioClipper extends LitElement { background-color: #0f172a; } .buffering-bar { position: absolute; width: 100%; height: 4px; background-color: gray; bottom: -4px; left: 0; } .slider { position: absolute; z-index: 10; Loading @@ -404,12 +459,6 @@ export class AudioClipper extends LitElement { width: 100%; } .slider__track-placeholder { width: 100%; height: 8px; background-color: #64748b; } .slider__segment--wrapper { position: absolute; height: 100%; Loading @@ -418,14 +467,17 @@ export class AudioClipper extends LitElement { .slider__segment { position: relative; display: flex; height: 100%; height: 120%; top: -10%; } .slider__segment-content { box-sizing: border-box; background-color: rgba(255, 255, 255, 0.5); height: 100%; width: 1px; border: none; border-top: 2px dashed #b91c1c; border-bottom: 2px dashed #b91c1c; } .slider__seeking-placeholder { Loading @@ -441,8 +493,9 @@ export class AudioClipper extends LitElement { position: absolute; width: 20px; height: 20px; top: -23px; top: -50%; left: -10px; margin-top: -2px; background-color: #3b82f6; border-radius: 50%; } Loading @@ -453,7 +506,7 @@ export class AudioClipper extends LitElement { width: 0px; height: 0px; bottom: -12px; left: 1px; left: 0; border: 10px solid transparent; border-top-color: transparent; border-top-style: solid; Loading @@ -464,7 +517,7 @@ export class AudioClipper extends LitElement { .slider__segment .slider__segment-handle { position: absolute; width: 1rem; height: 120%; height: 100%; background-color: #b91c1c; border: none; margin: auto 0; Loading @@ -475,7 +528,7 @@ export class AudioClipper extends LitElement { .slider__segment .slider__segment-handle::before { content: ""; position: absolute; height: 3rem; height: 50%; width: 2px; background-color: #ffffff; margin: auto; Loading @@ -487,12 +540,79 @@ export class AudioClipper extends LitElement { .slider__segment .clipper__handle-left { left: -1rem; border-radius: 0.2rem 9999px 9999px 0.2rem; border-radius: 0.2rem 0 0 0.2rem; } .slider__segment .clipper__handle-right { right: -1rem; border-radius: 9999px 0.2rem 0.2rem 9999px; border-radius: 0 0.2rem 0.2rem 0; } .toolbar { display: flex; align-items: center; padding: 0.5rem 0.5rem 0.25rem 0.5rem; justify-content: space-between; background-color: hsl(var(--color-background-elevated)); box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); border-radius: 0 0 0.25rem 0.25rem; flex-wrap: wrap; gap: 0.5rem; } .toolbar__audio-controls { display: flex; align-items: center; gap: 0.5rem; } .toolbar .toolbar__play-button { padding: 0.5rem; height: 32px; width: 32px; font-size: 1em; } .toolbar__trim-controls { display: flex; align-items: center; gap: 0.5rem; flex-shrink: 0; } .toolbar button { cursor: pointer; background-color: hsl(var(--color-accent-base)); color: hsl(var(--color-accent-contrast)); border-radius: 9999px; border: none; padding: 0.25rem 0.5rem; } .animate-spin { animation: spin 1s linear infinite; } @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } .volume { display: flex; font-size: 1.2rem; color: hsl(var(--color-accent-base)); align-items: center; gap: 0.25rem; } .range-slider { accent-color: hsl(var(--color-accent-base)); width: 100px; } `; Loading @@ -501,23 +621,9 @@ export class AudioClipper extends LitElement { <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> <div>${this._isLoading ? "loading..." : "not loading"}</div> <input type="range" id="volume" min="0" max="1" step="0.1" value="${this._volume}" @change="${this.setVolume}" /> <div class="slider-wrapper" style="height:${this.height}"> <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" Loading @@ -543,9 +649,36 @@ export class AudioClipper extends LitElement { </div> </div> </div> <canvas class="buffering-bar"></canvas> </div> <button @click="${this._isPlaying ? this.pause : this.play}"> ${this._isPlaying <div class="toolbar"> <div class="toolbar__audio-controls"> <button class="toolbar__play-button" @click="${this._isPlaying ? this.pause : this.play}" > ${this._isBuffering ? 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 viewBox="0 0 24 24" fill="currentColor" Loading @@ -569,6 +702,37 @@ export class AudioClipper extends LitElement { /> </svg>`} </button> <div class="volume"> <svg viewBox="0 0 24 24" fill="currentColor" width="1em" height="1em" > <g> <path fill="none" d="M0 0h24v24H0z" /> <path d="M8.889 16H5a1 1 0 0 1-1-1V9a1 1 0 0 1 1-1h3.889l5.294-4.332a.5.5 0 0 1 .817.387v15.89a.5.5 0 0 1-.817.387L8.89 16zm9.974.591l-1.422-1.422A3.993 3.993 0 0 0 19 12c0-1.43-.75-2.685-1.88-3.392l1.439-1.439A5.991 5.991 0 0 1 21 12c0 1.842-.83 3.49-2.137 4.591z" /> </g> </svg> <input class="range-slider" type="range" id="volume" min="0" max="1" step="0.1" value="${this._volume}" @change="${this.setVolume}" /> </div> </div> <div class="toolbar__trim-controls"> <button @click="${() => this.trim("start")}">Trim start</button> <button @click="${() => this.trim("end")}">Trim end</button> </div> </div> `; } }
app/Resources/js/modules/video-clip-previewer.ts 0 → 100644 +58 −0 Original line number Diff line number Diff line import { css, html, LitElement, TemplateResult } from "lit"; import { customElement, property, queryAssignedNodes } from "lit/decorators.js"; import { styleMap } from "lit/directives/style-map.js"; enum VideoFormats { Landscape = "landscape", Portrait = "portrait", Squared = "squared", } const formatMap = { [VideoFormats.Landscape]: "16/9", [VideoFormats.Portrait]: "9/16", [VideoFormats.Squared]: "1/1", }; @customElement("video-clip-previewer") export class VideoClipPreviewer extends LitElement { @queryAssignedNodes("preview_image", true) _previewImage!: NodeListOf<HTMLImageElement>; @property() format: VideoFormats = VideoFormats.Landscape; @property() theme = "#009486"; static styles = css` .video-background { display: grid; justify-items: center; align-items: center; background-color: black; width: 100%; aspect-ratio: 16 / 9; } .video-format { display: grid; align-items: center; justify-items: center; height: 100%; } `; render(): TemplateResult<1> { const styles = { aspectRatio: formatMap[this.format], backgroundColor: this.theme, }; return html`<div class="video-background"> <div class="video-format" style=${styleMap(styles)}> <slot name="preview_image"></slot> </div> </div>`; } }
themes/cp_admin/episode/video_clips_new.php +8 −6 Original line number Diff line number Diff line Loading @@ -10,12 +10,14 @@ <?= $this->section('content') ?> <form action="<?= route_to('video-clips-create', $podcast->id, $episode->id) ?>" method="POST" class="flex gap-4"> <form action="<?= route_to('video-clips-create', $podcast->id, $episode->id) ?>" method="POST" class="flex flex-col items-center gap-4 xl:items-start xl:flex-row"> <div class="flex-1 w-full"> <!-- <div class="h-full bg-black"></div> --> <video-clip-previewer format="portrait"> <img slot="preview_image" src="<?= $episode->cover->thumbnail_url ?>" alt="<?= $episode->cover->description ?>" /> </video-clip-previewer> <audio-clipper start-time="15" duration="10" min-duration="10" volume=".25" height="50"> <audio slot="audio" src="<?= $episode->audio->file_url ?>" class="w-full"> <audio slot="audio" src="<?= $episode->audio->file_url ?>" class="w-full" 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" /> Loading @@ -23,7 +25,7 @@ </audio-clipper> </div> <!-- <Forms.Section title="<?= lang('VideoClip.form.params_section_title') ?>" > <Forms.Section title="<?= lang('VideoClip.form.params_section_title') ?>" > <Forms.Field name="label" Loading Loading @@ -62,7 +64,7 @@ <Button variant="primary" type="submit" iconRight="arrow-right" class="self-end"><?= lang('VideoClip.form.submit') ?></Button> </Forms.Section> --> </Forms.Section> </form> Loading