jam-cloud/web/app/assets/javascripts/react-components/PopupMediaControls.js.jsx.c...

707 lines
27 KiB
CoffeeScript

context = window
logger = context.JK.logger
ReactCSSTransitionGroup = React.addons.CSSTransitionGroup;
mixins = []
# make sure this is actually us opening the window, not someone else (by checking for MixerStore)
accessOpener = false
if window.opener?
try
m = window.opener.MixerStore
accessOpener = true
catch e
if accessOpener
AppActions = window.opener.AppActions
SessionActions = window.opener.SessionActions
MixerActions = window.opener.MixerActions
MixerStore = window.opener.MixerStore
JamTrackActions = window.opener.JamTrackActions
JamTrackMixdownActions = window.opener.JamTrackMixdownActions
#JamTrackMixdownStore = window.opener.JamTrackMixdownStore
JamTrackMixdown = window.opener.JamTrackMixdown
JamTrackStore = window.opener.JamTrackStore
MixerStore = window.opener.MixerStore
SessionStore = window.opener.SessionStore
mixins.push(Reflux.listenTo(MixerStore, 'onMixersChanged'))
mixins.push(Reflux.listenTo(JamTrackStore, 'onJamTrackChanged'))
@PopupMediaControls = React.createClass({
mixins: mixins
updateFromMixerHelper: (mixers, session) ->
# the backend delete/adds the metronome rapidly when the user hits play. this is custom code to deal with that
state =
isRecording: session.isRecording
mediaSummary: mixers.mediaSummary
backingTracks: mixers.backingTracks
jamTracks: mixers.jamTracks
recordedTracks: mixers.recordedTracks
metronome: mixers.metronome
recordingName: mixers.recordingName()
jamTrackName: mixers.jamTrackName()
jamTrackMixdown: session.jamTrackMixdown()
return {media: state, downloadingJamTrack: session.downloadingJamTrack}
onMixersChanged: (sessionMixers) ->
session = sessionMixers.session
mixers = sessionMixers.mixers
@setState(@updateFromMixerHelper(mixers, session))
onMediaStateChanged: (changes) ->
if changes.currentTimeChanged && @root?
@setState({time: changes.time})
onJamTrackChanged: (changes) ->
logger.debug("PopupMediaControls: jamtrack changed", changes)
@setState({jamTrackState: changes})
showMetronome: (e) ->
e.preventDefault()
SessionActions.showNativeMetronomeGui()
getInitialState: () ->
if accessOpener
state = @updateFromMixerHelper(MixerStore.mixers, MixerStore.session)
state.jamTrackState = JamTrackStore.getState()
return state
else
return {
media: @props.media,
mixdown: @props.mixdown,
jamTrackState: @props.jamTrackState,
creatingMixdown: false,
createMixdownErrors: null,
editingMixdownId: null,
downloadingJamTrack: @props.downloadingJamTrack,
jamTrackMixdown: {}
}
close: () ->
window.close()
help: (e) ->
e.preventDefault()
AppActions.openExternalUrl($(e.target).attr('href'))
render: () ->
closeLinkText = null
header = null
extraControls = null
# give the users options to close it
if @state.media.mediaSummary.recordingOpen
mediaType = "Recording"
mediaName = @state.media.recordedTracks[0].recordingName
closeLinkText = 'close recording'
header = `<h3>{mediaType}: {mediaName}</h3>`
else if @state.media.mediaSummary.jamTrackOpen || @state.jamTrackState.jamTrack?
if @state.media.mediaSummary.isOpener || @state.jamTrackState.jamTrack?
# if you opened the JamTrack, then you get all the good info
jamTrack = @state.jamTrackState.jamTrack
mediaType = "JamTrack"
mediaName = jamTrack.name
closeLinkText = 'CLOSE JAMTRACK'
helpLink = 'https://jamkazam.desk.com/customer/portal/articles/2138903-using-custom-mixes-to-slow-tempo-change-pitch'
selectedMixdown = jamTrack.activeMixdown
if selectedMixdown?
jamTrackTypeHeader = 'Custom Mix'
disabled = true
if selectedMixdown.client_state?
switch selectedMixdown.client_state
when 'cant_open'
customMixName = `<h5>{selectedMixdown.name}<img src="/assets/content/icon-mix-fail@2X.png" /></h5>`
when 'keying_timeout'
customMixName = `<h5>{selectedMixdown.name}<img src="/assets/content/icon-mix-fail@2X.png" /></h5>`
when 'download_fail'
customMixName = `<h5>{selectedMixdown.name}<img src="/assets/content/icon-mix-fail@2X.png" /></h5>`
when 'keying'
customMixName = `<h5>Loading selected mix... <img src="/assets/shared/spinner.gif" /></h5>`
when 'downloading'
customMixName = `<h5>Loading selected mix... <img src="/assets/shared/spinner.gif" /></h5>`
when 'ready'
customMixName = `<h5>{selectedMixdown.name}</h5>`
disabled = false
else
if selectedMixdown.myPackage
customMixName = `<h5>Creating mixdown... <img src="/assets/shared/spinner.gif" /></h5>`
else
customMixName = `<h5>{selectedMixdown.name}<img src="/assets/content/icon-mix-fail@2X.png" /></h5>`
else
if SessionStore.downloadingJamTrack
downloader = `<img src="/assets/shared/spinner.gif" />`
jamTrackTypeHeader = `<span>Full JamTrack {downloader}</span>`
header = `
<div className="header">
<h3>{mediaType}: {mediaName}</h3>
<h4>{jamTrackTypeHeader}</h4>
{customMixName}
</div>`
myMixes = null
if @state.showMyMixes
myMixdowns = []
boundPlayClick = this.jamTrackPlay.bind(this, jamTrack);
boundDownloadClick = this.jamTrackDownload.bind(this, jamTrack);
active = jamTrack.last_mixdown_id == null
myMixdowns.push `
<div key="full-track" className={classNames({'full-track': true, 'mixdown-display': true, 'active' : active})}>
<div className="mixdown-name">
Full JamTrack
</div>
<div className="mixdown-actions">
<img src="/assets/content/icon_open@2X.png" className="mixdown-play" onClick={boundPlayClick}/>
<img src="/assets/content/icon_download@2X.png" className="mixdown-download" onClick={boundDownloadClick}/>
<img src ="/assets/content/icon-delete@2X.png" style={{visibility:'hidden'}} className="mixdown-edit" onClick={false} />
<img src ="/assets/content/icon-delete@2X.png" style={{visibility:'hidden'}} className="mixdown-delete" onClick={false} />
</div>
</div>`
for mixdown in jamTrack.mixdowns
boundPlayClick = this.mixdownPlay.bind(this, mixdown);
boundEditClick = this.mixdownEdit.bind(this, mixdown);
boundSaveClick = this.mixdownSave.bind(this, mixdown);
boundDeleteClick = this.mixdownDelete.bind(this, mixdown);
boundErrorClick = this.mixdownError.bind(this, mixdown);
boundEditKeydown = this.onEditKeydown.bind(this, mixdown);
boundDownloadReadyClick = this.downloadMixdownReady.bind(this, mixdown)
mixdown_package = mixdown.myPackage
active = mixdown.id == jamTrack.last_mixdown_id
editing = mixdown.id == @state.editingMixdownId
# if there is a package, check it's state; otherwise let the user enqueue it
if mixdown_package
switch mixdown_package.signing_state
when 'QUIET_TIMEOUT'
action = `<img src="/assets/content/icon-mix-fail@2X.png" className="mixdown-play" onClick={boundErrorClick}/>`
when 'QUIET'
action = `<img src="/assets/shared/spinner.gif" className="mixdown-play"/>`
when 'QUEUED'
action = `<img src="/assets/shared/spinner.gif" className="mixdown-play"/>`
when 'QUEUED_TIMEOUT'
action = `<img src="/assets/content/icon-mix-fail@2X.png" className="mixdown-play" onClick={boundErrorClick}/>`
when 'SIGNING'
action = `<img src="/assets/shared/spinner.gif" className="mixdown-play"/>`
when 'SIGNING_TIMEOUT'
action = `<img src="/assets/content/icon-mix-fail@2X.png" className="mixdown-play" onClick={boundErrorClick}/>`
when 'SIGNED'
action = `<img src="/assets/content/icon_open@2X.png" className="mixdown-play" onClick={boundPlayClick}/>`
when 'ERROR'
action = `<img src="/assets/content/icon-mix-fail@2X.png" className="mixdown-play" onClick={boundErrorClick}/>`
else
action = `<img src="/assets/content/icon-mix-fail@2X.png" className="mixdown-play" onClick={boundErrorClick}/>`
if editing
mixdownName = `<input className="edit-name" type="text" defaultValue={mixdown.name} onKeyDown={boundEditKeydown} />`
editIcon = `<img src="/assets/content/icon-save@2X.png" className="mixdown-edit" onClick={boundSaveClick}/>`
else
mixdownName = mixdown.name
editIcon = `<img src="/assets/content/icon-edit@2X.png" className="mixdown-edit" onClick={boundEditClick}/>`
download = `<img src="/assets/content/icon_download@2X.png" className="mixdown-download" onClick={boundDownloadReadyClick} />`
myMixdowns.push `
<div key={mixdown.id} className={classNames({'mixdown-display': true, 'active' : active})}>
<div className="mixdown-name">
{mixdownName}
</div>
<div className="mixdown-actions">
{action}
{download}
{editIcon}
<img src ="/assets/content/icon-delete@2X.png" className="mixdown-delete" onClick={boundDeleteClick} />
</div>
</div>`
trackOptions = []
for track in jamTrack.tracks
if track.track_type == 'Track' || track.track_type == 'Click'
if track.instrument
instrumentId = track.instrument.id
instrumentDescription = track.instrument.description
if track.part? && track.part != instrumentDescription
part = "(#{track.part})"
else
part = ''
trackOptions.push(`<option key={track.id} value={track.id}>{instrumentDescription} {part}</option>`)
myMixdowns.push `
<div key={track.id} className={classNames({'stem-track' : true, 'mixdown-display': true})}>
<div className="mixdown-name">
<select className="active-stem-select" name="active-stem-select" onChange={this.stemChanged}>
<option key="null" value="">or select a track</option>
{trackOptions}
</select>
</div>
<div className="mixdown-actions">
<img src="/assets/content/icon_open@2X.png" style={{visibility:'hidden'}} className="mixdown-play"/>
<img src="/assets/content/icon_download@2X.png" className="mixdown-download" onClick={this.stemDownload}/>
<img src ="/assets/content/icon-delete@2X.png" style={{visibility:'hidden'}} className="mixdown-edit" onClick={false} />
<img src ="/assets/content/icon-delete@2X.png" style={{visibility:'hidden'}} className="mixdown-delete" onClick={false} />
</div>
</div>`
myMixes = `<div key="my-mixes" className="my-mixes">{myMixdowns}</div>`
mixControls = null
if @state.showCustomMixes
nameClassData = {field: true}
if @state.createMixdownErrors?
errorHtml = context.JK.reactErrors(@state.createMixdownErrors, {name: 'Mix Name', settings: 'Settings', jam_track: 'JamTrack'})
createMixClasses = classNames({'button-orange' : true, 'create-mix-btn' : true, 'disabled' : @state.creatingMixdown})
if !selectedMixdown?
mixControls = `
<div key="create-mix" className="create-mix">
<p>Use the JamTrack controls on the session screen to set levels, mute/unmute, or pan any of the parts of the JamTrack as you like. You can also use the controls below to adjust the tempo or pitch of the JamTrack. Then give your custom mix a name, and click the Create Mix button.</p>
<div className="field">
<label>Change Tempo:</label>
<select name="mix-speed">
<option value="">No change</option>
<option value="separator-1">------------</option>
<option value="-5">Slower by 5%</option>
<option value="-10">Slower by 10%</option>
<option value="-15">Slower by 15%</option>
<option value="-20">Slower by 20%</option>
<option value="-25">Slower by 25%</option>
<option value="-30">Slower by 30%</option>
<option value="-35">Slower by 35%</option>
<option value="-40">Slower by 40%</option>
<option value="-45">Slower by 45%</option>
<option value="-50">Slower by 50%</option>
<option value="-60">Slower by 60%</option>
<option value="-70">Slower by 70%</option>
<option value="-80">Slower by 80%</option>
<option value="separator-2">------------</option>
<option value="5">Faster by 5%</option>
<option value="10">Faster by 10%</option>
<option value="15">Faster by 15%</option>
<option value="20">Faster by 20%</option>
<option value="30">Faster by 30%</option>
<option value="40">Faster by 40%</option>
<option value="50">Faster by 50%</option>
</select>
</div>
<div className="field">
<label>Change Pitch:</label>
<select name="mix-pitch">
<option value="">No change</option>
<option value="separator-1">---------------</option>
<option value="-1">Down 1 Semitone</option>
<option value="-2">Down 2 Semitones</option>
<option value="-3">Down 3 Semitones</option>
<option value="-4">Down 4 Semitones</option>
<option value="-5">Down 5 Semitones</option>
<option value="-6">Down 6 Semitones</option>
<option value="-7">Down 7 Semitones</option>
<option value="-8">Down 8 Semitones</option>
<option value="-9">Down 9 Semitones</option>
<option value="-10">Down 10 Semitones</option>
<option value="-11">Down 11 Semitones</option>
<option value="-12">Down 12 Semitones</option>
<option value="separator-2">---------------</option>
<option value="1">Up 1 Semitone</option>
<option value="2">Up 2 Semitones</option>
<option value="3">Up 3 Semitones</option>
<option value="4">Up 4 Semitones</option>
<option value="5">Up 5 Semitones</option>
<option value="6">Up 6 Semitones</option>
<option value="7">Up 7 Semitones</option>
<option value="8">Up 8 Semitones</option>
<option value="9">Up 9 Semitones</option>
<option value="10">Up 10 Semitones</option>
<option value="11">Up 11 Semitones</option>
<option value="12">Up 12 Semitones</option>
</select>
</div>
<div className={classNames(nameClassData)}>
<label>Mix Name:</label>
<input type="text" name="mix-name"/>
</div>
<div className="field">
<a className={createMixClasses} onClick={this.createMix}>CREATE MIX</a>
{errorHtml}
</div>
<div className="clearall"/>
</div>`
else
mixControls =
`<div key="no-full-mix-open" className="create-mix not-active">
<p>To create a custom mix, you must open the Full JamTrack in the My Mixes section above.</p>
</div>`
if @state.showMyMixes
showMyMixesText = `<a onClick={this.toggleMyMixes}>hide my mixes <div className="details-arrow arrow-up" /></a>`
else
showMyMixesText = `<a onClick={this.toggleMyMixes}>show my mixes <div className="details-arrow arrow-down" /></a>`
if @state.showCustomMixes
showMixControlsText = `<a onClick={this.toggleCustomMixes}>hide mix controls <div className="details-arrow arrow-up" /></a>`
else
showMixControlsText = `<a onClick={this.toggleCustomMixes}>show mix controls <div className="details-arrow arrow-down" /></a>`
extraControls = `
<div className="extra-controls">
<h4>My Mixes {showMyMixesText}</h4>
<ReactCSSTransitionGroup transitionName="session-track-list" transitionAppear={true}>
{myMixes}
</ReactCSSTransitionGroup>
<h4 className="custom-mix-header">Create Custom Mix {showMixControlsText}</h4>
<ReactCSSTransitionGroup transitionName="session-track-list" transitionAppear={true}>
{mixControls}
</ReactCSSTransitionGroup>
</div>`
else
mediaType = "JamTrack"
mediaName = @state.media.jamTrackName
closeLinkText = 'CLOSE JAMTRACK'
# implies we have a mixdown
if @state.media.jamTrackMixdown.id?
jamTrackTypeHeader = 'Custom Mix'
else
jamTrackTypeHeader = 'Full JamTrack'
header = `
<div className="header">
<h3>{mediaType}: {mediaName}</h3>
<h4>{jamTrackTypeHeader}</h4>
{customMixName}
</div>`
else if @state.media.mediaSummary.backingTrackOpen
mediaType = "Audio File"
mediaName = context.JK.getNameOfFile(@state.media.backingTracks[0].shortFilename)
closeLinkText = 'CLOSE AUDIO FILE'
header = `<h3>{mediaType}: {mediaName}</h3>`
extraControls =
`<div>
<div className="field">
<input type="checkbox" name="loop" /><label htmlFor="loop">Loop audio file playback</label>
</div>
<br className="clearall"/>
</div>`
else if @state.media.mediaSummary.metronomeOpen
mediaType = "Metronome"
closeLinkText = 'CLOSE METRONOME'
header = `<h3>Metronome</h3>`
extraControls =
`<div>
<a className="display-metronome" onClick={this.showMetronome}>Display visual metronome</a>
</div>`
else
mediaType = ""
if helpLink?
helpButton = `<a className="help-link button-grey" href={helpLink} onClick={this.help}>HELP</a>`
`<div className="media-controls-popup">
{header}
<MediaControls disabled={this.state.downloadingJamTrack || this.disableLoading}/>
{extraControls}
<div className="actions">
{helpButton}
<a className="close-link button-orange" onClick={this.close}>{closeLinkText}</a>
</div>
</div>`
windowUnloaded: () ->
SessionActions.closeMedia(false) unless window.DontAutoCloseMedia
toggleMyMixes: (e) ->
e.preventDefault()
@setState({showMyMixes: !@state.showMyMixes})
toggleCustomMixes: (e) ->
e.preventDefault()
@setState({showCustomMixes: !@state.showCustomMixes})
mixdownPlay: (mixdown, e) ->
@setState({editingMixdownId: null})
e.preventDefault()
if @disableLoading
alert('Certain actions are disabled while a track is being loaded.')
return
# make this package the active one
JamTrackMixdownActions.openMixdown(mixdown)
jamTrackPlay: (jamtrack, e) ->
e.preventDefault()
# user wants to select the full track
if @disableLoading
alert('Certain actions are disabled while a track is being loaded.')
return
JamTrackActions.activateNoMixdown(jamtrack)
jamTrackDownload: (jamTrack, e) ->
e.preventDefault()
AppActions.openExternalUrl(window.location.protocol + '//' + window.location.host + "/api/jamtracks/#{jamTrack.id}/stems/master/download.mp3?file_type=mp3&download=1")
stemChanged:() ->
stemDownload: (e) ->
e.preventDefault()
$select = $(this.getDOMNode()).find('.active-stem-select')
selectedTrackId = $select.val()
if !selectedTrackId? || selectedTrackId == ''
alert("You must select a track in order to download.")
else
e.preventDefault()
AppActions.openExternalUrl(window.location.protocol + '//' + window.location.host + "/api/jamtracks/#{@state.jamTrackState.jamTrack.id}/stems/#{selectedTrackId}/download.mp3?file_type=mp3&download=1")
onEditKeydown: (mixdown, e) ->
logger.debug("on edit keydown", e)
if e.keyCode == 13 # enter
@mixdownSave(mixdown, e)
else if e.keyCode == 27 # esc
@setState({editingMixdownId: null})
mixdownEdit: (mixdown) ->
@setState({editingMixdownId: mixdown.id})
mixdownSave: (mixdown, e) ->
e.preventDefault()
$input = $(this.getDOMNode()).find('input.edit-name')
newValue = $input.val()
logger.debug("editing mixdown name to be: " + newValue)
JamTrackMixdownActions.editMixdown({id: mixdown.id, name: newValue})
@setState({editingMixdownId: null})
mixdownDelete: (mixdown) ->
if @state.editingMixdownId?
@setState({editingMixdownId:null})
return
if confirm("Delete this custom mix?")
JamTrackMixdownActions.deleteMixdown(mixdown)
downloadMixdownReady: (mixdown, e) ->
e.preventDefault()
# find the mp3 package for browser state check
for mixdown_package in mixdown.packages
if mixdown_package.file_type == 'mp3'
browserPackage = mixdown_package
break
if browserPackage?.signing_state == 'SIGNED'
AppActions.openExternalUrl(window.location.protocol + '//' + window.location.host + "/api/mixdowns/#{mixdown.id}/download.mp3?file_type=mp3&sample_rate=48&download=1")
else
JamTrackMixdownActions.openDownloader(mixdown)
mixdownError: (mixdown) ->
myPackage = mixdown.myPackage
if myPackage?
switch myPackage.signing_state
when 'QUIET_TIMEOUT'
action = 'Custom mix never got created. Retry?'
when 'QUEUED_TIMEOUT'
action = 'Custom mix was never built. Retry?'
when 'SIGNING_TIMEOUT'
action = 'Custom mix took took long to build. Retry?'
when 'ERROR'
action = 'Custom mix failed to build. Retry?'
else
action = 'Custom mix never got created. Retry?'
return unless action?
if confirm(action)
JamTrackMixdownActions.enqueueMixdown(mixdown, @enqueueDone)
enqueueDone: (enqueued) ->
@promptEstimate(enqueued)
promptEstimate: (enqueued) ->
time = enqueued.queue_time
if time == 0
alert("Your custom mix will take about 1 minute to be created.")
else
guess = Math.ceil(time / 60.0)
if guess == 1
msg = '1 minute'
else
msg = "#{guess} minutes"
alert("Your custom mix will take about #{msg} to be created.")
createMix: (e) ->
e.preventDefault()
return if @state.creatingMix
$root = $(@getDOMNode())
name = $root.find('input[name="mix-name"]').val()
speed = $root.find('select[name="mix-speed"]').val()
pitch = $root.find('select[name="mix-pitch"]').val()
if @state.jamTrackState.jamTrack?.activeMixdown?
@setState({createMixdownErrors: {errors: {'Full JamTrack': ['must be selected']}}})
return
if name == null || name == ''
@setState({createMixdownErrors: {errors: {'Mix Name': ["can't be blank"]}}})
return
# sanitize junk out of speed/pitch
if speed == '' || speed.indexOf('separator') > -1
speed = undefined
else
speed = parseInt(speed)
if pitch == '' || pitch.indexOf('separator') > -1
pitch = undefined
else
pitch = parseInt(pitch)
mixdown = {jamTrackID: @state.jamTrackState.jamTrack.id, name: name, settings: {speed:speed, pitch: pitch}}
JamTrackMixdownActions.createMixdown(mixdown, @createMixdownDone, @createMixdownFail)
@setState({creatingMixdown: true, createMixdownErrors: null})
createMixdownDone: (created) ->
logger.debug("created (within PopupMediaControls)", created)
# automatically close the create custom mix area
@setState({creatingMixdown: false, showCustomMixes: false, showMyMixes: true})
@promptEstimate(created)
createMixdownFail: (jqXHR) ->
logger.debug("create mixdown fail (within PopupMediaControls)", jqXHR.status)
@setState({creatingMixdown: false})
if jqXHR.status == 422
response = JSON.parse(jqXHR.responseText)
logger.warn("failed to create mixdown", response, jqXHR.responseText)
@setState({createMixdownErrors: response})
componentDidMount: () ->
$(window).unload(@windowUnloaded)
@root = jQuery(this.getDOMNode())
$loop = @root.find('input[name="loop"]')
context.JK.checkbox($loop)
$loop.on('ifChecked', () =>
# it doesn't matter if you do personal or master, because backend just syncs both
MixerActions.loopChanged(@state.media.backingTracks[0].mixers.personal.mixer, true)
)
$loop.on('ifUnchecked', () =>
# it doesn't matter if you do personal or master, because backend just syncs both
MixerActions.loopChanged(@state.media.backingTracks[0].mixers.personal.mixer, false)
)
@resizeWindow()
# this is necessary due to whatever the client's rendering behavior is.
setTimeout(@resizeWindow, 300)
componentDidUpdate: () ->
@resizeWindow()
setTimeout(@resizeWindow, 1000)
resizeWindow: () =>
$container = $('#minimal-container')
width = $container.width()
height = $container.height()
# there is 20px or so of unused space at the top of the page. can't figure out why it's there. (above #minimal-container)
#mysteryTopMargin = 20
mysteryTopMargin = 0
# deal with chrome in real browsers
offset = (window.outerHeight - window.innerHeight) + mysteryTopMargin
# handle native client chrome that the above outer-inner doesn't catch
#if navigator.userAgent.indexOf('JamKazam') > -1
#offset += 25
window.resizeTo(width, height + offset)
computeDisableLoading: (state) ->
@disableLoading = false
return unless nextState?
selectedMixdown = state?.jamTrackState?.jamTrack?.activeMixdown
mixdownDownloading = false
if selectedMixdown?
switch selectedMixdown.client_state
when 'keying'
mixdownDownloading = true
when 'downloading'
mixdownDownloading = true
@disableLoading = SessionStore.downloadingJamTrack || mixdownDownloading
componentWillMount: () ->
@computeDisableLoading(@state)
componentWillUpdate: (nextProps, nextState) ->
@computeDisableLoading(nextState)
})