From 602654b99b33ee8c29da080058a0aaea976cd484 Mon Sep 17 00:00:00 2001 From: Yassine Doghri Date: Sun, 2 Jan 2022 14:11:05 +0000 Subject: [PATCH] fix(audio-clipper): add mouse position offset when stretching clip to prevent content from jumping update Forms.Section component to adapt to full width --- app/Helpers/components_helper.php | 8 +- .../MediaClipper/Config/MediaClipper.php | 2 +- app/Resources/js/admin.ts | 2 + app/Resources/js/modules/VideoClipBuilder.ts | 70 +++++++ app/Resources/js/modules/audio-clipper.ts | 172 +++++++++++++++--- .../js/modules/video-clip-previewer.ts | 90 ++++++++- .../Components/Forms/ColorRadioButton.php | 16 +- app/Views/Components/Forms/RadioButton.php | 18 +- app/Views/Components/Forms/Section.php | 2 +- modules/Admin/Config/Routes.php | 8 + .../Controllers/VideoClipsController.php | 38 +++- modules/Admin/Language/en/VideoClip.php | 9 + modules/Admin/Language/fr/VideoClip.php | 9 + themes/cp_admin/_partials/_nav_header.php | 2 +- themes/cp_admin/episode/create.php | 2 +- themes/cp_admin/episode/edit.php | 2 +- themes/cp_admin/episode/persons.php | 2 +- themes/cp_admin/episode/soundbites.php | 2 +- themes/cp_admin/episode/video_clips_list.php | 5 + themes/cp_admin/episode/video_clips_new.php | 90 ++++----- .../episode/video_clips_requirements.php | 29 +++ themes/cp_admin/fediverse/blocked_actors.php | 1 + themes/cp_admin/podcast/create.php | 2 +- themes/cp_admin/podcast/edit.php | 2 +- themes/cp_admin/podcast/import.php | 2 +- themes/cp_admin/podcast/persons.php | 2 +- themes/cp_admin/settings/general.php | 4 +- themes/cp_admin/settings/theme.php | 2 +- themes/cp_app/_admin_navbar.php | 2 +- 29 files changed, 491 insertions(+), 104 deletions(-) create mode 100644 app/Resources/js/modules/VideoClipBuilder.ts create mode 100644 themes/cp_admin/episode/video_clips_requirements.php diff --git a/app/Helpers/components_helper.php b/app/Helpers/components_helper.php index 89c5a121..2d12bc3d 100644 --- a/app/Helpers/components_helper.php +++ b/app/Helpers/components_helper.php @@ -85,7 +85,13 @@ if (! function_exists('data_table')) { $table->addRow($rowData); } } else { - return lang('Common.no_data'); + $table->addRow([ + [ + 'colspan' => count($tableHeaders), + 'class' => 'px-4 py-2 italic font-semibold text-center', + 'data' => lang('Common.no_data'), + ], + ]); } return '
' . diff --git a/app/Libraries/MediaClipper/Config/MediaClipper.php b/app/Libraries/MediaClipper/Config/MediaClipper.php index e300f24d..f4274d6d 100644 --- a/app/Libraries/MediaClipper/Config/MediaClipper.php +++ b/app/Libraries/MediaClipper/Config/MediaClipper.php @@ -213,7 +213,7 @@ class MediaClipper extends BaseConfig 'rescaleHeight' => 1200, 'x' => 0, 'y' => 600, - 'mask' => APPPATH . 'Libraries/MediaClipper/soundwaves-mask-square.png', + 'mask' => APPPATH . 'Libraries/MediaClipper/soundwaves-mask-squared.png', ], 'subtitles' => [ 'fontsize' => 20, diff --git a/app/Resources/js/admin.ts b/app/Resources/js/admin.ts index a626874e..310cc336 100644 --- a/app/Resources/js/admin.ts +++ b/app/Resources/js/admin.ts @@ -19,6 +19,7 @@ import ThemePicker from "./modules/ThemePicker"; import Time from "./modules/Time"; import Tooltip from "./modules/Tooltip"; import "./modules/video-clip-previewer"; +import VideoClipBuilder from "./modules/VideoClipBuilder"; import "./modules/xml-editor"; Dropdown(); @@ -35,3 +36,4 @@ Clipboard(); ThemePicker(); PublishMessageWarning(); HotKeys(); +VideoClipBuilder(); diff --git a/app/Resources/js/modules/VideoClipBuilder.ts b/app/Resources/js/modules/VideoClipBuilder.ts new file mode 100644 index 00000000..11c1a637 --- /dev/null +++ b/app/Resources/js/modules/VideoClipBuilder.ts @@ -0,0 +1,70 @@ +const VideoClipBuilder = (): void => { + const form = document.querySelector("form[id=new-video-clip-form]"); + + if (form) { + const videoClipPreviewer = form?.querySelector("video-clip-previewer"); + + if (videoClipPreviewer) { + const themeOptions: NodeListOf = form.querySelectorAll( + 'input[name="theme"]' + ) as NodeListOf; + const formatOptions: NodeListOf = form.querySelectorAll( + 'input[name="format"]' + ) as NodeListOf; + + const titleInput = form.querySelector( + 'input[name="label"]' + ) as HTMLInputElement; + if (titleInput) { + videoClipPreviewer.setAttribute("title", titleInput.value || ""); + titleInput.addEventListener("input", () => { + videoClipPreviewer.setAttribute("title", titleInput.value || ""); + }); + } + + let format = ( + form.querySelector('input[name="format"]:checked') as HTMLInputElement + )?.value; + videoClipPreviewer.setAttribute("format", format); + const watchFormatChange = (event: Event) => { + format = (event.target as HTMLInputElement).value; + videoClipPreviewer.setAttribute("format", format); + }; + for (let i = 0; i < formatOptions.length; i++) { + formatOptions[i].addEventListener("change", watchFormatChange); + } + + let theme = form + .querySelector('input[name="theme"]:checked') + ?.parentElement?.style.getPropertyValue("--color-accent-base"); + videoClipPreviewer.setAttribute("theme", theme || ""); + + const watchThemeChange = (event: Event) => { + theme = + ( + event.target as HTMLInputElement + ).parentElement?.style.getPropertyValue("--color-accent-base") ?? + theme; + videoClipPreviewer.setAttribute("theme", theme || ""); + }; + for (let i = 0; i < themeOptions.length; i++) { + themeOptions[i].addEventListener("change", watchThemeChange); + } + + const durationInput = form.querySelector( + 'input[name="duration"]' + ) as HTMLInputElement; + if (durationInput) { + videoClipPreviewer.setAttribute("duration", durationInput.value || "0"); + durationInput.addEventListener("change", () => { + videoClipPreviewer.setAttribute( + "duration", + durationInput.value || "0" + ); + }); + } + } + } +}; + +export default VideoClipBuilder; diff --git a/app/Resources/js/modules/audio-clipper.ts b/app/Resources/js/modules/audio-clipper.ts index 97549f7e..e77b4d2e 100644 --- a/app/Resources/js/modules/audio-clipper.ts +++ b/app/Resources/js/modules/audio-clipper.ts @@ -3,17 +3,23 @@ import { customElement, property, query, + queryAll, queryAssignedNodes, state, } from "lit/decorators.js"; import WaveSurfer from "wavesurfer.js"; -enum ACTIONS { +enum ActionType { StretchLeft, StretchRight, Seek, } +interface Action { + type: ActionType; + payload?: any; +} + interface EventElement { events: string[]; onEvent: EventListener; @@ -51,6 +57,9 @@ export class AudioClipper extends LitElement { @query(".buffering-bar") _bufferingBarNode!: HTMLCanvasElement; + @queryAll(".slider__segment-handle") + _segmentHandleNodes!: NodeListOf; + @property({ type: Number, attribute: "start-time" }) initStartTime = 0; @@ -76,7 +85,7 @@ export class AudioClipper extends LitElement { }; @state() - _action: ACTIONS | null = null; + _action: Action | null = null; @state() _audioDuration = 0; @@ -115,7 +124,7 @@ export class AudioClipper extends LitElement { onEvent: () => { if (this._action !== null) { document.body.style.cursor = ""; - if (this._action === ACTIONS.Seek && this._seekingTime) { + if (this._action.type === ActionType.Seek && this._seekingTime) { this._audio[0].currentTime = this._seekingTime; this._seekingTime = 0; } @@ -193,6 +202,31 @@ export class AudioClipper extends LitElement { }, ]; + _segmentHandleEvents: EventElement[] = [ + { + events: ["mouseenter", "focus"], + onEvent: (event: Event) => { + const timeInfoElement = ( + event.target as HTMLButtonElement + ).querySelector("span"); + if (timeInfoElement) { + timeInfoElement.style.opacity = "1"; + } + }, + }, + { + events: ["mouseleave", "blur"], + onEvent: (event: Event) => { + const timeInfoElement = ( + event.target as HTMLButtonElement + ).querySelector("span"); + if (timeInfoElement) { + timeInfoElement.style.opacity = "0"; + } + }, + }, + ]; + connectedCallback(): void { super.connectedCallback(); @@ -249,6 +283,14 @@ export class AudioClipper extends LitElement { this._audio[0].addEventListener(name, event.onEvent); }); } + + for (const event of this._segmentHandleEvents) { + event.events.forEach((name) => { + for (let i = 0; i < this._segmentHandleNodes.length; i++) { + this._segmentHandleNodes[i].addEventListener(name, event.onEvent); + } + }); + } } removeEventListeners(): void { @@ -269,6 +311,14 @@ export class AudioClipper extends LitElement { this._audio[0].removeEventListener(name, event.onEvent); }); } + + for (const event of this._segmentHandleEvents) { + event.events.forEach((name) => { + for (let i = 0; i < this._segmentHandleNodes.length; i++) { + this._segmentHandleNodes[i].addEventListener(name, event.onEvent); + } + }); + } } setSegmentPosition(): void { @@ -300,6 +350,7 @@ export class AudioClipper extends LitElement { this._durationInput[0].value = ( this._clip.endTime - this._clip.startTime ).toFixed(3); + this._durationInput[0].dispatchEvent(new Event("change")); this._audio[0].currentTime = this._clip.startTime; } if (_changedProperties.has("_seekingTime")) { @@ -318,15 +369,20 @@ export class AudioClipper extends LitElement { } private updatePosition(event: MouseEvent): void { + if (this._action === null) { + return; + } + const cursorPosition = - event.clientX - + event.clientX + + (this._action.payload?.offset || 0) - (this._sliderNode.getBoundingClientRect().left + document.documentElement.scrollLeft); const seconds = this.getSecondsFromPosition(cursorPosition); - switch (this._action) { - case ACTIONS.StretchLeft: { + switch (this._action.type) { + case ActionType.StretchLeft: { let startTime = 0; if (seconds > 0) { if (seconds > this._clip.endTime - this.minDuration) { @@ -341,7 +397,7 @@ export class AudioClipper extends LitElement { }; break; } - case ACTIONS.StretchRight: { + case ActionType.StretchRight: { let endTime; if (seconds < this._audioDuration) { if (seconds < this._clip.startTime + this.minDuration) { @@ -359,7 +415,7 @@ export class AudioClipper extends LitElement { }; break; } - case ACTIONS.Seek: { + case ActionType.Seek: { if (seconds < this._clip.startTime) { this._seekingTime = this._clip.startTime; } else if (seconds > this._clip.endTime) { @@ -401,14 +457,23 @@ export class AudioClipper extends LitElement { this._seekingNode.style.transform = `scaleX(${seekingTimePercentage})`; } - setAction(action: ACTIONS): void { - switch (action) { - case ACTIONS.StretchLeft: - case ACTIONS.StretchRight: - document.body.style.cursor = "grabbing"; + setAction(event: MouseEvent, action: Action): void { + switch (action.type) { + case ActionType.StretchLeft: + action.payload = { + offset: + this._segmentHandleNodes[0].getBoundingClientRect().right - + event.clientX, + }; + break; + case ActionType.StretchRight: + action.payload = { + offset: + this._segmentHandleNodes[1].getBoundingClientRect().left - + event.clientX, + }; break; default: - document.body.style.cursor = "default"; break; } this._action = action; @@ -421,7 +486,7 @@ export class AudioClipper extends LitElement { trim(side: "start" | "end") { if (side === "start") { this._clip = { - startTime: this._audio[0].currentTime, + startTime: parseFloat(this._audio[0].currentTime.toFixed(3)), endTime: this._clip.endTime, }; } else { @@ -498,6 +563,7 @@ export class AudioClipper extends LitElement { margin-top: -2px; background-color: #3b82f6; border-radius: 50%; + box-shadow: 0 0 0 2px #ffffff; } .slider__segment-progress-handle::after { @@ -543,6 +609,17 @@ export class AudioClipper extends LitElement { border-radius: 0.2rem 0 0 0.2rem; } + .slider__segment .slider__segment-handle span { + opacity: 0; + pointer-events: none; + position: absolute; + left: -100%; + top: -30%; + background-color: #0f172a; + color: #ffffff; + padding: 0 0.25rem; + } + .slider__segment .clipper__handle-right { right: -1rem; border-radius: 0 0.2rem 0.2rem 0; @@ -555,7 +632,7 @@ export class AudioClipper extends LitElement { 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; + border-radius: 0 0 0.75rem 0.75rem; flex-wrap: wrap; gap: 0.5rem; } @@ -587,6 +664,39 @@ export class AudioClipper extends LitElement { border-radius: 9999px; border: none; padding: 0.25rem 0.5rem; + box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); + } + + .toolbar button:hover { + background-color: hsl(var(--color-accent-hover)); + } + + .toolbar button:focus { + outline: 2px solid transparent; + outline-offset: 2px; + --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 + var(--tw-ring-offset-width) var(--tw-ring-offset-color); + --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 + calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color); + box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), + 0 0 rgba(0, 0, 0, 0); + box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), + 0 0 rgba(0, 0, 0, 0); + box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), + var(--tw-shadow, 0 0 rgba(0, 0, 0, 0)); + --tw-ring-offset-width: 2px; + --tw-ring-opacity: 1; + --tw-ring-color: hsl(var(--color-accent-base) / var(--tw-ring-opacity)); + --tw-ring-offset-color: hsl(var(--color-background-base)); + } + + .toolbar__trim-controls button { + font-weight: 600; + font-family: Inter, ui-sans-serif, system-ui, -apple-system, Segoe UI, + Roboto, Ubuntu, Cantarell, Noto Sans, sans-serif, BlinkMacSystemFont, + "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, + "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", + "Noto Color Emoji"; } .animate-spin { @@ -614,6 +724,11 @@ export class AudioClipper extends LitElement { accent-color: hsl(var(--color-accent-base)); width: 100px; } + + time { + font-size: 0.875rem; + font-family: "Mono"; + } `; render(): TemplateResult<1> { @@ -627,25 +742,33 @@ export class AudioClipper extends LitElement {
+ @mousedown="${(event: MouseEvent) => + this.setAction(event, { + type: ActionType.StretchLeft, + })}" + > + ${this.secondsToHHMMSS(this._clip.startTime)} +
+ @mousedown="${(event: MouseEvent) => + this.setAction(event, { type: ActionType.StretchRight })}" + > + ${this.secondsToHHMMSS(this._clip.endTime)} +
@@ -727,6 +850,7 @@ export class AudioClipper extends LitElement { @change="${this.setVolume}" /> +
diff --git a/app/Resources/js/modules/video-clip-previewer.ts b/app/Resources/js/modules/video-clip-previewer.ts index 1b9b3dc8..faa138af 100644 --- a/app/Resources/js/modules/video-clip-previewer.ts +++ b/app/Resources/js/modules/video-clip-previewer.ts @@ -1,5 +1,10 @@ import { css, html, LitElement, TemplateResult } from "lit"; -import { customElement, property, queryAssignedNodes } from "lit/decorators.js"; +import { + customElement, + property, + queryAssignedNodes, + state, +} from "lit/decorators.js"; import { styleMap } from "lit/directives/style-map.js"; enum VideoFormats { @@ -17,40 +22,115 @@ const formatMap = { @customElement("video-clip-previewer") export class VideoClipPreviewer extends LitElement { @queryAssignedNodes("preview_image", true) - _previewImage!: NodeListOf; + _image!: NodeListOf; @property() - format: VideoFormats = VideoFormats.Landscape; + title = ""; @property() - theme = "#009486"; + format: VideoFormats = VideoFormats.Portrait; + + @property() + theme = "173 44% 96%"; + + @property({ type: Number }) + duration!: number; + + @state() + _previewImage!: HTMLImageElement; + + protected firstUpdated(): void { + this._previewImage = this._image[0].cloneNode(true) as HTMLImageElement; + this._previewImage.classList.add("preview-bg"); + } + + private secondsToHHMMSS(seconds: number) { + // Adapted from https://stackoverflow.com/a/34841026 + const h = Math.floor(seconds / 3600); + const min = Math.floor(seconds / 60) % 60; + const s = seconds % 60; + + return [h, min, s] + .map((v) => (v < 10 ? "0" + v : v)) + .filter((v, i) => v !== "00" || i > 0) + .join(":"); + } static styles = css` + .metadata { + position: absolute; + top: 1rem; + left: 1.5rem; + color: #ffffff; + display: flex; + flex-direction: column; + } + + .title { + font-family: "Kumbh Sans"; + font-weight: 900; + font-size: 1.5rem; + text-shadow: 2px 3px 5px rgba(0, 0, 0, 0.5); + } + + .duration { + font-family: "Inter"; + font-weight: 600; + } + + .preview-bg { + position: absolute; + background-color: red; + width: 100%; + object-fit: cover; + filter: blur(30px); + opacity: 0.5; + } + .video-background { + position: relative; display: grid; justify-items: center; align-items: center; background-color: black; width: 100%; aspect-ratio: 16 / 9; + border-radius: 0.75rem 0.75rem 0 0; + overflow: hidden; } .video-format { + z-index: 10; display: grid; align-items: center; justify-items: center; height: 100%; + border: 4px solid hsl(0 0% 100% / 0.5); + transition: 300ms ease-in-out aspect-ratio; + } + + ::slotted(img) { + border-radius: 0.5rem; + box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), + 0 2px 4px -2px rgb(0 0 0 / 0.1); } `; render(): TemplateResult<1> { const styles = { aspectRatio: formatMap[this.format], - backgroundColor: this.theme, + backgroundColor: `hsl(${this.theme})`, }; return html`
+ ${this._previewImage}
+
`; diff --git a/app/Views/Components/Forms/ColorRadioButton.php b/app/Views/Components/Forms/ColorRadioButton.php index a3454f54..470200ef 100644 --- a/app/Views/Components/Forms/ColorRadioButton.php +++ b/app/Views/Components/Forms/ColorRadioButton.php @@ -17,12 +17,18 @@ class ColorRadioButton extends FormComponent public function render(): string { + $data = [ + 'id' => $this->value, + 'name' => $this->name, + 'class' => 'color-radio-btn', + ]; + + if ($this->required) { + $data['required'] = 'required'; + } + $radioInput = form_radio( - [ - 'id' => $this->value, - 'name' => $this->name, - 'class' => 'color-radio-btn', - ], + $data, $this->value, old($this->name) ? old($this->name) === $this->value : $this->isChecked, ); diff --git a/app/Views/Components/Forms/RadioButton.php b/app/Views/Components/Forms/RadioButton.php index f7d7015a..1772c3d4 100644 --- a/app/Views/Components/Forms/RadioButton.php +++ b/app/Views/Components/Forms/RadioButton.php @@ -17,12 +17,18 @@ class RadioButton extends FormComponent public function render(): string { + $data = [ + 'id' => $this->value, + 'name' => $this->name, + 'class' => 'form-radio-btn bg-elevated', + ]; + + if ($this->required) { + $data['required'] = 'required'; + } + $radioInput = form_radio( - [ - 'id' => $this->value, - 'name' => $this->name, - 'class' => 'form-radio-btn bg-elevated', - ], + $data, $this->value, old($this->name) ? old($this->name) === $this->value : $this->isChecked, ); @@ -30,7 +36,7 @@ class RadioButton extends FormComponent $hint = $this->hint ? hint_tooltip($this->hint, 'ml-1 text-base') : ''; return << +
{$radioInput}
diff --git a/app/Views/Components/Forms/Section.php b/app/Views/Components/Forms/Section.php index 60279a25..cf736744 100644 --- a/app/Views/Components/Forms/Section.php +++ b/app/Views/Components/Forms/Section.php @@ -19,7 +19,7 @@ class Section extends Component $subtitle = $this->subtitle === null ? '' : '

' . $this->subtitle . '

'; return << +
{$this->title} {$subtitle}
{$this->slot}
diff --git a/modules/Admin/Config/Routes.php b/modules/Admin/Config/Routes.php index a41bdb5d..dcaf460e 100644 --- a/modules/Admin/Config/Routes.php +++ b/modules/Admin/Config/Routes.php @@ -387,6 +387,14 @@ $routes->group( 'filter' => 'permission:podcast_episodes-edit', ], ); + $routes->get( + 'video-clips/(:num)/retry', + 'VideoClipsController::retry/$1/$2/$3', + [ + 'as' => 'video-clip-retry', + 'filter' => 'permission:podcast_episodes-edit', + ], + ); $routes->get( 'video-clips/(:num)/delete', 'VideoClipsController::delete/$1/$2/$3', diff --git a/modules/Admin/Controllers/VideoClipsController.php b/modules/Admin/Controllers/VideoClipsController.php index d24b1ccc..712377e8 100644 --- a/modules/Admin/Controllers/VideoClipsController.php +++ b/modules/Admin/Controllers/VideoClipsController.php @@ -108,8 +108,6 @@ class VideoClipsController extends BaseController public function create(): string { - helper('form'); - $data = [ 'podcast' => $this->podcast, 'episode' => $this->episode, @@ -120,7 +118,22 @@ class VideoClipsController extends BaseController 1 => $this->episode->title, ]); - $this->response->setHeader('Accept-Ranges', 'bytes'); + // First, check that requirements to create a video clip are met + $ffmpeg = trim(shell_exec('type -P ffmpeg')); + $checks = [ + 'ffmpeg' => ! empty($ffmpeg), + 'gd' => extension_loaded('gd'), + 'freetype' => extension_loaded('gd') && gd_info()['FreeType Support'], + 'transcript' => $this->episode->transcript !== null, + ]; + + if (in_array(false, $checks, true)) { + $data['checks'] = $checks; + + return view('episode/video_clips_requirements', $data); + } + + helper('form'); return view('episode/video_clips_new', $data); } @@ -171,6 +184,23 @@ class VideoClipsController extends BaseController ); } + public function retry(string $videoClipId): RedirectResponse + { + $videoClip = (new ClipModel())->getVideoClipById((int) $videoClipId); + + if ($videoClip === null) { + throw PageNotFoundException::forPageNotFound(); + } + + (new ClipModel())->update($videoClip->id, [ + 'status' => 'queued', + 'job_started_at' => null, + 'job_ended_at' => null, + ]); + + return redirect()->back(); + } + public function delete(string $videoClipId): RedirectResponse { $videoClip = (new ClipModel())->getVideoClipById((int) $videoClipId); @@ -181,7 +211,7 @@ class VideoClipsController extends BaseController if ($videoClip->media === null) { // delete Clip directly - (new ClipModel())->delete($videoClipId); + (new ClipModel())->delete($videoClip->id); } else { $mediaModel = new MediaModel(); if (! $mediaModel->deleteMedia($videoClip->media)) { diff --git a/modules/Admin/Language/en/VideoClip.php b/modules/Admin/Language/en/VideoClip.php index 49689c67..ef936401 100644 --- a/modules/Admin/Language/en/VideoClip.php +++ b/modules/Admin/Language/en/VideoClip.php @@ -31,6 +31,7 @@ return [ 'download_clip' => 'Download clip', 'create' => 'New video clip', 'go_to_page' => 'Go to clip page', + 'retry' => 'Retry clip generation', 'delete' => 'Delete clip', 'logs' => 'Job logs', 'form' => [ @@ -51,4 +52,12 @@ return [ 'duration' => 'Duration', 'submit' => 'Create video clip', ], + 'requirements' => [ + 'title' => 'Missing requirements', + 'missing' => 'You have missing requirements. Make sure to add all the required items to be allowed creating a video for this episode!', + 'ffmpeg' => 'FFmpeg', + 'gd' => 'Graphics Draw (GD)', + 'freetype' => 'Freetype library for GD', + 'transcript' => 'Transcript file (.srt)', + ], ]; diff --git a/modules/Admin/Language/fr/VideoClip.php b/modules/Admin/Language/fr/VideoClip.php index 6f4bc60e..c467714f 100644 --- a/modules/Admin/Language/fr/VideoClip.php +++ b/modules/Admin/Language/fr/VideoClip.php @@ -31,6 +31,7 @@ return [ 'download_clip' => 'Télécharger l’extrait', 'create' => 'Nouvel extrait vidéo', 'go_to_page' => 'Aller à la page de l’extrait', + 'retry' => 'Relancer la génération de l’extrait', 'delete' => 'Supprimer l’extrait', 'logs' => 'Historique d’exécution', 'form' => [ @@ -51,4 +52,12 @@ return [ 'duration' => 'Durée', 'submit' => 'Créer un extrait vidéo', ], + 'requirements' => [ + 'title' => 'Outils manquants', + 'missing' => 'Il vous manque des outils. Assurez vous d’avoir ajouté tous les outils nécessaires pour accéder au fomulaire de génération d’extrait vidéo !', + 'ffmpeg' => 'FFmpeg', + 'gd' => 'Graphics Draw (GD)', + 'freetype' => 'Librairie Freetype pour GD', + 'transcript' => 'Fichier de transcription (.srt)', + ], ]; diff --git a/themes/cp_admin/_partials/_nav_header.php b/themes/cp_admin/_partials/_nav_header.php index 107e8723..0631b61a 100644 --- a/themes/cp_admin/_partials/_nav_header.php +++ b/themes/cp_admin/_partials/_nav_header.php @@ -36,7 +36,7 @@ $interactButtons .= << - {$userPodcast->title}{$checkMark} +
{$userPodcast->title}{$checkMark}
CODE_SAMPLE; } diff --git a/themes/cp_admin/episode/create.php b/themes/cp_admin/episode/create.php index 617a2158..fdc20326 100644 --- a/themes/cp_admin/episode/create.php +++ b/themes/cp_admin/episode/create.php @@ -13,7 +13,7 @@ -
+ diff --git a/themes/cp_admin/episode/edit.php b/themes/cp_admin/episode/edit.php index 1c0add5d..acc4eb42 100644 --- a/themes/cp_admin/episode/edit.php +++ b/themes/cp_admin/episode/edit.php @@ -17,7 +17,7 @@ - + diff --git a/themes/cp_admin/episode/persons.php b/themes/cp_admin/episode/persons.php index 516f7ca0..534d0af9 100644 --- a/themes/cp_admin/episode/persons.php +++ b/themes/cp_admin/episode/persons.php @@ -14,7 +14,7 @@ section('content') ?> - + section('content') ?> - + lang('VideoClip.go_to_page'), 'uri' => route_to('video-clip', $videoClip->podcast_id, $videoClip->episode_id, $videoClip->id), ], + [ + 'type' => 'link', + 'title' => lang('VideoClip.retry'), + 'uri' => route_to('video-clip-retry', $videoClip->podcast_id, $videoClip->episode_id, $videoClip->id), + ], [ 'type' => 'separator', ], diff --git a/themes/cp_admin/episode/video_clips_new.php b/themes/cp_admin/episode/video_clips_new.php index 97a55476..dbfcd9fc 100644 --- a/themes/cp_admin/episode/video_clips_new.php +++ b/themes/cp_admin/episode/video_clips_new.php @@ -10,13 +10,13 @@ section('content') ?> - + -
- +
+ <?= $episode->cover->description ?> - + @@ -25,47 +25,49 @@
- - - - -
- - - - -
- -
- -
- themes as $themeName => $colors): ?> - - +
+ + +
+ + + + +
+
+ +
+ themes as $themeName => $colors): ?> + + +
+
+
+
-
- - - -
- endSection() ?> diff --git a/themes/cp_admin/episode/video_clips_requirements.php b/themes/cp_admin/episode/video_clips_requirements.php new file mode 100644 index 00000000..f9c714aa --- /dev/null +++ b/themes/cp_admin/episode/video_clips_requirements.php @@ -0,0 +1,29 @@ +extend('_layout') ?> + +section('title') ?> + +endSection() ?> + +section('pageTitle') ?> + +endSection() ?> + +section('content') ?> + +
+
+ +

+
+ $value): ?> + +
+ +
+ + +
+ +
+ +endSection() ?> diff --git a/themes/cp_admin/fediverse/blocked_actors.php b/themes/cp_admin/fediverse/blocked_actors.php index 2d4bf514..3446545a 100644 --- a/themes/cp_admin/fediverse/blocked_actors.php +++ b/themes/cp_admin/fediverse/blocked_actors.php @@ -40,6 +40,7 @@ ], ], $blockedActors, + 'mt-8' ) ?> diff --git a/themes/cp_admin/podcast/create.php b/themes/cp_admin/podcast/create.php index 452ff8d5..938121c1 100644 --- a/themes/cp_admin/podcast/create.php +++ b/themes/cp_admin/podcast/create.php @@ -14,7 +14,7 @@ section('content') ?> -
+
-
+
- + section('content') ?> - + section('content') ?>
- + - + section('content') ?> - + - {$userPodcast->title}{$checkMark} +
{$userPodcast->title}{$checkMark}
CODE_SAMPLE; }