How to make an image uploading app with Vue, Quasar, Firebase Storage and Cordova - Part 2
Tags: vue, cordova, firebase, images
Published: 2019-10-13 00:00:00

---
date: "2019-10-13"
title: "How to make an image uploading app with Vue, Quasar, Firebase Storage and Cordova - Part 2"
published: true
category: "vue"
tags: "vue, cordova, firebase, images"
canonical_url: "https://www.learningsomethingnew.com/how-to-make-an-image-uploading-app-with-vue-quasar-firebase-storage-and-cordova-part-2"
cover_image: "https://www.learningsomethingnew.com/vue-fb-image-upload/cover.jpg"
---

## What we're building

{% youtube Dhx1Wu6Gzu8 %}

We'll build a cross-platform mobile app for taking photos and uploading to firebase. 

In [Part 1](https://www.learningsomethingnew.com/how-to-make-an-image-uploading-app-with-vue-quasar-firebase-storage-and-cordova-part-1), we saw how to take a picture and save it to Firebase Cloud Storage.
In this post we'll move the uploading to a separate thread via web worker, and use the blueimp library to generate a thumbnail locally and show it while uploading.


## Web Workers

### What is a web worker

> Web Workers are a simple means for web content to run scripts in background threads. The worker thread can perform tasks without interfering with the user interface. ([mozilla.org](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers))

So we'll use a web worker to offload code that can potentially block our UI thread - in our case the Firebase Storage code - to another thread.

### Configuring workerize-loader

We'll use [workerize-loader](https://github.com/developit/workerize-loader) to make using web workers a little easier (web workers interface is a little weird).  

```sh
yarn add workerize-loader
```

We need to add some webpack configuration to tell webpack to use `workerize-loader`. In `quasar.conf.js` in the `build` section add:

```js
extendWebpack(cfg) {
    cfg.module.rules.push({
        test: /\.worker\.js$/,
        loader: 'workerize-loader',
    })
},

chainWebpack (chain, { isServer, isClient }) {
    chain.output.globalObject('self')
}
```

This tells webpack to load the file `worker.js` using `workerize-loader`.

### Moving code to the worker

Let's add the file `src/services/worker.js` and move our uploading code into it:

```js
import cloudStorage from './cloud-storage'

export async function uploadPicture(imageData) {
    let storageId = new Date().getTime().toString();

    let downloadURL = await cloudStorage.uploadBase64(
        imageData,
        storageId
    );

    return {
        storageId,
        downloadURL,
    }
}

export function initFB() {
    cloudStorage.initialize();
}

export async function deletePic(storageId) {
    await cloudStorage.deleteFromStorage(storageId)
}

```

We've also added a `deletePic` call which we'll see later in `cloudStorage.js`.


## Adding simple state management

We'll want to create an instance of the worker and save it in the app's state. We don't have state management yet, so let's add a simple state management pattern using a `store.js` file:

```js
import worker from "workerize-loader!./worker.js";

var store = {
    debug: true,
    state: {
        pics: [],
        uploading: false
    },
    initWorker() {
        this.state.workerInstance = worker();
        this.state.workerInstance.initFB();
    },
    async loadPictures() {
        if (this.debug) console.log("loadPictures triggered")
        let picsJson = await localStorage.getItem("pics");
        if (!picsJson) this.state.pics = [];
        else this.state.pics = JSON.parse(picsJson);
        this.state.pics = this.state.pics.filter(pic => !pic.uploading)
    },
    async addPic(pic) {
        if (this.debug) console.log("addPic triggered with", pic)

        pic.failed = false;
        this.state.pics.splice(0, 0, pic);
        localStorage.setItem("pics", JSON.stringify(this.state.pics));
    },
    async deletePic(idx) {
        if (this.debug) console.log("deletePic triggered with", idx)

        this.state.pics[idx].uploading = true;

        if (this.state.pics[idx].storageId) {
            await this.state.workerInstance.deletePic(this.state.pics[idx].storageId)
        }
        this.state.pics.splice(idx, 1);
        localStorage.setItem("pics", JSON.stringify(this.state.pics));
    },
    async updatePicUploaded(oldPic, newPic) {
        if (this.debug) console.log("updatePicUploaded triggered with", oldPic, newPic)

        oldPic.uploading = false
        oldPic.url = newPic.downloadURL
        oldPic.storageId = newPic.storageId
        oldPic.width = newPic.width
        oldPic.height = newPic.height

        localStorage.setItem("pics", JSON.stringify(this.state.pics));
    },
    async updatePicFailed(pic) {
        if (this.debug) console.log("updatePicFailed triggered with", pic)

        pic.failed = true
    },
}

export default {
    ...store
}
```

A few things going on here:
* We're importing our worker using the prefix `workerize-loader!` which tells webpack to use the loader we configured earlier.
* We moved the `pics` collection to the `state` object from `Index.vue`.
* We exposed the method `initWorker` which initializes the worker instance.
* We added some CRUD methods for persisting the `pics` collection in `localStorage`: `addPic`, `loadPictures` and `delete`.
* `updatePicUploaded` and `updatePicFailed` changes the `loading` property of the picture. We'll use this to show the spinner.

## Generating thumbnails

We're going to generate a thumbnail (on the client) to show while the image is uploading. We'll use the [blueimp](https://www.npmjs.com/package/blueimp-load-image) library for this:

```sh
yarn add blueimp-load-image
```

Let's add another service for manipulating images: `src/services/image-ops.js`:

```js
import loadImage from 'blueimp-load-image'

const base64JpegPrefix = "data:image/jpeg;base64,";

function removeBase64Prefix(base64Str) {
    return base64Str.substr(base64Str.indexOf(",") + 1);
}

function addBase64Prefix(imageData) {
    return base64JpegPrefix + imageData
}

async function generateThumbnail(imageData, maxWidth) {
    return new Promise(async resolve => {
        let url = base64JpegPrefix + imageData
        let res = await fetch(url)
        let blob = await res.blob()

        loadImage(
            blob,
            (canvas) => {
                let dataURL = canvas.toDataURL('image/jpeg');
                resolve(removeBase64Prefix(dataURL));
            }, {
                maxWidth: maxWidth,
                canvas: true
            }
        );
    });
}


export default {
    removeBase64Prefix,
    generateThumbnail,
    addBase64Prefix,
};

```

Notice we've moved the `removeBase64Prefix` and `addBase64Prefix` methods here.
The `generateThumbnail` function takes imageData - a base64 string, uses the `fetch` API, converts it to a `blob`, and then uses blueimp's `loadImage` to change it's size to `maxWidth`.
We're loading `maxWidth` from a new config file we've added to make things tidy - `src/services/config.js`:

```js
export default {
    photos: {
        maxWidth: 1000,
        thumbnailMaxWidth: 30
    }
}
```

## Adding the image-uploader service

We'll extract all the image uploading flow to a service to keep our UI component clean. Add the file `src/services/image-uploader.js`:

```js
import store from "./store";
import cordovaCamera from "./cordova-camera";
import imageOps from "./image-ops";
import config from "./config";


async function uploadImageFromCamera() {
    let base64 = await cordovaCamera.getBase64FromCamera();

    let imageData = imageOps.removeBase64Prefix(base64);

    let thumbnailImageData = await imageOps.generateThumbnail(
        imageData,
        config.photos.thumbnailMaxWidth
    );

    let localPic = {
        url: imageOps.addBase64Prefix(thumbnailImageData),
        uploading: true
    };
    store.addPic(localPic);

    let uploadedPic = await store.state.workerInstance.uploadPicture(
        imageData
    );
    store.updatePicUploaded(localPic, uploadedPic);
}

export default {
    uploadImageFromCamera
}
```

What we're doing in the `uploadImageFromCamera` method is: getting base64 from the cordova camera -> generating thumbnails using our `imageOps` -> generating a new `pic` object in our `state` with `uploading=true` -> uploading the picture to Firebase using the web worker -> updating the pic's `uploading` state property when uploading is finished.

## Putting it together

Now we've added all these services, we need to call them from our components.

In `App.vue`, we'll use the `mounted` lifecycle hook to initialize the worker and load the saved picture urls from the saved state:

```js
<template>
  <div id="q-app">
    <router-view />
  </div>
</template>

<script>
import store from "./services/store.js";

export default {
  name: "App",
  async mounted() {
    store.initWorker();
    store.loadPictures();
  }
};
</script>

<style>
</style>

```

Finally, we'll add the interface for viewing all this in `Index.vue`:


```html
<template>
  <q-page class>
    <div class="q-pa-md">
      <div class="row justify-center q-ma-md" v-for="(pic, idx) in pics" :key="idx">
        <div class="col">
          <q-card v-if="pic">
            <q-img spinner-color="white" :src="pic.url">
              <div class="spinner-container" v-if="pic.uploading && !pic.failed">
                <q-spinner color="white" size="4em" />
              </div>
              <div class="spinner-container" v-if="pic.failed">
                <q-icon name="cloud_off" style="font-size: 48px;"></q-icon>
              </div>
            </q-img>
            <q-card-actions align="around">
              <q-btn flat round color="red" icon="favorite" @click="notifyNotImplemented()" />
              <q-btn flat round color="teal" icon="bookmark" @click="notifyNotImplemented()" />
              <q-btn
                flat
                round
                color="primary"
                icon="delete"
                @click="deletePic(idx)"
                :disable="pic.uploading"
              />
            </q-card-actions>
          </q-card>
        </div>
      </div>
    </div>
  </q-page>
</template>

<style scoped>
.spinner-container {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
}

</style>

<script>
import store from "../services/store";
import { EventBus } from "../services/event-bus";
import imageUploader from "../services/image-uploader";

export default {
  name: "PageIndex",
  data() {
    return {
      state: store.state
    };
  },
  mounted() {
    EventBus.$off("takePicture");
    EventBus.$on("takePicture", this.uploadImageFromCamera);
  },
  computed: {
    pics() {
      return this.state.pics;
    }
  },
  methods: {
    notifyNotImplemented() {
      this.$q.notify({ message: "Not implemented yet :/" });
    },
    async deletePic(idx) {
      try {
        await store.deletePic(idx);
        this.$q.notify({ message: "Picture deleted." });
      } catch (err) {
        console.error(err);
        this.$q.notify({ message: "Delete failed. Check log." });
      }
    },
    async uploadImageFromCamera() {
      try {
        imageUploader.uploadImageFromCamera();
      } catch (err) {
        console.error("Uploading failed");
        console.dir(err);
        store.updatePicFailed(localPic);
        this.$q.notify({ message: "Uploading failed. Check log." });
      }
    }
  }
};
</script>

```

What's going on here:
* We've added a `q-spinner` to show when the `uploading` property of the picture is true.
* We're calling `imageUploader.uploadImageFromCamera` when clicking on the photo button, which handles our uploading.
* We've added some actions to the `q-card` of the picture (only delete is implemented for now)

## Final app

That's it! We have an image uploading app that shows a blurred thumbnails with a spinner, a little like WhatsApp's image upload.
Running all this using `quasar dev -m android/ios` will show the final result:

{% youtube Dhx1Wu6Gzu8 %}

## Further improvements

Some ideas for improving this app further would be:

- Offload thumbnail creation to another web worker
- Handle long lists of images with a virtual list component like [this one](https://github.com/tangbc/vue-virtual-scroll-list)
- Add a carousel / gallery component for viewing images full-size
- Add a retry mechanism for failed uploads
- Use [firebase auth](https://firebase.google.com/docs/auth) - so that users can only delete their own photos

## Source code

The full code is on GutHub here: [vue-firebase-image-upload](https://github.com/syonip/vue-firebase-image-upload).

Enjoy :)