Easy

Web Truths: Publishing on the web using web standards is easy and amazing

This is part of the web truths series of posts. A series where we look at true sounding statements that we keep using to have endless discussions instead of moving on. Today I want to talk about the notion of the web as an open publication platform and HTML as the way to publish.

Publishing on the web using web standards is easy and amazing

Those of us who were lucky enough to be around when the web started growing cherish this concept. It was exciting to be able to open a text editor – any text editor – write some tags and see our text come to life in a browser. Adding a H1 here gave us a headline, adding a P described paragraphs and gave free margins. Adding an A allowed us to point to other resources or a target in the same page. Adding an HR gave us a horizontal ruler. The latter later on turned out to be a pain in the backside to style and was generally a bad idea. We learned by doing and we learned by trying things out.

We also smirked at the people who looked down on us as not being “real developers”. These people considered us crazy for relying on a browser we don’t control to render our output. Could be that we were, but at least we didn’t have to use a convoluted IDE and follow a slow build process to get a result. Our work was immediate and satisfaction was only a ctrl+s, shift-tab and ctrl-r (or F5) away.

We also loved the fact that all we need is some hosting space and an FTP client to publish our work to the world.

If we disliked what our server companies did, we moved to the next one. After all, your domain is the thing you send to people, not your IP address. To get people to read what we wrote we published on mailing lists, in forums and on IRC. We linked to each other in webrings and banner exchanges. When Google came around we were the old guard that Google loved. Easy to index, with sensible TITLE and proper text content. Not like Flash pages that tried to trick Google with META keyword spamming or hidden text.

All the good things about HTML and publishing on your own server are still valid. It is a beautiful, free and open way to publish with nobody to answer to. You are your own marketing department and your server is your playground. If you are vigilant and you make sure that you have a lot of time to delete spam and defend yourself against attacks.

But here is the problem with telling this story over and over again. It doesn’t quite work any longer. And it is not as fascinating for people these days as it was for us. I remember the exhiliration of hearing a successful modem handshake and my HTML rendering in Netscape 3. I remember explaining to people that “when it sounds everything is broken, then you are online”. People these days don’t expect to be offline. Often they only experience being offline when they are on the go and the mobile connection dies. Those lucky enough to have a home with a fat wireless connection never put effort in to reach the content. They consume, much like we did when we watched TV.

The same happens to publication. It isn’t about writing the perfect article or blog post. It is about creating something fast that gets a lot of eyeballs. And if you want eyeballs, you keep publishing. Faster and faster, more and more. And as it is hard to create your own, you re-hash what other people have done instead and ride any success train of the day. There is no place for HTML or proper standards publication in this world.

I’m not saying at all that this is a good thing. But it is where we are. The web hasn’t won over Facebook, WhatsApp and other closed environments because it needs more effort to use. You still need to show interest and skill to build a web site. Writing three words and picking a GIF from a collection is easier.

The web we love and explain as the amazing publishing platform will survive. It will be a playground of enthusiasts and specialists. And old people. The disruptive platform of the past hasn’t become the mainstream. Everyone has the potential to be a creator and maker. But the marketing machine of the world wants us to be consumers instead. And the best way to keep people consuming is to lock them into a place that is ridiculously easy to publish in. More importantly you need to give them the feeling of being part of a community of cool people. And the explosive growth of the web and tweaking of search algorithms to show the “new” instead of the “correct” isn’t a group of cool people. It is hard work. There are no likes, kudos, claps or whatever on the web. Adding an immediate feedback channel for people is 90% removing horrible content and spam. The web isn’t a small group of cool people, but mainstream media makes pretty sure to tell us it is full of dangers and wrong information. Better stay where it is safe. In a controlled environment that has very enticing immediate feedback.

If you think this is dark, check out André Staltz’ The Web began dying in 2014, here’s how where he paints a pretty bleak future for the mainstream web. And darn him, there is some pretty good evidence in there that finding web content in the future not published inside Google, Amazon or Facebook products will be close to impossible.

So, yes, publishing on the web is amazing. Nobody denies that. But we’re dealing with a new generation of people who grew up with the web and don’t care about it. It is there, like water is when you open the tap. You don’t think about how it gets there or what is involved until it stops coming out. And that might be the same for the web.

