0

Gestor de descargas con AngularJS y servicios REST – 1º Creando la vista con AngularJS

 

angularjs_upload_1280x520

 

Hace relativamente poco (unos meses) que he profundizado un poco más en AngularJS, y me he empezado a dar cuenta como he podido estar todo este tiempo sin usar este maravillo framework MVC que me hubiera ahorrado un montón de trabajo en tarea tan rutinarias como el manejo del DOM con los datos traídos desde el backend.

Hoy quiero traeros esta serie de capítulos donde explotaremos la capacidad de este maravilloso framework creando una aplicación web que actúe a modo de gestor de descargas, así como la implementación de la parte del lado del servidor que actuará a modo de servicio REST. Para desarrollarlo usaremos el principio de ‘scaffolding’ de la mano de Yeoman que nos suministra todo lo esencial para empezar nuestro proyecto haciendo gala de un código de buenas prácticas. Si eres desarrollador web en activo o simplemente te gusta aprender, te recomiendo encarecidamente esta herramienta que sin duda te va a simplificar la vida. Usaremos el generador de angular para iniciar nuestra aplicación. En este enlace podéis encontrar como instalarlo y generar la estructura de la aplicación.

Si por el contrario, no deseas instalarlo, no habría mucho problema con seguirlo pero es recomendable que lo hagas, ya que yeoman a través de Grunt nos facilitará las tareas como minificación y lanzamiento de un servidor web para hacer nuestras pruebas. Tú decides.

Generando la aplicación

Si habéis seguido el tutorial, lo único que nos hace falta es ejecutar el comando en la carpeta que deseemos:

yo angular [nombre-app]

 

Y nos encontraremos con la siguiente estructura generada:

Captura de pantalla 2016-01-28 a las 11.25.44

En app tendríamos nuestra aplicación así como todos los archivos como pueden ser scripts, hojas de estilo, vistas, controladores, etc… y en el resto tendríamos todas las dependencias de bower y npm, así como también el gestor de tareas Grunt. Mención especial también a la carpeta test que nos genera. Los tests corren bajo Karma que es un test runner usado particularmente para correr tests unitarios. Muy importante tenerlo en cuenta a la hora de crear nuestra aplicación y queramos probar secciones críticas del mismo.

Si abrimos un terminal y nos dirigimos a la raíz de la carpeta del proyecto y ejecutamos el comando:

grunt serve

El mismo Grunt nos lanzará un servidor web corriendo nuestra aplicación en localhost:9000 y podremos ver en todo momento como quedaría nuestra aplicación. Grunt usa muchas tareas en pos de mejorar la productividad. Las más destacables pueden ser las tareas de minificación de archivos (uglyfy, imagemin) o tareas de observación que se dedican a buscar posibles errores en nuestra aplicación y recargar el servidor cada vez que guardemos nuestros cambios. De nuevo, otra herramienta que recomiendo su uso.

En la carpeta app tenemos lo siguiente:

Captura de pantalla 2016-01-28 a las 16.01.27

Nos fijaremos en la carpeta scripts y abriremos el archivo app.js

'use strict';

/**
 * @ngdoc overview
 * @name puploadAngularApp
 * @description
 * # puploadAngularApp
 *
 * Main module of the application.
 */
angular
  .module('puploadAngularApp', [
    'ngResource',
    'ngRoute'
  ])
  .config(function ($routeProvider) {
    $routeProvider
      .when('/', {
        templateUrl: 'views/main.html',
        controller: 'MainCtrl',
        controllerAs: 'main'
      })
      .when('/about', {
        templateUrl: 'views/about.html',
        controller: 'AboutCtrl',
        controllerAs: 'about'
      })
      .otherwise({
        redirectTo: '/'
      });
  });

Como observamos, Yeoman nos carga los módulos elegidos al momento de crear el proyecto. En este caso son ngResource y ngRoute. Seleccionando este último nos configura ya el enrutador asociados a cada una de las plantillas con sus respectivos controladores. Tanto el main como el about no lo usaremos por lo que los borraremos y ejecutaremos el siguiente comando:

yo angular:route filesupload

Esto nos creará un controlador y vista con el nombre filesupload y además se encargará de configurar por nosotros un acceso en el enrutador en app.js por lo que ahora tendríamos lo siguiente:

app.js

'use strict';

