Jonathan Perry

Fullstack developer that likes making stuff.

How To Make an Image Uploading App Using Vue, Quasar, Firebase Storage and Cordova [Part 1]

What we’re building

https://www.youtube.com/watch?v=Dhx1Wu6Gzu8

We’ll build a cross-platform mobile app for taking photos and uploading to firebase.
In Part 1, we’ll take a picture and save it to Firebase Cloud Storage, and then show it in our app. In Part 2, we’ll offload the uploading work, and use the blueimp library to generate a thumbnail locally and show it while uploading.

The stack

  • Vue JS — Component framework
  • Cordova — Cross platform mobile framework
  • Quasar — UI framework (and CLI)
  • Firebase Cloud Storage — For storing the photos
  • Web Workers — For offloading the uploading to a separate thread

Scaffolding

We’ll use Quasar CLI to initialize a new project, and run the cordova mode (android or ios) to see the app running on your connected device.
quasar create vue-firebase-image-upload 
cd vue-firebase-image-upload 
quasar dev -m android
You’ll have to add https: true in the devServer section of quasar.conf.js if running on android > 9
You should get a basic working version that looks like this:
This is a good time to make your first commit.
For more details on how to install and setup quasar, see this post

Taking a picture and getting base64

The way to store images in Firebase Cloud Storage is by saving the base-64 string of the image, using Firebase’s 
putString 
method. Notice that you have to remove the base64 prefix from the string before uploading, or Firebase will reject the string.
To begin, we'll add a button that takes a picture using Cordova's camera plugin, and print the base64 string.

Adding plugins

First, add cordova camera plugin and file plugin:
cd src-cordova 
cordova plugin add cordova-plugin-file 
cordova plugin add cordova-plugin-camera

Taking the picture

In order to take a picture, we’ll add some code in a new service file 
src/services/cordova-camera.js
:
async function getCameraFileObject() {
  return new Promise((resolve, reject) => {

    let camera = navigator.camera;

    const options = {
      quality: 50,
      destinationType: camera.DestinationType.FILE_URI,
      encodingType: camera.EncodingType.JPG,
      mediaType: camera.MediaType.PICTURE,
      saveToPhotoAlbum: true,
      correctOrientation: true
    };

    camera.getPicture(imageURI => {
      window.resolveLocalFileSystemURL(imageURI,
        function (fileEntry) {
          fileEntry.file(
            function (fileObject) {
              resolve(fileObject)
            },
            function (err) {
              console.error(err);
              reject(err);
            }
          );
        },
        function () { }
      );
    },
      console.error,
      options
    );
  })

}

async function getBase64FromFileObject(fileObject) {
  return new Promise((resolve, reject) => {
    var reader = new FileReader()
    reader.onloadend = function (evt) {
      var image = new Image()
      image.onload = function (e) {
        resolve(evt.target.result)
      }
      image.src = evt.target.result
    }
    reader.readAsDataURL(fileObject)
  })
}

async function getBase64FromCamera() {
  let fileObject = await getCameraFileObject();
  let base64 = await getBase64FromFileObject(fileObject);
  return base64;
}

export default {
  getBase64FromCamera
}
This service is doing several steps:
  1. Takes a picture using Cordova’s camera plugin. This returns an image URI.
  2. Gets a file entry using Cordova’s file plugin, with 
    resolveLocalFileSystemURL 
    function.
  3. Gets a File object using the file method.
  4. Uses FileReader to get the base64 representation of the file.
Now let’s add a simple event bus. Add the file 
src/services/event-bus.js
 with content:
import Vue from 'vue';
export const EventBus = new Vue();
Now in 
src/layouts/MyLayout.vue
, we'll add a button in the toolbar for taking a picture, and use our event bus to send the handling to 
Index.vue
:
<template>
  ...

  <q-btn flat dense round @click="takePicture">
    <q-icon name="camera" />
  </q-btn>

  ...
</template>

<script>
  import { EventBus } from "../services/event-bus.js";

  export default {
    ...

    methods: {
      takePicture() {
        EventBus.$emit('takePicture')
      }
    }
  }
  </script>