Instead of painting a romantic view of how the open web keeps prevailing, it may be time to tell people more about what their use of closed platforms does. How much they give away for the convenience of publishing something and harvesting some fake internet points. We’re past the format of the publication. We need to get people excited again about owning their data. For our sake and theirs.

View full post on Christian Heilmann

VN:F [1.9.22_1171]
Rating: 0.0/10 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)

Easy audio capture with the MediaRecorder API

The MediaRecorder API is a simple construct, used inside Navigator.getUserMedia(), which provides an easy way of recording media streams from the user’s input devices and instantly using them in web apps. This article provides a basic guide on how to use MediaRecorder, which is supported in Firefox Desktop/Mobile 25, and Firefox OS 2.0.

What other options are available?

Capturing media isn’t quite as simple as you’d think on Firefox OS. Using getUserMedia() alone yields raw PCM data, which is fine for a stream, but then if you want to capture some of the audio or video you start having to perform manual encoding operations on the PCM data, which can get complex very quickly.

Then you’ve got the Camera API on Firefox OS, which until recently was a certified API, but has been downgraded to privileged recently.

Web activities are also available to allow you to grab media via other applications (such as Camera).

the only trouble with these last two options is that they would capture only video with an audio track, and you would still have separate the audio if you just wanted an audio track. MediaRecorder provides an easy way to capture just audio (with video coming later — it is _just_ audio for now.)

A sample application: Web Dictaphone

An image of the Web dictaphone sample app - a sine wave sound visualization, then record and stop buttons, then an audio jukebox of recorded tracks that can be played back.

To demonstrate basic usage of the MediaRecorder API, we have built a web-based dictaphone. It allows you to record snippets of audio and then play them back. It even gives you a visualization of your device’s sound input, using the Web Audio API. We’ll concentrate on the recording and playback functionality for this article.

You can see this demo running live, or grab the source code on Github (direct zip file download.)

CSS goodies

The HTML is pretty simple in this app, so we won’t go through it here; there are a couple of slightly more interesting bits of CSS worth mentioning, however, so we’ll discuss them below. If you are not interested in CSS and want to get straight to the JavaScript, skip to the “Basic app setup” section.

Keeping the interface constrained to the viewport, regardless of device height, with calc()

The calc function is one of those useful little utility features that’s cropped up in CSS that doesn’t look like much initially, but soon starts to make you think “Wow, why didn’t we have this before? Why was CSS2 layout so awkward?” It allows you do a calculation to determine the computed value of a CSS unit, mixing different units in the process.

For example, in Web Dictaphone we have theee main UI areas, stacked vertically. We wanted to give the first two (the header and the controls) fixed heights:

header {
  height: 70px;
}
 
.main-controls {
  padding-bottom: 0.7rem;
  height: 170px;
}

However, we wanted to make the third area (which contains the recorded samples you can play back) take up whatever space is left, regardless of the device height. Flexbox could be the answer here, but it’s a bit overkill for such a simple layout. Instead, the problem was solved by making the third container’s height equal to 100% of the parent height, minus the heights and padding of the other two:

.sound-clips {
  box-shadow: inset 0 3px 4px rgba(0,0,0,0.7);
  background-color: rgba(0,0,0,0.1);
  height: calc(100% - 240px - 0.7rem);
  overflow: scroll;
}

Note: calc() has good support across modern browsers too, even going back to Internet Explorer 9.

Checkbox hack for showing/hiding

This is fairly well documented already, but we thought we’d give a mention to the checkbox hack, which abuses the fact that you can click on the <label> of a checkbox to toggle it checked/unchecked. In Web Dictaphone this powers the Information screen, which is shown/hidden by clicking the question mark icon in the top right hand corner. First of all, we style the <label> how we want it, making sure that it has enough z-index to always sit above the other elements and therefore be focusable/clickable:

label {
    font-family: 'NotoColorEmoji';
    font-size: 3rem;
    position: absolute;
    top: 2px;
    right: 3px;
    z-index: 5;
    cursor: pointer;
}

Then we hide the actual checkbox, because we don’t want it cluttering up our UI:

input[type=checkbox] {
   position: absolute;
   top: -100px;
}

