diff --git a/web/.gitignore b/web/.gitignore new file mode 100644 index 000000000..06d209305 --- /dev/null +++ b/web/.gitignore @@ -0,0 +1,38 @@ +# See http://help.github.com/ignore-files/ for more about ignoring files. +# +# If you find yourself ignoring temporary files generated by your text editor +# or operating system, you probably want to add a global ignore instead: +# git config --global core.excludesfile ~/.gitignore_global + +# Ignore bundler config +/.bundle + +# Ignore the default SQLite database. +/db/*.sqlite3 + +# Ignore all logfiles and tempfiles. +/log/*.log +/log/*.log.age +/tmp +# Ignore other unneeded files. +doc/ +*.swp +*~ +.project +.DS_Store +.idea +*.iml + + +artifacts + +Gemfile.lock +.sass-cache +log/development.log.age +log/test.log.age + +target +vendor/bundle +public/assets +public/uploads +/log/*.out diff --git a/web/.ruby-gemset b/web/.ruby-gemset new file mode 100644 index 000000000..fab2f148f --- /dev/null +++ b/web/.ruby-gemset @@ -0,0 +1 @@ +jamweb diff --git a/web/.ruby-version b/web/.ruby-version new file mode 100644 index 000000000..abf2ccea0 --- /dev/null +++ b/web/.ruby-version @@ -0,0 +1 @@ +ruby-2.0.0-p247 diff --git a/web/Gemfile b/web/Gemfile new file mode 100644 index 000000000..e7ac3d02f --- /dev/null +++ b/web/Gemfile @@ -0,0 +1,108 @@ +source 'https://rubygems.org' + +unless ENV["LOCAL_DEV"] == "1" + source 'https://jamjam:blueberryjam@int.jamkazam.com/gems/' +end +# Look for $WORKSPACE, otherwise use "workspace" as dev path. + +workspace = ENV["WORKSPACE"] || "~/workspace" +devenv = ENV["BUILD_NUMBER"].nil? # Jenkins sets a build number environment variable + +if devenv + gem 'jam_db', :path=> "#{workspace}/jam-db/target/ruby_package" + gem 'jampb', :path => "#{workspace}/jam-pb/target/ruby/jampb" + gem 'jam_ruby', :path => "#{workspace}/jam-ruby" + gem 'jam_websockets', :path => "#{workspace}/websocket-gateway" +else + gem 'jam_db' + gem 'jampb' + gem 'jam_ruby' + gem 'jam_websockets' +end + +gem 'rails', '>=3.2.11' +gem 'jquery-rails', '2.0.2' +gem 'bootstrap-sass', '2.0.4' +gem 'bcrypt-ruby', '3.0.1' +gem 'faker', '1.0.1' +gem 'will_paginate', '3.0.3' +gem 'bootstrap-will_paginate', '0.0.6' +gem 'em-websocket', '>=0.4.0' +gem 'uuidtools', '2.1.2' +gem 'ruby-protocol-buffers', '1.2.2' +gem 'pg', '0.15.1' +gem 'compass-rails' +gem 'rabl' # for JSON API development +gem 'gon' # for passthrough of Ruby variables to Javascript variables +gem 'eventmachine', '1.0.3' +gem 'amqp', '0.9.8' +gem 'logging-rails', :require => 'logging/rails' +gem 'omniauth', '1.1.1' +gem 'omniauth-facebook', '1.4.1' +gem 'omniauth-google-oauth2', '0.2.1' +gem 'fb_graph', '2.5.9' +gem 'sendgrid', '1.1.0' +gem 'recaptcha', '0.3.4' +gem 'filepicker-rails', '0.1.0' +gem 'aws-sdk', '1.8.0' +gem 'aasm', '3.0.16' +gem 'carrierwave' +gem 'fog' +gem 'devise', '>= 1.1.2' +#gem 'thin' # the presence of this gem on mac seems to prevent normal startup of rails. +gem 'postgres-copy' +#group :libv8 do +# gem 'libv8', "~> 3.11.8" +#end + +gem 'quiet_assets', :group => :development + +group :development, :test do + gem 'rspec-rails' + gem 'guard-rspec', '0.5.5' + gem 'jasmine', '1.3.1' + gem 'pry' + gem 'execjs', '1.4.0' +end +group :unix do + gem 'therubyracer' #, '0.11.0beta8' +end + +# Gems used only for assets and not required +# in production environments by default. +group :assets do + gem 'sass-rails' + gem 'coffee-rails' + gem 'uglifier' +end + +group :test, :cucumber do + gem 'capybara' +if ENV['JAMWEB_QT5'] == '1' + # necessary on platforms such as arch linux, where pacman -S qt5-webkit is your easiet option + gem "capybara-webkit", :git => 'git://github.com/thoughtbot/capybara-webkit.git' +else + gem "capybara-webkit" +end + gem 'capybara-screenshot' + gem 'cucumber-rails', :require => false #, '1.3.0', :require => false + gem 'factory_girl_rails', '4.1.0' + gem 'database_cleaner', '0.7.0' + gem 'guard-spork', '0.3.2' + gem 'spork', '0.9.0' + gem 'launchy', '2.1.0' + gem 'rack-test' + # gem 'rb-fsevent', '0.9.1', :require => false + # gem 'growl', '1.0.3' + gem 'poltergeist' , '1.3.0' # can't go to 1.4.0 until this is fixed https://github.com/jonleighton/poltergeist/issues/385 +end + +group :production do + gem 'unicorn' +end + +group :package do + gem 'fpm' +end + + diff --git a/web/Guardfile b/web/Guardfile new file mode 100644 index 000000000..b7b39f0c2 --- /dev/null +++ b/web/Guardfile @@ -0,0 +1,46 @@ +# A sample Guardfile +# More info at https://github.com/guard/guard#readme + +require 'active_support/core_ext' + +guard 'spork', :cucumber_env => { 'RAILS_ENV' => 'test' }, :rspec_env => { 'RAILS_ENV' => 'test' } do + watch('config/application.rb') + watch('config/environment.rb') + watch(%r{^config/environments/.+\.rb$}) + watch(%r{^config/initializers/.+\.rb$}) + watch('Gemfile') + watch('Gemfile.lock') + watch('spec/spec_helper.rb') + watch('test/test_helper.rb') + watch('spec/support/') +end + +guard 'rspec', :version => 2, :all_after_pass => false, :cli => '--drb' do + watch(%r{^spec/.+_spec\.rb$}) + watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" } + watch('spec/spec_helper.rb') { "spec" } + + # Rails example + watch(%r{^spec/.+_spec\.rb$}) + watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" } + watch(%r{^app/(.*)(\.erb|\.haml)$}) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" } + watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" } + watch(%r{^app/controllers/(.+)_(controller)\.rb$}) { |m| ["spec/routing/#{m[1]}_routing_spec.rb", "spec/#{m[2]}s/#{m[1]}_#{m[2]}_spec.rb", "spec/acceptance/#{m[1]}_spec.rb"] } + watch(%r{^spec/support/(.+)\.rb$}) { "spec" } + watch('spec/spec_helper.rb') { "spec" } + watch('config/routes.rb') { "spec/routing" } + watch('app/controllers/application_controller.rb') { "spec/controllers" } + # Capybara request specs + watch(%r{^app/views/(.+)/.*\.(erb|haml)$}) { |m| "spec/requests/#{m[1]}_spec.rb" } + watch(%r{^app/controllers/(.+)_(controller)\.rb$}) do |m| + ["spec/routing/#{m[1]}_routing_spec.rb", + "spec/#{m[2]}s/#{m[1]}_#{m[2]}_spec.rb", + "spec/acceptance/#{m[1]}_spec.rb", + "spec/requests/#{m[1].singularize}_pages_spec.rb", + (m[1][/_pages/] ? "spec/requests/#{m[1]}_spec.rb" : + "spec/requests/#{m[1].singularize}_pages_spec.rb")] + end + watch(%r{^app/views/(.+)/}) do |m| + "spec/requests/#{m[1].singularize}_pages_spec.rb" + end +end \ No newline at end of file diff --git a/web/ORIG_LICENSE b/web/ORIG_LICENSE new file mode 100644 index 000000000..4344917ef --- /dev/null +++ b/web/ORIG_LICENSE @@ -0,0 +1,33 @@ +The Ruby on Rails Tutorial source code is licensed jointly +under the MIT License and the Beerware License. + +The MIT License + +Copyright (c) 2012 Michael Hartl + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions:
+ +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +/* + * ---------------------------------------------------------------------------- + * "THE BEER-WARE LICENSE" (Revision 42): + * Michael Hartl wrote this code. As long as you retain this notice you + * can do whatever you want with this stuff. If we meet some day, and you think + * this stuff is worth it, you can buy me a beer in return. + * ---------------------------------------------------------------------------- + */ \ No newline at end of file diff --git a/web/README.md b/web/README.md new file mode 100644 index 000000000..a8544c34b --- /dev/null +++ b/web/README.md @@ -0,0 +1,12 @@ +* TODO + +Jasmine Javascript Unit Tests +============================= + +1. Ensure you have the jasmine Gem installed; +$ bundle + +2. Start the jasmine server (defaults to :8888) +$ rake jasmine + +Open browser to localhost:8888 diff --git a/web/Rakefile b/web/Rakefile new file mode 100644 index 000000000..e0f380c18 --- /dev/null +++ b/web/Rakefile @@ -0,0 +1,7 @@ +#!/usr/bin/env rake +# Add your own tasks in files placed in lib/tasks ending in .rake, +# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. + +require File.expand_path('../config/application', __FILE__) + +SampleApp::Application.load_tasks diff --git a/web/app/assets/flash/WebSocketMain.swf b/web/app/assets/flash/WebSocketMain.swf new file mode 100644 index 000000000..817446691 Binary files /dev/null and b/web/app/assets/flash/WebSocketMain.swf differ diff --git a/web/app/assets/fonts/lato/Lato-Bla-webfont.eot b/web/app/assets/fonts/lato/Lato-Bla-webfont.eot new file mode 100644 index 000000000..61fc9a31c Binary files /dev/null and b/web/app/assets/fonts/lato/Lato-Bla-webfont.eot differ diff --git a/web/app/assets/fonts/lato/Lato-Bla-webfont.svg b/web/app/assets/fonts/lato/Lato-Bla-webfont.svg new file mode 100644 index 000000000..a63cfd684 --- /dev/null +++ b/web/app/assets/fonts/lato/Lato-Bla-webfont.svg @@ -0,0 +1,147 @@ + + + \ No newline at end of file diff --git a/web/app/assets/fonts/lato/Lato-Bla-webfont.ttf b/web/app/assets/fonts/lato/Lato-Bla-webfont.ttf new file mode 100644 index 000000000..e85d1afb4 Binary files /dev/null and b/web/app/assets/fonts/lato/Lato-Bla-webfont.ttf differ diff --git a/web/app/assets/fonts/lato/Lato-Bla-webfont.woff b/web/app/assets/fonts/lato/Lato-Bla-webfont.woff new file mode 100644 index 000000000..fc4aab38b Binary files /dev/null and b/web/app/assets/fonts/lato/Lato-Bla-webfont.woff differ diff --git a/web/app/assets/fonts/lato/Lato-BlaIta-webfont.eot b/web/app/assets/fonts/lato/Lato-BlaIta-webfont.eot new file mode 100644 index 000000000..780ea9a6f Binary files /dev/null and b/web/app/assets/fonts/lato/Lato-BlaIta-webfont.eot differ diff --git a/web/app/assets/fonts/lato/Lato-BlaIta-webfont.svg b/web/app/assets/fonts/lato/Lato-BlaIta-webfont.svg new file mode 100644 index 000000000..0a9f257d2 --- /dev/null +++ b/web/app/assets/fonts/lato/Lato-BlaIta-webfont.svg @@ -0,0 +1,147 @@ + + + \ No newline at end of file diff --git a/web/app/assets/fonts/lato/Lato-BlaIta-webfont.ttf b/web/app/assets/fonts/lato/Lato-BlaIta-webfont.ttf new file mode 100644 index 000000000..24e567698 Binary files /dev/null and b/web/app/assets/fonts/lato/Lato-BlaIta-webfont.ttf differ diff --git a/web/app/assets/fonts/lato/Lato-BlaIta-webfont.woff b/web/app/assets/fonts/lato/Lato-BlaIta-webfont.woff new file mode 100644 index 000000000..e31a88e9d Binary files /dev/null and b/web/app/assets/fonts/lato/Lato-BlaIta-webfont.woff differ diff --git a/web/app/assets/fonts/lato/Lato-Bol-webfont.eot b/web/app/assets/fonts/lato/Lato-Bol-webfont.eot new file mode 100644 index 000000000..32b803800 Binary files /dev/null and b/web/app/assets/fonts/lato/Lato-Bol-webfont.eot differ diff --git a/web/app/assets/fonts/lato/Lato-Bol-webfont.svg b/web/app/assets/fonts/lato/Lato-Bol-webfont.svg new file mode 100644 index 000000000..f54015793 --- /dev/null +++ b/web/app/assets/fonts/lato/Lato-Bol-webfont.svg @@ -0,0 +1,147 @@ + + + \ No newline at end of file diff --git a/web/app/assets/fonts/lato/Lato-Bol-webfont.ttf b/web/app/assets/fonts/lato/Lato-Bol-webfont.ttf new file mode 100644 index 000000000..a62d61aeb Binary files /dev/null and b/web/app/assets/fonts/lato/Lato-Bol-webfont.ttf differ diff --git a/web/app/assets/fonts/lato/Lato-Bol-webfont.woff b/web/app/assets/fonts/lato/Lato-Bol-webfont.woff new file mode 100644 index 000000000..866d7867c Binary files /dev/null and b/web/app/assets/fonts/lato/Lato-Bol-webfont.woff differ diff --git a/web/app/assets/fonts/lato/Lato-BolIta-webfont.eot b/web/app/assets/fonts/lato/Lato-BolIta-webfont.eot new file mode 100644 index 000000000..c033eafa4 Binary files /dev/null and b/web/app/assets/fonts/lato/Lato-BolIta-webfont.eot differ diff --git a/web/app/assets/fonts/lato/Lato-BolIta-webfont.svg b/web/app/assets/fonts/lato/Lato-BolIta-webfont.svg new file mode 100644 index 000000000..228f7c522 --- /dev/null +++ b/web/app/assets/fonts/lato/Lato-BolIta-webfont.svg @@ -0,0 +1,147 @@ + + + \ No newline at end of file diff --git a/web/app/assets/fonts/lato/Lato-BolIta-webfont.ttf b/web/app/assets/fonts/lato/Lato-BolIta-webfont.ttf new file mode 100644 index 000000000..5157d9ea5 Binary files /dev/null and b/web/app/assets/fonts/lato/Lato-BolIta-webfont.ttf differ diff --git a/web/app/assets/fonts/lato/Lato-BolIta-webfont.woff b/web/app/assets/fonts/lato/Lato-BolIta-webfont.woff new file mode 100644 index 000000000..dd2ae3220 Binary files /dev/null and b/web/app/assets/fonts/lato/Lato-BolIta-webfont.woff differ diff --git a/web/app/assets/fonts/lato/Lato-Hai-webfont.eot b/web/app/assets/fonts/lato/Lato-Hai-webfont.eot new file mode 100644 index 000000000..0e8db1ab9 Binary files /dev/null and b/web/app/assets/fonts/lato/Lato-Hai-webfont.eot differ diff --git a/web/app/assets/fonts/lato/Lato-Hai-webfont.svg b/web/app/assets/fonts/lato/Lato-Hai-webfont.svg new file mode 100644 index 000000000..70ea07887 --- /dev/null +++ b/web/app/assets/fonts/lato/Lato-Hai-webfont.svg @@ -0,0 +1,147 @@ + + + \ No newline at end of file diff --git a/web/app/assets/fonts/lato/Lato-Hai-webfont.ttf b/web/app/assets/fonts/lato/Lato-Hai-webfont.ttf new file mode 100644 index 000000000..e9e8be703 Binary files /dev/null and b/web/app/assets/fonts/lato/Lato-Hai-webfont.ttf differ diff --git a/web/app/assets/fonts/lato/Lato-Hai-webfont.woff b/web/app/assets/fonts/lato/Lato-Hai-webfont.woff new file mode 100644 index 000000000..959f00829 Binary files /dev/null and b/web/app/assets/fonts/lato/Lato-Hai-webfont.woff differ diff --git a/web/app/assets/fonts/lato/Lato-HaiIta-webfont.eot b/web/app/assets/fonts/lato/Lato-HaiIta-webfont.eot new file mode 100644 index 000000000..9cee09402 Binary files /dev/null and b/web/app/assets/fonts/lato/Lato-HaiIta-webfont.eot differ diff --git a/web/app/assets/fonts/lato/Lato-HaiIta-webfont.svg b/web/app/assets/fonts/lato/Lato-HaiIta-webfont.svg new file mode 100644 index 000000000..5790a307f --- /dev/null +++ b/web/app/assets/fonts/lato/Lato-HaiIta-webfont.svg @@ -0,0 +1,147 @@ + + + \ No newline at end of file diff --git a/web/app/assets/fonts/lato/Lato-HaiIta-webfont.ttf b/web/app/assets/fonts/lato/Lato-HaiIta-webfont.ttf new file mode 100644 index 000000000..d2d01c643 Binary files /dev/null and b/web/app/assets/fonts/lato/Lato-HaiIta-webfont.ttf differ diff --git a/web/app/assets/fonts/lato/Lato-HaiIta-webfont.woff b/web/app/assets/fonts/lato/Lato-HaiIta-webfont.woff new file mode 100644 index 000000000..7987fa1b0 Binary files /dev/null and b/web/app/assets/fonts/lato/Lato-HaiIta-webfont.woff differ diff --git a/web/app/assets/fonts/lato/Lato-Lig-webfont.eot b/web/app/assets/fonts/lato/Lato-Lig-webfont.eot new file mode 100644 index 000000000..422659676 Binary files /dev/null and b/web/app/assets/fonts/lato/Lato-Lig-webfont.eot differ diff --git a/web/app/assets/fonts/lato/Lato-Lig-webfont.svg b/web/app/assets/fonts/lato/Lato-Lig-webfont.svg new file mode 100644 index 000000000..e89a231af --- /dev/null +++ b/web/app/assets/fonts/lato/Lato-Lig-webfont.svg @@ -0,0 +1,147 @@ + + + \ No newline at end of file diff --git a/web/app/assets/fonts/lato/Lato-Lig-webfont.ttf b/web/app/assets/fonts/lato/Lato-Lig-webfont.ttf new file mode 100644 index 000000000..86b4e957e Binary files /dev/null and b/web/app/assets/fonts/lato/Lato-Lig-webfont.ttf differ diff --git a/web/app/assets/fonts/lato/Lato-Lig-webfont.woff b/web/app/assets/fonts/lato/Lato-Lig-webfont.woff new file mode 100644 index 000000000..0c337a6a3 Binary files /dev/null and b/web/app/assets/fonts/lato/Lato-Lig-webfont.woff differ diff --git a/web/app/assets/fonts/lato/Lato-LigIta-webfont.eot b/web/app/assets/fonts/lato/Lato-LigIta-webfont.eot new file mode 100644 index 000000000..adde47b81 Binary files /dev/null and b/web/app/assets/fonts/lato/Lato-LigIta-webfont.eot differ diff --git a/web/app/assets/fonts/lato/Lato-LigIta-webfont.svg b/web/app/assets/fonts/lato/Lato-LigIta-webfont.svg new file mode 100644 index 000000000..43f86c818 --- /dev/null +++ b/web/app/assets/fonts/lato/Lato-LigIta-webfont.svg @@ -0,0 +1,147 @@ + + + \ No newline at end of file diff --git a/web/app/assets/fonts/lato/Lato-LigIta-webfont.ttf b/web/app/assets/fonts/lato/Lato-LigIta-webfont.ttf new file mode 100644 index 000000000..abda1bfa4 Binary files /dev/null and b/web/app/assets/fonts/lato/Lato-LigIta-webfont.ttf differ diff --git a/web/app/assets/fonts/lato/Lato-LigIta-webfont.woff b/web/app/assets/fonts/lato/Lato-LigIta-webfont.woff new file mode 100644 index 000000000..098a23713 Binary files /dev/null and b/web/app/assets/fonts/lato/Lato-LigIta-webfont.woff differ diff --git a/web/app/assets/fonts/lato/Lato-Reg-webfont.eot b/web/app/assets/fonts/lato/Lato-Reg-webfont.eot new file mode 100644 index 000000000..e648b1ade Binary files /dev/null and b/web/app/assets/fonts/lato/Lato-Reg-webfont.eot differ diff --git a/web/app/assets/fonts/lato/Lato-Reg-webfont.svg b/web/app/assets/fonts/lato/Lato-Reg-webfont.svg new file mode 100644 index 000000000..d300fe48e --- /dev/null +++ b/web/app/assets/fonts/lato/Lato-Reg-webfont.svg @@ -0,0 +1,147 @@ + + + \ No newline at end of file diff --git a/web/app/assets/fonts/lato/Lato-Reg-webfont.ttf b/web/app/assets/fonts/lato/Lato-Reg-webfont.ttf new file mode 100644 index 000000000..1d1e08655 Binary files /dev/null and b/web/app/assets/fonts/lato/Lato-Reg-webfont.ttf differ diff --git a/web/app/assets/fonts/lato/Lato-Reg-webfont.woff b/web/app/assets/fonts/lato/Lato-Reg-webfont.woff new file mode 100644 index 000000000..95bba7176 Binary files /dev/null and b/web/app/assets/fonts/lato/Lato-Reg-webfont.woff differ diff --git a/web/app/assets/fonts/lato/Lato-RegIta-webfont.eot b/web/app/assets/fonts/lato/Lato-RegIta-webfont.eot new file mode 100644 index 000000000..6f7fd309f Binary files /dev/null and b/web/app/assets/fonts/lato/Lato-RegIta-webfont.eot differ diff --git a/web/app/assets/fonts/lato/Lato-RegIta-webfont.svg b/web/app/assets/fonts/lato/Lato-RegIta-webfont.svg new file mode 100644 index 000000000..28bd02ab6 --- /dev/null +++ b/web/app/assets/fonts/lato/Lato-RegIta-webfont.svg @@ -0,0 +1,147 @@ + + + \ No newline at end of file diff --git a/web/app/assets/fonts/lato/Lato-RegIta-webfont.ttf b/web/app/assets/fonts/lato/Lato-RegIta-webfont.ttf new file mode 100644 index 000000000..60197266e Binary files /dev/null and b/web/app/assets/fonts/lato/Lato-RegIta-webfont.ttf differ diff --git a/web/app/assets/fonts/lato/Lato-RegIta-webfont.woff b/web/app/assets/fonts/lato/Lato-RegIta-webfont.woff new file mode 100644 index 000000000..4adbc518f Binary files /dev/null and b/web/app/assets/fonts/lato/Lato-RegIta-webfont.woff differ diff --git a/web/app/assets/fonts/lato/SIL Open Font License 1.1.txt b/web/app/assets/fonts/lato/SIL Open Font License 1.1.txt new file mode 100644 index 000000000..e4b0c4ff5 --- /dev/null +++ b/web/app/assets/fonts/lato/SIL Open Font License 1.1.txt @@ -0,0 +1,91 @@ +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. \ No newline at end of file diff --git a/web/app/assets/fonts/lato/demo.html b/web/app/assets/fonts/lato/demo.html new file mode 100644 index 000000000..7ba9456a9 --- /dev/null +++ b/web/app/assets/fonts/lato/demo.html @@ -0,0 +1,78 @@ + + + + + + +Lato Black Italic - Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+ + + +Lato Black - Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+ + + +Lato Bold Italic - Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+ + + +Lato Bold - Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+ + + +Lato Italic - Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+ + + +Lato Regular - Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+ + + +Lato Light Italic - Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+ + + +Lato Light - Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+ + + +Lato Hairline Italic - Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+ + + +Lato Hairline - Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+ +RouteMap holds an internal table of route patterns and method names in addition to some + * adding/removing/utility methods and a handler for request routing.
+ *It does not have any dependencies and is written in "plain old" JS, but it does require JS 1.8 array methods, so + * if the environment it will run in does not have those, the reference implementations from + * Mozilla should be + * supplied external to this library.
+ *It is designed to be used in both a browser setting and a server-side context (for example in node.js).
+ * LICENSING INFORMATION: + *+ * @see OpenGamma + * @see Apache License, Version 2.0 + * @see Mozilla Developer + * Network + * @name RouteMap + * @namespace RouteMap + * @author Afshin Darian + * @static + * @throws {Error} if JS 1.8 Array.prototype methods don't exist + */ +(function (pub, namespace) { // defaults to exports, uses window if exports does not exist + (function (arr, url) { // plain old JS, but needs some JS 1.8 array methods + if (!arr.every || !arr.filter || !arr.indexOf || !arr.map || !arr.reduce || !arr.some || !arr.forEach) + throw new Error('See ' + url + ' for reference versions of Array.prototype methods available in JS 1.8'); + })([], 'https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/array/'); + var routes /* internal reference to RouteMap */, active_routes = {}, added_routes = {}, flat_pages = [], + last = 0, current = 0, encode = encodeURIComponent, decode = decodeURIComponent, has = 'hasOwnProperty', + EQ = '=' /* equal string */, SL = '/' /* slash string */, PR = '#' /* default prefix string */, + token_exp = /\*|:|\?/, star_exp = /(^([^\*:\?]+):\*)|(^\*$)/, scalar_exp = /^:([^\*:\?]+)(\??)$/, + keyval_exp = /^([^\*:\?]+):(\??)$/, slash_exp = new RegExp('([^' + SL + '])$'), + context = typeof window !== 'undefined' ? window : {}, // where listeners reside, routes.context() overwrites it + /** @ignore */ + invalid_str = function (str) {return typeof str !== 'string' || !str.length;}, + /** @ignore */ + fingerprint = function (rule) {return [rule.method, rule.route].join('|');}, + /** + * merges one or more objects into a new object by value (nothing is a reference), useful for cloning + * @name RouteMap#merge + * @inner + * @function + * @type Object + * @returns {Object} a merged object + * @throws {TypeError} if one of the arguments is not a mergeable object (i.e. a primitive, null or array) + */ + merge = function () { + var self = 'merge', to_string = Object.prototype.toString, clone = function (obj) { + return typeof obj !== 'object' || obj === null ? obj // primitives + : to_string.call(obj) === '[object Array]' ? obj.map(clone) // arrays + : merge(obj); // objects + }; + return Array.prototype.reduce.call(arguments, function (acc, obj) { + if (!obj || typeof obj !== 'object' || to_string.call(obj) === '[object Array]') + throw new TypeError(self + ': ' + to_string.call(obj) + ' is not mergeable'); + for (var key in obj) if (obj[has](key)) acc[key] = clone(obj[key]); + return acc; + }, {}); + }, + /** + * parses a path and returns a list of objects that contain argument dictionaries, methods, and raw hash values + * @name RouteMap#parse + * @inner + * @function + * @param {String} path + * @type Array + * @returns {Array} a list of parsed objects in descending order of matched hash length + * @throws {TypeError} if the method specified by a rule specification does not exist during parse time + */ + parse = function (path) { + // go with the first matching page (longest) or any pages with * rules + var self = 'parse', pages = flat_pages.filter(function (val) { // add slash to paths so all vals match + return path.replace(slash_exp, '$1' + SL).indexOf(val) === 0; + }) + .filter(function (page, index) { + return !index || active_routes[page].some(function (val) {return !!val.rules.star;}); + }); + return !pages.length ? [] : pages.reduce(function (acc, page) { // flatten parsed rules for all pages + var current_page = active_routes[page].map(function (rule_set) { + var args = {}, scalars = rule_set.rules.scalars, keyvals = rule_set.rules.keyvals, method, + // populate the current request object as a collection of keys/values and scalars + request = path.replace(page, '').split(SL).reduce(function (acc, val) { + var split = val.split(EQ), key = split[0], value = split.slice(1).join(EQ); + return !val.length ? acc // discard empty values, separate rest into scalars or keyvals + : (value ? acc.keyvals[key] = value : acc.scalars.push(val)), acc; + }, {keyvals: {}, scalars: []}), star, keyval, + keyval_keys = keyvals.reduce(function (acc, val) {return (acc[val.name] = 0) || acc;}, {}), + required_scalars_length = scalars.filter(function (val) {return val.required;}).length, + required_keyvals = keyvals.filter(function (val) {return val.required;}) + .every(function (val) {return request.keyvals[has](val.name);}); + // not enough parameters are supplied in the request for this rule + if (required_scalars_length > request.scalars.length || !required_keyvals) return 0; + if (!rule_set.rules.star) { // too many params are only a problem if the rule isn't a wildcard + if (request.scalars.length > scalars.length) return 0; // if too many scalars are supplied + for (keyval in request.keyvals) // if too many keyvals are supplied + if (request.keyvals[has](keyval) && !keyval_keys[has](keyval)) return 0; + } + request.scalars.slice(0, scalars.length) // populate args scalars + .forEach(function (scalar, index) {args[scalars[index].name] = decode(scalar);}); + keyvals.forEach(function (keyval) { // populate args keyvals + if (request.keyvals[keyval.name]) args[keyval.name] = decode(request.keyvals[keyval.name]); + delete request.keyvals[keyval.name]; // remove so that * can be constructed + }); + if (rule_set.rules.star) { // all unused scalars and keyvals go into the * argument (still encoded) + star = request.scalars.slice(scalars.length, request.scalars.length); + for (keyval in request.keyvals) if (request.keyvals[has](keyval)) + star.push([keyval, request.keyvals[keyval]].join(EQ)); + args[rule_set.rules.star] = star.join(SL); + } + try { // make sure the rule's method actually exists and can be accessed + method = rule_set.method.split('.').reduce(function (acc, val) {return acc[val];}, context); + if (typeof method !== 'function') throw new Error; + } catch (error) { + throw new TypeError(self + ': ' + rule_set.method + ' is not a function in current context'); + } + return {page: page, hash: routes.hash({route: rule_set.raw}, args), method: method, args: args}; + }); + return acc.concat(current_page).filter(Boolean); // only return the parsed rules that matched + }, []).sort(function (a, b) {return b.hash.length - a.hash.length;}); // order in descending hash length + }, + /** + * builds the internal representation of a rule based on the route definition + * @inner + * @name RouteMap#compile + * @function + * @param {String} route + * @throws {SyntaxError} if any portion of a rule definition follows a+ * Copyright 2011 OpenGamma Inc. and the OpenGamma group of companies + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + *
* directive
+ * @throws {SyntaxError} if a required scalar follows an optional scalar
+ * @throws {SyntaxError} if a rule cannot be parsed
+ * @type {Object}
+ * @returns {Object} a compiled object, for example, the rule '/foo/:id/type:?/rest:*' would return
+ * an object of the form: {
+ * page:'/foo',
+ * rules:{
+ * keyvals:[{name: 'type', required: false}],
+ * scalars:[{name: 'id', required: true}],
+ * star:'rest' // false if not defined
+ * }
+ * }
+ * @see RouteMap.add
+ * @see RouteMap.hash
+ * @see RouteMap.remove
+ */
+ compile = (function (memo) { // compile is slow so cache compiled objects in a memo
+ return function (orig) {
+ var self = 'compile', compiled, index, names = {},
+ route = orig[0] === SL ? orig : ~(index = orig.indexOf(SL)) ? orig.slice(index) : 0,
+ /** @ignore */
+ valid_name = function (name) {
+ if (names[has](name) || (names[name] = 0))
+ throw new SyntaxError(self + ': "' + name + '" is repeated in: ' + orig);
+ };
+ if (!route) throw new SyntaxError(self + ': the route ' + orig + ' was not understood');
+ if (memo[route]) return memo[route];
+ compiled = route.split(SL).reduce(function (acc, val) {
+ var rules = acc.rules, scalars = rules.scalars, keyvals = rules.keyvals;
+ if (rules.star) throw new SyntaxError(self + ': no rules can follow a * directive in: ' + orig);
+ // construct the name of the page
+ if (!~val.search(token_exp) && !scalars.length && !keyvals.length) return acc.page.push(val), acc;
+ // construct the parameters
+ if (val.match(star_exp)) return (rules.star = RegExp.$2 || RegExp.$3), valid_name(rules.star), acc;
+ if (val.match(scalar_exp)) {
+ if (acc.has_optional_scalar) // no scalars can follow optional scalars
+ throw new SyntaxError(self + ': "' + val + '" cannot follow an optional rule in: ' + orig);
+ if (!!RegExp.$2) acc.has_optional_scalar = val;
+ return scalars.push({name: RegExp.$1, required: !RegExp.$2}), valid_name(RegExp.$1), acc;
+ }
+ if (val.match(keyval_exp))
+ return keyvals.push({name: RegExp.$1, required: !RegExp.$2}), valid_name(RegExp.$1), acc;
+ throw new SyntaxError(self + ': the rule "' + val + '" was not understood in: ' + orig);
+ }, {page: [], rules: {scalars: [], keyvals: [], star: false}, has_optional_scalar: ''});
+ delete compiled.has_optional_scalar; // this is just a temporary value and should not be exposed
+ compiled.page = compiled.page.join(SL).replace(new RegExp(SL + '$'), '') || SL;
+ return memo[route] = compiled;
+ };
+ })({});
+ pub[namespace] = (routes) = { // parens around routes to satisfy JSDoc's caprice
+ /**
+ * adds a rule to the internal table of routes and methods
+ * @name RouteMap.add
+ * @function
+ * @type undefined
+ * @param {Object} rule rule specification
+ * @param {String} rule.route route pattern definition; there are three types of pattern arguments: scalars,
+ * keyvals, and stars; scalars are individual values in a URL (all URL values are separate by the
+ * '/' character), keyvals are named values, e.g. 'foo=bar', and star values are wildcards; so for
+ * example, the following pattern represents all the possible options:
+ * '/foo/:id/:sub?/attr:/subattr:?/rest:*'
the ? means that argument is
+ * optional, the star rule is named rest but it could have just simply been left as *,
+ * which means the resultant dictionary would have put the wildcard remainder into args['*']
+ * instead of args.rest; so the following URL would match the pattern above:
+ * /foo/23/45/attr=something/subattr=something_else
+ * when its method is called, it will receive this arguments dictionary:
+ * {
+ * id:'23',
+ * subid:'45',
+ * attr:'something',
+ * subattr:'something_else',
+ * rest:''
+ * }
+ * add uses {@link #compile} and does not catch any errors thrown by that function
+ * @param {String} rule.method listener method for this route
+ * @throws {TypeError} if rule.route or rule.method are not strings or empty strings
+ * @throws {Error} if rule has already been added
+ * @see RouteMap.post_add
+ */
+ add: function (rule) {
+ var self = 'add', method = rule.method, route = rule.route, compiled, id = fingerprint(rule);
+ if ([route, method].some(invalid_str))
+ throw new TypeError(self + ': rule.route and rule.method must both be non-empty strings');
+ if (added_routes[id]) throw new Error(self + ': ' + route + ' to ' + method + ' already exists');
+ compiled = compile(route);
+ added_routes[id] = true;
+ if (!active_routes[compiled.page] && (active_routes[compiled.page] = [])) // add route to list and sort
+ flat_pages = flat_pages.concat(compiled.page).sort(function (a, b) {return b.length - a.length;});
+ active_routes[compiled.page].push(routes.post_add({method: method, rules: compiled.rules, raw: route}));
+ },
+ /**
+ * overrides the context where listener methods are sought, the default scope is window
+ * (in a browser setting), returns the current context, if no scope object is passed in, just
+ * returns current context without setting context
+ * @name RouteMap.context
+ * @function
+ * @type {Object}
+ * @returns {Object} the current context within which RouteMap searches for handlers
+ * @param {Object} scope the scope within which methods for mapped routes will be looked for
+ */
+ context: function (scope) {return context = typeof scope === 'object' ? scope : context;},
+ /**
+ * returns the parsed (see {@link #parse}) currently accessed route; after listeners have finished
+ * firing, current and last are the same
+ * @name RouteMap.current
+ * @function
+ * @type Object
+ * @returns {Object} the current parsed URL object
+ * @see RouteMap.last
+ */
+ current: function () {return current ? merge(current) : null;},
+ /**
+ * this function is fired when no rule is matched by a URL, by default it does nothing, but it could be set up
+ * to handle things like 404 responses on the server-side or bad hash fragments in the browser
+ * @name RouteMap.default_handler
+ * @function
+ * @type undefined
+ */
+ default_handler: function () {},
+ /**
+ * URL grabber function, defaults to checking the URL fragment (hash); this function should be
+ * overwritten in a server-side environment; this method is called by {@link RouteMap.handler}; without
+ * window.location.hash it will return '/'
+ * @name RouteMap.get
+ * @function
+ * @returns {String} by default, this returns a subset of the URL hash (everything after the first
+ * '/' character ... if nothing follows a slash, it returns '/'); if overwritten, it
+ * must be a function that returns URL path strings (beginning with '/') to match added rules
+ * @type String
+ */
+ get: function () {
+ if (typeof window === 'undefined') return SL;
+ var hash = window.location.hash, index = hash.indexOf(SL);
+ return ~index ? hash.slice(index) : SL;
+ },
+ /**
+ * in a browser setting, it changes window.location.hash, in other settings, it should be
+ * overwritten to do something useful (if necessary); it will not throw an error if window does
+ * not exist
+ * @name RouteMap.go
+ * @function
+ * @type undefined
+ * @param {String} hash the hash fragment to go to
+ */
+ go: function (hash) {
+ if (typeof window !== 'undefined') window.location.hash = (hash.indexOf(PR) === 0 ? '' : PR) + hash;
+ },
+ /**
+ * main handler function for routing, this should be bound to hashchange events in the browser, or
+ * (in conjunction with updating {@link RouteMap.get}) used with the HTML5 history API, it detects
+ * all the matching route patterns, parses the URL parameters and fires their methods with the arguments from
+ * the parsed URL; the timing of {@link RouteMap.current} and {@link RouteMap.last} being set is as follows
+ * (pseudo-code):
+ *
+ * path: get_route // {@link RouteMap.get}
+ * parsed: parse path // {@link #parse}
+ * current: longest parsed // {@link RouteMap.current}
+ * parsed: pre_dispatch parsed // {@link RouteMap.pre_dispatch}
+ * current: longest parsed // reset current
+ * fire matched rules in parsed
+ * last: current // {@link RouteMap.last}
+ *
+ * RouteMap.handler calls {@link #parse} and does not catch any errors that function throws
+ * @name RouteMap.handler
+ * @function
+ * @type undefined
+ * @see RouteMap.pre_dispatch
+ */
+ handler: function () {
+ var url = routes.get(), parsed = parse(url), args = Array.prototype.slice.call(arguments);
+ if (!parsed.length) return routes.default_handler.apply(null, [url].concat(args));
+ current = parsed[0]; // set current to the longest hash before pre_dispatch touches it
+ parsed = routes.pre_dispatch(parsed); // pre_dispatch might change the contents of parsed
+ current = parsed[0]; // set current to the longest hash again after pre_dispatch
+ parsed.forEach(function (val) {val.method.apply(null, [val.args].concat(args));}); // fire requested methods
+ last = parsed[0];
+ },
+ /**
+ * returns a URL fragment by applying parameters to a rule; uses {@link #compile} and does not catch any errors
+ * thrown by that function
+ * @name RouteMap.hash
+ * @function
+ * @type String
+ * @param {Object} rule the rule specification; it typically looks like:
+ * {route:'/foo', method:'bar'}
but only route is strictly necessary
+ * @param {Object} params a dictionary of argument key/value pairs required by the rule
+ * @returns {String} URL fragment resulting from applying arguments to rule pattern
+ * @throws {TypeError} if a required parameter is not present
+ */
+ hash: function (rule, params) {
+ var self = 'hash', hash, compiled, params = params || {};
+ if (invalid_str(rule.route)) throw new TypeError(self + ': rule.route must be a non-empty string');
+ compiled = compile(rule.route);
+ hash = compiled.page + (compiled.page === SL ? '' : SL) + // 1. start with page, then add params
+ compiled.rules.scalars.map(function (val) { // 2. add scalar values next
+ var value = encode(params[val.name]), bad_param = params[val.name] === void 0 || invalid_str(value);
+ if (val.required && bad_param)
+ throw new TypeError(self + ': params.' + val.name + ' is undefined, route: ' + rule.route);
+ return bad_param ? 0 : value;
+ })
+ .concat(compiled.rules.keyvals.map(function (val) { // 3. then concat keyval values
+ var value = encode(params[val.name]), bad_param = params[val.name] === void 0 || invalid_str(value);
+ if (val.required && bad_param)
+ throw new TypeError(self + ': params.' + val.name + ' is undefined, route: ' + rule.route);
+ return bad_param ? 0 : val.name + EQ + value;
+ }))
+ .filter(Boolean).join(SL); // remove empty (0) values
+ if (compiled.rules.star && params[compiled.rules.star]) // 4. add star value if it exists
+ hash += (hash[hash.length - 1] === SL ? '' : SL) + params[compiled.rules.star];
+ return hash;
+ },
+ /**
+ * returns the parsed (see {@link #parse}) last accessed route; when route listeners are being called,
+ * last is the previously accessed route, after listeners have finished firing, the current parsed
+ * route replaces last's value
+ * @name RouteMap.last
+ * @function
+ * @type Object
+ * @returns {Object} the last parsed URL object, will be null on first load
+ * @see RouteMap.current
+ */
+ last: function () {return last ? merge(last) : null;},
+ /**
+ * parses a URL fragment into a data structure only if there is a route whose pattern matches the fragment
+ * @name RouteMap.parse
+ * @function
+ * @type Object
+ * @returns {Object} of the form: {page:'/foo', args:{bar:'some_value'}}
+ * only if a rule with the route: '/foo/:bar' has already been added
+ * @throws {TypeError} if hash is not a string, is empty, or does not contain a '/' character
+ * @throws {SyntaxError} if hash cannot be parsed by {@link #parse}
+ */
+ parse: function (hash) {
+ var self = 'parse', parsed, index = hash.indexOf(SL);
+ hash = ~index ? hash.slice(index) : '';
+ if (invalid_str(hash)) throw new TypeError(self + ': hash must be a string with a ' + SL + ' character');
+ if (!(parsed = parse(hash)).length) throw new SyntaxError(self + ': ' + hash + ' cannot be parsed');
+ return {page: parsed[0].page, args: parsed[0].args};
+ },
+ /**
+ * this function is called by {@link RouteMap.add}, it receives a compiled rule object, e.g. for the rule:
+ * {route:'/foo/:id/:sub?/attr:/subattr:?/rest:*', method:'console.log'}
+ * post_add would receive the following object:
+ * {
+ * method:'console.log',
+ * rules:{
+ * scalars:[{name:'id',required:true},{name:'sub',required:false}],
+ * keyvals:[{name:'attr',required:true},{name:'subattr',required:false}],
+ * star:'rest'
+ * },
+ * raw:'/foo/:id/:sub?/attr:/subattr:?/rest:*'
+ * }
+ * and it is expected to pass back an object of the same format; it can be overwritten to post-process added
+ * rules e.g. to add extra default application-wide parameters; by default, it simply returns what was passed
+ * into it
+ * @name RouteMap.post_add
+ * @function
+ * @type Object
+ * @returns {Object} the default function returns the exact object it received; a custom function needs to
+ * an object that is of the same form (but could possibly have more or fewer parameters, etc.)
+ * @param {Object} compiled the compiled rule
+ */
+ post_add: function (compiled) {return compiled;},
+ /**
+ * like {@link RouteMap.post_add} this function can be overwritten to add application-specific code into
+ * route mapping, it is called before a route begins being dispatched to all matching rules; it receives the
+ * list of matching parsed route objects ({@link #parse}) and is expected to return it; one application of this
+ * function might be to set application-wide variables like debug flags
+ * @name RouteMap.pre_dispatch
+ * @function
+ * @type Array
+ * @returns {Array} a list of the same form as the one it receives
+ * @param {Array} parsed the parsed request
+ */
+ pre_dispatch: function (parsed) {return parsed;},
+ /**
+ * if a string is passed in, it overwrites the prefix that is removed from each URL before parsing; primarily
+ * used for hashbang (#!); either way, it returns the current prefix
+ * @name RouteMap.prefix
+ * @function
+ * @type undefined
+ * @param {String} prefix (optional) the prefix string
+ */
+ prefix: function (prefix) {return PR = typeof prefix !== 'undefined' ? prefix + '' : PR;},
+ /**
+ * counterpart to {@link RouteMap.add}, removes a rule specification; * remove uses
+ * {@link #compile} and does not catch any errors thrown by that function
+ * @name RouteMap.remove
+ * @function
+ * @type undefined
+ * @param {Object} rule the rule specification that was used in {@link RouteMap.add}
+ * @throws {TypeError} if rule.route or rule.method are not strings or empty strings
+ */
+ remove: function (rule) {
+ var self = 'remove', method = rule.method, route = rule.route, compiled, id = fingerprint(rule), index;
+ if ([route, method].some(invalid_str))
+ throw new TypeError(self + ': rule.route and rule.method must both be non-empty strings');
+ if (!added_routes[id]) return;
+ compiled = compile(route);
+ delete added_routes[id];
+ active_routes[compiled.page] = active_routes[compiled.page]
+ .filter(function (rule) {return (rule.raw !== route) || (rule.method !== method);});
+ if (!active_routes[compiled.page].length && (delete active_routes[compiled.page])) // delete active route
+ if (~(index = flat_pages.indexOf(compiled.page))) flat_pages.splice(index, 1); // then flat page
+ }
+ };
+})(typeof exports === 'undefined' ? window : exports, 'RouteMap');
\ No newline at end of file
diff --git a/web/app/assets/javascripts/search.js b/web/app/assets/javascripts/search.js
new file mode 100644
index 000000000..04e35767b
--- /dev/null
+++ b/web/app/assets/javascripts/search.js
@@ -0,0 +1,138 @@
+(function(context,$) {
+
+ // NOTE - not currently used. Was hooked up in header before
+ // Jeff's new mockups moved search to sidebar and made search results
+ // a full-blown screen. This should probably be removed, along with
+ // any HTML that accompanies it. Tests too.
+
+ "use strict";
+
+ context.JK = context.JK || {};
+ context.JK.Searcher = function(app) {
+ var logger = context.JK.logger;
+
+ var searchSectionTemplate;
+ var searchItemTemplate;
+ var noResultsTemplate;
+ var usernames = ['Brian', 'David'];
+ var userids = ['1', '2'];
+
+ function events() {
+ $('.searchtextinput').keyup(handleKeyup);
+ $('.searchtextinput').focus(function(evt) {
+ var searchVal = $(this).val();
+ search(searchVal);
+ });
+ $('.searchtextinput').blur(hideSearchResults);
+ }
+
+ function templates() {
+ searchSectionTemplate = $('#template-search-section').html();
+ searchItemTemplate = $('#template-search-item').html();
+ noResultsTemplate = $('#template-search-noresults').html();
+ }
+
+ function hideSearchResults() {
+ $('.searchresults').hide();
+ }
+
+ function showSearchResults() {
+ $('.searchresults').show();
+ }
+
+ function handleKeyup(evt) {
+ if (evt.which === 27) {
+ return hideSearchResults();
+ }
+ var searchVal = $(evt.currentTarget).val();
+ search(searchVal);
+ }
+
+ function search(query) {
+ if (query.length < 2) {
+ return;
+ }
+ $.ajax({
+ type: "GET",
+ url: "/api/search?query=" + query,
+ success: searchResponse,
+ error: app.ajaxError
+ });
+ }
+
+ function searchResponse(response) {
+ ensureResultsDiv();
+ updateResultsDiv(response);
+ positionResultsDiv();
+ showSearchResults();
+ }
+
+ function ensureResultsDiv() {
+ if ($('.searchresults').length === 0) {
+ var $searchresults = $('');
+ $searchresults.addClass('searchresults');
+ $searchresults.css({position:'absolute'});
+ $('body').append($searchresults);
+ }
+ }
+
+ function updateResultsDiv(searchResults) {
+ var sections = ['musicians', 'bands', 'fans', 'recordings'];
+ var fullHtml = '';
+ $.each(sections, function() {
+ fullHtml += getSectionHtml(this, searchResults);
+ });
+ if (fullHtml === '') {
+ fullHtml += getNoResultsMessage();
+ }
+ $('.searchresults').html(fullHtml);
+ }
+
+ function getNoResultsMessage() {
+ // No replacement needed at the moment.
+ return noResultsTemplate;
+ }
+
+ function getSectionHtml(section, searchResults) {
+ if (section in searchResults && searchResults[section].length === 0) {
+ return '';
+ }
+ var items = '';
+ $.each(searchResults[section], function() {
+ items += getItemHtml(this);
+ });
+ var html = context.JK.fillTemplate(
+ searchSectionTemplate,
+ { section: section, items: items });
+ return html;
+ }
+
+ function getItemHtml(item) {
+ var replacements = {
+ id: item.id,
+ name: item.first_name + " " + item.last_name,
+ image: item.photo_url,
+ subtext: item.location
+ };
+ return context.JK.fillTemplate(
+ searchItemTemplate, replacements);
+ }
+
+ function positionResultsDiv() {
+ var bodyOffset = $('body').offset();
+ var inputOffset = $('.searchtextinput').offset();
+ var inputHeight = $('.searchtextinput').outerHeight();
+ var resultsTop = bodyOffset.top + inputOffset.top + inputHeight;
+ var resultsLeft = bodyOffset.left + inputOffset.left;
+ $('.searchresults').css({
+ top: resultsTop + 'px',
+ left: resultsLeft + 'px'});
+ }
+
+ this.initialize = function() {
+ events();
+ templates();
+ };
+ };
+
+ })(window,jQuery);
\ No newline at end of file
diff --git a/web/app/assets/javascripts/searchResults.js b/web/app/assets/javascripts/searchResults.js
new file mode 100644
index 000000000..c67a72833
--- /dev/null
+++ b/web/app/assets/javascripts/searchResults.js
@@ -0,0 +1,131 @@
+(function(context,$) {
+
+ "use strict";
+
+ context.JK = context.JK || {};
+ context.JK.SearchResultScreen = function(app) {
+ var logger = context.JK.logger;
+
+ var instrument_logo_map = context.JK.getInstrumentIconMap24();
+
+ function beforeShow(data) {
+ var query = data.query;
+ }
+
+ function afterShow(data) {
+ }
+
+ function search(evt) {
+ evt.stopPropagation();
+
+ $('#search-results').empty();
+ var query = $('#search-input').val();
+ context.location = '#/searchResults/:' + query;
+
+ logger.debug('query=' + query);
+ if (query !== '') {
+ $('#query').html(query + "\"");
+ context.JK.search(query, app, onSearchSuccess);
+ }
+
+ else {
+ $('#result-count').html('');
+ $('#query').html('');
+ }
+
+ return false;
+ }
+
+ function onSearchSuccess(response) {
+
+ // TODO: generalize this for each search result type (band, musician, recordings, et. al.)
+ $.each(response.musicians, function(index, val) {
+ // fill in template for Connect pre-click
+ var template = $('#template-search-result').html();
+ var searchResultHtml = context.JK.fillTemplate(template, {
+ userId: val.id,
+ avatar_url: context.JK.resolveAvatarUrl(val.photo_url),
+ profile_url: "/#/profile/" + val.id,
+ userName: val.name,
+ location: val.location,
+ instruments: getInstrumentHtml(val.instruments)
+ });
+
+ $('#search-results').append(searchResultHtml);
+
+ // fill in template for Connect post-click
+ template = $('#template-invitation-sent').html();
+ var invitationSentHtml = context.JK.fillTemplate(template, {
+ userId: val.id,
+ first_name: val.first_name,
+ profile_url: "/#/profile/" + val.id
+ });
+
+ $('#search-results').append(invitationSentHtml);
+
+ // initialize visibility of the divs
+ $('div[user-id=' + val.id + '].search-connected').hide();
+ $('div[user-id=' + val.id + '].search-result').show();
+
+ // wire up button click handler if search result is not a friend or the current user
+ if (!val.is_friend && val.id !== context.JK.currentUserId) {
+ $('div[user-id=' + val.id + ']').find('#btn-connect-friend').click(sendFriendRequest);
+ }
+ else {
+ $('div[user-id=' + val.id + ']').find('#btn-connect-friend').hide();
+ }
+ });
+
+ var resultCount = response.musicians.length;
+ $('#result-count').html(resultCount);
+
+ if (resultCount === 1) {
+ $('#result-count').append(" Result for \"");
+ }
+ else {
+ $('#result-count').append(" Results for \"");
+ }
+ }
+
+ function friendRequestCallback(userId) {
+ // toggle the pre-click and post-click divs
+ $('div[user-id=' + userId + '].search-connected').show();
+ $('div[user-id=' + userId + '].search-result').hide();
+ }
+
+ function sendFriendRequest(evt) {
+ evt.stopPropagation();
+ var userId = $(this).parent().attr('user-id');
+ context.JK.sendFriendRequest(app, userId, friendRequestCallback);
+ }
+
+ function getInstrumentHtml(instruments) {
+ var instrumentLogoHtml = '';
+ if (instruments !== undefined) {
+ for (var i=0; i < instruments.length; i++) {
+ var inst = '../assets/content/icon_instrument_default24.png';
+ if (instruments[i].instrument_id in instrument_logo_map) {
+ inst = instrument_logo_map[instruments[i].instrument_id];
+ instrumentLogoHtml += '
';
+ }
+ }
+ }
+ return instrumentLogoHtml;
+ }
+
+ function events() {
+ $('#searchForm').submit(search);
+ }
+
+ this.initialize = function() {
+ var screenBindings = {
+ 'beforeShow': beforeShow,
+ 'afterShow': afterShow
+ };
+ app.bindScreen('searchResults', screenBindings);
+ events();
+ };
+
+ };
+
+})(window,jQuery);
\ No newline at end of file
diff --git a/web/app/assets/javascripts/session.js b/web/app/assets/javascripts/session.js
new file mode 100644
index 000000000..04db7f786
--- /dev/null
+++ b/web/app/assets/javascripts/session.js
@@ -0,0 +1,833 @@
+(function(context,$) {
+
+ "use strict";
+
+ context.JK = context.JK || {};
+ context.JK.SessionScreen = function(app) {
+ var logger = context.JK.logger;
+ var sessionModel = null;
+ var sessionId;
+ var tracks = {};
+ var myTracks = [];
+ var mixers = [];
+
+ var configureTrackDialog;
+ var addTrackDialog;
+ var addNewGearDialog;
+
+ var screenActive = false;
+
+ var currentMixerRangeMin = null;
+ var currentMixerRangeMax = null;
+
+ var lookingForMixersCount = 0;
+ var lookingForMixersTimer = null;
+ var lookingForMixers = {};
+
+ var RENDER_SESSION_DELAY = 750; // When I need to render a session, I have to wait a bit for the mixers to be there.
+
+ var defaultParticipant = {
+ tracks: [{
+ instrument_id: "unknown"
+ }],
+ user: {
+ first_name: 'Unknown',
+ last_name: 'User',
+ photo_url: null
+ }
+ };
+
+ // Be sure to copy/extend these instead of modifying in place
+ var trackVuOpts = {
+ vuType: "vertical",
+ lightCount: 13,
+ lightWidth: 3,
+ lightHeight: 17
+ };
+ // Must add faderId key to this
+ var trackFaderOpts = {
+ faderType: "vertical",
+ height: 83
+ };
+
+ // Recreate ChannelGroupIDs ENUM from C++
+ var ChannelGroupIds = {
+ "MasterGroup": 0,
+ "MonitorGroup": 1,
+ "AudioInputMusicGroup": 2,
+ "AudioInputChatGroup": 3,
+ "MediaTrackGroup": 4,
+ "StreamOutMusicGroup": 5,
+ "StreamOutChatGroup": 6,
+ "UserMusicInputGroup": 7,
+ "UserChatInputGroup": 8,
+ "PeerAudioInputMusicGroup": 9,
+ "PeerMediaTrackGroup": 10
+ };
+
+ // recreate eThresholdType enum from MixerDialog.h
+ var alert_type = {
+ 0: {"title": "", "message": ""}, // NO_EVENT,
+ 1: {"title": "", "message": ""}, // BACKEND_ERROR: generic error - eg P2P message error
+ 2: {"title": "", "message": ""}, // BACKEND_MIXER_CHANGE, - event that controls have been regenerated
+ 3: {"title": "", "message": ""}, // PACKET_JTR,
+ 4: { "title": "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": "", "message": ""}, // PACKET_LATE,
+ 6: {"title": "", "message": ""}, // JTR_QUEUE_DEPTH,
+ 7: {"title": "", "message": ""}, // NETWORK_JTR,
+ 8: { "title": "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": "", "message": ""}, // 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
+ //IO related events
+ 11: { "title": "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": "", "message": ""}, // INPUT_IO_JTR,
+ 13: { "title": "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": "", "message": ""}, // 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
+ 16: {"title": "", "message": ""}, // DECODE_VIOLATIONS,
+ 17: {"title": "", "message": ""} // LAST_THRESHOLD
+ };
+
+
+ function beforeShow(data) {
+ sessionId = data.id;
+ $('#session-mytracks-container').empty();
+ }
+
+ function alertCallback(type, text) {
+ if (type === 2) { // BACKEND_MIXER_CHANGE
+ sessionModel.refreshCurrentSession();
+ } else {
+ context.setTimeout(function() {
+ app.notify({
+ "title": alert_type[type].title,
+ "text": text,
+ "icon_url": "/assets/content/icon_alert_big.png"
+ }); }, 1);
+ }
+ }
+
+ function afterShow(data) {
+
+ // indicate that the screen is active, so that
+ // body-scoped drag handlers can go active
+ screenActive = true;
+
+ // Subscribe for callbacks on audio events
+ context.jamClient.RegisterVolChangeCallBack("JK.HandleVolumeChangeCallback");
+ context.jamClient.SessionRegisterCallback("JK.HandleBridgeCallback");
+ context.jamClient.SessionSetAlertCallback("JK.AlertCallback");
+
+ // If you load this page directly, the loading of the current user
+ // is happening in parallel. We can't join the session until the
+ // current user has been completely loaded. Poll for the current user
+ // before proceeding with session joining.
+ function checkForCurrentUser() {
+ if (context.JK.userMe) {
+ afterCurrentUserLoaded();
+ } else {
+ context.setTimeout(checkForCurrentUser, 100);
+ }
+ }
+ checkForCurrentUser();
+ }
+
+ function afterCurrentUserLoaded() {
+ logger.debug("afterCurrentUserLoaded");
+ // It seems the SessionModel should be a singleton.
+ // a client can only be in one session at a time,
+ // and other parts of the code want to know at any certain times
+ // about the current session, if any (for example, reconnect logic)
+ context.JK.CurrentSessionModel = sessionModel = new context.JK.SessionModel(
+ context.JK.JamServer,
+ context.jamClient
+ );
+
+ sessionModel.subscribe('sessionScreen', sessionChanged);
+ logger.debug("sessionId=" + sessionId);
+ sessionModel.joinSession(sessionId)
+ .fail(function(xhr, textStatus, errorMessage) {
+ if(xhr.status == 404) {
+ // we tried to join the session, but it's already gone. kick user back to join session screen
+ context.window.location = "#/findSession";
+ app.notify(
+ { title: "Unable to Join Session",
+ text: "The session you attempted to join is over."
+ },
+ { no_cancel: true });
+ }else {
+ app.ajaxError(xhr, textStatus, errorMessage);
+ }
+ });
+ }
+
+ function beforeHide(data) {
+ // track that the screen is inactive, to disable body-level handlers
+ screenActive = false;
+ sessionModel.leaveCurrentSession()
+ .fail(app.ajaxError);
+ }
+
+ function sessionChanged() {
+ logger.debug("sessionChanged()");
+
+ // TODO - in the specific case of a user changing their tracks using the configureTrack dialog,
+ // this event appears to fire before the underlying mixers have updated. I have no event to
+ // know definitively when the underlying mixers are up to date, so for now, we just delay slightly.
+ // This obviously has the possibility of introducing time-based bugs.
+ context.setTimeout(renderSession, RENDER_SESSION_DELAY);
+ }
+
+ /**
+ * the mixers object is a list. In order to find one by key,
+ * you must iterate. Convenience method to locate a particular
+ * mixer by id.
+ */
+ function getMixer(mixerId) {
+ var foundMixer = null;
+ $.each(mixers, function(index, mixer) {
+ if (mixer.id === mixerId) {
+ foundMixer = mixer;
+ }
+ });
+ return foundMixer;
+ }
+
+ function renderSession() {
+ $('#session-mytracks-container').empty();
+ $('.session-track').remove(); // Remove previous tracks
+ var $voiceChat = $('#voice-chat');
+ $voiceChat.hide();
+ _updateMixers();
+ _renderTracks();
+ _wireTopVolume();
+ _wireTopMix();
+ _addVoiceChat();
+ _initDialogs();
+ if ($('.session-livetracks .track').length === 0) {
+ $('.session-livetracks .when-empty').show();
+ }
+ }
+
+ function _initDialogs() {
+ logger.debug("Calling _initDialogs");
+ configureTrackDialog.initialize();
+ addTrackDialog.initialize();
+ addNewGearDialog.initialize();
+ }
+
+ // Get the latest list of underlying audio mixer channels
+ function _updateMixers() {
+ var mixerIds = context.jamClient.SessionGetIDs();
+ var holder = $.extend(true, {}, {mixers: context.jamClient.SessionGetControlState(mixerIds)});
+ mixers = holder.mixers;
+ // Always add a hard-coded simplified 'mixer' for the L2M mix
+ var l2m_mixer = {
+ id: '__L2M__',
+ range_low: -80,
+ range_high: 20,
+ volume_left: context.jamClient.SessionGetMasterLocalMix()
+ };
+ mixers.push(l2m_mixer);
+ }
+
+ // TODO FIXME - This needs to support multiple tracks for an individual
+ // client id and group.
+ function _mixerForClientId(clientId, groupIds, usedMixers) {
+ var foundMixer = null;
+ $.each(mixers, function(index, mixer) {
+ if (mixer.client_id === clientId) {
+ for (var i=0; i 20) {
+ lookingForMixersCount = 0;
+ lookingForMixers = {};
+ context.clearTimeout(lookingForMixersTimer);
+ lookingForMixersTimer = null;
+ }
+ }
+
+ // Given a mixerID and a value between 0.0-1.0,
+ // light up the proper VU lights.
+ function _updateVU(mixerId, value) {
+
+ // Special-case for mono tracks. If mono, and it's a _vul id,
+ // update both sides, otherwise do nothing.
+ // If it's a stereo track, just do the normal thing.
+ var selector;
+ var pureMixerId = mixerId.replace("_vul", "");
+ pureMixerId = pureMixerId.replace("_vur", "");
+ var mixer = getMixer(pureMixerId);
+ if (mixer) {
+ if (!(mixer.stereo)) { // mono track
+ if (mixerId.substr(-4) === "_vul") {
+ // Do the left
+ selector = '#tracks [mixer-id="' + pureMixerId + '_vul"]';
+ context.JK.VuHelpers.updateVU(selector, value);
+ // Do the right
+ selector = '#tracks [mixer-id="' + pureMixerId + '_vur"]';
+ context.JK.VuHelpers.updateVU(selector, value);
+ } // otherwise, it's a mono track, _vur event - ignore.
+ } else { // stereo track
+ selector = '#tracks [mixer-id="' + mixerId + '"]';
+ context.JK.VuHelpers.updateVU(selector, value);
+ }
+ }
+ }
+
+ function _addTrack(index, trackData) {
+ var parentSelector = '#session-mytracks-container';
+ var $destination = $(parentSelector);
+ if (trackData.clientId !== app.clientId) {
+ parentSelector = '#session-livetracks-container';
+ $destination = $(parentSelector);
+ $('.session-livetracks .when-empty').hide();
+ }
+ var template = $('#template-session-track').html();
+ var newTrack = context.JK.fillTemplate(template, trackData);
+ $destination.append(newTrack);
+
+ // Render VU meters and gain fader
+ var trackSelector = parentSelector + ' .session-track[track-id="' + trackData.trackId + '"]';
+ var gainPercent = trackData.gainPercent || 0;
+ connectTrackToMixer(trackSelector, trackData.clientId, trackData.mixerId, gainPercent);
+
+ var $closeButton = $('#div-track-close', 'div[track-id="' + trackData.trackId + '"]');
+ if (index === 0) {
+ $closeButton.hide();
+ }
+ else {
+ $closeButton.click(deleteTrack);
+ }
+
+ var $trackSettings = $('div[mixer-id="' + trackData.mixerId + '"].track-icon-settings');
+ $trackSettings.click(function() {
+ // call this to initialize Voice Chat tab
+ configureTrackDialog.showVoiceChatPanel(true);
+ configureTrackDialog.showMusicAudioPanel(true);
+ });
+
+ tracks[trackData.trackId] = new context.JK.SessionTrack(trackData.clientId);
+ }
+
+ /**
+ * Will be called when fader changes. The fader id (provided at subscribe time),
+ * the new value (0-100) and whether the fader is still being dragged are passed.
+ */
+ function faderChanged(faderId, newValue, dragging) {
+ var mixerIds = faderId.split(',');
+ $.each(mixerIds, function(i,v) {
+ var broadcast = !(dragging); // If fader is still dragging, don't broadcast
+ fillTrackVolumeObject(v, broadcast);
+ setMixerVolume(v, newValue);
+ });
+ }
+
+ function handleVolumeChangeCallback(mixerId, isLeft, value) {
+ // 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
+ var faderValue = percentFromMixerValue(-80, 20, value);
+ context.JK.FaderHelpers.setFaderValue(mixerId, faderValue);
+ }
+
+ function handleBridgeCallback() {
+ var eventName = null;
+ var mixerId = null;
+ var value = null;
+ var tuples = arguments.length / 3;
+ for (var i=0; i 100
+ if (value < min) {
+ value = min;
+ }
+ if (value > max) {
+ value = max;
+ }
+ return value;
+ }
+
+ // Given a volume percent (0-100), set the underlying
+ // audio volume level of the passed mixerId to the correct
+ // value.
+ function setMixerVolume(mixerId, 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.
+ var sliderValue = percentToMixerValue(
+ currentMixerRangeMin, currentMixerRangeMax, volumePercent);
+ context.trackVolumeObject.volL = sliderValue;
+ context.trackVolumeObject.volR = sliderValue;
+ // Special case for L2M mix:
+ if (mixerId === '__L2M__') {
+ logger.debug("L2M volumePercent=" + volumePercent);
+ var dbValue = context.JK.FaderHelpers.convertLinearToDb(volumePercent);
+ context.jamClient.SessionSetMasterLocalMix(dbValue);
+ // context.jamClient.SessionSetMasterLocalMix(sliderValue);
+ } else {
+ context.jamClient.SessionSetControlState(mixerId);
+ }
+ }
+
+ function sessionResync(evt) {
+ evt.preventDefault();
+ var response = context.jamClient.FTUESave(true);
+ if (response) {
+ app.notify({
+ "title": "Error",
+ "text": response,
+ "icon_url": "/assets/content/icon_alert_big.png"});
+ }
+ return false;
+ }
+
+ function events() {
+ $('#session-resync').on('click', sessionResync);
+ $('#session-contents').on("click", '[action="delete"]', deleteSession);
+ $('#tracks').on('click', 'div[control="mute"]', toggleMute);
+
+ $('.voicechat-settings').click(function() {
+ // call this to initialize Music Audio tab
+ configureTrackDialog.showMusicAudioPanel(true);
+ configureTrackDialog.showVoiceChatPanel(true);
+ });
+ }
+
+ this.initialize = function() {
+ context.jamClient.SetVURefreshRate(150);
+ events();
+ var screenBindings = {
+ 'beforeShow': beforeShow,
+ 'afterShow': afterShow,
+ 'beforeHide': beforeHide
+ };
+ app.bindScreen('session', screenBindings);
+ };
+
+ this.tracks = tracks;
+
+ this.getCurrentSession = function() {
+ return sessionModel.getCurrentSession();
+ };
+
+ this.refreshCurrentSession = function() {
+ sessionModel.refreshCurrentSession();
+ };
+
+ context.JK.HandleVolumeChangeCallback = handleVolumeChangeCallback;
+ context.JK.HandleBridgeCallback = handleBridgeCallback;
+ context.JK.AlertCallback = alertCallback;
+
+ };
+
+ })(window,jQuery);
\ No newline at end of file
diff --git a/web/app/assets/javascripts/sessionLatency.js b/web/app/assets/javascripts/sessionLatency.js
new file mode 100644
index 000000000..b0bb0dee6
--- /dev/null
+++ b/web/app/assets/javascripts/sessionLatency.js
@@ -0,0 +1,141 @@
+(function(context, $) {
+
+ "use strict";
+
+ context.JK = context.JK || {};
+
+ context.JK.SessionLatency = function(jamClient) {
+
+ var logger = context.JK.logger;
+ var sessionPingsOut = {};
+ var clientsToSessions = {};
+ var sessionLatency = {};
+ var subscribers = {};
+
+ function getSortScore(sessionId) {
+ return sessionLatency[sessionId].sortScore;
+ }
+
+ function ensureSessionLatencyEntry(sessionId) {
+ if (!(sessionId in sessionLatency)) {
+ sessionLatency[sessionId] = {
+ clientLatencies: {},
+ averageLatency: 0
+ };
+ }
+ }
+
+ function setInitialSortScore(session) {
+ var i,
+ p,
+ score = 0,
+ participant = null;
+
+ // user has invitations for this session
+ if ("invitations" in session) {
+ score += 2;
+ }
+ for (i=0, p=session.participants.length; i LATENCY.MEDIUM.min && totalLatency <= LATENCY.MEDIUM.max) {
+ latencyDescription = LATENCY.MEDIUM.description;
+ latencyStyle = LATENCY.MEDIUM.style;
+ }
+ else {
+ latencyDescription = LATENCY.POOR.description;
+ latencyStyle = LATENCY.POOR.style;
+ showJoinLink = false;
+ }
+ }
+
+ // audience
+ var audience = AUDIENCE.OPEN_TO_FANS;
+ if (!(session.fan_access)) {
+ audience = AUDIENCE.MUSICIANS_ONLY;
+ }
+
+ var i, participant = null;
+ var musicians = '';
+ var musicianArray = [];
+ for (i=0; i < session.participants.length; i++) {
+ participant = session.participants[i];
+
+ var instrumentLogoHtml = '';
+ var j;
+
+ // loop through the tracks to get the instruments
+ for (j=0; j < participant.tracks.length; j++) {
+ var track = participant.tracks[j];
+ logger.debug("Find:Finding instruments. Participant tracks:");
+ logger.debug(participant.tracks);
+ var inst = '../assets/content/icon_instrument_default24.png';
+ if (track.instrument_id in instrument_logo_map) {
+ inst = instrument_logo_map[track.instrument_id];
+ }
+ instrumentLogoHtml += '
';
+ }
+
+ var id = participant.user.id;
+ var name = participant.user.name;
+ var photoUrl = context.JK.resolveAvatarUrl(participant.user.photo_url);
+ var musicianVals = {
+ avatar_url: photoUrl,
+ profile_url: "/#/profile/" + id,
+ musician_name: name,
+ instruments: instrumentLogoHtml
+ };
+
+ var musician = {};
+ musician.id = id;
+ musician.name = name;
+ musicianArray[i] = musician;
+
+ var musicianInfo = context.JK.fillTemplate(musicianTemplate, musicianVals);
+ musicians += musicianInfo;
+ }
+
+ onMusiciansComplete(musicianArray);
+
+ var sessionVals = {
+ id: session.id,
+ genres: session.genres.join (', '),
+ description: session.description || "(No description)",
+ musician_template: musicians,
+ audience: audience,
+ latency_text: latencyDescription,
+ latency_style: latencyStyle,
+ sortScore: latencyInfo.sortScore,
+ play_url: "TODO",
+ join_url: "/#/session/" + session.id,
+ join_link_display_style: "block" // showJoinLink ? "block" : "none"
+ };
+
+ var row = context.JK.fillTemplate(rowTemplate, sessionVals);
+ var insertedEarly = false;
+ $.each($('tr', tbGroup), function(index, nextRow) {
+ var $nextRow = $(nextRow);
+ var rowSortScore = parseInt($nextRow.attr('data-sortScore'), 10);
+ if (sessionVals.sortScore > rowSortScore) {
+ $nextRow.before(row);
+ insertedEarly = true;
+ return false; // break
+ }
+ });
+ if (!insertedEarly) {
+ return $(tbGroup).append(row);
+ }
+ }
+
+ function events() {
+ }
+
+ this.renderSession = renderSession;
+
+ return this;
+ };
+
+ })(window,jQuery);
\ No newline at end of file
diff --git a/web/app/assets/javascripts/sessionModel.js b/web/app/assets/javascripts/sessionModel.js
new file mode 100644
index 000000000..7677b95a2
--- /dev/null
+++ b/web/app/assets/javascripts/sessionModel.js
@@ -0,0 +1,416 @@
+// The session model contains information about the music
+// sessions that the current client has joined.
+(function(context,$) {
+
+ "use strict";
+
+ context.JK = context.JK || {};
+ var logger = context.JK.logger;
+
+ context.JK.SessionModel = function(server, client) {
+ var clientId = client.clientID;
+ var currentSessionId = null; // Set on join, prior to setting currentSession.
+ var currentSession = null;
+ var subscribers = {};
+ var users = {}; // User info for session participants
+ var rest = context.JK.Rest();
+
+ function id() {
+ return currentSession.id;
+ }
+
+ function participants() {
+ if (currentSession) {
+ return currentSession.participants;
+ } else {
+ return [];
+ }
+ }
+
+ /**
+ * Join the session specified by the provided id.
+ */
+ function joinSession(sessionId) {
+ currentSessionId = sessionId;
+ logger.debug("SessionModel.joinSession(" + sessionId + ")");
+ var deferred = joinSessionRest(sessionId);
+
+ deferred
+ .done(function(){
+ logger.debug("calling jamClient.JoinSession");
+ client.JoinSession({ sessionID: sessionId });
+ refreshCurrentSession();
+ server.registerMessageCallback(context.JK.MessageType.MUSICIAN_SESSION_JOIN, refreshCurrentSession);
+ server.registerMessageCallback(context.JK.MessageType.MUSICIAN_SESSION_DEPART, refreshCurrentSession);
+ })
+ .fail(function() {
+ currentSessionId = null;
+ });
+
+ return deferred;
+ }
+
+ /**
+ * Leave the current session, if there is one.
+ * callback: called in all conditions; either after an attempt is made to tell the server that we are leaving,
+ * or immediately if there is no session
+ */
+ function leaveCurrentSession() {
+ var deferred;
+
+ if(currentSessionId) {
+ logger.debug("SessionModel.leaveCurrentSession()");
+ // TODO - sessionChanged will be called with currentSession = null
+ server.unregisterMessageCallback(context.JK.MessageType.MUSICIAN_SESSION_JOIN, refreshCurrentSession);
+ server.unregisterMessageCallback(context.JK.MessageType.MUSICIAN_SESSION_DEPART, refreshCurrentSession);
+ // 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("calling jamClient.LeaveSession for clientId=" + clientId);
+ client.LeaveSession({ sessionID: currentSessionId });
+ deferred = leaveSessionRest(currentSessionId);
+ deferred.done(function() {
+ sessionChanged();
+ });
+
+ // 'unregister' for callbacks
+ context.jamClient.SessionRegisterCallback("");
+ context.jamClient.SessionSetAlertCallback("");
+ currentSession = null;
+ currentSessionId = null;
+ }
+ else {
+ deferred = new $.Deferred();
+ deferred.resolve();
+ }
+
+ return deferred;
+ }
+
+ /**
+ * Refresh the current session, and participants.
+ */
+ function refreshCurrentSession() {
+ logger.debug("SessionModel.refreshCurrentSession()");
+ refreshCurrentSessionRest(function() {
+ refreshCurrentSessionParticipantsRest(sessionChanged);
+ });
+ }
+
+ /**
+ * Subscribe for sessionChanged events. Provide a subscriberId
+ * and a callback to be invoked on session changes.
+ */
+ function subscribe(subscriberId, sessionChangedCallback) {
+ logger.debug("SessionModel.subscribe(" + subscriberId + ", [callback])");
+ subscribers[subscriberId] = sessionChangedCallback;
+ }
+
+ /**
+ * Notify subscribers that the current session has changed.
+ */
+ function sessionChanged() {
+ logger.debug("SessionModel.sessionChanged()");
+ for (var subscriberId in subscribers) {
+ subscribers[subscriberId]();
+ }
+ }
+
+ /**
+ * Reload the session data from the REST server, calling
+ * the provided callback when complete.
+ */
+ function refreshCurrentSessionRest(callback) {
+ var url = "/api/sessions/" + currentSessionId;
+ $.ajax({
+ type: "GET",
+ url: url,
+ async: false,
+ success: function(response) {
+ sendClientParticipantChanges(currentSession, response);
+ logger.debug("Current Session Refreshed:");
+ logger.debug(response);
+ currentSession = response;
+ callback();
+ },
+ error: ajaxError
+ });
+ }
+
+ /**
+ * Seems silly. We should just have the bridge take sessionId, clientId
+ */
+ function _toJamClientParticipant(participant) {
+ return {
+ userID : "",
+ clientID : participant.client_id,
+ tcpPort : 0,
+ udpPort : 0,
+ localIPAddress : participant.ip_address, // ?
+ globalIPAddress : participant.ip_address, // ?
+ latency : 0,
+ natType : ""
+ };
+ }
+
+ function sendClientParticipantChanges(oldSession, newSession) {
+ var joins = [], leaves = []; // Will hold JamClientParticipants
+
+ var oldParticipants = []; // will be set to session.participants if session
+ var oldParticipantIds = [];
+ var newParticipants = [];
+ var newParticipantIds = [];
+
+ if (oldSession && oldSession.participants) {
+ oldParticipants = oldSession.participants;
+ $.each(oldParticipants, function() {
+ oldParticipantIds.push(this.client_id);
+ });
+ }
+ if (newSession && newSession.participants) {
+ newParticipants = newSession.participants;
+ $.each(newParticipants, function() {
+ newParticipantIds.push(this.client_id);
+ });
+ }
+
+ $.each(newParticipantIds, function(i,v) {
+ if ($.inArray(v, oldParticipantIds) === -1) {
+ // new participant id that's not in old participant ids: Join
+ joins.push(_toJamClientParticipant(newParticipants[i]));
+ }
+ });
+ $.each(oldParticipantIds, function(i,v) {
+ if ($.inArray(v, newParticipantIds) === -1) {
+ // old participant id that's not in new participant ids: Leave
+ leaves.push(_toJamClientParticipant(oldParticipants[i]));
+ }
+ });
+
+ $.each(joins, function(i,v) {
+ if (v.client_id != clientId) {
+ client.ParticipantJoined(newSession, v);
+ }
+ });
+ $.each(leaves, function(i,v) {
+ if (v.client_id != clientId) {
+ client.ParticipantLeft(newSession, v);
+ }
+ });
+ }
+
+ /**
+ * Ensure that we have user info for all current participants.
+ */
+ function refreshCurrentSessionParticipantsRest(callback) {
+ var callCount = 0;
+ $.each(participants(), function(index, value) {
+ if (!(this.user.id in users)) {
+ var userInfoUrl = "/api/users/" + this.user.id;
+ callCount += 1;
+ $.ajax({
+ type: "GET",
+ url: userInfoUrl,
+ async: false,
+ success: function(user) {
+ callCount -= 1;
+ users[user.id] = user;
+ },
+ error: function(jqXHR, textStatus, errorThrown) {
+ callCount -= 1;
+ logger.error('Error getting user info from ' + userInfoUrl);
+ }
+ });
+ }
+ });
+ if (!(callback)) {
+ return;
+ }
+ context.JK.joinCalls(
+ function() { return callCount === 0; }, callback, 10);
+ }
+
+ function participantForClientId(clientId) {
+ var foundParticipant = null;
+ $.each(currentSession.participants, function(index, participant) {
+ if (participant.client_id === clientId) {
+ foundParticipant = participant;
+ return false;
+ }
+ });
+ return foundParticipant;
+ }
+
+ function addTrack(sessionId, data) {
+ logger.debug("track data = " + JSON.stringify(data));
+ var url = "/api/sessions/" + sessionId + "/tracks";
+ $.ajax({
+ type: "POST",
+ dataType: "json",
+ contentType: 'application/json',
+ url: url,
+ async: false,
+ data: JSON.stringify(data),
+ processData:false,
+ success: function(response) {
+ // save to the backend
+ context.jamClient.TrackSaveAssignments();
+ logger.debug("Successfully added track (" + JSON.stringify(data) + ")");
+ refreshCurrentSession();
+ },
+ error: ajaxError
+ });
+ }
+
+ function updateTrack(sessionId, trackId, data) {
+ var url = "/api/sessions/" + sessionId + "/tracks/" + trackId;
+ $.ajax({
+ type: "POST",
+ dataType: "json",
+ contentType: 'application/json',
+ url: url,
+ async: false,
+ data: JSON.stringify(data),
+ processData:false,
+ success: function(response) {
+ logger.debug("Successfully updated track info (" + JSON.stringify(data) + ")");
+ },
+ error: ajaxError
+ });
+ }
+
+ function deleteTrack(sessionId, trackId) {
+ if (trackId) {
+ $.ajax({
+ type: "DELETE",
+ url: "/api/sessions/" + sessionId + "/tracks/" + trackId,
+ async: false,
+ success: function(response) {
+ // TODO: if in recording, more cleanup to do???
+
+ // update backend client (for now, only the second track can be removed)
+ client.TrackSetCount(1);
+ client.TrackSaveAssignments();
+
+ // refresh Session screen
+ refreshCurrentSession();
+ },
+ error: function(jqXHR, textStatus, errorThrown) {
+ logger.error("Error deleting track " + trackId);
+ }
+ });
+ }
+ }
+
+ /**
+ * Make the server calls to join the current user to
+ * the session provided.
+ */
+ function joinSessionRest(sessionId) {
+ var tracks = context.JK.TrackHelpers.getUserTracks(context.jamClient);
+ var data = {
+ client_id: clientId,
+ ip_address: server.publicIP,
+ as_musician: true,
+ tracks: tracks
+ };
+ var url = "/api/sessions/" + sessionId + "/participants";
+ return $.ajax({
+ type: "POST",
+ dataType: "json",
+ contentType: 'application/json',
+ url: url,
+ async: true,
+ data: JSON.stringify(data),
+ processData:false
+ });
+ }
+
+ function leaveSessionRest(sessionId) {
+ var url = "/api/participants/" + clientId;
+ return $.ajax({
+ type: "DELETE",
+ url: url,
+ async: true
+ });
+ }
+
+ function reconnect() {
+ context.JK.CurrentSessionModel.leaveCurrentSession()
+ .always(function() {
+ window.location.reload();
+ });
+ }
+
+ function registerReconnect(content) {
+ $('a.disconnected-reconnect', content).click(function() {
+
+ var template = $('#template-reconnecting').html();
+ var templateHtml = context.JK.fillTemplate(template, null);
+ var content = context.JK.Banner.show({
+ html : template
+ });
+
+ rest.serverHealthCheck()
+ .done(function() {
+ reconnect();
+ })
+ .fail(function(xhr, textStatus, errorThrown) {
+
+ if(xhr && xhr.status >= 100) {
+ // we could connect to the server, and it's alive
+ reconnect();
+ }
+ else {
+ var template = $('#template-could-not-reconnect').html();
+ var templateHtml = context.JK.fillTemplate(template, null);
+ var content = context.JK.Banner.show({
+ html : template
+ });
+
+ registerReconnect(content);
+ }
+ });
+
+ return false;
+ });
+ }
+
+ function onWebsocketDisconnected(in_error) {
+
+ // kill the streaming of the session immediately
+ logger.debug("calling jamClient.LeaveSession for clientId=" + clientId);
+ client.LeaveSession({ sessionID: currentSessionId });
+
+ if(in_error) {
+ var template = $('#template-disconnected').html();
+ var templateHtml = context.JK.fillTemplate(template, null);
+ var content = context.JK.Banner.show({
+ html : template
+ }) ;
+ registerReconnect(content);
+ }
+ }
+
+ function ajaxError(jqXHR, textStatus, errorMessage) {
+ logger.error("Unexpected ajax error: " + textStatus);
+ }
+
+ // Public interface
+ this.id = id;
+ this.participants = participants;
+ this.joinSession = joinSession;
+ this.leaveCurrentSession = leaveCurrentSession;
+ this.refreshCurrentSession = refreshCurrentSession;
+ this.subscribe = subscribe;
+ this.participantForClientId = participantForClientId;
+ this.addTrack = addTrack;
+ this.updateTrack = updateTrack;
+ this.deleteTrack = deleteTrack;
+ this.onWebsocketDisconnected = onWebsocketDisconnected;
+ this.getCurrentSession = function() {
+ return currentSession;
+ };
+ };
+
+ })(window,jQuery);
\ No newline at end of file
diff --git a/web/app/assets/javascripts/sessionSettingsDialog.js b/web/app/assets/javascripts/sessionSettingsDialog.js
new file mode 100644
index 000000000..d1558d4c4
--- /dev/null
+++ b/web/app/assets/javascripts/sessionSettingsDialog.js
@@ -0,0 +1,119 @@
+/**
+* Javascript for the session settings dialog.
+*/
+(function(context,$) {
+
+ context.JK = context.JK || {};
+ context.JK.SessionSettingsDialog = function(app, sessionScreen) {
+ var logger = context.JK.logger;
+ var $dialog;
+ var rest = new JK.Rest();
+
+ function beforeShow(data) {
+ context.JK.GenreSelectorHelper.render('#session-settings-genre');
+ $dialog = $('[layout-id="session-settings"]');
+ var currentSession = sessionScreen.getCurrentSession();
+ context.JK.GenreSelectorHelper.setSelectedGenres('#session-settings-genre', currentSession.genres);
+ // dynamic object binding to form.
+ // TODO: Generalize, test and bundle with formToObject
+ // It's the other direction... given an objects -- set the form's
+ // inputs/values to the corresponding object properties.
+ var skip = [
+ 'band_id', // not used
+ 'user_id', // not used
+ 'participants', // has its own API
+ 'invitations', // has its own API
+ 'join_requests', // has its own API
+ 'genres' // handled specifically
+ ];
+ var radioButtons = [
+ 'approval_required',
+ 'fan_chat'
+ ];
+ $.each(_.keys(currentSession), function(index,propName) {
+ if (context._.contains(skip, propName)) {
+ return true; // "continue"
+ }
+
+ var isRadio = (context._.contains(radioButtons, propName));
+
+ var desiredValue = null;
+ if ($.isArray(currentSession[propName])) {
+ desiredValue = currentSession[propName].join(',');
+ } else {
+ desiredValue = currentSession[propName];
+ }
+ desiredValue = String(desiredValue);
+
+ var inputSelector = '[name="' + propName + '"]';
+ var $input = [];
+
+ // radio buttons must be handled differently
+ if (isRadio) {
+ inputSelector += '[value="' + desiredValue + '"]';
+ $(inputSelector).removeAttr('checked');
+ $input = $(inputSelector, $dialog);
+ $input.prop('checked', true).change();
+ } else {
+ $input = $(inputSelector, $dialog);
+ $input.val(desiredValue).change();
+ }
+ });
+ }
+
+ function musicianAccessChanged(evt) {
+ $dialog = $('[layout-id="session-settings"]');
+ var hasMusicianAccess = $('select[name="musician_access"]', $dialog).val(); // string
+ hasMusicianAccess = context.JK.stringToBool(hasMusicianAccess);
+ if (hasMusicianAccess) {
+ $('input[name="approval_required"]', $dialog).removeAttr("disabled");
+ } else {
+ $('input[name="approval_required"]', $dialog).attr("disabled", "disabled");
+ }
+ }
+
+ function fanAccessChanged(evt) {
+ $dialog = $('[layout-id="session-settings"]');
+ var hasFanAccess = $('select[name="fan_access"]', $dialog).val(); // string
+ hasFanAccess = context.JK.stringToBool(hasFanAccess);
+ if (hasFanAccess) {
+ $('input[name="fan_chat"]', $dialog).removeAttr("disabled");
+ } else {
+ $('input[name="fan_chat"]', $dialog).attr("disabled", "disabled");
+ }
+ }
+
+ function saveSettings(evt) {
+ var newSessionInfo = $('#session-settings-dialog').formToObject();
+ var id = newSessionInfo.id;
+ delete newSessionInfo.id;
+ if (typeof newSessionInfo.genres === "string") {
+ newSessionInfo.genres = [newSessionInfo.genres];
+ }
+ rest.updateSession(id, newSessionInfo, settingsSaved);
+ }
+
+ function settingsSaved(response) {
+ // No response returned from this call. 204.
+ sessionScreen.refreshCurrentSession();
+ app.layout.closeDialog('session-settings');
+ }
+
+ function events() {
+ $('#session-settings-dialog-submit').on('click', saveSettings);
+ $('#session-settings-dialog select[name="fan_access"]').on('change', fanAccessChanged);
+ $('#session-settings-dialog select[name="musician_access"]').on('change', musicianAccessChanged);
+ }
+
+ this.initialize = function() {
+ events();
+ var dialogBindings = {
+ 'beforeShow': beforeShow
+ };
+ app.bindDialog('session-settings', dialogBindings);
+ };
+ };
+
+
+
+ })(window,jQuery);
\ No newline at end of file
diff --git a/web/app/assets/javascripts/sessionTrack.js b/web/app/assets/javascripts/sessionTrack.js
new file mode 100644
index 000000000..35316ca24
--- /dev/null
+++ b/web/app/assets/javascripts/sessionTrack.js
@@ -0,0 +1,90 @@
+(function(context,$) {
+
+ "use strict";
+
+ context.JK = context.JK || {};
+ context.JK.SessionTrack = function(clientId) {
+ var logger = context.JK.logger;
+ var selectorTemplate = '[client-id="' + clientId + '"] [track-role="{role}"]';
+ var parts = ['latency', 'vu', 'gain', 'mute', 'name', 'part', 'avatar'];
+ var $parts = {};
+ $.each(parts, function() {
+ var selector = context.JK.fillTemplate(selectorTemplate, {role: this});
+ $parts[this] = $(selector);
+ //logger.debug(this + ":" + $parts[this]);
+ });
+
+ function events() {}
+
+ /**
+ * Set latency. val = [good,medium,bad]
+ */
+ this.setLatency = function(val) {
+ $parts.latency.html(val);
+ };
+
+ /**
+ * Set VU level. val = 0.0-1.0
+ */
+ this.setVolumeUnit = function(val) {
+ $parts.vu.html(val);
+ };
+
+ /**
+ * Set the track's gain. val = 0.0-1.0
+ * Allows external control of channel fader.
+ */
+ this.setGain = function(val) {
+ logger.debug('setGain:' + val);
+ $parts.gain.html("Gain: " + val);
+ };
+
+ /**
+ * Get the track's gain from current fader. Returns 0.0-1.0
+ */
+ this.getGain = function() {
+ return $parts.gain.html().split("Gain: ")[1];
+ };
+
+ /**
+ * Set whether this channel is muted. Takes a boolean where
+ * true means mute, false, means unmuted.
+ */
+ function _mute(muted) {
+ }
+
+ /**
+ * Return whether the channel is currently muted.
+ */
+ function _isMute() {
+ }
+
+ /**
+ * Set the name (typically user name)
+ */
+ function _setName(name) {
+ }
+
+ /**
+ * Set the part this user is performing. If part is
+ * one of ENUM.FIGURE_ME_OUT then it is a recognized
+ * part with an icon, otherwise it is an 'other' value
+ * with a default icon.
+ */
+ function _setPart(part) {
+ }
+
+ /**
+ * Set the channel's avatar. Typically the user's profile
+ * avatar url.
+ */
+ function _setAvatar(avatar_url) {
+ }
+
+ this.initialize = function() {
+ events();
+ };
+
+ };
+
+ })(window,jQuery);
\ No newline at end of file
diff --git a/web/app/assets/javascripts/sessions.js.coffee b/web/app/assets/javascripts/sessions.js.coffee
new file mode 100644
index 000000000..761567942
--- /dev/null
+++ b/web/app/assets/javascripts/sessions.js.coffee
@@ -0,0 +1,3 @@
+# Place all the behaviors and hooks related to the matching controller here.
+# All this logic will automatically be available in application.js.
+# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/
diff --git a/web/app/assets/javascripts/sidebar.js b/web/app/assets/javascripts/sidebar.js
new file mode 100644
index 000000000..948aceba1
--- /dev/null
+++ b/web/app/assets/javascripts/sidebar.js
@@ -0,0 +1,499 @@
+(function(context,$) {
+
+ "use strict";
+
+ context.JK = context.JK || {};
+ context.JK.Sidebar = function(app) {
+ var logger = context.JK.logger;
+ var friends = [];
+ var notifications = [];
+
+ function initializeFriendsPanel() {
+
+ /////////////////////////////////////////////////////////////
+ // THIS IS TEST CODE TO GENERATE BACK TO BACK NOTIFICATIONS
+ // app.notify({
+ // "title": "TEST 1",
+ // "text": "Test 1",
+ // "icon_url": context.JK.resolveAvatarUrl("")
+ // });
+
+ // app.notify({
+ // "title": "TEST 2",
+ // "text": "Test 2",
+ // "icon_url": context.JK.resolveAvatarUrl("")
+ // });
+
+ // app.notify({
+ // "title": "TEST 3",
+ // "text": "Test 3",
+ // "icon_url": context.JK.resolveAvatarUrl("")
+ // });
+ /////////////////////////////////////////////////////////////
+
+ $('#sidebar-search-header').hide();
+
+ var url = "/api/users/" + context.JK.currentUserId + "/friends"
+ $.ajax({
+ type: "GET",
+ dataType: "json",
+ contentType: 'application/json',
+ url: url,
+ processData: false,
+ success: function(response) {
+
+ friends = response;
+ updateFriendList(response);
+
+ // set friend count
+ $('#sidebar-friend-count').html(response.length);
+ },
+ error: app.ajaxError
+ });
+
+ return false;
+ }
+
+ function updateFriendList(response) {
+ $('#sidebar-friend-list').empty();
+
+ // show online friends first (sort by first name within online/offline groups)
+ response.sort(function(a, b) {
+
+ var a_online = a.online;
+ var b_online = b.online;
+
+ var a_firstname = a.first_name.toLowerCase();
+ var b_firstname = b.first_name.toLowerCase();
+
+ if (b_online != a_online) {
+ if (b_online < a_online) return -1;
+ if (b_online > a_online) return 1;
+ return 0;
+ }
+
+ if (a_firstname < b_firstname) return -1;
+ if (a_firstname > b_firstname) return 1;
+ return 0;
+ });
+
+ $.each(response, function(index, val) {
+
+ var css = val.online ? '' : 'offline';
+
+ friends[val.id] = val;
+
+ // fill in template for Connect pre-click
+ var template = $('#template-friend-panel').html();
+ var searchResultHtml = context.JK.fillTemplate(template, {
+ userId: val.id,
+ cssClass: css,
+ avatar_url: context.JK.resolveAvatarUrl(val.photo_url),
+ userName: val.name,
+ status: val.online ? 'Available' : 'Offline',
+ extra_info: '',
+ info_image_url: ''
+ });
+
+ $('#sidebar-friend-list').append(searchResultHtml);
+ });
+ }
+
+ function initializeNotificationsPanel() {
+ // retrieve pending notifications for this user
+ var url = "/api/users/" + context.JK.currentUserId + "/notifications"
+ $.ajax({
+ type: "GET",
+ dataType: "json",
+ contentType: 'application/json',
+ url: url,
+ processData: false,
+ success: function(response) {
+
+ notifications = response;
+ updateNotificationList(response);
+
+ // set notification count
+ $('#sidebar-notification-count').html(response.length);
+ },
+ error: app.ajaxError
+ });
+ }
+
+ function updateNotificationList(response) {
+ $('#sidebar-notification-list').empty();
+
+ $.each(response, function(index, val) {
+
+ notifications[val.notification_id] = val;
+
+ // fill in template for Connect pre-click
+ var template = $('#template-notification-panel').html();
+ var notificationHtml = context.JK.fillTemplate(template, {
+ notificationId: val.notification_id,
+ avatar_url: context.JK.resolveAvatarUrl(val.photo_url),
+ text: val.formatted_msg,
+ date: context.JK.formatDate(val.created_at)
+ });
+
+ $('#sidebar-notification-list').append(notificationHtml);
+
+ initializeActions(val, val.description);
+ });
+ }
+
+ function initializeActions(notification, type) {
+ // wire up "x" button to delete notification
+ $('li[notification-id=' + notification.notification_id + ']').find('#img-delete-notification').click(deleteNotificationHandler);
+
+ if (type === context.JK.MessageType.FRIEND_REQUEST) {
+ var $action_btn = $('li[notification-id=' + notification.notification_id + ']').find('#btn-notification-action');
+ $action_btn.text('ACCEPT');
+ $action_btn.click(function() {
+ acceptFriendRequest({ "friend_request_id": notification.friend_request_id, "notification_id": notification.notification_id });
+ });
+ }
+ else if (type === context.JK.MessageType.FRIEND_REQUEST_ACCEPTED) {
+ $('li[notification-id=' + notification.notification_id + ']').find('#div-actions').hide();
+ }
+ }
+
+ function deleteNotificationHandler(evt) {
+ evt.stopPropagation();
+ var notificationId = $(this).attr('notification-id');
+ deleteNotification(notificationId);
+ }
+
+ function deleteNotification(notificationId) {
+ var url = "/api/users/" + context.JK.currentUserId + "/notifications/" + notificationId;
+ $.ajax({
+ type: "DELETE",
+ dataType: "json",
+ contentType: 'application/json',
+ url: url,
+ processData: false,
+ success: function(response) {
+ delete notifications[notificationId];
+ $('li[notification-id=' + notificationId + ']').hide();
+ decrementNotificationCount();
+ },
+ error: app.ajaxError
+ });
+ }
+
+ function initializeChatPanel() {
+ }
+
+ function search(query) {
+
+ logger.debug('query=' + query);
+ if (query !== '') {
+ context.JK.search(query, app, onSearchSuccess);
+ }
+ }
+
+ function onSearchSuccess(response) {
+
+ // TODO: generalize this for each search result type (band, musician, recordings, et. al.)
+ $.each(response.musicians, function(index, val) {
+ // fill in template for Connect pre-click
+ var template = $('#template-sidebar-search-result').html();
+ var searchResultHtml = context.JK.fillTemplate(template, {
+ userId: val.id,
+ avatar_url: context.JK.resolveAvatarUrl(val.photo_url),
+ profile_url: "/#/profile/" + val.id,
+ userName: val.name,
+ location: val.location
+ });
+
+ $('#sidebar-search-results').append(searchResultHtml);
+
+ // fill in template for Connect post-click
+ template = $('#template-sidebar-invitation-sent').html();
+ var invitationSentHtml = context.JK.fillTemplate(template, {
+ userId: val.id,
+ first_name: val.first_name,
+ profile_url: "/#/profile/" + val.id
+ });
+
+ $('#sidebar-search-results').append(invitationSentHtml);
+
+ // initialize visibility of the divs
+ $('div[layout=sidebar] div[user-id=' + val.id + '].sidebar-search-connected').hide();
+ $('div[layout=sidebar] div[user-id=' + val.id + '].sidebar-search-result').show();
+
+ // wire up button click handler if search result is not a friend or the current user
+ if (!val.is_friend && val.id !== context.JK.currentUserId) {
+ $('div[layout=sidebar] div[user-id=' + val.id + ']').find('#btn-connect-friend').click(sendFriendRequest);
+ }
+ // hide the button if the search result is already a friend
+ else {
+ $('div[layout=sidebar] div[user-id=' + val.id + ']').find('#btn-connect-friend').hide();
+ }
+ });
+
+ // show header
+ $('#sidebar-search-header').show();
+
+ // hide panels
+ $('[layout-panel="contents"]').hide();
+ $('[layout-panel="contents"]').css({"height": "1px"});
+
+ // resize search results area
+ $('#sidebar-search-results').height(getHeight() + 'px');
+ }
+
+ function getHeight() {
+ // TODO: refactor this - copied from layout.js
+ var sidebarHeight = $(context).height() - 75 - 2 * 60 + $('[layout-sidebar-expander]').height();
+ var combinedHeaderHeight = $('[layout-panel="contents"]').length * 36;
+ var searchHeight = $('.sidebar .search').first().height();
+ var expanderHeight = $('[layout-sidebar-expander]').height();
+ var expandedPanelHeight = sidebarHeight - (combinedHeaderHeight + expanderHeight + searchHeight);
+ return expandedPanelHeight;
+ }
+
+ function showFriendsPanel() {
+
+ var $expandedPanelContents = $('[layout-id="panelFriends"] [layout-panel="contents"]');
+ var expandedPanelHeight = getHeight();
+
+ // hide all other contents
+ $('[layout-panel="contents"]').hide();
+ $('[layout-panel="contents"]').css({"height": "1px"});
+
+ // show the appropriate contens
+ $expandedPanelContents.show();
+ $expandedPanelContents.animate({"height": expandedPanelHeight + "px"}, 400);
+ }
+
+ function friendRequestCallback(userId) {
+ // toggle the pre-click and post-click divs
+ $('div[layout=sidebar] div[user-id=' + userId + '].sidebar-search-connected').show();
+ $('div[layout=sidebar] div[user-id=' + userId + '].sidebar-search-result').hide();
+ }
+
+ function sendFriendRequest(evt) {
+ evt.stopPropagation();
+ var userId = $(this).parent().attr('user-id');
+ context.JK.sendFriendRequest(app, userId, friendRequestCallback);
+ }
+
+ function hideSearchResults() {
+ emptySearchResults();
+ $('#search-input').val('');
+ $('#sidebar-search-header').hide();
+ showFriendsPanel();
+ }
+
+ function emptySearchResults() {
+ $('#sidebar-search-results').empty();
+ $('#sidebar-search-results').height('0px');
+ }
+
+ function incrementNotificationCount() {
+ var count = parseInt($('#sidebar-notification-count').html());
+ $('#sidebar-notification-count').html(count + 1);
+ }
+
+ function decrementNotificationCount() {
+ var count = parseInt($('#sidebar-notification-count').html());
+ $('#sidebar-notification-count').html(count - 1);
+ }
+
+ function acceptFriendRequest(args) {
+ var friend_request = {};
+ friend_request.status = 'accept';
+
+ var jsonData = JSON.stringify(friend_request);
+
+ var url = "/api/users/" + context.JK.currentUserId + "/friend_requests/" + args.friend_request_id;
+ $.ajax({
+ type: "POST",
+ dataType: "json",
+ contentType: 'application/json',
+ url: url,
+ data: jsonData,
+ processData: false,
+ success: function(response) {
+ deleteNotification(args.notification_id); // delete notification corresponding to this friend request
+ initializeFriendsPanel(); // refresh friends panel when request is accepted
+ },
+ error: app.ajaxError
+ });
+ }
+
+ // default handler for incoming notification
+ function handleNotification(payload, type) {
+ // update notifications panel in sidebar
+ notifications[payload.notification_id] = {
+ "id": payload.notification_id,
+ "photo_url": payload.photo_url,
+ "formatted_msg": payload.msg,
+ "created_at": context.JK.formatDate(payload.created_at)
+ };
+
+ incrementNotificationCount();
+
+ var template = $("#template-notification-panel").html();
+ var notificationHtml = context.JK.fillTemplate(template, {
+ notificationId: payload.notification_id,
+ avatar_url: context.JK.resolveAvatarUrl(payload.photo_url),
+ text: payload.msg,
+ date: context.JK.formatDate(payload.created_at)
+ });
+
+ $('#sidebar-notification-list').prepend(notificationHtml);
+
+ initializeActions(payload, type);
+ }
+
+ var delay = (function(){
+ var timer = 0;
+ return function(callback, ms) {
+ clearTimeout(timer);
+ timer = setTimeout(callback, ms);
+ };
+ })();
+
+ function events() {
+ $('#search-input').keyup(function(evt) {
+ delay(function() {
+ // ENTER KEY
+ if (evt.which === 13) {
+ return hideSearchResults();
+ }
+
+ // ESCAPE KEY
+ if (evt.which === 27) {
+ return hideSearchResults();
+ }
+
+ var query = $('#search-input').val();
+ logger.debug("query=" + query);
+
+ if (query === '') {
+ return hideSearchResults();
+ }
+
+ if (query.length > 2) {
+ emptySearchResults();
+ search(query);
+ }
+ }, 1000);
+ });
+
+ $('#sidebar-search-expand').click(function(evt) {
+ $('#searchForm').submit();
+ hideSearchResults();
+ });
+
+ // $('#sidebar-div').mouseleave(function(evt) {
+ // hideSearchResults();
+ // });
+
+ // wire up FRIEND_UPDATE handler
+ context.JK.JamServer.registerMessageCallback(context.JK.MessageType.FRIEND_UPDATE, function(header, payload) {
+ logger.debug("Handling FRIEND_UPDATE msg " + JSON.stringify(payload));
+
+ // update friends panel in sidebar
+ friends[payload.user_id].online = payload.online;
+ updateFriendList(friends);
+
+ // display notification
+ var online_text = payload.online ? "online" : "offline";
+ app.notify({
+ "title": "Friend is now " + online_text,
+ "text": payload.msg,
+ "icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
+ });
+ });
+ //////////////////////////////////////////////////////////////
+
+ // wire up FRIEND_REQUEST handler
+ context.JK.JamServer.registerMessageCallback(context.JK.MessageType.FRIEND_REQUEST, function(header, payload) {
+ logger.debug("Handling FRIEND_REQUEST msg " + JSON.stringify(payload));
+
+ handleNotification(payload, header.type);
+
+ // display notification
+ app.notify({
+ "title": "New Friend Request",
+ "text": payload.msg,
+ "icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
+ },
+ {
+ "ok_text": "ACCEPT",
+ "ok_callback": acceptFriendRequest,
+ "ok_callback_args": { "friend_request_id": payload.friend_request_id, "notification_id": payload.notification_id }
+ }
+ );
+ });
+ //////////////////////////////////////////////////////////////
+
+ // wire up FRIEND_REQUEST_ACCEPTED handler
+ context.JK.JamServer.registerMessageCallback(context.JK.MessageType.FRIEND_REQUEST_ACCEPTED, function(header, payload) {
+ logger.debug("Handling FRIEND_REQUEST_ACCEPTED msg " + JSON.stringify(payload));
+
+ handleNotification(payload, header.type);
+
+ // refresh friends panel
+ initializeFriendsPanel();
+
+ // display notification
+ app.notify({
+ "title": "Friend Request Accepted",
+ "text": payload.msg,
+ "icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
+ });
+ });
+ //////////////////////////////////////////////////////////////
+
+ // wire up MUSICIAN_SESSION_JOIN handler
+ context.JK.JamServer.registerMessageCallback(context.JK.MessageType.MUSICIAN_SESSION_JOIN, function(header, payload) {
+ logger.debug("Handling MUSICIAN_SESSION_JOIN msg " + JSON.stringify(payload));
+
+ // display notification
+ app.notify({
+ "title": "Musician Joined Session",
+ "text": payload.username + " has joined the session.",
+ "icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
+ });
+ });
+ //////////////////////////////////////////////////////////////
+
+ // wire up MUSICIAN_SESSION_DEPART handler
+ context.JK.JamServer.registerMessageCallback(context.JK.MessageType.MUSICIAN_SESSION_DEPART, function(header, payload) {
+ logger.debug("Handling MUSICIAN_SESSION_DEPART msg " + JSON.stringify(payload));
+
+ // display notification
+ app.notify({
+ "title": "Musician Left Session",
+ "text": payload.username + " has left the session.",
+ "icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
+ });
+ });
+ //////////////////////////////////////////////////////////////
+
+ // wire up MUSICIAN_SESSION_DEPART handler
+ context.JK.JamServer.registerMessageCallback(context.JK.MessageType.FRIEND_SESSION_JOIN, function(header, payload) {
+ logger.debug("Handling FRIEND_SESSION_JOIN msg " + JSON.stringify(payload));
+
+ // display notification
+ app.notify({
+ "title": "Friend Joined Session",
+ "text": payload.username + " has joined a session.",
+ "icon_url": context.JK.resolveAvatarUrl(payload.photo_url)
+ });
+ });
+ //////////////////////////////////////////////////////////////
+ }
+
+ this.initialize = function() {
+ events();
+ initializeFriendsPanel();
+ initializeChatPanel();
+ initializeNotificationsPanel();
+ };
+ };
+})(window,jQuery);
\ No newline at end of file
diff --git a/web/app/assets/javascripts/static_pages.js.coffee b/web/app/assets/javascripts/static_pages.js.coffee
new file mode 100644
index 000000000..761567942
--- /dev/null
+++ b/web/app/assets/javascripts/static_pages.js.coffee
@@ -0,0 +1,3 @@
+# Place all the behaviors and hooks related to the matching controller here.
+# All this logic will automatically be available in application.js.
+# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/
diff --git a/web/app/assets/javascripts/swfobject.js b/web/app/assets/javascripts/swfobject.js
new file mode 100644
index 000000000..fd9254617
--- /dev/null
+++ b/web/app/assets/javascripts/swfobject.js
@@ -0,0 +1,4 @@
+ /* SWFObject v2.2
+ is released under the MIT License
+*/
+var swfobject=function(){var D="undefined",r="object",S="Shockwave Flash",W="ShockwaveFlash.ShockwaveFlash",q="application/x-shockwave-flash",R="SWFObjectExprInst",x="onreadystatechange",O=window,j=document,t=navigator,T=false,U=[h],o=[],N=[],I=[],l,Q,E,B,J=false,a=false,n,G,m=true,M=function(){var aa=typeof j.getElementById!=D&&typeof j.getElementsByTagName!=D&&typeof j.createElement!=D,ah=t.userAgent.toLowerCase(),Y=t.platform.toLowerCase(),ae=Y?/win/.test(Y):/win/.test(ah),ac=Y?/mac/.test(Y):/mac/.test(ah),af=/webkit/.test(ah)?parseFloat(ah.replace(/^.*webkit\/(\d+(\.\d+)?).*$/,"$1")):false,X=!+"\v1",ag=[0,0,0],ab=null;if(typeof t.plugins!=D&&typeof t.plugins[S]==r){ab=t.plugins[S].description;if(ab&&!(typeof t.mimeTypes!=D&&t.mimeTypes[q]&&!t.mimeTypes[q].enabledPlugin)){T=true;X=false;ab=ab.replace(/^.*\s+(\S+\s+\S+$)/,"$1");ag[0]=parseInt(ab.replace(/^(.*)\..*$/,"$1"),10);ag[1]=parseInt(ab.replace(/^.*\.(.*)\s.*$/,"$1"),10);ag[2]=/[a-zA-Z]/.test(ab)?parseInt(ab.replace(/^.*[a-zA-Z]+(.*)$/,"$1"),10):0}}else{if(typeof O.ActiveXObject!=D){try{var ad=new ActiveXObject(W);if(ad){ab=ad.GetVariable("$version");if(ab){X=true;ab=ab.split(" ")[1].split(",");ag=[parseInt(ab[0],10),parseInt(ab[1],10),parseInt(ab[2],10)]}}}catch(Z){}}}return{w3:aa,pv:ag,wk:af,ie:X,win:ae,mac:ac}}(),k=function(){if(!M.w3){return}if((typeof j.readyState!=D&&j.readyState=="complete")||(typeof j.readyState==D&&(j.getElementsByTagName("body")[0]||j.body))){f()}if(!J){if(typeof j.addEventListener!=D){j.addEventListener("DOMContentLoaded",f,false)}if(M.ie&&M.win){j.attachEvent(x,function(){if(j.readyState=="complete"){j.detachEvent(x,arguments.callee);f()}});if(O==top){(function(){if(J){return}try{j.documentElement.doScroll("left")}catch(X){setTimeout(arguments.callee,0);return}f()})()}}if(M.wk){(function(){if(J){return}if(!/loaded|complete/.test(j.readyState)){setTimeout(arguments.callee,0);return}f()})()}s(f)}}();function f(){if(J){return}try{var Z=j.getElementsByTagName("body")[0].appendChild(C("span"));Z.parentNode.removeChild(Z)}catch(aa){return}J=true;var X=U.length;for(var Y=0;Y0){for(var af=0;af0){var ae=c(Y);if(ae){if(F(o[af].swfVersion)&&!(M.wk&&M.wk<312)){w(Y,true);if(ab){aa.success=true;aa.ref=z(Y);ab(aa)}}else{if(o[af].expressInstall&&A()){var ai={};ai.data=o[af].expressInstall;ai.width=ae.getAttribute("width")||"0";ai.height=ae.getAttribute("height")||"0";if(ae.getAttribute("class")){ai.styleclass=ae.getAttribute("class")}if(ae.getAttribute("align")){ai.align=ae.getAttribute("align")}var ah={};var X=ae.getElementsByTagName("param");var ac=X.length;for(var ad=0;ad '}}aa.outerHTML='";N[N.length]=ai.id;X=c(ai.id)}else{var Z=C(r);Z.setAttribute("type",q);for(var ac in ai){if(ai[ac]!=Object.prototype[ac]){if(ac.toLowerCase()=="styleclass"){Z.setAttribute("class",ai[ac])}else{if(ac.toLowerCase()!="classid"){Z.setAttribute(ac,ai[ac])}}}}for(var ab in ag){if(ag[ab]!=Object.prototype[ab]&&ab.toLowerCase()!="movie"){e(Z,ab,ag[ab])}}aa.parentNode.replaceChild(Z,aa);X=Z}}return X}function e(Z,X,Y){var aa=C("param");aa.setAttribute("name",X);aa.setAttribute("value",Y);Z.appendChild(aa)}function y(Y){var X=c(Y);if(X&&X.nodeName=="OBJECT"){if(M.ie&&M.win){X.style.display="none";(function(){if(X.readyState==4){b(Y)}else{setTimeout(arguments.callee,10)}})()}else{X.parentNode.removeChild(X)}}}function b(Z){var Y=c(Z);if(Y){for(var X in Y){if(typeof Y[X]=="function"){Y[X]=null}}Y.parentNode.removeChild(Y)}}function c(Z){var X=null;try{X=j.getElementById(Z)}catch(Y){}return X}function C(X){return j.createElement(X)}function i(Z,X,Y){Z.attachEvent(X,Y);I[I.length]=[Z,X,Y]}function F(Z){var Y=M.pv,X=Z.split(".");X[0]=parseInt(X[0],10);X[1]=parseInt(X[1],10)||0;X[2]=parseInt(X[2],10)||0;return(Y[0]>X[0]||(Y[0]==X[0]&&Y[1]>X[1])||(Y[0]==X[0]&&Y[1]==X[1]&&Y[2]>=X[2]))?true:false}function v(ac,Y,ad,ab){if(M.ie&&M.mac){return}var aa=j.getElementsByTagName("head")[0];if(!aa){return}var X=(ad&&typeof ad=="string")?ad:"screen";if(ab){n=null;G=null}if(!n||G!=X){var Z=C("style");Z.setAttribute("type","text/css");Z.setAttribute("media",X);n=aa.appendChild(Z);if(M.ie&&M.win&&typeof j.styleSheets!=D&&j.styleSheets.length>0){n=j.styleSheets[j.styleSheets.length-1]}G=X}if(M.ie&&M.win){if(n&&typeof n.addRule==r){n.addRule(ac,Y)}}else{if(n&&typeof j.createTextNode!=D){n.appendChild(j.createTextNode(ac+" {"+Y+"}"))}}}function w(Z,X){if(!m){return}var Y=X?"visible":"hidden";if(J&&c(Z)){c(Z).style.visibility=Y}else{v("#"+Z,"visibility:"+Y)}}function L(Y){var Z=/[\\\"<>\.;]/;var X=Z.exec(Y)!=null;return X&&typeof encodeURIComponent!=D?encodeURIComponent(Y):Y}var d=function(){if(M.ie&&M.win){window.attachEvent("onunload",function(){var ac=I.length;for(var ab=0;ab 0) {
+ var track = {
+ instrument_id: context.JK.userMe.instruments[0].instrument_id,
+ sound: "stereo"
+ };
+ trackObjects.push(track);
+
+ var desc = context.JK.userMe.instruments[0].description;
+ jamClient.TrackSetInstrument(1, context.JK.server_to_client_instrument_map[desc]);
+ jamClient.TrackSaveAssignments();
+ }
+ }
+ // use all tracks previously configured
+ else {
+ console.log("localMusicTracks.length=" + localMusicTracks.length);
+ for (i=0; i < localMusicTracks.length; i++) {
+ var track = {};
+ var instrument_description = '';
+ console.log("localMusicTracks[" + i + "].instrument_id=" + localMusicTracks[i].instrument_id);
+
+ // no instruments configured
+ if (localMusicTracks[i].instrument_id === 0) {
+ if (context.JK.userMe.instruments && context.JK.userMe.instruments.length > 0) {
+ track.instrument_id = context.JK.userMe.instruments[0].instrument_id;
+ }
+ else {
+ track.instrument_id = context.JK.client_to_server_instrument_map[250].server_id;
+ }
+ }
+ // instruments are configured
+ else {
+ if (context.JK.client_to_server_instrument_map[localMusicTracks[i].instrument_id]) {
+ track.instrument_id = context.JK.client_to_server_instrument_map[localMusicTracks[i].instrument_id].server_id;
+ }
+ // fall back to Other
+ else {
+ track.instrument_id = context.JK.client_to_server_instrument_map[250].server_id;
+ jamClient.TrackSetInstrument(i+1, 250);
+ jamClient.TrackSaveAssignments();
+ }
+ }
+ if (localMusicTracks[i].stereo) {
+ track.sound = "stereo";
+ }
+ else {
+ track.sound = "mono";
+ }
+ trackObjects.push(track);
+ }
+ }
+ return trackObjects;
+ }
+ };
+
+})(window, jQuery);
\ No newline at end of file
diff --git a/web/app/assets/javascripts/users.js.coffee b/web/app/assets/javascripts/users.js.coffee
new file mode 100644
index 000000000..761567942
--- /dev/null
+++ b/web/app/assets/javascripts/users.js.coffee
@@ -0,0 +1,3 @@
+# Place all the behaviors and hooks related to the matching controller here.
+# All this logic will automatically be available in application.js.
+# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/
diff --git a/web/app/assets/javascripts/utils.js b/web/app/assets/javascripts/utils.js
new file mode 100644
index 000000000..d36d48180
--- /dev/null
+++ b/web/app/assets/javascripts/utils.js
@@ -0,0 +1,491 @@
+/**
+* Common utility functions.
+*/
+(function(context,$) {
+
+ "use strict";
+
+ context.JK = context.JK || {};
+ var logger = context.JK.logger;
+
+ context.JK.stringToBool = function(s) {
+ switch(s.toLowerCase()){
+ case "true": case "yes": case "1": return true;
+ case "false": case "no": case "0": case null: return false;
+ default: return Boolean(s);
+ }
+ };
+
+ // Build up two maps of images, for each instrument id.
+ // This map is a simple base map of instrument id to the basic image name.
+ // Below, a loop goes through this and builds two size-specific maps.
+ // In the future, we should test Whether having a single larger image
+ // available, and allowing the browser to resize offers better quality.
+ var icon_map_base = {
+ "accordion":"accordion",
+ "acoustic guitar":"acoustic",
+ "banjo":"banjo",
+ "bass guitar":"bass",
+ "cello":"cello",
+ "clarinet":"clarinet",
+ "computer":"computer",
+ "default":"default",
+ "drums":"drums",
+ "electric guitar":"guitar",
+ "euphonium":"euphonium",
+ "flute":"flute",
+ "french horn":"frenchhorn",
+ "harmonica":"harmonica",
+ "keyboard":"keyboard",
+ "mandolin":"mandolin",
+ "oboe":"oboe",
+ "other":"other",
+ "saxophone":"saxophone",
+ "trombone":"trombone",
+ "trumpet":"trumpet",
+ "tuba":"tuba",
+ "ukulele":"ukelele",
+ "viola":"viola",
+ "violin":"violin",
+ "voice":"vocals"
+ };
+
+ var instrumentIconMap24 = {};
+ var instrumentIconMap45 = {};
+
+ $.each(context._.keys(icon_map_base), function(index, instrumentId) {
+ var icon = icon_map_base[instrumentId];
+ instrumentIconMap24[instrumentId] = "../assets/content/icon_instrument_" + icon + "24.png";
+ instrumentIconMap45[instrumentId] = "../assets/content/icon_instrument_" + icon + "45.png";
+ });
+
+ // Uber-simple templating
+ // var template = "Hey {name}";
+ // var vals = { name: "Jon" };
+ // _fillTemplate(template, vals);
+ // --> "Hey Jon"
+ context.JK.fillTemplate = function(template, vals) {
+ for(var val in vals)
+ template=template.replace(new RegExp('{'+val+'}','g'), vals[val]);
+ return template;
+ };
+
+ context.JK.resolveAvatarUrl = function(photo_url) {
+ return photo_url ? photo_url : "/assets/shared/avatar_generic.png";
+ };
+
+ context.JK.getInstrumentIconMap24 = function() {
+ return instrumentIconMap24;
+ };
+
+ context.JK.getInstrumentIconMap45 = function() {
+ return instrumentIconMap45;
+ };
+
+ context.JK.getInstrumentIcon24 = function(instrument) {
+ if (instrument in instrumentIconMap24) {
+ return instrumentIconMap24[instrument];
+ }
+
+ return instrumentIconMap24["default"];
+ };
+
+ context.JK.getInstrumentIcon45 = function(instrument) {
+ if (instrument in instrumentIconMap45) {
+ return instrumentIconMap45[instrument];
+ }
+
+ return instrumentIconMap45["default"];
+ };
+
+ context.JK.listInstruments = function(app, callback) {
+ var url = "/api/instruments";
+ $.ajax({
+ type: "GET",
+ dataType: "json",
+ url: url,
+ success: function(response) {
+ callback(response);
+ },
+ error: app.ajaxError
+ });
+ };
+
+ context.JK.showErrorDialog = function(app, msg, title) {
+ app.layout.showDialog('error-dialog');
+ $('#error-msg', 'div[layout-id="error-dialog"]').html(msg);
+ $('#error-summary', 'div[layout-id="error-dialog"]').html(title);
+ }
+
+ // TODO: figure out how to handle this in layout.js for layered popups
+ context.JK.showOverlay = function() {
+ $('.dialog-overlay').show();
+ }
+
+ context.JK.hideOverlay = function() {
+ $('.dialog-overlay').hide();
+ }
+
+ /*
+ * Loads a listbox or dropdown with the values in input_array, setting the option value
+ * to the id_field and the option text to text_field. It will preselect the option with
+ * value equal to selected_id.
+ */
+ context.JK.loadOptions = function(templateHtml, listbox_id, input_array, id_field, text_field, selected_id) {
+ $.each(input_array, function(index, val) {
+ var isSelected = "";
+ if (val[id_field] === selected_id) {
+ isSelected = "selected";
+ }
+ var html = context.JK.fillTemplate(templateHtml, {
+ value: val[id_field],
+ label: val[text_field],
+ selected: isSelected
+ });
+
+ listbox_id.append(html);
+ });
+ }
+
+ context.JK.trimString = function(str) {
+ return str.replace(/^\s\s*/, '').replace(/\s\s*$/, '');
+ };
+
+ context.JK.padString = function(str, max) {
+ var retVal = '' + str;
+ while (retVal.length < max) {
+ retVal = '0' + retVal;
+ }
+
+ return retVal;
+ }
+
+ context.JK.formatDate = function(dateString) {
+ var date = new Date(dateString);
+ return date.getFullYear() + "-" + context.JK.padString(date.getMonth()+1, 2) + "-" + context.JK.padString(date.getDate(), 2) + " @ " + date.toLocaleTimeString();
+ }
+
+ context.JK.search = function(query, app, callback) {
+ $.ajax({
+ type: "GET",
+ dataType: "json",
+ contentType: 'application/json',
+ url: "/api/search?query=" + query,
+ processData: false,
+ success: function(response) {
+ callback(response);
+ },
+ error: app.ajaxError
+ });
+ };
+
+ context.JK.sendFriendRequest = function(app, userId, callback) {
+ var url = "/api/users/" + context.JK.currentUserId + "/friend_requests";
+ $.ajax({
+ type: "POST",
+ dataType: "json",
+ contentType: 'application/json',
+ url: url,
+ data: '{"friend_id":"' + userId + '"}',
+ processData: false,
+ success: function(response) {
+ callback(userId);
+ },
+ error: app.ajaxError
+ });
+ };
+
+ /*
+ * Get the length of a dictionary
+ */
+ context.JK.dlen = function(d) {
+ var count = 0;
+ for (var i in d) {
+ if (d.hasOwnProperty(i)) {
+ count++;
+ }
+ }
+ return count;
+ };
+
+ /**
+ * Finds the first error associated with the field.
+ * @param fieldName the name of the field
+ * @param errors_data response from a 422 response in ajax
+ * @returns null if no error for the field name, or the 1st error associated with that field
+ */
+ context.JK.get_first_error = function(fieldName, errors_data) {
+ var errors = errors_data["errors"];
+ if(errors == null) return null;
+
+ if(errors[fieldName] && errors[fieldName].length > 0) {
+ return errors[fieldName][0]
+ }
+ else {
+ return null;
+ }
+ }
+
+ /**
+ * Returns a ul with an li per field name.
+ * @param fieldName the name of the field
+ * @param errors_data error data return by a 422 ajax response
+ * @returns null if no error for the field name; otherwise a ul/li
+ */
+ context.JK.format_errors = function(fieldName, errors_data) {
+ var errors = errors_data["errors"];
+ if(errors == null) return null;
+
+ var field_errors = errors[fieldName];
+ if(field_errors == null) {
+ return null;
+ }
+
+ var ul = $('
')
+
+ $.each(field_errors, function(index, item) {
+ ul.append(context.JK.fillTemplate("- {message}
", {message: item}))
+ })
+
+ return ul;
+ }
+
+
+ /**
+ * Way to verify that a number of parallel tasks have all completed.
+ * Provide a function to evaluate completion, and a callback to
+ * invoke when that function evaluates to true.
+ * NOTE: this does not pause execution, it simply ensures that
+ * when the test function evaluates to true, the callback will
+ * be invoked.
+ */
+ context.JK.joinCalls = function(completionTestFunction, callback, interval) {
+ function doneYet() {
+ if (completionTestFunction()) {
+ callback();
+ } else {
+ context.setTimeout(doneYet, interval);
+ }
+ }
+ doneYet();
+ };
+
+ /**
+ * Returns 'MacOSX' if the os appears to be macintosh,
+ * 'Win32' if the os appears to be windows,
+ * 'Unix' if the OS appears to be linux,
+ * and null if unknown
+ * @returns {*}
+ */
+ context.JK.detectOS = function() {
+ if(!navigator.platform) { return null; }
+
+ if (navigator.platform.toLowerCase().indexOf( 'mac' ) !== -1) {
+ return "MacOSX";
+ }
+ if (navigator.platform.toLowerCase().indexOf( 'win' ) !== -1 ) {
+ return "Win32"
+ }
+ if (navigator.platform.toLowerCase().indexOf('linux') !== -1 ) {
+ return "Unix"
+ }
+
+ return null;
+ }
+
+ context.JK.popExternalLinks = function() {
+ // Allow any a link with a rel="external" attribute to launch
+ // the link in the default browser, using jamClient:
+ $('a[rel="external"]').click(function(evt) {
+ evt.preventDefault();
+ var href = $(this).attr("href");
+ if (href) {
+ // make absolute if not already
+ if(href.indexOf('http') != 0 && href.indexOf('mailto') != 0) {
+ href = window.location.protocol + '//' + window.location.host + href;
+ }
+
+ context.jamClient.OpenSystemBrowser(href);
+ }
+ return false;
+ });
+ }
+
+ /*
+ * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message
+ * Digest Algorithm, as defined in RFC 1321.
+ * Copyright (C) Paul Johnston 1999 - 2000.
+ * Updated by Greg Holt 2000 - 2001.
+ * See http://pajhome.org.uk/site/legal.html for details.
+ */
+
+ /*
+ * Convert a 32-bit number to a hex string with ls-byte first
+ */
+ var hex_chr = "0123456789abcdef";
+ function rhex(num) {
+ var str, j;
+ str = "";
+ for(j=0; j<=3; j++) {
+ str += hex_chr.charAt((num >> (j * 8 + 4)) & 0x0F) +
+ hex_chr.charAt((num >> (j * 8)) & 0x0F);
+ }
+ return str;
+ }
+
+ /*
+ * Convert a string to a sequence of 16-word blocks, stored as an array.
+ * Append padding bits and the length, as described in the MD5 standard.
+ */
+ function str2blks_MD5(str) {
+ var nblk, blks, i;
+ nblk = ((str.length + 8) >> 6) + 1;
+ blks = new Array(nblk * 16);
+ for(i = 0; i < nblk * 16; i++) blks[i] = 0;
+ for(i = 0; i < str.length; i++)
+ blks[i >> 2] |= str.charCodeAt(i) << ((i % 4) * 8);
+ blks[i >> 2] |= 0x80 << ((i % 4) * 8);
+ blks[nblk * 16 - 2] = str.length * 8;
+ return blks;
+ }
+
+ /*
+ * Add integers, wrapping at 2^32. This uses 16-bit operations internally
+ * to work around bugs in some JS interpreters.
+ */
+ function add(x, y)
+ {
+ var lsw = (x & 0xFFFF) + (y & 0xFFFF);
+ var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
+ return (msw << 16) | (lsw & 0xFFFF);
+ }
+
+ /*
+ * Bitwise rotate a 32-bit number to the left
+ */
+ function rol(num, cnt)
+ {
+ return (num << cnt) | (num >>> (32 - cnt));
+ }
+
+ /*
+ * These functions implement the basic operation for each round of the
+ * algorithm.
+ */
+ function cmn(q, a, b, x, s, t)
+ {
+ return add(rol(add(add(a, q), add(x, t)), s), b);
+ }
+ function ff(a, b, c, d, x, s, t)
+ {
+ return cmn((b & c) | ((~b) & d), a, b, x, s, t);
+ }
+ function gg(a, b, c, d, x, s, t)
+ {
+ return cmn((b & d) | (c & (~d)), a, b, x, s, t);
+ }
+ function hh(a, b, c, d, x, s, t)
+ {
+ return cmn(b ^ c ^ d, a, b, x, s, t);
+ }
+ function ii(a, b, c, d, x, s, t)
+ {
+ return cmn(c ^ (b | (~d)), a, b, x, s, t);
+ }
+
+ /*
+ * Take a string and return the hex representation of its MD5.
+ */
+ context.JK.calcMD5 = function(str) {
+ var x, a, b, c, d, i,
+ olda, oldb, oldc, oldd;
+ x = str2blks_MD5(str);
+ a = 1732584193;
+ b = -271733879;
+ c = -1732584194;
+ d = 271733878;
+
+ for(i = 0; i < x.length; i += 16)
+ {
+ olda = a;
+ oldb = b;
+ oldc = c;
+ oldd = d;
+
+ a = ff(a, b, c, d, x[i+ 0], 7 , -680876936);
+ d = ff(d, a, b, c, x[i+ 1], 12, -389564586);
+ c = ff(c, d, a, b, x[i+ 2], 17, 606105819);
+ b = ff(b, c, d, a, x[i+ 3], 22, -1044525330);
+ a = ff(a, b, c, d, x[i+ 4], 7 , -176418897);
+ d = ff(d, a, b, c, x[i+ 5], 12, 1200080426);
+ c = ff(c, d, a, b, x[i+ 6], 17, -1473231341);
+ b = ff(b, c, d, a, x[i+ 7], 22, -45705983);
+ a = ff(a, b, c, d, x[i+ 8], 7 , 1770035416);
+ d = ff(d, a, b, c, x[i+ 9], 12, -1958414417);
+ c = ff(c, d, a, b, x[i+10], 17, -42063);
+ b = ff(b, c, d, a, x[i+11], 22, -1990404162);
+ a = ff(a, b, c, d, x[i+12], 7 , 1804603682);
+ d = ff(d, a, b, c, x[i+13], 12, -40341101);
+ c = ff(c, d, a, b, x[i+14], 17, -1502002290);
+ b = ff(b, c, d, a, x[i+15], 22, 1236535329);
+
+ a = gg(a, b, c, d, x[i+ 1], 5 , -165796510);
+ d = gg(d, a, b, c, x[i+ 6], 9 , -1069501632);
+ c = gg(c, d, a, b, x[i+11], 14, 643717713);
+ b = gg(b, c, d, a, x[i+ 0], 20, -373897302);
+ a = gg(a, b, c, d, x[i+ 5], 5 , -701558691);
+ d = gg(d, a, b, c, x[i+10], 9 , 38016083);
+ c = gg(c, d, a, b, x[i+15], 14, -660478335);
+ b = gg(b, c, d, a, x[i+ 4], 20, -405537848);
+ a = gg(a, b, c, d, x[i+ 9], 5 , 568446438);
+ d = gg(d, a, b, c, x[i+14], 9 , -1019803690);
+ c = gg(c, d, a, b, x[i+ 3], 14, -187363961);
+ b = gg(b, c, d, a, x[i+ 8], 20, 1163531501);
+ a = gg(a, b, c, d, x[i+13], 5 , -1444681467);
+ d = gg(d, a, b, c, x[i+ 2], 9 , -51403784);
+ c = gg(c, d, a, b, x[i+ 7], 14, 1735328473);
+ b = gg(b, c, d, a, x[i+12], 20, -1926607734);
+
+ a = hh(a, b, c, d, x[i+ 5], 4 , -378558);
+ d = hh(d, a, b, c, x[i+ 8], 11, -2022574463);
+ c = hh(c, d, a, b, x[i+11], 16, 1839030562);
+ b = hh(b, c, d, a, x[i+14], 23, -35309556);
+ a = hh(a, b, c, d, x[i+ 1], 4 , -1530992060);
+ d = hh(d, a, b, c, x[i+ 4], 11, 1272893353);
+ c = hh(c, d, a, b, x[i+ 7], 16, -155497632);
+ b = hh(b, c, d, a, x[i+10], 23, -1094730640);
+ a = hh(a, b, c, d, x[i+13], 4 , 681279174);
+ d = hh(d, a, b, c, x[i+ 0], 11, -358537222);
+ c = hh(c, d, a, b, x[i+ 3], 16, -722521979);
+ b = hh(b, c, d, a, x[i+ 6], 23, 76029189);
+ a = hh(a, b, c, d, x[i+ 9], 4 , -640364487);
+ d = hh(d, a, b, c, x[i+12], 11, -421815835);
+ c = hh(c, d, a, b, x[i+15], 16, 530742520);
+ b = hh(b, c, d, a, x[i+ 2], 23, -995338651);
+
+ a = ii(a, b, c, d, x[i+ 0], 6 , -198630844);
+ d = ii(d, a, b, c, x[i+ 7], 10, 1126891415);
+ c = ii(c, d, a, b, x[i+14], 15, -1416354905);
+ b = ii(b, c, d, a, x[i+ 5], 21, -57434055);
+ a = ii(a, b, c, d, x[i+12], 6 , 1700485571);
+ d = ii(d, a, b, c, x[i+ 3], 10, -1894986606);
+ c = ii(c, d, a, b, x[i+10], 15, -1051523);
+ b = ii(b, c, d, a, x[i+ 1], 21, -2054922799);
+ a = ii(a, b, c, d, x[i+ 8], 6 , 1873313359);
+ d = ii(d, a, b, c, x[i+15], 10, -30611744);
+ c = ii(c, d, a, b, x[i+ 6], 15, -1560198380);
+ b = ii(b, c, d, a, x[i+13], 21, 1309151649);
+ a = ii(a, b, c, d, x[i+ 4], 6 , -145523070);
+ d = ii(d, a, b, c, x[i+11], 10, -1120210379);
+ c = ii(c, d, a, b, x[i+ 2], 15, 718787259);
+ b = ii(b, c, d, a, x[i+ 9], 21, -343485551);
+
+ a = add(a, olda);
+ b = add(b, oldb);
+ c = add(c, oldc);
+ d = add(d, oldd);
+ }
+ return rhex(a) + rhex(b) + rhex(c) + rhex(d);
+ };
+
+ })(window,jQuery);
\ No newline at end of file
diff --git a/web/app/assets/javascripts/vuHelpers.js b/web/app/assets/javascripts/vuHelpers.js
new file mode 100644
index 000000000..c63567321
--- /dev/null
+++ b/web/app/assets/javascripts/vuHelpers.js
@@ -0,0 +1,89 @@
+/**
+* Functions related to VU meters.
+* These functions are intimately tied to the markup defined in the
+* VU templates in _vu_meters.html.erb
+*/
+(function(context, $) {
+
+ "use strict";
+
+ context.JK = context.JK || {};
+
+ // As these are helper functions, just have a single
+ // static object with functions. Each function should
+ // take all necessary arguments to complete its work.
+ context.JK.VuHelpers = {
+
+ /**
+ * Render a VU meter into the provided selector.
+ * vuType can be either "horizontal" or "vertical"
+ */
+ renderVU: function(selector, userOptions) {
+ /**
+ * The default options for rendering a VU
+ */
+ var renderVUDefaults = {
+ vuType: "vertical",
+ lightCount: 12,
+ lightWidth: 3,
+ lightHeight: 17
+ };
+
+ var options = $.extend({}, renderVUDefaults, userOptions);
+ var templateSelector = "#template-vu-v";
+ if (options.vuType === "horizontal") {
+ templateSelector = "#template-vu-h";
+ }
+ var templateSource = $(templateSelector).html();
+ $(selector).empty();
+
+ $(selector).html(context._.template(templateSource, options, {variable: 'data'}));
+ },
+
+ /**
+ * Given a selector representing a container for a VU meter and
+ * a value between 0.0 and 1.0, light the appropriate lights.
+ */
+ updateVU: function (selector, value) {
+ // There are 13 VU lights. Figure out how many to
+ // light based on the incoming value.
+ var countSelector = 'tr';
+ var horizontal = ($('table.horizontal', selector).length);
+ if (horizontal) {
+ countSelector = 'td';
+ }
+
+ var lightCount = $(countSelector, selector).length;
+ 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 lightSelectorPrefix = selector + ' td.vu';
+ var thisLightSelector = null;
+
+ // Remove all light classes from all lights
+ var allLightsSelector = selector + ' td.vulight';
+ $(allLightsSelector).removeClass('vu-green-off vu-green-on vu-red-off vu-red-on');
+
+ // Set the lights
+ for (i=0; i= redSwitch) {
+ colorClass = 'vu-red-';
+ }
+ if (i >= lights) {
+ state = 'off';
+ }
+ thisLightSelector = lightSelectorPrefix + i;
+ $light = $(thisLightSelector);
+ $light.addClass(colorClass + state);
+ }
+ }
+
+ };
+
+})(window, jQuery);
\ No newline at end of file
diff --git a/web/app/assets/javascripts/web_socket.js b/web/app/assets/javascripts/web_socket.js
new file mode 100644
index 000000000..06cc5d027
--- /dev/null
+++ b/web/app/assets/javascripts/web_socket.js
@@ -0,0 +1,391 @@
+// Copyright: Hiroshi Ichikawa
+// License: New BSD License
+// Reference: http://dev.w3.org/html5/websockets/
+// Reference: http://tools.ietf.org/html/rfc6455
+
+(function() {
+
+ if (window.WEB_SOCKET_FORCE_FLASH) {
+ // Keeps going.
+ } else if (window.WebSocket) {
+ return;
+ } else if (window.MozWebSocket) {
+ // Firefox.
+ window.WebSocket = MozWebSocket;
+ return;
+ }
+
+ var logger;
+ if (window.WEB_SOCKET_LOGGER) {
+ logger = WEB_SOCKET_LOGGER;
+ } else if (window.console && window.console.log && window.console.error) {
+ // In some environment, console is defined but console.log or console.error is missing.
+ logger = window.console;
+ } else {
+ logger = {log: function(){ }, error: function(){ }};
+ }
+
+ // swfobject.hasFlashPlayerVersion("10.0.0") doesn't work with Gnash.
+ if (swfobject.getFlashPlayerVersion().major < 10) {
+ logger.error("Flash Player >= 10.0.0 is required.");
+ return;
+ }
+ if (location.protocol == "file:") {
+ logger.error(
+ "WARNING: web-socket-js doesn't work in file:///... URL " +
+ "unless you set Flash Security Settings properly. " +
+ "Open the page via Web server i.e. http://...");
+ }
+
+ /**
+ * Our own implementation of WebSocket class using Flash.
+ * @param {string} url
+ * @param {array or string} protocols
+ * @param {string} proxyHost
+ * @param {int} proxyPort
+ * @param {string} headers
+ */
+ window.WebSocket = function(url, protocols, proxyHost, proxyPort, headers) {
+ var self = this;
+ self.__id = WebSocket.__nextId++;
+ WebSocket.__instances[self.__id] = self;
+ self.readyState = WebSocket.CONNECTING;
+ self.bufferedAmount = 0;
+ self.__events = {};
+ if (!protocols) {
+ protocols = [];
+ } else if (typeof protocols == "string") {
+ protocols = [protocols];
+ }
+ // Uses setTimeout() to make sure __createFlash() runs after the caller sets ws.onopen etc.
+ // Otherwise, when onopen fires immediately, onopen is called before it is set.
+ self.__createTask = setTimeout(function() {
+ WebSocket.__addTask(function() {
+ self.__createTask = null;
+ WebSocket.__flash.create(
+ self.__id, url, protocols, proxyHost || null, proxyPort || 0, headers || null);
+ });
+ }, 0);
+ };
+
+ /**
+ * Send data to the web socket.
+ * @param {string} data The data to send to the socket.
+ * @return {boolean} True for success, false for failure.
+ */
+ WebSocket.prototype.send = function(data) {
+ if (this.readyState == WebSocket.CONNECTING) {
+ throw "INVALID_STATE_ERR: Web Socket connection has not been established";
+ }
+ // We use encodeURIComponent() here, because FABridge doesn't work if
+ // the argument includes some characters. We don't use escape() here
+ // because of this:
+ // https://developer.mozilla.org/en/Core_JavaScript_1.5_Guide/Functions#escape_and_unescape_Functions
+ // But it looks decodeURIComponent(encodeURIComponent(s)) doesn't
+ // preserve all Unicode characters either e.g. "\uffff" in Firefox.
+ // Note by wtritch: Hopefully this will not be necessary using ExternalInterface. Will require
+ // additional testing.
+ var result = WebSocket.__flash.send(this.__id, encodeURIComponent(data));
+ if (result < 0) { // success
+ return true;
+ } else {
+ this.bufferedAmount += result;
+ return false;
+ }
+ };
+
+ /**
+ * Close this web socket gracefully.
+ */
+ WebSocket.prototype.close = function() {
+ if (this.__createTask) {
+ clearTimeout(this.__createTask);
+ this.__createTask = null;
+ this.readyState = WebSocket.CLOSED;
+ return;
+ }
+ if (this.readyState == WebSocket.CLOSED || this.readyState == WebSocket.CLOSING) {
+ return;
+ }
+ this.readyState = WebSocket.CLOSING;
+ WebSocket.__flash.close(this.__id);
+ };
+
+ /**
+ * Implementation of {@link DOM 2 EventTarget Interface}
+ *
+ * @param {string} type
+ * @param {function} listener
+ * @param {boolean} useCapture
+ * @return void
+ */
+ WebSocket.prototype.addEventListener = function(type, listener, useCapture) {
+ if (!(type in this.__events)) {
+ this.__events[type] = [];
+ }
+ this.__events[type].push(listener);
+ };
+
+ /**
+ * Implementation of {@link DOM 2 EventTarget Interface}
+ *
+ * @param {string} type
+ * @param {function} listener
+ * @param {boolean} useCapture
+ * @return void
+ */
+ WebSocket.prototype.removeEventListener = function(type, listener, useCapture) {
+ if (!(type in this.__events)) return;
+ var events = this.__events[type];
+ for (var i = events.length - 1; i >= 0; --i) {
+ if (events[i] === listener) {
+ events.splice(i, 1);
+ break;
+ }
+ }
+ };
+
+ /**
+ * Implementation of {@link DOM 2 EventTarget Interface}
+ *
+ * @param {Event} event
+ * @return void
+ */
+ WebSocket.prototype.dispatchEvent = function(event) {
+ var events = this.__events[event.type] || [];
+ for (var i = 0; i < events.length; ++i) {
+ events[i](event);
+ }
+ var handler = this["on" + event.type];
+ if (handler) handler.apply(this, [event]);
+ };
+
+ /**
+ * Handles an event from Flash.
+ * @param {Object} flashEvent
+ */
+ WebSocket.prototype.__handleEvent = function(flashEvent) {
+
+ if ("readyState" in flashEvent) {
+ this.readyState = flashEvent.readyState;
+ }
+ if ("protocol" in flashEvent) {
+ this.protocol = flashEvent.protocol;
+ }
+
+ var jsEvent;
+ if (flashEvent.type == "open" || flashEvent.type == "error") {
+ jsEvent = this.__createSimpleEvent(flashEvent.type);
+ } else if (flashEvent.type == "close") {
+ jsEvent = this.__createSimpleEvent("close");
+ jsEvent.wasClean = flashEvent.wasClean ? true : false;
+ jsEvent.code = flashEvent.code;
+ jsEvent.reason = flashEvent.reason;
+ } else if (flashEvent.type == "message") {
+ var data = decodeURIComponent(flashEvent.message);
+ jsEvent = this.__createMessageEvent("message", data);
+ } else {
+ throw "unknown event type: " + flashEvent.type;
+ }
+
+ this.dispatchEvent(jsEvent);
+
+ };
+
+ WebSocket.prototype.__createSimpleEvent = function(type) {
+ if (document.createEvent && window.Event) {
+ var event = document.createEvent("Event");
+ event.initEvent(type, false, false);
+ return event;
+ } else {
+ return {type: type, bubbles: false, cancelable: false};
+ }
+ };
+
+ WebSocket.prototype.__createMessageEvent = function(type, data) {
+ if (document.createEvent && window.MessageEvent && !window.opera) {
+ var event = document.createEvent("MessageEvent");
+ event.initMessageEvent("message", false, false, data, null, null, window, null);
+ return event;
+ } else {
+ // IE and Opera, the latter one truncates the data parameter after any 0x00 bytes.
+ return {type: type, data: data, bubbles: false, cancelable: false};
+ }
+ };
+
+ /**
+ * Define the WebSocket readyState enumeration.
+ */
+ WebSocket.CONNECTING = 0;
+ WebSocket.OPEN = 1;
+ WebSocket.CLOSING = 2;
+ WebSocket.CLOSED = 3;
+
+ // Field to check implementation of WebSocket.
+ WebSocket.__isFlashImplementation = true;
+ WebSocket.__initialized = false;
+ WebSocket.__flash = null;
+ WebSocket.__instances = {};
+ WebSocket.__tasks = [];
+ WebSocket.__nextId = 0;
+
+ /**
+ * Load a new flash security policy file.
+ * @param {string} url
+ */
+ WebSocket.loadFlashPolicyFile = function(url){
+ WebSocket.__addTask(function() {
+ WebSocket.__flash.loadManualPolicyFile(url);
+ });
+ };
+
+ /**
+ * Loads WebSocketMain.swf and creates WebSocketMain object in Flash.
+ */
+ WebSocket.__initialize = function() {
+
+ if (WebSocket.__initialized) return;
+ WebSocket.__initialized = true;
+
+ if (WebSocket.__swfLocation) {
+ // For backword compatibility.
+ window.WEB_SOCKET_SWF_LOCATION = WebSocket.__swfLocation;
+ }
+ if (!window.WEB_SOCKET_SWF_LOCATION) {
+ logger.error("[WebSocket] set WEB_SOCKET_SWF_LOCATION to location of WebSocketMain.swf");
+ return;
+ }
+ if (!window.WEB_SOCKET_SUPPRESS_CROSS_DOMAIN_SWF_ERROR &&
+ !WEB_SOCKET_SWF_LOCATION.match(/(^|\/)WebSocketMainInsecure\.swf(\?.*)?$/) &&
+ WEB_SOCKET_SWF_LOCATION.match(/^\w+:\/\/([^\/]+)/)) {
+ var swfHost = RegExp.$1;
+ if (location.host != swfHost) {
+ logger.error(
+ "[WebSocket] You must host HTML and WebSocketMain.swf in the same host " +
+ "('" + location.host + "' != '" + swfHost + "'). " +
+ "See also 'How to host HTML file and SWF file in different domains' section " +
+ "in README.md. If you use WebSocketMainInsecure.swf, you can suppress this message " +
+ "by WEB_SOCKET_SUPPRESS_CROSS_DOMAIN_SWF_ERROR = true;");
+ }
+ }
+ var container = document.createElement("div");
+ container.id = "webSocketContainer";
+ // Hides Flash box. We cannot use display: none or visibility: hidden because it prevents
+ // Flash from loading at least in IE. So we move it out of the screen at (-100, -100).
+ // But this even doesn't work with Flash Lite (e.g. in Droid Incredible). So with Flash
+ // Lite, we put it at (0, 0). This shows 1x1 box visible at left-top corner but this is
+ // the best we can do as far as we know now.
+ container.style.position = "absolute";
+ if (WebSocket.__isFlashLite()) {
+ container.style.left = "0px";
+ container.style.top = "0px";
+ } else {
+ container.style.left = "-100px";
+ container.style.top = "-100px";
+ }
+ var holder = document.createElement("div");
+ holder.id = "webSocketFlash";
+ container.appendChild(holder);
+ document.body.appendChild(container);
+ // See this article for hasPriority:
+ // http://help.adobe.com/en_US/as3/mobile/WS4bebcd66a74275c36cfb8137124318eebc6-7ffd.html
+ swfobject.embedSWF(
+ WEB_SOCKET_SWF_LOCATION,
+ "webSocketFlash",
+ "1" /* width */,
+ "1" /* height */,
+ "10.0.0" /* SWF version */,
+ null,
+ null,
+ {hasPriority: true, swliveconnect : true, allowScriptAccess: "always"},
+ null,
+ function(e) {
+ if (!e.success) {
+ logger.error("[WebSocket] swfobject.embedSWF failed");
+ }
+ }
+ );
+
+ };
+
+ /**
+ * Called by Flash to notify JS that it's fully loaded and ready
+ * for communication.
+ */
+ WebSocket.__onFlashInitialized = function() {
+ // We need to set a timeout here to avoid round-trip calls
+ // to flash during the initialization process.
+ setTimeout(function() {
+ WebSocket.__flash = document.getElementById("webSocketFlash");
+ WebSocket.__flash.setCallerUrl(location.href);
+ WebSocket.__flash.setDebug(!!window.WEB_SOCKET_DEBUG);
+ for (var i = 0; i < WebSocket.__tasks.length; ++i) {
+ WebSocket.__tasks[i]();
+ }
+ WebSocket.__tasks = [];
+ }, 0);
+ };
+
+ /**
+ * Called by Flash to notify WebSockets events are fired.
+ */
+ WebSocket.__onFlashEvent = function() {
+ setTimeout(function() {
+ try {
+ // Gets events using receiveEvents() instead of getting it from event object
+ // of Flash event. This is to make sure to keep message order.
+ // It seems sometimes Flash events don't arrive in the same order as they are sent.
+ var events = WebSocket.__flash.receiveEvents();
+ for (var i = 0; i < events.length; ++i) {
+ WebSocket.__instances[events[i].webSocketId].__handleEvent(events[i]);
+ }
+ } catch (e) {
+ logger.error(e);
+ }
+ }, 0);
+ return true;
+ };
+
+ // Called by Flash.
+ WebSocket.__log = function(message) {
+ logger.log(decodeURIComponent(message));
+ };
+
+ // Called by Flash.
+ WebSocket.__error = function(message) {
+ logger.error(decodeURIComponent(message));
+ };
+
+ WebSocket.__addTask = function(task) {
+ if (WebSocket.__flash) {
+ task();
+ } else {
+ WebSocket.__tasks.push(task);
+ }
+ };
+
+ /**
+ * Test if the browser is running flash lite.
+ * @return {boolean} True if flash lite is running, false otherwise.
+ */
+ WebSocket.__isFlashLite = function() {
+ if (!window.navigator || !window.navigator.mimeTypes) {
+ return false;
+ }
+ var mimeType = window.navigator.mimeTypes["application/x-shockwave-flash"];
+ if (!mimeType || !mimeType.enabledPlugin || !mimeType.enabledPlugin.filename) {
+ return false;
+ }
+ return mimeType.enabledPlugin.filename.match(/flashlite/i) ? true : false;
+ };
+
+ if (!window.WEB_SOCKET_DISABLE_AUTO_INITIALIZATION) {
+ // NOTE:
+ // This fires immediately if web_socket.js is dynamically loaded after
+ // the document is loaded.
+ swfobject.addDomLoadEvent(function() {
+ WebSocket.__initialize();
+ });
+ }
+
+})();
diff --git a/web/app/assets/stylesheets/application.css b/web/app/assets/stylesheets/application.css
new file mode 100644
index 000000000..3b5cc6648
--- /dev/null
+++ b/web/app/assets/stylesheets/application.css
@@ -0,0 +1,13 @@
+/*
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
+ * listed below.
+ *
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
+ * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path.
+ *
+ * You're free to add application-wide styles to this file and they'll appear at the top of the
+ * compiled file, but it's generally better to create a new file per style scope.
+ *
+ *= require_self
+ *= require_tree .
+*/
diff --git a/web/app/assets/stylesheets/client/account.css.scss b/web/app/assets/stylesheets/client/account.css.scss
new file mode 100644
index 000000000..7b561c21a
--- /dev/null
+++ b/web/app/assets/stylesheets/client/account.css.scss
@@ -0,0 +1,161 @@
+@import 'common.css.scss';
+
+.account-content-scroller {
+
+ .content-wrapper {
+ padding:10px 35px;
+ }
+
+ .content-wrapper.account {
+ border-bottom: 1px dotted #444444;
+ color: #CCCCCC;
+ font-size: 15px;
+ overflow-x: hidden;
+ padding: 10px 35px;
+ white-space: nowrap;
+ }
+
+ h4 {
+ margin-bottom: 10px;
+ font-weight:bold;
+ }
+
+ form {
+ padding-right:10%;
+ }
+
+ label {
+ font-weight:400;
+ }
+
+ input[type=text], input[type=password] {
+ width:100%;
+ }
+
+ .error {
+ padding: 5px 15px 5px 5px;
+ }
+
+ .account-left {
+ float: left;
+ min-width: 165px;
+ width: 20%;
+ }
+ .account-left h2 {
+ color: #FFFFFF;
+ font-size: 23px;
+ font-weight: 400;
+ margin-bottom: 8px;
+ }
+ .account-mid {
+ float: left;
+ line-height: 150%;
+ min-width: 330px;
+ width: 50%;
+ }
+
+ .audio .audio-profiles-short{
+ white-space: normal;
+ }
+
+ .profile-instrumentlist {
+ background-color: #C5C5C5;
+ border: medium none;
+ box-shadow: 2px 2px 3px 0 #888888 inset;
+ color: #666666;
+ font-size: 14px;
+ height: 178px;
+ overflow: auto;
+ width: 100%;
+ }
+ .profile-instrumentlist select {
+ box-shadow: none !important;
+ color: #666666;
+ width: 100%;
+ }
+ .account-sub-description {
+ display: block;
+ white-space: normal;
+ }
+
+ .account-identity th {
+ font-weight : bold;
+ padding-bottom:10px;
+ }
+
+ a.small {
+ text-decoration:underline;
+ }
+
+ .button-orange, .button-grey {
+ line-height:14px;
+ margin-right:0;
+ }
+
+ .button-grey {
+ margin-right:6px;
+ }
+
+ div.field {
+ margin-bottom:27px;
+ }
+
+ div.profile-instrumentlist table {
+ border-collapse: separate;
+ border-spacing: 6px;
+ }
+
+ .account-edit-email, .account-edit-password {
+ width:35%;
+ }
+
+ .input-aligner {
+ margin-right:-4px;
+ }
+
+ .account-profile-avatar {
+
+ .avatar-space {
+ color: $color2;
+ margin-bottom: 20px;
+ position:relative;
+ min-height:300px;
+
+ img.preview_profile_avatar {
+ }
+ }
+
+
+ .spinner-large {
+ width:300px;
+ height:300px;
+ line-height: 300px;
+ position:absolute;
+ top:0;
+ left:0;
+ z-index: 2000; // to win over jcrop
+ }
+
+ .no-avatar-space {
+ border:1px dotted $color2;
+
+ color: $color2;
+ width:300px;
+ height:300px;
+ line-height: 300px;
+ text-align: center;
+ vertical-align: middle;
+ background-color:$ColorTextBoxBackground;
+
+ }
+ }
+
+ /** audio profiles */
+ .audioprofile {
+ .actions {
+ text-align: center;
+ }
+ }
+}
+
+
diff --git a/web/app/assets/stylesheets/client/banner.css.scss b/web/app/assets/stylesheets/client/banner.css.scss
new file mode 100644
index 000000000..e3b8de209
--- /dev/null
+++ b/web/app/assets/stylesheets/client/banner.css.scss
@@ -0,0 +1,8 @@
+#banner {
+ display:none;
+}
+
+#banner h2 {
+ font-weight:bold;
+ font-size:x-large;
+}
\ No newline at end of file
diff --git a/web/app/assets/stylesheets/client/client.css b/web/app/assets/stylesheets/client/client.css
new file mode 100644
index 000000000..7fb658934
--- /dev/null
+++ b/web/app/assets/stylesheets/client/client.css
@@ -0,0 +1,36 @@
+/*
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
+ * listed below.
+ *
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
+ * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path.
+ *
+ * You're free to add application-wide styles to this file and they'll appear at the top of the
+ * compiled file, but it's generally better to create a new file per style scope.
+ *
+ *= require_self
+ *= require ./ie
+ *= require ./jamkazam
+ *= require ./content
+ *= require ./faders
+ *= require ./header
+ *= require ./footer
+ *= require ./screen_common
+ *= require ./notify
+ *= require ./dialog
+ *= require ./sidebar
+ *= require ./home
+ *= require ./profile
+ *= require ./findSession
+ *= require ./session
+ *= require ./account
+ *= require ./search
+ *= require ./ftue
+ *= require ./createSession
+ *= require ./genreSelector
+ *= require ./sessionList
+ *= require ./searchResults
+ *= require ./banner
+ *= require ./clientUpdate
+ *= require jquery.Jcrop
+*/
\ No newline at end of file
diff --git a/web/app/assets/stylesheets/client/clientUpdate.css.scss b/web/app/assets/stylesheets/client/clientUpdate.css.scss
new file mode 100644
index 000000000..e5b1c5866
--- /dev/null
+++ b/web/app/assets/stylesheets/client/clientUpdate.css.scss
@@ -0,0 +1,26 @@
+#client_update {
+ display:none;
+}
+
+.progress-bar {
+ width:100%;
+ background-color:#000;
+ border: solid 1px #ED3618;
+ height:22px;
+}
+
+#progress-bar {
+ width:0%;
+}
+
+.progress-bar-progress {
+ background-color:#ED3618;
+ border:solid 1px #000;
+ height:20px;
+ display:block;
+}
+
+#client_update h2 {
+ font-weight:bold;
+ font-size:x-large;
+}
\ No newline at end of file
diff --git a/web/app/assets/stylesheets/client/common.css.scss b/web/app/assets/stylesheets/client/common.css.scss
new file mode 100644
index 000000000..67127c152
--- /dev/null
+++ b/web/app/assets/stylesheets/client/common.css.scss
@@ -0,0 +1,37 @@
+/*
+ * Variables used across files
+ */
+
+$ColorUIBackground: #262626; /* Dark Grey */
+/* $ColorScreenPrimary: #f34e1d; */ /* JW */
+$ColorScreenPrimary: #ed3618; /* Orange */
+$ColorElementPrimary: #0b6672; /* Teal */
+$ColorText: #ffffff; /* White */
+$ColorLink: #fc0;
+$ColorLinkHover: #82AEAF;
+$ColorSidebarText: #a0b9bd;
+$ColorScreenBackground: lighten($ColorUIBackground, 10%);
+$ColorTextBoxBackground: #c5c5c5;
+
+$color1: #006AB6; /* mid blue */
+$color2: #9A9084; /* warm gray */
+$color3: #B11254; /* magenta */
+$color4: #029FCA; /* bright blue */
+$color5: #B19975; /* tan */
+$color6: #F2532B; /* orange */
+$color7: #0A3369; /* deep blue */
+$color8: #FFC742; /* gold */
+$color9: #7D8590; /* slate blue */
+$color10: #81C882; /* seafoam */
+$color11: #f0eacb; /* warm white */
+$color12: shade($color5, 80%); /* warm black */
+$color13: #E9D384; /* wheat */
+$translucent1: rgba(#000, 0.3);
+$translucent2: rgba(#fff, 0.4);
+$text: #f3f1ee;
+
+$gradient-diff: 30%; $link: $color8;
+$border: hsl(210, 50%, 45%);
+
+
+
diff --git a/web/app/assets/stylesheets/client/content.css.scss b/web/app/assets/stylesheets/client/content.css.scss
new file mode 100644
index 000000000..96c769703
--- /dev/null
+++ b/web/app/assets/stylesheets/client/content.css.scss
@@ -0,0 +1,320 @@
+/* This is simply Jeff's content.css file */
+@charset "UTF-8";
+#content {
+ background-color: #353535;
+ border: 1px solid #ed3618;
+ clear: both;
+ float: left;
+ margin-top: 39px;
+ height: auto;
+ width: auto;
+ position:relative;
+ padding-bottom:3px;
+}
+
+.content-head {
+ height:21px;
+ padding:4px;
+ background-color:#ED3618;
+}
+
+.content-icon {
+ margin-right:10px;
+ float:left;
+}
+
+.content-head h1 {
+ margin: -6px 0px 0px 0px;
+ padding:0;
+ float:left;
+ font-weight:100;
+ font-size:24px;
+}
+
+.content-nav {
+ float:right;
+ margin-right:10px;
+}
+
+.home-icon {
+ float:left;
+ margin-right:20px;
+}
+
+.content-nav a.arrow-right {
+ float:left;
+ display:block;
+ margin-top:2px;
+ margin-right:10px;
+ width: 0;
+ height: 0;
+ border-top: 7px solid transparent;
+ border-bottom: 7px solid transparent;
+ border-left: 7px solid #FFF;
+}
+
+.content-nav a.arrow-left {
+ float:left;
+ display:block;
+ margin-top:2px;
+ margin-right:20px;
+ width: 0;
+ height: 0;
+ border-top: 7px solid transparent;
+ border-bottom: 7px solid transparent;
+ border-right:7px solid #FFF;
+}
+
+#content-scroller, .content-scroller {
+ height:inherit;
+ position:relative;
+ display:block;
+ overflow:auto;
+}
+
+.content-wrapper {
+ padding:10px 30px 10px 36px;
+ font-size:15px;
+ color:#ccc;
+ border-bottom: dotted 1px #444;
+ overflow-x:hidden;
+ white-space:nowrap;
+}
+
+.create-session-left {
+ width:50%;
+ float:left;
+}
+
+.create-session-right {
+ width:45%;
+ float:right;
+ font-size:13px;
+}
+
+.content-wrapper h2 {
+ color:#fff;
+ font-weight:600;
+ font-size:24px;
+}
+
+.content-wrapper select, .content-wrapper textarea, .content-wrapper input[type=text], .content-wrapper input[type=password], div.friendbox, .ftue-inner input[type=text], .ftue-inner input[type=password], .dialog-inner textarea, .dialog-inner input[type=text] {
+ font-family:"Raleway", arial, sans-serif;
+ background-color:#c5c5c5;
+ border:none;
+ -webkit-box-shadow: inset 2px 2px 3px 0px #888;
+ box-shadow: inset 2px 2px 3px 0px #888;
+ color:#666;
+}
+
+.create-session-description {
+ padding:5px;
+ width:100%;
+ height:80px;
+}
+
+.friendbox {
+ padding:5px;
+ width:100%;
+ height:60px;
+}
+
+.invite-friend {
+ margin:0px 4px 4px 4px;
+ float:left;
+ display:block;
+ background-color:#666;
+ color:#fff;
+ font-size:12px;
+ -webkit-border-radius: 7px;
+ border-radius: 7px;
+ padding:2px 2px 2px 4px;
+}
+
+.content-wrapper div.friendbox input[type=text] {
+ -webkit-box-shadow: inset 0px 0px 0px 0px #888;
+ box-shadow: inset 0px 0px 0px 0px #888;
+ color:#666;
+ font-style:italic;
+}
+
+#genrelist, #musicianlist {
+ position:relative;
+ z-index:99;
+ width: 175px;
+ -webkit-border-radius: 6px;
+ border-radius: 6px;
+ background-color:#C5C5C5;
+ border: none;
+ color:#333;
+ font-weight:400;
+ padding:0px 0px 0px 8px;
+ height:20px;
+ line-height:20px;
+ overflow:hidden;
+ -webkit-box-shadow: inset 2px 2px 3px 0px #888;
+ box-shadow: inset 2px 2px 3px 0px #888;
+}
+
+#musicianlist, .session-controls #genrelist {
+ width: 150px;
+}
+
+#genrelist a, #musicianlist a {
+ color:#333;
+ text-decoration:none;
+}
+
+.genre-wrapper, .musician-wrapper {
+ float:left;
+ width:175px;
+ height:127px;
+ overflow:auto;
+}
+
+.musician-wrapper, .session-controls .genre-wrapper {
+ width:150px;
+}
+
+.genrecategory {
+ font-size:11px;
+ float:left;
+ width:135px;
+}
+
+.filtercategory, .session-controls .genrecategory {
+ font-size:11px;
+ float:left;
+ width:110px;
+}
+
+a.arrow-up {
+ float:right;
+ margin-right:5px;
+ display:block;
+ margin-top:6px;
+ width: 0;
+ height: 0;
+ border-left: 7px solid transparent;
+ border-right: 7px solid transparent;
+ border-bottom: 7px solid #333;
+}
+
+a.arrow-down {
+ float:right;
+ margin-right:5px;
+ display:block;
+ margin-top:6px;
+ width: 0;
+ height: 0;
+ border-left: 7px solid transparent;
+ border-right: 7px solid transparent;
+ border-top: 7px solid #333;
+}
+
+.settings-session-description {
+ padding:10px;
+ width:300px;
+}
+
+#session-controls {
+ width:100%;
+ padding:11px 0px 11px 0px;
+ background-color:#4c4c4c;
+ min-height:20px;
+ overflow-x:hidden;
+ }
+
+#session-controls .searchbox {
+ float:left;
+ width:140px;
+ margin-left: 10px;
+ -webkit-border-radius: 6px;
+ border-radius: 6px;
+ background-color:#C5C5C5;
+ border: none;
+ color:#333;
+ font-weight:400;
+ padding:0px 0px 0px 8px;
+ height:20px;
+ line-height:20px;
+ overflow:hidden;
+ -webkit-box-shadow: inset 2px 2px 3px 0px #888;
+ box-shadow: inset 2px 2px 3px 0px #888;
+}
+
+#session-controls input[type=text] {
+ background-color:#c5c5c5;
+ border:none;
+ color:#666;
+}
+
+.avatar-tiny {
+ float:left;
+ padding:1px;
+ width:24px;
+ height:24px;
+ background-color:#ed3618;
+ -webkit-border-radius:12px;
+ -moz-border-radius:12px;
+ border-radius:12px;
+}
+
+.ftue-background {
+ background-image:url(../images/content/bkg_ftue.jpg);
+ background-repeat:no-repeat;
+ background-size:cover;
+ min-height:475px;
+ min-width:672px;
+}
+
+table.generaltable {
+ background-color: #262626;
+ border: 1px solid #4D4D4D;
+ color: #FFFFFF;
+ font-size: 11px;
+ margin-top: 6px;
+ width: 100%;
+
+ th {
+ background-color: #4D4D4D;
+ border-right: 1px solid #333333;
+ font-weight: 300;
+ padding: 6px;
+ }
+
+ td {
+ border-right: 1px solid #333333;
+ border-top: 1px solid #333333;
+ padding: 9px 5px 5px;
+ vertical-align: top;
+ white-space: normal;
+ }
+
+ .noborder {
+ border-right: medium none;
+ }
+}
+
+ul.account-shortcuts {
+ border:1px solid #ED3618;
+
+ li {
+ margin:0;
+ height:20px;
+ line-height:20px;
+ padding:2px;
+ }
+
+ .account-home {
+ border-bottom:1px;
+ border-style:solid;
+ border-color:#ED3618;
+ }
+
+ .audio {
+ border-bottom:1px;
+ border-style:solid;
+ border-color:#ED3618;
+ }
+}
diff --git a/web/app/assets/stylesheets/client/createSession.css.scss b/web/app/assets/stylesheets/client/createSession.css.scss
new file mode 100644
index 000000000..1c3489b48
--- /dev/null
+++ b/web/app/assets/stylesheets/client/createSession.css.scss
@@ -0,0 +1,142 @@
+.session-left {
+ width:40%;
+ float:left;
+ padding-top:10px;
+ margin-left:35px;
+}
+
+#create-session-genre select, #create-session-band select {
+ width:145px;
+}
+
+#find-session-genre select, #find-session-musician select {
+ width:145px;
+}
+
+.session-right {
+ width:50%;
+ float:right;
+ font-size:13px;
+ padding-top:10px;
+ margin-right:35px;
+}
+
+.session-description {
+ padding:5px;
+ width:80%;
+ height:80px;
+}
+
+.radio-text {
+ font-size:13px;
+}
+
+.friendbox {
+ padding:5px;
+ height:60px;
+ width:75%;
+}
+
+.terms-checkbox {
+ float:left;
+ display:block;
+ margin-right:5px;
+}
+
+.terms {
+ font-size:11px;
+ width:auto;
+ display:block;
+ white-space:normal;
+}
+
+div.friendbox {
+ background-color:#c5c5c5;
+ border:none;
+ -webkit-box-shadow: inset 2px 2px 3px 0px #888;
+ box-shadow: inset 2px 2px 3px 0px #888;
+ color:#333;
+}
+
+div.friendbox input[type=text] {
+ -webkit-box-shadow: inset 0px 0px 0px 0px #888;
+ box-shadow: inset 0px 0px 0px 0px #888;
+ color:#666;
+ font-style:italic;
+}
+
+.invitation {
+ margin:0px 4px 4px 4px;
+ float:left;
+ display:block;
+ background-color:#666;
+ color:#fff;
+ font-size:12px;
+ -webkit-border-radius: 7px;
+ border-radius: 7px;
+ padding:2px 2px 2px 4px;
+ vertical-align:middle;
+}
+
+.choosefriends-overlay {
+ width:384px;
+ height:344px;
+ padding:8px;
+ background-color:#787878;
+ position:fixed;
+ top:20%;
+ left:50%;
+ margin-left:-200px;
+}
+
+.choosefriends-inner {
+ height:300px;
+ overflow:auto;
+ background-color:#262626;
+}
+
+.choosefriends-inner tr.selected td {
+ background-color:#333333;
+ color:#787878;
+ cursor:default;
+}
+
+.choosefriends-inner tr.selected div, .choosefriends-inner tr.selected span {
+ opacity:.5;
+ -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=50)";
+ filter: alpha(opacity=50);
+}
+
+.choosefriends-inner td {
+ padding:10px;
+ border-bottom:solid 1px #999;
+ cursor:pointer;
+}
+
+.invitation-overlay {
+ width:384px;
+ height:344px;
+ padding:8px;
+ background-color:#787878;
+ position:fixed;
+ top:20%;
+ left:50%;
+ margin-left:-200px;
+}
+
+.invitation-overlay textarea {
+ font-family:"Raleway", arial, sans-serif;
+ background-color:#c5c5c5;
+ border:none;
+ -webkit-box-shadow: inset 2px 2px 3px 0px #888;
+ box-shadow: inset 2px 2px 3px 0px #888;
+ color:#666;
+ height:75px;
+ width:300px;
+}
+
+.invitation-inner {
+ height:300px;
+ overflow:auto;
+ background-color:#262626;
+}
\ No newline at end of file
diff --git a/web/app/assets/stylesheets/client/dialog.css.scss b/web/app/assets/stylesheets/client/dialog.css.scss
new file mode 100644
index 000000000..d1c188cfa
--- /dev/null
+++ b/web/app/assets/stylesheets/client/dialog.css.scss
@@ -0,0 +1,73 @@
+@import "client/common.css.scss";
+
+.dialog {
+ display:none;
+ background-color: #333;
+ border: 1px solid $ColorScreenPrimary;
+ color:#fff;
+ min-width: 400px;
+ min-height: 350px;
+ z-index: 100;
+}
+
+.dialog-inner {
+ padding:25px;
+ color:#aaa;
+ font-size:15px;
+}
+
+.dialog-tabs {
+ text-align:center;
+ margin-bottom:10px;
+ border-bottom:solid 1px #999;
+}
+
+.dialog-tabs a {
+ padding:10px;
+ background-color:#666;
+ color:#999;
+ text-decoration:none;
+}
+
+.dialog-tabs a.selected, .dialog-tabs a:hover {
+ background-color:#999;
+ color:#fff;
+}
+
+.dialog .tab {
+}
+
+.dialog-fixed {
+ min-height:600px;
+}
+
+.dialog-overlay-sm {
+ width:600px;
+ height:auto;
+ position:fixed;
+ left:50%;
+ top:20%;
+ margin-left:-300px;
+ background-color:#333;
+ border: 1px solid #ed3618;
+ z-index: 1000;
+}
+
+.dialog-overlay .dialog-inner {
+ width:750px;
+ height:auto;
+ padding:25px;
+ font-size:15px;
+ color:#aaa;
+}
+
+.dialog-overlay-sm .dialog-inner {
+ width:550px;
+ height:auto;
+ padding:25px;
+ font-size:15px;
+ color:#aaa;
+}
+
+
+
diff --git a/web/app/assets/stylesheets/client/faders.css.scss b/web/app/assets/stylesheets/client/faders.css.scss
new file mode 100644
index 000000000..5e075a5c5
--- /dev/null
+++ b/web/app/assets/stylesheets/client/faders.css.scss
@@ -0,0 +1,43 @@
+/* Fader Styles */
+
+.fader {
+ margin: 0px;
+ padding: 0px;
+ cursor: pointer;
+ position:relative;
+}
+
+.fader.vertical {
+ width: 28px;
+ background-image: url('/assets/content/bkg_gain_slider.png');
+ background-repeat:repeat-y;
+ background-position:bottom;
+}
+
+.fader.horizontal {
+ height: 17px;
+ background-image: url('/assets/content/bkg_slider_gain_horiz.png');
+ background-repeat:repeat-x;
+ background-position:left;
+}
+
+.fader .handle {
+ margin: 0px;
+ padding: 0px;
+ position:absolute;
+ cursor:pointer;
+ left:0px;
+}
+.fader.vertical .handle {
+ width:28px;
+ height:11px;
+ bottom:0px;
+}
+
+.fader.horizontal .handle {
+ width:8px;
+ height:17px;
+ top:0px;
+}
+
+
diff --git a/web/app/assets/stylesheets/client/findSession.css.scss b/web/app/assets/stylesheets/client/findSession.css.scss
new file mode 100644
index 000000000..df8e8de5e
--- /dev/null
+++ b/web/app/assets/stylesheets/client/findSession.css.scss
@@ -0,0 +1,24 @@
+div[layout-id="findSession"] {
+
+ th, td { margin: 4px; padding:4px; }
+
+}
+
+.session-filter {
+ width:100%;
+ padding-top: 11px;
+ padding-bottom: 11px;
+ background-color:#4c4c4c;
+ min-height:20px;
+ overflow-x:hidden;
+ vertical-align:middle;
+}
+
+.session-filter select {
+ font-family:"Raleway", arial, sans-serif;
+ background-color:#c5c5c5;
+ border:none;
+ -webkit-box-shadow: inset 2px 2px 3px 0px #888;
+ box-shadow: inset 2px 2px 3px 0px #888;
+ color:#666;
+}
\ No newline at end of file
diff --git a/web/app/assets/stylesheets/client/footer.css.scss b/web/app/assets/stylesheets/client/footer.css.scss
new file mode 100644
index 000000000..91463d23e
--- /dev/null
+++ b/web/app/assets/stylesheets/client/footer.css.scss
@@ -0,0 +1,40 @@
+@import "client/common.css.scss";
+
+@charset "UTF-8";
+#footer {
+ position:absolute;
+ bottom:0;
+ width:100%;
+ margin-top: 30px;
+ padding-top: 10px;
+ border-top:solid 1px #444;
+ height:13px;
+}
+
+#copyright {
+ float:left;
+ font-size:11px;
+ color:#ccc;
+}
+
+#footer-links {
+ float:right;
+ font-size:11px;
+ color:#ccc;
+}
+
+#footer-links a {
+ color:#ccc;
+ text-decoration:none;
+}
+
+#footer-links a:hover {
+ color:#fff;
+ text-decoration:underline;
+}
+
+#version {
+ font-size:11px;
+ color:#ccc;
+ text-align: center;
+}
\ No newline at end of file
diff --git a/web/app/assets/stylesheets/client/ftue.css.scss b/web/app/assets/stylesheets/client/ftue.css.scss
new file mode 100644
index 000000000..a1cb9228d
--- /dev/null
+++ b/web/app/assets/stylesheets/client/ftue.css.scss
@@ -0,0 +1,438 @@
+/* Custom Styles for the FTUE Dialogs */
+
+@import "client/common.css.scss";
+@charset "UTF-8";
+
+/* Jonathon's FTUE overrides */
+
+div.dialog.ftue .ftue-inner div[layout-wizard-step="1"] {
+ p.intro {
+ margin-top:0px;
+ }
+
+ ul {
+ margin:0px;
+ padding:0px;
+ margin-top:20px;
+ }
+
+ li {
+ text-align:center;
+ width: 31%;
+ height: 170px;
+ margin:0px;
+ /*padding:0px;*/
+ list-style: none;
+ float:left;
+ }
+ li.first {
+ width: 34%;
+ }
+}
+div.dialog.ftue .ftue-inner div[layout-wizard-step="3"] {
+ h5 {
+ color:#fff;
+ font-weight: bold;
+ margin:0px;
+ padding:0px;
+ margin-top:16px;
+ }
+ p {
+ margin-top:0px;
+ }
+}
+
+
+
+#ftue-latency-needle {
+ position:absolute;
+ width: 6px;
+ height: 220px;
+ top:50%;
+ left:50%;
+ overflow:visible;
+ margin: -106px -3px 0px 0px;
+ padding: 0px 0px 0px 0px;
+}
+#ftue-latency-numerical {
+ position:absolute;
+ width: 150px;
+ height: 32px;
+ top: 228px;
+ left: 50%;
+ margin: -73px;
+ text-align:center;
+ font-size: 1.8em;
+ font-weight: bold;
+}
+#ftue-latency-label {
+ position:absolute;
+ width: 150px;
+ height: 32px;
+ top: 250px;
+ left: 50%;
+ margin: -73px;
+ text-align:center;
+ font-size: 0.9em;
+}
+
+div.dialog.ftue {
+ min-width: 800px;
+ max-width: 800px;
+ min-height: 400px;
+ max-height: 400px;
+
+ .ftue-inner {
+ line-height: 1.3em;
+ width: auto;
+
+ a {
+ text-decoration: underline;
+ }
+
+ p {
+ margin-top: 12px;
+ }
+
+ ul {
+ list-style:disc;
+ }
+
+ li {
+ margin: 15px 12px 15px 36px;
+ }
+
+ select {
+ max-width: 220px;
+ }
+
+ .settings-profile {
+ margin-top: 12px;
+ }
+
+ .asio-settings {
+ clear:both;
+ position:relative;
+ width:100%;
+ height: 54px;
+ margin-top: 8px;
+ .column {
+ position:absolute;
+ width: 220px;
+ height: 50px;
+ margin-right:8px;
+ }
+ .settings-driver {
+ left:0px;
+ }
+ .settings-asio {
+ left:50%;
+ margin-left: -110px;
+ }
+ .settings-asio.mac {
+ left:0px;
+ margin-left: 0px;
+ }
+ .settings-asio-button {
+ right:0px;
+ height: 45px;
+ .bottom {
+ position:absolute;
+ bottom:0px;
+ }
+ }
+ .settings-asio-button.mac {
+ right:auto;
+ left:50%;
+ margin-left: -110px;
+ }
+ .subcolumn {
+ position:absolute;
+ width: 68px;
+ height: 48px;
+ }
+ .subcolumn select {
+ width: 68px;
+ }
+ .subcolumn.first {
+ left:0px;
+ }
+ .subcolumn.second {
+ left:50%;
+ margin-left:-34px;
+ }
+ .subcolumn.third {
+ right:0px;
+ }
+
+ }
+ .settings-controls {
+
+ clear:both;
+ position:relative;
+ width: 100%;
+ height: 100px;
+
+ div.section {
+ position:absolute;
+ width: 220px;
+ height: 100px;
+
+ select {
+ display:block;
+ width: 100%;
+ }
+ }
+ .audio-input {
+ left:0px;
+ }
+ .voice-chat-input {
+ left:50%;
+ margin-left: -110px;
+ }
+ .audio-output {
+ right:0px;
+ }
+ .ftue-controls {
+ margin-top: 16px;
+ position:relative;
+ height: 48px;
+ width: 220px;
+ background-color: #222;
+ }
+ .ftue-vu-left {
+ position:relative;
+ top: 0px;
+ }
+ .ftue-vu-right {
+ position:relative;
+ top: 22px;
+ }
+ .ftue-fader {
+ position:relative;
+ top: 14px;
+ left: 8px;
+ }
+ .gain-label {
+ color: $ColorScreenPrimary;
+ position:absolute;
+ top: 14px;
+ right: 6px;
+ }
+ }
+
+ .buttonbar {
+ position:absolute;
+ bottom: 24px;
+ right: 0px;
+ a {
+ color: darken(#fff, 5);
+ text-decoration: none;
+ }
+ }
+
+ }
+}
+
+/*
+div[layout-id="ftue2"] {
+ .formrow {
+ position:relative;
+ clear:both;
+ border:none;
+ margin: 12px;
+ padding:4px;
+ }
+ label {
+ text-align:left;
+ }
+ select {
+ display:block;
+ position:absolute;
+ left: 140px;
+ top: 0px;
+ }
+}
+
+div[layout-id="ftue3"] {
+ button {
+ margin: 0px 2em;
+ }
+
+ .progressContainer {
+ margin: 2em;
+ border: 1px solid #fff;
+ width: 400px;
+ height: 10px;
+ }
+
+ .progressFull {
+ height: 10px;
+ width: 2px;
+ background-color:#0f0;
+ }
+}
+*/
+
+/* Start Jeff's ftue.css */
+.signin-overlay {
+ z-index:100;
+ width:800px;
+ height:auto;
+ position:absolute;
+ left:50%;
+ top:150px;
+ margin-left:-400px;
+ background-color:#333;
+ border: 1px solid #ed3618;
+}
+
+.ftue-overlay {
+ z-index:100;
+ width:800px;
+ height:400px;
+ position:absolute;
+ left:50%;
+ top:20%;
+ margin-left:-400px;
+ background-color:#333;
+ border: 1px solid #ed3618;
+}
+
+.ftue-inner {
+ width:750px;
+ padding:25px;
+ font-size:15px;
+ color:#aaa;
+}
+
+.ftue-inner input[type=text], .ftue-inner input[type=password] {
+ padding:3px;
+ font-size:13px;
+ width:145px;
+}
+
+.ftue-inner input.gearprofile {
+ width:auto;
+}
+
+.ftue-inner select.audiodropdown {
+ width:223px;
+ color:#666;
+}
+
+.ftue-left {
+ float:left;
+ width:340px;
+ padding-right:30px;
+ border-right:solid 1px #484848;
+}
+
+.ftue-right {
+ width: 340px;
+ padding-left:30px;
+ float:left;
+}
+
+.ftue-inner a {
+ color:#ccc;
+}
+
+.ftue-inner a:hover {
+ color:#fff;
+}
+
+.ftue-instrumentlist {
+ width:340px;
+ height:178px;
+ background-color:#c5c5c5;
+ border:none;
+ -webkit-box-shadow: inset 2px 2px 3px 0px #888;
+ box-shadow: inset 2px 2px 3px 0px #888;
+ color:#666;
+ overflow:auto;
+ font-size:14px;
+}
+
+.ftue-instrumentlist select {
+ width:100%;
+ color:#666;
+}
+
+table.audiogeartable {
+ margin-left:-20px;
+ margin-right:-20px;
+}
+
+.ftue-gainmeter {
+ width:223px;
+ height:55px;
+ display:inline-block;
+ margin-right:8px;
+ position:relative;
+ background-color:#242323;
+}
+
+.gainmeter-vu-top {
+ position:absolute;
+ left:0px;
+ top:0px;
+}
+
+.gainmeter-vu-bottom {
+ position:absolute;
+ left:0px;
+ bottom:0px;
+}
+
+.gainmeter-gain {
+ position:absolute;
+ top:18px;
+ left:10px;
+ width:150px;
+ height:18px;
+ background-image:url(../images/content/bkg_slider_gain_horiz.png);
+ background-repeat:repeat-x;
+}
+
+.gainmeter-gain-wrapper {
+ position:relative;
+ width:51px;
+ height:18px;
+}
+
+.gainmeter-gain-slider {
+ position:absolute;
+ top:0px;
+ left:60%;
+ width:10px;
+ height:18px;
+}
+
+.gainmeter-label {
+ font-size:16px;
+ color:#ED3618;
+ position:absolute;
+ top:18px;
+ right:16px;
+}
+/* End Jeff's CSS */
+
+/* Added to deal with invitation codes */
+.ftue-invited, {
+ width:750px;
+ padding:25px;
+ font-size:15px;
+ color:#aaa;
+}
+
+.ftue-video-link {
+ padding:4px;
+ cursor:pointer;
+ background-color:#333;
+ border: 1px solid #333;
+}
+.ftue-video-link.hover {
+ background-color:#444;
+ border: 1px solid #555;
+}
diff --git a/web/app/assets/stylesheets/client/genreSelector.css.scss b/web/app/assets/stylesheets/client/genreSelector.css.scss
new file mode 100644
index 000000000..e69de29bb
diff --git a/web/app/assets/stylesheets/client/header.css.scss b/web/app/assets/stylesheets/client/header.css.scss
new file mode 100644
index 000000000..dc69a1a09
--- /dev/null
+++ b/web/app/assets/stylesheets/client/header.css.scss
@@ -0,0 +1,137 @@
+@charset "UTF-8";
+@import "compass/utilities/text/replacement";
+@import "client/common.css.scss";
+
+.header {
+ height: 55px;
+ width: 100%;
+ z-index:5;
+}
+
+div[layout="header"] h1 {
+ cursor:pointer;
+ width: 247px;
+ height:45px;
+ @include replace-text(image-url("header/logo.png"));
+ float:left;
+}
+
+#logo {
+ float:left;
+ width:247px;
+}
+
+#profile {
+ width:auto;
+ float:right;
+ height:54px;
+}
+
+.avatar_large {
+ float:left;
+ padding:2px;
+ width:54px;
+ height:54px;
+ background-color: $ColorScreenPrimary;
+ -webkit-border-radius:28px;
+ -moz-border-radius:28px;
+ border-radius:28px;
+}
+
+.avatar_large img {
+ width:54px;
+ height:54px;
+ -webkit-border-radius:26px;
+ -moz-border-radius:26px;
+ border-radius:26px;
+}
+
+#user {
+ margin:18px 0px 0px 10px;
+ font-size:20px;
+ font-weight:200;
+ color:#ccc;
+ float:left;
+}
+
+.arrow-down {
+ float:left;
+ cursor:pointer;
+ margin-left:16px;
+ margin-top:26px;
+ width: 0;
+ height: 0;
+ border-left: 8px solid transparent;
+ border-right: 8px solid transparent;
+ border-top: 8px solid #fff;
+}
+
+.userinfo {
+ cursor:pointer;
+}
+
+.userinfo ul {
+ clear:both;
+ background: scale-lightness($ColorUIBackground, 10%);
+ display:none;
+}
+.userinfo li {
+ display:block;
+ margin: 2px;
+ padding: 2px;
+ background: scale-lightness($ColorUIBackground, 20%);
+}
+
+
+/*
+div[layout="header"] h1 {
+ width: 252px;
+ height:47px;
+ @include replace-text(image-url("logo.png"));
+}
+*/
+
+/*.header h1 {*/
+ /*margin:22px;*/
+ /*font-size:300%;*/
+ /*font-family: 'LatoLight', Arial, sans-serif;*/
+/*}*/
+
+/*
+.userinfo {
+ position:absolute;
+ right:0px;
+ top:0px;
+ width: 266px;
+ z-index:5;
+}
+.userinfo img.avatar {
+ float:left;
+}
+.userinfo .username {
+ float:left;
+ margin-top: 18px;
+ cursor: pointer;
+}
+.userinfo h2 {
+ font-size:120%;
+ font-weight: bold;
+ float:left;
+ margin-right:4px;
+}
+.userinfo .profile-toggle {
+ display:inline-block;
+}
+.userinfo ul {
+ clear:both;
+ background: $color7;
+ display:none;
+}
+.userinfo li {
+ display:block;
+ margin: 2px;
+ padding: 2px;
+ background: scale-lightness($color7, 10%);
+}
+*/
+
diff --git a/web/app/assets/stylesheets/client/home.css.scss b/web/app/assets/stylesheets/client/home.css.scss
new file mode 100644
index 000000000..953d7f87e
--- /dev/null
+++ b/web/app/assets/stylesheets/client/home.css.scss
@@ -0,0 +1,98 @@
+@import "compass/css3/images";
+@import "compass/css3/background-size";
+@import "client/common.css.scss";
+
+.homecard {
+ cursor:pointer;
+ background-color: shade($ColorScreenPrimary, 10%);
+ background-repeat: no-repeat;
+ background-position: bottom left;
+ border: 1px solid $translucent1;
+}
+.homecard.createsession {
+ background-image: url(/assets/content/bkg_home_create.jpg);
+}
+.homecard.createsession-disabled {
+ cursor:default;
+ background-color:#333;
+ background-image: url(/assets/content/bkg_home_create_disabled.jpg);
+}
+.homecard.findsession {
+ background-image: url(/assets/content/bkg_home_find.jpg);
+}
+.homecard.findsession-disabled {
+ cursor:default;
+ background-color:#333;
+ background-image: url(/assets/content/bkg_home_find_disabled.jpg);
+}
+.homecard.profile {
+ background-image: url(/assets/content/bkg_home_profile.jpg);
+}
+.homecard.feed {
+ background-image: url(/assets/content/bkg_home_feed.jpg);
+}
+.homecard.account {
+ background-image: url(/assets/content/bkg_home_account.jpg);
+}
+.homecard.bands {
+ background-image: url(/assets/content/bkg_home_bands.jpg);
+}
+.homecard.musicians {
+ background-image: url(/assets/content/bkg_home_musicians.jpg);
+}
+
+.homebox-info {
+ position: absolute;
+ bottom: 7px;
+ right: 7px;
+ font-size:12px;
+ font-weight: 200;
+}
+
+.homecard h2 {
+ background-color: $ColorScreenPrimary;
+ margin: 0px;
+ padding: 3px;
+ text-align:right;
+ font-size:24px;
+ font-weight: 100;
+}
+
+.homecard h2.disabled {
+ background-color: #3f3f3f;
+ margin: 0px;
+ padding: 3px;
+ text-align:right;
+ font-size:24px;
+ font-weight: 100;
+ color:#666;
+}
+
+.homecard.hover {
+ border: 1px solid $translucent2 !important;
+ background-color: #b32712;
+}
+.homecard.createsession.hover {
+ background-image: url(/assets/content/bkg_home_create_x.jpg);
+}
+.homecard.findsession.hover {
+ background-image: url(/assets/content/bkg_home_find_x.jpg);
+}
+.homecard.profile.hover {
+ background-image: url(/assets/content/bkg_home_profile_x.jpg);
+}
+.homecard.feed.hover {
+ background-image: url(/assets/content/bkg_home_feed_x.jpg);
+}
+.homecard.account.hover {
+ background-image: url(/assets/content/bkg_home_account_x.jpg);
+}
+.homecard.bands.hover {
+ background-image: url(/assets/content/bkg_home_bands_x.jpg);
+}
+.homecard.musicians.hover {
+ background-image: url(/assets/content/bkg_home_musicians_x.jpg);
+}
+
+
+
diff --git a/web/app/assets/stylesheets/client/ie.css.scss b/web/app/assets/stylesheets/client/ie.css.scss
new file mode 100644
index 000000000..6e6016af1
--- /dev/null
+++ b/web/app/assets/stylesheets/client/ie.css.scss
@@ -0,0 +1,27 @@
+/* Welcome to Compass. Use this file to write IE specific override styles.
+ * Import this file using the following HTML or equivalent:
+ * */
+
+@import "compass/css3/images";
+@import "client/common.css.scss";
+
+/* Gradients in IE work with filter-gradient, but mess up event handling.
+ * Using solid colors via background-color for now.
+ */
+.homecard.createsession {
+/* @include filter-gradient(scale-lightness($color10, $gradient-diff), $color10, "horizontal"); */
+}
+.homecard.findsession {
+/* @include filter-gradient(scale-lightness($color6, $gradient-diff), $color6, "horizontal"); */
+}
+.homecard.practice {
+/* @include filter-gradient(scale-lightness($color9, $gradient-diff), $color9, "horizontal"); */
+}
+.homecard.bands {
+/* @include filter-gradient(scale-lightness($color1, $gradient-diff), $color1, "horizontal"); */
+}
+.homecard.recordings {
+/* @include filter-gradient(scale-lightness($color5, $gradient-diff), $color5, "horizontal"); */
+}
diff --git a/web/app/assets/stylesheets/client/jamkazam.css.scss b/web/app/assets/stylesheets/client/jamkazam.css.scss
new file mode 100644
index 000000000..48f71a781
--- /dev/null
+++ b/web/app/assets/stylesheets/client/jamkazam.css.scss
@@ -0,0 +1,446 @@
+/* Welcome to Compass.
+ * In this file you should write your main styles. (or centralize your imports)
+ * Import this file using the following HTML or equivalent:
+ * */
+
+
+@import "compass/reset";
+@import "compass/css3/images";
+@import "compass/css3/background-size";
+@import "compass/css3/opacity";
+
+@import "client/common.css.scss";
+
+body {
+ color: $ColorText;
+ background-color: $ColorUIBackground;
+ width: 100%;
+ height: 100%;
+ overflow: hidden;
+ font-family: Raleway, Arial, Helvetica, sans-serif;
+ font-size: 14px;
+ font-weight: 300;
+}
+
+b { font-weight: bold; }
+
+a {
+ cursor:pointer;
+ color: $ColorLink;
+ text-decoration: none;
+ display: inline-block;
+}
+
+[layout-link] {
+ cursor:pointer;
+}
+
+a:hover {
+ text-decoration: underline;
+ color: $ColorLinkHover;
+}
+
+a.arrow-up {
+ float:right;
+ margin-right:5px;
+ display:block;
+ margin-top:6px;
+ width: 0;
+ height: 0;
+ border-left: 7px solid transparent;
+ border-right: 7px solid transparent;
+ border-bottom: 7px solid #333;
+}
+
+a.arrow-down {
+ float:right;
+ margin-right:5px;
+ display:block;
+ margin-top:6px;
+ width: 0;
+ height: 0;
+ border-left: 7px solid transparent;
+ border-right: 7px solid transparent;
+ border-top: 7px solid #333;
+}
+
+select {
+ padding:3px;
+ font-size:15px;
+}
+
+form .body {
+ /* TODO - size with layout */
+ width: 100%;
+ height: 40%;
+ max-height: 40%;
+ overflow:auto;
+}
+
+.formrow {
+ margin: 1em;
+ padding: 1em;
+ border: 1px solid #555;
+ float:left;
+}
+
+label {
+ display:block;
+}
+
+.button1 {
+ background: $color1;
+ color: #fff !important;
+ font-weight:bold;
+ font-size: 120%;
+ margin: 2px;
+ padding:8px;
+}
+
+input[type="button"] {
+ margin:0px 8px 0px 8px;
+ background-color:#666;
+ border: solid 1px #868686;
+ outline:solid 2px #666;
+ padding:3px 10px;
+ font-family:raleway;
+ font-size:12px;
+ font-weight:300;
+ cursor:pointer;
+ color:#ccc;
+}
+
+.clearall {
+ clear:both;
+}
+
+.left {
+ float:left;
+}
+
+.right {
+ float:right;
+}
+
+.hidden {
+ display:none;
+}
+
+.small {
+ font-size:11px;
+}
+
+.curtain {
+ background-color: $ColorScreenBackground;
+ position:absolute;
+ top:0px;
+ left:0px;
+}
+.curtain .splash {
+ position:absolute;
+ left:50%;
+ top:50%;
+ margin-top:-100px;
+ margin-left:-125px;
+}
+.curtain .splash img {}
+.curtain .splash p {
+ width: 100%;
+ text-align:center;
+}
+
+.dialog-overlay {
+ background-color: #000;
+}
+
+.avatar {
+ display: block;
+ float:left;
+}
+
+.avatar.medium {
+ width: 48px;
+ height: 48px;
+ margin-right: 12px;
+}
+
+.avatar.small {
+ width: 32px;
+ height: 32px;
+ margin-right: 8px;
+}
+
+.avatar-tiny {
+ width:20px;
+ heigh:20px;
+ -webkit-border-radius:18px;
+ -moz-border-radius:18px;
+ border-radius:18px;
+}
+
+/* Invitations */
+#invitation-controls {
+ clear:both;
+}
+
+/* TODO - generalize */
+.instrument {
+ margin: 1px;
+ background: $ColorElementPrimary;
+ color:$ColorText;
+ padding: 3px;
+ width:auto;
+ float:left;
+}
+
+.instrument span {
+ font-size:85%;
+ font-weight:bold;
+ cursor:pointer;
+}
+
+.profiency {
+ float:left;
+}
+
+/* Autocomplete */
+.autocomplete {
+ background:$ColorScreenBackground;
+ color:$ColorText;
+ cursor:default;
+ text-align:left;
+ max-height:350px;
+ overflow:auto;
+ margin:-1px 6px 6px -6px;
+ _height:350px; /* IE6 specific: */
+ _margin:0;
+ _overflow-x:hidden;
+}
+
+.autocomplete .selected {
+ background:$ColorScreenBackground;
+ cursor:pointer;
+}
+
+.autocomplete div {
+ padding:5px 5px 5px 5px;
+ white-space:nowrap;
+ overflow:hidden;
+ border-bottom:1px solid #999;
+ border-left:1px solid #999;
+ border-right:1px solid #999;
+ text-align:center;
+}
+
+.autocomplete strong {
+ font-weight:normal;
+}
+
+.multiselect-dropdown {
+ position:relative;
+ z-index:99;
+ width: 175px;
+ -webkit-border-radius: 6px;
+ border-radius: 6px;
+ background-color:#C5C5C5;
+ border: none;
+ color:#333;
+ font-weight:400;
+ padding:0px 0px 0px 8px;
+ height:20px;
+ line-height:20px;
+ overflow:hidden;
+ -webkit-box-shadow: inset 2px 2px 3px 0px #888;
+ box-shadow: inset 2px 2px 3px 0px #888;
+}
+
+.multiselect-dropdown a {
+ color:#333;
+ text-decoration:none;
+}
+
+.list-items {
+ float:left;
+ width:175px;
+ height:127px;
+ overflow:auto;
+}
+
+.list-item-text {
+ font-size:11px;
+ float:left;
+ width:135px;
+}
+
+.search-box {
+ float:left;
+ width:140px;
+ margin-left: 10px;
+ -webkit-border-radius: 6px;
+ border-radius: 6px;
+ background-color:$ColorTextBoxBackground;
+ border: none;
+ color:#333;
+ font-weight:400;
+ padding:0px 0px 0px 8px;
+ height:18px;
+ line-height:18px;
+ overflow:hidden;
+ -webkit-box-shadow: inset 2px 2px 3px 0px #888;
+ box-shadow: inset 2px 2px 3px 0px #888;
+}
+
+input[type="text"], input[type="password"]{
+ background-color:$ColorTextBoxBackground;
+ border:none;
+ color:#666;
+ padding:3px;
+ font-size:15px;
+}
+
+
+.mr10 {
+ margin-right:10px;
+}
+
+.mr20 {
+ margin-right:20px;
+}
+
+.mr30 {
+ margin-right:30px;
+}
+
+.ml10 {
+ margin-left:10px;
+}
+
+.ml20 {
+ margin-left:20px;
+}
+
+.ml30 {
+ margin-left:30px;
+}
+
+.ml35 {
+ margin-left:35px;
+}
+
+.op50 {
+ opacity: .5;
+ -ms-filter: "alpha(opacity=50)";
+}
+.op70 {
+ opacity: .7;
+ -ms-filter: "alpha(opacity=70)";
+}
+
+.op30 {
+ opacity: .3;
+ -ms-filter: "alpha(opacity=30)";
+}
+
+.nowrap {
+ display:inline-block;
+ white-space:nowrap;
+}
+
+.error {
+ background-color:#300;
+ padding:5px;
+ border: solid 1px #900;
+
+}
+
+.error input {
+ background-color:#Fadfd1 !important;
+ margin-bottom:5px;
+}
+
+.error-text {
+ display:none;
+}
+
+.error .error-text {
+ display:block;
+ font-size:11px;
+ color:#F00;
+ margin:10px 0 0;
+}
+.grey {
+ color:#999;
+}
+
+.darkgrey {
+ color:#333;
+}
+
+.w10 {
+ width:10%;
+}
+
+.w20 {
+ width:20%;
+}
+
+.w25 {
+ width:25%;
+}
+
+.w27 {
+ width:27%;
+}
+
+.w30 {
+ width:30%;
+}
+
+.w38 {
+ width:38%;
+}
+
+.w40 {
+ width:40%;
+}
+
+.w45 {
+ width:45%;
+}
+
+.w50 {
+ width:50%;
+}
+
+.w60 {
+ width:60%;
+}
+
+.w70 {
+ width:70%;
+}
+
+.w80 {
+ width:80%;
+}
+
+.w90 {
+ width:90%;
+}
+
+.w100 {
+ width:100%;
+}
+
+/* TODO - we need a separate stylesheet(s) for signin/signup screens */
+/* Following is a style adjustment for the sign-in table spacing */
+#sign-in td { padding: 4px; }
+
+
+.spinner-large {
+ background-image: url('/assets/shared/spinner.gif');
+ background-repeat:no-repeat;
+ background-position:center;
+ width:128px;
+ height:128px;
+}
diff --git a/web/app/assets/stylesheets/client/notify.css.scss b/web/app/assets/stylesheets/client/notify.css.scss
new file mode 100644
index 000000000..d0ec4765f
--- /dev/null
+++ b/web/app/assets/stylesheets/client/notify.css.scss
@@ -0,0 +1,29 @@
+@import "client/common.css.scss";
+
+#notification {
+ position:absolute;
+ left:30%;
+ padding:20px;
+ width:400px;
+ border:solid 2px #0B6672;
+ background-image:url(/assets/shared/bkg_overlay.png);
+ color:#fff;
+ z-index:999;
+ -webkit-box-shadow: 0px 0px 15px rgba(50, 50, 50, 1);
+ -moz-box-shadow: 0px 0px 15px rgba(50, 50, 50, 1);
+ box-shadow: 0px 0px 15px rgba(50, 50, 50, 1);
+}
+
+#notification h2 {
+ font-size: 1.5em;
+ color:#fff;
+ font-weight:200;
+ display:block;
+ border-bottom: solid 1px #fff;
+ margin-bottom:10px;
+}
+
+#notification p {
+ margin-bottom:20px;
+}
+
diff --git a/web/app/assets/stylesheets/client/profile.css.scss b/web/app/assets/stylesheets/client/profile.css.scss
new file mode 100644
index 000000000..594b4ff07
--- /dev/null
+++ b/web/app/assets/stylesheets/client/profile.css.scss
@@ -0,0 +1,218 @@
+@import "client/common.css.scss";
+
+.profile-header {
+ padding:20px;
+ height:120px;
+}
+
+.profile-header h2 {
+ font-weight:200;
+ font-size: 28px;
+ float:left;
+ margin: 0px 150px 0px 0px;
+}
+
+.profile-status {
+ font-size:12px;
+ float:left;
+ display:inline-block;
+ vertical-align:middle;
+ line-height:30px;
+}
+
+.profile-photo {
+ height: 95px;
+ width: 15%;
+ float:left;
+}
+
+.profile-nav {
+ width:85%;
+ position:relative;
+ float:right;
+ margin-right:-10px;
+}
+
+.profile-nav a {
+ width:19%;
+ text-align:center;
+ height: 27px;
+ display: block;
+ float:left;
+ margin-right:5px;
+ vertical-align:bottom;
+ padding-top:65px;
+ background-color:#535353;
+ color:#ccc;
+ font-size:17px;
+ text-decoration:none;
+}
+
+.profile-nav a:hover {
+ background-color:#666;
+ color:#fff;
+}
+
+.profile-nav a.active {
+ background-color:#ed3618;
+ color:#fff;
+}
+
+.profile-nav a.active:hover {
+ background-color:#ed3618;
+ cursor:default;
+}
+
+.profile-nav a.last {
+ margin-right:0px !important;
+}
+
+.avatar-profile {
+ float:left;
+ padding:2px;
+ width:88px;
+ height:88px;
+ background-color:#ed3618;
+ -webkit-border-radius:44px;
+ -moz-border-radius:44px;
+ border-radius:44px;
+}
+
+.avatar-profile img {
+ width:88px;
+ height:88px;
+ -webkit-border-radius:44px;
+ -moz-border-radius:44px;
+ border-radius:44px;
+}
+
+.profile-wrapper {
+ padding:10px 25px 10px 25px;
+ font-size:15px;
+ color:#ccc;
+ border-bottom: dotted 1px #444;
+ position:relative;
+ display:block;
+}
+
+.profile-about-left {
+ width:16%;
+ float:left;
+ font-size:13px;
+ line-height:140%;
+ display:block;
+}
+
+.profile-about-left h3 {
+ color:#fff;
+ margin-bottom:0px;
+ font-size:13px;
+ font-weight:bold;
+ display:inline;
+}
+
+.profile-about-right {
+ float:right;
+ font-size:13px;
+ width:84%;
+ line-height:140%;
+ display:block;
+}
+
+.profile-about-right .profile-instrument {
+ text-align:center;
+ margin-right:15px;
+ float:left;
+}
+
+.proficiency-beginner {
+ font-size:10px;
+ color:#8ea415;
+ font-weight:600;
+}
+
+.proficiency-intermediate {
+ font-size:10px;
+ color:#0b6672;
+ font-weight:600;
+}
+
+.proficiency-expert {
+ font-size:10px;
+ color:#ed3618;
+ font-weight:600;
+}
+
+.profile-bands {
+ width:215px;
+ min-height:90px;
+ background-color:#242323;
+ position:relative;
+ float:left;
+ margin:10px 20px 10px 0px;
+ padding-bottom:5px;
+}
+
+.profile-band-name {
+ float:left;
+ font-size:12px;
+ margin-top:12px;
+ font-weight:bold;
+}
+
+.profile-band-location {
+ font-size:12px;
+ font-weight:200;
+}
+
+.profile-band-genres {
+ float:left;
+ width:40%;
+ font-size:10px;
+ margin-left:10px;
+ padding-right:5px;
+}
+
+.profile-social-left {
+ float:left;
+ width:32%;
+ margin-right:12px;
+ border-right:solid 1px #666;
+}
+
+.profile-social-mid {
+ float:left;
+ width:31%;
+ margin-right:12px;
+ border-right:solid 1px #666;
+}
+
+.profile-social-right {
+ float:left;
+ width:31%;
+}
+
+.profile-social-left h2, .profile-social-mid h2, .profile-social-right h2 {
+ font-weight:200;
+ color:#fff;
+ font-size:20px;
+}
+
+.profile-block {
+ clear:left;
+ white-space:nowrap;
+ display:block;
+ margin-bottom:10px;
+}
+
+.profile-block-name {
+ display:inline-block;
+ margin-top:13px;
+ font-size:14px;
+ color:#fff;
+ font-weight:bold;
+}
+
+.profile-block-city {
+ font-size:12px;
+}
diff --git a/web/app/assets/stylesheets/client/screen_common.css.scss b/web/app/assets/stylesheets/client/screen_common.css.scss
new file mode 100644
index 000000000..0edcc5c43
--- /dev/null
+++ b/web/app/assets/stylesheets/client/screen_common.css.scss
@@ -0,0 +1,461 @@
+/* Common styles used in screens */
+@import "client/common.css.scss";
+
+.screen {
+ display:none;
+}
+
+.screen.secondary {
+ border: 1px solid $ColorScreenPrimary;
+ background-color:$ColorScreenBackground;
+
+ .footer button {
+ margin:1em 0em 1em 1em;
+ }
+
+ .breadcrumb {
+ margin-bottom:3em;
+ }
+
+ .breadcrumb p {
+ float:left;
+ }
+
+ p {
+ margin: 1em;
+ cursor: pointer;
+ font-size: 120%;
+ line-height: 150%;
+ }
+
+ ul {
+ margin-left: 2em;
+ list-style: disc;
+ }
+
+ li {
+ margin-bottom:1.5em;
+ }
+
+ p.current {
+ font-weight: bold;
+ }
+}
+
+.content {
+ clear: both;
+ float: left;
+ height: 100%;
+ width: 100%;
+ position:relative;
+}
+
+.content-head {
+ height:21px;
+ padding:4px;
+ background-color:#ED3618;
+
+ .content-icon {
+ margin-right:10px;
+ float:left;
+ }
+
+ h1 {
+ margin: -3px 0px 0px 0px;
+ padding:0;
+ float:left;
+ font-weight:100;
+ font-size:24px;
+ }
+
+ .content-icon {
+ margin-right:10px;
+ float:left;
+ }
+
+ .content-nav {
+ float:right;
+ margin-right:10px;
+
+ .home-icon {
+ float:left;
+ margin-right:20px;
+ }
+
+ a.arrow-right {
+ float:left;
+ display:block;
+ margin-top:2px;
+ margin-right:10px;
+ width: 0;
+ height: 0;
+ border-top: 7px solid transparent;
+ border-bottom: 7px solid transparent;
+ border-left: 7px solid #FFF;
+ }
+
+ a.arrow-left {
+ float:left;
+ display:block;
+ margin-top:2px;
+ margin-right:20px;
+ width: 0;
+ height: 0;
+ border-top: 7px solid transparent;
+ border-bottom: 7px solid transparent;
+ border-right:7px solid #FFF;
+ }
+ }
+}
+
+.content-scroller {
+ height:inherit;
+ position:relative;
+ display:block;
+ overflow:auto;
+}
+
+.content-wrapper {
+ padding: 0px 0px 0px 0px;
+ font-size: 15px;
+ color: #ccc;
+ border-bottom: dotted 1px #444;
+}
+
+.content-wrapper h2 {
+ color:#fff;
+ font-weight:600;
+ font-size:24px;
+}
+
+.buttonrow, .screen.secondary .footer {
+ position: absolute;
+ bottom:0px;
+ right:0px;
+ z-index:5;
+ background-color:$translucent1;
+ width:100%;
+}
+
+textarea {
+ background-color: $ColorTextBoxBackground;
+ border:none;
+ -webkit-box-shadow: inset 2px 2px 3px 0px #888;
+ box-shadow: inset 2px 2px 3px 0px #888;
+ color:#333;
+}
+
+
+
+/* Start of Jeff's common.css file */
+body {
+ font-family: Raleway, Arial, Helvetica, sans-serif;
+ font-size: 14px;
+ font-weight: 300;
+ color: #FFF;
+ padding:3% 6%;
+ background-color: #262626;
+ position:relative;
+}
+
+h1, h2, h3, h4, h5, h6, p, div, table, form, img, tr, td, th {
+ margin:0;
+ border:0;
+ padding:0;
+}
+
+a {
+ display:inline-block;
+}
+
+small {
+ font-size:11px;
+}
+
+.button-grey {
+ margin:0px 8px 0px 8px;
+ background-color:#666;
+ border: solid 1px #868686;
+ outline:solid 2px #666;
+ padding:3px 10px;
+ font-family:raleway;
+ font-size:12px;
+ font-weight:300;
+ cursor:pointer;
+ color:#ccc;
+ text-decoration:none;
+}
+
+.button-grey:hover {
+ background-color:#999;
+ color:#FFF;
+ text-decoration:none;
+}
+
+.button-orange {
+ margin:0px 8px 0px 8px;
+ background-color: #ED3618;
+ border: solid 1px #F27861;
+ outline: solid 2px #ED3618;
+ padding:3px 10px;
+ font-family:raleway;
+ font-size:12px;
+ font-weight:300;
+ cursor:pointer;
+ color:#FC9;
+ text-decoration:none;
+}
+
+.button-orange:hover {
+ background-color:#f16750;
+ color:#FFF;
+ text-decoration:none;
+}
+
+.button-disabled {
+ border: none;
+ outline: none;
+ color:#EEE;
+ background-color:#666;
+ cursor:default;
+}
+
+.button-disabled:hover{
+ color:#EEE;
+ background-color:#666;
+}
+
+a img {
+ border:none;
+}
+
+.clearall {
+ clear:both;
+}
+
+.left {
+ float:left;
+}
+
+.right {
+ float:right;
+}
+
+.f8 {
+ font-size: 8px !important;
+}
+
+.f9 {
+ font-size: 9px !important;
+}
+
+.f10 {
+ font-size: 10px !important;
+}
+
+.f11 {
+ font-size: 11px !important;
+}
+
+.f12 {
+ font-size: 12px !important;
+}
+
+.f13 {
+ font-size: 13px !important;
+}
+
+.f14 {
+ font-size: 14px !important;
+}
+
+.f15 {
+ font-size: 15px !important;
+}
+
+.m0 {
+ margin: 0 !important;
+}
+
+.mr5 {
+ margin-right:5px;
+}
+
+.mr10 {
+ margin-right:10px;
+}
+
+.mr20 {
+ margin-right:20px;
+}
+
+.mr30 {
+ margin-right:30px;
+}
+
+.mr35 {
+ margin-right:35px;
+}
+
+.mr40 {
+ margin-right:40px;
+}
+
+.mr120 {
+ margin-right:120px;
+}
+
+.ml5 {
+ margin-left:5px;
+}
+
+.ml10 {
+ margin-left:10px;
+}
+
+.ml20 {
+ margin-left:20px;
+}
+
+.ml25 {
+ margin-left:25px;
+}
+
+.ml30 {
+ margin-left:30px;
+}
+
+.ml35 {
+ margin-left:35px;
+}
+
+.ml45 {
+ margin-left:45px;
+}
+
+.mt5 {
+ margin-top:5px;
+}
+
+.mt10 {
+ margin-top:10px;
+}
+
+.mt15 {
+ margin-top:15px;
+}
+
+.mt20 {
+ margin-top:20px;
+}
+
+.mt25 {
+ margin-top:25px;
+}
+
+.mt30 {
+ margin-top:30px;
+}
+
+.mt35 {
+ margin-top:35px;
+}
+
+.mt40 {
+ margin-top:40px;
+}
+
+.mt45 {
+ margin-top:45px;
+}
+
+.mt50 {
+ margin-top:50px;
+}
+
+.mt55 {
+ margin-top:55px;
+}
+
+.mt65 {
+ margin-top:65px;
+}
+
+.mb5 {
+ margin-bottom:5px;
+}
+
+.mb15 {
+ margin-bottom:15px;
+}
+
+.op50 {
+ opacity: .5;
+ -ms-filter: "alpha(opacity=50)";
+}
+
+.op30 {
+ opacity: .3;
+ -ms-filter: "alpha(opacity=30)";
+}
+
+.nowrap {
+ display:inline-block;
+ white-space:nowrap;
+}
+
+.overlay {
+ display:none;
+ position:fixed;
+ width:100%;
+ height:100%;
+ top:0px;
+ left:0px;
+ z-index: 999;
+ background-image:url('/assets/shared/bkg_overlay.png');
+}
+
+.overlay-small {
+ width:300px;
+ height:160px;
+ position:absolute;
+ left:50%;
+ top:20%;
+ margin-left:-150px;
+ background-color:#333;
+ border: 1px solid #ed3618;
+ z-index:1000;
+ display:none;
+}
+
+.overlay-inner {
+ width:250px;
+ height:130px;
+ padding:25px;
+ font-size:15px;
+ color:#aaa;
+}
+
+.overlay-inner input[type=text], .overlay-inner input[type=password] {
+ padding:3px;
+ font-size:13px;
+ width:239px;
+}
+
+.white {
+ color:#fff;
+}
+
+.lightgrey {
+ color:#ccc;
+}
+
+.grey {
+ color:#999;
+}
+
+.darkgrey {
+ color:#333;
+}
+
+/* End of Jeff's common.css file */
diff --git a/web/app/assets/stylesheets/client/search.css.scss b/web/app/assets/stylesheets/client/search.css.scss
new file mode 100644
index 000000000..488e053c0
--- /dev/null
+++ b/web/app/assets/stylesheets/client/search.css.scss
@@ -0,0 +1,57 @@
+/* Styles used by things related to search */
+@import "client/common.css.scss";
+
+/* Container for the search input */
+.header .search {
+ position: absolute;
+ left: 50%;
+ margin-left: -125px;
+ top: 26px;
+}
+
+.searchtextinput {
+ border: 1px solid #fff;
+ background:none;
+ color:#fff;
+ font-size: 120%;
+ line-height: 120%;
+ width: 250px;
+ padding: 6px;
+}
+
+.searchresults {
+ background-color:$color8;
+ border:1px solid #000;
+ color:#000;
+}
+
+.searchresults h2 {
+ font-size: 120%;
+ font-weight: bold;
+ background-color: shade($color8, 10%);
+}
+
+.searchresults li {
+ clear:both;
+ padding: 8px;
+}
+
+.searchresults img {
+ float:left;
+ width: 32px;
+ height: 32px;
+ border: 1px solid #333;
+ margin: 4px;
+}
+
+.searchresults .text {
+ color: #000;
+ font-weight: bold;
+}
+
+.searchresults .subtext {
+ display:block;
+ color: #000;
+ font-size: 90%;
+}
+
diff --git a/web/app/assets/stylesheets/client/searchResults.css.scss b/web/app/assets/stylesheets/client/searchResults.css.scss
new file mode 100644
index 000000000..6734ec71b
--- /dev/null
+++ b/web/app/assets/stylesheets/client/searchResults.css.scss
@@ -0,0 +1,94 @@
+.search-result-header {
+ width:100%;
+ padding:11px 0px 11px 0px;
+ background-color:#4c4c4c;
+ min-height:20px;
+ overflow-x:hidden;
+}
+
+a.search-nav {
+ font-size:13px;
+ color:#fff;
+ text-decoration:none;
+ margin-right:40px;
+ float:left;
+ font-weight:200;
+ padding-bottom:4px;
+}
+
+a.search-nav.active, a.search-nav.active:hover {
+ font-weight:700;
+ border-bottom:solid 3px #ed3618;
+}
+
+a.search-nav:hover {
+ border-bottom: dotted 2px #ed3618;
+}
+
+.search-result {
+ width:193px;
+ min-height:85px;
+ background-color:#242323;
+ position:relative;
+ float:left;
+ margin:10px 20px 10px 0px;
+ padding-bottom:5px;
+}
+
+.search-connected {
+ background-color:#4c4c4c;
+ width:193px;
+ min-height:85px;
+ position:relative;
+ float:left;
+ margin:10px 20px 10px 0px;
+ padding-bottom:5px;
+ font-size:11px;
+ text-align:center;
+ vertical-align:middle;
+}
+
+.search-connected a {
+ color:#B3DD15;
+}
+
+.search-band-genres {
+ float:left;
+ width:40%;
+ font-size:10px;
+ margin-left:10px;
+ padding-right:5px;
+}
+
+.search-result-name {
+ float:left;
+ font-size:12px;
+ margin-top:12px;
+ font-weight:bold;
+}
+
+.search-result-location {
+ font-size:11px;
+ color:#D5E2E4;
+ font-weight:200;
+}
+
+.avatar-small {
+ float:left;
+ padding:1px;
+ width:36px;
+ height:36px;
+ background-color:#ed3618;
+ margin:10px;
+ -webkit-border-radius:18px;
+ -moz-border-radius:18px;
+ border-radius:18px;
+}
+
+.avatar-small img {
+ width: 36px;
+ height: 36px;
+ -webkit-border-radius:18px;
+ -moz-border-radius:18px;
+ border-radius:18px;
+}
\ No newline at end of file
diff --git a/web/app/assets/stylesheets/client/session.css.scss b/web/app/assets/stylesheets/client/session.css.scss
new file mode 100644
index 000000000..fcd27f93b
--- /dev/null
+++ b/web/app/assets/stylesheets/client/session.css.scss
@@ -0,0 +1,674 @@
+@import "client/common.css.scss";
+
+[layout-id="session"] {
+
+ .resync {
+ margin-left:15px;
+ }
+
+ #session-controls {
+
+ width:100%;
+ padding:11px 0px 11px 0px;
+ background-color:#4c4c4c;
+ min-height:20px;
+ overflow-x:hidden;
+ position:relative;
+ }
+}
+
+
+.track {
+ width:70px;
+ height:290px;
+ display:inline-block;
+ margin-right:8px;
+ position:relative;
+ background-color:#242323;
+}
+
+.track-empty {
+ min-width:230px;
+ height:201px;
+ margin-right:8px;
+ padding-top:70px;
+ text-align:center;
+ font-weight:600;
+ font-style:italic;
+ font-size:16px;
+ white-space:normal;
+ color:#666;
+}
+
+#tracks {
+ margin-top:12px;
+ overflow:auto;
+}
+
+.track-empty a {
+ color:#aaa;
+}
+
+table.vu td {
+ border: 3px solid #222;
+}
+
+.track-vu-left {
+ position:absolute;
+ bottom:8px;
+ left:0px;
+}
+
+.track-vu-right {
+ position:absolute;
+ bottom:8px;
+ right:0px;
+}
+
+.vu-red-off {
+ background-color:#6c1709;
+}
+
+.vu-red-on {
+ background-color:#e73619;
+}
+
+.vu-green-off {
+ background-color:#244105;
+}
+
+.vu-green-on {
+ background-color:#72a43b;
+}
+
+.track-label {
+ position: absolute;
+ text-align:center;
+ width: 55px;
+ max-width: 55px;
+ white-space:normal;
+ top: 3px;
+ left: 7px;
+ font-family: Arial, Helvetica, sans-serif;
+ font-size: 11px;
+ font-weight: bold;
+}
+
+.track-close {
+ position:absolute;
+ top:3px;
+ right:3px;
+ width:12px;
+ height:12px;
+}
+
+
+
+/* Just bringing in Jeff's session.css file in entirety below */
+/* But have modified significantly to make things work. Needs cleanup */
+
+#session-controls {
+ .label {
+ float:left;
+ font-size:12px;
+ color:#ccc;
+ margin: 0px 0px 0px 4px;
+ }
+
+ .block {
+ float:left;
+ margin: 6px 8px 0px 8px;
+ }
+
+ .fader {
+ float:left;
+ position: relative;
+ margin: 0px 0px 0px 4px;
+ width: 60px;
+ height: 24px;
+ }
+ [control="fader"] {
+ height: 24px;
+ }
+
+ .fader.lohi {
+ background-image: url('/assets/content/bkg_volume.png');
+ background-repeat:no-repeat;
+ }
+
+ .fader.flat {
+ background-image: url('/assets/content/bkg_playcontrols.png');
+ height: 17px;
+ width: 80px;
+ }
+
+ .handle {
+ position:absolute;
+ top:2px;
+ width:8px;
+ height:17px;
+ }
+}
+
+.settings {
+ margin-left:15px;
+}
+
+.leave {
+ margin-right:25px;
+}
+
+.session-mytracks {
+ margin-left:15px;
+ float:left;
+ display:inline-block;
+ width:19%;
+ min-width:165px;
+ border-right:solid 1px #666;
+}
+
+.session-livetracks {
+ margin-left:15px;
+ float:left;
+ display:inline-block;
+ width:35%;
+ min-width:220px;
+ border-right:solid 1px #666;
+}
+
+.session-recordings {
+ margin-left:15px;
+ display:inline-block;
+ width:35%;
+ min-width:220px;
+}
+
+#tracks .when-empty {
+ margin: 0px;
+ padding:0px;
+ display:block;
+ padding-top: 125px;
+ vertical-align:middle;
+ text-align:center;
+ font-weight: bold;
+}
+#tracks .when-empty.recordings {
+ padding-top: 137px;
+}
+
+#tracks .when-empty a {
+ text-decoration: underline;
+ color: inherit;
+}
+
+
+
+.session-add {
+ margin-top:9px;
+ margin-bottom:8px;
+ font-size:16px;
+ height: 22px;
+ min-height: 22px;
+ max-height: 22px;
+}
+
+.session-add a {
+ color:#ccc;
+ text-decoration: none;
+}
+
+.session-add a img {
+ vertical-align:bottom;
+}
+
+.session-recording-name-wrapper {
+ position:relative;
+ white-space:nowrap;
+
+}
+
+.session-recording-name {
+ width:60%;
+ overflow:hidden;
+ margin-top:6px;
+ margin-bottom:8px;
+ font-size:16px;
+}
+
+.session-tracks-scroller {
+ position:relative;
+ overflow-x:auto;
+ overflow-y:hidden;
+ width:100%;
+ height:340px;
+ float:left;
+ white-space:nowrap;
+}
+
+.track {
+ width:70px;
+ height:290px;
+ display:inline-block;
+ margin-right:8px;
+ position:relative;
+ background-color:#242323;
+}
+
+
+.track-empty {
+ min-width:230px;
+ height:201px;
+ margin-right:8px;
+ padding-top:70px;
+ text-align:center;
+ font-weight:600;
+ font-style:italic;
+ font-size:16px;
+ white-space:normal;
+ color:#666;
+}
+
+.track-empty a {
+ color:#aaa;
+}
+
+.track-vu-left {
+ position:absolute;
+ bottom:8px;
+ left:0px;
+}
+
+.track-vu-right {
+ position:absolute;
+ bottom:8px;
+ right:0px;
+}
+
+.vu-red-off {
+ background-color:#6c1709;
+}
+
+.vu-red-on {
+ background-color:#e73619;
+}
+
+.vu-green-off {
+ background-color:#244105;
+}
+
+.vu-green-on {
+ background-color:#72a43b;
+}
+
+.track-label {
+ position: absolute;
+ width: 55px;
+ top: 3px;
+ left: 7px;
+ font-family: Arial, Helvetica, sans-serif;
+ font-size: 11px;
+ font-weight: bold;
+}
+
+.session-livetracks .loading {
+ opacity: .3;
+ -ms-filter: "alpha(opacity=30)";
+}
+
+.live-track-label {
+ white-space:normal;
+ text-align:center;
+ width: 60px;
+ height: 40px;
+ padding:5px;
+ font-family: Arial, Helvetica, sans-serif;
+ font-size: 11px;
+ font-weight: bold;
+}
+
+.recording-track-label {
+ white-space:normal;
+ text-align:center;
+ width: 60px;
+ height: 40px;
+ padding:5px;
+ font-family: Arial, Helvetica, sans-serif;
+ font-size: 11px;
+ font-weight: bold;
+}
+
+
+.track-close {
+ position:absolute;
+ top:3px;
+ right:3px;
+ width:12px;
+ height:12px;
+}
+
+.avatar-med {
+ position:absolute;
+ top:32px;
+ left:12px;
+ padding:1px;
+ width:44px;
+ height:44px;
+ background-color:#ed3618;
+ -webkit-border-radius:22px;
+ -moz-border-radius:22px;
+ border-radius:22px;
+}
+
+.avatar-recording {
+ position:absolute;
+ top:32px;
+ left:12px;
+ padding:1px;
+ width:44px;
+ height:44px;
+ background-color: none;
+ -webkit-border-radius:22px;
+ -moz-border-radius:22px;
+ border-radius:22px;
+}
+
+.avatar-med img {
+ width: 44px;
+ height: 44px;
+ -webkit-border-radius:22px;
+ -moz-border-radius:22px;
+ border-radius:22px;
+}
+
+.track-instrument {
+ position:absolute;
+ top:85px;
+ left:12px;
+}
+
+.track-gain {
+ position:absolute;
+ width:28px;
+ height:83px;
+ top:138px;
+ left:23px;
+ background-image:url('/assets/content/bkg_gain_slider.png');
+ background-repeat:repeat-y;
+ background-position:bottom;
+}
+
+.track-gain-wrapper {
+ cursor:pointer;
+ position:relative;
+ width:28px;
+ height:83px;
+}
+
+.track-gain-slider {
+ position:absolute;
+ cursor:pointer;
+ width:28px;
+ height:11px;
+ left:0px;
+}
+
+.track-icon-mute {
+ cursor: pointer;
+ position:absolute;
+ top:230px;
+ left:29px;
+ width: 20px;
+ height: 18px;
+ background-image:url('/assets/content/icon_mute.png');
+ background-repeat:no-repeat;
+}
+
+.track-icon-mute.muted {
+ background-position: 0px 0px;
+}
+.track-icon-mute.enabled {
+ background-position: -20px 0px;
+}
+
+.session-livetracks .track-icon-mute, .session-recordings .track-icon-mute {
+ top:245px;
+}
+
+.track-icon-settings {
+ position:absolute;
+ top:255px;
+ left:28px;
+}
+
+.track-connection-green {
+ position:absolute;
+ bottom:0px;
+ left:0px;
+ width: 70px;
+ height: 8px;
+ font-family:Arial, Helvetica, sans-serif;
+ font-weight:bold;
+ font-size:8px;
+ text-align:center;
+ background-color:#72a43b;
+ overflow:hidden;
+}
+
+.track-connection-yellow {
+ position:absolute;
+ bottom:0px;
+ left:0px;
+ width: 70px;
+ height: 8px;
+ font-family:Arial, Helvetica, sans-serif;
+ font-weight:bold;
+ font-size:8px;
+ text-align:center;
+ background-color:#cc9900;
+ overflow:hidden;
+}
+
+.track-connection-red {
+ position:absolute;
+ bottom:0px;
+ left:0px;
+ width: 70px;
+ height: 8px;
+ font-family:Arial, Helvetica, sans-serif;
+ font-weight:bold;
+ font-size:8px;
+ text-align:center;
+ background-color:#980006;
+ overflow:hidden;
+}
+
+.track-connection-grey {
+ position:absolute;
+ bottom:0px;
+ left:0px;
+ width: 70px;
+ height: 8px;
+ font-family:Arial, Helvetica, sans-serif;
+ font-weight:bold;
+ font-size:8px;
+ text-align:center;
+ background-color:#666;
+ overflow:hidden;
+}
+
+
+.voicechat {
+ margin-top:10px;
+ width:152px;
+ height:25px;
+ background-color:#242323;
+ position:absolute;
+ bottom:0px;
+}
+
+.voicechat-label {
+ position:absolute;
+ font-size:11px;
+ font-weight:500;
+ top:7px;
+ left:8px;
+}
+
+.voicechat-gain {
+ position:absolute;
+ top:4px;
+ left:45px;
+ width:51px;
+ height:186px;
+ background-image:url(/assets/content/bkg_slider_gain_horiz.png);
+ background-repeat:repeat-x;
+}
+
+.voicechat-gain-wrapper {
+ position:relative;
+ width:51px;
+ height:18px;
+}
+
+.voicechat-gain-slider {
+ position:absolute;
+ top:0px;
+ left:60%;
+ width:10px;
+ height:18px;
+}
+
+.voicechat-mute {
+ position:absolute;
+ top:4px;
+ left: 103px;
+ width: 20px;
+ height: 18px;
+ background-image:url('/assets/content/icon_mute.png');
+ background-repeat:no-repeat;
+}
+
+.voicechat-mute.muted {
+ background-position: 0px 0px;
+}
+.voicechat-mute.enabled {
+ background-position: -20px 0px;
+}
+
+
+.voicechat-settings {
+ position:absolute;
+ top:4px;
+ left: 127px;
+}
+
+.recording {
+ bottom: 0px;
+ margin-top:15px;
+ padding:3px;
+ height:19px;
+ width:95%;
+ background-color:#242323;
+ position:absolute;
+ text-align:center;
+ font-size:13px;
+}
+
+.recording a {
+ color:#ccc;
+ text-decoration:none;
+}
+
+.recording a img {
+ vertical-align:middle;
+}
+
+.recording-controls {
+ margin-top:15px;
+ padding:3px 5px 3px 10px;
+ height:19px;
+ width:93%;
+ min-width:200px;
+ background-color:#242323;
+ position:relative;
+ font-size:13px;
+ text-align:center;
+}
+
+.recording-position {
+ display:inline-block;
+ width:80%;
+
+ font-family:Arial, Helvetica, sans-serif;
+ font-size:11px;
+ height:18px;
+ vertical-align:top;
+}
+
+.recording-time {
+ display:inline-block;
+ height:16px;
+ vertical-align:top;
+ margin-top:4px;
+}
+
+.recording-playback {
+ display:inline-block;
+ background-image:url(/assets/content/bkg_playcontrols.png);
+ background-repeat:repeat-x;
+ position:relative;
+ width:65%;
+ height:16px;
+ margin-top:2px;
+}
+
+.recording-slider {
+ position:absolute;
+ left:40px;
+ top:0px;
+}
+
+.recording-current {
+ font-family:Arial, Helvetica, sans-serif;
+ display:inline-block;
+ font-size:18px;
+}
+
+/* GAIN SLIDER POSITIONS -- TEMPORARY FOR DISPLAY PURPOSES ONLY */
+.pos0 {
+ bottom:0px;
+}
+
+.pos10 {
+ bottom:10%;
+}
+
+.pos20 {
+ bottom:20%;
+}
+
+.pos30 {
+ bottom:30%;
+}
+
+.pos40 {
+ bottom:40%;
+}
+
+.pos50 {
+ bottom:50%;
+}
+
+.pos60 {
+ bottom:60%;
+}
+
+.pos70 {
+ bottom:70%;
+}
+
+.pos80 {
+ bottom:80%;
+}
+
+
diff --git a/web/app/assets/stylesheets/client/sessionList.css.scss b/web/app/assets/stylesheets/client/sessionList.css.scss
new file mode 100644
index 000000000..869353633
--- /dev/null
+++ b/web/app/assets/stylesheets/client/sessionList.css.scss
@@ -0,0 +1,106 @@
+table.findsession-table {
+ margin-top:6px;
+ width:98%;
+ font-size:11px;
+ color:#fff;
+ background-color:#262626;
+ border:solid 1px #4d4d4d;
+}
+
+.findsession-table th {
+ font-weight:300;
+ background-color:#4d4d4d;
+ padding:6px;
+ border-right:solid 1px #333;
+}
+
+.findsession-table td {
+ padding:9px 5px 5px 5px;
+ border-right:solid 1px #333;
+ border-top:solid 1px #333;
+ vertical-align:top;
+ white-space:normal;
+}
+
+.findsession-table .noborder {
+ border-right:none;
+}
+
+.findsession-table .musicians {
+ margin-top:-3px;
+}
+
+.findsession-table .musicians td {
+ border-right:none;
+ border-top:none;
+ padding:3px;
+ vertical-align:middle;
+}
+
+.findsession-table a {
+ color:#fff;
+ text-decoration:none;
+}
+
+.findsession-table a:hover {
+ color:#227985;
+}
+
+.latency-grey {
+ width: 75px;
+ height: 10px;
+ font-family:Arial, Helvetica, sans-serif;
+ font-weight:200;
+ font-size:9px;
+ text-align:center;
+ background-color:#868686;
+}
+
+.latency-green {
+ width: 50px;
+ height: 10px;
+ font-family:Arial, Helvetica, sans-serif;
+ font-weight:200;
+ font-size:9px;
+ text-align:center;
+ background-color:#72a43b;
+}
+
+.latency-yellow {
+ width: 50px;
+ height: 10px;
+ font-family:Arial, Helvetica, sans-serif;
+ font-weight:200;
+ font-size:9px;
+ text-align:center;
+ background-color:#cc9900;
+}
+
+.latency-red {
+ width: 50px;
+ height: 10px;
+ font-family:Arial, Helvetica, sans-serif;
+ font-weight:200;
+ font-size:9px;
+ text-align:center;
+ background-color:#980006;
+}
+
+.avatar-tiny {
+ float:left;
+ padding:1px;
+ width:24px;
+ height:24px;
+ background-color:#ed3618;
+ -webkit-border-radius:12px;
+ -moz-border-radius:12px;
+ border-radius:12px;
+}
+
+.avatar-tiny img {
+ width: 24px;
+ height: 24px;
+ -webkit-border-radius:12px;
+ -moz-border-radius:12px;
+ border-radius:12px;
+}
\ No newline at end of file
diff --git a/web/app/assets/stylesheets/client/sidebar.css.scss b/web/app/assets/stylesheets/client/sidebar.css.scss
new file mode 100644
index 000000000..b74011811
--- /dev/null
+++ b/web/app/assets/stylesheets/client/sidebar.css.scss
@@ -0,0 +1,248 @@
+@import "client/common.css.scss";
+@charset "UTF-8";
+
+.sidebar {
+
+ background-color: $ColorElementPrimary;
+
+ .panel-header {
+ margin:0px;
+ padding:0px;
+ cursor:pointer;
+ }
+
+ .panelcontents {
+ overflow-y:auto;
+ overflow-x:hidden;
+ }
+
+ h2 {
+ background-color: shade($ColorElementPrimary, 20);
+ margin:0px 0px 6px 0px;
+ padding: 3px 0px 3px 10px;
+ font-size: 180%;
+ font-weight: 100;
+ color: $ColorSidebarText;
+ cursor:pointer;
+ }
+
+ h2 .badge {
+ position:absolute;
+ height:15px;
+ top:8px;
+ right:5px;
+ width:19px;
+ background-color:lighten($ColorElementPrimary, 5);
+ color:shade($ColorElementPrimary, 30);
+ font-size:11px;
+ font-family:Arial, Helvetica, sans-serif;
+ font-weight:400;
+ text-align:center;
+ padding-top:4px;
+ -webkit-border-radius:50%;
+ -moz-border-radius:50%;
+ border-radius:50%;
+ }
+
+ .expander {
+ background-color:$ColorUIBackground;
+ font-size:80%;
+ color:shade($ColorText, 30);
+ cursor:pointer;
+ }
+ .expander p {
+ text-align:right;
+ padding: 0px 0px 6px 0px;
+ }
+ .expander img {
+ vertical-align: middle;
+ margin-left: 12px;
+ }
+ .expander.hidden p {
+ text-align:left;
+ }
+ .expander.hidden img {
+ margin-left: 0px;
+ }
+
+ .search form {
+ margin: 0px;
+ padding: 0px;
+ }
+
+ input[type="text"] {
+ background-color:#227985;
+ border: solid 1px #01545f;
+ font-weight:400;
+ font-size:15px;
+ color:#abd2d7;
+ width: 92%;
+ margin: 4px 4px 8px 8px;
+ padding: 3px;
+ }
+
+ li {
+ position: relative;
+ border-bottom:solid 1px shade($ColorElementPrimary, 20);
+ clear:both;
+ }
+
+ li strong {
+ font-weight:bold;
+ }
+
+ li.offline {
+ background-color: shade($ColorElementPrimary, 20);
+ color: shade($text, 10);
+ opacity:0.5;
+ ms-filter: "alpha(opacity=50)";
+ }
+
+ li .avatar-small {
+ float:left;
+ padding:1px;
+ width:36px;
+ height:36px;
+ background-color:#ed3618;
+ margin:10px;
+ -webkit-border-radius:18px;
+ -moz-border-radius:18px;
+ border-radius:18px;
+ }
+
+ li .avatar-small img {
+ width: 36px;
+ height: 36px;
+ -webkit-border-radius:18px;
+ -moz-border-radius:18px;
+ border-radius:18px;
+ }
+
+ li .friend-name {
+ float:left;
+ font-size:18px;
+ margin-top:12px;
+ }
+
+ li .friend-status {
+ font-size:11px;
+ color:#D5E2E4;
+ }
+
+ .friend-icon {
+ float:right;
+ margin-top:18px;
+ margin-right:20px;
+ }
+
+ li a {
+ color:#B3DD15;
+ }
+
+ li a:hover {
+ color:#FFF;
+ }
+
+ .name {
+ display:block;
+ position:absolute;
+ line-height: 18px;
+ height: 18px;
+ left: 48px;
+ top: 10px;
+ font-weight: bold;
+ font-size:110%;
+ }
+
+ .status {
+ display:block;
+ position:absolute;
+ line-height:12px;
+ height: 12px;
+ left: 48px;
+ top: 26px;
+ font-style: italic;
+ font-size: 100%;
+ }
+
+ .chat-fixed {
+ position:static;
+ }
+ .chat-select {
+ font-size:12px;
+ text-align:center;
+ margin-top:4px;
+ }
+
+ .chat-select select {
+ font-family:Raleway;
+ font-size:12px;
+ color:$ColorSidebarText;
+ background-color:shade($ColorElementPrimary, 20);
+ border:none;
+ }
+
+ .chat-text {
+ float:left;
+ width:180px;
+ font-size:11px;
+ margin-top:5px;
+ margin-bottom:5px;
+ color:#D5E2E4;
+ }
+
+ em {
+ color:#9DB8AF
+ }
+
+ .note-text {
+ font-size: 11px;
+ padding-right: 20px;
+ position:relative;
+ margin-top:5px;
+ margin-bottom:5px;
+ line-height:130%;
+ }
+
+ .note-delete {
+ position:absolute;
+ width:13px;
+ height:13px;
+ top:0px;
+ right:3px;
+ }
+
+ .sidebar-search-result {
+ display:block;
+ position:relative;
+ border-bottom:solid 1px #01545f;
+ }
+
+ .result-name {
+ float:left;
+ font-size:12px;
+ margin-top:12px;
+ font-weight:bold;
+ }
+
+ .result-location {
+ font-size:11px;
+ color:#D5E2E4;
+ font-weight:200;
+ }
+
+ .results-wrapper {
+ width: 300px;
+ overflow-y:auto;
+ overflow-x:hidden;
+ }
+
+ .sidebar-search-connected {
+ text-align:center;
+ vertical-align:middle;
+ background-color:#4c4c4c;
+ padding-bottom:10px;
+ font-size:11px;
+ }
+}
+
diff --git a/web/app/assets/stylesheets/corp/corporate.css.scss.erb b/web/app/assets/stylesheets/corp/corporate.css.scss.erb
new file mode 100644
index 000000000..d0d067896
--- /dev/null
+++ b/web/app/assets/stylesheets/corp/corporate.css.scss.erb
@@ -0,0 +1,153 @@
+/**
+*= require client/ie
+*= require client/jamkazam
+*= require client/header
+*= require client/screen_common
+*= require client/content
+*= require client/ftue
+*= require corp/footer
+*/
+
+
+.logo-home {
+ width:298px;
+ margin-top:30px;
+ display:inline-block;
+ float:left;
+}
+
+#nav {
+ margin-top:40px;
+ margin-bottom:30px;
+ height:43px;
+ border-left:solid 1px #f34f1d;
+ padding: 1px 0;
+}
+
+#nav a {
+ display:inline-block;
+ height:42px;
+ line-height:42px;
+ padding:0px 25px;
+ float:left;
+ border-right:solid 1px #f34f1d;
+ color:#f34f1d;
+ text-decoration:none;
+ font-size:20px;
+ font-weight:100;
+}
+
+#nav a:hover {
+ background-color:#333;
+ color:#999;
+}
+
+#nav a.active {
+ height:60px;
+ line-height:60px;
+ margin-top:-8px;
+ margin-left:-1px;
+ border-right:none;
+ background-color:#f34f1d;
+ color:#fff;
+ cursor:default;
+}
+
+
+
+body.corporate {
+ background-image:url(<%= asset_path('corp/bkg_corporate.gif') %>);
+ background-repeat:repeat-x;
+ background-color:#1c1c1c;
+ padding:30px 15px;
+ overflow:visible;
+ width:auto;
+ height:auto;
+}
+
+body.corporate h1 {
+ font-weight:200;
+ font-size:32px;
+ margin-bottom:10px;
+}
+
+body.corporate p {
+ color:#999;
+ line-height:160%;
+ margin-bottom:20px;
+ width:80%;
+ white-space:normal;
+}
+
+body.corporate p.support {
+ text-align:center;
+}
+
+body.corporate p.support a {
+ text-align:center;
+ padding:6px 20px;
+ font-size:24px;
+}
+
+body.corporate li {
+ color:#999;
+ line-height:160%;
+ width:80%;
+ white-space:normal;
+}
+
+body.corporate ul {
+ padding-left:20px;
+ margin-bottom:20px;
+ list-style:disc outside none;
+}
+
+.wrapper {
+ width:1100px;margin:0 auto;white-space:nowrap;
+}
+
+.wrapper h2 {
+ font-weight:300;
+ font-size:20px;
+}
+
+#terms-toc a {
+ display:block;
+}
+
+br {
+ line-height:16px;
+}
+
+a.button-orange {
+ line-height:16px;
+}
+
+body.corporate input[type=text], body.corporate textarea{
+ background-color: #C5C5C5;
+ border: medium none;
+ box-shadow: 2px 2px 3px 0 #888888 inset;
+ color: #666666;
+ font-family: "Raleway",arial,sans-serif;
+}
+
+body.corporate #contact-submit {
+ margin:20px 2px 0;
+}
+
+body.corporate .media_center {
+
+ td {
+ vertical-align: top;
+ }
+
+
+ ul.media_links {
+ list-style:none;
+ margin-left:58px;
+
+ li {
+ margin-bottom:10px;
+ }
+ }
+}
\ No newline at end of file
diff --git a/web/app/assets/stylesheets/corp/footer.css.scss b/web/app/assets/stylesheets/corp/footer.css.scss
new file mode 100644
index 000000000..459743019
--- /dev/null
+++ b/web/app/assets/stylesheets/corp/footer.css.scss
@@ -0,0 +1,36 @@
+#footer {
+ display:inline-block;
+ clear:both;
+ width: 100%;
+ margin-top: 30px;
+ padding-top: 10px;
+ border-top:solid 1px #444;
+}
+
+#copyright {
+ float:left;
+ font-size:11px;
+ color:#ccc;
+}
+
+#footer-links {
+ float:right;
+ font-size:11px;
+ color:#ccc;
+}
+
+#footer-links a {
+ color:#ccc;
+ text-decoration:none;
+}
+
+#footer-links a:hover {
+ color:#fff;
+ text-decoration:underline;
+}
+
+#version {
+ font-size:11px;
+ color:#ccc;
+ text-align: center;
+}
\ No newline at end of file
diff --git a/web/app/assets/stylesheets/custom.css.scss b/web/app/assets/stylesheets/custom.css.scss
new file mode 100644
index 000000000..974ed3a85
--- /dev/null
+++ b/web/app/assets/stylesheets/custom.css.scss
@@ -0,0 +1,236 @@
+@import "bootstrap";
+
+/* mixins, variables, etc. */
+
+$grayMediumLight: #eaeaea;
+
+@mixin box_sizing {
+ -moz-box-sizing: border-box;
+ -webkit-box-sizing: border-box;
+ box-sizing: border-box;
+}
+
+/* universal */
+
+html {
+ overflow-y: scroll;
+}
+
+body {
+ padding-top: 60px;
+}
+
+section {
+ overflow: auto;
+}
+
+textarea {
+ resize: vertical;
+}
+
+.center {
+ text-align: center;
+ h1 {
+ margin-bottom: 10px;
+ }
+}
+
+/* typography */
+
+h1, h2, h3, h4, h5, h6 {
+ line-height: 1;
+}
+
+h1 {
+ font-size: 3em;
+ letter-spacing: -2px;
+ margin-bottom: 30px;
+ text-align: center;
+}
+
+h2 {
+ font-size: 1.7em;
+ letter-spacing: -1px;
+ margin-bottom: 30px;
+ text-align: center;
+ font-weight: normal;
+ color: $grayLight;
+}
+
+p {
+ font-size: 1.1em;
+ line-height: 1.7em;
+}
+
+
+/* header */
+
+#logo {
+ float: left;
+ margin-right: 10px;
+ font-size: 1.7em;
+ color: white;
+ text-transform: uppercase;
+ letter-spacing: -1px;
+ padding-top: 9px;
+ font-weight: bold;
+ line-height: 1;
+ &:hover {
+ color: white;
+ text-decoration: none;
+ }
+}
+
+/* footer */
+
+footer {
+ margin-top: 45px;
+ padding-top: 5px;
+ border-top: 1px solid $grayMediumLight;
+ color: $grayLight;
+ a {
+ color: $gray;
+ &:hover {
+ color: $grayDarker;
+ }
+ }
+ small {
+ float: left;
+ }
+ ul {
+ float: right;
+ list-style: none;
+ li {
+ float: left;
+ margin-left: 10px;
+ }
+ }
+}
+
+/* miscellaneous */
+
+.debug_dump {
+ clear: both;
+ float: left;
+ width: 100%;
+ margin-top: 45px;
+ @include box_sizing;
+}
+
+/* sidebar */
+
+aside {
+ section {
+ padding: 10px 0;
+ border-top: 1px solid $grayLighter;
+ &:first-child {
+ border: 0;
+ padding-top: 0;
+ }
+ span {
+ display: block;
+ margin-bottom: 3px;
+ line-height: 1;
+ }
+ h1 {
+ font-size: 1.6em;
+ text-align: left;
+ letter-spacing: -1px;
+ margin-bottom: 3px;
+ }
+ }
+}
+
+.gravatar {
+ float: left;
+ margin-right: 10px;
+}
+
+.stats {
+ overflow: auto;
+ a {
+ float: left;
+ padding: 0 10px;
+ border-left: 1px solid $grayLighter;
+ color: gray;
+ &:first-child {
+ padding-left: 0;
+ border: 0;
+ }
+ &:hover {
+ text-decoration: none;
+ color: $blue;
+ }
+ }
+ strong {
+ display: block;
+ }
+}
+
+.user_avatars {
+ overflow: auto;
+ margin-top: 10px;
+ .gravatar {
+ margin: 1px 1px;
+ }
+}
+
+/* forms */
+
+input, textarea, select, .uneditable-input {
+ border: 1px solid #bbb;
+ width: 100%;
+ padding: 10px;
+ height: auto;
+ margin-bottom: 15px;
+ @include box_sizing;
+}
+
+/** MSC: did this because firefox clips text if it's padding:4px on text input fields */
+input[type=text] {
+ padding: 2px !important;
+}
+
+#error_explanation {
+ color:#f00;
+ ul {
+ list-style: none;
+ margin: 0 0 18px 0;
+ }
+}
+
+.field_with_errors {
+ @extend .control-group;
+ @extend .error;
+ }
+
+ /* users index */
+
+.users {
+ list-style: none;
+ margin: 0;
+ li {
+ overflow: auto;
+ padding: 10px 0;
+ border-top: 1px solid $grayLighter;
+ &:last-child {
+ border-bottom: 1px solid $grayLighter;
+ }
+ }
+}
+
+
+.content {
+ display: block;
+}
+
+.timestamp {
+ color: $grayLight;
+}
+
+aside {
+ textarea {
+ height: 100px;
+ margin-bottom: 5px;
+ }
+}
diff --git a/web/app/assets/stylesheets/landing/footer.css.scss b/web/app/assets/stylesheets/landing/footer.css.scss
new file mode 100644
index 000000000..ad1bc8b7f
--- /dev/null
+++ b/web/app/assets/stylesheets/landing/footer.css.scss
@@ -0,0 +1,43 @@
+@charset "UTF-8";
+
+#footer-container {
+ position:absolute;
+ width:100%;
+ bottom:60px;
+ height:13px;
+
+}
+
+#footer {
+ padding-top: 10px;
+ margin: 30px 30px 0;
+ border-top:solid 1px #444;
+}
+
+#copyright {
+ float:left;
+ font-size:11px;
+ color:#ccc;
+}
+
+#footer-links {
+ float:right;
+ font-size:11px;
+ color:#ccc;
+}
+
+#footer-links a {
+ color:#ccc;
+ text-decoration:none;
+}
+
+#footer-links a:hover {
+ color:#fff;
+ text-decoration:underline;
+}
+
+#version {
+ font-size:11px;
+ color:#ccc;
+ text-align: center;
+}
\ No newline at end of file
diff --git a/web/app/assets/stylesheets/landing/landing.css b/web/app/assets/stylesheets/landing/landing.css
new file mode 100644
index 000000000..ef1d495a8
--- /dev/null
+++ b/web/app/assets/stylesheets/landing/landing.css
@@ -0,0 +1,9 @@
+/**
+*= require client/ie
+*= require client/jamkazam
+*= require client/screen_common
+*= require client/content
+*= require client/ftue
+*= require landing/simple_landing
+*= require landing/footer
+*/
\ No newline at end of file
diff --git a/web/app/assets/stylesheets/landing/simple_landing.css.scss b/web/app/assets/stylesheets/landing/simple_landing.css.scss
new file mode 100644
index 000000000..801064534
--- /dev/null
+++ b/web/app/assets/stylesheets/landing/simple_landing.css.scss
@@ -0,0 +1,250 @@
+html {
+ height:100%;
+}
+
+body {
+ //position:absolute !important;
+ padding:0 !important;
+ overflow: visible !important;
+ height:100%;
+ margin:0 !important;
+}
+
+#landing-container {
+ padding: 3% 0;
+ position:relative;
+ text-align: center;
+ // min-height:100%;
+}
+
+#landing-inner {
+ display:inline-block;
+ text-align:left;
+}
+
+.signin-overlay {
+ position:relative;
+ top:0;
+}
+
+
+strong {
+ font-weight: 600;
+}
+.logo-message {
+ display: block;
+ margin: 0 auto;
+ width: 247px;
+}
+
+.message-wrapper {
+ margin: 0 auto;
+ width: 480px;
+}
+.message-wrapper .left {
+ display: block;
+ overflow: visible;
+}
+.message {
+ display: block;
+ float: left;
+ margin-left: 20px;
+ overflow: visible;
+ width: 320px;
+}
+
+.message h2 {
+ border-bottom: 1px solid #FFFFFF;
+ color: #FFFFFF;
+ display: block;
+ font-weight: 200;
+ margin-bottom: 10px;
+ font-size:21px;
+}
+
+.overlay-small {
+ position:relative;
+ top:0;
+ display:block;
+ line-height: 28px;
+ height:auto;
+
+ .overlay-inner {
+ line-height:18px;
+ height:auto;
+ }
+
+ .spinner-large {
+ margin-left:64px;
+ }
+}
+
+
+.client-download {
+ margin-bottom:20px;
+}
+
+.currentOS {
+ span.platform {
+ font-size:18px;
+ }
+}
+
+// all custom CSS for the sign-in page goes here
+.signin-page {
+ .ftue-inner {
+ line-height:18px;
+ }
+
+ .ftue-left, .ftue-right {
+
+ }
+
+ fieldset[name=text-input]{
+ float:right;
+ margin-right:18px;
+ }
+
+ fieldset[name=signin-options] {
+ float:left;
+ margin:10px 0 0 10px;
+
+ small {
+ float:left;
+ }
+ }
+
+ fieldset[name=actions] {
+ float:right;
+ margin: 10px 19px 0 0;
+ }
+
+ .field {
+ right:0;
+ }
+
+ .email {
+ float:left;
+ margin-right:10px;
+
+ }
+
+ .password {
+ float:left;
+ }
+
+ label {
+ margin:27px 0 10px;
+ }
+
+
+
+ .already-member {
+
+ }
+
+ .keep-logged-in {
+
+ }
+
+ .forgot-password {
+ font-size:11px;
+ float:right;
+ margin:15px 19px 0 0;
+
+ a {
+ text-decoration: underline;
+ }
+ }
+
+ .login-error {
+ background-color: #330000;
+ border: 1px solid #990000;
+ padding:4px;
+ }
+
+ .login-error-msg {
+ display:none;
+ margin-top:10px;
+ text-align:center;
+ color:#F00;
+ font-size:11px;
+ }
+
+ fieldset.login-error .login-error-msg {
+ display:block;
+ }
+}
+
+// all custom CSS for the register page goes here
+.register-page {
+ .register-container {
+ padding:10px;
+ }
+
+ input.register-musician {
+
+ }
+
+ .error {
+ padding: 5px 12px 5px 5px;
+ margin-left:-5px;
+ margin-right:-12px;
+ }
+
+ input.register-fan {
+ margin-left:20px;
+ }
+
+ input[type=text], input[type=password] {
+ margin-top:1px;
+ width:100%;
+ }
+
+ select {
+ width:100%;
+ }
+
+
+ .right-side {
+ margin-left:25px;
+ }
+
+ .ftue-left {
+ margin-bottom:30px;
+
+ select {
+ width:104%;
+ }
+
+ div.field {
+ margin-top:31px;
+ width:43%;
+ float:left;
+ }
+ }
+
+ .ftue-right {
+
+ table {
+ border-collapse:separate;
+ border-spacing:6px;
+ }
+
+ label.instruments {
+ margin-bottom:2px;
+ }
+
+ div.field {
+ margin-top:15px;
+ }
+
+ a.tos {
+ text-decoration: underline;
+ }
+
+ input[type=submit] {
+ margin-top:20px;
+ }
+ }
+}
\ No newline at end of file
diff --git a/web/app/assets/stylesheets/lato.css b/web/app/assets/stylesheets/lato.css
new file mode 100644
index 000000000..ed16aefd1
--- /dev/null
+++ b/web/app/assets/stylesheets/lato.css
@@ -0,0 +1,116 @@
+@font-face {
+ font-family: 'LatoBlackItalic';
+ src: url('lato/Lato-BlaIta-webfont.eot');
+ src: url('lato/Lato-BlaIta-webfont.eot?#iefix') format('embedded-opentype'),
+ url('lato/Lato-BlaIta-webfont.woff') format('woff'),
+ url('lato/Lato-BlaIta-webfont.ttf') format('truetype'),
+ url('lato/Lato-BlaIta-webfont.svg#LatoBlackItalic') format('svg');
+ font-weight: normal;
+ font-style: normal;
+
+}
+
+@font-face {
+ font-family: 'LatoBlack';
+ src: url('lato/Lato-Bla-webfont.eot');
+ src: url('lato/Lato-Bla-webfont.eot?#iefix') format('embedded-opentype'),
+ url('lato/Lato-Bla-webfont.woff') format('woff'),
+ url('lato/Lato-Bla-webfont.ttf') format('truetype'),
+ url('lato/Lato-Bla-webfont.svg#LatoBlack') format('svg');
+ font-weight: normal;
+ font-style: normal;
+
+}
+
+@font-face {
+ font-family: 'LatoBoldItalic';
+ src: url('lato/Lato-BolIta-webfont.eot');
+ src: url('lato/Lato-BolIta-webfont.eot?#iefix') format('embedded-opentype'),
+ url('lato/Lato-BolIta-webfont.woff') format('woff'),
+ url('lato/Lato-BolIta-webfont.ttf') format('truetype'),
+ url('lato/Lato-BolIta-webfont.svg#LatoBoldItalic') format('svg');
+ font-weight: normal;
+ font-style: normal;
+
+}
+
+@font-face {
+ font-family: 'LatoBold';
+ src: url('lato/Lato-Bol-webfont.eot');
+ src: url('lato/Lato-Bol-webfont.eot?#iefix') format('embedded-opentype'),
+ url('lato/Lato-Bol-webfont.woff') format('woff'),
+ url('lato/Lato-Bol-webfont.ttf') format('truetype'),
+ url('lato/Lato-Bol-webfont.svg#LatoBold') format('svg');
+ font-weight: normal;
+ font-style: normal;
+
+}
+
+@font-face {
+ font-family: 'LatoItalic';
+ src: url('lato/Lato-RegIta-webfont.eot');
+ src: url('lato/Lato-RegIta-webfont.eot?#iefix') format('embedded-opentype'),
+ url('lato/Lato-RegIta-webfont.woff') format('woff'),
+ url('lato/Lato-RegIta-webfont.ttf') format('truetype'),
+ url('lato/Lato-RegIta-webfont.svg#LatoItalic') format('svg');
+ font-weight: normal;
+ font-style: normal;
+
+}
+
+@font-face {
+ font-family: 'LatoRegular';
+ src: url('lato/Lato-Reg-webfont.eot');
+ src: url('lato/Lato-Reg-webfont.eot?#iefix') format('embedded-opentype'),
+ url('lato/Lato-Reg-webfont.woff') format('woff'),
+ url('lato/Lato-Reg-webfont.ttf') format('truetype'),
+ url('lato/Lato-Reg-webfont.svg#LatoRegular') format('svg');
+ font-weight: normal;
+ font-style: normal;
+
+}
+@font-face {
+ font-family: 'LatoLightItalic';
+ src: url('lato/Lato-LigIta-webfont.eot');
+ src: url('lato/Lato-LigIta-webfont.eot?#iefix') format('embedded-opentype'),
+ url('lato/Lato-LigIta-webfont.woff') format('woff'),
+ url('lato/Lato-LigIta-webfont.ttf') format('truetype'),
+ url('lato/Lato-LigIta-webfont.svg#LatoLightItalic') format('svg');
+ font-weight: normal;
+ font-style: normal;
+
+}
+@font-face {
+ font-family: 'LatoLight';
+ src: url('lato/Lato-Lig-webfont.eot');
+ src: url('lato/Lato-Lig-webfont.eot?#iefix') format('embedded-opentype'),
+ url('lato/Lato-Lig-webfont.woff') format('woff'),
+ url('lato/Lato-Lig-webfont.ttf') format('truetype'),
+ url('lato/Lato-Lig-webfont.svg#LatoLight') format('svg');
+ font-weight: normal;
+ font-style: normal;
+
+}
+@font-face {
+ font-family: 'LatoHairlineItalic';
+ src: url('lato/Lato-HaiIta-webfont.eot');
+ src: url('lato/Lato-HaiIta-webfont.eot?#iefix') format('embedded-opentype'),
+ url('lato/Lato-HaiIta-webfont.woff') format('woff'),
+ url('lato/Lato-HaiIta-webfont.ttf') format('truetype'),
+ url('lato/Lato-HaiIta-webfont.svg#LatoHairlineItalic') format('svg');
+ font-weight: normal;
+ font-style: normal;
+
+}
+
+@font-face {
+ font-family: 'LatoHairline';
+ src: url('lato/Lato-Hai-webfont.eot');
+ src: url('lato/Lato-Hai-webfont.eot?#iefix') format('embedded-opentype'),
+ url('lato/Lato-Hai-webfont.woff') format('woff'),
+ url('lato/Lato-Hai-webfont.ttf') format('truetype'),
+ url('lato/Lato-Hai-webfont.svg#LatoHairline') format('svg');
+ font-weight: normal;
+ font-style: normal;
+
+}
\ No newline at end of file
diff --git a/web/app/assets/stylesheets/screen_common.css.scss b/web/app/assets/stylesheets/screen_common.css.scss
new file mode 100644
index 000000000..aeb8ead26
--- /dev/null
+++ b/web/app/assets/stylesheets/screen_common.css.scss
@@ -0,0 +1 @@
+/* Styles used across multiple screens */
diff --git a/web/app/assets/stylesheets/sessions.css.scss b/web/app/assets/stylesheets/sessions.css.scss
new file mode 100644
index 000000000..ccb1ed25b
--- /dev/null
+++ b/web/app/assets/stylesheets/sessions.css.scss
@@ -0,0 +1,3 @@
+// Place all the styles related to the Sessions controller here.
+// They will automatically be included in application.css.
+// You can use Sass (SCSS) here: http://sass-lang.com/
diff --git a/web/app/assets/stylesheets/static_pages.css.scss b/web/app/assets/stylesheets/static_pages.css.scss
new file mode 100644
index 000000000..d55836c3c
--- /dev/null
+++ b/web/app/assets/stylesheets/static_pages.css.scss
@@ -0,0 +1,3 @@
+// Place all the styles related to the StaticPages controller here.
+// They will automatically be included in application.css.
+// You can use Sass (SCSS) here: http://sass-lang.com/
diff --git a/web/app/assets/stylesheets/users.css.scss b/web/app/assets/stylesheets/users.css.scss
new file mode 100644
index 000000000..31a2eacb8
--- /dev/null
+++ b/web/app/assets/stylesheets/users.css.scss
@@ -0,0 +1,3 @@
+// Place all the styles related to the Users controller here.
+// They will automatically be included in application.css.
+// You can use Sass (SCSS) here: http://sass-lang.com/
diff --git a/web/app/controllers/api_bands_controller.rb b/web/app/controllers/api_bands_controller.rb
new file mode 100644
index 000000000..b815208d0
--- /dev/null
+++ b/web/app/controllers/api_bands_controller.rb
@@ -0,0 +1,169 @@
+class ApiBandsController < ApiController
+
+ before_filter :api_signed_in_user, :except => [:index, :show, :follower_index]
+ before_filter :auth_band_member, :only => [:update,
+ :recording_create, :recording_update, :recording_destroy,
+ :invitation_index, :invitation_show, :invitation_create, :invitation_destroy]
+
+ respond_to :json
+
+ def index
+ @bands = Band.paginate(page: params[:page])
+ end
+
+ def show
+ @band = Band.find(params[:id])
+ end
+
+ def create
+ @band = Band.save(params[:id],
+ params[:name],
+ params[:website],
+ params[:biography],
+ params[:city],
+ params[:state],
+ params[:country],
+ params[:genres],
+ current_user.id,
+ params[:photo_url],
+ params[:logo_url])
+
+ respond_with @band, responder: ApiResponder, :status => 201, :location => api_band_detail_url(@band)
+ end
+
+ def update
+ @band = Band.save(params[:id],
+ params[:name],
+ params[:website],
+ params[:biography],
+ params[:city],
+ params[:state],
+ params[:country],
+ params[:genres],
+ current_user.id,
+ params[:photo_url],
+ params[:logo_url])
+
+ respond_with @band, responder: ApiResponder, :status => :ok
+ end
+
+ ###################### FOLLOWERS ########################
+ def liker_index
+ # NOTE: liker_index.rabl template references the likers property
+ @band = Band.find(params[:id])
+ end
+
+ ###################### FOLLOWERS ########################
+ def follower_index
+ # NOTE: follower_index.rabl template references the followers property
+ @band = Band.find(params[:id])
+ end
+
+ ###################### RECORDINGS #######################
+ def recording_index
+ @recordings = Band.recording_index(current_user, params[:id])
+ respond_with @recordings, responder: ApiResponder, :status => 200
+ end
+
+ def recording_show
+ hide_private = false
+ band = Band.find(params[:id])
+
+ # hide private Recordings from anyone who's not in the Band
+ unless band.users.exists? current_user
+ hide_private = true
+ end
+
+ @recording = Recording.find(params[:recording_id])
+ if !@recording.public && hide_private
+ render :json => { :message => "You are not allowed to access this recording." }, :status => 403
+ #respond_with "You are not allowed to view this recording.", responder: ApiResponder, :status => 403
+ else
+ respond_with @recording, responder: ApiResponder, :status => 200
+ end
+ end
+
+ def recording_create
+ @recording = Recording.save(params[:recording_id],
+ params[:public],
+ params[:description],
+ params[:genres],
+ current_user.id,
+ params[:id],
+ true)
+
+ respond_with @recording, responder: ApiResponder, :status => 201, :location => api_band_recording_detail_url(@band, @recording)
+ end
+
+ def recording_update
+ @recording = Recording.save(params[:recording_id],
+ params[:public],
+ params[:description],
+ params[:genres],
+ current_user.id,
+ params[:id],
+ false)
+
+ respond_with @recording, responder: ApiResponder, :status => 200
+ end
+
+ def recording_destroy
+ @recording = Recording.find(params[:recording_id])
+
+ unless @recording.nil?
+ @recording.delete
+ respond_with responder: ApiResponder, :status => 204
+ end
+
+ # no recording was found with this ID
+ render :json => { :message => ValidationMessages::RECORDING_NOT_FOUND }, :status => 404
+ end
+
+ ###################### INVITATIONS ######################
+ def invitation_index
+ @invitations = @band.invitations
+ respond_with @invitations, responder: ApiResponder, :status => 200
+ end
+
+ def invitation_show
+ begin
+ @invitation = BandInvitation.find(params[:invitation_id])
+ respond_with @invitation, responder: ApiResponder, :status => 200
+
+ rescue ActiveRecord::RecordNotFound
+ render :json => { :message => ValidationMessages::BAND_INVITATION_NOT_FOUND }, :status => 404
+ end
+ end
+
+ def invitation_create
+ @invitation = BandInvitation.save(params[:invitation_id],
+ params[:id],
+ params[:user_id],
+ current_user.id,
+ params[:accepted])
+
+ respond_with @invitation, responder: ApiResponder, :status => 201, :location => api_band_invitation_detail_url(@band, @invitation)
+ end
+
+
+ def invitation_destroy
+ begin
+ @invitation = BandInvitation.find(params[:invitation_id])
+ @invitation.delete
+ respond_with responder: ApiResponder, :status => 204
+
+ rescue ActiveRecord::RecordNotFound
+ render :json => { :message => ValidationMessages::BAND_INVITATION_NOT_FOUND }, :status => 404
+ end
+ end
+
+ #############################################################################
+ protected
+ # ensures user is a member of the band
+ def auth_band_member
+ @band = Band.find(params[:id])
+ unless @band.users.exists? current_user
+ raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR
+ end
+ end
+end
\ No newline at end of file
diff --git a/web/app/controllers/api_claimed_recordings_controller.rb b/web/app/controllers/api_claimed_recordings_controller.rb
new file mode 100644
index 000000000..f19fe5c88
--- /dev/null
+++ b/web/app/controllers/api_claimed_recordings_controller.rb
@@ -0,0 +1,44 @@
+class ApiClaimedRecordingsController < ApiController
+
+ before_filter :api_signed_in_user
+ before_filter :look_up_claimed_recording, :only => [ :show, :update, :delete ]
+
+ respond_to :json
+
+ def index
+ @claimed_recordings = ClaimedRecording.where(:user_id => current_user.id).order("created_at DESC").paginate(page: params[:page])
+ end
+
+ def show
+ @claimed_recording
+ end
+
+ def update
+ begin
+ @claimed_recording.update_fields(current_user, params)
+ respond_with responder: ApiResponder, :status => 204
+ rescue
+ render :json => { :message => "claimed_recording could not be updated" }, :status => 403
+ end
+ end
+
+ def delete
+ begin
+ @claimed_recording.discard(current_user)
+ render :json => {}, :status => 204
+# respond_with responder: ApiResponder, :status => 204
+ rescue
+ render :json => { :message => "claimed_recording could not be deleted" }, :status => 403
+ end
+ end
+
+ private
+
+ def look_up_claimed_recording
+ @claimed_recording = ClaimedRecording.find(params[:id])
+ if @claimed_recording.nil? || @claimed_recording.user_id != current_user.id
+ render :json => { :message => "claimed_recording not found" }, :status => 404
+ end
+ end
+
+end
diff --git a/web/app/controllers/api_controller.rb b/web/app/controllers/api_controller.rb
new file mode 100644
index 000000000..b3ae388e4
--- /dev/null
+++ b/web/app/controllers/api_controller.rb
@@ -0,0 +1,39 @@
+class ApiController < ApplicationController
+
+ @@log = Logging.logger[ApiController]
+
+ # define common error handlers
+ rescue_from 'JamRuby::StateError' do |exception|
+ @exception = exception
+ render "errors/state_error", :status => 400
+ end
+ rescue_from 'JamRuby::JamArgumentError' do |exception|
+ @exception = exception
+ render "errors/jam_argument_error", :status => 400
+ end
+ rescue_from 'JamRuby::PermissionError' do |exception|
+ @exception = exception
+ render "errors/permission_error", :status => 403
+ end
+ rescue_from 'ActiveRecord::RecordNotFound' do |exception|
+ @@log.debug(exception)
+ render :json => { :errors => { :resource => ["record not found"] } }, :status => 404
+ end
+ rescue_from 'PG::Error' do |exception|
+ @@log.debug(exception)
+ if exception.to_s.include? "duplicate key value violates unique constraint"
+ render :json => { :errors => { :resource => ["resource already exists"] } }, :status => 409 # 409 = conflict
+ else
+ raise exception
+ end
+ end
+
+ protected
+ def auth_user
+ unless current_user.id == params[:id]
+ raise PermissionError, ValidationMessages::PERMISSION_VALIDATION_ERROR
+ end
+
+ @user = User.find(params[:id])
+ end
+end
\ No newline at end of file
diff --git a/web/app/controllers/api_corporate_controller.rb b/web/app/controllers/api_corporate_controller.rb
new file mode 100644
index 000000000..be48fc830
--- /dev/null
+++ b/web/app/controllers/api_corporate_controller.rb
@@ -0,0 +1,21 @@
+class ApiCorporateController < ApiController
+
+ respond_to :json
+
+ def feedback
+ email = params[:email]
+ body = params[:body]
+
+ feedback = Feedback.new
+ feedback.email = email
+ feedback.body = body
+ feedback.save
+
+ if feedback.errors.any?
+ response.status = :unprocessable_entity
+ render :json => { :errors => feedback.errors }
+ else
+ render :json => {}, :status => :ok # an empty response, but 200 OK
+ end
+ end
+end
diff --git a/web/app/controllers/api_genres_controller.rb b/web/app/controllers/api_genres_controller.rb
new file mode 100644
index 000000000..43dac8740
--- /dev/null
+++ b/web/app/controllers/api_genres_controller.rb
@@ -0,0 +1,18 @@
+class ApiGenresController < ApiController
+
+ # have to be signed in currently to see this screen
+ before_filter :api_signed_in_user
+
+ respond_to :json
+
+ def index
+ @genres = Genre.find(:all)
+ end
+
+ def show
+ @genre = Genre.find(params[:id])
+ gon.genre_id = @genre.id
+ gon.description = @genre.description
+ end
+
+end
diff --git a/web/app/controllers/api_instruments_controller.rb b/web/app/controllers/api_instruments_controller.rb
new file mode 100644
index 000000000..fc2c44fd9
--- /dev/null
+++ b/web/app/controllers/api_instruments_controller.rb
@@ -0,0 +1,18 @@
+class ApiInstrumentsController < ApiController
+
+ # have to be signed in currently to see this screen
+ before_filter :api_signed_in_user
+
+ respond_to :json
+
+ def index
+ @instruments = Instrument.standard_list
+ end
+
+ def show
+ @instrument = Instrument.find(params[:id])
+ gon.instrument_id = @instrument.id
+ gon.description = @instrument.description
+ end
+
+end
diff --git a/web/app/controllers/api_invitations_controller.rb b/web/app/controllers/api_invitations_controller.rb
new file mode 100644
index 000000000..1e5a8c858
--- /dev/null
+++ b/web/app/controllers/api_invitations_controller.rb
@@ -0,0 +1,69 @@
+class ApiInvitationsController < ApiController
+
+ # have to be signed in currently to see this screen
+ before_filter :api_signed_in_user
+
+ respond_to :json
+
+ def index
+ conditions = {}
+ sender_id = params[:sender]
+ receiver_id = params[:receiver]
+
+ if !sender_id.nil?
+ if current_user.id != sender_id
+ raise PermissionError, "You can only ask for your own sent invitations"
+ end
+
+ @invitations = Invitation.where(:sender_id => current_user.id)
+ elsif !receiver_id.nil?
+ if current_user.id != receiver_id
+ raise PermissionError, "You can only ask for your own received invitations"
+ end
+
+ @invitations = Invitation.where(:receiver_id => current_user.id)
+ else
+ # default to invitations you've received
+ @invitations = Invitation.where(:receiver_id => current_user.id)
+ end
+ end
+
+ def create
+ music_session = MusicSession.find(params[:music_session])
+ receiver = User.find(params[:receiver])
+ sender = current_user
+ join_request = JoinRequest.find(params[:join_request]) unless params[:join_request].nil?
+
+ @invitation = Invitation.new
+ @invitation.music_session = music_session
+ @invitation.sender = sender
+ @invitation.receiver = receiver
+ @invitation.join_request = join_request
+
+ @invitation.save
+
+ unless @invitation.errors.any?
+ User.save_session_settings(current_user, music_session)
+
+ # send notification
+ # Notification.send_session_invitation(receiver.id, @invitation.id)
+ respond_with @invitation, :responder => ApiResponder, :location => api_invitation_detail_url(@invitation)
+
+ else
+ # we have to do this because api_invitation_detail_url will fail with a bad @invitation
+ response.status = :unprocessable_entity
+ respond_with @invitation
+ end
+ end
+
+ def show
+ @invitation = Invitation.find(params[:id], :conditions => ["receiver_id = ? or sender_id = ?", current_user.id, current_user.id])
+ end
+
+ def delete
+ @invitation = Invitation.find(params[:id], :conditions => ["sender_id = ?", current_user.id])
+ @invitation.delete
+
+ respond_with @invitation, responder => ApiResponder
+ end
+end
diff --git a/web/app/controllers/api_invited_users_controller.rb b/web/app/controllers/api_invited_users_controller.rb
new file mode 100644
index 000000000..d0540f24d
--- /dev/null
+++ b/web/app/controllers/api_invited_users_controller.rb
@@ -0,0 +1,32 @@
+class ApiInvitedUsersController < ApiController
+
+ # have to be signed in currently to see this screen
+ before_filter :api_signed_in_user
+
+ respond_to :json
+
+ def index
+ @invited_users = InvitedUser.index(current_user)
+ end
+
+ def show
+ @invited_user = InvitedUser.find(params[:id])
+ end
+
+ def create
+ @invited_user = InvitedUser.new
+ @invited_user.sender = current_user
+ @invited_user.email = params[:email]
+ @invited_user.autofriend = true
+ @invited_user.note = params[:note].blank? ? nil : params[:note]
+ @invited_user.save
+
+ unless @invited_user.errors.any?
+ respond_with @invited_user, :responder => ApiResponder, :location => api_invited_user_detail_url(@invited_user)
+ else
+ response.status = :unprocessable_entity
+ respond_with @invited_user
+ end
+ end
+
+end
diff --git a/web/app/controllers/api_join_requests_controller.rb b/web/app/controllers/api_join_requests_controller.rb
new file mode 100644
index 000000000..43c2a871c
--- /dev/null
+++ b/web/app/controllers/api_join_requests_controller.rb
@@ -0,0 +1,45 @@
+class ApiJoinRequestsController < ApiController
+
+ # have to be signed in currently to see this screen
+ before_filter :api_signed_in_user
+
+ respond_to :json
+
+ def index
+ @join_requests = JoinRequest.index(current_user)
+ end
+
+ def show
+ @join_request = JoinRequest.show(params[:id], current_user)
+ end
+
+ def create
+ music_session = MusicSession.find(params[:music_session])
+ text = params[:text]
+ sender = current_user
+
+ @join_request = JoinRequest.new
+ @join_request.music_session = music_session
+ @join_request.user = sender
+ @join_request.text = text
+
+ @join_request.save
+
+ if @join_request.errors.any?
+ response.status = :unprocessable_entity
+ respond_with @join_request
+ else
+ # send notification
+ Notification.send_join_request(music_session, @join_request, sender, text)
+ respond_with @join_request, :responder => ApiResponder, :location => api_join_request_detail_url(@join_request)
+ end
+ end
+
+ def delete
+ @join_request = JoinRequest.show(params[:id], current_user)
+ @join_request.delete
+
+ respond_with @join_request, responder => ApiResponder
+ end
+
+end
diff --git a/web/app/controllers/api_maxmind_requests_controller.rb b/web/app/controllers/api_maxmind_requests_controller.rb
new file mode 100644
index 000000000..5173c6754
--- /dev/null
+++ b/web/app/controllers/api_maxmind_requests_controller.rb
@@ -0,0 +1,37 @@
+class ApiMaxmindRequestsController < ApiController
+
+ respond_to :json
+
+ def countries
+ countries = MaxMindManager.countries()
+ render :json => { :countries => countries }, :status => 200
+ end
+
+ def regions
+ regions = MaxMindManager.regions(params[:country])
+ if regions && regions.length > 0
+ render :json => { :regions => regions }, :status => 200
+ else
+ render :json => { :message => "Unrecognized Country" }, :status => 422
+ end
+ end
+
+ def cities
+ cities = MaxMindManager.cities(params[:country], params[:region])
+ if cities && cities.length > 0
+ render :json => { :cities => cities }, :status => 200
+ else
+ render :json => { :message => "Unrecognized country or region" }, :status => 422
+ end
+ end
+
+ def isps
+ isps = MaxMindManager.isps(params[:country])
+ if isps && isps.length > 0
+ render :json => { :isps => isps }, :status => 200
+ else
+ render :json => { :message => "Unrecognized Country" }, :status => 422
+ end
+ end
+
+end
\ No newline at end of file
diff --git a/web/app/controllers/api_mixes_controller.rb b/web/app/controllers/api_mixes_controller.rb
new file mode 100644
index 000000000..013ed9435
--- /dev/null
+++ b/web/app/controllers/api_mixes_controller.rb
@@ -0,0 +1,55 @@
+class ApiMixesController < ApiController
+
+ # This must be present on requests from the cron to prevent hackers from
+ # hitting these routes.
+ CRON_TOKEN = "2kkl39sjjf3ijdsflje2923j"
+
+ before_filter :api_signed_in_user, :only => [ :schedule ]
+ before_filter :require_cron_token, :only => [ :next, :finish ]
+ before_filter :look_up_mix, :only => [ :finish ]
+
+ respond_to :json
+
+ def schedule
+ begin
+ Mix.schedule(params[:recording_id], current_user, params[:description], params[:manifest])
+ respond_with responder: ApiResponder, :status => 204
+ rescue
+ render :json => { :message => "mix could not be scheduled" }, :status => 403
+ end
+ end
+
+ def next
+ begin
+ mix = Mix.next(params[:server])
+ respond_with responder: ApiResponder, :status => 204 if mix.nil?
+ render :json => { :id => mix.id, :manifest => mix.manifest, :destination => mix.s3_url }, :status => 200
+ rescue
+ render :json => { :message => "next mix could not be found" }, :status => 403
+ end
+ end
+
+ def finish
+ begin
+ @mix.finish
+ rescue
+ render :json => { :message => "mix finish failed" }, :status => 403
+ end
+ respond_with responder: ApiResponder, :status => 204
+ end
+
+ private
+
+ def look_up_mix
+ @mix = Mix.find(params[:id])
+ if @mix.nil? || (!@is_cron && @mix.owner_id != current_user.id)
+ render :json => { :message => "mix not found" }, :status => 404
+ end
+ end
+
+ def require_cron_token
+ render :json => { :message => "bad token" }, :status => 403 unless params[:token] == CRON_TOKEN
+ @is_cron = true
+ end
+
+end
diff --git a/web/app/controllers/api_music_sessions_controller.rb b/web/app/controllers/api_music_sessions_controller.rb
new file mode 100644
index 000000000..49d8e5e98
--- /dev/null
+++ b/web/app/controllers/api_music_sessions_controller.rb
@@ -0,0 +1,212 @@
+require 'aws-sdk'
+
+class ApiMusicSessionsController < ApiController
+
+ # have to be signed in currently to see this screen
+ before_filter :api_signed_in_user
+ before_filter :lookup_session, only: [:show, :update, :delete]
+ skip_before_filter :api_signed_in_user, only: [:perf_upload]
+
+ respond_to :json
+
+ def index
+ # params[:participants] is either nil, meaning "everything", or it's an array of musician ids
+ # params[:genres] is either nil, meaning "everything", or it's an array of genre ids
+ # params[:friends_only] does the obvious.
+ # params[:my_bands_only] also does the obvious.
+ # Importantly, friends and my_bands are ORed not ANDed. So, if you specify both as true, you'll get more results, not fewer.
+ @music_sessions = MusicSession.index(current_user, params[:participants], params[:genres], params[:friends_only], params[:my_bands_only], params[:keyword])
+ end
+
+ def create
+ client_id = params[:client_id]
+
+ if client_id.nil?
+ raise JamArgumentError, "client_id must be specified"
+ end
+
+ if !params[:intellectual_property]
+ raise JamArgumentError, "You must agree to the intellectual property terms"
+ end
+
+ band = Band.find(params[:band]) unless params[:band].nil?
+
+ @music_session = MusicSessionManager.new.create(
+ current_user,
+ client_id,
+ params[:description],
+ params[:musician_access],
+ params[:approval_required],
+ params[:fan_chat],
+ params[:fan_access],
+ band,
+ params[:genres],
+ params[:tracks],
+ params[:legal_terms])
+
+ if @music_session.errors.any?
+ # we have to do this because api_session_detail_url will fail with a bad @music_session
+ response.status = :unprocessable_entity
+ respond_with @music_session
+ else
+ respond_with @music_session, responder: ApiResponder, :location => api_session_detail_url(@music_session)
+ end
+ end
+
+ def show
+ unless @music_session.can_see? current_user
+ raise ActiveRecord::RecordNotFound
+ end
+ end
+
+ def update
+ @music_session = MusicSessionManager.new.update(
+ @music_session,
+ params[:description],
+ params[:genres],
+ params[:musician_access],
+ params[:approval_required],
+ params[:fan_chat],
+ params[:fan_access])
+
+ if @music_session.errors.any?
+ # we have to do this because api_session_detail_url will fail with a bad @music_session
+ response.status = :unprocessable_entity
+ respond_with @music_session
+ else
+ respond_with @music_session, responder: ApiResponder, :location => api_session_detail_url(@music_session)
+ end
+ end
+
+ def participant_show
+ @connection = Connection.find_by_client_id(params[:id])
+ end
+
+ def participant_create
+
+ @connection = MusicSessionManager.new.participant_create(
+ current_user,
+ params[:id],
+ params[:client_id],
+ params[:as_musician],
+ params[:tracks])
+
+ if @connection.errors.any?
+ response.status = :unprocessable_entity
+ respond_with @connection
+ else
+ respond_with @connection, responder: ApiResponder, :location => api_session_participant_detail_url(@connection.client_id)
+ end
+ end
+
+ def participant_delete
+ client_id = params[:id]
+ @connection = Connection.find_by_client_id!(client_id)
+ music_session = MusicSession.find(@connection.music_session_id)
+
+ MusicSessionManager.new.participant_delete(current_user, @connection, music_session)
+
+ respond_with @connection, responder: ApiResponder
+ end
+
+ def lookup_session
+ @music_session = MusicSession.find(params[:id])
+ end
+
+ def track_index
+ @tracks = Track.index(current_user, params[:id])
+ end
+
+ def track_show
+ begin
+ @track = Track.joins(:connection)
+ .where(:connections => {:user_id => "#{current_user.id}"})
+ .find(params[:track_id])
+
+ rescue ActiveRecord::RecordNotFound
+ render :json => { :message => ValidationMessages::TRACK_NOT_FOUND }, :status => 404
+ end
+ end
+
+ def track_create
+ @track = Track.save(nil,
+ params[:connection_id],
+ params[:instrument_id],
+ params[:sound])
+
+ respond_with @track, responder: ApiResponder, :status => 201, :location => api_session_track_detail_url(@track.connection.music_session, @track)
+ end
+
+ def track_update
+ begin
+ @track = Track.save(params[:track_id],
+ nil,
+ params[:instrument_id],
+ params[:sound])
+
+ respond_with @track, responder: ApiResponder, :status => 200
+
+ rescue ActiveRecord::RecordNotFound
+ render :json => { :message => ValidationMessages::TRACK_NOT_FOUND }, :status => 404
+ end
+ end
+
+ def track_destroy
+ begin
+ @track = Track.find(params[:track_id])
+ unless @track.nil?
+ @track.delete
+ respond_with responder: ApiResponder, :status => 204
+ end
+ rescue ActiveRecord::RecordNotFound
+ render :json => { :message => ValidationMessages::TRACK_NOT_FOUND }, :status => 404
+ end
+ end
+
+ def perf_upload
+ # example of using curl to access this API:
+ # curl -L -T some_file -X PUT http://localhost:3000/api/sessions/[SESSION_ID]/perf.json?client_id=[CLIENT_ID]
+
+ music_session = MusicSessionHistory.find_by_music_session_id(params[:id])
+
+ @perfdata = MusicSessionPerfData.new()
+ @perfdata.music_session = music_session
+ @perfdata.client_id = params[:client_id]
+ unless @perfdata.save
+ # we have to do this because api_session_detail_url will fail with a bad @music_session
+ response.status = :unprocessable_entity
+ respond_with @perfdata
+ return
+ end
+
+ if SampleApp::Application.config.storage_type == :fog
+ uri = @perf_data.uri
+ s3 = AWS::S3.new(:access_key_id => SampleApp::Application.config.aws_access_key_id,
+ :secret_access_key => SampleApp::Application.config.aws_secret_access_key)
+ bucket = s3.buckets[SampleApp::Application.config.aws_bucket]
+
+ read_url = bucket.objects[uri].url_for(:read,
+ # :expires => SampleApp::Application.config.perf_data_signed_url_timeout * 90,
+ :'response_content_type' => 'text/csv').to_s
+ @perfdata.update_attribute(:uri, read_url)
+
+ write_url = bucket.objects[uri].url_for(:write,
+ :expires => SampleApp::Application.config.perf_data_signed_url_timeout,
+ :'response_content_type' => 'text/csv').to_s
+ logger.debug("*** server can upload to url #{write_url}")
+ redirect_to write_url
+
+ else
+ if params[:redirected_back].nil? || !params[:redirected_back]
+ # first time that a client has asked to do a PUT (not redirected back here)
+ redirect_to request.fullpath + '&redirected_back=true'
+ else
+ # we should store it here to aid in development, but we don't have to until someone wants the feature
+ # so... just return 200
+ render :json => { :id => @perfdata.id }, :status => 200
+ end
+
+ end
+
+ end
+end
diff --git a/web/app/controllers/api_recordings_controller.rb b/web/app/controllers/api_recordings_controller.rb
new file mode 100644
index 000000000..d0500a0dc
--- /dev/null
+++ b/web/app/controllers/api_recordings_controller.rb
@@ -0,0 +1,94 @@
+class ApiRecordingsController < ApiController
+
+ before_filter :api_signed_in_user
+ before_filter :look_up_recording, :only => [ :stop, :claim ]
+ before_filter :parse_filename, :only => [ :upload_next_part, :upload_sign, :upload_part_complete, :upload_complete ]
+
+ respond_to :json
+
+ # Returns all files this user should have synced down to his computer
+ def list
+ begin
+ render :json => Recording.list(current_user), :status => 200
+ rescue
+ render :json => { :message => "could not produce list of files" }, :status => 403
+ end
+ end
+
+ def start
+ begin
+ Recording.start(params[:music_session_id], current_user)
+ respond_with responder: ApiResponder, :status => 204
+ rescue
+ render :json => { :message => "recording could not be started" }, :status => 403
+ end
+ end
+
+ def stop
+ begin
+ if @recording.owner_id != current_user.id
+ render :json => { :message => "recording not found" }, :status => 404
+ end
+ @recording.stop
+ respond_with responder: ApiResponder, :status => 204
+ rescue
+ render :json => { :message => "recording could not be stopped" }, :status => 403
+ end
+ end
+
+
+ def claim
+ begin
+ claimed_recording = @recording.claim(current_user, params[:name], Genre.find(params[:genre_id]), params[:is_public], params[:is_downloadable])
+ render :json => { :claimed_recording_id => claimed_recording.id }, :status => 200
+ rescue
+ render :json => { :message => "recording could not be claimed" }, :status => 403
+ end
+ end
+
+ def upload_next_part
+ if @recorded_track.next_part_to_upload == 0
+ if (!params[:length] || !params[:md5])
+ render :json => { :message => "missing fields" }, :status => 403
+ end
+ @recorded_track.upload_start(params[:length], params[:md5])
+ end
+
+ render :json => { :part => @recorded_track.next_part_to_upload }, :status => 200
+ end
+
+ def upload_sign
+ render :json => @recorded_track.upload_sign(params[:content_md5]), :status => 200
+ end
+
+ def upload_part_complete
+ begin
+ @recorded_track.upload_part_complete(params[:part])
+ rescue
+ render :json => { :message => "part out of order" }, :status => 403
+ end
+ respond_with responder: ApiResponder, :status => 204
+ end
+
+ def upload_complete
+ @recorded_track.upload_complete
+ respond_with responder: ApiResponder, :status => 204
+ end
+
+ private
+
+ def parse_filename
+ @recorded_track = RecordedTrack.find_by_upload_filename(params[:filename])
+ unless @recorded_track
+ render :json => { :message => RECORDING_NOT_FOUND }, :status => 404
+ end
+ end
+
+ def look_up_recording
+ @recording = Recording.find(params[id])
+ if @recording.nil?
+ render :json => { :message => "recording not found" }, :status => 404
+ end
+ end
+
+end
diff --git a/web/app/controllers/api_search_controller.rb b/web/app/controllers/api_search_controller.rb
new file mode 100644
index 000000000..40e01637b
--- /dev/null
+++ b/web/app/controllers/api_search_controller.rb
@@ -0,0 +1,11 @@
+class ApiSearchController < ApiController
+
+ # have to be signed in currently to see this screen
+ before_filter :api_signed_in_user
+
+ respond_to :json
+
+ def index
+ @search = Search.search(params[:query], current_user.id)
+ end
+end
diff --git a/web/app/controllers/api_users_controller.rb b/web/app/controllers/api_users_controller.rb
new file mode 100644
index 000000000..3b471097c
--- /dev/null
+++ b/web/app/controllers/api_users_controller.rb
@@ -0,0 +1,585 @@
+class ApiUsersController < ApiController
+
+ before_filter :api_signed_in_user, :except => [:create, :signup_confirm, :auth_session_create, :complete, :finalize_update_email, :isp_scoring]
+ before_filter :auth_user, :only => [:session_settings_show, :session_history_index, :session_user_history_index, :update, :delete,
+ :like_create, :like_destroy, # likes
+ :following_create, :following_show, :following_destroy, # followings
+ :recording_update, :recording_destroy, # recordings
+ :favorite_create, :favorite_destroy, # favorites
+ :friend_request_index, :friend_request_show, :friend_request_create, :friend_request_update, # friend requests
+ :friend_show, :friend_destroy, # friends
+ :notification_index, :notification_destroy, # notifications
+ :band_invitation_index, :band_invitation_show, :band_invitation_update, # band invitations
+ :set_password, :begin_update_email, :update_avatar, :delete_avatar, :generate_filepicker_policy]
+
+ respond_to :json
+
+ def index
+ # don't return users that aren't yet confirmed
+ @users = User.where('email_confirmed=TRUE').paginate(page: params[:page])
+ respond_with @users, responder: ApiResponder, :status => 200
+ end
+
+ def show
+ # don't return users that aren't yet confirmed
+ @user = User.where('email_confirmed=TRUE').find(params[:id])
+ respond_with @user, responder: ApiResponder, :status => 200
+ end
+
+ # this API call is disabled by virtue of it being commented out in routes.rb
+ # the reason is that it has no captcha, and is therefore a bit abuseable
+ # if someone wants to use it, please add in captcha or some other bot-protector
+ def create
+ # sends email to email account for confirmation
+ @user = UserManager.new.signup(params[:first_name],
+ params[:last_name],
+ params[:email],
+ params[:password],
+ params[:password_confirmation],
+ params[:city],
+ params[:state],
+ params[:country],
+ params[:instruments],
+ params[:photo_url],
+ ApplicationHelper.base_uri(request) + "/confirm")
+
+ # check for errors
+ unless @user.errors.any?
+ render :json => {}, :status => :ok # an empty response, but 200 OK
+ else
+ response.status = :unprocessable_entity
+ respond_with @user, responder: ApiResponder
+ end
+ end
+
+ def update
+
+ @user = User.find(params[:id])
+
+
+ @user.first_name = params[:first_name] if params.has_key?(:first_name)
+ @user.last_name = params[:last_name] if params.has_key?(:last_name)
+ @user.gender = params[:gender] if params.has_key?(:gender)
+ @user.birth_date = Date.strptime(params[:birth_date], '%m-%d-%Y') if params.has_key?(:birth_date)
+ @user.city = params[:city] if params.has_key?(:city)
+ @user.state = params[:state] if params.has_key?(:state)
+ @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)
+
+ @user.save
+
+ if @user.errors.any?
+ respond_with @user, :status => :unprocessable_entity
+ else
+ respond_with @user, responder: ApiResponder, :status => 200
+ end
+ end
+
+ # a user that is created administratively has an incomplete profile
+ # when they first visit the confirmation page by clicking the link in their email.
+ def complete
+
+ signup_token = params[:signup_token]
+ user = User.find_by_signup_token(signup_token)
+
+ if user.nil?
+ return
+ end
+
+ user.updating_password = true
+
+ user.easy_save(
+ params[:first_name],
+ params[:last_name],
+ nil, # email can't be edited at this phase. We need to get them into the site, and they can edit on profile page if they really want
+ params[:password],
+ params[:password_confirmation],
+ true, # musician
+ params[:gender],
+ params[:birth_date],
+ params[:isp],
+ params[:city],
+ params[:state],
+ params[:country],
+ params[:instruments],
+ params[:photo_url])
+
+ if user.errors.any?
+ render :json => user.errors.full_messages(), :status => :unprocessable_entity
+ else
+ # log the user in automatically
+ user.signup_confirm
+ sign_in(user)
+ respond_with user, responder: ApiResponder, :status => 200
+ end
+ end
+
+ def delete
+ @user.destroy
+ respond_with responder: ApiResponder, :status => 204
+ end
+
+ def signup_confirm
+ @user = UserManager.new.signup_confirm(params[:signup_token])
+
+ unless @user.errors.any?
+ respond_with @user, responder: ApiResponder, :location => api_user_detail_url(@user)
+ else
+ response.status = :unprocessable_entity
+ respond_with @user, responder: ApiResponder
+ end
+ end
+
+ def set_password
+
+ @user.set_password(params[:old_password], params[:new_password], params[:new_password_confirm])
+
+ if @user.errors.any?
+ response.status = :unprocessable_entity
+ respond_with @user
+ else
+ sign_in(@user)
+ respond_with @user, responder: ApiResponder, status: 200
+ end
+ end
+
+ def reset_password
+ begin
+ User.reset_password(params[:email], ApplicationHelper.base_uri(request))
+ rescue JamRuby::JamArgumentError
+ render :json => { :message => ValidationMessages::EMAIL_NOT_FOUND }, :status => 403
+ end
+ respond_with responder: ApiResponder, :status => 204
+ end
+
+ def reset_password_token
+ begin
+ User.set_password_from_token(params[:email], params[:token], params[:new_password], params[:new_password_confirm])
+ rescue JamRuby::JamArgumentError
+ # FIXME
+ # There are some other errors that can happen here, besides just EMAIL_NOT_FOUND
+ render :json => { :message => ValidationMessages::EMAIL_NOT_FOUND }, :status => 403
+ end
+ set_remember_token(@user)
+ respond_with responder: ApiResponder, :status => 204
+ end
+
+ ###################### AUTHENTICATION ###################
+ def auth_session_create
+ @user = User.authenticate(params[:email], params[:password])
+
+ if @user.nil?
+ render :json => { :success => false }, :status => 404
+ else
+ sign_in @user
+ render :json => { :success => true }, :status => 200
+ end
+ end
+
+ def auth_session_delete
+ sign_out
+ render :json => { :success => true }, :status => 200
+ end
+
+ ###################### SESSION SETTINGS ###################
+ def session_settings_show
+ respond_with @user.my_session_settings, responder: ApiResponder
+ end
+
+ ###################### SESSION HISTORY ###################
+ def session_history_index
+ @session_history = @user.session_history(params[:id], params[:band_id], params[:genre])
+ end
+
+ def session_user_history_index
+ @session_user_history = @user.session_user_history(params[:id], params[:session_id])
+ end
+
+ ###################### BANDS ########################
+ def band_index
+ @bands = User.band_index(params[:id])
+ end
+
+ ###################### LIKERS ########################
+ def liker_index
+ # NOTE: liker_index.rabl template references the likers property
+ @user = User.find(params[:id])
+ end
+
+ ###################### LIKES #########################
+ def like_index
+ @user = User.find(params[:id])
+ end
+
+ def band_like_index
+ @user = User.find(params[:id])
+ end
+
+ def like_create
+ id = params[:id]
+
+ if !params[:user_id].nil?
+ User.create_user_like(params[:user_id], id)
+ respond_with @user, responder: ApiResponder, :location => api_user_like_index_url(@user)
+
+ elsif !params[:band_id].nil?
+ User.create_band_like(params[:band_id], id)
+ respond_with @user, responder: ApiResponder, :location => api_band_like_index_url(@user)
+ end
+ end
+
+ def like_destroy
+ if !params[:user_id].nil?
+ User.delete_like(params[:user_id], nil, params[:id])
+
+ elsif !params[:band_id].nil?
+ User.delete_like(nil, params[:band_id], params[:id])
+ end
+
+ respond_with responder: ApiResponder, :status => 204
+ end
+
+ ###################### FOLLOWERS ########################
+ def follower_index
+ # NOTE: follower_index.rabl template references the followers property
+ @user = User.find(params[:id])
+ end
+
+ ###################### FOLLOWINGS #######################
+ def following_index
+ @user = User.find(params[:id])
+ end
+
+ def following_show
+ @following = UserFollowing.find_by_user_id_and_follower_id(params[:user_id], params[:id])
+ end
+
+ def band_following_index
+ @user = User.find(params[:id])
+ end
+
+ def band_following_show
+ @following = BandFollowing.find_by_band_id_and_follower_id(params[:band_id], params[:id])
+ end
+
+ def following_create
+ id = params[:id]
+
+ if !params[:user_id].nil?
+ User.create_user_following(params[:user_id], id)
+ respond_with @user, responder: ApiResponder, :location => api_user_following_index_url(@user)
+
+ elsif !params[:band_id].nil?
+ User.create_band_following(params[:band_id], id)
+ respond_with @user, responder: ApiResponder, :location => api_band_following_index_url(@user)
+ end
+ end
+
+ def following_destroy
+ if !params[:user_id].nil?
+ User.delete_following(params[:user_id], nil, params[:id])
+
+ elsif !params[:band_id].nil?
+ User.delete_following(nil, params[:band_id], params[:id])
+ end
+
+ respond_with responder: ApiResponder, :status => 204
+ end
+
+ ###################### FAVORITES ########################
+ def favorite_index
+ @user = User.find(params[:id])
+ end
+
+ def favorite_create
+ @favorite = UserFavorite.new()
+ User.create_favorite(params[:id], params[:recording_id])
+
+ @user = User.find(params[:id])
+ respond_with @user, responder: ApiResponder, :location => api_favorite_index_url(@user)
+ end
+
+ def favorite_destroy
+ User.delete_favorite(params[:id], params[:recording_id])
+ respond_with responder: ApiResponder, :status => 204
+ end
+
+ ###################### FRIENDS ##########################
+ def friend_request_index
+ # get all outgoing and incoming friend requests
+ @friend_requests = FriendRequest.where("(friend_id='#{params[:id]}' AND status is null) OR user_id='#{params[:id]}'")
+ end
+
+ def friend_request_show
+ @friend_request = FriendRequest.find(params[:friend_request_id])
+ respond_with @friend_request, responder: ApiResponder, :status => 200
+ end
+
+ def friend_request_create
+ @friend_request = FriendRequest.save(nil,
+ params[:id],
+ params[:friend_id],
+ nil,
+ params[:message])
+
+ respond_with @friend_request, responder: ApiResponder, :status => 201, :location => api_friend_request_detail_url(@user, @friend_request)
+ end
+
+ def friend_request_update
+ @friend_request = FriendRequest.save(params[:friend_request_id],
+ params[:id],
+ params[:friend_id],
+ params[:status],
+ nil)
+ respond_with @friend_request, responder: ApiResponder, :status => 200
+ end
+
+ def friend_index
+ # NOTE: friend_index.rabl template references the friends property
+ @user = User.find(params[:id])
+ end
+
+ def friend_show
+ @friend = Friendship.find_by_user_id_and_friend_id(params[:id], params[:friend_id])
+ end
+
+ def friend_destroy
+ if current_user.id != params[:id] && current_user.id != params[:friend_id]
+ render :json => { :message => "You are not allowed to delete this friendship." }, :status => 403
+ end
+ # clean up both records representing this "friendship"
+ JamRuby::Friendship.delete_all "(user_id = '#{params[:id]}' AND friend_id = '#{params[:friend_id]}') OR (user_id = '#{params[:friend_id]}' AND friend_id = '#{params[:id]}')"
+ respond_with responder: ApiResponder, :status => 204
+ end
+
+ ###################### NOTIFICATIONS ####################
+ def notification_index
+ @notifications = @user.notifications
+ respond_with @notifications, responder: ApiResponder, :status => 200
+ end
+
+ def notification_destroy
+ Notification.delete(params[:notification_id])
+ respond_with responder: ApiResponder, :status => 204
+ end
+
+ ##################### BAND INVITATIONS ##################
+ def band_invitation_index
+ @invitations = @user.received_band_invitations
+ respond_with @invitations, responder: ApiResponder, :status => 200
+ end
+
+ def band_invitation_show
+ begin
+ @invitation = BandInvitation.find(params[:invitation_id])
+ respond_with @invitation, responder: ApiResponder, :status => 200
+
+ rescue ActiveRecord::RecordNotFound
+ render :json => { :message => ValidationMessages::BAND_INVITATION_NOT_FOUND }, :status => 404
+ end
+ end
+
+ def band_invitation_update
+ begin
+ @invitation = BandInvitation.save(params[:invitation_id],
+ nil,
+ nil,
+ nil,
+ params[:accepted])
+
+ respond_with @invitation, responder: ApiResponder, :status => 200
+
+ rescue ActiveRecord::RecordNotFound
+ render :json => { :message => ValidationMessages::BAND_INVITATION_NOT_FOUND }, :status => 404
+ end
+ end
+
+ ###################### ACCOUNT SETTINGS #################
+ def begin_update_email
+ # begins email update by sending an email for the user to confirm their new email
+
+ # NOTE: if you change confirm_email_link value below, you break outstanding email changes because links in user inboxes are broken
+ confirm_email_link = confirm_email_url + "?token="
+
+ current_user.begin_update_email(params[:update_email], params[:current_password], confirm_email_link)
+
+ if current_user.errors.any?
+ respond_with current_user, status: :unprocessable_entity
+ else
+ respond_with current_user, responder: ApiResponder, status: 200
+ end
+ end
+
+ def finalize_update_email
+ # used when the user goes to the confirmation link in their email
+ @user = User.finalize_update_email(params[:token])
+
+ sign_in(@user)
+
+ respond_with current_user, responder: ApiResponder, status: 200
+ end
+
+ def isp_scoring
+ if request.post?
+ data = request.body.read
+ User.connection.execute("INSERT INTO isp_score_batch(json_scoring_data) VALUES ('#{data}')")
+ render :text => 'scoring recorded'
+ return
+ end
+ render :nothing => true
+ end
+
+ ################# AVATAR #####################
+
+ def update_avatar
+ original_fpfile = params[:original_fpfile]
+ cropped_fpfile = params[:cropped_fpfile]
+ crop_selection = params[:crop_selection]
+
+ # public bucket to allow images to be available to public
+ @user.update_avatar(original_fpfile, cropped_fpfile, crop_selection, Rails.application.config.aws_bucket_public)
+
+ if @user.errors.any?
+ respond_with @user, status: :unprocessable_entity
+ else
+ respond_with @user, responder: ApiResponder, status: 200
+ end
+ end
+
+ def delete_avatar
+ @user.delete_avatar(Rails.application.config.aws_bucket_public)
+
+ if @user.errors.any?
+ respond_with @user, status: :unprocessable_entity
+ else
+ respond_with @user, responder: ApiResponder, status: 204
+ end
+ end
+
+ def generate_filepicker_policy
+ # generates a soon-expiring filepicker policy so that a user can only upload to their own folder in their bucket
+
+ handle = params[:handle]
+
+ call = 'pick,convert,store'
+
+ policy = { :expiry => (DateTime.now + 5.minutes).to_i(),
+ :call => call,
+ #:path => 'avatars/' + @user.id + '/.*jpg'
+ }
+
+ # if the caller specifies a handle, add it to the hash
+ unless handle.nil?
+ start = handle.rindex('/') + 1
+ policy[:handle] = handle[start..-1]
+ end
+
+ policy = Base64.urlsafe_encode64( policy.to_json )
+ digest = OpenSSL::Digest::Digest.new('sha256')
+ signature = OpenSSL::HMAC.hexdigest(digest, Rails.application.config.fp_secret, policy)
+
+ render :json => {
+ :signature => signature,
+ :policy => policy
+ }, :status => :ok
+ end
+
+
+ ###################### CRASH DUMPS #######################
+
+ # This is very similar to api_music_sessions#perf_upload
+ # This should largely be moved into a library somewhere in jam-ruby.
+ def crash_dump
+ # example of using curl to access this API:
+ # curl -L -T some_file -X PUT http://localhost:3000/api/users/dump.json?client_type=[MACOSX/Win32/JamBox]&client_version=[VERSION]&client_id=[CLIENT_ID]&session_id=[SESSION_ID]×tamp=[TIMESTAMP]
+ # user_id is deduced if possible from the user's cookie.
+ @dump = CrashDump.new
+
+ @dump.client_type = params[:client_type]
+ @dump.client_version = params[:client_version]
+ @dump.client_id = params[:client_id]
+ @dump.user_id = current_user
+ @dump.session_id = params[:session_id]
+ @dump.timestamp = params[:timestamp]
+
+ unless @dump.save
+ # There are at least some conditions on valid dumps (need client_type)
+ response.status = :unprocessable_entity
+ respond_with @dump
+ return
+ end
+
+ # This part is the piece that really needs to be decomposed into a library...
+ if SampleApp::Application.config.storage_type == :fog
+ s3 = AWS::S3.new(:access_key_id => SampleApp::Application.config.aws_access_key_id,
+ :secret_access_key => SampleApp::Application.config.aws_secret_access_key)
+ # Fixme: Should we use the same bucket for everything?
+ bucket = s3.buckets[SampleApp::Application.config.aws_bucket]
+ url = bucket.objects[@dump.uri].url_for(:write, :expires => SampleApp::Application.config.crash_dump_data_signed_url_timeout, :'response_content_type' => 'application/octet-stream').to_s
+
+ logger.debug("crash_dump can upload to url #{url}")
+
+ redirect_to url
+ else
+ # we should store it here to aid in development, but we don't have to until someone wants the feature
+ # so... just return 200
+ render :json => { :id => @dump.id }, :status => 200
+ end
+
+ end
+
+ ###################### RECORDINGS #######################
+ # def recording_index
+ # @recordings = User.recording_index(current_user, params[:id])
+ # respond_with @recordings, responder: ApiResponder, :status => 200
+ # end
+
+ # def recording_show
+ # hide_private = false
+
+ # # hide private recordings from anyone but the current user
+ # if current_user.id != params[:id]
+ # hide_private = true
+ # end
+
+ # @recording = Recording.find(params[:recording_id])
+ # if !@recording.public && hide_private
+ # render :json => { :message => "You are not allowed to access this recording." }, :status => 403
+ # #respond_with "You are not allowed to access this recording.", responder: ApiResponder, :status => 403
+ # else
+ # respond_with @recording, responder: ApiResponder, :status => 200
+ # end
+ # end
+
+ # def recording_create
+ # @recording = Recording.save(params[:recording_id],
+ # params[:public],
+ # params[:description],
+ # params[:genres],
+ # current_user.id,
+ # params[:id],
+ # false)
+
+ # @user = current_user
+ # respond_with @recording, responder: ApiResponder, :status => 201, :location => api_recording_detail_url(@user, @recording)
+ # end
+
+ # def recording_update
+ # @recording = Recording.save(params[:recording_id],
+ # params[:public],
+ # params[:description],
+ # params[:genres],
+ # current_user.id,
+ # params[:id],
+ # false)
+
+ # respond_with @recording, responder: ApiResponder, :status => 200
+ # end
+
+ # def recording_destroy
+ # @recording = Recording.find(params[:recording_id])
+ # @recording.delete
+ # respond_with responder: ApiResponder, :status => 204
+ # end
+end
diff --git a/web/app/controllers/application_controller.rb b/web/app/controllers/application_controller.rb
new file mode 100644
index 000000000..6d4bf3c1e
--- /dev/null
+++ b/web/app/controllers/application_controller.rb
@@ -0,0 +1,4 @@
+class ApplicationController < ActionController::Base
+ protect_from_forgery
+ include SessionsHelper
+end
diff --git a/web/app/controllers/artifacts_controller.rb b/web/app/controllers/artifacts_controller.rb
new file mode 100644
index 000000000..45c74c832
--- /dev/null
+++ b/web/app/controllers/artifacts_controller.rb
@@ -0,0 +1,64 @@
+class ArtifactsController < ApiController
+
+ respond_to :json
+
+ # retrieve all available client downloads
+ def client_downloads
+ clients = ArtifactUpdate.where("product like '%JamClient%' and environment = '#{ArtifactUpdate::DEFAULT_ENVIRONMENT}'").order(:product)
+
+ result = {}
+
+ clients.each do |client|
+ url = determine_url(client)
+ result[client.product] = { :uri => url, :size => client.size }
+ end
+ render :json => result, :status => :ok
+ end
+
+
+ def versioncheck
+
+ # the reported client version
+ #client_version = params[:ver]
+
+ # the name of the client, i.e.JamClient
+ product = params[:product]
+
+ # the os (Win32/MacOSX/Unix)
+ os = params[:os]
+
+ product = "#{product}/#{os}"
+
+ logger.debug "version check from #{product}"
+
+
+
+ unless ArtifactUpdate::PRODUCTS.include? product
+ render :json => { :errors => { :product => ['not a valid product'] } }, :status => :unprocessable_entity
+ return
+ end
+
+ @artifact = ArtifactUpdate.find_by_product_and_environment(product, ArtifactUpdate::DEFAULT_ENVIRONMENT)
+
+ if @artifact.nil?
+ render :json => {}, :status => :ok
+ else
+ url = determine_url(@artifact)
+
+ render :json => { "version" => @artifact.version, "uri" => url, "sha1" => @artifact.sha1, "size" => @artifact.size }, :status => :ok
+ end
+
+ end
+
+ def determine_url(artifact)
+ if SampleApp::Application.config.storage_type == :file
+ # this is basically a dev-time only path of code; we store real artifacts in s3
+ url = SampleApp::Application.config.jam_admin_root_url + artifact.uri.url
+ else
+ url = artifact.uri.url
+ end
+
+ return url
+ end
+
+end
diff --git a/web/app/controllers/clients_controller.rb b/web/app/controllers/clients_controller.rb
new file mode 100644
index 000000000..043c1efd7
--- /dev/null
+++ b/web/app/controllers/clients_controller.rb
@@ -0,0 +1,31 @@
+class ClientsController < ApplicationController
+
+ include UsersHelper
+
+ def index
+ # use gon to pass variables into javascript
+ gon.websocket_gateway_uri = Rails.application.config.websocket_gateway_uri
+ gon.check_for_client_updates = Rails.application.config.check_for_client_updates
+ gon.fp_apikey = Rails.application.config.filepicker_rails.api_key
+ gon.fp_upload_dir = Rails.application.config.filepicker_upload_dir
+ gon.allow_force_native_client = Rails.application.config.allow_force_native_client
+
+ # is this the native client or browser?
+ user_agent = request.env["HTTP_USER_AGENT"]
+ @nativeClient = !user_agent.blank? && user_agent.downcase.include?("jamkazam")
+
+ # allow override of the client type if configured to so, and if we find the override cookie in place
+ if Rails.application.config.allow_force_native_client
+ unless cookies[:act_as_native_client].nil?
+ @nativeClient = (cookies[:act_as_native_client] == "true") ? true : false
+ end
+ end
+
+ if current_user
+ render :layout => 'client'
+ else
+ redirect_to "/signin"
+ end
+ end
+
+end
diff --git a/web/app/controllers/corps_controller.rb b/web/app/controllers/corps_controller.rb
new file mode 100644
index 000000000..3a8025521
--- /dev/null
+++ b/web/app/controllers/corps_controller.rb
@@ -0,0 +1,40 @@
+class CorpsController < ApplicationController
+
+ layout "corporate"
+
+ def about
+
+ end
+
+ def contact
+
+ end
+
+ def help
+
+ end
+
+ def media_center
+
+ end
+
+ def news
+
+ end
+
+ def privacy
+
+ end
+
+ def terms
+
+ end
+
+ def cookie_policy
+
+ end
+
+ def premium_accounts_path
+
+ end
+end
\ No newline at end of file
diff --git a/web/app/controllers/music_sessions_controller.rb b/web/app/controllers/music_sessions_controller.rb
new file mode 100644
index 000000000..c72f6f22f
--- /dev/null
+++ b/web/app/controllers/music_sessions_controller.rb
@@ -0,0 +1,50 @@
+class MusicSessionsController < ApplicationController
+
+ # have to be signed in currently to see this screen
+ before_filter :signed_in_user
+
+ respond_to :html
+
+ def index
+ @music_sessions = MusicSession.paginate(page: params[:page])
+ end
+
+ def show
+ @music_session = MusicSession.find(params[:id])
+
+ # use gon to pass variables into javascript
+ gon.websocket_gateway_uri = Rails.application.config.websocket_gateway_uri
+ gon.music_session_id = @music_session.id
+ end
+
+ def new
+ @music_session = MusicSession.new
+ end
+
+ def create
+ @music_session = MusicSession.new()
+ @music_session.creator = current_user
+ @music_session.description = params[:jam_ruby_music_session][:description]
+ if @music_session.save
+ flash[:success] = "Music Session created"
+ redirect_to @music_session
+ else
+ render 'new'
+ end
+ end
+
+
+ def edit
+ end
+
+ def update
+
+ end
+
+ def destroy
+ MusicSession.find(params[:id]).destroy
+ flash[:success] = "Jam Session deleted."
+ redirect_to music_sessions_url
+ end
+
+end
diff --git a/web/app/controllers/sessions_controller.rb b/web/app/controllers/sessions_controller.rb
new file mode 100644
index 000000000..c516b01d3
--- /dev/null
+++ b/web/app/controllers/sessions_controller.rb
@@ -0,0 +1,127 @@
+# this is not a jam session - this is an 'auth session'
+class SessionsController < ApplicationController
+
+ def new
+ @login_error = false
+ render :layout => "landing"
+ end
+
+ def create
+ user = User.authenticate(params[:session][:email], params[:session][:password])
+
+ if user.nil?
+ @login_error = true
+ render 'new', :layout => "landing"
+ else
+ @session_only_cookie = !jkclient_agent? && !params[:user].nil? && 0 == params[:user][:remember_me].to_i
+ complete_sign_in user
+ end
+ end
+
+# OAuth docs
+# http://net.tutsplus.com/tutorials/ruby/how-to-use-omniauth-to-authenticate-your-users/
+ def create_oauth
+ auth_hash = request.env['omniauth.auth']
+ authorization = UserAuthorization.find_by_provider_and_uid(auth_hash["provider"], auth_hash["uid"])
+ if authorization
+ # Sign in for a user who has already registered.
+ complete_sign_in authorization.user
+ else
+ # Sign up for a completely new user.
+ # First/last name: auth_hash["info"]["first_name"] and auth_hash["info"]["last_name"]
+ # token: auth_hash["credentials"]["token"] -- "expires_at"
+ #
+ # For debugging - to see what all is there:
+ # render :text => auth_hash.to_yaml
+ #FbGraph.debug!
+ #app = FbGraph::Application.new '468555793186398', :secret => '546a5b253972f3e2e8b36d9a3dd5a06e'
+ token = auth_hash[:credentials][:token]
+
+ # FIXME:
+ # This should probably be in a transaction somehow, meaning the user
+ # create and the authorization create. Concern is UserManager.new.signup sends
+ # an email and whatnot.
+ #
+ # Also, should we grab their photo from facebook?
+ user = UserManager.new.signup(request.remote_ip,
+ auth_hash[:info][:first_name],
+ auth_hash[:info][:last_name],
+ auth_hash[:info][:email],
+ nil,
+ nil,
+ nil, # instruments
+ nil, # photo_url
+ nil)
+
+ # Users who sign up using oauth are presumed to have valid email adddresses.
+ user.confirm_email!
+
+ auth = user.user_authorizations.build :provider => auth_hash[:provider],
+ :uid => auth_hash[:uid],
+ :token => auth_hash[:credentials][:token],
+ :token_expiration => Time.at(auth_hash[:credentials][:expires_at])
+ user.save
+ complete_sign_in user
+ end
+ end
+
+
+ def oauth_callback
+ if current_user.nil?
+ render :nothing => true, :status => 404
+ return
+ end
+
+ auth_hash = request.env['omniauth.auth']
+ #authorization = UserAuthorization.find_by_provider_and_uid(auth_hash["provider"], auth_hash["uid"])
+
+ # Always make and save a new authorization. This is because they expire, and honestly there's no cost
+ # to just making and saving it.
+ #if authorization.nil?
+ authorization = current_user.user_authorizations.build :provider => auth_hash[:provider],
+ :uid => auth_hash[:uid],
+ :token => auth_hash[:credentials][:token],
+ :token_expiration => Time.at(auth_hash[:credentials][:expires_at])
+ authorization.save
+ #end
+
+ render 'oauth_complete', :layout => "landing"
+ end
+
+ def complete_sign_in(user)
+ sign_in user
+
+ if !params[:sso].nil? && params[:sso] == "desk"
+ # generate multipass token and sign it
+ multipass = DeskMultipass.new(user)
+ callback_url = SampleApp::Application.config.multipass_callback_url
+ redirect_to "#{callback_url}?multipass=#{multipass.token}&signature=#{multipass.signature}"
+ else
+ redirect_back_or client_url
+ end
+ end
+
+ def destroy
+ # earlier, code here would delete the connection using client_id from cookies
+ # however, we should never try to delete the client_id cookie (make it as permanent as possible)
+ # also, because the client will stop heartbeating and close the connection to gateway,
+ # in any case the server will notice after 10 seconds that the user is gone.
+ # if we really want someone to know right away that the client is gone, then just make sure the client calls
+ # leave session before it calls delete (VRFS-617 should solve that)
+ sign_out
+ redirect_to client_url
+ end
+
+ def failure
+
+ end
+
+ def connection_state
+ if (defined?(TEST_CONNECT_STATES) && TEST_CONNECT_STATES) || 'development'==Rails.env
+ @prefix = defined?(TEST_CONNECT_STATE_JS_LOG_PREFIX) ? TEST_CONNECT_STATE_JS_LOG_PREFIX : '*** '
+ render('connection_state', :layout => 'client') && return
+ end
+ render :nothing => true, :status => 404
+ end
+
+end
diff --git a/web/app/controllers/spikes_controller.rb b/web/app/controllers/spikes_controller.rb
new file mode 100644
index 000000000..0dde61524
--- /dev/null
+++ b/web/app/controllers/spikes_controller.rb
@@ -0,0 +1,39 @@
+class SpikesController < ApplicationController
+
+ def facebook_invite
+
+ end
+
+
+ def gmail_contacts
+ if current_user.nil?
+ render :nothing => true, :status => 404
+ return
+ end
+ authorization = current_user.user_authorizations.where(:provider => 'google_login')
+ if authorization.empty?
+ render :nothing => true, :status => 404
+ return
+ end
+ token = authorization.first.token
+ uri = URI.parse("https://www.google.com/m8/feeds/contacts/default/full?oauth_token=#{token}&max-results=50000&alt=json")
+
+ http = Net::HTTP.new(uri.host, uri.port)
+ http.use_ssl = true
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
+ request = Net::HTTP::Get.new(uri.request_uri)
+ response = http.request(request)
+ contacts = ActiveSupport::JSON.decode(response.body)
+ ret_contacts = []
+ contacts['feed']['entry'].each_with_index do |contact,index|
+ name = contact['title']['$t']
+ contact['gd$email'].to_a.each do |email|
+ email_address = email['address']
+ ret_contacts.push(email_address)
+ end
+ end
+
+ render :json => ret_contacts
+ end
+
+end
diff --git a/web/app/controllers/static_pages_controller.rb b/web/app/controllers/static_pages_controller.rb
new file mode 100644
index 000000000..c0a7151e1
--- /dev/null
+++ b/web/app/controllers/static_pages_controller.rb
@@ -0,0 +1,17 @@
+class StaticPagesController < ApplicationController
+
+ def home
+ if signed_in?
+ # current_user is reference to the current user... use it to ask stuff about the user and show something
+ end
+ end
+
+ def help
+ end
+
+ def about
+ end
+
+ def contact
+ end
+end
diff --git a/web/app/controllers/users_controller.rb b/web/app/controllers/users_controller.rb
new file mode 100644
index 000000000..75d60e7cc
--- /dev/null
+++ b/web/app/controllers/users_controller.rb
@@ -0,0 +1,328 @@
+# -*- coding: utf-8 -*-
+
+require 'builder'
+
+class UsersController < ApplicationController
+ before_filter :signed_in_user,
+ only: [:index, :edit, :update, :destroy]
+ before_filter :correct_user, only: [:edit, :update]
+ before_filter :admin_user, only: :destroy
+
+ rescue_from 'JamRuby::PermissionError' do |exception|
+ @exception = exception
+ render :file => 'public/403.html', :status => 403, :layout => false
+ end
+
+ def index
+ @users = User.paginate(page: params[:page])
+ end
+
+ def show
+ @user = User.find(params[:id])
+ end
+
+ def new
+ @invited_user = load_invited_user(params)
+
+ if !@invited_user.nil? && @invited_user.accepted
+ # short-circuit out if this invitation is already accepted
+ render "already_signed_up", :layout => 'landing'
+ return
+ end
+ @signup_postback = load_postback(@invited_user)
+ load_location(request.remote_ip)
+
+ @user = User.new
+ @user.musician = true # default the UI to musician as selected option
+
+ # preseed the form with the invited email as a convenience to the user
+ unless @invited_user.nil?
+ @user.email = @invited_user.email
+ end
+
+ render :layout => 'landing'
+ end
+
+ def create
+
+ @invited_user = load_invited_user(params)
+ @signup_postback = load_postback(@invited_user)
+
+ @user = User.new
+
+ # check recaptcha; if any errors seen, contribute it to the model
+ unless verify_recaptcha(:model => @user, :message => "recaptcha")
+ render 'new', :layout => 'landing'
+ return
+ end
+
+ instruments = fixup_instruments(params[:jam_ruby_user][:instruments])
+
+ birth_date = fixup_birthday(params[:jam_ruby_user]["birth_date(2i)"], params[:jam_ruby_user]["birth_date(3i)"], params[:jam_ruby_user]["birth_date(1i)"])
+ location = { :country => params[:jam_ruby_user][:country], :state => params[:jam_ruby_user][:state], :city => params[:jam_ruby_user][:city]}
+ terms_of_service = params[:jam_ruby_user][:terms_of_service].nil? ? false : true
+ subscribe_email = params[:jam_ruby_user][:subscribe_email].nil? ? false : true
+ musician = params[:jam_ruby_user][:musician]
+
+
+ @user = UserManager.new.signup(request.remote_ip,
+ params[:jam_ruby_user][:first_name],
+ params[:jam_ruby_user][:last_name],
+ params[:jam_ruby_user][:email],
+ params[:jam_ruby_user][:password],
+ params[:jam_ruby_user][:password_confirmation],
+ terms_of_service,
+ subscribe_email,
+ instruments,
+ birth_date,
+ location,
+ musician,
+ nil, # we don't accept photo url on the signup form yet
+ @invited_user,
+ ApplicationHelper.base_uri(request) + "/confirm")
+
+ # check for errors
+ if @user.errors.any?
+ # render any @user.errors on error
+ load_location(request.remote_ip, location)
+ gon.signup_errors = true
+ gon.musician_instruments = instruments
+ render 'new', :layout => 'landing'
+ else
+ sign_in @user
+
+ if @user.musician
+ redirect_to :congratulations_musician
+ else
+ redirect_to :congratulations_fan
+ end
+
+ end
+ end
+
+ def congratulations_fan
+ render :layout => "landing"
+ end
+
+ def congratulations_musician
+ render :layout => "landing"
+ end
+
+ def signup_confirm
+ signup_token = params[:signup_token]
+
+ @user = UserManager.new.signup_confirm(signup_token, request.remote_ip)
+
+ if !@user.nil? && !@user.errors.any?
+ sign_in @user
+ redirect_to :client
+ elsif !@user.nil?
+ # new user with validation errors;
+ logger.debug("#{@user} has errors. can not sign in until remedied. #{@user.errors.inspect}")
+
+ end
+ # let page have signup_token in javascript
+ gon.signup_token = signup_token
+
+ # let errors fall through to signup_confirm.html.erb
+ end
+
+ def edit
+ end
+
+ def update
+ if @user.update_attributes(params[:jam_ruby_user])
+ flash[:success] = "Profile updated"
+ sign_in @user
+ redirect_to @user
+ else
+ render 'edit'
+ end
+ end
+
+ def destroy
+ User.find(params[:id]).destroy
+ flash[:success] = "User destroyed."
+ redirect_to users_url
+ end
+
+ def request_reset_password
+ render 'request_reset_password', :layout => 'landing'
+ end
+
+ def reset_password
+ begin
+ @reset_password_email = params[:jam_ruby_user][:email]
+
+ if @reset_password_email.empty?
+ @reset_password_error = "Please enter an email address above"
+ render 'request_reset_password', :layout => 'landing'
+ return
+ end
+
+ @user = User.reset_password(@reset_password_email, ApplicationHelper.base_uri(request))
+ render 'sent_reset_password', :layout => 'landing'
+ rescue JamRuby::JamArgumentError
+ # Dont tell the user if this error occurred to prevent scraping email addresses.
+ #@reset_password_error = "Email address not found"
+ render 'sent_reset_password', :layout => 'landing'
+ end
+ end
+
+ def reset_password_token
+ render 'reset_password_token', :layout => 'landing'
+ end
+
+ def reset_password_complete
+ begin
+ User.set_password_from_token(params[:jam_ruby_user][:email], params[:jam_ruby_user][:token],
+ params[:jam_ruby_user][:password], params[:jam_ruby_user][:password_confirmation])
+ render 'reset_password_complete', :layout => 'landing'
+ rescue JamRuby::JamArgumentError
+ @password_error = "Entries don't match or are too short"
+ params[:email] = params[:jam_ruby_user][:email]
+ params[:token] = params[:jam_ruby_user][:token]
+ render 'reset_password_token', :layout => 'landing'
+ end
+ end
+
+ def finalize_update_email
+ # this corresponds to when the user clink a link in their new email address to configure they want to use it,
+ # and verify their new address is real
+ token = params[:token]
+
+ gon.ensure = true
+ gon.update_email_token = token
+
+ render :layout => 'landing'
+ end
+
+ def jnlp
+ headers["Content-Type"] = "application/x-java-jnlp-file"
+ headers["Cache-Control"] = "public"
+ headers["Content-Disposition"] = "attachment;filename='ping#{params[:isp]}.jnlp'"
+ jnlp = ''
+ xml = Builder::XmlMarkup.new(:indent => 2, :target => jnlp)
+ xml.instruct!
+ jnlpurl = isp_ping_url(:isp => params[:isp],
+ :format => :jnlp,
+ :host => 'www.jamkazam.com',
+ :port => '80')
+ xml.jnlp(:spec => '1.0+',
+ :href => jnlpurl,
+ :codebase => "http://www.jamkazam.com/isp") do
+ xml.information do
+ xml.title 'Ping'
+ xml.vendor 'JamKazam'
+ end
+ xml.resources do
+ xml.j2se(:version => "1.6+", :href => "http://java.sun.com/products/autodl/j2se")
+ xml.jar(:href => 'http://www.jamkazam.com/isp/ping.jar', :main => 'true')
+ end
+ xml.tag!('application-desc',
+ :name => "Ping",
+ 'main-class' => "com.jamkazam.ping.Ping",
+ :width => "400",
+ :height => "600") do
+ xml.comment!('usage: Ping [label=]addr[:port] ... [-c ] [-s ] -u -i [-a]')
+ xml.argument('foo=etch.dyndns.org:4442')
+ xml.argument('bar=etch.dyndns.org:4442')
+ xml.argument("-uhttp://www.jamkazam.com#{isp_scoring_path}")
+ xml.argument("-i#{params[:isp]}")
+ xml.argument('-a')
+ end
+ xml.update(:check => 'background')
+ end
+ send_data jnlp, :type=>"application/x-java-jnlp-file"
+ end
+
+ def isp
+ @isps = {
+ 'tw' => ['Time Warner', 'tw.jpg'],
+ 'vz' => ['Verizon', 'vz.png'],
+ 'att' => ['AT&T', 'att.png'],
+ 'cc' => ['Comcast', 'cc.png'],
+ 'other' => ['Other', 'other.jpg']
+ }
+ render :layout => "landing"
+ end
+
+ private
+
+ def correct_user
+ @user = User.find(params[:id])
+ redirect_to(root_url) unless current_user?(@user)
+ end
+
+ def admin_user
+ redirect_to(root_url) unless current_user.admin?
+ end
+
+ # the User Model expects instruments in a different format than the form submits it
+ # so we have to fix it up.
+ def fixup_instruments(original_instruments)
+ # if an instrument is selected by the user in the form, it'll show up in this array
+ instruments = []
+
+ # ok, sweep through all the fields submitted, looking for selected instruments.
+ # also, make up priority because we don't ask for it (but users can fix it later on their profile)
+ priority = 0
+ unless original_instruments == nil
+ original_instruments.each do |key, value|
+ if !value["selected"].nil?
+ instruments << { :instrument_id => key, :proficiency_level => value["proficiency"].to_i, :priority => priority }
+ priority = priority + 1
+ end
+ end
+ end
+
+ return instruments
+ end
+
+ # the User Model expects instruments in a different format than the form submits it
+ # so we have to fix it up.
+ def fixup_birthday(month, day, year)
+ if month.blank? || day.blank? || year.blank?
+ # invalid birthdate, so return nil
+ return nil
+ end
+
+ return Date.new(year.to_i, month.to_i, day.to_i)
+ end
+
+ def load_invited_user(params)
+ # check if this an anonymous request, or result of invitation code
+ invitation_code = params[:invitation_code]
+
+ invited_user = nil
+ unless invitation_code.nil?
+ # we only want to find invitations that have not been accepted
+ invited_user = InvitedUser.find_by_invitation_code(invitation_code)
+ end
+ return invited_user
+ end
+
+ def load_location(remote_ip, location = nil)
+ @location = location
+
+ if @location.nil?
+ @location = MaxMindManager.lookup(remote_ip)
+ end
+
+ @location[:country] = "US" if @location[:country].nil?
+
+ # right now we only accept US signups for beta
+ @countries = MaxMindManager.countries()
+ # populate regions based on current country
+ @regions = MaxMindManager.regions(@location[:country])
+ @cities = @location[:state].nil? ? [] : MaxMindManager.cities(@location[:country], @location[:state])
+ end
+
+ def load_postback(invited_user)
+ if invited_user.nil?
+ signup_path
+ else
+ signup_path + "?invitation_code=" + invited_user.invitation_code
+ end
+ end
+end
diff --git a/web/app/helpers/application_helper.rb b/web/app/helpers/application_helper.rb
new file mode 100644
index 000000000..3330b45ab
--- /dev/null
+++ b/web/app/helpers/application_helper.rb
@@ -0,0 +1,16 @@
+module ApplicationHelper
+
+ # Returns the full title on a per-page basis.
+ def full_title(page_title)
+ base_title = "JamKazam"
+ if page_title.empty?
+ base_title
+ else
+ "#{base_title} | #{page_title}"
+ end
+ end
+
+ def self.base_uri(request)
+ (request.ssl? ? "https://" : "http://") + request.host_with_port
+ end
+end
diff --git a/web/app/helpers/meta_helper.rb b/web/app/helpers/meta_helper.rb
new file mode 100644
index 000000000..0ba8cb259
--- /dev/null
+++ b/web/app/helpers/meta_helper.rb
@@ -0,0 +1,7 @@
+module MetaHelper
+
+ def version()
+ "web=#{::JamWeb::VERSION} lib=#{JamRuby::VERSION} db=#{JamDb::VERSION} pb=#{Jampb::VERSION}"
+ end
+
+end
diff --git a/web/app/helpers/sessions_helper.rb b/web/app/helpers/sessions_helper.rb
new file mode 100644
index 000000000..72e8618a6
--- /dev/null
+++ b/web/app/helpers/sessions_helper.rb
@@ -0,0 +1,68 @@
+module SessionsHelper
+
+ def sign_in(user)
+ set_remember_token(user)
+ self.current_user = user
+ end
+
+ def set_remember_token(user)
+ if @session_only_cookie
+ cookies.delete(:remember_token)
+ cookies[:remember_token] = user.remember_token
+ else
+ cookies[:remember_token] = {
+ :value => user.remember_token,
+ :expires => 20.years.from_now.utc
+ }
+ end
+ end
+
+ def signed_in?
+ !current_user.nil?
+ end
+
+ def current_user=(user)
+ @current_user = user
+ end
+
+ def current_user
+ @current_user ||= User.find_by_remember_token(cookies[:remember_token])
+ end
+
+ def current_user?(user)
+ user == current_user
+ end
+
+ def signed_in_user
+ unless signed_in?
+ store_location
+ redirect_to signin_url, notice: "Please sign in."
+ end
+ end
+
+
+ def api_signed_in_user
+ unless signed_in?
+ render :json => {}, :status => 403
+ end
+ end
+
+ def sign_out
+ current_user = nil
+ cookies.delete(:remember_token)
+ end
+
+ def redirect_back_or(default)
+ redirect_to(session[:return_to] || default)
+ session.delete(:return_to)
+ end
+
+ def store_location
+ session[:return_to] = request.url
+ end
+
+ def jkclient_agent?
+ request.env['HTTP_USER_AGENT'] =~ /JamKazam/
+ end
+
+end
diff --git a/web/app/helpers/static_pages_helper.rb b/web/app/helpers/static_pages_helper.rb
new file mode 100644
index 000000000..2d63e79e6
--- /dev/null
+++ b/web/app/helpers/static_pages_helper.rb
@@ -0,0 +1,2 @@
+module StaticPagesHelper
+end
diff --git a/web/app/helpers/users_helper.rb b/web/app/helpers/users_helper.rb
new file mode 100644
index 000000000..b3edd0a32
--- /dev/null
+++ b/web/app/helpers/users_helper.rb
@@ -0,0 +1,12 @@
+module UsersHelper
+
+ # Returns the Gravatar (http://gravatar.com/) for the given user.
+ def gravatar_for(user, options = { size: 50, hclass: "gravatar" })
+ gravatar_id = Digest::MD5::hexdigest(user.email.downcase)
+ size = options[:size]
+ hclass = options[:hclass]
+ gravatar =
+ gravatar_url = "https://secure.gravatar.com/avatar/#{gravatar_id}?s=#{size}"
+ image_tag(gravatar_url, alt: "#{user.first_name} #{user.last_name}", class: "#{hclass}")
+ end
+end
diff --git a/web/app/mailers/.gitkeep b/web/app/mailers/.gitkeep
new file mode 100644
index 000000000..e69de29bb
diff --git a/web/app/responders/api_responder.rb b/web/app/responders/api_responder.rb
new file mode 100644
index 000000000..64df5fd69
--- /dev/null
+++ b/web/app/responders/api_responder.rb
@@ -0,0 +1,16 @@
+class ApiResponder < ActionController::Responder
+ def to_format
+ case
+ when has_errors?
+ logger.debug("REST API entity has error: #{resource.errors.inspect}")
+ controller.response.status = :unprocessable_entity
+ when post?
+ logger.debug("REST API post")
+ controller.response.status = :created
+ end
+
+ default_render
+ rescue ActionView::MissingTemplate => e
+ api_behavior(e)
+ end
+end
diff --git a/web/app/views/api_bands/create.rabl b/web/app/views/api_bands/create.rabl
new file mode 100644
index 000000000..86df71066
--- /dev/null
+++ b/web/app/views/api_bands/create.rabl
@@ -0,0 +1,3 @@
+object @band
+
+extends "api_bands/show"
\ No newline at end of file
diff --git a/web/app/views/api_bands/follower_index.rabl b/web/app/views/api_bands/follower_index.rabl
new file mode 100644
index 000000000..5cb70590c
--- /dev/null
+++ b/web/app/views/api_bands/follower_index.rabl
@@ -0,0 +1,21 @@
+collection @band.followers
+
+node :user_id do |follower|
+ follower.id
+end
+
+node :name do |follower|
+ follower.name
+end
+
+node :location do |follower|
+ follower.location
+end
+
+node :musician do |follower|
+ follower.musician
+end
+
+node :photo_url do |follower|
+ follower.photo_url
+end
diff --git a/web/app/views/api_bands/index.rabl b/web/app/views/api_bands/index.rabl
new file mode 100644
index 000000000..74531e3ae
--- /dev/null
+++ b/web/app/views/api_bands/index.rabl
@@ -0,0 +1,4 @@
+collection @bands
+
+# do not retrieve all child collections when showing a list of bands
+attributes :id, :name, :city, :state, :country, :photo_url, :logo_url
\ No newline at end of file
diff --git a/web/app/views/api_bands/invitation_create.rabl b/web/app/views/api_bands/invitation_create.rabl
new file mode 100644
index 000000000..1cc5c9bde
--- /dev/null
+++ b/web/app/views/api_bands/invitation_create.rabl
@@ -0,0 +1,3 @@
+object @invitation
+
+extends "api_bands/invitation_show"
\ No newline at end of file
diff --git a/web/app/views/api_bands/invitation_index.rabl b/web/app/views/api_bands/invitation_index.rabl
new file mode 100644
index 000000000..2e12df14a
--- /dev/null
+++ b/web/app/views/api_bands/invitation_index.rabl
@@ -0,0 +1,3 @@
+object @invitations
+
+extends "api_bands/invitation_show"
\ No newline at end of file
diff --git a/web/app/views/api_bands/invitation_show.rabl b/web/app/views/api_bands/invitation_show.rabl
new file mode 100644
index 000000000..ffb49a994
--- /dev/null
+++ b/web/app/views/api_bands/invitation_show.rabl
@@ -0,0 +1,15 @@
+object @invitation
+
+attributes :id, :accepted
+
+node :sender do |i|
+ { :id => i.sender.id, :name => i.sender.name }
+end
+
+node :recipient do |i|
+ { :id => i.receiver.id, :name => i.receiver.name }
+end
+
+node :band do |i|
+ { :id => i.band.id, :name => i.band.name }
+end
\ No newline at end of file
diff --git a/web/app/views/api_bands/liker_index.rabl b/web/app/views/api_bands/liker_index.rabl
new file mode 100644
index 000000000..9750b6e33
--- /dev/null
+++ b/web/app/views/api_bands/liker_index.rabl
@@ -0,0 +1,31 @@
+object @band.likers
+
+attributes :liker_id => :user_id
+
+node :first_name do |liker|
+ liker.user.first_name
+end
+
+node :last_name do |liker|
+ liker.user.last_name
+end
+
+node :city do |liker|
+ liker.user.city
+end
+
+node :state do |liker|
+ liker.user.state
+end
+
+node :country do |liker|
+ liker.user.country
+end
+
+node :musician do |liker|
+ liker.user.musician
+end
+
+node :photo_url do |liker|
+ liker.user.photo_url
+end
\ No newline at end of file
diff --git a/web/app/views/api_bands/recording_create.rabl b/web/app/views/api_bands/recording_create.rabl
new file mode 100644
index 000000000..4aeb0e520
--- /dev/null
+++ b/web/app/views/api_bands/recording_create.rabl
@@ -0,0 +1,3 @@
+object @recording
+
+extends "api_bands/recording_show"
\ No newline at end of file
diff --git a/web/app/views/api_bands/recording_index.rabl b/web/app/views/api_bands/recording_index.rabl
new file mode 100644
index 000000000..153606970
--- /dev/null
+++ b/web/app/views/api_bands/recording_index.rabl
@@ -0,0 +1,12 @@
+collection @recordings
+
+attributes :id, :description, :public
+
+node :genres do |recording|
+ unless recording.genres.nil? || recording.genres.size == 0
+ child :genres => :genres do
+ attributes :id, :description
+ #partial('api_genres/index', :object => recording.genres)
+ end
+ end
+end
\ No newline at end of file
diff --git a/web/app/views/api_bands/recording_show.rabl b/web/app/views/api_bands/recording_show.rabl
new file mode 100644
index 000000000..4936e763d
--- /dev/null
+++ b/web/app/views/api_bands/recording_show.rabl
@@ -0,0 +1,10 @@
+object @recording
+
+attributes :id, :description, :public
+
+unless @recording.genres.nil? || @recording.genres.size == 0
+ child :genres => :genres do
+ attributes :id, :description
+ #partial('api_genres/index', :object => @recording.genres)
+ end
+end
\ No newline at end of file
diff --git a/web/app/views/api_bands/recording_update.rabl b/web/app/views/api_bands/recording_update.rabl
new file mode 100644
index 000000000..4aeb0e520
--- /dev/null
+++ b/web/app/views/api_bands/recording_update.rabl
@@ -0,0 +1,3 @@
+object @recording
+
+extends "api_bands/recording_show"
\ No newline at end of file
diff --git a/web/app/views/api_bands/show.rabl b/web/app/views/api_bands/show.rabl
new file mode 100644
index 000000000..fc4b171d9
--- /dev/null
+++ b/web/app/views/api_bands/show.rabl
@@ -0,0 +1,25 @@
+object @band
+
+attributes :id, :name, :city, :state, :country, :website, :biography, :photo_url, :logo_url, :liker_count, :follower_count, :recording_count, :session_count
+
+unless @band.users.nil? || @band.users.size == 0
+ child :users => :musicians do
+ attributes :id, :first_name, :last_name, :photo_url
+
+ # TODO: figure out how to omit empty arrays
+ node :instruments do |user|
+ unless user.instruments.nil? || user.instruments.size == 0
+ child :musician_instruments => :instruments do
+ attributes :instrument_id, :description, :proficiency_level, :priority
+ end
+ end
+ end
+ end
+end
+
+unless @band.genres.nil? || @band.genres.size == 0
+ child :genres => :genres do
+ attributes :id, :description
+ #partial('api_genres/index', :object => @band.genres)
+ end
+end
diff --git a/web/app/views/api_bands/update.rabl b/web/app/views/api_bands/update.rabl
new file mode 100644
index 000000000..86df71066
--- /dev/null
+++ b/web/app/views/api_bands/update.rabl
@@ -0,0 +1,3 @@
+object @band
+
+extends "api_bands/show"
\ No newline at end of file
diff --git a/web/app/views/api_claimed_recordings/index.rabl b/web/app/views/api_claimed_recordings/index.rabl
new file mode 100644
index 000000000..276a551ec
--- /dev/null
+++ b/web/app/views/api_claimed_recordings/index.rabl
@@ -0,0 +1,3 @@
+object @claimed_recordings
+
+extends "api_claimed_recordings/show"
\ No newline at end of file
diff --git a/web/app/views/api_claimed_recordings/show.rabl b/web/app/views/api_claimed_recordings/show.rabl
new file mode 100644
index 000000000..b9dd7af6f
--- /dev/null
+++ b/web/app/views/api_claimed_recordings/show.rabl
@@ -0,0 +1,29 @@
+# I'm not sure this is right at all. The idea is to bring in all the stuff you would need to play the tracks.
+# I don't think I need to include URLs since that's handled by syncing. This is jsut to make the metadata
+# depictable.
+
+object @claimed_recording
+
+attributes :id, :name, :is_public, :is_downloadable
+
+child(:recording => :recording) {
+ attributes :id, :created_at, :duration
+ child(:band => :band) {
+ attributes :id, :name
+ }
+
+ child(:mixes => :mixes) {
+ attributes :id, :url, :is_completed
+ }
+}
+
+child(:recorded_tracks => :recorded_tracks) {
+ attributes :id, :fully_uploaded, :url
+ child(:instrument => :instrument) {
+ attributes :id, :description
+ }
+ child(:user => :user) {
+ attributes :id, :email, :first_name, :last_name, :city, :state, :country, :photo_url
+ }
+}
+
diff --git a/web/app/views/api_genres/index.rabl b/web/app/views/api_genres/index.rabl
new file mode 100644
index 000000000..6fbde2f77
--- /dev/null
+++ b/web/app/views/api_genres/index.rabl
@@ -0,0 +1,3 @@
+object @genres
+
+extends "api_genres/show"
diff --git a/web/app/views/api_genres/show.rabl b/web/app/views/api_genres/show.rabl
new file mode 100644
index 000000000..bfd9107b4
--- /dev/null
+++ b/web/app/views/api_genres/show.rabl
@@ -0,0 +1,3 @@
+object @genre
+
+attributes :id, :description
diff --git a/web/app/views/api_instruments/index.rabl b/web/app/views/api_instruments/index.rabl
new file mode 100644
index 000000000..26f2348aa
--- /dev/null
+++ b/web/app/views/api_instruments/index.rabl
@@ -0,0 +1,3 @@
+object @instruments
+
+extends "api_instruments/show"
diff --git a/web/app/views/api_instruments/show.rabl b/web/app/views/api_instruments/show.rabl
new file mode 100644
index 000000000..8fe354804
--- /dev/null
+++ b/web/app/views/api_instruments/show.rabl
@@ -0,0 +1,3 @@
+object @instrument
+
+attributes :id, :description, :popularity
diff --git a/web/app/views/api_invitations/create.rabl b/web/app/views/api_invitations/create.rabl
new file mode 100644
index 000000000..3831ae78e
--- /dev/null
+++ b/web/app/views/api_invitations/create.rabl
@@ -0,0 +1,3 @@
+object @invitation
+
+extends "api_invitations/invitation"
diff --git a/web/app/views/api_invitations/index.rabl b/web/app/views/api_invitations/index.rabl
new file mode 100644
index 000000000..5f0a35545
--- /dev/null
+++ b/web/app/views/api_invitations/index.rabl
@@ -0,0 +1,3 @@
+object @invitations
+
+extends "api_invitations/invitation"
diff --git a/web/app/views/api_invitations/invitation.rabl b/web/app/views/api_invitations/invitation.rabl
new file mode 100644
index 000000000..95f472afd
--- /dev/null
+++ b/web/app/views/api_invitations/invitation.rabl
@@ -0,0 +1,21 @@
+object @invitation
+
+attributes :id, :join_request_id
+
+child(:sender => :sender) {
+ attributes :id, :name
+}
+
+child(:receiver => :receiver) {
+ attributes :id, :name
+}
+
+child(:music_session) {
+ attributes :id, :description
+}
+
+child(:join_request) {
+ attributes :id
+}
+
+
diff --git a/web/app/views/api_invitations/show.rabl b/web/app/views/api_invitations/show.rabl
new file mode 100644
index 000000000..3831ae78e
--- /dev/null
+++ b/web/app/views/api_invitations/show.rabl
@@ -0,0 +1,3 @@
+object @invitation
+
+extends "api_invitations/invitation"
diff --git a/web/app/views/api_invited_users/create.rabl b/web/app/views/api_invited_users/create.rabl
new file mode 100644
index 000000000..e62c95f69
--- /dev/null
+++ b/web/app/views/api_invited_users/create.rabl
@@ -0,0 +1,3 @@
+object @invited_user
+
+extends "api_invited_users/invited_user"
diff --git a/web/app/views/api_invited_users/index.rabl b/web/app/views/api_invited_users/index.rabl
new file mode 100644
index 000000000..cca853fd4
--- /dev/null
+++ b/web/app/views/api_invited_users/index.rabl
@@ -0,0 +1,3 @@
+object @invited_users
+
+extends "api_invited_users/invited_user"
diff --git a/web/app/views/api_invited_users/invited_user.rabl b/web/app/views/api_invited_users/invited_user.rabl
new file mode 100644
index 000000000..3bce327fa
--- /dev/null
+++ b/web/app/views/api_invited_users/invited_user.rabl
@@ -0,0 +1,3 @@
+object @invited_user
+
+attributes :id, :created_at, :updated_at, :email, :note, :accepted
\ No newline at end of file
diff --git a/web/app/views/api_invited_users/show.rabl b/web/app/views/api_invited_users/show.rabl
new file mode 100644
index 000000000..e62c95f69
--- /dev/null
+++ b/web/app/views/api_invited_users/show.rabl
@@ -0,0 +1,3 @@
+object @invited_user
+
+extends "api_invited_users/invited_user"
diff --git a/web/app/views/api_join_requests/create.rabl b/web/app/views/api_join_requests/create.rabl
new file mode 100644
index 000000000..0c3111a5f
--- /dev/null
+++ b/web/app/views/api_join_requests/create.rabl
@@ -0,0 +1,3 @@
+object @join_request
+
+extends "api_join_requests/join_request"
diff --git a/web/app/views/api_join_requests/index.rabl b/web/app/views/api_join_requests/index.rabl
new file mode 100644
index 000000000..ae5bf1f59
--- /dev/null
+++ b/web/app/views/api_join_requests/index.rabl
@@ -0,0 +1,3 @@
+object @join_requests
+
+extends "api_join_requests/join_request"
diff --git a/web/app/views/api_join_requests/join_request.rabl b/web/app/views/api_join_requests/join_request.rabl
new file mode 100644
index 000000000..a879ee6f2
--- /dev/null
+++ b/web/app/views/api_join_requests/join_request.rabl
@@ -0,0 +1,11 @@
+object @join_request
+
+attributes :id, :text
+attributes :music_session_id => :music_session
+
+child(:user => :user) {
+ attributes :id, :name
+}
+
+
+
diff --git a/web/app/views/api_join_requests/show.rabl b/web/app/views/api_join_requests/show.rabl
new file mode 100644
index 000000000..0c3111a5f
--- /dev/null
+++ b/web/app/views/api_join_requests/show.rabl
@@ -0,0 +1,3 @@
+object @join_request
+
+extends "api_join_requests/join_request"
diff --git a/web/app/views/api_music_sessions/create.rabl b/web/app/views/api_music_sessions/create.rabl
new file mode 100644
index 000000000..e34b6943d
--- /dev/null
+++ b/web/app/views/api_music_sessions/create.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/index.rabl b/web/app/views/api_music_sessions/index.rabl
new file mode 100644
index 000000000..4b7420de3
--- /dev/null
+++ b/web/app/views/api_music_sessions/index.rabl
@@ -0,0 +1,3 @@
+object @music_sessions
+
+extends "api_music_sessions/show"
diff --git a/web/app/views/api_music_sessions/member_show.rabl b/web/app/views/api_music_sessions/member_show.rabl
new file mode 100644
index 000000000..a6023dd1e
--- /dev/null
+++ b/web/app/views/api_music_sessions/member_show.rabl
@@ -0,0 +1,3 @@
+object @connection
+
+attributes :ip_address, :client_id => :id
diff --git a/web/app/views/api_music_sessions/participant_show.rabl b/web/app/views/api_music_sessions/participant_show.rabl
new file mode 100644
index 000000000..cbbf29c87
--- /dev/null
+++ b/web/app/views/api_music_sessions/participant_show.rabl
@@ -0,0 +1,11 @@
+object @connection
+
+attributes :ip_address, :client_id
+attribute :aasm_state => :connection_state
+
+node(:user_id, :if => lambda { |connection| connection.user.friends?(current_user) }) do |connection|
+ connection.user_id
+end
+child(:tracks => :tracks) {
+ attributes :id, :instrument_id, :sound
+}
\ No newline at end of file
diff --git a/web/app/views/api_music_sessions/show.rabl b/web/app/views/api_music_sessions/show.rabl
new file mode 100644
index 000000000..b9670496e
--- /dev/null
+++ b/web/app/views/api_music_sessions/show.rabl
@@ -0,0 +1,34 @@
+object @music_session
+
+attributes :id, :description, :musician_access, :approval_required, :fan_access, :fan_chat, :band_id, :user_id
+
+node :genres do |item|
+ item.genres.map(&:description)
+end
+
+child(:connections => :participants) {
+ collection @music_sessions, :object_root => false
+ attributes :ip_address, :client_id
+
+ node :user do |connection|
+ { :id => connection.user.id, :photo_url => connection.user.photo_url, :name => connection.user.name, :is_friend => connection.user.friends?(current_user), :connection_state => connection.aasm_state }
+ end
+
+ child(:tracks => :tracks) {
+ attributes :id, :connection_id, :instrument_id, :sound
+ }
+}
+
+child({:invitations => :invitations}) {
+ attributes :id, :sender_id, :receiver_id
+}
+
+# only show join_requests if the current_user is in the session
+node(:join_requests, :if => lambda { |music_session| music_session.users.exists?(current_user) } ) do |music_session|
+ child(:join_requests => :join_requests) {
+ attributes :id, :text
+ child(:user => :user) {
+ attributes :id, :name
+ }
+ }
+end
diff --git a/web/app/views/api_music_sessions/track_create.rabl b/web/app/views/api_music_sessions/track_create.rabl
new file mode 100644
index 000000000..1f086e30b
--- /dev/null
+++ b/web/app/views/api_music_sessions/track_create.rabl
@@ -0,0 +1,3 @@
+object @track
+
+extends "api_music_sessions/track_show"
\ No newline at end of file
diff --git a/web/app/views/api_music_sessions/track_index.rabl b/web/app/views/api_music_sessions/track_index.rabl
new file mode 100644
index 000000000..cbb27355e
--- /dev/null
+++ b/web/app/views/api_music_sessions/track_index.rabl
@@ -0,0 +1,3 @@
+object @tracks
+
+extends "api_music_sessions/track_show"
\ No newline at end of file
diff --git a/web/app/views/api_music_sessions/track_show.rabl b/web/app/views/api_music_sessions/track_show.rabl
new file mode 100644
index 000000000..c460b0085
--- /dev/null
+++ b/web/app/views/api_music_sessions/track_show.rabl
@@ -0,0 +1,3 @@
+object @track
+
+attributes :id, :connection_id, :instrument_id, :sound
\ No newline at end of file
diff --git a/web/app/views/api_music_sessions/track_update.rabl b/web/app/views/api_music_sessions/track_update.rabl
new file mode 100644
index 000000000..1f086e30b
--- /dev/null
+++ b/web/app/views/api_music_sessions/track_update.rabl
@@ -0,0 +1,3 @@
+object @track
+
+extends "api_music_sessions/track_show"
\ No newline at end of file
diff --git a/web/app/views/api_search/index.rabl b/web/app/views/api_search/index.rabl
new file mode 100644
index 000000000..1bcb83003
--- /dev/null
+++ b/web/app/views/api_search/index.rabl
@@ -0,0 +1,43 @@
+object @search
+
+unless @search.bands.nil? || @search.bands.size == 0
+ child(:bands => :bands) {
+ attributes :id, :name, :location, :photo_url, :logo_url
+ }
+end
+
+unless @search.musicians.nil? || @search.musicians.size == 0
+ child(:musicians => :musicians) {
+ attributes :id, :first_name, :last_name, :name, :location, :photo_url
+
+ node :is_friend do |musician|
+ musician.friends?(current_user)
+ end
+
+ child :musician_instruments => :instruments do
+ attributes :instrument_id, :description, :proficiency_level, :priority
+ end
+ }
+end
+
+unless @search.fans.nil? || @search.fans.size == 0
+ child(:fans => :fans) {
+ attributes :id, :first_name, :last_name, :name, :location, :photo_url
+
+ node :is_friend do |fan|
+ fan.friends?(current_user)
+ end
+ }
+end
+
+unless @search.recordings.nil? || @search.recordings.size == 0
+ child(:recordings => :recordings) {
+ attributes :id, :name
+ }
+end
+
+unless @search.friends.nil? || @search.friends.size == 0
+ child(:friends => :friends) {
+ attributes :id, :first_name, :last_name, :name, :location, :email, :online, :photo_url, :musician
+ }
+end
diff --git a/web/app/views/api_users/band_following_index.rabl b/web/app/views/api_users/band_following_index.rabl
new file mode 100644
index 000000000..0e49c2b49
--- /dev/null
+++ b/web/app/views/api_users/band_following_index.rabl
@@ -0,0 +1,21 @@
+collection @user.band_followings
+
+node :band_id do |following|
+ following.id
+end
+
+node :name do |following|
+ following.name
+end
+
+node :location do |following|
+ following.location
+end
+
+node :photo_url do |following|
+ following.photo_url
+end
+
+node :logo_url do |following|
+ following.logo_url
+end
\ No newline at end of file
diff --git a/web/app/views/api_users/band_following_show.rabl b/web/app/views/api_users/band_following_show.rabl
new file mode 100644
index 000000000..ad7786535
--- /dev/null
+++ b/web/app/views/api_users/band_following_show.rabl
@@ -0,0 +1,3 @@
+object @following
+
+attributes :id, :band_id, :follower_id
\ No newline at end of file
diff --git a/web/app/views/api_users/band_index.rabl b/web/app/views/api_users/band_index.rabl
new file mode 100644
index 000000000..6ac9c409c
--- /dev/null
+++ b/web/app/views/api_users/band_index.rabl
@@ -0,0 +1,12 @@
+collection @bands
+
+# do not retrieve all child collections when showing a list of bands
+attributes :id, :name, :location, :photo_url, :logo_url
+
+node :genres do |band|
+ unless band.genres.nil? || band.genres.size == 0
+ child :genres => :genres do
+ attributes :id, :description
+ end
+ end
+end
\ No newline at end of file
diff --git a/web/app/views/api_users/band_invitation_index.rabl b/web/app/views/api_users/band_invitation_index.rabl
new file mode 100644
index 000000000..a385b6d03
--- /dev/null
+++ b/web/app/views/api_users/band_invitation_index.rabl
@@ -0,0 +1,3 @@
+object @invitations
+
+extends "api_users/band_invitation_show"
\ No newline at end of file
diff --git a/web/app/views/api_users/band_invitation_show.rabl b/web/app/views/api_users/band_invitation_show.rabl
new file mode 100644
index 000000000..ffb49a994
--- /dev/null
+++ b/web/app/views/api_users/band_invitation_show.rabl
@@ -0,0 +1,15 @@
+object @invitation
+
+attributes :id, :accepted
+
+node :sender do |i|
+ { :id => i.sender.id, :name => i.sender.name }
+end
+
+node :recipient do |i|
+ { :id => i.receiver.id, :name => i.receiver.name }
+end
+
+node :band do |i|
+ { :id => i.band.id, :name => i.band.name }
+end
\ No newline at end of file
diff --git a/web/app/views/api_users/band_invitation_update.rabl b/web/app/views/api_users/band_invitation_update.rabl
new file mode 100644
index 000000000..68a2acead
--- /dev/null
+++ b/web/app/views/api_users/band_invitation_update.rabl
@@ -0,0 +1,3 @@
+object @invitation
+
+extends "api_users/band_invitation_show"
\ No newline at end of file
diff --git a/web/app/views/api_users/band_like_index.rabl b/web/app/views/api_users/band_like_index.rabl
new file mode 100644
index 000000000..7283388cd
--- /dev/null
+++ b/web/app/views/api_users/band_like_index.rabl
@@ -0,0 +1,27 @@
+object @user.band_likes
+
+attributes :band_id
+
+node :name do |like|
+ like.band.name
+end
+
+node :city do |like|
+ like.band.city
+end
+
+node :state do |like|
+ like.band.state
+end
+
+node :country do |like|
+ like.band.country
+end
+
+node :photo_url do |like|
+ like.band.photo_url
+end
+
+node :logo_url do |like|
+ like.band.logo_url
+end
\ No newline at end of file
diff --git a/web/app/views/api_users/create.rabl b/web/app/views/api_users/create.rabl
new file mode 100644
index 000000000..e7df79f18
--- /dev/null
+++ b/web/app/views/api_users/create.rabl
@@ -0,0 +1,3 @@
+object @user
+
+extends "api_users/show"
\ No newline at end of file
diff --git a/web/app/views/api_users/favorite_create.rabl b/web/app/views/api_users/favorite_create.rabl
new file mode 100644
index 000000000..4e83898ba
--- /dev/null
+++ b/web/app/views/api_users/favorite_create.rabl
@@ -0,0 +1,3 @@
+object @user.favorites
+
+extends "api_users/favorite_index"
\ No newline at end of file
diff --git a/web/app/views/api_users/favorite_index.rabl b/web/app/views/api_users/favorite_index.rabl
new file mode 100644
index 000000000..8c5da0be7
--- /dev/null
+++ b/web/app/views/api_users/favorite_index.rabl
@@ -0,0 +1,11 @@
+object @user.favorites
+
+attributes :recording_id
+
+node :description do |favorite|
+ favorite.recording.description
+end
+
+node :public do |favorite|
+ favorite.recording.public
+end
\ No newline at end of file
diff --git a/web/app/views/api_users/follower_index.rabl b/web/app/views/api_users/follower_index.rabl
new file mode 100644
index 000000000..418c61503
--- /dev/null
+++ b/web/app/views/api_users/follower_index.rabl
@@ -0,0 +1,21 @@
+collection @user.followers
+
+node :user_id do |follower|
+ follower.id
+end
+
+node :name do |follower|
+ follower.name
+end
+
+node :location do |follower|
+ follower.location
+end
+
+node :musician do |follower|
+ follower.musician
+end
+
+node :photo_url do |follower|
+ follower.photo_url
+end
diff --git a/web/app/views/api_users/following_create.rabl b/web/app/views/api_users/following_create.rabl
new file mode 100644
index 000000000..c95a073e0
--- /dev/null
+++ b/web/app/views/api_users/following_create.rabl
@@ -0,0 +1,3 @@
+object @user.followings
+
+extends "api_users/following_index"
\ No newline at end of file
diff --git a/web/app/views/api_users/following_index.rabl b/web/app/views/api_users/following_index.rabl
new file mode 100644
index 000000000..4c7941f2f
--- /dev/null
+++ b/web/app/views/api_users/following_index.rabl
@@ -0,0 +1,21 @@
+collection @user.followings
+
+node :user_id do |following|
+ following.id
+end
+
+node :name do |following|
+ following.name
+end
+
+node :location do |following|
+ following.location
+end
+
+node :musician do |following|
+ following.musician
+end
+
+node :photo_url do |following|
+ following.photo_url
+end
\ No newline at end of file
diff --git a/web/app/views/api_users/following_show.rabl b/web/app/views/api_users/following_show.rabl
new file mode 100644
index 000000000..fff81526b
--- /dev/null
+++ b/web/app/views/api_users/following_show.rabl
@@ -0,0 +1,3 @@
+object @following
+
+attributes :id, :user_id, :follower_id
\ No newline at end of file
diff --git a/web/app/views/api_users/friend_index.rabl b/web/app/views/api_users/friend_index.rabl
new file mode 100644
index 000000000..40eb8fd7c
--- /dev/null
+++ b/web/app/views/api_users/friend_index.rabl
@@ -0,0 +1,3 @@
+object @user.friends
+
+attributes :id, :first_name, :last_name, :name, :location, :city, :state, :country, :email, :online, :photo_url
\ No newline at end of file
diff --git a/web/app/views/api_users/friend_request_create.rabl b/web/app/views/api_users/friend_request_create.rabl
new file mode 100644
index 000000000..6f79cfc24
--- /dev/null
+++ b/web/app/views/api_users/friend_request_create.rabl
@@ -0,0 +1,3 @@
+object @friend_request
+
+extends "api_users/friend_request_show"
\ No newline at end of file
diff --git a/web/app/views/api_users/friend_request_index.rabl b/web/app/views/api_users/friend_request_index.rabl
new file mode 100644
index 000000000..5d5261574
--- /dev/null
+++ b/web/app/views/api_users/friend_request_index.rabl
@@ -0,0 +1,3 @@
+object @friend_requests
+
+extends "api_users/friend_request_show"
\ No newline at end of file
diff --git a/web/app/views/api_users/friend_request_show.rabl b/web/app/views/api_users/friend_request_show.rabl
new file mode 100644
index 000000000..a0bb6df49
--- /dev/null
+++ b/web/app/views/api_users/friend_request_show.rabl
@@ -0,0 +1,3 @@
+object @friend_request
+
+attributes :id, :user_id, :friend_id, :status, :message, :created_at
\ No newline at end of file
diff --git a/web/app/views/api_users/friend_request_update.rabl b/web/app/views/api_users/friend_request_update.rabl
new file mode 100644
index 000000000..6f79cfc24
--- /dev/null
+++ b/web/app/views/api_users/friend_request_update.rabl
@@ -0,0 +1,3 @@
+object @friend_request
+
+extends "api_users/friend_request_show"
\ No newline at end of file
diff --git a/web/app/views/api_users/friend_show.rabl b/web/app/views/api_users/friend_show.rabl
new file mode 100644
index 000000000..27bb3275e
--- /dev/null
+++ b/web/app/views/api_users/friend_show.rabl
@@ -0,0 +1,3 @@
+object @friend
+
+attributes :id, :user_id, :friend_id
\ No newline at end of file
diff --git a/web/app/views/api_users/index.rabl b/web/app/views/api_users/index.rabl
new file mode 100644
index 000000000..8f9bbf9a7
--- /dev/null
+++ b/web/app/views/api_users/index.rabl
@@ -0,0 +1,4 @@
+collection @users
+
+# do not retrieve all child collections when showing a list of users
+attributes :id, :first_name, :last_name, :name, :city, :state, :country, :email, :online, :musician, :photo_url
\ No newline at end of file
diff --git a/web/app/views/api_users/like_create.rabl b/web/app/views/api_users/like_create.rabl
new file mode 100644
index 000000000..426460cc7
--- /dev/null
+++ b/web/app/views/api_users/like_create.rabl
@@ -0,0 +1,3 @@
+object @user.likes
+
+extends "api_users/like_index"
\ No newline at end of file
diff --git a/web/app/views/api_users/like_index.rabl b/web/app/views/api_users/like_index.rabl
new file mode 100644
index 000000000..1976ef29f
--- /dev/null
+++ b/web/app/views/api_users/like_index.rabl
@@ -0,0 +1,31 @@
+object @user.likes
+
+attributes :user_id
+
+node :first_name do |like|
+ like.user.first_name
+end
+
+node :last_name do |like|
+ like.user.last_name
+end
+
+node :city do |like|
+ like.user.city
+end
+
+node :state do |like|
+ like.user.state
+end
+
+node :country do |like|
+ like.user.country
+end
+
+node :musician do |like|
+ like.user.musician
+end
+
+node :photo_url do |like|
+ like.user.photo_url
+end
\ No newline at end of file
diff --git a/web/app/views/api_users/liker_index.rabl b/web/app/views/api_users/liker_index.rabl
new file mode 100644
index 000000000..cbdf608bc
--- /dev/null
+++ b/web/app/views/api_users/liker_index.rabl
@@ -0,0 +1,31 @@
+object @user.likers
+
+attributes :liker_id => :user_id
+
+node :first_name do |liker|
+ liker.user.first_name
+end
+
+node :last_name do |liker|
+ liker.user.last_name
+end
+
+node :city do |liker|
+ liker.user.city
+end
+
+node :state do |liker|
+ liker.user.state
+end
+
+node :country do |liker|
+ liker.user.country
+end
+
+node :musician do |liker|
+ liker.user.musician
+end
+
+node :photo_url do |liker|
+ liker.user.photo_url
+end
\ No newline at end of file
diff --git a/web/app/views/api_users/notification_index.rabl b/web/app/views/api_users/notification_index.rabl
new file mode 100644
index 000000000..23f80c077
--- /dev/null
+++ b/web/app/views/api_users/notification_index.rabl
@@ -0,0 +1,7 @@
+collection @notifications
+
+attributes :description, :source_user_id, :target_user_id, :session_id, :recording_id, :invitation_id, :join_request_id, :friend_request_id, :formatted_msg, :created_at, :photo_url
+
+node :notification_id do |n|
+ n.id
+end
\ No newline at end of file
diff --git a/web/app/views/api_users/recording_create.rabl b/web/app/views/api_users/recording_create.rabl
new file mode 100644
index 000000000..8d8826efe
--- /dev/null
+++ b/web/app/views/api_users/recording_create.rabl
@@ -0,0 +1,3 @@
+object @recording
+
+extends "api_users/recording_show"
\ No newline at end of file
diff --git a/web/app/views/api_users/recording_index.rabl b/web/app/views/api_users/recording_index.rabl
new file mode 100644
index 000000000..d0bf502d6
--- /dev/null
+++ b/web/app/views/api_users/recording_index.rabl
@@ -0,0 +1,12 @@
+collection @recordings
+
+attributes :id, :description, :public, :favorite_count
+
+node :genres do |recording|
+ unless recording.genres.nil? || recording.genres.size == 0
+ child :genres => :genres do
+ attributes :id, :description
+ #partial('api_genres/index', :object => recording.genres)
+ end
+ end
+end
\ No newline at end of file
diff --git a/web/app/views/api_users/recording_show.rabl b/web/app/views/api_users/recording_show.rabl
new file mode 100644
index 000000000..f0248d79c
--- /dev/null
+++ b/web/app/views/api_users/recording_show.rabl
@@ -0,0 +1,12 @@
+object @recording
+
+attributes :id, :description, :public, :favorite_count
+
+unless @recording.genres.nil? || @recording.genres.size == 0
+ child :genres => :genres do
+ attributes :id, :description
+ #partial('api_genres/index', :object => @band.genres)
+ end
+end
+
+#TODO: show the Users that have added this Recording as a Favorite ONLY IF the owning user has "Better Analytics service level"
\ No newline at end of file
diff --git a/web/app/views/api_users/recording_update.rabl b/web/app/views/api_users/recording_update.rabl
new file mode 100644
index 000000000..8d8826efe
--- /dev/null
+++ b/web/app/views/api_users/recording_update.rabl
@@ -0,0 +1,3 @@
+object @recording
+
+extends "api_users/recording_show"
\ No newline at end of file
diff --git a/web/app/views/api_users/session_history_index.rabl b/web/app/views/api_users/session_history_index.rabl
new file mode 100644
index 000000000..4ba35abc2
--- /dev/null
+++ b/web/app/views/api_users/session_history_index.rabl
@@ -0,0 +1,7 @@
+object @session_history
+
+attributes :music_session_id, :user_id, :description, :band_id, :genres
+
+child :music_session_user_histories => :participants do
+ attributes :client_id, :user_id
+end
\ No newline at end of file
diff --git a/web/app/views/api_users/session_user_history_index.rabl b/web/app/views/api_users/session_user_history_index.rabl
new file mode 100644
index 000000000..73b769c16
--- /dev/null
+++ b/web/app/views/api_users/session_user_history_index.rabl
@@ -0,0 +1,3 @@
+object @session_user_history
+
+attributes :music_session_id, :user_id, :client_id
\ No newline at end of file
diff --git a/web/app/views/api_users/show.rabl b/web/app/views/api_users/show.rabl
new file mode 100644
index 000000000..09a5bb4bf
--- /dev/null
+++ b/web/app/views/api_users/show.rabl
@@ -0,0 +1,31 @@
+object @user
+
+attributes :id, :first_name, :last_name, :name, :city, :state, :country, :location, :online, :photo_url, :musician, :gender, :birth_date, :internet_service_provider, :friend_count, :liker_count, :like_count, :band_like_count, :follower_count, :following_count, :band_following_count, :recording_count, :session_count
+
+# give back more info if the user being fetched is yourself
+if @user == current_user
+ attributes :email, :original_fpfile, :cropped_fpfile, :crop_selection, :session_settings
+end
+
+unless @user.friends.nil? || @user.friends.size == 0
+ child :friends => :friends do
+ attributes :id, :first_name, :last_name, :online
+ end
+end
+
+unless @user.bands.nil? || @user.bands.size == 0
+ child :band_musicians => :bands do
+ attributes :id, :name, :admin, :photo_url, :logo_url
+
+ child :genres => :genres do
+ attributes :id, :description
+ #partial('api_genres/index', :object => @user.bands.genres)
+ end
+ end
+end
+
+unless @user.instruments.nil? || @user.instruments.size == 0
+ child :musician_instruments => :instruments do
+ attributes :description, :proficiency_level, :priority, :instrument_id
+ end
+end
diff --git a/web/app/views/api_users/signup_confirm.rabl b/web/app/views/api_users/signup_confirm.rabl
new file mode 100644
index 000000000..e7df79f18
--- /dev/null
+++ b/web/app/views/api_users/signup_confirm.rabl
@@ -0,0 +1,3 @@
+object @user
+
+extends "api_users/show"
\ No newline at end of file
diff --git a/web/app/views/api_users/update.rabl b/web/app/views/api_users/update.rabl
new file mode 100644
index 000000000..e7df79f18
--- /dev/null
+++ b/web/app/views/api_users/update.rabl
@@ -0,0 +1,3 @@
+object @user
+
+extends "api_users/show"
\ No newline at end of file
diff --git a/web/app/views/artifacts/versioncheck.html.erb b/web/app/views/artifacts/versioncheck.html.erb
new file mode 100644
index 000000000..417e14a28
--- /dev/null
+++ b/web/app/views/artifacts/versioncheck.html.erb
@@ -0,0 +1,10 @@
+
+
+
+ <%= image_tag "logo.png" %>
+ New version available
+
+ Version <%= @artifact.version %> is now released on http://jamkazam.com
+ Please download directly from here.
+
+
diff --git a/web/app/views/clients/_account.html.erb b/web/app/views/clients/_account.html.erb
new file mode 100644
index 000000000..0bd1a893c
--- /dev/null
+++ b/web/app/views/clients/_account.html.erb
@@ -0,0 +1,111 @@
+
+
+
+
+
+ my account
+ <%= render "screen_navigation" %>
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/app/views/clients/_account_audio_profile.html.erb b/web/app/views/clients/_account_audio_profile.html.erb
new file mode 100644
index 000000000..236bd2c8b
--- /dev/null
+++ b/web/app/views/clients/_account_audio_profile.html.erb
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+ my account
+ <%= render "screen_navigation" %>
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/app/views/clients/_account_identity.html.erb b/web/app/views/clients/_account_identity.html.erb
new file mode 100644
index 000000000..f99233cce
--- /dev/null
+++ b/web/app/views/clients/_account_identity.html.erb
@@ -0,0 +1,93 @@
+
+
+
+
+
+
+
+ my account
+ <%= render "screen_navigation" %>
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/app/views/clients/_account_profile.html.erb b/web/app/views/clients/_account_profile.html.erb
new file mode 100644
index 000000000..bbd0e8be7
--- /dev/null
+++ b/web/app/views/clients/_account_profile.html.erb
@@ -0,0 +1,130 @@
+
+
+
+
+
+
+
+ my account
+ <%= render "screen_navigation" %>
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/app/views/clients/_account_profile_avatar.html.erb b/web/app/views/clients/_account_profile_avatar.html.erb
new file mode 100644
index 000000000..57ff42942
--- /dev/null
+++ b/web/app/views/clients/_account_profile_avatar.html.erb
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+ my account
+ <%= render "screen_navigation" %>
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/app/views/clients/_addNewGear.html.erb b/web/app/views/clients/_addNewGear.html.erb
new file mode 100644
index 000000000..462040b57
--- /dev/null
+++ b/web/app/views/clients/_addNewGear.html.erb
@@ -0,0 +1,16 @@
+
+
+
+ <%= image_tag "content/icon_add.png", {:width => 19, :height => 19, :class => 'content-icon' } %>
+ add new audio gear
+
+
+ To add a new audio device, you must exit your current session and test the device using the JamKazam automated test feature.
+
+
+
+
+
\ No newline at end of file
diff --git a/web/app/views/clients/_addTrack.html.erb b/web/app/views/clients/_addTrack.html.erb
new file mode 100644
index 000000000..596b2884a
--- /dev/null
+++ b/web/app/views/clients/_addTrack.html.erb
@@ -0,0 +1,63 @@
+
+
+
+ <%= image_tag "content/icon_add.png", {:width => 19, :height => 19, :class => 'content-icon' } %>
+ add a track
+
+
+
+ Use arrow buttons to assign audio inputs to this new track, and choose what instrument you are playing on this track. Please note that you may only use one audio device for all audio inputs and outputs. If you don't see an audio device you think should be listed view this help topic to understand why.
+
+
+
+
+
+
+ Unused Inputs:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Track 2 Input:
+
+
+
+
+
+
+
+ Track 2 Instrument:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/web/app/views/clients/_bands.html.erb b/web/app/views/clients/_bands.html.erb
new file mode 100644
index 000000000..8505890b1
--- /dev/null
+++ b/web/app/views/clients/_bands.html.erb
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ bands
+ <%= render "screen_navigation" %>
+
+ This feature not yet implemented
+
diff --git a/web/app/views/clients/_banner.html.erb b/web/app/views/clients/_banner.html.erb
new file mode 100644
index 000000000..9a932e3a9
--- /dev/null
+++ b/web/app/views/clients/_banner.html.erb
@@ -0,0 +1,18 @@
+
+
+
+
\ No newline at end of file
diff --git a/web/app/views/clients/_client_update.html.erb b/web/app/views/clients/_client_update.html.erb
new file mode 100644
index 000000000..7861217b0
--- /dev/null
+++ b/web/app/views/clients/_client_update.html.erb
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/web/app/views/clients/_configureTrack.html.erb b/web/app/views/clients/_configureTrack.html.erb
new file mode 100644
index 000000000..b05eddd01
--- /dev/null
+++ b/web/app/views/clients/_configureTrack.html.erb
@@ -0,0 +1,184 @@
+
+
+
+ <%= image_tag "content/icon_add.png", {:width => 19, :height => 19, :class => 'content-icon' } %>
+ configure tracks
+
+
+
+ Music Audio
+ Voice Chat
+
+
+
+
+
+
+
+
+
+
+
+
+ Audio Device:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Unused Inputs:
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Track 1 Input:
+
+
+
+
+
+
+ Track 1 Instrument:
+
+
+
+
+
+
+
+
+
+
+ Track 2 Input:
+
+
+
+
+
+
+ Track 2 Instrument:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Unused Outputs:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Session Audio Output:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ CANCEL
+ UPDATE SETTINGS
+
+
+
+
+
\ No newline at end of file
diff --git a/web/app/views/clients/_createSession.html.erb b/web/app/views/clients/_createSession.html.erb
new file mode 100644
index 000000000..97336c799
--- /dev/null
+++ b/web/app/views/clients/_createSession.html.erb
@@ -0,0 +1,185 @@
+
+
+
+
+
+
+ create session
+
+ <%= render "screen_navigation" %>
+
+
+
+
+
+<%= render "friendSelector" %>
+<%= render "invitation" %>
+
+
+
+
+
+
diff --git a/web/app/views/clients/_error.html.erb b/web/app/views/clients/_error.html.erb
new file mode 100644
index 000000000..f1c131e73
--- /dev/null
+++ b/web/app/views/clients/_error.html.erb
@@ -0,0 +1,15 @@
+
+
+
+ <%= image_tag "content/icon_add.png", {:width => 19, :height => 19, :class => 'content-icon' } %>
+
+
+
+
\ No newline at end of file
diff --git a/web/app/views/clients/_faders.html.erb b/web/app/views/clients/_faders.html.erb
new file mode 100644
index 000000000..fd4a33046
--- /dev/null
+++ b/web/app/views/clients/_faders.html.erb
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
diff --git a/web/app/views/clients/_feed.html.erb b/web/app/views/clients/_feed.html.erb
new file mode 100644
index 000000000..5f09c1720
--- /dev/null
+++ b/web/app/views/clients/_feed.html.erb
@@ -0,0 +1,12 @@
+
+
+
+
+
+ feed
+ <%= render "screen_navigation" %>
+
+ This feature not yet implemented
+
diff --git a/web/app/views/clients/_findSession.html.erb b/web/app/views/clients/_findSession.html.erb
new file mode 100644
index 000000000..47d32b786
--- /dev/null
+++ b/web/app/views/clients/_findSession.html.erb
@@ -0,0 +1,119 @@
+
+
+
+
+
+
+ find a session
+
+ <%= render "screen_navigation" %>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/web/app/views/clients/_footer.html.erb b/web/app/views/clients/_footer.html.erb
new file mode 100644
index 000000000..adb4012d0
--- /dev/null
+++ b/web/app/views/clients/_footer.html.erb
@@ -0,0 +1,14 @@
+
+
+
diff --git a/web/app/views/clients/_friendSelector.html.erb b/web/app/views/clients/_friendSelector.html.erb
new file mode 100644
index 000000000..159f9a49f
--- /dev/null
+++ b/web/app/views/clients/_friendSelector.html.erb
@@ -0,0 +1,27 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/web/app/views/clients/_ftue.html.erb b/web/app/views/clients/_ftue.html.erb
new file mode 100644
index 000000000..3a1e5b1db
--- /dev/null
+++ b/web/app/views/clients/_ftue.html.erb
@@ -0,0 +1,276 @@
+
+
+
+ audio gear setup
+
+
+
+
+
+
+
+
+ Please identify which of the three types of audio gear below you
+ are going to use with the JamKazam service, and click one to
+ watch a video on how to navigate this initial setup and testing
+ process. After watching the video, click the 'NEXT' button to
+ get started. If you don't have your audio gear handy now, click
+ Cancel.
+
+
+
+ -
+ AUDIO DEVICE WITH PORTS FOR INSTRUMENT OR MIC INPUT JACKS
+ <%= image_tag "content/audio_capture_ftue.png", {:width => 243, :height => 70} %>
+
+ -
+ USB MICROPHONE
+ <%= image_tag "content/microphone_ftue.png", {:width => 70, :height => 113} %>
+
+ -
+ COMPUTER'S BUILT-IN MIC & SPEAKERS/HEADPHONES
+ <%= image_tag "content/computer_ftue.png", {:width => 118, :height => 105} %>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/app/views/clients/_genreSelector.html.erb b/web/app/views/clients/_genreSelector.html.erb
new file mode 100644
index 000000000..d63a04089
--- /dev/null
+++ b/web/app/views/clients/_genreSelector.html.erb
@@ -0,0 +1,3 @@
+
diff --git a/web/app/views/clients/_header.html.erb b/web/app/views/clients/_header.html.erb
new file mode 100644
index 000000000..e61a45c46
--- /dev/null
+++ b/web/app/views/clients/_header.html.erb
@@ -0,0 +1,85 @@
+
+
+
diff --git a/web/app/views/clients/_home.html.erb b/web/app/views/clients/_home.html.erb
new file mode 100644
index 000000000..1ae839f61
--- /dev/null
+++ b/web/app/views/clients/_home.html.erb
@@ -0,0 +1,72 @@
+
+
+ <% if @nativeClient %>
+
+
+ create session
+
+
+
+
+ find session
+
+
+
+
+ profile
+
+
+
+
+ feed
+
+
+
+
+ account
+
+
+
+
+ <% else %>
+
+
+ feed
+
+
+
+
+ bands
+
+
+
+
+ musicians
+
+
+
+
+ profile
+
+
+
+
+ account
+
+
+
+
+ <% end %>
+
+
diff --git a/web/app/views/clients/_invitation.html.erb b/web/app/views/clients/_invitation.html.erb
new file mode 100644
index 000000000..5e80baacc
--- /dev/null
+++ b/web/app/views/clients/_invitation.html.erb
@@ -0,0 +1,28 @@
+
+
diff --git a/web/app/views/clients/_musicians.html.erb b/web/app/views/clients/_musicians.html.erb
new file mode 100644
index 000000000..4a6eac046
--- /dev/null
+++ b/web/app/views/clients/_musicians.html.erb
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ musicians
+ <%= render "screen_navigation" %>
+
+ This feature not yet implemented
+
diff --git a/web/app/views/clients/_notify.html.erb b/web/app/views/clients/_notify.html.erb
new file mode 100644
index 000000000..d865136cf
--- /dev/null
+++ b/web/app/views/clients/_notify.html.erb
@@ -0,0 +1,15 @@
+
+
diff --git a/web/app/views/clients/_overlay_small.html.erb b/web/app/views/clients/_overlay_small.html.erb
new file mode 100644
index 000000000..199712e10
--- /dev/null
+++ b/web/app/views/clients/_overlay_small.html.erb
@@ -0,0 +1,18 @@
+
\ No newline at end of file
diff --git a/web/app/views/clients/_profile.html.erb b/web/app/views/clients/_profile.html.erb
new file mode 100644
index 000000000..70ec227df
--- /dev/null
+++ b/web/app/views/clients/_profile.html.erb
@@ -0,0 +1,126 @@
+
+
+
+
+
+ musician profile
+
+ <%= render "screen_navigation" %>
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/web/app/views/clients/_screen_navigation.html.erb b/web/app/views/clients/_screen_navigation.html.erb
new file mode 100644
index 000000000..230e076a6
--- /dev/null
+++ b/web/app/views/clients/_screen_navigation.html.erb
@@ -0,0 +1,6 @@
+
+
diff --git a/web/app/views/clients/_searchResults.html.erb b/web/app/views/clients/_searchResults.html.erb
new file mode 100644
index 000000000..3d68961a1
--- /dev/null
+++ b/web/app/views/clients/_searchResults.html.erb
@@ -0,0 +1,51 @@
+
+
+
+
+ search results
+ <%= render "screen_navigation" %>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/app/views/clients/_session.html.erb b/web/app/views/clients/_session.html.erb
new file mode 100644
index 000000000..af1489925
--- /dev/null
+++ b/web/app/views/clients/_session.html.erb
@@ -0,0 +1,180 @@
+
+
+
+
+ session
+ <%= render "screen_navigation" %>
+
+
+
+
+
+ <%= image_tag "content/icon_resync.png", {:align => "texttop", :height => 14, :width => 12} %>
+ RESYNC
+
+
+ <%= image_tag "content/icon_settings_sm.png", {:align => "texttop", :height => 12, :width => 12} %>
+ SETTINGS
+
+
+ <%= image_tag "content/icon_share.png", {:align => "texttop", :height => 12, :width => 12} %>
+ SHARE
+
+
+
+
+ VOLUME:
+
+
+
+
+
+ MIX:
+ others
+
+ me
+
+
+
+ X LEAVE
+
+
+
+
+
+
+
+
+
+
+
+
+ my tracks
+
+
+
+
+
+
+
+
+
+
+ live tracks
+
+
+
+
+ No Live Tracks:
+ Invite Other Musicians to
+ Add Live Tracks
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<%= render "configureTrack" %>
+<%= render "addTrack" %>
+<%= render "addNewGear" %>
+<%= render "error" %>
+<%= render "sessionSettings" %>
+
+
+
+
+
+
+
+
diff --git a/web/app/views/clients/_sessionList.html.erb b/web/app/views/clients/_sessionList.html.erb
new file mode 100644
index 000000000..451bb286c
--- /dev/null
+++ b/web/app/views/clients/_sessionList.html.erb
@@ -0,0 +1,14 @@
+<%= title %>
+
+
+
+ GENRE
+ DESCRIPTION
+ MUSICIANS
+ AUDIENCE
+ LATENCY
+ LISTEN
+ JOIN
+
+
+
\ No newline at end of file
diff --git a/web/app/views/clients/_sessionSettings.html.erb b/web/app/views/clients/_sessionSettings.html.erb
new file mode 100644
index 000000000..6dbc3d68e
--- /dev/null
+++ b/web/app/views/clients/_sessionSettings.html.erb
@@ -0,0 +1,75 @@
+
+
+
+
+
+ <%= image_tag "content/icon_settings_lg.png",
+ {:width => 18, :height => 18, :class => "content-icon"} %>
+ update session settings
+
+
+
+
+
+
+
+
+
+
+ Connecting...
+The JamKazam team loves music, and in particular live music. We’ve combined our passion for live music and technology to make it far easier for musicians to play live with each other.
+ +Using JamKazam, you can now play music with other musicians from your homes across the Internet as if you were sitting in the same room. You can record your performances at the track level, share your recordings, and even broadcast your live sessions to family, friends, and fans.
+ +We hope you enjoy using the service, making more musician friends, and making more live music!
diff --git a/web/app/views/corps/contact.html.erb b/web/app/views/corps/contact.html.erb new file mode 100644 index 000000000..f1eedfeec --- /dev/null +++ b/web/app/views/corps/contact.html.erb @@ -0,0 +1,36 @@ +<% provide(:title, 'Contact') %> +<% provide(:purpose, 'contact') %> +Thank you for your interest in JamKazam. Depending on your specific interest, please use one of the following avenues to reach out to us. We look forward to hearing from you!
+For technical support and help using our products and services, please visit the JamKazam Support Center.
+For partnering inquiries, please contact us at: partners@jamkazam.com.
+For inquiries from traditional media, bloggers, and others interested in sharing news and information about us with others, please visit the <%= link_to "JamKazam Media Center", corp_media_center_path %>, and you may also contact us at pr@jamkazam.com.
+To report content hosted on the JamKazam platform that that infringes your copyright or the copyright of a third party on whose behalf you are entitled to act, please see the Reporting Infringements section of the JamKazam Terms of Service.
+For general inquiries, please contact us at: info@jamkazam.com.
+This Cookies Policy forms part of our general Privacy Policy. You can read the full Privacy + Policy <%= link_to "here", privacy_path %>.
+In common with most other websites, we use cookies and similar technologies to help us understand how people use + JamKazam so that we can keep improving our Platform. We have created this Cookies Policy to provide you with clear and + explicit information about the technologies that we use on JamKazam, and your choices when it comes to these + technologies.
+If you choose to use the Platform without blocking or disabling cookies or opting out of other technologies, you will + indicate your consent to our use of these cookies and other technologies and to our use (in accordance with this + policy and the rest of our Privacy Policy) of any personal information that we collect using these technologies. If + you do not consent to the use of these technologies, please be sure to block or disable cookies using your browser + settings and/or the settings within our mobile apps.
+ +Cookies are small text files that are placed on your computer by websites that you visit. These text files can be + read by these websites and help to identify you when you return to a website. Cookies can be “persistent” or “session” + cookies. Persistent cookies remain on your computer when you have gone offline, while session cookies are deleted as + soon as you close your web browser.
+To find out more about cookies, including how to see what cookies have been set and how to block and delete cookies, + please visit http://www.aboutcookies.org/.
+ +We use our own cookies to recognize you when you visit our website or use our apps. This means that you don’t have to + log in each time you visit, and we can remember your preferences and settings. JamKazam cookie names are currently + “_jamkazam_session”, “client_id”, and “remember_token”.
+ +In addition to our own cookies, we work with various reputable companies to help us analyze how the Platform is used, + and to optimize our Website and Apps to deliver the best possible experience. The following companies help us achieve + this through use of cookies:
+ +Google Analytics and Google Web Optimizer are services provided by Google, Inc. (“Google”). Google Analytics uses + cookies to help us analyze how users use our website, our mobile site, our Apps, and any JamKazam widgets embedded on + third party sites. Google Web Optimizer uses the same cookies to measure how different users respond to different + content. The information generated by these cookies (including your truncated IP address) is transmitted to and stored + by Google on servers in the United States. Google will use this information for the purpose of evaluating your, and + other users’, use of our website, mobile site, Apps, and JamKazam widgets, compiling reports for us on website + activity and providing other services relating to website activity and Internet usage. Please note that Google only + receives your truncated IP address. This is sufficient for Google to identify (approximately) the country from which + you are visiting our sites or accessing our players, but is not sufficient to identify you, or your computer or mobile + device, individually.
+Google cookies are those beginning “__ut”.
+You can find more information here, + including a link to Google’s privacy + policy.
+ +In addition to cookies, we also use the following standard Internet technologies in connection with your use of the + Platform:
+Clear GIFs: We sometimes use “clear GIFs”, also known as “web bugs”, which are small image files that we embed into + our email newsletters. These clear GIFs tell us whether you opened the newsletter, clicked on any of the content or + forwarded the newsletter to someone else. This provides us with useful information regarding the effectiveness of our + email newsletters, which we can use to ensure that we are delivering information that is relevant to our users.
+Support: We work with Desk.com, a subsidiary of Salesforce.com, (“Desk.com”) to provide support to our users, and we + use Desk.com’s identity integration to automate our users’ access to Desk.com support accounts, so that you do not + have to set up separate accounts for JamKazam support services. + Click here to review Desk.com’s privacy policy.
+ +You can use the settings within your browser to control the cookies that are set on your computer or mobile device. + However, please be aware that cookies are important to many aspects for the Platform – if you set your browser to + reject cookies, you may not be able to enjoy all of the features of the Platform. To find out how to see what cookies + have been set and how to reject and delete cookies, please + visit http://www.aboutcookies.org.
+To opt-out of analysis by Google Analytics on our website and other websites, please visit + http://tools.google.com/dlpage/gaoptout.
+ +We have done our best to provide you with clear and comprehensive information about our use of cookies and similar + technologies. If you choose to use the Platform without blocking or disabling cookies or opting out of these + technologies (as described above), you will indicate your consent to our use of these cookies and other technologies + and to our use (in accordance with this policy and our Privacy Policy) of any personal information that we collect + using these technologies. If you do not consent to the use of these technologies, please be sure to block or disable + them using your browser settings, and/or choose not to use the JamKazam service.
+ +Effective Date: 1 August 2013
+ diff --git a/web/app/views/corps/help.html.erb b/web/app/views/corps/help.html.erb new file mode 100644 index 000000000..e5d6f7654 --- /dev/null +++ b/web/app/views/corps/help.html.erb @@ -0,0 +1,13 @@ +<% provide(:title, 'Help') %> +<% provide(:purpose, 'help') %> +To get help, please visit the JamKazam support center using the JamKazam Support link below. JamKazam uses the + Desk.com service to provide support. In the support center, you can browse knowledge base articles and search other + support cases to see if an answer to your question is already available. And if not, you can post your question there, + and we'll reply as soon as we can. When you click the JamKazam Support link, we'll automatically sign you into our + Desk.com service using your JamKazam credentials, so you won't have to register for a separate support account.
+ + \ No newline at end of file diff --git a/web/app/views/corps/media_center.html.erb b/web/app/views/corps/media_center.html.erb new file mode 100644 index 000000000..9652112bf --- /dev/null +++ b/web/app/views/corps/media_center.html.erb @@ -0,0 +1,58 @@ +<% provide(:title, 'Media Center') %> +<% provide(:purpose, 'media_center') %> + +|
+ <%= image_tag("content/icon_product.png", height: '58', width: '58') %>
+
+ + + Product Information++ +
|
+
+
+ <%= image_tag("content/icon_users.png", height: '58', width: '58') %>
+ + + User Examples++ +
|
+
+ <%= image_tag("content/icon_pr.png", height: '58', width: '58') %>
+ + + Press Releases++ +
|
+
August 27, 2013 -- Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. READ MORE �
+ +August 15, 2013 -- Aliquam et nisl vel ligula consectetuer suscipit. Morbi euismod enim eget neque. Donec sagittis massa. Vestibulum quis augue sit amet ipsum laoreet pretium. Nulla facilisi. READ MORE �
+ +August 2, 2013 -- Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Vestibulum tortor quam, feugiat vitae, ultricies eget, tempor sit amet, ante. READ MORE �
+ +July 12, 2013 -- Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. READ MORE �
+ +July 7, 2013 -- Aliquam et nisl vel ligula consectetuer suscipit. Morbi euismod enim eget neque. Donec sagittis massa. Vestibulum quis augue sit amet ipsum laoreet pretium. Nulla facilisi. READ MORE �
+ + + +