Finally we’ll catch the takePicture event in our main component: 
Index.vue
, call the 
cordova-camera.js
 service function and print the base64 result (which we'll later upload to firebase):

In 
Index.vue
 file in the <script> section:
import { EventBus } from "../services/event-bus";
  import cordovaCamera from "../services/cordova-camera";
  
  export default {
    name: "PageIndex",
    mounted() {
      EventBus.$off("takePicture");
      EventBus.$on("takePicture", this.uploadImageFromCamera);
    },
    methods: {
      async uploadImageFromCamera() {
        let base64 = await cordovaCamera.getBase64FromCamera();
        console.log("base64", base64)
      }
    }
  };
Running with 
quasar dev -m android
, and looking at the chrome dev tools, we can see the base64 output as a long string printed in the console:

Adding firebase


Web SDK or native SDK?

When using Firebase in a Cordova app, you can choose between using the Web SDK and a cordova plugin the wraps the Native SDKs. The state of the current Firebase cordova plugins seems a little unstable to me, not fully supporting Firebase Cloud Storage yet.
And with the ability to offload work to a web worker, we can use the full-featured official Web SDK, and not take the performance hit.
Add the firebase sdk using yarn:
yarn add firebase

Firebase setup

Follow the instructions in the Firebase Cloud Storage setup page. After the setup you should have a firebase project with Storage enabled. We’ll save the settings in a separate file: 
firebase-config.js
 (this file won't be committed to our repo).
The contents should look something like this:
export default {
    firebase: {
        apiKey: '<your-api-key>',
        authDomain: '<your-auth-domain>',
        databaseURL: '<your-database-url>',
        storageBucket: '<your-storage-bucket-url>'
    }
}

Uploading The Image

Now that we have Firebase settings in place, we’ll add another service for handling Firebase app initialization and uploading.
Add the file 
src/services/cloud-storage.js
 with content:
import * as firebase from "firebase/app";
import 'firebase/storage';
import firebaseConfig from './firebase-config';

async function uploadBase64(imageData, storageId) {
    return new Promise((resolve, reject) => {
        let uploadTask = firebase.storage().ref().child(storageId).putString(imageData, "base64");
        uploadTask.on(
            "state_changed",
            function (snapshot) {},
            function (error) {
                reject(error)
            },
            function () {
                uploadTask.snapshot.ref
                    .getDownloadURL()
                    .then(function (downloadURL) {
                        console.log("Uploaded a blob or file!");
                        console.log("got downloadURL: ", downloadURL);
                        resolve(downloadURL);
                    });
            }
        );
    });
}

function initialize() {
    firebase.initializeApp(firebaseConfig.firebase) 
}

export default {
    uploadBase64,
    initialize
}
The uploadBase64 function uploads imageData (the base64 string) using the putString method, under the Firebase child of storageId. You can have any ID you want, here I've chosen to use the unix datetime number as the child ID. After the uploading is done, we return the image URL using getDownloadURL function of the uploading task.
In our main 
App.vue
 file we'll call the initialize function when the app is mounted:
import cloudStorage from './services/cloud-storage'
export default {
    name: 'App',
    mounted() {
      cloudStorage.initialize();
    }
}
Finally we’ll call the uploading function from
 Index.vue
.
This is now the content of 
Index.vue
:
<template>
  <q-page>
    <div class="row justify-center q-ma-md" v-for="(pic, idx) in pics" :key="idx">
      <div class="col">
        <q-card>
          <q-img spinner-color="white" :src="pic" />
        </q-card>
      </div>
    </div>
  </q-page>
</template>

<script>
import { EventBus } from "../services/event-bus";
import cordovaCamera from "../services/cordova-camera";
import cloudStorage from "../services/cloud-storage";

export default {
  name: "PageIndex",
  data() {
    return {
      pics: []
    };
  },
  mounted() {
    EventBus.$off("takePicture");
    EventBus.$on("takePicture", this.uploadImageFromCamera);
  },
  methods: {
    removeBase64Prefix(base64Str) {
      return base64Str.substr(base64Str.indexOf(",") + 1);
    },

    async uploadImageFromCamera() {
      const base64 = await cordovaCamera.getBase64FromCamera();
      const imageData = this.removeBase64Prefix(base64);
      const storageId = new Date().getTime().toString();
      const uploadedPic = await cloudStorage.uploadBase64(imageData, storageId);
      this.pics.push(uploadedPic);
    }
  }
};
</script>
We’ve added the pics array to our component's data, containing URLs of all the pictures we uploaded. We've added a v-for that will show each picture in a quasar 
q-img
 component inside a 
q-card
 component. We've also changes 
uploadImageFromCamera 
to take a picture, get the base64 string (stripping the prefix so Firebase won't freak out), calculated the storageId using the current datetime, and called the uploading function. After it's uploaded, we add the resulting URL to the pics array.
And we can now see the uploaded image:
The full code is on GitHub.
In the next part of the series we’ll move tasks to a web worker, add a loading spinner, and save the URLs in local storage.
Stay tuned!

Tags

Comments

More by Jonathan Perry

Topics of interest