Next, we style the Information screen (wrapped in an <aside> element) how we want it, give it fixed position so that it doesn’t appear in the layout flow and affect the main UI, transform it to the position we want it to sit in by default, and give it a transition for smooth showing/hiding:

aside {
   position: fixed;
   top: 0;
   left: 0;
   text-shadow: 1px 1px 1px black;  
   width: 100%;
   height: 100%;
   transform: translateX(100%);
   transition: 0.6s all;
   background-color: #999;
    background-image: linear-gradient(to top right, rgba(0,0,0,0), rgba(0,0,0,0.5));
}

Last, we write a rule to say that when the checkbox is checked (when we click/focus the label), the adjacent <aside> element will have it’s horizontal translation value changed and transition smoothly into view:

input[type=checkbox]:checked ~ aside {
  transform: translateX(0);
}

Basic app setup

To grab the media stream we want to capture, we use getUserMedia() (gUM for short). We then use the MediaRecorder API to record the stream, and output each recorded snippet into the source of a generated <audio> element so it can be played back.

First, we’ll add in a forking mechanism to make gUM work, regardless of browser prefixes, and so that getting the app working on other browsers once they start supporting MediaRecorder will be easier in the future.

navigator.getUserMedia = ( navigator.getUserMedia ||
                       navigator.webkitGetUserMedia ||
                       navigator.mozGetUserMedia ||
                       navigator.msGetUserMedia);

Then we’ll declare some variables for the record and stop buttons, and the <article> that will contain the generated audio players:

var record = document.querySelector('.record');
var stop = document.querySelector('.stop');
var soundClips = document.querySelector('.sound-clips');

Finally for this section, we set up the basic gUM structure:

if (navigator.getUserMedia) {
   console.log('getUserMedia supported.');
   navigator.getUserMedia (
      // constraints - only audio needed for this app
      {
         audio: true
      },
 
      // Success callback
      function(stream) {
 
 
      },
 
      // Error callback
      function(err) {
         console.log('The following gUM error occured: ' + err);
      }
   );
} else {
   console.log('getUserMedia not supported on your browser!');
}

The whole thing is wrapped in a test that checks whether gUM is supported before running anything else. Next, we call getUserMedia() and inside it define:

  • The constraints: Only audio is to be captured; MediaRecorder only supports audio currently anyway.
  • The success callback: This code is run once the gUM call has been completed successfully.
  • The error/failure callback: The code is run if the gUM call fails for whatever reason.

Note: All of the code below is placed inside the gUM success callback.

Capturing the media stream

Once gUM has grabbed a media stream successfully, you create a new Media Recorder instance with the MediaRecorder() constructor and pass it the stream directly. This is your entry point into using the MediaRecorder API — the stream is now ready to be captured straight into a Blob, in the default encoding format of your browser.

var mediaRecorder = new MediaRecorder(stream);

There are a series of methods available in the MediaRecorder interface that allow you to control recording of the media stream; in Web Dictaphone we just make use of two. First of all, MediaRecorder.start() is used to start recording the stream into a Blob once the record button is pressed:

record.onclick = function() {
  mediaRecorder.start();
  console.log(mediaRecorder.state);
  console.log("recorder started");
  record.style.background = "red";
  record.style.color = "black";
}

When the MediaRecorder is recording, the MediaRecorder.state property will return a value of “recording”.

Second, we use the MediaRecorder.stop() method to stop the recording when the stop button is pressed, and finalize the Blob ready for use somewhere else in our application.

stop.onclick = function() {
  mediaRecorder.stop();
  console.log(mediaRecorder.state);
  console.log("recorder stopped");
  record.style.background = "";
  record.style.color = "";
}

When recording has been stopped, the state property returns a value of “inactive”.

Note that there are other ways that a Blob can be finalized and ready for use:

  • If the media stream runs out (e.g. if you were grabbing a song track and the track ended), the Blob is finalized.
  • If the MediaRecorder.requestData() method is invoked, the Blob is finalized, but recording then continues in a new Blob.
  • If you include a timeslice property when invoking the start() method — for example start(10000) — then a new Blob will be finalized (and a new recording started) each time that number of milliseconds has passed.

Grabbing and using the blob

When the blob is finalized and ready for use as described above, a dataavailable event is fired, which can be handled using a mediaRecorder.ondataavailable handler:

