diff --git a/admin/Gemfile b/admin/Gemfile index a2544488b..96f2e1ecd 100644 --- a/admin/Gemfile +++ b/admin/Gemfile @@ -58,7 +58,7 @@ gem 'resque' gem 'resque-retry' gem 'resque-failed-job-mailer' gem 'resque-lonely_job', '~> 1.0.0' -gem 'eventmachine', '1.0.3' +gem 'eventmachine', '1.0.4' gem 'amqp', '0.9.8' gem 'logging-rails', :require => 'logging/rails' gem 'pg_migrate' diff --git a/admin/build b/admin/build index 64c48f159..87540347c 100755 --- a/admin/build +++ b/admin/build @@ -77,7 +77,7 @@ EOF #bundle install --path vendor/bundle --local # prepare production acssets rm -rf $DIR/public/assets - bundle exec rake assets:precompile RAILS_ENV=production + #bundle exec rake assets:precompile RAILS_ENV=production # create debian using fpm bundle exec fpm -s dir -t deb -p target/deb/jam-admin_0.1.${BUILD_NUMBER}_${ARCH}.deb -n "jam-admin" -v "0.1.$BUILD_NUMBER" --prefix /var/lib/jam-admin --after-install $DIR/script/package/post-install.sh --before-install $DIR/script/package/pre-install.sh --before-remove $DIR/script/package/pre-uninstall.sh --after-remove $DIR/script/package/post-uninstall.sh Gemfile .bundle config Rakefile script config.ru lib public vendor app BUILD_NUMBER diff --git a/admin/config/initializers/email.rb b/admin/config/initializers/email.rb index 41e1651d0..dc39145d1 100644 --- a/admin/config/initializers/email.rb +++ b/admin/config/initializers/email.rb @@ -1,5 +1,10 @@ ActionMailer::Base.raise_delivery_errors = true -ActionMailer::Base.delivery_method = Rails.env == "test" ? :test : :smtp +begin + ActionMailer::Base.delivery_method = Rails.env == "test" ? :test : :smtp +rescue + # this can happen on the build server when it's compiling assets and doesn't have the 'jam' database + ActionMailer::Base.delivery_method = :test +end ActionMailer::Base.smtp_settings = { :address => Rails.application.config.email_smtp_address, :port => Rails.application.config.email_smtp_port, diff --git a/build b/build index db8ab33a4..1c5677dc3 100755 --- a/build +++ b/build @@ -61,9 +61,10 @@ popd > /dev/null if [ ! -z "$PACKAGE" ]; then - -DEB_SERVER=http://localhost:9010/apt-`uname -p` -GEM_SERVER=http://localhost:9000/gems + + source /etc/lsb-release + DEB_SERVER=https://int.jamkazam.com:9010/apt-`uname -p`/$DISTRIB_CODENAME + GEM_SERVER=http://localhost:9000/gems # if still going, then push all debs up if [[ "$GIT_BRANCH" == *develop* || "$GIT_BRANCH" == *master* || "$GIT_BRANCH" == *release* || "$GIT_BRANCH" == *feature* || "$GIT_BRANCH" == *hotfix* ]]; then diff --git a/db/jenkins b/db/jenkins index 275cb28a4..c617fd59f 100755 --- a/db/jenkins +++ b/db/jenkins @@ -1,7 +1,7 @@ #!/bin/bash -GEM_SERVER=http://localhost:9000/gems -DEB_SERVER=http://localhost:9010/apt-`uname -p` +GEM_SERVER=https://int.jamkazam.com:9000/gems +DEB_SERVER=https://int.jamkazam.com:9010/apt-`uname -p` echo "starting build..." ./build diff --git a/db/manifest b/db/manifest index 8c20bb7ce..bd6ca8ce6 100755 --- a/db/manifest +++ b/db/manifest @@ -276,12 +276,6 @@ jam_track_duration.sql sales.sql show_whats_next_count.sql recurly_adjustments.sql -alter_type_columns.sql -user_presences_rename.sql -add_genre_type.sql -add_description_to_perf_samples.sql -alter_genre_player_unique_constraint.sql -musician_search.sql signup_hints.sql packaging_notices.sql first_played_jamtrack_at.sql @@ -292,7 +286,14 @@ signing.sql optimized_redeemption.sql optimized_redemption_warn_mode.sql affiliate_partners2.sql -enhance_band_profile.sql broadcast_notifications.sql broadcast_notifications_fk.sql -calendar.sql \ No newline at end of file +calendar.sql +alter_type_columns.sql +user_presences_rename.sql +add_genre_type.sql +add_description_to_perf_samples.sql +alter_genre_player_unique_constraint.sql +musician_search.sql +enhance_band_profile.sql +alter_band_profile_rate_defaults.sql \ No newline at end of file diff --git a/db/up/alter_band_profile_rate_defaults.sql b/db/up/alter_band_profile_rate_defaults.sql new file mode 100644 index 000000000..e716f1cda --- /dev/null +++ b/db/up/alter_band_profile_rate_defaults.sql @@ -0,0 +1,2 @@ +ALTER TABLE bands ALTER COLUMN hourly_rate SET DEFAULT NULL; +ALTER TABLE bands ALTER COLUMN gig_minimum SET DEFAULT NULL; diff --git a/db/up/user_profile_corrections.sql b/db/up/user_profile_corrections.sql new file mode 100644 index 000000000..de6161845 --- /dev/null +++ b/db/up/user_profile_corrections.sql @@ -0,0 +1,2 @@ +ALTER TABLE users ALTER paid_sessions_hourly_rate TYPE integer; +ALTER TABLE users ALTER paid_sessions_daily_rate TYPE integer; \ No newline at end of file diff --git a/pb/jenkins b/pb/jenkins index 74e4ccbb0..fb8778501 100755 --- a/pb/jenkins +++ b/pb/jenkins @@ -1,6 +1,6 @@ #!/bin/bash -GEM_SERVER=http://localhost:9000/gems +GEM_SERVER=https://int.jamkazam.com:9000/gems echo "starting build..." ./build diff --git a/ruby/Gemfile b/ruby/Gemfile index 0fa0fcb34..f88a63b22 100644 --- a/ruby/Gemfile +++ b/ruby/Gemfile @@ -23,7 +23,7 @@ gem "activerecord-import", "~> 0.4.1" gem 'uuidtools', '2.1.2' gem 'bcrypt-ruby', '3.0.1' gem 'ruby-protocol-buffers', '1.2.2' -gem 'eventmachine', '1.0.3' +gem 'eventmachine', '1.0.4' gem 'amqp', '1.0.2' gem 'will_paginate' gem 'actionmailer', '3.2.13' diff --git a/ruby/jenkins b/ruby/jenkins index 8d4742fc6..97d39c693 100755 --- a/ruby/jenkins +++ b/ruby/jenkins @@ -1,6 +1,6 @@ #!/bin/bash -GEM_SERVER=http://localhost:9000/gems +GEM_SERVER=https://int.jamkazam.com:9000/gems echo "starting build..." ./build diff --git a/ruby/lib/jam_ruby/models/band.rb b/ruby/lib/jam_ruby/models/band.rb index c98f4a532..47ba88455 100644 --- a/ruby/lib/jam_ruby/models/band.rb +++ b/ruby/lib/jam_ruby/models/band.rb @@ -23,6 +23,8 @@ module JamRuby validate :validate_photo_info validate :require_at_least_one_genre, :unless => :skip_genre_validation validate :limit_max_genres + validates_numericality_of :hourly_rate, greater_than:0, less_than:100000, :allow_nil => true + validates_numericality_of :gig_minimum, greater_than:0, less_than:200000, :allow_nil => true before_save :check_lat_lng before_save :check_website_url @@ -192,6 +194,17 @@ module JamRuby band.photo_url = params[:photo_url] if params.has_key?(:photo_url) band.logo_url = params[:logo_url] if params.has_key?(:logo_url) + band.paid_gigs = params[:paid_gigs] if params.has_key?(:paid_gigs) + band.free_gigs = params[:free_gigs] if params.has_key?(:free_gigs) + band.hourly_rate = (params.has_key?(:hourly_rate) && params[:hourly_rate].to_i > 0) ? params[:hourly_rate] : nil + band.gig_minimum = (params.has_key?(:gig_minimum) && params[:hourly_rate].to_i > 0) ? params[:gig_minimum] : nil + band.add_new_members = params[:add_new_members] if params.has_key?(:add_new_members) + band.touring_option = params[:touring_option] if params.has_key?(:touring_option) + band.band_type = params[:band_type] if params.has_key?(:band_type) + band.band_status = params[:band_status] if params.has_key?(:band_status) + band.concert_count = params[:concert_count] if params.has_key?(:concert_count) + band.play_commitment = params[:play_commitment] if params.has_key?(:play_commitment) + if params.has_key?(:genres) && params[:genres] # loop through each genre in the array and save to the db genres = [] diff --git a/ruby/lib/jam_ruby/models/musician_search.rb b/ruby/lib/jam_ruby/models/musician_search.rb index 22d3d6221..9b780c53f 100644 --- a/ruby/lib/jam_ruby/models/musician_search.rb +++ b/ruby/lib/jam_ruby/models/musician_search.rb @@ -125,6 +125,7 @@ module JamRuby ms end + # XXX SQL INJECTION def _genres(rel) gids = json[KEY_GENRES] unless gids.blank? @@ -135,11 +136,12 @@ module JamRuby rel end + # XXX SQL INJECTION def _instruments(rel) unless (instruments = json['instruments']).blank? instsql = "SELECT player_id FROM musicians_instruments WHERE ((" instsql += instruments.collect do |inst| - "instrument_id = '#{inst['instrument_id']}' AND proficiency_level = #{inst['proficiency_level']}" + "instrument_id = '#{inst['id']}' AND proficiency_level = #{inst['level']}" end.join(") OR (") instsql += "))" rel = rel.where("users.id IN (#{instsql})") @@ -357,47 +359,54 @@ module JamRuby return 'Click search button to look for musicians with similar interests, skill levels, etc.' end jj = self.json - str = 'Current Search: ' - str += "Sort = #{SORT_ORDERS[json_value(MusicianSearch::KEY_SORT_ORDER)]}" + str = '' + if 0 < (val = jj[KEY_INSTRUMENTS]).length + str += ", Instruments = " + instr_ids = val.collect { |stored_instrument| stored_instrument['id'] } + instrs = Instrument.where(["id IN (?)", instr_ids]).order(:description) + instrs.each_with_index do |ii, idx| + proficiency = val.detect { |stored_instrument| stored_instrument['id'] == ii.id }['level'] + str += "#{ii.description} / #{INSTRUMENT_PROFICIENCY[proficiency.to_i]}" + str += ', ' unless idx==(instrs.length-1) + end + end + if (val = jj[KEY_INTERESTS]) != INTEREST_VALS[0] - str += "; Interest = #{INTERESTS[val]}" + str += ", Interest = #{INTERESTS[val]}" end if (val = jj[KEY_SKILL].to_i) != SKILL_VALS[0] - str += "; Skill = #{SKILL_LEVELS[val]}" + str += ", Skill = #{SKILL_LEVELS[val]}" end if (val = jj[KEY_STUDIOS].to_i) != STUDIO_COUNTS[0] - str += "; Studio Sessions = #{STUDIOS_LABELS[val]}" + str += ", Studio Sessions = #{STUDIOS_LABELS[val]}" end if (val = jj[KEY_GIGS].to_i) != GIG_COUNTS[0] - str += "; Concert Gigs = #{GIG_LABELS[val]}" + str += ", Concert Gigs = #{GIG_LABELS[val]}" end val = jj[KEY_AGES].map(&:to_i) val.sort! if !val.blank? - str += "; Ages = " + str += ", Ages = " val.each_with_index do |vv, idx| str += "#{AGES[vv]}" str += ', ' unless idx==(val.length-1) end end if 0 < (val = jj[KEY_GENRES]).length - str += "; Genres = " + str += ", Genres = " genres = Genre.where(["id IN (?)", val]).order('description').pluck(:description) genres.each_with_index do |gg, idx| str += "#{gg}" str += ', ' unless idx==(genres.length-1) end end - if 0 < (val = jj[KEY_INSTRUMENTS]).length - str += "; Instruments = " - instr_ids = val.collect { |vv| vv['instrument_id'] } - instrs = Instrument.where(["id IN (?)", instr_ids]).order(:description) - instrs.each_with_index do |ii, idx| - proficiency = val.detect { |vv| vv['instrument_id'] == ii.id }['proficiency_level'] - str += "#{ii.description} (#{INSTRUMENT_PROFICIENCY[proficiency.to_i]})" - str += ', ' unless idx==(instrs.length-1) - end + str += ", Sort = #{SORT_ORDERS[json_value(MusicianSearch::KEY_SORT_ORDER)]}" + + if str.start_with?(', ') + # trim off any leading , + str = str[2..-1] end + str = 'Current Search: ' + str str end diff --git a/ruby/lib/jam_ruby/models/user.rb b/ruby/lib/jam_ruby/models/user.rb index 0500a896b..602d93570 100644 --- a/ruby/lib/jam_ruby/models/user.rb +++ b/ruby/lib/jam_ruby/models/user.rb @@ -196,6 +196,11 @@ module JamRuby validates_numericality_of :last_jam_audio_latency, greater_than:MINIMUM_AUDIO_LATENCY, less_than:MAXIMUM_AUDIO_LATENCY, :allow_nil => true validates :last_jam_updated_reason, :inclusion => {:in => [nil, JAM_REASON_REGISTRATION, JAM_REASON_NETWORK_TEST, JAM_REASON_FTUE, JAM_REASON_JOIN, JAM_REASON_IMPORT, JAM_REASON_LOGIN] } + # stored in cents + validates_numericality_of :paid_sessions_hourly_rate, greater_than:0, less_than:200000, :allow_nil => true + # stored in cents + validates_numericality_of :paid_sessions_daily_rate, greater_than:0, less_than:5000000, :allow_nil => true + # custom validators validate :validate_musician_instruments validate :validate_current_password diff --git a/ruby/spec/jam_ruby/models/band_spec.rb b/ruby/spec/jam_ruby/models/band_spec.rb index 706f16691..23e36bb54 100644 --- a/ruby/spec/jam_ruby/models/band_spec.rb +++ b/ruby/spec/jam_ruby/models/band_spec.rb @@ -67,6 +67,40 @@ describe Band do band.country.should == band_params[:country] end + it "saves current interests" do + parms = band_params + parms[:paid_gigs]=true + parms[:free_gigs]=false + parms[:hourly_rate]=5000 + parms[:gig_minimum]=30000 + parms[:add_new_members]=true + parms[:touring_option]=false + parms[:band_type]="virtual" + parms[:band_status]="amateur" + parms[:concert_count]=3 + + band = Band.save(user, parms) + band.errors.any?.should be_false + + band.paid_gigs.should == true + band.free_gigs.should == false + band.hourly_rate.should == 5000 + parms[:gig_minimum]=30000 + band.add_new_members.should == true + band.touring_option.should == false + band.band_type.should == "virtual" + band.band_status.should == "amateur" + band.concert_count.should == 3 + + parms[:hourly_rate]="foobar" + parms[:gig_minimum]="barfoo" + band=Band.save(user, parms) + band.errors.any?.should be_true + band.errors[:hourly_rate].should == ["is not a number"] + band.errors[:gig_minimum].should == ["is not a number"] + end + + it "ensures user is a musician" do expect{ Band.save(fan, band_params) }.to raise_error("must be a musician") end @@ -173,5 +207,5 @@ describe Band do history = band.recent_history(nil, claimed_recording.id) history.size.should == 0 end - end + end end diff --git a/ruby/spec/support/utilities.rb b/ruby/spec/support/utilities.rb index 8d16dffbb..2717f63dc 100644 --- a/ruby/spec/support/utilities.rb +++ b/ruby/spec/support/utilities.rb @@ -206,6 +206,10 @@ def app_config 1 end + def google_public_server_key + "AIzaSyCPTPq5PEcl4XWcm7NZ2IGClZlbsiE8JNo" + end + private def audiomixer_workspace_path diff --git a/web/Gemfile b/web/Gemfile index 00e74d2a6..89cb15aac 100644 --- a/web/Gemfile +++ b/web/Gemfile @@ -36,7 +36,7 @@ gem 'pg', '0.17.1' gem 'compass-rails', '1.1.3' # 1.1.4 throws an exception on startup about !initialize on nil gem 'rabl', '0.11.0' # for JSON API development gem 'gon', '~>4.1.0' # for passthrough of Ruby variables to Javascript variables -gem 'eventmachine', '1.0.3' +gem 'eventmachine', '1.0.4' gem 'faraday', '~>0.9.0' gem 'amqp', '0.9.8' gem 'logging-rails', :require => 'logging/rails' @@ -95,6 +95,7 @@ gem 'react-rails', '~> 1.0' source 'https://rails-assets.org' do gem 'rails-assets-reflux' + gem 'rails-assets-classnames' end group :development, :test do diff --git a/web/app/assets/images/content/bkg_slider_gain_horiz_24.png b/web/app/assets/images/content/bkg_slider_gain_horiz_24.png new file mode 100644 index 000000000..aad8fb7b9 Binary files /dev/null and b/web/app/assets/images/content/bkg_slider_gain_horiz_24.png differ diff --git a/web/app/assets/images/content/icon_email.png b/web/app/assets/images/content/icon_email.png new file mode 100644 index 000000000..a3163f217 Binary files /dev/null and b/web/app/assets/images/content/icon_email.png differ diff --git a/web/app/assets/images/content/icon_equalizer.png b/web/app/assets/images/content/icon_equalizer.png new file mode 100644 index 000000000..9c63c8dcc Binary files /dev/null and b/web/app/assets/images/content/icon_equalizer.png differ diff --git a/web/app/assets/images/content/icon_instrument_chat45.png b/web/app/assets/images/content/icon_instrument_chat45.png new file mode 100644 index 000000000..4d0309da4 Binary files /dev/null and b/web/app/assets/images/content/icon_instrument_chat45.png differ diff --git a/web/app/assets/images/content/icon_instrument_headphones21.png b/web/app/assets/images/content/icon_instrument_headphones21.png new file mode 100644 index 000000000..79ef3c944 Binary files /dev/null and b/web/app/assets/images/content/icon_instrument_headphones21.png differ diff --git a/web/app/assets/images/content/icon_instrument_headphones45.png b/web/app/assets/images/content/icon_instrument_headphones45.png new file mode 100644 index 000000000..82533c0d2 Binary files /dev/null and b/web/app/assets/images/content/icon_instrument_headphones45.png differ diff --git a/web/app/assets/images/content/icon_instrument_metronome21.png b/web/app/assets/images/content/icon_instrument_metronome21.png new file mode 100644 index 000000000..362f08665 Binary files /dev/null and b/web/app/assets/images/content/icon_instrument_metronome21.png differ diff --git a/web/app/assets/images/content/icon_instrument_metronome45.png b/web/app/assets/images/content/icon_instrument_metronome45.png new file mode 100644 index 000000000..3c06b7544 Binary files /dev/null and b/web/app/assets/images/content/icon_instrument_metronome45.png differ diff --git a/web/app/assets/images/content/icon_leave.png b/web/app/assets/images/content/icon_leave.png new file mode 100644 index 000000000..1e2a53ff2 Binary files /dev/null and b/web/app/assets/images/content/icon_leave.png differ diff --git a/web/app/assets/images/content/icon_mixer.png b/web/app/assets/images/content/icon_mixer.png new file mode 100644 index 000000000..2de1cb8e2 Binary files /dev/null and b/web/app/assets/images/content/icon_mixer.png differ diff --git a/web/app/assets/images/content/icon_open_folder.png b/web/app/assets/images/content/icon_open_folder.png new file mode 100644 index 000000000..d4b9851ad Binary files /dev/null and b/web/app/assets/images/content/icon_open_folder.png differ diff --git a/web/app/assets/images/content/icon_pan.png b/web/app/assets/images/content/icon_pan.png new file mode 100644 index 000000000..3599333cd Binary files /dev/null and b/web/app/assets/images/content/icon_pan.png differ diff --git a/web/app/assets/images/content/icon_record.png b/web/app/assets/images/content/icon_record.png new file mode 100644 index 000000000..4ad12a2ae Binary files /dev/null and b/web/app/assets/images/content/icon_record.png differ diff --git a/web/app/assets/images/content/icon_resync.png b/web/app/assets/images/content/icon_resync.png index 86ed62630..b9c1b410f 100644 Binary files a/web/app/assets/images/content/icon_resync.png and b/web/app/assets/images/content/icon_resync.png differ diff --git a/web/app/assets/images/content/icon_settings_sm.png b/web/app/assets/images/content/icon_settings_sm.png index 6f43ad376..b686ebecc 100644 Binary files a/web/app/assets/images/content/icon_settings_sm.png and b/web/app/assets/images/content/icon_settings_sm.png differ diff --git a/web/app/assets/images/content/icon_share.png b/web/app/assets/images/content/icon_share.png index 5c73cce88..9f2ba73a7 100644 Binary files a/web/app/assets/images/content/icon_share.png and b/web/app/assets/images/content/icon_share.png differ diff --git a/web/app/assets/images/content/icon_sound.png b/web/app/assets/images/content/icon_sound.png new file mode 100644 index 000000000..ececf76d5 Binary files /dev/null and b/web/app/assets/images/content/icon_sound.png differ diff --git a/web/app/assets/images/content/icon_video.png b/web/app/assets/images/content/icon_video.png new file mode 100644 index 000000000..f6da5cc1c Binary files /dev/null and b/web/app/assets/images/content/icon_video.png differ diff --git a/web/app/assets/images/content/icon_volume.png b/web/app/assets/images/content/icon_volume.png new file mode 100644 index 000000000..339f21fd6 Binary files /dev/null and b/web/app/assets/images/content/icon_volume.png differ diff --git a/web/app/assets/images/content/icon_volume_lg.png b/web/app/assets/images/content/icon_volume_lg.png new file mode 100644 index 000000000..a99ca1b98 Binary files /dev/null and b/web/app/assets/images/content/icon_volume_lg.png differ diff --git a/web/app/assets/images/web/button_cta_jamtrack_free.png b/web/app/assets/images/web/button_cta_jamtrack_free.png new file mode 100644 index 000000000..0ffc3f32c Binary files /dev/null and b/web/app/assets/images/web/button_cta_jamtrack_free.png differ diff --git a/web/app/assets/javascripts/JamServer.js b/web/app/assets/javascripts/JamServer.js index 61b889eb2..ee570a513 100644 --- a/web/app/assets/javascripts/JamServer.js +++ b/web/app/assets/javascripts/JamServer.js @@ -198,50 +198,56 @@ function loggedIn(header, payload) { - server.signedIn = true; - server.clientID = payload.client_id; - server.publicIP = payload.public_ip; + // reason for setTimeout: + // loggedIn causes an absolute ton of initialization to happen, and errors sometimes happen + // but because loggedIn(header,payload) is a callback from a websocket, the browser doesn't show a stack trace... - if (context.jamClient !== undefined) { - context.jamClient.connected = true; - context.jamClient.clientID = server.clientID; - } + setTimeout(function() { + server.signedIn = true; + server.clientID = payload.client_id; + server.publicIP = payload.public_ip; - clearConnectTimeout(); + if (context.jamClient !== undefined) { + context.jamClient.connected = true; + context.jamClient.clientID = server.clientID; + } - heartbeatStateReset(); + clearConnectTimeout(); - app.clientId = payload.client_id; + heartbeatStateReset(); - if(isClientMode()) { - // tell the backend that we have logged in - context.jamClient.OnLoggedIn(payload.user_id, payload.token); // ACTS AS CONTINUATION - $.cookie('client_id', payload.client_id); - } + app.clientId = payload.client_id; - // this has to be after context.jamclient.OnLoggedIn, because it hangs in scenarios - // where there is no device on startup for the current profile. - // So, in that case, it's possible that a reconnect loop will attempt, but we *do not want* - // it to go through unless we've passed through .OnLoggedIn - server.connected = true; - server.reconnecting = false; - server.connecting = false; - initialConnectAttempt = false; + if (isClientMode()) { + // tell the backend that we have logged in + context.jamClient.OnLoggedIn(payload.user_id, payload.token); // ACTS AS CONTINUATION + $.cookie('client_id', payload.client_id); + } - heartbeatMS = payload.heartbeat_interval * 1000; - connection_expire_time = payload.connection_expire_time * 1000; - logger.info("loggedIn(): clientId=" + app.clientId + " heartbeat=" + payload.heartbeat_interval + "s expire_time=" + payload.connection_expire_time + 's'); - heartbeatInterval = context.setInterval(_heartbeat, heartbeatMS); - heartbeatAckCheckInterval = context.setInterval(_heartbeatAckCheck, 1000); - lastHeartbeatAckTime = new Date(new Date().getTime() + heartbeatMS); // add a little forgiveness to server for initial heartbeat - connectDeferred.resolve(); - $self.triggerHandler(EVENTS.CONNECTION_UP) + // this has to be after context.jamclient.OnLoggedIn, because it hangs in scenarios + // where there is no device on startup for the current profile. + // So, in that case, it's possible that a reconnect loop will attempt, but we *do not want* + // it to go through unless we've passed through .OnLoggedIn + server.connected = true; + server.reconnecting = false; + server.connecting = false; + initialConnectAttempt = false; - activeElementEvent('afterConnect', payload); + heartbeatMS = payload.heartbeat_interval * 1000; + connection_expire_time = payload.connection_expire_time * 1000; + logger.info("loggedIn(): clientId=" + app.clientId + " heartbeat=" + payload.heartbeat_interval + "s expire_time=" + payload.connection_expire_time + 's'); + heartbeatInterval = context.setInterval(_heartbeat, heartbeatMS); + heartbeatAckCheckInterval = context.setInterval(_heartbeatAckCheck, 1000); + lastHeartbeatAckTime = new Date(new Date().getTime() + heartbeatMS); // add a little forgiveness to server for initial heartbeat + connectDeferred.resolve(); + $self.triggerHandler(EVENTS.CONNECTION_UP) - if(payload.client_update && context.JK.ClientUpdateInstance) { - context.JK.ClientUpdateInstance.runCheck(payload.client_update.product, payload.client_update.version, payload.client_update.uri, payload.client_update.size) - } + activeElementEvent('afterConnect', payload); + + if (payload.client_update && context.JK.ClientUpdateInstance) { + context.JK.ClientUpdateInstance.runCheck(payload.client_update.product, payload.client_update.version, payload.client_update.uri, payload.client_update.size) + } + }, 0) } function heartbeatAck(header, payload) { diff --git a/web/app/assets/javascripts/accounts.js b/web/app/assets/javascripts/accounts.js index 3d52d2139..c04a6b329 100644 --- a/web/app/assets/javascripts/accounts.js +++ b/web/app/assets/javascripts/accounts.js @@ -130,7 +130,7 @@ $('#account-content-scroller').on('click', '#account-my-jamtracks-link', function(evt) { evt.stopPropagation(); navToMyJamTracks(); return false; } ); $('#account-content-scroller').on('click', '#account-edit-identity-link', function(evt) { evt.stopPropagation(); navToEditIdentity(); return false; } ); - $('#account-content-scroller').on('click', '#account-edit-profile-link', function(evt) { evt.stopPropagation(); navToEditProfile(); return false; } ); + $('#account-content-scroller').on('click', '.account-edit-profile-link', function(evt) { evt.stopPropagation(); navToEditProfile(); return false; } ); $('#account-content-scroller').on('click', '#account-edit-subscriptions-link', function(evt) { evt.stopPropagation(); navToEditSubscriptions(); return false; } ); $('#account-content-scroller').on('click', '#account-edit-payments-link', function(evt) { evt.stopPropagation(); navToEditPayments(); return false; } ); $('#account-content-scroller').on('click', '#account-edit-audio-link', function(evt) { evt.stopPropagation(); navToEditAudio(); return false; } ); diff --git a/web/app/assets/javascripts/accounts_affiliate.js b/web/app/assets/javascripts/accounts_affiliate.js index 21d0b8b1c..f9b5ec02a 100644 --- a/web/app/assets/javascripts/accounts_affiliate.js +++ b/web/app/assets/javascripts/accounts_affiliate.js @@ -207,7 +207,7 @@ rest.getLinks(type) .done(populateLinkTable) .fail(function() { - app.notify({message: 'Unable to fetch links. Please try again later.' }) + app.notify({text: 'Unable to fetch links. Please try again later.' }) }) } } diff --git a/web/app/assets/javascripts/accounts_profile_interests.js b/web/app/assets/javascripts/accounts_profile_interests.js index 6a4a65e2c..d4ad601db 100644 --- a/web/app/assets/javascripts/accounts_profile_interests.js +++ b/web/app/assets/javascripts/accounts_profile_interests.js @@ -145,8 +145,9 @@ $traditionalTouringOption.val(userDetail.traditional_band_touring ? '1' : '0') context.JK.dropdown($traditionalTouringOption) - $hourlyRate.val(userDetail.paid_sessions_hourly_rate) - $dailyRate.val(userDetail.paid_sessions_daily_rate) + // convert the value to cents + $hourlyRate.val(profileUtils.normalizeMoneyForDisplay(userDetail.paid_sessions_hourly_rate)); + $dailyRate.val(profileUtils.normalizeMoneyForDisplay(userDetail.paid_sessions_daily_rate)); $cowritingPurpose.val(userDetail.cowriting_purpose) context.JK.dropdown($cowritingPurpose) @@ -238,11 +239,11 @@ paid_sessions: $screen.find('input[name=paid_sessions]:checked').val(), paid_session_genres: $paidSessionsGenreList.html() === NONE_SPECIFIED ? [] : $paidSessionsGenreList.html().split(GENRE_LIST_DELIMITER), - paid_sessions_hourly_rate: $hourlyRate.val(), - paid_sessions_daily_rate: $dailyRate.val(), + paid_sessions_hourly_rate: profileUtils.normalizeMoneyForSubmit($hourlyRate.val()), + paid_sessions_daily_rate: profileUtils.normalizeMoneyForSubmit($dailyRate.val()), free_sessions: $screen.find('input[name=free_sessions]:checked').val(), - free_session_genre: $freeSessionsGenreList.html() === NONE_SPECIFIED ? [] : $freeSessionsGenreList.html().split(GENRE_LIST_DELIMITER), + free_session_genres: $freeSessionsGenreList.html() === NONE_SPECIFIED ? [] : $freeSessionsGenreList.html().split(GENRE_LIST_DELIMITER), cowriting: $screen.find('input[name=cowriting]:checked').val(), cowriting_genres: $cowritingGenreList.html() === NONE_SPECIFIED ? [] : $cowritingGenreList.html().split(GENRE_LIST_DELIMITER), @@ -263,7 +264,12 @@ var errors = JSON.parse(xhr.responseText) if(xhr.status == 422) { + context.JK.append_errors($hourlyRate, 'paid_sessions_hourly_rate', errors) + context.JK.append_errors($dailyRate, 'paid_sessions_daily_rate', errors) + if(errors.errors.length > 0) { + app.notifyServerError(xhr) + } } else { app.ajaxError(xhr, textStatus, errorMessage) diff --git a/web/app/assets/javascripts/accounts_profile_samples.js b/web/app/assets/javascripts/accounts_profile_samples.js index 6cce9de0e..112a0e78f 100644 --- a/web/app/assets/javascripts/accounts_profile_samples.js +++ b/web/app/assets/javascripts/accounts_profile_samples.js @@ -20,6 +20,9 @@ var ui = new context.JK.UIHelper(JK.app); var target = {}; var profileUtils = context.JK.ProfileUtils; + parent + var parent = $(".account-profile-samples") + var $screen = $('.profile-online-sample-controls', parent); // online presences var $website = $screen.find('.website'); @@ -60,6 +63,7 @@ function afterShow(data) { $.when(loadFn()) .done(function(targetPlayer) { + console.log("TARGET PLAYER", targetPlayer) if (targetPlayer && targetPlayer.keys && targetPlayer.keys.length > 0) { renderPlayer(targetPlayer) } @@ -147,24 +151,29 @@ if (samples && samples.length > 0) { $.each(samples, function(index, val) { - recordingSources.push({ + var source = { 'url': val.url, 'recording_id': val.service_id, 'recording_title': val.description - }); - - // TODO: this code is repeated in HTML file - var recordingIdAttr = ' data-recording-id="' + val.service_id + '" '; - var recordingUrlAttr = ' data-recording-url="' + val.url + '" '; - var recordingTitleAttr = ' data-recording-title="' + val.description + '"'; - var title = formatTitle(val.description); - $sampleList.append('
' + title + '
'); - $sampleList.append('
X
'); + } + recordingSources.push(source); + buildNonJamKazamEntry($sampleList, type, source); }); } } } + function buildNonJamKazamEntry($sampleList, type, source) { + // TODO: this code is repeated in HTML file + var recordingIdAttr = ' data-recording-id="' + source.recording_id + '" '; + var recordingUrlAttr = ' data-recording-url="' + source.url + '" '; + var recordingTitleAttr = ' data-recording-title="' + source.recording_title + '"'; + var title = formatTitle(source.recording_title); + $sampleList.find(".empty").addClass("hidden") + $sampleList.append('
' + title + '
'); + $sampleList.append('
X
'); + } + function buildJamkazamEntry(recordingId, recordingName) { var title = formatTitle(recordingName); @@ -179,25 +188,22 @@ $btnAddJkRecording.click(function(evt) { evt.preventDefault(); - // retrieve recordings and pass to modal dialog - api.getClaimedRecordings() - .done(function(response) { - ui.launchRecordingSelectorDialog(response, jamkazamRecordingSources, function(selectedRecordings) { - $jamkazamSampleList.empty(); + ui.launchRecordingSelectorDialog(jamkazamRecordingSources, function(selectedRecordings) { + $jamkazamSampleList.empty(); - jamkazamRecordingSources = []; + jamkazamRecordingSources = []; - // update the list with the selected recordings - $.each(selectedRecordings, function(index, val) { - jamkazamRecordingSources.push({ - 'claimed_recording_id': val.id, - 'description': val.name - }); - - buildJamkazamEntry(val.id, val.name); - }); + // update the list with the selected recordings + $.each(selectedRecordings, function(index, val) { + jamkazamRecordingSources.push({ + 'claimed_recording_id': val.id, + 'description': val.name }); + + buildJamkazamEntry(val.id, val.name); }); + }); + return false; }); @@ -287,6 +293,7 @@ disableSubmits() var player = buildPlayer() + updateFn({ website: player.website, online_presences: player.online_presences, @@ -316,8 +323,13 @@ addPerformanceSamples(ps, $soundCloudSampleList, performanceSampleTypes.SOUNDCLOUD.description); addPerformanceSamples(ps, $youTubeSampleList, performanceSampleTypes.YOUTUBE.description); + var website = $website.val() + if (website == '') { + website = null; + } + return { - website: $website.val(), + website: website, online_presences: op, performance_samples: ps } @@ -428,8 +440,8 @@ siteSuccessCallback($inputDiv, youTubeRecordingValidator, $youTubeSampleList, 'youtube'); } - function siteSuccessCallback($inputDiv, recordingSiteValidator, sampleList, type) { - sampleList.find(".empty").addClass("hidden") + function siteSuccessCallback($inputDiv, recordingSiteValidator, $sampleList, type) { + $sampleList.find(".empty").addClass("hidden") $inputDiv.removeClass('error'); $inputDiv.find('.error-text').remove(); @@ -437,13 +449,7 @@ if (recordingSources && recordingSources.length > 0) { var addedRecording = recordingSources[recordingSources.length-1]; - // TODO: this code is repeated in elsewhere in this JS file: - var recordingIdAttr = ' data-recording-id="' + addedRecording.recording_id + '" '; - var recordingUrlAttr = ' data-recording-url="' + addedRecording.url + '" '; - var recordingTitleAttr = ' data-recording-title="' + addedRecording.recording_title + '"'; - var title = formatTitle(addedRecording.recording_title); - sampleList.append('
' + title + '
'); - sampleList.append('
X
'); + buildNonJamKazamEntry($sampleList, type, addedRecording); } $inputDiv.find('input').val(''); diff --git a/web/app/assets/javascripts/addNewGear.js b/web/app/assets/javascripts/addNewGear.js index cbe9f7d50..ab45745fd 100644 --- a/web/app/assets/javascripts/addNewGear.js +++ b/web/app/assets/javascripts/addNewGear.js @@ -3,18 +3,16 @@ "use strict"; context.JK = context.JK || {}; - context.JK.AddNewGearDialog = function(app, sessionScreen) { + context.JK.AddNewGearDialog = function(app) { var logger = context.JK.logger; function events() { $('#btn-leave-session-test').click(function() { - sessionScreen.setPromptLeave(false); + context.SessionActions.leaveSession.trigger({location: '/client#/home'}) app.layout.closeDialog('configure-tracks'); - context.location = "/client#/home"; - app.layout.startNewFtue(); }); diff --git a/web/app/assets/javascripts/addTrack.js b/web/app/assets/javascripts/addTrack.js index ba6ccd459..00dd29aa6 100644 --- a/web/app/assets/javascripts/addTrack.js +++ b/web/app/assets/javascripts/addTrack.js @@ -161,7 +161,7 @@ /** setTimeout(function() { - var inputTracks = context.JK.TrackHelpers.getTracks(context.jamClient, 2); + var inputTracks = context.JK.TrackHelpers.getTracks(context.jamClient, 4); // this is some ugly logic coming up, here's why: // we need the id (guid) that the backend generated for the new track we just added diff --git a/web/app/assets/javascripts/application.js b/web/app/assets/javascripts/application.js index e77a3a58f..811277c13 100644 --- a/web/app/assets/javascripts/application.js +++ b/web/app/assets/javascripts/application.js @@ -38,6 +38,7 @@ //= require jquery.exists //= require jquery.payment //= require jquery.visible +//= require classnames //= require reflux //= require howler.core.js //= require jstz @@ -54,11 +55,12 @@ //= require react //= require react_ujs //= require react-init -//= require react-components //= require web/signup_helper //= require web/signin_helper //= require web/signin //= require web/tracking +//= require webcam_viewer +//= require react-components //= require_directory . //= require_directory ./dialog //= require_directory ./wizard diff --git a/web/app/assets/javascripts/backend_alerts.js b/web/app/assets/javascripts/backend_alerts.js index da48d9138..348560630 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, backend_detail:text, help: alert.help}) + } + else { + logger.debug("Unhandled Backend Event type %o, data %o", type, text) + } + } function alertCallback(type, text) { @@ -77,8 +73,11 @@ } if (type === 2) { // BACKEND_MIXER_CHANGE - if(context.JK.CurrentSessionModel) - context.JK.CurrentSessionModel.onBackendMixerChanged(type, text) + + context.MixerActions.mixersChanged(type, text) + + //if(context.JK.CurrentSessionModel) + // context.JK.CurrentSessionModel.onBackendMixerChanged(type, text) } else if (type === ALERT_NAMES.NO_VALID_AUDIO_CONFIG) { // NO_VALID_AUDIO_CONFIG if(context.JK.GearUtilsInstance && context.JK.GearUtilsInstance.isRestartingAudio()) { @@ -101,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/band_setup.js b/web/app/assets/javascripts/band_setup.js index 4e09a8e47..50f41bdf9 100644 --- a/web/app/assets/javascripts/band_setup.js +++ b/web/app/assets/javascripts/band_setup.js @@ -151,8 +151,8 @@ $("#play-commitment").val('1') - $("#hourly-rate").val("0.0") - $("#gig-minimum").val("0.0") + $screen.find("#hourly-rate").val("0") + $screen.find("#gig-minimum").val("0") resetGenres(); renderDesiredExperienceLabel([]) @@ -225,9 +225,14 @@ band.free_gigs=$('input[name="free_gigs"]:checked').val()=="yes" band.touring_option=$('#touring-option').val()=="yes" - band.play_commitment=$("#play-commitment").val() - band.hourly_rate=$("#hourly-rate").val() - band.gig_minimum=$("#gig-minimum").val() + + if ($screen.find("#play-commitment").length == 0) { + logger.error("MISSING PLAY MOTIMMENT") + } + + band.play_commitment = $screen.find("#play-commitment").val() + band.hourly_rate = profileUtils.normalizeMoneyForSubmit($screen.find("#hourly-rate").val()) + band.gig_minimum = profileUtils.normalizeMoneyForSubmit($("#gig-minimum").val()) if (currentStep==GENRE_STEP) { band.genres = getSelectedGenres(); @@ -424,8 +429,8 @@ $('#touring-option').val(band.touring_option ? 'yes' : 'no') $("#play-commitment").val(band.play_commitment) - $("#hourly-rate").val(band.hourly_rate) - $("#gig-minimum").val(band.gig_minimum) + $("#hourly-rate").val(profileUtils.normalizeMoneyForDisplay(band.hourly_rate)) + $("#gig-minimum").val(profileUtils.normalizeMoneyForDisplay(band.gig_minimum)) // Initialize avatar if (band.photo_url) { 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/client_init.js.coffee b/web/app/assets/javascripts/client_init.js.coffee index 9da3d933e..659e5a24d 100644 --- a/web/app/assets/javascripts/client_init.js.coffee +++ b/web/app/assets/javascripts/client_init.js.coffee @@ -3,7 +3,7 @@ $ = jQuery context = window context.JK ||= {}; -broadcastActions = context.JK.Actions.Broadcast +broadcastActions = @BroadcastActions context.JK.ClientInit = class ClientInit constructor: () -> @@ -21,7 +21,10 @@ context.JK.ClientInit = class ClientInit this.watchBroadcast() checkBroadcast: () => - broadcastActions.load.triggerPromise() + broadcastActions.load.triggerPromise().catch(() -> + false + ) + watchBroadcast: () => if context.JK.currentUserId diff --git a/web/app/assets/javascripts/dialog/configureTrackDialog.js b/web/app/assets/javascripts/dialog/configureTrackDialog.js index e4174df90..20801cfa6 100644 --- a/web/app/assets/javascripts/dialog/configureTrackDialog.js +++ b/web/app/assets/javascripts/dialog/configureTrackDialog.js @@ -112,10 +112,14 @@ $voiceChatTabSelector.click(function () { // validate audio settings if (validateAudioSettings()) { + logger.debug("initializing voice chat helper") configureTracksHelper.reset(); voiceChatHelper.reset(); showVoiceChatPanel(); } + else { + logger.debug("invalid audio settings; ignoring") + } }); $btnCancel.click(function() { 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/dialog/recordingFinishedDialog.js b/web/app/assets/javascripts/dialog/recordingFinishedDialog.js index 6152927a2..cceaaedc2 100644 --- a/web/app/assets/javascripts/dialog/recordingFinishedDialog.js +++ b/web/app/assets/javascripts/dialog/recordingFinishedDialog.js @@ -57,7 +57,7 @@ }); } else if (localResult.aggregate_state == 'PARTIALLY_MISSING') { - logger.error("unable to open recording due to some missing tracks: %o", localResults); + logger.error("unable to open recording due to some missing tracks: %o", recording, localResults); app.notify({ title: "Unable to Open Recording for Playback", text: "Some of your tracks associated with the recording are missing. This is a bug in the application.", diff --git a/web/app/assets/javascripts/dialog/recordingSelectorDialog.js b/web/app/assets/javascripts/dialog/recordingSelectorDialog.js index cc9e5dcf9..10e76595b 100644 --- a/web/app/assets/javascripts/dialog/recordingSelectorDialog.js +++ b/web/app/assets/javascripts/dialog/recordingSelectorDialog.js @@ -2,7 +2,7 @@ "use strict"; context.JK = context.JK || {}; - context.JK.RecordingSelectorDialog = function(app, recordings, selectedRecordings, selectCallback) { + context.JK.RecordingSelectorDialog = function(app, selectedRecordings, selectCallback) { var logger = context.JK.logger; var rest = context.JK.Rest(); var recordingUtils = context.JK.RecordingUtils; @@ -10,173 +10,26 @@ var dialogId = 'recording-selector-dialog'; var $screen = $('#' + dialogId); var $btnSelect = $screen.find(".btn-select-recordings"); - var $instructions = $screen.find('#instructions'); var $recordings = $screen.find('.recordings'); + var $paginatorHolder = null; var feedHelper = new context.JK.Feed(app); + var $scroller = $recordings; + var $content = $recordings; + var $noMoreFeeds = $screen.find('.end-of-list'); + var $empty = $(); + feedHelper.initialize($screen, $scroller, $content, $noMoreFeeds, $empty, $empty, $empty, $empty, {sort: 'date', time_range: 'all', type: 'recording', show_checkbox: true, hide_avatar: true}); function beforeShow(data) { + } function afterShow(data) { - $recordings.empty(); - - $.each(recordings, function(index, val) { - bindRecordingItem(val); - }); + feedHelper.setUser(context.JK.currentUserId) + feedHelper.refresh() // hide the avatars - $screen.find('.avatar-small.ib').hide(); - } - - /********* THE FOLLOWING BLOCK IS REPEATED IN feedHelper.js **********/ - function startRecordingPlay($feedItem) { - var img = $('.play-icon', $feedItem); - var $controls = $feedItem.find('.recording-controls'); - img.attr('src', '/assets/content/icon_pausebutton.png'); - $controls.trigger('play.listenRecording'); - $feedItem.data('playing', true); - } - - function stopRecordingPlay($feedItem) { - var img = $('.play-icon', $feedItem); - var $controls = $feedItem.find('.recording-controls'); - img.attr('src', '/assets/content/icon_playbutton.png'); - $controls.trigger('pause.listenRecording'); - $feedItem.data('playing', false); - } - - function toggleRecordingPlay() { - - var $playLink = $(this); - var $feedItem = $playLink.closest('.feed-entry'); - var playing = $feedItem.data('playing'); - - if(playing) { - stopRecordingPlay($feedItem); - } - else { - startRecordingPlay($feedItem); - } - return false; - } - - function toggleRecordingDetails() { - var $detailsLink = $(this); - var $feedItem = $detailsLink.closest('.feed-entry'); - var $musicians = $feedItem.find('.musician-detail'); - var $description = $feedItem.find('.description'); - var $name = $feedItem.find('.name'); - var toggledOpen = $detailsLink.data('toggledOpen'); - - if(toggledOpen) { - toggleClose($feedItem, $name, $description, $musicians) - } - else { - toggleOpen($feedItem, $name, $description, $musicians) - } - - toggledOpen = !toggledOpen; - $detailsLink.data('toggledOpen', toggledOpen); - - return false; - } - - function stateChangeRecording(e, data) { - var $controls = data.element; - var $feedItem = $controls.closest('.feed-entry'); - - var $sliderBar = $('.recording-position', $feedItem); - var $statusBar = $('.recording-status', $feedItem); - var $currentTime = $('.recording-current', $feedItem); - var $status = $('.status-text', $feedItem); - var $playButton = $('.play-button', $feedItem); - - if(data.isEnd) stopRecordingPlay($feedItem); - if(data.isError) { - $sliderBar.hide(); - $playButton.hide(); - $currentTime.hide(); - $statusBar.show(); - $status.text(data.displayText); - } - } - - function toggleOpen($feedItem, $name, $description, $musicians) { - $description.trigger('destroy.dot'); - $description.data('original-height', $description.css('height')).css('height', 'auto'); - $name.trigger('destroy.dot'); - $name.data('original-height', $name.css('height')).css('height', 'auto'); - $musicians.show(); - $feedItem.animate({'max-height': '1000px'}); - } - - function toggleClose($feedItem, $name, $description, $musicians, immediate) { - $feedItem.css('height', $feedItem.height() + 'px') - $feedItem.animate({'height': $feedItem.data('original-max-height')}, immediate ? 0 : 400).promise().done(function() { - $feedItem.css('height', 'auto').css('max-height', $feedItem.data('original-max-height')); - - $musicians.hide(); - $description.css('height', $description.data('original-height')); - $description.dotdotdot(); - $name.css('height', $name.data('original-height')); - $name.dotdotdot(); - }); - } - /**********************************************************/ - - function bindRecordingItem(claimedRecording) { - claimedRecording.recording.mix_info = recordingUtils.createMixInfo({state: claimedRecording.recording.mix_state}); - var options = { - feed_item: claimedRecording.recording, - candidate_claimed_recording: claimedRecording, - mix_class: claimedRecording['has_mix?'] ? 'has-mix' : 'no-mix', - }; - - var $feedItem = $(context._.template($('#template-feed-recording').html(), options, {variable: 'data'})); - var $controls = $feedItem.find('.recording-controls'); - - var $titleText = $feedItem.find('.title .title-text'); - - // if this item will be discarded, tack on a * to the RECORDING NAME - var discardTime = claimedRecording.recording['when_will_be_discarded?']; - if(discardTime) { - context.JK.helpBubble($titleText, 'recording-discarded-soon', {discardTime: discardTime}, {}); - $titleText.text($titleText.text() + '*'); - } - - $controls.data('mix-state', claimedRecording.recording.mix_info); // for recordingUtils helper methods - $controls.data('server-info', claimedRecording.recording.mix); // for recordingUtils helper methods - $controls.data('view-context', 'feed'); - - $('.timeago', $feedItem).timeago(); - context.JK.prettyPrintElements($('.duration', $feedItem)); - context.JK.setInstrumentAssetPath($('.instrument-icon', $feedItem)); - $('.details', $feedItem).click(toggleRecordingDetails); - $('.details-arrow', $feedItem).click(toggleRecordingDetails); - $('.play-button', $feedItem).click(toggleRecordingPlay); - - var checked = ''; - - var match = $.grep(selectedRecordings, function(obj, index) { - return obj.claimed_recording_id === claimedRecording.id; - }); - - if (match && match.length > 0) { - checked = 'checked'; - } - - // put the item on the page - $recordings.append("
"); - $recordings.append($feedItem); - - // these routines need the item to have height to work (must be after renderFeed) - $controls.listenRecording({recordingId: claimedRecording.recording.id, claimedRecordingId: options.candidate_claimed_recording.id, sliderSelector:'.recording-slider', sliderBarSelector: '.recording-playback', currentTimeSelector:'.recording-current'}); - $controls.bind('statechange.listenRecording', stateChangeRecording); - $('.dotdotdot', $feedItem).dotdotdot(); - $feedItem.data('original-max-height', $feedItem.css('height')); - context.JK.bindHoverEvents($feedItem); - context.JK.bindProfileClickEvents($feedItem); + //$screen.find('.avatar-small.ib').hide(); } function afterHide() { @@ -187,10 +40,10 @@ } function events() { - $btnSelect.click(function(evt) { + $btnSelect.off('click').on('click', function(evt) { evt.preventDefault(); var preSelectedRecordings = []; - $recordings.find('input[type=checkbox]:checked').each(function(index) { + $recordings.find('.select-box input[type=checkbox]:checked').each(function(index) { preSelectedRecordings.push({ "id": $(this).attr('data-recording-id'), "name": $(this).attr('data-recording-title') @@ -198,6 +51,7 @@ }); if (selectCallback) { + console.log("calling selectCallback", preSelectedRecordings) selectCallback(preSelectedRecordings); } @@ -217,8 +71,6 @@ app.bindDialog(dialogId, dialogBindings); - $instructions.html('Select one or more recordings and click ADD to add JamKazam recordings to your performance samples.'); - events(); } diff --git a/web/app/assets/javascripts/dialog/sessionMasterMixDialog.js.coffee b/web/app/assets/javascripts/dialog/sessionMasterMixDialog.js.coffee new file mode 100644 index 000000000..781031b95 --- /dev/null +++ b/web/app/assets/javascripts/dialog/sessionMasterMixDialog.js.coffee @@ -0,0 +1,36 @@ +$ = jQuery +context = window +context.JK ||= {} +MIX_MODES = context.JK.MIX_MODES + +context.JK.SessionMasterMixDialog = class SessionMasterMixDialog + constructor: (@app) -> + @rest = context.JK.Rest() + @logger = context.JK.logger + @screen = null + @dialogId = 'session-master-mix-dialog' + @dialog = null + @closeBtn = null + + initialize:() => + dialogBindings = + 'beforeShow' : @beforeShow + 'afterShow' : @afterShow + 'afterHide' : @afterHide + + + @dialog = $('[layout-id="' + @dialogId + '"]') + @app.bindDialog(@dialogId, dialogBindings) + @content = @dialog.find(".dialog-inner") + + beforeShow:() => + @logger.debug("session-master-mix-dlg: beforeShow") + context.jamClient.SetMixerMode(MIX_MODES.MASTER) + + afterShow:() => + @logger.debug("session-master-mix-dlg: afterShow") + + afterHide:() => + context.jamClient.SetMixerMode(MIX_MODES.PERSONAL) + + diff --git a/web/app/assets/javascripts/dialog/sessionSettingsDialog.js b/web/app/assets/javascripts/dialog/sessionSettingsDialog.js index 0c47eb20e..960d44cfd 100644 --- a/web/app/assets/javascripts/dialog/sessionSettingsDialog.js +++ b/web/app/assets/javascripts/dialog/sessionSettingsDialog.js @@ -1,16 +1,17 @@ (function(context,$) { context.JK = context.JK || {}; - context.JK.SessionSettingsDialog = function(app, sessionScreen) { + context.JK.SessionSettingsDialog = function(app) { var logger = context.JK.logger; var gearUtils = context.JK.GearUtilsInstance; var $dialog; var $screen = $('#session-settings'); - var $selectedFilenames = $screen.find('#selected-filenames'); - var $uploadSpinner = $screen.find('.upload-spinner'); - var $selectedFilenames = $('#settings-selected-filenames'); + //var $selectedFilenames = $screen.find('#selected-filenames'); + var $uploadSpinner = $screen.find('.spinner-small'); + //var $selectedFilenames = $('#settings-selected-filenames'); var $inputFiles = $screen.find('#session-select-files'); var $btnSelectFiles = $screen.find('.btn-select-files'); + var $inputBox = $screen.find('.inputbox') var rest = new JK.Rest(); var sessionId; @@ -21,7 +22,7 @@ context.JK.GenreSelectorHelper.render('#session-settings-genre'); $dialog = $('[layout-id="session-settings"]'); - var currentSession = sessionScreen.getCurrentSession(); + var currentSession = context.SessionStore.currentSession; sessionId = currentSession.id; // id @@ -65,13 +66,21 @@ $('#session-settings-fan-access').val('listen-chat-band'); } - // notation files + /** + // notation files in the account screen. ugh. $selectedFilenames.empty(); for (var i=0; i < currentSession.music_notations.length; i++) { var notation = currentSession.music_notations[i]; $selectedFilenames.append('' + notation.file_name + ' '); + }*/ + + $inputBox.empty(); + for (var i=0; i < currentSession.music_notations.length; i++) { + var notation = currentSession.music_notations[i]; + addNotation(notation) } + context.JK.dropdown($('#session-settings-language')); context.JK.dropdown($('#session-settings-musician-access')); context.JK.dropdown($('#session-settings-fan-access')); @@ -81,6 +90,29 @@ $('#session-settings-fan-access').easyDropDown(easyDropDownState) } + function addNotation(notation) { + + var $notation = $('
' + notation.file_name + '
X
') + $notation.find('a').on('click', function(e) { + + if($(this).attr('data-deleting')) { + // ignore duplicate delete attempts + return false; + } + + $(this).attr('data-deleting', true) + var $notationEntry = $(this).closest('.notation-entry').find('div').text('deleting...') + + rest.deleteMusicNotation({id: notation.id}) + .done(function() { + $notation.remove() + }) + .fail(app.ajaxError) + return false; + }) + $inputBox.append($notation); + } + function saveSettings(evt) { var data = {}; @@ -111,16 +143,14 @@ data.fan_access = false; data.fan_chat = false; } - else if (fanAccess == 'listen-chat-each') { - data.fan_access = true; - data.fan_chat = false; - } - else if (fanAccess == 'listen-chat-band') { + else if (fanAccess == 'listen-chat') { data.fan_access = true; data.fan_chat = true; } rest.updateSession($('#session-settings-id').val(), data).done(settingsSaved); + + return false; } function uploadNotations(notations) { @@ -177,7 +207,7 @@ } }) .always(function() { - $btnSelectFiles.text('SELECT FILES...').data('uploading', null) + $btnSelectFiles.text('ADD FILES...').data('uploading', null) $uploadSpinner.hide(); }); } @@ -203,10 +233,9 @@ else { // upload as soon as user picks their files. uploadNotations($inputFiles.get(0).files) - .done(function() { - context._.each(fileNames, function(fileName) { - var $text = $('').text(fileName); - $selectedFilenames.append($text); + .done(function(response) { + context._.each(response, function(notation) { + addNotation(notation) }) }) } @@ -225,13 +254,13 @@ function settingsSaved(response) { // No response returned from this call. 204. - sessionScreen.refreshCurrentSession(true); + context.SessionActions.syncWithServer() app.layout.closeDialog('session-settings'); } function events() { $('#session-settings-dialog-submit').on('click', saveSettings); - + $('#session-settings-dialog').on('submit', saveSettings) $inputFiles.on('change', changeSelectedFiles); $btnSelectFiles.on('click', toggleSelectFiles); } diff --git a/web/app/assets/javascripts/dialog/soundCloudPlayerDialog.js.coffee b/web/app/assets/javascripts/dialog/soundCloudPlayerDialog.js.coffee index 29d2670bf..14fbdd785 100644 --- a/web/app/assets/javascripts/dialog/soundCloudPlayerDialog.js.coffee +++ b/web/app/assets/javascripts/dialog/soundCloudPlayerDialog.js.coffee @@ -15,7 +15,8 @@ context.JK.SoundCloudPlayerDialog = class SoundCloudPlayerDialog initialize:(@url, @caption) => dialogBindings = { 'beforeShow' : @beforeShow, - 'afterShow' : @afterShow + 'afterShow' : @afterShow, + 'afterHide' : @afterHide } @dialog = $('[layout-id="' + @dialogId + '"]') @@ -27,9 +28,15 @@ context.JK.SoundCloudPlayerDialog = class SoundCloudPlayerDialog beforeShow:() => @player.addClass("hidden") @player.attr("src", "") - u = encodeURIComponent(@url) - src = "https://w.soundcloud.com/player/?url=#{u}&auto_play=true&hide_related=false&show_comments=true&show_user=true&show_reposts=false&visual=true&loop=true" - @player.attr("src", src) + + # the Windows client does not play back correctly + if context.jamClient.IsNativeClient() + context.JK.popExternalLink(@url) + return false + else + u = encodeURIComponent(@url) + src = "https://w.soundcloud.com/player/?url=#{u}&auto_play=true&hide_related=false&show_comments=true&show_user=true&show_reposts=false&visual=true&loop=true" + @player.attr("src", src) afterShow:() => @player.removeClass("hidden") @@ -37,4 +44,7 @@ context.JK.SoundCloudPlayerDialog = class SoundCloudPlayerDialog showDialog:() => @app.layout.showDialog(@dialogId) + afterHide: () => + @player.attr('src', '') + \ No newline at end of file diff --git a/web/app/assets/javascripts/faderHelpers.js b/web/app/assets/javascripts/faderHelpers.js index ba66a44ca..6d573a2c8 100644 --- a/web/app/assets/javascripts/faderHelpers.js +++ b/web/app/assets/javascripts/faderHelpers.js @@ -11,6 +11,7 @@ var $draggingFaderHandle = null; var $draggingFader = null; + var $floater = null; var draggingOrientation = null; var logger = g.JK.logger; @@ -20,6 +21,7 @@ e.stopPropagation(); var $fader = $(this); + var floaterConvert = $fader.data('floaterConverter') var sessionModel = window.JK.CurrentSessionModel || null; var mediaControlsDisabled = $fader.data('media-controls-disabled'); @@ -43,7 +45,7 @@ } } - draggingOrientation = $fader.attr('orientation'); + draggingOrientation = $fader.attr('data-orientation'); var offset = $fader.offset(); var position = { top: e.pageY - offset.top, left: e.pageX - offset.left} @@ -53,6 +55,10 @@ return false; } + if(floaterConvert) { + window.JK.FaderHelpers.setFloaterValue($fader.find('.floater'), floaterConvert(faderPct)) + } + $fader.parent().triggerHandler('fader_change', {percentage: faderPct, dragging: false}) setHandlePosition($fader, faderPct); @@ -61,9 +67,9 @@ function setHandlePosition($fader, value) { var ratio, position; - var $handle = $fader.find('div[control="fader-handle"]'); + var $handle = $fader.find('div[data-control="fader-handle"]'); - var orientation = $fader.attr('orientation'); + var orientation = $fader.attr('data-orientation'); var handleCssAttribute = getHandleCssAttribute($fader); // required because this method is entered directly when from a callback @@ -81,7 +87,7 @@ } function faderValue($fader, e, offset) { - var orientation = $fader.attr('orientation'); + var orientation = $fader.attr('data-orientation'); var getPercentFunction = getVerticalFaderPercent; var relativePosition = offset.top; if (orientation && orientation == 'horizontal') { @@ -92,7 +98,7 @@ } function getHandleCssAttribute($fader) { - var orientation = $fader.attr('orientation'); + var orientation = $fader.attr('data-orientation'); return (orientation === 'horizontal') ? 'left' : 'top'; } @@ -134,12 +140,34 @@ return false; } + // simple snap feature to stick to the mid point + if(faderPct > 46 && faderPct < 54 && $draggingFader.data('snap')) { + faderPct = 50 + var orientation = $draggingFader.attr('data-orientation'); + if(orientation == 'horizontal') { + var width = $draggingFader.width() + var left = width / 2 + ui.position.left = left + } + else { + var height = $draggingFader.height() + var top = height / 2 + ui.position.top = top + } + } + + var floaterConvert = $draggingFaderHandle.data('floaterConverter') + + if(floaterConvert && $floater) { + window.JK.FaderHelpers.setFloaterValue($floater, floaterConvert(faderPct)) + } $draggingFader.parent().triggerHandler('fader_change', {percentage: faderPct, dragging: true}) } function onFaderDragStart(e, ui) { $draggingFaderHandle = $(this); - $draggingFader = $draggingFaderHandle.closest('div[control="fader"]'); + $draggingFader = $draggingFaderHandle.closest('div[data-control="fader"]'); + $floater = $draggingFaderHandle.find('.floater') draggingOrientation = $draggingFader.attr('orientation'); var mediaControlsDisabled = $draggingFaderHandle.data('media-controls-disabled'); @@ -210,12 +238,12 @@ selector.html(g._.template(templateSource, options)); - selector.find('div[control="fader"]') + selector.find('div[data-control="fader"]') .data('media-controls-disabled', selector.data('media-controls-disabled')) .data('media-track-opener', selector.data('media-track-opener')) .data('showHelpAboutMediaMixers', selector.data('showHelpAboutMediaMixers')) - selector.find('div[control="fader-handle"]').draggable({ + selector.find('div[data-control="fader-handle"]').draggable({ drag: onFaderDrag, start: onFaderDragStart, stop: onFaderDragStop, @@ -233,6 +261,43 @@ } }, + renderFader2: function (selector, userOptions, floaterConverter) { + selector = $(selector); + if (userOptions === undefined) { + throw ("renderFader: userOptions is required"); + } + var renderDefaults = { + faderType: "vertical" + }; + var options = $.extend({}, renderDefaults, userOptions); + + selector.find('div[data-control="fader"]') + .data('media-controls-disabled', selector.data('media-controls-disabled')) + .data('media-track-opener', selector.data('media-track-opener')) + .data('showHelpAboutMediaMixers', selector.data('showHelpAboutMediaMixers')) + .data('floaterConverter', floaterConverter) + .data('snap', userOptions.snap) + + selector.find('div[data-control="fader-handle"]').draggable({ + drag: onFaderDrag, + start: onFaderDragStart, + stop: onFaderDragStop, + containment: "parent", + axis: options.faderType === 'horizontal' ? 'x' : 'y' + }).data('media-controls-disabled', selector.data('media-controls-disabled')) + .data('media-track-opener', selector.data('media-track-opener')) + .data('showHelpAboutMediaMixers', selector.data('showHelpAboutMediaMixers')) + .data('floaterConverter', floaterConverter) + .data('snap', userOptions.snap) + + // Embed any custom styles, applied to the .fader below selector + if ("style" in options) { + for (var key in options.style) { + selector.find(' .fader').css(key, options.style[key]); + } + } + }, + convertLinearToDb: function (input) { // deal with extremes better @@ -263,27 +328,48 @@ // composite function resembling audio taper if (input <= 1) { return -80; } - if (input <= 28) { return (2 * input - 80); } - if (input <= 79) { return (0.5 * input - 38); } - if (input < 99) { return (0.875 * input - 67.5); } + if (input <= 28) { return Math.round((2 * input - 80)); } // -78 to -24 db + if (input <= 79) { return Math.round((0.5 * input - 38)); } // -24 to 1.5 db + if (input < 99) { return Math.round((0.875 * input - 67.5)); } // 1.625 - 19.125 db if (input >= 99) { return 20; } }, + convertAudioTaperToPercent: function(db) { + if(db <= -78) { return 0} + if(db <= -24) { return (db + 80) / 2 } + if(db <= 1.5) { return (db + 38) / .5 } + if(db <= 19.125) { return (db + 67.5) / 0.875 } + return 100; + }, - setFaderValue: function (faderId, faderValue) { - var $fader = $('[fader-id="' + faderId + '"]'); + + setFaderValue: function (faderId, faderValue, floaterValue) { + var $fader = $('[data-fader-id="' + faderId + '"]'); this.setHandlePosition($fader, faderValue); + if(floaterValue !== undefined) { + var $floater = $fader.find('.floater') + this.setFloaterValue($floater, floaterValue) + } + }, + + showFader: function(faderId) { + var $fader = $('[data-fader-id="' + faderId + '"]'); + $fader.find('div[data-control="fader-handle"]').show() }, setHandlePosition: function ($fader, faderValue) { - draggingOrientation = $fader.attr('orientation'); + draggingOrientation = $fader.attr('data-orientation'); setHandlePosition($fader, faderValue); draggingOrientation = null; }, + setFloaterValue: function($floater, floaterValue) { + $floater.text(floaterValue) + }, + initialize: function () { - $('body').on('click', 'div[control="fader"]', faderClick); + $('body').on('click', 'div[data-control="fader"]', faderClick); } }; diff --git a/web/app/assets/javascripts/fakeJamClient.js b/web/app/assets/javascripts/fakeJamClient.js index 8347edae5..81826b5eb 100644 --- a/web/app/assets/javascripts/fakeJamClient.js +++ b/web/app/assets/javascripts/fakeJamClient.js @@ -4,7 +4,7 @@ context.JK = context.JK || {}; context.JK.FakeJamClient = function(app, p2pMessageFactory) { - var ChannelGroupIds = context.JK.ChannelGroupIds; + var ChannelGroupIds = context.JK.ChannelGroupIds var logger = context.JK.logger; logger.info("*** Fake JamClient instance initialized. ***"); @@ -170,22 +170,22 @@ function FTUEGetMusicInputs() { dbg('FTUEGetMusicInputs'); return { - "i~11~MultiChannel (FW AP Multi)~0^i~11~Multichannel (FW AP Multi)~1": - "Multichannel (FW AP Multi) - Channel 1/Multichannel (FW AP Multi) - Channel 2" + "i~11~MultiChannel (FWAPMulti)~0^i~11~Multichannel (FWAPMulti)~1": + "Multichannel (FWAPMulti) - Channel 1/Multichannel (FWAPMulti) - Channel 2" }; } function FTUEGetMusicOutputs() { dbg('FTUEGetMusicOutputs'); return { - "o~11~Multichannel (FW AP Multi)~0^o~11~Multichannel (FW AP Multi)~1": - "Multichannel (FW AP Multi) - Channel 1/Multichannel (FW AP Multi) - Channel 2" + "o~11~Multichannel (FWAPMulti)~0^o~11~Multichannel (FWAPMulti)~1": + "Multichannel (FWAPMulti) - Channel 1/Multichannel (FWAPMulti) - Channel 2" }; } function FTUEGetChatInputs() { dbg('FTUEGetChatInputs'); return { - "i~11~MultiChannel (FW AP Multi)~0^i~11~Multichannel (FW AP Multi)~1": - "Multichannel (FW AP Multi) - Channel 1/Multichannel (FW AP Multi) - Channel 2" + "i~11~MultiChannel (FWAPMulti)~0^i~11~Multichannel (FWAPMulti)~1": + "Multichannel (FWAPMulti) - Channel 1/Multichannel (FWAPMulti) - Channel 2" }; } function FTUEGetChannels() { @@ -450,7 +450,7 @@ } function GetASIODevices() { - var response =[{"device_id":0,"device_name":"Realtek High Definition Audio","device_type": 0,"interfaces":[{"interface_id":0,"interface_name":"Realtek HDA SPDIF Out","pins":[{"is_input":false,"pin_id":0,"pin_name":"PC Speaker"}]},{"interface_id":1,"interface_name":"Realtek HD Audio rear output","pins":[{"is_input":false,"pin_id":0,"pin_name":"PC Speaker"}]},{"interface_id":2,"interface_name":"Realtek HD Audio Mic input","pins":[{"is_input":true,"pin_id":0,"pin_name":"Recording Control"}]},{"interface_id":3,"interface_name":"Realtek HD Audio Line input","pins":[{"is_input":true,"pin_id":0,"pin_name":"Recording Control"}]},{"interface_id":4,"interface_name":"Realtek HD Digital input","pins":[{"is_input":true,"pin_id":0,"pin_name":"Capture"}]},{"interface_id":5,"interface_name":"Realtek HD Audio Stereo input","pins":[{"is_input":true,"pin_id":0,"pin_name":"Recording Control"}]}],"wavert_supported":false},{"device_id":1,"device_name":"M-Audio FW Audiophile","device_type": 1,"interfaces":[{"interface_id":0,"interface_name":"FW AP Multi","pins":[{"is_input":false,"pin_id":0,"pin_name":"Output"},{"is_input":true,"pin_id":1,"pin_name":"Input"}]},{"interface_id":1,"interface_name":"FW AP 1/2","pins":[{"is_input":false,"pin_id":0,"pin_name":"Output"},{"is_input":true,"pin_id":1,"pin_name":"Input"}]},{"interface_id":2,"interface_name":"FW AP SPDIF","pins":[{"is_input":false,"pin_id":0,"pin_name":"Output"},{"is_input":true,"pin_id":1,"pin_name":"Input"}]},{"interface_id":3,"interface_name":"FW AP 3/4","pins":[{"is_input":false,"pin_id":0,"pin_name":"Output"}]}],"wavert_supported":false},{"device_id":2,"device_name":"Virtual Audio Cable","device_type": 2,"interfaces":[{"interface_id":0,"interface_name":"Virtual Cable 2","pins":[{"is_input":true,"pin_id":0,"pin_name":"Capture"},{"is_input":false,"pin_id":1,"pin_name":"Output"}]},{"interface_id":1,"interface_name":"Virtual Cable 1","pins":[{"is_input":true,"pin_id":0,"pin_name":"Capture"},{"is_input":false,"pin_id":1,"pin_name":"Output"}]}],"wavert_supported":false},{"device_id":3,"device_name":"WebCamDV WDM Audio Capture","device_type": 3,"interfaces":[{"interface_id":0,"interface_name":"WebCamDV Audio","pins":[{"is_input":true,"pin_id":0,"pin_name":"Recording Control"},{"is_input":false,"pin_id":1,"pin_name":"Volume Control"}]}],"wavert_supported":false}]; + var response =[{"device_id":0,"device_name":"Realtek High Definition Audio","device_type": 0,"interfaces":[{"interface_id":0,"interface_name":"Realtek HDA SPDIF Out","pins":[{"is_input":false,"pin_id":0,"pin_name":"PC Speaker"}]},{"interface_id":1,"interface_name":"Realtek HD Audio rear output","pins":[{"is_input":false,"pin_id":0,"pin_name":"PC Speaker"}]},{"interface_id":2,"interface_name":"Realtek HD Audio Mic input","pins":[{"is_input":true,"pin_id":0,"pin_name":"Recording Control"}]},{"interface_id":3,"interface_name":"Realtek HD Audio Line input","pins":[{"is_input":true,"pin_id":0,"pin_name":"Recording Control"}]},{"interface_id":4,"interface_name":"Realtek HD Digital input","pins":[{"is_input":true,"pin_id":0,"pin_name":"Capture"}]},{"interface_id":5,"interface_name":"Realtek HD Audio Stereo input","pins":[{"is_input":true,"pin_id":0,"pin_name":"Recording Control"}]}],"wavert_supported":false},{"device_id":1,"device_name":"M-Audio FW Audiophile","device_type": 1,"interfaces":[{"interface_id":0,"interface_name":"FWAPMulti","pins":[{"is_input":false,"pin_id":0,"pin_name":"Output"},{"is_input":true,"pin_id":1,"pin_name":"Input"}]},{"interface_id":1,"interface_name":"FW AP 1/2","pins":[{"is_input":false,"pin_id":0,"pin_name":"Output"},{"is_input":true,"pin_id":1,"pin_name":"Input"}]},{"interface_id":2,"interface_name":"FW AP SPDIF","pins":[{"is_input":false,"pin_id":0,"pin_name":"Output"},{"is_input":true,"pin_id":1,"pin_name":"Input"}]},{"interface_id":3,"interface_name":"FW AP 3/4","pins":[{"is_input":false,"pin_id":0,"pin_name":"Output"}]}],"wavert_supported":false},{"device_id":2,"device_name":"Virtual Audio Cable","device_type": 2,"interfaces":[{"interface_id":0,"interface_name":"Virtual Cable 2","pins":[{"is_input":true,"pin_id":0,"pin_name":"Capture"},{"is_input":false,"pin_id":1,"pin_name":"Output"}]},{"interface_id":1,"interface_name":"Virtual Cable 1","pins":[{"is_input":true,"pin_id":0,"pin_name":"Capture"},{"is_input":false,"pin_id":1,"pin_name":"Output"}]}],"wavert_supported":false},{"device_id":3,"device_name":"WebCamDV WDM Audio Capture","device_type": 3,"interfaces":[{"interface_id":0,"interface_name":"WebCamDV Audio","pins":[{"is_input":true,"pin_id":0,"pin_name":"Recording Control"},{"is_input":false,"pin_id":1,"pin_name":"Volume Control"}]}],"wavert_supported":false}]; return response; } @@ -475,8 +475,8 @@ } function SessionGetControlState(mixerIds, isMasterOrPersonal) { dbg("SessionGetControlState"); - var groups = [ - ChannelGroupIds.MasterGroup, + var groups = + [ChannelGroupIds.MasterGroup, ChannelGroupIds.MonitorGroup, ChannelGroupIds.AudioInputMusicGroup, ChannelGroupIds.AudioInputChatGroup, @@ -485,13 +485,12 @@ ChannelGroupIds.UserChatInputGroup, ChannelGroupIds.PeerMediaTrackGroup, ChannelGroupIds.JamTrackGroup, - ChannelGroupIds.MetronomeGroup - ] + ChannelGroupIds.MetronomeGroup]; var names = [ - "FW AP Multi", - "FW AP Multi", - "FW AP Multi", - "FW AP Multi", + "FWAPMulti", + "FWAPMulti", + "FWAPMulti", + "FWAPMulti", "", "", "", @@ -545,6 +544,7 @@ stereo: true, volume_left: -40, volume_right:-40, + pan: 0, instrument_id:50, // see globals.js mode: isMasterOrPersonal, rid: mixerIds[i] @@ -554,10 +554,10 @@ } function SessionGetIDs() { return [ - "FW AP Multi_0_10000", - "FW AP Multi_1_10100", - "FW AP Multi_2_10200", - "FW AP Multi_3_10500", + "FWAPMulti_0_10000", + "FWAPMulti_1_10100", + "FWAPMulti_2_10200", + "FWAPMulti_3_10500", "User@208.191.152.98#", "User@208.191.152.98_*" ]; @@ -624,9 +624,9 @@ function doCallbacks() { var names = ["vu"]; - //var ids = ["FW AP Multi_2_10200", "FW AP Multi_0_10000"]; - var ids= ["i~11~MultiChannel (FW AP Multi)~0^i~11~Multichannel (FW AP Multi)~1", - "i~11~MultiChannel (FW AP Multi)~0^i~11~Multichannel (FW AP Multi)~2"]; + //var ids = ["FWAPMulti_2_10200", "FWAPMulti_0_10000"]; + var ids= ["i~11~MultiChannel (FWAPMulti)~0^i~11~Multichannel (FWAPMulti)~1", + "i~11~MultiChannel (FWAPMulti)~0^i~11~Multichannel (FWAPMulti)~2"]; var args = []; for (var i=0; iclick here."}, // PACKET_JTR, - 4: {"title": "High Packet Loss", "message": "Your network connection is currently experiencing packet loss at a rate that is too high to deliver good audio quality. For troubleshooting tips, click here." }, // PACKET_LOSS - 5: {"title": "High Packet Late", "message": "Your network connection is currently experiencing packet loss at a rate that is too high to deliver good audio quality. For troubleshooting tips, click here."}, // PACKET_LATE, - 6: {"title": "Large Jitter Queue", "message": "Your network connection is currently experiencing packet jitter at a level that is too high to deliver good audio quality. For troubleshooting tips, click here."}, // JTR_QUEUE_DEPTH, - 7: {"title": "High Network Jitter", "message": "Your network connection is currently experiencing network jitter at a level that is too high to deliver good audio quality. For troubleshooting tips, click here."}, // NETWORK_JTR, - 8: {"title": "High Session Latency", "message": "The latency of your audio device combined with your Internet connection has become high enough to impact your session quality. For troubleshooting tips, click here." }, // NETWORK_PING, - 9: {"title": "Bandwidth Throttled", "message": "The available bandwidth on your network has diminished, and this may impact your audio quality. For troubleshooting tips, click here."}, // BITRATE_THROTTLE_WARN, - 10:{"title": "Low Bandwidth", "message": "The available bandwidth on your network has become too low, and this may impact your audio quality. For troubleshooting tips, click here." }, // BANDWIDTH_LOW + 3: {"title": "High Packet Jitter", "message": "Your network connection is currently experiencing packet jitter at a level that is too high to deliver good audio quality. For troubleshooting tips, click the '?'.", help: "https://jamkazam.desk.com/customer/portal/articles/1288778-troubleshoot-session-quality-problems"}, // PACKET_JTR, + 4: {"title": "High Packet Loss", "message": "Your network connection is currently experiencing packet loss at a rate that is too high to deliver good audio quality. For troubleshooting tips, click the '?'.", help: "https://jamkazam.desk.com/customer/portal/articles/1288778-troubleshoot-session-quality-problems" }, // PACKET_LOSS + 5: {"title": "High Packet Late", "message": "Your network connection is currently experiencing packet loss at a rate that is too high to deliver good audio quality. For troubleshooting tips, click the '?'.", help: "https://jamkazam.desk.com/customer/portal/articles/1288778-troubleshoot-session-quality-problems"}, // PACKET_LATE, + 6: {"title": "Large Jitter Queue", "message": "Your network connection is currently experiencing packet jitter at a level that is too high to deliver good audio quality. For troubleshooting tips, click the '?'.", help: "https://jamkazam.desk.com/customer/portal/articles/1288778-troubleshoot-session-quality-problems"}, // JTR_QUEUE_DEPTH, + 7: {"title": "High Network Jitter", "message": "Your network connection is currently experiencing network jitter at a level that is too high to deliver good audio quality. For troubleshooting tips, click the '?'.", help: "https://jamkazam.desk.com/customer/portal/articles/1288778-troubleshoot-session-quality-problems"}, // NETWORK_JTR, + 8: {"title": "High Session Latency", "message": "The latency of your audio device combined with your Internet connection has become high enough to impact your session quality. For troubleshooting tips, click the '?'.", help: "https://jamkazam.desk.com/customer/portal/articles/1288778-troubleshoot-session-quality-problems" }, // NETWORK_PING, + 9: {"title": "Bandwidth Throttled", "message": "The available bandwidth on your network has diminished, and this may impact your audio quality. For troubleshooting tips, click the '?'.", help: "https://jamkazam.desk.com/customer/portal/articles/1288778-troubleshoot-session-quality-problems"}, // BITRATE_THROTTLE_WARN, + 10:{"title": "Low Bandwidth", "message": "The available bandwidth on your network has become too low, and this may impact your audio quality. For troubleshooting tips, click the '?'.", help: "https://jamkazam.desk.com/customer/portal/articles/1288778-troubleshoot-session-quality-problems" }, // BANDWIDTH_LOW // IO related events - 11:{"title": "Variable Input Rate", "message": "The input rate of your audio device is varying too much to deliver good audio quality. For troubleshooting tips, click here." }, // INPUT_IO_RATE - 12:{"title": "High Input Jitter", "message": "The input rate of your audio device is varying too much to deliver good audio quality. For troubleshooting tips, click here."}, // INPUT_IO_JTR, - 13:{"title": "Variable Output Rate", "message": "The output rate of your audio device is varying too much to deliver good audio quality. For troubleshooting tips, click here." }, // OUTPUT_IO_RATE - 14:{"title": "High Output Jitter", "message": "The output rate of your audio device is varying too much to deliver good audio quality. For troubleshooting tips, click here."}, // OUTPUT_IO_JTR, + 11:{"title": "Variable Input Rate", "message": "The input rate of your audio device is varying too much to deliver good audio quality. For troubleshooting tips, click the '?'.", help: "https://jamkazam.desk.com/customer/portal/articles/1288778-troubleshoot-session-quality-problems" }, // INPUT_IO_RATE + 12:{"title": "High Input Jitter", "message": "The input rate of your audio device is varying too much to deliver good audio quality. For troubleshooting tips, click the '?'.", help: "https://jamkazam.desk.com/customer/portal/articles/1288778-troubleshoot-session-quality-problems"}, // INPUT_IO_JTR, + 13:{"title": "Variable Output Rate", "message": "The output rate of your audio device is varying too much to deliver good audio quality. For troubleshooting tips, click the '?'.", help: "https://jamkazam.desk.com/customer/portal/articles/1288778-troubleshoot-session-quality-problems"}, // OUTPUT_IO_RATE + 14:{"title": "High Output Jitter", "message": "The output rate of your audio device is varying too much to deliver good audio quality. For troubleshooting tips, click the '?'.", help: "https://jamkazam.desk.com/customer/portal/articles/1288778-troubleshoot-session-quality-problems"}, // OUTPUT_IO_JTR, // CPU load related - 15: { "title": "CPU Utilization High", "message": "The CPU of your computer is unable to keep up with the current processing load, and this may impact your audio quality. For troubleshooting tips, click here." }, // CPU_LOAD + 15: { "title": "CPU Utilization High", "message": "The CPU of your computer is unable to keep up with the current processing load, and this may impact your audio quality. For troubleshooting tips, click the '?'.", help: "https://jamkazam.desk.com/customer/portal/articles/1288778-troubleshoot-session-quality-problems"}, // CPU_LOAD 16: {"title": "Decode Violations", "message": ""}, // DECODE_VIOLATIONS, 17: {"title": "", "message": ""}, // LAST_THRESHOLD 18: {"title": "Wifi Alert", "message": ""}, // WIFI_NETWORK_ALERT, //user or peer is using wifi @@ -162,10 +162,10 @@ 33: {"title": "Client No Longer Pinned", "message": "This client is no longer designated as the source of the broadcast."}, // SESSION_LIVEBROADCAST_UNPINNED, //node unpinned by user 34: {"title": "", "message": ""}, // BACKEND_STATUS_MSG, //status/informational message - 35: {"title": "LAN Unpredictable", "message": "Your local network is adding considerable variance to transmit times. For troubleshooting tips, click here."}, // LOCAL_NETWORK_VARIANCE_HIGH,//the ping time via a hairpin for the user network is unnaturally high or variable. + 35: {"title": "LAN Unpredictable", "message": "Your local network is adding considerable variance to transmit times. For troubleshooting tips, click the '?'.", help: "https://jamkazam.desk.com/customer/portal/articles/1288778-troubleshoot-session-quality-problems"}, // LOCAL_NETWORK_VARIANCE_HIGH,//the ping time via a hairpin for the user network is unnaturally high or variable. //indicates problem with user computer stack or network itself (wifi, antivirus etc) - 36: {"title": "LAN High Latency", "message": "Your local network is adding considerable latency. For troubleshooting tips, click here."}, // LOCAL_NETWORK_LATENCY_HIGH, + 36: {"title": "LAN High Latency", "message": "Your local network is adding considerable latency. For troubleshooting tips, click the '?'.", help: "https://jamkazam.desk.com/customer/portal/articles/1288778-troubleshoot-session-quality-problems"}, // LOCAL_NETWORK_LATENCY_HIGH, 37: {"title": "", "message": ""}, // RECORDING_CLOSE, //update and remove tracks from front-end 38: {"title": "No Audio Sent", "message": ""}, // PEER_REPORTS_NO_AUDIO_RECV, //update and remove tracks from front-end 39: {"title": "", "message": ""}, // SHOW_PREFERENCES, //show preferences dialog @@ -314,19 +314,17 @@ MASTER_VS_PERSONAL_MIX : 'master_vs_personal_mix' } - // Recreate ChannelGroupIDs ENUM from C++ - context.JK.ChannelGroupIds = - { + context.JK.ChannelGroupIds = { "MasterGroup": 0, "MonitorGroup": 1, - "MasterCatGroup": 2, - "MonitorCatGroup": 3, + "MasterCatGroup" : 2, + "MonitorCatGroup" : 3, "AudioInputMusicGroup": 4, "AudioInputChatGroup": 5, "MediaTrackGroup": 6, "StreamOutMusicGroup": 7, "StreamOutChatGroup": 8, - "StreamOutMediaGroup": 9, + "StreamOutMediaGroup" : 9, "UserMusicInputGroup": 10, "UserChatInputGroup": 11, "UserMediaInputGroup": 12, @@ -335,4 +333,34 @@ "JamTrackGroup": 15, "MetronomeGroup": 16 }; - })(window,jQuery); \ No newline at end of file + + context.JK.ChannelGroupLookup = { + 0: "MasterGroup", + 1: "MonitorGroup", + 2: "MasterCatGroup", + 3: "MonitorCatGroup", + 4: "AudioInputMusicGroup", + 5: "AudioInputChatGroup", + 6: "MediaTrackGroup", + 7: "StreamOutMusicGroup", + 8: "StreamOutChatGroup", + 9: "StreamOutMediaGroup", + 10: "UserMusicInputGroup", + 11: "UserChatInputGroup", + 12: "UserMediaInputGroup", + 13: "PeerAudioInputMusicGroup", + 14: "PeerMediaTrackGroup", + 15: "JamTrackGroup", + 16: "MetronomeGroup" + } + context.JK.CategoryGroupIds = { + "AudioInputMusic" : "AudioInputMusic", + "AudioInputChat" : "AudioInputChat", + "UserMusic" : "UserMusic", + "UserChat" : "UserChat", + "UserMedia" : "UserMedia", + "MediaTrack" : "MediaTrack", + "Metronome" : "Metronome" + } + +})(window,jQuery); diff --git a/web/app/assets/javascripts/helpBubbleHelper.js b/web/app/assets/javascripts/helpBubbleHelper.js index c8ccdc4cf..9e2e54083 100644 --- a/web/app/assets/javascripts/helpBubbleHelper.js +++ b/web/app/assets/javascripts/helpBubbleHelper.js @@ -29,6 +29,7 @@ function bigHelpOptions(options) { return {positions: options.positions, offsetParent: options.offsetParent, + width:options.width, spikeGirth: 15, spikeLength: 20, fill: 'white', @@ -68,13 +69,13 @@ helpBubble.jamtrackLandingPreview($preview, $preview.offsetParent()) setTimeout(function() { - helpBubble.jamtrackLandingVideo($video, $video.offsetParent()) + helpBubble.jamtrackLandingVideo($video, $video.closest('.row')) setTimeout(function() { helpBubble.jamtrackLandingCta($ctaButton, $alternativeCta) }, 11000); // 5 seconds on top of 6 second show time of bubbles }, 11000); // 5 seconds on top of 6 second show time of bubbles - }, 1500) + }, 15000) }) @@ -101,18 +102,19 @@ } helpBubble.jamtrackLandingPreview = function($element, $offsetParent) { - context.JK.prodBubble($element, 'jamtrack-landing-preview', {}, bigHelpOptions({positions:['right'], offsetParent: $offsetParent})) + console.log("SHOWING THE PREVIEW BUBBLE") + context.JK.prodBubble($element, 'jamtrack-landing-preview', {}, bigHelpOptions({positions:['right', 'top'], offsetParent: $offsetParent, width:250})) } helpBubble.jamtrackLandingVideo = function($element, $offsetParent) { - context.JK.prodBubble($element, 'jamtrack-landing-video', {}, bigHelpOptions({positions:['left'], offsetParent: $offsetParent})) + context.JK.prodBubble($element, 'jamtrack-landing-video', {}, bigHelpOptions({positions:['top', 'right'], offsetParent: $offsetParent})) } helpBubble.jamtrackLandingCta = function($element, $alternativeElement) { - if ($element.visible()) { - context.JK.prodBubble($element, 'jamtrack-landing-cta', {}, bigHelpOptions({positions:['left']})) + if (!$alternativeElement || $element.visible()) { + context.JK.prodBubble($element, 'jamtrack-landing-cta', {}, bigHelpOptions({positions:['top', 'right'], width:240})) } - else { + else if($alternativeElement) { context.JK.prodBubble($alternativeElement, 'jamtrack-landing-cta', {}, bigHelpOptions({positions:['right']})) } } diff --git a/web/app/assets/javascripts/instrumentSelector.js b/web/app/assets/javascripts/instrumentSelector.js index 51e740b75..7c4197a3c 100644 --- a/web/app/assets/javascripts/instrumentSelector.js +++ b/web/app/assets/javascripts/instrumentSelector.js @@ -10,6 +10,7 @@ var rest = new context.JK.Rest(); var _instruments = []; // will be list of structs: [ {label:xxx, value:yyy}, {...}, ... ] var _rsvp = false; + var _noICheck = false; if (typeof(_parentSelector)=="undefined") {_parentSelector=null} var _parentSelector = parentSelector; var deferredInstruments = null; @@ -100,7 +101,7 @@ selectedInstruments.push({id: id, name: name, level: level}); } }); - + return selectedInstruments; } @@ -109,13 +110,15 @@ return; } $.each(instrumentList, function (index, value) { - $('input[type="checkbox"][session-instrument-id="' + value.id + '"]') + var $item = $('input[type="checkbox"][session-instrument-id="' + value.id + '"]') .attr('checked', 'checked') - .iCheck({ + if(!_noICheck) { + $item.iCheck({ checkboxClass: 'icheckbox_minimal', radioClass: 'iradio_minimal', inheritClass: true - }); + }) + } if (_rsvp) { $('select[session-instrument-id="' + value.id + '"].rsvp-count', _parentSelector).val(value.count); $('select[session-instrument-id="' + value.id + '"].rsvp-level', _parentSelector).val(value.level); @@ -126,8 +129,9 @@ }); } - function initialize(rsvp) { + function initialize(rsvp, noICheck) { _rsvp = rsvp; + _noICheck = noICheck; // XXX; _instruments should be populated in a template, rather than round-trip to server if(!context.JK.InstrumentSelectorDeferred) { // this dance is to make sure there is only one server request instead of InstrumentSelector instances * diff --git a/web/app/assets/javascripts/jam_rest.js b/web/app/assets/javascripts/jam_rest.js index a05a24827..0f1445290 100644 --- a/web/app/assets/javascripts/jam_rest.js +++ b/web/app/assets/javascripts/jam_rest.js @@ -95,6 +95,14 @@ }); } + + function deleteMusicNotation(options) { + return $.ajax({ + type: "DELETE", + url: "/api/music_notations/" +options.id + }); + } + function legacyJoinSession(options) { var sessionId = options["session_id"]; delete options["session_id"]; @@ -478,6 +486,14 @@ }); } + function deleteParticipant(clientId) { + var url = "/api/participants/" + clientId; + return $.ajax({ + type: "DELETE", + url: url + }); + } + function login(options) { var url = '/api/auths/login'; @@ -507,16 +523,12 @@ function getUserProfile(options) { var id = getId(options); - var profile = null; - if (id != null && typeof(id) != 'undefined') { - profile = $.ajax({ + return $.ajax({ type: "GET", dataType: "json", url: "/api/users/" + id + "/profile", processData: false }); - } - return profile; } function createAffiliatePartner(options) { @@ -1827,6 +1839,7 @@ this.createScheduledSession = createScheduledSession; this.uploadMusicNotations = uploadMusicNotations; this.getMusicNotation = getMusicNotation; + this.deleteMusicNotation = deleteMusicNotation; this.getBroadcastNotification = getBroadcastNotification; this.quietBroadcastNotification = quietBroadcastNotification; this.legacyJoinSession = legacyJoinSession; @@ -1882,6 +1895,7 @@ this.addRecordingLike = addRecordingLike; this.addPlayablePlay = addPlayablePlay; this.getSession = getSession; + this.deleteParticipant = deleteParticipant; this.getClientDownloads = getClientDownloads; this.createEmailInvitations = createEmailInvitations; this.createMusicianInvite = createMusicianInvite; diff --git a/web/app/assets/javascripts/jam_track_screen.js.coffee b/web/app/assets/javascripts/jam_track_screen.js.coffee index ac25cc4c8..92c514be6 100644 --- a/web/app/assets/javascripts/jam_track_screen.js.coffee +++ b/web/app/assets/javascripts/jam_track_screen.js.coffee @@ -308,7 +308,6 @@ context.JK.JamTrackScreen=class JamTrackScreen rest.addJamtrackToShoppingCart(params).done((response) => if(isFree) if context.JK.currentUserId? - alert("TODO") context.JK.currentUserFreeJamTrack = true # make sure the user sees no more free notices context.location = '/client#/redeemComplete' else diff --git a/web/app/assets/javascripts/jamkazam.js b/web/app/assets/javascripts/jamkazam.js index d1b41d451..e32b0f041 100644 --- a/web/app/assets/javascripts/jamkazam.js +++ b/web/app/assets/javascripts/jamkazam.js @@ -226,7 +226,7 @@ var errors = JSON.parse(jqXHR.responseText); var $errors = context.JK.format_all_errors(errors); logger.debug("Unprocessable entity sent from server:", JSON.stringify(errors)) - this.notify({title: title, text: $errors, icon_url: "/assets/content/icon_alert_big.png"}) + this.notify({title: title || "Validation Error", text: $errors, icon_url: "/assets/content/icon_alert_big.png"}) } else if(jqXHR.status == 403) { logger.debug("permission error sent from server:", jqXHR.responseText) 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 new file mode 100644 index 000000000..70bfd5f0b --- /dev/null +++ b/web/app/assets/javascripts/minimal/minimal.js @@ -0,0 +1,25 @@ +//= require bugsnag +//= require bind-polyfill +//= 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 +//= require AAA_Log +//= require globals +//= require jam_rest +//= require ga +//= require utils +//= require playbackControls +//= require webcam_viewer +//= require react +//= require react_ujs +//= require react-init +//= require react-components \ No newline at end of file diff --git a/web/app/assets/javascripts/musician_search_filter.js.coffee b/web/app/assets/javascripts/musician_search_filter.js.coffee index 4187d6acd..9586374b8 100644 --- a/web/app/assets/javascripts/musician_search_filter.js.coffee +++ b/web/app/assets/javascripts/musician_search_filter.js.coffee @@ -14,6 +14,7 @@ context.JK.MusicianSearchFilter = class MusicianSearchFilter @isSearching = false @pageNumber = 1 @instrument_logo_map = context.JK.getInstrumentIconMap24() + @instrumentSelector = null init: (app) => @app = app @@ -23,14 +24,18 @@ context.JK.MusicianSearchFilter = class MusicianSearchFilter @screen = $('#musicians-screen') @resultsListContainer = @screen.find('#musician-search-filter-results-list') @spinner = @screen.find('.paginate-wait') + @instrumentSelector = new context.JK.InstrumentSelector(JK.app) + @instrumentSelector.initialize(false, true) this.registerResultsPagination() @screen.find('#btn-musician-search-builder').on 'click', => this.showBuilder() + false @screen.find('#btn-musician-search-reset').on 'click', => this.resetFilter() + false afterShow: () => @screen.find('#musician-search-filter-results').show() @@ -50,7 +55,7 @@ context.JK.MusicianSearchFilter = class MusicianSearchFilter this.loadSearchFilter(sFilter) loadSearchFilter: (sFilter) => - @searchFilter = JSON.parse(sFilter) + @searchFilter = JSON.parse(sFilter) args = interests: @searchFilter.data_blob.interests skill_level: @searchFilter.data_blob.skill_level @@ -64,9 +69,11 @@ context.JK.MusicianSearchFilter = class MusicianSearchFilter @screen.find('#btn-perform-musician-search').on 'click', => this.performSearch() + false @screen.find('#btn-musician-search-cancel').on 'click', => this.cancelFilter() + false this._populateSkill() this._populateStudio() @@ -86,15 +93,16 @@ context.JK.MusicianSearchFilter = class MusicianSearchFilter blankOption = $ '' blankOption.text label blankOption.attr 'value', value - blankOption.attr 'selected', '' if value == selection element.append(blankOption) + element.val(selection) context.JK.dropdown(element) _populateSelectIdentifier: (identifier) => elem = $ '#musician-search-filter-builder select[name='+identifier+']' struct = gon.musician_search_meta[identifier]['map'] keys = gon.musician_search_meta[identifier]['keys'] - this._populateSelectWithKeys(struct, @searchFilter[identifier], keys, elem) + console.log("@searchFilter", @searchFilter, identifier) + this._populateSelectWithKeys(struct, @searchFilter.data_blob[identifier], keys, elem) _populateSelectWithInt: (sourceStruct, selection, element) => struct = @@ -125,24 +133,23 @@ context.JK.MusicianSearchFilter = class MusicianSearchFilter ages_map = gon.musician_search_meta['ages']['map'] $.each gon.musician_search_meta['ages']['keys'], (index, key) => ageTemplate = @screen.find('#template-search-filter-setup-ages').html() - selected = '' ageLabel = ages_map[key] if 0 < @searchFilter.data_blob.ages.length key_val = key.toString() ageMatch = $.grep(@searchFilter.data_blob.ages, (n, i) -> n == key_val) selected = 'checked' if ageMatch.length > 0 - ageHtml = context.JK.fillTemplate(ageTemplate, - id: key + ageHtml = context._.template(ageTemplate, + { id: key description: ageLabel - checked: selected) + checked: selected}, + {variable: 'data'}) @screen.find('#search-filter-ages').append ageHtml _populateGenres: () => @screen.find('#search-filter-genres').empty() @rest.getGenres().done (genres) => genreTemplate = @screen.find('#template-search-filter-setup-genres').html() - selected = '' $.each genres, (index, genre) => if 0 < @searchFilter.data_blob.genres.length genreMatch = $.grep(@searchFilter.data_blob.genres, (n, i) -> @@ -150,35 +157,19 @@ context.JK.MusicianSearchFilter = class MusicianSearchFilter else genreMatch = [] selected = 'checked' if genreMatch.length > 0 - genreHtml = context.JK.fillTemplate(genreTemplate, - id: genre.id + genreHtml = context._.template(genreTemplate, + { id: genre.id description: genre.description - checked: selected) + checked: selected }, + { variable: 'data' }) @screen.find('#search-filter-genres').append genreHtml _populateInstruments: () => - @screen.find('#search-filter-instruments').empty() - @rest.getInstruments().done (instruments) => - $.each instruments, (index, instrument) => - instrumentTemplate = @screen.find('#template-search-filter-setup-instrument').html() - selected = '' - proficiency = '1' - if 0 < @searchFilter.data_blob.instruments.length - instMatch = $.grep(@searchFilter.data_blob.instruments, (inst, i) -> - yn = inst.instrument_id == instrument.id - proficiency = inst.proficiency_level if yn - yn) - selected = 'checked' if instMatch.length > 0 - instrumentHtml = context.JK.fillTemplate(instrumentTemplate, - id: instrument.id - description: instrument.description - checked: selected) - @screen.find('#search-filter-instruments').append instrumentHtml - profsel = '#search-filter-instruments tr[data-instrument-id="'+instrument.id+'"] select' - jprofsel = @screen.find(profsel) - jprofsel.val(proficiency) - context.JK.dropdown(jprofsel) - return true + + # TODO hydrate user's selection from json_store + @instrumentSelector.render(@screen.find('.session-instrumentlist'), []) + @instrumentSelector.setSelectedInstruments(@searchFilter.data_blob.instruments) + _builderSelectValue: (identifier) => elem = $ '#musician-search-filter-builder select[name='+identifier+']' @@ -188,12 +179,7 @@ context.JK.MusicianSearchFilter = class MusicianSearchFilter vals = [] elem = $ '#search-filter-'+identifier+' input[type=checkbox]:checked' if 'instruments' == identifier - elem.each (idx) -> - row = $(this).parent().parent() - instrument = - instrument_id: row.data('instrument-id') - proficiency_level: row.find('select').val() - vals.push instrument + vals = @instrumentSelector.getSelectedInstruments() else elem.each (idx) -> vals.push $(this).val() @@ -231,19 +217,21 @@ context.JK.MusicianSearchFilter = class MusicianSearchFilter performSearch: () => if this.willSearch(true) + filter = {} $.each gon.musician_search_meta.filter_keys.single, (index, key) => - @searchFilter[key] = this._builderSelectValue(key) + filter[key] = this._builderSelectValue(key) $.each gon.musician_search_meta.filter_keys.multi, (index, key) => - @searchFilter[key] = this._builderSelectMultiValue(key) - @rest.postMusicianSearchFilter({ filter: JSON.stringify(@searchFilter), page: @pageNumber }).done(this.didSearch) + filter[key] = this._builderSelectMultiValue(key) + @rest.postMusicianSearchFilter({ filter: JSON.stringify(filter), page: @pageNumber }).done(this.didSearch) renderResultsHeader: () => - @screen.find('#musician-search-filter-description').html(@searchResults.description) if @searchResults.is_blank_filter @screen.find('#btn-musician-search-reset').hide() + @screen.find('.musician-search-text').text('Click search button to look for musicians with similar interests, skill levels, etc.') else @screen.find('#btn-musician-search-reset').show() - + @screen.find('.musician-search-text').text(@searchResults.summary) + renderMusicians: () => this.renderResultsHeader() if @pageNumber == 1 musicians = @searchResults.musicians @@ -338,20 +326,25 @@ context.JK.MusicianSearchFilter = class MusicianSearchFilter @screen.find('.search-m-message').on 'click', (evt) -> userId = $(this).parent().data('musician-id') objThis.app.layout.showDialog 'text-message', d1: userId + return false _bindFriendMusician: () => objThis = this - @screen.find('.search-m-friend').on 'click', (evt) -> + @screen.find('.search-m-friend').on 'click', (evt) => # if the musician is already a friend, remove the button-orange class, and prevent the link from working - if 0 == $(this).closest('.button-orange').size() + $self = $(evt.target) + if 0 == $self.closest('.button-orange').size() return false - $(this).click (ee) -> + logger.debug("evt.target", evt.target) + $self.click (ee) -> ee.preventDefault() - return + return false evt.stopPropagation() - uid = $(this).parent().data('musician-id') + uid = $self.parent().data('musician-id') objThis.rest.sendFriendRequest objThis.app, uid, this.friendRequestCallback + @app.notify({text: 'A friend request has been sent.'}) + return false _bindFollowMusician: () => objThis = this @@ -361,7 +354,7 @@ context.JK.MusicianSearchFilter = class MusicianSearchFilter return false $(this).click (ee) -> ee.preventDefault() - return + return false evt.stopPropagation() newFollowing = {} newFollowing.user_id = $(this).parent().data('musician-id') @@ -377,8 +370,9 @@ context.JK.MusicianSearchFilter = class MusicianSearchFilter # remove the orange look to indicate it's not selectable # @FIXME -- this will need to be tweaked when we allow unfollowing objThis.screen.find('div[data-musician-id=' + newFollowing.user_id + '] .search-m-follow').removeClass('button-orange').addClass 'button-grey' - return + return false error: objThis.app.ajaxError + return false _formatLocation: (musician) -> if musician.city and musician.state @@ -394,10 +388,11 @@ context.JK.MusicianSearchFilter = class MusicianSearchFilter # TODO: paginate: () => + if @pageNumber < @searchResults.page_count && this.willSearch(false) @screen.find('.paginate-wait').show() @pageNumber += 1 - @rest.postMusicianSearchFilter({ filter: JSON.stringify(@searchFilter), page: @pageNumber }).done(this.didSearch) + @rest.postMusicianSearchFilter({ filter: JSON.stringify(@searchFilter.data_blob), page: @pageNumber }).done(this.didSearch) return true false diff --git a/web/app/assets/javascripts/panHelpers.js.coffee b/web/app/assets/javascripts/panHelpers.js.coffee new file mode 100644 index 000000000..3347d86b3 --- /dev/null +++ b/web/app/assets/javascripts/panHelpers.js.coffee @@ -0,0 +1,37 @@ +context = window +$ = jQuery + +panHelper = class PanHelper + + ### + Convert the pan value that comes from a backend mixer + to a 0-100 % usable by a draggable panner element + ### + convertPanToPercent: (mixerPan) -> + value = (((mixerPan + 90) / 90) * 100) / 2 + + if value < 0 + 0 + else if value > 100 + 100 + else + value + + ### + Convert the % value of a draggable panner element + to a mixer-ready pan value + ### + convertPercentToPan: (percent) -> + value = 2 * percent / 100 * 90 - 90 + + if value < -90 + -90 + else if value > 90 + 90 + else + Math.round(value) + + convertPercentToPanForDisplay: (percent) -> + Math.abs(context.JK.PanHelpers.convertPercentToPan(percent)) + +context.JK.PanHelpers = new panHelper() \ No newline at end of file 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/profile.js b/web/app/assets/javascripts/profile.js index fe616b7d7..d99bb421f 100644 --- a/web/app/assets/javascripts/profile.js +++ b/web/app/assets/javascripts/profile.js @@ -23,7 +23,7 @@ var $biography = $screen.find('#biography'); // musical experience - var $instruments = $screen.find('#instruments'); + var $instruments = $screen.find('.instruments-holder'); var $musicianStatus = $screen.find('#musician-status'); var $genres = $screen.find('#genres'); var $concertCount = $screen.find('#concert-count'); @@ -36,7 +36,7 @@ var $youTubeSamples = $screen.find('.youtube-samples'); // online presence - var $noOnlinePresence = $screen.find('.no-online-presence'); + var $userWebsite = $screen.find('.user-website'); var $soundCloudPresence = $screen.find('.soundcloud-presence'); var $reverbNationPresence = $screen.find('.reverbnation-presence'); @@ -95,7 +95,7 @@ var $age = $screen.find('#age'); // buttons - var $btnEdit = $screen.find('#btn-edit'); + var $btnEdit = $screen.find('.edit-profile-btn'); var $btnAddFriend = $screen.find('#btn-add-friend'); var $btnFollowUser = $screen.find('#btn-follow-user'); var $btnMessageUser = $screen.find('#btn-message-user'); @@ -128,7 +128,7 @@ } function resetForm() { - $instruments.empty(); + //$instruments.empty(); $aboutContent.show(); $historyContent.hide(); @@ -411,7 +411,7 @@ /****************** ABOUT TAB *****************/ function renderAbout() { - $instruments.empty(); + //$instruments.empty(); $aboutContent.show(); $historyContent.hide(); @@ -477,7 +477,7 @@ function renderBio() { $biography.html(user.biography ? user.biography : NOT_SPECIFIED_TEXT); - if (isCurrentUser() && !user.biography) { + if (isCurrentUser()) { $btnEditBio.show(); } else { $btnEditBio.hide(); @@ -485,19 +485,26 @@ } function renderMusicalExperience() { - profileUtils.renderMusicalExperience(user, $screen) + profileUtils.renderMusicalExperience(user, $screen, isCurrentUser()) } function renderPerformanceSamples() { - profileUtils.renderPerformanceSamples(user, $screen) + profileUtils.renderPerformanceSamples(user, $screen, isCurrentUser()) } function renderOnlinePresence() { - profileUtils.renderOnlinePresence(user, $screen) + profileUtils.renderOnlinePresence(user, $screen, isCurrentUser()) } function renderInterests() { // current interests + if (isCurrentUser()) { + $btnAddInterests.show(); + } + else { + $btnAddInterests.hide(); + } + var noInterests = !user.paid_sessions && !user.free_sessions && !user.cowriting && !user.virtual_band && !user.traditional_band; if (noInterests) { $noInterests.show(); @@ -506,12 +513,7 @@ $cowritingSection.hide(); $traditionalBandSection.hide(); $virtualBandSection.hide(); - - if (isCurrentUser()) { - $btnAddInterests.show(); - } } else { - $btnAddInterests.hide(); $noInterests.hide(); // paid sessions diff --git a/web/app/assets/javascripts/profile_utils.js b/web/app/assets/javascripts/profile_utils.js index 6c226b9e8..59065b289 100644 --- a/web/app/assets/javascripts/profile_utils.js +++ b/web/app/assets/javascripts/profile_utils.js @@ -59,19 +59,17 @@ profileUtils.gigMap = { "": "not specified", - "0": "zero", - "1": "under 10", - "2": "10 to 50", - "3": "50 to 100", - "4": "over 100" + "0": "under 10", + "1": "10 to 50", + "2": "50 to 100", + "3": "over 100" }; profileUtils.studioMap = { - "0": "zero", - "1": "under 10", - "2": "10 to 50", - "3": "50 to 100", - "4": "over 100" + "0": "under 10", + "1": "10 to 50", + "2": "50 to 100", + "3": "over 100" }; profileUtils.cowritingPurposeMap = { @@ -99,6 +97,31 @@ return list; } + // the server stores money in cents; display it as such + profileUtils.normalizeMoneyForDisplay = function(serverValue) { + if(serverValue || serverValue == 0) { + return (new Number(serverValue) / 100).toFixed(2) + } + else { + return 0; + } + } + + // the server stores money in cents; normalize it from what user entered + profileUtils.normalizeMoneyForSubmit = function(clientValue) { + var money = new Number(clientValue); + + if(!context._.isNaN(money)) { + money = Math.round(money * 100) + } + else { + // restore original value to allow server to reject with validation error + money = clientValue; + } + return money; + } + + // Initialize standard profile help bubbles (topics stored as attributes on element): profileUtils.initializeHelpBubbles = function(parentElement) { $(".help", parentElement).each(function( index ) { @@ -307,18 +330,25 @@ } function formatTitle(title) { - return title && title.length > 30 ? title.substring(0, 30) + "..." : title; + return title; } - profileUtils.renderMusicalExperience = function(player, $root) { - var $instruments = $root.find('#instruments'); + profileUtils.renderMusicalExperience = function(player, $root, isOwner) { + var $instruments = $root.find('.instruments-holder'); var $musicianStatus = $root.find('#musician-status'); var $genres = $root.find('#genres'); var $concertCount = $root.find('#concert-count'); var $studioCount = $root.find('#studio-count'); + var $btnAddExperiences = $root.find('.add-experiences') - $instruments.empty(); + $instruments.find('.profile-instrument').remove() + if(isOwner) { + $btnAddExperiences.show() + } + else { + $btnAddExperiences.hide() + } if (player.instruments) { for (var i = 0; i < player.instruments.length; i++) { var instrument = player.instruments[i]; @@ -335,7 +365,7 @@ proficiency_level_css: proficiencyCssMap[proficiency] }); - $instruments.append(instrumentHtml); + $instruments.prepend(instrumentHtml); } } @@ -367,16 +397,22 @@ var $youTubeSamples = $root.find('.youtube-samples'); var $btnAddRecordings = $root.find('.add-recordings'); + $jamkazamSamples.find('.playable').remove() + $soundCloudSamples.find('.playable').remove() + $youTubeSamples.find('.playable').remove() + + if (isOwner) { + $btnAddRecordings.show(); + } + else { + $btnAddRecordings.hide(); + } if (!performanceSamples || performanceSamples.length === 0) { $noSamples.show() $jamkazamSamples.hide() $soundCloudSamples.hide() $youTubeSamples.hide() - if (isOwner) { - $btnAddRecordings.show(); - } } else { - $btnAddRecordings.hide(); $noSamples.hide(); // show samples section @@ -402,15 +438,15 @@ } $.each(jamkazamSamples, function(index, sample) { - $jamkazamSamples.append("" + formatTitle(sample.claimed_recording.name) + "
"); + $jamkazamSamples.append("" + formatTitle(sample.claimed_recording.name) + ""); }); $.each(soundCloudSamples, function(index, sample) { - $soundCloudSamples.append("" + formatTitle(sample.description) + "
"); + $soundCloudSamples.append("" + formatTitle(sample.description) + ""); }); $.each(youTubeSamples, function(index, sample) { - $youTubeSamples.append("" + formatTitle(sample.description) + "
"); + $youTubeSamples.append("" + formatTitle(sample.description) + ""); }); } }// function renderPerformanceSamples @@ -425,13 +461,17 @@ var $youTubePresence = $root.find('.youtube-presence'); var $facebookPresence = $root.find('.facebook-presence'); var $twitterPresence = $root.find('.twitter-presence'); - var $btnAddSites = $root.find('.add-sites'); - + var $btnAddSites = $root.find('.add-presences'); + if (isOwner) { + $btnAddSites.show(); + } else { + $btnAddSites.hide(); + } // online presences var onlinePresences = player.online_presences; - if ((!onlinePresences || onlinePresences.length === 0) && !player.website) { + if (onlinePresences.length == 0 && !player.website) { $noOnlinePresence.show() $userWebsite.show() $soundCloudPresence.show() @@ -441,18 +481,19 @@ $youTubePresence.show() $facebookPresence.show() $twitterPresence.show() - - if (isOwner) { - $btnAddSites.show(); - } else { - $btnAddSites.hide(); - } } else { - $btnAddSites.hide(); $noOnlinePresence.hide(); if (player.website) { - $userWebsite.find('a').attr('href', player.website); + // make sure website is rooted + var website = player.website; + if(website.indexOf('http') == -1) { + website = 'http://' + website; + } + $userWebsite.removeClass('hidden').find('a').attr('href', website) + } + else { + $userWebsite.addClass('hidden').find('a').attr('href', '') } var soundCloudPresences = profileUtils.soundCloudPresences(onlinePresences); diff --git a/web/app/assets/javascripts/react-components.js b/web/app/assets/javascripts/react-components.js index c16b9faa7..14c31af19 100644 --- a/web/app/assets/javascripts/react-components.js +++ b/web/app/assets/javascripts/react-components.js @@ -1,3 +1,15 @@ -//= require ./react-components/actions/BroadcastActions -//= require ./react-components/stores/BroadcastStore -//= require_directory ./react-components \ No newline at end of file +//= require_directory ./react-components/helpers +//= require_directory ./react-components/actions +//= require ./react-components/stores/AppStore +//= 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/mixins +//= require_directory ./react-components +//= require_directory ./react-components/landing \ 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..87d0bdc13 --- /dev/null +++ b/web/app/assets/javascripts/react-components/PopupMediaControls.js.jsx.coffee @@ -0,0 +1,126 @@ +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', () => + logger.debug("@props", @props) + # it doesn't matter if you do personal or master, because backend just syncs both + MixerActions.loopChanged(@props.backingTracks[0].mixers.personal.mixer, true) + ) + $loop.on('ifUnchecked', () => + # it doesn't matter if you do personal or master, because backend just syncs both + MixerActions.loopChanged(@props.backingTracks[0].mixers.personal.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 new file mode 100644 index 000000000..7cb29df6e --- /dev/null +++ b/web/app/assets/javascripts/react-components/PopupRecordingStartStop.js.jsx.coffee @@ -0,0 +1,135 @@ +context = window + +mixins = [] + +# this check ensures we attempt to listen if this component is created in a popup +if window.opener + mixins.push(Reflux.listenTo(window.opener.RecordingStore,"onRecordingStateChanged")) + +@PopupRecordingStartStop = React.createClass({ + + mixins: mixins + + onRecordingStateChanged: (recordingState) -> + this.setState(isRecording: recordingState.isRecording, recordedOnce: this.state.recordedOnce || recordingState.isRecording) + + startStopRecording: () -> + if this.state.isRecording + window.opener.RecordingActions.stopRecording() + else + window.opener.RecordingActions.startRecording() + + onNoteShowHide: () -> + this.setState(showNote: !this.state.showNote) + + getInitialState: () -> + {isRecording: window.ParentIsRecording, showNote: true, recordedOnce: false} + + render: () -> + + recordingVerb = if this.state.isRecording then 'Stop' else 'Start' + + recordingBtnClasses = classNames({ + "currently-recording" : this.state.isRecording, + "control" : true + }) + + noteJSX = `
+
+ Important Note +
+
+ While playing in your session, you are listening to your own personal mix. This recording will use the master mix, + which may sound very different. To hear and adjust your master mix settings, click the MIXER button in the session toolbar. +
+
` + + recordingJSX = `
+
+ + +
+
+
+ + +
+
+
` + + if this.state.showNote + noteText = 'hide note' + else + noteText = 'show note' + + noteShowHideJSX = `{noteText}` + + note = null + recordingOptions = null + noteShowHide = null + + if this.state.showNote && !this.state.isRecording && !this.state.recordedOnce + # should we show the note itself? Only if not recording, too + note = noteJSX + + if !this.state.isRecording && !this.state.recordedOnce + noteShowHide = noteShowHideJSX + + if gon.global.video_available == "full" + recordingOptions = recordingJSX + + + `
+
+ + + + {recordingVerb} Recording + +
+ + {recordingOptions} + + {note} + + {noteShowHide} + +
` + + windowUnloaded: () -> + window.opener.RecordingActions.recordingControlsClosed() + + componentDidMount: () -> + $(window).unload(@windowUnloaded) + + $root = jQuery(this.getDOMNode()) + + $recordingType = $root.find('input[type="radio"]') + context.JK.checkbox($recordingType) + + @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 + + # 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/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..049234abd --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionBackingTrack.js.jsx.coffee @@ -0,0 +1,89 @@ +context = window + +MixerActions = @MixerActions + +@SessionBackingTrack = React.createClass({ + + mixins: [@MasterPersonalMixersMixin] + + propTypes: { + mode: React.PropTypes.bool.isRequired + } + + handleMute: (e) -> + e.preventDefault() + + mixer = @mixer() + + unless mixer? + logger.debug("ignoring mute because no media mixer") + return + + muting = $(e.currentTarget).is('.enabled') + + MixerActions.mute([mixer], muting) + + render: () -> + + mixers = @mixers() + muteMixer = mixers.mixer + muteMixerId = muteMixer?.id + + classes = classNames({ + 'track-icon-mute': true + 'enabled' : !muteMixer?.mute + 'muted' : muteMixer?.mute + }) + + componentClasses = classNames({ + "session-track" : true + "backing-track" : true + }) + + pan = if mixers.mixer? then mixers.mixer?.pan else 0 + + 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:@mixers()} + , + {width:235, positions:['right', 'left'], offsetParent:$root.closest('.top-parent')}) + + context.JK.interactReactBubble( + $pan, + 'SessionTrackPanHover', + () => + {mixers:@mixers()} + , + {width:331, positions:['right', 'left'], offsetParent:$root.closest('.top-parent')}) + +}) diff --git a/web/app/assets/javascripts/react-components/SessionChatMixer.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionChatMixer.js.jsx.coffee new file mode 100644 index 000000000..36e047005 --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionChatMixer.js.jsx.coffee @@ -0,0 +1,75 @@ +context = window + +@SessionChatMixer= React.createClass({ + + handleMute: (e) -> + e.preventDefault() + + unless @props.mixers.mixer + logger.debug("ignoring mute; no mixer") + return + + muting = $(e.currentTarget).is('.enabled') + + MixerActions.mute([@props.mixers.mixer], muting) + + render: () -> + + muteMixer = @props.mixers.muteMixer + vuMixer = @props.mixers.vuMixer + muteMixerId = muteMixer?.id + + classes = classNames({ + 'track-icon-mute': true + 'enabled' : !muteMixer?.mute + 'muted' : muteMixer?.mute + }) + + pan = if @props.mixers.mixer? then @props.mixers.mixer.pan else 0 + + panStyle = { + transform: "rotate(#{pan}deg)" + WebkitTransform: "rotate(#{pan}deg)" + } + + `
+
+
+
Session Voice Chat Output
+
+
+ +
+
+
+
+
+
+
+
+
` + + 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('.top-parent')}) + + context.JK.interactReactBubble( + $pan, + 'SessionTrackPanHover', + () => + {mixers:@props.mixers} + , + {width:331, positions:['right', 'left'], offsetParent:$root.closest('.top-parent')}) + + +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/SessionInviteMusiciansBtn.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionInviteMusiciansBtn.js.jsx.coffee new file mode 100644 index 000000000..d5ffb91fd --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionInviteMusiciansBtn.js.jsx.coffee @@ -0,0 +1,26 @@ +context = window + +@SessionInviteMusiciansBtn = React.createClass({ + + mixins: [Reflux.listenTo(@AppStore,"onAppInit")] + + onAppInit: (app) -> + @app = app + + @inviteMusiciansUtil = new JK.InviteMusiciansUtil(@app) + @inviteMusiciansUtil.initialize(JK.FriendSelectorDialogInstance) + + openInviteDialog : (e) -> + e.preventDefault() + + friendInput = @inviteMusiciansUtil.inviteSessionUpdate('#update-session-invite-musicians', context.SessionStore.currentSessionId) + @inviteMusiciansUtil.loadFriends() + $(friendInput).show() + @app.layout.showDialog('select-invites') + + render: () -> + ` + + Invite Musicians + ` +}) \ No newline at end of file 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..7ef35d6dd --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionJamTrack.js.jsx.coffee @@ -0,0 +1,89 @@ +context = window + +MixerActions = @MixerActions + +@SessionJamTrack = React.createClass({ + + mixins: [@MasterPersonalMixersMixin] + + propTypes: { + mode: React.PropTypes.bool.isRequired + } + + handleMute: (e) -> + e.preventDefault() + + mixer = @mixer() + + unless mixer? + logger.debug("ignoring mute because no media mixer") + return + + muting = $(e.currentTarget).is('.enabled') + + MixerActions.mute([mixer], muting) + + render: () -> + + mixers = @mixers() + muteMixer = mixers.mixer + muteMixerId = muteMixer?.id + + classes = classNames({ + 'track-icon-mute': true + 'enabled' : !muteMixer?.mute + 'muted' : muteMixer?.mute + }) + + componentClasses = classNames({ + "session-track" : true + "jam-track" : true + }) + + pan = if mixers.mixer? then mixers.mixer?.pan else 0 + + 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:@mixers()} + , + {width:235, positions:['right', 'left'], offsetParent:$root.closest('.top-parent')}) + + context.JK.interactReactBubble( + $pan, + 'SessionTrackPanHover', + () => + {mixers:@mixers()} + , + {width:331, positions:['right', 'left'], offsetParent:$root.closest('.top-parent')}) + +}) diff --git a/web/app/assets/javascripts/react-components/SessionJamTrackCategory.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionJamTrackCategory.js.jsx.coffee new file mode 100644 index 000000000..d6ce1da06 --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionJamTrackCategory.js.jsx.coffee @@ -0,0 +1,81 @@ +context = window + +MixerActions = @MixerActions + +@SessionJamTrackCategory = 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-category" : true + }) + + pan = mixers.mixer.pan + + panStyle = { + transform: "rotate(#{pan}deg)" + WebkitTransform: "rotate(#{pan}deg)" + } + + `
+
+
JamTrack:
+
{this.props.jamTrackName}
+
+ +
+
+
+
+
+
+ +
+
+
` + + 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('.top-parent')}) + + context.JK.interactReactBubble( + $pan, + 'SessionTrackPanHover', + () => + {mixers:@props.mixers} + , + {width:331, positions:['right', 'left'], offsetParent:$root.closest('.top-parent')}) + +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/SessionLeaveBtn.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionLeaveBtn.js.jsx.coffee new file mode 100644 index 000000000..a11467e65 --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionLeaveBtn.js.jsx.coffee @@ -0,0 +1,23 @@ +context = window + +@SessionLeaveBtn = React.createClass({ + + onLeave: (e) -> + e.preventDefault() + @rateSession() + + SessionActions.leaveSession.trigger({location: '/client#/home'}) + + rateSession: () -> + unless @rateSessionDialog? + @rateSessionDialog = new context.JK.RateSessionDialog(context.JK.app); + @rateSessionDialog.initialize(); + + @rateSessionDialog.showDialog(); + + render: () -> + ` + + LEAVE + ` +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/SessionMasterCategoryControls.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionMasterCategoryControls.js.jsx.coffee new file mode 100644 index 000000000..760495ef0 --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionMasterCategoryControls.js.jsx.coffee @@ -0,0 +1,53 @@ +context = window +rest = context.JK.Rest() +ReactCSSTransitionGroup = React.addons.CSSTransitionGroup +MIX_MODES = context.JK.MIX_MODES + +@SessionMasterCategoryControls = React.createClass({ + + mixins: [Reflux.listenTo(@SessionMediaTracksStore,"onInputsChanged"), Reflux.listenTo(@AppStore,"onAppInit")] + + onInputsChanged: (sessionMixers) -> + mixers = sessionMixers.mixers + inputGroupMixers = mixers.getAudioInputCategoryMixer(MIX_MODES.MASTER) + chatGroupMixers = mixers.getChatCategoryMixer(MIX_MODES.MASTER) + + @setState({inputGroupMixers: inputGroupMixers, chatGroupMixers: chatGroupMixers}) + + render: () -> + + categoryControls = [] + + if @state.inputGroupMixers? + input = + mixers: @state.inputGroupMixers + + categoryControls.push(``) + + if @state.chatGroupMixers? + input = + mixers: @state.chatGroupMixers + + categoryControls.push(``) + + + `
+

