(ns frontend.state "Provides main application state, fns associated to set and state based rum cursors" (:require [cljs-bean.core :as bean] [cljs.core.async :as async :refer [>!]] [cljs.spec.alpha :as s] [clojure.set :as set] [clojure.string :as string] [datascript.core :as d] [dommy.core :as dom] [electron.ipc :as ipc] [frontend.db.conn-state :as db-conn-state] [frontend.db.transact :as db-transact] [frontend.flows :as flows] [frontend.mobile.util :as mobile-util] [frontend.rum :as r] [frontend.spec.storage :as storage-spec] [frontend.storage :as storage] [frontend.util :as util] [frontend.util.cursor :as cursor] [goog.dom :as gdom] [goog.object :as gobj] [logseq.common.config :as common-config] [logseq.db :as ldb] [logseq.db.frontend.entity-plus :as entity-plus] [logseq.db.sqlite.util :as sqlite-util] [logseq.shui.dialog.core :as shui-dialog] [logseq.shui.ui :as shui] [promesa.core :as p] [rum.core :as rum])) (defonce *profile-state (volatile! {})) (defonce *db-worker (atom nil)) (defn- global mode, :graph -> add graph filter, etc. :search/args nil :search/result nil :search/graph-filters [] :search/engines {} ;; modals :modal/dropdowns {} :modal/id nil ;; ui :ui/viewport {} ;; 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 => {:block-id bool} :ui/collapsed-blocks {} :ui/sidebar-collapsed-blocks {} :ui/root-component nil :ui/file-component nil :ui/developer-mode? (or (= (storage/get "developer-mode") "true") false) ;; remember scroll positions of visited paths :ui/paths-scroll-positions (atom {}) :ui/main-container-scroll-top (atom nil) :ui/shortcut-tooltip? (if (false? (storage/get :ui/shortcut-tooltip?)) false true) :ui/scrolling? (atom false) :ui/show-empty-and-hidden-properties? (atom {:mode :global :show? false}) :document/mode? document-mode? :config {} :block/component-editing-mode? false :editor/op (atom nil) :editor/start-pos (atom nil) :editor/async-unsaved-chars (atom nil) :editor/hidden-editors #{} ;; page names :editor/draw-mode? false :editor/action (atom nil) :editor/action-data nil ;; With label or other data :editor/last-saved-cursor (atom {}) :editor/editing? (atom {}) :editor/in-composition? false :editor/content (atom {}) :editor/block (atom nil) :editor/block-dom-id (atom nil) :editor/set-timestamp-block (atom nil) ;; click rendered block timestamp-cp to set timestamp :editor/last-input-time (atom {}) :editor/document-mode? document-mode? :editor/args (atom nil) :editor/on-paste? (atom false) :editor/last-key-code (atom nil) :ui/global-last-key-code (atom nil) :editor/block-op-type nil ;; :cut, :copy :editor/block-refs (atom #{}) ;; Stores deleted refed blocks, indexed by repo :editor/last-replace-ref-content-tx nil ;; for audio record :editor/record-status "NONE" :editor/code-block-context nil :editor/latest-shortcut (atom nil) :history/paused? (atom false) :editor/cursor-range (atom nil) :editor/container-id (atom nil) :editor/next-edit-block (atom nil) :editor/raw-mode-block (atom nil) :editor/virtualized-scroll-fn nil ;; Warning: blocks order is determined when setting this attribute :selection/blocks (atom []) :selection/start-block (atom nil) ;; nil, :up or :down ;; used to determine selection direction when two or more blocks are selected :selection/direction (atom nil) :selection/selected-all? (atom 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 (storage/get :preferred-language) ;; electron :electron/auto-updater-downloaded false :electron/updater-pending? false :electron/updater {} :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) []) ;; mobile :mobile/container-urls nil :mobile/show-action-bar? false :mobile/actioned-block nil :mobile/show-toolbar? false :mobile/show-recording-bar? false :mobile/show-tabbar? false ;;; Used to monitor mobile app status, ;;; value spec: ;;; {:is-active? bool, :timestamp int} :mobile/app-state-change (atom nil) ;; 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")) ;; all notification contents as k-v pairs :notification/contents {} :graph/syncing? false ;; graph -> state :graph/parsing-state {} :copy/export-block-text-indent-style (or (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 :youtube/players {} ;; command palette :command-palette/commands (atom []) :view/components {} :view/selected-blocks nil :srs/mode? false :srs/cards-due-count nil :reactive/query-dbs {} ;; login, userinfo, token, ... :auth/refresh-token (storage/get "refresh-token") :auth/access-token nil :auth/id-token nil ;; file-sync :file-sync/jstour-inst nil :file-sync/onboarding-state (or (storage/get :file-sync/onboarding-state) {:welcome false}) :file-sync/remote-graphs {:loading false :graphs nil} :file-sync/set-remote-graph-password-result {} ;; graph-uuid -> {:graphs-txid {} ;; :file-sync/sync-manager {} ;; :file-sync/sync-state {} ;; ;; {file-path -> payload} ;; :file-sync/progress {} ;; :file-sync/start-time {} ;; :file-sync/last-synced-at {}} :file-sync/graph-state {:current-graph-uuid nil} ;; graph-uuid -> ... :rtc/state (atom {}) ;; only latest rtc-log stored here, when a log stream is needed, ;; use missionary to create a rtc-log-flow, use (missionary.core/watch ) :rtc/log (atom nil) :rtc/uploading? false :rtc/downloading-graph-uuid nil :rtc/graphs [] :rtc/online-info (atom {}) :rtc/asset-upload-download-progress (atom {}) :user/info {:UserGroups (storage/get :user-groups)} :encryption/graph-parsing? false :ui/loading? {} :ui/container-id (atom 0) :ui/cached-key->container-id (atom {}) :feature/enable-sync? (storage/get :logseq-sync-enabled) :feature/enable-sync-diff-merge? ((fnil identity true) (storage/get :logseq-sync-diff-merge-enabled)) :feature/enable-rtc? (storage/get :logseq-rtc-enabled) :file/rename-event-chan (async/chan 100) :ui/find-in-page nil :graph/importing nil :graph/importing-state {} :graph/loading? nil :handbook/route-chan (async/chan (async/sliding-buffer 1)) :whiteboard/onboarding-whiteboard? (or (storage/get :ls-onboarding-whiteboard?) false) :whiteboard/onboarding-tour? (or (storage/get :whiteboard-onboarding-tour?) false) :whiteboard/last-persisted-at {} :whiteboard/pending-tx-data {} :system/info {} ;; Whether block is selected :ui/select-query-cache (atom {}) :ui/toggle-highlight-recent-blocks? (atom false) :ui/highlight-recent-days (atom (or (storage/get :ui/highlight-recent-days) 3)) :favorites/updated? (atom 0) :db/async-query-loading (atom #{}) :db/async-queries (atom {}) :db/latest-transacted-entity-uuids (atom {})}))) ;; Block ast state ;; =============== ;; block uuid -> {content(String) -> ast} (def blocks-ast-cache (atom {})) (defn add-block-ast-cache! [block-uuid content ast] (when (and block-uuid content ast) (let [new-value (assoc-in @blocks-ast-cache [block-uuid content] ast) new-value (if (> (count new-value) 10000) (into {} (take 5000 new-value)) new-value)] (reset! blocks-ast-cache new-value)))) (defn get-block-ast [block-uuid content] (when (and block-uuid content) (get-in @blocks-ast-cache [block-uuid content]))) ;; 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 ;; For flushing the settings of old versions. Don't bump this value. ;; There are only two kinds of graph, one is not upgraded (:legacy) and one is upgraded (:triple-lowbar) ;; For not upgraded graphs, the config will have no key `:file/name-format` ;; Then the default value is applied :file/name-format :legacy}) (def file-default-config "Default repo config for file graphs" (merge common-default-config ;; The "NOW" query returns tasks with "NOW" or "DOING" status. ;; The "NEXT" query returns tasks with "NOW", "LATER", or "TODO" status. {:default-queries {:journals [{:title "🔨 NOW" :query '[:find (pull ?h [*]) :in $ ?start ?today :where (task ?h #{"NOW" "DOING"}) [?h :block/page ?p] [?p :block/journal-day ?d] [(>= ?d ?start)] [(<= ?d ?today)]] :inputs [:14d :today] :result-transform '(fn [result] (sort-by (fn [h] (get h :block/priority "Z")) result)) :group-by-page? false :collapsed? false} {:title "📅 NEXT" :query '[:find (pull ?h [*]) :in $ ?start ?next :where (task ?h #{"NOW" "LATER" "TODO"}) [?h :block/page ?p] [?p :block/journal-day ?d] [(> ?d ?start)] [(< ?d ?next)]] :inputs [:today :7d-after] :group-by-page? false :collapsed? false}]}})) (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 [:span (shui/tabler-icon "InProgress50" {:class "align-middle pr-1"}) [:span.align-middle "DOING"]] :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? false} {:title [:span (shui/tabler-icon "Todo" {:class "align-middle pr-1"}) [:span.align-middle "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? false}]} :ui/hide-empty-properties? false})) ;; State that most user config is dependent on (declare get-current-repo sub set-state!) (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 @state [:config ::global-config])) (defn get-global-config-str-content [] (get-in @state [:config ::global-config-str-content])) (defn get-graph-config ([] (get-graph-config (get-current-repo))) ([repo-url] (get-in @state [: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 (if (sqlite-util/db-based-graph? repo-url) db-default-config file-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 get-default-journal-template [] (when-let [template (get-in (get-config) [:default-templates :journals])] (when-not (string/blank? template) (string/trim template)))) (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)))) (defn get-preferred-format ([] (get-preferred-format (get-current-repo))) ([repo-url] (keyword (or (common-config/get-preferred-format (get-config repo-url)) (get-in @state [:me :preferred_format] "markdown"))))) (defn markdown? [] (= (keyword (get-preferred-format)) :markdown)) (defn get-pages-directory [] (or (when-let [repo (get-current-repo)] (:pages-directory (get-config repo))) "pages")) (defn get-journals-directory [] (or (when-let [repo (get-current-repo)] (:journals-directory (get-config repo))) "journals")) (defn get-whiteboards-directory [] (or (when-let [repo (get-current-repo)] (:whiteboards-directory (get-config repo))) "whiteboards")) (defn org-mode-file-link? [repo] (:org-mode/insert-file-link? (get-config repo))) (defn get-journal-file-name-format [] (when-let [repo (get-current-repo)] (:journal/file-name-format (get-config repo)))) (defn get-preferred-workflow [] (keyword (or (when-let [workflow (:preferred-workflow (get-config))] (let [workflow (name workflow)] (if (util/safe-re-find #"now|NOW" workflow) :now :todo))) (get-in @state [:me :preferred_workflow] :now)))) (defn get-preferred-todo [] (if (= (get-preferred-workflow) :now) "LATER" "TODO")) (defn get-filename-format ([] (get-filename-format (get-current-repo))) ([repo] (:file/name-format (get-config repo)))) (defn get-date-formatter [] (let [repo (get-current-repo)] (if (sqlite-util/db-based-graph? repo) (when-let [conn (db-conn-state/get-conn repo)] (get (entity-plus/entity-memoized @conn :logseq.class/Journal) :logseq.property.journal/title-format "MMM do, yyyy")) (common-config/get-date-formatter (get-config))))) (defn custom-shortcuts [] (merge (storage/get :ls-shortcuts) (: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 @state [:me :settings :start-of-week]) 6)) (defn get-ref-open-blocks-level [] (or (when-let [value (:ref/default-open-blocks-level (get-config))] (when (integer? value) value)) 2)) (defn get-linked-references-collapsed-threshold [] (or (when-let [value (:ref/linked-references-collapsed-threshold (get-config))] (when (integer? value) value)) 100)) (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))) ;; State cursor fns for use with rum components ;; ============================================ (declare document-mode?) (defn sub "Creates a rum cursor, https://github.com/tonsky/rum#cursors, for use in rum components. Similar to re-frame subscriptions" [ks & {:keys [path-in-sub-atom]}] (let [ks-coll? (coll? ks) get-fn (if ks-coll? get-in get) s (get-fn @state ks) s-atom? (util/atom? s) path-coll?-in-sub-atom (coll? path-in-sub-atom)] (cond (and s-atom? path-in-sub-atom path-coll?-in-sub-atom) (util/react (rum/cursor-in s path-in-sub-atom)) (and s-atom? path-in-sub-atom) (util/react (rum/cursor s path-in-sub-atom)) s-atom? (util/react s) ks-coll? (util/react (rum/cursor-in state ks)) :else (util/react (rum/cursor state ks))))) (defn set-editing-block-id! [container-block] (reset! (:editor/editing? @state) {container-block true})) (defn sub-editing? [container-block] (when container-block (rum/react (r/cached-derived-atom (:editor/editing? @state) [(get-current-repo) ::editing-block container-block] (fn [s] (get s container-block)))))) (defn sub-config "Sub equivalent to get-config which should handle all sub user-config access" ([] (sub-config (get-current-repo))) ([repo] (let [config (sub :config)] (merge-configs (if (and (string? repo) (sqlite-util/db-based-graph? repo)) db-default-config file-default-config) (get config ::global-config) (get config repo))))) (defn enable-grammarly? [] (true? (:feature/enable-grammarly? (sub-config)))) (defn scheduled-deadlines-disabled? [] (true? (:feature/disable-scheduled-and-deadline-query? (sub-config)))) (defn enable-timetracking? [] (not (false? (:feature/enable-timetracking? (sub-config))))) (defn enable-fold-button-right? [] (let [_ (sub :ui/viewport)] (and (util/mobile?) (util/sm-breakpoint?)))) (defn enable-journals? ([] (enable-journals? (get-current-repo))) ([repo] (not (false? (:feature/enable-journals? (sub-config repo)))))) (defn enable-flashcards? ([] (enable-flashcards? (get-current-repo))) ([repo] (not (false? (:feature/enable-flashcards? (sub-config repo)))))) (defn enable-sync? [] (sub :feature/enable-sync?)) (defn enable-sync-diff-merge? [] (sub :feature/enable-sync-diff-merge?)) (defn enable-whiteboards? ([] (enable-whiteboards? (get-current-repo))) ([repo] (not (false? (:feature/enable-whiteboards? (sub-config repo)))))) (defn enable-rtc? [] (sub :feature/enable-rtc?)) (defn enable-git-auto-push? [repo] (not (false? (:git-auto-push (sub-config repo))))) (defn graph-settings [] (:graph/settings (sub-config))) (defn graph-forcesettings [] (:graph/forcesettings (sub-config))) ;; Enable by default (defn show-brackets? [] (not (false? (:ui/show-brackets? (sub-config))))) (defn sub-default-home-page [] (get-in (sub-config) [:default-home :page] "")) (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 (sub-config repo)) ;; backward compatible (:block/content-max-length (sub-config repo)) 10000)) (defn mobile? [] (or (util/mobile?) (mobile-util/native-platform?))) (defn enable-tooltip? [] (if (mobile?) false (get (sub-config) :ui/enable-tooltip? true))) (defn show-command-doc? [] (get (sub-config) :ui/show-command-doc? true)) (defn logical-outdenting? [] (:editor/logical-outdenting? (sub-config))) (defn show-full-blocks? [] (:ui/show-full-blocks? (sub-config))) (defn preferred-pasting-file? [] (:editor/preferred-pasting-file? (sub-config))) (defn auto-expand-block-refs? [] (:ui/auto-expand-block-refs? (sub-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 (sub [:user/info :UserGroups]))) ;; State mutation helpers ;; ====================== (defn set-state! [path value & {:keys [path-in-sub-atom]}] (vswap! *profile-state update path inc) (let [path-coll? (coll? path) get-fn (if path-coll? get-in get) s (get-fn @state path) s-atom? (util/atom? s) path-coll?-in-sub-atom (coll? path-in-sub-atom)] (cond (and s-atom? path-in-sub-atom path-coll?-in-sub-atom) (let [old-v (get-in @s path-in-sub-atom)] (when (not= old-v value) (swap! s assoc-in path-in-sub-atom value))) (and s-atom? path-in-sub-atom) (let [old-v (get @s path-in-sub-atom)] (when (not= old-v value) (swap! s assoc path-in-sub-atom value))) s-atom? (when (not= @s value) (reset! s value)) path-coll? (when (not= s value) (swap! state assoc-in path value)) :else (when (not= s value) (swap! state assoc path value)))) nil) (defn update-state! [path f & {:keys [path-in-sub-atom]}] (vswap! *profile-state update path inc) (let [path-coll? (coll? path) get-fn (if path-coll? get-in get) s (get-fn @state path) s-atom? (util/atom? s) path-coll?-in-sub-atom (coll? path-in-sub-atom)] (cond (and s-atom? path-in-sub-atom path-coll?-in-sub-atom) (swap! s update-in path-in-sub-atom f) (and s-atom? path-in-sub-atom) (swap! s update path-in-sub-atom f) s-atom? (swap! s f) path-coll? (swap! state update-in path f) :else (swap! state update path f))) nil) ;; 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 @state)) (defn get-current-route [] (get-in (get-route-match) [:data :name])) (defn home? [] (= :home (get-current-route))) (defn whiteboard-dashboard? [] (= :whiteboards (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 @state)) (defn get-remote-file-graphs [] (get-in @state [:file-sync/remote-graphs :graphs])) (defn get-rtc-graphs [] (:rtc/graphs @state)) (defn get-remote-graph-info-by-uuid [uuid] (when-let [graphs (seq (get-in @state [:file-sync/remote-graphs :graphs]))] (some #(when (= (:GraphUUID %) (str uuid)) %) graphs))) (defn get-remote-graph-usage [] (when-let [graphs (seq (get-in @state [:file-sync/remote-graphs :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 delete-remote-graph! [repo] (let [remove-repo! (fn [repos] (remove #(and (:GraphUUID repo) (:GraphUUID %) (= (:GraphUUID repo) (:GraphUUID %))) repos))] (if (:rtc-graph? repo) (swap! state update :rtc/graphs remove-repo!) (swap! state update-in [:file-sync/remote-graphs :graphs] remove-repo!)))) (defn add-remote-graph! [repo] (swap! state update-in [:file-sync/remote-graphs :graphs] (fn [repos] (->> (conj repos repo) (distinct))))) (defn get-repos [] (get-in @state [: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] (swap! state assoc :git/current-repo repo) (reset! flows/*current-repo repo) (if repo (storage/set :git/current-repo repo) (storage/remove :git/current-repo)) (ipc/ipc "setCurrentGraph" repo)) (defn set-preferred-format! [format] (swap! state assoc-in [:me :preferred_format] (name format))) (defn set-preferred-workflow! [workflow] (swap! state assoc-in [:me :preferred_workflow] (name workflow))) (defn set-preferred-language! [language] (set-state! :preferred-language (name language)) (storage/set :preferred-language (name language))) (defn delete-repo! [repo] (swap! state update-in [:me :repos] (fn [repos] (->> (remove #(or (= (:url repo) (:url %)) (and (:GraphUUID repo) (:GraphUUID %) (= (:GraphUUID repo) (:GraphUUID %)))) repos) (util/distinct-by :url))))) (defn set-timestamp-block! [value] (set-state! :editor/set-timestamp-block value)) (defn get-timestamp-block [] @(:editor/set-timestamp-block @state)) (defn get-edit-block [] @(get @state :editor/block)) (defn set-edit-content! ([input-id value] (set-edit-content! input-id value true)) ([input-id value set-input-value?] (when input-id (when set-input-value? (when-let [input (gdom/getElement input-id)] (util/set-change-value input value))) (set-state! :editor/content value :path-in-sub-atom (or (:block/uuid (get-edit-block)) input-id))))) (defn editing? [] (seq @(:editor/editing? @state))) (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 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 @(:editor/content @state) id))) (defn sub-edit-content ([] (sub-edit-content (:block/uuid (get-edit-block)))) ([block-id] (when block-id (sub :editor/content {:path-in-sub-atom block-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 [id (str "ls-block-" (:block/uuid edit-block))] (set-selection-start-block! id) id)))) (defn get-cursor-range [] @(:editor/cursor-range @state)) (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 [] @(:editor/action @state)) (defn get-editor-action-data [] (:editor/action-data @state)) (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 [] @(:selection/direction @state)) (defn get-unsorted-selection-blocks [] @(:selection/blocks @state)) (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 sub-block-selected? [block-id] (assert (uuid? block-id)) (rum/react (r/cached-derived-atom (:selection/blocks @state) [(get-current-repo) ::ui-selected block-id] (fn [blocks] (some #{block-id} (get-selected-block-ids blocks)))))) (defn dom-clear-selection! [] (doseq [node (dom/by-class "ls-block selected")] (dom/remove-class! node "selected"))) (defn mark-dom-blocks-as-selected [nodes] (doseq [node nodes] (dom/add-class! node "selected"))) (defn get-events-chan [] (:system/events @state)) (defn pub-event! {:malli/schema [:=> [:cat vector?] :any]} [payload] (let [d (p/deferred) chan (get-events-chan)] (async/put! chan [payload d]) d)) (defn- set-selection-blocks-aux! [blocks] (set-state! :view/selected-blocks nil) (let [selected-ids (set (get-selected-block-ids @(:selection/blocks @state))) _ (set-state! :selection/blocks blocks) new-ids (set (get-selection-block-ids)) removed (set/difference selected-ids new-ids)] (mark-dom-blocks-as-selected blocks) (doseq [id removed] (doseq [node (array-seq (gdom/getElementsByClass (str "id" id)))] (dom/remove-class! node "selected"))))) (defn set-selection-blocks! ([blocks] (set-selection-blocks! blocks nil)) ([blocks direction] (when (seq blocks) (let [blocks (vec (remove nil? blocks))] (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 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 #(= block %) (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 @(:selection/blocks @state) 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)) (defn toggle-sidebar-open?! [] (swap! state update :ui/sidebar-open? not)) (defn open-right-sidebar! [] (swap! state assoc :ui/sidebar-open? true)) (defn hide-right-sidebar! [] (swap! state assoc :ui/sidebar-open? false)) (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 @state)) (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 @state)) (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 @state))) (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 @state))) 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 @state))] (doseq [item items] (set-state! [:ui/sidebar-collapsed-blocks item] collapsed?)))) (defn clear-editor-last-pos! [] (set-state! :editor/last-saved-cursor {})) (defn clear-cursor-range! [] (set-state! :editor/cursor-range nil)) (defn clear-edit! [& {:keys [clear-editing-block?] :or {clear-editing-block? true}}] (clear-editor-action!) (when clear-editing-block? (set-state! :editor/editing? {}) (set-state! :editor/block nil)) (set-state! :editor/start-pos nil) (clear-editor-last-pos!) (clear-cursor-range!) (set-state! :editor/content {}) (set-state! :ui/select-query-cache {}) (set-state! :editor/block-refs #{}) (set-state! :editor/action-data nil) (set-state! :view/selected-blocks nil)) (defn into-code-editor-mode! [] (set-state! :editor/cursor-range nil) (swap! state assoc :editor/code-mode? true)) (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 @(:editor/last-saved-cursor @state) (: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) (set-editor-last-pos! new-pos))) (defn set-theme-mode! [mode] (when (mobile-util/native-platform?) (if (= mode "light") (util/set-theme-light) (util/set-theme-dark))) (set-state! :ui/theme mode) (storage/set :ui/theme mode)) (defn sync-system-theme! [] (let [system-dark? (.-matches (js/window.matchMedia "(prefers-color-scheme: dark)"))] (set-theme-mode! (if system-dark? "dark" "light")) (set-state! :ui/system-theme? true) (storage/set :ui/system-theme? true))) (defn use-theme-mode! [theme-mode] (if (= theme-mode "system") (sync-system-theme!) (do (set-theme-mode! theme-mode) (set-state! :ui/system-theme? false) (storage/set :ui/system-theme? false)))) (defn- toggle-theme [theme] (if (= theme "dark") "light" "dark")) (defn toggle-theme! [] (use-theme-mode! (toggle-theme (:ui/theme @state)))) (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 @state)))) (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 (and (not system-theme?) (mobile-util/native-platform?)) (if (= mode "light") (util/set-theme-light) (util/set-theme-dark))))) (defn set-editing-block-dom-id! [block-dom-id] (set-state! :editor/block-dom-id block-dom-id)) (defn get-editing-block-dom-id [] @(:editor/block-dom-id @state)) (defn set-root-component! [component] (set-state! :ui/root-component component)) (defn get-root-component [] (get @state :ui/root-component)) (defn load-app-user-cfgs ([] (load-app-user-cfgs false)) ([refresh?] (when (util/electron?) (p/let [cfgs (if (or refresh? (nil? (:electron/user-cfgs @state))) (ipc/ipc :userAppCfgs) (:electron/user-cfgs @state)) cfgs (if (object? cfgs) (bean/->clj cfgs) cfgs)] (set-state! :electron/user-cfgs cfgs))))) (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 set-file-component! [component] (set-state! :ui/file-component component)) (defn clear-file-component! [] (set-state! :ui/file-component nil)) (defn set-journals-length! [value] (when value (set-state! :journals-length value))) (defn save-scroll-position! ([value] (save-scroll-position! value js/window.location.hash)) ([value path] (set-state! :ui/paths-scroll-positions value :path-in-sub-atom path))) (defn save-main-container-position! [value] (when (not= value @(:ui/main-container-scroll-top @state)) (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 @state)) (defn set-db-restoring! [value] (set-state! :db/restoring? value)) (defn set-indexedb-support! [value] (set-state! :indexeddb/support? value)) (defn modal-opened? [] (shui-dialog/has-modal?)) (defn close-modal! [] (shui/dialog-close!)) (defn get-reactive-custom-queries-chan [] (:reactive/custom-queries @state)) (defn get-left-sidebar-open? [] (get-in @state [: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? @state)) (defn get-notification-contents [] (get @state :notification/contents)) (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 @(:ui/toggle-highlight-recent-blocks? @state)] (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? @state)) (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 @state))))) (defn get-plugins-commands-with-type [type] (->> (apply concat (vals (:plugin/simple-commands @state))) (filterv #(= (keyword (first %)) (keyword type))))) (defn get-plugins-ui-items-with-type [type] (->> (apply concat (vals (:plugin/installed-ui-items @state))) (filterv #(= (keyword (first %)) (keyword type))))) (defn get-plugin-resources-with-type [pid type] (when-let [pid (and type (keyword pid))] (get-in @state [:plugin/installed-resources pid (keyword type)]))) (defn get-plugin-resource [pid type key] (when-let [resources (get-plugin-resources-with-type pid type)] (get resources key))) (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 @state))] (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 @state) 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 @state))] (mapcat (fn [s] (filter #(= (keyword type) (:type %)) s)) installed))) (defn get-all-plugin-search-engines [] (:search/engines @state)) (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 @state [: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 @state [: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 (sub :plugin/installed-hooks)] (contains? hooks (str "hook:editor:slot_" type))))) (defn active-tldraw-app [] (when-let [tldraw-el (.querySelector js/document.body ".logseq-tldraw[data-tlapp]")] (gobj/get js/window.tlapps (.. tldraw-el -dataset -tlapp)))) (defn tldraw-editing-logseq-block? [] (when-let [app (active-tldraw-app)] (and (= 1 (.. app -selectedShapesArray -length)) (= (.. app -editingShape) (.. app -selectedShapesArray (at 0)))))) (defn set-graph-syncing? [value] (set-state! :graph/syncing? value)) (defn set-editor-in-composition! [value] (set-state! :editor/in-composition? value)) (defn editor-in-composition? [] (:editor/in-composition? @state)) (defn set-loading-files! [repo value] (when repo (set-state! [:repo/loading-files? repo] value))) (defn loading-files? [repo] (get-in @state [:repo/loading-files? repo])) (defn set-editor-last-input-time! [repo time] (set-state! :editor/last-input-time time :path-in-sub-atom 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 ;; Is this a good idea to put whiteboard check here? (not (get-edit-input-id)))))) (defn set-nfs-refreshing! [value] (set-state! :nfs/refreshing? value)) (defn nfs-refreshing? [] (:nfs/refreshing? @state)) (defn set-search-result! [value] (set-state! :search/result value)) (defn clear-search-result! [] (set-search-result! nil)) (defn add-graph-search-filter! [q] (when-not (string/blank? q) (update-state! :search/graph-filters (fn [value] (vec (distinct (conj value q))))))) (defn remove-search-filter! [q] (when-not (string/blank? q) (update-state! :search/graph-filters (fn [value] (remove #{q} value))))) (defn clear-search-filters! [] (set-state! :search/graph-filters [])) (defn get-search-mode [] (:search/mode @state)) (defn toggle! [path] (update-state! path not)) (defn toggle-settings! [] (toggle! :ui/settings-open?)) (defn settings-open? [] (:ui/settings-open? @state)) (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?)) (let [page (and (sqlite-util/db-based-graph? repo) (= :page block-type) (some-> (db-conn-state/get-conn repo) deref (d/entity db-id)))] (if (and page ;; TODO: Use config/dev? when it's not a circular dep (not goog.DEBUG) (or (ldb/hidden? 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 [] (:copy/export-block-text-indent-style @state)) (defn set-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 @state [: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 @state))) (defn get-export-block-text-remove-options [] (:copy/export-block-text-remove-options @state)) (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 @state)) (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 editing-whiteboard-portal? [] (and (active-tldraw-app) (tldraw-editing-logseq-block?))) (defn block-component-editing? [] (and (:block/component-editing-mode? @state) (not (editing-whiteboard-portal?)))) (defn set-block-component-editing-mode! [value] (set-state! :block/component-editing-mode? value)) (defn get-editor-args [] @(:editor/args @state)) (defn set-page-blocks-cp! [value] (set-state! [:view/components :page-blocks] value)) (defn get-page-blocks-cp [] (get-in @state [: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 @state [: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 [db move-cursor? container-id property-block direction event pos] :or {move-cursor? true}}] (when-not (exists? js/process) (if (> (count content) (block-content-max-length (get-current-repo))) (let [elements (array-seq (js/document.getElementsByClassName (str "id" (:block/uuid block))))] (when (first elements) (util/scroll-to-element (gobj/get (first elements) "id"))) (exit-editing-and-set-selected-blocks! elements)) (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 ""))] (assert (and container-id (:block/uuid block)) "container-id or block uuid is missing") (set-state! :editor/block-refs #{}) (if property-block (set-editing-block-id! [container-id (:block/uuid property-block) (:block/uuid block)]) (set-editing-block-id! [container-id (:block/uuid block)])) (set-state! :editor/container-id container-id) (set-state! :editor/block block) (set-state! :editor/content content :path-in-sub-atom (:block/uuid block)) (set-state! :editor/last-key-code nil) (set-state! :editor/set-timestamp-block nil) (set-state! :editor/cursor-range cursor-range) (when (= :code (:logseq.property.node/display-type (d/entity db (:db/id 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)) (when (or (util/mobile?) (mobile-util/native-platform?)) (set-state! :mobile/show-action-bar? false))))))))) (defn action-bar-open? [] (:mobile/show-action-bar? @state)) (defn get-git-auto-commit-enabled? [] (false? (sub [:electron/user-cfgs :git/disable-auto-commit?]))) (defn get-git-commit-on-close-enabled? [] (sub [:electron/user-cfgs :git/commit-on-close?])) (defn set-last-key-code! [key-code] (set-state! :editor/last-key-code key-code)) (defn get-last-key-code [] @(:editor/last-key-code @state)) (defn set-ui-last-key-code! [key-code] (set-state! :ui/global-last-key-code key-code)) (defn get-ui-last-key-code [] @(:ui/global-last-key-code @state)) (defn set-block-op-type! [op-type] (set-state! :editor/block-op-type op-type)) (defn get-block-op-type [] (:editor/block-op-type @state)) (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 @state [: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 @state))))) (defn lsp-enabled?-or-theme [] (:plugin/enabled @state)) (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 @state)))] (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 @state) (keyword id)))] (coming-update-new-version? pkg))) (defn all-available-coming-updates ([] (all-available-coming-updates (:plugin/updates-coming @state))) ([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 @state)] (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 sub-right-sidebar-blocks [] (when-let [current-repo (get-current-repo)] (->> (sub :sidebar/blocks) (filter #(= (first %) current-repo))))) (defn toggle-collapsed-block! [block-id] (let [current-repo (get-current-repo)] (update-state! [:ui/collapsed-blocks current-repo block-id] not))) (defn set-collapsed-block! [block-id value] (let [current-repo (get-current-repo)] (set-state! [:ui/collapsed-blocks current-repo block-id] value))) (defn sub-block-collapsed [block-id] (sub [:ui/collapsed-blocks (get-current-repo) block-id])) (defn get-block-collapsed [block-id] (get-in @state [:ui/collapsed-blocks (get-current-repo) block-id])) (defn get-modal-id [] (shui-dialog/get-last-modal-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 [] (sub :auth/id-token)) (defn get-auth-refresh-token [] (:auth/refresh-token @state)) (defn set-file-sync-manager [graph-uuid v] (when (and graph-uuid v) (set-state! [:file-sync/graph-state graph-uuid :file-sync/sync-manager] v))) (defn get-file-sync-manager [graph-uuid] (get-in @state [:file-sync/graph-state graph-uuid :file-sync/sync-manager])) (defn clear-file-sync-state! [graph-uuid] (set-state! [:file-sync/graph-state graph-uuid] nil)) (defn clear-file-sync-progress! [graph-uuid] (set-state! [:file-sync/graph-state graph-uuid :file-sync/progress] nil)) (defn set-file-sync-state [graph-uuid v] (when v (s/assert :frontend.fs.sync/sync-state v)) (set-state! [:file-sync/graph-state graph-uuid :file-sync/sync-state] v)) (defn get-current-file-sync-graph-uuid [] (get-in @state [:file-sync/graph-state :current-graph-uuid])) (defn sub-current-file-sync-graph-uuid [] (sub [:file-sync/graph-state :current-graph-uuid])) (defn get-file-sync-state ([] (get-file-sync-state (get-current-file-sync-graph-uuid))) ([graph-uuid] (get-in @state [:file-sync/graph-state graph-uuid :file-sync/sync-state]))) (defn sub-file-sync-state [graph-uuid] (sub [:file-sync/graph-state graph-uuid :file-sync/sync-state])) (defn reset-parsing-state! [] (set-state! [:graph/parsing-state (get-current-repo)] {})) (defn set-parsing-state! [m] (update-state! [:graph/parsing-state (get-current-repo)] (if (fn? m) m (fn [old-value] (merge old-value m))))) (defn http-proxy-enabled-or-val? [] (when-let [{:keys [type protocol host port] :as agent-opts} (sub [:electron/user-cfgs :settings/agent])] (when (and (not (contains? #{"system"} type)) (every? not-empty (vals agent-opts))) (str protocol "://" host ":" port)))) (defn set-mobile-app-state-change [is-active?] (set-state! :mobile/app-state-change {:is-active? is-active? :timestamp (inst-ms (js/Date.))})) (defn get-sync-graph-by-id [graph-uuid] (when graph-uuid (let [graph (first (filter #(= graph-uuid (:GraphUUID %)) (get-repos)))] (when (:url graph) graph)))) (defn unlinked-dir? [dir] (contains? (:file/unlinked-dirs @state) dir)) (defn get-file-rename-event-chan [] (:file/rename-event-chan @state)) (defn offer-file-rename-event-chan! [v] {:pre [(map? v) (= #{:repo :old-path :new-path} (set (keys v)))]} (async/offer! (get-file-rename-event-chan) v)) (defn set-onboarding-whiteboard! [v] (set-state! :whiteboard/onboarding-whiteboard? v) (storage/set :ls-onboarding-whiteboard? v)) (defn get-onboarding-whiteboard? [] (get-in @state [:whiteboard/onboarding-whiteboard?])) (defn get-local-container-root-url [] (when (mobile-util/native-ios?) (get-in @state [:mobile/container-urls :localContainerUrl]))) (defn get-icloud-container-root-url [] (when (mobile-util/native-ios?) (get-in @state [:mobile/container-urls :iCloudContainerUrl]))) (defn get-current-pdf [] (:pdf/current @state)) (defn nfs-user-granted? [repo] (get-in @state [:nfs/user-granted? repo])) (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 focus-whiteboard-shape ([shape-id] (focus-whiteboard-shape (active-tldraw-app) shape-id)) ([tln shape-id] (when-let [^js api (gobj/get tln "api")] (when (and shape-id (parse-uuid shape-id)) (. api selectShapes shape-id) (. api zoomToSelection))))) (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 get-user-info [] (sub :user/info)) (defn clear-user-info! [] (storage/remove :user-groups)) (defn sub-async-query-loading [k] (assert (or (string? k) (uuid? k))) (let [k* (str k)] (rum/react (r/cached-derived-atom (:db/async-query-loading @state) [(get-current-repo) ::async-query k*] (fn [s] (contains? s k*)))))) (defn clear-async-query-state! [] (reset! (:db/async-query-loading @state) #{})) (defn set-color-accent! [color] (swap! state assoc :ui/radix-color color) (storage/set :ui/radix-color color) (util/set-android-theme)) (defn set-editor-font! [font] (let [font (if (keyword? font) (name font) (str font))] (swap! state assoc :ui/editor-font font) (storage/set :ui/editor-font font))) (defn handbook-open? [] (:ui/handbooks-open? @state)) (defn get-handbook-route-chan [] (:handbook/route-chan @state)) (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)) (def get-worker-next-request-id db-transact/get-next-request-id) (def add-worker-request! db-transact/add-request!) (defn get-next-container-id [] (swap! (:ui/container-id @state) inc)) (defn get-container-id "Either cached container-id or a new id" [key] (if (seq key) (or (get @(:ui/cached-key->container-id @state) key) (let [id (get-next-container-id)] (swap! (:ui/cached-key->container-id @state) assoc key id) id)) (get-next-container-id))) (defn get-current-editor-container-id [] @(:editor/container-id @state)) (comment (defn remove-container-key! [key] (swap! (:ui/cached-key->container-id @state) dissoc key))) (defn get-editor-info [] (when-let [edit-block (get-edit-block)] {:block-uuid (:block/uuid edit-block) :container-id (or @(:editor/container-id @state) :unknown-container) :start-pos @(:editor/start-pos @state) :end-pos (get-edit-pos)})) (defn conj-block-ref! [ref-entity] (let [refs! (:editor/block-refs @state)] (swap! refs! conj ref-entity))) (defn get-highlight-recent-days [] @(:ui/highlight-recent-days @state)) (defn set-highlight-recent-days! [days] (prn :debug :set :days days) (reset! (:ui/highlight-recent-days @state) days) (storage/set :ui/highlight-recent-days days)) (defn set-rtc-enabled! [value] (storage/set :logseq-rtc-enabled value) (set-state! :feature/enable-rtc? value))