Loading app/Helpers/components_helper.php +7 −1 Original line number Diff line number Diff line Loading @@ -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 '<div class="overflow-x-auto rounded-lg bg-elevated border-3 border-subtle ' . $class . '" >' . Loading app/Libraries/MediaClipper/Config/MediaClipper.php +1 −1 Original line number Diff line number Diff line Loading @@ -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, Loading app/Resources/js/admin.ts +2 −0 Original line number Diff line number Diff line Loading @@ -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(); Loading @@ -35,3 +36,4 @@ Clipboard(); ThemePicker(); PublishMessageWarning(); HotKeys(); VideoClipBuilder(); app/Resources/js/modules/VideoClipBuilder.ts 0 → 100644 +70 −0 Original line number Diff line number Diff line 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<HTMLInputElement> = form.querySelectorAll( 'input[name="theme"]' ) as NodeListOf<HTMLInputElement>; const formatOptions: NodeListOf<HTMLInputElement> = form.querySelectorAll( 'input[name="format"]' ) as NodeListOf<HTMLInputElement>; 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; app/Resources/js/modules/audio-clipper.ts +148 −24 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -51,6 +57,9 @@ export class AudioClipper extends LitElement { @query(".buffering-bar") _bufferingBarNode!: HTMLCanvasElement; @queryAll(".slider__segment-handle") _segmentHandleNodes!: NodeListOf<HTMLButtonElement>; @property({ type: Number, attribute: "start-time" }) initStartTime = 0; Loading @@ -76,7 +85,7 @@ export class AudioClipper extends LitElement { }; @state() _action: ACTIONS | null = null; _action: Action | null = null; @state() _audioDuration = 0; Loading Loading @@ -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; } Loading Loading @@ -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(); Loading Loading @@ -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 { Loading @@ -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 { Loading Loading @@ -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")) { Loading @@ -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) { Loading @@ -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) { Loading @@ -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) { Loading Loading @@ -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; Loading @@ -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 { Loading Loading @@ -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 { Loading Loading @@ -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; Loading @@ -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; } Loading Loading @@ -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 { Loading Loading @@ -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> { Loading @@ -627,25 +742,33 @@ export class AudioClipper extends LitElement { <div class="slider__segment--wrapper"> <div class="slider__segment-progress-handle" @mousedown="${() => this.setAction(ACTIONS.Seek)}" @mousedown="${(event: MouseEvent) => this.setAction(event, { type: ActionType.Seek })}" ></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> @mousedown="${(event: MouseEvent) => this.setAction(event, { type: ActionType.StretchLeft, })}" > <span>${this.secondsToHHMMSS(this._clip.startTime)}</span> </button> <div class="slider__seeking-placeholder"></div> <div class="slider__segment-content" @mousedown="${() => this.setAction(ACTIONS.Seek)}" @mousedown="${(event: MouseEvent) => this.setAction(event, { type: ActionType.Seek })}" @click="${(event: MouseEvent) => this.goTo(event)}" ></div> <button class="slider__segment-handle clipper__handle-right" title="${this.secondsToHHMMSS(this._clip.endTime)}" @mousedown="${() => this.setAction(ACTIONS.StretchRight)}" ></button> @mousedown="${(event: MouseEvent) => this.setAction(event, { type: ActionType.StretchRight })}" > <span>${this.secondsToHHMMSS(this._clip.endTime)}</span> </button> </div> </div> </div> Loading Loading @@ -727,6 +850,7 @@ export class AudioClipper extends LitElement { @change="${this.setVolume}" /> </div> <time>${this.secondsToHHMMSS(this._currentTime)}</time> </div> <div class="toolbar__trim-controls"> <button @click="${() => this.trim("start")}">Trim start</button> Loading Loading
app/Helpers/components_helper.php +7 −1 Original line number Diff line number Diff line Loading @@ -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 '<div class="overflow-x-auto rounded-lg bg-elevated border-3 border-subtle ' . $class . '" >' . Loading
app/Libraries/MediaClipper/Config/MediaClipper.php +1 −1 Original line number Diff line number Diff line Loading @@ -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, Loading
app/Resources/js/admin.ts +2 −0 Original line number Diff line number Diff line Loading @@ -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(); Loading @@ -35,3 +36,4 @@ Clipboard(); ThemePicker(); PublishMessageWarning(); HotKeys(); VideoClipBuilder();
app/Resources/js/modules/VideoClipBuilder.ts 0 → 100644 +70 −0 Original line number Diff line number Diff line 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<HTMLInputElement> = form.querySelectorAll( 'input[name="theme"]' ) as NodeListOf<HTMLInputElement>; const formatOptions: NodeListOf<HTMLInputElement> = form.querySelectorAll( 'input[name="format"]' ) as NodeListOf<HTMLInputElement>; 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;
app/Resources/js/modules/audio-clipper.ts +148 −24 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -51,6 +57,9 @@ export class AudioClipper extends LitElement { @query(".buffering-bar") _bufferingBarNode!: HTMLCanvasElement; @queryAll(".slider__segment-handle") _segmentHandleNodes!: NodeListOf<HTMLButtonElement>; @property({ type: Number, attribute: "start-time" }) initStartTime = 0; Loading @@ -76,7 +85,7 @@ export class AudioClipper extends LitElement { }; @state() _action: ACTIONS | null = null; _action: Action | null = null; @state() _audioDuration = 0; Loading Loading @@ -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; } Loading Loading @@ -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(); Loading Loading @@ -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 { Loading @@ -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 { Loading Loading @@ -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")) { Loading @@ -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) { Loading @@ -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) { Loading @@ -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) { Loading Loading @@ -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; Loading @@ -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 { Loading Loading @@ -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 { Loading Loading @@ -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; Loading @@ -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; } Loading Loading @@ -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 { Loading Loading @@ -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> { Loading @@ -627,25 +742,33 @@ export class AudioClipper extends LitElement { <div class="slider__segment--wrapper"> <div class="slider__segment-progress-handle" @mousedown="${() => this.setAction(ACTIONS.Seek)}" @mousedown="${(event: MouseEvent) => this.setAction(event, { type: ActionType.Seek })}" ></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> @mousedown="${(event: MouseEvent) => this.setAction(event, { type: ActionType.StretchLeft, })}" > <span>${this.secondsToHHMMSS(this._clip.startTime)}</span> </button> <div class="slider__seeking-placeholder"></div> <div class="slider__segment-content" @mousedown="${() => this.setAction(ACTIONS.Seek)}" @mousedown="${(event: MouseEvent) => this.setAction(event, { type: ActionType.Seek })}" @click="${(event: MouseEvent) => this.goTo(event)}" ></div> <button class="slider__segment-handle clipper__handle-right" title="${this.secondsToHHMMSS(this._clip.endTime)}" @mousedown="${() => this.setAction(ACTIONS.StretchRight)}" ></button> @mousedown="${(event: MouseEvent) => this.setAction(event, { type: ActionType.StretchRight })}" > <span>${this.secondsToHHMMSS(this._clip.endTime)}</span> </button> </div> </div> </div> Loading Loading @@ -727,6 +850,7 @@ export class AudioClipper extends LitElement { @change="${this.setVolume}" /> </div> <time>${this.secondsToHHMMSS(this._currentTime)}</time> </div> <div class="toolbar__trim-controls"> <button @click="${() => this.trim("start")}">Trim start</button> Loading