diff --git a/spine.el b/spine.el index e434f07..e515ecc 100644 --- a/spine.el +++ b/spine.el @@ -390,6 +390,81 @@ Signals an error if the book is not found." (when (eq (point) (point-min)) (error "spine-set-rating: no book found with ID %s" id))) (kill-buffer))))) + +(defun spine-move-book (id target-shelf) + "Move book with ID to TARGET-SHELF. +The book's level-2 headline is moved under the level-1 TARGET-SHELF headline. +Signals an error if the book or target shelf is not found." + (let ((org-file (expand-file-name spine-org-file)) + (shelf-re (format "^\\*+[ \t]+%s[ \t]*$" (regexp-quote target-shelf)))) + (unless (file-exists-p org-file) + (error "spine-move-book: file not found: %s" org-file)) + (with-current-buffer (find-file-noselect org-file) + (unwind-protect + (let (book-begin book-end book-text) + (org-element-map (org-element-parse-buffer 'headline) 'headline + (lambda (hl) + (when (= (org-element-property :level hl) 2) + (let ((hl-id (spine--prop (org-element-property :begin hl) "ID"))) + (when (equal id hl-id) + (setq book-begin (org-element-property :begin hl)) + (setq book-end (org-element-property :end hl)))))) + nil 'headline) + (unless book-begin + (error "spine-move-book: no book found with ID %s" id)) + (setq book-text (buffer-substring-no-properties book-begin book-end)) + (delete-region book-begin book-end) + ;; Re-find target shelf (positions shifted after deletion) + (goto-char (point-min)) + (let ((shelf-found nil)) + (while (and (not shelf-found) + (re-search-forward shelf-re nil t)) + (when (= (org-current-level) 1) + (setq shelf-found t))) + (unless shelf-found + (error "spine-move-book: shelf \"%s\" not found" target-shelf)) + (org-end-of-subtree) + (skip-chars-backward " \t\n") + (insert "\n") + (insert book-text) + (unless (bolp) (insert "\n")) + (save-buffer) + t)) + (kill-buffer))))) + +(defun spine-set-status (id new-status) + "Change the TODO keyword of book with ID to NEW-STATUS. +NEW-STATUS is one of \"want\", \"reading\", \"read\", or \"\" to remove. +Signals an error if the book is not found." + (let ((org-file (expand-file-name spine-org-file)) + (new-kw (car (rassoc new-status + '(("WANT" . "want") ("READING" . "reading") ("READ" . "read")))))) + (unless (file-exists-p org-file) + (error "spine-set-status: file not found: %s" org-file)) + (with-current-buffer (find-file-noselect org-file) + (unwind-protect + (progn + (org-element-map (org-element-parse-buffer 'headline) 'headline + (lambda (hl) + (when (= (org-element-property :level hl) 2) + (let ((pos (org-element-property :begin hl))) + (when (equal id (spine--prop pos "ID")) + (goto-char pos) + (let* ((raw (org-get-heading t t t t)) + (clean (if (string-match + "\\`\\(WANT\\|READING\\|READ\\)[ \t]+\\(.*\\)\\'" + raw) + (match-string 2 raw) + raw))) + (org-edit-headline + (if new-kw (format "%s %s" new-kw clean) clean))))))) + nil 'headline) + (unless (eq (point) (point-min)) + (save-buffer) + t) + (when (eq (point) (point-min)) + (error "spine-set-status: no book found with ID %s" id))) + (kill-buffer))))) (defun spine--extract-notes (hl-begin hl-end) "Extract reading notes from headline body between HL-BEGIN and HL-END. Returns list of (date-string text-string) pairs." @@ -540,13 +615,13 @@ Set by test harnesses that only need the model functions.") (defun httpd/edit (proc uri-path query request) - "Handle /edit: GET shows prompt page, POST processes actions. + "Handle /edit: POST processes actions; GET shows prompt pages. Query params: id - book ID - action - note | rating + action - note | rating | shelf | status text - note text (for note action) date - optional date string (for note action) - value - rating value (for rating action)" + value - rating value / shelf name / status key (for rating/shelf/status)" (let ((method (caar request)) (id (cadr (assoc "id" query))) (action (cadr (assoc "action" query))) @@ -559,8 +634,12 @@ Query params: ((equal action "note") (spine-add-note id text date)) ((equal action "rating") - (spine-set-rating id value))) - (httpd-redirect proc (format "/index?id=%s" id) 303)) + (spine-set-rating id value)) + ((equal action "shelf") + (spine-move-book id value)) + ((equal action "status") + (spine-set-status id value))) + (httpd-redirect proc (format "/book?id=%s" id) 303)) ;; GET: show prompt page for actions needing input (if (and id (equal action "note")) (let* ((books (spine-books)) diff --git a/templates/book.mustache b/templates/book.mustache index bd9d5ba..0f37a03 100644 --- a/templates/book.mustache +++ b/templates/book.mustache @@ -76,26 +76,30 @@