diff --git a/src/app/app.js b/src/app/app.js index 54b75518..a35520df 100644 --- a/src/app/app.js +++ b/src/app/app.js @@ -67,6 +67,7 @@ var app = angular.module('baw', 'templates-app', /* these are the precompiled templates */ 'templates-common', + 'bawApp.services.resource', // a custom wrapped around ngResource 'bawApp.directives', /* our directives.js */ 'bawApp.directives.ngAudio', /* our directives.js */ 'bawApp.filters', /* our filters.js */ diff --git a/src/app/listen/listen.js b/src/app/listen/listen.js index 7f7bbec5..92105e3b 100644 --- a/src/app/listen/listen.js +++ b/src/app/listen/listen.js @@ -18,6 +18,8 @@ angular.module('bawApp.listen', ['decipher.tags', 'ui.bootstrap.typeahead']) 'Site', 'Project', 'UserProfile', + 'UserProfileEvents', + 'Bookmark', /** * The listen controller. * @param $scope @@ -42,7 +44,7 @@ angular.module('bawApp.listen', ['decipher.tags', 'ui.bootstrap.typeahead']) */ function ListenCtrl( $scope, $resource, $location, $routeParams, $route, $q, paths, constants, $url, ngAudioEvents, - AudioRecording, Media, AudioEvent, Tag, Taggings, Site, Project, UserProfile) { + AudioRecording, Media, AudioEvent, Tag, Taggings, Site, Project, UserProfile, UserProfileEvents, Bookmark) { var CHUNK_DURATION_SECONDS = constants.listen.chunkDurationSeconds; @@ -96,7 +98,7 @@ angular.module('bawApp.listen', ['decipher.tags', 'ui.bootstrap.typeahead']) $scope.model.audioElement.volume = UserProfile.profile.preferences.volume; $scope.model.audioElement.muted = UserProfile.profile.preferences.muted; }; - $scope.$on(UserProfile.eventKeys.loaded, profileLoaded); + $scope.$on(UserProfileEvents.loaded, profileLoaded); if (UserProfile.profile && UserProfile.profile.preferences) { profileLoaded(null, UserProfile); } diff --git a/src/baw.configuration.tpl.js b/src/baw.configuration.tpl.js index ea5efdba..e81eb069 100644 --- a/src/baw.configuration.tpl.js +++ b/src/baw.configuration.tpl.js @@ -4,55 +4,10 @@ angular.module('bawApp.configuration', ['url']) * This module contains static paths that are stored centrally for easy configuration. * App dependent. * - * The root properties changed when the app is built with grunt. + * The root properties are changed when the app is built with grunt. */ .constant("conf.paths", (function () { - /** - * Joins path fragments together. - * @param {...[string]} fragments - * @returns {*} - */ - function joinPathFragments(fragments) { - fragments = Array.prototype.slice.call(arguments, 0); - - if (fragments.length === 0) { - return undefined; - } - else if (fragments.length === 1) { - return fragments[0]; - } - else { - var path = fragments[0]; - - if (path.slice(-1) === "/") { - path = path.slice(0, -1); - } - - for (var i = 1; i < fragments.length; i++) { - var f = fragments[i]; - - if ((typeof f) !== "string") { - throw "joinPathFragments: Path fragment " + f + " is not a string"; - } - - var hasFirst = f[0] === "/"; - var hasLast = (f.slice(-1))[0] === "/"; - - if (!hasFirst) { - f = "/" + f; - } - - if (hasLast && i !== (fragments.length - 1)) { - f = f.slice(0, -1); - } - - path += f; - } - - return path; - } - } var paths = { api: { @@ -92,6 +47,9 @@ angular.module('bawApp.configuration', ['url']) user: { profile: "/my_account", settings: "/my_account/prefs" + }, + bookmark: { + show: "user_accounts/{userId}/bookmarks/{bookmarkId}" } }, links: { @@ -139,6 +97,52 @@ angular.module('bawApp.configuration', ['url']) }; + /** + * Joins path fragments together. + * @param {...[string]} fragments + * @returns {*} + */ + function joinPathFragments(fragments) { + fragments = Array.prototype.slice.call(arguments, 0); + + if (fragments.length === 0) { + return undefined; + } + else if (fragments.length === 1) { + return fragments[0]; + } + else { + var path = fragments[0]; + + if (path.slice(-1) === "/") { + path = path.slice(0, -1); + } + + for (var i = 1; i < fragments.length; i++) { + var f = fragments[i]; + + if ((typeof f) !== "string") { + throw "joinPathFragments: Path fragment " + f + " is not a string"; + } + + var hasFirst = f[0] === "/"; + var hasLast = (f.slice(-1))[0] === "/"; + + if (!hasFirst) { + f = "/" + f; + } + + if (hasLast && i !== (fragments.length - 1)) { + f = f.slice(0, -1); + } + + path += f; + } + + return path; + } + } + // add helper paths function recursivePath(source, root) { for (var key in source) { @@ -164,7 +168,7 @@ angular.module('bawApp.configuration', ['url']) return paths; })() - ) +) .constant("conf.constants", { listen: { chunkDurationSeconds: 30.0, @@ -186,5 +190,9 @@ angular.module('bawApp.configuration', ['url']) }, annotationLibrary: { paddingSeconds: 1.0 + }, + bookmark: { + lastPlaybackPositionName: "Last playback position", + appCategory: "<>" } }); \ No newline at end of file diff --git a/src/components/services/bawResource.js b/src/components/services/bawResource.js new file mode 100644 index 00000000..59e63207 --- /dev/null +++ b/src/components/services/bawResource.js @@ -0,0 +1,36 @@ +angular.module("bawApp.services.resource", ["ngResource"]) + .factory("bawResource", ["$resource", function ($resource) { + + /** + * + * @param uri + * @returns {*} + */ + function uriConvert(uri) { + // find all place holders in this form: '{identifier}' + // replace with placeholder in this form: ':identifier' + return uri.replace(/(\{([^{}]*)\})/g, ":$2"); + } + + /** + * @name bawResource + * Helper method for adding a put request onto the standard angular resource service + * @param {string} path - the web server path + * @param {Object} paramDefaults - the default parameters + * @param {Object} [actions] - a set of actions to also add (extend) + * @return {*} + */ + var bawResource = function resourcePut(path, paramDefaults, actions) { + path = uriConvert(path); + + var a = actions || {}; + a.update = a.update || { method: 'PUT' }; + var resource = $resource(path, paramDefaults, a); + + resource.modifiedPath = path; + + return resource; + }; + + return bawResource; + }]); \ No newline at end of file diff --git a/src/components/services/bawResource.spec.js b/src/components/services/bawResource.spec.js new file mode 100644 index 00000000..1a61a57b --- /dev/null +++ b/src/components/services/bawResource.spec.js @@ -0,0 +1,24 @@ +describe("The bawResource service", function () { + + var Bookmark; + + beforeEach(module('bawApp.services')); + + beforeEach(inject(["Bookmark", function (providedBookmark) { + Bookmark = providedBookmark; + }])); + + + it("should return a resource constructor that includes update/put", function () { + + expect(Bookmark).toImplement({ + "get": null, + "save": null, + "query": null, + "remove": null, + "delete": null, + "update": null, + "modifiedPath": null + }); + }); +}); \ No newline at end of file diff --git a/src/components/services/bookmark.js b/src/components/services/bookmark.js new file mode 100644 index 00000000..5f62a79a --- /dev/null +++ b/src/components/services/bookmark.js @@ -0,0 +1,79 @@ +var bawss = bawss || angular.module("bawApp.services", ['bawApp.services.resource', 'bawApp.configuration']); + + +bawss.factory('Bookmark', [ + 'bawResource', + 'conf.paths', + 'conf.constants', + 'UserProfile', + '$q', + function (bawResource, paths, constants, UserProfile, $q) { + var bc = constants.bookmark; + + // valid query options: category + // required parameters: userId + // optional parameters: bookmarkId + + // at the moment we only support bookmark modification for users (not for recordings) + var resource = bawResource(paths.api.routes.bookmark.showAbsolute, {}); + + // retrieve or set the playback bookmark + resource.applicationBookmarks = {}; + function getApplicationBookmarks(userProfile) { + console.info("User profile hook success, retrieving app bookmarks", arguments); + var deferred = $q.defer(); + + resource.query({ + category: bc.appCategory, + userId: userProfile.id + }, + function appBookmarksQuerySuccess(values, headers) { + console.info("Application bookmarks received", values); + + // transform into associative hash + values.forEach(function (value, index) { + resource.applicationBookmarks[value.name] = value; + }); + + deferred.resolve(values); + }, + function appBookmarksQueryFailure() { + console.error("Retrieving application bookmarks failed"); + + deferred.reject(); + }); + + return deferred.promise; + } + + resource.applicationBookmarksPromise = UserProfile.get.then( + getApplicationBookmarks, + function () { + console.error("user profile hook failure", arguments); + }); + + + resource.savePlaybackPosition = function savePlaybackPosition(recordingId, offset) { + var bookmark = resource.applicationBookmarks[bc.lastPlaybackPositionName]; + if (bookmark) { + // update + bookmark.offsetSeconds = offset; + bookmark.audioRecordingId = recordingId; + + } + else { + // create + bookmark = { + name: bc.lastPlaybackPositionName, + category: bc.appCategory, + offsetSeconds: offset, + audioRecordingId: recordingId + }; + + + } + }; + + + return resource; + }]); \ No newline at end of file diff --git a/src/components/services/bookmark.spec.js b/src/components/services/bookmark.spec.js new file mode 100644 index 00000000..438b4119 --- /dev/null +++ b/src/components/services/bookmark.spec.js @@ -0,0 +1,20 @@ +describe("The bookmark service", function () { + + var bawResource; + + beforeEach(module('bawApp.services')); + + beforeEach(inject(["Bookmark", function (providedBawResource) { + bawResource = providedBawResource; + }])); + + + it("will return a promise for retrieving application bookmarks", function() { + + expect(bawResource.applicationBookmarksPromise).toImplement({ + catch: null, + finally: null, + then: null + }); + }); +}); \ No newline at end of file diff --git a/src/components/services/services.js b/src/components/services/services.js index e7e9e16d..1661dcb9 100644 --- a/src/components/services/services.js +++ b/src/components/services/services.js @@ -23,7 +23,7 @@ return uri.replace(/(\{([^{}]*)\})/g, ":$2"); } - var bawss = angular.module("bawApp.services", ['ngResource', 'bawApp.configuration']); + var bawss = bawss || angular.module("bawApp.services", ['ngResource', 'bawApp.configuration']); bawss.factory('Project', [ '$resource', 'conf.paths', function ($resource, paths) { return resourcePut($resource, uriConvert(paths.api.routes.projectAbsolute), {projectId: "@projectId"}); diff --git a/src/components/services/url.js b/src/components/services/url.js index 20f7a6ac..47d7a100 100644 --- a/src/components/services/url.js +++ b/src/components/services/url.js @@ -2,17 +2,6 @@ angular.module('url', ['ng']). service('$url', function () { - var copy = angular.copy, - equals = angular.equals, - extend = angular.extend, - forEach = angular.forEach, - isDefined = angular.isDefined, - isFunction = angular.isFunction, - isString = angular.isString, - jqLite = angular.element, - noop = angular.noop, - toJson = angular.toJson; - function fixedEncodeURIComponent(str) { str = str || ""; return encodeURIComponent(str) @@ -23,6 +12,7 @@ angular.module('url', ['ng']). .replace(/\*/g, '%2A') .replace(/%20/g, '+'); } + this.fixedEncodeURIComponent = fixedEncodeURIComponent; /** @@ -37,7 +27,7 @@ angular.module('url', ['ng']). * / "*" / "+" / "," / ";" / "=" */ function encodeUriQuery(val, pctEncodeSpaces) { - if(angular.isUndefined(val) || val === null){ + if (angular.isUndefined(val) || val === null) { return ''; } return encodeURIComponent(val). @@ -47,6 +37,7 @@ angular.module('url', ['ng']). replace(/%2C/gi, ','). replace(/%20/g, (pctEncodeSpaces ? '%20' : '+')); } + this.encodeUriQuery = encodeUriQuery; function toKeyValue(obj, validateKeys) { @@ -69,9 +60,10 @@ angular.module('url', ['ng']). }); return parts.length ? parts.join('&') : ''; } + this.toKeyValue = toKeyValue; - this.formatUri = function(uri, values) { + this.formatUri = function (uri, values) { // first format string var result = uri.formatReturnUnused(values), @@ -88,9 +80,9 @@ angular.module('url', ['ng']). // formatted = formatted.slice(0, 1); //} - var query = toKeyValue(unused, true); + var query = toKeyValue(unused, true); - if (formatted.indexOf("?") === -1 && query.length > 0) { + if (formatted.indexOf("?") === -1 && query.length > 0) { formatted += "?"; } @@ -101,7 +93,4 @@ angular.module('url', ['ng']). }; - - - }); \ No newline at end of file diff --git a/src/components/services/userProfile.js b/src/components/services/userProfile.js index fc53a67e..0e1af1cd 100644 --- a/src/components/services/userProfile.js +++ b/src/components/services/userProfile.js @@ -1,82 +1,82 @@ var bawss = bawss || angular.module("bawApp.services", ["ngResource", "bawApp.configuration"]); -bawss.factory("UserProfile", [ - "$rootScope", - "$http", - "conf.paths", - "conf.constants", - function ($rootScope, $http, paths, constants) { - var profileUrl = paths.api.routes.user.profileAbsolute, - preferencesUrl = paths.api.routes.user.settingsAbsolute, - eventKeys = { - "loaded": "UserProfile:Loaded"/*, - "preferencesChanged": "UserProfile:PreferencesChanged"*/ +bawss + .constant("UserProfileEvents", { + "loaded": "UserProfile:Loaded"/*, + "preferencesChanged": "UserProfile:PreferencesChanged"*/ + }) + .factory("UserProfile", [ + "$rootScope", + "$http", + "conf.paths", + "conf.constants", + "UserProfileEvents", + function ($rootScope, $http, paths, constants, UserProfileEvents) { + var profileUrl = paths.api.routes.user.profileAbsolute, + preferencesUrl = paths.api.routes.user.settingsAbsolute; + + var exports = { }; - var exports = { - eventKeys: eventKeys - }; - - var throttleCount = 0, - throttleAmount = 1000; - - /** - * Update the server's stored settings. - * Calls to this function are throttled. - */ - exports.updatePreferences = function throttleWrapper() { - console.debug("updatePreferences: throttled"); - throttleCount++; + var throttleCount = 0, + throttleAmount = 1000; + + /** + * Update the server's stored settings. + * Calls to this function are throttled. + */ + exports.updatePreferences = function throttleWrapper() { + console.debug("updatePreferences: throttled"); + throttleCount++; + + _.throttle(function updatePreferences() { + console.debug("updatePreferences: sending to server, waited %s attempts", throttleCount); + throttleCount = 0; + + $http.put(preferencesUrl, exports.profile.preferences).then( + function success(response) { + console.info("updatePreferences:success"); + }, + function error(response) { + console.error("updatePreferences:failed", response); + } + ); + }, throttleAmount)(); + }; - _.throttle(function updatePreferences() { - console.debug("updatePreferences: sending to server, waited %s attempts", throttleCount); - throttleCount = 0; + exports.profile = null; - $http.put(preferencesUrl, exports.profile.preferences).then( + exports.get = $http.get(profileUrl).then( function success(response) { - console.info("updatePreferences:success"); + console.log("User profile loaded"); + + exports.profile = (new baw.UserProfile(response.data, constants.defaultProfile)); + return exports.profile; }, function error(response) { - console.error("updatePreferences:failed", response); - } - ); - }, throttleAmount)(); - }; - - exports.profile = null; + console.error("User profile load failed, default profile loaded", response); - exports.get = function () { + exports.profile = (new baw.UserProfile(null, constants.defaultProfile)); + } + ).finally(function () { + $rootScope.$broadcast(UserProfileEvents.loaded, exports); + }); - $http.get(profileUrl).then( - function success(response) { - console.log("User profile loaded"); - exports.profile = (new baw.UserProfile(response.data, constants.defaultProfile)); - }, - function error(response) { - console.error("User profile load failed, default profile loaded", response); + exports.listen = function (events) { + angular.forEach(events, function (callback, key) { + $rootScope.$on(key, function (event, value) { - exports.profile = (new baw.UserProfile(null, constants.defaultProfile)); - } - ).finally(function () { - $rootScope.$broadcast(eventKeys.loaded, exports); + callback.apply(null, [key, exports, value]); + }); }); - }; - - exports.listen = function (events) { - angular.forEach(events, function (callback, key) { - $rootScope.$on(key, function (event, value) { - - callback.apply(null, [key, exports, value]); - }); - }); - }; + }; - // download asap (async) - exports.get(); + // download asap (async) + //exports.get(); - // return api - return exports; + // return api + return exports; - } -]); \ No newline at end of file + } + ]); \ No newline at end of file