<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Paul Kinlan</title>
    <description>The latest articles on DEV Community by Paul Kinlan (@paul_kinlan).</description>
    <link>https://dev.to/paul_kinlan</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F66453%2Fa38f58a9-ed01-4b9e-839c-0c7c2a7c568e.png</url>
      <title>DEV Community: Paul Kinlan</title>
      <link>https://dev.to/paul_kinlan</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/paul_kinlan"/>
    <language>en</language>
    <item>
      <title>Creating a commit with multiple files to Github with JS on the web</title>
      <dc:creator>Paul Kinlan</dc:creator>
      <pubDate>Fri, 24 May 2019 11:10:02 +0000</pubDate>
      <link>https://dev.to/chromiumdev/creating-a-commit-with-multiple-files-to-github-with-js-on-the-web-3mfm</link>
      <guid>https://dev.to/chromiumdev/creating-a-commit-with-multiple-files-to-github-with-js-on-the-web-3mfm</guid>
      <description>&lt;p&gt;My site is &lt;a href="https://github.com/PaulKinlan/paul.kinlan.me"&gt;entirely static&lt;/a&gt;. It’s built with &lt;a href="https://gohugo.io"&gt;Hugo&lt;/a&gt; and hosted with &lt;a href="https://zeit.co"&gt;Zeit&lt;/a&gt;. I’m pretty happy with the setup, I get near instant builds and super fast CDN’d content delivery and I can do all the things that I need to because I don’t have to manage any state.&lt;/p&gt;

&lt;p&gt;I’ve created a &lt;a href="https://github.com/PaulKinlan/paul.kinlan.me/tree/main/static/share/image"&gt;simple UI&lt;/a&gt; for this site and also my &lt;a href="https://github.com/PaulKinlan/podcastinabox-editor"&gt;podcast creator&lt;/a&gt; that enables me to quickly post new content to my statically hosted site.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--3BgIiU50--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://paul.kinlan.me/images/2019-05-24-creating-a-commit-with-multiple-files-to-github-with-js-on-the-web-0.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--3BgIiU50--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://paul.kinlan.me/images/2019-05-24-creating-a-commit-with-multiple-files-to-github-with-js-on-the-web-0.jpeg" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So. How did I do it?&lt;/p&gt;

&lt;p&gt;It’s a combination of Firebase Auth against my Github Repo, EditorJS to create edit the content (it’s neat) and Octokat.js to commit to the repo and then Zeit’s Github integration to do my hugo build. With this set up, I am able to have an entirely self hosted static CMS, similar to how a user might create posts in a database backed CMS like Wordpress.&lt;/p&gt;

&lt;p&gt;In this post I am just going to focus on one part of the infrastructure - committing multiple files to Github because it took me a little while to work out.&lt;/p&gt;

&lt;p&gt;The entire code can be seen on my &lt;a href="https://github.com/PaulKinlan/podcastinabox-editor/blob/master/record/javascripts/main.mjs#L90"&gt;repo&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you are building a Web UI that needs to commit directly to Github, the best library that I have found is Octokat - it works with CORS and it seems to handle the entire API surface of the Github API.&lt;/p&gt;

&lt;p&gt;Git can be a complex beast when it comes to understanding how the tree, branches and other pieces work so I took some decisions that made it easier.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;I will be only able to push to the master branch known as &lt;code&gt;heads/master&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;I will know where certain files will be stored (Hugo forces me to have a specific directory structure)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;With that in mind, the general process to creating a commit with multiple files is as follows:&lt;/p&gt;

