diff --git a/package.json b/package.json index 0a453bd9..2126cdfa 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "chorus", "private": true, "type": "module", - "version": "1.17.1", + "version": "1.18.0", "scripts": { "build": "rollup -c", "watch": "rollup -c -w" diff --git a/src/components/speed/speed-range.js b/src/components/speed/speed-range.js index 18f7cf4e..13ffd20c 100644 --- a/src/components/speed/speed-range.js +++ b/src/components/speed/speed-range.js @@ -8,7 +8,7 @@ export const createSpeedRange = () => ` ` -
- ${tag} - ${text} +const createTag = ({ tag, id, style = '' }) => ` +
+ ${tag} +
` export const createSpeedToggler = () => ` -
+
- ${createTag({ tag: 'T', id: 'speed-track-value', text: '1x', style: 'margin-bottom:4px' })} - ${createTag({ tag: 'G', id: 'speed-global-value', text: '1x' })} + ${createTag({ tag: 'Track', id: 'speed-track-value', style: 'margin-bottom:4px' })} + ${createTag({ tag: 'Global', id: 'speed-global-value' })}
diff --git a/src/data/song-state.js b/src/data/song-state.js index e8be8f0a..476967de 100644 --- a/src/data/song-state.js +++ b/src/data/song-state.js @@ -14,7 +14,7 @@ const sharedSnipValues = () => { endTime: parseInt(endTime, 10), startTime: parseInt(startTime, 10), preservesPitch: parseInt(preservesPitch, 10) == 1, - playbackRate: parseFloat(playbackRate) / 100, + playbackRate: parseFloat(playbackRate) / 1000, } } diff --git a/src/manifest.chrome.json b/src/manifest.chrome.json index f93ca80a..abb8e387 100644 --- a/src/manifest.chrome.json +++ b/src/manifest.chrome.json @@ -2,7 +2,7 @@ "short_name": "Chorus", "name": "Chorus - Spotify Enhancer", "description": "Enhance Spotify with controls to save favourite snips, auto-skip tracks, and set global and custom speed. More to come!", - "version": "1.17.1", + "version": "1.18.0", "manifest_version": 3, "author": "cdrani", "action": { diff --git a/src/manifest.firefox.json b/src/manifest.firefox.json index ea122b10..8419291f 100644 --- a/src/manifest.firefox.json +++ b/src/manifest.firefox.json @@ -2,7 +2,7 @@ "short_name": "Chorus", "name": "Chorus - Spotify Enhancer", "description": "Enhance Spotify with controls to save favourite snips, auto-skip tracks, and set global and custom speed. More to come!", - "version": "1.17.1", + "version": "1.18.0", "manifest_version": 3, "author": "cdrani", "action": { diff --git a/src/models/range/range.js b/src/models/range/range.js index 1a9b4c12..2df6a3c2 100644 --- a/src/models/range/range.js +++ b/src/models/range/range.js @@ -1,20 +1,16 @@ export default class RangeSlider { - #video - #data - constructor(video) { - this.#video = video + this._delay = 50 + this._video = video + this._data = null + this._delayTimeout = null } init(data) { - if (!this.#data) { - this.#setupEvents() - } + if (!this._data) this.#setupEvents() const { track, globals, preferredRate, preferredPitch } = data - const { - input, speedTrackValue, speedGlobalValue, speedCheckbox, pitchCheckbox - } = this.elements + const { input, speedTrackValue, speedGlobalValue, speedCheckbox, pitchCheckbox } = this.elements input.value = preferredRate this.#setSpeedValue({ playbackRate: preferredRate, preservesPitch: preferredPitch }) @@ -27,29 +23,29 @@ export default class RangeSlider { this.#setCheckedUI({ speedChecked, pitchChecked }) - speedTrackValue.textContent = `${this.#padValue(track?.playbackRate || 1)}x` - speedGlobalValue.textContent = `${this.#padValue(globals?.playbackRate || 1)}x` + speedTrackValue.value = this.#padValue(track?.playbackRate || 1) + speedGlobalValue.value = this.#padValue(globals?.playbackRate || 1) this.#hightlightSpeedValue(speedChecked) - this.#data = data + this._data = data } // TODO: move into utils - #padValue(value, decimalPlace = 2) { - if (isNaN(parseFloat(value))) { - return value - } + #padValue(value, decimalPlace = 3) { + if (isNaN(parseFloat(value))) return value - return parseFloat(value).toFixed(decimalPlace); + return parseFloat(value).toFixed(decimalPlace) } - + #setupEvents() { - const { - input, thumb, pitchToggleButton, speedToggleButton, + const { + input, thumb, speedGlobalValue, speedTrackValue, pitchToggleButton, speedToggleButton } = this.elements - input.oninput = () => this.#setSpeedValue({}) + input.oninput = () => this.#handleSlider() + speedTrackValue.onchange = (e) => this.#handleInput(e.target.value) + speedGlobalValue.onchange = (e) => this.#handleInput(e.target.value) input.addEventListener('mouseover', () => thumb.classList.add('hover')) input.addEventListener('mouseout', () => thumb.classList.remove('hover')) @@ -60,56 +56,79 @@ export default class RangeSlider { pitchToggleButton.onclick = () => this.#togglePitchCheckbox() } + #isValidRate(value) { + if (isNaN(value) || value < 0.1 || value > 4) return false + + return true + } + + #handleInput(inputValue) { + const parsedValue = parseFloat(inputValue) + const isValid = this.#isValidRate(parsedValue) + const currentValue = this.elements.input.value + const playbackRate = isValid ? this.#padValue(parsedValue) : this.#padValue(currentValue) + + this.elements.input.value = playbackRate + this.#setSpeedValue({ playbackRate }) + } + + #handleSlider() { + if (this._delayTimeout) clearTimeout(this._delayTimeout) + this._delayTimeout = setTimeout(() => this.#setSpeedValue({}), this._delay) + } + #setCheckedUI({ speedChecked, pitchChecked }) { - const { - speedLabel, - speedCheckbox, speedToggleOn, speedToggleOff, - pitchCheckbox, pitchToggleOn, pitchToggleOff - } = this.elements + const { pitchToggleOn, pitchToggleOff, speedToggleOn, speedToggleOff } = this.elements speedToggleOn.style.display = speedChecked ? 'block' : 'none' speedToggleOff.style.display = speedChecked ? 'none' : 'block' - pitchToggleOn.style.display = pitchChecked ? 'block' : 'none' pitchToggleOff.style.display = pitchChecked ? 'none' : 'block' - speedLabel.textContent = speedChecked ? 'Global Speed' : 'Track Speed' + const { pitchCheckbox, speedLabel, speedCheckbox } = this.elements + speedLabel.textContent = speedChecked ? 'Global Speed' : 'Track Speed' speedCheckbox.checked = speedChecked pitchCheckbox.checked = pitchChecked } #toggleSpeedCheckbox() { - const { - input, speedCheckbox, speedTrackValue, speedGlobalValue, - pitchCheckbox - } = this.elements + const { input, speedCheckbox, speedTrackValue, speedGlobalValue, pitchCheckbox } = this.elements speedCheckbox.checked = !speedCheckbox.checked const { checked } = speedCheckbox this.#setCheckedUI({ speedChecked: checked, pitchChecked: pitchCheckbox.checked }) - const { track, globals } = this.#data + const { track, globals } = this._data input.value = checked - ? parseFloat(speedGlobalValue.textContent) || globals?.playbackRate || 1 - : parseFloat(speedTrackValue.textContent)|| track?.playbackRate || 1 + ? parseFloat(speedGlobalValue.value) || globals?.playbackRate || 1 + : parseFloat(speedTrackValue.value)|| track?.playbackRate || 1 - this.#video.currentSpeed = input.value + this._video.currentSpeed = input.value this.#hightlightSpeedValue(checked) this.#setSpeedValue({ playbackRate: input.value }) } - #hightlightSpeedValue(checked) { + #hightlightSpeedValue(speedChecked) { const { speedTrackValue, speedGlobalValue } = this.elements - if (checked) { - speedGlobalValue.parentElement.style.background = 'green' - speedTrackValue.parentElement.style.background = 'unset' + speedGlobalValue.disabled = !speedChecked + speedTrackValue.disabled = speedChecked + + speedGlobalValue.style.backgroundColor = speedChecked ? 'green' : 'unset' + speedGlobalValue.style.outline = speedChecked ? 'solid 1px white' : '' + + speedTrackValue.style.backgroundColor = !speedChecked ? 'green' : 'unset' + speedTrackValue.style.outline = !speedChecked ? 'solid 1px white' : '' + + if (speedChecked) { + speedGlobalValue.focus() + speedTrackValue.blur() } else { - speedGlobalValue.parentElement.style.background = 'unset' - speedTrackValue.parentElement.style.background = 'green' + speedGlobalValue.blur() + speedTrackValue.focus() } } @@ -121,7 +140,7 @@ export default class RangeSlider { pitchToggleOn.style.display = checked ? 'block' : 'none' pitchToggleOff.style.display = checked ? 'none' : 'block' - this.#video.preservesPitch = checked + this._video.preservesPitch = checked } get elements() { @@ -150,11 +169,7 @@ export default class RangeSlider { } #setSpeedValue({ playbackRate, preservesPitch }) { - const { - input, speedCheckbox, pitchCheckbox, range, thumb, - speedTrackValue, speedGlobalValue, - minOutput, maxOutput - } = this.elements + const { input, speedCheckbox, pitchCheckbox, range, thumb } = this.elements const value = playbackRate ?? input.value const pitchPreserved = preservesPitch ?? pitchCheckbox?.checked @@ -163,17 +178,16 @@ export default class RangeSlider { thumb.style.right = `${100 - percent}%` range.style.right = `${100 - percent}%` + const { speedTrackValue, speedGlobalValue, minOutput, maxOutput } = this.elements + minOutput.textContent = `${input.min}x` maxOutput.textContent = `${input.max}x` - if (speedCheckbox?.checked) { - speedGlobalValue.textContent = ` ${this.#padValue(value)}x` - } else { - speedTrackValue.textContent = ` ${this.#padValue(value)}x` - } + if (speedCheckbox?.checked) (speedGlobalValue.value = this.#padValue(value)) + if (!speedCheckbox?.checked) (speedTrackValue.value = this.#padValue(value)) - this.#video.playbackRate = value - this.#video.currentSpeed = value - this.#video.preservesPitch = pitchPreserved + this._video.playbackRate = value + this._video.currentSpeed = value + this._video.preservesPitch = pitchPreserved } } diff --git a/src/models/snip/snip.js b/src/models/snip/snip.js index e807fe7a..6f47f06d 100644 --- a/src/models/snip/snip.js +++ b/src/models/snip/snip.js @@ -54,7 +54,7 @@ export default class Snip { async _share() { const { startTime, endTime, playbackRate = '1.00', preservesPitch = true } = await this.read() const pitch = preservesPitch ? 1 : 0 - const rate = parseFloat(playbackRate) * 100 + const rate = parseFloat(playbackRate) * 1000 const { tempEndTime = startTime, tempStartTime = endTime } = this.tempShareTimes diff --git a/src/styles.css b/src/styles.css index 1cfd282a..46efd9f7 100644 --- a/src/styles.css +++ b/src/styles.css @@ -247,6 +247,7 @@ input[type=number]::-webkit-outer-spin-button { width: 12px; height: 12px; border-radius: 50%; + cursor: grab; } .input[type="range"]::-webkit-slider-thumb { @@ -255,6 +256,7 @@ input[type=number]::-webkit-outer-spin-button { width: 12px; height: 12px; border-radius: 50%; + cursor: grab; } .slider>.thumb.hover { diff --git a/src/utils/higlight.js b/src/utils/higlight.js index e7ab7f4d..e5a188a3 100644 --- a/src/utils/higlight.js +++ b/src/utils/higlight.js @@ -4,7 +4,7 @@ const isHighlightable = ({ playbackRate = '1', preservesPitch = true, }) => ( - isSnip || isSkipped || !preservesPitch || playbackRate !== '1' + isSnip || isSkipped || !preservesPitch || !['1', '1.000', '1000'].includes(playbackRate) ) export const highlightElement = ({ diff --git a/src/utils/song.js b/src/utils/song.js index 5ba72d83..73c0d5a2 100644 --- a/src/utils/song.js +++ b/src/utils/song.js @@ -62,7 +62,9 @@ const getTrackId = row => { const getArtists = row => { const artistsList = row.querySelectorAll('span > div > a') // Here means we are at artist page and can get name from h1 - if (!artistsList.length) return document.querySelector('span[data-testid="entityTitle"] > h1').textContent + if (!artistsList.length) { + return document.querySelector('span[data-testid="entityTitle"] > h1')?.textContent || '' + } return Array.from(artistsList).filter(artist => artist.href.includes('artist')).map(artist => artist.textContent).join(', ') }