From 53d62f108787a3b7967eddf95674f2d7151b941d Mon Sep 17 00:00:00 2001 From: Seth Call Date: Sat, 20 Jun 2015 14:03:24 -0500 Subject: [PATCH 01/89] * synchronize jam-admin and jam-web secret token --- admin/config/initializers/secret_token.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/admin/config/initializers/secret_token.rb b/admin/config/initializers/secret_token.rb index f71a3cafa..f1967d9d5 100644 --- a/admin/config/initializers/secret_token.rb +++ b/admin/config/initializers/secret_token.rb @@ -4,4 +4,4 @@ # If you change this key, all old signed cookies will become invalid! # Make sure the secret is at least 30 characters and all random, # no regular words or you'll be exposed to dictionary attacks. -JamAdmin::Application.config.secret_token = 'e27a8deff5bc124d1c74cb86ebf38ac4a246091b859bcccf4f8076454e0ff3e04ffc87c9a0f4ddc801c4753e20b2a3cf06e5efc815cfe8e6377f912b737c5f77' +JamAdmin::Application.config.secret_token = 'ced345e01611593c1b783bae98e4e56dbaee787747e92a141565f7c61d0ab2c6f98f7396fb4b178258301e2713816e158461af58c14b695901692f91e72b6200' From 5c79bdab5c48fbbbfebdf70433dc2d6655d83491 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Sat, 20 Jun 2015 14:49:11 -0500 Subject: [PATCH 02/89] * make unsubscribed work, and only dump first_name --- admin/app/views/email/dump_emailables.csv.erb | 2 +- web/app/controllers/users_controller.rb | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/admin/app/views/email/dump_emailables.csv.erb b/admin/app/views/email/dump_emailables.csv.erb index 778d5dc89..efa44b0f8 100644 --- a/admin/app/views/email/dump_emailables.csv.erb +++ b/admin/app/views/email/dump_emailables.csv.erb @@ -1,2 +1,2 @@ <%- headers = ['email', 'name', 'unsubscribe_token'] -%> -<%= CSV.generate_line headers %><%- @users.each do |user| -%><%= CSV.generate_line([user.email, user.name, user.unsubscribe_token]) %><%- end -%> \ No newline at end of file +<%= CSV.generate_line headers %><%- @users.each do |user| -%><%= CSV.generate_line([user.email, user.first_name, user.unsubscribe_token]) %><%- end -%> \ No newline at end of file diff --git a/web/app/controllers/users_controller.rb b/web/app/controllers/users_controller.rb index 92fbb1f52..7824a0fe8 100644 --- a/web/app/controllers/users_controller.rb +++ b/web/app/controllers/users_controller.rb @@ -402,12 +402,14 @@ JS redirect_to '/' end if params[:user_token].present? - if request.get? + #if request.get? - elsif request.post? + #elsif request.post? @user.subscribe_email = false @user.save! - end + #end + + render text: 'You have been unsubscribed.' end private From 614cfcbe853ec3dc611950981cf03f5be8c5189b Mon Sep 17 00:00:00 2001 From: Steven Miers Date: Mon, 6 Jul 2015 15:34:27 -0500 Subject: [PATCH 03/89] Merge feature/calendaring branch: commit 8023d6481cbadd52e58b9a4342ac7636ce1807e3 VRFS-3276 : Hook calendar creation into user controller API. Add test to verify. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit commit 3a35002a46f870e2c490b88b3187e0b1569494fd VRFS-3276 : Calendar cleanup job * Add cleanup method to calendar manager * Create a daily job. * Add calendar cleanup to that job. * Add CRON entry * Daily job/ calendar cleanup test cases * Fix calendar manager spec for new required attribute commit 3ff5910f1f019ae8bcb5afe72a31f1d38bb7d7a3 VRFS-3276 : Add a delete-calendar directive when RSVP is canceled. VRFS-3276 : Include path to partial. This fails depending on the method used to start the web server. commit d2441cbf57e50895ac3b40534873c5d529cb3c4f VRFS-3276 : Test new calendar features. Use icalendar gem in test mode only to more deeply verify calendar in strict mode. commit 9ac272a0fb1e58d8cf9f02e7a0e04caada41f659 VRFS-3276 : Calendar manager updates to include manual calendars. Some refactoring to keep common stuff in one place. commit b5d0c758f0dcae41a5f24635e9da9ce6eda56670 VRFS-3276 : Schema, model updates and new calendar model. commit 20472b6b26c88c04edb9bc698e0c06c549e12eb5 VRFS-3276 : Change initial submit behavior of RSVP dialog to display calendar info. The user can then close the dialog after this prompt. commit 77c99103d0221f20ea342169821b90fa987ecf93 VRFS-3276 : Calendar feed markup and styling. Included as partial. commit e632f48600ae23b5f742773310b2a4ac16ae4ee8 VRFS-3276 : Routes and controller implementation of user calendar ICS feed, which uses calendar manager. commit 21fd80a188eae771a65333566b804ade795a1e8c VRFS-3276 : Initial tests for calendar manager commit 92a2524c65abf7b540f9d50049a1b760a5a9927f VRFS-3276 : Calendar manager * Streamline logic * Enable recurring sessions through rrule * Implement method to create ics feed for user * Extract a type-safe scheduled duration method on music_session for external and internal use. commit b71ad3a4cdd943eb84748abaa85fec263b9af468 VRFS-3276 : Include calendar manager commit f8eaafd03647613dafec9f9422282f8613d08e9a VRFS-3276 : Calendar Manager - initial checkin * Create ICS events given individual parameters * Create calendar from music session * Also will create ICS “delete” events --- db/Gemfile.lock | 3 - db/manifest | 1 + db/up/calendar.sql | 13 +++ ruby/Gemfile | 1 + ruby/jt_metadata.json | 2 +- ruby/lib/jam_ruby.rb | 3 + ruby/lib/jam_ruby/calendar_manager.rb | 106 ++++++++++++++++++ ruby/lib/jam_ruby/models/calendar.rb | 14 +++ ruby/lib/jam_ruby/models/music_session.rb | 27 +++-- ruby/lib/jam_ruby/models/rsvp_request.rb | 10 ++ ruby/lib/jam_ruby/models/user.rb | 17 +++ .../jam_ruby/resque/scheduled/daily_job.rb | 17 +++ ruby/spec/jam_ruby/calendar_manager_spec.rb | 85 ++++++++++++++ .../spec/jam_ruby/models/rsvp_request_spec.rb | 14 ++- .../resque/scheduled_daily_job_spec.rb | 43 +++++++ .../javascripts/dialog/rsvpSubmitDialog.js | 8 +- .../stylesheets/client/account.css.scss | 10 ++ .../stylesheets/dialogs/rsvpDialog.css.scss | 27 +++++ web/app/controllers/api_users_controller.rb | 77 +++++++------ .../views/clients/_account_sessions.html.haml | 4 + web/app/views/clients/_calendar.html.slim | 9 ++ .../views/dialogs/_rsvpSubmitDialog.html.haml | 29 +++-- web/config/routes.rb | 2 +- web/config/scheduler.yml | 5 + .../controllers/api_users_controller_spec.rb | 27 ++++- 25 files changed, 485 insertions(+), 69 deletions(-) create mode 100644 db/up/calendar.sql create mode 100644 ruby/lib/jam_ruby/calendar_manager.rb create mode 100644 ruby/lib/jam_ruby/models/calendar.rb create mode 100644 ruby/lib/jam_ruby/resque/scheduled/daily_job.rb create mode 100644 ruby/spec/jam_ruby/calendar_manager_spec.rb create mode 100644 ruby/spec/jam_ruby/resque/scheduled_daily_job_spec.rb create mode 100644 web/app/views/clients/_calendar.html.slim diff --git a/db/Gemfile.lock b/db/Gemfile.lock index 8d6d039c2..eb6aee107 100644 --- a/db/Gemfile.lock +++ b/db/Gemfile.lock @@ -16,6 +16,3 @@ PLATFORMS DEPENDENCIES pg_migrate (= 0.1.13) - -BUNDLED WITH - 1.10.3 diff --git a/db/manifest b/db/manifest index a3bddb189..8c20bb7ce 100755 --- a/db/manifest +++ b/db/manifest @@ -295,3 +295,4 @@ affiliate_partners2.sql enhance_band_profile.sql broadcast_notifications.sql broadcast_notifications_fk.sql +calendar.sql \ No newline at end of file diff --git a/db/up/calendar.sql b/db/up/calendar.sql new file mode 100644 index 000000000..5e27c7f14 --- /dev/null +++ b/db/up/calendar.sql @@ -0,0 +1,13 @@ +CREATE TABLE calendars ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4() NOT NULL, + user_id VARCHAR(64) NOT NULL REFERENCES users(id) ON DELETE CASCADE, + target_uid VARCHAR(64) NOT NULL, + name VARCHAR(128), + description VARCHAR(8000), + trigger_delete BOOLEAN DEFAULT FALSE, + start_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, + end_at TIMESTAMP WITHOUT TIME ZONE NOT NULL, + recurring_mode VARCHAR(50) NOT NULL DEFAULT 'once', + created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL, + updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL +); \ No newline at end of file diff --git a/ruby/Gemfile b/ruby/Gemfile index d46e44e82..0fa0fcb34 100644 --- a/ruby/Gemfile +++ b/ruby/Gemfile @@ -64,6 +64,7 @@ group :test do gem 'rspec-prof' gem 'time_difference' gem 'byebug' + gem 'icalendar' end # Specify your gem's dependencies in jam_ruby.gemspec diff --git a/ruby/jt_metadata.json b/ruby/jt_metadata.json index cc85875b4..fdcf32faf 100644 --- a/ruby/jt_metadata.json +++ b/ruby/jt_metadata.json @@ -1 +1 @@ -{"container_file": "/var/folders/fk/0ckzmddd4tq28kxbb09vckbr0000gn/T/d20150519-97259-1h1tbhj/jam-track-35.jkz", "version": "0", "coverart": null, "rsa_priv_file": "/var/folders/fk/0ckzmddd4tq28kxbb09vckbr0000gn/T/d20150519-97259-1h1tbhj/skey.pem", "tracks": [{"name": "/var/folders/fk/0ckzmddd4tq28kxbb09vckbr0000gn/T/d20150519-97259-1h1tbhj/7452fa4a-0c55-4cb2-948e-221475d7299c.ogg", "trackName": "track_00"}], "rsa_pub_file": "/var/folders/fk/0ckzmddd4tq28kxbb09vckbr0000gn/T/d20150519-97259-1h1tbhj/pkey.pem", "jamktrack_info": "/var/folders/fk/0ckzmddd4tq28kxbb09vckbr0000gn/T/tmpGdncJS"} \ No newline at end of file +{"container_file": "/var/folders/fk/0ckzmddd4tq28kxbb09vckbr0000gn/T/d20150706-18103-9lb217/jam-track-45.jkz", "version": "0", "coverart": null, "rsa_priv_file": "/var/folders/fk/0ckzmddd4tq28kxbb09vckbr0000gn/T/d20150706-18103-9lb217/skey.pem", "tracks": [{"name": "/var/folders/fk/0ckzmddd4tq28kxbb09vckbr0000gn/T/d20150706-18103-9lb217/4630741c-69a1-4bc6-8a9f-ec70cb5cd401.ogg", "trackName": "track_00"}], "rsa_pub_file": "/var/folders/fk/0ckzmddd4tq28kxbb09vckbr0000gn/T/d20150706-18103-9lb217/pkey.pem", "jamktrack_info": "/var/folders/fk/0ckzmddd4tq28kxbb09vckbr0000gn/T/tmpmwZtC7"} \ No newline at end of file diff --git a/ruby/lib/jam_ruby.rb b/ruby/lib/jam_ruby.rb index f132f7b10..969f026ef 100755 --- a/ruby/lib/jam_ruby.rb +++ b/ruby/lib/jam_ruby.rb @@ -51,6 +51,7 @@ require "jam_ruby/resque/scheduled/icecast_source_check" require "jam_ruby/resque/scheduled/cleanup_facebook_signup" require "jam_ruby/resque/scheduled/unused_music_notation_cleaner" require "jam_ruby/resque/scheduled/user_progress_emailer" +require "jam_ruby/resque/scheduled/daily_job" require "jam_ruby/resque/scheduled/daily_session_emailer" require "jam_ruby/resque/scheduled/new_musician_emailer" require "jam_ruby/resque/scheduled/music_session_scheduler" @@ -94,6 +95,7 @@ require "jam_ruby/amqp/amqp_connection_manager" require "jam_ruby/database" require "jam_ruby/message_factory" require "jam_ruby/models/backing_track" +require "jam_ruby/models/calendar" require "jam_ruby/models/feedback" require "jam_ruby/models/feedback_observer" #require "jam_ruby/models/max_mind_geo" @@ -227,6 +229,7 @@ require "jam_ruby/models/sale_line_item" require "jam_ruby/models/recurly_transaction_web_hook" require "jam_ruby/models/broadcast_notification" require "jam_ruby/models/broadcast_notification_view" +require "jam_ruby/calendar_manager" require "jam_ruby/jam_tracks_manager" require "jam_ruby/jam_track_importer" require "jam_ruby/jmep_manager" diff --git a/ruby/lib/jam_ruby/calendar_manager.rb b/ruby/lib/jam_ruby/calendar_manager.rb new file mode 100644 index 000000000..523e683d9 --- /dev/null +++ b/ruby/lib/jam_ruby/calendar_manager.rb @@ -0,0 +1,106 @@ +module JamRuby + class CalendarManager < BaseManager + DATE_FORMAT="%Y%m%dT%H%M%SZ" + def initialize(options={}) + super(options) + @log = Logging.logger[self] + end + + def cancel_ics_event(music_session, user) + Calendar.where( + user_id: user.id, + target_uid: music_session.id, + name: music_session.description) + .first_or_create( + description: music_session.description, + start_at: music_session.scheduled_start, + end_at: music_session.scheduled_start+music_session.safe_scheduled_duration, + trigger_delete: true) + + end + + # Remove all "delete" event calendar records older than 4 weeks: + def cleanup() + Calendar.where("trigger_delete=TRUE AND created_at < ?", 4.weeks.ago) + .destroy_all() + end + + # @return event (as ICS string) for a given music session + def ics_event_from_music_session(music_session, delete=false) + # Determine properties of calendar event and create: + uid = "#{music_session.id}@JamKazam" + text = "JamKazam Session #{music_session.description}" + rrule = nil + start_at = music_session.scheduled_start + stop_at = music_session.scheduled_start+music_session.safe_scheduled_duration + if !delete && music_session.recurring_mode==MusicSession::RECURRING_WEEKLY + rrule = "FREQ=WEEKLY;INTERVAL=1" + end + create_ics_event(uid, text, text, start_at, stop_at, delete, rrule) + end + + # @return event (as ICS string) for a given music session + def ics_event_from_calendar(calendar) + # Determine properties of calendar event and create: + rrule = nil + if !calendar.trigger_delete && calendar.recurring_mode==MusicSession::RECURRING_WEEKLY + rrule = "FREQ=WEEKLY;INTERVAL=1" + end + + create_ics_event( + calendar.target_uid, + "JamKazam Session #{calendar.name}", + calendar.description, + calendar.start_at, + calendar.end_at, + calendar.trigger_delete, + rrule + ) + end + + # @return calendar (as ICS string) for specified user + # Includes all RSVPed sessions, as well as any calendar + # entries for the given user: + def create_ics_feed(user) + ics_events = "" + MusicSession.scheduled_rsvp(user, true).each do |music_session| + ics_events << "\r\n" if(ics_events.length != 0) + ics_events << ics_event_from_music_session(music_session) + end + + user.calendars.each do |user| + ics_events << "\r\n" if(ics_events.length != 0) + ics_events << ics_event_from_calendar(user) + end + + create_ics_cal(ics_events) + end + + # @return event (as ICS string) for given arguments + def create_ics_event(uuid, name, description, start_at, end_at, delete=false, rrule=nil, sequence=nil) + uuid ||= UUID.timestamp_create + event = "BEGIN:VEVENT\r\n" + event << "UID:#{uuid}\r\n" + event << "DTSTAMP:#{Time.now.utc().strftime(DATE_FORMAT)}\r\n" + event << "DTSTART:#{start_at.utc().strftime(DATE_FORMAT)}\r\n" + event << "DTEND:#{end_at.utc().strftime(DATE_FORMAT)}\r\n" + event << "SUMMARY:#{name}\r\n" + event << "DESCRIPTION:#{description}\r\n" + if delete + event << "METHOD:CANCEL\r\n" + event << "STATUS:CANCELLED\r\n" + end + if rrule + event << "RRULE:#{rrule}\r\n" + end + event << "SEQUENCE:#{sequence}\r\n" if sequence + event << "END:VEVENT" + end + + # @return calendar (as ICS string) for specified events + def create_ics_cal(ics_events) + "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:JamKazam\r\n#{ics_events}\r\nEND:VCALENDAR" + end + + end # class +end # module \ No newline at end of file diff --git a/ruby/lib/jam_ruby/models/calendar.rb b/ruby/lib/jam_ruby/models/calendar.rb new file mode 100644 index 000000000..4f244b587 --- /dev/null +++ b/ruby/lib/jam_ruby/models/calendar.rb @@ -0,0 +1,14 @@ +module JamRuby + class Calendar < ActiveRecord::Base + include HtmlSanitize + html_sanitize strict: [:name, :description] + attr_accessible :name, :description, :target_uid, :trigger_delete, :start_at, :end_at + + @@log = Logging.logger[Calendar] + + self.table_name = "calendars" + self.primary_key = 'id' + + belongs_to :user, :class_name => 'JamRuby::User', :foreign_key => :user_id, :inverse_of => :calendars + end +end diff --git a/ruby/lib/jam_ruby/models/music_session.rb b/ruby/lib/jam_ruby/models/music_session.rb index 96b9b7831..94b2295d4 100644 --- a/ruby/lib/jam_ruby/models/music_session.rb +++ b/ruby/lib/jam_ruby/models/music_session.rb @@ -880,6 +880,21 @@ SQL end result end + + def safe_scheduled_duration + duration = scheduled_duration + # you can put seconds into the scheduled_duration field, but once stored, it comes back out as a string + if scheduled_duration.class == String + begin + bits = scheduled_duration.split(':') + duration = bits[0].to_i.hours + bits[1].to_i.minutes + bits[2].to_i.seconds + rescue Exception => e + duration = 1.hours + @@log.error("unable to parse duration #{scheduled_duration}") + end + end + duration + end # should create a timestamp like: # # with_timezone = TRUE @@ -910,17 +925,7 @@ SQL end end - duration = scheduled_duration - # you can put seconds into the scheduled_duration field, but once stored, it comes back out as a string - if scheduled_duration.class == String - begin - bits = scheduled_duration.split(':') - duration = bits[0].to_i.hours + bits[1].to_i.minutes + bits[2].to_i.seconds - rescue Exception => e - duration = 1.hours - @@log.error("unable to parse duration #{scheduled_duration}") - end - end + duration = safe_scheduled_duration end_time = start_time + duration if with_timezone "#{start_time.strftime("%A, %B %e")}, #{start_time.strftime("%l:%M").strip}-#{end_time.strftime("%l:%M %p").strip} #{timezone_display}" diff --git a/ruby/lib/jam_ruby/models/rsvp_request.rb b/ruby/lib/jam_ruby/models/rsvp_request.rb index f416640bf..20c3b9e9a 100644 --- a/ruby/lib/jam_ruby/models/rsvp_request.rb +++ b/ruby/lib/jam_ruby/models/rsvp_request.rb @@ -8,6 +8,7 @@ module JamRuby validates :user, presence: true validates :canceled, :inclusion => {:in => [nil, true, false]} validate :creator_rsvp_cancel + before_save :cancel_calendar # pulls all instruments from the associated rsvp_slots def instrument_list @@ -305,6 +306,15 @@ module JamRuby errors.add(:canceled, "can't be canceled by the session organizer") end end + + def cancel_calendar + calendar_manager = CalendarManager.new + if self.canceled + self.rsvp_slots.each do |slot| + calendar_manager.cancel_ics_event(slot.music_session, user) + end + end + end end end diff --git a/ruby/lib/jam_ruby/models/user.rb b/ruby/lib/jam_ruby/models/user.rb index ecd0a3501..0500a896b 100644 --- a/ruby/lib/jam_ruby/models/user.rb +++ b/ruby/lib/jam_ruby/models/user.rb @@ -45,6 +45,9 @@ module JamRuby # authorizations (for facebook, etc -- omniauth) has_many :user_authorizations, :class_name => "JamRuby::UserAuthorization" + # calendars (for scheduling NOT in music_session) + has_many :calendars, :class_name => "JamRuby::Calendar" + # connections (websocket-gateway) has_many :connections, :class_name => "JamRuby::Connection" @@ -698,6 +701,20 @@ module JamRuby end end + # Build calendars using given parameter. + # @param calendars (array of hash) + def update_calendars(calendars) + unless self.new_record? + Calendar.where("user_id = ?", self.id).delete_all + end + + unless calendars.nil? + calendars.each do |cal| + self.calendars << self.calendars.create(cal) + end + end + end + # given an array of instruments, update a user's instruments def update_instruments(instruments) # delete all instruments for this user first diff --git a/ruby/lib/jam_ruby/resque/scheduled/daily_job.rb b/ruby/lib/jam_ruby/resque/scheduled/daily_job.rb new file mode 100644 index 000000000..388516441 --- /dev/null +++ b/ruby/lib/jam_ruby/resque/scheduled/daily_job.rb @@ -0,0 +1,17 @@ +module JamRuby + class DailyJob + extend Resque::Plugins::JamLonelyJob + + @queue = :scheduled_daily_job + @@log = Logging.logger[DailyJob] + + class << self + def perform + @@log.debug("waking up") + calendar_manager = CalendarManager.new + calendar_manager.cleanup() + @@log.debug("done") + end + end + end +end diff --git a/ruby/spec/jam_ruby/calendar_manager_spec.rb b/ruby/spec/jam_ruby/calendar_manager_spec.rb new file mode 100644 index 000000000..1532fca64 --- /dev/null +++ b/ruby/spec/jam_ruby/calendar_manager_spec.rb @@ -0,0 +1,85 @@ +require 'spec_helper' +require 'icalendar' + +describe CalendarManager do + CALENDAR_NAME="Test Cal" + + before :all do + @genre1 = FactoryGirl.create(:genre) + @calendar_manager = JamRuby::CalendarManager.new + + # Time resolution is seconds: + @start = Time.at(Time.now.utc.to_i) + @stop =(@start+1.hours) + end + + before(:each) do + + end + + describe "with music sessions" do + before :all do + @creator = FactoryGirl.create(:user) + @music_session = FactoryGirl.create(:music_session, :creator => @creator, :description => CALENDAR_NAME, :genre => @genre1, :scheduled_start=>@start, :scheduled_duration=>3600) + @music_session.reload + end + + it "validator detects bad calendar" do + lambda{verify_ical("Bad medicine calendar")} + .should raise_error(RuntimeError) + end + + it "can create calendar feed" do + ics = @calendar_manager.create_ics_feed(@creator) + # Basic format checking...there are some online tools that + # check a lot more, but no ruby libs that I could find: + lines = ics.split("\r\n") + lines.should have(12).items + lines.first.should eq("BEGIN:VCALENDAR") + lines.last.should eq("END:VCALENDAR") + lines[-2].should eq("END:VEVENT") + verify_ical(ics) + end + end + + describe "with manual calendars" do + before :all do + @creator = FactoryGirl.create(:user) + @creator.calendars<CALENDAR_NAME, :description=>"This is a test", :start_at=>(@start), :end_at=>@stop, :trigger_delete=>false, :target_uid=>"2112"}) + end + + it "can create calendar feed" do + #pending "foobar" + ics = @calendar_manager.create_ics_feed(@creator) + + # Basic format checking...there are some online tools that + # check a lot more, but no ruby libs that I could find: + lines = ics.split("\r\n") + lines.should have(12).items + lines.first.should eq("BEGIN:VCALENDAR") + lines.last.should eq("END:VCALENDAR") + lines[-2].should eq("END:VEVENT") + verify_ical(ics) + end + end + + def verify_ical(ics) + strict_parser = Icalendar::Parser.new(ics, true) + cals = strict_parser.parse + cals.should_not be_nil + cals.should have(1).items + + cal = cals.first + cal.should_not be_nil + cal.events.should have(1).items + event = cal.events.first + event.should_not be_nil + + event.summary.should eq("JamKazam Session #{CALENDAR_NAME}") + event.dtstart.to_i.should_not be_nil + event.dtend.to_i.should_not be_nil + (event.dtstart).to_time.utc.to_i.should eq(@start.to_i) + (event.dtend).to_time.utc.to_i.should eq(@stop.to_i) + end +end + diff --git a/ruby/spec/jam_ruby/models/rsvp_request_spec.rb b/ruby/spec/jam_ruby/models/rsvp_request_spec.rb index f93501647..9da353746 100644 --- a/ruby/spec/jam_ruby/models/rsvp_request_spec.rb +++ b/ruby/spec/jam_ruby/models/rsvp_request_spec.rb @@ -30,10 +30,10 @@ describe RsvpRequest do @slot1 = FactoryGirl.build(:rsvp_slot, :music_session => @music_session, :instrument => JamRuby::Instrument.find('electric guitar')) @slot1.save - + @slot2 = FactoryGirl.build(:rsvp_slot, :music_session => @music_session, :instrument => JamRuby::Instrument.find('drums')) @slot2.save - + @invitation = FactoryGirl.build(:invitation, :sender => @session_creator, :receiver => @session_invitee, :music_session => @music_session) @invitation.save end @@ -53,12 +53,12 @@ describe RsvpRequest do @music_session.save RsvpRequest.create({:session_id => @music_session.id, :rsvp_slots => [@slot1.id, @slot2.id]}, @non_session_invitee) - expect {RsvpRequest.create({:session_id => @music_session.id, :rsvp_slots => [@slot1.id, @slot2.id]}, @non_session_invitee)}.to raise_error(JamRuby::StateError) + expect {RsvpRequest.create({:session_id => @music_session.id, :rsvp_slots => [@slot1.id, @slot2.id]}, @non_session_invitee)}.to raise_error(JamRuby::StateError) end it "should allow invitee to RSVP to session with closed RSVPs" do rsvp = RsvpRequest.create({:session_id => @music_session.id, :rsvp_slots => [@slot1.id, @slot2.id], :message => "We be jammin!"}, @session_invitee) - + # verify comment comment = SessionInfoComment.find_by_creator_id(@session_invitee) comment.comment.should == "We be jammin!" @@ -373,12 +373,14 @@ describe RsvpRequest do comment = SessionInfoComment.find_by_creator_id(@session_invitee) comment.comment.should == "Let's Jam!" - # cancel - expect {RsvpRequest.cancel({:id => rsvp.id, :session_id => @music_session.id, :cancelled => "all", :message => "Sorry, I'm bailing for all sessions"}, @session_invitee)}.to_not raise_error + calendar_count = Calendar.find(:all).count + # cancel & check that calendar has been added: + expect {RsvpRequest.cancel({:id => rsvp.id, :session_id => @music_session.id, :cancelled => "all", :message => "Sorry, I'm bailing for all sessions"}, @session_invitee)}.to_not raise_error rsvp = RsvpRequest.find_by_id(rsvp.id) rsvp.canceled.should == true rsvp.cancel_all.should == true + (Calendar.find(:all).count - calendar_count).should eq(1) # verify comment comment = SessionInfoComment.find_by_creator_id(@session_invitee) diff --git a/ruby/spec/jam_ruby/resque/scheduled_daily_job_spec.rb b/ruby/spec/jam_ruby/resque/scheduled_daily_job_spec.rb new file mode 100644 index 000000000..b53fdbca5 --- /dev/null +++ b/ruby/spec/jam_ruby/resque/scheduled_daily_job_spec.rb @@ -0,0 +1,43 @@ +require 'spec_helper' + +describe 'DailyJob' do + describe "calendar cleanup" do + shared_examples_for :calendar_cleanup do |trigger_delete, end_count| + before :each do + Calendar.destroy_all + @creator = FactoryGirl.create(:user) + @creator.calendars << Calendar.new( + :name=>"Test Cal", + :description=>"This is a test", + :start_at=>(Time.now), + :end_at=>Time.now, + :trigger_delete=>trigger_delete, + :target_uid=>"2112" + ) + end + + it "properly purges old 'delete' calendars" do + @creator.reload + @creator.calendars.should have(1).items + + JamRuby::DailyJob.perform + @creator.reload + @creator.calendars.should have(1).items + + Timecop.travel(Time.now + 5.weeks) + JamRuby::DailyJob.perform + @creator.reload + @creator.calendars.should have(end_count).items + Timecop.return + end + end + + describe "whacks old 'delete' calendars" do + it_behaves_like :calendar_cleanup, true, 0 + end + + describe "doesn't whacks non 'delete' calendars" do + it_behaves_like :calendar_cleanup, false, 1 + end + end # calendar cleanpu +end #spec diff --git a/web/app/assets/javascripts/dialog/rsvpSubmitDialog.js b/web/app/assets/javascripts/dialog/rsvpSubmitDialog.js index 9b29fcb28..d81b1de38 100644 --- a/web/app/assets/javascripts/dialog/rsvpSubmitDialog.js +++ b/web/app/assets/javascripts/dialog/rsvpSubmitDialog.js @@ -10,6 +10,7 @@ var dialogId = 'rsvp-submit-dialog'; var $btnSubmit = $("#btnSubmitRsvp"); + function beforeShow(data) { $('.error', $dialog).hide(); } @@ -56,7 +57,6 @@ $btnSubmit.unbind('click'); $btnSubmit.click(function(e) { e.preventDefault(); - var error = false; var slotIds = []; var selectedSlots = []; @@ -96,7 +96,11 @@ if (!error) { $dialog.triggerHandler(EVENTS.RSVP_SUBMITTED); - app.layout.closeDialog(dialogId); + + // Show confirmation & calendar; hide regular buttons. + $(".rsvp-options").addClass("hidden") + $(".rsvp-confirm").removeClass("hidden") + $(".buttons").addClass("hidden") } }) .fail(function(xhr, textStatus, errorMessage) { diff --git a/web/app/assets/stylesheets/client/account.css.scss b/web/app/assets/stylesheets/client/account.css.scss index fae2ccd46..606bd73f8 100644 --- a/web/app/assets/stylesheets/client/account.css.scss +++ b/web/app/assets/stylesheets/client/account.css.scss @@ -4,6 +4,16 @@ .session-detail-scroller, #account-identity-content-scroller { + .ics-feed-caption { + font-size: 1.2em; + margin: 0em 0em 1em 0em; + } + + .ics-feed-link { + font-size: 1.1em; + margin: 0.5em 0em 1em 0em; + } + .content-wrapper { padding:10px 30px; } diff --git a/web/app/assets/stylesheets/dialogs/rsvpDialog.css.scss b/web/app/assets/stylesheets/dialogs/rsvpDialog.css.scss index fc5c4879c..ff7c66b83 100644 --- a/web/app/assets/stylesheets/dialogs/rsvpDialog.css.scss +++ b/web/app/assets/stylesheets/dialogs/rsvpDialog.css.scss @@ -3,6 +3,33 @@ .rsvp-dialog { min-height:initial; + height:auto; + + .rsvp-confirm { + color: white; + margin-top: 1em; + .ics-feed-caption { + font-size: 1.2em; + margin: 0em 0em 1em 0em; + } + + .ics-feed-link { + font-size: 1.1em; + margin: 0.5em 0em 1em 0em; + } + + .ics-help-link { + display: inline; + font-size: 0.8em; + padding-right: 2em; + } + + .confirm-buttons { + text-align: center; + margin: 1em 0em 0em 0em; + } + } + .session-name { margin:3px 0 0; diff --git a/web/app/controllers/api_users_controller.rb b/web/app/controllers/api_users_controller.rb index ebe68290a..5c8f27377 100644 --- a/web/app/controllers/api_users_controller.rb +++ b/web/app/controllers/api_users_controller.rb @@ -1,7 +1,7 @@ require 'sanitize' class ApiUsersController < ApiController - before_filter :api_signed_in_user, :except => [:create, :show, :signup_confirm, :auth_session_create, :complete, :finalize_update_email, :isp_scoring, :add_play, :crash_dump, :validate_data] + before_filter :api_signed_in_user, :except => [:create, :calendar, :show, :signup_confirm, :auth_session_create, :complete, :finalize_update_email, :isp_scoring, :add_play, :crash_dump, :validate_data] before_filter :auth_user, :only => [:session_settings_show, :session_history_index, :session_user_history_index, :update, :delete, :liking_create, :liking_destroy, # likes :following_create, :following_show, :following_destroy, # followings @@ -15,19 +15,22 @@ class ApiUsersController < ApiController :share_session, :share_recording, :affiliate_report, :audio_latency, :broadcast_notification] - respond_to :json + respond_to :json, :except => :calendar + respond_to :ics, :only => :calendar def index @users = User.paginate(page: params[:page]) respond_with @users, responder: ApiResponder, :status => 200 end + def calendar + @user=lookup_user + ics = CalendarManager.new.create_ics_feed(@user) + send_data ics, :filename => 'JamKazam', :disposition => 'inline', :type => "text/calendar" + end + def show - @user = User.includes([{musician_instruments: :instrument}, - {band_musicians: :user}, - {genre_players: :genre}, - :bands, :instruments, :genres, :jam_track_rights, :affiliate_partner]) - .find(params[:id]) + @user=lookup_user respond_with @user, responder: ApiResponder, :status => 200 end @@ -80,10 +83,10 @@ class ApiUsersController < ApiController respond_with_model(@user, new: true, location: lambda { return api_user_detail_url(@user.id) }) end end - + def profile_save end - + def update @user = User.find(params[:id]) @@ -96,7 +99,7 @@ class ApiUsersController < ApiController @user.country = params[:country] if params.has_key?(:country) @user.musician = params[:musician] if params.has_key?(:musician) @user.update_instruments(params[:instruments].nil? ? [] : params[:instruments]) if params.has_key?(:instruments) - + # genres @user.update_genres(params[:genres].nil? ? [] : params[:genres], GenrePlayer::PROFILE) if params.has_key?(:genres) @user.update_genres(params[:virtual_band_genres].nil? ? [] : params[:virtual_band_genres], GenrePlayer::VIRTUAL_BAND) if params.has_key?(:virtual_band_genres) @@ -104,7 +107,7 @@ class ApiUsersController < ApiController @user.update_genres(params[:paid_session_genres].nil? ? [] : params[:paid_session_genres], GenrePlayer::PAID_SESSION) if params.has_key?(:paid_session_genres) @user.update_genres(params[:free_session_genres].nil? ? [] : params[:free_session_genres], GenrePlayer::FREE_SESSION) if params.has_key?(:free_session_genres) @user.update_genres(params[:cowriting_genres].nil? ? [] : params[:cowriting_genres], GenrePlayer::COWRITING) if params.has_key?(:cowriting_genres) - + @user.show_whats_next = params[:show_whats_next] if params.has_key?(:show_whats_next) @user.show_whats_next_count = params[:show_whats_next_count] if params.has_key?(:show_whats_next_count) @user.subscribe_email = params[:subscribe_email] if params.has_key?(:subscribe_email) @@ -146,7 +149,7 @@ class ApiUsersController < ApiController @user.update_online_presences(params[:online_presences]) if params.has_key?(:online_presences) @user.update_performance_samples(params[:performance_samples]) if params.has_key?(:performance_samples) - + @user.update_calendars(params[:calendars]) if params.has_key?(:calendars) @user.save if @user.errors.any? @@ -196,9 +199,9 @@ class ApiUsersController < ApiController end def delete - @user.destroy + @user.destroy respond_with responder: ApiResponder, :status => 204 - end + end def signup_confirm @user = UserManager.new.signup_confirm(params[:signup_token]) @@ -260,7 +263,7 @@ class ApiUsersController < ApiController def auth_session_delete sign_out render :json => { :success => true }, :status => 200 - end + end ###################### SESSION SETTINGS ################### def session_settings_show @@ -276,7 +279,7 @@ class ApiUsersController < ApiController @session_user_history = @user.session_user_history(params[:id], params[:session_id]) end - ###################### BANDS ######################## + ###################### BANDS ######################## def band_index @bands = User.band_index(params[:id]) end @@ -296,7 +299,7 @@ class ApiUsersController < ApiController @user = User.find(params[:id]) if !params[:user_id].nil? @user.create_user_liking(params[:user_id]) - + elsif !params[:band_id].nil? @user.create_band_liking(params[:band_id]) end @@ -454,7 +457,7 @@ class ApiUsersController < ApiController respond_with @invitation, responder: ApiResponder, :status => 200 rescue ActiveRecord::RecordNotFound - render :json => { :message => ValidationMessages::BAND_INVITATION_NOT_FOUND }, :status => 404 + render :json => { :message => ValidationMessages::BAND_INVITATION_NOT_FOUND }, :status => 404 end end @@ -467,9 +470,9 @@ class ApiUsersController < ApiController params[:accepted]) respond_with @invitation, responder: ApiResponder, :status => 200 - + rescue ActiveRecord::RecordNotFound - render :json => { :message => ValidationMessages::BAND_INVITATION_NOT_FOUND }, :status => 404 + render :json => { :message => ValidationMessages::BAND_INVITATION_NOT_FOUND }, :status => 404 end end @@ -576,11 +579,11 @@ class ApiUsersController < ApiController # user_id is deduced if possible from the user's cookie. @dump = CrashDump.new - @dump.client_type = params[:client_type] + @dump.client_type = params[:client_type] @dump.client_version = params[:client_version] @dump.client_id = params[:client_id] @dump.user_id = current_user.try(:id) - @dump.session_id = params[:session_id] + @dump.session_id = params[:session_id] @dump.timestamp = params[:timestamp] unless @dump.save @@ -589,7 +592,7 @@ class ApiUsersController < ApiController respond_with @dump return end - + # This part is the piece that really needs to be decomposed into a library... if Rails.application.config.storage_type == :fog s3 = AWS::S3.new(:access_key_id => Rails.application.config.aws_access_key_id, @@ -597,15 +600,15 @@ class ApiUsersController < ApiController bucket = s3.buckets[Rails.application.config.aws_bucket] uri = @dump.uri expire = Time.now + 20.years - read_url = bucket.objects[uri].url_for(:read, - :expires => expire, + read_url = bucket.objects[uri].url_for(:read, + :expires => expire, :'response_content_type' => 'application/octet-stream').to_s @dump.update_attribute(:uri, read_url) - write_url = bucket.objects[uri].url_for(:write, - :expires => Rails.application.config.crash_dump_data_signed_url_timeout, + write_url = bucket.objects[uri].url_for(:write, + :expires => Rails.application.config.crash_dump_data_signed_url_timeout, :'response_content_type' => 'application/octet-stream').to_s - + logger.debug("crash_dump can read from url #{read_url}") redirect_to write_url @@ -744,9 +747,9 @@ class ApiUsersController < ApiController if txt = oo.affiliate_legalese.try(:legalese) txt = ControllerHelp.instance.simple_format(txt) end - result['agreement'] = { - 'legalese' => txt, - 'signed_at' => oo.signed_at + result['agreement'] = { + 'legalese' => txt, + 'signed_at' => oo.signed_at } #result['signups'] = oo.referrals_by_date #result['earnings'] = [['April 2015', '1000 units', '$100']] @@ -851,7 +854,7 @@ class ApiUsersController < ApiController else render json: { message: 'Valid Site', data: data }, status: 200 end - else + else render json: { message: "unknown validation for data '#{params[:data]}', site '#{params[:site]}'" }, status: :unprocessable_entity end end @@ -880,6 +883,14 @@ class ApiUsersController < ApiController render json: { }, status: 200 end + def lookup_user + User.includes([{musician_instruments: :instrument}, + {band_musicians: :user}, + {genre_players: :genre}, + :bands, :instruments, :genres, :jam_track_rights, :affiliate_partner]) + .find(params[:id]) + end + ###################### RECORDINGS ####################### # def recording_index # @recordings = User.recording_index(current_user, params[:id]) @@ -932,5 +943,5 @@ class ApiUsersController < ApiController # @recording = Recording.find(params[:recording_id]) # @recording.delete # respond_with responder: ApiResponder, :status => 204 - # end + # end end diff --git a/web/app/views/clients/_account_sessions.html.haml b/web/app/views/clients/_account_sessions.html.haml index 8e0824c28..6489fb7e1 100644 --- a/web/app/views/clients/_account_sessions.html.haml +++ b/web/app/views/clients/_account_sessions.html.haml @@ -22,6 +22,10 @@ %thead %tbody .clearall + .content-wrapper + .ics-feed-caption Following is a URL for your personal JamKazam .ics calendar, which tracks all sessions and events to which you have RSVP'd: + =render "calendar" + / end content scrolling area %script{type: 'text/template', id: 'template-account-session'} diff --git a/web/app/views/clients/_calendar.html.slim b/web/app/views/clients/_calendar.html.slim new file mode 100644 index 000000000..85c808d2f --- /dev/null +++ b/web/app/views/clients/_calendar.html.slim @@ -0,0 +1,9 @@ +-if current_user + .account-calendar + .ics-feed-link + =api_users_calendar_feed_url(current_user) + .ics-help-links + .ics-help-link + a href="" How to subscribe to your calendar in Google Calendar + .ics-help-link + a href="" How to subscribe to your calendar in Microsoft Outlook diff --git a/web/app/views/dialogs/_rsvpSubmitDialog.html.haml b/web/app/views/dialogs/_rsvpSubmitDialog.html.haml index 1135fde57..f7eb20bbf 100644 --- a/web/app/views/dialogs/_rsvpSubmitDialog.html.haml +++ b/web/app/views/dialogs/_rsvpSubmitDialog.html.haml @@ -7,16 +7,27 @@ .session-name .scheduled-start .schedule-recurrence - .part - .slot-instructions Check the box(es) next to the track(s) you want to play in the session: - .error{:style => 'display:none'} - .rsvp-instruments + .rsvp-options + .part + .slot-instructions Check the box(es) next to the track(s) you want to play in the session: + .error{:style => 'display:none'} + .rsvp-instruments + + .comment-instructions Enter a message to the other musicians in the session (optional): + %textarea.txtComment{rows: '2', placeholder: 'Enter a comment...'} + .rsvp-confirm.hidden + %p SUCCESS! + %br + %p We recommend that you subscribe to your own personal JamKazam calendar in your favorite calendar app to help you remember this session, as well as other sessions and events to which you RSVP. + %br + %p Here is the URL for your calendar: + =render "clients/calendar" + .confirm-buttons + %a#btnClose.button-grey{'layout-action' => 'close'} CLOSE - .comment-instructions Enter a message to the other musicians in the session (optional): - %textarea.txtComment{rows: '2', placeholder: 'Enter a comment...'} .buttons .left - %a.button-grey{:href => 'http://jamkazam.desk.com', :rel => 'external', :target => '_blank'} HELP + %a#btnHelp.button-grey{:href => 'http://jamkazam.desk.com', :rel => 'external', :target => '_blank'} HELP .right - %a.button-grey{:id => 'btnCancel', 'layout-action' => 'close'} CANCEL - %a.button-orange{:id => 'btnSubmitRsvp'} SUBMIT RSVP \ No newline at end of file + %a#btnCancel.button-grey{'layout-action' => 'close'} CANCEL + %a#btnSubmitRsvp.button-orange SUBMIT RSVP \ No newline at end of file diff --git a/web/config/routes.rb b/web/config/routes.rb index 43ee05098..924abf233 100644 --- a/web/config/routes.rb +++ b/web/config/routes.rb @@ -271,9 +271,9 @@ SampleApp::Application.routes.draw do #match '/users' => 'api_users#create', :via => :post match '/users/:id' => 'api_users#update', :via => :post match '/users/:id' => 'api_users#delete', :via => :delete + match '/users/:id/calendar.ics' => 'api_users#calendar', :via => :get, :as => 'api_users_calendar_feed' match '/users/confirm/:signup_token' => 'api_users#signup_confirm', :via => :post, :as => 'api_signup_confirmation' match '/users/complete/:signup_token' => 'api_users#complete', as: 'complete', via: 'post' - match '/users/:id/set_password' => 'api_users#set_password', :via => :post # recurly diff --git a/web/config/scheduler.yml b/web/config/scheduler.yml index 1890a41d4..40aa901fc 100644 --- a/web/config/scheduler.yml +++ b/web/config/scheduler.yml @@ -40,6 +40,11 @@ DailySessionEmailer: class: "JamRuby::DailySessionEmailer" description: "Sends daily scheduled session emails" +DailyJob: + cron: "0 4 * * *" + class: "JamRuby::DailyJob" + description: "Aggregate task to perform general daily things" + ScheduledMusicSessionCleaner: cron: "0 3 * * *" class: "JamRuby::ScheduledMusicSessionCleaner" diff --git a/web/spec/controllers/api_users_controller_spec.rb b/web/spec/controllers/api_users_controller_spec.rb index a95ffebf7..065602e77 100644 --- a/web/spec/controllers/api_users_controller_spec.rb +++ b/web/spec/controllers/api_users_controller_spec.rb @@ -59,6 +59,27 @@ describe ApiUsersController do end end + describe "calendars" do + before :each do + Calendar.destroy_all + end + + it "adds calendar via update" do + cals = [{ + :name=>"Test Cal", + :description=>"This is a test", + :start_at=>(Time.now), + :end_at=>Time.now, + :trigger_delete=>true, + :target_uid=>"2112" + }] + post :update, id:user.id, calendars: cals, :format=>'json' + response.should be_success + user.reload + user.calendars.should have(1).items + end + end + describe "update mod" do it "empty mod" do post :update, id:user.id, mods: {}, :format=>'json' @@ -83,13 +104,13 @@ describe ApiUsersController do end end - describe 'site validation' do + describe 'site validation' do - it 'checks valid and invalid site types' do + it 'checks valid and invalid site types' do site_types = Utils::SITE_TYPES.clone << 'bandcamp-fan' site_types.each do |sitetype| rec_id = nil - case sitetype + case sitetype when 'url' valid, invalid = 'http://jamkazam.com', 'http://jamkazamxxx.com' when 'youtube' From 2761ea8ed7b203c9ba74350082003ad2f26a5c87 Mon Sep 17 00:00:00 2001 From: Steven Miers Date: Fri, 10 Jul 2015 10:51:12 -0500 Subject: [PATCH 04/89] Merged session reminder feature branch. Squashed commit of the following: commit 84293ed637bf8407fde2615553efbafeb9195cc0 Author: Steven Miers Date: Thu Jul 9 17:52:26 2015 -0500 VRFS-3300 : Reminder notification functionality -- sends in-app/1-day/upcoming emails as necessary. commit 521b0f4ba7467e03cf57a14f0b969d8c85f8a86e Author: Steven Miers Date: Thu Jul 9 17:50:33 2015 -0500 VRFS-3300 : Reminder mails on user mailer. Also add tests to spec. commit ed487e11a3bfec8e6b9576f4d1c766804bbc62dc Author: Steven Miers Date: Thu Jul 9 17:48:04 2015 -0500 VRFS-3300 : Music session reminder job and spec commit e5c7f50cd24efa6bcbb2e6140ec6dfc43844c213 Author: Steven Miers Date: Thu Jul 9 17:46:55 2015 -0500 VRFS-3300 : Reminder cron setting --- ruby/lib/jam_ruby.rb | 1 + ruby/lib/jam_ruby/app/mailers/user_mailer.rb | 30 +++-- .../scheduled_session_reminder.html.erb | 10 -- .../scheduled_session_reminder.text.erb | 6 - .../scheduled_session_reminder_day.html.erb | 18 +++ .../scheduled_session_reminder_day.text.erb | 8 ++ ...heduled_session_reminder_upcoming.html.erb | 17 +++ ...heduled_session_reminder_upcoming.text.erb | 10 ++ .../jam_ruby/constants/notification_types.rb | 4 +- ruby/lib/jam_ruby/models/notification.rb | 112 ++++++++++++------ .../scheduled/music_session_reminder.rb | 31 +++++ .../spec/jam_ruby/models/notification_spec.rb | 101 +++++++++++----- .../resque/music_session_reminder_spec.rb | 79 ++++++++++++ ruby/spec/mailers/user_mailer_spec.rb | 44 ++++++- web/config/scheduler.yml | 5 + 15 files changed, 383 insertions(+), 93 deletions(-) delete mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder.html.erb delete mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder.text.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_day.html.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_day.text.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_upcoming.html.erb create mode 100644 ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_upcoming.text.erb create mode 100644 ruby/lib/jam_ruby/resque/scheduled/music_session_reminder.rb create mode 100644 ruby/spec/jam_ruby/resque/music_session_reminder_spec.rb diff --git a/ruby/lib/jam_ruby.rb b/ruby/lib/jam_ruby.rb index 969f026ef..82484997e 100755 --- a/ruby/lib/jam_ruby.rb +++ b/ruby/lib/jam_ruby.rb @@ -54,6 +54,7 @@ require "jam_ruby/resque/scheduled/user_progress_emailer" require "jam_ruby/resque/scheduled/daily_job" require "jam_ruby/resque/scheduled/daily_session_emailer" require "jam_ruby/resque/scheduled/new_musician_emailer" +require "jam_ruby/resque/scheduled/music_session_reminder" require "jam_ruby/resque/scheduled/music_session_scheduler" require "jam_ruby/resque/scheduled/active_music_session_cleaner" require "jam_ruby/resque/scheduled/score_history_sweeper" diff --git a/ruby/lib/jam_ruby/app/mailers/user_mailer.rb b/ruby/lib/jam_ruby/app/mailers/user_mailer.rb index 46275db4a..40d5480f0 100644 --- a/ruby/lib/jam_ruby/app/mailers/user_mailer.rb +++ b/ruby/lib/jam_ruby/app/mailers/user_mailer.rb @@ -182,7 +182,7 @@ email = user.email subject = "Your band has a new follower on JamKazam" unique_args = {:type => "new_band_follower"} - + @body = msg sendgrid_category "Notification" sendgrid_unique_args :type => unique_args[:type] @@ -390,13 +390,23 @@ end end - def scheduled_session_reminder(user, msg, session) + def scheduled_session_reminder_upcoming(user, session) + subject = "Your JamKazam session starts in 1 hour!" + unique_args = {:type => "scheduled_session_reminder_upcoming"} + send_scheduled_session_reminder(user, session, subject, unique_args) + end + + def scheduled_session_reminder_day(user, session) + subject = "JamKazam Session Reminder" + unique_args = {:type => "scheduled_session_reminder_day"} + send_scheduled_session_reminder(user, session, subject, unique_args) + end + + def send_scheduled_session_reminder(user, session, subject, unique_args) return if !user.subscribe_email email = user.email - subject = "Session Rescheduled" - unique_args = {:type => "scheduled_session_reminder"} - @body = msg + @user = user @session_name = session.name @session_date = session.pretty_scheduled_start(true) @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session.id}/details" @@ -448,7 +458,7 @@ @sessions_and_latency = sessions_and_latency @title = 'New Scheduled Sessions Matched to You' - mail(:to => receiver.email, + mail(:to => receiver.email, :subject => EmailBatchScheduledSessions.subject) do |format| format.text format.html @@ -461,7 +471,7 @@ email = user.email subject = "A band that you follow has joined a session" unique_args = {:type => "band_session_join"} - + @body = msg @session_url = "#{APP_CONFIG.external_root_url}/sessions/#{session_id}" sendgrid_category "Notification" @@ -482,7 +492,7 @@ email = user.email subject = "A musician has saved a new recording on JamKazam" unique_args = {:type => "musician_recording_saved"} - + @body = msg sendgrid_category "Notification" sendgrid_unique_args :type => unique_args[:type] @@ -502,7 +512,7 @@ email = user.email subject = "A band has saved a new recording on JamKazam" unique_args = {:type => "band_recording_saved"} - + @body = msg sendgrid_category "Notification" sendgrid_unique_args :type => unique_args[:type] @@ -522,7 +532,7 @@ email = user.email subject = "You have been invited to join a band on JamKazam" unique_args = {:type => "band_invitation"} - + @body = msg sendgrid_category "Notification" sendgrid_unique_args :type => unique_args[:type] diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder.html.erb deleted file mode 100644 index 8582bfdbe..000000000 --- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder.html.erb +++ /dev/null @@ -1,10 +0,0 @@ -<% provide(:title, 'Scheduled Session Reminder') %> - -

