Skip to content

Commit

Permalink
Loading book list from local storage
Browse files Browse the repository at this point in the history
  • Loading branch information
rimutaka committed Jul 16, 2024
1 parent bcafb03 commit f134ea4
Show file tree
Hide file tree
Showing 15 changed files with 226 additions and 41 deletions.
8 changes: 4 additions & 4 deletions build/asset-manifest.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
{
"files": {
"main.css": "./static/css/main.02ddbbc4.css",
"main.js": "./static/js/main.71780584.js",
"static/media/isbn_mod_bg.wasm": "./static/media/isbn_mod_bg.433c5e2bd892af2c634b.wasm",
"main.js": "./static/js/main.646e4c21.js",
"static/media/isbn_mod_bg.wasm": "./static/media/isbn_mod_bg.6e6be425ee5d2d381bec.wasm",
"index.html": "./index.html",
"main.02ddbbc4.css.map": "./static/css/main.02ddbbc4.css.map",
"main.71780584.js.map": "./static/js/main.71780584.js.map"
"main.646e4c21.js.map": "./static/js/main.646e4c21.js.map"
},
"entrypoints": [
"static/css/main.02ddbbc4.css",
"static/js/main.71780584.js"
"static/js/main.646e4c21.js"
]
}
2 changes: 1 addition & 1 deletion build/index.html
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<!doctype html><html lang="en"><head prefix="og: http://ogp.me/ns#"><meta charset="utf-8"/><meta http-equiv="x-ua-compatible" content="ie=edge"><meta name="referrer" content="no-referrer"/><meta name="theme-color" content="#ffffff"/><meta name="viewport" content="width=device-width,initial-scale=1"/><title>📖📚📚</title><meta name="description" content="A pocket assistant for keen readers: find more information about the book or the author online, borrow it from your local library, buy, sell or share."><meta property="og:title" content="Scan ISBN to record or share a book"/><meta property="og:description" content="A pocket assistant for keen readers: find more information about the book or the author online, borrow it from your local library, buy, sell or share."><meta property="og:site_name" content="Bookworm Food"><meta property="og:type" content="website"><meta property="og:url" content="https://bookwormfood.com"><meta property="og:image" itemprop="image" content="http://bookwormfood.com/img/og-image-400.png"/><meta property="og:image:secure_url" itemprop="image" content="https://bookwormfood.com/img/og-image-400.png"><meta property="og:image:width" content="400"><meta property="og:image:height" content="400"><meta name="twitter:card" content="summary"/><meta name="twitter:title" content="Scan ISBN to record or share a book"/><meta name="twitter:description" content="A pocket assistant for keen readers: find more information about the book or the author online, borrow it from your local library, buy, sell or share."/><meta name="twitter:image" content="https://bookwormfood.com/img/og-image-400.png"/><link rel="apple-touch-icon" sizes="180x180" href="/img/apple-touch-icon.png"/><link rel="icon" type="image/png" sizes="32x32" href="/img/favicon-32x32.png"><link rel="icon" type="image/png" sizes="16x16" href="/img/favicon-16x16.png"><link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin><link href="https://fonts.googleapis.com/css2?family=Amatic+SC:wght@400;700&display=swap" rel="stylesheet"><link href="https://fonts.googleapis.com/css2?family=News+Cycle:wght@400;700&display=swap" rel="stylesheet"><script defer="defer" src="/static/js/main.71780584.js"></script><link href="/static/css/main.02ddbbc4.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="app"></div></body></html>
<!doctype html><html lang="en"><head prefix="og: http://ogp.me/ns#"><meta charset="utf-8"/><meta http-equiv="x-ua-compatible" content="ie=edge"><meta name="referrer" content="no-referrer"/><meta name="theme-color" content="#ffffff"/><meta name="viewport" content="width=device-width,initial-scale=1"/><title>📖📚📚</title><meta name="description" content="A pocket assistant for keen readers: find more information about the book or the author online, borrow it from your local library, buy, sell or share."><meta property="og:title" content="Scan ISBN to record or share a book"/><meta property="og:description" content="A pocket assistant for keen readers: find more information about the book or the author online, borrow it from your local library, buy, sell or share."><meta property="og:site_name" content="Bookworm Food"><meta property="og:type" content="website"><meta property="og:url" content="https://bookwormfood.com"><meta property="og:image" itemprop="image" content="http://bookwormfood.com/img/og-image-400.png"/><meta property="og:image:secure_url" itemprop="image" content="https://bookwormfood.com/img/og-image-400.png"><meta property="og:image:width" content="400"><meta property="og:image:height" content="400"><meta name="twitter:card" content="summary"/><meta name="twitter:title" content="Scan ISBN to record or share a book"/><meta name="twitter:description" content="A pocket assistant for keen readers: find more information about the book or the author online, borrow it from your local library, buy, sell or share."/><meta name="twitter:image" content="https://bookwormfood.com/img/og-image-400.png"/><link rel="apple-touch-icon" sizes="180x180" href="/img/apple-touch-icon.png"/><link rel="icon" type="image/png" sizes="32x32" href="/img/favicon-32x32.png"><link rel="icon" type="image/png" sizes="16x16" href="/img/favicon-16x16.png"><link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin><link href="https://fonts.googleapis.com/css2?family=Amatic+SC:wght@400;700&display=swap" rel="stylesheet"><link href="https://fonts.googleapis.com/css2?family=News+Cycle:wght@400;700&display=swap" rel="stylesheet"><script defer="defer" src="/static/js/main.646e4c21.js"></script><link href="/static/css/main.02ddbbc4.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="app"></div></body></html>

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions build/static/js/main.646e4c21.js.map

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion build/static/js/main.71780584.js.map

