pyc-website

main website for pyc inc.

git clone https://9o.is/git/pyc-website.git

commit 8f529e2b7c10f32b98913393cdf80f444d624313
parent 84befeeec80b60bcc496badeb0c65acf9c526105
Author: Jul <jul@9o.is>
Date:   Thu,  7 Aug 2014 05:54:32 -0700

front page video. needs a lot more compatibility testing

Diffstat:
Mbuild.config.js | 2++
Msrc/main/webapp/app/App.js | 114++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Msrc/main/webapp/index.html | 64++++++++++++++++++++++++++++++++++++++++++++++++++++------------
Asrc/main/webapp/less/other/videogular.less | 521+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/main/webapp/less/overrides.less | 10+++++++++-
Msrc/main/webapp/less/pages/index.less | 121++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------
Msrc/main/webapp/less/styles.less | 1+
Asrc/main/webapp/vendor/videogular-controls.js | 560+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/main/webapp/vendor/videogular.js | 754+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
9 files changed, 2111 insertions(+), 36 deletions(-)

diff --git a/build.config.js b/build.config.js @@ -63,6 +63,8 @@ module.exports = { "<%= dirs.vendor %>/angular-google-maps.min.js", "<%= dirs.vendor %>/ui-mask.js", "<%= dirs.vendor %>/liftAjax.js", + "<%= dirs.vendor %>/videogular.js", + "<%= dirs.vendor %>/videogular-controls.js", "<%= dirs.vendor %>/angular-file-upload.js", "<%= dirs.vendor %>/angular-google-analytics.js", "<%= dirs.vendor %>/angular-wizard.js", diff --git a/src/main/webapp/app/App.js b/src/main/webapp/app/App.js @@ -2,7 +2,9 @@ var app = angular.module("app", [ 'google-maps', 'ui.bootstrap', 'ui.router', - 'ui.mask', + 'ui.mask', + 'com.2fdevs.videogular', + 'com.2fdevs.videogular.plugins.controls', 'angularFileUpload', 'angular-google-analytics', 'ngThumb', @@ -22,6 +24,116 @@ var screen_xs = 480; var screen_md = 992; +app.controller('IntroVideoCtrl', ['$interval', '$timeout', '$scope', '$sce', '$rootScope', 'VG_EVENTS', function($interval, $timeout, $scope, $sce, $rootScope, VG_EVENTS) { + + $scope.playing = false; + + var video = document.getElementById("bgvid"); + + video.addEventListener('loadedmetadata', function() { + this.currentTime = 64; + this.muted = true; + this.playbackRate=0.75; + this.play(); + }, false); + + var i = 0; + var clips = [28,13.5,35,51,64]; + + $interval(function(){ + video.currentTime = clips[i % clips.length]; + i++; + }, 4000); + + var playbtn = document.getElementById("playbtn"); + + var flip = function() { + playbtn.className = "flip"; + playbtn.className = "flipback"; + + $timeout(function(){ + playbtn.className = ""; + }, 2500); + }; + + $timeout(function(){flip();}, 5000); + $interval(function(){flip();}, 60000); + + $scope.play = function() { + $scope.playing = true; + $scope.API.toggleFullScreen(); + $scope.API.play(); + $scope.$apply(); + }; + + $scope.pause = function() { + if($scope.playing) { + $scope.playing = false; + $scope.API.pause(); + $scope.$apply(); + } + }; + + + $scope.currentTime = 0; + $scope.totalTime = 0; + $scope.state = null; + $scope.volume = 1; + $scope.isCompleted = false; + $scope.API = null; + + $scope.onPlayerReady = function(API) { + $scope.API = API; + $rootScope.$on(VG_EVENTS.ON_EXIT_FULLSCREEN, $scope.pause); + }; + + $scope.onCompleteVideo = function() { + $scope.isCompleted = true; + $scope.playing = false; + }; + + $scope.onUpdateState = function(state) { + $scope.state = state; + }; + + $scope.onUpdateTime = function(currentTime, totalTime) { + $scope.currentTime = currentTime; + $scope.totalTime = totalTime; + }; + + $scope.onUpdateVolume = function(newVol) { + $scope.volume = newVol; + }; + + $scope.onUpdateSize = function(width, height) { + $scope.config.width = width; + $scope.config.height = height; + }; + + $scope.stretchModes = [ + {label: "None", value: "none"}, + {label: "Fit", value: "fit"}, + {label: "Fill", value: "fill"} + ]; + + $scope.config = { + width: 740, + height: 380, + autoHide: true, + autoHideTime: 3000, + autoPlay: false, + responsive: true, + stretch: $scope.stretchModes[1], + sources: [ + {src: $sce.trustAsResourceUrl("https://s3.amazonaws.com/assets-pyc/placeholder.mp4"), type: "video/mp4"}, + {src: $sce.trustAsResourceUrl("https://s3.amazonaws.com/assets-pyc/placeholder.webm"), type: "video/webm"} + ], + transclude: true + }; + +}]); + + app.controller('AtmApplicationAlert', ['$scope', '$controller', function($scope, $controller) { $controller('AlertCtrl', {$scope: $scope}); diff --git a/src/main/webapp/index.html b/src/main/webapp/index.html @@ -3,26 +3,66 @@ <div id="content-index" class="row no-gutter" data-lift="FindAtm" ng-controller="FindAtmCtrl" ng-class="{'mapexpanded': mapExpanded, 'nonfound': nonfound}"> <div class="col-xs-12 semi-content-index"> - <div id="intro" class="row"> + <div id="intro" class="row" ng-controller="IntroVideoCtrl"> + <video class="fade-in one" poster="http://artbees.net/themes/jupiter-demo/wp-content/uploads/2013/10/home-vid-img.jpg" preload="metadata" id="bgvid" ng-cloak> + + <source src="https://s3.amazonaws.com/assets-pyc/placeholder.webm" type="video/webm"> + </video> + + + <div id="video-viewer" ng-show="playing" ng-cloak> + <videogular + vg-player-ready="onPlayerReady" + vg-complete="onCompleteVideo" + vg-update-time="onUpdateTime" + vg-update-size="onUpdateSize" + vg-update-volume="onUpdateVolume" + vg-update-state="onUpdateState" + vg-autoplay="config.autoPlay" + vg-stretch="config.stretch.value" + vg-responsive="true"> + + <video vg-src="config.sources" preload="none"></video> + <vg-controls vg-autohide="config.autoHide" vg-autohide-time="config.autoHideTime" style="height:50px"> + <vg-play-pause-button></vg-play-pause-button> + <vg-timeDisplay>{{ currentTime }}</vg-timeDisplay> + <vg-scrubBar> + <vg-scrubbarcurrenttime></vg-scrubbarcurrenttime> + </vg-scrubBar> + <vg-timeDisplay>{{ timeLeft }}</vg-timeDisplay> + <vg-volume> + <vg-mutebutton></vg-mutebutton> + <vg-volumebar></vg-volumebar> + </vg-volume> + <vg-fullscreenButton + vg-enter-full-screen-icon="config.theme.enterFullScreenIcon" + vg-exit-full-screen-icon="config.theme.exitFullScreenIcon"></vg-fullscreenButton> + </vg-controls> + </videogular> + </div> + + + <div class="overlay"> <div class="col-sm-12 col-md-9"> - <div class="hidden-xs hidden-sm bitcoin-bg"></div> - <div class="jumbotron"> - <h1 class="fade-in one">Buy Bitcoin Instantly!</h1> + <div class="jumbotron text-white "> + <h1 class="fade-in one heavy text-shadow">Buy Bitcoin Instantly!</h1> <p> - <span class="jumbo bold fade-in one">ATM SERVICE OPERATOR FOR THE BITCOIN CURRENCY</span> + <span class="jumbo fade-in one heavy text-shadow">ATM SERVICE OPERATOR FOR THE BITCOIN CURRENCY</span> </p> - <p id="find-action" class="hidden-xxs"> - <a class="btn btn-primary btn-lg" ng-click="expandMap()" role="button"> + <p id="find-action" class="hidden-xxs hidden-xs hidden-sm"> + <a class="btn btn-primary btn-lg fade-in one" ng-click="expandMap()" role="button"> Find ATM Now </a> - </p> </div> </div> - <div class="hidden-xs hidden-sm col-md-3" ng-cloak> - <img class="bitcoin-solid-logo" width="200" src="https://s3.amazonaws.com/assets-pyc/bitcoin.png"></img> + <div class="col-xs-12 col-md-3"> + <div id="playbtn-wrapper" ng-click="play()" class="fade-in one"> + <img id="playbtn" src="https://s3.amazonaws.com/assets-pyc/playbtn.png"></img> + </div> + </div> </div> </div> @@ -133,8 +173,8 @@ {{m.address}}<br> {{m.city}}, {{m.state}} {{m.postal}}<br> <abbr title="Phone">P:</abbr> {{m.phone}}<br> - <a href="{{m.website}}" target="_blank">{{m.website}}</a> - <h6 class="store-status text-{{m.statusLevel}}">{{m.status}}</h6> + <a href="{{m.website}}" target="_blank">{{m.website}}</a> + <h6 class="store-status text-{{m.statusLevel}}">{{m.status}}</h6> </address> </div> </window> diff --git a/src/main/webapp/less/other/videogular.less b/src/main/webapp/less/other/videogular.less @@ -0,0 +1,521 @@ +videogular { + position: relative; + background-color: #000000; + overflow: hidden; + display: block; + + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: moz-none; + -ms-user-select: none; + user-select: none; +} + +videogular.fullscreen { + position: absolute; + left: 0; + top: 0; +} + +videogular video { + position: absolute; + width: 100%; + height: auto; + z-index: 1; +} + +.iconButton { + color: #FFFFFF; + font-family: 'FontAwesome'; + speak: none; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + -webkit-font-smoothing: antialiased; +} + +/**********************/ +/* OverlayPlay plugin */ +/**********************/ +vg-overlay-play { + width: 100%; + height: 100%; + position: absolute; + z-index: 3; +} + +vg-overlay-play div.play:before { + content: "\e000"; +} + +.overlayPlayContainer { + font-size: 100px; + width: 100%; + height: 100%; + position: absolute; + display: table; + cursor: pointer; + + zoom: 1; + filter: alpha(opacity=60); + opacity: 0.6; +} + +.overlayPlayContainer div { + vertical-align: middle; + text-align: center; + display: table-cell; +} + +/********************/ +/* Buffering plugin */ +/********************/ +vg-buffering { + width: 100%; + height: 100%; + position: absolute; + z-index: 4; +} + +.bufferingContainer { + width: 100%; + position: absolute; + cursor: pointer; + top: 50%; + margin-top: -50px; + + zoom: 1; + filter: alpha(opacity=60); + opacity: 0.6; +} + +/* Loading Spinner + * http://www.alessioatzeni.com/blog/css3-loading-animation-loop/ + */ +.loadingSpinner { + background-color: rgba(0,0,0,0); + border:5px solid rgba(255,255,255,1); + opacity:.9; + border-top:5px solid rgba(0,0,0,0); + border-left:5px solid rgba(0,0,0,0); + border-radius:50px; + box-shadow: 0 0 35px #FFFFFF; + width:50px; + height:50px; + margin: 0 auto; + -moz-animation:spin .5s infinite linear; + -webkit-animation:spin .5s infinite linear; +} + +.stop { + -webkit-animation-play-state:paused; + -moz-animation-play-state:paused; +} + +@-moz-keyframes spin { + 0% { -moz-transform:rotate(0deg); } + 100% { -moz-transform:rotate(360deg); } +} +@-moz-keyframes spinoff { + 0% { -moz-transform:rotate(0deg); } + 100% { -moz-transform:rotate(-360deg); } +} +@-webkit-keyframes spin { + 0% { -webkit-transform:rotate(0deg); } + 100% { -webkit-transform:rotate(360deg); } +} +@-webkit-keyframes spinoff { + 0% { -webkit-transform:rotate(0deg); } + 100% { -webkit-transform:rotate(-360deg); } +} + +/*********************/ +/* Controlbar plugin */ +/*********************/ +vg-controls { + width: 100%; + height: 50px; + position: absolute; + z-index: 5; +} +vg-controls #controls-container { + width: 100%; + height: 50px; + background-color: #000000; + position: absolute; + display: table; + + zoom: 1; + filter: alpha(opacity=50); + opacity: 0.5; +} + +vg-play-pause-button { + display: table-cell; + width: 50px; + vertical-align: middle; + text-align: center; + cursor: pointer; +} + +vg-timedisplay { + color: #FFFFFF; + display: table-cell; + font-family: Arial; + font-size: 18px; + width: 75px; + vertical-align: middle; + text-align: center; + cursor: default; +} + +.vgTimeDisplay { + display: table-cell; + font-family: Arial; + font-size: 18px; + width: auto; +} + +vg-scrubbar { + width: auto; + display: table-cell; + cursor: pointer; +} + +vg-scrubBarCurrentTime { + background-color: #FFFFFF; + width: 100%; + height: 50px; + display: block; + cursor: pointer; +} + +vg-volume { + display: table-cell; + width: 50px; + vertical-align: middle; + text-align: center; + cursor: pointer; +} + +vg-volumebar { + width: 50px; + height: 100px; + top: -100px; + margin-left: -25px; + vertical-align: middle; + text-align: center; + position: absolute; + cursor: pointer; +} + +/* IE10 hack */ +@media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) { + vg-volumebar { + zoom: 1; + filter: alpha(opacity=50); + opacity: 0.5; + } +} + +vg-fullscreenButton { + display: table-cell; + width: 50px; + vertical-align: middle; + text-align: center; + cursor: pointer; +} + +vg-volumebar .verticalVolumeBar { + width: 50px; + height: 100px; + background-color: #000000; + position: absolute; +} + +vg-volumebar .volumeBackground { + width: 20px; + height: 70px; + left: 15px; + top: 15px; + background-color: #222222; + position: absolute; +} + +vg-volumebar .volumeValue { + width: 20px; + height: 100%; + background-color: #FFFFFF; + position: absolute; +} + +vg-volumebar .volumeClickArea { + width: 20px; + height: 100%; + position: absolute; +} + +vg-controls .hide-animation { + animation: hideControlsAnimationFrames ease-out 0.5s; + animation-iteration-count: 1; + animation-fill-mode:forwards; /*when the spec is finished*/ + -webkit-animation: hideControlsAnimationFrames ease-out 0.5s; + -webkit-animation-iteration-count: 1; + -webkit-animation-fill-mode:forwards; /*Chrome 16+, Safari 4+*/ + -moz-animation: hideControlsAnimationFrames ease-out 0.5s; + -moz-animation-iteration-count: 1; + -moz-animation-fill-mode:forwards; /*FF 5+*/ + -o-animation: hideControlsAnimationFrames ease-out 0.5s; + -o-animation-iteration-count: 1; + -o-animation-fill-mode:forwards; /*Not implemented yet*/ + -ms-animation: hideControlsAnimationFrames ease-out 0.5s; + -ms-animation-iteration-count: 1; + -ms-animation-fill-mode:forwards; /*IE 10+*/ +} + +@keyframes hideControlsAnimationFrames{ + 0% { + left:0px; + top:0px; + opacity: 0.5; + transform: rotate(0deg) scaleX(1) scaleY(1) ; + } + 100% { + left:0px; + top:50px; + opacity: 0.5; + transform: rotate(0deg) scaleX(1) scaleY(1) ; + } +} + +@-moz-keyframes hideControlsAnimationFrames{ + 0% { + left:0px; + top:0px; + opacity: 0.5; + -moz-transform: rotate(0deg) scaleX(1) scaleY(1) ; + } + 100% { + left:0px; + top:50px; + opacity: 0.5; + -moz-transform: rotate(0deg) scaleX(1) scaleY(1) ; + } +} + +@-webkit-keyframes hideControlsAnimationFrames { + 0% { + left:0px; + top:0px; + opacity: 0.5; + -webkit-transform: rotate(0deg) scaleX(1) scaleY(1) ; + } + 100% { + left:0px; + top:50px; + opacity: 0.5; + -webkit-transform: rotate(0deg) scaleX(1) scaleY(1) ; + } +} + +@-o-keyframes hideControlsAnimationFrames { + 0% { + left:0px; + top:0px; + opacity: 0.5; + -o-transform: rotate(0deg) scaleX(1) scaleY(1) ; + } + 100% { + left:0px; + top:50px; + opacity: 0.5; + -o-transform: rotate(0deg) scaleX(1) scaleY(1) ; + } +} + +@-ms-keyframes hideControlsAnimationFrames { + 0% { + left:0px; + top:0px; + opacity: 0.5; + -ms-transform: rotate(0deg) scaleX(1) scaleY(1) ; + } + 100% { + left:0px; + top:50px; + opacity: 0.5; + -ms-transform: rotate(0deg) scaleX(1) scaleY(1) ; + } +} + +vg-controls .show-animation { + animation: showControlsAnimationFrames ease-out 0.5s; + animation-iteration-count: 1; + animation-fill-mode:forwards; /*when the spec is finished*/ + -webkit-animation: showControlsAnimationFrames ease-out 0.5s; + -webkit-animation-iteration-count: 1; + -webkit-animation-fill-mode:forwards; /*Chrome 16+, Safari 4+*/ + -moz-animation: showControlsAnimationFrames ease-out 0.5s; + -moz-animation-iteration-count: 1; + -moz-animation-fill-mode:forwards; /*FF 5+*/ + -o-animation: showControlsAnimationFrames ease-out 0.5s; + -o-animation-iteration-count: 1; + -o-animation-fill-mode:forwards; /*Not implemented yet*/ + -ms-animation: showControlsAnimationFrames ease-out 0.5s; + -ms-animation-iteration-count: 1; + -ms-animation-fill-mode:forwards; /*IE 10+*/ +} + +@keyframes showControlsAnimationFrames{ + 0% { + left:0px; + top:50px; + opacity: 0.5; + transform: rotate(0deg) scaleX(1) scaleY(1) ; + } + 100% { + left:0px; + top:0px; + opacity: 0.5; + transform: rotate(0deg) scaleX(1) scaleY(1) ; + } +} + +@-moz-keyframes showControlsAnimationFrames{ + 0% { + left:0px; + top:50px; + opacity: 0.5; + -moz-transform: rotate(0deg) scaleX(1) scaleY(1) ; + } + 100% { + left:0px; + top:0px; + opacity: 0.5; + -moz-transform: rotate(0deg) scaleX(1) scaleY(1) ; + } +} + +@-webkit-keyframes showControlsAnimationFrames { + 0% { + left:0px; + top:50px; + opacity: 0.5; + -webkit-transform: rotate(0deg) scaleX(1) scaleY(1) ; + } + 100% { + left:0px; + top:0px; + opacity: 0.5; + -webkit-transform: rotate(0deg) scaleX(1) scaleY(1) ; + } +} + +@-o-keyframes showControlsAnimationFrames { + 0% { + left:0px; + top:50px; + opacity: 0.5; + -o-transform: rotate(0deg) scaleX(1) scaleY(1) ; + } + 100% { + left:0px; + top:0px; + opacity: 0.5; + -o-transform: rotate(0deg) scaleX(1) scaleY(1) ; + } +} + +@-ms-keyframes showControlsAnimationFrames { + 0% { + left:0px; + top:50px; + opacity: 0.5; + -ms-transform: rotate(0deg) scaleX(1) scaleY(1) ; + } + 100% { + left:0px; + top:0px; + opacity: 0.5; + -ms-transform: rotate(0deg) scaleX(1) scaleY(1) ; + } +} + +/* Controlbar icons */ +vg-play-pause-button div.play:before { + content: "\e000"; +} + +vg-play-pause-button div.pause:before { + content: "\e001"; +} + +vg-mutebutton div.level3:before { + content: "\e002"; +} + +vg-mutebutton div.level2:before { + content: "\e003"; +} + +vg-mutebutton div.level1:before { + content: "\e004"; +} + +vg-mutebutton div.level0:before { + content: "\e005"; +} + +vg-mutebutton div.mute:before { + content: "\e006"; +} + +vg-fullscreenButton div.enter:before { + content: "\e007"; +} + +vg-fullscreenButton div.exit:before { + content: "\e008"; +} + +/*****************/ +/* Poster plugin */ +/*****************/ +vg-poster-image { + width: 100%; + height: 100%; + position: absolute; + z-index: 2; +} + +vg-poster-image img, vg-poster-image img.none { + top: 0; + bottom: 0; + right: 0; + left: 0; + margin: auto; + position: absolute; +} + +vg-poster-image img.fit { + width: 100%; +} + +vg-poster-image img.fill { + max-width: none; + height: 100%; +} + +/**********************/ +/* IMA ads plugin */ +/**********************/ +vg-ima-ads { + width: 100%; + height: 100%; + position: absolute; + z-index: 6; + display: none; +} diff --git a/src/main/webapp/less/overrides.less b/src/main/webapp/less/overrides.less @@ -70,9 +70,17 @@ body { } } - @media(max-width: @screen-xs-max) { + @media(max-width: @screen-md-max) { + h1 { font-size: 53px; } + } + + @media(max-width: @screen-sm-max) { h1 { font-size: 45px; } } + + @media(max-width: @screen-sm-max) { + h1 { font-size: 35px; } + } @media(max-width: @screen-xxs-max) { h1 { font-size: 25px; } diff --git a/src/main/webapp/less/pages/index.less b/src/main/webapp/less/pages/index.less @@ -16,7 +16,7 @@ @intro-div-height: 300px; @intro-div-height-sm: 300px; @intro-div-height-xs: 222px; - @intro-div-height-xxs: 145px; + @intro-div-height-xxs: 190px; @blusep-height: 15px; @@ -88,18 +88,112 @@ min-height: @intro-div-height - @blusep-height !important; margin: 0; - .bitcoin-solid-logo { - width: 200px; - margin-top: (@intro-div-height - @blusep-height) / 6; + video { + position: absolute; + top: 0; + left: 0; + width: 100%; + } + + #video-viewer { + position: fixed; + top: 0; + left: 0; + z-index: @zindex-modal; + min-width: 100%; + min-height: 100%; + background: rgba(0,0,0,0.85); + } + + .overlay { + position: absolute; + top: 0; + left: 0; + min-width: 100%; + min-height: 100%; + width: auto; + height: auto; + background-size: cover; + z-index: 0; + background: none repeat scroll 0% 0% rgba(0, 0, 0, 0.15); + box-shadow: 0px 50px 70px 0px rgba(50, 50, 50, 0.75) inset; + } + + #playbtn-wrapper { + width:100px; + height:100px; + background-image: url('https://s3.amazonaws.com/assets-pyc/bitcoin.png'); + background-size: cover; + + @media(min-width: @screen-md-min) { + margin-top: (@intro-div-height - @blusep-height) / 2.8; + } + + @media(max-width: @screen-sm-max) { + margin-left: auto; + margin-right: auto; + } + } + + #playbtn, #playbtn-wrapper { + width: 100px; + height: 100px; + + @media(max-width: @screen-sm-max) { + width: 80px; + height: 80px; + } + + @media(max-width: @screen-xs-max) { + width: 70px; + height: 70px; + } + + @media(max-width: @screen-xxs-max) { + width: 50px; + height: 50px; + } + } + + #playbtn { + &.flip { + -moz-transition:all 2s ease-in-out; + transform:rotateY(180deg); + backface-visibility:hidden; + } + + &.flipback { + -moz-transition:all 2s ease-in-out; + transform:rotateY(360deg); + backface-visibility:hidden; + } } .jumbotron { background: transparent; padding-bottom: 0; + + @media(max-width: @screen-sm-max) { + margin-bottom: @jumbotron-padding - (@jumbotron-padding / 3); + } + + @media(max-width: @screen-xxs-max) { + margin-bottom: @jumbotron-padding / 3; + } + + *.heavy { + font-weight: 600; + } + + *.text-shadow { + text-shadow: 2px 2px #000; + } } @media(max-width: @screen-sm-max) { - .text-center(); + :not(#controls-container) { + .text-center(); + } } @media(max-width: @screen-xs-max) { @@ -491,23 +585,6 @@ } } } - - .bitcoin-bg { - background: transparent url('https://s3.amazonaws.com/assets-pyc/bitcoin.png') no-repeat right center; - .background-size(inherit); - opacity: 0.2; - -moz-opacity: 0.2; - -khtml-opacity: 0.2; - filter: alpha(opacity=20); - position: absolute; - top: 0; - width: 100%; - height: 500px; - - @media(min-width: @screen-md) { - left: 300px; - } - } #twitter-widget-0 { width: 100% !important; diff --git a/src/main/webapp/less/styles.less b/src/main/webapp/less/styles.less @@ -143,6 +143,7 @@ @OtherPath: "./other"; @import "@{OtherPath}/angular-wizard.less"; +@import "@{OtherPath}/videogular.less"; /********************************************************* * My Website Pages diff --git a/src/main/webapp/vendor/videogular-controls.js b/src/main/webapp/vendor/videogular-controls.js @@ -0,0 +1,560 @@ +/** + * @license Videogular v0.4.0 http://videogular.com + * Two Fucking Developers http://twofuckingdevelopers.com + * License: MIT + */ +"use strict"; +angular.module("com.2fdevs.videogular.plugins.controls", []) + .directive( + "vgControls", + ["$timeout", "VG_STATES", "VG_EVENTS", function($timeout, VG_STATES, VG_EVENTS){ + return { + restrict: "E", + require: "^videogular", + transclude: true, + template: '<div id="controls-container" ng-show="isReady" ng-class="animationClass" ng-transclude></div>', + scope: { + autoHide: "=vgAutohide", + autoHideTime: "=vgAutohideTime" + }, + link: function(scope, elem, attr, API) { + var w = 0; + var h = 0; + var autoHideTime = 2000; + var controlBarHeight = elem[0].style.height; + var hideInterval; + var isReadyInterval; + + scope.isReady = false; + + function onUpdateSize(target, params) { + w = params[0]; + h = params[1]; + + elem.css("top", (parseInt(h, 10) - parseInt(controlBarHeight, 10)) + "px"); + } + + function onMouseMove() { + showControls(); + scope.$apply(); + } + + function hideControls() { + scope.animationClass = "hide-animation"; + } + + function showControls() { + scope.animationClass = "show-animation"; + $timeout.cancel(hideInterval); + if (scope.autoHide) hideInterval = $timeout(hideControls, autoHideTime); + } + + function onPlayerReady() { + var size = API.getSize(); + + elem.css("top", (parseInt(size.height, 10) - parseInt(controlBarHeight, 10)) + "px"); + isReadyInterval = $timeout(showWhenIsReady, 500); + } + + function showWhenIsReady() { + $timeout.cancel(isReadyInterval); + scope.isReady = true; + } + + // If vg-autohide has been set + if (scope.autoHide != undefined) { + scope.$watch("autoHide", function(value) { + if (value) { + scope.animationClass = "hide-animation"; + API.videogularElement.bind("mousemove", onMouseMove); + } + else { + scope.animationClass = ""; + $timeout.cancel(hideInterval); + API.videogularElement.unbind("mousemove", onMouseMove); + showControls(); + } + }); + } + + // If vg-autohide-time has been set + if (scope.autoHideTime != undefined) { + scope.$watch("autoHideTime", function(value) { + autoHideTime = value; + }); + } + + API.$on(VG_EVENTS.ON_UPDATE_SIZE, onUpdateSize); + + if (API.isPlayerReady()) onPlayerReady(); + else API.$on(VG_EVENTS.ON_PLAYER_READY, onPlayerReady); + } + } + } + ]) + .directive( + "vgPlayPauseButton", + ["VG_STATES", "VG_EVENTS", function(VG_STATES, VG_EVENTS) { + return { + restrict: "E", + require: "^videogular", + template: "<i class='fa fa-play fa-2x text-white' ng-class='playPauseIcon'></i>", + link: function(scope, elem, attr, API) { + function onChangeState(target, params) { + switch (params[0]) { + case VG_STATES.PLAY: + scope.playPauseIcon = {pause: true}; + break; + + case VG_STATES.PAUSE: + scope.playPauseIcon = {play: true}; + break; + + case VG_STATES.STOP: + scope.playPauseIcon = {play: true}; + break; + } + } + + function onClickPlayPause() { + API.playPause(); + scope.$apply(); + } + + scope.playPauseIcon = {play: true}; + + elem.bind("click", onClickPlayPause); + API.$on(VG_EVENTS.ON_SET_STATE, onChangeState); + } + } + } + ]) + .directive( + "vgTimedisplay", + ["VG_EVENTS", function(VG_EVENTS){ + return { + require: "^videogular", + restrict: "E", + link: function(scope, elem, attr, API) { + var showHours = false; + var totalTimeUnparsed = 0; + + function parseTime(time) { + var hours = Math.floor(time / 3600); + var mins = Math.floor((time % 3600) / 60); + var secs = Math.floor(time % 60); + + return {hours: hours, mins: mins, secs: secs}; + } + + function displayTime(h, m, s) { + var displayTime = ''; + + var hh = h < 10 ? "0" + h : h; + var mm = m < 10 ? "0" + m : m; + var ss = s < 10 ? "0" + s : s; + + if (showHours || h > 0) { + displayTime += hh + ':'; + } + + return displayTime + mm + ':' + ss; + } + + function onUpdateTime(target, params) { + var time = parseTime(params[0]); + var timeLeft = parseTime(totalTimeUnparsed - params[0]); + + scope.currentTime = displayTime(time.hours, time.mins, time.secs); + scope.timeLeft = displayTime(timeLeft.hours, timeLeft.mins, timeLeft.secs); + } + + function onComplete(target, params) { + scope.currentTime = "00:00"; + } + + function onStartPlaying(target, params) { + totalTimeUnparsed = params[0]; + + var time = parseTime(totalTimeUnparsed); + + showHours = (time.hours > 0); + + scope.totalTime = displayTime(time.hours, time.mins, time.secs); + } + + scope.currentTime = "00:00"; + scope.totalTime = "00:00"; + + API.$on(VG_EVENTS.ON_START_PLAYING, onStartPlaying); + API.$on(VG_EVENTS.ON_UPDATE_TIME, onUpdateTime); + API.$on(VG_EVENTS.ON_COMPLETE, onComplete); + } + } + } + ]) + .directive( + "vgScrubbar", + ["VG_EVENTS", "VG_STATES", "VG_UTILS", function(VG_EVENTS, VG_STATES, VG_UTILS){ + return { + restrict: "AE", + require: "^videogular", + replace: true, + link: function(scope, elem, attr, API) { + var isSeeking = false; + var isPlaying = false; + var isPlayingWhenSeeking = false; + var touchStartX = 0; + + function onScrubBarTouchStart(event) { + var touches = event.originalEvent.touches; + var touchX; + + if (VG_UTILS.isiOSDevice()) { + touchStartX = (touches[0].clientX - event.originalEvent.layerX) * -1; + } + else { + touchStartX = event.originalEvent.layerX; + } + + touchX = touches[0].clientX + touchStartX - touches[0].target.offsetLeft; + + isSeeking = true; + if (isPlaying) isPlayingWhenSeeking = true; + seekTime(touchX * API.videoElement[0].duration / elem[0].scrollWidth); + API.pause(); + } + function onScrubBarTouchEnd(event) { + if (isPlayingWhenSeeking) { + isPlayingWhenSeeking = false; + API.play(); + } + isSeeking = false; + } + function onScrubBarTouchMove(event) { + var touches = event.originalEvent.touches; + var touchX; + + if (isSeeking) { + touchX = touches[0].clientX + touchStartX - touches[0].target.offsetLeft; + seekTime(touchX * API.videoElement[0].duration / elem[0].scrollWidth); + } + } + function onScrubBarTouchLeave(event) { + isSeeking = false; + } + + function onScrubBarMouseDown(event) { + event = VG_UTILS.fixEventOffset(event); + + isSeeking = true; + if (isPlaying) isPlayingWhenSeeking = true; + seekTime(event.offsetX * API.videoElement[0].duration / elem[0].scrollWidth); + API.pause(); + } + function onScrubBarMouseUp(event) { + event = VG_UTILS.fixEventOffset(event); + + if (isPlayingWhenSeeking) { + isPlayingWhenSeeking = false; + API.play(); + } + isSeeking = false; + seekTime(event.offsetX * API.videoElement[0].duration / elem[0].scrollWidth); + } + function onScrubBarMouseMove(event) { + if (isSeeking) { + event = VG_UTILS.fixEventOffset(event); + seekTime(event.offsetX * API.videoElement[0].duration / elem[0].scrollWidth); + } + } + function onScrubBarMouseLeave(event) { + isSeeking = false; + } + function seekTime(time) { + API.seekTime(time, false); + } + + function onChangeState(target, params) { + if (!isSeeking) { + switch (params[0]) { + case VG_STATES.PLAY: + isPlaying = true; + break; + + case VG_STATES.PAUSE: + isPlaying = false; + break; + + case VG_STATES.STOP: + isPlaying = false; + break; + } + } + } + + API.$on(VG_EVENTS.ON_SET_STATE, onChangeState); + + // Touch move is really buggy in Chrome for Android, maybe we could use mouse move that works ok + if (VG_UTILS.isMobileDevice()) { + elem.bind("touchstart", onScrubBarTouchStart); + elem.bind("touchend", onScrubBarTouchEnd); + elem.bind("touchmove", onScrubBarTouchMove); + elem.bind("touchleave", onScrubBarTouchLeave); + } + else { + elem.bind("mousedown", onScrubBarMouseDown); + elem.bind("mouseup", onScrubBarMouseUp); + elem.bind("mousemove", onScrubBarMouseMove); + elem.bind("mouseleave", onScrubBarMouseLeave); + } + } + } + } + ]) + .directive( + "vgScrubbarcurrenttime", + ["VG_EVENTS", function(VG_EVENTS){ + return { + restrict: "E", + require: "^videogular", + link: function(scope, elem, attr, API) { + var percentTime = 0; + + function onUpdateTime(target, params){ + percentTime = Math.round((params[0] / params[1]) * 100); + elem.css("width", percentTime + "%"); + scope.$apply(); + } + + function onComplete(target, params){ + percentTime = 0; + elem.css("width", percentTime + "%"); + } + + API.$on(VG_EVENTS.ON_UPDATE_TIME, onUpdateTime); + API.$on(VG_EVENTS.ON_COMPLETE, onComplete); + } + } + } + ]) + .directive( + "vgVolume", + ["VG_UTILS", function(VG_UTILS) { + return { + restrict: "E", + link: function(scope, elem, attr) { + function onMouseOverVolume() { + scope.volumeVisibility = "visible"; + scope.$apply(); + } + + function onMouseLeaveVolume() { + scope.volumeVisibility = "hidden"; + scope.$apply(); + } + + // We hide volume controls on mobile devices + if (VG_UTILS.isMobileDevice()) { + elem.css("display", "none"); + } + else { + scope.volumeVisibility = "hidden"; + + elem.bind("mouseover", onMouseOverVolume); + elem.bind("mouseleave", onMouseLeaveVolume); + } + } + } + } + ]) + .directive( + "vgVolumebar", + ["VG_EVENTS", "VG_UTILS", function(VG_EVENTS, VG_UTILS) { + return { + restrict: "E", + require: "^videogular", + template: + "<div class='verticalVolumeBar'>"+ + "<div class='volumeBackground'>"+ + "<div class='volumeValue'></div>"+ + "<div class='volumeClickArea'></div>"+ + "</div>"+ + "</div>", + link: function(scope, elem, attr, API) { + var isChangingVolume = false; + var volumeBackElem = angular.element(elem[0].getElementsByClassName("volumeBackground")); + var volumeValueElem = angular.element(elem[0].getElementsByClassName("volumeValue")); + + function onClickVolume(event) { + event = VG_UTILS.fixEventOffset(event); + var volumeHeight = parseInt(volumeBackElem.prop("offsetHeight")); + var value = event.offsetY * 100 / volumeHeight; + var volValue = 1 - (value / 100); + updateVolumeView(value); + + API.setVolume(volValue); + + scope.$apply(); + } + + function onMouseDownVolume(event) { + isChangingVolume = true; + } + + function onMouseUpVolume(event) { + isChangingVolume = false; + } + + function onMouseLeaveVolume(event) { + isChangingVolume = false; + } + + function onMouseMoveVolume(event) { + if (isChangingVolume) { + event = VG_UTILS.fixEventOffset(event); + var volumeHeight = parseInt(volumeBackElem.prop("offsetHeight")); + var value = event.offsetY * 100 / volumeHeight; + var volValue = 1 - (value / 100); + updateVolumeView(value); + + API.setVolume(volValue); + + scope.$apply(); + } + } + + function updateVolumeView(value) { + volumeValueElem.css("height", value + "%"); + volumeValueElem.css("top", (100 - value) + "%"); + } + + function onSetVolume(target, params) { + updateVolumeView(params[0] * 100); + } + + function onChangeVisibility(value) { + elem.css("visibility", value); + } + + elem.css("visibility", scope.volumeVisibility); + + scope.$watch("volumeVisibility", onChangeVisibility); + + volumeBackElem.bind("click", onClickVolume); + volumeBackElem.bind("mousedown", onMouseDownVolume); + volumeBackElem.bind("mouseup", onMouseUpVolume); + volumeBackElem.bind("mousemove", onMouseMoveVolume); + volumeBackElem.bind("mouseleave", onMouseLeaveVolume); + + API.$on(VG_EVENTS.ON_SET_VOLUME, onSetVolume); + } + } + } + ]) + .directive( + "vgMutebutton", + ["VG_EVENTS", function(VG_EVENTS) { + return { + restrict: "E", + require: "^videogular", + template: "<i class='fa fa-volume-up fa-2x text-white' ng-class='muteIcon'></i>", + link: function(scope, elem, attr, API) { + var isMuted = false; + + function onClickMute(event) { + if (isMuted) { + scope.currentVolume = scope.defaultVolume; + } + else { + scope.currentVolume = 0; + scope.muteIcon = {mute: true}; + } + + isMuted = !isMuted; + + API.setVolume(scope.currentVolume); + + scope.$apply(); + } + + function onSetVolume(target, params) { + scope.currentVolume = params[0]; + + // TODO: Save volume with LocalStorage + // if it's not muted we save the default volume + if (!isMuted) { + scope.defaultVolume = params[0]; + } + else { + // if was muted but the user changed the volume + if (params[0] > 0) { + scope.defaultVolume = params[0]; + } + } + + var percentValue = Math.round(params[0] * 100); + if (percentValue == 0) { + scope.muteIcon = {mute: true}; + } + else if (percentValue > 0 && percentValue < 25) { + scope.muteIcon = {level0: true}; + } + else if (percentValue >= 25 && percentValue < 50) { + scope.muteIcon = {level1: true}; + } + else if (percentValue >= 50 && percentValue < 75) { + scope.muteIcon = {level2: true}; + } + else if (percentValue >= 75) { + scope.muteIcon = {level3: true}; + } + + //scope.$apply(); + } + + scope.defaultVolume = 1; + scope.currentVolume = scope.defaultVolume; + scope.muteIcon = {level3: true}; + + //TODO: get volume from localStorage + elem.bind("click", onClickMute); + + API.$on(VG_EVENTS.ON_SET_VOLUME, onSetVolume); + } + } + } + ]) + .directive( + "vgFullscreenbutton", + ["$window", "VG_EVENTS", function($window, VG_EVENTS){ + return { + restrict: "AE", + require: "^videogular", + scope: { + vgEnterFullScreenIcon: "=", + vgExitFullScreenIcon: "=" + }, + template: "<i class='fa fa-hand-o-left fa-2x text-white' ng-class='fullscreenIcon'></i>", + link: function(scope, elem, attr, API) { + function onEnterFullScreen() { + scope.fullscreenIcon = {exit: true}; + } + function onExitFullScreen() { + scope.fullscreenIcon = {enter: true}; + } + function onClickFullScreen(event) { + API.toggleFullScreen(); + + scope.$apply(); + } + + elem.bind("click", onClickFullScreen); + scope.fullscreenIcon = {enter: true}; + + API.$on(VG_EVENTS.ON_ENTER_FULLSCREEN, onEnterFullScreen); + API.$on(VG_EVENTS.ON_EXIT_FULLSCREEN, onExitFullScreen); + } + } + } + ]); diff --git a/src/main/webapp/vendor/videogular.js b/src/main/webapp/vendor/videogular.js @@ -0,0 +1,754 @@ +/** + * @license Videogular v0.4.0 http://videogular.com + * Two Fucking Developers http://twofuckingdevelopers.com + * License: MIT + */ +"use strict"; +angular.module("com.2fdevs.videogular", []) + .constant("VG_STATES", { + PLAY: "play", + PAUSE: "pause", + STOP: "stop" + }) + .constant("VG_EVENTS", { + ON_PLAY: "onVgPlay", + ON_PAUSE: "onVgPause", + ON_PLAY_PAUSE: "onVgPlayPause", + ON_START_PLAYING: "onVgStartPlaying", + ON_COMPLETE: "onVgComplete", + ON_SET_STATE: "onVgSetState", + ON_SET_VOLUME: "onVgSetVolume", + ON_TOGGLE_FULLSCREEN: "onVgToggleFullscreen", + ON_ENTER_FULLSCREEN: "onVgEnterFullscreen", + ON_EXIT_FULLSCREEN: "onVgExitFullscreen", + ON_BUFFERING: "onVgBuffering", + ON_UPDATE_TIME: "onVgUpdateTime", + ON_SEEK_TIME: "onVgSeekTime", + ON_UPDATE_SIZE: "onVgUpdateSize", + ON_UPDATE_THEME: "onVgUpdateTheme", + ON_PLAYER_READY: "onVgPlayerReady", + ON_LOAD_POSTER: "onVgLoadPoster", + ON_ERROR: "onVgError" + }) + .service("VG_UTILS", function() { + this.fixEventOffset = function($event) { + /** + * There's no offsetX in Firefox, so we fix that. + * Solution provided by Jack Moore in this post: + * http://www.jacklmoore.com/notes/mouse-position/ + * @param $event + * @returns {*} + */ + if (navigator.userAgent.match(/Firefox/i)) { + var style = $event.currentTarget.currentStyle || window.getComputedStyle($event.target, null); + var borderLeftWidth = parseInt(style['borderLeftWidth'], 10); + var borderTopWidth = parseInt(style['borderTopWidth'], 10); + var rect = $event.currentTarget.getBoundingClientRect(); + var offsetX = $event.clientX - borderLeftWidth - rect.left; + var offsetY = $event.clientY - borderTopWidth - rect.top; + + $event.offsetX = offsetX; + $event.offsetY = offsetY; + } + + return $event; + }; + /** + * Inspired by Paul Irish + * https://gist.github.com/paulirish/211209 + * @returns {number} + */ + this.getZIndex = function() { + var zIndex = 1; + + angular.element('*') + .filter(function(){ return angular.element(this).css('zIndex') !== 'auto'; }) + .each(function(){ + var thisZIndex = parseInt(angular.element(this).css('zIndex')); + if (zIndex < thisZIndex) zIndex = thisZIndex + 1; + }); + + return zIndex; + }; + + // Very simple mobile detection, not 100% reliable + this.isMobileDevice = function() { + return (typeof window.orientation !== "undefined") || (navigator.userAgent.indexOf("IEMobile") !== -1); + }; + + this.isiOSDevice = function() { + return (navigator.userAgent.match(/iPhone/i) || navigator.userAgent.match(/iPod/i) || navigator.userAgent.match(/iPad/i)); + }; + }) + .run(["$window", "VG_UTILS", + function($window, VG_UTILS) { + // Native fullscreen polyfill + var fullScreenAPI; + var APIs = { + w3: { + enabled: "fullscreenEnabled", + element: "fullscreenElement", + request: "requestFullscreen", + exit: "exitFullscreen", + onchange: "fullscreenchange", + onerror: "fullscreenerror" + }, + newWebkit: { + enabled: "webkitFullscreenEnabled", + element: "webkitFullscreenElement", + request: "webkitRequestFullscreen", + exit: "webkitExitFullscreen", + onchange: "webkitfullscreenchange", + onerror: "webkitfullscreenerror" + }, + oldWebkit: { + enabled: "webkitIsFullScreen", + element: "webkitCurrentFullScreenElement", + request: "webkitRequestFullScreen", + exit: "webkitCancelFullScreen", + onchange: "webkitfullscreenchange", + onerror: "webkitfullscreenerror" + }, + moz: { + enabled: "mozFullScreen", + element: "mozFullScreenElement", + request: "mozRequestFullScreen", + exit: "mozCancelFullScreen", + onchange: "mozfullscreenchange", + onerror: "mozfullscreenerror" + }, + ios: { + enabled: "webkitFullscreenEnabled", + element: "webkitFullscreenElement", + request: "webkitEnterFullscreen", + exit: undefined, + onexit: "webkitendfullscreen", + onchange: "webkitfullscreenchange", + onerror: "webkitfullscreenerror" + }, + ms: { + enabled: "msFullscreenEnabled", + element: "msFullscreenElement", + request: "msRequestFullscreen", + exit: "msExitFullscreen", + onchange: "msfullscreenchange", + onerror: "msfullscreenerror" + } + }; + + for (var browser in APIs) { + if (APIs[browser].enabled in document) { + fullScreenAPI = APIs[browser]; + fullScreenAPI.isFullScreen = function () { + return (document[this.element] != null); + }; + + break; + } + } + + // Override APIs on iOS + if (VG_UTILS.isiOSDevice()) { + fullScreenAPI = APIs.ios; + fullScreenAPI.isFullScreen = function () { + return (document[this.element] != null); + }; + } + + angular.element($window)[0].fullScreenAPI = fullScreenAPI; + } + ]) + /** + * @ngdoc directive + * @name com.2fdevs.videogular.videogular:videogular + * @restrict E + * @description + * Main directive that must wrap a &lt;video&gt; tag and all plugins. + * + * &lt;video&gt; tag usually will be above plugin tags, that's because plugins should be in a layer over the &lt;video&gt;. + * + * You can customize `videogular` with these attributes: + * + * @param {number or string} vgWidth This directive sets width for the entire player. Passing a number will set the width normally. Passing a string will create a binding with a scope variable in case it exists. + * + * If `vgWidth` or `vgHeight` are not declared, or `vgResponsive` is `"true"`, player will enter in a responsive mode and width will be 100% and height will be calculated through video metadata to preserve aspect ratio. + * + * @param {number or string} vgHeight This directive sets height for the entire player. Passing a number will set the height normally. Passing a string will create a binding with a scope variable in case it exists. + * + * If `vgWidth` or `vgHeight` are not declared, or `vgResponsive` is `"true"`, player will enter in a responsive mode and width will be 100% and height will be calculated through video metadata to preserve aspect ratio. + * + * @param {string} vgTheme String with a scope name variable. This directive will inject a CSS link in the header of your page. + * **This parameter is required.** + * + * @param {boolean or string} [autoPlay=false] vgAutoplay Boolean value or a String with a scope name variable to auto start playing video when it is initialized. + * + * **This parameter is disabled in mobile devices** because user must click on content to prevent consuming mobile data plans. + * + * @param {string} [stretch=none] vgStretch String value representing a stretch mode. This value controls how image will scale inside its container. Stretch modes available are "none", "fit" or "fill". + * + * - **"none"**: Will set the image in its original size. + * - **"fit"**: Will try to show always all the image leaving black bars above and below. + * - **"fill"**: Will try to cover all video player area to never show black bars above and below. + * + * Content will always appear centered. + * + * @param {boolean or string} [isResponsive=false] vgResponsive Boolean value or a String with a scope name variable to auto start playing video when it is initialized. + * + * @param {function} vgComplete Function name in controller's scope to call when video have been completed. + * @param {function} vgUpdateVolume Function name in controller's scope to call when volume changes. Receives a param with the new volume. + * @param {function} vgUpdateTime Function name in controller's scope to call when video playback time is updated. Receives two params with current time and duration in milliseconds. + * @param {function} vgUpdateSize Function name in controller's scope to call when videogular size is updated. Receives two param with the new width and height. + * @param {function} vgUpdateState Function name in controller's scope to call when video state changes. Receives a param with the new state. Possible values are "play", "stop" or "pause". + * @param {function} vgPlayerReady Function name in controller's scope to call when video have been initialized. Receives a param with the videogular API. + * @param {function} vgChangeSource Function name in controller's scope to change current video source. Receives a param with the new video. + * This is a free parameter and it could be values like "new.mp4", "320" or "sd". This will allow you to use this to change a video or video quality. + * This callback will not change the video, you should do that by updating your sources scope variable. + * + */ + .directive( + "videogular", + ["$window", "VG_STATES", "VG_EVENTS", "VG_UTILS", function($window, VG_STATES, VG_EVENTS, VG_UTILS) { + return { + restrict: "E", + scope: { + playerWidth: "=vgWidth", + playerHeight: "=vgHeight", + theme: "=vgTheme", + autoPlay: "=vgAutoplay", + responsive: "=vgResponsive", + stretch: "=vgStretch", + vgComplete: "&", + vgUpdateVolume: "&", + vgUpdateTime: "&", + vgUpdateSize: "&", + vgUpdateState: "&", + vgPlayerReady: "&", + vgChangeSource: "&" + }, + controller: ['$scope','$timeout', function($scope,$timeout) { + var currentTheme = null; + var currentWidth = null; + var currentHeight = null; + + var currentStretch = $scope.stretch; + var playerWidth = 0; + var playerHeight = 0; + var isFullScreenPressed = false; + var isFullScreen = false; + var isMetaDataLoaded = false; + var isElementReady = false; + var isVideoReady = false; + var isPlayerReady = false; + var isResponsive = false; + var vg = this; + + var vgCompleteCallBack = $scope.vgComplete(); + var vgUpdateVolumeCallBack = $scope.vgUpdateVolume(); + var vgUpdateTimeCallBack = $scope.vgUpdateTime(); + var vgUpdateSizeCallBack = $scope.vgUpdateSize(); + var vgUpdateStateCallBack = $scope.vgUpdateState(); + var vgPlayerReadyCallBack = $scope.vgPlayerReady(); + var vgChangeSourceCallBack = $scope.vgChangeSource(); + + $scope.currentState = VG_STATES.STOP; + + // PUBLIC $API + this.$on = function() { + $scope.$on.apply($scope, arguments); + }; + + this.isPlayerReady = function() { + return isPlayerReady; + }; + + this.seekTime = function(value, byPercent) { + var second; + if (byPercent) { + second = value * this.videoElement[0].duration / 100; + this.videoElement[0].currentTime = second; + } + else { + second = value; + this.videoElement[0].currentTime = second; + } + + $scope.$emit(VG_EVENTS.ON_SEEK_TIME, [second]); + }; + + this.playPause = function() { + if (this.videoElement[0].paused) { + this.play(); + } + else { + this.pause(); + } + }; + + this.setState = function(newState) { + if (newState && newState != $scope.currentState) { + if ($scope.vgUpdateState()) { + vgUpdateStateCallBack = $scope.vgUpdateState(); + vgUpdateStateCallBack(newState); + } + + $scope.currentState = newState; + $scope.$emit(VG_EVENTS.ON_SET_STATE, [$scope.currentState]); + } + + return $scope.currentState; + }; + + this.play = function() { + this.videoElement[0].play(); + this.setState(VG_STATES.PLAY); + $scope.$emit(VG_EVENTS.ON_PLAY); + }; + + this.pause = function() { + this.videoElement[0].pause(); + this.setState(VG_STATES.PAUSE); + $scope.$emit(VG_EVENTS.ON_PAUSE); + }; + + this.stop = function() { + this.videoElement[0].pause(); + this.videoElement[0].currentTime = 0; + this.setState(VG_STATES.STOP); + $scope.$emit(VG_EVENTS.ON_COMPLETE); + }; + + this.toggleFullScreen = function() { + // There is no native full screen support + if (!angular.element($window)[0].fullScreenAPI) { + if (isFullScreen) { + this.videogularElement.removeClass("fullscreen"); + this.videogularElement.css("z-index", 0); + } + else { + this.videogularElement.addClass("fullscreen"); + this.videogularElement.css("z-index", VG_UTILS.getZIndex()); + } + + isFullScreen = !isFullScreen; + + $scope.updateSize(); + } + // Perform native full screen support + else { + if (angular.element($window)[0].fullScreenAPI.isFullScreen()) { + if (!VG_UTILS.isMobileDevice()) { + document[angular.element($window)[0].fullScreenAPI.exit](); + } + } + else { + // On mobile devices we should make fullscreen only the video object + if (VG_UTILS.isMobileDevice()) { + // On iOS we should check if user pressed before fullscreen button + // and also if metadata is loaded + if (VG_UTILS.isiOSDevice()) { + if (isMetaDataLoaded) { + this.enterElementInFullScreen(this.videoElement[0]); + } + else { + isFullScreenPressed = true; + this.play(); + } + } + else { + this.enterElementInFullScreen(this.videoElement[0]); + } + } + else { + this.enterElementInFullScreen(this.elementScope[0]); + } + } + } + }; + + this.enterElementInFullScreen = function(element) { + element[angular.element($window)[0].fullScreenAPI.request](); + }; + + this.changeSource = function(newValue) { + if ($scope.vgChangeSource()) { + vgChangeSourceCallBack = $scope.vgChangeSource(); + vgChangeSourceCallBack(newValue); + } + }; + + this.setVolume = function(newVolume) { + if ($scope.vgUpdateVolume()) { + vgUpdateVolumeCallBack = $scope.vgUpdateVolume(); + vgUpdateVolumeCallBack(newVolume); + } + + this.videoElement[0].volume = newVolume; + $scope.$emit(VG_EVENTS.ON_SET_VOLUME, [newVolume]); + }; + + this.updateTheme = function(value) { + if (currentTheme) { + // Remove previous theme + var links = document.getElementsByTagName("link"); + for (var i=0, l=links.length; i<l; i++) { + if (links[i].outerHTML.indexOf(currentTheme) >= 0) { + links[i].parentNode.removeChild(links[i]); + } + } + } + + if (value) { + var headElem = angular.element(document).find("head"); + headElem.append("<link rel='stylesheet' href='" + value + "'>"); + + currentTheme = value; + } + }; + + this.updateStretch = function(value) { + currentStretch = value; + $scope.updateSize(); + }; + + this.setSize = function(newWidth, newHeight) { + currentWidth = newWidth; + currentHeight = newHeight; + + $scope.updateSize(); + }; + + this.getSize = function() { + return {width: currentWidth, height: currentHeight}; + }; + + // PRIVATE FUNCTIONS + $scope.API = this; + + $scope.init = function() { + vg.updateTheme($scope.theme); + $scope.addBindings(); + + if ($scope.playerWidth == undefined || $scope.playerHeight == undefined || $scope.responsive == true) { + isResponsive = true; + angular.element($window).bind("resize", $scope.onResizeBrowser); + } + else { + playerWidth = $scope.playerWidth; + playerHeight = $scope.playerHeight; + + vg.setSize(playerWidth, playerHeight); + } + + if (angular.element($window)[0].fullScreenAPI) { + document.addEventListener(angular.element($window)[0].fullScreenAPI.onchange, $scope.onFullScreenChange); + } + }; + + $scope.addBindings = function() { + $scope.$watch("playerWidth", function(newValue, oldValue) { + if (newValue != oldValue){ + vg.setSize(newValue, currentHeight); + } + }); + + $scope.$watch("playerHeight", function(newValue, oldValue) { + if (newValue != oldValue){ + vg.setSize(currentWidth, newValue); + } + }); + + $scope.$watch("theme", function(newValue, oldValue) { + if (newValue != oldValue){ + vg.updateTheme(newValue); + } + }); + + $scope.$watch("stretch", function(newValue, oldValue) { + if (newValue != oldValue){ + vg.updateStretch(newValue); + } + }); + + $scope.$watch("autoPlay", function(newValue, oldValue) { + if (newValue != oldValue){ + vg.play(); + } + }); + + $scope.$watch("responsive", function(newValue, oldValue) { + if (newValue != oldValue){ + isResponsive = newValue; + + if (isResponsive) { + angular.element($window).bind("resize", $scope.onResizeBrowser); + $scope.onResizeBrowser(); + } + else { + angular.element($window).unbind("resize", $scope.onResizeBrowser); + currentWidth = $scope.playerWidth; + currentHeight = $scope.playerHeight; + $scope.updateSize(); + } + } + }); + }; + + $scope.onElementReady = function() { + isElementReady = true; + + if (isVideoReady) { + $scope.onPlayerReady(); + } + }; + + $scope.onVideoReady = function() { + isVideoReady = true; + + if (isElementReady){ + $scope.onPlayerReady(); + } + }; + + $scope.onPlayerReady = function() { + vg.videoElement[0].addEventListener("loadedmetadata", $scope.onLoadedMetaData); + + $scope.doPlayerReady(); + }; + + $scope.onLoadedMetaData = function() { + isMetaDataLoaded = true; + $scope.doPlayerReady(); + }; + + $scope.doPlayerReady = function() { + if (isResponsive) { + var percentWidth = vg.elementScope[0].parentNode.clientWidth * 100 / vg.videoElement[0].videoWidth; + var videoHeight = vg.videoElement[0].videoHeight * percentWidth / 100; + currentWidth = vg.elementScope[0].parentNode.clientWidth; + currentHeight = videoHeight; + } + + isPlayerReady = true; + $scope.updateSize(); + if ($scope.vgPlayerReady()) { + vgPlayerReadyCallBack = $scope.vgPlayerReady(); + vgPlayerReadyCallBack(vg); + } + $scope.$emit(VG_EVENTS.ON_PLAYER_READY); + + if ($scope.autoPlay && !VG_UTILS.isMobileDevice() || $scope.currentState === VG_STATES.PLAY){ + $timeout(function() { + vg.play(); + }) + } + }; + + $scope.updateSize = function() { + if (isPlayerReady) { + var videoSize; + var videoTop; + var videoLeft; + + if (angular.element($window)[0].fullScreenAPI && angular.element($window)[0].fullScreenAPI.isFullScreen() || isFullScreen) { + vg.elementScope.css("width", parseInt(window.screen.width, 10) + "px"); + vg.elementScope.css("height", parseInt(window.screen.height, 10) + "px"); + + videoSize = $scope.getVideoSize(window.screen.width, window.screen.height); + + if (isFullScreen) { + playerWidth = $window.innerWidth; + playerHeight = $window.innerHeight; + } + else { + playerWidth = $window.screen.width; + playerHeight = $window.screen.height; + } + } + else { + vg.elementScope.css("width", parseInt(currentWidth, 10) + "px"); + vg.elementScope.css("height", parseInt(currentHeight, 10) + "px"); + + videoSize = $scope.getVideoSize(currentWidth, currentHeight); + + playerWidth = currentWidth; + playerHeight = currentHeight; + } + + if (currentHeight == 0 || isNaN(currentHeight)) { + playerWidth = videoSize.width; + playerHeight = videoSize.height; + } + + if (videoSize.width == 0 || isNaN(videoSize.width)) videoSize.width = currentWidth; + if (videoSize.height == 0 || isNaN(videoSize.height)) videoSize.height = currentHeight; + + videoLeft = (playerWidth - videoSize.width) / 2; + videoTop = (playerHeight - videoSize.height) / 2; + + vg.videoElement.attr("width", parseInt(videoSize.width, 10)); + vg.videoElement.attr("height", parseInt(videoSize.height, 10)); + vg.videoElement.css("width", parseInt(videoSize.width, 10) + "px"); + vg.videoElement.css("height", parseInt(videoSize.height, 10) + "px"); + vg.videoElement.css("top", videoTop + "px"); + vg.videoElement.css("left", videoLeft + "px"); + + vg.elementScope.css("width", parseInt(playerWidth, 10) + "px"); + vg.elementScope.css("height", parseInt(playerHeight, 10) + "px"); + + if ($scope.vgUpdateSize()) { + vgUpdateSizeCallBack = $scope.vgUpdateSize(); + vgUpdateSizeCallBack(playerWidth, playerHeight); + } + + $scope.$emit(VG_EVENTS.ON_UPDATE_SIZE, [playerWidth, playerHeight]); + } + }; + + $scope.onResizeBrowser = function() { + var percentWidth = vg.elementScope[0].parentNode.clientWidth * 100 / vg.videoElement[0].videoWidth; + var videoHeight = vg.videoElement[0].videoHeight * percentWidth / 100; + + currentWidth = vg.elementScope[0].parentNode.clientWidth; + currentHeight = videoHeight; + + $scope.updateSize(); + }; + + $scope.onFullScreenChange = function(event) { + if (angular.element($window)[0].fullScreenAPI.isFullScreen()) { + $scope.$emit(VG_EVENTS.ON_ENTER_FULLSCREEN); + } + else { + $scope.$emit(VG_EVENTS.ON_EXIT_FULLSCREEN); + } + + $scope.updateSize(); + }; + + $scope.onComplete = function(event) { + if ($scope.vgComplete()) { + vgCompleteCallBack = $scope.vgComplete(); + vgCompleteCallBack(); + } + + vg.setState(VG_STATES.STOP); + $scope.$emit(VG_EVENTS.ON_COMPLETE); + }; + + $scope.onStartBuffering = function(event) { + $scope.$emit(VG_EVENTS.ON_BUFFERING); + }; + + $scope.onStartPlaying = function(event) { + // Chrome fix: Chrome needs to update the video tag size or it will show a white screen + event.target.width++; + event.target.width--; + + $scope.$emit(VG_EVENTS.ON_START_PLAYING, [event.target.duration]); + }; + + $scope.onUpdateTime = function(event) { + if ($scope.vgUpdateTime()) { + vgUpdateTimeCallBack = $scope.vgUpdateTime(); + vgUpdateTimeCallBack(event.target.currentTime, event.target.duration); + } + + $scope.$emit(VG_EVENTS.ON_UPDATE_TIME, [event.target.currentTime, event.target.duration]); + }; + + $scope.getVideoSize = function(w, h) { + var percentageWidth; + var percentageHeight; + var result = {}; + var wider = vg.videoElement[0].videoWidth / vg.videoElement[0].videoHeight > w / h; + result.width = w; + result.height = h; + + if (currentStretch == "fit" && wider || currentStretch == "fill" && !wider) { + percentageWidth = w * 100 / vg.videoElement[0].videoWidth; + result.height = vg.videoElement[0].videoHeight * percentageWidth / 100; + } else if (currentStretch == "fill" && wider || currentStretch == "fit" && !wider) { + percentageHeight = h * 100 / vg.videoElement[0].videoHeight; + result.width = vg.videoElement[0].videoWidth * percentageHeight / 100; + } else { + result.width = vg.videoElement[0].videoWidth; + result.height = vg.videoElement[0].videoHeight; + } + + // Metadata has not been loaded or any problem has been happened + if (result.height == 0 || isNaN(result.height)) { + result.width = vg.elementScope[0].parentElement.clientWidth; + result.height = result.width * 9 / 16; + } + + return result; + }; + + $scope.init(); + }], + link: { + pre: function(scope, elem, attr, controller) { + controller.videogularElement = elem; + controller.elementScope = angular.element(elem); + controller.videoElement = controller.elementScope.find("video"); + + controller.videoElement[0].addEventListener("waiting", scope.onStartBuffering, false); + controller.videoElement[0].addEventListener("ended", scope.onComplete, false); + controller.videoElement[0].addEventListener("playing", scope.onStartPlaying, false); + controller.videoElement[0].addEventListener("timeupdate", scope.onUpdateTime, false); + + controller.elementScope.ready(scope.onElementReady); + controller.videoElement.ready(scope.onVideoReady); + } + } + } + } + ]) + .directive("vgSrc", + ["VG_EVENTS", "VG_UTILS", function(VG_EVENTS, VG_UTILS) { + return { + restrict: "A", + link: { + pre: function(scope, elem, attr) { + var element = elem; + var sources; + var canPlay; + + function changeSource() { + canPlay = ""; + + // It's a cool browser + if (element[0].canPlayType) { + for (var i = 0, l = sources.length; i < l; i++) { + canPlay = element[0].canPlayType(sources[i].type); + + if (canPlay == "maybe" || canPlay == "probably") { + element.attr("src", sources[i].src); + element.attr("type", sources[i].type); + break; + } + } + } + // It's a crappy browser and it doesn't deserve any respect + else { + // Get H264 or the first one + element.attr("src", sources[0].src); + element.attr("type", sources[0].type); + } + + if (canPlay == "") { + scope.$broadcast(VG_EVENTS.ON_ERROR, {type: "Can't play file"}) + } + } + + scope.$watch(attr.vgSrc, function(newValue, oldValue) { + if (!sources || newValue != oldValue) { + sources = newValue; + changeSource(); + } + }); + } + } + } + } + ]);