From 2e4dfaa728d8a93eeb42237f64c6c1b84fe0518a Mon Sep 17 00:00:00 2001 From: Steven Miers Date: Sat, 19 Sep 2015 16:33:39 -0500 Subject: [PATCH] Squashed commit of the following: commit 30965c6351a4db3897617a0b0d9ae8aabd06d930 Author: Seth Call Date: Tue Sep 15 05:23:27 2015 -0500 * allow jamblaster to fetch http commit 5c8fb6b01ecb11dc0417b3158044da5205759420 Author: Seth Call Date: Fri Sep 11 13:43:07 2015 -0500 * don't issue stop video in session end commit 3e27680ea9fc7161cc23b888a792ed1269bc327c Author: Seth Call Date: Fri Sep 11 13:40:34 2015 -0500 * decommision webcam_viewer in session page commit ac1cc0c8289bd6aefea3ffabbdd6cd9557be8872 Author: Seth Call Date: Thu Sep 10 07:24:42 2015 -0500 * VRFS-3541 - don't use HTML to store data sent to server for genre ID bug in profile commit 004991119a99d7826019c426d75ed1312feaba55 Author: Seth Call Date: Wed Sep 9 15:10:51 2015 -0500 * set 'are you our user' cookie to do better job with ad tracking commit 13a950e65ff0352b05aa8f0646295ed3909a20b2 Author: Seth Call Date: Wed Sep 9 07:58:46 2015 -0500 * align disable vide obutton better commit 9722c6cbc632daa40e06c67611a3a388b078cb82 Author: Seth Call Date: Wed Sep 9 07:45:18 2015 -0500 * whitesapce commit 3976707b14a061371544b4bedb1b991894eb4fb6 Author: Seth Call Date: Wed Sep 9 07:13:51 2015 -0500 * check for video enabled better commit b483dd537f087e29202d0deb6262540ac76014a9 Author: Seth Call Date: Wed Sep 9 07:02:12 2015 -0500 * better text for video test commit a4f465b6d19eabeabe940ddf1992e50d668d1f26 Author: Seth Call Date: Tue Sep 8 20:30:47 2015 -0500 * VRFS-3530, VRFS-3531 - allow user to test and disable video commit ba99f88048dc4e47adc220235d54b74b6f1afaee Author: Seth Call Date: Tue Sep 8 10:05:26 2015 -0500 * VRFS-3534 - fix start recording API signature commit 386ed8144c2d70447ae203c5a5e0b6afad25f654 Author: Seth Call Date: Sun Sep 6 19:03:08 2015 -0500 * VRFS-3528 - make sure open jamtrack dialog passes 'show_purchased_only' commit 6d010a561b389514116b24f9f9789a274659a287 Author: Seth Call Date: Fri Sep 4 20:43:15 2015 -0500 * deal with too-few tracks on landing page, and the 3rd CTA bubble clipping off text commit 0076f0205ab0ce5e8592c4dd7e101d72ca379f2c Author: Seth Call Date: Fri Sep 4 15:00:45 2015 -0500 * VRFS-352 - instrument-centric landing page commit 3ee71634b36d69e93cbf4b6aced2aa726f80b949 Author: Seth Call Date: Wed Sep 2 09:40:06 2015 -0500 * remove test stuff commit d07ac009bf8c51126af9d16ff592e7d547b85de9 Author: Seth Call Date: Tue Sep 1 08:11:35 2015 -0500 * VRFS-3509 - case where no device is configured handled commit 9420cebad48a3e9a854f497f2bf5e7b77cbb91f4 Author: Seth Call Date: Sun Aug 30 05:00:00 2015 -0500 * VRFS-3494 - show popup when video window launches for the 1st time to offer guidance commit c3f81a4d236126d4913da173fd9794cce525a2ce Author: Seth Call Date: Thu Aug 27 10:35:43 2015 -0500 * build bump commit e782d5f9bb31d62c555249466be6f34991f84074 Author: Seth Call Date: Thu Aug 27 09:43:40 2015 -0500 * VRFS-3419 - check better for window opener commit 36b6699cde5adf15d6bbcaf9f4b1442d644c52a4 Author: Seth Call Date: Thu Aug 27 08:12:47 2015 -0500 * validate popup VRFS-3419 commit 8948f0498f79675e9dd9568b82e7480a187dceee Author: Seth Call Date: Thu Aug 27 07:59:21 2015 -0500 * fix changed path commit 2bce35d60402bb06a6773c48323d3973143653e0 Author: Seth Call Date: Wed Aug 26 20:38:34 2015 -0500 * fix jamtrack test commit 63ef63c20daae56ded7367fe7bd7c7209371bacc Author: Seth Call Date: Wed Aug 26 20:34:40 2015 -0500 * fix typo again in webcamViewer. need to go to bed commit 8566cc5bc91bbead2ad3d9a812c28fb992c9ef6d Author: Seth Call Date: Wed Aug 26 20:31:34 2015 -0500 * fix typo added in webcamViewer commit 22ea6e89fdc73f2d1b34faf74ffc7a76b8c9fc99 Author: Seth Call Date: Wed Aug 26 20:26:39 2015 -0500 * VRFS-3488 - jamtrack search by artist and song need to pin to the match, not do a sloppy search commit a4bd28e1687984f35488da7b63e5bf3f5e0d881b Author: Seth Call Date: Wed Aug 26 16:43:34 2015 -0500 VRFS-3474 - watch for USB events and refresh video pages commit d2edfd22c501c4fcd73bee85ce32cfe23bcd703f Author: Seth Call Date: Wed Aug 26 12:01:52 2015 -0500 * VRFS-3467 - previews are 20 seconds long indicator on jamtracks commit defdfa8ce9e109961e2563e848e3fb44fce2b146 Author: Seth Call Date: Wed Aug 26 06:04:53 2015 -0500 * VRFS-3473 - fix 'videoShared' state in webcamViewer commit 090cfa17c0e3bab86f50bad4917b4e3701357166 Merge: 7560b34 818596a Author: Seth Call Date: Tue Aug 25 14:53:35 2015 -0500 Merge branch 'develop' of bitbucket.org:jamkazam/jam-cloud into develop commit 7560b340c777ff9d2c6cefc02fbc9d622df58452 Author: Seth Call Date: Tue Aug 25 14:52:05 2015 -0500 * VRFS-3466 - updated frontend to pass in GUIDs commit 1252dbe1786535982b8cd8336a1f7d5dde6dcb8b Author: Seth Call Date: Tue Aug 25 05:28:15 2015 -0500 * use new bridge calls to handle current FPS and resolution VRFS-3428 commit 818596ae36724d861a9f704d4d6c697b982df34d Author: Jonathan Kolyer Date: Tue Aug 25 08:23:52 2015 +0000 VRFS-3451 musician_search verifying instrument and genres inputs commit 6918eaf09573bfe592dd677f89ece86ef29e45f5 Author: Seth Call Date: Mon Aug 24 17:55:06 2015 -0500 more UI tweaks for video settup in account screen VRFS-3428 commit fc69242578f00e99cb83a3432dc0552c3be212c9 Author: Seth Call Date: Mon Aug 24 16:18:31 2015 -0500 * VRFS-3427 - update FTUE to test video, not just audio commit 729974013a242216570536938fb52f74de4387f9 Author: Seth Call Date: Mon Aug 24 16:17:53 2015 -0500 * VRFS-3428 - fix button text commit db1f1d60d5434abad4c112d5bc58e20b05d180f9 Merge: 04825d2 90c8d05 Author: Seth Call Date: Mon Aug 24 15:56:42 2015 -0500 Merge branch 'feature/video_frontend' into develop commit 04825d2659ebbc601069a3a0638aca2ff249ff6c Author: Seth Call Date: Mon Aug 24 15:54:59 2015 -0500 * VRFS-3428 - update how we query backend for frame rates commit 39d0731d7402a05edfe1e891132e118b234b6f1b Author: Seth Call Date: Sat Aug 22 05:44:59 2015 -0500 * VRFS-3456 - remove special chars from search commit 1874720ee87bc4ac0dd4bc48c462469b6ba34fd4 Author: Seth Call Date: Sat Aug 22 05:32:28 2015 -0500 * VRFS-3456 - protect special chars from tsquery commit 29104ff09b0d287b473af65522bd172115b0fd43 Author: Seth Call Date: Fri Aug 21 05:02:48 2015 -0500 * VRFS-3446 - bug fix for no genre specified on join of session; also fix search bar in jamtrack dialog commit 3b6d1febdb7ebd96b7ff3727e3094c6f968a09ea Author: Seth Call Date: Thu Aug 20 15:44:21 2015 -0500 * forget cta image commit 6ac622853c8a9b2c34961e7f922f57450f41c7e4 Author: Seth Call Date: Thu Aug 20 15:02:55 2015 -0500 * VRFS-3449 - a little more tweaking of JamTrack landing page commit d7fcadcd0dd21b24ce1166096a17216fec345fea Author: Seth Call Date: Thu Aug 20 14:49:07 2015 -0500 * VRFS-3450 - fix 'show all tracks' when pagination occurs by not doubleregistering commit e7b50ca4a84de67f0a5519457d93d5729f9c5236 Author: Seth Call Date: Thu Aug 20 14:19:07 2015 -0500 * VRFS-3449 - updates for direct landing pages commit 0d075a9568685aea40fdfe1106cf6332073d2494 Author: Seth Call Date: Thu Aug 20 09:19:17 2015 -0500 * fix spacing issue commit 9c17d9a024936f98b22bee4bfcbf8089c63b9383 Merge: 9873450 0b67ef5 Author: Seth Call Date: Thu Aug 20 09:06:48 2015 -0500 Merge branch 'develop' of bitbucket.org:jamkazam/jam-cloud into develop commit 98734506dfa2c6420fd683429697deb2ccddf572 Author: Seth Call Date: Thu Aug 20 09:06:36 2015 -0500 * VRFS-3448 - fix invisible downloader commit 90c8d05d00a98195617ed47b51f04e8a7584cba2 Author: Seth Call Date: Wed Aug 19 14:17:10 2015 -0500 * wip commit bf4044d92e172869e4e5cbe67e01bfc25b7e877f Author: Seth Call Date: Wed Aug 19 09:24:14 2015 -0500 * VRFS-3422 - don't die if the user has on sale_line_items commit 87c62b4db2a0e6618593ef5d1ec32a0c6b2eb284 Author: Seth Call Date: Wed Aug 19 08:29:22 2015 -0500 * a fix for linux? hfa code commit 3fa58715fcf0aa63017e0be42e7f5b0ac4b9b8ac Author: Seth Call Date: Wed Aug 19 07:36:04 2015 -0500 * fix open jamtrack dialog for people with less than 10 jamtracks commit d045c94f54095bd413add20fde3555d8c32279a1 Author: Seth Call Date: Wed Aug 19 07:17:37 2015 -0500 * more HFA request polish commit dc343f10e3ddf21560b9178f097a60eedd097669 Author: Seth Call Date: Wed Aug 19 07:01:47 2015 -0500 * don't show free jamtrack notice on landing page if redeemed_jamtrack cookie is set commit e6618da456a675ddbe4d1eb2a51bbf21ae86a41c Author: Seth Call Date: Tue Aug 18 21:29:15 2015 -0500 * fix a bug in figuring out if the user should be show GET IT FREE commit 5ba03a2755e7d84b4019c951ea59156500a1ea01 Author: Seth Call Date: Tue Aug 18 20:41:37 2015 -0500 * VRFS-3431 - better response when creating HFA request commit 37d6c3e57c64e5bc7655ff6de317ee628a9499f4 Author: Seth Call Date: Tue Aug 18 15:19:40 2015 -0500 * add csv to dump released JamTracks commit f6101f3621af96255070e3346f377d5e42895a87 Author: Seth Call Date: Tue Aug 18 14:26:41 2015 -0500 VRFS-3422, VRFS-3423, VRFS-3424, VRFS-3429 - JamTrack search/listing commit 0b67ef5f52416080dcb94d5a2a5a1a7a998a3f3f Author: Jonathan Kolyer Date: Sat Aug 15 15:03:00 2015 +0000 fixed test for instruments in musician search --- admin/app/admin/csv.rb | 7 + admin/app/admin/jam_ruby_users.rb | 15 + admin/app/admin/jam_track_hfa.rb | 22 + admin/app/controllers/email_controller.rb | 1 - admin/app/controllers/jam_track_controller.rb | 15 + admin/app/views/admin/users/_form.html.slim | 3 + .../views/jam_track/dump_released.html.erb | 6 + admin/config/initializers/jam_ruby_user.rb | 9 + admin/config/routes.rb | 6 +- admin/spec/factories.rb | 1 + db/manifest | 3 + db/up/harry_fox_agency.sql | 27 + db/up/jam_track_searchability.sql | 28 + db/up/jam_track_slug.sql | 1 + ruby/lib/jam_ruby.rb | 2 + ruby/lib/jam_ruby/jam_track_importer.rb | 44 +- ruby/lib/jam_ruby/lib/s3_manager_mixin.rb | 4 +- ruby/lib/jam_ruby/models/base_search.rb | 34 +- ruby/lib/jam_ruby/models/jam_track.rb | 101 +- .../jam_ruby/models/jam_track_hfa_request.rb | 103 + .../models/jam_track_hfa_request_id.rb | 18 + ruby/lib/jam_ruby/models/jam_track_right.rb | 4 +- .../jam_ruby/models/musician_instrument.rb | 7 +- ruby/lib/jam_ruby/models/sale.rb | 8 + ruby/lib/jam_ruby/models/search.rb | 7 +- ruby/lib/jam_ruby/models/user.rb | 2 + ruby/spec/factories.rb | 1 + .../models/jam_track_hfa_request_spec.rb | 34 + ruby/spec/jam_ruby/models/jam_track_spec.rb | 39 +- .../jam_ruby/models/musician_search_spec.rb | 5 +- web/Gemfile | 1 + web/README.md | 1 + .../images/content/webcam-icon-gray.png | Bin 0 -> 802 bytes web/app/assets/images/content/webcam-icon.png | Bin 0 -> 876 bytes .../assets/images/web/buy-jamtrack-cta.png | Bin 0 -> 18934 bytes web/app/assets/javascripts/accounts.js | 6 +- .../javascripts/accounts_jamtracks.js.coffee | 6 +- .../javascripts/accounts_profile_interests.js | 27 +- .../javascripts/accounts_session_detail.js | 1 - .../javascripts/accounts_video_profile.js | 11 +- web/app/assets/javascripts/backend_alerts.js | 6 + web/app/assets/javascripts/checkout_signin.js | 2 +- .../javascripts/checkout_utils.js.coffee | 2 +- .../assets/javascripts/client_init.js.coffee | 2 + web/app/assets/javascripts/dialog/banner.js | 3 +- .../javascripts/dialog/genreSelectorDialog.js | 9 +- .../dialog/gettingStartedDialog.js | 2 +- .../javascripts/dialog/openJamTrackDialog.js | 97 +- .../javascripts/download_jamtrack.js.coffee | 1 + web/app/assets/javascripts/fakeJamClient.js | 69 +- web/app/assets/javascripts/genreSelector.js | 21 + web/app/assets/javascripts/globals.js | 26 +- .../assets/javascripts/helpBubbleHelper.js | 2 +- .../assets/javascripts/instrumentSelector.js | 33 + web/app/assets/javascripts/jam_rest.js | 15 +- .../javascripts/jam_track_preview.js.coffee | 23 +- .../javascripts/jam_track_screen.js.coffee | 3 +- .../javascripts/jamtrack_landing.js.coffee | 7 +- web/app/assets/javascripts/layout.js | 2 + .../assets/javascripts/networkTestHelper.js | 662 +- web/app/assets/javascripts/paginator.js | 9 +- web/app/assets/javascripts/profile_utils.js | 11 + .../assets/javascripts/react-components.js | 7 +- .../JamTrackAutoComplete.js.jsx.coffee | 111 + .../JamTrackFilterScreen.js.jsx.coffee | 401 + .../JamTrackLandingScreen.js.jsx.coffee | 150 + .../JamTrackPreview.js.jsx.coffee | 191 + .../JamTrackSearchScreen.js.jsx.coffee | 539 + .../MediaControls.js.jsx.coffee | 6 + .../PopupConfigureVideoGear.js.jsx.coffee | 108 + .../PopupHowToUseVideo.js.jsx.coffee | 105 + .../PopupMediaControls.js.jsx.coffee | 11 + .../PopupRecordingStartStop.js.jsx.coffee | 13 +- .../SessionMediaTracks.js.jsx.coffee | 3 +- .../SessionMyTracks.js.jsx.coffee | 3 +- .../actions/JamTrackActions.js.coffee | 2 + .../actions/JamTrackPreviewActions.js.coffee | 8 + .../actions/VideoActions.js.coffee | 18 + .../helpers/MixerHelper.js.coffee | 8 +- ...e => IndividualJamTrackPage.js.jsx.coffee} | 8 +- .../landing/JamTrackCta.js.jsx.coffee | 60 +- .../stores/JamTrackPreviewStore.js.coffee | 40 + .../stores/JamTrackStore.js.coffee | 23 + .../stores/PlatformStore.js.coffee | 21 + .../stores/SessionStore.js.coffee | 32 +- .../stores/VideoStore.js.coffee | 253 + .../stores/WebcamViewer.js.jsx.coffee | 405 + web/app/assets/javascripts/recordingModel.js | 2 +- web/app/assets/javascripts/redeem_complete.js | 4 +- web/app/assets/javascripts/redeem_signup.js | 16 +- web/app/assets/javascripts/searchResults.js | 2 +- web/app/assets/javascripts/session.js | 4 +- web/app/assets/javascripts/session_utils.js | 3 +- web/app/assets/javascripts/utils.js | 7 +- .../javascripts/web/individual_jamtrack.js | 23 + .../web/individual_jamtrack_band_v1.js | 4 +- .../javascripts/web/individual_jamtrack_v1.js | 6 +- .../assets/javascripts/web/tracking.js.coffee | 36 +- .../javascripts/webcam_viewer.js.coffee | 219 +- .../wizard/gear/step_video_gear.js | 41 +- .../stylesheets/client/account.css.scss | 10 +- .../stylesheets/client/accountVideo.css.scss | 54 + web/app/assets/stylesheets/client/client.css | 3 + .../client/jamTrackPreview.css.scss | 18 +- .../stylesheets/client/jamkazam.css.scss | 62 + .../client/jamtrackSearch.css.scss | 298 + .../client/jamtrack_landing.css.scss | 94 +- .../JamTrackFilterScreen.css.scss | 16 + .../JamTrackSearchScreen.css.scss | 81 + .../react-components/ReactSelect.css.scss | 10 + .../client/wizard/gearWizard.css.scss | 112 +- .../dialogs/networkTestDialog.css.scss | 10 + .../dialogs/openJamTrackDialog.css.scss | 17 + .../landings/individual_jamtrack.css.scss | 36 +- .../minimal/configure_video_gear.css.scss | 173 + .../minimal/how_to_use_video.css.scss | 110 + .../stylesheets/minimal/minimal.css.scss | 1 + .../controllers/api_jam_tracks_controller.rb | 22 +- .../api_shopping_carts_controller.rb | 6 +- web/app/controllers/landings_controller.rb | 24 +- web/app/controllers/popups_controller.rb | 8 + web/app/helpers/sessions_helper.rb | 5 + .../views/api_jam_tracks/autocomplete.rabl | 7 + web/app/views/api_jam_tracks/index.rabl | 3 + .../views/api_jam_tracks/show_for_client.rabl | 6 +- web/app/views/api_users/show.rabl | 2 +- .../clients/_account_jamtracks.html.slim | 4 +- .../clients/_account_video_profile.html.erb | 8 - .../clients/_checkout_complete.html.slim | 2 +- .../views/clients/_checkout_order.html.slim | 2 +- web/app/views/clients/_help.html.slim | 30 +- web/app/views/clients/_home.html.slim | 4 +- .../clients/_jam_track_preview.html.slim | 7 +- .../views/clients/_jamtrack_filter.html.slim | 11 + .../views/clients/_jamtrack_landing.html.slim | 33 +- .../views/clients/_jamtrack_search.html.slim | 8 + web/app/views/clients/_network_test.html.haml | 23 +- .../views/clients/_redeem_complete.html.slim | 2 +- .../views/clients/_redeem_signup.html.slim | 2 +- .../views/clients/_scheduledSession.html.erb | 2 +- web/app/views/clients/_session.html.slim | 167 - web/app/views/clients/_session2.html.slim | 53 + .../views/clients/_shopping_cart.html.haml | 2 +- web/app/views/clients/_web_filter.html.haml | 5 +- web/app/views/clients/_webcam.html.slim | 18 +- web/app/views/clients/index.html.erb | 13 +- .../wizard/gear/_gear_wizard.html.haml | 4 +- .../clients/wizard/gear/_video_gear.html.haml | 17 +- .../dialogs/_loginRequiredDialog.html.slim | 2 +- .../dialogs/_openJamTrackDialog.html.slim | 5 +- .../landings/product_jamtracks.html.slim | 2 +- web/app/views/layouts/minimal.html.erb | 1 + web/app/views/layouts/web.html.erb | 2 +- .../views/popups/configure_video.html.slim | 2 + .../views/popups/how_to_use_video.html.slim | 2 + web/app/views/users/_downloads.html.slim | 2 +- web/app/views/users/home.html.slim | 2 +- web/config/application.rb | 5 +- web/config/initializers/gon.rb | 1 + web/config/routes.rb | 6 +- web/config/sitemap.rb | 6 +- web/dev_failures.txt | 35911 ++++++++++++++++ web/lib/tasks/jam_tracks.rake | 12 + .../api_recordings_controller_spec.rb | 1 + web/spec/factories.rb | 1 + web/spec/features/checkout_spec.rb | 22 +- web/spec/features/individual_jamtrack_spec.rb | 24 +- web/spec/features/jam_track_searching_spec.rb | 90 + web/spec/features/jamtrack_landing_spec.rb | 30 +- web/spec/features/jamtrack_shopping_spec.rb | 72 +- web/spec/features/network_test_spec.rb | 3 +- web/spec/spec_helper.rb | 2 +- web/spec/support/utilities.rb | 14 + .../javascripts/jquery.infinitescroll.js | 622 +- .../javascripts/react-infinite-scroll.js | 73 + 175 files changed, 41984 insertions(+), 1046 deletions(-) create mode 100644 admin/app/admin/csv.rb create mode 100644 admin/app/admin/jam_track_hfa.rb create mode 100644 admin/app/controllers/jam_track_controller.rb create mode 100644 admin/app/views/jam_track/dump_released.html.erb create mode 100644 db/up/harry_fox_agency.sql create mode 100644 db/up/jam_track_searchability.sql create mode 100644 db/up/jam_track_slug.sql create mode 100644 ruby/lib/jam_ruby/models/jam_track_hfa_request.rb create mode 100644 ruby/lib/jam_ruby/models/jam_track_hfa_request_id.rb create mode 100644 ruby/spec/jam_ruby/models/jam_track_hfa_request_spec.rb create mode 100644 web/app/assets/images/content/webcam-icon-gray.png create mode 100644 web/app/assets/images/content/webcam-icon.png create mode 100644 web/app/assets/images/web/buy-jamtrack-cta.png create mode 100644 web/app/assets/javascripts/react-components/JamTrackAutoComplete.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/JamTrackFilterScreen.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/JamTrackLandingScreen.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/JamTrackPreview.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/JamTrackSearchScreen.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/PopupConfigureVideoGear.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/PopupHowToUseVideo.js.jsx.coffee create mode 100644 web/app/assets/javascripts/react-components/actions/JamTrackPreviewActions.js.coffee create mode 100644 web/app/assets/javascripts/react-components/actions/VideoActions.js.coffee rename web/app/assets/javascripts/react-components/landing/{InvidualJamTrackPage.js.jsx.coffee => IndividualJamTrackPage.js.jsx.coffee} (84%) create mode 100644 web/app/assets/javascripts/react-components/stores/JamTrackPreviewStore.js.coffee create mode 100644 web/app/assets/javascripts/react-components/stores/PlatformStore.js.coffee create mode 100644 web/app/assets/javascripts/react-components/stores/VideoStore.js.coffee create mode 100644 web/app/assets/javascripts/react-components/stores/WebcamViewer.js.jsx.coffee create mode 100644 web/app/assets/stylesheets/client/accountVideo.css.scss create mode 100644 web/app/assets/stylesheets/client/jamtrackSearch.css.scss create mode 100644 web/app/assets/stylesheets/client/react-components/JamTrackFilterScreen.css.scss create mode 100644 web/app/assets/stylesheets/client/react-components/JamTrackSearchScreen.css.scss create mode 100644 web/app/assets/stylesheets/client/react-components/ReactSelect.css.scss create mode 100644 web/app/assets/stylesheets/minimal/configure_video_gear.css.scss create mode 100644 web/app/assets/stylesheets/minimal/how_to_use_video.css.scss create mode 100644 web/app/views/api_jam_tracks/autocomplete.rabl create mode 100644 web/app/views/clients/_jamtrack_filter.html.slim create mode 100644 web/app/views/clients/_jamtrack_search.html.slim create mode 100644 web/app/views/popups/configure_video.html.slim create mode 100644 web/app/views/popups/how_to_use_video.html.slim create mode 100644 web/dev_failures.txt create mode 100644 web/spec/features/jam_track_searching_spec.rb create mode 100644 web/vendor/assets/javascripts/react-infinite-scroll.js diff --git a/admin/app/admin/csv.rb b/admin/app/admin/csv.rb new file mode 100644 index 000000000..43996c828 --- /dev/null +++ b/admin/app/admin/csv.rb @@ -0,0 +1,7 @@ +ActiveAdmin.register_page "CSVs" do + menu :parent => 'Misc' + + content do + link_to('Released JamTracks', released_jamtracks_csv_path) + end +end diff --git a/admin/app/admin/jam_ruby_users.rb b/admin/app/admin/jam_ruby_users.rb index 61a1d3db7..828e6d7f1 100644 --- a/admin/app/admin/jam_ruby_users.rb +++ b/admin/app/admin/jam_ruby_users.rb @@ -113,6 +113,21 @@ ActiveAdmin.register JamRuby::User, :as => 'Users' do @user.delete_mod(User::MOD_GEAR, User::MOD_GEAR_FRAME_OPTIONS) end + + if params[:jam_ruby_user][:how_to_use_video_no_show].to_i == 1 + @user.mod_merge({User::MOD_NO_SHOW => {User::HOWTO_USE_VIDEO_NOSHOW => true}}) + else + @user.delete_mod(User::MOD_NO_SHOW, User::HOWTO_USE_VIDEO_NOSHOW) + end + + + if params[:jam_ruby_user][:configure_video_no_show].to_i == 1 + @user.mod_merge({User::MOD_NO_SHOW => {User::CONFIGURE_VIDEO_NOSHOW=> true}}) + else + @user.delete_mod(User::MOD_NO_SHOW, User::CONFIGURE_VIDEO_NOSHOW) + end + + @user.save! redirect_to edit_admin_user_path(@user) diff --git a/admin/app/admin/jam_track_hfa.rb b/admin/app/admin/jam_track_hfa.rb new file mode 100644 index 000000000..972856fc1 --- /dev/null +++ b/admin/app/admin/jam_track_hfa.rb @@ -0,0 +1,22 @@ +ActiveAdmin.register_page "Harry Fox Request" do + menu :parent => 'JamTracks' + + page_action :create_request, :method => :post do + + name = params[:jam_ruby_jam_track_hfa_request][:name] + + request = JamTrackHfaRequest.create(name) + redirect_to admin_harry_fox_request_path, :notice => "Request created. Check Amazon S3 in the 'jamkazam' bucket; specifically #{request.request_csv_filename}" + end + + + content do + semantic_form_for JamTrackHfaRequest.new, :url => admin_harry_fox_request_create_request_path, :builder => ActiveAdmin::FormBuilder do |f| + f.inputs "New Harry Fox Licensing Request" do + f.input :name, :hint => "Some sort of name to help us remember what this request was for" + end + f.actions + end + + end +end diff --git a/admin/app/controllers/email_controller.rb b/admin/app/controllers/email_controller.rb index bcfd0e7bb..1e0c55182 100644 --- a/admin/app/controllers/email_controller.rb +++ b/admin/app/controllers/email_controller.rb @@ -16,5 +16,4 @@ class EmailController < ApplicationController @users = User.where(subscribe_email: true) end - end \ No newline at end of file diff --git a/admin/app/controllers/jam_track_controller.rb b/admin/app/controllers/jam_track_controller.rb new file mode 100644 index 000000000..a0e504577 --- /dev/null +++ b/admin/app/controllers/jam_track_controller.rb @@ -0,0 +1,15 @@ +require 'csv' + +class JamTrackController < ApplicationController + + respond_to :html + + def dump_released + headers['Content-Disposition'] = "attachment; filename=\"released-jam-tracks.csv\"" + headers['Content-Type'] ||= 'text/csv' + + @jam_tracks = JamTrack.where(status: 'Production') + render "jam_track/dump_released", :layout => nil + end + +end \ No newline at end of file diff --git a/admin/app/views/admin/users/_form.html.slim b/admin/app/views/admin/users/_form.html.slim index 204d9a4da..f8803324d 100644 --- a/admin/app/views/admin/users/_form.html.slim +++ b/admin/app/views/admin/users/_form.html.slim @@ -9,4 +9,7 @@ = f.input :musician = f.inputs "Gear Mods" do = f.input :show_frame_options, as: :boolean + = f.inputs "Do Not Shows" do + = f.input :how_to_use_video_no_show, as: :boolean + = f.input :configure_video_no_show, as: :boolean = f.actions diff --git a/admin/app/views/jam_track/dump_released.html.erb b/admin/app/views/jam_track/dump_released.html.erb new file mode 100644 index 000000000..e04e8fe04 --- /dev/null +++ b/admin/app/views/jam_track/dump_released.html.erb @@ -0,0 +1,6 @@ +<%- headers = ['Artist Name', 'Song Name', 'Direct Landing', 'Generic Direct Landing', 'Band Landing'] -%> +<%= CSV.generate_line headers %><%- @jam_tracks.each do |jam_track| -%><%= CSV.generate_line([jam_track.original_artist, jam_track.name, + "https://www.jamkazam.com/landing/jamtracks/#{jam_track.slug}", + "https://www.jamkazam.com/landing/jamtracks/#{jam_track.slug}?generic=1", + "https://www.jamkazam.com/landing/jamtracks/band/#{jam_track.slug}" + ]) %><%- end -%> \ No newline at end of file diff --git a/admin/config/initializers/jam_ruby_user.rb b/admin/config/initializers/jam_ruby_user.rb index 6d7dca153..57435d83f 100644 --- a/admin/config/initializers/jam_ruby_user.rb +++ b/admin/config/initializers/jam_ruby_user.rb @@ -27,4 +27,13 @@ def show_frame_options self.get_gear_mod(MOD_GEAR_FRAME_OPTIONS) end + + + def how_to_use_video_no_show + self.get_no_show_mod(HOWTO_USE_VIDEO_NOSHOW) + end + + def configure_video_no_show + self.get_no_show_mod(CONFIGURE_VIDEO_NOSHOW) + end end diff --git a/admin/config/routes.rb b/admin/config/routes.rb index 4a7424abd..2bbf1b3fa 100644 --- a/admin/config/routes.rb +++ b/admin/config/routes.rb @@ -8,9 +8,6 @@ JamAdmin::Application.routes.draw do devise_for :users, :class_name => "JamRuby::User", :path_prefix => '/admin', :path => '', :path_names => {:sign_in => 'login', :sign_out => 'logout'} - - - scope ENV['RAILS_RELATIVE_URL_ROOT'] || '/' do root :to => "admin/dashboard#index" @@ -28,13 +25,12 @@ JamAdmin::Application.routes.draw do ActiveAdmin.routes(self) - - match '/api/artifacts' => 'artifacts#update_artifacts', :via => :post match '/api/mix/:id/enqueue' => 'admin/mixes#mix_again', :via => :post match '/api/checks/latency_tester' => 'checks#check_latency_tester', :via => :get match '/api/users/emailables/:code' => 'email#dump_emailables', :via => :get + match '/api/jam_tracks/released' => 'jam_track#dump_released', :via => :get, as: 'released_jamtracks_csv' mount Resque::Server.new, :at => "/resque" diff --git a/admin/spec/factories.rb b/admin/spec/factories.rb index ad1c0bf69..e0ad87d51 100644 --- a/admin/spec/factories.rb +++ b/admin/spec/factories.rb @@ -225,6 +225,7 @@ FactoryGirl.define do factory :jam_track, :class => JamRuby::JamTrack do sequence(:name) { |n| "jam-track-#{n}" } sequence(:description) { |n| "description-#{n}" } + sequence(:slug) { |n| "slug-#{n}" } time_signature '4/4' status 'Production' recording_type 'Cover' diff --git a/db/manifest b/db/manifest index 5fb93d0a8..3d992ef76 100755 --- a/db/manifest +++ b/db/manifest @@ -303,3 +303,6 @@ jam_track_onboarding_enhancements.sql jam_track_name_drop_unique.sql populate_languages.sql populate_subjects.sql +jam_track_searchability.sql +harry_fox_agency.sql +jam_track_slug.sql diff --git a/db/up/harry_fox_agency.sql b/db/up/harry_fox_agency.sql new file mode 100644 index 000000000..7c8371a17 --- /dev/null +++ b/db/up/harry_fox_agency.sql @@ -0,0 +1,27 @@ +ALTER TABLE jam_tracks ADD COLUMN server_fixation_date DATE DEFAULT NOW(); +ALTER TABLE jam_tracks ADD COLUMN hfa_license_status BOOLEAN DEFAULT FALSE; +ALTER TABLE jam_tracks ADD COLUMN hfa_license_desired BOOLEAN DEFAULT TRUE; +ALTER TABLE jam_tracks ADD COLUMN alternative_license_status BOOLEAN DEFAULT FALSE; +ALTER TABLE jam_tracks ADD COLUMN hfa_license_number INTEGER; +ALTER TABLE jam_tracks ADD COLUMN hfa_song_code VARCHAR; +ALTER TABLE jam_tracks ADD COLUMN album_title VARCHAR; + +CREATE TABLE jam_track_hfa_requests ( + id SERIAL PRIMARY KEY, + name VARCHAR NOT NULL, + request_csv_filename VARCHAR, + response_csv_filename VARCHAR, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + approved_at TIMESTAMP, + received_at TIMESTAMP +); + +CREATE TABLE jam_track_hfa_request_ids ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + jam_track_id VARCHAR(64) NOT NULL REFERENCES jam_tracks(id) ON DELETE SET NULL, + jam_track_hfa_request_id INTEGER REFERENCES jam_track_hfa_requests(id) ON DELETE SET NULL, + request_id INTEGER, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); \ No newline at end of file diff --git a/db/up/jam_track_searchability.sql b/db/up/jam_track_searchability.sql new file mode 100644 index 000000000..652f48ecf --- /dev/null +++ b/db/up/jam_track_searchability.sql @@ -0,0 +1,28 @@ +ALTER TABLE jam_tracks ADD COLUMN search_tsv tsvector; +ALTER TABLE jam_tracks ADD COLUMN artist_tsv tsvector; +ALTER TABLE jam_tracks ADD COLUMN name_tsv tsvector; + +CREATE FUNCTION jam_tracks_update_tsv() RETURNS TRIGGER AS $$ +BEGIN + new.search_tsv = to_tsvector('public.jamenglish', COALESCE(NEW.original_artist, '') || ' ' || COALESCE(NEW.name, '') || ' ' || COALESCE(NEW.additional_info, '')); + new.artist_tsv = to_tsvector('public.jamenglish', COALESCE(NEW.original_artist, '')); + new.name_tsv = to_tsvector('public.jamenglish', COALESCE(NEW.name, '')); + + RETURN NEW; +END +$$ LANGUAGE plpgsql; + +CREATE TRIGGER tsvectorupdate BEFORE INSERT OR UPDATE +ON jam_tracks FOR EACH ROW EXECUTE PROCEDURE +jam_tracks_update_tsv(); + +CREATE INDEX jam_tracks_search_tsv_index ON jam_tracks USING gin(search_tsv); +CREATE INDEX jam_tracks_artist_tsv_index ON jam_tracks USING gin(artist_tsv); +CREATE INDEX jam_tracks_name_tsv_index ON jam_tracks USING gin(name_tsv); + +CREATE INDEX jam_tracks_name_key ON jam_tracks USING btree (name); +CREATE INDEX jam_tracks_original_artist_key ON jam_tracks USING btree (original_artist); +CREATE INDEX jam_tracks_status_key ON jam_tracks USING btree (status); + + +UPDATE jam_tracks SET original_artist=original_artist, name=name, additional_info=additional_info; \ No newline at end of file diff --git a/db/up/jam_track_slug.sql b/db/up/jam_track_slug.sql new file mode 100644 index 000000000..e31ddc162 --- /dev/null +++ b/db/up/jam_track_slug.sql @@ -0,0 +1 @@ +ALTER TABLE jam_tracks ADD COLUMN slug VARCHAR(2000) UNIQUE; \ No newline at end of file diff --git a/ruby/lib/jam_ruby.rb b/ruby/lib/jam_ruby.rb index 574e114d5..139dc579a 100755 --- a/ruby/lib/jam_ruby.rb +++ b/ruby/lib/jam_ruby.rb @@ -203,6 +203,8 @@ require "jam_ruby/models/email_batch_scheduled_sessions" require "jam_ruby/models/email_batch_set" require "jam_ruby/models/jam_track_licensor" require "jam_ruby/models/jam_track" +require "jam_ruby/models/jam_track_hfa_request" +require "jam_ruby/models/jam_track_hfa_request_id" require "jam_ruby/models/jam_track_track" require "jam_ruby/models/jam_track_right" require "jam_ruby/models/jam_track_tap_in" diff --git a/ruby/lib/jam_ruby/jam_track_importer.rb b/ruby/lib/jam_ruby/jam_track_importer.rb index fc34eeac3..403c75330 100644 --- a/ruby/lib/jam_ruby/jam_track_importer.rb +++ b/ruby/lib/jam_ruby/jam_track_importer.rb @@ -182,6 +182,25 @@ module JamRuby finish("success", nil) end + def add_licensor_metadata(vendor, metalocation) + Dir.mktmpdir do |tmp_dir| + @@log.debug("update vendor metadata") + meta_yml = File.join(tmp_dir, 'meta.yml') + if jamkazam_s3_manager.exists?(metalocation) + jamkazam_s3_manager.download(metalocation, meta_yml) + meta = YAML.load_file(meta_yml) + else + meta = {} + end + + meta[:licensor] = vendor + + File.open(meta_yml, 'w') {|f| f.write meta.to_yaml } + + jamkazam_s3_manager.upload(metalocation, meta_yml) + end + end + def is_tency_storage? assert_storage_set @storage_format == 'Tency' @@ -421,10 +440,16 @@ module JamRuby jam_track.sales_region = 'Worldwide' jam_track.recording_type = 'Cover' jam_track.description = "This is a JamTrack audio file for use exclusively with the JamKazam service. This JamTrack is a high quality cover of the #{jam_track.original_artist} song \"#{jam_track.name}\"." + jam_track.hfa_license_status = false + jam_track.alternative_license_status = false + jam_track.hfa_license_desired = true + jam_track.server_fixation_date = Time.now + jam_track.slug = metadata['slug'] || jam_track.generate_slug if is_tency_storage? jam_track.vendor_id = metadata[:id] jam_track.licensor = JamTrackLicensor.find_by_name('Tency Music') + #add_licensor_metadata('Tency Music', metalocation) end else if !options[:resync_audio] @@ -1482,6 +1507,14 @@ module JamRuby end end + + def add_tency_metadata + JamTrackLicensor.find_by_name('Tency Music').jam_tracks.each do |jam_track| + jam_track_importer = JamTrackImporter.new(@storage_format) + jam_track_importer.add_licensor_metadata('Tency Music', jam_track.metalocation) + break + end + end def create_masters iterate_song_storage do |metadata, metalocation| next if metadata.nil? @@ -1711,11 +1744,20 @@ module JamRuby def remove_s3_special_chars(filename) filename.tr('/&@:,$=+?;\^`><{}[]#%~|', '') end + + def generate_slugs + JamTrack.all.each do |jam_track| + jam_track.generate_slug + jam_track.save! + end + end + def onboarding_exceptions JamTrack.all.each do |jam_track| jam_track.onboarding_exceptions end end + def synchronize_all(options) importers = [] @@ -1732,7 +1774,7 @@ module JamRuby end if count > 500 - break + #break end end diff --git a/ruby/lib/jam_ruby/lib/s3_manager_mixin.rb b/ruby/lib/jam_ruby/lib/s3_manager_mixin.rb index c294afe6d..b7759afc8 100644 --- a/ruby/lib/jam_ruby/lib/s3_manager_mixin.rb +++ b/ruby/lib/jam_ruby/lib/s3_manager_mixin.rb @@ -7,7 +7,9 @@ module JamRuby end module ClassMethods - + def s3_manager(options={:bucket => nil, :public => false}) + @s3_manager ||= S3Manager.new(options[:bucket] ? options[:bucket] : (options[:public] ? APP_CONFIG.aws_bucket_public : APP_CONFIG.aws_bucket), APP_CONFIG.aws_access_key_id, APP_CONFIG.aws_secret_access_key) + end end def s3_manager(options={:bucket => nil, :public => false}) diff --git a/ruby/lib/jam_ruby/models/base_search.rb b/ruby/lib/jam_ruby/models/base_search.rb index f08c46a7d..ed2feefb7 100644 --- a/ruby/lib/jam_ruby/models/base_search.rb +++ b/ruby/lib/jam_ruby/models/base_search.rb @@ -102,26 +102,38 @@ module JamRuby def self.search_target_class end - # FIXME: SQL INJECTION def _genres(rel, query_data=json) gids = query_data[KEY_GENRES] unless gids.blank? - gidsql = gids.join("','") - gpsql = "SELECT player_id FROM genre_players WHERE (player_type = '#{self.class.search_target_class.name}' AND genre_id IN ('#{gidsql}'))" - rel = rel.where("#{self.class.search_target_class.table_name}.id IN (#{gpsql})") + allgids = Genre.order(:id).pluck(:id) + gids = gids.select { |gg| allgids.index(gg).present? } + + unless gids.blank? + gidsql = gids.join("','") + gpsql = "SELECT player_id FROM genre_players WHERE (player_type = '#{self.class.search_target_class.name}' AND genre_id IN ('#{gidsql}'))" + rel = rel.where("#{self.class.search_target_class.table_name}.id IN (#{gpsql})") + end end rel end - # FIXME: SQL INJECTION def _instruments(rel, query_data=json) unless (instruments = query_data[KEY_INSTRUMENTS]).blank? - instsql = "SELECT player_id FROM musicians_instruments WHERE ((" - instsql += instruments.collect do |inst| - "instrument_id = '#{inst['instrument_id']}' AND proficiency_level = #{inst['proficiency_level']}" - end.join(") OR (") - instsql += "))" - rel = rel.where("#{self.class.search_target_class.table_name}.id IN (#{instsql})") + instrids = Instrument.order(:id).pluck(:id) + instruments = instruments.select { |ii| instrids.index(ii['instrument_id']).present? } + + unless instruments.blank? + instsql = "SELECT player_id FROM musicians_instruments WHERE ((" + instsql += instruments.collect do |inst| + unless MusicianInstrument::PROFICIENCY_RANGE === (proflvl=inst['proficiency_level'].to_i) + proflvl = MusicianInstrument::LEVEL_INTERMEDIATE + end + "instrument_id = '#{inst['instrument_id']}' AND proficiency_level = #{proflvl}" + end.join(") OR (") + instsql += "))" + + rel = rel.where("#{self.class.search_target_class.table_name}.id IN (#{instsql})") + end end rel end diff --git a/ruby/lib/jam_ruby/models/jam_track.rb b/ruby/lib/jam_ruby/models/jam_track.rb index 244cbc6f1..29995e9ad 100644 --- a/ruby/lib/jam_ruby/models/jam_track.rb +++ b/ruby/lib/jam_ruby/models/jam_track.rb @@ -17,7 +17,8 @@ module JamRuby :original_artist, :songwriter, :publisher, :licensor, :licensor_id, :pro, :genres_jam_tracks_attributes, :sales_region, :price, :reproduction_royalty, :public_performance_royalty, :reproduction_royalty_amount, :licensor_royalty_amount, :pro_royalty_amount, :plan_code, :initial_play_silence, :jam_track_tracks_attributes, - :jam_track_tap_ins_attributes, :genre_ids, :version, :jmep_json, :jmep_text, :pro_ascap, :pro_bmi, :pro_sesac, :duration, as: :admin + :jam_track_tap_ins_attributes, :genre_ids, :version, :jmep_json, :jmep_text, :pro_ascap, :pro_bmi, :pro_sesac, :duration, + :server_fixation_date, :hfa_license_status, :hfa_license_desired, :alternative_license_status, :hfa_license_number, :hfa_song_code, :album_title, as: :admin validates :name, presence: true, length: {maximum: 200} validates :plan_code, presence: true, uniqueness: true, length: {maximum: 50 } @@ -37,7 +38,14 @@ module JamRuby validates :public_performance_royalty, inclusion: {in: [nil, true, false]} validates :reproduction_royalty, inclusion: {in: [nil, true, false]} validates :public_performance_royalty, inclusion: {in: [nil, true, false]} - validates :duration, numericality: {only_integer: true}, :allow_nil => true + validates :duration, numericality: {only_integer: true}, :allow_nil => true + validates :hfa_license_status, inclusion: {in: [true, false]} + validates :hfa_license_desired, inclusion: {in: [true, false]} + validates :alternative_license_status, inclusion: {in: [true, false]} + validates :hfa_license_number, numericality: {only_integer: true}, :allow_nil => true + validates :hfa_song_code, length: {maximum: 200} + validates :album_title, length: {maximum: 200} + validates :slug, uniqueness: true validates_format_of :reproduction_royalty_amount, with: /^\d+\.*\d{0,4}$/, :allow_blank => true validates_format_of :licensor_royalty_amount, with: /^\d+\.*\d{0,4}$/, :allow_blank => true @@ -215,6 +223,28 @@ module JamRuby JamTrack.where("original_artist=?", artist_name).all end + # special case of index + def autocomplete(options, user) + + if options[:match].blank? + return {artists: [], songs: []} + end + + options[:show_purchased_only] = options[:show_purchased_only] + + options[:limit] = options[:limit] || 5 + + options[:artist_search] = options[:match] + artists, pager = artist_index(options, user) + + options.delete(:artist_search) + options[:song_search] = options[:match] + options[:sort_by] = 'jamtrack' + songs, pager = index(options, user) + + {artists: artists, songs:songs} + end + def index(options, user) if options[:page] page = options[:page].to_i @@ -252,10 +282,35 @@ module JamRuby query = query.where("jam_track_rights.user_id = ?", user.id) end + if options[:search] + tsquery = Search.create_tsquery(options[:search]) + if tsquery + query = query.where("(search_tsv @@ to_tsquery('jamenglish', ?))", tsquery) + end + end + + if options[:artist_search] + tsquery = Search.create_tsquery(options[:artist_search]) + if tsquery + query = query.where("(artist_tsv @@ to_tsquery('jamenglish', ?))", tsquery) + end + end + + if options[:song_search] + tsquery = Search.create_tsquery(options[:song_search]) + if tsquery + query = query.where("(name_tsv @@ to_tsquery('jamenglish', ?))", tsquery) + end + end + if options[:artist].present? query = query.where("original_artist=?", options[:artist]) end + if options[:song].present? + query = query.where("name=?", options[:song]) + end + if options[:id].present? query = query.where("jam_tracks.id=?", options[:id]) end @@ -266,10 +321,16 @@ module JamRuby query = query.order('jam_tracks.original_artist') else query = query.group("jam_tracks.id") - query = query.order('jam_tracks.original_artist, jam_tracks.name') + if options[:sort_by] == 'jamtrack' + query = query.order('jam_tracks.name') + else + query = query.order('jam_tracks.original_artist, jam_tracks.name') + end + + end - query = query.where("jam_tracks.status = ?", 'Production') unless user.admin + query = query.where("jam_tracks.status = ?", 'Production') unless user && user.admin unless options[:genre].blank? query = query.joins(:genres) @@ -279,13 +340,14 @@ module JamRuby query = query.where("jam_track_tracks.instrument_id = '#{options[:instrument]}' and jam_track_tracks.track_type != 'Master'") unless options[:instrument].blank? query = query.where("jam_tracks.sales_region = '#{options[:availability]}'") unless options[:availability].blank? + count = query.total_entries - if query.length == 0 - [query, nil] + if count == 0 + [query, nil, count] elsif query.length < limit - [query, nil] + [query, nil, count] else - [query, start + limit] + [query, start + limit, count] end end @@ -327,6 +389,19 @@ module JamRuby query = query.where("jam_tracks.status = ?", 'Production') unless user.admin + if options[:show_purchased_only] + query = query.joins(:jam_track_rights) + query = query.where("jam_track_rights.user_id = ?", user.id) + end + + if options[:artist_search] + tsquery = Search.create_tsquery(options[:artist_search]) + if tsquery + query = query.where("(artist_tsv @@ to_tsquery('jamenglish', ?))", tsquery) + end + end + + unless options[:genre].blank? query = query.joins(:genres) query = query.where('genre_id = ? ', options[:genre]) @@ -368,5 +443,15 @@ module JamRuby plan_code[prefix.length..-1] end + # http://stackoverflow.com/questions/4308377/ruby-post-title-to-slug + def sluggarize(field) + field.downcase.strip.gsub(' ', '-').gsub(/[^\w-]/, '') + end + + def generate_slug + self.slug = sluggarize(original_artist) + '-' + sluggarize(name) + puts "Self.slug #{self.slug}" + end + end end diff --git a/ruby/lib/jam_ruby/models/jam_track_hfa_request.rb b/ruby/lib/jam_ruby/models/jam_track_hfa_request.rb new file mode 100644 index 000000000..ed402dc2c --- /dev/null +++ b/ruby/lib/jam_ruby/models/jam_track_hfa_request.rb @@ -0,0 +1,103 @@ +module JamRuby + class JamTrackHfaRequest < ActiveRecord::Base + include JamRuby::S3ManagerMixin + + + @@log = Logging.logger[JamTrackHfaRequest] + + attr_accessible :name, as: :admin + + validates :name, presence: true, length: {maximum: 200} + + # look through all jam_track requests, and find the highest one that is associated with an approved harry fox requests + def self.find_max() + max = JamTrackHfaRequestId.select('coalesce(max(request_id), 0) as max').joins('INNER JOIN jam_track_hfa_requests ON jam_track_hfa_requests.id = jam_track_hfa_request_id').where('received_at IS NOT NULL').first()['max'] + max.to_i + end + + def self.create(name) + request = nil + + transaction do + + max = find_max() + + start = max + 1 + + request = JamTrackHfaRequest.new + request.name = name + request.save! + request.reload + + requests = [] + JamTrack.where(hfa_license_status: false).where(hfa_license_desired: true).where(alternative_license_status: false).each do |jam_track| + request_id = JamTrackHfaRequestId.new + request_id.jam_track = jam_track + request_id.jam_track_hfa_request = request + request_id.request_id = start + start += 1 + request_id.save + request_id.reload # to get back the request_id attribute + requests << request_id + end + + request_name = "JamKazam-#{request.id}-#{request.created_at.to_date.to_s}.csv" + Dir.mktmpdir do |tmp_dir| + out = File.join(tmp_dir, request_name) + + # Field 1 - HFA Agreement Code - Hardcode to "SSA". + # Field 2 - Manufacturer Number - Hardcode to "M18303". + # Field 3 - Transaction Date - Populate this field with the date that we generate this tab-delimited file, in the format YYYYMMDD - e.g. "20150813". + # Field 4 - License Request Number - This one is slightly more involved. Basically, according to HFA we need to generate a unique numeric ID for each JamTrack license request (as opposed to each unique JamTrack, as we might need to make more than one request per JamTrack if such requests were to fail in some cases). This unique numeric ID per request should start with the number 1, and increment by 1. So I guess this feature will need to remember which of these IDs are used on each run it makes so that it knows where to start on the next run. + # Field 7 - Total Playing Time - Minutes - We already have a JamTrack field for the duration of the JamTrack in seconds. We should keep that field, and keep using it as is. We need to use that field to populate this Field 7 and the next Field 8. So if the duration of the JamTrack in seconds were 90 seconds, then we should set Field 7 to "1" and Field 8 to "30" to signify a length of 1:30. + # Field 8 - Total Playing Time - Seconds - See note above on Field 7. + # Field 9 - Artist Name - Populate this field from the Artist Name field in the JamTrack record - e.g. "AC/DC". + # Field 10 - Song Title - Populate this field from the Song Name field in the JamTrack record - e.g. "Back In Black". + # Field 21 - Configuration Code - Hardcode to "SP". + # Field 22 - License Type - Hardcode to "G". + # Field 23 - Server Fixation Date - Set this to the approximate date that the JamTrack was uploaded to our servers, and format as YYYYMMDD - e.g. "20150813". I'm suggesting we update each JamTrack record with this date, just so that we have a record of this piece of data we submitted to HFA - even though HFA didn't seem at all clear about how this data is used or why it matters. + # Field 24 - Rate Code - Hardcode to "S". + # Field 37 - User Defined - Populate this field with our internal JamKazam unique JamTrack ID. This field value is supposed to be passed back to us from HFA in the processed output file, and we'll need this to associate the HFA License Number with our internal JamTrack ID. + # Field 38 - Track ID - Let's also populate this field with our internal JamKazam unique JamTrack ID, just like Field 37, just for fun. + + + CSV.open(out, "wb") do |csv| + requests.each do |request| + line = {} + line['1'] = 'SSA' + line['2'] = 'M18303' + line['3'] = Time.now.to_date.strftime('%Y%m%d') + line['4'] = request.request_id + line['7'] = request.jam_track.duration / 60 + line['8'] = request.jam_track.duration % 60 + line['9'] = request.jam_track.original_artist + line['10'] = request.jam_track.name + line['21'] = 'SP' + line['22'] = 'G' + line['23'] = request.jam_track.server_fixation_date.strftime('%Y%m%d') + line['24'] = 'S' + line['37'] = request.jam_track.id + line['38'] = request.jam_track.id + + entry = [] + 38.times do |i| + entry << line[(i + 1).to_s] + end + csv << entry + end + end + + upload_path = "harry_fox_requests/#{request_name}" + s3_manager.upload(upload_path, out, content_type: 'text/csv') + + request.request_csv_filename = upload_path + request.save! + end + + + request + end + end + end +end + diff --git a/ruby/lib/jam_ruby/models/jam_track_hfa_request_id.rb b/ruby/lib/jam_ruby/models/jam_track_hfa_request_id.rb new file mode 100644 index 000000000..8dab5fb5c --- /dev/null +++ b/ruby/lib/jam_ruby/models/jam_track_hfa_request_id.rb @@ -0,0 +1,18 @@ +module JamRuby + class JamTrackHfaRequestId < ActiveRecord::Base + include JamRuby::S3ManagerMixin + + @@log = Logging.logger[JamTrackHfaRequestId] + + attr_accessible :name, as: :admin + + belongs_to :jam_track, class_name: "JamRuby::JamTrack" + belongs_to :jam_track_hfa_request, class_name: "JamRuby::JamTrackHfaRequest" + + validates :jam_track, presence: true + validates :jam_track_hfa_request, presence:true + + + end +end + diff --git a/ruby/lib/jam_ruby/models/jam_track_right.rb b/ruby/lib/jam_ruby/models/jam_track_right.rb index f0749dc94..5217b620d 100644 --- a/ruby/lib/jam_ruby/models/jam_track_right.rb +++ b/ruby/lib/jam_ruby/models/jam_track_right.rb @@ -99,9 +99,9 @@ module JamRuby # the idea is that this is used when a user who has the rights to this tries to download this JamTrack # we would verify their rights (can_download?), and generates a URL in response to the click so that they can download # but the url is short lived enough so that it wouldn't be easily shared - def sign_url(expiration_time = 120, bitrate=48) + def sign_url(expiration_time = 120, bitrate=48, secure=true) field_name = (bitrate==48) ? "url_48" : "url_44" - s3_manager.sign_url(self[field_name], {:expires => expiration_time, :secure => true}) + s3_manager.sign_url(self[field_name], {:expires => expiration_time, :secure => secure}) end def delete_s3_files diff --git a/ruby/lib/jam_ruby/models/musician_instrument.rb b/ruby/lib/jam_ruby/models/musician_instrument.rb index 445070da6..db4b318f4 100644 --- a/ruby/lib/jam_ruby/models/musician_instrument.rb +++ b/ruby/lib/jam_ruby/models/musician_instrument.rb @@ -13,8 +13,13 @@ module JamRuby belongs_to :player, polymorphic: true belongs_to :instrument, :class_name => "JamRuby::Instrument" + LEVEL_BEGIN = 1 + LEVEL_INTERMEDIATE = 2 + LEVEL_EXPERT = 3 + PROFICIENCY_RANGE = (LEVEL_BEGIN..LEVEL_EXPERT) + def description @description = self.instrument.description end end -end \ No newline at end of file +end diff --git a/ruby/lib/jam_ruby/models/sale.rb b/ruby/lib/jam_ruby/models/sale.rb index bca02f526..70279f8eb 100644 --- a/ruby/lib/jam_ruby/models/sale.rb +++ b/ruby/lib/jam_ruby/models/sale.rb @@ -151,6 +151,14 @@ module JamRuby sale.recurly_total_in_cents = 0 sale.recurly_currency = 'USD' + if sale.sale_line_items.count == 0 + @@log.info("no sale line items associated with sale") + # we must have ditched some of the sale items. let's just abort this sale + sale.destroy + sale = nil + return sale + end + sale_line_item = sale.sale_line_items[0] sale_line_item.recurly_tax_in_cents = 0 sale_line_item.recurly_total_in_cents = 0 diff --git a/ruby/lib/jam_ruby/models/search.rb b/ruby/lib/jam_ruby/models/search.rb index d1a4d13ea..5e98c7590 100644 --- a/ruby/lib/jam_ruby/models/search.rb +++ b/ruby/lib/jam_ruby/models/search.rb @@ -130,15 +130,18 @@ module JamRuby args = nil search_terms.each do |search_term| + # remove ( ) ! : from query terms. parser blows up + search_term.gsub!(/[\(\)!:]/, '') if args == nil - args = search_term + args = '"' + search_term + '"' else - args = args + " & " + search_term + args = args + " & " + '"' + search_term + '"' end end args = args + ":*" args end + def order_param(params, keys=M_ORDERING_KEYS) ordering = params[:orderby] ordering.blank? ? keys[0] : keys.detect { |oo| oo.to_s == ordering } diff --git a/ruby/lib/jam_ruby/models/user.rb b/ruby/lib/jam_ruby/models/user.rb index 0e8afac88..f7d48c669 100644 --- a/ruby/lib/jam_ruby/models/user.rb +++ b/ruby/lib/jam_ruby/models/user.rb @@ -24,6 +24,8 @@ module JamRuby MOD_GEAR_FRAME_OPTIONS = "show_frame_options" MOD_NO_SHOW = "no_show" + HOWTO_USE_VIDEO_NOSHOW = 'how-to-use-video' + CONFIGURE_VIDEO_NOSHOW = 'configure-video' # MIN/MAX AUDIO LATENCY MINIMUM_AUDIO_LATENCY = 2 diff --git a/ruby/spec/factories.rb b/ruby/spec/factories.rb index a82c83a4a..49f14b592 100644 --- a/ruby/spec/factories.rb +++ b/ruby/spec/factories.rb @@ -736,6 +736,7 @@ FactoryGirl.define do factory :jam_track, :class => JamRuby::JamTrack do sequence(:name) { |n| "jam-track-#{n}" } sequence(:description) { |n| "description-#{n}" } + sequence(:slug) { |n| "slug-#{n}" } time_signature '4/4' status 'Production' recording_type 'Cover' diff --git a/ruby/spec/jam_ruby/models/jam_track_hfa_request_spec.rb b/ruby/spec/jam_ruby/models/jam_track_hfa_request_spec.rb new file mode 100644 index 000000000..95eef5d2f --- /dev/null +++ b/ruby/spec/jam_ruby/models/jam_track_hfa_request_spec.rb @@ -0,0 +1,34 @@ +require 'spec_helper' + +describe JamTrackHfaRequest do + include CarrierWave::Test::Matchers + include UsesTempFiles + + let(:jamtrack1) {FactoryGirl.create(:jam_track, duration: 90, server_fixation_date: Time.now.to_date ) } + + it "creates request" do + + + JamTrackHfaRequest.find_max().should eq(0) + jamtrack1.touch + JamTrackHfaRequest.create('request1') + + request = JamTrackHfaRequest.first + request.request_csv_filename.should_not be_nil + request.approved_at.should be_nil + request.received_at.should be_nil + + request_id = JamTrackHfaRequestId.first + request_id.request_id.should_not be_nil + + # as long as the previous request is not approved, we don't move on with the counter + JamTrackHfaRequest.find_max().should eq(0) + + request.received_at = Time.now + request.save! + + # but once it's approved, we move on + JamTrackHfaRequest.find_max().should eq(1) + end +end + diff --git a/ruby/spec/jam_ruby/models/jam_track_spec.rb b/ruby/spec/jam_ruby/models/jam_track_spec.rb index c1aecf71d..814a4cbb3 100644 --- a/ruby/spec/jam_ruby/models/jam_track_spec.rb +++ b/ruby/spec/jam_ruby/models/jam_track_spec.rb @@ -159,8 +159,8 @@ describe JamTrack do describe "index" do it "empty query" do - query, pager = JamTrack.index({}, user) - query.size.should == 0 + query, pager, count = JamTrack.index({}, user) + count.should == 0 end it "sorts by name" do @@ -190,24 +190,24 @@ describe JamTrack do jam_track1.save! jam_track2.save! - query, pager = JamTrack.index({genre: 'rock'}, user) - query.size.should == 1 + query, pager, count = JamTrack.index({genre: 'rock'}, user) + count.should == 1 query[0].should eq(jam_track1) - query, pager = JamTrack.index({genre: 'asian'}, user) - query.size.should == 1 + query, pager, count = JamTrack.index({genre: 'asian'}, user) + count.should == 1 query[0].should eq(jam_track2) - query, pager = JamTrack.index({genre: 'african'}, user) - query.size.should == 0 + query, pager, count = JamTrack.index({genre: 'african'}, user) + count.should == 0 end it "supports showing purchased only" do jam_track1 = FactoryGirl.create(:jam_track_with_tracks, name: 'a') # no results yet - query, pager = JamTrack.index({show_purchased_only:true}, user) - query.size.should == 0 + query, pager, count = JamTrack.index({show_purchased_only:true}, user) + count.should == 0 # but after the user buys it, it is returned FactoryGirl.create(:jam_track_right, jam_track: jam_track1, user: user) @@ -215,6 +215,25 @@ describe JamTrack do query.size.should == 1 query[0].should eq(jam_track1) end + + it "full text search" do + jam_track1 = FactoryGirl.create(:jam_track_with_tracks, name: 'Take a Chance On Me', original_artist: 'ABBA') + jam_track2 = FactoryGirl.create(:jam_track_with_tracks, name: 'Nothing Chance', original_artist: 'ABBA') + + query, pager = JamTrack.index({search: 'Take'}, user) + query.size.should == 1 + query[0].should eq(jam_track1) + + query, pager = JamTrack.index({search: 'ABB'}, user) + query.size.should == 2 + + query, pager = JamTrack.index({search: 'Chance'}, user) + query.size.should == 2 + + query, pager = JamTrack.index({search: 'Chan'}, user) + query.size.should == 2 + + end end describe "validations" do diff --git a/ruby/spec/jam_ruby/models/musician_search_spec.rb b/ruby/spec/jam_ruby/models/musician_search_spec.rb index 012dbf482..bfd46c1c2 100644 --- a/ruby/spec/jam_ruby/models/musician_search_spec.rb +++ b/ruby/spec/jam_ruby/models/musician_search_spec.rb @@ -172,8 +172,9 @@ describe 'Musician Search Model' do end it "gets expected number of users" do - instjson = [{ id: Instrument.first.id, level: 2 }, - { id: Instrument.first(2)[1].id, level: 2 } + instjson = [{ instrument_id: Instrument.first.id, proficiency_level: 2 }, + { instrument_id: Instrument.first(2)[1].id, proficiency_level: 2 }, + { instrument_id: 'foo', proficiency_level: 2 }, ] search.update_json_value(MusicianSearch::KEY_INSTRUMENTS, instjson) expect(search.do_search.count).to eq(3) diff --git a/web/Gemfile b/web/Gemfile index 924910f1b..96604366d 100644 --- a/web/Gemfile +++ b/web/Gemfile @@ -97,6 +97,7 @@ gem 'react-rails', '~> 1.0' source 'https://rails-assets.org' do gem 'rails-assets-reflux' gem 'rails-assets-classnames' + gem 'rails-assets-react-select' end #group :development, :production do diff --git a/web/README.md b/web/README.md index 393628cf1..2b5096424 100644 --- a/web/README.md +++ b/web/README.md @@ -1,3 +1,4 @@ + Jasmine Javascript Unit Tests ============================= diff --git a/web/app/assets/images/content/webcam-icon-gray.png b/web/app/assets/images/content/webcam-icon-gray.png new file mode 100644 index 0000000000000000000000000000000000000000..c2b03c469f8a95a9c7d4607f1509862aeb389b0d GIT binary patch literal 802 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 literal 0 HcmV?d00001 diff --git a/web/app/assets/javascripts/accounts.js b/web/app/assets/javascripts/accounts.js index c04a6b329..ac5d3fa18 100644 --- a/web/app/assets/javascripts/accounts.js +++ b/web/app/assets/javascripts/accounts.js @@ -47,7 +47,11 @@ if(gon.global.video_available && gon.global.video_available!="none" ) { var webcamName; - var webcam = context.jamClient.FTUECurrentSelectedVideoDevice() + var webcam = null; + if (context.jamClient.FTUECurrentSelectedVideoDevice) { + webcam = context.jamClient.FTUECurrentSelectedVideoDevice() + } + if (webcam == null || typeof(webcam) == "undefined" || Object.keys(webcam).length == 0) { webcamName = "None Configured" } else { diff --git a/web/app/assets/javascripts/accounts_jamtracks.js.coffee b/web/app/assets/javascripts/accounts_jamtracks.js.coffee index 362a2e4d6..08785bc50 100644 --- a/web/app/assets/javascripts/accounts_jamtracks.js.coffee +++ b/web/app/assets/javascripts/accounts_jamtracks.js.coffee @@ -49,11 +49,13 @@ context.JK.AccountJamTracks = class AccountJamTracks #context.location="client#/createSession" jamRow = $(e.target).parents("tr") @createSession(jamRow.data(), true, jamRow.data('jamTrack')) + return false; groupSession:(e) => #context.location="client#/createSession" jamRow = $(e.target).parents("tr") @createSession(jamRow.data(), false, jamRow.data('jamTrack')) + return false; createSession:(sessionData, solo, jamTrack) => tracks = context.JK.TrackHelpers.getUserTracks(context.jamClient) @@ -70,8 +72,8 @@ context.JK.AccountJamTracks = class AccountJamTracks data.musician_access = !solo data.fan_access = false data.fan_chat = false - data.genre = [sessionData.genre] - data.genres = [sessionData.genre] + data.genre = $.map(sessionData.jamTrack.genres, (genre) -> genre.id) + data.genres = $.map(sessionData.jamTrack.genres, (genre)-> genre.id) # data.genres = context.JK.GenreSelectorHelper.getSelectedGenres('#create-session-genre') # data.musician_access = if $('#musician-access option:selected').val() == 'true' then true else false # data.approval_required = if $('input[name=\'musician-access-option\']:checked').val() == 'true' then true else false diff --git a/web/app/assets/javascripts/accounts_profile_interests.js b/web/app/assets/javascripts/accounts_profile_interests.js index 84b205777..48674efd6 100644 --- a/web/app/assets/javascripts/accounts_profile_interests.js +++ b/web/app/assets/javascripts/accounts_profile_interests.js @@ -121,19 +121,24 @@ // Column 2 - genres var genres = profileUtils.virtualBandGenreList(userDetail.genres) - $virtualBandGenreList.html(genres && genres.length > 0 ? genres : NONE_SPECIFIED) + var genreIds = profileUtils.getGenreIds(profileUtils.virtualBandGenres(userDetail.genres)); + $virtualBandGenreList.data('genres', genreIds).html(genres && genres.length > 0 ? genres : NONE_SPECIFIED) genres = profileUtils.traditionalBandGenreList(userDetail.genres) - $traditionalBandGenreList.html(genres && genres.length > 0 ? genres : NONE_SPECIFIED) + var genreIds = profileUtils.getGenreIds(profileUtils.traditionalBandGenres(userDetail.genres)); + $traditionalBandGenreList.data('genres', genreIds).html(genres && genres.length > 0 ? genres : NONE_SPECIFIED) genres = profileUtils.paidSessionGenreList(userDetail.genres) - $paidSessionsGenreList.html(genres && genres.length > 0 ? genres : NONE_SPECIFIED) + var genreIds = profileUtils.getGenreIds(profileUtils.paidSessionGenres(userDetail.genres)); + $paidSessionsGenreList.data('genres', genreIds).html(genres && genres.length > 0 ? genres : NONE_SPECIFIED) genres = profileUtils.freeSessionGenreList(userDetail.genres) - $freeSessionsGenreList.html(genres && genres.length > 0 ? genres : NONE_SPECIFIED) + var genreIds = profileUtils.getGenreIds(profileUtils.freeSessionGenres(userDetail.genres)); + $freeSessionsGenreList.data('genres', genreIds).html(genres && genres.length > 0 ? genres : NONE_SPECIFIED) genres = profileUtils.cowritingGenreList(userDetail.genres) - $cowritingGenreList.html(genres && genres.length > 0 ? genres : NONE_SPECIFIED) + var genreIds = profileUtils.getGenreIds(profileUtils.cowritingGenres(userDetail.genres)); + $cowritingGenreList.data('genres', genreIds).html(genres && genres.length > 0 ? genres : NONE_SPECIFIED) // Column 3 - misc (play commitment, rates, cowriting purpose) $virtualBandCommitment.val(userDetail.virtual_band_commitment) @@ -165,7 +170,7 @@ } ui.launchGenreSelectorDialog(type, genres, function(selectedGenres) { - $genreList.html(selectedGenres && selectedGenres.length > 0 ? selectedGenres.join(GENRE_LIST_DELIMITER) : NONE_SPECIFIED) + $genreList.data('genres', selectedGenres).html(selectedGenres && selectedGenres.length > 0 ? selectedGenres.join(GENRE_LIST_DELIMITER) : NONE_SPECIFIED) }) return false @@ -278,24 +283,24 @@ api.updateUser({ virtual_band: $screen.find('input[name=virtual_band]:checked').val(), - virtual_band_genres: $virtualBandGenreList.html() === NONE_SPECIFIED ? [] : $virtualBandGenreList.html().split(GENRE_LIST_DELIMITER), + virtual_band_genres: $virtualBandGenreList.data('genres'), virtual_band_commitment: $virtualBandCommitment.val(), traditional_band: $screen.find('input[name=traditional_band]:checked').val(), - traditional_band_genres: $traditionalBandGenreList.html() === NONE_SPECIFIED ? [] : $traditionalBandGenreList.html().split(GENRE_LIST_DELIMITER), + traditional_band_genres: $traditionalBandGenreList.data('genres'), traditional_band_commitment: $traditionalBandCommitment.val(), traditional_band_touring: $traditionalTouringOption.val(), paid_sessions: $screen.find('input[name=paid_sessions]:checked').val(), - paid_session_genres: $paidSessionsGenreList.html() === NONE_SPECIFIED ? [] : $paidSessionsGenreList.html().split(GENRE_LIST_DELIMITER), + paid_session_genres: $paidSessionsGenreList.data('genres'), paid_sessions_hourly_rate: profileUtils.normalizeMoneyForSubmit($hourlyRate.val()), paid_sessions_daily_rate: profileUtils.normalizeMoneyForSubmit($dailyRate.val()), free_sessions: $screen.find('input[name=free_sessions]:checked').val(), - free_session_genres: $freeSessionsGenreList.html() === NONE_SPECIFIED ? [] : $freeSessionsGenreList.html().split(GENRE_LIST_DELIMITER), + free_session_genres: $freeSessionsGenreList.data('genres'), cowriting: $screen.find('input[name=cowriting]:checked').val(), - cowriting_genres: $cowritingGenreList.html() === NONE_SPECIFIED ? [] : $cowritingGenreList.html().split(GENRE_LIST_DELIMITER), + cowriting_genres: $cowritingGenreList.data('genres'), cowriting_purpose: $cowritingPurpose.val() }) .done(postUpdateProfileSuccess) diff --git a/web/app/assets/javascripts/accounts_session_detail.js b/web/app/assets/javascripts/accounts_session_detail.js index 82c028e52..20b8d27ce 100644 --- a/web/app/assets/javascripts/accounts_session_detail.js +++ b/web/app/assets/javascripts/accounts_session_detail.js @@ -24,7 +24,6 @@ var $templateOpenSlots = null; var $templateAccountPendingRsvp = null; var $templateAccountSessionDetail = null; - var instrument_logo_map = context.JK.getInstrumentIconMap24(); var invitationDialog = null; var inviteMusiciansUtil = null; var friendInput=null; diff --git a/web/app/assets/javascripts/accounts_video_profile.js b/web/app/assets/javascripts/accounts_video_profile.js index ef4d4cf63..ca5e667ad 100644 --- a/web/app/assets/javascripts/accounts_video_profile.js +++ b/web/app/assets/javascripts/accounts_video_profile.js @@ -4,7 +4,7 @@ context.JK = context.JK || {}; context.JK.AccountVideoProfile = function (app) { - var $webcamViewer = new context.JK.WebcamViewer() + var webcamViewerReact = null; function initialize() { var screenBindings = { 'beforeShow': beforeShow, @@ -12,15 +12,18 @@ }; app.bindScreen('account/video', screenBindings); - $webcamViewer.init($("#account-video-profile .webcam-container")) + var reactElement = React.createElement(window.WebcamViewer, {isVisible: false, show_header: true, show_disable: true}); + var reactDomNode = $("#account-video-profile .webcam-container").get(0) + webcamViewerReact = React.render(reactElement, reactDomNode) } + function beforeShow() { - $webcamViewer.beforeShow() + webcamViewerReact.beforeShow() } function beforeHide() { - $webcamViewer.setVideoOff() + webcamViewerReact.beforeHide() } this.beforeShow = beforeShow diff --git a/web/app/assets/javascripts/backend_alerts.js b/web/app/assets/javascripts/backend_alerts.js index 348560630..bf5b1993b 100644 --- a/web/app/assets/javascripts/backend_alerts.js +++ b/web/app/assets/javascripts/backend_alerts.js @@ -131,6 +131,12 @@ // context.JK.CurrentSessionModel.onPlaybackStateChange(type, text); context.MediaPlaybackActions.playbackStateChange(text); } + else if(type === ALERT_NAMES.VIDEO_WINDOW_OPENED) { + context.VideoActions.videoWindowOpened() + } + else if(type === ALERT_NAMES.VIDEO_WINDOW_CLOSED) { + context.VideoActions.videoWindowClosed() + } else if((!context.JK.CurrentSessionModel || !context.JK.CurrentSessionModel.inSession()) && (ALERT_NAMES.INPUT_IO_RATE == type || ALERT_NAMES.INPUT_IO_JTR == type || ALERT_NAMES.OUTPUT_IO_RATE == type || ALERT_NAMES.OUTPUT_IO_JTR== type)) { // squelch these events if not in session diff --git a/web/app/assets/javascripts/checkout_signin.js b/web/app/assets/javascripts/checkout_signin.js index 9dad91bdf..ee16bf2ee 100644 --- a/web/app/assets/javascripts/checkout_signin.js +++ b/web/app/assets/javascripts/checkout_signin.js @@ -111,7 +111,7 @@ } }) .fail(function() { - window.location = '/client#/jamtrackBrowse' + window.location = '/client#/jamtrack/search' window.location.reload(); }) }) diff --git a/web/app/assets/javascripts/checkout_utils.js.coffee b/web/app/assets/javascripts/checkout_utils.js.coffee index 4570bd052..c16e09f9b 100644 --- a/web/app/assets/javascripts/checkout_utils.js.coffee +++ b/web/app/assets/javascripts/checkout_utils.js.coffee @@ -29,7 +29,7 @@ class CheckoutUtils @logger.debug("deleted preserve billing"); - unless $.cookie(@cookie_name)? + if $.cookie(@cookie_name)? @logger.error("after deleting the preserve billing cookie, it still exists!") diff --git a/web/app/assets/javascripts/client_init.js.coffee b/web/app/assets/javascripts/client_init.js.coffee index 78f2f58b9..c84859b69 100644 --- a/web/app/assets/javascripts/client_init.js.coffee +++ b/web/app/assets/javascripts/client_init.js.coffee @@ -39,4 +39,6 @@ context.JK.ClientInit = class ClientInit nativeClientInit: () => @gearUtils.bootstrapDefaultPlaybackProfile(); + window.VideoActions.checkPromptConfigureVideo() + diff --git a/web/app/assets/javascripts/dialog/banner.js b/web/app/assets/javascripts/dialog/banner.js index 2c777c0f0..431152e2d 100644 --- a/web/app/assets/javascripts/dialog/banner.js +++ b/web/app/assets/javascripts/dialog/banner.js @@ -110,7 +110,8 @@ } if(options.type == "yes_no") { - $yesBtn.show().unbind('click').click(function() { + var yesText = options.yes_text || 'YES' + $yesBtn.text(yesText).show().unbind('click').click(function() { if(options.yes) { options.yes(); } diff --git a/web/app/assets/javascripts/dialog/genreSelectorDialog.js b/web/app/assets/javascripts/dialog/genreSelectorDialog.js index 3a9bf4c4f..c119cfb06 100644 --- a/web/app/assets/javascripts/dialog/genreSelectorDialog.js +++ b/web/app/assets/javascripts/dialog/genreSelectorDialog.js @@ -28,7 +28,14 @@ checked = 'checked'; } - $genres.append('' + val.description); + var $input = $('') + $input.val(val.id) + if(checked == 'checked') { + $input.attr('checked', 'checked') + } + + $genres.append($input); + $genres.append(val.description); $genres.append(''); }); } diff --git a/web/app/assets/javascripts/dialog/gettingStartedDialog.js b/web/app/assets/javascripts/dialog/gettingStartedDialog.js index 0faa28699..867a411f2 100644 --- a/web/app/assets/javascripts/dialog/gettingStartedDialog.js +++ b/web/app/assets/javascripts/dialog/gettingStartedDialog.js @@ -50,7 +50,7 @@ $browserJamTrackBtn.click(function() { app.layout.closeDialog('getting-started') - window.location = '/client#/jamtrackBrowse' + window.location = '/client#/jamtrack/search' return false; }) diff --git a/web/app/assets/javascripts/dialog/openJamTrackDialog.js b/web/app/assets/javascripts/dialog/openJamTrackDialog.js index 15e46d6dc..8f58b0728 100644 --- a/web/app/assets/javascripts/dialog/openJamTrackDialog.js +++ b/web/app/assets/javascripts/dialog/openJamTrackDialog.js @@ -13,9 +13,76 @@ var $templateOpenJamTrackRow = null; var $downloadedTrackHelp = null; var $whatAreJamTracks = null; + var $searchBtn = null; var sampleRate = null; var sampleRateForFilename = null; + var searchQuery = null; + var cookieName = 'jamtrack_session_search' + // called by react autocomplote component + function search(searchType, searchData) { + window.JamTrackSearchInput = searchData; + searchQuery = {searchType: searchType, searchData: searchData} + $.cookie(cookieName, JSON.stringify(searchQuery)) + doSearch(searchQuery); + } + + function userSearch(e) { + e.preventDefault(); + searchQuery = {searchType: 'user-input', searchData: window.JamTrackSearchInput} + $.cookie(cookieName, JSON.stringify(searchQuery)) + doSearch(searchQuery); + } + + function doSearch(query) { + emptyList(); + resetPagination(); + + app.user().done(function(user) { + + var showSearch = (user.purchased_jamtracks_count > perPage) + + var $autocomplete = $dialog.find('[data-react-class="JamTrackAutoComplete"]') + + if (showSearch) { + $autocomplete.show() + $searchBtn.show() + + // if no query specified, look in a cookie for last query + if (!query) { + query = $.cookie(cookieName) + + // and parse that cookie if defined + if (query) { + try { + query = JSON.parse(query) + } + catch (e) { + query = {searchType: 'user-input', searchData: ''} + logger.error("unable to parse search query: " + e) + } + } + } + + // if still no query (after checking cookie and what was specified in function, then default to anything + if(!query){ + query = {searchType: 'user-input', searchData: ''} + } + } + else { + $autocomplete.hide() + $searchBtn.hide() + } + + getPurchasedJamTracks(0) + .done(function (data, textStatus, jqXHR) { + // initialize pagination + var $paginator = context.JK.Paginator.create(parseInt(jqXHR.getResponseHeader('total-entries')), perPage, 0, onPageSelected, 20) + $paginatorHolder.append($paginator); + }); + + }) + } function emptyList() { $tbody.empty(); @@ -30,19 +97,13 @@ } function afterShow() { + $dialog.data('result', null) - emptyList(); - resetPagination(); showing = true; sampleRate = context.jamClient.GetSampleRate() sampleRateForFilename = sampleRate == 48 ? '48' : '44'; + doSearch(); - getPurchasedJamTracks(0) - .done(function(data, textStatus, jqXHR) { - // initialize pagination - var $paginator = context.JK.Paginator.create(parseInt(jqXHR.getResponseHeader('total-entries')), perPage, 0, onPageSelected) - $paginatorHolder.append($paginator); - }); } function afterHide() { @@ -55,7 +116,21 @@ } function getPurchasedJamTracks(page) { - return rest.getPurchasedJamTracks({page:page + 1, per_page:10}) + + var query = {page:page + 1, per_page:10} + if (searchQuery && searchQuery.searchData && searchQuery.searchData.length > 0 && searchQuery.searchType && searchQuery.searchType.length > 0) { + + if (searchQuery.searchType == 'user-input') { + query.search = searchQuery.searchData + } + else if(searchQuery.searchType == 'artist-select') { + query.artist_search = searchQuery.searchData + } + else if(searchQuery.searchType == 'song-select') { + query.song_search = searchQuery.searchData + } + } + return rest.getPurchasedJamTracks(query) .done(function(purchasedJamTracks) { emptyList(); @@ -104,6 +179,8 @@ context.JK.helpBubble($whatAreJamTracks, 'no help yet for this topic', {}, {positions:['bottom'], offsetParent: $dialog}) $whatAreJamTracks.on('click', false) // no help yet + + $searchBtn.on('click', userSearch) } function initialize(){ @@ -121,6 +198,7 @@ $templateOpenJamTrackRow = $('#template-jam-track-row') $downloadedTrackHelp = $dialog.find('.downloaded-jamtrack-help') $whatAreJamTracks = $dialog.find('.what-are-jamtracks') + $searchBtn = $dialog.find('.search-btn') registerStaticEvents(); }; @@ -128,6 +206,7 @@ this.initialize = initialize; this.isShowing = function isShowing() { return showing; } + this.search = search; // called by react } return this; diff --git a/web/app/assets/javascripts/download_jamtrack.js.coffee b/web/app/assets/javascripts/download_jamtrack.js.coffee index 58592a73c..8890e6b87 100644 --- a/web/app/assets/javascripts/download_jamtrack.js.coffee +++ b/web/app/assets/javascripts/download_jamtrack.js.coffee @@ -243,6 +243,7 @@ context.JK.DownloadJamTrack = class DownloadJamTrack @logger.debug "downloadCheck" retry: () => + @logger.debug "user initiated retry" @path = [] @path.push('retry') this.clear() diff --git a/web/app/assets/javascripts/fakeJamClient.js b/web/app/assets/javascripts/fakeJamClient.js index 81826b5eb..7ff48dd44 100644 --- a/web/app/assets/javascripts/fakeJamClient.js +++ b/web/app/assets/javascripts/fakeJamClient.js @@ -74,21 +74,30 @@ function FTUESelectVideoCaptureDevice(device, settings) { - + return true; } function FTUESetVideoEncodeResolution(resolution) { - } + } + + function testVideoRender() { + + } function FTUEGetVideoCaptureDeviceNames() { - return ["Built-in Webcam HD"] + return {"xy323ss": "Built-in Webcam HD"} } function FTUECurrentSelectedVideoDevice() { + //return {} return {"xy323ss": "Built-in Webcam HD"} } function FTUEGetAvailableEncodeVideoResolutions() { return { - 1: "1024x768", - 2: "800x600" + 1 : "CIF (352X288)", + 2 : "VGA (640X480)", + 3 : "4CIF (704X576)", + 4 : "1/2WHD (640X360)", + 5 : "WHD (1280X720)", + 6 : "FHD (1920x1080)" } } @@ -96,6 +105,30 @@ return {} } + function FTUESetSendFrameRates(fps) { + + } + + function FTUEGetSendFrameRates() { + return {20:20, 24:24, 30:30} + } + + function GetCurrentVideoResolution() { + return 3; + } + + function GetCurrentVideoFrameRate() { + return 30; + } + + function FTUESetVideoShareEnable(){ + + } + + function FTUEGetVideoShareEnable() { + return false; + } + function isSessVideoShared() { return videoShared; } @@ -336,8 +369,8 @@ function GetOS() { return 100000000; } function GetOSAsString() { - return "Win32"; - //return "MacOSX"; + //return "Win32"; + return "MacOSX"; } function LatencyUpdated(map) { dbg('LatencyUpdated:' + JSON.stringify(map)); } @@ -433,10 +466,19 @@ } + function SetVideoNetworkTestScore(numClients) { + + } + function GetNetworkTestScore() { return 8; } + + function GetVideoNetworkTestScore() { + return 8; + } + function SetLatencyTestBlocked(blocked) { } @@ -1036,6 +1078,8 @@ this.IsMyNetworkWireless = IsMyNetworkWireless; this.SetNetworkTestScore = SetNetworkTestScore; this.GetNetworkTestScore = GetNetworkTestScore; + this.SetVideoNetworkTestScore = SetVideoNetworkTestScore; + this.GetVideoNetworkTestScore = GetVideoNetworkTestScore; this.SetLatencyTestBlocked = SetLatencyTestBlocked; this.isLatencyTestBlocked = isLatencyTestBlocked; this.GetLastLatencyTestTimes = GetLastLatencyTestTimes; @@ -1220,14 +1264,19 @@ this.OnDownloadAvailable = OnDownloadAvailable; // Video functionality: - this.FTUESelectVideoCaptureDevice = FTUESelectVideoCaptureDevice + this.testVideoRender = testVideoRender; + this.FTUESelectVideoCaptureDevice = FTUESelectVideoCaptureDevice; this.FTUESetVideoEncodeResolution = FTUESetVideoEncodeResolution; this.FTUEGetVideoCaptureDeviceNames = FTUEGetVideoCaptureDeviceNames; this.FTUECurrentSelectedVideoDevice = FTUECurrentSelectedVideoDevice; this.FTUEGetAvailableEncodeVideoResolutions = FTUEGetAvailableEncodeVideoResolutions; this.FTUEGetVideoCaptureDeviceCapabilities = FTUEGetVideoCaptureDeviceCapabilities; - - + this.FTUEGetSendFrameRates = FTUEGetSendFrameRates; + this.FTUESetSendFrameRates = FTUESetSendFrameRates; + this.GetCurrentVideoResolution = GetCurrentVideoResolution; + this.GetCurrentVideoFrameRate = GetCurrentVideoFrameRate; + this.FTUESetVideoShareEnable = FTUESetVideoShareEnable; + this.FTUEGetVideoShareEnable = FTUEGetVideoShareEnable; this.isSessVideoShared = isSessVideoShared; this.SessStopVideoSharing = SessStopVideoSharing; this.SessStartVideoSharing = SessStartVideoSharing; diff --git a/web/app/assets/javascripts/genreSelector.js b/web/app/assets/javascripts/genreSelector.js index e8341291d..335e67030 100644 --- a/web/app/assets/javascripts/genreSelector.js +++ b/web/app/assets/javascripts/genreSelector.js @@ -43,6 +43,23 @@ context.JK.dropdown($('select', parentSelector)); } + function render2($select, notSelectedString) { + if(!notSelectedString) { + notSelectedString = 'Any Genre' + } + $select.empty(); + $select.append(''); + 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.append(genreOptionHtml); + }); + context.JK.dropdown($select); + } + + function getSelectedGenres(parentSelector) { var selectedGenres = []; var selectedVal = $('select', parentSelector).val(); @@ -112,6 +129,10 @@ render: function() { var _args = arguments; context.JK.GenreSelectorDeferred.done(function(){render.apply(self, _args)}) + }, + render2: function() { + var _args = arguments; + context.JK.GenreSelectorDeferred.done(function(){render2.apply(self, _args)}) } }; diff --git a/web/app/assets/javascripts/globals.js b/web/app/assets/javascripts/globals.js index b8f138522..396e30be4 100644 --- a/web/app/assets/javascripts/globals.js +++ b/web/app/assets/javascripts/globals.js @@ -116,7 +116,16 @@ SHOW_PREFERENCES : 39, // tell frontend to show preferences dialog USB_CONNECTED : 40, // tell frontend that a USB device was connected USB_DISCONNECTED : 41, // tell frontend that a USB device was disconnected - LAST_ALERT : 42 + JAM_TRACK_SERVER_ERROR : 42, //error talking with server + BAD_INTERVAL_RATE : 43, //the audio gear is calling back at rate that does not match the expected interval + FIRST_AUDIO_PACKET : 44,// we are receiving audio from peer + NETWORK_PORT_MANGLED : 45, // packet from peer indicates network port is being mangled + NO_GLOBAL_CLOCK_SERVER : 46, //can't reach global clock NTP server + GLOBAL_CLOCK_SYNCED : 47, //clock synced + RECORDING_DONE :48, //the recording writer thread is done + VIDEO_WINDOW_OPENED :49, //video window opened + VIDEO_WINDOW_CLOSED :50, + LAST_ALERT : 51 } // recreate eThresholdType enum from MixerDialog.h context.JK.ALERT_TYPES = { @@ -171,7 +180,16 @@ 39: {"title": "", "message": ""}, // SHOW_PREFERENCES, //show preferences dialog 40: {"title": "", "message": ""}, // USB_CONNECTED 41: {"title": "", "message": ""}, // USB_DISCONNECTED, // tell frontend that a USB device was disconnected - 42: {"title": "", "message": ""} // LAST_ALERT + 42: {"title": "", "message": ""}, // JAM_TRACK_SERVER_ERROR + 43: {"title": "", "message": ""}, // BAD_INTERVAL_RATE + 44: {"title": "", "message": ""}, // FIRST_AUDIO_PACKET + 45: {"title": "", "message": ""}, // NETWORK_PORT_MANGLED + 46: {"title": "", "message": ""}, // NO_GLOBAL_CLOCK_SERVER + 47: {"title": "", "message": ""}, // GLOBAL_CLOCK_SYNCED + 48: {"title": "", "message": ""}, // RECORDING_DONE + 49: {"title": "", "message": ""}, // VIDEO_WINDOW_OPENED + 50: {"title": "", "message": ""}, // VIDEO_WINDOW_CLOSED + 51: {"title": "", "message": ""} // LAST_ALERT }; // add the alert's name to the ALERT_TYPES structure @@ -311,7 +329,9 @@ /** NAMED_MESSAGES means messages that we show to the user (dialogs/banners/whatever), that we have formally named */ context.JK.NAMED_MESSAGES = { - MASTER_VS_PERSONAL_MIX : 'master_vs_personal_mix' + MASTER_VS_PERSONAL_MIX : 'master_vs_personal_mix', + HOWTO_USE_VIDEO_NOSHOW : 'how-to-use-video', + CONFIGURE_VIDEO_NOSHOW : 'configure-video' } context.JK.ChannelGroupIds = { diff --git a/web/app/assets/javascripts/helpBubbleHelper.js b/web/app/assets/javascripts/helpBubbleHelper.js index 9e2e54083..0a6774631 100644 --- a/web/app/assets/javascripts/helpBubbleHelper.js +++ b/web/app/assets/javascripts/helpBubbleHelper.js @@ -112,7 +112,7 @@ helpBubble.jamtrackLandingCta = function($element, $alternativeElement) { if (!$alternativeElement || $element.visible()) { - context.JK.prodBubble($element, 'jamtrack-landing-cta', {}, bigHelpOptions({positions:['top', 'right'], width:240})) + context.JK.prodBubble($element, 'jamtrack-landing-cta', {}, bigHelpOptions({positions:['top', 'right'], width:260})) } else if($alternativeElement) { context.JK.prodBubble($alternativeElement, 'jamtrack-landing-cta', {}, bigHelpOptions({positions:['right']})) diff --git a/web/app/assets/javascripts/instrumentSelector.js b/web/app/assets/javascripts/instrumentSelector.js index 7c4197a3c..3a9322f07 100644 --- a/web/app/assets/javascripts/instrumentSelector.js +++ b/web/app/assets/javascripts/instrumentSelector.js @@ -9,6 +9,7 @@ var logger = context.JK.logger; var rest = new context.JK.Rest(); var _instruments = []; // will be list of structs: [ {label:xxx, value:yyy}, {...}, ... ] + var _instrumentsSorted = []; var _rsvp = false; var _noICheck = false; if (typeof(_parentSelector)=="undefined") {_parentSelector=null} @@ -35,6 +36,17 @@ label: this.description }); }); + + _instrumentsSorted = _instruments.slice().sort(sortAlpha) + } + + function sortAlpha(a, b) { + if (a.value == b.value) + return 0; + if (a.value < b.value) + return -1; + else + return 1; } function render(parentSelector, userInstruments) { @@ -85,6 +97,23 @@ } + function renderDropdown($select, notSelectedString) { + if(!notSelectedString) { + notSelectedString = 'Any Instrument' + } + $select.empty(); + $select.append(''); + var template = $('#template-instrument-option-simple').html(); + $.each(_instrumentsSorted, function(index, value) { + // value will be a dictionary entry from _genres: + // { value: xxx, label: yyy } + var instrumentOptionHtml = context.JK.fillTemplate(template, value); + $select.append(instrumentOptionHtml); + }); + context.JK.dropdown($select); + } + + function getSelectedInstruments() { var selectedInstruments = []; var $selectedVal = $('input[type="checkbox"]:checked', _parentSelector); @@ -152,6 +181,10 @@ var _args = arguments; context.JK.InstrumentSelectorDeferred.done(function(){render.apply(self, _args)}) } + this.renderDropdown = function() { + var _args = arguments; + context.JK.InstrumentSelectorDeferred.done(function(){renderDropdown.apply(self, _args)}) + } }); })(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 index 2256520f9..ff06650c2 100644 --- a/web/app/assets/javascripts/jam_rest.js +++ b/web/app/assets/javascripts/jam_rest.js @@ -1642,6 +1642,15 @@ }); } + function autocompleteJamTracks(options) { + return $.ajax({ + type: "GET", + url: '/api/jamtracks/autocomplete?' + $.param(options), + dataType: "json", + contentType: 'application/json' + }); + } + function getJamTrackArtists(options) { return $.ajax({ type: "GET", @@ -1715,14 +1724,15 @@ type: "POST", url: '/api/shopping_carts/add_jamtrack?' + $.param(options), dataType: "json", - contentType: 'applications/json' + contentType: 'application/json' }); } function getShoppingCarts() { + // the need for the time de-duplicator indicates we are doing something wrong on the server return $.ajax({ type: "GET", - url: '/api/shopping_carts', + url: '/api/shopping_carts?time=' + new Date().getTime(), dataType: "json", contentType: 'application/json' }); @@ -2050,6 +2060,7 @@ this.getJamTrack = getJamTrack; this.getJamTrackWithArtistInfo = getJamTrackWithArtistInfo; this.getJamTracks = getJamTracks; + this.autocompleteJamTracks = autocompleteJamTracks; this.getJamTrackArtists = getJamTrackArtists; this.getPurchasedJamTracks = getPurchasedJamTracks; this.getPaymentHistory = getPaymentHistory; diff --git a/web/app/assets/javascripts/jam_track_preview.js.coffee b/web/app/assets/javascripts/jam_track_preview.js.coffee index f78a25d05..9e74ce05c 100644 --- a/web/app/assets/javascripts/jam_track_preview.js.coffee +++ b/web/app/assets/javascripts/jam_track_preview.js.coffee @@ -30,10 +30,12 @@ context.JK.JamTrackPreview = class JamTrackPreview @playButton = @root.find('.play-button') @stopButton = @root.find('.stop-button') @instrumentIcon = @root.find('.instrument-icon') + @instrumentPart = @root.find('.instrument-part') @instrumentName = @root.find('.instrument-name') @part = @root.find('.part') @loading = @root.find('.loading') @loadingText = @root.find('.loading-text') + @loadingTextText = @root.find('.loading-text-text') @playButton.on('click', @play) @stopButton.on('click', @stop) @@ -42,10 +44,12 @@ context.JK.JamTrackPreview = class JamTrackPreview instrumentId = null instrumentDescription = '?' if @jamTrackTrack.track_type == 'Track' + @loadingTextText.text('20 second preview loading') if @jamTrackTrack.instrument instrumentId = @jamTrackTrack.instrument.id instrumentDescription = @jamTrackTrack.instrument.description else + @loadingTextText.text('preview loading') instrumentId = 'other' instrumentDescription= 'Master Mix' @@ -64,7 +68,7 @@ context.JK.JamTrackPreview = class JamTrackPreview if @options.master_adds_line_break part = '"' + @jamTrack.name + '"' + ' by ' + @jamTrack.original_artist - @part.html("#{part}") if part != '' + @part.text("#{part}") if part != '' @part.addClass('adds-line-break') else @@ -79,6 +83,7 @@ context.JK.JamTrackPreview = class JamTrackPreview @part.text("(#{part})") if part != '' + @instrumentPart.text(@instrumentName.text() + ' ' + @part.text()) if @jamTrackTrack.preview_mp3_url? @@ -109,9 +114,7 @@ context.JK.JamTrackPreview = class JamTrackPreview @sound.unload() removeNowPlaying: () => - context.JK.JamTrackPreview.nowPlaying.splice(this) - if context.JK.JamTrackPreview.nowPlaying.length > 0 - @logger.warn("multiple jamtrack previews playing") + context.JamTrackPreviewActions.stoppedPlaying(this) onHowlerEnd: () => @@ -122,8 +125,8 @@ context.JK.JamTrackPreview = class JamTrackPreview onHowlerLoad: () => @loaded = true - @loading.fadeOut(); - @loadingText.fadeOut(); #addClass('hidden') + @loading.fadeOut(2000); + @loadingText.fadeOut(2000); #addClass('hidden') play: (e) => if e? @@ -153,10 +156,7 @@ context.JK.JamTrackPreview = class JamTrackPreview @logger.debug("play issued for jam track preview") @sound.play() - for playingSound in context.JK.JamTrackPreview.nowPlaying - playingSound.issueStop() - context.JK.JamTrackPreview.nowPlaying = [] - context.JK.JamTrackPreview.nowPlaying.push(this) + context.JamTrackPreviewActions.startedPlaying(this) @playButton.addClass('hidden') @stopButton.removeClass('hidden') @@ -182,7 +182,4 @@ context.JK.JamTrackPreview = class JamTrackPreview return false -context.JK.JamTrackPreview.nowPlaying = [] - - diff --git a/web/app/assets/javascripts/jam_track_screen.js.coffee b/web/app/assets/javascripts/jam_track_screen.js.coffee index 92c514be6..bb0c2fa2b 100644 --- a/web/app/assets/javascripts/jam_track_screen.js.coffee +++ b/web/app/assets/javascripts/jam_track_screen.js.coffee @@ -180,7 +180,7 @@ context.JK.JamTrackScreen=class JamTrackScreen else @availability.val('') - if window.history.replaceState #ie9 proofing + if window.history.replaceState #ie9 proofing window.history.replaceState({}, "", "/client#/jamtrackBrowse") getParams:() => @@ -448,6 +448,7 @@ context.JK.JamTrackScreen=class JamTrackScreen this.handleExpanded(jamtrackRecord) initialize:() => + screenBindings = 'beforeShow': this.beforeShow 'afterShow': this.afterShow diff --git a/web/app/assets/javascripts/jamtrack_landing.js.coffee b/web/app/assets/javascripts/jamtrack_landing.js.coffee index 5198a1f64..73cb6d3fe 100644 --- a/web/app/assets/javascripts/jamtrack_landing.js.coffee +++ b/web/app/assets/javascripts/jamtrack_landing.js.coffee @@ -17,7 +17,8 @@ context.JK.JamTrackLanding = class JamTrackLanding screenBindings = 'beforeShow': @beforeShow 'afterShow': @afterShow - @app.bindScreen('jamtrackLanding', screenBindings) + + #@app.bindScreen('jamtrackLanding', screenBindings) @screen = $('#jamtrackLanding') @noFreeJamTrack = @screen.find('.no-free-jamtrack') @freeJamTrack = @screen.find('.free-jamtrack') @@ -64,14 +65,14 @@ context.JK.JamTrackLanding = class JamTrackLanding # client#/jamtrack for artist in artists - artistLink = "#{artist.original_artist} (#{artist.song_count})" + artistLink = "#{artist.original_artist} (#{artist.song_count})" @bandList.append("
  • #{artistLink}
  • ") # We don't want to do a full page load if this is clicked on here: bindArtistLinks:() => that=this @bandList.on "click", "a.artist-link", (event)-> - context.location="client#/jamtrackBrowse" + context.location="client#/jamtrack/search" if window.history.replaceState # ie9 proofing window.history.replaceState({}, "", this.href) event.preventDefault() diff --git a/web/app/assets/javascripts/layout.js b/web/app/assets/javascripts/layout.js index 51ce008dd..fd39f6b9f 100644 --- a/web/app/assets/javascripts/layout.js +++ b/web/app/assets/javascripts/layout.js @@ -577,6 +577,8 @@ $(document).triggerHandler(EVENTS.SCREEN_CHANGED, {previousScreen: previousScreen, newScreen: currentScreen}) + context.JamTrackPreviewActions.screenChange() + screenEvent(currentScreen, 'beforeShow', data); // For now -- it seems we want it open always. diff --git a/web/app/assets/javascripts/networkTestHelper.js b/web/app/assets/javascripts/networkTestHelper.js index 7481672b7..32f52afec 100644 --- a/web/app/assets/javascripts/networkTestHelper.js +++ b/web/app/assets/javascripts/networkTestHelper.js @@ -13,8 +13,10 @@ PktTestRateSweep: 4, RcvOnly: 5 } - var STARTING_NUM_CLIENTS = 4; - var PAYLOAD_SIZE = gon.global.ftue_network_test_packet_size; + var STARTING_NUM_CLIENTS_AUDIO = 4; + var STARTING_NUM_CLIENTS_VIDEO = 2; + var AUDIO_PAYLOAD_SIZE = gon.global.ftue_network_test_packet_size; + var VIDEO_PAYLOAD_SIZE = gon.global.ftue_network_test_packet_size_video; var MINIMUM_ACCEPTABLE_SESSION_SIZE = 2; var RETRY_THRESHOLD = 2; @@ -28,24 +30,32 @@ var $startNetworkTestBtn = null; var $foreverNetworkTestBtn = null; var $testResults = null; - var $testScore = null; + var $testScoreAudio = null; + var $testScoreVideo= null; var $testText = null; + var $inProgressText = null; + var $audioResultText = null; + var $videoResultText = null; var testedSuccessfully = false; var $scoringBar = null; var $goodMarker = null; var $goodLine = null; var $currentScore = null; - var $scoredClients = null; + var $scoredClientsAudio = null; + var $scoredClientsVideo = null; var $subscore = null; var $watchVideo = null; + var $container = null; var backendGuardTimeout = null; var primeGuardTimeout = null; var primeDeferred = null; var serverClientId = ''; - var scoring = false; - var numClientsToTest = STARTING_NUM_CLIENTS; - var testSummary = {attempts: [], final: null} + var audioScoring = false; + var videoScoring = false; + var numClientToTestAudio = STARTING_NUM_CLIENTS_AUDIO; + var numClientToTestVideo = STARTING_NUM_CLIENTS_VIDEO; + var testSummary = {audioAttempts: [], videoAttempts: [], final: null} var $self = $(this); var scoringZoneWidth = 100;//px var inGearWizard = false; @@ -124,30 +134,99 @@ function resetTestState() { serverClientId = ''; - scoring = false; - numClientsToTest = STARTING_NUM_CLIENTS; - testSummary = {attempts: []}; + audioScoring = false; + videoScoring = false; + numClientToTestAudio = STARTING_NUM_CLIENTS_AUDIO; + numClientToTestVideo = STARTING_NUM_CLIENTS_VIDEO; + testSummary = {audioAttempts: [], videoAttempts:[]}; configureStartButton(); - $scoredClients.empty(); + $scoredClientsAudio.empty(); $testResults.removeClass('good acceptable bad testing'); - $testText.empty(); + $testScoreAudio.removeClass('good acceptable bad testing'); + $testScoreVideo.removeClass('good acceptable bad testing'); + $scoredClientsAudio.text('-') + $scoredClientsVideo.text('-') + $inProgressText.empty(); + $audioResultText.empty(); + $audioResultText.hide(); + $videoResultText.empty(); + $videoResultText.hide(); $subscore.empty(); $currentScore.width(0); bandwidthSamples = []; } - function renderStartTest() { + function renderStartTestAudio() { configureStartButton(); $testResults.addClass('testing'); + $testScoreAudio.addClass('testing'); $goodLine.css('left', (gon.ftue_packet_rate_treshold * 100) + '%'); $goodMarker.css('left', (gon.ftue_packet_rate_treshold * 100) + '%'); } - function renderStopTest(score, text) { - $scoredClients.html(score); - $testText.html(text); + function renderStartTestVideo() { + configureStartButton(); + $testResults.addClass('testing'); + $testScoreVideo.addClass('testing'); + $goodLine.css('left', (gon.ftue_packet_rate_treshold * 100) + '%'); + $goodMarker.css('left', (gon.ftue_packet_rate_treshold * 100) + '%'); + } + + function renderStopTestAudio(score, text) { + if(!score || score.length == 0) { + $scoredClientsAudio.html('0'); + + $inProgressText.html('The audio test did not pass. Video is not tested in this case.

    Please click HERE for help information.'); + } + else { + $scoredClientsAudio.html(score); + } + + if(text && text.length > 0) { + $audioResultText.text(text); + } $testResults.removeClass('testing'); + $testScoreAudio.removeClass('testing'); + + } + + function renderStopTestVideo(score, text) { + logger.debug("renderStopTestVideo", score, text) + + // don't show the audio result text until the test is over (it looks confusing otherwise). + if($audioResultText.text() && $audioResultText.text().length > 0) { + $audioResultText.show(); + } + + $inProgressText.text('Your router and Internet service will support:') + + if(!score || score.length == 0) { + $scoredClientsVideo.html('-'); + } + else { + + if(score < 2) { + $scoredClientsVideo.html('0') + $videoResultText.html('No other players when in a video + audio session.').show(); + $testScoreVideo.addClass('acceptable'); + } + else { + $scoredClientsVideo.html(score); + + var summary = "Video + audio sessions with up to " + score + " players"; + + if (text && text.length > 0) { + // presence of text means there was an error on the last test pass. + summary += '. Note that there was an error when testing for ' + (score + 1) + 'players. Support code=' + text + } + + $videoResultText.html(summary).show(); + } + } + + $testResults.removeClass('testing'); + $testScoreVideo.removeClass('testing'); } function postDiagnostic() { @@ -205,8 +284,8 @@ } } - function testFinished() { - var attempt = getCurrentAttempt(); + function testFinishedAudio() { + var attempt = getCurrentAttemptAudio(); if (!testSummary.final) { testSummary.final = {reason: attempt.reason}; @@ -216,7 +295,7 @@ var success = false; if (reason == "success") { - renderStopTest(attempt.num_clients, "Your router and Internet service will support sessions of up to " + attempt.num_clients + " JamKazam musicians.") + renderStopTestAudio(attempt.num_clients, "Audio-only sessions with up to " + attempt.num_clients + " players") testedSuccessfully = true; if (!testSummary.final.num_clients) { testSummary.final.num_clients = attempt.num_clients; @@ -231,112 +310,261 @@ context.jamClient.SetNetworkTestScore(attempt.num_clients); if (testSummary.final.num_clients == 2) { - $testResults.addClass('acceptable'); + $testScoreAudio.addClass('acceptable'); } else { - $testResults.addClass('good'); + $testScoreAudio.addClass('good'); } success = true; } else if (reason == "minimum_client_threshold") { context.jamClient.SetNetworkTestScore(0); - renderStopTest('', "We're sorry, but your router and Internet service will not effectively support JamKazam sessions. Please click the HELP button for more information.") + renderStopTestAudio('', "We're sorry, but your router and Internet service will not effectively support JamKazam sessions. Please click the HELP button for more information.") storeLastNetworkFailure(context.JK.GA.NetworkTestFailReasons.bandwidth, avgBandwidth(attempt.num_clients - 1)); } else if (reason == "unreachable" || reason == "no-transmit") { context.jamClient.SetNetworkTestScore(0); // https://jamkazam.atlassian.net/browse/VRFS-2323 - renderStopTest('', "We're sorry, but your router will not support JamKazam in its current configuration. Please click HERE for more information."); + renderStopTestAudio('', "We're sorry, but your router will not support JamKazam in its current configuration. Please click HERE for more information."); storeLastNetworkFailure(context.JK.GA.NetworkTestFailReasons.stun, attempt.num_clients); } else if (reason == "internal_error") { context.JK.alertSupportedNeeded("The JamKazam client software had an unexpected problem while scoring your Internet connection."); - renderStopTest('', ''); + renderStopTestAudio('', ''); storeLastNetworkFailure(context.JK.GA.NetworkTestFailReasons.jamerror); } else if (reason == "remote_peer_cant_test") { context.JK.alertSupportedNeeded("The JamKazam service is experiencing technical difficulties."); - renderStopTest('', ''); + renderStopTestAudio('', ''); storeLastNetworkFailure(context.JK.GA.NetworkTestFailReasons.jamerror); } else if (reason == "server_comm_timeout") { gearUtils.skipNetworkTest(); context.JK.alertSupportedNeeded("Communication with the JamKazam network service has timed out." + appendContextualStatement()); - renderStopTest('', ''); + renderStopTestAudio('', ''); storeLastNetworkFailure(context.JK.GA.NetworkTestFailReasons.jamerror); } else if (reason == 'backend_gone') { context.JK.alertSupportedNeeded("The JamKazam client is experiencing technical difficulties."); - renderStopTest('', ''); + renderStopTestAudio('', ''); storeLastNetworkFailure(context.JK.GA.NetworkTestFailReasons.jamerror); } else if (reason == "invalid_response") { gearUtils.skipNetworkTest(); context.JK.alertSupportedNeeded("The JamKazam client software had an unexpected problem while scoring your Internet connection.

    Reason: " + attempt.backend_data.reason + '.'); - renderStopTest('', ''); + renderStopTestAudio('', ''); storeLastNetworkFailure(context.JK.GA.NetworkTestFailReasons.jamerror); } else if (reason == 'no_servers') { gearUtils.skipNetworkTest(); context.JK.Banner.showAlert("No network test servers are available." + appendContextualStatement()); - renderStopTest('', ''); + renderStopTestAudio('', ''); testedSuccessfully = true; storeLastNetworkFailure(context.JK.GA.NetworkTestFailReasons.jamerror); } else if (reason == 'no_network') { context.JK.Banner.showAlert("Please try again later. Your network appears down."); - renderStopTest('', ''); + renderStopTestAudio('', ''); storeLastNetworkFailure(context.JK.GA.NetworkTestFailReasons.noNetwork); } else if (reason == "rest_api_error") { gearUtils.skipNetworkTest(); context.JK.alertSupportedNeeded("Unable to acquire a network test server." + appendContextualStatement()); testedSuccessfully = true; - renderStopTest('', ''); + renderStopTestAudio('', ''); storeLastNetworkFailure(context.JK.GA.NetworkTestFailReasons.jamerror); } else if (reason == "timeout") { gearUtils.skipNetworkTest(); context.JK.alertSupportedNeeded("Communication with the JamKazam network service timed out." + appendContextualStatement()); testedSuccessfully = true; - renderStopTest('', ''); + renderStopTestAudio('', ''); storeLastNetworkFailure(context.JK.GA.NetworkTestFailReasons.jamerror); } else { gearUtils.skipNetworkTest(); context.JK.alertSupportedNeeded("The JamKazam client software had a logic error while scoring your Internet connection."); - renderStopTest('', ''); + renderStopTestAudio('', ''); storeLastNetworkFailure(context.JK.GA.NetworkTestFailReasons.jamerror); } - numClientsToTest = STARTING_NUM_CLIENTS; - scoring = false; - configureStartButton(); + numClientToTestAudio = STARTING_NUM_CLIENTS_AUDIO; + audioScoring = false; postDiagnostic(); if (success) { - $self.triggerHandler(NETWORK_TEST_DONE) - if(forever) { - prepareNetworkTest(); + + if(window.VideoStore.isVideoEnabled()) { + startVideoTest(); + } + else { + // don't test video if it's disabled + renderStopTestVideo(null, null) + + numClientToTestVideo = STARTING_NUM_CLIENTS_VIDEO; + videoScoring = false; + configureStartButton(); + + $self.triggerHandler(NETWORK_TEST_DONE) } } else { + configureStartButton(); + $self.triggerHandler(NETWORK_TEST_FAIL) } } - function getCurrentAttempt() { - return testSummary.attempts[testSummary.attempts.length - 1]; + function testFinishedVideo() { + + var attempt = getCurrentAttemptVideo(); + + if (!testSummary.video_final) { + testSummary.video_final = {reason: attempt.reason, num_clients: attempt.num_clients}; + } + + if (!testSummary.video_final.num_clients) { + testSummary.video_final.num_clients = attempt.num_clients; + } + + var reason = testSummary.video_final.reason; + var success = false; + + logger.debug("testFinishedVideo", testSummary) + + if (reason == "success") { + renderStopTestVideo(attempt.num_clients, null) + //testedSuccessfully = true; + if (!testSummary.video_final.num_clients) { + testSummary.video_final.num_clients = attempt.num_clients; + } + + // context.jamClient.GetNetworkTestScore() == 0 is a rough approximation if the user has passed the FTUE before + if (inGearWizard || context.jamClient.GetVideoNetworkTestScore() == 0) { + //trackedPass = true; + //lastNetworkFailure = null; + //context.JK.GA.trackNetworkTest(context.JK.detectOS(), testSummary.final.num_clients); + } + + context.jamClient.SetVideoNetworkTestScore(attempt.num_clients); + if (!testSummary.video_final.num_clients) { + $testScoreVideo.addClass('acceptable'); + } + else if (testSummary.video_final.num_clients >= 2) { + $testScoreVideo.addClass('good'); + } + else { + $testScoreVideo.addClass('acceptable'); + } + success = true; + } + else if (reason == "minimum_client_threshold") { + context.jamClient.SetVideoNetworkTestScore(testSummary.video_final.num_clients - 1); + renderStopTestVideo(testSummary.video_final.num_clients - 1, reason) + //storeLastNetworkFailure(context.JK.GA.NetworkTestFailReasons.bandwidth, avgBandwidth(attempt.num_clients - 1)); + } + else if (reason == "unreachable" || reason == "no-transmit") { + context.jamClient.SetVideoNetworkTestScore(testSummary.video_final.num_clients - 1); + // https://jamkazam.atlassian.net/browse/VRFS-2323 + renderStopTestVideo(testSummary.video_final.num_clients - 1, reason); + // storeLastNetworkFailure(context.JK.GA.NetworkTestFailReasons.stun, attempt.num_clients); + } + else if (reason == "internal_error") { + context.JK.alertSupportedNeeded("The JamKazam client software had an unexpected problem while scoring your Internet connection."); + renderStopTestVideo(testSummary.video_final.num_clients - 1, reason); + //storeLastNetworkFailure(context.JK.GA.NetworkTestFailReasons.jamerror); + } + else if (reason == "remote_peer_cant_test") { + context.JK.alertSupportedNeeded("The JamKazam service is experiencing technical difficulties."); + renderStopTestVideo(testSummary.video_final.num_clients - 1, reason); + //storeLastNetworkFailure(context.JK.GA.NetworkTestFailReasons.jamerror); + } + else if (reason == "server_comm_timeout") { + //gearUtils.skipNetworkTest(); + context.JK.alertSupportedNeeded("Communication with the JamKazam network service has timed out."); + renderStopTestVideo(testSummary.video_final.num_clients - 1, reason); + //storeLastNetworkFailure(context.JK.GA.NetworkTestFailReasons.jamerror); + } + else if (reason == 'backend_gone') { + context.JK.alertSupportedNeeded("The JamKazam client is experiencing technical difficulties."); + renderStopTestVideo(testSummary.video_final.num_clients - 1, reason); + //storeLastNetworkFailure(context.JK.GA.NetworkTestFailReasons.jamerror); + } + else if (reason == "invalid_response") { + //gearUtils.skipNetworkTest(); + context.JK.alertSupportedNeeded("The JamKazam client software had an unexpected problem while scoring your Internet connection.

    Reason: " + attempt.backend_data.reason + '.'); + renderStopTestVideo(testSummary.video_final.num_clients - 1, reason); + //storeLastNetworkFailure(context.JK.GA.NetworkTestFailReasons.jamerror); + } + else if (reason == 'no_servers') { + // gearUtils.skipNetworkTest(); + context.JK.Banner.showAlert("No network test servers are available."); + renderStopTestVideo(null, reason); + //testedSuccessfully = true; + //storeLastNetworkFailure(context.JK.GA.NetworkTestFailReasons.jamerror); + } + else if (reason == 'no_network') { + context.JK.Banner.showAlert("Please try again later. Your network appears down."); + renderStopTestVideo(testSummary.video_final.num_clients - 1, reason); + //storeLastNetworkFailure(context.JK.GA.NetworkTestFailReasons.noNetwork); + } + else if (reason == "rest_api_error") { + //gearUtils.skipNetworkTest(); + context.JK.alertSupportedNeeded("Unable to acquire a network test server."); + //testedSuccessfully = true; + renderStopTestVideo(null, reason); + //storeLastNetworkFailure(context.JK.GA.NetworkTestFailReasons.jamerror); + } + else if (reason == "timeout") { + //gearUtils.skipNetworkTest(); + context.JK.alertSupportedNeeded("Communication with the JamKazam network service timed out."); + //testedSuccessfully = true; + renderStopTestVideo(testSummary.video_final.num_clients - 1, reason); + //storeLastNetworkFailure(context.JK.GA.NetworkTestFailReasons.jamerror); + } + else { + //gearUtils.skipNetworkTest(); + context.JK.alertSupportedNeeded("The JamKazam client software had a logic error while scoring your Internet connection."); + renderStopTestVideo(testSummary.video_final.num_clients - 1, reason); + //storeLastNetworkFailure(context.JK.GA.NetworkTestFailReasons.jamerror); + } + + numClientToTestVideo = STARTING_NUM_CLIENTS_VIDEO; + videoScoring = false; + configureStartButton(); + postDiagnostic(); + + $self.triggerHandler(NETWORK_TEST_DONE) } - function isFirstAttempt() { - return testSummary.attempts.length == 0 || testSummary.attempts.length == 1; + function startVideoTest() { + + videoScoring = true; + + renderStartTestVideo(); + + setTimeout(attemptTestPassVideo, 500); + } + + function getCurrentAttemptAudio() { + return testSummary.audioAttempts[testSummary.audioAttempts.length - 1]; + } + function getCurrentAttemptVideo() { + return testSummary.videoAttempts[testSummary.videoAttempts.length - 1]; + } + + function isFirstAttemptAudio() { + return testSummary.audioAttempts.length == 0 || testSummary.audioAttempts.length == 1; + } + + function isFirstAttemptVideo() { + return testSummary.videoAttempts.length == 0 || testSummary.videoAttempts.length == 1; } function hasGoneDown() { var goneDown = false; - context._.each(testSummary.attempts, function(attempt) { - if(attempt.num_clients == STARTING_NUM_CLIENTS - 1) { + context._.each(testSummary.audioAttempts, function(attempt) { + if(attempt.num_clients == STARTING_NUM_CLIENTS_AUDIO - 1) { goneDown = true return false; } @@ -354,8 +582,8 @@ var testSessionSize = null; var numSameSizeTests = 0; - for(i = testSummary.attempts.length - 1; i >= 0; i--) { - var attempt = testSummary.attempts[i]; + for(i = testSummary.audioAttempts.length - 1; i >= 0; i--) { + var attempt = testSummary.audioAttempts[i]; if(testSessionSize === null) { // this is the 1st loop through. just recording the testSessionSize @@ -381,13 +609,20 @@ function primeTimedOut() { logger.warn("backend never completed priming pump phase"); - scoring = false; + audioScoring = false; primeDeferred.reject(); } function backendTimedOut() { - testSummary.final = {reason: 'backend_gone'} - testFinished(); + + if (audioScoring) { + testSummary.final = {reason: 'backend_gone'} + testFinishedAudio(); + } + else { + testSummary.video_final = {reason: 'backend_gone'} + testFinishedVideo(); + } } function cancel() { @@ -429,36 +664,67 @@ }, (gon.ftue_network_test_duration + 5) * 1000); } - function attemptTestPass() { + function attemptTestPassAudio() { var attempt = {}; - attempt.payload_size = PAYLOAD_SIZE; + attempt.payload_size = AUDIO_PAYLOAD_SIZE; attempt.duration = gon.ftue_network_test_duration; attempt.test_type = 'PktTest400LowLatency'; - attempt.num_clients = numClientsToTest; + attempt.num_clients = numClientToTestAudio; attempt.server_client_id = serverClientId; attempt.received_progress = false; - testSummary.attempts.push(attempt); + testSummary.audioAttempts.push(attempt); + + $scoredClientsAudio.text(numClientToTestAudio); //context.jamClient.StopNetworkTest(''); - $testText.text("Simulating the network traffic of a " + numClientsToTest + "-person session."); + $inProgressText.html("Simulating the network traffic of a " + numClientToTestAudio + "-person audio-only session. Video will be tested next."); updateProgress(0, false); setBackendGuard(); - logger.debug("network test attempt: " + numClientsToTest + "-person session, 400 packets/s, " + PAYLOAD_SIZE + " byte payload") + audioScoring = true; + logger.debug("network test attempt: " + numClientToTestAudio + "-person audio session, 400 packets/s, " + AUDIO_PAYLOAD_SIZE + " byte payload") context.jamClient.TestNetworkPktBwRate(serverClientId, createSuccessCallbackName(false), createTimeoutCallbackName(false), NETWORK_TEST_TYPES.PktTest400LowLatency, gon.ftue_network_test_duration, - numClientsToTest - 1, - PAYLOAD_SIZE, gon.global.ftue_network_test_backend_retries); + numClientToTestAudio - 1, + AUDIO_PAYLOAD_SIZE, gon.global.ftue_network_test_backend_retries); } + function attemptTestPassVideo() { + + var attempt = {}; + attempt.payload_size = VIDEO_PAYLOAD_SIZE; + attempt.duration = gon.ftue_network_test_duration; + attempt.test_type = 'PktTest400LowLatency'; + attempt.num_clients = numClientToTestVideo; + attempt.server_client_id = serverClientId; + attempt.received_progress = false; + testSummary.videoAttempts.push(attempt); + + $scoredClientsVideo.text(numClientToTestVideo); + //context.jamClient.StopNetworkTest(''); + + $inProgressText.html("Simulating the network traffic of a " + numClientToTestVideo + "-person video-enabled session."); + + updateProgress(0, false); + + setBackendGuard(); + + logger.debug("network test attempt: " + numClientToTestVideo + "-person video session, 400 packets/s, " + VIDEO_PAYLOAD_SIZE + " byte payload") + context.jamClient.TestNetworkPktBwRate(serverClientId, createSuccessCallbackName(false), createTimeoutCallbackName(false), + NETWORK_TEST_TYPES.PktTest400LowLatency, + gon.ftue_network_test_duration, + numClientToTestVideo - 1, + VIDEO_PAYLOAD_SIZE, gon.global.ftue_network_test_backend_retries); + } + // you have to score a little to 'prime' the logic to know whether it's on wireless or not function primePump() { - scoring = true; + audioScoring = true; primeDeferred = new $.Deferred(); setPrimeGuard(); @@ -467,21 +733,21 @@ NETWORK_TEST_TYPES.PktTest400LowLatency, PRIME_PUMP_TIME, 2, - PAYLOAD_SIZE, gon.global.ftue_network_test_backend_retries); + AUDIO_PAYLOAD_SIZE, gon.global.ftue_network_test_backend_retries); return primeDeferred; } function cancelTest() { - scoring = false; + audioScoring = false; configureStartButton(); - renderStopTest(); + renderStopTestAudio(); $self.triggerHandler(NETWORK_TEST_CANCEL) } function postPumpRun() { -// check if on Wifi 1st + // check if on Wifi 1st var isWireless = context.jamClient.IsMyNetworkWireless(); if (isWireless == -1) { logger.warn("unable to determine if the user is on wireless or not for network test. skipping prompt.") @@ -493,7 +759,7 @@ cancelTest(); }}, {name: 'RUN NETWORK TEST ANYWAY', click: function () { - attemptTestPass(); + attemptTestPassAudio(); ; }} ], @@ -503,7 +769,7 @@ "A WiFi connection is likely to cause significant issues in both latency and audio quality.

    "}) } else { - attemptTestPass(); + attemptTestPassAudio(); } } @@ -565,16 +831,15 @@ } function prepareNetworkTest() { - if (scoring) return false; - + if (audioScoring || videoScoring) return false; setTimeout(function() { logger.info("starting network test"); resetTestState(); - scoring = true; + audioScoring = true; $self.triggerHandler(NETWORK_TEST_START); - renderStartTest(); + renderStartTestAudio(); rest.getLatencyTester() .done(function (response) { // ensure there are no tests ongoing @@ -598,7 +863,7 @@ cancelTest(); }}, {name: 'RUN NETWORK TEST ANYWAY', click: function () { - attemptTestPass(); + attemptTestPassAudio(); ; }} ], @@ -622,7 +887,7 @@ testSummary.final = {reason: 'rest_api_error'} } } - testFinished(); + testFinishedAudio(); }) }, pauseForRecentScoresTime()) @@ -660,7 +925,7 @@ return; } clearPrimeGuard(); - scoring = false; + audioScoring = false; logger.debug("the prime pump routine timed out") primeDeferred.reject(); } @@ -682,60 +947,60 @@ // the interface is wireless, or not setTimeout(function () { logger.debug("pump primed"); - scoring = false; + audioScoring = false; primeDeferred.resolve(); }, 500); // give backend room to breath for timing/race issues } } - function networkTestComplete(data) { + function networkTestCompleteAudio(data) { if(!isAttemptingPass()) { logger.error("networkTestComplete: already completed the test pass. indicates backend sent > 1 final event"); return; } - var attempt = getCurrentAttempt(); + var attempt = getCurrentAttemptAudio(); function refineTest(up) { if (up === null) { - logger.debug("retrying test at size: " + numClientsToTest); - setTimeout(attemptTestPass, 500); // wait a second to avoid race conditions with client/server comm + logger.debug("retrying test at size: " + numClientToTestAudio); + setTimeout(attemptTestPassAudio, 500); // wait a second to avoid race conditions with client/server comm } else if (up) { - if (numClientsToTest == gon.ftue_network_test_max_clients) { + if (numClientToTestAudio == gon.ftue_network_test_max_clients) { attempt.reason = "success"; - testFinished(); + testFinishedAudio(); } else if(hasGoneDown()) { // this means we've gone up before... so don't go back down (i.e., creating a loop) attempt.reason = "success"; - testSummary.final = { reason: 'success', num_clients: numClientsToTest } - testFinished(); + testSummary.final = { reason: 'success', num_clients: numClientToTestAudio } + testFinishedAudio(); } else { - numClientsToTest++; - logger.debug("increasing number of clients to " + numClientsToTest); - setTimeout(attemptTestPass, 500); // wait a second to avoid race conditions with client/server comm + numClientToTestAudio++; + logger.debug("increasing number of clients to " + numClientToTestAudio); + setTimeout(attemptTestPassAudio, 500); // wait a second to avoid race conditions with client/server comm } } else { // reduce numclients if we can - if (numClientsToTest == MINIMUM_ACCEPTABLE_SESSION_SIZE) { + if (numClientToTestAudio == MINIMUM_ACCEPTABLE_SESSION_SIZE) { // we are too low already. fail the user attempt.reason = "minimum_client_threshold"; - testFinished(); + testFinishedAudio(); } - else if (numClientsToTest > STARTING_NUM_CLIENTS) { + else if (numClientToTestAudio > STARTING_NUM_CLIENTS_AUDIO) { // this means we've gone up before... so don't go back down (i.e., creating a loop) attempt.reason = "success"; - testSummary.final = { reason: 'success', num_clients: numClientsToTest - 1 } - testFinished(); + testSummary.final = { reason: 'success', num_clients: numClientToTestAudio - 1 } + testFinishedAudio(); } else { - numClientsToTest--; - logger.debug("reducing number of clients to " + numClientsToTest); - setTimeout(attemptTestPass, 500); // wait a second to avoid race conditions with client/server comm + numClientToTestAudio--; + logger.debug("reducing number of clients to " + numClientToTestAudio); + setTimeout(attemptTestPassAudio, 500); // wait a second to avoid race conditions with client/server comm } } } @@ -771,36 +1036,36 @@ if (data.reason == "unreachable") { logger.debug("network test: unreachable (STUN issue or similar)") attempt.reason = data.reason; - testFinished(); + testFinishedAudio(); } else if (data.reason == "no-transmit") { logger.debug("network test: no-transmit (STUN issue or similar)"); attempt.reason = data.reason; - testFinished(); + testFinishedAudio(); } else if (data.reason == "internal_error") { // oops logger.debug("network test: internal_error (client had a unexpected problem)"); attempt.reason = data.reason; - testFinished(); + testFinishedAudio(); } else if (data.reason == "remote_peer_cant_test") { // old client logger.debug("network test: remote_peer_cant_test (old client)") attempt.reason = data.reason; - testFinished(); + testFinishedAudio(); } else if (data.reason == "server_comm_timeout") { logger.debug("network test: server_comm_timeout (communication with server problem)") attempt.reason = data.reason; - testFinished(); + testFinishedAudio(); } else { if (!data.downthroughput || !data.upthroughput) { // we have to assume this is bad. just not a reason we know about in code logger.debug("network test: no test data (unknown issue? " + data.reason + ")") attempt.reason = "invalid_response"; - testFinished(); + testFinishedAudio(); } else { // success... but we still have to verify if this data is within threshold @@ -826,9 +1091,123 @@ } - function networkTestTimeout(data) { + function networkTestCompleteVideo(data) { if(!isAttemptingPass()) { - logger.error("networkTestTimeout: already completed the test pass. indicates backend sent > 1 final event"); + logger.error("networkTestCompleteVideo: already completed the test pass. indicates backend sent > 1 final event"); + return; + } + + var attempt = getCurrentAttemptVideo(); + + function refineTest(up) { + if (up === null) { + logger.debug("retrying video test at size: " + numClientToTestVideo); + setTimeout(attemptTestPassVideo, 500); // wait a second to avoid race conditions with client/server comm + } + else if (up) { + if (numClientToTestVideo == gon.ftue_network_test_max_clients) { + attempt.reason = "success"; + testFinishedVideo(); + } + else { + numClientToTestVideo++; + logger.debug("increasing number of clients to " + numClientToTestVideo); + setTimeout(attemptTestPassVideo, 500); // wait a second to avoid race conditions with client/server comm + } + } + else { + attempt.reason = "success"; + testSummary.video_final = { reason: 'success', num_clients: numClientToTestVideo - 1 } + testFinishedVideo(); + } + } + + attempt.backend_data = data; + + if (data.progress === true) { + + setBackendGuard(); + + var animate = true; + if (data.downthroughput && data.upthroughput) { + + if (data.downthroughput > 0 || data.upthroughput > 0) { + attempt.received_progress = true; + animate = true; + } + + if (attempt.received_progress) { + // take the lower + var throughput = data.downthroughput < data.upthroughput ? data.downthroughput : data.upthroughput; + + bandwidthSamples.push(data.upthroughput); + + updateProgress(throughput, true); + } + } + } + else { + clearBackendGuard(); + logger.debug("network video test pass completed. data: ", data); + + if (data.reason == "unreachable") { + logger.debug("video network test: unreachable (STUN issue or similar)") + attempt.reason = data.reason; + testFinishedVideo(); + } + else if (data.reason == "no-transmit") { + logger.debug("video network test: no-transmit (STUN issue or similar)"); + attempt.reason = data.reason; + testFinishedVideo(); + } + else if (data.reason == "internal_error") { + // oops + logger.debug("video network test: internal_error (client had a unexpected problem)"); + attempt.reason = data.reason; + testFinishedVideo(); + } + else if (data.reason == "remote_peer_cant_test") { + // old client + logger.debug("video network test: remote_peer_cant_test (old client)") + attempt.reason = data.reason; + testFinishedVideo(); + } + else if (data.reason == "server_comm_timeout") { + logger.debug("video network test: server_comm_timeout (communication with server problem)") + attempt.reason = data.reason; + testFinishedVideo(); + } + else { + if (!data.downthroughput || !data.upthroughput) { + // we have to assume this is bad. just not a reason we know about in code + logger.debug("video network test: no test data (unknown issue? " + data.reason + ")") + attempt.reason = "invalid_response"; + testFinishedVideo(); + } + else { + // success... but we still have to verify if this data is within threshold + if (data.downthroughput < gon.ftue_packet_rate_treshold) { + logger.debug("video network test: downthroughput too low. downthroughput: " + data.downthroughput + ", threshold: " + gon.ftue_packet_rate_treshold); + refineTest(false); + } + else if (data.upthroughput < gon.ftue_packet_rate_treshold) { + logger.debug("video network test: upthroughput too low. upthroughput: " + data.upthroughput + ", threshold: " + gon.ftue_packet_rate_treshold); + refineTest(false); + } + else { + // true success. we can accept this score + logger.debug("vido network test: success") + refineTest(true); + } + } + } + } + + } + + function networkTestTimeoutAudio(data) { + if(!isAttemptingPass()) { + logger.error("networkTestTimeout: already completed the audio test pass. indicates backend sent > 1 final event"); return; } @@ -836,10 +1215,26 @@ logger.warn("network timeout when testing latency test: " + data); - var attempt = getCurrentAttempt(); + var attempt = getCurrentAttemptAudio(); attempt.reason = 'timeout'; attempt.backend_data = data; - testFinished(); + testFinishedAudio(); + } + + function networkTestTimeoutVideo(data) { + if(!isAttemptingPass()) { + logger.error("networkTestTimeout: already completed the video test pass. indicates backend sent > 1 final event"); + return; + } + + clearBackendGuard(); + + logger.warn("network timeout when testing latency test: " + data); + + var attempt = getCurrentAttemptVideo(); + attempt.reason = 'timeout'; + attempt.backend_data = data; + testFinishedVideo(); } function hasScoredNetworkSuccessfully() { @@ -847,7 +1242,7 @@ } function configureStartButton() { - if (scoring) { + if (audioScoring || videoScoring) { $startNetworkTestBtn.text('NETWORK TEST RUNNING...').removeClass('button-orange').addClass('button-grey') } else { @@ -856,11 +1251,11 @@ } function initializeNextButtonState() { - $dialog.setNextState(hasScoredNetworkSuccessfully() && !scoring); + $dialog.setNextState(hasScoredNetworkSuccessfully() && !audioScoring && !videoScoring); } function initializeBackButtonState() { - $dialog.setBackState(!scoring); + $dialog.setBackState(!audioScoring && !videoScoring); } function beforeHide() { @@ -886,23 +1281,38 @@ if ($startNetworkTestBtn.length == 0) throw 'no start network test button found in network-test' $testResults = $step.find('.network-test-results'); - $testScore = $step.find('.network-test-score'); + $testScoreAudio = $step.find('.network-test-score-audio'); + $scoredClientsAudio = $testScoreAudio.find('.scored-clients'); + $testScoreVideo = $step.find('.network-test-score-video'); + $scoredClientsVideo = $testScoreVideo.find('.scored-clients'); $testText = $step.find('.network-test-text'); + $inProgressText = $step.find('.in-progress') + $audioResultText = $step.find('.audio-result') + $videoResultText = $step.find('.video-result') $scoringBar = $step.find('.scoring-bar'); $goodMarker = $step.find('.good-marker'); $goodLine = $step.find('.good-line'); $currentScore = $step.find('.current-score'); - $scoredClients = $step.find('.scored-clients'); + $subscore = $step.find('.subscore'); $watchVideo = $step.find('.watch-video'); + $container = $step.find('.network-test'); + if(inGearWizard) { + $container.attr('data-mode', 'gear-wizard') + } + else { + $container.attr('data-mode', 'standalone') + } $startNetworkTestBtn.on('click', function () { forever = false; prepareNetworkTest(); + return false; }); if(context.JK.currentUserAdmin) { $foreverNetworkTestBtn.on('click', function() { forever = true; prepareNetworkTest(); + return false; }).show(); } @@ -920,10 +1330,25 @@ primePumpTimeout(data) }; context.JK.HandleNetworkTestSuccessForGearWizard = function (data) { - networkTestComplete(data) + if(audioScoring) { + networkTestCompleteAudio(data); + } + else if(videoScoring) { + networkTestCompleteVideo(data); + } + else { + logger.warn("unknown state in HandleNetworkTestSuccessForGearWizard"); + } + }; // pin to global for bridge callback context.JK.HandleNetworkTestTimeoutForGearWizard = function (data) { - networkTestTimeout(data) + if(audioScoring) { + networkTestTimeoutAudio(data); + } + else if(videoScoring) { + networkTestTimeoutVideo(data); + } + }; // pin to global for bridge callback } else { @@ -934,17 +1359,30 @@ primePumpTimeout(data) }; context.JK.HandleNetworkTestSuccessForDialog = function (data) { - networkTestComplete(data) + if(audioScoring) { + networkTestCompleteAudio(data); + } + else if(videoScoring) { + networkTestCompleteVideo(data); + } + else { + logger.warn("unknown state in HandleNetworkTestSuccessForDialog"); + } }; // pin to global for bridge callback context.JK.HandleNetworkTestTimeoutForDialog = function (data) { - networkTestTimeout(data) + if(audioScoring) { + networkTestTimeoutAudio(data); + } + else if(videoScoring) { + networkTestTimeoutVideo(data); + } }; // pin to global for bridge callback } } this.isScoring = function () { - return scoring; + return audioScoring || videoScoring; }; this.hasScoredNetworkSuccessfully = hasScoredNetworkSuccessfully; this.initialize = initialize; diff --git a/web/app/assets/javascripts/paginator.js b/web/app/assets/javascripts/paginator.js index a25be9696..b263eb422 100644 --- a/web/app/assets/javascripts/paginator.js +++ b/web/app/assets/javascripts/paginator.js @@ -18,7 +18,7 @@ * @param onPageSelected when a new page is selected. receives one argument; the page number. * the function should return a deferred object (whats returned by $.ajax), and that response has to have a 'total-entries' header set */ - create:function(totalEntries, perPage, currentPage, onPageSelected) { + create:function(totalEntries, perPage, currentPage, onPageSelected, maxPages) { if(this.$templatePaginator === null) { this.$templatePaginator = $('#template-paginator') @@ -100,6 +100,13 @@ var pages = calculatePages(totalEntries, perPage); + + if(maxPages) { + if((totalEntries / perPage) > maxPages) { + pages = calculatePages(maxPages * perPage, perPage); + } + } + var options = { pages: pages, currentPage: currentPage }; diff --git a/web/app/assets/javascripts/profile_utils.js b/web/app/assets/javascripts/profile_utils.js index c25035936..8629a738c 100644 --- a/web/app/assets/javascripts/profile_utils.js +++ b/web/app/assets/javascripts/profile_utils.js @@ -97,6 +97,17 @@ return list; } + + profileUtils.getGenreIds = function(genres) { + var list = [] + + for (var i=0; i < genres.length; i++) { + list.push(genres[i].genre_id); + } + + return list; + } + // the server stores money in cents; display it as such profileUtils.normalizeMoneyForDisplay = function(serverValue) { if (!serverValue || serverValue==="") { diff --git a/web/app/assets/javascripts/react-components.js b/web/app/assets/javascripts/react-components.js index a3f79c86e..118242dda 100644 --- a/web/app/assets/javascripts/react-components.js +++ b/web/app/assets/javascripts/react-components.js @@ -1,7 +1,10 @@ +//= require react-input-autosize +//= require react-select //= require_directory ./react-components/helpers //= require_directory ./react-components/actions //= require ./react-components/stores/AppStore //= require ./react-components/stores/RecordingStore +//= require ./react-components/stores/VideoStore //= require ./react-components/stores/SessionStore //= require ./react-components/stores/MixerStore //= require ./react-components/stores/JamTrackStore @@ -10,7 +13,9 @@ //= require ./react-components/stores/SessionMyTracksStore //= require ./react-components/stores/SessionOtherTracksStore //= require ./react-components/stores/SessionMediaTracksStore +//= require ./react-components/stores/PlatformStore //= require_directory ./react-components/stores //= require_directory ./react-components/mixins //= require_directory ./react-components -//= require_directory ./react-components/landing \ No newline at end of file + +//= require_directory ./react-components/landing diff --git a/web/app/assets/javascripts/react-components/JamTrackAutoComplete.js.jsx.coffee b/web/app/assets/javascripts/react-components/JamTrackAutoComplete.js.jsx.coffee new file mode 100644 index 000000000..f1bf96cc4 --- /dev/null +++ b/web/app/assets/javascripts/react-components/JamTrackAutoComplete.js.jsx.coffee @@ -0,0 +1,111 @@ +context = window +MIX_MODES = context.JK.MIX_MODES + + +@JamTrackAutoComplete = React.createClass({ + + EVENTS: context.JK.EVENTS + rest: context.JK.Rest() + logger: context.JK.logger + + + render: () -> + + window.JamTrackSearchInput = '' unless window.JamTrackSearchInput? # can't pass null to react-select + + searchValue = if @state.search == 'SEPARATOR' then '' else window.JamTrackSearchInput + + ` + + + + + + + + +
    +

    what are jamtracks?

    +
    +
    + JamTracks are the best way to play along with your favorite music! Unlike traditional backing tracks, JamTracks are professionally mastered, complete multitrack recordings, with fully isolated tracks for each part of the master mix. Used with the free JamKazam app & Internet service, you can: +
    +
      +
    • Solo just the part you want to play in order to hear and learn it
    • +
    • Mute just the part you want to play and play along with the rest
    • +
    • Slow down playback to practice without changing the pitch
    • +
    • Change the song key by raising or lowering pitch in half steps
    • +
    • Make audio recordings and share them via Facebook or URL
    • +
    • Make video recordings and share them via YouTube
    • +
    • And even go online to play with others live & in sync
    • +
    + + + +
    +
    + + ` + + + componentDidMount: () -> + $root = $(@getDOMNode()) + + search: (searchType, searchData) -> + context.JamTrackActions.requestSearch(searchType, searchData) + + searchByString: (e) -> + e.preventDefault() + + context.JamTrackActions.requestSearch('user-input', window.JamTrackSearchInput) + + searchByFilter: (e) -> + e.preventDefault() + + $root = $(@getDOMNode()) + genre = $root.find('select.genre-list').val() + instrument = $root.find('select.instrument-list').val() + context.JamTrackActions.requestFilter(genre, instrument) + + afterShow: (data) -> + + if context.JK.currentUserId + @app.user().done(@onUser) + else + @onUser({free_jamtrack: context.JK.currentUserFreeJamTrack}) + + beforeShow: () -> + @setState({user: null}) + + onUser:(user) -> + @setState({user: user}) + + # Get artist names and build links + #@rest.getJamTrackArtists({group_artist: true, per_page:100}) + #.done(this.buildArtistLinks) + #.fail(this.handleFailure) + + # Bind links to action that will open the jam_tracks list view filtered to given artist_name: + # artist_name + #@bindArtistLinks() + + + onAppInit: (@app) -> + @rest = context.JK.Rest() + @client = context.jamClient + @logger = context.JK.logger + @screen = null + @noFreeJamTrack = null + @freeJamTrack = null + @bandList = null + @noBandsFound = null + + screenBindings = + 'beforeShow': @beforeShow + 'afterShow': @afterShow + + @app.bindScreen('jamtrack', screenBindings) + + @screen = $('#jamtrackLanding') + @noFreeJamTrack = @screen.find('.no-free-jamtrack') + @freeJamTrack = @screen.find('.free-jamtrack') + @bandList = @screen.find('#band_list') + @noBandsFound = @screen.find('#no_bands_found') + + $root = $(@getDOMNode()) + context.JK.GenreSelectorHelper.render2($root.find('select.genre-list')) + + @instrumentSelector = new context.JK.InstrumentSelector(@app) + @instrumentSelector.initialize(false, true) + @instrumentSelector.renderDropdown($root.find('select.instrument-list')) +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/JamTrackPreview.js.jsx.coffee b/web/app/assets/javascripts/react-components/JamTrackPreview.js.jsx.coffee new file mode 100644 index 000000000..9adcfc249 --- /dev/null +++ b/web/app/assets/javascripts/react-components/JamTrackPreview.js.jsx.coffee @@ -0,0 +1,191 @@ +context = window +ReactCSSTransitionGroup = React.addons.CSSTransitionGroup + +@JamTrackPreview = React.createClass({ + + mixins: [Reflux.listenTo(@AppStore, "onAppInit")] + EVENTS: context.JK.EVENTS + logger: context.JK.logger + propTypes: { options: React.PropTypes.object } + + getDefaultProps: () -> + { options: {master_shows_duration: false, color: 'gray', add_line_break: false, preload_master: false}} + + getInitialState: () -> + { loaded: false, loading: false, playing: false, no_audio: false } + + render: () -> + playButtonClasses = { "play-button": true, disabled: @state.no_audio} + playButtonClasses[@props.options.color] = @props.options.color? + playButtonClasses = classNames(playButtonClasses) + + stopButtonClasses = { "stop-button": true, disabled: @state.no_audio } + stopButtonClasses[@props.options.color] = @props.options.color? + stopButtonClasses = classNames(stopButtonClasses) + + partClasses = {part: true} + partClasses['adds-line-break'] = true if @props.options.master_adds_line_break + partClasses = classNames(partClasses) + + if @state.playing + activeButton = `` + else + activeButton = `` + + loaders = [] + if @props.jamTrackTrack.track_type == 'Track' + loading_text = '20 second preview loading' + else + loading_text = 'preview loading' + + + if @state.loading + loaders.push `
    {loading_text} +
    +
    ` + + + `
    +
    + {activeButton} +
    + +
    {this.state.instrumentDescription}
    +
    {this.state.part}
    + + {loaders} + +
    ` + + + componentWillMount: () -> + instrumentId = null + instrumentDescription = '?' + if @props.jamTrackTrack.track_type == 'Track' + if @props.jamTrackTrack.instrument + instrumentId = @props.jamTrackTrack.instrument.id + instrumentDescription = @props.jamTrackTrack.instrument.description + else + instrumentId = 'other' + instrumentDescription= 'Master Mix' + + instrumentSrc = context.JK.getInstrumentIcon24(instrumentId) + + part = '' + + if @props.jamTrackTrack.track_type == 'Track' + part = "(#{@props.jamTrackTrack.part})" if @props.jamTrackTrack.part? && @props.jamTrackTrack.part != instrumentDescription + + else + if @props.options.master_adds_line_break + part = '"' + @props.jamTrack.name + '"' + ' by ' + @props.jamTrack.original_artist + else + if @props.options.master_shows_duration + duration = 'entire song' + if @props.jamTrack.duration + duration = "#{context.JK.prettyPrintSeconds(@props.jamTrack.duration)}" + part = duration + else + part = @props.jamTrack.name + ' by ' + @props.jamTrack.original_artist + + part = "(#{part})" unless part? + part = '' unless part? + + urls = null + no_audio = null + + if @props.jamTrackTrack.preview_mp3_url? + + urls = [@props.jamTrackTrack.preview_mp3_url] + if @props.jamTrackTrack.preview_ogg_url? + urls.push(@props.jamTrackTrack.preview_ogg_url) + urls = urls + + no_audio = false + else + no_audio = true + + + @setState({ + instrumentId: instrumentId, + instrumentDescription: instrumentDescription, + instrumentSrc: instrumentSrc, + part: part + urls: urls, + no_audio: no_audio}) + + componentDidMount: () -> + $root = $(@getDOMNode()); + + if @props.options.preload_master && @props.jamTrackTrack.track_type == 'Master' && !@state.no_audio + @sound = new Howl({ + src: @state.urls, + autoplay: false, + loop: false, + volume: 1.0, + preload: true, + onload: @onHowlerLoad + onend: @onHowlerEnd}) + + componentWillUnmount: () -> + @sound.unload() if @sound? + + removeNowPlaying: () -> + context.JamTrackPreviewActions.stoppedPlaying(this) + + onHowlerEnd: () -> + @logger.debug("on end") + @removeNowPlaying() + @setState(playing: false) + + onHowlerLoad: () -> + @setState(loaded: true, loading: false) + + play: (e) -> + if e? + e.stopPropagation() + e.preventDefault() + + $root = $(@getDOMNode()) + $root.triggerHandler(@EVENTS.PREVIEW_PLAYED) + + $playButton = $root.find('.play-button') + if @state.no_audio + context.JK.prodBubble($playButton, 'There is no preview available for this track.', {}, {duration:2000}) + else + unless @sound? + + @sound = new Howl({ + src: @state.urls, + autoplay: false, + loop: false, + volume: 1.0, + preload: true, + onload: @onHowlerLoad + onend: @onHowlerEnd}) + + + @logger.debug("play issued for jam track preview") + @sound.play() + context.JamTrackPreviewActions.startedPlaying(this) + @setState({playing: true, loading: !@state.loaded}) + + issueStop: () -> + @logger.debug("pause issued for jam track preview") + @sound.pause() if @sound? # stop does not actually stop in windows client + @setState({playing: false}) + + stop: (e) -> + if e? + e.stopPropagation() + e.preventDefault() + + if @state.no_audio + context.JK.helpBubble(@playButton, 'There is no preview available for this track.', {}, {duration:2000}) + else + @issueStop() + @removeNowPlaying() + + return false + +}) diff --git a/web/app/assets/javascripts/react-components/JamTrackSearchScreen.js.jsx.coffee b/web/app/assets/javascripts/react-components/JamTrackSearchScreen.js.jsx.coffee new file mode 100644 index 000000000..04b8fcb6b --- /dev/null +++ b/web/app/assets/javascripts/react-components/JamTrackSearchScreen.js.jsx.coffee @@ -0,0 +1,539 @@ +context = window +MIX_MODES = context.JK.MIX_MODES + + +@JamTrackSearchScreen = React.createClass({ + + mixins: [Reflux.listenTo(@AppStore,"onAppInit")] + + LIMIT: 10 + instrument_logo_map: context.JK.getInstrumentIconMap24() + input: null + MAX_ARTIST_SHOW: 3 + + filterOption:() -> + true + + render: () -> + + searchText = if @state.first_search then 'SEARCH' else 'SEARCH AGAIN' + + uiJamTracks = [] + for jamtrack in @state.jamtracks + trackRow = context._.clone(jamtrack) + trackRow.track_cnt = jamtrack.tracks.length + trackRow.tracks = [] + + # if an instrument is selected by the user, then re-order any jam tracks with a matching instrument to the top + + ###instrument = @instrument.val() + if instrument? + jamtrack.tracks.sort((a, b) => + aWeight = @computeWeight(a, instrument) + bWeight = @computeWeight(b, instrument) + return aWeight - bWeight + ) +### + for track in jamtrack.tracks + trackRow.tracks.push(track) + if track.track_type=='Master' + track.instrument_desc = "Master" + else + inst = '../assets/content/icon_instrument_default24.png' + if track.instrument? + if track.instrument.id in @instrument_logo_map + inst = @instrument_logo_map[track.instrument.id].asset + track.instrument_desc = track.instrument.description + track.instrument_url = inst + + if track.part != '' + track.instrument_desc += ' (' + track.part + ')' + + trackRow.free_state = if @state.is_free then 'free' else 'non-free' + + trackRow.is_free = @state.is_free + + uiJamTracks.push trackRow + + artists = [] + artistsShown = 0 + for artist in @state.artists + + if @state.show_all_artists || artistsShown < @MAX_ARTIST_SHOW + artists.push `
    ` + + artistsShown += 1 + + + artists.push `
    No matching artists
    ` if artists.length == 0 + + if !@state.show_all_artists && @state.artists.length > @MAX_ARTIST_SHOW + artists.push `
    show all
    ` + else if @state.show_all_artists + artists.push `
    hide artists
    ` + + jamtracks = [] + + + for jamtrack in uiJamTracks + + jamtrackPricesClasses = { "jamtrack-price" : true } + jamtrackPricesClasses[jamtrack.free_state] = true + jamtrackPricesClasses = classNames(jamtrackPricesClasses) + + tracks = [] + for track in jamtrack.tracks + tracks.push `
    +
    + +
    +
    +
    ` + + actionBtn = null + if jamtrack.is_free + actionBtn = ` GET IT FREE!` + else if jamtrack.purchased + actionBtn = `PURCHASED` + else if jamtrack.added_cart + actionBtn = `ALREADY IN CART` + else + actionBtn = `ADD TO CART` + + availabilityNotice = null + if jamtrack.sales_region==context.JK.AVAILABILITY_US + availabilityNotice = + `
    + This JamTrack available only to US customers.      + why? +
    ` + + jamtracks.push ` + +
    "{jamtrack.name}"
    +
    by {jamtrack.original_artist}
    +
    +
    Songwriters:
    +
    {jamtrack.songwriter}
    +
    Publishers:
    +
    {jamtrack.publisher}
    +
    Genres:
    +
    {jamtrack.genres.join(', ')}
    +
    Version:
    +
    {jamtrack.recording_type}
    + + +
    +
    + show all tracks +
    +
    + {tracks} + + +
    +
    +
    $ {jamtrack.price}
    + {actionBtn} + {availabilityNotice} +
    +
    + + + ` + + #jamtracks.push `
    No matching JamTracks
    ` if jamtracks.length == 0 + + searchClasses = classNames({ + "button-orange" : true, + "search-btn" : true, + "disabled" : @state.searching + }) + + artistSection = null + jamTracksSection = null + + if @state.type == 'user-input' + if @state.searching + jamtracksHeader = "searching..." + else + jamtracksHeader = "search results: #{@state.count} jamtracks" + + + + else if @state.type == 'artist-select' + jamtracksHeader = "search results: jamtracks for artist \"#{@state.artist}\"" + else if @state.type == 'song-select' + jamtracksHeader = "search results: jamtrack \"#{@state.song}\"" + else + throw "unknown search type #{@state.type}" + + if !@state.first_search + + + # only show the artists links if the user typed the results + if @state.type == 'user-input' + artistSection = + `
    +

    search results: artists

    +
    + {artists} +
    +
    ` + + jamTracksSection = + `
    +

    {jamtracksHeader} back to jamtracks home

    + + + + + + + + + + + {jamtracks} + +
    JAMTRACKTRACKS INCLUDED / PREVIEWSHOP
    +
    No more JamTracks
    +
    ` + + options = {} + + + searchValue = if @state.search == 'SEPARATOR' then '' else window.JamTrackSearchInput + + `
    +
    + + +
    +
    + {artistSection} + {jamTracksSection} +
    +
    ` + + + + clearResults:() -> + @setState({currentPage: 0, next: null, show_all_artists: false, artists:[], jamtracks:[], type: 'user-input', searching:false, artist: null, song:null, is_free: context.JK.currentUserFreeJamTrack, first_search: true}) + + + getInitialState: () -> + {search: '', type: 'user-input', artists:[], jamtracks:[], show_all_artists: false, currentPage: 0, next: null, searching: false, first_search: true, count: 0, is_free: context.JK.currentUserFreeJamTrack} + + onSelectChange: (val) -> + #@logger.debug("CHANGE #{val}") + + return false unless val? + + search_type + if val.indexOf('ARTIST=') == 0 + search_type = 'artist-select' + artist = val['ARTIST='.length..-1] + @search(search_type, artist) + else if val.indexOf('SONG=') == 0 + search_type = 'song-select' + song = val['SONG='.length..-1] + @search(search_type, song) + else + @logger.debug("user selected separator") + # this is to signal to the component that the separator was selected, and it has code in render to negate the selection + setTimeout((() => + @setState({search:val}) + ), 1) + + return false + + + onSelectBlur: (e) -> + + #@logger.debug("blur time") + + #@search() + + showAllArtists: () -> + @setState({show_all_artists: true}) + + hideExtraArtists: () -> + @setState({show_all_artists: false}) + + + defaultQuery:(extra) -> + query = + per_page: @LIMIT + page: @state.currentPage + 1 + sort_by: 'jamtrack' + if @state.next + query.since = @state.next + $.extend(query, extra) + + + + userSearch: (e) -> + e.preventDefault() + @search('user-input', window.JamTrackSearchInput) + + search: (search_type, input) -> + return if @state.searching + return unless input? + + window.JamTrackSearchInput = input + + $root = $(@getDOMNode()) + # disable scroll watching now that we've started a new search + #@logger.debug("disabling infinite scroll") + $root.find('.content-body-scroller').off('scroll') + $root.find('.end-of-jamtrack-list').hide() + + artistSearch = {limit:100} + if search_type == 'artist-select' + # the user wants to see just artists matching thes exact name + artistSearch.artist = input + else + # the user wants to see anything sort of matching input + artistSearch.artist_search = input + + if input? + @rest.getJamTrackArtists(artistSearch) + .done((response) => + @setState({artists:response.artists}) + + # we have to make sure the query starts from page 1, and no 'next' from previous causes a 'since' to show up + query = @defaultQuery({page: 1}) + delete query.since + + @logger.debug("Search type", search_type) + if search_type == 'artist-select' + query.artist = input # works like exact match + else if search_type == 'song-select' + query.song = input # works as exact match + + else + query.search = input # works with tsv + @rest.getJamTracks(query) + .done((response) => + @setState({jamtracks: response.jamtracks, next: response.next, searching: false, first_search: false, currentPage: 1, count: response.count}) + ) + .fail(() => + @app.notifyServerError jqXHR, 'Search Unavailable' + @setState({searching: false, first_search: false}) + ) + ) + .fail(() => + @app.notifyServerError jqXHR, 'Search Unavailable' + @setState({searching: false, first_search: false}) + ) + + @setState({currentPage: 0, next: null, artists: [], jamtracks:[], searching: true, artist: input, song: input, type: search_type, search:input, count:0}) + + getOptions: (input, callback) => + + #@logger.debug("getOptions input #{input}", this) + + # sigh. ugly global + window.JamTrackSearchInput = input + + if !input? || input.length == 0 + callback(null, {options: [], complete: false}) + return + + @rest.autocompleteJamTracks({match:input, limit:5}) + .done((autocomplete) => + + options = [] + for artist in autocomplete.artists + options.push { value: "ARTIST=#{artist.original_artist}", label: "Artist: #{artist.original_artist}" } + + if options.length > 0 && autocomplete.songs.length > 0 + options.push { value: 'SEPARATOR', label: "---------------"} + + for jamtrack in autocomplete.songs + options.push { value: "SONG=#{jamtrack.name}", label: "Song: #{jamtrack.name}" } + + callback(null, {options: options, complete: false}) + ) + + artistNavSelected: (e) -> + e.preventDefault() + + @search('artist-select', $(e.target).attr('data-artist')) + + componentDidMount: () -> + #@logger.debug("componentDidMount") + + componentDidUpdate: ( ) -> + $root = $(this.getDOMNode()) + $scroller = $root.find('.content-body-scroller') + + for jamTrack in @state.jamtracks + jamtrackElement = $root.find("tbody .jamtrack-record[data-jamtrack-id=\"#{jamTrack.id}\"]") + alreadyRegistered = jamtrackElement.data('registered') + + unless alreadyRegistered + jamtrackElement.data('jamTrack', jamTrack) + jamtrackElement.data('registered', true) + jamtrackElement.data('expanded', true) + + @handleExpanded(jamtrackElement) + @registerEvents(jamtrackElement) + + + if @state.next == null + $scroller = $root.find('.content-body-scroller') + # if we less results than asked for, end searching + #$scroller.infinitescroll 'pause' + #@logger.debug("disabling infinite scroll") + $scroller.off('scroll') + if @state.currentPage == 1 and @state.jamtracks.length == 0 + $root.find('.end-of-jamtrack-list').text('No JamTracks found matching your search').show() + @logger.debug("JamTrackSearch: empty search") + else if @state.currentPage > 0 + @logger.debug("end of search") + $noMoreJamtracks = $root.find('.end-of-jamtrack-list').text('No more JamTracks').show() + # there are bugs with infinitescroll not removing the 'loading'. + # it's most noticeable at the end of the list, so whack all such entries + else + @registerInfiniteScroll($scroller) + + + + registerInfiniteScroll:($scroller) -> + @logger.debug("registering infinite scroll") + $scroller.off('scroll') + $scroller.on('scroll', () => + + # be sure to not fire off many refreshes when user hits the bottom + return if @refreshing + + if $scroller.scrollTop() + $scroller.innerHeight() + 100 >= $scroller[0].scrollHeight + $scroller.append('
    ... Loading more JamTracks ...
    ') + @refreshing = true + @setState({searching: true}) + @logger.debug("refreshing more jamtracks for infinite scroll") + @rest.getJamTracks(@defaultQuery({search:@state.search})) + .done((json) => + @setState({jamtracks: @state.jamtracks.concat(json.jamtracks), next: json.next, first_search: false, currentPage: @state.currentPage + 1, count: json.count}) + ) + .always(() => + $scroller.find('.infinite-scroll-loader-2').remove() + @refreshing = false + @setState({searching: false}) + ) + ) + + playJamtrack:(e) -> + e.preventDefault() + + addToCartJamtrack:(e) -> + e.preventDefault() + $target = $(e.target) + params = id: $target.attr('data-jamtrack-id') + isFree = $(e.target).is('.is_free') + + @rest.addJamtrackToShoppingCart(params).done((response) => + if(isFree) + if context.JK.currentUserId? + context.JK.currentUserFreeJamTrack = true # make sure the user sees no more free notices + context.location = '/client#/redeemComplete' + else + # now make a rest call to buy it + context.location = '/client#/redeemSignup' + + else + context.location = '/client#/shoppingCart' + + ).fail(() => @app.ajaxError) + + licenseUSWhy:(e) -> + e.preventDefault() + @app.layout.showDialog 'jamtrack-availability-dialog' + + registerEvents:($parent) -> + $parent.find('.play-button').on 'click', @playJamtrack + $parent.find('.jamtrack-add-cart').on 'click', @addToCartJamtrack + $parent.find('.license-us-why').on 'click', @licenseUSWhy + $parent.find('.jamtrack-detail-btn').on 'click', @toggleExpanded + + toggleExpanded:(e) -> + e.preventDefault() + jamtrackRecord = $(e.target).parents('.jamtrack-record') + @handleExpanded(jamtrackRecord) + + handleExpanded:(trackElement) -> + jamTrack = trackElement.data('jamTrack') + expanded = trackElement.data('expanded') + expand = !expanded + trackElement.data('expanded', expand) + + detailArrow = trackElement.find('.jamtrack-detail-btn') + + if expand + trackElement.find('.extra').removeClass('hidden') + detailArrow.html('hide tracks ') + for track in jamTrack.tracks + trackElement.find("[data-jamtrack-track-id='#{track.id}']").removeClass('hidden') + else + trackElement.find('.extra').addClass('hidden') + detailArrow.html('show all tracks ') + count = 0 + for track in jamTrack.tracks + if count < 6 + trackElement.find("[data-jamtrack-track-id='#{track.id}']").removeClass('hidden') + else + trackElement.find("[data-jamtrack-track-id='#{track.id}']").addClass('hidden') + count++ + + + afterShow: (data) -> + + @setFilterFromURL() + + setFilterFromURL:() -> + + performSearch = false + if $.QueryString['artist']? + performSearch = true + @search('artist-select', $.QueryString['artist']) + else if $.QueryString['song']? + performSearch = true + @search('song-select', $.QueryString['song']) + else if $.QueryString['search']? + performSearch = true + @search('user-input', $.QueryString['search']) + else + # check if someone has requested a search for us as we transition to this screen + search = context.JamTrackStore.checkRequestedSearch() + if search? + performSearch = true + @search(search.searchType, search.searchData) + + if performSearch + if window.history.replaceState #ie9 proofing + window.history.replaceState({}, "", "/client#/jamtrack/search") + + + beforeShow: () -> + @setState({is_free: context.JK.currentUserFreeJamTrack}) + if !@state.first_search + @search(@state.type, window.JamTrackSearchInput) + + + + onAppInit: (@app) -> + + window.JamTrackSearchInput = '' # need to be not null; otherwise react-select chokes + @EVENTS = context.JK.EVENTS + @rest = context.JK.Rest() + @logger = context.JK.logger + + + screenBindings = + 'beforeShow': @beforeShow + 'afterShow': @afterShow + + @app.bindScreen('jamtrack/search', screenBindings) +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/MediaControls.js.jsx.coffee b/web/app/assets/javascripts/react-components/MediaControls.js.jsx.coffee index 024f11103..34899a6cf 100644 --- a/web/app/assets/javascripts/react-components/MediaControls.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/MediaControls.js.jsx.coffee @@ -7,6 +7,12 @@ mixins = [] # this check ensures we attempt to listen if this component is created in a popup reactContext = if window.opener? then window.opener else window +# make sure this is actually us opening the window, not someone else (by checking for MixerStore) +if window.opener? + try + m = window.opener.MixerStore + catch e + reactContext = window MixerStore = reactContext.MixerStore MixerActions = reactContext.MixerActions diff --git a/web/app/assets/javascripts/react-components/PopupConfigureVideoGear.js.jsx.coffee b/web/app/assets/javascripts/react-components/PopupConfigureVideoGear.js.jsx.coffee new file mode 100644 index 000000000..7ffd1e641 --- /dev/null +++ b/web/app/assets/javascripts/react-components/PopupConfigureVideoGear.js.jsx.coffee @@ -0,0 +1,108 @@ +context = window +logger = context.JK.logger + +mixins = [] + + +# make sure this is actually us opening the window, not someone else (by checking for MixerStore) + +accessOpener = false +if window.opener? + try + m = window.opener.MixerStore + accessOpener = true + catch e + + +if accessOpener + VideoActions = window.opener.VideoActions + VideoStore = window.opener.VideoStore + +#mixins.push(Reflux.listenTo(VideoStore, 'onVideoStateChanged')) + + +@PopupConfigureVideoGear = React.createClass({ + + mixins: mixins + logger: context.JK.logger + + render: () -> + `
    +
    +
    +

    video is not configured

    +
    + If you might like to use video in sessions, please select a webcam to use, and a video resolution and frame rate to capture. Then click the TEST WEBCAM button to verify that you see video from your webcam properly. In sessions, you can choose to turn video on or off any time. +
    +
    + +
    + +
    + +
    +
    + Important Note +
    +
    + You can update your video configuration any time in your Account settings, or in the menus of the video window while in a session. +
    +
    +
    +
    + +
    + + + + CLOSE +
    +
    ` + + close: () -> + $root = jQuery(this.getDOMNode()) + $dontShow = $root.find('input[name="dont_show"]') + VideoActions.configureVideoPopupClosed($dontShow.is(':checked')) + window.close() + + windowUnloaded: () -> + + $root = jQuery(this.getDOMNode()) + $dontShow = $root.find('input[name="dont_show"]') + + VideoActions.howToUseVideoPopupClosed($dontShow.is(':checked')) + + componentDidMount: () -> + $(window).unload(@windowUnloaded) + + $root = jQuery(this.getDOMNode()) + + $dontShow = $root.find('input[name="dont_show"]') + context.JK.checkbox($dontShow) + + @resizeWindow() + + # this is necessary due to whatever the client's rendering behavior is. + setTimeout(@resizeWindow, 300) + + componentDidUpdate: () -> + @resizeWindow() + + resizeWindow: () => + $container = $('#minimal-container') + width = $container.width() + height = $container.height() + + # there is 20px or so of unused space at the top of the page. can't figure out why it's there. (above #minimal-container) + mysteryTopMargin = 20 + + # deal with chrome in real browsers + offset = (window.outerHeight - window.innerHeight) + mysteryTopMargin + + # handle native client chrome that the above outer-inner doesn't catch + #if navigator.userAgent.indexOf('JamKazam') > -1 + + #offset += 25 + + window.resizeTo(width, height + offset) +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/PopupHowToUseVideo.js.jsx.coffee b/web/app/assets/javascripts/react-components/PopupHowToUseVideo.js.jsx.coffee new file mode 100644 index 000000000..792fee209 --- /dev/null +++ b/web/app/assets/javascripts/react-components/PopupHowToUseVideo.js.jsx.coffee @@ -0,0 +1,105 @@ +context = window +logger = context.JK.logger + +mixins = [] + + +# make sure this is actually us opening the window, not someone else (by checking for MixerStore) + +accessOpener = false +if window.opener? + try + m = window.opener.MixerStore + accessOpener = true + catch e + + +if accessOpener + VideoActions = window.opener.VideoActions + VideoStore = window.opener.VideoStore + +#mixins.push(Reflux.listenTo(VideoStore, 'onVideoStateChanged')) + + @PopupHowToUseVideo = React.createClass({ + + render: () -> + `
    +
    + + +
    +
    + Important Note +
    +
    + You can start and stop your webcam at any time by navigating to the Webcam menu of the video window and selecting Start/Stop Webcam. +
    +
    +
    + +
    + +
    + +
    + CLOSE +
    +
    ` + + close: () -> + $root = jQuery(this.getDOMNode()) + $dontShow = $root.find('input[name="dont_show"]') + VideoActions.howToUseVideoPopupClosed($dontShow.is(':checked')) + window.close() + + startVideo: (e) -> + e.preventDefault + VideoActions.startVideo() + + windowUnloaded: () -> + + $root = jQuery(this.getDOMNode()) + $dontShow = $root.find('input[name="dont_show"]') + + VideoActions.howToUseVideoPopupClosed($dontShow.is(':checked')) + + componentDidMount: () -> + $(window).unload(@windowUnloaded) + + $root = jQuery(this.getDOMNode()) + + $dontShow = $root.find('input[name="dont_show"]') + context.JK.checkbox($dontShow) + + @resizeWindow() + + # this is necessary due to whatever the client's rendering behavior is. + setTimeout(@resizeWindow, 300) + + componentDidUpdate: () -> + @resizeWindow() + + resizeWindow: () => + $container = $('#minimal-container') + width = $container.width() + height = $container.height() + + # there is 20px or so of unused space at the top of the page. can't figure out why it's there. (above #minimal-container) + mysteryTopMargin = 20 + + # deal with chrome in real browsers + offset = (window.outerHeight - window.innerHeight) + mysteryTopMargin + + # handle native client chrome that the above outer-inner doesn't catch + #if navigator.userAgent.indexOf('JamKazam') > -1 + + #offset += 25 + + window.resizeTo(width, height + offset) +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/PopupMediaControls.js.jsx.coffee b/web/app/assets/javascripts/react-components/PopupMediaControls.js.jsx.coffee index 235fc1fd2..37673bf87 100644 --- a/web/app/assets/javascripts/react-components/PopupMediaControls.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/PopupMediaControls.js.jsx.coffee @@ -3,7 +3,18 @@ logger = context.JK.logger mixins = [] + +# make sure this is actually us opening the window, not someone else (by checking for MixerStore) + +accessOpener = false if window.opener? + try + m = window.opener.MixerStore + accessOpener = true + catch e + + +if accessOpener SessionActions = window.opener.SessionActions MediaPlaybackStore = window.opener.MediaPlaybackStore MixerActions = window.opener.MixerActions diff --git a/web/app/assets/javascripts/react-components/PopupRecordingStartStop.js.jsx.coffee b/web/app/assets/javascripts/react-components/PopupRecordingStartStop.js.jsx.coffee index 7cb29df6e..d13f69dc6 100644 --- a/web/app/assets/javascripts/react-components/PopupRecordingStartStop.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/PopupRecordingStartStop.js.jsx.coffee @@ -2,8 +2,17 @@ context = window mixins = [] -# this check ensures we attempt to listen if this component is created in a popup -if window.opener +# make sure this is actually us opening the window, not someone else (by checking for MixerStore) + +accessOpener = false +if window.opener? + try + m = window.opener.MixerStore + accessOpener = true + catch e + + +if accessOpener mixins.push(Reflux.listenTo(window.opener.RecordingStore,"onRecordingStateChanged")) @PopupRecordingStartStop = React.createClass({ diff --git a/web/app/assets/javascripts/react-components/SessionMediaTracks.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionMediaTracks.js.jsx.coffee index d4d50a8ae..12af57803 100644 --- a/web/app/assets/javascripts/react-components/SessionMediaTracks.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/SessionMediaTracks.js.jsx.coffee @@ -203,6 +203,8 @@ ChannelGroupIds = context.JK.ChannelGroupIds contents = null mediaTracks = [] + mediaTracks.push `
    ` + if this.state.downloadJamTrack? closeOptions = `
    @@ -210,7 +212,6 @@ ChannelGroupIds = context.JK.ChannelGroupIds Close JamTrack -
    ` contents = closeOptions diff --git a/web/app/assets/javascripts/react-components/SessionMyTracks.js.jsx.coffee b/web/app/assets/javascripts/react-components/SessionMyTracks.js.jsx.coffee index 80b91a403..97323d6f8 100644 --- a/web/app/assets/javascripts/react-components/SessionMyTracks.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/SessionMyTracks.js.jsx.coffee @@ -31,8 +31,7 @@ ReactCSSTransitionGroup = React.addons.CSSTransitionGroup;

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

    ` diff --git a/web/app/assets/javascripts/react-components/actions/JamTrackActions.js.coffee b/web/app/assets/javascripts/react-components/actions/JamTrackActions.js.coffee index adcb9467a..0c122c4c6 100644 --- a/web/app/assets/javascripts/react-components/actions/JamTrackActions.js.coffee +++ b/web/app/assets/javascripts/react-components/actions/JamTrackActions.js.coffee @@ -3,5 +3,7 @@ context = window @JamTrackActions = Reflux.createActions({ open: {} close: {} + requestSearch: {} + requestFilter: {} }) diff --git a/web/app/assets/javascripts/react-components/actions/JamTrackPreviewActions.js.coffee b/web/app/assets/javascripts/react-components/actions/JamTrackPreviewActions.js.coffee new file mode 100644 index 000000000..16ef5706f --- /dev/null +++ b/web/app/assets/javascripts/react-components/actions/JamTrackPreviewActions.js.coffee @@ -0,0 +1,8 @@ +context = window + +@JamTrackPreviewActions = Reflux.createActions({ + startedPlaying: {} + stoppedPlaying: {} + screenChange: {} +}) + diff --git a/web/app/assets/javascripts/react-components/actions/VideoActions.js.coffee b/web/app/assets/javascripts/react-components/actions/VideoActions.js.coffee new file mode 100644 index 000000000..90bc00bbd --- /dev/null +++ b/web/app/assets/javascripts/react-components/actions/VideoActions.js.coffee @@ -0,0 +1,18 @@ +context = window + +@VideoActions = Reflux.createActions({ + refresh: {} + stopVideo: {} + startVideo: {} + setVideoEncodeResolution: {} + setSendFrameRate: {} + selectDevice: {} + videoWindowOpened : {} + videoWindowClosed : {} + howToUseVideoPopupClosed: {} + toggleVideo: {} + testVideo: {} + configureVideoPopupClosed: {} + checkPromptConfigureVideo: {} + setVideoEnabled: {} +}) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/helpers/MixerHelper.js.coffee b/web/app/assets/javascripts/react-components/helpers/MixerHelper.js.coffee index 7089f1af6..4639dc8e2 100644 --- a/web/app/assets/javascripts/react-components/helpers/MixerHelper.js.coffee +++ b/web/app/assets/javascripts/react-components/helpers/MixerHelper.js.coffee @@ -606,7 +606,7 @@ MIX_MODES = context.JK.MIX_MODES; # sanity check if mixer && mixer.group_id != ChannelGroupIds.PeerAudioInputMusicGroup - logger.error("found remote mixer that was not of groupID: PeerAudioInputMusicGroup", mixer) + logger.warn("master: found remote mixer that was not of groupID: PeerAudioInputMusicGroup", client_id, track.client_track_id, mixer) vuMixer = mixer muteMixer = mixer @@ -618,7 +618,7 @@ MIX_MODES = context.JK.MIX_MODES; oppositeMixer = oppositeMixers[ChannelGroupIds.UserMusicInputGroup][0] if !oppositeMixer - logger.error("unable to find UserMusicInputGroup corresponding to PeerAudioInputMusicGroup mixer", mixer ) + logger.warn("unable to find UserMusicInputGroup corresponding to PeerAudioInputMusicGroup mixer", mixer ) when MIX_MODES.PERSONAL mixers = @groupedMixersForClientId(client_id, [ ChannelGroupIds.UserMusicInputGroup], {}, MIX_MODES.PERSONAL) @@ -632,9 +632,9 @@ MIX_MODES = context.JK.MIX_MODES; # now grab the PeerAudioInputMusicGroup in master mode to satisfy the 'opposite' mixer oppositeMixer = @getMixerByTrackId(track.client_track_id, MIX_MODES.MASTER) if !oppositeMixer - logger.debug("unable to find a PeerAudioInputMusicGroup master mixer matching a UserMusicInput", client_id, track.client_track_id) + logger.debug("personal: unable to find a PeerAudioInputMusicGroup master mixer matching a UserMusicInput", client_id, track.client_track_id) else if oppositeMixer.group_id != ChannelGroupIds.PeerAudioInputMusicGroup - logger.error("found remote mixer that was not of groupID: PeerAudioInputMusicGroup", mixer) + logger.error("personaol: found remote mixer that was not of groupID: PeerAudioInputMusicGroup", client_id, track.client_track_id, mixer) #vuMixer = oppositeMixer; # for personal mode, use the PeerAudioInputMusicGroup's VUs diff --git a/web/app/assets/javascripts/react-components/landing/InvidualJamTrackPage.js.jsx.coffee b/web/app/assets/javascripts/react-components/landing/IndividualJamTrackPage.js.jsx.coffee similarity index 84% rename from web/app/assets/javascripts/react-components/landing/InvidualJamTrackPage.js.jsx.coffee rename to web/app/assets/javascripts/react-components/landing/IndividualJamTrackPage.js.jsx.coffee index 809a273a6..b2764cddf 100644 --- a/web/app/assets/javascripts/react-components/landing/InvidualJamTrackPage.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/landing/IndividualJamTrackPage.js.jsx.coffee @@ -4,17 +4,19 @@ context = window watchVideo: (e) -> e.preventDefault() - window.open("/popups/youtube/player?id=askHvcCoNfw", 'What Are JamTracks?', 'scrollbars=yes,toolbar=no,status=no,height=282,width=500') + window.open("/popups/youtube/player?id=askHvcCoNfw", 'What Are JamTracks?', 'scrollbars=yes,toolbar=no,status=no,height=540,width=960') render: () -> header = null - if @props.band + if @props.instrument + header = "We Have #{@props.instrument_count} JamTracks With #{@props.instrument} Parts - Play Along With Your Favorites!" + else if @props.band header = "#{@props.jam_track.original_artist} Backing Tracks - Complete Multitracks" else if @props.generic? header = "Backing Tracks + Free Amazing App = Unmatched Experience" else - header = "#{@props.jam_track.name} Backing Track by #{@props.jam_track.original_artist}" + header = "\"#{@props.jam_track.name}\" Backing Track by #{@props.jam_track.original_artist}" `
    diff --git a/web/app/assets/javascripts/react-components/landing/JamTrackCta.js.jsx.coffee b/web/app/assets/javascripts/react-components/landing/JamTrackCta.js.jsx.coffee index de888990e..490751fd5 100644 --- a/web/app/assets/javascripts/react-components/landing/JamTrackCta.js.jsx.coffee +++ b/web/app/assets/javascripts/react-components/landing/JamTrackCta.js.jsx.coffee @@ -39,22 +39,48 @@ rest = context.JK.Rest() {processing: false} render: () -> - bandBrowseUrl = "/client?artist=#{this.props.jam_track.original_artist}#/jamtrackBrowse" - `` + + isFree = context.JK.currentUserFreeJamTrack + + + if isFree + img =`` + else + img =`` + + + if @props.instrument? + getFreeText = "Get \"#{this.props.jam_track.name}\" JamTrack Free Now" + instrumentBrowseUrl = "/client?instrument=#{this.props.instrument_id}#/jamtrack/filter" + + `` + else + bandBrowseUrl = "/client?artist=#{this.props.jam_track.original_artist}#/jamtrack/search" + + `` }) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/stores/JamTrackPreviewStore.js.coffee b/web/app/assets/javascripts/react-components/stores/JamTrackPreviewStore.js.coffee new file mode 100644 index 000000000..c56fa9446 --- /dev/null +++ b/web/app/assets/javascripts/react-components/stores/JamTrackPreviewStore.js.coffee @@ -0,0 +1,40 @@ +$ = jQuery +context = window +logger = context.JK.logger +rest = context.JK.Rest() +EVENTS = context.JK.EVENTS + + +JamTrackPreviewActions = @JamTrackPreviewActions + +@JamTrackPreviewStore = Reflux.createStore( + { + listenables: JamTrackPreviewActions + logger: context.JK.logger + nowPlaying: [] + + init: -> + # Register with the app store to get @app + this.listenTo(context.AppStore, this.onAppInit) + + onAppInit: (app) -> + @app = app + + onStartedPlaying: (preview) -> + + for playingSound in @nowPlaying + playingSound.issueStop() + @nowPlaying = [] + @nowPlaying.push(preview) + + onStoppedPlaying: (preview) -> + @nowPlaying.splice(preview) + if @nowPlaying.length > 0 + @logger.warn("multiple jamtrack previews playing") + + onScreenChange: () -> + for playingSound in @nowPlaying + playingSound.issueStop() + @nowPlaying = [] + } +) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/stores/JamTrackStore.js.coffee b/web/app/assets/javascripts/react-components/stores/JamTrackStore.js.coffee index b06d128f5..53002cf8f 100644 --- a/web/app/assets/javascripts/react-components/stores/JamTrackStore.js.coffee +++ b/web/app/assets/javascripts/react-components/stores/JamTrackStore.js.coffee @@ -11,6 +11,8 @@ JamTrackActions = @JamTrackActions { listenables: JamTrackActions jamTrack: null + requestedSearch: null + requestedFilter: null init: -> # Register with the app store to get @app @@ -30,5 +32,26 @@ JamTrackActions = @JamTrackActions onClose: () -> @jamTrack = null this.trigger(@jamTrack) + + onRequestSearch:(searchType, searchData) -> + @requestedSearch = {searchType: searchType, searchData: searchData} + window.location.href = '/client#/jamtrack/search' + + # needed by the JamTrackSearchScreen + checkRequestedSearch:() -> + requested = @requestedSearch + @requestedSearch = null + requested + + onRequestFilter:(genre, instrument) -> + @requestedFilter = {genre: genre, instrument:instrument} + window.location.href = '/client#/jamtrack/filter' + + # needed by the JamTrackSearchScreen + checkRequestedFilter:() -> + requested = @requestedFilter + @requestedFilter = null + requested + } ) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/stores/PlatformStore.js.coffee b/web/app/assets/javascripts/react-components/stores/PlatformStore.js.coffee new file mode 100644 index 000000000..b7ea92db4 --- /dev/null +++ b/web/app/assets/javascripts/react-components/stores/PlatformStore.js.coffee @@ -0,0 +1,21 @@ +$ = jQuery +context = window +logger = context.JK.logger + +@PlatformStore = Reflux.createStore( + { + logger: context.JK.logger + os: null + + init: -> + this.listenTo(context.AppStore, this.onAppInit) + + onAppInit: (@app) -> + @os = context.jamClient.GetOSAsString() + this.trigger({os: @os, isWindows: @isWindows}) + + isWindows: -> + @os == 'Win32' + + } +) \ No newline at end of file diff --git a/web/app/assets/javascripts/react-components/stores/SessionStore.js.coffee b/web/app/assets/javascripts/react-components/stores/SessionStore.js.coffee index 65d06211b..f56828030 100644 --- a/web/app/assets/javascripts/react-components/stores/SessionStore.js.coffee +++ b/web/app/assets/javascripts/react-components/stores/SessionStore.js.coffee @@ -9,6 +9,7 @@ JamTrackActions = @JamTrackActions SessionActions = @SessionActions RecordingActions = @RecordingActions NotificationActions = @NotificationActions +VideoActions = @VideoActions @SessionStore = Reflux.createStore( { @@ -42,6 +43,7 @@ NotificationActions = @NotificationActions # Register with the app store to get @app this.listenTo(context.AppStore, this.onAppInit) this.listenTo(context.RecordingStore, this.onRecordingChanged) + this.listenTo(context.VideoStore, this.onVideoChanged) onAppInit: (@app) -> @@ -51,10 +53,8 @@ NotificationActions = @NotificationActions RecordingActions.initModel(@recordingModel) @helper = new context.SessionHelper(@app, @currentSession, @participantsEverSeen, @isRecording, @downloadingJamTrack) - if gon.global.video_available && gon.global.video_available!="none" && context.JK.WebcamViewer? - @webcamViewer = new context.JK.WebcamViewer() - @webcamViewer.init($("#create-session-layout")) - @webcamViewer.setVideoOff() + + onVideoChanged: (@videoState) -> issueChange: () -> @helper = new context.SessionHelper(@app, @currentSession, @participantsEverSeen, @isRecording, @downloadingJamTrack) @@ -181,8 +181,16 @@ NotificationActions = @NotificationActions @issueChange() onToggleSessionVideo: () -> - logger.debug("toggle session video") - @webcamViewer.toggleWebcam() if @webcamViewer? + + if @videoState?.videoEnabled + logger.debug("toggle session video") + VideoActions.toggleVideo() + else + context.JK.Banner.showAlert({ + title: "Video Is Disabled", + html: "To re-enable video, you must go your video settings in your account settings and enable video.", + }) + onAudioResync: () -> logger.debug("audio resyncing") @@ -558,15 +566,14 @@ NotificationActions = @NotificationActions shareDialog.initialize(context.JK.FacebookHelperInstance); # initialize webcamViewer - if gon.global.video_available && gon.global.video_available != "none" - @webcamViewer.beforeShow() + VideoActions.stopVideo(); # double-check that we are connected to the server via websocket return unless @ensureConnected() # just make double sure a previous session state is cleared out - @sessionEnded() + @sessionEnded(true) # update the session data to be empty @updateCurrentSession(null) @@ -973,8 +980,7 @@ NotificationActions = @NotificationActions logger.warn("no location specified in leaveSession action", behavior) window.location = '/client#/home' - if gon.global.video_available && gon.global.video_available != "none" - @webcamViewer.setVideoOff() + #VideoActions.stopVideo() @leaveSession() @@ -1019,7 +1025,7 @@ NotificationActions = @NotificationActions selfOpenedJamTracks: () -> @currentSession && (@currentSession.jam_track_initiator_id == context.JK.currentUserId) - sessionEnded: () -> + sessionEnded: (onJoin) -> # cleanup context.JK.JamServer.unregisterMessageCallback(context.JK.MessageType.SESSION_JOIN, @trackChanges); @@ -1053,7 +1059,7 @@ NotificationActions = @NotificationActions @controlsLockedForJamTrackRecording = false @openBackingTrack = null @downloadingJamTrack = false - @sessionUtils.setAutoOpenJamTrack(null) + @sessionUtils.setAutoOpenJamTrack(null) unless onJoin JamTrackActions.close() NotificationActions.sessionEnded() diff --git a/web/app/assets/javascripts/react-components/stores/VideoStore.js.coffee b/web/app/assets/javascripts/react-components/stores/VideoStore.js.coffee new file mode 100644 index 000000000..3cca5720e --- /dev/null +++ b/web/app/assets/javascripts/react-components/stores/VideoStore.js.coffee @@ -0,0 +1,253 @@ +$ = jQuery +context = window +logger = context.JK.logger +EVENTS = context.JK.EVENTS +NAMED_MESSAGES = context.JK.NAMED_MESSAGES + +VideoActions = @VideoActions + +BackendToFrontend = { + 1 : "CIF (352x288)", + 2 : "VGA (640x480)", + 3 : "4CIF (704x576)", + 4 : "1/2 720p HD (640x360)", + 5 : "720p HD (1280x720)", + 6 : "1080p HD (1920x1080)" +} + +BackendToFrontendFPS = { + + 0: 30, + 1: 24, + 2: 20, + 3: 15, + 4: 10 +} + +@VideoStore = Reflux.createStore( + { + listenables: VideoActions + logger: context.JK.logger + videoShared: false + videoOpen : false + state : null + everDisabled : false + + init: -> + this.listenTo(context.AppStore, this.onAppInit) + + onAppInit: (@app) -> + + + + # someone has requested us to refresh our config + onRefresh: -> + + # don't do any check if this is a client with no video enabled + return unless context.jamClient.FTUECurrentSelectedVideoDevice? + + videoEnabled = context.jamClient.FTUEGetVideoShareEnable() + + @videoEnabled = videoEnabled + + if videoEnabled + currentDevice = context.jamClient.FTUECurrentSelectedVideoDevice() + deviceNames = context.jamClient.FTUEGetVideoCaptureDeviceNames() + #deviceCaps = context.jamClient.FTUEGetVideoCaptureDeviceCapabilities() + currentResolution = context.jamClient.GetCurrentVideoResolution() + currentFrameRate = context.jamClient.GetCurrentVideoFrameRate() + encodeResolutions = context.jamClient.FTUEGetAvailableEncodeVideoResolutions() + frameRates = context.jamClient.FTUEGetSendFrameRates() + + autoSelect = false + if currentResolution == 0 + @logger.warn("current resolution not specified; defaulting to VGA") + autoSelect = true + currentResolution = 2 + + if currentFrameRate == 0 + autoSelect = true + @logger.warn("current frame rate not specified; defaulting to 30") + currentFrameRate = 30 + else + # backend accepts 10,20,30 etc for FPS, but returns an indexed value (1, 2, 3). + convertedFrameRate = BackendToFrontendFPS[currentFrameRate] + @logger.debug("translating FPS: backend numeric #{currentFrameRate} to #{convertedFrameRate}") + currentFrameRate = convertedFrameRate + + # backend needs to be same as frontend + if autoSelect + context.jamClient.FTUESetVideoEncodeResolution(currentResolution) + context.jamClient.FTUESetSendFrameRates(currentFrameRate) + else + @everDisabled = true + # don't talk to the backend when video is disabled; avoiding crashes + currentDevice = null + deviceNames = {} + currentResolution: 0 + currentFrameRate: 0 + encodeResolutions: {} + frameRates: {} + + + #deviceCaps: deviceCaps, + + @state = { + currentDevice: currentDevice, + deviceNames: deviceNames, + currentResolution: currentResolution, + currentFrameRate: currentFrameRate, + encodeResolutions: encodeResolutions, + frameRates: frameRates, + videoShared: @videoShared + videoOpen: @videoOpen, + videoEnabled: videoEnabled, + everDisabled: @everDisabled + } + this.trigger(@state) + + onSetVideoEnabled: (enable) -> + + return unless context.jamClient.FTUESetVideoShareEnable? + + context.jamClient.FTUESetVideoShareEnable(enable) + + # keep state in sync + @state.videoEnabled = enable + @onRefresh() + + onStartVideo: -> + return unless context.jamClient.SessStartVideoSharing? + + if @howtoWindow? + @howtoWindow.close() + @howtoWindow = null + #else # TESTING + # @howtoWindow = window.open("/popups/how-to-use-video", 'How to Use Video', 'scrollbars=yes,toolbar=no,status=no,height=315,width=320') + + @logger.debug("SessStartVideoSharing()") + context.jamClient.SessStartVideoSharing(0) + @videoShared = true + + @state.videoShared = @videoShared + this.trigger(@state) + + onStopVideo: -> + if @videoShared + @logger.debug("SessStopVideoSharing()") + context.jamClient.SessStopVideoSharing() + @videoShared = false + @state.videoShared = @videoShared + this.trigger(@state) + + onTestVideo: () -> + + return unless context.jamClient.testVideoRender? + result = context.jamClient.testVideoRender() + + if !result + @app.layout.notify({title: 'Unable to initialize video window', text: "Please contact support@jamkazam.com"}) + + onToggleVideo: () -> + if @videoShared + @onStopVideo() + else + @onStartVideo() + + onSetVideoEncodeResolution: (resolution) -> + @logger.debug("set capture resolution: #{resolution}") + context.jamClient.FTUESetVideoEncodeResolution(resolution) + @state.currentResolution = resolution + this.trigger(@state) + + onSetSendFrameRate: (frameRates) -> + @logger.debug("set capture frame rate: #{frameRates}") + context.jamClient.FTUESetSendFrameRates(frameRates) + @state.currentFrameRate = frameRates + this.trigger(@state) + + onSelectDevice: (device, caps) -> + + # don't do anything if no video capabilities + return unless context.jamClient.FTUESelectVideoCaptureDevice? + + result = context.jamClient.FTUESelectVideoCaptureDevice(device, caps) + if(!result) + @logger.error("onSelectDevice failed with device #{device}") + @app.layout.notify({title: 'Unable to select webcam', text: "Please try reconnecting webcam."}) + else + @state.currentDevice = context.jamClient.FTUECurrentSelectedVideoDevice(); + this.trigger(@state) + + onVideoWindowOpened: () -> + @onRefresh() unless @state? + + @logger.debug("in session? #{context.SessionStore.inSession()}, currentDevice? #{@state?.currentDevice?}, videoShared? #{@videoShared}") + + if context.SessionStore.inSession() && @state.currentDevice? && Object.keys(@state.currentDevice).length > 0 && !@videoShared + context.JK.ModUtils.shouldShow(NAMED_MESSAGES.HOWTO_USE_VIDEO_NOSHOW).done((shouldShow) => + @logger.debug("checking if user has 'should show' on video howto: #{shouldShow}") + if shouldShow + @howtoWindow = window.open("/popups/how-to-use-video", 'How to Use Video', 'scrollbars=yes,toolbar=no,status=no,height=315,width=320') + ) + + #@howtoWindo.ParentRecordingStore = context.RecordingStore + #@howtoWindo.ParentIsRecording = @recordingModel.isRecording() + + @videoOpen = true + @state.videoOpen = @videoOpen + this.trigger(@state) + + onVideoWindowClosed: () -> + @onRefresh() unless @state? + + if @howtoWindow? + @howtoWindow.close() + @howtoWindow = null + + @videoOpen = false + @state.videoOpen = @videoOpen + @videoShared = false + @state.videoShared = @videoShared + this.trigger(@state) + + onHowToUseVideoPopupClosed: (dontShow) -> + if (dontShow) + @logger.debug("requesting that user no longer see how-to-use-video") + context.JK.ModUtils.updateNoShow(NAMED_MESSAGES.HOWTO_USE_VIDEO_NOSHOW); + + logger.debug("how-to-use-video popup closed") + @howtoWindow = null + + onConfigureVideoPopupClosed: (dontShow) -> + if (dontShow) + @logger.debug("requesting that user no longer see configure-video") + context.JK.ModUtils.updateNoShow(NAMED_MESSAGES.CONFIGURE_VIDEO_NOSHOW); + + logger.debug("configure-video popup closed") + @configureWindow = null + + # if the user passes all the safeguards, let's see if we should get them to configure video + onCheckPromptConfigureVideo: () -> + # don't do any check if this is a client with no video enabled + return unless context.jamClient.FTUECurrentSelectedVideoDevice? + + @onRefresh() unless @state? + + @logger.debug("checkPromptConfigureVideo", @state.currentDevice, @state.deviceNames) + + # if no device configured and this is the native client and if you have at least 1 video + # currentDevice, from the backend, is '{'':''}' in the case of no device configured. But we also check for an empty object, or null object. + if (!@state.currentDevice? || Object.keys(@state.currentDevice).length == 0 || (Object.keys(@state.currentDevice).length == 1 && @state.currentDevice[''] == '')) && gon?.isNativeClient && Object.keys(@state.deviceNames).length > 0 + # and if they haven't said stop bothering me about this + context.JK.ModUtils.shouldShow(NAMED_MESSAGES.CONFIGURE_VIDEO_NOSHOW).done((shouldShow) => + @logger.debug("checking if user has 'should show' on video config: #{shouldShow}") + if shouldShow + @configureWindow = window.open("/popups/configure-video", 'Configure Video', 'scrollbars=yes,toolbar=no,status=no,height=395,width=444') + ) + + isVideoEnabled:() -> + return @videoEnabled + + } +) diff --git a/web/app/assets/javascripts/react-components/stores/WebcamViewer.js.jsx.coffee b/web/app/assets/javascripts/react-components/stores/WebcamViewer.js.jsx.coffee new file mode 100644 index 000000000..2ddf95e33 --- /dev/null +++ b/web/app/assets/javascripts/react-components/stores/WebcamViewer.js.jsx.coffee @@ -0,0 +1,405 @@ +context = window +logger = context.JK.logger + +reactContext = if window.opener? then window.opener else window +# make sure this is actually us opening the window, not someone else (by checking for MixerStore) +if window.opener? + try + m = window.opener.MixerStore + catch e + reactContext = window + +VideoStore = reactContext.VideoStore +VideoActions = reactContext.VideoActions +PlatformStore = reactContext.PlatformStore + +ALERT_NAMES = context.JK.ALERT_NAMES; + +BackendToFrontend = { + 1 : "CIF (352x288)", + 2 : "VGA (640x480)", + 3 : "4CIF (704x576)", + 4 : "1/2 720p HD (640x360)", + 5 : "720p HD (1280x720)", + 6 : "1080p HD (1920x1080)" +} + +BackendNumericToBackendString = { + 1 : "CIF (352X288)", + 2 : "VGA (640X480)", + 3 : "4CIF (704X576)", + 4 : "1/2WHD (640X360)", + 5 : "WHD (1280X720)", + 6 : "FHD (1920x1080)" +} + + +BackendToFrontendFPS = { + 1: 30, + 2: 24, + 3: 20, + 4: 15, + 5: 10 +} +FrontendToBackend = {} +for key, value of BackendToFrontend + FrontendToBackend[value] = key + +mixins = [] +mixins.push(Reflux.listenTo(VideoStore, 'onVideoStateChanged')) + +@WebcamViewer = React.createClass({ + + mixins: mixins + logger: context.JK.logger + visible: false + + getInitialState: () -> + { + currentDevice: null + deviceNames: {} + deviceCaps: null + currentResolution: 0 + currentFrameRate: 0 + encodeResolutions: {} + frameRates: {} + rescanning: false + } + + onVideoStateChanged: (changes) -> + @setState(changes) + + render: () -> + + if @props.showBackBtn + backBtn = `BACK` + + selectedDevice = this.selectedDeviceName(@state) + + # build list of webcams + + webcams = [] + noneSelected = selectedDevice == null || selectedDevice.length == 0 + + # the backend does not allow setting no video camera. So if a webcam is selected, prevent un-selecting + if noneSelected + webcams.push `` + + context._.each @state.deviceNames, (deviceName, deviceGuid) -> + selected = deviceGuid == selectedDevice + webcams.push `` + + noWebcams = Object.keys(@state.deviceNames).length == 0 + + # build list of capture resolutions + + captureResolutions = [] + # load current settings from backend + currentResolution = @state.currentResolution + currentFrameRate = @state.currentFrameRate + + # protect against non-video clients pointed at video-enabled server from getting into a session + resolutions = @state.encodeResolutions + frames = @state.frameRates + context._.each resolutions, (resolution, resolutionKey, obj) => + + #{1: "CIF (352X288)", 2: "VGA (640X480)", 3: "4CIF (704X576)", 4: "1/2WHD (640X360)", 5: "WHD (1280X720)", 6: "FHD (1920x1080)"} + context._.each frames, (frame, key, obj) => + + frontendResolution = BackendToFrontend[resolutionKey] + + @logger.error("unknown resolution! #{resolution}", BackendToFrontend) unless frontendResolution + + value = "#{resolutionKey}|#{frame}" + text = "#{frontendResolution} at #{frame} fps" + + selected = currentResolution + '|' + currentFrameRate == value + + captureResolutions.push `` + + + testBtnClassNames = {'button-orange' : true, 'webcam-test-btn' : true} + if noWebcams + if PlatformStore.isWindows() + testBtnClassNames.disabled = !@state.videoEnabled + testBtnClasses = classNames(testBtnClassNames) + testBtn = `TEST VIDEO` + else + testBtn = null + else if @state.videoShared + testBtnClassNames.disabled = !@state.videoEnabled + testBtnClasses = classNames(testBtnClassNames) + testBtn = `STOP WEBCAM` + else + testBtnClassNames.disabled = !@state.videoEnabled || noneSelected + testBtnClasses = classNames(testBtnClassNames) + testBtn = `TEST WEBCAM` + + if @state.rescanning + rescanning = + ` + + CHECKING GEAR + ` + + if @props.show_header + if noWebcams + if PlatformStore.isWindows() + testVideoHelpText = `The TEST VIDEO button will open the JamKazam video window to verify that receiving video works on your system.` + header = `
    +

    video gear:

    +
    + JamKazam does not detect any webcams. You will not be able to send video, but you can still receive it from others. {testVideoHelpText} +
    +
    ` + else + header = + `
    +

    video gear:

    +
    + Select webcam to use for video in sessions. Verify that you see video from webcam in the external application window (it may be behind this window). +
    +
    ` + + if @state.videoEnabled + disableVideoBtnText = "DISABLE VIDEO" + else + disableVideoBtnText = "ENABLE VIDEO" + + if @props.show_disable || !@state.videoEnabled || @state.everDisabled + + if @state.videoEnabled + disableHelpBtn = `[?]` + + disableBtnClasses = classNames({'button-grey' : true, 'disable-video' : true, 'disabled' : @state.videoShared}) + disableVideo = + `
    + {disableVideoBtnText} + {disableHelpBtn} +
    ` + + `
    + {header} +
    +

    select webcam:

    +
    + +
    +

    select video capture resolution & frame rate:

    +
    + + [?] +
    +
    + {backBtn} + {testBtn} +
    + {rescanning} +
    + {disableVideo} +
    ` + + componentDidMount: () -> + + if @props.isVisible + @beforeShow() + + $root = $(@getDOMNode()) + + $videoSettingsHelp = $root.find('.ftue-video-settings-help') + context.JK.helpBubble($videoSettingsHelp, 'ftue-video-settings', {}, {width:300}) if $videoSettingsHelp.length > 0 + $videoSettingsHelp.click(false) + + $videoDisableHelp = $root.find('.ftue-video-disable-help') + context.JK.helpBubble($videoDisableHelp, 'ftue-video-disable', {}, {width:300}) if $videoDisableHelp.length > 0 + $videoDisableHelp.click(false) + + + componentWillUpdate: (nextProps, nextState) -> + # protect against non-video clients pointed at video-enabled server from getting into a session + + @logger.debug("webcam devices", nextState.deviceNames, @state.deviceNames) + + if !@initialScan? + @initialScan = true + else if @visible + @findChangedWebcams(nextState.deviceNames, @state.deviceNames) + + componentWillReceiveProps:(nextProps) -> + if nextProps.isVisible + @beforeShow() + else + @beforeHide() + + beforeShow:() -> + + @visible = true + VideoActions.refresh() + VideoActions.stopVideo() + + context.JK.onBackendEvent(ALERT_NAMES.USB_CONNECTED, 'webcam-viewer', @onUsbDeviceConnected); + context.JK.onBackendEvent(ALERT_NAMES.USB_DISCONNECTED, 'webcam-viewer', @onUsbDeviceDisconnected); + + beforeHide: () -> + + @visible = false + context.JK.offBackendEvent(ALERT_NAMES.USB_CONNECTED, 'webcam-viewer', @onUsbDeviceConnected); + context.JK.offBackendEvent(ALERT_NAMES.USB_DISCONNECTED, 'webcam-viewer', @onUsbDeviceDisconnected); + + if @rescanTimeout? + clearTimeout(@rescanTimeout) + @rescanTimeout = null + + @setVideoOff() + + + onUsbDeviceConnected: () -> + # don't handle USB events when minimized + #return if !context.jamClient.IsFrontendVisible() + + logger.debug("USB device connected") + + @scheduleRescanSystem(3000) + + onUsbDeviceDisconnected:() -> + # don't handle USB events when minimized + #return if !context.jamClient.IsFrontendVisible() + + logger.debug("USB device disconnected") + + @scheduleRescanSystem(3000) + + scheduleRescanSystem: (time) -> + if @rescanTimeout? + clearTimeout(@rescanTimeout) + @rescanTimeout = null + + @setState({rescanning: true}) + @rescanTimeout = setTimeout(() => + @setState({rescanning: false}) + VideoActions.refresh() + , time) + + selectWebcam:(e) -> + e.preventDefault() + + device = $(e.target).val() + + VideoActions.selectDevice(device, {}) + + disableVideo: (e) -> + e.preventDefault() + + return if @state.videoShared + + if @state.videoEnabled + context.JK.Banner.showYesNo({ + title: "Disable Video?", + html: "You will not be able to send or receive video.", + yes: => + VideoActions.setVideoEnabled(false) + }) + else + VideoActions.setVideoEnabled(true) + + + updateBackend: (selectedResolution, selectedFps) -> + @logger.debug 'Selecting webcam resolution: ', selectedResolution + @logger.debug 'Selecting webcam fps: ', selectedFps + + VideoActions.setVideoEncodeResolution(selectedResolution) + VideoActions.setSendFrameRate(selectedFps) + + selectResolution:(e) -> + e.preventDefault() + + resolution = $(e.target).val() + @logger.debug 'new capture resolution selected: ' + resolution + + if resolution? + + bits = resolution.split('|') + selectedResolution = bits[0] + selectedFps = bits[1] + @updateBackend(selectedResolution, selectedFps) + + setVideoOff:() -> + VideoActions.stopVideo() + + back: () => + window.location = '/client#/account' + + toggleWebcam:(e) -> + e.preventDefault() + + return unless this.state.videoEnabled + + $toggleBtn = $(e.target) + + # we should only do this if no device is currently selected + $root = $(@getDOMNode()) + $select = $root.find('.webcam-select-container select') + if Object.keys(@state.deviceNames).length == 0 + + context.JK.Banner.showYesNo({ + yes_text: 'RUN TEST', + title: "Run Video Test?", + html: "A video window will show up with changing colors and shapes for 10 seconds. The test was successful if you were able to see the changing colors. Close the window once the colors and shapes stop changing.", + yes: => + VideoActions.testVideo() + }) + + else + device = $select.val() + #VideoActions.selectDevice(device, {}) + VideoActions.toggleVideo() + + #if this.isVideoShared() + # $toggleBtn.removeClass("selected") + # VideoActions.stopVideo() + # @setState({videoShared: false}) + #else + # $toggleBtn.addClass("selected") + # VideoActions.startVideo() + # @setState({videoShared: true}) + + selectedDeviceName:(state) -> + webcamName = null + # protect against non-video clients pointed at video-enabled server from getting into a session + webcam = state.currentDevice + @logger.debug("currently selected video device", webcam) + if (webcam? && Object.keys(webcam).length>0) + webcamName = Object.keys(webcam)[0] + + webcamName + + findChangedWebcams: (newList, oldList) -> + newKeys = Object.keys(newList) + oldKeys = Object.keys(oldList) + + webcamSelect = $(@getDOMNode()).find('.webcam-select-container select') + + if newKeys.length > oldKeys.length + for newKey in newKeys + if oldKeys.indexOf(newKey) == -1 + newWebcam = newList[newKey] + @logger.debug("new webcam found: " + newWebcam, newKey) + context.JK.prodBubble(webcamSelect, 'new-webcam-found', {name: newWebcam}, {positions:['right']}) + break + else if newKeys.length < oldKeys.length + for oldKey in oldKeys + if newKeys.indexOf(oldKey) == -1 + oldWebcam = oldList[oldKey] + @logger.debug("webcam no longer found: " + oldWebcam) + context.JK.prodBubble(webcamSelect, 'old-webcam-lost', {name: oldWebcam}, {positions:['right']}) + break + + + } +) + + diff --git a/web/app/assets/javascripts/recordingModel.js b/web/app/assets/javascripts/recordingModel.js index 0aec59c6a..aff392fba 100644 --- a/web/app/assets/javascripts/recordingModel.js +++ b/web/app/assets/javascripts/recordingModel.js @@ -95,7 +95,7 @@ // ask the backend to start the session. var groupedTracks = groupTracksToClient(recording); - jamClient.StartRecording(recording["id"], groupedTracks); + jamClient.StartRecording(recording["id"], groupedTracks, 0, false, 0); }) .fail(function(jqXHR) { var details = { clientId: app.clientId, reason: 'rest', detail: arguments, isRecording: false } diff --git a/web/app/assets/javascripts/redeem_complete.js b/web/app/assets/javascripts/redeem_complete.js index 6d5ec9a3d..1300e97f4 100644 --- a/web/app/assets/javascripts/redeem_complete.js +++ b/web/app/assets/javascripts/redeem_complete.js @@ -59,7 +59,7 @@ if(!checkoutUtils.hasOneFreeItemInShoppingCart(carts)) { // the user has multiple items in their shopping cart. They shouldn't be here. logger.error("invalid access of redeemComplete page") - window.location = '/client#/jamtrackBrowse' + window.location = '/client#/jamtrack/search' } else { // ok, we have one, free item. save it for @@ -226,7 +226,7 @@ $backBtn.on('click', function(e) { e.preventDefault(); - context.location = '/client#/jamtrackBrowse' + context.location = '/client#/jamtrack/search' }) } diff --git a/web/app/assets/javascripts/redeem_signup.js b/web/app/assets/javascripts/redeem_signup.js index e0785321f..d3d514258 100644 --- a/web/app/assets/javascripts/redeem_signup.js +++ b/web/app/assets/javascripts/redeem_signup.js @@ -27,11 +27,11 @@ var $signinLink = null; function beforeShow(data) { - renderLoggedInState(); + } function afterShow(data) { - + renderLoggedInState(); } @@ -67,13 +67,13 @@ if(carts.length == 0) { // nothing is in the user's shopping cart. They shouldn't be here. - logger.error("invalid access of redeemJamTrack page") - window.location = '/client#/jamtrackBrowse' + logger.error("invalid access of redeemJamTrack page; none") + window.location = '/client#/jamtrack/search' } else if(carts.length > 1) { // the user has multiple items in their shopping cart. They shouldn't be here. - logger.error("invalid access of redeemJamTrack page") - window.location = '/client#/jamtrackBrowse' + logger.error("invalid access of redeemJamTrack page; multiple") + window.location = '/client#/jamtrack/search' } else { var item = carts[0]; @@ -86,8 +86,8 @@ } else { // the user has a non-free, single item in their basket. They shouldn't be here. - logger.error("invalid access of redeemJamTrack page") - window.location = '/client#/jamtrackBrowse' + logger.error("invalid access of redeemJamTrack page, non-free/item") + window.location = '/client#/jamtrack/search' } } diff --git a/web/app/assets/javascripts/searchResults.js b/web/app/assets/javascripts/searchResults.js index 5b1b2f36a..12a127f1c 100644 --- a/web/app/assets/javascripts/searchResults.js +++ b/web/app/assets/javascripts/searchResults.js @@ -224,7 +224,7 @@ var instrumentLogoHtml = ''; if (instruments !== undefined) { for (var i=0; i < instruments.length; i++) { - var inst = '../assets/content/icon_instrument_default24.png'; + 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].asset; instrumentLogoHtml += ' '; diff --git a/web/app/assets/javascripts/session.js b/web/app/assets/javascripts/session.js index 7fb4f7058..5f5ef170c 100644 --- a/web/app/assets/javascripts/session.js +++ b/web/app/assets/javascripts/session.js @@ -3294,8 +3294,8 @@ $voiceChat = $screen.find('#voice-chat'); $tracksHolder = $screen.find('#tracks') if(gon.global.video_available && gon.global.video_available!="none") { - webcamViewer.init($("#create-session-layout .webcam-container")) - webcamViewer.setVideoOff() + //webcamViewer.init($("#create-session-layout .webcam-container"), false) + //webcamViewer.setVideoOff() } events(); diff --git a/web/app/assets/javascripts/session_utils.js b/web/app/assets/javascripts/session_utils.js index 9d54f2493..7e8de1acb 100644 --- a/web/app/assets/javascripts/session_utils.js +++ b/web/app/assets/javascripts/session_utils.js @@ -22,7 +22,7 @@ }; sessionUtils.setAutoOpenJamTrack = function(jamTrack) { - logger.debug("setting auto-load jamtrack") + logger.debug("setting auto-load jamtrack", jamTrack) autoOpenJamTrack = jamTrack; } @@ -30,6 +30,7 @@ sessionUtils.grabAutoOpenJamTrack = function() { var jamTrack = autoOpenJamTrack; autoOpenJamTrack = null; + logger.debug("grabbing auto-load jamtrack", jamTrack) return jamTrack; } diff --git a/web/app/assets/javascripts/utils.js b/web/app/assets/javascripts/utils.js index 3d8359137..fed60372a 100644 --- a/web/app/assets/javascripts/utils.js +++ b/web/app/assets/javascripts/utils.js @@ -1081,8 +1081,11 @@ }); } - context.JK.dropdown = function ($select) { + context.JK.dropdown = function ($select, options) { + var opts = options || {} + + opts = $.extend({}, {nativeTouch: !(context.jamClient && context.jamClient.IsNativeClient()) && gon.global.env != "test", cutOff: 7}, opts) $select.each(function (index) { var $item = $(this); @@ -1090,7 +1093,7 @@ // if this has already been initialized, re-init it so it picks up any new $item.easyDropDown('destroy') } - $item.easyDropDown({nativeTouch: !(context.jamClient && context.jamClient.IsNativeClient()) && gon.global.env != "test", cutOff: 7}); + $item.easyDropDown(opts); }) } diff --git a/web/app/assets/javascripts/web/individual_jamtrack.js b/web/app/assets/javascripts/web/individual_jamtrack.js index 6b0b8fd78..fc20baa68 100644 --- a/web/app/assets/javascripts/web/individual_jamtrack.js +++ b/web/app/assets/javascripts/web/individual_jamtrack.js @@ -12,10 +12,33 @@ var $jamTracksButton = null; var $ctaJamTracksButton = null; + function computeWeight (jam_track_track, instrument) { + var weight; + + if (jam_track_track.track_type == 'Master') { + weight = 0 + } + else if (jam_track_track.instrument.id == instrument) { + weight = 1 + jam_track_track.position + } + else { + weight = 10000 + jam_track_track.position + } + return weight; + } + function fetchJamTrack() { rest.getJamTrackWithArtistInfo({plan_code: gon.jam_track_plan_code}) .done(function (jam_track) { + if(gon.instrument_id) { + jam_track.tracks.sort(function(a, b) { + var aWeight = computeWeight(a, gon.instrument_id) + var bWeight = computeWeight(b, gon.instrument_id) + return aWeight - bWeight + }) + } + context._.each(jam_track.tracks, function (track) { var $element = $('
    ') diff --git a/web/app/assets/javascripts/web/individual_jamtrack_band_v1.js b/web/app/assets/javascripts/web/individual_jamtrack_band_v1.js index 22ceb52a3..25b5620b1 100644 --- a/web/app/assets/javascripts/web/individual_jamtrack_band_v1.js +++ b/web/app/assets/javascripts/web/individual_jamtrack_band_v1.js @@ -23,9 +23,9 @@ logger.debug("jam_track", jam_track) $jamtrack_band.text(jam_track.original_artist) - $jamTracksButton.attr('href', '/client?artist=' + jam_track.original_artist + '#/jamtrackBrowse') + $jamTracksButton.attr('href', '/client?artist=' + jam_track.original_artist + '#/jamtrack/search') $jamTracksButton.removeClass('hidden').text("Preview all " + jam_track.band_jam_track_count + " of our " + jam_track.original_artist + " JamTracks") - $ctaJamTracksButton.attr('href', '/client?artist=' + jam_track.original_artist + '#/jamtrackBrowse') + $ctaJamTracksButton.attr('href', '/client?artist=' + jam_track.original_artist + '#/jamtrack/searche') context._.each(jam_track.tracks, function (track) { diff --git a/web/app/assets/javascripts/web/individual_jamtrack_v1.js b/web/app/assets/javascripts/web/individual_jamtrack_v1.js index a9fc6ff38..c6d3e4dff 100644 --- a/web/app/assets/javascripts/web/individual_jamtrack_v1.js +++ b/web/app/assets/javascripts/web/individual_jamtrack_v1.js @@ -24,7 +24,7 @@ if(!gon.just_previews) { if (gon.generic) { $genericHeader.removeClass('hidden'); - $jamTracksButton.attr('href', '/client#/jamtrackBrowse') + $jamTracksButton.attr('href', '/client#/jamtrack/search') $jamTracksButton.removeClass('hidden').text("Check out all 100+ JamTracks") } @@ -32,9 +32,9 @@ $individualizedHeader.removeClass('hidden') $jamtrack_name.text('"' + jam_track.name + '"'); $jamtrack_band.text(jam_track.original_artist) - $jamTracksButton.attr('href', '/client?artist=' + jam_track.original_artist + '#/jamtrackBrowse') + $jamTracksButton.attr('href', '/client?artist=' + jam_track.original_artist + '#/jamtrack/search') $jamTracksButton.removeClass('hidden').text("Preview all " + jam_track.band_jam_track_count + " of our " + jam_track.original_artist + " JamTracks") - $ctaJamTracksButton.attr('href', '/client?artist=' + jam_track.original_artist + '#/jamtrackBrowse') + $ctaJamTracksButton.attr('href', '/client?artist=' + jam_track.original_artist + '#/jamtrack/search') } } diff --git a/web/app/assets/javascripts/web/tracking.js.coffee b/web/app/assets/javascripts/web/tracking.js.coffee index b3fb626f4..8186066de 100644 --- a/web/app/assets/javascripts/web/tracking.js.coffee +++ b/web/app/assets/javascripts/web/tracking.js.coffee @@ -10,25 +10,23 @@ class Tracking @rest = new context.JK.Rest(); adTrack: (app) => - utmSource = $.QueryString['utm_source'] - if utmSource == 'facebook-ads' || utmSource == 'google-ads' || utmSource == 'twitter-ads' || utmSource == 'affiliate' || utmSource == 'pr' - if !context.jamClient.IsNativeClient() - if context.JK.currentUserId? - app.user().done( (user) => - # relative to 1 day ago (24 * 60 * 60 * 1000) - if new Date(user.created_at).getTime() < new Date().getTime() - 86400000 - @logger.debug("existing user recorded") - context.JK.GA.virtualPageView('/landing/jamtracks/existing-user/'); - else - @logger.debug("new user recorded") - context.JK.GA.virtualPageView('/landing/jamtracks/new-user/') - ) - else - @logger.debug("new user recorded") - context.JK.GA.virtualPageView('/landing/jamtracks/new-user/') - else - @logger.debug("existing user recorded") - context.JK.GA.virtualPageView('/landing/jamtracks/existing-user/'); + if !context.jamClient.IsNativeClient() + if context.JK.currentUserId? + app.user().done( (user) => + # relative to 1 day ago (24 * 60 * 60 * 1000) + if new Date(user.created_at).getTime() < new Date().getTime() - 86400000 + @logger.debug("existing user recorded") + context.JK.GA.virtualPageView('/landing/jamtracks/existing-user/'); + else + @logger.debug("new user recorded") + context.JK.GA.virtualPageView('/landing/jamtracks/new-user/') + ) + else if $.cookie('jamkazam_user')? + @logger.debug("existing/logged out user") + context.JK.GA.virtualPageView('/landing/jamtracks/existing-user/') + else + @logger.debug("new user recorded") + context.JK.GA.virtualPageView('/landing/jamtracks/new-user/') jamtrackBrowseTrack: (app) => if context.JK.currentUserId? diff --git a/web/app/assets/javascripts/webcam_viewer.js.coffee b/web/app/assets/javascripts/webcam_viewer.js.coffee index a5538167a..be395557f 100644 --- a/web/app/assets/javascripts/webcam_viewer.js.coffee +++ b/web/app/assets/javascripts/webcam_viewer.js.coffee @@ -2,6 +2,40 @@ $ = jQuery context = window context.JK ||= {}; + +ALERT_NAMES = context.JK.ALERT_NAMES; + +BackendToFrontend = { + 1 : "CIF (352x288)", + 2 : "VGA (640x480)", + 3 : "4CIF (704x576)", + 4 : "1/2 720p HD (640x360)", + 5 : "720p HD (1280x720)", + 6 : "1080p HD (1920x1080)" +} + +BackendNumericToBackendString = { + 1 : "CIF (352X288)", + 2 : "VGA (640X480)", + 3 : "4CIF (704X576)", + 4 : "1/2WHD (640X360)", + 5 : "WHD (1280X720)", + 6 : "FHD (1920x1080)" +} + + +BackendToFrontendFPS = { + + 1: 30, + 2: 24, + 3: 20, + 4: 15, + 5: 10 +} +FrontendToBackend = {} +for key, value of BackendToFrontend + FrontendToBackend[value] = key + context.JK.WebcamViewer = class WebcamViewer constructor: (@root) -> @client = context.jamClient @@ -10,23 +44,34 @@ context.JK.WebcamViewer = class WebcamViewer @toggleBtn = null @webcamSelect = null @resolutionSelect = null - @videoShared=false - @resolution=null + @videoShared = false + @resolution = null + @videoSettingsHelp = null + @showBackBtn = false + @rescanTimeout = null + @lastDeviceList = null - init: (root) => - # the session usage of webcamViewer does not actually pass in anything - root = $() unless root? - - @root = root + init: (@root, @showBackButton) => @toggleBtn = @root.find(".webcam-test-btn") @webcamSelect = @root.find(".webcam-select-container select") @resolutionSelect = @root.find(".webcam-resolution-select-container select") - @webcamSelect.on("change", this.selectWebcam) + @videoSettingsHelp = @root.find('.ftue-video-settings-help') + @rescanningNotice = @root.find('.rescanning-notice') + @backBtn = @root.find('.back-btn') + @webcamSelect.on("change", @selectWebcam) @toggleBtn.on('click', @toggleWebcam) - @resolutionSelect.on("change", this.selectResolution) + @resolutionSelect.on("change", @selectResolution) + @backBtn.on('click', @back) + @backBtn.show() if @showBackBtn #logger.debug("Initialed with (unique) select",@webcamSelect) + + context.JK.helpBubble(@videoSettingsHelp, 'ftue-video-settings', {}, {width:300}) if @videoSettingsHelp.length > 0 + @videoSettingsHelp.click(false) + beforeShow:() => + + @videoShared = false # video can be assumed to be closed before htis is reached this.loadWebCams() this.selectWebcam() this.loadResolutions() @@ -35,30 +80,78 @@ context.JK.WebcamViewer = class WebcamViewer # protect against non-video clients pointed at video-enabled server from getting into a session if @client.SessStopVideoSharing @client.SessStopVideoSharing() + context.JK.onBackendEvent(ALERT_NAMES.USB_CONNECTED, 'webcam-viewer', @onUsbDeviceConnected); + context.JK.onBackendEvent(ALERT_NAMES.USB_DISCONNECTED, 'webcam-viewer', @onUsbDeviceDisconnected); #client.SessSetInsetPosition(5) #client.SessSetInsetSize(1) #client.FTUESetAutoSelectVideoLayout(false) #client.SessSelectVideoDisplayLayoutGroup(1) + onUsbDeviceConnected: () => + # don't handle USB events when minimized + return if !context.jamClient.IsFrontendVisible() + + logger.debug("USB device connected"); + + @scheduleRescanSystem(3000); + + onUsbDeviceDisconnected:() => + # don't handle USB events when minimized + return if !context.jamClient.IsFrontendVisible() + + logger.debug("USB device disconnected"); + + @scheduleRescanSystem(3000); + + scheduleRescanSystem: (time) => + if @rescanTimeout? + clearTimeout(@rescanTimeout) + @rescanTimeout = null + + @rescanningNotice.show() + @rescanTimeout = setTimeout(() => + @rescanningNotice.hide() + @loadWebCams() + , time) + selectWebcam:(e, data) => device = @webcamSelect.val() if device? caps = @client.FTUEGetVideoCaptureDeviceCapabilities(device) @logger.debug("Got capabilities from device", caps, device) - @client.FTUESelectVideoCaptureDevice(device, caps) + result = @client.FTUESelectVideoCaptureDevice(device, caps) + @logger.debug("FTUESelectVideoCaptureDevice result: ", result) + + updateBackend: (selectedResolution, selectedFps) => + @logger.debug 'Selecting webcam resolution: ', selectedResolution + @logger.debug 'Selecting webcam fps: ', selectedFps + + @client.FTUESetVideoEncodeResolution selectedResolution + @client.FTUESetSendFrameRates selectedFps selectResolution:() => @logger.debug 'Selecting from res control: ', @resolutionSelect @resolution = @resolutionSelect.val() if @resolution? - @logger.debug 'Selecting webcam resolution: ', @resolution - @client.FTUESetVideoEncodeResolution @resolution + bits = @resolution.split('|') + selectedResolution = bits[0] + selectedFps = bits[1] + @updateBackend(selectedResolution, selectedFps) + # if @isVideoShared # this.setVideoOff() # this.toggleWebcam() + beforeHide: () => + if @rescanTimeout? + clearTimeout(@rescanTimeout) + @rescanTimeout = null + + @setVideoOff() + setVideoOff:() => + if this.isVideoShared() @client.SessStopVideoSharing() @@ -71,6 +164,9 @@ context.JK.WebcamViewer = class WebcamViewer @toggleBtn.prop 'disabled', true @toggleBtn.prop 'disabled', !available + back: () => + window.location = '/client#/account' + toggleWebcam:() => @logger.debug 'Toggling webcam from: ', this.isVideoShared(), @toggleBtn if this.isVideoShared() @@ -79,6 +175,7 @@ context.JK.WebcamViewer = class WebcamViewer @videoShared = false else @toggleBtn.addClass("selected") + alert("HERE?") @client.SessStartVideoSharing 0 @videoShared = true @@ -86,23 +183,31 @@ context.JK.WebcamViewer = class WebcamViewer webcamName="None Configured" # protect against non-video clients pointed at video-enabled server from getting into a session webcam = if @client.FTUECurrentSelectedVideoDevice? then @client.FTUECurrentSelectedVideoDevice() else null + logger.debug("currently selected video device", webcam) if (webcam? && Object.keys(webcam).length>0) - webcamName = _.values(webcam)[0] + webcamName = Object.keys(webcam)[0] webcamName loadWebCams:() => # protect against non-video clients pointed at video-enabled server from getting into a session - devices = if @client.FTUEGetVideoCaptureDeviceNames? then @client.FTUEGetVideoCaptureDeviceNames() else [] + devices = if @client.FTUEGetVideoCaptureDeviceNames? then @client.FTUEGetVideoCaptureDeviceNames() else {} selectedDevice = this.selectedDeviceName() + @logger.debug("webcam devices", devices, selectedDevice) selectControl = @webcamSelect - context._.each devices, (device) -> - selected = device == selectedDevice + selectControl.empty() + newDeviceList = [] + context._.each devices, (deviceName, deviceGuid) -> + selected = deviceName == selectedDevice option = $('
    - + \ No newline at end of file diff --git a/web/app/views/clients/_session.html.slim b/web/app/views/clients/_session.html.slim index 1311d1dd7..e69de29bb 100644 --- a/web/app/views/clients/_session.html.slim +++ b/web/app/views/clients/_session.html.slim @@ -1,167 +0,0 @@ -#session-screen-old.screen.secondary[layout="screen" layout-id="session_old" layout-arg="id"] - .content-head - .content-icon - = image_tag "shared/icon_session.png", {:height => 19, :width => 19} - h1 - | session - .content-body - #session-controls - a#session-resync.button-grey.resync.left - = image_tag "content/icon_resync.png", {:align => "texttop", :height => 12, :width => 12} - | RESYNC - a#session-settings-button.button-grey.left[layout-link="session-settings"] - = image_tag "content/icon_settings_sm.png", {:align => "texttop", :height => 12, :width => 12} - | SETTINGS - a.button-grey.left[layout-link="share-dialog"] - = image_tag "content/icon_share.png", {:align => "texttop", :height => 12, :width => 12} - | SHARE - - - if (Rails.application.config.video_available && Rails.application.config.video_available!="none") - a#session-webcam.button-grey-toggle.video.left - = image_tag "content/icon_cam.png", {:align => "texttop", :height => 12, :width => 12} - | VIDEO - .block - .label - | VOLUME: - #volume.fader.lohi[mixer-id=""] - .block.monitor-mode-holder - .label - | MIX: - select.monitor-mode.easydropdown - option.label[value="personal"] - | Personal - option[value="master"] - | Master - a#session-leave.button-grey.right.leave[href="/client#/home"] - | X  LEAVE - #tracks - .content-scroller - .content-wrapper - .session-mytracks - h2 - | my tracks - #track-settings.session-add[style="display:block;" layout-link="configure-tracks"] - = image_tag "content/icon_settings_lg.png", {:width => 18, :height => 18} - span - | Settings - .session-tracks-scroller - #session-mytracks-notracks - p.notice - | You have not set up any inputs for your instrument or vocals.  - | If you want to hear yourself play through the JamKazam app,  - | and let the app mix your live playing with JamTracks, or with other musicians in online sessions,  - a.open-ftue-no-tracks href='#' click here now. - #session-mytracks-container - #voice-chat.voicechat[style="display:none;" mixer-id=""] - .voicechat-label - | CHAT - .voicechat-gain - .voicechat-mute.enabled[control="mute" mixer-id=""] - .session-fluidtracks - .session-livetracks - h2 - | live tracks - .session-add[layout-link="select-invites"] - a#session-invite-musicians[href="#"] - = image_tag "content/icon_add.png", {:width => 19, :height => 19, :align => "texttop"} - |   Invite Musicians - .session-tracks-scroller - #session-livetracks-container - .when-empty.livetracks - | No other musicians - br - | are in your session - br[clear="all"] - #recording-start-stop.recording - a - = image_tag "content/recordbutton-off.png", {:width => 20, :height => 20, :align => "absmiddle"} - |    - span#recording-status - | Make Recording - .session-recordings - h2 - | other audio - .session-recording-name-wrapper - .session-recording-name.left - | (No audio loaded) - .session-add.right - a#close-playback-recording[href="#"] - = image_tag "content/icon_close.png", {:width => 18, :height => 20, :align => "texttop"} - |   Close - .session-tracks-scroller - #session-recordedtracks-container - .when-empty.recordings - span.open-media-file-header - = image_tag "content/icon_folder.png", {width:22, height:20} - | Open: - ul.open-media-file-options - li - a#open-a-recording[href="#"] - | Recording - - if Rails.application.config.jam_tracks_available || (current_user && current_user.admin) - li.open-a-jamtrack - a#open-a-jamtrack[href="#"] - | JamTrack - - if Rails.application.config.backing_tracks_available - li - a#open-a-backingtrack[href="#"] - | Audio File - .when-empty.use-metronome-header - - if Rails.application.config.metronome_available - = image_tag "content/icon_metronome.png", {width:22, height:20} - a#open-a-metronome[href="#"] - | Use Metronome - br[clear="all"] - .play-controls-holder - = render "play_controls" - .webcam-container.hidden - / Webcam is actually in another window. - = render 'webcam' -= render "configureTrack" -= render "addTrack" -= render "addNewGear" -= render "error" -= render "sessionSettings" -script#template-session-track[type="text/template"] - .session-track.track client-id="{clientId}" track-id="{trackId}" - .track-vu-left.mixer-id="{vuMixerId}_vul" - .track-vu-right.mixer-id="{vuMixerId}_vur" - .track-label[title="{name}"] - span.name-text="{name}" - #div-track-close.track-close.op30 track-id="{trackId}" - =image_tag("content/icon_closetrack.png", {width: 12, height: 12}) - div class="{avatarClass}" - img src="{avatar}" - .track-instrument class="{preMasteredClass}" - img height="45" src="{instrumentIcon}" width="45" - .track-gain mixer-id="{mixerId}" - .track-icon-mute class="{muteClass}" control="mute" mixer-id="{muteMixerId}" - .track-icon-loop.hidden control="loop" - input#loop-button type="checkbox" value="loop" Loop - .track-connection.grey mixer-id="{mixerId}_connection" - CONNECTION - .disabled-track-overlay - .metronome-selects.hidden - select.metronome-select.metro-sound title="Metronome Sound" - option.label value="Beep" Knock - option.label value="Click" Tap - option.label value="Snare" Snare - option.label value="Kick" Kick - br - select.metronome-select.metro-tempo title="Metronome Tempo" - - metronome_tempos.each do |t| - option.label value=t - =t - -script#template-option type="text/template" - option value="{value}" title="{label}" selected="{selected}" - ="{label}" - -script#template-genre-option type="text/template" - option value="{value}" - ="{label}" - -script#template-pending-metronome type="text/template" - .pending-metronome - .spinner-large - p Your metronome is synchronizing. diff --git a/web/app/views/clients/_session2.html.slim b/web/app/views/clients/_session2.html.slim index 947b42d29..d8b263d3a 100644 --- a/web/app/views/clients/_session2.html.slim +++ b/web/app/views/clients/_session2.html.slim @@ -6,3 +6,56 @@ | session .content-body = react_component 'SessionScreen', {} + += render "configureTrack" += render "addTrack" += render "addNewGear" += render "error" += render "sessionSettings" +script#template-session-track[type="text/template"] + .session-track.track client-id="{clientId}" track-id="{trackId}" + .track-vu-left.mixer-id="{vuMixerId}_vul" + .track-vu-right.mixer-id="{vuMixerId}_vur" + .track-label[title="{name}"] + span.name-text="{name}" + #div-track-close.track-close.op30 track-id="{trackId}" + =image_tag("content/icon_closetrack.png", {width: 12, height: 12}) + div class="{avatarClass}" + img src="{avatar}" + .track-instrument class="{preMasteredClass}" + img height="45" src="{instrumentIcon}" width="45" + .track-gain mixer-id="{mixerId}" + .track-icon-mute class="{muteClass}" control="mute" mixer-id="{muteMixerId}" + .track-icon-loop.hidden control="loop" + input#loop-button type="checkbox" value="loop" Loop + .track-connection.grey mixer-id="{mixerId}_connection" + CONNECTION + .disabled-track-overlay + .metronome-selects.hidden + select.metronome-select.metro-sound title="Metronome Sound" + option.label value="Beep" Knock + option.label value="Click" Tap + option.label value="Snare" Snare + option.label value="Kick" Kick + br + select.metronome-select.metro-tempo title="Metronome Tempo" + - metronome_tempos.each do |t| + option.label value=t + =t + +script#template-option type="text/template" + option value="{value}" title="{label}" selected="{selected}" + ="{label}" + +script#template-genre-option type="text/template" + option value="{value}" + ="{label}" + +script#template-instrument-option-simple type="text/template" + option value="{value}" + ="{label}" + +script#template-pending-metronome type="text/template" + .pending-metronome + .spinner-large + p Your metronome is synchronizing. diff --git a/web/app/views/clients/_shopping_cart.html.haml b/web/app/views/clients/_shopping_cart.html.haml index 63c9de561..1db6fe56b 100644 --- a/web/app/views/clients/_shopping_cart.html.haml +++ b/web/app/views/clients/_shopping_cart.html.haml @@ -56,6 +56,6 @@ .clearall .right.actions %a.button-grey{href: "#"} HELP - %a.button-orange{href: "/client#/jamtrackBrowse"} CONTINUE SHOPPING + %a.button-orange{href: "/client#/jamtrack/search"} CONTINUE SHOPPING %a.button-orange.proceed-checkout{href: "#"} PROCEED TO CHECKOUT .clearall \ No newline at end of file diff --git a/web/app/views/clients/_web_filter.html.haml b/web/app/views/clients/_web_filter.html.haml index 9fd8b9150..734d6b1c7 100644 --- a/web/app/views/clients/_web_filter.html.haml +++ b/web/app/views/clients/_web_filter.html.haml @@ -22,8 +22,9 @@ / =select_tag("#{filter_label}_genre", | / options_for_select([['Any', '']].concat(JamRuby::Genre.all.collect { |ii| [ii.description, ii.id] })), {:class => 'easydropdown'}) | -if :jamtrack==filter_label - =content_tag(:div, 'Filter JamTracks:', :class => 'filter-element desc') - =select_tag("#{filter_label}_artist", options_for_select([['Any Band', '']].concat(JamTrack.all_artists.collect { |ii| [ii, ii] })), {:class => 'easydropdown'}) + =content_tag(:div, 'Filter JamTracks By:', :class => 'filter-element desc') + =select_tag("#{filter_label}_genre", + options_for_select([['Any Genre', '']].concat(JamRuby::Genre.order(:description).collect { |ii| [ii.description, ii.id] })), {:class => 'easydropdown'}) =content_tag(:div, :class => 'filter-element wrapper') do -if :musician==filter_label || :jamtrack==filter_label / =content_tag(:div, 'Instrument:', :class => 'filter-element desc instrument-selector') diff --git a/web/app/views/clients/_webcam.html.slim b/web/app/views/clients/_webcam.html.slim index 82182c2c8..75dad95e0 100644 --- a/web/app/views/clients/_webcam.html.slim +++ b/web/app/views/clients/_webcam.html.slim @@ -1,11 +1,17 @@ -h2.sub-header webcam: form.video + h2.sub-header.select-webcam select webcam: .webcam-select-container.wizard_control - select.w100 + select + h2.sub-header.select-resolution select video capture resolution & frame rate: .webcam-resolution-select-container.wizard_control - select.w100 + select + a.ftue-video-settings-help href='#' [?] .configure-webcam.wizard_control - a.button-grey-toggle.webcam-test-btn Test Webcam - .configure-webcam.wizard_control - a.button-grey-toggle.webcam-settings-btn Webcam Settings + a.hidden.button-grey.back-btn BACK + a.button-orange.webcam-test-btn TEST WEBCAM + .hidden.configure-webcam.wizard_control + a.hidden.button-orange.webcam-settings-btn WEBCAM SETTINGS + span.rescanning-notice + span.spinner-small + | CHECKING GEAR em.no-webcam-msg.hidden No webcam detected. If using an external webcam, please make sure it is plugged in to your computer. diff --git a/web/app/views/clients/index.html.erb b/web/app/views/clients/index.html.erb index ca1c5c24b..3ebe6f7fa 100644 --- a/web/app/views/clients/index.html.erb +++ b/web/app/views/clients/index.html.erb @@ -31,7 +31,6 @@ <%= render "sidebar" %> <%= render "scheduledSession" %> <%= render "findSession" %> -<%= render "session" %> <%= render "session2" %> <%= render "profile" %> <%= render "bandProfile" %> @@ -43,7 +42,8 @@ <%= render "clients/teachers/setup/pricing" %> <%= render "users/feed_music_session_ajax" %> <%= render "users/feed_recording_ajax" %> -<%= render "jamtrack_browse" %> +<%= render "jamtrack_search" %> +<%= render "jamtrack_filter" %> <%= render "jamtrack_landing" %> <%= render "shopping_cart" %> <%= render "checkout_signin" %> @@ -171,6 +171,7 @@ localRecordingsDialog.initialize(); var openJamTrackDialog = new JK.OpenJamTrackDialog(JK.app); + JK.OpenJamTrackDialogInstance = openJamTrackDialog; openJamTrackDialog.initialize(); var openBackingTrackDialog = new JK.OpenBackingTrackDialog(JK.app); @@ -288,11 +289,11 @@ // } // findSessionScreen.initialize(sessionLatency); - var jamtrackScreen = new JK.JamTrackScreen(JK.app); - jamtrackScreen.initialize(); + //var jamtrackScreen = new JK.JamTrackScreen(JK.app); + //jamtrackScreen.initialize(); - var jamtrackLanding = new JK.JamTrackLanding(JK.app); - jamtrackLanding.initialize(); + //var jamtrackLanding = new JK.JamTrackLanding(JK.app); + //jamtrackLanding.initialize(); var shoppingCartScreen = new JK.ShoppingCartScreen(JK.app); shoppingCartScreen.initialize(); diff --git a/web/app/views/clients/wizard/gear/_gear_wizard.html.haml b/web/app/views/clients/wizard/gear/_gear_wizard.html.haml index cfadad69c..c2c1d9b50 100644 --- a/web/app/views/clients/wizard/gear/_gear_wizard.html.haml +++ b/web/app/views/clients/wizard/gear/_gear_wizard.html.haml @@ -1,6 +1,6 @@ .dialog.gear-wizard{ layout: 'dialog', 'layout-id' => 'gear-wizard', id: 'gear-wizard-dialog'} .content-head - %h1 audio gear setup + %h1.top-header audio gear setup .ftue-inner{ 'layout-wizard' => 'gear-wizard' } .wizard-step{ 'layout-wizard-step' => "0", 'dialog-title' => "Understand Your Gear", 'dialog-purpose' => "Intro"} @@ -162,7 +162,7 @@ -step=4 -if (Rails.application.config.video_available && Rails.application.config.video_available!="none") - .wizard-step.video-gear{ 'layout-wizard-step' => "#{step+=1}", 'dialog-title' => "Select Video Gear", 'dialog-purpose' => "SelectVideoGear" } + .wizard-step.video-gear{ 'layout-wizard-step' => "#{step+=1}", 'dialog-title' => "Set Up Video Gear", 'dialog-purpose' => "SelectVideoGear" } .ftuesteps .clearall = render :partial => '/clients/wizard/gear/video_gear' diff --git a/web/app/views/clients/wizard/gear/_video_gear.html.haml b/web/app/views/clients/wizard/gear/_video_gear.html.haml index f87fd9802..cb6b649f0 100644 --- a/web/app/views/clients/wizard/gear/_video_gear.html.haml +++ b/web/app/views/clients/wizard/gear/_video_gear.html.haml @@ -1,16 +1,19 @@ -.help-text In this step, you will select your video gear. Please watch the video for best instructions. +.help-text In this step, you will select your video gear. .wizard-step-content .wizard-step-column %h2 Instructions .ftue-box.instructions - %ul + %ul.has-webcam %li Select webcam to use for video in sessions. - %li Verify that you see the video for the webcam in the window to the right. - %li Configure webcam settings if desired. - .center - %a.button-orange.watch-video{href:'https://www.youtube.com/watch?v=f7niycdWm7Y', rel:'external'} WATCH VIDEO + %li Select video capture settings. + %li Click the Test webcam button to open a window and verify that your webcam is properly capturing video. Then use the Window / Close Video Window menu command to close the window, and click the Next button to move forward. + %ul.no-webcam + %li JamKazam does not detect any webcams. + %li You will not be able to send video, but you can still receive it from others. + %li.is-windows The TEST VIDEO button will open the JamKazam video window to verify that receiving video works on your system. + %li.is-not-windows You can skip this step. .wizard-step-column - =render(partial: '/clients/webcam') + .webcam-container .clearall / Webcam from client can't currently be embedded: / .wizard-step-column diff --git a/web/app/views/dialogs/_loginRequiredDialog.html.slim b/web/app/views/dialogs/_loginRequiredDialog.html.slim index d2a17098b..d895fee2a 100644 --- a/web/app/views/dialogs/_loginRequiredDialog.html.slim +++ b/web/app/views/dialogs/_loginRequiredDialog.html.slim @@ -11,7 +11,7 @@ |  to access most functionality on this page. p | However, you can browse for  - a class="go-to-jamtracks" href='/client#/jamtrackBrowse' JamTracks + a class="go-to-jamtracks" href='/client#/jamtrack/search' JamTracks |  without logging in. br .clearall diff --git a/web/app/views/dialogs/_openJamTrackDialog.html.slim b/web/app/views/dialogs/_openJamTrackDialog.html.slim index d26aa8482..e2e85dd69 100644 --- a/web/app/views/dialogs/_openJamTrackDialog.html.slim +++ b/web/app/views/dialogs/_openJamTrackDialog.html.slim @@ -7,6 +7,9 @@ .dialog-inner + = react_component 'JamTrackAutoComplete', {:onSearch => 'window.JK.OpenJamTrackDialogInstance.search', show_purchased_only:true} + + button.search-btn.button-orange SEARCH .recording-wrapper table.open-jam-tracks cellspacing="0" cellpadding="0" border="0" thead @@ -28,7 +31,7 @@ .help-links a.what-are-jamtracks href='#' | What are JamTracks? - a href='/client#/jamtrackBrowse' rel="external" + a href='/client#/jamtrack/search' rel="external" | Shop for JamTracks .right a href="#" class="button-grey" layout-action="cancel" diff --git a/web/app/views/landings/product_jamtracks.html.slim b/web/app/views/landings/product_jamtracks.html.slim index 97196df8e..2b67ebf1d 100644 --- a/web/app/views/landings/product_jamtracks.html.slim +++ b/web/app/views/landings/product_jamtracks.html.slim @@ -22,7 +22,7 @@ p Click the GET A JAMTRACK FREE button below. Browse to find the one you want, click the Add to cart, and we'll apply a credit during checkout to make the first one free! We're confident you'll be back for more. .cta-big-button - a.white-bordered-button href="/client#/jamtrackBrowse" GET A JAMTRACK FREE! + a.white-bordered-button href="/client#/jamtrack/search" GET A JAMTRACK FREE! .column h1 Why are JamTracks Better than Backing Tracks? p diff --git a/web/app/views/layouts/minimal.html.erb b/web/app/views/layouts/minimal.html.erb index 3a5ad3ea9..5e7e80320 100644 --- a/web/app/views/layouts/minimal.html.erb +++ b/web/app/views/layouts/minimal.html.erb @@ -25,6 +25,7 @@ <%= yield %>
    + <%= render "clients/help" %>