Tweak html layout to improve formatting.
authorChris Jaekl <cejaekl@yahoo.com>
Tue, 7 Nov 2017 12:55:20 +0000 (21:55 +0900)
committerChris Jaekl <cejaekl@yahoo.com>
Tue, 7 Nov 2017 12:55:20 +0000 (21:55 +0900)
Also implement Previous/Next buttons.

app/index.html
app/lib.js
main/db.go
main/handler.go

index 322ac7e..4b246d4 100644 (file)
       Series: <input id="ser" type="text"/>
     </form>
 
-    <h1>Books</h1>
-
-    <div id="books">(No books found)</div>
-
     <div class="footer">
-        <input id="back" value="Back" type="button"/>
-        <input id="forward" value="Forward" type="button"/>
+        <input id="back" onclick="onPrev();" value="Back" type="button"/>
+        <input id="forward" onclick="onNext();" value="Forward" type="button"/>
       Showing <span id="first">0</span> through <span id="last">0</span> out of <span id="count">0</span> matching books.
     </div>
 
+    <div id="books">(No books found)</div>
+
     <script src="lib.js"></script>
   </body>
 </html>
index 7175471..5e2ecf8 100644 (file)
@@ -6,7 +6,8 @@ g_state = {
   count: 0,
   first: 0,
   ids: [],
-  last: 0,
+  last: (-1),
+  pageSize: 100
 }
 
 function constructSearchUrl() {
@@ -40,9 +41,27 @@ function constructSearchUrl() {
 }
 
 function onNext() {
+  if (g_state.last < (g_state.count - 1)) {
+    g_state.first += g_state.pageSize;
+    g_state.last += g_state.pageSize;
+    if (g_state.last >= g_state.count) {
+      g_state.last = g_state.count - 1;
+    }
+
+    refreshData();
+  }
 }
 
 function onPrev() {
+  if (g_state.first > 0) {
+    g_state.first -= g_state.pageSize;
+    if (g_state.first < 0) {
+      g_state.first = 0;
+    }
+    g_state.last = g_state.first + g_state.pageSize;
+    
+    refreshData();
+  }
 }
 
 function onSearch() {
@@ -58,10 +77,11 @@ function onSearch() {
   .then((jsonValue) => {
     console.log('JSON response:  ', jsonValue);
     g_state.ids = jsonValue
+    g_state.count = g_state.ids.length;
     g_state.first = 0
     g_state.last = (g_state.ids.length) - 1;
-    if (g_state.last > 100) {
-      g_state.last = 100;
+    if (g_state.last > g_state.pageSize) {
+      g_state.last = g_state.pageSize;
     }
     refreshData()
   })
@@ -78,7 +98,7 @@ function refreshData() {
   var i;
   var url = '/info/?ids=';
   for (i = g_state.first; i <= g_state.last; ++i) {
-    if (i > 0) {
+    if (i > g_state.first) {
       url += ',';
     }
     url += g_state.ids[i];
@@ -102,12 +122,16 @@ function refreshData() {
 function refreshLayout() {
   var i;
   var html = '';
-  for (i = g_state.first; i <= g_state.last; ++i) {
+  var limit = g_state.last - g_state.first;
+  for (i = 0; i <= limit; ++i) {
     var book = g_state.cache[i];
     html += bookHtml(book);
   }
 
   document.getElementById('books').innerHTML = html;
+  document.getElementById('first').innerHTML = (g_state.first + 1);
+  document.getElementById('last').innerHTML = (g_state.last + 1);
+  document.getElementById('count').innerHTML = g_state.count;
 }
 
 function report(message) {
@@ -116,10 +140,28 @@ function report(message) {
 
 function bookHtml(book) {
   console.log('bookHtml(): ', book);
-  var result = '<div class="book"><table><tr><td><a href="/book/' + book.Id + '">'
-             + '<img class="cover-thumb" src="/download/' + book.CoverId + '"/></a></td>'
-             + '<td><span class="popup">' + book.Description + '</span></td>'
-             + '</tr></table></div>';
+  var result = '<div class="book">'
+             +   '<table>'
+             +     '<tr>'
+             +       '<td><a href="/book/' + book.Id + '">';
+  if (0 == book.CoverId) {
+    result  +=          '(No cover available)'
+  } else {
+    result  +=          '<img class="cover-thumb" src="/download/' + book.CoverId + '"/>'
+  }
+  result    +=       '</a></td>'
+             +       '<td>'
+             +         '<p><b>' + book.Title + '</b></p>'
+             +         '<p>'
+             +           '<i>' + book.AuthorReading + '</i>';
+  if (typeof(book.SeriesName) !== 'undefined' && book.SeriesName.length > 0) {
+    result  +=           '<br/><i>' + book.SeriesName + ' ' + book.Volume + '</i>';
+  }
+  result    +=         '</p>'
+             +       '</td>'
+             +     '</tr>'
+             +   '</table>'
+             + '</div>';
   return result;
 }
 
index fa34828..21c6818 100644 (file)
@@ -161,12 +161,33 @@ func queryBooksByIds(ids []int) []Book {
   return res
 }
 
+func queryBookPathById(id int) (string) {
+  query := "SELECT b.path FROM Books b WHERE b.id=$1"
+
+  ps, err := getDb().Prepare(query)
+  if nil != err {
+    report("Failed to Prepare query:  " + query, err)
+    return ""
+  }
+  defer ps.Close()
+
+  row := ps.QueryRow(id)
+  var path sql.NullString
+  err = row.Scan(&path)
+  if nil != err {
+    report(fmt.Sprintf("Failed to retrieve path for book id %v: ", id), err)
+    return ""
+  }
+
+  return nsVal(path)
+}
+
 func queryIds(criteria []SearchTerm) []int {
   fmt.Println("queryIds():", criteria)
 
   query := "SELECT b.id FROM Books b" +
            " INNER JOIN Authors a ON a.id=b.author" +
-           " LEFT OUTER JOIN Series s ON s.id=b.series"
+           " LEFT OUTER JOIN Series s ON s.id=b.series" 
 
   args := make([]interface{}, len(criteria))
 
@@ -190,6 +211,8 @@ func queryIds(criteria []SearchTerm) []int {
     args[i] = criterion.Text
   }
 
+  query += " ORDER BY b.path"
+
   res := []int{}
 
   ps, err := getDb().Prepare(query)
@@ -207,7 +230,7 @@ func queryIds(criteria []SearchTerm) []int {
   }
   defer rows.Close()
 
-  for rows.Next(); rows.Next(); {
+  for rows.Next() {
     var id int
     rows.Scan(&id)
     res = append(res, id)
index 8686f2b..8a74c62 100644 (file)
@@ -6,6 +6,7 @@ import (
   "io"
   "net/http"
   "os"
+  "path/filepath"
   "strconv"
   "strings"
 )
@@ -14,6 +15,19 @@ const PARAM_IDS = "ids"
 const MAX_TERMS = 10
 
 // ============================================================================
+func bookMimeType(bookPath string) string {
+  upper := strings.ToUpper(bookPath)
+  
+  if strings.HasSuffix(upper, ".EPUB") {
+    return "application/epub+zip"
+  } else if strings.HasSuffix(upper, ".PDF") {
+    return "application/pdf"
+  } else {
+    fmt.Println("Warning:  Cannot determine MIME type, will use application/octet-stream:", bookPath)
+    return "application/octet-stream"
+  }
+}
+
 func efsPathForId(efsId int) string {
   config := getConfig()
 
@@ -33,6 +47,8 @@ func handler(w http.ResponseWriter, r *http.Request) {
   action := strings.Split(r.URL.Path[1:], "/")[0]
 
   switch(action) {
+  case "book":
+    handleBook(w, r)
   case "download":
     handleDownload(w, r)
   case "info":
@@ -48,27 +64,56 @@ func handler(w http.ResponseWriter, r *http.Request) {
   }
 }
 
-/*
-func handleApp(w http.ResponseWriter, r *http.Request) {
-  fmt.Println("handleApp():", r.URL.Path)
+// Download a book, based on the path stored in the DB
+func handleBook(w http.ResponseWriter, r *http.Request) {
+  path := r.URL.Path[1:]
+  tok := strings.Split(path, "/")
+  if 2 != len(tok) {
+    fmt.Fprintln(w, "Unexpected path for book download:", path)
+    return
+  }
+
+  bookId, err := strconv.Atoi(tok[1])
+  if (nil != err) || (0 == bookId) {
+    fmt.Fprintln(w, "Invalid id for book download:", path, err)
+    return
+  }
+
+  bookPath := queryBookPathById(bookId)
+  if 0 == len(bookPath) {
+    fmt.Fprintln(w, "No book for ID:", bookId)
+    return
+  }
+
+  bookFileName := filepath.Base(bookPath)
 
-  // Security check:  prevent walking up the directory
-  pos := strings.Index(r.Url.Path, "../")
-  if (-1) == pos {
-    fmt.Fprintln(w, "Paths containing \"../\" are not permitted:", r.URL.Path)
+  mimeType := bookMimeType(bookPath)
+  if 0 == len(mimeType) {
+    fmt.Fprintln(w, "Failed to determine MIME type for book:", bookPath)
     return
   }
 
-  fileName := "../app" + r.URL.Path
-  _, err := os.Stat(fileName)
-  if nil != err { 
-    fmt.Fprintln(w, "Failed to find file:", fileName, err)
+  var info os.FileInfo
+  info, err = os.Stat(bookPath)
+  if nil != err {
+    fmt.Fprintln(w, "Failed to read file metadata:", bookPath, err)
     return
   }
+  modTime := info.ModTime()
+  
+  var fd *os.File
+  fd, err = os.Open(bookPath)
+  if nil != err {
+    fmt.Fprintln(w, "Failed to open file:", bookPath, err)
+    return
+  }
+  defer fd.Close()
 
-  http.ServeFile(w, r, "../app/" + r.URL.Path[1:])
+  // TODO:  handle non-ASCII file names.  Need to look up the permutations on how to encode that.
+  w.Header().Set("Content-Disposition", "attachment; filename=" + bookFileName)
+  w.Header().Set("Content-Type", mimeType)
+  http.ServeContent(w, r, bookFileName, modTime, fd)
 }
-*/
 
 func handleDownload(w http.ResponseWriter, r *http.Request) {
   path := r.URL.Path[1:]
@@ -99,6 +144,7 @@ func handleDownload(w http.ResponseWriter, r *http.Request) {
   }
   defer fd.Close()
 
+  w.Header().Set("Content-Type", mimeType)
   io.Copy(w, fd)
 }