refactor(srs): update some basic ui

This commit is contained in:
rcmerci
2024-09-20 19:34:10 +08:00
parent ac752c8ba3
commit ef2f79e2a1
6 changed files with 146 additions and 27 deletions

View File

@@ -1,39 +1,152 @@
(ns frontend.extensions.fsrs
(:require [frontend.db :as db]
[frontend.handler.property :as property-handler]
"Flashcards functions based on FSRS, only works in db-based graphs"
(:require [datascript.core :as d]
[frontend.components.block :as component-block]
[frontend.context.i18n :refer [t]]
[frontend.db :as db]
[frontend.handler.db-based.property :as db-property-handler]
[frontend.state :as state]
[frontend.ui :as ui]
[frontend.util :as util]
[open-spaced-repetition.cljc-fsrs.core :as fsrs.core]
[rum.core :as rum]
[tick.core :as tick]))
(def ^:private instant->inst-ms (comp inst-ms tick/inst))
(defn- inst-ms->instant [ms] (tick/instant (js/Date. ms)))
(defn- fsrs-card-map->property-fsrs-state
"Convert card-map to value stored in property"
[fsrs-card-map]
(-> fsrs-card-map
(update :last-repeat tick/inst)
(update :due tick/inst)))
(update :last-repeat instant->inst-ms)
(update :due instant->inst-ms)))
(defn- property-fsrs-state->fsrs-card-map
"opposite version of `fsrs-card->property-fsrs-state`"
[prop-fsrs-state]
(-> prop-fsrs-state
(update :last-repeat tick/instant)
(update :due tick/instant)))
(update :last-repeat inst-ms->instant)
(update :due inst-ms->instant)))
(defn- get-card-map
"Return nil if block is not #card.
Return default card-map if :logseq.property/fsrs-state is nil"
Return default card-map if `:logseq.property.fsrs/state` or `:logseq.property.fsrs/due` is nil"
[block-entity]
(when (some (fn [tag] (= :logseq.class/Card (:db/ident tag))) ;block should contains #Card
(:block/tags block-entity))
(or (some-> (:logseq.property/fsrs-state block-entity)
property-fsrs-state->fsrs-card-map)
(fsrs.core/new-card!))))
(let [fsrs-state (:logseq.property.fsrs/state block-entity)
fsrs-due (:property.value/content (:logseq.property.fsrs/due block-entity))
return-default-card-map? (not (and fsrs-state fsrs-due))]
(if return-default-card-map?
(fsrs.core/new-card!)
(property-fsrs-state->fsrs-card-map (assoc fsrs-state :due fsrs-due))))))
(defn repeat-card!
[repo block-id rating]
(let [eid (if (uuid? block-id) [:block/uuid block-id] block-id)
block-entity (db/entity repo eid)]
(when-let [card-map (get-card-map block-entity)]
(let [next-card-map (fsrs.core/repeat-card! card-map rating)]
(property-handler/set-block-property!
repo block-id
:logseq.property/fsrs-state (fsrs-card-map->property-fsrs-state next-card-map))))))
(let [next-card-map (fsrs.core/repeat-card! card-map rating)
prop-card-map (fsrs-card-map->property-fsrs-state next-card-map)
prop-fsrs-state (dissoc prop-card-map :due)
prop-fsrs-due (:due prop-card-map)]
(db-property-handler/set-block-property!
block-id :logseq.property.fsrs/state prop-fsrs-state)
(db-property-handler/create-property-text-block!
block-id :logseq.property.fsrs/due (str prop-fsrs-due)
{:new-block-id (db/new-block-id)})))))
(defn get-card-block-ids
[repo]
(let [db (db/get-db repo)]
(->>
(d/q '[:find ?b
:where
[?b :block/tags :logseq.class/Card]
[?b :block/uuid]]
db)
(apply concat))))
(defn get-due-card-block-ids
[repo]
(let [db (db/get-db repo)
now-inst-ms (inst-ms (js/Date.))]
(->> (d/q '[:find ?b
:in $ ?now-inst-ms
:where
[?b :block/tags :logseq.class/Card]
[?b :logseq.property.fsrs/due ?due-b]
[?due-b :property.value/content ?due]
[(>= ?now-inst-ms ?due)]
[?b :block/uuid]]
db now-inst-ms)
(apply concat))))
(defn- btn-with-shortcut [{:keys [shortcut id btn-text background on-click class]}]
(ui/button
[:span btn-text (when-not (util/sm-breakpoint?)
[" " (ui/render-keyboard-shortcut shortcut {:theme :text})])]
:id id
:class (str id " " class)
:background background
:on-pointer-down (fn [e] (util/stop-propagation e))
:on-click (fn [_e]
(js/setTimeout #(on-click) 10))))
(def ^:private phase->next-phase
{:init :show-answer
:show-answer :end
:end :end})
(rum/defcs card <
(rum/local :init ::phase)
[state repo block-entity]
(let [*phase (::phase state)]
[:div.ls-card.content
[:div (component-block/breadcrumb {} repo (:block/uuid block-entity) {})]
(component-block/blocks-container
(cond-> {}
(contains? #{:init} @*phase) (assoc :hide-children? true))
[block-entity])
(btn-with-shortcut {:btn-text (t :flashcards/modal-btn-show-answers)
:shortcut "s"
:id (str "card-answers")
:on-click #(swap! *phase phase->next-phase)})]))
;; {
;; :again 1 ;; We got the answer wrong. Automatically means that we
;; ;; have forgotten the card. This is a lapse in memory.
;; :hard 2 ;; The answer was only partially correct and/or we took
;; ;; too long to recall it.
;; :good 3 ;; The answer was correct but we were not confident about it.
;; :easy 4 ;; The answer was correct and we were confident and quick
;; ;; in our recall.
;; }
(def ^:private rating->shortcut
{:again "1"
:hard "2"
:good "3"
:easy "4"})
(defn- rating-btns
[repo block-id *card-index]
(mapv
(fn [rating]
(btn-with-shortcut {:btn-text (name rating)
:shortcut (rating->shortcut rating)
:id (str "card-" (name rating))
:on-click #(do (repeat-card! repo block-id rating)
(swap! *card-index inc))}))
(keys rating->shortcut)))
(rum/defcs cards <
(rum/local 0 ::card-index)
[state]
(let [repo (state/get-current-repo)
block-ids (get-card-block-ids repo)
*card-index (::card-index state)]
(if-let [block-entity (some-> (nth block-ids @*card-index nil) db/entity)]
(vec (concat [:div (card repo block-entity)]
(rating-btns repo (:db/id block-entity) *card-index)))
[:p.p-2 (t :flashcards/modal-finished)])))