mediaRecorder.ondataavailable = function(e) {
  console.log("data available");
 
  var clipName = prompt('Enter a name for your sound clip');
 
  var clipContainer = document.createElement('article');
  var clipLabel = document.createElement('p');
  var audio = document.createElement('audio');
  var deleteButton = document.createElement('button');
 
  clipContainer.classList.add('clip');
  audio.setAttribute('controls', '');
  deleteButton.innerHTML = "Delete";
  clipLabel.innerHTML = clipName;
 
  clipContainer.appendChild(audio);
  clipContainer.appendChild(clipLabel);
  clipContainer.appendChild(deleteButton);
  soundClips.appendChild(clipContainer);
 
  var audioURL = window.URL.createObjectURL(e.data);
  audio.src = audioURL;
 
  deleteButton.onclick = function(e) {
    evtTgt = e.target;
    evtTgt.parentNode.parentNode.removeChild(evtTgt.parentNode);
  }
}

Let’s go through the above code and look at what’s happening.

First, we display a prompt asking the user to name their clip.

Next, we create an HTML structure like the following, inserting it into our clip container, which is a <section> element.

<article class="clip">
  <audio controls></audio>
  <p><em>your clip name</em></p>
  <button>Delete</button>
</article>

After that, we create an object URL pointing to the event’s data attribute, using window.URL.createObjectURL(e.data): this attribute contains the Blob of the recorded audio. We then set the value of the <audio> element’s src attribute to the object URL, so that when the play button is pressed on the audio player, it will play the Blob.

Finally, we set an onclick handler on the delete button to be a function that deletes the whole clip HTML structure.

Conclusion

And there you have it; MediaRecorder should serve to make your app media recording needs easier. Have a play around with it and let us know what you think: we are looking forward to seeing what you’ll build!

View full post on Mozilla Hacks – the Web developer blog

VN:F [1.9.22_1171]
Rating: 0.0/10 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)

Coordinate Conversion Made Easy – the power of GeometryUtils

In a previous post we introduced the GeometryUtils interface and the getBoxQuads() API for retrieving the CSS box geometry of a DOM node. GeometryUtils also takes care of another important problem: converting coordinates reliably from one DOM node to another. For example, you might want to find the bounding-box of one element relative to another element, or you might want to convert event coordinates from the viewport to some arbitrary element.

Existing APIs

Until now, simple cases could be handled using getBoundingClientRect() and some math, but complex cases (e.g. involving CSS transforms) were almost impossible to handle using standard APIs. The nonstandard APIs webkitConvertPointToPage and webkitConvertPageToPoint are a big improvement, but apart from not being standardized, they’re not as powerful as they need to be. In particular it’s more convenient and more robust to provide an API for directly converting coordinates from one element to another.[1]

New APIs

GeometryUtils introduces three new methods for coordinate conversion:

  • to.convertPointFromNode(point, from) converts a a point relative to the top-left of the first border-box of “from” to a point relative to the top-left of the first border-box of “to”. The point is a DOMPointInit, which means you can pass a DOMPoint or a JS object such as {x:0, y:0}.
  • to.convertRectFromNode(rect, from) converts a a DOMRect relative to the top-left of the first border-box of “from” to a DOMQuad relative to the top-left of the first border-box of “to” by converting the vertices of the DOMRect. It converts to a DOMQuad to ensure that the result is accurate even if it needs to be rotated or skewed by CSS transforms.
  • to.convertQuadFromNode(quad, from) converts a DOMQuad from “from” to “to”. It’s just like convertRectFromNode except for taking a DOMQuad.

As with getBoxQuads, a node can be an Element, TextNode or Document; when a Document is used, the coordinates are relative to the document’s viewport.

Example:

<div id="d" style="position:absolute; transform:rotate(45deg); left:100px; top:100px; width:100px; height:100px;"></div>
<div id="e" style="position:absolute; left:100px; top:100px; width:100px; height:100px;"></div>
var p1 = document.convertPointFromNode({
    x:0, y:0
  }, document.getElementById("e")
);
// p1.x == 100, p1.y == 100
 
var p2 = document.convertPointFromNode({
    x:0, y:0
  }, document.getElementById("d")
);
// p2.x == 150, p2.y == 150 - 50*sqrt(2) (approx)
 
