[Snowy] Very basic HTML5 app to talk to the Snowy API



So, since Snowy's API is all HTTP, and HTML5 apps are the new hawt, it
ought to be possible to combine the two.

Bit of background, here. It is possible to build an HTML page which can
appear as an application on a smartphone, like Androids or iPhones. It's
also possible to build an HTML page which is cached by the browser on
these phones, so that it works without actually hitting the internet.
It's *also* possible for HTML pages to store data locally. At that
point, that "HTML page" is basically an application, no? Mobile Safari
on the iPhone has an "add to front screen" button, which "installs" the
app on your iPhone; Android lets you bookmark it and then add the
bookmark to your front screen. All this lot, put together, is called
"HTML5 apps", and it's pretty cool.

So, back to the point. It oughta therefore be possible to build an HTML5
app which talks to the Snowy HTTP API to get your notes, stores them in
its local storage database, and works offline. So, it does roughly what
Tomdroid does.

Below is some HTML that does this.

Be told: it is very basic at the moment. Specifically, it probably
doesn't implement the sync algorithm quite right, and importantly it
doesn't let you edit notes; just view them. However, it should be a
useful basis for doing more of this inside Snowy.

HTML page below, which is a straight cut-and-paste from my local code
and will therefore need tweaking. It also requires iUI, available from
http://code.google.com/p/iui/. See the end for some notes, which are
important.

------------------8<---------------------------
<!DOCTYPE HTML>
<html manifest="html5app.manifest">
<head>
    <title>Ubuntu One: Notes</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <meta name="viewport" content="width=device-width;
initial-scale=1.0; maximum-scale=1.0; user-scalable-0;">
    <meta name="apple-touch-fullscreen" content="YES">
    <link rel="shortcut icon" href="/media/img/fav.ico">
    <link href="/media/apple-touch-icon.png" rel="apple-touch-icon">

