* 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
|
||||
store_s3_filenames.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
|
||||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ module ValidationMessages
|
|||
|
||||
# recordings
|
||||
ALREADY_BEING_RECORDED = "already being recorded"
|
||||
ALREADY_PLAYBACK_RECORDING = "already playing a recording"
|
||||
NO_LONGER_RECORDING = "no longer recording"
|
||||
NOT_IN_SESSION = "not in session"
|
||||
|
||||
|
|
@ -59,6 +60,10 @@ module ValidationMessages
|
|||
PART_NOT_STARTED = "not started"
|
||||
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,
|
||||
# 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 :genre, :class_name => "JamRuby::Genre"
|
||||
has_many :recorded_tracks, :through => :recording, :class_name => "JamRuby::RecordedTrack"
|
||||
has_many :playing_sessions, :class_name => "JamRuby::MusicSession"
|
||||
|
||||
# user must own this object
|
||||
# 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
|
||||
|
||||
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 :users, :through => :connections, :class_name => "JamRuby::User"
|
||||
|
|
@ -33,10 +35,20 @@ module JamRuby
|
|||
validates :legal_terms, :inclusion => {:in => [true]}, :on => :create
|
||||
validates :creator, :presence => true
|
||||
validate :creator_is_musician
|
||||
validate :no_new_playback_while_playing
|
||||
|
||||
def creator_is_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
|
||||
|
||||
|
|
@ -172,6 +184,10 @@ module JamRuby
|
|||
recordings.where(:duration => nil).count > 0
|
||||
end
|
||||
|
||||
def is_playing_recording?
|
||||
!self.claimed_recording.nil?
|
||||
end
|
||||
|
||||
def recording
|
||||
recordings.where(:duration => nil).first
|
||||
end
|
||||
|
|
@ -182,8 +198,20 @@ module JamRuby
|
|||
current_recording.stop unless current_recording.nil?
|
||||
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
|
||||
return description
|
||||
description
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ module JamRuby
|
|||
has_many :recorded_tracks, :class_name => "JamRuby::RecordedTrack", :foreign_key => :recording_id
|
||||
validates :music_session, :presence => true
|
||||
validate :not_already_recording, :on => :create
|
||||
validate :not_playback_recording, :on => :create
|
||||
validate :already_stopped_recording
|
||||
|
||||
def not_already_recording
|
||||
|
|
@ -22,6 +23,12 @@ module JamRuby
|
|||
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
|
||||
if is_done && is_done_was
|
||||
errors.add(:music_session, ValidationMessages::NO_LONGER_RECORDING)
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ module JamRuby
|
|||
has_many :owned_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 :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)
|
||||
has_many :likers, :class_name => "JamRuby::UserLiker", :foreign_key => "user_id", :inverse_of => :user
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ FactoryGirl.define do
|
|||
approval_required false
|
||||
musician_access true
|
||||
legal_terms true
|
||||
genres [JamRuby::Genre.first]
|
||||
association :creator, :factory => :user
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -430,7 +430,54 @@ describe MusicSession do
|
|||
it "stop_recording should return recording object if recording" do
|
||||
@music_session.stop_recording.should == @recording
|
||||
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
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ end
|
|||
|
||||
gem 'rails', '>=3.2.11'
|
||||
gem 'jquery-rails', '2.0.2'
|
||||
gem 'jquery-ui-rails'
|
||||
gem 'bootstrap-sass', '2.0.4'
|
||||
gem 'bcrypt-ruby', '3.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_ujs
|
||||
//= require jquery.ui.draggable
|
||||
//= require jquery.bt
|
||||
//= require jquery.icheck
|
||||
//= require jquery.color
|
||||
//= require jquery.cookie
|
||||
//= require jquery.Jcrop
|
||||
//= require jquery.naturalsize
|
||||
//= require jquery.queryparams
|
||||
//= require jquery.timeago
|
||||
//= require globals
|
||||
//= require_directory .
|
||||
|
|
|
|||
|
|
@ -11,21 +11,25 @@
|
|||
|
||||
var $draggingFaderHandle = null;
|
||||
var $draggingFader = null;
|
||||
var draggingOrientation = null;
|
||||
|
||||
var subscribers = {};
|
||||
var logger = g.JK.logger;
|
||||
var MAX_VISUAL_FADER = 95;
|
||||
|
||||
function faderClick(evt) {
|
||||
evt.stopPropagation();
|
||||
if (g.JK.$draggingFaderHandle) {
|
||||
return;
|
||||
function faderClick(e) {
|
||||
e.stopPropagation();
|
||||
|
||||
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
|
||||
g._.each(subscribers, function(changeFunc, index, list) {
|
||||
|
|
@ -39,75 +43,35 @@
|
|||
}
|
||||
|
||||
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 handleCssAttribute = getHandleCssAttribute($fader);
|
||||
$handle.css(handleCssAttribute, value + '%');
|
||||
}
|
||||
|
||||
|
||||
function faderHandleDown(evt) {
|
||||
evt.stopPropagation();
|
||||
$draggingFaderHandle = $(evt.currentTarget);
|
||||
$draggingFader = $draggingFaderHandle.closest('div[control="fader"]');
|
||||
return false;
|
||||
}
|
||||
|
||||
function faderMouseUp(evt) {
|
||||
evt.stopPropagation();
|
||||
if ($draggingFaderHandle) {
|
||||
var $fader = $draggingFaderHandle.closest('div[control="fader"]');
|
||||
var faderId = $fader.closest('[fader-id]').attr("fader-id");
|
||||
var faderPct = faderValue($fader, evt);
|
||||
// Notify subscribers of value change
|
||||
g._.each(subscribers, function(changeFunc, index, list) {
|
||||
if (faderId === index) {
|
||||
changeFunc(faderId, faderPct, false);
|
||||
}
|
||||
});
|
||||
$draggingFaderHandle = null;
|
||||
$draggingFader = null;
|
||||
if(draggingOrientation === "horizontal") {
|
||||
ratio = value / 100;
|
||||
position = ((ratio * $fader.width()) - (ratio * handleWidth(draggingOrientation))) + 'px';
|
||||
}
|
||||
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 getPercentFunction = getVerticalFaderPercent;
|
||||
var absolutePosition = evt.clientY;
|
||||
var relativePosition = offset.top;
|
||||
if (orientation && orientation == 'horizontal') {
|
||||
getPercentFunction = getHorizontalFaderPercent;
|
||||
absolutePosition = evt.clientX;
|
||||
relativePosition = offset.left;
|
||||
}
|
||||
return getPercentFunction(absolutePosition, $fader);
|
||||
return getPercentFunction(relativePosition, $fader);
|
||||
}
|
||||
|
||||
function getHandleCssAttribute($fader) {
|
||||
var orientation = $fader.attr('orientation');
|
||||
return (orientation === 'horizontal') ? 'left' : 'bottom';
|
||||
}
|
||||
|
||||
function faderMouseMove(evt) {
|
||||
// bail out early if there's no in-process drag
|
||||
if (!($draggingFaderHandle)) {
|
||||
return false;
|
||||
}
|
||||
var $fader = $draggingFader;
|
||||
var faderId = $fader.closest('[fader-id]').attr("fader-id"); var $handle = $draggingFaderHandle;
|
||||
evt.stopPropagation();
|
||||
var faderPct = faderValue($fader, evt);
|
||||
|
||||
// Notify subscribers of value change
|
||||
g._.each(subscribers, function(changeFunc, index, list) {
|
||||
if (faderId === index) {
|
||||
changeFunc(faderId, faderPct, true);
|
||||
}
|
||||
});
|
||||
|
||||
if (faderPct > MAX_VISUAL_FADER) { faderPct = MAX_VISUAL_FADER; } // Visual limit
|
||||
var handleCssAttribute = getHandleCssAttribute($fader);
|
||||
$handle.css(handleCssAttribute, faderPct + '%');
|
||||
return false;
|
||||
return (orientation === 'horizontal') ? 'left' : 'top';
|
||||
}
|
||||
|
||||
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) {
|
||||
var faderPosition = $fader.offset();
|
||||
var faderMin = faderPosition.top;
|
||||
var faderSize = $fader.height();
|
||||
var handleValue = (faderSize - (value-faderMin));
|
||||
var faderSize, faderPct;
|
||||
|
||||
// the handle takes up room, and all calculations use top. So when the
|
||||
// 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") {
|
||||
faderMin = faderPosition.left;
|
||||
faderSize = $fader.width();
|
||||
handleValue = (value - faderMin);
|
||||
faderPct = Math.round( ( value + (value / faderSize * handleWidth(orientation))) / faderSize * 100);
|
||||
}
|
||||
var faderPct = Math.round(handleValue/faderSize * 100);
|
||||
if (faderPct < 0) {
|
||||
faderPct = 0;
|
||||
}
|
||||
if (faderPct > 100) {
|
||||
faderPct = 100;
|
||||
else {
|
||||
faderSize = $fader.height();
|
||||
faderPct = Math.round((faderSize - handleWidth(orientation) - value)/(faderSize - handleWidth(orientation)) * 100);
|
||||
}
|
||||
|
||||
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 = {
|
||||
|
||||
/**
|
||||
|
|
@ -174,6 +185,15 @@
|
|||
var templateSource = $(templateSelector).html();
|
||||
|
||||
$(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
|
||||
if ("style" in options) {
|
||||
for (var key in options.style) {
|
||||
|
|
@ -213,11 +233,6 @@
|
|||
|
||||
initialize: function() {
|
||||
$('body').on('click', 'div[control="fader"]', faderClick);
|
||||
$('body').on('mousedown', 'div[control="fader-handle"]', faderHandleDown);
|
||||
$('body').on('mousemove', 'div[layout-id="session"], [layout-wizard="ftue"]', faderMouseMove);
|
||||
$('body').on('mouseup', 'div[layout-id="session"], [layout-wizard="ftue"]', faderMouseUp);
|
||||
//$('body').on('mousemove', faderMouseMove);
|
||||
//$('body').on('mouseup', faderMouseUp);
|
||||
}
|
||||
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
// Set the instance functions:
|
||||
|
|
@ -663,6 +685,11 @@
|
|||
this.OnLoggedIn = OnLoggedIn;
|
||||
this.OnLoggedOut = OnLoggedOut;
|
||||
|
||||
// Recording Playback
|
||||
this.GetLocalRecordingState = GetLocalRecordingState;
|
||||
this.OpenRecording = OpenRecording;
|
||||
this.CloseRecording = CloseRecording;
|
||||
|
||||
// fake calls; not a part of the actual jam client
|
||||
this.RegisterP2PMessageCallbacks = RegisterP2PMessageCallbacks;
|
||||
this.SetFakeRecordingImpl = SetFakeRecordingImpl;
|
||||
|
|
|
|||
|
|
@ -504,7 +504,18 @@
|
|||
dataType: "json",
|
||||
contentType: 'application/json',
|
||||
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) {
|
||||
|
|
@ -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) {
|
||||
var recordingId = options["id"];
|
||||
|
||||
|
|
@ -565,7 +606,7 @@
|
|||
this.getFriends = getFriends;
|
||||
this.updateSession = updateSession;
|
||||
this.getSession = getSession;
|
||||
this.getClientDownloads = getClientDownloads
|
||||
this.getClientDownloads = getClientDownloads;
|
||||
this.createInvitation = createInvitation;
|
||||
this.postFeedback = postFeedback;
|
||||
this.serverHealthCheck = serverHealthCheck;
|
||||
|
|
@ -580,7 +621,10 @@
|
|||
this.startRecording = startRecording;
|
||||
this.stopRecording = stopRecording;
|
||||
this.getRecording = getRecording;
|
||||
this.getClaimedRecordings = getClaimedRecordings;
|
||||
this.claimRecording = claimRecording;
|
||||
this.startPlayClaimedRecording = startPlayClaimedRecording;
|
||||
this.stopPlayClaimedRecording = stopPlayClaimedRecording;
|
||||
this.discardRecording = discardRecording;
|
||||
this.putTrackSyncChange = putTrackSyncChange;
|
||||
this.createBand = createBand;
|
||||
|
|
|
|||
|
|
@ -264,6 +264,23 @@
|
|||
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.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -638,7 +638,7 @@
|
|||
function setNotificationInfo(message, descriptor) {
|
||||
var $notify = $('[layout="notify"]');
|
||||
$('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) {
|
||||
$('#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) {
|
||||
var logger = context.JK.logger;
|
||||
var rest = context.JK.Rest();
|
||||
var playbackControls = null;
|
||||
|
||||
function resetForm() {
|
||||
// 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() {
|
||||
registerClaimRecordingHandlers(true);
|
||||
registerDiscardRecordingHandlers(true);
|
||||
|
||||
$(playbackControls)
|
||||
.on('pause', onPause)
|
||||
.on('play', onPlay)
|
||||
.on('change-position', onChangePlayPosition);
|
||||
}
|
||||
|
||||
function initialize(){
|
||||
|
|
@ -144,6 +163,8 @@
|
|||
|
||||
app.bindDialog('recordingFinished', dialogBindings);
|
||||
|
||||
playbackControls = new context.JK.PlaybackControls($('#recording-finished-dialog .recording-controls'));
|
||||
|
||||
registerStaticEvents();
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@
|
|||
var groupedTracks = groupTracksToClient(recording);
|
||||
jamClient.StartRecording(recording["id"], groupedTracks);
|
||||
})
|
||||
.fail(function() {
|
||||
.fail(function(jqXHR) {
|
||||
$self.triggerHandler('startedRecording', { clientId: app.clientId, reason: 'rest', detail: arguments });
|
||||
currentlyRecording = false;
|
||||
})
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
var mixers = [];
|
||||
var configureTrackDialog;
|
||||
var addNewGearDialog;
|
||||
var localRecordingsDialog = null;
|
||||
var screenActive = false;
|
||||
var currentMixerRangeMin = null;
|
||||
var currentMixerRangeMax = null;
|
||||
|
|
@ -22,6 +23,9 @@
|
|||
var recordingTimerInterval = null;
|
||||
var startTimeDate = null;
|
||||
var startingRecording = false; // double-click guard
|
||||
var claimedRecording = null;
|
||||
var playbackControls = null;
|
||||
var monitorPlaybackTimeout = null;
|
||||
|
||||
|
||||
var rest = JK.Rest();
|
||||
|
|
@ -242,6 +246,10 @@
|
|||
else if(data.reason == 'recording-engine-sample-rate') {
|
||||
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 {
|
||||
notifyWithUserInfo(title, 'Error Reason: ' + reason);
|
||||
}
|
||||
|
|
@ -356,8 +364,36 @@
|
|||
.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() {
|
||||
|
||||
handleTransitionsInRecordingPlayback();
|
||||
// TODO - in the specific case of a user changing their tracks using the configureTrack dialog,
|
||||
// this event appears to fire before the underlying mixers have updated. I have no event to
|
||||
// know definitively when the underlying mixers are up to date, so for now, we just delay slightly.
|
||||
|
|
@ -387,6 +423,7 @@
|
|||
$voiceChat.hide();
|
||||
_updateMixers();
|
||||
_renderTracks();
|
||||
_renderLocalMediaTracks();
|
||||
_wireTopVolume();
|
||||
_wireTopMix();
|
||||
_addVoiceChat();
|
||||
|
|
@ -394,6 +431,11 @@
|
|||
if ($('.session-livetracks .track').length === 0) {
|
||||
$('.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() {
|
||||
|
|
@ -406,6 +448,7 @@
|
|||
var mixerIds = context.jamClient.SessionGetIDs();
|
||||
var holder = $.extend(true, {}, {mixers: context.jamClient.SessionGetControlState(mixerIds)});
|
||||
mixers = holder.mixers;
|
||||
|
||||
// Always add a hard-coded simplified 'mixer' for the L2M mix
|
||||
var l2m_mixer = {
|
||||
id: '__L2M__',
|
||||
|
|
@ -416,6 +459,17 @@
|
|||
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
|
||||
// client id and group.
|
||||
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() {
|
||||
myTracks = [];
|
||||
|
||||
|
|
@ -572,7 +733,9 @@
|
|||
latency: "good",
|
||||
gainPercent: 0,
|
||||
muteClass: 'muted',
|
||||
mixerId: ""
|
||||
mixerId: "",
|
||||
avatarClass: 'avatar-med',
|
||||
preMasteredClass: ""
|
||||
};
|
||||
|
||||
// This is the likely cause of multi-track problems.
|
||||
|
|
@ -728,7 +891,7 @@
|
|||
$('.session-livetracks .when-empty').hide();
|
||||
}
|
||||
var template = $('#template-session-track').html();
|
||||
var newTrack = context.JK.fillTemplate(template, trackData);
|
||||
var newTrack = $(context.JK.fillTemplate(template, trackData));
|
||||
$destination.append(newTrack);
|
||||
|
||||
// Render VU meters and gain fader
|
||||
|
|
@ -747,6 +910,32 @@
|
|||
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),
|
||||
* the new value (0-100) and whether the fader is still being dragged are passed.
|
||||
|
|
@ -1063,6 +1252,57 @@
|
|||
.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() {
|
||||
if(sessionModel.recordingModel.isRecording()) {
|
||||
sessionModel.recordingModel.stopRecording();
|
||||
|
|
@ -1072,21 +1312,31 @@
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
function events() {
|
||||
$('#session-resync').on('click', sessionResync);
|
||||
$('#session-contents').on("click", '[action="delete"]', deleteSession);
|
||||
$('#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() {
|
||||
configureTrackDialog.showVoiceChatPanel(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);
|
||||
playbackControls = new context.JK.PlaybackControls($('.session-recordings .recording-controls'));
|
||||
events();
|
||||
|
||||
|
||||
var screenBindings = {
|
||||
'beforeShow': beforeShow,
|
||||
'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() {
|
||||
if(!currentSession) {
|
||||
throw "creator is not known"
|
||||
|
|
@ -454,12 +469,14 @@
|
|||
|
||||
// Public interface
|
||||
this.id = id;
|
||||
this.recordedTracks = recordedTracks;
|
||||
this.participants = participants;
|
||||
this.joinSession = joinSession;
|
||||
this.leaveCurrentSession = leaveCurrentSession;
|
||||
this.refreshCurrentSession = refreshCurrentSession;
|
||||
this.subscribe = subscribe;
|
||||
this.participantForClientId = participantForClientId;
|
||||
this.isPlayingRecording = isPlayingRecording;
|
||||
this.addTrack = addTrack;
|
||||
this.updateTrack = updateTrack;
|
||||
this.deleteTrack = deleteTrack;
|
||||
|
|
|
|||
|
|
@ -131,7 +131,7 @@
|
|||
notificationId: val.notification_id,
|
||||
avatar_url: context.JK.resolveAvatarUrl(val.photo_url),
|
||||
text: val.formatted_msg,
|
||||
date: context.JK.formatDate(val.created_at)
|
||||
date: context.JK.formatDateTime(val.created_at)
|
||||
});
|
||||
|
||||
$('#sidebar-notification-list').append(notificationHtml);
|
||||
|
|
@ -362,7 +362,7 @@
|
|||
notificationId: payload.notification_id,
|
||||
avatar_url: context.JK.resolveAvatarUrl(payload.photo_url),
|
||||
text: sidebarText,
|
||||
date: context.JK.formatDate(payload.created_at)
|
||||
date: context.JK.formatDateTime(payload.created_at)
|
||||
});
|
||||
|
||||
$('#sidebar-notification-list').prepend(notificationHtml);
|
||||
|
|
|
|||
|
|
@ -8,6 +8,14 @@
|
|||
context.JK = context.JK || {};
|
||||
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) {
|
||||
switch(s.toLowerCase()){
|
||||
case "true": case "yes": case "1": return true;
|
||||
|
|
@ -70,6 +78,69 @@
|
|||
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
|
||||
// var template = "Hey {name}";
|
||||
// var vals = { name: "Jon" };
|
||||
|
|
@ -175,11 +246,46 @@
|
|||
return retVal;
|
||||
}
|
||||
|
||||
context.JK.formatDate = function(dateString) {
|
||||
context.JK.formatDateTime = function(dateString) {
|
||||
var date = new Date(dateString);
|
||||
return date.getFullYear() + "-" + context.JK.padString(date.getMonth()+1, 2) + "-" + context.JK.padString(date.getDate(), 2) + " @ " + date.toLocaleTimeString();
|
||||
}
|
||||
|
||||
// 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) {
|
||||
$.ajax({
|
||||
type: "GET",
|
||||
|
|
@ -279,6 +385,22 @@
|
|||
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.
|
||||
|
|
|
|||
|
|
@ -10,8 +10,10 @@
|
|||
*
|
||||
*= require_self
|
||||
*= require ./ie
|
||||
*= require jquery.bt
|
||||
*= require ./jamkazam
|
||||
*= require ./content
|
||||
*= require ./paginator
|
||||
*= require ./faders
|
||||
*= require ./header
|
||||
#= require ./user_dropdown
|
||||
|
|
@ -30,6 +32,7 @@
|
|||
*= require ./ftue
|
||||
*= require ./invitationDialog
|
||||
*= require ./recordingFinishedDialog
|
||||
*= require ./localRecordingsDialog
|
||||
*= require ./createSession
|
||||
*= require ./genreSelector
|
||||
*= require ./sessionList
|
||||
|
|
|
|||
|
|
@ -341,6 +341,8 @@ ul.shortcuts {
|
|||
border-color:#ED3618;
|
||||
}
|
||||
|
||||
|
||||
|
||||
span.arrow-right {
|
||||
display:inline-block;
|
||||
width: 0;
|
||||
|
|
|
|||
|
|
@ -460,4 +460,4 @@ div[layout-id=session], div[layout-id=ftue], .no-selection-range {
|
|||
-moz-user-select: none;
|
||||
-ms-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 {
|
||||
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 {
|
||||
width:60%;
|
||||
overflow:hidden;
|
||||
margin-top:6px;
|
||||
margin-top:9px;
|
||||
margin-bottom:8px;
|
||||
font-size:16px;
|
||||
}
|
||||
|
|
@ -477,6 +491,28 @@ table.vu td {
|
|||
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 {
|
||||
margin-top:10px;
|
||||
|
|
@ -571,7 +607,7 @@ table.vu td {
|
|||
width:93%;
|
||||
min-width:200px;
|
||||
background-color:#242323;
|
||||
position:relative;
|
||||
position:absolute;
|
||||
font-size:13px;
|
||||
text-align:center;
|
||||
}
|
||||
|
|
@ -593,6 +629,10 @@ table.vu td {
|
|||
margin-top:4px;
|
||||
}
|
||||
|
||||
.recording-time.duration-time {
|
||||
padding-left:2px;
|
||||
}
|
||||
|
||||
.recording-playback {
|
||||
display:inline-block;
|
||||
background-image:url(/assets/content/bkg_playcontrols.png);
|
||||
|
|
@ -601,12 +641,17 @@ table.vu td {
|
|||
width:65%;
|
||||
height:16px;
|
||||
margin-top:2px;
|
||||
cursor:pointer;
|
||||
}
|
||||
|
||||
.recording-slider {
|
||||
position:absolute;
|
||||
left:40px;
|
||||
left:0px;
|
||||
top:0px;
|
||||
|
||||
img {
|
||||
position:absolute;
|
||||
}
|
||||
}
|
||||
|
||||
.recording-current {
|
||||
|
|
|
|||
|
|
@ -1,49 +1,49 @@
|
|||
table.findsession-table {
|
||||
margin-top:6px;
|
||||
width:98%;
|
||||
font-size:11px;
|
||||
color:#fff;
|
||||
background-color:#262626;
|
||||
border:solid 1px #4d4d4d;
|
||||
}
|
||||
table.findsession-table, table.local-recordings {
|
||||
margin-top:6px;
|
||||
width:98%;
|
||||
font-size:11px;
|
||||
color:#fff;
|
||||
background-color:#262626;
|
||||
border:solid 1px #4d4d4d;
|
||||
|
||||
.findsession-table th {
|
||||
th {
|
||||
font-weight:300;
|
||||
background-color:#4d4d4d;
|
||||
padding:6px;
|
||||
border-right:solid 1px #333;
|
||||
}
|
||||
}
|
||||
|
||||
.findsession-table td {
|
||||
td {
|
||||
padding:9px 5px 5px 5px;
|
||||
border-right:solid 1px #333;
|
||||
border-top:solid 1px #333;
|
||||
vertical-align:top;
|
||||
white-space:normal;
|
||||
}
|
||||
}
|
||||
|
||||
.findsession-table .noborder {
|
||||
.noborder {
|
||||
border-right:none;
|
||||
}
|
||||
}
|
||||
|
||||
.findsession-table .musicians {
|
||||
.musicians {
|
||||
margin-top:-3px;
|
||||
}
|
||||
}
|
||||
|
||||
.findsession-table .musicians td {
|
||||
.musicians td {
|
||||
border-right:none;
|
||||
border-top:none;
|
||||
padding:3px;
|
||||
vertical-align:middle;
|
||||
}
|
||||
}
|
||||
|
||||
.findsession-table a {
|
||||
a {
|
||||
color:#fff;
|
||||
text-decoration:none;
|
||||
}
|
||||
}
|
||||
|
||||
.findsession-table a:hover {
|
||||
a:hover {
|
||||
color:#227985;
|
||||
}
|
||||
}
|
||||
|
||||
.latency-grey {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ class ApiClaimedRecordingsController < ApiController
|
|||
respond_to :json
|
||||
|
||||
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
|
||||
|
||||
def show
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ class ApiMusicSessionsController < ApiController
|
|||
|
||||
# have to be signed in currently to see this screen
|
||||
before_filter :api_signed_in_user
|
||||
before_filter :lookup_session, only: [:show, :update, :delete]
|
||||
before_filter :lookup_session, only: [:show, :update, :delete, :claimed_recording_start, :claimed_recording_stop]
|
||||
skip_before_filter :api_signed_in_user, only: [:perf_upload]
|
||||
|
||||
respond_to :json
|
||||
|
|
@ -122,10 +122,6 @@ class ApiMusicSessionsController < ApiController
|
|||
end
|
||||
end
|
||||
|
||||
def lookup_session
|
||||
@music_session = MusicSession.find(params[:id])
|
||||
end
|
||||
|
||||
def track_index
|
||||
@tracks = Track.index(current_user, params[:id])
|
||||
end
|
||||
|
|
@ -235,8 +231,38 @@ class ApiMusicSessionsController < ApiController
|
|||
# so... just return 200
|
||||
render :json => { :id => @perfdata.id }, :status => 200
|
||||
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
|
||||
|
|
|
|||
|
|
@ -18,10 +18,8 @@ child(:recording => :recording) {
|
|||
}
|
||||
|
||||
child(:recorded_tracks => :recorded_tracks) {
|
||||
attributes :id, :fully_uploaded, :url, :client_track_id
|
||||
child(:instrument => :instrument) {
|
||||
attributes :id, :description
|
||||
}
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
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|
|
||||
item.genres.map(&:description)
|
||||
|
|
@ -38,3 +38,30 @@ node(:join_requests, :if => lambda { |music_session| music_session.users.exists?
|
|||
}
|
||||
}
|
||||
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>
|
||||
|
||||
<div class="left w50 ml30">
|
||||
Preview Recording:<br/>
|
||||
<!-- recording play controls -->
|
||||
<div class="recording-controls">
|
||||
|
||||
<!-- play button -->
|
||||
<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 -->
|
||||
<%= render "play_controls" %>
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
|
|
|||
|
|
@ -94,13 +94,22 @@
|
|||
<!-- recordings -->
|
||||
<div class="session-recordings">
|
||||
<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>
|
||||
<p class="when-empty recordings">
|
||||
No Recordings:<br/><a>Open a Recording</a>
|
||||
</p>
|
||||
<div class="session-tracks-scroller">
|
||||
<div id="session-recordedtracks-container">
|
||||
<p class="when-empty recordings">
|
||||
No Recordings:<br/><a href="#" id="open-a-recording">Open a Recording</a>
|
||||
</p>
|
||||
</div>
|
||||
<br clear="all" />
|
||||
|
||||
<%= render "play_controls" %>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- recording name and close button -->
|
||||
|
|
@ -114,8 +123,6 @@
|
|||
</div>
|
||||
</div>
|
||||
-->
|
||||
<div class="session-tracks-scroller">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
|
@ -140,10 +147,10 @@
|
|||
<div id="div-track-close" track-id="{trackId}" class="track-close op30">
|
||||
<%= image_tag "content/icon_closetrack.png", {:width => 12, :height => 12} %>
|
||||
</div>
|
||||
<div class="avatar-med">
|
||||
<div class="{avatarClass}">
|
||||
<img src="{avatar}"/>
|
||||
</div>
|
||||
<div class="track-instrument">
|
||||
<div class="track-instrument {preMasteredClass}">
|
||||
<img src="/assets/{instrumentIcon}" width="45" height="45"/>
|
||||
</div>
|
||||
<div class="track-gain" mixer-id="{mixerId}"></div>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
<%= render "header" %>
|
||||
<%= render "home" %>
|
||||
<%= render "footer" %>
|
||||
<%= render "paginator" %>
|
||||
<%= render "searchResults" %>
|
||||
<%= render "faders" %>
|
||||
<%= render "vu_meters" %>
|
||||
|
|
@ -36,11 +37,13 @@
|
|||
<%= render "invitationDialog" %>
|
||||
<%= render "whatsNextDialog" %>
|
||||
<%= render "recordingFinishedDialog" %>
|
||||
<%= render "localRecordingsDialog" %>
|
||||
<%= render "notify" %>
|
||||
<%= render "client_update" %>
|
||||
<%= render "banner" %>
|
||||
<%= render "clients/banners/disconnected" %>
|
||||
<%= render "overlay_small" %>
|
||||
<%= render "help" %>
|
||||
|
||||
<script type="text/javascript">
|
||||
$(function() {
|
||||
|
|
@ -91,6 +94,9 @@
|
|||
var invitationDialog = new JK.InvitationDialog(JK.app);
|
||||
invitationDialog.initialize();
|
||||
|
||||
var localRecordingsDialog = new JK.LocalRecordingsDialog(JK.app);
|
||||
localRecordingsDialog.initialize();
|
||||
|
||||
var friendSelectorDialog = new JK.FriendSelectorDialog(JK.app);
|
||||
friendSelectorDialog.initialize();
|
||||
|
||||
|
|
@ -159,7 +165,8 @@
|
|||
findBandScreen.initialize();
|
||||
|
||||
var sessionScreen = new JK.SessionScreen(JK.app);
|
||||
sessionScreen.initialize();
|
||||
sessionScreen.initialize(localRecordingsDialog);
|
||||
|
||||
var sessionSettingsDialog = new JK.SessionSettingsDialog(JK.app, sessionScreen);
|
||||
sessionSettingsDialog.initialize();
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ if defined?(Bundler)
|
|||
# Bundler.require(:default, :assets, Rails.env)
|
||||
end
|
||||
|
||||
include JamRuby
|
||||
include JamRuby
|
||||
# require "rails/test_unit/railtie"
|
||||
|
||||
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_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
|
||||
|
||||
# genres
|
||||
|
|
|
|||
|
|
@ -30,6 +30,19 @@ describe ApiRecordingsController do
|
|||
response_body["errors"]["music_session"][0].should == ValidationMessages::ALREADY_BEING_RECORDED
|
||||
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
|
||||
user2 = FactoryGirl.create(:user)
|
||||
controller.current_user = user2
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ FactoryGirl.define do
|
|||
approval_required false
|
||||
musician_access true
|
||||
legal_terms true
|
||||
genres [JamRuby::Genre.first]
|
||||
|
||||
after(:create) { |session|
|
||||
MusicSessionHistory.save(session)
|
||||
|
|
|
|||
|
|
@ -664,6 +664,36 @@ describe "Music Session API ", :type => :api do
|
|||
tracks[1]["sound"].should == "stereo"
|
||||
tracks[1]["client_track_id"].should == "client_track_id1"
|
||||
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
|
||||
|
||||
|
||||
|
|
|
|||
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