p2 = document.getElementById("e").convertPointFromNode({
    x:0, y:0
  }, document.getElementById("d")
);
// p2.x == 50, p2.y == 50 - 50*sqrt(2) (approx)
 
var q1 = document.convertRectFromNode(
  new DOMRect(0, 0, 50, 50), 
  document.getElementById("e")
);
// q1.p1.x == 100, q1.p1.y == 100
// q1.p2.x == 150, q1.p2.y == 100
// q1.p3.x == 150, q1.p3.y == 150
// q1.p4.x == 100, q1.p4.y == 150
 
var q2 = document.convertQuadFromNode(
  new DOMQuad({
    x:60, y:50
  }, {
    x:90, y:50
  }, {
    x:100, y:100
  }, {
    x:50, y:100
  }), 
  document.getElementById("e")
);
// q2.p1.x == 100, q2.p1.y == 100
// q2.p2.x == 150, q2.p2.y == 100
// q2.p3.x == 140, q2.p3.y == 150
// q2.p4.x == 110, q2.p4.y == 150
p1
p2

Sometimes it’s useful to convert to or from an element’s CSS content-box, padding-box or margin-box. This is supported via an optional ConvertCoordinateOptions dictionary with the following options:

  • fromBox: one of "content", "padding", "border" or "margin", selecting which CSS box of the first fragment of the from node the input point(s) are relative to.
  • toBox: selects which CSS box of the first fragment of the to node the returned point(s) are relative to.

As a special case, this makes it easy to convert points between different
CSS box types of the same element. For example, to convert a point from an
element’s border-box to be relative to its content-box, use
element.convertPointFromNode(point, element, {toBox:"content"}).

Example:

<div id="e" style="position:absolute; padding:20px; left:100px; top:100px; width:60px; height:60px;"></div>
var p1 = document.convertPointFromNode({
    x:0, y:0
  }, document.getElementById("e"), 
  {fromBox:"content"}
);
// p1.x == 120, p1.y == 120
 
p1 = document.getElementById("e").convertPointFromNode({
    x:120, y:120
  }, document,
  {toBox:"content"}
);
// p1.x == 0, p1.y == 0
 
p1 = document.getElementById("e").convertPointFromNode({
    x:0, y:0
  }, document.getElementById("e"), 
  {fromBox:"content"}
);
// p1.x == 20, p1.y == 20
 
p1 = document.getElementById("e").convertPointFromNode({
    x:20, y:20
  }, document.getElementById("e"), 
  {toBox:"content"}
);
// p1.x == 0, p1.y == 0
p1
e content-box
e border-box

These APIs are available in Firefox nightly builds and should be released in Firefox 31. Firefox is the first browser to implement these APIs.

Footnote

[1] Consider the following example:

<div style="transform:scale(0)">
  <div id="a">...<>
  <div id="b">...<>
</div>

In this case, converting a point relative to a to be relative to b by converting first to page coordinates and then back to b doesn’t work, because the scale(0) maps every point in a to a single point in the page.

View full post on Mozilla Hacks – the Web developer blog

VN:F [1.9.22_1171]
Rating: 0.0/10 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)

Too easy – didn’t learn – my keynote at jQuery Europe 2014

christian heilmann at jQuery Europe 2014

I am right now on the plane back to England after my quick stint at Vienna giving the keynote at jQuery Europe 2014. True to my ongoing task to challenge myself as a speaker (and as announced here before) I made a bit of a bet by giving a talk that is not in itself technical, but analytical of what we do as developers. The talk was filmed and if you can’t wait, I made the slides available and recorded a screencast (with low sound, sorry).

There is also a audio recording on SoundCloud and on archive.org.

Quick keynote recap

In the keynote, I tried to analyse the massive discrepancy between what we as web developers get and how happy we seem to be.

We are an elite group in the job market: we are paid well, our work environment is high-tech and our perks make other people jealous. We even get the proverbial free lunches.

And yet our image is that of unsatisfied, hard to work with people who need to be kept happy and are socially awkward. I was confused that a group with all the necessary creature comforts is not an example of how easy working together could be. Instead, we even seem to need codes of conduct for our events to remind people not to behave badly towards people of the other sex or cultural background. Are we spoiled? Are we just broken? Or is there more?