<%= @body %>

- -

- <%= @session_name %>
- <%= @session_date %> -

- -

View Session Details

\ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder.text.erb deleted file mode 100644 index 40ec73f65..000000000 --- a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder.text.erb +++ /dev/null @@ -1,6 +0,0 @@ -<%= @body %> - -<%= @session_name %> -<%= @session_date %> - -See session details at <%= @session_url %>. \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_day.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_day.html.erb new file mode 100644 index 000000000..b72d3c133 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_day.html.erb @@ -0,0 +1,18 @@ +<% provide(:title, 'JamKazam Session Reminder') %> + + +
+Hi <%= @user.first_name %>, +
+
+
+ This is a reminder that your JamKazam session + <%= @session_name %> + is scheduled for tomorrow. We hope you have fun! +
+
+
+Best Regards, +
+Team JamKazam +
\ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_day.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_day.text.erb new file mode 100644 index 000000000..c3f0576bf --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_day.text.erb @@ -0,0 +1,8 @@ +Hi <%= @user.first_name %>, + +This is a reminder that your JamKazam session <%=@session_name%> is scheduled for tomorrow. We hope you have fun! + +Best Regards, +Team JamKazam + +See session details at <%= @session_url %>. \ No newline at end of file diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_upcoming.html.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_upcoming.html.erb new file mode 100644 index 000000000..4fbc59ace --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_upcoming.html.erb @@ -0,0 +1,17 @@ +<% provide(:title, 'Your JamKazam session starts in 1 hour!') %> + +
+Hi <%= @user.first_name %>, +
+
+
+ This is a reminder that your JamKazam session + <%= @session_name %> + starts in 1 hour. We hope you have fun! +
+
+
+Best Regards, +
+Team JamKazam +
diff --git a/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_upcoming.text.erb b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_upcoming.text.erb new file mode 100644 index 000000000..70726a9e6 --- /dev/null +++ b/ruby/lib/jam_ruby/app/views/jam_ruby/user_mailer/scheduled_session_reminder_upcoming.text.erb @@ -0,0 +1,10 @@ +Hi <%= @user.first_name %>, + +This is a reminder that your JamKazam session +<%=@session_name%> +starts in 1 hour. We hope you have fun! + +Best Regards, +Team JamKazam + +See session details at <%= @session_url %>. \ No newline at end of file diff --git a/ruby/lib/jam_ruby/constants/notification_types.rb b/ruby/lib/jam_ruby/constants/notification_types.rb index 7460a2fbb..e05c8e00e 100644 --- a/ruby/lib/jam_ruby/constants/notification_types.rb +++ b/ruby/lib/jam_ruby/constants/notification_types.rb @@ -26,7 +26,9 @@ module NotificationTypes SCHEDULED_SESSION_RSVP_CANCELLED_ORG = "SCHEDULED_SESSION_RSVP_CANCELLED_ORG" SCHEDULED_SESSION_CANCELLED = "SCHEDULED_SESSION_CANCELLED" SCHEDULED_SESSION_RESCHEDULED = "SCHEDULED_SESSION_RESCHEDULED" - SCHEDULED_SESSION_REMINDER = "SCHEDULED_SESSION_REMINDER" + SCHEDULED_SESSION_REMINDER_DAY = "SCHEDULED_SESSION_REMINDER_DAY" + SCHEDULED_SESSION_REMINDER_UPCOMING = "SCHEDULED_SESSION_REMINDER_UPCOMING" + SCHEDULED_SESSION_REMINDER_IMMINENT = "SCHEDULED_SESSION_REMINDER_IMMINENT" SCHEDULED_SESSION_COMMENT = "SCHEDULED_SESSION_COMMENT" # recording notifications diff --git a/ruby/lib/jam_ruby/models/notification.rb b/ruby/lib/jam_ruby/models/notification.rb index db4018ab8..5b008a55f 100644 --- a/ruby/lib/jam_ruby/models/notification.rb +++ b/ruby/lib/jam_ruby/models/notification.rb @@ -73,6 +73,10 @@ module JamRuby @@message_factory = MessageFactory.new ################### HELPERS ################### + def notified?(music_session, notification_type) + Notification.where("session_id=? AND description=?", music_session, notification_type).count != 0 + end + def retrieve_friends(connection, user_id) friend_ids = [] connection.exec("SELECT f.friend_id as friend_id FROM friendships f WHERE f.user_id = $1", [user_id]) do |friend_results| @@ -203,9 +207,15 @@ module JamRuby when NotificationTypes::SCHEDULED_SESSION_RESCHEDULED return "The following session has been rescheduled." - when NotificationTypes::SCHEDULED_SESSION_REMINDER + when NotificationTypes::SCHEDULED_SESSION_REMINDER_DAY return "A session to which you have RSVPd will begin in one hour, so get ready to play!" + when NotificationTypes::SCHEDULED_SESSION_REMINDER_UPCOMING + return "A session to which you have RSVPd will begin in one hour, so get ready to play!" + + when NotificationTypes::SCHEDULED_SESSION_REMINDER_IMMINENT + return "A session to which you have RSVPd is scheduled to start in 5 minutes!" + when NotificationTypes::SCHEDULED_SESSION_COMMENT return "New message about session." @@ -515,7 +525,7 @@ module JamRuby end def send_session_join(active_music_session, connection, user) - + notification_msg = format_msg(NotificationTypes::SESSION_JOIN, {:user => user}) msg = @@message_factory.session_join( @@ -553,8 +563,8 @@ module JamRuby end def send_musician_session_join(music_session, user) - - if music_session.musician_access || music_session.fan_access + + if music_session.musician_access || music_session.fan_access friends = Friendship.where(:friend_id => user.id) user_followers = user.followers @@ -804,7 +814,7 @@ module JamRuby def send_scheduled_session_cancelled(music_session) return if music_session.nil? - + rsvp_requests = RsvpRequest.index(music_session) target_users = rsvp_requests.where(:canceled => false).map { |r| r.user } @@ -890,33 +900,52 @@ module JamRuby end end - def send_scheduled_session_reminder(music_session) + # Send session reminders to sessions that + # start in less than 24 hours, and haven't been + # notified for a particular interval yet: + def send_session_reminders + MusicSession.where("scheduled_start > NOW() AND scheduled_start <= (NOW()+INTERVAL '1 DAYS')").each do |candidate_session| + tm = candidate_session.scheduled_start + if (tm>(12.hours.from_now) && !notified?(candidate_session, NotificationTypes::SCHEDULED_SESSION_REMINDER_DAY)) + # Send 24 hour reminders: + send_session_reminder_day(candidate_session) + elsif (tm<=(65.minutes.from_now) && tm>(15.minutes.from_now) && !notified?(candidate_session, NotificationTypes::SCHEDULED_SESSION_REMINDER_UPCOMING)) + # Send 1 hour reminders: + send_session_reminder_upcoming(candidate_session) + elsif (tm<=(10.minutes.from_now) && !notified?(candidate_session, NotificationTypes::SCHEDULED_SESSION_REMINDER_IMMINENT)) + # Send 5 minute reminders: + send_session_reminder_imminent(candidate_session) + end + end + end - return if music_session.nil? + def send_session_reminder_day(music_session) + send_session_reminder(music_session, NotificationTypes::SCHEDULED_SESSION_REMINDER_DAY) do |music_session, target_user, notification| + begin + UserMailer.scheduled_session_reminder_day(target_user, music_session).deliver + rescue => e + @@log.error("Unable to send SCHEDULED_SESSION_REMINDER_DAY email to user #{target_user.email} #{e}") + end + end + end - rsvp_requests = RsvpRequest.index(music_session) - target_users = rsvp_requests.where(:canceled => false).map { |r| r.user } - - # remove the creator from the array - target_users = target_users.uniq - [music_session.creator] - - target_users.each do |target_user| - source_user = music_session.creator - - notification = Notification.new - notification.description = NotificationTypes::SCHEDULED_SESSION_REMINDER - notification.source_user_id = source_user.id - notification.target_user_id = target_user.id - notification.session_id = music_session.id - notification.save - - notification_msg = format_msg(notification.description, {:session => music_session}) + def send_session_reminder_upcoming(music_session) + send_session_reminder(music_session, NotificationTypes::SCHEDULED_SESSION_REMINDER_UPCOMING) do |music_session, target_user, notification| + begin + UserMailer.scheduled_session_reminder_upcoming(target_user, music_session).deliver + rescue => e + @@log.error("Unable to send SCHEDULED_SESSION_REMINDER_UPCOMING email to user #{target_user.email} #{e}") + end + end + end + def send_session_reminder_imminent(music_session) + send_session_reminder(music_session, NotificationTypes::SCHEDULED_SESSION_REMINDER_IMMINENT) do |music_session, target_user, notification| if target_user.online msg = @@message_factory.scheduled_session_reminder( target_user.id, music_session.id, - notification_msg, + format_msg(notification.description, {:session => music_session}), music_session.name, music_session.pretty_scheduled_start(false), notification.id, @@ -925,12 +954,27 @@ module JamRuby @@mq_router.publish_to_user(target_user.id, msg) end + end + end - begin - UserMailer.scheduled_session_reminder(target_user, notification_msg, music_session).deliver - rescue => e - @@log.error("Unable to send SCHEDULED_SESSION_REMINDER email to user #{target_user.email} #{e}") - end + # @param music_session - the session for which to send reminder + # @param reminder_type - the type of reminder; one of: + # => SCHEDULED_SESSION_REMINDER_DAY 24 hours + # => SCHEDULED_SESSION_REMINDER_UPCOMING 15 minutes + # => SCHEDULED_SESSION_REMINDER_IMMINENT 5 minutes (in-app) + def send_session_reminder(music_session, reminder_type) + raise ArgumentError, "Block required" unless block_given? + source_user = music_session.creator + rsvp_requests = RsvpRequest.index(music_session) + rsvp_requests.where(:canceled => false).each do |rsvp| + target_user = rsvp.user + notification = Notification.new + notification.description = reminder_type + notification.source_user_id = source_user.id + notification.target_user_id = target_user.id + notification.session_id = music_session.id + notification.save + yield(music_session, target_user, notification) end end @@ -984,12 +1028,12 @@ module JamRuby def send_band_session_join(music_session, band) # if the session is private, don't send any notifications - if music_session.musician_access || music_session.fan_access + if music_session.musician_access || music_session.fan_access notification_msg = format_msg(NotificationTypes::BAND_SESSION_JOIN, {:band => band}) followers = band.followers.map { |bf| bf.user } - + # do not send band session notifications to band members followers = followers - band.users @@ -1328,7 +1372,7 @@ module JamRuby end def send_band_invitation_accepted(band, band_invitation, sender, receiver) - + notification = Notification.new notification.band_id = band.id notification.description = NotificationTypes::BAND_INVITATION_ACCEPTED @@ -1362,7 +1406,7 @@ module JamRuby msg = @@message_factory.musician_session_fresh( music_session.id, - user.id, + user.id, user.name, user.photo_url ) diff --git a/ruby/lib/jam_ruby/resque/scheduled/music_session_reminder.rb b/ruby/lib/jam_ruby/resque/scheduled/music_session_reminder.rb new file mode 100644 index 000000000..503e31e9b --- /dev/null +++ b/ruby/lib/jam_ruby/resque/scheduled/music_session_reminder.rb @@ -0,0 +1,31 @@ +require 'json' +require 'resque' +require 'resque-retry' +require 'net/http' +require 'digest/md5' + +module JamRuby + class MusicSessionReminder + extend Resque::Plugins::JamLonelyJob + + @queue = :music_session_reminder + + @@log = Logging.logger[MusicSessionReminder] + + def self.lock_timeout + 120 + end + + def self.perform + @@log.debug("MusicSessionReminder waking up") + + MusicSessionReminder.new.run + + @@log.debug("MusicSessionReminder done") + end + + def run + Notification.send_session_reminders() + end + end +end \ No newline at end of file diff --git a/ruby/spec/jam_ruby/models/notification_spec.rb b/ruby/spec/jam_ruby/models/notification_spec.rb index 1f201c142..fcfae98a1 100644 --- a/ruby/spec/jam_ruby/models/notification_spec.rb +++ b/ruby/spec/jam_ruby/models/notification_spec.rb @@ -18,6 +18,13 @@ describe Notification do @session = FactoryGirl.create(:music_session) @band = FactoryGirl.create(:band) + @slot1 = FactoryGirl.build(:rsvp_slot, :music_session => @session, :instrument => JamRuby::Instrument.find('electric guitar')) + @slot1.save + + @slot2 = FactoryGirl.build(:rsvp_slot, :music_session => @session, :instrument => JamRuby::Instrument.find('drums')) + @slot2.save + + @friend_request = FactoryGirl.create(:friend_request, user: @sender, friend: @receiver) end @@ -199,7 +206,7 @@ describe Notification do it "does not send email when user is offline and opts out of emails" do FactoryGirl.create(:friendship, :user => @receiver, :friend => @recording.owner) FactoryGirl.create(:friendship, :user => @recording.owner, :friend => @receiver) - + @receiver.subscribe_email = false @receiver.save! @@ -284,7 +291,7 @@ describe Notification do @recording.band = @band @recording.save! - + follower.subscribe_email = false follower.save! @@ -671,35 +678,14 @@ describe Notification do end end - describe "send scheduled session reminder" do - # it "sends email when user is offline and subscribes to emails" do - # session.creator = sender - # session.save! - - # calls = count_publish_to_user_calls - # notification = Notification.send_scheduled_session_cancelled(session) - - # UserMailer.deliveries.length.should == 1 - # calls[:count].should == 1 - # end - - # it "does not send email when user is offline and opts out of emails" do - # session.creator = sender - # session.save! - - # receiver.subscribe_email = false - # receiver.save! - - # calls = count_publish_to_user_calls - # notification = Notification.send_scheduled_session_cancelled(session) - - # UserMailer.deliveries.length.should == 0 - # calls[:count].should == 1 - # end - + describe "reminders" do + let(:mail) { UserMailer.deliveries[0] } + before :each do + UserMailer.deliveries.clear + end it "sends no notification if session is nil" do calls = count_publish_to_user_calls - notification = Notification.send_scheduled_session_reminder(nil) + notification = Notification.send_session_reminders() UserMailer.deliveries.length.should == 0 calls[:count].should == 0 @@ -707,12 +693,65 @@ describe Notification do it "sends no notification if there are no rsvp requests" do calls = count_publish_to_user_calls - notification = Notification.send_scheduled_session_reminder(@session) + notification = Notification.send_session_reminders() UserMailer.deliveries.length.should == 0 calls[:count].should == 0 end - end + + it "sends email 24 hours before" do + @session.creator = @sender + @session.scheduled_start = Time.now + 23.hours + @session.save! + + notification = Notification.send_session_reminders() + + UserMailer.deliveries.length.should == 1 + calls = count_publish_to_user_calls + calls[:count].should == 0 + + mail.html_part.body.include?("is scheduled for tomorrow").should be_true + mail.text_part.body.include?("is scheduled for tomorrow").should be_true + + mail.html_part.body.include?("starts in 1 hour").should be_false + mail.text_part.body.include?("starts in 1 hour").should be_false + end + + it "sends email 1 hour before" do + @session.creator = @sender + @session.scheduled_start = Time.now + 59.minutes + @session.save! + + + notification = Notification.send_session_reminders() + + UserMailer.deliveries.length.should == 1 + calls = count_publish_to_user_calls + calls[:count].should == 0 + mail.html_part.body.include?("is scheduled for tomorrow").should be_false + mail.text_part.body.include?("is scheduled for tomorrow").should be_false + + mail.html_part.body.include?("starts in 1 hour").should be_true + mail.text_part.body.include?("starts in 1 hour").should be_true + + end + + it "sends notice 5 minutes before" do + UserMailer.deliveries.length.should == 0 + receiver_connection = FactoryGirl.create(:connection, user: @receiver) + @receiver.reload + + rsvp = RsvpRequest.create({:session_id => @session.id, :rsvp_slots => [@slot1.id, @slot2.id], :message => "We be jammin!"}, @receiver) + UserMailer.deliveries.clear + calls = count_publish_to_user_calls + @session.creator = @sender + @session.scheduled_start = Time.now + 4.minutes + @session.save! + notification = Notification.send_session_reminders() + calls[:count].should == 1 + UserMailer.deliveries.length.should == 0 + end + end # reminders describe "send scheduled session comment" do # it "sends email when user is offline and subscribes to emails" do diff --git a/ruby/spec/jam_ruby/resque/music_session_reminder_spec.rb b/ruby/spec/jam_ruby/resque/music_session_reminder_spec.rb new file mode 100644 index 000000000..2823d3150 --- /dev/null +++ b/ruby/spec/jam_ruby/resque/music_session_reminder_spec.rb @@ -0,0 +1,79 @@ +require 'spec_helper' + +describe 'MusicSessionReminder' do + let(:mail) { UserMailer.deliveries[0] } + before :each do + UserMailer.deliveries.clear + MusicSession.delete_all + User.delete_all + + @receiver = FactoryGirl.create(:user) + @sender = FactoryGirl.create(:user) + @session = FactoryGirl.create(:music_session) + + @slot1 = FactoryGirl.build(:rsvp_slot, :music_session => @session, :instrument => JamRuby::Instrument.find('electric guitar')) + @slot1.save + end + + it "sends email 24 hours before" do + @session.creator = @sender + @session.scheduled_start = Time.now + 23.hours + @session.save! + + JamRuby::MusicSessionReminder.perform + + UserMailer.deliveries.length.should == 1 + calls = count_publish_to_user_calls + calls[:count].should == 0 + + mail.html_part.body.include?("is scheduled for tomorrow").should be_true + mail.text_part.body.include?("is scheduled for tomorrow").should be_true + + mail.html_part.body.include?("starts in 1 hour").should be_false + mail.text_part.body.include?("starts in 1 hour").should be_false + end + + it "sends email 1 hour before" do + @session.creator = @sender + @session.scheduled_start = Time.now + 59.minutes + @session.save! + + + JamRuby::MusicSessionReminder.perform + + UserMailer.deliveries.length.should == 1 + calls = count_publish_to_user_calls + calls[:count].should == 0 + mail.html_part.body.include?("is scheduled for tomorrow").should be_false + mail.text_part.body.include?("is scheduled for tomorrow").should be_false + + mail.html_part.body.include?("starts in 1 hour").should be_true + mail.text_part.body.include?("starts in 1 hour").should be_true + + end + + it "sends notice 5 minutes before" do + UserMailer.deliveries.length.should == 0 + receiver_connection = FactoryGirl.create(:connection, user: @receiver) + @receiver.reload + + rsvp = RsvpRequest.create({:session_id => @session.id, :rsvp_slots => [@slot1.id, @slot2.id], :message => "We be jammin!"}, @receiver) + UserMailer.deliveries.clear + calls = count_publish_to_user_calls + @session.creator = @sender + @session.scheduled_start = Time.now + 4.minutes + @session.save! + JamRuby::MusicSessionReminder.perform + calls[:count].should == 1 + UserMailer.deliveries.length.should == 0 + end + + def count_publish_to_user_calls + result = {count: 0} + MQRouter.any_instance.stub(:publish_to_user) do |receiver_id, msg| + result[:count] += 1 + result[:msg] = msg + end + result + end +end #spec diff --git a/ruby/spec/mailers/user_mailer_spec.rb b/ruby/spec/mailers/user_mailer_spec.rb index c3d041060..320615020 100644 --- a/ruby/spec/mailers/user_mailer_spec.rb +++ b/ruby/spec/mailers/user_mailer_spec.rb @@ -123,7 +123,7 @@ describe UserMailer do before(:each) do user.update_email = "my_new_email@jamkazam.com" - UserMailer.updating_email(user).deliver + UserMailer.updating_email(user).deliver end it { UserMailer.deliveries.length.should == 1 } @@ -137,6 +137,48 @@ describe UserMailer do it { mail.text_part.body.include?("to confirm your change in email").should be_true } end + describe "notifications" do + + let(:mail) { UserMailer.deliveries[0] } + let(:music_session) { FactoryGirl.create(:music_session) } + + it "should send upcoming email" do + user.update_email = "my_new_email@jamkazam.com" + UserMailer.scheduled_session_reminder_upcoming(music_session.creator, music_session).deliver + UserMailer.deliveries.length.should == 1 + + mail['from'].to_s.should == UserMailer::DEFAULT_SENDER + mail['to'].to_s.should == music_session.creator.email# rsvp_requests.first.user.email + mail.multipart?.should == true # because we send plain + htm + + # verify that the messages are correctly configured + mail.html_part.body.include?("This is a reminder that your JamKazam session").should be_true + mail.text_part.body.include?("This is a reminder that your JamKazam session").should be_true + mail.html_part.body.include?("starts in 1 hour").should be_true + mail.text_part.body.include?("starts in 1 hour").should be_true + + + end + + it "should send 1-day reminder" do + user.update_email = "my_new_email@jamkazam.com" + UserMailer.scheduled_session_reminder_day(music_session.creator, music_session).deliver + UserMailer.deliveries.length.should == 1 + + mail['from'].to_s.should == UserMailer::DEFAULT_SENDER + mail['to'].to_s.should == music_session.creator.email# rsvp_requests.first.user.email + mail.multipart?.should == true # because we send plain + htm + + # verify that the messages are correctly configured + mail.html_part.body.include?("This is a reminder that your JamKazam session").should be_true + mail.text_part.body.include?("This is a reminder that your JamKazam session").should be_true + mail.html_part.body.include?("is scheduled for tomorrow").should be_true + mail.text_part.body.include?("is scheduled for tomorrow").should be_true + + end + end + + # describe "sends new musicians email" do diff --git a/web/config/scheduler.yml b/web/config/scheduler.yml index 40aa901fc..90ecfff55 100644 --- a/web/config/scheduler.yml +++ b/web/config/scheduler.yml @@ -70,6 +70,11 @@ ScoreHistorySweeper: class: "JamRuby::ScoreHistorySweeper" description: "Creates 'ScoreHistory' tables from Scores (disabled for now)" +SessionReminder: + cron: */5 * * * * + class: "JamRuby::MusicSessionReminder" + description: "Creates session reminder emails and notifications as needed." + RecordingsCleaner: cron: 0 * * * * class: "JamRuby::RecordingsCleaner" From a467388ac8ce7228f923df94bdb22755b5cfd354 Mon Sep 17 00:00:00 2001 From: Steven Miers Date: Fri, 10 Jul 2015 11:09:16 -0500 Subject: [PATCH 05/89] Merge test fix --- ruby/spec/jam_ruby/resque/music_session_reminder_spec.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ruby/spec/jam_ruby/resque/music_session_reminder_spec.rb b/ruby/spec/jam_ruby/resque/music_session_reminder_spec.rb index 2823d3150..35c8ae845 100644 --- a/ruby/spec/jam_ruby/resque/music_session_reminder_spec.rb +++ b/ruby/spec/jam_ruby/resque/music_session_reminder_spec.rb @@ -13,6 +13,9 @@ describe 'MusicSessionReminder' do @slot1 = FactoryGirl.build(:rsvp_slot, :music_session => @session, :instrument => JamRuby::Instrument.find('electric guitar')) @slot1.save + + @slot2 = FactoryGirl.build(:rsvp_slot, :music_session => @session, :instrument => JamRuby::Instrument.find('drums')) + @slot2.save end it "sends email 24 hours before" do From f2ce59005a305b3304cabd859860d156e25a3a8d Mon Sep 17 00:00:00 2001 From: Steven Miers Date: Fri, 10 Jul 2015 15:46:49 -0500 Subject: [PATCH 06/89] VRFS-3321 : Disable create account submit button until successful recaptcha callback. Re-disable if captcha expires. --- web/app/views/users/new.html.erb | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/web/app/views/users/new.html.erb b/web/app/views/users/new.html.erb index 01fc14c25..8c2f46163 100644 --- a/web/app/views/users/new.html.erb +++ b/web/app/views/users/new.html.erb @@ -127,7 +127,7 @@
<% if Rails.application.config.recaptcha_enable %> -
+
<% end %>
@@ -138,7 +138,7 @@ -->
- <%= f.submit "CREATE ACCOUNT", class: "button-orange" %> + <%= f.submit "CREATE ACCOUNT", class: "button-orange disabled", id:"create-account-submit"%> <%= link_to "CANCEL", root_path, :class=>'button-grey' %>


