404 lines
14 KiB
JavaScript
404 lines
14 KiB
JavaScript
(function (context, $) {
|
|
|
|
"use strict";
|
|
|
|
// Change underscore's default templating characters as they
|
|
// conflict withe the ERB rendering. Templates will use:
|
|
// {{ interpolate }}
|
|
// {% evaluate %}
|
|
// {{- escape }}
|
|
//
|
|
context._.templateSettings = {
|
|
evaluate: /\{%([\s\S]+?)%\}/g,
|
|
interpolate: /\{\{([\s\S]+?)\}\}/g,
|
|
escape: /\{\{-([\s\S]+?)\}\}/g
|
|
};
|
|
|
|
context.JK = context.JK || {};
|
|
|
|
var JamKazam = context.JK.JamKazam = function () {
|
|
var app;
|
|
var logger = context.JK.logger;
|
|
var rest = context.JK.Rest();
|
|
var inBadState = false;
|
|
var userDeferred = null;
|
|
var userData = null;
|
|
var self = this;
|
|
|
|
var opts = {
|
|
inClient: true, // specify false if you want the app object but none of the client-oriented features
|
|
layoutOpts: {
|
|
layoutFooter: true // specify false if you want footer to be left alone
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Dynamically build routes from markup. Any layout="screen" will get a route corresponding to
|
|
* his layout-id attribute. If a layout-arg attribute is present, that will be named as a data
|
|
* section of the route.
|
|
*/
|
|
function routing() {
|
|
var routes = context.RouteMap,
|
|
rules = {},
|
|
rule,
|
|
routingContext = {};
|
|
$('div[layout="screen"]').each(function () {
|
|
var target = $(this).attr('layout-id'),
|
|
targetUrl = target,
|
|
targetArg = $(this).attr('layout-arg'),
|
|
fn = function (data) {
|
|
data.screen = target;
|
|
app.layout.changeToScreen(target, data);
|
|
};
|
|
if (targetArg) {
|
|
targetUrl += "/:" + targetArg;
|
|
}
|
|
rules[target] = {route: '/' + targetUrl + '/:d?', method: target};
|
|
routingContext[target] = fn;
|
|
|
|
// allow dialogs to take an optional argument
|
|
rules[target+'opt'] = {route: '/' + targetUrl + '/:d?/d1:', method: target};
|
|
routingContext[target + 'opt'] = fn;
|
|
});
|
|
routes.context(routingContext);
|
|
for (rule in rules) if (rules.hasOwnProperty(rule)) routes.add(rules[rule]);
|
|
$(context).bind('hashchange', function(e) { app.layout.onHashChange(e, routes.handler)});
|
|
$(routes.handler);
|
|
}
|
|
|
|
/**
|
|
* This occurs when the websocket gateway loses a connection to the backend messaging system,
|
|
* resulting in severe loss of functionality
|
|
*/
|
|
function serverBadStateError() {
|
|
if (!inBadState) {
|
|
inBadState = true;
|
|
app.notify({title: "Server Unstable", text: "The server is currently unstable, resulting in feature loss. If you are experiencing any problems, please try to use JamKazam later."})
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This occurs when the websocket gateway loses a connection to the backend messaging system,
|
|
* resulting in severe loss of functionality
|
|
*/
|
|
function serverBadStateRecovered() {
|
|
if (inBadState) {
|
|
inBadState = false;
|
|
app.notify({title: "Server Recovered", text: "The server is now stable again. If you are still experiencing problems, either create a new music session or restart the client altogether."})
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This occurs when a new download from a recording has become available
|
|
*/
|
|
function downloadAvailable() {
|
|
context.jamClient.OnDownloadAvailable();
|
|
}
|
|
|
|
/**
|
|
* This most likely occurs when multiple tabs in the same browser are open, until we make a fix for this...
|
|
*/
|
|
function duplicateClientError() {
|
|
context.JK.Banner.showAlert("Duplicate Window (Development Mode Only)", "You have logged in another window in this browser. This window will continue to work but with degraded functionality. This limitation will soon be fixed.")
|
|
context.JK.JamServer.noReconnect = true;
|
|
}
|
|
|
|
function registerBadStateError() {
|
|
context.JK.JamServer.registerMessageCallback(context.JK.MessageType.SERVER_BAD_STATE_ERROR, serverBadStateError);
|
|
}
|
|
|
|
function registerBadStateRecovered() {
|
|
context.JK.JamServer.registerMessageCallback(context.JK.MessageType.SERVER_BAD_STATE_RECOVERED, serverBadStateRecovered);
|
|
}
|
|
|
|
function registerDownloadAvailable() {
|
|
context.JK.JamServer.registerMessageCallback(context.JK.MessageType.DOWNLOAD_AVAILABLE, downloadAvailable);
|
|
}
|
|
|
|
function registerDuplicateClientError() {
|
|
context.JK.JamServer.registerMessageCallback(context.JK.MessageType.SERVER_DUPLICATE_CLIENT_ERROR, duplicateClientError);
|
|
}
|
|
|
|
/**
|
|
* Generic error handler for Ajax calls.
|
|
*/
|
|
function ajaxError(jqXHR, textStatus, errorMessage) {
|
|
|
|
//var err;
|
|
//try { throw new Error('lurp'); } catch(e) { err = e };
|
|
//console.log("TRACE", JSON.stringify(err))
|
|
|
|
if (jqXHR.status === 404) {
|
|
logger.error("Unexpected ajax error: " + textStatus + ", msg:" + errorMessage);
|
|
app.notify({title: "Oops!", text: "What you were looking for is gone now."});
|
|
}
|
|
else if (jqXHR.status === 422) {
|
|
logger.error("Unexpected ajax error: " + textStatus + ", msg: " + errorMessage + ", response: " + jqXHR.responseText);
|
|
// present a nicer message
|
|
try {
|
|
var text = "<ul>";
|
|
var errorResponse = JSON.parse(jqXHR.responseText)["errors"];
|
|
for (var key in errorResponse) {
|
|
var errorsForKey = errorResponse[key];
|
|
logger.debug("key: " + key);
|
|
var prettyKey = context.JK.entityToPrintable[key];
|
|
if (!prettyKey) {
|
|
prettyKey = key;
|
|
}
|
|
for (var i = 0; i < errorsForKey.length; i++) {
|
|
|
|
text += "<li>" + prettyKey + " " + errorsForKey[i] + "</li>";
|
|
}
|
|
}
|
|
|
|
text += "<ul>";
|
|
|
|
app.notify({title: "Oops!", text: text, "icon_url": "/assets/content/icon_alert_big.png"});
|
|
}
|
|
catch (e) {
|
|
// give up; not formatted correctly
|
|
app.notify({title: textStatus, text: errorMessage, detail: jqXHR.responseText});
|
|
}
|
|
}
|
|
else {
|
|
|
|
app.notify({title: textStatus, text: errorMessage, detail: jqXHR.responseText});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Expose ajaxError.
|
|
*/
|
|
this.ajaxError = ajaxError;
|
|
|
|
/**
|
|
* Provide a handler object for events related to a particular screen
|
|
* being shown or hidden.
|
|
* @screen is a string corresponding to the screen's layout-id attribute
|
|
* @handler is an object with up to four optional keys:
|
|
* beforeHide, afterHide, beforeShow, afterShow, beforeDisconnect, which should all have
|
|
* functions as values. If there is data provided by the screen's route
|
|
* it will be provided to these functions.
|
|
*/
|
|
this.bindScreen = function (screen, handler) {
|
|
this.layout.bindScreen(screen, handler);
|
|
};
|
|
this.bindDialog = function (dialog, handler) {
|
|
this.layout.bindDialog(dialog, handler);
|
|
};
|
|
/**
|
|
* Allow individual wizard steps to register a function to be invokes
|
|
* when they are shown.
|
|
*/
|
|
this.registerWizardStepFunction = function (stepId, showFunction) {
|
|
this.layout.registerWizardStepFunction(stepId, showFunction);
|
|
};
|
|
|
|
/**
|
|
* Switch to the wizard step with the provided id.
|
|
*/
|
|
this.setWizardStep = function (targetStepId) {
|
|
this.layout.setWizardStep(targetStepId);
|
|
};
|
|
|
|
/**
|
|
* Show a notification. Expects an object with a
|
|
* title property and a text property.
|
|
*/
|
|
this.notify = function (message, descriptor) {
|
|
this.layout.notify(message, descriptor, true);
|
|
};
|
|
|
|
/** Shows an alert notification. Expects text, title */
|
|
this.notifyAlert = function (title, text) {
|
|
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);
|
|
logger.debug("Unprocessable entity sent from server:", JSON.stringify(errors))
|
|
this.notify({title: title, text: $errors, icon_url: "/assets/content/icon_alert_big.png"})
|
|
}
|
|
else if(jqXHR.status == 403) {
|
|
logger.debug("permission error sent from server:", jqXHR.responseText)
|
|
this.notify({title: 'Permission Error', text: 'You do not have permission to access this information', icon_url: "/assets/content/icon_alert_big.png"})
|
|
}
|
|
else {
|
|
if (jqXHR.responseText.indexOf('<!DOCTYPE html>') == 0 || jqXHR.responseText.indexOf('<html')) {
|
|
// we need to check more status codes and make tailored messages at this point
|
|
var showMore = $('<a href="#">Show Error Detail</a>');
|
|
showMore.data('responseText', jqXHR.responseText);
|
|
showMore.click(function () {
|
|
var self = $(this);
|
|
var text = self.data('responseText');
|
|
var bodyIndex = text.indexOf('<body');
|
|
if(bodyIndex > -1) {
|
|
text = text.substr(bodyIndex);
|
|
}
|
|
logger.debug("html", text);
|
|
$('#server-error-dialog .error-contents').html(text);
|
|
app.layout.showDialog('server-error-dialog')
|
|
return false;
|
|
});
|
|
this.notify({title: title, text: showMore, icon_url: "/assets/content/icon_alert_big.png"})
|
|
}
|
|
else {
|
|
this.notify({title: title, text: "status=" + jqXHR.status + ", message=" + jqXHR.responseText, icon_url: "/assets/content/icon_alert_big.png"})
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initialize any common events.
|
|
*/
|
|
function events() {
|
|
// Hook up the screen navigation controls.
|
|
$(".content-nav .arrow-left").click(function (evt) {
|
|
evt.preventDefault();
|
|
context.history.back();
|
|
return false;
|
|
});
|
|
$(".content-nav .arrow-right").click(function (evt) {
|
|
evt.preventDefault();
|
|
context.history.forward();
|
|
return false;
|
|
});
|
|
|
|
context.JK.popExternalLinks();
|
|
}
|
|
|
|
// Due to timing of initialization, this must be called externally
|
|
// after all screens have been given a chance to initialize.
|
|
// It is called from index.html.erb after connecting, and initialization
|
|
// of other screens.
|
|
function initialRouting() {
|
|
routing();
|
|
|
|
var hash = context.location.hash;
|
|
|
|
try {
|
|
context.RouteMap.parse(hash);
|
|
}
|
|
catch (e) {
|
|
logger.debug("ignoring bogus screen name: %o", hash)
|
|
hash = null;
|
|
}
|
|
|
|
var url = '/client#/home';
|
|
if (hash) {
|
|
url = hash;
|
|
}
|
|
|
|
logger.debug("Changing screen to " + url);
|
|
context.location = url;
|
|
}
|
|
|
|
this.updateUserModel = function(userUpdateData) {
|
|
|
|
var update = rest.updateUser(userUpdateData)
|
|
update.done(function() {
|
|
logger.debug("updating user info")
|
|
userDeferred = update; // update the global user object if this succeeded
|
|
})
|
|
update.done(this.updateUserCache)
|
|
return update;
|
|
}
|
|
|
|
// call .done/.fail on this to wait for safe user data
|
|
this.user = function() {
|
|
return userDeferred;
|
|
}
|
|
|
|
// gets the most recent user data. can be null when app is still initializing.
|
|
// user app.user() if initialize sequence is unknown/asynchronous
|
|
this.currentUser = function() {
|
|
if(userData == null) {
|
|
throw "currentUser has null user data"
|
|
}
|
|
return userData;
|
|
}
|
|
|
|
this.activeElementEvent = function(evtName, data) {
|
|
return this.layout.activeElementEvent(evtName, data);
|
|
}
|
|
|
|
this.updateNotificationSeen = function(notificationId, notificationCreatedAt) {
|
|
context.JK.JamServer.updateNotificationSeen(notificationId, notificationCreatedAt);
|
|
}
|
|
|
|
this.unloadFunction = function () {
|
|
logger.debug("window.unload function called.");
|
|
|
|
context.JK.JamServer.close(false);
|
|
|
|
if (context.jamClient) {
|
|
// Unregister for callbacks.
|
|
context.jamClient.RegisterRecordingCallbacks("", "", "", "", "");
|
|
context.jamClient.SessionRegisterCallback("");
|
|
context.jamClient.SessionSetAlertCallback("");
|
|
context.jamClient.FTUERegisterVUCallbacks("", "", "");
|
|
context.jamClient.FTUERegisterLatencyCallback("");
|
|
context.jamClient.RegisterVolChangeCallBack("");
|
|
}
|
|
};
|
|
|
|
this.updateUserCache = function(_userData) {
|
|
userData = _userData
|
|
}
|
|
|
|
this.initialize = function (inOpts) {
|
|
var url, hash;
|
|
app = this;
|
|
this.opts = $.extend(opts, inOpts);
|
|
this.layout = new context.JK.Layout();
|
|
this.layout.initialize(this.opts.layoutOpts);
|
|
events();
|
|
this.layout.handleDialogState();
|
|
|
|
userDeferred = rest.getUserDetail();
|
|
if (userDeferred) {
|
|
userDeferred.done(this.updateUserCache)
|
|
|
|
if (opts.inClient) {
|
|
registerBadStateRecovered();
|
|
registerBadStateError();
|
|
registerDownloadAvailable();
|
|
registerDuplicateClientError();
|
|
context.JK.FaderHelpers.initialize();
|
|
context.window.onunload = this.unloadFunction;
|
|
|
|
userDeferred.fail(function(jqXHR) {
|
|
app.notify({title: "Unable to Load User", text: "You should reload the page."})
|
|
});
|
|
}
|
|
} // if userDeferred
|
|
|
|
$(document).triggerHandler('JAMKAZAM_READY', {app:app})
|
|
|
|
};
|
|
|
|
// Holder for a function to invoke upon successfully completing the FTUE.
|
|
// See createSession.submitForm as an example.
|
|
this.afterFtue = null;
|
|
|
|
// enable temporary suspension of heartbeat for fine-grained control
|
|
this.heartbeatActive = true;
|
|
|
|
/**
|
|
* Expose clientId as a public variable.
|
|
* Will be set upon LOGIN_ACK
|
|
*/
|
|
this.clientId = null;
|
|
this.initialRouting = initialRouting;
|
|
|
|
$(document).triggerHandler('JAMKAZAM_CONSTRUCTED', {app:this})
|
|
return this;
|
|
};
|
|
|
|
})(window, jQuery); |