/**
 * @ngdoc overview
 * @name puploadAngularApp
 * @description
 * # puploadAngularApp
 *
 * Main module of the application.
 */
angular
     .module('puploadAngularApp', [
      'ngResource',
      'ngRoute'
 ])
 .config(function ($routeProvider) {
     $routeProvider
       .when('/', {
           templateUrl: 'views/filesupload.html',
           controller: 'FilesuploadCtrl', 
           controllerAs: 'filesUpload'
       })
       .otherwise({
          redirectTo: '/'
       });
 });

scripts/filesupload.js

'use strict';

/**
 * @ngdoc function
 * @name puploadAngularApp.controller:FilesuploadCtrl
 * @description
 * # FilesuploadCtrl
 * Controller of the puploadAngularApp
 */
angular.module('puploadAngularApp')
 .controller('FilesuploadCtrl', function () {
     this.awesomeThings = [
        'HTML5 Boilerplate',
        'AngularJS',
        'Karma'
     ];
 });

Y un archivo html con el mismo nombre en la carpeta views. Para la implementación de nuestro uploader usaremos una librería llamada Plupload el cual podemos encontrar aquí. Bajamos, descomprimimos y copiamos el contenido a una carpeta que crearemos de nombre plupload en la carpeta scripts del proyecto. Acto seguido abriremos el controlador filesupload.js antes generado:

'use strict';

/**
 * @ngdoc function
 * @name puploadAngularApp.controller:FilesuploadCtrl
 * @description
 * # FilesuploadCtrl
 * Controller of the puploadAngularApp
 */
angular.module('puploadAngularApp')
 .controller('FilesuploadCtrl', function () {
     $scope.filesUploads = [];
     $scope.uploader = new plupload.Uploader({
      runtimes : 'html5,flash,silverlight,html4',
      browse_button : 'pickfiles', // you can pass an id...
      container: document.getElementById('container'), // ... or DOM Element itself
      url : '',
      flash_swf_url : '../js/Moxie.swf',
      silverlight_xap_url : '../js/Moxie.xap',


    });
 });

$scope contendrá una instancia de la clase Uploader el cual la inicializaremos a los valores que vienen por defecto. La opción browse_button se define para el botón que lanzará el dialog para cargar los archivos que queramos subir. Mención especial a la opción url el cual no definiremos aún pero que en resumidas cuentas será la lógica que se encargue de gestionar la subida de los archivos en la parte del servidor. Por otro lado filesUploads irá almacenando todos los archivos que añadamos para mostrar su información en la vista.

    filters : {
         max_file_size : '10000mb',
         mime_types: [
            {title : "Image files", extensions : "jpg,gif,png"},
            {title : "Zip files", extensions : "zip"},
            {title : "Video files", extensions : "avi,mp4,wmv,mkv"},
            {title : "PDF files", extensions : "pdf"}
         ]
     },

Con la opción filters, se define los tipos de archivos que permitiremos así como el máximo tamaño de cada uno.