master output

+
+ {categoryControls} +
+
` + + + getInitialState:() -> + {inputGroupMixers: null, chatGroupMixers: null} + + + onAppInit: (app) -> + @app = app + + + + + +}) diff --git a/web/app/assets/javascripts/react-components/SessionMasterMediaTracks.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionMasterMediaTracks.js.jsx.coffee new file mode 100644 index 000000000..1b9a6aae2 --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionMasterMediaTracks.js.jsx.coffee @@ -0,0 +1,55 @@ +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 + +@SessionMasterMediaTracks = React.createClass({ + + mixins: [@SessionMediaTracksMixin, Reflux.listenTo(@SessionMediaTracksStore,"onInputsChanged"), Reflux.listenTo(@AppStore,"onAppInit")] + + render: () -> + + mediaTracks = [] + + if this.state.mediaSummary.mediaOpen + + if this.state.mediaSummary.backingTrackOpen + for backingTrack in @state.backingTracks + backingTrack.mode = MIX_MODES.MASTER + mediaTracks.push(``) + else if this.state.mediaSummary.jamTrackOpen + mediaTracks.push(``) + for jamTrack in @state.jamTracks + jamTrack.mode = MIX_MODES.MASTER + mediaTracks.push(``) + else if this.state.mediaSummary.recordingOpen + mediaTracks.push(``) + for recordedTrack in @state.recordedTracks + recordedTrack.mode = MIX_MODES.MASTER + mediaTracks.push(``) + else if this.state.mediaSummary.metronomeOpen + @state.metronome.mode = MIX_MODES.MASTER + mediaTracks.push(``) + + `
+