&lt;p&gt;Get a reference to the repo.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Get a reference to the tip of the tree on &lt;code&gt;heads/master&lt;/code&gt; branch.&lt;/li&gt;
&lt;li&gt;For each file that we want to commit create a &lt;code&gt;blob&lt;/code&gt; and then store the references to the &lt;code&gt;sha&lt;/code&gt; identifier, path, mode in an array.&lt;/li&gt;
&lt;li&gt;Create a new &lt;code&gt;tree&lt;/code&gt; that contains all the blobs to add to the reference to the tip of the &lt;code&gt;heads/master&lt;/code&gt; tree, and store the new &lt;code&gt;sha&lt;/code&gt; pointer to this tree.&lt;/li&gt;
&lt;li&gt;Create a commit that points to this new tree and then push to the &lt;code&gt;heads/master&lt;/code&gt; branch.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The code pretty much follows that flow. Because I can assume the path structure for certain inputs I don’t need to build any complex UI or management for the files.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const createCommit = async (repositoryUrl, filename, data, images, commitMessage, recording) =&amp;gt; {
  try {
    const token = localStorage.getItem('accessToken');
    const github = new Octokat({ 'token': token });
    const [user, repoName] = repositoryUrl.split('/');

    if(user === null || repoName === null) {
      alert('Please specifiy a repo');
      return;
    }

    const markdownPath = `site/content/${filename}.markdown`.toLowerCase();
    let repo = await github.repos(user, repoName).fetch();
    let main = await repo.git.refs('heads/master').fetch();
    let treeItems = [];

    for(let image of images) {
      let imageGit = await repo.git.blobs.create({ content: image.data, encoding: 'base64' });
      let imagePath = `site/static/images/${image.name}`.toLowerCase();
      treeItems.push({
        path: imagePath,
        sha: imageGit.sha,
        mode: "100644",
        type: "blob"
        });
    }

    if (recording) {
      let audioGit = await repo.git.blobs.create({ content: recording.data, encoding: 'base64' });
      let audioPath = `site/static/audio/${recording.name}.${recording.extension}`.toLowerCase();
      treeItems.push({
        path: audioPath,
        sha: audioGit.sha,
        mode: "100644",
        type: "blob"
        });
    }

    let markdownFile = await repo.git.blobs.create({ content: btoa(jsonEncode(data)), encoding: 'base64' });
    treeItems.push({
      path: markdownPath,
      sha: markdownFile.sha,
      mode: "100644",
      type: "blob"
    });

    let tree = await repo.git.trees.create({
      tree: treeItems,
      base_tree: main.object.sha
    });

    let commit = await repo.git.commits.create({
      message: `Created via Web - ${commitMessage}`,
      tree: tree.sha,
      parents: [main.object.sha]});

    main.update({sha: commit.sha})

    logToToast('Posted');
  } catch (err) {
    console.error(err);
    logToToast(err);
  }
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Let me know if you’ve done anything similar with static hosting. I’m very excited that I can build a modern frontend for what is an entirely server-less hosting infrastructure.&lt;/p&gt;

</description>
      <category>git</category>
      <category>octokat</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Screen Recorder: recording microphone and the desktop audio at the same time</title>
      <dc:creator>Paul Kinlan</dc:creator>
      <pubDate>Mon, 13 May 2019 19:47:24 +0000</pubDate>
      <link>https://dev.to/chromiumdev/screen-recorder-recording-microphone-and-the-desktop-audio-at-the-same-time-4c0o</link>
      <guid>https://dev.to/chromiumdev/screen-recorder-recording-microphone-and-the-desktop-audio-at-the-same-time-4c0o</guid>
      <description>&lt;p&gt;I have a goal of building the worlds simplest screen recording software and I’ve been slowly noodling around on the project for the last couple of months (I mean really slowly).&lt;/p&gt;

&lt;p&gt;In previous posts I had got the &lt;a href="https://dev.to/chromiumdev/building-a-video-editor-on-the-web-part-01---screencast-50li-temp-slug-1927058"&gt;screen recording and a voice overlay&lt;/a&gt; by futzing about with the streams from all the input sources. One area of frustration though was that I could not work out how to get the audio from the desktop &lt;em&gt;and&lt;/em&gt; overlay the audio from the speaker. I finally worked out how to do it.&lt;/p&gt;

&lt;p&gt;Firstly, &lt;code&gt;getDisplayMedia&lt;/code&gt; in Chrome now allows audio capture, there seems like an odd oversight in the Spec in that it did not allow you to specify &lt;code&gt;audio: true&lt;/code&gt; in the function call, now you can.&lt;/p&gt;



&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const audio = audioToggle.checked || false;
desktopStream = await navigator.mediaDevices.getDisplayMedia({ video:true, audio: audio });

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Secondly, I had originally thought that by creating two tracks in the audio stream I would be able to get what I wanted, however I learnt that Chrome’s &lt;code&gt;MediaRecorder&lt;/code&gt; API can only output one track, and 2nd, it wouldn’t have worked anyway because tracks are like the DVD mutliple audio tracks in that only one can play at a time.&lt;/p&gt;

&lt;p&gt;The solution is probably simple to a lot of people, but it was new to me: Use Web Audio.&lt;/p&gt;

&lt;p&gt;It turns out that WebAudio API has &lt;code&gt;createMediaStreamSource&lt;/code&gt; and &lt;code&gt;createMediaStreamDestination&lt;/code&gt;, both of which are API’s needed to solve the problem. The &lt;code&gt;createMediaStreamSource&lt;/code&gt; can take streams from my desktop audio and microphone, and by connecting the two together into the object created by &lt;code&gt;createMediaStreamDestination&lt;/code&gt; it gives me the ability to pipe this one stream into the &lt;code&gt;MediaRecorder&lt;/code&gt; API.&lt;/p&gt;



&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const mergeAudioStreams = (desktopStream, voiceStream) =&amp;gt; {
  const context = new AudioContext();

  // Create a couple of sources
  const source1 = context.createMediaStreamSource(desktopStream);
  const source2 = context.createMediaStreamSource(voiceStream);
  const destination = context.createMediaStreamDestination();

  const desktopGain = context.createGain();
  const voiceGain = context.createGain();

  desktopGain.gain.value = 0.7;
  voiceGain.gain.value = 0.7;

  source1.connect(desktopGain).connect(destination);
  // Connect source2
  source2.connect(voiceGain).connect(destination);

  return destination.stream.getAudioTracks();
};

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Simples.&lt;/p&gt;

&lt;p&gt;The full code can be found on &lt;a href="https://glitch.com/edit/#!/screen-record-voice"&gt;my glitch&lt;/a&gt;, and the demo can be found here: &lt;a href="https://screen-record-voice.glitch.me/"&gt;https://screen-record-voice.glitch.me/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>pwa</category>
      <category>screenrecording</category>
      <category>getdisplaymedia</category>
      <category>webaudio</category>
    </item>
    <item>
      <title>Extracting text from an image: Experiments with Shape Detection</title>
      <dc:creator>Paul Kinlan</dc:creator>
      <pubDate>Mon, 13 May 2019 12:39:45 +0000</pubDate>
      <link>https://dev.to/chromiumdev/extracting-text-from-an-image-experiments-with-shape-detection-2kce</link>
      <guid>https://dev.to/chromiumdev/extracting-text-from-an-image-experiments-with-shape-detection-2kce</guid>
      <description>&lt;p&gt;I had a little down time after Google IO and I wanted to scratch a long-term itch I’ve had. I just want to be able to copy text that is held inside images in the browser. That is all. I think it would be a neat feature for everyone.&lt;/p&gt;

&lt;p&gt;It’s not easy to add functionality directly into Chrome, but I know I can take advantage of the intent system on Android and I can now do that with the Web (or at least Chrome on Android).&lt;/p&gt;

&lt;p&gt;Two new additions to the web platform - Share Target Level 2 (or as I like to call it File Share) and the &lt;code&gt;TextDetector&lt;/code&gt; in the Shape Detection API - &lt;a href="https://copy-image-text.glitch.me/" rel="noopener noreferrer"&gt;have allowed me to build a utility that I can Share images to and get the text held inside them&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The basic implementation is relatively straight forwards, you create a Share Target and a handler in the Service Worker, and then once you have the image that the user has shared you run the &lt;code&gt;TextDetector&lt;/code&gt; on it.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;Share Target API&lt;/code&gt; allows your web application to be part of the native sharing sub-system, and in this case you can now register to handle all &lt;code&gt;image/*&lt;/code&gt; types by declaring it inside your &lt;code&gt;Web App Manifest&lt;/code&gt; as follows.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"share_target": {
  "action": "/index.html",
  "method": "POST",
  "enctype": "multipart/form-data",
  "params": {
    "files": [
      {
        "name": "file",
        "accept": ["image/*"]
      }
    ]
  }
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When your PWA is installed then you will see it in all the places where you share images from as follows:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fpaul.kinlan.me%2Fimages%2F2019-05-13-extracting-text-from-an-imageexperiments-with-shape-detection-0.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fpaul.kinlan.me%2Fimages%2F2019-05-13-extracting-text-from-an-imageexperiments-with-shape-detection-0.jpeg"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;Share Target&lt;/code&gt; API treats sharing files like a form post. When the file is shared to the Web App the service worker is activated the &lt;code&gt;fetch&lt;/code&gt; handler is invoked with the file data. The data is now inside the Service Worker but I need it in the current window so that I can process it, the service knows which window invoked the request, so you can easily target the client and send it the data.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;self.addEventListener('fetch', event =&amp;gt; {
  if (event.request.method === 'POST') {
    event.respondWith(Response.redirect('/index.html'));
    event.waitUntil(async function () {
      const data = await event.request.formData();
      const client = await self.clients.get(event.resultingClientId || event.clientId);
      const file = data.get('file');
      client.postMessage({ file, action: 'load-image' });
    }());

    return;
  }
  ...
  ...
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the image is in the user interface, I then process it with the text detection API.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;navigator.serviceWorker.onmessage = (event) =&amp;gt; {  
  const file = event.data.file;
  const imgEl = document.getElementById('img');
  const outputEl = document.getElementById('output');
  const objUrl = URL.createObjectURL(file);
  imgEl.src = objUrl;
  imgEl.onload = () =&amp;gt; {
    const texts = await textDetector.detect(imgEl);
    texts.forEach(text =&amp;gt; {
      const textEl = document.createElement('p');
      textEl.textContent = text.rawValue;
      outputEl.appendChild(textEl);
    });
  };
  ...
};

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The biggest issue is that the browser doesn’t naturally rotate the image (as you can see below), and the Shape Detection API needs the text to be in the correct reading orientation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fpaul.kinlan.me%2Fimages%2F2019-05-13-extracting-text-from-an-imageexperiments-with-shape-detection-1.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fpaul.kinlan.me%2Fimages%2F2019-05-13-extracting-text-from-an-imageexperiments-with-shape-detection-1.jpeg"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I used the rather easy to use &lt;a href="a%20href=%22https://github.com/exif-js/exif-js%22&amp;gt;https://github.com/exif-js/exif-js&amp;lt;/a"&gt;EXIF-Js library&lt;/a&gt; to detect the rotation and then do some basic canvas manipulation to re-orientate the image.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;EXIF.getData(imgEl, async function() {
  // http://sylvana.net/jpegcrop/exif_orientation.html
  const orientation = EXIF.getTag(this, 'Orientation');
  const [width, height] = (orientation &amp;gt; 4) 
                  ? [imgEl.naturalWidth, imgEl.naturalHeight]
                  : [imgEl.naturalHeight, imgEl.naturalWidth];

  canvas.width = width;
  canvas.height = height;
  const context = canvas.getContext('2d');
  // We have to get the correct orientation for the image
  // See also https://stackoverflow.com/questions/20600800/js-client-side-exif-orientation-rotate-and-mirror-jpeg-images
  switch(orientation) {
    case 2: context.transform(-1, 0, 0, 1, width, 0); break;
    case 3: context.transform(-1, 0, 0, -1, width, height); break;
    case 4: context.transform(1, 0, 0, -1, 0, height); break;
    case 5: context.transform(0, 1, 1, 0, 0, 0); break;
    case 6: context.transform(0, 1, -1, 0, height, 0); break;
    case 7: context.transform(0, -1, -1, 0, height, width); break;
    case 8: context.transform(0, -1, 1, 0, 0, width); break;
  }
  context.drawImage(imgEl, 0, 0);
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And Voila, if you share an image to the app it will rotate the image and then analyse it returning the output of the text that it has found.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fpaul.kinlan.me%2Fimages%2F2019-05-13-extracting-text-from-an-imageexperiments-with-shape-detection-2.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fpaul.kinlan.me%2Fimages%2F2019-05-13-extracting-text-from-an-imageexperiments-with-shape-detection-2.jpeg"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It was incredibly fun to create this little experiment, and it has been immediately useful for me. It does however, highlight the &lt;a href="https://paul.kinlan.me/the-lumpy-web/" rel="noopener noreferrer"&gt;inconsistency of the web platform&lt;/a&gt;. These API’s are not available in all browsers, they are not even available in all version of Chrome - this means that as I write this article Chrome OS, I can’t use the app, but at the same time, when I can use it… OMG, so cool.&lt;/p&gt;

</description>
      <category>pwa</category>
      <category>chrome</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Debugging Web Pages on the Nokia 8110 with KaiOS using Chrome OS</title>
      <dc:creator>Paul Kinlan</dc:creator>
      <pubDate>Mon, 15 Apr 2019 01:16:30 +0000</pubDate>
      <link>https://dev.to/chromiumdev/debugging-web-pages-on-the-nokia-8110-with-kaios-using-chrome-os-4h8c</link>
      <guid>https://dev.to/chromiumdev/debugging-web-pages-on-the-nokia-8110-with-kaios-using-chrome-os-4h8c</guid>
      <description>&lt;p&gt;This post is a continuation of the post on debugging a &lt;a href="a%20href=%22https://paul.kinlan.me/debugging-web-pages-on-the-nokia-8110-with-kaios/%22&amp;gt;https://paul.kinlan.me/debugging-web-pages-on-the-nokia-8110-with-kaios/&amp;lt;/a"&gt;KaiOS device with Web IDE&lt;/a&gt;, but instead of using macOS, you can now use Chrome OS (m75) with Crostini.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--HMGXH3Ua--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://paul.kinlan.me/images/2019-04-15-debugging-web-pages-on-the-nokia-8110-with-kaios-using-chrome-os-1.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--HMGXH3Ua--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://paul.kinlan.me/images/2019-04-15-debugging-web-pages-on-the-nokia-8110-with-kaios-using-chrome-os-1.jpeg" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I’m cribbing from the &lt;a href="a%20href=%22https://developer.kaiostech.com/environment-setup%22&amp;gt;https://developer.kaiostech.com/environment-setup&amp;lt;/a"&gt;KaiOS Environment Setup&lt;/a&gt; which is a good start, but not enough for getting going with Chrome OS and Crostini. Below is the rough guide that I followed.&lt;/p&gt;

&lt;p&gt;Make sure that you are using at least Chrome OS m75 (currently dev channel as of April 15th), then:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Ensure that you have Crostini USB support enabled - &lt;a href="https://dev.tochrome://flags/#crostini-usb-support"&gt;chrome://flags/#crostini-usb-support&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Open up the terminal in crostini&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sudo apt-get install usbutils udev&lt;/code&gt; - You need to make sure that you have the USB tools installed.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;lsusb&lt;/code&gt; - You should now see your connected device, if this doesn’t work there might be another issue.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sudo apt-get install --no-install-recommends autoconf2.13 bison bzip2 ccache curl flex gawk gcc g++ g++-multilib git lib32ncurses5-dev lib32z1-dev libgconf2-dev libgl1-mesa-dev libx11-dev make zip lzop libxml2-utils openjdk-8-jdk nodejs unzip python&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sudo apt install android-tools-adb android-tools-fastboot&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;I am not sure I needed it, but I also ran &lt;code&gt;wget -S -O - https://raw.githubusercontent.com/cm-b2g/B2G/1230463/tools/51-android.rules | sudo tee &amp;amp;gt;/dev/null /etc/udev/rules.d/51-android.rules; sudo udevadm control --reload-rules&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sudo chmod a+r /etc/udev/rules.d/51-android.rules&lt;/code&gt;  and then added the device vendor ID to the file.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If all the above is done, you should then be able to &lt;code&gt;adb devices&lt;/code&gt; and get your list of connected devices.&lt;/p&gt;

</description>
      <category>pwa</category>
      <category>kaios</category>
    </item>
    <item>
      <title>Offline fallback page with service worker</title>
      <dc:creator>Paul Kinlan</dc:creator>
      <pubDate>Fri, 05 Apr 2019 18:17:22 +0000</pubDate>
      <link>https://dev.to/chromiumdev/offline-fallback-page-with-service-worker-1jfe</link>
      <guid>https://dev.to/chromiumdev/offline-fallback-page-with-service-worker-1jfe</guid>
      <description>&lt;p&gt;Years ago, I did some research into how native applications responded to a lack of network connectivity. Whilst I’ve lost the link to the analysis (I could swear it was on Google+), the overarching narrative was that many native applications are inextricably tied to the internet that they just straight up refuse to function. Sounds like a lot of web apps, the thing that set them apart from the web though is that the experience was still ‘on-brand’, Bart Simpson would tell you that you need to be online (for example), and yet for the vast majority of web experiences you get a ‘Dino’ (see chrome://dino).&lt;/p&gt;

&lt;p&gt;We’ve been working on Service Worker for a long time now, and whilst we are seeing more and more sites have pages controlled by a Service Worker, the vast majority of sites don’t even have a basic fallback experience when the network is not available.&lt;/p&gt;

&lt;p&gt;I asked my good chum Jake if we have any guindance on how to build a generic fall-back page on the assumption that you don’t want to create an entirely offline-first experience, and within 10 minutes he had created it. &lt;a href="https://glitch.com/edit/#!/static-misc?path=sw-fallback-page/sw.js:6:9" rel="noopener noreferrer"&gt;Check it out&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For brevity, I have pasted the code in below because it is only about 20 lines long. It caches the offline assets, and then for every fetch that is a ‘navigation’ fetch it will see if it errors (because of the network) and then render the offline page in place of the original content.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;

&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;install&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitUntil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;caches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;static-v1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addAll&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;offline.html&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;styles.css&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="p"&gt;}());&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fetch&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Always bypass for range requests, due to browser bugs&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;range&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;respondWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Try to get from the cache:&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cachedResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;caches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cachedResponse&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;cachedResponse&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Otherwise, get from the network&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// If this was a navigation, show the offline page:&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mode&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;navigate&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;caches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;offline.html&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="c1"&gt;// Otherwise throw&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}());&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;



&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;That is all. When the user is online they will see the default experience.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0evx0hcvft3n9xee4alt.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0evx0hcvft3n9xee4alt.jpeg"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And when the user is offline, they will get the fallback page.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fchr6fbm5gzzjmb0igw31.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fchr6fbm5gzzjmb0igw31.jpeg"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I find this simple script incredibly powerful, and yes, whilst it can still be improved, I do believe that even just a simple change in the way that we speak to our users when there is an issue with the network has the ability to fundamentally improve the perception of the web for users all across the globe.&lt;/p&gt;

</description>
      <category>pwa</category>
      <category>serviceworker</category>
    </item>
    <item>
      <title>Quick Logcat - debugging android from the web</title>
      <dc:creator>Paul Kinlan</dc:creator>
      <pubDate>Sat, 30 Mar 2019 10:32:48 +0000</pubDate>
      <link>https://dev.to/chromiumdev/quick-logcat-debugging-android-from-the-web-29g3</link>
      <guid>https://dev.to/chromiumdev/quick-logcat-debugging-android-from-the-web-29g3</guid>
      <description>&lt;p&gt;I was on the flight to Delhi this last week and I wanted to be able to &lt;a href="https://dev.to/chromiumdev/debugging-web-pages-on-the-nokia-8110-with-kaios-1ed4-temp-slug-6021678"&gt;debug my KaiOS device&lt;/a&gt; with Chrome OS - I never quite got to the level that I needed for a number of reasons (port forwarding didn’t work - more on that in another post), but I did get to build a simple tool that really helps me build for the web on Android based devices.&lt;/p&gt;

&lt;p&gt;I’ve been using &lt;a href="https://github.com/webadb/webadb.js" rel="noopener noreferrer"&gt;WebADB.js&lt;/a&gt; for a couple of side projects, but I thought I would at least release one of the tools I made last week that will help you if you ever need to debug your Android device and you don’t have &lt;code&gt;adb&lt;/code&gt; installed or any other Android system tools.&lt;/p&gt;

&lt;p&gt;Quick Logcat is just that. It can connect to any Android device that is in developer mode and has USB enabled, is connected to your machine over USB and most importantly you grant access from the web page to connect to the device, and once that is all done it just runs &lt;code&gt;adb shell logcat&lt;/code&gt; to create the following output.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fpaul.kinlan.me%2Fimages%2F2019-03-30-quick-logcatdebugging-android-from-the-web.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fpaul.kinlan.me%2Fimages%2F2019-03-30-quick-logcatdebugging-android-from-the-web.jpeg"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Checkout the &lt;a href="https://github.com/PaulKinlan/QuickLogcat" rel="noopener noreferrer"&gt;source over on my github account&lt;/a&gt;, specifically &lt;a href="https://github.com/PaulKinlan/QuickLogcat/blob/master/app/scripts/main.mjs" rel="noopener noreferrer"&gt;the logger class&lt;/a&gt; that has the brunt of my logic - note a lot of this code is incredibly similar to the demo over at &lt;a href="https://webadb.github.io/" rel="noopener noreferrer"&gt;webadb.github.io&lt;/a&gt;, but it should hopefully be relatively clear to follow how I interface with the WebUSB API (which is very cool). The result is the following code that is in my index file: I instantiate a controller, connect to the device which will open up the USB port and then I start the &lt;code&gt;logcat&lt;/code&gt; process and well, &lt;code&gt;cat&lt;/code&gt; the log, via &lt;code&gt;logcat&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It even uses .mjs files :D&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; &amp;lt;script type="module"&amp;gt;
    import LogcatController from "/scripts/main.mjs";
    onload = () =&amp;gt; {
      const connect = document.getElementById("connect");
      const output = document.getElementById("output");
      let controller = new LogcatController();
      connect.addEventListener("click", async () =&amp;gt; {
        await controller.connect();
        controller.logcat((log) =&amp;gt; {
          output.innerText += log;
        })
      });
    };
  &amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ADB is an incredibly powerful protocol, you can read system files, you can write over personal data and you can even easily side-load apps, so if you give access to any external site to your Android device, you need to completely trust the operator of the site.&lt;/p&gt;

&lt;p&gt;This demo shows the power and capability of the &lt;a href="https://developers.google.com/web/updates/2016/03/access-usb-devices-on-the-web" rel="noopener noreferrer"&gt;WebUSB&lt;/a&gt; API, we can interface with hardware without any natively installed components, drivers or software and with a pervasive explicit user opt-in model that stops drive-by access to USB components.&lt;/p&gt;

&lt;p&gt;I’ve got a couple more ideas up my sleeve, it will totally be possible to do firmware updates via the web if you so choose. One thing we saw a lot of in India was the ability side-load APK’s on to user’s new phones, whilst I am not saying we must do it, a clean web-interface would be more more preferable to the software people use today.&lt;/p&gt;

&lt;p&gt;What do you think you could build with &lt;code&gt;Web USB&lt;/code&gt; and &lt;code&gt;adb&lt;/code&gt; access?&lt;/p&gt;

</description>
      <category>webusb</category>
      <category>android</category>
      <category>kaios</category>
    </item>
    <item>
      <title>Debugging Web Pages on the Nokia 8110 with KaiOS</title>
      <dc:creator>Paul Kinlan</dc:creator>
      <pubDate>Thu, 21 Mar 2019 21:41:53 +0000</pubDate>
      <link>https://dev.to/chromiumdev/debugging-web-pages-on-the-nokia-8110-with-kaios-3oc7</link>
      <guid>https://dev.to/chromiumdev/debugging-web-pages-on-the-nokia-8110-with-kaios-3oc7</guid>
      <description>&lt;p&gt;We’ve been doing a lot of development on feature phones recently and it’s been hard, but fun. The hardest bit is that on KaiOS we found it impossible to debug web pages, especially on the hardware that we had (The Nokia 8110). The Nokia is a great device, it’s built with KaiOS which we know is based on something akin to Firefox 48, but it’s locked down, there is no traditional developer mode like you get on other Android devices, which means you can connect Firefox’s WebIDE.&lt;/p&gt;

&lt;p&gt;Through a combination of reading a couple of blogs, and knowing a bit about &lt;code&gt;adb&lt;/code&gt; I worked out how to do it. Note, others might have been able to do it, but it’s not documented in one place cleanly.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz6a1id5550vyzk2ivqdr.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fz6a1id5550vyzk2ivqdr.jpeg" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;(Image above shows the DevTools and also the output of the screenshot tool)&lt;/p&gt;

&lt;p&gt;Here are the steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Connect a USB cable. Ensure you have &lt;code&gt;adb&lt;/code&gt; installed on your main machine.&lt;/li&gt;
&lt;li&gt;Download a copy of &lt;a href="https://archive.mozilla.org/pub/firefox/releases/42.0b2/" rel="noopener noreferrer"&gt;Firefox 48&lt;/a&gt; (this is the only one I could get to work)&lt;/li&gt;
&lt;li&gt;Enable ‘Developer Mode’ by entering &lt;code&gt;*#*#33284#*#*&lt;/code&gt; from your phone (note, don’t use the dialer). You will see a little ‘bug’ icon on the top of the screen. [&lt;a href="https://groups.google.com/forum/#!topic/bananahackers/MIpcrSXTRBk" rel="noopener noreferrer"&gt;Source&lt;/a&gt;]&lt;/li&gt;
&lt;li&gt;Attach your USB cable&lt;/li&gt;
&lt;li&gt;On your development machine run the following commands

&lt;ol&gt;
&lt;li&gt;&lt;code&gt;adb start-server&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;adb devices&lt;/code&gt; to check your phone is connected.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;adb forward tcp:6000 localfilesystem:/data/local/debugger-socket&lt;/code&gt; this sets up a channel from your machine to a socket on the phone. This is what Web IDE uses.&lt;/li&gt;
&lt;/ol&gt;


&lt;/li&gt;

&lt;li&gt;Start &lt;code&gt;Web IDE&lt;/code&gt; by opening Firefox, go to Tools and then Web IDE&lt;/li&gt;

&lt;li&gt;Web IDE will be open, click ‘Remote Runtime’ and click the open button that has ‘localhost:6000’ in. (this is the tcp forwarding port).&lt;/li&gt;

&lt;li&gt;Open a page on the phone, and you should see it on the left.Voila.&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>pwa</category>
    </item>
    <item>
      <title>Got web performance problems? Just wait...</title>
      <dc:creator>Paul Kinlan</dc:creator>
      <pubDate>Sat, 09 Mar 2019 08:10:52 +0000</pubDate>
      <link>https://dev.to/chromiumdev/got-web-performance-problems-just-wait-1fke</link>
      <guid>https://dev.to/chromiumdev/got-web-performance-problems-just-wait-1fke</guid>
      <description>&lt;p&gt;I saw a tweet by a good chum and colleague, &lt;a href="https://twitter.com/kosamari"&gt;Mariko&lt;/a&gt;, about testing on a range of low end devices keeping you really grounded.&lt;/p&gt;

&lt;p&gt;The context of the tweet is that we are looking at what Web Development is like when building for users who live daily on these classes of devices.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--4I9QDpix--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://paul.kinlan.me/images/2019-03-09-got-web-performance-problemsjust-wait.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--4I9QDpix--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://paul.kinlan.me/images/2019-03-09-got-web-performance-problemsjust-wait.jpeg" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The team is doing a lot of work now in this space, but I spent a day build a site and it was incredibly hard to make anything work at a even slightly reasonable level of performances - here are some of the problems that I ran into:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Viewport oddities, and mysterious re-introduction of 300ms click-delay (can work around).&lt;/li&gt;
&lt;li&gt;Huge repaints of entire screen, and it’s slow.&lt;/li&gt;
&lt;li&gt;Network is slow&lt;/li&gt;
&lt;li&gt;Memory is constrained, and subsequent GC’s lock the main thread for multiple seconds&lt;/li&gt;
&lt;li&gt;Incredibly slow JS execution&lt;/li&gt;
&lt;li&gt;DOM manipulation is slow&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For many of the pages I was building, even on a fast wifi connection pages took multiple seconds to load, and subsequent interactions were just plain slow. It was hard, but incredibly gratifying at a technical level to see changes in algorithms and logic that I wouldn’t have done for all my traditional web development, yield large improvements in performance.&lt;/p&gt;

&lt;p&gt;I am not sure what to do long-term, I suspect a huge swathe of developers that we work with in the more developed markets will have a reaction ‘I am not building sites for users in [insert country x]‘, and at a high-level it’s hard to argue with this statement, but I can’t ignore the fact that 10’s of millions of new users are coming to computing each year and they will be using these devices and we want the web to be &lt;em&gt;the&lt;/em&gt; platform of choice for content and apps lest we are happy with the &lt;a href="https://paul.kinlan.me/rise-of-the-meta-platforms/"&gt;rise of the meta platform&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;We’re going to need to keep pushing on performance for a long time to come. We will keep creating tools and guidance to help developers load quickly and have smooth user interfaces :)&lt;/p&gt;

</description>
      <category>chrome</category>
      <category>performance</category>
    </item>
    <item>
      <title>Browser Bug Searcher</title>
      <dc:creator>Paul Kinlan</dc:creator>
      <pubDate>Sat, 09 Mar 2019 07:49:18 +0000</pubDate>
      <link>https://dev.to/chromiumdev/browser-bug-searcher-5el3</link>
      <guid>https://dev.to/chromiumdev/browser-bug-searcher-5el3</guid>
      <description>&lt;p&gt;I was just reflecting on some of the &lt;a href="https://twitter.com/ChromiumDev"&gt;work our team has done&lt;/a&gt; and I found a project from 2017 that Robert Nyman and Eric Bidelman created. &lt;a href="https://browser-issue-tracker-search.appspot.com/?q=Web%20Share"&gt;Browser Bug Searcher!&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It’s incredible that with just a few key presses you have a great overview of your favourite features across all the major browser engines.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--G9BaabO5--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://paul.kinlan.me/images/2019-03-09-browser-bug-searcher.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--G9BaabO5--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://paul.kinlan.me/images/2019-03-09-browser-bug-searcher.jpeg" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/GoogleChrome/browser-bug-search"&gt;Source code available&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This actually highlights one of the issues that I have with crbug and webkit bug trackers, they don’t have a simple way to get feeds of data in formats like RSS. I would love to be able to use my &lt;a href="https://github.com/PaulKinlan/topicdeck"&gt;topicdeck&lt;/a&gt; aggregator with bug categories etc so I have a dashboard of all the things that I am interested in based on the latest information from each of the bug trackers.&lt;/p&gt;

</description>
      <category>chrome</category>
      <category>bugs</category>
    </item>
    <item>
      <title>File Web Share Target</title>
      <dc:creator>Paul Kinlan</dc:creator>
      <pubDate>Fri, 15 Feb 2019 15:52:03 +0000</pubDate>
      <link>https://dev.to/chromiumdev/file-web-share-target-4go4</link>
      <guid>https://dev.to/chromiumdev/file-web-share-target-4go4</guid>
      <description>&lt;p&gt;I’ve frequently said that for web apps to compete effectively in the world ofapps, they need to be integrated in to all of the places that users expect appsto be. Inter-app communication is one of the major missing pieces of the webplatform, and specifically one of the last major missing features is nativelevel sharing: Web apps need to be able to get &lt;a href="https://dev.to/paul_kinlan/web-sites-as-unintended-silos-the-problem-with-getting-data-in-and-out-of-the-web-client-4o9i"&gt;data out of theirsilo&lt;/a&gt; and into other web sites and apps; they also need tobe able to receive the data from other native apps and sites.&lt;/p&gt;

&lt;p&gt;The File Share Target API is a game-changer of an API that is now in ChromeCanary. The API extends the &lt;a href="https://github.com/WICG/web-share-target/blob/master/docs/explainer.md"&gt;Web Share TargetAPI&lt;/a&gt;that lets apps and sites share simple links and text to web sites by integratingthem into the systems sharing functionality.&lt;/p&gt;

&lt;p&gt;This very static file blog utilizes the Web Share Target API so I can quickly&lt;a href="https://dev.to/paul_kinlan/web-share-target-api-2e3g"&gt;share links&lt;/a&gt; that I find interesting to it from anyAndroid application, and as of last week &lt;a href="https://dev.to/chromiumdev/testing-file-share-target-from-camera-198j-temp-slug-2092201"&gt;I enabled the File Share Target API sothat I can upload images to my blog directly from the Camera app onAndroid&lt;/a&gt;. This post is all about how Idid it (and stole some code from Jake Archibald — tbf he worked out a lotof the bugs for an integration they are doing in to&lt;a href="https://squoosh.app/"&gt;squoosh.app&lt;/a&gt;.)&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://wicg.github.io/web-share-target/level-2/#example-3-manifest-webmanifest"&gt;File Share TargetAPI&lt;/a&gt;is a very novel API in that it is fully progressive. If your application canhandle Form &lt;code&gt;POST&lt;/code&gt; requests then you can integrate easily with this API. Thebasic flow is: when the user chooses your application from the native picker,Chrome will send a Form &lt;code&gt;POST&lt;/code&gt; request to your server, it is up to you what youdo with it (handle in a service worker or on the server).&lt;/p&gt;

&lt;p&gt;To add support for sharing files into your web app you need to do two things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Declare support for sharing files via the manifest file,&lt;/li&gt;
&lt;li&gt;Handle the Form &lt;code&gt;POST&lt;/code&gt; request in your Service Worker.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The manifest declares to the host system how Sharing should be mapped from thehost application to the web app. In the manifest below it essentially says “Whena user shares a file of type ‘image/*’ make a Form POST request to‘/share/image/’ and name the data ‘file’“.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;manifest.json&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "name": "Blog: Share Image",
  "short_name": "Blog: Share Image",
  "start_url": "/share/image/",
  "theme_color": "#000000",
  "background_color": "#000000",
  "icons": [ {
      "sizes": "192x192",
      "src": "/images/me.png",
      "type": "image/png"
  }],
  "share_target": {
    "action": "/share/image/",
    "method": "POST",
    "enctype": "multipart/form-data",
    "params": {
      "files": [
        {
          "name": "file",
          "accept": ["image/*"]
        }
      ]
    }
  },
  "display": "standalone",
  "scope": "/share/"
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Once the user shares to your web application, Chrome will make the web requestto your site with the file data as the payload.&lt;/p&gt;

&lt;p&gt;It is recommended that you handle the POST request inside your service worker sothat 1) it is fast, 2) resilient to the network not being available. You can dothis as follows:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;serviceworker.js&lt;/em&gt; - &lt;a href="https://paul.kinlan.me/share/image/sw.js"&gt;demo&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;onfetch = async (event) =&amp;gt; {
  if (event.request.method !== 'POST') return;
  if (event.request.url.startsWith('https://paul.kinlan.me/share/image/') === false) return;

  /* This is to fix the issue Jake found */
  event.respondWith(Response.redirect('/share/image/'));

  event.waitUntil(async function () {
    const data = await event.request.formData();
    const client = await self.clients.get(event.resultingClientId || event.clientId);
    // Get the data from the named element 'file'
    const file = data.get('file');

    console.log('file', file);
    client.postMessage({ file, action: 'load-image' });
  }());
};

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;There are a couple of interesting things happening above, which can quicklysummarized as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Render the UI as the result of the &lt;code&gt;POST&lt;/code&gt; request by performing a redirect.&lt;/li&gt;
&lt;li&gt;Read the data that is submitted via the form via &lt;code&gt;event.request.formData()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Send the data to the open window (this will be the UI that we redirected theuser to in the first point).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is entirely up to you what you do with the data that has been posted to yourservice worker, but in the case of my App I needed to show it directly in the UIso I have to find the window the user is using and &lt;code&gt;postMessage&lt;/code&gt; the data there.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;index.html&lt;/em&gt; - &lt;a href="https://paul.kinlan.me/share/image/index.html"&gt;demo&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;navigator.serviceWorker.onmessage = (event) =&amp;gt; {
  console.log(event);
  imageBlob = event.data.file;
  // Update the UI with the data that has been shared to it.
  imageShare.src = URL.createObjectURL(imageBlob);
};

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;And that’s about it. If you already have an API endpoint for your web forms,then this is a simple, yet powerful addition that you can make to your site.&lt;/p&gt;

