`
+
+ componentDidMount: () ->
+ $root = jQuery(this.getDOMNode())
+
+ # initialize icheck
+ $chatMuteCheckbox = $root.find('.chat-mixer input')
+ context.JK.checkbox($chatMuteCheckbox)
+ $chatMuteCheckbox.on('ifChanged', @handleChatMuteCheckbox);
+
+ if @state.chatGroupMixers.muteMixer.mute
+ $chatMuteCheckbox.iCheck('check').attr('checked', true)
+ else
+ $chatMuteCheckbox.iCheck('uncheck').attr('checked', false)
+
+ $audioInputMuteCheckbox = $root.find('.monitor-mixer input')
+ context.JK.checkbox($audioInputMuteCheckbox)
+ $audioInputMuteCheckbox.on('ifChanged', @handleAudioInputMuteCheckbox);
+
+ if @state.inputGroupMixers.muteMixer.mute
+ $audioInputMuteCheckbox.iCheck('check').attr('checked', true)
+ else
+ $audioInputMuteCheckbox.iCheck('uncheck').attr('checked', false)
+
+ componentWillUpdate: (nextProps, nextState) ->
+ $root = jQuery(this.getDOMNode())
+
+ # re-initialize icheck
+ $chatMuteCheckbox = $root.find('.chat-mixer input')
+
+ if nextState.chatGroupMixers.muteMixer?.mute
+ $chatMuteCheckbox.iCheck('check').attr('checked', true)
+ else
+ $chatMuteCheckbox.iCheck('uncheck').attr('checked', false)
+
+ $audioInputMuteCheckbox = $root.find('.monitor-mixer input')
+
+ if nextState.inputGroupMixers.muteMixer?.mute
+ $audioInputMuteCheckbox.iCheck('check').attr('checked', true)
+ else
+ $audioInputMuteCheckbox.iCheck('uncheck').attr('checked', false)
+
+})
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/SessionSettingsBtn.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionSettingsBtn.js.jsx.coffee
new file mode 100644
index 000000000..0719f136b
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/SessionSettingsBtn.js.jsx.coffee
@@ -0,0 +1,20 @@
+context = window
+
+@SessionSettingsBtn = React.createClass({
+
+ mixins: [Reflux.listenTo(@AppStore,"onAppInit")]
+
+ openSettings: (e) ->
+ e.preventDefault()
+
+ @app.layout.showDialog('session-settings')
+
+ render: () ->
+ `
`
+
+ onAppInit: (app) ->
+ @app = app
+})
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/SessionShareBtn.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionShareBtn.js.jsx.coffee
new file mode 100644
index 000000000..00af66683
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/SessionShareBtn.js.jsx.coffee
@@ -0,0 +1,20 @@
+context = window
+
+@SessionShareBtn = React.createClass({
+
+ mixins: [Reflux.listenTo(@AppStore,"onAppInit")]
+
+ onShare: (e) ->
+ e.preventDefault()
+
+ @app.layout.showDialog('share-dialog')
+
+ render: () ->
+ `
`
+
+ onAppInit: (app) ->
+ @app = app
+})
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/SessionTrackGain.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionTrackGain.js.jsx.coffee
new file mode 100644
index 000000000..e5dfa7f58
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/SessionTrackGain.js.jsx.coffee
@@ -0,0 +1,51 @@
+context = window
+logger = context.JK.logger
+
+@SessionTrackGain = React.createClass({
+
+ getInitialState: () ->
+ {
+ mixers: @props.mixers,
+ behaviors: @props.behaviors || {}
+ }
+
+ faderChanged: (e, data) ->
+ $target = $(this)
+ groupId = $target.data('groupId')
+ mixers = [@state.mixers.mixer]
+
+ MixerActions.faderChanged(data, mixers, groupId)
+
+ render: () ->
+ mixerId = this.state.mixers?.mixer?.id
+
+ `
`
+
+ componentDidMount: () ->
+ $root = jQuery(this.getDOMNode())
+ if !$root.is('.track-gain')
+ logger.error("unknown root node")
+
+ $fader = $root.attr('data-mixer-id', @state.mixers.mixer.id).data('groupId', @state.mixers.mixer.groupId).data('mixer', @state.mixers.mixer).data('opposite-mixer', @state.mixers.oppositeMixer)
+
+ if @state.behaviors.mediaControlsDisabled
+ $fader.data('media-controls-disabled', true).data('media-track-opener', @state.behaviors.mediaTrackOpener) # this we be applied later to the fader handle $element
+
+ $fader.data('showHelpAboutMediaMixers', @state.behaviors.showHelpAboutMediaMixers)
+
+ context.JK.FaderHelpers.renderFader2($fader, {faderType: 'vertical'});
+
+ # Initialize gain position
+ MixerActions.initGain(@state.mixers.mixer)
+
+ # watch for fader change events
+ $fader.on('fader_change', @faderChanged);
+
+
+})
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/SessionTrackPan.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionTrackPan.js.jsx.coffee
new file mode 100644
index 000000000..0bfae1005
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/SessionTrackPan.js.jsx.coffee
@@ -0,0 +1,54 @@
+context = window
+logger = context.JK.logger
+
+@SessionTrackPan = React.createClass({
+
+ getInitialState: () ->
+ {
+ mixers: this.props.mixers,
+ behaviors: this.props.behaviors || {}
+ }
+
+ panChanged: (e, data) ->
+ $target = $(this)
+ groupId = $target.data('groupId')
+ mixers = [@state.mixers.mixer]
+
+ MixerActions.panChanged(data, mixers, groupId)
+
+ render: () ->
+
+ mixerId = this.state.mixers?.mixer?.id
+
+ `
`
+
+ componentDidMount: () ->
+ $root = jQuery(this.getDOMNode())
+ if !$root.is('.track-pan')
+ logger.error("unknown root node")
+
+ $fader = $root.attr('data-mixer-id', this.state.mixers.mixer.id).data('groupId', this.state.mixers.mixer.groupId).data('mixer', this.state.mixers.mixer).data('opposite-mixer', this.state.mixers.oppositeMixer)
+
+ if this.state.behaviors.mediaControlsDisabled
+ $fader.data('media-controls-disabled', true).data('media-track-opener', this.state.behaviors.mediaTrackOpener) # this we be applied later to the fader handle $element
+
+ $fader.data('showHelpAboutMediaMixers', this.state.behaviors.showHelpAboutMediaMixers)
+
+ context.JK.FaderHelpers.renderFader2($fader, {faderType: 'horizontal', snap:true}, context.JK.PanHelpers.convertPercentToPanForDisplay)
+
+ # Initialize gain position
+ MixerActions.initPan(this.state.mixers.mixer)
+
+ # watch for fader change events
+ $fader.on('fader_change', this.panChanged)
+
+})
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/SessionTrackPanHover.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionTrackPanHover.js.jsx.coffee
new file mode 100644
index 000000000..8244991f4
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/SessionTrackPanHover.js.jsx.coffee
@@ -0,0 +1,51 @@
+context = window
+
+MixerActions = @MixerActions
+
+@SessionTrackPanHover = React.createClass({
+
+ mixins: [Reflux.listenTo(@SessionMyTracksStore, "onInputsChanged")]
+
+ closeHover: (e) ->
+ e.preventDefault()
+ $container = $(this.getDOMNode()).closest('.react-holder')
+ $container.data('bt').btOff()
+
+ onInputsChanged: (sessionMixers) ->
+ mixers = sessionMixers.mixers
+ newMixers = mixers.refreshMixer(@state.mixers)
+
+ this.setState({mixers: newMixers})
+
+
+ getInitialState: () ->
+ {mixers: this.props.mixers}
+
+ render: () ->
+
+ `
`
+
+ componentWillUpdate: (nextProps, nextState) ->
+ $root = jQuery(this.getDOMNode())
+
+ # if the mixers go dead, whack our selves out of existence
+ unless nextState.mixers?
+ $container = $root.closest('.react-holder')
+ $container.data('bt').btOff()
+ return
+})
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/SessionTrackSettingsBtn.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionTrackSettingsBtn.js.jsx.coffee
new file mode 100644
index 000000000..530df4c93
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/SessionTrackSettingsBtn.js.jsx.coffee
@@ -0,0 +1,22 @@
+context = window
+
+logger = context.JK.logger
+
+@SessionTrackSettingsBtn = React.createClass({
+
+ mixins: [Reflux.listenTo(@AppStore,"onAppInit")]
+
+ onConfigureSettings: (e) ->
+ e.preventDefault();
+
+ @app.layout.showDialog('configure-tracks')
+
+ onAppInit: (app) ->
+ @app = app
+
+ render: () ->
+ `
`
+})
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/SessionTrackVU.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionTrackVU.js.jsx.coffee
new file mode 100644
index 000000000..7abdad501
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/SessionTrackVU.js.jsx.coffee
@@ -0,0 +1,87 @@
+context = window
+ptrCount = 0
+
+@SessionTrackVU = React.createClass({
+
+
+ render: () ->
+ lights = []
+ redSwitch = Math.round(this.props.lightCount * 0.66);
+ lightClass = 'vu-red-off'
+
+ if this.props.orientation == 'horizontal'
+
+ for i in [0..this.props.lightCount-1]
+ lightClass = if i >= redSwitch then 'vu-red-off' else 'vu-green-off'
+
+ lightClasses = classNames('vulight', 'vu' + i, lightClass)
+
+ lights.push(`
`
+ else
+
+ for i in [0..this.props.lightCount-1].reverse()
+ lightClass = if (i >= redSwitch) then "vu-red-off" else "vu-green-off"
+
+ lightClasses = classNames('vulight', 'vu' + i, lightClass)
+
+ lights.push(`
`
+
+ getInitialState: () ->
+ {registered: null, ptr: @props.ptr || "STV#{ptrCount++}"}
+
+ registerVU: (mixer) ->
+
+ mixerChanged = false
+ if @state.registered? && mixer?
+
+ # see if the mixer ID changed; if so, we need to unregister and re-register
+ if @state.registered.mixer.id != mixer.id
+ logger.debug("unregistering vu due to mixer change", @state.registered.mixer)
+ context.JK.VuHelpers.unregisterVU(@state.registered.mixer, @state.registered.ptr)
+ mixerChanged = true
+
+ if !mixerChanged && (@state.registered? || !mixer?)
+ return
+
+ $root = $(this.getDOMNode())
+
+ if mixerChanged
+ logger.debug("re-registering VU #{context.JK.groupIdDisplay(mixer)}", mixer)
+ else
+ logger.debug("registered VU #{context.JK.groupIdDisplay(mixer)}", mixer)
+
+ context.JK.VuHelpers.registerVU(@props.side, mixer, @state.ptr, @props.orientation == 'horizontal', @props.lightCount, $root.find('td'))
+
+ @setState(registered: {mixer: mixer, ptr: @state.ptr})
+
+
+ componentWillReceiveProps: (nextProps) ->
+ @registerVU(nextProps.mixers?.vuMixer)
+
+ componentDidMount: () ->
+ @registerVU(@props.mixers?.vuMixer)
+
+ componentWillUnmount: () ->
+ if @state.registered?
+ logger.debug("unregistered VU #{context.JK.groupIdDisplay(@state.registered.mixer)}")
+ context.JK.VuHelpers.unregisterVU(@state.registered.mixer, @state.registered.ptr)
+
+})
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/SessionTrackVolumeHover.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionTrackVolumeHover.js.jsx.coffee
new file mode 100644
index 000000000..e6c6c87cb
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/SessionTrackVolumeHover.js.jsx.coffee
@@ -0,0 +1,117 @@
+context = window
+ChannelGroupIds = context.JK.ChannelGroupIds
+MixerActions = @MixerActions
+ptrCount = 0
+
+@SessionTrackVolumeHover = React.createClass({
+
+ mixins: [Reflux.listenTo(@SessionMyTracksStore,"onInputsChanged")]
+
+ closeHover: (e) ->
+ e.preventDefault()
+ $container = $(this.getDOMNode()).closest('.react-holder')
+ $container.data('bt').btOff()
+
+ onInputsChanged: (sessionMixers) ->
+
+ mixers = sessionMixers.mixers
+ newMixers = mixers.refreshMixer(@state.mixers)
+
+ this.setState({mixers: newMixers})
+
+ getInitialState: () ->
+ {mixers: this.props.mixers, ptr: "STVH#{ptrCount++}" }
+
+ handleMute: (e) ->
+ e.preventDefault()
+
+ muting = $(e.currentTarget).is('.enabled')
+
+ if @state.mixers.mixer.group_id == ChannelGroupIds.AudioInputMusicGroup || @state.mixers.mixer.group_id == ChannelGroupIds.AudioInputChatGroup
+ MixerActions.mute([this.state.mixers.mixer, this.state.mixers.oppositeMixer], muting)
+ else
+ MixerActions.mute([this.state.mixers.mixer], muting)
+
+
+ handleMuteCheckbox: (e) ->
+ muting = $(e.target).is(':checked')
+
+ if @state.mixers.mixer.group_id == ChannelGroupIds.AudioInputMusicGroup || @state.mixers.mixer.group_id == ChannelGroupIds.AudioInputChatGroup
+ MixerActions.mute([this.state.mixers.mixer, this.state.mixers.oppositeMixer], muting)
+ else
+ MixerActions.mute([this.state.mixers.mixer], muting)
+
+
+ render: () ->
+
+ muteMixer = this.state.mixers?.muteMixer
+
+ muteMixerId = muteMixer?.id
+ volume_left = this.state.mixers?.mixer?.volume_left
+
+ classes = classNames({
+ 'track-icon-mute': true
+ 'enabled' : !muteMixer?.mute
+ 'muted' : muteMixer?.mute
+ })
+
+ `
+
+
+
+
+
+
+
+
+
Volume
+
{volume_left}dB
+
+
+
+
+
+ Mute
+
+
+
+
Use this slider to control the volume of this track in your personal mix.
+
This will not affect the volume of this track for other musicians in the session.
+
To adjust master levels for all musicians for recordings and broadcasts, use Mixer button in the toolbar.
+
+
+
+
`
+
+ componentDidMount: () ->
+ $root = jQuery(this.getDOMNode())
+
+ # initialize icheck
+ $checkbox = $root.find('input')
+ context.JK.checkbox($checkbox)
+ $checkbox.on('ifChanged', this.handleMuteCheckbox);
+
+ if @state.mixers.muteMixer.mute
+ $checkbox.iCheck('check').attr('checked', true)
+ else
+ $checkbox.iCheck('uncheck').attr('checked', false)
+
+ componentWillUpdate: (nextProps, nextState) ->
+ $root = jQuery(this.getDOMNode())
+
+ # if the mixers go dead, whack our selves out of existence
+ unless nextState.mixers?
+ $container = $root.closest('.react-holder')
+ $container.data('bt').btOff()
+ return
+
+ # re-initialize icheck
+ $checkbox = $root.find('input')
+
+ if nextState.mixers?.muteMixer?.mute
+ $checkbox.iCheck('check').attr('checked', true)
+ else
+ $checkbox.iCheck('uncheck').attr('checked', false)
+})
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/SessionVideoBtn.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionVideoBtn.js.jsx.coffee
new file mode 100644
index 000000000..d89f7ae29
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/SessionVideoBtn.js.jsx.coffee
@@ -0,0 +1,16 @@
+context = window
+SessionActions = @SessionActions
+
+@SessionVideoBtn = React.createClass({
+
+ sessionWebCam: (e) ->
+ e.preventDefault();
+
+ SessionActions.toggleSessionVideo()
+
+ render: () ->
+ `
+
+ VIDEO
+ `
+})
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/SessionVolumeSettingsBtn.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionVolumeSettingsBtn.js.jsx.coffee
new file mode 100644
index 000000000..4b9932a6b
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/SessionVolumeSettingsBtn.js.jsx.coffee
@@ -0,0 +1,28 @@
+context = window
+MIX_MODES = context.JK.MIX_MODES
+
+@SessionVolumeSettingsBtn = React.createClass({
+
+ mixins: [Reflux.listenTo(@MixerStore,"onInputsChanged")]
+
+ onInputsChanged: (sessionMixers) ->
+ this.setState(sessionMixers)
+
+ render: () ->
+ `
+
+ VOLUME
+ `
+
+ componentDidMount: () ->
+ $root = $(this.getDOMNode())
+
+ context.JK.interactReactBubble(
+ $root,
+ 'SessionSelfVolumeHover',
+ () =>
+ {inputGroupMixers: @state.mixers.getAudioInputCategoryMixer(MIX_MODES.PERSONAL), chatGroupMixers: @state.mixers.getChatCategoryMixer( MIX_MODES.PERSONAL)}
+ ,
+ {width:470, positions:['right', 'bottom', 'left'], offsetParent:$root.closest('.screen')})
+
+})
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/Test.js.jsx.coffee b/web/app/assets/javascripts/react-components/Test.js.jsx.coffee
new file mode 100644
index 000000000..fbdc502a7
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/Test.js.jsx.coffee
@@ -0,0 +1,19 @@
+context = window
+
+@TestComponent = React.createClass({
+
+ getInitialState: () ->
+ {something: 1}
+
+ tick: () ->
+ console.log("tick")
+ this.setState({something: this.state.something + 1})
+
+ componentDidMount: () ->
+ console.log("here")
+ setInterval(@tick, 1000)
+
+ render: () ->
+ console.log("render")
+ `
{this.state.something}
`
+})
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/actions/AppActions.js.coffee b/web/app/assets/javascripts/react-components/actions/AppActions.js.coffee
new file mode 100644
index 000000000..6c054e3ec
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/actions/AppActions.js.coffee
@@ -0,0 +1,5 @@
+context = window
+
+@AppActions = Reflux.createActions({
+ appInit: {}
+})
diff --git a/web/app/assets/javascripts/react-components/actions/BroadcastActions.js.coffee b/web/app/assets/javascripts/react-components/actions/BroadcastActions.js.coffee
index 7761257d2..e4bc43707 100644
--- a/web/app/assets/javascripts/react-components/actions/BroadcastActions.js.coffee
+++ b/web/app/assets/javascripts/react-components/actions/BroadcastActions.js.coffee
@@ -1,6 +1,6 @@
context = window
-BroadcastActions = Reflux.createActions({
+@BroadcastActions = Reflux.createActions({
load: {asyncResult: true},
hide: {}
})
diff --git a/web/app/assets/javascripts/react-components/actions/MediaPlaybackActions.js.coffee b/web/app/assets/javascripts/react-components/actions/MediaPlaybackActions.js.coffee
new file mode 100644
index 000000000..0996e5745
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/actions/MediaPlaybackActions.js.coffee
@@ -0,0 +1,11 @@
+context = window
+
+@MediaPlaybackActions = Reflux.createActions({
+ playbackStateChange: {}
+ positionUpdate:{}
+ mediaStartPlay: {}
+ mediaStopPlay: {}
+ mediaPausePlay: {}
+ mediaChangePosition: {}
+ currentTimeChanged: {}
+})
diff --git a/web/app/assets/javascripts/react-components/actions/MixerActions.js.coffee b/web/app/assets/javascripts/react-components/actions/MixerActions.js.coffee
new file mode 100644
index 000000000..5d5678207
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/actions/MixerActions.js.coffee
@@ -0,0 +1,16 @@
+context = window
+
+@MixerActions = Reflux.createActions({
+ mute: {}
+ faderChanged: {}
+ initGain: {}
+ panChanged: {}
+ initPan: {}
+ mixersChanged: {}
+ syncTracks: {}
+ mixerModeChanged: {}
+ loopChanged: {}
+ openMetronome: {}
+ metronomeChanged: {}
+ deadUserRemove: {}
+})
diff --git a/web/app/assets/javascripts/react-components/actions/NotificationActions.js.coffee b/web/app/assets/javascripts/react-components/actions/NotificationActions.js.coffee
new file mode 100644
index 000000000..481dc8ff8
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/actions/NotificationActions.js.coffee
@@ -0,0 +1,9 @@
+context = window
+
+@NotificationActions = Reflux.createActions({
+ clear:{}
+ backendNotification: {}
+ frontendNotification: {}
+ sessionEnded: {}
+})
+
diff --git a/web/app/assets/javascripts/react-components/actions/RecordingActions.js.coffee b/web/app/assets/javascripts/react-components/actions/RecordingActions.js.coffee
new file mode 100644
index 000000000..f6b111ac9
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/actions/RecordingActions.js.coffee
@@ -0,0 +1,14 @@
+context = window
+
+@RecordingActions = Reflux.createActions({
+ initModel: {}
+ startRecording: {}
+ stopRecording: {}
+ startingRecording:{}
+ startedRecording: {}
+ stoppingRecording: {}
+ stoppedRecording: {}
+ abortedRecording: {}
+ openRecordingControls: {}
+ recordingControlsClosed: {}
+})
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/actions/SessionActions.js.coffee b/web/app/assets/javascripts/react-components/actions/SessionActions.js.coffee
new file mode 100644
index 000000000..634e7d419
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/actions/SessionActions.js.coffee
@@ -0,0 +1,22 @@
+context = window
+
+@SessionActions = Reflux.createActions({
+ joinSession: {}
+ leaveSession: {}
+ mixersChanged: {}
+ allowLeaveSession: {}
+ syncWithServer: {}
+ toggleSessionVideo : {}
+ audioResync: {}
+ openBackingTrack: {}
+ closeMedia: {}
+ updateSession: {}
+ downloadingJamTrack : {}
+ openMetronome: {}
+ showNativeMetronomeGui: {}
+ metronomeCricketChange: {}
+ windowBackgrounded: {}
+ broadcastFailure: {}
+ broadcastSuccess: {}
+ broadcastStopped: {}
+})
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/actions/SessionMyTracksActions.js.coffee b/web/app/assets/javascripts/react-components/actions/SessionMyTracksActions.js.coffee
new file mode 100644
index 000000000..568ba3b29
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/actions/SessionMyTracksActions.js.coffee
@@ -0,0 +1,5 @@
+context = window
+
+@SessionMyTracksActions = Reflux.createActions({
+
+})
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/helpers/MixerHelper.js.coffee b/web/app/assets/javascripts/react-components/helpers/MixerHelper.js.coffee
new file mode 100644
index 000000000..4800f3925
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/helpers/MixerHelper.js.coffee
@@ -0,0 +1,850 @@
+context = window
+
+logger = context.JK.logger
+ChannelGroupIds = context.JK.ChannelGroupIds
+CategoryGroupIds = context.JK.CategoryGroupIds
+MIX_MODES = context.JK.MIX_MODES;
+
+
+@MixerHelper = class MixerHelper
+
+ constructor: (@session, @masterMixers, @personalMixers, @metro, @noAudioUsers, @mixMode) ->
+ @mixMode = MIX_MODES.PERSONAL # TODO - remove mixMode from MixerHelper? Or at least stop using it in most functions
+ @app = @session.app
+ @mixersByResourceId = {}
+ @mixersByTrackId = {}
+ @allMixers = {}
+ @currentMixerRangeMin = null
+ @currentMixerRangeMax = null
+ @mediaSummary = {}
+ @mediaTrackGroups = [ChannelGroupIds.MediaTrackGroup, ChannelGroupIds.JamTrackGroup,
+ ChannelGroupIds.MetronomeGroup]
+ @muteBothMasterAndPersonalGroups = [ChannelGroupIds.AudioInputMusicGroup, ChannelGroupIds.MediaTrackGroup,
+ ChannelGroupIds.JamTrackGroup, ChannelGroupIds.MetronomeGroup]
+ @vuStats = {}
+ @shouldCollectVuStats = false
+ @organize()
+
+ organize: () ->
+ for masterMixer in @masterMixers
+ @allMixers['M' + masterMixer.id] = masterMixer; # populate allMixers by mixer.id
+
+ # populate mixer pair
+ mixerPair = {}
+ @mixersByResourceId[masterMixer.rid] = mixerPair
+ @mixersByTrackId[masterMixer.id] = mixerPair
+ mixerPair.master = masterMixer;
+
+ for personalMixer in @personalMixers
+
+ @allMixers['P' + personalMixer.id] = personalMixer
+
+ # populate other side of mixer pair
+
+ mixerPair = @mixersByResourceId[personalMixer.rid]
+ unless mixerPair
+ if personalMixer.group_id != ChannelGroupIds.MonitorGroup
+ logger.warn("there is no master version of ", personalMixer)
+
+ mixerPair = {}
+ @mixersByResourceId[personalMixer.rid] = mixerPair
+
+ @mixersByTrackId[personalMixer.id] = mixerPair;
+ mixerPair.personal = personalMixer;
+
+ @groupTypes()
+ @chatMixer = @resolveChatMixer()
+
+ groupTypes: () ->
+ localMediaMixers = @mixersForGroupIds(@mediaTrackGroups, MIX_MODES.MASTER)
+ peerLocalMediaMixers = @mixersForGroupId(ChannelGroupIds.PeerMediaTrackGroup, MIX_MODES.MASTER)
+
+ #logger.debug("localMediaMixers", localMediaMixers)
+ #logger.debug("peerLocalMediaMixers", peerLocalMediaMixers)
+
+ # get the server data regarding various media tracks
+ recordedBackingTracks = @session.recordedBackingTracks()
+ backingTracks = @session.backingTracks()
+ recordedJamTracks = @session.recordedJamTracks()
+ jamTracks = @session.jamTracks()
+
+ ###
+ with mixer info, we use these to decide what kind of tracks are open in the backend
+
+ each mixer has a media_type field, which describes the type of media track it is.
+ * JamTrack
+ * BackingTrack
+ * RecordingTrack
+ * MetronomeTrack
+ * "" - adhoc track (not supported visually)
+
+ it is supposed to be the case that there are only one type of track open at a time, however, that's a business policy/logic
+ constraint; and may be buggy. **So, we should render whatever we have, so that it's obvious what's really going on.**
+
+ so, let's group up all mixers by type, and then ask them to be rendered
+ ###
+
+ @recordingTrackMixers = []
+ @backingTrackMixers = []
+ @jamTrackMixers = []
+ @metronomeTrackMixers = []
+ @adhocTrackMixers = []
+
+ groupByType = (mixers, isLocalMixer) =>
+ for mixer in mixers
+ mediaType = mixer.media_type
+ groupId = mixer.group_id
+
+ if mediaType == 'MetronomeTrack' || groupId == ChannelGroupIds.MetronomeGroup
+ # Metronomes come across with a blank media type, so check group_id:
+ @metronomeTrackMixers.push(mixer)
+ else if mediaType == null || mediaType == "" || mediaType == 'RecordingTrack'
+ # additional check; if we can match an id in backing tracks or recorded backing track,
+ # we need to remove it from the recorded track set, but move it to the backing track set
+
+ isJamTrack = false;
+
+ if jamTracks
+ # check if the ID matches that of an open jam track
+ for jamTrack in jamTracks
+ if mixer.id == jamTrack.id
+ isJamTrack = true;
+ break
+
+ if !isJamTrack && recordedJamTracks
+ # then check if the ID matches that of a open, recorded jam track
+ for recordedJamTrack in recordedJamTracks
+ if mixer.id == recordedJamTrack.id
+ isJamTrack = true
+ break
+
+ if isJamTrack
+ @jamTrackMixers.push(mixer)
+ else
+ isBackingTrack = false
+ if recordedBackingTracks
+ for recordedBackingTrack in recordedBackingTracks
+ if mixer.id == 'L' + recordedBackingTrack.client_track_id
+ isBackingTrack = true
+ break
+
+ if backingTracks
+ for backingTrack in backingTracks
+ if mixer.id == 'L' + backingTrack.client_track_id
+ isBackingTrack = true
+ break
+
+ if isBackingTrack
+ @backingTrackMixers.push(mixer)
+ else
+ # couldn't resolve this as a JamTrack or Backing track, must be a normal recorded file
+ @recordingTrackMixers.push(mixer)
+
+ else if mediaType == 'PeerMediaTrack' || mediaType == 'BackingTrack'
+ @backingTrackMixers.push(mixer)
+ else if mediaType == 'JamTrack'
+ @jamTrackMixers.push(mixer);
+ else if mediaType == null || mediaType == "" || mediaType == 'RecordingTrack'
+ # mediaType == null is for backwards compat with older clients. Can be removed soon
+ @recordingTrackMixers.push(mixer)
+ else
+ logger.warn("Unknown track type: " + mediaType)
+ @adhocTrackMixers.push(mixer)
+
+ groupByType(localMediaMixers, true);
+ groupByType(peerLocalMediaMixers, false);
+
+ ###
+ if recordingTrackMixers.length > 0
+ renderRecordingTracks(recordingTrackMixers)
+
+ if backingTrackMixers.length > 0
+ renderBackingTracks(backingTrackMixers)
+
+ if jamTrackMixers.length > 0
+ renderJamTracks(jamTrackMixers);
+
+ if metronomeTrackMixers.length > 0 && @session.jamTracks() == null && @session.recordedJamTracks() == null
+ renderMetronomeTracks(metronomeTrackMixers);
+
+ checkMetronomeTransition();
+ ###
+
+ @backingTracks = @resolveBackingTracks()
+ @jamTracks = @resolveJamTracks()
+ @recordedTracks = @resolveRecordedTracks()
+ @metronome = @resolveMetronome()
+
+
+ if @adhocTrackMixers.length > 0
+ logger.warn("some tracks are open that we don't know how to show")
+
+ @mediaSummary =
+ recordingOpen: @recordedTracks.length > 0
+ jamTrackOpen: @jamTracks.length > 0
+ backingTrackOpen: @backingTracks.length > 0
+ metronomeOpen: @metronome?
+
+ # figure out if any media is open
+ mediaOpenSummary = false
+ for mediaType, mediaOpen of @mediaSummary
+ mediaOpenSummary = true if mediaOpen
+
+ @mediaSummary.mediaOpen = mediaOpenSummary
+
+ # this method is pretty complicated because it forks on a key bit of state:
+ # sessionModel.isPlayingRecording()
+ # a backing track opened as part of a recording has a different behavior and presence on the server (recording.recorded_backing_tracks)
+ # than a backing track opend ad-hoc (connection.backing_tracks)
+
+ resolveBackingTracks: () ->
+ backingTracks = []
+
+ return backingTracks unless @backingTrackMixers.length > 0
+
+ # find both client and server representation of the backing track
+ serverBackingTracks = []
+ backingTrackMixers = @backingTrackMixers
+
+ if @session.isPlayingRecording()
+ backingTrackMixers = context._.filter(backingTrackMixers, (mixer) -> return mixer.managed || !mixer.managed?)
+ serverBackingTracks = @session.recordedBackingTracks()
+ else
+ serverBackingTracks = @session.backingTracks();
+ backingTrackMixers = context._.filter(backingTrackMixers, (mixer) -> return !mixer.managed)
+ if backingTrackMixers.length > 1
+ logger.error("multiple, managed backing track mixers encountered", backingTrackMixers)
+ @app.notify({
+ title: "Multiple Backing Tracks Encountered",
+ text: "Only one backing track can be open a time.",
+ icon_url: "/assets/content/icon_alert_big.png"
+ });
+ return backingTracks;
+
+ # we don't render backing tracks unless we have server data to accompany
+ if !serverBackingTracks? || serverBackingTracks.length == 0
+ return backingTracks
+
+ noCorrespondingTracks = false
+ for mixer in backingTrackMixers
+ # find the track or tracks that correspond to the mixer
+ correspondingTracks = []
+ noCorrespondingTracks = false
+ if @session.isPlayingRecording()
+ for backingTrack in serverBackingTracks
+ # occurs if this client is the one that opened the track, # occurs if this client is a remote participant
+ if mixer.persisted_track_id == backingTrack.client_track_id || mixer.id == 'L' + backingTrack.client_track_id
+ correspondingTracks.push(backingTrack)
+ else
+ # if this is just an open backing track, then we can assume that the 1st backingTrackMixer is ours
+ correspondingTracks.push(serverBackingTracks[0])
+
+ if correspondingTracks.length == 0
+ noCorrespondingTracks = true
+ logger.debug("renderBackingTracks: could not map backing tracks")
+ @app.notify({
+ title: "Unable to Open Backing Track",
+ text: "Could not correlate server and client tracks",
+ icon_url: "/assets/content/icon_alert_big.png"
+ });
+ break
+
+ # now we have backing track and mixer in hand; we can render
+ serverBackingTrack = correspondingTracks[0]
+
+ oppositeMixer = @getMixerByResourceId(mixer.rid, MIX_MODES.PERSONAL);
+
+ isOpener = mixer.group_id == ChannelGroupIds.MediaTrackGroup
+ data =
+ isOpener: isOpener
+ shortFilename: context.JK.getNameOfFile(serverBackingTrack.filename)
+ instrumentIcon: context.JK.getInstrumentIcon45(serverBackingTrack.instrument_id)
+ photoUrl: "/assets/content/icon_recording.png"
+ showLoop: isOpener && !@session.isPlayingRecording()
+ track: serverBackingTrack
+ mixers: @mediaMixers(mixer, isOpener)
+
+ backingTracks.push(data)
+
+ backingTracks
+
+ resolveJamTracks: () ->
+ _jamTracks = []
+
+ return _jamTracks unless @jamTrackMixers.length > 0
+
+
+ jamTrackMixers = @jamTrackMixers.slice();
+ jamTracks = []
+ jamTrackName = null;
+
+ if @session.isPlayingRecording()
+ # only return managed mixers for recorded backing tracks
+ jamTracks = @session.recordedJamTracks()
+ jamTrackName = @session.recordedJamTrackName()
+ else
+ # only return un-managed (ad-hoc) mixers for normal backing tracks
+ jamTracks = @session.jamTracks()
+ jamTrackName = @session.jamTrackName()
+
+ # pluck the 1st mixer, and assume that all other mixers in this group are of the same type (between JamTrack vs Peer)
+ # if it's a locally opened track (JamTrackGroup), then we can say this person is the opener
+ isOpener = jamTrackMixers[0].group_id == ChannelGroupIds.JamTrackGroup;
+
+ if jamTracks
+ noCorrespondingTracks = false
+ for jamTrack in jamTracks
+ mixer = null
+ preMasteredClass = ""
+ # find the track or tracks that correspond to the mixer
+ correspondingTracks = []
+
+ for matchMixer in @jamTrackMixers
+ if matchMixer.id == jamTrack.id
+ correspondingTracks.push(jamTrack)
+ mixer = matchMixer
+
+ if correspondingTracks.length == 0
+ noCorrespondingTracks = true
+ logger.error("could not correlate jam tracks", jamTrackMixers, jamTracks)
+ @app.notify({
+ title: "Unable to Open JamTrack",
+ text: "Could not correlate server and client tracks",
+ icon_url: "/assets/content/icon_alert_big.png"})
+ return _jamTracks
+
+ #jamTracks = $.grep(jamTracks, (value) =>
+ # $.inArray(value, correspondingTracks) < 0
+ #)
+
+ # prune found mixers
+ jamTrackMixers.splice(mixer);
+
+ oneOfTheTracks = correspondingTracks[0];
+ instrumentIcon = context.JK.getInstrumentIcon24(oneOfTheTracks.instrument.id);
+
+ part = oneOfTheTracks.part
+ part = '' unless name?
+
+ data =
+ name: jamTrackName
+ part: part
+ isOpener: isOpener
+ instrumentIcon: instrumentIcon
+ track: oneOfTheTracks
+ mixers: @mediaMixers(mixer, isOpener)
+
+ _jamTracks.push(data)
+
+ _jamTracks
+
+ resolveRecordedTracks: () ->
+ recordedTracks = []
+
+ return recordedTracks unless @recordingTrackMixers.length > 0
+
+ serverRecordedTracks = @session.recordedTracks()
+
+ isOpener = @recordingTrackMixers[0].group_id == ChannelGroupIds.MediaTrackGroup
+
+ # using the server's info in conjuction with the client's, draw the recording tracks
+ if serverRecordedTracks
+ recordingName = @session.recordingName()
+ noCorrespondingTracks = false
+ for mixer in @recordingTrackMixers
+ preMasteredClass = ""
+ correspondingTracks = []
+ for recordedTrack in serverRecordedTracks
+ if mixer.id.indexOf("L") == 0
+ if mixer.id.substring(1) == recordedTrack.client_track_id
+ correspondingTracks.push(recordedTrack)
+ else if mixer.id.indexOf("C") == 0
+ if mixer.id.substring(1) == recordedTrack.client_id
+ correspondingTracks.push(recordedTrack)
+ preMasteredClass = "pre-mastered-track"
+ else
+ # this should not be possible
+ alert("Invalid state: the recorded track had neither persisted_track_id or persisted_client_id")
+
+ if correspondingTracks.length == 0
+ noCorrespondingTracks = true
+ logger.debug("unable to correlate all recorded tracks", recordingMixers, serverRecordedTracks)
+ @app.notify({
+ title: "Unable to Open Recording",
+ text: "Could not correlate server and client tracks",
+ icon_url: "/assets/content/icon_alert_big.png"});
+ return recordedTracks
+
+ serverRecordedTracks = $.grep(serverRecordedTracks,
+ (value) =>
+ $.inArray(value, correspondingTracks) < 0
+ )
+
+ oneOfTheTracks = correspondingTracks[0]
+ instrumentIcon = context.JK.getInstrumentIcon24(oneOfTheTracks.instrument_id)
+ userName = oneOfTheTracks.user.name
+ userName = oneOfTheTracks.user.first_name + ' ' + oneOfTheTracks.user.last_name unless userName?
+
+ data =
+ recordingName: recordingName
+ isOpener: isOpener
+ userName: userName
+ instrumentIcon: instrumentIcon
+ track: oneOfTheTracks
+ mixers: @mediaMixers(mixer, isOpener)
+
+ recordedTracks.push(data)
+
+ recordedTracks
+
+ resolveMetronome: () ->
+ metronome = null
+
+ return metronome if @metronomeTrackMixers.length == 0
+
+ mixer = @metronomeTrackMixers[0]
+
+ instrumentIcon = "/assets/content/icon_metronome.png"
+
+ oppositeMixer = @getMixerByResourceId(mixer.rid, MIX_MODES.PERSONAL);
+
+ metronome =
+ instrumentIcon: instrumentIcon
+ mixers: {mixer: mixer, oppositeMixer: oppositeMixer, vuMixer: mixer, muteMixer: mixer}
+
+ metronome
+
+ resolveChatMixer: () ->
+ masterChatMixers = @mixersForGroupId(ChannelGroupIds.AudioInputChatGroup, MIX_MODES.MASTER);
+
+ return null if masterChatMixers.length == 0
+
+ personalChatMixers = @mixersForGroupId(ChannelGroupIds.AudioInputChatGroup, MIX_MODES.PERSONAL);
+
+ if personalChatMixers.length == 0
+ logger.warn("unable to find personal mixer for voice chat");
+ return null
+
+
+ masterChatMixer = masterChatMixers[0];
+ personalChatMixer = personalChatMixers[0];
+
+ {
+ master: {
+ mixer: masterChatMixer
+ muteMixer: masterChatMixer
+ vuMixer: masterChatMixer
+ oppositeMixer: personalChatMixer
+ }
+ personal: {
+ mixer: personalChatMixer
+ muteMixer: personalChatMixer
+ vuMixer: personalChatMixer
+ oppositeMixer: masterChatMixer
+ }
+ }
+
+ # supply the master mixer of a media track, and this function will harvest out the rest
+ mediaMixers:(masterMixer, isOpener) ->
+ personalMixer = if isOpener then @getMixerByResourceId(masterMixer.rid, MIX_MODES.PERSONAL) else null
+ personalVuMixer = if isOpener then personalMixer else masterMixer
+ {
+ isOpener: isOpener
+
+ master: {
+ mixer: masterMixer
+ muteMixer: masterMixer
+ vuMixer: masterMixer
+ }
+ personal: {
+ mixer: personalMixer
+ muteMixer: personalMixer
+ vuMixer: personalVuMixer
+ }
+ }
+
+
+ mixersForGroupIds: (groupIds, mixMode) ->
+ foundMixers = []
+ mixers = if mixMode == MIX_MODES.MASTER then @masterMixers else @personalMixers;
+
+ for mixer in mixers
+ for groupId in groupIds
+ if mixer.group_id == groupId
+ foundMixers.push(mixer)
+
+ foundMixers
+
+ mixersForGroupId: (groupId, mixMode) ->
+ foundMixers = [];
+ mixers = if mixMode == MIX_MODES.MASTER then @masterMixers else @personalMixers;
+ for mixer in mixers
+ if mixer.group_id == groupId
+ foundMixers.push(mixer)
+
+ foundMixers
+
+ getMixer: (mixerId, mode) ->
+ mode = @mixMode unless mode?
+ @allMixers[(if mode then 'M' else 'P') + mixerId]
+
+ getMixerByTrackId: (trackId, mode) ->
+ mixerPair = @mixersByTrackId[trackId]
+
+ return null unless mixerPair
+
+ if mode == undefined
+ return mixerPair
+
+ else
+ if mode == MIX_MODES.MASTER
+ return mixerPair.master
+ else
+ return mixerPair.personal
+
+
+ groupedMixersForClientId: (clientId, groupIds, usedMixers, mixMode) ->
+ foundMixers = {};
+ mixers = if mixMode == MIX_MODES.MASTER then @masterMixers else @personalMixers;
+
+ for mixer in mixers
+ unless mixer?
+ logger.debug("empty mixer: ", mixers)
+ continue
+
+ if mixer.client_id == clientId
+ for groupId in groupIds
+ if mixer.group_id == groupId
+ if (mixer.groupId != ChannelGroupIds.UserMusicInputGroup) && !(mixer.id of usedMixers)
+ mixers = foundMixers[mixer.group_id]
+ if !mixers
+ mixers = []
+ foundMixers[mixer.group_id] = mixers
+ mixers.push(mixer)
+
+ foundMixers
+
+ getMixerByResourceId:(resourceId, mode) ->
+ mixerPair = @mixersByResourceId[resourceId];
+
+ return null if(!mixerPair)
+
+ if !mode?
+ return mixerPair;
+ else
+ if mode == MIX_MODES.MASTER
+ return mixerPair.master
+ else
+ return mixerPair.personal
+
+
+ findMixerForTrack: (client_id, track, myTrack, mode = MIX_MODES.PERSONAL) ->
+ mixer = null # what is the best mixer for this track/client ID?
+ oppositeMixer = null # what is the corresponding mixer in the opposite mode?
+ vuMixer = null
+ muteMixer = null
+
+
+ if myTrack
+ # when it's your track, look it up by the backend resource ID
+ mixer = @getMixerByTrackId(track.client_track_id, mode)
+ vuMixer = mixer
+ muteMixer = mixer
+
+ # sanity checks
+ if mixer && mixer.group_id != ChannelGroupIds.AudioInputMusicGroup
+ logger.error("found local mixer that was not of groupID: AudioInputMusicGroup", mixer)
+
+ if mixer
+ # find the matching AudioInputMusicGroup for the opposite mode
+ oppositeMixer = @getMixerByTrackId(track.client_track_id, !mode)
+
+ if mode == MIX_MODES.PERSONAL
+ muteMixer = oppositeMixer; # make the master mixer the mute mixer
+
+ # sanity checks
+ if !oppositeMixer
+ logger.error("unable to find opposite mixer for local mixer", mixer)
+ else if oppositeMixer.group_id != ChannelGroupIds.AudioInputMusicGroup
+ logger.error("found local mixer in opposite mode that was not of groupID: AudioInputMusicGroup", mixer, oppositeMixer)
+ else
+ logger.debug("local track is not present: ", track, @allMixers)
+ else
+ switch mode
+ when MIX_MODES.MASTER
+
+ # when it's a remote track and in master mode, we should find the PeerAudioInputMusicGroup
+ mixer = @getMixerByTrackId(track.client_track_id, MIX_MODES.MASTER)
+
+ # sanity check
+ if mixer && mixer.group_id != ChannelGroupIds.PeerAudioInputMusicGroup
+ logger.error("found remote mixer that was not of groupID: PeerAudioInputMusicGroup", mixer)
+
+ vuMixer = mixer
+ muteMixer = mixer
+
+ if mixer
+ # we should be able to find a UserMusicInputGroup for this clientId in personal mode
+ oppositeMixers = @groupedMixersForClientId(client_id, [ ChannelGroupIds.UserMusicInputGroup], {}, MIX_MODES.PERSONAL)
+ if oppositeMixers[ChannelGroupIds.UserMusicInputGroup]
+ oppositeMixer = oppositeMixers[ChannelGroupIds.UserMusicInputGroup][0]
+
+ if !oppositeMixer
+ logger.error("unable to find UserMusicInputGroup corresponding to PeerAudioInputMusicGroup mixer", mixer )
+
+ when MIX_MODES.PERSONAL
+ mixers = @groupedMixersForClientId(client_id, [ ChannelGroupIds.UserMusicInputGroup], {}, MIX_MODES.PERSONAL)
+ if mixers[ChannelGroupIds.UserMusicInputGroup]
+ mixer = mixers[ChannelGroupIds.UserMusicInputGroup][0]
+
+ vuMixer = mixer
+ muteMixer = mixer
+
+ if mixer
+ # now grab the PeerAudioInputMusicGroup in master mode to satisfy the 'opposite' mixer
+ oppositeMixer = @getMixerByTrackId(track.client_track_id, MIX_MODES.MASTER)
+ if !oppositeMixer
+ logger.debug("unable to find a PeerAudioInputMusicGroup master mixer matching a UserMusicInput", client_id, track.client_track_id)
+ else if oppositeMixer.group_id != ChannelGroupIds.PeerAudioInputMusicGroup
+ logger.error("found remote mixer that was not of groupID: PeerAudioInputMusicGroup", mixer)
+
+ #vuMixer = oppositeMixer; # for personal mode, use the PeerAudioInputMusicGroup's VUs
+
+ {
+ mixer: mixer,
+ oppositeMixer: oppositeMixer,
+ vuMixer: vuMixer,
+ muteMixer: muteMixer
+ }
+
+ mute: (mixerId, mode, muting) ->
+
+ mode = @mixMode unless mode?
+
+ @fillTrackVolumeObject(mixerId, mode)
+
+ context.trackVolumeObject.mute = muting
+
+ context.jamClient.SessionSetControlState(mixerId, mode)
+
+ # keep state of mixer in sync with backend
+ mixer = @getMixer(mixerId, mode)
+ mixer.mute = muting
+
+ faderChanged: (data, mixers, groupId) ->
+ for mixer in mixers
+ broadcast = !(data.dragging) # If fader is still dragging, don't broadcast
+ mixer = @fillTrackVolumeObject(mixer.id, mixer.mode, broadcast)
+
+ @setMixerVolume(mixer, data.percentage)
+
+ # keep state of mixer in sync with backend
+ mixer = @getMixer(mixer.id, mixer.mode)
+ mixer.volume_left = context.trackVolumeObject.volL
+
+ if groupId == ChannelGroupIds.UserMusicInputGroup
+ # there may be other mixers with this same ID in the case of a Peer Music Stream, so update them as well
+ context.JK.FaderHelpers.setFaderValue(mixerId, data.percentage)
+
+ initGain: (mixer) ->
+ gainPercent = context.JK.FaderHelpers.convertAudioTaperToPercent(mixer.volume_left)
+ context.JK.FaderHelpers.setFaderValue(mixer.id, gainPercent)
+ context.JK.FaderHelpers.showFader(mixer.id)
+
+ panChanged: (data, mixers, groupId) ->
+ # media tracks are the only controls that sometimes set two mixers right now
+ for mixer in mixers
+ broadcast = !(data.dragging) # If fader is still dragging, don't broadcast
+ mixer = @fillTrackVolumeObject(mixer.id, mixer.mode, broadcast)
+
+ @setMixerPan(mixer, data.percentage)
+
+ # keep state of mixer in sync with backend
+ mixer = @getMixer(mixer.id, mixer.mode)
+ mixer.pan = context.trackVolumeObject.pan
+
+ initPan: (mixer) ->
+ panPercent= context.JK.PanHelpers.convertPanToPercent(mixer.pan)
+ context.JK.FaderHelpers.setFaderValue(mixer.id, panPercent, Math.abs(mixer.pan))
+ context.JK.FaderHelpers.showFader(mixer.id)
+
+ setMixerPan: (mixer, panPercent) ->
+
+ context.trackVolumeObject.pan = context.JK.PanHelpers.convertPercentToPan(panPercent);
+ context.jamClient.SessionSetControlState(mixer.id, mixer.mode);
+
+ loopChanged: (mixer, shouldLoop) ->
+
+ @fillTrackVolumeObject(mixer.id, mixer.mode)
+ context.trackVolumeObject.loop = shouldLoop
+ context.jamClient.SessionSetControlState(mixer.id, mixer.mode)
+
+ # keep state of mixer in sync with backend
+ mixer = @getMixer(mixer.id, mixer.mode)
+ mixer.loop = context.trackVolumeObject.loop
+
+ setMixerVolume: (mixer, volumePercent) ->
+ ###
+ // The context.trackVolumeObject has been filled with the mixer values
+ // that go with mixerId, and the range of that mixer
+ // has been set in currentMixerRangeMin-Max.
+ // All that needs doing is to translate the incoming percent
+ // into the real value ont the sliders range. Set Left/Right
+ // volumes on trackVolumeObject, and call SetControlState to stick.
+ ###
+
+ context.trackVolumeObject.volL = context.JK.FaderHelpers.convertPercentToAudioTaper(volumePercent);
+ context.trackVolumeObject.volR = context.JK.FaderHelpers.convertPercentToAudioTaper(volumePercent);
+
+ context.jamClient.SessionSetControlState(mixer.id, mixer.mode);
+
+ percentFromMixerValue: (min, max, value) ->
+ try
+ range = Math.abs(max - min)
+ magnitude = value - min
+ percent = Math.round(100*(magnitude/range))
+ percent
+ catch err
+ 0
+
+
+ percentToMixerValue:(min, max, percent) ->
+ range = Math.abs(max - min);
+ multiplier = percent/100; # Change 85 into 0.85
+ value = min + (multiplier * range);
+
+ # Protect against percents < 0 and > 100
+ if value < min
+ value = min;
+
+ if value > max
+ value = max;
+
+ return value;
+
+ fillTrackVolumeObject: (mixerId, mode, broadcast) ->
+ _broadcast = true
+ if broadcast?
+ _broadcast = broadcast
+
+ mixer = @getMixer(mixerId, mode)
+ context.trackVolumeObject.clientID = mixer.client_id
+ context.trackVolumeObject.broadcast = _broadcast
+ context.trackVolumeObject.master = mixer.master
+ context.trackVolumeObject.monitor = mixer.monitor
+ context.trackVolumeObject.mute = mixer.mute
+ context.trackVolumeObject.name = mixer.name
+ context.trackVolumeObject.record = mixer.record
+ context.trackVolumeObject.volL = mixer.volume_left
+ context.trackVolumeObject.pan = mixer.pan
+
+ # today we treat all tracks as mono, but this is required to make a stereo track happy
+ # context.trackVolumeObject.volR = mixer.volume_right;
+ context.trackVolumeObject.volR = mixer.volume_left;
+
+ context.trackVolumeObject.loop = mixer.loop;
+ # trackVolumeObject doesn't have a place for range min/max
+ @currentMixerRangeMin = mixer.range_low;
+ @currentMixerRangeMax = mixer.range_high;
+ mixer
+
+ collectStats: (mixer) ->
+ mixerStats = @vuStats[mixer.id]
+
+ unless mixerStats?
+ mixerStats = {count: 0, group_name: context.JK.groupIdDisplay(mixer)}
+ @vuStats[mixer.id] = mixerStats
+
+ mixerStats.count++
+
+ dumpVUStats: () ->
+
+ # to use: check MixerStore for setInterval in cstr
+ logger.debug("VU STAT DUMP")
+ for mixerId, mixerStat of @vuStats
+ logger.debug("VU STAT: #{mixerState.group_name} count=#{mixerStat.count}")
+
+ updateVU: (mixerId, mode, leftValue, leftClipping, rightValue, rightClipping) ->
+ mixer = @getMixer(mixerId, mode)
+
+ if mixer?
+ @collectStats(mixer) if @shouldCollectVuStats
+ context.JK.VuHelpers.updateVU3(mixer, leftValue, leftClipping, rightValue, rightClipping)
+
+ ###
+ if mixer
+ if mixer.stereo # // stereo track
+ if mixerId.substr(-4) == "_vul"
+ context.JK.VuHelpers.updateVU2('vul', mixer, value)
+ else
+ context.JK.VuHelpers.updateVU2('vur', mixer, value)
+ else
+ if mixerId.substr(-4) == "_vul"
+ # Do the left
+ context.JK.VuHelpers.updateVU2('vul', mixer, value)
+ # Do the right
+ context.JK.VuHelpers.updateVU2('vur', mixer, value)
+ ###
+ getTrackInfo: () ->
+ context.JK.TrackHelpers.getTrackInfo(context.jamClient, @masterMixers)
+
+ getGroupMixer: (categoryId, mode) ->
+ groupId = if mode == MIX_MODES.MASTER then ChannelGroupIds.MasterCatGroup else ChannelGroupIds.MonitorCatGroup
+ mixers = @mixersForGroupId(groupId, mode)
+
+ if mixers.length == 0
+ logger.warn("could not find mixer with group ID: " + groupId + ', mode:' + mode)
+ return null
+
+ found = null
+ for mixer in mixers
+ if mixer.name == categoryId
+ found = mixer
+ break
+
+ unless found?
+ logger.warn("could not find mixer with categoryId: " + categoryId)
+ return null
+ else
+ {
+ mixer: found,
+ muteMixer : found,
+ vuMixer: found,
+ oppositeMixer: found
+ }
+
+ getAudioInputCategoryMixer: (mode) ->
+ @getGroupMixer(CategoryGroupIds.AudioInputMusic, mode)
+
+ getChatCategoryMixer: (mode) ->
+ @getGroupMixer(CategoryGroupIds.AudioInputChat, mode)
+
+ getMediaCategoryMixer: (mode) ->
+ @getGroupMixer(CategoryGroupIds.MediaTrack, mode)
+
+ getUserMediaCategoryMixer: (mode) ->
+ @getGroupMixer(CategoryGroupIds.UserMedia, mode)
+
+
+ refreshMixer: (mixers) ->
+ return null unless mixers? && mixers.mixer?
+
+ mixer = @getMixer(mixers.mixer.id, mixers.mixer.mode)
+
+ if mixer?
+ oppositeMixer = if mixers.oppositeMixer then @getMixer(mixers.oppositeMixer.id, mixers.oppositeMixer.mode) else null
+ {
+ mixer: mixer
+ vuMixer: @getMixer(mixers.vuMixer.id, mixers.vuMixer.mode)
+ muteMixer: @getMixer(mixers.muteMixer.id, mixers.muteMixer.mode)
+ oppositeMixer: oppositeMixer
+ }
+ else
+ return null
+
+
+ recordingName: () ->
+ @session.recordingName()
+
+ jamTrackName: () ->
+ @session.jamTrackName()
diff --git a/web/app/assets/javascripts/react-components/helpers/SessionHelper.js.coffee b/web/app/assets/javascripts/react-components/helpers/SessionHelper.js.coffee
new file mode 100644
index 000000000..8fe3d6098
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/helpers/SessionHelper.js.coffee
@@ -0,0 +1,112 @@
+context = window
+
+@SessionHelper = class SessionHelper
+
+ constructor: (app, session, participantsEverSeen, isRecording, downloadingJamTrack) ->
+ @app = app
+ @session = session
+ @participantsEverSeen = participantsEverSeen
+ @isRecording = isRecording
+ @downloadingJamTrack = downloadingJamTrack
+
+ inSession: () ->
+ @session?
+
+ participants: () ->
+ if @session
+ return @session.participants
+ else
+ []
+
+ otherParticipants: () ->
+ others = []
+ for participant in @participants()
+ myTrack = @app.clientId == participant.client_id
+
+ others.push(participant) unless myTrack
+ others
+
+ # if any participant has the metronome open, then we say this session has the metronome open
+ isMetronomeOpen: () ->
+ metronomeOpen = false;
+ for participant in @participants()
+ if participant.metronome_open
+ metronomeOpen = true
+ break
+
+ metronomeOpen
+
+ isPlayingRecording: () ->
+ # this is the server's state; there is no guarantee that the local tracks
+ # requested from the backend will have corresponding track information
+ return !!(@session && @session.claimed_recording);
+
+ recordedTracks: () ->
+ if @session && @session.claimed_recording
+ @session.claimed_recording.recording.recorded_tracks
+ else
+ null
+
+ recordedBackingTracks: () ->
+ if @session && @session.claimed_recording
+ @session.claimed_recording.recording.recorded_backing_tracks
+ else
+ null
+
+ backingTracks: () ->
+ backingTracks = []
+ # this may be wrong if we loosen the idea that only one person can have a backing track open.
+ # but for now, the 1st person we find with a backing track open is all there is to find...
+
+ for participant in @participants()
+ if participant.backing_tracks.length > 0
+ backingTracks = participant.backing_tracks
+ break
+
+ backingTracks
+
+ backingTrack: () ->
+ result = null
+ if @session
+ # TODO: objectize this for VRFS-2665, VRFS-2666, VRFS-2667, VRFS-2668
+ result =
+ path: @session.backing_track_path
+ result
+
+ jamTracks: () ->
+ if @session && @session.jam_track
+ @session.jam_track.tracks.filter((track)->
+ track.track_type == 'Track'
+ )
+ else
+ null
+
+ jamTrackName: () ->
+ @session?.jam_track?.name
+
+ recordedJamTracks:() ->
+ if @session && @session.claimed_recording
+ @session.claimed_recording.recording.recorded_jam_track_tracks
+ else
+ null
+
+ recordedJamTrackName: () ->
+ jam_track = @session?.claimed_recording?.recording?.jam_track
+
+ if jam_track? then jam_track.name else null
+
+ recordingName: () ->
+ @session?.claimed_recording?.name
+
+ getParticipant: (clientId) ->
+ found = null
+ for participant in @participants()
+ if participant.client_id == clientId
+ found = participant
+ break
+
+ logger.warn('unable to find participant with clientId: ' + clientId) unless found
+ found
+
+ id: () ->
+ @session.id
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/landing/InvidualJamTrackPage.js.jsx.coffee b/web/app/assets/javascripts/react-components/landing/InvidualJamTrackPage.js.jsx.coffee
new file mode 100644
index 000000000..809a273a6
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/landing/InvidualJamTrackPage.js.jsx.coffee
@@ -0,0 +1,52 @@
+context = window
+
+@IndividualJamTrackPage = React.createClass({
+
+ watchVideo: (e) ->
+ e.preventDefault()
+ window.open("/popups/youtube/player?id=askHvcCoNfw", 'What Are JamTracks?', 'scrollbars=yes,toolbar=no,status=no,height=282,width=500')
+
+ render: () ->
+
+ header = null
+ if @props.band
+ header = "#{@props.jam_track.original_artist} Backing Tracks - Complete Multitracks"
+ else if @props.generic?
+ header = "Backing Tracks + Free Amazing App = Unmatched Experience"
+ else
+ header = "#{@props.jam_track.name} Backing Track by #{@props.jam_track.original_artist}"
+
+
+ `
+
+
{header}
+
+
+
+
Here's Why 20,000 Musicians Love Our Backing Tracks
+
JamKazam gives you a better backing track experience:
+
+ Full multitrack recordings with isolated track for each part
+ Free JamKazam app to:
+
+ Hear just the part you want to play to learn it
+ Mute the part you want to play, and play live with other parts
+ Record and mix your live play with unmuted tracks
+
+
+ Free Internet Service to play this track live online with others
+
+
Watch A Video To See How It Works
+
+
+
Preview "{this.props.jam_track.name}" Backing Track by {this.props.jam_track.original_artist}
+
Click the play buttons below to preview the master mix and fully isolated tracks of the professional backing track recording. All are included in your backing track.
+
+
+
+
+
+
+
+
`
+})
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/landing/JamTrackCta.js.jsx.coffee b/web/app/assets/javascripts/react-components/landing/JamTrackCta.js.jsx.coffee
new file mode 100644
index 000000000..de888990e
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/landing/JamTrackCta.js.jsx.coffee
@@ -0,0 +1,60 @@
+context = window
+rest = context.JK.Rest()
+
+@JamTrackCta = React.createClass({
+
+ redeem: (e) ->
+ e.preventDefault()
+
+ return if @state.processing
+
+ isFree = context.JK.currentUserFreeJamTrack
+
+ rest.addJamtrackToShoppingCart({id: @props.jam_track.id}).done((response) =>
+ if(isFree)
+ if context.JK.currentUserId?
+ context.JK.currentUserFreeJamTrack = true # make sure the user sees no more free notices
+ context.location = '/client#/redeemComplete'
+ else
+ # now make a rest call to buy it
+ context.location = '/client#/redeemSignup'
+
+ else
+ context.location = '/client#/shoppingCart'
+
+ ).fail((jqXHR, textStatus, errorMessage) =>
+ if jqXHR.status == 422
+ errors = JSON.parse(jqXHR.responseText)
+ cart_errors = errors?.errors?.cart_id
+ if cart_errors?.length == 1 && cart_errors[0] == 'has already been taken'
+ context.location = '/client#/shoppingCart'
+ else
+ context.JK.app.ajaxError(jqXHR, textStatus, errorMessage)
+ @setState({processing:false})
+ )
+
+ @setState({processing:true})
+
+ getInitialState:() ->
+ {processing: false}
+
+ render: () ->
+ bandBrowseUrl = "/client?artist=#{this.props.jam_track.original_artist}#/jamtrackBrowse"
+
+ `
+
+
+
+
+
$1.99 value
+
+
+
+
+
+
`
+})
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/landing/PopupYoutubePlayer.js.jsx.coffee b/web/app/assets/javascripts/react-components/landing/PopupYoutubePlayer.js.jsx.coffee
new file mode 100644
index 000000000..2d7ef7ce1
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/landing/PopupYoutubePlayer.js.jsx.coffee
@@ -0,0 +1,11 @@
+context = window
+
+@PopupYoutubePlayer = React.createClass({
+
+ render: () ->
+ video_url = "//www.youtube.com/embed/#{this.props.video_id}"
+
+ `
+
+
`
+})
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/mixins/MasterPersonalMixersMixin.js.coffee b/web/app/assets/javascripts/react-components/mixins/MasterPersonalMixersMixin.js.coffee
new file mode 100644
index 000000000..553aa3a5e
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/mixins/MasterPersonalMixersMixin.js.coffee
@@ -0,0 +1,17 @@
+context = window
+MIX_MODES = context.JK.MIX_MODES
+
+@MasterPersonalMixersMixin = {
+
+ mixer: () ->
+ if @props.mode == MIX_MODES.MASTER
+ @props.mixers['master'].mixer
+ else
+ @props.mixers['personal'].mixer
+
+ mixers: () ->
+ if @props.mode == MIX_MODES.MASTER
+ @props.mixers['master']
+ else
+ @props.mixers['personal']
+}
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/mixins/SessionMediaTracksMixin.js.coffee b/web/app/assets/javascripts/react-components/mixins/SessionMediaTracksMixin.js.coffee
new file mode 100644
index 000000000..9517dff6f
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/mixins/SessionMediaTracksMixin.js.coffee
@@ -0,0 +1,51 @@
+context = window
+MIX_MODES = context.JK.MIX_MODES
+logger = context.JK.logger
+
+@SessionMediaTracksMixin = {
+
+ metronomeTrulyGoneCheck: () ->
+
+ logger.debug("metronome is completely gone")
+ @setState({metronomeFlickerTimeout: null})
+
+ onInputsChanged: (sessionMixers) ->
+
+ session = sessionMixers.session
+ mixers = sessionMixers.mixers
+
+ # the backend delete/adds the metronome rapidly when the user hits play. this is custom code to deal with that
+
+ metronomeFlickerTimeout = @state.metronomeFlickerTimeout
+
+ if mixers.metronome?
+ if metronomeFlickerTimeout?
+ logger.debug("canceling metronome flicker timeout because metronome mixer reappeared")
+ clearTimeout(metronomeFlickerTimeout)
+ metronomeFlickerTimeout = null
+ else
+ if @state.metronomeIsShowing
+ logger.debug("setting metronome flicker timeout")
+ clearTimeout(metronomeFlickerTimeout) if metronomeFlickerTimeout?
+ metronomeFlickerTimeout = setTimeout(@metronomeTrulyGoneCheck, 1000)
+
+ metronomeIsShowing = mixers.metronome?
+
+ state =
+ isRecording: session.isRecording
+ mediaSummary: mixers.mediaSummary
+ backingTracks: mixers.backingTracks
+ jamTracks: mixers.jamTracks
+ recordedTracks: mixers.recordedTracks
+ metronome: mixers.metronome
+ mediaCategoryMixer: mixers.getMediaCategoryMixer(@props.mode)
+ recordingName: mixers.recordingName()
+ jamTrackName: mixers.jamTrackName()
+ metronomeIsShowing: metronomeIsShowing
+ metronomeFlickerTimeout: metronomeFlickerTimeout
+
+ @inputsChangedProcessed(state) if @inputsChangedProcessed?
+
+ @setState(state)
+
+}
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/mixins/SessionMyTracksMixin.js.coffee b/web/app/assets/javascripts/react-components/mixins/SessionMyTracksMixin.js.coffee
new file mode 100644
index 000000000..a692f171a
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/mixins/SessionMyTracksMixin.js.coffee
@@ -0,0 +1,45 @@
+context = window
+
+@SessionMyTracksMixin = {
+
+ onInputsChanged: (sessionMixers) ->
+
+
+ session = sessionMixers.session
+ mixers = sessionMixers.mixers
+
+ tracks = []
+
+ if session.inSession()
+ participant = session.getParticipant(@app.clientId)
+
+ if participant
+ photoUrl = context.JK.resolveAvatarUrl(participant.user.photo_url);
+
+ chatMixer = mixers.chatMixer
+ chat = null
+ if chatMixer
+ chat =
+ mixers: chatMixer
+ mode: @props.mode
+ photoUrl: photoUrl
+
+ name = participant.user.name;
+
+ for track in participant.tracks
+ # try to find mixer info for this track
+ mixerFinder = [participant.client_id, track, true] # so that other callers can re-find their mixer data
+ mixerData = mixers.findMixerForTrack(participant.client_id, track, true, @props.mode)
+
+ # todo: sessionModel.setAudioEstablished
+
+ instrumentIcon = context.JK.getInstrumentIcon45(track.instrument_id);
+
+ tracks.push({track: track, mixerFinder: mixerFinder, mixers: mixerData, name: name, instrumentIcon: instrumentIcon, photoUrl: photoUrl, clientId: participant.client_id})
+
+
+ else
+ logger.warn("SessionMyTracks: unable to find participant")
+
+ this.setState(tracks: tracks, session:session, chat: chat)
+}
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/mixins/SessionOtherTracksMixin.js.coffee b/web/app/assets/javascripts/react-components/mixins/SessionOtherTracksMixin.js.coffee
new file mode 100644
index 000000000..adda6eac3
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/mixins/SessionOtherTracksMixin.js.coffee
@@ -0,0 +1,6 @@
+context = window
+
+@SessionOtherTracksMixin = {
+
+
+}
\ No newline at end of file
diff --git a/web/app/assets/javascripts/react-components/stores/AppStore.js.coffee b/web/app/assets/javascripts/react-components/stores/AppStore.js.coffee
new file mode 100644
index 000000000..bb5a11d98
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/stores/AppStore.js.coffee
@@ -0,0 +1,12 @@
+$ = jQuery
+context = window
+logger = context.JK.logger
+
+@AppStore = Reflux.createStore(
+ {
+ listenables: @AppActions
+
+ onAppInit: (app) ->
+ @trigger(app)
+ }
+)
diff --git a/web/app/assets/javascripts/react-components/stores/MediaPlaybackStore.js.coffee b/web/app/assets/javascripts/react-components/stores/MediaPlaybackStore.js.coffee
new file mode 100644
index 000000000..051e0047e
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/stores/MediaPlaybackStore.js.coffee
@@ -0,0 +1,120 @@
+$ = jQuery
+context = window
+logger = context.JK.logger
+PLAYBACK_MONITOR_MODE = context.JK.PLAYBACK_MONITOR_MODE
+RecordingActions = @RecordingActions
+
+@MediaPlaybackStore = Reflux.createStore(
+ {
+ listenables: @MediaPlaybackActions
+
+ playbackStateChanged: false
+ positionUpdateChanged: false
+ currentTimeChanged: false
+ playbackState: null
+ positionMs: 0
+ durationMs: 0
+ isRecording: false
+ sessionHelper: null
+
+
+ init: () ->
+ this.listenTo(context.SessionStore, this.onSessionChanged);
+
+ onCurrentTimeChanged: (time) ->
+ @time = time
+ @currentTimeChanged = true
+ @issueChange()
+
+ onSessionChanged: (session) ->
+ @isRecording = session.isRecording
+ @sessionHelper = session
+
+ onMediaStartPlay: (data) ->
+ logger.debug("calling jamClient.SessionStartPlay");
+ context.jamClient.SessionStartPlay(data.playbackMode);
+
+ onMediaStopPlay: (data) ->
+ # if a JamTrack is open, and the user hits 'pause' or 'stop', we need to automatically stop the recording
+ if @sessionHelper.jamTracks() && @isRecording
+ logger.debug("preemptive jamtrack stop")
+ @startStopRecording();
+
+ if !data.endReached
+ logger.debug("calling jamClient.SessionStopPlay. endReached:", data.endReached)
+ context.jamClient.SessionStopPlay()
+
+ onMediaPausePlay: (data) ->
+ # if a JamTrack is open, and the user hits 'pause' or 'stop', we need to automatically stop the recording
+ if @sessionHelper.jamTracks() && @isRecording
+ logger.debug("preemptive jamtrack stop")
+ @startStopRecording();
+
+
+ if !data.endReached
+ logger.debug("calling jamClient.SessionPausePlay. endReached:", data.endReached)
+ context.jamClient.SessionPausePlay()
+
+ startStopRecording: () ->
+ if @isRecording
+ RecordingActions.stopRecording.trigger()
+ else
+ RecordingActions.startRecording.trigger()
+
+ onMediaChangePosition: (data) ->
+ seek = data.positionMs;
+
+ if data.playbackMonitorMode == PLAYBACK_MONITOR_MODE.JAMTRACK
+ # if positionMs == 0, then seek it back to whatever the earliest play start is to catch all the prelude
+
+ if(seek == 0)
+ duration = context.jamClient.SessionGetJamTracksPlayDurationMs();
+ seek = duration.start;
+
+ logger.debug("calling jamClient.SessionTrackSeekMs(" + seek + ")");
+
+ if data.playbackMonitorMode == PLAYBACK_MONITOR_MODE.JAMTRACK
+ context.jamClient.SessionJamTrackSeekMs(seek);
+ else
+ context.jamClient.SessionTrackSeekMs(seek);
+
+
+ issueChange: () ->
+
+ @state =
+ playbackState: @playbackState
+ playbackStateChanged: @playbackStateChanged
+ positionUpdateChanged: @positionUpdateChanged
+ currentTimeChanged: @currentTimeChanged
+ positionMs: @positionMs
+ durationMs: @durationMs
+ isPlaying: @isPlaying
+ time: @time
+
+ this.trigger(@state)
+ @playbackStateChanged = false
+ @positionUpdateChanged = false
+ @currentTimeChanged = false
+
+ onPlaybackStateChange: (text) ->
+ @playbackState = text
+ @playbackStateChanged = true
+
+ @issueChange()
+
+ onPositionUpdate: (playbackMode) ->
+ if playbackMode == PLAYBACK_MONITOR_MODE.JAMTRACK
+ @positionMs = context.jamClient.SessionCurrrentJamTrackPlayPosMs()
+ duration = context.jamClient.SessionGetJamTracksPlayDurationMs()
+ @durationMs = duration.media_len
+ else
+ @positionMs = context.jamClient.SessionCurrrentPlayPosMs()
+ @durationMs = context.jamClient.SessionGetTracksPlayDurationMs()
+
+ @isPlaying = context.jamClient.isSessionTrackPlaying()
+
+ @positionUpdateChanged = true
+ @issueChange()
+
+ }
+)
diff --git a/web/app/assets/javascripts/react-components/stores/MixerStore.js.coffee b/web/app/assets/javascripts/react-components/stores/MixerStore.js.coffee
new file mode 100644
index 000000000..c2b57fe00
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/stores/MixerStore.js.coffee
@@ -0,0 +1,244 @@
+context = window
+logger = context.JK.logger
+MIX_MODES = context.JK.MIX_MODES
+rest = context.JK.Rest()
+
+@MixerStore = Reflux.createStore(
+ {
+ METRO_SOUND_LOOKUP: {
+ 0 : "BuiltIn",
+ 1 : "SineWave",
+ 2 : "Beep",
+ 3 : "Click",
+ 4 : "Kick",
+ 5 : "Snare",
+ 6 : "MetroFile"
+ }
+
+ metro: {tempo: 120, cricket: false, sound: "Beep" }
+ noAudioUsers : {}
+
+ init: ->
+ # Register with the app store to get @app
+ this.listenTo(context.AppStore, this.onAppInit);
+ this.listenTo(context.SessionStore, this.onSessionChange)
+ this.listenTo(context.MixerActions.mute, this.onMute)
+ this.listenTo(context.MixerActions.faderChanged, this.onFaderChanged)
+ this.listenTo(context.MixerActions.initGain, this.onInitGain)
+ this.listenTo(context.MixerActions.initPan, this.onInitPan)
+ this.listenTo(context.MixerActions.panChanged, this.onPanChanged)
+ this.listenTo(context.MixerActions.mixersChanged, this.onMixersChanged)
+ this.listenTo(context.MixerActions.syncTracks, this.onSyncTracks)
+ this.listenTo(context.MixerActions.mixerModeChanged, this.onMixerModeChanged)
+ this.listenTo(context.MixerActions.loopChanged, this.onLoopChanged)
+ this.listenTo(context.MixerActions.openMetronome, this.onOpenMetronome)
+ this.listenTo(context.MixerActions.metronomeChanged, this.onMetronomeChanged)
+ this.listenTo(context.MixerActions.deadUserRemove, this.onDeadUserRemove)
+
+ context.JK.HandleVolumeChangeCallback2 = @handleVolumeChangeCallback
+ context.JK.HandleMetronomeCallback2 = @handleMetronomeCallback
+ context.JK.HandleBridgeCallback2 = @handleBridgeCallback
+ context.JK.HandleBackingTrackSelectedCallback2 = @handleBackingTrackSelectedCallback
+
+ #setInterval(@dumpVUStats, 5000)
+
+ dumpVUStats: () ->
+ @mixers.dumpVUStats() if @mixers?
+
+ issueChange: () ->
+ @trigger({session: @session, mixers: @mixers})
+
+ handleVolumeChangeCallback: (mixerId, isLeft, value, isMuted) ->
+ # TODO
+ # Visually update mixer
+ # There is no need to actually set the back-end mixer value as the
+ # back-end will already have updated the audio mixer directly prior to sending
+ # me this event. I simply need to visually show the new fader position.
+ # TODO: Use mixer's range
+ #faderValue = percentFromMixerValue(-80, 20, value);
+ #context.JK.FaderHelpers.setFaderValue(mixerId, faderValue);
+ #var $muteControl = $('[control="mute"][mixer-id="' + mixerId + '"]');
+ #_toggleVisualMuteControl($muteControl, isMuted);
+ logger.debug("volume change")
+
+
+ handleMetronomeCallback: (args) ->
+ logger.debug("MetronomeCallback: ", args)
+ @metro.tempo = args.bpm
+ @metro.cricket = args.cricket;
+ @metro.sound = @METRO_SOUND_LOOKUP[args.sound];
+
+ # This isn't actually there, so we rely on the metroSound as set from select on form:
+ # metroSound = args.sound
+ SessionActions.syncWithServer()
+
+ @mixers = new context.MixerHelper(@session, @masterMixers, @personalMixers, @metro, @noAudioUsers, @mixers?.mixMode || MIX_MODES.PERSONAL)
+
+ @issueChange()
+
+ handleBridgeCallback: (vuData) ->
+
+ eventName = null
+ mixerId = null
+ value = null
+ vuInfo = null
+
+ for vuInfo in vuData
+ eventName = vuInfo[0];
+ vuVal = 0.0;
+ if eventName == "vu"
+ mixerId = vuInfo[1];
+ mode = vuInfo[2];
+ leftValue = vuInfo[3];
+ leftClipping = vuInfo[4];
+ rightValue = vuInfo[5];
+ rightClipping = vuInfo[6];
+ # TODO - no guarantee range will be -80 to 20. Get from the
+ # GetControlState for this mixer which returns min/max
+ # value is a DB value from -80 to 20. Convert to float from 0.0-1.0
+
+ @mixers.updateVU(mixerId, mode, (leftValue + 80) / 80, leftClipping, (rightValue + 80) / 80, rightClipping)
+ #@mixers.updateVU(mixerId + "_vur", (rightValue + 80) / 80, rightClipping)
+
+
+ handleBackingTrackSelectedCallback: () ->
+ logger.debug("backing track selected")
+
+ onAppInit: (@app) ->
+ @gearUtils = context.JK.GearUtilsInstance
+ @sessionUtils = context.JK.SessionUtils
+
+ context.jamClient.SetVURefreshRate(150)
+ context.jamClient.RegisterVolChangeCallBack("JK.HandleVolumeChangeCallback2")
+ context.jamClient.setMetronomeOpenCallback("JK.HandleMetronomeCallback2")
+
+
+ sessionEnded: () ->
+ @noAudioUsers = {}
+
+ onSessionChange: (session) ->
+
+ @sessionEnded() unless session.inSession()
+
+ @session = session
+
+ @masterMixers = context.jamClient.SessionGetAllControlState(true);
+ @personalMixers = context.jamClient.SessionGetAllControlState(false);
+
+ @mixers = new context.MixerHelper(@session, @masterMixers, @personalMixers, @metro, @noAudioUsers, @mixers?.mixMode || MIX_MODES.PERSONAL)
+
+ @issueChange()
+
+ onMute: (mixers, muting) ->
+
+ for mixer in mixers
+ @mixers.mute(mixer.id, mixer.mode, muting);
+
+ # simulate a state change to cause a UI redraw
+ @issueChange()
+
+ onFaderChanged: (data, mixers, groupId) ->
+
+ @mixers.faderChanged(data, mixers, groupId)
+
+ @issueChange()
+
+ onPanChanged: (data, mixers, groupId) ->
+ @mixers.panChanged(data, mixers, groupId)
+
+ @issueChange()
+
+ onLoopChanged: (mixer, shouldLoop) ->
+ @mixers.loopChanged(mixer, shouldLoop)
+
+ onOpenMetronome: () ->
+ context.jamClient.SessionStopPlay()
+ context.jamClient.SessionOpenMetronome(@mixers.metro.tempo, @mixers.metro.sound, 1, 0)
+
+ onMetronomeChanged: (tempo, sound) ->
+ logger.debug("onMetronomeChanged", tempo, sound)
+
+ @metro.tempo = tempo
+ @metro.sound = sound
+ context.jamClient.SessionSetMetronome(@metro.tempo, @metro.sound, 1, 0);
+
+ @mixers = new context.MixerHelper(@session, @masterMixers, @personalMixers, @metro, @noAudioUsers, @mixers?.mixMode || MIX_MODES.PERSONAL)
+ @issueChange()
+
+ onDeadUserRemove: (clientId) ->
+ return unless @session.inSession()
+
+ participant = @session.participantsEverSeen[clientId];
+
+ if participant?
+ logger.debug("todo :notify dead user")
+ # XXX TODO trigger some notification store
+
+ #app.notify({
+ # "title": ALERT_TYPES[type].title,
+ # "text": participant.user.name + " is no longer sending audio.",
+ # "icon_url": context.JK.resolveAvatarUrl(participant.user.photo_url)
+ #});
+
+ @noAudioUsers[clientId] = true
+
+ @issueChange()
+
+ onInitGain: (mixer) ->
+ @mixers.initGain(mixer)
+
+ onInitPan: (mixer) ->
+ @mixers.initPan(mixer)
+
+ onMixersChanged: (type, text) ->
+ @masterMixers = context.jamClient.SessionGetAllControlState(true);
+ @personalMixers = context.jamClient.SessionGetAllControlState(false);
+
+ logger.debug("MixerStore: onMixersChanged")
+
+ @mixers = new context.MixerHelper(@session, @masterMixers, @personalMixers, @metro, @noAudioUsers, @mixers?.mixMode || MIX_MODES.PERSONAL)
+
+ SessionActions.mixersChanged.trigger(type, text, @mixers.getTrackInfo())
+
+ @issueChange()
+
+ onMixerModeChanged: (mode) ->
+ if mode == MIX_MODES.MASTER
+ @app.layout.showDialog('session-master-mix-dialog') unless @app.layout.isDialogShowing('session-master-mix-dialog')
+ else
+ @app.layout.closeDialog('session-master-mix-dialog') if @app.layout.isDialogShowing('session-master-mix-dialog')
+
+ onSyncTracks: () ->
+ logger.debug("MixerStore: onSyncTracks")
+ unless @session.inSession()
+ logger.debug("dropping queued up sync tracks because no longer in session")
+ return
+
+ allTracks = @mixers.getTrackInfo()
+
+ inputTracks = allTracks.userTracks;
+ backingTracks = allTracks.backingTracks;
+ metronomeTracks = allTracks.metronomeTracks;
+
+ # create a trackSync request based on backend data
+ syncTrackRequest = {}
+ syncTrackRequest.client_id = @app.clientId
+ syncTrackRequest.tracks = inputTracks
+ syncTrackRequest.backing_tracks = backingTracks
+ syncTrackRequest.metronome_open = metronomeTracks.length > 0
+ syncTrackRequest.id = @session.id()
+
+ rest.putTrackSyncChange(syncTrackRequest)
+ .fail((jqXHR)=>
+ if jqXHR.status != 404
+ @app.notify({
+ "title": "Can't Sync Local Tracks",
+ "text": "The client is unable to sync local track information with the server. You should rejoin the session to ensure a good experience.",
+ "icon_url": "/assets/content/icon_alert_big.png"
+ })
+
+ else
+ logger.debug("Unable to sync local tracks because session is gone.")
+ )
+ }
+)
diff --git a/web/app/assets/javascripts/react-components/stores/RecordingStore.js.jsx.coffee b/web/app/assets/javascripts/react-components/stores/RecordingStore.js.jsx.coffee
new file mode 100644
index 000000000..7e9543b70
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/stores/RecordingStore.js.jsx.coffee
@@ -0,0 +1,71 @@
+$ = jQuery
+context = window
+logger = context.JK.logger
+
+@RecordingStore = Reflux.createStore(
+ {
+ listenables: @RecordingActions
+ recordingWindow: null
+
+
+ init: ->
+ # Register with the app store to get @app
+ this.listenTo(context.AppStore, this.onAppInit)
+
+ onAppInit: (app) ->
+ @app = app
+
+ onInitModel: (recordingModel) ->
+ @recordingModel = recordingModel
+ this.trigger({isRecording: @recordingModel.isRecording()})
+
+ onStartRecording: () ->
+ @recordingModel.startRecording()
+
+ onStopRecording: () ->
+ @recordingModel.stopRecording()
+
+ onStartingRecording: (details) ->
+ details.cause = 'starting'
+ this.trigger(details)
+
+ @popupRecordingControls() unless @recordingWindow?
+
+ onStartedRecording: (details) ->
+ details.cause = 'started'
+ this.trigger(details)
+
+ @popupRecordingControls() unless @recordingWindow?
+
+ onStoppingRecording: (details) ->
+ details.cause = 'stopping'
+ this.trigger(details)
+
+ onStoppedRecording: (details) ->
+ details.cause = 'stopped'
+ this.trigger(details)
+
+ onAbortedRecording: (details) ->
+ details.cause = 'aborted'
+ this.trigger(details)
+
+ onOpenRecordingControls: () ->
+ logger.debug("recording controls opening")
+
+ if @recordingWindow?
+ @recordingWindow.close()
+
+ @popupRecordingControls()
+
+ onRecordingControlsClosed: () ->
+ logger.debug("recording controls closed")
+ @recordingWindow = null
+
+ popupRecordingControls: () ->
+ logger.debug("poupRecordingControls")
+ @recordingWindow = window.open("/popups/recording-controls", 'Recording', 'scrollbars=yes,toolbar=no,status=no,height=315,width=350')
+ @recordingWindow.ParentRecordingStore = context.RecordingStore
+ @recordingWindow.ParentIsRecording = @recordingModel.isRecording()
+
+ }
+)
diff --git a/web/app/assets/javascripts/react-components/stores/SessionMediaTracksStore.js.coffee b/web/app/assets/javascripts/react-components/stores/SessionMediaTracksStore.js.coffee
new file mode 100644
index 000000000..08e0544ce
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/stores/SessionMediaTracksStore.js.coffee
@@ -0,0 +1,21 @@
+$ = jQuery
+context = window
+logger = context.JK.logger
+
+@SessionMediaTracksStore = Reflux.createStore(
+ {
+
+ init: ->
+ # Register with the app store to get @app
+ this.listenTo(context.AppStore, this.onAppInit);
+ this.listenTo(context.MixerStore, this.onSessionMixerChange)
+
+ onAppInit: (@app) ->
+ @gearUtils = context.JK.GearUtilsInstance
+ @sessionUtils = context.JK.SessionUtils
+
+ onSessionMixerChange: (sessionMixers) ->
+
+ this.trigger(sessionMixers)
+ }
+)
diff --git a/web/app/assets/javascripts/react-components/stores/SessionMyTracksStore.js.coffee b/web/app/assets/javascripts/react-components/stores/SessionMyTracksStore.js.coffee
new file mode 100644
index 000000000..d50dce041
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/stores/SessionMyTracksStore.js.coffee
@@ -0,0 +1,21 @@
+$ = jQuery
+context = window
+logger = context.JK.logger
+
+@SessionMyTracksStore = Reflux.createStore(
+ {
+
+ init: ->
+ # Register with the app store to get @app
+ this.listenTo(context.AppStore, this.onAppInit);
+ this.listenTo(context.MixerStore, this.onSessionMixerChange)
+
+ onAppInit: (@app) ->
+ @gearUtils = context.JK.GearUtilsInstance
+ @sessionUtils = context.JK.SessionUtils
+
+ onSessionMixerChange: (sessionMixers) ->
+
+ this.trigger(sessionMixers)
+ }
+)
diff --git a/web/app/assets/javascripts/react-components/stores/SessionNotificationStore.js.coffee b/web/app/assets/javascripts/react-components/stores/SessionNotificationStore.js.coffee
new file mode 100644
index 000000000..54b42c1b7
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/stores/SessionNotificationStore.js.coffee
@@ -0,0 +1,61 @@
+$ = jQuery
+context = window
+logger = context.JK.logger
+rest = context.JK.Rest()
+
+@SessionNotificationStore = Reflux.createStore(
+ {
+ listenables: @NotificationActions
+
+ notifications: []
+ count: 0
+
+ issueChange: () ->
+ @trigger(@notifications)
+
+ onClear: () ->
+ @notifications = []
+ @issueChange()
+
+ onSessionEnded: () ->
+ @notifications = []
+ @issueChange()
+
+ processNotification: (notification) ->
+ notification.id = ++@count
+
+ title = 'n/a'
+ extra = null
+
+ if notification.backend_detail?
+ if notification.backend_detail == 'Network Issues'
+ title = 'Network Issues'
+ extra = notification.msg
+ else
+ title = notification.msg
+ extra = notification.backend_detail
+ else
+ title = notification.msg
+
+ detail = if notification.detail? && notification.detail != "" then notification.detail else null
+
+ data =
+ title: title
+ extra: extra
+ detail: detail
+ help: notification.help
+
+ @notifications.unshift(data)
+
+ if @notifications.length > 100
+ @notifications.pop();
+ @issueChange()
+
+ onBackendNotification: (notification) ->
+ @processNotification(notification)
+
+ onFrontendNotification: (notification) ->
+ @processNotification(notification)
+ }
+)
+
diff --git a/web/app/assets/javascripts/react-components/stores/SessionOtherTracksStore.js.coffee b/web/app/assets/javascripts/react-components/stores/SessionOtherTracksStore.js.coffee
new file mode 100644
index 000000000..703d65305
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/stores/SessionOtherTracksStore.js.coffee
@@ -0,0 +1,21 @@
+$ = jQuery
+context = window
+logger = context.JK.logger
+
+@SessionOtherTracksStore = Reflux.createStore(
+ {
+
+ init: ->
+ # Register with the app store to get @app
+ this.listenTo(context.AppStore, this.onAppInit);
+ this.listenTo(context.MixerStore, this.onSessionMixerChange)
+
+ onAppInit: (@app) ->
+ @gearUtils = context.JK.GearUtilsInstance
+ @sessionUtils = context.JK.SessionUtils
+
+ onSessionMixerChange: (sessionMixers) ->
+
+ this.trigger(sessionMixers)
+ }
+)
diff --git a/web/app/assets/javascripts/react-components/stores/SessionStore.js.coffee b/web/app/assets/javascripts/react-components/stores/SessionStore.js.coffee
new file mode 100644
index 000000000..635b4b055
--- /dev/null
+++ b/web/app/assets/javascripts/react-components/stores/SessionStore.js.coffee
@@ -0,0 +1,1053 @@
+$ = jQuery
+context = window
+logger = context.JK.logger
+rest = context.JK.Rest()
+EVENTS = context.JK.EVENTS
+MIX_MODES = context.JK.MIX_MODES
+
+
+SessionActions = @SessionActions
+RecordingActions = @RecordingActions
+NotificationActions = @NotificationActions
+
+@SessionStore = Reflux.createStore(
+ {
+ listenables: SessionActions
+
+ userTracks: null # comes from the backend
+ currentSessionId: null
+ currentSession: null
+ currentOrLastSession: null
+ startTime: null
+ currentParticipants: {}
+ participantsEverSeen: {}
+ users: {} # // User info for session participants
+ requestingSessionRefresh: false
+ pendingSessionRefresh: false
+ sessionPageEnterTimeout: null
+ sessionPageEnterDeferred: null
+ gearUtils: null
+ sessionUtils: null
+ joinDeferred: null
+ recordingModel: null
+ currentTrackChanges: 0
+ isRecording: false
+ previousAllTracks: {userTracks: [], backingTracks: [], metronomeTracks: []}
+ webcamViewer: null
+ openBackingTrack: null
+ helper: null
+ downloadingJamTrack: false
+
+ init: ->
+ # Register with the app store to get @app
+ this.listenTo(context.AppStore, this.onAppInit)
+ this.listenTo(context.RecordingStore, this.onRecordingChanged)
+
+ onAppInit: (@app) ->
+ @gearUtils = context.JK.GearUtilsInstance
+ @sessionUtils = context.JK.SessionUtils
+ @recordingModel = new context.JK.RecordingModel(@app, rest, context.jamClient);
+ RecordingActions.initModel(@recordingModel)
+ @helper = new context.SessionHelper(@app, @currentSession, @participantsEverSeen, @isRecording, @downloadingJamTrack)
+
+ if gon.global.video_available && gon.global.video_available!="none" && context.JK.WebcamViewer?
+ @webcamViewer = new context.JK.WebcamViewer()
+ @webcamViewer.init()
+ @webcamViewer.setVideoOff()
+
+
+ issueChange: () ->
+ @helper = new context.SessionHelper(@app, @currentSession, @participantsEverSeen, @isRecording, @downloadingJamTrack)
+ this.trigger(@helper)
+
+ onWindowBackgrounded: () ->
+ @app.user()
+ .done((userProfile) =>
+ if userProfile.show_whats_next &&
+ window.location.pathname.indexOf(gon.client_path) == 0 &&
+ !@app.layout.isDialogShowing('getting-started')
+ @app.layout.showDialog('getting-started')
+ )
+
+ return unless @inSession()
+
+ # the window was closed; just attempt to nav to home, which will cause all the right REST calls to happen
+ logger.debug("leaving session because window was closed")
+ SessionActions.leaveSession({location: '/client#/home'})
+
+ onBroadcastFailure: (text) ->
+ logger.debug("SESSION_LIVEBROADCAST_FAIL alert. reason:" + text);
+
+ if @currentSession? && @currentSession.mount?
+ rest.createSourceChange({
+ mount_id: @currentSession.mount.id,
+ source_direction: true,
+ success: false,
+ reason: text,
+ client_id: @app.clientId
+ })
+ else
+ logger.debug("unable to report source change because no mount seen on session")
+
+ onBroadcastSuccess: (text) ->
+ logger.debug("SESSION_LIVEBROADCAST_ACTIVE alert. reason:" + text);
+
+ if @currentSession? && @currentSession.mount?
+ rest.createSourceChange({
+ mount_id: @currentSession.mount.id,
+ source_direction: true,
+ success: true,
+ reason: text,
+ client_id: @app.clientId
+ })
+ else
+ logger.debug("unable to report source change because no mount seen on session")
+
+ onBroadcastStopped: (text) ->
+ logger.debug("SESSION_LIVEBROADCAST_STOPPED alert. reason:" + text);
+
+ if @currentSession? && @currentSession.mount?
+ rest.createSourceChange({
+ mount_id: @currentSession.mount.id,
+ source_direction: false,
+ success: true,
+ reason: text,
+ client_id: @app.clientId
+ })
+ else
+ logger.debug("unable to report source change because no mount seen on session")
+
+ onShowNativeMetronomeGui: () ->
+ context.jamClient.SessionShowMetronomeGui()
+
+ onOpenMetronome: () ->
+ unstable = @unstableNTPClocks()
+ if @participants().length > 1 && unstable.length > 0
+ names = unstable.join(", ")
+ logger.debug("Unstable clocks: ", names, unstable)
+ context.JK.Banner.showAlert("Couldn't open metronome", context._.template($('#template-help-metronome-unstable').html(), {names: names}, { variable: 'data' }));
+ else
+ data =
+ value: 1
+ session_size: @participants().length
+ user_id: context.JK.currentUserId
+ user_name: context.JK.currentUserName
+
+ context.stats.write('web.metronome.open', data)
+ rest.openMetronome({id: @currentSessionId})
+ .done((response) =>
+ MixerActions.openMetronome()
+ @updateSessionInfo(response, true)
+ )
+ .fail((jqXHR) =>
+ @app.notify({
+ "title": "Couldn't open metronome",
+ "text": "Couldn't inform the server to open metronome. msg=" + jqXHR.responseText,
+ "icon_url": "/assets/content/icon_alert_big.png"
+ })
+ )
+
+ onMetronomeCricketChange: (isCricket) ->
+ context.jamClient.setMetronomeCricketTestState(isCricket);
+
+ unstableNTPClocks: () ->
+ unstable = []
+ # This should be handled in the below loop, actually:
+ myState = context.jamClient.getMyNetworkState()
+ map = null
+ for participant in @participants()
+ isSelf = participant.client_id == @app.clientId
+
+ if isSelf
+ isStable = myState.ntp_stable
+ else
+ map = context.jamClient.getPeerState(participant.client_id)
+ isStable = map.ntp_stable
+
+ if !isStable
+ name = participant.user.name
+
+ if isSelf
+ name += " (this computer)"
+
+ unstable.push(name)
+ unstable
+
+
+
+ onDownloadingJamTrack: (downloading) ->
+ @downloadingJamTrack = downloading
+
+ @issueChange()
+
+ onToggleSessionVideo: () ->
+ logger.debug("toggle session video")
+ @webcamViewer.toggleWebcam() if @webcamViewer?
+
+ onAudioResync: () ->
+ logger.debug("audio resyncing")
+ response = context.jamClient.SessionAudioResync()
+ if response?
+ @app.notify({
+ "title": "Error",
+ "text": response,
+ "icon_url": "/assets/content/icon_alert_big.png"})
+
+ onSyncWithServer: () ->
+ @refreshCurrentSession(true)
+
+ onWatchedInputs: (inputTracks) ->
+
+ logger.debug("obtained tracks at start of session")
+ @sessionPageEnterDeferred.resolve(inputTracks);
+ @sessionPageEnterDeferred = null
+
+
+
+ onCloseMedia: () ->
+
+ logger.debug("SessionStore: onCloseMedia")
+ if @helper.recordedTracks()
+ @closeRecording()
+ else if @helper.jamTracks() || @downloadingJamTrack
+ @closeJamTrack()
+ else if @helper.backingTrack() && @helper.backingTrack().path
+ @closeBackingTrack()
+ else if @helper.isMetronomeOpen()
+ @closeMetronomeTrack()
+ else
+ logger.error("don't know how to close open media");
+
+ closeJamTrack: () ->
+ logger.debug("closing jam track");
+
+ if @isRecording
+ logger.debug("can't close jamtrack while recording")
+ @app.notify({title: 'Can Not Close JamTrack', text: 'A JamTrack can not be closed while recording.'})
+ return
+
+ unless @selfOpenedJamTracks()
+ logger.debug("can't close jamtrack if not the opener")
+ @app.notify({title: 'Can Not Close JamTrack', text: 'Only the person who opened the JamTrack can close it.'})
+ return
+
+ rest.closeJamTrack({id: @currentSessionId})
+ .done(() =>
+ @refreshCurrentSession(true)
+ )
+ .fail((jqXHR) =>
+ @app.notify({
+ "title": "Couldn't Close JamTrack",
+ "text": "Couldn't inform the server to close JamTrack. msg=" + jqXHR.responseText,
+ "icon_url": "/assets/content/icon_alert_big.png"
+ })
+ )
+
+ context.jamClient.JamTrackStopPlay()
+
+
+ onOpenBackingTrack: (result) ->
+ unless @inSession()
+ logger.debug("ignoring backing track selected callback (not in session)")
+ return
+
+ if result.success
+ logger.debug("backing track selected: " + result.file);
+
+ rest.openBackingTrack({id: @currentSessionId, backing_track_path: result.file})
+ .done(() =>
+
+ openResult = context.jamClient.SessionOpenBackingTrackFile(result.file, false);
+
+ if openResult
+ # storing session state in memory, not in response of Session server response. bad.
+ @openBackingTrack = result.file
+ else
+ @app.notify({
+ "title": "Couldn't Open Backing Track",
+ "text": "Is the file a valid audio file?",
+ "icon_url": "/assets/content/icon_alert_big.png"
+ });
+ @closeBackingTrack()
+ )
+ .fail((jqXHR) =>
+ @app.notifyServerError(jqXHR, "Unable to Open Backing Track For Playback");
+ )
+
+ closeRecording: () ->
+ logger.debug("closing recording");
+
+ rest.stopPlayClaimedRecording({id: @currentSessionId, claimed_recording_id: @currentSession.claimed_recording.id})
+ .done((response) =>
+ #sessionModel.refreshCurrentSession(true);
+ # update session info
+ @onUpdateSession(response)
+ )
+ .fail((jqXHR) =>
+ @app.notify({
+ "title": "Couldn't Stop Recording Playback",
+ "text": "Couldn't inform the server to stop playback. msg=" + jqXHR.responseText,
+ "icon_url": "/assets/content/icon_alert_big.png"
+ })
+ )
+
+ context.jamClient.CloseRecording()
+
+ closeMetronomeTrack:() ->
+ logger.debug("SessionStore: closeMetronomeTrack")
+ rest.closeMetronome({id: @currentSessionId})
+ .done(() =>
+ context.jamClient.SessionCloseMetronome()
+ @refreshCurrentSession(true)
+ )
+ .fail((jqXHR) =>
+ @app.notify({
+ "title": "Couldn't Close MetronomeTrack",
+ "text": "Couldn't inform the server to close MetronomeTrack. msg=" + jqXHR.responseText,
+ "icon_url": "/assets/content/icon_alert_big.png"
+ })
+ )
+
+ closeBackingTrack: () ->
+ if @isRecording
+ logger.debug("can't close backing track while recording")
+ return
+
+ rest.closeBackingTrack({id: @currentSessionId})
+ .done(() =>
+ )
+ .fail(() =>
+ @app.notify({
+ "title": "Couldn't Close Backing Track",
+ "text": "Couldn't inform the server to close Backing Track. msg=" + jqXHR.responseText,
+ "icon_url": "/assets/content/icon_alert_big.png"
+ });
+ )
+
+ # '' closes all open backing tracks
+ context.jamClient.SessionStopPlay();
+ context.jamClient.SessionCloseBackingTrackFile('');
+
+
+ onMixersChanged: (type, text, trackInfo) ->
+
+ return unless @inSession()
+
+ if text == 'RebuildAudioIoControl'
+
+ if @backendMixerAlertThrottleTimer
+ clearTimeout(@backendMixerAlertThrottleTimer)
+
+ @backendMixerAlertThrottleTimer =
+ setTimeout(() =>
+ @backendMixerAlertThrottleTimer = null
+ if @sessionPageEnterDeferred
+ # this means we are still waiting for the BACKEND_MIXER_CHANGE that indicates we have user tracks built-out/ready
+
+ # we will get at least one BACKEND_MIXER_CHANGE that corresponds to the backend doing a 'audio pause', which won't matter much
+ # so we need to check that we actaully have userTracks before considering ourselves done
+ if trackInfo.userTracks.length > 0
+ logger.debug("obtained tracks at start of session")
+ @sessionPageEnterDeferred.resolve(trackInfo.userTracks)
+ @sessionPageEnterDeferred = null
+
+ return
+
+ # wait until we are fully in session before trying to sync tracks to server
+ if @joinDeferred
+ @joinDeferred
+ .done(()=>
+ MixerActions.syncTracks()
+ )
+ , 100)
+ else if text == 'RebuildMediaControl' || text == 'RebuildRemoteUserControl'
+
+ backingTracks = trackInfo.backingTracks
+ previousBackingTracks = @previousAllTracks.backingTracks
+ metronomeTracks = trackInfo.metronomeTracks
+ previousMetronomeTracks = @previousAllTracks.metronomeTracks
+
+ # the way we know if backing tracks changes, or recordings are opened, is via this event.
+ # but we want to report to the user when backing tracks change; so we need to detect change on our own
+ if !(previousBackingTracks.length == 0 && backingTracks.length == 0) && previousBackingTracks != backingTracks
+ logger.debug("backing tracks changed", previousBackingTracks, backingTracks)
+ MixerActions.syncTracks()
+ else if !(previousMetronomeTracks.length == 0 && metronomeTracks.length == 0) && previousMetronomeTracks != metronomeTracks
+ logger.debug("metronome state changed ", previousMetronomeTracks, metronomeTracks)
+ MixerActions.syncTracks()
+ else
+ @refreshCurrentSession(true)
+
+ @previousAllTracks = trackInfo
+
+ else if text == 'Global Peer Input Mixer Mode'
+ MixerActions.mixerModeChanged(MIX_MODES.MASTER)
+
+ else if text == 'Local Peer Stream Mixer Mode'
+ MixerActions.mixerModeChanged(MIX_MODES.PERSONAL)
+
+ onRecordingChanged: (details) ->
+ logger.debug("SessionStore.onRecordingChanged: " + details.cause)
+ @isRecording = details.isRecording
+
+ switch details.cause
+ when 'started'
+
+ if details.reason
+ reason = details.reason;
+ detail = details.detail;
+ title = "Could Not Start Recording";
+
+ switch reason
+ when 'client-no-response'
+ @notifyWithUserInfo(title, 'did not respond to the start signal.', detail)
+ when 'empty-recording-id'
+ @app.notifyAlert(title, "No recording ID specified.")
+ when 'missing-client'
+ @notifyWithUserInfo(title, 'could not be signalled to start recording.', detail)
+ when 'already-recording'
+ @app.notifyAlert(title, 'Already recording. If this appears incorrect, try restarting JamKazam.')
+ when 'recording-engine-unspecified'
+ @notifyWithUserInfo(title, 'had a problem writing recording data to disk.', detail)
+ when 'recording-engine-create-directory'
+ @notifyWithUserInfo(title, 'had a problem creating a recording folder.', detail)
+ when 'recording-engine-create-file'
+ @notifyWithUserInfo(title, 'had a problem creating a recording file.', detail)
+ when 'recording-engine-sample-rate'
+ @notifyWithUserInfo(title, 'had a problem recording at the specified sample rate.', detail)
+ when 'rest'
+ jqXHR = detail[0];
+ @app.notifyServerError(jqXHR);
+ else
+ @notifyWithUserInfo(title, 'Error Reason: ' + reason)
+ else
+ @displayWhoCreatedRecording(details.clientId)
+
+ when 'stopped'
+ if @selfOpenedJamTracks()
+ timeline = context.jamClient.GetJamTrackTimeline();
+
+ rest.addRecordingTimeline(details.recordingId, timeline)
+ .fail(()=>
+ @app.notify({
+ title: "Unable to Add JamTrack Volume Data",
+ text: "The volume of the JamTrack will not be correct in the recorded mix."
+ }, null, true)
+ )
+
+ if details.reason
+ logger.warn("Recording Discarded: ", details)
+ reason = details.reason
+ detail = details.detail
+ title = "Recording Discarded"
+
+ switch reason
+ when 'client-no-response'
+ @notifyWithUserInfo(title, 'did not respond to the stop signal.', detail)
+ when 'missing-client'
+ @notifyWithUserInfo(title, 'could not be signalled to stop recording.', detail)
+ when 'empty-recording-id'
+ @app.notifyAlert(title, "No recording ID specified.")
+ when 'wrong-recording-id'
+ @app.notifyAlert(title, "Wrong recording ID specified.")
+ when 'not-recording'
+ @app.notifyAlert(title, "Not currently recording.")
+ when 'already-stopping'
+ @app.notifyAlert(title, "Already stopping the current recording.")
+ when 'start-before-stop'
+ @notifyWithUserInfo(title, 'asked that we start a new recording; cancelling the current one.', detail)
+ else
+ @app.notifyAlert(title, "Error reason: " + reason)
+ else
+ @promptUserToSave(details.recordingId, timeline);
+
+ when 'abortedRecording'
+ reason = details.reason
+ detail = details.detail
+
+ title = "Recording Cancelled"
+
+ switch reason
+ when 'client-no-response'
+ @notifyWithUserInfo(title, 'did not respond to the start signal.', detail)
+ when 'missing-client'
+ @notifyWithUserInfo(title, 'could not be signalled to start recording.', detail)
+ when 'populate-recording-info'
+ @notifyWithUserInfo(title, 'could not synchronize with the server.', detail)
+ when 'recording-engine-unspecified'
+ @notifyWithUserInfo(title, 'had a problem writing recording data to disk.', detail)
+ when 'recording-engine-create-directory'
+ @notifyWithUserInfo(title, 'had a problem creating a recording folder.', detail)
+ when 'recording-engine-create-file'
+ @notifyWithUserInfo(title, 'had a problem creating a recording file.', detail)
+ when 'recording-engine-sample-rate'
+ @notifyWithUserInfo(title, 'had a problem recording at the specified sample rate.', detail)
+ else
+ @app.notifyAlert(title, "Error reason: " + reason)
+
+ @issueChange()
+
+ notifyWithUserInfo: (title , text, clientId) ->
+ @findUserBy({clientId: clientId})
+ .done((user)=>
+ @app.notify({
+ "title": title,
+ "text": user.name + " " + text,
+ "icon_url": context.JK.resolveAvatarUrl(user.photo_url)
+ });
+ )
+ .fail(()=>
+ @app.notify({
+ "title": title,
+ "text": 'Someone ' + text,
+ "icon_url": "/assets/content/icon_alert_big.png"
+ })
+ )
+
+ findUserBy: (finder) ->
+ if finder.clientId
+ foundParticipant = null
+ for participant in @participants()
+ if participant.client_id == finder.clientId
+ foundParticipant = participant
+ break
+
+ if foundParticipant
+ return $.Deferred().resolve(foundParticipant.user).promise();
+
+ # TODO: find it via some REST API if not found?
+ return $.Deferred().reject().promise();
+
+ displayWhoCreatedRecording: (clientId) ->
+ if @app.clientId != clientId # don't show to creator
+ @findUserBy({clientId: clientId})
+ .done((user) =>
+ @app.notify({
+ "title": "Recording Started",
+ "text": user.name + " started a recording",
+ "icon_url": context.JK.resolveAvatarUrl(user.photo_url)
+ })
+ )
+ .fail(() =>
+ @app.notify({
+ "title": "Recording Started",
+ "text": "Oops! Can't determine who started this recording",
+ "icon_url": "/assets/content/icon_alert_big.png"
+ })
+ )
+
+ promptUserToSave: (recordingId, timeline) ->
+ rest.getRecording( {id: recordingId} )
+ .done((recording) =>
+ if timeline
+ recording.timeline = timeline.global
+
+ context.JK.recordingFinishedDialog.setRecording(recording)
+ @app.layout.showDialog('recordingFinished').one(EVENTS.DIALOG_CLOSED, (e, data) =>
+ if data.result && data.result.keep
+ context.JK.prodBubble($('#recording-manager-viewer'), 'file-manager-poke', {}, {positions:['top', 'left', 'right', 'bottom'], offsetParent: $('#session-screen').parent()})
+ )
+ )
+ .fail(@app.ajaxError)
+
+ onJoinSession: (sessionId) ->
+
+ # poke ShareDialog
+ shareDialog = new JK.ShareDialog(@app, sessionId, "session");
+ shareDialog.initialize(context.JK.FacebookHelperInstance);
+
+ # initialize webcamViewer
+ if gon.global.video_available && gon.global.video_available != "none"
+ @webcamViewer.beforeShow()
+
+ # double-check that we are connected to the server via websocket
+
+ return unless @ensureConnected()
+
+ # just make double sure a previous session state is cleared out
+ @sessionEnded()
+
+ # update the session data to be empty
+ @updateCurrentSession(null)
+
+ # start setting data for this new session
+ @currentSessionId = sessionId
+ @startTime = new Date().getTime()
+
+ # let's find out the public/private nature of this session,
+ # so that we can decide whether we need to validate the audio profile more aggressively
+ rest.getSessionHistory(@currentSessionId)
+ .done((musicSession)=>
+ musicianAccessOnJoin = musicSession.musician_access
+
+ shouldVerifyNetwork = musicSession.musician_access;
+
+ @gearUtils.guardAgainstInvalidConfiguration(@app, shouldVerifyNetwork).fail(() =>
+ SessionActions.leaveSession.trigger({location: '/client#/home'})
+ ).done(() =>
+ result = @sessionUtils.SessionPageEnter();
+
+ @gearUtils.guardAgainstActiveProfileMissing(@app, result)
+ .fail((data) =>
+ leaveBehavior = {}
+
+ if data && data.reason == 'handled'
+ if data.nav == 'BACK'
+ leaveBehavior.location = -1
+ else
+ leaveBehavior.location = data.nav
+ else
+ leaveBehavior.location = '/client#/home';
+
+ SessionActions.leaveSession.trigger(leaveBehavior)
+ ).done(() =>
+ @waitForSessionPageEnterDone()
+ .done((userTracks) =>
+ @userTracks = userTracks
+
+ @ensureAppropriateProfile(musicianAccessOnJoin)
+ .done(() =>
+ logger.debug("user has passed all session guards")
+ @joinSession()
+ )
+ .fail((result) =>
+ unless result.controlled_location
+ SessionActions.leaveSession.trigger({location: "/client#/home"})
+ )
+ ).fail((data) =>
+ if data == "timeout"
+ context.JK.alertSupportedNeeded('The audio system has not reported your configured tracks in a timely fashion.')
+ else if data == 'session_over'
+ # do nothing; session ended before we got the user track info. just bail
+ logger.debug("session is over; bailing")
+ else
+ context.JK.alertSupportedNeeded('Unable to determine configured tracks due to reason: ' + data)
+
+ SessionActions.leaveSession.trigger({location: '/client#/home'})
+ )
+ )
+ )
+ )
+ .fail(() =>
+ logger.error("unable to fetch session history")
+ )
+
+ waitForSessionPageEnterDone: () ->
+ @sessionPageEnterDeferred = $.Deferred()
+
+ # see if we already have tracks; if so, we need to run with these
+ inputTracks = context.JK.TrackHelpers.getUserTracks(context.jamClient)
+
+ logger.debug("isNoInputProfile", @gearUtils.isNoInputProfile())
+ if inputTracks.length > 0 || @gearUtils.isNoInputProfile()
+ logger.debug("on page enter, tracks are already available")
+ @sessionPageEnterDeferred.resolve(inputTracks)
+ deferred = @sessionPageEnterDeferred
+ @sessionPageEnterDeferred = null
+ return deferred
+
+ @sessionPageEnterTimeout = setTimeout(()=>
+ if @sessionPageEnterTimeout
+ if @sessionPageEnterDeferred
+ @sessionPageEnterDeferred.reject('timeout')
+ @sessionPageEnterDeferred = null
+ @sessionPageEnterTimeout = null
+ , 5000)
+
+ @sessionPageEnterDeferred
+
+ ensureAppropriateProfile: (musicianAccess) ->
+ deferred = new $.Deferred();
+ if musicianAccess
+ deferred = context.JK.guardAgainstSinglePlayerProfile(@app)
+ else
+ deferred.resolve();
+ deferred
+
+ joinSession: () ->
+ context.jamClient.SessionRegisterCallback("JK.HandleBridgeCallback2");
+ context.jamClient.RegisterRecordingCallbacks("JK.HandleRecordingStartResult", "JK.HandleRecordingStopResult", "JK.HandleRecordingStarted", "JK.HandleRecordingStopped", "JK.HandleRecordingAborted");
+ context.jamClient.SessionSetConnectionStatusRefreshRate(1000);
+ #context.JK.HelpBubbleHelper.jamtrackGuideSession($screen.find('li.open-a-jamtrack'), $screen)
+
+ # subscribe to events from the recording model
+ @recordingRegistration()
+
+ # tell the server we want to join
+
+ @joinDeferred = rest.joinSession({
+ client_id: @app.clientId,
+ ip_address: context.JK.JamServer.publicIP,
+ as_musician: true,
+ tracks: @userTracks,
+ session_id: @currentSessionId,
+ audio_latency: context.jamClient.FTUEGetExpectedLatency().latency
+ })
+ .done((response) =>
+
+ unless @inSession()
+ # the user has left the session before they got joined. We need to issue a leave again to the server to make sure they are out
+ logger.debug("user left before fully joined to session. telling server again that they have left")
+ @leaveSessionRest(@currentSessionId)
+ return
+
+ logger.debug("calling jamClient.JoinSession");
+ # on temporary disconnect scenarios, a user may already be in a session when they enter this path
+ # so we avoid double counting
+ unless @alreadyInSession()
+ if response.music_session.participant_count == 1
+ context.JK.GA.trackSessionMusicians(context.JK.GA.SessionCreationTypes.create);
+ else
+ context.JK.GA.trackSessionMusicians(context.JK.GA.SessionCreationTypes.join);
+
+ @recordingModel.reset(@currentSessionId);
+
+ context.jamClient.JoinSession({sessionID: @currentSessionId});
+
+ @refreshCurrentSession(true);
+
+ context.JK.JamServer.registerMessageCallback(context.JK.MessageType.SESSION_JOIN, @trackChanges);
+ context.JK.JamServer.registerMessageCallback(context.JK.MessageType.SESSION_DEPART, @trackChanges);
+ context.JK.JamServer.registerMessageCallback(context.JK.MessageType.TRACKS_CHANGED, @trackChanges);
+ context.JK.JamServer.registerMessageCallback(context.JK.MessageType.HEARTBEAT_ACK, @trackChanges);
+
+ $(document).trigger(EVENTS.SESSION_STARTED, {session: {id: @currentSessionId}}) if document
+
+ @handleAutoOpenJamTrack()
+ )
+ .fail((xhr) =>
+ @updateCurrentSession(null)
+
+ if xhr.status == 404
+ # we tried to join the session, but it's already gone. kick user back to join session screen
+ leaveBehavior =
+ location: "/client#/findSession"
+ notify:
+ title: "Unable to Join Session",
+ text: " The session you attempted to join is over."
+ SessionActions.leaveSession.trigger(leaveBehavior)
+ else if xhr.status == 422
+ response = JSON.parse(xhr.responseText);
+ if response["errors"] && response["errors"]["tracks"] && response["errors"]["tracks"][0] == "Please select at least one track"
+ @app.notifyAlert("No Inputs Configured", $('
You will need to reconfigure your audio device. '))
+
+ else if response["errors"] && response["errors"]["music_session"] && response["errors"]["music_session"][0] == ["is currently recording"]
+
+ leaveBehavior =
+ location: "/client#/findSession"
+ notify:
+ title: "Unable to Join Session"
+ text: "The session is currently recording."
+ SessionActions.leaveSession.trigger(leaveBehavior)
+ else
+ @app.notifyServerError(xhr, 'Unable to Join Session');
+ else
+ @app.notifyServerError(xhr, 'Unable to Join Session');
+ )
+
+ trackChanges: (header, payload) ->
+ if @currentTrackChanges < payload.track_changes_counter
+ # we don't have the latest info. try and go get it
+ logger.debug("track_changes_counter = stale. refreshing...")
+ @refreshCurrentSession();
+
+ else
+ if header.type != 'HEARTBEAT_ACK'
+ # don't log if HEARTBEAT_ACK, or you will see this log all the time
+ logger.info("track_changes_counter = fresh. skipping refresh...", header, payload)
+
+ handleAutoOpenJamTrack: () ->
+ jamTrack = @sessionUtils.grabAutoOpenJamTrack();
+ if jamTrack
+ # give the session to settle just a little (call a timeout of 1 second)
+ setTimeout(()=>
+ # tell the server we are about to open a jamtrack
+ rest.openJamTrack({id: @currentSessionId, jam_track_id: jamTrack.id})
+ .done((response) =>
+ logger.debug("jamtrack opened")
+ # now actually load the jamtrack
+ # TODO
+ # context.JK.CurrentSessionModel.updateSession(response);
+ # loadJamTrack(jamTrack);
+ )
+ .fail((jqXHR) =>
+ @app.notifyServerError(jqXHR, "Unable to Open JamTrack For Playback")
+ )
+ , 1000)
+
+ inSession: () ->
+ !!@currentSessionId
+
+ alreadyInSession: () ->
+ inSession = false
+ for participant in @participants()
+ if participant.user.id == context.JK.currentUserId
+ inSession = true
+ break
+
+ participants: () ->
+ if @currentSession
+ @currentSession.participants;
+ else
+ []
+
+ refreshCurrentSession: (force) ->
+ logger.debug("refreshCurrentSession(force=true)") if force
+
+ @refreshCurrentSessionRest(force)
+
+ refreshCurrentSessionRest: (force) ->
+ unless @inSession()
+ logger.debug("refreshCurrentSession skipped: ")
+ return
+
+ if @requestingSessionRefresh
+ # if someone asks for a refresh while one is going on, we ask for another to queue up
+ logger.debug("queueing refresh")
+ @pendingSessionRefresh = true;
+ else
+ @requestingSessionRefresh = true
+ rest.getSession(@currentSessionId)
+ .done((response) =>
+ @updateSessionInfo(response, force)
+ )
+ .fail((jqXHR) =>
+ if jqXHR.status != 404
+ @app.notifyServerError(jqXHR, "Unable to refresh session data")
+ else
+ logger.debug("refreshCurrentSessionRest: could not refresh data for session because it's gone")
+ )
+ .always(() =>
+ @requestingSessionRefresh = false
+ if @pendingSessionRefresh
+ # and when the request is done, if we have a pending, fire it off again
+ @pendingSessionRefresh = false
+ @refreshCurrentSessionRest(force)
+ )
+
+ onUpdateSession: (session) ->
+ @updateSessionInfo(session, true)
+
+ updateSessionInfo: (session, force) ->
+ if force == true || @currentTrackChanges < session.track_changes_counter
+ logger.debug("updating current track changes from %o to %o", @currentTrackChanges, session.track_changes_counter)
+ @currentTrackChanges = session.track_changes_counter;
+ @sendClientParticipantChanges(@currentSession, session);
+ @updateCurrentSession(session);
+ #if(callback != null) {
+ # callback();
+ #}
+ else
+ logger.info("ignoring refresh because we already have current: " + @currentTrackChanges + ", seen: " + session.track_changes_counter);
+
+
+ leaveSessionRest: () ->
+ rest.deleteParticipant(@app.clientId);
+
+ sendClientParticipantChanges: (oldSession, newSession) ->
+ joins = []
+ leaves = []
+ leaveJoins = []; # Will hold JamClientParticipants
+
+ oldParticipants = []; # will be set to session.participants if session
+ oldParticipantIds = {};
+ newParticipants = [];
+ newParticipantIds = {};
+
+ if oldSession && oldSession.participants
+ for oldParticipant in oldSession.participants
+ oldParticipantIds[oldParticipant.client_id] = oldParticipant
+
+ if newSession && newSession.participants
+ for newParticipant in newSession.participants
+ newParticipantIds[newParticipant.client_id] = newParticipant
+
+ for client_id, participant of newParticipantIds
+ # grow the 'all participants seen' list
+ unless (client_id of @participantsEverSeen)
+ @participantsEverSeen[client_id] = participant;
+
+
+ if client_id of oldParticipantIds
+ # if the participant is here now, and here before, there is still a chance we missed a
+ # very fast leave/join. So check if joined_session_at is different
+ if oldParticipantIds[client_id].joined_session_at != participant.joined_session_at
+ leaveJoins.push(participant)
+ else
+ # new participant id that's not in old participant ids: Join
+ joins.push(participant);
+
+ for client_id, participant of oldParticipantIds
+ unless (client_id of newParticipantIds)
+ # old participant id that's not in new participant ids: Leave
+ leaves.push(participant);
+
+ for i, v of joins
+ if v.client_id != @app.clientId
+ @participantJoined(newSession, v)
+
+ for i,v of leaves
+ if v.client_id != @app.clientId
+ @participantLeft(newSession, v)
+
+ for i,v of leaveJoins
+ if v.client_id != @app.clientId
+ logger.debug("participant had a rapid leave/join")
+ @participantLeft(newSession, v)
+ @participantJoined(newSession, v)
+
+ participantJoined: (newSession, participant) ->
+ logger.debug("jamClient.ParticipantJoined", participant.client_id)
+ context.jamClient.ParticipantJoined(newSession, @toJamClientParticipant(participant));
+ @currentParticipants[participant.client_id] = {server: participant, client: {audio_established: null}}
+
+ participantLeft: (newSession, participant) ->
+ logger.debug("jamClient.ParticipantLeft", participant.client_id)
+ context.jamClient.ParticipantLeft(newSession, @toJamClientParticipant(participant));
+ delete @currentParticipants[participant.client_id]
+
+ toJamClientParticipant: (participant) ->
+ {
+ userID: "",
+ clientID: participant.client_id,
+ tcpPort: 0,
+ udpPort: 0,
+ localIPAddress: participant.ip_address, # ?
+ globalIPAddress: participant.ip_address, # ?
+ latency: 0,
+ natType: ""
+ }
+
+ recordingRegistration: () ->
+ logger.debug("recording registration not hooked up yet")
+
+ updateCurrentSession: (sessionData) ->
+ if sessionData != null
+ @currentOrLastSession = sessionData
+
+ @currentSession = sessionData
+
+ #logger.debug("session changed")
+
+ @issueChange()
+
+ ensureConnected: () ->
+ unless context.JK.JamServer.connected
+ leaveBehavior =
+ location: '/client#/home'
+ notify:
+ title: "Not Connected"
+ text: 'To create or join a session, you must be connected to the server.'
+
+ SessionActions.leaveSession.trigger(leaveBehavior)
+
+ context.JK.JamServer.connected
+
+ # called by anyone wanting to leave the session with a certain behavior
+ onLeaveSession: (behavior) ->
+ logger.debug("attempting to leave session", behavior)
+ if behavior.notify
+ @app.layout.notify(behavior.notify)
+
+ SessionActions.allowLeaveSession.trigger()
+
+ if behavior.location
+ if jQuery.isNumeric(behavior.location)
+ window.history.go(behavior.location)
+ else
+ window.location = behavior.location
+ else
+ logger.warn("no location specified in leaveSession action", behavior)
+ window.location = '/client#/home'
+
+ if gon.global.video_available && gon.global.video_available != "none"
+ @webcamViewer.setVideoOff()
+
+ @leaveSession()
+
+ @sessionUtils.SessionPageLeave()
+
+ leaveSession: () ->
+
+ if @joinDeferred?.state() == 'resolved'
+ deferred = new $.Deferred()
+
+ @recordingModel.stopRecordingIfNeeded()
+ .always(()=>
+ @performLeaveSession(deferred)
+ )
+
+ performLeaveSession: (deferred) ->
+
+ logger.debug("SessionModel.leaveCurrentSession()")
+ # TODO - sessionChanged will be called with currentSession = null\
+
+ # leave the session right away without waiting on REST. Why? If you can't contact the server, or if it takes a long
+ # time, for that entire duration you'll still be sending voice data to the other users.
+ # this may be bad if someone decides to badmouth others in the left-session during this time
+ logger.debug("performLeaveSession: calling jamClient.LeaveSession for clientId=" + @app.clientId)
+ context.jamClient.LeaveSession({ sessionID: @currentSessionId })
+ @leaveSessionRest(@currentSessionId)
+ .done(=>
+ deferred.resolve(arguments[0], arguments[1], arguments[2]))
+ .fail(=>
+ deferred.reject(arguments[0], arguments[1], arguments[2]);
+ )
+
+ # 'unregister' for callbacks
+ context.jamClient.SessionRegisterCallback("");
+ #context.jamClient.SessionSetAlertCallback("");
+ context.jamClient.SessionSetConnectionStatusRefreshRate(0);
+
+ @sessionEnded()
+
+ @issueChange()
+
+ selfOpenedJamTracks: () ->
+ @currentSession && (@currentSession.jam_track_initiator_id == context.JK.currentUserId)
+
+ sessionEnded: () ->
+ # cleanup
+
+ context.JK.JamServer.unregisterMessageCallback(context.JK.MessageType.SESSION_JOIN, @trackChanges);
+ context.JK.JamServer.unregisterMessageCallback(context.JK.MessageType.SESSION_DEPART, @trackChanges);
+ context.JK.JamServer.unregisterMessageCallback(context.JK.MessageType.TRACKS_CHANGED, @trackChanges);
+ context.JK.JamServer.unregisterMessageCallback(context.JK.MessageType.HEARTBEAT_ACK, @trackChanges);
+
+ if @sessionPageEnterDeferred?
+ @sessionPageEnterDeferred.reject('session_over')
+ @sessionPageEnterDeferred = null
+
+ if @backendMixerAlertThrottleTimer
+ clearTimeout(@backendMixerAlertThrottleTimer)
+ @backendMixerAlertThrottleTimer = null
+
+ @userTracks = null;
+ @startTime = null;
+
+ if @joinDeferred?.state() == 'resolved'
+ $(document).trigger(EVENTS.SESSION_ENDED, {session: {id: @currentSessionId}})
+
+ @currentTrackChanges = 0
+ @currentSession = null
+ @joinDeferred = null
+ @isRecording = false
+ @currentSessionId = null
+ @currentParticipants = {}
+ @previousAllTracks = {userTracks: [], backingTracks: [], metronomeTracks: []}
+ @openBackingTrack = null
+ @shownAudioMediaMixerHelp = false
+ @controlsLockedForJamTrackRecording = false
+ @openBackingTrack = null
+ @downloadingJamTrack = false
+
+ NotificationActions.sessionEnded()
+
+ id: () ->
+ @currentSessionId
+
+ getCurrentOrLastSession: () ->
+ @currentOrLastSession
+
+ }
+)
\ No newline at end of file
diff --git a/web/app/assets/javascripts/recordingModel.js b/web/app/assets/javascripts/recordingModel.js
index 4f0b84e00..0aec59c6a 100644
--- a/web/app/assets/javascripts/recordingModel.js
+++ b/web/app/assets/javascripts/recordingModel.js
@@ -18,7 +18,7 @@
context.JK = context.JK || {};
var logger = context.JK.logger;
- context.JK.RecordingModel = function(app, sessionModel, _rest, _jamClient) {
+ context.JK.RecordingModel = function(app, _rest, _jamClient) {
var currentRecording = null; // the JSON response from the server for a recording
var currentOrLastRecordingId = null;
var currentRecordingId = null;
@@ -31,7 +31,7 @@
var waitingOnStopTimer = null;
var jamClient = _jamClient;
- var sessionModel = sessionModel;
+ var sessionId = null;
var $self = $(this);
function isRecording (recordingId) {
@@ -46,7 +46,7 @@
}
/** called every time a session is joined, to ensure clean state */
- function reset() {
+ function reset(_sessionId) {
currentlyRecording = false;
waitingOnServerStop = false;
waitingOnClientStop = false;
@@ -57,9 +57,11 @@
currentRecording = null;
currentRecordingId = null;
stoppingRecording = false;
+ sessionId = _sessionId
}
+
function groupTracksToClient(recording) {
// group N tracks to the same client Id
var groupedTracks = {};
@@ -84,7 +86,9 @@
currentlyRecording = true;
stoppingRecording = false;
- currentRecording = rest.startRecording({"music_session_id": sessionModel.id()})
+ context.RecordingActions.startingRecording({isRecording: false})
+
+ currentRecording = rest.startRecording({"music_session_id": sessionId})
.done(function(recording) {
currentRecordingId = recording.id;
currentOrLastRecordingId = recording.id;
@@ -94,8 +98,10 @@
jamClient.StartRecording(recording["id"], groupedTracks);
})
.fail(function(jqXHR) {
- $self.triggerHandler('startedRecording', { clientId: app.clientId, reason: 'rest', detail: arguments });
+ var details = { clientId: app.clientId, reason: 'rest', detail: arguments, isRecording: false }
+ $self.triggerHandler('startedRecording', details);
currentlyRecording = false;
+ context.RecordingActions.startedRecording(details);
})
@@ -116,6 +122,7 @@
waitingOnStopTimer = setTimeout(timeoutTransitionToStop, 5000);
$self.triggerHandler('stoppingRecording', {reason: reason, detail: detail});
+ context.RecordingActions.stoppingRecording({reason: reason, detail: detail, isRecording:true})
// this path assumes that the currentRecording info has, or can be, retrieved
// failure for currentRecording is handled elsewhere
@@ -145,7 +152,9 @@
else {
logger.error("unable to stop recording %o", arguments);
transitionToStopped();
- $self.triggerHandler('stoppedRecording', {'recordingId': recording.id, 'reason' : 'rest', 'details' : arguments});
+ var details = {'recordingId': recording.id, 'reason' : 'rest', 'details' : arguments, isRecording: false}
+ $self.triggerHandler('stoppedRecording', details);
+ context.RecordingActions.stoppedRecording(details)
}
});
});
@@ -168,7 +177,9 @@
if(!waitingOnClientStop && !waitingOnServerStop) {
transitionToStopped();
- $self.triggerHandler('stoppedRecording', {recordingId: recordingId, reason: errorReason, detail: errorDetail});
+ var details = {recordingId: recordingId, reason: errorReason, detail: errorDetail, isRecording: false}
+ $self.triggerHandler('stoppedRecording', details)
+ context.RecordingActions.stoppedRecording(details)
}
}
@@ -198,12 +209,16 @@
if(success) {
- $self.triggerHandler('startedRecording', {clientId: app.clientId})
+ var details = {clientId: app.clientId, isRecording:true}
+ $self.triggerHandler('startedRecording', details)
+ context.RecordingActions.startedRecording(details)
}
else {
currentlyRecording = false;
logger.error("unable to start the recording %o, %o", reason, detail);
- $self.triggerHandler('startedRecording', { clientId: app.clientId, reason: reason, detail: detail});
+ var details = { clientId: app.clientId, reason: reason, detail: detail, isRecording: false}
+ $self.triggerHandler('startedRecording', details);
+ context.RecordingActions.startedRecording(details)
}
}
@@ -221,7 +236,9 @@
else {
transitionToStopped();
logger.error("backend unable to stop the recording %o, %o", reason, detail);
- $self.triggerHandler('stoppedRecording', {recordingId: recordingId, reason: reason, detail : detail});
+ var details = {recordingId: recordingId, reason: reason, detail : detail, isRecording: false}
+ $self.triggerHandler('stoppedRecording', details);
+ context.RecordingActions.stoppedRecording(details)
}
}
@@ -242,9 +259,14 @@
currentOrLastRecordingId = recording.id;
});
- $self.triggerHandler('startingRecording', {recordingId: recordingId});
+ var details = {recordingId: recordingId, isRecording: false}
+ $self.triggerHandler('startingRecording', details);
+ context.RecordingActions.startingRecording(details)
currentlyRecording = true;
- $self.triggerHandler('startedRecording', {clientId: clientId, recordingId: recordingId});
+
+ details = {clientId: clientId, recordingId: recordingId, isRecording: true}
+ $self.triggerHandler('startedRecording', details);
+ context.RecordingActions.startedRecording(details)
}
function handleRecordingStopped(recordingId, result) {
@@ -253,7 +275,10 @@
var detail = result.detail;
- $self.triggerHandler('stoppingRecording', {recordingId: recordingId, reason: reason, detail: detail });
+ var details = {recordingId: recordingId, reason: reason, detail: detail, isRecording: true }
+ $self.triggerHandler('stoppingRecording', details);
+ context.RecordingActions.stoppingRecording(details)
+
// the backend says the recording must be stopped.
// tell the server to stop it too
rest.stopRecording({
@@ -265,18 +290,26 @@
.fail(function(jqXHR, textStatus, errorMessage) {
if(jqXHR.status == 422) {
logger.debug("recording already stopped %o", arguments);
- $self.triggerHandler('stoppedRecording', {recordingId: recordingId, reason: reason, detail: detail});
+ var details = {recordingId: recordingId, reason: reason, detail: detail, isRecording: false}
+ $self.triggerHandler('stoppedRecording', details);
+ context.RecordingActions.stoppedRecording(details)
}
else if(jqXHR.status == 404) {
logger.debug("recording is already deleted %o", arguments);
- $self.triggerHandler('stoppedRecording', {recordingId: recordingId, reason: reason, detail: detail});
+ var details = {recordingId: recordingId, reason: reason, detail: detail, isRecording: false}
+ $self.triggerHandler('stoppedRecording', details);
+ context.RecordingActions.stoppedRecording(details)
}
else {
- $self.triggerHandler('stoppedRecording', {recordingId: recordingId, reason: textStatus, detail: errorMessage});
+ var details = {recordingId: recordingId, reason: textStatus, detail: errorMessage, isRecording: false}
+ $self.triggerHandler('stoppedRecording', details);
+ context.RecordingActions.stoppedRecording(details)
}
})
.done(function() {
- $self.triggerHandler('stoppedRecording', {recordingId: recordingId, reason: reason, detail: detail});
+ var details = {recordingId: recordingId, reason: reason, detail: detail, isRecording: false}
+ $self.triggerHandler('stoppedRecording', details);
+ context.RecordingActions.stoppedRecording(details)
})
}
@@ -287,7 +320,9 @@
stoppingRecording = false;
- $self.triggerHandler('abortedRecording', {recordingId: recordingId, reason: reason, detail: detail });
+ var details = {recordingId: recordingId, reason: reason, detail: detail, isRecording: false }
+ $self.triggerHandler('abortedRecording', details);
+ context.RecordingActions.abortedRecording(details)
// the backend says the recording must be stopped.
// tell the server to stop it too
rest.stopRecording({
diff --git a/web/app/assets/javascripts/session.js b/web/app/assets/javascripts/session.js
index 1827758af..204751763 100644
--- a/web/app/assets/javascripts/session.js
+++ b/web/app/assets/javascripts/session.js
@@ -1409,13 +1409,13 @@
var metronome = {}
$('.session-recording-name').text(name);//sessionModel.getCurrentSession().backing_track_path);
- var noCorrespondingTracks = false;
- var mixer = metronomeTrackMixers[0]
- var preMasteredClass = "";
- // find the track or tracks that correspond to the mixer
- var correspondingTracks = []
- correspondingTracks.push(metronome);
-
+ var noCorrespondingTracks = false;
+ var mixer = metronomeTrackMixers[0]
+ var preMasteredClass = "";
+ // find the track or tracks that correspond to the mixer
+ var correspondingTracks = []
+ correspondingTracks.push(metronome);
+
if(correspondingTracks.length == 0) {
noCorrespondingTracks = true;
app.notify({
@@ -1941,7 +1941,7 @@
// Given a mixerID and a value between 0.0-1.0,
// light up the proper VU lights.
- function _updateVU(mixerId, value, isClipping) {
+ function _updateVU(mixerId, value, isClipping) {
// Special-case for mono tracks. If mono, and it's a _vul id,
// update both sides, otherwise do nothing.
@@ -2142,8 +2142,8 @@
setFormFromMetronome();
// This isn't actually there, so we rely on the metroSound as set from select on form:
- // metroSound = args.sound
- context.JK.CurrentSessionModel.refreshCurrentSession(true);
+ // metroSound = args.sound
+ context.JK.CurrentSessionModel.refreshCurrentSession(true);
}
function handleVolumeChangeCallback(mixerId, isLeft, value, isMuted) {
@@ -2177,8 +2177,8 @@
// TODO - no guarantee range will be -80 to 20. Get from the
// GetControlState for this mixer which returns min/max
// value is a DB value from -80 to 20. Convert to float from 0.0-1.0
- _updateVU(mixerId + "_vul", (leftValue + 80) / 100, leftClipping);
- _updateVU(mixerId + "_vur", (rightValue + 80) / 100, rightClipping);
+ _updateVU(mixerId + "_vul", (leftValue + 80) / 80, leftClipping);
+ _updateVU(mixerId + "_vur", (rightValue + 80) / 80, rightClipping);
}
else if(eventName === 'connection_status') {
var mixerId = vuInfo[1];
@@ -3009,6 +3009,7 @@
function closeMetronomeTrack() {
rest.closeMetronome({id: sessionModel.id()})
.done(function() {
+ logger.debug("session: SessionCloseMetronome")
context.jamClient.SessionCloseMetronome();
sessionModel.refreshCurrentSession(true);
})
@@ -3248,6 +3249,7 @@
$metronomePlaybackSelect.metronomePlaybackMode().on(EVENTS.METRONOME_PLAYBACK_MODE_SELECTED, metronomePlaybackModeChanged)
context.JK.helpBubble($metronomePlaybackHelp, 'metromone-playback-modes', {} , {offsetParent: $screen, width:'400px'});
$(document).on('layout_resized', function() {
+ console.log("RESIZE FLUID")
resizeFluid();
});
}
@@ -3269,10 +3271,10 @@
'beforeLeave' : beforeLeave,
'beforeDisconnect' : beforeDisconnect,
};
- app.bindScreen('session', screenBindings);
+ //app.bindScreen('session', screenBindings);
$recordingManagerViewer = $('#recording-manager-viewer');
- $screen = $('#session-screen');
+ $screen = $('#session-screen-old');
$mixModeDropdown = $screen.find('select.monitor-mode')
$templateMixerModeChange = $('#template-mixer-mode-change');
$otherAudioContainer = $('#session-recordedtracks-container');
diff --git a/web/app/assets/javascripts/sidebar.js b/web/app/assets/javascripts/sidebar.js
index cf733c451..19c43bc94 100644
--- a/web/app/assets/javascripts/sidebar.js
+++ b/web/app/assets/javascripts/sidebar.js
@@ -263,8 +263,8 @@
var recordingId = payload.recording_id;
- if(recordingId && context.JK.CurrentSessionModel.recordingModel.isRecording(recordingId)) {
- context.JK.CurrentSessionModel.recordingModel.onServerStopRecording(recordingId);
+ if(recordingId && context.RecordingStore.recordingModel.isRecording(recordingId)) {
+ context.RecordingStore.recordingModel.onServerStopRecording(recordingId);
}
else {
app.notify({
@@ -305,11 +305,11 @@
logger.debug("Handling SOURCE_UP_REQUESTED msg " + JSON.stringify(payload));
- var current_session_id = context.JK.CurrentSessionModel.id();
+ var current_session_id = context.SessionStore.id();
if (!current_session_id) {
// we are not in a session
- var last_session = context.JK.CurrentSessionModel.getCurrentOrLastSession();
+ var last_session = context.SessionStore.getCurrentOrLastSession();
if(last_session && last_session.id == payload.music_session) {
// the last session we were in was responsible for this message. not that odd at all
logger.debug("SOURCE_UP_REQUESTED came in for session_id" + payload.music_session + ", but was dropped because we have left that session")
@@ -328,7 +328,7 @@
'', payload.bitrate)
}
else {
- var last_session = context.JK.CurrentSessionModel.getCurrentOrLastSession();
+ var last_session = context.SessionStore.getCurrentOrLastSession();
if(last_session && last_session.id == payload.music_session) {
// the last session we were in was responsible for this message. not that odd at all
logger.debug("SOURCE_UP_REQUESTED came in for session_id" + payload.music_session + ", but was dropped because we have left that session and are in a new one")
@@ -346,11 +346,11 @@
function registerSourceDownRequested() {
context.JK.JamServer.registerMessageCallback(context.JK.MessageType.SOURCE_DOWN_REQUESTED, function(header, payload) {
logger.debug("Handling SOURCE_DOWN_REQUESTED msg " + JSON.stringify(payload));
- var current_session_id = context.JK.CurrentSessionModel.id();
+ var current_session_id = context.SessionStore.id();
if (!current_session_id) {
// we are not in a session
- var last_session = context.JK.CurrentSessionModel.getCurrentOrLastSession();
+ var last_session = context.SessionStore.getCurrentOrLastSession();
if(last_session && last_session.id == payload.music_session) {
// the last session we were in was responsible for this message. not that odd at all
logger.debug("SOURCE_DOWN_REQUESTED came in for session_id" + payload.music_session + ", but was dropped because we have left that session")
@@ -367,7 +367,7 @@
context.jamClient.SessionLiveBroadcastStop();
}
else {
- var last_session = context.JK.CurrentSessionModel.getCurrentOrLastSession();
+ var last_session = context.SessionStore.getCurrentOrLastSession();
if(last_session && last_session.id == payload.music_session) {
// the last session we were in was responsible for this message. not that odd at all
logger.debug("SOURCE_DOWN_REQUESTED came in for session_id" + payload.music_session + ", but was dropped because we have left that session and are in a new one")
diff --git a/web/app/assets/javascripts/sync_viewer.js.coffee b/web/app/assets/javascripts/sync_viewer.js.coffee
index 0bf819d2e..a3122282e 100644
--- a/web/app/assets/javascripts/sync_viewer.js.coffee
+++ b/web/app/assets/javascripts/sync_viewer.js.coffee
@@ -672,7 +672,7 @@ context.JK.SyncViewer = class SyncViewer
sendCommand: ($retry, cmd) =>
- if context.JK.CurrentSessionModel and context.JK.CurrentSessionModel.inSession()
+ if context.SessionStore.inSession()
context.JK.ackBubble($retry, 'sync-viewer-paused', {}, {offsetParent: $retry.closest('.dialog')})
else
context.jamClient.OnTrySyncCommand(cmd)
@@ -817,7 +817,7 @@ context.JK.SyncViewer = class SyncViewer
exportRecording: (e) =>
$export = $(e.target)
- if context.JK.CurrentSessionModel and context.JK.CurrentSessionModel.inSession()
+ if context.SessionStore.inSession()
context.JK.ackBubble($export, 'sync-viewer-paused', {}, {offsetParent: $export.closest('.dialog')})
return
@@ -837,7 +837,7 @@ context.JK.SyncViewer = class SyncViewer
deleteRecording: (e) =>
$delete = $(e.target)
- if context.JK.CurrentSessionModel and context.JK.CurrentSessionModel.inSession()
+ if context.SessionStore.inSession()
context.JK.ackBubble($delete, 'sync-viewer-paused', {}, {offsetParent: $delete.closest('.dialog')})
return
diff --git a/web/app/assets/javascripts/trackHelpers.js b/web/app/assets/javascripts/trackHelpers.js
index b6c61385e..f2be21ea5 100644
--- a/web/app/assets/javascripts/trackHelpers.js
+++ b/web/app/assets/javascripts/trackHelpers.js
@@ -7,6 +7,8 @@
"use strict";
+ var ChannelGroupIds = context.JK.ChannelGroupIds
+
context.JK = context.JK || {};
// As these are helper functions, just have a single
@@ -14,13 +16,15 @@
// take all necessary arguments to complete its work.
context.JK.TrackHelpers = {
- getTrackInfo: function(jamClient) {
+ getTrackInfo: function(jamClient, masterTracks) {
- var allTracks = context.jamClient.SessionGetAllControlState(true);
+ if(masterTracks === undefined) {
+ masterTracks = context.jamClient.SessionGetAllControlState(true);
+ }
- var userTracks = context.JK.TrackHelpers.getUserTracks(jamClient, allTracks);
- var backingTracks = context.JK.TrackHelpers.getBackingTracks(jamClient, allTracks);
- var metronomeTracks = context.JK.TrackHelpers.getTracks(jamClient, 16);
+ var userTracks = context.JK.TrackHelpers.getUserTracks(jamClient, masterTracks);
+ var backingTracks = context.JK.TrackHelpers.getBackingTracks(jamClient, masterTracks);
+ var metronomeTracks = context.JK.TrackHelpers.getTracks(jamClient, ChannelGroupIds.MetronomeGroup);
return {
userTracks: userTracks,
@@ -51,7 +55,7 @@
// allTracks is the result of SessionGetAllControlState; as an optimization
getBackingTracks: function(jamClient, allTracks) {
- var mediaTracks = context.JK.TrackHelpers.getTracks(jamClient, 6, allTracks);
+ var mediaTracks = context.JK.TrackHelpers.getTracks(jamClient, ChannelGroupIds.MediaTrackGroup, allTracks);
var backingTracks = []
context._.each(mediaTracks, function(mediaTrack) {
@@ -80,7 +84,7 @@
var localMusicTracks = [];
var i;
- localMusicTracks = context.JK.TrackHelpers.getTracks(jamClient, 4, allTracks);
+ localMusicTracks = context.JK.TrackHelpers.getTracks(jamClient, ChannelGroupIds.AudioInputMusicGroup, allTracks);
var trackObjects = [];
diff --git a/web/app/assets/javascripts/ui_helper.js b/web/app/assets/javascripts/ui_helper.js
index 11fb08af1..5ba6362af 100644
--- a/web/app/assets/javascripts/ui_helper.js
+++ b/web/app/assets/javascripts/ui_helper.js
@@ -74,8 +74,8 @@
return genreSelectorDialog.showDialog();
}
- function launchRecordingSelectorDialog(recordings, selectedRecordings, callback) {
- var recordingSelectorDialog = new JK.RecordingSelectorDialog(JK.app, recordings, selectedRecordings, callback);
+ function launchRecordingSelectorDialog(selectedRecordings, callback) {
+ var recordingSelectorDialog = new JK.RecordingSelectorDialog(JK.app, selectedRecordings, callback);
recordingSelectorDialog.initialize();
return recordingSelectorDialog.showDialog();
}
diff --git a/web/app/assets/javascripts/utils.js b/web/app/assets/javascripts/utils.js
index 8af882715..3d8359137 100644
--- a/web/app/assets/javascripts/utils.js
+++ b/web/app/assets/javascripts/utils.js
@@ -21,6 +21,7 @@
var os = null;
+ var reactHovers = []
context.JK.getGenreList = function() {
return context.JK.Rest().getGenres();
}
@@ -209,6 +210,84 @@
})
return $element;
}
+
+ /** Creates a hover element that does not dissappear when the user mouses over the hover.
+ *
+ * @param $element
+ * @param text
+ * @param options
+ */
+ context.JK.interactReactBubble = function($element, reactElementName, reactPropsCallback, options) {
+
+ if(!options) options = {};
+
+ context._.each(reactHovers, function(react) {
+ reactHovers.btOff();
+ })
+ reactHovers = []
+ var reactElement = null
+ var reactDomNode = null;
+
+ function cleanupReact() {
+ if(reactDomNode) {
+ logger.debug()
+ React.unmountComponentAtNode(reactDomNode)
+ }
+ }
+ function waitForBubbleHover($bubble) {
+ $bubble.hoverIntent({
+ over: function() {
+ if(timeout) {
+ clearTimeout(timeout);
+ timeout = null;
+ }
+ },
+ out: function() {
+ //$element.btOff();
+ }});
+ }
+
+ var timeout = null;
+
+ options.postHide = cleanupReact;
+ options.trigger = 'none'
+ options.clickAnywhereToClose = true
+ options.closeWhenOthersOpen = true
+ options.preShow = function(container) {
+ var reactElement = context[reactElementName]
+ if(!reactElementName) {
+ throw "unknown react element" + reactElementName
+ }
+ reactElement= React.createElement(reactElement, reactPropsCallback());
+ var $container = $(container)
+ reactDomNode = $container.find('.react-holder').get(0)
+ $(reactDomNode).data('bt', $element)
+ React.render(reactElement, reactDomNode)
+ }
+ options.postShow = function(container) {
+
+ if(timeout) {
+ clearTimeout(timeout);
+ timeout = null;
+ }
+ waitForBubbleHover($(container))
+ timeout = setTimeout(function() {/**$element.btOff()*/}, 3000)
+ }
+
+ $element.hoverIntent({
+ over: function() {
+ $element.btOn();
+ },
+ out: function() {
+
+ }});
+
+ options.cssStyles = {}
+ options.padding = 0;
+ context.JK.hoverBubble($element, '
', options)
+ return $element;
+ }
+
/**
* Associates a bubble on hover (by default) with the specified $element, using jquery.bt.js (BeautyTips)
* @param $element The element that should show the bubble when hovered
@@ -263,6 +342,15 @@
}
+ context.JK.groupIdDisplay = function(mixer) {
+ if(mixer && mixer.group_id) {
+ return context.JK.ChannelGroupLookup[mixer.group_id]
+ }
+ else {
+ return "?group?"
+ }
+ }
+
context.JK.bindProfileClickEvents = function($parent, dialogsToClose) {
if (!$parent) {
$parent = $('body');
@@ -804,6 +892,19 @@
return ul;
}
+ context.JK.reset_errors = function($container) {
+ $container.find('.error-text').remove()
+ $container.find('.error').removeClass("error")
+ }
+
+ context.JK.append_errors = function($field, fieldName, errors_data) {
+ var $ul = context.JK.format_errors(fieldName, errors_data);
+ if($ul != null) {
+ delete errors_data['errors'][fieldName];
+ $field.closest('div.field').addClass('error').end().after($ul);
+ }
+ }
+
context.JK.format_all_errors = function (errors_data) {
var errors = errors_data["errors"];
if (errors == null) return $('
');
@@ -1353,7 +1454,7 @@
/** validates that no changes are being made to tracks while recording */
context.JK.verifyNotRecordingForTrackChange = function (app) {
- if (context.JK.CurrentSessionModel.recordingModel.isRecording()) {
+ if (context.RecordingStore.recordingModel.isRecording()) {
app.notify({
title: "Currently Recording",
text: "Tracks cannot be modified while recording.",
diff --git a/web/app/assets/javascripts/voiceChatHelper.js b/web/app/assets/javascripts/voiceChatHelper.js
index e4d744178..d9e14d73f 100644
--- a/web/app/assets/javascripts/voiceChatHelper.js
+++ b/web/app/assets/javascripts/voiceChatHelper.js
@@ -336,14 +336,14 @@
// renders volumes based on what the backend says
function renderVolumes() {
- var $fader = $voiceChatFader.find('[control="fader"]');
+ var $fader = $voiceChatFader.find('[data-control="fader"]');
var db = context.jamClient.FTUEGetChatInputVolume();
var faderPct = db + 80;
context.JK.FaderHelpers.setHandlePosition($fader, faderPct);
}
function renderNoVolume() {
- var $fader = $voiceChatFader.find('[control="fader"]');
+ var $fader = $voiceChatFader.find('[data-control="fader"]');
context.JK.FaderHelpers.setHandlePosition($fader, 50);
context.JK.VuHelpers.updateVU($voiceChatVuLeft, 0);
context.JK.VuHelpers.updateVU($voiceChatVuRight, 0);
@@ -384,7 +384,7 @@
renderVolumes();
uniqueCallbackName = 'voiceChatHelperChatInputVUCallback' + caller;
- context.JK[uniqueCallbackName] = function(dbValue) {
+ context.JK[uniqueCallbackName] = function(dbValue, leftClip, rightClip) {
context.JK.ftueVUCallback(dbValue, $voiceChatVuLeft);
context.JK.ftueVUCallback(dbValue, $voiceChatVuRight);
}
diff --git a/web/app/assets/javascripts/vuHelpers.js b/web/app/assets/javascripts/vuHelpers.js
index e9b4c70ce..e2173f6dd 100644
--- a/web/app/assets/javascripts/vuHelpers.js
+++ b/web/app/assets/javascripts/vuHelpers.js
@@ -14,6 +14,8 @@
// take all necessary arguments to complete its work.
context.JK.VuHelpers = {
+ registeredMixers: [],
+
/**
* Render a VU meter into the provided selector.
* vuType can be either "horizontal" or "vertical"
@@ -93,7 +95,154 @@
}
})
- }
+ },
+
+ createQualifiedId: function(mixer) {
+ return (mixer.mode ? 'M' : 'P') + mixer.id
+ },
+
+ // type can be 'single' or 'double', meaning how the VU is represented (one set of lights, two)
+ // mixerId is the ID of the mixer
+ // and someFunction is used to make the registration (equality check).
+ registerVU: function(type, mixer, someFunction, horizontal, lightCount, lights) {
+
+ var fqId = this.createQualifiedId(mixer)
+ var registrations = this.registeredMixers[fqId]
+ if (!registrations) {
+ registrations = []
+ this.registeredMixers[fqId] = registrations
+ }
+
+ if(type == 'best') {
+ registrations.push({type:type, ptr: someFunction, ptrCount: 1, horizontal: horizontal, lightCount: lightCount, lights:lights})
+ }
+ else {
+ // find the right registration and add left lights or right lights to it
+ var found = null
+ context._.each(registrations, function(registration) {
+ if(registration.ptr == someFunction) {
+ found = registration;
+ return false;
+ }
+ })
+
+ if(!found) {
+ found = {type:type, ptr: someFunction, ptrCount: 1, horizontal: horizontal, lightCount: lightCount}
+ registrations.push(found);
+ }
+ else {
+ found.ptrCount++;
+ }
+
+ if(type == 'left') {
+ logger.debug("adding left lights")
+ found.leftLights = lights;
+ }
+ else {
+ logger.debug("adding right lights");
+ found.rightLights = lights;
+ }
+
+ }
+ },
+
+ unregisterVU: function(mixer, someFunction) {
+ var fqId = this.createQualifiedId(mixer)
+ var registrations = this.registeredMixers[fqId]
+ if (!registrations || registrations.length == 0) {
+ logger.debug("no registration found for:" + fqId, registrations, this.registeredMixers)
+ return
+ }
+ else {
+ logger.debug("unregistering " + fqId + ", " + registrations.length)
+ }
+
+ var origLength = registrations.length;
+ registrations = registrations.filter(function(element) {
+ var isMatch = element.ptr == someFunction;
+
+ if(isMatch) {
+ // found a registration that matches
+ logger.debug("removing matching ptr", element.ptr)
+ element.ptrCount--;
+
+ // keep the registration if any ptr's still left
+ var keepRegistration = element.ptrCount > 0;
+ if(!keepRegistration) {
+ logger.debug("getting rid of the registration; no more ptrs");
+ }
+ return keepRegistration;
+ }
+ else {
+ // keep the registration if this does not match the ptr
+ return true;
+ }
+ })
+
+ this.registeredMixers[fqId] = registrations
+ },
+
+ updateSingleVU: function(horizontal, lightCount, $lights, value, isClipping) {
+
+ var i = 0;
+ var state = 'on';
+ var lights = Math.round(value * lightCount);
+ var redSwitch = Math.round(lightCount * 0.6666667);
+
+ var $light = null;
+ var colorClass = 'vu-green-';
+ var thisLightSelector = null;
+
+ // Remove all light classes from all lights
+ $lights.removeClass('vu-green-off vu-green-on vu-red-off vu-red-on');
+
+ // Set the lights
+ for (i = 0; i < lightCount; i++) {
+ colorClass = 'vu-green-';
+ state = 'on';
+ if (i >= redSwitch) {
+ colorClass = 'vu-red-';
+ }
+ if (i >= lights) {
+ state = 'off';
+ }
+
+ var lightIndex = horizontal ? i : lightCount - i - 1;
+ $lights.eq(lightIndex).addClass(colorClass + state);
+ }
+ },
+
+ // sentMixerId ends with vul or vur
+ updateVU3: function(mixer, leftValue, leftClipping, rightValue, rightClipping) {
+
+ var fqId = this.createQualifiedId(mixer)
+
+ var registrations = this.registeredMixers[fqId]
+ if (registrations) {
+ var j;
+ for(j = 0; j < registrations.length; j++) {
+ var registration = registrations[j]
+ var horizontal = registration.horizontal;
+ var lightCount = registration.lightCount;
+
+ if(registration.type == 'best') {
+ // TODO: find 'active' VU ... is it left value, or right value?
+ var $lights = registration.lights;
+ this.updateSingleVU(horizontal, lightCount, $lights, leftValue, leftClipping)
+ }
+ else {
+ if(mixer.stereo) {
+ this.updateSingleVU(horizontal, lightCount, registration.leftLights, leftValue, leftClipping)
+ this.updateSingleVU(horizontal, lightCount, registration.rightLights, rightValue, rightClipping)
+ }
+ else {
+ this.updateSingleVU(horizontal, lightCount, registration.leftLights, leftValue, leftClipping)
+ this.updateSingleVU(horizontal, lightCount, registration.rightLights, leftValue, leftClipping)
+ }
+ }
+ }
+ }
+ },
};
diff --git a/web/app/assets/javascripts/web/individual_jamtrack.js b/web/app/assets/javascripts/web/individual_jamtrack.js
index ebb720f2c..6b0b8fd78 100644
--- a/web/app/assets/javascripts/web/individual_jamtrack.js
+++ b/web/app/assets/javascripts/web/individual_jamtrack.js
@@ -8,35 +8,13 @@
var rest = context.JK.Rest();
var logger = context.JK.logger;
var $page = null;
- var $jamtrack_name = null;
- var $jamtrack_band = null;
var $previews = null;
var $jamTracksButton = null;
- var $genericHeader = null;
- var $individualizedHeader = null;
var $ctaJamTracksButton = null;
function fetchJamTrack() {
rest.getJamTrackWithArtistInfo({plan_code: gon.jam_track_plan_code})
.done(function (jam_track) {
- logger.debug("jam_track", jam_track)
-
- if(!gon.just_previews) {
- if (gon.generic) {
- $genericHeader.removeClass('hidden');
- $jamTracksButton.attr('href', '/client#/jamtrackBrowse')
- $jamTracksButton.removeClass('hidden').text("Check out all 100+ JamTracks")
-
- }
- else {
- $individualizedHeader.removeClass('hidden')
- $jamtrack_name.text('"' + jam_track.name + '"');
- $jamtrack_band.text(jam_track.original_artist)
- $jamTracksButton.attr('href', '/client?artist=' + jam_track.original_artist + '#/jamtrackBrowse')
- $jamTracksButton.removeClass('hidden').text("Preview all " + jam_track.band_jam_track_count + " of our " + jam_track.original_artist + " JamTracks")
- $ctaJamTracksButton.attr('href', '/client?artist=' + jam_track.original_artist + '#/jamtrackBrowse')
- }
- }
context._.each(jam_track.tracks, function (track) {
@@ -44,10 +22,10 @@
$previews.append($element);
- new context.JK.JamTrackPreview(app, $element, jam_track, track, {master_shows_duration: false, color:'black', master_adds_line_break: true, preload_master:true})
+ new context.JK.JamTrackPreview(app, $element, jam_track, track, {master_shows_duration: true, color:'black', master_adds_line_break: false, preload_master:true})
if(track.track_type =='Master') {
- context.JK.HelpBubbleHelper.rotateJamTrackLandingBubbles($element.find('.jam-track-preview'), $page.find('.video-wrapper'), $page.find('.cta-free-jamtrack a'), $page.find('a.browse-jamtracks'));
+ context.JK.HelpBubbleHelper.rotateJamTrackLandingBubbles($element.find('.jam-track-preview'), $page.find('.one_by_two .watch-video'), $page.find('.checkout'));
}
})
@@ -60,13 +38,9 @@
function initialize() {
$page = $('body')
- $jamtrack_name = $page.find('.jamtrack_name')
- $jamtrack_band = $page.find('.jamtrack_band')
$previews = $page.find('.previews')
$jamTracksButton = $page.find('.browse-jamtracks')
$ctaJamTracksButton = $page.find('.cta-free-jamtrack');
- $genericHeader = $page.find('h1.generic')
- $individualizedHeader = $page.find('h1.individualized')
context.JK.Tracking.adTrack(app)
fetchJamTrack();
diff --git a/web/app/assets/javascripts/web/individual_jamtrack_band.js b/web/app/assets/javascripts/web/individual_jamtrack_band_v1.js
similarity index 100%
rename from web/app/assets/javascripts/web/individual_jamtrack_band.js
rename to web/app/assets/javascripts/web/individual_jamtrack_band_v1.js
diff --git a/web/app/assets/javascripts/web/individual_jamtrack_v1.js b/web/app/assets/javascripts/web/individual_jamtrack_v1.js
new file mode 100644
index 000000000..a9fc6ff38
--- /dev/null
+++ b/web/app/assets/javascripts/web/individual_jamtrack_v1.js
@@ -0,0 +1,77 @@
+(function (context, $) {
+
+ "use strict";
+
+ context.JK = context.JK || {};
+ context.JK.IndividualJamTrackv1 = function (app) {
+
+ var rest = context.JK.Rest();
+ var logger = context.JK.logger;
+ var $page = null;
+ var $jamtrack_name = null;
+ var $jamtrack_band = null;
+ var $previews = null;
+ var $jamTracksButton = null;
+ var $genericHeader = null;
+ var $individualizedHeader = null;
+ var $ctaJamTracksButton = null;
+
+ function fetchJamTrack() {
+ rest.getJamTrackWithArtistInfo({plan_code: gon.jam_track_plan_code})
+ .done(function (jam_track) {
+ logger.debug("jam_track", jam_track)
+
+ if(!gon.just_previews) {
+ if (gon.generic) {
+ $genericHeader.removeClass('hidden');
+ $jamTracksButton.attr('href', '/client#/jamtrackBrowse')
+ $jamTracksButton.removeClass('hidden').text("Check out all 100+ JamTracks")
+
+ }
+ else {
+ $individualizedHeader.removeClass('hidden')
+ $jamtrack_name.text('"' + jam_track.name + '"');
+ $jamtrack_band.text(jam_track.original_artist)
+ $jamTracksButton.attr('href', '/client?artist=' + jam_track.original_artist + '#/jamtrackBrowse')
+ $jamTracksButton.removeClass('hidden').text("Preview all " + jam_track.band_jam_track_count + " of our " + jam_track.original_artist + " JamTracks")
+ $ctaJamTracksButton.attr('href', '/client?artist=' + jam_track.original_artist + '#/jamtrackBrowse')
+ }
+ }
+
+ context._.each(jam_track.tracks, function (track) {
+
+ var $element = $('
')
+
+ $previews.append($element);
+
+ new context.JK.JamTrackPreview(app, $element, jam_track, track, {master_shows_duration: false, color:'black', master_adds_line_break: true, preload_master:true})
+
+ if(track.track_type =='Master') {
+ context.JK.HelpBubbleHelper.rotateJamTrackLandingBubbles($element.find('.jam-track-preview'), $page.find('.video-wrapper'), $page.find('.cta-free-jamtrack a'), $page.find('a.browse-jamtracks'));
+ }
+ })
+
+ $previews.append('
')
+ })
+ .fail(function () {
+ app.notify({title: 'Unable to fetch JamTrack', text: "Please refresh the page or try again later."})
+ })
+ }
+ function initialize() {
+
+ $page = $('body')
+ $jamtrack_name = $page.find('.jamtrack_name')
+ $jamtrack_band = $page.find('.jamtrack_band')
+ $previews = $page.find('.previews')
+ $jamTracksButton = $page.find('.browse-jamtracks')
+ $ctaJamTracksButton = $page.find('.cta-free-jamtrack');
+ $genericHeader = $page.find('h1.generic')
+ $individualizedHeader = $page.find('h1.individualized')
+
+ context.JK.Tracking.adTrack(app)
+ fetchJamTrack();
+ }
+
+ this.initialize = initialize;
+ }
+})(window, jQuery);
\ No newline at end of file
diff --git a/web/app/assets/javascripts/web/web.js b/web/app/assets/javascripts/web/web.js
index 89f49d6fe..561e44bf9 100644
--- a/web/app/assets/javascripts/web/web.js
+++ b/web/app/assets/javascripts/web/web.js
@@ -66,7 +66,8 @@
//= require web/home
//= require web/tracking
//= require web/individual_jamtrack
-//= require web/individual_jamtrack_band
+//= require web/individual_jamtrack_v1
+//= require web/individual_jamtrack_band_v1
//= require web/affiliate_program
//= require web/affiliate_links
//= require fakeJamClient
@@ -75,3 +76,9 @@
//= require JamServer
//= require_directory ../dialog
//= require everywhere/everywhere
+//= require classnames
+//= require reflux
+//= require react
+//= require react_ujs
+//= require react-init
+//= require react-components
diff --git a/web/app/assets/javascripts/webcam_viewer.js.coffee b/web/app/assets/javascripts/webcam_viewer.js.coffee
index ed2b1d772..931fef139 100644
--- a/web/app/assets/javascripts/webcam_viewer.js.coffee
+++ b/web/app/assets/javascripts/webcam_viewer.js.coffee
@@ -14,6 +14,10 @@ context.JK.WebcamViewer = class WebcamViewer
@resolution=null
init: (root) =>
+
+ # the session usage of webcamViewer does not actually pass in anything
+ root = $() unless root?
+
@root = root
@toggleBtn = @root.find(".webcam-test-btn")
@webcamSelect = @root.find(".webcam-select-container select")
diff --git a/web/app/assets/javascripts/wizard/loopback/step_loopback_test.js b/web/app/assets/javascripts/wizard/loopback/step_loopback_test.js
index ccba2ae88..da4a65af3 100644
--- a/web/app/assets/javascripts/wizard/loopback/step_loopback_test.js
+++ b/web/app/assets/javascripts/wizard/loopback/step_loopback_test.js
@@ -217,13 +217,13 @@
function renderVolumes() {
// input
- var $inputFader = $audioInputFader.find('[control="fader"]');
+ var $inputFader = $audioInputFader.find('[data-control="fader"]');
var db = context.jamClient.FTUEGetInputVolume();
var faderPct = db + 80;
context.JK.FaderHelpers.setHandlePosition($inputFader, faderPct);
// output
- var $outputFader = $audioOutputFader.find('[control="fader"]');
+ var $outputFader = $audioOutputFader.find('[data-control="fader"]');
var db = context.jamClient.FTUEGetOutputVolume();
var faderPct = db + 80;
context.JK.FaderHelpers.setHandlePosition($outputFader, faderPct);
diff --git a/web/app/assets/stylesheets/client/accountProfileInterests.css.scss b/web/app/assets/stylesheets/client/accountProfileInterests.css.scss
index 7e11df987..3f01f5de9 100644
--- a/web/app/assets/stylesheets/client/accountProfileInterests.css.scss
+++ b/web/app/assets/stylesheets/client/accountProfileInterests.css.scss
@@ -14,6 +14,7 @@
div.genres {
width: 20%;
margin-bottom: 15px;
+ float:left;
}
a.select-genre {
@@ -28,14 +29,42 @@
}
.interest-options {
- width: 30%;
- margin-bottom: 15px;
+ width: 33%;
+ margin-right: 20px;
+ margin-bottom: 20px;
label {
margin-bottom: 10px;
}
}
+ .play-commitment, .purpose {
+ width:150px;
+ .easydropdown-wrapper {
+ width:150px;
+ }
+ margin-right:20px;
+ }
+ .hourly-rate-holder {
+ margin-right:20px;
+ }
+
+ .yes-no-options {
+ .option {
+ float:left;
+ .iradio_minimal {
+ float:left;
+ }
+ label {
+ float:left;
+ margin:3px 0 0 3px;
+ }
+ &:nth-child(2) {
+ margin-left:20px;
+ }
+ }
+ }
+
input[type=text].rate {
width: 100px;
}
diff --git a/web/app/assets/stylesheets/client/band.css.scss b/web/app/assets/stylesheets/client/band.css.scss
index a9645cd01..e3b33b7a3 100644
--- a/web/app/assets/stylesheets/client/band.css.scss
+++ b/web/app/assets/stylesheets/client/band.css.scss
@@ -40,10 +40,10 @@
}
.radio-field {
- display: inline;
+ display: inline-block;
padding: 2px;
- margin: 0.5em 2em 0.5em 0.25em;
- label {
+ margin:5px 40px 0 0;
+ label {
display: inline;
padding: 2px;
}
@@ -53,9 +53,15 @@
}
}
+ .actions {
+ margin-right:13px;
+ float:right;
+ }
.band-setup-genres {
+ @include border_box_sizing;
+ padding:10px;
width:100%;
height:200px;
background-color:#c5c5c5;
@@ -399,16 +405,48 @@
width: 100%;
}
-
+ .band-step {
+ h2 {
+ margin-left:10px;
+ }
+ padding:10px;
+ }
+
+ #band-setup-step-0 {
+ .band-name {
+ width:36%;
+ }
+ }
+
+ #band-setup-step-1 {
+ .easydropdown-wrapper {
+ width:100% !important;
+ }
+ }
+
+ #band-setup-step-2 {
+ .band-form-table {
+ margin:20px 0 0 10px;
+ }
+
+ .iradio_minimal {
+ float:left;
+ }
+ label.radio-label {
+ float:left;
+ margin-left:5px;
+ }
+
+ tr:nth-child(even) td {
+ padding-bottom:20px;
+ }
+ }
+
#band-setup-form {
margin: 0.25em 0.5em 1.25em 0.25em;
table.band-form-table {
width: 100%;
- margin: 1em;
-
- tr:nth-child(even) td {
- padding-bottom: 1em;
- }
+ @include border_box_sizing;
td.band-biography, td.tdBandGenres {
height:100%;
diff --git a/web/app/assets/stylesheets/client/common.css.scss b/web/app/assets/stylesheets/client/common.css.scss
index 75edd1bc1..7441918f3 100644
--- a/web/app/assets/stylesheets/client/common.css.scss
+++ b/web/app/assets/stylesheets/client/common.css.scss
@@ -55,6 +55,14 @@ $poor: #980006;
$error: #980006;
$fair: #cc9900;
+$labelFontFamily: Arial, Helvetica, sans-serif;
+$labelFontSize: 12px;
+
+@mixin labelFont {
+ font-family: $labelFontFamily;
+ font-size: $labelFontSize;
+}
+
@mixin border_box_sizing {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
@@ -335,3 +343,7 @@ $fair: #cc9900;
text-transform: capitalize
}
+.vertical-helper {
+ display: inline-block;
+ height: 100%;
+}
diff --git a/web/app/assets/stylesheets/client/content-orig.css.scss b/web/app/assets/stylesheets/client/content-orig.css.scss
index e653f3f50..5c898a493 100644
--- a/web/app/assets/stylesheets/client/content-orig.css.scss
+++ b/web/app/assets/stylesheets/client/content-orig.css.scss
@@ -349,7 +349,7 @@ ul.shortcuts {
white-space:normal;
}
-.smallbutton {
+ .smallbutton {
font-size:10px !important;
padding:2px 8px !important;
}
diff --git a/web/app/assets/stylesheets/client/content.css.scss b/web/app/assets/stylesheets/client/content.css.scss
index 50d7e4876..a9292790a 100644
--- a/web/app/assets/stylesheets/client/content.css.scss
+++ b/web/app/assets/stylesheets/client/content.css.scss
@@ -179,7 +179,7 @@
margin-top: 10px;
margin-bottom: 10px;
> a.smallbutton {
- margin: 2px;
+ margin: 4px;
&.button-grey {
display:none; // @FIXME VRFS-930 / VRFS-931 per comment from David - don't show.
}
@@ -217,7 +217,7 @@
.content-wrapper, .dialog, .dialog-inner, .ftue-inner {
- select, textarea, input[type=text], input[type=password], div.friendbox {
+ select, textarea, input[type=text], input[type=password], div.friendbox, div.inputbox {
background-color:#c5c5c5;
border:none;
-webkit-box-shadow: inset 2px 2px 3px 0px #888;
diff --git a/web/app/assets/stylesheets/client/metronomePlaybackModeSelect.css.scss b/web/app/assets/stylesheets/client/metronomePlaybackModeSelect.css.scss
index 79d52c274..ff88057ea 100644
--- a/web/app/assets/stylesheets/client/metronomePlaybackModeSelect.css.scss
+++ b/web/app/assets/stylesheets/client/metronomePlaybackModeSelect.css.scss
@@ -1,6 +1,7 @@
@import "client/common";
.metronome-playback-mode-selector-popup {
+ text-align:left;
.bt-content {
width:180px;
background-color:#333;
diff --git a/web/app/assets/stylesheets/client/musician.css.scss b/web/app/assets/stylesheets/client/musician.css.scss
index e075a212e..7fcd81f54 100644
--- a/web/app/assets/stylesheets/client/musician.css.scss
+++ b/web/app/assets/stylesheets/client/musician.css.scss
@@ -11,6 +11,39 @@
}
}
+ .field > label {
+ margin-bottom:5px;
+ }
+ .session-instrumentlist {
+ padding: 10px;
+ height: 100px;
+ background-color: #c5c5c5;
+ border: none;
+ -webkit-box-shadow: inset 2px 2px 3px 0px #888;
+ box-shadow: inset 2px 2px 3px 0px #888;
+ color: #000;
+ overflow: auto;
+ font-size: 14px;
+ @include border_box_sizing;
+
+ select, .easydropdown {
+ @include flat_dropdown;
+ @include no_top_padding_dropdown;
+
+ .selected {
+ font-size:13px;
+ }
+ }
+
+ .dropdown-container {
+ @include white_dropdown;
+ }
+
+ label {
+ display:inline;
+ }
+ }
+
.btn-refresh-holder {
float:right;
margin-right:10px;
@@ -39,6 +72,11 @@
.musician-stats {
margin-top:10px;
+ img {
+ position: relative;
+ top: 2px;
+ left: 1px;
+ }
}
.musician-info {
margin-top: 12px;
@@ -138,17 +176,56 @@
}
}
- #musician-filter-results {
- margin: 0 10px 0px 10px;
+ #musician-search-filter-results-wrapper {
+ margin: 0 10px;
}
#musician-search-filter-results-header {
padding: 10px 10px 10px 10px;
+ background-color: #4C4C4C;
+ }
+
+ #search-filter-genres, #search-filter-ages {
+ background-color:$ColorTextBoxBackground;
+ height:150px;
+ overflow:auto;
+ padding:10px;
+ width:100%;
+ @include border_box_sizing;
+
+ label {
+ display:inline;
+ color:black;
+ margin-left: 3px;
+ }
+ }
+
+ #search-filter-ages {
+ height:85px;
+ width:75%;
+ }
+
+ .genre-option, .age-option {
+ font-size:14px;
+ margin:0 0 5px 0;
+ }
+
+ #btn-perform-musician-search {
+ margin-right:0;
}
#btn-musician-search-builder {
float: left;
}
+ .musician-search-text {
+ float:left;
+ font-size:12px;
+ margin-top:4px;
+ white-space: nowrap;
+ width: calc(100% - 180px);
+ text-overflow: ellipsis;
+ overflow: hidden;
+ }
#musician-search-filter-description {
padding: 5px 5px 5px 5px;
@@ -175,17 +252,26 @@
}
.col-left {
+ @include border_box_sizing;
float: left;
- width: 50%;
+ width: 33%;
margin-left: auto;
margin-right: auto;
}
.col-right {
float: right;
- width: 50%;
+ width: 67%;
+ @include border_box_sizing;
margin-left: auto;
margin-right: auto;
+
+ .col-left {
+ width: 50%;
+ }
+ .col-right {
+ width: 50%;
+ }
}
.builder-section {
@@ -196,18 +282,20 @@
float: right;
}
.band-setup-genres {
- width: 80%;
+ width: 80% !important;
}
.easydropdown-wrapper {
width: 80%;
}
.builder-sort-order {
+ padding-right:13.5%;
text-align: right;
.easydropdown-wrapper {
width: 140px;
+ vertical-align:middle;
}
.text-label {
- vertical-align: top;
+ vertical-align: middle;
margin-right: 5px;
display: inline;
line-height: 2em;
@@ -227,7 +315,10 @@
margin-top: 10px;
}
.builder-action-buttons {
- margin-top: 20px;
+ margin-top: 20px;
+ .col-right {
+ padding-right:13.5%;
+ }
}
}
diff --git a/web/app/assets/stylesheets/client/profile.css.scss b/web/app/assets/stylesheets/client/profile.css.scss
index 6e5665b5e..2f481f330 100644
--- a/web/app/assets/stylesheets/client/profile.css.scss
+++ b/web/app/assets/stylesheets/client/profile.css.scss
@@ -1,6 +1,10 @@
@import "client/common.css.scss";
#user-profile, #band-profile {
+ .user-header {
+ h2 {display:inline;}
+ a {margin:2px 0 0 20px}
+ }
.profile-about-right {
textarea {
@@ -9,29 +13,42 @@
padding:0;
}
}
-
- div.logo, div.item {
- text-align: bottom;
+ .section {
+ margin-bottom:40px;
+ }
+ .add-recordings, .add-interests, .add-presences, .add-bio, .add-experiences {
+ display:inline;
+ font-size:11px;
}
-
.online-presence-option, .performance-sample-option {
- margin-right: 1em;
+ display:block;
+ vertical-align:middle;
+ margin-bottom:20px;
+ &.no-online-presence {
+ display:block;
+ }
+ }
+ .instruments-holder {
+ margin-bottom:20px;
+ }
+ .statuses {
+ clear:both;
}
-
img.logo {
margin-right: 20px;
}
-
ul {
margin:0px 0px 10px 0px;
padding:0px;
}
-
li {
margin-left: 15px;
margin-bottom: 0px !important;
list-style: disc;
}
+ .playable {
+ display:block;
+ }
}
.profile-head {
@@ -56,14 +73,12 @@
.section-header {
font-weight:600;
font-size:18px;
- float:left;
margin: 0px 0px 10px 0px;
}
.section-content {
font-weight:normal;
font-size:1.2em;
- float:left;
margin: 0px 0px 10px 0px;
}
}
diff --git a/web/app/assets/stylesheets/client/react-components/MediaControls.scss.scss b/web/app/assets/stylesheets/client/react-components/MediaControls.scss.scss
new file mode 100644
index 000000000..94e75da6c
--- /dev/null
+++ b/web/app/assets/stylesheets/client/react-components/MediaControls.scss.scss
@@ -0,0 +1,206 @@
+@import "client/common";
+
+.media-controls {
+ padding: 3px 0;
+ width:100%;
+ min-width:100%;
+ background-color: #242323;
+ position: relative;
+ font-size: 13px;
+ text-align: center;
+ @include border_box_sizing;
+ height: 36px;
+ display:block;
+ white-space:nowrap;
+
+ .play-buttons {
+ float:left;
+ margin-top:5px;
+ }
+
+ .recording-position {
+ float:left;
+ margin-left:10px;
+
+ }
+
+ .recording-time {
+ float:left;
+ margin-top:8px;
+ &.start-time {
+ margin-left:10px;
+ }
+ &.duration-time {
+ float:right;
+ }
+ }
+
+ .recording-playback {
+ float:left;
+ }
+
+ .recording-current {
+ display:none;
+ }
+
+ .recording-playback {
+ display:inline-block;
+ background-image:url(/assets/content/bkg_playcontrols.png);
+ background-repeat:repeat-x;
+ position:relative;
+ width:calc(100% - 115px);
+ margin-left:83px;
+ margin-right:20px;
+ margin-top:8px;
+ cursor:pointer;
+ height:16px;
+ position:absolute;
+ left:0;
+
+ }
+
+ .recording-slider {
+ position:absolute;
+ left:0;
+ top:0;
+
+ img {
+ position:absolute;
+ }
+ }
+
+ .metronome-playback-options {
+ float:left;
+ margin-left:10px;
+ margin-top:8px;
+ }
+
+ .metronome-options {
+ float:right;
+ }
+
+ &.jamtrack-mode, &.mediafile-mode {
+ .metronome-playback-options {
+ display:none;
+ }
+ .metronome-text {
+ display:none;
+ }
+ .metronome-options {
+ display:none;
+ }
+ }
+
+
+ &.metronome-mode {
+ .recording-time {display:none}
+ .recording-playback {display:none}
+ .recording-current {display:none}
+ .playback-mode-buttons {display:none}
+ .stop-button {display:none}
+
+ select {
+ width:75px;
+ float:right;
+ }
+
+ label {
+ float: right !important;
+ margin-left: 5px;
+ margin-top: 7px !important;
+ margin-right: 5px !important;
+ }
+ }
+
+ .recording-status {
+ font-size:15px;
+ }
+
+ .recording-status .recording-duration {
+ font-family:Arial, Helvetica, sans-serif;
+ display:inline-block;
+ font-size:18px;
+ position:absolute;
+ //top:3px;
+ right:4px;
+ }
+
+ .recording-slider {
+ cursor:pointer;
+ }
+
+
+ &.has-mix {
+ .recording-status {
+ display:none;
+ }
+ }
+
+ &:not(.has-mix) {
+
+ border-width: 0; // override screen_common's .error
+
+ .play-button {
+ display:none;
+ }
+ .recording-current {
+ display:none;
+ }
+ .recording-position {
+ display:none;
+ }
+ }
+
+ .jam-track-get-ready, .media-seeking {
+ display:none;
+ position:absolute;
+ top:20px;
+ margin-left:-50px;
+ width:100px;
+ vertical-align:middle;
+ height:32px;
+ line-height:32px;
+ left:50%;
+
+ .spinner-small {
+ vertical-align:middle;
+ display:inline-block;
+ }
+
+ span {
+ vertical-align:middle;
+ }
+ }
+
+ .jam-track-get-ready[data-mode="JAMTRACK"][data-current-time="0"] {
+ display:block;
+ }
+
+ .media-seeking[data-mode="SEEKING"] {
+ display:block;
+ }
+
+ .playback-mode-buttons {
+ display:none;
+ }
+
+ .play-button, .stop-button {
+ outline:none;
+ }
+
+ .stop-button {
+ margin-left:3px;
+ }
+
+ .play-button img.pausebutton {
+ display:none;
+ }
+
+ .metronome-controls {
+ float:left;
+ }
+
+ .metronome-options {
+ float:right;
+ }
+}
\ No newline at end of file
diff --git a/web/app/assets/stylesheets/client/react-components/SessionScreen.css.scss b/web/app/assets/stylesheets/client/react-components/SessionScreen.css.scss
new file mode 100644
index 000000000..783aad059
--- /dev/null
+++ b/web/app/assets/stylesheets/client/react-components/SessionScreen.css.scss
@@ -0,0 +1,386 @@
+@import 'client/common';
+
+$session-screen-divider: 1190px;
+@mixin session-small {
+ @media (max-width: #{$session-screen-divider - 1px}) {
+ @content;
+ }
+}
+@mixin session-normal {
+ @media (min-width: #{$session-screen-divider}) {
+ @content;
+ }
+}
+
+#session-screen {
+ .session-container {
+ overflow-x: hidden;
+ }
+
+ .session-track {
+ @include session-small {
+ max-width: 120px;
+ }
+
+ &.metronome, &.jam-track, &.recorded-track, &.backing-track {
+ @include session-small {
+ height:auto;
+ }
+ .track-icon-pan {
+ @include session-small {
+ margin-right:0;
+ }
+ }
+
+ .track-buttons {
+ @include session-small {
+ margin:12px 0 0;
+ }
+ }
+
+ table.vu {
+ @include session-small {
+ margin-top:5px;
+ }
+ }
+
+ .track-controls {
+ @include session-small {
+ margin-right:8px;
+ }
+ }
+
+ .track-icon-pan {
+ @include session-small {
+ margin-right:2px;
+ }
+ }
+ .track-instrument {
+ @include session-small {
+ margin: -4px 12px 0 0;
+ }
+ }
+ }
+ &.jam-track-category, &.recorded-category {
+ .track-controls {
+ @include session-small {
+ margin-top:5px;
+ margin-right:8px;
+ }
+ }
+ table.vu {
+ @include session-small {
+ margin-top:0;
+ }
+ }
+ .jam-track-header {
+ @include session-small {
+ float:left;
+ }
+ }
+ .name {
+ @include session-small {
+ float:left;
+ }
+ }
+ .track-buttons {
+ @include session-small {
+ margin-top:12px;
+ }
+ }
+ }
+ }
+
+ .track-controls {
+ @include session-small {
+ margin-top:8px;
+ }
+ }
+
+ .track-buttons {
+ @include session-small {
+ margin-left:14px;
+ }
+ }
+
+ h2 {
+ color: #fff;
+ font-weight: 600;
+ font-size: 24px;
+ margin-bottom: 15px;
+ }
+
+ .tracks {
+ position: absolute;
+ @include border_box_sizing;
+ top: 71px;
+ bottom: 0;
+ width: 100%;
+ }
+
+ .session-my-tracks, .session-other-tracks, .session-media-tracks {
+ @include border_box_sizing;
+ float: left;
+ width: 33%;
+ border-right: 1px solid #4c4c4c;
+ padding: 10px;
+ height: 100%;
+ margin-bottom: 15px;
+ color:$ColorTextTypical;
+ overflow:hidden;
+ position:relative;
+ }
+
+ .session-media-tracks {
+ width:34%;
+ }
+
+ .session-notifications {
+ border-right-width: 0;
+ display:none; //temp
+ }
+
+ .in-session-controls {
+
+ width: 100%;
+ padding: 11px 0px 11px 0px;
+ background-color: #4c4c4c;
+ min-height: 20px;
+ position: relative;
+ min-width: 690px;
+
+ .label {
+ float: left;
+ font-size: 12px;
+ color: #ccc;
+ margin: 0px 0px 0px 4px;
+ }
+
+ .block {
+ float: left;
+ margin: 6px 8px 0px 8px;
+ }
+
+ a {
+ img {
+ vertical-align:top;
+ margin-right: 4px;
+ }
+ span {
+ vertical-align:middle;
+ }
+ }
+
+ .button-grey {
+ margin:0 5px;
+ padding: 3px 7px;
+
+ &.session-leave {
+ margin-right:10px;
+ }
+ }
+ }
+
+ .session-tracks-scroller {
+ overflow-x: hidden;
+ overflow-y: auto;
+ width: 100%;
+
+ position: absolute;
+ top: 90px;
+ padding: 0 10px;
+ @include border_box_sizing;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ text-align:left;
+
+ &.media-options-showing {
+ top:180px;
+ }
+ }
+
+ p {
+ line-height: 125%;
+ margin: 0;
+ }
+
+ .download-jamtrack {
+ margin-top:20px;
+ }
+
+
+ .when-empty {
+ margin-top:20px;
+ margin-left:22px;
+ color:$ColorTextTypical;
+ overflow:hidden;
+ }
+
+ .session-track-settings {
+ height:20px;
+ cursor:pointer;
+ padding-bottom:1px; // to line up with SessionOtherTracks
+ color:$ColorTextTypical;
+
+ &:hover {
+ color:white;
+ }
+
+ span {
+ top: -5px;
+ position: relative;
+ left:3px;
+ }
+ }
+
+ .session-invite-musicians {
+ height:20px;
+ cursor: pointer;
+ color:$ColorTextTypical;
+
+ &:hover {
+ color:white;
+ }
+
+ span {
+ top:-5px;
+ position:relative;
+ left:3px;
+ }
+ }
+
+ .closeAudio, .session-clear-notifications {
+ cursor: pointer;
+ color:$ColorTextTypical;
+ height:20px;
+
+ img {
+ top:-2px
+ }
+ span {
+ top: -5px;
+ position: relative;
+ left: 3px;
+ }
+ }
+
+
+
+ .open-media-file-header, .use-metronome-header {
+ font-size:16px;
+ line-height:100%;
+ margin:0;
+
+ img {
+ position:relative;
+ top:3px;
+ }
+ }
+ .open-media-file-header {
+
+ img {
+ vertical-align:middle;
+ }
+
+ .open-text {
+ margin-left:5px;
+ vertical-align:bottom;
+ }
+ }
+
+ .use-metronome-header {
+ clear: both;
+ a {
+ color:$ColorTextTypical;
+ &:hover {
+ text-decoration: underline;
+ color:white;
+ }
+ }
+ }
+
+ .open-media-file-options {
+ font-size:14px;
+ margin: 7px 0 0 7px !important;
+ color:$ColorTextTypical;
+ li {
+ margin-bottom:5px !important;
+ margin-left:38px !important;
+ a {
+ text-decoration: none;
+ &:hover {
+ text-decoration: underline;
+ color:white;
+ }
+ color:$ColorTextTypical;
+ }
+ }
+ }
+
+ .open-metronome {
+ margin-left:5px;
+ }
+
+ .media-options {
+ padding-bottom:10px;
+ }
+
+ .session-notification {
+ color: white;
+ background-color: #666666;
+ border-radius: 6px;
+ min-height: 36px;
+ width:100%;
+ position:relative;
+ @include border_box_sizing;
+ padding:6px;
+ margin:10px 0;
+
+ &.has-details {
+ cursor:pointer;
+ }
+
+ .msg {
+ font-size:14px;
+ }
+ .detail {
+ font-size:12px;
+ margin-top:5px;
+ }
+ .notify-help {
+ color:#ffcc00;
+ text-decoration: none;
+ font-size:12px;
+ margin-left:5px;
+
+ &:hover {
+ text-decoration:underline !important;
+ }
+ }
+ }
+
+ .close-window {
+ text-align:center;
+ clear:both;
+ }
+}
+
+
+.session-track-list-enter {
+ opacity: 0.01;
+ transition: opacity .5s ease-in;
+
+ &.session-track-list-enter-active {
+ opacity: 1;
+ }
+}
+
+.session-track-list-leave {
+ opacity:1;
+ transition: opacity .5s ease-in;
+
+ &.session-track-list-leave-active {
+ opacity: 0.01;
+ }
+}
diff --git a/web/app/assets/stylesheets/client/react-components/SessionSelfVolumeHover.css.scss b/web/app/assets/stylesheets/client/react-components/SessionSelfVolumeHover.css.scss
new file mode 100644
index 000000000..1d6c5eb35
--- /dev/null
+++ b/web/app/assets/stylesheets/client/react-components/SessionSelfVolumeHover.css.scss
@@ -0,0 +1,2 @@
+@import "client/common";
+
diff --git a/web/app/assets/stylesheets/client/react-components/SessionTrack.css.scss b/web/app/assets/stylesheets/client/react-components/SessionTrack.css.scss
new file mode 100644
index 000000000..3e4bb4f6c
--- /dev/null
+++ b/web/app/assets/stylesheets/client/react-components/SessionTrack.css.scss
@@ -0,0 +1,409 @@
+@import "client/common";
+
+.session-track {
+
+ display:inline-block;
+ margin: 10px 0;
+ color: $ColorTextTypical;
+ background-color: #242323;
+ border-radius: 6px;
+ min-height: 76px;
+ max-width: 210px;
+ position:relative;
+ @include border_box_sizing;
+
+ .name {
+ width: 100%;
+ margin-bottom: 6px;
+ @include labelFont;
+ }
+
+ .track-avatar {
+ float: left;
+ padding: 1px;
+ width: 44px;
+ height: 44px;
+ background-color: #ed3618;
+ -webkit-border-radius: 22px;
+ -moz-border-radius: 22px;
+ border-radius: 22px;
+
+ img {
+ width: 44px;
+ height: 44px;
+ -webkit-border-radius: 22px;
+ -moz-border-radius: 22px;
+ border-radius: 22px;
+ }
+ }
+
+ .track-instrument {
+ float: left;
+ padding: 1px;
+ margin-left: 5px;
+ }
+
+ table.vu {
+ float: left;
+
+ td {
+ border: 3px solid #242323;
+ }
+ }
+
+ .session-track-contents {
+ padding: 6px 6px 6px 10px;
+ }
+ .disabled-track-overlay {
+ width: 0;
+ height: 0;
+ position: absolute;
+ background-color:#555;
+ border-radius: 6px;
+ }
+
+ &.no-mixer, &.no-audio {
+ .disabled-track-overlay {
+ width: 100%;
+ height: 100%;
+ opacity:0.5;
+ }
+ }
+
+
+ .track-controls {
+ margin-top: 2px;
+ margin-left: 10px;
+ float:left;
+ }
+
+ .track-buttons {
+ margin-top:22px;
+ padding:0 0 0 3px;
+ }
+
+ .track-icon-mute {
+ float:left;
+ position:relative;
+ top:0;
+ left:0;
+ opacity: 0.7;
+ &:hover {
+ opacity:1;
+ }
+ }
+
+ .track-icon-pan {
+ float:left;
+ cursor: pointer;
+ width: 20px;
+ height: 20px;
+ background-image:url('/assets/content/icon_pan.png');
+ background-repeat:no-repeat;
+ text-align: center;
+ margin-left:10px;
+ opacity:0.7;
+ //-webkit-transform: rotate(7deg); /* Chrome, Safari, Opera */
+ //transform: rotate(7deg);
+ &:hover {
+ opacity:1;
+ }
+ }
+
+ .track-icon-equalizer {
+ float:left;
+ cursor: pointer;
+ width: 20px;
+ height: 20px;
+ background-image:url('/assets/content/icon_sound.png');
+ background-repeat:no-repeat;
+ text-align: center;
+ margin-left:7px;
+ opacity: 0.7;
+ &:hover {
+ opacity:1;
+ }
+ }
+
+
+ // media overrides
+ &.backing-track, &.recorded-track, &.jam-track, &.metronome, &.recorded-category, &.jam-track-category {
+ width:210px;
+ table.vu {
+ float: right;
+ margin-top: 30px;
+ margin-right: 4px;
+ }
+ .track-controls {
+ float:right;
+ }
+ .track-buttons {
+ float:right;
+ }
+ .track-icon-pan {
+ float:right;
+ margin-right:15px;
+ }
+ .track-icon-mute{
+ float:right;
+ }
+ }
+
+ &.metronome {
+ .track-instrument {
+ float:left;
+ margin-left:0;
+ margin-right: 10px;
+ margin-top: -4px;
+ }
+ .track-controls {
+ margin-left:0;
+ }
+ }
+
+ &.recorded-track, &.jam-track, &.recorded-category, &.jam-track-category {
+ height:56px;
+ min-height:56px;
+ .track-buttons {
+ margin-top:2px;
+ }
+ .track-controls {
+ margin-left:0;
+ }
+ table.vu {
+ margin-top:10px;
+ }
+ .track-instrument {
+ float: left;
+ margin: -4px 10px 0 0;
+ }
+ }
+ &.recorded-category, &.jam-track-category {
+ height:auto !important;
+ }
+
+ &.jam-track-category {
+ .jam-track-header {
+ position:absolute;
+ @include labelFont;
+
+ }
+ .name {
+ width:auto;
+ margin-left:60px;
+ }
+ }
+}
+
+.react-holder {
+ &.SessionTrackVolumeHover, &.SessionSelfVolumeHover {
+ height:343px;
+ width:235px;
+
+ .session-track {
+ float:left;
+ background-color: #242323;
+ border-radius: 4px;
+ display: inline-block;
+ height: 300px;
+ margin-right: 14px;
+ position: relative;
+ width: 70px;
+ margin-top:19px;
+ margin-left:24px;
+ }
+
+ .track-icon-mute {
+ float:none;
+ position: absolute;
+ top: 246px;
+ left: 29px;
+ }
+
+ .track-gain {
+ position:absolute;
+ width:28px;
+ height:209px;
+ top:32px;
+ left:23px;
+ }
+
+ .fader {
+ height:209px;
+ }
+
+ .handle {
+ bottom:0%;
+ display:none;
+ }
+
+ .textual-help {
+ float:left;
+ width:100px;
+ }
+
+ p {
+ font-size:12px !important;
+ padding:0;
+ margin:16px 0 0 !important;
+ line-height:125% !important;
+ &:nth-child(1) {
+ margin-top: 19px !important;
+ }
+ }
+
+ .icheckbox_minimal {
+ position:absolute;
+ top: 271px;
+ left: 12px;
+ }
+
+ input {
+ position:absolute;
+ top: 271px;
+ left: 12px;
+ }
+
+ label {
+ @include labelFont;
+ position:absolute;
+ top:273px;
+ left:34px
+ }
+ }
+
+ #self-volume-hover {
+ h3 {
+ font-size:16px;
+ font-weight:bold;
+ margin-bottom:10px;
+ }
+
+ .monitor-mixer {
+ float:left;
+ width:235px;
+ @include border_box_sizing;
+ padding: 15px 0 15px 0;
+
+ h3 {
+ margin-left:36px;
+ }
+
+ .textual-help {
+ border-right:1px solid $ColorTextTypical;
+ float:right;
+ padding-right:25px !important;
+
+ p:nth-child(1) {
+ margin-top:0 !important;
+ }
+ }
+ }
+
+ .chat-mixer {
+ float:left;
+ width:235px;
+ @include border-box-sizing;
+ padding: 15px 0 15px 0;
+
+ h3 {
+ margin-left:41px;
+ }
+ }
+
+ .mixer-holder {
+
+ padding-bottom:0;
+
+ .session-track {
+ margin-top:0;
+ }
+
+ .textual-help {
+ margin-top:0;
+ padding-right:10px;
+
+ p:nth-child(1) {
+ margin-top:0;
+ }
+ }
+ }
+
+ }
+ &.SessionTrackVolumeHover {
+ .session-track {
+ margin-bottom:0;
+ }
+ }
+
+ &.SessionSelfVolumeHover {
+ width:470px ! important;
+ height:380px ! important;
+ }
+
+ &.SessionTrackPanHover {
+ width:331px;
+ height:197px;
+ padding:15px;
+ @include border_box_sizing;
+
+ .session-pan {
+ .textual-help {
+ float:left;
+ width:100px;
+ }
+ }
+
+ p {
+ font-size:12px !important;
+ padding:0 !important;
+ line-height:125% !important;
+ margin:0 !important;
+ }
+ .track-pan {
+ background-color: #242323;
+ border-radius: 4px;
+ display: inline-block;
+ height: 70px;
+ position: relative;
+ width: 300px;
+ margin-top:15px;
+ }
+ .fader {
+ position:absolute;
+ width:205px;
+ height:24px;
+ top:34px;
+ left:44px;
+ background-image: url('/assets/content/bkg_slider_gain_horiz_24.png');
+ }
+ .handle {
+ display:none;
+
+ img {
+ position:absolute;
+ left:-5px;
+ }
+ }
+ .left-label {
+ @include labelFont;
+ position:absolute;
+ left:13px;
+ top:40px;
+ }
+ .right-label {
+ @include labelFont;
+ position:absolute;
+ right:12px;
+ top:40px;
+ }
+ .floater {
+ width:20px;
+ text-align:center;
+ top:-22px;
+ left:-8px;
+ @include labelFont;
+ position:absolute;
+ }
+ }
+}
diff --git a/web/app/assets/stylesheets/client/session.css.scss b/web/app/assets/stylesheets/client/session.css.scss
index 213a4d726..f1d2cdc8f 100644
--- a/web/app/assets/stylesheets/client/session.css.scss
+++ b/web/app/assets/stylesheets/client/session.css.scss
@@ -1,6 +1,6 @@
@import "client/common";
-[layout-id="session"] {
+[layout-id="session_old"] {
.resync {
margin-left:15px;
diff --git a/web/app/assets/stylesheets/dialogs/inviteMusiciansDialog.css.scss b/web/app/assets/stylesheets/dialogs/inviteMusiciansDialog.css.scss
new file mode 100644
index 000000000..4f0041966
--- /dev/null
+++ b/web/app/assets/stylesheets/dialogs/inviteMusiciansDialog.css.scss
@@ -0,0 +1,18 @@
+#invite-musician-friends-dialog {
+ width:450px;
+
+ p.instructions {
+ margin-bottom:30px;
+ }
+
+ .actions {
+ text-align:center;
+ }
+
+ #btn-cancel-invites {
+
+ }
+ #btn-save-invites {
+
+ }
+}
\ No newline at end of file
diff --git a/web/app/assets/stylesheets/dialogs/recordingSelectorDialog.css.scss b/web/app/assets/stylesheets/dialogs/recordingSelectorDialog.css.scss
index c9994cff9..f37d81972 100644
--- a/web/app/assets/stylesheets/dialogs/recordingSelectorDialog.css.scss
+++ b/web/app/assets/stylesheets/dialogs/recordingSelectorDialog.css.scss
@@ -2,13 +2,37 @@
#recording-selector-dialog {
- min-height:initial;
+ height:600px;
.dialog-inner {
color:white;
+ height:100%;
+ }
+
+ .recordings {
+ max-height:450px;
+ margin-bottom:20px;
+ overflow-y:auto;
}
.action-buttons {
margin-bottom:10px;
}
+
+ .instructions {
+ margin-bottom:20px;
+ }
+
+ .select-recording {
+ position:relative;
+ input {
+ z-index:1;
+ position:absolute;
+ top:7px;
+ }
+ .feed-entry {
+ margin-top: 0;
+ padding-top: 10px;
+ }
+ }
}
\ No newline at end of file
diff --git a/web/app/assets/stylesheets/dialogs/sessionMasterMixDialog.css.scss b/web/app/assets/stylesheets/dialogs/sessionMasterMixDialog.css.scss
new file mode 100644
index 000000000..570e07ac7
--- /dev/null
+++ b/web/app/assets/stylesheets/dialogs/sessionMasterMixDialog.css.scss
@@ -0,0 +1,61 @@
+@import "client/common";
+
+#session-master-mix-dialog {
+ width:1100px;
+ height:466px;
+
+ #master-tracks {
+ position: absolute;
+ bottom: 50px;
+ top: 120px;
+ width: 100%;
+ padding-right: 20px;
+ @include border_box_sizing;
+ }
+ .dialog-inner {
+ padding: 10px 20px;
+ width:100%;
+
+ p.notice {
+ width:800px;
+ line-height:125%;
+ }
+
+ h2 {
+ font-size:24px;
+ }
+ }
+
+ .session-my-tracks, .session-other-tracks, .session-media-tracks, .session-category-controls {
+ @include border_box_sizing;
+ float: left;
+ width: 25%;
+ padding: 15px;
+ height: 100%;
+ margin-bottom: 15px;
+ color:$ColorTextTypical;
+ overflow:hidden;
+ position:relative;
+ }
+
+ .session-tracks-scroller {
+ overflow-x: hidden;
+ overflow-y: auto;
+ width: 100%;
+ text-align:center;
+ padding: 0 15px 0 0;
+ @include border_box_sizing;
+ left: 0;
+ right: 0;
+ bottom:0;
+ position:absolute;
+ top: 40px;
+ }
+
+ .close-button {
+ position:absolute;
+ margin:0 0 0 -30px;
+ bottom:10px;
+ left: 50%;
+ }
+}
\ No newline at end of file
diff --git a/web/app/assets/stylesheets/dialogs/sessionSettingsDialog.css.scss b/web/app/assets/stylesheets/dialogs/sessionSettingsDialog.css.scss
new file mode 100644
index 000000000..13395483d
--- /dev/null
+++ b/web/app/assets/stylesheets/dialogs/sessionSettingsDialog.css.scss
@@ -0,0 +1,67 @@
+@import "client/common";
+
+#session-settings {
+
+ width:500px;
+
+ .dropdown-wrapper {
+ width:100%;
+ }
+
+ input, textarea {
+ width:100%;
+ @include border_box_sizing;
+ }
+
+ .btn-select-files {
+ position:absolute;
+ width: 90px;
+ top: 2px;
+ }
+
+ .notation-selector {
+ position:absolute;
+ right:0
+ }
+
+ .notation-files {
+ position:relative;
+ }
+
+ .inputbox {
+ height:60px;
+ padding:5px;
+ }
+
+ .notation-file {
+
+ }
+
+ .input-holder {
+ width:350px;
+ }
+
+ .notation-entry {
+ div {
+ display:block;
+ width:90%;
+ overflow:hidden;
+ white-space: nowrap;
+ float: left;
+ color:black;
+ }
+ a {
+ float:right;
+ color:black;
+ }
+ }
+
+ #session-settings-dialog-submit {
+ margin-right:1px;
+ }
+
+ .spinner-small {
+ position: absolute;
+ top: 20px;
+ }
+}
\ No newline at end of file
diff --git a/web/app/assets/stylesheets/dialogs/shareDialog.css.scss b/web/app/assets/stylesheets/dialogs/shareDialog.css.scss
index 60bff5e51..541d0489a 100644
--- a/web/app/assets/stylesheets/dialogs/shareDialog.css.scss
+++ b/web/app/assets/stylesheets/dialogs/shareDialog.css.scss
@@ -2,6 +2,10 @@
width:500px;
+ .dialog-inner {
+ padding-bottom:6px;
+ }
+
.button-orange {
margin:0 2px 0 0;
}
@@ -272,6 +276,9 @@
}
.share-link {
+
+ height:75px;
+
h3 {
margin-bottom:20px;
}
@@ -292,4 +299,9 @@
text-align: center;
margin: 125px auto;
}
+
+ .actions {
+ text-align:center;
+ margin-top:20px;
+ }
}
\ No newline at end of file
diff --git a/web/app/assets/stylesheets/landings/individual_jamtrack.css.scss b/web/app/assets/stylesheets/landings/individual_jamtrack.css.scss
index 773f12be1..c663d91e6 100644
--- a/web/app/assets/stylesheets/landings/individual_jamtrack.css.scss
+++ b/web/app/assets/stylesheets/landings/individual_jamtrack.css.scss
@@ -1,5 +1,62 @@
body.web.landing_jamtrack.individual_jamtrack {
+ .landing-content {
+ .header {
+ text-align:center;
+ }
+ p {
+ width:100%;
+ }
+ h1 {
+ font-size:24px;
+ }
+ h2 {
+ font-size:18px;
+ color:white;
+ text-decoration:underline;
+ margin-bottom:8px;
+ }
+ ul {
+ width: 100%;
+ margin:5px 0 0;
+ }
+ li {
+ margin-bottom:4px;
+ }
+ .row .column {
+ padding:0 30px;
+ position:absolute;
+ float:none;
+ }
+ .row:nth-of-type(2) {
+ margin-top:30px;
+ position:relative;
+ }
+
+ .row .column:nth-of-type(1) {
+ width: 50%;
+ left:0;
+ height:100%;
+ }
+ .row .column:nth-of-type(2) {
+ width: 50%;
+ left:50%;
+ position:relative;
+ border-width:0 0 0 1px;
+ border-color:white;
+ border-style:solid;
+ }
+ }
+ .watch-video {
+ position:absolute;
+ bottom:25px;
+ text-align:center;
+ display:block;
+ width: 50%;
+ left: 20%;
+ font-size:16px;
+ }
+
.previews {
margin-top:10px;
}
@@ -23,13 +80,42 @@ body.web.landing_jamtrack.individual_jamtrack {
.jam-track-preview-holder {
margin-bottom: 7px;
-
+ float:left;
&[data-track-type="Master"] {
width: 100%;
}
&[data-track-type="Track"] {
- width: 100%;
+ width: 50%;
+ }
+ }
+
+ .jam-track-preview {
+ font-size:12px;
+ }
+
+ .cta-holder {
+ margin-top:30px;
+ text-align:center;
+ .checkout {
+ display:inline-block;
+ position:relative;
+ }
+ .browse-band {
+ display:inline-block;
+ position:relative;
+ margin-top:20px;
+ }
+ .browse-all {
+ display:inline-block;
+ position:relative;
+ margin-top:20px;
+ }
+ .value-indicator {
+ position:absolute;
+ left: 301px;
+ top: 19px;
+ width:80px;
}
}
}
\ No newline at end of file
diff --git a/web/app/assets/stylesheets/landings/individual_jamtrack_band_v1.css.scss b/web/app/assets/stylesheets/landings/individual_jamtrack_band_v1.css.scss
new file mode 100644
index 000000000..d0290cda1
--- /dev/null
+++ b/web/app/assets/stylesheets/landings/individual_jamtrack_band_v1.css.scss
@@ -0,0 +1,35 @@
+body.web.landing_jamtrack.individual_jamtrack_band_v1 {
+
+ .previews {
+ margin-top:10px;
+ }
+ .jamtrack-reasons {
+ margin: 10px 0 0 20px;
+ }
+
+ .white-bordered-button {
+ margin-top: 20px;
+ }
+
+ .browse-jamtracks-wrapper {
+ text-align:center;
+ width:90%;
+ }
+
+ .prompt {
+ margin-top:10px;
+ }
+
+ .jam-track-preview-holder {
+
+ margin-bottom: 7px;
+
+ &[data-track-type="Master"] {
+ width: 100%;
+ }
+
+ &[data-track-type="Track"] {
+ width: 100%;
+ }
+ }
+}
\ No newline at end of file
diff --git a/web/app/assets/stylesheets/landings/individual_jamtrack_band.css.scss b/web/app/assets/stylesheets/landings/individual_jamtrack_v1.css.scss
similarity index 89%
rename from web/app/assets/stylesheets/landings/individual_jamtrack_band.css.scss
rename to web/app/assets/stylesheets/landings/individual_jamtrack_v1.css.scss
index 56c4fc1ae..5999b06cb 100644
--- a/web/app/assets/stylesheets/landings/individual_jamtrack_band.css.scss
+++ b/web/app/assets/stylesheets/landings/individual_jamtrack_v1.css.scss
@@ -1,4 +1,4 @@
-body.web.landing_jamtrack.individual_jamtrack_band {
+body.web.landing_jamtrack.individual_jamtrack_v1 {
.previews {
margin-top:10px;
diff --git a/web/app/assets/stylesheets/minimal/media_controls.css.scss b/web/app/assets/stylesheets/minimal/media_controls.css.scss
new file mode 100644
index 000000000..2abdda8fc
--- /dev/null
+++ b/web/app/assets/stylesheets/minimal/media_controls.css.scss
@@ -0,0 +1,45 @@
+@import "client/common";
+
+body.media-controls-popup.popup {
+
+ text-align:center;
+
+ background-color: #242323;
+
+ #minimal-container {
+ padding-bottom:0px;
+ }
+
+ .media-controls-popup {
+ padding:15px 15px 3px 15px;
+ }
+
+ .field {
+ margin-top:20px;
+ }
+
+ .icheckbox_minimal {
+ float:left;
+ }
+
+ label {
+ float: left;
+ margin-left: 5px;
+ margin-top:2px;
+ }
+
+ h3 {
+ text-align:left;
+ margin-bottom:5px;
+ }
+
+ .close-link {
+ margin-top:20px;
+ font-size:11px;
+ }
+
+ .display-metronome {
+ font-size:12px;
+ margin-top:35px;
+ }
+}
\ No newline at end of file
diff --git a/web/app/assets/stylesheets/minimal/minimal.css.scss b/web/app/assets/stylesheets/minimal/minimal.css.scss
index f97d05c7f..6ee69d0f5 100644
--- a/web/app/assets/stylesheets/minimal/minimal.css.scss
+++ b/web/app/assets/stylesheets/minimal/minimal.css.scss
@@ -5,5 +5,8 @@
*= require client/screen_common
*= require client/content
*= require client/ftue
-*= require minimal/minimal_main
+*= require icheck/minimal/minimal
+*= require_directory .
+*= require client/metronomePlaybackModeSelect
+*= require_directory ../client/react-components
*/
\ No newline at end of file
diff --git a/web/app/assets/stylesheets/minimal/minimal_main.css.scss b/web/app/assets/stylesheets/minimal/minimal_main.css.scss
index 50feaf50f..78ca52f36 100644
--- a/web/app/assets/stylesheets/minimal/minimal_main.css.scss
+++ b/web/app/assets/stylesheets/minimal/minimal_main.css.scss
@@ -8,9 +8,4 @@ body {
overflow: visible !important;
height:100%;
margin:0 !important;
-}
-
-.wrapper {
- width:1280px;
- margin:0 auto;
}
\ No newline at end of file
diff --git a/web/app/assets/stylesheets/minimal/popup.css.scss b/web/app/assets/stylesheets/minimal/popup.css.scss
new file mode 100644
index 000000000..5db8190c4
--- /dev/null
+++ b/web/app/assets/stylesheets/minimal/popup.css.scss
@@ -0,0 +1,6 @@
+body.popup {
+ width:100%;
+ height:100%;
+ background-color:#404040;
+ overflow: hidden !important;
+}
\ No newline at end of file
diff --git a/web/app/assets/stylesheets/minimal/recording_controls.css.scss b/web/app/assets/stylesheets/minimal/recording_controls.css.scss
new file mode 100644
index 000000000..b2817b8ee
--- /dev/null
+++ b/web/app/assets/stylesheets/minimal/recording_controls.css.scss
@@ -0,0 +1,95 @@
+@import "client/common";
+
+body.recording-start-stop {
+
+ position:relative;
+ color: $ColorTextTypical;
+
+ #minimal-container {
+ padding-bottom:20px;
+ }
+
+ .recording-start-stop {
+ padding-left:44px;
+ }
+
+ .control-holder {
+ width:100%;
+ margin: 1em 0;
+ }
+
+ .helper {
+ display: inline-block;
+ height: 100%;
+ vertical-align: middle;
+ }
+
+ .control {
+ width:231px;
+ height:34px;
+ @include border_box_sizing;
+ margin-top:15px;
+ padding:3px;
+ background-color:#242323;
+ text-align:center;
+ font-size:13px;
+ border-radius:5px;
+ vertical-align:middle;
+ color:#ccc;
+ }
+
+
+ .control img {
+ vertical-align:middle;
+ margin-right:5px;
+ }
+
+ .control span {
+ vertical-align:middle;
+ }
+
+ .iradio_minimal {
+ float:left;
+ margin-right:5px;
+ }
+
+ label {
+ padding-top:2px;
+ }
+
+ .field {
+ height:18px;
+ &:nth-child(1) {
+
+ }
+ &:nth-child(2) {
+ margin-top:9px;
+ }
+ }
+
+ .note-show-hide {
+ font-size:11px;
+ }
+
+ h5 {
+ text-decoration:underline;
+ margin-bottom:5px;
+ }
+
+ .important-note {
+ margin-top:30px;
+ line-height:150%;
+ font-size:12px;
+ width:260px;
+ }
+
+ a.note-show-hide {
+ margin-top:5px;
+ text-decoration:underline;
+ font-size:11px;
+ }
+
+ .currently-recording {
+ background-color: $ColorRecordingBackground;
+ }
+}
\ No newline at end of file
diff --git a/web/app/assets/stylesheets/minimal/youtube_player.css.scss b/web/app/assets/stylesheets/minimal/youtube_player.css.scss
new file mode 100644
index 000000000..1425bbd92
--- /dev/null
+++ b/web/app/assets/stylesheets/minimal/youtube_player.css.scss
@@ -0,0 +1,8 @@
+body.youtube-player {
+ .video-container {
+ position:absolute;
+ width:100%;
+ height:100%;
+ }
+
+}
diff --git a/web/app/assets/stylesheets/web/audioWidgets.css.scss b/web/app/assets/stylesheets/web/audioWidgets.css.scss
index 44a4fbe1a..621a173a3 100644
--- a/web/app/assets/stylesheets/web/audioWidgets.css.scss
+++ b/web/app/assets/stylesheets/web/audioWidgets.css.scss
@@ -145,6 +145,10 @@
overflow:hidden;
margin-top:20px;
+ .select-box {
+ position:absolute;
+ }
+
&:nth-child(1) {
margin-top:0;
}
diff --git a/web/app/assets/stylesheets/web/web.css b/web/app/assets/stylesheets/web/web.css
index 12c614f5c..b5b4707c4 100644
--- a/web/app/assets/stylesheets/web/web.css
+++ b/web/app/assets/stylesheets/web/web.css
@@ -34,4 +34,5 @@
*= require web/affiliate_links
*= require_directory ../landings
*= require icheck/minimal/minimal
+*= require_directory ../client/react-components
*/
\ No newline at end of file
diff --git a/web/app/controllers/api_music_notations_controller.rb b/web/app/controllers/api_music_notations_controller.rb
index 25997b24b..90e64435c 100644
--- a/web/app/controllers/api_music_notations_controller.rb
+++ b/web/app/controllers/api_music_notations_controller.rb
@@ -25,14 +25,28 @@ class ApiMusicNotationsController < ApiController
def download
@music_notation = MusicNotation.find(params[:id])
- unless @music_notation.music_session.nil? || @music_notation.music_session.can_join?(current_user, true)
+ unless @music_notation.music_session.can_join?(current_user, true)
render :text => "Permission denied", status:403
return
end
+
if '_blank'==params[:target]
redirect_to @music_notation.sign_url
else
render :text => @music_notation.sign_url
end
end
+
+ def delete
+ @music_notation = MusicNotation.find(params[:id])
+
+ unless @music_notation.music_session.can_join?(current_user, true)
+ render :text => "Permission denied", status:403
+ return
+ end
+
+ @music_notation.destroy
+
+ render :json => {}, status: 204
+ end
end
diff --git a/web/app/controllers/landings_controller.rb b/web/app/controllers/landings_controller.rb
index ef23ba7d8..70fc1bb71 100644
--- a/web/app/controllers/landings_controller.rb
+++ b/web/app/controllers/landings_controller.rb
@@ -1,5 +1,7 @@
class LandingsController < ApplicationController
+ include LandingsHelper
+
respond_to :html
def watch_bands
@@ -68,20 +70,46 @@ class LandingsController < ApplicationController
def individual_jamtrack
@no_landing_tag = true
- @show_cta_free_jamtrack = true
@jam_track = JamTrack.find_by_plan_code("jamtrack-" + params[:plan_code])
+ band_jam_track_count = @jam_track.band_jam_track_count
+ jam_track_count = JamTrack.count
+ @title = individual_jamtrack_title(false, params[:generic], @jam_track)
+ @description = individual_jamtrack_desc(false, params[:generic], @jam_track)
+ @page_data = {jam_track: @jam_track, all_track_count: jam_track_count, band_track_count: band_jam_track_count, band: false, generic: params[:generic]}
gon.jam_track_plan_code = params[:plan_code] ? "jamtrack-" + params[:plan_code] : nil
gon.generic = params[:generic]
render 'individual_jamtrack', layout: 'web'
end
def individual_jamtrack_band
+ @no_landing_tag = true
+ @jam_track = JamTrack.find_by_plan_code("jamtrack-" + params[:plan_code])
+ band_jam_track_count = @jam_track.band_jam_track_count
+ jam_track_count = JamTrack.count
+ @title = individual_jamtrack_title(true, params[:generic], @jam_track)
+ @description = individual_jamtrack_desc(true, params[:generic], @jam_track)
+ @page_data = {jam_track: @jam_track, all_track_count: jam_track_count, band_track_count: band_jam_track_count, band: true, generic: params[:generic]}
+ gon.jam_track_plan_code = params[:plan_code] ? "jamtrack-" + params[:plan_code] : nil
+ gon.generic = params[:generic]
+ render 'individual_jamtrack', layout: 'web'
+ end
+
+ def individual_jamtrack_v1
+ @no_landing_tag = true
+ @show_cta_free_jamtrack = true
+ @jam_track = JamTrack.find_by_plan_code("jamtrack-" + params[:plan_code])
+ gon.jam_track_plan_code = params[:plan_code] ? "jamtrack-" + params[:plan_code] : nil
+ gon.generic = params[:generic]
+ render 'individual_jamtrack_v1', layout: 'web'
+ end
+
+ def individual_jamtrack_band_v1
@no_landing_tag = true
@show_cta_free_jamtrack = true
@jam_track = JamTrack.find_by_plan_code("jamtrack-" + params[:plan_code])
gon.jam_track_plan_code = params[:plan_code] ? "jamtrack-" + params[:plan_code] : nil
- render 'individual_jamtrack_band', layout: 'web'
+ render 'individual_jamtrack_band_v1', layout: 'web'
end
def product_jamblaster
diff --git a/web/app/controllers/popups_controller.rb b/web/app/controllers/popups_controller.rb
new file mode 100644
index 000000000..21c3693b3
--- /dev/null
+++ b/web/app/controllers/popups_controller.rb
@@ -0,0 +1,17 @@
+class PopupsController < ApplicationController
+
+ respond_to :html
+
+ def recording_controls
+ render :layout => "minimal"
+ end
+
+ def media_controls
+ render :layout => "minimal"
+ end
+
+ def youtube_player
+ @video_id = params[:id]
+ render :layout => "minimal"
+ end
+end
\ No newline at end of file
diff --git a/web/app/helpers/landings_helper.rb b/web/app/helpers/landings_helper.rb
new file mode 100644
index 000000000..1c9e6340d
--- /dev/null
+++ b/web/app/helpers/landings_helper.rb
@@ -0,0 +1,28 @@
+module LandingsHelper
+
+ def individual_jamtrack_title(is_band, is_generic, jam_track)
+
+ return 'Free Backing Track - Multitrack' unless jam_track
+
+ if is_band
+ "#{jam_track.original_artist} - Get a Free Backing Track - Multitrack"
+ elsif is_generic
+ "Backing Tracks - Full Multitracks with Unique Features"
+ else
+ "#{jam_track.name} - Free Backing Track - Multitrack"
+ end
+ end
+
+ def individual_jamtrack_desc(is_band, is_generic, jam_track)
+
+ return "" unless jam_track
+
+ if is_band
+ "Full multitrack recordings by #{jam_track.original_artist} deliver flexible backing tracks for any instrument or vocals"
+ elsif is_generic
+ "Full multitrack recordings plus free app deliver flexible backing tracks for any instrument or vocals with unique features"
+ else
+ "Full multitrack recording of \"#{jam_track.name}\" by #{jam_track.original_artist} delivers flexible backing track for any instrument or vocals."
+ end
+ end
+end
diff --git a/web/app/views/api_music_notations/create.rabl b/web/app/views/api_music_notations/create.rabl
index 1df3bde7d..1e9d0b313 100644
--- a/web/app/views/api_music_notations/create.rabl
+++ b/web/app/views/api_music_notations/create.rabl
@@ -1,3 +1,7 @@
object @music_notations
-attribute :id, :file_name
\ No newline at end of file
+attribute :id, :file_name
+
+node do |music_notation|
+ { file_url: "/api/music_notations/#{music_notation.id}" }
+end
\ No newline at end of file
diff --git a/web/app/views/api_music_sessions/jam_track_open.rabl b/web/app/views/api_music_sessions/jam_track_open.rabl
new file mode 100644
index 000000000..e34b6943d
--- /dev/null
+++ b/web/app/views/api_music_sessions/jam_track_open.rabl
@@ -0,0 +1,3 @@
+object @music_session
+
+extends "api_music_sessions/show"
\ No newline at end of file
diff --git a/web/app/views/api_music_sessions/metronome_close.rabl b/web/app/views/api_music_sessions/metronome_close.rabl
new file mode 100644
index 000000000..e34b6943d
--- /dev/null
+++ b/web/app/views/api_music_sessions/metronome_close.rabl
@@ -0,0 +1,3 @@
+object @music_session
+
+extends "api_music_sessions/show"
\ No newline at end of file
diff --git a/web/app/views/api_music_sessions/metronome_open.rabl b/web/app/views/api_music_sessions/metronome_open.rabl
new file mode 100644
index 000000000..e34b6943d
--- /dev/null
+++ b/web/app/views/api_music_sessions/metronome_open.rabl
@@ -0,0 +1,3 @@
+object @music_session
+
+extends "api_music_sessions/show"
\ No newline at end of file
diff --git a/web/app/views/api_music_sessions/open_jam_track.rabl b/web/app/views/api_music_sessions/open_jam_track.rabl
deleted file mode 100644
index f79061b5b..000000000
--- a/web/app/views/api_music_sessions/open_jam_track.rabl
+++ /dev/null
@@ -1,3 +0,0 @@
-object @music_session
-
-attributes :id
\ No newline at end of file
diff --git a/web/app/views/api_search/index.rabl b/web/app/views/api_search/index.rabl
index ea256bf52..4296eb164 100644
--- a/web/app/views/api_search/index.rabl
+++ b/web/app/views/api_search/index.rabl
@@ -19,7 +19,11 @@ if @search.is_a?(MusicianSearch)
node :filter_json do |foo|
@search.to_json
end
-
+
+ node :summary do |foo|
+ @search.description
+ end
+
child(:results => :musicians) {
attributes :id, :first_name, :last_name, :name, :city, :state, :country, :online, :musician, :photo_url, :biography, :regionname, :score, :full_score
diff --git a/web/app/views/clients/_account_profile_interests.html.erb b/web/app/views/clients/_account_profile_interests.html.erb
index 82aca53b9..1550905a2 100644
--- a/web/app/views/clients/_account_profile_interests.html.erb
+++ b/web/app/views/clients/_account_profile_interests.html.erb
@@ -18,22 +18,17 @@
I would like to join a virtual band [?]
-
-
+
-
@@ -41,7 +36,7 @@
-
+
Play Commitment
infrequent
@@ -57,19 +52,13 @@
I would like to join a traditional band [?]
-
-
+
-
-
+
Play Commitment
infrequent
@@ -90,7 +79,7 @@
-
+
Touring Option
Yes
@@ -104,35 +93,29 @@
I am available to play in paid sessions [?]
-
-
+
-
-
+
-
+
Hourly Rate:
-
+
Daily Rate:
@@ -143,19 +126,13 @@
I am available to play in free sessions [?]
-
-
+
-
-
@@ -172,19 +149,13 @@
I would like to co-write with partner(s) [?]
-
-
+
-
-
+
Purpose
just for fun
diff --git a/web/app/views/clients/_addNewGear.html.erb b/web/app/views/clients/_addNewGear.html.erb
index 462040b57..2740e74ca 100644
--- a/web/app/views/clients/_addNewGear.html.erb
+++ b/web/app/views/clients/_addNewGear.html.erb
@@ -9,7 +9,7 @@
diff --git a/web/app/views/clients/_band_setup.html.slim b/web/app/views/clients/_band_setup.html.slim
index 05682a802..e1b2740f0 100644
--- a/web/app/views/clients/_band_setup.html.slim
+++ b/web/app/views/clients/_band_setup.html.slim
@@ -100,7 +100,7 @@
table.band-form-table
tr
td
- label.strong-label for="new-member"
+ label for="new-member"
| We want to add a new member
a.help help-topic="band-profile-add-new-member" [?]
td.new-member-dependent
@@ -115,10 +115,10 @@
td
.radio-field
input#new-member-yes.iradio-inline.dependent-master type="radio" name="add_new_members" value='yes'
- label for='new-member-yes' Yes
+ label.radio-label for='new-member-yes' Yes
.radio-field
input#new-member-no.iradio-inline.dependent-master type="radio" name="add_new_members" value='no'
- label for='new-member-no' No
+ label.radio-label for='new-member-no' No
td.new-member-dependent
#desired-experience-label None specified
td.new-member-dependent
@@ -133,7 +133,7 @@
option value="no" No
tr
td
- label.strong-label for="paid-gigs"
+ label for="paid-gigs"
| We want to play paid gigs
a.help help-topic="band-profile-play-paid-gigs" [?]
td.paid-gigs-dependent
@@ -144,27 +144,27 @@
td
.radio-field
input#paid-gigs-yes.iradio-inline.dependent-master type="radio" name="paid_gigs" value='yes'
- label for="paid-gigs-yes" Yes
+ label.radio-label for="paid-gigs-yes" Yes
.radio-field
input#paid-gigs-no.iradio-inline.dependent-master type="radio" name="paid_gigs" value='no'
- label for="paid-gigs-no" No
+ label.radio-label for="paid-gigs-no" No
td.paid-gigs-dependent
- input#hourly-rate type="number" name="hourly_rate"
+ input#hourly-rate name="hourly_rate"
td.paid-gigs-dependent
- input#gig-minimum type="number" name="gig_minimum"
+ input#gig-minimum name="gig_minimum"
tr
td
- label.strong-label for="free-gigs"
+ label for="free-gigs"
| We want to play free gigs
a.help help-topic="band-profile-play-free-gigs" [?]
tr
td
.radio-field
input#free-gigs-yes.iradio-inline type="radio" name="free_gigs" value='yes'
- label for="free-gigs-yes" Yes
+ label.radio-label for="free-gigs-yes" Yes
.radio-field
input#free-gigs-no.iradio-inline type="radio" name="free_gigs" value='no'
- label for="free-gigs-no" No
+ label.radio-label for="free-gigs-no" No
#band-setup-step-3.band-step.content-wrapper
h2 set up band: online presence & performance samples
@@ -199,7 +199,7 @@
| Google+
br clear="all"
- .right
+ .actions
a#btn-band-setup-cancel.nav-button.button-grey
| CANCEL Â Â
a#btn-band-setup-back.nav-button.button-grey.hidden
diff --git a/web/app/views/clients/_faders.html.erb b/web/app/views/clients/_faders.html.erb
index fd4a33046..0f3fab350 100644
--- a/web/app/views/clients/_faders.html.erb
+++ b/web/app/views/clients/_faders.html.erb
@@ -2,8 +2,8 @@