recorded audio

+
+ {mediaTracks} +
+
` + + + getInitialState:() -> + {mediaSummary:{mediaOpen: false}, isRecording: false, backingTracks: [], jamTracks: [], recordedTracks: [], metronome: null} + + onAppInit: (app) -> + @app = app + + + + + +}) diff --git a/web/app/assets/javascripts/react-components/SessionMasterMix.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionMasterMix.js.jsx.coffee new file mode 100644 index 000000000..8227a6e06 --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionMasterMix.js.jsx.coffee @@ -0,0 +1,13 @@ +context = window +MIX_MODES = context.JK.MIX_MODES + +@SessionMasterMix = React.createClass({ + + render: () -> + `
+ + + + +
` +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/SessionMasterMyTracks.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionMasterMyTracks.js.jsx.coffee new file mode 100644 index 000000000..44f879eed --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionMasterMyTracks.js.jsx.coffee @@ -0,0 +1,40 @@ +context = window +ReactCSSTransitionGroup = React.addons.CSSTransitionGroup +MIX_MODES = context.JK.MIX_MODES +logger = context.JK.logger + +@SessionMasterMyTracks = React.createClass({ + + mixins: [@SessionMyTracksMixin, Reflux.listenTo(@SessionMyTracksStore,"onInputsChanged"), Reflux.listenTo(@AppStore,"onAppInit")] + + render: () -> + + content = null + tracks = [] + + if this.state.tracks.length > 0 + for track in this.state.tracks + track.mode = MIX_MODES.MASTER + tracks.push(``) + + if @state.chat + @state.chat.mode = @props.mode + tracks.push(``) + + else if this.state.session? && this.state.session.inSession() + logger.debug("no 'my inputs' for master mix") + + `
+

