Add more commands
This commit is contained in:
parent
084ffad14f
commit
b6065cd2f7
1 changed files with 99 additions and 32 deletions
119
anki-editor.el
119
anki-editor.el
|
@ -1,6 +1,6 @@
|
||||||
;;; anki-editor.el --- Minor mode for making Anki cards with Org -*- lexical-binding: t; -*-
|
;;; anki-editor.el --- Minor mode for making Anki cards with Org -*- lexical-binding: t; -*-
|
||||||
;;
|
;;
|
||||||
;; Copyright (C) 2018 Lei Tan <louietanlei@gmail.com>
|
;; Copyright (C) 2018-2019 Lei Tan <louietanlei[at]gmail[dot]com>
|
||||||
;;
|
;;
|
||||||
;; Description: Make Anki Cards in Org-mode
|
;; Description: Make Anki Cards in Org-mode
|
||||||
;; Author: Lei Tan
|
;; Author: Lei Tan
|
||||||
|
@ -76,6 +76,7 @@
|
||||||
(defconst anki-editor-prop-failure-reason "ANKI_FAILURE_REASON")
|
(defconst anki-editor-prop-failure-reason "ANKI_FAILURE_REASON")
|
||||||
(defconst anki-editor-buffer-html-output "*AnkiEditor HTML Output*")
|
(defconst anki-editor-buffer-html-output "*AnkiEditor HTML Output*")
|
||||||
(defconst anki-editor-org-tag-regexp "^\\([[:alnum:]_@#%]+\\)+$")
|
(defconst anki-editor-org-tag-regexp "^\\([[:alnum:]_@#%]+\\)+$")
|
||||||
|
(defconst anki-editor-ankiconnect-version 5)
|
||||||
|
|
||||||
(defgroup anki-editor nil
|
(defgroup anki-editor nil
|
||||||
"Customizations for anki-editor."
|
"Customizations for anki-editor."
|
||||||
|
@ -136,7 +137,8 @@ See https://apps.ankiweb.net/docs/manual.html#latex-conflicts.")
|
||||||
|
|
||||||
(defun anki-editor--anki-connect-invoke (action &optional params)
|
(defun anki-editor--anki-connect-invoke (action &optional params)
|
||||||
"Invoke AnkiConnect with ACTION and PARAMS."
|
"Invoke AnkiConnect with ACTION and PARAMS."
|
||||||
(let ((request-body (json-encode (anki-editor--anki-connect-action action params 5)))
|
(let ((request-body (json-encode
|
||||||
|
(anki-editor--anki-connect-action action params anki-editor-ankiconnect-version)))
|
||||||
(request-backend 'curl)
|
(request-backend 'curl)
|
||||||
(json-array-type 'list)
|
(json-array-type 'list)
|
||||||
reply err)
|
reply err)
|
||||||
|
@ -170,6 +172,7 @@ See https://apps.ankiweb.net/docs/manual.html#latex-conflicts.")
|
||||||
.result))
|
.result))
|
||||||
|
|
||||||
(defun anki-editor--anki-connect-invoke-multi (&rest actions)
|
(defun anki-editor--anki-connect-invoke-multi (&rest actions)
|
||||||
|
"Invoke AnkiConnect with ACTIONS, a list of (action . result-handler) pairs."
|
||||||
(-zip-with (lambda (result handler)
|
(-zip-with (lambda (result handler)
|
||||||
(when-let ((_ (listp result))
|
(when-let ((_ (listp result))
|
||||||
(err (alist-get 'error result)))
|
(err (alist-get 'error result)))
|
||||||
|
@ -203,7 +206,7 @@ The result is the path to the newly stored media file."
|
||||||
(when (equal :json-false (anki-editor--anki-connect-invoke-result
|
(when (equal :json-false (anki-editor--anki-connect-invoke-result
|
||||||
"retrieveMediaFile"
|
"retrieveMediaFile"
|
||||||
`((filename . ,media-file-name))))
|
`((filename . ,media-file-name))))
|
||||||
(message "Storing media file to Anki for %s..." path)
|
(message "Storing media file %s to Anki, this might take a while" path)
|
||||||
(setq content (base64-encode-string
|
(setq content (base64-encode-string
|
||||||
(with-temp-buffer
|
(with-temp-buffer
|
||||||
(insert-file-contents path)
|
(insert-file-contents path)
|
||||||
|
@ -433,7 +436,6 @@ Where the subtree is created depends on PREFIX."
|
||||||
|
|
||||||
(defun anki-editor--update-note (note)
|
(defun anki-editor--update-note (note)
|
||||||
"Request AnkiConnect for updating fields and tags of NOTE."
|
"Request AnkiConnect for updating fields and tags of NOTE."
|
||||||
|
|
||||||
(let ((queue (anki-editor--anki-connect-invoke-queue)))
|
(let ((queue (anki-editor--anki-connect-invoke-queue)))
|
||||||
(funcall queue
|
(funcall queue
|
||||||
'updateNoteFields
|
'updateNoteFields
|
||||||
|
@ -550,7 +552,9 @@ Where the subtree is created depends on PREFIX."
|
||||||
(mapcar #'org-entry-restore-space values)))
|
(mapcar #'org-entry-restore-space values)))
|
||||||
|
|
||||||
(defun anki-editor--build-fields ()
|
(defun anki-editor--build-fields ()
|
||||||
"Build a list of fields from subheadings of current heading, each element of which is a cons cell, the car of which is field name and the cdr of which is field content."
|
"Build a list of fields from subheadings of current heading,
|
||||||
|
each element of which is a cons cell, the car of which is field
|
||||||
|
name and the cdr of which is field content."
|
||||||
(save-excursion
|
(save-excursion
|
||||||
(let (fields
|
(let (fields
|
||||||
(point-of-last-child (point)))
|
(point-of-last-child (point)))
|
||||||
|
@ -620,6 +624,7 @@ Where the subtree is created depends on PREFIX."
|
||||||
|
|
||||||
(defun anki-editor-setup-minor-mode ()
|
(defun anki-editor-setup-minor-mode ()
|
||||||
"Set up this minor mode."
|
"Set up this minor mode."
|
||||||
|
(anki-editor-anki-connect-check)
|
||||||
(add-hook 'org-property-allowed-value-functions #'anki-editor--get-allowed-values-for-property nil t)
|
(add-hook 'org-property-allowed-value-functions #'anki-editor--get-allowed-values-for-property nil t)
|
||||||
(advice-add 'org-set-tags :before #'anki-editor--before-set-tags)
|
(advice-add 'org-set-tags :before #'anki-editor--before-set-tags)
|
||||||
(advice-add 'org-get-buffer-tags :around #'anki-editor--get-buffer-tags)
|
(advice-add 'org-get-buffer-tags :around #'anki-editor--get-buffer-tags)
|
||||||
|
@ -632,7 +637,7 @@ Where the subtree is created depends on PREFIX."
|
||||||
|
|
||||||
;;; Commands
|
;;; Commands
|
||||||
|
|
||||||
(defun anki-editor-push-notes (&optional arg match scope)
|
(defun anki-editor-push-notes (&optional scope match)
|
||||||
"Build notes from headings that can be matched by MATCH within SCOPE and push them to Anki.
|
"Build notes from headings that can be matched by MATCH within SCOPE and push them to Anki.
|
||||||
|
|
||||||
The default search condition `&ANKI_NOTE_TYPE<>\"\"' will always
|
The default search condition `&ANKI_NOTE_TYPE<>\"\"' will always
|
||||||
|
@ -654,17 +659,14 @@ See doc string of `org-map-entries' for what these different options mean.
|
||||||
|
|
||||||
If one fails, the failure reason will be set in property drawer
|
If one fails, the failure reason will be set in property drawer
|
||||||
of that heading."
|
of that heading."
|
||||||
(interactive "P")
|
(interactive (list (cond
|
||||||
|
|
||||||
(unless scope
|
|
||||||
(setq scope (cond
|
|
||||||
((region-active-p) 'region)
|
((region-active-p) 'region)
|
||||||
((equal arg '(4)) 'tree)
|
((equal current-prefix-arg '(4)) 'tree)
|
||||||
((equal arg '(16)) 'file)
|
((equal current-prefix-arg '(16)) 'file)
|
||||||
((equal arg '(64)) 'agenda)
|
((equal current-prefix-arg '(64)) 'agenda)
|
||||||
(t nil))))
|
(t nil))))
|
||||||
|
|
||||||
(let* ((total (progn
|
(let ((total (progn
|
||||||
(message "Counting notes...")
|
(message "Counting notes...")
|
||||||
(length (anki-editor-map-note-entries t match scope))))
|
(length (anki-editor-map-note-entries t match scope))))
|
||||||
(acc 0)
|
(acc 0)
|
||||||
|
@ -678,20 +680,39 @@ of that heading."
|
||||||
(anki-editor--push-note (anki-editor-note-at-point))
|
(anki-editor--push-note (anki-editor-note-at-point))
|
||||||
(error (cl-incf failed)
|
(error (cl-incf failed)
|
||||||
(anki-editor--set-failure-reason (error-message-string err)))))
|
(anki-editor--set-failure-reason (error-message-string err)))))
|
||||||
match
|
match scope)
|
||||||
scope)
|
|
||||||
|
|
||||||
(message (if (= 0 failed)
|
(message
|
||||||
(format "Successfully pushed %d notes to Anki." acc)
|
(cond
|
||||||
(format "Pushed %d notes, %d of which are failed. Check property drawers for failure reasons. Once you've fixed the issues, you could use `anki-editor-retry-failure-notes' to re-push the failed notes."
|
((zerop total) "Nothing to push")
|
||||||
acc failed)))))
|
((zerop failed) (format "Pushed %d notes to Anki successfully" acc))
|
||||||
|
(t (format "Pushed %d notes in total, among which %d were failed. Check property drawers for failure reasons.
|
||||||
|
When the issues are resolved, you could repush the failed ones with `anki-editor-retry-failed-notes'."
|
||||||
|
acc failed))))))
|
||||||
|
|
||||||
(defun anki-editor-retry-failure-notes (&optional arg scope)
|
(defun anki-editor-push-new-notes (&optional scope)
|
||||||
"Retry pushing notes that were failed.
|
"Push note entries without ANKI_NOTE_ID in SCOPE to Anki."
|
||||||
|
(interactive)
|
||||||
|
(anki-editor-push-notes scope (concat anki-editor-prop-note-id "=\"\"")))
|
||||||
|
|
||||||
|
(defun anki-editor-retry-failed-notes (&optional scope)
|
||||||
|
"Retry pushing notes marked as failed.
|
||||||
This command just calls `anki-editor-submit' with match string
|
This command just calls `anki-editor-submit' with match string
|
||||||
matching non-empty `ANKI_FAILURE_REASON' properties."
|
matching non-empty `ANKI_FAILURE_REASON' properties."
|
||||||
(interactive "P")
|
(interactive)
|
||||||
(anki-editor-push-notes arg (concat anki-editor-prop-failure-reason "<>\"\"") scope))
|
(anki-editor-push-notes scope (concat anki-editor-prop-failure-reason "<>\"\"")))
|
||||||
|
|
||||||
|
(defun anki-editor-delete-notes (noteids)
|
||||||
|
"Delete notes in NOTEIDS or the note at point."
|
||||||
|
(interactive (list (list (org-entry-get nil anki-editor-prop-note-id))))
|
||||||
|
(when (or (not (called-interactively-p 'interactive))
|
||||||
|
(yes-or-no-p (format "Do you really want to delete note %s? The deletion can't be undone. " (nth 0 noteids))))
|
||||||
|
(anki-editor--anki-connect-invoke-result
|
||||||
|
"deleteNotes"
|
||||||
|
`((notes . ,noteids)))
|
||||||
|
(org-entry-delete nil anki-editor-prop-note-id)
|
||||||
|
(when (called-interactively-p 'interactive)
|
||||||
|
(message "Deleted note %s" (nth 0 noteids)))))
|
||||||
|
|
||||||
(defun anki-editor-insert-note (&optional prefix)
|
(defun anki-editor-insert-note (&optional prefix)
|
||||||
"Insert a note interactively.
|
"Insert a note interactively.
|
||||||
|
@ -757,6 +778,19 @@ same as how it is used by `M-RET'(org-insert-heading)."
|
||||||
(interactive)
|
(interactive)
|
||||||
(org-export-replace-region-by anki-editor--ox-anki-html-backend))
|
(org-export-replace-region-by anki-editor--ox-anki-html-backend))
|
||||||
|
|
||||||
|
|
||||||
|
;;; More utilities
|
||||||
|
|
||||||
|
(defun anki-editor-anki-connect-check ()
|
||||||
|
"Check if correct version of AnkiConnect is serving."
|
||||||
|
(interactive)
|
||||||
|
(let ((ver (anki-editor--anki-connect-invoke-result "version")))
|
||||||
|
(if (<= anki-editor-ankiconnect-version ver)
|
||||||
|
(when (called-interactively-p 'interactive)
|
||||||
|
(message "AnkiConnect v.%d is running" ver))
|
||||||
|
(error "anki-editor requires minimal version %d of AnkiConnect installed"
|
||||||
|
anki-editor-ankiconnect-version))))
|
||||||
|
|
||||||
(defun anki-editor-anki-connect-upgrade ()
|
(defun anki-editor-anki-connect-upgrade ()
|
||||||
"Upgrade AnkiConnect to the latest version.
|
"Upgrade AnkiConnect to the latest version.
|
||||||
|
|
||||||
|
@ -767,11 +801,44 @@ code in the master branch of its Github repo.
|
||||||
This is useful when new version of this package depends on the
|
This is useful when new version of this package depends on the
|
||||||
bugfixes or new features of AnkiConnect."
|
bugfixes or new features of AnkiConnect."
|
||||||
(interactive)
|
(interactive)
|
||||||
(when (yes-or-no-p "NOTE: This will download the latest codebase of AnkiConnect to your system, which is not guaranteed to be safe or stable. Generally, you don't need this command, this is useful only when new version of this package requires the updates of AnkiConnect that are not released yet. Do you still want to continue?")
|
(when (yes-or-no-p "This is going to download the latest AnkiConnect from the Internet to your computer, do you want to continue? ")
|
||||||
(let ((result (anki-editor--anki-connect-invoke-result "upgrade")))
|
(let ((result (anki-editor--anki-connect-invoke-result "upgrade")))
|
||||||
(when (and (booleanp result) result)
|
(when (and (booleanp result) result)
|
||||||
(message "AnkiConnect has been upgraded, you might have to restart Anki to make it in effect.")))))
|
(message "AnkiConnect has been upgraded, you might have to restart Anki for the changes to take effect.")))))
|
||||||
|
|
||||||
|
(defun anki-editor-sync-collections ()
|
||||||
|
"Synchronizes the local anki collections with ankiweb."
|
||||||
|
(interactive)
|
||||||
|
(anki-editor--anki-connect-invoke "sync"))
|
||||||
|
|
||||||
|
(defun anki-editor-gui-browse (&optional query)
|
||||||
|
"Open Anki Browser with QUERY.
|
||||||
|
When called interactively, it will try to set QUERY to current
|
||||||
|
note or deck."
|
||||||
|
(interactive (list (pcase (org-entry-get-with-inheritance anki-editor-prop-note-id)
|
||||||
|
((and (pred stringp) nid) (format "nid:%s" nid))
|
||||||
|
(_ (format "deck:%s"
|
||||||
|
(or (org-entry-get-with-inheritance anki-editor-prop-deck)
|
||||||
|
"current"))))))
|
||||||
|
(anki-editor--anki-connect-invoke "guiBrowse" `((query . ,(or query "")))))
|
||||||
|
|
||||||
|
(defun anki-editor-gui-add-cards ()
|
||||||
|
"Open Anki Add Cards dialog with presets from current note
|
||||||
|
entry."
|
||||||
|
(interactive)
|
||||||
|
(anki-editor--anki-connect-invoke-result
|
||||||
|
"guiAddCards"
|
||||||
|
`((note . ,(cons '(options . ((closeAfterAdding . t)))
|
||||||
|
(anki-editor--anki-connect-map-note
|
||||||
|
(anki-editor-note-at-point)))))))
|
||||||
|
|
||||||
|
(defun anki-editor-find-notes (&optional query)
|
||||||
|
"Find notes with QUERY."
|
||||||
|
(interactive "sQuery: ")
|
||||||
|
(let ((nids (anki-editor--anki-connect-invoke-result "findNotes" `((query . ,query)))))
|
||||||
|
(if (called-interactively-p 'interactive)
|
||||||
|
(message "%S" nids)
|
||||||
|
nids)))
|
||||||
|
|
||||||
(provide 'anki-editor)
|
(provide 'anki-editor)
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue