new initial db + fix
[wl-mobile.git] / assets / www / js / catalogue.js
1 // FIXME: htmlescape strings!
2
3 var VERSION = '0.1';
4
5
6 var FileRepo = new function() {
7         /* API for files repository */
8         var self = this;
9         const WL_URL = 'http://www.wolnelektury.pl';
10         this.root = null;
11
12         this.init = function(success, error) {
13                 self.initRoot(success);
14         };
15
16         this.initRoot = function(success) {
17                 // fs size is irrelevant, PERSISTENT is futile (on Android, at least)
18                 window.requestFileSystem(LocalFileSystem.TEMPORARY, 0, function(fs) {
19                         console.log('local fs found: ' + fs.root.fullPath);
20                         self.root = fs.root;
21                         success && success();
22                 }, function() {
23                         console.log('local fs not found');
24                         success && success();
25                 });
26         };
27
28
29         this.withLocalHtml = function(book_id, success, error) {
30                 console.log('info:withLocalHtml: id:' + book_id);
31                 View.spinner('Otwieranie treści utworu');
32                 if (!self.root)
33                         error && error('info:withLocalHtml: no local html: no usable filesystem');
34
35                 var url = "file://" + self.root.fullPath + "/html/" + book_id;
36                 console.log('info:withLocalHtml: local ajax: ' + url);
37                 var xhr = new XMLHttpRequest();
38                 xhr.open('GET', url, true);
39                 xhr.onload = function() {
40                         console.log('info:withLocalHtml: fetched by local ajax: ' + url);
41                         success && success(xhr.responseText);
42                 }
43                 xhr.onerror = error;
44                 xhr.send();
45         };
46
47
48         // downloads HTML file from server, saves it in cache and calls success with file contents
49         this.withHtmlFromServer = function(book_id, success, error) {
50                 console.log('info:withHtmlFromServer: id:' + book_id);
51                 // read file from WL
52                 Catalogue.withBook(book_id, function(book) {
53                         var url = WL_URL + book.html_file;
54                         console.log('info:withHtmlFromServer: fetching url: ' + url);
55
56                         View.spinner("Pobieranie treści utworu z sieci");
57
58                         if (self.root) {
59                                 window.plugins.downloader.downloadFile(url, self.root.fullPath + "/html/", ""+book_id, true,
60                                         function(data){
61                                                 console.log('info:withHtmlFromServer: loaded file from WL');
62                                                 self.withLocalHtml(book_id, success, error);
63                                         }, function(data) {
64                                                 console.log('error downloading file!')
65                                                 error && error("error: "+data);
66                                         });
67                         }
68                         else {
69                                 // there's no big fs, so we'll just get the text from AJAX
70                                 console.log('info:withHtmlFromServer: ajax: ' + url);
71                                 var xhr = new XMLHttpRequest();
72                                 xhr.open(url);
73                                 xhr.onload = function() {
74                                         console.log('info:withHtmlFromServer: fetched by ajax: ' + url);
75                                         success && success(xhr.responseText);
76                                 }
77                                 xhr.send();
78                         }
79                 });             
80         };
81         
82         // calls the callback with contents of the HTML file for a given book,
83         // loaded from the server and cached locally
84         this.withHtml = function(id, success, error) {
85                 console.log('info:withHtml: id:' + id);
86                 self.withLocalHtml(id, success, function() {
87                         self.withHtmlFromServer(id, success, error);
88                 });
89         };
90 };
91
92
93 var View = new function() {
94         var self = this;
95         self.minOffset = 1000;
96         //self.element
97         self.categories = {
98                         autor: 'Autorzy', 
99                         rodzaj: 'Rodzaje',
100                         gatunek: 'Gatunki',
101                         epoka: 'Epoki'
102         };
103         
104
105         self.init = function() {
106                 console.log('View.init');
107
108                 self.viewStack = [];
109                 self.current;
110                 navigator.app.overrideBackbutton(); 
111                 document.addEventListener("backbutton", View.goBack, true);
112
113                 self._searchbox = document.getElementById("searchbox");
114                 self._searchinput = document.getElementById("search");
115                 self._content = document.getElementById("content");
116
117                 self.enter(location.href);
118         };
119
120
121         this.sanitize = function(text) {
122                 return text.replace(/&/g, "&amp;").replace(/</g, "&lt;");
123         };
124
125         this.showSearch = function() {
126                 self._searchbox.style.display = "block";
127         };
128
129         this.hideSearch = function() {
130                 self._searchbox.style.display = "none";
131         };
132
133         this.spinner = function(text) {
134                 if (!text)
135                         text = "Ładowanie";
136                 self._content.innerHTML = "<div class='spinner'><img src='img/spinner.gif' /><br/><span id='spinnertext'>" + text +"</span></div>";
137                 scroll(0, 0);
138         };
139
140         this.content = function(text) {
141                 console.log('content');
142
143                 self._content.innerHTML = '';
144                 self._content.innerHTML = text;
145                 scroll(0, 0);
146         }
147         
148         this.enter = function(url) {
149                 console.log('View.enter: ' + url);
150
151                 self.current = url;
152                 var view = 'Index';
153                 var arg = null;
154
155                 var query_start = url.indexOf('?');
156                 if (query_start != -1) {
157                         var slash_index = url.indexOf('/', query_start + 1);
158                         if (slash_index != -1) {
159                                 view = url.substr(query_start + 1, slash_index - query_start - 1);
160                                 arg = url.substr(slash_index + 1);
161                         }
162                         else {
163                                 view = url.substr(query_start + 1);
164                         }
165                 }
166                 console.log('View.enter: ' + view + ' ' + arg);
167                 self['enter' + view](arg);
168         }
169         
170         this.enterIndex = function(arg) {
171                 console.log('enterIndex');
172                 self.showSearch();
173                 var html = "<div class='book-list'>";
174                 for (category in self.categories)
175                         html += self.a('Category', category) + self.categories[category] + "</a>\n"; 
176                 html += "</div>" +
177                                 "<p id='logo'><img src='img/wl-logo.png' alt='Wolne Lektury' /><br/>\n" +
178                                 "szkolna biblioteka internetowa" +
179                                 //"<br/>v. " + VERSION +
180                                 "</p>";
181                 self.content(html);
182         };
183         
184         this.enterBook = function(id) {
185                 id = parseInt(id);
186                 console.log('enterBook: ' + id);
187                 self.showSearch();
188
189                 Catalogue.withBook(id, function(book) {
190                 Catalogue.withChildren(id, function(children) {
191                 Catalogue.withAuthors(id, function(authors) {
192                         var html = "<h1><span class='subheader'>";
193                         var auths = [];
194                         for (a in authors) auths.push(authors[a].name);
195                         html += auths.join(", ");
196                         html += "</span>" + book.title + "</h1>\n";
197                         if (book.html_file) {
198                                 html += "<p class='buttons'>" + self.a('BookText', id) + "Czytaj tekst</a></p>";
199                         }
200                         if (children.length) {
201                                 html += "<div class='book-list'>";
202                                 for (c in children) {
203                                         child = children[c];
204                                         html += self.a('Book', child.id) + child.title + "</a>\n";
205                                 }
206                                 html += "</div>";
207                         }
208                         self.content(html);                             
209                 });
210                 });
211                 });
212         }
213         
214         this.enterBookText = function(id) {
215                 id = parseInt(id);
216                 self.spinner("Otwieranie utworu");
217                 console.log('enterBookText: ' + id);
218                 self.hideSearch();
219                 
220                 FileRepo.withHtml(id, function(data) {
221                         self.content(data);
222                 });
223         }
224
225         this.enterTag = function(id) {
226                 id = parseInt(id);
227                 console.log('enterTag: ' + id);
228                 self.showSearch();
229
230                 self.spinner("Otwieranie listy utworów");
231
232                 Catalogue.withTag(id, function(tag) {
233                         var html = "<h1><span class='subheader upper'>" + tag.category + ': </span>' + tag.name + "</h1>\n";
234                         html += "<div class='book-list'>";
235                         if (tag._books) {
236                                 Catalogue.withBooks(tag._books, function(books) {
237                                         for (var i in books) {
238                                                 var book = books[i];
239                                                 html += self.a('Book', book.id) + book.title + "</a>\n";
240                                         }
241                                         html += "</div>";
242                                         self.content(html);
243                                 });
244                         }
245                 });
246         };
247
248
249         this.enterCategory = function(category) {
250                 console.log('enterCategory: ' + category);
251                 self.spinner("Otwieranie katalogu");
252                 self.showSearch();
253
254                 Catalogue.withCategory(category, function(tags) {
255                         var html = "<h1>" + self.categories[category] + "</h1>\n";
256                         html += "<div class='book-list'>";
257                         for (i in tags) {
258                                 tag = tags[i];
259                                 html += self.a('Tag', tag.id) + tag.name + "</a>\n";
260                         }
261                         html += "</div>";
262                         self.content(html);
263                 });
264         };
265
266
267
268         this.enterSearch = function(query) {
269                 console.log('enterTag: ' + query);
270                 self.showSearch();
271
272                 var html = "<h1><span class='subheader'>Szukana fraza:</span>" + View.sanitize(query) + "</h1>\n";
273
274                 if (query.length < 2) {
275                         html += "<p>Szukana fraza musi mieć co najmniej dwa znaki</p>";
276                         self.content(html);
277                         return;
278                 }
279
280                 Catalogue.withSearch(query, function(results) {
281                         if (results.length == 1) {
282                                 self.enter(self.href(results[0].view, results[0].id));
283                                 return;
284                         }
285                         if (results.length == 0) {
286                                 html += "<p>Brak wyników wyszukiwania</p>";
287                         }
288                         else {
289                                 html += "<div class='book-list'>";
290                                 for (var i in results) {
291                                         var result = results[i];
292                                         html += self.a(result.view, result.id) + result.label + "</a>\n";
293                                 }
294                                 html += "</div>";
295                         }
296                         self.content(html);
297                 });
298         };
299
300
301         /* search form submit callback */
302         this.search = function() {
303                 self.goTo('?Search/' + self._searchinput.value);
304                 return false;
305         }
306         
307         
308         this.href = function(view, par) {
309                 return "?"+view+"/"+par;
310         };
311         
312         this.a = function(view, par) {
313                 return "<a class='"+view+"' onclick='View.goTo(\"" + 
314                                         self.href(view, par).replace(/["']/g, "\\$&") + "\");'>";
315         };
316         
317         this.goTo = function(url) {
318                 self.viewStack.push(self.current);
319                 console.log('goTo: ' + url);
320                 self.enter(url);
321         };
322         
323         this.goBack = function() {
324                 if (self.viewStack.length > 0) {
325                         var url = self.viewStack.pop();
326                         console.log('goBack: ' + url);
327                         self.enter(url);
328                 }
329                 else {
330                         console.log('exiting');
331                         navigator.app.exitApp();
332                 }
333         };
334 }
335
336
337 /*
338 // for preparing sql statements
339 // use like: 
340 //   var s = new Sql("INSERT ... '{0}', '{1}' ...";
341 //   s.prepare("abc", ...)
342 var Sql = function(scheme) {
343         var self = this;
344         self.text = scheme;
345         
346         self.sql_escape = function(term) {
347                 return term.toString().replace("'", "''");
348         };
349         
350         self.prepare = function() {
351                 var args = arguments;
352                 return self.text.replace(/{(\d+)}/g, function(match, number) {
353                         return self.sql_escape(args[number]);
354                 });
355         }
356 };*/
357
358
359 var Catalogue = new function() {
360         /* API for database */
361
362         var self = this;
363         self.db = null;
364
365         this.init = function(success, error) {
366                 console.log('Catalogue.init');
367                 
368                 self.updateDB(function() {
369                         self.db = window.openDatabase("wolnelektury", "1.0", "WL Catalogue", 1);
370                         if (self.db) {
371                                 /*var regexp = {
372                                                 onFunctionCall: function(val) {
373                                                         var re = new RegExp(val.getString(0));
374                                                                 if (val.getString(1).match(re))
375                                                                         return 1;
376                                                                 else
377                                                                         return 0;
378                                                 }
379                                         };
380                                 self.db.createFunction("REGEXP", 2, regexp);*/
381
382                                 success && success();
383                         } else {
384                                 error && error('Nie mogę otworzyć bazy danych: ' + err);
385                         }
386                         
387                 }, function(err) {
388                         error && error('Błąd migracji: ' + err);
389                 });
390         };
391
392         self.sqlSanitize = function(term) {
393                 return term.toString().replace("'", "''");
394         };
395
396
397         /* check if DB needs updating and upload a fresh copy, if so */
398         this.updateDB = function(success, error) {
399                 var db_ver = '0.1.4';
400                 if (window.localStorage.getItem('db_ver') == db_ver) {
401                         console.log('db ok, skipping')
402                         success && success();
403                         return;
404                 }
405
406                 var done = function() {
407                         window.localStorage.setItem('db_ver', db_ver);
408                         console.log('db updated');
409                         success && success();
410                 };
411
412                 // db initialize
413                 // this is Android-specific for now
414                 var version = device.version.split('.')[0];
415                 switch(version) {
416                 case '1':
417                         self.upload_db_android1(done, error);
418                         break;
419                 case '2':
420                         self.upload_db_android2(done, error);
421                         break;
422                 case '3':
423                 default:
424                         error && error('Błąd migracji: ' + err);
425                 };
426         };
427
428
429         this.upload_db_android1 = function(success, error) {
430                 console.log('upload db for Android 1.x');
431                 window.requestFileSystem(LocalFileSystem.APPLICATION, 0, function(fs) {
432                         window.plugins.assetcopy.copy("initial/wolnelektury.db",
433                                         fs.root.fullPath + "/databases/wolnelektury.db", true,
434                                         function(data) {
435                                                 console.log('db upload successful');
436                                                 success && success();
437                                         }, function(data) {
438                                                 error && error("database upload error: " + data);
439                                         });
440                 }, error);
441         };
442
443
444         this.upload_db_android2 = function(success, error) {
445                 console.log('upload db for Android 2.x');
446                 window.requestFileSystem(LocalFileSystem.APPLICATION, 0, function(fs) {
447
448                         // upload databases description file
449                         window.plugins.assetcopy.copy("initial/Databases.db",
450                                 fs.root.fullPath + "/app_database/Databases.db", true,
451                                 function(data) {
452                                         console.log('db descriptior upload successful');
453
454                                         // upload the database file
455                                         window.plugins.assetcopy.copy("initial/0000000000000001.db",
456                                                 fs.root.fullPath + "/app_database/file__0/0000000000000001.db", true,
457                                                 function(data) {
458                                                         console.log('db upload successful');
459                                                         success && success();
460                                                 }, function(data) {
461                                                         error && error("database upload error: " + data);
462                                                 });
463
464                                         
465                                 }, function(data) {
466                                         error && error("database descriptor upload error: " + data);
467                                 });
468
469                 }, error);
470         };
471
472
473         this.withBook = function(id, callback) {
474                 console.log('withBook '+id)
475                 self.db.transaction(function(tx) {
476                         tx.executeSql("SELECT * FROM book WHERE id="+id, [], 
477                                 function(tx, results) {
478                                         callback(results.rows.item(0));
479                                 });
480                 });
481         };
482
483         this.withBooks = function(ids, callback) {
484                 console.log('withBooks ' + ids)
485                 self.db.transaction(function(tx) {
486                         tx.executeSql("SELECT * FROM book WHERE id IN ("+ids+") ORDER BY title", [], 
487                                 function(tx, results) {
488                                         var items = [];
489                                         var count = results.rows.length;
490                                         for (var i=0; i<count; ++i) {
491                                                 items.push(results.rows.item(i));
492                                         }
493                                         callback(items);
494                                 });
495                 });
496         };
497
498         this.withAuthors = function(id, callback) {
499                 console.log('withAuthors ' + id);
500
501                 self.db.transaction(function(tx) {
502                         tx.executeSql("SELECT t.name " +
503                                         "FROM book_tag bt LEFT JOIN tag t ON t.id=bt.tag " +
504                                         "WHERE bt.book="+id+" AND t.category='autor' " +
505                                         "ORDER BY t.sort_key", [], 
506                                 function(tx, results) {
507                                         var tags = [];
508                                         var count = results.rows.length;
509                                         for (var i=0; i<count; ++i) {
510                                                 tags.push(results.rows.item(i));
511                                         }
512                                         callback(tags);
513                                 });
514                 });
515         };
516         
517         this.withChildren = function(id, callback) {
518                 console.log('withChildren ' + id)
519                 self.db.transaction(function(tx) {
520                         tx.executeSql("SELECT * FROM book WHERE parent="+id+" ORDER BY parent_number", [], 
521                                 function(tx, results) {
522                                         var books = [];
523                                         var count = results.rows.length;
524                                         for (var i=0; i<count; ++i) {
525                                                 books.push(results.rows.item(i));
526                                         }
527                                         callback(books);
528                         });
529                 });
530         };
531
532         this.withTag = function(id, callback) {
533                 console.log('withTag '+id)
534                 self.db.transaction(function(tx) {
535                         tx.executeSql("SELECT * FROM tag WHERE id="+id, [], 
536                                 function(tx, results) {
537                                         callback(results.rows.item(0));
538                                 });
539                 });
540         };
541
542         this.withCategory = function(category, callback) {
543                 console.log('withCategory ' + category)
544                 self.db.transaction(function(tx) {
545                         tx.executeSql("SELECT * FROM tag WHERE category='"+category+"'", [], 
546                                 function(tx, results) {
547                                         var items = [];
548                                         var count = results.rows.length;
549                                         for (var i=0; i<count; ++i)
550                                                 items.push(results.rows.item(i));
551                                         callback(items);
552                                 });
553                 });
554         };
555
556
557 /*      this.withUnrelatedBooks = function(books, success) {
558                 if (books.length == 0) return books;
559
560                 var book_ids = {};
561                 for (i in books) {
562                         book_ids[books[i].id] = true;
563                 }
564                 var new_books = [];
565
566                 var addIfUnrelated = function(book) {
567                         var addIfUnrelated_wrapped = function(b) {
568                                 if (b.parent) {
569                                         if (b.parent in book_ids) {
570                                                 // go to next book
571                                                 filterBooks();
572                                         }
573                                         else self.withBook(b.parent, addIfUnrelated_wrapped);
574                                 }
575                                 else {
576                                         new_books.push(book);
577                                         // go to next book
578                                         filterBooks();
579                                 }
580                         };
581                         addIfUnrelated_wrapped(book);
582                 };
583
584
585                 var filterBooks = function() {
586                         console.log('filterBooks: ' + books.length);
587                         if (books.length) {
588                                 addIfUnrelated(books.shift());
589                         }
590                         else {
591                                 success && success(new_books);
592                         }
593                 };
594                 
595                 filterBooks();
596         };
597
598
599         this.withBooksTagged = function(tag, callback, withChildren) {
600                 self.db.transaction(function(tx) {
601                         tx.executeSql("SELECT book.* " +
602                                         "FROM book LEFT JOIN book_tag ON book_tag.book=book.id " +
603                                         "WHERE book_tag.tag="+tag+" ORDER BY book.title", [], 
604                                 function(tx, results) {
605                                         var books = [];
606                                         var count = results.rows.length;
607                                         console.log('withBooksTagged('+tag+'): '+count);
608                                         for (var i=0; i<count; ++i) {
609                                                 books.push(results.rows.item(i));
610                                         }
611                                         if (withChildren)
612                                                 callback(books);
613                                         else self.withUnrelatedBooks(books, callback);
614                                 });
615                 });
616         };
617 */
618
619         /* takes a query, returns a list of {view,id,label} objects to a callback */
620         this.withSearch = function(term, callback) {
621                 console.log('searching...');
622                 var found = [];
623
624                 function booksFound(tx, results) {
625                         var len = results.rows.length;
626                         console.log('found books: ' + len);
627                         for (var i=0; i<len; i++) {
628                                 var item = results.rows.item(i);
629                                 found.push({
630                                         view: "Book",
631                                         id: item.id,
632                                         label: item.title
633                                 });
634                         }
635                 };
636
637                 function tagsFound(tx, results) {
638                         var len = results.rows.length;
639                         console.log('found tags: ' + len);
640                         for (var i=0; i<len; i++) {
641                                 var item = results.rows.item(i);
642                                 found.push({
643                                         view: "Tag",
644                                         id: item.id,
645                                         label: item.name + ' (' + item.category + ')'
646                                 });
647                         }
648                         // TODO error handling
649                         callback(found);
650                 };
651
652
653                 // FIXME escaping
654                 // TODO pliterki, start of the word match
655                 self.db.transaction(function(tx) {
656                         sql_term = self.sqlSanitize(term); // this is still insane, % and _
657                         tx.executeSql("SELECT id, title FROM book WHERE title LIKE '%"+sql_term+"%' ORDER BY title LIMIT 10", [],
658                         //tx.executeSql("SELECT id, title FROM book WHERE title REGEXP '.*"+sql_term+".*' ORDER BY title", [],
659                                 function(tx, results) {
660                                         // save the books
661                                         booksFound(tx, results);
662                                         // and proceed to tags
663                                         tx.executeSql("SELECT id, name, category FROM tag WHERE name LIKE '%"+sql_term+"%' ORDER BY name LIMIT 10",
664                                                         [], tagsFound);
665                                 },
666                                 function(err) {
667                                         console.log('ERROR:search: '+err.code);
668                                         callback([]);
669                                 });
670                 });
671         };
672
673 /*
674         // each book and tag in its own transaction
675         // TODO: error handling
676         self.parse = function(json, success, error) {
677                 console.log('parsing');
678                 var sqls = [];
679                 var addBookSql = new Sql("INSERT INTO book (id, title, html_file, html_file_size) VALUES ('{0}', '{1}', '{2}', '{3}')");
680                 var addBookTagSql = new Sql("INSERT INTO book_tag (book, tag) VALUES ('{0}', '{1}')");
681                 var addTagSql = new Sql("INSERT INTO tag (id, category, name) VALUES ('{0}', '{1}', '{2}')");
682
683                 var bookValues = [];
684                 var books = json.added.books;
685                 while (books.length) {
686                         var book = books.shift();
687                         if (!book.html) book.html = '';
688                         if (!book.html_size) book.html_size = '';
689                         sqls.push(addBookSql.prepare(book.id, book.title, book.html, book.html_size));
690                         for (var t in book.tags) {
691                                 sqls.push(addBookTagSql.prepare(book.id, book.tags[t]));
692                         }
693                 }
694                 console.log('ASSERT json.added.books.length=0: ' + json.added.books.length);
695
696                 var categories = {author: 'autor', epoch: 'epoch', genre: 'genre', kind: 'kind', theme: 'motyw'};
697                 var tags = json.added.tags;
698                 while (tags.length) {
699                         var tag = tags.shift();
700                         sqls.push(addTagSql.prepare(tag.id, categories[tag.category], tag.name));
701                 }
702
703                 self.chainSql(sqls, success, error);
704         }; */
705 }
706
707
708
709 function onLoad() {
710         console.log('onLoad');
711         document.addEventListener("deviceready", onDeviceReady, false);
712 }
713
714 function onDeviceReady() {
715         console.log('onDeviceReady');
716         var error = function(err) { alert(err); };
717         Catalogue.init(
718                 function() {
719                         console.log('after catalogue.init');
720                         FileRepo.init(
721                                 function() {
722                                         console.log('after FileRepo.init');
723                                         View.init();
724                                 },
725                                 error);
726                 }, error);
727 }