I’ve found a few reasons why we can come across as unsatisfied and hard to handle and the biggest to me was that whilst we are getting pampered, we lack real recognition for what we do.

When you get a lot, but you yourself feel you are not really doing much, you are stuck between feeling superior to others who struggle with things you consider easy and feeling like a fraud. Instead of trying to communicate out about what we do, how much work it involves and why we do things in a certain way we seem to flee into a world of blaming our tools and trying to impress one another.

Initial Feedback

I am very happy to report that the feedback I got at the event was very good. I had some criticism, which is great as it gives me something to think about. And I had some heartfelt remarks from people who said I’ve opened their eyes a bit as to why they behaved in a certain way and now know how to fix some issues and clashes they had.

Want more?

I don’t want to repeat it all here again – if wanted, I could write a larger article on the subject to be published somewhere with more eyeballs. Simply listen to the recording or wait for the video to be released.

Material

I couldn’t have done this without watching some other talks and reading some other posts, so here are links to the materials used:

Thanks

I want to thank the audience of jQuery Europe for listening and being open to something different. I also want to thank the organisers for taking the chance (and setting the speakers up in the most epic hotel I ever stayed in). I also want to point out that another talk at jQuery Europe 2014 – “A Web Beyond Touch” by Petro Salema was one of the most amazing first stage performances by a speaker I have seen. So keep your eyes open for this video.

Photo by Douglas Neiner

View full post on Christian Heilmann

VN:F [1.9.22_1171]
Rating: 0.0/10 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)

Wercker – Continuous Delivery Made Easy – a webFWD project

There is a great quote by Marc Andreessen who said that “software is eating the world”. What Marc means by this is that software is defining every industry we know; we’re no longer buying records at our local retailer but stream them via Rdio or Spotify. Skype is now the largest telecommunications provider and we’re even talking about the software-defined data-center.

The cloud has become the defacto distribution mechanism for these software services, but has also allowed for disruptive change in how these services are delivered and consumed. Whereas it used to be the case that you would have to purchase a new version of your favorite word processing software at your local retailer, with the cloud, updates can be pushed out incrementally.

Introducing wercker

The key enabler for this new way of developing software is continuous delivery. Software is eating the world, and wercker makes it taste better.

There are several successful companies out there which are big proponents of continuous delivery such as Netflix and Etsy. It is wercker’s mission to democratize continuous delivery for every developer and was founded on this very same premise in the beginning of last year by me, Micha Hernandez van Leuffen, and my cofounder Wouter Mooij out of frustration with existing solutions.

This video introduces wercker and presents our vision on the product:

How it works

Wercker’s flow is simple; it integrates with popular version controls platforms such as Github and Bitbucket on one end and Infrastructure-as-a-Service providers and Platform clouds like Heroku on the other end.

We run any unit tests you might have included in your project and subsequently present the results on a comprehensive dashboard.

You are able to define different environments or deploy targets for, for instance staging or production to which you can deploy your project with a push of a button.

Software is better developed together so wercker also captures the social dynamics that are paired with continuous delivery. The activity feed showcases who in your team broke the build or deployed to which environment. This increases transparency and trust within your team.

Open Source

Apart from offering wercker for free to open source projects we are also in the process of opening up wercker’s build environments. These environments are similar to Heroku’s buildpacks, allowing developers to define not only their own programming stack that they would like to use on wercker, but also the various build and test steps that they want to run.

New languages and frameworks can be integrated with ease as we’ve built these environments upon Chef cookbooks which can subsequently be used for both provisioning and deployment as well. Cookbooks and recipes are already a very big open source movement, which we’re stimulating even more.

The Future

We’re very excited that we’ve raised a seed round led by Shamrock Ventures, Amsterdam-based MicroVC Vitulum Ventures and Greylock Partners. The funding will help us grow our platform and expand our operations.

If you are a developer, sign up for the beta at http://beta.wercker.com. We are also interested in hearing what programming stacks developers are leveraging for their applications and to which environments they are deploying.

View full post on Mozilla Hacks – the Web developer blog

VN:F [1.9.22_1171]
Rating: 0.0/10 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)