diff --git a/.travis.yml b/.travis.yml index a59245d..0df39ec 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,6 +3,13 @@ language: node_js node_js: - "0.10" +before_install: npm install -g grunt-cli + +install: npm install + +script: grunt test + branches: only: - - develop \ No newline at end of file + - develop + - master diff --git a/api/controllers/PageController.js b/api/controllers/PageController.js new file mode 100644 index 0000000..aeec543 --- /dev/null +++ b/api/controllers/PageController.js @@ -0,0 +1,84 @@ +/** + * PageController + * + * @description :: Server-side logic for managing pages + * @help :: See http://links.sailsjs.org/docs/controllers + * https://github.com/irlnathan/activityoverlord20/blob/master/api/controllers/PageController.js + */ + +module.exports = { + + showHomePage: function (req, res) { + + // If not logged in, show the public view. + if (!req.session.me) { + //return res.view('homepage'); + return res.view('homepage', { + me: [], + video: [] + }); + } + + // Otherwise, look up the logged-in user and show the logged-in view, + // bootstrapping basic user data in the HTML sent from the server + User.findOne(req.session.me, function (err, user){ + if (err) { + return res.negotiate(err); + } + + if (!user) { + sails.log.verbose('Session refers to a user who no longer exists- did you delete a user, then try to refresh the page with an open tab logged-in as that user?'); + //return res.view('homepage'); + return res.view('homepage', { + me: [], + video: [] + }); + } + + return res.view('dashboard', { + me: user, + video: [] + }); + + }); + }, + + showEditPage: function (req, res) { + + // If not logged in, show the public view. + if (!req.session.me) { + return res.view('homepage'); + } + + // Otherwise, look up the logged-in user and show the logged-in view, + // bootstrapping basic user data in the HTML sent from the server + User.findOne(req.session.me, function (err, user){ + if (err) { + return res.negotiate(err); + } + + if (!user) { + sails.log.verbose('Session refers to a user who no longer exists- did you delete a user, then try to refresh the page with an open tab logged-in as that user?'); + return res.view('homepage', { + me: [], + video: [] + }); + } + + // retreive the video object using the id + Video.findOne(req.param('id'), function(err, video){ + if (err) { + // return error + } + + // if successful, return user and video object to frontend + return res.view('edit', { + me: user, + video: video + }); + + }); + }); + } + +}; diff --git a/api/controllers/UserController.js b/api/controllers/UserController.js index 1a958c4..c2c4e7a 100644 --- a/api/controllers/UserController.js +++ b/api/controllers/UserController.js @@ -12,6 +12,8 @@ module.exports = { /** * `UserController.login()` + * Usage: POST /api/user/login + * Content: {username: ':username', password: ':password'} */ login: function (req, res) { // Look for user using given username @@ -49,19 +51,73 @@ module.exports = { /** * `UserController.signup()` + * Usage: POST /api/user/signup + * Content: {username: ':username', password: ':password', email: ':emailaddress'} */ signup: function (req, res) { - return res.json({ - todo: 'signup() is not implemented yet!' + Passwords.encryptPassword({ + // Encrypt with BCrypt algo + password: req.param('password'), + difficulty: 10, + }).exec({ + error: function(err) { + return res.negotiate(err); + }, + + success: function(encryptedPassword) { + User.create({ + username: req.param('username'), + encryptedPassword: encryptedPassword, + email: req.param('email') + }).exec(function(err, newUser) { + if (err) { + console.log("err: ", err); + console.log("err.invalidAttributes: ", err.invalidAttributes); + + // If this is a uniqueness error about the email attribute, + // send back an easily parseable status code. + if (err.invalidAttributes && err.invalidAttributes.email && err.invalidAttributes.email[0] + && err.invalidAttributes.email[0].rule === 'unique') { + return res.emailAddressInUse(); + } + + return res.negotitate(err); + } + + // Log user in + req.session.me = newUser.id; + + // Send back the id of the new user + return res.json({ + id: newUser.id + }); + }); + }, }); }, /** * `UserController.logout()` + * Usage: GET /api/user/logout */ logout: function (req, res) { - return res.json({ - todo: 'logout() is not implemented yet!' + // Look up the user record from the database which is + // referenced by the id in the user session (req.session.me) + User.findOne(req.session.me, function foundUser(err, user) { + if (err) return res.negotiate(err); + + // If session refers to a user who no longer exists, still allow logout. + if (!user) { + sails.log.verbose('Session refers to a user who no longer exists.'); + return res.backToHomePage(); + } + + // Wipe out the session (log out) + req.session.me = null; + + // Either send a 200 OK or redirect to the home page + return res.backToHomePage(); + }); }, diff --git a/api/controllers/VideoController.js b/api/controllers/VideoController.js index 61ab520..e67e2b5 100644 --- a/api/controllers/VideoController.js +++ b/api/controllers/VideoController.js @@ -50,7 +50,6 @@ module.exports = { Video.destroy({ id: req.param('id') }).exec(function (err, video) { - console.log(req.body.videoId); if (err) throw err; res.json(video); }); diff --git a/api/models/User.js b/api/models/User.js index 5cff32e..281e76e 100644 --- a/api/models/User.js +++ b/api/models/User.js @@ -20,11 +20,22 @@ module.exports = { columnName: 'encrypted_password' }, + lastLoggedIn: { + type: 'datetime', + defaultsTo: function() {return new Date(); } + }, + + email: { + type: 'string', + email: true, + required: true, + unique: true + }, + // 0 for admin, 1 for normal user permission: { type: 'integer', - defaultTo: 1, - required: true + defaultsTo: 1 } }, } diff --git a/api/models/Video.js b/api/models/Video.js index 913e1f2..347d1eb 100644 --- a/api/models/Video.js +++ b/api/models/Video.js @@ -32,7 +32,6 @@ module.exports = { // 0 for self only, 1 for public privacy: { type: 'integer', - required: true, defaultsTo: 1 }, diff --git a/api/policies/sessionAuth.js b/api/policies/sessionAuth.js index d305168..fe3605b 100644 --- a/api/policies/sessionAuth.js +++ b/api/policies/sessionAuth.js @@ -11,7 +11,7 @@ module.exports = function(req, res, next) { // User is allowed, proceed to the next policy, // or if this is the last policy, the controller - if (req.session.authenticated) { + if (req.session.me) { return next(); } diff --git a/api/responses/backToHomePage.js b/api/responses/backToHomePage.js new file mode 100644 index 0000000..4187de5 --- /dev/null +++ b/api/responses/backToHomePage.js @@ -0,0 +1,22 @@ +/** + * Usage: + * res.backToHomePage(); // (default to 200 "OK" status code) + * res.backToHomePage(400); + * + */ + +module.exports = function backToHomePage (statusCode){ + + // Get access to `req` and `res` + // (since the arguments are up to us) + var req = this.req; + var res = this.res; + + // All done- either send back an empty response w/ just the status code + // (e.g. for AJAX requests) + if (req.wantsJSON) { + return res.send(statusCode||200); + } + // or redirect to the home page + return res.redirect('/'); +}; \ No newline at end of file diff --git a/api/responses/emailAddressInUse.js b/api/responses/emailAddressInUse.js new file mode 100644 index 0000000..d669817 --- /dev/null +++ b/api/responses/emailAddressInUse.js @@ -0,0 +1,17 @@ +/** + * 409 (Conflict) Handler + * + * Usage: + * res.emailAddressInUse(); + * + * @reference: https://github.com/irlnathan/activityoverlord20/blob/master/api/responses/emailAddressInUse.js + */ + +module.exports = function emailAddressInUse (){ + + // Get access to `res` + // (since the arguments are up to us) + var res = this.res; + + return res.send(409, 'Email address is already taken by another user.'); +}; \ No newline at end of file diff --git a/assets/images/zoomable_logo_black.svg b/assets/images/zoomable_logo_black.svg new file mode 100644 index 0000000..84b2fe8 --- /dev/null +++ b/assets/images/zoomable_logo_black.svg @@ -0,0 +1,79 @@ + + + + + + diff --git a/assets/index.html b/assets/index.html deleted file mode 100644 index 9d713b2..0000000 --- a/assets/index.html +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - Zoomable - - - - - - - - - - - - -
- - -
- - \ No newline at end of file diff --git a/assets/js/public/appRoutes.js b/assets/js/public/appRoutes.js deleted file mode 100644 index 27d98bc..0000000 --- a/assets/js/public/appRoutes.js +++ /dev/null @@ -1,23 +0,0 @@ -angular.module('appRoutes', []).config(['$stateProvider', '$urlRouterProvider', function($stateProvider, $urlRouterProvider) { - - // Redirect unmatched URL to login state - $urlRouterProvider.otherwise("/"); - - $stateProvider - .state('login', { - url: "/", - templateUrl: "views/login.html", - controller: 'loginController' - }) - .state('dashboard', { - url: "/dashboard", - templateUrl: "views/dashboard.html", - controller: 'dashboardController' - }) - .state('edit', { - url: "/edit/:videoId", - templateUrl: "views/edit.html", - controller: 'editController' - }); - -}]); diff --git a/assets/js/public/controllers/dashboardController.js b/assets/js/public/controllers/dashboardController.js index f4ab573..09015ed 100644 --- a/assets/js/public/controllers/dashboardController.js +++ b/assets/js/public/controllers/dashboardController.js @@ -1,10 +1,4 @@ -angular.module('zoomableApp').controller('dashboardController', function($scope, servicesAPI){ - - // MESSAGES - $scope.MESSAGE_MY_VIDEOS = 'My Videos'; - $scope.MESSAGE_VIEWS = 'Views'; - $scope.MESSAGE_COUNT_ZERO = '0'; - $scope.MESSAGE_ERROR_NO_VIDEO = 'No Video Yet'; +angular.module('zoomableApp').controller('dashboardController', function($scope, servicesAPI, $mdDialog, $mdMedia){ // VARIABLES $scope.defaultImagePath = 'images/bunny.png'; @@ -12,27 +6,20 @@ angular.module('zoomableApp').controller('dashboardController', function($scope, $scope.iconTagPath = 'images/ic_tag_black_24px.svg'; $scope.iconViewPath = 'images/ic_remove_red_eye_black_24px.svg'; $scope.filterStates = ['Newest','Popular','Public','Private']; - $scope.buttonStates = ['Private','Public','Delete']; + $scope.buttonStates = ['Public','Private','Delete']; $scope.userFilterState = ''; $scope.userButtonState = ''; + var PUBLIC = 0; + var PRIVATE = 1; + var DELETE = 2; + var videoData = {}; + var uploadUrl = '/upload'; $scope.model = { selectedVideoList : [] } - /* Get video object */ - servicesAPI.get() - .success(function(data) { - $scope.videoList = data; - }) - .error(function(data) { - console.log('Error: ' + data); - }); - - /* Button Handler */ - $scope.toggleButton = function(buttonId) { - $scope.userButtonState = buttonId; - }; + getVideoList(); /* Checkbox Handler */ $scope.isSelectAll = function() { @@ -60,4 +47,135 @@ angular.module('zoomableApp').controller('dashboardController', function($scope, } } + /* Dialog Handler */ + $scope.showConfirm = function(ev, buttonState) { + // Check if at least 1 video is checked + if($scope.model.selectedVideoList.length > 0) { + + $scope.userButtonState = buttonState; + var MESSAGE_VIDEO = 'videos'; + + // Check plural for confirm dialog text + if($scope.model.selectedVideoList.length === 1) { + MESSAGE_VIDEO = MESSAGE_VIDEO.substring(0, MESSAGE_VIDEO.length - 1); + } + + // DIALOGUE MESSAGES + var MESSAGE_TITLE_PRIVATE = 'Make Video Private?'; + var MESSAGE_TITLE_PUBLIC = 'Make Video Public?'; + var MESSAGE_TITLE_DELETE = 'Delete Video?'; + var MESSAGE_TEXT_CONTENT_PRIVATE = 'Are you sure you want to set ' + $scope.model.selectedVideoList.length + ' ' + MESSAGE_VIDEO + ' to Private?'; + var MESSAGE_TEXT_CONTENT_PUBLIC = 'Are you sure you want to set ' + $scope.model.selectedVideoList.length + ' ' + MESSAGE_VIDEO + ' to Public?'; + var MESSAGE_TEXT_CONTENT_DELETE = 'Are you sure you want to delete ' + $scope.model.selectedVideoList.length + ' ' + MESSAGE_VIDEO + '?'; + + var MESSAGE_TITLE = ''; + var MESSAGE_TEXT_CONTENT = ''; + + if(buttonState === $scope.buttonStates[PRIVATE]) { + MESSAGE_TITLE = MESSAGE_TITLE_PRIVATE; + MESSAGE_TEXT_CONTENT = MESSAGE_TEXT_CONTENT_PRIVATE; + } else if (buttonState === $scope.buttonStates[PUBLIC]) { + MESSAGE_TITLE = MESSAGE_TITLE_PUBLIC; + MESSAGE_TEXT_CONTENT = MESSAGE_TEXT_CONTENT_PUBLIC; + } else if (buttonState === $scope.buttonStates[DELETE]) { + MESSAGE_TITLE = MESSAGE_TITLE_DELETE; + MESSAGE_TEXT_CONTENT = MESSAGE_TEXT_CONTENT_DELETE; + } + + // Appending dialog to document.body to cover sidenav in docs app + var confirm = $mdDialog.confirm() + .title(MESSAGE_TITLE) + .textContent(MESSAGE_TEXT_CONTENT) + .ariaLabel('Confirm Dialog') + .targetEvent(ev) + .ok('Confirm') + .cancel('Cancel'); + $mdDialog.show(confirm).then(function() { + if(buttonState === $scope.buttonStates[PRIVATE]) { + for(var i=0;i<$scope.model.selectedVideoList.length;i++) { + servicesAPI.update($scope.model.selectedVideoList[i], {privacy: PRIVATE}).then(function() { + getVideoList(); + }); + } + } else if (buttonState === $scope.buttonStates[PUBLIC]) { + for(var i=0;i<$scope.model.selectedVideoList.length;i++) { + servicesAPI.update($scope.model.selectedVideoList[i], {privacy: PUBLIC}).then(function() { + getVideoList(); + }); + } + } else if (buttonState === $scope.buttonStates[DELETE]) { + for(var i=0;i<$scope.model.selectedVideoList.length;i++) { + servicesAPI.delete($scope.model.selectedVideoList[i]).then(function() { + getVideoList(); + }); + } + } + // Empty video list + $scope.model.selectedVideoList = []; + }); + + } else { + return; + } + }; + + /* Sort video list according to filter states */ + $scope.updateFilterState = function (state) { + $scope.userFilterState = state; + if ($scope.userFilterState === 'Newest') { + $scope.sortType = '-createdAt'; + } else if ($scope.userFilterState === 'Popular') { + $scope.sortType = '-views'; + } else if ($scope.userFilterState === 'Public') { + $scope.sortType = 'privacy'; + } else if ($scope.userFilterState === 'Private') { + $scope.sortType = '-privacy'; + } + }; + + /* GET Video Object */ + function getVideoList() { + servicesAPI.get() + .success(function(data) { + $scope.videoList = data; + }) + .error(function(data) { + console.log('Error: ' + data); + }); + } + + /* To display confirmation dialog */ + function DialogController($scope, $mdDialog) { + $scope.hide = function() { + $mdDialog.hide(); + }; + $scope.cancel = function() { + $mdDialog.cancel(); + }; + $scope.answer = function(answer) { + $mdDialog.hide(answer); + }; + } + + $scope.uploadVideoFile = function (filelist) { + for (var i = 0; i < filelist.length; ++i) { + var file = filelist.item(i); + + videoData = { + title : file.name, + videoDir : uploadUrl, + thumbnailDir : uploadUrl + }; + + servicesAPI.create(videoData) + .success(function(data) { + videoData = {}; + getVideoList(); + }) + .error(function(data) { + console.log('Error: ' + data); + }); + } + }; + }); diff --git a/assets/js/public/controllers/editController.js b/assets/js/public/controllers/editController.js index dc52a72..8a1c532 100644 --- a/assets/js/public/controllers/editController.js +++ b/assets/js/public/controllers/editController.js @@ -1,27 +1,81 @@ -angular.module('zoomableApp').controller('editController', function($scope, $stateParams, servicesAPI){ - +angular.module('zoomableApp').controller('editController', function($scope, $mdToast, $mdDialog, $state, servicesAPI){ // VARIABLES $scope.defaultImagePath = 'images/bunny.png'; - $scope.video_id = $stateParams.videoId; - - /* Get video object by video id */ - servicesAPI.getOne($scope.video_id) - .success(function(data) { - $scope.video = data; - }) - .error(function(data) { - console.log('Error: ' + data); - }); - - /* Copy embed link to system clipboard */ - $scope.copyEmbedLink = function(link) { - console.log(link); - // TO BE IMPLEMENTED + $scope.originalVideoTitle = ''; + $scope.video = {}; + $scope.tags = []; + + $scope.init = function() { + $scope.video = window.SAILS_LOCALS.video; + // prevent page header from changing when title is being edited + $scope.originalVideoTitle = $scope.video.title; } + /* Save changes made to video fields */ + /* Frontend checks ensure this ftn only called when there are changes & form is valid */ + $scope.saveChanges = function() { + // create object for editable fields + var updatedData = { + title: $scope.video.title, + description: $scope.video.description, + //tags: $scope.video.tags, + privacy: $scope.video.privacy + }; + + // update changes into database + servicesAPI.update($scope.video.id, updatedData) + .success(function(data) { + // update page header title with new title + $scope.originalVideoTitle = $scope.video.title; + + // show toast if changes saved successfully + var toast = $mdToast.simple() + .content('Changes Saved!') + .action('OK').highlightAction(true) + .hideDelay(1500) + .position('top right') + .parent(document.getElementById('toast-area')); + + $mdToast.show(toast); + + // set form back to clean state to disable save button + $scope.videoForm.$setPristine(); + }) + .error(function(data) { + console.log('Error: ' + data); + }); + }; + + /* Show dialog when user click cancel when changes made */ + $scope.showConfirm = function(ev) { + if ($scope.videoForm.$dirty) { + // Appending dialog to document.bod + var confirm = $mdDialog.confirm() + .title('Are you sure you want to leave this page?') + .textContent('You have unsaved changes. Your changes will not be saved if you leave this page.') + .ariaLabel('Confirm Navigation') + .targetEvent(ev) + .ok('Stay on this page') + .cancel('Leave this page'); + $mdDialog.show(confirm).then(function() { + // if user stay on page, do nothing + }, function() { + // if user leave page, redirect to dashboard page + window.location = '/'; + }); + } + else { + // just redirect to dashboard page if no changes made + window.location = '/'; + } + }; + /* Update video privacy field */ $scope.updatePrivacy = function(privacy) { $scope.video.privacy = privacy; + + // set form to dirty to enable save button + $scope.videoForm.$setDirty(); } }); diff --git a/assets/js/public/controllers/loginController.js b/assets/js/public/controllers/loginController.js index dde8d35..25ed33d 100644 --- a/assets/js/public/controllers/loginController.js +++ b/assets/js/public/controllers/loginController.js @@ -1,10 +1,66 @@ -angular.module('zoomableApp').controller('loginController', function($scope){ - $scope.user = true; // set to true for now +angular.module('zoomableApp').controller('loginController', function($scope, $state, servicesAPI){ + // VARIABLES + $scope.username = ''; + $scope.password = ''; + $scope.emailAddress = ''; + $scope.isCreate = false; + $scope.errorMsg = ''; - /* FOR NAVBAR */ - $scope.username = "USERNAME"; // TO BE EDITED WHEN LINK TO DB - $scope.profileItems = ['Settings', 'Log Out']; + // FUNCTIONS FOR LOGIN FORM + $scope.submitForm = function() { + if ($scope.isCreate) { + var accountData = { + username: $scope.username, + password: $scope.password, + email: $scope.emailAddress + } + + // create a new account for new user + servicesAPI.createAccount(accountData) + .success(function(data) { + // redirect to dashboard page + window.location = '/'; + }) + .error(function(data) { + console.log('Error: ' + data); + // add prompt if email is not unique + }); + } + else { + var accountData = { + username: $scope.username, + password: $scope.password + } + + // check if user entered correct username and password + servicesAPI.login(accountData) + .success(function(data) { + // redirect to dashboard page + window.location = '/'; + }) + .error(function(data) { + console.log('Error: ' + data); + // unsuccessful login, update error message and initialise password field + $scope.errorMsg = "Incorrect username/password. Please try again."; + $scope.password = ''; + }); + } + }; + + $scope.hasEmptyFields = function() { + if ($scope.isCreate) { + // enable submit only if all fields are entered + if ($scope.emailAddress && $scope.username && $scope.password) { + return false; + } + } + else { + // enable submit only if all fields are entered + if ($scope.username && $scope.password) { + return false; + } + } + return true; + } - /* FOR LOGIN */ - $scope.greeting = "Welcome to Zoomable"; }); diff --git a/assets/js/public/directives.js b/assets/js/public/directives.js index 85b1669..37bfb95 100644 --- a/assets/js/public/directives.js +++ b/assets/js/public/directives.js @@ -6,4 +6,25 @@ angular.module('zoomableApp') replace: true, templateUrl: "../../../views/navbar.html" } - }); + }) + .directive('dbinfOnFilesSelected', [function() { + return { + restrict: 'A', + scope: { + //attribute data-dbinf-on-files-selected (normalized to dbinfOnFilesSelected) identifies the action + //to take when file(s) are selected. The '&' says to execute the expression for attribute + //data-dbinf-on-files-selected in the context of the parent scope. Note though that this '&' + //concerns visibility of the properties and functions of the parent scope, it does not + //fire the parent scope's $digest (dirty checking): use $scope.$apply() to update views + //(calling scope.$apply() here in the directive had no visible effect for me). + dbinfOnFilesSelected: '&' + }, + link: function(scope, element, attr, ctrl) { + element.bind("change", function() + { //match the selected files to the name 'selectedFileList', and + //execute the code in the data-dbinf-on-files-selected attribute + scope.dbinfOnFilesSelected({selectedFileList : element[0].files}); + }); + } + } + }]); \ No newline at end of file diff --git a/assets/js/public/servicesAPI.js b/assets/js/public/servicesAPI.js index 07824f8..d4448f1 100644 --- a/assets/js/public/servicesAPI.js +++ b/assets/js/public/servicesAPI.js @@ -11,6 +11,15 @@ angular.module('zoomableApp').factory('servicesAPI', function($http) { }, delete : function(id) { return $http.delete('/api/video/' + id); + }, + update : function(id, videoData) { + return $http.put('/api/video/' + id, videoData); + }, + createAccount : function(accountData) { + return $http.post('/api/user/signup', accountData); + }, + login : function(accountData) { + return $http.post('/api/user/login', accountData); } } }); diff --git a/assets/js/public/zoomableApp.js b/assets/js/public/zoomableApp.js index 310496c..44dc222 100644 --- a/assets/js/public/zoomableApp.js +++ b/assets/js/public/zoomableApp.js @@ -1,4 +1,4 @@ -angular.module('zoomableApp', ['ui.router', 'appRoutes', 'ngMaterial']) +angular.module('zoomableApp', ['ui.router', 'ngMaterial', 'ngMessages', 'ngclipboard']) // Define standard theme for dashboard UI .config(function($mdThemingProvider) { $mdThemingProvider.theme('default') diff --git a/assets/styles/custom/dashboard.less b/assets/styles/custom/dashboard.less index 8c80e2b..86af0ff 100644 --- a/assets/styles/custom/dashboard.less +++ b/assets/styles/custom/dashboard.less @@ -3,6 +3,14 @@ */ .dashboard { + .action-btns-left { + margin-top: 8px; + } + + .action-btns-right { + margin-right: 15px; + } + img { height: @dashboard-video-height; width: @dashboard-video-width; diff --git a/assets/styles/custom/edit.less b/assets/styles/custom/edit.less index 387be6c..2735d4d 100644 --- a/assets/styles/custom/edit.less +++ b/assets/styles/custom/edit.less @@ -3,7 +3,40 @@ */ #edit { - a.md-button.cancel-btn, + .input-chips { + margin-top: 10px; + + .md-chips { + padding-bottom: 3px; + + /* do not group these rules */ + *::-webkit-input-placeholder { + color: fade(@color-black, 26%); + } + *:-moz-placeholder { + /* FF 4-18 */ + color: fade(@color-black, 26%); + } + *::-moz-placeholder { + /* FF 19+ */ + color: fade(@color-black, 26%); + } + *:-ms-input-placeholder { + /* IE 10+ */ + color: fade(@color-black, 26%); + } + + &.md-focused { + box-shadow: 0 1px @color-primary; + } + } + + .md-chip.md-focused { + background-color: @color-primary; + } + } + + button.md-button.cancel-btn, button.md-button.save-btn { margin: 0px 0px 0px 10px; } @@ -61,6 +94,10 @@ padding: 15px 5px 0px 5px; } + .md-chips { + padding-bottom: 5px; + } + md-icon.tab-icon { width: 20px; opacity: 0.54; @@ -83,8 +120,18 @@ vertical-align: middle; } + #toast-area { + display: block; + height: 70px; + width: 100%; + z-index: 90; + + button { + color: @color-white; + } + } + .video-info { - margin-top: 60px; font-size: 13px; button.md-button.md-icon-button { diff --git a/assets/styles/custom/elements.less b/assets/styles/custom/elements.less index 325d3ca..cee55b0 100644 --- a/assets/styles/custom/elements.less +++ b/assets/styles/custom/elements.less @@ -2,8 +2,13 @@ * elements.less */ +body { + min-width: 800px; +} + md-card { display: block; // override default card styling to inherit height of children + padding: 0px !important; } md-card-header { // override default card header styling @@ -13,6 +18,14 @@ md-card-header { // override default card header styling md-content.layout-padding { top: @navbar-height; + padding: 16px; +} + +md-menu-content { + a { + color: @color-black !important; + text-decoration: none !important; + } } .page-header { diff --git a/assets/styles/custom/login.less b/assets/styles/custom/login.less new file mode 100644 index 0000000..4794eb2 --- /dev/null +++ b/assets/styles/custom/login.less @@ -0,0 +1,86 @@ +/** + * login.less + */ + +#login { + md-card { + background-color: @color-primary; + margin: 10px auto; + width: 550px; + + .card-contents { + padding: 50px 0px 30px 0px; + } + + .description { + color: fade(@color-black, 87%); + font-size: 20px; + text-align: center; + padding: 20px 60px 40px 60px; + } + + .field-area { + margin: 10px 60px; + text-align: center; + + .create-account-link, + .forgot-password-link { + display: block; + font-size: 18px; + } + + .create-account-link { + text-align: center; + padding: 15px 0px 5px 0px; + } + + .forgot-password-link { + text-align: right; + } + + .error-message { + color: #BF360C; + font-size: 16px; + text-align: center; + } + + input, + button { + border: none; + border-radius: 3px; + height: 55px; + margin-bottom: 15px; + outline: none; + padding: 0px 20px; + width: 100%; + } + + input { + background-color: fade(@color-white, 95%); + font-size: 19px; + } + + button { + background-color: fade(@color-button-primary, 95%); + color: @color-white; + font-size: 22px; + font-weight: bold; + margin: 50px 0px 0px 0px; + text-transform: uppercase; + + &:disabled { + background-color: fade(@color-button-primary, 50%); + color: fade(@color-white, 50%); + } + } + } + + .logo { + background: url(../../images/zoomable_logo_black.svg) no-repeat; + background-position: center; + height: 60px; + margin: auto; + width: 300px; + } + } +} diff --git a/assets/styles/importer.less b/assets/styles/importer.less index d7bfa72..0a30f78 100644 --- a/assets/styles/importer.less +++ b/assets/styles/importer.less @@ -33,3 +33,4 @@ @import 'custom/elements.less'; @import 'custom/dashboard.less'; @import 'custom/edit.less'; +@import 'custom/login.less'; diff --git a/assets/views/dashboard.html b/assets/views/dashboard.html deleted file mode 100644 index e1d0fe9..0000000 --- a/assets/views/dashboard.html +++ /dev/null @@ -1,84 +0,0 @@ -
- - -
- - - - - - - -
- - - - {{ buttonState }} - - -
- -
- - - - {{ filterState }} - - -
-
-
- - - - - - - - - - - {{ video.title }} - - -
-

{{ video.title }}

-

- {{ video.createdAt | date:'medium'}} - - - -

-

- - - {{ video.tags }} - -

-
- -
- - {{ video.views }} {{ MESSAGE_VIEWS }} -
-
- - - {{ MESSAGE_ERROR_NO_VIDEO }} - -
- -
-
- -
-
\ No newline at end of file diff --git a/assets/views/edit.html b/assets/views/edit.html deleted file mode 100644 index 317672e..0000000 --- a/assets/views/edit.html +++ /dev/null @@ -1,138 +0,0 @@ -
- - -
- - - - - - - - - - - INFO & SETTINGS - - - -
-
- - {{ video.title }} -
-
-
-
-
- Save - Cancel -
-
-
-

Video Information

-

- Time Uploaded: - {{video.createdAt | date:'MMMM d, y h:mma'}} -

-

- Video Duration: - ??? -

- -
-
-
-
-
-
- - - -
-
This is required.
-
-
- - - - -
-
- - - - -
-
-
-
-
- -
-
-
-
- Public -
-
- Private -
-
-
-
-
-
-
-
-
-
-
-
- - - - - - SUBTITLES & CC - - - -

Subtitles Here

-

TO BE IMPLEMENTED.

-
-
-
- - - - - - STATISTICS - - - -

Statistics Here

-

TO BE IMPLEMENTED.

-
-
-
- -
-
-
- -
-
diff --git a/assets/views/login.html b/assets/views/login.html deleted file mode 100644 index 13978df..0000000 --- a/assets/views/login.html +++ /dev/null @@ -1,13 +0,0 @@ -
- - - -
- {{greeting}} -
-
- Log In -
-
- -
diff --git a/bower.json b/bower.json index ed8a780..b6cbc72 100644 --- a/bower.json +++ b/bower.json @@ -23,6 +23,8 @@ "jasmine-jquery": "~2.1.1", "angular": "~1.4.9", "angular-ui-router": "~0.2.16", - "angular-material": "~1.0.3" + "angular-material": "~1.0.3", + "ngclipboard": "^1.1.0", + "clipboard": "^1.5.8" } } diff --git a/config/connections.js b/config/connections.js index 656046d..64ff7ee 100644 --- a/config/connections.js +++ b/config/connections.js @@ -80,7 +80,7 @@ module.exports.connections = { user: 'YOUR_POSTGRES_USER', password: 'YOUR_POSTGRES_PASSWORD', database: 'YOUR_POSTGRES_DB' - } + }, /*************************************************************************** @@ -88,5 +88,8 @@ module.exports.connections = { * More adapters: https://github.com/balderdashy/sails * * * ***************************************************************************/ - + test: { + adapter: 'sails-memory' + } + }; diff --git a/config/policies.js b/config/policies.js index 460cc28..de54634 100644 --- a/config/policies.js +++ b/config/policies.js @@ -28,24 +28,10 @@ module.exports.policies = { // '*': true, - /*************************************************************************** - * * - * Here's an example of mapping some policies to run before a controller * - * and its actions * - * * - ***************************************************************************/ - // RabbitController: { - - // Apply the `false` policy as the default for all of RabbitController's actions - // (`false` prevents all access, which ensures that nothing bad happens to our rabbits) - // '*': false, - - // For the action `nurture`, apply the 'isRabbitMother' policy - // (this overrides `false` above) - // nurture : 'isRabbitMother', + VideoController: { - // Apply the `isNiceToAnimals` AND `hasRabbitFood` policies - // before letting any users feed our rabbits - // feed : ['isNiceToAnimals', 'hasRabbitFood'] - // } + // Apply the `false` policy as the default for all of VideoController's actions + // (`false` prevents all access) + '*': 'sessionAuth', + } }; diff --git a/config/routes.js b/config/routes.js index 1b1a277..6a8e924 100644 --- a/config/routes.js +++ b/config/routes.js @@ -32,11 +32,14 @@ module.exports.routes = { * * ***************************************************************************/ - /* Added 'index.html' as view. Routing now done via appRoutes.js - '/': { - view: 'homepage' - } - */ + // '/': { + // view: 'homepage' + // } + + // server rendered webpages + 'GET /': 'PageController.showHomePage', + 'GET /edit/:id': 'PageController.showEditPage', + /*************************************************************************** * * diff --git a/config/views.js b/config/views.js index aec51ea..1675c90 100644 --- a/config/views.js +++ b/config/views.js @@ -30,9 +30,9 @@ module.exports.views = { * * ****************************************************************************/ - /* Remove ejs as engine, using HTML instead + // Remove ejs as engine, using Jade instead engine: 'ejs', - */ + /**************************************************************************** * * @@ -76,7 +76,7 @@ module.exports.views = { * * ****************************************************************************/ - layout: false, + layout: 'layout', /**************************************************************************** * * diff --git a/package.json b/package.json index c2ee589..a7d98da 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,8 @@ }, "scripts": { "start": "node app.js", - "debug": "node debug app.js" + "debug": "node debug app.js", + "test": "node ./node_modules/mocha/bin/mocha test/bootstrap.test.js test/integration/**/*.test.js" }, "main": "app.js", "repository": { @@ -37,6 +38,12 @@ "author": "Meteoria", "license": "MIT", "devDependencies": { - "grunt-contrib-jasmine": "^0.9.2" + "barrels": "^1.6.4", + "grunt-contrib-jasmine": "^0.9.2", + "grunt-mocha-test": "^0.12.7", + "mocha": "^2.4.5", + "sails-memory": "^0.10.6", + "should": "^8.2.2", + "supertest": "^1.2.0" } } diff --git a/player/assets/javascripts/player.js b/player/assets/javascripts/player.js new file mode 100644 index 0000000..93f9586 --- /dev/null +++ b/player/assets/javascripts/player.js @@ -0,0 +1,497 @@ + +var Player = function(vid,canv) { + + this.video = vid; + this.canvas = canv; + this.ctx = canv.getContext('2d'); + this.scaleFactor = 1.1; + this.zoomFactor = 1; + this.dimensions = { cw:canvas.width, ch:canvas.height }; + this.last; + this.dragStart; + this.dragged; + this.mouseactions; + this.scroll; + this.controls; + this.volume; + this.seek; + this.zoom; + this.transforms; + this.util; + + this.init = function() { + this.scroll = new Scroll(this); + this.volume = new Volume(this); + this.zoom = new Zoom(this); + this.controls = new Controls(this); + this.transforms = new Transforms(this); + this.seek = new Seek(this); + this.transforms = new Transforms(this); + this.util = new Util(this); + this.transforms.redraw(); + this.last = { x: canvas.width/2, y: canvas.height/2 }; + this.volume.setVolume(0.5); //set default vol of video + this.mouseactions = new MouseActions(this); + }; + + var MouseActions = function(player) { + player.canvas.addEventListener('mousedown',function(event) { player.mouseactions.mouseDown(event); },false); + player.canvas.addEventListener('mousemove',function(event) { player.mouseactions.mouseMove(event); },false); + player.canvas.addEventListener('mouseup',function(event) { player.mouseactions.mouseUp(event); },false); + + + this.mouseDown = function(evt){ + document.body.style.mozUserSelect = + document.body.style.webkitUserSelect = + document.body.style.userSelect = 'none'; + player.last.x = evt.offsetX || (evt.pageX - player.canvas.offsetLeft); + player.last.y = evt.offsetY || (evt.pageY - player.canvas.offsetTop); + player.dragStart = player.ctx.transformedPoint(player.last.x,player.last.y); + player.dragged = false; + }; + this.mouseMove = function(evt){ + player.last.x = evt.offsetX || (evt.pageX - player.canvas.offsetLeft); + player.last.y = evt.offsetY || (evt.pageY - player.canvas.offsetTop); + player.dragged = true; + if (player.dragStart){ + player.transforms.outerTranslate(); + } + }; + this.mouseUp = function(evt){ + player.dragStart = null; + } + }; + + var Scroll = function(player) { + canvas.addEventListener('DOMMouseScroll',function(event) { player.scroll.handle(event); },false); + canvas.addEventListener('mousewheel',function(event) { player.scroll.handle(event); },false); + + this.handle = function(evt){ + var delta = evt.wheelDelta ? evt.wheelDelta/40 : evt.detail ? -evt.detail : 0; + if (delta) { + //updateSliderUI(zoomCtrl); + player.zoom.zoom(delta, player.last.x, player.last.y); + player.controls.updateZoomUI(); + } + return evt.preventDefault() && false; + } + }; + + var Controls = function(player) { + + /* binds to UI and adds listeners for controls*/ + this.playPauseBtn = document.getElementById('playPauseBtn'); + this.uiControls = document.getElementById('uiControls'); + this.currentTimeTxt = document.getElementById('currentTimeTxt'); + this.totalTimeTxt = document.getElementById('totalTimeTxt'); + this.seekCtrl = document.getElementById('seekCtrl'); + this.volumeBtn = document.getElementById('volumeBtn'); + this.volumeCtrl = document.getElementById('volumeCtrl'); + this.zoomOutBtn = document.getElementById('zoomOutBtn'); + this.zoomCtrl = document.getElementById('zoomCtrl'); + this.zoomInBtn = document.getElementById('zoomInBtn'); + + player.video.addEventListener('loadedmetadata',function(){ + player.controls.getVideoLength() + },false); + this.playPauseBtn.addEventListener('click',function(){ + player.controls.playPauseVideo(this.playPauseVideo); + },false); + player.video.addEventListener('pause',function(){ + player.controls.changeToPauseState(this.playPauseBtn, this.uiControls); + },false); + player.video.addEventListener('play',function(){ + player.controls.changeToPlayState(this.playPauseBtn, this.uiControls); + },false); + this.volumeBtn.addEventListener('click',function(){ + player.volume.toggleMuteState(event); + player.controls.updateSliderUI(player.controls.volumeCtrl); + },false); + this.volumeCtrl.addEventListener('change',function(){ + player.volume.volumeAdjust(); + player.controls.updateSliderUI(player.controls.volumeCtrl); + },false); + player.video.addEventListener('volumechange',function(){ + player.controls.updateSliderUI(player.controls.volumeCtrl); + },false); + this.volumeCtrl.addEventListener('mousemove',function(){ + player.controls.updateSliderUI(player.controls.volumeCtrl); + },false); + this.zoomInBtn.addEventListener('click',function(){ + player.zoom.in(); + },false); + this.zoomOutBtn.addEventListener('click',function(){ + player.zoom.out(); + },false); + this.zoomCtrl.addEventListener('change',function(){ + player.zoom.adjust(); + },false); + this.zoomCtrl.addEventListener('mousemove',function(){ + player.controls.updateSliderUI(player.controls.zoomCtrl); + },false); + + /* Play or pause the video */ + this.playPauseVideo = function() { + if(player.video.paused) { + player.video.play(); + } + else { + player.video.pause(); + } + } + + /* Updates icon to "play" button during pause state, show UI controls bar */ + this.changeToPauseState = function() { + this.playPauseBtn.className = 'play'; + this.uiControls.className = ''; + } + + /* Updates icon to "pause" button during play state, hide UI controls bar */ + this.changeToPlayState = function() { + this.playPauseBtn.className = 'pause'; + this.uiControls.className = 'hideOnHover'; + } + /* Retrieve total duration of video and update total time text */ + this.getVideoLength = function() { + var convertedTotalTime = player.util.convertSecondsToHMS(player.video.duration); + this.totalTimeTxt.innerHTML = convertedTotalTime; + }; + + /* Convert and update current time text */ + this.updateCurrentTimeText = function(time) { + var convertedTime = player.util.convertSecondsToHMS(time); + this.currentTimeTxt.innerHTML = convertedTime; + }; + + /* Update zoom control UI */ + this.updateZoomUI = function() { + this.zoomCtrl.value = player.util.convertScaleToPercent(player.transforms.xform.a); + this.updateSliderUI(this.zoomCtrl); + }; + + /* Update slider color when slider value changes - for zoomCtrl/volumeCtrl */ + this.updateSliderUI = function(element) { + var gradient = ['to right']; + gradient.push('#ccc ' + (element.value * 100) + '%'); + gradient.push('rgba(255, 255, 255, 0.3) ' + (element.value * 100) + '%'); + gradient.push('rgba(255, 255, 255, 0.3) 100%'); + element.style.background = 'linear-gradient(' + gradient.join(',') + ')'; + }; + + }; + + var Volume = function(player){ + this.previousVolume = { + state: 'low', + value: player.video.volume + }; + this.setVolume = function(val) { + player.video.volume = val; + }; + this.volumeAdjust = function() { + player.video.volume = player.controls.volumeCtrl.value; + if (player.video.volume > 0) { + player.video.muted = false; + if (player.video.volume > 0.5) player.controls.volumeBtn.className = 'high'; + else player.controls.volumeBtn.className = 'low'; + } else { + player.video.muted = true; + player.controls.volumeBtn.className = 'off'; + } + // update previous state at the end so mute can be toggled correctly + player.volume.previousVolume.value = player.video.volume; + player.volume.previousVolume.state = player.volumeBtn.className; + }; + + this.toggleMuteState = function(evt) { + // temporary variables to store current volume values + var currentVolumeState = evt.target.className; + var currentVolumeControlValue = player.video.volume; + + if (currentVolumeState == 'low' || currentVolumeState == 'high') { + evt.target.className = 'off'; + player.video.muted = true; + player.controls.volumeCtrl.value = 0; + player.video.volume = 0; + } + else { + evt.target.className = this.previousVolume.state; + player.video.muted = false; + player.controls.volumeCtrl.value = this.previousVolume.value; + player.video.volume = this.previousVolume.value; + } + + // update previous state + this.previousVolume.state = currentVolumeState; + this.previousVolume.value = currentVolumeControlValue; + } + }; + + var Seek = function(player){ + /* Update seek control value and current time text */ + player.video.addEventListener('timeupdate',function() { player.seek.updateSeekTime(); },false); + player.controls.seekCtrl.addEventListener('change',function() { player.seek.setVideoTime(); },false); + + this.updateSeekTime = function(){ + var newTime = player.video.currentTime/player.video.duration; + var gradient = ['to right']; + var buffered = player.video.buffered; + player.controls.seekCtrl.value = newTime; + if (buffered.length == 0) { + gradient.push('rgba(255, 255, 255, 0.1) 0%'); + } else { + // NOTE: the fallback to zero eliminates NaN. + var bufferStartFraction = (buffered.start(0) / player.video.duration) || 0; + var bufferEndFraction = (buffered.end(0) / player.video.duration) || 0; + var playheadFraction = (player.video.currentTime / player.video.duration) || 0; + gradient.push('rgba(255, 255, 255, 0.1) ' + (bufferStartFraction * 100) + '%'); + gradient.push('rgba(255, 255, 255, 0.7) ' + (bufferStartFraction * 100) + '%'); + gradient.push('rgba(255, 255, 255, 0.7) ' + (playheadFraction * 100) + '%'); + gradient.push('rgba(255, 255, 255, 0.4) ' + (playheadFraction * 100) + '%'); + gradient.push('rgba(255, 255, 255, 0.4) ' + (bufferEndFraction * 100) + '%'); + gradient.push('rgba(255, 255, 255, 0.1) ' + (bufferEndFraction * 100) + '%'); + } + player.controls.seekCtrl.style.background = 'linear-gradient(' + gradient.join(',') + ')'; + + player.controls.updateCurrentTimeText(player.video.currentTime); + }; + /* Change current video time and text according to seek control value */ + this.setVideoTime = function(){ + var seekTo = player.video.duration * player.controls.seekCtrl.value; + player.video.currentTime = seekTo; + player.controls.updateCurrentTimeText(player.video.currentTime); + }; + + }; + + var Zoom = function(player) { + this.maxZoom = 7; + + /* Zooms into the position x, y with the amount clicks */ + this.zoom = function(clicks, x, y){ + //tt(ctx); + var pt = player.ctx.transformedPoint(x, y); + var factor = Math.pow(player.scaleFactor,clicks); + var tx = player.transforms.xform.e; + var ty = player.transforms.xform.f; + var s = player.transforms.xform.a; + if (factor*s >= 1 && factor*s <= this.maxZoom) { + player.transforms.translate(pt.x,pt.y); + player.transforms.scale(factor,factor); + player.transforms.translate(-pt.x,-pt.y); + player.controls.zoomCtrl.value = player.util.convertScaleToPercent(player.transforms.xform.a); + player.transforms.refit(); + } + player.transforms.redraw(); + } + + /* Private function to call zoom(clicks,x,y) from the UI Controls. */ + function zoomHelper(value) { + var tx = player.transforms.xform.e; + var ty = player.transforms.xform.f; + var old_s = player.transforms.xform.a; + var x = player.dimensions.cw/2; + var y = player.dimensions.ch/2; + player.zoom.zoom(value, x, y); + player.controls.updateZoomUI(); + } + /* Adjust zoom by adjusting the slider */ + this.adjust = function() { + var zoomPercent = player.controls.zoomCtrl.value; + var new_s = player.util.convertPercentToScale(zoomPercent); + var old_s = player.transforms.xform.a; + var delta_clicks = Math.log(new_s/old_s) / Math.log(scaleFactor); + zoomHelper(delta_clicks); + } + + /* Adjust zoom by clicking zoom in and out buttons */ + this.in = function() { + zoomHelper(1); + } + this.out = function() { + zoomHelper(-1); + } + } + + var Transforms = function(player) { + player.video.addEventListener('play', function(){ + player.transforms.draw(); + },false); + + var svg = document.createElementNS("http://www.w3.org/2000/svg",'svg'); + this.savedTransforms = []; + this.xform = svg.createSVGMatrix(); + + var save = player.ctx.save; + player.ctx.save = function(){ + this.savedTransforms.push(this.xform.translate(0,0)); + return save.call(player.ctx); + }; + + this.restore = function(){ + var restore = player.ctx.restore; + this.xform = savedTransforms.pop(); + return restore.call(player.ctx); + }; + + this.scale = function(sx,sy){ + var scale = player.ctx.scale; + this.xform = this.xform.scaleNonUniform(sx,sy); + return scale.call(player.ctx,sx,sy); + }; + + this.rotate = function(radians){ + var rotate = player.ctx.rotate; + this.xform = this.xform.rotate(radians*180/Math.PI); + return rotate.call(player.ctx,radians); + }; + + this.translate = function(dx,dy){ + var translate = player.ctx.translate; + this.xform = this.xform.translate(dx,dy); + return translate.call(player.ctx,dx,dy); + }; + + this.transform = function(a,b,c,d,e,f){ + var transform = player.ctx.transform; + var m2 = svg.createSVGMatrix(); + m2.a=a; m2.b=b; m2.c=c; m2.d=d; m2.e=e; m2.f=f; + this.xform = this.xform.multiply(m2); + return transform.call(player.ctx,a,b,c,d,e,f); + }; + + this.setTransform = function(a,b,c,d,e,f){ + var setTransform = player.ctx.setTransform; + this.xform.a = a; + this.xform.b = b; + this.xform.c = c; + this.xform.d = d; + this.xform.e = e; + this.xform.f = f; + return setTransform.call(player.ctx,a,b,c,d,e,f); + }; + + var pt = svg.createSVGPoint(); + player.ctx.transformedPoint = function(x,y){ + pt.x=x; pt.y=y; + return pt.matrixTransform(player.transforms.xform.inverse()); + } + + /* Checks if the viewport borders intersect with the canvas borders + ** If it intersects, then scale/translate back the canvas accordingly to fit the viewport.*/ + this.refit = function() { + var tx = player.transforms.xform.e; + var ty = player.transforms.xform.f; + var s = player.transforms.xform.a; + if (s < 1 || s > player.zoom.maxZoom) { + this.scale(1/s, 1/s); + } + if (tx > 0 ) { + this.translate(-tx/s,0); + } + if (ty > 0) { + this.translate(0,-ty/s); + } + if (tx+player.dimensions.cw*s < player.dimensions.cw) { + var dx = (player.dimensions.cw - tx-player.dimensions.cw*s)/s; + this.translate(dx, 0); + } + if (ty+player.dimensions.ch*s < player.dimensions.ch) { + var dy = (player.dimensions.ch - ty-player.dimensions.ch*s)/s; + this.translate(0, dy); + } + } + + this.draw = function() { + //if(v.paused || v.ended) return false; + player.ctx.drawImage(player.video,0,0,player.dimensions.cw,player.dimensions.ch); + setTimeout(player.transforms.draw,20); + } + + this.redraw = function(){ + // Clear the entire canvas + var p1 = player.ctx.transformedPoint(0,0); + var p2 = player.ctx.transformedPoint(player.dimensions.cw,player.dimensions.ch); + //ctx.clearRect(p1.x,p1.y,p2.x-p1.x,p2.y-p1.y); + player.ctx.fillStyle = 'rgb(0,0,0)'; + player.ctx.fillRect(p1.x,p1.y,p2.x-p1.x,p2.y-p1.y); + // Alternatively: + // ctx.save(); + // ctx.setTransform(1,0,0,1,0,0); + // ctx.clearRect(0,0,canvas.width,canvas.height); + // ctx.restore(); + player.transforms.refit(); + this.draw(); + } + + this.outerTranslate = function() { + var pt = player.ctx.transformedPoint(player.last.x,player.last.y); + var dx = pt.x-player.dragStart.x; + var dy = pt.y-player.dragStart.y; + var tx = player.transforms.xform.e; + var ty = player.transforms.xform.f; + var flag = 0; + var s = player.transforms.xform.a; + + if (tx+dx <= 0 && tx+player.dimensions.cw*s+dx > player.dimensions.cw) { + this.translate(dx,0); + flag = 1; + } + if (ty+dy <= 0 && ty+player.dimensions.ch*s+dy > player.dimensions.ch) { + this.translate(0,dy); + flag = 1; + } + /* if (flag = 0) { + ctx.translate(pt.x-dragStart.x,pt.y-dragStart.y); + }*/ + this.redraw(); + } + } + + var Util = function(player) { + /* Helper methods to convert between the slider values and transformation matrix values */ + this.convertPercentToScale = function(percent) { + var range = player.zoom.maxZoom - 1; + return percent*range + 1; + } + this.convertScaleToPercent = function(scale) { + var range = player.zoom.maxZoom - 1; + return (scale-1)/range; + } + /* Function to converts seconds to HH:MM:SS format */ + this.convertSecondsToHMS = function(timeInSeconds) { + var formattedTime = ''; + var hours = Math.floor(timeInSeconds / 3600); + var mins = Math.floor((timeInSeconds / 60) % 60); + var secs = Math.floor(timeInSeconds % 60); + + if (secs < 10) + secs = '0' + secs; + if (mins < 10) + mins = '0' + mins; + + formattedTime = hours+':'+mins+':'+secs; + + return formattedTime; + } + } +} + +document.addEventListener('DOMContentLoaded', function() { + var zoomable = new Player(document.getElementById('video'), document.getElementById('canvas')); + zoomable.init(); +}, false); + +/* +var vidCount = 1; +document.addEventListener('DOMContentLoaded', function() { + // To loop through the rows while we are on a column + for(var rowNum = 0; rowNum < 3; rowNum++) { + // To loop through the columns while we are on a row + for(var colNum = 0; colNum < 4; colNum++) { + var zoomable = new Player(document.getElementById('video_' + vidCount), document.getElementById('canvas'), colNum*160, rowNum*120); + zoomable.init() + vidCount++; + } + } +}, false);*/ \ No newline at end of file diff --git a/player/player.html b/player/player.html index 8bbb788..0cce533 100644 --- a/player/player.html +++ b/player/player.html @@ -6,23 +6,25 @@ + - - + + - @@ -33,9 +35,98 @@ width="640" height="360" crossorigin="anonymous" controls - style="display:none"> +> + Your browser does not support HTML5 video. + + +
diff --git a/tasks/config/copy.js b/tasks/config/copy.js index f6669e5..a22cbca 100644 --- a/tasks/config/copy.js +++ b/tasks/config/copy.js @@ -33,7 +33,8 @@ module.exports = function(grunt) { cwd: './libs', src: ['angular/angular.js', 'angular-ui-router/release/angular-ui-router.js', 'angular-aria/angular-aria.js', 'angular-animate/angular-animate.js', - 'angular-material/angular-material.js'], + 'angular-material/angular-material.js', 'angular-messages/angular-messages.js', + 'clipboard/dist/clipboard.js', 'ngclipboard/dist/ngclipboard.js'], flatten: true, dest: '.tmp/public/js/dependencies' }] diff --git a/tasks/config/jasmine.js b/tasks/config/jasmine.js index 1a2adb9..445ad3d 100644 --- a/tasks/config/jasmine.js +++ b/tasks/config/jasmine.js @@ -1,5 +1,5 @@ /** - * Compress CSS files. + * To run jasmine test (Not working currently due to phantomJS not support html5
Video Source: - + +