(ns frontend.handler.events.ui "UI events" (:require-macros [frontend.handler.events.macros :refer [defevent!]]) (:require [clojure.core.async :as async] [frontend.components.assets :as assets] [frontend.components.cmdk.core :as cmdk] [frontend.components.icon :as icon-component] [frontend.components.page :as component-page] [frontend.components.page-menu :as page-menu] [frontend.components.plugins :as plugin] [frontend.components.property.dialog :as property-dialog] [frontend.components.quick-add :as quick-add] [frontend.components.repo :as repo] [frontend.components.select :as select] [frontend.components.selection :as selection] [frontend.components.settings :as settings] [frontend.components.shell :as shell] [frontend.components.user.login :as login] [frontend.config :as config] [frontend.context.i18n :refer [t]] [frontend.db.async :as db-async] [frontend.extensions.fsrs :as fsrs] [frontend.extensions.lightbox :as lightbox] [frontend.extensions.pdf.assets :as pdf-assets] [frontend.fs :as fs] [frontend.handler.assets :as assets-handler] [frontend.handler.editor :as editor-handler] [frontend.handler.events :as events] [frontend.handler.notification :as notification] [frontend.handler.page :as page-handler] [frontend.handler.plugin :as plugin-handler] [frontend.handler.reaction :as reaction-handler] [frontend.handler.repo :as repo-handler] [frontend.handler.route :as route-handler] [frontend.handler.user :as user-handler] [frontend.mobile.util :as mobile-util] [frontend.modules.instrumentation.sentry :as sentry-event] [frontend.state :as state] [frontend.util :as util] [frontend.util.entity :as entity] [goog.dom :as gdom] [lambdaisland.glogi :as log] [logseq.common.config :as common-config] [logseq.common.path :as path] [logseq.common.util :as common-util] [logseq.shui.ui :as shui] [promesa.core :as p] [logseq.shui.dialog.core :as shui-dialog])) (defevent! :go/search [_] (when-not (editor-handler/dialog-exists? :ls-dialog-cmdk) (shui/dialog-open! cmdk/cmdk-modal {:id :ls-dialog-cmdk :align :top :content-props {:class "ls-dialog-cmdk"} :close-btn? false :onEscapeKeyDown (fn [e] (.preventDefault e))}))) (defevent! :notification/show [[_ {:keys [content status clear?]}]] (notification/show! content status clear?)) (defevent! :shell/run [_] (when (util/electron?) (shui/dialog-open! shell/shell))) (defevent! :go/plugins [_] (plugin/open-plugins-modal!)) (defevent! :go/plugins-waiting-lists [_] (plugin/open-waiting-updates-modal!)) (defevent! :go/plugins-from-file [[_ plugins]] (plugin/open-plugins-from-file-modal! plugins)) (defevent! :go/install-plugin-from-github [[_]] (shui/dialog-open! (plugin/install-from-github-release-container))) (defevent! :go/plugins-settings [[_ pid nav? title]] (when pid (state/set-state! :plugin/focused-settings pid) (state/set-state! :plugin/navs-settings? (not (false? nav?))) (plugin/open-focused-settings-modal! title))) (defevent! :go/proxy-settings [[_ agent-opts]] (shui/dialog-open! (plugin/user-proxy-settings-container agent-opts) {:id :https-proxy-panel :center? true :class "lg:max-w-2xl"})) (defevent! :go/db-worker-server-settings [[_]] (shui/dialog-open! (settings/db-worker-server-settings-container) {:id :db-worker-server-panel :center? true :class "lg:max-w-2xl"})) (defevent! :go/publish-server-settings [[_]] (shui/dialog-open! (settings/publish-server-url-settings-container) {:id :publish-server-panel :center? true :class "lg:max-w-2xl"})) (defevent! :redirect-to-home [_] (page-handler/create-today-journal!) (when (util/capacitor?) (state/pub-event! [:mobile/set-tab "home"]))) (defevent! :page/show-delete-dialog [[_ selected-rows ok-handler]] (shui/dialog-open! (component-page/batch-delete-dialog selected-rows ok-handler))) (defevent! :modal/show-cards [[_ cards-id]] (shui/dialog-open! (fn [] (fsrs/cards-view cards-id nil)) {:id :srs :label :flashcards__cp})) (defevent! :modal/show-themes-modal [[_ classic?]] (if classic? (plugin/open-select-theme!) (route-handler/go-to-search! :themes))) (defn- " error-code) :error))))) (if (and updated? downloading?) ;; try to start consume downloading item (if-let [next-coming (state/get-next-selected-coming-update)] (plugin-handler/check-or-update-marketplace-plugin! (assoc next-coming :only-check false :error-code nil) (fn [^js e] (js/console.error "[Download Err]" next-coming e))) (plugin-handler/close-updates-downloading)) ;; try to start consume pending item (if-let [next-pending (second (first (:plugin/updates-pending (state/get-state))))] (do (println "Updates: take next pending - " (:id next-pending)) (js/setTimeout #(plugin-handler/check-or-update-marketplace-plugin! (assoc next-pending :only-check true :auto-check auto-checking? :error-code nil) (fn [^js e] (notification/show! (.toString e) :error) (js/console.error "[Check Err]" next-pending e))) 500)) ;; try to open waiting updates list (do (when (and prev-pending? (not auto-checking?) (seq (state/all-available-coming-updates))) (plugin/open-waiting-updates-modal!)) (plugin-handler/set-auto-checking! false)))))) (defevent! :plugin/loader-perf-tip [[_ {:keys [^js o _s _e]}]] (when-let [opts (.-options o)] (notification/show! (plugin/perf-tip-content (.-id o) (.-name opts) (.-url opts)) :warning false (.-id o)))) (defn- = (count content) pos) (>= pos 2) (= (util/nth-safe content (dec pos)) (util/nth-safe content (- pos 2)) ";")) [(str (common-util/safe-subs content 0 (- pos 2)) (common-util/safe-subs content pos)) (- pos 2)] :else [nil pos])] (when content' (if editing-default-property? (editor-handler/save-block! (state/get-current-repo) (:block/uuid editing-block) content') (editor-handler/edit-block! editing-block (or pos :max) (cond-> {} content' (assoc :custom-content content')))))))))) (defn- editor-new-property [block target {:keys [selected-blocks popup-id editing-block editing-pos editing-target] :as opts}] (p/let [editing-block (if editing-block (p/resolved editing-block) ( (state/get-edit-block) :block/uuid))) selected-blocks (cond editing-block (p/resolved nil) (seq selected-blocks) (p/resolved selected-blocks) :else ( opts editing-block (assoc :original-block editing-block :edit-original-block #(edit-original-block! editing-block pos %)))] (when (seq blocks) (let [target' (or target editing-target (some-> (state/get-edit-input-id) (gdom/getElement)) (first (state/get-selection-blocks)))] (if target' (shui/popup-show! target' #(property-dialog/dialog blocks opts') (cond-> {:align "start" :force-popover? true} popup-id (assoc :id popup-id))) (shui/dialog-open! #(property-dialog/dialog blocks opts') {:id :property-dialog :align "start"}))))))) (defevent! :editor/new-property [[_ {:keys [block target] :as opts}]] (when-not config/publishing? (p/let [editing-block ( (state/get-edit-block) :block/uuid)) editing-target (some-> (state/get-edit-input-id) (gdom/getElement)) opts' (cond-> opts editing-block (assoc :editing-block editing-block :editing-pos (state/get-edit-pos)) editing-target (assoc :editing-target editing-target))] (p/do! (editor-handler/save-current-block!) (editor-new-property block target opts'))))) (defn- reaction-target-block-ids [blocks] (let [blocks' (cond (nil? blocks) nil (sequential? blocks) blocks :else [blocks])] (vec (or (seq (keep :block/uuid blocks')) (some-> (state/get-edit-block) :block/uuid vector) (seq (state/get-selection-block-ids)))))) (defn- editor-new-reaction [blocks target] (let [target-block-ids (reaction-target-block-ids blocks) target' (or target (some-> (state/get-edit-input-id) (gdom/getElement)) (first (state/get-selection-blocks))) on-pick (fn [popup-id icon] (let [emoji-id (:id icon) emoji? (= :emoji (:type icon))] (if emoji? (do (doseq [target-block-id target-block-ids] (reaction-handler/toggle-reaction! target-block-id emoji-id)) (shui/popup-hide! popup-id)) (notification/show! (t :block.reaction/emoji-required-warning) :warning))))] (when (and (seq target-block-ids) target') (shui/popup-hide! :selection-action-bar) (shui/popup-show! target' (fn [{:keys [id]}] (icon-component/icon-search {:on-chosen (fn [_e icon _keep-popup?] (on-pick id icon)) :tabs [[:emoji (t :icon/tab-emojis)]] :default-tab :emoji :show-used? true :icon-value nil})) {:align :start :content-props {:class "ls-icon-picker"}})))) (defevent! :editor/new-reaction [[_ {:keys [block blocks target]}]] (when-not config/publishing? (p/do! (editor-handler/save-current-block!) (editor-new-reaction (if (seq blocks) blocks block) target)))) (defevent! :graph/new-db-graph [[_ _opts]] (shui/dialog-open! repo/new-db-graph {:id :new-db-graph :title (t :graph/create-new) :align (if (util/mobile?) :top :center) :style {:max-width "500px"}})) (defevent! :dialog-select/graph-open [] (select/dialog-select! :graph-open)) (defevent! :dialog-select/graph-remove [] (select/dialog-select! :graph-remove)) (defevent! :dialog-select/db-graph-replace [] (select/dialog-select! :db-graph-replace)) (defn- hide-action-bar! [] (when (editor-handler/popup-exists? :selection-action-bar) (shui/popup-hide! :selection-action-bar))) (defevent! :editor/show-action-bar [] (let [selection (state/get-selection-blocks) first-visible-block (some #(when (util/el-visible-in-viewport? % true) %) selection)] (when (and first-visible-block (not (shui-dialog/has-dialog?))) (hide-action-bar!) (shui/popup-show! first-visible-block (fn [] (selection/action-bar)) {:id :selection-action-bar :focus-trigger? false :force-popover? true :root-props {:modal false} :content-props {:side "top" :onCloseAutoFocus #(.preventDefault %) :class "!w-max !max-w-none !overflow-visible !py-0 !px-0 !border-none"} :auto-side? false :align :start})))) (defevent! :editor/hide-action-bar [] (hide-action-bar!) (state/set-state! :mobile/show-action-bar? false)) (defevent! :user/logout [[_]] (login/sign-out!)) (defevent! :user/login [[_]] (if (mobile-util/native-platform?) (route-handler/redirect! {:to :user-login}) (login/open-login-modal!))) (defevent! :asset/dialog-edit-external-url [[_ asset-block pdf-current]] (shui/dialog-open! (assets/edit-external-url-content asset-block pdf-current) {:id :edit-external-asset-source-dialog :title (if asset-block (t :asset/edit-title) (t :asset/create-title)) :center? true})) (defevent! :asset/show-preview [[_ asset]] (when-let [asset-type-str (:logseq.property.asset/type asset)] (let [asset-type (keyword asset-type-str) image? (contains? (common-config/img-formats) asset-type) video? (contains? config/video-formats asset-type) pdf? (= :pdf asset-type) file-name (str (:block/uuid asset) "." asset-type-str) ;; Prefer external-url so plugin-sandboxed assets resolve to their ;; real on-disk path; mirrors the asset-cp render-side fix. rel-path (or (:logseq.property.asset/external-url asset) (path/path-join (str "../" common-config/local-assets-dir) file-name))] (cond image? (p/let [url (assets-handler/