my live tracks

+
+ {content} + {tracks} +
+
` + + getInitialState:() -> + {tracks:[], session: null} + + onAppInit: (app) -> + @app = app +}) diff --git a/web/app/assets/javascripts/react-components/SessionMasterOtherTrack.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionMasterOtherTrack.js.jsx.coffee new file mode 100644 index 000000000..707c2fb0c --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionMasterOtherTrack.js.jsx.coffee @@ -0,0 +1,93 @@ +context = window + +MixerActions = @MixerActions + +@SessionMasterOtherTrack = React.createClass({ + + handleMute: (e) -> + e.preventDefault() + + unless @props.mixers.mixer? + logger.debug("ignoring mute; no mixer") + return + + muting = $(e.currentTarget).is('.enabled') + + MixerActions.mute([@props.mixers.mixer], muting) + + render: () -> + + muteMixer = @props.mixers.muteMixer + vuMixer = @props.mixers.vuMixer + muteMixerId = muteMixer?.id + + classes = classNames({ + 'track-icon-mute': true + 'enabled' : !muteMixer?.mute + 'muted' : muteMixer?.mute + }) + + pan = if @props.mixers.mixer? then @props.mixers.mixer.pan else 0 + + panStyle = { + transform: "rotate(#{pan}deg)" + WebkitTransform: "rotate(#{pan}deg)" + } + + #
+ + `
+
+
+
{this.props.name}
+
+
+
+ +
+
+
+
+
+
+ + +
+
+
` + + componentDidMount: () -> + + $root = $(this.getDOMNode()) + $mute = $root.find('.track-icon-mute') + $pan = $root.find('.track-icon-pan') + + context.JK.interactReactBubble( + $mute, + 'SessionTrackVolumeHover', + () => + {mixers:this.props.mixers} + , + {width:235, positions:['right', 'left'], offsetParent:$root.closest('.top-parent')}) + + context.JK.interactReactBubble( + $pan, + 'SessionTrackPanHover', + () => + {mixers:this.props.mixers} + , + {width:331, positions:['right', 'left'], offsetParent:$root.closest('.top-parent')}) + + componentWillUpdate: (nextProps, nextState) -> + $root = $(this.getDOMNode()) + $mute = $root.find('.track-icon-mute') + $pan = $root.find('.track-icon-pan') + + # disable hover effects if there is no mixer + if nextProps.mixers.mixer? + $mute.off("click", false) + $pan.off("click", false) + else + $mute.on("click", false) + $pan.on("click", false) +}) diff --git a/web/app/assets/javascripts/react-components/SessionMasterOtherTracks.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionMasterOtherTracks.js.jsx.coffee new file mode 100644 index 000000000..a441c6679 --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionMasterOtherTracks.js.jsx.coffee @@ -0,0 +1,66 @@ +context = window +ReactCSSTransitionGroup = React.addons.CSSTransitionGroup + +@SessionMasterOtherTracks = React.createClass({ + + mixins: [Reflux.listenTo(@SessionOtherTracksStore,"onInputsChanged"), Reflux.listenTo(@AppStore,"onAppInit")] + + onInputsChanged: (sessionMixers) -> + session = sessionMixers.session + mixers = sessionMixers.mixers + noAudioUsers = mixers.noAudioUsers + + tracks = [] + + if session.inSession() + + for participant in session.otherParticipants() + + name = participant.user.name; + + firstTrack = participant.tracks[0] + + photoUrl = context.JK.resolveAvatarUrl(participant.user.photo_url) + + for track in participant.tracks + + mixerData = mixers.findMixerForTrack(participant.client_id, track, false, @props.mode) + + instrumentIcon = context.JK.getInstrumentIcon45(firstTrack.instrument_id) + + trackState = { + participant: participant, + track: track, + mixers: mixerData, + name: name, + instrumentIcon: instrumentIcon, + photoUrl: photoUrl, + hasMixer: mixerData.mixer? , + noAudio: noAudioUsers[participant.client_id] + } + + tracks.push(trackState) + # todo: sessionModel.setAudioEstablished + + this.setState(tracks: tracks, session: session) + + render: () -> + + tracks = [] + + for track in @state.tracks + tracks.push(``) + + `
+