&lt;p&gt;The Web Share Target API incredibly powerful platform primitive that breaks downanother barrier that web apps have had on their host platforms.&lt;/p&gt;

</description>
      <category>share</category>
      <category>pwa</category>
      <category>chrome</category>
    </item>
    <item>
      <title>Registering as a Share Target with the Web Share Target API</title>
      <dc:creator>Paul Kinlan</dc:creator>
      <pubDate>Fri, 07 Dec 2018 05:42:30 +0000</pubDate>
      <link>https://dev.to/chromiumdev/registering-as-a-share-target-with-the-web-share-target-api-3aep</link>
      <guid>https://dev.to/chromiumdev/registering-as-a-share-target-with-the-web-share-target-api-3aep</guid>
      <description>&lt;p&gt;Pete LePage introduces the Web Share Target API and the the availability in Chrome via an origin trial&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Until now, only native apps could register as a share target. The Web Share Target API allows installed web apps to register with the underlying OS as a share target to receive shared content from either the Web Share API or system events, like the OS-level share button.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://developers.google.com/web/updates/2018/12/web-share-target?utm_source=feed&amp;amp;utm_medium=feed&amp;amp;utm_campaign=updates_feed"&gt;Read full post&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This API is a game changer on the web, it opens the web up to something that was only once available to native apps: Native Sharing. Apps are silos, they suck in all data and make it hard to be accessible across platforms. Share Target starts to level the playing field so that the web can play in the same game.&lt;/p&gt;

