feat: concise view sorts all books by LAST_MODIFIED, add property to sample data
This commit is contained in:
@@ -10,6 +10,7 @@
|
||||
:REC_NOTE: Le Guin at her most human
|
||||
:RATING:
|
||||
:ADDED: [2026-04-01]
|
||||
:LAST_MODIFIED: [2026-04-01]
|
||||
:ID: 9a2b-lhd
|
||||
:END:
|
||||
|
||||
@@ -23,6 +24,7 @@
|
||||
:REC_NOTE:
|
||||
:RATING:
|
||||
:ADDED: [2026-05-10]
|
||||
:LAST_MODIFIED: [2026-05-10]
|
||||
:ID: 7f3c-dun
|
||||
:END:
|
||||
|
||||
@@ -36,6 +38,7 @@
|
||||
:REC_NOTE:
|
||||
:RATING:
|
||||
:ADDED: [2026-06-01]
|
||||
:LAST_MODIFIED: [2026-06-01]
|
||||
:ID: 5e8d-pir
|
||||
:END:
|
||||
|
||||
@@ -49,6 +52,7 @@
|
||||
:REC_NOTE: If you liked Player of Games, this one will wreck you
|
||||
:RATING:
|
||||
:ADDED: [2026-02-20]
|
||||
:LAST_MODIFIED: [2026-03-09]
|
||||
:ID: 8c1e-uow
|
||||
:END:
|
||||
:LOGBOOK:
|
||||
@@ -68,6 +72,7 @@
|
||||
:REC_NOTE:
|
||||
:RATING: 5
|
||||
:ADDED: [2025-11-15]
|
||||
:LAST_MODIFIED: [2025-11-15]
|
||||
:ID: 3a1b-aj
|
||||
:END:
|
||||
:LOGBOOK:
|
||||
|
||||
@@ -105,6 +105,20 @@ SELECTED-ID expands the matching book's detail section."
|
||||
(push book (gethash status grouped))))
|
||||
(let ((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)
|
||||
(let ((group-books (nreverse (gethash status grouped))))
|
||||
(when group-books
|
||||
@@ -118,18 +132,6 @@ SELECTED-ID expands the matching book's detail section."
|
||||
(when (or (null effective-filter)
|
||||
(string= (downcase status) effective-filter)
|
||||
(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))
|
||||
(dolist (book group-books)
|
||||
(let* ((id (plist-get book :id))
|
||||
@@ -174,38 +176,6 @@ SELECTED-ID expands the matching book's detail section."
|
||||
collect (ht
|
||||
("date" (format "[%s]" date))
|
||||
("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 (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")
|
||||
("href" (format "/edit?id=%s&action=status&value=READING" selected-id)))))
|
||||
(t nil))
|
||||
nil)))))))))
|
||||
))))
|
||||
nil))))))))
|
||||
)))))
|
||||
(defun spine-add-book (&rest args)
|
||||
"Add a new WANT book to `spine-org-file'.
|
||||
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")))))
|
||||
(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)
|
||||
"Redirect root to /index."
|
||||
(httpd-redirect proc "/index" 302))
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
"Use of Weapons"))))
|
||||
|
||||
(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))
|
||||
(groups (ht-get model "groups"))
|
||||
(want-group (cl-find "on deck" groups
|
||||
|
||||
Reference in New Issue
Block a user