Guardando fotos - CGastrell/phonegap GitHub Wiki

Guardar las fotos que sacamos desde la aplicacion de Galeria utilizando los metodos anteriores. Trabajen sobre prueba12.

HTML

El archivo index.html queda asi (los botones que usamos para probar los archivos de texto no molestan, asi que los dejamos). No hay desafios aca.

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta name="format-detection" content="telephone=no" />
    <meta name="msapplication-tap-highlight" content="no" />
    <!-- WARNING: for iOS 7, remove the width=device-width and height=device-height attributes. See https://issues.apache.org/jira/browse/CB-4323 -->
    <meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height, target-densitydpi=device-dpi" />
    <link rel="stylesheet" type="text/css" href="css/jquery.mobile-1.4.5.min.css" />
    <script type="text/javascript" src="js/jquery.min.js"></script>
    <title>Galeria</title>
    <style>
      .picThumb {
        max-width: 20%;
      }
      img.foto {
        max-width: 100%;
      }
    </style>
  </head>
  <body>

    <div data-role="page" id="home">
      <div data-role="header">
        <h1>Galeria de fotos</h1>
      </div>
      <div data-role="content" class="galeria">
        <p class="fileContent"></p>
        <a href="#" id="grabarArchivo" class="ui-btn ui-btn-b">Grabar</a>
        <a href="#" id="leerArchivo" class="ui-btn ui-btn-b">Leer</a>
      </div>
      <div data-role="footer" data-position="fixed">
        <a href="#" id="sacarFoto" style="display:block" class="ui-btn ui-btn-b ui-icon-camera ui-btn-icon-top">Foto!</a>
      </div>
    </div>

    <div data-role="page" id="fotoView">
      <div data-role="header">
        <a href="#" class="ui-btn" data-rel="back">Volver</a>
        <h1>Foto</h1>
      </div>
      <div data-role="content">
        <img class="foto" src="" />
      </div>
    </div>

    <script type="text/javascript" src="cordova.js"></script>
    <script type="text/javascript" src="js/index.js"></script>
    <script type="text/javascript" src="js/jquery.mobile-1.4.5.min.js"></script>
    <script type="text/javascript">

    </script>
  </body>
</html>

Codigo

En el archivo js/index.js tenemos la inicializacion con Deferred's (pueden ir copiando y pegando los bloques de codigo, deberian terminar armando todo el archivo):

var deviceReady = $.Deferred();
var jqmReady = $.Deferred();
var documentReady = $.Deferred();

$(document).on("ready", function() {
  documentReady.resolve();
});

$(document).on("deviceready", function() {
  deviceReady.resolve();
});

$(document).on("mobileinit", function () {
  jqmReady.resolve();
});
//cuando los 3 se resuelvan, ejecutar init()
$.when(deviceReady, jqmReady, documentReady).then(init);

Inicializacion

La funcion init() inicializa la configuracion de jQuery Mobile, la funcionalidad de la camara y el fileApi que hicimos:

function init() {
  // Configuracion de JQM para phonegap
  $.mobile.allowCrossDomainPages = true;
  $.support.cors = true;
  $.mobile.buttonMarkup.hoverDelay = 0;
  $.mobile.pushStateEnabled = false;
  $.mobile.defaultPageTransition = "none";

  //inicializamos la funcion de la camara y el fileApi
  camara.initialize();
  fileApi.initialize();
}

NOTA: el objeto camara y fileApi pueden ubicarse en cualquier orden en el codigo mientras mantengan la integridad del objeto (estos bloques de codigo son demostrativos, no van en el archivo index.js):

  • el objeto se declara como una variable: var camara = {}
  • el objeto puede tener metodos dentro, separados por coma:
var camara = {
  metodo1: function() { alert('Hola'); },
  metodo2: function() { alert('Chau'); }
}
  • el ultimo metodo no debe llevar una coma cuando se cierra
  • los metodos son como variables dentro del objeto, pero se asignan con dos puntos (:), no con el signo igual (=)
var camara = {
  metodo1: function() { alert('metodo correcto'); },
  metodo2 = function() { alert('metodo erroneo, eleva un error y detiente la ejecucion') }
//^^^^^^^^^^^^^^^^^^^^
}