This file was deleted.

Binary file not shown.
2 changes: 1 addition & 1 deletion build/sw.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ if ("function" === typeof importScripts) {
self.addEventListener("install", event => {
self.skipWaiting();
});
workbox.precaching.precacheAndRoute([{"revision":"cbc1c827d728b9d78aef1486cfa168e4","url":"asset-manifest.json"},{"revision":"4020571efe44dc33d271798e6a18e0c1","url":"favicon.ico"},{"revision":"f98923403f5c78d689d56b18f947c520","url":"img/apple-touch-icon.png"},{"revision":"a62aa63bb4d0a3dd08820787bd7e118f","url":"img/favicon-16x16.png"},{"revision":"a5229a03fcfe584a3031846fe3c19ccf","url":"img/favicon-32x32.png"},{"revision":"37ff8dc0d50cd7705fc65fc84837c2aa","url":"img/og-image-400.png"},{"revision":"085b7864d9041485e8b3f4444fbdd09a","url":"index-pocket.html"},{"revision":"f76054aa87dbc2cb0a5a3a3202be1934","url":"index.html"},{"revision":"0d1e6da8a5d82671c489f9b0e264ec5f","url":"static/css/main.02ddbbc4.css"},{"revision":"baa05839f3898527798a8f6a0caad6c2","url":"static/js/main.71780584.js"},{"revision":"f29adfea861c856e2fb5c1a83d2b8a44","url":"static/media/isbn_mod_bg.433c5e2bd892af2c634b.wasm"},{"revision":"24f2b115d3964c9f977462cdd38b066a","url":"wasm/koder.js"},{"revision":"6f11e7db4fe9aca82cac7150bfc33769","url":"wasm/zbar.js"},{"revision":"e8789bf03df9c2c85e9c59ab0a0cd0c6","url":"wasm/zbar.wasm"},{"revision":"bb1c649a95ffa80369254cc3e51b9a41","url":"wasmWorker.js"}]);
workbox.precaching.precacheAndRoute([{"revision":"90329cfc4cd4d0cd45486f4fdfe4fa32","url":"asset-manifest.json"},{"revision":"4020571efe44dc33d271798e6a18e0c1","url":"favicon.ico"},{"revision":"f98923403f5c78d689d56b18f947c520","url":"img/apple-touch-icon.png"},{"revision":"a62aa63bb4d0a3dd08820787bd7e118f","url":"img/favicon-16x16.png"},{"revision":"a5229a03fcfe584a3031846fe3c19ccf","url":"img/favicon-32x32.png"},{"revision":"37ff8dc0d50cd7705fc65fc84837c2aa","url":"img/og-image-400.png"},{"revision":"085b7864d9041485e8b3f4444fbdd09a","url":"index-pocket.html"},{"revision":"1b06395634c7e2b9fad4a110239d2972","url":"index.html"},{"revision":"0d1e6da8a5d82671c489f9b0e264ec5f","url":"static/css/main.02ddbbc4.css"},{"revision":"49f4350a11f5667f690ab804c32c1def","url":"static/js/main.646e4c21.js"},{"revision":"e337a4bb9e13501cf60d85c4868714b0","url":"static/media/isbn_mod_bg.6e6be425ee5d2d381bec.wasm"},{"revision":"24f2b115d3964c9f977462cdd38b066a","url":"wasm/koder.js"},{"revision":"6f11e7db4fe9aca82cac7150bfc33769","url":"wasm/zbar.js"},{"revision":"e8789bf03df9c2c85e9c59ab0a0cd0c6","url":"wasm/zbar.wasm"},{"revision":"bb1c649a95ffa80369254cc3e51b9a41","url":"wasmWorker.js"}]);
workbox.routing.registerRoute(
new RegExp("https://fonts.(?:.googlepis|gstatic).com/(.*)"),
new workbox.strategies.CacheFirst({
Expand Down
1 change: 1 addition & 0 deletions isbn_wasm_mod/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ console_error_panic_hook = { version = "0.1.7", optional = true }
# allocator, however.
wee_alloc = { version = "0.4.5", optional = true }
chrono = { version = "0.4.38", features = ["serde"] }
anyhow = "1.0.86"

[dependencies.web-sys]
version = "0.3"
Expand Down
47 changes: 46 additions & 1 deletion isbn_wasm_mod/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,13 +84,15 @@ pub async fn get_book_data(isbn: String) {
log!("Book data received");
WasmResponse {
google_books: Some(WasmResult::Ok(v)),
..Default::default()
}
}
Err(e) => {
log!("Failed to get book data");
log!("{:?}", e);
WasmResponse {
google_books: Some(WasmResult::Err(format!("{:?}", e))),
..Default::default()
}
}
};
Expand All @@ -104,11 +106,54 @@ pub async fn get_book_data(isbn: String) {
if let Some(Ok(v)) = resp.google_books {
log!("Storing book in local storage");
if let Some(v) = BookRecord::from_google_books(v, &isbn) {
v.add_note(&runtime, "Book scanned".to_string());
v.store_locally(&runtime);
}
}
}

/// Returns the list of previously scanned books from the local storage.
/// See `fn report_progress()` for more details.
#[wasm_bindgen]
pub async fn get_scanned_books() {
log!("Getting the list of books from local storage");

// need the runtime for the global context and fetch
let runtime = match get_runtime().await {
Ok(v) => v,

// if this happened it would be a bug
Err(e) => {
log!("Failed to get runtime: {:?}", e);
return;
}
};

// get Books from local storage and wrap them into a response struct
let resp = match storage::Books::get(&runtime) {
Ok(v) => {
log!("Book list retrieved");
WasmResponse {
local_books: Some(WasmResult::Ok(v)),
..Default::default()
}
}
Err(e) => {
log!("Failed to get list of books");
log!("{:?}", e);
WasmResponse {
local_books: Some(WasmResult::Err(format!("{:?}", e))),
..Default::default()
}
}
};

// log!("Book data below:");
// log!("{:?}", resp);

// send the response back to the UI thread
report_progress(resp.to_string());
}

/// All error handling in this crate is based on either retrying a request after some time
/// or exiting gracefully.
#[derive(Debug, Clone, PartialEq)]
Expand Down
112 changes: 94 additions & 18 deletions isbn_wasm_mod/src/storage.rs
Original file line number Diff line number Diff line change
@@ -1,25 +1,24 @@
use crate::google::Volumes;
use crate::utils::log;
use anyhow::{bail, Result};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use web_sys::Window;

#[derive(Deserialize, Serialize, Debug)]
pub struct BookNote {
pub timestamp: DateTime<Utc>,
pub note: String,
}

#[derive(Deserialize, Serialize, Debug)]
pub struct BookRecord {
#[serde(default)]
pub isbn: String,
#[serde(default)]
pub title: String,
#[serde(default)]
pub author: String,
pub notes: Vec<BookNote>,
#[serde(default)]
pub timestamp: DateTime<Utc>,
}

#[derive(Deserialize, Serialize, Debug)]
struct BookDB {
pub struct Books {
books: Vec<BookRecord>,
}

Expand All @@ -28,7 +27,7 @@ impl BookRecord {
/// The book record is stored in the local storage (front-end only access).
/// Fails silently if the record cannot be stored.
/// TODO: Add error handling.
pub(crate) fn add_note(self, runtime: &Window, note: String) {
pub(crate) fn store_locally(self, runtime: &Window) {
// get the book record from the database

let ls = match runtime.local_storage() {
Expand All @@ -45,7 +44,10 @@ impl BookRecord {

let mut book_record = match ls.get_item(&self.isbn) {
Ok(Some(v)) => match serde_json::from_str::<BookRecord>(&v) {
Ok(v) => v,
Ok(v) => {
log!("Book record found in local storage");
v
}
Err(e) => {
log!("Failed to parse local storage book record: {:?}", e);
return;
Expand All @@ -62,23 +64,20 @@ impl BookRecord {
};

// add the note to the book record
book_record.notes.push(BookNote {
timestamp: Utc::now(),
note,
});
book_record.timestamp = Utc::now();

// replace the record in the database
let isbn = book_record.isbn.clone();
let book_record = match serde_json::to_string(&book_record) {
Ok(v) => v,
Err(e) => {
log!("Failed to serialize book record: {:?}", e);
log!("Failed to serialize book record for {isbn}: {:?}", e);
return;
}
};
match ls.set_item(&isbn, &book_record) {
Ok(()) => log!("Book record updated"),
Err(e) => log!("Failed to update book record: {:?}", e),
Ok(()) => log!("Book record saved"),
Err(e) => log!("Failed to save book record: {:?}", e),
}
}

Expand All @@ -93,7 +92,84 @@ impl BookRecord {
isbn: isbn.to_string(),
title: volumes.items[0].volume_info.title.clone(),
author: volumes.items[0].volume_info.authors[0].clone(),
notes: Vec::new(),
timestamp: Utc::now(),
})
}
}

impl Books {
/// Returns a sorted array of all book records stored locally.
/// Errors are logged.
pub(crate) fn get(runtime: &Window) -> Result<Self> {
// connect to the local storage
let ls = match runtime.local_storage() {
Ok(Some(v)) => v,
Err(e) => {
bail!("Failed to get local storage: {:?}", e);
}
_ => {
bail!("Local storage not available (OK(None))");
}
};

// get the total number of records
let number_of_records = match ls.length() {
Ok(v) => v,
Err(e) => {
bail!("Failed to get local storage length: {:?}", e);
}
};

// init the books array to the max possible size
let mut books = Vec::with_capacity(number_of_records.try_into().unwrap_or_else(|_e| {
log!("Failed to convert local storage length {number_of_records} to usize. It's a bug.");
0
}));

// get one key at a time (inefficient, but the best we have with Local Storage)
for i in 0..number_of_records {
// get the key by index
let key = match ls.key(i) {
Ok(Some(v)) => v,
Ok(None) => {
log!("Key {i} not found in local storage");
continue;
}
Err(e) => {
log!("Failed to get key {i} from local storage: {:?}", e);
continue;
}
};

// get value by key
let value = match ls.get_item(&key) {
Ok(Some(v)) => v,
Ok(None) => {
log!("Value not found in local storage: {key}");
continue;
}
Err(e) => {
log!("Failed to get value from local storage for {key}: {:?}", e);
continue;
}
};

// parse the string value into a book record
let book_record = match serde_json::from_str::<BookRecord>(&value) {
Ok(v) => v,
Err(e) => {
log!("Failed to parse local storage book record for {key}: {:?}", e);
continue;
}
};

books.push(book_record);
}

// the items in the local storage are never sorted
// sort the list to make the latest scanned book come first
books.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));

Ok(Books { books })
}
}
3 changes: 2 additions & 1 deletion isbn_wasm_mod/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,10 +174,11 @@ pub(crate) type WasmResult<T> = std::result::Result<T, String>;
/// A shared container for all types of responses placed in their own fields.
/// There can only be one type of response at a time.
/// This is needed for easy identification of the response type in JS.
#[derive(Serialize, Debug)]
#[derive(Serialize, Debug, Default)]
#[serde(rename_all = "camelCase")]
pub(crate) struct WasmResponse {
pub google_books: Option<WasmResult<crate::google::Volumes>>,
pub local_books: Option<WasmResult<crate::storage::Books>>,
}

impl fmt::Display for WasmResponse {
Expand Down
56 changes: 48 additions & 8 deletions src/components/welcome.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import React, { useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { build_book_url } from "./scanResult";
import useState from 'react-usestateref';
import initWasmModule, { get_scanned_books } from '../wasm-rust/isbn_mod.js';


export default function Welcome() {

const navigate = useNavigate();
const [books, setBooks] = useState([]); // the list of books saved in localStorage

useEffect(() => {

Expand All @@ -14,9 +17,47 @@ export default function Welcome() {
// a scan that sets them to the book details
// make sure the values are synchronized with index.html
// TODO: change ids to constants
document.title = "📖📚📚"
document.title = "📖📚📚";

// get the list of books from the localStorage
(async () => {
await initWasmModule(); // run the wasm initializer before calling wasm methods
// console.log("Requesting scanned books");
// request book data from WASM module
// the responses are sent back as messages to the window object
get_scanned_books();
// console.log("Requested scanned books (inside async)");
})();

// console.log("Requested scanned books (outside async)");
}, []);

window.addEventListener("message", (msg) => {
// console.log(`WASM msg: ${msg.data} / ${msg.origin} / ${msg.source}`);
// WASM messages should be JSON objects
let data;
try {
data = JSON.parse(msg.data);
}
catch (e) {
// use this log for debugging, but this mostly logs messages sent from React tooling
// in development mode, not sure it's worth logging this in production
// console.log(`Error parsing JSON data: ${e}`);
return;
}

// see `WasmResult` and `WasmResponse` in the WASM code for the structure of the data
if (data?.localBooks?.Ok) {
let list_of_books = data.localBooks.Ok?.books;
// console.log(`Books: ${JSON.stringify(list_of_books)}`);
setBooks(list_of_books);
}
else {
// console.log("Welcome screen received a message that is not a list of books");
// console.log(data);
}
});

const onBtnClickHandler = async (e) => {
e.preventDefault();
navigate(`scan`)
Expand All @@ -31,16 +72,15 @@ export default function Welcome() {
// renders the list of books saved in localStorage
const renderList = () => {

const records = [];
const book_list = [];

// for (let i = 0; i < localStorage.length; i++) {
// let record =JSON.parse(localStorage.getItem(localStorage.key(i)));
// let url = build_book_url(record.title, record.author, record.isbn);
// records.push(<li><a href={url}>{record.title}</a> {" by " + record.author}</li>);
// }
books.forEach((book) => {
let url = build_book_url(book.title, book.author, book.isbn);
book_list.push(<li key={book.isbn}><a href={url}>{book.title}</a> {" by " + book.author}</li>);
});

return <ul className="scanList">
{records}
{book_list}
</ul>
};

Expand Down
Loading

0 comments on commit f134ea4

Please sign in to comment.