Building a PWA Music Player Part 3: Branding and OS File Handling
Tags: pwa, vanillajs, webdev, webcomponents
Published: 2021-02-15 18:54:26


# Cleanup

I did some small updates to the UI to look more like an app with a full-screen player.  Also got rid of the cache bust button since we don't really need it right now.

# Manifest.json

Most of the branding is done via the `manifest.json` as well a few other PWA features including file handling.  You can add one you your page by specifying the path to it in a link tag:

```html
<link rel="manifest" href="manifest.json">
```

The file itself can located anywhere and is just some simple JSON data.

# Icons

Let's start with perhaps the most important branding part of the PWA, icons.  PWA icons can on paper be SVGs in most modern browsers however when I went to upgrade some of my boilerplate I found this was not working quite as expected.  I was really hoping for it to work because I don't want to have a bunch of different images for different sizes.  Unfortunately as of this writing, Chrome does not properly support SVG images as icons.  If you add them you will get warnings in the application panel about them not being the correct size. Given this it's also not surprising that space delimited sizes and the `any` keyword also don't work.
Instead we'll need to use PNGs and we'll specifically need two sizes: 192x192 and 512x512. These are the two sizes that Chrome can use to scale the others it needs to display.  You can add your own if you'd like more manual control. 

Also we need at least one other icon, the favicon!  Most modern browsers support SVGs so I suggest using that but otherwise a 32x32 ico is most compatible.

```html
<link rel="icon" href="img/icon512.svg" type="image/xml+svg">
```
I really wish I could stop using this tag and have it default to the manifest.json.

Speaking of things that need to use the manifest.json, optionally you can also add Apple specific meta tags for `apple-touch-icon` etc as well to improve the iOS experience. 

```html
<link rel="apple-touch-icon" href="img/icon192.png">
```

# Other manifest values

Let's look at the manifest:

```js
//manifest.json
{
  "name": "Music Player",
  "short_name" :"Music Player",
  "icons": [
    {
      "src": "img/icon512.png",
      "sizes": "512x512",
      "type": "image/png"
    },
    {
      "src": "img/icon192.png",
      "sizes": "192x192",
      "type": "image/png"
    }
  ],
  "theme_color": "#ff6400",
  "background_color": "#9E9E9E",
  "start_url": "index.html",
  "display": "standalone",
  "orientation": "portrait"
}
```

`icon192` and `icon512` are the respective icon sizes.  You can optionally specify the `purpose` key per image.  Values be `any` or `maskable`.  These let you specify if it's maskable (ie "can I cut the corners off?").  I'm not going to worry about that as my icon should be okay, but if you need a special icon for that then you can add it along with the rest.

The other stuff here should be relatively straightforward. `name` is the name of the app while `short_name` might be used for the icon where there is less space.  `theme_color` is the color the title bar takes in Android.  Typically this will be primary color of your app.  `background_color` is the color of the splash screen but browsers and OSs can use these values for whatever they want.  `start_url` is the url that your app starts on when opened.  This is important, it has to be a real url that the service worker can respond to, if not then you can't get an install prompt.  `orientation` sets which type of display you want it to have `portrait` or `landscape` the latter might be useful if you are making a game.  `display` is an interesting one that basically let's you set the browser chrome.  `standalone` is the most typical if you are making a native looking app without default forward/back url bar, or `fullscreen` if you want fullscreen.  The other values like `browser` will just make it a normal tab or `minimal-ui` will give you some browser ui.

# File handling

Now that we have the manifest we can move on to file handling.  This will allow us to be used like a real app when you double click a file!  We just need to add this key to manifest.json:

```js
//manifest.json
{
//...
  "file_handlers": [
    {
      "action": "index.html",
      "accept": {
        "audio/mp3": ".mp3"
      }
    },
    {
      "action": "index.html",
      "accept": {
        "audio/mp4": ".m4a"
      }
    }
  ],
//...
}
```
We can register multiple handlers.  Each has an action and a collection of MIME types that it should accept.  The MIME types also include extensions (and you should include the ".").  Since we're a music player `audio/mp3` is a good one to support.  We could probably do mp4a but it really depends on what the browser supports.  Since I'm not maintaining my own codecs right now I'm going to be a bit conservative and not include exotic types like FLAC or OGG.  The "action" is the page that will launch when you open that file type.  You probably want to make sure that your service-worker can serve that page offline.