other live tracks

+
+ {tracks} +
+
` + + getInitialState:() -> + {tracks:[], session: null} + + onAppInit: (app) -> + @app = app +}) diff --git a/web/app/assets/javascripts/react-components/SessionMediaTracks.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionMediaTracks.js.jsx.coffee new file mode 100644 index 000000000..2a92ce681 --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionMediaTracks.js.jsx.coffee @@ -0,0 +1,307 @@ +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: [@SessionMediaTracksMixin, Reflux.listenTo(@SessionMediaTracksStore,"onInputsChanged"), Reflux.listenTo(@AppStore,"onAppInit")] + + inputsChangedProcessed: (state) -> + + 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.metronomeFlickerTimeout? # if the metronomeFlickerTimeout is active, we don't consider closing the childWindow + @checkCloseWindow() + state.childWindow = null + + checkCloseWindow: () -> + if @state.childWindow? + @state.childWindow.DontAutoCloseMedia = true + @state.childWindow.close() + + + 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} + ` + + + if this.state.mediaSummary.backingTrackOpen + for backingTrack in @state.backingTracks + backingTrack.mode = MIX_MODES.PERSONAL + mediaTracks.push(``) + else if this.state.mediaSummary.jamTrackOpen + mediaTracks.push(``) + for jamTrack in @state.jamTracks + jamTrack.mode = MIX_MODES.PERSONAL + mediaTracks.push(``) + else if this.state.mediaSummary.recordingOpen + mediaTracks.push(``) + for recordedTrack in @state.recordedTracks + recordedTrack.mode = MIX_MODES.PERSONAL + mediaTracks.push(``) + else if this.state.mediaSummary.metronomeOpen + @state.metronome.mode = MIX_MODES.PERSONAL + 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() + + @checkCloseWindow() if !@state.mediaSummary.mediaOpen && !@state.metronomeFlickerTimeout? + + +}) 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..5c34c0357 --- /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 = if mixers.mixer? then mixers.mixer?.pan else 0 + + 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('.top-parent')}) + + context.JK.interactReactBubble( + $pan, + 'SessionTrackPanHover', + () => + {mixers:@props.mixers} + , + {width:331, positions:['right', 'left'], offsetParent:$root.closest('.top-parent')}) + +}) diff --git a/web/app/assets/javascripts/react-components/SessionMixerBtn.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionMixerBtn.js.jsx.coffee new file mode 100644 index 000000000..c19e4d24f --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionMixerBtn.js.jsx.coffee @@ -0,0 +1,14 @@ +context = window + +@SessionMixerBtn = React.createClass({ + + openDialog: (e) -> + e.preventDefault() + context.JK.app.layout.showDialog('session-master-mix-dialog') + + render: () -> + ` + + MIXER + ` +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/SessionMusicMixer.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionMusicMixer.js.jsx.coffee new file mode 100644 index 000000000..407956c35 --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionMusicMixer.js.jsx.coffee @@ -0,0 +1,75 @@ +context = window + +@SessionMusicMixer= React.createClass({ + + handleMute: (e) -> + e.preventDefault() + + unless @props.mixers.mixer + logger.debug("ignoring mute; no mixer") + return + + muting = $(e.currentTarget).is('.enabled') + + MixerActions.mute([@props.mixers.mixer], muting) + + render: () -> + + muteMixer = @props.mixers.muteMixer + vuMixer = @props.mixers.vuMixer + muteMixerId = muteMixer?.id + + classes = classNames({ + 'track-icon-mute': true + 'enabled' : !muteMixer?.mute + 'muted' : muteMixer?.mute + }) + + pan = if @props.mixers.mixer? then @props.mixers.mixer.pan else 0 + + panStyle = { + transform: "rotate(#{pan}deg)" + WebkitTransform: "rotate(#{pan}deg)" + } + + `
+
+
+
Session Music Output
+
+
+ +
+
+
+
+
+
+
+
+
` + + 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('.top-parent')}) + + context.JK.interactReactBubble( + $pan, + 'SessionTrackPanHover', + () => + {mixers:@props.mixers} + , + {width:331, positions:['right', 'left'], offsetParent:$root.closest('.top-parent')}) + + +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/SessionMyChat.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionMyChat.js.jsx.coffee new file mode 100644 index 000000000..1fad123f2 --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionMyChat.js.jsx.coffee @@ -0,0 +1,69 @@ +context = window + +MixerActions = @MixerActions + +@SessionMyChat = React.createClass({ + + mixins: [@MasterPersonalMixersMixin] + + + handleMute: (e) -> + e.preventDefault() + + mixers = @mixers() + unless mixers.mixer + logger.debug("ignoring mute; no mixer") + return + + muting = $(e.currentTarget).is('.enabled') + + MixerActions.mute([mixers.mixer, mixers.oppositeMixer], muting) + + render: () -> + + mixers = @mixers() + muteMixer = mixers.muteMixer + vuMixer = mixers.vuMixer + muteMixerId = muteMixer?.id + + classes = classNames({ + 'track-icon-mute': true + 'enabled' : !muteMixer?.mute + 'muted' : muteMixer?.mute + }) + + + #
+ + `
+
+
+
{this.props.name}
+
+
+
+ +
+
+
+
+
+ + +
+
+
` + + componentDidMount: () -> + + $root = $(this.getDOMNode()) + $mute = $root.find('.track-icon-mute') + + context.JK.interactReactBubble( + $mute, + 'SessionTrackVolumeHover', + () => + {mixers:@mixers()} + , + {width:235, positions:['right', 'left'], offsetParent:$root.closest('.top-parent')}) +}) diff --git a/web/app/assets/javascripts/react-components/SessionMyTrack.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionMyTrack.js.jsx.coffee new file mode 100644 index 000000000..9a5a63e7a --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionMyTrack.js.jsx.coffee @@ -0,0 +1,96 @@ +context = window + +MixerActions = @MixerActions + +@SessionMyTrack = React.createClass({ + + + handleMute: (e) -> + e.preventDefault() + + unless this.props.mixers.mixer + logger.debug("ignoring mute; no mixer") + return + + muting = $(e.currentTarget).is('.enabled') + + MixerActions.mute([this.props.mixers.mixer, this.props.mixers.oppositeMixer], muting) + + render: () -> + + muteMixer = this.props.mixers.muteMixer + vuMixer = this.props.mixers.vuMixer + muteMixerId = muteMixer?.id + + classes = classNames({ + 'track-icon-mute': true + 'enabled' : !muteMixer?.mute + 'muted' : muteMixer?.mute + }) + + pan = if this.props.mixers.mixer? then this.props.mixers.mixer.pan else 0 + + panStyle = { + transform: "rotate(#{pan}deg)" + WebkitTransform: "rotate(#{pan}deg)" + } + + #
+ + `
+
+
+
{this.props.name}
+
+
+
+ +
+
+
+
+
+
+ + +
+
+
` + + componentDidMount: () -> + + context.jamClient.SessionSetUserName(this.props.clientId, this.props.name) + + $root = $(this.getDOMNode()) + $mute = $root.find('.track-icon-mute') + $pan = $root.find('.track-icon-pan') + + context.JK.interactReactBubble( + $mute, + 'SessionTrackVolumeHover', + () => + {mixers:this.props.mixers} + , + {width:235, positions:['right', 'left'], offsetParent:$root.closest('.top-parent')}) + + context.JK.interactReactBubble( + $pan, + 'SessionTrackPanHover', + () => + {mixers:this.props.mixers} + , + {width:331, positions:['right', 'left'], offsetParent:$root.closest('.top-parent')}) + + componentWillUpdate: (nextProps, nextState) -> + $root = $(this.getDOMNode()) + $mute = $root.find('.track-icon-mute') + $pan = $root.find('.track-icon-pan') + + # disable hover effects if there is no mixer + if nextProps.mixers.mixer? + $mute.off("click", false) + $pan.off("click", false) + else + $mute.on("click", false) + $pan.on("click", false) +}) diff --git a/web/app/assets/javascripts/react-components/SessionMyTracks.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionMyTracks.js.jsx.coffee new file mode 100644 index 000000000..3f98515e5 --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionMyTracks.js.jsx.coffee @@ -0,0 +1,50 @@ +context = window +MIX_MODES = context.JK.MIX_MODES + +ReactCSSTransitionGroup = React.addons.CSSTransitionGroup; + +@SessionMyTracks = React.createClass({ + + mixins: [@SessionMyTracksMixin, Reflux.listenTo(@SessionMyTracksStore,"onInputsChanged"), Reflux.listenTo(@AppStore,"onAppInit")] + + render: () -> + + content = null + tracks = [] + + if @state.tracks.length > 0 + for track in @state.tracks + track.mode = MIX_MODES.PERSONAL + tracks.push(``) + + if @state.chat + @state.chat.mode = @props.mode + tracks.push(``) + + else if @state.session? && @state.session.inSession() + content = `
+

+ You have not set up any inputs for your instrument or vocals. + If you want to hear yourself play through the JamKazam app, + and let the app mix your live playing with JamTracks, or with other musicians in online sessions, + click here now. +

+
` + + `
+

my live tracks

+ +
+ {content} + + {tracks} + +
+
` + + getInitialState:() -> + {tracks:[], session: null, chat:null} + + onAppInit: (app) -> + @app = app +}) 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..b1decbf08 --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionNotification.js.jsx.coffee @@ -0,0 +1,29 @@ +context = window + +@SessionNotification = React.createClass({ + + render: () -> + + classes = classNames({ + 'session-notification' : true + 'has-details' : @props.detail? + }) + + help = `?` if @props.help? + + title = `
{this.props.title}{help}
` + extra = `
{this.props.extra}
` if @props.extra? + + `
+ {title} + {extra} +
` + + componentDidMount: () -> + + $root = $(@getDOMNode()) + context.JK.popExternalLinks($root) + + if @props.detail? + context.JK.hoverBubble($root, @props.detail, {offsetParent:$root.closest('.top-parent'), positions: ['left', 'bottom']}) +}) \ 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 new file mode 100644 index 000000000..96941368b --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionNotifications.js.jsx.coffee @@ -0,0 +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 new file mode 100644 index 000000000..7bb9e531e --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionOtherTrack.js.jsx.coffee @@ -0,0 +1,121 @@ +context = window + +MixerActions = @MixerActions + +@SessionOtherTrack = React.createClass({ + + handleMute: (e) -> + e.preventDefault() + + unless this.props.hasMixer + logger.debug("ignoring mute; no mixer") + return + + muting = $(e.currentTarget).is('.enabled') + + mixers = if this.props.tracks.length > 0 then this.props.tracks[0].mixers else {} + + MixerActions.mute([mixers.mixer], muting) + + render: () -> + + # today, all mixers are the same for a remote participant; so just grab the 1st + mixers = if this.props.tracks.length > 0 then this.props.tracks[0].mixers else {} + + 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 + "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 + + panStyle = { + transform: "rotate(#{pan}deg)" + WebkitTransform: "rotate(#{pan}deg)" + } + + `
+
+
+
{this.props.name}
+
+
+
+ +
+
+
+
+
+
+ +
+
+
` + + componentDidMount: () -> + + if this.props.participant.client_id? + context.jamClient.SessionSetUserName(this.props.participant.client_id, this.props.name) + else + logger.error("no participant client ID") + + $root = $(this.getDOMNode()) + $mute = $root.find('.track-icon-mute') + $pan = $root.find('.track-icon-pan') + + context.JK.interactReactBubble( + $mute, + 'SessionTrackVolumeHover', + () => + mixers = if this.props.tracks.length > 0 then this.props.tracks[0].mixers else {} + {mixers:mixers} + , + {width:235, positions:['right', 'left'], offsetParent:$root.closest('.screen')}) + + context.JK.interactReactBubble( + $pan, + 'SessionTrackPanHover', + () => + mixers = if this.props.tracks.length > 0 then this.props.tracks[0].mixers else {} + {mixers:mixers} + , + {width:331, positions:['right', 'left'], offsetParent:$root.closest('.screen')}) + + unless this.props.hasMixer + $mute.on("mouseenter", false) + $mute.on("mouseleave", false) + $pan.on("mouseentere", false) + $pan.on("mouseleave", false) + + componentWillUpdate: (nextProps, nextState) -> + $root = $(this.getDOMNode()) + $mute = $root.find('.track-icon-mute') + $pan = $root.find('.track-icon-pan') + + # disable hover effects if there is no mixer + if nextProps.hasMixer + $mute.off("mouseenter", false) + $mute.off("mouseleave", false) + $pan.off("mouseenter", false) + $pan.off("mouseleave", false) + else + $mute.on("mouseenter", false) + $mute.on("mouseleave", false) + $pan.on("mouseentere", false) + $pan.on("mouseleave", false) +}) diff --git a/web/app/assets/javascripts/react-components/SessionOtherTracks.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionOtherTracks.js.jsx.coffee new file mode 100644 index 000000000..19c6e50a5 --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionOtherTracks.js.jsx.coffee @@ -0,0 +1,83 @@ +context = window +ReactCSSTransitionGroup = React.addons.CSSTransitionGroup + +@SessionOtherTracks = React.createClass({ + + mixins: [Reflux.listenTo(@SessionOtherTracksStore,"onInputsChanged"), Reflux.listenTo(@AppStore,"onAppInit")] + + onInputsChanged: (sessionMixers) -> + session = sessionMixers.session + mixers = sessionMixers.mixers + noAudioUsers = mixers.noAudioUsers + + participants = [] + + if session.inSession() + + for participant in session.otherParticipants() + + tracks = [] + name = participant.user.name; + + firstTrack = participant.tracks[0] + hasMixer = false + + for track in participant.tracks + # try to find mixer info for this track + mixerFinder = [participant.client_id, track, false] # so that other callers can re-find their mixer data + + mixerData = mixers.findMixerForTrack(participant.client_id, track, false, @props.mode) + if mixerData.mixer? + hasMixer = true + + tracks.push(track: track, mixers: mixerData, mixerFinder: mixerFinder) + # todo: sessionModel.setAudioEstablished + + 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, + noAudio: noAudioUsers[participant.client_id] + } + + participants.push(participantState) + + this.setState(participants: participants, session: session) + + render: () -> + + content = null + participants = [] + + noOthers = `
No other musicians are in your session.
` + + if this.state.participants.length > 0 + for participant in this.state.participants + participants.push(``) + else if this.state.session? && this.state.session.inSession() + content = noOthers + + `
+

other live tracks

+ +
+ {content} + + {participants} + +
+
` + + getInitialState:() -> + {participants:[], session: null} + + onAppInit: (app) -> + @app = app +}) diff --git a/web/app/assets/javascripts/react-components/SessionRecordBtn.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionRecordBtn.js.jsx.coffee new file mode 100644 index 000000000..c6d75af93 --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionRecordBtn.js.jsx.coffee @@ -0,0 +1,24 @@ +context = window +RecordingActions = @RecordingActions + +@SessionRecordBtn = React.createClass({ + + mixins: [Reflux.listenTo(@MixerStore,"onSessionMixerChange")] + + onSessionMixerChange: (sessionMixers) -> + + this.setState({isRecording: sessionMixers.session.isRecording}) + + getInitialState: () -> + {childWindow: null, isRecording: false} + + openRecording: () -> + + RecordingActions.openRecordingControls() + + render: () -> + ` + + RECORD + ` +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/SessionRecordedCategory.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionRecordedCategory.js.jsx.coffee new file mode 100644 index 000000000..31528bac4 --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionRecordedCategory.js.jsx.coffee @@ -0,0 +1,84 @@ +context = window + +MixerActions = @MixerActions + +@SessionRecordedCategory = React.createClass({ + + propTypes: { + mode: React.PropTypes.bool.isRequired + } + + 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-category" : true + }) + + pan = mixers.mixer.pan + + panStyle = { + transform: "rotate(#{pan}deg)" + WebkitTransform: "rotate(#{pan}deg)" + } + + `
+
+
{this.props.recordingName}
+
+ +
+
+
+
+
+
+ +
+
+
` + + 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('.top-parent')}) + + context.JK.interactReactBubble( + $pan, + 'SessionTrackPanHover', + () => + {mixers:@props.mixers} + , + {width:331, positions:['right', 'left'], offsetParent:$root.closest('.top-parent')}) + +}) \ No newline at end of file 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..6965ad3c5 --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionRecordedTrack.js.jsx.coffee @@ -0,0 +1,89 @@ +context = window + +MixerActions = @MixerActions + +@SessionRecordedTrack = React.createClass({ + + mixins: [@MasterPersonalMixersMixin] + + propTypes: { + mode: React.PropTypes.bool.isRequired + } + + handleMute: (e) -> + e.preventDefault() + + mixer = @mixer() + + unless mixer? + logger.debug("ignoring mute because no media mixer") + return + + muting = $(e.currentTarget).is('.enabled') + + MixerActions.mute([mixer], muting) + + render: () -> + + mixers = @mixers() + muteMixer = mixers.mixer + muteMixerId = muteMixer?.id + + classes = classNames({ + 'track-icon-mute': true + 'enabled' : !muteMixer?.mute + 'muted' : muteMixer?.mute + }) + + componentClasses = classNames({ + "session-track" : true + "recorded-track" : true + }) + + pan = if mixers.mixer? then mixers.mixer?.pan else 0 + + 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:@mixers()} + , + {width:235, positions:['right', 'left'], offsetParent:$root.closest('.top-parent')}) + + context.JK.interactReactBubble( + $pan, + 'SessionTrackPanHover', + () => + {mixers:@mixers()} + , + {width:331, positions:['right', 'left'], offsetParent:$root.closest('.top-parent')}) + +}) diff --git a/web/app/assets/javascripts/react-components/SessionResyncBtn.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionResyncBtn.js.jsx.coffee new file mode 100644 index 000000000..67b26884e --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionResyncBtn.js.jsx.coffee @@ -0,0 +1,20 @@ +context = window + +@SessionResyncBtn = React.createClass({ + + mixins: [Reflux.listenTo(@AppStore,"onAppInit")] + + resync: (e) -> + e.preventDefault() + + SessionActions.audioResync() + + render: () -> + ` + + RESYNC + ` + + onAppInit: (app) -> + @app = app +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/SessionScreen.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionScreen.js.jsx.coffee new file mode 100644 index 000000000..38587d094 --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionScreen.js.jsx.coffee @@ -0,0 +1,78 @@ +context = window +MIX_MODES = context.JK.MIX_MODES + +SessionActions = @SessionActions + +@SessionScreen = React.createClass({ + + mixins: [Reflux.listenTo(@AppStore,"onAppInit"), Reflux.listenTo(@SessionActions.allowLeaveSession, "onAllowLeaveSession")] + + render: () -> + `
+
+ + + + + + + + +
+
+ + + + +
+
` + + componentDidMount: () -> + @logger = context.JK.logger + + beforeShow: (data) -> + @logger.debug("session beforeShow") + @allowLeave = false + + afterShow: (data) -> + @logger.debug("session afterShow") + + SessionActions.joinSession.trigger(data.id) + + beforeHide: () -> + context.JK.HelpBubbleHelper.clearJamTrackGuide(); + + beforeLeave: (data) -> + @logger.debug("session beforeLeave", @allowLeave) + + if @allowLeave + return true + else + leaveSessionWarningDialog = new context.JK.LeaveSessionWarningDialog(context.JK.app, + () => + @allowLeave = true + context.location.hash = data.hash + ) + + leaveSessionWarningDialog.initialize() + @app.layout.showDialog('leave-session-warning') + return false + + beforeDisconnect: () -> + @logger.debug("session beforeDisconnect") + + onAllowLeaveSession: () -> + @allowLeave = true + + onAppInit: (@app) -> + + screenBindings = { + 'beforeShow': @beforeShow, + 'afterShow': @afterShow, + 'beforeHide': @beforeHide, + 'beforeLeave' : @beforeLeave, + 'beforeDisconnect' : @beforeDisconnect, + }; + + @app.bindScreen('session', screenBindings); +}) diff --git a/web/app/assets/javascripts/react-components/SessionSelfVolumeHover.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionSelfVolumeHover.js.jsx.coffee new file mode 100644 index 000000000..981f285e7 --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionSelfVolumeHover.js.jsx.coffee @@ -0,0 +1,169 @@ +context = window +MIX_MODES = context.JK.MIX_MODES + +MixerActions = @MixerActions + +@SessionSelfVolumeHover = React.createClass({ + + mixins: [Reflux.listenTo(@SessionMyTracksStore,"onInputsChanged")] + + closeHover: (e) -> + e.preventDefault() + $container = $(this.getDOMNode()).closest('.react-holder') + $container.data('bt').btOff() + + onInputsChanged: (sessionMixers) -> + + mixers = sessionMixers.mixers + inputGroupMixers = mixers.getAudioInputCategoryMixer(MIX_MODES.PERSONAL) + chatGroupMixers = mixers.getChatCategoryMixer( MIX_MODES.PERSONAL) + + @setState({inputGroupMixers: inputGroupMixers, chatGroupMixers: chatGroupMixers}) + + getInitialState: () -> + {inputGroupMixers: @props.inputGroupMixers, chatGroupMixers: @props.chatGroupMixers} + + handleAudioInputMute: (e) -> + e.preventDefault() + + muting = $(e.currentTarget).is('.enabled') + + MixerActions.mute([@state.inputGroupMixers.muteMixer], muting) + + handleChatInputMute: (e) -> + e.preventDefault() + + muting = $(e.currentTarget).is('.enabled') + + MixerActions.mute([@state.chatGroupMixers.muteMixer], muting) + + handleAudioInputMuteCheckbox: (e) -> + muting = $(e.target).is(':checked') + + MixerActions.mute([@state.inputGroupMixers.muteMixer], muting) + + handleChatMuteCheckbox: (e) -> + muting = $(e.target).is(':checked') + + MixerActions.mute([@state.chatGroupMixers.muteMixer], muting) + + render: () -> + + monitorMuteMixer = @state.inputGroupMixers.muteMixer + monitorMuteMixerId = monitorMuteMixer?.id + monitorVolumeLeft = @state.inputGroupMixers.mixer?.volume_left + monitorMuteClasses = classNames({ + 'track-icon-mute': true + 'enabled' : !monitorMuteMixer?.mute + 'muted' : monitorMuteMixer?.mute + }) + + chatMuteMixer = @state.chatGroupMixers.muteMixer + chatMuteMixerId = chatMuteMixer?.id + chatVolumeLeft = @state.chatGroupMixers.mixer?.volume_left + chatMuteClasses = classNames({ + 'track-icon-mute': true + 'enabled' : !chatMuteMixer?.mute + 'muted' : chatMuteMixer?.mute + }) + + `
+
+

Music

+
+
+ +
+
+ +
+
+
Volume
+
{monitorVolumeLeft}dB
+
+ +
+ + + +
+ +
+

Use this slider to control the volume of all the music in the session in your headphones or speakers.

+

This will not affect the volume for other musicians in the session.

+

To adjust master levels for all musicians for recordings and broadcasts, use Mixer button in the toolbar.

+
+
+ +
+

Chat

+
+
+ +
+
+ +
+
+
Volume
+
{chatVolumeLeft}dB
+
+ +
+ + + +
+ +
+

Use this slider to control the volume of all the voice chat in the session in your headphones or speakers.

+

This will not affect the volume for other musicians in the session.

+
+
+ +
+ close +
+
` + + componentDidMount: () -> + $root = jQuery(this.getDOMNode()) + + # initialize icheck + $chatMuteCheckbox = $root.find('.chat-mixer input') + context.JK.checkbox($chatMuteCheckbox) + $chatMuteCheckbox.on('ifChanged', @handleChatMuteCheckbox); + + if @state.chatGroupMixers.muteMixer.mute + $chatMuteCheckbox.iCheck('check').attr('checked', true) + else + $chatMuteCheckbox.iCheck('uncheck').attr('checked', false) + + $audioInputMuteCheckbox = $root.find('.monitor-mixer input') + context.JK.checkbox($audioInputMuteCheckbox) + $audioInputMuteCheckbox.on('ifChanged', @handleAudioInputMuteCheckbox); + + if @state.inputGroupMixers.muteMixer.mute + $audioInputMuteCheckbox.iCheck('check').attr('checked', true) + else + $audioInputMuteCheckbox.iCheck('uncheck').attr('checked', false) + + componentWillUpdate: (nextProps, nextState) -> + $root = jQuery(this.getDOMNode()) + + # re-initialize icheck + $chatMuteCheckbox = $root.find('.chat-mixer input') + + if nextState.chatGroupMixers.muteMixer?.mute + $chatMuteCheckbox.iCheck('check').attr('checked', true) + else + $chatMuteCheckbox.iCheck('uncheck').attr('checked', false) + + $audioInputMuteCheckbox = $root.find('.monitor-mixer input') + + if nextState.inputGroupMixers.muteMixer?.mute + $audioInputMuteCheckbox.iCheck('check').attr('checked', true) + else + $audioInputMuteCheckbox.iCheck('uncheck').attr('checked', false) + +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/SessionSettingsBtn.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionSettingsBtn.js.jsx.coffee new file mode 100644 index 000000000..0719f136b --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionSettingsBtn.js.jsx.coffee @@ -0,0 +1,20 @@ +context = window + +@SessionSettingsBtn = React.createClass({ + + mixins: [Reflux.listenTo(@AppStore,"onAppInit")] + + openSettings: (e) -> + e.preventDefault() + + @app.layout.showDialog('session-settings') + + render: () -> + ` + + SETTINGS + ` + + onAppInit: (app) -> + @app = app +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/SessionShareBtn.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionShareBtn.js.jsx.coffee new file mode 100644 index 000000000..00af66683 --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionShareBtn.js.jsx.coffee @@ -0,0 +1,20 @@ +context = window + +@SessionShareBtn = React.createClass({ + + mixins: [Reflux.listenTo(@AppStore,"onAppInit")] + + onShare: (e) -> + e.preventDefault() + + @app.layout.showDialog('share-dialog') + + render: () -> + ` + + SHARE + ` + + onAppInit: (app) -> + @app = app +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/SessionTrackGain.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionTrackGain.js.jsx.coffee new file mode 100644 index 000000000..e5dfa7f58 --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionTrackGain.js.jsx.coffee @@ -0,0 +1,51 @@ +context = window +logger = context.JK.logger + +@SessionTrackGain = React.createClass({ + + getInitialState: () -> + { + mixers: @props.mixers, + behaviors: @props.behaviors || {} + } + + faderChanged: (e, data) -> + $target = $(this) + groupId = $target.data('groupId') + mixers = [@state.mixers.mixer] + + MixerActions.faderChanged(data, mixers, groupId) + + render: () -> + mixerId = this.state.mixers?.mixer?.id + + `
+
+
+ +
+
+
` + + componentDidMount: () -> + $root = jQuery(this.getDOMNode()) + if !$root.is('.track-gain') + logger.error("unknown root node") + + $fader = $root.attr('data-mixer-id', @state.mixers.mixer.id).data('groupId', @state.mixers.mixer.groupId).data('mixer', @state.mixers.mixer).data('opposite-mixer', @state.mixers.oppositeMixer) + + if @state.behaviors.mediaControlsDisabled + $fader.data('media-controls-disabled', true).data('media-track-opener', @state.behaviors.mediaTrackOpener) # this we be applied later to the fader handle $element + + $fader.data('showHelpAboutMediaMixers', @state.behaviors.showHelpAboutMediaMixers) + + context.JK.FaderHelpers.renderFader2($fader, {faderType: 'vertical'}); + + # Initialize gain position + MixerActions.initGain(@state.mixers.mixer) + + # watch for fader change events + $fader.on('fader_change', @faderChanged); + + +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/SessionTrackPan.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionTrackPan.js.jsx.coffee new file mode 100644 index 000000000..0bfae1005 --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionTrackPan.js.jsx.coffee @@ -0,0 +1,54 @@ +context = window +logger = context.JK.logger + +@SessionTrackPan = React.createClass({ + + getInitialState: () -> + { + mixers: this.props.mixers, + behaviors: this.props.behaviors || {} + } + + panChanged: (e, data) -> + $target = $(this) + groupId = $target.data('groupId') + mixers = [@state.mixers.mixer] + + MixerActions.panChanged(data, mixers, groupId) + + render: () -> + + mixerId = this.state.mixers?.mixer?.id + + `
+
Left
+
Right
+
+
+
+ +
+
+
` + + componentDidMount: () -> + $root = jQuery(this.getDOMNode()) + if !$root.is('.track-pan') + logger.error("unknown root node") + + $fader = $root.attr('data-mixer-id', this.state.mixers.mixer.id).data('groupId', this.state.mixers.mixer.groupId).data('mixer', this.state.mixers.mixer).data('opposite-mixer', this.state.mixers.oppositeMixer) + + if this.state.behaviors.mediaControlsDisabled + $fader.data('media-controls-disabled', true).data('media-track-opener', this.state.behaviors.mediaTrackOpener) # this we be applied later to the fader handle $element + + $fader.data('showHelpAboutMediaMixers', this.state.behaviors.showHelpAboutMediaMixers) + + context.JK.FaderHelpers.renderFader2($fader, {faderType: 'horizontal', snap:true}, context.JK.PanHelpers.convertPercentToPanForDisplay) + + # Initialize gain position + MixerActions.initPan(this.state.mixers.mixer) + + # watch for fader change events + $fader.on('fader_change', this.panChanged) + +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/SessionTrackPanHover.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionTrackPanHover.js.jsx.coffee new file mode 100644 index 000000000..8244991f4 --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionTrackPanHover.js.jsx.coffee @@ -0,0 +1,51 @@ +context = window + +MixerActions = @MixerActions + +@SessionTrackPanHover = React.createClass({ + + mixins: [Reflux.listenTo(@SessionMyTracksStore, "onInputsChanged")] + + closeHover: (e) -> + e.preventDefault() + $container = $(this.getDOMNode()).closest('.react-holder') + $container.data('bt').btOff() + + onInputsChanged: (sessionMixers) -> + mixers = sessionMixers.mixers + newMixers = mixers.refreshMixer(@state.mixers) + + this.setState({mixers: newMixers}) + + + getInitialState: () -> + {mixers: this.props.mixers} + + render: () -> + + `
+
+

+ Use this slider to pan the audio of this track left or right in your personal mix. + This will not pan audio for other musicians in the session. + To pan audio in the master mix for recordings and broadcasts, use the Mixer button in the toolbar. +

+
+ +
+ +
+
+ close +
+
` + + componentWillUpdate: (nextProps, nextState) -> + $root = jQuery(this.getDOMNode()) + + # if the mixers go dead, whack our selves out of existence + unless nextState.mixers? + $container = $root.closest('.react-holder') + $container.data('bt').btOff() + return +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/SessionTrackSettingsBtn.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionTrackSettingsBtn.js.jsx.coffee new file mode 100644 index 000000000..530df4c93 --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionTrackSettingsBtn.js.jsx.coffee @@ -0,0 +1,22 @@ +context = window + +logger = context.JK.logger + +@SessionTrackSettingsBtn = React.createClass({ + + mixins: [Reflux.listenTo(@AppStore,"onAppInit")] + + onConfigureSettings: (e) -> + e.preventDefault(); + + @app.layout.showDialog('configure-tracks') + + onAppInit: (app) -> + @app = app + + render: () -> + ` + + Settings + ` +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/SessionTrackVU.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionTrackVU.js.jsx.coffee new file mode 100644 index 000000000..7abdad501 --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionTrackVU.js.jsx.coffee @@ -0,0 +1,87 @@ +context = window +ptrCount = 0 + +@SessionTrackVU = React.createClass({ + + + render: () -> + lights = [] + redSwitch = Math.round(this.props.lightCount * 0.66); + lightClass = 'vu-red-off' + + if this.props.orientation == 'horizontal' + + for i in [0..this.props.lightCount-1] + lightClass = if i >= redSwitch then 'vu-red-off' else 'vu-green-off' + + lightClasses = classNames('vulight', 'vu' + i, lightClass) + + lights.push(``) + + tableClasses = classNames('vu', 'horizontal') + + ` + + + {lights} + + +
` + else + + for i in [0..this.props.lightCount-1].reverse() + lightClass = if (i >= redSwitch) then "vu-red-off" else "vu-green-off" + + lightClasses = classNames('vulight', 'vu' + i, lightClass) + + lights.push(``) + + tableClasses = classNames('vu', 'vertical') + + ` + + {lights} + +
` + + getInitialState: () -> + {registered: null, ptr: @props.ptr || "STV#{ptrCount++}"} + + registerVU: (mixer) -> + + mixerChanged = false + if @state.registered? && mixer? + + # see if the mixer ID changed; if so, we need to unregister and re-register + if @state.registered.mixer.id != mixer.id + logger.debug("unregistering vu due to mixer change", @state.registered.mixer) + context.JK.VuHelpers.unregisterVU(@state.registered.mixer, @state.registered.ptr) + mixerChanged = true + + if !mixerChanged && (@state.registered? || !mixer?) + return + + $root = $(this.getDOMNode()) + + if mixerChanged + logger.debug("re-registering VU #{context.JK.groupIdDisplay(mixer)}", mixer) + else + logger.debug("registered VU #{context.JK.groupIdDisplay(mixer)}", mixer) + + context.JK.VuHelpers.registerVU(@props.side, mixer, @state.ptr, @props.orientation == 'horizontal', @props.lightCount, $root.find('td')) + + @setState(registered: {mixer: mixer, ptr: @state.ptr}) + + + componentWillReceiveProps: (nextProps) -> + @registerVU(nextProps.mixers?.vuMixer) + + componentDidMount: () -> + @registerVU(@props.mixers?.vuMixer) + + componentWillUnmount: () -> + if @state.registered? + logger.debug("unregistered VU #{context.JK.groupIdDisplay(@state.registered.mixer)}") + context.JK.VuHelpers.unregisterVU(@state.registered.mixer, @state.registered.ptr) + +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/SessionTrackVolumeHover.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionTrackVolumeHover.js.jsx.coffee new file mode 100644 index 000000000..e6c6c87cb --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionTrackVolumeHover.js.jsx.coffee @@ -0,0 +1,117 @@ +context = window +ChannelGroupIds = context.JK.ChannelGroupIds +MixerActions = @MixerActions +ptrCount = 0 + +@SessionTrackVolumeHover = React.createClass({ + + mixins: [Reflux.listenTo(@SessionMyTracksStore,"onInputsChanged")] + + closeHover: (e) -> + e.preventDefault() + $container = $(this.getDOMNode()).closest('.react-holder') + $container.data('bt').btOff() + + onInputsChanged: (sessionMixers) -> + + mixers = sessionMixers.mixers + newMixers = mixers.refreshMixer(@state.mixers) + + this.setState({mixers: newMixers}) + + getInitialState: () -> + {mixers: this.props.mixers, ptr: "STVH#{ptrCount++}" } + + handleMute: (e) -> + e.preventDefault() + + muting = $(e.currentTarget).is('.enabled') + + if @state.mixers.mixer.group_id == ChannelGroupIds.AudioInputMusicGroup || @state.mixers.mixer.group_id == ChannelGroupIds.AudioInputChatGroup + MixerActions.mute([this.state.mixers.mixer, this.state.mixers.oppositeMixer], muting) + else + MixerActions.mute([this.state.mixers.mixer], muting) + + + handleMuteCheckbox: (e) -> + muting = $(e.target).is(':checked') + + if @state.mixers.mixer.group_id == ChannelGroupIds.AudioInputMusicGroup || @state.mixers.mixer.group_id == ChannelGroupIds.AudioInputChatGroup + MixerActions.mute([this.state.mixers.mixer, this.state.mixers.oppositeMixer], muting) + else + MixerActions.mute([this.state.mixers.mixer], muting) + + + render: () -> + + muteMixer = this.state.mixers?.muteMixer + + muteMixerId = muteMixer?.id + volume_left = this.state.mixers?.mixer?.volume_left + + classes = classNames({ + 'track-icon-mute': true + 'enabled' : !muteMixer?.mute + 'muted' : muteMixer?.mute + }) + + `
+
+
+ +
+
+ +
+
+
Volume
+
{volume_left}dB
+
+ +
+ + + +
+ +
+

Use this slider to control the volume of this track in your personal mix.

+

This will not affect the volume of this track for other musicians in the session.

+

To adjust master levels for all musicians for recordings and broadcasts, use Mixer button in the toolbar.

+
+ +
+ close +
+
` + + componentDidMount: () -> + $root = jQuery(this.getDOMNode()) + + # initialize icheck + $checkbox = $root.find('input') + context.JK.checkbox($checkbox) + $checkbox.on('ifChanged', this.handleMuteCheckbox); + + if @state.mixers.muteMixer.mute + $checkbox.iCheck('check').attr('checked', true) + else + $checkbox.iCheck('uncheck').attr('checked', false) + + componentWillUpdate: (nextProps, nextState) -> + $root = jQuery(this.getDOMNode()) + + # if the mixers go dead, whack our selves out of existence + unless nextState.mixers? + $container = $root.closest('.react-holder') + $container.data('bt').btOff() + return + + # re-initialize icheck + $checkbox = $root.find('input') + + if nextState.mixers?.muteMixer?.mute + $checkbox.iCheck('check').attr('checked', true) + else + $checkbox.iCheck('uncheck').attr('checked', false) +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/SessionVideoBtn.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionVideoBtn.js.jsx.coffee new file mode 100644 index 000000000..d89f7ae29 --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionVideoBtn.js.jsx.coffee @@ -0,0 +1,16 @@ +context = window +SessionActions = @SessionActions + +@SessionVideoBtn = React.createClass({ + + sessionWebCam: (e) -> + e.preventDefault(); + + SessionActions.toggleSessionVideo() + + render: () -> + ` + + VIDEO + ` +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/SessionVolumeSettingsBtn.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionVolumeSettingsBtn.js.jsx.coffee new file mode 100644 index 000000000..4b9932a6b --- /dev/null +++ b/web/app/assets/javascripts/react-components/SessionVolumeSettingsBtn.js.jsx.coffee @@ -0,0 +1,28 @@ +context = window +MIX_MODES = context.JK.MIX_MODES + +@SessionVolumeSettingsBtn = React.createClass({ + + mixins: [Reflux.listenTo(@MixerStore,"onInputsChanged")] + + onInputsChanged: (sessionMixers) -> + this.setState(sessionMixers) + + render: () -> + ` + + VOLUME + ` + + componentDidMount: () -> + $root = $(this.getDOMNode()) + + context.JK.interactReactBubble( + $root, + 'SessionSelfVolumeHover', + () => + {inputGroupMixers: @state.mixers.getAudioInputCategoryMixer(MIX_MODES.PERSONAL), chatGroupMixers: @state.mixers.getChatCategoryMixer( MIX_MODES.PERSONAL)} + , + {width:470, positions:['right', 'bottom', 'left'], offsetParent:$root.closest('.screen')}) + +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/Test.js.jsx.coffee b/web/app/assets/javascripts/react-components/Test.js.jsx.coffee new file mode 100644 index 000000000..fbdc502a7 --- /dev/null +++ b/web/app/assets/javascripts/react-components/Test.js.jsx.coffee @@ -0,0 +1,19 @@ +context = window + +@TestComponent = React.createClass({ + + getInitialState: () -> + {something: 1} + + tick: () -> + console.log("tick") + this.setState({something: this.state.something + 1}) + + componentDidMount: () -> + console.log("here") + setInterval(@tick, 1000) + + render: () -> + console.log("render") + `
{this.state.something}
` +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/actions/AppActions.js.coffee b/web/app/assets/javascripts/react-components/actions/AppActions.js.coffee new file mode 100644 index 000000000..6c054e3ec --- /dev/null +++ b/web/app/assets/javascripts/react-components/actions/AppActions.js.coffee @@ -0,0 +1,5 @@ +context = window + +@AppActions = Reflux.createActions({ + appInit: {} +}) diff --git a/web/app/assets/javascripts/react-components/actions/BroadcastActions.js.coffee b/web/app/assets/javascripts/react-components/actions/BroadcastActions.js.coffee index 7761257d2..e4bc43707 100644 --- a/web/app/assets/javascripts/react-components/actions/BroadcastActions.js.coffee +++ b/web/app/assets/javascripts/react-components/actions/BroadcastActions.js.coffee @@ -1,6 +1,6 @@ context = window -BroadcastActions = Reflux.createActions({ +@BroadcastActions = Reflux.createActions({ load: {asyncResult: true}, hide: {} }) diff --git a/web/app/assets/javascripts/react-components/actions/MediaPlaybackActions.js.coffee b/web/app/assets/javascripts/react-components/actions/MediaPlaybackActions.js.coffee new file mode 100644 index 000000000..0996e5745 --- /dev/null +++ b/web/app/assets/javascripts/react-components/actions/MediaPlaybackActions.js.coffee @@ -0,0 +1,11 @@ +context = window + +@MediaPlaybackActions = Reflux.createActions({ + playbackStateChange: {} + positionUpdate:{} + mediaStartPlay: {} + mediaStopPlay: {} + mediaPausePlay: {} + mediaChangePosition: {} + currentTimeChanged: {} +}) diff --git a/web/app/assets/javascripts/react-components/actions/MixerActions.js.coffee b/web/app/assets/javascripts/react-components/actions/MixerActions.js.coffee new file mode 100644 index 000000000..5d5678207 --- /dev/null +++ b/web/app/assets/javascripts/react-components/actions/MixerActions.js.coffee @@ -0,0 +1,16 @@ +context = window + +@MixerActions = Reflux.createActions({ + mute: {} + faderChanged: {} + initGain: {} + panChanged: {} + initPan: {} + mixersChanged: {} + syncTracks: {} + mixerModeChanged: {} + loopChanged: {} + openMetronome: {} + metronomeChanged: {} + deadUserRemove: {} +}) diff --git a/web/app/assets/javascripts/react-components/actions/NotificationActions.js.coffee b/web/app/assets/javascripts/react-components/actions/NotificationActions.js.coffee new file mode 100644 index 000000000..481dc8ff8 --- /dev/null +++ b/web/app/assets/javascripts/react-components/actions/NotificationActions.js.coffee @@ -0,0 +1,9 @@ +context = window + +@NotificationActions = Reflux.createActions({ + clear:{} + backendNotification: {} + frontendNotification: {} + sessionEnded: {} +}) + diff --git a/web/app/assets/javascripts/react-components/actions/RecordingActions.js.coffee b/web/app/assets/javascripts/react-components/actions/RecordingActions.js.coffee new file mode 100644 index 000000000..f6b111ac9 --- /dev/null +++ b/web/app/assets/javascripts/react-components/actions/RecordingActions.js.coffee @@ -0,0 +1,14 @@ +context = window + +@RecordingActions = Reflux.createActions({ + initModel: {} + startRecording: {} + stopRecording: {} + startingRecording:{} + startedRecording: {} + stoppingRecording: {} + stoppedRecording: {} + abortedRecording: {} + openRecordingControls: {} + recordingControlsClosed: {} +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/actions/SessionActions.js.coffee b/web/app/assets/javascripts/react-components/actions/SessionActions.js.coffee new file mode 100644 index 000000000..634e7d419 --- /dev/null +++ b/web/app/assets/javascripts/react-components/actions/SessionActions.js.coffee @@ -0,0 +1,22 @@ +context = window + +@SessionActions = Reflux.createActions({ + joinSession: {} + leaveSession: {} + mixersChanged: {} + allowLeaveSession: {} + syncWithServer: {} + toggleSessionVideo : {} + audioResync: {} + openBackingTrack: {} + closeMedia: {} + updateSession: {} + downloadingJamTrack : {} + openMetronome: {} + showNativeMetronomeGui: {} + metronomeCricketChange: {} + windowBackgrounded: {} + broadcastFailure: {} + broadcastSuccess: {} + broadcastStopped: {} +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/actions/SessionMyTracksActions.js.coffee b/web/app/assets/javascripts/react-components/actions/SessionMyTracksActions.js.coffee new file mode 100644 index 000000000..568ba3b29 --- /dev/null +++ b/web/app/assets/javascripts/react-components/actions/SessionMyTracksActions.js.coffee @@ -0,0 +1,5 @@ +context = window + +@SessionMyTracksActions = Reflux.createActions({ + +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/helpers/MixerHelper.js.coffee b/web/app/assets/javascripts/react-components/helpers/MixerHelper.js.coffee new file mode 100644 index 000000000..4800f3925 --- /dev/null +++ b/web/app/assets/javascripts/react-components/helpers/MixerHelper.js.coffee @@ -0,0 +1,850 @@ +context = window + +logger = context.JK.logger +ChannelGroupIds = context.JK.ChannelGroupIds +CategoryGroupIds = context.JK.CategoryGroupIds +MIX_MODES = context.JK.MIX_MODES; + + +@MixerHelper = class MixerHelper + + constructor: (@session, @masterMixers, @personalMixers, @metro, @noAudioUsers, @mixMode) -> + @mixMode = MIX_MODES.PERSONAL # TODO - remove mixMode from MixerHelper? Or at least stop using it in most functions + @app = @session.app + @mixersByResourceId = {} + @mixersByTrackId = {} + @allMixers = {} + @currentMixerRangeMin = null + @currentMixerRangeMax = null + @mediaSummary = {} + @mediaTrackGroups = [ChannelGroupIds.MediaTrackGroup, ChannelGroupIds.JamTrackGroup, + ChannelGroupIds.MetronomeGroup] + @muteBothMasterAndPersonalGroups = [ChannelGroupIds.AudioInputMusicGroup, ChannelGroupIds.MediaTrackGroup, + ChannelGroupIds.JamTrackGroup, ChannelGroupIds.MetronomeGroup] + @vuStats = {} + @shouldCollectVuStats = false + @organize() + + organize: () -> + for masterMixer in @masterMixers + @allMixers['M' + masterMixer.id] = masterMixer; # populate allMixers by mixer.id + + # populate mixer pair + mixerPair = {} + @mixersByResourceId[masterMixer.rid] = mixerPair + @mixersByTrackId[masterMixer.id] = mixerPair + mixerPair.master = masterMixer; + + for personalMixer in @personalMixers + + @allMixers['P' + personalMixer.id] = personalMixer + + # populate other side of mixer pair + + mixerPair = @mixersByResourceId[personalMixer.rid] + unless mixerPair + if personalMixer.group_id != ChannelGroupIds.MonitorGroup + logger.warn("there is no master version of ", personalMixer) + + mixerPair = {} + @mixersByResourceId[personalMixer.rid] = mixerPair + + @mixersByTrackId[personalMixer.id] = mixerPair; + mixerPair.personal = personalMixer; + + @groupTypes() + @chatMixer = @resolveChatMixer() + + groupTypes: () -> + localMediaMixers = @mixersForGroupIds(@mediaTrackGroups, MIX_MODES.MASTER) + peerLocalMediaMixers = @mixersForGroupId(ChannelGroupIds.PeerMediaTrackGroup, MIX_MODES.MASTER) + + #logger.debug("localMediaMixers", localMediaMixers) + #logger.debug("peerLocalMediaMixers", peerLocalMediaMixers) + + # get the server data regarding various media tracks + recordedBackingTracks = @session.recordedBackingTracks() + backingTracks = @session.backingTracks() + recordedJamTracks = @session.recordedJamTracks() + jamTracks = @session.jamTracks() + + ### + with mixer info, we use these to decide what kind of tracks are open in the backend + + each mixer has a media_type field, which describes the type of media track it is. + * JamTrack + * BackingTrack + * RecordingTrack + * MetronomeTrack + * "" - adhoc track (not supported visually) + + it is supposed to be the case that there are only one type of track open at a time, however, that's a business policy/logic + constraint; and may be buggy. **So, we should render whatever we have, so that it's obvious what's really going on.** + + so, let's group up all mixers by type, and then ask them to be rendered + ### + + @recordingTrackMixers = [] + @backingTrackMixers = [] + @jamTrackMixers = [] + @metronomeTrackMixers = [] + @adhocTrackMixers = [] + + groupByType = (mixers, isLocalMixer) => + for mixer in mixers + mediaType = mixer.media_type + groupId = mixer.group_id + + if mediaType == 'MetronomeTrack' || groupId == ChannelGroupIds.MetronomeGroup + # Metronomes come across with a blank media type, so check group_id: + @metronomeTrackMixers.push(mixer) + else if mediaType == null || mediaType == "" || mediaType == 'RecordingTrack' + # additional check; if we can match an id in backing tracks or recorded backing track, + # we need to remove it from the recorded track set, but move it to the backing track set + + isJamTrack = false; + + if jamTracks + # check if the ID matches that of an open jam track + for jamTrack in jamTracks + if mixer.id == jamTrack.id + isJamTrack = true; + break + + if !isJamTrack && recordedJamTracks + # then check if the ID matches that of a open, recorded jam track + for recordedJamTrack in recordedJamTracks + if mixer.id == recordedJamTrack.id + isJamTrack = true + break + + if isJamTrack + @jamTrackMixers.push(mixer) + else + isBackingTrack = false + if recordedBackingTracks + for recordedBackingTrack in recordedBackingTracks + if mixer.id == 'L' + recordedBackingTrack.client_track_id + isBackingTrack = true + break + + if backingTracks + for backingTrack in backingTracks + if mixer.id == 'L' + backingTrack.client_track_id + isBackingTrack = true + break + + if isBackingTrack + @backingTrackMixers.push(mixer) + else + # couldn't resolve this as a JamTrack or Backing track, must be a normal recorded file + @recordingTrackMixers.push(mixer) + + else if mediaType == 'PeerMediaTrack' || mediaType == 'BackingTrack' + @backingTrackMixers.push(mixer) + else if mediaType == 'JamTrack' + @jamTrackMixers.push(mixer); + else if mediaType == null || mediaType == "" || mediaType == 'RecordingTrack' + # mediaType == null is for backwards compat with older clients. Can be removed soon + @recordingTrackMixers.push(mixer) + else + logger.warn("Unknown track type: " + mediaType) + @adhocTrackMixers.push(mixer) + + groupByType(localMediaMixers, true); + groupByType(peerLocalMediaMixers, false); + + ### + if recordingTrackMixers.length > 0 + renderRecordingTracks(recordingTrackMixers) + + if backingTrackMixers.length > 0 + renderBackingTracks(backingTrackMixers) + + if jamTrackMixers.length > 0 + renderJamTracks(jamTrackMixers); + + if metronomeTrackMixers.length > 0 && @session.jamTracks() == null && @session.recordedJamTracks() == null + renderMetronomeTracks(metronomeTrackMixers); + + checkMetronomeTransition(); + ### + + @backingTracks = @resolveBackingTracks() + @jamTracks = @resolveJamTracks() + @recordedTracks = @resolveRecordedTracks() + @metronome = @resolveMetronome() + + + if @adhocTrackMixers.length > 0 + logger.warn("some tracks are open that we don't know how to show") + + @mediaSummary = + recordingOpen: @recordedTracks.length > 0 + jamTrackOpen: @jamTracks.length > 0 + backingTrackOpen: @backingTracks.length > 0 + metronomeOpen: @metronome? + + # figure out if any media is open + mediaOpenSummary = false + for mediaType, mediaOpen of @mediaSummary + mediaOpenSummary = true if mediaOpen + + @mediaSummary.mediaOpen = mediaOpenSummary + + # this method is pretty complicated because it forks on a key bit of state: + # sessionModel.isPlayingRecording() + # a backing track opened as part of a recording has a different behavior and presence on the server (recording.recorded_backing_tracks) + # than a backing track opend ad-hoc (connection.backing_tracks) + + resolveBackingTracks: () -> + backingTracks = [] + + return backingTracks unless @backingTrackMixers.length > 0 + + # find both client and server representation of the backing track + serverBackingTracks = [] + backingTrackMixers = @backingTrackMixers + + if @session.isPlayingRecording() + backingTrackMixers = context._.filter(backingTrackMixers, (mixer) -> return mixer.managed || !mixer.managed?) + serverBackingTracks = @session.recordedBackingTracks() + else + serverBackingTracks = @session.backingTracks(); + backingTrackMixers = context._.filter(backingTrackMixers, (mixer) -> return !mixer.managed) + if backingTrackMixers.length > 1 + logger.error("multiple, managed backing track mixers encountered", backingTrackMixers) + @app.notify({ + title: "Multiple Backing Tracks Encountered", + text: "Only one backing track can be open a time.", + icon_url: "/assets/content/icon_alert_big.png" + }); + return backingTracks; + + # we don't render backing tracks unless we have server data to accompany + if !serverBackingTracks? || serverBackingTracks.length == 0 + return backingTracks + + noCorrespondingTracks = false + for mixer in backingTrackMixers + # find the track or tracks that correspond to the mixer + correspondingTracks = [] + noCorrespondingTracks = false + if @session.isPlayingRecording() + for backingTrack in serverBackingTracks + # occurs if this client is the one that opened the track, # occurs if this client is a remote participant + if mixer.persisted_track_id == backingTrack.client_track_id || mixer.id == 'L' + backingTrack.client_track_id + correspondingTracks.push(backingTrack) + else + # if this is just an open backing track, then we can assume that the 1st backingTrackMixer is ours + correspondingTracks.push(serverBackingTracks[0]) + + if correspondingTracks.length == 0 + noCorrespondingTracks = true + logger.debug("renderBackingTracks: could not map backing tracks") + @app.notify({ + title: "Unable to Open Backing Track", + text: "Could not correlate server and client tracks", + icon_url: "/assets/content/icon_alert_big.png" + }); + break + + # now we have backing track and mixer in hand; we can render + serverBackingTrack = correspondingTracks[0] + + oppositeMixer = @getMixerByResourceId(mixer.rid, MIX_MODES.PERSONAL); + + isOpener = mixer.group_id == ChannelGroupIds.MediaTrackGroup + data = + isOpener: isOpener + shortFilename: context.JK.getNameOfFile(serverBackingTrack.filename) + instrumentIcon: context.JK.getInstrumentIcon45(serverBackingTrack.instrument_id) + photoUrl: "/assets/content/icon_recording.png" + showLoop: isOpener && !@session.isPlayingRecording() + track: serverBackingTrack + mixers: @mediaMixers(mixer, isOpener) + + backingTracks.push(data) + + backingTracks + + resolveJamTracks: () -> + _jamTracks = [] + + return _jamTracks unless @jamTrackMixers.length > 0 + + + jamTrackMixers = @jamTrackMixers.slice(); + jamTracks = [] + jamTrackName = null; + + if @session.isPlayingRecording() + # only return managed mixers for recorded backing tracks + jamTracks = @session.recordedJamTracks() + jamTrackName = @session.recordedJamTrackName() + else + # only return un-managed (ad-hoc) mixers for normal backing tracks + jamTracks = @session.jamTracks() + jamTrackName = @session.jamTrackName() + + # pluck the 1st mixer, and assume that all other mixers in this group are of the same type (between JamTrack vs Peer) + # if it's a locally opened track (JamTrackGroup), then we can say this person is the opener + isOpener = jamTrackMixers[0].group_id == ChannelGroupIds.JamTrackGroup; + + if jamTracks + noCorrespondingTracks = false + for jamTrack in jamTracks + mixer = null + preMasteredClass = "" + # find the track or tracks that correspond to the mixer + correspondingTracks = [] + + for matchMixer in @jamTrackMixers + if matchMixer.id == jamTrack.id + correspondingTracks.push(jamTrack) + mixer = matchMixer + + if correspondingTracks.length == 0 + noCorrespondingTracks = true + logger.error("could not correlate jam tracks", jamTrackMixers, jamTracks) + @app.notify({ + title: "Unable to Open JamTrack", + text: "Could not correlate server and client tracks", + icon_url: "/assets/content/icon_alert_big.png"}) + return _jamTracks + + #jamTracks = $.grep(jamTracks, (value) => + # $.inArray(value, correspondingTracks) < 0 + #) + + # prune found mixers + jamTrackMixers.splice(mixer); + + oneOfTheTracks = correspondingTracks[0]; + instrumentIcon = context.JK.getInstrumentIcon24(oneOfTheTracks.instrument.id); + + part = oneOfTheTracks.part + part = '' unless name? + + data = + name: jamTrackName + part: part + isOpener: isOpener + instrumentIcon: instrumentIcon + track: oneOfTheTracks + mixers: @mediaMixers(mixer, isOpener) + + _jamTracks.push(data) + + _jamTracks + + resolveRecordedTracks: () -> + recordedTracks = [] + + return recordedTracks unless @recordingTrackMixers.length > 0 + + serverRecordedTracks = @session.recordedTracks() + + isOpener = @recordingTrackMixers[0].group_id == ChannelGroupIds.MediaTrackGroup + + # using the server's info in conjuction with the client's, draw the recording tracks + if serverRecordedTracks + recordingName = @session.recordingName() + noCorrespondingTracks = false + for mixer in @recordingTrackMixers + preMasteredClass = "" + correspondingTracks = [] + for recordedTrack in serverRecordedTracks + if mixer.id.indexOf("L") == 0 + if mixer.id.substring(1) == recordedTrack.client_track_id + correspondingTracks.push(recordedTrack) + else if mixer.id.indexOf("C") == 0 + if mixer.id.substring(1) == recordedTrack.client_id + correspondingTracks.push(recordedTrack) + preMasteredClass = "pre-mastered-track" + else + # this should not be possible + alert("Invalid state: the recorded track had neither persisted_track_id or persisted_client_id") + + if correspondingTracks.length == 0 + noCorrespondingTracks = true + logger.debug("unable to correlate all recorded tracks", recordingMixers, serverRecordedTracks) + @app.notify({ + title: "Unable to Open Recording", + text: "Could not correlate server and client tracks", + icon_url: "/assets/content/icon_alert_big.png"}); + return recordedTracks + + serverRecordedTracks = $.grep(serverRecordedTracks, + (value) => + $.inArray(value, correspondingTracks) < 0 + ) + + oneOfTheTracks = correspondingTracks[0] + instrumentIcon = context.JK.getInstrumentIcon24(oneOfTheTracks.instrument_id) + userName = oneOfTheTracks.user.name + userName = oneOfTheTracks.user.first_name + ' ' + oneOfTheTracks.user.last_name unless userName? + + data = + recordingName: recordingName + isOpener: isOpener + userName: userName + instrumentIcon: instrumentIcon + track: oneOfTheTracks + mixers: @mediaMixers(mixer, isOpener) + + recordedTracks.push(data) + + recordedTracks + + resolveMetronome: () -> + metronome = null + + return metronome if @metronomeTrackMixers.length == 0 + + mixer = @metronomeTrackMixers[0] + + instrumentIcon = "/assets/content/icon_metronome.png" + + oppositeMixer = @getMixerByResourceId(mixer.rid, MIX_MODES.PERSONAL); + + metronome = + instrumentIcon: instrumentIcon + mixers: {mixer: mixer, oppositeMixer: oppositeMixer, vuMixer: mixer, muteMixer: mixer} + + metronome + + resolveChatMixer: () -> + masterChatMixers = @mixersForGroupId(ChannelGroupIds.AudioInputChatGroup, MIX_MODES.MASTER); + + return null if masterChatMixers.length == 0 + + personalChatMixers = @mixersForGroupId(ChannelGroupIds.AudioInputChatGroup, MIX_MODES.PERSONAL); + + if personalChatMixers.length == 0 + logger.warn("unable to find personal mixer for voice chat"); + return null + + + masterChatMixer = masterChatMixers[0]; + personalChatMixer = personalChatMixers[0]; + + { + master: { + mixer: masterChatMixer + muteMixer: masterChatMixer + vuMixer: masterChatMixer + oppositeMixer: personalChatMixer + } + personal: { + mixer: personalChatMixer + muteMixer: personalChatMixer + vuMixer: personalChatMixer + oppositeMixer: masterChatMixer + } + } + + # supply the master mixer of a media track, and this function will harvest out the rest + mediaMixers:(masterMixer, isOpener) -> + personalMixer = if isOpener then @getMixerByResourceId(masterMixer.rid, MIX_MODES.PERSONAL) else null + personalVuMixer = if isOpener then personalMixer else masterMixer + { + isOpener: isOpener + + master: { + mixer: masterMixer + muteMixer: masterMixer + vuMixer: masterMixer + } + personal: { + mixer: personalMixer + muteMixer: personalMixer + vuMixer: personalVuMixer + } + } + + + mixersForGroupIds: (groupIds, mixMode) -> + foundMixers = [] + mixers = if mixMode == MIX_MODES.MASTER then @masterMixers else @personalMixers; + + for mixer in mixers + for groupId in groupIds + if mixer.group_id == groupId + foundMixers.push(mixer) + + foundMixers + + mixersForGroupId: (groupId, mixMode) -> + foundMixers = []; + mixers = if mixMode == MIX_MODES.MASTER then @masterMixers else @personalMixers; + for mixer in mixers + if mixer.group_id == groupId + foundMixers.push(mixer) + + foundMixers + + getMixer: (mixerId, mode) -> + mode = @mixMode unless mode? + @allMixers[(if mode then 'M' else 'P') + mixerId] + + getMixerByTrackId: (trackId, mode) -> + mixerPair = @mixersByTrackId[trackId] + + return null unless mixerPair + + if mode == undefined + return mixerPair + + else + if mode == MIX_MODES.MASTER + return mixerPair.master + else + return mixerPair.personal + + + groupedMixersForClientId: (clientId, groupIds, usedMixers, mixMode) -> + foundMixers = {}; + mixers = if mixMode == MIX_MODES.MASTER then @masterMixers else @personalMixers; + + for mixer in mixers + unless mixer? + logger.debug("empty mixer: ", mixers) + continue + + if mixer.client_id == clientId + for groupId in groupIds + if mixer.group_id == groupId + if (mixer.groupId != ChannelGroupIds.UserMusicInputGroup) && !(mixer.id of usedMixers) + mixers = foundMixers[mixer.group_id] + if !mixers + mixers = [] + foundMixers[mixer.group_id] = mixers + mixers.push(mixer) + + foundMixers + + getMixerByResourceId:(resourceId, mode) -> + mixerPair = @mixersByResourceId[resourceId]; + + return null if(!mixerPair) + + if !mode? + return mixerPair; + else + if mode == MIX_MODES.MASTER + return mixerPair.master + else + return mixerPair.personal + + + findMixerForTrack: (client_id, track, myTrack, mode = MIX_MODES.PERSONAL) -> + mixer = null # what is the best mixer for this track/client ID? + oppositeMixer = null # what is the corresponding mixer in the opposite mode? + vuMixer = null + muteMixer = null + + + if myTrack + # when it's your track, look it up by the backend resource ID + mixer = @getMixerByTrackId(track.client_track_id, mode) + vuMixer = mixer + muteMixer = mixer + + # sanity checks + if mixer && mixer.group_id != ChannelGroupIds.AudioInputMusicGroup + logger.error("found local mixer that was not of groupID: AudioInputMusicGroup", mixer) + + if mixer + # find the matching AudioInputMusicGroup for the opposite mode + oppositeMixer = @getMixerByTrackId(track.client_track_id, !mode) + + if mode == MIX_MODES.PERSONAL + muteMixer = oppositeMixer; # make the master mixer the mute mixer + + # sanity checks + if !oppositeMixer + logger.error("unable to find opposite mixer for local mixer", mixer) + else if oppositeMixer.group_id != ChannelGroupIds.AudioInputMusicGroup + logger.error("found local mixer in opposite mode that was not of groupID: AudioInputMusicGroup", mixer, oppositeMixer) + else + logger.debug("local track is not present: ", track, @allMixers) + else + switch mode + when MIX_MODES.MASTER + + # when it's a remote track and in master mode, we should find the PeerAudioInputMusicGroup + mixer = @getMixerByTrackId(track.client_track_id, MIX_MODES.MASTER) + + # sanity check + if mixer && mixer.group_id != ChannelGroupIds.PeerAudioInputMusicGroup + logger.error("found remote mixer that was not of groupID: PeerAudioInputMusicGroup", mixer) + + vuMixer = mixer + muteMixer = mixer + + if mixer + # we should be able to find a UserMusicInputGroup for this clientId in personal mode + oppositeMixers = @groupedMixersForClientId(client_id, [ ChannelGroupIds.UserMusicInputGroup], {}, MIX_MODES.PERSONAL) + if oppositeMixers[ChannelGroupIds.UserMusicInputGroup] + oppositeMixer = oppositeMixers[ChannelGroupIds.UserMusicInputGroup][0] + + if !oppositeMixer + logger.error("unable to find UserMusicInputGroup corresponding to PeerAudioInputMusicGroup mixer", mixer ) + + when MIX_MODES.PERSONAL + mixers = @groupedMixersForClientId(client_id, [ ChannelGroupIds.UserMusicInputGroup], {}, MIX_MODES.PERSONAL) + if mixers[ChannelGroupIds.UserMusicInputGroup] + mixer = mixers[ChannelGroupIds.UserMusicInputGroup][0] + + vuMixer = mixer + muteMixer = mixer + + if mixer + # now grab the PeerAudioInputMusicGroup in master mode to satisfy the 'opposite' mixer + oppositeMixer = @getMixerByTrackId(track.client_track_id, MIX_MODES.MASTER) + if !oppositeMixer + logger.debug("unable to find a PeerAudioInputMusicGroup master mixer matching a UserMusicInput", client_id, track.client_track_id) + else if oppositeMixer.group_id != ChannelGroupIds.PeerAudioInputMusicGroup + logger.error("found remote mixer that was not of groupID: PeerAudioInputMusicGroup", mixer) + + #vuMixer = oppositeMixer; # for personal mode, use the PeerAudioInputMusicGroup's VUs + + { + mixer: mixer, + oppositeMixer: oppositeMixer, + vuMixer: vuMixer, + muteMixer: muteMixer + } + + mute: (mixerId, mode, muting) -> + + mode = @mixMode unless mode? + + @fillTrackVolumeObject(mixerId, mode) + + context.trackVolumeObject.mute = muting + + context.jamClient.SessionSetControlState(mixerId, mode) + + # keep state of mixer in sync with backend + mixer = @getMixer(mixerId, mode) + mixer.mute = muting + + faderChanged: (data, mixers, groupId) -> + for mixer in mixers + broadcast = !(data.dragging) # If fader is still dragging, don't broadcast + mixer = @fillTrackVolumeObject(mixer.id, mixer.mode, broadcast) + + @setMixerVolume(mixer, data.percentage) + + # keep state of mixer in sync with backend + mixer = @getMixer(mixer.id, mixer.mode) + mixer.volume_left = context.trackVolumeObject.volL + + if groupId == ChannelGroupIds.UserMusicInputGroup + # there may be other mixers with this same ID in the case of a Peer Music Stream, so update them as well + context.JK.FaderHelpers.setFaderValue(mixerId, data.percentage) + + initGain: (mixer) -> + gainPercent = context.JK.FaderHelpers.convertAudioTaperToPercent(mixer.volume_left) + context.JK.FaderHelpers.setFaderValue(mixer.id, gainPercent) + context.JK.FaderHelpers.showFader(mixer.id) + + panChanged: (data, mixers, groupId) -> + # media tracks are the only controls that sometimes set two mixers right now + for mixer in mixers + broadcast = !(data.dragging) # If fader is still dragging, don't broadcast + mixer = @fillTrackVolumeObject(mixer.id, mixer.mode, broadcast) + + @setMixerPan(mixer, data.percentage) + + # keep state of mixer in sync with backend + mixer = @getMixer(mixer.id, mixer.mode) + mixer.pan = context.trackVolumeObject.pan + + initPan: (mixer) -> + panPercent= context.JK.PanHelpers.convertPanToPercent(mixer.pan) + context.JK.FaderHelpers.setFaderValue(mixer.id, panPercent, Math.abs(mixer.pan)) + context.JK.FaderHelpers.showFader(mixer.id) + + setMixerPan: (mixer, panPercent) -> + + context.trackVolumeObject.pan = context.JK.PanHelpers.convertPercentToPan(panPercent); + context.jamClient.SessionSetControlState(mixer.id, mixer.mode); + + loopChanged: (mixer, shouldLoop) -> + + @fillTrackVolumeObject(mixer.id, mixer.mode) + context.trackVolumeObject.loop = shouldLoop + context.jamClient.SessionSetControlState(mixer.id, mixer.mode) + + # keep state of mixer in sync with backend + mixer = @getMixer(mixer.id, mixer.mode) + mixer.loop = context.trackVolumeObject.loop + + setMixerVolume: (mixer, volumePercent) -> + ### + // The context.trackVolumeObject has been filled with the mixer values + // that go with mixerId, and the range of that mixer + // has been set in currentMixerRangeMin-Max. + // All that needs doing is to translate the incoming percent + // into the real value ont the sliders range. Set Left/Right + // volumes on trackVolumeObject, and call SetControlState to stick. + ### + + context.trackVolumeObject.volL = context.JK.FaderHelpers.convertPercentToAudioTaper(volumePercent); + context.trackVolumeObject.volR = context.JK.FaderHelpers.convertPercentToAudioTaper(volumePercent); + + context.jamClient.SessionSetControlState(mixer.id, mixer.mode); + + percentFromMixerValue: (min, max, value) -> + try + range = Math.abs(max - min) + magnitude = value - min + percent = Math.round(100*(magnitude/range)) + percent + catch err + 0 + + + percentToMixerValue:(min, max, percent) -> + range = Math.abs(max - min); + multiplier = percent/100; # Change 85 into 0.85 + value = min + (multiplier * range); + + # Protect against percents < 0 and > 100 + if value < min + value = min; + + if value > max + value = max; + + return value; + + fillTrackVolumeObject: (mixerId, mode, broadcast) -> + _broadcast = true + if broadcast? + _broadcast = broadcast + + mixer = @getMixer(mixerId, mode) + context.trackVolumeObject.clientID = mixer.client_id + context.trackVolumeObject.broadcast = _broadcast + context.trackVolumeObject.master = mixer.master + context.trackVolumeObject.monitor = mixer.monitor + context.trackVolumeObject.mute = mixer.mute + context.trackVolumeObject.name = mixer.name + context.trackVolumeObject.record = mixer.record + context.trackVolumeObject.volL = mixer.volume_left + context.trackVolumeObject.pan = mixer.pan + + # today we treat all tracks as mono, but this is required to make a stereo track happy + # context.trackVolumeObject.volR = mixer.volume_right; + context.trackVolumeObject.volR = mixer.volume_left; + + context.trackVolumeObject.loop = mixer.loop; + # trackVolumeObject doesn't have a place for range min/max + @currentMixerRangeMin = mixer.range_low; + @currentMixerRangeMax = mixer.range_high; + mixer + + collectStats: (mixer) -> + mixerStats = @vuStats[mixer.id] + + unless mixerStats? + mixerStats = {count: 0, group_name: context.JK.groupIdDisplay(mixer)} + @vuStats[mixer.id] = mixerStats + + mixerStats.count++ + + dumpVUStats: () -> + + # to use: check MixerStore for setInterval in cstr + logger.debug("VU STAT DUMP") + for mixerId, mixerStat of @vuStats + logger.debug("VU STAT: #{mixerState.group_name} count=#{mixerStat.count}") + + updateVU: (mixerId, mode, leftValue, leftClipping, rightValue, rightClipping) -> + mixer = @getMixer(mixerId, mode) + + if mixer? + @collectStats(mixer) if @shouldCollectVuStats + context.JK.VuHelpers.updateVU3(mixer, leftValue, leftClipping, rightValue, rightClipping) + + ### + if mixer + if mixer.stereo # // stereo track + if mixerId.substr(-4) == "_vul" + context.JK.VuHelpers.updateVU2('vul', mixer, value) + else + context.JK.VuHelpers.updateVU2('vur', mixer, value) + else + if mixerId.substr(-4) == "_vul" + # Do the left + context.JK.VuHelpers.updateVU2('vul', mixer, value) + # Do the right + context.JK.VuHelpers.updateVU2('vur', mixer, value) + ### + getTrackInfo: () -> + context.JK.TrackHelpers.getTrackInfo(context.jamClient, @masterMixers) + + getGroupMixer: (categoryId, mode) -> + groupId = if mode == MIX_MODES.MASTER then ChannelGroupIds.MasterCatGroup else ChannelGroupIds.MonitorCatGroup + mixers = @mixersForGroupId(groupId, mode) + + if mixers.length == 0 + logger.warn("could not find mixer with group ID: " + groupId + ', mode:' + mode) + return null + + found = null + for mixer in mixers + if mixer.name == categoryId + found = mixer + break + + unless found? + logger.warn("could not find mixer with categoryId: " + categoryId) + return null + else + { + mixer: found, + muteMixer : found, + vuMixer: found, + oppositeMixer: found + } + + getAudioInputCategoryMixer: (mode) -> + @getGroupMixer(CategoryGroupIds.AudioInputMusic, mode) + + getChatCategoryMixer: (mode) -> + @getGroupMixer(CategoryGroupIds.AudioInputChat, mode) + + getMediaCategoryMixer: (mode) -> + @getGroupMixer(CategoryGroupIds.MediaTrack, mode) + + getUserMediaCategoryMixer: (mode) -> + @getGroupMixer(CategoryGroupIds.UserMedia, mode) + + + refreshMixer: (mixers) -> + return null unless mixers? && mixers.mixer? + + mixer = @getMixer(mixers.mixer.id, mixers.mixer.mode) + + if mixer? + oppositeMixer = if mixers.oppositeMixer then @getMixer(mixers.oppositeMixer.id, mixers.oppositeMixer.mode) else null + { + mixer: mixer + vuMixer: @getMixer(mixers.vuMixer.id, mixers.vuMixer.mode) + muteMixer: @getMixer(mixers.muteMixer.id, mixers.muteMixer.mode) + oppositeMixer: oppositeMixer + } + else + return null + + + recordingName: () -> + @session.recordingName() + + jamTrackName: () -> + @session.jamTrackName() diff --git a/web/app/assets/javascripts/react-components/helpers/SessionHelper.js.coffee b/web/app/assets/javascripts/react-components/helpers/SessionHelper.js.coffee new file mode 100644 index 000000000..8fe3d6098 --- /dev/null +++ b/web/app/assets/javascripts/react-components/helpers/SessionHelper.js.coffee @@ -0,0 +1,112 @@ +context = window + +@SessionHelper = class SessionHelper + + constructor: (app, session, participantsEverSeen, isRecording, downloadingJamTrack) -> + @app = app + @session = session + @participantsEverSeen = participantsEverSeen + @isRecording = isRecording + @downloadingJamTrack = downloadingJamTrack + + inSession: () -> + @session? + + participants: () -> + if @session + return @session.participants + else + [] + + otherParticipants: () -> + others = [] + for participant in @participants() + myTrack = @app.clientId == participant.client_id + + others.push(participant) unless myTrack + others + + # if any participant has the metronome open, then we say this session has the metronome open + isMetronomeOpen: () -> + metronomeOpen = false; + for participant in @participants() + if participant.metronome_open + metronomeOpen = true + break + + metronomeOpen + + isPlayingRecording: () -> + # this is the server's state; there is no guarantee that the local tracks + # requested from the backend will have corresponding track information + return !!(@session && @session.claimed_recording); + + recordedTracks: () -> + if @session && @session.claimed_recording + @session.claimed_recording.recording.recorded_tracks + else + null + + recordedBackingTracks: () -> + if @session && @session.claimed_recording + @session.claimed_recording.recording.recorded_backing_tracks + else + null + + backingTracks: () -> + backingTracks = [] + # this may be wrong if we loosen the idea that only one person can have a backing track open. + # but for now, the 1st person we find with a backing track open is all there is to find... + + for participant in @participants() + if participant.backing_tracks.length > 0 + backingTracks = participant.backing_tracks + break + + backingTracks + + backingTrack: () -> + result = null + if @session + # TODO: objectize this for VRFS-2665, VRFS-2666, VRFS-2667, VRFS-2668 + result = + path: @session.backing_track_path + result + + jamTracks: () -> + if @session && @session.jam_track + @session.jam_track.tracks.filter((track)-> + track.track_type == 'Track' + ) + else + null + + jamTrackName: () -> + @session?.jam_track?.name + + recordedJamTracks:() -> + if @session && @session.claimed_recording + @session.claimed_recording.recording.recorded_jam_track_tracks + else + null + + recordedJamTrackName: () -> + jam_track = @session?.claimed_recording?.recording?.jam_track + + if jam_track? then jam_track.name else null + + recordingName: () -> + @session?.claimed_recording?.name + + getParticipant: (clientId) -> + found = null + for participant in @participants() + if participant.client_id == clientId + found = participant + break + + logger.warn('unable to find participant with clientId: ' + clientId) unless found + found + + id: () -> + @session.id \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/landing/InvidualJamTrackPage.js.jsx.coffee b/web/app/assets/javascripts/react-components/landing/InvidualJamTrackPage.js.jsx.coffee new file mode 100644 index 000000000..809a273a6 --- /dev/null +++ b/web/app/assets/javascripts/react-components/landing/InvidualJamTrackPage.js.jsx.coffee @@ -0,0 +1,52 @@ +context = window + +@IndividualJamTrackPage = React.createClass({ + + watchVideo: (e) -> + e.preventDefault() + window.open("/popups/youtube/player?id=askHvcCoNfw", 'What Are JamTracks?', 'scrollbars=yes,toolbar=no,status=no,height=282,width=500') + + render: () -> + + header = null + if @props.band + header = "#{@props.jam_track.original_artist} Backing Tracks - Complete Multitracks" + else if @props.generic? + header = "Backing Tracks + Free Amazing App = Unmatched Experience" + else + header = "#{@props.jam_track.name} Backing Track by #{@props.jam_track.original_artist}" + + + `
+
+

{header}

+
+
+
+

Here's Why 20,000 Musicians Love Our Backing Tracks

+

JamKazam gives you a better backing track experience:

+
    +
  • Full multitrack recordings with isolated track for each part
  • +
  • Free JamKazam app to: +
      +
    • Hear just the part you want to play to learn it
    • +
    • Mute the part you want to play, and play live with other parts
    • +
    • Record and mix your live play with unmuted tracks
    • +
    +
  • +
  • Free Internet Service to play this track live online with others
  • +
+ Watch A Video To See How It Works +
+
+

Preview "{this.props.jam_track.name}" Backing Track by {this.props.jam_track.original_artist}

+

Click the play buttons below to preview the master mix and fully isolated tracks of the professional backing track recording. All are included in your backing track.

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