&lt;p&gt;The Twitter Mobile experience has Share Target &lt;a href="https://mobile.twitter.com/manifest.json"&gt;already enabled&lt;/a&gt;. This post was created using the Share Target I have defined in my sites ‘admin panel’ &lt;a href="https://paul.kinlan.me/share/share-manifest.json"&gt;manifest.json&lt;/a&gt; - it works pretty well, and the minute they land file support I will be able to post any image or blob on my device to my blog.&lt;/p&gt;

&lt;p&gt;Very exciting times.&lt;/p&gt;

&lt;p&gt;Read the linked post to learn more about the time-lines for when this API should go live and how to use the API.&lt;/p&gt;

</description>
      <category>pwa</category>
      <category>share</category>
    </item>
    <item>
      <title>Barcode detection in a Web Worker using Comlink</title>
      <dc:creator>Paul Kinlan</dc:creator>
      <pubDate>Tue, 02 Oct 2018 21:05:31 +0000</pubDate>
      <link>https://dev.to/chromiumdev/barcode-detection-in-a-web-worker-using-comlink-2mfd</link>
      <guid>https://dev.to/chromiumdev/barcode-detection-in-a-web-worker-using-comlink-2mfd</guid>
      <description>&lt;p&gt;I’m a big fan of QRCodes, they are very simple and neat way to exchange data between the real world and the digital world. For a few years now I’ve had a little side project called &lt;a href="https://qrsnapper.com"&gt;QRSnapper&lt;/a&gt; — well it’s had a few names, but this is the one I’ve settled on — that uses the &lt;code&gt;getUserMedia&lt;/code&gt; API to take live data from the user’s camera so that it can scan for QR Codes in near real time.&lt;/p&gt;

