615f30f9c5e4bdf209e3a8efb92168c0178e6f6a
[wl-mobile.git] / assets / www / js / catalogue.js
1 /*
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.
4  */
5
6 var DB_VER = '0.9.15';
7
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';
12
13
14
15 var categories = {'author': 'autor',
16               'epoch': 'epoka', 
17               'genre': 'gatunek', 
18               'kind': 'rodzaj', 
19               'theme': 'motyw'
20               }
21
22 // FIXME: htmlescape strings!
23
24
25 // for preparing sql statements
26 // use like: 
27 //   var s = new Sql("INSERT ... '{0}', '{1}' ...";
28 //   s.prepare("abc", ...)
29 var Sql = function(scheme) {
30         var self = this;
31         self.text = scheme;
32         
33         self.sql_escape = function(term) {
34                 return term.toString().replace("'", "''");
35         };
36         
37         self.prepare = function() {
38                 var args = arguments;
39                 return self.text.replace(/{(\d+)}/g, function(match, number) {
40                         return self.sql_escape(args[parseInt(number)]);
41                 });
42         }
43 };
44
45
46 var Catalogue = new function() {
47         /* API for database */
48
49         var self = this;
50         self.db = null;
51
52         this.init = function(success, error) {
53                 console.log('Catalogue.init');
54                 
55                 self.updateDB(function() {
56                         self.db = window.openDatabase("wolnelektury", "1.0", "WL Catalogue", 1);
57                         if (self.db) {
58                                 /*var regexp = {
59                                                 onFunctionCall: function(val) {
60                                                         var re = new RegExp(val.getString(0));
61                                                                 if (val.getString(1).match(re))
62                                                                         return 1;
63                                                                 else
64                                                                         return 0;
65                                                 }
66                                         };
67                                 self.db.createFunction("REGEXP", 2, regexp);*/
68
69                                 success && success();
70                         } else {
71                                 error && error('Nie mogę otworzyć bazy danych: ' + err);
72                         }
73                         
74                 }, function(err) {
75                         error && error('Błąd migracji: ' + err);
76                 });
77         };
78
79         self.sqlSanitize = function(term) {
80                 return term.toString().replace("'", "''");
81         };
82
83
84         /* check if DB needs updating and upload a fresh copy, if so */
85         this.updateDB = function(success, error) {
86                 var has_ver = window.localStorage.getItem('db_ver');
87                 if (has_ver == DB_VER) {
88                         console.log('db ok, skipping')
89                         success && success();
90                         return;
91                 }
92
93                 var done = function() {
94                         FileRepo.clear();
95                         window.localStorage.setItem('db_ver', DB_VER);
96                         console.log('db updated');
97                         success && success();
98                 };
99
100                 // db initialize
101                 // this is Android-specific for now
102                 var android = device.version.split('.')[0];
103                 if (android > 1) {
104                         self.upload_db_android(done, error);
105                 } else {
106                         error && error("Nieobsługiwana wersja systemu. Wymagany Android>=2.0.");
107                 };
108         };
109
110
111         this.upload_db_android = function(success, error) {
112                 // TODO: this should be downloaded from teh net, not stored in res
113
114                 console.log('upload db for Android 2.x+');
115                 // upload databases description file
116
117                 var dbname = "wolnelektury";
118                 var db = window.openDatabase(dbname, "1.0", "WL Catalogue", 500000);
119                 if (db) {
120                         console.log('db created successfully');
121                         DBPut.fetch(WL_INITIAL, function(data) {
122                                 console.log('db fetch successful');
123                                 success && success();
124                         }, function(data) {
125                                 error && error('Błąd podczas pobierania bazy danych: ' + data);
126                         });
127                 } else {
128                         error && error('Błąd podczas inicjowania bazy danych: ' + data);
129                 }
130         };
131
132
133         this.withState = function(callback) {
134                 self.db.transaction(function(tx) {
135                         tx.executeSql("SELECT * FROM state", [], 
136                                 function(tx, results) {
137                                         if (results.rows.length) {
138                                                 callback(results.rows.item(0));
139                                         }
140                                         else {
141                                                 callback({last_checked: 0});
142                                         }
143                                 });
144                 });
145         };
146
147
148         this.withBook = function(id, callback, error) {
149                 console.log('withBook '+id)
150                 self.db.transaction(function(tx) {
151                         tx.executeSql("SELECT * FROM book WHERE id="+id, [], 
152                                 function(tx, results) {
153                                         if (results.rows.length) {
154                                                 callback(results.rows.item(0));
155                                         }
156                                         else {
157                                                 error && error();
158                                         }
159                                 });
160                 });
161         };
162
163         this.withBooks = function(ids, callback) {
164                 console.log('withBooks ' + ids)
165                 self.db.transaction(function(tx) {
166                         tx.executeSql("SELECT * FROM book WHERE id IN ("+ids+") ORDER BY sort_key", [], 
167                                 function(tx, results) {
168                                         var items = [];
169                                         var count = results.rows.length;
170                                         for (var i=0; i<count; ++i) {
171                                                 items.push(results.rows.item(i));
172                                         }
173                                         callback(items);
174                                 });
175                 });
176         };
177
178
179         this.withChildren = function(id, callback) {
180                 console.log('withChildren ' + id)
181                 self.db.transaction(function(tx) {
182                         tx.executeSql("SELECT * FROM book WHERE parent="+id+" ORDER BY parent_number, sort_key", [], 
183                                 function(tx, results) {
184                                         var books = [];
185                                         var count = results.rows.length;
186                                         for (var i=0; i<count; ++i) {
187                                                 books.push(results.rows.item(i));
188                                         }
189                                         callback(books);
190                         });
191                 });
192         };
193
194         this.withTag = function(id, callback, error) {
195                 console.log('withTag '+id)
196                 self.db.transaction(function(tx) {
197                         tx.executeSql("SELECT * FROM tag WHERE id="+id, [], 
198                                 function(tx, results) {
199                                         if (results.rows.length) {
200                                                 callback(results.rows.item(0));
201                                         }
202                                         else {
203                                                 error && error();
204                                         }
205                                 });
206                 });
207         };
208
209         this.withCategory = function(category, callback) {
210                 console.log('withCategory ' + category)
211                 self.db.transaction(function(tx) {
212                         tx.executeSql("SELECT * FROM tag WHERE category='"+category+"' ORDER BY sort_key", [], 
213                                 function(tx, results) {
214                                         var items = [];
215                                         var count = results.rows.length;
216                                         for (var i=0; i<count; ++i)
217                                                 items.push(results.rows.item(i));
218                                         callback(items);
219                                 });
220                 });
221         };
222
223
224         /* takes a query, returns a list of {view,id,label} objects to a callback */
225         this.withSearch = function(term, callback) {
226                 console.log('searching...');
227                 term =  term.replace(/^\s+|\s+$/g, '') ;
228                 var found = [];
229
230                 function booksFound(tx, results) {
231                         var len = results.rows.length;
232                         console.log('found books: ' + len);
233                         for (var i=0; i<len; i++) {
234                                 var item = results.rows.item(i);
235                                 found.push({
236                                         view: "Book",
237                                         item: item
238                                 });
239                         }
240                 };
241
242                 function tagsFound(tx, results) {
243                         var len = results.rows.length;
244                         console.log('found tags: ' + len);
245                         for (var i=0; i<len; i++) {
246                                 var item = results.rows.item(i);
247                                 found.push({
248                                         view: "Tag",
249                                         item: item
250                                 });
251                         }
252                         // TODO error handling
253                         callback(found);
254                 };
255
256
257                 // FIXME escaping
258                 // TODO pliterki, start of the word match
259                 self.db.transaction(function(tx) {
260                         sql_term = self.sqlSanitize(term); // this is still insane, % and _
261                         tx.executeSql("SELECT * FROM book WHERE title LIKE '%"+sql_term+"%' ORDER BY sort_key LIMIT 10", [],
262                         //tx.executeSql("SELECT * FROM book WHERE title REGEXP '.*"+sql_term+".*' ORDER BY sort_key", [],
263                                 function(tx, results) {
264                                         // save the books
265                                         booksFound(tx, results);
266                                         // and proceed to tags
267                                         tx.executeSql("SELECT * FROM tag WHERE name LIKE '%"+sql_term+"%' ORDER BY sort_key LIMIT 10",
268                                                         [], tagsFound);
269                                 },
270                                 function(err) {
271                                         console.log('ERROR:search: '+err.code);
272                                         callback([]);
273                                 });
274                 });
275         };
276
277         self.chainSqls = function(sqls, success, error) {
278                 self.db.transaction(function(tx) {
279                         var do_next = function() {
280                                 if (sqls.length) {
281                                         var sql = sqls.shift();
282                                         console.log(sql);
283                                         tx.executeSql(sql, [], do_next, error);
284                                 }
285                                 else {
286                                         success && success();
287                                 }
288                         }
289                         do_next();
290                 });
291         };
292
293
294         self.update = function(data, success, error) {
295                 var addBookSql = new Sql("\
296                         INSERT OR REPLACE INTO book \
297                                 (id, title, html_file,  html_file_size, parent, parent_number, sort_key, pretty_size, authors) \
298                         VALUES \
299                                 ('{0}', '{1}', '{2}', '{3}', '{4}', '{5}', '{6}', '{7}', '{8}')");
300                 var addTagSql = new Sql("INSERT OR REPLACE INTO tag (id, category, name, sort_key, books) VALUES ('{0}', '{1}', '{2}', '{3}', '{4}')");
301
302                 var sqls = [];
303
304                 if (data.deleted) {
305                         for (i in data.deleted.books) {
306                                 var book_id = data.deleted.books[i];
307                                 sqls.push("DELETE FROM book WHERE id=" + book_id);
308                                 FileRepo.deleteIfExists(book_id);
309                         }
310
311                         for (i in data.deleted.tags) {
312                                 var tag_id = data.deleted.tags[i];
313                                 sqls.push("DELETE FROM tag WHERE id=" + tag_id);
314                         }
315                 }
316
317                 if (data.updated) {
318                         for (i in data.updated.books) {
319                                 var book = data.updated.books[i];
320                                 if (!book.html) book.html = {};
321                                 if (!book.html.url) book.html.url = '';
322                                 if (!book.html.size) book.html.size = '';
323                                 if (!book.parent) book.parent = '';
324                                 if (!book.parent_number) book.parent_number = '';
325                                 var pretty_size = prettySize(book.html.size);
326                                 sqls.push(addBookSql.prepare(
327                                         book.id, book.title, book.html.url, book.html.size,
328                                         book.parent, book.parent_number, book.sort_key, pretty_size, book.author
329                                 ));
330                                 FileRepo.deleteIfExists(book.id);
331                         }
332
333                         for (i in data.updated.tags) {
334                                 var tag = data.updated.tags[i];
335                                 var category = categories[tag.category];
336                                 var books = tag.books.join(',');
337                                 sqls.push(addTagSql.prepare(tag.id, category, tag.name, tag.sort_key, books));
338                         }
339                 }
340
341                 sqls.push("UPDATE state SET last_checked=" + data.time_checked);
342
343                 self.chainSqls(sqls, success, error);
344         };
345
346
347         this.sync = function(success, error) {
348                 self.withState(function(state) {
349                         var url = WL_UPDATE.replace("SINCE", state.last_checked); 
350                         console.log('sync: ' + url);
351                         var xhr = new XMLHttpRequest();
352                         xhr.open("GET", url);
353                         xhr.onload = function() {
354                                 console.log('sync: fetched by ajax: ' + url);                   
355                                 self.update(JSON.parse(xhr.responseText), success, error);
356                         }
357                         xhr.onerror = function(e) {
358                                 error && error("Błąd aktualizacji bazy danych." + e);
359                         }
360                         xhr.send();
361                 });
362         };
363
364         this.updateLocal = function() {
365                 FileRepo.withLocal(function(local) {
366                         self.db.transaction(function(tx) {
367                                 tx.executeSql("UPDATE book SET _local=0", [], function(tx, results) {
368                                         ll = local.length;
369                                         var ids = [];
370                                         for (var i = 0; i < ll; i ++) {
371                                                 ids.push(local[i].name);
372                                         }
373                                         ids = ids.join(',');
374                                         tx.executeSql("UPDATE book SET _local=1 where id in ("+ids+")"); 
375                                 });
376                         });
377                 }, function() {
378                         self.db.transaction(function(tx) {
379                                 tx.executeSql("UPDATE book SET _local=0");
380                         });
381                 });
382         };
383 }