## Launch Queue

The launch queue is a property on window that exists for browsers that can support file handling.  It's pretty new (at least at the time of writing) so let's do feature detection:

```js
if ("launchQueue" in window) {
  launchQueue.setConsumer(async launchParams => {
  }
}
```
The `launchParams` will have the list of files that were sent.

In our app we'll just add a little bit of code to pass the file to the music player on load:

```js
//app.js
const musicPlayer = document.querySelector("wc-music-player");
if ("launchQueue" in window) {
	launchQueue.setConsumer(async launchParams => {
		if (launchParams.files.length) {
			musicPlayer.loaded.then(() => musicPlayer.addFiles(launchParams.files, true));
		}
	});
}
```
In order for this to work we need to build some extra features into `WcMusicPlayer`.  The first is to have a way to tell when the element is loaded.  This state is different from `ready` which we used earlier to determine if it has file system access to play file (in retrospect the component itself dealing with the file picker was a bad idea).  `loaded` will be to see if the element has been inserted and has a complete shadow DOM.

We'll add the `loaded` property in the constructor.
```js
//wc-music-player
#setLoaded;
constructor() {
	super();
	this.#fileLinks = new WeakMap();
	this.bind(this);
	this.loaded = new Promise((res,rej) => this.#setLoaded = res)
}
```
I then use a little trick to externalize the resolve callback.  Normally you don't have access to it from the outside but if we assign it to `#setLoaded` then we can resolve this promise somewhere else.  That somewhere else is right after we add the event handlers:

```js
//wc-music-player
async connectedCallback() {
	this.#storage = new IdbStorage({ siloName: "file-handles" });
	this.#handle = await this.#storage.get("handle");
	this.render();
	this.cacheDom();
	this.attachEvents();
	this.#setLoaded(); //loaded!
	if(this.#handle){
		this.getFiles();
	}
}
```
This makes sure that we don't try to add elements to the track list before it exists.

Next I've extracted the file adding from `getFiles` into it's own method `addFiles`:

```js
addFiles(files, shouldPlay = false){
	this.isReady = true;
	const docFrag = document.createDocumentFragment();
	files.forEach(f => {
		this.#files.push(f);
		const li = document.createElement("li");
		li.textContent = f.name;
		docFrag.appendChild(li);
		this.#fileLinks.set(li, f);
	});
	this.dom.list.appendChild(docFrag);
	if(shouldPlay){
		files[0].getFile().then(f => this.playFile(f));
	}
}
```
It's the same thing though I've made a few changes.  Now adding files either internally or externally will set the player state to ready.  It also take a flag `shouldPlay`.  This is a UI choice.  When we add thing we might want to play them right away, such as when we use the file handler, but we might also want to just add them to the playlist silently, especially if the user already has something playing.  This gives us that choice.

`getFiles` was rewritten to call `addFiles`:

```js
async getFiles(){
	this.addFiles(await collectAsyncIterableAsArray(filterAsyncIterable(this.#handle.values(), f => 
		f.kind === "file" && (f.name.endsWith(".mp3") || f.name.endsWith(".m4a")
		))));
}
```
Also `playFile` makes playing files simple:

```js
//this is the actual file, not a file handle!
playFile(file){
	const url = URL.createObjectURL(file);
	this.dom.audio.src = url;
	this.dom.title.textContent = file.name;
	this.togglePlay(true);
}
```
and `selectTrack` will use that now:

```js
async selectTrack(e){
	const fileHandle = this.#fileLinks.get(e.target);
	if(fileHandle){
		const file = await fileHandle.getFile();
		this.playFile(file);
	}
}
```
So now, if we have files in the `launchParams` they are inserted into the playlist and the first one starts playing!

You can now right-click files to play them in the music player:

![Screenshot 2021-02-13 223415](https://dev-to-uploads.s3.amazonaws.com/i/2ruxskzgxsduibxvhvr5.png)

You can see the Music Player option with the terrible icon.  You could even make this the default player! 

# Caveats

If you decide to add new types later you need to re-install the app for them to appear.  Also, because other means of adding files are not related to the file picker API we cannot persist them into the list as we will lose access to them next time.

Code is available here: https://github.com/ndesmic/music-player/tree/v0.4