FIN DE NOTA


Camara

Luego tenemos este objeto que nos sirve para inicializar la camara de manera ordenada y reusable (voy a partir el objeto en cada metodo, pero pueden seguir copiando y pegando cada bloque en orden y deberia quedar bien):

Declaracion del objeto y metodo para inicializacion

var camara = {
  initialize: function() {
    if(localStorage.galeria) {
      $.each(JSON.parse(localStorage.galeria), function(i,e){
        camara.addPictureToGallery(e.path);
      });
    }else{
      localStorage.galeria = JSON.stringify([]);
    }
    $('#sacarFoto').on('click', camara.sacarFoto);
  },

Al inicializar, verificamos si existe localStorage.galeria. De ser asi lo parseamos y asumimos que es un array. Iteramos sobre este array y (este es uno de los cambios) por cada item del array sabemos/suponemos que tendremos un objeto {name: "nombreDelArchivo", path: "rutaAlArchivo"}. Sabiendo esto, llamamos a camara.addPictureToGallery(e.path) (donde e es el valor del item iterado).

Si localStorage.galeria no existe, le asignamos un array vacio convertido en texto con el metodo JSON.stringify.

Por ultimo, asignamos al boton $('#sacarFoto') la funcion camara.sacarFoto cuando surja el evento click.


sacarFoto

  sacarFoto: function() {
    var options = {
      destinationType : Camera.DestinationType.DATA_URL,
      sourceType : Camera.PictureSourceType.CAMERA,
      saveToPhotoAlbum: true
    };

    navigator.camera.getPicture(camara.onDataUrlSuccess, camara.onError, options);
  },

El metodo sacarFoto llama a navigator.camera.getPicture con un set simple de opciones. Como callback pasa camara.onDataUrlSuccess


onDataUrlSuccess

  onDataUrlSuccess: function(imageData) {
    //en cuanto empieza esta funcion
    //mostramos un "loading" o spinner
    $.mobile.loading('show');

    //traigo la galeria del localStorage y la parseo
    var fotos = JSON.parse(localStorage.galeria);

    //genero un id para la imagen, sirve de filename tambien
    var id = 'img_' + parseInt(Math.random() * 1000000);

    //escribo la imagen y a la vuelta agrego el id:path al storage
    fileApi.writeJpeg(id + '.jpg', imageData, function(jpegPath){
      //con el id y el path, hago un objeto y lo agregamos al array
      fotos.push({name:id, path: jpegPath});
      
      //por ultimo convertimos el array en string
      //y lo volvemos a almacenar en localStorage
      localStorage.galeria = JSON.stringify(fotos);

      //agregamos la imagen a la galeria (HTML)
      camara.addPictureToGallery(jpegPath);
    });
  },

El metodo onDataUrlSuccess es el callback asignado para cuando sacamos la foto. Recibe como parametro el archivo JPG codificado en base64.

Luego actualiza el registro de fotos en localStorage.galeria, guarda la foto y la agrega a la galeria (HTML):

  • Parsea el valor de localStorage.galeria en un array fotos
  • Genera un id img_ con un numero al azar concatenado
  • Llama a fileApi.writeJpeg pasandole
    • El id generado (ej: img_823391) y la extension ".jpg" como nombre de archivo
    • El archivo codificado en base64 como contenido
    • Una funcion para el callback que
      • Recibe el path donde queda guardado el archivo
      • Agrega un objeto al array fotos con:
        • name: id generado
        • path: la ruta recibida por la funcion
      • Vuelve a convertir el array fotos con JSON.stringify y lo almacena en localStorage.galeria
      • Llama a camara.addPictureToGallery pasandole el path donde queda guardado el archivo

addPictureToGallery y onError

  addPictureToGallery: function(image) {
    var pic = $('<img />')
      .addClass('picThumb')
      .attr('src', image)
      .appendTo('.galeria');

    pic.click(function(e){
      $('#fotoView img').first().attr('src',image);
      $('body').pagecontainer('change','#fotoView');
    });
    $.mobile.loading('hide');
  },
  onError: function(message){
    console.log(arguments);
    alert('Error: ' + message);
  }
}

