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 @@ + + + + +This is a custom SVG webfont generated by Font Squirrel. +Copyright : Copyright c 20102011 by tyPoland Lukasz Dziedzic with Reserved Font Name Lato Licensed under the SIL Open Font License Version 11 +Designer : Lukasz Dziedzic +Foundry : tyPoland Lukasz Dziedzic +Foundry URL : httpwwwtypolandcom + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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 @@ + + + + +This is a custom SVG webfont generated by Font Squirrel. +Copyright : Copyright c 20102011 by tyPoland Lukasz Dziedzic with Reserved Font Name Lato Licensed under the SIL Open Font License Version 11 +Designer : Lukasz Dziedzic +Foundry : tyPoland Lukasz Dziedzic +Foundry URL : httpwwwtypolandcom + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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 @@ + + + + +This is a custom SVG webfont generated by Font Squirrel. +Copyright : Copyright c 20102011 by tyPoland Lukasz Dziedzic with Reserved Font Name Lato Licensed under the SIL Open Font License Version 11 +Designer : Lukasz Dziedzic +Foundry : tyPoland Lukasz Dziedzic +Foundry URL : httpwwwtypolandcom + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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 @@ + + + + +This is a custom SVG webfont generated by Font Squirrel. +Copyright : Copyright c 20102011 by tyPoland Lukasz Dziedzic with Reserved Font Name Lato Licensed under the SIL Open Font License Version 11 +Designer : Lukasz Dziedzic +Foundry : tyPoland Lukasz Dziedzic +Foundry URL : httpwwwtypolandcom + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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 @@ + + + + +This is a custom SVG webfont generated by Font Squirrel. +Copyright : Copyright c 20102011 by tyPoland Lukasz Dziedzic with Reserved Font Name Lato Licensed under the SIL Open Font License Version 11 +Designer : Lukasz Dziedzic +Foundry : Lukasz Dziedzic +Foundry URL : httpwwwtypolandcom + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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 @@ + + + + +This is a custom SVG webfont generated by Font Squirrel. +Copyright : Copyright c 20102011 by tyPoland Lukasz Dziedzic with Reserved Font Name Lato Licensed under the SIL Open Font License Version 11 +Designer : Lukasz Dziedzic +Foundry : Lukasz Dziedzic +Foundry URL : httpwwwtypolandcom + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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 @@ + + + + +This is a custom SVG webfont generated by Font Squirrel. +Copyright : Copyright c 20102011 by tyPoland Lukasz Dziedzic with Reserved Font Name Lato Licensed under the SIL Open Font License Version 11 +Designer : Lukasz Dziedzic +Foundry : Lukasz Dziedzic +Foundry URL : httpwwwtypolandcom + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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 @@ + + + + +This is a custom SVG webfont generated by Font Squirrel. +Copyright : Copyright c 20102011 by tyPoland Lukasz Dziedzic with Reserved Font Name Lato Licensed under the SIL Open Font License Version 11 +Designer : Lukasz Dziedzic +Foundry : Lukasz Dziedzic +Foundry URL : httpwwwtypolandcom + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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 @@ + + + + +This is a custom SVG webfont generated by Font Squirrel. +Copyright : Copyright c 20102011 by tyPoland Lukasz Dziedzic with Reserved Font Name Lato Licensed under the SIL Open Font License Version 11 +Designer : Lukasz Dziedzic +Foundry : tyPoland Lukasz Dziedzic +Foundry URL : httpwwwtypolandcom + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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 @@ + + + + +This is a custom SVG webfont generated by Font Squirrel. +Copyright : Copyright c 20102011 by tyPoland Lukasz Dziedzic with Reserved Font Name Lato Licensed under the SIL Open Font License Version 11 +Designer : Lukasz Dziedzic +Foundry : tyPoland Lukasz Dziedzic +Foundry URL : httpwwwtypolandcom + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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 @@ + + + + + + + Font Face Demo + + + + + +
+

Font-face Demo for the Lato Font

+ + + +

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.

+ +
+ + diff --git a/web/app/assets/images/avatars/avatar_ben.jpg b/web/app/assets/images/avatars/avatar_ben.jpg new file mode 100644 index 000000000..1a9c645df Binary files /dev/null and b/web/app/assets/images/avatars/avatar_ben.jpg differ diff --git a/web/app/assets/images/avatars/avatar_brian.jpg b/web/app/assets/images/avatars/avatar_brian.jpg new file mode 100644 index 000000000..7b33135b7 Binary files /dev/null and b/web/app/assets/images/avatars/avatar_brian.jpg differ diff --git a/web/app/assets/images/avatars/avatar_craig.jpg b/web/app/assets/images/avatars/avatar_craig.jpg new file mode 100644 index 000000000..617d095db Binary files /dev/null and b/web/app/assets/images/avatars/avatar_craig.jpg differ diff --git a/web/app/assets/images/avatars/avatar_david.jpg b/web/app/assets/images/avatars/avatar_david.jpg new file mode 100644 index 000000000..b64f743fc Binary files /dev/null and b/web/app/assets/images/avatars/avatar_david.jpg differ diff --git a/web/app/assets/images/avatars/avatar_jonathon.jpg b/web/app/assets/images/avatars/avatar_jonathon.jpg new file mode 100644 index 000000000..479155518 Binary files /dev/null and b/web/app/assets/images/avatars/avatar_jonathon.jpg differ diff --git a/web/app/assets/images/avatars/avatar_jonathon.png b/web/app/assets/images/avatars/avatar_jonathon.png new file mode 100644 index 000000000..4f09c1bed Binary files /dev/null and b/web/app/assets/images/avatars/avatar_jonathon.png differ diff --git a/web/app/assets/images/avatars/avatar_peter.jpg b/web/app/assets/images/avatars/avatar_peter.jpg new file mode 100644 index 000000000..211c297ef Binary files /dev/null and b/web/app/assets/images/avatars/avatar_peter.jpg differ diff --git a/web/app/assets/images/avatars/avatar_seth.jpg b/web/app/assets/images/avatars/avatar_seth.jpg new file mode 100644 index 000000000..4caaf0814 Binary files /dev/null and b/web/app/assets/images/avatars/avatar_seth.jpg differ diff --git a/web/app/assets/images/content/arrow_left_24.png b/web/app/assets/images/content/arrow_left_24.png new file mode 100644 index 000000000..bc5ac6232 Binary files /dev/null and b/web/app/assets/images/content/arrow_left_24.png differ diff --git a/web/app/assets/images/content/arrow_right_24.png b/web/app/assets/images/content/arrow_right_24.png new file mode 100644 index 000000000..abc4fc481 Binary files /dev/null and b/web/app/assets/images/content/arrow_right_24.png differ diff --git a/web/app/assets/images/content/audio_capture.png b/web/app/assets/images/content/audio_capture.png new file mode 100644 index 000000000..11ea488ad Binary files /dev/null and b/web/app/assets/images/content/audio_capture.png differ diff --git a/web/app/assets/images/content/audio_capture_ftue.png b/web/app/assets/images/content/audio_capture_ftue.png new file mode 100644 index 000000000..0a7e91ccd Binary files /dev/null and b/web/app/assets/images/content/audio_capture_ftue.png differ diff --git a/web/app/assets/images/content/bkg_ftue.jpg b/web/app/assets/images/content/bkg_ftue.jpg new file mode 100644 index 000000000..e6fed91c8 Binary files /dev/null and b/web/app/assets/images/content/bkg_ftue.jpg differ diff --git a/web/app/assets/images/content/bkg_gain_slider.png b/web/app/assets/images/content/bkg_gain_slider.png new file mode 100644 index 000000000..44b20b5d6 Binary files /dev/null and b/web/app/assets/images/content/bkg_gain_slider.png differ diff --git a/web/app/assets/images/content/bkg_home_account.jpg b/web/app/assets/images/content/bkg_home_account.jpg new file mode 100644 index 000000000..8d19ddc39 Binary files /dev/null and b/web/app/assets/images/content/bkg_home_account.jpg differ diff --git a/web/app/assets/images/content/bkg_home_account_x.jpg b/web/app/assets/images/content/bkg_home_account_x.jpg new file mode 100644 index 000000000..43cc67ff6 Binary files /dev/null and b/web/app/assets/images/content/bkg_home_account_x.jpg differ diff --git a/web/app/assets/images/content/bkg_home_bands.jpg b/web/app/assets/images/content/bkg_home_bands.jpg new file mode 100644 index 000000000..9280dbe43 Binary files /dev/null and b/web/app/assets/images/content/bkg_home_bands.jpg differ diff --git a/web/app/assets/images/content/bkg_home_bands_x.jpg b/web/app/assets/images/content/bkg_home_bands_x.jpg new file mode 100644 index 000000000..24af1c19e Binary files /dev/null and b/web/app/assets/images/content/bkg_home_bands_x.jpg differ diff --git a/web/app/assets/images/content/bkg_home_create.jpg b/web/app/assets/images/content/bkg_home_create.jpg new file mode 100644 index 000000000..9a8bfade1 Binary files /dev/null and b/web/app/assets/images/content/bkg_home_create.jpg differ diff --git a/web/app/assets/images/content/bkg_home_create_disabled.jpg b/web/app/assets/images/content/bkg_home_create_disabled.jpg new file mode 100644 index 000000000..2f3946458 Binary files /dev/null and b/web/app/assets/images/content/bkg_home_create_disabled.jpg differ diff --git a/web/app/assets/images/content/bkg_home_create_x.jpg b/web/app/assets/images/content/bkg_home_create_x.jpg new file mode 100644 index 000000000..4e05cbda8 Binary files /dev/null and b/web/app/assets/images/content/bkg_home_create_x.jpg differ diff --git a/web/app/assets/images/content/bkg_home_feed.jpg b/web/app/assets/images/content/bkg_home_feed.jpg new file mode 100644 index 000000000..c02d92a43 Binary files /dev/null and b/web/app/assets/images/content/bkg_home_feed.jpg differ diff --git a/web/app/assets/images/content/bkg_home_feed_x.jpg b/web/app/assets/images/content/bkg_home_feed_x.jpg new file mode 100644 index 000000000..2535c1bc4 Binary files /dev/null and b/web/app/assets/images/content/bkg_home_feed_x.jpg differ diff --git a/web/app/assets/images/content/bkg_home_find.jpg b/web/app/assets/images/content/bkg_home_find.jpg new file mode 100644 index 000000000..0641a9458 Binary files /dev/null and b/web/app/assets/images/content/bkg_home_find.jpg differ diff --git a/web/app/assets/images/content/bkg_home_find_disabled.jpg b/web/app/assets/images/content/bkg_home_find_disabled.jpg new file mode 100644 index 000000000..50f293b55 Binary files /dev/null and b/web/app/assets/images/content/bkg_home_find_disabled.jpg differ diff --git a/web/app/assets/images/content/bkg_home_find_x.jpg b/web/app/assets/images/content/bkg_home_find_x.jpg new file mode 100644 index 000000000..fd93fbc4c Binary files /dev/null and b/web/app/assets/images/content/bkg_home_find_x.jpg differ diff --git a/web/app/assets/images/content/bkg_home_musicians.jpg b/web/app/assets/images/content/bkg_home_musicians.jpg new file mode 100644 index 000000000..700f012c6 Binary files /dev/null and b/web/app/assets/images/content/bkg_home_musicians.jpg differ diff --git a/web/app/assets/images/content/bkg_home_musicians_x.jpg b/web/app/assets/images/content/bkg_home_musicians_x.jpg new file mode 100644 index 000000000..22df61a3d Binary files /dev/null and b/web/app/assets/images/content/bkg_home_musicians_x.jpg differ diff --git a/web/app/assets/images/content/bkg_home_profile.jpg b/web/app/assets/images/content/bkg_home_profile.jpg new file mode 100644 index 000000000..18b6b3b2e Binary files /dev/null and b/web/app/assets/images/content/bkg_home_profile.jpg differ diff --git a/web/app/assets/images/content/bkg_home_profile_x.jpg b/web/app/assets/images/content/bkg_home_profile_x.jpg new file mode 100644 index 000000000..6d4fe73c7 Binary files /dev/null and b/web/app/assets/images/content/bkg_home_profile_x.jpg differ diff --git a/web/app/assets/images/content/bkg_playcontrols.png b/web/app/assets/images/content/bkg_playcontrols.png new file mode 100644 index 000000000..fcd10c8dc Binary files /dev/null and b/web/app/assets/images/content/bkg_playcontrols.png differ diff --git a/web/app/assets/images/content/bkg_slider_gain_horiz.png b/web/app/assets/images/content/bkg_slider_gain_horiz.png new file mode 100644 index 000000000..559a36200 Binary files /dev/null and b/web/app/assets/images/content/bkg_slider_gain_horiz.png differ diff --git a/web/app/assets/images/content/bkg_volume.png b/web/app/assets/images/content/bkg_volume.png new file mode 100644 index 000000000..288c08ff5 Binary files /dev/null and b/web/app/assets/images/content/bkg_volume.png differ diff --git a/web/app/assets/images/content/button_facebook.png b/web/app/assets/images/content/button_facebook.png new file mode 100644 index 000000000..436c78565 Binary files /dev/null and b/web/app/assets/images/content/button_facebook.png differ diff --git a/web/app/assets/images/content/captcha.png b/web/app/assets/images/content/captcha.png new file mode 100644 index 000000000..04df260c0 Binary files /dev/null and b/web/app/assets/images/content/captcha.png differ diff --git a/web/app/assets/images/content/computer_ftue.png b/web/app/assets/images/content/computer_ftue.png new file mode 100644 index 000000000..835555673 Binary files /dev/null and b/web/app/assets/images/content/computer_ftue.png differ diff --git a/web/app/assets/images/content/icon_account.png b/web/app/assets/images/content/icon_account.png new file mode 100644 index 000000000..fa785d6da Binary files /dev/null and b/web/app/assets/images/content/icon_account.png differ diff --git a/web/app/assets/images/content/icon_add.png b/web/app/assets/images/content/icon_add.png new file mode 100644 index 000000000..0df0af051 Binary files /dev/null and b/web/app/assets/images/content/icon_add.png differ diff --git a/web/app/assets/images/content/icon_alert.png b/web/app/assets/images/content/icon_alert.png new file mode 100644 index 000000000..41e90959e Binary files /dev/null and b/web/app/assets/images/content/icon_alert.png differ diff --git a/web/app/assets/images/content/icon_alert_big.png b/web/app/assets/images/content/icon_alert_big.png new file mode 100644 index 000000000..f768306f8 Binary files /dev/null and b/web/app/assets/images/content/icon_alert_big.png differ diff --git a/web/app/assets/images/content/icon_bands.png b/web/app/assets/images/content/icon_bands.png new file mode 100644 index 000000000..8fc608b83 Binary files /dev/null and b/web/app/assets/images/content/icon_bands.png differ diff --git a/web/app/assets/images/content/icon_check_white.png b/web/app/assets/images/content/icon_check_white.png new file mode 100644 index 000000000..c0e70cfb7 Binary files /dev/null and b/web/app/assets/images/content/icon_check_white.png differ diff --git a/web/app/assets/images/content/icon_close.png b/web/app/assets/images/content/icon_close.png new file mode 100644 index 000000000..286ac920e Binary files /dev/null and b/web/app/assets/images/content/icon_close.png differ diff --git a/web/app/assets/images/content/icon_closetrack.png b/web/app/assets/images/content/icon_closetrack.png new file mode 100644 index 000000000..c75881fbc Binary files /dev/null and b/web/app/assets/images/content/icon_closetrack.png differ diff --git a/web/app/assets/images/content/icon_facebook.png b/web/app/assets/images/content/icon_facebook.png new file mode 100644 index 000000000..dcdb73fdc Binary files /dev/null and b/web/app/assets/images/content/icon_facebook.png differ diff --git a/web/app/assets/images/content/icon_feed.png b/web/app/assets/images/content/icon_feed.png new file mode 100644 index 000000000..0cd0a5ca5 Binary files /dev/null and b/web/app/assets/images/content/icon_feed.png differ diff --git a/web/app/assets/images/content/icon_findsession.png b/web/app/assets/images/content/icon_findsession.png new file mode 100644 index 000000000..3dbffabd0 Binary files /dev/null and b/web/app/assets/images/content/icon_findsession.png differ diff --git a/web/app/assets/images/content/icon_gmail.png b/web/app/assets/images/content/icon_gmail.png new file mode 100644 index 000000000..0a4c1f97c Binary files /dev/null and b/web/app/assets/images/content/icon_gmail.png differ diff --git a/web/app/assets/images/content/icon_goodquality.png b/web/app/assets/images/content/icon_goodquality.png new file mode 100644 index 000000000..01961d218 Binary files /dev/null and b/web/app/assets/images/content/icon_goodquality.png differ diff --git a/web/app/assets/images/content/icon_google.png b/web/app/assets/images/content/icon_google.png new file mode 100644 index 000000000..40750ef36 Binary files /dev/null and b/web/app/assets/images/content/icon_google.png differ diff --git a/web/app/assets/images/content/icon_home.png b/web/app/assets/images/content/icon_home.png new file mode 100644 index 000000000..e54be652b Binary files /dev/null and b/web/app/assets/images/content/icon_home.png differ diff --git a/web/app/assets/images/content/icon_information_big.png b/web/app/assets/images/content/icon_information_big.png new file mode 100644 index 000000000..44bc24c96 Binary files /dev/null and b/web/app/assets/images/content/icon_information_big.png differ diff --git a/web/app/assets/images/content/icon_instrument_accordion24.png b/web/app/assets/images/content/icon_instrument_accordion24.png new file mode 100644 index 000000000..948c4fc9a Binary files /dev/null and b/web/app/assets/images/content/icon_instrument_accordion24.png differ diff --git a/web/app/assets/images/content/icon_instrument_accordion45.png b/web/app/assets/images/content/icon_instrument_accordion45.png new file mode 100644 index 000000000..34db5299a Binary files /dev/null and b/web/app/assets/images/content/icon_instrument_accordion45.png differ diff --git a/web/app/assets/images/content/icon_instrument_acoustic24.png b/web/app/assets/images/content/icon_instrument_acoustic24.png new file mode 100644 index 000000000..8227ee0a4 Binary files /dev/null and b/web/app/assets/images/content/icon_instrument_acoustic24.png differ diff --git a/web/app/assets/images/content/icon_instrument_acoustic45.png b/web/app/assets/images/content/icon_instrument_acoustic45.png new file mode 100644 index 000000000..3e8779181 Binary files /dev/null and b/web/app/assets/images/content/icon_instrument_acoustic45.png differ diff --git a/web/app/assets/images/content/icon_instrument_banjo24.png b/web/app/assets/images/content/icon_instrument_banjo24.png new file mode 100644 index 000000000..9b4dc7128 Binary files /dev/null and b/web/app/assets/images/content/icon_instrument_banjo24.png differ diff --git a/web/app/assets/images/content/icon_instrument_banjo45.png b/web/app/assets/images/content/icon_instrument_banjo45.png new file mode 100644 index 000000000..7da8eb1f1 Binary files /dev/null and b/web/app/assets/images/content/icon_instrument_banjo45.png differ diff --git a/web/app/assets/images/content/icon_instrument_bass24.png b/web/app/assets/images/content/icon_instrument_bass24.png new file mode 100644 index 000000000..5112fb65e Binary files /dev/null and b/web/app/assets/images/content/icon_instrument_bass24.png differ diff --git a/web/app/assets/images/content/icon_instrument_bass45.png b/web/app/assets/images/content/icon_instrument_bass45.png new file mode 100644 index 000000000..49d0938cf Binary files /dev/null and b/web/app/assets/images/content/icon_instrument_bass45.png differ diff --git a/web/app/assets/images/content/icon_instrument_cello24.png b/web/app/assets/images/content/icon_instrument_cello24.png new file mode 100644 index 000000000..c054f32ad Binary files /dev/null and b/web/app/assets/images/content/icon_instrument_cello24.png differ diff --git a/web/app/assets/images/content/icon_instrument_cello45.png b/web/app/assets/images/content/icon_instrument_cello45.png new file mode 100644 index 000000000..7c2b8e1d1 Binary files /dev/null and b/web/app/assets/images/content/icon_instrument_cello45.png differ diff --git a/web/app/assets/images/content/icon_instrument_clarinet24.png b/web/app/assets/images/content/icon_instrument_clarinet24.png new file mode 100644 index 000000000..f4aa35574 Binary files /dev/null and b/web/app/assets/images/content/icon_instrument_clarinet24.png differ diff --git a/web/app/assets/images/content/icon_instrument_clarinet45.png b/web/app/assets/images/content/icon_instrument_clarinet45.png new file mode 100644 index 000000000..0394cbeab Binary files /dev/null and b/web/app/assets/images/content/icon_instrument_clarinet45.png differ diff --git a/web/app/assets/images/content/icon_instrument_computer24.png b/web/app/assets/images/content/icon_instrument_computer24.png new file mode 100644 index 000000000..0861eccb9 Binary files /dev/null and b/web/app/assets/images/content/icon_instrument_computer24.png differ diff --git a/web/app/assets/images/content/icon_instrument_computer45.png b/web/app/assets/images/content/icon_instrument_computer45.png new file mode 100644 index 000000000..cc17b7b1c Binary files /dev/null and b/web/app/assets/images/content/icon_instrument_computer45.png differ diff --git a/web/app/assets/images/content/icon_instrument_default24.png b/web/app/assets/images/content/icon_instrument_default24.png new file mode 100644 index 000000000..7237729c3 Binary files /dev/null and b/web/app/assets/images/content/icon_instrument_default24.png differ diff --git a/web/app/assets/images/content/icon_instrument_default45.png b/web/app/assets/images/content/icon_instrument_default45.png new file mode 100644 index 000000000..14ca55d2e Binary files /dev/null and b/web/app/assets/images/content/icon_instrument_default45.png differ diff --git a/web/app/assets/images/content/icon_instrument_drums24.png b/web/app/assets/images/content/icon_instrument_drums24.png new file mode 100644 index 000000000..90b1e579d Binary files /dev/null and b/web/app/assets/images/content/icon_instrument_drums24.png differ diff --git a/web/app/assets/images/content/icon_instrument_drums45.png b/web/app/assets/images/content/icon_instrument_drums45.png new file mode 100644 index 000000000..a22db1ac9 Binary files /dev/null and b/web/app/assets/images/content/icon_instrument_drums45.png differ diff --git a/web/app/assets/images/content/icon_instrument_euphonium24.png b/web/app/assets/images/content/icon_instrument_euphonium24.png new file mode 100644 index 000000000..358cece5e Binary files /dev/null and b/web/app/assets/images/content/icon_instrument_euphonium24.png differ diff --git a/web/app/assets/images/content/icon_instrument_euphonium45.png b/web/app/assets/images/content/icon_instrument_euphonium45.png new file mode 100644 index 000000000..d040ee8cf Binary files /dev/null and b/web/app/assets/images/content/icon_instrument_euphonium45.png differ diff --git a/web/app/assets/images/content/icon_instrument_flute24.png b/web/app/assets/images/content/icon_instrument_flute24.png new file mode 100644 index 000000000..6a7e529bb Binary files /dev/null and b/web/app/assets/images/content/icon_instrument_flute24.png differ diff --git a/web/app/assets/images/content/icon_instrument_flute45.png b/web/app/assets/images/content/icon_instrument_flute45.png new file mode 100644 index 000000000..c7d4e192c Binary files /dev/null and b/web/app/assets/images/content/icon_instrument_flute45.png differ diff --git a/web/app/assets/images/content/icon_instrument_frenchhorn24.png b/web/app/assets/images/content/icon_instrument_frenchhorn24.png new file mode 100644 index 000000000..6166d1d63 Binary files /dev/null and b/web/app/assets/images/content/icon_instrument_frenchhorn24.png differ diff --git a/web/app/assets/images/content/icon_instrument_frenchhorn45.png b/web/app/assets/images/content/icon_instrument_frenchhorn45.png new file mode 100644 index 000000000..7543bbc79 Binary files /dev/null and b/web/app/assets/images/content/icon_instrument_frenchhorn45.png differ diff --git a/web/app/assets/images/content/icon_instrument_guitar24.png b/web/app/assets/images/content/icon_instrument_guitar24.png new file mode 100644 index 000000000..8048c96ef Binary files /dev/null and b/web/app/assets/images/content/icon_instrument_guitar24.png differ diff --git a/web/app/assets/images/content/icon_instrument_guitar45.png b/web/app/assets/images/content/icon_instrument_guitar45.png new file mode 100644 index 000000000..009c970b7 Binary files /dev/null and b/web/app/assets/images/content/icon_instrument_guitar45.png differ diff --git a/web/app/assets/images/content/icon_instrument_harmonica24.png b/web/app/assets/images/content/icon_instrument_harmonica24.png new file mode 100644 index 000000000..c77911cfe Binary files /dev/null and b/web/app/assets/images/content/icon_instrument_harmonica24.png differ diff --git a/web/app/assets/images/content/icon_instrument_harmonica45.png b/web/app/assets/images/content/icon_instrument_harmonica45.png new file mode 100644 index 000000000..29a6d145c Binary files /dev/null and b/web/app/assets/images/content/icon_instrument_harmonica45.png differ diff --git a/web/app/assets/images/content/icon_instrument_keyboard24.png b/web/app/assets/images/content/icon_instrument_keyboard24.png new file mode 100644 index 000000000..6f5761df9 Binary files /dev/null and b/web/app/assets/images/content/icon_instrument_keyboard24.png differ diff --git a/web/app/assets/images/content/icon_instrument_keyboard45.png b/web/app/assets/images/content/icon_instrument_keyboard45.png new file mode 100644 index 000000000..5d44d4420 Binary files /dev/null and b/web/app/assets/images/content/icon_instrument_keyboard45.png differ diff --git a/web/app/assets/images/content/icon_instrument_mandolin24.png b/web/app/assets/images/content/icon_instrument_mandolin24.png new file mode 100644 index 000000000..b8f84bdd2 Binary files /dev/null and b/web/app/assets/images/content/icon_instrument_mandolin24.png differ diff --git a/web/app/assets/images/content/icon_instrument_mandolin45.png b/web/app/assets/images/content/icon_instrument_mandolin45.png new file mode 100644 index 000000000..6dc2193e1 Binary files /dev/null and b/web/app/assets/images/content/icon_instrument_mandolin45.png differ diff --git a/web/app/assets/images/content/icon_instrument_oboe24.png b/web/app/assets/images/content/icon_instrument_oboe24.png new file mode 100644 index 000000000..fc446387c Binary files /dev/null and b/web/app/assets/images/content/icon_instrument_oboe24.png differ diff --git a/web/app/assets/images/content/icon_instrument_oboe45.png b/web/app/assets/images/content/icon_instrument_oboe45.png new file mode 100644 index 000000000..850664c33 Binary files /dev/null and b/web/app/assets/images/content/icon_instrument_oboe45.png differ diff --git a/web/app/assets/images/content/icon_instrument_other24.png b/web/app/assets/images/content/icon_instrument_other24.png new file mode 100644 index 000000000..65286141c Binary files /dev/null and b/web/app/assets/images/content/icon_instrument_other24.png differ diff --git a/web/app/assets/images/content/icon_instrument_other45.png b/web/app/assets/images/content/icon_instrument_other45.png new file mode 100644 index 000000000..67b8512c8 Binary files /dev/null and b/web/app/assets/images/content/icon_instrument_other45.png differ diff --git a/web/app/assets/images/content/icon_instrument_saxophone24.png b/web/app/assets/images/content/icon_instrument_saxophone24.png new file mode 100644 index 000000000..c736bb8ea Binary files /dev/null and b/web/app/assets/images/content/icon_instrument_saxophone24.png differ diff --git a/web/app/assets/images/content/icon_instrument_saxophone45.png b/web/app/assets/images/content/icon_instrument_saxophone45.png new file mode 100644 index 000000000..b0fd69c09 Binary files /dev/null and b/web/app/assets/images/content/icon_instrument_saxophone45.png differ diff --git a/web/app/assets/images/content/icon_instrument_trombone24.png b/web/app/assets/images/content/icon_instrument_trombone24.png new file mode 100644 index 000000000..fb2339998 Binary files /dev/null and b/web/app/assets/images/content/icon_instrument_trombone24.png differ diff --git a/web/app/assets/images/content/icon_instrument_trombone45.png b/web/app/assets/images/content/icon_instrument_trombone45.png new file mode 100644 index 000000000..3f1e7e61a Binary files /dev/null and b/web/app/assets/images/content/icon_instrument_trombone45.png differ diff --git a/web/app/assets/images/content/icon_instrument_trumpet24.png b/web/app/assets/images/content/icon_instrument_trumpet24.png new file mode 100644 index 000000000..f32966597 Binary files /dev/null and b/web/app/assets/images/content/icon_instrument_trumpet24.png differ diff --git a/web/app/assets/images/content/icon_instrument_trumpet45.png b/web/app/assets/images/content/icon_instrument_trumpet45.png new file mode 100644 index 000000000..92f7c6f19 Binary files /dev/null and b/web/app/assets/images/content/icon_instrument_trumpet45.png differ diff --git a/web/app/assets/images/content/icon_instrument_tuba24.png b/web/app/assets/images/content/icon_instrument_tuba24.png new file mode 100644 index 000000000..4d91b8834 Binary files /dev/null and b/web/app/assets/images/content/icon_instrument_tuba24.png differ diff --git a/web/app/assets/images/content/icon_instrument_tuba45.png b/web/app/assets/images/content/icon_instrument_tuba45.png new file mode 100644 index 000000000..b397edc46 Binary files /dev/null and b/web/app/assets/images/content/icon_instrument_tuba45.png differ diff --git a/web/app/assets/images/content/icon_instrument_ukelele24.png b/web/app/assets/images/content/icon_instrument_ukelele24.png new file mode 100644 index 000000000..84c390485 Binary files /dev/null and b/web/app/assets/images/content/icon_instrument_ukelele24.png differ diff --git a/web/app/assets/images/content/icon_instrument_ukelele45.png b/web/app/assets/images/content/icon_instrument_ukelele45.png new file mode 100644 index 000000000..9562e690f Binary files /dev/null and b/web/app/assets/images/content/icon_instrument_ukelele45.png differ diff --git a/web/app/assets/images/content/icon_instrument_viola24.png b/web/app/assets/images/content/icon_instrument_viola24.png new file mode 100644 index 000000000..e0dc441f5 Binary files /dev/null and b/web/app/assets/images/content/icon_instrument_viola24.png differ diff --git a/web/app/assets/images/content/icon_instrument_viola45.png b/web/app/assets/images/content/icon_instrument_viola45.png new file mode 100644 index 000000000..cf1dd5ad5 Binary files /dev/null and b/web/app/assets/images/content/icon_instrument_viola45.png differ diff --git a/web/app/assets/images/content/icon_instrument_violin24.png b/web/app/assets/images/content/icon_instrument_violin24.png new file mode 100644 index 000000000..4cdb1e1f5 Binary files /dev/null and b/web/app/assets/images/content/icon_instrument_violin24.png differ diff --git a/web/app/assets/images/content/icon_instrument_violin45.png b/web/app/assets/images/content/icon_instrument_violin45.png new file mode 100644 index 000000000..1dd7566bc Binary files /dev/null and b/web/app/assets/images/content/icon_instrument_violin45.png differ diff --git a/web/app/assets/images/content/icon_instrument_vocal24.png b/web/app/assets/images/content/icon_instrument_vocal24.png new file mode 100644 index 000000000..a4ecbe0ed Binary files /dev/null and b/web/app/assets/images/content/icon_instrument_vocal24.png differ diff --git a/web/app/assets/images/content/icon_instrument_vocal45.png b/web/app/assets/images/content/icon_instrument_vocal45.png new file mode 100644 index 000000000..082370333 Binary files /dev/null and b/web/app/assets/images/content/icon_instrument_vocal45.png differ diff --git a/web/app/assets/images/content/icon_instrument_vocals24.png b/web/app/assets/images/content/icon_instrument_vocals24.png new file mode 100644 index 000000000..0685fea2e Binary files /dev/null and b/web/app/assets/images/content/icon_instrument_vocals24.png differ diff --git a/web/app/assets/images/content/icon_instrument_vocals45.png b/web/app/assets/images/content/icon_instrument_vocals45.png new file mode 100644 index 000000000..c9413d941 Binary files /dev/null and b/web/app/assets/images/content/icon_instrument_vocals45.png differ diff --git a/web/app/assets/images/content/icon_join.png b/web/app/assets/images/content/icon_join.png new file mode 100644 index 000000000..fbf4351dd Binary files /dev/null and b/web/app/assets/images/content/icon_join.png differ diff --git a/web/app/assets/images/content/icon_lowquality.png b/web/app/assets/images/content/icon_lowquality.png new file mode 100644 index 000000000..3fcf35a1e Binary files /dev/null and b/web/app/assets/images/content/icon_lowquality.png differ diff --git a/web/app/assets/images/content/icon_musicians.png b/web/app/assets/images/content/icon_musicians.png new file mode 100644 index 000000000..3e415a64b Binary files /dev/null and b/web/app/assets/images/content/icon_musicians.png differ diff --git a/web/app/assets/images/content/icon_mute.png b/web/app/assets/images/content/icon_mute.png new file mode 100644 index 000000000..efd05612b Binary files /dev/null and b/web/app/assets/images/content/icon_mute.png differ diff --git a/web/app/assets/images/content/icon_pdf.png b/web/app/assets/images/content/icon_pdf.png new file mode 100644 index 000000000..a08a55700 Binary files /dev/null and b/web/app/assets/images/content/icon_pdf.png differ diff --git a/web/app/assets/images/content/icon_playbutton.png b/web/app/assets/images/content/icon_playbutton.png new file mode 100644 index 000000000..fa080e080 Binary files /dev/null and b/web/app/assets/images/content/icon_playbutton.png differ diff --git a/web/app/assets/images/content/icon_pr.png b/web/app/assets/images/content/icon_pr.png new file mode 100644 index 000000000..99d0e7aae Binary files /dev/null and b/web/app/assets/images/content/icon_pr.png differ diff --git a/web/app/assets/images/content/icon_product.png b/web/app/assets/images/content/icon_product.png new file mode 100644 index 000000000..739cf6200 Binary files /dev/null and b/web/app/assets/images/content/icon_product.png differ diff --git a/web/app/assets/images/content/icon_profile.png b/web/app/assets/images/content/icon_profile.png new file mode 100644 index 000000000..173d8d77a Binary files /dev/null and b/web/app/assets/images/content/icon_profile.png differ diff --git a/web/app/assets/images/content/icon_recording.png b/web/app/assets/images/content/icon_recording.png new file mode 100644 index 000000000..bb91a2f56 Binary files /dev/null and b/web/app/assets/images/content/icon_recording.png differ diff --git a/web/app/assets/images/content/icon_resync.png b/web/app/assets/images/content/icon_resync.png new file mode 100644 index 000000000..86ed62630 Binary files /dev/null and b/web/app/assets/images/content/icon_resync.png differ diff --git a/web/app/assets/images/content/icon_search.png b/web/app/assets/images/content/icon_search.png new file mode 100644 index 000000000..3dbffabd0 Binary files /dev/null and b/web/app/assets/images/content/icon_search.png differ diff --git a/web/app/assets/images/content/icon_settings_lg.png b/web/app/assets/images/content/icon_settings_lg.png new file mode 100644 index 000000000..2435d8b02 Binary files /dev/null and b/web/app/assets/images/content/icon_settings_lg.png differ diff --git a/web/app/assets/images/content/icon_settings_sm.png b/web/app/assets/images/content/icon_settings_sm.png new file mode 100644 index 000000000..6f43ad376 Binary files /dev/null and b/web/app/assets/images/content/icon_settings_sm.png differ diff --git a/web/app/assets/images/content/icon_share.png b/web/app/assets/images/content/icon_share.png new file mode 100644 index 000000000..5c73cce88 Binary files /dev/null and b/web/app/assets/images/content/icon_share.png differ diff --git a/web/app/assets/images/content/icon_twitter.png b/web/app/assets/images/content/icon_twitter.png new file mode 100644 index 000000000..0856da375 Binary files /dev/null and b/web/app/assets/images/content/icon_twitter.png differ diff --git a/web/app/assets/images/content/icon_users.png b/web/app/assets/images/content/icon_users.png new file mode 100644 index 000000000..889d5395f Binary files /dev/null and b/web/app/assets/images/content/icon_users.png differ diff --git a/web/app/assets/images/content/latency_gauge.png b/web/app/assets/images/content/latency_gauge.png new file mode 100644 index 000000000..aadb41987 Binary files /dev/null and b/web/app/assets/images/content/latency_gauge.png differ diff --git a/web/app/assets/images/content/latency_gauge_back.png b/web/app/assets/images/content/latency_gauge_back.png new file mode 100644 index 000000000..6498f5bfc Binary files /dev/null and b/web/app/assets/images/content/latency_gauge_back.png differ diff --git a/web/app/assets/images/content/latency_gauge_needle.png b/web/app/assets/images/content/latency_gauge_needle.png new file mode 100644 index 000000000..df076e4f7 Binary files /dev/null and b/web/app/assets/images/content/latency_gauge_needle.png differ diff --git a/web/app/assets/images/content/microphone_ftue.png b/web/app/assets/images/content/microphone_ftue.png new file mode 100644 index 000000000..4e15623f6 Binary files /dev/null and b/web/app/assets/images/content/microphone_ftue.png differ diff --git a/web/app/assets/images/content/recordbutton-off.png b/web/app/assets/images/content/recordbutton-off.png new file mode 100644 index 000000000..3ecd0cba9 Binary files /dev/null and b/web/app/assets/images/content/recordbutton-off.png differ diff --git a/web/app/assets/images/content/slider_gain_horiz.png b/web/app/assets/images/content/slider_gain_horiz.png new file mode 100644 index 000000000..db1da2e5a Binary files /dev/null and b/web/app/assets/images/content/slider_gain_horiz.png differ diff --git a/web/app/assets/images/content/slider_gain_vertical.png b/web/app/assets/images/content/slider_gain_vertical.png new file mode 100644 index 000000000..e0705922a Binary files /dev/null and b/web/app/assets/images/content/slider_gain_vertical.png differ diff --git a/web/app/assets/images/content/slider_playcontrols.png b/web/app/assets/images/content/slider_playcontrols.png new file mode 100644 index 000000000..a03779931 Binary files /dev/null and b/web/app/assets/images/content/slider_playcontrols.png differ diff --git a/web/app/assets/images/content/slider_volume.png b/web/app/assets/images/content/slider_volume.png new file mode 100644 index 000000000..26a847013 Binary files /dev/null and b/web/app/assets/images/content/slider_volume.png differ diff --git a/web/app/assets/images/corp/bkg_corporate.gif b/web/app/assets/images/corp/bkg_corporate.gif new file mode 100644 index 000000000..c0f94155e Binary files /dev/null and b/web/app/assets/images/corp/bkg_corporate.gif differ diff --git a/web/app/assets/images/corp/logo_corporate.png b/web/app/assets/images/corp/logo_corporate.png new file mode 100644 index 000000000..529cff728 Binary files /dev/null and b/web/app/assets/images/corp/logo_corporate.png differ diff --git a/web/app/assets/images/down_arrow.png b/web/app/assets/images/down_arrow.png new file mode 100644 index 000000000..a1da1f992 Binary files /dev/null and b/web/app/assets/images/down_arrow.png differ diff --git a/web/app/assets/images/email/header.png b/web/app/assets/images/email/header.png new file mode 100644 index 000000000..d1837ce24 Binary files /dev/null and b/web/app/assets/images/email/header.png differ diff --git a/web/app/assets/images/header/avatar_jonathon.png b/web/app/assets/images/header/avatar_jonathon.png new file mode 100644 index 000000000..4f09c1bed Binary files /dev/null and b/web/app/assets/images/header/avatar_jonathon.png differ diff --git a/web/app/assets/images/header/logo.png b/web/app/assets/images/header/logo.png new file mode 100644 index 000000000..74fc8844f Binary files /dev/null and b/web/app/assets/images/header/logo.png differ diff --git a/web/app/assets/images/isps/att.png b/web/app/assets/images/isps/att.png new file mode 100644 index 000000000..4d3a7d257 Binary files /dev/null and b/web/app/assets/images/isps/att.png differ diff --git a/web/app/assets/images/isps/cc.png b/web/app/assets/images/isps/cc.png new file mode 100644 index 000000000..8377aa38d Binary files /dev/null and b/web/app/assets/images/isps/cc.png differ diff --git a/web/app/assets/images/isps/other.jpg b/web/app/assets/images/isps/other.jpg new file mode 100644 index 000000000..222bf8f44 Binary files /dev/null and b/web/app/assets/images/isps/other.jpg differ diff --git a/web/app/assets/images/isps/tw.jpg b/web/app/assets/images/isps/tw.jpg new file mode 100644 index 000000000..327445cc2 Binary files /dev/null and b/web/app/assets/images/isps/tw.jpg differ diff --git a/web/app/assets/images/isps/vz.png b/web/app/assets/images/isps/vz.png new file mode 100644 index 000000000..0087b2e91 Binary files /dev/null and b/web/app/assets/images/isps/vz.png differ diff --git a/web/app/assets/images/logo.png b/web/app/assets/images/logo.png new file mode 100644 index 000000000..8192e9d51 Binary files /dev/null and b/web/app/assets/images/logo.png differ diff --git a/web/app/assets/images/logo_corporate.png b/web/app/assets/images/logo_corporate.png new file mode 100644 index 000000000..529cff728 Binary files /dev/null and b/web/app/assets/images/logo_corporate.png differ diff --git a/web/app/assets/images/rails.png b/web/app/assets/images/rails.png new file mode 100644 index 000000000..d5edc04e6 Binary files /dev/null and b/web/app/assets/images/rails.png differ diff --git a/web/app/assets/images/shared/avatar_creepyeye.jpg b/web/app/assets/images/shared/avatar_creepyeye.jpg new file mode 100644 index 000000000..7d911e103 Binary files /dev/null and b/web/app/assets/images/shared/avatar_creepyeye.jpg differ diff --git a/web/app/assets/images/shared/avatar_david.jpg b/web/app/assets/images/shared/avatar_david.jpg new file mode 100644 index 000000000..4489aa229 Binary files /dev/null and b/web/app/assets/images/shared/avatar_david.jpg differ diff --git a/web/app/assets/images/shared/avatar_default.jpg b/web/app/assets/images/shared/avatar_default.jpg new file mode 100644 index 000000000..9824bfb05 Binary files /dev/null and b/web/app/assets/images/shared/avatar_default.jpg differ diff --git a/web/app/assets/images/shared/avatar_generic.png b/web/app/assets/images/shared/avatar_generic.png new file mode 100644 index 000000000..6da187cb8 Binary files /dev/null and b/web/app/assets/images/shared/avatar_generic.png differ diff --git a/web/app/assets/images/shared/avatar_jonathon.png b/web/app/assets/images/shared/avatar_jonathon.png new file mode 100644 index 000000000..4f09c1bed Binary files /dev/null and b/web/app/assets/images/shared/avatar_jonathon.png differ diff --git a/web/app/assets/images/shared/avatar_saltnpepper.jpg b/web/app/assets/images/shared/avatar_saltnpepper.jpg new file mode 100644 index 000000000..dc28376ab Binary files /dev/null and b/web/app/assets/images/shared/avatar_saltnpepper.jpg differ diff --git a/web/app/assets/images/shared/avatar_silverfox.jpg b/web/app/assets/images/shared/avatar_silverfox.jpg new file mode 100644 index 000000000..3ae180e13 Binary files /dev/null and b/web/app/assets/images/shared/avatar_silverfox.jpg differ diff --git a/web/app/assets/images/shared/bkg_overlay.png b/web/app/assets/images/shared/bkg_overlay.png new file mode 100644 index 000000000..9e1cb4caf Binary files /dev/null and b/web/app/assets/images/shared/bkg_overlay.png differ diff --git a/web/app/assets/images/shared/icon_delete_sm.png b/web/app/assets/images/shared/icon_delete_sm.png new file mode 100644 index 000000000..d17c9d604 Binary files /dev/null and b/web/app/assets/images/shared/icon_delete_sm.png differ diff --git a/web/app/assets/images/shared/icon_help.png b/web/app/assets/images/shared/icon_help.png new file mode 100644 index 000000000..034d0c370 Binary files /dev/null and b/web/app/assets/images/shared/icon_help.png differ diff --git a/web/app/assets/images/shared/icon_session.png b/web/app/assets/images/shared/icon_session.png new file mode 100644 index 000000000..848f5393d Binary files /dev/null and b/web/app/assets/images/shared/icon_session.png differ diff --git a/web/app/assets/images/shared/loading-animation-4.gif b/web/app/assets/images/shared/loading-animation-4.gif new file mode 100755 index 000000000..eff340c65 Binary files /dev/null and b/web/app/assets/images/shared/loading-animation-4.gif differ diff --git a/web/app/assets/images/shared/spinner.gif b/web/app/assets/images/shared/spinner.gif new file mode 100644 index 000000000..ccc99328b Binary files /dev/null and b/web/app/assets/images/shared/spinner.gif differ diff --git a/web/app/assets/images/sidebar/expand_arrows_left.jpg b/web/app/assets/images/sidebar/expand_arrows_left.jpg new file mode 100644 index 000000000..a5a377378 Binary files /dev/null and b/web/app/assets/images/sidebar/expand_arrows_left.jpg differ diff --git a/web/app/assets/images/sidebar/expand_arrows_right.jpg b/web/app/assets/images/sidebar/expand_arrows_right.jpg new file mode 100644 index 000000000..748d228d4 Binary files /dev/null and b/web/app/assets/images/sidebar/expand_arrows_right.jpg differ diff --git a/web/app/assets/images/sidebar/icon_recording.png b/web/app/assets/images/sidebar/icon_recording.png new file mode 100644 index 000000000..a5f51c4fa Binary files /dev/null and b/web/app/assets/images/sidebar/icon_recording.png differ diff --git a/web/app/assets/javascripts/AAA_Log.js b/web/app/assets/javascripts/AAA_Log.js new file mode 100644 index 000000000..ecd63180a --- /dev/null +++ b/web/app/assets/javascripts/AAA_Log.js @@ -0,0 +1,44 @@ +(function(context, $) { + + "use strict"; + + /* + internal logger with no-ops when console is missing. + */ + context.JK = context.JK || {}; + + var console_methods = [ + 'log', 'debug', 'info', 'warn', 'error', 'assert', + 'clear', 'dir', 'dirxml', 'trace', 'group', + 'groupCollapsed', 'groupEnd', 'time', 'timeEnd', + 'timeStamp', 'profile', 'profileEnd', 'count', + 'exception', 'table' + ]; + + if ('undefined' === typeof(context.console)) { + context.console = {}; + $.each(console_methods, function(index, value) { + context.console[value] = $.noop; + }); + } + + context.JK.logger = context.console; + + // JW - some code to tone down logging. Uncomment the following, and + // then do your logging to logger.dbg - and it will be the only thing output. + // TODO - find a way to wrap this up so that debug logs can stay in, but this + // class can provide a way to enable/disable certain namespaces of logs. + /* + var fakeLogger = {}; + $.each(console_methods, function(index, value) { + fakeLogger[value] = $.noop; + }); + fakeLogger.dbg = function(m) { + context.console.debug(m); + }; + context.JK.logger = fakeLogger; + */ + + + +})(window, jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/AAB_message_factory.js b/web/app/assets/javascripts/AAB_message_factory.js new file mode 100644 index 000000000..e8cff21e8 --- /dev/null +++ b/web/app/assets/javascripts/AAB_message_factory.js @@ -0,0 +1,106 @@ +/* + Message builder for communicating over the websocket + */ +(function(context, $) { + + "use strict"; + + context.JK = context.JK || {}; + + var msg = context.JK.MessageType = { + LOGIN : "LOGIN", + LOGIN_ACK : "LOGIN_ACK", + LOGIN_MUSIC_SESSION : "LOGIN_MUSIC_SESSION", + LOGIN_MUSIC_SESSION_ACK : "LOGIN_MUSIC_SESSION_ACK", + FRIEND_SESSION_JOIN : "FRIEND_SESSION_JOIN", + MUSICIAN_SESSION_JOIN : "MUSICIAN_SESSION_JOIN", + MUSICIAN_SESSION_DEPART : "MUSICIAN_SESSION_DEPART", + LEAVE_MUSIC_SESSION : "LEAVE_MUSIC_SESSION", + LEAVE_MUSIC_SESSION_ACK : "LEAVE_MUSIC_SESSION_ACK", + HEARTBEAT : "HEARTBEAT", + HEARTBEAT_ACK : "HEARTBEAT_ACK", + FRIEND_UPDATE : "FRIEND_UPDATE", + SESSION_INVITATION : "SESSION_INVITATION", + JOIN_REQUEST : "JOIN_REQUEST", + FRIEND_REQUEST : "FRIEND_REQUEST", + FRIEND_REQUEST_ACCEPTED : "FRIEND_REQUEST_ACCEPTED", + TEST_SESSION_MESSAGE : "TEST_SESSION_MESSAGE", + PING_REQUEST : "PING_REQUEST", + PING_ACK : "PING_ACK", + PEER_MESSAGE : "PEER_MESSAGE", + SERVER_BAD_STATE_RECOVERED: "SERVER_BAD_STATE_RECOVERED", + SERVER_GENERIC_ERROR : "SERVER_GENERIC_ERROR", + SERVER_REJECTION_ERROR : "SERVER_REJECTION_ERROR", + SERVER_BAD_STATE_ERROR : "SERVER_BAD_STATE_ERROR" + }; + + var route_to = context.JK.RouteToPrefix = { + CLIENT : "client", + SESSION : "session", + SERVER : "server", + USER : "user" + }; + + var factory = {}; + + function client_container(type, target, inner) { + var type_field = type.toLowerCase(); + var body = { "type" : type, "route_to" : target}; + body[type_field] = inner; + return body; + } + + function route_to_client(client_id) { + return route_to.CLIENT + ":" + client_id; + } + + function route_to_session(session_id) { + return route_to.SESSION + ":" + session_id; + } + + // ping the provided client_id + factory.ping = function(client_id) { + var data = {}; + return client_container(msg.PING_REQUEST, route_to_client(client_id), data); + }; + + // Heartbeat message + factory.heartbeat = function() { + var data = {}; + return client_container(msg.HEARTBEAT, route_to.SERVER, data); + }; + + // create a login message using user/pass + factory.login_with_user_pass = function(username, password) { + var login = { username : username , password : password }; + return client_container(msg.LOGIN, route_to.SERVER, login); + }; + + // create a login message using token (a cookie or similiar) + // reconnect_music_session_id is an optional argument that allows the session to be immediately associated + // with a music session. + factory.login_with_token = function(token, reconnect_music_session_id) { + //context.JK.logger.debug("*** login_with_token: client_id = "+$.cookie("client_id")); + var login = { token : token, + client_id : $.cookie("client_id") + }; + return client_container(msg.LOGIN, route_to.SERVER, login); + }; + + // create a music session login message + factory.login_music_session = function(music_session) { + var login_music_session = { music_session : music_session }; + return client_container(msg.LOGIN_MUSIC_SESSION, route_to.SERVER, login_music_session); + }; + + // client-to-client message + factory.client_p2p_message = function(sender_client_id, receiver_client_id, message) { + var peer_message = { "message" : message }; + var result = client_container(msg.PEER_MESSAGE, route_to_client(receiver_client_id), peer_message); + result.from = sender_client_id; + return result; + }; + + context.JK.MessageFactory = factory; + +})(window, jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/AAC_underscore-min.js b/web/app/assets/javascripts/AAC_underscore-min.js new file mode 100644 index 000000000..c1d9d3aed --- /dev/null +++ b/web/app/assets/javascripts/AAC_underscore-min.js @@ -0,0 +1 @@ +(function(){var n=this,t=n._,r={},e=Array.prototype,u=Object.prototype,i=Function.prototype,a=e.push,o=e.slice,c=e.concat,l=u.toString,f=u.hasOwnProperty,s=e.forEach,p=e.map,h=e.reduce,v=e.reduceRight,d=e.filter,g=e.every,m=e.some,y=e.indexOf,b=e.lastIndexOf,x=Array.isArray,_=Object.keys,j=i.bind,w=function(n){return n instanceof w?n:this instanceof w?(this._wrapped=n,void 0):new w(n)};"undefined"!=typeof exports?("undefined"!=typeof module&&module.exports&&(exports=module.exports=w),exports._=w):n._=w,w.VERSION="1.4.4";var A=w.each=w.forEach=function(n,t,e){if(null!=n)if(s&&n.forEach===s)n.forEach(t,e);else if(n.length===+n.length){for(var u=0,i=n.length;i>u;u++)if(t.call(e,n[u],u,n)===r)return}else for(var a in n)if(w.has(n,a)&&t.call(e,n[a],a,n)===r)return};w.map=w.collect=function(n,t,r){var e=[];return null==n?e:p&&n.map===p?n.map(t,r):(A(n,function(n,u,i){e[e.length]=t.call(r,n,u,i)}),e)};var O="Reduce of empty array with no initial value";w.reduce=w.foldl=w.inject=function(n,t,r,e){var u=arguments.length>2;if(null==n&&(n=[]),h&&n.reduce===h)return e&&(t=w.bind(t,e)),u?n.reduce(t,r):n.reduce(t);if(A(n,function(n,i,a){u?r=t.call(e,r,n,i,a):(r=n,u=!0)}),!u)throw new TypeError(O);return r},w.reduceRight=w.foldr=function(n,t,r,e){var u=arguments.length>2;if(null==n&&(n=[]),v&&n.reduceRight===v)return e&&(t=w.bind(t,e)),u?n.reduceRight(t,r):n.reduceRight(t);var i=n.length;if(i!==+i){var a=w.keys(n);i=a.length}if(A(n,function(o,c,l){c=a?a[--i]:--i,u?r=t.call(e,r,n[c],c,l):(r=n[c],u=!0)}),!u)throw new TypeError(O);return r},w.find=w.detect=function(n,t,r){var e;return E(n,function(n,u,i){return t.call(r,n,u,i)?(e=n,!0):void 0}),e},w.filter=w.select=function(n,t,r){var e=[];return null==n?e:d&&n.filter===d?n.filter(t,r):(A(n,function(n,u,i){t.call(r,n,u,i)&&(e[e.length]=n)}),e)},w.reject=function(n,t,r){return w.filter(n,function(n,e,u){return!t.call(r,n,e,u)},r)},w.every=w.all=function(n,t,e){t||(t=w.identity);var u=!0;return null==n?u:g&&n.every===g?n.every(t,e):(A(n,function(n,i,a){return(u=u&&t.call(e,n,i,a))?void 0:r}),!!u)};var E=w.some=w.any=function(n,t,e){t||(t=w.identity);var u=!1;return null==n?u:m&&n.some===m?n.some(t,e):(A(n,function(n,i,a){return u||(u=t.call(e,n,i,a))?r:void 0}),!!u)};w.contains=w.include=function(n,t){return null==n?!1:y&&n.indexOf===y?n.indexOf(t)!=-1:E(n,function(n){return n===t})},w.invoke=function(n,t){var r=o.call(arguments,2),e=w.isFunction(t);return w.map(n,function(n){return(e?t:n[t]).apply(n,r)})},w.pluck=function(n,t){return w.map(n,function(n){return n[t]})},w.where=function(n,t,r){return w.isEmpty(t)?r?null:[]:w[r?"find":"filter"](n,function(n){for(var r in t)if(t[r]!==n[r])return!1;return!0})},w.findWhere=function(n,t){return w.where(n,t,!0)},w.max=function(n,t,r){if(!t&&w.isArray(n)&&n[0]===+n[0]&&65535>n.length)return Math.max.apply(Math,n);if(!t&&w.isEmpty(n))return-1/0;var e={computed:-1/0,value:-1/0};return A(n,function(n,u,i){var a=t?t.call(r,n,u,i):n;a>=e.computed&&(e={value:n,computed:a})}),e.value},w.min=function(n,t,r){if(!t&&w.isArray(n)&&n[0]===+n[0]&&65535>n.length)return Math.min.apply(Math,n);if(!t&&w.isEmpty(n))return 1/0;var e={computed:1/0,value:1/0};return A(n,function(n,u,i){var a=t?t.call(r,n,u,i):n;e.computed>a&&(e={value:n,computed:a})}),e.value},w.shuffle=function(n){var t,r=0,e=[];return A(n,function(n){t=w.random(r++),e[r-1]=e[t],e[t]=n}),e};var k=function(n){return w.isFunction(n)?n:function(t){return t[n]}};w.sortBy=function(n,t,r){var e=k(t);return w.pluck(w.map(n,function(n,t,u){return{value:n,index:t,criteria:e.call(r,n,t,u)}}).sort(function(n,t){var r=n.criteria,e=t.criteria;if(r!==e){if(r>e||r===void 0)return 1;if(e>r||e===void 0)return-1}return n.indexi;){var o=i+a>>>1;u>r.call(e,n[o])?i=o+1:a=o}return i},w.toArray=function(n){return n?w.isArray(n)?o.call(n):n.length===+n.length?w.map(n,w.identity):w.values(n):[]},w.size=function(n){return null==n?0:n.length===+n.length?n.length:w.keys(n).length},w.first=w.head=w.take=function(n,t,r){return null==n?void 0:null==t||r?n[0]:o.call(n,0,t)},w.initial=function(n,t,r){return o.call(n,0,n.length-(null==t||r?1:t))},w.last=function(n,t,r){return null==n?void 0:null==t||r?n[n.length-1]:o.call(n,Math.max(n.length-t,0))},w.rest=w.tail=w.drop=function(n,t,r){return o.call(n,null==t||r?1:t)},w.compact=function(n){return w.filter(n,w.identity)};var R=function(n,t,r){return A(n,function(n){w.isArray(n)?t?a.apply(r,n):R(n,t,r):r.push(n)}),r};w.flatten=function(n,t){return R(n,t,[])},w.without=function(n){return w.difference(n,o.call(arguments,1))},w.uniq=w.unique=function(n,t,r,e){w.isFunction(t)&&(e=r,r=t,t=!1);var u=r?w.map(n,r,e):n,i=[],a=[];return A(u,function(r,e){(t?e&&a[a.length-1]===r:w.contains(a,r))||(a.push(r),i.push(n[e]))}),i},w.union=function(){return w.uniq(c.apply(e,arguments))},w.intersection=function(n){var t=o.call(arguments,1);return w.filter(w.uniq(n),function(n){return w.every(t,function(t){return w.indexOf(t,n)>=0})})},w.difference=function(n){var t=c.apply(e,o.call(arguments,1));return w.filter(n,function(n){return!w.contains(t,n)})},w.zip=function(){for(var n=o.call(arguments),t=w.max(w.pluck(n,"length")),r=Array(t),e=0;t>e;e++)r[e]=w.pluck(n,""+e);return r},w.object=function(n,t){if(null==n)return{};for(var r={},e=0,u=n.length;u>e;e++)t?r[n[e]]=t[e]:r[n[e][0]]=n[e][1];return r},w.indexOf=function(n,t,r){if(null==n)return-1;var e=0,u=n.length;if(r){if("number"!=typeof r)return e=w.sortedIndex(n,t),n[e]===t?e:-1;e=0>r?Math.max(0,u+r):r}if(y&&n.indexOf===y)return n.indexOf(t,r);for(;u>e;e++)if(n[e]===t)return e;return-1},w.lastIndexOf=function(n,t,r){if(null==n)return-1;var e=null!=r;if(b&&n.lastIndexOf===b)return e?n.lastIndexOf(t,r):n.lastIndexOf(t);for(var u=e?r:n.length;u--;)if(n[u]===t)return u;return-1},w.range=function(n,t,r){1>=arguments.length&&(t=n||0,n=0),r=arguments[2]||1;for(var e=Math.max(Math.ceil((t-n)/r),0),u=0,i=Array(e);e>u;)i[u++]=n,n+=r;return i},w.bind=function(n,t){if(n.bind===j&&j)return j.apply(n,o.call(arguments,1));var r=o.call(arguments,2);return function(){return n.apply(t,r.concat(o.call(arguments)))}},w.partial=function(n){var t=o.call(arguments,1);return function(){return n.apply(this,t.concat(o.call(arguments)))}},w.bindAll=function(n){var t=o.call(arguments,1);return 0===t.length&&(t=w.functions(n)),A(t,function(t){n[t]=w.bind(n[t],n)}),n},w.memoize=function(n,t){var r={};return t||(t=w.identity),function(){var e=t.apply(this,arguments);return w.has(r,e)?r[e]:r[e]=n.apply(this,arguments)}},w.delay=function(n,t){var r=o.call(arguments,2);return setTimeout(function(){return n.apply(null,r)},t)},w.defer=function(n){return w.delay.apply(w,[n,1].concat(o.call(arguments,1)))},w.throttle=function(n,t){var r,e,u,i,a=0,o=function(){a=new Date,u=null,i=n.apply(r,e)};return function(){var c=new Date,l=t-(c-a);return r=this,e=arguments,0>=l?(clearTimeout(u),u=null,a=c,i=n.apply(r,e)):u||(u=setTimeout(o,l)),i}},w.debounce=function(n,t,r){var e,u;return function(){var i=this,a=arguments,o=function(){e=null,r||(u=n.apply(i,a))},c=r&&!e;return clearTimeout(e),e=setTimeout(o,t),c&&(u=n.apply(i,a)),u}},w.once=function(n){var t,r=!1;return function(){return r?t:(r=!0,t=n.apply(this,arguments),n=null,t)}},w.wrap=function(n,t){return function(){var r=[n];return a.apply(r,arguments),t.apply(this,r)}},w.compose=function(){var n=arguments;return function(){for(var t=arguments,r=n.length-1;r>=0;r--)t=[n[r].apply(this,t)];return t[0]}},w.after=function(n,t){return 0>=n?t():function(){return 1>--n?t.apply(this,arguments):void 0}},w.keys=_||function(n){if(n!==Object(n))throw new TypeError("Invalid object");var t=[];for(var r in n)w.has(n,r)&&(t[t.length]=r);return t},w.values=function(n){var t=[];for(var r in n)w.has(n,r)&&t.push(n[r]);return t},w.pairs=function(n){var t=[];for(var r in n)w.has(n,r)&&t.push([r,n[r]]);return t},w.invert=function(n){var t={};for(var r in n)w.has(n,r)&&(t[n[r]]=r);return t},w.functions=w.methods=function(n){var t=[];for(var r in n)w.isFunction(n[r])&&t.push(r);return t.sort()},w.extend=function(n){return A(o.call(arguments,1),function(t){if(t)for(var r in t)n[r]=t[r]}),n},w.pick=function(n){var t={},r=c.apply(e,o.call(arguments,1));return A(r,function(r){r in n&&(t[r]=n[r])}),t},w.omit=function(n){var t={},r=c.apply(e,o.call(arguments,1));for(var u in n)w.contains(r,u)||(t[u]=n[u]);return t},w.defaults=function(n){return A(o.call(arguments,1),function(t){if(t)for(var r in t)null==n[r]&&(n[r]=t[r])}),n},w.clone=function(n){return w.isObject(n)?w.isArray(n)?n.slice():w.extend({},n):n},w.tap=function(n,t){return t(n),n};var I=function(n,t,r,e){if(n===t)return 0!==n||1/n==1/t;if(null==n||null==t)return n===t;n instanceof w&&(n=n._wrapped),t instanceof w&&(t=t._wrapped);var u=l.call(n);if(u!=l.call(t))return!1;switch(u){case"[object String]":return n==t+"";case"[object Number]":return n!=+n?t!=+t:0==n?1/n==1/t:n==+t;case"[object Date]":case"[object Boolean]":return+n==+t;case"[object RegExp]":return n.source==t.source&&n.global==t.global&&n.multiline==t.multiline&&n.ignoreCase==t.ignoreCase}if("object"!=typeof n||"object"!=typeof t)return!1;for(var i=r.length;i--;)if(r[i]==n)return e[i]==t;r.push(n),e.push(t);var a=0,o=!0;if("[object Array]"==u){if(a=n.length,o=a==t.length)for(;a--&&(o=I(n[a],t[a],r,e)););}else{var c=n.constructor,f=t.constructor;if(c!==f&&!(w.isFunction(c)&&c instanceof c&&w.isFunction(f)&&f instanceof f))return!1;for(var s in n)if(w.has(n,s)&&(a++,!(o=w.has(t,s)&&I(n[s],t[s],r,e))))break;if(o){for(s in t)if(w.has(t,s)&&!a--)break;o=!a}}return r.pop(),e.pop(),o};w.isEqual=function(n,t){return I(n,t,[],[])},w.isEmpty=function(n){if(null==n)return!0;if(w.isArray(n)||w.isString(n))return 0===n.length;for(var t in n)if(w.has(n,t))return!1;return!0},w.isElement=function(n){return!(!n||1!==n.nodeType)},w.isArray=x||function(n){return"[object Array]"==l.call(n)},w.isObject=function(n){return n===Object(n)},A(["Arguments","Function","String","Number","Date","RegExp"],function(n){w["is"+n]=function(t){return l.call(t)=="[object "+n+"]"}}),w.isArguments(arguments)||(w.isArguments=function(n){return!(!n||!w.has(n,"callee"))}),"function"!=typeof/./&&(w.isFunction=function(n){return"function"==typeof n}),w.isFinite=function(n){return isFinite(n)&&!isNaN(parseFloat(n))},w.isNaN=function(n){return w.isNumber(n)&&n!=+n},w.isBoolean=function(n){return n===!0||n===!1||"[object Boolean]"==l.call(n)},w.isNull=function(n){return null===n},w.isUndefined=function(n){return n===void 0},w.has=function(n,t){return f.call(n,t)},w.noConflict=function(){return n._=t,this},w.identity=function(n){return n},w.times=function(n,t,r){for(var e=Array(n),u=0;n>u;u++)e[u]=t.call(r,u);return e},w.random=function(n,t){return null==t&&(t=n,n=0),n+Math.floor(Math.random()*(t-n+1))};var M={escape:{"&":"&","<":"<",">":">",'"':""","'":"'","/":"/"}};M.unescape=w.invert(M.escape);var S={escape:RegExp("["+w.keys(M.escape).join("")+"]","g"),unescape:RegExp("("+w.keys(M.unescape).join("|")+")","g")};w.each(["escape","unescape"],function(n){w[n]=function(t){return null==t?"":(""+t).replace(S[n],function(t){return M[n][t]})}}),w.result=function(n,t){if(null==n)return null;var r=n[t];return w.isFunction(r)?r.call(n):r},w.mixin=function(n){A(w.functions(n),function(t){var r=w[t]=n[t];w.prototype[t]=function(){var n=[this._wrapped];return a.apply(n,arguments),D.call(this,r.apply(w,n))}})};var N=0;w.uniqueId=function(n){var t=++N+"";return n?n+t:t},w.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var T=/(.)^/,q={"'":"'","\\":"\\","\r":"r","\n":"n"," ":"t","\u2028":"u2028","\u2029":"u2029"},B=/\\|'|\r|\n|\t|\u2028|\u2029/g;w.template=function(n,t,r){var e;r=w.defaults({},r,w.templateSettings);var u=RegExp([(r.escape||T).source,(r.interpolate||T).source,(r.evaluate||T).source].join("|")+"|$","g"),i=0,a="__p+='";n.replace(u,function(t,r,e,u,o){return a+=n.slice(i,o).replace(B,function(n){return"\\"+q[n]}),r&&(a+="'+\n((__t=("+r+"))==null?'':_.escape(__t))+\n'"),e&&(a+="'+\n((__t=("+e+"))==null?'':__t)+\n'"),u&&(a+="';\n"+u+"\n__p+='"),i=o+t.length,t}),a+="';\n",r.variable||(a="with(obj||{}){\n"+a+"}\n"),a="var __t,__p='',__j=Array.prototype.join,"+"print=function(){__p+=__j.call(arguments,'');};\n"+a+"return __p;\n";try{e=Function(r.variable||"obj","_",a)}catch(o){throw o.source=a,o}if(t)return e(t,w);var c=function(n){return e.call(this,n,w)};return c.source="function("+(r.variable||"obj")+"){\n"+a+"}",c},w.chain=function(n){return w(n).chain()};var D=function(n){return this._chain?w(n).chain():n};w.mixin(w),A(["pop","push","reverse","shift","sort","splice","unshift"],function(n){var t=e[n];w.prototype[n]=function(){var r=this._wrapped;return t.apply(r,arguments),"shift"!=n&&"splice"!=n||0!==r.length||delete r[0],D.call(this,r)}}),A(["concat","join","slice"],function(n){var t=e[n];w.prototype[n]=function(){return D.call(this,t.apply(this._wrapped,arguments))}}),w.extend(w.prototype,{chain:function(){return this._chain=!0,this},value:function(){return this._wrapped}})}).call(this); \ No newline at end of file diff --git a/web/app/assets/javascripts/AAC_underscore.js b/web/app/assets/javascripts/AAC_underscore.js new file mode 100644 index 000000000..32ca0c1b1 --- /dev/null +++ b/web/app/assets/javascripts/AAC_underscore.js @@ -0,0 +1,1227 @@ +// Underscore.js 1.4.4 +// =================== + +// > http://underscorejs.org +// > (c) 2009-2013 Jeremy Ashkenas, DocumentCloud Inc. +// > Underscore may be freely distributed under the MIT license. + +// Baseline setup +// -------------- +(function() { + + // Establish the root object, `window` in the browser, or `global` on the server. + var root = this; + + // Save the previous value of the `_` variable. + var previousUnderscore = root._; + + // Establish the object that gets returned to break out of a loop iteration. + var breaker = {}; + + // Save bytes in the minified (but not gzipped) version: + var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype; + + // Create quick reference variables for speed access to core prototypes. + var push = ArrayProto.push, + slice = ArrayProto.slice, + concat = ArrayProto.concat, + toString = ObjProto.toString, + hasOwnProperty = ObjProto.hasOwnProperty; + + // All **ECMAScript 5** native function implementations that we hope to use + // are declared here. + var + nativeForEach = ArrayProto.forEach, + nativeMap = ArrayProto.map, + nativeReduce = ArrayProto.reduce, + nativeReduceRight = ArrayProto.reduceRight, + nativeFilter = ArrayProto.filter, + nativeEvery = ArrayProto.every, + nativeSome = ArrayProto.some, + nativeIndexOf = ArrayProto.indexOf, + nativeLastIndexOf = ArrayProto.lastIndexOf, + nativeIsArray = Array.isArray, + nativeKeys = Object.keys, + nativeBind = FuncProto.bind; + + // Create a safe reference to the Underscore object for use below. + var _ = function(obj) { + if (obj instanceof _) return obj; + if (!(this instanceof _)) return new _(obj); + this._wrapped = obj; + }; + + // Export the Underscore object for **Node.js**, with + // backwards-compatibility for the old `require()` API. If we're in + // the browser, add `_` as a global object via a string identifier, + // for Closure Compiler "advanced" mode. + if (typeof exports !== 'undefined') { + if (typeof module !== 'undefined' && module.exports) { + exports = module.exports = _; + } + exports._ = _; + } else { + root._ = _; + } + + // Current version. + _.VERSION = '1.4.4'; + + // Collection Functions + // -------------------- + + // The cornerstone, an `each` implementation, aka `forEach`. + // Handles objects with the built-in `forEach`, arrays, and raw objects. + // Delegates to **ECMAScript 5**'s native `forEach` if available. + var each = _.each = _.forEach = function(obj, iterator, context) { + if (obj == null) return; + if (nativeForEach && obj.forEach === nativeForEach) { + obj.forEach(iterator, context); + } else if (obj.length === +obj.length) { + for (var i = 0, l = obj.length; i < l; i++) { + if (iterator.call(context, obj[i], i, obj) === breaker) return; + } + } else { + for (var key in obj) { + if (_.has(obj, key)) { + if (iterator.call(context, obj[key], key, obj) === breaker) return; + } + } + } + }; + + // Return the results of applying the iterator to each element. + // Delegates to **ECMAScript 5**'s native `map` if available. + _.map = _.collect = function(obj, iterator, context) { + var results = []; + if (obj == null) return results; + if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context); + each(obj, function(value, index, list) { + results[results.length] = iterator.call(context, value, index, list); + }); + return results; + }; + + var reduceError = 'Reduce of empty array with no initial value'; + + // **Reduce** builds up a single result from a list of values, aka `inject`, + // or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available. + _.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) { + var initial = arguments.length > 2; + if (obj == null) obj = []; + if (nativeReduce && obj.reduce === nativeReduce) { + if (context) iterator = _.bind(iterator, context); + return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator); + } + each(obj, function(value, index, list) { + if (!initial) { + memo = value; + initial = true; + } else { + memo = iterator.call(context, memo, value, index, list); + } + }); + if (!initial) throw new TypeError(reduceError); + return memo; + }; + + // The right-associative version of reduce, also known as `foldr`. + // Delegates to **ECMAScript 5**'s native `reduceRight` if available. + _.reduceRight = _.foldr = function(obj, iterator, memo, context) { + var initial = arguments.length > 2; + if (obj == null) obj = []; + if (nativeReduceRight && obj.reduceRight === nativeReduceRight) { + if (context) iterator = _.bind(iterator, context); + return initial ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator); + } + var length = obj.length; + if (length !== +length) { + var keys = _.keys(obj); + length = keys.length; + } + each(obj, function(value, index, list) { + index = keys ? keys[--length] : --length; + if (!initial) { + memo = obj[index]; + initial = true; + } else { + memo = iterator.call(context, memo, obj[index], index, list); + } + }); + if (!initial) throw new TypeError(reduceError); + return memo; + }; + + // Return the first value which passes a truth test. Aliased as `detect`. + _.find = _.detect = function(obj, iterator, context) { + var result; + any(obj, function(value, index, list) { + if (iterator.call(context, value, index, list)) { + result = value; + return true; + } + }); + return result; + }; + + // Return all the elements that pass a truth test. + // Delegates to **ECMAScript 5**'s native `filter` if available. + // Aliased as `select`. + _.filter = _.select = function(obj, iterator, context) { + var results = []; + if (obj == null) return results; + if (nativeFilter && obj.filter === nativeFilter) return obj.filter(iterator, context); + each(obj, function(value, index, list) { + if (iterator.call(context, value, index, list)) results[results.length] = value; + }); + return results; + }; + + // Return all the elements for which a truth test fails. + _.reject = function(obj, iterator, context) { + return _.filter(obj, function(value, index, list) { + return !iterator.call(context, value, index, list); + }, context); + }; + + // Determine whether all of the elements match a truth test. + // Delegates to **ECMAScript 5**'s native `every` if available. + // Aliased as `all`. + _.every = _.all = function(obj, iterator, context) { + iterator || (iterator = _.identity); + var result = true; + if (obj == null) return result; + if (nativeEvery && obj.every === nativeEvery) return obj.every(iterator, context); + each(obj, function(value, index, list) { + if (!(result = result && iterator.call(context, value, index, list))) return breaker; + }); + return !!result; + }; + + // Determine if at least one element in the object matches a truth test. + // Delegates to **ECMAScript 5**'s native `some` if available. + // Aliased as `any`. + var any = _.some = _.any = function(obj, iterator, context) { + iterator || (iterator = _.identity); + var result = false; + if (obj == null) return result; + if (nativeSome && obj.some === nativeSome) return obj.some(iterator, context); + each(obj, function(value, index, list) { + if (result || (result = iterator.call(context, value, index, list))) return breaker; + }); + return !!result; + }; + + // Determine if the array or object contains a given value (using `===`). + // Aliased as `include`. + _.contains = _.include = function(obj, target) { + if (obj == null) return false; + if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1; + return any(obj, function(value) { + return value === target; + }); + }; + + // Invoke a method (with arguments) on every item in a collection. + _.invoke = function(obj, method) { + var args = slice.call(arguments, 2); + var isFunc = _.isFunction(method); + return _.map(obj, function(value) { + return (isFunc ? method : value[method]).apply(value, args); + }); + }; + + // Convenience version of a common use case of `map`: fetching a property. + _.pluck = function(obj, key) { + return _.map(obj, function(value){ return value[key]; }); + }; + + // Convenience version of a common use case of `filter`: selecting only objects + // containing specific `key:value` pairs. + _.where = function(obj, attrs, first) { + if (_.isEmpty(attrs)) return first ? null : []; + return _[first ? 'find' : 'filter'](obj, function(value) { + for (var key in attrs) { + if (attrs[key] !== value[key]) return false; + } + return true; + }); + }; + + // Convenience version of a common use case of `find`: getting the first object + // containing specific `key:value` pairs. + _.findWhere = function(obj, attrs) { + return _.where(obj, attrs, true); + }; + + // Return the maximum element or (element-based computation). + // Can't optimize arrays of integers longer than 65,535 elements. + // See: https://bugs.webkit.org/show_bug.cgi?id=80797 + _.max = function(obj, iterator, context) { + if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) { + return Math.max.apply(Math, obj); + } + if (!iterator && _.isEmpty(obj)) return -Infinity; + var result = {computed : -Infinity, value: -Infinity}; + each(obj, function(value, index, list) { + var computed = iterator ? iterator.call(context, value, index, list) : value; + computed >= result.computed && (result = {value : value, computed : computed}); + }); + return result.value; + }; + + // Return the minimum element (or element-based computation). + _.min = function(obj, iterator, context) { + if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) { + return Math.min.apply(Math, obj); + } + if (!iterator && _.isEmpty(obj)) return Infinity; + var result = {computed : Infinity, value: Infinity}; + each(obj, function(value, index, list) { + var computed = iterator ? iterator.call(context, value, index, list) : value; + computed < result.computed && (result = {value : value, computed : computed}); + }); + return result.value; + }; + + // Shuffle an array. + _.shuffle = function(obj) { + var rand; + var index = 0; + var shuffled = []; + each(obj, function(value) { + rand = _.random(index++); + shuffled[index - 1] = shuffled[rand]; + shuffled[rand] = value; + }); + return shuffled; + }; + + // An internal function to generate lookup iterators. + var lookupIterator = function(value) { + return _.isFunction(value) ? value : function(obj){ return obj[value]; }; + }; + + // Sort the object's values by a criterion produced by an iterator. + _.sortBy = function(obj, value, context) { + var iterator = lookupIterator(value); + return _.pluck(_.map(obj, function(value, index, list) { + return { + value : value, + index : index, + criteria : iterator.call(context, value, index, list) + }; + }).sort(function(left, right) { + var a = left.criteria; + var b = right.criteria; + if (a !== b) { + if (a > b || a === void 0) return 1; + if (a < b || b === void 0) return -1; + } + return left.index < right.index ? -1 : 1; + }), 'value'); + }; + + // An internal function used for aggregate "group by" operations. + var group = function(obj, value, context, behavior) { + var result = {}; + var iterator = lookupIterator(value || _.identity); + each(obj, function(value, index) { + var key = iterator.call(context, value, index, obj); + behavior(result, key, value); + }); + return result; + }; + + // Groups the object's values by a criterion. Pass either a string attribute + // to group by, or a function that returns the criterion. + _.groupBy = function(obj, value, context) { + return group(obj, value, context, function(result, key, value) { + (_.has(result, key) ? result[key] : (result[key] = [])).push(value); + }); + }; + + // Counts instances of an object that group by a certain criterion. Pass + // either a string attribute to count by, or a function that returns the + // criterion. + _.countBy = function(obj, value, context) { + return group(obj, value, context, function(result, key) { + if (!_.has(result, key)) result[key] = 0; + result[key]++; + }); + }; + + // Use a comparator function to figure out the smallest index at which + // an object should be inserted so as to maintain order. Uses binary search. + _.sortedIndex = function(array, obj, iterator, context) { + iterator = iterator == null ? _.identity : lookupIterator(iterator); + var value = iterator.call(context, obj); + var low = 0, high = array.length; + while (low < high) { + var mid = (low + high) >>> 1; + iterator.call(context, array[mid]) < value ? low = mid + 1 : high = mid; + } + return low; + }; + + // Safely convert anything iterable into a real, live array. + _.toArray = function(obj) { + if (!obj) return []; + if (_.isArray(obj)) return slice.call(obj); + if (obj.length === +obj.length) return _.map(obj, _.identity); + return _.values(obj); + }; + + // Return the number of elements in an object. + _.size = function(obj) { + if (obj == null) return 0; + return (obj.length === +obj.length) ? obj.length : _.keys(obj).length; + }; + + // Array Functions + // --------------- + + // Get the first element of an array. Passing **n** will return the first N + // values in the array. Aliased as `head` and `take`. The **guard** check + // allows it to work with `_.map`. + _.first = _.head = _.take = function(array, n, guard) { + if (array == null) return void 0; + return (n != null) && !guard ? slice.call(array, 0, n) : array[0]; + }; + + // Returns everything but the last entry of the array. Especially useful on + // the arguments object. Passing **n** will return all the values in + // the array, excluding the last N. The **guard** check allows it to work with + // `_.map`. + _.initial = function(array, n, guard) { + return slice.call(array, 0, array.length - ((n == null) || guard ? 1 : n)); + }; + + // Get the last element of an array. Passing **n** will return the last N + // values in the array. The **guard** check allows it to work with `_.map`. + _.last = function(array, n, guard) { + if (array == null) return void 0; + if ((n != null) && !guard) { + return slice.call(array, Math.max(array.length - n, 0)); + } else { + return array[array.length - 1]; + } + }; + + // Returns everything but the first entry of the array. Aliased as `tail` and `drop`. + // Especially useful on the arguments object. Passing an **n** will return + // the rest N values in the array. The **guard** + // check allows it to work with `_.map`. + _.rest = _.tail = _.drop = function(array, n, guard) { + return slice.call(array, (n == null) || guard ? 1 : n); + }; + + // Trim out all falsy values from an array. + _.compact = function(array) { + return _.filter(array, _.identity); + }; + + // Internal implementation of a recursive `flatten` function. + var flatten = function(input, shallow, output) { + each(input, function(value) { + if (_.isArray(value)) { + shallow ? push.apply(output, value) : flatten(value, shallow, output); + } else { + output.push(value); + } + }); + return output; + }; + + // Return a completely flattened version of an array. + _.flatten = function(array, shallow) { + return flatten(array, shallow, []); + }; + + // Return a version of the array that does not contain the specified value(s). + _.without = function(array) { + return _.difference(array, slice.call(arguments, 1)); + }; + + // Produce a duplicate-free version of the array. If the array has already + // been sorted, you have the option of using a faster algorithm. + // Aliased as `unique`. + _.uniq = _.unique = function(array, isSorted, iterator, context) { + if (_.isFunction(isSorted)) { + context = iterator; + iterator = isSorted; + isSorted = false; + } + var initial = iterator ? _.map(array, iterator, context) : array; + var results = []; + var seen = []; + each(initial, function(value, index) { + if (isSorted ? (!index || seen[seen.length - 1] !== value) : !_.contains(seen, value)) { + seen.push(value); + results.push(array[index]); + } + }); + return results; + }; + + // Produce an array that contains the union: each distinct element from all of + // the passed-in arrays. + _.union = function() { + return _.uniq(concat.apply(ArrayProto, arguments)); + }; + + // Produce an array that contains every item shared between all the + // passed-in arrays. + _.intersection = function(array) { + var rest = slice.call(arguments, 1); + return _.filter(_.uniq(array), function(item) { + return _.every(rest, function(other) { + return _.indexOf(other, item) >= 0; + }); + }); + }; + + // Take the difference between one array and a number of other arrays. + // Only the elements present in just the first array will remain. + _.difference = function(array) { + var rest = concat.apply(ArrayProto, slice.call(arguments, 1)); + return _.filter(array, function(value){ return !_.contains(rest, value); }); + }; + + // Zip together multiple lists into a single array -- elements that share + // an index go together. + _.zip = function() { + var args = slice.call(arguments); + var length = _.max(_.pluck(args, 'length')); + var results = new Array(length); + for (var i = 0; i < length; i++) { + results[i] = _.pluck(args, "" + i); + } + return results; + }; + + // Converts lists into objects. Pass either a single array of `[key, value]` + // pairs, or two parallel arrays of the same length -- one of keys, and one of + // the corresponding values. + _.object = function(list, values) { + if (list == null) return {}; + var result = {}; + for (var i = 0, l = list.length; i < l; i++) { + if (values) { + result[list[i]] = values[i]; + } else { + result[list[i][0]] = list[i][1]; + } + } + return result; + }; + + // If the browser doesn't supply us with indexOf (I'm looking at you, **MSIE**), + // we need this function. Return the position of the first occurrence of an + // item in an array, or -1 if the item is not included in the array. + // Delegates to **ECMAScript 5**'s native `indexOf` if available. + // If the array is large and already in sort order, pass `true` + // for **isSorted** to use binary search. + _.indexOf = function(array, item, isSorted) { + if (array == null) return -1; + var i = 0, l = array.length; + if (isSorted) { + if (typeof isSorted == 'number') { + i = (isSorted < 0 ? Math.max(0, l + isSorted) : isSorted); + } else { + i = _.sortedIndex(array, item); + return array[i] === item ? i : -1; + } + } + if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item, isSorted); + for (; i < l; i++) if (array[i] === item) return i; + return -1; + }; + + // Delegates to **ECMAScript 5**'s native `lastIndexOf` if available. + _.lastIndexOf = function(array, item, from) { + if (array == null) return -1; + var hasIndex = from != null; + if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) { + return hasIndex ? array.lastIndexOf(item, from) : array.lastIndexOf(item); + } + var i = (hasIndex ? from : array.length); + while (i--) if (array[i] === item) return i; + return -1; + }; + + // Generate an integer Array containing an arithmetic progression. A port of + // the native Python `range()` function. See + // [the Python documentation](http://docs.python.org/library/functions.html#range). + _.range = function(start, stop, step) { + if (arguments.length <= 1) { + stop = start || 0; + start = 0; + } + step = arguments[2] || 1; + + var len = Math.max(Math.ceil((stop - start) / step), 0); + var idx = 0; + var range = new Array(len); + + while(idx < len) { + range[idx++] = start; + start += step; + } + + return range; + }; + + // Function (ahem) Functions + // ------------------ + + // Create a function bound to a given object (assigning `this`, and arguments, + // optionally). Delegates to **ECMAScript 5**'s native `Function.bind` if + // available. + _.bind = function(func, context) { + if (func.bind === nativeBind && nativeBind) return nativeBind.apply(func, slice.call(arguments, 1)); + var args = slice.call(arguments, 2); + return function() { + return func.apply(context, args.concat(slice.call(arguments))); + }; + }; + + // Partially apply a function by creating a version that has had some of its + // arguments pre-filled, without changing its dynamic `this` context. + _.partial = function(func) { + var args = slice.call(arguments, 1); + return function() { + return func.apply(this, args.concat(slice.call(arguments))); + }; + }; + + // Bind all of an object's methods to that object. Useful for ensuring that + // all callbacks defined on an object belong to it. + _.bindAll = function(obj) { + var funcs = slice.call(arguments, 1); + if (funcs.length === 0) funcs = _.functions(obj); + each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); }); + return obj; + }; + + // Memoize an expensive function by storing its results. + _.memoize = function(func, hasher) { + var memo = {}; + hasher || (hasher = _.identity); + return function() { + var key = hasher.apply(this, arguments); + return _.has(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments)); + }; + }; + + // Delays a function for the given number of milliseconds, and then calls + // it with the arguments supplied. + _.delay = function(func, wait) { + var args = slice.call(arguments, 2); + return setTimeout(function(){ return func.apply(null, args); }, wait); + }; + + // Defers a function, scheduling it to run after the current call stack has + // cleared. + _.defer = function(func) { + return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1))); + }; + + // Returns a function, that, when invoked, will only be triggered at most once + // during a given window of time. + _.throttle = function(func, wait) { + var context, args, timeout, result; + var previous = 0; + var later = function() { + previous = new Date; + timeout = null; + result = func.apply(context, args); + }; + return function() { + var now = new Date; + var remaining = wait - (now - previous); + context = this; + args = arguments; + if (remaining <= 0) { + clearTimeout(timeout); + timeout = null; + previous = now; + result = func.apply(context, args); + } else if (!timeout) { + timeout = setTimeout(later, remaining); + } + return result; + }; + }; + + // Returns a function, that, as long as it continues to be invoked, will not + // be triggered. The function will be called after it stops being called for + // N milliseconds. If `immediate` is passed, trigger the function on the + // leading edge, instead of the trailing. + _.debounce = function(func, wait, immediate) { + var timeout, result; + return function() { + var context = this, args = arguments; + var later = function() { + timeout = null; + if (!immediate) result = func.apply(context, args); + }; + var callNow = immediate && !timeout; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + if (callNow) result = func.apply(context, args); + return result; + }; + }; + + // Returns a function that will be executed at most one time, no matter how + // often you call it. Useful for lazy initialization. + _.once = function(func) { + var ran = false, memo; + return function() { + if (ran) return memo; + ran = true; + memo = func.apply(this, arguments); + func = null; + return memo; + }; + }; + + // Returns the first function passed as an argument to the second, + // allowing you to adjust arguments, run code before and after, and + // conditionally execute the original function. + _.wrap = function(func, wrapper) { + return function() { + var args = [func]; + push.apply(args, arguments); + return wrapper.apply(this, args); + }; + }; + + // Returns a function that is the composition of a list of functions, each + // consuming the return value of the function that follows. + _.compose = function() { + var funcs = arguments; + return function() { + var args = arguments; + for (var i = funcs.length - 1; i >= 0; i--) { + args = [funcs[i].apply(this, args)]; + } + return args[0]; + }; + }; + + // Returns a function that will only be executed after being called N times. + _.after = function(times, func) { + if (times <= 0) return func(); + return function() { + if (--times < 1) { + return func.apply(this, arguments); + } + }; + }; + + // Object Functions + // ---------------- + + // Retrieve the names of an object's properties. + // Delegates to **ECMAScript 5**'s native `Object.keys` + _.keys = nativeKeys || function(obj) { + if (obj !== Object(obj)) throw new TypeError('Invalid object'); + var keys = []; + for (var key in obj) if (_.has(obj, key)) keys[keys.length] = key; + return keys; + }; + + // Retrieve the values of an object's properties. + _.values = function(obj) { + var values = []; + for (var key in obj) if (_.has(obj, key)) values.push(obj[key]); + return values; + }; + + // Convert an object into a list of `[key, value]` pairs. + _.pairs = function(obj) { + var pairs = []; + for (var key in obj) if (_.has(obj, key)) pairs.push([key, obj[key]]); + return pairs; + }; + + // Invert the keys and values of an object. The values must be serializable. + _.invert = function(obj) { + var result = {}; + for (var key in obj) if (_.has(obj, key)) result[obj[key]] = key; + return result; + }; + + // Return a sorted list of the function names available on the object. + // Aliased as `methods` + _.functions = _.methods = function(obj) { + var names = []; + for (var key in obj) { + if (_.isFunction(obj[key])) names.push(key); + } + return names.sort(); + }; + + // Extend a given object with all the properties in passed-in object(s). + _.extend = function(obj) { + each(slice.call(arguments, 1), function(source) { + if (source) { + for (var prop in source) { + obj[prop] = source[prop]; + } + } + }); + return obj; + }; + + // Return a copy of the object only containing the whitelisted properties. + _.pick = function(obj) { + var copy = {}; + var keys = concat.apply(ArrayProto, slice.call(arguments, 1)); + each(keys, function(key) { + if (key in obj) copy[key] = obj[key]; + }); + return copy; + }; + + // Return a copy of the object without the blacklisted properties. + _.omit = function(obj) { + var copy = {}; + var keys = concat.apply(ArrayProto, slice.call(arguments, 1)); + for (var key in obj) { + if (!_.contains(keys, key)) copy[key] = obj[key]; + } + return copy; + }; + + // Fill in a given object with default properties. + _.defaults = function(obj) { + each(slice.call(arguments, 1), function(source) { + if (source) { + for (var prop in source) { + if (obj[prop] == null) obj[prop] = source[prop]; + } + } + }); + return obj; + }; + + // Create a (shallow-cloned) duplicate of an object. + _.clone = function(obj) { + if (!_.isObject(obj)) return obj; + return _.isArray(obj) ? obj.slice() : _.extend({}, obj); + }; + + // Invokes interceptor with the obj, and then returns obj. + // The primary purpose of this method is to "tap into" a method chain, in + // order to perform operations on intermediate results within the chain. + _.tap = function(obj, interceptor) { + interceptor(obj); + return obj; + }; + + // Internal recursive comparison function for `isEqual`. + var eq = function(a, b, aStack, bStack) { + // Identical objects are equal. `0 === -0`, but they aren't identical. + // See the Harmony `egal` proposal: http://wiki.ecmascript.org/doku.php?id=harmony:egal. + if (a === b) return a !== 0 || 1 / a == 1 / b; + // A strict comparison is necessary because `null == undefined`. + if (a == null || b == null) return a === b; + // Unwrap any wrapped objects. + if (a instanceof _) a = a._wrapped; + if (b instanceof _) b = b._wrapped; + // Compare `[[Class]]` names. + var className = toString.call(a); + if (className != toString.call(b)) return false; + switch (className) { + // Strings, numbers, dates, and booleans are compared by value. + case '[object String]': + // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is + // equivalent to `new String("5")`. + return a == String(b); + case '[object Number]': + // `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for + // other numeric values. + return a != +a ? b != +b : (a == 0 ? 1 / a == 1 / b : a == +b); + case '[object Date]': + case '[object Boolean]': + // Coerce dates and booleans to numeric primitive values. Dates are compared by their + // millisecond representations. Note that invalid dates with millisecond representations + // of `NaN` are not equivalent. + return +a == +b; + // RegExps are compared by their source patterns and flags. + case '[object RegExp]': + return a.source == b.source && + a.global == b.global && + a.multiline == b.multiline && + a.ignoreCase == b.ignoreCase; + } + if (typeof a != 'object' || typeof b != 'object') return false; + // Assume equality for cyclic structures. The algorithm for detecting cyclic + // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. + var length = aStack.length; + while (length--) { + // Linear search. Performance is inversely proportional to the number of + // unique nested structures. + if (aStack[length] == a) return bStack[length] == b; + } + // Add the first object to the stack of traversed objects. + aStack.push(a); + bStack.push(b); + var size = 0, result = true; + // Recursively compare objects and arrays. + if (className == '[object Array]') { + // Compare array lengths to determine if a deep comparison is necessary. + size = a.length; + result = size == b.length; + if (result) { + // Deep compare the contents, ignoring non-numeric properties. + while (size--) { + if (!(result = eq(a[size], b[size], aStack, bStack))) break; + } + } + } else { + // Objects with different constructors are not equivalent, but `Object`s + // from different frames are. + var aCtor = a.constructor, bCtor = b.constructor; + if (aCtor !== bCtor && !(_.isFunction(aCtor) && (aCtor instanceof aCtor) && + _.isFunction(bCtor) && (bCtor instanceof bCtor))) { + return false; + } + // Deep compare objects. + for (var key in a) { + if (_.has(a, key)) { + // Count the expected number of properties. + size++; + // Deep compare each member. + if (!(result = _.has(b, key) && eq(a[key], b[key], aStack, bStack))) break; + } + } + // Ensure that both objects contain the same number of properties. + if (result) { + for (key in b) { + if (_.has(b, key) && !(size--)) break; + } + result = !size; + } + } + // Remove the first object from the stack of traversed objects. + aStack.pop(); + bStack.pop(); + return result; + }; + + // Perform a deep comparison to check if two objects are equal. + _.isEqual = function(a, b) { + return eq(a, b, [], []); + }; + + // Is a given array, string, or object empty? + // An "empty" object has no enumerable own-properties. + _.isEmpty = function(obj) { + if (obj == null) return true; + if (_.isArray(obj) || _.isString(obj)) return obj.length === 0; + for (var key in obj) if (_.has(obj, key)) return false; + return true; + }; + + // Is a given value a DOM element? + _.isElement = function(obj) { + return !!(obj && obj.nodeType === 1); + }; + + // Is a given value an array? + // Delegates to ECMA5's native Array.isArray + _.isArray = nativeIsArray || function(obj) { + return toString.call(obj) == '[object Array]'; + }; + + // Is a given variable an object? + _.isObject = function(obj) { + return obj === Object(obj); + }; + + // Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp. + each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp'], function(name) { + _['is' + name] = function(obj) { + return toString.call(obj) == '[object ' + name + ']'; + }; + }); + + // Define a fallback version of the method in browsers (ahem, IE), where + // there isn't any inspectable "Arguments" type. + if (!_.isArguments(arguments)) { + _.isArguments = function(obj) { + return !!(obj && _.has(obj, 'callee')); + }; + } + + // Optimize `isFunction` if appropriate. + if (typeof (/./) !== 'function') { + _.isFunction = function(obj) { + return typeof obj === 'function'; + }; + } + + // Is a given object a finite number? + _.isFinite = function(obj) { + return isFinite(obj) && !isNaN(parseFloat(obj)); + }; + + // Is the given value `NaN`? (NaN is the only number which does not equal itself). + _.isNaN = function(obj) { + return _.isNumber(obj) && obj != +obj; + }; + + // Is a given value a boolean? + _.isBoolean = function(obj) { + return obj === true || obj === false || toString.call(obj) == '[object Boolean]'; + }; + + // Is a given value equal to null? + _.isNull = function(obj) { + return obj === null; + }; + + // Is a given variable undefined? + _.isUndefined = function(obj) { + return obj === void 0; + }; + + // Shortcut function for checking if an object has a given property directly + // on itself (in other words, not on a prototype). + _.has = function(obj, key) { + return hasOwnProperty.call(obj, key); + }; + + // Utility Functions + // ----------------- + + // Run Underscore.js in *noConflict* mode, returning the `_` variable to its + // previous owner. Returns a reference to the Underscore object. + _.noConflict = function() { + root._ = previousUnderscore; + return this; + }; + + // Keep the identity function around for default iterators. + _.identity = function(value) { + return value; + }; + + // Run a function **n** times. + _.times = function(n, iterator, context) { + var accum = Array(n); + for (var i = 0; i < n; i++) accum[i] = iterator.call(context, i); + return accum; + }; + + // Return a random integer between min and max (inclusive). + _.random = function(min, max) { + if (max == null) { + max = min; + min = 0; + } + return min + Math.floor(Math.random() * (max - min + 1)); + }; + + // List of HTML entities for escaping. + var entityMap = { + escape: { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + '/': '/' + } + }; + entityMap.unescape = _.invert(entityMap.escape); + + // Regexes containing the keys and values listed immediately above. + var entityRegexes = { + escape: new RegExp('[' + _.keys(entityMap.escape).join('') + ']', 'g'), + unescape: new RegExp('(' + _.keys(entityMap.unescape).join('|') + ')', 'g') + }; + + // Functions for escaping and unescaping strings to/from HTML interpolation. + _.each(['escape', 'unescape'], function(method) { + _[method] = function(string) { + if (string == null) return ''; + return ('' + string).replace(entityRegexes[method], function(match) { + return entityMap[method][match]; + }); + }; + }); + + // If the value of the named property is a function then invoke it; + // otherwise, return it. + _.result = function(object, property) { + if (object == null) return null; + var value = object[property]; + return _.isFunction(value) ? value.call(object) : value; + }; + + // Add your own custom functions to the Underscore object. + _.mixin = function(obj) { + each(_.functions(obj), function(name){ + var func = _[name] = obj[name]; + _.prototype[name] = function() { + var args = [this._wrapped]; + push.apply(args, arguments); + return result.call(this, func.apply(_, args)); + }; + }); + }; + + // Generate a unique integer id (unique within the entire client session). + // Useful for temporary DOM ids. + var idCounter = 0; + _.uniqueId = function(prefix) { + var id = ++idCounter + ''; + return prefix ? prefix + id : id; + }; + + // By default, Underscore uses ERB-style template delimiters, change the + // following template settings to use alternative delimiters. + _.templateSettings = { + evaluate : /<%([\s\S]+?)%>/g, + interpolate : /<%=([\s\S]+?)%>/g, + escape : /<%-([\s\S]+?)%>/g + }; + + // When customizing `templateSettings`, if you don't want to define an + // interpolation, evaluation or escaping regex, we need one that is + // guaranteed not to match. + var noMatch = /(.)^/; + + // Certain characters need to be escaped so that they can be put into a + // string literal. + var escapes = { + "'": "'", + '\\': '\\', + '\r': 'r', + '\n': 'n', + '\t': 't', + '\u2028': 'u2028', + '\u2029': 'u2029' + }; + + var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g; + + // JavaScript micro-templating, similar to John Resig's implementation. + // Underscore templating handles arbitrary delimiters, preserves whitespace, + // and correctly escapes quotes within interpolated code. + _.template = function(text, data, settings) { + var render; + settings = _.defaults({}, settings, _.templateSettings); + + // Combine delimiters into one regular expression via alternation. + var matcher = new RegExp([ + (settings.escape || noMatch).source, + (settings.interpolate || noMatch).source, + (settings.evaluate || noMatch).source + ].join('|') + '|$', 'g'); + + // Compile the template source, escaping string literals appropriately. + var index = 0; + var source = "__p+='"; + text.replace(matcher, function(match, escape, interpolate, evaluate, offset) { + source += text.slice(index, offset) + .replace(escaper, function(match) { return '\\' + escapes[match]; }); + + if (escape) { + source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'"; + } + if (interpolate) { + source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'"; + } + if (evaluate) { + source += "';\n" + evaluate + "\n__p+='"; + } + index = offset + match.length; + return match; + }); + source += "';\n"; + + // If a variable is not specified, place data values in local scope. + if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n'; + + source = "var __t,__p='',__j=Array.prototype.join," + + "print=function(){__p+=__j.call(arguments,'');};\n" + + source + "return __p;\n"; + + try { + render = new Function(settings.variable || 'obj', '_', source); + } catch (e) { + e.source = source; + throw e; + } + + if (data) return render(data, _); + var template = function(data) { + return render.call(this, data, _); + }; + + // Provide the compiled function source as a convenience for precompilation. + template.source = 'function(' + (settings.variable || 'obj') + '){\n' + source + '}'; + + return template; + }; + + // Add a "chain" function, which will delegate to the wrapper. + _.chain = function(obj) { + return _(obj).chain(); + }; + + // OOP + // --------------- + // If Underscore is called as a function, it returns a wrapped object that + // can be used OO-style. This wrapper holds altered versions of all the + // underscore functions. Wrapped objects may be chained. + + // Helper function to continue chaining intermediate results. + var result = function(obj) { + return this._chain ? _(obj).chain() : obj; + }; + + // Add all of the Underscore functions to the wrapper object. + _.mixin(_); + + // Add all mutator Array functions to the wrapper. + each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) { + var method = ArrayProto[name]; + _.prototype[name] = function() { + var obj = this._wrapped; + method.apply(obj, arguments); + if ((name == 'shift' || name == 'splice') && obj.length === 0) delete obj[0]; + return result.call(this, obj); + }; + }); + + // Add all accessor Array functions to the wrapper. + each(['concat', 'join', 'slice'], function(name) { + var method = ArrayProto[name]; + _.prototype[name] = function() { + return result.call(this, method.apply(this._wrapped, arguments)); + }; + }); + + _.extend(_.prototype, { + + // Start chaining a wrapped Underscore object. + chain: function() { + this._chain = true; + return this; + }, + + // Extracts the result from a wrapped and chained object. + value: function() { + return this._wrapped; + } + + }); + +}).call(this); diff --git a/web/app/assets/javascripts/JamServer.js b/web/app/assets/javascripts/JamServer.js new file mode 100644 index 000000000..651ad616d --- /dev/null +++ b/web/app/assets/javascripts/JamServer.js @@ -0,0 +1,195 @@ +// The wrapper around the web-socket connection to the server +(function(context, $) { + + "use strict"; + + context.JK = context.JK || {}; + + var logger = context.JK.logger; + var msg_factory = context.JK.MessageFactory; + + // Let socket.io know where WebSocketMain.swf is + context.WEB_SOCKET_SWF_LOCATION = "assets/flash/WebSocketMain.swf"; + + var server = {}; + server.socket = {}; + server.signedIn = false; + server.clientID = ""; + server.publicIP = ""; + server.dispatchTable = {}; + server.socketClosedListeners = []; + server.connected = false; + + + // handles logic if the websocket connection closes, and if it was in error then also prompt for reconnect + function closedCleanup(in_error) { + if(server.connected) { + server.connected = false; + + context.JK.CurrentSessionModel.onWebsocketDisconnected(in_error); + + // notify anyone listening that the socket closed + var len = server.socketClosedListeners.length; + for(var i = 0; i < len; i++) { + try { + server.socketClosedListeners[i](in_error); + } catch (ex) { + logger.warn('exception in callback for websocket closed event:' + ex); + } + } + } + } + + server.registerOnSocketClosed = function(callback) { + server.socketClosedListeners.push(callback); + } + + server.registerMessageCallback = function(messageType, callback) { + if (server.dispatchTable[messageType] === undefined) { + server.dispatchTable[messageType] = []; + } + + server.dispatchTable[messageType].push(callback); + }; + + server.unregisterMessageCallback = function(messageType, callback) { + if (server.dispatchTable[messageType] !== undefined) { + for(var i = server.dispatchTable[messageType].length; i--;) { + if (server.dispatchTable[messageType][i] === callback) + { + server.dispatchTable[messageType].splice(i, 1); + break; + } + } + } + + if (server.dispatchTable[messageType].length === 0) { + delete server.dispatchTable[messageType]; + } + }; + + server.connect = function() { + logger.log("server.connect"); + var uri = context.JK.websocket_gateway_uri; // Set in index.html.erb. + //var uri = context.gon.websocket_gateway_uri; // Leaving here for now, as we're looking for a better solution. + server.socket = new context.WebSocket(uri); + server.socket.onopen = server.onOpen; + server.socket.onmessage = server.onMessage; + server.socket.onclose = server.onClose; + }; + + server.close = function(in_error) { + logger.log("closing websocket"); + + server.socket.close(); + + closedCleanup(in_error); + } + + server.rememberLogin = function() { + var token, loginMessage; + token = $.cookie("remember_token"); + loginMessage = msg_factory.login_with_token(token, null); + server.send(loginMessage); + }; + + server.onOpen = function() { + logger.log("server.onOpen"); + server.rememberLogin(); + }; + + server.onMessage = function(e) { + var message = JSON.parse(e.data), + messageType = message.type.toLowerCase(), + payload = message[messageType], + callbacks = server.dispatchTable[message.type]; + + logger.log("server.onMessage:" + messageType + " payload:" + JSON.stringify(payload)); + + if (callbacks !== undefined) { + var len = callbacks.length; + for(var i = 0; i < len; i++) { + try { + callbacks[i](message, payload); + } catch (ex) { + logger.warn('exception in callback for websocket message:' + ex); + } + } + } + else { + logger.log("Unexpected message type %s.", message.type); + } + }; + + server.onClose = function() { + logger.log("Socket to server closed."); + + closedCleanup(true); + }; + + server.send = function(message) { + + var jsMessage = JSON.stringify(message); + + logger.log("server.send(" + jsMessage + ")"); + if (server !== undefined && server.socket !== undefined && server.socket.send !== undefined) { + server.socket.send(jsMessage); + } else { + logger.log("Dropped message because server connection is closed."); + } + }; + + server.loginSession = function(sessionId) { + var loginMessage; + + if (!server.signedIn) { + logger.log("Not signed in!"); + // TODO: surface the error + return; + } + + loginMessage = msg_factory.login_jam_session(sessionId); + server.send(loginMessage); + }; + + server.sendP2PMessage = function(receiver_id, message) { + logger.log("P2P message from [" + server.clientID + "] to [" + receiver_id + "]: " + message); + var outgoing_msg = msg_factory.client_p2p_message(server.clientID, receiver_id, message); + server.send(outgoing_msg); + }; + + context.JK.JamServer = server; + + // Message callbacks + server.registerMessageCallback(context.JK.MessageType.LOGIN_ACK, function(header, payload) { + server.signedIn = true; + logger.debug("Handling LOGIN_ACK. Updating client id to " + payload.client_id); + server.clientID = payload.client_id; + server.publicIP = payload.public_ip; + server.connected = true; + + if (context.jamClient !== undefined) + { + logger.debug("... (handling LOGIN_ACK) Updating backend client, connected to true and clientID to " + + payload.client_id); + context.jamClient.connected = true; + context.jamClient.clientID = server.clientID; + } + }); + + server.registerMessageCallback(context.JK.MessageType.PEER_MESSAGE, function(header, payload) { + if (context.jamClient !== undefined) + { + context.jamClient.P2PMessageReceived(header.from, payload.message); + } + }); + + + // Callbacks from jamClient + if (context.jamClient !== undefined) + { + context.jamClient.SendP2PMessage.connect(server.sendP2PMessage); + } + + +})(window, jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/accounts.js b/web/app/assets/javascripts/accounts.js new file mode 100644 index 000000000..50a79821b --- /dev/null +++ b/web/app/assets/javascripts/accounts.js @@ -0,0 +1,145 @@ +(function(context,$) { + + "use strict"; + + context.JK = context.JK || {}; + context.JK.AccountScreen = function(app) { + var logger = context.JK.logger; + var rest = context.JK.Rest(); + var userId; + var user = {}; + + function beforeShow(data) { + userId = data.id; + } + + function afterShow(data) { + resetForm(); + renderAccount() + } + + function resetForm() { + // remove all display errors + $('#account-content-scroller form .error-text').remove() + $('#account-content-scroller form .error').removeClass("error") + } + + function populateAccount(userDetail) { + + var audioProfiles = prettyPrintAudioProfiles(context.jamClient.TrackGetDevices()); + + var template = context.JK.fillTemplate($('#template-account-main').html(), { + email: userDetail.email, + name: userDetail.name, + location : userDetail.location, + instruments : prettyPrintInstruments(userDetail.instruments), + photoUrl : context.JK.resolveAvatarUrl(userDetail.photo_url), + profiles : audioProfiles + }); + $('#account-content-scroller').html(template ); + } + + function prettyPrintAudioProfiles(devices) { + if(devices && Object.keys(devices).length > 0) { + var profiles = ""; + var delimiter = ", "; + + $.each(devices, function(deviceId, deviceLabel) { + profiles += deviceLabel; + profiles += delimiter; + }) + + return profiles.substring(0, profiles.length - delimiter.length); + } + else { + return "no qualified audio profiles" + } + } + + function prettyPrintInstruments(instruments) { + if(!instruments || instruments.length == 0) { + return "no instruments"; + } + else { + var pp = ""; + $.each(instruments, function(index, item) { + pp += item.description; + if(index < instruments.length - 1) { + pp += ", "; + } + }) + return pp; + } + } + + // events for main screen + function events() { + // wire up main panel clicks + $('#account-content-scroller').on('click', '#account-edit-identity-link', function(evt) { evt.stopPropagation(); navToEditIdentity(); return false; } ); + $('#account-content-scroller').on('click', '#account-edit-profile-link', function(evt) { evt.stopPropagation(); navToEditProfile(); return false; } ); + $('#account-content-scroller').on('click', '#account-edit-subscriptions-link', function(evt) { evt.stopPropagation(); navToEditSubscriptions(); return false; } ); + $('#account-content-scroller').on('click', '#account-edit-payments-link', function(evt) { evt.stopPropagation(); navToEditPayments(); return false; } ); + $('#account-content-scroller').on('click', '#account-edit-audio-link', function(evt) { evt.stopPropagation(); navToEditAudio(); return false; } ); + $('#account-content-scroller').on('avatar_changed', '#profile-avatar', function(evt, newAvatarUrl) { evt.stopPropagation(); updateAvatar(newAvatarUrl); return false; }) + } + + function renderAccount() { + rest.getUserDetail() + .done(populateAccount) + .error(app.ajaxError) + } + + function navToEditIdentity() { + resetForm() + window.location = '#/account/identity' + } + + function navToEditProfile() { + resetForm() + window.location = '#/account/profile' + } + + function navToEditSubscriptions() { + + } + + function navToEditPayments() { + + } + + function navToEditAudio() { + resetForm() + window.location = "#/account/audio" + } + + // handle update avatar event + function updateAvatar(avatar_url) { + var photoUrl = context.JK.resolveAvatarUrl(avatar_url); + var avatar = $(new Image()); + avatar.attr('src', photoUrl + '?cache_bust=' + new Date().getTime()); + avatar.attr('alt', "Avatar"); + avatar.attr('id', 'profile-avatar'); + $('#profile-avatar').replaceWith(avatar); + } + + function navToAccount() { + resetForm(); + renderAccount(); + } + + function initialize() { + var screenBindings = { + 'beforeShow': beforeShow, + 'afterShow': afterShow + }; + app.bindScreen('account', screenBindings); + events(); + } + + this.initialize = initialize; + this.beforeShow = beforeShow; + this.afterShow = afterShow; + return this; + }; + +})(window,jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/accounts_audio_profile.js b/web/app/assets/javascripts/accounts_audio_profile.js new file mode 100644 index 000000000..62cf308e9 --- /dev/null +++ b/web/app/assets/javascripts/accounts_audio_profile.js @@ -0,0 +1,112 @@ +(function(context,$) { + + "use strict"; + + context.JK = context.JK || {}; + context.JK.AccountAudioProfile = function(app) { + var self = this; + var logger = context.JK.logger; + var rest = context.JK.Rest(); + var userId; + var user = {}; + var tmpUploadPath = null; + var userDetail = null; + var avatar; + var selection = null; + var targetCropSize = 88; + var updatingAvatar = false; + + function beforeShow(data) { + userId = data.id; + + registerFtueSuccess(); + } + + + function afterShow(data) { + resetForm(); + renderAudioProfileScreen() + } + + function beforeHide() { + unregisterFtueSuccess(); + + } + + function renderAudioProfileScreen() { + populateAccountAudio() + } + + function populateAccountAudio() { + + $('#account-audio-content-scroller').empty(); + + // load Audio Driver dropdown + var devices = context.jamClient.TrackGetDevices(); + + var options = { + devices: devices + } + + var template = context._.template($('#template-account-audio').html(), options, {variable: 'data'}); + + appendAudio(template); + } + + function appendAudio(template) { + $('#account-audio-content-scroller').html(template); + } + + function resetForm() { + } + + function handleDeleteAudioProfile(audioProfileId) { + console.log("deleting audio profile: " + audioProfileId); + + context.jamClient.TrackDeleteProfile(audioProfileId); + + // redraw after deletion of profile + populateAccountAudio(); + } + + function handleStartAudioQualification() { + app.setWizardStep(2); + app.layout.showDialog('ftue'); + } + + function registerFtueSuccess() { + $('div[layout-id=ftue]').on("ftue_success", ftueSuccessHandler); + } + + function unregisterFtueSuccess() { + $('div[layout-id=ftue]').off("ftue_success", ftueSuccessHandler); + } + + function ftueSuccessHandler() { + populateAccountAudio(); + } + + // events for main screen + function events() { + // wire up main panel clicks + $('#account-audio-content-scroller').on('click', 'a[data-purpose=delete-audio-profile]', function(evt) { evt.stopPropagation(); handleDeleteAudioProfile($(this).attr('data-id')); return false; } ); + $('#account-audio-content-scroller').on('click', 'a[data-purpose=add-profile]', function(evt) { evt.stopPropagation(); handleStartAudioQualification(); return false; } ); + } + + function initialize() { + var screenBindings = { + 'beforeShow': beforeShow, + 'afterShow': afterShow + }; + app.bindScreen('account/audio', screenBindings); + events(); + } + + this.initialize = initialize; + this.beforeShow = beforeShow; + this.afterShow = afterShow; + this.beforeHide = beforeHide; + return this; + }; + +})(window,jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/accounts_identity.js b/web/app/assets/javascripts/accounts_identity.js new file mode 100644 index 000000000..27f71ec2f --- /dev/null +++ b/web/app/assets/javascripts/accounts_identity.js @@ -0,0 +1,243 @@ +(function(context,$) { + + "use strict"; + + context.JK = context.JK || {}; + context.JK.AccountIdentityScreen = function(app) { + var logger = context.JK.logger; + var rest = context.JK.Rest(); + var userId; + var user = {}; + + function beforeShow(data) { + userId = data.id; + } + + function afterShow(data) { + resetForm(); + renderAccountIdentity(); + } + + function resetForm() { + // remove all display errors + $('#account-identity-content-scroller form .error-text').remove() + $('#account-identity-content-scroller form .error').removeClass("error") + } + + function populateAccountIdentity(userDetail) { + var template = context.JK.fillTemplate($('#template-account-identity').html(), { + email: userDetail.email + }); + + appendPasswordPrompt(template); + } + + function appendPasswordPrompt(template) { + $('#account-identity-content-scroller').html(template); + + // append overlay-small + var overlay = context.JK.fillTemplate($('#template-overlay-small').html(), { + id: 'email-update-password-dialog', + title : 'your password is required' + }); + + overlay = $(overlay); + + var passwordForm = context.JK.fillTemplate($('#template-account-update-email-password-prompt-dialog').html(), { + + }); + + $('.overlay-inner', overlay).append(passwordForm); + + $('#account-identity-content-scroller').append(overlay); + } + + /****************** MAIN PORTION OF SCREEN *****************/ + // events for main screen + function events() { + $('#account-identity-content-scroller').on('click', '#account-edit-email-cancel', function(evt) { evt.stopPropagation(); navToAccount(); return false; } ); + $('#account-identity-content-scroller').on('click', '#account-edit-email-submit', function(evt) { evt.stopPropagation(); handleUpdateEmail(); return false; } ); + $('#account-identity-content-scroller').on('submit', '#account-edit-email-form', function(evt) { evt.stopPropagation(); handleUpdateEmail(); return false; } ); + $('#account-identity-content-scroller').on('click', '#account-edit-password-cancel', function(evt) { evt.stopPropagation(); navToAccount(); return false; } ); + $('#account-identity-content-scroller').on('click', '#account-edit-password-submit', function(evt) { evt.stopPropagation(); handleUpdatePassword(); return false; } ); + $('#account-identity-content-scroller').on('submit', '#account-edit-password-form', function(evt) { evt.stopPropagation(); handleUpdatePassword(); return false; } ); + $('#account-identity-content-scroller').on('click', '#email-update-password-dialog a[data-purpose=cancel]', function(evt) { evt.stopPropagation(); handleUpdateEmailPasswordPromptCancel(); return false; } ); + $('#account-identity-content-scroller').on('click', '#email-update-password-dialog a[data-purpose=submit]', function(evt) { evt.stopPropagation(); handleUpdateEmailPasswordPromptSubmit(); return false; } ); + $('#account-identity-content-scroller').on('submit', '#email-update-password-dialog form', function(evt) { evt.stopPropagation(); handleUpdateEmailPasswordPromptSubmit(); return false; } ); + } + + function renderAccountIdentity() { + rest.getUserDetail() + .done(populateAccountIdentity) + .error(app.ajaxError); + } + + function navToAccount() { + resetForm(); + window.location = '#/account'; + } + + function handleUpdateEmail() { + resetForm(); + + $('#account-edit-email-submit').addClass('button-disabled'); + $('#account-edit-email-submit').bind('click', false); + + $('#email-update-password-dialog *').show(); + $('#email-update-password-dialog input[name=password]').focus(); + } + + function handleUpdateEmailPasswordPromptCancel() { + $('#account-edit-email-submit').removeClass('button-disabled'); + $('#account-edit-email-submit').unbind('click', false); + $('#email-update-password-dialog *').hide(); + $('#email-update-password-dialog input[name=password]').val(''); + } + + function handleUpdateEmailPasswordPromptSubmit() { + $('#email-update-password-dialog *').hide(); + + var email = $('#account_update_email').val(); + var password = $('#email-update-password-dialog input[name=password]').val(); + $('#email-update-password-dialog input[name=password]').val(''); + + + postUpdateEmail(email, password) + .done(function(response) { postUpdateEmailSuccess(response, email) }) + .fail(postUpdateEmailFailure) + } + + function handleUpdatePassword() { + resetForm(); + + $('#account-edit-password-submit').addClass('button-disabled'); + $('#account-edit-password-submit').bind('click', false); + + var currentPassword = $('#account-edit-password-form input[name=current_password]').val() + var password = $('#account-edit-password-form input[name=password]').val() + var passwordConfirmation = $('#account-edit-password-form input[name=password_confirmation]').val() + + postUpdatePassword(currentPassword, password, passwordConfirmation) + .done(postUpdatePasswordSuccess) + .fail(postUpdatePasswordFailure) + } + + function postUpdateEmail(email, current_password) { + + var url = "/api/users/" + context.JK.currentUserId + "/update_email"; + return $.ajax({ + type: "POST", + dataType: "json", + contentType: 'application/json', + url: url, + data: JSON.stringify({ update_email: email, current_password : current_password}), + processData: false + }); + } + + function postUpdateEmailSuccess(response, email) { + $('#account-edit-email-submit').removeClass('button-disabled'); + $('#account-edit-email-submit').unbind('click', false); + app.notify( + { title: "Confirmation Email Sent", + text: "A confirmation email should arrive shortly at " + email + ". Please click the confirmation link in it to confirm your email change." + }, + { no_cancel: true }); + } + + function postUpdateEmailFailure(xhr, textStatus, errorMessage) { + $('#account-edit-email-submit').removeClass('button-disabled'); + $('#account-edit-email-submit').unbind('click', false); + var errors = JSON.parse(xhr.responseText) + + if(xhr.status == 422) { + $('#account_update_email').closest("div.field").addClass("error") + var emailError = context.JK.get_first_error("update_email", errors); + var passwordError = context.JK.get_first_error("current_password", errors); + var combinedErrorMsg = "" + if(emailError != null) { + combinedErrorMsg += "
  • email " + emailError + "
  • " + } + if(passwordError != null) { + combinedErrorMsg += "
  • password " + passwordError + "
  • " + } + + if(combinedErrorMsg.length == 0) { + combinedErrorMsg = "unknown error" + } + + $('#account_update_email').after("") + } + else { + app.ajaxError(xhr, textStatus, errorMessage) + } + } + + function postUpdatePassword(currentPassword, newPassword, newPasswordConfirm) { + + var url = "/api/users/" + context.JK.currentUserId + "/set_password"; + return $.ajax({ + type: "POST", + dataType: "json", + contentType: 'application/json', + url: url, + data: JSON.stringify({ old_password: currentPassword, new_password: newPassword, new_password_confirm: newPasswordConfirm }), + processData: false + }); + } + + function postUpdatePasswordSuccess(response) { + $('#account-edit-password-submit').removeClass('button-disabled'); + $('#account-edit-password-submit').unbind('click', false); + app.notify( + { title: "Password Changed", + text: "You have changed your password successfully." + }, + { no_cancel: true }); + } + + function postUpdatePasswordFailure(xhr, textStatus, errorMessage) { + + $('#account-edit-password-submit').removeClass('button-disabled'); + $('#account-edit-password-submit').unbind('click', false); + var errors = JSON.parse(xhr.responseText) + + if(xhr.status == 422) { + + var current_password_errors = context.JK.format_errors("current_password", errors) + var password_errors = context.JK.format_errors("password", errors) + var password_confirmation_errors = context.JK.format_errors("password_confirmation", errors) + + if(current_password_errors != null) { + $('#account-edit-password-form #account-forgot-password').closest('div.field').addClass('error').end().after(current_password_errors); + } + + if(password_errors != null) { + $('#account-edit-password-form input[name=password]').closest('div.field').addClass('error').end().after(password_errors); + } + + if(password_confirmation_errors != null) { + $('#account-edit-password-form input[name=password_confirmation]').closest('div.field').addClass('error').end().after(password_confirmation_errors); + } + } + else { + app.ajaxError(xhr, textStatus, errorMessage) + } + } + + function initialize() { + var screenBindings = { + 'beforeShow': beforeShow, + 'afterShow': afterShow + }; + app.bindScreen('account/identity', screenBindings); + events(); + } + + this.initialize = initialize; + this.beforeShow = beforeShow; + this.afterShow = afterShow; + return this; + }; + +})(window,jQuery); diff --git a/web/app/assets/javascripts/accounts_profile.js b/web/app/assets/javascripts/accounts_profile.js new file mode 100644 index 000000000..97f563159 --- /dev/null +++ b/web/app/assets/javascripts/accounts_profile.js @@ -0,0 +1,538 @@ +(function(context,$) { + + "use strict"; + + context.JK = context.JK || {}; + context.JK.AccountProfileScreen = function(app) { + var logger = context.JK.logger; + var api = context.JK.Rest(); + var userId; + var user = {}; + var recentUserDetail = null; + var loadingCitiesData = false; + var loadingRegionsData = false; + var loadingCountriesData = false; + var nilOptionText = 'n/a'; + + function beforeShow(data) { + userId = data.id; + } + + function afterShow(data) { + resetForm(); + renderAccountProfile(); + } + + function resetForm() { + // remove all display errors + $('#account-profile-content-scroller form .error-text').remove() + $('#account-profile-content-scroller form .error').removeClass("error") + } + + function populateAccountProfile(userDetail, instruments) { + var template = context.JK.fillTemplate($('#template-account-profile').html(), { + country: userDetail.country, + region: userDetail.state, + city: userDetail.city, + first_name: userDetail.first_name, + last_name: userDetail.last_name, + user_instruments: userDetail.instruments, + birth_date : userDetail.birth_date, + gender: userDetail.gender + }); + + var content_root = $('#account-profile-content-scroller') + content_root.html(template); + + // now use javascript to fix up values too hard to do with templating + + // set gender + $('select[name=gender]', content_root).val(userDetail.gender) + + // set birth_date + if(userDetail.birth_date) { + var birthDateFields = userDetail.birth_date.split('-') + var birthDateYear = birthDateFields[0]; + var birthDateMonth = birthDateFields[1]; + var birthDateDay = birthDateFields[2]; + + $('select#user_birth_date_1i', content_root).val(parseInt(birthDateYear)); + $('select#user_birth_date_2i', content_root).val(parseInt(birthDateMonth)); + $('select#user_birth_date_3i', content_root).val(parseInt(birthDateDay)); + } + + // update instruments + $.each(instruments, function(index, instrument) { + + var template = context.JK.fillTemplate($('#account-profile-instrument').html(), { + checked : isUserInstrument(instrument, userDetail.instruments) ? "checked=\"checked\"" :"", + description : instrument.description, + id : instrument.id + }) + $('.instrument_selector', content_root).append(template) + }) + // and fill in the proficiency for the instruments that the user can play + if(userDetail.instruments) { + $.each(userDetail.instruments, function(index, userInstrument) { + $('tr[data-instrument-id="' + userInstrument.instrument_id + '"] select.proficiency_selector', content_root).val(userInstrument.proficiency_level) + }) + } + } + + function isUserInstrument(instrument, userInstruments) { + var isUserInstrument = false; + if(!userInstruments) return false; + + $.each(userInstruments, function(index, userInstrument) { + if(instrument.id == userInstrument.instrument_id) { + isUserInstrument = true; + return false; + } + }) + return isUserInstrument; + } + + function populateAccountProfileLocation(userDetail, regions, cities) { + populateRegions(regions, userDetail.state); + populateCities(cities, userDetail.city); + } + + + function populateCountries(countries, userCountry) { + + var foundCountry = false; + var countrySelect = getCountryElement() + countrySelect.children().remove() + + var nilOption = $(''); + nilOption.text(nilOptionText); + countrySelect.append(nilOption); + + $.each(countries, function(index, country) { + if(!country) return; + + var option = $(''); + option.text(country); + option.attr("value", country); + + if(country == userCountry) { + foundCountry = true; + } + + countrySelect.append(option) + }); + + if(!foundCountry) { + // in this case, the user has a country that is not in the database + // this can happen in a development/test scenario, but let's assume it can + // happen in production too. + var option = $(''); + option.text(userCountry); + option.attr("value", userCountry); + countrySelect.append(option); + } + + countrySelect.val(userCountry); + countrySelect.attr("disabled", null) + } + + + function populateRegions(regions, userRegion) { + var regionSelect = getRegionElement() + regionSelect.children().remove() + + var nilOption = $(''); + nilOption.text(nilOptionText); + regionSelect.append(nilOption); + + $.each(regions, function(index, region) { + if(!region) return; + + var option = $('') + option.text(region) + option.attr("value", region) + + regionSelect.append(option) + }) + + regionSelect.val(userRegion) + regionSelect.attr("disabled", null) + } + + function populateCities(cities, userCity) { + var citySelect = getCityElement(); + citySelect.children().remove(); + + var nilOption = $(''); + nilOption.text(nilOptionText); + citySelect.append(nilOption); + + $.each(cities, function(index, city) { + if(!city) return; + + var option = $('') + option.text(city) + option.attr("value", city) + + citySelect.append(option) + }) + + citySelect.val(userCity) + citySelect.attr("disabled", null) + } + + /****************** MAIN PORTION OF SCREEN *****************/ + // events for main screen + function events() { + $('#account-profile-content-scroller').on('click', '#account-edit-profile-cancel', function(evt) { evt.stopPropagation(); navToAccount(); return false; } ); + $('#account-profile-content-scroller').on('click', '#account-edit-profile-submit', function(evt) { evt.stopPropagation(); handleUpdateProfile(); return false; } ); + $('#account-profile-content-scroller').on('submit', '#account-edit-email-form', function(evt) { evt.stopPropagation(); handleUpdateProfile(); return false; } ); + $('#account-profile-content-scroller').on('change', 'select[name=country]', function(evt) { evt.stopPropagation(); handleCountryChanged(); return false; } ); + $('#account-profile-content-scroller').on('change', 'select[name=region]', function(evt) { evt.stopPropagation(); handleRegionChanged(); return false; } ); + $('#account-profile-content-scroller').on('click', '#account-change-avatar', function(evt) { evt.stopPropagation(); navToAvatar(); return false; } ); + } + + function regionListFailure(jqXHR, textStatus, errorThrown) { + if(jqXHR.status == 422) { + console.log("no regions found for country: " + recentUserDetail.country); + } + else { + app.ajaxError(arguments); + } + } + + function cityListFailure(jqXHR, textStatus, errorThrown) { + if(jqXHR.status == 422) { + console.log("no cities found for country/region: " + recentUserDetail.country + "/" + recentUserDetail.state); + } + else { + app.ajaxError(arguments); + } + } + function renderAccountProfile() { + + $.when( api.getUserDetail(), + api.getInstruments()) + .done(function(userDetailResponse, instrumentsResponse) { + var userDetail = userDetailResponse[0]; + recentUserDetail = userDetail // store userDetail for later + var instruments = instrumentsResponse[0]; + // show page; which can be done quickly at this point + populateAccountProfile(userDetail, instruments); + + var country = userDetail.country; + + if(!country) { + // this case shouldn't happen because sign up makes you pick a location. This is just 'in case', so that the UI is more error-resilient + country = 'US'; + } + + loadingCountriesData = true; + loadingRegionsData = true; + loadingCitiesData = true; + + // make the 3 slower requests, which only matter if the user wants to affect their ISP or location + + api.getCountries() + .done(function(countries) { populateCountries(countries["countries"], userDetail.country); } ) + .fail(app.ajaxError) + .always(function() { loadingCountriesData = false; }) + + var country = userDetail.country; + var state = userDetail.state; + + if(country) { + api.getRegions( { country: country } ) + .done(function(regions) { populateRegions(regions["regions"], userDetail.state); } ) + .fail(regionListFailure) + .always(function() { loadingRegionsData = false; }) + + if(state) { + api.getCities( { country: country, region: state }) + .done(function(cities) { + populateCities(cities["cities"], userDetail.city) + }) + .fail(cityListFailure) + .always(function() { loadingCitiesData = false;}) + } + } + + + }) + } + + function navToAccount() { + resetForm(); + window.location = '#/account'; + } + + function navToAvatar() { + resetForm(); + window.location = '#/account/profile/avatar'; + } + + function handleUpdateProfile() { + + resetForm(); + + var country = getCountryElement().val(); + var region = getRegionElement().val(); + var city = getCityElement().val(); + var firstName = getFirstNameElement().val(); + var lastName = getLastNameElement().val(); + var gender = getGenderElement().val() + var birthDate = getBirthDate(); + var instruments = getInstrumentsValue(); + + postUpdateProfile({ + country: country, + state: region, + city: city, + first_name: firstName, + last_name: lastName, + gender: gender, + birth_date: birthDate, + instruments: instruments + }) + .done(postUpdateProfileSuccess) + .fail(postUpdateProfileFailure) + } + + function postUpdateProfile(options) { + + var url = "/api/users/" + context.JK.currentUserId; + return $.ajax({ + type: "POST", + dataType: "json", + contentType: 'application/json', + url: url, + data: JSON.stringify(options), + processData: false + }); + } + + function postUpdateProfileSuccess(response) { + app.notify( + { title: "Profile Changed", + text: "You have updated your profile successfully." + }, + { no_cancel: true }); + } + + function postUpdateProfileFailure(xhr, textStatus, errorMessage) { + + var errors = JSON.parse(xhr.responseText) + + if(xhr.status == 422) { + + var first_name = context.JK.format_errors("first_name", errors); + var last_name = context.JK.format_errors("last_name", errors); + var country = context.JK.format_errors("country", errors); + var state = context.JK.format_errors("state", errors); + var city = context.JK.format_errors("city", errors); + var birth_date = context.JK.format_errors("birth_date", errors); + var gender = context.JK.format_errors("birth_date", errors); + var instruments = context.JK.format_errors("musician_instruments", errors) + + if(first_name != null) { + getFirstNameElement().closest('div.field').addClass('error').end().after(first_name); + } + + if(last_name != null) { + getLastNameElement().closest('div.field').addClass('error').end().after(last_name); + } + + if(country != null) { + getCountryElement().closest('div.field').addClass('error').end().after(country); + } + + if(state != null) { + getRegionElement().closest('div.field').addClass('error').end().after(state); + } + + if(city != null) { + getCityElement().closest('div.field').addClass('error').end().after(city); + } + + if(birth_date != null) { + getYearElement().closest('div.field').addClass('error').end().after(birth_date); + } + + if(gender != null) { + getGenderElement().closest('div.field').addClass('error').end().after(gender); + } + + if(instruments != null) { + getInstrumentsElement().closest('div.field').addClass('error').append(instruments); + } + } + else { + app.ajaxError(xhr, textStatus, errorMessage) + } + } + + function handleCountryChanged() { + var selectedCountry = getCountryElement().val() + var selectedRegion = getRegionElement().val() + var cityElement = getCityElement(); + + updateRegionList(selectedCountry, getRegionElement()); + } + + function updateRegionList(selectedCountry, regionElement) { + // only update region + if (selectedCountry) { + // set city disabled while updating + regionElement.attr('disabled', true); + loadingRegionsData = true; + + regionElement.children().remove() + regionElement.append($('').text('loading...')) + + api.getRegions({ country: selectedCountry }) + .done(getRegionsDone) + .fail(app.ajaxError) + .always(function () { + loadingRegionsData = false; + }) + } + else { + regionElement.children().remove() + regionElement.append($('').text(nilOptionText)) + } + } + + function updateCityList(selectedCountry, selectedRegion, cityElement) { + console.log("updating city list: selectedCountry %o, selectedRegion %o", selectedCountry, selectedRegion); + + // only update cities + if (selectedCountry && selectedRegion) { + // set city disabled while updating + cityElement.attr('disabled', true); + loadingCitiesData = true; + + cityElement.children().remove() + cityElement.append($('').text('loading...')) + + api.getCities({ country: selectedCountry, region: selectedRegion }) + .done(getCitiesDone) + .fail(app.ajaxError) + .always(function () { + loadingCitiesData = false; + }) + } + else { + cityElement.children().remove() + cityElement.append($('').text(nilOptionText)) + } + } + + function handleRegionChanged() { + var selectedCountry = getCountryElement().val() + var selectedRegion = getRegionElement().val() + var cityElement = getCityElement(); + + updateCityList(selectedCountry, selectedRegion, cityElement); + } + + function getCitiesDone(data) { + populateCities(data['cities'], recentUserDetail.city); + } + + function getRegionsDone(data) { + populateRegions(data['regions'], recentUserDetail.state); + updateCityList(getCountryElement().val(), getRegionElement().val(), getCityElement()); + } + + function getCountryElement() { + return $('#account-profile-content-scroller select[name=country]'); + } + + function getRegionElement() { + return $('#account-profile-content-scroller select[name=region]'); + } + + function getCityElement() { + return $('#account-profile-content-scroller select[name=city]'); + } + + function getFirstNameElement() { + return $('#account-profile-content-scroller input[name=first_name]'); + } + + function getLastNameElement() { + return $('#account-profile-content-scroller input[name=last_name]'); + } + + function getGenderElement() { + return $('#account-profile-content-scroller select[name=gender]'); + } + + function getMonthElement() { + return $('#account-profile-content-scroller select#user_birth_date_2i'); + } + + function getDayElement() { + return $('#account-profile-content-scroller select#user_birth_date_3i'); + } + + function getYearElement() { + return $('#account-profile-content-scroller select#user_birth_date_1i'); + } + + function getInstrumentsElement() { + return $('#account-profile-content-scroller .instrument_selector'); + } + + + function getBirthDate() { + var month = getMonthElement().val() + var day = getDayElement().val() + var year = getYearElement().val() + + if(month != null && month.length > 0 && day != null && day.length > 0 && year != null && year.length > 0) { + return month + "-" + day + "-" + year; + } + else { + return null; + } + } + + // looks in instrument_selector parent element, and gathers up all + // selected elements, and the proficiency level declared + function getInstrumentsValue() { + var instrumentsParentElement = getInstrumentsElement(); + + var instruments = [] + $('input[type=checkbox]:checked', instrumentsParentElement).each(function(i) { + + var instrumentElement = $(this).closest('tr'); + // traverse up to common parent of this instrument, and pick out proficiency selector + var proficiency = $('select.proficiency_selector', instrumentElement).val() + + instruments.push({ + instrument_id: instrumentElement.attr('data-instrument-id'), + proficiency_level: proficiency, + priority : i + }) + }); + + return instruments; + } + + function initialize() { + var screenBindings = { + 'beforeShow': beforeShow, + 'afterShow': afterShow + }; + app.bindScreen('account/profile', screenBindings); + events(); + } + + this.initialize = initialize; + this.beforeShow = beforeShow; + this.afterShow = afterShow; + return this; + }; + +})(window,jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/accounts_profile_avatar.js b/web/app/assets/javascripts/accounts_profile_avatar.js new file mode 100644 index 000000000..4f1757f30 --- /dev/null +++ b/web/app/assets/javascripts/accounts_profile_avatar.js @@ -0,0 +1,432 @@ +(function(context,$) { + + "use strict"; + + context.JK = context.JK || {}; + context.JK.AccountProfileAvatarScreen = function(app) { + var self = this; + var logger = context.JK.logger; + var rest = context.JK.Rest(); + var userId; + var user = {}; + var tmpUploadPath = null; + var userDetail = null; + var avatar; + var selection = null; + var targetCropSize = 88; + var updatingAvatar = false; + + function beforeShow(data) { + userId = data.id; + } + + + function afterShow(data) { + resetForm(); + renderAvatarScreen() + } + + function resetForm() { + // remove all display errors + $('#account-profile-avatar-content-scroller form .error-text').remove() + $('#account-profile-avatar-content-scroller form .error').removeClass("error") + } + + function populateAvatar(userDetail) { + self.userDetail = userDetail; + rest.getFilepickerPolicy() + .done(function(filepicker_policy) { + var template= context.JK.fillTemplate($('#template-account-profile-avatar').html(), { + "fp_apikey" : gon.fp_apikey, + "data-fp-store-path" : createStorePath(userDetail) + createOriginalFilename(userDetail), + "fp_policy" : filepicker_policy.policy, + "fp_signature" : filepicker_policy.signature + }); + $('#account-profile-avatar-content-scroller').html(template); + + + var currentFpfile = determineCurrentFpfile(); + var currentCropSelection = determineCurrentSelection(userDetail); + renderAvatar(currentFpfile, currentCropSelection ? JSON.parse(currentCropSelection) : null); + }) + .error(app.ajaxError); + + } + + // events for main screen + function events() { + // wire up main panel clicks + $('#account-profile-avatar-content-scroller').on('click', '#account-edit-avatar-upload', function(evt) { evt.stopPropagation(); handleFilePick(); return false; } ); + $('#account-profile-avatar-content-scroller').on('click', '#account-edit-avatar-delete', function(evt) { evt.stopPropagation(); handleDeleteAvatar(); return false; } ); + $('#account-profile-avatar-content-scroller').on('click', '#account-edit-avatar-cancel', function(evt) { evt.stopPropagation(); navToEditProfile(); return false; } ); + $('#account-profile-avatar-content-scroller').on('click', '#account-edit-avatar-submit', function(evt) { evt.stopPropagation(); handleUpdateAvatar(); return false; } ); + //$('#account-profile-avatar-content-scroller').on('change', 'input[type=filepicker-dragdrop]', function(evt) { evt.stopPropagation(); afterImageUpload(evt.originalEvent.fpfile); return false; } ); + } + + function handleDeleteAvatar() { + + if(self.updatingAvatar) { + // protect against concurrent update attempts + return; + } + + self.updatingAvatar = true; + renderAvatarSpinner(); + + rest.deleteAvatar() + .done(function() { + removeAvatarSpinner({ delete:true }); + deleteAvatarSuccess(arguments); + selection = null; + }) + .fail(function() { + app.ajaxError(arguments); + $.cookie('original_fpfile', null); + self.updatingAvatar = false; + }) + .always(function() { + + }) + } + + function deleteAvatarSuccess(response) { + + renderAvatar(null, null); + JK.Header.loadMe(); + + rest.getUserDetail() + .done(function(userDetail) { + self.userDetail = userDetail; + }) + .error(app.ajaxError) + .always(function() { + self.updatingAvatar = false; + }) + } + + function handleFilePick() { + + rest.getFilepickerPolicy() + .done(function(filepickerPolicy) { + renderAvatarSpinner(); + filepicker.setKey(gon.fp_apikey); + filepicker.pickAndStore({ + mimetype: 'image/*', + maxSize: 10000*1024, + policy: filepickerPolicy.policy, + signature: filepickerPolicy.signature + }, { path: createStorePath(self.userDetail), access: 'public' }, + function(fpfiles) { + removeAvatarSpinner(); + afterImageUpload(fpfiles[0]); + }, function(fperror) { + removeAvatarSpinner(); + + if(fperror.code != 101) { // 101 just means the user closed the dialog + alert("unable to upload file: " + JSON.stringify(fperror)) + } + }) + }) + .fail(app.ajaxError); + + } + function renderAvatarScreen() { + + rest.getUserDetail() + .done(populateAvatar) + .error(app.ajaxError) + } + + function navToEditProfile() { + resetForm(); + window.location = '#/account/profile' + } + + function renderAvatarSpinner() { + var avatarSpace = $('#account-profile-avatar-content-scroller .account-profile-avatar .avatar-space'); + // if there is already an image tag, we only obscure it. + + var avatar = $('img.preview_profile_avatar', avatarSpace); + + var spinner = $('
    ') + if(avatar.length == 0) { + avatarSpace.prepend(spinner); + } + else { + // in this case, just style the spinner to obscure using opacity, and center it + var jcropHolder = $('.jcrop-holder', avatarSpace); + spinner.width(jcropHolder.width()); + spinner.height(jcropHolder.height()); + spinner.addClass('op50'); + var jcrop = avatar.data('Jcrop'); + if(jcrop) { + jcrop.disable(); + } + avatarSpace.append(spinner); + } + } + + function removeAvatarSpinner(options) { + var avatarSpace = $('#account-profile-avatar-content-scroller .account-profile-avatar .avatar-space'); + + if(options && options.delete) { + avatarSpace.children().remove(); + } + + var spinner = $('.spinner-large', avatarSpace); + spinner.remove(); + var avatar = $('img.preview_profile_avatar', avatarSpace); + var jcrop = avatar.data('Jcrop') + if(jcrop) { + jcrop.enable(); + } + } + + function renderAvatar(fpfile, storedSelection) { + + // clear out + var avatarSpace = $('#account-profile-avatar-content-scroller .account-profile-avatar .avatar-space'); + + if(!fpfile) { + renderNoAvatar(avatarSpace); + } + else { + + rest.getFilepickerPolicy({handle: fpfile.url}) + .done(function(filepickerPolicy) { + avatarSpace.children().remove(); + renderAvatarSpinner(); + + var photo_url = fpfile.url + '?signature=' + filepickerPolicy.signature + '&policy=' + filepickerPolicy.policy; + avatar = new Image(); + $(avatar) + .load(function(e) { + removeAvatarSpinner(); + + avatar = $(this); + avatarSpace.append(avatar); + var width = avatar.naturalWidth(); + var height = avatar.naturalHeight(); + + if(storedSelection) { + var left = storedSelection.x; + var right = storedSelection.x2; + var top = storedSelection.y; + var bottom = storedSelection.y2; + } + else { + if(width < height) { + var left = width * .25; + var right = width * .75; + var top = (height / 2) - (width / 4); + var bottom = (height / 2) + (width / 4); + } + else { + var top = height * .25; + var bottom = height * .75; + var left = (width / 2) - (height / 4); + var right = (width / 2) + (height / 4); + } + } + + // jcrop only works well with px values (not percentages) + // so we get container, and work out a decent % ourselves + var container = $('#account-profile-avatar-content-scroller'); + + avatar.Jcrop({ + aspectRatio: 1, + boxWidth: container.width() * .75, + boxHeight: container.height() * .75, + // minSelect: [targetCropSize, targetCropSize], unnecessary with scaling involved + setSelect: [ left, top, right, bottom ], + trueSize: [width, height], + onRelease: onSelectRelease, + onSelect: onSelect, + onChange: onChange + }); + }) + .error(function() { + // default to no avatar look of UI + renderNoAvatar(avatarSpace); + }) + .attr('src', photo_url) + .attr('alt', 'profile avatar') + .addClass('preview_profile_avatar'); + }) + .fail(app.ajaxError); + } + } + + function afterImageUpload(fpfile) { + + $.cookie('original_fpfile', JSON.stringify(fpfile)); + + renderAvatar(fpfile, null); + } + + function renderNoAvatar(avatarSpace) { + // no avatar found for account + + removeAvatarSpinner(); + + var noAvatarSpace = $('
    '); + noAvatarSpace.addClass('no-avatar-space'); + noAvatarSpace.text('Please upload a photo'); + avatarSpace.append(noAvatarSpace); + } + + function handleUpdateAvatar(event) { + + if(self.updatingAvatar) { + // protect against concurrent update attempts + return; + } + + if(selection) { + var currentSelection = selection; + self.updatingAvatar = true; + renderAvatarSpinner(); + + console.log("Converting..."); + + // we convert two times; first we crop to the selected region, + // then we scale to 88x88 (targetCropSize X targetCropSize), which is the largest size we use throughout the site. + var fpfile = determineCurrentFpfile(); + rest.getFilepickerPolicy({ handle: fpfile.url, convert: true }) + .done(function(filepickerPolicy) { + filepicker.setKey(gon.fp_apikey); + filepicker.convert(fpfile, { + crop: [ + Math.round(currentSelection.x), + Math.round(currentSelection.y), + Math.round(currentSelection.w), + Math.round(currentSelection.w)], + fit: 'crop', + format: 'jpg', + quality: 90, + policy: filepickerPolicy.policy, + signature: filepickerPolicy.signature + }, { path: createStorePath(self.userDetail) + 'cropped.jpg', access: 'public' }, + function(cropped) { + rest.getFilepickerPolicy({handle: cropped.url, convert: true}) + .done(function(filepickerPolicy) { + filepicker.convert(cropped, { + height: targetCropSize, + width: targetCropSize, + fit: 'scale', + format: 'jpg', + quality: 75, + policy: filepickerPolicy.policy, + signature: filepickerPolicy.signature + }, { path: createStorePath(self.userDetail), access: 'public' }, + function(scaled) { + rest.updateAvatar({ + original_fpfile: determineCurrentFpfile(), + cropped_fpfile: scaled, + crop_selection: currentSelection + }) + .done(updateAvatarSuccess) + .fail(app.ajaxError) + .always(function() { removeAvatarSpinner(); self.updatingAvatar = false;}) + }, + function(fperror) { + alert("unable to scale selection. error code: " + fperror.code); + removeAvatarSpinner(); + self.updatingAvatar = false; + }) + }) + .fail(app.ajaxError); + }, + function(fperror) { + alert("unable to crop selection. error code: " + fperror.code); + removeAvatarSpinner(); + self.updatingAvatar = false; + } + ); + }) + .fail(app.ajaxError); + } + else { + app.notify( + { title: "Upload an Avatar First", + text: "To update your avatar, first you must upload an image using the UPLOAD button" + }, + { no_cancel: true }); + } + } + + function updateAvatarSuccess(response) { + $.cookie('original_fpfile', null); + + self.userDetail = response; + + // notify any listeners that the avatar changed + JK.Header.loadMe(); + // $('.avatar_large img').trigger('avatar_changed', [self.userDetail.photo_url]); + + app.notify( + { title: "Avatar Changed", + text: "You have updated your avatar successfully." + }, + { no_cancel: true }); + } + + function onSelectRelease(event) { + } + + function onSelect(event) { + selection = event; + } + + function onChange(event) { + } + + function createStorePath(userDetail) { + + return gon.fp_upload_dir + '/' + userDetail.id + '/' + } + + function createOriginalFilename(userDetail) { + // get the s3 + var fpfile = userDetail.original_fpfile ? JSON.parse(userDetail.original_fpfile) : null; + return 'original_avatar.jpg' + } + + // retrieves a file that has not yet been used as an avatar (uploaded, but not cropped) + function getWorkingFpfile() { + return JSON.parse($.cookie('original_fpfile')) + } + + function determineCurrentFpfile() { + // precedence is as follows: + // * tempOriginal: if set, then the user is working on a new upload + // * storedOriginal: if set, then the user has previously uploaded and cropped an avatar + // * null: neither are set above + + var tempOriginal = getWorkingFpfile(); + var storedOriginal = self.userDetail.original_fpfile ? JSON.parse(self.userDetail.original_fpfile) : null; + + return tempOriginal ? tempOriginal : storedOriginal; + } + + function determineCurrentSelection(userDetail) { + // if the cookie is set, don't use the storage selection, just default to null + return $.cookie('original_fpfile') == null ? userDetail.crop_selection : null; + } + + function initialize() { + var screenBindings = { + 'beforeShow': beforeShow, + 'afterShow': afterShow + }; + app.bindScreen('account/profile/avatar', screenBindings); + events(); + } + + this.initialize = initialize; + this.beforeShow = beforeShow; + this.afterShow = afterShow; + return this; + }; + +})(window,jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/addNewGear.js b/web/app/assets/javascripts/addNewGear.js new file mode 100644 index 000000000..b12573b9c --- /dev/null +++ b/web/app/assets/javascripts/addNewGear.js @@ -0,0 +1,33 @@ +(function(context,$) { + + "use strict"; + + context.JK = context.JK || {}; + context.JK.AddNewGearDialog = function(app, ftueCallback) { + var logger = context.JK.logger; + + function events() { + $('#btn-leave-session-test').click(function() { + ftueCallback(); + + // TODO: THIS IS A HACK - THIS DIALOG IS LAYERED + // ON TOP OF OTHER DIALOGS. ANY OTHER DIALOGS THAT + // USE THIS NEED TO BE ADDED TO THE FOLLOWING LIST. + // NEED TO FIGURE OUT A CLEANER WAY TO HANDLE THIS. + app.layout.closeDialog('add-track'); + app.layout.closeDialog('configure-audio'); + }); + + $('#btn-cancel-new-audio').click(function() { + app.layout.closeDialog('add-new-audio-gear'); + }); + } + + this.initialize = function() { + events(); + }; + + return this; + }; + + })(window,jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/addTrack.js b/web/app/assets/javascripts/addTrack.js new file mode 100644 index 000000000..fdd3cb56f --- /dev/null +++ b/web/app/assets/javascripts/addTrack.js @@ -0,0 +1,205 @@ +(function(context,$) { + + "use strict"; + + context.JK = context.JK || {}; + context.JK.AddTrackDialog = function(app, myTracks, sessionId, sessionModel) { + var logger = context.JK.logger; + + var ASSIGNMENT = { + CHAT: -2, + OUTPUT: -1, + UNASSIGNED: 0, + TRACK1: 1, + TRACK2: 2 + }; + + var instrument_array = []; + + // dialog variables + // dialog variables + var inputUnassignedList = []; + var track2AudioInputChannels = []; + + function events() { + + // Track 2 Add + $('#img-add-track2-input-add').unbind("click"); + $('#img-add-track2-input-add').click(function() { + $('#add-track2-unused > option:selected').remove().appendTo('#add-track2-input'); + }); + + // Track 2 Remove + $('#img-add-track2-input-remove').unbind("click"); + $('#img-add-track2-input-remove').click(function() { + $('#add-track2-input > option:selected').remove().appendTo('#add-track2-unused'); + }); + + $('#btn-cancel-new-audio').click(context.JK.showOverlay); + $('#btn-error-ok').click(context.JK.showOverlay); + + // $('#btn-cancel-new-audio').click(function() { + // app.layout.closeDialog('add-new-audio-gear'); + // }); + // $('#btn-error-ok').click(function() { + // app.layout.closeDialog('error-dialog'); + // }); + + $('#btn-add-track').unbind("click"); + $('#btn-add-track').click(saveSettings); + } + + function showDialog() { + + $('#add-track2-unused').empty(); + $('#add-track2-input').empty(); + $('#add-track2-instrument').empty(); + + initDialogData(); + + // load Unused Inputs + context.JK.loadOptions($('#template-option').html(), $('#add-track2-unused'), inputUnassignedList, "id", "name", -1); + + // load Track 2 Input(s) + context.JK.loadOptions($('#template-option').html(), $('#add-track2-input'), track2AudioInputChannels, "id", "name", -1); + + // load Track 2 Instrument + context.JK.loadOptions($('#template-option').html(), $('#add-track2-instrument'), instrument_array, "id", "description", -1); + } + + function initDialogData() { + + // set arrays + inputUnassignedList = _loadList(ASSIGNMENT.UNASSIGNED, true, false); + console.log("inputUnassignedList: " + JSON.stringify(inputUnassignedList)); + track2AudioInputChannels = _loadList(ASSIGNMENT.TRACK2, true, false); + } + + function _loadList(assignment, input, chat) { + var list = []; + + // get data needed for listboxes + var channels = context.jamClient.TrackGetChannels(); + + var musicDevices = context.jamClient.TrackGetMusicDeviceNames(input); + + // SEE loadList function in TrackAssignGui.cpp of client code + $.each(channels, function(index, val) { + + if (input !== val.input) { + return; + } + + var currAssignment = context.jamClient.TrackGetAssignment(val.id, val.input); + if (assignment !== currAssignment) { + return; + } + + logger.debug("channel id=" + val.id + ", channel input=" + val.input + ", channel assignment=" + currAssignment + + ", channel name=" + val.name + ", channel type=" + val.device_type + ", chat=" + val.chat); + + var os = context.jamClient.GetOSAsString(); + if (os === context.JK.OS.WIN32) { + if (chat && ($.inArray(val.device_id, musicDevices) > -1 || context.jamClient.TrackIsMusicDeviceType(val.device_type))) { + return; + } + } + else { + if (chat && ($.inArray(val.device_id, musicDevices) > -1 || !context.jamClient.TrackIsMusicDeviceType(val.device_type))) { + return; + } + } + + if (!chat && $.inArray(val.device_id, musicDevices) === -1) { + return; + } + + if ((chat && !val.chat) || (!chat && val.chat)) { + return; + } + + list.push(val); + }); + + return list; + } + + function saveSettings() { + if (!validateSettings()) { + return; + } + + saveTrack(); + app.layout.closeDialog('add-track'); + } + + function saveTrack() { + // TRACK 2 INPUTS + $("#add-track2-input > option").each(function() { + logger.debug("Saving track 2 input = " + this.value); + context.jamClient.TrackSetAssignment(this.value, true, ASSIGNMENT.TRACK2); + }); + + // TRACK 2 INSTRUMENT + var instrumentVal = $('#add-track2-instrument').val(); + var instrumentText = $('#add-track2-instrument > option:selected').text().toLowerCase(); + + logger.debug("Saving track 2 instrument = " + instrumentVal); + context.jamClient.TrackSetInstrument(ASSIGNMENT.TRACK2, instrumentVal); + + // UPDATE SERVER + logger.debug("Adding track with instrument " + instrumentText); + var data = {}; + // use the first track's connection_id (not sure why we need this on the track data model) + logger.debug("myTracks[0].connection_id=" + myTracks[0].connection_id); + data.connection_id = myTracks[0].connection_id; + data.instrument_id = instrumentText; + data.sound = "stereo"; + sessionModel.addTrack(sessionId, data); + } + + function validateSettings() { + var isValid = true; + var noTrackErrMsg = 'You must assign at least one input port to each of your tracks. Please update your settings to correct this. If you want to delete a track, please return to the session screen and delete the track by clicking the "x" box in the upper right-hand corner of the track.'; + var noInstrumentErrMsg = 'You must specify what instrument is being played for this new track. Please update your settings to correct this.'; + + var errMsg; + + // verify Input and Instrument exist + if ($('#add-track2-input > option').size() === 0 || $('#add-track2-input > option').size() > 2) { + errMsg = noTrackErrMsg; + isValid = false; + } + + if (isValid && $('#add-track2-instrument > option:selected').length === 0) { + errMsg = noInstrumentErrMsg; + isValid = false; + } + + if (!isValid) { + context.JK.showErrorDialog(app, errMsg, "invalid settings"); + } + return isValid; + } + + // TODO: repeated in configureTrack.js + function _init() { + // load instrument array for populating listboxes, using client_id in instrument_map as ID + context.JK.listInstruments(app, function(instruments) { + $.each(instruments, function(index, val) { + instrument_array.push({"id": context.JK.server_to_client_instrument_map[val.description].client_id, "description": val.description}); + }); + }); + } + + this.initialize = function() { + events(); + _init(); + }; + + this.showDialog = showDialog; + + return this; + }; + + })(window,jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/application.js b/web/app/assets/javascripts/application.js new file mode 100644 index 000000000..55a45e8b4 --- /dev/null +++ b/web/app/assets/javascripts/application.js @@ -0,0 +1,20 @@ +// This is a manifest file that'll be compiled into application.js, which will include all the files +// listed below. +// +// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, +// or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path. +// +// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the +// the compiled file. +// +// WARNING: THE FIRST BLANK LINE MARKS THE END OF WHAT'S TO BE PROCESSED, ANY BLANK LINE SHOULD +// GO AFTER THE REQUIRES BELOW. +// +//= require jquery +//= require jquery_ujs +//= require jquery.color +//= require jquery.cookie +//= require jquery.Jcrop +//= require jquery.naturalsize +//= require bootstrap +//= require_directory . diff --git a/web/app/assets/javascripts/banner.js b/web/app/assets/javascripts/banner.js new file mode 100644 index 000000000..d0aa78aa7 --- /dev/null +++ b/web/app/assets/javascripts/banner.js @@ -0,0 +1,56 @@ +(function(context,$) { + + "use strict"; + + context.JK = context.JK || {}; + context.JK.Banner = (function() { + var self = this; + var logger = context.JK.logger; + + // responsible for updating the contents of the update dialog + // as well as registering for any event handlers + function show(options) { + var text = options.text; + var html = options.html; + + var newContent = null; + if (html) { + newContent = $('#banner .dialog-inner').html(html); + } + else if(text) { + newContent = $('#banner .dialog-inner').html(text); + } + else { + console.error("unable to show banner for empty message") + return newContent; + } + + $('#banner').show() + $('#banner_overlay').show() + + // return the core of the banner so that caller can attach event handlers to newly created HTML + return newContent; + } + + function hide() { + $('#banner').hide(); + $('#banner_overlay .dialog-inner').html(""); + $('#banner_overlay').hide(); + } + + function initialize() { + + return self; + } + + // Expose publics + var me = { + initialize: initialize, + show : show, + hide : hide + } + + return me; + })(); + +})(window,jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/callbackReceiver.js b/web/app/assets/javascripts/callbackReceiver.js new file mode 100644 index 000000000..e1c8e1725 --- /dev/null +++ b/web/app/assets/javascripts/callbackReceiver.js @@ -0,0 +1,15 @@ +(function(context) { + + "use strict"; + + context.JK = context.JK || {}; + context.JK.Callbacks = { + makeStatic: function(staticName, instanceFunction, thisObject) { + context.JK.Callbacks[staticName] = function() { + var _this = thisObject || context.JK.Callback; + return instanceFunction.apply(_this, arguments); + }; + } + }; + +})(window); \ No newline at end of file diff --git a/web/app/assets/javascripts/clientUpdate.js b/web/app/assets/javascripts/clientUpdate.js new file mode 100644 index 000000000..87a741096 --- /dev/null +++ b/web/app/assets/javascripts/clientUpdate.js @@ -0,0 +1,207 @@ +(function(context,$) { + + "use strict"; + + context.JK = context.JK || {}; + + + context.JK.ClientUpdate = function() { + var self = this; + var logger = context.JK.logger; + + // updated once a download is started + var updateSize = 0; + + function cancelUpdate(e) { + if((e.ctrlKey || e.metaKey) && e.keyCode == 78) { + console.log("update canceled!"); + $('#client_update').hide(); + $('#client_update_overlay').hide(); + } + } + + // responsible for updating the contents of the update dialog + // as well as registering for any event handlers + function updateClientUpdateDialog(templateId, options) { + var template = $('#template-' + templateId).html(); + var templateHtml = context.JK.fillTemplate(template, options); + + $('#client_update .dialog-inner').html(templateHtml); + + $('#client_update').attr('data-mode', templateId); + + // assign click handlers + if(templateId == "update-start") { + + $('body').on('keyup', cancelUpdate); + + $("#client_update a.close-application").click(function() { + // noop atm + return false; + }) + + $("#client_update a.start-update").click(function() { + startDownload(options.uri) + return false; + }) + } + else if(templateId == "update-downloading") { + + $('body').off('keyup', cancelUpdate); + + $("#client_update a.close-application").click(function() { + // noop atm + return false; + }) + } + + $('#client_update').show() + $('#client_update_overlay').show() + } + + /***************************************/ + /******** CALLBACKS FROM BACKEND *******/ + /***************************************/ + function clientUpdateDownloadProgress(bytesReceived, bytesTotal, downloadSpeedMegSec, timeRemaining) { + // this fires way too many times to leave in. uncomment if debugging update feature + //logger.debug("bytesReceived: " + bytesReceived, ", bytesTotal: " + bytesTotal, ", downloadSpeed: " + downloadSpeedMegSec, ", timeRemaining: " + timeRemaining ); + + bytesReceived = Number(bytesReceived) + bytesTotal = Number(bytesTotal) + // bytesTotal from Qt is not trust worthy; trust server's answer instead + $('#progress-bar').width( ((bytesReceived/updateSize) * 100).toString() + "%" ) + //$("#progressbar_detail").text(parseInt(bytesReceived) + "/" + parseInt(updateSize)) + } + + function clientUpdateDownloadSuccess(updateLocation) { + logger.debug("client update downloaded successfully to: " + updateLocation); + + updateClientUpdateDialog("update-restarting"); + + setTimeout(function() { + // This method is synchronous, and does alot of work on a mac in particular, hanging the UI. + // So, we do a sleep loop to make sure the UI is updated with the last message to the user, before we hang the UI + startUpdate(updateLocation); + }, 500); + + } + + function clientUpdateDownloadFailure(errorMsg) { + logger.error("client update download error: " + errorMsg) + + updateClientUpdateDialog("update-error", {error_msg: "Unable to download client update. Error reason:
    " + errorMsg }); + } + + + function clientUpdateLaunchSuccess(updateLocation) { + logger.debug("client update launched successfully to: " + updateLocation); + } + + function clientUpdateLaunchFailure(errorMsg) { + logger.error("client update launch error: " + errorMsg) + + updateClientUpdateDialog("update-error", {error_msg: "Unable to launch client updater. Error reason:
    " + errorMsg}); + } + /********************************************/ + /******** END: CALLBACKS FROM BACKEND *******/ + /********************************************/ + + // if the current version doesn't not match the server version, attempt to do an upgrade + function shouldUpdate(currentVersion, version) { + if(version === undefined || version == null || version == "") { + return false; + } + else { + return currentVersion != version; + } + } + + // check if updated is needed + function check() { + + // check kill switch before all other logic + if(!gon.check_for_client_updates) { + logger.debug("skipping client update because the server is telling us not to") + return; + } + + var product = "JamClient" + var os = context.jamClient.GetOSAsString() + var currentVersion = context.jamClient.ClientUpdateVersion(); + + + if(currentVersion == null || currentVersion.indexOf("Compiled") > -1) { + // this is a developer build; it doesn't make much sense to do an packaged update, so skip + logger.debug("skipping client update check because this is a development build ('" + currentVersion + "')") + return; + } + + // # strange client oddity: remove quotes, if found, from start and finish of version. + if(currentVersion.indexOf('"') == 0 && currentVersion.lastIndexOf('"') == currentVersion.length -1 ) { + currentVersion = currentVersion.substring(1, currentVersion.length - 1); + } + + + $.ajax({ + type: "GET", + url: "/api/versioncheck?product=" + product + "&os=" + os, + success: function(response) { + var version = response.version; + logger.debug("our client version: " + currentVersion + ", server client version: " + version); + + // test url in lieu of having a configured server with a client-update available + + if(shouldUpdate(currentVersion, version)) { + updateSize = response.size; + + // test metadata in lieu of having a configured server with a client-update available + //updateSize = 10000; + //version = "1.2.3" + + // this will update the client dialog to how it should look when an update is just starting + // and show it front-and-center on the screen + updateClientUpdateDialog("update-start", { uri : response.uri } ) + } + }, + error: function(jqXHR, textStatus, errorThrown) { + logger.error("Unable to do a client update check against /api/versioncheck"); + } + }); + } + + function startDownload(url) { + logger.debug("starting client updater download from: " + url); + + updateClientUpdateDialog("update-downloading") + + context.jamClient.ClientUpdateStartDownload(url, + "JK.ClientUpdate.DownloadProgressCallback", + "JK.ClientUpdate.DownloadSuccessCallback", + "JK.ClientUpdate.DownloadFailureCallback"); + } + + function startUpdate(updaterFilePath) { + logger.debug("starting client update from: " + updaterFilePath) + + context.jamClient.ClientUpdateStartUpdate(updaterFilePath, + "JK.ClientUpdate.LaunchUpdateSuccessCallback", + "JK.ClientUpdate.LaunchUpdateFailureCallback"); + + } + + function initialize() { + context.JK.ClientUpdate.DownloadProgressCallback = clientUpdateDownloadProgress; + context.JK.ClientUpdate.DownloadSuccessCallback = clientUpdateDownloadSuccess; + context.JK.ClientUpdate.DownloadFailureCallback = clientUpdateDownloadFailure; + context.JK.ClientUpdate.LaunchUpdateSuccessCallback = clientUpdateLaunchSuccess; + context.JK.ClientUpdate.LaunchUpdateFailureCallback = clientUpdateLaunchFailure; + + return self; + } + + // Expose publics + this.initialize = initialize; + this.check = check; + } + +})(window,jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/configureTrack.js b/web/app/assets/javascripts/configureTrack.js new file mode 100644 index 000000000..ededb4025 --- /dev/null +++ b/web/app/assets/javascripts/configureTrack.js @@ -0,0 +1,834 @@ +(function(context,$) { + + "use strict"; + + context.JK = context.JK || {}; + context.JK.ConfigureTrackDialog = function(app, myTracks, sessionId, sessionModel) { + var logger = context.JK.logger; + var myTrackCount; + + var ASSIGNMENT = { + CHAT: -2, + OUTPUT: -1, + UNASSIGNED: 0, + TRACK1: 1, + TRACK2: 2 + }; + + var VOICE_CHAT = { + NO_CHAT: "0", + CHAT: "1" + }; + + var instrument_array = []; + + // dialog variables + var inputUnassignedList = []; + var track1AudioInputChannels = []; + var track2AudioInputChannels = []; + + var outputUnassignedList = []; + var outputAssignedList = []; + + var chatUnassignedList = []; + var chatAssignedList = []; + + var chatOtherUnassignedList = []; + var chatOtherAssignedList = []; + + var devices = []; + var originalDeviceId; + var originalVoiceChat; + + var configure_audio_instructions = { + "Win32": "Choose the audio profile you would like to use for this session. If needed, use arrow buttons to assign audio inputs " + + "to your tracks, to indicate what instrument you are playing on each track, and to assign audio outputs for listening. " + + "If you want to use a new audio device you have not tested/certified for latency using JamKazam, click the Add New Audio " + + "Gear button to test that device.", + + "MacOSX": "Choose the audio profile you would like to use for this session. If needed, use arrow buttons to assign audio inputs " + + "to your tracks, to indicate what instrument you are playing on each track, and to assign audio outputs for listening. " + + "If you want to use a new audio device you have not tested/certified for latency using JamKazam, click the Add New Audio " + + "Gear button to test that device.", + + "Unix": "Choose the audio profile you would like to use for this session. If needed, use arrow buttons to assign audio inputs " + + "to your tracks, to indicate what instrument you are playing on each track, and to assign audio outputs for listening. " + + "If you want to use a new audio device you have not tested/certified for latency using JamKazam, click the Add New Audio " + + "Gear button to test that device." + }; + + var configure_voice_instructions = "If you are using a microphone to capture your instrumental or vocal audio, you can simply use that mic " + + "for both music and chat. Otherwise, choose a device to use for voice chat, and use arrow buttons to " + + "select an input on that device."; + + function toggleTrack2ConfigDetails(visible) { + if (visible) { + $('#track2-details').show(); + $('#track2-input-buttons').show(); + $('#track1-input').height('92px'); + $('#track1-instrument').height('92px'); + $('#track1-input-buttons').addClass('mt30'); + $('#track1-input-buttons').removeClass('mt65'); + } + else { + $('#track2-details').hide(); + $('#track2-input-buttons').hide(); + $('#track1-input').height('195px'); + $('#track1-instrument').height('195px'); + $('#track1-input-buttons').addClass('mt65'); + $('#track1-input-buttons').removeClass('mt30'); + } + } + + function events() { + + // Music Audio Tab + var $tabConfigureAudio = $('#tab-configure-audio'); + $tabConfigureAudio.unbind("click"); + $tabConfigureAudio.click(function() { + // validate voice chat settings + if (validateVoiceChatSettings(true)) { + showMusicAudioPanel(false); + } + }); + + // Voice Chat Tab + var $tabConfigureVoice = $('#tab-configure-voice'); + $tabConfigureVoice.unbind("click"); + $tabConfigureVoice.click(function() { + // validate audio settings + if (validateAudioSettings(true)) { + showVoiceChatPanel(false); + } + }); + + // Track 1 Add + var $imgTrack1Add = $('#img-track1-input-add'); + $imgTrack1Add.unbind("click"); + $imgTrack1Add.click(function() { + var $unusedMusicInputs = $('#audio-inputs-unused > option:selected'); + _handleTrackInputAdd($unusedMusicInputs, '#track1-input'); + }); + + // Track 2 Add + var $imgTrack2Add = $('#img-track2-input-add'); + $imgTrack2Add.unbind("click"); + $imgTrack2Add.click(function() { + var $unusedMusicInputs = $('#audio-inputs-unused > option:selected'); + _handleTrackInputAdd($unusedMusicInputs, '#track2-input'); + }); + + // Track 1 Remove + var $imgTrack1Remove = $('#img-track1-input-remove'); + $imgTrack1Remove.unbind("click"); + $imgTrack1Remove.click(function() { + _handleTrackInputRemove('#track1-input'); + }); + + // Track 2 Remove + var $imgTrack2Remove = $('#img-track2-input-remove'); + $imgTrack2Remove.unbind("click"); + $imgTrack2Remove.click(function() { + _handleTrackInputRemove('#track2-input'); + }); + + // Audio Output Add + var $imgAudioOutputAdd = $('#img-audio-output-add'); + $imgAudioOutputAdd.unbind("click"); + $imgAudioOutputAdd.click(function() { + var $unusedAudioOutputs = $('#audio-output-unused > option:selected'); + $unusedAudioOutputs.remove().appendTo('#audio-output-selection'); + }); + + // Audio Output Remove + var $imgAudioOutputRemove = $('#img-audio-output-remove'); + $imgAudioOutputRemove.unbind("click"); + $imgAudioOutputRemove.click(function() { + var $usedAudioOutputs = $('#audio-output-selection > option:selected'); + $usedAudioOutputs.remove().appendTo('#audio-output-unused'); + }); + + // Voice Chat Add + var $imgVoiceAdd = $('#img-voice-input-add'); + $imgVoiceAdd.unbind("click"); + $imgVoiceAdd.click(function() { + var $unusedVoiceInputs = $('#voice-inputs-unused > option:selected'); + _handleVoiceInputAdd($unusedVoiceInputs); + }); + + // Voice Chat Remove + var $imgVoiceRemove = $('#img-voice-input-remove'); + $imgVoiceRemove.unbind("click"); + $imgVoiceRemove.click(function() { + var $usedVoiceInputs = $("#voice-inputs-selection > option:selected"); + _handleVoiceInputRemove($usedVoiceInputs); + }); + + $('#audio-drivers').unbind("change"); + $('#audio-drivers').change(function() { + audioDriverChanged(); + }); + + $('#voice-chat-type').unbind("change"); + $('#voice-chat-type').change(function() { + voiceChatChanged(); + }); + + $('#btn-driver-settings').unbind("click"); + $('#btn-driver-settings').click(function() { + context.jamClient.TrackOpenControlPanel(); + }); + + $('#btn-cancel-new-audio').unbind("click"); + $('#btn-cancel-new-audio').click(context.JK.showOverlay); + + $('#btn-error-ok').click(context.JK.showOverlay); + + $('#btn-save-settings').unbind("click"); + $('#btn-save-settings').click(saveSettings); + + $('#btn-cancel-settings').unbind("click"); + $('#btn-cancel-settings').click(cancelSettings); + } + + function _handleTrackInputAdd($selectedMusicInputs, selector) { + $selectedMusicInputs.each(function() { + var deviceId = this.value; + var description = this.text; + $(this).remove().appendTo(selector); + + // if this input exists in the Voice Chat unused box, remove it + var $voiceChatUnused = $('#voice-inputs-unused > option[value="' + deviceId + '"]'); + if ($voiceChatUnused.length > 0) { + logger.debug("Removing " + deviceId + " from Voice Chat Unused"); + $voiceChatUnused.remove(); + } + }); + + _syncVoiceChatType(); + } + + function _handleTrackInputRemove(trackSelector) { + trackSelector = trackSelector + ' > option:selected'; + $(trackSelector).each(function() { + var $removedInput = $(this).remove(); + var $cloneAudio = $removedInput.clone(true, true); + var $cloneChat = $removedInput.clone(true, true); + + $cloneAudio.appendTo('#audio-inputs-unused'); + + // add it to the unused Voice Chat box + if ($('#voice-chat-type').val() == VOICE_CHAT.CHAT) { + $cloneChat.appendTo('#voice-inputs-unused'); + } + }); + + _syncVoiceChatType(); + } + + function _syncVoiceChatType() { + var $option1 = $('#voice-chat-type > option[value="1"]'); + var voiceChatType = $('#voice-chat-type').val(); + + // remove option 1 from voice chat type dropdown if no music (based on what's unused on the Music Audio tab) or chat inputs are available + if ($('#audio-inputs-unused > option').size() === 0 && chatOtherUnassignedList.length === 0 && chatOtherAssignedList.length === 0) { + logger.debug("Removing Option 1 from Voice Chat dropdown."); + $option1.remove(); + } + else { + // make sure it's not already in list before adding back + if ($option1.length === 0) { + logger.debug("Adding Option 1 back to Voice Chat dropdown."); + $('#voice-chat-type').append(''); + } + } + } + + function _handleVoiceInputAdd($selectedVoiceInputs) { + $selectedVoiceInputs.each(function() { + var id = this.value; + + // if this input is in the unused track input box, remove it + var $unusedMusic = $('#audio-inputs-unused > option[value="' + id + '"]'); + if ($unusedMusic.length > 0) { + $unusedMusic.remove(); + } + $(this).remove().appendTo('#voice-inputs-selection'); + }); + } + + function _handleVoiceInputRemove($selectedVoiceInputs) { + $selectedVoiceInputs.each(function() { + var $removedInput = $(this).remove(); + var $cloneAudio = $removedInput.clone(true, true); + var $cloneChat = $removedInput.clone(true, true); + + $cloneChat.appendTo('#voice-inputs-unused'); + + // add it to the unused Music Input box if the selected input is not type "chat" + if (!isChatInput(this.value)) { + $cloneAudio.appendTo('#audio-inputs-unused'); + } + }); + } + + function isChatInput(id) { + // copy the arrays since $.grep modifies them + var chatOtherUnassignedListCopy = chatOtherUnassignedList; + var chatOtherAssignedListCopy = chatOtherAssignedList; + + // is this input in the unassigned list? + $.grep(chatOtherUnassignedListCopy, function(n,i){ + return n.chat && n.id === id; + }); + + // is this input in the assigned list? + $.grep(chatOtherAssignedListCopy, function(n,i){ + return n.chat && n.id === id; + }); + + logger.debug("chatOtherUnassignedListCopy=" + JSON.stringify(chatOtherUnassignedListCopy)); + logger.debug("chatOtherAssignedListCopy=" + JSON.stringify(chatOtherAssignedListCopy)); + + return chatOtherUnassignedListCopy.length > 0 || chatOtherAssignedListCopy.length > 0; + } + + function audioDriverChanged() { + + context.jamClient.TrackSetMusicDevice($('#audio-drivers').val()); + + logger.debug("Called TrackSetMusicDevice with " + $('#audio-drivers').val()); + + context.jamClient.TrackLoadAssignments(); + initDialogData(); + + // refresh dialog + showVoiceChatPanel(true); + showMusicAudioPanel(true); + } + + function voiceChatChanged() { + var voiceChatVal = $('#voice-chat-type').val(); + + logger.debug("voiceChatVal=" + voiceChatVal); + + if (voiceChatVal == VOICE_CHAT.NO_CHAT) { + logger.debug("VOICE_CHAT.NO_CHAT"); + _addSelectedVoiceInputsToMusicInputs(); + + $('#voice-inputs-unused').empty(); + $('#voice-inputs-selection').empty(); + } + else if (voiceChatVal == VOICE_CHAT.CHAT) { + logger.debug("VOICE_CHAT.CHAT"); + + $('#voice-inputs-unused').empty(); + $('#voice-inputs-selection').empty(); + + // add the chat inputs (unassigned and assigned) to the unused box to force the user to select again + context.JK.loadOptions($('#template-option').html(), $('#voice-inputs-unused'), chatOtherUnassignedList, "id", "name", -1); + context.JK.loadOptions($('#template-option').html(), $('#voice-inputs-unused'), chatOtherAssignedList, "id", "name", -1); + + // add each unused music input if it doesn't already exist + $('#audio-inputs-unused > option').each(function() { + if ($('#voice-inputs-unused > option[value="' + this.value + '"]').length === 0) { + $('#voice-inputs-unused').append(''); + } + }); + } + } + + function _addSelectedVoiceInputsToMusicInputs() { + $('#voice-inputs-selection > option').each(function() { + // if this input is not already in the available music inputs box and the selected input is not chat input, add + // it to the unused music inputs box + if ($('#audio-inputs-unused > option[value="' + this.value + '"]').length === 0 && !isChatInput(this.value)) { + $('#audio-inputs-unused').append(''); + } + }); + } + + function configureDriverSettingsButton() { + if (context.jamClient.TrackHasControlPanel()) { + $('#btn-driver-settings').show(); + } + else { + $('#btn-driver-settings').hide(); + } + } + + function showMusicAudioPanel(refreshLists) { + _setInstructions('audio'); + _activateTab('audio'); + + if (refreshLists) { + $('#audio-drivers').empty(); + + // determine correct music device to preselect + var deviceId = context.jamClient.TrackGetMusicDeviceID(); + logger.debug("deviceId = " + deviceId); + + // load Audio Driver dropdown + devices = context.jamClient.TrackGetDevices(); + var keys = Object.keys(devices); + + for (var i=0; i < keys.length; i++) { + var template = $('#template-option').html(); + var isSelected = ""; + if (keys[i] === deviceId) { + isSelected = "selected"; + } + var html = context.JK.fillTemplate(template, { + value: keys[i], + label: devices[keys[i]], + selected: isSelected + }); + + $('#audio-drivers').append(html); + } + + if (deviceId === '') { + context.jamClient.TrackSetMusicDevice($('#audio-drivers').val()); + } + + configureDriverSettingsButton(); + + $('#audio-inputs-unused').empty(); + $('#track1-input').empty(); + $('#track1-instrument').empty(); + $('#track2-input').empty(); + $('#track2-instrument').empty(); + $('#audio-output-unused').empty(); + $('#audio-output-selection').empty(); + + _initMusicTabData(); + + // load Unused Inputs + context.JK.loadOptions($('#template-option').html(), $('#audio-inputs-unused'), inputUnassignedList, "id", "name", -1); + + // load Track 1 Input(s) + context.JK.loadOptions($('#template-option').html(), $('#track1-input'), track1AudioInputChannels, "id", "name", -1); + + // load Track 1 Instrument + var current_instrument = context.jamClient.TrackGetInstrument(ASSIGNMENT.TRACK1); + + // if no instrument is stored on the backend, the user is opening this dialog for the first time after FTUE; + // initialize to the user's first instrument + if (current_instrument === 0) { + if (context.JK.userMe.instruments && context.JK.userMe.instruments.length > 0) { + var instrument_desc = context.JK.userMe.instruments[0].description; + current_instrument = context.JK.server_to_client_instrument_map[instrument_desc].client_id; + } + } + + context.JK.loadOptions($('#template-option').html(), $('#track1-instrument'), instrument_array, "id", "description", current_instrument); + + // load Track 2 config details if necessary + if (myTrackCount > 1) { + // load Track 2 Input(s) + context.JK.loadOptions($('#template-option').html(), $('#track2-input'), track2AudioInputChannels, "id", "name", -1); + + // load Track 2 Instrument + current_instrument = context.jamClient.TrackGetInstrument(ASSIGNMENT.TRACK2); + context.JK.loadOptions($('#template-option').html(), $('#track2-instrument'), instrument_array, "id", "description", current_instrument); + } + + // load Unused Outputs + context.JK.loadOptions($('#template-option').html(), $('#audio-output-unused'), outputUnassignedList, "id", "name", -1); + + // load Session Audio Output + context.JK.loadOptions($('#template-option').html(), $('#audio-output-selection'), outputAssignedList, "id", "name", -1); + } + } + + function showVoiceChatPanel(refreshLists) { + _setInstructions('voice'); + _activateTab('voice'); + + if (refreshLists) { + $('#voice-inputs-unused').empty(); + $('#voice-inputs-selection').empty(); + + _initVoiceChatTabData(); + + var chatOption = $('#voice-chat-type').val(); + + if (chatOption == VOICE_CHAT.CHAT) { + context.JK.loadOptions($('#template-option').html(), $('#voice-inputs-unused'), chatUnassignedList, "id", "name", -1); + context.JK.loadOptions($('#template-option').html(), $('#voice-inputs-selection'), chatAssignedList, "id", "name", -1); + + context.JK.loadOptions($('#template-option').html(), $('#voice-inputs-unused'), chatOtherUnassignedList, "id", "name", -1); + context.JK.loadOptions($('#template-option').html(), $('#voice-inputs-selection'), chatOtherAssignedList, "id", "name", -1); + } + + // disable inputs + else if (chatOption == VOICE_CHAT.NO_CHAT) { + } + } + } + + function _activateTab(type) { + if (type === 'voice') { + $('div[tab-id="music-audio"]').hide(); + $('div[tab-id="voice-chat"]').show(); + + $('#tab-configure-audio').removeClass('selected'); + $('#tab-configure-voice').addClass('selected'); + } + else { + $('div[tab-id="music-audio"]').show(); + $('div[tab-id="voice-chat"]').hide(); + + $('#tab-configure-audio').addClass('selected'); + $('#tab-configure-voice').removeClass('selected'); + } + } + + function initDialogData() { + _initMusicTabData(); + _initVoiceChatTabData(); + } + + function _initMusicTabData() { + inputUnassignedList = _loadList(ASSIGNMENT.UNASSIGNED, true, false); + logger.debug("inputUnassignedList=" + JSON.stringify(inputUnassignedList)); + + track1AudioInputChannels = _loadList(ASSIGNMENT.TRACK1, true, false); + logger.debug("track1AudioInputChannels=" + JSON.stringify(track1AudioInputChannels)); + + track2AudioInputChannels = _loadList(ASSIGNMENT.TRACK2, true, false); + logger.debug("track2AudioInputChannels=" + JSON.stringify(track2AudioInputChannels)); + + outputUnassignedList = _loadList(ASSIGNMENT.UNASSIGNED, false, false); + outputAssignedList = _loadList(ASSIGNMENT.OUTPUT, false, false); + } + + function _initVoiceChatTabData() { + chatUnassignedList = _loadList(ASSIGNMENT.UNASSIGNED, true, false); + logger.debug("chatUnassignedList=" + JSON.stringify(chatUnassignedList)); + + chatAssignedList = _loadList(ASSIGNMENT.CHAT, true, false); + logger.debug("chatAssignedList=" + JSON.stringify(chatAssignedList)); + + chatOtherUnassignedList = _loadList(ASSIGNMENT.UNASSIGNED, true, true); + logger.debug("chatOtherUnassignedList=" + JSON.stringify(chatOtherUnassignedList)); + + chatOtherAssignedList = _loadList(ASSIGNMENT.CHAT, true, true); + logger.debug("chatOtherAssignedList=" + JSON.stringify(chatOtherAssignedList)); + } + + // TODO: copied in addTrack.js - refactor to common place + function _loadList(assignment, input, chat) { + var list = []; + + // get data needed for listboxes + var channels = context.jamClient.TrackGetChannels(); + + var musicDevices = context.jamClient.TrackGetMusicDeviceNames(input); + + // SEE loadList function in TrackAssignGui.cpp of client code + $.each(channels, function(index, val) { + + if (input !== val.input) { + return; + } + + var currAssignment = context.jamClient.TrackGetAssignment(val.id, val.input); + if (assignment !== currAssignment) { + return; + } + + // logger.debug("channel id=" + val.id + ", channel input=" + val.input + ", channel assignment=" + currAssignment + + // ", channel name=" + val.name + ", channel type=" + val.device_type + ", chat=" + val.chat); + + var os = context.jamClient.GetOSAsString(); + if (os === context.JK.OS.WIN32) { + if (chat && ($.inArray(val.device_id, musicDevices) > -1 || context.jamClient.TrackIsMusicDeviceType(val.device_type))) { + return; + } + } + else { + if (chat && ($.inArray(val.device_id, musicDevices) > -1 || !context.jamClient.TrackIsMusicDeviceType(val.device_type))) { + return; + } + } + + if (!chat && $.inArray(val.device_id, musicDevices) === -1) { + return; + } + + if ((chat && !val.chat) || (!chat && val.chat)) { + return; + } + + list.push(val); + }); + + return list; + } + + function saveSettings() { + if (!validateAudioSettings(false)) { + return; + } + + if (!validateVoiceChatSettings(false)) { + return; + } + + saveAudioSettings(); + saveVoiceChatSettings(); + + context.jamClient.TrackSaveAssignments(); + + originalDeviceId = $('#audio-drivers').val(); + app.layout.closeDialog('configure-audio'); + + // refresh Session screen + sessionModel.refreshCurrentSession(); + } + + function saveAudioSettings() { + + context.jamClient.TrackSetMusicDevice($('#audio-drivers').val()); + + // UNASSIGNED INPUTS + $('#audio-inputs-unused > option').each(function() { + logger.debug("Marking " + this.value + " as unassigned input."); + context.jamClient.TrackSetAssignment(this.value, true, ASSIGNMENT.UNASSIGNED); + }); + + // TRACK 1 INPUTS + $('#track1-input > option').each(function() { + logger.debug("Saving track 1 input = " + this.value); + context.jamClient.TrackSetAssignment(this.value, true, ASSIGNMENT.TRACK1); + }); + + // TRACK 1 INSTRUMENT + var instrumentVal = $('#track1-instrument').val(); + var instrumentText = $('#track1-instrument > option:selected').text().toLowerCase(); + + // logger.debug("Saving track 1 instrument = " + instrumentVal); + context.jamClient.TrackSetInstrument(ASSIGNMENT.TRACK1, instrumentVal); + + // UPDATE SERVER + //logger.debug("Updating track " + myTracks[0].trackId + " with instrument " + instrumentText); + var data = {}; + data.instrument_id = instrumentText; + sessionModel.updateTrack(sessionId, myTracks[0].trackId, data); + + if (myTrackCount > 1) { + // TRACK 2 INPUTS + $('#track2-input > option').each(function() { + logger.debug("Saving track 2 input = " + this.value); + context.jamClient.TrackSetAssignment(this.value, true, ASSIGNMENT.TRACK2); + }); + + // TRACK 2 INSTRUMENT + instrumentVal = $('#track2-instrument').val(); + instrumentText = $('#track2-instrument > option:selected').text().toLowerCase(); + + logger.debug("Saving track 2 instrument = " + instrumentVal); + context.jamClient.TrackSetInstrument(ASSIGNMENT.TRACK2, instrumentVal); + + // UPDATE SERVER + //logger.debug("Updating track " + myTracks[1].trackId + " with instrument " + instrumentText); + data.instrument_id = instrumentText; + sessionModel.updateTrack(sessionId, myTracks[1].trackId, data); + } + + // UNASSIGNED OUTPUTS + $('#audio-output-unused > option').each(function() { + logger.debug("Marking " + this.value + " as unassigned output."); + context.jamClient.TrackSetAssignment(this.value, false, ASSIGNMENT.UNASSIGNED); + }); + + // OUTPUT + $('#audio-output-selection > option').each(function() { + logger.debug("Saving session audio output = " + this.value); + context.jamClient.TrackSetAssignment(this.value, false, ASSIGNMENT.OUTPUT); + }); + } + + function saveVoiceChatSettings() { + var voiceChatType = $('#voice-chat-type').val(); + originalVoiceChat = voiceChatType; + + logger.debug("Calling TrackSetChatEnable with value = " + voiceChatType); + context.jamClient.TrackSetChatEnable(voiceChatType == VOICE_CHAT.CHAT ? true : false); + + // UNASSIGNED VOICE CHAT + $('#voice-inputs-unused > option').each(function() { + logger.debug("Marking " + this.value + " as unassigned voice input."); + context.jamClient.TrackSetAssignment(this.value, true, ASSIGNMENT.UNASSIGNED); + }); + + // VOICE CHAT INPUT + $("#voice-inputs-selection > option").each(function() { + logger.debug("Saving chat input = " + this.value); + context.jamClient.TrackSetAssignment(this.value, true, ASSIGNMENT.CHAT); + }); + } + + function cancelSettings() { + logger.debug("Cancel settings"); + + // reset to original device ID + context.jamClient.TrackSetMusicDevice(originalDeviceId); + + $('#voice-chat-type').val(originalVoiceChat); + + app.layout.closeDialog('configure-audio'); + } + + function validateAudioSettings(allowEmptyInput) { + var isValid = true; + var noTrackErrMsg = 'You must assign at least one input port to each of your tracks. Please update your settings to correct this. If you want to delete a track, please return to the session screen and delete the track by clicking the "x" box in the upper right-hand corner of the track.'; + var noInstrumentErrMsg = 'You must specify what instrument is being played for each track. Please update your settings to correct this.'; + var outputErrMsg = 'You must assign two output ports for stereo session audio to hear music. Please update your settings to correct this.'; + + var errMsg; + + // if there are no inputs remaining then allow the user to switch to the Voice Chat tab + // to remove some + if (allowEmptyInput && $('#audio-inputs-unused > option').size() === 0) { + return isValid; + } + else { + // verify Track 1 Input and Instrument exist + if ($('#track1-input > option').size() === 0 || $('#track1-input > option').size() > 2) { + errMsg = noTrackErrMsg; + isValid = false; + } + + if (isValid) { + if ($('#track1-instrument > option:selected').length === 0) { + errMsg = noInstrumentErrMsg; + isValid = false; + } + } + + logger.debug("validateAudioSettings:myTrackCount=" + myTrackCount); + + // if Track 2 exists, verify Input and Instrument exist + if (isValid && myTrackCount > 1) { + if ($('#track2-input > option').size() === 0 || $('#track2-input > option').size() > 2) { + errMsg = noTrackErrMsg; + isValid = false; + } + + if (isValid && $('#track2-instrument > option:selected').length === 0) { + errMsg = noInstrumentErrMsg; + isValid = false; + } + } + + // verify Session Audio Output exists + if (isValid && $('#audio-output-selection > option').size() !== 2) { + errMsg = outputErrMsg; + isValid = false; + } + + if (!isValid) { + context.JK.showErrorDialog(app, errMsg, "invalid settings"); + } + } + + return isValid; + } + + function validateVoiceChatSettings(allowEmptyInput) { + var isValid = true; + var noTrackErrMsg = 'You must select a voice input.'; + var limitExceededErrMsg = 'You cannot select more than 1 voice chat input.'; + var errMsg; + + if (allowEmptyInput && $('#voice-inputs-unused > option').size() === 0) { + return isValid; + } + else { + var chatType = $('#voice-chat-type').val(); + if (chatType == VOICE_CHAT.CHAT) { + var $voiceInputSelection = $('#voice-inputs-selection > option'); + var count = $voiceInputSelection.size(); + if (count === 0) { + errMsg = noTrackErrMsg; + isValid = false; + } + else if (count > 1) { + errMsg = limitExceededErrMsg; + isValid = false; + } + } + else if (chatType == VOICE_CHAT.NO_CHAT) { + // NO VALIDATION NEEDED + } + + if (!isValid) { + context.JK.showErrorDialog(app, errMsg, "invalid settings"); + } + } + + return isValid; + } + + function _setInstructions(type) { + var $instructions = $('#instructions', 'div[layout-id="configure-audio"]'); + if (type === 'audio') { + var os = context.jamClient.GetOSAsString(); + $instructions.html(configure_audio_instructions[os]); + } + else if (type === 'voice') { + $instructions.html(configure_voice_instructions); + } + } + + function _init() { + // load instrument array for populating listboxes, using client_id in instrument_map as ID + context.JK.listInstruments(app, function(instruments) { + $.each(instruments, function(index, val) { + instrument_array.push({"id": context.JK.server_to_client_instrument_map[val.description].client_id, "description": val.description}); + }); + }); + + originalVoiceChat = context.jamClient.TrackGetChatEnable() ? VOICE_CHAT.CHAT : VOICE_CHAT.NO_CHAT; + logger.debug("originalVoiceChat=" + originalVoiceChat); + + $('#voice-chat-type').val(originalVoiceChat); + + originalDeviceId = context.jamClient.TrackGetMusicDeviceID(); + + context.jamClient.TrackLoadAssignments(); + initDialogData(); + + var $option1 = $('#voice-chat-type > option[value="1"]'); + + // remove option 1 from voice chat if none are available and not already assigned + if (inputUnassignedList.length === 0 && chatAssignedList.length === 0 && chatOtherAssignedList.length === 0 && chatOtherUnassignedList.length === 0) { + logger.debug("Removing Option 1 from Voice Chat dropdown."); + $option1.remove(); + } + // add it if it doesn't exist + else { + if ($option1.length === 0) { + logger.debug("Adding Option 1 back to Voice Chat dropdown."); + $('#voice-chat-type').append(''); + } + } + } + + this.initialize = function() { + events(); + _init(); + myTrackCount = myTracks.length; + logger.debug("initialize:myTrackCount=" + myTrackCount); + toggleTrack2ConfigDetails(myTrackCount > 1); + }; + + this.showMusicAudioPanel = showMusicAudioPanel; + this.showVoiceChatPanel = showVoiceChatPanel; + + return this; + }; + + })(window,jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/corp/contact.js b/web/app/assets/javascripts/corp/contact.js new file mode 100644 index 000000000..0b963ab68 --- /dev/null +++ b/web/app/assets/javascripts/corp/contact.js @@ -0,0 +1,81 @@ +(function(context, $) { + + var api = context.JK.Rest(); + + function clearIfNeeded(input) { + var cleared = input.data("cleared"); + + if(!cleared) { + input.val(""); + input.data("cleared", true); + } + } + + function postFeedbackFailure(xhr, textStatus, errorMessage) { + + var errors = JSON.parse(xhr.responseText); + + if(xhr.status == 422) { + + var $feedback = $('#feedback'); + + var email = context.JK.format_errors("email", errors); + var body = context.JK.format_errors("body", errors); + + if(email != null) { + $('input[name=email]', $feedback).closest('div.field').addClass('error').end().after(email); + } + + if(body != null) { + $('textarea[name=body]', $feedback).closest('div.field').addClass('error').end().after(body); + } + } + else {app.ajaxError(xhr, textStatus, errorMessage); + } + } + + function events() { + + var $submit = $('#contact-submit'); + var $feedback = $('#feedback'); + + $feedback.submit(function(e) { + + $submit.val("SENDING..."); + + var $email = $('input[name=email]', $feedback); + var $body = $('textarea[name=body]', $feedback); + + api.postFeedback($email.val(), $body.val()) + .done(function() { + // remove any error fields still present + $('div.field', $feedback).removeClass('error'); + + $submit.val("THANKS!"); + }). + fail(function(xhr, textStatus, errorMessage) { + $submit.val("SEND"); + postFeedbackFailure(xhr, textStatus, errorMessage); + }); + + e.stopPropagation(); + return false; + }); + + $('input[name=email]', $feedback).focus(function() { + clearIfNeeded($(this)); + }); + + $('textarea[name=body]', $feedback).focus(function() { + clearIfNeeded($(this)); + }); + } + + function initialize() { + events(); + } + + context.JK.Corporate.contact = {}; + context.JK.Corporate.contact.initialize = initialize; + +})(window, jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/corp/corporate.js b/web/app/assets/javascripts/corp/corporate.js new file mode 100644 index 000000000..c720ba5d9 --- /dev/null +++ b/web/app/assets/javascripts/corp/corporate.js @@ -0,0 +1,7 @@ +//= require jquery +//= require AAC_underscore +//= require jamkazam +//= require utils +//= require jam_rest +//= require corp/init +//= require_directory ../corp \ No newline at end of file diff --git a/web/app/assets/javascripts/corp/init.js b/web/app/assets/javascripts/corp/init.js new file mode 100644 index 000000000..9bb209302 --- /dev/null +++ b/web/app/assets/javascripts/corp/init.js @@ -0,0 +1 @@ +window.JK.Corporate = {}; \ No newline at end of file diff --git a/web/app/assets/javascripts/corp/navigation.js b/web/app/assets/javascripts/corp/navigation.js new file mode 100644 index 000000000..f918221b0 --- /dev/null +++ b/web/app/assets/javascripts/corp/navigation.js @@ -0,0 +1,6 @@ +$(function() { + // nav highlight + var purpose = $('body').attr('data-purpose'); + + $('#nav a[data-purpose="' + purpose + '"]').addClass('active'); +}); diff --git a/web/app/assets/javascripts/createSession.js b/web/app/assets/javascripts/createSession.js new file mode 100644 index 000000000..31363bcb8 --- /dev/null +++ b/web/app/assets/javascripts/createSession.js @@ -0,0 +1,506 @@ +(function(context,$) { + + "use strict"; + + context.JK = context.JK || {}; + context.JK.CreateSessionScreen = function(app) { + var logger = context.JK.logger; + var realtimeMessaging = context.JK.JamServer; + var friendSelectorDialog = new context.JK.FriendSelectorDialog(app, friendSelectorCallback); + var invitationDialog = new context.JK.InvitationDialog(app); + var autoComplete = null; + var userNames = []; + var userIds = []; + var userPhotoUrls = []; + var MAX_GENRES = 1; + var selectedFriendIds = {}; + var sessionSettings = {}; + + function beforeShow(data) { + userNames = []; + userIds = []; + userPhotoUrls = []; + context.JK.GenreSelectorHelper.render('#create-session-genre'); + resetForm(); + } + + function afterShow(data) { + $.ajax({ + type: "GET", + url: "/api/users/" + context.JK.currentUserId + "/friends", + async: false + }).done(function(response) { + $.each(response, function() { + userNames.push(this.name); + userIds.push(this.id); + userPhotoUrls.push(this.photo_url); + }); + + var autoCompleteOptions = { + lookup: { suggestions: userNames, data: userIds }, + onSelect: addInvitation + }; + if (!autoComplete) { + autoComplete = $('#friend-input').autocomplete(autoCompleteOptions); + } + else { + autoComplete.setOptions(autoCompleteOptions); + } + }); + + // var autoCompleteOptions = { + // serviceUrl: "/api/users/" + context.JK.currentUserId + "/friends", + // minChars: 3, + // dataType: 'jsonp', + // transformResult: function(response) { + // logger.debug("transforming..."); + // logger.debug("response.length=" + response.length); + // return { + // suggestions: $.map(response, function(dataItem) { + // return { value: dataItem.id, data: dataItem.name }; + // }) + // }; + // }, + // onSelect: addInvitation + // }; + + // if (!autoComplete) { + // autoComplete = $('#friend-input').autocomplete(autoCompleteOptions); + // } + // else { + // logger.debug("here2"); + // autoComplete.setOptions(autoCompleteOptions); + // } + } + + function friendSelectorCallback(newSelections) { + var keys = Object.keys(newSelections); + for (var i=0; i < keys.length; i++) { + addInvitation(newSelections[keys[i]].userName, newSelections[keys[i]].userId); + } + } + + function addInvitation(value, data) { + if ($('#selected-friends div[user-id=' + data + ']').length === 0) { + var template = $('#template-added-invitation').html(); + var invitationHtml = context.JK.fillTemplate(template, {userId: data, userName: value}); + $('#selected-friends').append(invitationHtml); + $('#friend-input').select(); + selectedFriendIds[data] = true; + } + else { + $('#friend-input').select(); + context.alert('Invitation already exists for this musician.'); + } + } + + function removeInvitation(evt) { + delete selectedFriendIds[$(evt.currentTarget).parent().attr('user-id')]; + $(evt.currentTarget).closest('.invitation').remove(); + } + + function resetForm() { + $('#intellectual-property').attr('checked', false); + + var $form = $('#create-session-form'); + var description = sessionSettings.hasOwnProperty('description') ? sessionSettings.description : ''; + $('textarea[name="description"]', $form).val(description); + var genre = sessionSettings.hasOwnProperty('genres') && sessionSettings.genres.length > 0 ? sessionSettings.genres[0].id : ''; + context.JK.GenreSelectorHelper.reset('#create-session-genre', genre); + + var musician_access = sessionSettings.hasOwnProperty('musician_access') ? sessionSettings.musician_access : true; + $('#musician-access option[value=' + musician_access + ']').attr('selected', 'selected'); + toggleMusicianAccess(); + + if (musician_access) { + var approval_required = sessionSettings.hasOwnProperty('approval_required') ? sessionSettings.approval_required : false; + $('#musician-access-option-' + approval_required).attr('checked', 'checked'); + } + + var fan_access = sessionSettings.hasOwnProperty('fan_access') ? sessionSettings.fan_access : true; + $('#fan-access option[value=' + fan_access + ']').attr('selected', 'selected'); + toggleFanAccess(); + + if (fan_access) { + var fan_chat = sessionSettings.hasOwnProperty('fan_chat') ? sessionSettings.fan_chat : false; + $('#fan-chat-option-' + fan_chat).attr('checked', 'checked'); + } + // Should easily be able to grab other items out of sessionSettings and put them into the appropriate ui elements. + } + + function validateForm() { + //var errors = []; + var isValid = true; + var $form = $('#create-session-form'); + + // Description can't be empty + var description = $('#description').val(); + if (!description) { + $('#divDescription .error-text').remove(); + $('#divDescription').addClass("error"); + $('#description').after(""); + isValid = false; + } + else { + $('#divDescription').removeClass("error"); + } + + var genres = context.JK.GenreSelectorHelper.getSelectedGenres('#create-session-genre'); + + if (genres.length === 0) { + $('#divGenre .error-text').remove(); + $('#divGenre').addClass("error"); + $('#create-session-genre').after(""); + isValid = false; + } + else { + $('#divGenre').removeClass("error"); + } + + // if (genres.length > MAX_GENRES) { + // errors.push(['#genre-list', "No more than " + MAX_GENRES + "genres are allowed."]); + // } + + var intellectualPropertyChecked = $('#intellectual-property').is(':checked'); + if (!intellectualPropertyChecked) { + $('#divIntellectualProperty .error-text').remove(); + $('#divIntellectualProperty').addClass("error"); + $('#divTerms').after(""); + isValid = false; + } + else { + $('#divIntellectualProperty').removeClass("error"); + } + + return isValid; + } + + function submitForm(evt) { + evt.preventDefault(); + + var isValid = validateForm(); + if (!isValid) { + // app.notify({ + // title: "Validation Errors", + // text: JSON.stringify(formErrors) + // }); + return false; + } + + var data = {}; + data.client_id = app.clientId; + data.description = $('#description').val(); + data.as_musician = true; + data.legal_terms = true; // this overrides the default of 'on', which isn't satisfying our concept of boolean + data.intellectual_property = $('#intellectual-property').is(':checked'); + + data.genres = context.JK.GenreSelectorHelper.getSelectedGenres('#create-session-genre'); + + data.musician_access = $('#musician-access option:selected').val() === "true" ? true : false; + data.approval_required = $("input[name='musician-access-option']:checked").val() === "true" ? true : false; + + data.fan_access = $('#fan-access option:selected').val() === "true" ? true : false; + data.fan_chat = $("input[name='fan-chat-option']:checked").val() === "true" ? true : false; + + if ($('#band-list option:selected').val() !== '') { + data.band = $('#band-list option:selected').val(); + } + + // 1. If no previous session data, a single stereo track with the + // top instrument in the user's profile. + // 2. Otherwise, use the tracks from the last created session. + // Defaulting to 1st instrument in profile always at the moment. + data.tracks = context.JK.TrackHelpers.getUserTracks(context.jamClient); + + var jsonData = JSON.stringify(data); + + console.log("session data=" + jsonData); + + $('#btn-create-session').addClass('button-disabled'); + $('#btn-create-session').bind('click', false); + + var url = "/api/sessions"; + $.ajax({ + type: "POST", + dataType: "json", + contentType: 'application/json', + url: url, + processData:false, + data: jsonData, + success: function(response) { + var newSessionId = response.id; + createInvitations(newSessionId, function() { + context.location = '#/session/' + newSessionId; + }); + // Re-loading the session settings will cause the form to reset with the right stuff in it. + // This is an extra xhr call, but it keeps things to a single codepath + loadSessionSettings(); + $('#btn-create-session').removeClass('button-disabled'); + $('#btn-create-session').unbind('click', false); + }, + error: function() { + app.ajaxError(arguments); + $('#btn-create-session').removeClass('button-disabled'); + $('#btn-create-session').unbind('click', false); + } + }); + return false; + } + + function createInvitations(sessionId, onComplete) { + var callCount = 0; + $('#selected-friends .invitation').each(function(index, invitation) { + callCount++; + var invite_id = $(invitation).attr('user-id'); + var invite = { + music_session: sessionId, + receiver: invite_id + }; + $.ajax({ + type: "POST", + url: "/api/invitations", + data: invite + }).done(function(response) { + callCount--; + }).fail(app.ajaxError); + }); + // TODO - this is the second time I've used this pattern. + // refactor to make a common utility for this. + function checker() { + if (callCount === 0) { + onComplete(); + } else { + context.setTimeout(checker, 10); + } + } + checker(); + } + + function events() { + $('#btn-create-session').on("click", submitForm); + $('#selected-friends').on("click", ".invitation a", removeInvitation); + $('#musician-access').change(toggleMusicianAccess); + $('#fan-access').change(toggleFanAccess); + + $('#btn-choose-friends').click(function() { + friendSelectorDialog.showDialog(selectedFriendIds); + }); + + $('#btn-email-invitation').click(function() { + $('#invitation-textarea-container').show(); + $('#invitation-checkbox-container').hide(); + $('#btn-send-invitation').show(); + $('#btn-next-invitation').hide(); + invitationDialog.showDialog(); + }); + + $('#btn-gmail-invitation').click(function() { + $('#invitation-textarea-container').hide(); + $('#invitation-checkbox-container').show(); + $('#btn-send-invitation').hide(); + $('#btn-next-invitation').show(); + invitationDialog.showDialog(); + $('#invitation-checkboxes').html('
    Loading your contacts...
    '); + window._oauth_callback = function() { + window._oauth_win.close(); + window._oauth_win = null; + window._oauth_callback = null; + $.ajax({ + type: "GET", + url: "/gmail_contacts", + success: function(response) { + $('#invitation-checkboxes').html(''); + for (var i in response) { + $('#invitation-checkboxes').append(""); + } + + $('.invitation-checkbox').change(function() { + var checkedBoxes = $('.invitation-checkbox:checkbox:checked'); + var emails = ''; + for (var i = 0; i < checkedBoxes.length; i++) { + emails += $(checkedBoxes[i]).data('email') + ', '; + } + emails = emails.replace(/, $/, ''); + $('#txt-emails').val(emails); + }); + }, + error: function() { + $('#invitation-checkboxes').html("Load failed"); + } + }); + + }; + window._oauth_win = window.open("/auth/google_login", "_blank", "height=500,width=500,menubar=no,resizable=no,status=no"); + }); + + $('#btn-facebook-invitation').click(function() { + /* + $('#invitation-textarea-container').hide(); + $('#invitation-checkbox-container').show(); + $('#btn-send-invitation').hide(); + $('#btn-next-invitation').show(); + invitationDialog.showDialog(); + $('#invitation-checkboxes').html('
    Loading your contacts...
    '); + */ + window._oauth_callback = function() { + window._oauth_win.close(); + window._oauth_win = null; + window._oauth_callback = null; + /* + $.ajax({ + type: "GET", + url: "/gmail_contacts", + success: function(response) { + $('#invitation-checkboxes').html(''); + for (var i in response) { + $('#invitation-checkboxes').append(""); + } + + $('.invitation-checkbox').change(function() { + var checkedBoxes = $('.invitation-checkbox:checkbox:checked'); + var emails = ''; + for (var i = 0; i < checkedBoxes.length; i++) { + emails += $(checkedBoxes[i]).data('email') + ', '; + } + emails = emails.replace(/, $/, ''); + $('#txt-emails').val(emails); + }); + }, + error: function() { + $('#invitation-checkboxes').html("Load failed"); + } + }); + */ + }; + window._oauth_win = window.open("/auth/facebook_login", "_blank", "height=500,width=500,menubar=no,resizable=no,status=no"); + }); + + + $('#btn-next-invitation').click(function() { + $('#invitation-textarea-container').show(); + $('#invitation-checkbox-container').hide(); + $('#btn-send-invitation').show(); + $('#btn-next-invitation').hide(); + }); + + // friend input focus + $('#friend-input').focus(function() { + $(this).val(''); + }); + + // friend input blur + $('#friend-input').blur(function() { + $(this).val('Type a friend\'s name'); + }); + } + + function toggleMusicianAccess() { + var value = $("#musician-access option:selected").val(); + if (value == "false") { + $("input[name='musician-access-option']").attr('disabled', 'disabled'); + $("input[name='musician-access-option']").parent().addClass("op50"); + } + else { + $("input[name='musician-access-option']").removeAttr('disabled'); + $("input[name='musician-access-option']").parent().removeClass("op50"); + } + } + + function toggleFanAccess() { + var value = $("#fan-access option:selected").val(); + if (value == "false") { + $("input[name='fan-chat-option']").attr('disabled', 'disabled'); + $("input[name='fan-chat-option']").parent().addClass("op50"); + } + else { + $("input[name='fan-chat-option']").removeAttr('disabled'); + $("input[name='fan-chat-option']").parent().removeClass("op50"); + } + } + + function loadBands() { + var url = "/api/users/" + context.JK.currentUserId + "/bands"; + $.ajax({ + type: "GET", + url: url, + success: bandsLoaded + }); + } + + function bandsLoaded(response) { + $.each(response, function() { + var template = $('#template-band-option').html(); + var bandOptionHtml = context.JK.fillTemplate(template, {value: this.id, label: this.name}); + $('#band-list').append(bandOptionHtml); + }); + } + + function loadSessionSettings() { + var url = "/api/users/" + context.JK.currentUserId + "/session_settings"; + $.ajax({ + type: "GET", + url: url, + success: sessionSettingsLoaded + }); + } + + function sessionSettingsLoaded(response) { + if (response != null) + { + sessionSettings = response; + } + resetForm(); + } + + function searchFriends(query) { + if (query.length < 2) { + $('#friend-search-results').empty(); + return; + } + var url = "/api/search?query=" + query + "&userId=" + context.JK.currentUserId; + $.ajax({ + type: "GET", + url: url, + success: friendSearchComplete + }); + } + + function friendSearchComplete(response) { + // reset search results each time + $('#friend-search-results').empty(); + + // loop through each + $.each(response.friends, function() { + // only show friends who are musicians + if (this.musician === true) { + var template = $('#template-friend-search-results').html(); + var searchResultHtml = context.JK.fillTemplate(template, {userId: this.id, name: this.first_name + ' ' + this.last_name}); + $('#friend-search-results').append(searchResultHtml); + $('#friend-search-results').attr('style', 'display:block'); + } + }); + } + + function initialize() { + friendSelectorDialog.initialize(); + invitationDialog.initialize(); + events(); + loadBands(); + loadSessionSettings(); + var screenBindings = { 'beforeShow': beforeShow, 'afterShow': afterShow }; + app.bindScreen('createSession', screenBindings); + } + + // Expose publics + this.initialize = initialize; + this.resetForm = resetForm; + this.submitForm = submitForm; + this.validateForm = validateForm; + this.loadBands = loadBands; + this.searchFriends = searchFriends; + this.addInvitation = addInvitation; + + return this; + }; + + })(window,jQuery); diff --git a/web/app/assets/javascripts/faderHelpers.js b/web/app/assets/javascripts/faderHelpers.js new file mode 100644 index 000000000..951ee5e4c --- /dev/null +++ b/web/app/assets/javascripts/faderHelpers.js @@ -0,0 +1,226 @@ +/** +* Functions related to faders (slider controls for volume). +* These functions are intimately tied to the markup defined in +* the templates in _faders.html.erb +*/ +(function(g, $) { + + "use strict"; + + g.JK = g.JK || {}; + + var $draggingFaderHandle = null; + var $draggingFader = null; + + var subscribers = {}; + var logger = g.JK.logger; + var MAX_VISUAL_FADER = 95; + + function faderClick(evt) { + evt.stopPropagation(); + if (g.JK.$draggingFaderHandle) { + return; + } + var $fader = $(evt.currentTarget); + var faderId = $fader.closest('[fader-id]').attr("fader-id"); + var $handle = $fader.find('div[control="fader-handle"]'); + + var faderPct = faderValue($fader, evt); + + // Notify subscribers of value change + g._.each(subscribers, function(changeFunc, index, list) { + if (faderId === index) { + changeFunc(faderId, faderPct, false); + } + }); + + setHandlePosition($fader, faderPct); + return false; + } + + function setHandlePosition($fader, value) { + if (value > MAX_VISUAL_FADER) { value = MAX_VISUAL_FADER; } // Visual limit + var $handle = $fader.find('div[control="fader-handle"]'); + var handleCssAttribute = getHandleCssAttribute($fader); + $handle.css(handleCssAttribute, value + '%'); + } + + + function faderHandleDown(evt) { + evt.stopPropagation(); + $draggingFaderHandle = $(evt.currentTarget); + $draggingFader = $draggingFaderHandle.closest('div[control="fader"]'); + return false; + } + + function faderMouseUp(evt) { + evt.stopPropagation(); + if ($draggingFaderHandle) { + var $fader = $draggingFaderHandle.closest('div[control="fader"]'); + var faderId = $fader.closest('[fader-id]').attr("fader-id"); + var faderPct = faderValue($fader, evt); + // Notify subscribers of value change + g._.each(subscribers, function(changeFunc, index, list) { + if (faderId === index) { + changeFunc(faderId, faderPct, false); + } + }); + $draggingFaderHandle = null; + $draggingFader = null; + } + return false; + } + + function faderValue($fader, evt) { + var orientation = $fader.attr('orientation'); + var getPercentFunction = getVerticalFaderPercent; + var absolutePosition = evt.clientY; + if (orientation && orientation == 'horizontal') { + getPercentFunction = getHorizontalFaderPercent; + absolutePosition = evt.clientX; + } + return getPercentFunction(absolutePosition, $fader); + } + + function getHandleCssAttribute($fader) { + var orientation = $fader.attr('orientation'); + return (orientation === 'horizontal') ? 'left' : 'bottom'; + } + + function faderMouseMove(evt) { + // bail out early if there's no in-process drag + if (!($draggingFaderHandle)) { + return false; + } + var $fader = $draggingFader; + var faderId = $fader.closest('[fader-id]').attr("fader-id"); var $handle = $draggingFaderHandle; + evt.stopPropagation(); + var faderPct = faderValue($fader, evt); + + // Notify subscribers of value change + g._.each(subscribers, function(changeFunc, index, list) { + if (faderId === index) { + changeFunc(faderId, faderPct, true); + } + }); + + if (faderPct > MAX_VISUAL_FADER) { faderPct = MAX_VISUAL_FADER; } // Visual limit + var handleCssAttribute = getHandleCssAttribute($fader); + $handle.css(handleCssAttribute, faderPct + '%'); + return false; + } + + function getVerticalFaderPercent(eventY, $fader) { + return getFaderPercent(eventY, $fader, 'vertical'); + } + + function getHorizontalFaderPercent(eventX, $fader) { + return getFaderPercent(eventX, $fader, 'horizontal'); + } + + /** + * Returns the current value of the fader as int percent 0-100 + */ + function getFaderPercent(value, $fader, orientation) { + var faderPosition = $fader.offset(); + var faderMin = faderPosition.top; + var faderSize = $fader.height(); + var handleValue = (faderSize - (value-faderMin)); + if (orientation === "horizontal") { + faderMin = faderPosition.left; + faderSize = $fader.width(); + handleValue = (value - faderMin); + } + var faderPct = Math.round(handleValue/faderSize * 100); + if (faderPct < 0) { + faderPct = 0; + } + if (faderPct > 100) { + faderPct = 100; + } + return faderPct; + } + + g.JK.FaderHelpers = { + + /** + * Subscribe to fader change events. Provide a subscriber id + * and a function in the form: change(faderId, newValue) which + * will be called anytime a fader changes value. + */ + subscribe: function(subscriberId, changeFunction) { + subscribers[subscriberId] = changeFunction; + }, + + /** + * Render a fader into the element identifed by the provided + * selector, with the provided options. + */ + renderFader: function(selector, userOptions) { + if (userOptions === undefined) { + throw ("renderFader: userOptions is required"); + } + if (!(userOptions.hasOwnProperty("faderId"))) { + throw ("renderFader: userOptions.faderId is required"); + } + var renderDefaults = { + faderType: "vertical", + height: 83, // only used for vertical + width: 83 // only used for horizontal + }; + var options = $.extend({}, renderDefaults, userOptions); + var templateSelector = (options.faderType === 'horizontal') ? + "#template-fader-h" : '#template-fader-v'; + var templateSource = $(templateSelector).html(); + + $(selector).html(g._.template(templateSource, options)); + // Embed any custom styles, applied to the .fader below selector + if ("style" in options) { + for (var key in options.style) { + $(selector + ' .fader').css(key, options.style[key]); + } + } + }, + + convertLinearToDb: function(input) { + var temp; + temp = 0.0; + // coefficients + var a = -8.0013990435159329E+01; + var b = 4.1755639785242042E+00; + var c = -1.4036729740086906E-01; + var d = 3.1788545454166156E-03; + var f = -3.5148685730880861E-05; + var g = 1.4221429222004657E-07; + temp += a + + b * input + + c * Math.pow(input, 2.0) + + d * Math.pow(input, 3.0) + + f * Math.pow(input, 4.0) + + g * Math.pow(input, 5.0); + + return temp; + }, + + setFaderValue: function(faderId, faderValue) { + var $fader = $('[fader-id="' + faderId + '"]'); + this.setHandlePosition($fader, faderValue); + }, + + setHandlePosition: function($fader, faderValue) { + setHandlePosition($fader, faderValue); + }, + + initialize: function() { + $('body').on('click', 'div[control="fader"]', faderClick); + $('body').on('mousedown', 'div[control="fader-handle"]', faderHandleDown); + $('body').on('mousemove', 'div[layout-id="session"], [layout-wizard="ftue"]', faderMouseMove); + $('body').on('mouseup', 'div[layout-id="session"], [layout-wizard="ftue"]', faderMouseUp); + //$('body').on('mousemove', faderMouseMove); + //$('body').on('mouseup', faderMouseUp); + } + + }; + + +})(window, jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/fakeJamClient.js b/web/app/assets/javascripts/fakeJamClient.js new file mode 100644 index 000000000..662859ea2 --- /dev/null +++ b/web/app/assets/javascripts/fakeJamClient.js @@ -0,0 +1,592 @@ +(function(context,$) { + + "use strict"; + + context.JK = context.JK || {}; + context.JK.FakeJamClient = function(app) { + var logger = context.JK.logger; + logger.info("*** Fake JamClient instance initialized. ***"); + + // Change this to false if you want to see FTUE with fake jam client + var ftueStatus = true; + var eventCallbackName = ''; + var eventCallbackRate = 1000; + var vuValue = -70; + var vuChange = 10; + var callbackTimer = null; + var _mix = -30; + var device_id = -1; + var latencyCallback = null; + var frameSize = 2.5; + + function dbg(msg) { logger.debug('FakeJamClient: ' + msg); } + + // Bummer that javascript doesn't have much in the way of reflection. + // arguments.callee.name would probably do what we want, but it's deprecated + // and not allowed when using "strict" + // (Wishing I could write a single function which debug logs the name of the + // current function and a JSON stringify of the arguments). + + function OpenSystemBrowser(href) { + dbg("OpenSystemBrowser('" + href + "')"); + context.window.open(href); + } + function FTUEGetInputLatency() { + dbg("FTUEGetInputLatency"); + return 2; + } + function FTUESetInputLatency(latency) { + dbg("FTUESetInputLatency:" + latency); + } + function FTUEGetOutputLatency() { + dbg("FTUEGetOutputLatency"); + return 2; + } + function FTUESetOutputLatency(latency) { + dbg("FTUESetOutputLatency:" + latency); + } + function FTUEGetVolumeRanges () { + dbg("FTUEGetVolumeRanges"); + return { + input_maximum: 20, + input_minimum: -80, + output_maximum: 20, + output_minimum: -80 + }; + } + function FTUEHasControlPanel() { + dbg("FTUEHasControlPanel"); + return true; + } + function FTUEGetFrameSize(newSize) { + dbg("FTUEGetFrameSize"); + return frameSize; + } + function FTUESetFrameSize(newSize) { + dbg("FTUESetFrameSize:" + newSize); + // one of 2.5, 5 or 10 + frameSize = newSize; + } + function FTUEOpenControlPanel() { + dbg("FTUEOpenControlPanel"); + context.alert("Simulated ASIO Dialog"); + } + function FTUEInit() { dbg("FTUEInit"); } + function FTUERefreshDevices() { dbg("FTUERefreshDevices()"); } + function FTUESave(b) { dbg("FTUESave(" + b + ")"); } + function FTUEGetStatus() { return ftueStatus; } + function FTUESetStatus(b) { ftueStatus = b; } + function FTUESetMusicDevice(id) { dbg("FTUESetMusicDevice"); } + function FTUEGetDevices() { + dbg('FTUEGetMusicDevices'); + return { + "ASIO4ALL v2":"ASIO4ALL v2 - ASIO", + "M-Audio FW ASIO":"M-AUDIO FW ASIO - ASIO" + }; + } + function FTUEGetMusicInputs() { + dbg('FTUEGetMusicInputs'); + return { + "i~11~MultiChannel (FW AP Multi)~0^i~11~Multichannel (FW AP Multi)~1": + "Multichannel (FW AP Multi) - Channel 1/Multichannel (FW AP Multi) - Channel 2" + }; + } + function FTUEGetMusicOutputs() { + dbg('FTUEGetMusicOutputs'); + return { + "o~11~Multichannel (FW AP Multi)~0^o~11~Multichannel (FW AP Multi)~1": + "Multichannel (FW AP Multi) - Channel 1/Multichannel (FW AP Multi) - Channel 2" + }; + } + function FTUEGetChatInputs() { + dbg('FTUEGetChatInputs'); + return { + "i~11~MultiChannel (FW AP Multi)~0^i~11~Multichannel (FW AP Multi)~1": + "Multichannel (FW AP Multi) - Channel 1/Multichannel (FW AP Multi) - Channel 2" + }; + } + function FTUESetMusicInput() { dbg('FTUESetMusicInput'); } + function FTUESetChatInput() { dbg('FTUESetChatInput'); } + function FTUESetMusicOutput() { dbg('FTUESetMusicOutput'); } + function FTUEGetInputVolume() { dbg('FTUEGetInputVolume'); return -60; } + function FTUESetInputVolume() { dbg('FTUESetInputVolume'); } + function FTUEGetOutputVolume() { dbg('FTUEGetOutputVolume'); return -40; } + function FTUESetOutputVolume() { dbg('FTUESetOutputVolume'); } + function FTUEGetChatInputVolume() { dbg('FTUEGetChatInputVolume'); return -10; } + function FTUESetChatInputVolume() { dbg('FTUESetChatInputVolume'); } + function FTUERegisterVUCallbacks() { + dbg('FTUERegisterVUCallbacks'); + } + function FTUERegisterLatencyCallback(functionName) { + dbg('FTUERegisterLatencyCallback'); + latencyCallback = functionName; + } + function FTUEStartLatency() { + function cb() { + // Change 4192 to modify latency MS results (in microseconds) + eval(latencyCallback + "(10.0)"); + } + context.setTimeout(cb, 1000); + } + + function RegisterVolChangeCallBack(functionName) { + dbg('RegisterVolChangeCallBack'); + } + + + function GetOS() { return 100000000; } + function GetOSAsString() { + return "Win32"; + //return "Mac"; + } + + function LatencyUpdated(map) { dbg('LatencyUpdated:' + JSON.stringify(map)); } + function LeaveSession(map) { dbg('LeaveSession:' + JSON.stringify(map)); } + function P2PMessageReceived(s1,s2) { dbg('P2PMessageReceived:' + s1 + ',' + s2); } + function JoinSession(sessionId) {dbg('JoinSession:' + sessionId);} + function ParticipantLeft(session, participant) { + dbg('ParticipantLeft:' + JSON.stringify(session) + ',' + + JSON.stringify(participant)); + } + function ParticipantJoined(session, participant) { + dbg('ParticipantJoined:' + JSON.stringify(session) + ',' + + JSON.stringify(participant)); + } + function RecordTestBegin() { dbg('RecordTestBegin'); } + function RecordTestEnd() { dbg('RecordTestBegin'); } + function RecordTestPlayback() { dbg('RecordTestBegin'); } + function SendP2PMessage(s1,s2) { dbg('SendP2PMessage:' + s1 + ',' + s2); } + function SetASIOEnabled(i1, i2, i3, i4, i5, i6) { + dbg('SetASIOEnabled(' + i1 + ',' + i2 + ',' + i3 + ',' + i4 + ',' + i5 + ',' + i6 + ')'); + } + function SignalLatencyUpdated(client) { + dbg('SignalLatencyUpdated:' + JSON.stringify(client)); + } + function SignalSendP2PMessage(s1,ba) { + dbg('SignalSendP2PMessage:' + JSON.stringify(arguments)); + } + function StartPlayTest(s) { dbg('StartPlayTest' + JSON.stringify(arguments)); } + function StartRecordTest(s) { dbg('StartRecordTest' + JSON.stringify(arguments)); } + function StartRecording(map) { dbg('StartRecording' + JSON.stringify(arguments)); } + function StopPlayTest() { dbg('StopPlayTest'); } + function StopRecording(map) { dbg('StopRecording' + JSON.stringify(arguments)); } + function TestASIOLatency(s) { dbg('TestASIOLatency' + JSON.stringify(arguments)); } + + function TestLatency(clientID, callbackFunctionName, timeoutCallbackName) { + logger.debug("Fake JamClient: TestLatency called with client, " + clientID + " and callback function name: " + callbackFunctionName); + var response = { + clientID: clientID, + latency: 50 + }; + var js = callbackFunctionName + "(" + JSON.stringify(response) + ");"; + eval(js); + } + + function GetASIODevices() { + var response =[{"device_id":0,"device_name":"Realtek High Definition Audio","device_type": 0,"interfaces":[{"interface_id":0,"interface_name":"Realtek HDA SPDIF Out","pins":[{"is_input":false,"pin_id":0,"pin_name":"PC Speaker"}]},{"interface_id":1,"interface_name":"Realtek HD Audio rear output","pins":[{"is_input":false,"pin_id":0,"pin_name":"PC Speaker"}]},{"interface_id":2,"interface_name":"Realtek HD Audio Mic input","pins":[{"is_input":true,"pin_id":0,"pin_name":"Recording Control"}]},{"interface_id":3,"interface_name":"Realtek HD Audio Line input","pins":[{"is_input":true,"pin_id":0,"pin_name":"Recording Control"}]},{"interface_id":4,"interface_name":"Realtek HD Digital input","pins":[{"is_input":true,"pin_id":0,"pin_name":"Capture"}]},{"interface_id":5,"interface_name":"Realtek HD Audio Stereo input","pins":[{"is_input":true,"pin_id":0,"pin_name":"Recording Control"}]}],"wavert_supported":false},{"device_id":1,"device_name":"M-Audio FW Audiophile","device_type": 1,"interfaces":[{"interface_id":0,"interface_name":"FW AP Multi","pins":[{"is_input":false,"pin_id":0,"pin_name":"Output"},{"is_input":true,"pin_id":1,"pin_name":"Input"}]},{"interface_id":1,"interface_name":"FW AP 1/2","pins":[{"is_input":false,"pin_id":0,"pin_name":"Output"},{"is_input":true,"pin_id":1,"pin_name":"Input"}]},{"interface_id":2,"interface_name":"FW AP SPDIF","pins":[{"is_input":false,"pin_id":0,"pin_name":"Output"},{"is_input":true,"pin_id":1,"pin_name":"Input"}]},{"interface_id":3,"interface_name":"FW AP 3/4","pins":[{"is_input":false,"pin_id":0,"pin_name":"Output"}]}],"wavert_supported":false},{"device_id":2,"device_name":"Virtual Audio Cable","device_type": 2,"interfaces":[{"interface_id":0,"interface_name":"Virtual Cable 2","pins":[{"is_input":true,"pin_id":0,"pin_name":"Capture"},{"is_input":false,"pin_id":1,"pin_name":"Output"}]},{"interface_id":1,"interface_name":"Virtual Cable 1","pins":[{"is_input":true,"pin_id":0,"pin_name":"Capture"},{"is_input":false,"pin_id":1,"pin_name":"Output"}]}],"wavert_supported":false},{"device_id":3,"device_name":"WebCamDV WDM Audio Capture","device_type": 3,"interfaces":[{"interface_id":0,"interface_name":"WebCamDV Audio","pins":[{"is_input":true,"pin_id":0,"pin_name":"Recording Control"},{"is_input":false,"pin_id":1,"pin_name":"Volume Control"}]}],"wavert_supported":false}]; + return response; + } + + // Session Functions + function SessionAddTrack() {} + function SessionGetControlState(mixerIds) { + dbg("SessionGetControlState"); + var groups = [0, 1, 2, 3, 7, 9]; + var names = [ + "FW AP Multi", + "FW AP Multi", + "FW AP Multi", + "FW AP Multi", + "", + "" + ]; + var clientIds = [ + "", + "", + "", + "", + "3933ebec-913b-43ab-a4d3-f21dc5f8955b", + "" + ]; + var response = []; + for (var i=0; i 10 || vuValue < -70) { vuChange = vuChange * -1; } + } + function SetVURefreshRate(rateMS) { + eventCallbackRate = rateMS; + if (callbackTimer) { context.clearInterval(callbackTimer); } + if (eventCallbackName) { + callbackTimer = context.setInterval(doCallbacks, eventCallbackRate); + } + } + + // Track Functions + + + // Returns a list of objects representing all available audio devices and + // pins on the current system. On my windows box, I get 38 objects back. + // First couple examples included here. Note that the list tends to come + // back with all of the left inputs, then all the right inputs, then all + // the left outputs, then all the right outputs. + function TrackGetChannels() { + // Real example: + /* +{ + device_id: "1394\\M-Audio&FW_Audiophile", + full_id: "i~1~\\\\?\\1394#m-audio&fw_audiophile#d4eb0700036c0d00#{6994ad04-93ef-11d0-a3cc-00a0c9223196}\\fwap_12", + id: "\\\\?\\1394#m-audio&fw_audiophile#d4eb0700036c0d00#{6994ad04-93ef-11d0-a3cc-00a0c9223196}\\fwap_12", + input: true, + left: true, + name: "M-Audio FW Audiophile: FW AP 1/2 - Left" +}, + */ + + // But we'll just build a list of names and fake it + var devices = [ + "M-Audio FW Audiophile: FW AP 1/2" + //"M-Audio FW Audiophile: FW Multi 1/2", + //"M-Audio FW Audiophile: FW SPDIF 1/2", + //"Realtek High Definition Audio: Realtek HD Digital", + // "Realtek High Definition Audio: Realtek HD Line", + // "Realtek High Definition Audio: Realtek HD Audio Mic", + // "Realtek High Definition Audio: Realtek HD Audio Stereo", + //"WebCamDV WDM Audio Capture: WebCamDV Audio" + ]; + var suffixes = [ " - Left", " - Right", " - Left", " - Right"]; + var lefts = [true, false, true, false]; + var inputs = [true, true, false, false]; + var types = ["music", "non-music", "music", "non-music"]; + var response = []; + var name, o, i, j; + for (j=0; j<4; j++) { + for (i=0; i 0) { + queryString += "participants=" + musicians.join(','); + } + + // genre filter + var genres = context.JK.GenreSelectorHelper.getSelectedGenres('#find-session-genre'); + if (genres !== null && genres.length > 0) { + if (queryString.length > 0) { + queryString += "&"; + } + + queryString += "genres=" + genres.join(','); + } + + // keyword filter + var keyword = $('#session-keyword-srch').val(); + if (keyword !== null && keyword.length > 0 && keyword !== 'Search by Keyword') { + if (queryString.length > 0) { + queryString += "&"; + } + + queryString += "keyword=" + $('#session-keyword-srch').val(); + } + + if (queryString.length > 0) { + $.ajax({ + type: "GET", + url: "/api/sessions?" + queryString, + async: false, + success: startSessionLatencyChecks + }); + } + else { + loadSessions(); + } + } + + function refreshDisplay() { + var priorVisible; + + var INVITATION = 'div#sessions-invitations'; + var FRIEND = 'div#sessions-friends'; + var OTHER = 'div#sessions-other'; + + // INVITATION + logger.debug("sessionCounts[CATEGORY.INVITATION.index]=" + sessionCounts[CATEGORY.INVITATION.index]); + if (sessionCounts[CATEGORY.INVITATION.index] === 0) { + priorVisible = false; + $(INVITATION).hide(); + } + else { + priorVisible = true; + $(INVITATION).show(); + } + + // FRIEND + if (!priorVisible) { + $(FRIEND).removeClass('mt35'); + } + + logger.debug("sessionCounts[CATEGORY.FRIEND.index]=" + sessionCounts[CATEGORY.FRIEND.index]); + if (sessionCounts[CATEGORY.FRIEND.index] === 0) { + priorVisible = false; + $(FRIEND).hide(); + } + else { + priorVisible = true; + $(FRIEND).show(); + } + + // OTHER + if (!priorVisible) { + $(OTHER).removeClass('mt35'); + } + + logger.debug("sessionCounts[CATEGORY.OTHER.index]=" + sessionCounts[CATEGORY.OTHER.index]); + if (sessionCounts[CATEGORY.OTHER.index] === 0) { + $(OTHER).hide(); + } + else { + $(OTHER).show(); + } + } + + function getSelectedMusicians() { + var selectedMusicians = []; + // $('#musician-list-items :checked').each(function() { + // selectedMusicians.push($(this).val()); + // }); + + var selectedVal = $('#musician-list').val(); + if (selectedVal !== '') { + selectedMusicians.push(selectedVal); + } + + return selectedMusicians; + } + + function startSessionLatencyChecks(response) { + logger.debug("Starting latency checks"); + sessionLatency.subscribe(app.clientId, latencyResponse); + $.each(response, function(index, session) { + sessions[session.id] = session; + sessionLatency.sessionPings(session); + }); + } + + function containsInvitation(session) { + var i, invitation = null; + // user has invitations for this session + for (i=0; i < session.invitations.length; i++) { + invitation = session.invitations[i]; + // session contains an invitation for this user + if (invitation.receiver_id == context.JK.currentUserId) { + return true; + } + } + + return false; + } + + function containsFriend(session) { + var i, participant = null; + + for (i=0; i < session.participants.length; i++) { + participant = session.participants[i]; + // this session participant is a friend + if (participant !== null && participant !== undefined && participant.user.is_friend) { + return true; + } + } + return false; + } + + function latencyResponse(sessionId) { + logger.debug("Received latency response for session " + sessionId); + renderSession(sessionId); + } + + /** + * Not used normally. Allows modular unit testing + * of the renderSession method without having to do + * as much heavy setup. + */ + function setSession(session) { + invitationSessionGroup[session.id] = session; + } + + /** + * Render a single session line into the table. + * It will be inserted at the appropriate place according to the + * sortScore in sessionLatency. + */ + function renderSession(sessionId) { + + // store session in the appropriate bucket and increment category counts + var session = sessions[sessionId]; + if (containsInvitation(session)) { + invitationSessionGroup[sessionId] = session; + sessionCounts[CATEGORY.INVITATION.index]++; + } + else if (containsFriend(session)) { + friendSessionGroup[sessionId] = session; + sessionCounts[CATEGORY.FRIEND.index]++; + } + else { + otherSessionGroup[sessionId] = session; + sessionCounts[CATEGORY.OTHER.index]++; + } + + // hack to prevent duplicate rows from being rendered when filtering + var sessionAlreadyRendered = false; + var $tbGroup; + + logger.debug('Rendering session ID = ' + sessionId); + + if (invitationSessionGroup[sessionId] != null) { + $tbGroup = $(CATEGORY.INVITATION.id); + + if ($("table#sessions-invitations tr[id='" + sessionId + "']").length > 0) { + sessionAlreadyRendered = true; + } + } + else if (friendSessionGroup[sessionId] != null) {; + $tbGroup = $(CATEGORY.FRIEND.id); + + if ($("table#sessions-friends tr[id='" + sessionId + "']").length > 0) { + sessionAlreadyRendered = true; + } + } + else if (otherSessionGroup[sessionId] != null) { + $tbGroup = $(CATEGORY.OTHER.id); + + if ($("table#sessions-other tr[id='" + sessionId + "']").length > 0) { + sessionAlreadyRendered = true; + } + } + else { + logger.debug('ERROR: No session with ID = ' + sessionId + ' found.'); + return; + } + + if (!sessionAlreadyRendered) { + var row = sessionList.renderSession(session, sessionLatency, $tbGroup, $('#template-session-row').html(), $('#template-musician-info').html(), + // populate the musician filter with musicians that haven't already been added + function(musicianArray) { + var template = $('#template-musician-option').html(); + $.each(musicianArray, function(index, val) { + // check if this musician is already in the filter + if ( $('#musician-list option[value=' + val.id + ']').length === 0 ) { + var musicianOptionHtml = context.JK.fillTemplate(template, {value: val.id, label: val.name}); + $('#musician-list').append(musicianOptionHtml); + } + }); + }); + } + + refreshDisplay(); + } + + // TODO: refactor this and GenreSelector into common code + // function toggleMusicianBox() { + // var boxHeight = $('#musician-list').css("height"); + // // TODO: clean this up (check class name of arrow to determine current state) + // if (boxHeight == "20px") { + // $('#musician-list').css({height: "auto"}); + // $('#musician-list-arrow').removeClass("arrow-down").addClass("arrow-up"); + + // } + // else { + // $('#musician-list').css({height: "20px"}); + // $('#musician-list-arrow').removeClass("arrow-up").addClass("arrow-down"); + // } + // } + + function beforeShow(data) { + context.JK.GenreSelectorHelper.render('#find-session-genre'); + } + + function afterShow(data) { + clearResults(); + refreshDisplay(); + loadSessions(); + } + + function clearResults() { + $('table#sessions-invitations').children(':not(:first-child)').remove(); + $('table#sessions-friends').children(':not(:first-child)').remove(); + $('table#sessions-other').children(':not(:first-child)').remove(); + + sessionCounts = [0, 0, 0]; + + sessions = {}; + invitationSessionGroup = {}; + friendSessionGroup = {}; + otherSessionGroup = {}; + } + + function deleteSession(evt) { + var sessionId = $(evt.currentTarget).attr("action-id"); + if (sessionId) { + $.ajax({ + type: "DELETE", + url: "/api/sessions/" + sessionId + }).done(loadSessions); + } + } + + function events() { + //$('#findSession-tableBody').on("click", '[action="delete"]', deleteSession); + // $('#musician-list-header').on("click", toggleMusicianBox); + // $('#musician-list-arrow').on("click", toggleMusicianBox); + + $('#session-keyword-srch').focus(function() { + $(this).val(''); + }); + + $('#btn-refresh').on("click", search); + } + + /** + * Initialize, providing an instance of the SessionLatency class. + */ + function initialize(latency) { + + var screenBindings = { + 'beforeShow': beforeShow, + 'afterShow': afterShow + }; + app.bindScreen('findSession', screenBindings); + + if (latency) { + sessionLatency = latency; + } + else { + logger.warn("No sessionLatency provided."); + } + + sessionList = new context.JK.SessionList(app); + + events(); + } + + this.initialize = initialize; + this.renderSession = renderSession; + this.afterShow = afterShow; + + // Following exposed for easier testing. + this.setSession = setSession; + this.clearResults = clearResults; + this.getCategoryEnum = getCategoryEnum; + + return this; + }; + + })(window,jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/friendSelector.js b/web/app/assets/javascripts/friendSelector.js new file mode 100644 index 000000000..4e76ea128 --- /dev/null +++ b/web/app/assets/javascripts/friendSelector.js @@ -0,0 +1,85 @@ +(function(context,$) { + + "use strict"; + + context.JK = context.JK || {}; + context.JK.FriendSelectorDialog = function(app, saveCallback) { + var logger = context.JK.logger; + var rest = context.JK.Rest(); + var selectedIds = {}; + var newSelections = {}; + + function events() { + $('#btn-save-friends').click(saveFriendInvitations); + } + + function loadFriends() { + $('#friend-selector-list').empty(); + + // TODO: page this as users scroll - show selected friends from parent screen at top + rest.getFriends({ id: context.JK.currentUserId }) + .done(function(friends) { + var template = $('#template-friend-selection').html(); + $.each(friends, function(index, val) { + var id = val.id; + var isSelected = selectedIds[id]; + + var html = context.JK.fillTemplate(template, { + userId: id, + css_class: isSelected ? 'selected' : '', + userName: val.name, + avatar_url: context.JK.resolveAvatarUrl(val.photo_url), + status: "", + status_img_url: "", + check_mark_display: isSelected ? "block" : "none", + status_img_display: "none" + }); + + $('#friend-selector-list').append(html); + + // disable row click if it was chosen on parent screen + if (!isSelected) { + $('#friend-selector-list tr[user-id="' + id + '"]').click(function() { + updateSelectionList(id, val.name, $(this), $(this).find('img[user-id="' + id + '"]')); + }); + } + }); + }) + .error(app.ajaxError); + } + + function updateSelectionList(id, name, tr, img) { + if (!tr.hasClass('selected')) { + tr.addClass('selected'); + img.show(); + newSelections[id] = { userId: id, userName: name }; + } + else { + tr.removeClass('selected'); + img.hide(); + delete newSelections[id]; + } + } + + function saveFriendInvitations(evt) { + evt.stopPropagation(); + saveCallback(newSelections); + } + + function showDialog(ids) { + selectedIds = ids; + newSelections = {}; + loadFriends(); + // HACK TO OVERRIDE PADDING SET SOMEWHERE ELSE + $('.choosefriends-overlay').css({"padding":"8px"}); + } + + this.initialize = function() { + events(); + }; + + this.showDialog = showDialog; + return this; + }; + + })(window,jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/ftue.js b/web/app/assets/javascripts/ftue.js new file mode 100644 index 000000000..05b60012b --- /dev/null +++ b/web/app/assets/javascripts/ftue.js @@ -0,0 +1,501 @@ +/** +* FtueAudioSelectionScreen +* Javascript that goes with the screen where initial +* selection of the audio devices takes place. +* Corresponds to /#ftue2 +*/ +(function(context,$) { + + "use strict"; + + context.JK = context.JK || {}; + context.JK.FtueWizard = function(app) { + context.JK.FtueWizard.latencyTimeout = true; + context.JK.FtueWizard.latencyMS = Number.MAX_VALUE; + + var logger = context.JK.logger; + var jamClient = context.jamClient; + var win32 = true; + + var deviceSetMap = { + 'audio-input': jamClient.FTUESetMusicInput, + 'audio-output': jamClient.FTUESetMusicOutput, + 'voice-chat-input': jamClient.FTUESetChatInput + }; + + var faderMap = { + 'ftue-audio-input-fader': jamClient.FTUESetInputVolume, + 'ftue-voice-input-fader': jamClient.FTUESetChatInputVolume, + 'ftue-audio-output-fader': jamClient.FTUESetOutputVolume + }; + + function latencyTimeoutCheck() { + if (context.JK.FtueWizard.latencyTimeout) { + jamClient.FTUERegisterLatencyCallback(''); + context.JK.app.setWizardStep("5"); + } + } + + function afterHide(data) { + // Unsubscribe from FTUE VU callbacks. + jamClient.FTUERegisterVUCallbacks('','',''); + } + + function beforeShow(data) { + var vuMeters = [ + '#ftue-audio-input-vu-left', + '#ftue-audio-input-vu-right', + '#ftue-voice-input-vu-left', + '#ftue-voice-input-vu-right', + '#ftue-audio-output-vu-left', + '#ftue-audio-output-vu-right' + ]; + $.each(vuMeters, function() { + context.JK.VuHelpers.renderVU(this, + {vuType:"horizontal", lightCount: 12, lightWidth: 15, lightHeight: 3}); + }); + + var faders = context._.keys(faderMap); + $.each(faders, function() { + var fid = this; + context.JK.FaderHelpers.renderFader('#' + fid, + {faderId: fid, faderType:"horizontal", width:163}); + context.JK.FaderHelpers.subscribe(fid, faderChange); + }); + } + + function afterShow(data) {} + + function faderChange(faderId, newValue, dragging) { + var setFunction = faderMap[faderId]; + // TODO - using hardcoded range of -80 to 20 for output levels. + var mixerLevel = newValue - 80; // Convert our [0-100] to [-80 - +20] range + setFunction(mixerLevel); + } + + function settingsInit() { + jamClient.FTUEInit(); + setLevels(0); + // Always reset the driver select box to "Choose..." which forces everything + // to sync properly when the user reselects their driver of choice. + // VRFS-375 and VRFS-561 + $('[layout-wizard="ftue"] [layout-wizard-step="2"] .asio-settings .settings-driver select').val(""); + } + + function setLevels(db) { + if (db < -80 || db > 20) { + throw ("BUG! ftue.js:setLevels db arg must be between -80 and 20"); + } + var trackIds = jamClient.SessionGetIDs(); + var controlStates = jamClient.SessionGetControlState(trackIds); + $.each(controlStates, function(index, value) { + context.JK.Mixer.fillTrackVolume(value, false); + // Default input/output to 0 DB + context.trackVolumeObject.volL = db; + context.trackVolumeObject.volR = db; + jamClient.SessionSetControlState(trackIds[index]); + }); + $.each(context._.keys(faderMap), function(index, faderId) { + // faderChange takes a value from 0-100 + var $fader = $('[fader-id="' + faderId + '"]'); + context.JK.FaderHelpers.setHandlePosition($fader, db + 80); + }); + } + + function testComplete() { + logger.debug("Test complete"); + var latencyMS = context.JK.FtueWizard.latencyMS; + if (latencyMS <= 20) { + logger.debug(latencyMS + " is <= 20. Setting FTUE status to true"); + ftueSave(true); // Save the profile + context.jamClient.FTUESetStatus(true); // No FTUE wizard next time + + // notify anyone curious about how it went + $('div[layout-id=ftue]').trigger('ftue_success'); + } + updateGauge(); + } + + function updateGauge() { + var latencyMS = context.JK.FtueWizard.latencyMS; + // Round to 2 decimal places + latencyMS = (Math.round(latencyMS * 100)) / 100; + logger.debug("Latency Value: " + latencyMS); + if (latencyMS > 20) { + $('#ftue-latency-congrats').hide(); + $('#ftue-latency-fail').show(); + } else { + $('#ftue-latency-ms').html(latencyMS); + $('#ftue-latency-congrats').show(); + $('#ftue-latency-fail').hide(); + if (latencyMS <= 10) { + $('[layout-wizard-step="6"] .btnHelp').hide(); + $('[layout-wizard-step="6"] .btnRepeat').hide(); + } + } + setNeedleValue(latencyMS); + } + + // Function to calculate an absolute value and an absolute value range into + // a number of degrees on a circualar "speedometer" gauge. The 0 degrees value + // points straight up to the middle of the real-world value range. + // Arguments: + // value: The real-world value (e.g. 20 milliseconds) + // minValue: The real-world minimum value (e.g. 0 milliseconds) + // maxValue: The real-world maximum value (e.g. 40 milliseconds) + // degreesUsed: The number of degrees used to represent the full real-world + // range. 360 would be the entire circle. 270 would be 3/4ths + // of the circle. The unused gap will be at the bottom of the + // gauge. + // (e.g. 300) + function degreesFromRange(value, minValue, maxValue, degreesUsed) { + if (value > maxValue) { value = maxValue; } + if (value < minValue) { value = minValue; } + var range = maxValue-minValue; + var floatVal = value/range; + var degrees = Math.round(floatVal * degreesUsed); + degrees -= Math.round(degreesUsed/2); + if (degrees < 0) { + degrees += 360; + } + return degrees; + } + + // Given a number of MS, and assuming the gauge has a range from + // 0 to 40 ms. Update the gauge to the proper value. + function setNeedleValue(ms) { + logger.debug("setNeedleValue: " + ms); + var deg = degreesFromRange(ms, 0, 40, 300); + + // Supporting Firefix, Chrome, Safari, Opera and IE9+. + // Should we need to support < IE9, this will need more work + // to compute the matrix transformations in those browsers - + // and I don't believe they support transparent PNG graphic + // rotation, so we'll have to change the needle itself. + var css = { + //"behavior":"url(-ms-transform.htc)", + /* Firefox */ + "-moz-transform":"rotate(" + deg + "deg)", + /* Safari and Chrome */ + "-webkit-transform":"rotate(" + deg + "deg)", + /* Opera */ + "-o-transform":"rotate(" + deg + "deg)", + /* IE9 */ + "-ms-transform":"rotate(" + deg + "deg)" + /* IE6,IE7 */ + //"filter": "progid:DXImageTransform.Microsoft.Matrix(sizingMethod='auto expand', M11=0.7071067811865476, M12=-0.7071067811865475, M21=0.7071067811865475, M22=0.7071067811865476)", + /* IE8 */ + //"-ms-filter": '"progid:DXImageTransform.Microsoft.Matrix(SizingMethod=\'auto expand\', M11=0.7071067811865476, M12=-0.7071067811865475, M21=0.7071067811865475, M22=0.7071067811865476)"' + }; + + $('#ftue-latency-numerical').html(ms); + $('#ftue-latency-needle').css(css); + + } + + function testLatency() { + // we'll just register for call back right here and unregister in the callback. + context.JK.FtueWizard.latencyTimeout = true; + var cbFunc = 'JK.ftueLatencyCallback'; + logger.debug("Registering latency callback: " + cbFunc); + jamClient.FTUERegisterLatencyCallback('JK.ftueLatencyCallback'); + var now = new Date(); + logger.debug("Starting Latency Test..." + now); + context.setTimeout(latencyTimeoutCheck, 300 * 1000); // Timeout to 5 minutes + jamClient.FTUEStartLatency(); + } + + function openASIOControlPanel(evt) { + if (win32) { + logger.debug("Calling FTUEOpenControlPanel()"); + jamClient.FTUEOpenControlPanel(); + } + } + + function asioResync(evt) { + jamClient.FTUERefreshDevices(); + ftueSave(false); + } + + function ftueSave(persist) { + // Explicitly set inputs and outputs to dropdown values + // before save as the client seems to want this on changes to + // things like frame size, etc.. + var $audioSelects = $('[layout-wizard-step="2"] .settings-controls select'); + $.each($audioSelects, function(index, value) { + var $select = $(value); + setAudioDevice($select); + }); + if (musicInAndOutSet()) { + + // If there is no voice-chat-input selected, let the back-end know + // that we're using music for voice-chat. + if ($('[layout-wizard-step="2"] select[data-device="voice-chat-input"]').val()) { + // Voice input selected + jamClient.TrackSetChatEnable(true); + } else { + // No voice input selected. + jamClient.TrackSetChatEnable(false); + } + + logger.debug("Calling FTUESave(" + persist + ")"); + var response = jamClient.FTUESave(persist); + setLevels(0); + if (response) { + logger.warn(response); + // TODO - we may need to do something about errors on save. + // per VRFS-368, I'm hiding the alert, and logging a warning. + // context.alert(response); + } + } else { + logger.debug("Aborting FTUESave as we need input + output selected."); + } + } + + function setAsioFrameSize(evt) { + var val = parseFloat($(evt.currentTarget).val(),10); + if (isNaN(val)) { + return; + } + logger.debug("Calling FTUESetFrameSize(" + val + ")"); + jamClient.FTUESetFrameSize(val); + ftueSave(false); + } + function setAsioInputLatency(evt) { + var val = parseInt($(evt.currentTarget).val(),10); + if (isNaN(val)) { + return; + } + logger.debug("Calling FTUESetInputLatency(" + val + ")"); + jamClient.FTUESetInputLatency(val); + ftueSave(false); + } + function setAsioOutputLatency(evt) { + var val = parseInt($(evt.currentTarget).val(),10); + if (isNaN(val)) { + return; + } + logger.debug("Calling FTUESetOutputLatency(" + val + ")"); + jamClient.FTUESetOutputLatency(val); + ftueSave(false); + } + + function videoLinkClicked(evt) { + var myOS = jamClient.GetOSAsString(); + var link; + if (myOS === 'MacOSX') { + link = $(evt.currentTarget).attr('external-link-mac'); + } else if (myOS === 'Win32') { + link = $(evt.currentTarget).attr('external-link-win'); + } + if (link) { + context.jamClient.OpenSystemBrowser(link); + } + } + + function events() { + $('.ftue-video-link').hover( + function(evt) { // handlerIn + $(this).addClass('hover'); + }, + function(evt) { // handlerOut + $(this).removeClass('hover'); + } + ); + $('.ftue-video-link').on('click', videoLinkClicked); + $('[layout-wizard-step="2"] .settings-driver select').on('change', audioDriverChanged); + $('[layout-wizard-step="2"] .settings-controls select').on('change', audioDeviceChanged); + $('#btn-asio-control-panel').on('click', openASIOControlPanel); + $('#btn-asio-resync').on('click', asioResync); + $('#asio-framesize').on('change', setAsioFrameSize); + $('#asio-input-latency').on('change', setAsioInputLatency); + $('#asio-output-latency').on('change', setAsioOutputLatency); + } + + /** + * This function loads the available audio devices from jamClient, and + * builds up the select dropdowns in the audio-settings step of the FTUE wizard. + */ + function loadAudioDevices() { + var funcs = [ + jamClient.FTUEGetMusicInputs, + jamClient.FTUEGetChatInputs, + jamClient.FTUEGetMusicOutputs + ]; + var selectors = [ + '[layout-wizard-step="2"] .audio-input select', + '[layout-wizard-step="2"] .voice-chat-input select', + '[layout-wizard-step="2"] .audio-output select' + ]; + var optionsHtml = ''; + var deviceOptionFunc = function(deviceKey, index, list) { + optionsHtml += ''; + }; + for (var i=0; i' + + drivers[driverKey] + ''; + }; + + var optionsHtml = ''; + var $select = $('[layout-wizard-step="2"] .settings-driver select'); + $select.empty(); + var sortedDeviceKeys = context._.keys(drivers).sort(); + context._.each(sortedDeviceKeys, driverOptionFunc); + $select.html(optionsHtml); + } + + function audioDriverChanged(evt) { + var $select = $(evt.currentTarget); + var driverId = $select.val(); + jamClient.FTUESetMusicDevice(driverId); + loadAudioDevices(); + setAsioSettingsVisibility(); + } + + function audioDeviceChanged(evt) { + var $select = $(evt.currentTarget); + setAudioDevice($select); + if (musicInAndOutSet()) { + ftueSave(false); + setVuCallbacks(); + } + } + + function setAudioDevice($select) { + var device = $select.data('device'); + var deviceId = $select.val(); + // Note: We always set, even on the "Choose" value of "", which clears + // the current setting. + var setFunction = deviceSetMap[device]; + setFunction(deviceId); + } + + /** + * Return a boolean indicating whether both the MusicInput + * and MusicOutput devices are set. + */ + function musicInAndOutSet() { + var audioInput = $('[layout-wizard-step="2"] .audio-input select').val(); + var audioOutput = $('[layout-wizard-step="2"] .audio-output select').val(); + return (audioInput && audioOutput); + } + + function setVuCallbacks() { + jamClient.FTUERegisterVUCallbacks( + "JK.ftueAudioOutputVUCallback", + "JK.ftueAudioInputVUCallback", + "JK.ftueChatInputVUCallback" + ); + jamClient.SetVURefreshRate(200); + } + + function setAsioSettingsVisibility() { + logger.debug("jamClient.FTUEHasControlPanel()=" + jamClient.FTUEHasControlPanel()); + if (jamClient.FTUEHasControlPanel()) { + logger.debug("Showing ASIO button"); + $('#btn-asio-control-panel').show(); + } + else { + logger.debug("Hiding ASIO button"); + $('#btn-asio-control-panel').hide(); + } + } + + function initialize() { + // If not on windows, hide ASIO settings + if (jamClient.GetOSAsString() != "Win32") { + logger.debug("Not on Win32 - modifying UI for Mac/Linux"); + win32 = false; + $('[layout-wizard-step="2"] p[os="win32"]').hide(); + $('[layout-wizard-step="2"] p[os="mac"]').show(); + $('#btn-asio-control-panel').hide(); + $('[layout-wizard-step="2"] .settings-controls select').removeAttr("disabled"); + loadAudioDevices(); + } + + setAsioSettingsVisibility(); + + events(); + var dialogBindings = { 'beforeShow': beforeShow, + 'afterShow': afterShow, 'afterHide': afterHide }; + app.bindDialog('ftue', dialogBindings); + app.registerWizardStepFunction("2", settingsInit); + app.registerWizardStepFunction("4", testLatency); + app.registerWizardStepFunction("6", testComplete); + loadAudioDrivers(); + } + + // Expose publics + this.initialize = initialize; + + // Expose degreesFromRange outside for testing + this._degreesFromRange = degreesFromRange; + + return this; + }; + + + + // Common VU updater taking a dbValue (-80 to 20) and a CSS selector for the VU. + context.JK.ftueVUCallback = function(dbValue, selector) { + // Convert DB into a value from 0.0 - 1.0 + var floatValue = (dbValue + 80) / 100; + context.JK.VuHelpers.updateVU(selector, floatValue); + }; + + context.JK.ftueAudioInputVUCallback = function(dbValue) { + context.JK.ftueVUCallback(dbValue, '#ftue-audio-input-vu-left'); + context.JK.ftueVUCallback(dbValue, '#ftue-audio-input-vu-right'); + }; + context.JK.ftueAudioOutputVUCallback = function(dbValue) { + context.JK.ftueVUCallback(dbValue, '#ftue-audio-output-vu-left'); + context.JK.ftueVUCallback(dbValue, '#ftue-audio-output-vu-right'); + }; + context.JK.ftueChatInputVUCallback = function(dbValue) { + context.JK.ftueVUCallback(dbValue, '#ftue-voice-input-vu-left'); + context.JK.ftueVUCallback(dbValue, '#ftue-voice-input-vu-right'); + }; + + // Latency Callback + context.JK.ftueLatencyCallback = function(latencyMS) { + // We always show gauge screen if we hit this. + // Clear out the 'timeout' variable. + context.JK.FtueWizard.latencyTimeout = false; + var now = new Date(); + context.console.debug("ftueLatencyCallback: " + now); + context.JK.FtueWizard.latencyMS = latencyMS; + + // Unregister callback: + context.jamClient.FTUERegisterLatencyCallback(''); + // Go to 'congrats' screen -- although latency may be too high. + context.JK.app.setWizardStep("6"); + }; + + + })(window,jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/genreSelector.js b/web/app/assets/javascripts/genreSelector.js new file mode 100644 index 000000000..3b3848c4d --- /dev/null +++ b/web/app/assets/javascripts/genreSelector.js @@ -0,0 +1,87 @@ +(function(context,$) { + + /** + * Javascript for managing genre selectors. + */ + + "use strict"; + + context.JK = context.JK || {}; + context.JK.GenreSelectorHelper = (function() { + + var logger = context.JK.logger; + var _genres = []; // will be list of structs: [ {label:xxx, value:yyy}, {...}, ... ] + + function loadGenres() { + var url = "/api/genres"; + $.ajax({ + type: "GET", + url: url, + async: false, // do this synchronously so the event handlers in events() can be wired up + success: genresLoaded + }); + } + + function reset(parentSelector, defaultGenre) { + defaultGenre = typeof(defaultGenre) == 'undefined' ? '' : defaultGenre; + $('select', parentSelector).val(defaultGenre); + } + + function genresLoaded(response) { + $.each(response, function(index) { + _genres.push({ + value: this.id, + label: this.description + }); + }); + } + + function render(parentSelector) { + $('select', parentSelector).empty(); + var template = $('#template-genre-option').html(); + $.each(_genres, function(index, value) { + // value will be a dictionary entry from _genres: + // { value: xxx, label: yyy } + var genreOptionHtml = context.JK.fillTemplate(template, value); + $('select', parentSelector).append(genreOptionHtml); + }); + + } + + function getSelectedGenres(parentSelector) { + var selectedGenres = []; + var selectedVal = $('select', parentSelector).val(); + if (selectedVal !== '') { + selectedGenres.push(selectedVal); + } + return selectedGenres; + } + + function setSelectedGenres(parentSelector, genreList) { + if (!genreList) { + return; + } + var values = []; + $.each(genreList, function(index, value) { + values.push(value.toLowerCase()); + }); + var selectedVal = $('select', parentSelector).val(values); + } + + function initialize() { + loadGenres(); + } + + var me = { // This will be our singleton. + initialize: initialize, + getSelectedGenres: getSelectedGenres, + setSelectedGenres: setSelectedGenres, + reset: reset, + render: render, + loadGenres: loadGenres + }; + + return me; + + })(); +})(window,jQuery); diff --git a/web/app/assets/javascripts/globals.js b/web/app/assets/javascripts/globals.js new file mode 100644 index 000000000..117e99960 --- /dev/null +++ b/web/app/assets/javascripts/globals.js @@ -0,0 +1,75 @@ +/** +* Static, simple definitions like strings, hashes, enums +*/ +(function(context,$) { + + "use strict"; + + context.JK = context.JK || {}; + var logger = context.JK.logger; + + context.JK.OS = { + WIN32: "Win32", + OSX: "MacOSX", + UNIX: "Unix" + }; + + // TODO: store these client_id values in instruments table, or store + // server_id as the client_id to prevent maintenance nightmares. As it's + // set up now, we will have to deploy each time we add new instruments. + context.JK.server_to_client_instrument_map = { + "Acoustic Guitar": { "client_id": 10, "server_id": "acoustic guitar" }, + "Bass Guitar": { "client_id": 20, "server_id": "bass guitar" }, + "Computer": { "client_id": 30, "server_id": "computer" }, + "Drums": { "client_id": 40, "server_id": "drums" }, + "Electric Guitar": { "client_id": 50, "server_id": "electric guitar" }, + "Keyboard": { "client_id": 60, "server_id": "keyboard" }, + "Voice": { "client_id": 70, "server_id": "voice" }, + "Flute": { "client_id": 80, "server_id": "flute" }, + "Clarinet": { "client_id": 90, "server_id": "clarinet" }, + "Saxophone": { "client_id": 100, "server_id": "saxophone" }, + "Trumpet": { "client_id": 110, "server_id": "trumpet" }, + "Violin": { "client_id": 120, "server_id": "violin" }, + "Trombone": { "client_id": 130, "server_id": "trombone" }, + "Banjo": { "client_id": 140, "server_id": "banjo" }, + "Harmonica": { "client_id": 150, "server_id": "harmonica" }, + "Accordion": { "client_id": 160, "server_id": "accordion" }, + "French Horn": { "client_id": 170, "server_id": "french horn" }, + "Euphonium": { "client_id": 180, "server_id": "euphonium" }, + "Tuba": { "client_id": 190, "server_id": "tuba" }, + "Oboe": { "client_id": 200, "server_id": "oboe" }, + "Ukulele": { "client_id": 210, "server_id": "ukulele" }, + "Cello": { "client_id": 220, "server_id": "cello" }, + "Viola": { "client_id": 230, "server_id": "viola" }, + "Mandolin": { "client_id": 240, "server_id": "mandolin" }, + "Other": { "client_id": 250, "server_id": "other" } + }; + + context.JK.client_to_server_instrument_map = { + 10: { "server_id": "acoustic guitar" }, + 20: { "server_id": "bass guitar" }, + 30: { "server_id": "computer" }, + 40: { "server_id": "drums" }, + 50: { "server_id": "electric guitar" }, + 60: { "server_id": "keyboard" }, + 70: { "server_id": "voice" }, + 80: { "server_id": "flute" }, + 90: { "server_id": "clarinet" }, + 100: { "server_id": "saxophone" }, + 110: { "server_id": "trumpet" }, + 120: { "server_id": "violin" }, + 130: { "server_id": "trombone" }, + 140: { "server_id": "banjo" }, + 150: { "server_id": "harmonica" }, + 160: { "server_id": "accordion" }, + 170: { "server_id": "french horn" }, + 180: { "server_id": "euphonium" }, + 190: { "server_id": "tuba" }, + 200: { "server_id": "oboe" }, + 210: { "server_id": "ukulele" }, + 220: { "server_id": "cello" }, + 230: { "server_id": "viola" }, + 240: { "server_id": "mandolin" }, + 250: { "server_id": "other" } + }; + })(window,jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/header.js b/web/app/assets/javascripts/header.js new file mode 100644 index 000000000..55eb58f50 --- /dev/null +++ b/web/app/assets/javascripts/header.js @@ -0,0 +1,216 @@ +(function(context,$) { + + /** + * Javascript for managing the header (account dropdown) as well + * as any dialogs reachable from there. Account settings dialog. + */ + + "use strict"; + + context.JK = context.JK || {}; + context.JK.Header = function(app) { + + var logger = context.JK.logger; + var searcher; // Will hold an instance to a JK.Searcher (search.js) + var userMe = null; + var instrumentAutoComplete; + var instrumentIds = []; + var instrumentNames = []; + var instrumentPopularities = {}; // id -> popularity + + function loadInstruments() { + // TODO: This won't work in the long-term. We'll need to provide + // a handlers which accepts some characters and only returns users + // who are musicians who match that input string. Once we get there, + // we could just use the ajax functionality of the autocomplete plugin. + // + // But for now: + // Load the users list into our local array for autocomplete. + // var response = context.JK.getInstruments(app); + // $.each(response, function() { + // instrumentNames.push(this.description); + // instrumentIds.push(this.id); + // // TODO - unused at the moment. + // instrumentPopularities[this.id] = this.popularity; + // }); + // // Hook up the autocomplete. + // var options = { + // lookup: {suggestions:instrumentNames, data: instrumentIds}, + // onSelect: addInstrument, + // width: 120 + // }; + // if (!(instrumentAutoComplete)) { // Shouldn't happen here. Header only drawn once. + // instrumentAutoComplete = $('#profile-instruments').autocomplete(options); + // } else { + // instrumentAutoComplete.setOptions(options); + // } + + $.ajax({ + type: "GET", + url: "/api/instruments" + }).done(function(response) { + $.each(response, function() { + instrumentNames.push(this.description); + instrumentIds.push(this.id); + // TODO - unused at the moment. + instrumentPopularities[this.id] = this.popularity; + }); + // Hook up the autocomplete. + var options = { + lookup: {suggestions:instrumentNames, data: instrumentIds}, + onSelect: addInstrument, + width: 120 + }; + if (!(instrumentAutoComplete)) { // Shouldn't happen here. Header only drawn once. + instrumentAutoComplete = $('#profile-instruments').autocomplete(options); + } else { + instrumentAutoComplete.setOptions(options); + } + }); + } + + function addInstrument(value, data) { + var instrumentName = value; + var instrumentId = data; + var template = $('#template-profile-instrument').html(); // TODO: cache this + var instrumentHtml = context.JK.fillTemplate( + template, {instrumentId: instrumentId, instrumentName: instrumentName}); + $('#added-profile-instruments').append(instrumentHtml); + $('#profile-instruments').select(); + } + + function setProficiency(id, proficiency) { + logger.debug("setProficiency: " + id + ',' + proficiency); + var selector = '#added-profile-instruments div.profile-instrument[instrument-id="' + + id + '"] select'; + logger.debug("Finding select to set proficiency. Length? " + $(selector).length); + $(selector).val(proficiency); + } + + function removeInvitation(evt) { + $(evt.currentTarget).closest('.profile-instrument').remove(); + } + + function events() { + $('body').on('click', 'div[layout="header"] h1', function() { + context.location = '#/home'; + }); + $('.userinfo').on('click', function() { + $('ul', this).toggle(); + }); + + $('#account-identity-form').submit(handleIdentitySubmit); + $('#account-profile-form').submit(handleProfileSubmit); + // Remove added instruments when 'X' is clicked + $('#added-profile-instruments').on("click", ".instrument span", removeInvitation); + + $('#header-avatar').on('avatar_changed', function(event, newAvatarUrl) { + updateAvatar(newAvatarUrl); + event.preventDefault(); + return false; + }) + } + + function handleIdentitySubmit(evt) { + evt.preventDefault(); + var user = $(evt.currentTarget).formToObject(); + if (!user.password) { + delete user.password; + delete user.password_confirmation; + } + $.ajax({ + type: "POST", + url: "/api/users/" + userMe.id, + contentType: "application/json", + processData:false, + data: JSON.stringify(user) + }).done(function(response) { + userMe = response; + }).fail(app.ajaxError); + return false; + } + + function handleProfileSubmit(evt) { + evt.preventDefault(); + var user = { + name: $('#account-profile-form input[name="name"]').val(), + instruments: [] + }; + var $added_instruments = $('.profile-instrument', '#added-profile-instruments'); + var count = 1; + $.each($added_instruments, function() { + var instrumentId = $(this).attr('instrument-id'); + var proficiency = $('select', this).val(); + logger.debug("Instrument ID:" + instrumentId + ", proficiency: " + proficiency); + var instrument = { + id: instrumentId, + proficiency_level: proficiency, + priority: count++ + }; + user.instruments.push(instrument); + }); + + logger.debug("About to submit profile. User:"); + logger.debug(user); + + $.ajax({ + type: "POST", + url: "/api/users/" + userMe.id, + contentType: "application/json", + processData:false, + data: JSON.stringify(user) + }).done(function(response) { + userMe = response; + }).fail(app.ajaxError); + + return false; + } + + function loadMe() { + $.ajax({ + url: '/api/users/' + context.JK.currentUserId + }).done(function(r) { + userMe = r; + // TODO - Setting global variable for local user. + context.JK.userMe = r; + updateHeader(); + }).fail(app.ajaxError); + } + + function updateHeader() { + + $('#user').html(userMe.name); + showAvatar(); + } + + // initially show avatar + function showAvatar() { + var photoUrl = context.JK.resolveAvatarUrl(userMe.photo_url); + $('#header-avatar').attr('src', photoUrl); + } + + // handle update avatar event + function updateAvatar(avatar_url) { + var photoUrl = context.JK.resolveAvatarUrl(avatar_url); + var avatar = $(new Image()); + avatar.attr('src', photoUrl + '?cache_bust=' + new Date().getTime()); + avatar.attr('alt', "Avatar"); + avatar.attr('id', 'header-avatar'); + $('#header-avatar').replaceWith(avatar); + } + + this.initialize = function() { + + events(); + loadInstruments(); + loadMe(); + + + //searcher = new context.JK.Searcher(app); + //searcher.initialize(); + }; + + this.loadMe = loadMe; + }; + + })(window,jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/homeScreen.js b/web/app/assets/javascripts/homeScreen.js new file mode 100644 index 000000000..56e802e0d --- /dev/null +++ b/web/app/assets/javascripts/homeScreen.js @@ -0,0 +1,135 @@ +(function(context,$) { + + "use strict"; + + context.JK = context.JK || {}; + context.JK.HomeScreen = function(app) { + var logger = context.JK.logger; + var isFtueComplete = false; + + function beforeShow(data) { + refreshDeviceState(); + } + + function refreshDeviceState() { + isFtueComplete = checkDeviceState(); + updateTiles(); + } + + function checkDeviceState() { + var devices = context.jamClient.TrackGetDevices(); + return Object.keys(devices).length > 0; + } + + function mouseenterTile() { + $(this).addClass('hover'); + } + + function mouseleaveTile() { + $(this).removeClass('hover'); + } + + function switchClientMode(e) { + // ctrl + shift + 0 + if(e.ctrlKey && e.shiftKey && e.keyCode == 48) { + console.log("switch client mode!"); + var act_as_native_client = $.cookie('act_as_native_client'); + + console.log("currently: " + act_as_native_client); + if(act_as_native_client == null || act_as_native_client != "true") { + console.log("forcing act as native client!"); + $.cookie('act_as_native_client', 'true', { expires: 120, path: '/' }); + } + else { + console.log("remove act as native client!"); + $.removeCookie('act_as_native_client'); + } + window.location.reload(); + } + } + + function userHasDevices() { + var $createSession = $('div[type="createSession"]'); + var $findSession = $('div[type="findSession"]'); + + $createSession.attr('layout-link', 'createSession'); + $findSession.attr('layout-link', 'findSession'); + + // undo any earlier disabling + $('h2', $createSession).removeClass('disabled'); + $('h2', $findSession).removeClass('disabled'); + $createSession.removeClass('createsession-disabled'); + $createSession.addClass('createsession'); + $findSession.removeClass('findsession-disabled'); + $findSession.addClass('findsession'); + + // and enable features + $($createSession).on('mouseenter', mouseenterTile); + $($createSession).on('mouseleave', mouseleaveTile); + $($findSession).on('mouseenter', mouseenterTile); + $($findSession).on('mouseleave', mouseleaveTile); + } + + function userHasNoDevices() { + + var $createSession = $('div[type="createSession"]'); + var $findSession = $('div[type="findSession"]'); + + $createSession.removeAttr('layout-link'); + $findSession.removeAttr('layout-link'); + + // undo any earlier enabling + $($createSession).off('mouseenter', mouseenterTile); + $($createSession).off('mouseleave', mouseleaveTile); + + $($findSession).off('mouseenter', mouseenterTile); + $($findSession).off('mouseleave', mouseleaveTile); + + // disable Create Session and Find Session tiles + $('h2', $createSession).addClass('disabled'); + $('h2', $findSession).addClass('disabled'); + $createSession.addClass('createsession-disabled'); + $createSession.removeClass('createsession'); + $findSession.addClass('findsession-disabled'); + $findSession.removeClass('findsession'); + } + + // used to initialize things that do not have to be touched up in the same UI sessiion + function events() { + // initialize profile, feed and account tiles normally + $('.homecard.profile, .homecard.feed, .homecard.account').on('mouseenter', mouseenterTile); + $('.homecard.profile, .homecard.feed, .homecard.account').on('mouseleave', mouseleaveTile); + $('div[layout-id=ftue]').on("ftue_success", refreshDeviceState); + if(gon.allow_force_native_client) { + $('body').on('keyup', switchClientMode); + } + } + + function updateTiles() { + logger.debug("isFtueComplete=" + isFtueComplete); + + if(isFtueComplete) { + userHasDevices(); + } + else { + userHasNoDevices(); + } + + $('.profile').on('click', function() { + context.location = '#/profile/' + context.JK.currentUserId; + }); + } + + + this.initialize = function() { + var screenBindings = { 'beforeShow': beforeShow }; + app.bindScreen('home', screenBindings); + events(); + + + }; + + this.beforeShow = beforeShow; + }; + + })(window,jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/invitation.js b/web/app/assets/javascripts/invitation.js new file mode 100644 index 000000000..ecfb10598 --- /dev/null +++ b/web/app/assets/javascripts/invitation.js @@ -0,0 +1,39 @@ +(function(context,$) { + + "use strict"; + + context.JK = context.JK || {}; + context.JK.InvitationDialog = function(app) { + var logger = context.JK.logger; + var rest = context.JK.Rest(); + + function events() { + $('#btn-send-invitation').click(sendEmail); + } + + function sendEmail() { + var emails = []; + emails = $('#txt-emails').val().split(','); + + for (var i=0; i < emails.length; i++) { + rest.createInvitation($.trim(emails[i]), $('#txt-message').val()) + .done(function() { }); + } + } + + this.showDialog = function() { + clearTextFields(); + } + + function clearTextFields() { + $('#txt-emails').val(''); + $('#txt-message').val(''); + } + + this.initialize = function() { + events(); + }; + } + + return this; +})(window,jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/jam_rest.js b/web/app/assets/javascripts/jam_rest.js new file mode 100644 index 000000000..c5ffeec90 --- /dev/null +++ b/web/app/assets/javascripts/jam_rest.js @@ -0,0 +1,219 @@ +(function(context,$) { + + /** + * Javascript wrappers for the REST API + */ + + "use strict"; + + context.JK = context.JK || {}; + context.JK.Rest = function() { + + var self = this; + var logger = context.JK.logger; + + function updateSession(id, newSession, onSuccess) { + logger.debug('Rest.updateSession'); + return $.ajax('/api/sessions/' + id, { + type: "PUT", + data : newSession, + dataType : 'json', + success: onSuccess + }); + } + + + function getUserDetail(options) { + var id = getId(options); + + var url = "/api/users/" + id; + return $.ajax({ + type: "GET", + dataType: "json", + url: url, + async: true, + processData: false + }); + } + + function getCities (options) { + var country = options['country'] + var region = options['region'] + + return $.ajax('/api/cities', { + data : { country: country, region: region }, + dataType : 'json' + }); + } + + function getRegions(options) { + var country = options["country"] + + return $.ajax('/api/regions', { + data : { country: country}, + dataType : 'json' + }); + } + + function getCountries() { + return $.ajax('/api/countries', { + dataType : 'json' + }); + } + + function getIsps(options) { + var country = options["country"] + + return $.ajax('/api/isps', { + data : { country: country}, + dataType : 'json' + }); + } + + function getInstruments(options) { + return $.ajax('/api/instruments', { + data : { }, + dataType : 'json' + }); + } + + function updateAvatar(options) { + var id = getId(options); + + var original_fpfile = options['original_fpfile']; + var cropped_fpfile = options['cropped_fpfile']; + var crop_selection = options['crop_selection']; + + var url = "/api/users/" + id + "/avatar"; + return $.ajax({ + type: "POST", + dataType: "json", + url: url, + contentType: 'application/json', + processData:false, + data: JSON.stringify({ + original_fpfile : original_fpfile, + cropped_fpfile : cropped_fpfile, + crop_selection : crop_selection + }) + }); + } + + function deleteAvatar(options) { + var id = getId(options); + + var url = "/api/users/" + id + "/avatar"; + return $.ajax({ + type: "DELETE", + dataType: "json", + url: url, + contentType: 'application/json', + processData:false + }); + } + + function getFilepickerPolicy(options) { + var id = getId(options); + var handle = options && options["handle"]; + var convert = options && options["convert"] + + var url = "/api/users/" + id + "/filepicker_policy"; + + return $.ajax(url, { + data : { handle : handle, convert: convert }, + dataType : 'json' + }); + } + + function getFriends(options) { + var id = getId(options); + + return $.ajax({ + type: "GET", + url: '/api/users/' + id + '/friends', + dataType: 'json' + }); + } + + function getClientDownloads(options) { + + return $.ajax({ + type: "GET", + url: '/api/artifacts/clients', + dataType: 'json' + }); + } + + /** check if the server is alive */ + function serverHealthCheck(options) { + console.log("serverHealthCheck") + return $.ajax({ + type: "GET", + url: "/api/versioncheck" + }); + } + + function getId(options) { + var id = options && options["id"] + + if(!id) { + id = context.JK.currentUserId; + } + return id; + } + + function createInvitation(emailAddress, message) { + return $.ajax({ + type: "POST", + dataType: "json", + url: '/api/invited_users', + contentType: 'application/json', + processData:false, + data: JSON.stringify({ + email : emailAddress, + note: message + }) + }); + } + + function postFeedback(email, body) { + return $.ajax({ + type: "POST", + dataType: "json", + url: '/api/feedback', + contentType: 'application/json', + processData:false, + data: JSON.stringify({ + email : email, + body: body + }) + }); + } + + function initialize() { + return self; + } + + // Expose publics + this.initialize = initialize; + this.getUserDetail = getUserDetail; + this.getCities = getCities; + this.getRegions = getRegions; + this.getCountries = getCountries; + this.getIsps = getIsps; + this.getInstruments = getInstruments; + this.updateAvatar = updateAvatar; + this.deleteAvatar = deleteAvatar; + this.getFilepickerPolicy = getFilepickerPolicy; + this.getFriends = getFriends; + this.updateSession = updateSession; + this.getClientDownloads = getClientDownloads + this.createInvitation = createInvitation; + this.postFeedback = postFeedback; + this.serverHealthCheck = serverHealthCheck; + + return this; + }; + + +})(window,jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/jamkazam.js b/web/app/assets/javascripts/jamkazam.js new file mode 100644 index 000000000..f0a67cc4b --- /dev/null +++ b/web/app/assets/javascripts/jamkazam.js @@ -0,0 +1,296 @@ +(function(context,$) { + + "use strict"; + + // Change underscore's default templating characters as they + // conflict withe the ERB rendering. Templates will use: + // {{ interpolate }} + // {% evaluate %} + // {{- escape }} + // + context._.templateSettings = { + evaluate : /\{%([\s\S]+?)%\}/g, + interpolate : /\{\{([\s\S]+?)\}\}/g, + escape : /\{\{-([\s\S]+?)\}\}/g + }; + + context.JK = context.JK || {}; + + var JamKazam = context.JK.JamKazam = function() { + var app; + var logger = context.JK.logger; + var heartbeatInterval=null; + var heartbeatMS=null; + var heartbeatMissedMS= 10000; // if 5 seconds go by and we haven't seen a heartbeat ack, get upset + var inBadState=false; + var lastHeartbeatAckTime=null; + var heartbeatAckCheckInterval = null; + + var opts = { + layoutOpts: {} + }; + + /** + * Dynamically build routes from markup. Any layout="screen" will get a route corresponding to + * his layout-id attribute. If a layout-arg attribute is present, that will be named as a data + * section of the route. + */ + function routing() { + var routes = context.RouteMap, + rules = {}, + rule, + routingContext = {}; + $('div[layout="screen"]').each(function() { + var target = $(this).attr('layout-id'), + targetUrl = target, + targetArg = $(this).attr('layout-arg'), + fn = function(data) { + app.layout.changeToScreen(target, data); + }; + if (targetArg) { + targetUrl += "/:" + targetArg; + } + rules[target] = {route: '/' + targetUrl + '/d:?', method: target}; + routingContext[target] = fn; + }); + routes.context(routingContext); + for (rule in rules) if (rules.hasOwnProperty(rule)) routes.add(rules[rule]); + $(context).bind('hashchange', routes.handler); + $(routes.handler); + } + + function _heartbeatAckCheck() { + // check if the server is still sending heartbeat acks back down + // this logic equates to 'if we have not received a heartbeat within heartbeatMissedMS, then get upset + if(new Date().getTime() - lastHeartbeatAckTime.getTime() > heartbeatMissedMS) { + logger.error("no heartbeat ack received from server after ", heartbeatMissedMS, " seconds . giving up on socket connection"); + context.JK.JamServer.close(true); + } + } + + function _heartbeat() { + if (app.heartbeatActive) { + + var message = context.JK.MessageFactory.heartbeat(); + context.JK.JamServer.send(message); + } + } + + function loggedIn(header, payload) { + app.clientId = payload.client_id; + $.cookie('client_id', payload.client_id); + // $.cookie('remember_token', payload.token); // removed per vrfs-273/403 + + heartbeatMS = payload.heartbeat_interval * 1000; + logger.debug("jamkazam.js.loggedIn(): clientId now " + app.clientId + "; Setting up heartbeat every " + heartbeatMS + " MS"); + heartbeatInterval = context.setInterval(_heartbeat, heartbeatMS); + heartbeatAckCheckInterval = context.setInterval(_heartbeatAckCheck, 1000); + lastHeartbeatAckTime = new Date(new Date().getTime() + heartbeatMS); // add a little forgiveness to server for initial heartbeat + } + + function heartbeatAck(header, payload) { + lastHeartbeatAckTime = new Date(); + } + + /** + * This occurs when the websocket gateway loses a connection to the backend messaging system, + * resulting in severe loss of functionality + */ + function serverBadStateError() { + if(!inBadState) { + inBadState = true; + app.notify({title: "Server Unstable", text: "The server is currently unstable, resulting in feature loss. If you are experiencing any problems, please try to use JamKazam later."}) + } + } + + /** + * This occurs when the websocket gateway loses a connection to the backend messaging system, + * resulting in severe loss of functionality + */ + function serverBadStateRecovered() { + if(inBadState) { + inBadState = false; + app.notify({title: "Server Recovered", text: "The server is now stable again. If you are still experiencing problems, either create a new music session or restart the client altogether."}) + } + } + + /** + * Called whenever the websocket closes; this gives us a chance to cleanup things that should be stopped/cleared + * @param in_error did the socket close abnormally? + */ + function socketClosed(in_error) { + + // stop future heartbeats + if(heartbeatInterval != null) { + clearInterval(heartbeatInterval); + heartbeatInterval = null; + } + + // stop checking for heartbeat acks + if(heartbeatAckCheckInterval != null) { + clearTimeout(heartbeatAckCheckInterval); + heartbeatAckCheckInterval = null; + } + } + + function registerLoginAck() { + logger.debug("register for loggedIn to set clientId"); + context.JK.JamServer.registerMessageCallback(context.JK.MessageType.LOGIN_ACK, loggedIn); + } + + function registerHeartbeatAck() { + logger.debug("register for heartbeatAck"); + context.JK.JamServer.registerMessageCallback(context.JK.MessageType.HEARTBEAT_ACK, heartbeatAck); + } + + function registerBadStateError() { + logger.debug("register for server_bad_state_error"); + context.JK.JamServer.registerMessageCallback(context.JK.MessageType.SERVER_BAD_STATE_ERROR, serverBadStateError); + } + + function registerBadStateRecovered() { + logger.debug("register for server_bad_state_recovered"); + context.JK.JamServer.registerMessageCallback(context.JK.MessageType.SERVER_BAD_STATE_RECOVERED, serverBadStateRecovered); + } + + function registerSocketClosed() { + logger.debug("register for socket closed"); + context.JK.JamServer.registerOnSocketClosed(socketClosed); + } + + /** + * Generic error handler for Ajax calls. + */ + function ajaxError(jqXHR, textStatus, errorMessage) { + logger.error("Unexpected ajax error: " + textStatus); + app.notify({title: textStatus, text: errorMessage, detail: jqXHR.responseText}); + } + + /** + * Expose ajaxError. + */ + this.ajaxError = ajaxError; + + /** + * Provide a handler object for events related to a particular screen + * being shown or hidden. + * @screen is a string corresponding to the screen's layout-id attribute + * @handler is an object with up to four optional keys: + * beforeHide, afterHide, beforeShow, afterShow, which should all have + * functions as values. If there is data provided by the screen's route + * it will be provided to these functions. + */ + this.bindScreen = function(screen, handler) { + this.layout.bindScreen(screen, handler); + }; + this.bindDialog = function(dialog, handler) { + this.layout.bindDialog(dialog, handler); + }; + /** + * Allow individual wizard steps to register a function to be invokes + * when they are shown. + */ + this.registerWizardStepFunction = function(stepId, showFunction) { + this.layout.registerWizardStepFunction(stepId, showFunction); + }; + + /** + * Switch to the wizard step with the provided id. + */ + this.setWizardStep = function(targetStepId) { + this.layout.setWizardStep(targetStepId); + }; + + /** + * Show a notification. Expects an object with a + * title property and a text property. + */ + this.notify = function(message, descriptor) { + this.layout.notify(message, descriptor); + }; + + /** + * Initialize any common events. + */ + function events() { + // Hook up the screen navigation controls. + $(".content-nav .arrow-left").click(function(evt) { + evt.preventDefault(); + context.history.back(); + return false; + }); + $(".content-nav .arrow-right").click(function(evt) { + evt.preventDefault(); + context.history.forward(); + return false; + }); + + context.JK.popExternalLinks(); + } + + // Due to timing of initialization, this must be called externally + // after all screens have been given a chance to initialize. + // It is called from index.html.erb after connecting, and initialization + // of other screens. + function initialRouting() { + routing(); + + var hash = context.location.hash; + + var url = '#/home'; + if (hash) { + url = hash; + } + logger.debug("Changing screen to " + url); + context.location = url; + + if (!(context.jamClient.FTUEGetStatus())) { + app.layout.showDialog('ftue'); + } + } + + this.unloadFunction = function() { + logger.debug("window.unload function called."); + + context.JK.JamServer.close(false); + + if (context.jamClient) { + // Unregister for callbacks. + context.jamClient.SessionRegisterCallback(""); + context.jamClient.SessionSetAlertCallback(""); + context.jamClient.FTUERegisterVUCallbacks(""); + context.jamClient.FTUERegisterLatencyCallback(""); + } + }; + + this.initialize = function(inOpts) { + var url, hash; + app = this; + this.opts = $.extend(opts, inOpts); + this.layout = new context.JK.Layout(); + this.layout.initialize(this.opts.layoutOpts); + registerLoginAck(); + registerHeartbeatAck(); + registerBadStateRecovered(); + registerBadStateError(); + registerSocketClosed(); + events(); + context.JK.FaderHelpers.initialize(); + context.window.onunload = this.unloadFunction; + }; + + + // enable temporary suspension of heartbeat for fine-grained control + this.heartbeatActive = true; + + /** + * Expose clientId as a public variable. + * Will be set upon LOGIN_ACK + */ + this.clientId = null; + this.initialRouting = initialRouting; + + return this; + }; + + })(window, jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/jamsocket.js b/web/app/assets/javascripts/jamsocket.js new file mode 100644 index 000000000..761034b22 --- /dev/null +++ b/web/app/assets/javascripts/jamsocket.js @@ -0,0 +1,66 @@ +// defines session-centric websocket code +(function(context, $) { + + "use strict"; + + var jamsocket = {}; + + function debug_print(msg, inner) { + var msg_div = $("
    "); + var msg_header = $("

    ").text(msg.type); + msg_div.append(msg_header); + + var list = $("
    "); + msg_div.append(list); + + for (var key in inner) { + list.append($("
    ").text(key)); + list.append($("
    ").text(inner[key])); + } + + + $("#internal_session_activity").append(msg_div); + } + + jamsocket.init = function() { + + function send(msg) { + ws.send(context.JSON.stringify(msg)); + } + + // Let the library know where WebSocketMain.swf is: + context.WEB_SOCKET_SWF_LOCATION = "assets/flash/WebSocketMain.swf"; + + var mf = context.message_factory; + + // Write your code in the same way as for native WebSocket: + var ws = new context.WebSocket(context.gon.websocket_gateway_uri); + ws.onopen = function() { + var token = $.cookie("remember_token"); + // there is a chance the token is invalid at this point + // but if it is, login should fail, and we can catch that as an error + // and deal with it then. + $("#internal_session_activity").children().remove(); + var login = mf.login_with_token(token); + send(login); + }; + ws.onmessage = function(e) { + var msg = JSON.parse(e.data); + var inner = msg[msg.type.toLowerCase()]; + + debug_print(msg, inner); + + if(msg.type == context.JK.MessageType.LOGIN_ACK) { + // we are in... sign in to jam session + + var login_jam = mf.login_music_session(context.gon.music_session_id); + send(login_jam); + } + }; + ws.onclose = function() { + context.alert("websocket connection closed"); + }; + }; + + context.jamsocket = jamsocket; +})(window, jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/joinSession.js b/web/app/assets/javascripts/joinSession.js new file mode 100644 index 000000000..25c3e3300 --- /dev/null +++ b/web/app/assets/javascripts/joinSession.js @@ -0,0 +1,57 @@ +(function(context,$) { + + "use strict"; + + context.JK = context.JK || {}; + + context.JK.joinMusicSession = function(session_id, app) { + var logger = context.JK.logger; + var server = context.JK.JamServer; + var client = context.jamClient; + + if (!server.signedIn) + { + logger.log("Can't join a session because the client is not connected."); + return; + } + + logger.log("Joining session: " + session_id); + + + // FIXME. Setting tracks for join session. Only looking at profile + // for now. Needs to check jamClient instruments and if set, use those + // as first preference. + var track = { instrument_id: "electric guitar", sound: "stereo" }; + if (context.JK.userMe.instruments.length) { + track = { + instrument_id: context.JK.userMe.instruments[0].instrument_id, + sound: "stereo" + }; + } + + var data = { + client_id: server.clientID, + ip_address: server.publicIP, + as_musician: true, + tracks: [ track ] + }; + var url = "/api/sessions/" + session_id + "/participants"; + $.ajax({ + type: "POST", + dataType: "json", + contentType: 'application/json', + url: url, + data: JSON.stringify(data), + processData:false, + success: function(response) { + context.JK.Sessions.JoinSession(session_id); + if (client !== undefined) { + client.JoinSession({ sessionID: session_id }); + } + context.JK.refreshMusicSession(session_id); + }, + error: app.ajaxError + }); + }; + + })(window,jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/jquery.autocomplete.js b/web/app/assets/javascripts/jquery.autocomplete.js new file mode 100644 index 000000000..da94f65bf --- /dev/null +++ b/web/app/assets/javascripts/jquery.autocomplete.js @@ -0,0 +1,433 @@ +/** +* Ajax Autocomplete for jQuery, version 1.1.5 +* (c) 2010 Tomas Kirda, Vytautas Pranskunas +* +* Ajax Autocomplete for jQuery is freely distributable under the terms of an MIT-style license. +* For details, see the web site: http://www.devbridge.com/projects/autocomplete/jquery/ +* +* Last Review: 07/24/2012 +*/ + +/*jslint onevar: true, evil: true, nomen: true, eqeqeq: true, bitwise: true, regexp: true, newcap: true, immed: true */ +/*global window: true, document: true, clearInterval: true, setInterval: true, jQuery: true */ + +(function ($) { + + var reEscape = new RegExp('(\\' + ['/', '.', '*', '+', '?', '|', '(', ')', '[', ']', '{', '}', '\\'].join('|\\') + ')', 'g'); + + function fnFormatResult(value, data, currentValue) { + var pattern = '(' + currentValue.replace(reEscape, '\\$1') + ')'; + return value.replace(new RegExp(pattern, 'gi'), '$1<\/strong>'); + } + + function Autocomplete(el, options) { + this.el = $(el); + this.el.attr('autocomplete', 'off'); + this.suggestions = []; + this.data = []; + this.badQueries = []; + this.selectedIndex = -1; + this.currentValue = this.el.val(); + this.intervalId = 0; + this.cachedResponse = []; + this.onChangeInterval = null; + this.onChange = null; + this.ignoreValueChange = false; + this.serviceUrl = options.serviceUrl; + this.isLocal = false; + this.options = { + autoSubmit: false, + minChars: 1, + maxHeight: 300, + deferRequestBy: 0, + width: 0, + highlight: true, + params: {}, + fnFormatResult: fnFormatResult, + delimiter: null, + zIndex: 9999 + }; + this.initialize(); + this.setOptions(options); + this.el.data('autocomplete', this); + } + + $.fn.autocomplete = function (options, optionName) { + + var autocompleteControl; + + if (typeof options == 'string') { + autocompleteControl = this.data('autocomplete'); + if (typeof autocompleteControl[options] == 'function') { + autocompleteControl[options](optionName); + } + } else { + autocompleteControl = new Autocomplete(this.get(0) || $(''), options); + } + return autocompleteControl; + }; + + + Autocomplete.prototype = { + + killerFn: null, + + initialize: function () { + + var me, uid, autocompleteElId; + me = this; + uid = Math.floor(Math.random() * 0x100000).toString(16); + autocompleteElId = 'Autocomplete_' + uid; + + this.killerFn = function (e) { + if ($(e.target).parents('.autocomplete').size() === 0) { + me.killSuggestions(); + me.disableKillerFn(); + } + }; + + if (!this.options.width) { this.options.width = this.el.width(); } + this.mainContainerId = 'AutocompleteContainter_' + uid; + + $('
    ').appendTo('body'); + + this.container = $('#' + autocompleteElId); + this.fixPosition(); + if (window.opera) { + this.el.keypress(function (e) { me.onKeyPress(e); }); + } else { + this.el.keydown(function (e) { me.onKeyPress(e); }); + } + this.el.keyup(function (e) { me.onKeyUp(e); }); + this.el.blur(function () { me.enableKillerFn(); }); + this.el.focus(function () { me.fixPosition(); }); + this.el.change(function () { me.onValueChanged(); }); + }, + + extendOptions: function (options) { + $.extend(this.options, options); + }, + + setOptions: function (options) { + var o = this.options; + this.extendOptions(options); + if (o.lookup || o.isLocal) { + this.isLocal = true; + if ($.isArray(o.lookup)) { o.lookup = { suggestions: o.lookup, data: [] }; } + } + $('#' + this.mainContainerId).css({ zIndex: o.zIndex }); + this.container.css({ maxHeight: o.maxHeight + 'px', width: o.width }); + }, + + clearCache: function () { + this.cachedResponse = []; + this.badQueries = []; + }, + + disable: function () { + this.disabled = true; + }, + + enable: function () { + this.disabled = false; + }, + + fixPosition: function () { + var offset = this.el.offset(); + $('#' + this.mainContainerId).css({ top: (offset.top + this.el.innerHeight()) + 'px', left: offset.left + 'px' }); + }, + + enableKillerFn: function () { + var me = this; + $(document).bind('click', me.killerFn); + }, + + disableKillerFn: function () { + var me = this; + $(document).unbind('click', me.killerFn); + }, + + killSuggestions: function () { + var me = this; + this.stopKillSuggestions(); + this.intervalId = window.setInterval(function () { me.hide(); me.stopKillSuggestions(); }, 300); + }, + + stopKillSuggestions: function () { + window.clearInterval(this.intervalId); + }, + + onValueChanged: function () { + this.change(this.selectedIndex); + }, + + onKeyPress: function (e) { + if (this.disabled || !this.enabled) { return; } + // return will exit the function + // and event will not be prevented + switch (e.keyCode) { + case 27: //KEY_ESC: + this.el.val(this.currentValue); + this.hide(); + break; + case 9: //KEY_TAB: + case 13: //KEY_RETURN: + if (this.selectedIndex === -1) { + this.hide(); + return; + } + this.select(this.selectedIndex); + if (e.keyCode === 9) { return; } + break; + case 38: //KEY_UP: + this.moveUp(); + break; + case 40: //KEY_DOWN: + this.moveDown(); + break; + default: + return; + } + e.stopImmediatePropagation(); + e.preventDefault(); + }, + + onKeyUp: function (e) { + if (this.disabled) { return; } + switch (e.keyCode) { + case 38: //KEY_UP: + case 40: //KEY_DOWN: + return; + } + clearInterval(this.onChangeInterval); + if (this.currentValue !== this.el.val()) { + if (this.options.deferRequestBy > 0) { + // Defer lookup in case when value changes very quickly: + var me = this; + this.onChangeInterval = setInterval(function () { me.onValueChange(); }, this.options.deferRequestBy); + } else { + this.onValueChange(); + } + } + }, + + onValueChange: function () { + clearInterval(this.onChangeInterval); + this.currentValue = this.el.val(); + var q = this.getQuery(this.currentValue); + this.selectedIndex = -1; + if (this.ignoreValueChange) { + this.ignoreValueChange = false; + return; + } + if (q === '' || q.length < this.options.minChars) { + this.hide(); + } else { + this.getSuggestions(q); + } + }, + + getQuery: function (val) { + var d, arr; + d = this.options.delimiter; + if (!d) { return $.trim(val); } + arr = val.split(d); + return $.trim(arr[arr.length - 1]); + }, + + getSuggestionsLocal: function (q) { + var ret, arr, len, val, i; + arr = this.options.lookup; + len = arr.suggestions.length; + ret = { suggestions: [], data: [] }; + q = q.toLowerCase(); + for (i = 0; i < len; i++) { + val = arr.suggestions[i]; + if (val.toLowerCase().indexOf(q) === 0) { + ret.suggestions.push(val); + ret.data.push(arr.data[i]); + } + } + return ret; + }, + + getSuggestions: function (q) { + + var cr, me; + cr = this.isLocal ? this.getSuggestionsLocal(q) : this.cachedResponse[q]; //dadeta this.options.isLocal || + if (cr && $.isArray(cr.suggestions)) { + this.suggestions = cr.suggestions; + this.data = cr.data; + this.suggest(); + } else if (!this.isBadQuery(q)) { + me = this; + me.options.params.query = q; + $.get(this.serviceUrl, me.options.params, function (txt) { me.processResponse(txt); }, 'text'); + } + }, + + isBadQuery: function (q) { + var i = this.badQueries.length; + while (i--) { + if (q.indexOf(this.badQueries[i]) === 0) { return true; } + } + return false; + }, + + hide: function () { + this.enabled = false; + this.selectedIndex = -1; + this.container.hide(); + }, + + suggest: function () { + + if (this.suggestions.length === 0) { + this.hide(); + return; + } + + var me, len, div, f, v, i, s, mOver, mClick; + me = this; + len = this.suggestions.length; + f = this.options.fnFormatResult; + v = this.getQuery(this.currentValue); + mOver = function (xi) { return function () { me.activate(xi); }; }; + mClick = function (xi) { return function () { me.select(xi); }; }; + this.container.hide().empty(); + for (i = 0; i < len; i++) { + s = this.suggestions[i]; + div = $((me.selectedIndex === i ? '
    ' + f(s, this.data[i], v) + '
    '); + div.mouseover(mOver(i)); + div.click(mClick(i)); + this.container.append(div); + } + this.enabled = true; + this.container.show(); + }, + + processResponse: function (text) { + var response; + try { + response = eval('(' + text + ')'); + } catch (err) { return; } + if (!$.isArray(response.data)) { response.data = []; } + if (!this.options.noCache) { + this.cachedResponse[response.query] = response; + if (response.suggestions.length === 0) { this.badQueries.push(response.query); } + } + if (response.query === this.getQuery(this.currentValue)) { + this.suggestions = response.suggestions; + this.data = response.data; + this.suggest(); + } + }, + + activate: function (index) { + var divs, activeItem; + divs = this.container.children(); + // Clear previous selection: + if (this.selectedIndex !== -1 && divs.length > this.selectedIndex) { + $(divs.get(this.selectedIndex)).removeClass(); + } + this.selectedIndex = index; + if (this.selectedIndex !== -1 && divs.length > this.selectedIndex) { + activeItem = divs.get(this.selectedIndex); + $(activeItem).addClass('selected'); + } + return activeItem; + }, + + deactivate: function (div, index) { + div.className = ''; + if (this.selectedIndex === index) { this.selectedIndex = -1; } + }, + + select: function (i) { + var selectedValue, f; + selectedValue = this.suggestions[i]; + if (selectedValue) { + this.el.val(selectedValue); + if (this.options.autoSubmit) { + f = this.el.parents('form'); + if (f.length > 0) { f.get(0).submit(); } + } + this.ignoreValueChange = true; + this.hide(); + this.onSelect(i); + } + }, + + change: function (i) { + var selectedValue, fn, me; + me = this; + selectedValue = this.suggestions[i]; + if (selectedValue) { + var s, d; + s = me.suggestions[i]; + d = me.data[i]; + me.el.val(me.getValue(s)); + } + else { + s = ''; + d = -1; + } + + fn = me.options.onChange; + if ($.isFunction(fn)) { fn(s, d, me.el); } + }, + + moveUp: function () { + if (this.selectedIndex === -1) { return; } + if (this.selectedIndex === 0) { + this.container.children().get(0).className = ''; + this.selectedIndex = -1; + this.el.val(this.currentValue); + return; + } + this.adjustScroll(this.selectedIndex - 1); + }, + + moveDown: function () { + if (this.selectedIndex === (this.suggestions.length - 1)) { return; } + this.adjustScroll(this.selectedIndex + 1); + }, + + adjustScroll: function (i) { + var activeItem, offsetTop, upperBound, lowerBound; + activeItem = this.activate(i); + offsetTop = activeItem.offsetTop; + upperBound = this.container.scrollTop(); + lowerBound = upperBound + this.options.maxHeight - 25; + if (offsetTop < upperBound) { + this.container.scrollTop(offsetTop); + } else if (offsetTop > lowerBound) { + this.container.scrollTop(offsetTop - this.options.maxHeight + 25); + } + this.el.val(this.getValue(this.suggestions[i])); + }, + + onSelect: function (i) { + var me, fn, s, d; + me = this; + fn = me.options.onSelect; + s = me.suggestions[i]; + d = me.data[i]; + me.el.val(me.getValue(s)); + if ($.isFunction(fn)) { fn(s, d, me.el); } + }, + + getValue: function (value) { + var del, currVal, arr, me; + me = this; + del = me.options.delimiter; + if (!del) { return value; } + currVal = me.currentValue; + arr = currVal.split(del); + if (arr.length === 1) { return value; } + return currVal.substr(0, currVal.length - arr[arr.length - 1].length) + value; + } + + }; + +} (jQuery)); diff --git a/web/app/assets/javascripts/jquery.formToObject.js b/web/app/assets/javascripts/jquery.formToObject.js new file mode 100644 index 000000000..e50881c72 --- /dev/null +++ b/web/app/assets/javascripts/jquery.formToObject.js @@ -0,0 +1,110 @@ +(function($) { + + /** + * Go through the current JQuery match, and build a + * javascript object based on the name attributes of + * child elements. This is intended to be used for forms + * with inputs: var jsObj = $('#myform').formToObject(); + * + * Using dots in the name attributes will build nested + * structures into the resulting object: + * + * + * returns + * { foo: { bar:"x" } } + * + * The same name appearing twice will be returned as an array: + * + * + * + * returns + * { foo: ['a','b'] } + * + * Checkboxes and Radio inputs which are not checked will not + * appear at all, nor will any inputs which are disabled. + * + * Form fields which are optional can be omitted from the resulting + * object by providing a value for that field of "__OMIT__". + * + */ + "use strict"; + + $.fn.formToObject = function() { + // don't want to depend on other libs. Provide our own size to check + // length of an associative array: + function _size(obj) { + var key, size=0; + for (key in obj) { + if (obj.hasOwnProperty(key)) size++; + } + return size; + } + + var formObject = {}, + a = this.serializeArray(); + + $.each(a, function() { + var data = $('[name="' + this.name + '"]').first().data(), + current = formObject, + childKey, + path = this.name.split('.'), + key = path.pop(), + val = (data && data.parseInt !== undefined && data.parseInt) ? parseInt(this.value, 10) : ( + (this.value.toLowerCase() == 'true') ? true : ( + (this.value.toLowerCase() == 'false') ? false : ( + (this.value.toLowerCase() == 'null') ? null : ( + (typeof this.value == 'undefined') ? '' : this.value + ) + ) + ) + ); + + while (path.length > 0) { + // Shift the front element from path into childKey + childKey = path.shift(); + if (!(childKey in current)) { + current[childKey] = {}; + } + // Move down and start working with the child object + current = current[childKey]; + } + + // We're at the right level in hierarchy. Define property next. + if (key in current) { // already exists. + if ($.isPlainObject(current[key])) { // about to overwrite obj with property! + throw "Can't overwrite named structure"; + } + if (!current[key].push) { // convert property to array + current[key] = [current[key]]; + } + current[key].push(val); + } else { // doesn't exist. Just set. + if (val !== '__OMIT__') { + current[key] = val; + } + } + }); + + // Trim any keys who hold nothing but empty objects: {} + + var removeEmptyObjects = function(o) { + var k; + var trimlist = []; + for (k in o) { + if ($.isPlainObject(o[k])) { // recurse: deep first + removeEmptyObjects(o[k]); + } + if ($.isPlainObject(o[k]) && _size(o[k]) === 0) { + trimlist.push(k); + } + } + $.each(trimlist, function() { + delete o[this]; + }); + }; + + removeEmptyObjects(formObject); + return (_size(formObject) === 0) ? null : formObject; + }; + +})(jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/jquery.hoverIntent.js b/web/app/assets/javascripts/jquery.hoverIntent.js new file mode 100644 index 000000000..3dcff261f --- /dev/null +++ b/web/app/assets/javascripts/jquery.hoverIntent.js @@ -0,0 +1,106 @@ +/** +* hoverIntent is similar to jQuery's built-in "hover" function except that +* instead of firing the onMouseOver event immediately, hoverIntent checks +* to see if the user's mouse has slowed down (beneath the sensitivity +* threshold) before firing the onMouseOver event. +* +* hoverIntent r6 // 2011.02.26 // jQuery 1.5.1+ +* +* +* hoverIntent is currently available for use in all personal or commercial +* projects under both MIT and GPL licenses. This means that you can choose +* the license that best suits your project, and use it accordingly. +* +* // basic usage (just like .hover) receives onMouseOver and onMouseOut functions +* $("ul li").hoverIntent( showNav , hideNav ); +* +* // advanced usage receives configuration object only +* $("ul li").hoverIntent({ +* sensitivity: 7, // number = sensitivity threshold (must be 1 or higher) +* interval: 100, // number = milliseconds of polling interval +* over: showNav, // function = onMouseOver callback (required) +* timeout: 0, // number = milliseconds delay before onMouseOut function call +* out: hideNav // function = onMouseOut callback (required) +* }); +* +* @param f onMouseOver function || An object with configuration options +* @param g onMouseOut function || Nothing (use configuration options object) +* @author Brian Cherne brian(at)cherne(dot)net +*/ +(function($) { + $.fn.hoverIntent = function(f,g) { + // default configuration options + var cfg = { + sensitivity: 7, + interval: 100, + timeout: 0 + }; + // override configuration options with user supplied object + cfg = $.extend(cfg, g ? { over: f, out: g } : f ); + + // instantiate variables + // cX, cY = current X and Y position of mouse, updated by mousemove event + // pX, pY = previous X and Y position of mouse, set by mouseover and polling interval + var cX, cY, pX, pY; + + // A private function for getting mouse position + var track = function(ev) { + cX = ev.pageX; + cY = ev.pageY; + }; + + // A private function for comparing current and previous mouse position + var compare = function(ev,ob) { + ob.hoverIntent_t = clearTimeout(ob.hoverIntent_t); + // compare mouse positions to see if they've crossed the threshold + if ( ( Math.abs(pX-cX) + Math.abs(pY-cY) ) < cfg.sensitivity ) { + $(ob).unbind("mousemove",track); + // set hoverIntent state to true (so mouseOut can be called) + ob.hoverIntent_s = 1; + return cfg.over.apply(ob,[ev]); + } else { + // set previous coordinates for next time + pX = cX; pY = cY; + // use self-calling timeout, guarantees intervals are spaced out properly (avoids JavaScript timer bugs) + ob.hoverIntent_t = setTimeout( function(){compare(ev, ob);} , cfg.interval ); + } + }; + + // A private function for delaying the mouseOut function + var delay = function(ev,ob) { + ob.hoverIntent_t = clearTimeout(ob.hoverIntent_t); + ob.hoverIntent_s = 0; + return cfg.out.apply(ob,[ev]); + }; + + // A private function for handling mouse 'hovering' + var handleHover = function(e) { + // copy objects to be passed into t (required for event object to be passed in IE) + var ev = jQuery.extend({},e); + var ob = this; + + // cancel hoverIntent timer if it exists + if (ob.hoverIntent_t) { ob.hoverIntent_t = clearTimeout(ob.hoverIntent_t); } + + // if e.type == "mouseenter" + if (e.type == "mouseenter") { + // set "previous" X and Y position based on initial entry point + pX = ev.pageX; pY = ev.pageY; + // update "current" X and Y position based on mousemove + $(ob).bind("mousemove",track); + // start polling interval (self-calling timeout) to compare mouse coordinates over time + if (ob.hoverIntent_s != 1) { ob.hoverIntent_t = setTimeout( function(){compare(ev,ob);} , cfg.interval );} + + // else e.type == "mouseleave" + } else { + // unbind expensive mousemove event + $(ob).unbind("mousemove",track); + // if hoverIntent state is true, then call the mouseOut function after the specified delay + if (ob.hoverIntent_s == 1) { ob.hoverIntent_t = setTimeout( function(){delay(ev,ob);} , cfg.timeout );} + } + }; + + // bind the function to the two event listeners + return this.bind('mouseenter',handleHover).bind('mouseleave',handleHover); + }; +})(jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/landing/congratulations.js b/web/app/assets/javascripts/landing/congratulations.js new file mode 100644 index 000000000..3fafd07e0 --- /dev/null +++ b/web/app/assets/javascripts/landing/congratulations.js @@ -0,0 +1,59 @@ +(function(context,$) { + + "use strict"; + + var congratulations = {}; + + congratulations.initialize = function initialize() { + var rest = context.JK.Rest(); + + var currentOS = context.JK.detectOS(); + + var downloads = $('.downloads'); + rest.getClientDownloads() + .done(function(data) { + downloads.removeClass('spinner-large'); + + var count = 0; + for ( var property in data ) count++; + + + if(count == 0) { + alert("Currently unable to list client software downloads."); + } + else { + + $.each(data, function(key, item) { + + + // if the currentOS we detect from browser is found within the product of an available client + // we flag it with this boolean + var matchesUserOS = currentOS != null && key.toLowerCase().indexOf(currentOS.toLowerCase()) > -1; + + var options = { + emphasis: matchesUserOS ? "currentOS" : "", + uri: item.uri, + platform: key.substring('JamClient/'.length) + } + + var download = context._.template($('#client-download-link').html(), options, {variable: 'data'}); + + if(matchesUserOS) { + // make sure the current user OS is at the top + downloads.prepend(download); + } + else { + downloads.append(download) + } + }); + } + }) + .fail(function() { + downloads.removeClass('spinner-large'); + alert("Currently unable to list client software downloads due to error."); + }) + } + + window.congratulations = congratulations; + +})(window, jQuery) \ No newline at end of file diff --git a/web/app/assets/javascripts/landing/init.js b/web/app/assets/javascripts/landing/init.js new file mode 100644 index 000000000..bc2a5c696 --- /dev/null +++ b/web/app/assets/javascripts/landing/init.js @@ -0,0 +1,9 @@ +(function(context,$) { + + "use strict"; + + $(function() { + context.JK.popExternalLinks(); + }) + +})(window, jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/landing/landing.js b/web/app/assets/javascripts/landing/landing.js new file mode 100644 index 000000000..144b25c75 --- /dev/null +++ b/web/app/assets/javascripts/landing/landing.js @@ -0,0 +1,8 @@ +//= require jquery +//= require AAC_underscore +//= require jamkazam +//= require utils +//= require jam_rest +//= require landing/init +//= require landing/signup +//= require landing/congratulations \ No newline at end of file diff --git a/web/app/assets/javascripts/landing/signup.js b/web/app/assets/javascripts/landing/signup.js new file mode 100644 index 000000000..b0b006caa --- /dev/null +++ b/web/app/assets/javascripts/landing/signup.js @@ -0,0 +1,189 @@ +(function() { + + var signup = {} + + function enable_disable_instruments(enable) { + var instrument_selector = $('#instrument_selector'); + + if(enable) { + instrument_selector.removeAttr('style') + $('input', instrument_selector).removeAttr('disabled').removeAttr('style'); + $('select', instrument_selector).removeAttr('disabled').removeAttr('style'); + } + else { + instrument_selector.css('color', '#AAA') + $('input', instrument_selector).attr('disabled', 'disabled').css('color', '#AAA'); + $('select', instrument_selector).attr('disabled', 'disabled').css('color', '#AAA'); + } + } + + // instruments are selectable if musician = true + signup.handle_register_as_changes = function handle_register_as_changes() { + + var initial_value = $('input.register-as:checked').val(); + enable_disable_instruments(initial_value == "true"); + + $('input.register-as').change(function() { + var value = $(this).val() + enable_disable_instruments(value == "true") + }) + } + + // register form elements relating to location to update appropriately as the user makes changes + signup.handle_location_changes = function handle_location_changes() { + + var country_select = $('#country_select') + var region_select = $('#region_select') + var city_select = $('#city_select') + + country_select.change(function() { + var selected_country = $(this).val() + if(selected_country) { + // set region disabled while updating + region_select.attr('disabled', true); + + $.ajax('/api/regions', { + data : { country: selected_country }, + dataType : 'json' + }).done(regions_done).fail(regions_fail).always(function() { region_select.attr('disabled', false) }) + } + }) + + function regions_done(data) { + region_select.children().remove() + + region_select.append("") + + $(data.regions).each(function(index, item) { + region_select.append("") + }) + } + + function regions_fail() { + alert("something went wrong in looking up regions") + } + + region_select.change(function() { + var selected_country = country_select.val() + var selected_region = $(this).val() + + // only update + if(selected_country && selected_region) { + // set city disabled while updating + city_select.attr('disabled', true); + + $.ajax('/api/cities', { + data : { country: selected_country, region: selected_region }, + dataType : 'json' + }).done(cities_done).fail(cities_fail).always(function() { city_select.attr('disabled', false) }) + } + }) + + function cities_done(data) { + city_select.children().remove() + + city_select.append("") + + $(data.cities).each(function(index, item) { + city_select.append("") + }) + } + + function cities_fail() { + alert("something went wrong in looking up cities") + } + } + + signup.handle_completion_submit = function handle_completion_submit() { + + // based on rails date_select element, gather up birth_date selected by the user. + // returns null if any field (day/month/year) is left blank + function gather_birth_date() { + var month = $('#jam_ruby_user_birth_date_2i').value + var day = $('#jam_ruby_user_birth_date_3i').value + var year = $('#jam_ruby_user_birth_date_1i').value + + if(month != null && month.length > 0 && day != null && day.length > 0 && year != null && year.length > 0) { + return month + "-" + day + "-" + year; + } + else { + return null; + } + } + + // looks in instrument_selector parent element, and gathers up all + // selected elements, and the proficiency level declared + function gather_instruments() { + var instruments_parent_element = $(".instrument_selector") + + var instruments = [] + $('input[type=checkbox]:checked', instruments_parent_element).each(function(i) { + + // traverse up to common parent of this intrument, and pick out proficienc selector + var proficiency = $('select.proficiency_selector', $(this).closest('tr')).val() + + instruments.push({ + instrument_id: $(this).attr('name'), + proficiency_level: proficiency, + priority : i + }) + }); + + return instruments; + } + + $('form.edit_jam_ruby_user').submit(function() { + + var form = $(this) + + var submit_data = {}; + // gather up all fields from the form + submit_data.first_name = $('#jam_ruby_user_first_name').val() + submit_data.last_name = $('#jam_ruby_user_last_name').val() + submit_data.password = $('#jam_ruby_user_password').val() + submit_data.password_confirmation = $('#jam_ruby_user_password_confirmation').val() + submit_data.country = $('#jam_ruby_user_country').val() + submit_data.state = $('#jam_ruby_user_state').val() + submit_data.city = $('#jam_ruby_user_city').val() + submit_data.birth_date = gather_birth_date() + submit_data.instruments = gather_instruments() + //submit_data.photo_url = $('#jam_ruby_user_instruments').val() + + + $.ajax({ + type: "POST", + url: "/api/users/complete/" + gon.signup_token, + contentType: 'application/json', + data: JSON.stringify(submit_data) + }).done(completion_done).fail(completion_fail) + + return false; + }) + + function completion_done(data) { + // we can redirect on to client + window.location.href = '/client' + + } + + function completion_fail(xhr, status) { + // if status = 422, then we can display errors + if(xhr.status == 422) { + data = JSON.parse(xhr.responseText) + var errors = $('#errors') + errors.children().remove() + var list = $("
      ") + errors.append(list) + $(data).each(function(i) { + list.append("
    • " + this + "
    • ") + }) + } + else { + alert("something went wrong with the service. please try again later") + } + } + + } + + window.signup = signup +})() \ No newline at end of file diff --git a/web/app/assets/javascripts/layout.js b/web/app/assets/javascripts/layout.js new file mode 100644 index 000000000..2e3688d08 --- /dev/null +++ b/web/app/assets/javascripts/layout.js @@ -0,0 +1,689 @@ +/* + * View framework for JamKazam. + * + * Processes proprietary attributes in markup to convert a set of HTML elements + * into the JamKazam screen layout. This module is only responsible for size + * and position. All other visual aspects should be elsewhere. + * + * See the layout-example.html file for a simple working example. + */ +(function(context,$) { + + "use strict"; + + context.JK = context.JK || {}; + + // Static function to hide the 'curtain' which hides the underlying + // stuff until we can get it laid out. Called from both the main + // client as well as the landing page. + context.JK.hideCurtain = function(duration) { + context.setTimeout(function() { + $('.curtain').fadeOut(2*duration); + }, duration); + }; + + context.JK.Layout = function() { + + // privates + var logger = context.JK.logger; + + var me = null; // Reference to this instance for context sanity. + + var opts = { + headerHeight: 75, + sidebarWidth: 300, + notifyHeight: 150, + notifyGutter: 10, + collapsedSidebar: 30, + panelHeaderHeight: 36, + gutter: 60, // Margin around the whole UI + screenMargin: 0, // Margin around screens (not headers/sidebar) + gridOuterMargin: 6, // Outer margin on Grids (added to screenMargin if screen) + gridPadding: 8, // Padding around grid cells. Added to outer margin. + animationDuration: 400, + allowBodyOverflow: false // Allow tests to disable the body-no-scroll policy + }; + + var width = $(context).width(); + var height = $(context).height(); + var resizing = null; + var sidebarVisible = true; + var expandedPanel = null; + var previousScreen = null; + var currentScreen = null; + + var screenBindings = {}; + var dialogBindings = {}; + var wizardShowFunctions = {}; + + function setup() { + requiredStyles(); + hideAll(); + setInitialExpandedSidebarPanel(); + sizeScreens(width, height, '[layout="screen"]', true); + positionOffscreens(width, height); + $('[layout="sidebar"]').show(); + $('[layout="panel"]').show(); + layout(); + } + + function setInitialExpandedSidebarPanel() { + expandedPanel = $('[layout="panel"]').first().attr("layout-id"); + } + + function layout() { + width = $(context).width(); + height = $(context).height(); + // TODO + // Work on naming. File is layout, class is Layout, this method + // is layout and every other method starts with 'layoutX'. Perhaps + // a little redundant? + layoutCurtain(width, height); + layoutDialogOverlay(width, height); + layoutScreens(width, height); + layoutSidebar(width, height); + layoutHeader(width, height); + layoutNotify(width, height); + layoutFooter(width, height); + } + + function layoutCurtain(screenWidth, screenHeight) { + var curtainStyle = { + position: 'absolute', + width: screenWidth + 'px', + height: screenHeight + 'px' + }; + $('.curtain').css(curtainStyle); + } + + function layoutDialogOverlay(screenWidth, screenHeight) { + var style = { + position: 'absolute', + width: screenWidth + 'px', + height: screenHeight + 'px' + }; + $('.dialog-overlay').css(style); + } + + function layoutScreens(screenWidth, screenHeight) { + var previousScreenSelector = '[layout-id="' + previousScreen + '"]'; + var currentScreenSelector = '[layout-id="' + currentScreen + '"]'; + $(currentScreenSelector).show(); + + var width = screenWidth - (2 * opts.gutter + 2 * opts.screenMargin); + var left = -1 * width - 100; + + if (currentScreenSelector === previousScreenSelector) { + left = $(currentScreenSelector).css("left"); + if (left) { + left = left.split("px")[0]; + } + } + $(previousScreenSelector).animate({left: left}, {duration: opts.animationDuration, queue: false}); + sizeScreens(screenWidth, screenHeight, '[layout="screen"]'); + positionOffscreens(screenWidth, screenHeight); + positionOnscreen(screenWidth, screenHeight); + } + + function sizeScreens(screenWidth, screenHeight, selector, immediate) { + var duration = opts.animationDuration; + if (immediate) { + duration = 0; + } + + var width = screenWidth - (2 * opts.gutter + 2 * opts.screenMargin); + if (sidebarVisible) { + width -= (opts.sidebarWidth + 2*opts.gridPadding); + } else { + width -= opts.collapsedSidebar + 2*opts.gridPadding; + width += opts.gutter; // Add back in the right gutter width. + } + var height = screenHeight - opts.headerHeight - (2 * opts.gutter + 2 * opts.screenMargin); + var css = { + width: width, + height: height + }; + var $screens = $(selector); + $screens.animate(css, {duration:duration, queue:false}); + layoutHomeScreen(width, height); + } + + /** + * Postition all screens that are not the current screen. + */ + function positionOffscreens(screenWidth, screenHeight) { + var top = opts.headerHeight + opts.gutter + opts.screenMargin; + var left = -1 * (screenWidth + 2*opts.gutter); + var $screens = $('[layout="screen"]').not('[layout-id="' + currentScreen + '"]'); + $screens.css({ + top: top, + left: left + }); + } + + /** + * Position the current screen + */ + function positionOnscreen(screenWidth, screenHeight, immediate) { + var duration = opts.animationDuration; + if (immediate) { + duration = 0; + } + var top = opts.headerHeight + opts.gutter + opts.screenMargin; + var left = opts.gutter + opts.screenMargin; + var $screen = $('[layout-id="' + currentScreen + '"]'); + $screen.animate({ + top: top, + left: left, + overflow: 'auto' + }, duration); + } + + function layoutHomeScreen(homeScreenWidth, homeScreenHeight) { + var $grid = $('[layout-grid]'); + var gridWidth = homeScreenWidth; + var gridHeight = homeScreenHeight; + $grid.css({width:gridWidth, height: gridHeight}); + var layout = $grid.attr('layout-grid'); + if (!layout) + return; + var gridRows = layout.split('x')[0]; + var gridCols = layout.split('x')[1]; + + $grid.children().each(function() { + var childPosition = $(this).attr("layout-grid-position"); + var childRow = childPosition.split(',')[1]; + var childCol = childPosition.split(',')[0]; + var childRowspan = $(this).attr("layout-grid-rows"); + var childColspan = $(this).attr("layout-grid-columns"); + var childLayout = me.getCardLayout(gridWidth, gridHeight, gridRows, gridCols, + childRow, childCol, childRowspan, childColspan); + + $(this).animate({ + width: childLayout.width, + height: childLayout.height, + top: childLayout.top, + left: childLayout.left + }, opts.animationDuration); + }); + } + + function layoutSidebar(screenWidth, screenHeight) { + var width = opts.sidebarWidth; + var expanderHeight = $('[layout-sidebar-expander]').height(); + var height = screenHeight - opts.headerHeight - 2 * opts.gutter + expanderHeight; + var right = opts.gutter; + if (!sidebarVisible) { + // Negative right to hide most of sidebar + right = (0 - opts.sidebarWidth) + opts.collapsedSidebar; + } + var top = opts.headerHeight + opts.gutter - expanderHeight; + var css = { + width: width, + height: height, + top: top, + right: right + }; + $('[layout="sidebar"]').animate(css, opts.animationDuration); + layoutPanels(width, height); + if (sidebarVisible) { + $('[layout-panel="collapsed"]').hide(); + $('[layout-panel="expanded"]').show(); + $('[layout-sidebar-expander="hidden"]').hide(); + $('[layout-sidebar-expander="visible"]').show(); + } else { + $('[layout-panel="collapsed"]').show(); + $('[layout-panel="expanded"]').hide(); + $('[layout-sidebar-expander="hidden"]').show(); + $('[layout-sidebar-expander="visible"]').hide(); + } + } + + function layoutPanels(sidebarWidth, sidebarHeight) { + // TODO - don't like the accordian - poor usability. Requires longest mouse + // reach when switching panels. Probably better to do tabs. + if (!sidebarVisible) { + return; + } + var $expandedPanelContents = $('[layout-id="' + expandedPanel + '"] [layout-panel="contents"]'); + var combinedHeaderHeight = $('[layout-panel="contents"]').length * opts.panelHeaderHeight; + var searchHeight = $('.sidebar .search').first().height(); + var expanderHeight = $('[layout-sidebar-expander]').height(); + var expandedPanelHeight = sidebarHeight - (combinedHeaderHeight + expanderHeight + searchHeight); + $('[layout-panel="contents"]').hide(); + $('[layout-panel="contents"]').css({"height": "1px"}); + $expandedPanelContents.show(); + $expandedPanelContents.animate({"height": expandedPanelHeight + "px"}, opts.animationDuration); + } + + function layoutHeader(screenWidth, screenHeight) { + var width = screenWidth - 2*opts.gutter; + var height = opts.headerHeight - opts.gutter; + var top = opts.gutter; + var left = opts.gutter; + var css = { + width: width + "px", + height: height + "px", + top: top + "px", + left: left + "px" + }; + $('[layout="header"]').css(css); + } + + function layoutNotify(screenWidth, screenHeight) { + var $notify = $('[layout="notify"]'); + var nHeight = $notify.height(); + var notifyStyle = { + bottom: '0px', + position: 'fixed' + }; + $notify.css(notifyStyle); + } + + function layoutFooter(screenWidth, screenHeight) { + var $footer = $('#footer'); + var nHeight = $footer.height(); + var footerStyle = { + top: (screenHeight - 80) + 'px' + }; + var width = screenWidth - (2 * opts.gutter + 2 * opts.screenMargin); + var left = -1 * width - 100; + $footer.animate({ "left" : opts.gutter, "width" : width, "top": (screenHeight - 78) + "px"}, opts.animationDuration); + } + + function requiredStyles() { + var bodyStyle = { + margin: '0px', + padding: '0px', + overflow: 'hidden' + }; + if (opts.allowBodyOverflow) { + delete bodyStyle.overflow; + } + $('body').css(bodyStyle); + + var layoutStyle = { + position: 'absolute', + margin: '0px', + padding: '0px' + }; + $('[layout]').css(layoutStyle); + $('[layout="notify"]').css({"z-index": "9", "padding": "20px"}); + $('[layout="panel"]').css({position: 'relative'}); + $('[layout-panel="expanded"] [layout-panel="header"]').css({ + margin: "0px", + padding: "0px", + height: opts.panelHeaderHeight + "px" + }); + $('[layout-grid]').css({ + position: "relative" + }); + $('[layout-grid]').children().css({ + position: "absolute" + }); + var curtainStyle = { + position: "absolute", + margin: '0px', + padding: '0px', + overflow: 'hidden', + zIndex: 100 + }; + $('.curtain').css(curtainStyle); + } + + function hideAll() { + $('[layout]').hide(); + $('[layout="header"]').show(); + } + + function showSidebar() { + sidebarVisible = true; + layout(); + } + + function hideSidebar() { + sidebarVisible = false; + layout(); + } + + function toggleSidebar() { + if (sidebarVisible) { + hideSidebar(); + } else { + showSidebar(); + } + } + + function hideDialogs() { + // TODO - may need dialogEvents here for specific dialogs. + $('[layout="dialog"]').hide(); + $('.dialog-overlay').hide(); + } + + function tabClicked(evt) { + evt.preventDefault(); + var destination = $(evt.currentTarget).attr('tab-target'); + $('[tab-target]').removeClass('selected'); + $(evt.currentTarget).addClass('selected'); + $('.tab').hide(); + $('[tab-id="' + destination + '"]').show(); + } + + function linkClicked(evt) { + evt.preventDefault(); + + // allow links to be disabled + if($(evt.currentTarget).hasClass("disabled") ) { + return; + } + + var destination = $(evt.currentTarget).attr('layout-link'); + var destinationType = $('[layout-id="' + destination + '"]').attr("layout"); + if (destinationType === "screen") { + context.location = '#/' + destination; + } else if (destinationType === "dialog") { + showDialog(destination); + } + } + + function close(evt) { + var $target = $(evt.currentTarget).closest('[layout]'); + var layoutId = $target.attr('layout-id'); + var isDialog = ($target.attr('layout') === 'dialog'); + if (isDialog) { + closeDialog(layoutId); + } else { + $target.hide(); + } + return false; + } + + function closeDialog(dialog) { + var $dialog = $('[layout-id="' + dialog + '"]'); + dialogEvent(dialog, 'beforeHide'); + $('.dialog-overlay').hide(); + $dialog.hide(); + dialogEvent(dialog, 'afterHide'); + } + + function screenEvent(screen, evtName, data) { + if (screen && screen in screenBindings) { + if (evtName in screenBindings[screen]) { + screenBindings[screen][evtName].call(me, data); + } + } + } + + function dialogEvent(dialog, evtName, data) { + if (dialog && dialog in dialogBindings) { + if (evtName in dialogBindings[dialog]) { + dialogBindings[dialog][evtName].call(me, data); + } + } + } + + function changeToScreen(screen, data) { + previousScreen = currentScreen; + currentScreen = screen; + + screenEvent(previousScreen, 'beforeHide', data); + screenEvent(currentScreen, 'beforeShow', data); + + // For now -- it seems we want it open always. + // TODO - support user preference here? Remember how they left it? + sidebarVisible = true; + /* + var openSidebarScreens = [ + 'home', 'session', 'createSession', + 'findSession', 'searchResults' + ]; + $.each(openSidebarScreens, function() { + logger.debug("comparing " + this + " to " + currentScreen); + if (this === currentScreen) { + sidebarVisible = true; + return false; + } + }); + */ + layout(); + + screenEvent(previousScreen, 'afterHide', data); + screenEvent(currentScreen, 'afterShow', data); + + // Show any requested dialog + if ("d" in data) { + showDialog(data.d); + } + } + + function showDialog(dialog) { + dialogEvent(dialog, 'beforeShow'); + $('.dialog-overlay').show(); + centerDialog(dialog); + $('[layout-id="' + dialog + '"]').show(); + dialogEvent(dialog, 'afterShow'); + } + + function centerDialog(dialog) { + var $dialog = $('[layout-id="' + dialog + '"]'); + $dialog.css({ + left: width/2 - ($dialog.width()/2) + "px", + top: height/2 - ($dialog.height()/2) + "px" + }); + } + + + function panelHeaderClicked(evt) { + evt.preventDefault(); + expandedPanel = $(evt.currentTarget).closest('[layout="panel"]').attr("layout-id"); + layout(); + return false; + } + + function wizardLinkClicked(evt) { + evt.preventDefault(); + var targetStepId = $(evt.currentTarget).attr("layout-wizard-link"); + setWizardStep(targetStepId); + return false; + } + + function setWizardStep(targetStepId) { + var selector = '[layout-wizard-step="' + targetStepId + '"]'; + var $targetStep = $(selector); + var stepDialogTitle = $targetStep.attr("dialog-title"); + if (stepDialogTitle) { + var $myDialog = $targetStep.closest('[layout="dialog"]'); + var $myTitle = $('.content-head h1', $myDialog); + $myTitle.html(stepDialogTitle); + } + // Hide all steps: + // Invoke the 'show' function, if present prior to actually showing. + if (context._.contains(context._.keys(wizardShowFunctions), targetStepId)) { + wizardShowFunctions[targetStepId](); + } + $('[layout-wizard-step]').hide(); + $targetStep.show(); + } + + function events() { + $(context).resize(function() { + if (resizing) { + context.clearTimeout(resizing); + } + resizing = context.setTimeout(layout, 80); + }); + $('body').on('click', '[layout-link]', linkClicked); + $('[layout-action="close"]').on('click', close); + $('[layout-sidebar-expander]').on('click', toggleSidebar); + $('[layout-panel="expanded"] [layout-panel="header"]').on('click', panelHeaderClicked); + $('[layout-wizard-link]').on('click', wizardLinkClicked); + $('[tab-target]').on('click', tabClicked); + } + + // public functions + this.getOpts = function() { + return opts; + }; + + // used for concurrent notifications + var notifyQueue = []; + var firstNotification = false; + var notifyDetails; + + this.notify = function(message, descriptor) { + var $notify = $('[layout="notify"]'); + + if (notifyQueue.length === 0) { + firstNotification = true; + setNotificationInfo(message, descriptor); + } + + notifyQueue.push({message: message, descriptor: descriptor}); + $notify.slideDown(2000) + .delay(2000) + .slideUp({ + duration: 2000, + queue: true, + complete: function() { + notifyDetails = notifyQueue.shift(); + + // shift 1 more time if this is first notification being displayed + if (firstNotification) { + notifyDetails = notifyQueue.shift(); + firstNotification = false; + } + + if (notifyDetails !== undefined) { + setNotificationInfo(notifyDetails.message, notifyDetails.descriptor); + } + } + }); + } + + function setNotificationInfo(message, descriptor) { + var $notify = $('[layout="notify"]'); + $('h2', $notify).text(message.title); + $('p', $notify).text(message.text); + + if (message.icon_url) { + $('#avatar', $notify).attr('src', message.icon_url); + $('#avatar', $notify).show(); + } + else { + $('#avatar', $notify).hide(); + } + + if (message.detail) { + $('div.detail', $notify).html(message.detail).show(); + } + else { + $('div.detail', $notify).hide(); + } + + if (descriptor) { + if (descriptor.ok_text) { + $('#ok-button', $notify).html(descriptor.ok_text); + } + else { + $('#ok-button', $notify).html("OKAY"); + } + + if (descriptor.ok_callback !== undefined) { + $('#ok-button', $notify).click(function() { + if (descriptor.ok_callback_args) { + logger.debug("descriptor.ok_callback_args=" + descriptor.ok_callback_args); + descriptor.ok_callback(descriptor.ok_callback_args); + return false; + } + else { + descriptor.ok_callback(); + return false; + } + }); + } + + if (descriptor.cancel_text) { + $('#cancel-button', $notify).html(descriptor.cancel_text); + } + else { + if(descriptor.no_cancel) { + $('#cancel-button', $notify).hide(); + } + else { + $('#cancel-button', $notify).html("CANCEL"); + } + } + } + else { + $('#ok-button', $notify).html("OKAY"); + $('#cancel-button', $notify).html("CANCEL"); + } + } + + this.setWizardStep = setWizardStep; + + this.changeToScreen = function(screen, data) { + changeToScreen(screen, data); + }; + + this.showDialog = function(dialog) { + showDialog(dialog); + }; + + this.close = function(evt) { + close(evt); + }; + + this.closeDialog = closeDialog; + + /** + * Given information on a grid, and a given card's grid settings, use the + * margin options and return a list of [top, left, width, height] + * for the cell. + */ + this.getCardLayout = function(gridWidth, gridHeight, gridRows, gridCols, + row, col, rowspan, colspan) { + + var _gridWidth = gridWidth + 3*opts.gridPadding; + var _gridHeight = gridHeight + 3*opts.gridPadding; + var cellWidth, cellHeight, top, left, width, height; + + cellWidth = Math.floor((_gridWidth-2*opts.gridOuterMargin) / gridCols); + cellHeight = Math.floor((_gridHeight-2*opts.gridOuterMargin) / gridRows); + width = colspan * cellWidth - 2*opts.gridPadding; + height = rowspan * cellHeight - 2*opts.gridPadding; + top = row * cellHeight;// + opts.gridOuterMargin; // + opts.gridPadding; + left = col * cellWidth;// + opts.gridOuterMargin; // + opts.gridPadding; + + return { + top: top, + left: left, + width: width, + height: height + }; + }; + + this.bindScreen = function(screen, handler) { + screenBindings[screen] = handler; + }; + + this.bindDialog = function(dialog, handler) { + dialogBindings[dialog] = handler; + }; + + this.registerWizardStepFunction = function(stepId, showFunction) { + wizardShowFunctions[stepId] = showFunction; + }; + + this.initialize = function(inOpts) { + me = this; + opts = $.extend(opts, inOpts); + setup(); + events(); + }; + + return this; + + }; + +}(window, jQuery)); \ No newline at end of file diff --git a/web/app/assets/javascripts/leaveSession.js b/web/app/assets/javascripts/leaveSession.js new file mode 100644 index 000000000..11d90b736 --- /dev/null +++ b/web/app/assets/javascripts/leaveSession.js @@ -0,0 +1,32 @@ +(function(context,$) { + + "use strict"; + + context.JK = context.JK || {}; + + context.JK.leaveMusicSession = function(session_id) { + var logger = context.JK.logger; + var server = context.JK.JamServer; + var client = context.jamClient; + + if (!server.signedIn) + { + logger.log("Can't leave a session because the client is not connected."); + return; + } + + logger.log("Leaving session: " + session_id); + + var url = "/api/participants/" + server.clientID; + $.ajax({ + type: "DELETE", + url: url + }).done(function (response) { + context.JK.Sessions.LeaveSession(session_id); + if (client !== undefined) { + client.LeaveSession({ sessionID: session_id }); + } + }); + }; + + })(window,jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/mixer.js b/web/app/assets/javascripts/mixer.js new file mode 100644 index 000000000..e855d08b9 --- /dev/null +++ b/web/app/assets/javascripts/mixer.js @@ -0,0 +1,41 @@ +/** +* Static functions for controlling underlying jamClient mixer +* functions. +*/ +(function(context, $) { + + /** + * We'll want to move control over mixer functions out of the session.js + * file and into here, as it is becoming apparent we'll need better + * audio control globally. + */ + + "use strict"; + + context.JK = context.JK || {}; + + context.JK.Mixer = { + + /** + * Fill the track volume object, given a controlState structure. + * Provide a boolean for broadcast indicating whether the change + * should go out over the network + */ + fillTrackVolume: function(controlState, broadcast) { + context.trackVolumeObject.clientID = controlState.client_id; + context.trackVolumeObject.broadcast = broadcast; + context.trackVolumeObject.master = controlState.master; + context.trackVolumeObject.monitor = controlState.monitor; + context.trackVolumeObject.mute = controlState.mute; + context.trackVolumeObject.name = controlState.name; + context.trackVolumeObject.record = controlState.record; + context.trackVolumeObject.volL = controlState.volume_left; + context.trackVolumeObject.volR = controlState.volume_right; + // trackVolumeObject doesn't have a place for range min/max + //currentMixerRangeMin = mixer.range_low; + //currentMixerRangeMax = mixer.range_high; + } + + }; + +})(window, jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/profile.js b/web/app/assets/javascripts/profile.js new file mode 100644 index 000000000..3268d1834 --- /dev/null +++ b/web/app/assets/javascripts/profile.js @@ -0,0 +1,610 @@ +(function(context,$) { + + "use strict"; + + context.JK = context.JK || {}; + context.JK.ProfileScreen = function(app) { + var logger = context.JK.logger; + var userId; + var user = {}; + + var proficiencyDescriptionMap = { + "1": "BEGINNER", + "2": "INTERMEDIATE", + "3": "EXPERT" + }; + + var proficiencyCssMap = { + "1": "proficiency-beginner", + "2": "proficiency-intermediate", + "3": "proficiency-expert" + }; + + function beforeShow(data) { + userId = data.id; + } + + function afterShow(data) { + resetForm(); + events(); + renderActive(); + } + + function resetForm() { + $('#profile-instruments').empty(); + + $('#profile-about').show(); + $('#profile-history').hide(); + $('#profile-bands').hide(); + $('#profile-social').hide(); + $('#profile-favorites').hide(); + + $('.profile-nav a.active').removeClass('active'); + $('.profile-nav a.#profile-about-link').addClass('active'); + } + + /****************** MAIN PORTION OF SCREEN *****************/ + // events for main screen + function events() { + // wire up panel clicks + $('#profile-about-link').click(renderAbout); + $('#profile-history-link').click(renderHistory); + $('#profile-bands-link').click(renderBands); + $('#profile-social-link').click(renderSocial); + $('#profile-favorites-link').click(renderFavorites); + + // wire up buttons if you're not viewing your own profile + if (userId != context.JK.currentUserId) { + // wire up Add Friend click + var friend = isFriend(); + configureFriendButton(friend); + + // wire up Follow click + var following = isFollowing(); + configureFollowingButton(following); + } + else { + $('#btn-add-friend').hide(); + $('#btn-follow-user').hide(); + } + } + + function sendFriendRequest(evt) { + evt.stopPropagation(); + context.JK.sendFriendRequest(app, userId, friendRequestCallback); + } + + function removeFriend(evt) { + evt.stopPropagation(); + + var url = "/api/users/" + context.JK.currentUserId + "/friends/" + userId; + $.ajax({ + type: "DELETE", + dataType: "json", + url: url, + processData: false, + success: function(response) { + renderActive(); // refresh stats + configureFriendButton(false); + }, + error: app.ajaxError + }); + } + + function isFriend() { + var alreadyFriend = false; + + var url = "/api/users/" + context.JK.currentUserId + "/friends/" + userId; + $.ajax({ + type: "GET", + dataType: "json", + url: url, + async: false, + processData: false, + success: function(response) { + if (response.id !== undefined) { + alreadyFriend = true; + } + else { + alreadyFriend = false; + } + }, + error: app.ajaxError + }); + + return alreadyFriend; + } + + function friendRequestCallback() { + configureFriendButton(true); + } + + function configureFriendButton(friend) { + $('#btn-add-friend').unbind("click"); + + if (friend) { + $('#btn-add-friend').text('REMOVE FRIEND'); + $('#btn-add-friend').click(removeFriend); + } + else { + $('#btn-add-friend').text('ADD FRIEND'); + $('#btn-add-friend').click(sendFriendRequest); + } + } + + function addFollowing() { + var newFollowing = {}; + newFollowing.user_id = userId; + + var url = "/api/users/" + context.JK.currentUserId + "/followings"; + $.ajax({ + type: "POST", + dataType: "json", + contentType: 'application/json', + url: url, + data: JSON.stringify(newFollowing), + processData: false, + success: function(response) { + renderActive(); // refresh stats + configureFollowingButton(true); + }, + error: app.ajaxError + }); + } + + function removeFollowing(isBand, id) { + var following = {}; + + if (!isBand) { + following.user_id = id; + } + else { + following.band_id = id; + } + + var url = "/api/users/" + context.JK.currentUserId + "/followings"; + $.ajax({ + type: "DELETE", + dataType: "json", + contentType: 'application/json', + url: url, + data: JSON.stringify(following), + processData: false, + success: function(response) { + renderActive(); // refresh stats + if (!isBand) { + configureFollowingButton(false); + } + else { + configureBandFollowingButton(false, id); + } + }, + error: app.ajaxError + }); + } + + function isFollowing() { + var alreadyFollowing = false; + + var url = "/api/users/" + context.JK.currentUserId + "/followings/" + userId; + $.ajax({ + type: "GET", + dataType: "json", + url: url, + async: false, + processData: false, + success: function(response) { + if (response.id !== undefined) { + alreadyFollowing = true; + } + else { + alreadyFollowing = false; + } + }, + error: app.ajaxError + }); + + return alreadyFollowing; + } + + function configureFollowingButton(following) { + $('#btn-follow-user').unbind("click"); + + if (following) { + $('#btn-follow-user').text('STOP FOLLOWING'); + $('#btn-follow-user').click(function() { + removeFollowing(false, userId); + }); + } + else { + $('#btn-follow-user').text('FOLLOW'); + $('#btn-follow-user').click(addFollowing); + } + } + + // refreshes the currently active tab + function renderActive() { + if ($('#profile-about-link').hasClass('active')) { + renderAbout(); + } + else if ($('#profile-history-link').hasClass('active')) { + renderHistory(); + } + else if ($('#profile-bands-link').hasClass('active')) { + renderBands(); + } + else if ($('#profile-social-link').hasClass('active')) { + renderSocial(); + } + else if ($('#profile-favorites-link').hasClass('active')) { + renderFavorites(); + } + } + + /****************** ABOUT TAB *****************/ + function renderAbout() { + $('#profile-instruments').empty(); + + $('#profile-about').show(); + $('#profile-history').hide(); + $('#profile-bands').hide(); + $('#profile-social').hide(); + $('#profile-favorites').hide(); + + $('.profile-nav a.active').removeClass('active'); + $('.profile-nav a.#profile-about-link').addClass('active'); + + bindAbout(); + } + + function bindAbout() { + $('#profile-instruments').empty(); + var url = "/api/users/" + userId; + $.ajax({ + type: "GET", + dataType: "json", + url: url, + async: false, + processData:false, + success: function(response) { + user = response; + }, + error: app.ajaxError + }); + + if (user) { + + // name + $('#profile-username').html(user.name); + + // avatar + $('#profile-avatar').attr('src', context.JK.resolveAvatarUrl(user.photo_url)); + + // instruments + if (user.instruments) { + for (var i=0; i < user.instruments.length; i++) { + var instrument = user.instruments[i]; + var description = instrument.instrument_id; + var proficiency = instrument.proficiency_level; + var instrument_icon_url = context.JK.getInstrumentIcon45(description); + + // add instrument info to layout + var template = $('#template-profile-instruments').html(); + var instrumentHtml = context.JK.fillTemplate(template, { + instrument_logo_url: instrument_icon_url, + instrument_description: description, + proficiency_level: proficiencyDescriptionMap[proficiency], + proficiency_level_css: proficiencyCssMap[proficiency] + }); + + $('#profile-instruments').append(instrumentHtml); + } + } + + // location + $('#profile-location').html(user.location); + + // stats + var text = user.friend_count > 1 || user.friend_count == 0 ? " Friends" : " Friend"; + $('#profile-friend-stats').html(user.friend_count + text); + + text = user.follower_count > 1 || user.follower_count == 0 ? " Followers" : " Follower"; + $('#profile-follower-stats').html(user.follower_count + text); + + text = user.session_count > 1 || user.session_count == 0 ? " Sessions" : " Session"; + $('#profile-session-stats').html(user.session_count + text); + + text = user.recording_count > 1 || user.recording_count == 0 ? " Recordings" : " Recording"; + $('#profile-recording-stats').html(user.recording_count + text); + + //$('#profile-biography').html(user.biography); + } + else { + + } + } + + /****************** SOCIAL TAB *****************/ + function renderSocial() { + $('#profile-social-friends').empty(); + $('#profile-social-followings').empty(); + $('#profile-social-followers').empty(); + + $('#profile-about').hide(); + $('#profile-history').hide(); + $('#profile-bands').hide(); + $('#profile-social').show(); + $('#profile-favorites').hide(); + + $('.profile-nav a.active').removeClass('active'); + $('.profile-nav a.#profile-social-link').addClass('active'); + + bindSocial(); + } + + function bindSocial() { + // FRIENDS + var url = "/api/users/" + userId + "/friends"; + $.ajax({ + type: "GET", + dataType: "json", + url: url, + async: false, + processData:false, + success: function(response) { + $.each(response, function(index, val) { + var template = $('#template-profile-social').html(); + var friendHtml = context.JK.fillTemplate(template, { + avatar_url: context.JK.resolveAvatarUrl(val.photo_url), + userName: val.name, + location: val.location, + type: "Friends" + }); + + $('#profile-social-friends').append(friendHtml); + }); + }, + error: app.ajaxError + }); + + // FOLLOWINGS (USERS) + url = "/api/users/" + userId + "/followings"; + $.ajax({ + type: "GET", + dataType: "json", + url: url, + async: false, + processData:false, + success: function(response) { + $.each(response, function(index, val) { + var template = $('#template-profile-social').html(); + var followingHtml = context.JK.fillTemplate(template, { + avatar_url: context.JK.resolveAvatarUrl(val.photo_url), + userName: val.name, + location: val.location + }); + + $('#profile-social-followings').append(followingHtml); + }); + }, + error: app.ajaxError + }); + + // FOLLOWINGS (BANDS) + url = "/api/users/" + userId + "/band_followings"; + $.ajax({ + type: "GET", + dataType: "json", + url: url, + async: false, + processData:false, + success: function(response) { + $.each(response, function(index, val) { + var template = $('#template-profile-social').html(); + var followingHtml = context.JK.fillTemplate(template, { + avatar_url: context.JK.resolveAvatarUrl(val.logo_url), + userName: val.name, + location: val.location + }); + + $('#profile-social-followings').append(followingHtml); + }); + }, + error: app.ajaxError + }); + + // FOLLOWERS + url = "/api/users/" + userId + "/followers"; + $.ajax({ + type: "GET", + dataType: "json", + url: url, + async: false, + processData:false, + success: function(response) { + $.each(response, function(index, val) { + var template = $('#template-profile-social').html(); + var followerHtml = context.JK.fillTemplate(template, { + avatar_url: context.JK.resolveAvatarUrl(val.photo_url), + userName: val.name, + location: val.location + }); + + $('#profile-social-followers').append(followerHtml); + }); + }, + error: app.ajaxError + }); + } + + /****************** HISTORY TAB *****************/ + function renderHistory() { + $('#profile-about').hide(); + $('#profile-history').show(); + $('#profile-bands').hide(); + $('#profile-social').hide(); + $('#profile-favorites').hide(); + + $('.profile-nav a.active').removeClass('active'); + $('.profile-nav a.#profile-history-link').addClass('active'); + + bindHistory(); + } + + function bindHistory() { + + } + + /****************** BANDS TAB *****************/ + function renderBands() { + $('#profile-bands').empty(); + + $('#profile-about').hide(); + $('#profile-history').hide(); + $('#profile-bands').show(); + $('#profile-social').hide(); + $('#profile-favorites').hide(); + + $('.profile-nav a.active').removeClass('active'); + $('.profile-nav a.#profile-bands-link').addClass('active'); + + bindBands(); + } + + function bindBands() { + var url = "/api/users/" + userId + "/bands"; + $.ajax({ + type: "GET", + dataType: "json", + url: url, + async: false, + processData:false, + success: function(response) { + $.each(response, function(index, val) { + var template = $('#template-profile-bands').html(); + var bandHtml = context.JK.fillTemplate(template, { + bandId: val.id, + avatar_url: context.JK.resolveAvatarUrl(val.logo_url), + name: val.name, + location: val.location, + genres: formatGenres(val.genres) + }); + + $('#profile-bands').append(bandHtml); + + // wire up Band Follow button click handler + var following = isFollowingBand(val.id); + configureBandFollowingButton(following, val.id); + }); + }, + error: app.ajaxError + }); + } + + function formatGenres(genres) { + var formattedGenres = ''; + for (var i=0; i < genres.length; i++) { + var genre = genres[i]; + formattedGenres += genre.description; + if (i < genres.length -1) { + formattedGenres += ', '; + } + } + return formattedGenres; + } + + function addBandFollowing(evt) { + evt.stopPropagation(); + var bandId = $(this).parent().attr('band-id'); + + var newFollowing = {}; + newFollowing.band_id = bandId; + + var url = "/api/users/" + context.JK.currentUserId + "/followings"; + $.ajax({ + type: "POST", + dataType: "json", + contentType: 'application/json', + url: url, + data: JSON.stringify(newFollowing), + processData: false, + success: function(response) { + renderActive(); // refresh stats + configureBandFollowingButton(true, bandId); + }, + error: app.ajaxError + }); + } + + function isFollowingBand(bandId) { + var alreadyFollowing = false; + + var url = "/api/users/" + context.JK.currentUserId + "/band_followings/" + bandId; + $.ajax({ + type: "GET", + dataType: "json", + url: url, + async: false, + processData: false, + success: function(response) { + if (response.id !== undefined) { + alreadyFollowing = true; + } + else { + alreadyFollowing = false; + } + }, + error: app.ajaxError + }); + + return alreadyFollowing; + } + + function configureBandFollowingButton(following, bandId) { + var $btnFollowBand = $('div[band-id=' + bandId + ']', '#profile-bands').find('#btn-follow-band'); + $btnFollowBand.unbind("click"); + + if (following) { + $btnFollowBand.text('UN-FOLLOW'); + $btnFollowBand.click(function() { + removeFollowing(true, bandId); + }); + } + else { + $btnFollowBand.text('FOLLOW'); + $btnFollowBand.click(addBandFollowing); + } + } + + /****************** FAVORITES TAB *****************/ + function renderFavorites() { + $('#profile-about').hide(); + $('#profile-history').hide(); + $('#profile-bands').hide(); + $('#profile-social').hide(); + $('#profile-favorites').show(); + + $('.profile-nav a.active').removeClass('active'); + $('.profile-nav a.#profile-favorites-link').addClass('active'); + + bindFavorites(); + } + + function bindFavorites() { + } + + function initialize() { + var screenBindings = { + 'beforeShow': beforeShow, + 'afterShow': afterShow + }; + app.bindScreen('profile', screenBindings); + } + + this.initialize = initialize; + this.beforeShow = beforeShow; + this.afterShow = afterShow; + return this; + }; + + })(window,jQuery); \ No newline at end of file diff --git a/web/app/assets/javascripts/routemap.js b/web/app/assets/javascripts/routemap.js new file mode 100644 index 000000000..e5e3824bb --- /dev/null +++ b/web/app/assets/javascripts/routemap.js @@ -0,0 +1,445 @@ +/** + *

      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: + *
      + * 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.
      + * 
      + * @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 * 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 '; + } + + 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='"+af+"";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 "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 @@ + +
        + +
        +
        + <%= image_tag "content/icon_account.png", {:height => 18, :width => 18} %> +
        +

        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 @@ + +
        + +
        + +
        + <%= image_tag "content/icon_account.png", {:width => 27, :height => 20} %> +
        + +

        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 @@ + +
        + +
        + +
        + <%= image_tag "content/icon_account.png", {:width => 27, :height => 20} %> +
        + +

        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 @@ + +
        + +
        + +
        + <%= image_tag "content/icon_account.png", {:width => 27, :height => 20} %> +
        + +

        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 @@ + +
        + +
        + +
        + <%= image_tag "content/icon_account.png", {:width => 27, :height => 20} %> +
        + +

        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:
        +
        +
        + +
        +
        +
        +
        +
        +
        + +
        + CANCEL  + ADD TRACK +
        +
        +
        +
        +
        \ 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 @@ + +
        +
        + +
        + <%= image_tag "content/icon_bands.png", {:height => 19, :width => 19} %> +
        + +

        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 @@ + +
        +
        + + +
        + <%= image_tag("content/icon_alert.png", :height => '24', :width => '24', :class => "content-icon") %>

        alert

        +
        + + +
        + +
        + +
        + +
        + + + + + + + + \ 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

        +
        +
        + +
        + +
        + + +
        + + +
        +
        +
        +
        Audio Device:
        +
        +
        + +
        +
        + +
        +
        + +
        + + +
        +
        +
        Unused Inputs:
        +
        +
        + +
        +
        +
        +
        +
        + +
        +
        + +
        +
        +
        +
        +
        + +
        +
        + +
        +
        +
        +
        +
        +
        +
        Track 1 Input:
        +
        +
        + +
        +
        +
        +
        Track 1 Instrument:
        +
        +
        + +
        +
        + +
        + +
        +
        +
        Track 2 Input:
        +
        +
        + +
        +
        +
        +
        Track 2 Instrument:
        +
        +
        + +
        +
        +
        +
        +
        +
        + +
        + + +
        +
        +
        Unused Outputs:
        +
        +
        + +
        +
        +
        +
        + +
        +
        + +
        +
        +
        +
        Session Audio Output:
        +
        +
        + +
        +
        +
        +
        +
        + + + +
        + +
        +
        +
        \ 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 @@ + +
        +
        +
        +
        + <%= image_tag "content/icon_add.png", :size => "19x19" %> +
        + +

        create session

        + + <%= render "screen_navigation" %> +
        +
        +
        +
        + +
        + +

        session info

        + +
        + +
        +
        Genre:
        +
        + <%= render "genreSelector" %> +
        +
        + +
        +
        Band:
        +
        + +
        +
        + +

        + +
        +
        Description:
        +
        + +
        +
        + +
        + +
        Musician Access:
        +
        +
        + +
        + +
        +  Open   +  By Approval +
        +
        + +

        + +
        Fan Access:
        +
        +
        + +
        + +
        +  Chat   +  No Fan Chat +
        +
        +
        + +
        +

        invite musicians

        + +
        + +
        +
        + Start typing friends' names or: +
        + +
        + +
        +
        + + +
        +
        + +
        + +
        + Invite friends and contacts to join you on JamKazam from: +
        +
        + + + + +
        +
        +
        + +
        +
        + +
        +
        + I agree that intellectual property ownership of any musical works created during this session shall be governed by the terms of the Creative Commons CC BY-NC-SA license in accordance with the JamKazam Terms of Service. +
        +
        +
        +
        +
        +
        + CANCEL + JAM! +
        +
        +
        +
        +
        +
        +
        +
        +
        + +<%= 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' } %> +

        +
        +
        + +

        +
        + OK +
        +
        +
        +
        \ 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 @@ + +
        +
        + +
        + <%= image_tag "content/icon_feed.png", {:height => 19, :width => 19} %> +
        +

        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 @@ + +
        +
        +
        +
        + <%= image_tag "content/icon_search.png", :size => "19x19" %> +
        + +

        find a session

        + + <%= render "screen_navigation" %> +
        +
        +
        +
        +
        Filter Session List:
        + + +
        + + + +
        + + +
        + <%= render "genreSelector" %> +
        + + + +
        + REFRESH +
        +
        +
        +
        +
        +
        + <%= render :partial => "sessionList", :locals => {:title => "sessions you're invited to", :category => "sessions-invitations"} %> +
        +
        + <%= render :partial => "sessionList", :locals => {:title => "sessions with friends or bandmates", :category => "sessions-friends"} %> +
        +
        + <%= render :partial => "sessionList", :locals => {:title => "other sessions", :category => "sessions-other"} %> +
        +
        +
        +
        +
        +
        + + + + + + + + + + \ 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 @@ + +
        +
        + +
        +

        +
        +
        +
        + CANCEL  +
        + +
        + + + \ 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. +

        + +
          + + + +
        + +
        + CANCEL  + NEXT +
        + +
        + + +
        +

        + Choose an ASIO driver, and choose the devices to use for audio and + chat. Then play and speak, and use the gain sliders so that you hear + both your instrument and voice well through your headphones. + Finally, use the ASIO Settings button to tune your latency settings + as low as possible, and set the frame and buffer values as low as + possible while listening to be sure your audio still sounds good. + Click the resync button if audio stops. When done click Next. +

        +

        + Choose the devices to use for audio and chat below. Then play and + speak, and use the gain sliders so that you hear both your + instrument and voice well through your headphones. Finally, set the + frame and buffer values as low as possible while listening to be + sure your audio still sounds good. Click the resync button if audio + stops. When done click Next. +

        + +
        +
        + Audio Driver:
        + +
        +
        +
        + Frame:
        + +
        +
        + Buffer/In:
        + +
        +
        + Buffer/Out:
        + +
        +
        +
        +
        + ASIO SETTINGS + +
        +
        +
        + +
        +
        +
        + <%= image_tag "shared/icon_help.png", {:width => 12, :height => 12} %> +
        + Audio Input: + +
        +
        +
        +
        GAIN
        +
        +
        +
        +
        +
        + <%= image_tag "shared/icon_help.png", {:width => 12, :height => 12} %> +
        + Voice Chat Input: + +
        +
        +
        +
        GAIN
        +
        +
        +
        +
        +
        + <%= image_tag "shared/icon_help.png", {:width => 12, :height => 12} %> +
        + Audio Output: + +
        +
        +
        +
        GAIN
        +
        +
        +
        +
        + +
        + BACK + HELP  + NEXT +
        + +
        + + + +
        +

        Please set up your audio gear for the automated latency test as described in the tutorial video. Quick reminders on how to do this follow. When ready, click the "START TEST" button to begin the automated test process.

        + +
        Audio Device with Input/Output Ports for Instruments & Mics
        +

        Plug one end of an audio cable into an input port and the other end into a headphone or output port.

        + +
        USB Microphone
        +

        Use an audio cable to connect the headphone jack on the mic to a speaker and set the speaker next to the mic, or plug headphones into the headphone jack and place the headphone output against the mic.

        + +
        Computer's Built-In Mic & Speakers
        +

        Turn up the computers built-in speaker volume to about two-thirds of max volume.

        + +
        + BACK + HELP  + START TEST +
        + +
        + + +
        +

        + <%= image_tag "shared/loading-animation-4.gif", + {} %> + Testing round trip latency for the audio device you selected. This should take just a few seconds. +

        +
        + + +
        +

        + <%= image_tag "content/icon_alert_big.png", + {:width => 96, :height => 96, :class => "left"} %> + We're sorry, but there appears to be a problem with your audio + gear setup, and we cannot run a latency test successfully. + Please use the Help button below for troubleshooting tips to + correct your audio gear setup, and then use the Run Test Again + button to re-run the test when you think your setup is ready. +

        + + + +
        + + +
        +
        + <%= image_tag "content/latency_gauge_back.png", + {:width => 247, :height => 242, :class => "left"} %> +
        + <%= image_tag "content/latency_gauge_needle.png", + {:width => 6, :height => 220 } %> +
        +
        10
        +
        milliseconds
        +
        +

        +



        +

        + Congratulations! Your local end-to-end audio gear latency is + milliseconds, which is good. You may now create and join sessions + to play with other musicians using JamKazam. +
        + + +

        + + + +
        + + +
        + +
        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

        + +
        +
        + +
        + <% else %> +
        +
        +

        feed

        + +
        +
        +
        +

        bands

        + +
        +
        +
        +

        musicians

        + +
        +
        +
        +

        profile

        + +
        +
        + +
        + <% 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 @@ + +
        + +
        +
        +
        +
        +
        +
        +
        +
        +
        + CANCEL  +
        +
        + INVITE + NEXT +
        +
        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 @@ + +
        +
        + +
        + <%= image_tag "content/icon_musicians.png", {:height => 19, :width => 19} %> +
        + +

        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 @@ + +
        +
        +
        + <%= image_tag "content/icon_profile.png", :size => "19x19" %> +
        + +

        musician profile

        + + <%= render "screen_navigation" %> +
        +
        +
        + + +

        + + +
        +
        + + + +

        + + +
        +
        + +
        +
        + + + +
        +
        +
        +
        + +
        +

        Location:


        +


        +

        Stats:


        +
        +
        +
        +
        +
        +
        +


        +
        +
        +
        +
        +
        +
        +
        +
        +
        +
        +
        +
        +
        +

        Friends

        +
        +
        +
        +
        +

        Following

        +
        +
        +
        +
        +

        Followers

        +
        +
        +
        +
        +
        +
        +
        +
        +
        +
        +
        + + + + + + \ 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 @@ + +
        +
        +
        + <%= image_tag "shared/icon_session.png", {:height => 19, :width => 19} %> +
        +

        session

        + <%= render "screen_navigation" %> +
        + + + + + + +
        +
        + + +
        + + +
        +

        my tracks

        + + +
        +
        + +
        +
        + + + + + +
        +

        recordings

        +
        +   +
        +
        +

        + No Recordings:
        Open a Recording +

        +
        + + + +
        +
        +
        + +
        +
        +
        +
        + + +<%= 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 %>

        + + + + + + + + + + + + +
        GENREDESCRIPTIONMUSICIANSAUDIENCELATENCYLISTENJOIN
        \ 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

        +
        + +
        +
        + + +
        + + + Genre:
        + +
        + <%= render "genreSelector" %> +
        +
        +
        + +
        + Musician Access:
        +
        + +  Open  
        +  By Approval +
        +
        + + Fan Access:
        + +
        +  Chat  
        +  No Fan Chat + +
        +
        + + +
        + + + Description:
        + +
        +
        + + +
        + +
        + +
        +
        + + +
        + +
        diff --git a/web/app/views/clients/_sidebar.html.erb b/web/app/views/clients/_sidebar.html.erb new file mode 100644 index 000000000..51681fb58 --- /dev/null +++ b/web/app/views/clients/_sidebar.html.erb @@ -0,0 +1,178 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/web/app/views/clients/_testBridge.html.erb b/web/app/views/clients/_testBridge.html.erb new file mode 100644 index 000000000..6b6d51ada --- /dev/null +++ b/web/app/views/clients/_testBridge.html.erb @@ -0,0 +1,62 @@ + +
        +
        +

        Javascript Bridge Test

        + <%= render "screen_navigation" %> +
        +
        I don't think I'm connected!
        +
        + +
        +

        Create hard-coded session

        + +
        +
        + +
        +

        Join sesssion

        + +
        + +
        +

        TestLatency

        + +
        + +
        +
        +
        +

        GetFTUE

        + + +
        +
        SetFTUE

        +
        + + + +
        +
        + +
        +

        GetASIODevices

        + + +
        + + + + diff --git a/web/app/views/clients/_vu_meters.html.erb b/web/app/views/clients/_vu_meters.html.erb new file mode 100644 index 000000000..bd72905d3 --- /dev/null +++ b/web/app/views/clients/_vu_meters.html.erb @@ -0,0 +1,33 @@ + + + + + + + + diff --git a/web/app/views/clients/banners/_disconnected.html.erb b/web/app/views/clients/banners/_disconnected.html.erb new file mode 100644 index 000000000..4a5b341be --- /dev/null +++ b/web/app/views/clients/banners/_disconnected.html.erb @@ -0,0 +1,42 @@ + + + + + \ No newline at end of file diff --git a/web/app/views/clients/index.html.erb b/web/app/views/clients/index.html.erb new file mode 100644 index 000000000..d2f129f30 --- /dev/null +++ b/web/app/views/clients/index.html.erb @@ -0,0 +1,185 @@ +
        +
        + +

        Connecting...

        +
        +
        + + +<%= render "header" %> +<%= render "home" %> +<%= render "footer" %> +<%= render "searchResults" %> +<%= render "faders" %> +<%= render "vu_meters" %> +<%= render "ftue" %> +<%= render "sidebar" %> +<%= render "createSession" %> +<%= render "findSession" %> +<%= render "session" %> +<%= render "profile" %> +<%= render "feed" %> +<%= render "bands" %> +<%= render "musicians" %> +<%= render "testBridge" %> +<%= render "account" %> +<%= render "account_identity" %> +<%= render "account_profile" %> +<%= render "account_profile_avatar" %> +<%= render "account_audio_profile" %> +<%= render "notify" %> +<%= render "client_update" %> +<%= render "banner" %> +<%= render "clients/banners/disconnected" %> +<%= render "overlay_small" %> + + + + + + diff --git a/web/app/views/corps/about.html.erb b/web/app/views/corps/about.html.erb new file mode 100644 index 000000000..3aff18037 --- /dev/null +++ b/web/app/views/corps/about.html.erb @@ -0,0 +1,9 @@ +<% provide(:title, 'About') %> +<% provide(:purpose, 'about') %> +

        About Us

        + +

        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') %> +

        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!

        +

        Support

        +

        For technical support and help using our products and services, please visit the JamKazam Support Center.

        +

        Partnerships

        +

        For partnering inquiries, please contact us at: partners@jamkazam.com.

        +

        Media

        +

        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.

        +

        Infringement

        +

        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.

        +

        General

        +

        For general inquiries, please contact us at: info@jamkazam.com.

        +

        Feedback:

        +
        +
        + +
        +
        +
        +
        + +
        + + +
        +

        + + \ No newline at end of file diff --git a/web/app/views/corps/cookie_policy.html.erb b/web/app/views/corps/cookie_policy.html.erb new file mode 100644 index 000000000..675da3d07 --- /dev/null +++ b/web/app/views/corps/cookie_policy.html.erb @@ -0,0 +1,84 @@ +<% provide(:title, 'Cookies Policy') %> +<% provide(:purpose, 'cookie_policy') %> + +

        Cookies Policy

        + +

        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.

        + +

        What are cookies?

        +

        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/.

        + +

        JamKazam Cookies

        +

        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”.

        + +

        Third Party Cookies

        +

        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

        +

        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.

        + +

        Similar Technologies

        +

        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.

        + +

        Cookie Controls

        +

        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.

        + +

        Consent to Cookies and Similar Technologies

        +

        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') %> +

        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.

        + +

        +JamKazam Support +

        \ 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') %> + +

        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

        +

        + + +
        + diff --git a/web/app/views/corps/news.html.erb b/web/app/views/corps/news.html.erb new file mode 100644 index 000000000..5160df07c --- /dev/null +++ b/web/app/views/corps/news.html.erb @@ -0,0 +1,17 @@ +<% provide(:title, 'News') %> +<% provide(:purpose, 'news') %> + +

        News

        +

        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 �

        + + + + \ No newline at end of file diff --git a/web/app/views/corps/premium_accounts.html.erb b/web/app/views/corps/premium_accounts.html.erb new file mode 100644 index 000000000..324f5edff --- /dev/null +++ b/web/app/views/corps/premium_accounts.html.erb @@ -0,0 +1,55 @@ +<% provide(:title, 'Premium Accounts') %> +<% provide(:purpose, 'premium') %> + +

        Premium Accounts

        + +

        Certain features of the Platform are only available to registered users who subscribe for a paid premium account (“Premium Account”). Below, you’ll find a few additional terms that apply to those Premium Account holders, including, but not limited to, terms relating to payment, the conclusion and renewal of your contract, your right of cancellation during the first 14 days of your subscription, and certain technical usage limitations.

        + +

        Premium Account Features and Prices

        +

        A detailed description of our Premium Accounts, including the prices and the features associated with the various Premium Account service levels, is available in the account section of the Website and Apps.

        + +

        Purchasing a Premium Account Subscription

        +

        Selecting a Premium Account plan and submitting your payment information for such plan is an offer to execute a contract. The contract itself is not concluded until you receive confirmation from us or until we activate your Premium Account, as described below.

        + +

        Execution of Contract and Start of Subscription

        +

        Once your payment has been received, your Premium Account will be activated, and we will send confirmation by email to the address associated with your account.

        +

        The contract for your Premium Account is fully executed and effective when you receive our confirmation email (or when we activate your Premium Account, if this happens first), not when you make your payment.

        + +

        Payment

        +

        Fees for Premium Accounts are payable in advance. For yearly plans, your payment will be taken at the time the contract is executed as described above. For monthly plans, payment will be taken 14 days after the contract is initially executed, but for any subsequent renewals, payment will be taken at the beginning of the respective monthly period.

        +

        Payments can be made by major credit card or PayPal and/or any other payment method that is presented to you during the purchase process.

        + +

        Term and Renewal

        +

        If you choose a monthly payment option, your Premium Account subscription will run from month to month, and will renew automatically at the end of each monthly period. Payment for the next monthly period will be taken at that time, unless you cancel your Premium Account, which you can do at any time from the account section of the Website or Apps.

        +

        Annual accounts run for twelve months from the date on which your Premium Account is activated, and will renew automatically at the end of each such annual period. Payment for the next annual period will be taken at that time, unless you cancel your Premium Account, which you can do at any time from the account section of the Website or Apps.

        + +

        Upgrading your Premium Account

        +

        If you wish to upgrade your Premium Account to a higher service level, you may do so at any time within the account section of the Website or Apps. The upgraded service level of your subscription will become effective immediately after your payment has been processed, and we will tell you the increased cost as well as any prorated effects of time remaining on your existing Premium Account during the payment process.

        + +

        Right of Cancellation

        +

        All of our customers have the right to cancel any purchase of a Premium Account within the first fourteen (14) days, starting from the day on which the contract is executed.

        +

        In order to cancel your subscription, simply go to the account section of either the Website or Apps, and use the option available there to cancel your Premium Account subscription.

        +

        If you are located in the European Union, you can also exercise your right of cancellation by contacting us by email, letter or fax to confirm your cancellation. You do not have to provide any reason for your cancellation. If you wish to exercise your right of cancellation in this way, you should contact us at:

        +

        JamKazam, Inc.
        +5813 Lookout Mountain Drive
        +Austin, TX 78731
        +

        + +

        Fax: (512) 857.1300
        + termination@jamkazam.com +

        + +

        Your notice of cancellation is deemed to be served as soon as it is posted or sent to us using the methods described above.

        +

        If you cancel a Premium Account in accordance with this process, we will refund you for any payments you have made with respect to your Premium Account, and will do so within (30) days of the date of receipt of your valid notice of cancellation.

        +

        Following receipt of your notice of cancellation, JamKazam will downgrade your Premium Account to a free account and any Content in your account that exceeds the then current service level limits for a free account will be deleted and/or hidden. Only Your Content created most recently will be shown in your account, up to the free account service level limit. If you wish to delete your account altogether, you can do this by sending an email from the email address associated with your account to support@jamkazam.com.

        + +

        Usage Limitations

        +

        Please note that Premium Accounts are subject to service level limits on various types of activities, storage, etc. These service level constraints are clearly documented in the descriptions of the various Premium Account service levels in the account section of the Website and Apps.

        + +

        Changes to Pricing and Features

        +

        We may occasionally need to change the features of our Premium Accounts and/or change the prices charged for these accounts. In the event of any increase in the price or material reduction in the features of any Premium Account to which you subscribe, we will communicate these change(s) to you at least six (6) weeks in advance and the changes will only take effect with respect to any subsequent renewal of your subscription.

        + + + + + diff --git a/web/app/views/corps/privacy.html.erb b/web/app/views/corps/privacy.html.erb new file mode 100644 index 000000000..fc9bc8a5f --- /dev/null +++ b/web/app/views/corps/privacy.html.erb @@ -0,0 +1,412 @@ +<% provide(:title, 'Privacy') %> +<% provide(:purpose, 'privacy') %> + +

        Privacy Policy

        + +

        Welcome to JamKazam™, a service provided by JamKazam, Inc. (“JamKazam”, “we” “our”, or “us”).

        + +

        Your privacy is important to us. This Privacy Policy explains how we collect, store, use and disclose your personal + information when you use jamkazam.com and m.jamkazam.com (together, the “Website”), our mobile and desktop apps (the + "Apps") and all related sites, players, widgets, tools, apps, data, software, APIs and other services provided by + JamKazam (the “Services”).

        + +

        This Privacy Policy explains the following:

        + + + +

        + By using the Website, the Apps or any of the Services, and in particular by registering a JamKazam account, you are + consenting to the use of your personal information in the manner set out in this Privacy Policy. +

        + +

        + For your convenience, information relating to our use of cookies and similar technologies is set out in a + separate <%= link_to "Cookies Policy", corp_cookie_policy_path %>. The Cookies Policy forms part of this Privacy Policy, + and whenever we refer to the Privacy Policy, we are referring to the Privacy Policy incorporating the Cookies Policy. +

        + +

        + Please take some time to read this Privacy Policy (including the Cookies Policy), and make sure that you find our use + and disclosure of your personal information to be acceptable. +

        + +

        + If you do not agree to any of the provisions of this Privacy Policy, you should not use the Website, the Apps or any + of the Services. If you have any questions or concerns, you can contact us + at privacy@jamkazam.com. +

        + +

        + Please note that this Privacy Policy only applies to the Website, the Apps and the Services (together, the + “Platform”). When using the Platform, you may find links to other websites, apps and services, or tools that enable + you to share information with other websites, apps and services. JamKazam is not responsible for the privacy practices + of these other websites, apps and services and we recommend that you review the privacy policies of each of these + websites, apps or services before connecting your JamKazam account or sharing any personal information. +

        + +

        Our Principles

        +

        + We have a pretty simple approach to data protection and privacy, which can be summarized as follows: +

        + +

        You should know exactly what we do with your data

        +

        + This Privacy Policy is designed to give you full transparency regarding our data protection practices. If there’s + anything that’s not clear from this Privacy Policy, please feel free to email us + at privacy@jamkazam.com. +

        + +

        You should have full control over your data

        +

        + We’ve designed the Platform to give you control over the information you publish and share using JamKazam and other + sites and services to which you connect your JamKazam account. Please take full advantage of these tools and make sure + you only share what you want to share. +

        + +

        Information we collect about you

        +

        + We collect personal information about you from various sources: +

        + +

        Information provided by you

        +

        + You don’t need to provide us with any personal information in order to visit the Website. However, certain Services do + require that you register for a JamKazam account and, by doing so, you will provide us with certain personal + information: +

        + +
          +
        • Essential Information: When you register any kind of JamKazam account, you will need to provide your name, email + address, a password, and if you are a musician, information on the instruments you can play. In addition, if you + register a premium account, you will also need to provide your address and payment verification information. +
        • + +
        • Profile Information: You may choose, at your discretion, to provide additional information for your public profile + on JamKazam – for example: +
            +
          • the city and country in which you live
          • +
          • a profile picture or avatar
          • +
          • details of your other websites and social media profiles, including links to those websites and profiles
          • +
          + +

          None of this profile information is mandatory, and any information you do provide may be deleted, edited, changed + or amended by you at any time. For more information, see the Choice and Control section + below.

          +
        • +
        • Information from Correspondence: You will provide certain personal information if you contact us by email, use any + of the webforms on the Website, or contact us by mail, fax or other offline means. +
        • +
        • Survey Information: If you participate in any survey, you will provide certain personal information as part of + your response, unless you respond anonymously. +
        • +
        • Information that you post: You may provide personal information when you create or participate in sessions, make + recordings, live broadcast sessions, post comments, participate on the support forums, or contribute to community + discussions. +
        • +
        + + +

        Information we collect automatically

        +

        + There is certain information that we collect automatically as the result of your use of the Platform, or through the + use of web analytics services as described in our <%= link_to "Cookies Policy", corp_cookie_policy_path %>. This + information includes: +

        + +
          +
        • the Internet Protocol (IP) address of the device from which you access the Platform (this can sometimes be used to + derive the country or city from which you are accessing the Platform, and the ISP you are using to access the + Platform) +
        • +
        • the site that you visited immediately prior to visiting the Website
        • +
        • the specific actions that you take on the Platform, including but not limited to the pages that you visit, the + audio gear you set up, sessions you create or participate in, recordings you make, connecting your Facebook or + Google+ account, connecting as a friend with another user, following or unfollowing another user, posting a comment, + or performing a search +
        • +
        • any search terms that you may enter on the Platform
        • +
        • the time, frequency and duration of your visits to the Platform
        • +
        • your browser type and operating system
        • +
        • the nature of the device from which you are accessing the Platform, for example, whether you are accessing the + Platform from a personal computer or from a mobile device +
        • +
        • information collected through cookies and similar technology, as described in + our <%= link_to "Cookies Policy", corp_cookie_policy_path %>
        • +
        • information regarding your interaction with email messages, for example, whether you opened, clicked on, or + forwarded the email message +
        • +
        + +

        Information from Other Sources

        +
        +

        Facebook

        +

        JamKazam allows you to sign up and log in using your Facebook account. If you sign up using Facebook, Facebook will + ask your permission to share certain information from your Facebook account with JamKazam. This includes your first + name, last name, gender, general location, a link to your Facebook profile, your timezone, birthday, profile picture, + your "likes" and your list of friends.

        +

        This information is collected by Facebook and is provided to us under the terms + of Facebook’s privacy policy. You can control the + information that we receive from Facebook using the privacy settings in your Facebook account.

        +

        If you sign up to JamKazam using Facebook, your JamKazam account will be connected automatically to your Facebook + account, and information regarding your activity on JamKazam, specifically any new sessions you create or join, any + recordings you make, and any new favorites you save will be shared with Facebook and will be published in your + timeline and your Facebook friends’ newsfeeds. If you do not wish to share your JamKazam activity with your Facebook + friends, you can control this from your account settings on the Website and Apps – for more information, see + the Choice and Control section below. If you signed up using Facebook by mistake, + you can delete your account by emailing a request to do so to us + at support@jamkazam.com.

        + +

        How we use your personal information

        +

        We use the information that we collect about you for the following purposes:

        +
          +
        • Your email address and password are used to identify you when you log into the Platform.
        • +
        • Any additional information that you provide as part of your public profile, such as your real name, and links to + your website and other social media profiles (but not your email address), will be published on your profile page. + This information will be publicly accessible and may be viewed by anyone accessing the Website, or using our API or + other Services. Please bear this in mind when considering whether to provide any additional information about + yourself. +
        • +
        • If you subscribe to a premium account, your name, address and payment verification information will be used to + process your account subscription and to collect your payment. All payment verification information is transmitted + using industry-standard SSL (Secure Socket Layer) encryption. JamKazam also complies with the Payment Card Industry + Security Standards. +
        • +
        • Your email address will be used to send you service updates and notifications regarding your account, and + newsletters, marketing messages and certain email notifications from JamKazam. +
        • +
        • If you are a Facebook user, and one of your Facebook friends registers for a JamKazam account, we will suggest to + your Facebook friend that you might be someone they may wish to connect with as a friend or follow on JamKazam. +
        • +
        • If you are a Facebook user and sign up using Facebook, we may also use the information we receive about you from + Facebook to suggest users that you may wish to connect with as a friend or follow on JamKazam. It will be up to you + if you choose to connect with or follow these users and you can disconnect or unfollow them at any time. +
        • +
        + +

        + Your personal information is also used for the following general purposes: +

        + +
          +
        • To operate and maintain your JamKazam account, and to provide you with access to the Website and use of the Apps + and any Services that you may request from time to time. +
        • +
        • To identify you as a musician participant in sessions and recordings in which you participate, the comments that + you post and/or the other contributions that you make to the Platform. +
        • +
        • To seek your participation in surveys, and to conduct and analyze the results of those surveys if you choose to + participate. +
        • +
        • To provide you with technical support.
        • +
        • To provide other users with data regarding users who are listening to and downloading their sessions and + recordings. +
        • +
        • To analyze the use of the Platform, and the people visiting the Website and using the Apps and Services, in order + to improve JamKazam. +
        • +
        • To respond to you about any comment or inquiry you have submitted.
        • +
        • To customize your use of the Platform and/or the content of any email newsletter or other material that we may + send to you from time to time. +
        • +
        • To prevent or take action against activities that are, or may be, in breach of + our <%= link_to "Terms of Service", corp_terms_path %> or applicable law. +
        • +
        • For other purposes, provided we disclose this to you at the relevant time, and provided that you agree to the + proposed use of your personal information. +
        • +
        + +

        Sharing of your personal information

        +

        We will not share your personal information with any third party, except as described in this Privacy Policy + (including our <%= link_to "Cookies Policy", corp_cookie_policy_path %>). There are circumstances where we may need to + share some of the personal information we collect about you or which you provide to us - these circumstances are as + follows:

        +
          +
        • Other Users: Any personal information in your public profile (other than your email address) will be accessible by + other users of the Platform, who may view your profile information, listen to and comment on any of your public + sessions and recordings, add themselves to your list of followers, and send you messages. If you listen to any + sessions or recordings of a premium user, the fact that you have listened will be shared with that premium user, so + that they can track the popularity of their sessions and recordings. Similarly, if you comment on any sessions or + recordings on the Platform, your comments will be available to all users of the Platform. +
        • +
        • With your consent: We will disclose your personal information if you have explicitly agreed that we may do so. We + will make this clear to you at the point at which we collect your information. +
        • +
        • Service Providers: We use certain reputable third parties, some of whom may be located outside of the United + States, to provide us with certain specialized services related to the Platform. These third parties will have + access to certain information about you, but only where this is necessary in order for those third parties to + provide their services to us. Where we transfer personal information to these third parties, we ask and require + these third parties to implement appropriate organizational and technical security measures to protect against + unauthorized disclosure of personal information, and only to process personal information in accordance with our + instructions and to the extent necessary to provide their services to us. +
        • +
        • Users of our API: JamKazam has an open API, which enables application developers to integrate elements of the + Platform into exciting new apps. Some of these developers may be located outside of the United States. Any personal + information that you choose to make public on the Platform, including your public profile information and any public + sessions and recordings, may be accessible to these developers. Please bear this in mind when deciding what + information to publish on your profile. For information on how to limit the information that is available over the + API, please see the Choice and Control section below. +
        • +
        • Other networks and connected apps: If you choose to connect your JamKazam account to other apps or social networks + such as Facebook, Google+, or Twitter, you will be sharing certain information with these apps or networks. In the + case of Facebook, Google+, or Twitter, any new public sessions or recordings that you participate in on JamKazam or + any new favorites you save will be shared to those networks and will be visible to your followers and friends on + those networks. JamKazam is not responsible for these other apps or networks, so please make sure you only connect + your account with apps or services that you trust. For information on how to control the information that your + share, and how to disconnect your account from other networks or apps, please see the Choice + and Control section below. +
        • +
        • As aggregated data: We may aggregate your personal data with similar data relating to other users of the Platform + in order to create statistical information regarding the Platform and its use, which we may then share with third + parties or make publicly available. However, none of this information would include any email address or other + contact information, or anything that could be used to identify you individually, either online or in real life. +
        • +
        • If required by law: We will disclose your personal information if we believe in good faith that we are permitted + or required to do so by law, including in response to a court order, subpoena or other legal demand or request. +
        • +
        • To protect our interests: We may disclose your personal information if we feel this is necessary in order to + protect or defend our legitimate rights and interests, or those of our users, employees, directors or shareholders, + and/or to ensure the safety and security of the Platform and/or the JamKazam community. +
        • +
        • In the context of a business transfer: We may transfer your personal information to any person or company that + acquires all or substantially all of the assets or business of JamKazam, or on a merger of our business, or in the + event of our insolvency. +
        • +
        + +

        Cookies and similar technology

        +

        As is common Internet practice, we use cookies and other standard Internet technologies to help us improve the + Website and the Apps and Services we provide. We have included information about our use of cookies and similar + technology in a separate <%= link_to "Cookies Policy", corp_cookie_policy_path %> that forms part of this Privacy Policy. + The Cookies Policy also includes information about how you can block or disable third party cookies (which we only use + for the purposes of analyzing the use of our Platform and optimizing the content on our Platform), and how to opt out + of other technologies such as bug reporting within our mobile apps.

        +

        We have done our best to provide you with as much information as possible about our use of cookies and similar + technology. If you choose to use the Platform without blocking or disabling these cookies or opting out of other + technologies as described in our <%= link_to "Cookies Policy", corp_cookie_policy_path %>, you will indicate your consent + to our use of these technologies and to our use (in accordance with this Privacy Policy and our Cookies Policy) of any + personal information that we collect using these technologies.

        + +

        Choice and Control

        +

        We do our best to give you as much choice as possible regarding the amount of personal information you provide to us, + and the control you have over that information.

        +

        It is not necessary for you to provide us with any information in order to visit the Website, although certain + information will be collected automatically by virtue of your visit (as described above). However, if you do decide to + register a JamKazam account, you can control your personal information in the following ways.

        + +

        Sharing with other users

        +

        JamKazam is a very social place, but if you would prefer to be more anonymous, simply enter less information in your + public profile. You can update your public profile data at any time by clicking the Profile tile from the home screen + of the Website or App.

        +

        If any of your sessions or recordings contain personal or sensitive information, you can control who has access to + these sessions and recordings by setting these things to “private”, rather than “public”. When private, only those who + participate in the session or recording will have access to it. Public settings allow anyone to be aware of and listen + to your sessions and recordings.

        + +

        Sharing with other apps and networks

        +

        The Integrations section of the Account feature enables you to control the information you share with other networks + and applications. As well as selecting those of your social networks you wish to share to, you can also control access + to other services and applications to which you have connected your JamKazam account.

        +

        If you sign up for JamKazam using your Facebook account, your JamKazam account and your Facebook account will be + connected automatically. Information regarding the public sessions and recordings that you participate in and + recordings you favorite on JamKazam will be shared with the connected Facebook account. Content that you share with + Facebook will appear on your timeline and in your Facebook friends’ newsfeeds. If you do not want this information + shared with Facebook, please change your account settings.

        +

        Please note that this Privacy Policy does not apply to any third party sites or applications, and we cannot control + the activities of those sites or applications. You are advised to read the privacy policies of those sites or + applications before sharing your information with, or connecting your JamKazam account to, any of these third party + sites or applications.

        + +

        Sharing with app developers

        +

        JamKazam has an open API, which allows third party developers to build some really cool apps as an extension of the + Platform. If you would prefer that your sessions and recordings are not made available to third party app developers, + you can disable API access in the Integrations section of your account settings. Please note that your public profile + information will still be accessible, but this does not include anything that this not publicly available on the + Website.

        + +

        Cookies

        +

        Information on how to block or disable cookies is included in our <%= link_to "Cookies Policy", corp_cookie_policy_path %> + . Further information about cookies in general is available at + http://aboutcookies.org

        + +

        Deleting your account

        +

        You can delete your account from the JamKazam Platform by sending an email requesting account deletion from the email + address associated with your account to support@jamkazam.com. + Please bear in mind that, if you delete your account, all data associated with your account, including the sounds that + you have uploaded and the usage data associated with those sounds will no longer be available to you or others. You + are therefore advised to copy or back up all content uploaded to your account before you delete your account.

        +

        Even if you delete your JamKazam account, it is possible that your information may still show up in some internet + search results for a short while afterwards, if the search engine maintains a temporary cache of web pages. Search + engines' caching processes are outside of JamKazam’s control and therefore we cannot be responsible for any + information that remains cached by search engines after that information has been removed from the Platform. Also, be + aware that your data may still exist in some form on JamKazam’s servers, but if it does, it will not be accessible to + you or others, and may not be recoverable in any case.

        +

        Please note that deleting any JamKazam Apps, or any third party apps to which your JamKazam account is connected, + will not delete your JamKazam account. If you wish to delete your account, you will need to do so as described + above.

        + +

        Access to your Data

        +

        As described above, most of the personal information you provide to us can be accessed and updated in the Profile and + Account pages of the JamKazam Website and Apps. If you wish to access, amend or delete any other personal information + we hold about you, or if you have any objection to the processing of any information that we hold about you, please + contact us at privacy@jamkazam.com, or the address provided + below.

        +

        If you ask us to delete your account, we will do so within a commercially reasonable period of time, but we may need + to retain some of your personal information in order to satisfy our legal obligations, or where we have a legitimate + reason for doing so.

        + +

        International data transfers

        +

        JamKazam is based in the United States, and your personal information is collected, stored, used and shared in + accordance with United States laws. However, from time to time, it may be necessary for us to transfer your personal + data outside the United States. You should be aware that privacy laws outside the United States may not be equivalent + to the laws in the U.S., and by using the Platform, you consent to the transfer, storage and processing of your + personal information outside the United States in accordance with this Privacy Policy and applicable law.

        + +

        Children

        +

        JamKazam is not intended for use by children. Anyone under the age of 13 is not permitted to use the Platform and + must not attempt to register an account or submit any personal information to us. We do not knowingly collect any + personal information from any person who is under the age of 13 or allow them to register an account. If it comes to + our attention that we have collected personal data from a person under the age of 13, we will delete this information + as quickly as is commercially reasonable. If you have reason to believe that we may have collected any such data, + please notify us immediately at privacy@jamkazam.com.

        + +

        Changes and Updates to this Privacy Policy

        +

        We may occasionally update this Privacy Policy, and when we do so, we will also revise the Effective Date set out + below. Any changes to our Privacy Policy will always be available here so that JamKazam users are always aware of what + information we gather, and how we might use and share that information. Please be sure to check back here from time to + time to ensure that you are aware of any changes to this Privacy Policy. Any material changes to this Privacy Policy + will be communicated to registered users by a notification to their account and/or by posting a notice of the change + on the Website.

        + +

        Questions?

        +

        If you have questions about this Privacy Policy, want to suggest changes to this Privacy Policy or want to know, what + information we store about you, please contact us by email + at privacy@jamkazam.com.

        + +

        Don’t want to give us your information?

        +

        If you decide that you do not want us to use your personal information in the manner described in the Privacy Policy + (including our <%= link_to "Cookies Policy", corp_cookie_policy_path %>), please do not use the Platform. If you have + already registered an account, you can cancel your account by sending an email from the email address associated with + your account, requesting deletion of your account, to + privacy@jamkazam.com.

        + +
        +

        + Last amended: +
        + 1 August 2013 +

        \ No newline at end of file diff --git a/web/app/views/corps/terms.html.erb b/web/app/views/corps/terms.html.erb new file mode 100644 index 000000000..17c00b04e --- /dev/null +++ b/web/app/views/corps/terms.html.erb @@ -0,0 +1,729 @@ +<% provide(:title, 'Terms of Service') %> +<% provide(:purpose, 'terms') %> + +

        Terms of Service

        +

        Welcome to JamKazam, a service provided by JamKazam, Inc. (“JamKazam”, “we” “our”, or “us”).

        +

        These Terms of Service govern your use of jamkazam.com and m.jamkazam.com (together, the “Website”), our mobile and + desktop apps (our "Apps") and all related players, widgets, tools, applications, data, software, APIs (which may also + be subject to separate API Terms of Service) and other services provided by JamKazam (the “Services”).

        +

        These Terms of Service, together with our Privacy Policy, Cookies Policy, and any other terms specifically referred + to in any of those documents, constitute a legally binding agreement (the “Agreement”) between you and JamKazam in + relation to your use of the Website, Apps and Services (together, the “Platform”).

        +

        These Terms of Service are divided into the following sections:

        + +
          +
        • + Acceptance of Terms of Service + Basically, by using JamKazam you accept our Terms of Service, Privacy Policy, and Cookies Policy and agree to abide + by them. +
        • +
        • + Changes to Terms of Service + This section explains that our Terms of Service may change from time to time. +
        • + +
        • + Description of the Platform + This provides a general description of the Platform, its features and functionality. +
        • +
        • + Your JamKazam Account + This section explains your responsibilities should you choose to register for a JamKazam account. +
        • +
        • + Your Use of the Platform + This section sets out your right to use the Platform, and the conditions that apply to your use of the Platform. +
        • +
        • + Your Content + This section deals with ownership of your content, and includes your agreement not to perform, broadcast, record, or + upload anything that infringes on anyone else’s rights. +
        • +
        • + Grant of License + This section explains how your content will be used on JamKazam and the permissions that you grant by performing, + broadcasting, recording, or uploading your content - for example, the right for other users to listen to your + sessions and recordings. +
        • +
        • + Representations and Warranties + This section includes important promises and guarantees that you give when uploading content to JamKazam - in + particular, your promise that everything you perform, broadcast, record, upload and share is owned by you and won’t + infringe anyone else’s rights. +
        • +
        • + Liability for Content + This section explains that JamKazam is a hosting service and that its users are solely liable for material that they + perform, broadcast, record, or upload to JamKazam. +
        • +
        • + Reporting Infringements + This section explains how to notify us of any content on JamKazam that you believe infringes your copyright or any + other intellectual property right, or that is offensive, abusive, defamatory or otherwise contrary to our Terms of + Service. +
        • +
        • + Third Party Websites and Services + Through JamKazam you may have access to other websites and services. This section explains that these are separate + third party services that are not under the control of JamKazam. +
        • +
        • + Blocking and Removal of Content + This section makes it clear that JamKazam may block or remove content from the Platform. +
        • +
        • + Repeat Infringers + Users who repeatedly infringe third party rights or breach our Terms of Service risk having their JamKazam accounts + suspended or terminated, as explained in this section. +
        • +
        • + Disclaimer + This section explains that JamKazam cannot give any guarantees that the Platform will always be available – + sometimes even very good technology platforms will have a few problems. +
        • +
        • + Limitation of Liability + This section explains some of those things that JamKazam will not be liable for. Please make sure you read and + understand this section. +
        • +
        • + Indemnification + If you use the Platform in a way that results in damage to us, you will need to take responsibility for that. +
        • +
        • + Data Protection, Privacy and Cookies + It is really important to us that you understand how we use your personal information. All information is collected, + stored and used in accordance with our Privacy Policy, so please make sure that you read and understand that policy. + Like most other websites, we also use cookies to help us analyze how people use JamKazam, so that we can keep + improving our service. Our use of cookies is explained in our Cookies Policy. Please note: if you choose not to + disable cookies within your browser, you will be indicating your consent to our use of cookies as described in our + Cookies Policy, so please make sure that you read the policy carefully. +
        • +
        • + Use of JamKazam Widgets + This section includes a few restrictions on how you can use our widgets – basically, don’t try to use our widgets to + create a new music or audio streaming service. +
        • +
        • + Premium Accounts + This section links you to information explaining how to purchase Premium Account subscriptions and how you can + cancel your subscription purchases in certain circumstances. +
        • +
        • + Changes to the Platform, Accounts and Pricing + From time to time, we may need to make some changes to JamKazam. This section explains your rights in this + situation. +
        • +
        • + Termination + This section explains how you can terminate your JamKazam account, and the grounds on which we can terminate your + use of JamKazam. +
        • +
        • + Assignment to Third Parties + This section deals with JamKazam’s right to transfer this agreement to someone else. +
        • +
        • + Severability + This is a standard legal provision, which says that any term that is not valid will be removed from the agreement + without affecting the validity of the rest of the agreement. +
        • +
        • + Entire Agreement + Your use of JamKazam is governed by these Terms of Service, our Privacy Policy, and Cookies Policy. Any changes need + to be made in writing. +
        • +
        • + Third Party Rights + These Terms of Service apply to the relationship between you and JamKazam only. +
        • +
        • + Applicable Law and Jurisdiction + All of our documents are generally governed by the laws of the State of Texas in the United States. +
        • +
        • + Disclosures + This section provides information about JamKazam, including how to contact us. +
        • +
        + +

        Acceptance of Terms of Service

        +

        + Please read these Terms of Service, our <%= link_to "Privacy Policy", corp_privacy_path %> + , and <%= link_to "Cookies Policy", corp_cookie_policy_path %> carefully. If you do not agree to any of + the provisions set out in those documents, you should not use the Website, Apps or any of the Services. By accessing + or using the Platform, registering an account, or by viewing, accessing, streaming, uploading or downloading any + information or content from or to the Platform, you represent and warrant that you have read and understood the Terms + of Service, <%= link_to "Privacy Policy", corp_privacy_path %>, and <%= link_to "Cookies Policy", corp_cookie_policy_path %>, + will abide by them, and that you are either 18 years of age or more, + or you are 13 years of age or more and have your parent(s)’ or legal guardian(s)’ permission to use the Platform. +

        + +

        Changes to Terms of Service

        +

        + We reserve the right to change, alter, replace or otherwise modify these Terms of Service at any time. The date of + last modification is stated at the end of these Terms of Service. It is your responsibility to check this page from + time to time for updates. +

        +

        + When we make any updates to these Terms of Service, we will highlight this fact on the Website. In addition, if you + register an account and these Terms of Service are subsequently changed in any material respect (for example, for + security, legal, or regulatory reasons), we will notify you in advance by sending a message to your JamKazam account + and/or an email to the email address that you have provided to us, and the revised Terms of Service will become + effective six (6) weeks after such notification. You will have no obligation to continue using the Platform following + any such notification, but if you do not terminate your account as described in + the Termination section below during such six (6) week period, your continued use of the + Platform after the end of that six (6) week period will constitute your acceptance of the revised Terms of Service. +

        + +

        Description of the Platform

        +

        The Platform is a hosting service. Registered users of the Platform may submit, upload and post text, photos, + pictures, comments, and other content, data or information, and may perform, stream, broadcast, and record musical + pieces (“Content”), which will be broadcast, stored, displayed and/or played by JamKazam at the direction of such + registered users, and may be shared and distributed by such registered users, and other users of the Platform, using + the tools and features provided as part of the Platform and accessible via the Website, Apps and elsewhere. The + Platform also enables registered users to interact with one another and to contribute to discussions, and enables any + user of the Website, Apps or certain Services (who may or may not be registered users of the Platform) to view, listen + to and share Content uploaded and otherwise made available by registered users.

        +

        We may, from time to time, release new tools and resources on the Website, release new versions of our Apps, or + introduce other services and/or features for the Platform. Any new services and features will be subject to these + Terms of Service as well as any additional terms of service that we may release for those specific services or + features.

        + +

        Your JamKazam Account

        +

        You are not obliged to register to use the Platform. However, access to the Apps and certain Services is only + available to registered users.

        +

        When you register to use the Platform, you will provide us with your name, email address, instruments played, and + will choose a password for your account. You must ensure that the email address that you provide is, and remains, + valid. Your email address and any other information you choose to provide about yourself will be treated in accordance + with our <%= link_to "Privacy Policy", corp_privacy_path %>.

        +

        You are solely responsible for maintaining the confidentiality and security of your password, and you will remain + responsible for all use of your password, and all activity emanating from your account, whether or not such activity + was authorized by you.

        +

        If your password is lost or stolen, or if you believe that your account has been accessed by unauthorized third + parties, you are advised to notify JamKazam in writing, and should change your password at the earliest possible + opportunity.

        +

        We reserve the right to, with or without prior notice, suspend or terminate your account if activities occur on that + account which, in our sole discretion, would or might constitute a violation of these Terms of Service, or an + infringement or violation of the rights of any third party, or of any applicable laws or regulations.

        +

        You may terminate your account at any time as described in the Termination section below. +

        + + +

        Your Use of the Platform

        +

        Subject to your strict compliance with these Terms of Service, JamKazam grants you a limited, personal, + non-exclusive, revocable, non-assignable and non-transferable right and license to use the Platform in order to view + Content uploaded and posted to the Platform, to listen to audio Content streamed from the Platform and to share and + download audio Content using the features of the Platform where the appropriate functionality has been enabled by the + user who uploaded or created the relevant audio Content (the “Uploader”).

        +

        In addition, if you register to use the Platform, and subject to your strict compliance with these Terms of Service, + JamKazam grants you a limited, personal, non-exclusive, revocable, non-assignable and non-transferable right and + license to:

        +

        (i) participate in sessions, make recordings, and submit, upload or post other Content to the Platform strictly as + permitted in accordance with these Terms of Service and any other applicable terms posted on the Website and Apps from + time to time;

        +

        (ii) participate in the community areas and communicate with other members of the JamKazam community strictly in + accordance with these Terms of Service; and

        +

        (iii) use our Apps and other Services provided as part of the Platform strictly as permitted in accordance with these + Terms of Service and any other terms applicable to those Apps or Services from time to time.

        +

        The above licenses are conditional upon your strict compliance with these Terms of Service, including, without + limitation, the following:

        +

        (i) You must not copy, rip or capture, or attempt to copy, rip or capture, any audio Content from the Platform or any + part of the Platform, other than by means of download in circumstances where the relevant Uploader has elected to + permit downloads of the relevant item of Content.

        +

        (ii) You must not adapt, copy, republish, make available or otherwise communicate to the public, display, perform, + transfer, share, distribute or otherwise use or exploit any Content on or from the Platform, except (i) where such + Content is Your Content, or (ii) as permitted under these Terms of Service, and within the parameters set by the + Uploader (for example, under the terms of Creative Commons licenses selected by the Uploader).

        +

        (iii) You must not use any Content (other than Your Content) in any way that is designed to create a separate content + service or that replicates any part of the Platform offering.

        +

        (iv) You must not employ scraping or similar techniques to aggregate, repurpose, republish or otherwise make use of + any Content.

        +

        (v) You must not employ any techniques or make use of any services, automated or otherwise, designed to misrepresent + the popularity of Your Content on the Platform, or to misrepresent your activity on the Platform, including without + limitation by the use of bots, botnets, scripts, apps, plugins, extensions or other automated means to register + accounts, log in, add followers to your account, play Content, follow or unfollow other users, send messages, post + comments, or otherwise to act on your behalf, particularly where such activity occurs in a multiple or repetitive + fashion. You must not offer or promote the availability of any such techniques or services to any other users of the + Platform.

        +

        (vi) You must not alter or remove, attempt to alter or remove, any trademark, copyright or other proprietary or legal + notices contained in, or appearing on, the Platform or any Content appearing on the Platform (other than Your + Content).

        +

        (vii) You must not, and must not permit any third party to, copy or adapt the object code of the Website or any of + the Apps or Services, or reverse engineer, reverse assemble, decompile, modify or attempt to discover any source or + object code of any part of the Platform, or circumvent or attempt to circumvent or copy any copy protection mechanism + or access any rights management information pertaining to Content other than Your Content.

        +

        (viii) You must not use the Platform to perform, record, upload, post, store, transmit, display, copy, distribute, + promote, make available or otherwise communicate to the public:

        +
          +
        • any Content that is offensive, abusive, libellous, defamatory, obscene, racist, sexually explicit, ethnically or + culturally offensive, indecent, that promotes violence, terrorism, or illegal acts, incites hatred on grounds of + race, gender, religion or sexual orientation, or is otherwise objectionable in JamKazam’s reasonable discretion; +
        • +
        • any information, Content or other material that violates, plagiarises, misappropriates or infringes the rights of + third parties including, without limitation, copyright, trademark rights, rights of privacy or publicity, + confidential information or any other right; +
        • +
        • any Content that violates, breaches or is contrary to any law, rule, regulation, court order or is otherwise is + illegal or unlawful in JamKazam’s reasonable opinion; +
        • +
        • any material of any kind that contains any virus, Trojan horse, spyware, adware, malware, bot, time bomb, worm, or + other harmful or malicious component, which or might overburden, impair or disrupt the Platform or servers or + networks forming part of, or connected to, the Platform, or which does or might restrict or inhibit any other user's + use and enjoyment of the Platform; or +
        • +
        • any unsolicited or unauthorized advertising, promotional messages, spam or any other form of solicitation.
        • +
        +

        (ix) You must not commit or engage in, or encourage, induce, solicit or promote, any conduct that would constitute a + criminal offense, give rise to civil liability or otherwise violate any law or regulation.

        +

        (x) You must not rent, sell or lease access to the Platform, or any Content on the Platform, although this shall not + prevent you from including links from Your Content to any legitimate online download store from where any item of Your + Content may be purchased.

        +

        (xi) You must not deliberately impersonate any person or entity or otherwise misrepresent your affiliation with a + person or entity, for example, by registering an account in the name of another person or company, or sending messages + or making comments using the name of another person.

        +

        (xii) You must not stalk, exploit, threaten, abuse or otherwise harass another user, or any JamKazam employee.

        +

        (xiii) You must not use or attempt to use another person's account, password, or other information, unless you have + express permission from that other person.

        +

        (xiv) You must not sell or transfer, or offer to sell or transfer, any JamKazam account to any third party without + the prior written approval of JamKazam.

        +

        (xv) You must not collect or attempt to collect personal data, or any other kind of information about other users, + including without limitation, through spidering or any form of scraping.

        +

        (xvi) You must not violate, circumvent or attempt to violate or circumvent any data security measures employed by + JamKazam or any Uploader; access or attempt to access data or materials which are not intended for your use; log into, + or attempt to log into, a server or account which you are not authorized to access; attempt to scan or test the + vulnerability of JamKazam’s servers, system or network or attempt to breach JamKazam’s data security or authentication + procedures; attempt to interfere with the Website or the Services by any means including, without limitation, hacking + JamKazam’s servers or systems, submitting a virus, overloading, mail-bombing or crashing. Without limitation to any + other rights or remedies of JamKazam under these Terms of Service, JamKazam reserves the right to investigate any + situation that appears to involve any of the above, and may report such matters to, and co-operate with, appropriate + law enforcement authorities in prosecuting any users who have participated in any such violations.

        +

        You agree to comply with the above conditions, and acknowledge and agree that JamKazam has the right, in its sole + discretion, to terminate your account or take such other action as we see fit if you breach any of the above + conditions or any of the other terms of these Terms of Service. This may include taking court action and/or reporting + offending users to the relevant authorities.

        + +

        Your Content

        +

        Any and all audio, text, photos, pictures, graphics, comments, and other content, data or information that you + perform, record, upload, store, transmit, submit, exchange or make available to or via the Platform (hereinafter "Your + Content") is generated, owned and controlled solely by you, and not by JamKazam.

        +

        JamKazam does not claim any ownership rights in Your Content, and you hereby expressly acknowledge and agree that + Your Content remains your sole responsibility.

        +

        Without prejudice to the conditions set forth in Your Use of the Platform you + must not perform, record, upload, store, distribute, send, transmit, display, make available or otherwise communicate + to the public any Content to which you do not hold the necessary rights. In particular, any unauthorized use of + copyright protected material within Your Content (including by way of reproduction, distribution, modification, + adaptation, public display, public performance, preparation of derivative works, making available or otherwise + communicating to the public via the Platform) may constitute an infringement of third party rights and is strictly + prohibited. Any such infringements may result in termination of your access to the Platform as described in + the Repeat Infringers section below, and may also result in civil litigation or + criminal prosecution by or on behalf of the relevant rights holder.

        +

        We may, from time to time, invite or provide you with means to provide feedback regarding the Platform, and in such + circumstances, any feedback you provide will be deemed non-confidential and JamKazam shall have the right, but not the + obligation, to use such feedback on an unrestricted basis.

        + + +

        Grant of License

        +

        By performing, recording, uploading, posting, or otherwise transmitting Your Content to the Platform, you initiate an + automated system to process any such Content, including but not limited to audio, graphic, and textual Content, and + direct JamKazam to store Your Content on our servers, from where you may control and authorize the use, reproduction, + transmission, distribution, public display, public performance, making available and other communication to the public + of Your Content on the Platform and elsewhere using the Services. To the extent it is necessary in order for JamKazam + to provide you with any of the aforementioned hosting services, to undertake any of the tasks set forth in these Terms + of Service and/or to enable your use of the Platform, you hereby grant such licenses to JamKazam on a limited, + worldwide, non-exclusive, royalty-free and fully paid basis.

        +

        By performing, recording, uploading, posting, or otherwise transmitting Your Content to the Platform, you also grant + a limited, worldwide, non-exclusive, royalty-free, fully paid up license to other users of the Platform, and to + operators and users of any other websites, apps and/or platforms to which Your Content has been shared or embedded + using the Services (“Linked Services”), to use, copy, repost, transmit or otherwise distribute, publicly display, + publicly perform, adapt, prepare derivative works of, compile, make available and otherwise communicate to the public, + Your Content utilizing the features of the Platform from time to time, and within the parameters set by you using the + Services. You can limit and restrict the availability of certain of Your Content to other users of the Platform, and + to users of Linked Services, at any time using the settings available in the account features of the Website and Apps, + subject to the provisions of the Disclaimer section below. Notwithstanding the foregoing, + nothing in these Terms of Service grants any rights to any other user of the Platform with respect to any proprietary + name, logo, trademark or service mark uploaded by you as part of Your Content (for example, your profile picture) + (“Marks”), other than the right to reproduce, publicly display, make available and otherwise communicate to the public + those Marks, automatically and without alteration, as part of the act of reposting Content with which you have + associated those Marks.

        +

        The licenses granted in this section are granted separately with respect to each item of Your Content that you upload + to the Platform. Licenses with respect to audio Content, and any images or text within your account, will (subject to + the following paragraph of these Terms of Service) terminate automatically when you remove such Content from your + account. Licenses with respect to comments or other contributions that you make on the Platform will be perpetual and + irrevocable, and will continue notwithstanding any termination of your account. However, notwithstanding the + foregoing, you hereby acknowledge that any of Your Content that was created and stored on the Platform in conjunction + with other users of the Platform, including but not limited to sessions and recordings, may be maintained on the + Platform until and unless all other users who participated in the creation of such Content also take action to remove + such Content from their accounts. (For purposes of clarity, the reason for this is that otherwise one user could, for + example, force the removal of a musical recording made with multiple other users when those other users do not want + the recording removed.)

        +

        Removal of audio Content from your account will automatically result in the disassociation of such Content from your + account, but it will not result in the deletion of the relevant files from JamKazam’s systems and servers until or + unless all other users who participated in the creation of such audio Content also request removal of such Content. + Notwithstanding the foregoing, you hereby acknowledge and agree that once Your Content is distributed to a Linked + Service, JamKazam is not obligated to ensure the deletion of Your Content from any servers or systems operated by the + operators of any Linked Service, or to require that any user of the Platform or any Linked Service deletes any item of + Your Content.

        +

        Any Content other than Your Content is the property of the relevant Uploader, and is or may be subject to copyright, + trademark rights or other intellectual property or proprietary rights. Such Content may not be downloaded, reproduced, + distributed, transmitted, re-uploaded, republished, displayed, sold, licensed, made available or otherwise + communicated to the public or exploited for any purposes except via the features of the Platform from time to time and + within the parameters set by the Uploader on the Platform or with the express written consent of the Uploader. Where + you repost another user’s Content, or include another user’s Content in a set, you acquire no ownership rights + whatsoever in that Content. Subject to the rights expressly granted in this section, all rights in Content are + reserved to the relevant Uploader.

        + +

        Representations and Warranties

        +

        You hereby represent and warrant to JamKazam as follows:

        +

        (i) Your Content, and each and every part thereof, is an original work by you, or you have obtained all rights, + licenses, consents and permissions necessary in order to use, and (if and where relevant) to authorize JamKazam to + use, Your Content pursuant to these Terms of Service, including, without limitation, the right to upload, reproduce, + store, transmit, distribute, share, publicly display, publicly perform, make available and otherwise communicate to + the public Your Content, and each and every part thereof, on, through or via the Website, Apps, any and all Services + and any Linked Services.

        +

        (ii) Your Content and the availability thereof on the Platform does not and will not infringe or violate the rights + of any third party, including, without limitation, any intellectual property rights, performers’ rights, rights of + privacy or publicity, or rights in confidential information.

        +

        (iii) You have obtained any and all necessary consents, permissions and/or releases from any and all persons + appearing in Your Content in order to include their name, voice, performance or likeness in Your Content and to + publish the same on the Platform and via any Linked Services.

        +

        (iv) Your Content, including any comments that you may post on the Website, is not and will not be unlawful, + offensive, abusive, libelous, defamatory, obscene, racist, sexually explicit, ethnically or culturally offensive, + indecent, will not promote violence, terrorism, or illegal acts, or incite hatred on grounds of race, gender, religion + or sexual orientation.

        +

        (v) Your Content does not and will not create any liability on the part of JamKazam, its subsidiaries, affiliates, + successors, and assigns, and their respective employees, agents, directors, officers and/or shareholders.

        +

        JamKazam reserves the right to remove Your Content, suspend or terminate your access to the Platform and/or pursue + all legal remedies if we believe that any of Your Content breaches any of the foregoing representations or warranties, + or otherwise infringes another person's rights or violates any law, rule or regulation.

        + +

        Liability for Content

        +

        You hereby acknowledge and agree that JamKazam (i) stores Content and other information at the direction, request and + with the authorization of its users, (ii) acts merely as a passive conduit and/or host for the performance, recording, + uploading, storage and distribution of such Content, and (iii) plays no active role and gives no assistance in the + presentation or use of the Content. You are solely responsible for all of Your Content that you perform, record, + upload, post or distribute to, on or through the Platform, and to the extent permissible by law, JamKazam excludes all + liability with respect to all Content (including Your Content) and the activities of its users with respect + thereto.

        +

        You hereby acknowledge and agree that JamKazam cannot and does not review the Content created or uploaded by its + users, and neither JamKazam nor its subsidiaries, affiliates, successors, assigns, employees, agents, directors, + officers and shareholders has any obligation, and does not undertake or assume any duty, to monitor the Platform for + Content that is inappropriate, that does or might infringe any third party rights, or has otherwise been performed, + recorded, entered, or uploaded in breach of these Terms of Service or applicable law.

        +

        JamKazam and its subsidiaries, affiliates, successors, assigns, employees, agents, directors, officers and + shareholders hereby exclude, to the fullest extent permitted by law, any and all liability which may arise from any + Content performed, recorded, entered, or uploaded to the Platform by users, including, but not limited to, any claims + for infringement of intellectual property rights, rights of privacy or publicity rights, any claims relating to + publication of defamatory, pornographic, obscene or offensive material, or any claims relating to the completeness, + accuracy, currency or reliability of any information provided by users of the Platform. By using the Platform, you + irrevocably waive the right to assert any claim with respect to any of the foregoing against JamKazam or any of its + subsidiaries, affiliates, successors, assigns, employees, agents, directors, officers or shareholders.

        + + +

        Reporting Infringements

        +

        If you discover any Content on the Platform that you believe infringes your copyright, please report this to us using + the method described below:

        +

        Your notice should be sent to us by email + to copyright@jamkazam.com.

        +

        Please make sure that you include the following information

        +
          +
        • a statement that you have identified Content on JamKazam that infringes your copyright or the copyright of a third + party on whose behalf you are entitled to act; +
        • +
        • a description of the copyright work(s) that you claim have been infringed;
        • +
        • a description of the Content that you claim is infringing and the JamKazam URL(s) where such Content can be + located; +
        • +
        • your full name, address and telephone number, and a valid email address at which you can be reliably and promptly + contacted; +
        • +
        • a statement by you that you have a good faith belief that the disputed use of the material is not authorized by + the copyright owner, its agent, or the law; and +
        • +
        • a statement by you that the information in your notice is accurate and that you are authorized to act on behalf of + the owner of the exclusive right that is allegedly infringed. +
        • +
        +

        In addition, if you wish for your notice to be considered as a notice pursuant to the United States Digital + Millennium Copyright Act 17 U.S.C. §512(c), please also include the following:

        +
          +
        • with respect to your statement that you are authorized to act on behalf of the owner of the exclusive right that + is allegedly infringed, confirmation that such statement is made under penalty of perjury; and +
        • +
        • your electronic or physical signature (which may be a scanned copy).
        • +
        +

        + The foregoing process applies to copyright only. If you discover any Content that you believe to be in violation of + your trademark rights, please report this to us by email at + trademarks@jamkazam.com. In all other cases, if you + discover Content that infringes any or violates any of your other rights, which you believe is defamatory, + pornographic, obscene, racist or otherwise liable to cause widespread offence, or which constitutes impersonation, + harassment, spam or otherwise violates these Terms of Service or applicable law, please + report this to us at legal@jamkazam.com. +

        + + +

        Third Party Websites and Services

        +

        The Platform may provide you with access to third party websites, databases, networks, servers, information, + software, programs, systems, directories, applications, products or services, including without limitation, Linked + Services (hereinafter “External Services”).

        +

        JamKazam does not have or maintain any control over External Services, and is not and cannot be responsible for their + content, operation or use. By linking or otherwise providing access to any External Services, JamKazam does not give + any representation, warranty or endorsement, express or implied, with respect to the legality, accuracy, quality or + authenticity of content, information or services provided by such External Services.

        +

        External Services may have their own Terms of Service and/or privacy policy, and may have different practices and + requirements to those operated by JamKazam with respect to the Platform. You are solely responsible for reviewing any + Terms of Service, privacy policy or other terms governing your use of these External Services, which you use at your + own risk. You are advised to make reasonable inquiries and investigations before entering into any transaction, + financial or otherwise, and whether online or offline, with any third party related to any External Services.

        +

        You are solely responsible for taking the precautions necessary to protect yourself from fraud when using External + Services, and to protect your computer systems from viruses, worms, Trojan horses, and other harmful or destructive + content and material that may be included on or may emanate from any External Services.

        +

        JamKazam disclaims any and all responsibility or liability for any harm resulting from your use of External Services, + and you hereby irrevocably waive any claim against JamKazam with respect to the content or operation of any External + Services.

        + +

        Blocking and Removal of Content

        +

        Notwithstanding the fact that JamKazam has no legal obligation to monitor the Content on the Platform, JamKazam + reserves the right to block, remove or delete any Content at any time, and to limit or restrict access to any Content, + for any reason and without liability, including without limitation, if we have reason to believe that such Content + does or might infringe the rights of any third party, has been uploaded or posted in breach of these Terms of Service + or applicable law, or is otherwise unacceptable to JamKazam.

        +

        Please also note that individual Uploaders have control over the audio Content that they store in their account from + time to time, and may remove any or all audio Content or other Content without notice. You have no right of continued + access to any particular item of Content and JamKazam shall have no liability in the event that you are unable to + access an item of Content due to its removal from the Platform, whether by JamKazam or the relevant Uploader.

        + +

        Repeat Infringers

        +

        JamKazam will suspend or terminate your access to the Platform if JamKazam determines, in its reasonable discretion, + that you have repeatedly breached these Terms of Service.

        +

        If we receive a valid notification from a third party in accordance with our reporting processes or applicable law + that any of Your Content infringes the copyright or other rights of such third party, or if we believe that your + behavior is inappropriate and violates our Terms of Service, we will send you a written warning to this effect. Any + user that receives more than one of these warnings is liable to have their access to the Platform terminated + forthwith.

        +

        We will also suspend or terminate your account without warning if ordered to do so by a court, and/or in other + appropriate circumstances, as determined by JamKazam at its discretion.

        +

        Please note we do not offer refunds to Premium Account holders whose accounts are terminated as a result of repeated + infringement of these Terms of Service.

        + +

        Disclaimer

        +

        THE PLATFORM, INCLUDING, WITHOUT LIMITATION, THE WEBSITE, THE APPS AND ALL CONTENT AND SERVICES ACCESSED THROUGH OR + VIA THE WEBSITE, THE APPS OR OTHERWISE, ARE PROVIDED “AS IS”, “AS AVAILABLE”, AND “WITH ALL FAULTS”.

        +

        WHILE JAMKAZAM USES COMMERCIALLY REASONABLE EFFORTS TO CORRECT ANY ERRORS OR OMISSIONS IN THE PLATFORM AS SOON AS + PRACTICABLE ONCE THEY HAVE BEEN BROUGHT TO OUR ATTENTION, JAMKAZAM MAKES NO PROMISES, GUARANTEES, REPRESENTATIONS OR + WARRANTIES OF ANY KIND WHATSOEVER (EXPRESS OR IMPLIED) REGARDING THE WEBSITE, THE APPS, THE SERVICES OR ANY PART OR + PARTS THEREOF, ANY CONTENT, OR ANY LINKED SERVICES OR OTHER EXTERNAL SERVICES. JAMKAZAM DOES NOT WARRANT THAT YOUR USE + OF THE PLATFORM WILL BE UNINTERRUPTED, TIMELY, SECURE OR ERROR-FREE, THAT DEFECTS WILL BE CORRECTED, OR THAT THE + PLATFORM OR ANY PART OR PARTS THEREOF, THE CONTENT, OR THE SERVERS ON WHICH THE PLATFORM OPERATES ARE OR WILL BE FREE + OF VIRUSES OR OTHER HARMFUL COMPONENTS. JAMKAZAM DOES NOT WARRANT THAT ANY TRANSMISSION OF CONTENT UPLOADED TO THE + PLATFORM WILL BE SECURE OR THAT ANY ELEMENTS OF THE PLATFORM DESIGNED TO PREVENT UNAUTHORISED ACCESS, SHARING OR + DOWNLOAD OF CONTENT WILL BE EFFECTIVE IN ANY AND ALL CASES, AND DOES NOT WARRANT THAT YOUR USE OF THE PLATFORM IS + LAWFUL IN ANY PARTICULAR JURISDICTION.

        +

        JAMKAZAM AND ITS SUBSIDIARIES, AFFILIATES, SUCCESSORS, AND ASSIGNS, AND THEIR RESPECTIVE EMPLOYEES, AGENTS, + DIRECTORS, OFFICERS AND SHAREHOLDERS, SPECIFICALLY DISCLAIM ALL OF THE FOREGOING WARRANTIES AND ANY OTHER WARRANTIES + NOT EXPRESSLY SET OUT HEREIN TO THE FULLEST EXTENT PERMITTED BY LAW, INCLUDING WITHOUT LIMITATION ANY EXPRESS OR + IMPLIED WARRANTIES REGARDING NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.

        +

        WHERE THE LAW OF ANY JURISDICTION LIMITS OR PROHIBITS THE DISCLAIMER OF IMPLIED OR OTHER WARRANTIES AS SET OUT ABOVE, + THE ABOVE DISCLAIMERS SHALL NOT APPLY TO THE EXTENT THAT THE LAW OF SUCH JURISDICTION APPLIES TO THIS AGREEMENT.

        + +

        Limitation of Liability

        +

        IN NO EVENT SHALL JAMKAZAM’S AGGREGATE LIABILITY TO YOU UNDER THIS AGREEMENT EXCEED THE GREATER OF 100 US DOLLARS OR + THE AMOUNTS (IF ANY) PAID BY YOU TO JAMKAZAM DURING THE PREVIOUS TWELVE (12) MONTHS FOR THE SERVICES GIVING RISE TO + THE CLAIM.

        +

        JAMKAZAM AND ITS SUBSIDIARIES, AFFILIATES, SUCCESSORS, AND ASSIGNS, AND THEIR RESPECTIVE EMPLOYEES, AGENTS, + DIRECTORS, OFFICERS AND SHAREHOLDERS, SHALL HAVE NO LIABILITY FOR:

        +
          +
        • +

          ANY LOSS OR DAMAGE ARISING FROM:

          + +

          (A) YOUR INABILITY TO ACCESS OR USE THE PLATFORM OR ANY PART OR PARTS THEREOF, OR TO ACCESS ANY CONTENT OR ANY + EXTERNAL SERVICES VIA THE PLATFORM;

          + +

          (B) ANY CHANGES THAT JAMKAZAM MAY MAKE TO THE PLATFORM OR ANY PART THEREOF, OR ANY TEMPORARY OR PERMANENT + SUSPENSION OR CESSATION OF ACCESS TO THE PLATFORM OR ANY CONTENT IN OR FROM ANY OR ALL TERRITORIES;

          + +

          (C) ANY ACTION TAKEN AGAINST YOU BY THIRD PARTY RIGHTSHOLDERS WITH RESPECT TO ANY ALLEGED INFRINGEMENT OF SUCH + THIRD PARTY’S RIGHTS RELATING TO YOUR CONTENT OR YOUR USE OF THE PLATFORM, OR ANY ACTION TAKEN AS PART OF AN + INVESTIGATION BY JAMKAZAM OR ANY RELEVANT LAW ENFORCEMENT AUTHORITY REGARDING YOUR USE OF THE PLATFORM;

          + +

          (D) ANY ERRORS OR OMISSIONS IN THE PLATFORM’S TECHNICAL OPERATION, OR FROM ANY INACCURACY OR DEFECT IN ANY + CONTENT OR ANY INFORMATION RELATING TO CONTENT;

          + +

          (E) YOUR FAILURE TO PROVIDE JAMKAZAM WITH ACCURATE OR COMPLETE INFORMATION, OR YOUR FAILURE TO KEEP YOUR PASSWORD + SUITABLY CONFIDENTIAL;

          +
        • +
        • +

          ANY LOSS OR DAMAGE TO ANY COMPUTER HARDWARE OR SOFTWARE, ANY LOSS OF DATA (INCLUDING YOUR CONTENT), OR ANY LOSS + OR DAMAGE FROM ANY SECURITY BREACH; AND/OR

        • + +
        • ANY LOSS OF PROFITS, OR ANY LOSS YOU SUFFER WHICH IS NOT A FORESEEABLE CONSEQUENCE OF JAMKAZAM BREACHING THESE + TERMS OF SERVICE. LOSSES ARE FORESEEABLE WHERE THEY COULD BE CONTEMPLATED BY YOU AND JAMKAZAM AT THE TIME YOU AGREE + TO THESE TERMS OF SERVICE, AND THEREFORE DO NOT INCLUDE ANY INDIRECT LOSSES, SUCH AS LOSS OF OPPORTUNITY.

        • +
        +

        ANY CLAIM OR CAUSE OF ACTION ARISING OUT OF OR RELATED TO YOUR USE OF THE PLATFORM MUST BE NOTIFIED TO JAMKAZAM AS + SOON AS POSSIBLE.

        +

        APPLICABLE LAW MAY NOT ALLOW THE LIMITATION OR EXCLUSION OF LIABILITY FOR INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO THE + ABOVE LIMITATIONS OR EXCLUSIONS MAY NOT APPLY TO YOU. IN SUCH CASES, YOU ACKNOWLEDGE AND AGREE THAT SUCH LIMITATIONS + AND EXCLUSIONS REFLECT A REASONABLE AND FAIR ALLOCATION OF RISK BETWEEN YOU AND JAMKAZAM AND ARE FUNDAMENTAL ELEMENTS + OF THE BARGAIN BETWEEN YOU AND JAMKAZAM, AND THAT JAMKAZAM’S LIABILITY WILL BE LIMITED TO THE MAXIMUM EXTENT PERMITTED + BY LAW.

        +

        NOTHING IN THESE TERMS OF SERVICE LIMITS OR EXCLUDES THE LIABILITY OF JAMKAZAM, ITS SUBSIDIARIES, SUCCESSORS, + ASSIGNS, OR THEIR RESPECTIVE EMPLOYEES, AGENTS, DIRECTORS, OFFICERS AND/OR SHAREHOLDERS: (I) FOR ANY DEATH OR PERSONAL + INJURY CAUSED BY ITS OR THEIR NEGLIGENCE, (II) FOR ANY FORM OF FRAUD OR DECEIT, (III) FOR ANY DAMAGES CAUSED WILFULLY + OR BY GROSS NEGLIGENCE, OR (IV) FOR ANY FORM OF LIABILITY WHICH CANNOT BE LIMITED OR EXCLUDED BY LAW.

        + +

        Indemnification

        +

        You hereby agree to indemnify, defend and hold harmless JamKazam, its successors, assigns, affiliates, agents, + directors, officers, employees and shareholders from and against any and all claims, obligations, damages, losses, + expenses, and costs, including reasonable attorneys' fees, resulting from:

        +

        (i) any violation by you of these Terms of Service;

        +

        (ii) any third party claim of infringement of copyright or other intellectual property rights or invasion of privacy + arising from the hosting of Your Content on the Platform, and/or your making available thereof to other users of the + Platform, and/or the actual use of Your Content by other users of the Platform or Linked Services in accordance with + these Terms of Service and the parameters set by you with respect to the distribution and sharing of Your Content;

        +

        (iii) any activity related to your account, be it by you or by any other person accessing your account with or + without your consent unless such activity was caused by the act or default of JamKazam.

        + +

        Data Protection, Privacy and Cookies

        +

        All personal data that you provide to us in connection with your use of the Platform is collected, stored, used and + disclosed by JamKazam in accordance with our  <%= link_to "Privacy Policy", corp_privacy_path %>. In addition, in common + with most online services, we use cookies to help us understand how people are using the Platform, so that we can + continue to improve the service we offer. Our use of cookies, and how to disable cookies, is explained in our Cookies + Policy. By accepting these Terms of Service and using the Platform, you also accept the terms of + the <%= link_to "Privacy Policy", corp_privacy_path %> and our <%= link_to "Cookies Policy", corp_cookie_policy_path %>.

        + +

        Use of JamKazam Widgets

        +

        The Platform includes access to embeddable JamKazam services (“Widgets”) for incorporation into users’ own sites, + third party sites or social media profiles, whether or not a Linked Service. This functionality is provided to enable + Uploaders to put their Content wherever they wish, and to enable other users of the Platform to share and distribute + Content within the parameters set by the Uploader.

        +

        You may not, without the prior written consent of JamKazam, use the Widgets in such a way that you aggregate Content + from the Platform into a separate destination that replicates substantially the offering of the Website, or comprises + a content service of which Content from the Platform forms a material part. Similarly, you may not, without the prior + written consent of JamKazam, use the Widgets to embed Content into any website or other destination dedicated to a + particular artist (except where the relevant Content is Your Content and you are the person or are authorized to + represent the person to whom the site or destination is dedicated), or to a particular genre. You may not use the + Widgets in any way that suggests that JamKazam or any artist, audio creator or other third party endorses or supports + your website, or your use of the Widgets. The foregoing shall apply whether such use is commercial or + non-commercial.

        +

        JamKazam reserves the right to block your use of the Widgets at any time and for any reason in its sole + discretion.

        + +

        Premium Accounts

        +

        Certain features of the Platform are only available to registered users who subscribe for a paid premium account + (“Premium Account”).

        +

        The purchase of a Premium Account subscription is subject to additional terms, which you will + find <%= link_to "here", corp_premium_accounts_path %>. These terms include, among other things, terms relating to payment, + the conclusion and renewal of your subscription contract, your right of cancellation during the first 14 days of your + subscription, and certain technical usage limitations.

        +

        These additional terms are applicable to Premium Account users in addition to these general Terms of Service.

        + +

        Changes to the Platform, Accounts and Pricing

        +

        JamKazam reserves the right at any time and for any reason to suspend, discontinue, terminate or cease providing + access to the Platform or any part thereof, temporarily or permanently, and whether in its entirety or with respect to + individual territories only. In the case of any temporary or permanent suspension, discontinuation, termination or + cessation of access, JamKazam shall use commercially reasonable efforts to notify registered users of such decision in + advance.

        +

        You hereby agree that JamKazam and its subsidiaries, affiliates, successors, assigns, employees, agents, directors, + officers and shareholders shall not be liable to you or to any third party for any changes or modifications to the + Website, Apps and/or any Services that JamKazam may wish to make from time to time, or for any decision to suspend, + discontinue or terminate the Website, Apps, or Services or any part or parts thereof, or your possibility to use or + access the same from or within any territory or territories.

        +

        JamKazam may change the features of any type of account, may withdraw or introduce new features, products or types of + account at any time and for any reason, and may change the prices charged for any of its Premium Accounts from time to + time. In the event of any increase in the price or material reduction in the features of any Premium Account to which + you have subscribed, such change(s) will be communicated to you and will only take effect with respect to any + subsequent renewal of your subscription. In all other cases, where JamKazam proposes to make changes to any type of + account to which you subscribe (Premium Account or otherwise), and these changes are material and to your + disadvantage, JamKazam will notify you of the proposed changes by sending a message to your JamKazam account and/or an + email to the then current email address that we have for your account four (4) weeks in advance or such changes. You + will have no obligation to continue using the Platform following any such notification, but if you do not terminate + your account as described in the Termination section below during such four (4) week + period, your continued use of your account after the end of that four (4) week period will constitute your acceptance + of the changes to your account.

        + +

        Termination

        +

        You may terminate this Agreement at any time by sending notice of such termination by email to + termination@jamkazam.com, by removing all of Your Content + from your account, by deleting your account, and thereafter by ceasing to use the Platform. If you have a Premium + Account and terminate this Agreement before the end of your subscription, we are unable to offer any refund for any + unexpired period of your subscription.

        +

        JamKazam may suspend your access to the Platform and/or terminate this Agreement at any time if (i) you are deemed to + be a Repeat Infringer as described above; (ii) you are in breach of any of the + material provision of these Terms of Service, including without limitation, the provisions of the following sections: Your + Use of the PlatformYour Content, Grant of License, + and Your Representations and Warranties; (iii) JamKazam elects at its + discretion to cease providing access to the Platform in the jurisdiction where you reside or from where you are + attempting to access the Platform, or (iv) in other reasonable circumstances as determined by JamKazam at its + discretion. If you have a Premium Account and your account is suspended or terminated by JamKazam pursuant to (i) or + (ii) above, you will not be entitled to any refund for any unexpired period of your subscription. If your account is + terminated pursuant to (iii) or (iv), refunds may be payable at the reasonable discretion of JamKazam.

        +

        Once your account has been terminated, any and all Content residing in your account, or pertaining to activity from + your account will be irretrievably deleted by JamKazam, except to the extent that we are obliged or permitted to + retain such content, data or information for a certain period of time in accordance with applicable laws and + regulations and/or to protect our legitimate business interests. You are advised to save or back up any material that + you have uploaded to your account before terminating your account, as JamKazam assumes no liability for any material + that is irretrievably deleted following any termination of your account. JamKazam is not able to provide you with any + .csv or other similar file of data relating to activity associated with your account, whether before or after + termination or cancellation. This data is provided and is accessible only for viewing via your account page on the + Website and Apps for as long as your account is active.

        +

        If you access the Platform via any of our Apps or via any third party app connected to your account, deleting that + app will not delete your account. If you wish to delete your account, you will need to do so by sending an email + requesting account deletion to support@jamkazam.com.

        +

        The provisions of these Terms of Service that are intended by their nature to survive the termination or cancellation + of this Agreement will survive the termination of this Agreement, including, but not limited to, those Sections + entitled Your JamKazam AccountYour + ContentGrant of License , Representations + and WarrantiesLiability for ContentDisclaimer, + Limitation of + LiabilityIndemnificationTerminationAssignment to Third + PartiesSeverability, Entire Agreement, + and Applicable Law and Jurisdiction, respectively.

        + +

        Assignment to Third Parties

        +

        JamKazam may assign its rights and (where permissible by law) its obligations under this Agreement, in whole or in + part, to any third party at any time without notice, including without limitation, to any person or entity acquiring + all or substantially all of the assets or business of JamKazam. You may not assign this Agreement or the rights and + duties hereunder, in whole or in part, to any third party without the prior written consent of JamKazam.

        + +

        Severability

        +

        Should one or more provisions of these Terms of Service be found to be unlawful, void or unenforceable, such + provision(s) shall be deemed severable and will not affect the validity and/or enforceability of the remaining + provisions of the Terms of Service, which will remain in full force and effect.

        + +

        Entire Agreement

        +

        These Terms of Service, together with the <%= link_to "Privacy Policy", corp_privacy_path %> +  and <%= link_to "Cookies Policy", corp_cookie_policy_path %>, constitute the entire agreement between you and JamKazam + with respect to your use of the Platform (other than any use of JamKazam’s APIs which may also be subject to separate + API Terms of Service), and supersede any prior agreement between you and JamKazam. Any modifications to this Agreement + must be made in writing.

        + + +

        Third Party Rights

        +

        These Terms of Service are not intended to give rights to anyone except you and JamKazam. This does not affect our + right to transfer our rights or obligations to a third party as described in + the Assignment to Third Parties section.

        + +

        Applicable Law and Jurisdiction

        +

        Except where otherwise required by the mandatory law of the United States or any member state of the European Union, + you agree, and JamKazam agrees, that this Agreement shall be construed in accordance with and governed by the laws of + the State of Texas without regard to choice of law or conflict of laws principles (both domestic and international) + and without regard to the application of that law known as the United Nations Convention on Contracts for the + International Sale of Goods (CISG). Notwithstanding the foregoing, you agree, and JamKazam agrees, that any litigation + regarding this Agreement shall take place in the courts located in Travis County, Texas, and all parties consent to + personal jurisdiction in the federal and state courts located in Travis County, Texas for the resolution of any such + issues or disputes.

        +

        The foregoing provisions of this Applicable Law and Jurisdiction section do not apply to any claim in which JamKazam + seeks equitable relief of any kind. You acknowledge that, in the event of a breach of this Agreement by JamKazam or + any third party, the damage or harm, if any, caused to you will not entitle you to seek injunctive or other equitable + relief against JamKazam, including with respect to Your Content, and your only remedy shall be for monetary damages, + subject to the limitations of liability set forth in these Terms of Service.

        + +

        Disclosures

        +

        The services hereunder are offered by JamKazam, Inc., a Delaware corporation and with its main place of business at + 5813 Lookout Mountain Drive, Austin TX 78731. You may contact us by sending correspondence to the foregoing address or + by emailing us at info@jamkazam.com.

        + +
        +

        Last Amended: 1 August 2013

        diff --git a/web/app/views/errors/jam_argument_error.rabl b/web/app/views/errors/jam_argument_error.rabl new file mode 100644 index 000000000..e54bffffc --- /dev/null +++ b/web/app/views/errors/jam_argument_error.rabl @@ -0,0 +1,7 @@ +object @exception + +attributes :message + +node "type" do + "ArgumentError" +end \ No newline at end of file diff --git a/web/app/views/errors/permission_error.rabl b/web/app/views/errors/permission_error.rabl new file mode 100644 index 000000000..f66bc5c1a --- /dev/null +++ b/web/app/views/errors/permission_error.rabl @@ -0,0 +1,7 @@ +object @exception + +attributes :message + +node "type" do + "PermissionError" +end \ No newline at end of file diff --git a/web/app/views/errors/state_error.rabl b/web/app/views/errors/state_error.rabl new file mode 100644 index 000000000..d41f225fd --- /dev/null +++ b/web/app/views/errors/state_error.rabl @@ -0,0 +1,7 @@ +object @exception + +attributes :message + +node "type" do + "StateError" +end \ No newline at end of file diff --git a/web/app/views/jam_ruby/music_sessions/_music_session.html.erb b/web/app/views/jam_ruby/music_sessions/_music_session.html.erb new file mode 100644 index 000000000..5f2d5a861 --- /dev/null +++ b/web/app/views/jam_ruby/music_sessions/_music_session.html.erb @@ -0,0 +1,7 @@ +
      • + <%= link_to music_session.description, music_session %> + <% if music_session.creator == current_user || current_user.admin %> + | <%= link_to "delete", music_session, method: :delete, + data: { confirm: "You sure?" } %> + <% end %> +
      • \ No newline at end of file diff --git a/web/app/views/jam_ruby/users/_user.html.erb b/web/app/views/jam_ruby/users/_user.html.erb new file mode 100644 index 000000000..25a8cbb95 --- /dev/null +++ b/web/app/views/jam_ruby/users/_user.html.erb @@ -0,0 +1,8 @@ +
      • + <%= gravatar_for user, size: 52 %> + <%= link_to user.name, user %> + <% if current_user.admin? && !current_user?(user) %> + | <%= link_to "delete", user, method: :delete, + data: { confirm: "You sure?" } %> + <% end %> +
      • \ No newline at end of file diff --git a/web/app/views/layouts/_footer.html.erb b/web/app/views/layouts/_footer.html.erb new file mode 100644 index 000000000..8297f4e70 --- /dev/null +++ b/web/app/views/layouts/_footer.html.erb @@ -0,0 +1,9 @@ +
        + +
        diff --git a/web/app/views/layouts/_header.html.erb b/web/app/views/layouts/_header.html.erb new file mode 100644 index 000000000..2c9bb44ef --- /dev/null +++ b/web/app/views/layouts/_header.html.erb @@ -0,0 +1,4 @@ +
        + +

        JamKazam

        +
        diff --git a/web/app/views/layouts/_shim.html.erb b/web/app/views/layouts/_shim.html.erb new file mode 100644 index 000000000..efa9707e7 --- /dev/null +++ b/web/app/views/layouts/_shim.html.erb @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/web/app/views/layouts/application.html.erb b/web/app/views/layouts/application.html.erb new file mode 100644 index 000000000..788a39568 --- /dev/null +++ b/web/app/views/layouts/application.html.erb @@ -0,0 +1,38 @@ + + + + <%= full_title(yield(:title)) %> + + + + <%= stylesheet_link_tag "client/ie", media: "all" %> + <%= stylesheet_link_tag "client/jamkazam", media: "all" %> + <%= stylesheet_link_tag "client/faders", media: "all" %> + <%= stylesheet_link_tag "client/content", media: "all" %> + <%= stylesheet_link_tag "client/header", media: "all" %> + <%= stylesheet_link_tag "client/footer", media: "all" %> + <%= stylesheet_link_tag "client/screen_common", media: "all" %> + <%= stylesheet_link_tag "client/notify", media: "all" %> + <%= stylesheet_link_tag "client/dialog", media: "all" %> + <%= stylesheet_link_tag "client/sidebar", media: "all" %> + <%= stylesheet_link_tag "client/home", media: "all" %> + <%= stylesheet_link_tag "client/findSession", media: "all" %> + <%= stylesheet_link_tag "client/session", media: "all" %> + <%= stylesheet_link_tag "client/search", media: "all" %> + <%= stylesheet_link_tag "client/ftue", media: "all" %> + <%= stylesheet_link_tag "client/createSession", media: "all" %> + <%= include_gon %> + <%= csrf_meta_tags %> + + + <%= render 'layouts/header' %> +
        + <% flash.each do |key, value| %> +
        <%= value %>
        + <% end %> +
        + <%= yield %> + + diff --git a/web/app/views/layouts/client.html.erb b/web/app/views/layouts/client.html.erb new file mode 100644 index 000000000..f6028e0e3 --- /dev/null +++ b/web/app/views/layouts/client.html.erb @@ -0,0 +1,17 @@ + + + + <%= full_title(yield(:title)) %> + + + <%= stylesheet_link_tag "client/client", media: "all" %> + <%= include_gon %> + <%= javascript_include_tag "application" %> + <%= csrf_meta_tags %> + + + <%= yield %> + + diff --git a/web/app/views/layouts/corporate.html.erb b/web/app/views/layouts/corporate.html.erb new file mode 100644 index 000000000..8ea0135ad --- /dev/null +++ b/web/app/views/layouts/corporate.html.erb @@ -0,0 +1,54 @@ + + + + <%= full_title(yield(:title)) %> + + + <%= stylesheet_link_tag "corp/corporate", :media => "all" %> + <%= javascript_include_tag "corp/corporate" %> + <%= csrf_meta_tags %> + + + +
        + + + + + +
        + + + + +
        + + <%= yield %> + + + +
        + + + diff --git a/web/app/views/layouts/landing.erb b/web/app/views/layouts/landing.erb new file mode 100644 index 000000000..dff9ab8a5 --- /dev/null +++ b/web/app/views/layouts/landing.erb @@ -0,0 +1,38 @@ + + + + <%= full_title(yield(:title)) %> + + + + <%= stylesheet_link_tag "landing/landing", media: "all" %> + <%= include_gon %> + <%= csrf_meta_tags %> + + +
        + <%= javascript_include_tag "landing/landing" %> + +
        + <%= link_to root_path do %> + <%= image_tag("header/logo.png", :alt => "JamKazam logo", :size => "247x45") %> + <% end %> +
        + +
        +
        + +
        + <%= yield %> +
        +
        + + + + + + diff --git a/web/app/views/music_sessions/index.html.erb b/web/app/views/music_sessions/index.html.erb new file mode 100644 index 000000000..9099cc92a --- /dev/null +++ b/web/app/views/music_sessions/index.html.erb @@ -0,0 +1,13 @@ +<% provide(:title, 'Music Sessions') %> +

        Music Sessions

        + +<%= will_paginate %> + +
          + <%= render @music_sessions %> +
        + +<%= link_to "Create Music Session", new_music_session_path, + class: "btn btn-large btn-primary" %> + +<%= will_paginate %> diff --git a/web/app/views/music_sessions/new.html.erb b/web/app/views/music_sessions/new.html.erb new file mode 100644 index 000000000..c88ab3195 --- /dev/null +++ b/web/app/views/music_sessions/new.html.erb @@ -0,0 +1,13 @@ +<% provide(:title, 'New Jam Session') %> +

        New Jam Session

        + +
        +
        + <%= form_for(@music_session) do |f| %> + <%= render 'shared/error_messages', object: f.object %> + <%= f.label :description %> + <%= f.text_field :description %> + <%= f.submit "Create new Music Session", class: "btn btn-large btn-primary" %> + <% end %> +
        +
        \ No newline at end of file diff --git a/web/app/views/music_sessions/show.html.erb b/web/app/views/music_sessions/show.html.erb new file mode 100644 index 000000000..d60140bb3 --- /dev/null +++ b/web/app/views/music_sessions/show.html.erb @@ -0,0 +1,24 @@ +<% provide(:title, "Now Playing: #{@music_session.description}") %> +
        + +
        +

        Internal Session Activity

        +
        +

        Wait a moment...

        +
        +
        +
        + +<% content_for :post_scripts do %> + +<% end %> diff --git a/web/app/views/relationships/create.js.erb b/web/app/views/relationships/create.js.erb new file mode 100644 index 000000000..6adae737b --- /dev/null +++ b/web/app/views/relationships/create.js.erb @@ -0,0 +1,2 @@ +$("#follow_form").html("<%= escape_javascript(render('users/unfollow')) %>") +$("#followers").html('<%= @user.followers.count %>') \ No newline at end of file diff --git a/web/app/views/relationships/destroy.js.erb b/web/app/views/relationships/destroy.js.erb new file mode 100644 index 000000000..6382b4aae --- /dev/null +++ b/web/app/views/relationships/destroy.js.erb @@ -0,0 +1,2 @@ +$("#follow_form").html("<%= escape_javascript(render('users/follow')) %>") +$("#followers").html('<%= @user.followers.count %>') \ No newline at end of file diff --git a/web/app/views/sessions/connection_state.html.erb b/web/app/views/sessions/connection_state.html.erb new file mode 100644 index 000000000..f3cfa8783 --- /dev/null +++ b/web/app/views/sessions/connection_state.html.erb @@ -0,0 +1,136 @@ +<% if Rails.env == "test" || Rails.env == "development" %> + +<% end %> diff --git a/web/app/views/sessions/new.html.erb b/web/app/views/sessions/new.html.erb new file mode 100644 index 000000000..f0a266cd8 --- /dev/null +++ b/web/app/views/sessions/new.html.erb @@ -0,0 +1,99 @@ +<% provide(:title, 'Sign in') %> + + + + + + diff --git a/web/app/views/sessions/oauth_complete.erb b/web/app/views/sessions/oauth_complete.erb new file mode 100644 index 000000000..a6c22d587 --- /dev/null +++ b/web/app/views/sessions/oauth_complete.erb @@ -0,0 +1,3 @@ + diff --git a/web/app/views/shared/_error_messages.html.erb b/web/app/views/shared/_error_messages.html.erb new file mode 100644 index 000000000..c0cdbbb85 --- /dev/null +++ b/web/app/views/shared/_error_messages.html.erb @@ -0,0 +1,12 @@ +<% if object.errors.any? %> +
        +
        + The form contains <%= pluralize(object.errors.count, "error") %>. +
        +
          + <% object.errors.full_messages.each do |msg| %> +
        • * <%= msg %>
        • + <% end %> +
        +
        +<% end %> diff --git a/web/app/views/shared/_user_info.html.erb b/web/app/views/shared/_user_info.html.erb new file mode 100644 index 000000000..4d4d3c86e --- /dev/null +++ b/web/app/views/shared/_user_info.html.erb @@ -0,0 +1,12 @@ + + <%= gravatar_for current_user, size: 52 %> + +

        + <%= current_user.name %> +

        + + <%= link_to "view my profile", current_user %> + + + This space not taken. + diff --git a/web/app/views/spikes/facebook_invite.html.erb b/web/app/views/spikes/facebook_invite.html.erb new file mode 100644 index 000000000..9ad848a6a --- /dev/null +++ b/web/app/views/spikes/facebook_invite.html.erb @@ -0,0 +1,233 @@ +<% provide(:title, "Facebook Invitations") %> +<%= javascript_include_tag "jquery" %> +<%= javascript_include_tag "jqfmfs/jquery.facebook.multifriend.select" %> +<%= stylesheet_link_tag "jqfmfs/jquery.facebook.multifriend.select" %> +

        Facebook Invitations

        + + +
        +
        + +
        + + +
        +
        +
        + + + + + diff --git a/web/app/views/static_pages/about.html.erb b/web/app/views/static_pages/about.html.erb new file mode 100644 index 000000000..0a8ce82c7 --- /dev/null +++ b/web/app/views/static_pages/about.html.erb @@ -0,0 +1,5 @@ +<% provide(:title, 'About Us') %> +

        About Us

        +

        + JamKazam is bringing musicians and fans together everywhere. +

        diff --git a/web/app/views/static_pages/contact.html.erb b/web/app/views/static_pages/contact.html.erb new file mode 100644 index 000000000..7cb78ad40 --- /dev/null +++ b/web/app/views/static_pages/contact.html.erb @@ -0,0 +1,6 @@ +<% provide(:title, 'Contact') %> +

        Contact

        +

        + Contact JamKazam at + info@jamkazam.com. +

        diff --git a/web/app/views/static_pages/faders.html.erb b/web/app/views/static_pages/faders.html.erb new file mode 100644 index 000000000..88a41cf8c --- /dev/null +++ b/web/app/views/static_pages/faders.html.erb @@ -0,0 +1,38 @@ +<% provide(:title, 'Fader Tests') %> + + +

        Fader Tests

        + +

        Vertical

        +
        + +

        Horizontal

        +
        + +<%= render "clients/faders" %> + + + + + + diff --git a/web/app/views/static_pages/help.html.erb b/web/app/views/static_pages/help.html.erb new file mode 100644 index 000000000..8745ee990 --- /dev/null +++ b/web/app/views/static_pages/help.html.erb @@ -0,0 +1,5 @@ +<% provide(:title, 'Help') %> +

        Help

        +

        + To get help, wait for this section to provide a link to our help page. +

        diff --git a/web/app/views/static_pages/home.html.erb b/web/app/views/static_pages/home.html.erb new file mode 100644 index 000000000..b034c4aa1 --- /dev/null +++ b/web/app/views/static_pages/home.html.erb @@ -0,0 +1,19 @@ +<% if signed_in? %> +
        + +
        +

        Empty section

        +
        +
        +<% else %> +
        +

        Are you ready to Jam!?

        + + <%= link_to "Sign up now!", signup_path, + class: "btn btn-large btn-primary" %> +
        +<% end %> diff --git a/web/app/views/users/_create_account_form.html.erb b/web/app/views/users/_create_account_form.html.erb new file mode 100644 index 000000000..44d0567b2 --- /dev/null +++ b/web/app/views/users/_create_account_form.html.erb @@ -0,0 +1,117 @@ + + + + diff --git a/web/app/views/users/_instrument_selector.html.erb b/web/app/views/users/_instrument_selector.html.erb new file mode 100644 index 000000000..0b8c1d180 --- /dev/null +++ b/web/app/views/users/_instrument_selector.html.erb @@ -0,0 +1,8 @@ + +<% Instrument.standard_list.each do |instrument| %> + + + + +<% end %> +
        <%= instrument.description %>
        \ No newline at end of file diff --git a/web/app/views/users/_user_location.html.erb b/web/app/views/users/_user_location.html.erb new file mode 100644 index 000000000..e69de29bb diff --git a/web/app/views/users/already_signed_up.html.erb b/web/app/views/users/already_signed_up.html.erb new file mode 100644 index 000000000..6bd614ec1 --- /dev/null +++ b/web/app/views/users/already_signed_up.html.erb @@ -0,0 +1,17 @@ +<% provide(:title, 'Already Signed Up') %> + + + + + \ No newline at end of file diff --git a/web/app/views/users/congratulations_fan.html.erb b/web/app/views/users/congratulations_fan.html.erb new file mode 100644 index 000000000..122e34fb2 --- /dev/null +++ b/web/app/views/users/congratulations_fan.html.erb @@ -0,0 +1,20 @@ +<% provide(:title, 'Congratulations') %> +
        + + +
        +

        congratulations

        +
        + + +
        + + You have successfully registered as a JamKazam fan. +
        + +
        + +
        <%= link_to "PROCEED TO JAMKAZAM SITE", root_path, :class =>"button-orange m0" %>
        +
        + +
        \ No newline at end of file diff --git a/web/app/views/users/congratulations_musician.html.erb b/web/app/views/users/congratulations_musician.html.erb new file mode 100644 index 000000000..0f1146603 --- /dev/null +++ b/web/app/views/users/congratulations_musician.html.erb @@ -0,0 +1,32 @@ +<% provide(:title, 'Congratulations') %> +
        + + +
        +

        congratulations

        +
        + + +
        + + You have successfully registered as a JamKazam musician. + To get started playing with others, use the button below to download the JamKazam software, then click to open the download and follow the on-screen instructions to install the application on your computer.
        + +
        + +
        + +
        +
        + + +
        + + + + + diff --git a/web/app/views/users/edit.html.erb b/web/app/views/users/edit.html.erb new file mode 100644 index 000000000..ecf4ec961 --- /dev/null +++ b/web/app/views/users/edit.html.erb @@ -0,0 +1,40 @@ +<% provide(:title, "Edit user") %> +

        Update your profile

        + +
        +
        + <%= form_for(@user) do |f| %> + <%= render 'shared/error_messages', object: f.object %> + + <%= f.label :first_name, "First Name" %> + <%= f.text_field :first_name %> + + <%= f.label :last_name, "Last Name" %> + <%= f.text_field :last_name %> + + <%= f.label :email %> + <%= f.text_field :email %> + + <%= f.label :password %> + <%= f.password_field :password %> + + <%= f.label :password_confirmation, "Confirm Password" %> + <%= f.password_field :password_confirmation %> + + + <%= f.label :city %> + <%= f.text_field :city %> + + <%= f.label :state %> + <%= f.text_field :state %> + + <%= f.label :country %> + <%= f.text_field :country %> + + <%= f.submit "Save changes", class: "btn btn-large btn-primary" %> + <% end %> + + <%= gravatar_for @user %> + change +
        +
        \ No newline at end of file diff --git a/web/app/views/users/email_sent.html.erb b/web/app/views/users/email_sent.html.erb new file mode 100644 index 000000000..9bda28190 --- /dev/null +++ b/web/app/views/users/email_sent.html.erb @@ -0,0 +1,7 @@ +<% provide(:title, 'Confirmation Email Sent') %> + +
        +
        + +
        +
        \ No newline at end of file diff --git a/web/app/views/users/finalize_update_email.html.erb b/web/app/views/users/finalize_update_email.html.erb new file mode 100644 index 000000000..df7f19506 --- /dev/null +++ b/web/app/views/users/finalize_update_email.html.erb @@ -0,0 +1,49 @@ +<% provide(:title, 'Email Change Confirmation') %> + + + + + + diff --git a/web/app/views/users/index.html.erb b/web/app/views/users/index.html.erb new file mode 100644 index 000000000..4afe4581f --- /dev/null +++ b/web/app/views/users/index.html.erb @@ -0,0 +1,10 @@ +<% provide(:title, 'All users') %> +

        All users

        + +<%= will_paginate %> + +
          + <%= render @users, :template=> 'users/user' %> +
        + +<%= will_paginate %> diff --git a/web/app/views/users/isp.html.erb b/web/app/views/users/isp.html.erb new file mode 100644 index 000000000..8e25edb01 --- /dev/null +++ b/web/app/views/users/isp.html.erb @@ -0,0 +1,13 @@ +<%= content_for(:title) { 'Internet Latency Test' }%> + +

        JamKazam Internet Latency Test: Select your Internet Service Provider

        +

        Java must be installed to run this tool. Please select logo corresponding to your ISP, or if you're not sure, choose "Other". OSX not currently supported.

        +
        + <% @isps.each do |isp_id, isp_data| %> + <% url_jnlp = isp_ping_url(:isp => isp_id, :format => :jnlp, :host => 'www.jamkazam.com', :port => '80') %> +
        + <%= content_tag(:div, "My ISP is #{isp_data[0]}:") %> + <%= link_to(image_tag("isps/#{isp_data[1]}", :alt => isp_data[0]), url_jnlp) %> +
        + <% end %> +
        diff --git a/web/app/views/users/new.html.erb b/web/app/views/users/new.html.erb new file mode 100644 index 000000000..58c57c90d --- /dev/null +++ b/web/app/views/users/new.html.erb @@ -0,0 +1,255 @@ +<% provide(:title, 'Register') %> + + + + + + + + + + + diff --git a/web/app/views/users/request_reset_password.erb b/web/app/views/users/request_reset_password.erb new file mode 100644 index 000000000..c07c1778c --- /dev/null +++ b/web/app/views/users/request_reset_password.erb @@ -0,0 +1,39 @@ +<% provide(:title, "Reset password") %> + diff --git a/web/app/views/users/reset_password_complete.erb b/web/app/views/users/reset_password_complete.erb new file mode 100644 index 000000000..41ceff6f1 --- /dev/null +++ b/web/app/views/users/reset_password_complete.erb @@ -0,0 +1,17 @@ +<% provide(:title, "Reset password") %> + + diff --git a/web/app/views/users/reset_password_token.erb b/web/app/views/users/reset_password_token.erb new file mode 100644 index 000000000..9c9d0a58a --- /dev/null +++ b/web/app/views/users/reset_password_token.erb @@ -0,0 +1,49 @@ +<% provide(:title, "Reset password") %> + + + diff --git a/web/app/views/users/sent_reset_password.erb b/web/app/views/users/sent_reset_password.erb new file mode 100644 index 000000000..cdc2da0d6 --- /dev/null +++ b/web/app/views/users/sent_reset_password.erb @@ -0,0 +1,23 @@ +<% provide(:title, "Reset password") %> + + + diff --git a/web/app/views/users/show.html.erb b/web/app/views/users/show.html.erb new file mode 100644 index 000000000..72cbcfacf --- /dev/null +++ b/web/app/views/users/show.html.erb @@ -0,0 +1,14 @@ +<% provide(:title, @user.name) %> +
        + +
        + This space not taken. +
        +
        diff --git a/web/app/views/users/show_follow.html.erb b/web/app/views/users/show_follow.html.erb new file mode 100644 index 000000000..7635275ab --- /dev/null +++ b/web/app/views/users/show_follow.html.erb @@ -0,0 +1,19 @@ +<% provide(:title, @title) %> +
        + +
        +

        <%= @title %>

        + <% if @users.any? %> +
          + <%= render @users %> +
        + <%= will_paginate %> + <% end %> +
        +
        diff --git a/web/app/views/users/signup_confirm.html.erb b/web/app/views/users/signup_confirm.html.erb new file mode 100644 index 000000000..ead385897 --- /dev/null +++ b/web/app/views/users/signup_confirm.html.erb @@ -0,0 +1,117 @@ +<% provide(:title, 'Signup Confirmation') %> +<%= javascript_include_tag "landing/landing" %> + +<% if @user.nil? %> +

        Signup Confirmation Failure

        + +
        +
        + Unable to confirm registration email +
        +
        +<% else %> + <% # Seth: I'm commenting out all non-required fields for now, because I think we currently have too much %> + + + + + + + <% end %> + + diff --git a/web/build b/web/build new file mode 100755 index 000000000..beef77515 --- /dev/null +++ b/web/build @@ -0,0 +1,115 @@ +#!/bin/bash + +echo "updating dependencies" + +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +# 'target' is the output directory +rm -rf $DIR/target +mkdir $DIR/target +mkdir $DIR/target/deb + +rm -rf vendor/bundle +rm -rf tmp/capybara +rm Gemfile.lock # if we don't want versions to float, pin it in the Gemfile, not count on Gemfile.lock +# put all dependencies into vendor/bundle +bundle install --path vendor/bundle +bundle update + +# clean assets, because they may be lingering from last build +bundle exec rake assets:clean + +if [ "$?" = "0" ]; then + echo "success: updated dependencies" +else + echo "could not update dependencies" + exit 1 +fi + +if [ -z $SKIP_TESTS ]; then + + phantomjs --version + if [ "$?" != "0" ]; then + echo "phantomjs is required to run rspec tests; please install it" + echo "emitting path to debug on build server $PATH" + exit 1 + fi + + echo "running rspec tests" + bundle exec rspec + if [ "$?" = "0" ]; then + echo "success: ran rspec tests" + else + echo "running rspec tests failed." + exit 1 + fi + + + if [ -z $SKIP_KARMA ]; then + echo "running karma tests" + #http://shortforgilbert.com/blog/2011/03/25/headless-jasmine-ci + # TODO starting Xvfb here because we don't do this on start of build server + # If you run it once, it will background/nohup itself, so this is 'lazy' + Xvfb :99 -screen 0 1440x900x16 > /dev/null 2>&1 & + # run jasmine using the virtual screen, and in the test environment to use the jam_web_test db + DISPLAY=":99" karma start spec/javascripts/karma.ci.conf.js + + if [ "$?" = "0" ]; then + echo "success: karma tests completed" + else + echo "running karma tests failed" + exit 1 + fi + fi + + if [ -z "$SKIP_CUCUMBER_TESTS" ]; then + echo "running cucumber tests" +# DISPLAY=":99" bundle exec cucumber + if [ "$?" = "0" ]; then + echo "success: cucumber tests completed" + else + echo "running cucumber tests failed" + exit 1 + fi + fi +fi + +if [ -n "$PACKAGE" ]; then + + if [ -z "$BUILD_NUMBER" ]; then + echo "BUILD NUMBER is not defined" + exit 1 + fi + + cat > lib/jam_web/version.rb << EOF +module JamWeb + VERSION = "0.1.$BUILD_NUMBER" +end +EOF + + + type -P dpkg-architecture > /dev/null + + + if [ "$?" = "0" ]; then + ARCH=`dpkg-architecture -qDEB_HOST_ARCH` + else + echo "WARN: unable to determine architecture." + ARCH=`all` + fi + + + set -e + # cache all gems local, and tell bundle to use local gems only + #bundle install --path vendor/bundle --local + # prepare production acssets + rm -rf $DIR/public/assets + bundle exec rake assets:precompile RAILS_ENV=production + + # create debian using fpm + bundle exec fpm -s dir -t deb -p target/deb/jam-web_0.1.${BUILD_NUMBER}_${ARCH}.deb -n "jam-web" -v "0.1.$BUILD_NUMBER" --prefix /var/lib/jam-web --after-install $DIR/script/package/post-install.sh --before-install $DIR/script/package/pre-install.sh --before-remove $DIR/script/package/pre-uninstall.sh --after-remove $DIR/script/package/post-uninstall.sh Gemfile .bundle config Rakefile script config.ru lib public vendor app + +fi + +echo "build complete" + diff --git a/web/config.ru b/web/config.ru new file mode 100644 index 000000000..550b8227a --- /dev/null +++ b/web/config.ru @@ -0,0 +1,4 @@ +# This file is used by Rack-based servers to start the application. + +require ::File.expand_path('../config/environment', __FILE__) +run SampleApp::Application diff --git a/web/config/application.rb b/web/config/application.rb new file mode 100644 index 000000000..28c019531 --- /dev/null +++ b/web/config/application.rb @@ -0,0 +1,153 @@ +require File.expand_path('../boot', __FILE__) + +# Pick the frameworks you want: +require "active_record/railtie" +require "action_controller/railtie" +require "action_mailer/railtie" +require "active_resource/railtie" +require "sprockets/railtie" + + +# initialize ActiveRecord's db connection +# why? Because user.rb uses validates :acceptance, which needs a connection to the database. if there is better way... +ActiveRecord::Base.establish_connection(YAML::load(File.open('config/database.yml'))[Rails.env]) + +if defined?(Bundler) + # If you precompile assets before deploying to production, use this line + Bundler.require(*Rails.groups(:assets => %w(development test))) + # If you want your assets lazily compiled in production, use this line + # Bundler.require(:default, :assets, Rails.env) + end + + include JamRuby +# require "rails/test_unit/railtie" + + module SampleApp + class Application < Rails::Application + # Settings in config/environments/* take precedence over those specified here. + # Application configuration should go into files in config/initializers + # -- all .rb files in that directory are automatically loaded. + + # Custom directories with classes and modules you want to be autoloadable. + config.autoload_paths += %W(#{config.root}/lib) + + # Only load the plugins named here, in the order given (default is alphabetical). + # :all can be used as a placeholder for all plugins not explicitly named. + # config.plugins = [ :exception_notification, :ssl_requirement, :all ] + + # Activate observers that should always be running. + # config.active_record.observers = :cacher, :garbage_collector, :forum_observer + config.active_record.observers = "JamRuby::InvitedUserObserver", "JamRuby::UserObserver", "JamRuby::FeedbackObserver" + + # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. + # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. + # config.time_zone = 'Central Time (US & Canada)' + + # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. + # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] + # config.i18n.default_locale = :de + + # Configure the default encoding used in templates for Ruby 1.9. + config.encoding = "utf-8" + + # Configure sensitive parameters which will be filtered from the log file. + config.filter_parameters += [:password] + + # Use SQL instead of Active Record's schema dumper when creating the database. + # This is necessary if your schema can't be completely dumped by the schema dumper, + # like if you have constraints or database-specific column types + # config.active_record.schema_format = :sql + + # Enforce whitelist mode for mass assignment. + # This will create an empty whitelist of attributes available for mass-assignment for all models + # in your app. As such, your models will need to explicitly whitelist or blacklist accessible + # parameters by using an attr_accessible or attr_protected declaration. + config.active_record.whitelist_attributes = true + + # Enable the asset pipeline + config.assets.enabled = true + + # Version of your assets, change this if you want to expire all your assets + config.assets.version = '1.0' + + # Add the assets/fonts directory to assets.paths + config.assets.paths << "#{Rails.root}/app/assets/fonts" + + # Precompile additional assets (application.js, application.css, and all non-JS/CSS (i.e., images) are already added) + config.assets.precompile += %w( client/client.css ) + config.assets.precompile += %w( landing/landing.js landing/landing.css ) + config.assets.precompile += %w( corp/corporate.js corp/corporate.css ) + + + # where is rabbitmq? + config.rabbitmq_host = "localhost" + config.rabbitmq_port = 5672 + + # filepicker app configured to use S3 bucket jamkazam-dev + config.filepicker_rails.api_key = "Asx4wh6GSlmpAAzoM0Cunz" + config.filepicker_upload_dir = 'avatars' + config.fp_secret = 'YSES4ABIMJCWDFSLCFJUGEBKSE' + + config.recaptcha_enable = false + + # create one user per real jamkazam employee? + config.bootstrap_dev_users = true + + # websocket-gateway configs + + # Websocket-gateway embedded configs + config.websocket_gateway_enable = false + if Rails.env=='test' + config.websocket_gateway_connect_time_stale = 2 + config.websocket_gateway_connect_time_expire = 5 + else + config.websocket_gateway_connect_time_stale = 6 + config.websocket_gateway_connect_time_expire = 10 + end + config.websocket_gateway_internal_debug = false + config.websocket_gateway_port = 6767 + # Runs the websocket gateway within the web app + config.websocket_gateway_uri = "ws://localhost:#{config.websocket_gateway_port}/websocket" + + # set this to false if you want to disable signups (lock down public user creation) + config.signup_enabled = true + + config.storage_type = :file # or :fog, if using AWS + + # these only used if storage_type = :fog + config.aws_access_key_id = ENV['AWS_KEY'] + config.aws_secret_access_key = ENV['AWS_SECRET'] + config.aws_region = 'us-east-1' + config.aws_bucket = 'jamkazam-dev' + config.aws_bucket_public = 'jamkazam-dev-public' + config.aws_cache = '315576000' + + # facebook keys + config.facebook_key = '468555793186398' + + # google api keys + config.google_client_id = '785931784279-gd0g8on6sc0tuesj7cu763pitaiv2la8.apps.googleusercontent.com' + config.google_secret = 'UwzIcvtErv9c2-GIsNfIo7bA' + + if Rails.env == 'production' + config.desk_url = 'https://jamkazam.desk.com' + config.multipass_callback_url = "http://jamkazam.desk.com/customer/authentication/multipass/callback" + else + config.desk_url = 'https://jamkazam.desk.com' # TODO: replace with test URL + config.multipass_callback_url = "http://jamkazam.desk.com/customer/authentication/multipass/callback" + end + + # perf_data configs + #config.perf_data_bucket_key = "perf_data" + config.perf_data_signed_url_timeout = 3600 * 24 # 1 day + + # crash_dump configs + config.crash_dump_data_signed_url_timeout = 3600 * 24 # 1 day + + # client update killswitch; turn on if client updates are broken and are affecting users + config.check_for_client_updates = true + + # allow hot-key to switch between native and normal client + config.allow_force_native_client = true + end +end diff --git a/web/config/aws.yml b/web/config/aws.yml new file mode 100644 index 000000000..4e5e80726 --- /dev/null +++ b/web/config/aws.yml @@ -0,0 +1,15 @@ +development: + access_key_id: AKIAIFFBNBRQG5YQ5WHA + secret_access_key: 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 + +test: + access_key_id: AKIAIFFBNBRQG5YQ5WHA + secret_access_key: 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 + +production: + access_key_id: AKIAIFFBNBRQG5YQ5WHA + secret_access_key: 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 + +cucumber: &test + <<: *test + diff --git a/web/config/boot.rb b/web/config/boot.rb new file mode 100644 index 000000000..4489e5868 --- /dev/null +++ b/web/config/boot.rb @@ -0,0 +1,6 @@ +require 'rubygems' + +# Set up gems listed in the Gemfile. +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) + +require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE']) diff --git a/web/config/cucumber.yml b/web/config/cucumber.yml new file mode 100644 index 000000000..19b288df9 --- /dev/null +++ b/web/config/cucumber.yml @@ -0,0 +1,8 @@ +<% +rerun = File.file?('rerun.txt') ? IO.read('rerun.txt') : "" +rerun_opts = rerun.to_s.strip.empty? ? "--format #{ENV['CUCUMBER_FORMAT'] || 'progress'} features" : "--format #{ENV['CUCUMBER_FORMAT'] || 'pretty'} #{rerun}" +std_opts = "--format #{ENV['CUCUMBER_FORMAT'] || 'pretty'} --strict --tags ~@wip" +%> +default: <%= std_opts %> features +wip: --tags @wip:3 --wip features +rerun: <%= rerun_opts %> --format rerun --out rerun.txt --strict --tags ~@wip diff --git a/web/config/database.yml b/web/config/database.yml new file mode 100644 index 000000000..644020af3 --- /dev/null +++ b/web/config/database.yml @@ -0,0 +1,37 @@ +# SQLite version 3.x +# gem install sqlite3 +# +# Ensure the SQLite 3 gem is defined in your Gemfile +# gem 'sqlite3' +development: + adapter: postgresql + database: jam + username: postgres + password: postgres + host: localhost + pool: 5 + timeout: 5000 + +# Warning: The database defined as "test" will be erased and +# re-generated from your development database when you run "rake". +# Do not set this db to the same as development or production. +test: &test + adapter: postgresql + database: jam_web_test + username: postgres + password: postgres + host: localhost + pool: 5 + timeout: 5000 + +production: + adapter: postgresql + database: jam + username: postgres + password: postgres + host: localhost + pool: 5 + timeout: 5000 + +cucumber: + <<: *test diff --git a/web/config/environment.rb b/web/config/environment.rb new file mode 100644 index 000000000..58e6ea6d2 --- /dev/null +++ b/web/config/environment.rb @@ -0,0 +1,5 @@ +# Load the rails application +require File.expand_path('../application', __FILE__) + +# Initialize the rails application +SampleApp::Application.initialize! diff --git a/web/config/environments/cucumber.rb b/web/config/environments/cucumber.rb new file mode 100644 index 000000000..70fd60304 --- /dev/null +++ b/web/config/environments/cucumber.rb @@ -0,0 +1,58 @@ +SampleApp::Application.configure do + # Settings specified here will take precedence over those in config/application.rb + + # The test environment is used exclusively to run your application's + # test suite. You never need to work with it otherwise. Remember that + # your test database is "scratch space" for the test suite and is wiped + # and recreated between test runs. Don't rely on the data there! + config.cache_classes = true + + # Configure static asset server for tests with Cache-Control for performance + config.serve_static_assets = true + config.static_cache_control = "public, max-age=3600" + + # Log error messages when you accidentally call methods on nil + config.whiny_nils = true + + # Show full error reports and disable caching + config.consider_all_requests_local = true + config.action_controller.perform_caching = false + + # Raise exceptions instead of rendering exception templates + config.action_dispatch.show_exceptions = false + + # Disable request forgery protection in test environment + config.action_controller.allow_forgery_protection = false + + # Tell Action Mailer not to deliver emails to the real world. + # The :test delivery method accumulates sent emails in the + # ActionMailer::Base.deliveries array. + config.action_mailer.delivery_method = :test + + # Raise exception on mass assignment protection for Active Record models + config.active_record.mass_assignment_sanitizer = :strict + + # Print deprecation notices to the stderr + config.active_support.deprecation = :stderr + + require 'bcrypt' + silence_warnings do + BCrypt::Engine::DEFAULT_COST = BCrypt::Engine::MIN_COST + end + + # For testing omniauth + OmniAuth.config.test_mode = true + + # websocket-gateway configs + + # Runs the websocket gateway within the web app + config.websocket_gateway_uri = "ws://localhost:6777/websocket" + + # Websocket-gateway embedded configs + config.websocket_gateway_enable = true + config.websocket_gateway_connect_time_stale = 30 + config.websocket_gateway_connect_time_expire = 180 + config.websocket_gateway_internal_debug = false + config.websocket_gateway_port = 6777 +end + diff --git a/web/config/environments/development.rb b/web/config/environments/development.rb new file mode 100644 index 000000000..1d15d9669 --- /dev/null +++ b/web/config/environments/development.rb @@ -0,0 +1,51 @@ +SampleApp::Application.configure do + # Settings specified here will take precedence over those in config/application.rb + + # In the development environment your application's code is reloaded on + # every request. This slows down response time but is perfect for development + # since you don't have to restart the web server when you make code changes. + config.cache_classes = false + + # Log error messages when you accidentally call methods on nil. + config.whiny_nils = true + + # Show full error reports and disable caching + config.consider_all_requests_local = true + config.action_controller.perform_caching = false + + # Don't care if the mailer can't send + config.action_mailer.raise_delivery_errors = false + + # Print deprecation notices to the Rails logger + config.active_support.deprecation = :log + + # Only use best-standards-support built into browsers + config.action_dispatch.best_standards_support = :builtin + + # Raise exception on mass assignment protection for Active Record models + config.active_record.mass_assignment_sanitizer = :strict + + # Log the query plan for queries taking more than this (works + # with SQLite, MySQL, and PostgreSQL) + config.active_record.auto_explain_threshold_in_seconds = 0.5 + + # Do not compress assets + config.assets.compress = false + + # Expands the lines which load the assets + config.assets.debug = false + + # Set the logging destination(s) + config.log_to = %w[stdout file] + + # Show the logging configuration on STDOUT + config.show_log_configuration = true + + config.websocket_gateway_enable = true + + TEST_CONNECT_STATES = false + + # this is totally awful and silly; the reason this exists is so that if you upload an artifact + # through jam-admin, then jam-web can point users at it. I think 99% of devs won't even see or care about this config, and 0% of users + config.jam_admin_root_url = 'http://192.168.1.122:3333' +end diff --git a/web/config/environments/production.rb b/web/config/environments/production.rb new file mode 100644 index 000000000..f4328abb5 --- /dev/null +++ b/web/config/environments/production.rb @@ -0,0 +1,85 @@ +SampleApp::Application.configure do + # Settings specified here will take precedence over those in config/application.rb + + # Code is not reloaded between requests + config.cache_classes = true + + # Full error reports are disabled and caching is turned on + config.consider_all_requests_local = false + config.action_controller.perform_caching = true + + # Disable Rails's static asset server (Apache or nginx will already do this) + config.serve_static_assets = false + + # Compress JavaScripts and CSS + config.assets.compress = false # this seems like a bad idea in early development phases + + # Don't fallback to assets pipeline if a precompiled asset is missed + config.assets.compile = false + + # Generate digests for assets URLs + config.assets.digest = true + + config.assets.initialize_on_precompile = false + + # Defaults to Rails.root.join("public/assets") + # config.assets.manifest = YOUR_PATH + + # Specifies the header that your server uses for sending files + # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache + # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx + + # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. + config.force_ssl = false + + # See everything in the log (default is :info) + config.log_level = :debug + + # Prepend all log lines with the following tags + # config.log_tags = [ :subdomain, :uuid ] + + # Use a different logger for distributed setups + # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) + + # Use a different cache store in production + # config.cache_store = :mem_cache_store + + # Enable serving of images, stylesheets, and JavaScripts from an asset server + # config.action_controller.asset_host = "http://assets.example.com" + + + # Disable delivery errors, bad email addresses will be ignored + # config.action_mailer.raise_delivery_errors = false + + # Enable threaded mode + # config.threadsafe! + + # Enable locale fallbacks for I18n (makes lookups for any locale fall back to + # the I18n.default_locale when a translation can not be found) + config.i18n.fallbacks = true + + # Send deprecation notices to registered listeners + config.active_support.deprecation = :notify + + # Log the query plan for queries taking more than this (works + # with SQLite, MySQL, and PostgreSQL) + # config.active_record.auto_explain_threshold_in_seconds = 0.5 + + # Set the logging destination(s) + config.log_to = %w[file] + + # Show the logging configuration on STDOUT + config.show_log_configuration = true + + # run websocket-gateway embedded + config.websocket_gateway_enable = false + + config.aws_bucket = 'jamkazam' + config.aws_bucket_public = 'jamkazam-public' + + # filepicker app configured to use S3 bucket jamkazam + config.filepicker_rails.api_key = "AhUoVoBZSLirP3esyCl7Zz" + config.fp_secret = 'HZBIMSOI5VAQ5LXT4XLG6XA7IE' + + config.allow_force_native_client = false +end diff --git a/web/config/environments/test.rb b/web/config/environments/test.rb new file mode 100644 index 000000000..af5296ce3 --- /dev/null +++ b/web/config/environments/test.rb @@ -0,0 +1,56 @@ +SampleApp::Application.configure do + # Settings specified here will take precedence over those in config/application.rb + + # The test environment is used exclusively to run your application's + # test suite. You never need to work with it otherwise. Remember that + # your test database is "scratch space" for the test suite and is wiped + # and recreated between test runs. Don't rely on the data there! + config.cache_classes = true + + # Configure static asset server for tests with Cache-Control for performance + config.serve_static_assets = true + config.static_cache_control = "public, max-age=3600" + + # Log error messages when you accidentally call methods on nil + config.whiny_nils = true + + # Show full error reports and disable caching + config.consider_all_requests_local = true + config.action_controller.perform_caching = false + + # Raise exceptions instead of rendering exception templates + config.action_dispatch.show_exceptions = false + + # Disable request forgery protection in test environment + config.action_controller.allow_forgery_protection = false + + # Tell Action Mailer not to deliver emails to the real world. + # The :test delivery method accumulates sent emails in the + # ActionMailer::Base.deliveries array. + config.action_mailer.delivery_method = :test + + # Raise exception on mass assignment protection for Active Record models + config.active_record.mass_assignment_sanitizer = :strict + + # Print deprecation notices to the stderr + config.active_support.deprecation = :stderr + + require 'bcrypt' + silence_warnings do + BCrypt::Engine::DEFAULT_COST = BCrypt::Engine::MIN_COST + end + + # For testing omniauth + OmniAuth.config.test_mode = true + + config.websocket_gateway_enable = true + config.websocket_gateway_port = 6769 + config.websocket_gateway_uri = "ws://localhost:#{config.websocket_gateway_port}/websocket" + + # this is totally awful and silly; the reason this exists is so that if you upload an artifact + # through jam-admin, then jam-web can point users at it. I think 99% of devs won't even see or care about this config, and 0% of users + config.jam_admin_root_url = 'http://localhost:3333' + + config.storage_type = :file +end + diff --git a/web/config/initializers/backtrace_silencers.rb b/web/config/initializers/backtrace_silencers.rb new file mode 100644 index 000000000..59385cdf3 --- /dev/null +++ b/web/config/initializers/backtrace_silencers.rb @@ -0,0 +1,7 @@ +# Be sure to restart your server when you modify this file. + +# You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. +# Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } + +# You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. +# Rails.backtrace_cleaner.remove_silencers! diff --git a/web/config/initializers/carrierwave.rb b/web/config/initializers/carrierwave.rb new file mode 100644 index 000000000..52f19fce3 --- /dev/null +++ b/web/config/initializers/carrierwave.rb @@ -0,0 +1,20 @@ +require 'carrierwave' + +CarrierWave.root = Rails.root.join(Rails.public_path).to_s +CarrierWave.base_path = ENV['RAILS_RELATIVE_URL_ROOT'] + +CarrierWave.configure do |config| + config.storage = SampleApp::Application.config.storage_type + config.fog_credentials = { + :provider => 'AWS', + :aws_access_key_id => SampleApp::Application.config.aws_access_key_id, + :aws_secret_access_key => SampleApp::Application.config.aws_secret_access_key, + :region => SampleApp::Application.config.aws_region, + } + config.fog_directory = SampleApp::Application.config.aws_bucket_public # required + config.fog_public = true # optional, defaults to true + config.fog_attributes = {'Cache-Control'=>"max-age=#{SampleApp::Application.config.aws_cache}"} # optional, defaults to {} +end + + +require 'carrierwave/orm/activerecord' diff --git a/web/config/initializers/dev_users.rb b/web/config/initializers/dev_users.rb new file mode 100644 index 000000000..71d95c629 --- /dev/null +++ b/web/config/initializers/dev_users.rb @@ -0,0 +1,17 @@ +if Rails.env == "development" && Rails.application.config.bootstrap_dev_users + + # create one user per employee, +1 for peter2 because he asked for it + User.create_dev_user("Seth", "Call", "seth@jamkazam.com", "jam123", "Austin", "TX", "US", nil, 'http://www.jamkazam.com/assets/avatars/avatar_seth.jpg') + User.create_dev_user("Brian", "Smith", "briansmith@jamkazam.com", "jam123", "Apex", "NC", "US", nil, 'http://www.jamkazam.com/assets/avatars/avatar_brian.jpg') + User.create_dev_user("Mike", "Slemmer", "mike@jamkazam.com", "jam123", "San Jose", "CA", "US", nil, nil) + User.create_dev_user("Peter", "Walker", "peter@jamkazam.com", "jam123", "Austin", "TX", "US", nil, 'http://www.jamkazam.com/assets/avatars/avatar_peter.jpg') + User.create_dev_user("Peter", "Walker", "peter2@jamkazam.com", "jam123", "Austin", "TX", "US", nil, 'http://www.jamkazam.com/assets/avatars/avatar_peter.jpg') + User.create_dev_user("David", "Wilson", "david@jamkazam.com", "jam123", "Austin", "TX", "US", nil, 'http://www.jamkazam.com/assets/avatars/avatar_david.jpg') + User.create_dev_user("Nat", "Meo", "nat@jamkazam.com", "jam123", "Raleigh", "Virginia", "US", nil, nil) + User.create_dev_user("Jonathon", "Wilson", "jonathon@jamkazam.com", "jam123", "Bozeman", "Montana", "US", [{:instrument_id => "keyboard", :proficiency_level => 4, :priority => 1}], 'http://www.jamkazam.com/assets/avatars/avatar_jonathon.jpg') + User.create_dev_user("Jonathan", "Kolyer", "jonathan@jamkazam.com", "jam123", "San Francisco", "CA", "US", nil, nil) + User.create_dev_user("Oswald", "Becca", "os@jamkazam.com", "jam123", "Austin", "TX", "US", nil, nil) + User.create_dev_user("Anthony", "Davis", "anthony@jamkazam.com", "jam123", "Austin", "TX", "US", nil, nil) + +end + diff --git a/web/config/initializers/email.rb b/web/config/initializers/email.rb new file mode 100644 index 000000000..8b6bb118f --- /dev/null +++ b/web/config/initializers/email.rb @@ -0,0 +1,11 @@ +ActionMailer::Base.raise_delivery_errors = true +ActionMailer::Base.delivery_method = Rails.env == "test" ? :test : :smtp +ActionMailer::Base.smtp_settings = { + :address => "smtp.sendgrid.net", + :port => 587, + :domain => "www.jamkazam.com", + :authentication => :plain, + :user_name => "jamkazam", + :password => "jamjamblueberryjam", + :enable_starttls_auto => true +} \ No newline at end of file diff --git a/web/config/initializers/eventmachine.rb b/web/config/initializers/eventmachine.rb new file mode 100644 index 000000000..6912e586b --- /dev/null +++ b/web/config/initializers/eventmachine.rb @@ -0,0 +1,69 @@ +require 'amqp' +require 'jam_ruby' + +# Creates a connection to RabbitMQ. +# On that single connection, a channel is created (which is a way to multiplex multiple queues/topics over the same TCP connection with rabbitmq) +# Then connections to the client_exchange and user_exchange are made, and put into the MQRouter static variables +# If this code completes (which implies that Rails can start to begin with, because this is in an initializer), +# then the Rails app itself is free to send messages over these exchanges +# TODO: reconnect logic if rabbitmq goes down... + +module JamWebEventMachine + + def self.run_em + EM.run do + # this is global because we need to check elsewhere if we are currently connected to amqp before signalling success with some APIs, such as 'create session' + $amqp_connection_manager = AmqpConnectionManager.new(true, 4, :host => Rails.application.config.rabbitmq_host, :port => Rails.application.config.rabbitmq_port) + $amqp_connection_manager.connect do |channel| + + AMQP::Exchange.new(channel, :topic, "clients") do |exchange| + Rails.logger.debug("#{exchange.name} is ready to go") + MQRouter.client_exchange = exchange + end + + AMQP::Exchange.new(channel, :topic, "users") do |exchange| + Rails.logger.debug("#{exchange.name} is ready to go") + MQRouter.user_exchange = exchange + Rails.logger.debug "MQRouter.user_exchange = #{MQRouter.user_exchange}" + end + end + end + end + + def self.die_gracefully_on_signal + Rails.logger.debug("*** die_gracefully_on_signal") + Signal.trap("INT") { EM.stop } + Signal.trap("TERM") { EM.stop } + end + + def self.start + if defined?(PhusionPassenger) + Rails.logger.debug("PhusionPassenger detected") + + PhusionPassenger.on_event(:starting_worker_process) do |forked| + # for passenger, we need to avoid orphaned threads + if forked && EM.reactor_running? + Rails.logger.debug("stopping EventMachine") + EM.stop + end + Rails.logger.debug("starting EventMachine") + Thread.new { + run_em + } + die_gracefully_on_signal + end + elsif defined?(Unicorn) + Rails.logger.debug("Unicorn detected--do nothing at initializer phase") + else + Rails.logger.debug("Development environment detected") + Thread.abort_on_exception = true + + # create a new thread separate from the Rails main thread that EventMachine can run on + Thread.new do + run_em + end + end + end +end + +JamWebEventMachine.start diff --git a/web/config/initializers/inflections.rb b/web/config/initializers/inflections.rb new file mode 100644 index 000000000..5d8d9be23 --- /dev/null +++ b/web/config/initializers/inflections.rb @@ -0,0 +1,15 @@ +# Be sure to restart your server when you modify this file. + +# Add new inflection rules using the following format +# (all these examples are active by default): +# ActiveSupport::Inflector.inflections do |inflect| +# inflect.plural /^(ox)$/i, '\1en' +# inflect.singular /^(ox)en/i, '\1' +# inflect.irregular 'person', 'people' +# inflect.uncountable %w( fish sheep ) +# end +# +# These inflection rules are supported but not enabled by default: +# ActiveSupport::Inflector.inflections do |inflect| +# inflect.acronym 'RESTful' +# end diff --git a/web/config/initializers/mime_types.rb b/web/config/initializers/mime_types.rb new file mode 100644 index 000000000..72aca7e44 --- /dev/null +++ b/web/config/initializers/mime_types.rb @@ -0,0 +1,5 @@ +# Be sure to restart your server when you modify this file. + +# Add new mime types for use in respond_to blocks: +# Mime::Type.register "text/richtext", :rtf +# Mime::Type.register_alias "text/html", :iphone diff --git a/web/config/initializers/omniauth.rb b/web/config/initializers/omniauth.rb new file mode 100644 index 000000000..243e880cf --- /dev/null +++ b/web/config/initializers/omniauth.rb @@ -0,0 +1,5 @@ +Rails.application.config.middleware.use OmniAuth::Builder do + provider :facebook, '468555793186398', '546a5b253972f3e2e8b36d9a3dd5a06e', {name: "facebook", :scope => 'email,user_location'} + provider :google_oauth2, Rails.application.config.google_client_id, Rails.application.config.google_secret, {name: "google_login", approval_prompt: '', scope: 'userinfo.email, userinfo.profile, https://www.google.com/m8/feeds'} +end + diff --git a/web/config/initializers/rabl_init.rb b/web/config/initializers/rabl_init.rb new file mode 100644 index 000000000..cfa6b995c --- /dev/null +++ b/web/config/initializers/rabl_init.rb @@ -0,0 +1,20 @@ +Rabl.configure do |config| + # Commented as these are defaults + # config.cache_all_output = false + # config.cache_sources = Rails.env != 'development' # Defaults to false + # config.cache_engine = Rabl::CacheEngine.new # Defaults to Rails cache + # config.escape_all_output = false + # config.json_engine = nil # Any multi_json engines or a Class with #encode method + # config.msgpack_engine = nil # Defaults to ::MessagePack + # config.bson_engine = nil # Defaults to ::BSON + # config.plist_engine = nil # Defaults to ::Plist::Emit + config.include_json_root = false + # config.include_msgpack_root = true + # config.include_bson_root = true + # config.include_plist_root = true + # config.include_xml_root = false + config.include_child_root = false + # config.enable_json_callbacks = false + # config.xml_options = { :dasherize => true, :skip_types => false } + # config.view_paths = [] +end \ No newline at end of file diff --git a/web/config/initializers/recaptcha.rb b/web/config/initializers/recaptcha.rb new file mode 100644 index 000000000..b7c4fd0f6 --- /dev/null +++ b/web/config/initializers/recaptcha.rb @@ -0,0 +1,24 @@ +# this gem turns recaptcha verification off during tests by default. +# The public key/private keys shown below valid for all jamkazam.com domains +# note that all recaptcha keys work on localhost/127.0.0.1 +# the keys are created at https://www.google.com/recaptcha/admin/create +Recaptcha.configure do |config| + # created using seth@jamkazam.com; can't see way to delegate + config.public_key = '6Let8dgSAAAAAFheKGWrs6iaq_hIlPOZ2f3Bb56B' + config.private_key = '6Let8dgSAAAAAJzFxL9w2QR5auxjk0ol1_xAtOGO' + + if Rails.application.config.recaptcha_enable + # mirrors default behavior, but it's nice to see it without digging in recaptcha gem source + config.skip_verify_env = ['test', 'cucumber'] + else + # disabled in all environments at the moment + config.skip_verify_env = ['test', 'cucumber', 'development', 'production'] + end + # other config options available with this gem: + #nonssl_api_server_url, + #ssl_api_server_url, + #verify_url, + #skip_verify_env, + #proxy, + #handle_timeouts_gracefully +end \ No newline at end of file diff --git a/web/config/initializers/secret_token.rb b/web/config/initializers/secret_token.rb new file mode 100644 index 000000000..6f8d4d750 --- /dev/null +++ b/web/config/initializers/secret_token.rb @@ -0,0 +1,7 @@ +# Be sure to restart your server when you modify this file. + +# Your secret key for verifying the integrity of signed cookies. +# If you change this key, all old signed cookies will become invalid! +# Make sure the secret is at least 30 characters and all random, +# no regular words or you'll be exposed to dictionary attacks. +SampleApp::Application.config.secret_token = 'ced345e01611593c1b783bae98e4e56dbaee787747e92a141565f7c61d0ab2c6f98f7396fb4b178258301e2713816e158461af58c14b695901692f91e72b6200' diff --git a/web/config/initializers/session_store.rb b/web/config/initializers/session_store.rb new file mode 100644 index 000000000..323e244f6 --- /dev/null +++ b/web/config/initializers/session_store.rb @@ -0,0 +1,8 @@ +# Be sure to restart your server when you modify this file. + +SampleApp::Application.config.session_store :cookie_store, key: '_jamkazam_session' + +# Use the database for sessions instead of the cookie-based default, +# which shouldn't be used to store highly confidential information +# (create the session table with "rails generate session_migration") +# SampleApp::Application.config.session_store :active_record_store diff --git a/web/config/initializers/websocket_gateway.rb b/web/config/initializers/websocket_gateway.rb new file mode 100644 index 000000000..ae8dabf47 --- /dev/null +++ b/web/config/initializers/websocket_gateway.rb @@ -0,0 +1,7 @@ +if Rails.application.config.websocket_gateway_enable + + JamWebsockets::Server.new.run :port => Rails.application.config.websocket_gateway_port, + :emwebsocket_debug => Rails.application.config.websocket_gateway_internal_debug, + :connect_time_stale => Rails.application.config.websocket_gateway_connect_time_stale, + :connect_time_expire => Rails.application.config.websocket_gateway_connect_time_expire +end diff --git a/web/config/initializers/wrap_parameters.rb b/web/config/initializers/wrap_parameters.rb new file mode 100644 index 000000000..999df2018 --- /dev/null +++ b/web/config/initializers/wrap_parameters.rb @@ -0,0 +1,14 @@ +# Be sure to restart your server when you modify this file. +# +# This file contains settings for ActionController::ParamsWrapper which +# is enabled by default. + +# Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. +ActiveSupport.on_load(:action_controller) do + wrap_parameters format: [:json] +end + +# Disable root element in JSON by default. +ActiveSupport.on_load(:active_record) do + self.include_root_in_json = false +end diff --git a/web/config/locales/en.yml b/web/config/locales/en.yml new file mode 100644 index 000000000..f2ab8d805 --- /dev/null +++ b/web/config/locales/en.yml @@ -0,0 +1,8 @@ +# Sample localization file for English. Add more files in this directory for other locales. +# See https://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points. + +en: + activerecord: + attributes: + user: + password_digest: "Password" diff --git a/web/config/logging.rb b/web/config/logging.rb new file mode 100644 index 000000000..961981c43 --- /dev/null +++ b/web/config/logging.rb @@ -0,0 +1,115 @@ + +Logging::Rails.configure do |config| + + # Objects will be converted to strings using the :format_as method. + Logging.format_as :inspect + + # The default layout used by the appenders. + layout = Logging.layouts.pattern(:pattern => '[%d] %-5l %c : %m\n') + + # Setup a color scheme called 'bright' than can be used to add color codes + # to the pattern layout. Color schemes should only be used with appenders + # that write to STDOUT or STDERR; inserting terminal color codes into a file + # is generally considered bad form. + # + Logging.color_scheme( 'bright', + :levels => { + :info => :green, + :warn => :yellow, + :error => :red, + :fatal => [:white, :on_red] + }, + :date => :blue, + :logger => :cyan, + :message => :magenta + ) + + # Configure an appender that will write log events to STDOUT. A colorized + # pattern layout is used to format the log events into strings before + # writing. + # + Logging.appenders.stdout( 'stdout', + :auto_flushing => true, + :layout => Logging.layouts.pattern( + :pattern => '[%d] %-5l %c : %m\n', + :color_scheme => 'bright' + ) + ) if config.log_to.include? 'stdout' + + # Configure an appender that will write log events to a file. The file will + # be rolled on a daily basis, and the past 7 rolled files will be kept. + # Older files will be deleted. The default pattern layout is used when + # formatting log events into strings. + # + Logging.appenders.rolling_file( 'file', + :filename => config.paths['log'].first, + :keep => 7, + :age => 'daily', + :truncate => false, + :auto_flushing => true, + :layout => layout + ) if config.log_to.include? 'file' + + # Configure an appender that will send an email for "error" and "fatal" log + # events. All other log events will be ignored. Furthermore, log events will + # be buffered for one minute (or 200 events) before an email is sent. This + # is done to prevent a flood of messages. + # + Logging.appenders.email( 'email', + :from => "server@#{config.action_mailer.smtp_settings[:domain]}", + :to => "developers@#{config.action_mailer.smtp_settings[:domain]}", + :subject => "Rails Error [#{%x(uname -n).strip}]", + :server => config.action_mailer.smtp_settings[:address], + :domain => config.action_mailer.smtp_settings[:domain], + :acct => config.action_mailer.smtp_settings[:user_name], + :passwd => config.action_mailer.smtp_settings[:password], + :authtype => config.action_mailer.smtp_settings[:authentication], + + :auto_flushing => 200, # send an email after 200 messages have been buffered + :flush_period => 60, # send an email after one minute + :level => :error, # only process log events that are "error" or "fatal" + :layout => layout + ) if config.log_to.include? 'email' + + # Setup the root logger with the Rails log level and the desired set of + # appenders. The list of appenders to use should be set in the environment + # specific configuration file. + # + # For example, in a production application you would not want to log to + # STDOUT, but you would want to send an email for "error" and "fatal" + # messages: + # + # => config/environments/production.rb + # + # config.log_to = %w[file email] + # + # In development you would want to log to STDOUT and possibly to a file: + # + # => config/environments/development.rb + # + # config.log_to = %w[stdout file] + # + Logging.logger.root.level = config.log_level + Logging.logger.root.appenders = config.log_to unless config.log_to.empty? + + # Under Phusion Passenger smart spawning, we need to reopen all IO streams + # after workers have forked. + # + # The rolling file appender uses shared file locks to ensure that only one + # process will roll the log file. Each process writing to the file must have + # its own open file descriptor for `flock` to function properly. Reopening + # the file descriptors after forking ensures that each worker has a unique + # file descriptor. + # + + Logging.logger['ActiveSupport::Cache::FileStore'].level = :info + Logging.logger['ActiveSupport::OrderedOptions'].level = :warn + + if defined?(PhusionPassenger) + PhusionPassenger.on_event(:starting_worker_process) do |forked| + Logging.reopen if forked + end + end + +end + diff --git a/web/config/profanity.yml b/web/config/profanity.yml new file mode 100755 index 000000000..35767f0e0 --- /dev/null +++ b/web/config/profanity.yml @@ -0,0 +1,202 @@ +# Note: I got this from here: https://github.com/intridea/profanity_filter/blob/master/config/dictionary.yml +# I doubt that this list can be copyrighted +# the filter currently checks for words that are 3 or more characters. +--- +ass: "*ss" +asses: "*ss*s" +asshole: "*ssh*l*" +assholes: "*ssh*l*s" +bastard: b*st*rd +beastial: b**st**l +beastiality: b**st**l*ty +beastility: b**st*l*ty +bestial: b*st**l +bestiality: b*st**l*ty +bitch: b*tch +bitcher: b*tch*r +bitchers: b*tch*rs +bitches: b*tch*s +bitchin: b*tch*n +bitching: b*tch*ng +blowjob: bl*wj*b +blowjobs: bl*wj*bs +bullshit: b*llsh*t +clit: cl*t +cock: c*ck +cocks: c*cks +cocksuck: c*cks*ck +cocksucked: c*cks*ck*d +cocksucker: c*cks*ck*r +cocksucking: c*cks*ck*ng +cocksucks: c*cks*cks +cum: c*m +cummer: c*mm*r +cumming: c*mm*ng +cums: c*ms +cumshot: c*msh*t +cunillingus: c*n*ll*ng*s +cunnilingus: c*nn*l*ng*s +cunt: c*nt +cuntlick: c*ntl*ck +cuntlicker: c*ntl*ck*r +cuntlicking: c*ntl*ck*ng +cunts: c*nts +cyberfuc: cyb*rf*c +cyberfuck: cyb*rf*ck +cyberfucked: cyb*rf*ck*d +cyberfucker: cyb*rf*ck*r +cyberfuckers: cyb*rf*ck*rs +cyberfucking: cyb*rf*ck*ng +damn: d*mn +dildo: d*ld* +dildos: d*ld*s +dick: d*ck +dink: d*nk +dinks: d*nks +ejaculate: "*j*c*l*t*" +ejaculated: "*j*c*l*t*d" +ejaculates: "*j*c*l*t*s" +ejaculating: "*j*c*l*t*ng" +ejaculatings: "*j*c*l*t*ngs" +ejaculation: "*j*c*l*t**n" +fag: f*g +fagging: f*gg*ng +faggot: f*gg*t +faggs: f*ggs +fagot: f*g*t +fagots: f*g*ts +fags: f*gs +fart: f*rt +farted: f*rt*d +farting: f*rt*ng +fartings: f*rt*ngs +farts: f*rts +farty: f*rty +felatio: f*l*t** +fellatio: f*ll*t** +fingerfuck: f*ng*rf*ck +fingerfucked: f*ng*rf*ck*d +fingerfucker: f*ng*rf*ck*r +fingerfuckers: f*ng*rf*ck*rs +fingerfucking: f*ng*rf*ck*ng +fingerfucks: f*ng*rf*cks +fistfuck: f*stf*ck +fistfucked: f*stf*ck*d +fistfucker: f*stf*ck*r +fistfuckers: f*stf*ck*rs +fistfucking: f*stf*ck*ng +fistfuckings: f*stf*ck*ngs +fistfucks: f*stf*cks +fuck: f*ck +fucked: f*ck*d +fucker: f*ck*r +fuckers: f*ck*rs +fuckin: f*ck*n +fucking: f*ck*ng +fuckings: f*ck*ngs +fuckme: f*ckm* +fucks: f*cks +fuk: f*k +fuks: f*ks +gangbang: g*ngb*ng +gangbanged: g*ngb*ng*d +gangbangs: g*ngb*ngs +gaysex: g*ys*x +goddamn: g*dd*mn +hardcoresex: h*rdc*r*s*x +hell: h*ll +horniest: h*rn**st +horny: h*rny +hotsex: h*ts*x +jism: j*sm +jiz: j*z +jizm: j*zm +kock: k*ck +kondum: k*nd*m +kondums: k*nd*ms +kum: k*m +kumer: k*mm*r +kummer: k*mm*r +kumming: k*mm*ng +kums: k*ms +kunilingus: k*n*l*ng*s +lust: l*st +lusting: l*st*ng +mothafuck: m*th*f*ck +mothafucka: m*th*f*ck* +mothafuckas: m*th*f*ck*s +mothafuckaz: m*th*f*ck*z +mothafucked: m*th*f*ck*d +mothafucker: m*th*f*ck*r +mothafuckers: m*th*f*ck*rs +mothafuckin: m*th*f*ck*n +mothafucking: m*th*f*ck*ng +mothafuckings: m*th*f*ck*ngs +mothafucks: m*th*f*cks +motherfuck: m*th*rf*ck +motherfucked: m*th*rf*ck*d +motherfucker: m*th*rf*ck*r +motherfuckers: m*th*rf*ck*rs +motherfuckin: m*th*rf*ck*n +motherfucking: m*th*rf*ck*ng +motherfuckings: m*th*rf*ck*ngs +motherfucks: m*th*rf*cks +niger: n*gg*r +nigger: n*gg*r +niggers: n*gg*rs +orgasim: "*rg*s*m" +orgasims: "*rg*s*ms" +orgasm: "*rg*sm" +orgasms: "*rg*sms" +phonesex: ph*n*s*x +phuk: ph*k +phuked: ph*k*d +phuking: ph*k*ng +phukked: ph*kk*d +phukking: ph*kk*ng +phuks: ph*ks +phuq: ph*q +pis: p*ss +piss: p*ss +pisser: p*ss*r +pissed: p*ss*d +pisser: p*ss*r +pissers: p*ss*rs +pises: p*ss*s +pisses: p*ss*s +pisin: p*ss*n +pissin: p*ss*n +pising: p*ss*ng +pissing: p*ss*ng +pisof: p*ss*ff +pissoff: p*ss*ff +porn: p*rn +porno: p*rn* +pornography: p*rn*gr*phy +pornos: p*rn*s +prick: pr*ck +pricks: pr*cks +pussies: p*ss**s +pusies: p*ss**s +pussy: p*ssy +pusy: p*ssy +pussys: p*ssys +pusys: p*ssys +shit: sh*t +shited: sh*t*d +shitfull: sh*tf*ll +shiting: sh*t*ng +shitings: sh*t*ngs +shits: sh*ts +shitted: sh*tt*d +shitter: sh*tt*r +shitters: sh*tt*rs +shitting: sh*tt*ng +shittings: sh*tt*ngs +shitty: sh*tty +shity: sh*tty +slut: sl*t +sluts: sl*ts +smut: sm*t +spunk: sp*nk +twat: tw*t diff --git a/web/config/routes.rb b/web/config/routes.rb new file mode 100644 index 000000000..0d6078811 --- /dev/null +++ b/web/config/routes.rb @@ -0,0 +1,279 @@ +SampleApp::Application.routes.draw do + + scope :as => 'jam_ruby' do + resources :users + resources :music_sessions + end + + resources :users + + resources :sessions, only: [:new, :create, :destroy] + + #root to: 'static_pages#home' + root to: 'clients#index' + + # This page is still here, and is under test. Keep a route to it. + match '/oldhome', to: 'static_pages#home' + + # signup, and signup completed, related pages + match '/signup', to: 'users#new', :via => 'get' + match '/signup', to: 'users#create', :via => 'post' + match '/congratulations_musician', to: 'users#congratulations_musician' + match '/congratulations_fan', to: 'users#congratulations_fan' + + match '/signin', to: 'sessions#new' + match '/signout', to: 'sessions#destroy', via: :delete + # oauth + match '/auth/:provider/callback', :to => 'sessions#oauth_callback' + match '/auth/failure', :to => 'sessions#failure' + + + match '/isp', :to => 'users#isp' + match '/isp/ping.jar', :to => redirect('/ping.jar') + match '/isp/ping:isp', :to => 'users#jnlp', :constraints => {:format => :jnlp}, :as => 'isp_ping' + + match '/help', to: 'static_pages#help' + match '/about', to: 'static_pages#about' + match '/contact', to: 'static_pages#contact' + match '/faders', to: 'static_pages#faders' + + match '/client', to: 'clients#index' + + match '/confirm/:signup_token', to: 'users#signup_confirm', as: 'signup_confirm' + + match '/test_connection', to: 'sessions#connection_state', :as => :connection_state + + # spikes + match '/facebook_invite', to: 'spikes#facebook_invite' + match '/gmail_contacts', to: 'spikes#gmail_contacts' + + # password reset + match '/request_reset_password' => 'users#request_reset_password', :via => :get + match '/reset_password' => 'users#reset_password', :via => :post + match '/reset_password_token' => 'users#reset_password_token', :via => :get + match '/reset_password_complete' => 'users#reset_password_complete', :via => :post + + # email update + match '/confirm_email' => 'users#finalize_update_email', :as => 'confirm_email' # NOTE: if you change this, you break outstanding email changes because links in user inboxes are broken + + + scope '/corp' do + match '/about', to: 'corps#about', as: 'corp_about' + match '/about', to: 'corps#about', as: 'corp_about' + match '/contact', to: 'corps#contact', as: 'corp_contact' + match '/help', to: 'corps#help', as: 'corp_help' + match '/media_center', to: 'corps#media_center', as: 'corp_media_center' + match '/news', to: 'corps#news', as: 'corp_news' + match '/privacy', to: 'corps#privacy', as: 'corp_privacy' + match '/terms', to: 'corps#terms', as: 'corp_terms' + match '/cookies_policy',to: 'corps#cookie_policy', as: 'corp_cookie_policy' + match '/premium_accounts',to: 'corps#premium_accounts', as: 'corp_premium_accounts' + end + + scope '/api' do + # music sessions + match '/sessions/:id/participants' => 'api_music_sessions#participant_create', :via => :post + match '/participants/:id' => 'api_music_sessions#participant_show', :via => :get, :as => 'api_session_participant_detail' + match '/participants/:id' => 'api_music_sessions#participant_delete', :via => :delete + match '/sessions/:id' => 'api_music_sessions#show', :via => :get, :as => 'api_session_detail' + match '/sessions/:id' => 'api_music_sessions#update', :via => :put + match '/sessions' => 'api_music_sessions#index', :via => :get + match '/sessions' => 'api_music_sessions#create', :via => :post + match '/sessions/:id/perf' => 'api_music_sessions#perf_upload', :via => :put + + # music session tracks + match '/sessions/:id/tracks' => 'api_music_sessions#track_create', :via => :post + match '/sessions/:id/tracks' => 'api_music_sessions#track_index', :via => :get + match '/sessions/:id/tracks/:track_id' => 'api_music_sessions#track_update', :via => :post + match '/sessions/:id/tracks/:track_id' => 'api_music_sessions#track_show', :via => :get, :as => 'api_session_track_detail' + match '/sessions/:id/tracks/:track_id' => 'api_music_sessions#track_destroy', :via => :delete + + # genres + match '/genres' => 'api_genres#index', :via => :get + + # users + match '/users/isp_scoring' => 'api_users#isp_scoring', :via => :post , :as => 'isp_scoring' + + match '/users' => 'api_users#index', :via => :get + match '/users/:id' => 'api_users#show', :via => :get, :as => 'api_user_detail' + #match '/users' => 'api_users#create', :via => :post + match '/users/:id' => 'api_users#update', :via => :post + match '/users/:id' => 'api_users#delete', :via => :delete + match '/users/confirm/:signup_token' => 'api_users#signup_confirm', :via => :post, :as => 'api_signup_confirmation' + match '/users/complete/:signup_token' => 'api_users#complete', as: 'complete', via: 'post' + + match '/users/:id/set_password' => 'api_users#set_password', :via => :post + + # login/logout + match '/auth_session' => 'api_users#auth_session_create', :via => :post + match '/auth_session' => 'api_users#auth_session_delete', :via => :delete + + # session settings + match '/users/:id/session_settings' => 'api_users#session_settings_show', :via => :get + + # session history + match '/users/:id/session_history' => 'api_users#session_history_index', :via => :get + match '/users/:id/session_history/:session_id/users' => 'api_users#session_user_history_index', :via => :get + + # user bands + match '/users/:id/bands' => 'api_users#band_index', :via => :get + + # user likers + match '/users/:id/likers' => 'api_users#liker_index', :via => :get + + # user likes + match '/users/:id/likes' => 'api_users#like_index', :via => :get, :as => 'api_user_like_index' + match '/users/:id/band_likes' => 'api_users#band_like_index', :via => :get, :as => 'api_band_like_index' + match '/users/:id/likes' => 'api_users#like_create', :via => :post + match '/users/:id/likes' => 'api_users#like_destroy', :via => :delete + + # user followers + match '/users/:id/followers' => 'api_users#follower_index', :via => :get, :as => 'api_user_follower_index' + + # user followings + match '/users/:id/followings' => 'api_users#following_index', :via => :get, :as => 'api_user_following_index' + match '/users/:id/followings/:user_id' => 'api_users#following_show', :via => :get, :as => 'api_following_detail' + + match '/users/:id/band_followings' => 'api_users#band_following_index', :via => :get, :as => 'api_band_following_index' + match '/users/:id/band_followings/:band_id' => 'api_users#band_following_show', :via => :get, :as => 'api_band_following_detail' + + match '/users/:id/followings' => 'api_users#following_create', :via => :post + match '/users/:id/followings' => 'api_users#following_destroy', :via => :delete + + # favorites + match '/users/:id/favorites' => 'api_users#favorite_index', :via => :get, :as => 'api_favorite_index' + match '/users/:id/favorites' => 'api_users#favorite_create', :via => :post + match '/users/:id/favorites/:recording_id' => 'api_users#favorite_destroy', :via => :delete + + # friend requests + match '/users/:id/friend_requests' => 'api_users#friend_request_index', :via => :get + match '/users/:id/friend_requests/:friend_request_id' => 'api_users#friend_request_show', :via => :get, :as => 'api_friend_request_detail' + match '/users/:id/friend_requests' => 'api_users#friend_request_create', :via => :post + match '/users/:id/friend_requests/:friend_request_id' => 'api_users#friend_request_update', :via => :post + + # friends + match '/users/:id/friends' => 'api_users#friend_index', :via => :get + match '/users/:id/friends/:friend_id' => 'api_users#friend_show', :via => :get, :as => 'api_friend_detail' + match '/users/:id/friends/:friend_id' => 'api_users#friend_destroy', :via => :delete + + # notifications + match '/users/:id/notifications' => 'api_users#notification_index', :via => :get + match '/users/:id/notifications/:notification_id' => 'api_users#notification_destroy', :via => :delete + + # user band invitations + match '/users/:id/band_invitations' => 'api_users#band_invitation_index', :via => :get + match '/users/:id/band_invitations/:invitation_id' => 'api_users#band_invitation_show', :via => :get, :as => 'api_user_band_invitation_detail' + match '/users/:id/band_invitations/:invitation_id' => 'api_users#band_invitation_update', :via => :post + + # user account settings + match '/users/:id/update_email' => 'api_users#begin_update_email', :via => :post, :as => 'begin_update_email' + match '/users/update_email/:token' => 'api_users#finalize_update_email', :via => :post, :as => 'finalize_update_email' + + # user profile + match '/users/:id/avatar' => 'api_users#update_avatar', :via => :post + match '/users/:id/avatar' => 'api_users#delete_avatar', :via => :delete + match '/users/:id/filepicker_policy' => 'api_users#generate_filepicker_policy', :via => :get + + # user recordings + # match '/users/:id/recordings' => 'api_users#recording_index', :via => :get + # match '/users/:id/recordings/:recording_id' => 'api_users#recording_show', :via => :get, :as => 'api_recording_detail' + # match '/users/:id/recordings' => 'api_users#recording_create', :via => :post + # match '/users/:id/recordings/:recording_id' => 'api_users#recording_update', :via => :post + # match '/users/:id/recordings/:recording_id' => 'api_users#recording_destroy', :via => :delete + + # bands + match '/bands' => 'api_bands#index', :via => :get + match '/bands/:id' => 'api_bands#show', :via => :get, :as => 'api_band_detail' + match '/bands' => 'api_bands#create', :via => :post + match '/bands/:id' => 'api_bands#update', :via => :post + + # band members (NOT DONE) + match '/bands/:id/musicians' => 'api_bands#musician_index', :via => :get + match '/bands/:id/musicians' => 'api_bands#musician_create', :via => :post + match '/bands/:id/musicians/:user_id' => 'api_bands#musician_destroy', :via => :delete + + # band likers + match '/bands/:id/likers' => 'api_bands#liker_index', :via => :get + + # band followers + match '/bands/:id/followers' => 'api_bands#follower_index', :via => :get + + # band recordings + match '/bands/:id/recordings' => 'api_bands#recording_index', :via => :get + match '/bands/:id/recordings/:recording_id' => 'api_bands#recording_show', :via => :get, :as => 'api_band_recording_detail' + match '/bands/:id/recordings' => 'api_bands#recording_create', :via => :post + match '/bands/:id/recordings/:recording_id' => 'api_bands#recording_update', :via => :post + match '/bands/:id/recordings/:recording_id' => 'api_bands#recording_destroy', :via => :delete + + # band invitations + match '/bands/:id/invitations' => 'api_bands#invitation_index', :via => :get + match '/bands/:id/invitations/:invitation_id' => 'api_bands#invitation_show', :via => :get, :as => 'api_band_invitation_detail' + match '/bands/:id/invitations' => 'api_bands#invitation_create', :via => :post + match '/bands/:id/invitations/:invitation_id' => 'api_bands#invitation_destroy', :via => :delete + + # invitations + match '/invitations/:id' => 'api_invitations#show', :via => :get, :as => 'api_invitation_detail' + match '/invitations/:id' => 'api_invitations#delete', :via => :delete + match '/invitations' => 'api_invitations#index', :via => :get + match '/invitations' => 'api_invitations#create', :via => :post + + # invited users + match '/invited_users/:id' => 'api_invited_users#show', :via => :get, :as => 'api_invited_user_detail' + match '/invited_users' => 'api_invited_users#index', :via => :get + match '/invited_users' => 'api_invited_users#create', :via => :post + + # instruments + match '/instruments/:id' => 'api_instruments#show', :via => :get, :as => 'api_instrument_detail' + match '/instruments' => 'api_instruments#index', :via => :get + + # search + match '/search' => 'api_search#index', :via => :get + + # join requests + match '/join_requests/:id' => 'api_join_requests#show', :via => :get, :as => 'api_join_request_detail' + match '/join_requests/:id' => 'api_join_requests#delete', :via => :delete + match '/join_requests' => 'api_join_requests#create', :via => :post + match '/join_requests' => 'api_join_requests#index', :via => :get + + # Location lookups + match '/countries' => 'api_maxmind_requests#countries', :via => :get + match '/regions' => 'api_maxmind_requests#regions', :via => :get + match '/cities' => 'api_maxmind_requests#cities', :via => :get + match '/isps' => 'api_maxmind_requests#isps', :via => :get + + # Recordings + match '/recordings/list' => 'api_recordings#list', :via => :get + match '/recordings/start' => 'api_recordings#start', :via => :post + match '/recordings/:id/stop' => 'api_recordings#stop', :via => :put + match '/recordings/:id/claim' => 'api_recordings#claim', :via => :post + match '/recordings/upload_next_part' => 'api_recordings#upload_next_part', :via => :get + match '/recordings/upload_sign' => 'api_recordings#upload_sign', :via => :get + match '/recordings/upload_part_complete' => 'api_recordings#upload_part_complete', :via => :put + match '/recordings/upload_complete' => 'api_recordings#upload_complete', :via => :put + + # Claimed Recordings + match '/claimed_recordings' => 'api_claimed_recordings#index', :via => :get + match '/claimed_recordings/:id' => 'api_claimed_recordings#show', :via => :get + match '/claimed_recordings/:id' => 'api_claimed_recordings#update', :via => :put + match '/claimed_recordings/:id' => 'api_claimed_recordings#delete', :via => :delete + + # Mixes + match '/mixes/schedule' => 'api_mixes#schedule', :via => :post + match '/mixes/next' => 'api_mixes#next', :via => :get + match '/mixes/finish' => 'api_mixes#finish', :via => :put + + # version check for JamClient + match '/versioncheck' => 'artifacts#versioncheck' + + # list all uris for available clients on mac, windows, linux, if available + match '/artifacts/clients' => 'artifacts#client_downloads' + + # crash logs + match '/dumps' => 'api_users#crash_dump', :via => :put + + # feedback from corporate site api + match '/feedback' => 'api_corporate#feedback', :via => :post + + + end +end diff --git a/web/config/unicorn.rb b/web/config/unicorn.rb new file mode 100644 index 000000000..cd85380f7 --- /dev/null +++ b/web/config/unicorn.rb @@ -0,0 +1,105 @@ +# Sample verbose configuration file for Unicorn (not Rack) +# +# This configuration file documents many features of Unicorn +# that may not be needed for some applications. See +# http://unicorn.bogomips.org/examples/unicorn.conf.minimal.rb +# for a much simpler configuration file. +# +# See http://unicorn.bogomips.org/Unicorn/Configurator.html for complete +# documentation. + +# Use at least one worker per core if you're on a dedicated server, +# more will usually help for _short_ waits on databases/caches. +worker_processes 4 + +# Since Unicorn is never exposed to outside clients, it does not need to +# run on the standard HTTP port (80), there is no reason to start Unicorn +# as root unless it's from system init scripts. +# If running the master process as root and the workers as an unprivileged +# user, do this to switch euid/egid in the workers (also chowns logs): +user "jam-web", "jam-web" + +# Help ensure your application will always spawn in the symlinked +# "current" directory that Capistrano sets up. +working_directory "/var/lib/jam-web" # available in 0.94.0+ + +# listen on both a Unix domain socket and a TCP port, +# we use a shorter backlog for quicker failover when busy +listen "/tmp/.sock", :backlog => 64 +listen 3100, :tcp_nopush => true + +# nuke workers after 30 seconds instead of 60 seconds (the default) +timeout 30 + +# feel free to point this anywhere accessible on the filesystem +pid "/var/run/jam-web.pid" + +# By default, the Unicorn logger will write to stderr. +# Additionally, ome applications/frameworks log to stderr or stdout, +# so prevent them from going to /dev/null when daemonized here: +stderr_path "/var/lib/jam-web/log/unicorn.stderr.log" +stdout_path "/var/lib/jam-web/log/unicorn.stdout.log" + +# combine Ruby 2.0.0dev or REE with "preload_app true" for memory savings +# http://rubyenterpriseedition.com/faq.html#adapt_apps_for_cow +preload_app true +GC.respond_to?(:copy_on_write_friendly=) and + GC.copy_on_write_friendly = true + +# Enable this flag to have unicorn test client connections by writing the +# beginning of the HTTP headers before calling the application. This +# prevents calling the application for connections that have disconnected +# while queued. This is only guaranteed to detect clients on the same +# host unicorn runs on, and unlikely to detect disconnects even on a +# fast LAN. +check_client_connection false + +before_fork do |server, worker| + # the following is highly recomended for Rails + "preload_app true" + # as there's no need for the master process to hold a connection + defined?(ActiveRecord::Base) and + ActiveRecord::Base.connection.disconnect! + + # The following is only recommended for memory/DB-constrained + # installations. It is not needed if your system can house + # twice as many worker_processes as you have configured. + # + # # This allows a new master process to incrementally + # # phase out the old master process with SIGTTOU to avoid a + # # thundering herd (especially in the "preload_app false" case) + # # when doing a transparent upgrade. The last worker spawned + # # will then kill off the old master process with a SIGQUIT. + # old_pid = "#{server.config[:pid]}.oldbin" + # if old_pid != server.pid + # begin + # sig = (worker.nr + 1) >= server.worker_processes ? :QUIT : :TTOU + # Process.kill(sig, File.read(old_pid).to_i) + # rescue Errno::ENOENT, Errno::ESRCH + # end + # end + # + # Throttle the master from forking too quickly by sleeping. Due + # to the implementation of standard Unix signal handlers, this + # helps (but does not completely) prevent identical, repeated signals + # from being lost when the receiving process is busy. + # sleep 1 +end + +after_fork do |server, worker| + # per-process listener ports for debugging/admin/migrations + # addr = "127.0.0.1:#{9293 + worker.nr}" + # server.listen(addr, :tries => -1, :delay => 5, :tcp_nopush => true) + + # the following is *required* for Rails + "preload_app true", + defined?(ActiveRecord::Base) and + ActiveRecord::Base.establish_connection + + Thread.new do + JamWebEventMachine.run_em + end + # if preload_app is true, then you may also want to check and + # restart any other shared sockets/descriptors such as Memcached, + # and Redis. TokyoCabinet file handles are safe to reuse + # between any number of forked children (assuming your kernel + # correctly implements pread()/pwrite() system calls) +end diff --git a/web/db/schema.rb b/web/db/schema.rb new file mode 100644 index 000000000..b5e6a7964 --- /dev/null +++ b/web/db/schema.rb @@ -0,0 +1,16 @@ +# encoding: UTF-8 +# This file is auto-generated from the current state of the database. Instead +# of editing this file, please use the migrations feature of Active Record to +# incrementally modify your database, and then regenerate this schema definition. +# +# Note that this schema.rb definition is the authoritative source for your +# database schema. If you need to create the application database on another +# system, you should be using db:schema:load, not running all the migrations +# from scratch. The latter is a flawed and unsustainable approach (the more migrations +# you'll amass, the slower it'll run and the greater likelihood for issues). +# +# It's strongly recommended to check this file into your version control system. + +ActiveRecord::Schema.define(:version => 0) do + +end diff --git a/web/db/seeds.rb b/web/db/seeds.rb new file mode 100644 index 000000000..4edb1e857 --- /dev/null +++ b/web/db/seeds.rb @@ -0,0 +1,7 @@ +# This file should contain all the record creation needed to seed the database with its default values. +# The data can then be loaded with the rake db:seed (or created alongside the db with db:setup). +# +# Examples: +# +# cities = City.create([{ name: 'Chicago' }, { name: 'Copenhagen' }]) +# Mayor.create(name: 'Emanuel', city: cities.first) diff --git a/web/doc/README_FOR_APP b/web/doc/README_FOR_APP new file mode 100644 index 000000000..fe41f5cc2 --- /dev/null +++ b/web/doc/README_FOR_APP @@ -0,0 +1,2 @@ +Use this README file to introduce your application and point to useful places in the API for learning more. +Run "rake doc:app" to generate API documentation for your models, controllers, helpers, and libraries. diff --git a/web/features/create_session.feature b/web/features/create_session.feature new file mode 100644 index 000000000..596fbe66e --- /dev/null +++ b/web/features/create_session.feature @@ -0,0 +1,11 @@ +Feature: Users can create session + In order to play music with other people + As a musician + I want to create a music session + + @javascript + Scenario: Create a public music session + Given I am logged in to the client + When I create a public music session + Then I should be in a music session that another musician can find + And that other musician should be able to join my public session diff --git a/web/features/home_page.feature b/web/features/home_page.feature new file mode 100644 index 000000000..8e9e401fc --- /dev/null +++ b/web/features/home_page.feature @@ -0,0 +1,13 @@ +# because the create_session test creates a second session, I think it cause test to fail +# the reason I think this is because if you run just this test, it's fine. so something about creating multiple sessions +# in capybara seems to upset cucumber/capybara for a 2nd test +@wip +Feature: Viewer can use the home page + In order to use the rest of the service + As a viewer + I want to see the home page of the service + + @javascript + Scenario: View home page + Given I am on the home page + Then I should see "sign in or register" diff --git a/web/features/step_definitions/create_session_steps.rb b/web/features/step_definitions/create_session_steps.rb new file mode 100644 index 000000000..fde1be260 --- /dev/null +++ b/web/features/step_definitions/create_session_steps.rb @@ -0,0 +1,81 @@ +Given /^I am logged in to the client$/ do + @user = FactoryGirl.create(:user) + + login(@user, page) # page == the default context +end + +When /^I create a public music session$/ do + + # the relatively unique name of the session we will create + @session_description = 'A new session for anyone to find and play with me' + + # Walk us through the FTUE screens... + #page.find("a[href='#/ftue2'] button:contains('Next')").click + #sleep 1 + #page.find("a[href='#/ftue3']").click + #sleep 1 + #page.find('div[data-step="step1"] button.startTestButton').click + #page.find("#poorAudioButton").click + #page.find("#goodAudioButton").click + #page.find("[cucumber-id='ftue-home-link']").click + # to wait for animation to end + sleep 3 + page.find("[layout-link='createSession']").click + + # pick a genre, any genre + sleep 5 + within '#create-session-form' do + within '#genre-list' do + find("option[value='rock']").click + end + end + + # fill in description + page.fill_in 'description', :with => @session_description + + # create the session + page.find('#btn-create-session').click + + # verify that the 'in-session' page is showing, with our description showing + # page.find("#session-info").should have_content @session_description +end + +Then /^I should be in a music session that another musician can find$/ do + + # now log in a second user + @second_session = Capybara::Session.new(Capybara.current_driver, Capybara.app) + @user2 = FactoryGirl.create(:user) + + login(@user2, @second_session) + + # Walk us through the FTUE screens... + #@second_session.find("a[href='#/ftue2'] button:contains('Next')").click + #sleep 1 + #@second_session.find("a[href='#/ftue3']").click + #sleep 1 + #@second_session.find('div[data-step="step1"] button.startTestButton').click + #sleep 1 + #@second_session.find("#poorAudioButton").click + #sleep 1 + #@second_session.find("#goodAudioButton").click + #sleep 1 + @second_session.find("[cucumber-id='ftue-home-link']").click + sleep 1 + + # click find sessions + @second_session.find("[layout-link='findSession']").click + + # and see the session with the same description we created earlier show up + sleep 5 + # comment out until I can figure out why it's failing + #@second_session.find("tr[data-sortScore]").should have_content @session_description +end + +When /^that other musician should be able to join my public session$/ do + + # and finally join that session as the other user + #@second_session.find("tr[data-sortScore] a", :text =>"Join").click + + # verify that the 'in-session' page is showing, with our description showing + #@second_session.find("#session-info").should have_content @session_description +end diff --git a/web/features/step_definitions/jamweb_steps.rb b/web/features/step_definitions/jamweb_steps.rb new file mode 100644 index 000000000..aa769eb2b --- /dev/null +++ b/web/features/step_definitions/jamweb_steps.rb @@ -0,0 +1,9 @@ +Given /^I am on the home page$/ do + $home_page_session = Capybara::Session.new(Capybara.current_driver, Capybara.app) + $home_page_session.visit root_path +end + +Then /^I should see "(.*?)"$/ do |content| + $home_page_session.find("div.content-head h1").should have_content content +end + diff --git a/web/features/support/before_cucumber.rb b/web/features/support/before_cucumber.rb new file mode 100644 index 000000000..36d80aeda --- /dev/null +++ b/web/features/support/before_cucumber.rb @@ -0,0 +1,25 @@ +require 'active_record' +require 'action_mailer' +require 'jam_db' +require 'capybara' +require 'selenium/webdriver' + +require File.expand_path('../../../spec/spec_db', __FILE__) + +# put this in a class, so that multiple loads of this file +# don't cause the database to be recreated multiple times +class BeforeCucumber + def initialize + # recreate test database and migrate it + db_config = YAML::load(File.open('config/database.yml'))["cucumber"] + # initialize ActiveRecord's db connection + + SpecDb::recreate_database(db_config) + ActiveRecord::Base.establish_connection(db_config) + + + # put ActionMailer into test mode + ActionMailer::Base.delivery_method = :test + end +end + diff --git a/web/features/support/env.rb b/web/features/support/env.rb new file mode 100644 index 000000000..d6e814ff6 --- /dev/null +++ b/web/features/support/env.rb @@ -0,0 +1,117 @@ +# IMPORTANT: This file is generated by cucumber-rails - edit at your own peril. +# It is recommended to regenerate this file in the future when you upgrade to a +# newer version of cucumber-rails. Consider adding your own code to a new file +# instead of editing this one. Cucumber will automatically load all features/**/*.rb +# files. + +# force cucumber environment +ENV['RAILS_ENV'] = 'cucumber' + +# SETH: ADDED TO FORCE OUR OWN DB LOADING +require File.expand_path('../before_cucumber', __FILE__) +BeforeCucumber.new +require 'jam_ruby' +include JamRuby + +require 'cucumber/rails' +require 'capybara-screenshot/cucumber' # https://github.com/mattheworiordan/capybara-screenshot +require 'capybara-webkit' + + + After do |scenario| + # if the scenario failed, we dump all the javascript console logs out + if scenario.failed? + if page.driver.to_s.match("Webkit") + $stdout.puts "" + $stdout.puts "===============================" + $stdout.puts "= primary session console out =" + $stdout.puts "===============================" + $stdout.puts page.driver.console_messages + $stdout.puts "" + $stdout.puts "===============================" + $stdout.puts "= primary session error =" + $stdout.puts "===============================" + $stdout.puts page.driver.error_messages + $stdout.puts "" + if @second_session.nil? + $stdout.puts "the second session was not reached. no output" + else + $stdout.puts "===============================" + $stdout.puts "= second session console out =" + $stdout.puts "===============================" + $stdout.puts @second_session.driver.console_messages + $stdout.puts "" + $stdout.puts "===============================" + $stdout.puts "= sceond session error =" + $stdout.puts "===============================" + $stdout.puts @second_session.driver.error_messages + $stdout.puts "" + end + + $stdout.puts ">>> using headless webkit. If you want selenium, do `DRIVER=selenium bundle exec cucumber`" + end + end +end + +# Capybara defaults to XPath selectors rather than Webrat's default of CSS3. In +# order to ease the transition to Capybara we set the default here. If you'd +# prefer to use XPath just remove this line and adjust any selectors in your +# steps to use the XPath syntax. +Capybara.default_selector = :css + +# By default, any exception happening in your Rails application will bubble up +# to Cucumber so that your scenario will fail. This is a different from how +# your application behaves in the production environment, where an error page will +# be rendered instead. +# +# Sometimes we want to override this default behaviour and allow Rails to rescue +# exceptions and display an error page (just like when the app is running in production). +# Typical scenarios where you want to do this is when you test your error pages. +# There are two ways to allow Rails to rescue exceptions: +# +# 1) Tag your scenario (or feature) with @allow-rescue +# +# 2) Set the value below to true. Beware that doing this globally is not +# recommended as it will mask a lot of errors for you! +# +ActionController::Base.allow_rescue = false + +# Remove/comment out the lines below if your app doesn't have a database. +# For some databases (like MongoDB and CouchDB) you may need to use :truncation instead. +begin + #DatabaseCleaner.strategy = :transaction + #DatabaseCleaner.strategy = :truncation, {:except => %w[instruments genres] } + #DatabaseCleaner.clean_with(:truncation, {:except => %w[instruments genres] }) +rescue NameError + raise "You need to add database_cleaner to your Gemfile (in the :test group) if you wish to use it." +end + +# You may also want to configure DatabaseCleaner to use different strategies for certain features and scenarios. +# See the DatabaseCleaner documentation for details. Example: +# +# Before('@no-txn,@selenium,@culerity,@celerity,@javascript') do +# # { :except => [:widgets] } may not do what you expect here +# # as tCucumber::Rails::Database.javascript_strategy overrides +# # this setting. +# DatabaseCleaner.strategy = :truncation +# end +# +# Before('~@no-txn', '~@selenium', '~@culerity', '~@celerity', '~@javascript') do +# DatabaseCleaner.strategy = :transaction +# end +# + +if ENV['DRIVER'] == 'selenium' + $stdout.puts ">>> using selenium" + Capybara.javascript_driver = :selenium +else + $stdout.puts ">>> using headless webkit. If you want selenium, do `DRIVER=selenium bundle exec cucumber`" + Capybara.javascript_driver = :webkit +end + +# Possible values are :truncation and :transaction +# The :transaction strategy is faster, but might give you threading problems. +# See https://github.com/cucumber/cucumber-rails/blob/master/features/choose_javascript_database_strategy.feature +Cucumber::Rails::Database.javascript_strategy = :truncation + + diff --git a/web/features/support/extensions/JSErrorCollector.xpi b/web/features/support/extensions/JSErrorCollector.xpi new file mode 100644 index 000000000..3e8af6a84 Binary files /dev/null and b/web/features/support/extensions/JSErrorCollector.xpi differ diff --git a/web/features/support/login_helper.rb b/web/features/support/login_helper.rb new file mode 100644 index 000000000..53a670c04 --- /dev/null +++ b/web/features/support/login_helper.rb @@ -0,0 +1,28 @@ +module LoginHelper + def login(user, context) + + context.visit root_path + + # When troubleshooting JS errors, sleeping when the UI first comes up is helpful. + # It allows you to bring up a JS console on the test browser and see what the problem is. + # sleep 60 + + # verify that the sign in form is showing + context.should have_content "sign in or register" + + context.fill_in 'Email', :with => user.email + context.fill_in 'Password', :with => user.password + context.click_button 'SIGN IN' + + # There are delays in the main app due to us waiting until we're connected to + # the websocket gateway. Tough to test, but adding a sleep to see if this + # gets us past it. + sleep 4 + + # verify that the client page is showing + # Temporary comment - we're skipping FTUE for video... + # context.find("h1:contains('Audio Gear Setup')").should be_visible + end +end + +World(LoginHelper) diff --git a/web/jenkins b/web/jenkins new file mode 100755 index 000000000..67733e8c2 --- /dev/null +++ b/web/jenkins @@ -0,0 +1,29 @@ +#!/bin/bash + +DEB_SERVER=http://localhost:9010/apt-`uname -p` + +echo "starting build..." +./build + +if [ "$?" = "0" ]; then + echo "build succeeded" + + if [ ! -z "$PACKAGE" ]; then + echo "publishing ubuntu package (.deb)" + DEBPATH=`find target/deb -name *.deb` + DEBNAME=`basename $DEBPATH` + + curl -f -T $DEBPATH $DEB_SERVER/$DEBNAME + + if [ "$?" != "0" ]; then + echo "deb publish failed" + exit 1 + fi + echo "done publishing deb" + fi +else + echo "build failed" + exit 1 +fi + + diff --git a/web/lib/assets/.gitkeep b/web/lib/assets/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/web/lib/jam_web/version.rb b/web/lib/jam_web/version.rb new file mode 100644 index 000000000..d6efd19ab --- /dev/null +++ b/web/lib/jam_web/version.rb @@ -0,0 +1,3 @@ +module JamWeb + VERSION = "0.0.1" +end \ No newline at end of file diff --git a/web/lib/max_mind_manager.rb b/web/lib/max_mind_manager.rb new file mode 100644 index 000000000..dc28a2ca7 --- /dev/null +++ b/web/lib/max_mind_manager.rb @@ -0,0 +1,140 @@ +class MaxMindManager < BaseManager + + def initialize(options={}) + super(options) + end + + + # Returns a hash with location information. Fields are nil if they can't be figured. + # This is a class method because it doesn't need to be in a transaction. + def self.lookup(ip_address) + + city = state = country = nil + + unless ip_address.nil? || ip_address !~ /^\d+\.\d+\.\d+\.\d+$/ + ActiveRecord::Base.connection_pool.with_connection do |connection| + pg_conn = connection.instance_variable_get("@connection") + ip_as_int = ip_address_to_int(ip_address) + pg_conn.exec("SELECT country, region, city FROM max_mind_geo WHERE ip_bottom <= $1 AND ip_top >= $2", [ip_as_int, ip_as_int]) do |result| + if !result.nil? && result.ntuples > 0 + country = result.getvalue(0, 0) + state = result[0]['region'] + city = result[0]['city'] + end + end + end + end + + { + :city => city, + :state => state, + :country => country + } + + end + + def self.lookup_isp(ip_address) + + isp = nil + + unless ip_address.nil? || ip_address !~ /^\d+\.\d+\.\d+\.\d+$/ + ActiveRecord::Base.connection_pool.with_connection do |connection| + pg_conn = connection.instance_variable_get("@connection") + ip_as_int = ip_address_to_int(ip_address) + pg_conn.exec("SELECT isp FROM max_mind_isp WHERE ip_bottom <= $1 AND ip_top >= $2", [ip_as_int, ip_as_int]) do |result| + if !result.nil? && result.ntuples > 0 + isp = result.getvalue(0, 0) + end + end + end + end + + return isp + end + + def self.countries() + ActiveRecord::Base.connection_pool.with_connection do |connection| + pg_conn = connection.instance_variable_get("@connection") + pg_conn.exec("SELECT DISTINCT country FROM max_mind_geo ORDER BY country ASC").map do |tuple| + tuple["country"] + end + end + end + + + def self.regions(country) + ActiveRecord::Base.connection_pool.with_connection do |connection| + pg_conn = connection.instance_variable_get("@connection") + pg_conn.exec("SELECT DISTINCT region FROM max_mind_geo WHERE country = $1 ORDER BY region ASC", [country]).map do |tuple| + tuple["region"] + end + end + end + + + def self.cities(country, region) + ActiveRecord::Base.connection_pool.with_connection do |connection| + pg_conn = connection.instance_variable_get("@connection") + pg_conn.exec("SELECT DISTINCT city FROM max_mind_geo WHERE country = $1 AND region = $2 ORDER BY city ASC", [country, region]).map do |tuple| + tuple["city"] + end + end + end + + + def self.isps(country) + ActiveRecord::Base.connection_pool.with_connection do |connection| + pg_conn = connection.instance_variable_get("@connection") + pg_conn.exec("SELECT DISTINCT isp FROM max_mind_isp WHERE country = $1 ORDER BY isp ASC", [country]).map do |tuple| + tuple["isp"] + end + end + end + + # Note that there's one big country, and then two cities in each region. + def create_phony_database() + clear_location_table + (0..255).each do |top_octet| + @pg_conn.exec("INSERT INTO max_mind_geo (ip_bottom, ip_top, country, region, city) VALUES ($1, $2, $3, $4, $5)", + [ + self.class.ip_address_to_int("#{top_octet}.0.0.0"), + self.class.ip_address_to_int("#{top_octet}.255.255.255"), + "US", + "Region #{(top_octet / 2).floor}", + "City #{top_octet}" + ]).clear + end + + clear_isp_table + (0..255).each do |top_octet| + @pg_conn.exec("INSERT INTO max_mind_isp (ip_bottom, ip_top, isp, country) VALUES ($1, $2, $3, $4)", + [ + self.class.ip_address_to_int("#{top_octet}.0.0.0"), + self.class.ip_address_to_int("#{top_octet}.255.255.255"), + "ISP #{top_octet}", + "US" + ]).clear + end + + + end + + private + + # Make an IP address fit in a signed int. Just divide it by 2, as the least significant part + # just can't possibly matter. We can verify this if needed. My guess is the entire bottom octet is + # actually irrelevant + def self.ip_address_to_int(ip) + ip.split('.').inject(0) {|total,value| (total << 8 ) + value.to_i} / 2 + end + + def clear_location_table + @pg_conn.exec("DELETE FROM max_mind_geo").clear + end + + def clear_isp_table + @pg_conn.exec("DELETE FROM max_mind_isp").clear + end + +end + diff --git a/web/lib/music_session_manager.rb b/web/lib/music_session_manager.rb new file mode 100644 index 000000000..c6c43b014 --- /dev/null +++ b/web/lib/music_session_manager.rb @@ -0,0 +1,141 @@ +require 'recaptcha' +class MusicSessionManager < BaseManager + + include Recaptcha::Verify + + def initialize(options={}) + super(options) + @log = Logging.logger[self] + end + + def create(user, client_id, description, musician_access, approval_required, fan_chat, fan_access, band, genres, tracks, legal_terms) + return_value = nil + + ActiveRecord::Base.transaction do + # check if we are connected to rabbitmq + music_session = MusicSession.new() + music_session.creator = user + music_session.description = description + music_session.musician_access = musician_access + music_session.approval_required = approval_required + music_session.fan_chat = fan_chat + music_session.fan_access = fan_access + music_session.band = band + music_session.legal_terms = legal_terms + + #genres = genres + @log.debug "Genres class: " + genres.class.to_s() + + unless genres.nil? + genres.each do |genre_id| + loaded_genre = Genre.find(genre_id) + music_session.genres << loaded_genre + end + end + + music_session.save + + unless music_session.errors.any? + # save session parameters for next session + User.save_session_settings(user, music_session) + + # save session history + MusicSessionHistory.save(music_session) + + # auto-join this user into the newly created session + as_musician = true + connection = ConnectionManager.new.join_music_session(user, client_id, music_session, as_musician, tracks) do |db_conn, connection| + if as_musician && music_session.musician_access + Notification.send_musician_session_join(music_session, connection, user) + Notification.send_friend_session_join(db_conn, connection, user) + end + end + + unless connection.errors.any? + return_value = music_session + else + return_value = connection + # rollback the transaction to make sure nothing is disturbed in the database + raise ActiveRecord::Rollback + end + else + return_value = music_session + # rollback the transaction to make sure nothing is disturbed in the database + raise ActiveRecord::Rollback + end + end + + return return_value + end + + # Update the session. If a field is left out (meaning, it's set to nil), it's not updated. + def update(music_session, description, genres, musician_access, approval_required, fan_chat, fan_access) + + update = {} + update[:description] = description unless description.nil? + update[:musician_access] = musician_access unless musician_access.nil? + update[:approval_required] = approval_required unless approval_required.nil? + update[:fan_chat] = fan_chat unless fan_chat.nil? + update[:fan_access] = fan_access unless fan_access.nil? + # Do I have to do this the way he did above? Not sure. Probably yes. + genre_array = [] + if genres.nil? + music_session.skip_genre_validation = true + else + genres.each do |genre_id| + loaded_genre = Genre.find(genre_id) + genre_array << loaded_genre + end + update[:genres] = genre_array + end + + if music_session.update_attributes(update) + # save session history (only thing that could change is description) + MusicSessionHistory.save(music_session) + end + + return music_session + end + + def participant_create(user, music_session_id, client_id, as_musician, tracks) + connection = nil + ActiveRecord::Base.transaction do + + music_session = MusicSession.find(music_session_id) + + connection = ConnectionManager.new.join_music_session(user, client_id, music_session, as_musician, tracks) do |db_conn, connection| + if as_musician && music_session.musician_access + Notification.send_musician_session_join(music_session, connection, user) + Notification.send_friend_session_join(db_conn, connection, user) + end + end + + if connection.errors.any? + # rollback the transaction to make sure nothing is disturbed in the database + raise ActiveRecord::Rollback + else + # send out notification to queue to the rest of the session + # TODO: also this isn't necessarily a user leaving; it's a client leaving' + end + end + + return connection + end + + def participant_delete(user, connection, music_session) + + if connection.user.id != user.id + raise PermissionError, "you do not own this connection" + end + + ConnectionManager.new.leave_music_session(user, connection, music_session) do + Notification.send_musician_session_depart(music_session, connection.client_id, user) + end + + unless music_session.nil? + # send out notification to queue to the rest of the session + # TODO: we should rename the notification to music_session_participants_change or something + # TODO: also this isn't necessarily a user leaving; it's a client leaving + end + end +end diff --git a/web/lib/tasks/.gitkeep b/web/lib/tasks/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/web/lib/tasks/cucumber.rake b/web/lib/tasks/cucumber.rake new file mode 100644 index 000000000..83f79471e --- /dev/null +++ b/web/lib/tasks/cucumber.rake @@ -0,0 +1,65 @@ +# IMPORTANT: This file is generated by cucumber-rails - edit at your own peril. +# It is recommended to regenerate this file in the future when you upgrade to a +# newer version of cucumber-rails. Consider adding your own code to a new file +# instead of editing this one. Cucumber will automatically load all features/**/*.rb +# files. + + +unless ARGV.any? {|a| a =~ /^gems/} # Don't load anything when running the gems:* tasks + +vendored_cucumber_bin = Dir["#{Rails.root}/vendor/{gems,plugins}/cucumber*/bin/cucumber"].first +$LOAD_PATH.unshift(File.dirname(vendored_cucumber_bin) + '/../lib') unless vendored_cucumber_bin.nil? + +begin + require 'cucumber/rake/task' + + namespace :cucumber do + Cucumber::Rake::Task.new({:ok => 'db:test:prepare'}, 'Run features that should pass') do |t| + t.binary = vendored_cucumber_bin # If nil, the gem's binary is used. + t.fork = true # You may get faster startup if you set this to false + t.profile = 'default' + end + + Cucumber::Rake::Task.new({:wip => 'db:test:prepare'}, 'Run features that are being worked on') do |t| + t.binary = vendored_cucumber_bin + t.fork = true # You may get faster startup if you set this to false + t.profile = 'wip' + end + + Cucumber::Rake::Task.new({:rerun => 'db:test:prepare'}, 'Record failing features and run only them if any exist') do |t| + t.binary = vendored_cucumber_bin + t.fork = true # You may get faster startup if you set this to false + t.profile = 'rerun' + end + + desc 'Run all features' + task :all => [:ok, :wip] + + task :statsetup do + require 'rails/code_statistics' + ::STATS_DIRECTORIES << %w(Cucumber\ features features) if File.exist?('features') + ::CodeStatistics::TEST_TYPES << "Cucumber features" if File.exist?('features') + end + end + desc 'Alias for cucumber:ok' + task :cucumber => 'cucumber:ok' + + task :default => :cucumber + + task :features => :cucumber do + STDERR.puts "*** The 'features' task is deprecated. See rake -T cucumber ***" + end + + # In case we don't have ActiveRecord, append a no-op task that we can depend upon. + task 'db:test:prepare' do + end + + task :stats => 'cucumber:statsetup' +rescue LoadError + desc 'cucumber rake task not available (cucumber not installed)' + task :cucumber do + abort 'Cucumber rake task is not available. Be sure to install cucumber as a gem or plugin' + end +end + +end diff --git a/web/lib/tasks/import_max_mind.rake b/web/lib/tasks/import_max_mind.rake new file mode 100644 index 000000000..08098a6e7 --- /dev/null +++ b/web/lib/tasks/import_max_mind.rake @@ -0,0 +1,21 @@ +namespace :db do + desc "Import a maxmind database; run like this: rake db:import_maxmind_geo file=" + task :import_maxmind_geo do + MaxMindGeo.import_from_max_mind ENV['file'] + end + + desc "Import a maxmind isp database; run like this: rake db:import_maxmind_isp file=" + task :import_maxmind_isp do + MaxMindIsp.import_from_max_mind ENV['file'] + end + + desc "Create a fake set of maxmind data" + task :phony_maxmind do + MaxMindManager.active_record_transaction do |manager| + manager.create_phony_database() + end + end +end + + + diff --git a/web/lib/tasks/sample_data.rake b/web/lib/tasks/sample_data.rake new file mode 100644 index 000000000..5eca16faf --- /dev/null +++ b/web/lib/tasks/sample_data.rake @@ -0,0 +1,107 @@ +namespace :db do + desc "Fill database with sample data" + task populate: :environment do + make_users + make_microposts + make_relationships + end + + desc "Fill database with music session sample data" + task populate_music_sessions: :environment do + make_users(10) if 14 > User.count + make_bands if 0==Band.count + make_music_sessions_history + make_music_sessions_user_history + end +end + +def make_music_sessions_history + users = User.all.map(&:id) + bands = Band.all.map(&:id) + genres = Genre.all.map(&:description) + 50.times do |nn| + obj = MusicSessionHistory.new + obj.music_session_id = rand(100000000).to_s + obj.description = Faker::Lorem.paragraph + obj.user_id = users[rand(users.count)] + obj.band_id = bands[rand(bands.count)] + obj.created_at = Time.now - rand(1.month.seconds) + obj.session_removed_at = obj.created_at + (rand(3)+1).hour + obj.genres = genres.shuffle[0..rand(4)].join(' | ') + obj.save! + end +end + +def make_music_sessions_user_history + users = User.all.map(&:id) + hists = MusicSessionHistory.all + hists.each do |msh| + (rand(9)+1).times do |nn| + obj = MusicSessionUserHistory.new + obj.music_session_id = msh.music_session_id + obj.user_id = users[rand(users.count)] + obj.created_at = msh.created_at + obj.session_removed_at = obj.created_at + (rand(3)+1).hour + obj.client_id = rand(100000000).to_s + obj.save! + end + end +end + +def make_bands + 10.times do |nn| + name = Faker::Name.name + website = Faker::Internet.url + biography = Faker::Lorem.sentence + city = Faker::Address.city + state = Faker::Address.state_abbr + country = Faker::Address.country + + Band.create!( + name: name, + website: website, + biography: biography, + city: city, + state: state, + country: country, + ) + + end +end + +def make_users(num=99) + admin = User.create!( first_name: Faker::Name.name, + last_name: Faker::Name.name, + email: "example@railstutorial.org", + password: "foobar", + password_confirmation: "foobar", + terms_of_service: true) + admin.toggle!(:admin) + num.times do |n| + email = "example-#{n+1}@railstutorial.org" + password = "password" + User.create!(first_name: Faker::Name.name, + last_name: Faker::Name.name, + terms_of_service: true, + email: email, + password: password, + password_confirmation: password) + end +end + +def make_microposts + users = User.all(limit: 6) + 50.times do + content = Faker::Lorem.sentence(5) + users.each { |user| user.microposts.create!(content: content) } + end +end + +def make_relationships + users = User.all + user = users.first + followed_users = users[2..50] + followers = users[3..40] + followed_users.each { |followed| user.follow!(followed) } + followers.each { |follower| follower.follow!(user) } +end \ No newline at end of file diff --git a/web/lib/user_manager.rb b/web/lib/user_manager.rb new file mode 100644 index 000000000..4eb487be5 --- /dev/null +++ b/web/lib/user_manager.rb @@ -0,0 +1,54 @@ +require 'recaptcha' +class UserManager < BaseManager + + include Recaptcha::Verify + + def initialize(options={}) + super(options) + @log = Logging.logger[self] + end + + # Note that almost everything can be nil here. This is because when users sign up via social media, + # we don't know much about them. + def signup(remote_ip, first_name, last_name, email, password = nil, password_confirmation = nil, terms_of_service = nil, subscribe_email = nil, + instruments = nil, birth_date = nil, location = nil, musician = nil, photo_url = nil, invited_user = nil, signup_confirm_url = nil) + + @user = User.new + + # check if we have disabled open signup for this site. open == invited users can still get in + if !SampleApp::Application.config.signup_enabled && invited_user.nil? + raise PermissionError, "Signups are currently disabled" + end + + # a user should be able to specify their location, but if they don't, we'll best effort it + if location.nil? + location = MaxMindManager.lookup(remote_ip) + end + + # TODO: figure out why can't user verify_recaptcha here + # ALSO: make sure we dont do the recaptcha stuff if used facebook. + + # check recaptcha; if any errors seen, contribute it to the model + #unless verify_recaptcha(:model => @user, :message => "recaptcha") + # return @user # @user.errors.any? is true now + #else + # sends email to email account for confirmation + @user = User.signup(first_name, last_name, email, password, password_confirmation, terms_of_service, subscribe_email, + location, instruments, birth_date, musician, photo_url, invited_user, signup_confirm_url) + + return @user + #end + end + + def signup_confirm(signup_token, remote_ip=nil) + begin + @user = User.signup_confirm(signup_token) + @user.location = MaxMindManager.lookup(remote_ip) if remote_ip + rescue ActiveRecord::RecordNotFound + @user = nil + end + + return @user + end + +end diff --git a/web/log/test.log.age b/web/log/test.log.age new file mode 100644 index 000000000..e69de29bb diff --git a/web/migrate.sh b/web/migrate.sh new file mode 100755 index 000000000..a9afa1578 --- /dev/null +++ b/web/migrate.sh @@ -0,0 +1,2 @@ +#!/bin/bash +bundle exec jam_db up --connopts=dbname:jam host:localhost user:postgres password:postgres --verbose diff --git a/web/public/403.html b/web/public/403.html new file mode 100644 index 000000000..d4246fdec --- /dev/null +++ b/web/public/403.html @@ -0,0 +1,26 @@ + + + + The change you wanted was rejected (403) + + + + + +
        +

        The change you wanted was rejected.

        +

        You tried to change something you didn't have access to.

        +
        + + diff --git a/web/public/404.html b/web/public/404.html new file mode 100644 index 000000000..9a48320a5 --- /dev/null +++ b/web/public/404.html @@ -0,0 +1,26 @@ + + + + The page you were looking for doesn't exist (404) + + + + + +
        +

        The page you were looking for doesn't exist.

        +

        You may have mistyped the address or the page may have moved.

        +
        + + diff --git a/web/public/422.html b/web/public/422.html new file mode 100644 index 000000000..83660ab18 --- /dev/null +++ b/web/public/422.html @@ -0,0 +1,26 @@ + + + + The change you wanted was rejected (422) + + + + + +
        +

        The change you wanted was rejected.

        +

        Maybe you tried to change something you didn't have access to.

        +
        + + diff --git a/web/public/500.html b/web/public/500.html new file mode 100644 index 000000000..f3648a0db --- /dev/null +++ b/web/public/500.html @@ -0,0 +1,25 @@ + + + + We're sorry, but something went wrong (500) + + + + + +
        +

        We're sorry, but something went wrong.

        +
        + + diff --git a/web/public/favicon.ico b/web/public/favicon.ico new file mode 100644 index 000000000..e69de29bb diff --git a/web/public/fb-signup-button.png b/web/public/fb-signup-button.png new file mode 100644 index 000000000..d60bbeb48 Binary files /dev/null and b/web/public/fb-signup-button.png differ diff --git a/web/public/ping.jar b/web/public/ping.jar new file mode 100644 index 000000000..5ae83b495 Binary files /dev/null and b/web/public/ping.jar differ diff --git a/web/public/robots.txt b/web/public/robots.txt new file mode 100644 index 000000000..085187fa5 --- /dev/null +++ b/web/public/robots.txt @@ -0,0 +1,5 @@ +# See http://www.robotstxt.org/wc/norobots.html for documentation on how to use the robots.txt file +# +# To ban all spiders from the entire site uncomment the next two lines: +# User-Agent: * +# Disallow: / diff --git a/web/script/cucumber b/web/script/cucumber new file mode 100755 index 000000000..7fa5c9208 --- /dev/null +++ b/web/script/cucumber @@ -0,0 +1,10 @@ +#!/usr/bin/env ruby + +vendored_cucumber_bin = Dir["#{File.dirname(__FILE__)}/../vendor/{gems,plugins}/cucumber*/bin/cucumber"].first +if vendored_cucumber_bin + load File.expand_path(vendored_cucumber_bin) +else + require 'rubygems' unless ENV['NO_RUBYGEMS'] + require 'cucumber' + load Cucumber::BINARY +end diff --git a/web/script/package/jam-web.conf b/web/script/package/jam-web.conf new file mode 100755 index 000000000..d3caa8853 --- /dev/null +++ b/web/script/package/jam-web.conf @@ -0,0 +1,7 @@ +description "jam-web" + +start on startup +start on runlevel [2345] +stop on runlevel [016] + +exec start-stop-daemon --start --chdir /var/lib/jam-web --exec /var/lib/jam-web/script/package/upstart-run.sh diff --git a/web/script/package/post-install.sh b/web/script/package/post-install.sh new file mode 100755 index 000000000..4dfd6cdc1 --- /dev/null +++ b/web/script/package/post-install.sh @@ -0,0 +1,20 @@ +#!/bin/sh + +set -eu + +NAME="jam-web" + +USER="$NAME" +GROUP="$NAME" + +# copy upstart file +cp /var/lib/$NAME/script/package/$NAME.conf /etc/init/$NAME.conf + +mkdir -p /var/lib/$NAME/log +mkdir -p /var/lib/$NAME/tmp +mkdir -p /etc/$NAME +mkdir -p /var/log/$NAME + +chown -R $USER:$GROUP /var/lib/$NAME +chown -R $USER:$GROUP /etc/$NAME +chown -R $USER:$GROUP /var/log/$NAME diff --git a/web/script/package/post-uninstall.sh b/web/script/package/post-uninstall.sh new file mode 100755 index 000000000..a4014a177 --- /dev/null +++ b/web/script/package/post-uninstall.sh @@ -0,0 +1,27 @@ +#!/bin/sh + + + +NAME="jam-web" + +set -e +if [ "$1" = "remove" ] +then + set +e + # stop the process, if any is found. we don't want this failing to cause an error, though. + sudo stop $NAME + set -e + + if [ -f /etc/init/$NAME.conf ]; then + rm /etc/init/$NAME.conf + fi +fi + +if [ "$1" = "purge" ] +then + if [ -d /var/lib/$NAME ]; then + rm -rf /var/lib/$NAME + fi + + userdel $NAME +fi diff --git a/web/script/package/pre-install.sh b/web/script/package/pre-install.sh new file mode 100755 index 000000000..bc597c863 --- /dev/null +++ b/web/script/package/pre-install.sh @@ -0,0 +1,36 @@ +#!/bin/sh + +set -eu + +NAME="jam-web" + +HOME="/var/lib/$NAME" +USER="$NAME" +GROUP="$NAME" + +# if NIS is used, then errors can occur but be non-fatal +if which ypwhich >/dev/null 2>&1 && ypwhich >/dev/null 2>&1 +then + set +e +fi + +if ! getent group "$GROUP" >/dev/null +then + addgroup --system "$GROUP" >/dev/null +fi + +# creating user if it isn't already there +if ! getent passwd "$USER" >/dev/null +then + adduser \ + --system \ + --home $HOME \ + --shell /bin/false \ + --disabled-login \ + --ingroup "$GROUP" \ + --gecos "$USER" \ + "$USER" >/dev/null +fi + +# NISno longer a possible problem; stop ignoring errors +set -e diff --git a/web/script/package/pre-uninstall.sh b/web/script/package/pre-uninstall.sh new file mode 100755 index 000000000..e69de29bb diff --git a/web/script/package/upstart-run.sh b/web/script/package/upstart-run.sh new file mode 100755 index 000000000..44317db99 --- /dev/null +++ b/web/script/package/upstart-run.sh @@ -0,0 +1,19 @@ +#!/bin/bash -l + +# default config values +PORT=3000 +BUILD_NUMBER=1 + + +CONFIG_FILE="/etc/jam-web/upstart.conf" +if [ -e "$CONFIG_FILE" ]; then + . "$CONFIG_FILE" +fi + +# I don't like doing this, but the next command (bundle exec) retouches/generates +# the gemfile. This unfortunately means the next debian update doesn't update this file. +# Ultimately this means an old Gemfile.lock is left behind for a new package, +# and bundle won't run because it thinks it has the wrong versions of gems +rm -f Gemfile.lock + +BUILD_NUMBER=$BUILD_NUMBER exec bundle exec unicorn_rails -p $PORT -E production -c config/unicorn.rb # -D diff --git a/web/script/rails b/web/script/rails new file mode 100755 index 000000000..f8da2cffd --- /dev/null +++ b/web/script/rails @@ -0,0 +1,6 @@ +#!/usr/bin/env ruby +# This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application. + +APP_PATH = File.expand_path('../../config/application', __FILE__) +require File.expand_path('../../config/boot', __FILE__) +require 'rails/commands' diff --git a/web/spec/controllers/api_corporate_controller_spec.rb b/web/spec/controllers/api_corporate_controller_spec.rb new file mode 100644 index 000000000..aa44fdab1 --- /dev/null +++ b/web/spec/controllers/api_corporate_controller_spec.rb @@ -0,0 +1,31 @@ +require 'spec_helper' + +describe ApiCorporateController do + render_views + + + before(:each) do + CorpMailer.deliveries.clear + end + + describe "success" do + it "should work" do + post :feedback, :email => "seth@jamkazam.com", :body => "Hey could someone help me?" + response.should be_success + CorpMailer.deliveries.length.should == 1 + + end + end + + describe "fail" do + it "should fail due to bad email" do + post :feedback, :email => "seth", :body => "Hey could someone help me?" + response.status.should == 422 + CorpMailer.deliveries.length.should == 0 + error = JSON.parse(response.body) + error["errors"]["email"].should_not be_nil + error["errors"]["email"][0].include?("invalid").should be_true + end + end + +end diff --git a/web/spec/controllers/claimed_recordings_spec.rb b/web/spec/controllers/claimed_recordings_spec.rb new file mode 100644 index 000000000..6aecf0458 --- /dev/null +++ b/web/spec/controllers/claimed_recordings_spec.rb @@ -0,0 +1,103 @@ +require 'spec_helper' + +describe ApiClaimedRecordingsController do + render_views + + before(:each) do + S3Manager.set_unit_test + @user = FactoryGirl.create(:user) + @connection = FactoryGirl.create(:connection, :user => @user) + @instrument = FactoryGirl.create(:instrument, :description => 'a great instrument') + @track = FactoryGirl.create(:track, :connection => @connection, :instrument => @instrument) + @music_session = FactoryGirl.create(:music_session, :creator => @user, :musician_access => true) + @music_session.connections << @connection + @music_session.save + @recording = Recording.start(@music_session.id, @user) + @recording.stop + @recording.reload + @genre = FactoryGirl.create(:genre) + @recording.claim(@user, "name", @genre, true, true) + @recording.reload + @claimed_recording = @recording.claimed_recordings.first + end + + describe "GET 'show'" do + + it "should show the right thing when one recording just finished" do + controller.current_user = @user + get :show, :id => @claimed_recording.id +# puts response.body + response.should be_success + json = JSON.parse(response.body) + json.should_not be_nil + json["id"].should == @claimed_recording.id + json["name"].should == @claimed_recording.name + json["recording"]["id"].should == @recording.id + json["recording"]["mixes"].length.should == 0 + json["recording"]["band"].should be_nil + json["recorded_tracks"].length.should == 1 + json["recorded_tracks"].first["id"].should == @recording.recorded_tracks.first.id + json["recorded_tracks"].first["url"].should == @recording.recorded_tracks.first.url + json["recorded_tracks"].first["instrument"]["id"].should == @instrument.id + json["recorded_tracks"].first["user"]["id"].should == @user.id + end + + it "should show the right thing when one recording was just uploaded" do + @recording.recorded_tracks.first.upload_complete + controller.current_user = @user + get :show, :id => @claimed_recording.id + response.should be_success + json = JSON.parse(response.body) + json.should_not be_nil + json["recording"]["mixes"].length.should == 1 + json["recording"]["mixes"].first["id"].should == @recording.mixes.first.id + json["recording"]["mixes"].first["url"].should == @recording.mixes.first.url + json["recording"]["mixes"].first["is_completed"].should == false + end + + + it "should show the right thing when the mix was just uploaded" do + @recording.recorded_tracks.first.upload_complete + @mix = Mix.next("server") + @mix.finish(10000, "md5") + controller.current_user = @user + get :show, :id => @claimed_recording.id + response.should be_success + json = JSON.parse(response.body) + json.should_not be_nil + json["recording"]["mixes"].length.should == 1 + json["recording"]["mixes"].first["id"].should == @recording.mixes.first.id + json["recording"]["mixes"].first["is_completed"].should == true + end + end + + describe "GET 'index'" do + it "should generate a single output" do + controller.current_user = @user + get :index + response.should be_success + json = JSON.parse(response.body) + json.should_not be_nil + json.length.should == 1 + json.first["id"].should == @claimed_recording.id + + end + end + +=begin + We can't test these because rspec doesn't like that we return 204. It causes rails to return a 406. + describe "DELETE 'destroy'" do + it "should delete properly" do + controller.current_user = @user + delete :delete, :id => @claimed_recording.id + puts response + puts response.body + + response.should be_success + expect { ClaimedRecording.find(@claimed_recording.id) }.to raise_error + end + end +=end + + +end \ No newline at end of file diff --git a/web/spec/controllers/sessions_controller_spec.rb b/web/spec/controllers/sessions_controller_spec.rb new file mode 100644 index 000000000..f0de20a45 --- /dev/null +++ b/web/spec/controllers/sessions_controller_spec.rb @@ -0,0 +1,88 @@ +require 'spec_helper' + +describe SessionsController do + render_views + + describe "GET 'new'" do + it "should work" do + get :new + response.should be_success + end + + it "should have the right title" do + get :new + response.body.should have_title("JamKazam | Sign in") + end + end + + + describe "POST 'create'" do + before(:each) do + @user = FactoryGirl.create(:user) + @attr = { :email => @user.email, :password => @user.password } + end + + it "should sign the user in" do + post :create, :session => @attr + controller.current_user.should == @user + controller.signed_in?.should == true + end + + it "should redirect the user to the proper page" do + post :create, :session => @attr + response.should redirect_to(client_url) + end + + end + + describe "create_oauth" do + + before(:each) do + OmniAuth.config.mock_auth[:facebook] = OmniAuth::AuthHash.new({ + 'uid' => '100', + 'provider' => 'facebook', + 'info' => { + 'first_name' => 'FirstName', + 'last_name' => 'LastName', + 'email' => 'test_oauth@example.com', + 'location' => 'mylocation' + }, + 'credentials' => { + 'token' => 'facebooktoken', + 'expires_at' => 1000000000 + } + }) + end + + it "should create a user when oauth comes in with a non-currently existing user" do + pending "needs this fixed: https://jamkazam.atlassian.net/browse/VRFS-271" + request.env["omniauth.auth"] = OmniAuth.config.mock_auth[:facebook] + lambda do + visit '/auth/facebook' + end.should change(User, :count).by(1) + user = User.find_by_email('test_oauth@example.com') + user.should_not be_nil + user.first_name.should == "FirstName" + response.should be_success + + # also verify that a second visit does *not* create another new user + lambda do + visit '/auth/facebook' + end.should change(User, :count).by(0) + end + + + it "should not create a user when oauth comes in with a currently existing user" do + user = FactoryGirl.create(:user) # in the jam session + OmniAuth.config.mock_auth[:facebook][:info][:email] = user.email + OmniAuth.config.mock_auth[:facebook] = OmniAuth.config.mock_auth[:facebook] + + lambda do + visit '/auth/facebook' + end.should change(User, :count).by(0) + end + + end + + +end diff --git a/web/spec/controllers/users_controller_spec.rb b/web/spec/controllers/users_controller_spec.rb new file mode 100644 index 000000000..e69de29bb diff --git a/web/spec/factories.rb b/web/spec/factories.rb new file mode 100644 index 000000000..78040b55f --- /dev/null +++ b/web/spec/factories.rb @@ -0,0 +1,126 @@ +include ActionDispatch::TestProcess # added for artifact_update http://stackoverflow.com/questions/5990835/factory-with-carrierwave-upload-field + +FactoryGirl.define do + factory :user, :class => JamRuby::User do + sequence(:email) { |n| "person_#{n}@example.com"} + sequence(:first_name) { |n| "Person" } + sequence(:last_name) { |n| "#{n}" } + password "foobar" + password_confirmation "foobar" + email_confirmed true + musician true + city "Apex" + state "NC" + country "USA" + terms_of_service true + + + factory :admin do + admin true + end + + before(:create) do |user| + user.musician_instruments << FactoryGirl.build(:musician_instrument, user: user) + end + + factory :single_user_session do + after(:create) do |user, evaluator| + music_session = FactoryGirl.create(:music_session, :creator => user) + connection = FactoryGirl.create(:connection, :user => user, :music_session => music_session) + end + end + end + + factory :fan, :class => JamRuby::User do + sequence(:first_name) { |n| "Person" } + sequence(:last_name) { |n| "#{n}" } + sequence(:email) { |n| "fan_#{n}@example.com"} + password "foobar" + password_confirmation "foobar" + email_confirmed true + musician false + city "Apex" + state "NC" + country "USA" + terms_of_service true + end + + factory :invited_user, :class => JamRuby::InvitedUser do + sequence(:email) { |n| "user#{n}@someservice.com" } + autofriend false + end + + factory :music_session, :class => JamRuby::MusicSession do + sequence(:description) { |n| "Music Session #{n}" } + fan_chat true + fan_access true + approval_required false + musician_access true + legal_terms true + + after(:create) { |session| + MusicSessionHistory.save(session) + } + end + + + factory :connection, :class => JamRuby::Connection do + ip_address "1.1.1.1" + as_musician true + sequence(:client_id) { |n| "client_id#{n}"} + end + + factory :friendship, :class => JamRuby::Friendship do + + end + + factory :invitation, :class => JamRuby::Invitation do + + end + + factory :band, :class => JamRuby::Band do + sequence(:name) { |n| "Band" } + biography "Established 1978" + city "Apex" + state "NC" + country "USA" + end + + factory :join_request, :class => JamRuby::JoinRequest do + text 'let me in to the session!' + end + + factory :genre, :class => JamRuby::Genre do + description { |n| "Genre #{n}" } + end + + factory :instrument, :class => JamRuby::Instrument do + description { |n| "Instrument #{n}" } + end + + factory :artifact_update, :class => JamRuby::ArtifactUpdate do + sequence(:version) { |n| "0.1.#{n}" } + uri { fixture_file_upload("#{Rails.root.to_s}/spec/fixtures/files/jkclient.exe", "application/x-msdownload") } + product { "JamClient/Win32" } + environment { "public" } + sha1 { "blurp" } + size { 20 } + end + + factory :musician_instrument, :class=> JamRuby::MusicianInstrument do + instrument { Instrument.find('electric guitar') } + proficiency_level 1 + priority 0 + end + + factory :track, :class => JamRuby::Track do + sound "mono" + + end + + factory :recorded_track, :class => JamRuby::RecordedTrack do + end + + + +end diff --git a/web/spec/features/account_spec.rb b/web/spec/features/account_spec.rb new file mode 100644 index 000000000..a2c229c35 --- /dev/null +++ b/web/spec/features/account_spec.rb @@ -0,0 +1,109 @@ +require 'spec_helper' + +describe "Account", :js => true, :type => :feature, :capybara_feature => true do + + subject { page } + + before(:all) do + Capybara.javascript_driver = :poltergeist + Capybara.current_driver = Capybara.javascript_driver + Capybara.default_wait_time = 10 + end + + let(:user) { FactoryGirl.create(:user) } + + before(:each) do + UserMailer.deliveries.clear + sign_in_poltergeist user + visit "/#/account" + + find('div.account-mid.identity') + end + + it { should have_selector('h1', text: 'my account') } + + describe "identity" do + before(:each) do + find("#account-edit-identity-link").trigger(:click) + end + + it { should have_selector('h2', text: 'identity:' ) } + it { should have_selector('form#account-edit-email-form h4', text: 'Update your email address:') } + it { should have_selector('form#account-edit-password-form h4', text: 'Update your password:') } + + describe "update email" do + + before(:each) do + find('form#account-edit-email-form h4', text: 'Update your email address:') + fill_in "account_update_email", with: "junk@jamkazam.com" + #sleep 1 + find("#account-edit-email-submit").trigger(:click) + fill_in "update-email-confirm-password", with: user.password + find("#account-edit-email-confirm-password-submit").trigger(:click) + + end + + it { should have_selector('h1', text: 'my account') } + it { should have_selector('#notification h2', text: 'Confirmation Email Sent') } + end + + describe "update password" do + + describe "successfully" do + + before(:each) do + fill_in "current_password", with: user.password + fill_in "password", with: user.password + fill_in "password_confirmation", with: user.password + find("#account-edit-password-submit").trigger(:click) + end + + it { should have_selector('h1', text: 'my account') } + end + + describe "unsuccessfully" do + + before(:each) do + find("#account-edit-password-submit").trigger(:click) + end + + it { should have_selector('h2', text: 'identity:') } + it { should have_selector('div.field.error input[name=current_password] ~ ul li', text: "can't be blank") } + it { should have_selector('div.field.error input[name=password] ~ ul li', text: "is too short (minimum is 6 characters)") } + it { should have_selector('div.field.error input[name=password_confirmation] ~ ul li', text: "can't be blank") } + end + end + + describe "profile" + + before(:each) do + find("#account-edit-profile-link").trigger(:click) + find('a.small', text: 'Change Avatar') + end + + describe "successfully" do + + before(:each) do + fill_in "first_name", with: "Bobby" + fill_in "last_name", with: "Toes" + find("#account-edit-profile-submit").trigger(:click) + end + + it { should have_selector('h1', text: 'my account') } + it { should have_selector('#notification h2', text: 'Profile Changed') } + end + + describe "unsuccessfully" do + + before(:each) do + fill_in "first_name", with: "" + fill_in "last_name", with: "" + find("#account-edit-profile-submit").trigger(:click) + end + + it { should have_selector('h2', text: 'profile:') } + it { should have_selector('div.field.error input[name=first_name] ~ ul li', text: "can't be blank") } + it { should have_selector('div.field.error input[name=last_name] ~ ul li', text: "can't be blank") } + end + end +end diff --git a/web/spec/features/authentication_pages_spec.rb b/web/spec/features/authentication_pages_spec.rb new file mode 100644 index 000000000..93c72693e --- /dev/null +++ b/web/spec/features/authentication_pages_spec.rb @@ -0,0 +1,107 @@ +require 'spec_helper' + +describe "Authentication" do + + subject { page } + + describe "signin page" do + before { visit signin_path } + + it { should have_selector('h1', text: 'sign in or register') } + it { page.should have_title("JamKazam | Sign in") } + end + + describe "signin" do + before { visit signin_path } + + describe "with invalid information" do + before { click_button "SIGN IN" } + + it { page.should have_title("JamKazam | Sign in") } + it { should have_selector('div.login-error-msg', text: 'Invalid login') } + + #describe "after visiting another page" do + #before { click_link "Home" } + #it { should_not have_selector('div.alert.alert-error') } + #end + end + + describe "with valid information" do + let(:user) { FactoryGirl.create(:user) } + before do + fill_in "Email", with: user.email + fill_in "Password", with: user.password + click_button "SIGN IN" + end + + # Successful sign-in goes to the client + it { page.should have_title("JamKazam") } + it { should have_selector('h1', text: "audio gear setup") } + end + end + + describe "authorization" do + + describe "for non-signed-in users" do + let(:user) { FactoryGirl.create(:user) } + + describe "when attempting to visit a protected page" do + before do + visit edit_user_path(user) + fill_in "Email", with: user.email + fill_in "Password", with: user.password + click_button "SIGN IN" + end + + describe "after signing in" do + + it "should render the desired protected page" do + page.should have_title("JamKazam | Edit user") + end + + describe "when signing in again" do + before do + visit signin_path + fill_in "Email", with: user.email + fill_in "Password", with: user.password + click_button "SIGN IN" + end + + it "should render the signed-in client page" do + # it now goes to /music_sessions + page.should have_title("JamKazam") + page.should have_selector('h1', text: "audio gear setup") + end + end + end + end + + describe "in the Users controller" do + + describe "visiting the edit page" do + before { visit edit_user_path(user) } + it { page.should have_title("JamKazam | Sign in") } + end + + describe "visiting user index" do + before { visit users_path } + it { page.should have_title("JamKazam | Sign in") } + end + end + end + + describe "as wrong user" do + let(:user) { FactoryGirl.create(:user) } + let(:wrong_user) { FactoryGirl.create(:user, email: "wrong@example.com") } + before { sign_in user } + + describe "visiting Users#edit page" do + before { visit edit_user_path(wrong_user) } + it { + pending "this should work, but right now you get redirected to ftue" + page.should have_title('Action Controller: Exception caught') + } + end + end + end +end diff --git a/web/spec/features/connection_states_spec.rb b/web/spec/features/connection_states_spec.rb new file mode 100644 index 000000000..c8e0a468e --- /dev/null +++ b/web/spec/features/connection_states_spec.rb @@ -0,0 +1,47 @@ +require 'spec_helper' + +if defined?(TEST_CONNECT_STATES) && TEST_CONNECT_STATES + describe "ConnectionStates", :js => true, :type => :feature, :capybara_feature => true do + + before(:all) do + Capybara.javascript_driver = :poltergeist + Capybara.current_driver = Capybara.javascript_driver + @user = FactoryGirl.create(:user) + end + + it "visits the connection_state test page and let it run its cycle", :js => true do + visit "/test_connection?user=#{@user.email}&password=foobar" + page.status_code.should be(200) + + # sleep for the duration of stale+expire delay to give browser time to run through the JS + sleep_dur = Rails.application.config.websocket_gateway_connect_time_stale + + Rails.application.config.websocket_gateway_connect_time_expire + # add 1 second each for stale and expire dur used in test_connection; plus 10% buffer + sleep_dur = (sleep_dur + 2) * 1.1 + $stdout.puts("*** sleeping for: #{sleep_dur} seconds to allow browser JS to run") + sleep(sleep_dur) + + # FIXME: The next step is to process the JS console output and raise assertions + # as appropriate; there is currently a database problem wherein inserted Connection records + # are not found after login; it's prolly an issue with db transactions, but will require more + # debugging to determine the cause. The connection row is created properly in the login process + # but when creating music_session, the connection is not found. + + File.exists?(TEST_CONNECT_STATE_JS_CONSOLE).should be_true + TEST_CONNECT_STATE_JS_CONSOLE_IO.flush + + jsfunctions = %W{ myLoggedIn createMusicSession isStale isExpired } + jsconsole = File.read(TEST_CONNECT_STATE_JS_CONSOLE) + jsconsole.split("\n").each do |line| + next unless line =~ /^#{Regexp.escape(TEST_CONNECT_STATE_JS_LOG_PREFIX)}/ + # $stdout.puts("*** console line = #{line}") + /ERROR/.match(line).should be_nil + + # FIXME: do more validation of console output here... + jsfunctions.delete_if { |fcn| line =~ /#{fcn}/ } + end + jsfunctions.count.should == 0 + end + + end +end diff --git a/web/spec/features/regression_spec.rb b/web/spec/features/regression_spec.rb new file mode 100644 index 000000000..e69de29bb diff --git a/web/spec/features/signup_spec.rb b/web/spec/features/signup_spec.rb new file mode 100644 index 000000000..6d7e19c18 --- /dev/null +++ b/web/spec/features/signup_spec.rb @@ -0,0 +1,161 @@ +require 'spec_helper' + +describe "Signup" do + + subject { page } + + before(:each) do + @mac_client = FactoryGirl.create(:artifact_update) + UserMailer.deliveries.clear + end + + describe "signup page" do + before { visit signup_path } + + it { should have_selector('h1', text: 'create a jamkazam account') } + + describe "with valid musician information" do + before do + fill_in "jam_ruby_user[first_name]", with: "Mike" + fill_in "jam_ruby_user[last_name]", with: "Jones" + fill_in "jam_ruby_user[email]", with: "noone@jamkazam.com" + fill_in "jam_ruby_user[password]", with: "jam123" + fill_in "jam_ruby_user[password_confirmation]", with: "jam123" + check("jam_ruby_user[instruments][drums][selected]") + check("jam_ruby_user[terms_of_service]") + click_button "CREATE ACCOUNT" + end + + # Successful signup with no invitation tells you to go sign up + it { page.should have_title("JamKazam | Congratulations") } + it { should have_selector('.overlay-inner', text: "You have successfully registered as a JamKazam musician.") } + it { User.find_by_email('noone@jamkazam.com').musician_instruments.length.should == 1 } + # an email is sent on no-invite signup + it { UserMailer.deliveries.length.should == 1 } + end + + describe "with valid fan information" do + before do + fill_in "jam_ruby_user[first_name]", with: "Mike" + fill_in "jam_ruby_user[last_name]", with: "Jones" + fill_in "jam_ruby_user[email]", with: "somefan@jamkazam.com" + fill_in "jam_ruby_user[password]", with: "jam123" + fill_in "jam_ruby_user[password_confirmation]", with: "jam123" + choose "jam_ruby_user_musician_false" + + check("jam_ruby_user[terms_of_service]") + click_button "CREATE ACCOUNT" + end + + # Successful signup with no invitation tells you to go sign up + it { page.should have_title("JamKazam | Congratulations") } + it { should have_selector('.overlay-inner', text: "You have successfully registered as a JamKazam fan.") } + it { should have_selector('a.button-orange.m0', text: 'PROCEED TO JAMKAZAM SITE') } + it { User.find_by_email('somefan@jamkazam.com').musician_instruments.length.should == 0 } + # an email is sent on no-invite signup + it { UserMailer.deliveries.length.should == 1 } + end + + describe "with service invite" do + before do + @invited_user = FactoryGirl.create(:invited_user, :email => "noone@jamkazam.com") + visit "#{signup_path}?invitation_code=#{@invited_user.invitation_code}" + + UserMailer.deliveries.clear + fill_in "jam_ruby_user[first_name]", with: "Mike" + fill_in "jam_ruby_user[last_name]", with: "Jones" + fill_in "jam_ruby_user[email]", with: "noone@jamkazam.com" + fill_in "jam_ruby_user[password]", with: "jam123" + fill_in "jam_ruby_user[password_confirmation]", with: "jam123" + check("jam_ruby_user[instruments][drums][selected]") + check("jam_ruby_user[terms_of_service]") + click_button "CREATE ACCOUNT" + end + + # Successful sign-in goes to the client + it { page.should have_title("JamKazam") } + it { should have_selector('h1', text: "congratulations") } + + # there is no email sent though when you signup based on an invite (because you just left your email to get here) + it { UserMailer.deliveries.length.should == 0 } + end + + describe "with user invite and autofriend" do + before do + @user = FactoryGirl.create(:user) + @invited_user = FactoryGirl.create(:invited_user, :sender => @user, :autofriend => true, :email => "noone@jamkazam.com") + visit "#{signup_path}?invitation_code=#{@invited_user.invitation_code}" + + fill_in "jam_ruby_user[first_name]", with: "Mike" + fill_in "jam_ruby_user[last_name]", with: "Jones" + fill_in "jam_ruby_user[email]", with: "noone@jamkazam.com" + fill_in "jam_ruby_user[password]", with: "jam123" + fill_in "jam_ruby_user[password_confirmation]", with: "jam123" + check("jam_ruby_user[instruments][drums][selected]") + check("jam_ruby_user[terms_of_service]") + click_button "CREATE ACCOUNT" + end + + # Successful sign-in goes to the client + it { page.should have_title("JamKazam") } + it { should have_selector('h1', text: "congratulations") } + it { @user.friends?(User.find_by_email("noone@jamkazam.com")) } + it { User.find_by_email("noone@jamkazam.com").friends?(@user) } + end + + describe "can't signup to the same invite twice" do + before do + @invited_user = FactoryGirl.create(:invited_user, :email => "noone@jamkazam.com") + visit "#{signup_path}?invitation_code=#{@invited_user.invitation_code}" + + fill_in "jam_ruby_user[first_name]", with: "Mike" + fill_in "jam_ruby_user[last_name]", with: "Jones" + fill_in "jam_ruby_user[email]", with: "noone@jamkazam.com" + fill_in "jam_ruby_user[password]", with: "jam123" + fill_in "jam_ruby_user[password_confirmation]", with: "jam123" + check("jam_ruby_user[instruments][drums][selected]") + check("jam_ruby_user[terms_of_service]") + click_button "CREATE ACCOUNT" + page.should have_title("JamKazam") + should have_selector('h1', text: "congratulations") + visit "#{signup_path}?invitation_code=#{@invited_user.invitation_code}" + end + + it { should have_selector('h1', text: "You have already signed up with this invitation") } + + end + + describe "can signup with an email different than the one used to invite" do + before do + @invited_user = FactoryGirl.create(:invited_user, :email => "what@jamkazam.com") + visit "#{signup_path}?invitation_code=#{@invited_user.invitation_code}" + + UserMailer.deliveries.clear + + fill_in "jam_ruby_user[first_name]", with: "Mike" + fill_in "jam_ruby_user[last_name]", with: "Jones" + fill_in "jam_ruby_user[email]", with: "noone@jamkazam.com" + fill_in "jam_ruby_user[password]", with: "jam123" + fill_in "jam_ruby_user[password_confirmation]", with: "jam123" + check("jam_ruby_user[instruments][drums][selected]") + check("jam_ruby_user[terms_of_service]") + click_button "CREATE ACCOUNT" + end + + it { page.should have_title("JamKazam | Congratulations") } + it { should have_selector('.overlay-inner', text: "You have successfully registered as a JamKazam musician.") } + it { User.find_by_email('noone@jamkazam.com').musician_instruments.length.should == 1 } + it { User.find_by_email('what@jamkazam.com').should be_nil } + # an email is sent when you invite but use a different email than the one used to invite + it { UserMailer.deliveries.length.should == 1 } + it { + visit "#{signup_path}?invitation_code=#{@invited_user.invitation_code}" + should have_selector('h1', text: "You have already signed up with this invitation") + } + + end + + + end + +end diff --git a/web/spec/fixtures/files/jkclient.exe b/web/spec/fixtures/files/jkclient.exe new file mode 100644 index 000000000..b076f565f --- /dev/null +++ b/web/spec/fixtures/files/jkclient.exe @@ -0,0 +1,2 @@ +Some serious binary stuff is in here. +010101010 beep boop 01010101 \ No newline at end of file diff --git a/web/spec/helpers/application_helper_spec.rb b/web/spec/helpers/application_helper_spec.rb new file mode 100644 index 000000000..5549737c1 --- /dev/null +++ b/web/spec/helpers/application_helper_spec.rb @@ -0,0 +1,18 @@ +require 'spec_helper' + +describe ApplicationHelper do + + describe "full_title" do + it "should include the page name" do + full_title("foo").should =~ /foo/ + end + + it "should includes the base name" do + full_title("foo").should =~ /^JamKazam/ + end + + it "should not include a bar for the home page" do + full_title("").should_not =~ /\|/ + end + end +end diff --git a/web/spec/javascripts/callbackReceiver.spec.js b/web/spec/javascripts/callbackReceiver.spec.js new file mode 100644 index 000000000..404c97730 --- /dev/null +++ b/web/spec/javascripts/callbackReceiver.spec.js @@ -0,0 +1,48 @@ +(function(context, $) { + describe("Callbacks", function() { + describe("makeStatic", function() { + it("should create static function which invokes instance function", function() { + var MyObj = function() { + this.hey = function() { return "hey"; }; + }; + var myInst = new MyObj(); + + context.JK.Callbacks.makeStatic("callHey", myInst.hey); + var result = context.JK.Callbacks.callHey(); + expect(result).toEqual("hey"); + }); + }); + + describe("invocation", function() { + it("should pass arguments", function() { + var MyObj = function() { + this.addTwo = function(input) { return input+2; }; + }; + var myInst = new MyObj(); + + context.JK.Callbacks.makeStatic("addTwo", myInst.addTwo); + var result = context.JK.Callbacks.addTwo(5); + expect(result).toEqual(7); + + }); + }); + + describe("context", function() { + it("should set 'this' to provided context", function() { + var MyObj = function() { + this.counter = 0; + this.addToCounter = function(incAmount) { + this.counter += incAmount; + }; + }; + var myInst = new MyObj(); + + context.JK.Callbacks.makeStatic("incInstanceCounter", myInst.addToCounter, myInst); + context.JK.Callbacks.incInstanceCounter(2); + context.JK.Callbacks.incInstanceCounter(3); + expect(myInst.counter).toEqual(5); + }); + }); + + }); + })(window, jQuery); \ No newline at end of file diff --git a/web/spec/javascripts/createSession.spec.js b/web/spec/javascripts/createSession.spec.js new file mode 100644 index 000000000..37e755dc6 --- /dev/null +++ b/web/spec/javascripts/createSession.spec.js @@ -0,0 +1,164 @@ +(function(context,$) { + + describe("CreateSession", function() { + + var css; + var ajaxSpy; + + var appFake = { + clientId: '12345', + notify: function(){}, + ajaxError: function() { console.debug("ajaxError"); } + }; + + var selectors = { + form: '#create-session-form', + genres: '#genre-list', + description: '#description' + }; + + function makeValid() { + //var genre = '
        1
        '; + $(selectors.genres).val('african'); + //$(selectors.genres, $(selectors.form)).append(genre); + $(selectors.description).val('XYZ'); + } + + beforeEach(function() { + // Use the actual screen markup + jasmine.getFixtures().fixturesPath = '/app/views/clients'; + loadFixtures('_createSession.html.erb'); + spyOn(appFake, 'notify'); + css = new context.JK.CreateSessionScreen(appFake); + css.getGenreSelector().initialize('Choose up to 3 genres', 3, $(selectors.form)); + }); + + describe("resetForm", function() { + it("description should be empty", function() { + $(selectors.description).val('XYZ'); + css.resetForm(); + expect($(selectors.description).val()).toEqual(''); + }); + }); + + describe("loadGenres", function() { + beforeEach(function() { + spyOn($, "ajax").andCallFake(function(opts) { + opts.success(TestResponses.loadGenres); + }); + }); + it("should populate genres select", function() { + css.loadGenres(); + $genres = $(selectors.genres); + alert($genres.html()); + expect($genres.length).toEqual(2); + }); + }); + + describe("submitForm", function() { + + var fakeEvt; + var passedData = {}; + + beforeEach(function() { + makeValid(); + fakeEvt = { + preventDefault: $.noop, + currentTarget: $(selectors.form) + }; + spyOn($, "ajax").andCallFake(function(opts) { + opts.success(TestResponses.sessionPost); + }); + css.submitForm(fakeEvt); + passedData = JSON.parse($.ajax.mostRecentCall.args[0].data); + }); + + it("should pass client_id", function() { + expect(passedData.client_id).toEqual("12345"); + }); + + it("should pass genres as non-empty list", function() { + expect("genres" in passedData).toBeTruthy(); + var isArray = $.isArray(passedData.genres); + expect(isArray).toBeTruthy(); + expect(passedData.genres.length).toBeGreaterThan(0); + }); + + it("should pass tracks as non-empty list", function() { + expect("tracks" in passedData).toBeTruthy(); + var isArray = $.isArray(passedData.tracks); + expect(isArray).toBeTruthy(); + expect(passedData.tracks.length).toBeGreaterThan(0); + }); + + it("should pass musician_access as boolean", function() { + expect("musician_access" in passedData).toBeTruthy(); + expect(typeof(passedData.musician_access)).toEqual("boolean"); + }); + + it("should pass approval_required as boolean", function() { + expect("approval_required" in passedData).toBeTruthy(); + expect(typeof(passedData.approval_required)).toEqual("boolean"); + }); + + it("should pass fan_access as boolean", function() { + expect("fan_access" in passedData).toBeTruthy(); + expect(typeof(passedData.fan_access)).toEqual("boolean"); + }); + + it("should pass fan_chat as boolean", function() { + expect("fan_chat" in passedData).toBeTruthy(); + expect(typeof(passedData.fan_chat)).toEqual("boolean"); + }); + + }); + + describe("validateForm", function() { + + beforeEach(function() { + makeValid(); + }); + + it("valid form", function() { + var errs = css.validateForm(); + expect(errs).toBeNull(); + }); + + // it("should fail with > 3 genres", function() { + // var htm = '
        2
        ' + + // '
        3
        ' + + // '
        4
        ' + + // '
        5
        '; + // $(selectors.genres, $(selectors.form)).append(htm); + // var errs = css.validateForm(); + // // Verify that we have an error. + // expect(errs).toBeTruthy(); + // // Verify that the error is a two-part list + // expect(errs[0].length).toEqual(2); + // // Verify that the first part is a selector for the problem. + // expect(errs[0][0]).toEqual('#genre-list-items'); + // }); + + it("should fail with 0 genres", function() { + $(selectors.genres, $(selectors.form)).html(''); + var errs = css.validateForm(); + // Verify that we have an error. + expect(errs).toBeTruthy(); + // Verify that the first part is a selector for the problem. + expect(errs[0][0]).toEqual('#genre-list'); + }); + + it("should fail with empty description", function() { + $(selectors.description).val(''); + var errs = css.validateForm(); + // Verify that we have an error. + expect(errs).toBeTruthy(); + // Verify that the first part is a selector for the problem. + expect(errs[0][0]).toEqual('#description'); + }); + + }); + + }); + + }) // Intentionally not running tests as they're failing. (window, jQuery); \ No newline at end of file diff --git a/web/spec/javascripts/faderHelpers.spec.js b/web/spec/javascripts/faderHelpers.spec.js new file mode 100644 index 000000000..de8eabacb --- /dev/null +++ b/web/spec/javascripts/faderHelpers.spec.js @@ -0,0 +1,37 @@ + +(function(g,$) { + + describe("faderHelpers tests", function() { + + beforeEach(function() { + JKTestUtils.loadFixtures('/base/app/views/clients/_faders.html.erb'); + JKTestUtils.loadFixtures('/base/spec/javascripts/fixtures/faders.htm'); + }); + + describe("renderVU", function() { + + describe("with defaults", function() { + it("should add vertical fader to selector", function() { + JK.FaderHelpers.renderFader('#fader', {faderId:'a'}); + $fader = $('#fader div[control="fader"]'); + orientation = $fader.attr('orientation'); + expect($fader.length).toEqual(1); + expect(orientation).toEqual('vertical'); + }); + }); + + describe("horizontal", function() { + it("should add horizontal fader to selector", function() { + JK.FaderHelpers.renderFader('#fader', {faderId:'a',faderType: "horizontal"}); + $fader = $('#fader div[control="fader"]'); + orientation = $fader.attr('orientation'); + expect($fader.length).toEqual(1); + expect(orientation).toEqual('horizontal'); + }); + }); + + }); + + }); + +})(window, jQuery); \ No newline at end of file diff --git a/web/spec/javascripts/findSession.spec.js b/web/spec/javascripts/findSession.spec.js new file mode 100644 index 000000000..9e8d27f87 --- /dev/null +++ b/web/spec/javascripts/findSession.spec.js @@ -0,0 +1,144 @@ +(function(context,$) { + + describe("FindSession", function() { + + var fss; + + var appFake = { + clientId: '12345', + bindScreen: function(){}, + notify: function(){}, + ajaxError: function() { console.debug("ajaxError"); } + }; + + var jamClientFake = { + TestLatency: function(clientID, fnName) { + var js = fnName + '(' + JSON.stringify({clientID: clientID, latency: 50}) + ');'; + eval(js); + } + }; + + beforeEach(function() { + fss = null; + // Use the actual screen markup + JKTestUtils.loadFixtures('/base/app/views/clients/_findSession.html.erb'); + spyOn(appFake, 'notify'); + }); + + var sessionLatencyReal; + + describe("RealSessionLatency", function() { + beforeEach(function() { + sessionLatencyReal = new JK.SessionLatency(jamClientFake); + spyOn(sessionLatencyReal, 'sessionPings').andCallThrough(); + fss = new context.JK.FindSessionScreen(appFake); + fss.initialize(sessionLatencyReal); + fss.clearResults(); + }); + + describe("loadSessions", function() { + beforeEach(function() { + spyOn($, "ajax"); + }); + it("should query ajax for sessions", function() { + fss.afterShow({}); + expect($.ajax).toHaveBeenCalled(); + }); + }); + + /*describe("afterShow flow", function() { + beforeEach(function() { + spyOn($, "ajax").andCallFake(function(opts) { + opts.success(TestGetSessionResponses.oneOfEach); + }); + }); + + it("should output table rows for sessions", function() { + fss.afterShow({}); + expect($(fss.getCategoryEnum().INVITATION.id + ' tr').length).toEqual(5); + }); + + it("should call sessionPings", function() { + fss.afterShow({}); + expect(sessionLatencyReal.sessionPings).toHaveBeenCalled(); + }); + + });*/ + }); + + describe("FakeSessionLatency", function() { + + beforeEach(function() { + sessionInfoResponses = { + "1": {id:"1", sortScore: 3}, + "2": {id:"2", sortScore: 2} + }; + sessionLatencyFake = { + sessionInfo: null + }; + spyOn(sessionLatencyFake, 'sessionInfo').andCallFake(function(id) { + return sessionInfoResponses[id]; + }); + fss = new context.JK.FindSessionScreen(appFake); + fss.initialize(sessionLatencyFake); + fss.clearResults(); + }); + + /*describe("renderSession", function() { + + describe("layout", function() { + var tbody; + + beforeEach(function() { + var session = TestGetSessionResponses.oneOfEach[0]; + alert(JSON.parse(session)); + fss.setSession(session); + fss.renderSession(session.id); + tbody = $(fss.getCategoryEnum().INVITATION.id); + }); + + it("single session should render", function() { + expect($('tr', tbody).length).toEqual(1); + }); + + it("Should render genre", function() { + expect($('tr td', tbody).first().text()).toEqual('classical'); + }); + + it("Should render description", function() { + expect($('tr td', tbody).first().next().text()).toEqual('Invited'); + }); + + it("Should render musician count", function() { + expect($('tr td', tbody).first().next().next().text()).toEqual('1'); + }); + + // TODO - test audience + // TODO - test latency + // TODO - test Listen + + // it("Should render join link", function() { + // expect($('tr td', tbody).last().text()).toEqual('JOIN'); + // }); + + }); + + it("higher sortScore inserted before lower sortScore", function() { + var sessionLow = TestGetSessionResponses.oneOfEach[1]; + var sessionHigh = TestGetSessionResponses.oneOfEach[0]; + fss.setSession(sessionLow); + fss.setSession(sessionHigh); + fss.renderSession(sessionLow.id); + fss.renderSession(sessionHigh.id); + + var tbody = $(fss.getCategoryEnum().INVITATION.id); + expect($('tr', tbody).length).toEqual(2); + expect($('tr', tbody).first().attr('data-sortScore')).toEqual('3'); + expect($('tr', tbody).first().next().attr('data-sortScore')).toEqual('2'); + }); + });*/ + }); + + }); + + })(window,jQuery); \ No newline at end of file diff --git a/web/spec/javascripts/fixtures/faders.htm b/web/spec/javascripts/fixtures/faders.htm new file mode 100644 index 000000000..1156b98fa --- /dev/null +++ b/web/spec/javascripts/fixtures/faders.htm @@ -0,0 +1 @@ +
        \ No newline at end of file diff --git a/web/spec/javascripts/fixtures/formToObject.htm b/web/spec/javascripts/fixtures/formToObject.htm new file mode 100644 index 000000000..28822195d --- /dev/null +++ b/web/spec/javascripts/fixtures/formToObject.htm @@ -0,0 +1,57 @@ + +
        +
        +
        +
        + + +
        +
        + +
        +
        + +
        +
        + + +
        +
        + + +
        + + +
        + +
        + + + + + +
        + +
        + + + + + +
        + +
        + +
        + + +
        + + +
        diff --git a/web/spec/javascripts/fixtures/searcher.htm b/web/spec/javascripts/fixtures/searcher.htm new file mode 100644 index 000000000..9992a1f20 --- /dev/null +++ b/web/spec/javascripts/fixtures/searcher.htm @@ -0,0 +1,24 @@ + + + + + + + + \ No newline at end of file diff --git a/web/spec/javascripts/fixtures/vuHelpers.htm b/web/spec/javascripts/fixtures/vuHelpers.htm new file mode 100644 index 000000000..eb7dbcc04 --- /dev/null +++ b/web/spec/javascripts/fixtures/vuHelpers.htm @@ -0,0 +1 @@ +
        \ No newline at end of file diff --git a/web/spec/javascripts/formToObject.spec.js b/web/spec/javascripts/formToObject.spec.js new file mode 100644 index 000000000..8a6d72fd3 --- /dev/null +++ b/web/spec/javascripts/formToObject.spec.js @@ -0,0 +1,144 @@ +(function(global) { + + describe("jquery.formToObject tests", function() { + + beforeEach(function() { + JKTestUtils.loadFixtures('/base/spec/javascripts/fixtures/formToObject.htm'); + }); + + describe("Top level", function() { + + describe("text input named foo, val bar", function() { + it("should have variable named foo, val bar", function() { + var o = $('#test1').formToObject(); + expect(o.foo).toEqual("bar"); + }); + }); + + describe("text area named foo, contents bar", function() { + it("should have variable named foo, val bar", function() { + var o = $('#test1a').formToObject(); + expect(o.foo).toEqual("bar"); + }); + }); + + describe("single checkbox named foo, val bar", function() { + it("should have variable named foo, val bar", function() { + var o = $('#test2').formToObject(); + expect(o.foo).toEqual("bar"); + }); + }); + + describe("multi checkboxes named foo, vals 'a' and 'b'", function() { + it("should have variable named foo, val ['a','b']", function() { + var o = $('#test3').formToObject(); + expect(o.foo).toEqual(['a','b']); + }); + }); + + describe("select named foo, selected op val 'a'", function() { + it("should have variable named foo, val 'a'", function() { + var o = $('#test4').formToObject(); + expect(o.foo).toEqual('a'); + }); + }); + + describe("multiselect named foo, selected opts vals 'a', 'c'", function() { + it("should have variable named foo, val ['a','c']", function() { + var o = $('#test5').formToObject(); + expect(o.foo).toEqual(['a','c']); + }); + }); + + describe("single radio named foo, val bar", function() { + it("should have variable named foo, val bar", function() { + var o = $('#test6').formToObject(); + expect(o.foo).toEqual("bar"); + }); + }); + + }); + + + describe("Second level", function() { + + describe("text input named foo.bar, val 'x'", function() { + it("should have object foo with prop bar, value 'x'", function() { + var o = $('#test7').formToObject(); + expect(o.foo.bar).toEqual("x"); + }); + }); + + describe("multi checkboxes named foo.bar, vals 'a' and 'b'", function() { + it("should have object named foo, prop bar, val ['a','b']", function() { + var o = $('#test9').formToObject(); + expect(o.foo.bar).toEqual(['a','b']); + }); + }); + + + }); + + describe("Third level", function() { + + describe("text input named foo.bar.spam, val 'x'", function() { + it("should have object foo with obj bar, with prop spam, value 'x'", function() { + var o = $('#test8').formToObject(); + expect(o.foo.bar.spam).toEqual("x"); + }); + }); + + }); + + + describe("Mixed cases", function() { + + describe("flipping in and out of hierarchy", function() { + it("should properly capture all properties", function() { + var o = $('#test10').formToObject(); + expect(o.foo.sponge).toEqual("99"); + expect(o.foo.bar.potato).toEqual("mashed"); + expect(o.apple).toEqual("sweet"); + expect(o.foo.bar.strawberry).toEqual("ripe"); + expect(o.foo.bar.kentucky.bluegrass.yards).toEqual("north"); + }); + }); + + }); + + describe("Bad cases", function() { + describe("Overriding object with property", function() { + it("should raise an error", function() { + expect( function() { + $('#error1').formToObject(); + }).toThrow("Can't overwrite named structure"); + }); + }); + }); + + describe("__OMIT__ special value", function() { + describe("When value for key is __OMIT__", function() { + it("should remove key from object", function() { + var o = $('#OMIT').formToObject(); + expect(o.foo.sponge).toEqual("99"); + expect(o.foo.bar.potato).toEqual("mashed"); + expect("apple" in o).toBeFalsy('apple not in o'); + // would be foo.bar.kentucky.bluegrass.yard - all empty should be gone: + expect("kentucky" in o.foo.bar).toBeFalsy('kentucky not in bar'); + }); + }); + + describe("When resulting formToObject is {}", function() { + it("should return null instead of {}", function() { + var o = $('#OMIT_ALL').formToObject(); + expect(o).toBeNull(); + + }); + + }); + }); + + + }); + +})(window); \ No newline at end of file diff --git a/web/spec/javascripts/ftue.spec.js b/web/spec/javascripts/ftue.spec.js new file mode 100644 index 000000000..847c6199a --- /dev/null +++ b/web/spec/javascripts/ftue.spec.js @@ -0,0 +1,99 @@ +// Unit tests for things related to FTUE + +(function(g,$) { + + describe("ftue tests", function() { + + var ftue = null; + + var appFake = { + clientId: '12345', + bindScreen: function(){}, + notify: function(){}, + ajaxError: function() { console.debug("ajaxError"); } + }; + + beforeEach(function() { + g.jamClient = new JK.FakeJamClient(); + ftue = JK.FtueWizard(appFake); + }); + + describe("degreesFromRange", function() { + + describe("20, 0, 40, 360", function() { + it("should return 0", function() { + var deg = ftue._degreesFromRange(20,0,40,360); + expect(deg).toEqual(0); + }); + }); + + describe("0, 0, 40, 360", function() { + it("should return 180", function() { + var deg = ftue._degreesFromRange(0,0,40,360); + expect(deg).toEqual(180); + }); + }); + + + describe("10, 0, 40, 360", function() { + it("should return 270", function() { + var deg = ftue._degreesFromRange(10,0,40,360); + expect(deg).toEqual(270); + }); + }); + + describe("30, 0, 40, 360", function() { + it("should return 90", function() { + var deg = ftue._degreesFromRange(30,0,40,360); + expect(deg).toEqual(90); + }); + }); + + // Test value outside of range + describe("100, 0, 40, 360", function() { + it("should return 180", function() { + var deg = ftue._degreesFromRange(100,0,40,360); + expect(deg).toEqual(180); + }); + }); + describe("-120, 0, 40, 360", function() { + it("should return 180", function() { + var deg = ftue._degreesFromRange(-120,0,40,360); + expect(deg).toEqual(180); + }); + }); + + // Limit degrees to 300 + describe("20, 0, 40, 300", function() { + it("should return 0", function() { + var deg = ftue._degreesFromRange(20,0,40,300); + expect(deg).toEqual(0); + }); + }); + + describe("0, 0, 40, 300", function() { + it("should return 210", function() { + var deg = ftue._degreesFromRange(0,0,40,300); + expect(deg).toEqual(210); + }); + }); + + describe("10, 0, 40, 300", function() { + it("should return 285", function() { + var deg = ftue._degreesFromRange(10,0,40,300); + expect(deg).toEqual(285); + }); + }); + + describe("30, 0, 40, 300", function() { + it("should return 75", function() { + var deg = ftue._degreesFromRange(30,0,40,300); + expect(deg).toEqual(75); + }); + }); + + }); + + }); + +})(window, jQuery); \ No newline at end of file diff --git a/web/spec/javascripts/helpers/.gitkeep b/web/spec/javascripts/helpers/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/web/spec/javascripts/helpers/jasmine-jquery.js b/web/spec/javascripts/helpers/jasmine-jquery.js new file mode 100644 index 000000000..ca8f6b0ee --- /dev/null +++ b/web/spec/javascripts/helpers/jasmine-jquery.js @@ -0,0 +1,546 @@ +var readFixtures = function() { + return jasmine.getFixtures().proxyCallTo_('read', arguments) +} + +var preloadFixtures = function() { + jasmine.getFixtures().proxyCallTo_('preload', arguments) +} + +var loadFixtures = function() { + jasmine.getFixtures().proxyCallTo_('load', arguments) +} + +var appendLoadFixtures = function() { + jasmine.getFixtures().proxyCallTo_('appendLoad', arguments) +} + +var setFixtures = function(html) { + jasmine.getFixtures().proxyCallTo_('set', arguments) +} + +var appendSetFixtures = function() { + jasmine.getFixtures().proxyCallTo_('appendSet', arguments) +} + +var sandbox = function(attributes) { + return jasmine.getFixtures().sandbox(attributes) +} + +var spyOnEvent = function(selector, eventName) { + return jasmine.JQuery.events.spyOn(selector, eventName) +} + +var preloadStyleFixtures = function() { + jasmine.getStyleFixtures().proxyCallTo_('preload', arguments) +} + +var loadStyleFixtures = function() { + jasmine.getStyleFixtures().proxyCallTo_('load', arguments) +} + +var appendLoadStyleFixtures = function() { + jasmine.getStyleFixtures().proxyCallTo_('appendLoad', arguments) +} + +var setStyleFixtures = function(html) { + jasmine.getStyleFixtures().proxyCallTo_('set', arguments) +} + +var appendSetStyleFixtures = function(html) { + jasmine.getStyleFixtures().proxyCallTo_('appendSet', arguments) +} + +var loadJSONFixtures = function() { + return jasmine.getJSONFixtures().proxyCallTo_('load', arguments) +} + +var getJSONFixture = function(url) { + return jasmine.getJSONFixtures().proxyCallTo_('read', arguments)[url] +} + +jasmine.spiedEventsKey = function (selector, eventName) { + return [$(selector).selector, eventName].toString() +} + +jasmine.getFixtures = function() { + return jasmine.currentFixtures_ = jasmine.currentFixtures_ || new jasmine.Fixtures() +} + +jasmine.getStyleFixtures = function() { + return jasmine.currentStyleFixtures_ = jasmine.currentStyleFixtures_ || new jasmine.StyleFixtures() +} + +jasmine.Fixtures = function() { + this.containerId = 'jasmine-fixtures' + this.fixturesCache_ = {} + this.fixturesPath = 'spec/javascripts/fixtures' +} + +jasmine.Fixtures.prototype.set = function(html) { + this.cleanUp() + this.createContainer_(html) +} + +jasmine.Fixtures.prototype.appendSet= function(html) { + this.addToContainer_(html) +} + +jasmine.Fixtures.prototype.preload = function() { + this.read.apply(this, arguments) +} + +jasmine.Fixtures.prototype.load = function() { + this.cleanUp() + this.createContainer_(this.read.apply(this, arguments)) +} + +jasmine.Fixtures.prototype.appendLoad = function() { + this.addToContainer_(this.read.apply(this, arguments)) +} + +jasmine.Fixtures.prototype.read = function() { + var htmlChunks = [] + + var fixtureUrls = arguments + for(var urlCount = fixtureUrls.length, urlIndex = 0; urlIndex < urlCount; urlIndex++) { + htmlChunks.push(this.getFixtureHtml_(fixtureUrls[urlIndex])) + } + + return htmlChunks.join('') +} + +jasmine.Fixtures.prototype.clearCache = function() { + this.fixturesCache_ = {} +} + +jasmine.Fixtures.prototype.cleanUp = function() { + $('#' + this.containerId).remove() +} + +jasmine.Fixtures.prototype.sandbox = function(attributes) { + var attributesToSet = attributes || {} + return $('
        ').attr(attributesToSet) +} + +jasmine.Fixtures.prototype.createContainer_ = function(html) { + var container + if(html instanceof $) { + container = $('
        ') + container.html(html) + } else { + container = '
        ' + html + '
        ' + } + $('body').append(container) +} + +jasmine.Fixtures.prototype.addToContainer_ = function(html){ + var container = $('body').find('#'+this.containerId).append(html) + if(!container.length){ + this.createContainer_(html) + } +} + +jasmine.Fixtures.prototype.getFixtureHtml_ = function(url) { + if (typeof this.fixturesCache_[url] === 'undefined') { + this.loadFixtureIntoCache_(url) + } + return this.fixturesCache_[url] +} + +jasmine.Fixtures.prototype.loadFixtureIntoCache_ = function(relativeUrl) { + var url = this.makeFixtureUrl_(relativeUrl) + var request = $.ajax({ + type: "GET", + url: url + "?" + new Date().getTime(), + async: false + }) + this.fixturesCache_[relativeUrl] = request.responseText +} + +jasmine.Fixtures.prototype.makeFixtureUrl_ = function(relativeUrl){ + return this.fixturesPath.match('/$') ? this.fixturesPath + relativeUrl : this.fixturesPath + '/' + relativeUrl +} + +jasmine.Fixtures.prototype.proxyCallTo_ = function(methodName, passedArguments) { + return this[methodName].apply(this, passedArguments) +} + + +jasmine.StyleFixtures = function() { + this.fixturesCache_ = {} + this.fixturesNodes_ = [] + this.fixturesPath = 'spec/javascripts/fixtures' +} + +jasmine.StyleFixtures.prototype.set = function(css) { + this.cleanUp() + this.createStyle_(css) +} + +jasmine.StyleFixtures.prototype.appendSet = function(css) { + this.createStyle_(css) +} + +jasmine.StyleFixtures.prototype.preload = function() { + this.read_.apply(this, arguments) +} + +jasmine.StyleFixtures.prototype.load = function() { + this.cleanUp() + this.createStyle_(this.read_.apply(this, arguments)) +} + +jasmine.StyleFixtures.prototype.appendLoad = function() { + this.createStyle_(this.read_.apply(this, arguments)) +} + +jasmine.StyleFixtures.prototype.cleanUp = function() { + while(this.fixturesNodes_.length) { + this.fixturesNodes_.pop().remove() + } +} + +jasmine.StyleFixtures.prototype.createStyle_ = function(html) { + var styleText = $('
        ').html(html).text(), + style = $('') + + this.fixturesNodes_.push(style) + + $('head').append(style) +} + +jasmine.StyleFixtures.prototype.clearCache = jasmine.Fixtures.prototype.clearCache + +jasmine.StyleFixtures.prototype.read_ = jasmine.Fixtures.prototype.read + +jasmine.StyleFixtures.prototype.getFixtureHtml_ = jasmine.Fixtures.prototype.getFixtureHtml_ + +jasmine.StyleFixtures.prototype.loadFixtureIntoCache_ = jasmine.Fixtures.prototype.loadFixtureIntoCache_ + +jasmine.StyleFixtures.prototype.makeFixtureUrl_ = jasmine.Fixtures.prototype.makeFixtureUrl_ + +jasmine.StyleFixtures.prototype.proxyCallTo_ = jasmine.Fixtures.prototype.proxyCallTo_ + +jasmine.getJSONFixtures = function() { + return jasmine.currentJSONFixtures_ = jasmine.currentJSONFixtures_ || new jasmine.JSONFixtures() +} + +jasmine.JSONFixtures = function() { + this.fixturesCache_ = {} + this.fixturesPath = 'spec/javascripts/fixtures/json' +} + +jasmine.JSONFixtures.prototype.load = function() { + this.read.apply(this, arguments) + return this.fixturesCache_ +} + +jasmine.JSONFixtures.prototype.read = function() { + var fixtureUrls = arguments + for(var urlCount = fixtureUrls.length, urlIndex = 0; urlIndex < urlCount; urlIndex++) { + this.getFixtureData_(fixtureUrls[urlIndex]) + } + return this.fixturesCache_ +} + +jasmine.JSONFixtures.prototype.clearCache = function() { + this.fixturesCache_ = {} +} + +jasmine.JSONFixtures.prototype.getFixtureData_ = function(url) { + this.loadFixtureIntoCache_(url) + return this.fixturesCache_[url] +} + +jasmine.JSONFixtures.prototype.loadFixtureIntoCache_ = function(relativeUrl) { + var self = this + var url = this.fixturesPath.match('/$') ? this.fixturesPath + relativeUrl : this.fixturesPath + '/' + relativeUrl + $.ajax({ + async: false, // must be synchronous to guarantee that no tests are run before fixture is loaded + cache: false, + dataType: 'json', + url: url, + success: function(data) { + self.fixturesCache_[relativeUrl] = data + }, + error: function(jqXHR, status, errorThrown) { + throw Error('JSONFixture could not be loaded: ' + url + ' (status: ' + status + ', message: ' + errorThrown.message + ')') + } + }) +} + +jasmine.JSONFixtures.prototype.proxyCallTo_ = function(methodName, passedArguments) { + return this[methodName].apply(this, passedArguments) +} + +jasmine.JQuery = function() {} + +jasmine.JQuery.browserTagCaseIndependentHtml = function(html) { + return $('
        ').append(html).html() +} + +jasmine.JQuery.elementToString = function(element) { + var domEl = $(element).get(0) + if (domEl == undefined || domEl.cloneNode) + return $('
        ').append($(element).clone()).html() + else + return element.toString() +} + +jasmine.JQuery.matchersClass = {} + +!function(namespace) { + var data = { + spiedEvents: {}, + handlers: [] + } + + namespace.events = { + spyOn: function(selector, eventName) { + var handler = function(e) { + data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)] = e + } + $(selector).bind(eventName, handler) + data.handlers.push(handler) + return { + selector: selector, + eventName: eventName, + handler: handler, + reset: function(){ + delete data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)] + } + } + }, + + wasTriggered: function(selector, eventName) { + return !!(data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)]) + }, + + wasPrevented: function(selector, eventName) { + return data.spiedEvents[jasmine.spiedEventsKey(selector, eventName)].isDefaultPrevented() + }, + + cleanUp: function() { + data.spiedEvents = {} + data.handlers = [] + } + } +}(jasmine.JQuery) + +!function(){ + var jQueryMatchers = { + toHaveClass: function(className) { + return this.actual.hasClass(className) + }, + + toHaveCss: function(css){ + for (var prop in css){ + if (this.actual.css(prop) !== css[prop]) return false + } + return true + }, + + toBeVisible: function() { + return this.actual.is(':visible') + }, + + toBeHidden: function() { + return this.actual.is(':hidden') + }, + + toBeSelected: function() { + return this.actual.is(':selected') + }, + + toBeChecked: function() { + return this.actual.is(':checked') + }, + + toBeEmpty: function() { + return this.actual.is(':empty') + }, + + toExist: function() { + return $(document).find(this.actual).length + }, + + toHaveAttr: function(attributeName, expectedAttributeValue) { + return hasProperty(this.actual.attr(attributeName), expectedAttributeValue) + }, + + toHaveProp: function(propertyName, expectedPropertyValue) { + return hasProperty(this.actual.prop(propertyName), expectedPropertyValue) + }, + + toHaveId: function(id) { + return this.actual.attr('id') == id + }, + + toHaveHtml: function(html) { + return this.actual.html() == jasmine.JQuery.browserTagCaseIndependentHtml(html) + }, + + toContainHtml: function(html){ + var actualHtml = this.actual.html() + var expectedHtml = jasmine.JQuery.browserTagCaseIndependentHtml(html) + return (actualHtml.indexOf(expectedHtml) >= 0) + }, + + toHaveText: function(text) { + var trimmedText = $.trim(this.actual.text()) + if (text && $.isFunction(text.test)) { + return text.test(trimmedText) + } else { + return trimmedText == text + } + }, + + toHaveValue: function(value) { + return this.actual.val() == value + }, + + toHaveData: function(key, expectedValue) { + return hasProperty(this.actual.data(key), expectedValue) + }, + + toBe: function(selector) { + return this.actual.is(selector) + }, + + toContain: function(selector) { + return this.actual.find(selector).length + }, + + toBeDisabled: function(selector){ + return this.actual.is(':disabled') + }, + + toBeFocused: function(selector) { + return this.actual.is(':focus') + }, + + toHandle: function(event) { + + var events = $._data(this.actual.get(0), "events") + + if(!events || !event || typeof event !== "string") { + return false + } + + var namespaces = event.split(".") + var eventType = namespaces.shift() + var sortedNamespaces = namespaces.slice(0).sort() + var namespaceRegExp = new RegExp("(^|\\.)" + sortedNamespaces.join("\\.(?:.*\\.)?") + "(\\.|$)") + + if(events[eventType] && namespaces.length) { + for(var i = 0; i < events[eventType].length; i++) { + var namespace = events[eventType][i].namespace + if(namespaceRegExp.test(namespace)) { + return true + } + } + } else { + return events[eventType] && events[eventType].length > 0 + } + }, + + // tests the existence of a specific event binding + handler + toHandleWith: function(eventName, eventHandler) { + var stack = $._data(this.actual.get(0), "events")[eventName] + for (var i = 0; i < stack.length; i++) { + if (stack[i].handler == eventHandler) return true + } + return false + } + } + + var hasProperty = function(actualValue, expectedValue) { + if (expectedValue === undefined) return actualValue !== undefined + return actualValue == expectedValue + } + + var bindMatcher = function(methodName) { + var builtInMatcher = jasmine.Matchers.prototype[methodName] + + jasmine.JQuery.matchersClass[methodName] = function() { + if (this.actual + && (this.actual instanceof $ + || jasmine.isDomNode(this.actual))) { + this.actual = $(this.actual) + var result = jQueryMatchers[methodName].apply(this, arguments) + var element + if (this.actual.get && (element = this.actual.get()[0]) && !$.isWindow(element) && element.tagName !== "HTML") + this.actual = jasmine.JQuery.elementToString(this.actual) + return result + } + + if (builtInMatcher) { + return builtInMatcher.apply(this, arguments) + } + + return false + } + } + + for(var methodName in jQueryMatchers) { + bindMatcher(methodName) + } +}() + +beforeEach(function() { + this.addMatchers(jasmine.JQuery.matchersClass) + this.addMatchers({ + toHaveBeenTriggeredOn: function(selector) { + this.message = function() { + return [ + "Expected event " + this.actual + " to have been triggered on " + selector, + "Expected event " + this.actual + " not to have been triggered on " + selector + ] + } + return jasmine.JQuery.events.wasTriggered(selector, this.actual) + } + }) + this.addMatchers({ + toHaveBeenTriggered: function(){ + var eventName = this.actual.eventName, + selector = this.actual.selector + this.message = function() { + return [ + "Expected event " + eventName + " to have been triggered on " + selector, + "Expected event " + eventName + " not to have been triggered on " + selector + ] + } + return jasmine.JQuery.events.wasTriggered(selector, eventName) + } + }) + this.addMatchers({ + toHaveBeenPreventedOn: function(selector) { + this.message = function() { + return [ + "Expected event " + this.actual + " to have been prevented on " + selector, + "Expected event " + this.actual + " not to have been prevented on " + selector + ] + } + return jasmine.JQuery.events.wasPrevented(selector, this.actual) + } + }) + this.addMatchers({ + toHaveBeenPrevented: function() { + var eventName = this.actual.eventName, + selector = this.actual.selector + this.message = function() { + return [ + "Expected event " + eventName + " to have been prevented on " + selector, + "Expected event " + eventName + " not to have been prevented on " + selector + ] + } + return jasmine.JQuery.events.wasPrevented(selector, eventName) + } + }) +}) + +afterEach(function() { + jasmine.getFixtures().cleanUp() + jasmine.getStyleFixtures().cleanUp() + jasmine.JQuery.events.cleanUp() +}) diff --git a/web/spec/javascripts/helpers/test_getSessionResponses.js b/web/spec/javascripts/helpers/test_getSessionResponses.js new file mode 100644 index 000000000..b526587be --- /dev/null +++ b/web/spec/javascripts/helpers/test_getSessionResponses.js @@ -0,0 +1,120 @@ +window.TestGetSessionResponses = { + oneOfEach: [ + + // Session 1 - you're invited to this. + { + "id": "1", + "description": "Invited", + "musician_access": true, + "genres" : [ "classical" ], + "participants": [ + { + "client_id": "0f8f7987-29a0-4e5d-a60c-6b23103e3533", + "ip_address":"1.1.1.1", + "user_id" : "02303020402042040", + "tracks" : [ + { + "id" : "xxxx", + "instrument_id" : "electric guitar", + "sound" : "mono" + } + ] + } + ], + "invitations" : [ + { + "id" : "3948797598275987", + "sender_id" : "02303020402042040" + + } + ] + }, + + // Session 2 - no invite, but friend #1 (good latency) + { + "id": "2", + "description": "Friends 1", + "musician_access": true, + "genres" : [ "blues" ], + "participants": [ + { + "client_id": "0f8f7987-29a0-4e5d-a60c-6b23103e3533", + "ip_address":"1.1.1.1", + "user_id" : "02303020402042040", + "tracks" : [ + { + "id" : "xxxx", + "instrument_id" : "electric guitar", + "sound" : "mono" + } + ] + } + ] + }, + + // Session 3 - no invite, but friend #2 (med latency) + { + "id": "3", + "description": "Friends 2", + "musician_access": true, + "genres" : [ "blues" ], + "participants": [ + { + "client_id": "0f8f7987-29a0-4e5d-a60c-6b23103e3533", + "ip_address":"1.1.1.1", + "user_id" : "02303020402042040", + "tracks" : [ + { + "id" : "xxxx", + "instrument_id" : "electric guitar", + "sound" : "mono" + } + ] + } + ] + }, + + // Session 4 - no invite, no friends 1 + { + "id": "4", + "description": "Anonymous 1", + "musician_access": true, + "genres" : [ "blues" ], + "participants": [ + { + "client_id": "0f8f7987-29a0-4e5d-a60c-6b23103e3533", + "ip_address":"1.1.1.1", + "tracks" : [ + { + "id" : "xxxx", + "instrument_id" : "electric guitar", + "sound" : "mono" + } + ] + } + ] + }, + + // Session 5 - no invite, no friends 2 + { + "id": "5", + "description": "Anonymous 2", + "musician_access": true, + "genres" : [ "blues" ], + "participants": [ + { + "client_id": "0f8f7987-29a0-4e5d-a60c-6b23103e3533", + "ip_address":"1.1.1.1", + "tracks" : [ + { + "id" : "xxxx", + "instrument_id" : "electric guitar", + "sound" : "mono" + } + ] + } + ] + } + + ] +}; \ No newline at end of file diff --git a/web/spec/javascripts/helpers/test_responses.js b/web/spec/javascripts/helpers/test_responses.js new file mode 100644 index 000000000..ca9d890cb --- /dev/null +++ b/web/spec/javascripts/helpers/test_responses.js @@ -0,0 +1,94 @@ +window.TestResponses = { + sessionPost: { + "id": "1234", + "description": "Hello", + "musician_access" : true, + "genres" : ['classical'], + "participants": [ + { + "client_id": "0f8f7987-29a0-4e5d-a60c-6b23103e3533", + "ip_address":"1.1.1.1", + "user_id" : "02303020402042040", // NOTE THIS WILL BE UNDEFINED (ABSENT) IF THIS CLIENT IS NOT YOUR FRIEND + "tracks" : [ + { + "id" : "xxxx", + "instrument_id" : "electric guitar", + "sound" : "mono" + } + ] + } + ] + }, + + loadGenres: [ + { + id: "african", + description: "African" + }, + { + id: "ambient", + description: "Ambient" + } + ], + + search : { + bands: [ ], + musicians: [ ], + fans: [ + { + id: "1", + first_name: "Test", + last_name: "User", + location: "Austin, TX", + photo_url: "http://www.jamkazam.com/images/users/photos/1.gif" + } + ], + recordings: [ ] + }, + + emptySearch: { + bands: [], + musicians: [], + fans: [], + recordings: [] + }, + + fullSearch: { + bands: [ + { + id: "1", + first_name: "Test", + last_name: "User", + location: "Austin, TX", + photo_url: "http://www.jamkazam.com/images/users/photos/1.gif" + } + ], + musicians: [ + { + id: "1", + first_name: "Test", + last_name: "User", + location: "Austin, TX", + photo_url: "http://www.jamkazam.com/images/users/photos/1.gif" + } + ], + fans: [ + { + id: "1", + first_name: "Test", + last_name: "User", + location: "Austin, TX", + photo_url: "http://www.jamkazam.com/images/users/photos/1.gif" + } + ], + recordings: [ + { + id: "1", + first_name: "Test", + last_name: "User", + location: "Austin, TX", + photo_url: "http://www.jamkazam.com/images/users/photos/1.gif" + } + ] + } +}; \ No newline at end of file diff --git a/web/spec/javascripts/helpers/testutil.js b/web/spec/javascripts/helpers/testutil.js new file mode 100644 index 000000000..7ab470170 --- /dev/null +++ b/web/spec/javascripts/helpers/testutil.js @@ -0,0 +1,29 @@ +(function(context, $) { + + context.JKTestUtils = { + loadFixtures: function(path) { + //path = 'file:///home/jonathon/dev/jamkazam/jam-web' + path; + //path = 'http://localhost:9876' + path; + var needTestFixtures = ($('#test-fixtures').length === 0); + if (needTestFixtures) { + $('body').append('
        '); + } + $.ajax({ + url:path, + async: false, + success: function(r) { + $('#test-fixtures').append(r); + }, + error: function(jqXHR, textStatus, errorThrown) { + // Assumes we're in a jasmine context + throw 'loadFixtures error: ' + path + ': ' + errorThrown; + } + }); + }, + + removeFixtures: function() { + $('#test-fixtures').remove(); + } + }; + +})(window, jQuery); \ No newline at end of file diff --git a/web/spec/javascripts/jamserver.spec.js b/web/spec/javascripts/jamserver.spec.js new file mode 100644 index 000000000..b19d799be --- /dev/null +++ b/web/spec/javascripts/jamserver.spec.js @@ -0,0 +1,57 @@ +(function(context, $) { + + describe("JamServer", function() { + var jamserver; + + beforeEach(function() { + var opts = { + layoutOpts: { + allowBodyOverflow: true + } + }; + jamserver = context.JK.JamServer; + }); + + describe("Event Subscription", function() { + + it("Subscribing to ping should call function", function() { + var called = false; + jamserver.registerMessageCallback(context.JK.MessageType.PING_ACK, function() { + called = true; + }); + var msg = {type: context.JK.MessageType.PING_ACK}; + msg[context.JK.MessageType.PING_ACK] = {}; + var e = {data:JSON.stringify(msg)}; + jamserver.onMessage(e); + expect(called).toBeTruthy(); + }); + + it("All registerMessageCallbackrs should be called", function() { + var callCount = 0; + jamserver.registerMessageCallback(context.JK.MessageType.PING_ACK, function() {callCount += 1;}); + jamserver.registerMessageCallback(context.JK.MessageType.PING_ACK, function() {callCount += 2;}); + var msg = {type: context.JK.MessageType.PING_ACK}; + msg[context.JK.MessageType.PING_ACK] = {}; + var e = {data:JSON.stringify(msg)}; + jamserver.onMessage(e); + expect(callCount).toEqual(3); + }); + + it("An error in a callback should be caught", function() { + var callCount = 0; + jamserver.registerMessageCallback(context.JK.MessageType.PING_ACK, function() {callCount += 1;}); + jamserver.registerMessageCallback(context.JK.MessageType.PING_ACK, function() {throw "Intentional Error";}); + jamserver.registerMessageCallback(context.JK.MessageType.PING_ACK, function() {callCount += 1;}); + var msg = {type: context.JK.MessageType.PING_ACK}; + msg[context.JK.MessageType.PING_ACK] = {}; + var e = {data:JSON.stringify(msg)}; + jamserver.onMessage(e); + expect(callCount).toEqual(2); + }); + + }); + + }); + + +}(window, jQuery)); \ No newline at end of file diff --git a/web/spec/javascripts/karma.ci.conf.js b/web/spec/javascripts/karma.ci.conf.js new file mode 100644 index 000000000..1c1e1cdcf --- /dev/null +++ b/web/spec/javascripts/karma.ci.conf.js @@ -0,0 +1,82 @@ +// Karma configuration +// Generated on Sat Mar 30 2013 13:54:13 GMT-0600 (MDT) + + +// base path, that will be used to resolve files and exclude +var workspace = process.env.WORKSPACE || '/home/jonathon/dev/jamkazam'; +basePath = workspace + "/jam-web"; + + +// list of files / patterns to load in the browser +files = [ + JASMINE, + JASMINE_ADAPTER, + 'spec/javascripts/vendor/jquery.js', + 'spec/javascripts/helpers/test_responses.js', + 'spec/javascripts/helpers/testutil.js', + 'app/assets/javascripts/**/*.js', + 'spec/javascripts/*.spec.js', + { + pattern:'app/views/clients/*.html.erb', + included: false, + served: true, + watched: true + }, + { + pattern:'spec/javascripts/fixtures/*.htm', + included: false, + served: true, + watched: true + } +]; + + +// list of files to exclude +exclude = [ + +]; + + +// test results reporter to use +// possible values: 'dots', 'progress', 'junit' +reporters = ['progress']; + + +// web server port +port = 9876; + + +// cli runner port +runnerPort = 9100; + + +// enable / disable colors in the output (reporters and logs) +colors = true; + + +// level of logging +// possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG +logLevel = LOG_INFO; + + +// enable / disable watching file and executing tests whenever any file changes +autoWatch = false; + + +// Start these browsers, currently available: +// - Chrome +// - ChromeCanary +// - Firefox +// - Opera +// - Safari (only Mac) +// - PhantomJS +// - IE (only Windows) +browsers = ['PhantomJS']; + + +// If browser does not capture in given timeout [ms], kill it +captureTimeout = 60000; + +// Continuous Integration mode +// if true, it capture browsers, run tests and exit +singleRun = true; \ No newline at end of file diff --git a/web/spec/javascripts/karma.conf.js b/web/spec/javascripts/karma.conf.js new file mode 100644 index 000000000..c7a9d8a41 --- /dev/null +++ b/web/spec/javascripts/karma.conf.js @@ -0,0 +1,84 @@ +// Karma configuration +// Generated on Sat Mar 30 2013 13:54:13 GMT-0600 (MDT) + + +// base path, that will be used to resolve files and exclude +var workspace = process.env.WORKSPACE || '/home/jonathon/dev/jamkazam'; +basePath = workspace + "/jam-web"; + + +// list of files / patterns to load in the browser +files = [ + JASMINE, + JASMINE_ADAPTER, + 'spec/javascripts/vendor/jquery.js', + 'spec/javascripts/helpers/test_responses.js', + 'spec/javascripts/helpers/testutil.js', + 'app/assets/javascripts/**/*.js', + //'spec/javascripts/ftue.spec.js', // single test file + 'spec/javascripts/*.spec.js', + { + pattern:'app/views/clients/*.html.erb', + included: false, + served: true, + watched: true + }, + { + pattern:'spec/javascripts/fixtures/*.htm', + included: false, + served: true, + watched: true + } +]; + + +// list of files to exclude +exclude = [ + +]; + + +// test results reporter to use +// possible values: 'dots', 'progress', 'junit' +reporters = ['progress']; + + +// web server port +port = 9876; + + +// cli runner port +runnerPort = 9100; + + +// enable / disable colors in the output (reporters and logs) +colors = true; + + +// level of logging +// possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG +logLevel = LOG_INFO; + + +// enable / disable watching file and executing tests whenever any file changes +autoWatch = false; + + +// Start these browsers, currently available: +// - Chrome +// - ChromeCanary +// - Firefox +// - Opera +// - Safari (only Mac) +// - PhantomJS +// - IE (only Windows) +browsers = ['Chrome']; + + +// If browser does not capture in given timeout [ms], kill it +captureTimeout = 60000; + + +// Continuous Integration mode +// if true, it capture browsers, run tests and exit +singleRun = false; \ No newline at end of file diff --git a/web/spec/javascripts/layout.spec.js b/web/spec/javascripts/layout.spec.js new file mode 100644 index 000000000..988915742 --- /dev/null +++ b/web/spec/javascripts/layout.spec.js @@ -0,0 +1,162 @@ +(function(context, $) { + + describe("Layout", function() { + var layout, cardLayout, testOpts; + + beforeEach(function() { + layout = new context.JK.Layout(); + }); + + describe("Construct", function() { + describe("defaults", function() { + it("headerHeight should be 75", function() { + expect(layout.getOpts().headerHeight).toEqual(75); + }); + it("sidebarWidth should be 300", function() { + expect(layout.getOpts().sidebarWidth).toEqual(300); + }); + it("gutter should be 60", function() { + expect(layout.getOpts().gutter).toEqual(60); + }); + }); + describe("override one default", function() { + it("headerHeight should be 300", function() { + testOpts = { + allowBodyOverflow: true, + headerHeight: 300 + }; + layout.initialize(testOpts); + expect(layout.getOpts().headerHeight).toEqual(300); + }); + it("sidebarWidth should be 300", function() { + expect(layout.getOpts().sidebarWidth).toEqual(300); + }); + }); + + }); + + describe("getScreenDimensions", function() { + describe("Description", function() { + + }); + }); + + describe("CardLayout", function() { + describe("One cell, zero margins", function() { + it("should fill space", function() { + testOpts = { allowBodyOverflow:true, gridOuterMargin: 0, gridPadding: 0}; + layout.initialize(testOpts); + cardLayout = layout.getCardLayout(100, 100, 1, 1, 0, 0, 1, 1); + expect(cardLayout).toEqual({top:0,left:0,width:100,height:100}); + }); + }); + + describe("Two columns, zero margins", function() { + it("should be half width each", function() { + testOpts = { allowBodyOverflow:true, gridOuterMargin: 0, gridPadding: 0}; + layout.initialize(testOpts); + cardLayout = layout.getCardLayout(100, 100, 1, 2, 0, 0, 1, 1); + expect(cardLayout).toEqual({top:0,left:0,width:50,height:100}); + + cardLayout = layout.getCardLayout(100, 100, 1, 2, 0, 1, 1, 1); + expect(cardLayout).toEqual({top:0,left:50,width:50,height:100}); + }); + }); + + describe("Two rows, zero margins", function() { + it("should be half height each", function() { + testOpts = { allowBodyOverflow:true, gridOuterMargin: 0, gridPadding: 0}; + layout.initialize(testOpts); + cardLayout = layout.getCardLayout(100, 100, 2, 1, 0, 0, 1, 1); + expect(cardLayout).toEqual({top:0,left:0,width:100,height:50}); + + cardLayout = layout.getCardLayout(100, 100, 2, 1, 1, 0, 1, 1); + expect(cardLayout).toEqual({top:50,left:0,width:100,height:50}); + }); + }); + + describe("two cols, colspan 2, zero margins", function() { + it("should fill width", function() { + testOpts = { allowBodyOverflow: true, gridOuterMargin: 0, gridPadding: 0}; + layout.initialize(testOpts); + cardLayout = layout.getCardLayout(100, 100, 1, 2, 0, 0, 1, 2); + expect(cardLayout).toEqual({top:0,left:0,width:100,height:100}); + }); + }); + + describe("two rows, rowspan 2, zero margins", function() { + it("should fill height", function() { + testOpts = { allowBodyOverflow: true, gridOuterMargin: 0, gridPadding: 0}; + layout.initialize(testOpts); + cardLayout = layout.getCardLayout(100, 100, 2, 1, 0, 0, 2, 1); + expect(cardLayout).toEqual({top:0,left:0,width:100,height:100}); + }); + }); + + describe("4x4, zero margins, row 1, col 1, rowspan 2, colspan 2", function() { + it("should fill middle 4 cells", function() { + testOpts = { allowBodyOverflow: true, gridOuterMargin: 0, gridPadding: 0}; + layout.initialize(testOpts); + cardLayout = layout.getCardLayout(100, 100, 4, 4, 1, 1, 2, 2); + expect(cardLayout).toEqual({top:25,left:25,width:50,height:50}); + }); + }); + + // Outer margins + describe("1x1, 100x100, outermargin 10", function() { + it("should be at 0x0, 80x80", function() { + testOpts = { allowBodyOverflow: true, gridOuterMargin: 10, gridPadding: 0}; + layout.initialize(testOpts); + cardLayout = layout.getCardLayout(100, 100, 1, 1, 0, 0, 1, 1); + expect(cardLayout).toEqual({top:0,left:0,width:80,height:80}); + }); + }); + describe("2x2, 100x100, outermargin 10", function() { + it("should be at 0x0, 40x40", function() { + testOpts = { allowBodyOverflow: true, gridOuterMargin: 10, gridPadding: 0}; + layout.initialize(testOpts); + cardLayout = layout.getCardLayout(100, 100, 2, 2, 0, 0, 1, 1); + expect(cardLayout).toEqual({top:0,left:0,width:40,height:40}); + }); + }); + + // Inner margins + describe("2x2, 100x100, padding 10", function() { + it("10 pixels in and 10 pixel gutters", function() { + testOpts = { allowBodyOverflow: true, gridOuterMargin: 0, gridPadding: 10}; + layout.initialize(testOpts); + // upper left + cardLayout = layout.getCardLayout(100, 100, 2, 2, 0, 0, 1, 1); + expect(cardLayout).toEqual({top:0,left:0,width:45,height:45}); + // upper right + cardLayout = layout.getCardLayout(100, 100, 2, 2, 0, 1, 1, 1); + expect(cardLayout).toEqual({top:0,left:65,width:45,height:45}); + }); + }); + + // 5 block test like starting home. + describe("home page test", function() { + it("5 blocks", function() { + testOpts = { allowBodyOverflow: true, gridOuterMargin: 10, gridPadding: 10}; + layout.initialize(testOpts); + // Cell 1 + cardLayout = layout.getCardLayout(1000, 1000, 2, 4, 0, 0, 1, 1); + expect(cardLayout).toEqual({top:0,left:0,width:232,height:485}); + // Cell 2 + cardLayout = layout.getCardLayout(1000, 1000, 2, 4, 0, 1, 1, 1); + expect(cardLayout).toEqual({top:0,left:252,width:232,height:485}); + // Cell 3 + cardLayout = layout.getCardLayout(1000, 1000, 2, 4, 0, 2, 1, 2); + expect(cardLayout).toEqual({top:0,left:504,width:484,height:485}); + // Cell 4 + cardLayout = layout.getCardLayout(1000, 1000, 2, 4, 1, 0, 1, 2); + expect(cardLayout).toEqual({top:505,left:0,width:484,height:485}); + // Cell 5 + cardLayout = layout.getCardLayout(1000, 1000, 2, 4, 1, 2, 1, 2); + expect(cardLayout).toEqual({top:505,left:504,width:484,height:485}); + }); + }); + + }); + }); +}(window, jQuery)); \ No newline at end of file diff --git a/web/spec/javascripts/log.spec.js b/web/spec/javascripts/log.spec.js new file mode 100644 index 000000000..44ca9c428 --- /dev/null +++ b/web/spec/javascripts/log.spec.js @@ -0,0 +1,16 @@ +(function(context, $) { + + describe("Logger", function() { + var logger; + + beforeEach(function() { + logger = context.JK.logger; + }); + + it("logger.log present", function() { + logger.log('foo'); + }); + + }); + +}(window, jQuery)); \ No newline at end of file diff --git a/web/spec/javascripts/searcher.spec.js b/web/spec/javascripts/searcher.spec.js new file mode 100644 index 000000000..5cdc368fc --- /dev/null +++ b/web/spec/javascripts/searcher.spec.js @@ -0,0 +1,197 @@ +(function(context) { + + describe("searcher.js tests", function() { + + beforeEach(function() { + JKTestUtils.loadFixtures('/base/spec/javascripts/fixtures/searcher.htm'); + }); + + describe("Empty Search", function() { + + // See the markup in fixtures/searcher.htm + + var searcher; + var ajaxSpy; + + var fakeApp = { + ajaxError: function() { + console.debug("ajaxError"); + } + }; + + beforeEach(function() { + spyOn($, "ajax").andCallFake(function(opts) { + opts.success(TestResponses.emptySearch); + }); + searcher = new JK.Searcher(fakeApp); + searcher.initialize(); + }); + + it("No Results message shown", function() { + // Workaround for key events not being reflected in val() calls + $('.searchtextinput').val('AA'); + var e = jQuery.Event("keyup"); + e.which = 65; // "a" + $('.searchtextinput').focus(); + $('.searchtextinput').trigger(e); + $('.searchtextinput').trigger(e); + + expect($('.searchresults .emptyresult').length).toEqual(1); + }); + }); + + describe("Full Search", function() { + + // See the markup in fixtures/searcher.htm + + var searcher; + var ajaxSpy; + + var fakeApp = { + ajaxError: function() { + console.debug("ajaxError"); + } + }; + + beforeEach(function() { + spyOn($, "ajax").andCallFake(function(opts) { + opts.success(TestResponses.fullSearch); + }); + searcher = new JK.Searcher(fakeApp); + searcher.initialize(); + }); + + it("No Results message shown", function() { + // Workaround for key events not being reflected in val() calls + $('.searchtextinput').val('AA'); + var e = jQuery.Event("keyup"); + e.which = 65; // "a" + $('.searchtextinput').focus(); + $('.searchtextinput').trigger(e); + $('.searchtextinput').trigger(e); + + expect($('.searchresults h2').length).toEqual(4); + }); + }); + + describe("Search Tests", function() { + + // See the markup in fixtures/searcher.htm + + var searcher; + var ajaxSpy; + + var fakeApp = { + ajaxError: function() { + console.debug("ajaxError"); + } + }; + + beforeEach(function() { + spyOn($, "ajax").andCallFake(function(opts) { + opts.success(TestResponses.search); + }); + searcher = new JK.Searcher(fakeApp); + searcher.initialize(); + }); + + it("first keypress should not search", function() { + // Workaround for key events not being reflected in val() calls + $('.searchtextinput').val('A'); + var e = jQuery.Event("keyup"); + e.which = 65; // "a" + $('.searchtextinput').focus(); + $('.searchtextinput').trigger(e); + expect($.ajax.wasCalled).toBe(false); + }); + + it("second keypress should search", function() { + $('.searchtextinput').val('AA'); + $('.searchtextinput').focus(); + var e = jQuery.Event("keyup"); + e.which = 65; // "a" + $('.searchtextinput').trigger(e); + // trigger again + $('.searchtextinput').trigger(e); + + expect($.ajax).toHaveBeenCalled(); + }); + + it("response div is absolute position", function() { + $('.searchtextinput').val('AA'); + $('.searchtextinput').focus(); + var e = jQuery.Event("keyup"); + e.which = 65; // "a" + $('.searchtextinput').trigger(e); + $('.searchtextinput').trigger(e); + expect($('.searchresults').css('position')).toEqual('absolute'); + }); + + it("response displayed in results", function() { + $('.searchtextinput').val('AA'); + $('.searchtextinput').focus(); + var e = jQuery.Event("keyup"); + e.which = 65; // "a" + $('.searchtextinput').trigger(e); + $('.searchtextinput').trigger(e); + + expect($('.searchresults').length).toEqual(1); + expect($('.searchresults h2').length).toEqual(1); + expect($('.searchresults li').length).toEqual(1); + expect($('.searchresults li img').length).toEqual(1); + expect($('.searchresults li span.text').length).toEqual(1); + expect($('.searchresults li span.subtext').length).toEqual(1); + }); + + + it("response positioned under input", function() { + $('.searchtextinput').val('AA'); + $('.searchtextinput').focus(); + var e = jQuery.Event("keyup"); + e.which = 65; // "a" + $('.searchtextinput').trigger(e); + $('.searchtextinput').trigger(e); + + expect($('.searchresults').length).toEqual(1); + var bodyOffset = $('body').offset(); + var inputOffset = $('.searchtextinput').offset(); + var inputHeight = $('.searchtextinput').outerHeight(); + var expectedTop = bodyOffset.top + inputOffset.top + inputHeight; + var expectedLeft = bodyOffset.left + inputOffset.left; + + var searchResultOffset = $('.searchresults').offset(); + expect(searchResultOffset.top).toEqual(expectedTop); + expect(searchResultOffset.left).toEqual(expectedLeft); + }); + + it("search results are visible", function() { + $('.searchtextinput').val('AA'); + $('.searchtextinput').focus(); + var e = jQuery.Event("keyup"); + e.which = 65; // "a" + $('.searchtextinput').trigger(e); + $('.searchtextinput').trigger(e); + + var visible = $('.searchresults').is(':visible'); + expect(visible).toBe(true); + }); + + it("escape key hides search results", function() { + $('.searchtextinput').val('AA'); + $('.searchtextinput').focus(); + var e = jQuery.Event("keyup"); + e.which = 65; // "a" + $('.searchtextinput').trigger(e); + $('.searchtextinput').trigger(e); + + e = jQuery.Event("keyup"); + e.which = 27; // ESCAPE + $('.searchtextinput').trigger(e); + + var visible = $('.searchresults').is(':visible'); + expect(visible).toBe(false); + }); + }); + + }); +})(window); \ No newline at end of file diff --git a/web/spec/javascripts/sessionLatency.spec.js b/web/spec/javascripts/sessionLatency.spec.js new file mode 100644 index 000000000..beb07a122 --- /dev/null +++ b/web/spec/javascripts/sessionLatency.spec.js @@ -0,0 +1,121 @@ +(function(context, $) { + + describe("SessionLatency", function() { + + var sessionLatency; // Instance of SessionLatency class for test + + fakeJamClient = { + TestLatency: function() {} // Will be overridden by test cases + }; + + var sessions = [ + {id: "1", participants: [ { client_id: "1", ip_address: "1.1.1.1" } ] }, + {id: "2", participants: [ { client_id: "2", ip_address: "1.1.1.2" } ] }, + {id: "3", participants: [ { client_id: "3", ip_address: "1.1.1.3", user: {is_friend:true} } ] }, + {id: "4", participants: [ { client_id: "4", ip_address: "1.1.1.4" } ], invitations: [{id:'1', sender_id:'1'}] }, + {id: "5", participants: [ + { client_id: "5", ip_address: "1.1.1.5" }, + { client_id: "6", ip_address: "1.1.1.6" }, + { client_id: "7", ip_address: "1.1.1.7" } + ]} + ]; + var callCount = 0; + var testLatencyResponses = { + "1": {clientID: "1", latency: 35}, + "2": {clientID: "2", latency: 50}, + "3": {clientID: "3", latency: 150}, + "4": {clientID: "4", latency: 200}, + "5": {clientID: "5", latency: 100}, + "6": {clientID: "6", latency: 10}, + "7": {clientID: "7", latency: 10} + }; + + beforeEach(function() { + callCount = 0; + sessionLatency = new context.JK.SessionLatency(fakeJamClient); + spyOn(fakeJamClient, "TestLatency").andCallFake(function(clientID, callbackName) { + + var js = callbackName + '(' + JSON.stringify(testLatencyResponses[clientID]) + ');'; + eval(js); + //callback(testLatencyResponses[client.id]); + callCount++; + }); + }); + + describe("SessionPings", function() { + it("should call jamClient.TestLatency and compute new average", function() { + sessionLatency.sessionPings(sessions[0]); + expect(fakeJamClient.TestLatency).toHaveBeenCalled(); + var info = sessionLatency.sessionInfo(sessions[0].id); + expect(info.averageLatency).toEqual(35); + }); + + it("should average multiple client pings", function() { + sessionLatency.sessionPings(sessions[4]); + var info = sessionLatency.sessionInfo(sessions[4].id); + expect(info.averageLatency).toEqual(40); + }); + + }); + + describe("SessionSorting", function() { + beforeEach(function() { + $.each(sessions, function(index, session) { + sessionLatency.sessionPings(session); + }); + }); + + it("should return >= 2 for invited sessions", function() { + var score = sessionLatency.getSortScore('4'); + expect(score >= 2).toBeTruthy(); + }); + + it("should return < 2, >= 1 for friend sessions", function() { + var score = sessionLatency.getSortScore('3'); + expect(score < 2).toBeTruthy(); + expect(score >= 1).toBeTruthy(); + }); + it("should return < 1 for unknown sessions", function() { + var score = sessionLatency.getSortScore('2'); + expect(score < 1).toBeTruthy(); + }); + it("should return 1/AvgLatency for unknown pingable sessions", function() { + var score = sessionLatency.getSortScore('2'); + expect(score).toEqual(0.02); + }); + it("should return higher sort value for lower latency", function() { + var score1 = sessionLatency.getSortScore('1'); + var score2 = sessionLatency.getSortScore('2'); + expect(score1).toBeGreaterThan(score2); + }); + }); + + describe("Register for Events", function() { + it("should register successfully", function() { + var cb = jasmine.createSpy(); + sessionLatency.subscribe('test', cb); + }); + it("should invoke callback on latency result", function() { + var cb = jasmine.createSpy("Latency Subscription Callback"); + sessionLatency.subscribe('test', cb); + $.each(sessions, function(index, session) { + sessionLatency.sessionPings(session); + }); + expect(cb).toHaveBeenCalled(); + }); + it("should not add same listener twice", function() { + var cb = jasmine.createSpy(); + sessionLatency.subscribe('test', cb); + sessionLatency.subscribe('test', cb); + sessionLatency.subscribe('test', cb); + $.each(sessions, function(index, session) { + sessionLatency.sessionPings(session); + }); + expect(cb.callCount).toEqual(7); // 7 clients to ping, once. + }); + }); + + }); + + + })(window, jQuery); \ No newline at end of file diff --git a/web/spec/javascripts/support/jasmine.yml b/web/spec/javascripts/support/jasmine.yml new file mode 100644 index 000000000..9bfa261a3 --- /dev/null +++ b/web/spec/javascripts/support/jasmine.yml @@ -0,0 +1,76 @@ +# src_files +# +# Return an array of filepaths relative to src_dir to include before jasmine specs. +# Default: [] +# +# EXAMPLE: +# +# src_files: +# - lib/source1.js +# - lib/source2.js +# - dist/**/*.js +# +src_files: + - assets/application.js + +# stylesheets +# +# Return an array of stylesheet filepaths relative to src_dir to include before jasmine specs. +# Default: [] +# +# EXAMPLE: +# +# stylesheets: +# - css/style.css +# - stylesheets/*.css +# +stylesheets: + - stylesheets/**/*.css + +# helpers +# +# Return an array of filepaths relative to spec_dir to include before jasmine specs. +# Default: ["helpers/**/*.js"] +# +# EXAMPLE: +# +# helpers: +# - helpers/**/*.js +# +helpers: + - helpers/**/*.js + +# spec_files +# +# Return an array of filepaths relative to spec_dir to include. +# Default: ["**/*[sS]pec.js"] +# +# EXAMPLE: +# +# spec_files: +# - **/*[sS]pec.js +# +spec_files: + - '**/*[sS]pec.js' + +# src_dir +# +# Source directory path. Your src_files must be returned relative to this path. Will use root if left blank. +# Default: project root +# +# EXAMPLE: +# +# src_dir: public +# +src_dir: + +# spec_dir +# +# Spec directory path. Your spec_files must be returned relative to this path. +# Default: spec/javascripts +# +# EXAMPLE: +# +# spec_dir: spec/javascripts +# +spec_dir: spec/javascripts diff --git a/web/spec/javascripts/vendor/jquery.js b/web/spec/javascripts/vendor/jquery.js new file mode 100644 index 000000000..7343eba6f --- /dev/null +++ b/web/spec/javascripts/vendor/jquery.js @@ -0,0 +1,9405 @@ +/*! + * jQuery JavaScript Library v1.7.2 + * http://jquery.com/ + * + * Copyright 2011, John Resig + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * Includes Sizzle.js + * http://sizzlejs.com/ + * Copyright 2011, The Dojo Foundation + * Released under the MIT, BSD, and GPL Licenses. + * + * Date: Wed Mar 21 12:46:34 2012 -0700 + */ + +(function( window, undefined ) { + +// Use the correct document accordingly with window argument (sandbox) +var document = window.document, + navigator = window.navigator, + location = window.location; +var jQuery = (function() { + +// Define a local copy of jQuery +var jQuery = function( selector, context ) { + // The jQuery object is actually just the init constructor 'enhanced' + return new jQuery.fn.init( selector, context, rootjQuery ); + }, + + // Map over jQuery in case of overwrite + _jQuery = window.jQuery, + + // Map over the $ in case of overwrite + _$ = window.$, + + // A central reference to the root jQuery(document) + rootjQuery, + + // A simple way to check for HTML strings or ID strings + // Prioritize #id over to avoid XSS via location.hash (#9521) + quickExpr = /^(?:[^#<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/, + + // Check if a string has a non-whitespace character in it + rnotwhite = /\S/, + + // Used for trimming whitespace + trimLeft = /^\s+/, + trimRight = /\s+$/, + + // Match a standalone tag + rsingleTag = /^<(\w+)\s*\/?>(?:<\/\1>)?$/, + + // JSON RegExp + rvalidchars = /^[\],:{}\s]*$/, + rvalidescape = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, + rvalidtokens = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, + rvalidbraces = /(?:^|:|,)(?:\s*\[)+/g, + + // Useragent RegExp + rwebkit = /(webkit)[ \/]([\w.]+)/, + ropera = /(opera)(?:.*version)?[ \/]([\w.]+)/, + rmsie = /(msie) ([\w.]+)/, + rmozilla = /(mozilla)(?:.*? rv:([\w.]+))?/, + + // Matches dashed string for camelizing + rdashAlpha = /-([a-z]|[0-9])/ig, + rmsPrefix = /^-ms-/, + + // Used by jQuery.camelCase as callback to replace() + fcamelCase = function( all, letter ) { + return ( letter + "" ).toUpperCase(); + }, + + // Keep a UserAgent string for use with jQuery.browser + userAgent = navigator.userAgent, + + // For matching the engine and version of the browser + browserMatch, + + // The deferred used on DOM ready + readyList, + + // The ready event handler + DOMContentLoaded, + + // Save a reference to some core methods + toString = Object.prototype.toString, + hasOwn = Object.prototype.hasOwnProperty, + push = Array.prototype.push, + slice = Array.prototype.slice, + trim = String.prototype.trim, + indexOf = Array.prototype.indexOf, + + // [[Class]] -> type pairs + class2type = {}; + +jQuery.fn = jQuery.prototype = { + constructor: jQuery, + init: function( selector, context, rootjQuery ) { + var match, elem, ret, doc; + + // Handle $(""), $(null), or $(undefined) + if ( !selector ) { + return this; + } + + // Handle $(DOMElement) + if ( selector.nodeType ) { + this.context = this[0] = selector; + this.length = 1; + return this; + } + + // The body element only exists once, optimize finding it + if ( selector === "body" && !context && document.body ) { + this.context = document; + this[0] = document.body; + this.selector = selector; + this.length = 1; + return this; + } + + // Handle HTML strings + if ( typeof selector === "string" ) { + // Are we dealing with HTML string or an ID? + if ( selector.charAt(0) === "<" && selector.charAt( selector.length - 1 ) === ">" && selector.length >= 3 ) { + // Assume that strings that start and end with <> are HTML and skip the regex check + match = [ null, selector, null ]; + + } else { + match = quickExpr.exec( selector ); + } + + // Verify a match, and that no context was specified for #id + if ( match && (match[1] || !context) ) { + + // HANDLE: $(html) -> $(array) + if ( match[1] ) { + context = context instanceof jQuery ? context[0] : context; + doc = ( context ? context.ownerDocument || context : document ); + + // If a single string is passed in and it's a single tag + // just do a createElement and skip the rest + ret = rsingleTag.exec( selector ); + + if ( ret ) { + if ( jQuery.isPlainObject( context ) ) { + selector = [ document.createElement( ret[1] ) ]; + jQuery.fn.attr.call( selector, context, true ); + + } else { + selector = [ doc.createElement( ret[1] ) ]; + } + + } else { + ret = jQuery.buildFragment( [ match[1] ], [ doc ] ); + selector = ( ret.cacheable ? jQuery.clone(ret.fragment) : ret.fragment ).childNodes; + } + + return jQuery.merge( this, selector ); + + // HANDLE: $("#id") + } else { + elem = document.getElementById( match[2] ); + + // Check parentNode to catch when Blackberry 4.6 returns + // nodes that are no longer in the document #6963 + if ( elem && elem.parentNode ) { + // Handle the case where IE and Opera return items + // by name instead of ID + if ( elem.id !== match[2] ) { + return rootjQuery.find( selector ); + } + + // Otherwise, we inject the element directly into the jQuery object + this.length = 1; + this[0] = elem; + } + + this.context = document; + this.selector = selector; + return this; + } + + // HANDLE: $(expr, $(...)) + } else if ( !context || context.jquery ) { + return ( context || rootjQuery ).find( selector ); + + // HANDLE: $(expr, context) + // (which is just equivalent to: $(context).find(expr) + } else { + return this.constructor( context ).find( selector ); + } + + // HANDLE: $(function) + // Shortcut for document ready + } else if ( jQuery.isFunction( selector ) ) { + return rootjQuery.ready( selector ); + } + + if ( selector.selector !== undefined ) { + this.selector = selector.selector; + this.context = selector.context; + } + + return jQuery.makeArray( selector, this ); + }, + + // Start with an empty selector + selector: "", + + // The current version of jQuery being used + jquery: "1.7.2", + + // The default length of a jQuery object is 0 + length: 0, + + // The number of elements contained in the matched element set + size: function() { + return this.length; + }, + + toArray: function() { + return slice.call( this, 0 ); + }, + + // Get the Nth element in the matched element set OR + // Get the whole matched element set as a clean array + get: function( num ) { + return num == null ? + + // Return a 'clean' array + this.toArray() : + + // Return just the object + ( num < 0 ? this[ this.length + num ] : this[ num ] ); + }, + + // Take an array of elements and push it onto the stack + // (returning the new matched element set) + pushStack: function( elems, name, selector ) { + // Build a new jQuery matched element set + var ret = this.constructor(); + + if ( jQuery.isArray( elems ) ) { + push.apply( ret, elems ); + + } else { + jQuery.merge( ret, elems ); + } + + // Add the old object onto the stack (as a reference) + ret.prevObject = this; + + ret.context = this.context; + + if ( name === "find" ) { + ret.selector = this.selector + ( this.selector ? " " : "" ) + selector; + } else if ( name ) { + ret.selector = this.selector + "." + name + "(" + selector + ")"; + } + + // Return the newly-formed element set + return ret; + }, + + // Execute a callback for every element in the matched set. + // (You can seed the arguments with an array of args, but this is + // only used internally.) + each: function( callback, args ) { + return jQuery.each( this, callback, args ); + }, + + ready: function( fn ) { + // Attach the listeners + jQuery.bindReady(); + + // Add the callback + readyList.add( fn ); + + return this; + }, + + eq: function( i ) { + i = +i; + return i === -1 ? + this.slice( i ) : + this.slice( i, i + 1 ); + }, + + first: function() { + return this.eq( 0 ); + }, + + last: function() { + return this.eq( -1 ); + }, + + slice: function() { + return this.pushStack( slice.apply( this, arguments ), + "slice", slice.call(arguments).join(",") ); + }, + + map: function( callback ) { + return this.pushStack( jQuery.map(this, function( elem, i ) { + return callback.call( elem, i, elem ); + })); + }, + + end: function() { + return this.prevObject || this.constructor(null); + }, + + // For internal use only. + // Behaves like an Array's method, not like a jQuery method. + push: push, + sort: [].sort, + splice: [].splice +}; + +// Give the init function the jQuery prototype for later instantiation +jQuery.fn.init.prototype = jQuery.fn; + +jQuery.extend = jQuery.fn.extend = function() { + var options, name, src, copy, copyIsArray, clone, + target = arguments[0] || {}, + i = 1, + length = arguments.length, + deep = false; + + // Handle a deep copy situation + if ( typeof target === "boolean" ) { + deep = target; + target = arguments[1] || {}; + // skip the boolean and the target + i = 2; + } + + // Handle case when target is a string or something (possible in deep copy) + if ( typeof target !== "object" && !jQuery.isFunction(target) ) { + target = {}; + } + + // extend jQuery itself if only one argument is passed + if ( length === i ) { + target = this; + --i; + } + + for ( ; i < length; i++ ) { + // Only deal with non-null/undefined values + if ( (options = arguments[ i ]) != null ) { + // Extend the base object + for ( name in options ) { + src = target[ name ]; + copy = options[ name ]; + + // Prevent never-ending loop + if ( target === copy ) { + continue; + } + + // Recurse if we're merging plain objects or arrays + if ( deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) ) ) { + if ( copyIsArray ) { + copyIsArray = false; + clone = src && jQuery.isArray(src) ? src : []; + + } else { + clone = src && jQuery.isPlainObject(src) ? src : {}; + } + + // Never move original objects, clone them + target[ name ] = jQuery.extend( deep, clone, copy ); + + // Don't bring in undefined values + } else if ( copy !== undefined ) { + target[ name ] = copy; + } + } + } + } + + // Return the modified object + return target; +}; + +jQuery.extend({ + noConflict: function( deep ) { + if ( window.$ === jQuery ) { + window.$ = _$; + } + + if ( deep && window.jQuery === jQuery ) { + window.jQuery = _jQuery; + } + + return jQuery; + }, + + // Is the DOM ready to be used? Set to true once it occurs. + isReady: false, + + // A counter to track how many items to wait for before + // the ready event fires. See #6781 + readyWait: 1, + + // Hold (or release) the ready event + holdReady: function( hold ) { + if ( hold ) { + jQuery.readyWait++; + } else { + jQuery.ready( true ); + } + }, + + // Handle when the DOM is ready + ready: function( wait ) { + // Either a released hold or an DOMready/load event and not yet ready + if ( (wait === true && !--jQuery.readyWait) || (wait !== true && !jQuery.isReady) ) { + // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443). + if ( !document.body ) { + return setTimeout( jQuery.ready, 1 ); + } + + // Remember that the DOM is ready + jQuery.isReady = true; + + // If a normal DOM Ready event fired, decrement, and wait if need be + if ( wait !== true && --jQuery.readyWait > 0 ) { + return; + } + + // If there are functions bound, to execute + readyList.fireWith( document, [ jQuery ] ); + + // Trigger any bound ready events + if ( jQuery.fn.trigger ) { + jQuery( document ).trigger( "ready" ).off( "ready" ); + } + } + }, + + bindReady: function() { + if ( readyList ) { + return; + } + + readyList = jQuery.Callbacks( "once memory" ); + + // Catch cases where $(document).ready() is called after the + // browser event has already occurred. + if ( document.readyState === "complete" ) { + // Handle it asynchronously to allow scripts the opportunity to delay ready + return setTimeout( jQuery.ready, 1 ); + } + + // Mozilla, Opera and webkit nightlies currently support this event + if ( document.addEventListener ) { + // Use the handy event callback + document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false ); + + // A fallback to window.onload, that will always work + window.addEventListener( "load", jQuery.ready, false ); + + // If IE event model is used + } else if ( document.attachEvent ) { + // ensure firing before onload, + // maybe late but safe also for iframes + document.attachEvent( "onreadystatechange", DOMContentLoaded ); + + // A fallback to window.onload, that will always work + window.attachEvent( "onload", jQuery.ready ); + + // If IE and not a frame + // continually check to see if the document is ready + var toplevel = false; + + try { + toplevel = window.frameElement == null; + } catch(e) {} + + if ( document.documentElement.doScroll && toplevel ) { + doScrollCheck(); + } + } + }, + + // See test/unit/core.js for details concerning isFunction. + // Since version 1.3, DOM methods and functions like alert + // aren't supported. They return false on IE (#2968). + isFunction: function( obj ) { + return jQuery.type(obj) === "function"; + }, + + isArray: Array.isArray || function( obj ) { + return jQuery.type(obj) === "array"; + }, + + isWindow: function( obj ) { + return obj != null && obj == obj.window; + }, + + isNumeric: function( obj ) { + return !isNaN( parseFloat(obj) ) && isFinite( obj ); + }, + + type: function( obj ) { + return obj == null ? + String( obj ) : + class2type[ toString.call(obj) ] || "object"; + }, + + isPlainObject: function( obj ) { + // Must be an Object. + // Because of IE, we also have to check the presence of the constructor property. + // Make sure that DOM nodes and window objects don't pass through, as well + if ( !obj || jQuery.type(obj) !== "object" || obj.nodeType || jQuery.isWindow( obj ) ) { + return false; + } + + try { + // Not own constructor property must be Object + if ( obj.constructor && + !hasOwn.call(obj, "constructor") && + !hasOwn.call(obj.constructor.prototype, "isPrototypeOf") ) { + return false; + } + } catch ( e ) { + // IE8,9 Will throw exceptions on certain host objects #9897 + return false; + } + + // Own properties are enumerated firstly, so to speed up, + // if last one is own, then all properties are own. + + var key; + for ( key in obj ) {} + + return key === undefined || hasOwn.call( obj, key ); + }, + + isEmptyObject: function( obj ) { + for ( var name in obj ) { + return false; + } + return true; + }, + + error: function( msg ) { + throw new Error( msg ); + }, + + parseJSON: function( data ) { + if ( typeof data !== "string" || !data ) { + return null; + } + + // Make sure leading/trailing whitespace is removed (IE can't handle it) + data = jQuery.trim( data ); + + // Attempt to parse using the native JSON parser first + if ( window.JSON && window.JSON.parse ) { + return window.JSON.parse( data ); + } + + // Make sure the incoming data is actual JSON + // Logic borrowed from http://json.org/json2.js + if ( rvalidchars.test( data.replace( rvalidescape, "@" ) + .replace( rvalidtokens, "]" ) + .replace( rvalidbraces, "")) ) { + + return ( new Function( "return " + data ) )(); + + } + jQuery.error( "Invalid JSON: " + data ); + }, + + // Cross-browser xml parsing + parseXML: function( data ) { + if ( typeof data !== "string" || !data ) { + return null; + } + var xml, tmp; + try { + if ( window.DOMParser ) { // Standard + tmp = new DOMParser(); + xml = tmp.parseFromString( data , "text/xml" ); + } else { // IE + xml = new ActiveXObject( "Microsoft.XMLDOM" ); + xml.async = "false"; + xml.loadXML( data ); + } + } catch( e ) { + xml = undefined; + } + if ( !xml || !xml.documentElement || xml.getElementsByTagName( "parsererror" ).length ) { + jQuery.error( "Invalid XML: " + data ); + } + return xml; + }, + + noop: function() {}, + + // Evaluates a script in a global context + // Workarounds based on findings by Jim Driscoll + // http://weblogs.java.net/blog/driscoll/archive/2009/09/08/eval-javascript-global-context + globalEval: function( data ) { + if ( data && rnotwhite.test( data ) ) { + // We use execScript on Internet Explorer + // We use an anonymous function so that context is window + // rather than jQuery in Firefox + ( window.execScript || function( data ) { + window[ "eval" ].call( window, data ); + } )( data ); + } + }, + + // Convert dashed to camelCase; used by the css and data modules + // Microsoft forgot to hump their vendor prefix (#9572) + camelCase: function( string ) { + return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase ); + }, + + nodeName: function( elem, name ) { + return elem.nodeName && elem.nodeName.toUpperCase() === name.toUpperCase(); + }, + + // args is for internal usage only + each: function( object, callback, args ) { + var name, i = 0, + length = object.length, + isObj = length === undefined || jQuery.isFunction( object ); + + if ( args ) { + if ( isObj ) { + for ( name in object ) { + if ( callback.apply( object[ name ], args ) === false ) { + break; + } + } + } else { + for ( ; i < length; ) { + if ( callback.apply( object[ i++ ], args ) === false ) { + break; + } + } + } + + // A special, fast, case for the most common use of each + } else { + if ( isObj ) { + for ( name in object ) { + if ( callback.call( object[ name ], name, object[ name ] ) === false ) { + break; + } + } + } else { + for ( ; i < length; ) { + if ( callback.call( object[ i ], i, object[ i++ ] ) === false ) { + break; + } + } + } + } + + return object; + }, + + // Use native String.trim function wherever possible + trim: trim ? + function( text ) { + return text == null ? + "" : + trim.call( text ); + } : + + // Otherwise use our own trimming functionality + function( text ) { + return text == null ? + "" : + text.toString().replace( trimLeft, "" ).replace( trimRight, "" ); + }, + + // results is for internal usage only + makeArray: function( array, results ) { + var ret = results || []; + + if ( array != null ) { + // The window, strings (and functions) also have 'length' + // Tweaked logic slightly to handle Blackberry 4.7 RegExp issues #6930 + var type = jQuery.type( array ); + + if ( array.length == null || type === "string" || type === "function" || type === "regexp" || jQuery.isWindow( array ) ) { + push.call( ret, array ); + } else { + jQuery.merge( ret, array ); + } + } + + return ret; + }, + + inArray: function( elem, array, i ) { + var len; + + if ( array ) { + if ( indexOf ) { + return indexOf.call( array, elem, i ); + } + + len = array.length; + i = i ? i < 0 ? Math.max( 0, len + i ) : i : 0; + + for ( ; i < len; i++ ) { + // Skip accessing in sparse arrays + if ( i in array && array[ i ] === elem ) { + return i; + } + } + } + + return -1; + }, + + merge: function( first, second ) { + var i = first.length, + j = 0; + + if ( typeof second.length === "number" ) { + for ( var l = second.length; j < l; j++ ) { + first[ i++ ] = second[ j ]; + } + + } else { + while ( second[j] !== undefined ) { + first[ i++ ] = second[ j++ ]; + } + } + + first.length = i; + + return first; + }, + + grep: function( elems, callback, inv ) { + var ret = [], retVal; + inv = !!inv; + + // Go through the array, only saving the items + // that pass the validator function + for ( var i = 0, length = elems.length; i < length; i++ ) { + retVal = !!callback( elems[ i ], i ); + if ( inv !== retVal ) { + ret.push( elems[ i ] ); + } + } + + return ret; + }, + + // arg is for internal usage only + map: function( elems, callback, arg ) { + var value, key, ret = [], + i = 0, + length = elems.length, + // jquery objects are treated as arrays + isArray = elems instanceof jQuery || length !== undefined && typeof length === "number" && ( ( length > 0 && elems[ 0 ] && elems[ length -1 ] ) || length === 0 || jQuery.isArray( elems ) ) ; + + // Go through the array, translating each of the items to their + if ( isArray ) { + for ( ; i < length; i++ ) { + value = callback( elems[ i ], i, arg ); + + if ( value != null ) { + ret[ ret.length ] = value; + } + } + + // Go through every key on the object, + } else { + for ( key in elems ) { + value = callback( elems[ key ], key, arg ); + + if ( value != null ) { + ret[ ret.length ] = value; + } + } + } + + // Flatten any nested arrays + return ret.concat.apply( [], ret ); + }, + + // A global GUID counter for objects + guid: 1, + + // Bind a function to a context, optionally partially applying any + // arguments. + proxy: function( fn, context ) { + if ( typeof context === "string" ) { + var tmp = fn[ context ]; + context = fn; + fn = tmp; + } + + // Quick check to determine if target is callable, in the spec + // this throws a TypeError, but we will just return undefined. + if ( !jQuery.isFunction( fn ) ) { + return undefined; + } + + // Simulated bind + var args = slice.call( arguments, 2 ), + proxy = function() { + return fn.apply( context, args.concat( slice.call( arguments ) ) ); + }; + + // Set the guid of unique handler to the same of original handler, so it can be removed + proxy.guid = fn.guid = fn.guid || proxy.guid || jQuery.guid++; + + return proxy; + }, + + // Mutifunctional method to get and set values to a collection + // The value/s can optionally be executed if it's a function + access: function( elems, fn, key, value, chainable, emptyGet, pass ) { + var exec, + bulk = key == null, + i = 0, + length = elems.length; + + // Sets many values + if ( key && typeof key === "object" ) { + for ( i in key ) { + jQuery.access( elems, fn, i, key[i], 1, emptyGet, value ); + } + chainable = 1; + + // Sets one value + } else if ( value !== undefined ) { + // Optionally, function values get executed if exec is true + exec = pass === undefined && jQuery.isFunction( value ); + + if ( bulk ) { + // Bulk operations only iterate when executing function values + if ( exec ) { + exec = fn; + fn = function( elem, key, value ) { + return exec.call( jQuery( elem ), value ); + }; + + // Otherwise they run against the entire set + } else { + fn.call( elems, value ); + fn = null; + } + } + + if ( fn ) { + for (; i < length; i++ ) { + fn( elems[i], key, exec ? value.call( elems[i], i, fn( elems[i], key ) ) : value, pass ); + } + } + + chainable = 1; + } + + return chainable ? + elems : + + // Gets + bulk ? + fn.call( elems ) : + length ? fn( elems[0], key ) : emptyGet; + }, + + now: function() { + return ( new Date() ).getTime(); + }, + + // Use of jQuery.browser is frowned upon. + // More details: http://docs.jquery.com/Utilities/jQuery.browser + uaMatch: function( ua ) { + ua = ua.toLowerCase(); + + var match = rwebkit.exec( ua ) || + ropera.exec( ua ) || + rmsie.exec( ua ) || + ua.indexOf("compatible") < 0 && rmozilla.exec( ua ) || + []; + + return { browser: match[1] || "", version: match[2] || "0" }; + }, + + sub: function() { + function jQuerySub( selector, context ) { + return new jQuerySub.fn.init( selector, context ); + } + jQuery.extend( true, jQuerySub, this ); + jQuerySub.superclass = this; + jQuerySub.fn = jQuerySub.prototype = this(); + jQuerySub.fn.constructor = jQuerySub; + jQuerySub.sub = this.sub; + jQuerySub.fn.init = function init( selector, context ) { + if ( context && context instanceof jQuery && !(context instanceof jQuerySub) ) { + context = jQuerySub( context ); + } + + return jQuery.fn.init.call( this, selector, context, rootjQuerySub ); + }; + jQuerySub.fn.init.prototype = jQuerySub.fn; + var rootjQuerySub = jQuerySub(document); + return jQuerySub; + }, + + browser: {} +}); + +// Populate the class2type map +jQuery.each("Boolean Number String Function Array Date RegExp Object".split(" "), function(i, name) { + class2type[ "[object " + name + "]" ] = name.toLowerCase(); +}); + +browserMatch = jQuery.uaMatch( userAgent ); +if ( browserMatch.browser ) { + jQuery.browser[ browserMatch.browser ] = true; + jQuery.browser.version = browserMatch.version; +} + +// Deprecated, use jQuery.browser.webkit instead +if ( jQuery.browser.webkit ) { + jQuery.browser.safari = true; +} + +// IE doesn't match non-breaking spaces with \s +if ( rnotwhite.test( "\xA0" ) ) { + trimLeft = /^[\s\xA0]+/; + trimRight = /[\s\xA0]+$/; +} + +// All jQuery objects should point back to these +rootjQuery = jQuery(document); + +// Cleanup functions for the document ready method +if ( document.addEventListener ) { + DOMContentLoaded = function() { + document.removeEventListener( "DOMContentLoaded", DOMContentLoaded, false ); + jQuery.ready(); + }; + +} else if ( document.attachEvent ) { + DOMContentLoaded = function() { + // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443). + if ( document.readyState === "complete" ) { + document.detachEvent( "onreadystatechange", DOMContentLoaded ); + jQuery.ready(); + } + }; +} + +// The DOM ready check for Internet Explorer +function doScrollCheck() { + if ( jQuery.isReady ) { + return; + } + + try { + // If IE is used, use the trick by Diego Perini + // http://javascript.nwbox.com/IEContentLoaded/ + document.documentElement.doScroll("left"); + } catch(e) { + setTimeout( doScrollCheck, 1 ); + return; + } + + // and execute any waiting functions + jQuery.ready(); +} + +return jQuery; + +})(); + + +// String to Object flags format cache +var flagsCache = {}; + +// Convert String-formatted flags into Object-formatted ones and store in cache +function createFlags( flags ) { + var object = flagsCache[ flags ] = {}, + i, length; + flags = flags.split( /\s+/ ); + for ( i = 0, length = flags.length; i < length; i++ ) { + object[ flags[i] ] = true; + } + return object; +} + +/* + * Create a callback list using the following parameters: + * + * flags: an optional list of space-separated flags that will change how + * the callback list behaves + * + * By default a callback list will act like an event callback list and can be + * "fired" multiple times. + * + * Possible flags: + * + * once: will ensure the callback list can only be fired once (like a Deferred) + * + * memory: will keep track of previous values and will call any callback added + * after the list has been fired right away with the latest "memorized" + * values (like a Deferred) + * + * unique: will ensure a callback can only be added once (no duplicate in the list) + * + * stopOnFalse: interrupt callings when a callback returns false + * + */ +jQuery.Callbacks = function( flags ) { + + // Convert flags from String-formatted to Object-formatted + // (we check in cache first) + flags = flags ? ( flagsCache[ flags ] || createFlags( flags ) ) : {}; + + var // Actual callback list + list = [], + // Stack of fire calls for repeatable lists + stack = [], + // Last fire value (for non-forgettable lists) + memory, + // Flag to know if list was already fired + fired, + // Flag to know if list is currently firing + firing, + // First callback to fire (used internally by add and fireWith) + firingStart, + // End of the loop when firing + firingLength, + // Index of currently firing callback (modified by remove if needed) + firingIndex, + // Add one or several callbacks to the list + add = function( args ) { + var i, + length, + elem, + type, + actual; + for ( i = 0, length = args.length; i < length; i++ ) { + elem = args[ i ]; + type = jQuery.type( elem ); + if ( type === "array" ) { + // Inspect recursively + add( elem ); + } else if ( type === "function" ) { + // Add if not in unique mode and callback is not in + if ( !flags.unique || !self.has( elem ) ) { + list.push( elem ); + } + } + } + }, + // Fire callbacks + fire = function( context, args ) { + args = args || []; + memory = !flags.memory || [ context, args ]; + fired = true; + firing = true; + firingIndex = firingStart || 0; + firingStart = 0; + firingLength = list.length; + for ( ; list && firingIndex < firingLength; firingIndex++ ) { + if ( list[ firingIndex ].apply( context, args ) === false && flags.stopOnFalse ) { + memory = true; // Mark as halted + break; + } + } + firing = false; + if ( list ) { + if ( !flags.once ) { + if ( stack && stack.length ) { + memory = stack.shift(); + self.fireWith( memory[ 0 ], memory[ 1 ] ); + } + } else if ( memory === true ) { + self.disable(); + } else { + list = []; + } + } + }, + // Actual Callbacks object + self = { + // Add a callback or a collection of callbacks to the list + add: function() { + if ( list ) { + var length = list.length; + add( arguments ); + // Do we need to add the callbacks to the + // current firing batch? + if ( firing ) { + firingLength = list.length; + // With memory, if we're not firing then + // we should call right away, unless previous + // firing was halted (stopOnFalse) + } else if ( memory && memory !== true ) { + firingStart = length; + fire( memory[ 0 ], memory[ 1 ] ); + } + } + return this; + }, + // Remove a callback from the list + remove: function() { + if ( list ) { + var args = arguments, + argIndex = 0, + argLength = args.length; + for ( ; argIndex < argLength ; argIndex++ ) { + for ( var i = 0; i < list.length; i++ ) { + if ( args[ argIndex ] === list[ i ] ) { + // Handle firingIndex and firingLength + if ( firing ) { + if ( i <= firingLength ) { + firingLength--; + if ( i <= firingIndex ) { + firingIndex--; + } + } + } + // Remove the element + list.splice( i--, 1 ); + // If we have some unicity property then + // we only need to do this once + if ( flags.unique ) { + break; + } + } + } + } + } + return this; + }, + // Control if a given callback is in the list + has: function( fn ) { + if ( list ) { + var i = 0, + length = list.length; + for ( ; i < length; i++ ) { + if ( fn === list[ i ] ) { + return true; + } + } + } + return false; + }, + // Remove all callbacks from the list + empty: function() { + list = []; + return this; + }, + // Have the list do nothing anymore + disable: function() { + list = stack = memory = undefined; + return this; + }, + // Is it disabled? + disabled: function() { + return !list; + }, + // Lock the list in its current state + lock: function() { + stack = undefined; + if ( !memory || memory === true ) { + self.disable(); + } + return this; + }, + // Is it locked? + locked: function() { + return !stack; + }, + // Call all callbacks with the given context and arguments + fireWith: function( context, args ) { + if ( stack ) { + if ( firing ) { + if ( !flags.once ) { + stack.push( [ context, args ] ); + } + } else if ( !( flags.once && memory ) ) { + fire( context, args ); + } + } + return this; + }, + // Call all the callbacks with the given arguments + fire: function() { + self.fireWith( this, arguments ); + return this; + }, + // To know if the callbacks have already been called at least once + fired: function() { + return !!fired; + } + }; + + return self; +}; + + + + +var // Static reference to slice + sliceDeferred = [].slice; + +jQuery.extend({ + + Deferred: function( func ) { + var doneList = jQuery.Callbacks( "once memory" ), + failList = jQuery.Callbacks( "once memory" ), + progressList = jQuery.Callbacks( "memory" ), + state = "pending", + lists = { + resolve: doneList, + reject: failList, + notify: progressList + }, + promise = { + done: doneList.add, + fail: failList.add, + progress: progressList.add, + + state: function() { + return state; + }, + + // Deprecated + isResolved: doneList.fired, + isRejected: failList.fired, + + then: function( doneCallbacks, failCallbacks, progressCallbacks ) { + deferred.done( doneCallbacks ).fail( failCallbacks ).progress( progressCallbacks ); + return this; + }, + always: function() { + deferred.done.apply( deferred, arguments ).fail.apply( deferred, arguments ); + return this; + }, + pipe: function( fnDone, fnFail, fnProgress ) { + return jQuery.Deferred(function( newDefer ) { + jQuery.each( { + done: [ fnDone, "resolve" ], + fail: [ fnFail, "reject" ], + progress: [ fnProgress, "notify" ] + }, function( handler, data ) { + var fn = data[ 0 ], + action = data[ 1 ], + returned; + if ( jQuery.isFunction( fn ) ) { + deferred[ handler ](function() { + returned = fn.apply( this, arguments ); + if ( returned && jQuery.isFunction( returned.promise ) ) { + returned.promise().then( newDefer.resolve, newDefer.reject, newDefer.notify ); + } else { + newDefer[ action + "With" ]( this === deferred ? newDefer : this, [ returned ] ); + } + }); + } else { + deferred[ handler ]( newDefer[ action ] ); + } + }); + }).promise(); + }, + // Get a promise for this deferred + // If obj is provided, the promise aspect is added to the object + promise: function( obj ) { + if ( obj == null ) { + obj = promise; + } else { + for ( var key in promise ) { + obj[ key ] = promise[ key ]; + } + } + return obj; + } + }, + deferred = promise.promise({}), + key; + + for ( key in lists ) { + deferred[ key ] = lists[ key ].fire; + deferred[ key + "With" ] = lists[ key ].fireWith; + } + + // Handle state + deferred.done( function() { + state = "resolved"; + }, failList.disable, progressList.lock ).fail( function() { + state = "rejected"; + }, doneList.disable, progressList.lock ); + + // Call given func if any + if ( func ) { + func.call( deferred, deferred ); + } + + // All done! + return deferred; + }, + + // Deferred helper + when: function( firstParam ) { + var args = sliceDeferred.call( arguments, 0 ), + i = 0, + length = args.length, + pValues = new Array( length ), + count = length, + pCount = length, + deferred = length <= 1 && firstParam && jQuery.isFunction( firstParam.promise ) ? + firstParam : + jQuery.Deferred(), + promise = deferred.promise(); + function resolveFunc( i ) { + return function( value ) { + args[ i ] = arguments.length > 1 ? sliceDeferred.call( arguments, 0 ) : value; + if ( !( --count ) ) { + deferred.resolveWith( deferred, args ); + } + }; + } + function progressFunc( i ) { + return function( value ) { + pValues[ i ] = arguments.length > 1 ? sliceDeferred.call( arguments, 0 ) : value; + deferred.notifyWith( promise, pValues ); + }; + } + if ( length > 1 ) { + for ( ; i < length; i++ ) { + if ( args[ i ] && args[ i ].promise && jQuery.isFunction( args[ i ].promise ) ) { + args[ i ].promise().then( resolveFunc(i), deferred.reject, progressFunc(i) ); + } else { + --count; + } + } + if ( !count ) { + deferred.resolveWith( deferred, args ); + } + } else if ( deferred !== firstParam ) { + deferred.resolveWith( deferred, length ? [ firstParam ] : [] ); + } + return promise; + } +}); + + + + +jQuery.support = (function() { + + var support, + all, + a, + select, + opt, + input, + fragment, + tds, + events, + eventName, + i, + isSupported, + div = document.createElement( "div" ), + documentElement = document.documentElement; + + // Preliminary tests + div.setAttribute("className", "t"); + div.innerHTML = "
        a"; + + all = div.getElementsByTagName( "*" ); + a = div.getElementsByTagName( "a" )[ 0 ]; + + // Can't get basic test support + if ( !all || !all.length || !a ) { + return {}; + } + + // First batch of supports tests + select = document.createElement( "select" ); + opt = select.appendChild( document.createElement("option") ); + input = div.getElementsByTagName( "input" )[ 0 ]; + + support = { + // IE strips leading whitespace when .innerHTML is used + leadingWhitespace: ( div.firstChild.nodeType === 3 ), + + // Make sure that tbody elements aren't automatically inserted + // IE will insert them into empty tables + tbody: !div.getElementsByTagName("tbody").length, + + // Make sure that link elements get serialized correctly by innerHTML + // This requires a wrapper element in IE + htmlSerialize: !!div.getElementsByTagName("link").length, + + // Get the style information from getAttribute + // (IE uses .cssText instead) + style: /top/.test( a.getAttribute("style") ), + + // Make sure that URLs aren't manipulated + // (IE normalizes it by default) + hrefNormalized: ( a.getAttribute("href") === "/a" ), + + // Make sure that element opacity exists + // (IE uses filter instead) + // Use a regex to work around a WebKit issue. See #5145 + opacity: /^0.55/.test( a.style.opacity ), + + // Verify style float existence + // (IE uses styleFloat instead of cssFloat) + cssFloat: !!a.style.cssFloat, + + // Make sure that if no value is specified for a checkbox + // that it defaults to "on". + // (WebKit defaults to "" instead) + checkOn: ( input.value === "on" ), + + // Make sure that a selected-by-default option has a working selected property. + // (WebKit defaults to false instead of true, IE too, if it's in an optgroup) + optSelected: opt.selected, + + // Test setAttribute on camelCase class. If it works, we need attrFixes when doing get/setAttribute (ie6/7) + getSetAttribute: div.className !== "t", + + // Tests for enctype support on a form(#6743) + enctype: !!document.createElement("form").enctype, + + // Makes sure cloning an html5 element does not cause problems + // Where outerHTML is undefined, this still works + html5Clone: document.createElement("nav").cloneNode( true ).outerHTML !== "<:nav>", + + // Will be defined later + submitBubbles: true, + changeBubbles: true, + focusinBubbles: false, + deleteExpando: true, + noCloneEvent: true, + inlineBlockNeedsLayout: false, + shrinkWrapBlocks: false, + reliableMarginRight: true, + pixelMargin: true + }; + + // jQuery.boxModel DEPRECATED in 1.3, use jQuery.support.boxModel instead + jQuery.boxModel = support.boxModel = (document.compatMode === "CSS1Compat"); + + // Make sure checked status is properly cloned + input.checked = true; + support.noCloneChecked = input.cloneNode( true ).checked; + + // Make sure that the options inside disabled selects aren't marked as disabled + // (WebKit marks them as disabled) + select.disabled = true; + support.optDisabled = !opt.disabled; + + // Test to see if it's possible to delete an expando from an element + // Fails in Internet Explorer + try { + delete div.test; + } catch( e ) { + support.deleteExpando = false; + } + + if ( !div.addEventListener && div.attachEvent && div.fireEvent ) { + div.attachEvent( "onclick", function() { + // Cloning a node shouldn't copy over any + // bound event handlers (IE does this) + support.noCloneEvent = false; + }); + div.cloneNode( true ).fireEvent( "onclick" ); + } + + // Check if a radio maintains its value + // after being appended to the DOM + input = document.createElement("input"); + input.value = "t"; + input.setAttribute("type", "radio"); + support.radioValue = input.value === "t"; + + input.setAttribute("checked", "checked"); + + // #11217 - WebKit loses check when the name is after the checked attribute + input.setAttribute( "name", "t" ); + + div.appendChild( input ); + fragment = document.createDocumentFragment(); + fragment.appendChild( div.lastChild ); + + // WebKit doesn't clone checked state correctly in fragments + support.checkClone = fragment.cloneNode( true ).cloneNode( true ).lastChild.checked; + + // Check if a disconnected checkbox will retain its checked + // value of true after appended to the DOM (IE6/7) + support.appendChecked = input.checked; + + fragment.removeChild( input ); + fragment.appendChild( div ); + + // Technique from Juriy Zaytsev + // http://perfectionkills.com/detecting-event-support-without-browser-sniffing/ + // We only care about the case where non-standard event systems + // are used, namely in IE. Short-circuiting here helps us to + // avoid an eval call (in setAttribute) which can cause CSP + // to go haywire. See: https://developer.mozilla.org/en/Security/CSP + if ( div.attachEvent ) { + for ( i in { + submit: 1, + change: 1, + focusin: 1 + }) { + eventName = "on" + i; + isSupported = ( eventName in div ); + if ( !isSupported ) { + div.setAttribute( eventName, "return;" ); + isSupported = ( typeof div[ eventName ] === "function" ); + } + support[ i + "Bubbles" ] = isSupported; + } + } + + fragment.removeChild( div ); + + // Null elements to avoid leaks in IE + fragment = select = opt = div = input = null; + + // Run tests that need a body at doc ready + jQuery(function() { + var container, outer, inner, table, td, offsetSupport, + marginDiv, conMarginTop, style, html, positionTopLeftWidthHeight, + paddingMarginBorderVisibility, paddingMarginBorder, + body = document.getElementsByTagName("body")[0]; + + if ( !body ) { + // Return for frameset docs that don't have a body + return; + } + + conMarginTop = 1; + paddingMarginBorder = "padding:0;margin:0;border:"; + positionTopLeftWidthHeight = "position:absolute;top:0;left:0;width:1px;height:1px;"; + paddingMarginBorderVisibility = paddingMarginBorder + "0;visibility:hidden;"; + style = "style='" + positionTopLeftWidthHeight + paddingMarginBorder + "5px solid #000;"; + html = "
        " + + "" + + "
        "; + + container = document.createElement("div"); + container.style.cssText = paddingMarginBorderVisibility + "width:0;height:0;position:static;top:0;margin-top:" + conMarginTop + "px"; + body.insertBefore( container, body.firstChild ); + + // Construct the test element + div = document.createElement("div"); + container.appendChild( div ); + + // Check if table cells still have offsetWidth/Height when they are set + // to display:none and there are still other visible table cells in a + // table row; if so, offsetWidth/Height are not reliable for use when + // determining if an element has been hidden directly using + // display:none (it is still safe to use offsets if a parent element is + // hidden; don safety goggles and see bug #4512 for more information). + // (only IE 8 fails this test) + div.innerHTML = "
        t
        "; + tds = div.getElementsByTagName( "td" ); + isSupported = ( tds[ 0 ].offsetHeight === 0 ); + + tds[ 0 ].style.display = ""; + tds[ 1 ].style.display = "none"; + + // Check if empty table cells still have offsetWidth/Height + // (IE <= 8 fail this test) + support.reliableHiddenOffsets = isSupported && ( tds[ 0 ].offsetHeight === 0 ); + + // Check if div with explicit width and no margin-right incorrectly + // gets computed margin-right based on width of container. For more + // info see bug #3333 + // Fails in WebKit before Feb 2011 nightlies + // WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right + if ( window.getComputedStyle ) { + div.innerHTML = ""; + marginDiv = document.createElement( "div" ); + marginDiv.style.width = "0"; + marginDiv.style.marginRight = "0"; + div.style.width = "2px"; + div.appendChild( marginDiv ); + support.reliableMarginRight = + ( parseInt( ( window.getComputedStyle( marginDiv, null ) || { marginRight: 0 } ).marginRight, 10 ) || 0 ) === 0; + } + + if ( typeof div.style.zoom !== "undefined" ) { + // Check if natively block-level elements act like inline-block + // elements when setting their display to 'inline' and giving + // them layout + // (IE < 8 does this) + div.innerHTML = ""; + div.style.width = div.style.padding = "1px"; + div.style.border = 0; + div.style.overflow = "hidden"; + div.style.display = "inline"; + div.style.zoom = 1; + support.inlineBlockNeedsLayout = ( div.offsetWidth === 3 ); + + // Check if elements with layout shrink-wrap their children + // (IE 6 does this) + div.style.display = "block"; + div.style.overflow = "visible"; + div.innerHTML = "
        "; + support.shrinkWrapBlocks = ( div.offsetWidth !== 3 ); + } + + div.style.cssText = positionTopLeftWidthHeight + paddingMarginBorderVisibility; + div.innerHTML = html; + + outer = div.firstChild; + inner = outer.firstChild; + td = outer.nextSibling.firstChild.firstChild; + + offsetSupport = { + doesNotAddBorder: ( inner.offsetTop !== 5 ), + doesAddBorderForTableAndCells: ( td.offsetTop === 5 ) + }; + + inner.style.position = "fixed"; + inner.style.top = "20px"; + + // safari subtracts parent border width here which is 5px + offsetSupport.fixedPosition = ( inner.offsetTop === 20 || inner.offsetTop === 15 ); + inner.style.position = inner.style.top = ""; + + outer.style.overflow = "hidden"; + outer.style.position = "relative"; + + offsetSupport.subtractsBorderForOverflowNotVisible = ( inner.offsetTop === -5 ); + offsetSupport.doesNotIncludeMarginInBodyOffset = ( body.offsetTop !== conMarginTop ); + + if ( window.getComputedStyle ) { + div.style.marginTop = "1%"; + support.pixelMargin = ( window.getComputedStyle( div, null ) || { marginTop: 0 } ).marginTop !== "1%"; + } + + if ( typeof container.style.zoom !== "undefined" ) { + container.style.zoom = 1; + } + + body.removeChild( container ); + marginDiv = div = container = null; + + jQuery.extend( support, offsetSupport ); + }); + + return support; +})(); + + + + +var rbrace = /^(?:\{.*\}|\[.*\])$/, + rmultiDash = /([A-Z])/g; + +jQuery.extend({ + cache: {}, + + // Please use with caution + uuid: 0, + + // Unique for each copy of jQuery on the page + // Non-digits removed to match rinlinejQuery + expando: "jQuery" + ( jQuery.fn.jquery + Math.random() ).replace( /\D/g, "" ), + + // The following elements throw uncatchable exceptions if you + // attempt to add expando properties to them. + noData: { + "embed": true, + // Ban all objects except for Flash (which handle expandos) + "object": "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000", + "applet": true + }, + + hasData: function( elem ) { + elem = elem.nodeType ? jQuery.cache[ elem[jQuery.expando] ] : elem[ jQuery.expando ]; + return !!elem && !isEmptyDataObject( elem ); + }, + + data: function( elem, name, data, pvt /* Internal Use Only */ ) { + if ( !jQuery.acceptData( elem ) ) { + return; + } + + var privateCache, thisCache, ret, + internalKey = jQuery.expando, + getByName = typeof name === "string", + + // We have to handle DOM nodes and JS objects differently because IE6-7 + // can't GC object references properly across the DOM-JS boundary + isNode = elem.nodeType, + + // Only DOM nodes need the global jQuery cache; JS object data is + // attached directly to the object so GC can occur automatically + cache = isNode ? jQuery.cache : elem, + + // Only defining an ID for JS objects if its cache already exists allows + // the code to shortcut on the same path as a DOM node with no cache + id = isNode ? elem[ internalKey ] : elem[ internalKey ] && internalKey, + isEvents = name === "events"; + + // Avoid doing any more work than we need to when trying to get data on an + // object that has no data at all + if ( (!id || !cache[id] || (!isEvents && !pvt && !cache[id].data)) && getByName && data === undefined ) { + return; + } + + if ( !id ) { + // Only DOM nodes need a new unique ID for each element since their data + // ends up in the global cache + if ( isNode ) { + elem[ internalKey ] = id = ++jQuery.uuid; + } else { + id = internalKey; + } + } + + if ( !cache[ id ] ) { + cache[ id ] = {}; + + // Avoids exposing jQuery metadata on plain JS objects when the object + // is serialized using JSON.stringify + if ( !isNode ) { + cache[ id ].toJSON = jQuery.noop; + } + } + + // An object can be passed to jQuery.data instead of a key/value pair; this gets + // shallow copied over onto the existing cache + if ( typeof name === "object" || typeof name === "function" ) { + if ( pvt ) { + cache[ id ] = jQuery.extend( cache[ id ], name ); + } else { + cache[ id ].data = jQuery.extend( cache[ id ].data, name ); + } + } + + privateCache = thisCache = cache[ id ]; + + // jQuery data() is stored in a separate object inside the object's internal data + // cache in order to avoid key collisions between internal data and user-defined + // data. + if ( !pvt ) { + if ( !thisCache.data ) { + thisCache.data = {}; + } + + thisCache = thisCache.data; + } + + if ( data !== undefined ) { + thisCache[ jQuery.camelCase( name ) ] = data; + } + + // Users should not attempt to inspect the internal events object using jQuery.data, + // it is undocumented and subject to change. But does anyone listen? No. + if ( isEvents && !thisCache[ name ] ) { + return privateCache.events; + } + + // Check for both converted-to-camel and non-converted data property names + // If a data property was specified + if ( getByName ) { + + // First Try to find as-is property data + ret = thisCache[ name ]; + + // Test for null|undefined property data + if ( ret == null ) { + + // Try to find the camelCased property + ret = thisCache[ jQuery.camelCase( name ) ]; + } + } else { + ret = thisCache; + } + + return ret; + }, + + removeData: function( elem, name, pvt /* Internal Use Only */ ) { + if ( !jQuery.acceptData( elem ) ) { + return; + } + + var thisCache, i, l, + + // Reference to internal data cache key + internalKey = jQuery.expando, + + isNode = elem.nodeType, + + // See jQuery.data for more information + cache = isNode ? jQuery.cache : elem, + + // See jQuery.data for more information + id = isNode ? elem[ internalKey ] : internalKey; + + // If there is already no cache entry for this object, there is no + // purpose in continuing + if ( !cache[ id ] ) { + return; + } + + if ( name ) { + + thisCache = pvt ? cache[ id ] : cache[ id ].data; + + if ( thisCache ) { + + // Support array or space separated string names for data keys + if ( !jQuery.isArray( name ) ) { + + // try the string as a key before any manipulation + if ( name in thisCache ) { + name = [ name ]; + } else { + + // split the camel cased version by spaces unless a key with the spaces exists + name = jQuery.camelCase( name ); + if ( name in thisCache ) { + name = [ name ]; + } else { + name = name.split( " " ); + } + } + } + + for ( i = 0, l = name.length; i < l; i++ ) { + delete thisCache[ name[i] ]; + } + + // If there is no data left in the cache, we want to continue + // and let the cache object itself get destroyed + if ( !( pvt ? isEmptyDataObject : jQuery.isEmptyObject )( thisCache ) ) { + return; + } + } + } + + // See jQuery.data for more information + if ( !pvt ) { + delete cache[ id ].data; + + // Don't destroy the parent cache unless the internal data object + // had been the only thing left in it + if ( !isEmptyDataObject(cache[ id ]) ) { + return; + } + } + + // Browsers that fail expando deletion also refuse to delete expandos on + // the window, but it will allow it on all other JS objects; other browsers + // don't care + // Ensure that `cache` is not a window object #10080 + if ( jQuery.support.deleteExpando || !cache.setInterval ) { + delete cache[ id ]; + } else { + cache[ id ] = null; + } + + // We destroyed the cache and need to eliminate the expando on the node to avoid + // false lookups in the cache for entries that no longer exist + if ( isNode ) { + // IE does not allow us to delete expando properties from nodes, + // nor does it have a removeAttribute function on Document nodes; + // we must handle all of these cases + if ( jQuery.support.deleteExpando ) { + delete elem[ internalKey ]; + } else if ( elem.removeAttribute ) { + elem.removeAttribute( internalKey ); + } else { + elem[ internalKey ] = null; + } + } + }, + + // For internal use only. + _data: function( elem, name, data ) { + return jQuery.data( elem, name, data, true ); + }, + + // A method for determining if a DOM node can handle the data expando + acceptData: function( elem ) { + if ( elem.nodeName ) { + var match = jQuery.noData[ elem.nodeName.toLowerCase() ]; + + if ( match ) { + return !(match === true || elem.getAttribute("classid") !== match); + } + } + + return true; + } +}); + +jQuery.fn.extend({ + data: function( key, value ) { + var parts, part, attr, name, l, + elem = this[0], + i = 0, + data = null; + + // Gets all values + if ( key === undefined ) { + if ( this.length ) { + data = jQuery.data( elem ); + + if ( elem.nodeType === 1 && !jQuery._data( elem, "parsedAttrs" ) ) { + attr = elem.attributes; + for ( l = attr.length; i < l; i++ ) { + name = attr[i].name; + + if ( name.indexOf( "data-" ) === 0 ) { + name = jQuery.camelCase( name.substring(5) ); + + dataAttr( elem, name, data[ name ] ); + } + } + jQuery._data( elem, "parsedAttrs", true ); + } + } + + return data; + } + + // Sets multiple values + if ( typeof key === "object" ) { + return this.each(function() { + jQuery.data( this, key ); + }); + } + + parts = key.split( ".", 2 ); + parts[1] = parts[1] ? "." + parts[1] : ""; + part = parts[1] + "!"; + + return jQuery.access( this, function( value ) { + + if ( value === undefined ) { + data = this.triggerHandler( "getData" + part, [ parts[0] ] ); + + // Try to fetch any internally stored data first + if ( data === undefined && elem ) { + data = jQuery.data( elem, key ); + data = dataAttr( elem, key, data ); + } + + return data === undefined && parts[1] ? + this.data( parts[0] ) : + data; + } + + parts[1] = value; + this.each(function() { + var self = jQuery( this ); + + self.triggerHandler( "setData" + part, parts ); + jQuery.data( this, key, value ); + self.triggerHandler( "changeData" + part, parts ); + }); + }, null, value, arguments.length > 1, null, false ); + }, + + removeData: function( key ) { + return this.each(function() { + jQuery.removeData( this, key ); + }); + } +}); + +function dataAttr( elem, key, data ) { + // If nothing was found internally, try to fetch any + // data from the HTML5 data-* attribute + if ( data === undefined && elem.nodeType === 1 ) { + + var name = "data-" + key.replace( rmultiDash, "-$1" ).toLowerCase(); + + data = elem.getAttribute( name ); + + if ( typeof data === "string" ) { + try { + data = data === "true" ? true : + data === "false" ? false : + data === "null" ? null : + jQuery.isNumeric( data ) ? +data : + rbrace.test( data ) ? jQuery.parseJSON( data ) : + data; + } catch( e ) {} + + // Make sure we set the data so it isn't changed later + jQuery.data( elem, key, data ); + + } else { + data = undefined; + } + } + + return data; +} + +// checks a cache object for emptiness +function isEmptyDataObject( obj ) { + for ( var name in obj ) { + + // if the public data object is empty, the private is still empty + if ( name === "data" && jQuery.isEmptyObject( obj[name] ) ) { + continue; + } + if ( name !== "toJSON" ) { + return false; + } + } + + return true; +} + + + + +function handleQueueMarkDefer( elem, type, src ) { + var deferDataKey = type + "defer", + queueDataKey = type + "queue", + markDataKey = type + "mark", + defer = jQuery._data( elem, deferDataKey ); + if ( defer && + ( src === "queue" || !jQuery._data(elem, queueDataKey) ) && + ( src === "mark" || !jQuery._data(elem, markDataKey) ) ) { + // Give room for hard-coded callbacks to fire first + // and eventually mark/queue something else on the element + setTimeout( function() { + if ( !jQuery._data( elem, queueDataKey ) && + !jQuery._data( elem, markDataKey ) ) { + jQuery.removeData( elem, deferDataKey, true ); + defer.fire(); + } + }, 0 ); + } +} + +jQuery.extend({ + + _mark: function( elem, type ) { + if ( elem ) { + type = ( type || "fx" ) + "mark"; + jQuery._data( elem, type, (jQuery._data( elem, type ) || 0) + 1 ); + } + }, + + _unmark: function( force, elem, type ) { + if ( force !== true ) { + type = elem; + elem = force; + force = false; + } + if ( elem ) { + type = type || "fx"; + var key = type + "mark", + count = force ? 0 : ( (jQuery._data( elem, key ) || 1) - 1 ); + if ( count ) { + jQuery._data( elem, key, count ); + } else { + jQuery.removeData( elem, key, true ); + handleQueueMarkDefer( elem, type, "mark" ); + } + } + }, + + queue: function( elem, type, data ) { + var q; + if ( elem ) { + type = ( type || "fx" ) + "queue"; + q = jQuery._data( elem, type ); + + // Speed up dequeue by getting out quickly if this is just a lookup + if ( data ) { + if ( !q || jQuery.isArray(data) ) { + q = jQuery._data( elem, type, jQuery.makeArray(data) ); + } else { + q.push( data ); + } + } + return q || []; + } + }, + + dequeue: function( elem, type ) { + type = type || "fx"; + + var queue = jQuery.queue( elem, type ), + fn = queue.shift(), + hooks = {}; + + // If the fx queue is dequeued, always remove the progress sentinel + if ( fn === "inprogress" ) { + fn = queue.shift(); + } + + if ( fn ) { + // Add a progress sentinel to prevent the fx queue from being + // automatically dequeued + if ( type === "fx" ) { + queue.unshift( "inprogress" ); + } + + jQuery._data( elem, type + ".run", hooks ); + fn.call( elem, function() { + jQuery.dequeue( elem, type ); + }, hooks ); + } + + if ( !queue.length ) { + jQuery.removeData( elem, type + "queue " + type + ".run", true ); + handleQueueMarkDefer( elem, type, "queue" ); + } + } +}); + +jQuery.fn.extend({ + queue: function( type, data ) { + var setter = 2; + + if ( typeof type !== "string" ) { + data = type; + type = "fx"; + setter--; + } + + if ( arguments.length < setter ) { + return jQuery.queue( this[0], type ); + } + + return data === undefined ? + this : + this.each(function() { + var queue = jQuery.queue( this, type, data ); + + if ( type === "fx" && queue[0] !== "inprogress" ) { + jQuery.dequeue( this, type ); + } + }); + }, + dequeue: function( type ) { + return this.each(function() { + jQuery.dequeue( this, type ); + }); + }, + // Based off of the plugin by Clint Helfers, with permission. + // http://blindsignals.com/index.php/2009/07/jquery-delay/ + delay: function( time, type ) { + time = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time; + type = type || "fx"; + + return this.queue( type, function( next, hooks ) { + var timeout = setTimeout( next, time ); + hooks.stop = function() { + clearTimeout( timeout ); + }; + }); + }, + clearQueue: function( type ) { + return this.queue( type || "fx", [] ); + }, + // Get a promise resolved when queues of a certain type + // are emptied (fx is the type by default) + promise: function( type, object ) { + if ( typeof type !== "string" ) { + object = type; + type = undefined; + } + type = type || "fx"; + var defer = jQuery.Deferred(), + elements = this, + i = elements.length, + count = 1, + deferDataKey = type + "defer", + queueDataKey = type + "queue", + markDataKey = type + "mark", + tmp; + function resolve() { + if ( !( --count ) ) { + defer.resolveWith( elements, [ elements ] ); + } + } + while( i-- ) { + if (( tmp = jQuery.data( elements[ i ], deferDataKey, undefined, true ) || + ( jQuery.data( elements[ i ], queueDataKey, undefined, true ) || + jQuery.data( elements[ i ], markDataKey, undefined, true ) ) && + jQuery.data( elements[ i ], deferDataKey, jQuery.Callbacks( "once memory" ), true ) )) { + count++; + tmp.add( resolve ); + } + } + resolve(); + return defer.promise( object ); + } +}); + + + + +var rclass = /[\n\t\r]/g, + rspace = /\s+/, + rreturn = /\r/g, + rtype = /^(?:button|input)$/i, + rfocusable = /^(?:button|input|object|select|textarea)$/i, + rclickable = /^a(?:rea)?$/i, + rboolean = /^(?:autofocus|autoplay|async|checked|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped|selected)$/i, + getSetAttribute = jQuery.support.getSetAttribute, + nodeHook, boolHook, fixSpecified; + +jQuery.fn.extend({ + attr: function( name, value ) { + return jQuery.access( this, jQuery.attr, name, value, arguments.length > 1 ); + }, + + removeAttr: function( name ) { + return this.each(function() { + jQuery.removeAttr( this, name ); + }); + }, + + prop: function( name, value ) { + return jQuery.access( this, jQuery.prop, name, value, arguments.length > 1 ); + }, + + removeProp: function( name ) { + name = jQuery.propFix[ name ] || name; + return this.each(function() { + // try/catch handles cases where IE balks (such as removing a property on window) + try { + this[ name ] = undefined; + delete this[ name ]; + } catch( e ) {} + }); + }, + + addClass: function( value ) { + var classNames, i, l, elem, + setClass, c, cl; + + if ( jQuery.isFunction( value ) ) { + return this.each(function( j ) { + jQuery( this ).addClass( value.call(this, j, this.className) ); + }); + } + + if ( value && typeof value === "string" ) { + classNames = value.split( rspace ); + + for ( i = 0, l = this.length; i < l; i++ ) { + elem = this[ i ]; + + if ( elem.nodeType === 1 ) { + if ( !elem.className && classNames.length === 1 ) { + elem.className = value; + + } else { + setClass = " " + elem.className + " "; + + for ( c = 0, cl = classNames.length; c < cl; c++ ) { + if ( !~setClass.indexOf( " " + classNames[ c ] + " " ) ) { + setClass += classNames[ c ] + " "; + } + } + elem.className = jQuery.trim( setClass ); + } + } + } + } + + return this; + }, + + removeClass: function( value ) { + var classNames, i, l, elem, className, c, cl; + + if ( jQuery.isFunction( value ) ) { + return this.each(function( j ) { + jQuery( this ).removeClass( value.call(this, j, this.className) ); + }); + } + + if ( (value && typeof value === "string") || value === undefined ) { + classNames = ( value || "" ).split( rspace ); + + for ( i = 0, l = this.length; i < l; i++ ) { + elem = this[ i ]; + + if ( elem.nodeType === 1 && elem.className ) { + if ( value ) { + className = (" " + elem.className + " ").replace( rclass, " " ); + for ( c = 0, cl = classNames.length; c < cl; c++ ) { + className = className.replace(" " + classNames[ c ] + " ", " "); + } + elem.className = jQuery.trim( className ); + + } else { + elem.className = ""; + } + } + } + } + + return this; + }, + + toggleClass: function( value, stateVal ) { + var type = typeof value, + isBool = typeof stateVal === "boolean"; + + if ( jQuery.isFunction( value ) ) { + return this.each(function( i ) { + jQuery( this ).toggleClass( value.call(this, i, this.className, stateVal), stateVal ); + }); + } + + return this.each(function() { + if ( type === "string" ) { + // toggle individual class names + var className, + i = 0, + self = jQuery( this ), + state = stateVal, + classNames = value.split( rspace ); + + while ( (className = classNames[ i++ ]) ) { + // check each className given, space seperated list + state = isBool ? state : !self.hasClass( className ); + self[ state ? "addClass" : "removeClass" ]( className ); + } + + } else if ( type === "undefined" || type === "boolean" ) { + if ( this.className ) { + // store className if set + jQuery._data( this, "__className__", this.className ); + } + + // toggle whole className + this.className = this.className || value === false ? "" : jQuery._data( this, "__className__" ) || ""; + } + }); + }, + + hasClass: function( selector ) { + var className = " " + selector + " ", + i = 0, + l = this.length; + for ( ; i < l; i++ ) { + if ( this[i].nodeType === 1 && (" " + this[i].className + " ").replace(rclass, " ").indexOf( className ) > -1 ) { + return true; + } + } + + return false; + }, + + val: function( value ) { + var hooks, ret, isFunction, + elem = this[0]; + + if ( !arguments.length ) { + if ( elem ) { + hooks = jQuery.valHooks[ elem.type ] || jQuery.valHooks[ elem.nodeName.toLowerCase() ]; + + if ( hooks && "get" in hooks && (ret = hooks.get( elem, "value" )) !== undefined ) { + return ret; + } + + ret = elem.value; + + return typeof ret === "string" ? + // handle most common string cases + ret.replace(rreturn, "") : + // handle cases where value is null/undef or number + ret == null ? "" : ret; + } + + return; + } + + isFunction = jQuery.isFunction( value ); + + return this.each(function( i ) { + var self = jQuery(this), val; + + if ( this.nodeType !== 1 ) { + return; + } + + if ( isFunction ) { + val = value.call( this, i, self.val() ); + } else { + val = value; + } + + // Treat null/undefined as ""; convert numbers to string + if ( val == null ) { + val = ""; + } else if ( typeof val === "number" ) { + val += ""; + } else if ( jQuery.isArray( val ) ) { + val = jQuery.map(val, function ( value ) { + return value == null ? "" : value + ""; + }); + } + + hooks = jQuery.valHooks[ this.type ] || jQuery.valHooks[ this.nodeName.toLowerCase() ]; + + // If set returns undefined, fall back to normal setting + if ( !hooks || !("set" in hooks) || hooks.set( this, val, "value" ) === undefined ) { + this.value = val; + } + }); + } +}); + +jQuery.extend({ + valHooks: { + option: { + get: function( elem ) { + // attributes.value is undefined in Blackberry 4.7 but + // uses .value. See #6932 + var val = elem.attributes.value; + return !val || val.specified ? elem.value : elem.text; + } + }, + select: { + get: function( elem ) { + var value, i, max, option, + index = elem.selectedIndex, + values = [], + options = elem.options, + one = elem.type === "select-one"; + + // Nothing was selected + if ( index < 0 ) { + return null; + } + + // Loop through all the selected options + i = one ? index : 0; + max = one ? index + 1 : options.length; + for ( ; i < max; i++ ) { + option = options[ i ]; + + // Don't return options that are disabled or in a disabled optgroup + if ( option.selected && (jQuery.support.optDisabled ? !option.disabled : option.getAttribute("disabled") === null) && + (!option.parentNode.disabled || !jQuery.nodeName( option.parentNode, "optgroup" )) ) { + + // Get the specific value for the option + value = jQuery( option ).val(); + + // We don't need an array for one selects + if ( one ) { + return value; + } + + // Multi-Selects return an array + values.push( value ); + } + } + + // Fixes Bug #2551 -- select.val() broken in IE after form.reset() + if ( one && !values.length && options.length ) { + return jQuery( options[ index ] ).val(); + } + + return values; + }, + + set: function( elem, value ) { + var values = jQuery.makeArray( value ); + + jQuery(elem).find("option").each(function() { + this.selected = jQuery.inArray( jQuery(this).val(), values ) >= 0; + }); + + if ( !values.length ) { + elem.selectedIndex = -1; + } + return values; + } + } + }, + + attrFn: { + val: true, + css: true, + html: true, + text: true, + data: true, + width: true, + height: true, + offset: true + }, + + attr: function( elem, name, value, pass ) { + var ret, hooks, notxml, + nType = elem.nodeType; + + // don't get/set attributes on text, comment and attribute nodes + if ( !elem || nType === 3 || nType === 8 || nType === 2 ) { + return; + } + + if ( pass && name in jQuery.attrFn ) { + return jQuery( elem )[ name ]( value ); + } + + // Fallback to prop when attributes are not supported + if ( typeof elem.getAttribute === "undefined" ) { + return jQuery.prop( elem, name, value ); + } + + notxml = nType !== 1 || !jQuery.isXMLDoc( elem ); + + // All attributes are lowercase + // Grab necessary hook if one is defined + if ( notxml ) { + name = name.toLowerCase(); + hooks = jQuery.attrHooks[ name ] || ( rboolean.test( name ) ? boolHook : nodeHook ); + } + + if ( value !== undefined ) { + + if ( value === null ) { + jQuery.removeAttr( elem, name ); + return; + + } else if ( hooks && "set" in hooks && notxml && (ret = hooks.set( elem, value, name )) !== undefined ) { + return ret; + + } else { + elem.setAttribute( name, "" + value ); + return value; + } + + } else if ( hooks && "get" in hooks && notxml && (ret = hooks.get( elem, name )) !== null ) { + return ret; + + } else { + + ret = elem.getAttribute( name ); + + // Non-existent attributes return null, we normalize to undefined + return ret === null ? + undefined : + ret; + } + }, + + removeAttr: function( elem, value ) { + var propName, attrNames, name, l, isBool, + i = 0; + + if ( value && elem.nodeType === 1 ) { + attrNames = value.toLowerCase().split( rspace ); + l = attrNames.length; + + for ( ; i < l; i++ ) { + name = attrNames[ i ]; + + if ( name ) { + propName = jQuery.propFix[ name ] || name; + isBool = rboolean.test( name ); + + // See #9699 for explanation of this approach (setting first, then removal) + // Do not do this for boolean attributes (see #10870) + if ( !isBool ) { + jQuery.attr( elem, name, "" ); + } + elem.removeAttribute( getSetAttribute ? name : propName ); + + // Set corresponding property to false for boolean attributes + if ( isBool && propName in elem ) { + elem[ propName ] = false; + } + } + } + } + }, + + attrHooks: { + type: { + set: function( elem, value ) { + // We can't allow the type property to be changed (since it causes problems in IE) + if ( rtype.test( elem.nodeName ) && elem.parentNode ) { + jQuery.error( "type property can't be changed" ); + } else if ( !jQuery.support.radioValue && value === "radio" && jQuery.nodeName(elem, "input") ) { + // Setting the type on a radio button after the value resets the value in IE6-9 + // Reset value to it's default in case type is set after value + // This is for element creation + var val = elem.value; + elem.setAttribute( "type", value ); + if ( val ) { + elem.value = val; + } + return value; + } + } + }, + // Use the value property for back compat + // Use the nodeHook for button elements in IE6/7 (#1954) + value: { + get: function( elem, name ) { + if ( nodeHook && jQuery.nodeName( elem, "button" ) ) { + return nodeHook.get( elem, name ); + } + return name in elem ? + elem.value : + null; + }, + set: function( elem, value, name ) { + if ( nodeHook && jQuery.nodeName( elem, "button" ) ) { + return nodeHook.set( elem, value, name ); + } + // Does not return so that setAttribute is also used + elem.value = value; + } + } + }, + + propFix: { + tabindex: "tabIndex", + readonly: "readOnly", + "for": "htmlFor", + "class": "className", + maxlength: "maxLength", + cellspacing: "cellSpacing", + cellpadding: "cellPadding", + rowspan: "rowSpan", + colspan: "colSpan", + usemap: "useMap", + frameborder: "frameBorder", + contenteditable: "contentEditable" + }, + + prop: function( elem, name, value ) { + var ret, hooks, notxml, + nType = elem.nodeType; + + // don't get/set properties on text, comment and attribute nodes + if ( !elem || nType === 3 || nType === 8 || nType === 2 ) { + return; + } + + notxml = nType !== 1 || !jQuery.isXMLDoc( elem ); + + if ( notxml ) { + // Fix name and attach hooks + name = jQuery.propFix[ name ] || name; + hooks = jQuery.propHooks[ name ]; + } + + if ( value !== undefined ) { + if ( hooks && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ) { + return ret; + + } else { + return ( elem[ name ] = value ); + } + + } else { + if ( hooks && "get" in hooks && (ret = hooks.get( elem, name )) !== null ) { + return ret; + + } else { + return elem[ name ]; + } + } + }, + + propHooks: { + tabIndex: { + get: function( elem ) { + // elem.tabIndex doesn't always return the correct value when it hasn't been explicitly set + // http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/ + var attributeNode = elem.getAttributeNode("tabindex"); + + return attributeNode && attributeNode.specified ? + parseInt( attributeNode.value, 10 ) : + rfocusable.test( elem.nodeName ) || rclickable.test( elem.nodeName ) && elem.href ? + 0 : + undefined; + } + } + } +}); + +// Add the tabIndex propHook to attrHooks for back-compat (different case is intentional) +jQuery.attrHooks.tabindex = jQuery.propHooks.tabIndex; + +// Hook for boolean attributes +boolHook = { + get: function( elem, name ) { + // Align boolean attributes with corresponding properties + // Fall back to attribute presence where some booleans are not supported + var attrNode, + property = jQuery.prop( elem, name ); + return property === true || typeof property !== "boolean" && ( attrNode = elem.getAttributeNode(name) ) && attrNode.nodeValue !== false ? + name.toLowerCase() : + undefined; + }, + set: function( elem, value, name ) { + var propName; + if ( value === false ) { + // Remove boolean attributes when set to false + jQuery.removeAttr( elem, name ); + } else { + // value is true since we know at this point it's type boolean and not false + // Set boolean attributes to the same name and set the DOM property + propName = jQuery.propFix[ name ] || name; + if ( propName in elem ) { + // Only set the IDL specifically if it already exists on the element + elem[ propName ] = true; + } + + elem.setAttribute( name, name.toLowerCase() ); + } + return name; + } +}; + +// IE6/7 do not support getting/setting some attributes with get/setAttribute +if ( !getSetAttribute ) { + + fixSpecified = { + name: true, + id: true, + coords: true + }; + + // Use this for any attribute in IE6/7 + // This fixes almost every IE6/7 issue + nodeHook = jQuery.valHooks.button = { + get: function( elem, name ) { + var ret; + ret = elem.getAttributeNode( name ); + return ret && ( fixSpecified[ name ] ? ret.nodeValue !== "" : ret.specified ) ? + ret.nodeValue : + undefined; + }, + set: function( elem, value, name ) { + // Set the existing or create a new attribute node + var ret = elem.getAttributeNode( name ); + if ( !ret ) { + ret = document.createAttribute( name ); + elem.setAttributeNode( ret ); + } + return ( ret.nodeValue = value + "" ); + } + }; + + // Apply the nodeHook to tabindex + jQuery.attrHooks.tabindex.set = nodeHook.set; + + // Set width and height to auto instead of 0 on empty string( Bug #8150 ) + // This is for removals + jQuery.each([ "width", "height" ], function( i, name ) { + jQuery.attrHooks[ name ] = jQuery.extend( jQuery.attrHooks[ name ], { + set: function( elem, value ) { + if ( value === "" ) { + elem.setAttribute( name, "auto" ); + return value; + } + } + }); + }); + + // Set contenteditable to false on removals(#10429) + // Setting to empty string throws an error as an invalid value + jQuery.attrHooks.contenteditable = { + get: nodeHook.get, + set: function( elem, value, name ) { + if ( value === "" ) { + value = "false"; + } + nodeHook.set( elem, value, name ); + } + }; +} + + +// Some attributes require a special call on IE +if ( !jQuery.support.hrefNormalized ) { + jQuery.each([ "href", "src", "width", "height" ], function( i, name ) { + jQuery.attrHooks[ name ] = jQuery.extend( jQuery.attrHooks[ name ], { + get: function( elem ) { + var ret = elem.getAttribute( name, 2 ); + return ret === null ? undefined : ret; + } + }); + }); +} + +if ( !jQuery.support.style ) { + jQuery.attrHooks.style = { + get: function( elem ) { + // Return undefined in the case of empty string + // Normalize to lowercase since IE uppercases css property names + return elem.style.cssText.toLowerCase() || undefined; + }, + set: function( elem, value ) { + return ( elem.style.cssText = "" + value ); + } + }; +} + +// Safari mis-reports the default selected property of an option +// Accessing the parent's selectedIndex property fixes it +if ( !jQuery.support.optSelected ) { + jQuery.propHooks.selected = jQuery.extend( jQuery.propHooks.selected, { + get: function( elem ) { + var parent = elem.parentNode; + + if ( parent ) { + parent.selectedIndex; + + // Make sure that it also works with optgroups, see #5701 + if ( parent.parentNode ) { + parent.parentNode.selectedIndex; + } + } + return null; + } + }); +} + +// IE6/7 call enctype encoding +if ( !jQuery.support.enctype ) { + jQuery.propFix.enctype = "encoding"; +} + +// Radios and checkboxes getter/setter +if ( !jQuery.support.checkOn ) { + jQuery.each([ "radio", "checkbox" ], function() { + jQuery.valHooks[ this ] = { + get: function( elem ) { + // Handle the case where in Webkit "" is returned instead of "on" if a value isn't specified + return elem.getAttribute("value") === null ? "on" : elem.value; + } + }; + }); +} +jQuery.each([ "radio", "checkbox" ], function() { + jQuery.valHooks[ this ] = jQuery.extend( jQuery.valHooks[ this ], { + set: function( elem, value ) { + if ( jQuery.isArray( value ) ) { + return ( elem.checked = jQuery.inArray( jQuery(elem).val(), value ) >= 0 ); + } + } + }); +}); + + + + +var rformElems = /^(?:textarea|input|select)$/i, + rtypenamespace = /^([^\.]*)?(?:\.(.+))?$/, + rhoverHack = /(?:^|\s)hover(\.\S+)?\b/, + rkeyEvent = /^key/, + rmouseEvent = /^(?:mouse|contextmenu)|click/, + rfocusMorph = /^(?:focusinfocus|focusoutblur)$/, + rquickIs = /^(\w*)(?:#([\w\-]+))?(?:\.([\w\-]+))?$/, + quickParse = function( selector ) { + var quick = rquickIs.exec( selector ); + if ( quick ) { + // 0 1 2 3 + // [ _, tag, id, class ] + quick[1] = ( quick[1] || "" ).toLowerCase(); + quick[3] = quick[3] && new RegExp( "(?:^|\\s)" + quick[3] + "(?:\\s|$)" ); + } + return quick; + }, + quickIs = function( elem, m ) { + var attrs = elem.attributes || {}; + return ( + (!m[1] || elem.nodeName.toLowerCase() === m[1]) && + (!m[2] || (attrs.id || {}).value === m[2]) && + (!m[3] || m[3].test( (attrs[ "class" ] || {}).value )) + ); + }, + hoverHack = function( events ) { + return jQuery.event.special.hover ? events : events.replace( rhoverHack, "mouseenter$1 mouseleave$1" ); + }; + +/* + * Helper functions for managing events -- not part of the public interface. + * Props to Dean Edwards' addEvent library for many of the ideas. + */ +jQuery.event = { + + add: function( elem, types, handler, data, selector ) { + + var elemData, eventHandle, events, + t, tns, type, namespaces, handleObj, + handleObjIn, quick, handlers, special; + + // Don't attach events to noData or text/comment nodes (allow plain objects tho) + if ( elem.nodeType === 3 || elem.nodeType === 8 || !types || !handler || !(elemData = jQuery._data( elem )) ) { + return; + } + + // Caller can pass in an object of custom data in lieu of the handler + if ( handler.handler ) { + handleObjIn = handler; + handler = handleObjIn.handler; + selector = handleObjIn.selector; + } + + // Make sure that the handler has a unique ID, used to find/remove it later + if ( !handler.guid ) { + handler.guid = jQuery.guid++; + } + + // Init the element's event structure and main handler, if this is the first + events = elemData.events; + if ( !events ) { + elemData.events = events = {}; + } + eventHandle = elemData.handle; + if ( !eventHandle ) { + elemData.handle = eventHandle = function( e ) { + // Discard the second event of a jQuery.event.trigger() and + // when an event is called after a page has unloaded + return typeof jQuery !== "undefined" && (!e || jQuery.event.triggered !== e.type) ? + jQuery.event.dispatch.apply( eventHandle.elem, arguments ) : + undefined; + }; + // Add elem as a property of the handle fn to prevent a memory leak with IE non-native events + eventHandle.elem = elem; + } + + // Handle multiple events separated by a space + // jQuery(...).bind("mouseover mouseout", fn); + types = jQuery.trim( hoverHack(types) ).split( " " ); + for ( t = 0; t < types.length; t++ ) { + + tns = rtypenamespace.exec( types[t] ) || []; + type = tns[1]; + namespaces = ( tns[2] || "" ).split( "." ).sort(); + + // If event changes its type, use the special event handlers for the changed type + special = jQuery.event.special[ type ] || {}; + + // If selector defined, determine special event api type, otherwise given type + type = ( selector ? special.delegateType : special.bindType ) || type; + + // Update special based on newly reset type + special = jQuery.event.special[ type ] || {}; + + // handleObj is passed to all event handlers + handleObj = jQuery.extend({ + type: type, + origType: tns[1], + data: data, + handler: handler, + guid: handler.guid, + selector: selector, + quick: selector && quickParse( selector ), + namespace: namespaces.join(".") + }, handleObjIn ); + + // Init the event handler queue if we're the first + handlers = events[ type ]; + if ( !handlers ) { + handlers = events[ type ] = []; + handlers.delegateCount = 0; + + // Only use addEventListener/attachEvent if the special events handler returns false + if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) { + // Bind the global event handler to the element + if ( elem.addEventListener ) { + elem.addEventListener( type, eventHandle, false ); + + } else if ( elem.attachEvent ) { + elem.attachEvent( "on" + type, eventHandle ); + } + } + } + + if ( special.add ) { + special.add.call( elem, handleObj ); + + if ( !handleObj.handler.guid ) { + handleObj.handler.guid = handler.guid; + } + } + + // Add to the element's handler list, delegates in front + if ( selector ) { + handlers.splice( handlers.delegateCount++, 0, handleObj ); + } else { + handlers.push( handleObj ); + } + + // Keep track of which events have ever been used, for event optimization + jQuery.event.global[ type ] = true; + } + + // Nullify elem to prevent memory leaks in IE + elem = null; + }, + + global: {}, + + // Detach an event or set of events from an element + remove: function( elem, types, handler, selector, mappedTypes ) { + + var elemData = jQuery.hasData( elem ) && jQuery._data( elem ), + t, tns, type, origType, namespaces, origCount, + j, events, special, handle, eventType, handleObj; + + if ( !elemData || !(events = elemData.events) ) { + return; + } + + // Once for each type.namespace in types; type may be omitted + types = jQuery.trim( hoverHack( types || "" ) ).split(" "); + for ( t = 0; t < types.length; t++ ) { + tns = rtypenamespace.exec( types[t] ) || []; + type = origType = tns[1]; + namespaces = tns[2]; + + // Unbind all events (on this namespace, if provided) for the element + if ( !type ) { + for ( type in events ) { + jQuery.event.remove( elem, type + types[ t ], handler, selector, true ); + } + continue; + } + + special = jQuery.event.special[ type ] || {}; + type = ( selector? special.delegateType : special.bindType ) || type; + eventType = events[ type ] || []; + origCount = eventType.length; + namespaces = namespaces ? new RegExp("(^|\\.)" + namespaces.split(".").sort().join("\\.(?:.*\\.)?") + "(\\.|$)") : null; + + // Remove matching events + for ( j = 0; j < eventType.length; j++ ) { + handleObj = eventType[ j ]; + + if ( ( mappedTypes || origType === handleObj.origType ) && + ( !handler || handler.guid === handleObj.guid ) && + ( !namespaces || namespaces.test( handleObj.namespace ) ) && + ( !selector || selector === handleObj.selector || selector === "**" && handleObj.selector ) ) { + eventType.splice( j--, 1 ); + + if ( handleObj.selector ) { + eventType.delegateCount--; + } + if ( special.remove ) { + special.remove.call( elem, handleObj ); + } + } + } + + // Remove generic event handler if we removed something and no more handlers exist + // (avoids potential for endless recursion during removal of special event handlers) + if ( eventType.length === 0 && origCount !== eventType.length ) { + if ( !special.teardown || special.teardown.call( elem, namespaces ) === false ) { + jQuery.removeEvent( elem, type, elemData.handle ); + } + + delete events[ type ]; + } + } + + // Remove the expando if it's no longer used + if ( jQuery.isEmptyObject( events ) ) { + handle = elemData.handle; + if ( handle ) { + handle.elem = null; + } + + // removeData also checks for emptiness and clears the expando if empty + // so use it instead of delete + jQuery.removeData( elem, [ "events", "handle" ], true ); + } + }, + + // Events that are safe to short-circuit if no handlers are attached. + // Native DOM events should not be added, they may have inline handlers. + customEvent: { + "getData": true, + "setData": true, + "changeData": true + }, + + trigger: function( event, data, elem, onlyHandlers ) { + // Don't do events on text and comment nodes + if ( elem && (elem.nodeType === 3 || elem.nodeType === 8) ) { + return; + } + + // Event object or event type + var type = event.type || event, + namespaces = [], + cache, exclusive, i, cur, old, ontype, special, handle, eventPath, bubbleType; + + // focus/blur morphs to focusin/out; ensure we're not firing them right now + if ( rfocusMorph.test( type + jQuery.event.triggered ) ) { + return; + } + + if ( type.indexOf( "!" ) >= 0 ) { + // Exclusive events trigger only for the exact event (no namespaces) + type = type.slice(0, -1); + exclusive = true; + } + + if ( type.indexOf( "." ) >= 0 ) { + // Namespaced trigger; create a regexp to match event type in handle() + namespaces = type.split("."); + type = namespaces.shift(); + namespaces.sort(); + } + + if ( (!elem || jQuery.event.customEvent[ type ]) && !jQuery.event.global[ type ] ) { + // No jQuery handlers for this event type, and it can't have inline handlers + return; + } + + // Caller can pass in an Event, Object, or just an event type string + event = typeof event === "object" ? + // jQuery.Event object + event[ jQuery.expando ] ? event : + // Object literal + new jQuery.Event( type, event ) : + // Just the event type (string) + new jQuery.Event( type ); + + event.type = type; + event.isTrigger = true; + event.exclusive = exclusive; + event.namespace = namespaces.join( "." ); + event.namespace_re = event.namespace? new RegExp("(^|\\.)" + namespaces.join("\\.(?:.*\\.)?") + "(\\.|$)") : null; + ontype = type.indexOf( ":" ) < 0 ? "on" + type : ""; + + // Handle a global trigger + if ( !elem ) { + + // TODO: Stop taunting the data cache; remove global events and always attach to document + cache = jQuery.cache; + for ( i in cache ) { + if ( cache[ i ].events && cache[ i ].events[ type ] ) { + jQuery.event.trigger( event, data, cache[ i ].handle.elem, true ); + } + } + return; + } + + // Clean up the event in case it is being reused + event.result = undefined; + if ( !event.target ) { + event.target = elem; + } + + // Clone any incoming data and prepend the event, creating the handler arg list + data = data != null ? jQuery.makeArray( data ) : []; + data.unshift( event ); + + // Allow special events to draw outside the lines + special = jQuery.event.special[ type ] || {}; + if ( special.trigger && special.trigger.apply( elem, data ) === false ) { + return; + } + + // Determine event propagation path in advance, per W3C events spec (#9951) + // Bubble up to document, then to window; watch for a global ownerDocument var (#9724) + eventPath = [[ elem, special.bindType || type ]]; + if ( !onlyHandlers && !special.noBubble && !jQuery.isWindow( elem ) ) { + + bubbleType = special.delegateType || type; + cur = rfocusMorph.test( bubbleType + type ) ? elem : elem.parentNode; + old = null; + for ( ; cur; cur = cur.parentNode ) { + eventPath.push([ cur, bubbleType ]); + old = cur; + } + + // Only add window if we got to document (e.g., not plain obj or detached DOM) + if ( old && old === elem.ownerDocument ) { + eventPath.push([ old.defaultView || old.parentWindow || window, bubbleType ]); + } + } + + // Fire handlers on the event path + for ( i = 0; i < eventPath.length && !event.isPropagationStopped(); i++ ) { + + cur = eventPath[i][0]; + event.type = eventPath[i][1]; + + handle = ( jQuery._data( cur, "events" ) || {} )[ event.type ] && jQuery._data( cur, "handle" ); + if ( handle ) { + handle.apply( cur, data ); + } + // Note that this is a bare JS function and not a jQuery handler + handle = ontype && cur[ ontype ]; + if ( handle && jQuery.acceptData( cur ) && handle.apply( cur, data ) === false ) { + event.preventDefault(); + } + } + event.type = type; + + // If nobody prevented the default action, do it now + if ( !onlyHandlers && !event.isDefaultPrevented() ) { + + if ( (!special._default || special._default.apply( elem.ownerDocument, data ) === false) && + !(type === "click" && jQuery.nodeName( elem, "a" )) && jQuery.acceptData( elem ) ) { + + // Call a native DOM method on the target with the same name name as the event. + // Can't use an .isFunction() check here because IE6/7 fails that test. + // Don't do default actions on window, that's where global variables be (#6170) + // IE<9 dies on focus/blur to hidden element (#1486) + if ( ontype && elem[ type ] && ((type !== "focus" && type !== "blur") || event.target.offsetWidth !== 0) && !jQuery.isWindow( elem ) ) { + + // Don't re-trigger an onFOO event when we call its FOO() method + old = elem[ ontype ]; + + if ( old ) { + elem[ ontype ] = null; + } + + // Prevent re-triggering of the same event, since we already bubbled it above + jQuery.event.triggered = type; + elem[ type ](); + jQuery.event.triggered = undefined; + + if ( old ) { + elem[ ontype ] = old; + } + } + } + } + + return event.result; + }, + + dispatch: function( event ) { + + // Make a writable jQuery.Event from the native event object + event = jQuery.event.fix( event || window.event ); + + var handlers = ( (jQuery._data( this, "events" ) || {} )[ event.type ] || []), + delegateCount = handlers.delegateCount, + args = [].slice.call( arguments, 0 ), + run_all = !event.exclusive && !event.namespace, + special = jQuery.event.special[ event.type ] || {}, + handlerQueue = [], + i, j, cur, jqcur, ret, selMatch, matched, matches, handleObj, sel, related; + + // Use the fix-ed jQuery.Event rather than the (read-only) native event + args[0] = event; + event.delegateTarget = this; + + // Call the preDispatch hook for the mapped type, and let it bail if desired + if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) { + return; + } + + // Determine handlers that should run if there are delegated events + // Avoid non-left-click bubbling in Firefox (#3861) + if ( delegateCount && !(event.button && event.type === "click") ) { + + // Pregenerate a single jQuery object for reuse with .is() + jqcur = jQuery(this); + jqcur.context = this.ownerDocument || this; + + for ( cur = event.target; cur != this; cur = cur.parentNode || this ) { + + // Don't process events on disabled elements (#6911, #8165) + if ( cur.disabled !== true ) { + selMatch = {}; + matches = []; + jqcur[0] = cur; + for ( i = 0; i < delegateCount; i++ ) { + handleObj = handlers[ i ]; + sel = handleObj.selector; + + if ( selMatch[ sel ] === undefined ) { + selMatch[ sel ] = ( + handleObj.quick ? quickIs( cur, handleObj.quick ) : jqcur.is( sel ) + ); + } + if ( selMatch[ sel ] ) { + matches.push( handleObj ); + } + } + if ( matches.length ) { + handlerQueue.push({ elem: cur, matches: matches }); + } + } + } + } + + // Add the remaining (directly-bound) handlers + if ( handlers.length > delegateCount ) { + handlerQueue.push({ elem: this, matches: handlers.slice( delegateCount ) }); + } + + // Run delegates first; they may want to stop propagation beneath us + for ( i = 0; i < handlerQueue.length && !event.isPropagationStopped(); i++ ) { + matched = handlerQueue[ i ]; + event.currentTarget = matched.elem; + + for ( j = 0; j < matched.matches.length && !event.isImmediatePropagationStopped(); j++ ) { + handleObj = matched.matches[ j ]; + + // Triggered event must either 1) be non-exclusive and have no namespace, or + // 2) have namespace(s) a subset or equal to those in the bound event (both can have no namespace). + if ( run_all || (!event.namespace && !handleObj.namespace) || event.namespace_re && event.namespace_re.test( handleObj.namespace ) ) { + + event.data = handleObj.data; + event.handleObj = handleObj; + + ret = ( (jQuery.event.special[ handleObj.origType ] || {}).handle || handleObj.handler ) + .apply( matched.elem, args ); + + if ( ret !== undefined ) { + event.result = ret; + if ( ret === false ) { + event.preventDefault(); + event.stopPropagation(); + } + } + } + } + } + + // Call the postDispatch hook for the mapped type + if ( special.postDispatch ) { + special.postDispatch.call( this, event ); + } + + return event.result; + }, + + // Includes some event props shared by KeyEvent and MouseEvent + // *** attrChange attrName relatedNode srcElement are not normalized, non-W3C, deprecated, will be removed in 1.8 *** + props: "attrChange attrName relatedNode srcElement altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "), + + fixHooks: {}, + + keyHooks: { + props: "char charCode key keyCode".split(" "), + filter: function( event, original ) { + + // Add which for key events + if ( event.which == null ) { + event.which = original.charCode != null ? original.charCode : original.keyCode; + } + + return event; + } + }, + + mouseHooks: { + props: "button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "), + filter: function( event, original ) { + var eventDoc, doc, body, + button = original.button, + fromElement = original.fromElement; + + // Calculate pageX/Y if missing and clientX/Y available + if ( event.pageX == null && original.clientX != null ) { + eventDoc = event.target.ownerDocument || document; + doc = eventDoc.documentElement; + body = eventDoc.body; + + event.pageX = original.clientX + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - ( doc && doc.clientLeft || body && body.clientLeft || 0 ); + event.pageY = original.clientY + ( doc && doc.scrollTop || body && body.scrollTop || 0 ) - ( doc && doc.clientTop || body && body.clientTop || 0 ); + } + + // Add relatedTarget, if necessary + if ( !event.relatedTarget && fromElement ) { + event.relatedTarget = fromElement === event.target ? original.toElement : fromElement; + } + + // Add which for click: 1 === left; 2 === middle; 3 === right + // Note: button is not normalized, so don't use it + if ( !event.which && button !== undefined ) { + event.which = ( button & 1 ? 1 : ( button & 2 ? 3 : ( button & 4 ? 2 : 0 ) ) ); + } + + return event; + } + }, + + fix: function( event ) { + if ( event[ jQuery.expando ] ) { + return event; + } + + // Create a writable copy of the event object and normalize some properties + var i, prop, + originalEvent = event, + fixHook = jQuery.event.fixHooks[ event.type ] || {}, + copy = fixHook.props ? this.props.concat( fixHook.props ) : this.props; + + event = jQuery.Event( originalEvent ); + + for ( i = copy.length; i; ) { + prop = copy[ --i ]; + event[ prop ] = originalEvent[ prop ]; + } + + // Fix target property, if necessary (#1925, IE 6/7/8 & Safari2) + if ( !event.target ) { + event.target = originalEvent.srcElement || document; + } + + // Target should not be a text node (#504, Safari) + if ( event.target.nodeType === 3 ) { + event.target = event.target.parentNode; + } + + // For mouse/key events; add metaKey if it's not there (#3368, IE6/7/8) + if ( event.metaKey === undefined ) { + event.metaKey = event.ctrlKey; + } + + return fixHook.filter? fixHook.filter( event, originalEvent ) : event; + }, + + special: { + ready: { + // Make sure the ready event is setup + setup: jQuery.bindReady + }, + + load: { + // Prevent triggered image.load events from bubbling to window.load + noBubble: true + }, + + focus: { + delegateType: "focusin" + }, + blur: { + delegateType: "focusout" + }, + + beforeunload: { + setup: function( data, namespaces, eventHandle ) { + // We only want to do this special case on windows + if ( jQuery.isWindow( this ) ) { + this.onbeforeunload = eventHandle; + } + }, + + teardown: function( namespaces, eventHandle ) { + if ( this.onbeforeunload === eventHandle ) { + this.onbeforeunload = null; + } + } + } + }, + + simulate: function( type, elem, event, bubble ) { + // Piggyback on a donor event to simulate a different one. + // Fake originalEvent to avoid donor's stopPropagation, but if the + // simulated event prevents default then we do the same on the donor. + var e = jQuery.extend( + new jQuery.Event(), + event, + { type: type, + isSimulated: true, + originalEvent: {} + } + ); + if ( bubble ) { + jQuery.event.trigger( e, null, elem ); + } else { + jQuery.event.dispatch.call( elem, e ); + } + if ( e.isDefaultPrevented() ) { + event.preventDefault(); + } + } +}; + +// Some plugins are using, but it's undocumented/deprecated and will be removed. +// The 1.7 special event interface should provide all the hooks needed now. +jQuery.event.handle = jQuery.event.dispatch; + +jQuery.removeEvent = document.removeEventListener ? + function( elem, type, handle ) { + if ( elem.removeEventListener ) { + elem.removeEventListener( type, handle, false ); + } + } : + function( elem, type, handle ) { + if ( elem.detachEvent ) { + elem.detachEvent( "on" + type, handle ); + } + }; + +jQuery.Event = function( src, props ) { + // Allow instantiation without the 'new' keyword + if ( !(this instanceof jQuery.Event) ) { + return new jQuery.Event( src, props ); + } + + // Event object + if ( src && src.type ) { + this.originalEvent = src; + this.type = src.type; + + // Events bubbling up the document may have been marked as prevented + // by a handler lower down the tree; reflect the correct value. + this.isDefaultPrevented = ( src.defaultPrevented || src.returnValue === false || + src.getPreventDefault && src.getPreventDefault() ) ? returnTrue : returnFalse; + + // Event type + } else { + this.type = src; + } + + // Put explicitly provided properties onto the event object + if ( props ) { + jQuery.extend( this, props ); + } + + // Create a timestamp if incoming event doesn't have one + this.timeStamp = src && src.timeStamp || jQuery.now(); + + // Mark it as fixed + this[ jQuery.expando ] = true; +}; + +function returnFalse() { + return false; +} +function returnTrue() { + return true; +} + +// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding +// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html +jQuery.Event.prototype = { + preventDefault: function() { + this.isDefaultPrevented = returnTrue; + + var e = this.originalEvent; + if ( !e ) { + return; + } + + // if preventDefault exists run it on the original event + if ( e.preventDefault ) { + e.preventDefault(); + + // otherwise set the returnValue property of the original event to false (IE) + } else { + e.returnValue = false; + } + }, + stopPropagation: function() { + this.isPropagationStopped = returnTrue; + + var e = this.originalEvent; + if ( !e ) { + return; + } + // if stopPropagation exists run it on the original event + if ( e.stopPropagation ) { + e.stopPropagation(); + } + // otherwise set the cancelBubble property of the original event to true (IE) + e.cancelBubble = true; + }, + stopImmediatePropagation: function() { + this.isImmediatePropagationStopped = returnTrue; + this.stopPropagation(); + }, + isDefaultPrevented: returnFalse, + isPropagationStopped: returnFalse, + isImmediatePropagationStopped: returnFalse +}; + +// Create mouseenter/leave events using mouseover/out and event-time checks +jQuery.each({ + mouseenter: "mouseover", + mouseleave: "mouseout" +}, function( orig, fix ) { + jQuery.event.special[ orig ] = { + delegateType: fix, + bindType: fix, + + handle: function( event ) { + var target = this, + related = event.relatedTarget, + handleObj = event.handleObj, + selector = handleObj.selector, + ret; + + // For mousenter/leave call the handler if related is outside the target. + // NB: No relatedTarget if the mouse left/entered the browser window + if ( !related || (related !== target && !jQuery.contains( target, related )) ) { + event.type = handleObj.origType; + ret = handleObj.handler.apply( this, arguments ); + event.type = fix; + } + return ret; + } + }; +}); + +// IE submit delegation +if ( !jQuery.support.submitBubbles ) { + + jQuery.event.special.submit = { + setup: function() { + // Only need this for delegated form submit events + if ( jQuery.nodeName( this, "form" ) ) { + return false; + } + + // Lazy-add a submit handler when a descendant form may potentially be submitted + jQuery.event.add( this, "click._submit keypress._submit", function( e ) { + // Node name check avoids a VML-related crash in IE (#9807) + var elem = e.target, + form = jQuery.nodeName( elem, "input" ) || jQuery.nodeName( elem, "button" ) ? elem.form : undefined; + if ( form && !form._submit_attached ) { + jQuery.event.add( form, "submit._submit", function( event ) { + event._submit_bubble = true; + }); + form._submit_attached = true; + } + }); + // return undefined since we don't need an event listener + }, + + postDispatch: function( event ) { + // If form was submitted by the user, bubble the event up the tree + if ( event._submit_bubble ) { + delete event._submit_bubble; + if ( this.parentNode && !event.isTrigger ) { + jQuery.event.simulate( "submit", this.parentNode, event, true ); + } + } + }, + + teardown: function() { + // Only need this for delegated form submit events + if ( jQuery.nodeName( this, "form" ) ) { + return false; + } + + // Remove delegated handlers; cleanData eventually reaps submit handlers attached above + jQuery.event.remove( this, "._submit" ); + } + }; +} + +// IE change delegation and checkbox/radio fix +if ( !jQuery.support.changeBubbles ) { + + jQuery.event.special.change = { + + setup: function() { + + if ( rformElems.test( this.nodeName ) ) { + // IE doesn't fire change on a check/radio until blur; trigger it on click + // after a propertychange. Eat the blur-change in special.change.handle. + // This still fires onchange a second time for check/radio after blur. + if ( this.type === "checkbox" || this.type === "radio" ) { + jQuery.event.add( this, "propertychange._change", function( event ) { + if ( event.originalEvent.propertyName === "checked" ) { + this._just_changed = true; + } + }); + jQuery.event.add( this, "click._change", function( event ) { + if ( this._just_changed && !event.isTrigger ) { + this._just_changed = false; + jQuery.event.simulate( "change", this, event, true ); + } + }); + } + return false; + } + // Delegated event; lazy-add a change handler on descendant inputs + jQuery.event.add( this, "beforeactivate._change", function( e ) { + var elem = e.target; + + if ( rformElems.test( elem.nodeName ) && !elem._change_attached ) { + jQuery.event.add( elem, "change._change", function( event ) { + if ( this.parentNode && !event.isSimulated && !event.isTrigger ) { + jQuery.event.simulate( "change", this.parentNode, event, true ); + } + }); + elem._change_attached = true; + } + }); + }, + + handle: function( event ) { + var elem = event.target; + + // Swallow native change events from checkbox/radio, we already triggered them above + if ( this !== elem || event.isSimulated || event.isTrigger || (elem.type !== "radio" && elem.type !== "checkbox") ) { + return event.handleObj.handler.apply( this, arguments ); + } + }, + + teardown: function() { + jQuery.event.remove( this, "._change" ); + + return rformElems.test( this.nodeName ); + } + }; +} + +// Create "bubbling" focus and blur events +if ( !jQuery.support.focusinBubbles ) { + jQuery.each({ focus: "focusin", blur: "focusout" }, function( orig, fix ) { + + // Attach a single capturing handler while someone wants focusin/focusout + var attaches = 0, + handler = function( event ) { + jQuery.event.simulate( fix, event.target, jQuery.event.fix( event ), true ); + }; + + jQuery.event.special[ fix ] = { + setup: function() { + if ( attaches++ === 0 ) { + document.addEventListener( orig, handler, true ); + } + }, + teardown: function() { + if ( --attaches === 0 ) { + document.removeEventListener( orig, handler, true ); + } + } + }; + }); +} + +jQuery.fn.extend({ + + on: function( types, selector, data, fn, /*INTERNAL*/ one ) { + var origFn, type; + + // Types can be a map of types/handlers + if ( typeof types === "object" ) { + // ( types-Object, selector, data ) + if ( typeof selector !== "string" ) { // && selector != null + // ( types-Object, data ) + data = data || selector; + selector = undefined; + } + for ( type in types ) { + this.on( type, selector, data, types[ type ], one ); + } + return this; + } + + if ( data == null && fn == null ) { + // ( types, fn ) + fn = selector; + data = selector = undefined; + } else if ( fn == null ) { + if ( typeof selector === "string" ) { + // ( types, selector, fn ) + fn = data; + data = undefined; + } else { + // ( types, data, fn ) + fn = data; + data = selector; + selector = undefined; + } + } + if ( fn === false ) { + fn = returnFalse; + } else if ( !fn ) { + return this; + } + + if ( one === 1 ) { + origFn = fn; + fn = function( event ) { + // Can use an empty set, since event contains the info + jQuery().off( event ); + return origFn.apply( this, arguments ); + }; + // Use same guid so caller can remove using origFn + fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ ); + } + return this.each( function() { + jQuery.event.add( this, types, fn, data, selector ); + }); + }, + one: function( types, selector, data, fn ) { + return this.on( types, selector, data, fn, 1 ); + }, + off: function( types, selector, fn ) { + if ( types && types.preventDefault && types.handleObj ) { + // ( event ) dispatched jQuery.Event + var handleObj = types.handleObj; + jQuery( types.delegateTarget ).off( + handleObj.namespace ? handleObj.origType + "." + handleObj.namespace : handleObj.origType, + handleObj.selector, + handleObj.handler + ); + return this; + } + if ( typeof types === "object" ) { + // ( types-object [, selector] ) + for ( var type in types ) { + this.off( type, selector, types[ type ] ); + } + return this; + } + if ( selector === false || typeof selector === "function" ) { + // ( types [, fn] ) + fn = selector; + selector = undefined; + } + if ( fn === false ) { + fn = returnFalse; + } + return this.each(function() { + jQuery.event.remove( this, types, fn, selector ); + }); + }, + + bind: function( types, data, fn ) { + return this.on( types, null, data, fn ); + }, + unbind: function( types, fn ) { + return this.off( types, null, fn ); + }, + + live: function( types, data, fn ) { + jQuery( this.context ).on( types, this.selector, data, fn ); + return this; + }, + die: function( types, fn ) { + jQuery( this.context ).off( types, this.selector || "**", fn ); + return this; + }, + + delegate: function( selector, types, data, fn ) { + return this.on( types, selector, data, fn ); + }, + undelegate: function( selector, types, fn ) { + // ( namespace ) or ( selector, types [, fn] ) + return arguments.length == 1? this.off( selector, "**" ) : this.off( types, selector, fn ); + }, + + trigger: function( type, data ) { + return this.each(function() { + jQuery.event.trigger( type, data, this ); + }); + }, + triggerHandler: function( type, data ) { + if ( this[0] ) { + return jQuery.event.trigger( type, data, this[0], true ); + } + }, + + toggle: function( fn ) { + // Save reference to arguments for access in closure + var args = arguments, + guid = fn.guid || jQuery.guid++, + i = 0, + toggler = function( event ) { + // Figure out which function to execute + var lastToggle = ( jQuery._data( this, "lastToggle" + fn.guid ) || 0 ) % i; + jQuery._data( this, "lastToggle" + fn.guid, lastToggle + 1 ); + + // Make sure that clicks stop + event.preventDefault(); + + // and execute the function + return args[ lastToggle ].apply( this, arguments ) || false; + }; + + // link all the functions, so any of them can unbind this click handler + toggler.guid = guid; + while ( i < args.length ) { + args[ i++ ].guid = guid; + } + + return this.click( toggler ); + }, + + hover: function( fnOver, fnOut ) { + return this.mouseenter( fnOver ).mouseleave( fnOut || fnOver ); + } +}); + +jQuery.each( ("blur focus focusin focusout load resize scroll unload click dblclick " + + "mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " + + "change select submit keydown keypress keyup error contextmenu").split(" "), function( i, name ) { + + // Handle event binding + jQuery.fn[ name ] = function( data, fn ) { + if ( fn == null ) { + fn = data; + data = null; + } + + return arguments.length > 0 ? + this.on( name, null, data, fn ) : + this.trigger( name ); + }; + + if ( jQuery.attrFn ) { + jQuery.attrFn[ name ] = true; + } + + if ( rkeyEvent.test( name ) ) { + jQuery.event.fixHooks[ name ] = jQuery.event.keyHooks; + } + + if ( rmouseEvent.test( name ) ) { + jQuery.event.fixHooks[ name ] = jQuery.event.mouseHooks; + } +}); + + + +/*! + * Sizzle CSS Selector Engine + * Copyright 2011, The Dojo Foundation + * Released under the MIT, BSD, and GPL Licenses. + * More information: http://sizzlejs.com/ + */ +(function(){ + +var chunker = /((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g, + expando = "sizcache" + (Math.random() + '').replace('.', ''), + done = 0, + toString = Object.prototype.toString, + hasDuplicate = false, + baseHasDuplicate = true, + rBackslash = /\\/g, + rReturn = /\r\n/g, + rNonWord = /\W/; + +// Here we check if the JavaScript engine is using some sort of +// optimization where it does not always call our comparision +// function. If that is the case, discard the hasDuplicate value. +// Thus far that includes Google Chrome. +[0, 0].sort(function() { + baseHasDuplicate = false; + return 0; +}); + +var Sizzle = function( selector, context, results, seed ) { + results = results || []; + context = context || document; + + var origContext = context; + + if ( context.nodeType !== 1 && context.nodeType !== 9 ) { + return []; + } + + if ( !selector || typeof selector !== "string" ) { + return results; + } + + var m, set, checkSet, extra, ret, cur, pop, i, + prune = true, + contextXML = Sizzle.isXML( context ), + parts = [], + soFar = selector; + + // Reset the position of the chunker regexp (start from head) + do { + chunker.exec( "" ); + m = chunker.exec( soFar ); + + if ( m ) { + soFar = m[3]; + + parts.push( m[1] ); + + if ( m[2] ) { + extra = m[3]; + break; + } + } + } while ( m ); + + if ( parts.length > 1 && origPOS.exec( selector ) ) { + + if ( parts.length === 2 && Expr.relative[ parts[0] ] ) { + set = posProcess( parts[0] + parts[1], context, seed ); + + } else { + set = Expr.relative[ parts[0] ] ? + [ context ] : + Sizzle( parts.shift(), context ); + + while ( parts.length ) { + selector = parts.shift(); + + if ( Expr.relative[ selector ] ) { + selector += parts.shift(); + } + + set = posProcess( selector, set, seed ); + } + } + + } else { + // Take a shortcut and set the context if the root selector is an ID + // (but not if it'll be faster if the inner selector is an ID) + if ( !seed && parts.length > 1 && context.nodeType === 9 && !contextXML && + Expr.match.ID.test(parts[0]) && !Expr.match.ID.test(parts[parts.length - 1]) ) { + + ret = Sizzle.find( parts.shift(), context, contextXML ); + context = ret.expr ? + Sizzle.filter( ret.expr, ret.set )[0] : + ret.set[0]; + } + + if ( context ) { + ret = seed ? + { expr: parts.pop(), set: makeArray(seed) } : + Sizzle.find( parts.pop(), parts.length === 1 && (parts[0] === "~" || parts[0] === "+") && context.parentNode ? context.parentNode : context, contextXML ); + + set = ret.expr ? + Sizzle.filter( ret.expr, ret.set ) : + ret.set; + + if ( parts.length > 0 ) { + checkSet = makeArray( set ); + + } else { + prune = false; + } + + while ( parts.length ) { + cur = parts.pop(); + pop = cur; + + if ( !Expr.relative[ cur ] ) { + cur = ""; + } else { + pop = parts.pop(); + } + + if ( pop == null ) { + pop = context; + } + + Expr.relative[ cur ]( checkSet, pop, contextXML ); + } + + } else { + checkSet = parts = []; + } + } + + if ( !checkSet ) { + checkSet = set; + } + + if ( !checkSet ) { + Sizzle.error( cur || selector ); + } + + if ( toString.call(checkSet) === "[object Array]" ) { + if ( !prune ) { + results.push.apply( results, checkSet ); + + } else if ( context && context.nodeType === 1 ) { + for ( i = 0; checkSet[i] != null; i++ ) { + if ( checkSet[i] && (checkSet[i] === true || checkSet[i].nodeType === 1 && Sizzle.contains(context, checkSet[i])) ) { + results.push( set[i] ); + } + } + + } else { + for ( i = 0; checkSet[i] != null; i++ ) { + if ( checkSet[i] && checkSet[i].nodeType === 1 ) { + results.push( set[i] ); + } + } + } + + } else { + makeArray( checkSet, results ); + } + + if ( extra ) { + Sizzle( extra, origContext, results, seed ); + Sizzle.uniqueSort( results ); + } + + return results; +}; + +Sizzle.uniqueSort = function( results ) { + if ( sortOrder ) { + hasDuplicate = baseHasDuplicate; + results.sort( sortOrder ); + + if ( hasDuplicate ) { + for ( var i = 1; i < results.length; i++ ) { + if ( results[i] === results[ i - 1 ] ) { + results.splice( i--, 1 ); + } + } + } + } + + return results; +}; + +Sizzle.matches = function( expr, set ) { + return Sizzle( expr, null, null, set ); +}; + +Sizzle.matchesSelector = function( node, expr ) { + return Sizzle( expr, null, null, [node] ).length > 0; +}; + +Sizzle.find = function( expr, context, isXML ) { + var set, i, len, match, type, left; + + if ( !expr ) { + return []; + } + + for ( i = 0, len = Expr.order.length; i < len; i++ ) { + type = Expr.order[i]; + + if ( (match = Expr.leftMatch[ type ].exec( expr )) ) { + left = match[1]; + match.splice( 1, 1 ); + + if ( left.substr( left.length - 1 ) !== "\\" ) { + match[1] = (match[1] || "").replace( rBackslash, "" ); + set = Expr.find[ type ]( match, context, isXML ); + + if ( set != null ) { + expr = expr.replace( Expr.match[ type ], "" ); + break; + } + } + } + } + + if ( !set ) { + set = typeof context.getElementsByTagName !== "undefined" ? + context.getElementsByTagName( "*" ) : + []; + } + + return { set: set, expr: expr }; +}; + +Sizzle.filter = function( expr, set, inplace, not ) { + var match, anyFound, + type, found, item, filter, left, + i, pass, + old = expr, + result = [], + curLoop = set, + isXMLFilter = set && set[0] && Sizzle.isXML( set[0] ); + + while ( expr && set.length ) { + for ( type in Expr.filter ) { + if ( (match = Expr.leftMatch[ type ].exec( expr )) != null && match[2] ) { + filter = Expr.filter[ type ]; + left = match[1]; + + anyFound = false; + + match.splice(1,1); + + if ( left.substr( left.length - 1 ) === "\\" ) { + continue; + } + + if ( curLoop === result ) { + result = []; + } + + if ( Expr.preFilter[ type ] ) { + match = Expr.preFilter[ type ]( match, curLoop, inplace, result, not, isXMLFilter ); + + if ( !match ) { + anyFound = found = true; + + } else if ( match === true ) { + continue; + } + } + + if ( match ) { + for ( i = 0; (item = curLoop[i]) != null; i++ ) { + if ( item ) { + found = filter( item, match, i, curLoop ); + pass = not ^ found; + + if ( inplace && found != null ) { + if ( pass ) { + anyFound = true; + + } else { + curLoop[i] = false; + } + + } else if ( pass ) { + result.push( item ); + anyFound = true; + } + } + } + } + + if ( found !== undefined ) { + if ( !inplace ) { + curLoop = result; + } + + expr = expr.replace( Expr.match[ type ], "" ); + + if ( !anyFound ) { + return []; + } + + break; + } + } + } + + // Improper expression + if ( expr === old ) { + if ( anyFound == null ) { + Sizzle.error( expr ); + + } else { + break; + } + } + + old = expr; + } + + return curLoop; +}; + +Sizzle.error = function( msg ) { + throw new Error( "Syntax error, unrecognized expression: " + msg ); +}; + +/** + * Utility function for retreiving the text value of an array of DOM nodes + * @param {Array|Element} elem + */ +var getText = Sizzle.getText = function( elem ) { + var i, node, + nodeType = elem.nodeType, + ret = ""; + + if ( nodeType ) { + if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) { + // Use textContent || innerText for elements + if ( typeof elem.textContent === 'string' ) { + return elem.textContent; + } else if ( typeof elem.innerText === 'string' ) { + // Replace IE's carriage returns + return elem.innerText.replace( rReturn, '' ); + } else { + // Traverse it's children + for ( elem = elem.firstChild; elem; elem = elem.nextSibling) { + ret += getText( elem ); + } + } + } else if ( nodeType === 3 || nodeType === 4 ) { + return elem.nodeValue; + } + } else { + + // If no nodeType, this is expected to be an array + for ( i = 0; (node = elem[i]); i++ ) { + // Do not traverse comment nodes + if ( node.nodeType !== 8 ) { + ret += getText( node ); + } + } + } + return ret; +}; + +var Expr = Sizzle.selectors = { + order: [ "ID", "NAME", "TAG" ], + + match: { + ID: /#((?:[\w\u00c0-\uFFFF\-]|\\.)+)/, + CLASS: /\.((?:[\w\u00c0-\uFFFF\-]|\\.)+)/, + NAME: /\[name=['"]*((?:[\w\u00c0-\uFFFF\-]|\\.)+)['"]*\]/, + ATTR: /\[\s*((?:[\w\u00c0-\uFFFF\-]|\\.)+)\s*(?:(\S?=)\s*(?:(['"])(.*?)\3|(#?(?:[\w\u00c0-\uFFFF\-]|\\.)*)|)|)\s*\]/, + TAG: /^((?:[\w\u00c0-\uFFFF\*\-]|\\.)+)/, + CHILD: /:(only|nth|last|first)-child(?:\(\s*(even|odd|(?:[+\-]?\d+|(?:[+\-]?\d*)?n\s*(?:[+\-]\s*\d+)?))\s*\))?/, + POS: /:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^\-]|$)/, + PSEUDO: /:((?:[\w\u00c0-\uFFFF\-]|\\.)+)(?:\((['"]?)((?:\([^\)]+\)|[^\(\)]*)+)\2\))?/ + }, + + leftMatch: {}, + + attrMap: { + "class": "className", + "for": "htmlFor" + }, + + attrHandle: { + href: function( elem ) { + return elem.getAttribute( "href" ); + }, + type: function( elem ) { + return elem.getAttribute( "type" ); + } + }, + + relative: { + "+": function(checkSet, part){ + var isPartStr = typeof part === "string", + isTag = isPartStr && !rNonWord.test( part ), + isPartStrNotTag = isPartStr && !isTag; + + if ( isTag ) { + part = part.toLowerCase(); + } + + for ( var i = 0, l = checkSet.length, elem; i < l; i++ ) { + if ( (elem = checkSet[i]) ) { + while ( (elem = elem.previousSibling) && elem.nodeType !== 1 ) {} + + checkSet[i] = isPartStrNotTag || elem && elem.nodeName.toLowerCase() === part ? + elem || false : + elem === part; + } + } + + if ( isPartStrNotTag ) { + Sizzle.filter( part, checkSet, true ); + } + }, + + ">": function( checkSet, part ) { + var elem, + isPartStr = typeof part === "string", + i = 0, + l = checkSet.length; + + if ( isPartStr && !rNonWord.test( part ) ) { + part = part.toLowerCase(); + + for ( ; i < l; i++ ) { + elem = checkSet[i]; + + if ( elem ) { + var parent = elem.parentNode; + checkSet[i] = parent.nodeName.toLowerCase() === part ? parent : false; + } + } + + } else { + for ( ; i < l; i++ ) { + elem = checkSet[i]; + + if ( elem ) { + checkSet[i] = isPartStr ? + elem.parentNode : + elem.parentNode === part; + } + } + + if ( isPartStr ) { + Sizzle.filter( part, checkSet, true ); + } + } + }, + + "": function(checkSet, part, isXML){ + var nodeCheck, + doneName = done++, + checkFn = dirCheck; + + if ( typeof part === "string" && !rNonWord.test( part ) ) { + part = part.toLowerCase(); + nodeCheck = part; + checkFn = dirNodeCheck; + } + + checkFn( "parentNode", part, doneName, checkSet, nodeCheck, isXML ); + }, + + "~": function( checkSet, part, isXML ) { + var nodeCheck, + doneName = done++, + checkFn = dirCheck; + + if ( typeof part === "string" && !rNonWord.test( part ) ) { + part = part.toLowerCase(); + nodeCheck = part; + checkFn = dirNodeCheck; + } + + checkFn( "previousSibling", part, doneName, checkSet, nodeCheck, isXML ); + } + }, + + find: { + ID: function( match, context, isXML ) { + if ( typeof context.getElementById !== "undefined" && !isXML ) { + var m = context.getElementById(match[1]); + // Check parentNode to catch when Blackberry 4.6 returns + // nodes that are no longer in the document #6963 + return m && m.parentNode ? [m] : []; + } + }, + + NAME: function( match, context ) { + if ( typeof context.getElementsByName !== "undefined" ) { + var ret = [], + results = context.getElementsByName( match[1] ); + + for ( var i = 0, l = results.length; i < l; i++ ) { + if ( results[i].getAttribute("name") === match[1] ) { + ret.push( results[i] ); + } + } + + return ret.length === 0 ? null : ret; + } + }, + + TAG: function( match, context ) { + if ( typeof context.getElementsByTagName !== "undefined" ) { + return context.getElementsByTagName( match[1] ); + } + } + }, + preFilter: { + CLASS: function( match, curLoop, inplace, result, not, isXML ) { + match = " " + match[1].replace( rBackslash, "" ) + " "; + + if ( isXML ) { + return match; + } + + for ( var i = 0, elem; (elem = curLoop[i]) != null; i++ ) { + if ( elem ) { + if ( not ^ (elem.className && (" " + elem.className + " ").replace(/[\t\n\r]/g, " ").indexOf(match) >= 0) ) { + if ( !inplace ) { + result.push( elem ); + } + + } else if ( inplace ) { + curLoop[i] = false; + } + } + } + + return false; + }, + + ID: function( match ) { + return match[1].replace( rBackslash, "" ); + }, + + TAG: function( match, curLoop ) { + return match[1].replace( rBackslash, "" ).toLowerCase(); + }, + + CHILD: function( match ) { + if ( match[1] === "nth" ) { + if ( !match[2] ) { + Sizzle.error( match[0] ); + } + + match[2] = match[2].replace(/^\+|\s*/g, ''); + + // parse equations like 'even', 'odd', '5', '2n', '3n+2', '4n-1', '-n+6' + var test = /(-?)(\d*)(?:n([+\-]?\d*))?/.exec( + match[2] === "even" && "2n" || match[2] === "odd" && "2n+1" || + !/\D/.test( match[2] ) && "0n+" + match[2] || match[2]); + + // calculate the numbers (first)n+(last) including if they are negative + match[2] = (test[1] + (test[2] || 1)) - 0; + match[3] = test[3] - 0; + } + else if ( match[2] ) { + Sizzle.error( match[0] ); + } + + // TODO: Move to normal caching system + match[0] = done++; + + return match; + }, + + ATTR: function( match, curLoop, inplace, result, not, isXML ) { + var name = match[1] = match[1].replace( rBackslash, "" ); + + if ( !isXML && Expr.attrMap[name] ) { + match[1] = Expr.attrMap[name]; + } + + // Handle if an un-quoted value was used + match[4] = ( match[4] || match[5] || "" ).replace( rBackslash, "" ); + + if ( match[2] === "~=" ) { + match[4] = " " + match[4] + " "; + } + + return match; + }, + + PSEUDO: function( match, curLoop, inplace, result, not ) { + if ( match[1] === "not" ) { + // If we're dealing with a complex expression, or a simple one + if ( ( chunker.exec(match[3]) || "" ).length > 1 || /^\w/.test(match[3]) ) { + match[3] = Sizzle(match[3], null, null, curLoop); + + } else { + var ret = Sizzle.filter(match[3], curLoop, inplace, true ^ not); + + if ( !inplace ) { + result.push.apply( result, ret ); + } + + return false; + } + + } else if ( Expr.match.POS.test( match[0] ) || Expr.match.CHILD.test( match[0] ) ) { + return true; + } + + return match; + }, + + POS: function( match ) { + match.unshift( true ); + + return match; + } + }, + + filters: { + enabled: function( elem ) { + return elem.disabled === false && elem.type !== "hidden"; + }, + + disabled: function( elem ) { + return elem.disabled === true; + }, + + checked: function( elem ) { + return elem.checked === true; + }, + + selected: function( elem ) { + // Accessing this property makes selected-by-default + // options in Safari work properly + if ( elem.parentNode ) { + elem.parentNode.selectedIndex; + } + + return elem.selected === true; + }, + + parent: function( elem ) { + return !!elem.firstChild; + }, + + empty: function( elem ) { + return !elem.firstChild; + }, + + has: function( elem, i, match ) { + return !!Sizzle( match[3], elem ).length; + }, + + header: function( elem ) { + return (/h\d/i).test( elem.nodeName ); + }, + + text: function( elem ) { + var attr = elem.getAttribute( "type" ), type = elem.type; + // IE6 and 7 will map elem.type to 'text' for new HTML5 types (search, etc) + // use getAttribute instead to test this case + return elem.nodeName.toLowerCase() === "input" && "text" === type && ( attr === type || attr === null ); + }, + + radio: function( elem ) { + return elem.nodeName.toLowerCase() === "input" && "radio" === elem.type; + }, + + checkbox: function( elem ) { + return elem.nodeName.toLowerCase() === "input" && "checkbox" === elem.type; + }, + + file: function( elem ) { + return elem.nodeName.toLowerCase() === "input" && "file" === elem.type; + }, + + password: function( elem ) { + return elem.nodeName.toLowerCase() === "input" && "password" === elem.type; + }, + + submit: function( elem ) { + var name = elem.nodeName.toLowerCase(); + return (name === "input" || name === "button") && "submit" === elem.type; + }, + + image: function( elem ) { + return elem.nodeName.toLowerCase() === "input" && "image" === elem.type; + }, + + reset: function( elem ) { + var name = elem.nodeName.toLowerCase(); + return (name === "input" || name === "button") && "reset" === elem.type; + }, + + button: function( elem ) { + var name = elem.nodeName.toLowerCase(); + return name === "input" && "button" === elem.type || name === "button"; + }, + + input: function( elem ) { + return (/input|select|textarea|button/i).test( elem.nodeName ); + }, + + focus: function( elem ) { + return elem === elem.ownerDocument.activeElement; + } + }, + setFilters: { + first: function( elem, i ) { + return i === 0; + }, + + last: function( elem, i, match, array ) { + return i === array.length - 1; + }, + + even: function( elem, i ) { + return i % 2 === 0; + }, + + odd: function( elem, i ) { + return i % 2 === 1; + }, + + lt: function( elem, i, match ) { + return i < match[3] - 0; + }, + + gt: function( elem, i, match ) { + return i > match[3] - 0; + }, + + nth: function( elem, i, match ) { + return match[3] - 0 === i; + }, + + eq: function( elem, i, match ) { + return match[3] - 0 === i; + } + }, + filter: { + PSEUDO: function( elem, match, i, array ) { + var name = match[1], + filter = Expr.filters[ name ]; + + if ( filter ) { + return filter( elem, i, match, array ); + + } else if ( name === "contains" ) { + return (elem.textContent || elem.innerText || getText([ elem ]) || "").indexOf(match[3]) >= 0; + + } else if ( name === "not" ) { + var not = match[3]; + + for ( var j = 0, l = not.length; j < l; j++ ) { + if ( not[j] === elem ) { + return false; + } + } + + return true; + + } else { + Sizzle.error( name ); + } + }, + + CHILD: function( elem, match ) { + var first, last, + doneName, parent, cache, + count, diff, + type = match[1], + node = elem; + + switch ( type ) { + case "only": + case "first": + while ( (node = node.previousSibling) ) { + if ( node.nodeType === 1 ) { + return false; + } + } + + if ( type === "first" ) { + return true; + } + + node = elem; + + /* falls through */ + case "last": + while ( (node = node.nextSibling) ) { + if ( node.nodeType === 1 ) { + return false; + } + } + + return true; + + case "nth": + first = match[2]; + last = match[3]; + + if ( first === 1 && last === 0 ) { + return true; + } + + doneName = match[0]; + parent = elem.parentNode; + + if ( parent && (parent[ expando ] !== doneName || !elem.nodeIndex) ) { + count = 0; + + for ( node = parent.firstChild; node; node = node.nextSibling ) { + if ( node.nodeType === 1 ) { + node.nodeIndex = ++count; + } + } + + parent[ expando ] = doneName; + } + + diff = elem.nodeIndex - last; + + if ( first === 0 ) { + return diff === 0; + + } else { + return ( diff % first === 0 && diff / first >= 0 ); + } + } + }, + + ID: function( elem, match ) { + return elem.nodeType === 1 && elem.getAttribute("id") === match; + }, + + TAG: function( elem, match ) { + return (match === "*" && elem.nodeType === 1) || !!elem.nodeName && elem.nodeName.toLowerCase() === match; + }, + + CLASS: function( elem, match ) { + return (" " + (elem.className || elem.getAttribute("class")) + " ") + .indexOf( match ) > -1; + }, + + ATTR: function( elem, match ) { + var name = match[1], + result = Sizzle.attr ? + Sizzle.attr( elem, name ) : + Expr.attrHandle[ name ] ? + Expr.attrHandle[ name ]( elem ) : + elem[ name ] != null ? + elem[ name ] : + elem.getAttribute( name ), + value = result + "", + type = match[2], + check = match[4]; + + return result == null ? + type === "!=" : + !type && Sizzle.attr ? + result != null : + type === "=" ? + value === check : + type === "*=" ? + value.indexOf(check) >= 0 : + type === "~=" ? + (" " + value + " ").indexOf(check) >= 0 : + !check ? + value && result !== false : + type === "!=" ? + value !== check : + type === "^=" ? + value.indexOf(check) === 0 : + type === "$=" ? + value.substr(value.length - check.length) === check : + type === "|=" ? + value === check || value.substr(0, check.length + 1) === check + "-" : + false; + }, + + POS: function( elem, match, i, array ) { + var name = match[2], + filter = Expr.setFilters[ name ]; + + if ( filter ) { + return filter( elem, i, match, array ); + } + } + } +}; + +var origPOS = Expr.match.POS, + fescape = function(all, num){ + return "\\" + (num - 0 + 1); + }; + +for ( var type in Expr.match ) { + Expr.match[ type ] = new RegExp( Expr.match[ type ].source + (/(?![^\[]*\])(?![^\(]*\))/.source) ); + Expr.leftMatch[ type ] = new RegExp( /(^(?:.|\r|\n)*?)/.source + Expr.match[ type ].source.replace(/\\(\d+)/g, fescape) ); +} +// Expose origPOS +// "global" as in regardless of relation to brackets/parens +Expr.match.globalPOS = origPOS; + +var makeArray = function( array, results ) { + array = Array.prototype.slice.call( array, 0 ); + + if ( results ) { + results.push.apply( results, array ); + return results; + } + + return array; +}; + +// Perform a simple check to determine if the browser is capable of +// converting a NodeList to an array using builtin methods. +// Also verifies that the returned array holds DOM nodes +// (which is not the case in the Blackberry browser) +try { + Array.prototype.slice.call( document.documentElement.childNodes, 0 )[0].nodeType; + +// Provide a fallback method if it does not work +} catch( e ) { + makeArray = function( array, results ) { + var i = 0, + ret = results || []; + + if ( toString.call(array) === "[object Array]" ) { + Array.prototype.push.apply( ret, array ); + + } else { + if ( typeof array.length === "number" ) { + for ( var l = array.length; i < l; i++ ) { + ret.push( array[i] ); + } + + } else { + for ( ; array[i]; i++ ) { + ret.push( array[i] ); + } + } + } + + return ret; + }; +} + +var sortOrder, siblingCheck; + +if ( document.documentElement.compareDocumentPosition ) { + sortOrder = function( a, b ) { + if ( a === b ) { + hasDuplicate = true; + return 0; + } + + if ( !a.compareDocumentPosition || !b.compareDocumentPosition ) { + return a.compareDocumentPosition ? -1 : 1; + } + + return a.compareDocumentPosition(b) & 4 ? -1 : 1; + }; + +} else { + sortOrder = function( a, b ) { + // The nodes are identical, we can exit early + if ( a === b ) { + hasDuplicate = true; + return 0; + + // Fallback to using sourceIndex (in IE) if it's available on both nodes + } else if ( a.sourceIndex && b.sourceIndex ) { + return a.sourceIndex - b.sourceIndex; + } + + var al, bl, + ap = [], + bp = [], + aup = a.parentNode, + bup = b.parentNode, + cur = aup; + + // If the nodes are siblings (or identical) we can do a quick check + if ( aup === bup ) { + return siblingCheck( a, b ); + + // If no parents were found then the nodes are disconnected + } else if ( !aup ) { + return -1; + + } else if ( !bup ) { + return 1; + } + + // Otherwise they're somewhere else in the tree so we need + // to build up a full list of the parentNodes for comparison + while ( cur ) { + ap.unshift( cur ); + cur = cur.parentNode; + } + + cur = bup; + + while ( cur ) { + bp.unshift( cur ); + cur = cur.parentNode; + } + + al = ap.length; + bl = bp.length; + + // Start walking down the tree looking for a discrepancy + for ( var i = 0; i < al && i < bl; i++ ) { + if ( ap[i] !== bp[i] ) { + return siblingCheck( ap[i], bp[i] ); + } + } + + // We ended someplace up the tree so do a sibling check + return i === al ? + siblingCheck( a, bp[i], -1 ) : + siblingCheck( ap[i], b, 1 ); + }; + + siblingCheck = function( a, b, ret ) { + if ( a === b ) { + return ret; + } + + var cur = a.nextSibling; + + while ( cur ) { + if ( cur === b ) { + return -1; + } + + cur = cur.nextSibling; + } + + return 1; + }; +} + +// Check to see if the browser returns elements by name when +// querying by getElementById (and provide a workaround) +(function(){ + // We're going to inject a fake input element with a specified name + var form = document.createElement("div"), + id = "script" + (new Date()).getTime(), + root = document.documentElement; + + form.innerHTML = ""; + + // Inject it into the root element, check its status, and remove it quickly + root.insertBefore( form, root.firstChild ); + + // The workaround has to do additional checks after a getElementById + // Which slows things down for other browsers (hence the branching) + if ( document.getElementById( id ) ) { + Expr.find.ID = function( match, context, isXML ) { + if ( typeof context.getElementById !== "undefined" && !isXML ) { + var m = context.getElementById(match[1]); + + return m ? + m.id === match[1] || typeof m.getAttributeNode !== "undefined" && m.getAttributeNode("id").nodeValue === match[1] ? + [m] : + undefined : + []; + } + }; + + Expr.filter.ID = function( elem, match ) { + var node = typeof elem.getAttributeNode !== "undefined" && elem.getAttributeNode("id"); + + return elem.nodeType === 1 && node && node.nodeValue === match; + }; + } + + root.removeChild( form ); + + // release memory in IE + root = form = null; +})(); + +(function(){ + // Check to see if the browser returns only elements + // when doing getElementsByTagName("*") + + // Create a fake element + var div = document.createElement("div"); + div.appendChild( document.createComment("") ); + + // Make sure no comments are found + if ( div.getElementsByTagName("*").length > 0 ) { + Expr.find.TAG = function( match, context ) { + var results = context.getElementsByTagName( match[1] ); + + // Filter out possible comments + if ( match[1] === "*" ) { + var tmp = []; + + for ( var i = 0; results[i]; i++ ) { + if ( results[i].nodeType === 1 ) { + tmp.push( results[i] ); + } + } + + results = tmp; + } + + return results; + }; + } + + // Check to see if an attribute returns normalized href attributes + div.innerHTML = ""; + + if ( div.firstChild && typeof div.firstChild.getAttribute !== "undefined" && + div.firstChild.getAttribute("href") !== "#" ) { + + Expr.attrHandle.href = function( elem ) { + return elem.getAttribute( "href", 2 ); + }; + } + + // release memory in IE + div = null; +})(); + +if ( document.querySelectorAll ) { + (function(){ + var oldSizzle = Sizzle, + div = document.createElement("div"), + id = "__sizzle__"; + + div.innerHTML = "

        "; + + // Safari can't handle uppercase or unicode characters when + // in quirks mode. + if ( div.querySelectorAll && div.querySelectorAll(".TEST").length === 0 ) { + return; + } + + Sizzle = function( query, context, extra, seed ) { + context = context || document; + + // Only use querySelectorAll on non-XML documents + // (ID selectors don't work in non-HTML documents) + if ( !seed && !Sizzle.isXML(context) ) { + // See if we find a selector to speed up + var match = /^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec( query ); + + if ( match && (context.nodeType === 1 || context.nodeType === 9) ) { + // Speed-up: Sizzle("TAG") + if ( match[1] ) { + return makeArray( context.getElementsByTagName( query ), extra ); + + // Speed-up: Sizzle(".CLASS") + } else if ( match[2] && Expr.find.CLASS && context.getElementsByClassName ) { + return makeArray( context.getElementsByClassName( match[2] ), extra ); + } + } + + if ( context.nodeType === 9 ) { + // Speed-up: Sizzle("body") + // The body element only exists once, optimize finding it + if ( query === "body" && context.body ) { + return makeArray( [ context.body ], extra ); + + // Speed-up: Sizzle("#ID") + } else if ( match && match[3] ) { + var elem = context.getElementById( match[3] ); + + // Check parentNode to catch when Blackberry 4.6 returns + // nodes that are no longer in the document #6963 + if ( elem && elem.parentNode ) { + // Handle the case where IE and Opera return items + // by name instead of ID + if ( elem.id === match[3] ) { + return makeArray( [ elem ], extra ); + } + + } else { + return makeArray( [], extra ); + } + } + + try { + return makeArray( context.querySelectorAll(query), extra ); + } catch(qsaError) {} + + // qSA works strangely on Element-rooted queries + // We can work around this by specifying an extra ID on the root + // and working up from there (Thanks to Andrew Dupont for the technique) + // IE 8 doesn't work on object elements + } else if ( context.nodeType === 1 && context.nodeName.toLowerCase() !== "object" ) { + var oldContext = context, + old = context.getAttribute( "id" ), + nid = old || id, + hasParent = context.parentNode, + relativeHierarchySelector = /^\s*[+~]/.test( query ); + + if ( !old ) { + context.setAttribute( "id", nid ); + } else { + nid = nid.replace( /'/g, "\\$&" ); + } + if ( relativeHierarchySelector && hasParent ) { + context = context.parentNode; + } + + try { + if ( !relativeHierarchySelector || hasParent ) { + return makeArray( context.querySelectorAll( "[id='" + nid + "'] " + query ), extra ); + } + + } catch(pseudoError) { + } finally { + if ( !old ) { + oldContext.removeAttribute( "id" ); + } + } + } + } + + return oldSizzle(query, context, extra, seed); + }; + + for ( var prop in oldSizzle ) { + Sizzle[ prop ] = oldSizzle[ prop ]; + } + + // release memory in IE + div = null; + })(); +} + +(function(){ + var html = document.documentElement, + matches = html.matchesSelector || html.mozMatchesSelector || html.webkitMatchesSelector || html.msMatchesSelector; + + if ( matches ) { + // Check to see if it's possible to do matchesSelector + // on a disconnected node (IE 9 fails this) + var disconnectedMatch = !matches.call( document.createElement( "div" ), "div" ), + pseudoWorks = false; + + try { + // This should fail with an exception + // Gecko does not error, returns false instead + matches.call( document.documentElement, "[test!='']:sizzle" ); + + } catch( pseudoError ) { + pseudoWorks = true; + } + + Sizzle.matchesSelector = function( node, expr ) { + // Make sure that attribute selectors are quoted + expr = expr.replace(/\=\s*([^'"\]]*)\s*\]/g, "='$1']"); + + if ( !Sizzle.isXML( node ) ) { + try { + if ( pseudoWorks || !Expr.match.PSEUDO.test( expr ) && !/!=/.test( expr ) ) { + var ret = matches.call( node, expr ); + + // IE 9's matchesSelector returns false on disconnected nodes + if ( ret || !disconnectedMatch || + // As well, disconnected nodes are said to be in a document + // fragment in IE 9, so check for that + node.document && node.document.nodeType !== 11 ) { + return ret; + } + } + } catch(e) {} + } + + return Sizzle(expr, null, null, [node]).length > 0; + }; + } +})(); + +(function(){ + var div = document.createElement("div"); + + div.innerHTML = "
        "; + + // Opera can't find a second classname (in 9.6) + // Also, make sure that getElementsByClassName actually exists + if ( !div.getElementsByClassName || div.getElementsByClassName("e").length === 0 ) { + return; + } + + // Safari caches class attributes, doesn't catch changes (in 3.2) + div.lastChild.className = "e"; + + if ( div.getElementsByClassName("e").length === 1 ) { + return; + } + + Expr.order.splice(1, 0, "CLASS"); + Expr.find.CLASS = function( match, context, isXML ) { + if ( typeof context.getElementsByClassName !== "undefined" && !isXML ) { + return context.getElementsByClassName(match[1]); + } + }; + + // release memory in IE + div = null; +})(); + +function dirNodeCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) { + for ( var i = 0, l = checkSet.length; i < l; i++ ) { + var elem = checkSet[i]; + + if ( elem ) { + var match = false; + + elem = elem[dir]; + + while ( elem ) { + if ( elem[ expando ] === doneName ) { + match = checkSet[elem.sizset]; + break; + } + + if ( elem.nodeType === 1 && !isXML ){ + elem[ expando ] = doneName; + elem.sizset = i; + } + + if ( elem.nodeName.toLowerCase() === cur ) { + match = elem; + break; + } + + elem = elem[dir]; + } + + checkSet[i] = match; + } + } +} + +function dirCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) { + for ( var i = 0, l = checkSet.length; i < l; i++ ) { + var elem = checkSet[i]; + + if ( elem ) { + var match = false; + + elem = elem[dir]; + + while ( elem ) { + if ( elem[ expando ] === doneName ) { + match = checkSet[elem.sizset]; + break; + } + + if ( elem.nodeType === 1 ) { + if ( !isXML ) { + elem[ expando ] = doneName; + elem.sizset = i; + } + + if ( typeof cur !== "string" ) { + if ( elem === cur ) { + match = true; + break; + } + + } else if ( Sizzle.filter( cur, [elem] ).length > 0 ) { + match = elem; + break; + } + } + + elem = elem[dir]; + } + + checkSet[i] = match; + } + } +} + +if ( document.documentElement.contains ) { + Sizzle.contains = function( a, b ) { + return a !== b && (a.contains ? a.contains(b) : true); + }; + +} else if ( document.documentElement.compareDocumentPosition ) { + Sizzle.contains = function( a, b ) { + return !!(a.compareDocumentPosition(b) & 16); + }; + +} else { + Sizzle.contains = function() { + return false; + }; +} + +Sizzle.isXML = function( elem ) { + // documentElement is verified for cases where it doesn't yet exist + // (such as loading iframes in IE - #4833) + var documentElement = (elem ? elem.ownerDocument || elem : 0).documentElement; + + return documentElement ? documentElement.nodeName !== "HTML" : false; +}; + +var posProcess = function( selector, context, seed ) { + var match, + tmpSet = [], + later = "", + root = context.nodeType ? [context] : context; + + // Position selectors must be done after the filter + // And so must :not(positional) so we move all PSEUDOs to the end + while ( (match = Expr.match.PSEUDO.exec( selector )) ) { + later += match[0]; + selector = selector.replace( Expr.match.PSEUDO, "" ); + } + + selector = Expr.relative[selector] ? selector + "*" : selector; + + for ( var i = 0, l = root.length; i < l; i++ ) { + Sizzle( selector, root[i], tmpSet, seed ); + } + + return Sizzle.filter( later, tmpSet ); +}; + +// EXPOSE +// Override sizzle attribute retrieval +Sizzle.attr = jQuery.attr; +Sizzle.selectors.attrMap = {}; +jQuery.find = Sizzle; +jQuery.expr = Sizzle.selectors; +jQuery.expr[":"] = jQuery.expr.filters; +jQuery.unique = Sizzle.uniqueSort; +jQuery.text = Sizzle.getText; +jQuery.isXMLDoc = Sizzle.isXML; +jQuery.contains = Sizzle.contains; + + +})(); + + +var runtil = /Until$/, + rparentsprev = /^(?:parents|prevUntil|prevAll)/, + // Note: This RegExp should be improved, or likely pulled from Sizzle + rmultiselector = /,/, + isSimple = /^.[^:#\[\.,]*$/, + slice = Array.prototype.slice, + POS = jQuery.expr.match.globalPOS, + // methods guaranteed to produce a unique set when starting from a unique set + guaranteedUnique = { + children: true, + contents: true, + next: true, + prev: true + }; + +jQuery.fn.extend({ + find: function( selector ) { + var self = this, + i, l; + + if ( typeof selector !== "string" ) { + return jQuery( selector ).filter(function() { + for ( i = 0, l = self.length; i < l; i++ ) { + if ( jQuery.contains( self[ i ], this ) ) { + return true; + } + } + }); + } + + var ret = this.pushStack( "", "find", selector ), + length, n, r; + + for ( i = 0, l = this.length; i < l; i++ ) { + length = ret.length; + jQuery.find( selector, this[i], ret ); + + if ( i > 0 ) { + // Make sure that the results are unique + for ( n = length; n < ret.length; n++ ) { + for ( r = 0; r < length; r++ ) { + if ( ret[r] === ret[n] ) { + ret.splice(n--, 1); + break; + } + } + } + } + } + + return ret; + }, + + has: function( target ) { + var targets = jQuery( target ); + return this.filter(function() { + for ( var i = 0, l = targets.length; i < l; i++ ) { + if ( jQuery.contains( this, targets[i] ) ) { + return true; + } + } + }); + }, + + not: function( selector ) { + return this.pushStack( winnow(this, selector, false), "not", selector); + }, + + filter: function( selector ) { + return this.pushStack( winnow(this, selector, true), "filter", selector ); + }, + + is: function( selector ) { + return !!selector && ( + typeof selector === "string" ? + // If this is a positional selector, check membership in the returned set + // so $("p:first").is("p:last") won't return true for a doc with two "p". + POS.test( selector ) ? + jQuery( selector, this.context ).index( this[0] ) >= 0 : + jQuery.filter( selector, this ).length > 0 : + this.filter( selector ).length > 0 ); + }, + + closest: function( selectors, context ) { + var ret = [], i, l, cur = this[0]; + + // Array (deprecated as of jQuery 1.7) + if ( jQuery.isArray( selectors ) ) { + var level = 1; + + while ( cur && cur.ownerDocument && cur !== context ) { + for ( i = 0; i < selectors.length; i++ ) { + + if ( jQuery( cur ).is( selectors[ i ] ) ) { + ret.push({ selector: selectors[ i ], elem: cur, level: level }); + } + } + + cur = cur.parentNode; + level++; + } + + return ret; + } + + // String + var pos = POS.test( selectors ) || typeof selectors !== "string" ? + jQuery( selectors, context || this.context ) : + 0; + + for ( i = 0, l = this.length; i < l; i++ ) { + cur = this[i]; + + while ( cur ) { + if ( pos ? pos.index(cur) > -1 : jQuery.find.matchesSelector(cur, selectors) ) { + ret.push( cur ); + break; + + } else { + cur = cur.parentNode; + if ( !cur || !cur.ownerDocument || cur === context || cur.nodeType === 11 ) { + break; + } + } + } + } + + ret = ret.length > 1 ? jQuery.unique( ret ) : ret; + + return this.pushStack( ret, "closest", selectors ); + }, + + // Determine the position of an element within + // the matched set of elements + index: function( elem ) { + + // No argument, return index in parent + if ( !elem ) { + return ( this[0] && this[0].parentNode ) ? this.prevAll().length : -1; + } + + // index in selector + if ( typeof elem === "string" ) { + return jQuery.inArray( this[0], jQuery( elem ) ); + } + + // Locate the position of the desired element + return jQuery.inArray( + // If it receives a jQuery object, the first element is used + elem.jquery ? elem[0] : elem, this ); + }, + + add: function( selector, context ) { + var set = typeof selector === "string" ? + jQuery( selector, context ) : + jQuery.makeArray( selector && selector.nodeType ? [ selector ] : selector ), + all = jQuery.merge( this.get(), set ); + + return this.pushStack( isDisconnected( set[0] ) || isDisconnected( all[0] ) ? + all : + jQuery.unique( all ) ); + }, + + andSelf: function() { + return this.add( this.prevObject ); + } +}); + +// A painfully simple check to see if an element is disconnected +// from a document (should be improved, where feasible). +function isDisconnected( node ) { + return !node || !node.parentNode || node.parentNode.nodeType === 11; +} + +jQuery.each({ + parent: function( elem ) { + var parent = elem.parentNode; + return parent && parent.nodeType !== 11 ? parent : null; + }, + parents: function( elem ) { + return jQuery.dir( elem, "parentNode" ); + }, + parentsUntil: function( elem, i, until ) { + return jQuery.dir( elem, "parentNode", until ); + }, + next: function( elem ) { + return jQuery.nth( elem, 2, "nextSibling" ); + }, + prev: function( elem ) { + return jQuery.nth( elem, 2, "previousSibling" ); + }, + nextAll: function( elem ) { + return jQuery.dir( elem, "nextSibling" ); + }, + prevAll: function( elem ) { + return jQuery.dir( elem, "previousSibling" ); + }, + nextUntil: function( elem, i, until ) { + return jQuery.dir( elem, "nextSibling", until ); + }, + prevUntil: function( elem, i, until ) { + return jQuery.dir( elem, "previousSibling", until ); + }, + siblings: function( elem ) { + return jQuery.sibling( ( elem.parentNode || {} ).firstChild, elem ); + }, + children: function( elem ) { + return jQuery.sibling( elem.firstChild ); + }, + contents: function( elem ) { + return jQuery.nodeName( elem, "iframe" ) ? + elem.contentDocument || elem.contentWindow.document : + jQuery.makeArray( elem.childNodes ); + } +}, function( name, fn ) { + jQuery.fn[ name ] = function( until, selector ) { + var ret = jQuery.map( this, fn, until ); + + if ( !runtil.test( name ) ) { + selector = until; + } + + if ( selector && typeof selector === "string" ) { + ret = jQuery.filter( selector, ret ); + } + + ret = this.length > 1 && !guaranteedUnique[ name ] ? jQuery.unique( ret ) : ret; + + if ( (this.length > 1 || rmultiselector.test( selector )) && rparentsprev.test( name ) ) { + ret = ret.reverse(); + } + + return this.pushStack( ret, name, slice.call( arguments ).join(",") ); + }; +}); + +jQuery.extend({ + filter: function( expr, elems, not ) { + if ( not ) { + expr = ":not(" + expr + ")"; + } + + return elems.length === 1 ? + jQuery.find.matchesSelector(elems[0], expr) ? [ elems[0] ] : [] : + jQuery.find.matches(expr, elems); + }, + + dir: function( elem, dir, until ) { + var matched = [], + cur = elem[ dir ]; + + while ( cur && cur.nodeType !== 9 && (until === undefined || cur.nodeType !== 1 || !jQuery( cur ).is( until )) ) { + if ( cur.nodeType === 1 ) { + matched.push( cur ); + } + cur = cur[dir]; + } + return matched; + }, + + nth: function( cur, result, dir, elem ) { + result = result || 1; + var num = 0; + + for ( ; cur; cur = cur[dir] ) { + if ( cur.nodeType === 1 && ++num === result ) { + break; + } + } + + return cur; + }, + + sibling: function( n, elem ) { + var r = []; + + for ( ; n; n = n.nextSibling ) { + if ( n.nodeType === 1 && n !== elem ) { + r.push( n ); + } + } + + return r; + } +}); + +// Implement the identical functionality for filter and not +function winnow( elements, qualifier, keep ) { + + // Can't pass null or undefined to indexOf in Firefox 4 + // Set to 0 to skip string check + qualifier = qualifier || 0; + + if ( jQuery.isFunction( qualifier ) ) { + return jQuery.grep(elements, function( elem, i ) { + var retVal = !!qualifier.call( elem, i, elem ); + return retVal === keep; + }); + + } else if ( qualifier.nodeType ) { + return jQuery.grep(elements, function( elem, i ) { + return ( elem === qualifier ) === keep; + }); + + } else if ( typeof qualifier === "string" ) { + var filtered = jQuery.grep(elements, function( elem ) { + return elem.nodeType === 1; + }); + + if ( isSimple.test( qualifier ) ) { + return jQuery.filter(qualifier, filtered, !keep); + } else { + qualifier = jQuery.filter( qualifier, filtered ); + } + } + + return jQuery.grep(elements, function( elem, i ) { + return ( jQuery.inArray( elem, qualifier ) >= 0 ) === keep; + }); +} + + + + +function createSafeFragment( document ) { + var list = nodeNames.split( "|" ), + safeFrag = document.createDocumentFragment(); + + if ( safeFrag.createElement ) { + while ( list.length ) { + safeFrag.createElement( + list.pop() + ); + } + } + return safeFrag; +} + +var nodeNames = "abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|" + + "header|hgroup|mark|meter|nav|output|progress|section|summary|time|video", + rinlinejQuery = / jQuery\d+="(?:\d+|null)"/g, + rleadingWhitespace = /^\s+/, + rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig, + rtagName = /<([\w:]+)/, + rtbody = /]", "i"), + // checked="checked" or checked + rchecked = /checked\s*(?:[^=]|=\s*.checked.)/i, + rscriptType = /\/(java|ecma)script/i, + rcleanScript = /^\s*", "" ], + legend: [ 1, "
        ", "
        " ], + thead: [ 1, "", "
        " ], + tr: [ 2, "", "
        " ], + td: [ 3, "", "
        " ], + col: [ 2, "", "
        " ], + area: [ 1, "", "" ], + _default: [ 0, "", "" ] + }, + safeFragment = createSafeFragment( document ); + +wrapMap.optgroup = wrapMap.option; +wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; +wrapMap.th = wrapMap.td; + +// IE can't serialize and