<style type="text/css" media="screen">@import "/media/iui/iui.css";</style>

    <style type="text/css" media="screen">
    .footer{padding:30px 0
10px;font-size:10px!important;color:#999;text-align:center;}
    .footer p{margin-bottom:4px}
    .footer p.plain a{color:inherit;}
    </style>
    <script type="application/x-javascript"
src="/media/iui/iui.js"></script>
</head>
<body>


<div class="toolbar">
    <h1 id="pageTitle">Notes</h1>
    <a id="backButton" class="button" href="#"></a>
    <a class="button" href="#sync" id="syncbutton">Sync</a>
</div>
<ul id="home" selected="true">
</ul>
<div id="new">
new note!
</div>
<div id="sync" class="panel">
<h2>Synchronising notes with Ubuntu One</h2>
<fieldset>
</fieldset>
</div>
<script>
iui.animOn = true;

var u1notes = {
    CLIENT_LATEST_SYNC_REVISION: "client-latest-sync-revision",
    CLIENT_LATEST_CHANGE: "client-latest-change",
    CLIENT_LATEST_SYNC_TIME: "client-latest-sync-time",

    init: function() {
        document.getElementById("syncbutton").onclick = u1notes.sync;
        u1notes.currentlySyncing = false;
        u1notes.populateNotesList();
    },

    getLocalNotes: function() {
        var localnotes = [];
        for (i=0; i<=localStorage.length-1; i++) {
            key = localStorage.key(i);
            if (key.match(/^guid-/)) {
                localnotes.push(JSON.parse(localStorage.getItem(key)));
            }
        }
        return localnotes;
    },

    populateNotesList: function() {
        var notes = u1notes.getLocalNotes();
        var hm = document.getElementById("home");
        hm.innerHTML = "";
        for (i=0; i<notes.length; i++) {
            var note = notes[i];
            var divid = "guid-" + note.guid;
            var ndiv = document.getElementById(divid);
            if (!ndiv) {
                ndiv = document.createElement("div");
                ndiv.id = divid;
                document.body.appendChild(ndiv);
            }
            ndiv.innerHTML = note["note-content"];
            var nli = document.createElement("li");
            var nlia = document.createElement("a");
            nlia.href = "#guid-" + note.guid;
            nlia.appendChild(document.createTextNode(note.title))
            nli.appendChild(nlia);
            hm.appendChild(nli);
        }
    },

    sync: function(e) {
        if (!u1notes.currentlySyncing) {
            u1notes.currentlySyncing = true;
            document.querySelector("#sync fieldset").innerHTML = "";
            u1notes.addSyncLine("Start", new Date());
            u1notes.api_request("op/", function(basenotedata) {
                var clientLatestSyncRevision =
localStorage.getItem(u1notes.CLIENT_LATEST_SYNC_REVISION);
                if (!clientLatestSyncRevision) clientLatestSyncRevision
= -1;
                var serverLatestSyncRevision =
parseInt(basenotedata["latest-sync-revision"]);
                var proposedSyncRevision = serverLatestSyncRevision;
                var clientLatestChangeTime =
parseInt(localStorage.getItem(u1notes.CLIENT_LATEST_CHANGE));
                if (!clientLatestChangeTime) clientLatestChangeTime = 0;
                var clientLatestSyncTime =
parseInt(localStorage.getItem(u1notes.CLIENT_LATEST_SYNC_TIME));
                if (!clientLatestSyncTime) clientLatestSyncTime = 0;
                u1notes.addSyncLine("Revnos", "clientLatestSyncRevision:" +
                    clientLatestSyncRevision + ",
serverLatestSyncRevision:" +
                    serverLatestSyncRevision);
                if (serverLatestSyncRevision > clientLatestSyncRevision
|| clientLatestChangeTime > clientLatestSyncTime) {
                    // something has changed on either server or client,
so we need to sync

                    // Get list of note updates since
client.LastSyncRevision
                    u1notes.api_request("op/?include_notes=true&since="
+ clientLatestSyncRevision, function(serverupdates) {

                        // Next, process updates from the server:
                        // Foreach new or updated note in the list of
updates
                        for (var i=0; i<serverupdates.notes.length; i++) {
                            // Look for a local note with the same GUID
                            var localguid = "guid-" +
serverupdates.notes[i].guid;
                            var localnote = localStorage.getItem(localguid);
                            if (!localnote) {
                                // If there is no note with the same
GUID, this is a new note
                                // Create a new local note from the update

serverupdates.notes[i]["last-sync-revision"] = proposedSyncRevision;
                                localStorage.setItem(localguid,
JSON.stringify(serverupdates.notes[i]));
                                u1notes.addSyncLine("New",
serverupdates.notes[i].title);
                            } else {
                                // If there is a note with the same
GUID, this is an updated note
                                var note_change_time =
parseInt(localnote["last-change-time"]);
                                if (!note_change_time) note_change_time = 0;
                                if (note_change_time <
clientLatestSyncTime) {
                                    // If the local note has not been
modified since client.LastSyncDate
                                    // apply the update from the server

serverupdates.notes[i]["last-sync-revision"] = proposedSyncRevision;

serverupdates.notes[i]["last-change-time"] = (new
Date()).getTime().toString();
                                    localStorage.setItem(localguid,
JSON.stringify(serverupdates.notes[i]));
                                    u1notes.addSyncLine("Received",
serverupdates.notes[i].title);
                                } else {
                                    // If the local note *has* been
modified since client.LastSyncDate
                                    // there is a conflict that must be
handled
                                    u1notes.addSyncLine("Conflict",
"Conflict on " + localnote.title);
                                }
                            }
                        }
                        u1notes.completeSync(proposedSyncRevision);
                    });
                } else {
                    u1notes.addSyncLine("No change", "");
                    u1notes.completeSync(proposedSyncRevision);
                }
            });
        }
    },

    completeSync: function(proposedSyncRevision) {
        var dt = new Date();
        localStorage.setItem(u1notes.CLIENT_LATEST_SYNC_TIME,
dt.getTime().toString());
        u1notes.addSyncLine("Complete", new Date());
        u1notes.currentlySyncing = false;
        localStorage.setItem(u1notes.CLIENT_LATEST_SYNC_REVISION,
proposedSyncRevision);
        u1notes.populateNotesList();
    },

    addSyncLine: function(lbl, data) {
        var fs = document.querySelector("#sync fieldset");
        var d = document.createElement("div");
        d.className = "row";
        var l = document.createElement("label");
        l.appendChild(document.createTextNode(lbl));
        var s = document.createElement("span");
        s.appendChild(document.createTextNode(data));
        d.appendChild(l);
        d.appendChild(s);
        fs.appendChild(d);
    },

    api_request: function(url, cb) {
        var rq_url = "api/1.0/" + url;
        var xhr = new XMLHttpRequest();
        xhr.open("GET", rq_url, true);
        console.log(rq_url);
        var aborttimer = setTimeout(function() {
            xhr.abort();
            u1notes.addSyncLine("Offline", "Couldn't contact Ubuntu One");
            u1notes.currentlySyncing = false;
        }, 5000);
        xhr.onreadystatechange = function() {
            if (xhr.readyState == 4) {
                clearTimeout(aborttimer);
                if (xhr.status != 200) {
                    u1notes.addSyncLine("Error", "The Ubuntu One servers
returned an error (" + xhr.status + ")");
                    u1notes.currentlySyncing = false;
                    return;
                }
                cb(JSON.parse(xhr.responseText));
            }
        }
        xhr.send();
    }

}
u1notes.init();


</script>
</body>
</html>
------------------8<---------------------------

Notes:

You have to provide html5app.manifest. It's a text file which looks like
this:

------------------8<---------------------------
CACHE MANIFEST
#version 1272748372
/media/iui/iui.css
/media/iui/iui.js
/media/iui/toolbar.png
/media/iui/toolButton.png
/media/iui/backButton.png
/media/iui/listArrow.png
/media/iui/selection.png
/media/iui/loading.gif
/media/iui/blueButton.png
/media/iui/listGroup.png
/media/iui/listArrowSel.png

NETWORK:
/notes/api
------------------8<---------------------------

and it *must* be served with mimetype text/cache-manifest or it won't work.

It relies on being able to hit the Snowy API without authentication.
Because it's served as *part* of Snowy, it should be able to do this,
but if Snowy's configured to *only* allow OAuth to the API (and not
cookie auth for logged-in users) then that'll need tweaking.

Have fun.

sil


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]