diff --git a/web/app/assets/javascripts/backend_alerts.js b/web/app/assets/javascripts/backend_alerts.js index b9c55c332..b3b090c5e 100644 --- a/web/app/assets/javascripts/backend_alerts.js +++ b/web/app/assets/javascripts/backend_alerts.js @@ -37,20 +37,16 @@ } function onGenericEvent(type, text) { - context.setTimeout(function() { - var alert = ALERT_TYPES[type]; - if(alert && alert.title) { - app.notify({ - "title": ALERT_TYPES[type].title, - "text": text, - "icon_url": "/assets/content/icon_alert_big.png" - }); - } - else { - logger.debug("Unhandled Backend Event type %o, data %o", type, text) - } - }, 1); + var alert = ALERT_TYPES[type]; + + if(alert && alert.title) { + context.NotificationActions.backendNotification({msg: alert.title, detail: alert.message}) + } + else { + logger.debug("Unhandled Backend Event type %o, data %o", type, text) + } + } function alertCallback(type, text) { @@ -104,28 +100,36 @@ onStunEvent(); } else if (type === 26) { // DEAD_USER_REMOVE_EVENT - if(context.JK.CurrentSessionModel) - context.JK.CurrentSessionModel.onDeadUserRemove(type, text); + MixerActions.deadUserRemove(text); + //if(context.JK.CurrentSessionModel) + // context.JK.CurrentSessionModel.onDeadUserRemove(type, text); } else if (type === 27) { // WINDOW_CLOSE_BACKGROUND_MODE - if(context.JK.CurrentSessionModel) - context.JK.CurrentSessionModel.onWindowBackgrounded(type, text); + + SessionActions.windowBackgrounded() + + //if(context.JK.CurrentSessionModel) + // context.JK.CurrentSessionModel.onWindowBackgrounded(type, text); } else if(type === ALERT_NAMES.SESSION_LIVEBROADCAST_FAIL) { - if(context.JK.CurrentSessionModel) - context.JK.CurrentSessionModel.onBroadcastFailure(type, text); + SessionActions.broadcastFailure(text) + //if(context.JK.CurrentSessionModel) + // context.JK.CurrentSessionModel.onBroadcastFailure(type, text); } else if(type === ALERT_NAMES.SESSION_LIVEBROADCAST_ACTIVE) { - if(context.JK.CurrentSessionModel) - context.JK.CurrentSessionModel.onBroadcastSuccess(type, text); + SessionActions.broadcastSuccess(text) + //if(context.JK.CurrentSessionModel) + // context.JK.CurrentSessionModel.onBroadcastSuccess(type, text); } else if(type === ALERT_NAMES.SESSION_LIVEBROADCAST_STOPPED) { - if(context.JK.CurrentSessionModel) - context.JK.CurrentSessionModel.onBroadcastStopped(type, text); + SessionActions.broadcastStopped(text) + //if(context.JK.CurrentSessionModel) + //context.JK.CurrentSessionModel.onBroadcastStopped(type, text); } else if(type === ALERT_NAMES.RECORD_PLAYBACK_STATE) { - if(context.JK.CurrentSessionModel) - context.JK.CurrentSessionModel.onPlaybackStateChange(type, text); + //if(context.JK.CurrentSessionModel) + // context.JK.CurrentSessionModel.onPlaybackStateChange(type, text); + context.MediaPlaybackActions.playbackStateChange(text); } else if((!context.JK.CurrentSessionModel || !context.JK.CurrentSessionModel.inSession()) && (ALERT_NAMES.INPUT_IO_RATE == type || ALERT_NAMES.INPUT_IO_JTR == type || ALERT_NAMES.OUTPUT_IO_RATE == type || ALERT_NAMES.OUTPUT_IO_JTR== type)) { diff --git a/web/app/assets/javascripts/clientUpdate.js b/web/app/assets/javascripts/clientUpdate.js index 56f7ce8ee..ca5bdd428 100644 --- a/web/app/assets/javascripts/clientUpdate.js +++ b/web/app/assets/javascripts/clientUpdate.js @@ -216,7 +216,7 @@ updateUri = uri; updateSize = size; - if(context.JK.CurrentSessionModel && context.JK.CurrentSessionModel.inSession()) { + if(context.SessionStore.inSession()) { logger.debug("deferring client update because in session") return; } diff --git a/web/app/assets/javascripts/dialog/localRecordingsDialog.js b/web/app/assets/javascripts/dialog/localRecordingsDialog.js index 10fdae9a1..2402c0023 100644 --- a/web/app/assets/javascripts/dialog/localRecordingsDialog.js +++ b/web/app/assets/javascripts/dialog/localRecordingsDialog.js @@ -120,11 +120,11 @@ openingRecording = true; // tell the server we are about to start a recording - rest.startPlayClaimedRecording({id: context.JK.CurrentSessionModel.id(), claimed_recording_id: claimedRecording.id}) + rest.startPlayClaimedRecording({id: context.SessionStore.id(), claimed_recording_id: claimedRecording.id}) .done(function(response) { // update session info - context.JK.CurrentSessionModel.updateSession(response); + context.SessionActions.updateSession.trigger(response); var recordingId = $(this).attr('data-recording-id'); var openRecordingResult = context.jamClient.OpenRecording(claimedRecording.recording); @@ -138,7 +138,7 @@ "icon_url": "/assets/content/icon_alert_big.png" }); - rest.stopPlayClaimedRecording({id: context.JK.CurrentSessionModel.id(), claimed_recording_id: claimedRecording.id}) + rest.stopPlayClaimedRecording({id: context.SessionStore.id(), claimed_recording_id: claimedRecording.id}) .fail(function(jqXHR) { app.notify({ "title": "Couldn't Stop Recording Playback", diff --git a/web/app/assets/javascripts/dialog/openBackingTrackDialog.js b/web/app/assets/javascripts/dialog/openBackingTrackDialog.js index bd0d136aa..7474c6a8a 100644 --- a/web/app/assets/javascripts/dialog/openBackingTrackDialog.js +++ b/web/app/assets/javascripts/dialog/openBackingTrackDialog.js @@ -85,7 +85,7 @@ var backingTrack = $(this).data('server-model'); // tell the server we are about to open a backing track: - rest.openBackingTrack({id: context.JK.CurrentSessionModel.id(), backing_track_path: backingTrack.name}) + rest.openBackingTrack({id: context.SessionStore.id(), backing_track_path: backingTrack.name}) .done(function(response) { var result = context.jamClient.SessionOpenBackingTrackFile(backingTrack.name, false); @@ -99,7 +99,7 @@ // else { // logger.error("unable to open backing track") // } - context.JK.CurrentSessionModel.refreshCurrentSession(true); + context.SessionActions.syncWithServer() }) .fail(function(jqXHR) { diff --git a/web/app/assets/javascripts/dialog/openJamTrackDialog.js b/web/app/assets/javascripts/dialog/openJamTrackDialog.js index 867314632..15e46d6dc 100644 --- a/web/app/assets/javascripts/dialog/openJamTrackDialog.js +++ b/web/app/assets/javascripts/dialog/openJamTrackDialog.js @@ -86,10 +86,10 @@ var jamTrack = $(this).data('server-model'); // tell the server we are about to open a jamtrack - rest.openJamTrack({id: context.JK.CurrentSessionModel.id(), jam_track_id: jamTrack.id}) + rest.openJamTrack({id: context.SessionStore.id(), jam_track_id: jamTrack.id}) .done(function(response) { $dialog.data('result', {success:true, jamTrack: jamTrack}) - context.JK.CurrentSessionModel.updateSession(response); + context.SessionActions.updateSession.trigger(response); app.layout.closeDialog('open-jam-track-dialog'); }) .fail(function(jqXHR) { diff --git a/web/app/assets/javascripts/dialog/rateSessionDialog.js b/web/app/assets/javascripts/dialog/rateSessionDialog.js index 644585bb5..6f501f645 100644 --- a/web/app/assets/javascripts/dialog/rateSessionDialog.js +++ b/web/app/assets/javascripts/dialog/rateSessionDialog.js @@ -54,6 +54,7 @@ function events() { $('#btn-rate-session-cancel', $scopeSelector).click(function(evt) { closeDialog(); + return false; }); $('#btn-rate-session-up', $scopeSelector).click(function(evt) { if ($(this).hasClass('selected')) { diff --git a/web/app/assets/javascripts/fakeJamClientRecordings.js b/web/app/assets/javascripts/fakeJamClientRecordings.js index 9fee4360f..78ff96867 100644 --- a/web/app/assets/javascripts/fakeJamClientRecordings.js +++ b/web/app/assets/javascripts/fakeJamClientRecordings.js @@ -94,7 +94,7 @@ function onStartRecording(from, payload) { logger.debug("received start recording request from " + from); - if(context.JK.CurrentSessionModel.recordingModel.isRecording()) { + if(context.SessionStore.isRecording()) { // reject the request to start the recording context.JK.JamServer.sendP2PMessage(from, JSON.stringify(p2pMessageFactory.startRecordingAck(payload.recordingId, false, "already-recording", null))); } diff --git a/web/app/assets/javascripts/globals.js b/web/app/assets/javascripts/globals.js index a7f96cfc1..c0e50c728 100644 --- a/web/app/assets/javascripts/globals.js +++ b/web/app/assets/javascripts/globals.js @@ -334,4 +334,14 @@ "MetronomeGroup": 16 }; + context.JK.CategoryGroupIds = { + "AudioInputMusic" : "AudioInputMusic", + "AudioInputChat" : "AudioInputChat", + "UserMusic" : "UserMusic", + "UserChat" : "UserChat", + "UserMedia" : "UserMedia", + "MediaTrack" : "MediaTrack", + "Metronome" : "Metronome" + } + })(window,jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/jquery.metronomePlaybackMode.js b/web/app/assets/javascripts/jquery.metronomePlaybackMode.js index 720cea539..222c4f2e4 100644 --- a/web/app/assets/javascripts/jquery.metronomePlaybackMode.js +++ b/web/app/assets/javascripts/jquery.metronomePlaybackMode.js @@ -24,7 +24,7 @@ $.fn.metronomePlaybackMode = function(options) { - options = options || {mode: 'self'} + options = $.extend(false, {mode: 'self', positions: ['top']}, options); return this.each(function(index) { @@ -78,8 +78,8 @@ spikeLength:0, width:180, closeWhenOthersOpen: true, - offsetParent: $parent.offsetParent(), - positions:['top'], + offsetParent: options.offsetParent || $parent.offsetParent(), + positions: options.positions, preShow: function() { $parent.find('.down-arrow').removeClass('down-arrow').addClass('up-arrow') }, diff --git a/web/app/assets/javascripts/minimal/minimal.js b/web/app/assets/javascripts/minimal/minimal.js index 37c70efb7..0181340c6 100644 --- a/web/app/assets/javascripts/minimal/minimal.js +++ b/web/app/assets/javascripts/minimal/minimal.js @@ -3,9 +3,12 @@ //= require jquery //= require jquery.monkeypatch //= require jquery_ujs +//= require jquery.ui.draggable +//= require jquery.ui.droppable //= require jquery.bt //= require jquery.icheck //= require jquery.easydropdown +//= require jquery.metronomePlaybackMode //= require classnames //= require reflux //= require AAC_underscore @@ -14,6 +17,7 @@ //= require jam_rest //= require ga //= require utils +//= require playbackControls //= require react //= require react_ujs //= require react-init diff --git a/web/app/assets/javascripts/playbackControls.js b/web/app/assets/javascripts/playbackControls.js index 84ec5aed6..84f4cb7ba 100644 --- a/web/app/assets/javascripts/playbackControls.js +++ b/web/app/assets/javascripts/playbackControls.js @@ -1,6 +1,7 @@ /** * Playback widget (play, pause , etc) */ + (function(context, $) { "use strict"; @@ -18,7 +19,7 @@ context.JK = context.JK || {}; context.JK.PlaybackControls = function($parentElement, options){ - options = $.extend(false, {playmodeControlsVisible:false}, options); + options = $.extend(false, {playmodeControlsVisible:false, mediaActions:null}, options); var logger = context.JK.logger; if($parentElement.length == 0) { @@ -68,23 +69,42 @@ if(endReached) { update(0, playbackDurationMs, playbackPlaying); } - $self.triggerHandler('play', {playbackMode: playbackMode, playbackMonitorMode: playbackMonitorMode}); + if(options.mediaActions) { + options.mediaActions.mediaStartPlay({playbackMode: playbackMode, playbackMonitorMode: playbackMonitorMode}) + } + else { + $self.triggerHandler('play', {playbackMode: playbackMode, playbackMonitorMode: playbackMonitorMode}); + + } if(playbackMonitorMode == PLAYBACK_MONITOR_MODE.JAMTRACK) { - var sessionModel = context.JK.CurrentSessionModel || null; - context.JK.GA.trackJamTrackPlaySession(sessionModel.id(), true) + context.JK.GA.trackJamTrackPlaySession(context.SessionStore.id(), true) } } function stopPlay(endReached) { + logger.debug("STOP PLAY CLICKED") updateIsPlaying(false); - $self.triggerHandler('stop', {playbackMode: playbackMode, playbackMonitorMode: playbackMonitorMode, endReached : endReached}); + + if(options.mediaActions) { + logger.debug("mediaStopPlay", endReached) + options.mediaActions.mediaStopPlay({playbackMode: playbackMode, playbackMonitorMode: playbackMonitorMode, endReached : endReached}) + } + else { + $self.triggerHandler('stop', {playbackMode: playbackMode, playbackMonitorMode: playbackMonitorMode, endReached : endReached}); + } } function pausePlay(endReached) { updateIsPlaying(false); - $self.triggerHandler('pause', {playbackMode: playbackMode, playbackMonitorMode: playbackMonitorMode, endReached : endReached}); + + if(options.mediaActions) { + options.mediaActions.mediaPausePlay({playbackMode: playbackMode, playbackMonitorMode: playbackMonitorMode, endReached : endReached}) + } + else { + $self.triggerHandler('pause', {playbackMode: playbackMode, playbackMonitorMode: playbackMonitorMode, endReached : endReached}); + } } function updateOffsetBasedOnPosition(offsetLeft) { @@ -93,8 +113,13 @@ playbackPositionMs = parseInt((offsetLeft / sliderBarWidth) * playbackDurationMs); updateCurrentTimeText(playbackPositionMs); if(canUpdateBackend) { + if(options.mediaActions) { + options.mediaActions.mediaChangePosition({positionMs: playbackPositionMs, playbackMonitorMode: playbackMonitorMode}) + } + else { $self.triggerHandler('change-position', {positionMs: playbackPositionMs, playbackMonitorMode: playbackMonitorMode}); - canUpdateBackend = false; + } + canUpdateBackend = false; } } @@ -127,34 +152,17 @@ } $playButton.on('click', function(e) { - var sessionModel = context.JK.CurrentSessionModel || null; - //if(sessionModel && sessionModel.areControlsLockedForJamTrackRecording() && $parentElement.closest('.session-track').data('track_data').type == 'jam_track') { - // context.JK.prodBubble($fader, 'jamtrack-controls-disabled', {}, {positions:['top'], offsetParent: $playButton}) - // return false; - //} - + console.log("CLICKED PLAY") startPlay(); return false; }); $pauseButton.on('click', function(e) { - var sessionModel = context.JK.CurrentSessionModel || null; - //if(sessionModel && sessionModel.areControlsLockedForJamTrackRecording() && $parentElement.closest('.session-track').data('track_data').type == 'jam_track') { - // context.JK.prodBubble($pauseButton, 'jamtrack-controls-disabled', {}, {positions:['top'], offsetParent: $pauseButton}) - // return false; - //} - pausePlay(); return false; }); $stopButton.on('click', function(e) { - var sessionModel = context.JK.CurrentSessionModel || null; - //if(sessionModel && sessionModel.areControlsLockedForJamTrackRecording() && $parentElement.closest('.session-track').data('track_data').type == 'jam_track') { - // context.JK.prodBubble($pauseButton, 'jamtrack-controls-disabled', {}, {positions:['top'], offsetParent: $pauseButton}) - // return false; - //} - stopPlay(); return false; }); @@ -211,53 +219,61 @@ throw "unknown playbackMonitorMode: " + playbackMonitorMode; } } + + function executeMonitor(positionMs, durationMs, isPlaying) { + + if(positionMs < 0) { + // bug in backend? + positionMs = 0; + } + + if(positionMs > 0) { + seenActivity = true; + } + + if(playbackMonitorMode == PLAYBACK_MONITOR_MODE.METRONOME) { + updateIsPlaying(isPlaying); + } + else { + update(positionMs, durationMs, isPlaying); + } + + if(playbackMonitorMode == PLAYBACK_MONITOR_MODE.JAMTRACK) { + + if(playbackPlaying) { + $jamTrackGetReady.attr('data-current-time', positionMs) + } + else { + // this is so the jamtrack 'Get Ready!' stays hidden when it's not playing + $jamTrackGetReady.attr('data-current-time', -1) + } + } + + monitorPlaybackTimeout = setTimeout(monitorRecordingPlayback, 500); + } + function monitorRecordingPlayback() { if(!monitoring) { return; } - if(playbackMonitorMode == PLAYBACK_MONITOR_MODE.JAMTRACK) { - var positionMs = context.jamClient.SessionCurrrentJamTrackPlayPosMs(); - var duration = context.jamClient.SessionGetJamTracksPlayDurationMs(); - var durationMs = duration.media_len; - var start = duration.start; // needed to understand start offset, and prevent slider from moving in tapins + if(options.mediaActions) { + options.mediaActions.positionUpdate(playbackMonitorMode) } else { - var positionMs = context.jamClient.SessionCurrrentPlayPosMs(); - var durationMs = context.jamClient.SessionGetTracksPlayDurationMs(); - } - - var isPlaying = context.jamClient.isSessionTrackPlaying(); - - if(positionMs < 0) { - // bug in backend? - positionMs = 0; - } - - if(positionMs > 0) { - seenActivity = true; - } - - - if(playbackMonitorMode == PLAYBACK_MONITOR_MODE.METRONOME) { - updateIsPlaying(isPlaying); - } - else { - update(positionMs, durationMs, isPlaying); - } - - if(playbackMonitorMode == PLAYBACK_MONITOR_MODE.JAMTRACK) { - - if(playbackPlaying) { - $jamTrackGetReady.attr('data-current-time', positionMs) + if(playbackMonitorMode == PLAYBACK_MONITOR_MODE.JAMTRACK) { + var positionMs = context.jamClient.SessionCurrrentJamTrackPlayPosMs(); + var duration = context.jamClient.SessionGetJamTracksPlayDurationMs(); + var durationMs = duration.media_len; } else { - // this is so the jamtrack 'Get Ready!' stays hidden when it's not playing - $jamTrackGetReady.attr('data-current-time', -1) + var positionMs = context.jamClient.SessionCurrrentPlayPosMs(); + var durationMs = context.jamClient.SessionGetTracksPlayDurationMs(); } - } + var isPlaying = context.jamClient.isSessionTrackPlaying(); - monitorPlaybackTimeout = setTimeout(monitorRecordingPlayback, 500); + executeMonitor(positionMs, durationMs, isPlaying) + } } function update(currentTimeMs, durationTimeMs, isPlaying, offsetStart) { @@ -304,7 +320,11 @@ } function updateCurrentTimeText(timeMs) { - $currentTime.text(context.JK.prettyPrintSeconds(parseInt(timeMs / 1000))); + var time = context.JK.prettyPrintSeconds(parseInt(timeMs / 1000)) + $currentTime.text(time); + if(options.mediaActions) { + options.mediaActions.currentTimeChanged(time) + } } function updateSliderPosition(timeMs) { @@ -362,6 +382,12 @@ } function startMonitor(_playbackMonitorMode) { + logger.debug("startMonitor: " + _playbackMonitorMode) + + if(monitoring && _playbackMonitorMode == playbackMonitorMode) { + return; + } + monitoring = true; // resets everything to zero init(); @@ -376,6 +402,11 @@ logger.debug("playbackControl.startMonitor " + playbackMonitorMode + "") styleControls(); + + if(monitorPlaybackTimeout != null) { + clearTimeout(monitorPlaybackTimeout); + monitorPlaybackTimeout = null; + } monitorRecordingPlayback(); } @@ -407,6 +438,7 @@ this.setPlaybackMode = setPlaybackMode; this.startMonitor = startMonitor; this.stopMonitor = stopMonitor; + this.executeMonitor = executeMonitor; this.onPlayStopEvent = onPlayStopEvent; this.onPlayStartEvent = onPlayStartEvent; this.onPlayPauseEvent = onPlayPauseEvent; diff --git a/web/app/assets/javascripts/react-components.js b/web/app/assets/javascripts/react-components.js index 793ddb737..880aa84c4 100644 --- a/web/app/assets/javascripts/react-components.js +++ b/web/app/assets/javascripts/react-components.js @@ -4,7 +4,10 @@ //= require ./react-components/stores/RecordingStore //= require ./react-components/stores/SessionStore //= require ./react-components/stores/MixerStore +//= require ./react-components/stores/SessionNotificationStore +//= require ./react-components/stores/MediaPlaybackStore //= require ./react-components/stores/SessionMyTracksStore //= require ./react-components/stores/SessionOtherTracksStore +//= require ./react-components/stores/SessionMediaTracksStore //= require_directory ./react-components/stores //= require_directory ./react-components \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/MediaControls.js.jsx.coffee b/web/app/assets/javascripts/react-components/MediaControls.js.jsx.coffee new file mode 100644 index 000000000..024f11103 --- /dev/null +++ b/web/app/assets/javascripts/react-components/MediaControls.js.jsx.coffee @@ -0,0 +1,204 @@ +context = window +PLAYBACK_MONITOR_MODE = context.JK.PLAYBACK_MONITOR_MODE +EVENTS = context.JK.EVENTS +logger = context.JK.logger + +mixins = [] + +# this check ensures we attempt to listen if this component is created in a popup +reactContext = if window.opener? then window.opener else window + +MixerStore = reactContext.MixerStore +MixerActions = reactContext.MixerActions +MediaPlaybackStore = reactContext.MediaPlaybackStore +SessionActions = reactContext.SessionActions +MediaPlaybackActions = reactContext.MediaPlaybackActions + +mixins.push(Reflux.listenTo(MixerStore,"onInputsChanged")) +mixins.push(Reflux.listenTo(MediaPlaybackStore, 'onMediaStateChanged')) + + +@MediaControls = React.createClass({ + + mixins: mixins + tempos : [ 40, 42, 44, 46, 48, 50, 52, 54, 56, 58, 60, 63, 66, 69, 72, 76, 80, 84, 88, 92, 96, 100, 104, 108, 112, 116, 120, 126, 132, 138, 144, 152, 160, 168, 176, 184, 192, 200, 208 ] + + onMediaStateChanged: (changes) -> + if changes.playbackStateChanged + if @state.controls? + if changes.playbackState == 'play_start' + @state.controls.onPlayStartEvent() + else if changes.playbackState == 'play_stop' + @state.controls.onPlayStopEvent() + else if changes.playbackState == 'play_pause' + @state.controls.onPlayPauseEvent(); + else if changes.positionUpdateChanged + if @state.controls? + @state.controls.executeMonitor(changes.positionMs, changes.durationMs, changes.isPlaying) + + onInputsChanged: (sessionMixers) -> + + session = sessionMixers.session + mixers = sessionMixers.mixers + + if @state.controls? + mediaSummary = mixers.mediaSummary + metro = mixers.metro + + @monitorControls(@state.controls, mediaSummary) + @setState({mediaSummary: mediaSummary, metro: metro}) + + @updateMetronomeDetails(metro, @state.initializedMetronomeControls) + + updateMetronomeDetails: (metro, initializedMetronomeControls) -> + logger.debug("MediaControls: setting tempo/sound/cricket", metro) + $root = jQuery(this.getDOMNode()) + $root.find("select.metro-tempo").val(metro.tempo) + $root.find("select.metro-sound").val(metro.sound) + + if initializedMetronomeControls + mode = if metro.cricket then 'cricket' else 'self' + logger.debug("settingcricket", mode) + $root.find('#metronome-playback-select').metronomeSetPlaybackMode(mode) + + monitorControls: (controls, mediaSummary) -> + + if mediaSummary.mediaOpen + if mediaSummary.jamTrackOpen + controls.startMonitor(PLAYBACK_MONITOR_MODE.JAMTRACK) + else if mediaSummary.backingTrackOpen + controls.startMonitor(PLAYBACK_MONITOR_MODE.MEDIA_FILE) + else if mediaSummary.metronomeOpen + controls.startMonitor(PLAYBACK_MONITOR_MODE.METRONOME) + else if mediaSummary.recordingOpen + controls.startMonitor(PLAYBACK_MONITOR_MODE.MEDIA_FILE) + else + logger.debug("unable to determine mediaOpen type", mediaSummary) + controls.startMonitor(PLAYBACK_MONITOR_MODE.MEDIA_FILE) + else + controls.stopMonitor() + + metronomePlaybackModeChanged: (e, data) -> + + mode = data.playbackMode # will be either 'self' or 'cricket' + + logger.debug("setting metronome playback mode: ", mode) + isCricket = mode == 'cricket'; + SessionActions.metronomeCricketChange(isCricket) + + + onMetronomeChanged: () -> + + @setMetronomeFromForm() + + setMetronomeFromForm: () -> + $root = jQuery(this.getDOMNode()) + tempo = $root.find("select.metro-tempo:visible option:selected").val() + sound = $root.find("select.metro-sound:visible option:selected").val() + + t = parseInt(tempo) + s = null + if tempo == NaN || tempo == 0 || tempo == null + t = 120 + + if sound == null || typeof(sound)=='undefined' || sound == "" + s = "Beep" + else + s = sound + + logger.debug("Setting tempo and sound:", t, s) + MixerActions.metronomeChanged(t, s, 1, 0) + + render: () -> + + + tempo_options = [] + for tempo in @tempos + tempo_options.push(``) + + `
+ +
+
+ Get Ready! +
+ +
+ + + + + + + +
+ +
+ +
+ +
+
+
+ + +
+
+ + +
+
+
+ +
0:00
+
+
+
+
0:00
+ +
0:00
+ +
+ + +
+
` + + + getInitialState: () -> + {controls: null, mediaSummary: {}, initializedMetronomeControls: false} + + tryPrepareMetronome: (metro) -> + if @state.mediaSummary.metronomeOpen && !@state.initializedMetronomeControls + $root = jQuery(this.getDOMNode()) + $root.on("change", ".metronome-select", @onMetronomeChanged) + $root.find('#metronome-playback-select').metronomePlaybackMode({positions:['bottom'], offsetParent:$('#minimal-container')}).on(EVENTS.METRONOME_PLAYBACK_MODE_SELECTED, @metronomePlaybackModeChanged) + @updateMetronomeDetails(metro, true) + @setState({initializedMetronomeControls: true}) + + + componentDidUpdate: (prevProps, prevState) -> + @tryPrepareMetronome(@state.metro) + + componentDidMount: () -> + + + $root = jQuery(this.getDOMNode()) + controls = context.JK.PlaybackControls($root, {mediaActions: MediaPlaybackActions}) + + mediaSummary = MixerStore.mixers.mediaSummary + metro = MixerStore.mixers.metro + + @monitorControls(controls, mediaSummary) + + @tryPrepareMetronome(metro) + + @setState({mediaSummary: mediaSummary, controls: controls, metro: metro}) +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/PopupMediaControls.js.jsx.coffee b/web/app/assets/javascripts/react-components/PopupMediaControls.js.jsx.coffee new file mode 100644 index 000000000..06e7d6535 --- /dev/null +++ b/web/app/assets/javascripts/react-components/PopupMediaControls.js.jsx.coffee @@ -0,0 +1,124 @@ +context = window +logger = context.JK.logger + +mixins = [] + +if window.opener? + SessionActions = window.opener.SessionActions + MediaPlaybackStore = window.opener.MediaPlaybackStore + MixerActions = window.opener.MixerActions + +mixins.push(Reflux.listenTo(MediaPlaybackStore, 'onMediaStateChanged')) + +@PopupMediaControls = React.createClass({ + + mixins: mixins + + onMediaStateChanged: (changes) -> + if changes.currentTimeChanged && @root? + @setState({time: changes.time}) + + showMetronome: (e) -> + e.preventDefault() + + SessionActions.showNativeMetronomeGui() + + getInitialState: () -> + {time: '0:00'} + + close: () -> + window.close() + + render: () -> + + closeLinkText = null + header = null + extraControls = null + + # give the users options to close it + if @props.mediaSummary.jamTrackOpen + mediaType = "JamTrack" + mediaName = @props.jamTracks[0].name + closeLinkText = 'close JamTrack' + header = `

{mediaType}: {mediaName} ({this.state.time})

` + else if @props.mediaSummary.backingTrackOpen + mediaType = "Audio File" + mediaName = context.JK.getNameOfFile(@props.backingTracks[0].shortFilename) + closeLinkText = 'close audio file' + header = `

{mediaType}: {mediaName} ({this.state.time})

` + extraControls = + `
+
+ +
+
+
` + else if @props.mediaSummary.metronomeOpen + mediaType = "Metronome" + closeLinkText = 'close metronome' + header = `

Metronome

` + extraControls = + `
+ Display visual metronome +
` + else if @props.mediaSummary.recordingOpen + mediaType = "Recording" + mediaName = @props.recordedTracks[0].recordingName + closeLinkText = 'close recording' + header = `

{mediaType}: {mediaName} ({this.state.time})

` + else + mediaType = "" + + `
+ {header} + + {extraControls} + {closeLinkText} +
` + + windowUnloaded: () -> + SessionActions.closeMedia() unless window.DontAutoCloseMedia + + componentDidMount: () -> + + $(window).unload(@windowUnloaded ) + + @root = jQuery(this.getDOMNode()) + + $loop = @root.find('input[name="loop"]') + context.JK.checkbox($loop) + + $loop.on('ifChecked', () => + console.log("@props", @props) + MixerActions.loopChanged(@props.backingTracks[0].mixers.mixer, true) + ) + $loop.on('ifUnchecked', () => + MixerActions.loopChanged(@props.backingTracks[0].mixers.mixer, false) + ) + + @resizeWindow() + + # this is necessary due to whatever the client's rendering behavior is. + setTimeout(@resizeWindow, 300) + + componentDidUpdate: () -> + @resizeWindow() + + resizeWindow: () => + $container = $('#minimal-container') + width = $container.width() + height = $container.height() + + # there is 20px or so of unused space at the top of the page. can't figure out why it's there. (above #minimal-container) + #mysteryTopMargin = 20 + mysteryTopMargin = 0 + # deal with chrome in real browsers + offset = (window.outerHeight - window.innerHeight) + mysteryTopMargin + + # handle native client chrome that the above outer-inner doesn't catch + #if navigator.userAgent.indexOf('JamKazam') > -1 + + #offset += 25 + + window.resizeTo(width, height + offset) +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/PopupRecordingStartStop.js.jsx.coffee b/web/app/assets/javascripts/react-components/PopupRecordingStartStop.js.jsx.coffee index a4d3d6c76..5eaa6a050 100644 --- a/web/app/assets/javascripts/react-components/PopupRecordingStartStop.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/PopupRecordingStartStop.js.jsx.coffee @@ -8,7 +8,6 @@ if window.opener @PopupRecordingStartStop = React.createClass({ - # this comes from the parent window mixins: mixins onRecordingStateChanged: (recordingState) -> diff --git a/web/app/assets/javascripts/react-components/PopupWrapper.js.jsx.coffee b/web/app/assets/javascripts/react-components/PopupWrapper.js.jsx.coffee new file mode 100644 index 000000000..d75cb5b8e --- /dev/null +++ b/web/app/assets/javascripts/react-components/PopupWrapper.js.jsx.coffee @@ -0,0 +1,13 @@ +context = window +logger = context.JK.logger + +@PopupWrapper = React.createClass({ + + getInitialState: () -> + {ready: false} + + render: () -> + logger.debug("PopupProps", window.PopupProps) + return React.createElement(window[this.props.component], window.PopupProps) + +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/SessionBackingTrack.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionBackingTrack.js.jsx.coffee new file mode 100644 index 000000000..536f13e20 --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionBackingTrack.js.jsx.coffee @@ -0,0 +1,80 @@ +context = window + +MixerActions = @MixerActions + +@SessionBackingTrack = React.createClass({ + + handleMute: (e) -> + e.preventDefault() + + muting = $(e.currentTarget).is('.enabled') + + MixerActions.mute([this.props.mixers.mixer], muting) + + render: () -> + + # today, all mixers are the same for a remote participant; so just grab the 1st + mixers = @props.mixers + + muteMixer = mixers.muteMixer + vuMixer = mixers.vuMixer + muteMixerId = muteMixer?.id + + classes = classNames({ + 'track-icon-mute': true + 'enabled' : !muteMixer?.mute + 'muted' : muteMixer?.mute + }) + + componentClasses = classNames({ + "session-track" : true + "backing-track" : true + }) + + pan = mixers.mixer.pan + + panStyle = { + transform: "rotate(#{pan}deg)" + WebkitTransform: "rotate(#{pan}deg)" + } + + `
+
+
{this.props.shortFilename}
+
+ +
+
+
+
+
+
+ +
+
+
` + + componentDidMount: () -> + + + $root = $(this.getDOMNode()) + $mute = $root.find('.track-icon-mute') + $pan = $root.find('.track-icon-pan') + + context.JK.interactReactBubble( + $mute, + 'SessionTrackVolumeHover', + () => + {mixers:@props.mixers} + , + {width:235, positions:['right', 'left'], offsetParent:$root.closest('.screen')}) + + context.JK.interactReactBubble( + $pan, + 'SessionTrackPanHover', + () => + {mixers:@props.mixers} + , + {width:331, positions:['right', 'left'], offsetParent:$root.closest('.screen')}) + +}) diff --git a/web/app/assets/javascripts/react-components/SessionJamTrack.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionJamTrack.js.jsx.coffee new file mode 100644 index 000000000..740b3edb4 --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionJamTrack.js.jsx.coffee @@ -0,0 +1,81 @@ +context = window + +MixerActions = @MixerActions + +@SessionJamTrack = React.createClass({ + + handleMute: (e) -> + e.preventDefault() + + muting = $(e.currentTarget).is('.enabled') + + MixerActions.mute([this.props.mixers.mixer], muting) + + render: () -> + + # today, all mixers are the same for a remote participant; so just grab the 1st + mixers = @props.mixers + + muteMixer = mixers.muteMixer + vuMixer = mixers.vuMixer + muteMixerId = muteMixer?.id + + classes = classNames({ + 'track-icon-mute': true + 'enabled' : !muteMixer?.mute + 'muted' : muteMixer?.mute + }) + + componentClasses = classNames({ + "session-track" : true + "jam-track" : true + }) + + pan = mixers.mixer.pan + + panStyle = { + transform: "rotate(#{pan}deg)" + WebkitTransform: "rotate(#{pan}deg)" + } + + `
+
+
{this.props.part}
+
+ +
+
+
+
+
+
+
+ +
+
+
` + + componentDidMount: () -> + + + $root = $(this.getDOMNode()) + $mute = $root.find('.track-icon-mute') + $pan = $root.find('.track-icon-pan') + + context.JK.interactReactBubble( + $mute, + 'SessionTrackVolumeHover', + () => + {mixers:@props.mixers} + , + {width:235, positions:['right', 'left'], offsetParent:$root.closest('.screen')}) + + context.JK.interactReactBubble( + $pan, + 'SessionTrackPanHover', + () => + {mixers:@props.mixers} + , + {width:331, positions:['right', 'left'], offsetParent:$root.closest('.screen')}) + +}) diff --git a/web/app/assets/javascripts/react-components/SessionMediaTracks.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionMediaTracks.js.jsx.coffee index 095975828..8199ed192 100644 --- a/web/app/assets/javascripts/react-components/SessionMediaTracks.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/SessionMediaTracks.js.jsx.coffee @@ -1,13 +1,316 @@ context = window +rest = context.JK.Rest() +SessionActions = @SessionActions +ReactCSSTransitionGroup = React.addons.CSSTransitionGroup +MIX_MODES = context.JK.MIX_MODES +EVENTS = context.JK.EVENTS +ChannelGroupIds = context.JK.ChannelGroupIds @SessionMediaTracks = React.createClass({ + mixins: [Reflux.listenTo(@SessionMediaTracksStore,"onInputsChanged"), Reflux.listenTo(@AppStore,"onAppInit")] + + + + onInputsChanged: (sessionMixers) -> + + session = sessionMixers.session + mixers = sessionMixers.mixers + + backingTracks = mixers.backingTracks + jamTracks = mixers.jamTracks + recordedTracks = mixers.recordedTracks + metronome = mixers.metronome + + state = + isRecording: session.isRecording + mediaSummary: mixers.mediaSummary + backingTracks: backingTracks + jamTracks: jamTracks + recordedTracks: recordedTracks + metronome: metronome + + if state.mediaSummary.mediaOpen + if !@state.childWindow? + childWindow = window.open("/popups/media-controls", 'Media Controls', 'scrollbars=yes,toolbar=no,status=no,height=155,width=350') + childWindow.PopupProps = state + state.childWindow = childWindow + else + if @state.childWindow? + @state.childWindow.DontAutoCloseMedia = true + @state.childWindow.close() + state.childWindow = null + + @setState(state) + + + closeAudio: (e) -> + e.preventDefault() + + SessionActions.closeMedia() + + cancelDownloadJamTrack: (e) -> + e.preventDefault() + + logger.debug("closing DownloadJamTrack widget") + @state.downloadJamTrack.root.remove() + @state.downloadJamTrack.destroy() + + SessionActions.downloadingJamTrack(false) + + @setState({downloadJamTrack: null}) + + openRecording: (e) -> + e.preventDefault() + + # just ignore the click if they are currently recording for now + if @state.isRecording + @app.notify({ + "title": "Currently Recording", + "text": "You can't open a recording while creating a recording.", + "icon_url": "/assets/content/icon_alert_big.png" + }) + return + + @app.layout.showDialog('localRecordings') unless @app.layout.isDialogShowing('localRecordings') + + openBackingTrack: (e) -> + e.preventDefault() + if @state.backingTrackDialogOpen + logger.debug("backing track dialog already open") + return + + + # just ignore the click if they are currently recording for now + if @state.isRecording + @app.notify({ + "title": "Currently Recording", + "text": "You can't open a backing track while creating a recording.", + "icon_url": "/assets/content/icon_alert_big.png" + }); + return + + @setState({backingTrackDialogOpen: true}) + context.jamClient.ShowSelectBackingTrackDialog("window.JK.HandleBackingTrackSelectedCallback2"); + + openMetronome: (e) -> + + if @state.isRecording + @app.notify({ + "title": "Currently Recording", + "text": "You can't open a metronome while creating a recording.", + "icon_url": "/assets/content/icon_alert_big.png" + }) + return + + SessionActions.openMetronome() + + openJamTrack: (e) -> + e.preventDefault() + + if @state.isRecording + @app.notify({ + "title": "Currently Recording", + "text": "You can't open a jam track while creating a recording.", + "icon_url": "/assets/content/icon_alert_big.png" + }) + return + + @app.layout.showDialog('open-jam-track-dialog').one(EVENTS.DIALOG_CLOSED, (e, data) => + # once the dialog is closed, see if the user has a jamtrack selected + if !data.canceled && data.result.jamTrack + @loadJamTrack(data.result.jamTrack) + + else + logger.debug("OpenJamTrack dialog closed with no selection; ignoring", data) + ) + + loadJamTrack: (jamTrack) -> + if @state.downloadJamTrack + # if there was one showing before somehow, destroy it. + logger.warn("destroying existing JamTrack") + @state.downloadJamTrack.root.remove() + @state.downloadJamTrack.destroy() + #set to null + + + downloadJamTrack = new context.JK.DownloadJamTrack(@app, jamTrack, 'large'); + + # the widget indicates when it gets to any transition; we can hide it once it reaches completion + $(downloadJamTrack).on(EVENTS.JAMTRACK_DOWNLOADER_STATE_CHANGED, (e, data) => + if data.state == downloadJamTrack.states.synchronized + logger.debug("jamtrack synchronized; hide widget and show tracks") + downloadJamTrack.root.remove() + downloadJamTrack.destroy() + downloadJamTrack = null + + this.setState({downloadJamTrack: null}) + + # XXX: test with this removed; it should be unnecessary + context.jamClient.JamTrackStopPlay(); + + sampleRate = context.jamClient.GetSampleRate() + sampleRateForFilename = if sampleRate == 48 then '48' else '44' + fqId = jamTrack.id + '-' + sampleRateForFilename + + if jamTrack.jmep + logger.debug("setting jmep data") + + context.jamClient.JamTrackLoadJmep(fqId, jamTrack.jmep) + else + logger.debug("no jmep data for jamtrack") + + # JamTrackPlay means 'load' + result = context.jamClient.JamTrackPlay(fqId); + + SessionActions.downloadingJamTrack(false) + + console.log("JamTrackPlay: result", ) + if !result + @app.notify( + { + title: "JamTrack Can Not Open", + text: "Unable to open your JamTrack. Please contact support@jamkazam.com" + } + , null, true) + else + participantCnt = context.SessionStore.participants().length + rest.playJamTrack(jamTrack.id) + .done(() => + @app.refreshUser(); + ) + + context.stats.write('web.jamtrack.open', { + value: 1, + session_size: participantCnt, + user_id: context.JK.currentUserId, + user_name: context.JK.currentUserName + }) + ) + + + @setState({downloadJamTrack: downloadJamTrack}) + render: () -> + scrollerClassData = {'session-tracks-scroller': true} + mediaOptions = `
+
+
+ + Open: +
+ + +
` + + contents = null + mediaTracks = [] + + if this.state.downloadJamTrack? + closeOptions = + `` + + contents = closeOptions + + else if this.state.mediaSummary.mediaOpen + + # give the users options to close it + if this.state.mediaSummary.jamTrackOpen + mediaType = "JamTrack" + else if this.state.mediaSummary.backingTrackOpen + mediaType = "Audio File" + else if this.state.mediaSummary.metronomeOpen + mediaType = "Metronome" + else if this.state.mediaSummary.recordingOpen + mediaType = "Recording" + else + mediaType = "" + + closeOptions = ` + + Close {mediaType} + ` + + + for backingTrack in @state.backingTracks + mediaTracks.push(``) + + for jamTrack in @state.jamTracks + mediaTracks.push(``) + + for recordedTrack in @state.recordedTracks + mediaTracks.push(``) + + if @state.metronome + mediaTracks.push(``) + + contents = closeOptions + else + + scrollerClassData['media-options-showing'] = true + contents = mediaOptions + + scrollerClasses = classNames(scrollerClassData) + `

recorded audio

-
- + {contents} +
+ + {mediaTracks} +
` + + + getInitialState:() -> + {mediaSummary:{mediaOpen: false}, isRecording: false, backingTracks: [], jamTracks: [], recordedTracks: [], metronome: null} + + onAppInit: (app) -> + @app = app + + handleBackingTrackSelectedCallback: (result) -> + + @setState({backingTrackDialogOpen: false}) + + SessionActions.openBackingTrack(result) + + componentDidMount: () -> + context.JK.HandleBackingTrackSelectedCallback2 = @handleBackingTrackSelectedCallback + + componentDidUpdate: () -> + + if @state.downloadJamTrack? + $holder = $(@getDOMNode()).find('.download-jamtrack-holder') + + if $holder.find('.download-jamtrack').length == 0 + + SessionActions.downloadingJamTrack(true) + $holder.append(@state.downloadJamTrack.root) + + # kick off the download JamTrack process + @state.downloadJamTrack.init() + + + }) diff --git a/web/app/assets/javascripts/react-components/SessionMetronome.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionMetronome.js.jsx.coffee new file mode 100644 index 000000000..18fae8b0f --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionMetronome.js.jsx.coffee @@ -0,0 +1,81 @@ +context = window + +MixerActions = @MixerActions + +@SessionMetronome = React.createClass({ + + handleMute: (e) -> + e.preventDefault() + + muting = $(e.currentTarget).is('.enabled') + + MixerActions.mute([this.props.mixers.mixer], muting) + + render: () -> + + # today, all mixers are the same for a remote participant; so just grab the 1st + mixers = @props.mixers + + muteMixer = mixers.muteMixer + vuMixer = mixers.vuMixer + muteMixerId = muteMixer?.id + + classes = classNames({ + 'track-icon-mute': true + 'enabled' : !muteMixer?.mute + 'muted' : muteMixer?.mute + }) + + componentClasses = classNames({ + "session-track" : true + "metronome" : true + }) + + pan = mixers.mixer.pan + + panStyle = { + transform: "rotate(#{pan}deg)" + WebkitTransform: "rotate(#{pan}deg)" + } + + `
+
+
Metronome
+
+ +
+
+
+
+
+
+
+ +
+
+
` + + componentDidMount: () -> + + + $root = $(this.getDOMNode()) + $mute = $root.find('.track-icon-mute') + $pan = $root.find('.track-icon-pan') + + context.JK.interactReactBubble( + $mute, + 'SessionTrackVolumeHover', + () => + {mixers:@props.mixers} + , + {width:235, positions:['right', 'left'], offsetParent:$root.closest('.screen')}) + + context.JK.interactReactBubble( + $pan, + 'SessionTrackPanHover', + () => + {mixers:@props.mixers} + , + {width:331, positions:['right', 'left'], offsetParent:$root.closest('.screen')}) + +}) diff --git a/web/app/assets/javascripts/react-components/SessionMyTracks.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionMyTracks.js.jsx.coffee index 7f3d1f189..1d6b8ea26 100644 --- a/web/app/assets/javascripts/react-components/SessionMyTracks.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/SessionMyTracks.js.jsx.coffee @@ -15,21 +15,24 @@ ReactCSSTransitionGroup = React.addons.CSSTransitionGroup; if session.inSession() participant = session.getParticipant(@app.clientId) - name = participant.user.name; + if participant + 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) + 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) - # todo: sessionModel.setAudioEstablished + # todo: sessionModel.setAudioEstablished - instrumentIcon = context.JK.getInstrumentIcon45(track.instrument_id); - photoUrl = context.JK.resolveAvatarUrl(participant.user.photo_url); + instrumentIcon = context.JK.getInstrumentIcon45(track.instrument_id); + photoUrl = context.JK.resolveAvatarUrl(participant.user.photo_url); - tracks.push({track: track, mixerFinder: mixerFinder, mixers: mixerData, name: name, instrumentIcon: instrumentIcon, photoUrl: photoUrl, clientId: participant.client_id}) + tracks.push({track: track, mixerFinder: mixerFinder, mixers: mixerData, name: name, instrumentIcon: instrumentIcon, photoUrl: photoUrl, clientId: participant.client_id}) - # TODO: also deal with chat + # TODO: also deal with chat + else + logger.debug("SessionMyTracks: unable to find participant") this.setState(tracks: tracks, session:session) diff --git a/web/app/assets/javascripts/react-components/SessionNotification.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionNotification.js.jsx.coffee new file mode 100644 index 000000000..d537441ce --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionNotification.js.jsx.coffee @@ -0,0 +1,16 @@ +context = window + +@SessionNotification = React.createClass({ + + render: () -> + `
+
{this.props.msg}
+
` + + componentDidMount: () -> + + $root = $(@getDOMNode()) + + if @props.detail + context.JK.hoverBubble($root, @props.detail) +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/SessionNotifications.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionNotifications.js.jsx.coffee index 819d8f962..96941368b 100644 --- a/web/app/assets/javascripts/react-components/SessionNotifications.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/SessionNotifications.js.jsx.coffee @@ -1,13 +1,41 @@ context = window +ReactCSSTransitionGroup = React.addons.CSSTransitionGroup +NotificationActions = @NotificationActions @SessionNotifications = React.createClass({ + mixins: [Reflux.listenTo(@SessionNotificationStore,"onNotificationsChanged"), Reflux.listenTo(@AppStore,"onAppInit")] + + onNotificationsChanged: (notifications) -> + @setState({notifications: notifications}) + + getInitialState: () -> + {notifications: []} + + clearNotifications: (e) -> + e.preventDefault() + + NotificationActions.clear() + render: () -> + notifications = [] + for notification in @state.notifications + notifications.push(``) + `

notifications

+ + + Clear Notifications +
- + + {notifications} +
` + + onAppInit: (app) -> + @app = app }) diff --git a/web/app/assets/javascripts/react-components/SessionOtherTrack.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionOtherTrack.js.jsx.coffee index c55b515d8..2129481da 100644 --- a/web/app/assets/javascripts/react-components/SessionOtherTrack.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/SessionOtherTrack.js.jsx.coffee @@ -35,6 +35,8 @@ MixerActions = @MixerActions "my-track" : true "has-mixer" : this.props.hasMixer "no-mixer" : !this.props.hasMixer + "has-audio" : this.props.noAudio != true + "no-audio" : this.props.noAudio == true }) pan = if mixers.mixer? then mixers.mixer?.pan else 0 diff --git a/web/app/assets/javascripts/react-components/SessionOtherTracks.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionOtherTracks.js.jsx.coffee index 0de14ad6b..9e6815212 100644 --- a/web/app/assets/javascripts/react-components/SessionOtherTracks.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/SessionOtherTracks.js.jsx.coffee @@ -1,5 +1,5 @@ context = window -ReactCSSTransitionGroup = React.addons.CSSTransitionGroup; +ReactCSSTransitionGroup = React.addons.CSSTransitionGroup @SessionOtherTracks = React.createClass({ @@ -9,6 +9,7 @@ ReactCSSTransitionGroup = React.addons.CSSTransitionGroup; session = sessionMixers.session mixers = sessionMixers.mixers + noAudioUsers = mixers.noAudioUsers participants = [] @@ -36,7 +37,7 @@ ReactCSSTransitionGroup = React.addons.CSSTransitionGroup; instrumentIcon = context.JK.getInstrumentIcon45(firstTrack.instrument_id) photoUrl = context.JK.resolveAvatarUrl(participant.user.photo_url) - participantState = {participant:participant, tracks: tracks, name: name, instrumentIcon: instrumentIcon, photoUrl: photoUrl, hasMixer: hasMixer} + participantState = {participant:participant, tracks: tracks, name: name, instrumentIcon: instrumentIcon, photoUrl: photoUrl, hasMixer: hasMixer, noAudio: noAudioUsers[participant.client_id]} participants.push(participantState) diff --git a/web/app/assets/javascripts/react-components/SessionRecordedTrack.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionRecordedTrack.js.jsx.coffee new file mode 100644 index 000000000..0ff654dd2 --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionRecordedTrack.js.jsx.coffee @@ -0,0 +1,81 @@ +context = window + +MixerActions = @MixerActions + +@SessionRecordedTrack = React.createClass({ + + handleMute: (e) -> + e.preventDefault() + + muting = $(e.currentTarget).is('.enabled') + + MixerActions.mute([this.props.mixers.mixer], muting) + + render: () -> + + # today, all mixers are the same for a remote participant; so just grab the 1st + mixers = @props.mixers + + muteMixer = mixers.muteMixer + vuMixer = mixers.vuMixer + muteMixerId = muteMixer?.id + + classes = classNames({ + 'track-icon-mute': true + 'enabled' : !muteMixer?.mute + 'muted' : muteMixer?.mute + }) + + componentClasses = classNames({ + "session-track" : true + "recorded-track" : true + }) + + pan = mixers.mixer.pan + + panStyle = { + transform: "rotate(#{pan}deg)" + WebkitTransform: "rotate(#{pan}deg)" + } + + `
+
+
{this.props.userName}
+
+ +
+
+
+
+
+
+
+ +
+
+
` + + componentDidMount: () -> + + + $root = $(this.getDOMNode()) + $mute = $root.find('.track-icon-mute') + $pan = $root.find('.track-icon-pan') + + context.JK.interactReactBubble( + $mute, + 'SessionTrackVolumeHover', + () => + {mixers:@props.mixers} + , + {width:235, positions:['right', 'left'], offsetParent:$root.closest('.screen')}) + + context.JK.interactReactBubble( + $pan, + 'SessionTrackPanHover', + () => + {mixers:@props.mixers} + , + {width:331, positions:['right', 'left'], offsetParent:$root.closest('.screen')}) + +}) diff --git a/web/app/assets/javascripts/react-components/SessionSelfVolumeHover.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionSelfVolumeHover.js.jsx.coffee index b01da5418..cfe67e8d7 100644 --- a/web/app/assets/javascripts/react-components/SessionSelfVolumeHover.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/SessionSelfVolumeHover.js.jsx.coffee @@ -1,39 +1,61 @@ context = window +MIX_MODES = context.JK.MIX_MODES MixerActions = @MixerActions @SessionSelfVolumeHover = React.createClass({ + mixins: [Reflux.listenTo(@SessionMyTracksStore,"onInputsChanged")] + + onInputsChanged: (sessionMixers) -> + + mixers = sessionMixers.mixers + inputGroupMixers = mixers.getAudioInputCategoryMixer(MIX_MODES.PERSONAL) + chatGroupMixers = mixers.getChatCategoryMixer( MIX_MODES.PERSONAL) + + this.setState({inputGroupMixers: inputGroupMixers, chatGroupMixers: chatGroupMixers}) getInitialState: () -> - {mixers: this.props.mixers} + {inputGroupMixers: @props.inputGroupMixers, chatGroupMixers: @props.chatGroupMixers} - handleMute: (e) -> + handleAudioInputMute: (e) -> e.preventDefault() muting = $(e.currentTarget).is('.enabled') - MixerActions.mute([this.props.mixers.mixer], muting) + MixerActions.mute([@state.inputGroupMixers.muteMixer], muting) - handleMuteCheckbox: (e) -> + handleChatInputMute: (e) -> + e.preventDefault() + + muting = $(e.currentTarget).is('.enabled') + + MixerActions.mute([@state.chatGroupMixers.muteMixer], muting) + + handleAudioInputMuteCheckbox: (e) -> muting = $(e.target).is(':checked') - MixerActions.mute([this.props.mixers.mixer, this.props.mixers.oppositeMixer], muting) + MixerActions.mute([@state.inputGroupMixers.muteMixer], muting) + + handleChatMuteCheckbox: (e) -> + muting = $(e.target).is(':checked') + + MixerActions.mute([@state.chatGroupMixers.muteMixer], muting) render: () -> - monitorMuteMixer = this.props.inputGroupMixers.muteMixer + monitorMuteMixer = @state.inputGroupMixers.muteMixer monitorMuteMixerId = monitorMuteMixer?.id - monitorVolumeLeft = this.props.inputGroupMixers.mixer?.volume_left + monitorVolumeLeft = @state.inputGroupMixers.mixer?.volume_left monitorMuteClasses = classNames({ 'track-icon-mute': true 'enabled' : !monitorMuteMixer?.mute 'muted' : monitorMuteMixer?.mute }) - chatMuteMixer = this.props.chatGroupMixers.muteMixer + chatMuteMixer = @state.chatGroupMixers.muteMixer chatMuteMixerId = chatMuteMixer?.id - chatVolumeLeft = this.props.chatGroupMixers.mixer?.volume_left + chatVolumeLeft = @state.chatGroupMixers.mixer?.volume_left chatMuteClasses = classNames({ 'track-icon-mute': true 'enabled' : !chatMuteMixer?.mute @@ -45,20 +67,20 @@ MixerActions = @MixerActions

Music

- +
- +
Volume
{monitorVolumeLeft}dB
- -
+ +
- +
@@ -72,20 +94,20 @@ MixerActions = @MixerActions

Chat

- +
- +
Volume
{chatVolumeLeft}dB
- -
+ +
- +
@@ -99,23 +121,40 @@ MixerActions = @MixerActions $root = jQuery(this.getDOMNode()) # initialize icheck - $checkbox = $root.find('input') - context.JK.checkbox($checkbox) - $checkbox.on('ifChanged', this.handleMuteCheckbox); + $chatMuteCheckbox = $root.find('.chat-mixer input') + context.JK.checkbox($chatMuteCheckbox) + $chatMuteCheckbox.on('ifChanged', @handleChatMuteCheckbox); - #if this.props.mixers.muteMixer.mute - # $checkbox.iCheck('check').attr('checked', true) - #else - # $checkbox.iCheck('uncheck').attr('checked', false) + 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 - $checkbox = $root.find('input') + $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) - #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/SessionTrackVU.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionTrackVU.js.jsx.coffee index 68b0d5b67..a9ad12cd5 100644 --- a/web/app/assets/javascripts/react-components/SessionTrackVU.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/SessionTrackVU.js.jsx.coffee @@ -17,12 +17,14 @@ context = window lights.push(``) - tableClasses = classNames('vu', 'horizontal', this.props.side + '-' + this.props.mixers.mixer?.id) + tableClasses = classNames('vu', 'horizontal') ` - - {lights} - + + + {lights} + +
` else @@ -31,11 +33,39 @@ context = window lightClasses = classNames('vulight', 'vu' + i, lightClass) - lights.push(``) + lights.push(``) - tableClasses = classNames('vu', 'vertical', this.props.side + '-' + this.props.mixers.mixer?.id) + tableClasses = classNames('vu', 'vertical') ` + {lights} +
` + + getInitialState: () -> + {registered: null} + + registerVU: (mixerId) -> + + return if @state.registered? || !mixerId? + + $root = $(this.getDOMNode()) + + context.JK.VuHelpers.registerVU('single', mixerId, @render, @props.orientation == 'horizontal', @props.lightCount, $root.find('td')) + + @setState(registered: {mixerId: mixerId, ptr: @render}) + + + componentWillReceiveProps: (nextProps) -> + @registerVU(nextProps.mixers.mixer?.id) + + componentDidMount: () -> + @registerVU(@props.mixers.mixer?.id) + + componentWillUnmount: () -> + console.log("UNMOUNTING") + if @state.registered? + context.JK.VuHelpers.unregisterVU(@state.registered.mixerId, @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 index f8147e2ed..503b215a3 100644 --- a/web/app/assets/javascripts/react-components/SessionTrackVolumeHover.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/SessionTrackVolumeHover.js.jsx.coffee @@ -58,7 +58,7 @@ MixerActions = @MixerActions
- +
diff --git a/web/app/assets/javascripts/react-components/SessionVolumeSettingsBtn.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionVolumeSettingsBtn.js.jsx.coffee index 0a9ee0b9b..e530b1bc0 100644 --- a/web/app/assets/javascripts/react-components/SessionVolumeSettingsBtn.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/SessionVolumeSettingsBtn.js.jsx.coffee @@ -1,4 +1,5 @@ context = window +MIX_MODES = context.JK.MIX_MODES @SessionVolumeSettingsBtn = React.createClass({ @@ -20,7 +21,7 @@ context = window $root, 'SessionSelfVolumeHover', () => - {inputGroupMixers: this.state.mixers.getAudioInputChatGroupMixer(), chatGroupMixers: this.state.mixers.getChatGroupMixer()} + {inputGroupMixers: @state.mixers.getAudioInputCategoryMixer(MIX_MODES.PERSONAL), chatGroupMixers: @state.mixers.getChatCategoryMixer( MIX_MODES.PERSONAL)} , {width:470, positions:['right', 'bottom', 'left'], offsetParent:$root.closest('.screen')}) 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 index 9aa71fa0d..5d5678207 100644 --- a/web/app/assets/javascripts/react-components/actions/MixerActions.js.coffee +++ b/web/app/assets/javascripts/react-components/actions/MixerActions.js.coffee @@ -9,4 +9,8 @@ context = window 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/SessionActions.js.coffee b/web/app/assets/javascripts/react-components/actions/SessionActions.js.coffee index 50d071cf2..634e7d419 100644 --- a/web/app/assets/javascripts/react-components/actions/SessionActions.js.coffee +++ b/web/app/assets/javascripts/react-components/actions/SessionActions.js.coffee @@ -8,4 +8,15 @@ context = window 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/helpers/MixerHelper.js.coffee b/web/app/assets/javascripts/react-components/helpers/MixerHelper.js.coffee index 07107298d..11c83bf08 100644 --- a/web/app/assets/javascripts/react-components/helpers/MixerHelper.js.coffee +++ b/web/app/assets/javascripts/react-components/helpers/MixerHelper.js.coffee @@ -1,31 +1,25 @@ context = window ChannelGroupIds = context.JK.ChannelGroupIds +CategoryGroupIds = context.JK.CategoryGroupIds MIX_MODES = context.JK.MIX_MODES; @MixerHelper = class MixerHelper - constructor: (@session, @masterMixers, @personalMixers, @mixMode) -> + constructor: (@session, @masterMixers, @personalMixers, @metro, @noAudioUsers, @mixMode) -> @mixersByResourceId = {} @mixersByTrackId = {} @allMixers = {} @currentMixerRangeMin = null @currentMixerRangeMax = null + @mediaSummary = {} @mediaTrackGroups = [ChannelGroupIds.MediaTrackGroup, ChannelGroupIds.JamTrackGroup, ChannelGroupIds.MetronomeGroup] @muteBothMasterAndPersonalGroups = [ChannelGroupIds.AudioInputMusicGroup, ChannelGroupIds.MediaTrackGroup, ChannelGroupIds.JamTrackGroup, ChannelGroupIds.MetronomeGroup] - @organize() - updateMixers: (type, text, @masterMixers, @personalMixers) -> - - @organize() - - - - organize: () -> for masterMixer in @masterMixers @allMixers['M' + masterMixer.id] = masterMixer; # populate allMixers by mixer.id @@ -84,20 +78,20 @@ MIX_MODES = context.JK.MIX_MODES; so, let's group up all mixers by type, and then ask them to be rendered ### - recordingTrackMixers = [] - backingTrackMixers = [] - jamTrackMixers = [] - metronomeTrackMixers = [] - adhocTrackMixers = [] + @recordingTrackMixers = [] + @backingTrackMixers = [] + @jamTrackMixers = [] + @metronomeTrackMixers = [] + @adhocTrackMixers = [] - groupByType = (mixers, isLocalMixer) -> + 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) + @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 @@ -119,7 +113,7 @@ MIX_MODES = context.JK.MIX_MODES; break if isJamTrack - jamTrackMixers.push(mixer) + @jamTrackMixers.push(mixer) else isBackingTrack = false if recordedBackingTracks @@ -135,21 +129,21 @@ MIX_MODES = context.JK.MIX_MODES; break if isBackingTrack - backingTrackMixers.push(mixer) + @backingTrackMixers.push(mixer) else # couldn't resolve this as a JamTrack or Backing track, must be a normal recorded file - recordingTrackMixers.push(mixer) + @recordingTrackMixers.push(mixer) else if mediaType == 'PeerMediaTrack' || mediaType == 'BackingTrack' - backingTrackMixers.push(mixer) + @backingTrackMixers.push(mixer) else if mediaType == 'JamTrack' - jamTrackMixers.push(mixer); + @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) + @recordingTrackMixers.push(mixer) else logger.warn("Unknown track type: " + mediaType) - adhocTrackMixers.push(mixer) + @adhocTrackMixers.push(mixer) groupByType(localMediaMixers, true); groupByType(peerLocalMediaMixers, false); @@ -170,17 +164,261 @@ MIX_MODES = context.JK.MIX_MODES; checkMetronomeTransition(); ### - if adhocTrackMixers.length > 0 + @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: {mixer: mixer, oppositeMixer: oppositeMixer, vuMixer: mixer, muteMixer: mixer} + + 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? + + oppositeMixer = @getMixerByResourceId(mixer.rid, MIX_MODES.PERSONAL) + + data = + name: jamTrackName + part: part + isOpener: isOpener + instrumentIcon: instrumentIcon + track: oneOfTheTracks + mixers: {mixer: mixer, oppositeMixer: oppositeMixer, vuMixer: mixer, muteMixer: mixer} + + _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? + oppositeMixer = @getMixerByResourceId(mixer.rid, MIX_MODES.PERSONAL) + + isOpener = mixer.group_id == ChannelGroupIds.MediaTrackGroup + data = + recordingName: recordingName + isOpener: isOpener + userName: userName + instrumentIcon: instrumentIcon + track: oneOfTheTracks + mixers: {mixer: mixer, oppositeMixer: oppositeMixer, vuMixer: mixer, muteMixer: mixer} + + 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 + mixersForGroupIds: (groupIds, mixMode) -> foundMixers = [] mixers = if mixMode == MIX_MODES.MASTER then @masterMixers else @personalMixers; for mixer in mixers - groupIdLen = groupIds.length - for i in groupIdLen - if mixer.group_id == groupIds[i] + for groupId in groupIds + if mixer.group_id == groupId foundMixers.push(mixer) foundMixers @@ -231,6 +469,20 @@ MIX_MODES = context.JK.MIX_MODES; 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) -> mixer = null # what is the best mixer for this track/client ID? oppositeMixer = null # what is the corresponding mixer in the opposite mode? @@ -373,6 +625,17 @@ MIX_MODES = context.JK.MIX_MODES; context.trackVolumeObject.pan = context.JK.PanHelpers.convertPercentToPan(panPercent); context.jamClient.SessionSetControlState(mixer.id, mixer.mode); + loopChanged: (mixer, shouldLoop) -> + console.log("mixer, shouldLoop", 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 @@ -438,15 +701,16 @@ MIX_MODES = context.JK.MIX_MODES; @currentMixerRangeMax = mixer.range_high; mixer - updateVU: (mixerId, value, isClipping) -> - selector = null - pureMixerId = mixerId.replace("_vul", "") - pureMixerId = pureMixerId.replace("_vur", "") - mixer = @getMixer(pureMixerId, @mixMode) + updateVU: (mixerId, leftValue, leftClipping, rightValue, rightClipping) -> + mixer = @getMixer(mixerId, @mixMode) unless mixer # try again, in the opposite mode (awful that this is necessary) - mixer = @getMixer(pureMixerId, !@mixMode) + mixer = @getMixer(mixerId, !@mixMode) + if mixer + context.JK.VuHelpers.updateVU3(mixer, leftValue, leftClipping, rightValue, rightClipping) + + ### if mixer if mixer.stereo # // stereo track if mixerId.substr(-4) == "_vul" @@ -459,30 +723,37 @@ MIX_MODES = context.JK.MIX_MODES; 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: (groupId, mode) -> - mixers = @mixersForGroupId(groupId, MIX_MODES.PERSONAL) + 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 {} + + 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 {} else - mixer = mixers[0] { - mixer: mixer, - muteMixer : mixer, - vuMixer: mixer, - oppositeMixer: mixer + mixer: found, + muteMixer : found, + vuMixer: found, + oppositeMixer: found } - console.log("M MIXERS", @masterMixers) - console.log("P MIXERS", @personalMixers) + getAudioInputCategoryMixer: (mode) -> + @getGroupMixer(CategoryGroupIds.AudioInputMusic, mode) - getAudioInputChatGroupMixer: () -> - @getGroupMixer(ChannelGroupIds.AudioInputMusicGroup, MIX_MODES.PERSONAL) - - getChatGroupMixer: () -> - @getGroupMixer(ChannelGroupIds.AudioInputChatGroup, MIX_MODES.PERSONAL) \ No newline at end of file + getChatCategoryMixer: (mode) -> + @getGroupMixer(CategoryGroupIds.AudioInputChat, mode) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/helpers/SessionHelper.js.coffee b/web/app/assets/javascripts/react-components/helpers/SessionHelper.js.coffee index 839d53b45..8fe3d6098 100644 --- a/web/app/assets/javascripts/react-components/helpers/SessionHelper.js.coffee +++ b/web/app/assets/javascripts/react-components/helpers/SessionHelper.js.coffee @@ -2,10 +2,12 @@ context = window @SessionHelper = class SessionHelper - constructor: (app, session, isRecording) -> + constructor: (app, session, participantsEverSeen, isRecording, downloadingJamTrack) -> @app = app @session = session + @participantsEverSeen = participantsEverSeen @isRecording = isRecording + @downloadingJamTrack = downloadingJamTrack inSession: () -> @session? @@ -55,6 +57,7 @@ context = window 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 @@ -62,6 +65,14 @@ context = window 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)-> @@ -70,12 +81,22 @@ context = window 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 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 index 620ca65dd..70c97763a 100644 --- a/web/app/assets/javascripts/react-components/stores/MixerStore.js.coffee +++ b/web/app/assets/javascripts/react-components/stores/MixerStore.js.coffee @@ -5,6 +5,19 @@ 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); @@ -17,17 +30,47 @@ rest = context.JK.Rest() 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 - handleVolumeChangeCallback: () -> + + 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: () -> - logger.debug("metronome callback") + + 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) -> @@ -49,8 +92,8 @@ rest = context.JK.Rest() # 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 + "_vul", (leftValue + 80) / 80, leftClipping) - @mixers.updateVU(mixerId + "_vur", (rightValue + 80) / 80, rightClipping) + @mixers.updateVU(mixerId, (leftValue + 80) / 80, leftClipping, (rightValue + 80) / 80, rightClipping) + #@mixers.updateVU(mixerId + "_vur", (rightValue + 80) / 80, rightClipping) handleBackingTrackSelectedCallback: () -> @@ -60,17 +103,26 @@ rest = context.JK.Rest() @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, @mixers?.mixMode || MIX_MODES.PERSONAL) - - this.trigger({session: @session, mixers: @mixers}) + @mixers = new context.MixerHelper(@session, @masterMixers, @personalMixers, @metro, @noAudioUsers, @mixers?.mixMode || MIX_MODES.PERSONAL) + @issueChange() onMute: (mixers, muting) -> @@ -78,18 +130,54 @@ rest = context.JK.Rest() @mixers.mute(mixer.id, mixer.mode, muting); # simulate a state change to cause a UI redraw - this.trigger({session: @session, mixers: @mixers}) + @issueChange() onFaderChanged: (data, mixerIds, groupId) -> @mixers.faderChanged(data, mixerIds, groupId) - this.trigger({session: @session, mixers: @mixers}) + @issueChange() onPanChanged: (data, mixerIds, groupId) -> @mixers.panChanged(data, mixerIds, groupId) - this.trigger({session: @session, mixers: @mixers}) + @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) @@ -99,19 +187,23 @@ rest = context.JK.Rest() onMixersChanged: (type, text) -> + console.log("MixerStore: onMixersChanged") + @masterMixers = context.jamClient.SessionGetAllControlState(true); @personalMixers = context.jamClient.SessionGetAllControlState(false); - @mixers = new context.MixerHelper(@session, @masterMixers, @personalMixers, @mixers?.mixMode || MIX_MODES.PERSONAL) + console.log("masterMixers", @masterMixers) + console.log("personalMixers", @personalMixers) + @mixers = new context.MixerHelper(@session, @masterMixers, @personalMixers, @metro, @noAudioUsers, @mixers?.mixMode || MIX_MODES.PERSONAL) SessionActions.mixersChanged.trigger(type, text, @mixers.getTrackInfo()) - this.trigger({session: @session, mixers: @mixers}) + @issueChange() onMixerModeChanged: (mode) -> - @mixers = new context.MixerHelper(@session, @masterMixers, @personalMixers, mode) - this.trigger({session: @session, mixers: @mixers}) + @mixers = new context.MixerHelper(@session, @masterMixers, @personalMixers, @metro, @noAudioUsers, mode) + @issueChange() onSyncTracks: () -> logger.debug("MixerStore: onSyncTracks") 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/SessionNotificationStore.js.coffee b/web/app/assets/javascripts/react-components/stores/SessionNotificationStore.js.coffee new file mode 100644 index 000000000..03d20f5dc --- /dev/null +++ b/web/app/assets/javascripts/react-components/stores/SessionNotificationStore.js.coffee @@ -0,0 +1,39 @@ +$ = 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 + @notifications.unshift(notification) + + 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/SessionStore.js.coffee b/web/app/assets/javascripts/react-components/stores/SessionStore.js.coffee index 6a9ed99df..1742ab26e 100644 --- a/web/app/assets/javascripts/react-components/stores/SessionStore.js.coffee +++ b/web/app/assets/javascripts/react-components/stores/SessionStore.js.coffee @@ -8,6 +8,7 @@ MIX_MODES = context.JK.MIX_MODES SessionActions = @SessionActions RecordingActions = @RecordingActions +NotificationActions = @NotificationActions @SessionStore = Reflux.createStore( { @@ -33,6 +34,9 @@ RecordingActions = @RecordingActions isRecording: false previousAllTracks: {userTracks: [], backingTracks: [], metronomeTracks: []} webcamViewer: null + openBackingTrack: null + helper: null + downloadingJamTrack: false init: -> # Register with the app store to get @app @@ -48,6 +52,131 @@ RecordingActions = @RecordingActions @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) + + 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") @@ -71,6 +200,133 @@ RecordingActions = @RecordingActions @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() @@ -227,7 +483,7 @@ RecordingActions = @RecordingActions else @app.notifyAlert(title, "Error reason: " + reason) - this.trigger(new context.SessionHelper(@app, @currentSession, @isRecording)) + @issueChange() notifyWithUserInfo: (title , text, clientId) -> @findUserBy({clientId: clientId}) @@ -504,7 +760,7 @@ RecordingActions = @RecordingActions # 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: context.JK.CurrentSessionModel.id(), jam_track_id: jamTrack.id}) + rest.openJamTrack({id: @currentSessionId, jam_track_id: jamTrack.id}) .done((response) => logger.debug("jamtrack opened") # now actually load the jamtrack @@ -567,6 +823,9 @@ RecordingActions = @RecordingActions @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) @@ -668,7 +927,7 @@ RecordingActions = @RecordingActions console.log("SESSION CHANGED", sessionData) - this.trigger(new context.SessionHelper(@app, @currentSession, @isRecording)) + @issueChange() ensureConnected: () -> unless context.JK.JamServer.connected @@ -740,7 +999,7 @@ RecordingActions = @RecordingActions @sessionEnded() - this.trigger(new context.SessionHelper(@app, @currentSession, @isRecording)) + @issueChange() selfOpenedJamTracks: () -> @currentSession && (@currentSession.jam_track_initiator_id == context.JK.currentUserId) @@ -776,6 +1035,17 @@ RecordingActions = @RecordingActions @previousAllTracks = {userTracks: [], backingTracks: [], metronomeTracks: []} @openBackingTrack = null @shownAudioMediaMixerHelp = false - @controlsLockedForJamTrackRecording = 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/session.js b/web/app/assets/javascripts/session.js index 1175e8ad8..4f8ed92c6 100644 --- a/web/app/assets/javascripts/session.js +++ b/web/app/assets/javascripts/session.js @@ -1425,13 +1425,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({ @@ -2158,8 +2158,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) { @@ -3025,6 +3025,7 @@ function closeMetronomeTrack() { rest.closeMetronome({id: sessionModel.id()}) .done(function() { + logger.debug("session: SessionCloseMetronome") context.jamClient.SessionCloseMetronome(); sessionModel.refreshCurrentSession(true); }) 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/utils.js b/web/app/assets/javascripts/utils.js index 1a11989bc..5332f9b88 100644 --- a/web/app/assets/javascripts/utils.js +++ b/web/app/assets/javascripts/utils.js @@ -1422,7 +1422,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/vuHelpers.js b/web/app/assets/javascripts/vuHelpers.js index 07e47d7e1..d49fde42c 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" @@ -95,6 +97,105 @@ }, + // 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, mixerId, someFunction, horizontal, lightCount, leftLights, rightLights) { + var registrations = this.registeredMixers[mixerId] + if (!registrations) { + registrations = [] + this.registeredMixers[mixerId] = registrations + } + + if(type == 'single') { + registrations.push({type:type, ptr: someFunction, horizontal: horizontal, lightCount: lightCount, lights:leftLights}) + } + else { + registrations.push({type:type, ptr: someFunction, horizontal: horizontal, lightCount: lightCount, leftLights:leftLights, rightLights: rightLights}) + } + + + }, + + unregisterVU: function(mixerId, someFunction) { + var registrations = this.registeredMixers[mixerId] + if (!registrations || registrations.length == 0) { + logger.debug("no registration found for: " + type + ":" + mixerId) + return + } + + var origLength = registrations.length + registrations = registrations.filter(function(element) { + someFunction !== element.ptr + }) + + this.registeredMixers[mixerId] = registrations + + if(origLength == registrations.length) { + logger.warn("did not find anything to unregister for: " + type + ':' + mixerId) + } + }, + + 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 registrations = this.registeredMixers[mixer.id] + 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 == 'single') { + // 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) + } + } + } + } + }, + /** * Given a selector representing a container for a VU meter and * a value between 0.0 and 1.0, light the appropriate lights. diff --git a/web/app/assets/stylesheets/client/common.css.scss b/web/app/assets/stylesheets/client/common.css.scss index 2f242f0b0..7441918f3 100644 --- a/web/app/assets/stylesheets/client/common.css.scss +++ b/web/app/assets/stylesheets/client/common.css.scss @@ -343,3 +343,7 @@ $labelFontSize: 12px; text-transform: capitalize } +.vertical-helper { + display: inline-block; + height: 100%; +} 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/react-components/MediaControls.scss.scss b/web/app/assets/stylesheets/client/react-components/MediaControls.scss.scss new file mode 100644 index 000000000..5bf94e719 --- /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:-29px; + 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 index 237d06fbf..7674ab62e 100644 --- a/web/app/assets/stylesheets/client/react-components/SessionScreen.css.scss +++ b/web/app/assets/stylesheets/client/react-components/SessionScreen.css.scss @@ -26,6 +26,10 @@ padding: 15px; height: 100%; margin-bottom: 15px; + color:$ColorTextTypical; + overflow:hidden; + + position:relative; } .session-notifications { @@ -62,10 +66,21 @@ } .session-tracks-scroller { - position: relative; overflow-x: hidden; overflow-y: auto; width: 100%; + + position: absolute; + top: 90px; + padding: 0 15px; + box-sizing: border-box; + bottom: 0; + left: 0; + right: 0; + + &.media-options-showing { + top:180px; + } } p { @@ -73,10 +88,13 @@ margin: 0; } + .download-jamtrack { + margin-top:20px; + } + .session-track { float:left; margin: 10px 0; - color: $ColorTextTypical; background-color: #242323; border-radius: 6px; @@ -96,15 +114,71 @@ border-radius: 6px; } - &.no-mixer { + &.no-mixer, &.no-audio { .disabled-track-overlay { width: 100%; height: 100%; opacity:0.5; } } + + + // media overrides + &.backing-track, &.recorded-track, &.jam-track, &.metronome { + 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:20px; + } + .track-icon-mute{ + float:right; + } + } + + &.metronome { + .track-instrument { + float:left; + margin-left:0; + margin-right: 8px; + margin-top: -3px; + } + .track-controls { + margin-left:0; + } + } + + &.recorded-track, &.jam-track { + 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: -2px 7px 0 0; + } + } } + + .react-holder { &.SessionTrackVolumeHover, &.SessionSelfVolumeHover { height:331px; @@ -423,29 +497,114 @@ } } + .session-track-settings { - height:18px; + height:20px; cursor:pointer; - color:white; padding-bottom:1px; // to line up with SessionOtherTracks + color:$ColorTextTypical; span { - top: -4px; + top: -5px; position: relative; left:3px; } } .session-invite-musicians { - height:19px; + height:20px; cursor: pointer; - color:white; + color:$ColorTextTypical; 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; + } + } + } + + .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:$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; } } \ No newline at end of file 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 7ea6d2c7c..be342382b 100644 --- a/web/app/assets/stylesheets/minimal/minimal.css.scss +++ b/web/app/assets/stylesheets/minimal/minimal.css.scss @@ -8,5 +8,8 @@ *= require icheck/minimal/minimal *= require minimal/popup *= require minimal/recording_controls +*= require minimal/media_controls *= require minimal/minimal_main +*= require client/metronomePlaybackModeSelect +*= require_directory ../client/react-components */ \ No newline at end of file diff --git a/web/app/controllers/popups_controller.rb b/web/app/controllers/popups_controller.rb index e05e2f8ee..1af9f57de 100644 --- a/web/app/controllers/popups_controller.rb +++ b/web/app/controllers/popups_controller.rb @@ -6,4 +6,8 @@ class PopupsController < ApplicationController render :layout => "minimal" end + def media_controls + render :layout => "minimal" + end + 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/clients/_metronome_playback_mode.slim b/web/app/views/clients/_metronome_playback_mode.slim index 2af251789..da6cdfe3e 100644 --- a/web/app/views/clients/_metronome_playback_mode.slim +++ b/web/app/views/clients/_metronome_playback_mode.slim @@ -8,4 +8,4 @@ script type='text/template' id='template-metronome-playback-mode' a href='#' - Play cluster test li data-ui-option="show-metronome-window" - a href='#' - Show visual metronome + a href='#' - Show visual metronome \ No newline at end of file diff --git a/web/app/views/clients/_metronome_playback_mode2.slim b/web/app/views/clients/_metronome_playback_mode2.slim new file mode 100644 index 000000000..cdf4b19ff --- /dev/null +++ b/web/app/views/clients/_metronome_playback_mode2.slim @@ -0,0 +1,8 @@ +script type='text/template' id='template-metronome-playback-mode' + p.please-select Please select one: + ul + li data-playback-option="self" + a href='#' - Play metronome + + li data-playback-option="cricket" + a href='#' - Play cluster test \ No newline at end of file diff --git a/web/app/views/landings/affiliate_program.html.slim b/web/app/views/landings/affiliate_program.html.slim index 34859ff76..64b61ba87 100644 --- a/web/app/views/landings/affiliate_program.html.slim +++ b/web/app/views/landings/affiliate_program.html.slim @@ -12,7 +12,7 @@ h1 Learn How to Make Money by Referring Users .video-wrapper .video-container - iframe src="//www.youtube.com/embed/ylYcvTY9CVo" frameborder="0" allowfullscreen + iframe src="//www.youtube.com/embed/96YTnO_H9a4" frameborder="0" allowfullscreen br clear="all" .row h1 JamKazam Affiliate Agreement diff --git a/web/app/views/popups/media_controls.html.slim b/web/app/views/popups/media_controls.html.slim new file mode 100644 index 000000000..edb4f088e --- /dev/null +++ b/web/app/views/popups/media_controls.html.slim @@ -0,0 +1,3 @@ +- provide(:page_name, 'media-controls-popup popup') += render "clients/metronome_playback_mode2" += react_component 'PopupWrapper', {component: 'PopupMediaControls'} \ No newline at end of file diff --git a/web/config/routes.rb b/web/config/routes.rb index b51157a0c..90c6547f5 100644 --- a/web/config/routes.rb +++ b/web/config/routes.rb @@ -136,7 +136,8 @@ SampleApp::Application.routes.draw do match '/extras/settings', to: 'extras#settings' scope '/popups' do - match '/recording-controls', to: 'popups#recording_controls' + match '/recording-controls', to: 'popups#recording_controls' + match '/media-controls', to: 'popups#media_controls' end scope '/corp' do