init: {
   PostInit: function() {
       $scope.uploadFiles = function(){
       $scope.uploader.start();
       return false; // Prevent event default fires...
   },

A continuación definimos la funcionalidad para los distintos eventos en los que se comportará la instancia. PostInit es el evento que se ejecutará cuando llamemos $scope.uploader.init(); para inicializar $scope.uploader.


FilesAdded: function(up, files) {
         plupload.each(files, function(file) {
           file.formatSize = plupload.formatSize(file.size);
           file.progress = 0;
           file.showProgress = false;
           file.showError = false;
           file.processingFile = false;
           file.complete = false;
           $scope.filesUploads.push(file);
           $scope.$apply(); // probably plupload is blocking the scope apply event so we have to aply ourselves.

           // Set preview image
           var img = new mOxie.Image();
        		img.onload = function() {
                this.embed($('#preview-'+file.id).get(0), {
                  width: 100,
                  height: 100,
                  crop: true
                });
        		};
        		img.onembedded = function() {
        			this.destroy();
        		};
        		img.onerror = function() {
        			this.destroy();
        		};
            if(file.type === 'image/jpeg' || file.type === 'image/png'){
                img.load(file.getSource());
            }
            else{
              $('#preview-'+file.id).prepend('<span style="font-size: 100px; color: #fff;"><i class="glyphicon glyphicon-file"></i></span>');
            }
         });
       },

FilesAdded se ejecutará cada vez que añadamos un nuevo archivo a la cola. Por cada uno de los archivos se le asignará unos atributos relacionados con el muestreo del progreso de subida, así como el procesamiento del archivo una vez que haya sido subido al servidor o algún error provocado en este. Por otro lado mediante la función formatSize, se pasará el tamaño de bytes del archivo a un formato más legible dependiendo de su peso final. Cada uno de estos archivos se añadirán al array $scope.filesUploads que se usará para iterar en la vista para mostrar cada uno de los archivos. Tened en cuenta la llamada a $scope.$apply() para que el ámbito $scope reconozca estos cambios. Esto no debería ser necesario pero he experimentado problemas con esta librería y Angular a la hora de reflejarse los cambios en $scope.

Para los archivos de imágenes estableceremos una previsualización a la hora de subirlo. Para ello usaremos la clase Image del paquete mOxie que ya viene incluido en la librería plupload. Siempre que nos encontremos con un tipo mime image/jpeg o image/png mediante el evento img.onload incrustaremos la imagen en el div asociado al identificador del archivo. En otro caso, añadiremos un icono glyphicon simbolizando un archivo.

BeforeUpload: function(up, file) {
         file.showProgress = true;
         file.showError = false;
       },

BeforeUpload se ejecutará inmediatamente antes de empezar el proceso de subida. Se le pasa como parámetro la instancia y el archivo que se esté subiendo en ese momento. Mostraremos el div que contenga la barra de progreso para ese archivo.


UploadProgress: function(up, file) {
         file.progressFile = file.percent;
         file.processingFile = false;
         if(file.percent === 100){
           file.processingFile = true;
         }
         $scope.$apply();
       },

Este evento se irá llamando conforme el archivo se va subiendo para mostrar el progreso del mismo. Igualmente aquí llamaremos a la función $scope.$apply() para guardar los cambios. Una vez que el archivo se haya subido, se procederá a su procesamiento en el lado del servidor.


FileUploaded: function(up, file, response) {
         console.log("File uploaded: "+file.name);
         console.log("Response from the server: "+response.message+ " with status: "+ response.status);
         console.log("Response headers: "+response.responseHeaders);
         file.processingFile = false;
         file.complete = true;
         $scope.$apply();
       },

Este evento se llamará cuando el archivo haya sido subido. Esta vez tenemos un parámetro extra response que nos dará la respuesta final del servidor así como el estado y las cabeceras.


 Error: function(up, err) {
         err.file.error = err.response+" with status: "+err.status;
         err.file.showError = true;
         err.file.showProgress = false;
         err.file.processingFile = false;
         err.file.complete = false;         
         $scope.$apply();
         console.log(err.file.name+" showError: "+err.file.showError);
         console.log("Error Plupload code#" + err.code + ": " + err.message);
         console.log("Error with file: "+err.file.name);
         console.log("Response from the server: "+err.response+" with status: "+err.status);
         console.log("Response headers: "+err.responseHeaders);
       }

Mismo que el anterior sólo que en este caso se ejecutará siempre que tengamos un error en el proceso de subida. Estableceremos showError para el archivo a true y la notificación de progreso a false, de forma que siempre que haya un error se muestre una alerta en vez del proceso de subida.

Al final el controlador filesupload.js quedaría así:


'use strict';

/**
 * @ngdoc function
 * @name puploadAngularApp.controller:FilesuploadCtrl
 * @description
 * # FilesuploadCtrl
 * Controller of the puploadAngularApp
 */
angular.module('puploadAngularApp')
  .controller('FilesuploadCtrl', ['$scope', function ($scope) {
    $scope.filesUploads = [];
    $scope.uploader = new plupload.Uploader({
     runtimes : 'html5,flash,silverlight,html4',
     browse_button : 'pickfiles', // you can pass an id...
     container: document.getElementById('container'), // ... or DOM Element itself
     url : 'example.php',
     flash_swf_url : '../plupload/Moxie.swf',
     silverlight_xap_url : '../plupload/Moxie.xap',


     filters : {
       max_file_size : '10000mb',
       mime_types: [
         {title : "Image files", extensions : "jpg,gif,png"},
         {title : "Zip files", extensions : "zip"},
         {title : "Video files", extensions : "avi,mp4,wmv,mkv"},
         {title : "PDF files", extensions : "pdf"}
       ]
     },

     init: {
       PostInit: function() {
         $scope.uploadFiles = function(){
           $scope.uploader.start();
           return false;
         };
       },

       FilesAdded: function(up, files) {
         plupload.each(files, function(file) {
           file.formatSize = plupload.formatSize(file.size);
           file.progress = 0;
           file.showProgress = false;
           file.showError = false;
           file.processingFile = false;
           file.complete = false;
           $scope.filesUploads.push(file);
           $scope.$apply(); // probably plupload is blocking the scope apply event so we have to aply ourselves.

           // Set preview image
           var img = new mOxie.Image();
        		img.onload = function() {
                this.embed($('#preview-'+file.id).get(0), {
                  width: 100,
                  height: 100,
                  crop: true
                });
        		};
        		img.onembedded = function() {
        			this.destroy();
        		};
        		img.onerror = function() {
        			this.destroy();
        		};
            if(file.type === 'image/jpeg' || file.type === 'image/png'){
                img.load(file.getSource());
            }
            else{
              $('#preview-'+file.id).prepend('<span style="font-size: 100px; color: #fff;"><i class="glyphicon glyphicon-file"></i></span>');
            }
         });
       },

       BeforeUpload: function(up, file) {
         file.showProgress = true;
         file.showError = false;
       },

       UploadProgress: function(up, file) {
         file.progressFile = file.percent;
         file.processingFile = false;
         if(file.percent === 100){
           file.processingFile = true;
         }
         $scope.$apply();
       },

       FileUploaded: function(up, file, response) {
         console.log("File uploaded: "+file.name);
         console.log("Response from the server: "+response.message+ " with status: "+ response.status);
         console.log("Response headers: "+response.responseHeaders);
         file.processingFile = false;
         file.complete = true;
         $scope.$apply();
       },

       Error: function(up, err) {
         err.file.error = err.response+" with status: "+err.status;
         err.file.showError = true;
         err.file.showProgress = false;
         err.file.processingFile = false;
         err.file.complete = false;
         $scope.$apply();
         console.log(err.file.name+" showError: "+err.file.showError);
         console.log("Error Plupload code#" + err.code + ": " + err.message);
         console.log("Error with file: "+err.file.name);
         console.log("Response from the server: "+err.response+" with status: "+err.status);
         console.log("Response headers: "+err.responseHeaders);
       }
     }
   });

   $scope.uploader.init();


  }]);



La vista filesupload.html la dividiremos en 2 partes. La primera, y en la que nos enfocaremos este capítulo, contendrá el formulario para subir y mostrar la lista de archivos.



<div id="upload-form" class="row">
                    <button type="button" id="pickfiles" class="btn btn-success">
                      Browse File
                    </button>
                    <button type="button" ng-click="uploadFiles()" id="uploadfiles" class="btn btn-warning">
                      Start Upload
                    </button>

...


El botón de búsqueda de archivos irá asociado a la opción browse_button de $scope.pluploader que definimos por el identificador pickfiles. El botón de subidas se le asociará la directiva ng-click que llamará a la función uploadFiles() que definimos en PostInit que arrancará la subida de los archivos.


<ul id="filelist" class="list-unstyled">

<li ng-repeat="f in filesUploads">

<div class="media">

<div class="media-left">

<div class="media-object" id="preview-{{f.id}}"></div>

                         </div>


<div class="media-body">

<h4 class="media-heading info-file">{{ f.name }}</h4>


<h5>File size: <b>{{ f.formatSize }}</b></h5>


<h5>File type: <b>{{ f.type }}</b></h5>



<div ng-show="f.showError" class="alert alert-danger fade in">
                            <a class="close" data-dismiss="alert" aria-label="close">&times;</a>
                            <strong>Error!</strong> {{f.error}}
                           </div>


<div class="row">

<div class="col-xs-10">

<div ng-show="f.showProgress && !f.showError" class="progress progress-striped active">

<div class="progress-bar progress-bar-custom" role="progressbar" style="width: {{f.progressFile}}%;">
                                        <span class="sr-only">{{f.progressFile}}%</span>
                                    </div>

                                 </div>

                               </div>


<div class="col-xs-2">
                                 <span ng-show="f.processingFile" class="glyphicon glyphicon-refresh glyphicon-refresh-animate"></span>
                                 <span ng-show="f.complete" class="glyphicon glyphicon glyphicon-ok"></span>
                               </div>

                             </div>

                         </div>

                       </div>

                      </li>

                    </ul>


Aquí imprimiremos la lista de archivos a través de la directiva ng-repeat. Usando el array filesUploads, se iterará a través de cada uno de los elementos mostrando la información pertinente. Con las directivas ng-show se mostrará las notificaciones de progreso, procesamiento de archivos o error, en el caso de haberlos.

Al final el código en filesupload.html quedará así:



<style media="screen">
@-webkit-keyframes spin2 {
    from { -webkit-transform: rotate(0deg);}
    to { -webkit-transform: rotate(360deg);}
}

</style>


<div class="container">


<div class="col-xs-12">

<div class="row marketing">

<div id="container">


<div class="row">

<div class="col-xs-10">

<div class="panel panel-default">

<div class="panel-heading">
<h4>Form file upload</h4>
</div>


<div class="panel-body">


Browse for files you want upload to. Only jpg and png files will show a preview, otherwise a glyphicon file will show instead.


<div id="upload-form" class="row">
                    <button type="button" id="pickfiles" class="btn btn-success">
                      Browse File
                    </button>
                    <button type="button" ng-click="uploadFiles()" id="uploadfiles" class="btn btn-warning">
                      Start Upload
                    </button>


<ul id="filelist" class="list-unstyled">

<li ng-repeat="f in filesUploads">

<div class="media">

<div class="media-left">

<div class="media-object" id="preview-{{f.id}}"></div>

                         </div>


<div class="media-body">

<h4 class="media-heading info-file">{{ f.name }}</h4>


<h5>File size: <b>{{ f.formatSize }}</b></h5>


<h5>File type: <b>{{ f.type }}</b></h5>



<div ng-show="f.showError" class="alert alert-danger fade in">
                            <a class="close" data-dismiss="alert" aria-label="close">&times;</a>
                            <strong>Error!</strong> {{f.error}}
                           </div>


<div class="row">

<div class="col-xs-10">

<div ng-show="f.showProgress && !f.showError" class="progress progress-striped active">

<div class="progress-bar progress-bar-custom" role="progressbar" style="width: {{f.progressFile}}%;">
                                        <span class="sr-only">{{f.progressFile}}%</span>
                                    </div>

                                 </div>

                               </div>


<div class="col-xs-2">
                                 <span ng-show="f.processingFile" class="glyphicon glyphicon-refresh glyphicon-refresh-animate"></span>
                                 <span ng-show="f.complete" class="glyphicon glyphicon glyphicon-ok"></span>
                               </div>

                             </div>

                         </div>

                       </div>

                      </li>

                    </ul>

                </div>

              </div>

            </div>

          </div>


<div class="row">

<div class="col-xs-12">

           </div>

        </div>

      </div>

     

  </div>


</div>



Por último editaremos el archivo index.html y las css con lo siguiente:

index.html


<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <title></title>
    <meta name="description" content="">
    <meta name="viewport" content="width=device-width">
    <meta http-equiv="content-type" content="text/html; charset=UTF-8"/>
    <!-- Place favicon.ico and apple-touch-icon.png in the root directory -->
    <!-- build:css(.) styles/vendor.css -->
    <!-- bower:css -->
    <link rel="stylesheet" href="bower_components/bootstrap/dist/css/bootstrap.css" />
    <!-- endbower -->
    <!-- endbuild -->
    <link rel="stylesheet" href="styles/animations.css">
    <!-- build:css(.tmp) styles/main.css -->

    <link rel="stylesheet" href="styles/main.css">
    <!-- endbuild -->
  </head>
  <body ng-app="puploadAngularApp">
    <!--[if lte IE 8]>


You are using an <strong>outdated</strong> browser. Please <a href="http://browsehappy.com/">upgrade your browser</a> to improve your experience.

    <![endif]-->

    <!-- Add your site or application content here -->

<div class="header">

<div class="navbar navbar-default" role="navigation">

<div class="container">

<div class="navbar-header">

            <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#js-navbar-collapse">
              <span class="sr-only">Toggle navigation</span>
              <span class="icon-bar"></span>
              <span class="icon-bar"></span>
              <span class="icon-bar"></span>
            </button>

            <a class="navbar-brand" href="#/">File uploader with AngularJS and Plupload</a>
          </div>

        </div>

      </div>

    </div>




<div id="wrapper" ng-view=""></div>




<div class="footer">

<div class="container">


<span class="glyphicon glyphicon-heart"></span> from the Yeoman team

      </div>

    </div>



    <!-- Google Analytics: change UA-XXXXX-X to be your site's ID -->
     <script>
       !function(A,n,g,u,l,a,r){A.GoogleAnalyticsObject=l,A[l]=A[l]||function(){
       (A[l].q=A[l].q||[]).push(arguments)},A[l].l=+new Date,a=n.createElement(g),
       r=n.getElementsByTagName(g)[0],a.src=u,r.parentNode.insertBefore(a,r)
       }(window,document,'script','https://www.google-analytics.com/analytics.js','ga');

       ga('create', 'UA-XXXXX-X');
       ga('send', 'pageview');
    </script>


    <!-- build:js(.) scripts/vendor.js -->
    <!-- bower:js -->
    <script src="bower_components/jquery/dist/jquery.js"></script>
    <script src="bower_components/angular/angular.js"></script>
    <script src="bower_components/bootstrap/dist/js/bootstrap.js"></script>
    <script src="bower_components/angular-animate/angular-animate.js"></script>
    <script src="bower_components/angular-cookies/angular-cookies.js"></script>
    <script src="bower_components/angular-resource/angular-resource.js"></script>
    <script src="bower_components/angular-route/angular-route.js"></script>
    <script src="bower_components/angular-sanitize/angular-sanitize.js"></script>
    <!-- endbower -->
    <!-- endbuild -->

        <!-- build:js({.tmp,app}) scripts/scripts.js -->
        <script src="scripts/plupload/plupload.full.min.js"></script>
        <script src="scripts/app.js"></script>
        <script src="scripts/controllers/filesupload.js"></script>
        <!-- endbuild -->
</body>
</html>


main.css




/***********************/
.browsehappy {
  margin: 0.2em 0;
  background: #ccc;
  color: #000;
  padding: 0.2em 0;
}

body {
  padding: 0;

}

/* Everything but the jumbotron gets side spacing for mobile first views */
.header,
.marketing,
.footer {
  padding-left: 15px;
  padding-right: 15px;
}

/* Custom page header */
.header {
  margin-bottom: 10px;
}
/* Make the masthead heading the same height as the navigation */
.header h3 {
  margin-top: 0;
  margin-bottom: 0;
  line-height: 40px;
  padding-bottom: 19px;
}

/* Custom page footer */
.footer {
  /*margin-top: 200px;*/
  padding-top: 19px;
  color: #777;
  background-color: #fff;
}

.container-narrow > hr {
  margin: 30px 0;
}

/* Main marketing message and sign up button */
.jumbotron {
  text-align: center;
  border-bottom: 1px solid #e5e5e5;
}
.jumbotron .btn {
  font-size: 21px;
  padding: 14px 24px;
}

/* Supporting marketing content */
.marketing {
  margin: 40px 0;
}
.marketing p + h4 {
  margin-top: 28px;
}

/* Responsive: Portrait tablets and up */
@media screen and (min-width: 768px) {
  .container {
    max-width: 730px;
  }

  /* Remove the padding we set earlier */
  .header,
  .marketing,
  .footer {
    padding-left: 0;
    padding-right: 0;
  }
  /* Space out the masthead */
  .header {
    margin-bottom: -20px;
  }
  /* Remove the bottom border on the jumbotron for visual effect */
  .jumbotron {
    border-bottom: 0;
  }
}


/******** CUSTOM CSS ************/

#filelist > li {
  border: 1px solid rgba(0, 0, 0, 0.25);
  margin: 10px;
  line-height: 30px;
  border-radius: 4px;
  box-shadow: inset 0px 0px 0 rgba(0, 0, 0, 0.2), inset 0 -1px 0 rgba(255, 255, 255, 0.25), inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.5);
  background-color: rgba(0, 0, 0, 0.08);

}

