+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/trainer/app/js/7MinWorkout/directives.js b/trainer/app/js/7MinWorkout/directives.js
new file mode 100644
index 0000000..b7c3409
--- /dev/null
+++ b/trainer/app/js/7MinWorkout/directives.js
@@ -0,0 +1,39 @@
+angular.module('7minWorkout').directive('owlCarousel', ['$compile', '$timeout', function ($compile, $timeout) {
+ var owl = null;
+ return {
+ scope: {
+ options: '=',
+ source: '=',
+ onUpdate: '&',
+ },
+ link: function (scope, element, attr) {
+ var defaultOptions = {
+ singleItem: true,
+ pagination: false,
+ afterAction: function () {
+ var itemIndex = this.currentItem;
+ $timeout(function () {
+ scope.onUpdate({ currentItemIndex: itemIndex });
+ })
+ },
+ };
+ if (scope.options) angular.extend(defaultOptions, scope.options);
+ scope.$watch("source", function (newValue) {
+ if (newValue) {
+ $timeout(function () {
+ owl = element.owlCarousel(defaultOptions);
+ }, 0);
+ }
+ });
+ },
+ controller: ['$scope', '$attrs', function ($scope, $attrs) {
+ if ($attrs.owlCarousel) $scope.$parent[$attrs.owlCarousel] = this;
+ this.next = function () {
+ owl.trigger('owl.next');
+ };
+ this.previous = function () {
+ owl.trigger('owl.prev');
+ };
+ }]
+ };
+}]);
\ No newline at end of file
diff --git a/trainer/app/js/7MinWorkout/filters.spec.js b/trainer/app/js/7MinWorkout/filters.spec.js
new file mode 100644
index 0000000..bc46a2f
--- /dev/null
+++ b/trainer/app/js/7MinWorkout/filters.spec.js
@@ -0,0 +1,19 @@
+describe("Filters", function () {
+ beforeEach(module('7minWorkout'));
+
+ describe("secondsToTime filter", function () {
+ it('should convert integer to time format', inject(function ($filter) {
+ expect($filter("secondsToTime")(5)).toBe("00:00:05");
+
+ expect($filter("secondsToTime")(65)).toBe("00:01:05");
+
+ expect($filter("secondsToTime")(3610)).toBe("01:00:10");
+ }));
+
+ it('should convert return 00:00:00 if not integer', inject(function ($filter) {
+ expect($filter("secondsToTime")("")).toBe("00:00:00");
+
+ expect($filter("secondsToTime")("test")).toBe("00:00:00");
+ }));
+ });
+});
\ No newline at end of file
diff --git a/trainer/app/js/7MinWorkout/services.js b/trainer/app/js/7MinWorkout/services.js
new file mode 100644
index 0000000..e18b7d5
--- /dev/null
+++ b/trainer/app/js/7MinWorkout/services.js
@@ -0,0 +1,45 @@
+'use strict';
+
+/* Services */
+angular.module('7minWorkout')
+ .factory('workoutHistoryTracker', ['$rootScope', 'appEvents', 'localStorageService', function ($rootScope, appEvents, localStorageService) {
+ var maxHistoryItems = 20 //We only track for last 20 exercise
+ , storageKey = "workouthistory"
+ , workoutHistory = localStorageService.get(storageKey) || []
+ , currentWorkoutLog = null
+ , service = {};
+
+ service.startTracking = function () {
+ currentWorkoutLog = { startedOn: new Date().toISOString(), completed: false, exercisesDone: 0 };
+ if (workoutHistory.length >= maxHistoryItems) {
+ workoutHistory.shift();
+ }
+ workoutHistory.push(currentWorkoutLog);
+ localStorageService.add(storageKey, workoutHistory);
+ };
+
+ $rootScope.$on(appEvents.workout.exerciseStarted, function (e, args) {
+ currentWorkoutLog.lastExercise = args.title;
+ ++currentWorkoutLog.exercisesDone;
+ localStorageService.add(storageKey, workoutHistory);
+ });
+
+ $rootScope.$on("$routeChangeSuccess", function (e, args) {
+ if (currentWorkoutLog) {
+ service.endTracking(false); // End the current tracking if in progress the route changes.
+ }
+ });
+
+ service.endTracking = function (completed) {
+ currentWorkoutLog.completed = completed;
+ currentWorkoutLog.endedOn = new Date().toISOString();
+ currentWorkoutLog = null;
+ localStorageService.add(storageKey, workoutHistory);
+ };
+
+ service.getHistory = function () {
+ return workoutHistory;
+ }
+
+ return service;
+ }]);
\ No newline at end of file
diff --git a/trainer/app/js/7MinWorkout/services.spec.js b/trainer/app/js/7MinWorkout/services.spec.js
new file mode 100644
index 0000000..e69de29
diff --git a/trainer/app/js/7MinWorkout/workout.js b/trainer/app/js/7MinWorkout/workout.js
new file mode 100644
index 0000000..d8b63f6
--- /dev/null
+++ b/trainer/app/js/7MinWorkout/workout.js
@@ -0,0 +1,196 @@
+'use strict';
+
+/* Controllers */
+
+angular.module('7minWorkout')
+ .controller('WorkoutController', ['$scope', '$interval', '$location', 'workoutHistoryTracker', 'appEvents', 'WorkoutService', '$routeParams', 'Exercise', function ($scope, $interval, $location, workoutHistoryTracker, appEvents, WorkoutService, $routeParams, Exercise) {
+ var restExercise;
+ var exerciseIntervalPromise;
+ var startWorkout = function () {
+ WorkoutService
+ .getWorkout($routeParams.id)
+ .then(function (workout) {
+ $scope.workoutPlan = workout;
+ $scope.workoutTimeRemaining = $scope.workoutPlan.totalWorkoutDuration();
+ restExercise = {
+ details: new Exercise({
+ name: "rest",
+ title: "Relax!",
+ description: "Relax a bit!",
+ image: "img/rest.png",
+ }),
+ duration: $scope.workoutPlan.restBetweenExercise
+ };
+ workoutHistoryTracker.startTracking();
+ $scope.currentExerciseIndex = -1;
+ fillImages();
+ startExercise($scope.workoutPlan.exercises[0]);
+ });
+ };
+
+ var fillImages = function () {
+ $scope.exerciseImages = [];
+ angular.forEach($scope.workoutPlan.exercises, function (exercise, index) {
+ $scope.exerciseImages.push(exercise.details.image);
+ if (index < $scope.workoutPlan.exercises.length - 1) $scope.exerciseImages.push("img/rest.png");
+ });
+ }
+
+ var startExercise = function (exercisePlan) {
+ $scope.currentExercise = exercisePlan;
+ $scope.currentExerciseDuration = 0;
+
+ if (exercisePlan.details.name != 'rest') {
+ $scope.currentExerciseIndex++;
+ $scope.$emit(appEvents.workout.exerciseStarted, exercisePlan.details);
+ }
+ exerciseIntervalPromise = startExerciseTimeTracking();
+ };
+
+ var getNextExercise = function (currentExercisePlan) {
+ var nextExercise = null;
+ if (currentExercisePlan === restExercise) {
+ nextExercise = $scope.workoutPlan.exercises[$scope.currentExerciseIndex + 1];
+ }
+ else {
+ if ($scope.currentExerciseIndex < $scope.workoutPlan.exercises.length - 1) {
+ nextExercise = restExercise;
+ }
+ }
+ return nextExercise;
+ };
+
+ $scope.pauseWorkout = function () {
+ $interval.cancel(exerciseIntervalPromise);
+ $scope.workoutPaused = true;
+ };
+
+ $scope.resumeWorkout = function () {
+ if (!$scope.workoutPaused) return;
+ exerciseIntervalPromise = startExerciseTimeTracking();
+ $scope.workoutPaused = false;
+ };
+
+ $scope.pauseResumeToggle = function () {
+ if ($scope.workoutPaused) {
+ $scope.resumeWorkout();
+ }
+ else {
+ $scope.pauseWorkout();
+ }
+ }
+
+ var startExerciseTimeTracking = function () {
+ var promise = $interval(function () {
+ ++$scope.currentExerciseDuration;
+ --$scope.workoutTimeRemaining;
+ }, 1000, $scope.currentExercise.duration - $scope.currentExerciseDuration);
+
+ promise.then(function () {
+ var next = getNextExercise($scope.currentExercise);
+ if (next) {
+ $scope.carousel.next();
+ startExercise(next);
+ }
+ else {
+ workoutComplete();
+ }
+ }, function (error) {
+ console.log('Inteval promise cancelled. Error reason -' + error);
+ });
+ return promise;
+ }
+
+ $scope.onKeyPressed = function (event) {
+ if (event.which == 80 || event.which == 112) { // 'p' or 'P' key to toggle pause and resume.
+ $scope.pauseResumeToggle();
+ }
+ };
+
+ $scope.imageUpdated = function (imageIndex) {
+ console.log($scope.exerciseImages[imageIndex]);
+ };
+
+ var workoutComplete = function () {
+ workoutHistoryTracker.endTracking(true);
+ $location.path('/finish');
+ }
+
+
+ //$scope.$watch('currentExerciseDuration', function (nVal) {
+ // if (nVal == $scope.currentExercise.duration) {
+ // var next = getNextExercise($scope.currentExercise);
+ // if (next) {
+ // startExercise(next);
+ // } else {
+ // console.log("Workout complete!")
+ // }
+ // }
+ //});
+
+ var init = function () {
+ startWorkout();
+ };
+
+ init();
+ }]);
+
+angular.module('7minWorkout')
+ .controller('WorkoutAudioController', ['$scope', '$interval', '$location', '$timeout', function ($scope, $interval, $location, $timeout) {
+ $scope.exercisesAudio = [];
+
+ var workoutPlanwatch = $scope.$watch('workoutPlan', function (newValue, oldValue) {
+ if (newValue) {
+ angular.forEach($scope.workoutPlan.exercises, function (exercise) {
+ $scope.exercisesAudio.push({ src: exercise.details.nameSound, type: "audio/wav" });
+ });
+ workoutPlanwatch(); //unbind the watch.
+ }
+ });
+
+ $scope.$watch('currentExercise', function (newValue, oldValue) {
+ if (newValue && newValue != oldValue) {
+ if ($scope.currentExercise.details.name == 'rest') {
+ $timeout(function () {
+ $scope.nextUpAudio.play();
+ }, 2000);
+ $timeout(function () {
+ $scope.nextUpExerciseAudio.play($scope.currentExerciseIndex + 1, true);
+ }, 3000);
+ }
+ }
+ });
+
+ $scope.$watch('currentExerciseDuration', function (newValue, oldValue) {
+ if (newValue) {
+ if (newValue == Math.floor($scope.currentExercise.duration / 2) && $scope.currentExercise.details.name != 'rest') {
+ $scope.halfWayAudio.play();
+ }
+ else if (newValue == $scope.currentExercise.duration - 3) {
+ $scope.aboutToCompleteAudio.play();
+ }
+ }
+ });
+
+ $scope.$watch('workoutPaused', function (newValue, oldValue) {
+ if (newValue) {
+ $scope.ticksAudio.pause();
+ $scope.nextUpAudio.pause();
+ $scope.nextUpExerciseAudio.pause();
+ $scope.halfWayAudio.pause();
+ $scope.aboutToCompleteAudio.pause();
+ }
+ else {
+ if (angular.isUndefined(newValue)) return;
+ $scope.ticksAudio.play();
+ if ($scope.halfWayAudio.currentTime > 0 && $scope.halfWayAudio.currentTime < $scope.halfWayAudio.duration) $scope.halfWayAudio.play();
+ if ($scope.aboutToCompleteAudio.currentTime > 0 && $scope.aboutToCompleteAudio.currentTime < $scope.aboutToCompleteAudio.duration) $scope.aboutToCompleteAudio.play();
+ }
+ });
+
+ var init = function () {
+ }
+
+ init();
+
+ }]);
\ No newline at end of file
diff --git a/trainer/app/js/7MinWorkout/workout.spec.js b/trainer/app/js/7MinWorkout/workout.spec.js
new file mode 100644
index 0000000..c46efcd
--- /dev/null
+++ b/trainer/app/js/7MinWorkout/workout.spec.js
@@ -0,0 +1,364 @@
+///
+describe("Controllers", function () {
+
+ beforeEach(module('app'));
+
+ beforeEach(module(function ($provide) {
+ $provide.factory("WorkoutService", function ($q, WorkoutPlan, Exercise) {
+ var mock = {};
+ mock.sampleWorkout = new WorkoutPlan({
+ name: "testworkout",
+ title: "Test Workout",
+ description: "This is a test workout",
+ restBetweenExercise: "40",
+ exercises: [{ details: new Exercise({ name: "exercise1", title: "Exercise 1", description: "Exercise 1 description", image: "/image1/path", nameSound: "audio1/path" }), duration: 50 },
+ { details: new Exercise({ name: "exercise2", title: "Exercise 2", description: "Exercise 2 description", image: "/image2/path", nameSound: "audio2/path" }), duration: 30 },
+ { details: new Exercise({ name: "exercise3", title: "Exercise 3", description: "Exercise 3 description", image: "/image3/path", nameSound: "audio3/path" }), duration: 20 }, ]
+ });
+ mock.getWorkout = function (name) {
+ return $q.when(mock.sampleWorkout);
+ }
+ mock.totalWorkoutDuration = 180;
+ return mock;
+ });
+ }));
+
+ describe("WorkoutController", function () {
+ var ctrl, $scope;
+
+ beforeEach(function () {
+ module(function ($provide) {
+ $provide.value("workoutHistoryTracker", { startTracking: function () { }, endTracking: function () { } });
+ });
+ });
+
+ beforeEach(inject(function ($rootScope, $controller, $interval, $location, $timeout, workoutHistoryTracker, WorkoutService, appEvents, Exercise) {
+ $scope = $rootScope.$new();
+ $scope.carousel = { next: function () { } };
+ ctrl = $controller('WorkoutController', {
+ $scope: $scope,
+ $interval: $interval,
+ $location: $location,
+ $timeout: $timeout,
+ workoutHistoryTracker: workoutHistoryTracker,
+ appEvents: appEvents,
+ WorkoutService: WorkoutService,
+ $routeParams: { id: "DummyWorkout" },
+ Exercise: Exercise
+ });
+
+ spyOn(workoutHistoryTracker, 'startTracking');
+ spyOn(workoutHistoryTracker, 'endTracking');
+ spyOn($scope, "$emit");
+ spyOn($scope.carousel, "next");
+
+ $scope.$digest();
+ }));
+
+ it("should load the workoutController", function () {
+ expect(ctrl).toBeDefined();
+ });
+
+ it("should start the workout", inject(function (WorkoutService) {
+ expect($scope.workoutPlan).toEqual(WorkoutService.sampleWorkout);
+ expect($scope.workoutTimeRemaining).toEqual(WorkoutService.totalWorkoutDuration);
+ expect($scope.workoutPaused).toBeFalsy();
+ }));
+
+ it("should start the first exercise", inject(function (WorkoutService, appEvents) {
+ expect($scope.currentExercise).toEqual(WorkoutService.sampleWorkout.exercises[0]);
+ expect($scope.currentExerciseIndex).toEqual(0);
+ expect($scope.$emit).toHaveBeenCalledWith(appEvents.workout.exerciseStarted, WorkoutService.sampleWorkout.exercises[0].details);
+ }));
+
+ it("should setup interleaved images correctly", function () {
+ expect($scope.exerciseImages.length).toEqual(5);
+ expect($scope.exerciseImages[1]).toEqual("img/rest.png");
+ expect($scope.exerciseImages[3]).toEqual("img/rest.png");
+ });
+
+ it("should start history tracking", inject(function (workoutHistoryTracker) {
+ expect(workoutHistoryTracker.startTracking).toHaveBeenCalled();
+ }));
+
+ it("should increase current exercise duration with time", inject(function ($interval) {
+ expect($scope.currentExerciseDuration).toBe(0);
+ $interval.flush(1000);
+ expect($scope.currentExerciseDuration).toBe(1);
+ $interval.flush(1000);
+ expect($scope.currentExerciseDuration).toBe(2);
+ $interval.flush(8000);
+ expect($scope.currentExerciseDuration).toBe(10);
+ }));
+
+ it("should decrease total workout duration with time", inject(function (WorkoutService, $interval) {
+ expect($scope.workoutTimeRemaining).toBe(WorkoutService.totalWorkoutDuration);
+ $interval.flush(1000);
+ expect($scope.workoutTimeRemaining).toBe(WorkoutService.totalWorkoutDuration - 1);
+ $interval.flush(1000);
+ expect($scope.workoutTimeRemaining).toBe(WorkoutService.totalWorkoutDuration - 2);
+ }));
+
+ it("should transition to next exercise on one exercise complete", inject(function (WorkoutService, $interval) {
+ $interval.flush(WorkoutService.sampleWorkout.exercises[0].duration * 1000);
+ expect($scope.currentExercise.details.name).toBe('rest');
+ expect($scope.currentExercise.duration).toBe(WorkoutService.sampleWorkout.restBetweenExercise);
+ }));
+
+ it("should flip between rest and workout", inject(function (WorkoutService, $interval) {
+ // first exercise
+ expect($scope.currentExercise).toBe(WorkoutService.sampleWorkout.exercises[0]);
+ $interval.flush(WorkoutService.sampleWorkout.exercises[0].duration * 1000);
+ //rest exercise
+ expect($scope.currentExercise.details.name).toBe('rest');
+ expect($scope.currentExercise.duration).toBe(WorkoutService.sampleWorkout.restBetweenExercise);
+ $interval.flush(WorkoutService.sampleWorkout.restBetweenExercise * 1000);
+ //second exercise
+ expect($scope.currentExercise).toBe(WorkoutService.sampleWorkout.exercises[1]);
+ $interval.flush(WorkoutService.sampleWorkout.exercises[1].duration * 1000);
+ //rest exercise
+ expect($scope.currentExercise.details.name).toBe('rest');
+ expect($scope.currentExercise.duration).toBe(WorkoutService.sampleWorkout.restBetweenExercise);
+ }));
+
+ it("should reset currentExerciseDuration on exercise flip", inject(function (WorkoutService, $interval) {
+ expect($scope.currentExerciseDuration).toBe(0);
+ $interval.flush(1000);
+ expect($scope.currentExerciseDuration).toBe(1);
+ $interval.flush(1000);
+ expect($scope.currentExerciseDuration).toBe(2);
+ $interval.flush((WorkoutService.sampleWorkout.exercises[0].duration - 2) * 1000);
+ expect($scope.currentExerciseDuration).toBe(0);
+ }));
+
+ it("should move carousel forward on completion of exercise", inject(function (WorkoutService, $interval) {
+ $interval.flush(WorkoutService.sampleWorkout.exercises[0].duration * 1000);
+ expect($scope.carousel.next).toHaveBeenCalled();
+ $interval.flush(WorkoutService.sampleWorkout.restBetweenExercise * 1000);
+ expect($scope.carousel.next.calls.count()).toEqual(2);
+ }));
+
+ it("should end the workout when all exercises are complete", inject(function (WorkoutService, $interval, workoutHistoryTracker, $location) {
+ $interval.flush(WorkoutService.sampleWorkout.exercises[0].duration * 1000);
+ $interval.flush(WorkoutService.sampleWorkout.restBetweenExercise * 1000);
+ $interval.flush(WorkoutService.sampleWorkout.exercises[1].duration * 1000);
+ $interval.flush(WorkoutService.sampleWorkout.restBetweenExercise * 1000);
+ $interval.flush(WorkoutService.sampleWorkout.exercises[2].duration * 1000);
+
+ expect(workoutHistoryTracker.endTracking).toHaveBeenCalled();
+ expect($location.path()).toEqual("/finish");
+ expect($scope.workoutTimeRemaining).toBe(0);
+ expect($scope.currentExercise).toBe(WorkoutService.sampleWorkout.exercises[2]);
+
+ }));
+
+ it("should pause workout on invoking pauseWorkout", function () {
+ expect($scope.workoutPaused).toBeFalsy();
+ $scope.pauseWorkout();
+ expect($scope.workoutPaused).toBe(true);
+ });
+
+ it("should not update workoutTimeRemaining for paused workout on interval lapse", inject(function (WorkoutService, $interval) {
+ expect($scope.workoutPaused).toBeFalsy();
+
+ $interval.flush(1000);
+ expect($scope.workoutTimeRemaining).toBe(WorkoutService.totalWorkoutDuration - 1);
+
+ $scope.pauseWorkout();
+ expect($scope.workoutPaused).toBe(true);
+
+ $interval.flush(5000);
+ expect($scope.workoutTimeRemaining).toBe(WorkoutService.totalWorkoutDuration - 1);
+ }));
+
+ it("should not update currentExerciseDuration for paused workout on interval lapse", inject(function (WorkoutService, $interval) {
+ expect($scope.workoutPaused).toBeFalsy();
+
+ $interval.flush(1000);
+ expect($scope.currentExerciseDuration).toBe(1);
+
+ $scope.pauseWorkout();
+ expect($scope.workoutPaused).toBe(true);
+
+ $interval.flush(5000);
+ expect($scope.currentExerciseDuration).toBe(1);
+ }));
+
+ it("should not throw error if paused multiple times", function () {
+ expect($scope.workoutPaused).toBeFalsy();
+ $scope.pauseWorkout();
+ expect($scope.workoutPaused).toBe(true);
+ $scope.pauseWorkout();
+ expect($scope.workoutPaused).toBe(true);
+ $scope.pauseWorkout();
+ expect($scope.workoutPaused).toBe(true);
+ });
+
+ it("should resume workout on invoking resumeWorkout", inject(function ($interval) {
+ expect($scope.workoutPaused).toBeFalsy();
+ $scope.pauseWorkout();
+ expect($scope.workoutPaused).toBe(true);
+ $scope.resumeWorkout();
+ expect($scope.workoutPaused).toBe(false);
+
+ $interval.flush(1000);
+ expect($scope.currentExerciseDuration).toBe(1);
+ }));
+
+ it("should not throw error on multiple resumeWorkout invocations", inject(function ($interval) {
+ expect($scope.workoutPaused).toBeFalsy();
+ $scope.pauseWorkout();
+ expect($scope.workoutPaused).toBe(true);
+ $scope.resumeWorkout();
+ expect($scope.workoutPaused).toBe(false);
+
+ $interval.flush(1000);
+ expect($scope.currentExerciseDuration).toBe(1);
+
+ $scope.resumeWorkout();
+ expect($scope.workoutPaused).toBe(false);
+
+ $interval.flush(1000);
+ expect($scope.currentExerciseDuration).toBe(2);
+
+ $interval.flush(1000);
+ expect($scope.currentExerciseDuration).toBe(3);
+ }));
+
+ it("should toggle workout state on invoking pauseResumeToggle", function () {
+ expect($scope.workoutPaused).toBeFalsy();
+ $scope.pauseResumeToggle();
+ expect($scope.workoutPaused).toBe(true);
+ $scope.pauseResumeToggle();
+ expect($scope.workoutPaused).toBeFalsy();
+ });
+
+ it("should toggle pause resume on keycodes for 'p' and 'P'", function () {
+ expect($scope.workoutPaused).toBeFalsy();
+ $scope.onKeyPressed({ which: 80 });
+ expect($scope.workoutPaused).toBe(true);
+ $scope.onKeyPressed({ which: 112 });
+ expect($scope.workoutPaused).toBeFalsy();
+ });
+ });
+
+ describe("WorkoutAudioController", function () {
+ function AudioController() {
+ this.pause = function () { }
+ this.play = function () { }
+ this.currentTime = 0;
+ this.duration = 0;
+ };
+
+ var ctrl, $scope;
+
+ beforeEach(inject(function ($rootScope, $controller, $interval, $location, $timeout, workoutHistoryTracker, WorkoutService, appEvents, Exercise, WorkoutService) {
+ $scope = $rootScope.$new();
+
+ // Mocking audio controller
+ $scope.ticksAudio = new AudioController();
+ $scope.nextUpAudio = new AudioController();
+ $scope.nextUpExerciseAudio = new AudioController();
+ $scope.halfWayAudio = new AudioController();
+ $scope.aboutToCompleteAudio = new AudioController();
+
+
+ ctrl = $controller('WorkoutAudioController', {
+ $scope: $scope,
+ $interval: $interval,
+ $location: $location,
+ $timeout: $timeout,
+ });
+
+ $scope.$digest();
+ }));
+
+ it("should load the WorkoutAudioController", function () {
+ expect(ctrl).toBeDefined();
+ });
+
+ it("should load the audio files when workout loaded", inject(function (WorkoutService) {
+ $scope.workoutPlan = WorkoutService.sampleWorkout;
+ $scope.$digest();
+ expect($scope.exercisesAudio.length).toBe(3);
+ }));
+
+ it("should play half way audio when halfway duration reached", inject(function (WorkoutService) {
+ spyOn($scope.halfWayAudio, "play");
+ $scope.currentExercise = WorkoutService.sampleWorkout.exercises[0];
+ $scope.currentExerciseDuration = 2;
+ $scope.$digest();
+
+ expect($scope.halfWayAudio.play).not.toHaveBeenCalled();
+
+ $scope.currentExerciseDuration = WorkoutService.sampleWorkout.exercises[0].duration / 2;
+ $scope.$digest();
+
+ expect($scope.halfWayAudio.play).toHaveBeenCalled();
+ }));
+
+ it("should play about to complete when exercise about to complete", inject(function (WorkoutService) {
+ spyOn($scope.aboutToCompleteAudio, "play");
+
+ $scope.currentExercise = WorkoutService.sampleWorkout.exercises[0];
+
+ $scope.currentExerciseDuration = 2;
+ $scope.$digest();
+ expect($scope.aboutToCompleteAudio.play).not.toHaveBeenCalled();
+
+ $scope.currentExerciseDuration = WorkoutService.sampleWorkout.exercises[0].duration / 2;
+ $scope.$digest();
+ expect($scope.aboutToCompleteAudio.play).not.toHaveBeenCalled();
+
+ $scope.currentExerciseDuration = WorkoutService.sampleWorkout.exercises[0].duration - 3;
+ $scope.$digest();
+ expect($scope.aboutToCompleteAudio.play).toHaveBeenCalled();
+
+ }));
+
+ it("should play next up audio at the end of rest exercise", inject(function (WorkoutService, $timeout) {
+ spyOn($scope.nextUpAudio, "play");
+ spyOn($scope.nextUpExerciseAudio, "play");
+
+ $scope.currentExercise = { details: { name: 'rest' } };
+ $scope.$digest();
+
+ expect($scope.nextUpAudio.play).not.toHaveBeenCalled();
+ expect($scope.nextUpExerciseAudio.play).not.toHaveBeenCalled();
+
+ $timeout.flush(2000);
+ expect($scope.nextUpAudio.play).toHaveBeenCalled();
+ expect($scope.nextUpExerciseAudio.play).not.toHaveBeenCalled();
+
+ $timeout.flush(1000);
+ expect($scope.nextUpExerciseAudio.play).toHaveBeenCalled();
+ }));
+
+ it("should pause all audios when workout paused", function () {
+ spyOn($scope.ticksAudio, "pause");
+ spyOn($scope.nextUpAudio, "pause");
+ spyOn($scope.nextUpExerciseAudio, "pause");
+ spyOn($scope.halfWayAudio, "pause");
+ spyOn($scope.aboutToCompleteAudio, "pause");
+
+ $scope.workoutPaused = true;
+ $scope.$digest();
+
+ expect($scope.ticksAudio.pause).toHaveBeenCalled();
+ expect($scope.nextUpAudio.pause).toHaveBeenCalled();
+ expect($scope.nextUpExerciseAudio.pause).toHaveBeenCalled();
+ expect($scope.halfWayAudio.pause).toHaveBeenCalled();
+ expect($scope.aboutToCompleteAudio.pause).toHaveBeenCalled();
+
+ });
+
+ it("should resume ticking audio when workout resumes", function () {
+ spyOn($scope.ticksAudio, "play");
+
+ $scope.workoutPaused = false;
+ $scope.$digest();
+
+ expect($scope.ticksAudio.play).toHaveBeenCalled();
+ });
+ });
+});
\ No newline at end of file
diff --git a/trainer/app/js/7MinWorkout/workoutvideos.js b/trainer/app/js/7MinWorkout/workoutvideos.js
new file mode 100644
index 0000000..f86d67d
--- /dev/null
+++ b/trainer/app/js/7MinWorkout/workoutvideos.js
@@ -0,0 +1,33 @@
+'use strict';
+
+angular.module('7minWorkout')
+ .controller('WorkoutVideosController', ['$scope', '$modal', function ($scope, $modal) {
+ $scope.playVideo = function (videoId) {
+ $scope.pauseWorkout();
+ var dailog = $modal.open({
+ templateUrl: 'youtube-modal',
+ controller: VideoPlayerController,
+ scope:$scope.$new(true),
+ resolve: {
+ video: function () {
+ return '//www.youtube.com/embed/' + videoId;
+ }
+ },
+ size: 'lg'
+ }).result['finally'](function () {
+ $scope.resumeWorkout();
+ });
+ };
+
+ var VideoPlayerController = function ($scope, $modalInstance, video) {
+ $scope.video = video;
+ $scope.ok = function () {
+ $modalInstance.close();
+ };
+ };
+ VideoPlayerController['$inject'] = ['$scope', '$modalInstance', 'video'];
+
+ var init = function () {
+ };
+ init();
+ }]);
\ No newline at end of file
diff --git a/trainer/app/js/7MinWorkout/workoutvideos.spec.js b/trainer/app/js/7MinWorkout/workoutvideos.spec.js
new file mode 100644
index 0000000..e69de29
diff --git a/trainer/app/js/WorkoutBuilder/directives.js b/trainer/app/js/WorkoutBuilder/directives.js
new file mode 100644
index 0000000..f89613f
--- /dev/null
+++ b/trainer/app/js/WorkoutBuilder/directives.js
@@ -0,0 +1,7 @@
+angular.module('WorkoutBuilder')
+ .directive('workoutTile', function () {
+ return {
+ restrict:'EA',
+ templateUrl:'/partials/workoutbuilder/workout-tile.html'
+ }
+ });
\ No newline at end of file
diff --git a/trainer/app/js/WorkoutBuilder/directives.spec.js b/trainer/app/js/WorkoutBuilder/directives.spec.js
new file mode 100644
index 0000000..e88e622
--- /dev/null
+++ b/trainer/app/js/WorkoutBuilder/directives.spec.js
@@ -0,0 +1,21 @@
+describe("Directives", function () {
+ var $compile, $rootScope, $scope;
+
+ beforeEach(module('app'));
+ beforeEach(module('/partials/workoutbuilder/workout-tile.html'));
+
+ beforeEach(inject(function (_$compile_, _$rootScope_) {
+ $compile = _$compile_;
+ $rootScope = _$rootScope_;
+ $scope = $rootScope.$new();
+ }));
+
+ describe("Workout tile", function () {
+ it("should load workout tile directive", inject(function ($templateCache) {
+ var e = $compile(" 0);
+ }));
+ });
+
+});
\ No newline at end of file
diff --git a/trainer/app/js/WorkoutBuilder/exercise .spec.js b/trainer/app/js/WorkoutBuilder/exercise .spec.js
new file mode 100644
index 0000000..e69de29
diff --git a/trainer/app/js/WorkoutBuilder/exercise.js b/trainer/app/js/WorkoutBuilder/exercise.js
new file mode 100644
index 0000000..47cf35c
--- /dev/null
+++ b/trainer/app/js/WorkoutBuilder/exercise.js
@@ -0,0 +1,78 @@
+'use strict';
+
+angular.module('WorkoutBuilder')
+ .controller('ExercisesNavController', ['$scope', 'WorkoutService', 'WorkoutBuilderService', function ($scope, WorkoutService, WorkoutBuilderService) {
+ $scope.addExercise = function (exercise) {
+ WorkoutBuilderService.addExercise(exercise);
+ }
+ var init = function () {
+ $scope.exercises = WorkoutService.Exercises.query();
+ };
+ init();
+ }]);
+
+angular.module('WorkoutBuilder')
+ .controller('ExerciseListController', ['$scope', 'WorkoutService', '$location', function ($scope, WorkoutService, $location) {
+ $scope.goto = function (exercise) {
+ $location.path('/builder/exercises/' + exercise.name);
+ }
+ var init = function () {
+ $scope.exercises = WorkoutService.Exercises.query();
+ };
+ init();
+ }]);
+
+angular.module('WorkoutBuilder')
+ .controller('ExerciseDetailController', ['$scope', 'WorkoutService', '$routeParams', 'ExerciseBuilderService', '$location', function ($scope, WorkoutService, $routeParams, ExerciseBuilderService, $location) {
+
+ $scope.save = function () {
+ $scope.submitted = true; // Will force validations
+ if ($scope.formExercise.$invalid) return;
+ ExerciseBuilderService.save().then(function (data) {
+ $scope.formExercise.$setPristine();
+ $scope.submitted = false;
+ });
+ };
+
+ $scope.hasError = function (modelController, error) {
+ return (modelController.$dirty || $scope.submitted) && error;
+ };
+
+ $scope.reset = function () {
+ $scope.exercise = ExerciseBuilderService.startBuilding($routeParams.id);
+ $scope.formExercise.$setPristine();
+ $scope.submitted = false; // Will force validations
+ };
+
+ $scope.canDeleteExercise = function () {
+ return ExerciseBuilderService.canDeleteExercise();
+ }
+
+ $scope.deleteExercise = function () {
+ ExerciseBuilderService.delete().then(function (data) {
+ $location.path('/builder/exercises/');
+ });
+ };
+
+ $scope.addVideo = function () {
+ ExerciseBuilderService.addVideo();
+ };
+
+ $scope.deleteVideo = function (index) {
+ ExerciseBuilderService.deleteVideo(index);
+ };
+
+ var init = function () {
+ // We do not use the resolve property on the route to load exercise as we do it with workout.
+ $scope.exercise = ExerciseBuilderService.startBuilding($routeParams.id);
+
+ if ($routeParams.id) { // In case of existing workout loaded from server need to wait to know whether the exercise exists.
+ $scope.exercise.$promise.then(null, function (error) {
+ // If exercise not found we redirect back to exercise list page.
+ $location.path('/builder/exercises/');
+ })
+ }
+ };
+
+ init();
+ }]);
\ No newline at end of file
diff --git a/trainer/app/js/WorkoutBuilder/services.js b/trainer/app/js/WorkoutBuilder/services.js
new file mode 100644
index 0000000..d698482
--- /dev/null
+++ b/trainer/app/js/WorkoutBuilder/services.js
@@ -0,0 +1,112 @@
+///
+'use strict';
+
+/* Services */
+angular.module('app')
+ .value("appEvents", {
+ workout: { exerciseStarted: "event:workout:exerciseStarted" }
+ });
+
+angular.module('WorkoutBuilder')
+ .factory("WorkoutBuilderService", ['WorkoutService', 'WorkoutPlan', 'Exercise', '$q', function (WorkoutService, WorkoutPlan, Exercise, $q) {
+ var service = {};
+ var buildingWorkout;
+ var newWorkout;
+ service.startBuilding = function (name) {
+ //We are going to edit an existing workout
+ if (name) {
+ return WorkoutService.getWorkout(name).then(function (workout) {
+ buildingWorkout = workout;
+ newWorkout = false;
+ return buildingWorkout;
+ });
+ }
+ else {
+ buildingWorkout = new WorkoutPlan({});
+ newWorkout = true;
+ return $q.when(buildingWorkout);
+ }
+ };
+
+ service.removeExercise = function (exercise) {
+ buildingWorkout.exercises.splice(buildingWorkout.exercises.indexOf(exercise), 1);
+ };
+
+ service.addExercise = function (exercise) {
+ buildingWorkout.exercises.push({ details: exercise, duration: 30 });
+ };
+
+ service.save = function () {
+ var promise = newWorkout ? WorkoutService.addWorkout(buildingWorkout)
+ : WorkoutService.updateWorkout(buildingWorkout);
+ promise.then(function (workout) {
+ newWorkout = false;
+ });
+ return promise;
+ };
+
+ service.moveExerciseTo = function (exercise, toIndex) {
+ if (toIndex < 0 || toIndex >= buildingWorkout.exercises) return;
+ var currentIndex = buildingWorkout.exercises.indexOf(exercise);
+ buildingWorkout.exercises.splice(toIndex, 0, buildingWorkout.exercises.splice(currentIndex, 1)[0]);
+ }
+
+ service.canDeleteWorkout = function () {
+ return !newWorkout;
+ }
+
+ service.delete = function () {
+ if (newWorkout) return; // A new workout cannot be deleted.
+ return WorkoutService.deleteWorkout(buildingWorkout.name);
+ }
+
+ return service;
+ }]);
+
+angular.module('WorkoutBuilder')
+ .factory("ExerciseBuilderService", ['WorkoutService', 'Exercise', '$q', function (WorkoutService, Exercise, $q) {
+ var service = {};
+ var buildingExercise;
+ var newExercise;
+ service.startBuilding = function (name) {
+ //We are going to edit an existing exercise
+ if (name) {
+ buildingExercise = WorkoutService.Exercises.get({ id: name }, function (data) {
+ newExercise = false;
+ });
+ }
+ else {
+ buildingExercise = new Exercise({});
+ newExercise = true;
+ }
+ return buildingExercise;
+ };
+
+ service.save = function () {
+ if (!buildingExercise._id) buildingExercise._id = buildingExercise.name;
+ var promise = newExercise ? WorkoutService.Exercises.save({}, buildingExercise).$promise
+ : buildingExercise.$update({ id: buildingExercise.name });
+ return promise.then(function (data) {
+ newExercise = false;
+ return buildingExercise;
+ });
+ };
+
+ service.delete = function () {
+ return buildingExercise.$delete({ id: buildingExercise.name });
+ };
+
+ service.addVideo = function () {
+ buildingExercise.related.videos.push("");
+ };
+
+ service.canDeleteExercise = function () {
+ return !newExercise;
+ }
+
+ service.deleteVideo = function (index) {
+ if (index >= 0) buildingExercise.related.videos.splice(index, 1);
+ }
+
+ return service;
+ }]);
diff --git a/trainer/app/js/WorkoutBuilder/services.spec.js b/trainer/app/js/WorkoutBuilder/services.spec.js
new file mode 100644
index 0000000..e69de29
diff --git a/trainer/app/js/WorkoutBuilder/workout.js b/trainer/app/js/WorkoutBuilder/workout.js
new file mode 100644
index 0000000..187ae53
--- /dev/null
+++ b/trainer/app/js/WorkoutBuilder/workout.js
@@ -0,0 +1,115 @@
+'use strict';
+
+angular.module('WorkoutBuilder')
+ .controller('WorkoutListController', ['$scope', 'WorkoutService', '$location', function ($scope, WorkoutService, $location) {
+ $scope.goto = function (workout) {
+ $location.path('/builder/workouts/' + workout.name);
+ }
+ var init = function () {
+ WorkoutService.getWorkouts().then(function (data) {
+ $scope.workouts = data;
+ });
+ };
+ init();
+ }]);
+
+angular.module('WorkoutBuilder')
+ .controller('WorkoutDetailController', ['$scope', 'WorkoutBuilderService', 'selectedWorkout', '$location', '$routeParams', 'WorkoutService', '$q', function ($scope, WorkoutBuilderService, selectedWorkout, $location, $routeParams, WorkoutService, $q) {
+ $scope.removeExercise = function (exercise) {
+ WorkoutBuilderService.removeExercise(exercise);
+ };
+
+ $scope.save = function () {
+ if ($scope.formWorkout.$invalid) return;
+ $scope.submitted = true; // Will force validations
+ return WorkoutBuilderService.save().then(function (workout) {
+ $scope.workout = workout;
+ $scope.formWorkout.$setPristine();
+ $scope.submitted = false;
+ });
+ }
+
+ $scope.$watch('formWorkout.exerciseCount', function (newValue) {
+ if (newValue) {
+ newValue.$setValidity("count", $scope.workout.exercises.length > 0);
+ }
+ });
+
+ $scope.$watch('workout.exercises.length', function (newValue, oldValue) {
+ if (newValue != oldValue) {
+ $scope.formWorkout.exerciseCount.$dirty = true;
+ $scope.formWorkout.$setDirty();
+ $scope.formWorkout.exerciseCount.$setValidity("count", newValue > 0);
+ }
+ });
+
+ //var restWatch = $scope.$watch('formWorkout.restBetweenExercise', function (newValue) {
+ // // Conversion logic courtesy http://stackoverflow.com/questions/596467/how-do-i-convert-a-number-to-an-integer-in-javascript
+ // if (newValue) {
+ // newValue.$parsers.unshift(function (value) {
+ // return isNaN(parseInt(value)) ? value : parseInt(value);
+ // });
+ // newValue.$formatters.push(function (value) {
+ // return isNaN(parseInt(value)) ? value : parseInt(value);
+ // });
+ // restWatch(); //De-register the watch.
+ // }
+ //});
+ $scope.hasError = function (modelController, error) {
+ return (modelController.$dirty || $scope.submitted) && error;
+ }
+
+ $scope.reset = function () {
+ $scope.workout = WorkoutBuilderService.startBuilding($routeParams.id);
+ $scope.formWorkout.$setPristine();
+ $scope.submitted = false; // Will force validations
+ };
+
+ $scope.moveExerciseTo = function (exercise, location) {
+ WorkoutBuilderService.moveExerciseTo(exercise, location);
+ };
+
+ $scope.durations = [{ title: "15 seconds", value: 15 },
+ { title: "30 seconds", value: 30 },
+ { title: "45 seconds", value: 45 },
+ { title: "1 minute", value: 60 },
+ { title: "1 minute 15 seconds", value: 75 },
+ { title: "1 minute 30 seconds", value: 90 },
+ { title: "1 minute 45 seconds", value: 105 },
+ { title: "2 minutes", value: 120 },
+ { title: "2 minutes 15 seconds", value: 135 },
+ { title: "2 minutes 30 seconds", value: 150 },
+ { title: "2 minutes 45 seconds", value: 165 },
+ { title: "3 minutes", value: 180 },
+ { title: "3 minutes 15 seconds", value: 195 },
+ { title: "3 minutes 30 seconds", value: 210 },
+ { title: "3 minutes 45 seconds", value: 225 },
+ { title: "4 minutes", value: 240 },
+ { title: "4 minutes 15 seconds", value: 255 },
+ { title: "4 minutes 30 seconds", value: 270 },
+ { title: "4 minutes 45 seconds", value: 285 },
+ { title: "5 minutes", value: 300 }];
+
+ $scope.canDeleteWorkout = function () {
+ return WorkoutBuilderService.canDeleteWorkout();
+ }
+
+ $scope.uniqueUserName = function (value) {
+ // Empty workout name or existing workout name does not require validation.
+ if (!value || value === $routeParams.id) return $q.when(true);
+ return WorkoutService
+ .getWorkout(value.toLowerCase())
+ .then(function (data) { return $q.reject(); },
+ function (error) { return true; });
+ };
+
+ $scope.deleteWorkout = function () {
+ WorkoutBuilderService.delete().then(function (data) {
+ $location.path('/builder/workouts/');
+ });
+ };
+ var init = function () {
+ $scope.workout = selectedWorkout;
+ };
+ init();
+ }]);
\ No newline at end of file
diff --git a/trainer/app/js/WorkoutBuilder/workout.spec.js b/trainer/app/js/WorkoutBuilder/workout.spec.js
new file mode 100644
index 0000000..db5cf32
--- /dev/null
+++ b/trainer/app/js/WorkoutBuilder/workout.spec.js
@@ -0,0 +1,63 @@
+describe("Workout Builder", function () {
+ beforeEach(module('app'));
+ beforeEach(module('WorkoutBuilder'));
+
+ beforeEach(function () {
+ module(function ($provide) {
+ $provide.factory("WorkoutBuilderService", function ($q, WorkoutPlan, Exercise) {
+ var mock = {};
+ mock.startBuilding = function (name) { };
+
+ mock.removeExercise = function (exercise) { };
+
+ mock.addExercise = function (exercise) { };
+
+ mock.save = function () { };
+
+ mock.moveExerciseTo = function (exercise, toIndex) { }
+
+ mock.canDeleteWorkout = function () { }
+
+ mock.delete = function () { }
+
+ return mock;
+ });
+ });
+ });
+
+ beforeEach(function () {
+ module(function ($provide) {
+ $provide.factory("WorkoutService", function ($q, WorkoutPlan, Exercise) {
+ var mock = {};
+ mock.getWorkout = function (name) { return name == "thisOnlyExists" ? $q.when({}) : $q.error("Not Found") };
+ return mock;
+ });
+ });
+ });
+
+ describe("WorkoutDetailController", function () {
+ var ctrl, $scope;
+
+ beforeEach(inject(function ($rootScope, $controller, WorkoutBuilderService, $location, $routeParams, WorkoutService, WorkoutPlan, $q) {
+ $scope = $rootScope.$new();
+ ctrl = $controller("WorkoutDetailController", {
+ $scope: $scope,
+ WorkoutBuilderService: WorkoutBuilderService,
+ selectedWorkout: new WorkoutPlan({}),
+ $location: $location,
+ $routeParams: $routeParams,
+ WorkoutService: WorkoutService,
+ $q: $q
+ });
+ }));
+
+ it("should load the WorkoutDetailController", function () {
+ expect(ctrl).toBeDefined();
+ });
+
+ it("should setup selected workout", function () {
+ expect($scope.workout).toBeDefined();
+ });
+
+ });
+});
\ No newline at end of file
diff --git a/trainer/app/js/app.js b/trainer/app/js/app.js
new file mode 100644
index 0000000..726d438
--- /dev/null
+++ b/trainer/app/js/app.js
@@ -0,0 +1,6 @@
+'use strict';
+
+angular.module('app', ['ngRoute', 'ngSanitize', '7minWorkout', 'WorkoutBuilder', 'ui.bootstrap', 'LocalStorageModule', 'ngAnimate','ngMessages', 'ngResource', 'oc.lazyLoad', 'pascalprecht.translate']);
+
+angular.module('7minWorkout', []);
+angular.module('WorkoutBuilder', []);
\ No newline at end of file
diff --git a/trainer/app/js/appe2e.js b/trainer/app/js/appe2e.js
new file mode 100644
index 0000000..5a2562f
--- /dev/null
+++ b/trainer/app/js/appe2e.js
@@ -0,0 +1,29 @@
+angular.module('appe2e', ['app', 'ngMockE2E'])
+ .run(function ($httpBackend) {
+ var dbName = "angularjsbyexample";
+ var dbEndpoint = "https://api.mongolab.com/api/1/databases/" + dbName + "/";
+ var workoutsEndpoint = dbEndpoint + "collections/workouts";
+ //var workoutEndpointRegex = new RegExp("https://api\.mongolab\.com/api/1/databases/"+angularjsbyexample/collections/workouts/.+");
+ var exercisesEndpoint = dbEndpoint + "collections/exercises";
+ var apiKey = "E16WgslFduXHiMAdAg6qcG1KKYx7WNWg";
+
+ $httpBackend.whenGET(/^partials\//).passThrough();
+
+ // All workouts
+ $httpBackend.whenGET(workoutsEndpoint + "?apiKey=" + apiKey).respond([{ "_id": "7minworkout", "exercises": [{ "name": "jumpingJacks", "duration": 30 }, { "name": "wallSit", "duration": 30 }, { "name": "pushUp", "duration": 30 }, { "name": "crunches", "duration": 30 }, { "name": "stepUpOntoChair", "duration": 30 }, { "name": "squat", "duration": 30 }, { "name": "tricepdips", "duration": 30 }, { "name": "plank", "duration": 30 }, { "name": "highKnees", "duration": 30 }, { "name": "lunges", "duration": 30 }, { "name": "pushupNRotate", "duration": 30 }, { "name": "sidePlank", "duration": 30 }], "name": "7minworkout", "title": "7 Minute Workout", "description": "A high intensity workout that consists of 12 exercises.", "restBetweenExercise": 10 },
+ { "_id": "testworkout", "exercises": [{ "name": "crunches", "duration": 5 }, { "name": "pushUp", "duration": 5 }], "name": "testworkout", "title": "A test Workout", "description": "This is a test workout for E2E testing.", "restBetweenExercise": 5 }]);
+
+ // All exercises
+ $httpBackend.whenGET(exercisesEndpoint + "?apiKey=" + apiKey).respond([{ "_id": "jumpingJacks", "name": "jumpingJacks", "title": "Jumping Jacks", "description": "A jumping jack or star jump, also called side-straddle hop is a physical jumping exercise.", "image": "img/JumpingJacks.png", "nameSound": "content/jumpingjacks.wav", "related": { "videos": ["dmYwZH_BNd0", "BABOdJ-2Z6o", "c4DAnQ6DtF8"] }, "procedure": "Assume an erect position, with feet together and arms at your side. Slightly bend your knees, and propel yourself a few inches into the air. While in air, bring your legs out to the side about shoulder width or slightly wider. As you are moving your legs outward, you should raise your arms up over your head; arms should be slightly bent throughout the entire in-air movement. Your feet should land shoulder width or wider as your hands meet above your head with arms slightly bent" }, { "_id": "wallSit", "name": "wallSit", "title": "Wall Sit", "description": "A wall sit, also known as a Roman Chair, is an exercise done to strengthen the quadriceps muscles.", "image": "img/wallsit.png", "nameSound": "content/wallsit.wav", "related": { "videos": ["y-wV4Venusw", "MMV3v4ap4ro"] }, "procedure": "Place your back against a wall with your feet shoulder width apart and a little ways out from the wall. Then, keeping your back against the wall, lower your hips until your knees form right angles. " }, { "_id": "crunches", "name": "crunches", "title": "Abdominal Crunches", "description": "The basic crunch is a abdominal exercise in a strength-training program.", "image": "img/crunches.png", "nameSound": "content/crunches.wav", "related": { "videos": ["Xyd_fa5zoEU", "MKmrqcoCZ-M"] }, "procedure": "Lie on your back with your knees bent and feet flat on the floor, hip-width apart. Place your hands behind your head so your thumbs are behind your ears. Hold your elbows out to the sides but rounded slightly in. Gently pull your abdominals inward. Curl up and forward so that your head, neck, and shoulder blades lift off the floor. Hold for a moment at the top of the movement and then lower slowly back down." }, { "_id": "stepUpOntoChair", "name": "stepUpOntoChair", "title": "Step Up Onto Chair", "description": "Step exercises are ideal for building muscle in your lower body.", "image": "img/stepUpOntoChair.jpeg", "nameSound": "content/stepup.wav", "related": { "videos": ["aajhW7DD1EA"] }, "procedure": "Position your chair in front of you.Stand with your feet about hip width apart, arms at your sides. Step up onto the seat with one foot, pressing down while bringing your other foot up next to it. Step back with the leading foot and bring the trailing foot down to finish one step-up." }, { "_id": "tricepdips", "name": "tricepdips", "title": "Tricep Dips On Chair", "description": "A body weight exercise that targets triceps.", "image": "img/tricepdips.jpg", "nameSound": "content/tricepdips.wav", "related": { "videos": ["tKjcgfu44sI", "jox1rb5krQI"] }, "procedure": "Sit up on a chair. Your legs should be slightly extended, with your feet flat on the floor.Place your hands edges of the chair. Your palms should be down, fingertips pointing towards the floor.\\\n Without moving your legs, bring your glutes forward off the chair.Steadily lower yourself. When your elbows form 90 degrees angles, push yourself back up to starting position." }, { "_id": "plank", "name": "plank", "title": "Plank", "description": "The plank (also called a front hold, hover, or abdominal bridge) is an isometric core strength exercise that involves maintaining a difficult position for extended periods of time. ", "image": "img/plank.png", "nameSound": "content/plank.wav", "related": { "videos": ["pSHjTRCQxIw", "TvxNkmjdhMM"] }, "procedure": "Get into pushup position on the floor. Bend your elbows 90 degrees and rest your weight on your forearms. Your elbows should be directly beneath your shoulders, and your body should form a straight line from head to feet. Hold this position." }, { "_id": "highKnees", "name": "highKnees", "title": "High Knees", "description": "A form exercise that develops strength and endurance of the hip flexors and quads and stretches the hip extensors.", "image": "img/highknees.png", "nameSound": "content/highknees.wav", "related": { "videos": ["OAJ_J3EZkdY", "8opcQdC-V-U"] }, "procedure": "Start standing with feet hip-width apart. Do inplace jog with your knees lifting as much as possible towards your chest." }, { "_id": "lunges", "name": "lunges", "title": "Lunges", "description": "Lunges are a good exercise for strengthening, sculpting and building several muscles/muscle groups, including the quadriceps (or thighs), the gluteus maximus (or buttocks) as well as the hamstrings. ", "image": "img/lunges.png", "nameSound": "content/lunge.wav", "related": { "videos": ["Z2n58m2i4jg"] }, "procedure": "Stand erect with your feet about one shoulder width apart.Put your hands on your hips, keep your back as straight as possible, relax your shoulders and keep your eyes facing directly ahead. Take a large step forward with one leg. As you step forward, lower your hips and bend your knees until they both form 90 degree angles. Return to starting position. Repeat with your alternate leg." }, { "_id": "pushupNRotate", "name": "pushupNRotate", "title": "Pushup And Rotate", "description": "A variation of pushup that requires you to rotate.", "image": "img/pushupNRotate.jpg", "nameSound": "content/pushupandrotate.wav", "related": { "videos": ["qHQ_E-f5278"] }, "procedure": "Assume the classic pushup position, but as you come up, rotate your body so your right arm lifts up and extends overhead.Return to the starting position, lower yourself, then push up and rotate till your left hand points toward the ceiling." }, { "_id": "sidePlank", "name": "sidePlank", "title": "Side Plank", "description": "A variation to Plank done using one hand only", "image": "img/sideplank.png", "nameSound": "content/sideplank.wav", "related": { "videos": ["wqzrb67Dwf8", "_rdfjFSFKMY"] }, "procedure": "Lie on your side, in a straight line from head to feet, resting on your forearm.Your elbow should be directly under your shoulder.With your abdominals gently contracted, lift your hips off the floor, maintaining the line. Keep your hips square and your neck in line with your spine. Hold the position." }, { "_id": "pushUp", "name": "pushUp", "title": "Push Up", "description": "A push-up is a common exercise performed in a prone position by raising and lowering the body using the arms", "image": "img/pushup.png", "nameSound": "content/pushups.wav", "related": { "videos": ["Eh00_rniF8E", "ZWdBqFLNljc", "UwRLWMcOdwI", "ynPwl6qyUNM", "OicNTT2xzMI"] }, "procedure": "Lie prone on the ground with hands placed as wide or slightly wider than shoulder width. Keeping the body straight, lower body to the ground by bending arms at the elbows. Raise body up off the ground by extending the arms." }, { "_id": "squat", "name": "squat", "title": "Squat", "description": "The squat is a compound, full body exercise that trains primarily the muscles of the thighs, hips, buttocks and quads.", "image": "img/squat.png", "nameSound": "content/squats.wav", "related": { "videos": ["QKKZ9AGYTi4", "UXJrBgI2RxA"] }, "procedure": "Stand with your head facing forward and your chest held up and out.Place your feet shoulder-width apart or little wider. Extend your hands straight out in front of you. Sit back and down like you're sitting into a chair. Keep your head facing straight as your upper body bends forward a bit. Rather than allowing your back to round, let your lower back arch slightly as you go down. Lower down so your thighs are parallel to the floor, with your knees over your ankles. Press your weight back into your heels. Keep your body tight, and push through your heels to bring yourself back to the starting position." }]);
+
+ // 7 min workout
+ $httpBackend.whenGET(workoutsEndpoint + "/7minworkout?apiKey=" + apiKey).respond({ "_id": "7minworkout", "exercises": [{ "name": "jumpingJacks", "duration": 30 }, { "name": "wallSit", "duration": 30 }, { "name": "pushUp", "duration": 30 }, { "name": "crunches", "duration": 30 }, { "name": "stepUpOntoChair", "duration": 30 }, { "name": "squat", "duration": 30 }, { "name": "tricepdips", "duration": 30 }, { "name": "plank", "duration": 30 }, { "name": "highKnees", "duration": 30 }, { "name": "lunges", "duration": 30 }, { "name": "pushupNRotate", "duration": 30 }, { "name": "sidePlank", "duration": 30 }], "name": "7minworkout", "title": "7 Minute Workout", "description": "A high intensity workout that consists of 12 exercises.", "restBetweenExercise": 10 });
+ // test workout
+ $httpBackend.whenGET(workoutsEndpoint + "/testworkout?apiKey=" + apiKey).respond({ "_id": "testworkout", "exercises": [{ "name": "crunches", "duration": 5 }, { "name": "pushUp", "duration": 5 }], "name": "testworkout", "title": "A test Workout", "description": "This is a test workout for E2E testing.", "restBetweenExercise": 10 });
+
+ // adds a new phone to the phones array
+ $httpBackend.whenPOST('/phones').respond(function (method, url, data) {
+ phones.push(angular.fromJson(data));
+ });
+
+ });
\ No newline at end of file
diff --git a/trainer/app/js/config.js b/trainer/app/js/config.js
new file mode 100644
index 0000000..30ada7e
--- /dev/null
+++ b/trainer/app/js/config.js
@@ -0,0 +1,147 @@
+angular.module('app').
+config(function ($routeProvider, $sceDelegateProvider, WorkoutServiceProvider, $httpProvider, ApiKeyAppenderInterceptorProvider, $translateProvider, $translatePartialLoaderProvider) {
+
+ // IMPORTANT: Set the database name and API Key here before running the application
+ ApiKeyAppenderInterceptorProvider.setApiKey("E16WgslFduXHiMAdAg6qcG1KKYx7WNWg");
+
+ $httpProvider.interceptors.push('ApiKeyAppenderInterceptor');
+
+ WorkoutServiceProvider.configure("angularjsbyexample");
+
+ $routeProvider.when('/start', {
+ templateUrl: 'partials/workout/start.html',
+ resolve: {
+ depends: ['$ocLazyLoad', function ($ocLazyLoad) {
+ // lazy load files for an existing module
+ return $ocLazyLoad.load([{
+ name: 'WorkoutBuilder',
+ files: ['/js/workoutbuilder/workout.js']
+ }]);
+ }],
+ }
+ });
+
+ $routeProvider.when('/workout/:id', {
+ templateUrl: 'partials/workout/workout.html',
+ controller: 'WorkoutController',
+ resolve: {
+ depends: ['$ocLazyLoad', function ($ocLazyLoad) {
+ // lazy load files for an existing module
+ return $ocLazyLoad.load([{
+ name: 'WorkoutBuilder',
+ files: ['/js/workoutbuilder/workout.js']
+ },
+ {
+ name: '7minWorkout',
+ files: ['/js/7minworkout/directives.js', '/js/7minworkout/services.js', '/js/7minworkout/workout.js', '/js/7minworkout/workoutvideos.js']
+ }, {
+ files: ['//cdnjs.cloudflare.com/ajax/libs/owl-carousel/1.3.2/owl.carousel.js']
+ },
+ {
+ name: 'mediaPlayer',
+ files: ['/js/vendor/angular-media-player.js']
+ }]);
+ }],
+ }
+ });
+
+ $routeProvider.when('/finish', { templateUrl: 'partials/workout/finish.html' });
+
+ $routeProvider.when('/builder', {
+ redirectTo: '/builder/workouts'
+ });
+
+ var workoutBuilderModuleLoader = ['$ocLazyLoad', function ($ocLazyLoad) {
+ // lazy load files for an existing module
+ return $ocLazyLoad.load([{
+ name: 'WorkoutBuilder',
+ files: ['/js/workoutbuilder/directives.js', '/js/workoutbuilder/exercise.js', '/js/workoutbuilder/services.js', '/js/workoutbuilder/workout.js']
+ },
+ ]);
+ }];
+
+ $routeProvider.when('/builder/workouts', {
+ templateUrl: 'partials/workoutbuilder/workouts.html',
+ leftNav: 'partials/workoutbuilder/left-nav-main.html',
+ topNav: 'partials/workoutbuilder/top-nav.html',
+ controller: 'WorkoutListController',
+ resolve: {
+ depends: workoutBuilderModuleLoader,
+ }
+ });
+ $routeProvider.when('/builder/exercises', {
+ templateUrl: 'partials/workoutbuilder/exercises.html',
+ leftNav: 'partials/workoutbuilder/left-nav-main.html',
+ topNav: 'partials/workoutbuilder/top-nav.html',
+ controller: 'ExerciseListController',
+ resolve: {
+ depends: workoutBuilderModuleLoader,
+ }
+ });
+ $routeProvider.when('/builder/workouts/new', {
+ templateUrl: 'partials/workoutbuilder/workout.html',
+ leftNav: 'partials/workoutbuilder/left-nav-exercises.html',
+ topNav: 'partials/workoutbuilder/top-nav.html',
+ controller: 'WorkoutDetailController',
+ resolve: {
+ selectedWorkout: ['$ocLazyLoad', '$injector', '$route', function ($ocLazyLoad, $injector, $route) {
+ return $ocLazyLoad.load([{
+ name: 'WorkoutBuilder',
+ files: ['/js/workoutbuilder/directives.js', '/js/workoutbuilder/exercise.js', '/js/workoutbuilder/services.js', '/js/workoutbuilder/workout.js']
+ }]).then(function () {
+ return $injector.get("WorkoutBuilderService").startBuilding();
+ });
+ }]
+ }
+ });
+ $routeProvider.when('/builder/workouts/:id', {
+ templateUrl: 'partials/workoutbuilder/workout.html',
+ leftNav: 'partials/workoutbuilder/left-nav-exercises.html',
+ controller: 'WorkoutDetailController',
+ topNav: 'partials/workoutbuilder/top-nav.html',
+ routeErrorMessage: "Could not load the specific workout!",
+ resolve: {
+ selectedWorkout: ['$ocLazyLoad', '$injector', '$route', function ($ocLazyLoad, $injector, $route) {
+ return $ocLazyLoad.load([{
+ name: 'WorkoutBuilder',
+ files: ['/js/workoutbuilder/directives.js', '/js/workoutbuilder/exercise.js', '/js/workoutbuilder/services.js', '/js/workoutbuilder/workout.js']
+ }]).then(function () {
+ return $injector.get("WorkoutBuilderService").startBuilding($route.current.params.id);
+ });
+ }]
+ }
+ });
+ $routeProvider.when('/builder/exercises/new', {
+ templateUrl: 'partials/workoutbuilder/exercise.html',
+ controller: 'ExerciseDetailController',
+ topNav: 'partials/workoutbuilder/top-nav.html',
+ resolve: {
+ depends: workoutBuilderModuleLoader,
+ }
+ });
+ $routeProvider.when('/builder/exercises/:id', {
+ templateUrl: 'partials/workoutbuilder/exercise.html',
+ controller: 'ExerciseDetailController',
+ resolve: {
+ depends: workoutBuilderModuleLoader,
+ },
+ topNav: 'partials/workoutbuilder/top-nav.html'
+ });
+
+
+ $routeProvider.otherwise({ redirectTo: '/start' });
+
+ $sceDelegateProvider.resourceUrlWhitelist([
+ // Allow same origin resource loads.
+ 'self',
+ // Allow loading from our assets domain. Notice the difference between * and **.
+ 'http://*.youtube.com/**']);
+
+ $translatePartialLoaderProvider.addPart('workoutrunner');
+ $translatePartialLoaderProvider.addPart('workoutbuilder');
+ $translateProvider.useLoader('$translatePartialLoader', {
+ urlTemplate: '/i18n/{lang}/{part}.json'
+ });
+
+ $translateProvider.preferredLanguage('en');
+});
diff --git a/trainer/app/js/config.spec.js b/trainer/app/js/config.spec.js
new file mode 100644
index 0000000..13882cc
--- /dev/null
+++ b/trainer/app/js/config.spec.js
@@ -0,0 +1,32 @@
+describe("Trainer routes", function () {
+ beforeEach(module('app'));
+
+ it("should default to start workout route", inject(function ($rootScope, $location, $route, $httpBackend) { //Unless we inject $route route transitions do not happen, even if we change location\
+ $httpBackend.whenGET("partials/workout/start.html").respond("");
+ $location.path("/");
+ $rootScope.$digest();
+ expect($location.path()).toBe("/start");
+ expect($route.current.templateUrl).toBe("partials/workout/start.html");
+ expect($route.current.controller).toBeUndefined();
+
+ }));
+
+ it("should load the workout.", inject(function ($rootScope, $location, $route, $httpBackend) {
+ $httpBackend.whenGET("partials/workout/workout.html").respond("");
+ $location.path("/workout/dummyWorkout");
+ $rootScope.$digest();
+ expect($location.path()).toBe("/workout/dummyWorkout");
+ expect($route.current.params.id).toBe('dummyWorkout');
+ }));
+
+ it("should start workout building when navigating to workout builder route.", inject(function ($rootScope, $location, $route, $httpBackend,WorkoutBuilderService) {
+ spyOn(WorkoutBuilderService, "startBuilding");
+ $httpBackend.whenGET("partials/workoutbuilder/workout.html").respond("");
+ $location.path("/builder/workouts/new");
+ $rootScope.$digest();
+ expect($location.path()).toBe("/builder/workouts/new");
+ expect(WorkoutBuilderService.startBuilding).toHaveBeenCalled();
+ expect(WorkoutBuilderService.startBuilding.calls.count()).toBe(1);
+ }));
+
+});
\ No newline at end of file
diff --git a/trainer/app/js/root.js b/trainer/app/js/root.js
new file mode 100644
index 0000000..f4f4491
--- /dev/null
+++ b/trainer/app/js/root.js
@@ -0,0 +1,47 @@
+'use strict';
+
+angular.module('app')
+ .controller('RootController', ['$scope', '$modal', '$translate', function ($scope, $modal, $translate) {
+ $scope.showWorkoutHistory = function () {
+ var dailog = $modal.open({
+ templateUrl: 'partials/workout/workout-history.html',
+ controller: WorkoutHistoryController,
+ size: 'lg'
+ });
+ };
+
+ var WorkoutHistoryController = function ($scope, $modalInstance, workoutHistoryTracker) {
+ $scope.search = {};
+ $scope.search.completed = '';
+ $scope.history = workoutHistoryTracker.getHistory();
+
+ $scope.ok = function () {
+ $modalInstance.close();
+ };
+ };
+ WorkoutHistoryController['$inject'] = ['$scope', '$modalInstance', 'workoutHistoryTracker'];
+
+ $scope.$on('$routeChangeSuccess', function (event, current, previous) {
+ $scope.currentRoute = current;
+ $scope.routeHasError = false;
+ });
+
+ $scope.$on('$routeChangeError', function (event, current, previous, error) {
+ if (error.status === 404 && current.originalPath === "/builder/workouts/:id") {
+ $scope.routeHasError = true;
+ $scope.routeError = current.routeErrorMessage;
+ }
+
+ });
+
+ $scope.setLanguage = function (languageKey) {
+ $translate.use(languageKey);
+ $scope.language = languageKey;
+
+ };
+
+ var init = function () {
+ $scope.language = $translate.preferredLanguage();
+ };
+ init();
+ }]);
diff --git a/trainer/app/js/seed.js b/trainer/app/js/seed.js
new file mode 100644
index 0000000..45e2a48
--- /dev/null
+++ b/trainer/app/js/seed.js
@@ -0,0 +1,21 @@
+/* Workout list
+ To import workout list use a tool that can make POST request. The below instruction are using POSTMAN addin for chrome browser. Other tools like CURL, that can make http requests can also be used instead of POSTMAN.
+ 1. Open POSTMAN and paste the url https://api.mongolab.com/api/1/databases//collections/workouts?apiKey=
+ 2. Update url with your database name () and api key ().
+ 3. Change option from action dropdown to POST.
+ 4. Change data format tab to "raw"
+ 5. Click on the "Headers" button or the top right next to "URL Params" button.
+ 6. Clicking on "Headers" shows up a key-value data entry section. Add key "Content-Type" and value "application/json".
+ 7. Copy and paste the below json array in the text area.
+ 8. Click the button "Send".
+ 9. Check for sucess response
+*/
+[{ "_id": "7minworkout", "exercises": [{ "name": "jumpingJacks", "duration": 30 }, { "name": "wallSit", "duration": 30 }, { "name": "pushUp", "duration": 30 }, { "name": "crunches", "duration": 30 }, { "name": "stepUpOntoChair", "duration": 30 }, { "name": "squat", "duration": 30 }, { "name": "tricepdips", "duration": 30 }, { "name": "plank", "duration": 30 }, { "name": "highKnees", "duration": 30 }, { "name": "lunges", "duration": 30 }, { "name": "pushupNRotate", "duration": 30 }, { "name": "sidePlank", "duration": 30 }], "name": "7minworkout", "title": "7 Minute Workout", "description": "A high intensity workout that consists of 12 exercises.", "restBetweenExercise": 10 }]
+
+/* Workout list
+ To import workout list use a tool that can make POST request. The below instruction are using POSTMAN addin for chrome browser. Other tools like CURL, that can make http requests can also be used instead of POSTMAN.
+ 1. Open POSTMAN and paste the url https://api.mongolab.com/api/1/databases//collections/exercises?apiKey=
+ 2. Update url with your database name () and api key ().
+ 3. Follow step 3 and 9 from above.
+*/
+[{ "_id": "jumpingJacks", "name": "jumpingJacks", "title": "Jumping Jacks", "description": "A jumping jack or star jump, also called side-straddle hop is a physical jumping exercise.", "image": "img/JumpingJacks.png", "nameSound": "content/jumpingjacks.wav", "related": { "videos": ["dmYwZH_BNd0", "BABOdJ-2Z6o", "c4DAnQ6DtF8"] }, "procedure": "Assume an erect position, with feet together and arms at your side. Slightly bend your knees, and propel yourself a few inches into the air. While in air, bring your legs out to the side about shoulder width or slightly wider. As you are moving your legs outward, you should raise your arms up over your head; arms should be slightly bent throughout the entire in-air movement. Your feet should land shoulder width or wider as your hands meet above your head with arms slightly bent" }, { "_id": "wallSit", "name": "wallSit", "title": "Wall Sit", "description": "A wall sit, also known as a Roman Chair, is an exercise done to strengthen the quadriceps muscles.", "image": "img/wallsit.png", "nameSound": "content/wallsit.wav", "related": { "videos": ["y-wV4Venusw", "MMV3v4ap4ro"] }, "procedure": "Place your back against a wall with your feet shoulder width apart and a little ways out from the wall. Then, keeping your back against the wall, lower your hips until your knees form right angles. " }, { "_id": "crunches", "name": "crunches", "title": "Abdominal Crunches", "description": "The basic crunch is a abdominal exercise in a strength-training program.", "image": "img/crunches.png", "nameSound": "content/crunches.wav", "related": { "videos": ["Xyd_fa5zoEU", "MKmrqcoCZ-M"] }, "procedure": "Lie on your back with your knees bent and feet flat on the floor, hip-width apart. Place your hands behind your head so your thumbs are behind your ears. Hold your elbows out to the sides but rounded slightly in. Gently pull your abdominals inward. Curl up and forward so that your head, neck, and shoulder blades lift off the floor. Hold for a moment at the top of the movement and then lower slowly back down." }, { "_id": "stepUpOntoChair", "name": "stepUpOntoChair", "title": "Step Up Onto Chair", "description": "Step exercises are ideal for building muscle in your lower body.", "image": "img/stepUpOntoChair.png", "nameSound": "content/stepup.wav", "related": { "videos": ["aajhW7DD1EA"] }, "procedure": "Position your chair in front of you.Stand with your feet about hip width apart, arms at your sides. Step up onto the seat with one foot, pressing down while bringing your other foot up next to it. Step back with the leading foot and bring the trailing foot down to finish one step-up." }, { "_id": "tricepdips", "name": "tricepdips", "title": "Tricep Dips On Chair", "description": "A body weight exercise that targets triceps.", "image": "img/tricepdips.png", "nameSound": "content/tricepdips.wav", "related": { "videos": ["tKjcgfu44sI", "jox1rb5krQI"] }, "procedure": "Sit up on a chair. Your legs should be slightly extended, with your feet flat on the floor.Place your hands edges of the chair. Your palms should be down, fingertips pointing towards the floor.\\\n Without moving your legs, bring your glutes forward off the chair.Steadily lower yourself. When your elbows form 90 degrees angles, push yourself back up to starting position." }, { "_id": "plank", "name": "plank", "title": "Plank", "description": "The plank (also called a front hold, hover, or abdominal bridge) is an isometric core strength exercise that involves maintaining a difficult position for extended periods of time. ", "image": "img/plank.png", "nameSound": "content/plank.wav", "related": { "videos": ["pSHjTRCQxIw", "TvxNkmjdhMM"] }, "procedure": "Get into pushup position on the floor. Bend your elbows 90 degrees and rest your weight on your forearms. Your elbows should be directly beneath your shoulders, and your body should form a straight line from head to feet. Hold this position." }, { "_id": "highKnees", "name": "highKnees", "title": "High Knees", "description": "A form exercise that develops strength and endurance of the hip flexors and quads and stretches the hip extensors.", "image": "img/highknees.png", "nameSound": "content/highknees.wav", "related": { "videos": ["OAJ_J3EZkdY", "8opcQdC-V-U"] }, "procedure": "Start standing with feet hip-width apart. Do inplace jog with your knees lifting as much as possible towards your chest." }, { "_id": "lunges", "name": "lunges", "title": "Lunges", "description": "Lunges are a good exercise for strengthening, sculpting and building several muscles/muscle groups, including the quadriceps (or thighs), the gluteus maximus (or buttocks) as well as the hamstrings. ", "image": "img/lunges.png", "nameSound": "content/lunge.wav", "related": { "videos": ["Z2n58m2i4jg"] }, "procedure": "Stand erect with your feet about one shoulder width apart.Put your hands on your hips, keep your back as straight as possible, relax your shoulders and keep your eyes facing directly ahead. Take a large step forward with one leg. As you step forward, lower your hips and bend your knees until they both form 90 degree angles. Return to starting position. Repeat with your alternate leg." }, { "_id": "pushupNRotate", "name": "pushupNRotate", "title": "Pushup And Rotate", "description": "A variation of pushup that requires you to rotate.", "image": "img/pushupNRotate.png", "nameSound": "content/pushupandrotate.wav", "related": { "videos": ["qHQ_E-f5278"] }, "procedure": "Assume the classic pushup position, but as you come up, rotate your body so your right arm lifts up and extends overhead.Return to the starting position, lower yourself, then push up and rotate till your left hand points toward the ceiling." }, { "_id": "sidePlank", "name": "sidePlank", "title": "Side Plank", "description": "A variation to Plank done using one hand only", "image": "img/sideplank.png", "nameSound": "content/sideplank.wav", "related": { "videos": ["wqzrb67Dwf8", "_rdfjFSFKMY"] }, "procedure": "Lie on your side, in a straight line from head to feet, resting on your forearm.Your elbow should be directly under your shoulder.With your abdominals gently contracted, lift your hips off the floor, maintaining the line. Keep your hips square and your neck in line with your spine. Hold the position." }, { "_id": "pushUp", "name": "pushUp", "title": "Push Up", "description": "A push-up is a common exercise performed in a prone position by raising and lowering the body using the arms", "image": "img/pushup.png", "nameSound": "content/pushups.wav", "related": { "videos": ["Eh00_rniF8E", "ZWdBqFLNljc", "UwRLWMcOdwI", "ynPwl6qyUNM", "OicNTT2xzMI"] }, "procedure": "Lie prone on the ground with hands placed as wide or slightly wider than shoulder width. Keeping the body straight, lower body to the ground by bending arms at the elbows. Raise body up off the ground by extending the arms." }, { "_id": "squat", "name": "squat", "title": "Squat", "description": "The squat is a compound, full body exercise that trains primarily the muscles of the thighs, hips, buttocks and quads.", "image": "img/squat.png", "nameSound": "content/squats.wav", "related": { "videos": ["QKKZ9AGYTi4", "UXJrBgI2RxA"] }, "procedure": "Stand with your head facing forward and your chest held up and out.Place your feet shoulder-width apart or little wider. Extend your hands straight out in front of you. Sit back and down like you're sitting into a chair. Keep your head facing straight as your upper body bends forward a bit. Rather than allowing your back to round, let your lower back arch slightly as you go down. Lower down so your thighs are parallel to the floor, with your knees over your ankles. Press your weight back into your heels. Keep your body tight, and push through your heels to bring yourself back to the starting position." }]
\ No newline at end of file
diff --git a/trainer/app/js/shared/directives.js b/trainer/app/js/shared/directives.js
new file mode 100644
index 0000000..e6164db
--- /dev/null
+++ b/trainer/app/js/shared/directives.js
@@ -0,0 +1,189 @@
+///
+'use strict';
+
+/* directives */
+angular.module('app').directive('ngConfirm', [function () {
+ return {
+ restrict: 'A',
+ link: function (scope, element, attrs) {
+ element.bind('click', function () {
+ var message = attrs.ngConfirmMessage || 'Are you sure?';
+ if (message && confirm(message)) {
+ scope.$apply(attrs.ngConfirm);
+ }
+ });
+ }
+ }
+}]);
+// Angular validator pre Angular 1.3. Use this if you are on an earlier branch
+//angular.module('app').directive('remoteValidator', ['$parse', function ($parse) {
+// return {
+// restrict: 'A',
+// priority: 5,
+// require: ['ngModel', '?^busyIndicator'],
+// link: function (scope, elm, attr, ctrls) {
+// var expfn = $parse(attr["remoteValidatorFunction"]);
+// var validatorName = attr["remoteValidator"];
+// var ngModelCtrl = ctrls[0];
+// var busyIndicator = ctrls[1];
+// ngModelCtrl.$parsers.push(function (value) {
+// var result = expfn(scope, { 'value': value });
+// if (result.then) {
+// if (busyIndicator) busyIndicator.show();
+// result.then(function (data) { //For promise type result object
+// if (busyIndicator) busyIndicator.hide();
+// ngModelCtrl.$setValidity(validatorName, data);
+// }, function (error) {
+// if (busyIndicator) busyIndicator.hide();
+// ngModelCtrl.$setValidity(validatorName, true);
+// });
+// }
+// return value;
+// });
+// }
+// }
+//}]);
+
+// Angular validator using Angular 1.3. Use this if you are on 1.3
+angular.module('app').directive('remoteValidator', ['$parse', function ($parse) {
+ return {
+ restrict: 'A',
+ require: ['ngModel', '?^busyIndicator'],
+ link: function (scope, elm, attr, ctrls) {
+ var expfn = $parse(attr["remoteValidatorFunction"]);
+ var validatorName = attr["remoteValidator"];
+ var ngModelCtrl = ctrls[0];
+ var busyIndicator = ctrls[1];
+
+ ngModelCtrl.$asyncValidators[validatorName] = function (value) {
+ return expfn(scope, { 'value': value });
+ }
+ if (busyIndicator) {
+ scope.$watch(function () { return ngModelCtrl.$pending; }, function (newValue) {
+ if (newValue) busyIndicator.show();
+ else busyIndicator.hide();
+ });
+ }
+ }
+ }
+}]);
+
+// Angular 1.3 has already a built-in directive for supporting update on blur. Instead of using update-on-blur use ng-model-options="{ updateOn: 'blur' }" for Angular 1.3
+angular.module('app').directive('updateOnBlur', function () {
+ return {
+ restrict: 'A',
+ require: 'ngModel',
+ priority: '100',
+ link: function (scope, elm, attr, ngModelCtrl) {
+ if (attr.type === 'radio' || attr.type === 'checkbox') return;
+ elm.unbind('input').unbind('keydown').unbind('change');
+ elm.bind('blur', function () {
+ scope.$apply(function () {
+ ngModelCtrl.$setViewValue(elm.val());
+ });
+ });
+ }
+ };
+});
+
+angular.module('app').directive('busyIndicator', ['$compile', function ($compile) {
+ return {
+ scope: true,
+ transclude: true,
+ template: '
',
+ //compile: function (element, attr) {
+ // // Injecting dynamic html at compile phase.
+ // // To use it comment transclude:true and template:
.. properties
+ // // No need to compile the DOM.
+ // // Just append it to element, and Angular will compile and later link it.
+ // var busyHtml = '';
+ // element.append(busyHtml);
+ // return function (scope, element, attr) { } //link function
+ //},
+ //link: function (scope, element, attr) {
+ // // Inject dynamic html at link phase.
+ // // To use it comment transclude:true and template:
..
properties
+ // // Then inject the dependency $compile on the directive
+ // // Need to compile the DOM that before adding.
+ // // Just append it to element, and Angular will compile it.
+ // var linkfn = $compile('');
+ // element.append(linkfn(scope));
+ //},
+ controller: ['$scope', function ($scope) {
+ this.show = function () { $scope.busy = true; }
+ this.hide = function () { $scope.busy = false; }
+ }]
+
+ }
+}]);
+
+angular.module('app').directive('ajaxButton', ['$compile', '$animate', function ($compile, $animate) {
+ return {
+ transclude: true,
+ restrict: 'E',
+ scope: {
+ onClick: '&',
+ submitting: '@'
+ },
+ replace: true,
+ template: '',
+ link: function (scope, element, attr) {
+ if (attr.submitting !== undefined && attr.submitting != null) {
+ attr.$observe("submitting", function (value) {
+ if (value) scope.busy = JSON.parse(value);
+ });
+ }
+ if (attr.onClick) {
+ element.on('click', function (event) {
+ scope.$apply(function () {
+ var result = scope.onClick();
+ if (attr.submitting !== undefined && attr.submitting != null) return; //submitting attribute if there takes priority.
+ if (result.finally) {
+ scope.busy = true;
+ result.finally(function () { scope.busy = false });
+ }
+ });
+ });
+ }
+ }
+ }
+}]);
+angular.module('app').directive('owlCarousel', ['$compile', '$timeout', function ($compile, $timeout) {
+ var owl = null;
+ return {
+ scope: {
+ options: '=',
+ source: '=',
+ onUpdate: '&',
+ },
+ link: function (scope, element, attr) {
+ var defaultOptions = {
+ singleItem: true,
+ pagination: false,
+ afterAction: function () {
+ var itemIndex = this.currentItem;
+ scope.$evalAsync(function () {
+ scope.onUpdate({ currentItemIndex: itemIndex });
+ })
+ },
+ };
+ if (scope.options) angular.extend(defaultOptions, scope.options);
+ scope.$watch("source", function (newValue) {
+ if (newValue) {
+ $timeout(function () {
+ owl = element.owlCarousel(defaultOptions);
+ }, 0);
+ }
+ });
+ },
+ controller: ['$scope', '$attrs', function ($scope, $attrs) {
+ if ($attrs.owlCarousel) $scope.$parent[$attrs.owlCarousel] = this;
+ this.next = function () {
+ owl.trigger('owl.next');
+ };
+ this.previous = function () {
+ owl.trigger('owl.prev');
+ };
+ }]
+ };
+}]);
diff --git a/trainer/app/js/shared/directives.spec.js b/trainer/app/js/shared/directives.spec.js
new file mode 100644
index 0000000..c45c4e6
--- /dev/null
+++ b/trainer/app/js/shared/directives.spec.js
@@ -0,0 +1,145 @@
+describe("Directives", function () {
+ var $compile, $rootScope, $scope;
+
+ beforeEach(module('app'));
+
+ beforeEach(inject(function (_$compile_, _$rootScope_) {
+ $compile = _$compile_;
+ $rootScope = _$rootScope_;
+ $scope = $rootScope.$new();
+ }));
+
+ describe("remote validator", function () {
+ var inputElement;
+ beforeEach(inject(function () {
+ $scope.validate = function (value) { };
+ inputElement = "";
+ }));
+
+ it("should fail if ng-model not defined.", function () {
+ expect($compile("")).toThrow();
+
+ });
+
+ it("should load the directive without error", function () {
+ $compile(inputElement)($scope);
+ });
+
+ it("should verify unique value when use input changes", inject(function ($q) {
+ spyOn($scope, "validate").and.returnValue($q.when(true));
+ $compile(inputElement)($scope);
+ $scope.testForm.unique.$setViewValue("dummy");
+ expect($scope.validate).toHaveBeenCalled();
+ }));
+
+ it("verify failed 'unqiue' validation should set model controller invalid.", inject(function ($q) {
+ spyOn($scope, "validate").and.returnValue($q.reject());
+ $compile(inputElement)($scope);
+ $scope.testForm.unique.$setViewValue("dummy");
+ expect($scope.validate).toHaveBeenCalled();
+ $scope.$digest();
+
+ expect($scope.testForm.$valid).toBe(false);
+ expect($scope.testForm.unique.$valid).toBe(false);
+ expect($scope.testForm.unique.$error.unique).toBe(true);
+
+
+ }));
+
+ it("should not have error if remote validation success", inject(function ($q) {
+ spyOn($scope, "validate").and.returnValue($q.when(true));
+ $scope.name = "initialValue";
+ $compile(inputElement)($scope);
+ $scope.testForm.unique.$setViewValue("dummy");
+ $scope.$digest();
+
+ expect($scope.validate).toHaveBeenCalled();
+ expect($scope.testForm.$valid).toBe(true);
+ expect($scope.testForm.unique.$valid).toBe(true);
+ expect($scope.testForm.unique.$error.unique).toBeUndefined(false);
+ }));
+ });
+
+ describe("remote validator with busy indicator", function () {
+ var inputElement;
+ beforeEach(inject(function ($q) {
+ $scope.validate = function () { };
+ inputElement = "";
+ }));
+
+ it("should load busy indicator", function () {
+ var e = $compile(inputElement)($scope);
+
+ expect(e.html().indexOf("glyphicon glyphicon-refresh") > 0).toBe(true);
+ });
+
+ it("should show busy indicator when remote request is made and hide later", inject(function ($q) {
+ var defer = $q.defer(),
+ html = $compile(inputElement)($scope),
+ childElementScope = html.children().scope();
+
+ spyOn($scope, "validate").and.returnValue(defer.promise);
+
+ expect(childElementScope.busy).toBeUndefined();
+
+ $scope.testForm.unique.$setViewValue("dummy");
+ expect(childElementScope.busy).toBe(true);
+
+ defer.resolve(true);
+ $scope.$digest();
+
+ expect(childElementScope.busy).toBe(false);
+
+ }));
+ });
+
+ describe("ajax button validator", function () {
+
+ beforeEach(inject(function () {
+ $scope.save = function (value) { };
+ $scope.submitted = false;
+ }));
+ it("should load ajax button validator", function () {
+ var inputElement = 'Save';
+ var e = $compile(inputElement)($scope);
+ expect(e[0] instanceof HTMLButtonElement).toBe(true);
+
+ });
+
+ it("should load set indicator busy when request is made.", inject(function ($q) {
+ var defer = $q.defer();
+ spyOn($scope, "save").and.returnValue(defer.promise);
+ var inputElement = 'Save';
+ var e = $compile(inputElement)($scope),
+ isolatedScope = e.isolateScope();
+
+ e[0].click();
+
+ expect(isolatedScope.busy).toBe(true);
+ expect($scope.save).toHaveBeenCalled();
+
+ defer.resolve(true);
+ $scope.$digest();
+ expect(isolatedScope.busy).toBe(false);
+ }));
+
+ it("should load set indicator busy when submitted flag is set to true.", inject(function ($q) {
+ spyOn($scope, "save").and.returnValue($q.when(true));
+ var inputElement = 'Save';
+ var e = $compile(inputElement)($scope),
+ isolatedScope = e.isolateScope();
+
+ $scope.submitted = true;
+
+ e[0].click();
+
+ expect(isolatedScope.busy).toBe(true);
+ expect($scope.save).toHaveBeenCalled();
+
+ $scope.submitted = false;
+ $scope.$digest();
+ expect(isolatedScope.busy).toBe(false);
+ }));
+
+ });
+});
\ No newline at end of file
diff --git a/trainer/app/js/shared/filters.js b/trainer/app/js/shared/filters.js
new file mode 100644
index 0000000..2eec648
--- /dev/null
+++ b/trainer/app/js/shared/filters.js
@@ -0,0 +1,17 @@
+'use strict';
+
+/* Filters */
+angular.module('app').filter('secondsToTime', function () {
+ return function (input) {
+ var sec = parseInt(input, 10);
+ if (isNaN(sec)) return "00:00:00";
+
+ var hours = Math.floor(sec / 3600);
+ var minutes = Math.floor((sec - (hours * 3600)) / 60);
+ var seconds = sec - (hours * 3600) - (minutes * 60);
+
+ return ("0" + hours).substr(-2) + ':'
+ + ("0" + minutes).substr(-2) + ':'
+ + ("0" + seconds).substr(-2);
+ }
+});
\ No newline at end of file
diff --git a/trainer/app/js/shared/model.js b/trainer/app/js/shared/model.js
new file mode 100644
index 0000000..b02a073
--- /dev/null
+++ b/trainer/app/js/shared/model.js
@@ -0,0 +1,37 @@
+'use strict';
+
+/* Model classes */
+angular.module('app')
+ .factory('Exercise', function () {
+ function Exercise(args) {
+ this.name = args.name;
+ this.title = args.title;
+ this.description = args.description;
+ this.image = args.image;
+ this.related = {};
+ this.related.videos = (args.related && args.related.videos) ? args.related.videos : [];
+ this.nameSound = args.nameSound;
+ this.procedure = args.procedure;
+ }
+ return Exercise;
+ });
+
+angular.module('app')
+ .factory('WorkoutPlan', function () {
+ function WorkoutPlan(args) {
+ this.exercises = args.exercises || [];
+ this.name = args.name;
+ this.title = args.title;
+ this.description = args.description;
+ this.restBetweenExercise = args.restBetweenExercise;
+ };
+ WorkoutPlan.prototype.totalWorkoutDuration = function () {
+ if (this.exercises.length == 0) return 0;
+ var total = 0;
+ angular.forEach(this.exercises, function (exercise) {
+ total = total + (exercise.duration ? exercise.duration : 0);
+ });
+ return (this.restBetweenExercise ? this.restBetweenExercise : 0) * (this.exercises.length - 1) + total;
+ }
+ return WorkoutPlan;
+ });
diff --git a/trainer/app/js/shared/model.spec.js b/trainer/app/js/shared/model.spec.js
new file mode 100644
index 0000000..e69de29
diff --git a/trainer/app/js/shared/services.js b/trainer/app/js/shared/services.js
new file mode 100644
index 0000000..cb45df9
--- /dev/null
+++ b/trainer/app/js/shared/services.js
@@ -0,0 +1,106 @@
+'use strict';
+
+/* Services */
+angular.module('app')
+ .value("appEvents", {
+ workout: { exerciseStarted: "event:workout:exerciseStarted" }
+ });
+
+angular.module('app')
+ .provider("WorkoutService", function () {
+ var apiUrl = "https://api.mongolab.com/api/1/databases/";
+ var collectionsUrl = null;
+ var database = null;
+ var apiKey = null;
+
+ this.configure = function (dbName) {
+ database = database;
+ collectionsUrl = apiUrl + dbName + "/collections";
+ }
+
+ this.$get = ['WorkoutPlan', 'Exercise', '$http', '$q', '$resource', function (WorkoutPlan, Exercise, $http, $q, $resource) {
+ var service = {};
+ var workouts = [];
+ var exercises = [];
+
+ service.Exercises = $resource(collectionsUrl + "/exercises/:id", {}, { update: { method: 'PUT' } });
+
+ service.getWorkouts = function () {
+ return $http.get(collectionsUrl + "/workouts")
+ .then(function (response) {
+ return response.data.map(function (workout) {
+ return new WorkoutPlan(workout);
+ });
+ });
+ };
+
+ service.getWorkout = function (name) {
+ return $q.all([service.Exercises.query().$promise, $http.get(collectionsUrl + "/workouts/" + name)])
+ .then(function (response) {
+ var allExercises = response[0];
+ var workout = new WorkoutPlan(response[1].data);
+
+ angular.forEach(response[1].data.exercises, function (exercise) {
+ exercise.details = allExercises.filter(function (e) { return e.name === exercise.name; })[0];
+ });
+ return workout;
+ });
+ };
+
+ service.updateWorkout = function (workout) {
+ return service.getWorkout(workout.name)
+ .then(function (original) {
+ if (original) {
+ var workoutToSave = angular.copy(workout);
+ workoutToSave.exercises = workoutToSave.exercises.map(function (exercise) { return { name: exercise.details.name, duration: exercise.duration } });
+ return $http.put(collectionsUrl + "/workouts/" + original.name, workoutToSave);
+ }
+ })
+ .then(function (response) {
+ return workout;
+ });
+ };
+
+ service.addWorkout = function (workout) {
+ if (workout.name) {
+ var workoutToSave = angular.copy(workout);
+ workoutToSave.exercises = workoutToSave.exercises.map(function (exercise) { return { name: exercise.details.name, duration: exercise.duration } });
+ workoutToSave._id = workoutToSave.name;
+ return $http.post(collectionsUrl + "/workouts", workoutToSave)
+ .then(function (response) {
+ return workout
+ });
+ }
+ }
+
+ service.deleteWorkout = function (workoutName) {
+ return $http.delete(collectionsUrl + "/workouts/" + workoutName);
+ };
+
+ return service;
+ }];
+
+ var init = function () {
+ };
+
+ init();
+ });
+
+angular.module('app')
+ .provider('ApiKeyAppenderInterceptor', function () {
+ var apiKey = null;
+ this.setApiKey = function (key) {
+ apiKey = key;
+ }
+ this.$get = ['$q', function ($q) {
+ return {
+ 'request': function (config) {
+ if (apiKey && config && config.url.toLowerCase().indexOf("https://api.mongolab.com") >= 0) {
+ config.params = config.params || {};
+ config.params.apiKey = apiKey;
+ }
+ return config || $q.when(config);
+ }
+ }
+ }];
+ });
\ No newline at end of file
diff --git a/trainer/app/js/shared/services.spec.js b/trainer/app/js/shared/services.spec.js
new file mode 100644
index 0000000..f0ffa61
--- /dev/null
+++ b/trainer/app/js/shared/services.spec.js
@@ -0,0 +1,76 @@
+describe("Shared Services", function () {
+ beforeEach(module('app'));
+
+ describe("Workout Service", function () {
+ var WorkoutService, $httpBackend,
+ collectionUrl = "https://api.mongolab.com/api/1/databases/testdb/collections",
+ apiKey = "testKey";
+
+ beforeEach(module(function (WorkoutServiceProvider, ApiKeyAppenderInterceptorProvider) {
+ WorkoutServiceProvider.configure("testdb");
+ ApiKeyAppenderInterceptorProvider.setApiKey("testKey")
+ }));
+
+ beforeEach(inject(function (_WorkoutService_, _$httpBackend_) {
+ WorkoutService = _WorkoutService_;
+ $httpBackend = _$httpBackend_;
+ }));
+
+ it("should load Workout service", function () {
+ expect(WorkoutService).toBeDefined();
+ });
+
+ it("should request all workouts endpoints", function () {
+ $httpBackend.expectGET(collectionUrl + "/workouts?apiKey=" + "testKey").respond([]);
+ WorkoutService.getWorkouts();
+ $httpBackend.flush();
+ });
+
+ it("should return all workout plans", inject(function (WorkoutPlan) {
+ $httpBackend.expectGET(collectionUrl + "/workouts?apiKey=" + "testKey").respond([{ name: "Workout1", title: "workout1" }, { name: "Workout1", title: "workout1" }]);
+ var result = null;
+ WorkoutService.getWorkouts()
+ .then(function (workouts) {
+ result = workouts;
+ });
+ $httpBackend.flush();
+
+ expect(result.length).toBe(2);
+ expect(result[0] instanceof WorkoutPlan).toBe(true);
+ }));
+
+ it("should return a workout plan with specific name", inject(function (WorkoutPlan, $q) {
+ spyOn(WorkoutService.Exercises, "query").and.returnValue({ $promise: $q.when([{ name: "exercise1", title: "exercise 1" }]) });
+ $httpBackend.expectGET(collectionUrl + "/workouts/testplan?apiKey=" + "testKey").respond({ name: "Workout1", title: "Workout 1", restBetweenExercise: 30 });
+ var result = null;
+ WorkoutService.getWorkout("testplan")
+ .then(function (workout) { result = workout; });
+ $httpBackend.flush();
+
+ expect(result.name).toBe("Workout1");
+ expect(result instanceof WorkoutPlan).toBe(true);
+ expect(WorkoutService.Exercises.query).toHaveBeenCalled();
+ }));
+
+ it("should map exercises to workout plan correctly in getWorkout", inject(function (WorkoutPlan, Exercise, $q) {
+ spyOn(WorkoutService.Exercises, "query").and.returnValue({ $promise: $q.when([{ name: "exercise1", title: "exercise 1" }, { name: "exercise2", title: "exercise 2" }, { name: "exercise3", title: "exercise 3" }], { name: "exercise4", title: "exercise 4" }) });
+ $httpBackend.expectGET(collectionUrl + "/workouts/testplan?apiKey=" + "testKey").respond({ name: "Workout2", title: "Workout 1", restBetweenExercise: 30, exercises: [{ name: "exercise2", duration: 31 }, { name: "exercise4", duration: 31 }] });
+ var result = null;
+ WorkoutService.getWorkout("testplan")
+ .then(function (workout) { result = workout; });
+ $httpBackend.flush();
+
+ expect(result.name).toBe("Workout2");
+ expect(WorkoutService.Exercises.query).toHaveBeenCalled();
+ expect(result instanceof WorkoutPlan).toBe(true);
+ expect(result.exercises.length).toBe(2);
+ expect(result.exercises[0].name).toBe("exercise2");
+ expect(result.exercises[1].name).toBe("exercise4");
+ }));
+
+ afterEach(function () {
+ $httpBackend.verifyNoOutstandingExpectation();
+ $httpBackend.verifyNoOutstandingRequest();
+ });
+ });
+});
\ No newline at end of file
diff --git a/trainer/app/js/vendor/LICENSE.angular.media.player b/trainer/app/js/vendor/LICENSE.angular.media.player
new file mode 100644
index 0000000..98c6968
--- /dev/null
+++ b/trainer/app/js/vendor/LICENSE.angular.media.player
@@ -0,0 +1,20 @@
+The MIT License (MIT)
+
+Copyright (c) 2014 Valerio Coltrè
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/trainer/app/js/vendor/LICENSE.local.storage b/trainer/app/js/vendor/LICENSE.local.storage
new file mode 100644
index 0000000..cd18384
--- /dev/null
+++ b/trainer/app/js/vendor/LICENSE.local.storage
@@ -0,0 +1,8 @@
+Angular Local Storage
+Copyright 2013 Gregory Pike
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
\ No newline at end of file
diff --git a/trainer/app/js/vendor/angular-local-storage.js b/trainer/app/js/vendor/angular-local-storage.js
new file mode 100644
index 0000000..0a8ee3f
--- /dev/null
+++ b/trainer/app/js/vendor/angular-local-storage.js
@@ -0,0 +1,388 @@
+(function() {
+/* Start angularLocalStorage */
+'use strict';
+var angularLocalStorage = angular.module('LocalStorageModule', []);
+
+angularLocalStorage.provider('localStorageService', function() {
+
+ // You should set a prefix to avoid overwriting any local storage variables from the rest of your app
+ // e.g. localStorageServiceProvider.setPrefix('youAppName');
+ // With provider you can use config as this:
+ // myApp.config(function (localStorageServiceProvider) {
+ // localStorageServiceProvider.prefix = 'yourAppName';
+ // });
+ this.prefix = 'ls';
+
+ // You could change web storage type localstorage or sessionStorage
+ this.storageType = 'localStorage';
+
+ // Cookie options (usually in case of fallback)
+ // expiry = Number of days before cookies expire // 0 = Does not expire
+ // path = The web path the cookie represents
+ this.cookie = {
+ expiry: 30,
+ path: '/'
+ };
+
+ // Send signals for each of the following actions?
+ this.notify = {
+ setItem: true,
+ removeItem: false
+ };
+
+ // Setter for the prefix
+ this.setPrefix = function(prefix) {
+ this.prefix = prefix;
+ };
+
+ // Setter for the storageType
+ this.setStorageType = function(storageType) {
+ this.storageType = storageType;
+ };
+
+ // Setter for cookie config
+ this.setStorageCookie = function(exp, path) {
+ this.cookie = {
+ expiry: exp,
+ path: path
+ };
+ };
+
+ // Setter for cookie domain
+ this.setStorageCookieDomain = function(domain) {
+ this.cookie.domain = domain;
+ };
+
+ // Setter for notification config
+ // itemSet & itemRemove should be booleans
+ this.setNotify = function(itemSet, itemRemove) {
+ this.notify = {
+ setItem: itemSet,
+ removeItem: itemRemove
+ };
+ };
+
+
+
+ this.$get = ['$rootScope', '$window', '$document', function($rootScope, $window, $document) {
+
+ var prefix = this.prefix;
+ var cookie = this.cookie;
+ var notify = this.notify;
+ var storageType = this.storageType;
+ var webStorage;
+
+ // When Angular's $document is not available
+ if (!$document) {
+ $document = document;
+ }
+
+ // If there is a prefix set in the config lets use that with an appended period for readability
+ if (prefix.substr(-1) !== '.') {
+ prefix = !!prefix ? prefix + '.' : '';
+ }
+ var deriveQualifiedKey = function(key) {
+ return prefix + key;
+ }
+ // Checks the browser to see if local storage is supported
+ var browserSupportsLocalStorage = (function () {
+ try {
+ var supported = (storageType in $window && $window[storageType] !== null);
+
+ // When Safari (OS X or iOS) is in private browsing mode, it appears as though localStorage
+ // is available, but trying to call .setItem throws an exception.
+ //
+ // "QUOTA_EXCEEDED_ERR: DOM Exception 22: An attempt was made to add something to storage
+ // that exceeded the quota."
+ var key = deriveQualifiedKey('__' + Math.round(Math.random() * 1e7));
+ if (supported) {
+ webStorage = $window[storageType];
+ webStorage.setItem(key, '');
+ webStorage.removeItem(key);
+ }
+
+ return supported;
+ } catch (e) {
+ storageType = 'cookie';
+ $rootScope.$broadcast('LocalStorageModule.notification.error', e.message);
+ return false;
+ }
+ }());
+
+
+
+ // Directly adds a value to local storage
+ // If local storage is not available in the browser use cookies
+ // Example use: localStorageService.add('library','angular');
+ var addToLocalStorage = function (key, value) {
+
+ // If this browser does not support local storage use cookies
+ if (!browserSupportsLocalStorage) {
+ $rootScope.$broadcast('LocalStorageModule.notification.warning', 'LOCAL_STORAGE_NOT_SUPPORTED');
+ if (notify.setItem) {
+ $rootScope.$broadcast('LocalStorageModule.notification.setitem', {key: key, newvalue: value, storageType: 'cookie'});
+ }
+ return addToCookies(key, value);
+ }
+
+ // Let's convert undefined values to null to get the value consistent
+ if (typeof value === "undefined") {
+ value = null;
+ }
+
+ try {
+ if (angular.isObject(value) || angular.isArray(value)) {
+ value = angular.toJson(value);
+ }
+ if (webStorage) {webStorage.setItem(deriveQualifiedKey(key), value)};
+ if (notify.setItem) {
+ $rootScope.$broadcast('LocalStorageModule.notification.setitem', {key: key, newvalue: value, storageType: this.storageType});
+ }
+ } catch (e) {
+ $rootScope.$broadcast('LocalStorageModule.notification.error', e.message);
+ return addToCookies(key, value);
+ }
+ return true;
+ };
+
+ // Directly get a value from local storage
+ // Example use: localStorageService.get('library'); // returns 'angular'
+ var getFromLocalStorage = function (key) {
+
+ if (!browserSupportsLocalStorage) {
+ $rootScope.$broadcast('LocalStorageModule.notification.warning','LOCAL_STORAGE_NOT_SUPPORTED');
+ return getFromCookies(key);
+ }
+
+ var item = webStorage ? webStorage.getItem(deriveQualifiedKey(key)) : null;
+ // angular.toJson will convert null to 'null', so a proper conversion is needed
+ // FIXME not a perfect solution, since a valid 'null' string can't be stored
+ if (!item || item === 'null') {
+ return null;
+ }
+
+ if (item.charAt(0) === "{" || item.charAt(0) === "[") {
+ return angular.fromJson(item);
+ }
+
+ return item;
+ };
+
+ // Remove an item from local storage
+ // Example use: localStorageService.remove('library'); // removes the key/value pair of library='angular'
+ var removeFromLocalStorage = function (key) {
+ if (!browserSupportsLocalStorage) {
+ $rootScope.$broadcast('LocalStorageModule.notification.warning', 'LOCAL_STORAGE_NOT_SUPPORTED');
+ if (notify.removeItem) {
+ $rootScope.$broadcast('LocalStorageModule.notification.removeitem', {key: key, storageType: 'cookie'});
+ }
+ return removeFromCookies(key);
+ }
+
+ try {
+ webStorage.removeItem(deriveQualifiedKey(key));
+ if (notify.removeItem) {
+ $rootScope.$broadcast('LocalStorageModule.notification.removeitem', {key: key, storageType: this.storageType});
+ }
+ } catch (e) {
+ $rootScope.$broadcast('LocalStorageModule.notification.error', e.message);
+ return removeFromCookies(key);
+ }
+ return true;
+ };
+
+ // Return array of keys for local storage
+ // Example use: var keys = localStorageService.keys()
+ var getKeysForLocalStorage = function () {
+
+ if (!browserSupportsLocalStorage) {
+ $rootScope.$broadcast('LocalStorageModule.notification.warning', 'LOCAL_STORAGE_NOT_SUPPORTED');
+ return false;
+ }
+
+ var prefixLength = prefix.length;
+ var keys = [];
+ for (var key in webStorage) {
+ // Only return keys that are for this app
+ if (key.substr(0,prefixLength) === prefix) {
+ try {
+ keys.push(key.substr(prefixLength));
+ } catch (e) {
+ $rootScope.$broadcast('LocalStorageModule.notification.error', e.Description);
+ return [];
+ }
+ }
+ }
+ return keys;
+ };
+
+ // Remove all data for this app from local storage
+ // Also optionally takes a regular expression string and removes the matching key-value pairs
+ // Example use: localStorageService.clearAll();
+ // Should be used mostly for development purposes
+ var clearAllFromLocalStorage = function (regularExpression) {
+
+ regularExpression = regularExpression || "";
+ //accounting for the '.' in the prefix when creating a regex
+ var tempPrefix = prefix.slice(0, -1);
+ var testRegex = new RegExp(tempPrefix + '.' + regularExpression);
+
+ if (!browserSupportsLocalStorage) {
+ $rootScope.$broadcast('LocalStorageModule.notification.warning', 'LOCAL_STORAGE_NOT_SUPPORTED');
+ return clearAllFromCookies();
+ }
+
+ var prefixLength = prefix.length;
+
+ for (var key in webStorage) {
+ // Only remove items that are for this app and match the regular expression
+ if (testRegex.test(key)) {
+ try {
+ removeFromLocalStorage(key.substr(prefixLength));
+ } catch (e) {
+ $rootScope.$broadcast('LocalStorageModule.notification.error',e.message);
+ return clearAllFromCookies();
+ }
+ }
+ }
+ return true;
+ };
+
+ // Checks the browser to see if cookies are supported
+ var browserSupportsCookies = function() {
+ try {
+ return navigator.cookieEnabled ||
+ ("cookie" in $document && ($document.cookie.length > 0 ||
+ ($document.cookie = "test").indexOf.call($document.cookie, "test") > -1));
+ } catch (e) {
+ $rootScope.$broadcast('LocalStorageModule.notification.error', e.message);
+ return false;
+ }
+ };
+
+ // Directly adds a value to cookies
+ // Typically used as a fallback is local storage is not available in the browser
+ // Example use: localStorageService.cookie.add('library','angular');
+ var addToCookies = function (key, value) {
+
+ if (typeof value === "undefined") {
+ return false;
+ }
+
+ if (!browserSupportsCookies()) {
+ $rootScope.$broadcast('LocalStorageModule.notification.error', 'COOKIES_NOT_SUPPORTED');
+ return false;
+ }
+
+ try {
+ var expiry = '',
+ expiryDate = new Date(),
+ cookieDomain = '';
+
+ if (value === null) {
+ // Mark that the cookie has expired one day ago
+ expiryDate.setTime(expiryDate.getTime() + (-1 * 24 * 60 * 60 * 1000));
+ expiry = "; expires=" + expiryDate.toGMTString();
+ value = '';
+ } else if (cookie.expiry !== 0) {
+ expiryDate.setTime(expiryDate.getTime() + (cookie.expiry * 24 * 60 * 60 * 1000));
+ expiry = "; expires=" + expiryDate.toGMTString();
+ }
+ if (!!key) {
+ var cookiePath = "; path=" + cookie.path;
+ if(cookie.domain){
+ cookieDomain = "; domain=" + cookie.domain;
+ }
+ $document.cookie = deriveQualifiedKey(key) + "=" + encodeURIComponent(value) + expiry + cookiePath + cookieDomain;
+ }
+ } catch (e) {
+ $rootScope.$broadcast('LocalStorageModule.notification.error',e.message);
+ return false;
+ }
+ return true;
+ };
+
+ // Directly get a value from a cookie
+ // Example use: localStorageService.cookie.get('library'); // returns 'angular'
+ var getFromCookies = function (key) {
+ if (!browserSupportsCookies()) {
+ $rootScope.$broadcast('LocalStorageModule.notification.error', 'COOKIES_NOT_SUPPORTED');
+ return false;
+ }
+
+ var cookies = $document.cookie && $document.cookie.split(';') || [];
+ for(var i=0; i < cookies.length; i++) {
+ var thisCookie = cookies[i];
+ while (thisCookie.charAt(0) === ' ') {
+ thisCookie = thisCookie.substring(1,thisCookie.length);
+ }
+ if (thisCookie.indexOf(deriveQualifiedKey(key) + '=') === 0) {
+ return decodeURIComponent(thisCookie.substring(prefix.length + key.length + 1, thisCookie.length));
+ }
+ }
+ return null;
+ };
+
+ var removeFromCookies = function (key) {
+ addToCookies(key,null);
+ };
+
+ var clearAllFromCookies = function () {
+ var thisCookie = null, thisKey = null;
+ var prefixLength = prefix.length;
+ var cookies = $document.cookie.split(';');
+ for(var i = 0; i < cookies.length; i++) {
+ thisCookie = cookies[i];
+
+ while (thisCookie.charAt(0) === ' ') {
+ thisCookie = thisCookie.substring(1, thisCookie.length);
+ }
+
+ var key = thisCookie.substring(prefixLength, thisCookie.indexOf('='));
+ removeFromCookies(key);
+ }
+ };
+
+ var getStorageType = function() {
+ return storageType;
+ };
+
+ var bindToScope = function(scope, key, def) {
+ var value = getFromLocalStorage(key);
+
+ if (value === null && angular.isDefined(def)) {
+ value = def;
+ } else if (angular.isObject(value) && angular.isObject(def)) {
+ value = angular.extend(def, value);
+ }
+
+ scope[key] = value;
+
+ scope.$watchCollection(key, function(newVal) {
+ addToLocalStorage(key, newVal);
+ });
+ };
+
+ return {
+ isSupported: browserSupportsLocalStorage,
+ getStorageType: getStorageType,
+ set: addToLocalStorage,
+ add: addToLocalStorage, //DEPRECATED
+ get: getFromLocalStorage,
+ keys: getKeysForLocalStorage,
+ remove: removeFromLocalStorage,
+ clearAll: clearAllFromLocalStorage,
+ bind: bindToScope,
+ deriveKey: deriveQualifiedKey,
+ cookie: {
+ set: addToCookies,
+ add: addToCookies, //DEPRECATED
+ get: getFromCookies,
+ remove: removeFromCookies,
+ clearAll: clearAllFromCookies
+ }
+ };
+ }];
+});
+}).call(this);
+
diff --git a/trainer/app/js/vendor/angular-media-player.js b/trainer/app/js/vendor/angular-media-player.js
new file mode 100644
index 0000000..ed55d34
--- /dev/null
+++ b/trainer/app/js/vendor/angular-media-player.js
@@ -0,0 +1,511 @@
+/**
+ * MDN references for hackers:
+ * ===========================
+ * Media events on