2 * This file is part of WolneLektury-Mobile, licensed under GNU Affero GPLv3 or later.
3 * Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
8 var WL_INITIAL = WL + '/media/api/mobile/initial/initial.db';
9 var WL_UPDATE = WL + '/api/changes/SINCE.json?book_fields=author,html,parent,parent_number,sort_key,title' +
10 '&tag_fields=books,category,name,sort_key' +
11 '&tag_categories=author,epoch,genre,kind';
15 var categories = {'author': 'autor',
22 // FIXME: htmlescape strings!
25 // for preparing sql statements
27 // var s = new Sql("INSERT ... '{0}', '{1}' ...";
28 // s.prepare("abc", ...)
29 var Sql = function(scheme) {
33 self.sql_escape = function(term) {
34 return term.toString().replace("'", "''");
37 self.prepare = function() {
39 return self.text.replace(/{(\d+)}/g, function(match, number) {
40 return self.sql_escape(args[parseInt(number)]);
46 var Catalogue = new function() {
47 /* API for database */
52 this.init = function(success, error) {
53 debug('Catalogue.init');
55 self.updateDB(success, function(err) {
56 error && error('Błąd migracji: ' + err);
60 self.sqlSanitize = function(term) {
61 return term.toString().replace("'", "''");
65 /* check if DB needs updating and create it, if so */
66 this.updateDB = function(success, error) {
67 var has_ver = window.localStorage.getItem('db_ver');
68 if (has_ver == DB_VER) {
69 debug('db ok, skipping');
71 self.db = window.openDatabase("wolnelektury", "1.0", "WL Catalogue", 1000000);
75 error && error('Nie mogę otworzyć bazy danych: ' + err);
80 var done = function() {
82 window.localStorage.setItem('db_ver', DB_VER);
88 self.createdb(done, error);
92 this.createdb = function(success, error) {
95 var dbname = "wolnelektury";
96 var db = window.openDatabase(dbname, "1.0", "WL Catalogue", 1000000);
98 debug('db created successfully');
101 sqls.push('CREATE TABLE IF NOT EXISTS book (\
102 id INTEGER PRIMARY KEY,\
105 html_file_size INTEGER,\
107 parent_number INTEGER,\
109 pretty_size VARCHAR,\
113 sqls.push('CREATE INDEX IF NOT EXISTS book_title_index ON book (title);');
114 sqls.push('CREATE INDEX IF NOT EXISTS book_sort_key_index ON book (sort_key);');
115 sqls.push('CREATE INDEX IF NOT EXISTS book_parent_index ON book (parent);');
116 sqls.push('CREATE TABLE IF NOT EXISTS tag (\
117 id INTEGER PRIMARY KEY,\
123 sqls.push('CREATE INDEX IF NOT EXISTS tag_name_index ON tag (name);');
124 sqls.push('CREATE INDEX IF NOT EXISTS tag_category_index ON tag (category);');
125 sqls.push('CREATE INDEX IF NOT EXISTS tag_sort_key_index ON tag (name);');
126 sqls.push('CREATE TABLE IF NOT EXISTS state (last_checked INTEGER);');
127 sqls.push('DELETE FROM state;');
128 sqls.push('INSERT INTO state (last_checked) VALUES(0);');
129 // create database and sync it
130 self.chainSqls(sqls, function() {self.sync(success, error);}, error);
133 error && error('Błąd podczas inicjowania bazy danych: ' + data);
138 this.withState = function(callback) {
139 self.db.transaction(function(tx) {
140 tx.executeSql("SELECT * FROM state", [],
141 function(tx, results) {
142 if (results.rows.length) {
143 callback(results.rows.item(0));
146 callback({last_checked: 0});
153 this.withBook = function(id, callback, error) {
154 debug('withBook '+id)
155 self.db.transaction(function(tx) {
156 tx.executeSql("SELECT * FROM book WHERE id="+id, [],
157 function(tx, results) {
158 if (results.rows.length) {
159 callback(results.rows.item(0));
168 this.withBooks = function(ids, callback) {
169 debug('withBooks ' + ids)
170 self.db.transaction(function(tx) {
171 tx.executeSql("SELECT * FROM book WHERE id IN ("+ids+") ORDER BY sort_key", [],
172 function(tx, results) {
174 var count = results.rows.length;
175 for (var i=0; i<count; ++i) {
176 items.push(results.rows.item(i));
184 this.withChildren = function(id, callback) {
185 debug('withChildren ' + id)
186 self.db.transaction(function(tx) {
187 tx.executeSql("SELECT * FROM book WHERE parent="+id+" ORDER BY parent_number, sort_key", [],
188 function(tx, results) {
190 var count = results.rows.length;
191 for (var i=0; i<count; ++i) {
192 books.push(results.rows.item(i));
199 this.withTag = function(id, callback, error) {
201 self.db.transaction(function(tx) {
202 tx.executeSql("SELECT * FROM tag WHERE id="+id, [],
203 function(tx, results) {
204 if (results.rows.length) {
205 callback(results.rows.item(0));
214 this.withCategory = function(category, callback) {
215 debug('withCategory ' + category)
216 self.db.transaction(function(tx) {
217 tx.executeSql("SELECT * FROM tag WHERE category='"+category+"' ORDER BY sort_key", [],
218 function(tx, results) {
220 var count = results.rows.length;
221 for (var i=0; i<count; ++i)
222 items.push(results.rows.item(i));
229 /* takes a query, returns a list of {view,id,label} objects to a callback */
230 this.withSearch = function(term, callback) {
231 debug('searching...');
232 term = term.replace(/^\s+|\s+$/g, '') ;
235 function booksFound(tx, results) {
236 var len = results.rows.length;
237 debug('found books: ' + len);
238 for (var i=0; i<len; i++) {
239 var item = results.rows.item(i);
247 function tagsFound(tx, results) {
248 var len = results.rows.length;
249 debug('found tags: ' + len);
250 for (var i=0; i<len; i++) {
251 var item = results.rows.item(i);
257 // TODO error handling
263 // TODO pliterki, start of the word match
264 self.db.transaction(function(tx) {
265 sql_term = self.sqlSanitize(term); // this is still insane, % and _
266 tx.executeSql("SELECT * FROM book WHERE title LIKE '%"+sql_term+"%' ORDER BY sort_key LIMIT 10", [],
267 //tx.executeSql("SELECT * FROM book WHERE title REGEXP '.*"+sql_term+".*' ORDER BY sort_key", [],
268 function(tx, results) {
270 booksFound(tx, results);
271 // and proceed to tags
272 tx.executeSql("SELECT * FROM tag WHERE name LIKE '%"+sql_term+"%' ORDER BY sort_key LIMIT 10",
276 debug('ERROR:search: '+err.code);
282 self.chainSqls = function(sqls, success, error) {
283 self.db.transaction(function(tx) {
284 var do_next = function() {
286 var sql = sqls.shift();
288 tx.executeSql(sql, [], do_next, error);
291 success && success();
299 self.update = function(data, success, error) {
300 var addBookSql = new Sql("\
301 INSERT OR REPLACE INTO book \
302 (id, title, html_file, html_file_size, parent, parent_number, sort_key, pretty_size, authors) \
304 ('{0}', '{1}', '{2}', '{3}', '{4}', '{5}', '{6}', '{7}', '{8}')");
305 var addTagSql = new Sql("INSERT OR REPLACE INTO tag (id, category, name, sort_key, books) VALUES ('{0}', '{1}', '{2}', '{3}', '{4}')");
310 for (i in data.deleted.books) {
311 var book_id = data.deleted.books[i];
312 sqls.push("DELETE FROM book WHERE id=" + book_id);
313 FileRepo.deleteIfExists(book_id);
316 for (i in data.deleted.tags) {
317 var tag_id = data.deleted.tags[i];
318 sqls.push("DELETE FROM tag WHERE id=" + tag_id);
323 for (i in data.updated.books) {
324 var book = data.updated.books[i];
325 if (!book.html) book.html = {};
326 if (!book.html.url) book.html.url = '';
327 if (!book.html.size) book.html.size = '';
328 if (!book.parent) book.parent = '';
329 if (!book.parent_number) book.parent_number = '';
330 var pretty_size = prettySize(book.html.size);
331 sqls.push(addBookSql.prepare(
332 book.id, book.title, book.html.url, book.html.size,
333 book.parent, book.parent_number, book.sort_key, pretty_size, book.author
335 FileRepo.deleteIfExists(book.id);
338 for (i in data.updated.tags) {
339 var tag = data.updated.tags[i];
340 var category = categories[tag.category];
341 var books = tag.books.join(',');
342 sqls.push(addTagSql.prepare(tag.id, category, tag.name, tag.sort_key, books));
346 sqls.push("UPDATE state SET last_checked=" + data.time_checked);
348 self.chainSqls(sqls, function() {self.updateLocal(); success && success();}, error);
352 this.sync = function(success, error) {
353 self.withState(function(state) {
354 var url = WL_UPDATE.replace("SINCE", state.last_checked);
355 debug('sync: ' + url);
356 var xhr = new XMLHttpRequest();
357 xhr.open("GET", url);
358 xhr.onload = function() {
359 debug('sync: fetched by ajax: ' + url);
360 self.update(JSON.parse(xhr.responseText), success, error);
362 xhr.onerror = function(e) {
363 error && error("Błąd aktualizacji bazy danych." + e);
369 this.updateLocal = function() {
370 FileRepo.withLocal(function(local) {
371 self.db.transaction(function(tx) {
372 tx.executeSql("UPDATE book SET _local=0", [], function(tx, results) {
375 for (var i = 0; i < ll; i ++) {
376 ids.push(local[i].name);
379 tx.executeSql("UPDATE book SET _local=1 where id in ("+ids+")");
383 self.db.transaction(function(tx) {
384 tx.executeSql("UPDATE book SET _local=0");