#filelist > li h4, #filelist > li h5{
  text-shadow: 1px 1px 0 #Fff;
  color: rgb(56, 57, 58);
}

.info-file{
  width: 350px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  margin-left: 10px;
}

.progress {background: rgba(245, 245, 245, 1);
          border: 2px solid rgba(245, 245, 245, 1);
          border-radius: 25px;
          height: 22px;
          width: 93%;
          margin: 25px 10px 0px 10px;}
.progress-bar-custom {background: rgba(119, 0, 224, 1); background: -webkit-linear-gradient(top, rgba(201, 126, 65, 1) 0%, rgba(119, 0, 224, 1) 100%); background: linear-gradient(to bottom, rgba(201, 126, 65, 1) 0%, rgba(119, 0, 224, 1) 100%);}
.progress-striped .progress-bar-custom {background-color: rgba(119, 0, 224, 1); background-image: -webkit-gradient(linear,0 100%,100% 0,color-stop(0.25,rgba(255, 255, 255, 0.15),color-stop(0.25,transparent),color-stop(0.5,transparent),color-stop(0.5,rgba(255, 255, 255, 0.15)),color-stop(0.75,rgba(255, 255, 255, 0.15)),color-stop(0.75,transparent),to(transparent))); background-image: -webkit-linear-gradient(45deg,rgba(255, 255, 255, 0.15) 25%,transparent 25%,transparent 50%,rgba(255, 255, 255, 0.15) 50%,rgba(255, 255, 255, 0.15) 75%,transparent 75%,transparent); background-image: linear-gradient(45deg,rgba(255, 255, 255, 0.15) 25%,transparent 25%,transparent 50%,rgba(255, 255, 255, 0.15) 50%,rgba(255, 255, 255, 0.15) 75%,transparent 75%,transparent); background-size: 74px 74px;}
.sr-only{
  position: relative !important;
}