addPictureToGallery genera un tag/elemento HTML <img />, le agrega la clase picThumb, le agrega el attributo src con el path a la imagen (recibida como parametro en la funcion) y agrega el elemento al elemento que tiene clase .galeria.

A este mismo <img /> le asigna una funcion al evento click:

  • Buscar un img dentro de #fotoView (la pagina que usamos para ver las fotos)
  • Como puede devolver mas de uno, pedimos el primero con .first()
  • A ese img le cambia el valor del attributo src por el del path de la foto (insertando efectivamente la imagen en la pagina #fotoView)
  • Navega a la pagina #fotoView (por codigo)

Y por ultimo esconde el loading/spinner que mostramos cuando iniciamos el proceso, alla por onDataUrlSuccess.


fileApi

El objeto que generamos para ayudarnos con la lectura y grabacion de archivos. Este lo tienen mas fresquito, asi que no voy tan detallado.

var fileApi = {
  initialize: function(){
    window.resolveLocalFileSystemURL(cordova.file.externalDataDirectory, fileApi.onDir, fileApi.onError);

    $('#grabarArchivo').click(function(){
      fileApi.writeTextFile('test.txt', 'texto de prueba', function(data){
        alert('Escribi '+data+' en test.txt');
      });
    });

    $('#leerArchivo').click(function(){
      fileApi.readTextFile('test.txt', function(data){
        $('.fileContent').text('Contenido del archivo: '+data);
      });
    });
  },
  onDir: function(directoryEntry) {
    fileApi.dir = directoryEntry;
  },
  onError: function(err) {
    alert(err.code);
  },
  writeTextFile: function(file, content, callback) {
    var onFile = function(fileEntry) {
      fileEntry.createWriter(function(fileWriter){
        fileWriter.write(content);
        callback && callback(content);
      }, fileApi.onError);
    }
    fileApi.dir.getFile(file, {create: true}, onFile, fileApi.onError);
  },
  readTextFile: function(file, callback) {
    var onFile = function(fileEntry) {
      //convierte el fileEntry en un fileObject
      fileEntry.file(function(fileObject){
        var reader = new FileReader();
        reader.onloadend = function(){
          callback && callback(this.result);
        }
        reader.readAsText(fileObject);
      });
    }
    fileApi.dir.getFile(file, {create:false}, onFile, fileApi.onError);
  },
  writeJpeg: function(fileName, base64content, callback) {
    var onFile = function(fileEntry) {
      fileEntry.createWriter(function(fileWriter){
        var blob = b64toBlob(base64content,'image/jpeg');
        fileWriter.onwriteend = function(){
          console.log(this);
          callback && callback(fileEntry.nativeURL);
        }
        fileWriter.write(blob);
      }, fileApi.onError);
    }
    fileApi.dir.getFile(fileName, {create: true}, onFile, fileApi.onError);
  }
}

b64toBlob

function b64toBlob(b64Data, contentType, sliceSize) {
  contentType = contentType || '';
  sliceSize = sliceSize || 512;

  var byteCharacters = atob(b64Data);
  var byteArrays = [];

  for (var offset = 0; offset < byteCharacters.length; offset += sliceSize) {
    var slice = byteCharacters.slice(offset, offset + sliceSize);

    var byteNumbers = new Array(slice.length);
    for (var i = 0; i < slice.length; i++) {
        byteNumbers[i] = slice.charCodeAt(i);
    }

    var byteArray = new Uint8Array(byteNumbers);

    byteArrays.push(byteArray);
  }

  var blob = new Blob(byteArrays, {type: contentType});
  return blob;
}

Esta es la funcion que convierte el archivo codificado en base64 a Blob. Deberian tenerla al menos al final del archivo

Consigna

Entender el codigo e implementarlo en la aplicacion. Copien y peguen, drive safe. Traten de ver las imagenes/archivos de texto a traves del explorador de windows (como para verificar).

Para entretenerse:

https://github.com/AllThingsSmitty/jquery-your-mom-should-know

⚠️ **GitHub.com Fallback** ⚠️