Adds dev-only counters, with no behavior change, to answer the open
questions from the migration plan before touching the transport:
- frontend.state/get-invoke-stats: per-thread-api-method count/total-ms/
max-ms for every <invoke-db-worker call, to census how many invokes a
typical navigation generates and their latency distribution.
- db-worker-node: per-repo in-flight /v1/invoke counter that logs a new
concurrency high-water mark, to check whether concurrent client writes
actually overlap before deciding on a write queue.
- subs-loader/get-stats: dedup-hit vs new-entry counts for load!, and
batch-size distribution per slot kind at flush, to check whether
in-flight snapshot requests are already deduplicated and whether the
{:children 25 :resources 25} limits are worth raising for network RTT.
Verified with clojure -M:cljs compile app and compile db-worker-node
(0 warnings on both).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2177 lines
70 KiB
Clojure
2177 lines
70 KiB
Clojure
(ns frontend.state
|
|
"Provides main application state and subscriptions."
|
|
(:require [cljs-bean.core :as bean]
|
|
[cljs.core.async :as async :refer [>!]]
|
|
[clojure.set :as set]
|
|
[clojure.string :as string]
|
|
[dommy.core :as dom]
|
|
[electron.ipc :as ipc]
|
|
[electron.locale :as electron-locale]
|
|
[frontend.dicts :as dicts]
|
|
[frontend.graph-tab :as graph-tab]
|
|
[frontend.mobile.util :as mobile-util]
|
|
[frontend.rfx :as rfx]
|
|
[frontend.spec.storage :as storage-spec]
|
|
[frontend.storage :as storage]
|
|
[frontend.util :as util]
|
|
[frontend.util.cursor :as cursor]
|
|
[frontend.util.entity :as entity]
|
|
[goog.dom :as gdom]
|
|
[goog.object :as gobj]
|
|
[logseq.common.config :as common-config]
|
|
[logseq.db :as ldb]
|
|
[logseq.shui.dialog.core :as shui-dialog]
|
|
[logseq.shui.hooks :as hooks]
|
|
[logseq.shui.ui :as shui]
|
|
[promesa.core :as p]))
|
|
|
|
(defonce *profile-state (volatile! {}))
|
|
|
|
(defonce *db-worker (atom nil))
|
|
(defonce *db-worker-thread (atom nil))
|
|
(defonce *db-worker-client-id (atom (storage/get :db-worker-client-id)))
|
|
(defonce *editor-info (atom nil))
|
|
(defonce app-ready-promise (p/deferred))
|
|
|
|
(def ^:private supported-locale-tags
|
|
(into {}
|
|
(map (fn [locale]
|
|
[(string/lower-case (name locale)) locale])
|
|
(keys dicts/dicts))))
|
|
|
|
(defn- canonical-preferred-language
|
|
"Convert browser/storage locale tags into one supported locale string."
|
|
[language]
|
|
(let [tag (cond
|
|
(keyword? language) (name language)
|
|
(string? language) language
|
|
:else nil)]
|
|
(when-not (string/blank? tag)
|
|
(let [normalized-tag (some-> tag string/trim string/lower-case)
|
|
base-tag (some-> normalized-tag (string/split #"-") first)]
|
|
(some-> (or (get supported-locale-tags normalized-tag)
|
|
(get supported-locale-tags base-tag)
|
|
:en)
|
|
name)))))
|
|
|
|
(def db-worker-ready?
|
|
"`<invoke-db-worker` throws err if `*db-worker` not ready yet.
|
|
Use this atom to wait till db-worker ready."
|
|
(let [ready? (atom (fn? @*db-worker))]
|
|
(add-watch *db-worker ::db-worker-ready?
|
|
(fn [_ _ _ worker]
|
|
(reset! ready? (fn? worker))))
|
|
ready?))
|
|
|
|
(defonce ^:private *invoke-stats (atom {}))
|
|
|
|
(defn get-invoke-stats
|
|
"Snapshot of db-worker invoke counters, keyed by thread-api method kw. Each
|
|
entry is {:count N :total-ms N :max-ms N}. Dev-only instrumentation for the
|
|
server-client migration latency baseline (see docs/agent-guide); does not
|
|
affect invocation behavior."
|
|
[]
|
|
@*invoke-stats)
|
|
|
|
(defn reset-invoke-stats!
|
|
[]
|
|
(reset! *invoke-stats {}))
|
|
|
|
(defn- record-invoke-stat!
|
|
[qkw elapsed-ms]
|
|
(swap! *invoke-stats update qkw
|
|
(fn [{:keys [count total-ms max-ms] :or {count 0 total-ms 0 max-ms 0}}]
|
|
{:count (inc count)
|
|
:total-ms (+ total-ms elapsed-ms)
|
|
:max-ms (max max-ms elapsed-ms)})))
|
|
|
|
(defn <invoke-db-worker
|
|
"invoke db-worker thread api"
|
|
[qkw & args]
|
|
(let [worker @*db-worker]
|
|
(when (nil? worker)
|
|
(prn :<invoke-db-worker-error qkw)
|
|
(throw (ex-info "db-worker has not been initialized" {})))
|
|
(let [started-at (js/performance.now)]
|
|
(-> (p/let [result (apply worker qkw args)]
|
|
(if (or (instance? ExceptionInfo result)
|
|
(instance? js/Error result))
|
|
(p/rejected result)
|
|
result))
|
|
(p/finally (fn [] (record-invoke-stat! qkw (- (js/performance.now) started-at))))))))
|
|
|
|
(def ^:private export-block-text-indent-styles #{"dashes" "spaces" "no-indent"})
|
|
|
|
(def ^:private legacy-export-block-text-indent-styles
|
|
{"indent-style-dashes" "dashes"
|
|
"indent-style-spaces" "spaces"
|
|
"indent-style-none" "no-indent"})
|
|
|
|
(defn- normalize-export-block-text-indent-style
|
|
[v]
|
|
(cond
|
|
(nil? v) nil
|
|
(contains? export-block-text-indent-styles v) v
|
|
(contains? legacy-export-block-text-indent-styles v) (get legacy-export-block-text-indent-styles v)
|
|
:else (throw (ex-info "Invalid export block text indent style" {:value v}))))
|
|
|
|
;; Initial main application state
|
|
(defonce ^:large-vars/data-var initial-state
|
|
(let [document-mode? (or (storage/get :document/mode?) false)
|
|
current-graph (let [url-graph (:graph (util/parse-params))
|
|
graph (or url-graph
|
|
(graph-tab/get-tab-repo)
|
|
(storage/get :git/current-repo))]
|
|
(when graph (ipc/ipc "setCurrentGraph" graph))
|
|
graph)]
|
|
{:client-id (str (random-uuid))
|
|
:route-match nil
|
|
:today nil
|
|
:instrument/disabled? (storage/get "instrument-disabled")
|
|
;; TODO: how to detect the network reliably?
|
|
;; NOTE: prefer to use flows/network-online?
|
|
:network/online? true
|
|
:me nil
|
|
:git/current-repo current-graph
|
|
:db/restoring? nil
|
|
:search/q ""
|
|
:search/mode nil
|
|
:search/args nil
|
|
:search/result nil
|
|
:search/engines {}
|
|
:search/index-build {:running? false
|
|
:repo nil
|
|
:progress 0
|
|
:processed 0
|
|
:total 0}
|
|
|
|
;; modals
|
|
:modal/dropdowns {}
|
|
:modal/id nil
|
|
:notification/contents {}
|
|
|
|
;; ui
|
|
:ui/viewport {}
|
|
:ui/show-property-dialog? false
|
|
|
|
;; left sidebar
|
|
:ui/navigation-item-collapsed? {}
|
|
:ui/recent-pages (or (storage/get :ui/recent-pages) {})
|
|
|
|
;; right sidebar
|
|
:ui/handbooks-open? false
|
|
:ui/help-open? false
|
|
:ui/fullscreen? false
|
|
:ui/settings-open? false
|
|
:ui/sidebar-open? false
|
|
:ui/sidebar-width "40%"
|
|
:ui/left-sidebar-open? (boolean (storage/get :ls-left-sidebar-open?))
|
|
:ui/theme (or (storage/get :ui/theme) "light")
|
|
:ui/system-theme? ((fnil identity (or util/mac? util/win32? false)) (storage/get :ui/system-theme?))
|
|
:ui/custom-theme (or (storage/get :ui/custom-theme) {:light {:mode "light"} :dark {:mode "dark"}})
|
|
:ui/wide-mode? (storage/get :ui/wide-mode)
|
|
:ui/radix-color (storage/get :ui/radix-color)
|
|
:ui/editor-font (storage/get :ui/editor-font)
|
|
|
|
;; ui/collapsed-blocks is to separate the collapse/expand state from db for:
|
|
;; 1. right sidebar
|
|
;; 2. zoom-in view
|
|
;; 3. queries
|
|
;; 4. references
|
|
;; graph => {container-id {:block-id bool}}
|
|
:ui/collapsed-blocks {}
|
|
:ui/sidebar-collapsed-blocks {}
|
|
:ui/file-component nil
|
|
:ui/developer-mode? (or (= (storage/get "developer-mode") "true")
|
|
false)
|
|
;; remember scroll positions of visited paths
|
|
:ui/paths-scroll-positions {}
|
|
:ui/main-container-scroll-top nil
|
|
:ui/shortcut-tooltip? (if (false? (storage/get :ui/shortcut-tooltip?))
|
|
false
|
|
true)
|
|
:ui/shortcut-handler-refreshing? false
|
|
:ui/scrolling? false
|
|
:ui/show-empty-and-hidden-properties? {:mode :global
|
|
:show? false}
|
|
:document/mode? document-mode?
|
|
|
|
:config {}
|
|
:block/component-editing-mode? false
|
|
:editor/op nil
|
|
:editor/start-pos nil
|
|
:editor/hidden-editors #{} ;; page names
|
|
|
|
:editor/action nil
|
|
:editor/action-data nil
|
|
;; With label or other data
|
|
:editor/last-saved-cursor {}
|
|
:editor/editing? nil
|
|
:editor/in-composition? false
|
|
:editor/code-mode? false
|
|
:editor/content {}
|
|
:editor/block nil
|
|
:editor/set-timestamp-block nil ;; click rendered block timestamp-cp to set timestamp
|
|
:editor/last-input-time {}
|
|
:editor/document-mode? document-mode?
|
|
:editor/args nil
|
|
:editor/on-paste? false
|
|
:editor/last-key-code nil
|
|
:ui/global-last-key-code nil
|
|
:editor/block-op-type nil ;; :cut, :copy
|
|
:editor/block-refs #{}
|
|
|
|
;; Stores deleted refed blocks, indexed by repo
|
|
:editor/last-replace-ref-content-tx nil
|
|
|
|
:editor/code-block-context nil
|
|
:editor/latest-shortcut nil
|
|
|
|
:history/paused? false
|
|
:editor/cursor-range nil
|
|
:editor/container-id nil
|
|
:editor/next-edit-block nil
|
|
:editor/raw-mode-block nil
|
|
:editor/virtualized-scroll-fn nil
|
|
|
|
;; Warning: blocks order is determined when setting this attribute
|
|
:selection/blocks []
|
|
:selection/start-block nil
|
|
;; nil, :up or :down
|
|
;; used to determine selection direction when two or more blocks are selected
|
|
:selection/direction nil
|
|
:selection/selected-all? false
|
|
:custom-context-menu/show? false
|
|
:custom-context-menu/links nil
|
|
:custom-context-menu/position nil
|
|
|
|
;; pages or blocks in the right sidebar
|
|
;; It is a list of `[repo db-id block-type block-data]` 4-tuple
|
|
:sidebar/blocks '()
|
|
|
|
:preferred-language (canonical-preferred-language (storage/get :preferred-language))
|
|
|
|
;; electron
|
|
:electron/auto-updater-downloaded false
|
|
:electron/updater-pending? false
|
|
:electron/updater {}
|
|
:electron/app-base-info nil
|
|
:electron/user-cfgs nil
|
|
:electron/server nil
|
|
:electron/window-maximized? false
|
|
:electron/window-fullscreen? false
|
|
|
|
;; assets
|
|
:assets/alias-enabled? (or (storage/get :assets/alias-enabled?) false)
|
|
:assets/alias-dirs (or (storage/get :assets/alias-dirs) [])
|
|
:assets/asset-file-write-finish {}
|
|
|
|
;; mobile
|
|
:mobile/container-urls nil
|
|
:mobile/show-action-bar? false
|
|
:mobile/show-tabbar? false
|
|
|
|
;; plugin
|
|
:plugin/enabled (and util/plugin-platform?
|
|
;; true false :theme-only
|
|
((fnil identity true) (storage/get ::storage-spec/lsp-core-enabled)))
|
|
:plugin/preferences nil
|
|
:plugin/indicator-text nil
|
|
:plugin/installed-plugins {}
|
|
:plugin/installed-themes []
|
|
:plugin/installed-slash-commands {}
|
|
:plugin/installed-ui-items {}
|
|
:plugin/installed-resources {}
|
|
:plugin/installed-hooks {}
|
|
:plugin/installed-services {}
|
|
:plugin/simple-commands {}
|
|
:plugin/selected-theme nil
|
|
:plugin/selected-unpacked-pkg nil
|
|
:plugin/marketplace-pkgs nil
|
|
:plugin/marketplace-stats nil
|
|
:plugin/installing nil
|
|
:plugin/active-readme nil
|
|
:plugin/updates-auto-checking? false
|
|
:plugin/updates-pending {}
|
|
:plugin/updates-coming {}
|
|
:plugin/updates-downloading? false
|
|
:plugin/updates-unchecked #{}
|
|
:plugin/navs-settings? true
|
|
:plugin/focused-settings nil ;; plugin id
|
|
|
|
;; pdf
|
|
:pdf/system-win? false
|
|
:pdf/current nil
|
|
:pdf/ref-highlight nil
|
|
:pdf/block-highlight-colored? (or (storage/get "ls-pdf-hl-block-is-colored") true)
|
|
:pdf/auto-open-ctx-menu? (not= false (storage/get "ls-pdf-auto-open-ctx-menu"))
|
|
|
|
:copy/export-block-text-indent-style (or (normalize-export-block-text-indent-style
|
|
(storage/get :copy/export-block-text-indent-style))
|
|
"dashes")
|
|
:copy/export-block-text-remove-options (or (storage/get :copy/export-block-text-remove-options)
|
|
#{})
|
|
:copy/export-block-text-other-options (or (storage/get :copy/export-block-text-other-options)
|
|
{})
|
|
:date-picker/date nil
|
|
:page-title/context nil
|
|
:block-ref/context nil
|
|
|
|
:youtube/players {}
|
|
|
|
;; command palette
|
|
:command-palette/commands []
|
|
|
|
:view/components {}
|
|
:view/selected-blocks nil
|
|
|
|
:srs/mode? false
|
|
|
|
:srs/cards-due-count nil
|
|
|
|
:reactive/query-dbs {}
|
|
|
|
;; login, userinfo, token, ...
|
|
:auth/refresh-token (some-> (storage/get "refresh-token") str)
|
|
:auth/access-token nil
|
|
:auth/id-token nil
|
|
:auth/oauth-token-url nil
|
|
:auth/oauth-domain nil
|
|
:auth/oauth-client-id nil
|
|
:auth/current-login-user nil
|
|
|
|
;; graph-uuid -> ...
|
|
:rtc/state {}
|
|
:rtc/editing-users-by-block {}
|
|
:rtc/loading-graphs? nil
|
|
;; only latest rtc-log stored here, derive a log stream from state when needed.
|
|
:rtc/log nil
|
|
:rtc/uploading? false
|
|
:rtc/downloading-graph-uuid nil
|
|
:rtc/graphs []
|
|
:rtc/online-info {}
|
|
:rtc/asset-upload-download-progress {}
|
|
:rtc/users-info {}
|
|
:sync/block-conflicts {}
|
|
|
|
:user/info {:UserGroups (storage/get :user-groups)}
|
|
:encryption/graph-parsing? false
|
|
|
|
:ui/loading? {}
|
|
:ui/container-id 0
|
|
:ui/cached-key->container-id {}
|
|
:feature/enable-sync? (storage/get :logseq-sync-enabled)
|
|
|
|
:ui/find-in-page nil
|
|
:comments/inline-thread nil
|
|
:graph/exporting nil
|
|
:graph/exporting-state nil
|
|
:graph/importing nil
|
|
:graph/importing-state {}
|
|
:graph/loading? nil
|
|
:handbook/route-chan (async/chan (async/sliding-buffer 1))
|
|
|
|
:system/info {}
|
|
;; Whether block is selected
|
|
:ui/select-query-cache {}
|
|
:view/table-selection {}
|
|
:ui/toggle-highlight-recent-blocks? false
|
|
:ui/highlight-recent-days (or (storage/get :ui/highlight-recent-days)
|
|
3)
|
|
:favorites/updated? 0
|
|
:db/async-queries {}
|
|
:sync-graph/init? nil}))
|
|
|
|
(rfx/init-once! {:initial-value initial-state
|
|
:registry (atom {})})
|
|
|
|
(defn register-rfx-state-subs!
|
|
[]
|
|
(rfx/reg-sub! ::state-key
|
|
(fn [db [_ k]]
|
|
(get db k)))
|
|
(doseq [k (keys (rfx/snapshot))]
|
|
(rfx/reg-sub! k
|
|
[[::state-key k]]
|
|
(fn [value [_ & path]]
|
|
(if (seq path)
|
|
(get-in value path)
|
|
value))))
|
|
(rfx/reg-sub! :selection/block-selected?
|
|
[[:selection/blocks]]
|
|
(fn [selection-blocks [_ block-id]]
|
|
(boolean
|
|
(some #(= block-id (some-> % (dom/attr "blockid") uuid))
|
|
selection-blocks))))
|
|
true)
|
|
|
|
(register-rfx-state-subs!)
|
|
|
|
(declare get-state update-state! set-state!)
|
|
(defn clear-async-queries!
|
|
[]
|
|
(set-state! :db/async-queries {}))
|
|
|
|
(defn set-sync-block-conflicts!
|
|
([repo conflicts-by-block]
|
|
(when-not (map? conflicts-by-block)
|
|
(throw (ex-info "Expected sync conflicts grouped by block"
|
|
{:repo repo
|
|
:conflicts conflicts-by-block})))
|
|
(set-state! :sync/block-conflicts
|
|
conflicts-by-block
|
|
:nested-path [repo]))
|
|
([repo block-id conflicts]
|
|
(when-not (vector? conflicts)
|
|
(throw (ex-info "Expected block sync conflicts"
|
|
{:repo repo
|
|
:block-id block-id
|
|
:conflicts conflicts})))
|
|
(set-state! :sync/block-conflicts
|
|
conflicts
|
|
:nested-path [repo (str block-id)])))
|
|
|
|
;; User configuration getters under :config (and sometimes :me)
|
|
;; ========================================
|
|
;; TODO: Refactor default config values to be data driven. Currently they are all
|
|
;; buried in getters
|
|
;; TODO: Refactor our access to be more data driven. Currently each getter
|
|
;; (re-)fetches get-current-repo needlessly
|
|
;; TODO: Add consistent validation. Only a few config options validate at get time
|
|
|
|
(def common-default-config
|
|
"Common default config for a user's repo config"
|
|
{:feature/enable-search-remove-accents? true
|
|
:ui/auto-expand-block-refs? true})
|
|
|
|
(def db-default-config
|
|
"Default repo config for DB graphs"
|
|
(merge common-default-config
|
|
;; The "DOING" query returns tasks with "Doing" status for recent past days
|
|
;; The "TODO" query returns tasks with "Todo" status for upcoming future days
|
|
{:default-queries
|
|
{:journals
|
|
[{:title-key :journal.default-query/doing
|
|
:title-icon "InProgress50"
|
|
:query '[:find (pull ?b [*])
|
|
:in $ ?start ?today
|
|
:where
|
|
(task ?b #{"Doing"})
|
|
[?b :block/page ?p]
|
|
[?p :block/journal-day ?d]
|
|
[(>= ?d ?start)]
|
|
[(<= ?d ?today)]]
|
|
:inputs [:14d :today]
|
|
:collapsed? true}
|
|
{:title-key :journal.default-query/todo
|
|
:title-icon "Todo"
|
|
:query '[:find (pull ?b [*])
|
|
:in $ ?start ?next
|
|
:where
|
|
(task ?b #{"Todo"})
|
|
[?b :block/page ?p]
|
|
[?p :block/journal-day ?d]
|
|
[(> ?d ?start)]
|
|
[(< ?d ?next)]]
|
|
:inputs [:today :7d-after]
|
|
:group-by-page? false
|
|
:collapsed? true}]}
|
|
:ui/hide-empty-properties? false
|
|
:ui/mask-email? true}))
|
|
|
|
;; State that most user config is dependent on
|
|
(declare get-current-repo)
|
|
|
|
(defn merge-configs
|
|
"Merges user configs in given orders. All values are overridden except for maps
|
|
which are merged."
|
|
[& configs]
|
|
(->> configs
|
|
(filter map?)
|
|
(apply merge-with
|
|
(fn merge-config [current new]
|
|
(if (and (map? current) (map? new))
|
|
(merge current new)
|
|
new)))))
|
|
|
|
(defn get-global-config
|
|
[]
|
|
(get-in (rfx/snapshot) [:config ::global-config]))
|
|
|
|
(defn get-global-config-str-content
|
|
[]
|
|
(get-in (rfx/snapshot) [:config ::global-config-str-content]))
|
|
|
|
(defn get-graph-config
|
|
([] (get-graph-config (get-current-repo)))
|
|
([repo-url] (get-in (rfx/snapshot) [:config repo-url])))
|
|
|
|
(defn get-config
|
|
"User config for the given repo or current repo if none given. All config fetching
|
|
should be done through this fn in order to get global config and config defaults"
|
|
([]
|
|
(get-config (get-current-repo)))
|
|
([repo-url]
|
|
(merge-configs
|
|
db-default-config
|
|
(get-global-config)
|
|
(get-graph-config repo-url))))
|
|
|
|
(defn publishing-enable-editing?
|
|
[]
|
|
(and common-config/PUBLISHING (:publishing/enable-editing? (get-config))))
|
|
|
|
(defn enable-editing?
|
|
[]
|
|
(or (not common-config/PUBLISHING) (:publishing/enable-editing? (get-config))))
|
|
|
|
(defonce built-in-macros
|
|
{"img" "[:img.$4 {:src \"$1\" :style {:width $2 :height $3}}]"})
|
|
|
|
(defn get-macros
|
|
[]
|
|
(merge
|
|
built-in-macros
|
|
(:macros (get-config))))
|
|
|
|
(defn set-assets-alias-enabled!
|
|
[v]
|
|
(set-state! :assets/alias-enabled? (boolean v))
|
|
(storage/set :assets/alias-enabled? (boolean v)))
|
|
|
|
(defn set-assets-alias-dirs!
|
|
[dirs]
|
|
(when dirs
|
|
(set-state! :assets/alias-dirs dirs)
|
|
(storage/set :assets/alias-dirs dirs)))
|
|
|
|
(defn get-custom-css-link
|
|
[]
|
|
(:custom-css-url (get-config)))
|
|
|
|
(defn get-custom-js-link
|
|
[]
|
|
(:custom-js-url (get-config)))
|
|
|
|
(defn all-pages-public?
|
|
[]
|
|
(let [value (:publishing/all-pages-public? (get-config))
|
|
value (if (some? value) value (:all-pages-public? (get-config)))]
|
|
(true? value)))
|
|
|
|
(defn get-default-home
|
|
[]
|
|
(:default-home (get-config)))
|
|
|
|
(defn custom-home-page?
|
|
[]
|
|
(some? (:page (get-default-home))))
|
|
|
|
;; TODO: Move or remove as this is no longer stateful
|
|
(defn get-preferred-format
|
|
[& _args]
|
|
:markdown)
|
|
|
|
(defn markdown?
|
|
[]
|
|
(= (keyword (get-preferred-format))
|
|
:markdown))
|
|
|
|
(def ^:private default-date-formatter "MMM do, yyyy")
|
|
|
|
(defn set-date-formatter!
|
|
[repo formatter]
|
|
(when repo
|
|
(set-state! :ui/date-formatter (or formatter default-date-formatter) :nested-path repo)))
|
|
|
|
(defn get-date-formatter
|
|
[]
|
|
(or (when-let [repo (get-current-repo)]
|
|
(get-state :ui/date-formatter :nested-path repo))
|
|
default-date-formatter))
|
|
|
|
(defn custom-shortcuts []
|
|
(merge (try (storage/get :ls-shortcuts)
|
|
(catch :default e
|
|
(prn :shortcut/storage-read-error e)
|
|
nil))
|
|
(:shortcuts (get-config))))
|
|
|
|
(defn get-commands
|
|
[]
|
|
(:commands (get-config)))
|
|
|
|
(defn get-scheduled-future-days
|
|
[]
|
|
(let [days (:scheduled/future-days (get-config))]
|
|
(or (when (int? days) days) 7)))
|
|
|
|
(defn get-start-of-week
|
|
[]
|
|
(or (:start-of-week (get-config))
|
|
(get-in (rfx/snapshot) [:me :settings :start-of-week])
|
|
6))
|
|
|
|
(defn get-ref-open-blocks-level
|
|
[]
|
|
(if-let [value (:ref/default-open-blocks-level (get-config))]
|
|
(if (and (int? value) (>= value 0))
|
|
(min value 9)
|
|
2)
|
|
2))
|
|
|
|
(defn get-export-bullet-indentation
|
|
[]
|
|
(case (get (get-config) :export/bullet-indentation :tab)
|
|
:eight-spaces
|
|
" "
|
|
:four-spaces
|
|
" "
|
|
:two-spaces
|
|
" "
|
|
:tab
|
|
"\t"))
|
|
|
|
(defn enable-search-remove-accents?
|
|
[]
|
|
(:feature/enable-search-remove-accents? (get-config)))
|
|
|
|
(defn enable-semantic-search?
|
|
[]
|
|
(true? (get-in (rfx/snapshot) [:electron/user-cfgs :feature/enable-semantic-search?])))
|
|
|
|
;; State subscription helpers
|
|
;; ==========================
|
|
|
|
(declare document-mode?)
|
|
|
|
(defn- read-state-value
|
|
[db ks nested-path]
|
|
(let [ks-coll? (coll? ks)
|
|
get-fn (if ks-coll? get-in get)
|
|
s (get-fn db ks)
|
|
nested-path-coll? (coll? nested-path)]
|
|
(cond
|
|
(and nested-path nested-path-coll?)
|
|
(get-in s nested-path)
|
|
|
|
nested-path
|
|
(get s nested-path)
|
|
|
|
:else s)))
|
|
|
|
(defn get-state
|
|
"Returns a plain state value.
|
|
|
|
Use this outside component rendering or when the caller does not need reactive
|
|
rendering. Components that render from state should use `frontend.rfx/use-sub`."
|
|
([] (rfx/snapshot))
|
|
([ks & {:keys [nested-path]}]
|
|
(read-state-value (rfx/snapshot) ks nested-path)))
|
|
|
|
(defn set-editing-block-id!
|
|
[container-block]
|
|
(set-state! :editor/editing? {container-block true}))
|
|
|
|
(defn config-for-repo
|
|
[config repo]
|
|
(merge-configs db-default-config
|
|
(get config ::global-config)
|
|
(get config repo)))
|
|
|
|
(defn enable-grammarly?
|
|
[]
|
|
(true? (:feature/enable-grammarly? (get-config))))
|
|
|
|
(defn scheduled-deadlines-disabled?
|
|
[]
|
|
(true? (:feature/disable-scheduled-and-deadline-query? (get-config))))
|
|
|
|
(defn enable-fold-button-right?
|
|
[]
|
|
(and (util/mobile?)
|
|
(util/sm-breakpoint?)))
|
|
|
|
(defn use-enable-fold-button-right?
|
|
[]
|
|
(let [_ (rfx/use-sub [:ui/viewport])]
|
|
(enable-fold-button-right?)))
|
|
|
|
(defn enable-flashcards?
|
|
([]
|
|
(enable-flashcards? (get-current-repo)))
|
|
([repo]
|
|
(not (false? (:feature/enable-flashcards? (get-config repo))))))
|
|
|
|
;; Enable by default
|
|
(defn show-brackets?
|
|
[]
|
|
(not (false? (:ui/show-brackets? (get-config)))))
|
|
|
|
(defn- get-selected-block-ids
|
|
[blocks]
|
|
(->> blocks
|
|
(remove nil?)
|
|
(keep #(when-let [id (dom/attr % "blockid")]
|
|
(uuid id)))
|
|
(distinct)))
|
|
|
|
(defn block-content-max-length
|
|
[repo]
|
|
(or (:block/title-max-length (get-config repo))
|
|
;; backward compatible
|
|
(:block/content-max-length (get-config repo))
|
|
10000))
|
|
|
|
(defn mobile?
|
|
[]
|
|
(or (util/mobile?) (mobile-util/native-platform?)))
|
|
|
|
(defn enable-tooltip?
|
|
[]
|
|
(if (mobile?)
|
|
false
|
|
(get (get-config) :ui/enable-tooltip? true)))
|
|
|
|
(defn show-command-doc?
|
|
[]
|
|
(get (get-config) :ui/show-command-doc? true))
|
|
|
|
(defn logical-outdenting?
|
|
[]
|
|
(:editor/logical-outdenting? (get-config)))
|
|
|
|
(defn show-full-blocks?
|
|
[]
|
|
(:ui/show-full-blocks? (get-config)))
|
|
|
|
(defn preferred-pasting-file?
|
|
[]
|
|
(:editor/preferred-pasting-file? (get-config)))
|
|
|
|
(defn auto-expand-block-refs?
|
|
[]
|
|
(:ui/auto-expand-block-refs? (get-config)))
|
|
|
|
(defn doc-mode-enter-for-new-line?
|
|
[]
|
|
(and (document-mode?)
|
|
(not (:shortcut/doc-mode-enter-for-new-block? (get-config)))))
|
|
|
|
(defn user-groups
|
|
[]
|
|
(set (get-state [:user/info :UserGroups])))
|
|
|
|
;; State mutation helpers
|
|
;; ======================
|
|
|
|
(defn- assoc-state-db
|
|
[db path value nested-path]
|
|
(let [path-coll? (coll? path)
|
|
nested-path-coll? (coll? nested-path)]
|
|
(cond
|
|
(and nested-path nested-path-coll?)
|
|
(if path-coll?
|
|
(assoc-in db (into (vec path) nested-path) value)
|
|
(assoc-in db (into [path] nested-path) value))
|
|
|
|
nested-path
|
|
(if path-coll?
|
|
(assoc-in db (conj (vec path) nested-path) value)
|
|
(assoc-in db [path nested-path] value))
|
|
|
|
path-coll?
|
|
(assoc-in db path value)
|
|
|
|
:else
|
|
(assoc db path value))))
|
|
|
|
(defn- full-state-path
|
|
[path nested-path]
|
|
(let [base-path (if (coll? path) (vec path) [path])]
|
|
(cond
|
|
(and nested-path (coll? nested-path))
|
|
(into base-path nested-path)
|
|
|
|
nested-path
|
|
(conj base-path nested-path)
|
|
|
|
:else
|
|
base-path)))
|
|
|
|
(defn- update-state-db
|
|
[db path f nested-path]
|
|
(let [path-coll? (coll? path)
|
|
nested-path-coll? (coll? nested-path)]
|
|
(cond
|
|
(and nested-path nested-path-coll?)
|
|
(if path-coll?
|
|
(update-in db (into (vec path) nested-path) f)
|
|
(update-in db (into [path] nested-path) f))
|
|
|
|
nested-path
|
|
(if path-coll?
|
|
(update-in db (conj (vec path) nested-path) f)
|
|
(update-in db [path nested-path] f))
|
|
|
|
path-coll?
|
|
(update-in db path f)
|
|
|
|
:else
|
|
(update db path f))))
|
|
|
|
(defn set-state!
|
|
[path value & {:keys [nested-path changed-paths]}]
|
|
(vswap! *profile-state update path inc)
|
|
(let [old-v (read-state-value (rfx/snapshot) path nested-path)]
|
|
(when (not= old-v value)
|
|
(let [db' (assoc-state-db (rfx/snapshot) path value nested-path)]
|
|
(if (seq changed-paths)
|
|
(rfx/replace-state-paths! db' changed-paths)
|
|
(rfx/replace-state! db' (full-state-path path nested-path))))))
|
|
nil)
|
|
|
|
(defn update-state!
|
|
[path f & {:keys [nested-path]}]
|
|
(vswap! *profile-state update path inc)
|
|
(let [db' (update-state-db (rfx/snapshot) path f nested-path)]
|
|
(rfx/replace-state! db' (full-state-path path nested-path)))
|
|
nil)
|
|
|
|
(defn replace-state!
|
|
[db]
|
|
(rfx/replace-state! db)
|
|
nil)
|
|
|
|
(defn swap-state!
|
|
[f & args]
|
|
(replace-state! (apply f (rfx/snapshot) args)))
|
|
|
|
;; State getters and setters
|
|
;; =========================
|
|
;; These fns handle any key except :config.
|
|
;; Some state is also stored in local storage and/or sent to electron's main process
|
|
|
|
(defn get-route-match
|
|
[]
|
|
(:route-match (rfx/snapshot)))
|
|
|
|
(defn get-current-route
|
|
[]
|
|
(get-in (get-route-match) [:data :name]))
|
|
|
|
(defn home?
|
|
[]
|
|
(= :home (get-current-route)))
|
|
|
|
(defn get-current-page
|
|
[]
|
|
(when (= :page (get-current-route))
|
|
(get-in (get-route-match)
|
|
[:path-params :name])))
|
|
|
|
(defn route-has-p?
|
|
[]
|
|
(get-in (get-route-match) [:query-params :p]))
|
|
|
|
(defn get-current-repo
|
|
"Returns the current repo URL, or else open demo graph"
|
|
[]
|
|
(:git/current-repo (rfx/snapshot)))
|
|
|
|
(defn get-rtc-graphs
|
|
[]
|
|
(:rtc/graphs (rfx/snapshot)))
|
|
|
|
;; TODO: rtc version
|
|
(comment
|
|
(defn get-remote-graph-usage
|
|
[graphs]
|
|
(->> graphs
|
|
(map #(hash-map :uuid (:GraphUUID %)
|
|
:name (:GraphName %)
|
|
:used-gbs (/ (:GraphStorageUsage %) 1024 1024 1024)
|
|
:limit-gbs (/ (:GraphStorageLimit %) 1024 1024 1024)
|
|
:used-percent (/ (:GraphStorageUsage %) (:GraphStorageLimit %) 0.01)))
|
|
(map #(assoc % :free-gbs (- (:limit-gbs %) (:used-gbs %))))
|
|
(vec))))
|
|
|
|
(defn get-repos
|
|
[]
|
|
(get-in (rfx/snapshot) [:me :repos]))
|
|
|
|
(defn set-repos!
|
|
[repos]
|
|
(set-state! [:me :repos] (distinct repos)))
|
|
|
|
(defn add-repo!
|
|
[repo]
|
|
(when (not (string/blank? repo))
|
|
(update-state! [:me :repos]
|
|
(fn [repos]
|
|
(->> (conj repos repo)
|
|
(distinct))))))
|
|
|
|
(defn set-current-repo!
|
|
[repo]
|
|
(set-state! :git/current-repo repo)
|
|
(if repo
|
|
(storage/set :git/current-repo repo)
|
|
(storage/remove :git/current-repo))
|
|
(ipc/ipc "setCurrentGraph" repo))
|
|
|
|
(defn set-preferred-language!
|
|
[language]
|
|
(let [old-language (:preferred-language (rfx/snapshot))
|
|
new-language (canonical-preferred-language language)]
|
|
(when (not= new-language old-language)
|
|
(set-state! :preferred-language new-language)
|
|
(storage/set :preferred-language new-language)
|
|
(electron-locale/push-locale! new-language))))
|
|
|
|
(defn delete-repo!
|
|
[repo]
|
|
(swap-state!
|
|
(fn [db]
|
|
(-> db
|
|
(update-in [:me :repos]
|
|
(fn [repos]
|
|
(->> (remove #(or (= (:url repo) (:url %))
|
|
(and
|
|
(:GraphUUID repo)
|
|
(:GraphUUID %)
|
|
(= (:GraphUUID repo) (:GraphUUID %)))) repos)
|
|
(util/distinct-by :url))))
|
|
(update :sync/block-conflicts dissoc (:url repo))))))
|
|
|
|
(defn set-timestamp-block!
|
|
[value]
|
|
(set-state! :editor/set-timestamp-block value))
|
|
|
|
(defn get-timestamp-block
|
|
[]
|
|
(get-state :editor/set-timestamp-block))
|
|
|
|
(defn get-edit-block
|
|
[]
|
|
(get-state :editor/block))
|
|
|
|
(defn editing?
|
|
[]
|
|
(seq (get-state :editor/editing?)))
|
|
|
|
(defn get-edit-input-id
|
|
[]
|
|
(when-not (exists? js/process)
|
|
(when (editing?)
|
|
(try
|
|
(when-let [elem (or (when-let [id (:block/uuid (get-edit-block))]
|
|
(gdom/getElement (str "edit-block-" id)))
|
|
js/document.activeElement)]
|
|
(when (util/input? elem)
|
|
(let [id (gobj/get elem "id")]
|
|
(when (string/starts-with? id "edit-block-")
|
|
id))))
|
|
(catch :default _e)))))
|
|
|
|
(defn set-edit-content!
|
|
([value] (set-edit-content! (get-edit-input-id) value))
|
|
([input-id value] (set-edit-content! input-id value true))
|
|
([input-id value set-input-value?] (set-edit-content! input-id value set-input-value? nil))
|
|
([input-id value set-input-value? caret-pos]
|
|
(when input-id
|
|
(when set-input-value?
|
|
(when-let [input (gdom/getElement input-id)]
|
|
(util/set-change-value input value caret-pos)))
|
|
(set-state! :editor/content value :nested-path
|
|
(or (:block/uuid (get-edit-block)) input-id)))))
|
|
|
|
(defn get-input
|
|
[]
|
|
(when-let [id (get-edit-input-id)]
|
|
(gdom/getElement id)))
|
|
|
|
(defn get-edit-content
|
|
[]
|
|
(when-let [id (:block/uuid (get-edit-block))]
|
|
(get (get-state :editor/content) id)))
|
|
|
|
(defn set-selection-start-block!
|
|
[start-block]
|
|
(set-state! :selection/start-block start-block))
|
|
|
|
(defn get-selection-start-block
|
|
[]
|
|
(or (get-state :selection/start-block)
|
|
(when-let [edit-block (get-edit-block)]
|
|
(let [node (util/rec-get-node edit-block "ls-block")]
|
|
(set-selection-start-block! node)
|
|
node))))
|
|
|
|
(defn get-cursor-range
|
|
[]
|
|
(get-state :editor/cursor-range))
|
|
|
|
(defn set-cursor-range!
|
|
[range]
|
|
(set-state! :editor/cursor-range range))
|
|
|
|
(defn set-search-mode!
|
|
([value] (set-search-mode! value nil))
|
|
([value args]
|
|
(set-state! :search/mode value)
|
|
(set-state! :search/args args)))
|
|
|
|
(defn set-editor-action!
|
|
[value]
|
|
(set-state! :editor/action value))
|
|
|
|
(defn set-editor-action-data!
|
|
[value]
|
|
(set-state! :editor/action-data value))
|
|
|
|
(defn get-editor-action
|
|
[]
|
|
(get-state :editor/action))
|
|
|
|
(defn get-editor-action-data
|
|
[]
|
|
(get-state :editor/action-data))
|
|
|
|
(defn get-editor-show-page-search?
|
|
[]
|
|
(= (get-editor-action) :page-search))
|
|
|
|
(defn get-editor-show-page-search-hashtag?
|
|
[]
|
|
(= (get-editor-action) :page-search-hashtag))
|
|
|
|
(defn get-editor-show-block-search?
|
|
[]
|
|
(= (get-editor-action) :block-search))
|
|
|
|
(defn set-editor-show-input!
|
|
[value]
|
|
(if value
|
|
(do
|
|
(set-editor-action-data! (assoc (get-editor-action-data) :options value))
|
|
(set-editor-action! :input))
|
|
(do
|
|
(set-editor-action! nil)
|
|
(set-editor-action-data! nil))))
|
|
|
|
(defn get-editor-show-input
|
|
[]
|
|
(when (= (get-editor-action) :input)
|
|
(get-state :editor/action-data)))
|
|
|
|
(defn set-editor-show-commands!
|
|
[]
|
|
(when-not (get-editor-action) (set-editor-action! :commands)))
|
|
|
|
(defn clear-editor-action!
|
|
[]
|
|
(set-state! :editor/action nil))
|
|
|
|
(defn get-edit-pos
|
|
[]
|
|
(when-let [input (get-input)]
|
|
(util/get-selection-start input)))
|
|
|
|
(defn get-selection-direction
|
|
[]
|
|
(get-state :selection/direction))
|
|
|
|
(defn get-unsorted-selection-blocks
|
|
[]
|
|
(get-state :selection/blocks))
|
|
|
|
(defn get-selection-blocks
|
|
[]
|
|
(let [result (get-unsorted-selection-blocks)
|
|
direction (get-selection-direction)]
|
|
(if (= direction :up)
|
|
(vec (reverse result))
|
|
result)))
|
|
|
|
(defn get-selection-block-ids
|
|
[]
|
|
(get-selected-block-ids (get-selection-blocks)))
|
|
|
|
(defn dom-clear-selection!
|
|
[]
|
|
(doseq [node (dom/by-class "selected")]
|
|
(dom/remove-class! node "selected")))
|
|
|
|
(defn mark-dom-blocks-as-selected
|
|
[nodes]
|
|
(doseq [node nodes]
|
|
(dom/add-class! node "selected")
|
|
(when (dom/has-class? node "ls-table-row")
|
|
(.focus node))))
|
|
|
|
(defn pub-event!
|
|
{:malli/schema [:=> [:cat vector?] :any]}
|
|
[payload]
|
|
(rfx/pub-event! payload))
|
|
|
|
(defn- unselect-node
|
|
[node]
|
|
(dom/remove-class! node "selected")
|
|
(when (dom/has-class? node "ls-table-row")
|
|
(.blur node)))
|
|
|
|
(defn- set-selection-blocks-aux!
|
|
[blocks]
|
|
(set-state! :view/selected-blocks nil)
|
|
(let [selected-blocks (get-state :selection/blocks)
|
|
selected-ids (set (get-selected-block-ids selected-blocks))
|
|
_ (set-state! :selection/blocks blocks)
|
|
new-ids (set (get-selection-block-ids))
|
|
removed (set/difference selected-ids new-ids)
|
|
next-blocks (set (remove nil? blocks))
|
|
removed-nodes-without-blockid (->> selected-blocks
|
|
(remove nil?)
|
|
(remove #(contains? next-blocks %))
|
|
(remove #(dom/attr % "blockid")))]
|
|
(mark-dom-blocks-as-selected blocks)
|
|
(doseq [id removed]
|
|
(doseq [node (dom/sel (util/format "[blockid='%s']" id))]
|
|
(unselect-node node)))
|
|
(doseq [node removed-nodes-without-blockid]
|
|
(unselect-node node))))
|
|
|
|
(defn set-selection-blocks!
|
|
([blocks]
|
|
(set-selection-blocks! blocks nil))
|
|
([blocks direction]
|
|
(when (seq blocks)
|
|
(let [blocks (->> blocks
|
|
(remove nil?)
|
|
(remove (fn [block]
|
|
(let [class-list (some-> block .-classList)]
|
|
(and class-list
|
|
(.contains class-list "line-through")
|
|
(.contains class-list "opacity-70")))))
|
|
vec)]
|
|
(set-selection-blocks-aux! blocks)
|
|
(when direction (set-state! :selection/direction direction))))))
|
|
|
|
(defn state-clear-selection!
|
|
[]
|
|
(set-state! :selection/blocks nil)
|
|
(set-state! :selection/direction nil)
|
|
(set-state! :selection/start-block nil)
|
|
(set-state! :selection/selected-all? false)
|
|
(pub-event! [:editor/hide-action-bar]))
|
|
|
|
(defn clear-selection!
|
|
[]
|
|
(dom-clear-selection!)
|
|
(state-clear-selection!))
|
|
|
|
(defn get-selection-start-block-or-first
|
|
[]
|
|
(or (get-selection-start-block)
|
|
(some-> (first (get-selection-blocks))
|
|
(gobj/get "id"))))
|
|
|
|
(defn selection?
|
|
"True sense of selection mode with valid selected block"
|
|
[]
|
|
(seq (get-selection-blocks)))
|
|
|
|
(defn conj-selection-block!
|
|
([block-or-blocks]
|
|
(conj-selection-block! block-or-blocks (get-selection-direction)))
|
|
([block-or-blocks direction]
|
|
(let [selection-blocks (get-unsorted-selection-blocks)
|
|
block-or-blocks (if (sequential? block-or-blocks) block-or-blocks [block-or-blocks])
|
|
blocks (-> (concat selection-blocks block-or-blocks)
|
|
distinct)]
|
|
(set-selection-blocks! blocks direction))))
|
|
|
|
(defn drop-selection-block!
|
|
[block]
|
|
(set-selection-blocks-aux! (-> (remove #(= (.-id block) (.-id %)) (get-unsorted-selection-blocks))
|
|
vec)))
|
|
|
|
(defn drop-selection-blocks-starts-with!
|
|
[block]
|
|
(let [blocks (get-unsorted-selection-blocks)
|
|
blocks' (-> (take-while (fn [b] (not= (.-id b) (.-id block))) blocks)
|
|
vec
|
|
(conj block))]
|
|
(set-selection-blocks-aux! blocks')))
|
|
|
|
(defn drop-last-selection-block!
|
|
[]
|
|
(let [blocks (get-state :selection/blocks)
|
|
blocks' (vec (butlast blocks))]
|
|
(set-selection-blocks-aux! blocks')
|
|
(last blocks)))
|
|
|
|
(defn hide-custom-context-menu!
|
|
[]
|
|
(swap-state! assoc
|
|
:custom-context-menu/show? false
|
|
:custom-context-menu/links nil
|
|
:custom-context-menu/position nil))
|
|
|
|
(defn toggle-navigation-item-collapsed!
|
|
[item]
|
|
(update-state! [:ui/navigation-item-collapsed? item] not))
|
|
|
|
(declare sidebar-add-block!)
|
|
(defn- sidebar-add-content-when-open!
|
|
[]
|
|
(when (empty? (:sidebar/blocks (rfx/snapshot)))
|
|
(sidebar-add-block! (get-current-repo) "contents" :contents)))
|
|
|
|
(defn open-right-sidebar!
|
|
[]
|
|
(sidebar-add-content-when-open!)
|
|
(swap-state! assoc :ui/sidebar-open? true))
|
|
|
|
(defn hide-right-sidebar!
|
|
[]
|
|
(swap-state! assoc :ui/sidebar-open? false))
|
|
|
|
(defn toggle-sidebar-open?!
|
|
[]
|
|
(if (:ui/sidebar-open? (rfx/snapshot))
|
|
(hide-right-sidebar!)
|
|
(open-right-sidebar!)))
|
|
|
|
(defn sidebar-move-block!
|
|
[from to]
|
|
(update-state! :sidebar/blocks (fn [blocks]
|
|
(let [to (if (> from to) (inc to) to)]
|
|
(if (not= to from)
|
|
(let [item (nth blocks from)
|
|
blocks (keep-indexed #(when (not= %1 from) %2) blocks)
|
|
[l r] (split-at to blocks)]
|
|
(concat l [item] r))
|
|
blocks)))))
|
|
|
|
(defn sidebar-remove-block!
|
|
[idx]
|
|
(update-state! :sidebar/blocks (fn [blocks]
|
|
(if (string? idx)
|
|
(remove #(= (second %) idx) blocks)
|
|
(util/drop-nth idx blocks))))
|
|
(when (empty? (:sidebar/blocks (rfx/snapshot)))
|
|
(hide-right-sidebar!)))
|
|
|
|
(defn sidebar-remove-deleted-block!
|
|
[ids]
|
|
(let [ids-set (set ids)]
|
|
(update-state! :sidebar/blocks (fn [items]
|
|
(remove (fn [[repo id _]]
|
|
(and (= repo (get-current-repo)) (contains? ids-set id))) items)))
|
|
(when (empty? (:sidebar/blocks (rfx/snapshot)))
|
|
(hide-right-sidebar!))))
|
|
|
|
(defn sidebar-remove-rest!
|
|
[db-id]
|
|
(update-state! :sidebar/blocks (fn [blocks]
|
|
(remove #(not= (second %) db-id) blocks)))
|
|
(set-state! [:ui/sidebar-collapsed-blocks db-id] false))
|
|
|
|
(defn sidebar-replace-block!
|
|
[old-sidebar-key new-sidebar-key]
|
|
(update-state! :sidebar/blocks (fn [blocks]
|
|
(map #(if (= % old-sidebar-key)
|
|
new-sidebar-key
|
|
%) blocks))))
|
|
|
|
(defn sidebar-block-exists?
|
|
[idx]
|
|
(some #(= (second %) idx) (:sidebar/blocks (rfx/snapshot))))
|
|
|
|
(defn clear-sidebar-blocks!
|
|
[]
|
|
(set-state! :sidebar/blocks '()))
|
|
|
|
(defn sidebar-block-toggle-collapse!
|
|
[db-id]
|
|
(when db-id
|
|
(update-state! [:ui/sidebar-collapsed-blocks db-id] not)))
|
|
|
|
(defn sidebar-block-collapse-rest!
|
|
[db-id]
|
|
(let [items (disj (set (map second (:sidebar/blocks (rfx/snapshot)))) db-id)]
|
|
(doseq [item items] (set-state! [:ui/sidebar-collapsed-blocks item] true))))
|
|
|
|
(defn sidebar-block-set-collapsed-all!
|
|
[collapsed?]
|
|
(let [items (map second (:sidebar/blocks (rfx/snapshot)))]
|
|
(doseq [item items]
|
|
(set-state! [:ui/sidebar-collapsed-blocks item] collapsed?))))
|
|
|
|
(defn clear-edit!
|
|
[& {:keys [clear-editing-block?]
|
|
:or {clear-editing-block? true}}]
|
|
(let [online-users (some-> (get-state :rtc/state) :online-users)]
|
|
(swap-state!
|
|
(fn [db]
|
|
(cond-> (assoc db
|
|
:editor/action nil
|
|
:editor/args nil
|
|
:editor/start-pos nil
|
|
:editor/last-saved-cursor {}
|
|
:editor/cursor-range nil
|
|
:ui/select-query-cache {}
|
|
:editor/block-refs #{}
|
|
:editor/action-data nil
|
|
:view/selected-blocks nil)
|
|
clear-editing-block?
|
|
(assoc :editor/editing? nil
|
|
:editor/block nil
|
|
:editor/content {}))))
|
|
(when (and clear-editing-block?
|
|
(coll? online-users)
|
|
(> (count online-users) 1))
|
|
(pub-event! [:rtc/presence-update {:editing-block-uuid nil}])))
|
|
nil)
|
|
|
|
(defn set-editor-last-pos!
|
|
[new-pos]
|
|
(update-state! :editor/last-saved-cursor
|
|
(fn [m] (assoc m (:block/uuid (get-edit-block)) new-pos))))
|
|
|
|
(defn get-editor-last-pos
|
|
[]
|
|
(get (get-state :editor/last-saved-cursor) (:block/uuid (get-edit-block))))
|
|
|
|
(defn set-block-content-and-last-pos!
|
|
[edit-input-id content new-pos]
|
|
(when edit-input-id
|
|
(set-edit-content! edit-input-id content true new-pos)
|
|
(set-editor-last-pos! new-pos)))
|
|
|
|
(defn set-theme-mode!
|
|
([mode] (set-theme-mode! mode (:ui/system-theme? (rfx/snapshot))))
|
|
([mode system-theme?]
|
|
(when (mobile-util/native-platform?)
|
|
(if (= mode "light")
|
|
(util/set-theme-light)
|
|
(util/set-theme-dark)))
|
|
(when (mobile-util/native-platform?)
|
|
(mobile-util/set-native-interface-style! mode system-theme?))
|
|
(set-state! :ui/theme mode)
|
|
(storage/set :ui/theme mode)))
|
|
|
|
(defn sync-system-theme!
|
|
[]
|
|
(when (:ui/system-theme? (rfx/snapshot))
|
|
(let [system-dark? (.-matches (js/window.matchMedia "(prefers-color-scheme: dark)"))]
|
|
(set-theme-mode! (if system-dark? "dark" "light") true)
|
|
(set-state! :ui/system-theme? true)
|
|
(storage/set :ui/system-theme? true))))
|
|
|
|
(defn use-theme-mode!
|
|
[theme-mode]
|
|
(if (= theme-mode "system")
|
|
(do
|
|
(set-state! :ui/system-theme? true)
|
|
(storage/set :ui/system-theme? true)
|
|
(sync-system-theme!))
|
|
(do
|
|
(set-state! :ui/system-theme? false)
|
|
(storage/set :ui/system-theme? false)
|
|
(set-theme-mode! theme-mode false))))
|
|
|
|
(defn- toggle-theme
|
|
[theme]
|
|
(if (= theme "dark") "light" "dark"))
|
|
|
|
(defn toggle-theme!
|
|
[]
|
|
(use-theme-mode! (toggle-theme (:ui/theme (rfx/snapshot)))))
|
|
|
|
(defn set-custom-theme!
|
|
([custom-theme]
|
|
(set-custom-theme! nil custom-theme))
|
|
([mode theme]
|
|
(set-state! (if mode [:ui/custom-theme (keyword mode)] :ui/custom-theme) theme)
|
|
(storage/set :ui/custom-theme (:ui/custom-theme (rfx/snapshot)))))
|
|
|
|
(defn restore-mobile-theme!
|
|
"Restore mobile theme setting from local storage"
|
|
[]
|
|
(let [mode (or (storage/get :ui/theme) "light")
|
|
system-theme? (storage/get :ui/system-theme?)]
|
|
(when (mobile-util/native-platform?)
|
|
(mobile-util/set-native-interface-style! mode system-theme?))
|
|
(when (and (not system-theme?)
|
|
(mobile-util/native-platform?))
|
|
(if (= mode "light")
|
|
(util/set-theme-light)
|
|
(util/set-theme-dark)))))
|
|
|
|
(defn load-app-user-cfgs
|
|
([] (load-app-user-cfgs false))
|
|
([refresh?]
|
|
(when (util/electron?)
|
|
(p/let [cfgs (if (or refresh? (nil? (:electron/user-cfgs (rfx/snapshot))))
|
|
(ipc/ipc :userAppCfgs)
|
|
(:electron/user-cfgs (rfx/snapshot)))
|
|
cfgs (if (object? cfgs) (bean/->clj cfgs) cfgs)]
|
|
(set-state! :electron/user-cfgs cfgs)))))
|
|
|
|
(defn load-electron-app-base-info
|
|
[]
|
|
(when (util/electron?)
|
|
(p/let [info (ipc/ipc :getAppBaseInfo)
|
|
info (if (object? info) (bean/->clj info) info)]
|
|
(set-state! :electron/app-base-info info))))
|
|
|
|
(defn setup-electron-updater!
|
|
[]
|
|
(when (util/electron?)
|
|
(js/window.apis.setUpdatesCallback
|
|
(fn [_ args]
|
|
(let [data (bean/->clj args)
|
|
pending? (not= (:type data) "completed")]
|
|
(set-state! :electron/updater-pending? pending?)
|
|
(when pending? (set-state! :electron/updater data))
|
|
nil)))))
|
|
|
|
(defn clear-file-component!
|
|
[]
|
|
(set-state! :ui/file-component nil))
|
|
|
|
(defn save-scroll-position!
|
|
([value]
|
|
(save-scroll-position! value js/window.location.hash))
|
|
([value path]
|
|
(set-state! :ui/paths-scroll-positions value :nested-path path)))
|
|
|
|
(defn save-main-container-position!
|
|
[value]
|
|
(when (not= value (get-state :ui/main-container-scroll-top))
|
|
(set-state! :ui/main-container-scroll-top value)))
|
|
|
|
(defn get-saved-scroll-position
|
|
([]
|
|
(get-saved-scroll-position js/window.location.hash))
|
|
([path]
|
|
(get (get-state :ui/paths-scroll-positions) path 0)))
|
|
|
|
(defn set-today!
|
|
[value]
|
|
(set-state! :today value))
|
|
|
|
(defn get-me
|
|
[]
|
|
(:me (rfx/snapshot)))
|
|
|
|
(defn set-db-restoring!
|
|
[value]
|
|
(set-state! :db/restoring? value))
|
|
|
|
(defn dialog-opened?
|
|
[]
|
|
(shui-dialog/has-dialog?))
|
|
|
|
(defn close-dialog! []
|
|
(shui/dialog-close!))
|
|
|
|
(defn get-left-sidebar-open?
|
|
[]
|
|
(get-in (rfx/snapshot) [:ui/left-sidebar-open?]))
|
|
|
|
(defn set-left-sidebar-open!
|
|
[value]
|
|
(storage/set "ls-left-sidebar-open?" (boolean value))
|
|
(set-state! :ui/left-sidebar-open? value))
|
|
|
|
(defn toggle-left-sidebar!
|
|
[]
|
|
(set-left-sidebar-open!
|
|
(not (get-left-sidebar-open?))))
|
|
|
|
(defn set-developer-mode!
|
|
[value]
|
|
(set-state! :ui/developer-mode? value)
|
|
(storage/set "developer-mode" (str value)))
|
|
|
|
(defn developer-mode?
|
|
[]
|
|
(:ui/developer-mode? (rfx/snapshot)))
|
|
|
|
(defn document-mode?
|
|
[]
|
|
(get-state :document/mode?))
|
|
|
|
(defn toggle-document-mode!
|
|
[]
|
|
(let [mode (document-mode?)]
|
|
(set-state! :document/mode? (not mode))
|
|
(storage/set :document/mode? (not mode))))
|
|
|
|
(defn toggle-highlight-recent-blocks!
|
|
[]
|
|
(let [value (get-state :ui/toggle-highlight-recent-blocks?)]
|
|
(set-state! :ui/toggle-highlight-recent-blocks? (not value))))
|
|
|
|
(defn shortcut-tooltip-enabled?
|
|
[]
|
|
(get-state :ui/shortcut-tooltip?))
|
|
|
|
(defn toggle-shortcut-tooltip!
|
|
[]
|
|
(let [mode (shortcut-tooltip-enabled?)]
|
|
(set-state! :ui/shortcut-tooltip? (not mode))
|
|
(storage/set :ui/shortcut-tooltip? (not mode))))
|
|
|
|
(defn set-config!
|
|
[repo-url value]
|
|
(when value (set-state! [:config repo-url] value)))
|
|
|
|
(defn set-global-config!
|
|
[value str-content]
|
|
;; Placed under :config so cursors can work seamlessly
|
|
(when value
|
|
(set-config! ::global-config value)
|
|
(set-config! ::global-config-str-content str-content)))
|
|
|
|
(defn get-wide-mode?
|
|
[]
|
|
(:ui/wide-mode? (rfx/snapshot)))
|
|
|
|
(defn toggle-wide-mode!
|
|
[]
|
|
(update-state! :ui/wide-mode? not))
|
|
|
|
(defn set-online!
|
|
[value]
|
|
(set-state! :network/online? value))
|
|
|
|
(defn get-plugins-slash-commands
|
|
[]
|
|
(mapcat seq (flatten (vals (:plugin/installed-slash-commands (rfx/snapshot))))))
|
|
|
|
(defn get-plugins-commands-with-type
|
|
[type]
|
|
(->> (apply concat (vals (:plugin/simple-commands (rfx/snapshot))))
|
|
(filterv #(= (keyword (first %)) (keyword type)))))
|
|
|
|
(defn get-plugins-ui-items-with-type
|
|
[type]
|
|
(->> (apply concat (vals (:plugin/installed-ui-items (rfx/snapshot))))
|
|
(filterv #(= (keyword (first %)) (keyword type)))))
|
|
|
|
(defn get-plugin-resources-with-type
|
|
[pid type]
|
|
(when-let [pid (and type (keyword pid))]
|
|
(get-in (rfx/snapshot) [:plugin/installed-resources pid (keyword type)])))
|
|
|
|
(defn get-plugin-resource
|
|
[pid type key]
|
|
(when-let [resources (get-plugin-resources-with-type pid type)]
|
|
(some->> key (keyword) (get resources))))
|
|
|
|
(defn upt-plugin-resource
|
|
[pid type key attr val]
|
|
(when-let [resource (get-plugin-resource pid type key)]
|
|
(let [resource (assoc resource (keyword attr) val)]
|
|
(set-state!
|
|
[:plugin/installed-resources (keyword pid) (keyword type) key] resource)
|
|
resource)))
|
|
|
|
(defn get-plugin-services
|
|
[pid type]
|
|
(when-let [installed (and pid (:plugin/installed-services (rfx/snapshot)))]
|
|
(some->> (seq (get installed (keyword pid)))
|
|
(filterv #(= type (:type %))))))
|
|
|
|
(defn install-plugin-service
|
|
([pid type name] (install-plugin-service pid type name nil))
|
|
([pid type name opts]
|
|
(when-let [pid (and pid type name (keyword pid))]
|
|
(let [exists (get-plugin-services pid type)]
|
|
(when-let [service (and (or (not exists) (not (some #(= name (:name %)) exists)))
|
|
{:pid pid :type type :name name :opts opts})]
|
|
(update-state! [:plugin/installed-services pid] #(conj (vec %) service))
|
|
|
|
;; search engines state for results
|
|
(when (= type :search)
|
|
(set-state! [:search/engines (str pid name)] service)))))))
|
|
|
|
(defn uninstall-plugin-service
|
|
[pid type-or-all]
|
|
(when-let [pid (keyword pid)]
|
|
(when-let [installed (get (:plugin/installed-services (rfx/snapshot)) pid)]
|
|
(let [remove-all? (or (true? type-or-all) (nil? type-or-all))
|
|
remains (if remove-all? nil (filterv #(not= type-or-all (:type %)) installed))
|
|
removed (if remove-all? installed (filterv #(= type-or-all (:type %)) installed))]
|
|
(set-state! [:plugin/installed-services pid] remains)
|
|
|
|
;; search engines state for results
|
|
(when-let [removed' (seq (filter #(= :search (:type %)) removed))]
|
|
(update-state! :search/engines #(apply dissoc % (mapv (fn [{:keys [pid name]}] (str pid name)) removed'))))))))
|
|
|
|
(defn get-all-plugin-services-with-type
|
|
[type]
|
|
(when-let [installed (vals (:plugin/installed-services (rfx/snapshot)))]
|
|
(mapcat (fn [s] (filter #(= (keyword type) (:type %)) s)) installed)))
|
|
|
|
(defn get-all-plugin-search-engines
|
|
[]
|
|
(:search/engines (rfx/snapshot)))
|
|
|
|
(defn update-plugin-search-engine
|
|
[pid name f]
|
|
(when-let [pid (keyword pid)]
|
|
(set-state! :search/engines
|
|
(update-vals (get-all-plugin-search-engines)
|
|
#(if (and (= pid (:pid %)) (= name (:name %)))
|
|
(f %) %)))))
|
|
|
|
(defn reset-plugin-search-engines
|
|
[]
|
|
(when-let [engines (get-all-plugin-search-engines)]
|
|
(set-state! :search/engines
|
|
(update-vals engines #(assoc % :result nil)))))
|
|
|
|
(defn install-plugin-hook
|
|
([pid hook] (install-plugin-hook pid hook true))
|
|
([pid hook opts]
|
|
(when-let [pid (keyword pid)]
|
|
(set-state!
|
|
[:plugin/installed-hooks hook]
|
|
(assoc
|
|
((fnil identity {}) (get-in (rfx/snapshot) [:plugin/installed-hooks hook]))
|
|
pid opts)) true)))
|
|
|
|
(defn uninstall-plugin-hook
|
|
[pid hook-or-all]
|
|
(when-let [pid (keyword pid)]
|
|
(if (nil? hook-or-all)
|
|
(swap-state! update :plugin/installed-hooks #(update-vals % (fn [ids] (dissoc ids pid))))
|
|
(when-let [coll (get-in (rfx/snapshot) [:plugin/installed-hooks hook-or-all])]
|
|
(set-state! [:plugin/installed-hooks hook-or-all] (dissoc coll pid))))
|
|
true))
|
|
|
|
(defn slot-hook-exist?
|
|
[uuid]
|
|
(when-let [type (and uuid (string/replace (str uuid) "-" "_"))]
|
|
(when-let [hooks (get-state :plugin/installed-hooks)]
|
|
(contains? hooks (str "hook:editor:slot_" type)))))
|
|
|
|
(defn set-editor-in-composition!
|
|
[value]
|
|
(set-state! :editor/in-composition? value))
|
|
|
|
(defn editor-in-composition?
|
|
[]
|
|
(:editor/in-composition? (rfx/snapshot)))
|
|
|
|
(defn set-editor-last-input-time!
|
|
[repo time]
|
|
(set-state! :editor/last-input-time time :nested-path repo))
|
|
|
|
(defn input-idle?
|
|
[repo & {:keys [diff]
|
|
:or {diff 1000}}]
|
|
(when repo
|
|
(let [last-input-time (get (get-state :editor/last-input-time) repo)]
|
|
(or
|
|
(nil? last-input-time)
|
|
|
|
(let [now (util/time-ms)]
|
|
(>= (- now last-input-time) diff))
|
|
|
|
;; not in editing mode
|
|
(not (get-edit-input-id))))))
|
|
|
|
(defn set-search-result!
|
|
[value]
|
|
(set-state! :search/result value))
|
|
|
|
(defn clear-search-result!
|
|
[]
|
|
(set-search-result! nil))
|
|
|
|
(defn get-search-mode
|
|
[]
|
|
(:search/mode (rfx/snapshot)))
|
|
|
|
(defn toggle!
|
|
[path]
|
|
(update-state! path not))
|
|
|
|
(defn toggle-settings!
|
|
[]
|
|
(toggle! :ui/settings-open?))
|
|
|
|
(defn close-settings!
|
|
[]
|
|
(set-state! :ui/settings-open? false))
|
|
|
|
(defn open-settings!
|
|
([] (open-settings! true))
|
|
([active-tab] (set-state! :ui/settings-open? active-tab)))
|
|
|
|
(defn sidebar-add-block!
|
|
[repo db-id block-type]
|
|
(when (not (util/sm-breakpoint?))
|
|
(p/let [page (when (and repo (= :page block-type) (number? db-id))
|
|
(<invoke-db-worker :thread-api/pull
|
|
repo
|
|
[:block/title
|
|
:logseq.property/hide?
|
|
:logseq.property/deleted-at
|
|
:logseq.property/built-in?
|
|
{:block/parent
|
|
[:db/id :logseq.property/hide? :logseq.property/deleted-at]}
|
|
{:block/tags [:db/ident]}]
|
|
db-id))]
|
|
(if (and page
|
|
;; TODO: Use config/dev? when it's not a circular dep
|
|
(not goog.DEBUG)
|
|
(not= common-config/recycle-page-name (:block/title page))
|
|
(or (and (ldb/hidden? page) (not (entity/property? page)))
|
|
(and (ldb/built-in? page) (ldb/private-built-in-page? page))))
|
|
(pub-event! [:notification/show {:content "Cannot open an internal page." :status :warning}])
|
|
(when db-id
|
|
(update-state! :sidebar/blocks (fn [blocks]
|
|
(->> (remove #(= (second %) db-id) blocks)
|
|
(cons [repo db-id block-type])
|
|
(distinct))))
|
|
(set-state! [:ui/sidebar-collapsed-blocks db-id] false)
|
|
(open-right-sidebar!)
|
|
(when-let [elem (gdom/getElementByClass "sidebar-item-list")]
|
|
(util/scroll-to elem 0)))))))
|
|
|
|
(defn get-export-block-text-indent-style []
|
|
(normalize-export-block-text-indent-style (:copy/export-block-text-indent-style (rfx/snapshot))))
|
|
|
|
(defn set-export-block-text-indent-style!
|
|
[v]
|
|
(let [v* (normalize-export-block-text-indent-style v)]
|
|
(set-state! :copy/export-block-text-indent-style v*)
|
|
(storage/set :copy/export-block-text-indent-style v*)))
|
|
|
|
(defn get-recent-pages
|
|
[]
|
|
(get-in (rfx/snapshot) [:ui/recent-pages (get-current-repo)]))
|
|
|
|
(defn set-recent-pages!
|
|
[v]
|
|
(set-state! [:ui/recent-pages (get-current-repo)] v)
|
|
(storage/set :ui/recent-pages (:ui/recent-pages (rfx/snapshot))))
|
|
|
|
(defn remove-pages-from-recent!
|
|
[page-ids]
|
|
(when (seq page-ids)
|
|
(let [page-ids (set page-ids)
|
|
recent-pages (get-recent-pages)
|
|
remaining-pages (vec (remove page-ids recent-pages))]
|
|
(when (and (seq recent-pages)
|
|
(not= recent-pages remaining-pages))
|
|
(set-recent-pages! remaining-pages)))))
|
|
|
|
(defn get-export-block-text-remove-options []
|
|
(:copy/export-block-text-remove-options (rfx/snapshot)))
|
|
|
|
(defn update-export-block-text-remove-options!
|
|
[e k]
|
|
(let [f (if (util/echecked? e) conj disj)]
|
|
(update-state! :copy/export-block-text-remove-options
|
|
#(f % k))
|
|
(storage/set :copy/export-block-text-remove-options
|
|
(get-export-block-text-remove-options))))
|
|
|
|
(defn get-export-block-text-other-options []
|
|
(:copy/export-block-text-other-options (rfx/snapshot)))
|
|
|
|
(defn update-export-block-text-other-options!
|
|
[k v]
|
|
(update-state! :copy/export-block-text-other-options #(assoc % k v)))
|
|
|
|
(defn set-editor-args!
|
|
[args]
|
|
(set-state! :editor/args args))
|
|
|
|
(defn block-component-editing?
|
|
[]
|
|
(:block/component-editing-mode? (rfx/snapshot)))
|
|
|
|
(defn set-block-component-editing-mode!
|
|
[value]
|
|
(set-state! :block/component-editing-mode? value))
|
|
|
|
(defn get-editor-args
|
|
[]
|
|
(get-state :editor/args))
|
|
|
|
(defn get-editor-block-container
|
|
[]
|
|
(some-> (get-edit-input-id)
|
|
(gdom/getElement)
|
|
(util/rec-get-node "ls-block")))
|
|
|
|
(defn set-page-blocks-cp!
|
|
[value]
|
|
(set-state! [:view/components :page-blocks] value))
|
|
|
|
(defn get-page-blocks-cp
|
|
[]
|
|
(get-in (rfx/snapshot) [:view/components :page-blocks]))
|
|
|
|
;; To avoid circular dependencies
|
|
(defn set-component!
|
|
[k value]
|
|
(set-state! [:view/components k] value))
|
|
|
|
(defn get-component
|
|
[k]
|
|
(get-in (rfx/snapshot) [:view/components k]))
|
|
|
|
(defn exit-editing-and-set-selected-blocks!
|
|
([blocks]
|
|
(exit-editing-and-set-selected-blocks! blocks nil))
|
|
([blocks direction]
|
|
(clear-edit!)
|
|
(set-selection-blocks! blocks direction)))
|
|
|
|
(defn set-editing!
|
|
[edit-input-id content block cursor-range & {:keys [move-cursor? container-id property-block direction event pos]
|
|
:or {move-cursor? true}}]
|
|
(when-not (exists? js/process)
|
|
(when (and edit-input-id block
|
|
(or
|
|
(publishing-enable-editing?)
|
|
(not common-config/PUBLISHING)))
|
|
(let [block-element (gdom/getElement (string/replace edit-input-id "edit-block" "ls-block"))
|
|
container (util/get-block-container block-element)
|
|
block (if container
|
|
(assoc block
|
|
:block.temp/container (gobj/get container "id"))
|
|
block)
|
|
block (assoc block :block.editing/direction direction
|
|
:block.editing/event event
|
|
:block.editing/pos pos)
|
|
content (string/trim (or content ""))
|
|
block-id (:block/uuid block)
|
|
editing-block-id (if property-block
|
|
[container-id (:block/uuid property-block) block-id]
|
|
[container-id block-id])
|
|
native-platform? (mobile-util/native-platform?)]
|
|
(assert (and container-id (:block/uuid block))
|
|
"container-id or block uuid is missing")
|
|
(swap-state!
|
|
(fn [db]
|
|
(cond-> (-> db
|
|
(assoc :editor/block-refs #{}
|
|
:editor/block block
|
|
:editor/editing? {editing-block-id true}
|
|
:editor/container-id container-id
|
|
:editor/last-key-code nil
|
|
:editor/set-timestamp-block nil
|
|
:editor/cursor-range cursor-range)
|
|
(assoc-in [:editor/content block-id] content))
|
|
native-platform?
|
|
(assoc :mobile/show-action-bar? false))))
|
|
(let [online-users (some-> (get-state :rtc/state) :online-users)]
|
|
(when (and (coll? online-users) (> (count online-users) 1))
|
|
(pub-event! [:rtc/presence-update {:editing-block-uuid (str block-id)}])))
|
|
(when (= :code (:logseq.property.node/display-type block))
|
|
(pub-event! [:editor/focus-code-editor block block-element]))
|
|
(when-let [input (gdom/getElement edit-input-id)]
|
|
(let [pos (count cursor-range)]
|
|
(when content
|
|
(util/set-change-value input content))
|
|
|
|
(when (and move-cursor? (not (block-component-editing?)))
|
|
(cursor/move-cursor-to input pos))))))))
|
|
|
|
(defn set-last-key-code!
|
|
[key-code]
|
|
(set-state! :editor/last-key-code key-code))
|
|
|
|
(defn get-last-key-code
|
|
[]
|
|
(get-state :editor/last-key-code))
|
|
|
|
(defn set-ui-last-key-code!
|
|
[key-code]
|
|
(set-state! :ui/global-last-key-code key-code))
|
|
|
|
(defn get-ui-last-key-code
|
|
[]
|
|
(get-state :ui/global-last-key-code))
|
|
|
|
(defn set-block-op-type!
|
|
[op-type]
|
|
(set-state! :editor/block-op-type op-type))
|
|
|
|
(defn get-block-op-type
|
|
[]
|
|
(:editor/block-op-type (rfx/snapshot)))
|
|
|
|
(defn feature-http-server-enabled?
|
|
[]
|
|
(boolean (storage/get ::storage-spec/http-server-enabled)))
|
|
|
|
(defn get-plugin-by-id
|
|
[id]
|
|
(when-let [id (and id (keyword id))]
|
|
(get-in (rfx/snapshot) [:plugin/installed-plugins id])))
|
|
|
|
(defn get-enabled?-installed-plugins
|
|
([theme?] (get-enabled?-installed-plugins theme? true false false))
|
|
([theme? enabled? include-unpacked? include-all?]
|
|
(filterv
|
|
#(and (if include-unpacked? true (or (:webMode %) (:iir %)))
|
|
(if-not (boolean? enabled?) true (= (not enabled?) (boolean (get-in % [:settings :disabled]))))
|
|
(or include-all? (if (boolean? theme?) (= (boolean theme?) (:theme %)) true)))
|
|
(vals (:plugin/installed-plugins (rfx/snapshot))))))
|
|
|
|
(defn lsp-enabled?-or-theme
|
|
[]
|
|
(:plugin/enabled (rfx/snapshot)))
|
|
|
|
(def lsp-enabled?
|
|
(lsp-enabled?-or-theme))
|
|
|
|
(defn consume-updates-from-coming-plugin!
|
|
[payload updated?]
|
|
(when-let [id (keyword (:id payload))]
|
|
(let [prev-pending? (boolean (seq (:plugin/updates-pending (rfx/snapshot))))]
|
|
(println "Updates: consumed pending - " id)
|
|
(swap-state! update :plugin/updates-pending dissoc id)
|
|
(if updated?
|
|
(if-let [error (:error-code payload)]
|
|
(swap-state! update-in [:plugin/updates-coming id] assoc :error-code error)
|
|
(swap-state! update :plugin/updates-coming dissoc id))
|
|
(swap-state! update :plugin/updates-coming assoc id payload))
|
|
(pub-event! [:plugin/consume-updates id prev-pending? updated?]))))
|
|
|
|
(defn coming-update-new-version?
|
|
[pkg]
|
|
(and pkg (:latest-version pkg)))
|
|
|
|
(defn plugin-update-available?
|
|
[id]
|
|
(when-let [pkg (and id (get (:plugin/updates-coming (rfx/snapshot)) (keyword id)))]
|
|
(coming-update-new-version? pkg)))
|
|
|
|
(defn all-available-coming-updates
|
|
([] (all-available-coming-updates (:plugin/updates-coming (rfx/snapshot))))
|
|
([updates] (when-let [updates (vals updates)]
|
|
(filterv #(coming-update-new-version? %) updates))))
|
|
|
|
(defn get-next-selected-coming-update
|
|
[]
|
|
(when-let [updates (all-available-coming-updates)]
|
|
(let [unchecked (:plugin/updates-unchecked (rfx/snapshot))]
|
|
(first (filter #(and (not (and (seq unchecked) (contains? unchecked (:id %))))
|
|
(not (:error-code %))) updates)))))
|
|
|
|
(defn set-unchecked-update
|
|
[id unchecked?]
|
|
(swap-state! update :plugin/updates-unchecked (if unchecked? conj disj) id))
|
|
|
|
(defn reset-unchecked-update
|
|
[]
|
|
(swap-state! assoc :plugin/updates-unchecked #{}))
|
|
|
|
(defn reset-all-updates-state
|
|
[]
|
|
(swap-state! assoc
|
|
:plugin/updates-auto-checking? false
|
|
:plugin/updates-pending {}
|
|
:plugin/updates-coming {}
|
|
:plugin/updates-downloading? false))
|
|
|
|
(defn use-right-sidebar-blocks
|
|
[]
|
|
(let [current-repo (rfx/use-sub [:git/current-repo])
|
|
blocks (rfx/use-sub [:sidebar/blocks])]
|
|
(when current-repo
|
|
(filter #(= (first %) current-repo) blocks))))
|
|
|
|
(defn get-current-editor-container-id
|
|
[]
|
|
(get-state :editor/container-id))
|
|
|
|
(defn resolve-container-id
|
|
[container-id]
|
|
(or container-id (get-current-editor-container-id) :unknown-container))
|
|
|
|
(defn toggle-collapsed-block!
|
|
([block-id] (toggle-collapsed-block! block-id nil))
|
|
([block-id container-id]
|
|
(let [current-repo (get-current-repo)
|
|
container-id (resolve-container-id container-id)]
|
|
(update-state! [:ui/collapsed-blocks current-repo container-id block-id] not))))
|
|
|
|
(defn set-collapsed-block!
|
|
([block-id value] (set-collapsed-block! block-id value nil))
|
|
([block-id value container-id]
|
|
(let [current-repo (get-current-repo)
|
|
container-id (resolve-container-id container-id)]
|
|
(set-state! [:ui/collapsed-blocks current-repo container-id block-id] value))))
|
|
|
|
(defn get-block-collapsed
|
|
([block-id] (get-block-collapsed block-id nil))
|
|
([block-id container-id]
|
|
(get-in (rfx/snapshot) [:ui/collapsed-blocks (get-current-repo) (resolve-container-id container-id) block-id])))
|
|
|
|
(defn get-dialog-id
|
|
[]
|
|
(shui-dialog/get-last-dialog-id))
|
|
|
|
(defn set-auth-id-token
|
|
[id-token]
|
|
(set-state! :auth/id-token id-token))
|
|
|
|
(defn set-auth-refresh-token
|
|
[refresh-token]
|
|
(set-state! :auth/refresh-token refresh-token))
|
|
|
|
(defn set-auth-access-token
|
|
[access-token]
|
|
(set-state! :auth/access-token access-token))
|
|
|
|
(defn get-auth-id-token []
|
|
(get-state :auth/id-token))
|
|
|
|
(defn get-auth-refresh-token []
|
|
(:auth/refresh-token (rfx/snapshot)))
|
|
|
|
(defn http-proxy-enabled-or-val? []
|
|
(when-let [{:keys [type protocol host port]} (get-state [:electron/user-cfgs :settings/agent])]
|
|
;; Older saved proxy settings may only have :protocol.
|
|
(let [proxy-type (or type protocol)]
|
|
(when (and (contains? #{"http" "socks5"} proxy-type)
|
|
(every? not-empty [proxy-type host port]))
|
|
(str proxy-type "://" host ":" port)))))
|
|
|
|
(defn get-current-pdf
|
|
[]
|
|
(:pdf/current (rfx/snapshot)))
|
|
|
|
(defn set-current-pdf!
|
|
[inflated-file]
|
|
(let [settle-file! #(set-state! :pdf/current inflated-file)]
|
|
(if-not (get-current-pdf)
|
|
(settle-file!)
|
|
(when (apply not= (map :identity [inflated-file (get-current-pdf)]))
|
|
(set-state! :pdf/current nil)
|
|
(js/setTimeout #(settle-file!) 16)))))
|
|
|
|
(defn set-user-info!
|
|
[info]
|
|
(when info
|
|
(set-state! :user/info info)
|
|
(let [groups (:UserGroups info)]
|
|
(when (seq groups)
|
|
(storage/set :user-groups groups)))))
|
|
|
|
(defn clear-user-info!
|
|
[]
|
|
(storage/remove :user-groups))
|
|
|
|
(defn set-color-accent! [color]
|
|
(swap-state! assoc :ui/radix-color color)
|
|
(storage/set :ui/radix-color color))
|
|
|
|
(defn set-editor-font! [config]
|
|
(let [config' (:ui/editor-font (rfx/snapshot))
|
|
config (if (map? config') (merge config' config) {})]
|
|
(swap-state! assoc :ui/editor-font config)
|
|
(storage/set :ui/editor-font config)))
|
|
|
|
(defn handbook-open?
|
|
[]
|
|
(:ui/handbooks-open? (rfx/snapshot)))
|
|
|
|
(defn get-handbook-route-chan
|
|
[]
|
|
(:handbook/route-chan (rfx/snapshot)))
|
|
|
|
(defn open-handbook-pane!
|
|
[k]
|
|
(when-not (handbook-open?)
|
|
(set-state! :ui/handbooks-open? true))
|
|
(js/setTimeout #(async/go
|
|
(>! (get-handbook-route-chan) k))))
|
|
|
|
(defn update-favorites-updated!
|
|
[]
|
|
(update-state! :favorites/updated? inc))
|
|
|
|
(defn get-next-container-id
|
|
[]
|
|
(let [next-id (inc (or (get-state :ui/container-id) 0))]
|
|
(set-state! :ui/container-id next-id)
|
|
next-id))
|
|
|
|
(defn get-container-id
|
|
"Either cached container-id or a new id"
|
|
[key]
|
|
(if (seq key)
|
|
(let [db (rfx/snapshot)]
|
|
(or (get (:ui/cached-key->container-id db) key)
|
|
(let [id (inc (or (:ui/container-id db) 0))
|
|
db' (-> db
|
|
(assoc :ui/container-id id)
|
|
(assoc-in [:ui/cached-key->container-id key] id))]
|
|
(vswap! *profile-state update :ui/container-id inc)
|
|
(vswap! *profile-state update :ui/cached-key->container-id inc)
|
|
(rfx/replace-state-paths! db' [[:ui/container-id]
|
|
[:ui/cached-key->container-id key]])
|
|
id)))
|
|
(get-next-container-id)))
|
|
|
|
(defn use-container-id
|
|
"Return a stable container id for the component lifetime."
|
|
([]
|
|
(use-container-id nil))
|
|
([key]
|
|
(hooks/use-memo #(get-container-id key) [key])))
|
|
|
|
(comment
|
|
(defn remove-container-key!
|
|
[key]
|
|
(update-state! :ui/cached-key->container-id #(dissoc % key))))
|
|
|
|
(defn get-editor-info
|
|
[]
|
|
(let [selected-block-uuids (some-> (get-selection-block-ids) seq vec)
|
|
selection-info (when selected-block-uuids
|
|
{:selected-block-uuids selected-block-uuids
|
|
:selection-direction (get-selection-direction)})]
|
|
(if-let [edit-block (get-edit-block)]
|
|
(cond-> {:block-uuid (:block/uuid edit-block)
|
|
:container-id (or (get-state :editor/container-id) :unknown-container)
|
|
:start-pos (get-state :editor/start-pos)
|
|
:end-pos (get-edit-pos)}
|
|
selection-info
|
|
(merge selection-info))
|
|
selection-info)))
|
|
|
|
(defn conj-block-ref!
|
|
[ref-entity]
|
|
(update-state! :editor/block-refs conj ref-entity))
|
|
|
|
(defn get-highlight-recent-days
|
|
[]
|
|
(get-state :ui/highlight-recent-days))
|
|
|
|
(defn set-highlight-recent-days!
|
|
[days]
|
|
(set-state! :ui/highlight-recent-days days)
|
|
(storage/set :ui/highlight-recent-days days))
|
|
|
|
(defn set-db-worker-client-id!
|
|
[new-id]
|
|
(when new-id
|
|
(reset! *db-worker-client-id new-id)
|
|
(storage/set :db-worker-client-id new-id)))
|