@@ -163,6 +163,14 @@ window.signup.handle_register_as_changes() + function recaptcha_success(response) { + $("#create-account-submit").removeClass("disabled") + } + + function recaptcha_expired(response) { + $("#create-account-submit").addClass("disabled") + } + function get_first_error(field) { if (errors[field] && errors[field].length > 0) { return errors[field][0] @@ -239,7 +247,7 @@ recaptchaInput.closest('div.field').addClass('error') recaptchaInput.after("
" + recaptcha + "
") } - + if (musician_instruments) { var musicianInstrumentsInput = $('#instrument_selector'); musicianInstrumentsInput.closest('div.ftue-instrumentlist-wrapper').addClass('error') From 3b71931e7334e2ddb12009f40234e87ebfa8a013 Mon Sep 17 00:00:00 2001 From: Seth Call Date: Wed, 15 Jul 2015 10:04:45 -0500 Subject: [PATCH 07/89] merge conflict from feature/new_session (VRFS-3283) --- admin/Gemfile | 2 +- admin/build | 2 +- admin/config/initializers/email.rb | 7 +- build | 7 +- db/jenkins | 4 +- db/manifest | 17 +- db/up/alter_band_profile_rate_defaults.sql | 2 + db/up/user_profile_corrections.sql | 2 + pb/jenkins | 2 +- ruby/Gemfile | 2 +- ruby/jenkins | 2 +- ruby/lib/jam_ruby/models/band.rb | 13 + ruby/lib/jam_ruby/models/musician_search.rb | 45 +- ruby/lib/jam_ruby/models/user.rb | 5 + ruby/spec/jam_ruby/models/band_spec.rb | 36 +- ruby/spec/support/utilities.rb | 4 + web/Gemfile | 3 +- .../content/bkg_slider_gain_horiz_24.png | Bin 0 -> 263 bytes web/app/assets/images/content/icon_email.png | Bin 0 -> 296 bytes .../assets/images/content/icon_equalizer.png | Bin 0 -> 1010 bytes .../images/content/icon_instrument_chat45.png | Bin 0 -> 2296 bytes .../content/icon_instrument_headphones21.png | Bin 0 -> 1545 bytes .../content/icon_instrument_headphones45.png | Bin 0 -> 2322 bytes .../content/icon_instrument_metronome21.png | Bin 0 -> 1354 bytes .../content/icon_instrument_metronome45.png | Bin 0 -> 2998 bytes web/app/assets/images/content/icon_leave.png | Bin 0 -> 1077 bytes web/app/assets/images/content/icon_mixer.png | Bin 0 -> 1042 bytes .../images/content/icon_open_folder.png | Bin 0 -> 1290 bytes web/app/assets/images/content/icon_pan.png | Bin 0 -> 1268 bytes web/app/assets/images/content/icon_record.png | Bin 0 -> 1021 bytes web/app/assets/images/content/icon_resync.png | Bin 320 -> 1206 bytes .../images/content/icon_settings_sm.png | Bin 1165 -> 1184 bytes web/app/assets/images/content/icon_share.png | Bin 1121 -> 1165 bytes web/app/assets/images/content/icon_sound.png | Bin 0 -> 1245 bytes web/app/assets/images/content/icon_video.png | Bin 0 -> 1080 bytes web/app/assets/images/content/icon_volume.png | Bin 0 -> 1018 bytes .../assets/images/content/icon_volume_lg.png | Bin 0 -> 1279 bytes .../images/web/button_cta_jamtrack_free.png | Bin 0 -> 17603 bytes web/app/assets/javascripts/JamServer.js | 76 +- web/app/assets/javascripts/accounts.js | 2 +- .../assets/javascripts/accounts_affiliate.js | 2 +- .../javascripts/accounts_profile_interests.js | 16 +- .../javascripts/accounts_profile_samples.js | 76 +- web/app/assets/javascripts/addNewGear.js | 6 +- web/app/assets/javascripts/addTrack.js | 2 +- web/app/assets/javascripts/application.js | 4 +- web/app/assets/javascripts/backend_alerts.js | 61 +- web/app/assets/javascripts/band_setup.js | 19 +- web/app/assets/javascripts/clientUpdate.js | 2 +- .../assets/javascripts/client_init.js.coffee | 7 +- .../dialog/configureTrackDialog.js | 4 + .../dialog/localRecordingsDialog.js | 6 +- .../dialog/openBackingTrackDialog.js | 4 +- .../javascripts/dialog/openJamTrackDialog.js | 4 +- .../javascripts/dialog/rateSessionDialog.js | 1 + .../dialog/recordingFinishedDialog.js | 2 +- .../dialog/recordingSelectorDialog.js | 176 +-- .../dialog/sessionMasterMixDialog.js.coffee | 36 + .../dialog/sessionSettingsDialog.js | 65 +- .../dialog/soundCloudPlayerDialog.js.coffee | 18 +- web/app/assets/javascripts/faderHelpers.js | 116 +- web/app/assets/javascripts/fakeJamClient.js | 46 +- .../javascripts/fakeJamClientRecordings.js | 4 +- web/app/assets/javascripts/feedHelper.js | 31 +- web/app/assets/javascripts/globals.js | 72 +- .../assets/javascripts/helpBubbleHelper.js | 16 +- .../assets/javascripts/instrumentSelector.js | 14 +- web/app/assets/javascripts/jam_rest.js | 24 +- .../javascripts/jam_track_screen.js.coffee | 1 - web/app/assets/javascripts/jamkazam.js | 2 +- .../jquery.metronomePlaybackMode.js | 6 +- web/app/assets/javascripts/minimal/minimal.js | 25 + .../musician_search_filter.js.coffee | 99 +- .../assets/javascripts/panHelpers.js.coffee | 37 + .../assets/javascripts/playbackControls.js | 156 ++- web/app/assets/javascripts/profile.js | 30 +- web/app/assets/javascripts/profile_utils.js | 107 +- .../assets/javascripts/react-components.js | 18 +- .../MediaControls.js.jsx.coffee | 204 ++++ .../PopupMediaControls.js.jsx.coffee | 126 ++ .../PopupRecordingStartStop.js.jsx.coffee | 135 +++ .../PopupWrapper.js.jsx.coffee | 13 + .../SessionBackingTrack.js.jsx.coffee | 89 ++ .../SessionChatMixer.js.jsx.coffee | 75 ++ .../SessionInviteMusiciansBtn.js.jsx.coffee | 26 + .../SessionJamTrack.js.jsx.coffee | 89 ++ .../SessionJamTrackCategory.js.jsx.coffee | 81 ++ .../SessionLeaveBtn.js.jsx.coffee | 23 + ...essionMasterCategoryControls.js.jsx.coffee | 53 + .../SessionMasterMediaTracks.js.jsx.coffee | 55 + .../SessionMasterMix.js.jsx.coffee | 13 + .../SessionMasterMyTracks.js.jsx.coffee | 40 + .../SessionMasterOtherTrack.js.jsx.coffee | 93 ++ .../SessionMasterOtherTracks.js.jsx.coffee | 66 ++ .../SessionMediaTracks.js.jsx.coffee | 307 +++++ .../SessionMetronome.js.jsx.coffee | 81 ++ .../SessionMixerBtn.js.jsx.coffee | 14 + .../SessionMusicMixer.js.jsx.coffee | 75 ++ .../SessionMyChat.js.jsx.coffee | 69 ++ .../SessionMyTrack.js.jsx.coffee | 96 ++ .../SessionMyTracks.js.jsx.coffee | 50 + .../SessionNotification.js.jsx.coffee | 29 + .../SessionNotifications.js.jsx.coffee | 41 + .../SessionOtherTrack.js.jsx.coffee | 121 ++ .../SessionOtherTracks.js.jsx.coffee | 83 ++ .../SessionRecordBtn.js.jsx.coffee | 24 + .../SessionRecordedCategory.js.jsx.coffee | 84 ++ .../SessionRecordedTrack.js.jsx.coffee | 89 ++ .../SessionResyncBtn.js.jsx.coffee | 20 + .../SessionScreen.js.jsx.coffee | 78 ++ .../SessionSelfVolumeHover.js.jsx.coffee | 169 +++ .../SessionSettingsBtn.js.jsx.coffee | 20 + .../SessionShareBtn.js.jsx.coffee | 20 + .../SessionTrackGain.js.jsx.coffee | 51 + .../SessionTrackPan.js.jsx.coffee | 54 + .../SessionTrackPanHover.js.jsx.coffee | 51 + .../SessionTrackSettingsBtn.js.jsx.coffee | 22 + .../SessionTrackVU.js.jsx.coffee | 87 ++ .../SessionTrackVolumeHover.js.jsx.coffee | 117 ++ .../SessionVideoBtn.js.jsx.coffee | 16 + .../SessionVolumeSettingsBtn.js.jsx.coffee | 28 + .../react-components/Test.js.jsx.coffee | 19 + .../actions/AppActions.js.coffee | 5 + .../actions/BroadcastActions.js.coffee | 2 +- .../actions/MediaPlaybackActions.js.coffee | 11 + .../actions/MixerActions.js.coffee | 16 + .../actions/NotificationActions.js.coffee | 9 + .../actions/RecordingActions.js.coffee | 14 + .../actions/SessionActions.js.coffee | 22 + .../actions/SessionMyTracksActions.js.coffee | 5 + .../helpers/MixerHelper.js.coffee | 850 +++++++++++++ .../helpers/SessionHelper.js.coffee | 112 ++ .../InvidualJamTrackPage.js.jsx.coffee | 52 + .../landing/JamTrackCta.js.jsx.coffee | 60 + .../landing/PopupYoutubePlayer.js.jsx.coffee | 11 + .../MasterPersonalMixersMixin.js.coffee | 17 + .../mixins/SessionMediaTracksMixin.js.coffee | 51 + .../mixins/SessionMyTracksMixin.js.coffee | 45 + .../mixins/SessionOtherTracksMixin.js.coffee | 6 + .../stores/AppStore.js.coffee | 12 + .../stores/MediaPlaybackStore.js.coffee | 120 ++ .../stores/MixerStore.js.coffee | 244 ++++ .../stores/RecordingStore.js.jsx.coffee | 71 ++ .../stores/SessionMediaTracksStore.js.coffee | 21 + .../stores/SessionMyTracksStore.js.coffee | 21 + .../stores/SessionNotificationStore.js.coffee | 61 + .../stores/SessionOtherTracksStore.js.coffee | 21 + .../stores/SessionStore.js.coffee | 1053 +++++++++++++++++ web/app/assets/javascripts/recordingModel.js | 71 +- web/app/assets/javascripts/session.js | 30 +- web/app/assets/javascripts/sidebar.js | 16 +- .../assets/javascripts/sync_viewer.js.coffee | 6 +- web/app/assets/javascripts/trackHelpers.js | 18 +- web/app/assets/javascripts/ui_helper.js | 4 +- web/app/assets/javascripts/utils.js | 103 +- web/app/assets/javascripts/voiceChatHelper.js | 6 +- web/app/assets/javascripts/vuHelpers.js | 151 ++- .../javascripts/web/individual_jamtrack.js | 30 +- ...band.js => individual_jamtrack_band_v1.js} | 0 .../javascripts/web/individual_jamtrack_v1.js | 77 ++ web/app/assets/javascripts/web/web.js | 9 +- .../javascripts/webcam_viewer.js.coffee | 4 + .../wizard/loopback/step_loopback_test.js | 4 +- .../client/accountProfileInterests.css.scss | 33 +- .../assets/stylesheets/client/band.css.scss | 56 +- .../assets/stylesheets/client/common.css.scss | 12 + .../stylesheets/client/content-orig.css.scss | 2 +- .../stylesheets/client/content.css.scss | 4 +- .../metronomePlaybackModeSelect.css.scss | 1 + .../stylesheets/client/musician.css.scss | 105 +- .../stylesheets/client/profile.css.scss | 35 +- .../react-components/MediaControls.scss.scss | 206 ++++ .../react-components/SessionScreen.css.scss | 386 ++++++ .../SessionSelfVolumeHover.css.scss | 2 + .../react-components/SessionTrack.css.scss | 409 +++++++ .../stylesheets/client/session.css.scss | 2 +- .../dialogs/inviteMusiciansDialog.css.scss | 18 + .../dialogs/recordingSelectorDialog.css.scss | 26 +- .../dialogs/sessionMasterMixDialog.css.scss | 61 + .../dialogs/sessionSettingsDialog.css.scss | 67 ++ .../stylesheets/dialogs/shareDialog.css.scss | 12 + .../landings/individual_jamtrack.css.scss | 90 +- .../individual_jamtrack_band_v1.css.scss | 35 + ...s.scss => individual_jamtrack_v1.css.scss} | 2 +- .../minimal/media_controls.css.scss | 45 + .../stylesheets/minimal/minimal.css.scss | 5 +- .../stylesheets/minimal/minimal_main.css.scss | 5 - .../assets/stylesheets/minimal/popup.css.scss | 6 + .../minimal/recording_controls.css.scss | 95 ++ .../minimal/youtube_player.css.scss | 8 + .../stylesheets/web/audioWidgets.css.scss | 4 + web/app/assets/stylesheets/web/web.css | 1 + .../api_music_notations_controller.rb | 16 +- web/app/controllers/landings_controller.rb | 32 +- web/app/controllers/popups_controller.rb | 17 + web/app/helpers/landings_helper.rb | 28 + web/app/views/api_music_notations/create.rabl | 6 +- .../api_music_sessions/jam_track_open.rabl | 3 + .../api_music_sessions/metronome_close.rabl | 3 + .../api_music_sessions/metronome_open.rabl | 3 + .../api_music_sessions/open_jam_track.rabl | 3 - web/app/views/api_search/index.rabl | 6 +- .../_account_profile_interests.html.erb | 75 +- web/app/views/clients/_addNewGear.html.erb | 2 +- web/app/views/clients/_band_setup.html.slim | 24 +- web/app/views/clients/_faders.html.erb | 8 +- web/app/views/clients/_help.html.slim | 2 +- .../views/clients/_inviteMusicians.html.erb | 21 +- .../clients/_metronome_playback_mode.slim | 2 +- .../clients/_metronome_playback_mode2.slim | 8 + .../clients/_musician_search_filter.html.slim | 51 +- web/app/views/clients/_profile.html.erb | 182 ++- .../_profile_edit_presence_controls.html.slim | 12 +- ..._profile_summary_online_presence.html.slim | 18 +- ...file_summary_performance_samples.html.slim | 11 +- web/app/views/clients/_session.html.slim | 2 +- web/app/views/clients/_session2.html.slim | 8 + .../views/clients/_sessionSettings.html.haml | 24 +- web/app/views/clients/index.html.erb | 18 +- web/app/views/dialogs/_dialogs.html.haml | 3 +- .../_recordingSelectorDialog.html.haml | 13 +- .../dialogs/_sessionMasterMixDialog.html.slim | 11 + web/app/views/dialogs/_shareDialog.html.erb | 10 +- .../landings/affiliate_program.html.slim | 2 +- .../landings/individual_jamtrack.html.slim | 29 +- ... => individual_jamtrack_band_v1.html.slim} | 2 +- .../landings/individual_jamtrack_v1.html.slim | 34 + .../landings/product_jamtracks.html.slim | 2 +- web/app/views/layouts/minimal.html.erb | 2 +- web/app/views/layouts/web.html.erb | 3 +- web/app/views/popups/media_controls.html.slim | 3 + .../views/popups/recording_controls.html.slim | 2 + web/app/views/popups/youtube_player.html.slim | 2 + .../users/_feed_recording_ajax.html.haml | 3 + web/build | 5 + web/config/application.rb | 1 + web/config/initializers/email.rb | 7 +- web/config/initializers/gon.rb | 1 + web/config/routes.rb | 12 + web/jenkins | 2 +- web/lib/utils.rb | 8 +- web/spec/factories.rb | 2 + web/spec/features/account_spec.rb | 19 +- web/spec/features/checkout_spec.rb | 2 +- web/spec/features/create_session_spec.rb | 20 +- web/spec/features/gear_wizard_spec.rb | 2 +- web/spec/features/in_session_spec.rb | 4 +- .../features/individual_jamtrack_band_spec.rb | 67 -- web/spec/features/individual_jamtrack_spec.rb | 56 +- web/spec/features/musician_profile_spec.rb | 37 +- web/spec/features/musician_search_spec.rb | 17 +- web/spec/features/recordings_spec.rb | 2 +- web/spec/features/signup_spec.rb | 8 +- web/spec/javascripts/faderHelpers.spec.js | 4 +- web/spec/requests/bands_api_spec.rb | 6 +- web/spec/support/app_config.rb | 4 + web/spec/support/utilities.rb | 39 +- web/vendor/assets/javascripts/bugsnag.js | 4 +- websocket-gateway/jenkins | 4 +- 259 files changed, 9674 insertions(+), 1183 deletions(-) create mode 100644 db/up/alter_band_profile_rate_defaults.sql create mode 100644 db/up/user_profile_corrections.sql create mode 100644 web/app/assets/images/content/bkg_slider_gain_horiz_24.png create mode 100644 web/app/assets/images/content/icon_email.png create mode 100644 web/app/assets/images/content/icon_equalizer.png create mode 100644 web/app/assets/images/content/icon_instrument_chat45.png create mode 100644 web/app/assets/images/content/icon_instrument_headphones21.png create mode 100644 web/app/assets/images/content/icon_instrument_headphones45.png create mode 100644 web/app/assets/images/content/icon_instrument_metronome21.png create mode 100644 web/app/assets/images/content/icon_instrument_metronome45.png create mode 100644 web/app/assets/images/content/icon_leave.png create mode 100644 web/app/assets/images/content/icon_mixer.png create mode 100644 web/app/assets/images/content/icon_open_folder.png create mode 100644 web/app/assets/images/content/icon_pan.png create mode 100644 web/app/assets/images/content/icon_record.png create mode 100644 web/app/assets/images/content/icon_sound.png create mode 100644 web/app/assets/images/content/icon_video.png create mode 100644 web/app/assets/images/content/icon_volume.png create mode 100644 web/app/assets/images/content/icon_volume_lg.png create mode 100644 web/app/assets/images/web/button_cta_jamtrack_free.png create mode 100644 web/app/assets/javascripts/dialog/sessionMasterMixDialog.js.coffee create mode 100644 web/app/assets/javascripts/minimal/minimal.js create mode 100644 web/app/assets/javascripts/panHelpers.js.coffee create mode 100644 web/app/assets/javascripts/react-components/MediaControls.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/PopupMediaControls.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/PopupRecordingStartStop.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/PopupWrapper.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/SessionBackingTrack.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/SessionChatMixer.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/SessionInviteMusiciansBtn.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/SessionJamTrack.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/SessionJamTrackCategory.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/SessionLeaveBtn.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/SessionMasterCategoryControls.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/SessionMasterMediaTracks.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/SessionMasterMix.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/SessionMasterMyTracks.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/SessionMasterOtherTrack.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/SessionMasterOtherTracks.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/SessionMediaTracks.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/SessionMetronome.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/SessionMixerBtn.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/SessionMusicMixer.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/SessionMyChat.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/SessionMyTrack.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/SessionMyTracks.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/SessionNotification.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/SessionNotifications.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/SessionOtherTrack.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/SessionOtherTracks.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/SessionRecordBtn.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/SessionRecordedCategory.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/SessionRecordedTrack.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/SessionResyncBtn.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/SessionScreen.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/SessionSelfVolumeHover.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/SessionSettingsBtn.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/SessionShareBtn.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/SessionTrackGain.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/SessionTrackPan.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/SessionTrackPanHover.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/SessionTrackSettingsBtn.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/SessionTrackVU.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/SessionTrackVolumeHover.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/SessionVideoBtn.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/SessionVolumeSettingsBtn.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/Test.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/actions/AppActions.js.coffee create mode 100644 web/app/assets/javascripts/react-components/actions/MediaPlaybackActions.js.coffee create mode 100644 web/app/assets/javascripts/react-components/actions/MixerActions.js.coffee create mode 100644 web/app/assets/javascripts/react-components/actions/NotificationActions.js.coffee create mode 100644 web/app/assets/javascripts/react-components/actions/RecordingActions.js.coffee create mode 100644 web/app/assets/javascripts/react-components/actions/SessionActions.js.coffee create mode 100644 web/app/assets/javascripts/react-components/actions/SessionMyTracksActions.js.coffee create mode 100644 web/app/assets/javascripts/react-components/helpers/MixerHelper.js.coffee create mode 100644 web/app/assets/javascripts/react-components/helpers/SessionHelper.js.coffee create mode 100644 web/app/assets/javascripts/react-components/landing/InvidualJamTrackPage.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/landing/JamTrackCta.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/landing/PopupYoutubePlayer.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/mixins/MasterPersonalMixersMixin.js.coffee create mode 100644 web/app/assets/javascripts/react-components/mixins/SessionMediaTracksMixin.js.coffee create mode 100644 web/app/assets/javascripts/react-components/mixins/SessionMyTracksMixin.js.coffee create mode 100644 web/app/assets/javascripts/react-components/mixins/SessionOtherTracksMixin.js.coffee create mode 100644 web/app/assets/javascripts/react-components/stores/AppStore.js.coffee create mode 100644 web/app/assets/javascripts/react-components/stores/MediaPlaybackStore.js.coffee create mode 100644 web/app/assets/javascripts/react-components/stores/MixerStore.js.coffee create mode 100644 web/app/assets/javascripts/react-components/stores/RecordingStore.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/stores/SessionMediaTracksStore.js.coffee create mode 100644 web/app/assets/javascripts/react-components/stores/SessionMyTracksStore.js.coffee create mode 100644 web/app/assets/javascripts/react-components/stores/SessionNotificationStore.js.coffee create mode 100644 web/app/assets/javascripts/react-components/stores/SessionOtherTracksStore.js.coffee create mode 100644 web/app/assets/javascripts/react-components/stores/SessionStore.js.coffee rename web/app/assets/javascripts/web/{individual_jamtrack_band.js => individual_jamtrack_band_v1.js} (100%) create mode 100644 web/app/assets/javascripts/web/individual_jamtrack_v1.js create mode 100644 web/app/assets/stylesheets/client/react-components/MediaControls.scss.scss create mode 100644 web/app/assets/stylesheets/client/react-components/SessionScreen.css.scss create mode 100644 web/app/assets/stylesheets/client/react-components/SessionSelfVolumeHover.css.scss create mode 100644 web/app/assets/stylesheets/client/react-components/SessionTrack.css.scss create mode 100644 web/app/assets/stylesheets/dialogs/inviteMusiciansDialog.css.scss create mode 100644 web/app/assets/stylesheets/dialogs/sessionMasterMixDialog.css.scss create mode 100644 web/app/assets/stylesheets/dialogs/sessionSettingsDialog.css.scss create mode 100644 web/app/assets/stylesheets/landings/individual_jamtrack_band_v1.css.scss rename web/app/assets/stylesheets/landings/{individual_jamtrack_band.css.scss => individual_jamtrack_v1.css.scss} (89%) create mode 100644 web/app/assets/stylesheets/minimal/media_controls.css.scss create mode 100644 web/app/assets/stylesheets/minimal/popup.css.scss create mode 100644 web/app/assets/stylesheets/minimal/recording_controls.css.scss create mode 100644 web/app/assets/stylesheets/minimal/youtube_player.css.scss create mode 100644 web/app/controllers/popups_controller.rb create mode 100644 web/app/helpers/landings_helper.rb create mode 100644 web/app/views/api_music_sessions/jam_track_open.rabl create mode 100644 web/app/views/api_music_sessions/metronome_close.rabl create mode 100644 web/app/views/api_music_sessions/metronome_open.rabl delete mode 100644 web/app/views/api_music_sessions/open_jam_track.rabl create mode 100644 web/app/views/clients/_metronome_playback_mode2.slim create mode 100644 web/app/views/clients/_session2.html.slim create mode 100644 web/app/views/dialogs/_sessionMasterMixDialog.html.slim rename web/app/views/landings/{individual_jamtrack_band.html.slim => individual_jamtrack_band_v1.html.slim} (97%) create mode 100644 web/app/views/landings/individual_jamtrack_v1.html.slim create mode 100644 web/app/views/popups/media_controls.html.slim create mode 100644 web/app/views/popups/recording_controls.html.slim create mode 100644 web/app/views/popups/youtube_player.html.slim delete mode 100644 web/spec/features/individual_jamtrack_band_spec.rb 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 0000000000000000000000000000000000000000..aad8fb7b9e3336e25c04072b0ef639b0db9d61d1 GIT binary patch literal 263 zcmeAS@N?(olHy`uVBq!ia0vp^>_9BR!3HGFE|gW!U_%O?XxI14-? ziy0WWg+Z8+Vb&Z8pdfpRr>`sfeKvk3MWGhkz)Yaf6i*k&5RLO^FB)Uj(MB|?X_`?sk7(Q-1n4Rjxar>mdKI;Vst E0PCGz>i_@% literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..a3163f217b341d24f5468101bccabcafca8cd67a GIT binary patch literal 296 zcmeAS@N?(olHy`uVBq!ia0vp^Vn8g)!3HFq6y&A?DYhhUcNd2LAh=-f^2tCE&H|6f zVg?4j!ywFfJby(BP>{XE)7O>#KAWrEak-aXPsoTkYfgTMyRGJW_w`KMSNm0NJ=gBgTe~DWM4f%gtwI literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..9c63c8dcc3d44de4749a973b3fe577c3a05043fe GIT binary patch literal 1010 zcmVVGd000McNliru-v$g5IxUnu(Psbv03B&m zSad^gZEa<4bN~PV002XBWnpw>WFU8GbZ8()Nlj2>E@cM*00UM@L_t(I%axVSPa9Vd z$3N@c^}>P)l7)@=Wtnv>yKYD$a%h@ML5&1Oq?bl&#I-pAm*$pArGEg2^uQGfaY)6T zgKs{h2#QiE$Os!Y6&OqW5Ee+rUhG}_^^n>sf#%0ZBfZ1uGxO&AW}Xx_48cd55Q3JL z7Jhv4gg;)sWOa2FRaGy=xcCHf9EVUS#PswuV`F1Dj&mjElYrG~6(bx5pr@w?S(e`q zv~3&Pw%>c@STy<;*xQS)>j)vP2i9sezPNLT!NEalwc52nMNx=EA^>!BbkN@3ekImb z&)n41#N6B*v$L}(ih>Y=i;D{?l}e*JkH>>y80_rqpzAt5pYM90<2VF^K{`7->FMny z7!2Y#4igg-jpkRYRs4QGKEIztB7qQszP`SzK-;z{7K?lqk0Z-6s;V+DFo3S>{4z7c zPfwqsC<>0_(BI#Ws;aE4tZ;sQj$s&7DizA*a>JFuyLV}8Yva3z582(_1>mdu_erPI z=(_&4DxZ(P-@G9ji%}>P$fVO89~}`2g$Rek#N%-!NkRyLGC4WP@bEA~2=^${alVfR~J)LQ~dDwaYINcPIq@V-#>c9;lTl=Qi*|Ex6s^fZbl+xG8rx`3)3_)41+K4-6NSyGCVZIx1*yd zxm>Q%SRn+dR0_+oh(sd%{^A8UBM}B}-=@RoqpPcn#l=OCBuvx9>-FOGdRbapVsmqo z=H_Nxbs_40aBx5-od)2~^>sF0za|=u5{t#qG>uF;jjF0-v)RU@-rn9u(==37y*w$B zBvCGxSzBAfaU3>NDK<7X@caFIJv4-6S?ulYp{i=59ky+=x4YXIL*?CR&@_#Og$0(E zmoZHfx7&>@%Z!hYlgs5eIXOX56i!c1DHICGvdrP(AugBedZ5eYBA?H5e0+>7%k1y( zlh5Y~1Ooi^>J_C@2~E?mEQ@3^i4cNfvH1TL)}i%`XJ==aJ39md0Wz6PLqJ)U`T5zi z#`&Yym*Y5W{re`WFU8GbZ8()Nlj2>E@cM*00>-3L_t(o!_ApnP}5f) z$3OWeAwezz0wENkY%BxB#DbSRs4zyU)#?HbbiH&HXDnK0 zY-JzXRUdSFV=CxKSF222uI3_w5{%lQkw_Xhgd`-{hbmiys8!n5UFjEJmSFke8Q7ettfGP^rks$RIvG9zhTYg+knOyXooaq04He{n90_T)9GT zZ!f`M@M)Jt^fb^ZDJjgFHH&$AJw-)DWM*a}lgS7MgZO+td_EuHa2P=lh>3|oCX*o+ zi}8BB*zIQU8jcl=$l$6b*4QQuLW9QDDXtY|s`Q{rw+O!Fi(MTv1dg!%9kKPqQ zfN*4E27^JaUcE{~Ljwwh0=-_(v}w~YnoRWe_C7guen9~{ckDnlWeR_*M*P~XeX=-Z1HHw3dfL1D%Y}>XCjaEzbp+i(vRpIq|c^>i& z4A9uv$i#^gDJv@@J3E_(^XEsbKMYzXld=B2_gJ`aA^$vnoXX0|=LY;hfk1%Pwl;Ed za#*ln0f^=>`z;L(4U7lX*Vj{5SI3MQGkA5;qT$f#>FF$4vV=Ph2S<+{eWqe&Eamt6 zIdb?gZnqnQ!9Y?{(*K}m&z?@Qm^5h;gM)+Ix^)YYNHmn- zy02X$7!0CPsgNi#GU)H`$LVweAdyO$n3za%ax%-7En~*anK+$JKHa~c#>U2|gu{e{ z1m>2MP*zrkR;wj5Gn3}#X7*H8($Ud@$K%1-+snYf07|72$;8A&yk756jghHTQc_aF zs@1DWOG^X5?RGO&qoJ*>Eh^9|m5Sxdm!r{WkV>Th=yW*Vc@vMvgU{z9 zF)(j#>S%6YRS#bjmpu;FDPKjlqo~N50sUa#d{kz zAeYMt1Of~W4kDAu5W=AlB9RC|5b*o`R8&+D8yh>KHBzh96crUk1$ts)V#KlH;$k!! z4HBsoi9~|m??FMdDrlum7%ej5)7J}7kMJ|_< zot;g5Lc$~eye~@0C;5IM6iNTNcaNMGUqmbx(`B_HT(VdQ27?q97UFO?B2JV@BxBlz zKivF2>_@+?zfYe&O<`dne!rjg_I8AmCr?r`cP`b{)wDD>M+|9$gM-98o!}& zrsKwq8yJm7v|258yPeL?PK5scejE-5cDo&u$wW&_OXN1l?T!j`YilbOi)Dl*ygbxxGCq(B0kL7>&kZP4t~R zclg`KAM@`E7ua1{iAW^khx&Sg!63upjKkr;!0wojOHV zS65VOjzA!Q*<`|Eu`p@UB$^r<`QnQ&=gr#AAMN1>m6i1o_i4{VbLPz9wbx$5Y&KI< zQ#1O#$9R%bsbuw()_|j^y`*rf5cw^z?MzdG}r3 zc;gMMRx6u6{E$nRE{z^leou25t=>?l)3NrQcPK6{rlqBYty{KWu~;I9!m(Psq_h;B zPB-F3J3Bk6`R=<>VO1&>Z@&2^OJ0AS*w|Q(A3H`>RTXx-{n7Dzte|x|9V=I^99axq zzdj7Qq_h;hUXKV^P*z5IdO95)9US`nb55K%!N9=4e2eSJOW&z% S$2t}O0000eM}Q)96soTP3z~Fikb5)GwP(hYpRfS7C8LtMp^JjV4~Q``x42<8XNW@`vP6wvq=7k`s8blOFxeFW_s7^TxqCnIJkRel z?;Epx8#MMUTJzUIg2$ zYEizzfEt{cpv1Pk%mvnz8BJ8#MoMKBrD@<)FU|`%07t@Jhn;3|uUa&$i}U;FH6ntC zA>2l_=w(o5Lk^tDxB#q>q=+drMF}fa5~)I|lq-{A87fsED28CDScc(p6qn235fky= zT-Nn?u9g__#h=uo5{`4?2;%X0Bpyt{xQY>}N~J1*3 z|5KPXZFT}A7qE=mMe&VWpA>cF#4}xh(+sHwqf#fB#`a$+~sxemtJvbSfMUAIJ7W=?^m^>50a*V$i^1+c)0mXtCcsU7MGex3=J{y|(Shp+j&}eSLjwc3=~%yZ7DJfffG4A5}ow zEP|+yXHj*exWsHw2<&a~Mx=GFH0 z_6k>hfx8dP*{Rt!_&g46ze|4Od3^W!(Uk$ok)4MdR@G@Dk-o4p8@_V;92cwXz34cFMf;1<)#f11Py{pr<`JbuiL5>7u)J!>$;(6?!OKkk)$$Js5qQ^*W>W)G|xu EKNDI`g8%>k literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..82533c0d2c12ca295fa96a68003619f595cc9fc0 GIT binary patch literal 2322 zcmaJ@eLU0q9v?{#O+rhonUUz&^E7rhGlqC=<1R&_tnJ4Zd$9d5EJTu=bGe=-R}ZRF z)6)s*=2D3sklT87L=rtYFAqf$Cvzu))!{k-1q_vi8Zdj0Z3H~~;o zTT=)G0u9{4j`$UreK7T^LrK}te9 zK!re-xrjo;!EiQRsba6jCIhp~BqR zIJV3e5Q?^>%7Lw^oKSvh9N$xbqI)5!N{UWE41heOQXDT)P?R*(XI+YJuQy{+$j=Zk zj)wXpsBm@&(pM@6knWhxXg+Q;8A`!dR-*V)CSAd>N}ddh$KdoSEdkl={|^<5 zzoHc&7x**Y|0%2pO_2duE})Pm%K5s+#W?C+$tb>ZfCozDp;Br5QWZmlQc$W8N@Yl2 z-_KP;y0CeCkwkBF{mf#sDS;9N$dmAaKqd{P)4_;D0t$nK$NBpceDVH75+3j8?%_cs z`Fr3!H@g!^{=P)g5|=6ECyD_HxWpCw$z^_%s~-rlOy`*i$VEwjz+WyEBR?lj5q&!r z#y9o8a0TCv#pjz`tS%U=zPc=d9e+Tgn|ZlzxSebh>miWknSo43 zsPbv&ktmkqHtTz5`^zRaY*3XM?pS8+2J5y6?}#;+Hn8eG0KqumqnQY!AF!8C?C>+| zw+4M%@(V4TsO6LIo3rn(F;GLsjDuJPA~S*dXKTZbq^e&}g=lYGbq#+~CgyPnBb`SZ zhJH^Sf0nlR(L6`F{KLcq($dQ66p=u9R8&;Ny%sr+b}mk_lHlDCX_vO2Kh6#gUi9?x z@}f|wR8(nDtnLb6*AP8CX5^P4_x1{fnh+Q30;q9o)vcB*5o6E$`uc)jhP=e?vg7nQ zbhpk84-p<(r)|1@=T1&nSC{ITTK)06$kaW(EiG9BnM~>pTzkpKTc(9W~AzeNl!m zuV`#+v@Os#>M3YBR3@moUuEu9kSryrt%l?=674Ksk1Oxtk%%p7BphJPW|@V z8A}dnsvzM9^toFzNsOVPq34?o$YgRxP-GR4YULq+Vg#j8Z7PH#luT`wo_FpwMHE{dNqes_IO-`QNwUKl;z;@+-_X#|hLgVjS$=;0^X%TDHzOk=OaNlg=SExGmNNwf+6*$}T+5SL zBjwnuS3X@mJsC~)^*@oSD>WJoF4iY|(Zb|Z?rUK}Lc*2W+DY>qAADkv0l~y5OB=G| zB{Ac>;3A`%m*j9)l&ll z16~#%BFl5e|6c7_R#s-3ng64zIb|8lE-=)C`~D2Kx}>|iJDpLem1G`Vw7mN^q3KXT z!Ac!}2P;hXt*4cq%5Sd)K%A>&hfn2k&ut4tV=?RvB{z4zKjxEcHA zvx+t6?F&baB5Z7CRufp6n&R>+xsQLMYMSiZAQwYi(#=gx<-fjHzLcfJZ%^s&)SRVf zsnf>~Pfz8IOwXZh4&U6Lv!egpvGjIW$xewx5)%~_Rb3Z`K|0#7+3ek6+qUg9ksq3V zRBCo)?3p`i2Xy=E)oU-^ZI9VJmR9(0x1@4J)U{S7tS$~)z-mq!tJtOPt)sIQf)`y~ zq2|Z6!3Q`@a;b=988(yV*^?xaWejMf{pXjXr0HtoervLx| literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..362f086655e886061f667635f1cc300e4744126f GIT binary patch literal 1354 zcmeAS@N?(olHy`uVBq!ia0vp^q9Dw{1|(OCFP#RYBuiW)N`mv#O3D+9QW+dm@{>{( zJaZG%Q-e|yQz{EjrrIztFl%InM3hAM`dB6B=jtVb)aX^@765fKFxc2v6eK2Rr0UTq__OB&@Hb09I0xZL0)vRD^GUf^&XRs)DJWnQpRynYn_wrJkXw zxw(nCj)IYap{c%svA(f^u92~oiGh`gkpdJb0c|TvNwW%aaf8|g1^l#~=$>Fbx5 zm+O@q>*W`v>l<2HTIw4Z=^Gj80#)c1SLT%@R_NvxE5l51Ni9w;$}A|!%+FH*nV6WA zUs__T1av9H3%LbwWAlok!2}F2{ffi_eM3D1ke6TzeSPsO&CP|YE-nd5MYtEM!Nnn! z1*!T$sm1xFMajU3OH&3}Rbb^@l$uzQUlfv`p92fUfQafhdi zV@SoVHB&cwI|Ygy^Y5Ru=^#(jv}p>Q3zz<5P?m{`nw_=fS;oX~87tYQw=D?#D=br# z&ADW$D?6u@u*-B2uKfE~-yeJU^7Yb`Q#eVx~K#r$8K!CAA)SJZ6^tGt5y?nT#bd_AfVs2joBejxZ?`Gzwa-Rna+ zr~g@GsanAuG^d`$SSG(wvrpf`uW+yMxy-=wHh-@Rn-;d1EYGq#TH9vzsMAAECFP3T zjrSYt+1!rZ7JZ(7VD_!)60aP0F4Q}kT=MPobOp%?W}d&^emqk&fA^j?gZDl9?@!Ii zZWb1JT+>rF``;%0^|`M*z1~%S7Yk`_W;NIHowG()M(#wIpH*fcqt<=%zSHny~j-HgRq63RWPm|R|EB$rVn za_AxFIBDqekdlLlO3I~@rstbSB06Xo z5J=UW;S<0QApC-+u$h)2OE#7wCI>)+Ko};!Wb+E$W&u91k9j1K>Te8 za017bMrYuoxis%+q7NlHoPwf4oE*Uz5gH(1()b}@5%UO(hZZ?NzVM=f{?arI0{&vc z4|jn49aI2;1a@U}Xw(D{508U~AujI@kIShBfcFe?-a1%q3|tgWGd1(X-X;)jT! zES||X1}u$7;W9XU2Ac(5VhlON7VsS)K%{>!fywz!%i?`a6Ob^ND1-yEvV<=!>6;;e z@c&Jj%gCc&nJpQgL%<-Yyp=76fV?c$rT6f%B6+y*<2qs`^dK}lIUzc zn@4AJz^<-evIaIMgisi)rO9nyPy_k}Ad%`fm66t*>p@ zs;2P5`YthkUyYc&WaAemOTMN6cjRVs_ZRbhCrk$vj#D`^(h*LB5pl4Sd_l1g^LeIu zaKUkECboG4BChE2<&I9NZC8J5YpcLGp|-ZR^}%KDyKl3NEVU0d)nf$$!Nbwf(PA79 zcYXe7>|(`%>wEeiKfc}hd=nx}fqZ(MP3A{LSdlH(MV_LyOKsWvqs8LJ;>`V_#eK~)P_gr)uItFz_1=!o$XQia1C*eHm6oscEKqeRb(bd%zl-bnKfL86AO1!L5Eyj6zdU|IBX2{~HQ<;ICX<;h8tJ*X8J~DJX zYM1bDd(K}iAA#h^_EcYQZ)^LYvyww1b-S2mnhFsJM3%OeR+6@XX>fgWb91t_F!Oxa z`_}{E0bAm{>#Bp&)6;c~$`TFH_V#9y^i8Om8J_k1v(N*6UUB!DhTgU~6KZN}ZCK&q zS4ir?>;F=zkAaz$0-zCzMa1VtrNy$m>}nTj1$?g@lb3}heLX%Ko}Mu_by zaBALq_l^;p&3?Ti!M`eK0Csj`(nMex$WT{T7o4tsMvBuh*+!ae{qxVmkY)MXgLneL zoP*GsuJF+-oST~)^xb<}jX)q|Uk$xG9S6@69jH;-3twqC3qGzpdQ|Hni{S6?uU#io z%gc-Z$*^D}QthGB$nWXwX5RBvPDmtj8KgXP;$HC%-O3n!SWxHL3^eu^OQuR2q1kM0 z;}GZ5hqsfWLt>g_gnMJm+-fp>vsRLgDba?yV!|)mN_T+@+jat9jyf zu+|AgQFdyuKTNxtNE)`fF+Xa10sYpkTP)>WoW_c* zf%%!|?-AWs*A&G;HOJYf6B3rIZqQn8xVZJ#*0Hg%n)>?ssV%>pZ`u1`NTPdFF2zmr z#L>|~w_Zl5_UvE?YG6?4C?Ys{#jMJPUSURHvh$uje>&nH_(g#{3mlLO3JKZ8CH^nz~T6bH_eL#29I#$o1%!7lUeK8q4Z+Ljv6DeJMb&fo51Rrr zZYwU>D-Rz&Y*bVD*ttKIj?adqfuA%D#h*LUs;OLav3$=@%7{e%PH!^#^ZCVHFDb=s zJl!n09B!+xukS&tU{I--lpp6_Zg@sE^bQCRnj7(+D{9s5zKe6#M1cbF zpVi3dbZ$4V(9_P$%q$YP)=@V;kT?50O*;L?TPNun?H+5(Ib!^S33_tPCf_3;>1rvO zSJeB<0$1mPH+mHvOV#J8%}W6>7$<##c*udw;Ca8e73q7?)#V@ z5)|~5J)I56cv8N7muo^o2j!T2hl_c00iS=`Ju!LBiNQyYjsoW$7H<^e1lTWmY|@ZC%~E7al@u%LF@3nvAfckVr8(tWJPRf3-JxB zVpdLH$k=j!9xhc|p=0!<&CkOqefN}aXST}flZ)zyk2j{c%tkG|o7}-rwZ}BvhVcEK z5R<84VFfB3_TBC=v9Yl)f(?SfQs(2j7}s#uJ1dSuW2qO;Vsh_`g(eSak7H4S<|9ODE-i2 z(}YWcZJfvb9?cVl#WgsolcKFH{y6JX^@B^=Yi>R0?j|KZHG|yp52aG`cJ-7}dsl0o zP^Sx<^rq$3%MEW9B7_Og(?`Z&RF4Y0X<13ydY${mF)_WWq~d8IzHXgk!Dlg@K5pHg zQ?#l!6C=sV$@xtxeT+Fsj<|79|7iY6wM(4t3?q~GJ?dtU;)zXpY8WKZ2sG%H^icc# dZ=YAfLHg&8se0a6^Y@=Yj`wl!8jD8nJaH#7eUxM zo)rvlC)KB73?)SB}L53Pb|PDU+(n=7MB_m&IqxOp&1Af+m9GUQ#JO211$vf!FDC zh%TQW_ybPb>-T&7cEGr3FXeJmZkK~`vmO`gVL&&{HnlVPvfk1$AF%-i%u!qB(QIU{ysAg-4frF-K$U2fW6T;*^mJR zq!~#~n=ExPmeG)AW;7jyLWQn@4qgyt)iRzfpm?5*swNUt5k|QniFur|EV1E`p9wSl zewyQaG(F($>~wp=opiwGWjx`K+f%}FnmDCE6_v2k0k*R&))IuGRjhdfi~FuZPveKN66N}m zxsN0Hqm{L_wJnViTizby^KIk#&8r*2#^U$6<)*jQwsvyRZ`(=q=IgGnrx)y-=T0;> z(&*6Qr(?G(KL3t2_4)2Cwsq&P5f3g^e0etX%T>Fp=a(_i~OfZh43i@$mv?)Gj3)|Z;W YD`NZBon_hnbIp3l(SaduwLiV^2e7JHegFUf literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..2de1cb8e2ece690bd487ef4894c19398d7bd3892 GIT binary patch literal 1042 zcmaJ=TSyd97#^rBBqbq&O7Ji~q|wf8Z|>kGy0fDT%_i%DE4?_*9NkIh;>_80+=A9F zP=pVL4@o|SFVT$xdnwF(kw7AY3ZjRg$0A=M2#Pwh?yfzw4V*dW{QTed-@bFTKiS(* zf2f`yh=xR;kivVTxAxTGe@mZl18)aWJc9)=h0z4j+2vZYG7b?Kfn%BxB{!cuBY~zy$-$t+NLCDHwZ4K4 z2MWowQW#evD%o`$bh@n6t4|mX??%ZIFOScjnI=KW1&v3^t)wziKZuz&1VJj~ zQ7^?{ohsBtKJSu!R>tiN$jMjEJ&xIW9DoH4{o&83uW;!8_LME z)22C5>0*D@M5dE9Ef9;9x(3=LS>6VF@KNYlB6YB{=w{TGnqnHr7)9(Al z)O14;>^=3S_gm`c%I=x4Tb#dkt2iM%Oia(sE)h58cdh|q*Z!rtE4c^DulPsof$zz~ zUw(;?3-af8H?5||)gPhZX7yh8_1UA7aN*LO*U#5yK5`e&uHVkTJh|$wEq)yopWeOv nc4KI0?gO>Fuz7wd^od#_e(yUn-_ktU?!D4PJSp7o8NTobP4!c- literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..d4b9851ad1e511784eca37ee2599315a0895c3d2 GIT binary patch literal 1290 zcmaJ>ZA=?w96wS(UQ*L!0db3R9wvg)d+FV^y@N`*H>tBgN@X?CWJm9To^W^N?pO=P zjZHLNG-f6X`=qf;HisY7XiSD?VMdm0pG0L}<`NT(x@9H~#rP7l`V=Vo0e8vW^Y;J! zf4}Gda>obzpYS&9Z9ovj8|#x2aBM2Sb+z!jaNn7EINa~bDR;;kb#tl>kY3Fi0cgxn z$3Oz8+VtcV(2XD-O;4uWRQxGEZ5g;)w!!m87NQZPyCMa zCnRN?3$7wq#&xqiLF95dJV)b}Jw{L*#}QIJ{?Lfhh5qULBP&i2#M)_Ww){dJ1|H|0a+DT4l0g(WX zHD#w^K1SOrU>Li*6O|328h*gmVN%oy$x2Tdz;t6$1cN8IrfWQ{P~o5?D6$X|WRjx7 zq$CR>Em2A^NYesU@v$2=#0m_{2^6K!97V}&I7~B2nBszLfKf!6sbFKK6x%Y;&FJ#_ zF6^`*sH^uDSN9O#G`+Fb7bw}4En<>H^I?QUT=D}F^XS6mW68JgXY48B3|ohW@^ySf16FU zynCb3S9<8+jYl$be|grs&^rb5T+`~yr?urj?e-U&=CwP5-WBjlwZDJqy;>Bm^&FpQ zZ}Z(se73Tm6ni_ORfRL{jPK-dE7|Lz98?)(xE4U-~H87ON>->R@iOzHev0Kex29aYZT?iMR2ZeQs?*E?oX@?&34(;d)2! u^3me2U5i(d_7X8W*`h!F=HLTJbqiwDf8BZVtg>4EJH_OF={@1#v3~&i>aHRH literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..3599333cd2c5e0725426184547e6573fcc56707a GIT binary patch literal 1268 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE;=WQl7;NpOBzNqJ&XDuZK6ep0G} zXKrG8YEWuoN@d~6R2v2cW{u2{h>{3jAFJg2T)jk)8oi3#0-$aN1{?c|g2d$P)DnfH z)bz|eTc!8A_bVx6rr0WloBA5~7C5J7WO`H;r3P2|g(O#HCtIc{+1n}DR9FEG$W1Lt zRH(?!$t$+1uvG$^YXxM3g!Ppaz)DK8ZIvL7itr6kaLzAERWQ{v(@i!oGgmOT)H5_S zH#afYQ7|$vG}SjS);BiLH8Qp`F|aZ*Qh)*_plwAdX;wilZcw{`JX@uVl9B=|ef{$C za=mh6z5JqdeM3u2OML?)eIp}XpbFjM%Dj@q3f;V7Wta&rsl~}fnFS@8`FRQ;6BCp2 zOG|8(fG&l2A-4c-Y+f-mn1BJMUy)d#Z>VPg@)As;uP=V3xw&xF#U(+h2=`(&xHzP; zAXPsowK%`DC>a=WY04n03ap%qQWHz^i$e1Ab6}wukda@KU!0L&py2Eb4mAZ0-^Aq1 zJdmI!SeLJ_m1kaYNn&1ds;7&s63`sI%*+%kXG3R4H**(9Ll+k(Lqk_n3kzctHw!~c zGgBiIH)mrLm|mCsATTyF!Yo_k^J8U3uEcY|pTw}&!7YXIk#LiRR22Vw;J~EUzo(XK*!)Bb7m=*cMc;ggS ziS8pVhtfZ?#x@H}cTTRG^mO;{l=WWPpy&>k^ZSj~E^ug; zIU~LQtmNvYZ6eZ*Gd&`jh2}I>e_%Riv0bdt_*oujmSR{l(~>W;P3|AK)aH0g26G4M zvKA|vy`A!~L3>M6DxW-=>{bOOWq55a{nSd-e^foQUQdf%2!< zW=z~YZ^n#&y@7SV1s<2jZ+KW5l=M1wH^cP@Y;(RJQLzsfKD}ngolWW1ic_wv=+Vfy zZCAf_%Fg@k!WVAXZIiNo@{0H2o~nJv54L_Ul&IS0cO%{Kxa0Zj`)xN^ec-+!8h;?` e1@{l31O~lV!Kpvb{#gSmhdf>VT-G@yGywnwA-{M4 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..4ad12a2ae05e46784f21b23bd2af7d18ee412800 GIT binary patch literal 1021 zcmaJ=&ui0A9M9Mob5I2pQ4jh|2Trj0*|beqon6wpf|;YMSoG2~d0S(byqLVL*)DV9 z!70lA0|gHr1o0?%5Mc;{XcbSMMDQm5xa}a!m#(cIHUls3y)WO-=ll8ocx%&B=VHT0 zhbfAR6(_Y4+4n|kXpsDmPbS`z?Fi0S@C=&8O~Z#&&O$W^ijFY{OVF^EuDyn16xBa! zmn*oUpI1!eFh=CVgpNnh6g4&;dWPA67}VgL?Plp8&tK5MwzBjkN#}G=g>`#!*@rXB zQ)P3xVagUgeg=$%3L$VHHbCgiyMYpB={B!I&XJj=LE8m4vh=p7iarfgu!4{v9*JPd#YUKLgLs!ggMsPW9=4GSB1WTz7I2m(O1D#RydJF^bjm~u z#)gK+@(dTH)OFPL|GPR)&pW^+_*d_a#6fw%Nq zD~F$In_F8eUsgWk==d$GTCG|j_WR_Oc>l+%gCpOl4RLj|@8G+ew@*BN*%~T&gLm$~ zePF+8We!}p{$}IRufAh<_pM6;zrLp>ecyO^=;FQ0t18B=@vl#*wGnSW9UJK%I82=# VSbPR+uUDf7S>?5hW46K32*3xq68pHF_1f1wh>l3^w)^1&PVosU-?Y zsp*+{wo31J?^jaDOtDo8H}y5}EpSfF$n>ZxN)4{^3rViZPPR-@vbR&PsjvbXkegbP zs8ErclUHn2VXFi-*9yo63F|8hm3bwJ6}oxF$}kgLQj3#|G7CyF^YauyCMG83 zmzLNn0bL65LT&-v*t}wBFaZNhzap_f-%!s0*lEl2^R8JRMC7?NanVBh8&W6s8ZssmVhAu8nhK8=D78b@PZWe}? zW~N3aZqCLgFugAM$)&lec_lEtDG0sBIQ4=OL~a4lW|!2W%(B!Jx1#)91+d4hGO@VD zz|Gjz$j!tJXr3i@w?Oo!U~vmXuc-k}z4}1M=!2pcDY{`oz%&G6!V@o$15f^`dB8MZ z1WerL4LUY5Ffgikx;TbZ+^V^_(Tmwp;MhmouWJ@{C@u@oo~Y3wz#;VJiqpYU$IJz! z9pw+23kZrFJg4Ql%ErOU*G1s%mViL+WNGQy(f?Q9KP6Qp`Qz@vcW3UNTeI=nED=Mq zxA`)q@Azl@H|$|b`x3WMokQ1V_LmdM z+`$%Qt@Ce59LnfFWGU*m=-TneobM&B?>!pZmGsv40ki(QH3+?YfC5fl2pUXO@geCwwwxfms delta 291 zcmV+;0o?w!3BUp&iBL{Q4GJ0x0000DNk~Le0000C0000E2nGNE02{si?2#cSe*of1 zL_t(|+G6`900^ef%yA>RIn6?IgvGR zLlxo!#xM;alMf?FyanRfK>QKP2hk?TPI5$2ya{GC5Pt>YJwVKd?&(!f#UKT|NRl#8 z+ff3Ag@J)Vp8*1n0`X@C1bBkPd1eG+HX#1X1SEby1^F0ogGEqvhe6g%MDl|wmUvM> zl3xT8P(c!S0mS-1%nZfaK>P$rUJGVQHCmXzfMR6OfgEw5s4u`1Y2~ow0Va@u1P})U pu{{v~2I3PyyatG$!xR7n7yuU1)Ar(Lf~Eif002ovPDHLkV1nR$Y=Qs) diff --git a/web/app/assets/images/content/icon_settings_sm.png b/web/app/assets/images/content/icon_settings_sm.png index 6f43ad3769b7fdfec91d92d2369a072cf623da08..b686ebecce8dd55ebf183df61bf70081d2f5919e 100644 GIT binary patch delta 493 zcmeC>T)?T=8Q|y6%O%Cdz`(%k>ERLtr1?OYgAGW^d+1)AsJLC#OgGuU%v{0TQqR!T z+}y-mN5ROz&{W^RSl`${*T~q)#K6kLXyV_J$+H+!CTB5fPR?PHP;fGKF*7x?FgJ8@ zwlFkwHMOuXHgU5sv;+#8xH%h}Ox9tNfhjV>spuD@@#LjUW|RGyq$hu4Qk>k)BsJNP z*|C0pTJmcK21X%I7srr_TXQZMdOHP594lNOd-Q~uY>q%-_w8jb6n5_55c(>+i2uO~ z=ji)OGP(q0?neJ+c=SX}Niay9edpi!%2k&oWojO>KaV^AyXago?<5t^x51h3Egj-s z>$OFt3%<+JNocc*+Tqe7c@0 z?PFh4@XT!1thxpW+X;oBz>+fgCn_sfkd1<{@U-9`n-fw>=*K->%uwUA(pLgxhSzrt@ Nc)I$ztaD0e0strZx!3>z delta 421 zcmV;W0b2f`35^LMiBL{Q4GJ0x0000DNk~Le0000C0000C2nGNE09JKe=aC_|B{MB! zFflP8Ha0FXG&MFhF)Sc5FflSOFfuPNHZ3tUIyE*rGB=U^bd%fxB9m$XYm;^YF9;xb zZE#IZI!Tkk0yUHT0UDDK10j>80yY6LliUJ30Wy=^0vVGr13`b(T$)V)00935b)&(RX&qW(9FhLEk7~&QKG~*3cxr8FV z@Fx~aY;dj9bGCos9CLi)M~VNKX}v%XE8K;4s`_B@fr?t}`hBFX!CvnbI@qhwWSi5~}G1Iz%aj0~o3tBuQk1SeItB;{2oAbA(G1pIPqmQiyFvSR$ zq0g2^*Yk&AMe1naG=8|EPD!=#7f%{S{n$(A^s*UE?oA!v%Qy*nI{^j&;>F0CoUDQ~ P00000NkvXXu0mjfONgy1 diff --git a/web/app/assets/images/content/icon_share.png b/web/app/assets/images/content/icon_share.png index 5c73cce88dff465c5d22a9f59334ac937357fedb..9f2ba73a7a1ecdfaf53883a6c49241bef1c9f8c4 100644 GIT binary patch delta 474 zcmaFJ(aWjW8Q|y6%O%Cdz`(%k>ERLtr1?OYgAGW^d+1)AsJLC#OgGuU%v{0TQqR!T z+}y-mN5ROz&{W^RSl`${*T~q)#K6kLXyV_J$+H+!CTB5fPR?PHP;hfLH*zy_GB9*; zF*7uDHMOuXHgU5sv@|m{GI4V@Hkqu$Bm-6C=!R3#FGl0ZOPS0j`!h*T{>G#@xtmF9 zvLUl${iNN$cK}`Z-_yl0q~g|;OZ&Z?3PlclT&%$-IHP64rn%nE*RLBnhZd)_1F zFATY7S?!u`TzDirL$`@T_f?TB^FlG+7?qztZ7UDdKiw<)DKiBL{Q4GJ0x0000DNk~Le0000D0000F2nGNE06!x|cab5tB{MB! zFflP8Ha0FXG&MFhF)Sc5FflSOFfuPNHZ3tUIyE*rGB=U^bd%fxB9m$XYm;^YF9;xb zZE#IZI!Tkk0yUHT0UDDK10j>80yY6PliUJ30XCD|0vVGr13`Z~XnaTj007fTL_t(| z+KkdW4uU`sfZ;(XETAy4w$Z}c&O=z6*h-+%*2Yk92G3wjJb~Q9&I*muLgNkAAF>5( zV8bLI;WZ318Bt34h>#=H*J4u~VpWg0OVYzgdc(0L^+Y6NS3|oAV zVTz5O^wB9B6Img_87@XR;euZ=WOL-9(dZQuc(#qmjigtXH8LwjU6OpA*m=wU0t^7| WyucxUK5Eba0000{3jAFJg2T)jk)8oi3#0-$aN1{?c|g2d$P)DnfH z)bz|eTc!8A_bVx6rr0WloBA5~7C5J7WO`H;r3P2|g(O#HCtIc{+1n}DR9FEG$W1Lt zRH(?!$t$+1uvG$^YXxM3g!Ppaz)DK8ZIvL7itr6kaLzAERWQ{v(@i!oGgmOT)H5_S zH#afYQ7|$vG}SjS);BiLH8Qp`F|aZ*Qh)*_plwAdX;wilZcw{`JX@uVl9B=|ef{$C za=mh6z5JqdeM3u2OML?)eIp}XpbFjM%Dj@q3f;V7Wta&rsl~}fnFS@8`FRQ;6BCp2 zOG|8(fG&l2A-4c-Y+f-mn1BJMUy)d#Z>VPg@)As;uP=V3xw&xF#U(+h2=`(&xHzP; zAXPsowK%`DC>a=WY04n03ap%qQWHz^i$e1Ab6}wukda@KU!0L&py2Eb4mAZ0-^Aq1 zJdmI!SeLJ_m1kaYNn&1ds;7&s63`sI%*+%kM>7jYb0cFHLl;*QLqk_n3kzctHw!~c zGgBiIH)mrLm|mCsATTyF_jDqjNYCujv*Dd&P?(3Vs;d0wVrHf*d%n9<>b9q?eKYy0r?MDk9_thi*va1 zRA85gtYNxY3fwNXPXpy_G zfTXPl^POgvI`O($zLlaSA@PkR3;X(>rCh(H(fs!F8O65aRTXI`vL76H(dw#Xb2Vz- z`PI1xkG!F1zXxJo`#V%+a9Gmid$NJZ@&oE?1SQYlt-vY*&xE z5`KG&tL>-c`13(-FI(I5&$TaDKUe`bc!9_G9(@F?D$v%Kx5bm7H)n)_^}|MWA?Ub(qv4QItkJUZ{(XE3-5k_=N=n$Sf1RgpV1SLV)o7G)Av<G z>I%qAR|G_26dR62xX4iuWY{pxgy;|x2!?o$;kh8#`bZqjQil1Qklu=gZwYb~A(N-+ zVzEdSLzH2S&}=LgqnRKb3VEt%<}*Sv z?<^PAH#Ro>K>v9vPCQAx^%Pj}`YibUrgZ2lF~Qv1{qcqO+RKOY{*@2)xyQum9l3>B zZ|D5Z#mt_?D;>`db3gXV?FUNZ=RAwoJ3e`G3(fnYYo#0ed!Bk1AIz+`HTgilGqcV& z_cQEVW2uYkvAWqy^-GPN4Xtx`S8uQM%`JZcH;X05=WR(|Sj*n|e2bXydS_c7efRW~ Z>WCx0U+mSoci-H%oar49o*h4b`48$USZ)9S literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..339f21fd60175a46f515e666f9e0da8ebd62306c GIT binary patch literal 1018 zcmaJ=J#W)M7`9qKP!XgGhQd%zE}$ZazhWn`RnxR~(psvkD2+tMI`*ZpTKkNBZQKrg zlrBgh1TzbN05KpjFaS~|io`$}pknP0fFFP;=QK$f2$s%w_v(F~_v^U_<@uS^(1jt2 zqEg1JULpILWSu-g{ugJ{yJS0$i#5D}8n|VJkeYH(9RkC%mS6>1&ibv7aDt)+$K7fT z*UUM^Mjm4&K1}Qd1Wi#BlW}0#%MgP)TylMt{_*B54O~a1Z%QU-1{!R-vl}5?*qE={ z8_Txr(33a7M63`34`K_%-ijY7u}Zgj6>?6@EDhQ&cv+?`%mtiwpTF59Or-%aRd^Cv`It`_wmpZY?O{JR{2MWu!S}mrPVNkfl z^0F+moWKe~ns}t6bst-C+K(l9o* z0+wgEq@=E+Y5w2U^LpM9uE3*w|4AHG*8|8_V1!mfn+&comV^ovErb?EVHKg3ZWqf< zgi+K)0noH|*TA@G*{+`$ueMR9sTh8QE#HQQuF`~uaa~7|wW2sB6-8dx^E_Xa^7)Kd z%JXto62y|05xZC&*{dG(aTn_xVWqyguZ+otkXZ2 zOkXTZl3|na{x$BdiD)P}_QXprJ^4eQXg(z3R;@*!JdCEHPgUdZ^3u*>~GN-NM(M$NshbI|I+}pUc0zJFxp` z?zFRcZ+EY-rQCkFRWNY*85Ovt*TbJ0mvT=AkBv}Yesjak>G(tP8ja$-{$hG@^A9$K BKPCVG literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..a99ca1b98d34bd09e86f0fa49494e91247fded07 GIT binary patch literal 1279 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqE;=WQl7;NpOBzNqJ&XDuZK6ep0G} zXKrG8YEWuoN@d~6R2v2cW{u2{h>{3jAFJg2T)jk)8oi3#0-$aN1{?c|g2d$P)DnfH z)bz|eTc!8A_bVx6rr0WloBA5~7C5J7WO`H;r3P2|g(O#HCtIc{+1n}DR9FEG$W1Lt zRH(?!$t$+1uvG$^YXxM3g!Ppaz)DK8ZIvL7itr6kaLzAERWQ{v(@i!oGgmOT)H5_S zH#afYQ7|$vG}SjS);BiLH8Qp`F|aZ*Qh)*_plwAdX;wilZcw{`JX@uVl9B=|ef{$C za=mh6z5JqdeM3u2OML?)eIp}XpbFjM%Dj@q3f;V7Wta&rsl~}fnFS@8`FRQ;6BCp2 zOG|8(fG&l2A-4c-Y+f-mn1BJMUy)d#Z>VPg@)As;uP=V3xw&xF#U(+h2=`(&xHzP; zAXPsowK%`DC>a=WY04n03ap%qQWHz^i$e1Ab6}wukda@KU!0L&py2Eb4mAZ0-^Aq1 zJdmI!SeLJ_m1kaYNn&1ds;7&s63`sI%*+%kXG3R4H**&=Ll+k(Lqk_n3kzctHw!~c zGgBiIH)mrLm|mCsATTy0=9j_C?Rn4l8-fvDcCFd10>9V!MYTzXY8FuN%i5 zno)G2w>0GJ`wv3j9?F<|yqbIB*5;bdm<`RgIgihn^#790dvRx9-)F11ZN6W2txO2H zuO4u0#-`F5{nX&!?mKH2b?E6la-5WSD|2C+erdXeKs-#J=i8@m}} z9=hH$x31#e!L9KoGM6Lh`0icvc(eI3r#Q_zJZatB7w4RBhpQL;68^DuJ(J(Wu1ovY r$Zy~A%J_X%?&;+1zx5w@H!w2f+~0ZY;KQHNpd!iB)z4*}Q$iB}IeE}L literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..0ffc3f32cfc7a4e144fe6897b3afc5e885e37e6c GIT binary patch literal 17603 zcmXtA18^kY*PYn5?QCq@*2cEAv2Aa#(Iy+)wr$(CCeA;ulUMPQ|-=_=}AXJ=;b`h5xjh&r1Y zxtf_0yIZ+h5=%+~70BJE{HIKE+POB7+#aRxYMfv#{78nsY(&UB-ma!7j zh!PlyeH%IBFwX1b0m~z9+*s+H0Y2-a<@9taW{KD#lDJ+^ArKI=Bw?WhVIiR+NUy({ zTaO=H#}|6)-rGVskCp#@8t6c-1b{eCtL<)!>Ob|wkp**KKlCa=)WBLmds+bsP%|K3 zpD)1aZvRigHzUXh0Hly4@6@NjEyU3U8D&6vJyK-P3l{Y1cIm}nodG(~^qQGBNLX%_ zeZbDq4aVEB^~E{NX_XWBVFOJ5hfU$B zD?k@<-+cr`tBPiF>M?}*+*_!yi`v906Th3Mc zC-$y&MNvAne!u`=G+3f#DyYb(?u6*Ci|v3My-YEs3}4d%8Ex`#BD~3XJZ)tm^O9kI zl?~9?RwT&O=ax(9_HIR0nKnx8Wj?*JfGOd5;~qrH)}s ze;7Yzk>8rqPZ9mF{1v!RK87jfV=tztyaA0O%DC*)r)q4S>Mb<))5r*U`MQj9C`L4^TqdkpaYO0JstZ!`~wk zawNB?oB$)eFQaF5!2~@A3yAm}mT3f#%&31a@S#~lLTyHzkw*iy_)NP-^aOA;z4-Cn z*d?CY_qw#OX{q!{djs!BMM5*jU&`saa{UuO_{rqxr>-fw^{6WFUILg@}0Y*PHQVjF$l=*^K>sW!}5SU2#G zkz8;6M*9*Ab2OnplG&!vt2mHI?kPk19Nt} z4X{Z}#!h@lbVq&3fO2&VdzCp@s@N8)Xof_Y$N16IY`HJVf1(Z9pD9=zJ|Xam0J}mPTSgvj%`qwylOKJvKL{_2-@i2Hy6>^#N$| zNcT07@Rbrihk@jcQN9UtDYYR6y)hO;SQ1(uF$Q4}JPN%SmVP?gJp9~wqJm^CcFA*l zRysW~e@Zpgs%M2xoFRm5VFuFwpqnM~AKO%b>G<%VY(VJ63 z`EjI1je)^Yml@mv$I8iIJ6xQL@bBqlU6@Hg#c+*e4(u785)!86N>-t=E4v?~uKyt_3V0Gq(iG}*>D|o64{87myb23iE z>HjT~{=p9G&^NjPEorZ;sUcoK?4-Q3o73y*+b4>cKfYAyGDKyv3z7Vg-ze-v-)COu zXKHV%w=0XD>2s56I{4+Mh*6awiN^K zu=2Nc%=&Co27j7VK|LqKRZ`1>T`K+9(Vx?uZ&+OmnbTB)XQKo-+t&9gB{E9mN*YSAC=J ze50k6&sm|1^;@#hCdcTOL81?Zl>{A?B$Jyn4aTGb&4%Bgz=e8@o=)zSay-{#F zqCPHbB;?#k$)mfDMtYhLTf2@|EX7sY$3W9?DJRgYng#5Kpwf7tSx92#F|5Z;++-j~ zL98Q9hF_YM_7PWkk5_G>KBaWRPg9serxcoB46G4js(W14NdgqVVJhpq|1MO>b@Oou zRZsHNi9-y$WZmMU7vQ_eP{PI4HPk9u%7tK#^Fc^z33PICU!*v1wjlQH#$_Iafl>uQ6Nnu717TnHb!epgf9F` zgUmrzzCjbbNdr_2@i61FZE{8Em4r!@VC#E*1#P;GWMD5I^&l?gr+Nlg^bjCQENe$d^hBtxKzLf#Y-gC${btMKnFR{JTPf0 zVkoHT++6Oe$m4KTe(z@u&;T3c>vxn2v%og zp4o2Fqk1$nO`?-Jd$O3+%*hOg%^V>K4%Zt08Z$t}b|yLjCZ-tfaBmfQnRjQV`wbJp z7xFyy<}_;+GOaG+q8-;xp(eWbJ8d}rQcKHbnaxDVc@)>MBDOH|qD&>_p>Q$h!s=tt z>8ANij3)-FZgmV#fc_Gh0^83%01oZ?peHfnIIH*&<4532lo}34RSbitn0l#ezNhae zLGKkpJmiQ>@sFCQ=OUt$pgT4BLzkRg<+D)q*SEbw@}In(oM&-188?+F&hvW z#?kJ{XZP}?9EeOw02r+hcXZ;)3RjD!Xi9R{wEG{CzERxyRFuZ3!6Qkv>Pt~c69px; zw7p@;Au5Q>10|#|u5*qP=pCq(&^U~nhcH1G0=5p z=fW@&a9MZYA{3|=2sAr&F*%E6kaY>gNlj%bdR3huRMVkqp({g*X-_D}W=fb?Yg$)f zs+Uxy;b~Nrgc2M$lGIMcUP2yf3Th8eDC&N3?jf)b-l;Q+hP^_Vh+2%Ze1(`qNy7>! zYog)hvz{(=xU+`<@%S)V)G$ge*z-*jZYv6k8EWJ*GE&SZRjB*^TH7vCzN}L6Wp()1 z!^rqB`gcVEZ0i(0U>!m!!bn8xz-bKH1J@9f-P!bXtNwzr;Bmkp(Fz`i%)jn-tHohRt$W$c~yA0;}PmNfb&`-zVLcK)8#?{bf3-z&zPpM`@`Rz+)I|ZorDasI3HXe zPTlgmr!ueY`voz7^m?;HeH{3nl&=B>-_HdlS{5o2`)^C@hfIb)aW|fCl0CkZDz`SV zJ)X;R@3(=0C0sjs)#%;bVBpXGM3=|?-ECmhF1Gb9TpJ4^z$c;}xbmf)3=D@*Z|)qI zuh+PUP9?tnTR*G;u5vGcIU-SzYMU!0z;>+@N|8Y;Fcgo&s(Kzn`j2C0V)0WSlmvok zcXC_4of!q<0;l2P67Z&{Z*Zlndx$Z0&r{&NoL3}5JSLC7+;J91l)2`NyrJAS^yAkr z&9^NEzPlk7+pW%JgL025!Rd^6sU*fUt!>y3k3YdjroZv2GW zE#Q+3A?8UP@F>DU##X3R#MJQVWGy-B9Ltd`G{0MnQi+4|&on7DLmx0z*gStrNA_`{ z9p%9{i?_L>9#Hlcu`_d+{v`&eQtr&F4w)>K?4D7$@#pauAyy4NgZMgq1S=ZuzqOO~ zcHWjjOj&P>xI6&a@@WE z3E*Yy_2-AO?Pj~6Lcn3>@Ncx?y}4SlMWEu@3JBG&i-6h>#ld)@jM(O;YbD0he`rI7 zz3bP8z&!25mpx0Drv+9P97V*@#2{4ENe_kYa*p4>xf;(STddc#009;n9TcRn+0U)k zuUcH*GeTb>LjZSe>pCtaaI(sXtq7&3x=J z=2h}7=|X7J-p`QAVlES_{R9N3uk$|aB!0zTwb*V@v+;(%8F+&+R%nBn?`-sf-Ofgu zI1N6}YPOx~Rcya>4G-~22S5|y2topG;+NC*O3lh^s=E-W+%F=^i*iGI`O9x^BTa7W zsdCiudqKSsGmpnROiJbaEnB{9+} z#1A54!)Ap}C;cEALNZ8m2dgmr}oW_%< zjSkz_9f2F6j`*ry1Ul9Nk_T8*-JB+xlxH_1c;imf8{#YKF31S-k~OCD8XI@h$F+N# zb}impB42!K!NxJC^LPdV{G~Lnek0?X-cd`Ks@6E-$lzZqMJ9g!RGs0FW%f z4(x3+564;5i^|IWt~MUN_g5CaMjs5BLR1E9W^hFK<39W!SN~++ng9WwHk^jbdD?6Q zPAeT2ME2fKW`Ldqef8B3US5r!1>s^4z~}3k$bVqj;lG27n;Ew=>E671Twm|*y-WwU z;0+OgrBT^?($&e_+SYv>&n?RB^uqfKCMOzvaCbRr-ep=1t;MJ7NJU)i#vX*ExL*`hJjfcQZQM!eG!`GriB=HE+Xl9hZ z4%lKHOZ|7!?`vbM`KDL7pZVl~P(YMQ&uuuyEVFrU0>t;$fT$B3ZpmR_ynNZ{w&=%7 zn}Z<3=gTE!JGEWty^Ce!dOAdi*A?6ewS8u8`W21cQ+g4246Vpt$63-` z4kyG9S8EUmv9T>5p+B83XiS&u!BJ^;wvGJ=|B#IT2rlcs|INW6`iI^V6@1#1#M8}3 zTy0gKTp(*414JxU$aGVx;7Jel4VwO-qzO(tFfgxtyjS}!D3s)MgJGT9eF6U_S{-zZ z-h&b|cutp9L0##Upt)sg>YYc6CRCG7B~2Q>C*W$+@vuCHIK!OZVYN^k zsLt`^J`yN>akw+G?c<8#ME2|*?n?AC05WP;#H5+M;g>k{yoUo^N%g5fYuH8JF@G#+d5np<&3$sV&#-hj^Z-0K-{DThO$`8aL`1dhEZ2A=y{y$ z^E!*u5k}8QRlCkoySzdZ@h1ug5iK~?xQ^QMF8!VY8(bukTzpO(R(OJWeZ_p@fEsyN zODf_#q=JaTiA{&@Em)SZ?`YHeCc0R^hf%-}$2<-@=sPws|Lt6gq&1+*2`h>RTJr8* zPA49jZ*YG7>S*yIa3iXCyx%cshtosXpOJF4wCWKgO_5pkC7`j!hvUo)cpZ|!AX+1F zhW`(8eqQ?_{|C=my^Gl2Awt1&9v3T{8GRn|=YCjtZqL~z@54Pd&Il^>E}Ql|Kbft^P%@K7XUcTn39rR{1x780X&6eowBw9FV2*rGbsZgHAHOuxM3&HGq0lkW+!tL-}E_k@g| z>eO3&-Yq-oZ8(oQ9^IO}D)e>4q*8i>WNc^;?Ofg^#Xf4q&P>G>cZId`f6yVN=klkIh*6uQa&f{EE5CeOm~fT3TormxjAFLqyKDWKg`Ke9$3@mdUb?fmM))jZCxIx z&}F(2e%~*JvzhONYw@HGPG^P2kK=MH7?0?a3XzCA=zGNl*i7+4{glrPa`?J53Xn-G zmX@KTF+dLskp|?vAF)dlKj!QBr_sBGD)>ITG}~(h)>~po|;I5a*zpCo;Zl^#BTb7k@+Q%y$rWwVua)JHtgsT;m2hJ}oVI zer7Hd-$*0`{9LLU&T2eYUd@LXdg8?(!e__tS2&HF&S8ZyXa|y*QNvA`8Q2k&(emcY zzPYVvxy)d$Q;8VMtGsXCR~w35=REIu9`Bs-eiiv&vw30yaO3fXGsni@*_mlpqA&}6 z>K!FanY^26`A`d~<=VDNZWaZ6m%tmWS5XDMOgU2RR-=GO_hlasoE3oQ`zTUU+&ylAyL?-umLoXu0t+S0#m)SY?N)O8&=l7@o zu5xEB?UVcbI#NTb84JjUIDzvt_*_wvK&i^45+YTcdMN;~)_ARZ0|Kl>6|k=Pc#k$n z76AdxO_$I+-Q8|a*I%C(!?y>V)e_feyumnJJ_R@~%bmeQpZmFhfGxfDgXO)GP4fp3 zL)|ZdPd#0aal01cuUuDqy*|Us>hfU%#wk(tU zgFl_Qa3>rv>f)^$G%-j1 zPM?AM7$*0bo!tgAOwWgQ0)>v0XBwrMv##3@w`k_?|2nydljpo4<>T6?nYB{&LtCSv zXQiw2HXCWG=Vq((W){Ze?wtFcUcpeGzRsI+9!z8TWhJDRtJYUX?fcU7?@RSO3HoKN zZ3R#<8^&u;kMRZg%|O>0tt0#PticeITT#NcnH28~#I*RnQnP-P8-`Y6P>=Q@WI8UF zSAzg3eP{J-|iLgSC9zS8s;bxNo94^2u2VF@2}r27)%fUW=G{mO|FOZLD#Z+vSDU; z+2LWYw)#e3VYzBM$p7q81%JT#8^EPrYh}bV7>g3>y?b4Nvlz{3-+N(3=d_C>>U3D( z)S_}>2&n09nzEof7_{&||5JeH-#ghITW1P?axO<0PDgcqelcFXPx>baaIjnRy$C(b z2X$Hu2avz-3;6p}D>tN6EbS(^ZgQxiaysdi`%`n5CMMM%&wB^;nClc+(EY$rS>~ea zQtYjEtf~w@j#P)OFi`}AVf^~NjWWYj_-^sQ`Kg6=?uxZGFktr+PgOb-X0D(+tbqR& z`mfKoqwcuH_3d%iWCf#N6Fmk7S^<$@uSP=g$v2sbzG76US^b+XSg?)C*@l|6cD$b) z4k!W~R=Rg<-iU;KzSW(1@P568<1%_IDZu6FjtD`YBa;%#SyCaq(AVG@kZzkR(3p#P zxXyOBUU{MyB~oc8u75*Z!Hh=i#U*_8!mDwQ8cw9EXSMe&xwg6iw8)c@k-8b&O8Wm`BY6_2VSj7`CdpuwLOm z7w83?^LOUY)zX7#Y025)rK3~QSu)dAv52uiZylYGBDeYOQMcAU3ID5^2ng=c&*0_9(J5B#H|De$ie;SRb79jkx8d)&i%4BeyBrzK zS$#0M3?NVFchOQ2(F%u&S+AZE{g(Y85-hl)P#RFUH&7Ia{_kF#%ykiTaqZh~JUp`w z!}OH978$=uT1$6rX|x}^chv##+^&Yghhl`f?e#!FIj+`XC9xQXv)Iou?hS$)Vm=-b zp4U2&c9WBb_hVGO^F%Y3N_aOSn%%GZg5lB(Pme04X);Me{gyXEHJi-n1h}lZ6sphS z5bd;0-ac{px}6bnH0waaU3HDv>qaRfOZ54ujuMH_qwDHiT4;Q|D7OE|F_b# zQ?w1=x<39xenOo)$|6ME2-)G7p33^%Wr%5pll)Oq^duWTtaTGcxwN!u=2ISFRriwU z_)th_9c{Ob({S_mi)DJnfq|&%z95pZ8J_9LWvs(eXpw9*Cfs=m-@(-V@jeNf%6{$b z@Q#{p`xIf*XcjijpBnv1zpxM@vCF)K^J-ui5oF6ksF+Sg&uLbjWH>XSY4e);Bhn&3 zsFdVJOJ~CZrOK!%?1fB1hdfD;F75El_rN-*Xr zN#4tV$dXz$$??4|Qy~fcuVD-YiB}iyh`O^4OTjaASK?FYxbXP2P_I7~yzq@0JOhm- zN@`^zBKcF$O*FIqR8kodSusA4(nWf(UF2!GYcg33B)ysiQ>s3w2F?3(bkes5n!lb# zQ;@00IJ}VbaD6D=Q_l}%cRi#wY4SV=A}h+ds!gciTL|sA<0_!TM1IJI`D8sAZEdby4zad$;zc8w-)BuSpUSeB2=ETVJ)E^R@P-3d7u1UlHC(CWIj7|s63 zhT6Z*F!~BkR9yB3W>aZwaw#f&jn>h%ulcm_Kba#EOCV4S$s?j1O!^8^9n96i>0Gz9 z>We?J=mYe&`=lGR?+^7sa-egH<<{%1Cksds=5L5Y3h|e~1{<^Jy%1iHdG~JFvwtVsbyTe zC-O%0PCEv*$ez})oyLZrq#fJV3?#vNCP9fd7Fc7-3%hAm*x_N}(TUF+Gn1)7e^|Vz zYL+X$&LNYdS=w^kqE0*EtPnni`HviPMWIZqNG_s`E1AEyEkGtej#F8%ADnVb*(Eq2 z!^svO3_2@%OIm#P9S#H8YaUnD*^st03R$DQ6b23z3<9Lc0!|*fNvmIiCt9QBRBcS7 zJQ*#Q`?`fx>{k9*f6zpUb?(8$g@!L=5$=J9yU921hHKn}?}C+ug0Vx=YuBZtBBJsS z37s$_QRFjIp+~JkK?h0{MIpPBf{`A}=#eNzMn?UQwcWe>_NNr#eEis*#6d-x6&gx>_YY7d4}va8&*SrtZ3lng(37@>z8Ed>z^(@P(dxQp0c zWZd^-boeRfDEjuiAn@{ZVEYSCI>}L0u zwV<^lX;>*Fi0sqBByiDUCqY0UE-W#)om%n!VY){QGvt-6x@YPO*U45Al!LBT$)r95viNCSUWov<_s6S!A3DMF%^H1Eakh^*2=}OHCyW+F&D0znB?S;#E@qdwdI; zur&uIvjKN&a%GJ!Pqz*sS{wpPbc=qNuu631%BfsNve!|qW^484{c!X_;9by)ltZ+{ zMob8(r7qKCQvS|(h%>)aYUN$l+2;j9CDXet0O5y*c?X9PCc$V(3`dYmps4hA{oJN` zttgbdOKf16yyAod>0*x%P`7~P<1mJhp9=L+S!D4k?w@5c!fo@=9NW$WBAor~9W4p7p^$sR z$=bqB#cSyvqtX%)(6J*l3bI@lZAkFq%Z1=@A!I#lpj|Xq9HNq*sJsTFrii_`05_O^ zE3RskkG|Cj?VV)^1)Ct~V)%G1dby?Mq~?7eseE^l)4cIl7L}Ghv=65@6#LFM+Mt3T;lgz_B2-u-L4RZ zft@tpJ=I6U3ZJvnMvMQnvTW|+9UwqF*t?6rM-~;MSl@uXhx?NM+RAv>nyf>=t6uc$ z4-B90V?N)78%1nVnT02-oOj({f<{?7>S{Zq4*Id*3QviH?EsYhN4BA=1*7p5!uE~D z>$9Ez&=J3PXY1vBBivQ%E9=+(t$eIiy^E!#ZQoMo+s(inhKK9z9W-X#^&1Diy|si0 z7{WXx(m;hWnWJ{Rz2$lt#uGu=ARb=xPSsBXG&y^2iDrq``0r9}EaPN)l$|$lSJvOR z=O>yJ>TT`OZ~@U~S(QtL1w%{${@1_UH;?#E2mlkKzhbh#M>!D~m56LoteU(?Z$D9y z``YzzCDdKH4fC}P)Ou}V4%Ps0&sw=kNWe~6sjB1Q z0qbIQ8IQZzH#<*P*(5UP1{!s|Tye1RzIT738Jp75O(uR477tIFc)oHI3_p)NfV4mkZE%u{eOcr1MQ|qvDn9+~naU*O#or#t%S1`icNM{{yON^f9VoFy! zypg(Gm%&ziOEh*SE2Ia9|0tUI{k+Q9{4$&(815+#J&S6*yItKz=4^?6bRmSG8=hBv2cb5A<{dFaQEqxfnpBnI6*RtveF2NJWZOt4huyA=Hv<33G%EBm3&Z=rQRI?d#}B=czLm?N9w7-LP;Wzq1=86{wuI^KDl~C4!BaC!>S@h6mL@m`B;&FsH5&qIrJM_AZ0>5*pNZNX z^>br`lbm3ZXxogy;9ycS`ojSLKj1rEi^Ue{MG1FAzvQ1TD25*#CV%0>10@)I@up>z1SePJmanGsbrBNDvjl3powzvl2Iu*PLp=QKO5Yx3y zUPOE$RTox{j&7*`#2J90TCS>EnV$4IwN}=`Oa1hkdecA#*z2BBztP6~feV-Bb^t5X zgLwN`sj@NOV{r!cx&sn{qgqgJSg<92JG)KYn|74AOGmPHt>L02NDVSs-j_5 zY=`_O?WNjo1g#sSgOSbb4?BIc>zfZ*h5_F8t>H!Yt?+N8-O;J=j-?&z&Yh#nC;mvX zECO(AIIj4fAluXAwCc5M5@;0dLu?KNq7nrCMK2*2IL4oW|NC@fgm29S{}jDSwY?(- z#GO6u9f z&Yfw&_2({Egj;KVy;v2_zNQs12B=2A!e~g7zJ5Ie)&Q)rTX~b+|FLLUj?PTqBlukv zoT3?DaFLtwlQ$^lA2^({BwfZc*;iw`&LYL=zv>}}J#RV0tYftmX?UmeAMro{?(f6D zpKs0TnNA!0sDB5@w*!seV>$Obaz7~jUle{t-I4~2)E8%AuW<*LD{CnSaywM^e`_Qj zPLJAZ8frUP!i(BjRW~79eo#MTDCF>0h%N%8iytMURbcM7{Uod9sjh}yyPPg&=-HAN zT#om3X;IqTPXZitnFs(ie#38NXj9z53UI*9mGn*baacJ5SM3$KbY?azYj5Mz%okpG zf=~_H%{a#QLqGb}26mck)-pwh_XGdQZx_4Id6&PTzg{OxQk>>5nOfZa!S-7M#U7_& zlG-iW5(qC>K9Ftt9M578^IHfSl@rv>qz7DRliqwVIU^?E^3uPesx5aA+NHm1H|Z(! z$aWEOA~B5q62oImZ&QLndWbY~G!OrxCiD7en512QB@`yxhOn);k#eXxT81^p+w$0F?oCmRoZWwlxT98dRz`Uq#<-qzxPhQ;^=Z;Ku1=XW$Dqm!4cQg zo9zcc2);itJHdoP9grL<=6XQ#^ZXj?bdrRRUF)iSJ3F&hgWc6RHQl!@j zq$##|o_}w|yLt36s+D%5HldHQSOp}*`8{VJJTGwI4>{!ej#@C|?381gllhJIZ|Ua~T}u6N;9qYO$#zFJk7&Wvt4-D9Mg@_Lqr#4BJ&=T7^9DE}ZX z9LH9jX1NN>70>;hiPwk_ew%4w5LFN`9-AmAxZ4s(;N9;JVz{udh%+675?sk(K3p2p z7=5=<(Xh%sI8!EClTVpAeW&qkZI_2Z&dpUp-XRK`(!$pqG!Tu*IV!`OAx?`ER-O#< z7sVYXiN2kh`Nm@F#-K^;6Ner|aEIq2@Ov${%ura}`dj4D*GW#T;+4uaxtpV)?qB1(@lM!N(PaWh z-!7uqJBf;MG$o7YgUAMQM&miV+Mg=!8#@P8(HD|b?NkvsGB`kP%zvJ=0 z?;0pivqRd(Xfaad(6)=b&FSW(zc&~Iks#zxMP58RZFS?%BzIRTJ@q-$^PZLEEUgfm ziAO=v^)c8iK_BQYr(V~FD%(gGNQ8lp>P`~aKA8D1#5-UoQ>~Uf9tSiMy$2Y;=vx$i zI9WhZ&*n9{zv}!0Nxw*g(DVK@rT09xiL~c9kMe%?!u`Bek@5T_&U)kj`DXuvEN@LP z(F^m34w+n|$FfOPH;f&f&fdF`=uIqKy+sa>CGoe)g4b0cIn~d+ylq;70NTT`aXS0b z@vmSH{V-PQ?Cxr(h~cMtc={n)rJV{&u_NhK$c}GLKuRH#m9cCDYR;mu3;n`2r5x`(v8v=ryx0Ewq6O zi!-nW_w-J%+52Rst#MdF!E%!ajsTIxT|G?@(Imfmg;2H5#_hSk?NY`%i$88EFwaGiuKHPz$bn=i0)>?U%i?zRI3 zgO1F@g}*I*9~4-$IEQ~q^4nAwP9n1b}o2Y+8RWXvK z)Zp`cTRE4r$HQ_~rK9nZKZdIjOMuH+(=v{@vF{%xf&yE$`D`cFEMF!6Mu;RPW54^= zSGbq?6nB6Vm`0@_BQKmJHgtVo_zF4IH-`|F*UP>JT&z6cCrA_go(Wpsg5sV%As1YN zSnuOT#zeKM0668&G4aFE;!{bf=5;!1m&hr>`w;QxQXWc4ZiLH{i+#$|2!%kQx6?5b z`uWl@@R?BmCvZeSPgncEvHwCi@t%u(-Wl1o-UEx&_lT?uG64~pS|AFQ`Os|BXm7=r z4nDa^Q6rk`YjV9G3xX-UJeXy**V&#MM*)$p7uE& z$6jiu@^U?ixMY7@L}Og5fkB9sx71rjeToE^D zcFz6UL5KXJDngUj$04AgD~ZCHBj}&(eypnwMfu>@A~Nf)_lMeIr)BlrzBF^W!RYek zJwwrdw~!{4z8MINRGBpD6#9D2AkF+=EL}c+PZO$8)qT+pm9m74l${9`(iWuAmZ7#< z$Y9+buMoY!;GG3wEYoB5e2w|2kR^T$!rThhkOjyYBTyL0J`E^t3H`Ia*YPgwG;UI3 z6sz%yG4d>}hP$F~%f4EqwJ@r(>uEsYH}Vo0WJOQs@ZxN@IP!Ek?Ychpxe;(hd|OJ7 z*KM!_qrdLG&Fp4I&i!!J(DD~5SRrbI)W=mAsnz>6M$EYmufhlEcvg}~Mse0a4i}bd?H;cjM`Yzd|hsnOVToqx!AYSv|TPYXQ1T8C2k}avEclr-}_pUYo z8q`yNx}WdOrWWh%>$g?N|Jl%gd$Jql?+w9(e}A|wHlm>58nq)+b_R{zj%rn= zZ{;=cL_kwKk7xc+7<9MDju*${+&BC%@_$dt)zxv+&vn{VT*wkwt0k*`gTd~Z`|4nL z12zgqF4`0lmnlU6WZgUx;m9_9K#V?kJq3R7Ud8`PQq0(viVSQaKsWx3cPcG2k$+q2fLRrgna38oIwjWyv6^QhdHO{VFKHw`tgS0xIpE3ggCk#??4k zWci(I=ogYe9N6RI$v5lQ<+Uklb8y7RME}wMZQ9v8UWg~kjKw4L0+r?LjQ$oz|Eq+g zZhAX?**NTst$zg}@VW=vfvrWrLA{@4VAXavA*RLQM9!CcJrClv1>#g|J))i_6ME45AE7yJR%GCA^By$_bg?I#Wm3lqqhCcbso#-g`xhcIAlY=rGIyfeT}Gmz z@;D=U)tC(cKa))gw(jPnYg0y}v$Zh9Ww{QC{b{@4=G`QmmBg=Cz;pQvg22QDYT?jI zm*#n(Eb^2*#YJcxP|79n?4&Xd>8Jh;?Ci^tq-eBd5sMs!ReUxtJ;tG;%OK)NR1j+4 z%bGj?SKo2Ao_+e6Ub|oGh#3Ftp&90xAk?OU}%xBN4&1HQI zRY=*(Q#?*g7M669qhxw}fL06B+F+>#q6lH#O~;Deix4t{hiaEOh&RhF)VoYP3kGj? zprL+Ri?Rw#{NXh4ZA+4PIIWw*QZj?-#cz{BQfr21boEU)*%KO_p$g(isxYd9&B3SL zT7_K_r+6b(a`6i`{3m(<`{S8f`h<>Q*AfPc?{rY`Amq&vr_-bLaOQwuoW2a??w+GgK_Ic}cqI zaevnIoJ92T^OR>$uSb-IBr}x5h@?~uU~5`yIf-a-o~4uD^hqHO@z6QTk6ej~5G)(c z3LpRIS;fdgF1(vVp73mjtu>LN;0J-^1^>X;tD>EY(q|e!U7+u4^ch2MbxP{&mU?5> zKdn#rZ5CCQ-9elM*bEg4Wnf36nMJRnsXoRk&NB$W?ix%d$)csugC5m^jTYX?DM~}J z(gug{B^$k-<;?mkqY1Nw?wF}mZPNc1IhDr;E$o$oIuD~eFS9_i%XvDeIzTlgE)Ri7 z+CflKly-+vztV~$JdhM?{)?Ex+llM6mo85zj)RN*uX@_d9RsAyT*j>m%z<3XMK}iPQBPk*qo{ z{0IFIQ+i2uW#P$Wkt}-+D7x5dd}u5YsYH8n(Y8tv1Z8h2Ynm~RUGm@jG?cY~HLsc! zdCbd^f<`xTOv{9Z-z+x?%aehWl>3pRFCt!&6mjlQa5RoAr?SE|3R5_*NZZ2iu28gzxWgo93%7Di7>_W|d!q^Q={Zyyj~WR*jHLSUF-hb7 z$S{ZcLWkgCQeFSv5<@mdF+1mPYDEqdedm~S@+OnfMihyE05hnjri&IcW3g*P$SUTU zN*+pqXe8OpvA__w+Rx3+(FU&pwO`LT11ZrJwS27(Hz4Luk5d7%_GNo zg(@Er_7n>W$Dljr%1&pp^+rXYRRiKTN}Kyv8XkSXyUyK!Q>ppNmlVzo7I{l$n5R_m zbo|ld){-P2i7BfMOra1zn2||Pei|dgSUT8(%#bkh!y;dqYR{96jdn*&)PmuyL`%=M z&q&Gs({N8`lVq9<@x`UE++1t>A}_)w59prB)1LLOSdqiGsmx_64Qf778>Z6n`n;k}~uMDIP(g%^l_LveP7sSFp!4sB}r zmg_3~KlnMi-A@~JN+pzzdt*4pOFxF8Ex*ykA!3nnmK|^?$%}%}?6!?RH1&pL*+1## zq2=yV&Rv<+B+K1Wh0B_~E{zJ4U>_FVkqXxZPfukFN!Cw$2Swu1f${(YoRjGaMGT;O zixVfD-2mWC0J9iq2?2Db(~^i3{!$(+du;_^oWGKn0hN@&qrDd0B+Pq73g2cV08ahRAEo_5(&EQCN zdNX|RdH`1dSnBU#z&CC<1I+gV7@Qn1iZkH!uj?N@?$# z?fjPVC4r|z5mFU0W*lYhbQOh6ps41=vJZjTwPjSoHp21xtzjzKBk6TP-jsx7 zi#mmc!!|06vQUymjH4vpjtxJB0Z#SLJ{{fYlqxU($V=w>!002ovPDHLkV1k=F6+{34 literal 0 HcmV?d00001 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 = + `` + 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 + + + `
+ + + {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 @@
-
+
-
+
-
-
-
-
-
+
-
-
-