.media{
  padding: 10px;
}



#wrapper{
  min-height: 600px;
  /* Permalink - use to edit and share this gradient: http://colorzilla.com/gradient-editor/#1e5799+0,207cca+37,207cca+37,2989d8+50,7db9e8+100 */
  background: #1e5799; /* Old browsers */
  background: -moz-linear-gradient(top,  #1e5799 0%, #207cca 37%, #207cca 37%, #2989d8 50%, #7db9e8 100%); /* FF3.6-15 */
  background: -webkit-linear-gradient(top,  #1e5799 0%,#207cca 37%,#207cca 37%,#2989d8 50%,#7db9e8 100%); /* Chrome10-25,Safari5.1-6 */
  background: linear-gradient(to bottom,  #1e5799 0%,#207cca 37%,#207cca 37%,#2989d8 50%,#7db9e8 100%); /* W3C, IE10+, FF16+, Chrome26+, Opera12+, Safari7+ */
  filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#1e5799', endColorstr='#7db9e8',GradientType=0 ); /* IE6-9 */

}

.glyphicon-file{
  text-shadow: 1px 1px 5px #010101;
}

#upload-form{
  padding: 10px;
}

.sr-only{
  text-shadow: 0px 0px 0px;
}

.glyphicon-refresh-animate, .glyphicon-ok{
  position: relative;
  top: 20px;
}

.glyphicon-ok{
  color: #5CB85C;
  text-shadow: 1px 1px 0px #000;
}

/* ANIMATE PROCESSING FILE */

.glyphicon-refresh-animate {
    -animation: spin .7s infinite linear;
    -webkit-animation: spin2 .7s infinite linear;
}
/*@-webkit-keyframes spin2 {
    from { -webkit-transform: rotate(0deg);}
    to { -webkit-transform: rotate(360deg);}
}*/

@keyframes spin {
    from { transform: scale(1) rotate(0deg);}
    to { transform: scale(1) rotate(360deg);}
}


Si lanzamos el servidor con el comando:

grunt serve

Podemos acceder en el navegador por http://localhost:9000 y elegir un archivo cualquiera y al subirlo nos encontraremos con el siguiente resultado:

Captura de pantalla 2016-01-30 a las 20.32.18
Es normal, ya que aún no hemos realizado la parte del servidor para manejar los archivos. En el siguiente capítulo nos centraremos en la realización de la parte del backend así como la implementación de los servicios REST para realizar las operaciones básicas en lo que a CRUD se refiere para la gestión de la subida de los archivos.

Tone

Ingeniero del Software y procrastinador sin remedio, interesado en todo lo que tenga que ver con el mundo del desarrollo web y la inteligencia artificial, no sé si seré el responsable de la creación de Skynet algún día pero se intenta.

ESCRIBIR UN COMENTARIO
  • (will not be published)

XHTML: Puedes usar estas etiquetas: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>