* VRFS-946 - in-session recording
This commit is contained in:
parent
c3954d65f0
commit
ba3b9ab873
|
|
@ -82,3 +82,4 @@ band_photo_filepicker.sql
|
||||||
bands_geocoding.sql
|
bands_geocoding.sql
|
||||||
store_s3_filenames.sql
|
store_s3_filenames.sql
|
||||||
discardable_recorded_tracks.sql
|
discardable_recorded_tracks.sql
|
||||||
|
music_sessions_have_claimed_recording.sql
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
-- let a music_session reference a claimed recording, so that the state of the session knows if someone is playing a recording back
|
||||||
|
ALTER TABLE music_sessions ADD COLUMN claimed_recording_id VARCHAR(64) REFERENCES claimed_recordings(id);
|
||||||
|
ALTER TABLE music_sessions ADD COLUMN claimed_recording_initiator_id VARCHAR(64) REFERENCES users(id);
|
||||||
|
|
@ -270,7 +270,14 @@ SQL
|
||||||
raise Exception, msg
|
raise Exception, msg
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
else
|
||||||
|
# there are still people in the session
|
||||||
|
|
||||||
|
#ensure that there is no active claimed recording if the owner of that recording left the session
|
||||||
|
conn.exec("UPDATE music_sessions set claimed_recording_id = NULL, claimed_recording_initiator_id = NULL where claimed_recording_initiator_id = $1 and id = $2",
|
||||||
|
[user_id, previous_music_session_id])
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ module ValidationMessages
|
||||||
|
|
||||||
# recordings
|
# recordings
|
||||||
ALREADY_BEING_RECORDED = "already being recorded"
|
ALREADY_BEING_RECORDED = "already being recorded"
|
||||||
|
ALREADY_PLAYBACK_RECORDING = "already playing a recording"
|
||||||
NO_LONGER_RECORDING = "no longer recording"
|
NO_LONGER_RECORDING = "no longer recording"
|
||||||
NOT_IN_SESSION = "not in session"
|
NOT_IN_SESSION = "not in session"
|
||||||
|
|
||||||
|
|
@ -59,6 +60,10 @@ module ValidationMessages
|
||||||
PART_NOT_STARTED = "not started"
|
PART_NOT_STARTED = "not started"
|
||||||
UPLOAD_FAILURES_EXCEEDED = "exceeded"
|
UPLOAD_FAILURES_EXCEEDED = "exceeded"
|
||||||
|
|
||||||
|
# music sessions
|
||||||
|
MUST_BE_A_MUSICIAN = "must be a musician"
|
||||||
|
CLAIMED_RECORDING_ALREADY_IN_PROGRESS = "already started by someone else"
|
||||||
|
|
||||||
|
|
||||||
# takes either a string/string hash, or a string/array-of-strings|symbols hash,
|
# takes either a string/string hash, or a string/array-of-strings|symbols hash,
|
||||||
# and creates a ActiveRecord.errors style object
|
# and creates a ActiveRecord.errors style object
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ module JamRuby
|
||||||
belongs_to :user, :class_name => "JamRuby::User", :inverse_of => :claimed_recordings
|
belongs_to :user, :class_name => "JamRuby::User", :inverse_of => :claimed_recordings
|
||||||
belongs_to :genre, :class_name => "JamRuby::Genre"
|
belongs_to :genre, :class_name => "JamRuby::Genre"
|
||||||
has_many :recorded_tracks, :through => :recording, :class_name => "JamRuby::RecordedTrack"
|
has_many :recorded_tracks, :through => :recording, :class_name => "JamRuby::RecordedTrack"
|
||||||
|
has_many :playing_sessions, :class_name => "JamRuby::MusicSession"
|
||||||
|
|
||||||
# user must own this object
|
# user must own this object
|
||||||
# params is a hash, and everything is optional
|
# params is a hash, and everything is optional
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ module JamRuby
|
||||||
attr_accessible :creator, :description, :musician_access, :approval_required, :fan_chat, :fan_access, :genres
|
attr_accessible :creator, :description, :musician_access, :approval_required, :fan_chat, :fan_access, :genres
|
||||||
|
|
||||||
belongs_to :creator, :inverse_of => :music_sessions, :class_name => "JamRuby::User", :foreign_key => "user_id"
|
belongs_to :creator, :inverse_of => :music_sessions, :class_name => "JamRuby::User", :foreign_key => "user_id"
|
||||||
|
belongs_to :claimed_recording, :class_name => "JamRuby::ClaimedRecording", :foreign_key => "claimed_recording_id", :inverse_of => :playing_sessions
|
||||||
|
belongs_to :claimed_recording_initiator, :class_name => "JamRuby::User", :inverse_of => :playing_claimed_recordings, :foreign_key => "claimed_recording_initiator_id"
|
||||||
|
|
||||||
has_many :connections, :class_name => "JamRuby::Connection"
|
has_many :connections, :class_name => "JamRuby::Connection"
|
||||||
has_many :users, :through => :connections, :class_name => "JamRuby::User"
|
has_many :users, :through => :connections, :class_name => "JamRuby::User"
|
||||||
|
|
@ -33,10 +35,20 @@ module JamRuby
|
||||||
validates :legal_terms, :inclusion => {:in => [true]}, :on => :create
|
validates :legal_terms, :inclusion => {:in => [true]}, :on => :create
|
||||||
validates :creator, :presence => true
|
validates :creator, :presence => true
|
||||||
validate :creator_is_musician
|
validate :creator_is_musician
|
||||||
|
validate :no_new_playback_while_playing
|
||||||
|
|
||||||
def creator_is_musician
|
def creator_is_musician
|
||||||
unless creator.musician?
|
unless creator.musician?
|
||||||
errors.add(:creator, "must be a musician")
|
errors.add(:creator, ValidationMessages::MUST_BE_A_MUSICIAN)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def no_new_playback_while_playing
|
||||||
|
# if we previous had a claimed recording and are trying to set one
|
||||||
|
# and if also the previous initiator is different than the current one... it's a no go
|
||||||
|
if !claimed_recording_id_was.nil? && !claimed_recording_id.nil? &&
|
||||||
|
claimed_recording_initiator_id_was != claimed_recording_initiator_id
|
||||||
|
errors.add(:claimed_recording, ValidationMessages::CLAIMED_RECORDING_ALREADY_IN_PROGRESS)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -172,6 +184,10 @@ module JamRuby
|
||||||
recordings.where(:duration => nil).count > 0
|
recordings.where(:duration => nil).count > 0
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def is_playing_recording?
|
||||||
|
!self.claimed_recording.nil?
|
||||||
|
end
|
||||||
|
|
||||||
def recording
|
def recording
|
||||||
recordings.where(:duration => nil).first
|
recordings.where(:duration => nil).first
|
||||||
end
|
end
|
||||||
|
|
@ -182,8 +198,20 @@ module JamRuby
|
||||||
current_recording.stop unless current_recording.nil?
|
current_recording.stop unless current_recording.nil?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def claimed_recording_start(owner, claimed_recording)
|
||||||
|
self.claimed_recording = claimed_recording
|
||||||
|
self.claimed_recording_initiator = owner
|
||||||
|
self.save
|
||||||
|
end
|
||||||
|
|
||||||
|
def claimed_recording_stop
|
||||||
|
self.claimed_recording = nil
|
||||||
|
self.claimed_recording_initiator = nil
|
||||||
|
self.save
|
||||||
|
end
|
||||||
|
|
||||||
def to_s
|
def to_s
|
||||||
return description
|
description
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ module JamRuby
|
||||||
has_many :recorded_tracks, :class_name => "JamRuby::RecordedTrack", :foreign_key => :recording_id
|
has_many :recorded_tracks, :class_name => "JamRuby::RecordedTrack", :foreign_key => :recording_id
|
||||||
validates :music_session, :presence => true
|
validates :music_session, :presence => true
|
||||||
validate :not_already_recording, :on => :create
|
validate :not_already_recording, :on => :create
|
||||||
|
validate :not_playback_recording, :on => :create
|
||||||
validate :already_stopped_recording
|
validate :already_stopped_recording
|
||||||
|
|
||||||
def not_already_recording
|
def not_already_recording
|
||||||
|
|
@ -22,6 +23,12 @@ module JamRuby
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def not_playback_recording
|
||||||
|
if music_session.is_playing_recording?
|
||||||
|
errors.add(:music_session, ValidationMessages::ALREADY_PLAYBACK_RECORDING)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def already_stopped_recording
|
def already_stopped_recording
|
||||||
if is_done && is_done_was
|
if is_done && is_done_was
|
||||||
errors.add(:music_session, ValidationMessages::NO_LONGER_RECORDING)
|
errors.add(:music_session, ValidationMessages::NO_LONGER_RECORDING)
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ module JamRuby
|
||||||
has_many :owned_recordings, :class_name => "JamRuby::Recording"
|
has_many :owned_recordings, :class_name => "JamRuby::Recording"
|
||||||
has_many :recordings, :through => :claimed_recordings, :class_name => "JamRuby::Recording"
|
has_many :recordings, :through => :claimed_recordings, :class_name => "JamRuby::Recording"
|
||||||
has_many :claimed_recordings, :class_name => "JamRuby::ClaimedRecording", :inverse_of => :user
|
has_many :claimed_recordings, :class_name => "JamRuby::ClaimedRecording", :inverse_of => :user
|
||||||
|
has_many :playing_claimed_recordings, :class_name => "JamRuby::MusicSession", :inverse_of => :claimed_recording_initiator
|
||||||
|
|
||||||
# user likers (a musician has likers and may have likes too; fans do not have likers)
|
# user likers (a musician has likers and may have likes too; fans do not have likers)
|
||||||
has_many :likers, :class_name => "JamRuby::UserLiker", :foreign_key => "user_id", :inverse_of => :user
|
has_many :likers, :class_name => "JamRuby::UserLiker", :foreign_key => "user_id", :inverse_of => :user
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ FactoryGirl.define do
|
||||||
approval_required false
|
approval_required false
|
||||||
musician_access true
|
musician_access true
|
||||||
legal_terms true
|
legal_terms true
|
||||||
|
genres [JamRuby::Genre.first]
|
||||||
association :creator, :factory => :user
|
association :creator, :factory => :user
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -430,7 +430,54 @@ describe MusicSession do
|
||||||
it "stop_recording should return recording object if recording" do
|
it "stop_recording should return recording object if recording" do
|
||||||
@music_session.stop_recording.should == @recording
|
@music_session.stop_recording.should == @recording
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "claim a recording" do
|
||||||
|
|
||||||
|
before(:each) do
|
||||||
|
@recording = Recording.start(@music_session, @user1)
|
||||||
|
@recording.errors.any?.should be_false
|
||||||
|
@recording.stop
|
||||||
|
@recording.reload
|
||||||
|
@claimed_recording = @recording.claim(@user1, "name", "description", Genre.first, true, true)
|
||||||
|
@claimed_recording.errors.any?.should be_false
|
||||||
|
end
|
||||||
|
|
||||||
|
it "allow a claimed recording to be associated" do
|
||||||
|
@music_session.claimed_recording_start(@user1, @claimed_recording)
|
||||||
|
@music_session.errors.any?.should be_false
|
||||||
|
@music_session.reload
|
||||||
|
@music_session.claimed_recording.should == @claimed_recording
|
||||||
|
@music_session.claimed_recording_initiator.should == @user1
|
||||||
|
end
|
||||||
|
|
||||||
|
it "allow a claimed recording to be removed" do
|
||||||
|
@music_session.claimed_recording_start(@user1, @claimed_recording)
|
||||||
|
@music_session.errors.any?.should be_false
|
||||||
|
@music_session.claimed_recording_stop
|
||||||
|
@music_session.errors.any?.should be_false
|
||||||
|
@music_session.reload
|
||||||
|
@music_session.claimed_recording.should be_nil
|
||||||
|
@music_session.claimed_recording_initiator.should be_nil
|
||||||
|
end
|
||||||
|
|
||||||
|
it "disallow a claimed recording to be started when already started by someone else" do
|
||||||
|
@user2 = FactoryGirl.create(:user)
|
||||||
|
@music_session.claimed_recording_start(@user1, @claimed_recording)
|
||||||
|
@music_session.errors.any?.should be_false
|
||||||
|
@music_session.claimed_recording_start(@user2, @claimed_recording)
|
||||||
|
@music_session.errors.any?.should be_true
|
||||||
|
@music_session.errors[:claimed_recording] == [ValidationMessages::CLAIMED_RECORDING_ALREADY_IN_PROGRESS]
|
||||||
|
end
|
||||||
|
|
||||||
|
it "allow a claimed recording to be started when already started by self" do
|
||||||
|
@user2 = FactoryGirl.create(:user)
|
||||||
|
@claimed_recording2 = @recording.claim(@user1, "name", "description", Genre.first, true, true)
|
||||||
|
@music_session.claimed_recording_start(@user1, @claimed_recording)
|
||||||
|
@music_session.errors.any?.should be_false
|
||||||
|
@music_session.claimed_recording_start(@user1, @claimed_recording2)
|
||||||
|
@music_session.errors.any?.should be_false
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ end
|
||||||
|
|
||||||
gem 'rails', '>=3.2.11'
|
gem 'rails', '>=3.2.11'
|
||||||
gem 'jquery-rails', '2.0.2'
|
gem 'jquery-rails', '2.0.2'
|
||||||
|
gem 'jquery-ui-rails'
|
||||||
gem 'bootstrap-sass', '2.0.4'
|
gem 'bootstrap-sass', '2.0.4'
|
||||||
gem 'bcrypt-ruby', '3.0.1'
|
gem 'bcrypt-ruby', '3.0.1'
|
||||||
gem 'faker', '1.0.1'
|
gem 'faker', '1.0.1'
|
||||||
|
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
|
|
@ -12,11 +12,14 @@
|
||||||
//
|
//
|
||||||
//= require jquery
|
//= require jquery
|
||||||
//= require jquery_ujs
|
//= require jquery_ujs
|
||||||
|
//= require jquery.ui.draggable
|
||||||
|
//= require jquery.bt
|
||||||
//= require jquery.icheck
|
//= require jquery.icheck
|
||||||
//= require jquery.color
|
//= require jquery.color
|
||||||
//= require jquery.cookie
|
//= require jquery.cookie
|
||||||
//= require jquery.Jcrop
|
//= require jquery.Jcrop
|
||||||
//= require jquery.naturalsize
|
//= require jquery.naturalsize
|
||||||
//= require jquery.queryparams
|
//= require jquery.queryparams
|
||||||
|
//= require jquery.timeago
|
||||||
//= require globals
|
//= require globals
|
||||||
//= require_directory .
|
//= require_directory .
|
||||||
|
|
|
||||||
|
|
@ -11,21 +11,25 @@
|
||||||
|
|
||||||
var $draggingFaderHandle = null;
|
var $draggingFaderHandle = null;
|
||||||
var $draggingFader = null;
|
var $draggingFader = null;
|
||||||
|
var draggingOrientation = null;
|
||||||
|
|
||||||
var subscribers = {};
|
var subscribers = {};
|
||||||
var logger = g.JK.logger;
|
var logger = g.JK.logger;
|
||||||
var MAX_VISUAL_FADER = 95;
|
|
||||||
|
|
||||||
function faderClick(evt) {
|
function faderClick(e) {
|
||||||
evt.stopPropagation();
|
e.stopPropagation();
|
||||||
if (g.JK.$draggingFaderHandle) {
|
|
||||||
return;
|
var $fader = $(this);
|
||||||
|
draggingOrientation = $fader.attr('orientation');
|
||||||
|
var faderId = $fader.attr("fader-id");
|
||||||
|
var offset = $fader.offset();
|
||||||
|
var position = { top: e.pageY - offset.top, left: e.pageX - offset.left}
|
||||||
|
|
||||||
|
var faderPct = faderValue($fader, e, position);
|
||||||
|
|
||||||
|
if (faderPct < 0 || faderPct > 100) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
var $fader = $(evt.currentTarget);
|
|
||||||
var faderId = $fader.closest('[fader-id]').attr("fader-id");
|
|
||||||
var $handle = $fader.find('div[control="fader-handle"]');
|
|
||||||
|
|
||||||
var faderPct = faderValue($fader, evt);
|
|
||||||
|
|
||||||
// Notify subscribers of value change
|
// Notify subscribers of value change
|
||||||
g._.each(subscribers, function(changeFunc, index, list) {
|
g._.each(subscribers, function(changeFunc, index, list) {
|
||||||
|
|
@ -39,75 +43,35 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function setHandlePosition($fader, value) {
|
function setHandlePosition($fader, value) {
|
||||||
if (value > MAX_VISUAL_FADER) { value = MAX_VISUAL_FADER; } // Visual limit
|
var ratio, position;
|
||||||
var $handle = $fader.find('div[control="fader-handle"]');
|
var $handle = $fader.find('div[control="fader-handle"]');
|
||||||
var handleCssAttribute = getHandleCssAttribute($fader);
|
var handleCssAttribute = getHandleCssAttribute($fader);
|
||||||
$handle.css(handleCssAttribute, value + '%');
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if(draggingOrientation === "horizontal") {
|
||||||
function faderHandleDown(evt) {
|
ratio = value / 100;
|
||||||
evt.stopPropagation();
|
position = ((ratio * $fader.width()) - (ratio * handleWidth(draggingOrientation))) + 'px';
|
||||||
$draggingFaderHandle = $(evt.currentTarget);
|
|
||||||
$draggingFader = $draggingFaderHandle.closest('div[control="fader"]');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function faderMouseUp(evt) {
|
|
||||||
evt.stopPropagation();
|
|
||||||
if ($draggingFaderHandle) {
|
|
||||||
var $fader = $draggingFaderHandle.closest('div[control="fader"]');
|
|
||||||
var faderId = $fader.closest('[fader-id]').attr("fader-id");
|
|
||||||
var faderPct = faderValue($fader, evt);
|
|
||||||
// Notify subscribers of value change
|
|
||||||
g._.each(subscribers, function(changeFunc, index, list) {
|
|
||||||
if (faderId === index) {
|
|
||||||
changeFunc(faderId, faderPct, false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
$draggingFaderHandle = null;
|
|
||||||
$draggingFader = null;
|
|
||||||
}
|
}
|
||||||
return false;
|
else {
|
||||||
|
ratio = (100 - value) / 100;
|
||||||
|
position = ((ratio * $fader.height()) - (ratio * handleWidth(draggingOrientation))) + 'px';
|
||||||
|
}
|
||||||
|
$handle.css(handleCssAttribute, position);
|
||||||
}
|
}
|
||||||
|
|
||||||
function faderValue($fader, evt) {
|
function faderValue($fader, e, offset) {
|
||||||
var orientation = $fader.attr('orientation');
|
var orientation = $fader.attr('orientation');
|
||||||
var getPercentFunction = getVerticalFaderPercent;
|
var getPercentFunction = getVerticalFaderPercent;
|
||||||
var absolutePosition = evt.clientY;
|
var relativePosition = offset.top;
|
||||||
if (orientation && orientation == 'horizontal') {
|
if (orientation && orientation == 'horizontal') {
|
||||||
getPercentFunction = getHorizontalFaderPercent;
|
getPercentFunction = getHorizontalFaderPercent;
|
||||||
absolutePosition = evt.clientX;
|
relativePosition = offset.left;
|
||||||
}
|
}
|
||||||
return getPercentFunction(absolutePosition, $fader);
|
return getPercentFunction(relativePosition, $fader);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getHandleCssAttribute($fader) {
|
function getHandleCssAttribute($fader) {
|
||||||
var orientation = $fader.attr('orientation');
|
var orientation = $fader.attr('orientation');
|
||||||
return (orientation === 'horizontal') ? 'left' : 'bottom';
|
return (orientation === 'horizontal') ? 'left' : 'top';
|
||||||
}
|
|
||||||
|
|
||||||
function faderMouseMove(evt) {
|
|
||||||
// bail out early if there's no in-process drag
|
|
||||||
if (!($draggingFaderHandle)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
var $fader = $draggingFader;
|
|
||||||
var faderId = $fader.closest('[fader-id]').attr("fader-id"); var $handle = $draggingFaderHandle;
|
|
||||||
evt.stopPropagation();
|
|
||||||
var faderPct = faderValue($fader, evt);
|
|
||||||
|
|
||||||
// Notify subscribers of value change
|
|
||||||
g._.each(subscribers, function(changeFunc, index, list) {
|
|
||||||
if (faderId === index) {
|
|
||||||
changeFunc(faderId, faderPct, true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (faderPct > MAX_VISUAL_FADER) { faderPct = MAX_VISUAL_FADER; } // Visual limit
|
|
||||||
var handleCssAttribute = getHandleCssAttribute($fader);
|
|
||||||
$handle.css(handleCssAttribute, faderPct + '%');
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getVerticalFaderPercent(eventY, $fader) {
|
function getVerticalFaderPercent(eventY, $fader) {
|
||||||
|
|
@ -119,28 +83,75 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the current value of the fader as int percent 0-100
|
* Returns the current value of the fader as int percent 0-100
|
||||||
*/
|
*/
|
||||||
function getFaderPercent(value, $fader, orientation) {
|
function getFaderPercent(value, $fader, orientation) {
|
||||||
var faderPosition = $fader.offset();
|
var faderSize, faderPct;
|
||||||
var faderMin = faderPosition.top;
|
|
||||||
var faderSize = $fader.height();
|
// the handle takes up room, and all calculations use top. So when the
|
||||||
var handleValue = (faderSize - (value-faderMin));
|
// handle *looks* like it's at the bottom by the user, it won't give a 0% value.
|
||||||
|
// so, we subtract handleWidth from the size of it's parent
|
||||||
|
|
||||||
if (orientation === "horizontal") {
|
if (orientation === "horizontal") {
|
||||||
faderMin = faderPosition.left;
|
|
||||||
faderSize = $fader.width();
|
faderSize = $fader.width();
|
||||||
handleValue = (value - faderMin);
|
faderPct = Math.round( ( value + (value / faderSize * handleWidth(orientation))) / faderSize * 100);
|
||||||
}
|
}
|
||||||
var faderPct = Math.round(handleValue/faderSize * 100);
|
else {
|
||||||
if (faderPct < 0) {
|
faderSize = $fader.height();
|
||||||
faderPct = 0;
|
faderPct = Math.round((faderSize - handleWidth(orientation) - value)/(faderSize - handleWidth(orientation)) * 100);
|
||||||
}
|
|
||||||
if (faderPct > 100) {
|
|
||||||
faderPct = 100;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return faderPct;
|
return faderPct;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onFaderDrag(e, ui) {
|
||||||
|
var faderId = $draggingFader.attr("fader-id");
|
||||||
|
var faderPct = faderValue($draggingFader, e, ui.position);
|
||||||
|
|
||||||
|
// protect against attempts to drag outside of the slider, which jquery.draggable sometimes allows
|
||||||
|
if (faderPct < 0 || faderPct > 100) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify subscribers of value change
|
||||||
|
g._.each(subscribers, function(changeFunc, index, list) {
|
||||||
|
if (faderId === index) {
|
||||||
|
changeFunc(faderId, faderPct, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onFaderDragStart(e, ui) {
|
||||||
|
$draggingFaderHandle = $(this);
|
||||||
|
$draggingFader = $draggingFaderHandle.closest('div[control="fader"]');
|
||||||
|
draggingOrientation = $draggingFader.attr('orientation');
|
||||||
|
}
|
||||||
|
|
||||||
|
function onFaderDragStop(e, ui) {
|
||||||
|
var faderId = $draggingFader.attr("fader-id");
|
||||||
|
var faderPct = faderValue($draggingFader, e, ui.position);
|
||||||
|
|
||||||
|
// protect against attempts to drag outside of the slider, which jquery.draggable sometimes allows
|
||||||
|
// do not return 'false' though, because that stops future drags from working, for some reason
|
||||||
|
if (faderPct < 0 || faderPct > 100) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify subscribers of value change
|
||||||
|
g._.each(subscribers, function(changeFunc, index, list) {
|
||||||
|
if (faderId === index) {
|
||||||
|
changeFunc(faderId, faderPct, false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$draggingFaderHandle = null;
|
||||||
|
$draggingFader = null;
|
||||||
|
draggingOrientation = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleWidth(orientation) {
|
||||||
|
return orientation === "horizontal" ? 8 : 11;
|
||||||
|
}
|
||||||
|
|
||||||
g.JK.FaderHelpers = {
|
g.JK.FaderHelpers = {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -174,6 +185,15 @@
|
||||||
var templateSource = $(templateSelector).html();
|
var templateSource = $(templateSelector).html();
|
||||||
|
|
||||||
$(selector).html(g._.template(templateSource, options));
|
$(selector).html(g._.template(templateSource, options));
|
||||||
|
|
||||||
|
$('div[control="fader-handle"]', $(selector)).draggable({
|
||||||
|
drag: onFaderDrag,
|
||||||
|
start: onFaderDragStart,
|
||||||
|
stop: onFaderDragStop,
|
||||||
|
containment: "parent",
|
||||||
|
axis: options.faderType === 'horizontal' ? 'x' : 'y'
|
||||||
|
})
|
||||||
|
|
||||||
// Embed any custom styles, applied to the .fader below selector
|
// Embed any custom styles, applied to the .fader below selector
|
||||||
if ("style" in options) {
|
if ("style" in options) {
|
||||||
for (var key in options.style) {
|
for (var key in options.style) {
|
||||||
|
|
@ -213,11 +233,6 @@
|
||||||
|
|
||||||
initialize: function() {
|
initialize: function() {
|
||||||
$('body').on('click', 'div[control="fader"]', faderClick);
|
$('body').on('click', 'div[control="fader"]', faderClick);
|
||||||
$('body').on('mousedown', 'div[control="fader-handle"]', faderHandleDown);
|
|
||||||
$('body').on('mousemove', 'div[layout-id="session"], [layout-wizard="ftue"]', faderMouseMove);
|
|
||||||
$('body').on('mouseup', 'div[layout-id="session"], [layout-wizard="ftue"]', faderMouseUp);
|
|
||||||
//$('body').on('mousemove', faderMouseMove);
|
|
||||||
//$('body').on('mouseup', faderMouseUp);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -542,6 +542,28 @@
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// passed an array of recording objects from the server
|
||||||
|
function GetLocalRecordingState(recordings) {
|
||||||
|
var result = { recordings:[]};
|
||||||
|
var recordingResults = result.recordings;
|
||||||
|
|
||||||
|
var possibleAnswers = ['HQ', 'RT', 'MISSING', 'PARTIALLY_MISSING'];
|
||||||
|
|
||||||
|
$.each(recordings.claimed_recordings, function(i, recordings) {
|
||||||
|
// just make up a random yes-hq/yes-rt/missing answer
|
||||||
|
var recordingResult = {};
|
||||||
|
recordingResult['aggregate_state'] = possibleAnswers[Math.floor((Math.random()*4))];
|
||||||
|
recordingResults.push(recordingResult);
|
||||||
|
})
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function OpenRecording(claimedRecording) {
|
||||||
|
return {success: true}
|
||||||
|
}
|
||||||
|
function CloseRecording() {}
|
||||||
|
|
||||||
|
|
||||||
// Javascript Bridge seems to camel-case
|
// Javascript Bridge seems to camel-case
|
||||||
// Set the instance functions:
|
// Set the instance functions:
|
||||||
|
|
@ -663,6 +685,11 @@
|
||||||
this.OnLoggedIn = OnLoggedIn;
|
this.OnLoggedIn = OnLoggedIn;
|
||||||
this.OnLoggedOut = OnLoggedOut;
|
this.OnLoggedOut = OnLoggedOut;
|
||||||
|
|
||||||
|
// Recording Playback
|
||||||
|
this.GetLocalRecordingState = GetLocalRecordingState;
|
||||||
|
this.OpenRecording = OpenRecording;
|
||||||
|
this.CloseRecording = CloseRecording;
|
||||||
|
|
||||||
// fake calls; not a part of the actual jam client
|
// fake calls; not a part of the actual jam client
|
||||||
this.RegisterP2PMessageCallbacks = RegisterP2PMessageCallbacks;
|
this.RegisterP2PMessageCallbacks = RegisterP2PMessageCallbacks;
|
||||||
this.SetFakeRecordingImpl = SetFakeRecordingImpl;
|
this.SetFakeRecordingImpl = SetFakeRecordingImpl;
|
||||||
|
|
|
||||||
|
|
@ -504,7 +504,18 @@
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
contentType: 'application/json',
|
contentType: 'application/json',
|
||||||
url: "/api/recordings/" + recordingId
|
url: "/api/recordings/" + recordingId
|
||||||
})
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getClaimedRecordings(options) {
|
||||||
|
|
||||||
|
return $.ajax({
|
||||||
|
type: "GET",
|
||||||
|
dataType: "json",
|
||||||
|
contentType: 'application/json',
|
||||||
|
url: "/api/claimed_recordings",
|
||||||
|
data: options
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function claimRecording(options) {
|
function claimRecording(options) {
|
||||||
|
|
@ -519,6 +530,36 @@
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function startPlayClaimedRecording(options) {
|
||||||
|
var musicSessionId = options["id"];
|
||||||
|
var claimedRecordingId = options["claimed_recording_id"];
|
||||||
|
delete options["id"];
|
||||||
|
delete options["claimed_recording_id"];
|
||||||
|
|
||||||
|
return $.ajax({
|
||||||
|
type: "POST",
|
||||||
|
dataType: "json",
|
||||||
|
contentType: 'application/json',
|
||||||
|
url: "/api/sessions/" + musicSessionId + "/claimed_recording/" + claimedRecordingId + "/start",
|
||||||
|
data: JSON.stringify(options)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopPlayClaimedRecording(options) {
|
||||||
|
var musicSessionId = options["id"];
|
||||||
|
var claimedRecordingId = options["claimed_recording_id"];
|
||||||
|
delete options["id"];
|
||||||
|
delete options["claimed_recording_id"];
|
||||||
|
|
||||||
|
return $.ajax({
|
||||||
|
type: "POST",
|
||||||
|
dataType: "json",
|
||||||
|
contentType: 'application/json',
|
||||||
|
url: "/api/sessions/" + musicSessionId + "/claimed_recording/" + claimedRecordingId + "/stop",
|
||||||
|
data: JSON.stringify(options)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function discardRecording(options) {
|
function discardRecording(options) {
|
||||||
var recordingId = options["id"];
|
var recordingId = options["id"];
|
||||||
|
|
||||||
|
|
@ -565,7 +606,7 @@
|
||||||
this.getFriends = getFriends;
|
this.getFriends = getFriends;
|
||||||
this.updateSession = updateSession;
|
this.updateSession = updateSession;
|
||||||
this.getSession = getSession;
|
this.getSession = getSession;
|
||||||
this.getClientDownloads = getClientDownloads
|
this.getClientDownloads = getClientDownloads;
|
||||||
this.createInvitation = createInvitation;
|
this.createInvitation = createInvitation;
|
||||||
this.postFeedback = postFeedback;
|
this.postFeedback = postFeedback;
|
||||||
this.serverHealthCheck = serverHealthCheck;
|
this.serverHealthCheck = serverHealthCheck;
|
||||||
|
|
@ -580,7 +621,10 @@
|
||||||
this.startRecording = startRecording;
|
this.startRecording = startRecording;
|
||||||
this.stopRecording = stopRecording;
|
this.stopRecording = stopRecording;
|
||||||
this.getRecording = getRecording;
|
this.getRecording = getRecording;
|
||||||
|
this.getClaimedRecordings = getClaimedRecordings;
|
||||||
this.claimRecording = claimRecording;
|
this.claimRecording = claimRecording;
|
||||||
|
this.startPlayClaimedRecording = startPlayClaimedRecording;
|
||||||
|
this.stopPlayClaimedRecording = stopPlayClaimedRecording;
|
||||||
this.discardRecording = discardRecording;
|
this.discardRecording = discardRecording;
|
||||||
this.putTrackSyncChange = putTrackSyncChange;
|
this.putTrackSyncChange = putTrackSyncChange;
|
||||||
this.createBand = createBand;
|
this.createBand = createBand;
|
||||||
|
|
|
||||||
|
|
@ -264,6 +264,23 @@
|
||||||
this.notify({title:title, text:text, icon_url: "/assets/content/icon_alert_big.png"});
|
this.notify({title:title, text:text, icon_url: "/assets/content/icon_alert_big.png"});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Using the standard rails style error object, shows an alert with all seen errors */
|
||||||
|
this.notifyServerError = function(jqXHR, title) {
|
||||||
|
if(!title) {
|
||||||
|
title = "Server Error";
|
||||||
|
}
|
||||||
|
if(jqXHR.status == 422) {
|
||||||
|
var errors = JSON.parse(jqXHR.responseText);
|
||||||
|
var $errors = context.JK.format_all_errors(errors);
|
||||||
|
this.notify({title:title, text:$errors, icon_url: "/assets/content/icon_alert_big.png"})
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// we need to cehck more status codes and make tailored messages at this point
|
||||||
|
this.notify({title:title, text:"status=" + jqXHR.status + ", message=" + jqXHR.responseText, icon_url: "/assets/content/icon_alert_big.png"})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize any common events.
|
* Initialize any common events.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -638,7 +638,7 @@
|
||||||
function setNotificationInfo(message, descriptor) {
|
function setNotificationInfo(message, descriptor) {
|
||||||
var $notify = $('[layout="notify"]');
|
var $notify = $('[layout="notify"]');
|
||||||
$('h2', $notify).text(message.title);
|
$('h2', $notify).text(message.title);
|
||||||
$('p', $notify).html(message.text);
|
$('p', $notify).html(message.text instanceof jQuery ? message.text.html() : message.text);
|
||||||
|
|
||||||
if (message.icon_url) {
|
if (message.icon_url) {
|
||||||
$('#avatar', $notify).attr('src', message.icon_url);
|
$('#avatar', $notify).attr('src', message.icon_url);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,169 @@
|
||||||
|
(function(context,$) {
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
context.JK = context.JK || {};
|
||||||
|
context.JK.LocalRecordingsDialog = function(app) {
|
||||||
|
var logger = context.JK.logger;
|
||||||
|
var rest = context.JK.Rest();
|
||||||
|
var showing = false;
|
||||||
|
var perPage = 10;
|
||||||
|
|
||||||
|
function tbody() {
|
||||||
|
return $('#local-recordings-dialog table.local-recordings tbody');
|
||||||
|
}
|
||||||
|
|
||||||
|
function emptyList() {
|
||||||
|
tbody().empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetPagination() {
|
||||||
|
$('#local-recordings-dialog .paginator').remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function beforeShow() {
|
||||||
|
emptyList();
|
||||||
|
resetPagination();
|
||||||
|
showing = true;
|
||||||
|
getRecordings(0)
|
||||||
|
.done(function(data, textStatus, jqXHR) {
|
||||||
|
// initialize pagination
|
||||||
|
var $paginator = context.JK.Paginator.create(parseInt(jqXHR.getResponseHeader('total-entries')), perPage, 0, onPageSelected)
|
||||||
|
$('#local-recordings-dialog .paginator-holder').append($paginator);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function afterHide() {
|
||||||
|
showing = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function onPageSelected(targetPage) {
|
||||||
|
return getRecordings(targetPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRecordings(page) {
|
||||||
|
return rest.getClaimedRecordings({page:page + 1, per_page:10})
|
||||||
|
.done(function(claimedRecordings) {
|
||||||
|
|
||||||
|
emptyList();
|
||||||
|
|
||||||
|
var recordings = [];
|
||||||
|
var $tbody = tbody();
|
||||||
|
|
||||||
|
var localResults = context.jamClient.GetLocalRecordingState({claimed_recordings: claimedRecordings});
|
||||||
|
|
||||||
|
if(localResults['error']) {
|
||||||
|
app.notify({
|
||||||
|
title : "Get Recording State Failure",
|
||||||
|
text : localResults['error'],
|
||||||
|
"icon_url": "/assets/content/icon_alert_big.png"
|
||||||
|
});
|
||||||
|
app.layout.closeDialog('localRecordings');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$.each(claimedRecordings, function(index, claimedRecording) {
|
||||||
|
|
||||||
|
var options = {
|
||||||
|
recordingId: claimedRecording.recording.id,
|
||||||
|
//date: context.JK.formatDate(claimedRecording.recording.created_at),
|
||||||
|
//time: context.JK.formatTime(claimedRecording.recording.created_at),
|
||||||
|
timeago: $.timeago(claimedRecording.recording.created_at),
|
||||||
|
name: claimedRecording.name,
|
||||||
|
aggregate_state: localResults.recordings[index]['aggregate_state'],
|
||||||
|
duration: context.JK.prettyPrintSeconds(claimedRecording.recording.duration)
|
||||||
|
};
|
||||||
|
|
||||||
|
var tr = $(context._.template($('#template-claimed-recording-row').html(), options, { variable: 'data' }));
|
||||||
|
|
||||||
|
tr.data('server-model', claimedRecording);
|
||||||
|
$tbody.append(tr);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.fail(function(jqXHR, textStatus, errorMessage) {
|
||||||
|
app.ajaxError(jqXHR, textStatus, errorMessage);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerStaticEvents() {
|
||||||
|
$('#local-recordings-dialog table.local-recordings tbody').on('click', 'tr', function(e) {
|
||||||
|
|
||||||
|
var localState = $(this).attr('data-local-state');
|
||||||
|
|
||||||
|
if(localState == 'MISSING') {
|
||||||
|
app.notify({
|
||||||
|
title : "Can't Open Recording",
|
||||||
|
text : "The recording is missing all tracks",
|
||||||
|
"icon_url": "/assets/content/icon_alert_big.png"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else if(localState == 'PARTIALLY_MISSING') {
|
||||||
|
app.notify({
|
||||||
|
title : "Can't Open Recording",
|
||||||
|
text : "The recording is missing some tracks",
|
||||||
|
"icon_url": "/assets/content/icon_alert_big.png"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var claimedRecording = $(this).data('server-model');
|
||||||
|
|
||||||
|
// tell the server we are about to start a recording
|
||||||
|
rest.startPlayClaimedRecording({id: context.JK.CurrentSessionModel.id(), claimed_recording_id: claimedRecording.id})
|
||||||
|
.done(function(response) {
|
||||||
|
var recordingId = $(this).attr('data-recording-id');
|
||||||
|
var openRecordingResult = context.jamClient.OpenRecording(claimedRecording);
|
||||||
|
|
||||||
|
logger.debug("OpenRecording response: %o", openRecordingResult);
|
||||||
|
|
||||||
|
if(openRecordingResult.error) {
|
||||||
|
app.notify({
|
||||||
|
"title": "Can't Open Recording",
|
||||||
|
"text": openRecordingResult.error,
|
||||||
|
"icon_url": "/assets/content/icon_alert_big.png"
|
||||||
|
});
|
||||||
|
|
||||||
|
rest.stopPlayClaimedRecording({id: context.JK.CurrentSessionModel.id(), claimed_recording_id: claimedRecording.id})
|
||||||
|
.fail(function(jqXHR) {
|
||||||
|
app.notify({
|
||||||
|
"title": "Couldn't Stop Recording Playback",
|
||||||
|
"text": "Couldn't inform the server to stop playback. msg=" + jqXHR.responseText,
|
||||||
|
"icon_url": "/assets/content/icon_alert_big.png"
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
app.layout.closeDialog('localRecordings');
|
||||||
|
$(this).triggerHandler('openedSession', {});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.fail(function(jqXHR) {
|
||||||
|
app.notifyServerError(jqXHR, "Unable to Open Recording For Playback");
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function initialize(){
|
||||||
|
var dialogBindings = {
|
||||||
|
'beforeShow' : beforeShow,
|
||||||
|
'afterHide': afterHide
|
||||||
|
};
|
||||||
|
|
||||||
|
app.bindDialog('localRecordings', dialogBindings);
|
||||||
|
|
||||||
|
registerStaticEvents();
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
this.initialize = initialize;
|
||||||
|
this.isShowing = function isShowing() { return showing; }
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
})(window,jQuery);
|
||||||
|
|
@ -0,0 +1,100 @@
|
||||||
|
/**
|
||||||
|
* Static functions for creating pagination
|
||||||
|
*/
|
||||||
|
(function(context, $) {
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
context.JK = context.JK || {};
|
||||||
|
|
||||||
|
context.JK.Paginator = {
|
||||||
|
|
||||||
|
/** returns a jquery object that encapsulates pagination markup.
|
||||||
|
* It's left to the caller to append it to the page as they like.
|
||||||
|
* @param pages the number of pages
|
||||||
|
* @param currentPage the current page
|
||||||
|
* @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) {
|
||||||
|
|
||||||
|
function calculatePages(total, perPageValue) {
|
||||||
|
return Math.ceil(total / perPageValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function attemptToMoveToTargetPage(targetPage) {
|
||||||
|
|
||||||
|
// 'working' == click guard
|
||||||
|
var working = paginator.data('working');
|
||||||
|
if(!working) {
|
||||||
|
paginator.data('working', true);
|
||||||
|
|
||||||
|
onPageSelected(targetPage)
|
||||||
|
.done(function(data, textStatus, jqXHR) {
|
||||||
|
totalEntries = parseInt(jqXHR.getResponseHeader('total-entries'));
|
||||||
|
pages = calculatePages(totalEntries, perPage);
|
||||||
|
options = { pages: pages,
|
||||||
|
currentPage: targetPage };
|
||||||
|
|
||||||
|
// recreate the pagination, and
|
||||||
|
var newPaginator = $(context._.template($('#template-paginator').html(), options, { variable: 'data' }));
|
||||||
|
registerEvents(newPaginator);
|
||||||
|
paginator.replaceWith(newPaginator);
|
||||||
|
paginator = newPaginator;
|
||||||
|
})
|
||||||
|
.always(function() {
|
||||||
|
paginator.data('working', false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.log("workin fool: %o", working)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerEvents(paginator) {
|
||||||
|
$('a.page-less', paginator).click(function(e) {
|
||||||
|
var currentPage = parseInt($(this).attr('data-current-page'));
|
||||||
|
if (currentPage > 0) {
|
||||||
|
var targetPage = currentPage - 1;
|
||||||
|
attemptToMoveToTargetPage(targetPage);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
$('a.page-more', paginator).click(function(e) {
|
||||||
|
var currentPage = parseInt($(this).attr('data-current-page'));
|
||||||
|
if (currentPage < pages - 1) {
|
||||||
|
var targetPage = currentPage + 1;
|
||||||
|
attemptToMoveToTargetPage(targetPage);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
$('a.page-link', paginator).click(function(e) {
|
||||||
|
var targetPage = parseInt($(this).attr('data-page'));
|
||||||
|
attemptToMoveToTargetPage(targetPage);
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var pages = calculatePages(totalEntries, perPage);
|
||||||
|
|
||||||
|
var options = { pages: pages,
|
||||||
|
currentPage: currentPage };
|
||||||
|
|
||||||
|
var paginator = $(context._.template($('#template-paginator').html(), options, { variable: 'data' }));
|
||||||
|
|
||||||
|
registerEvents(paginator);
|
||||||
|
|
||||||
|
return paginator;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})(window, jQuery);
|
||||||
|
|
@ -0,0 +1,184 @@
|
||||||
|
/**
|
||||||
|
* Static functions for creating pagination
|
||||||
|
*/
|
||||||
|
(function(context, $) {
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
context.JK = context.JK || {};
|
||||||
|
context.JK.PlaybackControls = function($parentElement){
|
||||||
|
var logger = context.JK.logger;
|
||||||
|
var $playButton = $('.play-button img.playbutton', $parentElement);
|
||||||
|
var $pauseButton = $('.play-button img.pausebutton', $parentElement);
|
||||||
|
var $currentTime = $('.recording-current', $parentElement);
|
||||||
|
var $duration = $('.duration-time', $parentElement);
|
||||||
|
var $sliderBar = $('.recording-playback', $parentElement);
|
||||||
|
var $slider = $('.recording-slider', $parentElement);
|
||||||
|
var $self = $(this);
|
||||||
|
|
||||||
|
var playbackPlaying = false;
|
||||||
|
var playbackDurationMs = 0;
|
||||||
|
var playbackPositionMs = 0;
|
||||||
|
var durationChanged = false;
|
||||||
|
|
||||||
|
var endReached = false;
|
||||||
|
var dragging = false;
|
||||||
|
var playingWhenDragStart = false;
|
||||||
|
var draggingUpdateTimer = null;
|
||||||
|
var canUpdateBackend = false;
|
||||||
|
|
||||||
|
function startPlay() {
|
||||||
|
updateIsPlaying(true);
|
||||||
|
if(endReached) {
|
||||||
|
update(0, playbackDurationMs, playbackPlaying);
|
||||||
|
}
|
||||||
|
$self.triggerHandler('play');
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopPlay() {
|
||||||
|
updateIsPlaying(false);
|
||||||
|
$self.triggerHandler('pause');
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateOffsetBasedOnPosition(offsetLeft) {
|
||||||
|
var sliderBarWidth = $sliderBar.width();
|
||||||
|
|
||||||
|
playbackPositionMs = parseInt((offsetLeft / sliderBarWidth) * playbackDurationMs);
|
||||||
|
updateCurrentTimeText(playbackPositionMs);
|
||||||
|
if(canUpdateBackend) {
|
||||||
|
$self.triggerHandler('change-position', {positionMs: playbackPositionMs});
|
||||||
|
canUpdateBackend = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startDrag(e, ui) {
|
||||||
|
dragging = true;
|
||||||
|
playingWhenDragStart = playbackPlaying;
|
||||||
|
draggingUpdateTimer = setInterval(function() { canUpdateBackend = true; }, 333); // only call backend up to 3 times a second while dragging
|
||||||
|
if(playingWhenDragStart) {
|
||||||
|
stopPlay();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopDrag(e, ui) {
|
||||||
|
dragging = false;
|
||||||
|
|
||||||
|
clearInterval(draggingUpdateTimer);
|
||||||
|
|
||||||
|
canUpdateBackend = true;
|
||||||
|
updateOffsetBasedOnPosition(ui.position.left);
|
||||||
|
|
||||||
|
if(playingWhenDragStart) {
|
||||||
|
playingWhenDragStart = false;
|
||||||
|
startPlay();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDrag(e, ui) {
|
||||||
|
updateOffsetBasedOnPosition(ui.position.left);
|
||||||
|
}
|
||||||
|
|
||||||
|
$playButton.on('click', function(e) {
|
||||||
|
startPlay();
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
$pauseButton.on('click', function(e) {
|
||||||
|
stopPlay();
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
$sliderBar.on('click', function(e) {
|
||||||
|
var offset = e.pageX - $(this).offset().left;
|
||||||
|
canUpdateBackend = true;
|
||||||
|
updateOffsetBasedOnPosition(offset);
|
||||||
|
updateSliderPosition(playbackPositionMs);
|
||||||
|
return false;
|
||||||
|
})
|
||||||
|
|
||||||
|
$slider.draggable({
|
||||||
|
axis: 'x',
|
||||||
|
containment: $sliderBar,
|
||||||
|
start: startDrag,
|
||||||
|
stop: stopDrag,
|
||||||
|
drag: onDrag
|
||||||
|
});
|
||||||
|
|
||||||
|
function update(currentTimeMs, durationTimeMs, isPlaying) {
|
||||||
|
|
||||||
|
if(dragging) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// at the end of the play, the duration sets to 0, as does currentTime. but isPlaying does not reset to
|
||||||
|
if(currentTimeMs == 0 && durationTimeMs == 0) {
|
||||||
|
if(isPlaying) {
|
||||||
|
isPlaying = false;
|
||||||
|
durationTimeMs = playbackDurationMs;
|
||||||
|
currentTimeMs = playbackDurationMs;
|
||||||
|
stopPlay();
|
||||||
|
endReached = true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateDurationTime(durationTimeMs);
|
||||||
|
updateCurrentTime(currentTimeMs);
|
||||||
|
updateIsPlaying(isPlaying);
|
||||||
|
|
||||||
|
durationChanged = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateDurationTime(timeMs) {
|
||||||
|
if(timeMs != playbackDurationMs) {
|
||||||
|
$duration.text(context.JK.prettyPrintSeconds(parseInt(timeMs / 1000)));
|
||||||
|
playbackDurationMs = timeMs;
|
||||||
|
durationChanged = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCurrentTimeText(timeMs) {
|
||||||
|
$currentTime.text(context.JK.prettyPrintSeconds(parseInt(timeMs / 1000)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSliderPosition(timeMs) {
|
||||||
|
|
||||||
|
var slideWidthPx = $sliderBar.width();
|
||||||
|
var xPos = Math.ceil(timeMs / playbackDurationMs * slideWidthPx);
|
||||||
|
$slider.css('left', xPos);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCurrentTime(timeMs) {
|
||||||
|
if(timeMs != playbackPositionMs || durationChanged) {
|
||||||
|
|
||||||
|
updateCurrentTimeText(timeMs);
|
||||||
|
updateSliderPosition(timeMs);
|
||||||
|
|
||||||
|
playbackPositionMs = timeMs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateIsPlaying(isPlaying) {
|
||||||
|
if(isPlaying != playbackPlaying) {
|
||||||
|
if(isPlaying) {
|
||||||
|
$playButton.hide();
|
||||||
|
$pauseButton.show();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$playButton.show();
|
||||||
|
$pauseButton.hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
playbackPlaying = isPlaying;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
this.update = update;
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
})(window, jQuery);
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
context.JK.RecordingFinishedDialog = function(app) {
|
context.JK.RecordingFinishedDialog = function(app) {
|
||||||
var logger = context.JK.logger;
|
var logger = context.JK.logger;
|
||||||
var rest = context.JK.Rest();
|
var rest = context.JK.Rest();
|
||||||
|
var playbackControls = null;
|
||||||
|
|
||||||
function resetForm() {
|
function resetForm() {
|
||||||
// remove all display errors
|
// remove all display errors
|
||||||
|
|
@ -130,10 +131,28 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onPause() {
|
||||||
|
logger.debug("calling jamClient.SessionStopPlay");
|
||||||
|
context.jamClient.SessionStopPlay();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPlay() {
|
||||||
|
logger.debug("calling jamClient.SessionStartPlay");
|
||||||
|
context.jamClient.SessionStartPlay();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onChangePlayPosition() {
|
||||||
|
logger.debug("calling jamClient.SessionTrackSeekMs(" + data.positionMs + ")");
|
||||||
|
context.jamClient.SessionTrackSeekMs(data.positionMs);
|
||||||
|
}
|
||||||
|
|
||||||
function registerStaticEvents() {
|
function registerStaticEvents() {
|
||||||
registerClaimRecordingHandlers(true);
|
registerClaimRecordingHandlers(true);
|
||||||
registerDiscardRecordingHandlers(true);
|
registerDiscardRecordingHandlers(true);
|
||||||
|
$(playbackControls)
|
||||||
|
.on('pause', onPause)
|
||||||
|
.on('play', onPlay)
|
||||||
|
.on('change-position', onChangePlayPosition);
|
||||||
}
|
}
|
||||||
|
|
||||||
function initialize(){
|
function initialize(){
|
||||||
|
|
@ -144,6 +163,8 @@
|
||||||
|
|
||||||
app.bindDialog('recordingFinished', dialogBindings);
|
app.bindDialog('recordingFinished', dialogBindings);
|
||||||
|
|
||||||
|
playbackControls = new context.JK.PlaybackControls($('#recording-finished-dialog .recording-controls'));
|
||||||
|
|
||||||
registerStaticEvents();
|
registerStaticEvents();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -91,7 +91,7 @@
|
||||||
var groupedTracks = groupTracksToClient(recording);
|
var groupedTracks = groupTracksToClient(recording);
|
||||||
jamClient.StartRecording(recording["id"], groupedTracks);
|
jamClient.StartRecording(recording["id"], groupedTracks);
|
||||||
})
|
})
|
||||||
.fail(function() {
|
.fail(function(jqXHR) {
|
||||||
$self.triggerHandler('startedRecording', { clientId: app.clientId, reason: 'rest', detail: arguments });
|
$self.triggerHandler('startedRecording', { clientId: app.clientId, reason: 'rest', detail: arguments });
|
||||||
currentlyRecording = false;
|
currentlyRecording = false;
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@
|
||||||
var mixers = [];
|
var mixers = [];
|
||||||
var configureTrackDialog;
|
var configureTrackDialog;
|
||||||
var addNewGearDialog;
|
var addNewGearDialog;
|
||||||
|
var localRecordingsDialog = null;
|
||||||
var screenActive = false;
|
var screenActive = false;
|
||||||
var currentMixerRangeMin = null;
|
var currentMixerRangeMin = null;
|
||||||
var currentMixerRangeMax = null;
|
var currentMixerRangeMax = null;
|
||||||
|
|
@ -22,6 +23,9 @@
|
||||||
var recordingTimerInterval = null;
|
var recordingTimerInterval = null;
|
||||||
var startTimeDate = null;
|
var startTimeDate = null;
|
||||||
var startingRecording = false; // double-click guard
|
var startingRecording = false; // double-click guard
|
||||||
|
var claimedRecording = null;
|
||||||
|
var playbackControls = null;
|
||||||
|
var monitorPlaybackTimeout = null;
|
||||||
|
|
||||||
|
|
||||||
var rest = JK.Rest();
|
var rest = JK.Rest();
|
||||||
|
|
@ -242,6 +246,10 @@
|
||||||
else if(data.reason == 'recording-engine-sample-rate') {
|
else if(data.reason == 'recording-engine-sample-rate') {
|
||||||
notifyWithUserInfo(title, 'had a problem recording at the specified sample rate.', detail);
|
notifyWithUserInfo(title, 'had a problem recording at the specified sample rate.', detail);
|
||||||
}
|
}
|
||||||
|
else if(data.reason == 'rest') {
|
||||||
|
var jqXHR = detail[0];
|
||||||
|
app.notifyServerError(jqXHR);
|
||||||
|
}
|
||||||
else {
|
else {
|
||||||
notifyWithUserInfo(title, 'Error Reason: ' + reason);
|
notifyWithUserInfo(title, 'Error Reason: ' + reason);
|
||||||
}
|
}
|
||||||
|
|
@ -356,8 +364,36 @@
|
||||||
.fail(app.ajaxError);
|
.fail(app.ajaxError);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function monitorRecordingPlayback() {
|
||||||
|
var isPlaying = context.jamClient.isSessionTrackPlaying();
|
||||||
|
var positionMs = context.jamClient.SessionCurrrentPlayPosMs();
|
||||||
|
var durationMs = context.jamClient.SessionGetTracksPlayDurationMs();
|
||||||
|
|
||||||
|
playbackControls.update(positionMs, durationMs, isPlaying);
|
||||||
|
|
||||||
|
monitorPlaybackTimeout = setTimeout(monitorRecordingPlayback, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleTransitionsInRecordingPlayback() {
|
||||||
|
// let's see if we detect a transition to start playback or stop playback
|
||||||
|
|
||||||
|
var currentSession = sessionModel.getCurrentSession();
|
||||||
|
|
||||||
|
if(claimedRecording == null && (currentSession && currentSession.claimed_recording != null)) {
|
||||||
|
// this is a 'started with a claimed_recording' transition.
|
||||||
|
// we need to start a timer to watch for the state of the play session
|
||||||
|
monitorRecordingPlayback();
|
||||||
|
}
|
||||||
|
else if(claimedRecording && (currentSession == null || currentSession.claimed_recording == null)) {
|
||||||
|
clearTimeout(monitorPlaybackTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
claimedRecording = currentSession == null ? null : currentSession.claimed_recording;
|
||||||
|
|
||||||
|
}
|
||||||
function sessionChanged() {
|
function sessionChanged() {
|
||||||
|
|
||||||
|
handleTransitionsInRecordingPlayback();
|
||||||
// TODO - in the specific case of a user changing their tracks using the configureTrack dialog,
|
// TODO - in the specific case of a user changing their tracks using the configureTrack dialog,
|
||||||
// this event appears to fire before the underlying mixers have updated. I have no event to
|
// this event appears to fire before the underlying mixers have updated. I have no event to
|
||||||
// know definitively when the underlying mixers are up to date, so for now, we just delay slightly.
|
// know definitively when the underlying mixers are up to date, so for now, we just delay slightly.
|
||||||
|
|
@ -387,6 +423,7 @@
|
||||||
$voiceChat.hide();
|
$voiceChat.hide();
|
||||||
_updateMixers();
|
_updateMixers();
|
||||||
_renderTracks();
|
_renderTracks();
|
||||||
|
_renderLocalMediaTracks();
|
||||||
_wireTopVolume();
|
_wireTopVolume();
|
||||||
_wireTopMix();
|
_wireTopMix();
|
||||||
_addVoiceChat();
|
_addVoiceChat();
|
||||||
|
|
@ -394,6 +431,11 @@
|
||||||
if ($('.session-livetracks .track').length === 0) {
|
if ($('.session-livetracks .track').length === 0) {
|
||||||
$('.session-livetracks .when-empty').show();
|
$('.session-livetracks .when-empty').show();
|
||||||
}
|
}
|
||||||
|
if ($('.session-recordings .track').length === 0) {
|
||||||
|
$('.session-recordings .when-empty').show();
|
||||||
|
$('.session-recording-name-wrapper').hide();
|
||||||
|
$('.recording-controls').hide();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function _initDialogs() {
|
function _initDialogs() {
|
||||||
|
|
@ -406,6 +448,7 @@
|
||||||
var mixerIds = context.jamClient.SessionGetIDs();
|
var mixerIds = context.jamClient.SessionGetIDs();
|
||||||
var holder = $.extend(true, {}, {mixers: context.jamClient.SessionGetControlState(mixerIds)});
|
var holder = $.extend(true, {}, {mixers: context.jamClient.SessionGetControlState(mixerIds)});
|
||||||
mixers = holder.mixers;
|
mixers = holder.mixers;
|
||||||
|
|
||||||
// Always add a hard-coded simplified 'mixer' for the L2M mix
|
// Always add a hard-coded simplified 'mixer' for the L2M mix
|
||||||
var l2m_mixer = {
|
var l2m_mixer = {
|
||||||
id: '__L2M__',
|
id: '__L2M__',
|
||||||
|
|
@ -416,6 +459,17 @@
|
||||||
mixers.push(l2m_mixer);
|
mixers.push(l2m_mixer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _mixersForGroupId(groupId) {
|
||||||
|
var foundMixers = [];
|
||||||
|
$.each(mixers, function(index, mixer) {
|
||||||
|
if (mixer.group_id === groupId) {
|
||||||
|
foundMixers.push(mixer);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
return foundMixers;
|
||||||
|
}
|
||||||
|
|
||||||
// TODO FIXME - This needs to support multiple tracks for an individual
|
// TODO FIXME - This needs to support multiple tracks for an individual
|
||||||
// client id and group.
|
// client id and group.
|
||||||
function _mixerForClientId(clientId, groupIds, usedMixers) {
|
function _mixerForClientId(clientId, groupIds, usedMixers) {
|
||||||
|
|
@ -538,6 +592,113 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _renderLocalMediaTracks() {
|
||||||
|
var localMediaMixers = _mixersForGroupId(ChannelGroupIds.MediaTrackGroup);
|
||||||
|
if(localMediaMixers.length == 0) {
|
||||||
|
localMediaMixers = _mixersForGroupId(ChannelGroupIds.PeerMediaTrackGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
var recordedTracks = sessionModel.recordedTracks();
|
||||||
|
|
||||||
|
console.log("recorded tracks=%o local_media_mixers=%o", recordedTracks, localMediaMixers);
|
||||||
|
|
||||||
|
if(recordedTracks && localMediaMixers.length == 0) {
|
||||||
|
// if we are the creator, then rather than raise an error, tell the server the recording is over.
|
||||||
|
// this shoudl only happen if we get temporarily disconnected by forced reload, which isn't a very normal scenario
|
||||||
|
if(sessionModel.getCurrentSession().claimed_recording_initiator_id == context.JK.userMe.id) {
|
||||||
|
closeRecording();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(recordedTracks) {
|
||||||
|
|
||||||
|
$('.session-recording-name').text(sessionModel.getCurrentSession().claimed_recording.name);
|
||||||
|
|
||||||
|
var noCorrespondingTracks = false;
|
||||||
|
$.each(localMediaMixers, function(index, mixer) {
|
||||||
|
var preMasteredClass = "";
|
||||||
|
// find the track or tracks that correspond to the mixer
|
||||||
|
var correspondingTracks = []
|
||||||
|
$.each(recordedTracks, function(i, recordedTrack) {
|
||||||
|
if(mixer.id.indexOf("L") == 0) {
|
||||||
|
if(mixer.id.substring(1) == recordedTrack.client_track_id) {
|
||||||
|
correspondingTracks.push(recordedTrack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if(mixer.id.indexOf("C") == 0) {
|
||||||
|
if(mixer.id.substring(1) == recordedTrack.client_id) {
|
||||||
|
correspondingTracks.push(recordedTrack);
|
||||||
|
preMasteredClass = "pre-mastered-track";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// this should not be possible
|
||||||
|
alert("Invalid state: the recorded track had neither persisted_track_id or persisted_client_id");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if(correspondingTracks.length == 0) {
|
||||||
|
noCorrespondingTracks = true;
|
||||||
|
app.notify({
|
||||||
|
title: "Unable to Open Recording",
|
||||||
|
text: "Could not correlate server and client tracks",
|
||||||
|
icon_url: "/assets/content/icon_alert_big.png"});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// prune found recorded tracks
|
||||||
|
recordedTracks = $.grep(recordedTracks, function(value) {
|
||||||
|
return $.inArray(value, correspondingTracks) < 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
var oneOfTheTracks = correspondingTracks[0];
|
||||||
|
var instrumentIcon = context.JK.getInstrumentIcon45(oneOfTheTracks.instrument_id);
|
||||||
|
var photoUrl = "/assets/content/icon_recording.png";
|
||||||
|
|
||||||
|
var name = oneOfTheTracks.user.name;
|
||||||
|
if (!(name)) {
|
||||||
|
name = oneOfTheTracks.user.first_name + ' ' + oneOfTheTracks.user.last_name;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Default trackData to participant + no Mixer state.
|
||||||
|
var trackData = {
|
||||||
|
trackId: oneOfTheTracks.id,
|
||||||
|
clientId: oneOfTheTracks.client_id,
|
||||||
|
name: name,
|
||||||
|
instrumentIcon: instrumentIcon,
|
||||||
|
avatar: photoUrl,
|
||||||
|
latency: "good",
|
||||||
|
gainPercent: 0,
|
||||||
|
muteClass: 'muted',
|
||||||
|
mixerId: "",
|
||||||
|
avatarClass : 'avatar-recording',
|
||||||
|
preMasteredClass: preMasteredClass
|
||||||
|
};
|
||||||
|
|
||||||
|
var gainPercent = percentFromMixerValue(
|
||||||
|
mixer.range_low, mixer.range_high, mixer.volume_left);
|
||||||
|
var muteClass = "enabled";
|
||||||
|
if (mixer.mute) {
|
||||||
|
muteClass = "muted";
|
||||||
|
}
|
||||||
|
trackData.gainPercent = gainPercent;
|
||||||
|
trackData.muteClass = muteClass;
|
||||||
|
trackData.mixerId = mixer.id;
|
||||||
|
|
||||||
|
_addMediaTrack(index, trackData);
|
||||||
|
});
|
||||||
|
|
||||||
|
if(!noCorrespondingTracks && recordedTracks.length > 0) {
|
||||||
|
logger.error("unable to find all recorded tracks against client tracks");
|
||||||
|
app.notify({title:"All tracks not found",
|
||||||
|
text: "Some tracks in the recording are not present in the playback",
|
||||||
|
icon_url: "/assets/content/icon_alert_big.png"})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function _renderTracks() {
|
function _renderTracks() {
|
||||||
myTracks = [];
|
myTracks = [];
|
||||||
|
|
||||||
|
|
@ -572,7 +733,9 @@
|
||||||
latency: "good",
|
latency: "good",
|
||||||
gainPercent: 0,
|
gainPercent: 0,
|
||||||
muteClass: 'muted',
|
muteClass: 'muted',
|
||||||
mixerId: ""
|
mixerId: "",
|
||||||
|
avatarClass: 'avatar-med',
|
||||||
|
preMasteredClass: ""
|
||||||
};
|
};
|
||||||
|
|
||||||
// This is the likely cause of multi-track problems.
|
// This is the likely cause of multi-track problems.
|
||||||
|
|
@ -728,7 +891,7 @@
|
||||||
$('.session-livetracks .when-empty').hide();
|
$('.session-livetracks .when-empty').hide();
|
||||||
}
|
}
|
||||||
var template = $('#template-session-track').html();
|
var template = $('#template-session-track').html();
|
||||||
var newTrack = context.JK.fillTemplate(template, trackData);
|
var newTrack = $(context.JK.fillTemplate(template, trackData));
|
||||||
$destination.append(newTrack);
|
$destination.append(newTrack);
|
||||||
|
|
||||||
// Render VU meters and gain fader
|
// Render VU meters and gain fader
|
||||||
|
|
@ -747,6 +910,32 @@
|
||||||
tracks[trackData.trackId] = new context.JK.SessionTrack(trackData.clientId);
|
tracks[trackData.trackId] = new context.JK.SessionTrack(trackData.clientId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function _addMediaTrack(index, trackData) {
|
||||||
|
var parentSelector = '#session-recordedtracks-container';
|
||||||
|
var $destination = $(parentSelector);
|
||||||
|
$('.session-recordings .when-empty').hide();
|
||||||
|
$('.session-recording-name-wrapper').show();
|
||||||
|
$('.recording-controls').show();
|
||||||
|
|
||||||
|
var template = $('#template-session-track').html();
|
||||||
|
var newTrack = $(context.JK.fillTemplate(template, trackData));
|
||||||
|
$destination.append(newTrack);
|
||||||
|
if(trackData.preMasteredClass) {
|
||||||
|
context.JK.helpBubble($('.track-instrument', newTrack), 'pre-processed-track', {}, {offsetParent: newTrack.closest('.content-body')});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render VU meters and gain fader
|
||||||
|
var trackSelector = parentSelector + ' .session-track[track-id="' + trackData.trackId + '"]';
|
||||||
|
var gainPercent = trackData.gainPercent || 0;
|
||||||
|
connectTrackToMixer(trackSelector, trackData.clientId, trackData.mixerId, gainPercent);
|
||||||
|
|
||||||
|
tracks[trackData.trackId] = new context.JK.SessionTrack(trackData.clientId);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Will be called when fader changes. The fader id (provided at subscribe time),
|
* Will be called when fader changes. The fader id (provided at subscribe time),
|
||||||
* the new value (0-100) and whether the fader is still being dragged are passed.
|
* the new value (0-100) and whether the fader is still being dragged are passed.
|
||||||
|
|
@ -1063,6 +1252,57 @@
|
||||||
.fail(app.ajaxError);
|
.fail(app.ajaxError);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openRecording(e) {
|
||||||
|
// just ignore the click if they are currently recording for now
|
||||||
|
if(sessionModel.recordingModel.isRecording()) {
|
||||||
|
app.notify({
|
||||||
|
"title": "Currently Recording",
|
||||||
|
"text": "You can't open a recording while creating a recording.",
|
||||||
|
"icon_url": "/assets/content/icon_alert_big.png"
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!localRecordingsDialog.isShowing()) {
|
||||||
|
app.layout.showDialog('localRecordings');
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeRecording() {
|
||||||
|
rest.stopPlayClaimedRecording({id: sessionModel.id(), claimed_recording_id: sessionModel.getCurrentSession().claimed_recording.id})
|
||||||
|
.done(function() {
|
||||||
|
sessionModel.refreshCurrentSession();
|
||||||
|
})
|
||||||
|
.fail(function(jqXHR) {
|
||||||
|
app.notify({
|
||||||
|
"title": "Couldn't Stop Recording Playback",
|
||||||
|
"text": "Couldn't inform the server to stop playback. msg=" + jqXHR.responseText,
|
||||||
|
"icon_url": "/assets/content/icon_alert_big.png"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
context.jamClient.CloseRecording();
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPause() {
|
||||||
|
logger.debug("calling jamClient.SessionStopPlay");
|
||||||
|
context.jamClient.SessionStopPlay();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPlay() {
|
||||||
|
logger.debug("calling jamClient.SessionStartPlay");
|
||||||
|
context.jamClient.SessionStartPlay();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onChangePlayPosition(e, data){
|
||||||
|
logger.debug("calling jamClient.SessionTrackSeekMs(" + data.positionMs + ")");
|
||||||
|
context.jamClient.SessionTrackSeekMs(data.positionMs);
|
||||||
|
}
|
||||||
|
|
||||||
function startStopRecording() {
|
function startStopRecording() {
|
||||||
if(sessionModel.recordingModel.isRecording()) {
|
if(sessionModel.recordingModel.isRecording()) {
|
||||||
sessionModel.recordingModel.stopRecording();
|
sessionModel.recordingModel.stopRecording();
|
||||||
|
|
@ -1072,21 +1312,31 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function events() {
|
function events() {
|
||||||
$('#session-resync').on('click', sessionResync);
|
$('#session-resync').on('click', sessionResync);
|
||||||
$('#session-contents').on("click", '[action="delete"]', deleteSession);
|
$('#session-contents').on("click", '[action="delete"]', deleteSession);
|
||||||
$('#tracks').on('click', 'div[control="mute"]', toggleMute);
|
$('#tracks').on('click', 'div[control="mute"]', toggleMute);
|
||||||
$('#recording-start-stop').on('click', startStopRecording)
|
$('#recording-start-stop').on('click', startStopRecording);
|
||||||
|
$('#open-a-recording').on('click', openRecording);
|
||||||
$('#track-settings').click(function() {
|
$('#track-settings').click(function() {
|
||||||
configureTrackDialog.showVoiceChatPanel(true);
|
configureTrackDialog.showVoiceChatPanel(true);
|
||||||
configureTrackDialog.showMusicAudioPanel(true);
|
configureTrackDialog.showMusicAudioPanel(true);
|
||||||
});
|
});
|
||||||
|
$('#close-playback-recording').on('click', closeRecording);
|
||||||
|
$(playbackControls)
|
||||||
|
.on('pause', onPause)
|
||||||
|
.on('play', onPlay)
|
||||||
|
.on('change-position', onChangePlayPosition);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.initialize = function() {
|
this.initialize = function(localRecordingsDialogInstance) {
|
||||||
|
localRecordingsDialog = localRecordingsDialogInstance;
|
||||||
context.jamClient.SetVURefreshRate(150);
|
context.jamClient.SetVURefreshRate(150);
|
||||||
|
playbackControls = new context.JK.PlaybackControls($('.session-recordings .recording-controls'));
|
||||||
events();
|
events();
|
||||||
|
|
||||||
|
|
||||||
var screenBindings = {
|
var screenBindings = {
|
||||||
'beforeShow': beforeShow,
|
'beforeShow': beforeShow,
|
||||||
'afterShow': afterShow,
|
'afterShow': afterShow,
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,21 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isPlayingRecording() {
|
||||||
|
// this is the server's state; there is no guarantee that the local tracks
|
||||||
|
// requested from the backend will have corresponding track information
|
||||||
|
return currentSession && currentSession.claimed_recording;
|
||||||
|
}
|
||||||
|
|
||||||
|
function recordedTracks() {
|
||||||
|
if(currentSession && currentSession.claimed_recording) {
|
||||||
|
return currentSession.claimed_recording.recorded_tracks
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function creatorId() {
|
function creatorId() {
|
||||||
if(!currentSession) {
|
if(!currentSession) {
|
||||||
throw "creator is not known"
|
throw "creator is not known"
|
||||||
|
|
@ -454,12 +469,14 @@
|
||||||
|
|
||||||
// Public interface
|
// Public interface
|
||||||
this.id = id;
|
this.id = id;
|
||||||
|
this.recordedTracks = recordedTracks;
|
||||||
this.participants = participants;
|
this.participants = participants;
|
||||||
this.joinSession = joinSession;
|
this.joinSession = joinSession;
|
||||||
this.leaveCurrentSession = leaveCurrentSession;
|
this.leaveCurrentSession = leaveCurrentSession;
|
||||||
this.refreshCurrentSession = refreshCurrentSession;
|
this.refreshCurrentSession = refreshCurrentSession;
|
||||||
this.subscribe = subscribe;
|
this.subscribe = subscribe;
|
||||||
this.participantForClientId = participantForClientId;
|
this.participantForClientId = participantForClientId;
|
||||||
|
this.isPlayingRecording = isPlayingRecording;
|
||||||
this.addTrack = addTrack;
|
this.addTrack = addTrack;
|
||||||
this.updateTrack = updateTrack;
|
this.updateTrack = updateTrack;
|
||||||
this.deleteTrack = deleteTrack;
|
this.deleteTrack = deleteTrack;
|
||||||
|
|
|
||||||
|
|
@ -131,7 +131,7 @@
|
||||||
notificationId: val.notification_id,
|
notificationId: val.notification_id,
|
||||||
avatar_url: context.JK.resolveAvatarUrl(val.photo_url),
|
avatar_url: context.JK.resolveAvatarUrl(val.photo_url),
|
||||||
text: val.formatted_msg,
|
text: val.formatted_msg,
|
||||||
date: context.JK.formatDate(val.created_at)
|
date: context.JK.formatDateTime(val.created_at)
|
||||||
});
|
});
|
||||||
|
|
||||||
$('#sidebar-notification-list').append(notificationHtml);
|
$('#sidebar-notification-list').append(notificationHtml);
|
||||||
|
|
@ -362,7 +362,7 @@
|
||||||
notificationId: payload.notification_id,
|
notificationId: payload.notification_id,
|
||||||
avatar_url: context.JK.resolveAvatarUrl(payload.photo_url),
|
avatar_url: context.JK.resolveAvatarUrl(payload.photo_url),
|
||||||
text: sidebarText,
|
text: sidebarText,
|
||||||
date: context.JK.formatDate(payload.created_at)
|
date: context.JK.formatDateTime(payload.created_at)
|
||||||
});
|
});
|
||||||
|
|
||||||
$('#sidebar-notification-list').prepend(notificationHtml);
|
$('#sidebar-notification-list').prepend(notificationHtml);
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,14 @@
|
||||||
context.JK = context.JK || {};
|
context.JK = context.JK || {};
|
||||||
var logger = context.JK.logger;
|
var logger = context.JK.logger;
|
||||||
|
|
||||||
|
var days = new Array("Sun", "Mon", "Tue",
|
||||||
|
"Wed", "Thu", "Fri", "Sat");
|
||||||
|
|
||||||
|
var months = new Array("January", "February", "March",
|
||||||
|
"April", "May", "June", "July", "August", "September",
|
||||||
|
"October", "November", "December");
|
||||||
|
|
||||||
|
|
||||||
context.JK.stringToBool = function(s) {
|
context.JK.stringToBool = function(s) {
|
||||||
switch(s.toLowerCase()){
|
switch(s.toLowerCase()){
|
||||||
case "true": case "yes": case "1": return true;
|
case "true": case "yes": case "1": return true;
|
||||||
|
|
@ -70,6 +78,69 @@
|
||||||
instrumentIconMap45[instrumentId] = "../assets/content/icon_instrument_" + icon + "45.png";
|
instrumentIconMap45[instrumentId] = "../assets/content/icon_instrument_" + icon + "45.png";
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Associates a help bubble on hover (by default) with the specified $element, using jquery.bt.js (BeautyTips)
|
||||||
|
* @param $element The element that should show the help when hovered
|
||||||
|
* @param templateName the name of the help template (without the '#template-help' prefix). Add to _help.html.erb
|
||||||
|
* @param data (optional) data for your template, if applicable
|
||||||
|
* @param options (optional) You can override the default BeautyTips options: https://github.com/dillon-sellars/BeautyTips
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
context.JK.helpBubble = function($element, templateName, data, options) {
|
||||||
|
if(!data) {
|
||||||
|
data = {}
|
||||||
|
}
|
||||||
|
var helpText = context._.template($('#template-help-' + templateName).html(), data, { variable: 'data' });
|
||||||
|
|
||||||
|
var holder = $('<div class="hover-bubble help-bubble"></div>');
|
||||||
|
holder.append(helpText);
|
||||||
|
|
||||||
|
context.JK.hoverBubble($element, helpText, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Associates a bubble on hover (by default) with the specified $element, using jquery.bt.js (BeautyTips)
|
||||||
|
* @param $element The element that should show the bubble when hovered
|
||||||
|
* @param text the text or jquery element that should be shown as contents of the bubble
|
||||||
|
* @param options (optional) You can override the default BeautyTips options: https://github.com/dillon-sellars/BeautyTips
|
||||||
|
*/
|
||||||
|
context.JK.hoverBubble = function($element, text, options) {
|
||||||
|
if(!text) {
|
||||||
|
logger.error("hoverBubble: no text to attach to $element %o", $element);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if($element instanceof jQuery) {
|
||||||
|
if ($element.length == 0) {
|
||||||
|
logger.error("hoverBubble: no element specified with text %o", text);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultOpts = {
|
||||||
|
fill: '#333',
|
||||||
|
strokeStyle: '#ED3618',
|
||||||
|
spikeLength: 10,
|
||||||
|
spikeGirth: 10,
|
||||||
|
padding: 8,
|
||||||
|
cornerRadius: 0,
|
||||||
|
cssStyles: {
|
||||||
|
fontFamily: 'Raleway, Arial, Helvetica, sans-serif',
|
||||||
|
fontSize: '11px',
|
||||||
|
color:'white',
|
||||||
|
whiteSpace:'normal'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if(options) {
|
||||||
|
options = $.extend(false, defaultOpts, options);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
options = defaultOpts;
|
||||||
|
}
|
||||||
|
|
||||||
|
$element.bt(text, options);
|
||||||
|
}
|
||||||
// Uber-simple templating
|
// Uber-simple templating
|
||||||
// var template = "Hey {name}";
|
// var template = "Hey {name}";
|
||||||
// var vals = { name: "Jon" };
|
// var vals = { name: "Jon" };
|
||||||
|
|
@ -175,11 +246,46 @@
|
||||||
return retVal;
|
return retVal;
|
||||||
}
|
}
|
||||||
|
|
||||||
context.JK.formatDate = function(dateString) {
|
context.JK.formatDateTime = function(dateString) {
|
||||||
var date = new Date(dateString);
|
var date = new Date(dateString);
|
||||||
return date.getFullYear() + "-" + context.JK.padString(date.getMonth()+1, 2) + "-" + context.JK.padString(date.getDate(), 2) + " @ " + date.toLocaleTimeString();
|
return date.getFullYear() + "-" + context.JK.padString(date.getMonth()+1, 2) + "-" + context.JK.padString(date.getDate(), 2) + " @ " + date.toLocaleTimeString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// returns Fri May 20, 2013
|
||||||
|
context.JK.formatDate = function(dateString) {
|
||||||
|
var date = new Date(dateString);
|
||||||
|
|
||||||
|
return days[date.getDay()] + ' ' + months[date.getMonth()] + ' ' + context.JK.padString(date.getDate(), 2) + ', ' + date.getFullYear();
|
||||||
|
}
|
||||||
|
|
||||||
|
context.JK.formatTime = function(dateString) {
|
||||||
|
var date = new Date(dateString);
|
||||||
|
return date.toLocaleTimeString();
|
||||||
|
}
|
||||||
|
|
||||||
|
context.JK.prettyPrintSeconds = function(seconds) {
|
||||||
|
// from: http://stackoverflow.com/questions/3733227/javascript-seconds-to-minutes-and-seconds
|
||||||
|
|
||||||
|
// Minutes and seconds
|
||||||
|
var mins = ~~(seconds / 60);
|
||||||
|
var secs = seconds % 60;
|
||||||
|
|
||||||
|
// Hours, minutes and seconds
|
||||||
|
var hrs = ~~(seconds / 3600);
|
||||||
|
var mins = ~~((seconds % 3600) / 60);
|
||||||
|
var secs = seconds % 60;
|
||||||
|
|
||||||
|
// Output like "1:01" or "4:03:59" or "123:03:59"
|
||||||
|
var ret = "";
|
||||||
|
|
||||||
|
if (hrs > 0)
|
||||||
|
ret += "" + hrs + ":" + (mins < 10 ? "0" : "");
|
||||||
|
|
||||||
|
ret += "" + mins + ":" + (secs < 10 ? "0" : "");
|
||||||
|
ret += "" + secs;
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
context.JK.search = function(query, app, callback) {
|
context.JK.search = function(query, app, callback) {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
type: "GET",
|
type: "GET",
|
||||||
|
|
@ -279,6 +385,22 @@
|
||||||
return ul;
|
return ul;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
context.JK.format_all_errors = function(errors_data) {
|
||||||
|
var errors = errors_data["errors"];
|
||||||
|
if(errors == null) return $('<ul class="error-text"><li>unknown error</li></ul>');
|
||||||
|
|
||||||
|
var ul = $('<ul class="error-text"></ul>');
|
||||||
|
|
||||||
|
$.each(errors, function(fieldName, field_errors) {
|
||||||
|
|
||||||
|
$.each(field_errors, function(index, item) {
|
||||||
|
ul.append(context.JK.fillTemplate("<li>{field} {message}</li>", {field: fieldName, message: item}))
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return ul;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Way to verify that a number of parallel tasks have all completed.
|
* Way to verify that a number of parallel tasks have all completed.
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,10 @@
|
||||||
*
|
*
|
||||||
*= require_self
|
*= require_self
|
||||||
*= require ./ie
|
*= require ./ie
|
||||||
|
*= require jquery.bt
|
||||||
*= require ./jamkazam
|
*= require ./jamkazam
|
||||||
*= require ./content
|
*= require ./content
|
||||||
|
*= require ./paginator
|
||||||
*= require ./faders
|
*= require ./faders
|
||||||
*= require ./header
|
*= require ./header
|
||||||
#= require ./user_dropdown
|
#= require ./user_dropdown
|
||||||
|
|
@ -30,6 +32,7 @@
|
||||||
*= require ./ftue
|
*= require ./ftue
|
||||||
*= require ./invitationDialog
|
*= require ./invitationDialog
|
||||||
*= require ./recordingFinishedDialog
|
*= require ./recordingFinishedDialog
|
||||||
|
*= require ./localRecordingsDialog
|
||||||
*= require ./createSession
|
*= require ./createSession
|
||||||
*= require ./genreSelector
|
*= require ./genreSelector
|
||||||
*= require ./sessionList
|
*= require ./sessionList
|
||||||
|
|
|
||||||
|
|
@ -341,6 +341,8 @@ ul.shortcuts {
|
||||||
border-color:#ED3618;
|
border-color:#ED3618;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
span.arrow-right {
|
span.arrow-right {
|
||||||
display:inline-block;
|
display:inline-block;
|
||||||
width: 0;
|
width: 0;
|
||||||
|
|
|
||||||
|
|
@ -460,4 +460,4 @@ div[layout-id=session], div[layout-id=ftue], .no-selection-range {
|
||||||
-moz-user-select: none;
|
-moz-user-select: none;
|
||||||
-ms-user-select: none;
|
-ms-user-select: none;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
#local-recordings-dialog {
|
||||||
|
table.local-recordings {
|
||||||
|
tbody {
|
||||||
|
tr:hover {
|
||||||
|
background-color: #400606;
|
||||||
|
cursor:pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr[data-local-state=MISSING], tr[data-local-state=PARTIALLY_MISSING] {
|
||||||
|
background-color:#777;
|
||||||
|
color:#aaa;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
|
||||||
|
div.paginator {
|
||||||
|
.arrow-right {
|
||||||
|
display:inline-block;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-top: 4px solid transparent;
|
||||||
|
border-bottom: 4px solid transparent;
|
||||||
|
border-left: 4px solid #FFCC00;
|
||||||
|
padding-left:5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
span.arrow-right {
|
||||||
|
border-left: 4px solid #aaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow-left {
|
||||||
|
display:inline-block;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-top: 4px solid transparent;
|
||||||
|
border-bottom: 4px solid transparent;
|
||||||
|
border-right: 4px solid #FFCC00;
|
||||||
|
padding-right:5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
span.arrow-left {
|
||||||
|
border-right: 4px solid #aaa;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -232,14 +232,28 @@ table.vu td {
|
||||||
|
|
||||||
.session-recording-name-wrapper {
|
.session-recording-name-wrapper {
|
||||||
position:relative;
|
position:relative;
|
||||||
white-space:nowrap;
|
white-space:nowrap;
|
||||||
|
display:none;
|
||||||
|
|
||||||
|
.session-add {
|
||||||
|
margin-top:9px;
|
||||||
|
}
|
||||||
|
.session-add a {
|
||||||
|
vertical-align:top;
|
||||||
|
outline:none;
|
||||||
|
|
||||||
|
img {
|
||||||
|
margin-top:-3px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.session-recording-name {
|
.session-recording-name {
|
||||||
width:60%;
|
width:60%;
|
||||||
overflow:hidden;
|
overflow:hidden;
|
||||||
margin-top:6px;
|
margin-top:9px;
|
||||||
margin-bottom:8px;
|
margin-bottom:8px;
|
||||||
font-size:16px;
|
font-size:16px;
|
||||||
}
|
}
|
||||||
|
|
@ -477,6 +491,28 @@ table.vu td {
|
||||||
background-color:#666;
|
background-color:#666;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.session-recordings {
|
||||||
|
.track-connection {
|
||||||
|
display:none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-close {
|
||||||
|
display:none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.recording-controls {
|
||||||
|
display:none;
|
||||||
|
|
||||||
|
.play-button {
|
||||||
|
outline:none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.play-button img.pausebutton {
|
||||||
|
display:none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.voicechat {
|
.voicechat {
|
||||||
margin-top:10px;
|
margin-top:10px;
|
||||||
|
|
@ -571,7 +607,7 @@ table.vu td {
|
||||||
width:93%;
|
width:93%;
|
||||||
min-width:200px;
|
min-width:200px;
|
||||||
background-color:#242323;
|
background-color:#242323;
|
||||||
position:relative;
|
position:absolute;
|
||||||
font-size:13px;
|
font-size:13px;
|
||||||
text-align:center;
|
text-align:center;
|
||||||
}
|
}
|
||||||
|
|
@ -593,6 +629,10 @@ table.vu td {
|
||||||
margin-top:4px;
|
margin-top:4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.recording-time.duration-time {
|
||||||
|
padding-left:2px;
|
||||||
|
}
|
||||||
|
|
||||||
.recording-playback {
|
.recording-playback {
|
||||||
display:inline-block;
|
display:inline-block;
|
||||||
background-image:url(/assets/content/bkg_playcontrols.png);
|
background-image:url(/assets/content/bkg_playcontrols.png);
|
||||||
|
|
@ -601,12 +641,17 @@ table.vu td {
|
||||||
width:65%;
|
width:65%;
|
||||||
height:16px;
|
height:16px;
|
||||||
margin-top:2px;
|
margin-top:2px;
|
||||||
|
cursor:pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.recording-slider {
|
.recording-slider {
|
||||||
position:absolute;
|
position:absolute;
|
||||||
left:40px;
|
left:0px;
|
||||||
top:0px;
|
top:0px;
|
||||||
|
|
||||||
|
img {
|
||||||
|
position:absolute;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.recording-current {
|
.recording-current {
|
||||||
|
|
|
||||||
|
|
@ -1,49 +1,49 @@
|
||||||
table.findsession-table {
|
table.findsession-table, table.local-recordings {
|
||||||
margin-top:6px;
|
margin-top:6px;
|
||||||
width:98%;
|
width:98%;
|
||||||
font-size:11px;
|
font-size:11px;
|
||||||
color:#fff;
|
color:#fff;
|
||||||
background-color:#262626;
|
background-color:#262626;
|
||||||
border:solid 1px #4d4d4d;
|
border:solid 1px #4d4d4d;
|
||||||
}
|
|
||||||
|
|
||||||
.findsession-table th {
|
th {
|
||||||
font-weight:300;
|
font-weight:300;
|
||||||
background-color:#4d4d4d;
|
background-color:#4d4d4d;
|
||||||
padding:6px;
|
padding:6px;
|
||||||
border-right:solid 1px #333;
|
border-right:solid 1px #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
.findsession-table td {
|
td {
|
||||||
padding:9px 5px 5px 5px;
|
padding:9px 5px 5px 5px;
|
||||||
border-right:solid 1px #333;
|
border-right:solid 1px #333;
|
||||||
border-top:solid 1px #333;
|
border-top:solid 1px #333;
|
||||||
vertical-align:top;
|
vertical-align:top;
|
||||||
white-space:normal;
|
white-space:normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
.findsession-table .noborder {
|
.noborder {
|
||||||
border-right:none;
|
border-right:none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.findsession-table .musicians {
|
.musicians {
|
||||||
margin-top:-3px;
|
margin-top:-3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.findsession-table .musicians td {
|
.musicians td {
|
||||||
border-right:none;
|
border-right:none;
|
||||||
border-top:none;
|
border-top:none;
|
||||||
padding:3px;
|
padding:3px;
|
||||||
vertical-align:middle;
|
vertical-align:middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
.findsession-table a {
|
a {
|
||||||
color:#fff;
|
color:#fff;
|
||||||
text-decoration:none;
|
text-decoration:none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.findsession-table a:hover {
|
a:hover {
|
||||||
color:#227985;
|
color:#227985;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.latency-grey {
|
.latency-grey {
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,8 @@ class ApiClaimedRecordingsController < ApiController
|
||||||
respond_to :json
|
respond_to :json
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@claimed_recordings = ClaimedRecording.where(:user_id => current_user.id).order("created_at DESC").paginate(page: params[:page])
|
@claimed_recordings = ClaimedRecording.where(:user_id => current_user.id).order("created_at DESC").paginate(page: params[:page], per_page: params[:per_page])
|
||||||
|
response.headers['total-entries'] = @claimed_recordings.total_entries.to_s
|
||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ class ApiMusicSessionsController < ApiController
|
||||||
|
|
||||||
# have to be signed in currently to see this screen
|
# have to be signed in currently to see this screen
|
||||||
before_filter :api_signed_in_user
|
before_filter :api_signed_in_user
|
||||||
before_filter :lookup_session, only: [:show, :update, :delete]
|
before_filter :lookup_session, only: [:show, :update, :delete, :claimed_recording_start, :claimed_recording_stop]
|
||||||
skip_before_filter :api_signed_in_user, only: [:perf_upload]
|
skip_before_filter :api_signed_in_user, only: [:perf_upload]
|
||||||
|
|
||||||
respond_to :json
|
respond_to :json
|
||||||
|
|
@ -122,10 +122,6 @@ class ApiMusicSessionsController < ApiController
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def lookup_session
|
|
||||||
@music_session = MusicSession.find(params[:id])
|
|
||||||
end
|
|
||||||
|
|
||||||
def track_index
|
def track_index
|
||||||
@tracks = Track.index(current_user, params[:id])
|
@tracks = Track.index(current_user, params[:id])
|
||||||
end
|
end
|
||||||
|
|
@ -235,8 +231,38 @@ class ApiMusicSessionsController < ApiController
|
||||||
# so... just return 200
|
# so... just return 200
|
||||||
render :json => { :id => @perfdata.id }, :status => 200
|
render :json => { :id => @perfdata.id }, :status => 200
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
def claimed_recording_start
|
||||||
|
@music_session.claimed_recording_start(current_user, ClaimedRecording.find(params[:claimed_recording_id]))
|
||||||
|
|
||||||
|
if @music_session.errors.any?
|
||||||
|
# we have to do this because api_session_detail_url will fail with a bad @music_session
|
||||||
|
response.status = :unprocessable_entity
|
||||||
|
respond_with @music_session
|
||||||
|
else
|
||||||
|
respond_with @music_session, responder: ApiResponder
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def claimed_recording_stop
|
||||||
|
@music_session.claimed_recording_stop
|
||||||
|
|
||||||
|
if @music_session.errors.any?
|
||||||
|
# we have to do this because api_session_detail_url will fail with a bad @music_session
|
||||||
|
response.status = :unprocessable_entity
|
||||||
|
respond_with @music_session
|
||||||
|
else
|
||||||
|
respond_with @music_session, responder: ApiResponder
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def lookup_session
|
||||||
|
@music_session = MusicSession.find(params[:id])
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -18,10 +18,8 @@ child(:recording => :recording) {
|
||||||
}
|
}
|
||||||
|
|
||||||
child(:recorded_tracks => :recorded_tracks) {
|
child(:recorded_tracks => :recorded_tracks) {
|
||||||
attributes :id, :fully_uploaded, :url, :client_track_id
|
attributes :id, :fully_uploaded, :url, :client_track_id, :client_id, :instrument_id
|
||||||
child(:instrument => :instrument) {
|
|
||||||
attributes :id, :description
|
|
||||||
}
|
|
||||||
child(:user => :user) {
|
child(:user => :user) {
|
||||||
attributes :id, :first_name, :last_name, :city, :state, :country, :photo_url
|
attributes :id, :first_name, :last_name, :city, :state, :country, :photo_url
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
object @music_session
|
object @music_session
|
||||||
|
|
||||||
attributes :id, :description, :musician_access, :approval_required, :fan_access, :fan_chat, :band_id, :user_id
|
attributes :id, :description, :musician_access, :approval_required, :fan_access, :fan_chat, :band_id, :user_id, :claimed_recording_initiator_id
|
||||||
|
|
||||||
node :genres do |item|
|
node :genres do |item|
|
||||||
item.genres.map(&:description)
|
item.genres.map(&:description)
|
||||||
|
|
@ -38,3 +38,30 @@ node(:join_requests, :if => lambda { |music_session| music_session.users.exists?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# only show currently playing recording data if the current_user is in the session
|
||||||
|
node(:claimed_recording, :if => lambda { |music_session| music_session.users.exists?(current_user) } ) do |music_session|
|
||||||
|
|
||||||
|
child(:claimed_recording => :claimed_recording) {
|
||||||
|
attributes :id, :name, :description, :is_public, :is_downloadable
|
||||||
|
|
||||||
|
child(:recording => :recording) {
|
||||||
|
attributes :id, :created_at, :duration
|
||||||
|
child(:band => :band) {
|
||||||
|
attributes :id, :name
|
||||||
|
}
|
||||||
|
|
||||||
|
child(:mixes => :mixes) {
|
||||||
|
attributes :id, :url, :is_completed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
child(:recorded_tracks => :recorded_tracks) {
|
||||||
|
attributes :id, :fully_uploaded, :url, :client_track_id, :client_id, :instrument_id
|
||||||
|
|
||||||
|
child(:user => :user) {
|
||||||
|
attributes :id, :first_name, :last_name, :city, :state, :country, :photo_url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
<script type="text/template" id="template-help-pre-processed-track">
|
||||||
|
This track has not yet been processed into master form and may include multiple streams from the source musician.
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
<script type="text/template" id="template-hover-bubble">
|
||||||
|
<div class="hover-bubble">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
<!-- Invitation Dialog -->
|
||||||
|
<div class="dialog localRecordings-overlay ftue-overlay tall" layout="dialog" layout-id="localRecordings" id="local-recordings-dialog">
|
||||||
|
|
||||||
|
<div class="content-head">
|
||||||
|
<%= image_tag "content/icon_add.png", {:width => 19, :height => 19, :class => 'content-icon' } %>
|
||||||
|
<h1>open a recording</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dialog-inner">
|
||||||
|
|
||||||
|
<div class="recording-wrapper">
|
||||||
|
<table class="local-recordings" cellspacing="0" cellpadding="0" border="0">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th align="left">WHEN</th>
|
||||||
|
<th align="left">NAME</th>
|
||||||
|
<th align="right" class="noborder">DURATION</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<div class="left paginator-holder" >
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="right">
|
||||||
|
<a href="#" class="button-grey" layout-action="close">CANCEL</a><!-- <a href="#" class="button-orange">OPEN RECORDING</a>-->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br clear="all"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<script type="text/template" id="template-claimed-recording-row">
|
||||||
|
<tr data-recording-id="{{data.recordingId}}" data-local-state="{{data.aggregate_state}}">
|
||||||
|
<td>{{data.timeago}}</td>
|
||||||
|
<td>{{data.name}}</td>
|
||||||
|
<td>{{data.duration}}</td>
|
||||||
|
</tr>
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
<script type="text/template" id="template-paginator">
|
||||||
|
<div class="paginator" data-current-page="{{data.currentPage}}">
|
||||||
|
|
||||||
|
{% if (0 == data.currentPage) { %}
|
||||||
|
<span class="page-less arrow-left"></span>
|
||||||
|
{% } else { %}
|
||||||
|
<a href='#' class="page-less arrow-left"></a>
|
||||||
|
{% } %}
|
||||||
|
|
||||||
|
{% for(var i = 0; i < data.pages; i++) { %}
|
||||||
|
{% if (i == data.currentPage) { %}
|
||||||
|
<span class="page-link" data-page="{{i}}">{{i + 1}}</span>
|
||||||
|
{% } else { %}
|
||||||
|
<a href='#' class="page-link" data-page="{{i}}">{{i + 1}}</a>
|
||||||
|
{% } %}
|
||||||
|
{% } %}
|
||||||
|
|
||||||
|
{% if (data.currentPage == data.pages - 1) { %}
|
||||||
|
<span class="page-less arrow-right"></span>
|
||||||
|
{% } else { %}
|
||||||
|
<a href='#' class="page-more arrow-right"></a>
|
||||||
|
{% } %}
|
||||||
|
</div>
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
<!-- recording play controls -->
|
||||||
|
<div class="recording recording-controls">
|
||||||
|
|
||||||
|
<!-- play button -->
|
||||||
|
<a class="left play-button" href="#">
|
||||||
|
<%= image_tag "content/icon_playbutton.png", {:height => 20, :width => 20, :class=> "playbutton"} %>
|
||||||
|
<%= image_tag "content/icon_pausebutton.png", {:height => 20, :width => 20, :class=> "pausebutton"} %>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- playback position -->
|
||||||
|
<div class="recording-position">
|
||||||
|
|
||||||
|
<!-- start time -->
|
||||||
|
<div class="recording-time">0:00</div>
|
||||||
|
|
||||||
|
<!-- playback background & slider -->
|
||||||
|
<div class="recording-playback">
|
||||||
|
<div class="recording-slider"><%= image_tag "content/slider_playcontrols.png", {:height => 16, :width => 5} %></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- end time -->
|
||||||
|
<div class="recording-time duration-time">0:00</div>
|
||||||
|
</div>
|
||||||
|
<!-- end playback position -->
|
||||||
|
|
||||||
|
<!-- current playback time -->
|
||||||
|
<div class="recording-current">0:00</div>
|
||||||
|
</div>
|
||||||
|
<!-- end recording play controls -->
|
||||||
|
|
@ -40,36 +40,8 @@
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div class="left w50 ml30">
|
<div class="left w50 ml30">
|
||||||
Preview Recording:<br/>
|
|
||||||
<!-- recording play controls -->
|
|
||||||
<div class="recording-controls">
|
|
||||||
|
|
||||||
<!-- play button -->
|
<%= render "play_controls" %>
|
||||||
<a class="left" href="#"><%= image_tag "content/icon_playbutton.png", {:height => 20, :width => 20} %></a>
|
|
||||||
|
|
||||||
<!-- playback position -->
|
|
||||||
<div class="recording-position">
|
|
||||||
|
|
||||||
<!-- start time -->
|
|
||||||
<div class="recording-time">0:00</div>
|
|
||||||
|
|
||||||
<!-- playback background & slider -->
|
|
||||||
<div class="recording-playback">
|
|
||||||
<div class="recording-slider"><%= image_tag "content/slider_playcontrols.png", {:height => 16, :width => 5} %></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- end time -->
|
|
||||||
<div class="recording-time">4:59</div>
|
|
||||||
</div>
|
|
||||||
<!-- end playback position -->
|
|
||||||
|
|
||||||
<!-- current playback time -->
|
|
||||||
<div class="recording-current">
|
|
||||||
1:23
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<!-- end recording play controls -->
|
|
||||||
|
|
||||||
<br/>
|
<br/>
|
||||||
<br/>
|
<br/>
|
||||||
|
|
|
||||||
|
|
@ -94,13 +94,22 @@
|
||||||
<!-- recordings -->
|
<!-- recordings -->
|
||||||
<div class="session-recordings">
|
<div class="session-recordings">
|
||||||
<h2>recordings</h2>
|
<h2>recordings</h2>
|
||||||
<div class="session-add">
|
<div class="session-recording-name-wrapper">
|
||||||
|
<div class="session-recording-name left">(No recording loaded)</div>
|
||||||
|
<div class="session-add right">
|
||||||
|
<a id='close-playback-recording' href="#"><%= image_tag "content/icon_close.png", {:width => 18, :height => 20, :align => "texttop"} %> Close</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="session-tracks-scroller">
|
||||||
<p class="when-empty recordings">
|
<div id="session-recordedtracks-container">
|
||||||
No Recordings:<br/><a>Open a Recording</a>
|
<p class="when-empty recordings">
|
||||||
</p>
|
No Recordings:<br/><a href="#" id="open-a-recording">Open a Recording</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<br clear="all" />
|
||||||
|
|
||||||
|
<%= render "play_controls" %>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- recording name and close button -->
|
<!-- recording name and close button -->
|
||||||
|
|
@ -114,8 +123,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
-->
|
-->
|
||||||
<div class="session-tracks-scroller">
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -140,10 +147,10 @@
|
||||||
<div id="div-track-close" track-id="{trackId}" class="track-close op30">
|
<div id="div-track-close" track-id="{trackId}" class="track-close op30">
|
||||||
<%= image_tag "content/icon_closetrack.png", {:width => 12, :height => 12} %>
|
<%= image_tag "content/icon_closetrack.png", {:width => 12, :height => 12} %>
|
||||||
</div>
|
</div>
|
||||||
<div class="avatar-med">
|
<div class="{avatarClass}">
|
||||||
<img src="{avatar}"/>
|
<img src="{avatar}"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="track-instrument">
|
<div class="track-instrument {preMasteredClass}">
|
||||||
<img src="/assets/{instrumentIcon}" width="45" height="45"/>
|
<img src="/assets/{instrumentIcon}" width="45" height="45"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="track-gain" mixer-id="{mixerId}"></div>
|
<div class="track-gain" mixer-id="{mixerId}"></div>
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
<%= render "header" %>
|
<%= render "header" %>
|
||||||
<%= render "home" %>
|
<%= render "home" %>
|
||||||
<%= render "footer" %>
|
<%= render "footer" %>
|
||||||
|
<%= render "paginator" %>
|
||||||
<%= render "searchResults" %>
|
<%= render "searchResults" %>
|
||||||
<%= render "faders" %>
|
<%= render "faders" %>
|
||||||
<%= render "vu_meters" %>
|
<%= render "vu_meters" %>
|
||||||
|
|
@ -36,11 +37,13 @@
|
||||||
<%= render "invitationDialog" %>
|
<%= render "invitationDialog" %>
|
||||||
<%= render "whatsNextDialog" %>
|
<%= render "whatsNextDialog" %>
|
||||||
<%= render "recordingFinishedDialog" %>
|
<%= render "recordingFinishedDialog" %>
|
||||||
|
<%= render "localRecordingsDialog" %>
|
||||||
<%= render "notify" %>
|
<%= render "notify" %>
|
||||||
<%= render "client_update" %>
|
<%= render "client_update" %>
|
||||||
<%= render "banner" %>
|
<%= render "banner" %>
|
||||||
<%= render "clients/banners/disconnected" %>
|
<%= render "clients/banners/disconnected" %>
|
||||||
<%= render "overlay_small" %>
|
<%= render "overlay_small" %>
|
||||||
|
<%= render "help" %>
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
$(function() {
|
$(function() {
|
||||||
|
|
@ -91,6 +94,9 @@
|
||||||
var invitationDialog = new JK.InvitationDialog(JK.app);
|
var invitationDialog = new JK.InvitationDialog(JK.app);
|
||||||
invitationDialog.initialize();
|
invitationDialog.initialize();
|
||||||
|
|
||||||
|
var localRecordingsDialog = new JK.LocalRecordingsDialog(JK.app);
|
||||||
|
localRecordingsDialog.initialize();
|
||||||
|
|
||||||
var friendSelectorDialog = new JK.FriendSelectorDialog(JK.app);
|
var friendSelectorDialog = new JK.FriendSelectorDialog(JK.app);
|
||||||
friendSelectorDialog.initialize();
|
friendSelectorDialog.initialize();
|
||||||
|
|
||||||
|
|
@ -159,7 +165,8 @@
|
||||||
findBandScreen.initialize();
|
findBandScreen.initialize();
|
||||||
|
|
||||||
var sessionScreen = new JK.SessionScreen(JK.app);
|
var sessionScreen = new JK.SessionScreen(JK.app);
|
||||||
sessionScreen.initialize();
|
sessionScreen.initialize(localRecordingsDialog);
|
||||||
|
|
||||||
var sessionSettingsDialog = new JK.SessionSettingsDialog(JK.app, sessionScreen);
|
var sessionSettingsDialog = new JK.SessionSettingsDialog(JK.app, sessionScreen);
|
||||||
sessionSettingsDialog.initialize();
|
sessionSettingsDialog.initialize();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ if defined?(Bundler)
|
||||||
# Bundler.require(:default, :assets, Rails.env)
|
# Bundler.require(:default, :assets, Rails.env)
|
||||||
end
|
end
|
||||||
|
|
||||||
include JamRuby
|
include JamRuby
|
||||||
# require "rails/test_unit/railtie"
|
# require "rails/test_unit/railtie"
|
||||||
|
|
||||||
module SampleApp
|
module SampleApp
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,10 @@ SampleApp::Application.routes.draw do
|
||||||
match '/sessions/:id/tracks/:track_id' => 'api_music_sessions#track_show', :via => :get, :as => 'api_session_track_detail'
|
match '/sessions/:id/tracks/:track_id' => 'api_music_sessions#track_show', :via => :get, :as => 'api_session_track_detail'
|
||||||
match '/sessions/:id/tracks/:track_id' => 'api_music_sessions#track_destroy', :via => :delete
|
match '/sessions/:id/tracks/:track_id' => 'api_music_sessions#track_destroy', :via => :delete
|
||||||
|
|
||||||
|
# music session playback recording state
|
||||||
|
match '/sessions/:id/claimed_recording/:claimed_recording_id/start' => 'api_music_sessions#claimed_recording_start', :via => :post
|
||||||
|
match '/sessions/:id/claimed_recording/:claimed_recording_id/stop' => 'api_music_sessions#claimed_recording_stop', :via => :post
|
||||||
|
|
||||||
match '/participant_histories/:id/rating' => 'api_music_sessions#participant_rating', :via => :post
|
match '/participant_histories/:id/rating' => 'api_music_sessions#participant_rating', :via => :post
|
||||||
|
|
||||||
# genres
|
# genres
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,19 @@ describe ApiRecordingsController do
|
||||||
response_body["errors"]["music_session"][0].should == ValidationMessages::ALREADY_BEING_RECORDED
|
response_body["errors"]["music_session"][0].should == ValidationMessages::ALREADY_BEING_RECORDED
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it "should not allow start while playback ongoing" do
|
||||||
|
recording = Recording.start(@music_session, @user)
|
||||||
|
recording.stop
|
||||||
|
recording.reload
|
||||||
|
claimed_recording = recording.claim(@user, "name", "description", Genre.first, true, true)
|
||||||
|
@music_session.claimed_recording_start(@user, claimed_recording)
|
||||||
|
@music_session.errors.any?.should be_false
|
||||||
|
post :start, { :format => 'json', :music_session_id => @music_session.id }
|
||||||
|
response.status.should == 422
|
||||||
|
response_body = JSON.parse(response.body)
|
||||||
|
response_body["errors"]["music_session"][0].should == ValidationMessages::ALREADY_PLAYBACK_RECORDING
|
||||||
|
end
|
||||||
|
|
||||||
it "should not allow start by somebody not in the music session" do
|
it "should not allow start by somebody not in the music session" do
|
||||||
user2 = FactoryGirl.create(:user)
|
user2 = FactoryGirl.create(:user)
|
||||||
controller.current_user = user2
|
controller.current_user = user2
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,7 @@ FactoryGirl.define do
|
||||||
approval_required false
|
approval_required false
|
||||||
musician_access true
|
musician_access true
|
||||||
legal_terms true
|
legal_terms true
|
||||||
|
genres [JamRuby::Genre.first]
|
||||||
|
|
||||||
after(:create) { |session|
|
after(:create) { |session|
|
||||||
MusicSessionHistory.save(session)
|
MusicSessionHistory.save(session)
|
||||||
|
|
|
||||||
|
|
@ -664,6 +664,36 @@ describe "Music Session API ", :type => :api do
|
||||||
tracks[1]["sound"].should == "stereo"
|
tracks[1]["sound"].should == "stereo"
|
||||||
tracks[1]["client_track_id"].should == "client_track_id1"
|
tracks[1]["client_track_id"].should == "client_track_id1"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it "allows start/stop recording playback of a claimed recording" do
|
||||||
|
|
||||||
|
user = FactoryGirl.create(:user)
|
||||||
|
connection = FactoryGirl.create(:connection, :user => user)
|
||||||
|
track = FactoryGirl.create(:track, :connection => connection, :instrument => Instrument.first)
|
||||||
|
music_session = FactoryGirl.create(:music_session, :creator => user, :musician_access => true)
|
||||||
|
music_session.connections << connection
|
||||||
|
music_session.save
|
||||||
|
recording = Recording.start(music_session, user)
|
||||||
|
recording.stop
|
||||||
|
recording.reload
|
||||||
|
claimed_recording = recording.claim(user, "name", "description", Genre.first, true, true)
|
||||||
|
recording.reload
|
||||||
|
|
||||||
|
login(user)
|
||||||
|
post "/api/sessions/#{music_session.id}/claimed_recording/#{claimed_recording.id}/start.json", {}.to_json, "CONTENT_TYPE" => "application/json"
|
||||||
|
|
||||||
|
last_response.status.should == 201
|
||||||
|
music_session.reload
|
||||||
|
music_session.claimed_recording.should == claimed_recording
|
||||||
|
music_session.claimed_recording_initiator.should == user
|
||||||
|
|
||||||
|
post "/api/sessions/#{music_session.id}/claimed_recording/#{claimed_recording.id}/stop.json", {}.to_json, "CONTENT_TYPE" => "application/json"
|
||||||
|
|
||||||
|
last_response.status.should == 201
|
||||||
|
music_session.reload
|
||||||
|
music_session.claimed_recording.should be_nil
|
||||||
|
music_session.claimed_recording_initiator.should be_nil
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,202 @@
|
||||||
|
/**
|
||||||
|
* Timeago is a jQuery plugin that makes it easy to support automatically
|
||||||
|
* updating fuzzy timestamps (e.g. "4 minutes ago" or "about 1 day ago").
|
||||||
|
*
|
||||||
|
* @name timeago
|
||||||
|
* @version 1.3.1
|
||||||
|
* @requires jQuery v1.2.3+
|
||||||
|
* @author Ryan McGeary
|
||||||
|
* @license MIT License - http://www.opensource.org/licenses/mit-license.php
|
||||||
|
*
|
||||||
|
* For usage and examples, visit:
|
||||||
|
* http://timeago.yarp.com/
|
||||||
|
*
|
||||||
|
* Copyright (c) 2008-2013, Ryan McGeary (ryan -[at]- mcgeary [*dot*] org)
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function (factory) {
|
||||||
|
if (typeof define === 'function' && define.amd) {
|
||||||
|
// AMD. Register as an anonymous module.
|
||||||
|
define(['jquery'], factory);
|
||||||
|
} else {
|
||||||
|
// Browser globals
|
||||||
|
factory(jQuery);
|
||||||
|
}
|
||||||
|
}(function ($) {
|
||||||
|
$.timeago = function(timestamp) {
|
||||||
|
if (timestamp instanceof Date) {
|
||||||
|
return inWords(timestamp);
|
||||||
|
} else if (typeof timestamp === "string") {
|
||||||
|
return inWords($.timeago.parse(timestamp));
|
||||||
|
} else if (typeof timestamp === "number") {
|
||||||
|
return inWords(new Date(timestamp));
|
||||||
|
} else {
|
||||||
|
return inWords($.timeago.datetime(timestamp));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var $t = $.timeago;
|
||||||
|
|
||||||
|
$.extend($.timeago, {
|
||||||
|
settings: {
|
||||||
|
refreshMillis: 60000,
|
||||||
|
allowFuture: false,
|
||||||
|
localeTitle: false,
|
||||||
|
cutoff: 0,
|
||||||
|
strings: {
|
||||||
|
prefixAgo: null,
|
||||||
|
prefixFromNow: null,
|
||||||
|
suffixAgo: "ago",
|
||||||
|
suffixFromNow: "from now",
|
||||||
|
seconds: "less than a minute",
|
||||||
|
minute: "about a minute",
|
||||||
|
minutes: "%d minutes",
|
||||||
|
hour: "about an hour",
|
||||||
|
hours: "about %d hours",
|
||||||
|
day: "a day",
|
||||||
|
days: "%d days",
|
||||||
|
month: "about a month",
|
||||||
|
months: "%d months",
|
||||||
|
year: "about a year",
|
||||||
|
years: "%d years",
|
||||||
|
wordSeparator: " ",
|
||||||
|
numbers: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
inWords: function(distanceMillis) {
|
||||||
|
var $l = this.settings.strings;
|
||||||
|
var prefix = $l.prefixAgo;
|
||||||
|
var suffix = $l.suffixAgo;
|
||||||
|
if (this.settings.allowFuture) {
|
||||||
|
if (distanceMillis < 0) {
|
||||||
|
prefix = $l.prefixFromNow;
|
||||||
|
suffix = $l.suffixFromNow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var seconds = Math.abs(distanceMillis) / 1000;
|
||||||
|
var minutes = seconds / 60;
|
||||||
|
var hours = minutes / 60;
|
||||||
|
var days = hours / 24;
|
||||||
|
var years = days / 365;
|
||||||
|
|
||||||
|
function substitute(stringOrFunction, number) {
|
||||||
|
var string = $.isFunction(stringOrFunction) ? stringOrFunction(number, distanceMillis) : stringOrFunction;
|
||||||
|
var value = ($l.numbers && $l.numbers[number]) || number;
|
||||||
|
return string.replace(/%d/i, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
var words = seconds < 45 && substitute($l.seconds, Math.round(seconds)) ||
|
||||||
|
seconds < 90 && substitute($l.minute, 1) ||
|
||||||
|
minutes < 45 && substitute($l.minutes, Math.round(minutes)) ||
|
||||||
|
minutes < 90 && substitute($l.hour, 1) ||
|
||||||
|
hours < 24 && substitute($l.hours, Math.round(hours)) ||
|
||||||
|
hours < 42 && substitute($l.day, 1) ||
|
||||||
|
days < 30 && substitute($l.days, Math.round(days)) ||
|
||||||
|
days < 45 && substitute($l.month, 1) ||
|
||||||
|
days < 365 && substitute($l.months, Math.round(days / 30)) ||
|
||||||
|
years < 1.5 && substitute($l.year, 1) ||
|
||||||
|
substitute($l.years, Math.round(years));
|
||||||
|
|
||||||
|
var separator = $l.wordSeparator || "";
|
||||||
|
if ($l.wordSeparator === undefined) { separator = " "; }
|
||||||
|
return $.trim([prefix, words, suffix].join(separator));
|
||||||
|
},
|
||||||
|
parse: function(iso8601) {
|
||||||
|
var s = $.trim(iso8601);
|
||||||
|
s = s.replace(/\.\d+/,""); // remove milliseconds
|
||||||
|
s = s.replace(/-/,"/").replace(/-/,"/");
|
||||||
|
s = s.replace(/T/," ").replace(/Z/," UTC");
|
||||||
|
s = s.replace(/([\+\-]\d\d)\:?(\d\d)/," $1$2"); // -04:00 -> -0400
|
||||||
|
s = s.replace(/([\+\-]\d\d)$/," $100"); // +09 -> +0900
|
||||||
|
return new Date(s);
|
||||||
|
},
|
||||||
|
datetime: function(elem) {
|
||||||
|
var iso8601 = $t.isTime(elem) ? $(elem).attr("datetime") : $(elem).attr("title");
|
||||||
|
return $t.parse(iso8601);
|
||||||
|
},
|
||||||
|
isTime: function(elem) {
|
||||||
|
// jQuery's `is()` doesn't play well with HTML5 in IE
|
||||||
|
return $(elem).get(0).tagName.toLowerCase() === "time"; // $(elem).is("time");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// functions that can be called via $(el).timeago('action')
|
||||||
|
// init is default when no action is given
|
||||||
|
// functions are called with context of a single element
|
||||||
|
var functions = {
|
||||||
|
init: function(){
|
||||||
|
var refresh_el = $.proxy(refresh, this);
|
||||||
|
refresh_el();
|
||||||
|
var $s = $t.settings;
|
||||||
|
if ($s.refreshMillis > 0) {
|
||||||
|
this._timeagoInterval = setInterval(refresh_el, $s.refreshMillis);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
update: function(time){
|
||||||
|
var parsedTime = $t.parse(time);
|
||||||
|
$(this).data('timeago', { datetime: parsedTime });
|
||||||
|
if($t.settings.localeTitle) $(this).attr("title", parsedTime.toLocaleString());
|
||||||
|
refresh.apply(this);
|
||||||
|
},
|
||||||
|
updateFromDOM: function(){
|
||||||
|
$(this).data('timeago', { datetime: $t.parse( $t.isTime(this) ? $(this).attr("datetime") : $(this).attr("title") ) });
|
||||||
|
refresh.apply(this);
|
||||||
|
},
|
||||||
|
dispose: function () {
|
||||||
|
if (this._timeagoInterval) {
|
||||||
|
window.clearInterval(this._timeagoInterval);
|
||||||
|
this._timeagoInterval = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$.fn.timeago = function(action, options) {
|
||||||
|
var fn = action ? functions[action] : functions.init;
|
||||||
|
if(!fn){
|
||||||
|
throw new Error("Unknown function name '"+ action +"' for timeago");
|
||||||
|
}
|
||||||
|
// each over objects here and call the requested function
|
||||||
|
this.each(function(){
|
||||||
|
fn.call(this, options);
|
||||||
|
});
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
function refresh() {
|
||||||
|
var data = prepareData(this);
|
||||||
|
var $s = $t.settings;
|
||||||
|
|
||||||
|
if (!isNaN(data.datetime)) {
|
||||||
|
if ( $s.cutoff == 0 || distance(data.datetime) < $s.cutoff) {
|
||||||
|
$(this).text(inWords(data.datetime));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
function prepareData(element) {
|
||||||
|
element = $(element);
|
||||||
|
if (!element.data("timeago")) {
|
||||||
|
element.data("timeago", { datetime: $t.datetime(element) });
|
||||||
|
var text = $.trim(element.text());
|
||||||
|
if ($t.settings.localeTitle) {
|
||||||
|
element.attr("title", element.data('timeago').datetime.toLocaleString());
|
||||||
|
} else if (text.length > 0 && !($t.isTime(element) && element.attr("title"))) {
|
||||||
|
element.attr("title", text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return element.data("timeago");
|
||||||
|
}
|
||||||
|
|
||||||
|
function inWords(date) {
|
||||||
|
return $t.inWords(distance(date));
|
||||||
|
}
|
||||||
|
|
||||||
|
function distance(date) {
|
||||||
|
return (new Date().getTime() - date.getTime());
|
||||||
|
}
|
||||||
|
|
||||||
|
// fix for IE6 suckage
|
||||||
|
document.createElement("abbr");
|
||||||
|
document.createElement("time");
|
||||||
|
}));
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
/**
|
||||||
|
* styling for tip content
|
||||||
|
* mostly for example
|
||||||
|
* note: canvas (the tip itself) cannot be styled here. use javascript options for that.
|
||||||
|
*/
|
||||||
|
.bt-content {
|
||||||
|
font-size: small;
|
||||||
|
color: #000;
|
||||||
|
line-height: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* styling for active target elements - usually for background hilighting */
|
||||||
|
.bt-active {
|
||||||
|
/* example:
|
||||||
|
background-color: yellow !important;
|
||||||
|
*/
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue