diff --git a/app/assets/stylesheets/shared/form.scss b/app/assets/stylesheets/shared/form.scss index 655d12d30..6e574aa64 100644 --- a/app/assets/stylesheets/shared/form.scss +++ b/app/assets/stylesheets/shared/form.scss @@ -10,7 +10,7 @@ box-shadow: $input-focus-box-shadow; } - &.is-changed { + &.js-is-changed { border-color: $orange; &:focus-within { diff --git a/app/javascript/controllers/audio_breakpoint_controller.js b/app/javascript/controllers/audio_breakpoint_controller.js index 36ff8ca16..faeaff97b 100644 --- a/app/javascript/controllers/audio_breakpoint_controller.js +++ b/app/javascript/controllers/audio_breakpoint_controller.js @@ -34,35 +34,36 @@ export default class extends Controller { if (!this.hasStartTimeTarget) return if (this.hasInitialMarkerValue) { - const isChanged = this.startTimeValue !== (this.initialMarkerValue.startTime || 0) + const isChanged = this.hasStartTimeValue && this.startTimeValue !== (this.initialMarkerValue.startTime || 0) - this.startTimeTarget.parentNode.classList.toggle("is-changed", isChanged) + this.startTimeTarget.parentNode.classList.toggle("js-is-changed", isChanged) } else { - this.startTimeTarget.parentNode.classList.add("is-changed") + this.startTimeTarget.parentNode.classList.add("js-is-changed") } - this.startTimeTarget.placeholder = convertSecondsToDuration(this.startTimeValue) + if (this.hasStartTimeValue) { + this.startTimeTarget.placeholder = convertSecondsToDuration(this.startTimeValue) + } else if (this.idValue === 'postRoll') { + this.startTimeTarget.placeholder = convertSecondsToDuration(this.initialMarkerValue.endTime) + } } - endTimeValueChanged(newValue) { + endTimeValueChanged() { if (!this.hasEndTimeTarget) return - console.log('endTimeValueChanged', newValue, this.endTimeValue, this.hasEndTimeValue) - if (this.hasInitialMarkerValue) { - const isChanged = this.endTimeValue !== (this.initialMarkerValue.endTime || 0) + const isChanged = this.hasEndTimeValue && this.endTimeValue !== (this.initialMarkerValue.endTime || 0) - this.endTimeTarget.parentNode.classList.toggle("is-changed", isChanged) + this.endTimeTarget.parentNode.classList.toggle("js-is-changed", isChanged) if (this.hasStartTimeTarget) { const isStartTimeChanged = - this.startTimeTarget.parentNode.classList.contains("is-changed") - || (isChanged && !this.hasEndTimeValue) + this.startTimeTarget.parentNode.classList.contains("js-is-changed") || (isChanged && !this.hasEndTimeValue) - this.startTimeTarget.parentNode.classList.toggle("is-changed", isStartTimeChanged) + this.startTimeTarget.parentNode.classList.toggle("js-is-changed", isStartTimeChanged) } } else { - this.endTimeTarget.parentNode.classList.add("is-changed") + this.endTimeTarget.parentNode.classList.add("js-is-changed") } if (this.hasEndTimeValue) { @@ -72,7 +73,11 @@ export default class extends Controller { updateStartTime(newTime) { this.dispatch("marker.update", { - detail: { id: this.idValue, startTime: newTime || this.startTimeValue, endTime: this.endTimeValue }, + detail: { + id: this.idValue, + startTime: newTime || this.startTimeValue, + ...(this.hasEndTimeValue && { endTime: this.endTimeValue }), + }, }) } @@ -84,8 +89,8 @@ export default class extends Controller { this.dispatch("marker.update-start-time-to-playhead", { detail: { id: this.idValue, - ...(this.hasEndTimeValue && { endTime: this.endTimeValue }) - } + ...(this.hasEndTimeValue && { endTime: this.endTimeValue }), + }, }) } @@ -113,6 +118,14 @@ export default class extends Controller { this.updateEndTime(null) } + minEndTime() { + this.updateEndTime(0) + } + + maxStartTime() { + this.updateStartTime(Infinity) + } + play() { this.dispatch("play", { detail: { id: this.idValue } }) } diff --git a/app/javascript/controllers/audio_breakpoints_controller.js b/app/javascript/controllers/audio_breakpoints_controller.js index f3d9dc999..e7b004e41 100644 --- a/app/javascript/controllers/audio_breakpoints_controller.js +++ b/app/javascript/controllers/audio_breakpoints_controller.js @@ -20,7 +20,6 @@ export default class extends Controller { adBreaks: { type: Number, default: 1 }, adBreaksValid: { type: Boolean, default: false }, segments: Array, - markers: Array, } minTime = 0.000001 @@ -29,16 +28,6 @@ export default class extends Controller { this.initMarkers() } - /** - * Handle external changes to incoming marker values. - * Should only run when controller initializes, or we need to edit ad locations for a different episode. - * DO NOT UPDATE THIS VALUE AS A RESULT OF EDITS IN THE AD LOCATIONS UI. - */ - markersValueChanged() { - if (!this.breakpointMarkers) return - this.initMarkers() - } - durationValueChanged() { const endTimeInput = this.postRollControlTemplateTarget.content.querySelector( '[data-audio-breakpoint-target="endTime"]' @@ -47,8 +36,13 @@ export default class extends Controller { endTimeInput?.setAttribute("placeholder", convertSecondsToDuration(this.durationValue)) } + /** + * Handle external changes to incoming segments values. + * Should only run when controller initializes, or we need to edit ad locations for a different episode. + * DO NOT UPDATE THIS VALUE AS A RESULT OF EDITS IN THE AD LOCATIONS UI. + */ segmentsValueChanged(value) { - console.log("segments", value) + if (!Array.isArray(value)) return this.preRollPoint = value[0][0] this.postRollPoint = value[value.length - 1][1] @@ -60,8 +54,6 @@ export default class extends Controller { }, []), ] - console.log(this.preRollPoint, this.adBreaks, this.postRollPoint) - if (!this.breakpointMarkers) return this.initMarkers() } @@ -75,7 +67,7 @@ export default class extends Controller { * Initial render of ad markers. */ initMarkers() { - if (!this.hasMarkersValue || !this.hasAdBreaksValue || !this.adBreaksValidValue) return + if (!this.hasSegmentsValue || !this.hasAdBreaksValue || !this.adBreaksValidValue) return const preRollMarker = this.breakpointMarkers?.shift() const postRollMarker = this.breakpointMarkers?.pop() @@ -127,8 +119,6 @@ export default class extends Controller { }) } - console.log(this.initialMarkers, this.breakpointMarkers) - this.updateBreakpointMarkers() } @@ -140,8 +130,6 @@ export default class extends Controller { const breakpointMarkerIndex = this.breakpointMarkers.findIndex((marker) => marker.id === id) const breakpointMarker = this.breakpointMarkers[breakpointMarkerIndex] - console.log('updateBreakpointMarker', id, startTime, endTime, breakpointMarker) - if (!breakpointMarker) return const newStartTime = convertToSeconds(startTime) @@ -150,8 +138,8 @@ export default class extends Controller { let newBreakpointMarker = { ...breakpointMarker, changed: new Date().getMilliseconds(), - startTime: hasEndTime ? Math.max(this.minTime, Math.min(newStartTime, newEndTime)) : newStartTime, - endTime: hasEndTime ? Math.min(this.durationValue, Math.max(newStartTime, newEndTime)) : undefined, + startTime: hasEndTime ? Math.max(this.minTime, Math.min(newStartTime, newEndTime, this.durationValue)) : newStartTime, + endTime: hasEndTime ? Math.min(this.durationValue, Math.max(newStartTime, newEndTime, this.minTime)) : undefined, } const isSegment = !!newBreakpointMarker.endTime @@ -161,7 +149,6 @@ export default class extends Controller { // Prevent marker from starting before previous marker's end time. if (previousBreakpointMarker) { - console.log(newBreakpointMarker) newBreakpointMarker = { ...newBreakpointMarker, startTime: Math.max( @@ -262,8 +249,6 @@ export default class extends Controller { id !== iId && newBreakpointMarker.startTime > iStartTime && newBreakpointMarker.startTime < iEndTime ) - console.log(newBreakpointMarker, intersectingSegment, this.breakpointMarkers) - // Prevent point marker from being dropped in a segment. if (intersectingSegment) { // If restored within a segment, clear start tie so itt can be reset by user. @@ -291,10 +276,10 @@ export default class extends Controller { */ renderMarkers() { // Convert ad markers to markers array. - const markers = this.getMarkersTimesArray() + const newSegments = this.getSegmentsTimesArray() // Updated markers form input value. - this.markersInputTarget.value = markers.length ? JSON.stringify(markers) : null + this.markersInputTarget.value = newSegments.length ? JSON.stringify(newSegments) : null this.markersInputTarget.dispatchEvent(new Event("change")) // Update waveform inspector. @@ -328,7 +313,7 @@ export default class extends Controller { * Get ad markers as times only in array format. (e.g. [start, [start, end]) * @returns Array of arrays containing start and end times for markers. */ - getMarkersTimesArray() { + getSegmentsTimesArray() { return this.breakpointMarkers .filter(({ startTime }) => startTime !== undefined) .reduce((a, current, index, all) => { @@ -359,8 +344,6 @@ export default class extends Controller { const { id, labelText, startTime, endTime } = marker const initialMarker = this.initialMarkers.find(({ id: iId }) => iId === id) - console.log('renderAdMarkerControls', marker); - if (initialMarker) { control.dataset.audioBreakpointInitialMarkerValue = JSON.stringify(initialMarker) } @@ -368,12 +351,17 @@ export default class extends Controller { control.dataset.audioBreakpointIdValue = id control.dataset.audioBreakpointLabelValue = labelText - if (startTime != null) { + if (startTime != null && startTime > this.minTime && startTime < this.durationValue) { control.dataset.audioBreakpointStartTimeValue = startTime } - if (endTime != null) { + if (endTime != null && endTime > this.minTime && endTime < this.durationValue) { + control.dataset.audioBreakpointEndTimeValue = endTime + } + + if (marker.id === 'postRoll') { control.dataset.audioBreakpointEndTimeValue = endTime + control.querySelector('[data-audio-breakpoint-target="startTime"]').setAttribute('placeholder', convertSecondsToDuration(this.durationValue)) } controls.push(control) diff --git a/app/javascript/controllers/waveform_inspector_controller.js b/app/javascript/controllers/waveform_inspector_controller.js index 5ddbf5812..06ae6ec79 100644 --- a/app/javascript/controllers/waveform_inspector_controller.js +++ b/app/javascript/controllers/waveform_inspector_controller.js @@ -175,7 +175,6 @@ export default class extends Controller { if (startTime == null) return if (endTime != null) { - console.log('init segment', id, startTime, endTime) segments.push({ ...optionsDefault, id, @@ -283,8 +282,6 @@ export default class extends Controller { updateBreakpointMarkerStartTimeToPlayhead({ detail }) { const { id, endTime } = detail || {} - console.log('updateBreakpointMarkerStartTimeToPlayhead', detail) - // Dispatch marker update event. this.dispatch("marker.update", { detail: { id, startTime: this.peaks.player.getCurrentTime(), endTime } }) } diff --git a/app/javascript/custom/PrxSegmentMarker.js b/app/javascript/custom/PrxSegmentMarker.js index 4d1733d5a..99c87f3d4 100644 --- a/app/javascript/custom/PrxSegmentMarker.js +++ b/app/javascript/custom/PrxSegmentMarker.js @@ -20,36 +20,47 @@ class PrxSegmentMarker { this._paddingX = 8 this._paddingY = 8 - if (this._options.view === "zoomview") { + if (options.view === "zoomview") { const axisMarkerHeight = this._peaks.options.zoomview.axisBottomMarkerHeight || 10 const axisMarkerFontSize = this._peaks.options.zoomview.fontSize || 11 this._paddingBottom = axisMarkerHeight + axisMarkerFontSize + this._paddingY } + + if ( + options.startMarker && this._segment.id === 'preRoll' + || !options.startMarker && this._segment.id === 'postRoll' + ) { + this._disabled = true + } + + if (!options.startMarker && this._segment.id === 'preRoll') { + this._isPreRollMarker = true + } + + if (options.startMarker && this._segment.id === 'postRoll') { + this._isPostRollMarker = true + } } init(group) { - if ( - this._options.startMarker && this._segment.id === 'preRoll' - || !this._options.startMarker && this._segment.id === 'postRoll' - ) return + if (this._disabled) return // Label - if (this._options.view === "zoomview" && this._options.startMarker) { + if (this._options.view === "zoomview" && (this._options.startMarker || this._isPreRollMarker)) { // Label - create with default y, the real value is set in fitToView(). const label = new Konva.Label({ x: 8, y: 0, }) - label.add( - new Konva.Tag({ - fill: this._options.color, - lineJoin: "round", - cornerRadius: this._tagCornerRadius, - }) - ) + this._labelTag = new Konva.Tag({ + fill: this._options.color, + lineJoin: "round", + cornerRadius: this._tagCornerRadius, + }) + label.add(this._labelTag) label.add( new Text({ @@ -161,21 +172,52 @@ class PrxSegmentMarker { group.on('xChange', (evt) => { const layer = evt.currentTarget.getLayer() - self._overlay = layer.children.find((child) => child.attrs.name === 'overlay' && child.attrs.draggable) + self._canvas = layer.canvas + self._overlay = self._overlay || layer.children.find((child) => child.attrs.name === 'overlay' && child.attrs.draggable) - if (self._overlay && ['preRoll', 'postRoll'].includes(this._segment.id)) { + if (self._isPreRollMarker || self._isPostRollMarker) { self._overlay.draggable(false) } - self.updateLabelPosition(evt.newVal, layer) - self.updateTimePosition(evt.newVal, layer) + self.updateLabelPosition(evt.newVal) + self.updateTimePosition(evt.newVal) }) } - updateLabelPosition(posX, layer) { - if(!this._options.startMarker || !this._label || !this._overlay) return + updateLabelPosition(posX) { + if(!this._label || !this._overlay) return - // Position label sticky within bounds of segment overlay. + if (this._isPreRollMarker || this._isPostRollMarker) { + // Position Pre/Post roll marker label. + this.positionPreOrPostRollLabel(posX); + } else { + // Position label sticky within bounds of segment overlay. + this.positionLabelSticky(posX) + } + } + + positionPreOrPostRollLabel(posX) { + const canvasWidth = this._canvas.getWidth() + const labelWidth = this._label.getWidth() + const offset = labelWidth / 2 + let newX = -offset + let cornerRadius = this._tagCornerRadius + + if (posX <= offset) { + newX = 0 + cornerRadius = [0, this._tagCornerRadius, this._tagCornerRadius, 0] + } + + if (posX >= canvasWidth - offset) { + newX = -labelWidth + cornerRadius = [this._tagCornerRadius, 0, 0, this._tagCornerRadius] + } + + this._label.x(newX) + this._labelTag.cornerRadius(cornerRadius) + } + + positionLabelSticky(posX) { const segmentWidth = this._overlay.getWidth() const labelWidth = this._label.getWidth() const rightBound = segmentWidth - this._paddingX - labelWidth @@ -188,10 +230,10 @@ class PrxSegmentMarker { this._label.x(newX) } - updateTimePosition(posX, layer) { - if(!this._time || !layer?.canvas) return + updateTimePosition(posX) { + if(!this._time || !this._canvas) return - const canvasWidth = layer.canvas.getWidth() + const canvasWidth = this._canvas.getWidth() const timeWidth = this._time.getWidth() if ( @@ -207,6 +249,8 @@ class PrxSegmentMarker { } fitToView() { + if (this._disabled) return; + const height = this._options.layer.getHeight() this._line.points([0, 0, 0, height]) @@ -221,6 +265,8 @@ class PrxSegmentMarker { } timeUpdated(time) { + if (this._disabled) return; + if (this._time) { this._timeText.setText(this._options.layer.formatTime(time)) } diff --git a/app/javascript/util/convertToSeconds.js b/app/javascript/util/convertToSeconds.js index 048e4b359..ce51c4262 100644 --- a/app/javascript/util/convertToSeconds.js +++ b/app/javascript/util/convertToSeconds.js @@ -4,9 +4,7 @@ * @returns Value in seconds as Number, or NaN. */ export default function convertToSeconds(value) { - if (!["string", "number"].includes(typeof value)) return value - - if (typeof value === "number") return value + if (!["string"].includes(typeof value)) return value if (value.indexOf(":") != -1) { // Convert duration string to seconds. diff --git a/app/views/episode_media/segmenter/_segmenter.html.erb b/app/views/episode_media/segmenter/_segmenter.html.erb index ed29142dd..157e4dd6b 100644 --- a/app/views/episode_media/segmenter/_segmenter.html.erb +++ b/app/views/episode_media/segmenter/_segmenter.html.erb @@ -3,7 +3,6 @@ data-audio-breakpoints-label-prefix-value="<%= t(".label.prefix") %>" data-audio-breakpoints-ad-breaks-value="<%= episode.ad_breaks %>" data-audio-breakpoints-ad-breaks-valid-value="<%= episode.validate(:ad_breaks) %>" - data-audio-breakpoints-markers-value="<%= (uncut&.ad_breaks || []).to_json %>" data-audio-breakpoints-segments-value="<%= (uncut&.segmentation || []).to_json %>" data-audio-breakpoints-target="waveformInspector" data-waveform-inspector-playing-class="player--playing" @@ -21,7 +20,7 @@ audio-breakpoint:seekTo->waveform-inspector#seekToMarker "> - <%= form.text_field :segmentation, value: (uncut&.segmentation || []).to_json, class: "d-none", type: "hidden", data: {audio_breakpoints_target: "markersInput"} %> + <%= form.hidden_field :segmentation, value: uncut&.segmentation&.to_json, data: {audio_breakpoints_target: "markersInput", action: "change->unsaved#change"} %>