feat: concise view sorts all books by LAST_MODIFIED, add property to sample data

This commit is contained in:
2026-06-21 22:02:12 -04:00
parent fa6be9ff03
commit 653ee3442d
3 changed files with 79 additions and 47 deletions
+5
View File
@@ -10,6 +10,7 @@
:REC_NOTE: Le Guin at her most human :REC_NOTE: Le Guin at her most human
:RATING: :RATING:
:ADDED: [2026-04-01] :ADDED: [2026-04-01]
:LAST_MODIFIED: [2026-04-01]
:ID: 9a2b-lhd :ID: 9a2b-lhd
:END: :END:
@@ -23,6 +24,7 @@
:REC_NOTE: :REC_NOTE:
:RATING: :RATING:
:ADDED: [2026-05-10] :ADDED: [2026-05-10]
:LAST_MODIFIED: [2026-05-10]
:ID: 7f3c-dun :ID: 7f3c-dun
:END: :END:
@@ -36,6 +38,7 @@
:REC_NOTE: :REC_NOTE:
:RATING: :RATING:
:ADDED: [2026-06-01] :ADDED: [2026-06-01]
:LAST_MODIFIED: [2026-06-01]
:ID: 5e8d-pir :ID: 5e8d-pir
:END: :END:
@@ -49,6 +52,7 @@
:REC_NOTE: If you liked Player of Games, this one will wreck you :REC_NOTE: If you liked Player of Games, this one will wreck you
:RATING: :RATING:
:ADDED: [2026-02-20] :ADDED: [2026-02-20]
:LAST_MODIFIED: [2026-03-09]
:ID: 8c1e-uow :ID: 8c1e-uow
:END: :END:
:LOGBOOK: :LOGBOOK:
@@ -68,6 +72,7 @@
:REC_NOTE: :REC_NOTE:
:RATING: 5 :RATING: 5
:ADDED: [2025-11-15] :ADDED: [2025-11-15]
:LAST_MODIFIED: [2025-11-15]
:ID: 3a1b-aj :ID: 3a1b-aj
:END: :END:
:LOGBOOK: :LOGBOOK:
+73 -46
View File
@@ -105,6 +105,20 @@ SELECTED-ID expands the matching book's detail section."
(push book (gethash status grouped)))) (push book (gethash status grouped))))
(let ((groups nil) (let ((groups nil)
(summary-groups nil)) (summary-groups nil))
;; Concise view: sort all books by :LAST_MODIFIED:, take top 5
(when (null effective-filter)
(clrhash grouped)
(dolist (b (cl-subseq (sort (copy-sequence books)
(lambda (a b)
(let ((a-date (plist-get a :last_modified))
(b-date (plist-get b :last_modified)))
(cond
((null a-date) nil)
((null b-date) t)
(t (string> a-date b-date))))))
0 (min (length books) 5)))
(let ((status (or (plist-get b :status) "WANT")))
(push b (gethash status grouped)))))
(dolist (status order) (dolist (status order)
(let ((group-books (nreverse (gethash status grouped)))) (let ((group-books (nreverse (gethash status grouped))))
(when group-books (when group-books
@@ -118,18 +132,6 @@ SELECTED-ID expands the matching book's detail section."
(when (or (null effective-filter) (when (or (null effective-filter)
(string= (downcase status) effective-filter) (string= (downcase status) effective-filter)
(string= effective-filter "all")) (string= effective-filter "all"))
(when (and (null effective-filter)
(string= status "WANT"))
(setq group-books
(cl-subseq (sort group-books
(lambda (a b)
(let ((a-date (plist-get a :added))
(b-date (plist-get b :added)))
(cond
((null a-date) nil)
((null b-date) t)
(t (string> a-date b-date))))))
0 (min (length group-books) 5))))
(let ((book-models nil)) (let ((book-models nil))
(dolist (book group-books) (dolist (book group-books)
(let* ((id (plist-get book :id)) (let* ((id (plist-get book :id))
@@ -174,38 +176,6 @@ SELECTED-ID expands the matching book's detail section."
collect (ht collect (ht
("date" (format "[%s]" date)) ("date" (format "[%s]" date))
("text" text)))))))) ("text" text))))))))
("minibuffer"
(when selected
(let ((status (plist-get book :status)))
(ht ("actions"
(append
(cond
((equal status "WANT")
(list
(ht ("command" "status") ("label" "Mark reading")
("href" (format "/edit?id=%s&action=status&value=READING" id)))
(ht ("command" "note") ("label" "Add note")
("href" (format "/edit?id=%s&action=note" id)))))
((equal status "READING")
(list
(ht ("command" "status") ("label" "Mark read")
("href" (format "/edit?id=%s&action=status&value=READ" id)))
(ht ("command" "status") ("label" "Abandon")
("href" (format "/edit?id=%s&action=status&value=ABANDONED" id)))
(ht ("command" "note") ("label" "Add note")
("href" (format "/edit?id=%s&action=note" id)))))
((equal status "READ")
(list
(ht ("command" "status") ("label" "Read again")
("href" (format "/edit?id=%s&action=status&value=READING" id)))
(ht ("command" "note") ("label" "Add note")
("href" (format "/edit?id=%s&action=note" id)))))
((equal status "ABANDONED")
(list
(ht ("command" "status") ("label" "Try again")
("href" (format "/edit?id=%s&action=status&value=READING" id)))))
(t nil))
nil))))))
))) )))
(push model book-models))) (push model book-models)))
(push (ht ("label" (cdr (assoc status status-labels))) (push (ht ("label" (cdr (assoc status status-labels)))
@@ -254,8 +224,8 @@ SELECTED-ID expands the matching book's detail section."
(ht ("command" "status") ("label" "Try again") (ht ("command" "status") ("label" "Try again")
("href" (format "/edit?id=%s&action=status&value=READING" selected-id))))) ("href" (format "/edit?id=%s&action=status&value=READING" selected-id)))))
(t nil)) (t nil))
nil))))))))) nil))))))))
)))) )))))
(defun spine-add-book (&rest args) (defun spine-add-book (&rest args)
"Add a new WANT book to `spine-org-file'. "Add a new WANT book to `spine-org-file'.
Keyword arguments: :title :author :category :format :isbn :cover Keyword arguments: :title :author :category :format :isbn :cover
@@ -517,6 +487,63 @@ Set by test harnesses that only need the model functions.")
(ht ("app_title" "spine"))))) (ht ("app_title" "spine")))))
(insert (spine-render "add.mustache" model))))))) (insert (spine-render "add.mustache" model)))))))
(defun httpd/edit (proc uri-path query request)
"Handle /edit: GET shows prompt page, POST processes actions.
Query params:
id - book ID
action - status | note | rating
value - target status / rating value (for status/rating actions)
text - note text (for note action)
date - optional date string (for note/read actions)"
(let ((method (caar request))
(id (cadr (assoc "id" query)))
(action (cadr (assoc "action" query)))
(value (cadr (assoc "value" query)))
(text (cadr (assoc "text" query)))
(date (cadr (assoc "date" query))))
(if (equal method "POST")
(progn
(cond
((equal action "note")
(spine-add-note id text date))
((equal action "status")
(spine-set-status id value))
((equal action "rating")
(spine-set-rating id value)))
(httpd-redirect proc (format "/index?id=%s" id) 303))
;; GET: show prompt page for actions needing input
(if (and id (equal action "note"))
(let* ((books (spine-books))
(book (cl-find id books :key (lambda (b) (plist-get b :id)) :test #'equal))
(title (if book (plist-get book :title) "Unknown")))
(httpd-with-buffer proc "text/html"
(insert (format
"<!DOCTYPE html>
<html lang=\"en\">
<head><meta charset=\"utf-8\"/><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"/>
<meta name=\"color-scheme\" content=\"light dark\"/>
<title>Spine — note</title>
<link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/@picocss/pico@2.1.1/css/pico.min.css\"/>
</head>
<body>
<main class=\"container\">
<article style=\"max-width:560px;margin-inline:auto;\">
<header><strong>spine · add note</strong></header>
<p><em>%s</em></p>
<form method=\"post\" action=\"/edit\">
<input type=\"hidden\" name=\"id\" value=\"%s\"/>
<input type=\"hidden\" name=\"action\" value=\"note\"/>
<label>Date <input type=\"date\" name=\"date\" value=\"%s\"/></label>
<label>Note <textarea name=\"text\" rows=\"3\" required autofocus></textarea></label>
<button type=\"submit\">Add note</button>
</form>
</article>
</main>
</body>
</html>"
title id (format-time-string "%Y-%m-%d"))))))
;; Unknown action or missing id: redirect to index
(httpd-redirect proc "/index" 302))))
(defun httpd/ (proc uri-path query request) (defun httpd/ (proc uri-path query request)
"Redirect root to /index." "Redirect root to /index."
(httpd-redirect proc "/index" 302)) (httpd-redirect proc "/index" 302))
+1 -1
View File
@@ -41,7 +41,7 @@
"Use of Weapons")))) "Use of Weapons"))))
(ert-deftest spine-index-model-concise-want-limited-to-five () (ert-deftest spine-index-model-concise-want-limited-to-five ()
"Concise view limits WANT group to 5 books, sorted by :ADDED: desc." "Concise view limits WANT group to 5 books, sorted by :LAST_MODIFIED: desc."
(let* ((model (spine-index-model spine-index-model-test--books)) (let* ((model (spine-index-model spine-index-model-test--books))
(groups (ht-get model "groups")) (groups (ht-get model "groups"))
(want-group (cl-find "on deck" groups (want-group (cl-find "on deck" groups