diff --git a/library/README.md b/library/README.md index 227c702..bd1a2ad 100644 --- a/library/README.md +++ b/library/README.md @@ -243,3 +243,15 @@ Instead: the book page has an **Open AR BookFind** link (plus **Copy title** to `library.csv` (one row per copy, with any reading level) and imports a CSV of `isbn, level, points[, quiz]` you made yourself (for example from a spreadsheet or a Renaissance report you are entitled to export). Levels are stored in `reading-levels.json` in the Add books data volume, keyed by ISBN, so they survive a Koha reinstall and are included in the usual backups of that folder. + +### Live camera scanning (needs HTTPS) + +Browsers only allow a live camera on HTTPS pages (or on the machine itself). Put the library site behind Caddy with a real +certificate and **Live scan** buttons appear: Add books (scan a stack of ISBNs one after another), Lend / return (live card or +phone-QR scan, live book check-out for the chosen person, live returns). Chrome on Android reads codes in the browser; other +browsers send small camera frames to the server's decoder a couple of times a second. On plain HTTP the photo buttons stay. + +### Copying reading levels from AR BookFind + +On the AR BookFind book page, select everything (Ctrl/Cmd+A), copy, and paste into the box on the book page here: the level, +points, quiz number and interest level fill in by themselves; check them and press Save. diff --git a/library/quickadd/__pycache__/app.cpython-313.pyc b/library/quickadd/__pycache__/app.cpython-313.pyc index 345fff7..507af44 100644 Binary files a/library/quickadd/__pycache__/app.cpython-313.pyc and b/library/quickadd/__pycache__/app.cpython-313.pyc differ diff --git a/library/quickadd/app.py b/library/quickadd/app.py index 7b7f4e2..e810082 100644 --- a/library/quickadd/app.py +++ b/library/quickadd/app.py @@ -1247,12 +1247,13 @@ def _num(v, maxlen=8): def op_ar_set(payload): key = _isbn13(payload.get("isbn")) level, points, quiz = _num(payload.get("level")), _num(payload.get("points")), re.sub(r"[^0-9]", "", str(payload.get("quiz") or ""))[:8] + interest = re.sub(r"[^A-Za-z+]", "", str(payload.get("interest") or ""))[:4].upper() if not key or level is None or points is None: return 400, {"status": "invalid", "message": "Level and points are numbers like 4.5 and 6."} with LOCK: d = _ar_load() - if level or points or quiz: - d[key] = {"level": level, "points": points, "quiz": quiz} + if level or points or quiz or interest: + d[key] = {"level": level, "points": points, "quiz": quiz, "interest": interest} else: d.pop(key, None) _ar_save(d) @@ -1272,6 +1273,7 @@ def op_ar_import(payload): if n in head: return head.index(n) return None + cint = col("interest", "interestlevel", "il") ci, cl, cp, cq = col("isbn", "isbn13"), col("level", "arlevel", "bl", "booklevel"), col("points", "arpoints", "pts"), col("quiz", "quizno", "quiznumber") if ci is None or (cl is None and cp is None and cq is None): return 400, {"status": "invalid", "message": "Need a header row with an isbn column and at least one of: level, points, quiz."} @@ -1287,7 +1289,8 @@ def op_ar_import(payload): continue old = d.get(key, {}) d[key] = {"level": lv or old.get("level", ""), "points": pt or old.get("points", ""), - "quiz": re.sub(r"[^0-9]", "", get(cq))[:8] or old.get("quiz", "")} + "quiz": re.sub(r"[^0-9]", "", get(cq))[:8] or old.get("quiz", ""), + "interest": re.sub(r"[^A-Za-z+]", "", get(cint))[:4].upper() or old.get("interest", "")} done += 1 _ar_save(d) return 200, {"status": "imported", "count": done, "skipped": skipped} @@ -1301,7 +1304,7 @@ def export_csv(): ar = _ar_load() out = io.StringIO() w = csv.writer(out) - w.writerow(["title", "author", "isbn", "year", "barcode", "call number", "shelf", "collection", "status", "ar level", "ar points", "ar quiz"]) + w.writerow(["title", "author", "isbn", "year", "barcode", "call number", "shelf", "collection", "status", "ar level", "ar points", "ar quiz", "ar interest"]) page = 1 while True: books = koha("GET", "/biblios", query={"_per_page": "200", "_page": str(page), "_order_by": "title"}) or [] @@ -1316,7 +1319,7 @@ def export_csv(): for i in mine: w.writerow([(b.get("title") or "").rstrip(" /:"), b.get("author") or "", isbn, b.get("copyright_date") or "", i.get("external_id") or "", i.get("callnumber") or "", where.get(i.get("location"), ""), - colls.get(i.get("collection_code"), ""), item_status(i) if i else "", a.get("level", ""), a.get("points", ""), a.get("quiz", "")]) + colls.get(i.get("collection_code"), ""), item_status(i) if i else "", a.get("level", ""), a.get("points", ""), a.get("quiz", ""), a.get("interest", "")]) if len(books) < 200: break page += 1 diff --git a/library/quickadd/index.html b/library/quickadd/index.html index 11ee74f..7fc17f9 100644 --- a/library/quickadd/index.html +++ b/library/quickadd/index.html @@ -135,6 +135,12 @@ .cardsheet { display: grid; grid-template-columns: repeat(2, 85.6mm); gap: 6mm; justify-content: start; } .cardsheet .libcard { break-inside: avoid; } @media print { header, .noprint, nav { display: none !important; } body { background: #fff; } main { padding: 0; max-width: none; } .libcard { border: 1px solid #000; } } + #live { position: fixed; inset: 0; z-index: 50; background: rgba(0,0,0,.92); display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px; padding: 12px; } + #live[hidden] { display: none; } + #live video { width: min(100%, 640px); max-height: 60vh; border-radius: 12px; background: #000; object-fit: cover; } + #live .frame { position: relative; width: min(100%, 640px); } + #live .frame::after { content: ''; position: absolute; inset: 18% 12%; border: 3px solid rgba(255,255,255,.85); border-radius: 12px; pointer-events: none; } + #live .msg { color: #fff; font-weight: 600; text-align: center; min-height: 3em; max-width: 640px; } .sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); } @@ -190,7 +196,7 @@ -
+
@@ -200,7 +206,7 @@ -
+
Photograph the library card, or the QR picture on the borrower's phone.
@@ -262,6 +268,7 @@
+ Opens your phone's camera: fill the frame with the barcode and hold steady.
@@ -309,6 +316,11 @@ +