&lt;p&gt;The goal of the app was to maintain 60fps in the UI and near instant detection of the QR Code, this meant that I had to put the detection code in to a Web Worker (pretty standard stuff). In this post I just wanted to quickly share how I used &lt;a href="https://github.com/GoogleChromeLabs/comlink"&gt;comlink&lt;/a&gt; to massively simplify the logic in the Worker.&lt;/p&gt;

&lt;h4&gt;
  
  
  qrclient.js
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import * as Comlink from './comlink.js';

const proxy = Comlink.proxy(new Worker('/scripts/qrworker.js')); 

export const decode = async function (context) {
  try {
    let canvas = context.canvas;
    let width = canvas.width;
    let height = canvas.height;
    let imageData = context.getImageData(0, 0, width, height);
    return await proxy.detectUrl(width, height, imageData);
  } catch (err) {
    console.log(err);
  }
};

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  qrworker.js (web worker)
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import * as Comlink from './comlink.js';
import {qrcode} from './qrcode.js';

// Use the native API's
let nativeDetector = async (width, height, imageData) =&amp;gt; {
  try {
    let barcodeDetector = new BarcodeDetector();
    let barcodes = await barcodeDetector.detect(imageData);
    // return the first barcode.
    if (barcodes.length &amp;gt; 0) {
      return barcodes[0].rawValue;
    }
  } catch(err) {
    detector = workerDetector;
  }
};

// Use the polyfil
let workerDetector = async (width, height, imageData) =&amp;gt; {
  try {
    return qrcode.decode(width, height, imageData);
  } catch (err) {
    // the library throws an excpetion when there are no qrcodes.
    return;
  }
}

let detectUrl = async (width, height, imageData) =&amp;gt; {
  return detector(width, height, imageData);
};

let detector = ('BarcodeDetector' in self) ? nativeDetector : workerDetector;
// Expose the API to the client pages.
Comlink.expose({detectUrl}, self);

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I really love Comlink, I think it is a game changer of a library especially when it comes to creating idiomatic JavaScript that works across threads. Finally a neat thing here, is that the native Barcode detection API can be run inside a worker so all the logic is encapsulated away from the UI.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/PaulKinlan/qrcode/blob/production/app/scripts/qrworker.js"&gt;Read full post&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>pwa</category>
      <category>camera</category>
      <category>qrcode</category>
    </item>
  </channel>
</rss>
