nicer spinner animation, some cleaning
[wl-mobile.git] / assets / www / js / catalogue.js
index db9c069..615f30f 100644 (file)
-// FIXME: htmlescape strings!
-
-var VERSION = '0.1';
-
-
-var FileRepo = new function() {
-       /* API for files repository */
-       var self = this;
-       const WL_URL = 'http://www.wolnelektury.pl';
-       this.root = null;
-
-       this.init = function(success, error) {
-               self.initRoot(success);
-       };
-
-       this.initRoot = function(success) {
-               // fs size is irrelevant, PERSISTENT is futile (on Android, at least)
-               window.requestFileSystem(LocalFileSystem.TEMPORARY, 0, function(fs) {
-                       console.log('local fs found: ' + fs.root.fullPath);
-                       self.root = fs.root;
-                       success && success();
-               }, function() {
-                       console.log('local fs not found');
-                       success && success();
-               });
-       };
-
-
-       this.withLocalHtml = function(book_id, success, error) {
-               console.log('info:withLocalHtml: id:' + book_id);
-               View.spinner('Otwieranie treści utworu');
-               if (!self.root)
-                       error && error('info:withLocalHtml: no local html: no usable filesystem');
-
-               var url = "file://" + self.root.fullPath + "/html/" + book_id;
-               console.log('info:withLocalHtml: local ajax: ' + url);
-               var xhr = new XMLHttpRequest();
-               xhr.open('GET', url, true);
-               xhr.onload = function() {
-                       console.log('info:withLocalHtml: fetched by local ajax: ' + url);
-                       success && success(xhr.responseText);
-               }
-               xhr.onerror = error;
-               xhr.send();
-       };
-
-
-       // downloads HTML file from server, saves it in cache and calls success with file contents
-       this.withHtmlFromServer = function(book_id, success, error) {
-               console.log('info:withHtmlFromServer: id:' + book_id);
-               // read file from WL
-               Catalogue.withBook(book_id, function(book) {
-                       var url = WL_URL + book.html_file;
-                       console.log('info:withHtmlFromServer: fetching url: ' + url);
-
-                       View.spinner("Pobieranie treści utworu z sieci");
-
-                       if (self.root) {
-                               window.plugins.downloader.downloadFile(url, self.root.fullPath + "/html/", ""+book_id, true,
-                                       function(data){
-                                               console.log('info:withHtmlFromServer: loaded file from WL');
-                                               self.withLocalHtml(book_id, success, error);
-                                       }, function(data) {
-                                               console.log('error downloading file!')
-                                               error && error("error: "+data);
-                                       });
-                       }
-                       else {
-                               // there's no big fs, so we'll just get the text from AJAX
-                               console.log('info:withHtmlFromServer: ajax: ' + url);
-                               var xhr = new XMLHttpRequest();
-                               xhr.open(url);
-                               xhr.onload = function() {
-                                       console.log('info:withHtmlFromServer: fetched by ajax: ' + url);
-                                       success && success(xhr.responseText);
-                               }
-                               xhr.send();
-                       }
-               });             
-       };
-       
-       // calls the callback with contents of the HTML file for a given book,
-       // loaded from the server and cached locally
-       this.withHtml = function(id, success, error) {
-               console.log('info:withHtml: id:' + id);
-               self.withLocalHtml(id, success, function() {
-                       self.withHtmlFromServer(id, success, error);
-               });
-       };
-};
-
-
-var View = new function() {
-       var self = this;
-       self.minOffset = 1000;
-       //self.element
-       self.categories = {
-                       autor: 'Autorzy', 
-                       rodzaj: 'Rodzaje',
-                       gatunek: 'Gatunki',
-                       epoka: 'Epoki'
-       };
-       
-
-       self.init = function() {
-               console.log('View.init');
-
-               self.viewStack = [];
-               self.current;
-               navigator.app.overrideBackbutton(); 
-               document.addEventListener("backbutton", View.goBack, true);
-
-               self._searchbox = document.getElementById("searchbox");
-               self._searchinput = document.getElementById("search");
-               self._content = document.getElementById("content");
-
-               self.enter(location.href);
-       };
-
-
-       this.sanitize = function(text) {
-               return text.replace(/&/g, "&amp;").replace(/</g, "&lt;");
-       };
-
-       this.showSearch = function() {
-               self._searchbox.style.display = "block";
-       };
-
-       this.hideSearch = function() {
-               self._searchbox.style.display = "none";
-       };
-
-       this.spinner = function(text) {
-               if (!text)
-                       text = "Ładowanie";
-               self._content.innerHTML = "<div class='spinner'><img src='img/spinner.gif' /><br/><span id='spinnertext'>" + text +"</span></div>";
-               scroll(0, 0);
-       };
-
-       this.content = function(text) {
-               console.log('content');
-
-               self._content.innerHTML = '';
-               self._content.innerHTML = text;
-               scroll(0, 0);
-       }
-       
-       this.enter = function(url) {
-               console.log('View.enter: ' + url);
-
-               self.current = url;
-               var view = 'Index';
-               var arg = null;
-
-               var query_start = url.indexOf('?');
-               if (query_start != -1) {
-                       var slash_index = url.indexOf('/', query_start + 1);
-                       if (slash_index != -1) {
-                               view = url.substr(query_start + 1, slash_index - query_start - 1);
-                               arg = url.substr(slash_index + 1);
-                       }
-                       else {
-                               view = url.substr(query_start + 1);
-                       }
-               }
-               console.log('View.enter: ' + view + ' ' + arg);
-               self['enter' + view](arg);
-       }
-       
-       this.enterIndex = function(arg) {
-               console.log('enterIndex');
-               self.showSearch();
-               var html = "<div class='book-list'>";
-               for (category in self.categories)
-                       html += self.a('Category', category) + self.categories[category] + "</a>\n"; 
-               html += "</div>" +
-                               "<p id='logo'><img src='img/wl-logo.png' alt='Wolne Lektury' /><br/>\n" +
-                               "szkolna biblioteka internetowa" +
-                               //"<br/>v. " + VERSION +
-                               "</p>";
-               self.content(html);
-       };
-       
-       this.enterBook = function(id) {
-               id = parseInt(id);
-               console.log('enterBook: ' + id);
-               self.showSearch();
-
-               Catalogue.withBook(id, function(book) {
-               Catalogue.withChildren(id, function(children) {
-               Catalogue.withAuthors(id, function(authors) {
-                       var html = "<h1><span class='subheader'>";
-                       var auths = [];
-                       for (a in authors) auths.push(authors[a].name);
-                       html += auths.join(", ");
-                       html += "</span>" + book.title + "</h1>\n";
-                       if (book.html_file) {
-                               html += "<p class='buttons'>" + self.a('BookText', id) + "Czytaj tekst</a></p>";
-                       }
-                       if (children.length) {
-                               html += "<div class='book-list'>";
-                               for (c in children) {
-                                       child = children[c];
-                                       html += self.a('Book', child.id) + child.title + "</a>\n";
-                               }
-                               html += "</div>";
-                       }
-                       self.content(html);                             
-               });
-               });
-               });
-       }
-       
-       this.enterBookText = function(id) {
-               id = parseInt(id);
-               self.spinner("Otwieranie utworu");
-               console.log('enterBookText: ' + id);
-               self.hideSearch();
-               
-               FileRepo.withHtml(id, function(data) {
-                       self.content(data);
-               });
-       }
-
-       this.enterTag = function(id) {
-               id = parseInt(id);
-               console.log('enterTag: ' + id);
-               self.showSearch();
-
-               self.spinner("Otwieranie listy utworów");
-
-               Catalogue.withTag(id, function(tag) {
-                       var html = "<h1><span class='subheader upper'>" + tag.category + ': </span>' + tag.name + "</h1>\n";
-                       html += "<div class='book-list'>";
-                       if (tag._books) {
-                               Catalogue.withBooks(tag._books, function(books) {
-                                       for (var i in books) {
-                                               var book = books[i];
-                                               html += self.a('Book', book.id) + book.title + "</a>\n";
-                                       }
-                                       html += "</div>";
-                                       self.content(html);
-                               });
-                       }
-               });
-       };
-
-
-       this.enterCategory = function(category) {
-               console.log('enterCategory: ' + category);
-               self.spinner("Otwieranie katalogu");
-               self.showSearch();
-
-               Catalogue.withCategory(category, function(tags) {
-                       var html = "<h1>" + self.categories[category] + "</h1>\n";
-                       html += "<div class='book-list'>";
-                       for (i in tags) {
-                               tag = tags[i];
-                               html += self.a('Tag', tag.id) + tag.name + "</a>\n";
-                       }
-                       html += "</div>";
-                       self.content(html);
-               });
-       };
-
-
+/*
+ * This file is part of WolneLektury-Mobile, licensed under GNU Affero GPLv3 or later.
+ * Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
+ */
 
 
-       this.enterSearch = function(query) {
-               console.log('enterTag: ' + query);
-               self.showSearch();
+var DB_VER = '0.9.15';
 
 
-               var html = "<h1><span class='subheader'>Szukana fraza:</span>" + View.sanitize(query) + "</h1>\n";
+var WL_INITIAL = WL + '/media/api/mobile/initial/initial.db';
+var WL_UPDATE = WL + '/api/changes/SINCE.json?book_fields=author,html,parent,parent_number,sort_key,title' +
+               '&tag_fields=books,category,name,sort_key' +
+               '&tag_categories=author,epoch,genre,kind';
 
 
-               if (query.length < 2) {
-                       html += "<p>Szukana fraza musi mieć co najmniej dwa znaki</p>";
-                       self.content(html);
-                       return;
-               }
 
 
-               Catalogue.withSearch(query, function(results) {
-                       if (results.length == 1) {
-                               self.enter(self.href(results[0].view, results[0].id));
-                               return;
-                       }
-                       if (results.length == 0) {
-                               html += "<p>Brak wyników wyszukiwania</p>";
-                       }
-                       else {
-                               html += "<div class='book-list'>";
-                               for (var i in results) {
-                                       var result = results[i];
-                                       html += self.a(result.view, result.id) + result.label + "</a>\n";
-                               }
-                               html += "</div>";
-                       }
-                       self.content(html);
-               });
-       };
 
 
+var categories = {'author': 'autor',
+              'epoch': 'epoka', 
+              'genre': 'gatunek', 
+              'kind': 'rodzaj', 
+              'theme': 'motyw'
+              }
 
 
-       /* search form submit callback */
-       this.search = function() {
-               self.goTo('?Search/' + self._searchinput.value);
-               return false;
-       }
-       
-       
-       this.href = function(view, par) {
-               return "?"+view+"/"+par;
-       };
-       
-       this.a = function(view, par) {
-               return "<a class='"+view+"' onclick='View.goTo(\"" + 
-                                       self.href(view, par).replace(/["']/g, "\\$&") + "\");'>";
-       };
-       
-       this.goTo = function(url) {
-               self.viewStack.push(self.current);
-               console.log('goTo: ' + url);
-               self.enter(url);
-       };
-       
-       this.goBack = function() {
-               if (self.viewStack.length > 0) {
-                       var url = self.viewStack.pop();
-                       console.log('goBack: ' + url);
-                       self.enter(url);
-               }
-               else {
-                       console.log('exiting');
-                       navigator.app.exitApp();
-               }
-       };
-}
+// FIXME: htmlescape strings!
 
 
 
 
-/*
 // for preparing sql statements
 // use like: 
 //   var s = new Sql("INSERT ... '{0}', '{1}' ...";
 // for preparing sql statements
 // use like: 
 //   var s = new Sql("INSERT ... '{0}', '{1}' ...";
@@ -350,10 +37,10 @@ var Sql = function(scheme) {
        self.prepare = function() {
                var args = arguments;
                return self.text.replace(/{(\d+)}/g, function(match, number) {
        self.prepare = function() {
                var args = arguments;
                return self.text.replace(/{(\d+)}/g, function(match, number) {
-                       return self.sql_escape(args[number]);
+                       return self.sql_escape(args[parseInt(number)]);
                });
        }
                });
        }
-};*/
+};
 
 
 var Catalogue = new function() {
 
 
 var Catalogue = new function() {
@@ -396,86 +83,79 @@ var Catalogue = new function() {
 
        /* check if DB needs updating and upload a fresh copy, if so */
        this.updateDB = function(success, error) {
 
        /* check if DB needs updating and upload a fresh copy, if so */
        this.updateDB = function(success, error) {
-               var db_ver = '0.1.4';
-               if (window.localStorage.getItem('db_ver') == db_ver) {
+               var has_ver = window.localStorage.getItem('db_ver');
+               if (has_ver == DB_VER) {
                        console.log('db ok, skipping')
                        success && success();
                        return;
                }
 
                var done = function() {
                        console.log('db ok, skipping')
                        success && success();
                        return;
                }
 
                var done = function() {
-                       window.localStorage.setItem('db_ver', db_ver);
+                       FileRepo.clear();
+                       window.localStorage.setItem('db_ver', DB_VER);
                        console.log('db updated');
                        success && success();
                };
 
                // db initialize
                // this is Android-specific for now
                        console.log('db updated');
                        success && success();
                };
 
                // db initialize
                // this is Android-specific for now
-               var version = device.version.split('.')[0];
-               switch(version) {
-               case '1':
-                       self.upload_db_android1(done, error);
-                       break;
-               case '2':
-                       self.upload_db_android2(done, error);
-                       break;
-               case '3':
-               default:
-                       error && error('Błąd migracji: ' + err);
+               var android = device.version.split('.')[0];
+               if (android > 1) {
+                       self.upload_db_android(done, error);
+               } else {
+                       error && error("Nieobsługiwana wersja systemu. Wymagany Android>=2.0.");
                };
        };
 
 
                };
        };
 
 
-       this.upload_db_android1 = function(success, error) {
-               console.log('upload db for Android 1.x');
-               window.requestFileSystem(LocalFileSystem.APPLICATION, 0, function(fs) {
-                       window.plugins.assetcopy.copy("initial/wolnelektury.db",
-                                       fs.root.fullPath + "/databases/wolnelektury.db", true,
-                                       function(data) {
-                                               console.log('db upload successful');
-                                               success && success();
-                                       }, function(data) {
-                                               error && error("database upload error: " + data);
-                                       });
-               }, error);
+       this.upload_db_android = function(success, error) {
+               // TODO: this should be downloaded from teh net, not stored in res
+
+               console.log('upload db for Android 2.x+');
+               // upload databases description file
+
+               var dbname = "wolnelektury";
+               var db = window.openDatabase(dbname, "1.0", "WL Catalogue", 500000);
+               if (db) {
+                       console.log('db created successfully');
+                       DBPut.fetch(WL_INITIAL, function(data) {
+                               console.log('db fetch successful');
+                               success && success();
+                       }, function(data) {
+                               error && error('Błąd podczas pobierania bazy danych: ' + data);
+                       });
+               } else {
+                       error && error('Błąd podczas inicjowania bazy danych: ' + data);
+               }
        };
 
 
        };
 
 
-       this.upload_db_android2 = function(success, error) {
-               console.log('upload db for Android 2.x');
-               window.requestFileSystem(LocalFileSystem.APPLICATION, 0, function(fs) {
-
-                       // upload databases description file
-                       window.plugins.assetcopy.copy("initial/Databases.db",
-                               fs.root.fullPath + "/app_database/Databases.db", true,
-                               function(data) {
-                                       console.log('db descriptior upload successful');
-
-                                       // upload the database file
-                                       window.plugins.assetcopy.copy("initial/0000000000000001.db",
-                                               fs.root.fullPath + "/app_database/file__0/0000000000000001.db", true,
-                                               function(data) {
-                                                       console.log('db upload successful');
-                                                       success && success();
-                                               }, function(data) {
-                                                       error && error("database upload error: " + data);
-                                               });
-
-                                       
-                               }, function(data) {
-                                       error && error("database descriptor upload error: " + data);
+       this.withState = function(callback) {
+               self.db.transaction(function(tx) {
+                       tx.executeSql("SELECT * FROM state", [], 
+                               function(tx, results) {
+                                       if (results.rows.length) {
+                                               callback(results.rows.item(0));
+                                       }
+                                       else {
+                                               callback({last_checked: 0});
+                                       }
                                });
                                });
-
-               }, error);
+               });
        };
 
 
        };
 
 
-       this.withBook = function(id, callback) {
+       this.withBook = function(id, callback, error) {
                console.log('withBook '+id)
                self.db.transaction(function(tx) {
                        tx.executeSql("SELECT * FROM book WHERE id="+id, [], 
                                function(tx, results) {
                console.log('withBook '+id)
                self.db.transaction(function(tx) {
                        tx.executeSql("SELECT * FROM book WHERE id="+id, [], 
                                function(tx, results) {
-                                       callback(results.rows.item(0));
+                                       if (results.rows.length) {
+                                               callback(results.rows.item(0));
+                                       }
+                                       else {
+                                               error && error();
+                                       }
                                });
                });
        };
                                });
                });
        };
@@ -483,7 +163,7 @@ var Catalogue = new function() {
        this.withBooks = function(ids, callback) {
                console.log('withBooks ' + ids)
                self.db.transaction(function(tx) {
        this.withBooks = function(ids, callback) {
                console.log('withBooks ' + ids)
                self.db.transaction(function(tx) {
-                       tx.executeSql("SELECT * FROM book WHERE id IN ("+ids+") ORDER BY title", [], 
+                       tx.executeSql("SELECT * FROM book WHERE id IN ("+ids+") ORDER BY sort_key", [], 
                                function(tx, results) {
                                        var items = [];
                                        var count = results.rows.length;
                                function(tx, results) {
                                        var items = [];
                                        var count = results.rows.length;
@@ -495,29 +175,11 @@ var Catalogue = new function() {
                });
        };
 
                });
        };
 
-       this.withAuthors = function(id, callback) {
-               console.log('withAuthors ' + id);
 
 
-               self.db.transaction(function(tx) {
-                       tx.executeSql("SELECT t.name " +
-                                       "FROM book_tag bt LEFT JOIN tag t ON t.id=bt.tag " +
-                                       "WHERE bt.book="+id+" AND t.category='autor' " +
-                                       "ORDER BY t.sort_key", [], 
-                               function(tx, results) {
-                                       var tags = [];
-                                       var count = results.rows.length;
-                                       for (var i=0; i<count; ++i) {
-                                               tags.push(results.rows.item(i));
-                                       }
-                                       callback(tags);
-                               });
-               });
-       };
-       
        this.withChildren = function(id, callback) {
                console.log('withChildren ' + id)
                self.db.transaction(function(tx) {
        this.withChildren = function(id, callback) {
                console.log('withChildren ' + id)
                self.db.transaction(function(tx) {
-                       tx.executeSql("SELECT * FROM book WHERE parent="+id+" ORDER BY parent_number", [], 
+                       tx.executeSql("SELECT * FROM book WHERE parent="+id+" ORDER BY parent_number, sort_key", [], 
                                function(tx, results) {
                                        var books = [];
                                        var count = results.rows.length;
                                function(tx, results) {
                                        var books = [];
                                        var count = results.rows.length;
@@ -529,12 +191,17 @@ var Catalogue = new function() {
                });
        };
 
                });
        };
 
-       this.withTag = function(id, callback) {
+       this.withTag = function(id, callback, error) {
                console.log('withTag '+id)
                self.db.transaction(function(tx) {
                        tx.executeSql("SELECT * FROM tag WHERE id="+id, [], 
                                function(tx, results) {
                console.log('withTag '+id)
                self.db.transaction(function(tx) {
                        tx.executeSql("SELECT * FROM tag WHERE id="+id, [], 
                                function(tx, results) {
-                                       callback(results.rows.item(0));
+                                       if (results.rows.length) {
+                                               callback(results.rows.item(0));
+                                       }
+                                       else {
+                                               error && error();
+                                       }
                                });
                });
        };
                                });
                });
        };
@@ -542,7 +209,7 @@ var Catalogue = new function() {
        this.withCategory = function(category, callback) {
                console.log('withCategory ' + category)
                self.db.transaction(function(tx) {
        this.withCategory = function(category, callback) {
                console.log('withCategory ' + category)
                self.db.transaction(function(tx) {
-                       tx.executeSql("SELECT * FROM tag WHERE category='"+category+"'", [], 
+                       tx.executeSql("SELECT * FROM tag WHERE category='"+category+"' ORDER BY sort_key", [], 
                                function(tx, results) {
                                        var items = [];
                                        var count = results.rows.length;
                                function(tx, results) {
                                        var items = [];
                                        var count = results.rows.length;
@@ -554,71 +221,10 @@ var Catalogue = new function() {
        };
 
 
        };
 
 
-/*     this.withUnrelatedBooks = function(books, success) {
-               if (books.length == 0) return books;
-
-               var book_ids = {};
-               for (i in books) {
-                       book_ids[books[i].id] = true;
-               }
-               var new_books = [];
-
-               var addIfUnrelated = function(book) {
-                       var addIfUnrelated_wrapped = function(b) {
-                               if (b.parent) {
-                                       if (b.parent in book_ids) {
-                                               // go to next book
-                                               filterBooks();
-                                       }
-                                       else self.withBook(b.parent, addIfUnrelated_wrapped);
-                               }
-                               else {
-                                       new_books.push(book);
-                                       // go to next book
-                                       filterBooks();
-                               }
-                       };
-                       addIfUnrelated_wrapped(book);
-               };
-
-
-               var filterBooks = function() {
-                       console.log('filterBooks: ' + books.length);
-                       if (books.length) {
-                               addIfUnrelated(books.shift());
-                       }
-                       else {
-                               success && success(new_books);
-                       }
-               };
-               
-               filterBooks();
-       };
-
-
-       this.withBooksTagged = function(tag, callback, withChildren) {
-               self.db.transaction(function(tx) {
-                       tx.executeSql("SELECT book.* " +
-                                       "FROM book LEFT JOIN book_tag ON book_tag.book=book.id " +
-                                       "WHERE book_tag.tag="+tag+" ORDER BY book.title", [], 
-                               function(tx, results) {
-                                       var books = [];
-                                       var count = results.rows.length;
-                                       console.log('withBooksTagged('+tag+'): '+count);
-                                       for (var i=0; i<count; ++i) {
-                                               books.push(results.rows.item(i));
-                                       }
-                                       if (withChildren)
-                                               callback(books);
-                                       else self.withUnrelatedBooks(books, callback);
-                               });
-               });
-       };
-*/
-
        /* takes a query, returns a list of {view,id,label} objects to a callback */
        this.withSearch = function(term, callback) {
                console.log('searching...');
        /* takes a query, returns a list of {view,id,label} objects to a callback */
        this.withSearch = function(term, callback) {
                console.log('searching...');
+               term =  term.replace(/^\s+|\s+$/g, '') ;
                var found = [];
 
                function booksFound(tx, results) {
                var found = [];
 
                function booksFound(tx, results) {
@@ -628,8 +234,7 @@ var Catalogue = new function() {
                                var item = results.rows.item(i);
                                found.push({
                                        view: "Book",
                                var item = results.rows.item(i);
                                found.push({
                                        view: "Book",
-                                       id: item.id,
-                                       label: item.title
+                                       item: item
                                });
                        }
                };
                                });
                        }
                };
@@ -641,8 +246,7 @@ var Catalogue = new function() {
                                var item = results.rows.item(i);
                                found.push({
                                        view: "Tag",
                                var item = results.rows.item(i);
                                found.push({
                                        view: "Tag",
-                                       id: item.id,
-                                       label: item.name + ' (' + item.category + ')'
+                                       item: item
                                });
                        }
                        // TODO error handling
                                });
                        }
                        // TODO error handling
@@ -654,13 +258,13 @@ var Catalogue = new function() {
                // TODO pliterki, start of the word match
                self.db.transaction(function(tx) {
                        sql_term = self.sqlSanitize(term); // this is still insane, % and _
                // TODO pliterki, start of the word match
                self.db.transaction(function(tx) {
                        sql_term = self.sqlSanitize(term); // this is still insane, % and _
-                       tx.executeSql("SELECT id, title FROM book WHERE title LIKE '%"+sql_term+"%' ORDER BY title LIMIT 10", [],
-                       //tx.executeSql("SELECT id, title FROM book WHERE title REGEXP '.*"+sql_term+".*' ORDER BY title", [],
+                       tx.executeSql("SELECT * FROM book WHERE title LIKE '%"+sql_term+"%' ORDER BY sort_key LIMIT 10", [],
+                       //tx.executeSql("SELECT * FROM book WHERE title REGEXP '.*"+sql_term+".*' ORDER BY sort_key", [],
                                function(tx, results) {
                                        // save the books
                                        booksFound(tx, results);
                                        // and proceed to tags
                                function(tx, results) {
                                        // save the books
                                        booksFound(tx, results);
                                        // and proceed to tags
-                                       tx.executeSql("SELECT id, name, category FROM tag WHERE name LIKE '%"+sql_term+"%' ORDER BY name LIMIT 10",
+                                       tx.executeSql("SELECT * FROM tag WHERE name LIKE '%"+sql_term+"%' ORDER BY sort_key LIMIT 10",
                                                        [], tagsFound);
                                },
                                function(err) {
                                                        [], tagsFound);
                                },
                                function(err) {
@@ -670,58 +274,110 @@ var Catalogue = new function() {
                });
        };
 
                });
        };
 
-/*
-       // each book and tag in its own transaction
-       // TODO: error handling
-       self.parse = function(json, success, error) {
-               console.log('parsing');
+       self.chainSqls = function(sqls, success, error) {
+               self.db.transaction(function(tx) {
+                       var do_next = function() {
+                               if (sqls.length) {
+                                       var sql = sqls.shift();
+                                       console.log(sql);
+                                       tx.executeSql(sql, [], do_next, error);
+                               }
+                               else {
+                                       success && success();
+                               }
+                       }
+                       do_next();
+               });
+       };
+
+
+       self.update = function(data, success, error) {
+               var addBookSql = new Sql("\
+                       INSERT OR REPLACE INTO book \
+                               (id, title, html_file,  html_file_size, parent, parent_number, sort_key, pretty_size, authors) \
+                       VALUES \
+                               ('{0}', '{1}', '{2}', '{3}', '{4}', '{5}', '{6}', '{7}', '{8}')");
+               var addTagSql = new Sql("INSERT OR REPLACE INTO tag (id, category, name, sort_key, books) VALUES ('{0}', '{1}', '{2}', '{3}', '{4}')");
+
                var sqls = [];
                var sqls = [];
-               var addBookSql = new Sql("INSERT INTO book (id, title, html_file, html_file_size) VALUES ('{0}', '{1}', '{2}', '{3}')");
-               var addBookTagSql = new Sql("INSERT INTO book_tag (book, tag) VALUES ('{0}', '{1}')");
-               var addTagSql = new Sql("INSERT INTO tag (id, category, name) VALUES ('{0}', '{1}', '{2}')");
-
-               var bookValues = [];
-               var books = json.added.books;
-               while (books.length) {
-                       var book = books.shift();
-                       if (!book.html) book.html = '';
-                       if (!book.html_size) book.html_size = '';
-                       sqls.push(addBookSql.prepare(book.id, book.title, book.html, book.html_size));
-                       for (var t in book.tags) {
-                               sqls.push(addBookTagSql.prepare(book.id, book.tags[t]));
+
+               if (data.deleted) {
+                       for (i in data.deleted.books) {
+                               var book_id = data.deleted.books[i];
+                               sqls.push("DELETE FROM book WHERE id=" + book_id);
+                               FileRepo.deleteIfExists(book_id);
+                       }
+
+                       for (i in data.deleted.tags) {
+                               var tag_id = data.deleted.tags[i];
+                               sqls.push("DELETE FROM tag WHERE id=" + tag_id);
                        }
                }
                        }
                }
-               console.log('ASSERT json.added.books.length=0: ' + json.added.books.length);
 
 
-               var categories = {author: 'autor', epoch: 'epoch', genre: 'genre', kind: 'kind', theme: 'motyw'};
-               var tags = json.added.tags;
-               while (tags.length) {
-                       var tag = tags.shift();
-                       sqls.push(addTagSql.prepare(tag.id, categories[tag.category], tag.name));
+               if (data.updated) {
+                       for (i in data.updated.books) {
+                               var book = data.updated.books[i];
+                               if (!book.html) book.html = {};
+                               if (!book.html.url) book.html.url = '';
+                               if (!book.html.size) book.html.size = '';
+                               if (!book.parent) book.parent = '';
+                               if (!book.parent_number) book.parent_number = '';
+                               var pretty_size = prettySize(book.html.size);
+                               sqls.push(addBookSql.prepare(
+                                       book.id, book.title, book.html.url, book.html.size,
+                                       book.parent, book.parent_number, book.sort_key, pretty_size, book.author
+                               ));
+                               FileRepo.deleteIfExists(book.id);
+                       }
+
+                       for (i in data.updated.tags) {
+                               var tag = data.updated.tags[i];
+                               var category = categories[tag.category];
+                               var books = tag.books.join(',');
+                               sqls.push(addTagSql.prepare(tag.id, category, tag.name, tag.sort_key, books));
+                       }
                }
 
                }
 
-               self.chainSql(sqls, success, error);
-       }; */
-}
+               sqls.push("UPDATE state SET last_checked=" + data.time_checked);
 
 
+               self.chainSqls(sqls, success, error);
+       };
 
 
 
 
-function onLoad() {
-       console.log('onLoad');
-       document.addEventListener("deviceready", onDeviceReady, false);
-}
+       this.sync = function(success, error) {
+               self.withState(function(state) {
+                       var url = WL_UPDATE.replace("SINCE", state.last_checked); 
+                       console.log('sync: ' + url);
+                       var xhr = new XMLHttpRequest();
+                       xhr.open("GET", url);
+                       xhr.onload = function() {
+                               console.log('sync: fetched by ajax: ' + url);                   
+                               self.update(JSON.parse(xhr.responseText), success, error);
+                       }
+                       xhr.onerror = function(e) {
+                               error && error("Błąd aktualizacji bazy danych." + e);
+                       }
+                       xhr.send();
+               });
+       };
 
 
-function onDeviceReady() {
-       console.log('onDeviceReady');
-       var error = function(err) { alert(err); };
-       Catalogue.init(
-               function() {
-                       console.log('after catalogue.init');
-                       FileRepo.init(
-                               function() {
-                                       console.log('after FileRepo.init');
-                                       View.init();
-                               },
-                               error);
-               }, error);
+       this.updateLocal = function() {
+               FileRepo.withLocal(function(local) {
+                       self.db.transaction(function(tx) {
+                               tx.executeSql("UPDATE book SET _local=0", [], function(tx, results) {
+                                       ll = local.length;
+                                       var ids = [];
+                                       for (var i = 0; i < ll; i ++) {
+                                               ids.push(local[i].name);
+                                       }
+                                       ids = ids.join(',');
+                                       tx.executeSql("UPDATE book SET _local=1 where id in ("+ids+")"); 
+                               });
+                       });
+               }, function() {
+                       self.db.transaction(function(tx) {
+                               tx.executeSql("UPDATE book SET _local=0");
+                       });
+               });
+       };
 }
 }