瀏覽代碼

Enhance/marketplace (#3686)

Marketplace enhancement

Nested modal support
Charlie 3 年之前
父節點
當前提交
3ea7635daa

+ 1 - 0
e2e-tests/basic.spec.ts

@@ -7,6 +7,7 @@ import { randomString, createRandomPage, newBlock } from './utils'
 
 test('render app', async ({ page }) => {
   // NOTE: part of app startup tests is moved to `fixtures.ts`.
+  await page.waitForFunction('window.document.title != "Loading"')
 
   expect(await page.title()).toMatch(/^Logseq.*?/)
 })

+ 3 - 1
libs/src/LSPlugin.core.ts

@@ -152,6 +152,7 @@ type PluginLocalOptions = {
   settings?: PluginSettings
   logger?: PluginLogger
   effect?: boolean
+  theme?: boolean
 
   [key: string]: any
 }
@@ -488,7 +489,7 @@ class PluginLocal
 
       // Pick legal attrs
     ;['name', 'author', 'repository', 'version',
-      'description', 'repo', 'title', 'effect',
+      'description', 'repo', 'title', 'effect', 'sponsors'
     ].concat(!this.isInstalledInDotRoot ? ['devEntry'] : []).forEach(k => {
       this._options[k] = pkg[k]
     })
@@ -512,6 +513,7 @@ class PluginLocal
     this._options.title = title
     this._options.icon = icon &&
       this._resolveResourceFullUrl(icon)
+    this._options.theme = Boolean(logseq.theme || !!logseq.themes)
 
     // TODO: strategy for Logseq plugins center
     if (this.isInstalledInDotRoot) {

文件差異過大導致無法顯示
+ 0 - 0
resources/js/lsplugin.core.js


+ 4 - 0
src/electron/electron/handler.cljs

@@ -351,6 +351,10 @@
         windows (filter #(.isVisible %) windows)]
     (> (count windows) 1)))
 
+(defmethod handle :reloadWindowPage [^js win]
+  (when-let [web-content (.-webContents win)]
+    (.reload web-content)))
+
 (defmethod handle :default [args]
   (println "Error: no ipc handler for: " (bean/->js args)))
 

+ 29 - 17
src/electron/electron/plugin.cljs

@@ -12,7 +12,7 @@
             [electron.utils :refer [*win fetch extract-zip] :as utils]))
 
 ;; update & install
-(def *installing-or-updating (atom nil))
+;;(def *installing-or-updating (atom nil))
 (def debug (fn [& args] (apply (.-info logger) (conj args "[Marketplace]"))))
 (def emit (fn [type payload]
             (doseq [^js win (get-all-windows)]
@@ -30,7 +30,7 @@
             endpoint (api "releases/latest")
             ^js res (fetch endpoint)
             res (.json res)
-            _ (js/console.debug "[Release Latest] " endpoint)
+            _ (debug "[Release Latest] " endpoint)
             res (bean/->clj res)
             version (:tag_name res)
             asset (first (filter #(string/ends-with? (:name %) ".zip") (:assets res)))]
@@ -50,9 +50,9 @@
   [repo tag])
 
 (defn download-asset-zip
-  [{:keys [id repo title author description effect]} url dot-extract-to]
+  [{:keys [id repo title author description effect sponsors]} dl-url dl-version dot-extract-to]
   (p/catch
-    (p/let [^js res (fetch url)
+    (p/let [^js res (fetch dl-url #js {:timeout 30000})
             _ (if-not (.-ok res) (throw (js/Error. :download-network-issue)))
             frm-zip (p/create
                       (fn [resolve1 reject1]
@@ -61,7 +61,7 @@
                               total-size (js/parseInt (.get headers "content-length"))
                               start-at (.now js/Date)
                               *downloaded (atom 0)
-                              dest-basename (path/basename url)
+                              dest-basename (path/basename dl-url)
                               dest-basename (if-not (string/ends-with? dest-basename ".zip")
                                               (str id "_" dest-basename ".zip") dest-basename)
                               tmp-dest-file (path/join (os/tmpdir) (str dest-basename ".pending"))
@@ -104,12 +104,17 @@
             _ (fs/moveSync tmp-extracted-root dot-extract-to)
 
             _ (let [src (.join path dot-extract-to "package.json")
+                    ^js sponsors (bean/->js sponsors)
                     ^js pkg (fs/readJsonSync src)]
                 (set! (.-repo pkg) repo)
                 (set! (.-title pkg) title)
                 (set! (.-author pkg) author)
                 (set! (.-description pkg) description)
                 (set! (.-effect pkg) (boolean effect))
+                ;; Force overwrite version because of developers tend to
+                ;; forget to update the version number of package.json
+                (when dl-version (set! (.-version pkg) dl-version))
+                (when sponsors (set! (.-sponsors pkg) sponsors))
                 (fs/writeJsonSync src pkg))
 
             _ (do
@@ -121,15 +126,15 @@
       (throw e))))
 
 (defn install-or-update!
-  [{:keys [version repo] :as item}]
-  (when (and (not @*installing-or-updating) repo)
+  [{:keys [version repo only-check] :as item}]
+  (when repo
     (let [updating? (and version (. semver valid version))]
 
-      (js/console.debug (if updating? "Updating:" "Installing:") repo)
+      (debug (if updating? "Updating:" "Installing:") repo)
 
       (-> (p/create
             (fn [resolve reject]
-              (reset! *installing-or-updating item)
+              ;;(reset! *installing-or-updating item)
               ;; get releases
               (-> (p/let [[asset latest-version] (fetch-latest-release-asset item)
 
@@ -152,28 +157,35 @@
                               (throw (js/Error. :release-asset-not-found)))
 
                           dest (.join path cfgs/dot-root "plugins" (:id item))
-                          _ (download-asset-zip item dl-url dest)
-                          _ (debug "[Updated DONE] " latest-version)]
+                          _ (if-not only-check (download-asset-zip item dl-url latest-version dest))
+                          _ (debug "[" (if only-check "Checked" "Updated") "DONE] " latest-version)]
 
                     (emit :lsp-installed
-                          {:status  :completed
-                           :payload (assoc item :zip dl-url :dst dest)})
+                          {:status     :completed
+                           :only-check only-check
+                           :payload    (if only-check
+                                         (assoc item :latest-version latest-version)
+                                         (assoc item :zip dl-url :dst dest))})
+
+                    (resolve nil))
 
-                    (resolve))
                   (p/catch
                     (fn [^js e]
                       (emit :lsp-installed
-                            {:status  :error
-                             :payload (.-message e)}))
+                            {:status     :error
+                             :only-check only-check
+                             :payload    (assoc item :error-code (.-message e))}))
                     (resolve nil)))))
 
-          (p/finally #(reset! *installing-or-updating nil))))))
+          (p/finally
+            (fn []))))))
 
 (defn uninstall!
   [id]
   (let [id (string/replace id #"^[.\/]+" "")
         plugin-path (.join path (utils/get-ls-dotdir-root) "plugins" id)
         settings-path (.join path (utils/get-ls-dotdir-root) "settings" (str id ".json"))]
+    (debug "[Uninstall]" plugin-path)
     (when (fs/pathExistsSync plugin-path)
       (fs/removeSync plugin-path)
       (fs/removeSync settings-path))))

+ 1 - 1
src/main/frontend/components/command_palette.cljs

@@ -60,7 +60,7 @@
            (cp/top-commands limit)
            (get-matched-commands commands @input limit t))
          {:item-render render-command
-          :class       "palette-results"
+          :class       "cp__palette-results"
           :on-chosen   (fn [cmd] (cp/invoke-command cmd))})]])))
 
 (rum/defc command-palette-modal < rum/reactive

+ 3 - 0
src/main/frontend/components/command_palette.css

@@ -45,5 +45,8 @@
   }
 
   &-results {
+    .tip code {
+      white-space: nowrap;
+    }
   }
 }

+ 10 - 8
src/main/frontend/components/header.cljs

@@ -82,32 +82,34 @@
       [(when-not (state/publishing-enable-editing?)
          {:title (t :settings)
           :options {:on-click state/open-settings!}
-          :icon svg/settings-sm})
+          :icon (ui/icon "settings")})
 
-       (when (and developer-mode? (util/electron?))
+       (when plugin-handler/lsp-enabled?
          {:title (t :plugins)
-          :options {:href (rfe/href :plugins)}})
+          :options {:on-click #(plugin-handler/goto-plugins-dashboard!)}
+          :icon (ui/icon "apps")})
 
-       (when developer-mode?
+       (when plugin-handler/lsp-enabled?
          {:title (t :themes)
-          :options {:on-click #(plugins/open-select-theme!)}})
+          :options {:on-click #(plugins/open-select-theme!)}
+          :icon (ui/icon "palette")})
 
        (when current-repo
          {:title (t :export-graph)
           :options {:on-click #(state/set-modal! export/export)}
-          :icon nil})
+          :icon (ui/icon "database-export")})
 
        (when current-repo
          {:title (t :import)
           :options {:href (rfe/href :import)}
-          :icon svg/import-sm})
+          :icon (ui/icon "file-upload")})
 
        {:title [:div.flex-row.flex.justify-between.items-center
                 [:span (t :join-community)]]
         :options {:href "https://discord.gg/KpN4eHY"
                   :title (t :discord-title)
                   :target "_blank"}
-        :icon svg/discord}
+        :icon (ui/icon "brand-discord")}
        (when logged?
          {:title (t :sign-out)
           :options {:on-click user-handler/sign-out!}

+ 7 - 0
src/main/frontend/components/header.css

@@ -102,6 +102,13 @@
       }
     }
   }
+
+  .dropdown-wrapper {
+    .ti {
+      margin-right: 5px;
+      opacity: .9;
+    }
+  }
 }
 
 .is-electron.is-mac .cp__header {

+ 277 - 87
src/main/frontend/components/plugins.cljs

@@ -5,6 +5,7 @@
             [frontend.context.i18n :as i18n]
             [frontend.ui :as ui]
             [frontend.handler.ui :as ui-handler]
+            [frontend.search :as search]
             [frontend.util :as util]
             [frontend.mixins :as mixins]
             [electron.ipc :as ipc]
@@ -55,24 +56,25 @@
 
       [:div.cp__themes-installed
        {:tab-index -1}
-       [:h1.mb-4.text-2xl.p-2 (t :themes)]
+       [:h1.mb-4.text-2xl.p-1 (t :themes)]
        (map-indexed
          (fn [idx opt]
            (let [current-selected (:selected opt)
                  plg (get (:plugin/installed-plugins @state/state) (keyword (:pid opt)))]
              [:div.it.flex.px-3.py-1.5.rounded-sm.justify-between
-              {:key      (:url opt)
+              {:key      (str idx (:url opt))
                :title    (if current-selected "Cancel selected theme")
                :class    (util/classnames
                            [{:is-selected current-selected
                              :is-active   (= idx @*cursor)}])
                :on-click #(do (js/LSPluginCore.selectTheme (if current-selected nil (clj->js opt)))
-                              (state/set-modal! nil))}
+                              (state/close-modal!))}
               [:section
-               [:strong.block (when plg (str (:name plg) " / ")) (:name opt)]
-               [:small.opacity-50.italic (:description opt)]]
+               [:strong.block
+                [:small.opacity-60 (str (or (:name plg) "Logseq") " • ")]
+                (:name opt)]]
               [:small.flex-shrink-0.flex.items-center.opacity-10
-               (if current-selected (svg/check 28))]]))
+               (if current-selected (ui/icon "check"))]]))
          themes)])))
 
 (rum/defc unpacked-plugin-loader
@@ -155,16 +157,23 @@
       understand the source code."]))
 
 (rum/defc plugin-item-card < rum/static
-  [{:keys [id name title settings version url description author icon usf iir repo] :as item}
-   market? installing-or-updating? installed? stat]
+  [{:keys [id name title settings version url description author icon usf iir repo sponsors] :as item}
+   market? *search-key has-other-pending?
+   installing-or-updating? installed? stat coming-update]
 
   (let [disabled (:disabled settings)
-        name (or title name "Untitled")]
+        name (or title name "Untitled")
+        unpacked? (not iir)
+        new-version (and coming-update (:latest-version coming-update))]
     (rum/with-context
       [[t] i18n/*tongue-context*]
 
       [:div.cp__plugins-item-card
-       {:class (util/classnames [{:market market?}])}
+       {:class (util/classnames
+                 [{:market          market?
+                   :installed       installed?
+                   :updating        installing-or-updating?
+                   :has-new-version new-version}])}
 
        [:div.l.link-block
         {:on-click #(plugin-handler/open-readme!
@@ -173,7 +182,7 @@
           [:img.icon {:src (if market? (plugin-handler/pkg-asset id icon) icon)}]
           svg/folder)
 
-        (when-not (or market? iir)
+        (when (and (not market?) unpacked?)
           [:span.flex.justify-center.text-xs.text-red-500.pt-2 "unpacked"])]
 
        [:div.r
@@ -187,14 +196,18 @@
          ;;[:small (js/JSON.stringify (bean/->js settings))]
          ]
 
+        ;; Author & Identity
         [:div.flag
          [:p.text-xs.pr-2.flex.justify-between
-          [:small author]
+          [:small {:on-click #(when-let [^js el (js/document.querySelector ".cp__plugins-page .search-ctls input")]
+                                (reset! *search-key (str "@" author))
+                                (.select el))} author]
           [:small {:on-click #(do
                                 (notification/show! "Copied!" :success)
                                 (util/copy-to-clipboard! id))}
            (str "ID: " id)]]]
 
+        ;; Github repo
         [:div.flag.is-top.opacity-50
          (if repo
            [:a.flex {:target "_blank"
@@ -205,13 +218,15 @@
           ;; market ctls
           [:div.ctl
            [:ul.l.flex.items-center
-            ;; downloads
-            [:li.flex.text-sm.items-center.pr-3 (svg/star 16) [:span.pl-1 (:stargazers_count stat)]]
-
             ;; stars
-            (when-let [downloads (and stat (reduce (fn [a b] (+ a (get b 2))) 0 (:releases stat)))]
+            [:li.flex.text-sm.items-center.pr-3
+             (svg/star 16) [:span.pl-1 (:stargazers_count stat)]]
+
+            ;; downloads
+            (when-let [downloads (and stat (:total_downloads stat))]
               (if (and downloads (> downloads 0))
-                [:li.flex.text-sm.items-center.pr-3 (svg/cloud-down 16) [:span.pl-1 downloads]]))]
+                [:li.flex.text-sm.items-center.pr-3
+                 (svg/cloud-down 16) [:span.pl-1 downloads]]))]
 
            [:div.r.flex.items-center
 
@@ -230,36 +245,50 @@
           [:div.ctl
            [:div.l
             [:div.de
-             [:strong (svg/settings)]
+             [:strong (ui/icon "settings")]
              [:ul.menu-list
               [:li {:on-click #(if usf (js/apis.openPath usf))} (t :plugin/open-settings)]
               [:li {:on-click #(js/apis.openPath url)} (t :plugin/open-package)]
               [:li {:on-click
                     #(let [confirm-fn
                            (ui/make-confirm-modal
-                             {:title      (str "Are you sure uninstall plugin [" name "] ?")
+                             {:title      (t :plugin/delete-alert name)
                               :on-confirm (fn [_ {:keys [close-fn]}]
                                             (close-fn)
                                             (plugin-handler/unregister-plugin id))})]
-                       (state/set-modal! confirm-fn))}
-               (t :plugin/uninstall)]]]]
+                       (state/set-sub-modal! confirm-fn {:center? true}))}
+               (t :plugin/uninstall)]]]
+
+            (when (seq sponsors)
+              [:div.de.sponsors
+               [:strong (ui/icon "coffee")]
+               [:ul.menu-list
+                (for [link sponsors]
+                  [:li [:a {:href link :target "_blank"}
+                        [:span.flex.items-center link (ui/icon "external-link")]]])]])
+            ]
 
            [:div.r.flex.items-center
-            (if (and (not iir) (not disabled))
+            (if (and unpacked? (not disabled))
               [:a.btn
                {:on-click #(js-invoke js/LSPluginCore "reload" id)}
                (t :plugin/reload)])
 
-            (if iir
-              [:a.btn
-               {:class    (util/classnames [{:disabled (or installing-or-updating?)
-                                             :updating installing-or-updating?}])
-                :on-click #(plugin-handler/update-marketplace-plugin
-                             item (fn [e] (notification/show! e :error)))}
-
-               (if installing-or-updating?
-                 (t :plugin/updating)
-                 (t :plugin/update))])
+            (when (not unpacked?)
+              [:div.updates-actions
+               [:a.btn
+                {:class    (util/classnames [{:disabled (or installing-or-updating?)}])
+                 :on-click #(if-not has-other-pending?
+                              (plugin-handler/check-or-update-marketplace-plugin
+                                (assoc item :only-check (not new-version))
+                                (fn [e] (notification/show! e :error))))}
+
+                (if installing-or-updating?
+                  (t :plugin/updating)
+                  (if new-version
+                    (str (t :plugin/update) " 👉 " new-version)
+                    (t :plugin/check-update))
+                  )]])
 
             (ui/toggle (not disabled)
                        (fn []
@@ -267,49 +296,191 @@
                          (page-handler/init-commands!))
                        true)]])]])))
 
+(rum/defc panel-control-tabs
+  < rum/static
+  [t search-key *search-key category *category
+   sort-by *sort-by selected-unpacked-pkg
+   market? develop-mode? reload-market-fn]
+
+  (let [*search-ref (rum/create-ref)]
+    [:div.mb-2.flex.justify-between.control-tabs.relative
+     [:div.flex.items-center.l
+      (category-tabs t category #(reset! *category %))
+
+      (when (and develop-mode? (not market?))
+        [:div
+         (ui/tippy {:html  [:div (t :plugin/unpacked-tips)]
+                    :arrow true}
+                   (ui/button
+                     [:span (ui/icon "upload") (t :plugin/load-unpacked)]
+                     :intent "logseq"
+                     :class "load-unpacked"
+                     :on-click plugin-handler/load-unpacked-plugin))
+
+         (unpacked-plugin-loader selected-unpacked-pkg)])]
+
+     [:div.flex.items-center.r
+
+      ;;(ui/button
+      ;;  (t :plugin/open-preferences)
+      ;;  :intent "logseq"
+      ;;  :on-click (fn []
+      ;;              (p/let [root (plugin-handler/get-ls-dotdir-root)]
+      ;;                (js/apis.openPath (str root "/preferences.json")))))
+
+      ;; search
+      [:div.search-ctls
+       [:small.absolute.s1
+        (ui/icon "search")]
+       (when-not (string/blank? search-key)
+         [:small.absolute.s2
+          {:on-click #(when-let [^js target (rum/deref *search-ref)]
+                        (reset! *search-key nil)
+                        (.focus target))}
+          (ui/icon "x")])
+       [:input.form-input.is-small
+        {:placeholder "Search plugins"
+         :ref         *search-ref
+         :on-key-down (fn [^js e]
+                        (if (= 27 (.-keyCode e))
+                          (when-not (string/blank? search-key)
+                            (util/stop e)
+                            (reset! *search-key nil))))
+         :on-change   #(let [^js target (.-target %)]
+                         (reset! *search-key (util/trim-safe (.-value target))))
+         :value       (or search-key "")}]]
+
+
+      ;; sorter
+      (ui/dropdown-with-links
+        (fn [{:keys [toggle-fn]}]
+          (ui/button
+            [:span (ui/icon "arrows-sort") ""]
+            :class "sort-by"
+            :on-click toggle-fn
+            :intent "link"))
+        (let [aim-icon #(if (= sort-by %) "check" "circle")]
+          (if market?
+            [{:title   "Downloads (Desc)"
+              :options {:on-click #(reset! *sort-by :downloads)}
+              :icon    (ui/icon (aim-icon :downloads))}
+
+             {:title   "Stars (Desc)"
+              :options {:on-click #(reset! *sort-by :stars)}
+              :icon    (ui/icon (aim-icon :stars))}
+
+             {:title   "Title (A - Z)"
+              :options {:on-click #(reset! *sort-by :letters)}
+              :icon    (ui/icon (aim-icon :letters))}]
+
+            [{:title   (t :plugin/enabled)
+              :options {:on-click #(reset! *sort-by :enabled)}
+              :icon    (ui/icon (aim-icon :enabled))}]))
+        {})
+
+      ;; more - updater
+      (ui/dropdown-with-links
+        (fn [{:keys [toggle-fn]}]
+          (ui/button
+            [:span (ui/icon "dots-vertical")]
+            :class "more-do"
+            :on-click toggle-fn
+            :intent "link"))
+
+        (concat (if market?
+                  [{:title   [:span (ui/icon "rotate-clockwise") (t :plugin/refresh-lists)]
+                    :options {:on-click #(reload-market-fn)}}]
+                  [{:title   [:span (ui/icon "rotate-clockwise") (t :plugin/check-all-updates)]
+                    :options {:on-click #(plugin-handler/check-enabled-for-updates (not= :plugins category))}}])
+                (when (state/developer-mode?)
+                  [{:hr true}
+                   {:title   [:span (ui/icon "file-code") "Open Preferences"]
+                    :options {:on-click
+                              #(p/let [root (plugin-handler/get-ls-dotdir-root)]
+                                 (js/apis.openPath (str root "/preferences.json")))}}
+                   {:title   [:span (ui/icon "bug") "Open " [:code " ~/.logseq"]]
+                    :options {:on-click
+                              #(p/let [root (plugin-handler/get-ls-dotdir-root)]
+                                 (js/apis.openPath root))}}]))
+        {})
+
+      ;; developer
+      (ui/button
+        (t :plugin/contribute)
+        :href "https://github.com/logseq/marketplace"
+        :class "contribute"
+        :intent "logseq"
+        :target "_blank")
+      ]]))
+
 (rum/defcs marketplace-plugins
   < rum/static rum/reactive
     (rum/local false ::fetching)
+    (rum/local "" ::search-key)
     (rum/local :plugins ::category)
+    (rum/local :downloads ::sort-by)                        ;; downloads / stars / letters / updates
     (rum/local nil ::error)
     {:did-mount (fn [s]
-                  (reset! (::fetching s) true)
-                  (reset! (::error s) nil)
-                  (-> (plugin-handler/load-marketplace-plugins false)
-                      (p/then #(plugin-handler/load-marketplace-stats false))
-                      (p/catch #(do (js/console.error %) (reset! (::error s) %)))
-                      (p/finally #(reset! (::fetching s) false)))
-                  s)}
+                  (let [reload-fn (fn [force-refresh?]
+                                    (when-not @(::fetching s)
+                                      (reset! (::fetching s) true)
+                                      (reset! (::error s) nil)
+                                      (-> (plugin-handler/load-marketplace-plugins force-refresh?)
+                                          (p/then #(plugin-handler/load-marketplace-stats false))
+                                          (p/catch #(do (js/console.error %) (reset! (::error s) %)))
+                                          (p/finally #(reset! (::fetching s) false)))))]
+                    (reload-fn false)
+                    (assoc s ::reload (partial reload-fn true))))}
   [state]
   (let [pkgs (state/sub :plugin/marketplace-pkgs)
         stats (state/sub :plugin/marketplace-stats)
         installed-plugins (state/sub :plugin/installed-plugins)
         installing (state/sub :plugin/installing)
         online? (state/sub :network/online?)
+        develop-mode? (state/sub :ui/developer-mode?)
+        *search-key (::search-key state)
         *category (::category state)
+        *sort-by (::sort-by state)
         *fetching (::fetching state)
         *error (::error state)
         filtered-pkgs (when (seq pkgs)
                         (if (= @*category :themes)
                           (filter #(:theme %) pkgs)
-                          (filter #(not (:theme %)) pkgs)))]
+                          (filter #(not (:theme %)) pkgs)))
+        filtered-pkgs (if-not (string/blank? @*search-key)
+                        (if-let [author (and (string/starts-with? @*search-key "@")
+                                             (subs @*search-key 1))]
+                          (filter #(= author (:author %)) filtered-pkgs)
+                          (search/fuzzy-search
+                            filtered-pkgs @*search-key
+                            :limit 30
+                            :extract-fn :title))
+                        filtered-pkgs)
+        filtered-pkgs (map #(if-let [stat (get stats (keyword (:id %)))]
+                              (let [downloads (:total_downloads stat)
+                                    stars (:stargazers_count stat)]
+                                (assoc % :stat stat
+                                         :stars stars
+                                         :downloads downloads))
+                              %) filtered-pkgs)
+        sorted-pkgs (apply sort-by
+                           (conj
+                             (case @*sort-by
+                               :letters [:title #(compare %1 %2)]
+                               [@*sort-by #(compare %2 %1)])
+                             filtered-pkgs))]
 
     (rum/with-context
       [[t] i18n/*tongue-context*]
 
       [:div.cp__plugins-marketplace
 
-       [:div.mb-4.flex.justify-between
-        [:div.flex.align-items
-         (category-tabs t @*category #(reset! *category %))]
-
-        [:div.flex.align-items
-         (ui/button
-           (t :plugin/contribute)
-           :href "https://github.com/logseq/marketplace"
-           :intent "logseq"
-           :target "_blank")
-         ]]
+       (panel-control-tabs
+         t
+         @*search-key *search-key
+         @*category *category
+         @*sort-by *sort-by nil true
+         develop-mode? (::reload state))
 
        (cond
          (not online?)
@@ -328,24 +499,45 @@
          [:div.cp__plugins-marketplace-cnt
           {:class (util/classnames [{:has-installing (boolean installing)}])}
           [:div.cp__plugins-item-lists.grid-cols-1.md:grid-cols-2.lg:grid-cols-3
-           (for [item filtered-pkgs]
+           (for [item sorted-pkgs]
              (rum/with-key
-               (let [pid (keyword (:id item))]
+               (let [pid (keyword (:id item))
+                     stat (:stat item)]
                  (plugin-item-card
-                   item true
+                   item true *search-key installing
                    (and installing (= (keyword (:id installing)) pid))
-                   (contains? installed-plugins pid)
-                   (get stats pid)))
-               (:id item)))]])])
-    ))
+                   (contains? installed-plugins pid) stat nil))
+               (:id item)))]])])))
 
 (rum/defcs installed-plugins
   < rum/static rum/reactive
+    (rum/local "" ::search-key)
+    (rum/local :enabled ::sort-by)                          ;; enabled / letters / updates
+    (rum/local :plugins ::category)
   [state]
   (let [installed-plugins (state/sub :plugin/installed-plugins)
+        installed-plugins (vals installed-plugins)
         updating (state/sub :plugin/installing)
+        develop-mode? (state/sub :ui/developer-mode?)
         selected-unpacked-pkg (state/sub :plugin/selected-unpacked-pkg)
-        sorted-plugins (->> (vals installed-plugins)
+        coming-updates (state/sub :plugin/updates-coming)
+        *sort-by (::sort-by state)
+        *search-key (::search-key state)
+        *category (::category state)
+        filtered-plugins (when (seq installed-plugins)
+                           (if (= @*category :themes)
+                             (filter #(:theme %) installed-plugins)
+                             (filter #(not (:theme %)) installed-plugins)))
+        filtered-plugins (if-not (string/blank? @*search-key)
+                           (if-let [author (and (string/starts-with? @*search-key "@")
+                                                (subs @*search-key 1))]
+                             (filter #(= author (:author %)) filtered-plugins)
+                             (search/fuzzy-search
+                               filtered-plugins @*search-key
+                               :limit 30
+                               :extract-fn :name))
+                           filtered-plugins)
+        sorted-plugins (->> filtered-plugins
                             (reduce #(let [k (if (get-in %2 [:settings :disabled]) 1 0)]
                                        (update %1 k conj %2)) [[] []])
                             (#(update % 0 (fn [it] (sort-by :iir it))))
@@ -354,43 +546,27 @@
       [[t] i18n/*tongue-context*]
 
       [:div.cp__plugins-installed
-       [:div.mb-4.flex.items-center.justify-between
 
-        [:div.flex.align-items.secondary-tabs
-         (ui/tippy {:html  [:div (t :plugin/unpacked-tips)]
-                    :arrow true}
-                   (ui/button
-                     [:span (ui/icon "upload") (t :plugin/load-unpacked)]
-                     :intent "logseq"
-                     :on-click plugin-handler/load-unpacked-plugin))
+       (panel-control-tabs
+         t
+         @*search-key *search-key
+         @*category *category
+         @*sort-by *sort-by
+         selected-unpacked-pkg
+         false develop-mode? nil)
 
-         (unpacked-plugin-loader selected-unpacked-pkg)]
-
-        [:div.flex.align-items
-         ;;(ui/button
-         ;;  (t :plugin/open-preferences)
-         ;;  :intent "logseq"
-         ;;  :on-click (fn []
-         ;;              (p/let [root (plugin-handler/get-ls-dotdir-root)]
-         ;;                (js/apis.openPath (str root "/preferences.json")))))
-         (ui/button
-           (t :plugin/contribute)
-           :href "https://github.com/logseq/marketplace"
-           :intent "logseq"
-           :target "_blank")
-         ]]
        [:div.cp__plugins-item-lists.grid-cols-1.md:grid-cols-2.lg:grid-cols-3
         (for [item sorted-plugins]
           (rum/with-key
             (let [pid (keyword (:id item))]
               (plugin-item-card
-                item false
+                item false *search-key updating
                 (and updating (= (keyword (:id updating)) pid))
-                true nil)) (:id item)))]])))
+                true nil (get coming-updates pid))) (:id item)))]])))
 
 (defn open-select-theme!
   []
-  (state/set-modal! installed-themes))
+  (state/set-sub-modal! installed-themes))
 
 (rum/defc hook-ui-slot
   ([type payload] (hook-ui-slot type payload nil))
@@ -403,7 +579,7 @@
          #())
        [id])
      [:div.lsp-hook-ui-slot
-      (merge opts {:id id
+      (merge opts {:id            id
                    :on-mouse-down (fn [e] (util/stop e))})])))
 
 (rum/defc ui-item-renderer
@@ -443,12 +619,20 @@
   []
 
   (let [[active set-active!] (rum/use-state :installed)
-        market? (= active :marketplace)]
+        market? (= active :marketplace)
+        *el-ref (rum/create-ref)]
+
+    (rum/use-effect!
+      #(let [^js el (rum/deref *el-ref)]
+         (js/setTimeout (fn [] (.focus el)) 100))
+      [])
 
     (rum/with-context
       [[t] i18n/*tongue-context*]
 
       [:div.cp__plugins-page
+       {:ref       *el-ref
+        :tab-index "-1"}
        [:h1 (t :plugins)]
        (security-warning)
        [:hr]
@@ -477,3 +661,9 @@
         (ui-handler/exec-js-if-exists-&-allowed! t)))
     [current-repo db-restoring? nfs-granted?])
   nil)
+
+(defn open-plugins-modal!
+  []
+  (state/set-modal!
+    (fn [close!]
+      (plugins-page))))

+ 164 - 23
src/main/frontend/components/plugins.css

@@ -4,9 +4,28 @@
 
 .cp__plugins {
   &-page {
+    background-color: var(--ls-primary-background-color);
+    margin: -2rem;
+    padding: 1.5rem 2rem;
+    outline: none;
+
+    @screen xl {
+      width: 1260px;
+    }
+
     > h1 {
-      padding: 0 0 20px;
-      font-size: 38px;
+      margin: 0;
+      padding: 0;
+      font-size: 22px;
+      font-weight: 600;
+    }
+
+    .admonitionblock {
+      margin: 1rem 15px;
+
+      .text-lg {
+        font-size: 16px;
+      }
     }
 
     .tabs {
@@ -46,17 +65,85 @@
         }
       }
     }
+
+    .control-tabs {
+      .ti {
+        margin-right: 4px;
+      }
+
+      .ui__dropdown-trigger {
+        .ti-circle {
+          visibility: hidden;
+        }
+      }
+    }
+
+    .ui__button {
+      border: none;
+
+      &:active {
+        opacity: .8;
+      }
+
+      &.contribute {
+        position: absolute;
+        top: -46px;
+        right: 0;
+        background: transparent;
+        font-size: 12px;
+        opacity: .8;
+        display: none;
+
+        @screen md {
+          display: block;
+        }
+      }
+
+      &.load-unpacked {
+        opacity: .9;
+        background: transparent;
+      }
+
+      &.sort-by, &.more-do {
+        padding: 0 4px;
+      }
+    }
+
+    .search-ctls {
+      margin: 3px 13px;
+      display: flex;
+      align-items: center;
+      position: relative;
+
+      small.s1 {
+        left: 6px;
+        top: 6px;
+      }
+
+      small.s2 {
+        right: 4px;
+        top: 6px;
+        user-select: none;
+        cursor: pointer;
+        z-index: 1;
+      }
+
+      .form-input {
+        background-color: var(--ls-primary-background-color);
+        padding: 5px 7px 5px 22px;
+        opacity: .5;
+
+        &:focus {
+          background-color: var(--ls-secondary-background-color);
+          opacity: 1;
+        }
+      }
+    }
   }
 
   &-installed {
     min-height: 60vh;
     padding-top: 5px;
-
-    .secondary-tabs {
-      button {
-        background: var(--ls-secondary-background-color);
-      }
-    }
   }
 
   &-marketplace {
@@ -80,25 +167,34 @@
     @apply flex py-3 px-1 rounded-md;
 
     background-color: var(--ls-secondary-background-color);
-    height: 160px;
+    height: 150px;
 
     li {
       margin: 0;
     }
 
     .head {
-      max-height: 60px;
+      max-height: 50px;
       overflow: hidden;
+      line-height: 24px;
+      padding-right: 24px;
     }
 
     .desc {
       height: 60px;
       overflow: hidden;
+
+      > p {
+        @apply overflow-hidden overflow-ellipsis;
+        display: -webkit-box;
+        -webkit-line-clamp: 2;
+        -webkit-box-orient: vertical;
+      }
     }
 
     .flag {
       position: absolute;
-      bottom: 24px;
+      bottom: 16px;
       left: 0;
       width: 100%;
 
@@ -106,7 +202,7 @@
         color: var(--ls-primary-text-color);
         opacity: .8;
 
-        > small:last-child {
+        > small {
           cursor: pointer;
         }
       }
@@ -128,8 +224,8 @@
       padding: 8px;
 
       svg, .icon {
-        width: 70px;
-        height: 70px;
+        width: 60px;
+        height: 60px;
         opacity: .8;
 
         &:hover {
@@ -154,20 +250,25 @@
 
         .de {
           font-size: 10px;
-          padding: 5px 0;
-          padding-right: 10px;
+          padding: 5px 2px;
+          padding-right: 3px;
           border-radius: 2px;
+          margin-right: 5px;
           user-select: none;
           transition: none;
           opacity: .2;
           position: relative;
           z-index: var(--ls-z-index-level-1);
 
+          .ti {
+            font-size: 16px;
+          }
+
           .menu-list {
             @apply shadow-md rounded-sm absolute hidden list-none overflow-hidden m-0 p-0;
 
             background-color: var(--ls-primary-background-color);
-            top: 20px;
+            top: 22px;
             left: 0;
             min-width: 120px;
 
@@ -197,6 +298,23 @@
             padding: 5px;
           }
 
+          &.sponsors {
+            .menu-list {
+              min-width: auto;
+              white-space: nowrap;
+
+              > li {
+                &:hover {
+                  background-color: unset;
+                }
+              }
+
+              .ti {
+                font-size: 12px;
+              }
+            }
+          }
+
           svg {
             width: 15px;
             height: 15px;
@@ -213,7 +331,7 @@
 
         > .l {
           @apply flex items-center;
-          margin-left: -80px;
+          margin-left: -70px;
         }
 
         a.btn {
@@ -244,6 +362,8 @@
           &.disabled {
             pointer-events: none;
             cursor: default;
+            color: var(--ls-primary-text-color);
+            opacity: .3;
           }
 
           &.installing {
@@ -262,11 +382,31 @@
     }
 
     &.market {
+      &.installed {
+
+      }
+
       .ctl {
         padding-left: 12px;
         bottom: -5px;
       }
     }
+
+    .updates-actions {
+      opacity: 0;
+    }
+
+    &:hover {
+      .updates-actions {
+        opacity: 1;
+      }
+    }
+
+    &.has-new-version, &.updating {
+      .updates-actions {
+        opacity: 1;
+      }
+    }
   }
 
   &-details {
@@ -291,11 +431,8 @@
         line-height: 1.1em;
 
         > strong {
-          font-size: 14px;
-        }
-
-        > small {
-          font-size: 11px;
+          font-size: 13px;
+          font-weight: 600;
         }
       }
 
@@ -305,6 +442,10 @@
         opacity: 1;
       }
 
+      &.is-selected {
+        opacity: 1;
+      }
+
       &:hover {
         opacity: 1;
       }

+ 31 - 10
src/main/frontend/components/settings.cljs

@@ -3,6 +3,7 @@
             [frontend.components.svg :as svg]
             [frontend.config :as config]
             [frontend.context.i18n :as i18n]
+            [frontend.storage :as storage]
             [frontend.date :as date]
             [frontend.dicts :as dicts]
             [frontend.handler :as handler]
@@ -515,12 +516,31 @@
           developer-mode?
           (fn []
             (let [mode (not developer-mode?)]
-              (state/set-developer-mode! mode)
-              (and mode (util/electron?)
-                   (when (js/confirm (t :developer-mode-alert))
-                     (js/logseq.api.relaunch)))))
+              (state/set-developer-mode! mode)))
           [:div.text-sm.opacity-50 (t :settings-page/developer-mode-desc)]))
 
+(rum/defc plugin-enabled-switcher
+  [t]
+  (let [value (state/lsp-enabled?-or-theme)
+        [on? set-on?] (rum/use-state value)
+        on-toggle #(let [v (not on?)]
+                     (set-on? v)
+                     (storage/set :lsp-core-enabled v))]
+    [:div.flex.items-center
+     (ui/toggle on? on-toggle true)
+     (when (not= (boolean value) on?)
+       [:div.relative.opacity-70
+        [:span.absolute.whitespace-nowrap
+         {:style {:top -18 :left 10}}
+         (ui/button (t :plugin/restart)
+                    :on-click #(js/logseq.api.relaunch)
+                    :small? true :intent "logseq")]])]))
+
+(defn plugin-system-switcher-row [t]
+  (row-with-button-action
+    {:left-label "Plug-in system"
+     :action (plugin-enabled-switcher t)}))
+
 (rum/defcs settings
   < (rum/local :general ::active)
   {:will-mount
@@ -533,12 +553,12 @@
      state)}
   rum/reactive
   [state]
-  (let [preferred-format (state/get-preferred-format)
+  (let [current-repo (state/sub :git/current-repo)
+        preferred-format (state/get-preferred-format)
         preferred-date-format (state/get-date-formatter)
         preferred-workflow (state/get-preferred-workflow)
         preferred-language (state/sub [:preferred-language])
         enable-timetracking? (state/enable-timetracking?)
-        current-repo (state/get-current-repo)
         enable-journals? (state/enable-journals? current-repo)
         enable-encryption? (state/enable-encryption? current-repo)
         enable-all-pages-public? (state/all-pages-public?)
@@ -579,7 +599,8 @@
 
             (when label
               [:li
-               {:class    (util/classnames [{:active (= label @*active)}])
+               {:key text
+                :class    (util/classnames [{:active (= label @*active)}])
                 :on-click #(reset! *active label)}
 
                [:a.flex.items-center
@@ -596,9 +617,8 @@
               (version-row t version))
             (language-row t preferred-language)
             (theme-modes-row t switch-theme system-theme? dark?)
-            (when-let [current-repo (state/sub :git/current-repo)]
-              [(edit-config-edn)
-               (edit-custom-css)])
+            (when current-repo (edit-config-edn))
+            (when current-repo (edit-custom-css))
             (keyboard-shortcuts-row t)]
 
            :editor
@@ -644,6 +664,7 @@
             (when (and util/mac? (util/electron?)) (app-auto-update-row t))
             (usage-diagnostics-row t instrument-disabled?)
             (if-not (mobile-util/is-native-platform?) (developer-mode-row t developer-mode?))
+            (when (util/electron?) (plugin-system-switcher-row t))
             (clear-cache-row t)
 
             (ui/admonition

+ 7 - 20
src/main/frontend/components/sidebar.cljs

@@ -286,9 +286,9 @@
     [:div#left-sidebar.cp__sidebar-left-layout
      {:class (util/classnames [{:is-open left-sidebar-open?}])}
 
-     [ ;; sidebar contents
-      (sidebar-nav route-match close-fn)
-      [:span.shade-mask {:on-click close-fn}]]]))
+     ;; sidebar contents
+     (sidebar-nav route-match close-fn)
+     [:span.shade-mask {:on-click close-fn}]]))
 
 (rum/defc main <
   {:did-mount (fn [state]
@@ -303,8 +303,7 @@
                 state)}
   [{:keys [route-match global-graph-pages? logged? home? route-name indexeddb-support? white? db-restoring? main-content]}]
 
-  (let [left-sidebar-open? (state/sub :ui/left-sidebar-open?)
-        mobile? (util/mobile?)]
+  (let [left-sidebar-open? (state/sub :ui/left-sidebar-open?)]
     (rum/with-context [[t] i18n/*tongue-context*]
       [:div#main-content.cp__sidebar-main-layout.flex-1.flex
        {:class (util/classnames [{:is-left-sidebar-open left-sidebar-open?}])}
@@ -461,17 +460,6 @@
                     (state/sidebar-add-block! (state/get-current-repo) "help" :help nil))}
        "?"])))
 
-(rum/defc settings-modal < rum/reactive
-  []
-  (let [settings-open? (state/sub :ui/settings-open?)]
-    (if settings-open?
-      (do
-        (state/set-modal!
-         (fn [] [:div.settings-modal (settings/settings)]))
-        (util/lock-global-scroll settings-open?))
-      (state/set-modal! nil))
-    nil))
-
 (defn- hide-context-menu-and-clear-selection
   []
   (state/hide-custom-context-menu!)
@@ -494,9 +482,6 @@
                 state)}
   [state route-match main-content]
   (let [{:keys [open? close-fn open-fn]} state
-        close-fn (fn []
-                   (close-fn)
-                   (state/set-left-sidebar-open! false))
         me (state/sub :me)
         current-repo (state/sub :git/current-repo)
         granted? (state/sub [:nfs/user-granted? (state/get-current-repo)])
@@ -504,6 +489,7 @@
         system-theme? (state/sub :ui/system-theme?)
         white? (= "white" (state/sub :ui/theme))
         sidebar-open?  (state/sub :ui/sidebar-open?)
+        settings-open? (state/sub :ui/settings-open?)
         left-sidebar-open?  (state/sub :ui/left-sidebar-open?)
         right-sidebar-blocks (state/sub-right-sidebar-blocks)
         route-name (get-in route-match [:data :name])
@@ -525,6 +511,7 @@
         :nfs-granted?  granted?
         :db-restoring? db-restoring?
         :sidebar-open? sidebar-open?
+        :settings-open? settings-open?
         :sidebar-blocks-len (count right-sidebar-blocks)
         :system-theme? system-theme?
         :on-click      (fn [e]
@@ -566,7 +553,7 @@
 
         (ui/notification)
         (ui/modal)
-        (settings-modal)
+        (ui/sub-modal)
         (command-palette/command-palette-modal)
         (custom-context-menu)
         (plugins/custom-js-installer {:t t

+ 10 - 1
src/main/frontend/components/theme.cljs

@@ -5,13 +5,15 @@
             [frontend.handler.ui :as ui-handler]
             [frontend.ui :as ui]
             [frontend.util :as util]
+            [frontend.state :as state]
+            [frontend.components.settings :as settings]
             [frontend.rum :refer [use-mounted]]
             [rum.core :as rum]
             [frontend.mobile.util :as mobile-util]))
 
 (rum/defc container
   [{:keys [t route theme on-click current-repo nfs-granted? db-restoring?
-           sidebar-open? system-theme? sidebar-blocks-len edit?] :as props} child]
+           settings-open? sidebar-open? system-theme? sidebar-blocks-len edit?] :as props} child]
   (let [mounted-fn (use-mounted)
         [restored-sidebar? set-restored-sidebar?] (rum/use-state false)]
 
@@ -62,6 +64,13 @@
      #(when system-theme?
         (ui/setup-system-theme-effect!))
      [system-theme?])
+
+    (rum/use-effect!
+      #(state/set-modal!
+         (when settings-open?
+           (fn [] [:div.settings-modal (settings/settings)])))
+      [settings-open?])
+
     [:div
      {:class    (str theme "-theme")
       :on-click on-click}

+ 10 - 0
src/main/frontend/dicts.cljs

@@ -350,9 +350,14 @@
         :plugin/install "Install"
         :plugin/reload "Reload"
         :plugin/update "Update"
+        :plugin/check-update "Check update"
+        :plugin/check-all-updates "Check all updates"
+        :plugin/refresh-lists "Refresh lists"
+        :plugin/enabled "Enabled"
         :plugin/updating "Updating"
         :plugin/uninstall "Uninstall"
         :plugin/marketplace "Marketplace"
+        :plugin/delete-alert "Are you sure uninstall plugin [{1}]?"
         :plugin/open-settings "Open settings"
         :plugin/open-package "Open package"
         :plugin/load-unpacked "Load unpacked plugin"
@@ -1122,6 +1127,11 @@
            :plugin/install "安装"
            :plugin/reload "重载"
            :plugin/update "更新"
+           :plugin/check-update "检查更新"
+           :plugin/check-all-updates "一键检查更新"
+           :plugin/refresh-lists "刷新插件列表"
+           :plugin/delete-alert "确定删除插件 [{1}]?"
+           :plugin/enabled "已开启"
            :plugin/updating "更新中"
            :plugin/uninstall "卸载"
            :plugin/marketplace "插件市场"

+ 6 - 1
src/main/frontend/handler.cljs

@@ -193,10 +193,15 @@
 
 (defn clear-cache!
   []
+  (notification/show! "Clearing..." :warning false)
   (p/let [_ (when (util/electron?)
               (ipc/ipc "clearCache"))
           _ (idb/clear-local-storage-and-idb!)]
-    (js/setTimeout #(js/window.location.reload %) 3000)))
+    (js/setTimeout
+      (fn [] (if (util/electron?)
+               (ipc/ipc :reloadWindowPage)
+               (js/window.location.reload)))
+      2000)))
 
 (defn- register-components-fns!
   []

+ 2 - 3
src/main/frontend/handler/editor.cljs

@@ -2920,9 +2920,8 @@
       (state/selection?)
       (do
         (util/stop e)
-        (on-tab direction))
-
-      :else nil)))
+        (on-tab direction)))
+    nil))
 
 (defn keydown-not-matched-handler
   [format]

+ 16 - 0
src/main/frontend/handler/events.cljs

@@ -4,6 +4,7 @@
             [clojure.set :as set]
             [datascript.core :as d]
             [frontend.components.diff :as diff]
+            [frontend.handler.plugin :as plugin-handler]
             [frontend.components.plugins :as plugin]
             [frontend.components.encryption :as encryption]
             [frontend.components.git :as git-component]
@@ -207,6 +208,9 @@
                     {:fullscreen? false
                      :close-btn?  false}))
 
+(defmethod handle :go/plugins [_]
+  (plugin/open-plugins-modal!))
+
 (defmethod handle :redirect-to-home [_]
   (page-handler/create-today-journal!))
 
@@ -231,6 +235,18 @@
   (when-let [input (state/get-input)]
     (util/make-el-into-viewport input)))
 
+(defmethod handle :plugin/consume-updates [[_ id pending? updated?]]
+  (when-let [coming (get-in @state/state [:plugin/updates-coming id])]
+    (notification/show!
+      (str "Checked: " (:title coming))
+      :success))
+
+  ;; try to start consume pending item
+  (when-let [n (second (first (:plugin/updates-pending @state/state)))]
+    (plugin-handler/check-or-update-marketplace-plugin
+      (assoc n :only-check true)
+      (fn [^js e] (js/console.error "[Check Err]" n e)))))
+
 (defn run!
   []
   (let [chan (state/get-events-chan)]

+ 165 - 141
src/main/frontend/handler/plugin.cljs

@@ -18,8 +18,8 @@
             [frontend.format :as format]))
 
 (defonce lsp-enabled?
-  (and (util/electron?)
-       (= (storage/get "developer-mode") "true")))
+         (and (util/electron?)
+              (state/lsp-enabled?-or-theme)))
 
 (defn invoke-exported-api
   [type & args]
@@ -45,26 +45,33 @@
   [refresh?]
   (if (or refresh? (nil? (:plugin/marketplace-pkgs @state/state)))
     (p/create
-     (fn [resolve reject]
-       (-> (util/fetch plugins-url
-                       (fn [res]
-                         (let [pkgs (:packages res)]
-                           (state/set-state! :plugin/marketplace-pkgs pkgs)
-                           (resolve pkgs)))
-                       reject)
-           (p/catch reject))))
+      (fn [resolve reject]
+        (-> (util/fetch plugins-url
+                        (fn [res]
+                          (let [pkgs (:packages res)]
+                            (state/set-state! :plugin/marketplace-pkgs pkgs)
+                            (resolve pkgs)))
+                        reject)
+            (p/catch reject))))
     (p/resolved (:plugin/marketplace-pkgs @state/state))))
 
 (defn load-marketplace-stats
   [refresh?]
   (if (or refresh? (nil? (:plugin/marketplace-stats @state/state)))
     (p/create
-     (fn [resolve reject]
-       (util/fetch stats-url
-                   (fn [res]
-                     (state/set-state! :plugin/marketplace-stats res)
-                     (resolve nil))
-                   reject)))
+      (fn [resolve reject]
+        (util/fetch stats-url
+                    (fn [res]
+                      (when res
+                        (state/set-state!
+                          :plugin/marketplace-stats
+                          (into {} (map (fn [[k stat]]
+                                          [k (assoc stat
+                                               :total_downloads
+                                               (reduce (fn [a b] (+ a (get b 2))) 0 (:releases stat)))])
+                                        res)))
+                        (resolve nil)))
+                    reject)))
     (p/resolved nil)))
 
 (defn installed?
@@ -77,30 +84,30 @@
   (when-not (and (:plugin/installing @state/state)
                  (installed? id))
     (p/create
-     (fn [resolve]
-       (state/set-state! :plugin/installing mft)
-       (ipc/ipc "installMarketPlugin" mft)
-       (resolve id)))))
+      (fn [resolve]
+        (state/set-state! :plugin/installing mft)
+        (ipc/ipc "installMarketPlugin" mft)
+        (resolve id)))))
 
-(defn update-marketplace-plugin
+(defn check-or-update-marketplace-plugin
   [{:keys [id] :as pkg} error-handler]
   (when-not (and (:plugin/installing @state/state)
                  (not (installed? id)))
     (p/catch
-     (p/then
-      (do (state/set-state! :plugin/installing pkg)
-          (load-marketplace-plugins false))
-      (fn [mfts]
-        (if-let [mft (some #(if (= (:id %) id) %) mfts)]
-          (do
-            (ipc/ipc "updateMarketPlugin" (merge (dissoc pkg :logger) mft)))
-          (throw (js/Error. (str ":central-not-matched " id))))
-        true))
-
-     (fn [^js e]
-       (error-handler "Update Error: remote error")
-       (state/set-state! :plugin/installing nil)
-       (js/console.error e)))))
+      (p/then
+        (do (state/set-state! :plugin/installing pkg)
+            (load-marketplace-plugins false))
+        (fn [mfts]
+          (if-let [mft (some #(if (= (:id %) id) %) mfts)]
+            (do
+              (ipc/ipc "updateMarketPlugin" (merge (dissoc pkg :logger) mft)))
+            (throw (js/Error. (str ":central-not-matched " id))))
+          true))
+
+      (fn [^js e]
+        (error-handler "Update Error: remote error")
+        (state/set-state! :plugin/installing nil)
+        (js/console.error e)))))
 
 (defn get-plugin-inst
   [id]
@@ -115,47 +122,54 @@
         listener (fn [^js _ ^js e]
                    (js/console.debug :lsp-installed e)
 
-                   (when-let [{:keys [status payload]} (bean/->clj e)]
+                   (when-let [{:keys [status payload only-check]} (bean/->clj e)]
                      (case (keyword status)
 
                        :completed
                        (let [{:keys [id dst name title version theme]} payload
                              name (or title name "Untitled")]
-                         (if (installed? id)
-                           (when-let [^js pl (get-plugin-inst id)] ;; update
-                             (p/then
-                              (.reload pl)
-                              #(do
-                                  ;;(if theme (select-a-plugin-theme id))
-                                 (notifications/show!
-                                  (str (t :plugin/update) (t :plugins) ": " name " - " (.-version (.-options pl))) :success))))
-
-                           (do                              ;; register new
-                             (p/then
-                              (js/LSPluginCore.register (bean/->js {:key id :url dst}))
-                              (fn [] (if theme (js/setTimeout #(select-a-plugin-theme id) 300))))
-                             (notifications/show!
-                              (str (t :plugin/installed) (t :plugins) ": " name) :success))))
+                         (if only-check
+                           (state/consume-updates-coming-plugin payload false)
+                           (if (installed? id)
+                             (when-let [^js pl (get-plugin-inst id)] ;; update
+                               (p/then
+                                 (.reload pl)
+                                 #(do
+                                    ;;(if theme (select-a-plugin-theme id))
+                                    (notifications/show!
+                                      (str (t :plugin/update) (t :plugins) ": " name " - " (.-version (.-options pl))) :success)
+                                    (state/consume-updates-coming-plugin payload true))))
+
+                             (do                            ;; register new
+                               (p/then
+                                 (js/LSPluginCore.register (bean/->js {:key id :url dst}))
+                                 (fn [] (if theme (js/setTimeout #(select-a-plugin-theme id) 300))))
+                               (notifications/show!
+                                 (str (t :plugin/installed) (t :plugins) ": " name) :success)))))
 
                        :error
-                       (let [[msg type] (case (keyword (string/replace payload #"^[\s\:]+" ""))
+                       (let [error-code (keyword (string/replace (:error-code payload) #"^[\s\:]+" ""))
+                             [msg type] (case error-code
 
                                           :no-new-version
                                           [(str (t :plugin/up-to-date) " :)") :success]
 
-                                          [payload :error])]
+                                          [error-code :error])
+                             updates-pending? (seq (:plugin/updates-pending @state/state))]
 
-                         (notifications/show!
-                          (str
-                           (if (= :error type) "[Install Error]" "")
-                           msg) type)
+                         (if (and only-check updates-pending?)
+                           (state/consume-updates-coming-plugin payload false)
+                           (notifications/show!
+                             (str
+                               (if (= :error type) "[Install Error]" "")
+                               msg) type))
 
                          (js/console.error payload))
 
                        :dunno))
 
                    ;; reset
-                   (state/set-state! :plugin/installing nil)
+                   (js/setTimeout #(state/set-state! :plugin/installing nil) 512)
                    true)]
 
     (js/window.apis.addListener channel listener)
@@ -195,17 +209,17 @@
 
 (defn simple-cmd->palette-cmd
   [pid {:keys [key label type desc keybinding] :as cmd} action]
-  (let [palette-cmd {:id       (keyword (str "plugin." pid "/" key))
-                     :desc     (or desc label)
-                     :shortcut (when-let [shortcut (:binding keybinding)]
-                                 (if util/mac?
-                                   (or (:mac keybinding) shortcut)
-                                   shortcut))
+  (let [palette-cmd {:id         (keyword (str "plugin." pid "/" key))
+                     :desc       (or desc label)
+                     :shortcut   (when-let [shortcut (:binding keybinding)]
+                                   (if util/mac?
+                                     (or (:mac keybinding) shortcut)
+                                     shortcut))
                      :handler-id (let [mode (or (:mode keybinding) :global)]
                                    (get keybinding-mode-handler-map (keyword mode)))
-                     :action   (fn []
-                                 (state/pub-event!
-                                  [:exec-plugin-cmd {:type type :key key :pid pid :cmd cmd :action action}]))}]
+                     :action     (fn []
+                                   (state/pub-event!
+                                     [:exec-plugin-cmd {:type type :key key :pid pid :cmd cmd :action action}]))}]
     palette-cmd))
 
 (defn simple-cmd-keybinding->shortcut-args
@@ -267,11 +281,11 @@
     (when-not (string/blank? content)
       (let [content (if-not (string/blank? url)
                       (string/replace
-                       content #"!\[[^\]]*\]\((.*?)\s*(\"(?:.*[^\"])\")?\s*\)"
-                       (fn [[matched link]]
-                         (if (and link (not (string/starts-with? link "http")))
-                           (string/replace matched link (util/node-path.join url link))
-                           matched)))
+                        content #"!\[[^\]]*\]\((.*?)\s*(\"(?:.*[^\"])\")?\s*\)"
+                        (fn [[matched link]]
+                          (if (and link (not (string/starts-with? link "http")))
+                            (string/replace matched link (util/node-path.join url link))
+                            matched)))
                       content)]
         (format/to-html content :markdown (mldoc/default-config :markdown))))
     (catch js/Error e
@@ -287,11 +301,11 @@
                   content (parse-user-md-content content item)]
             (and (string/blank? (string/trim content)) (throw nil))
             (state/set-state! :plugin/active-readme [content item])
-            (state/set-modal! (fn [_] (display))))
+            (state/set-sub-modal! (fn [_] (display))))
           (p/catch #(do (js/console.warn %)
                         (notifications/show! "No README content." :warn))))
       ;; market
-      (state/set-modal! (fn [_] (display repo nil))))))
+      (state/set-sub-modal! (fn [_] (display repo nil))))))
 
 (defn load-unpacked-plugin
   []
@@ -333,16 +347,26 @@
 
 (defn goto-plugins-dashboard!
   []
-  (rfe/push-state :plugins))
+  (state/pub-event! [:go/plugins]))
 
 (defn- get-user-default-plugins
   []
   (p/catch
-   (p/let [files ^js (ipc/ipc "getUserDefaultPlugins")
-           files (js->clj files)]
-     (map #(hash-map :url %) files))
-   (fn [e]
-     (js/console.error e))))
+    (p/let [files ^js (ipc/ipc "getUserDefaultPlugins")
+            files (js->clj files)]
+      (map #(hash-map :url %) files))
+    (fn [e]
+      (js/console.error e))))
+
+(defn check-enabled-for-updates
+  [theme?]
+  (let [pending? (seq (:plugin/updates-pending @state/state))]
+    (when-let [plugins (and (not pending?)
+                            ;; TODO: too many requests may be limited by Github api
+                            (seq (take 16 (state/get-enabled-installed-plugins theme?))))]
+      (state/set-state! :plugin/updates-pending
+        (into {} (map (fn [v] [(keyword (:id v)) v]) plugins)))
+      (state/pub-event! [:plugin/consume-updates]))))
 
 ;; components
 (rum/defc lsp-indicator < rum/reactive
@@ -360,76 +384,76 @@
   (let [el (js/document.createElement "div")]
     (.appendChild js/document.body el)
     (rum/mount
-     (lsp-indicator) el))
+      (lsp-indicator) el))
 
   (state/set-state! :plugin/indicator-text "LOADING")
 
   (p/then
-   (p/let [root (get-ls-dotdir-root)
-           _ (.setupPluginCore js/LSPlugin (bean/->js {:localUserConfigRoot root :dotConfigRoot root}))
+    (p/let [root (get-ls-dotdir-root)
+            _ (.setupPluginCore js/LSPlugin (bean/->js {:localUserConfigRoot root :dotConfigRoot root}))
 
-           clear-commands! (fn [pid]
+            clear-commands! (fn [pid]
                               ;; commands
-                             (unregister-plugin-slash-command pid)
-                             (invoke-exported-api "unregister_plugin_simple_command" pid)
-                             (unregister-plugin-ui-items pid))
-
-           _ (doto js/LSPluginCore
-               (.on "registered"
-                    (fn [^js pl]
-                      (register-plugin
-                       (bean/->clj (.parse js/JSON (.stringify js/JSON pl))))))
-
-               (.on "reloaded"
-                    (fn [^js pl]
-                      (register-plugin
-                       (bean/->clj (.parse js/JSON (.stringify js/JSON pl))))))
-
-               (.on "unregistered" (fn [pid]
-                                     (let [pid (keyword pid)]
+                              (unregister-plugin-slash-command pid)
+                              (invoke-exported-api "unregister_plugin_simple_command" pid)
+                              (unregister-plugin-ui-items pid))
+
+            _ (doto js/LSPluginCore
+                (.on "registered"
+                     (fn [^js pl]
+                       (register-plugin
+                         (bean/->clj (.parse js/JSON (.stringify js/JSON pl))))))
+
+                (.on "reloaded"
+                     (fn [^js pl]
+                       (register-plugin
+                         (bean/->clj (.parse js/JSON (.stringify js/JSON pl))))))
+
+                (.on "unregistered" (fn [pid]
+                                      (let [pid (keyword pid)]
                                         ;; effects
-                                       (unregister-plugin-themes pid)
+                                        (unregister-plugin-themes pid)
                                         ;; plugins
-                                       (swap! state/state md/dissoc-in [:plugin/installed-plugins pid])
+                                        (swap! state/state md/dissoc-in [:plugin/installed-plugins pid])
                                         ;; commands
-                                       (clear-commands! pid))))
-
-               (.on "unlink-plugin" (fn [pid]
-                                      (let [pid (keyword pid)]
-                                        (ipc/ipc "uninstallMarketPlugin" (name pid)))))
-
-               (.on "beforereload" (fn [^js pl]
-                                     (let [pid (.-id pl)]
-                                       (clear-commands! pid)
-                                       (unregister-plugin-themes pid false))))
-
-               (.on "disabled" (fn [pid]
-                                 (clear-commands! pid)
-                                 (unregister-plugin-themes pid)))
-
-               (.on "theme-changed" (fn [^js themes]
-                                      (swap! state/state assoc :plugin/installed-themes
-                                             (vec (mapcat (fn [[pid vs]] (mapv #(assoc % :pid pid) (bean/->clj vs))) (bean/->clj themes))))))
-
-               (.on "theme-selected" (fn [^js opts]
-                                       (let [opts (bean/->clj opts)
-                                             url (:url opts)
-                                             mode (:mode opts)]
-                                         (when mode (state/set-theme! mode))
-                                         (state/set-state! :plugin/selected-theme url))))
-
-               (.on "settings-changed" (fn [id ^js settings]
-                                         (let [id (keyword id)]
-                                           (when (and settings
-                                                      (contains? (:plugin/installed-plugins @state/state) id))
-                                             (update-plugin-settings id (bean/->clj settings)))))))
-
-           default-plugins (get-user-default-plugins)
-
-           _ (.register js/LSPluginCore (bean/->js (if (seq default-plugins) default-plugins [])) true)])
-   #(do
-      (state/set-state! :plugin/indicator-text "END")
-      (callback))))
+                                        (clear-commands! pid))))
+
+                (.on "unlink-plugin" (fn [pid]
+                                       (let [pid (keyword pid)]
+                                         (ipc/ipc "uninstallMarketPlugin" (name pid)))))
+
+                (.on "beforereload" (fn [^js pl]
+                                      (let [pid (.-id pl)]
+                                        (clear-commands! pid)
+                                        (unregister-plugin-themes pid false))))
+
+                (.on "disabled" (fn [pid]
+                                  (clear-commands! pid)
+                                  (unregister-plugin-themes pid)))
+
+                (.on "theme-changed" (fn [^js themes]
+                                       (swap! state/state assoc :plugin/installed-themes
+                                              (vec (mapcat (fn [[pid vs]] (mapv #(assoc % :pid pid) (bean/->clj vs))) (bean/->clj themes))))))
+
+                (.on "theme-selected" (fn [^js opts]
+                                        (let [opts (bean/->clj opts)
+                                              url (:url opts)
+                                              mode (:mode opts)]
+                                          (when mode (state/set-theme! mode))
+                                          (state/set-state! :plugin/selected-theme url))))
+
+                (.on "settings-changed" (fn [id ^js settings]
+                                          (let [id (keyword id)]
+                                            (when (and settings
+                                                       (contains? (:plugin/installed-plugins @state/state) id))
+                                              (update-plugin-settings id (bean/->clj settings)))))))
+
+            default-plugins (get-user-default-plugins)
+
+            _ (.register js/LSPluginCore (bean/->js (if (seq default-plugins) default-plugins [])) true)])
+    #(do
+       (state/set-state! :plugin/indicator-text "END")
+       (callback))))
 
 (defn setup!
   "setup plugin core handler"

+ 5 - 3
src/main/frontend/modules/shortcut/config.cljs

@@ -414,9 +414,11 @@
                                      :binding "t i"
                                      :fn      plugin-handler/show-themes-modal!}
 
-   :ui/goto-plugins                 {:desc    "Go to plugins dashboard"
-                                     :binding "t p"
-                                     :fn      plugin-handler/goto-plugins-dashboard!}
+   :ui/goto-plugins                  (when plugin-handler/lsp-enabled?
+                                       {:desc    "Go to plugins dashboard"
+                                        :binding "t p"
+                                        :fn      plugin-handler/goto-plugins-dashboard!})
+
 
    :editor/toggle-open-blocks       {:desc    "Toggle open blocks (collapse or expand all blocks)"
                                      :binding "t o"

+ 180 - 100
src/main/frontend/state.cljs

@@ -5,6 +5,7 @@
             [cljs.core.async :as async]
             [clojure.string :as string]
             [dommy.core :as dom]
+            [medley.core :as medley]
             [electron.ipc :as ipc]
             [frontend.storage :as storage]
             [frontend.util :as util]
@@ -58,7 +59,12 @@
       :search/graph-filters []
 
       ;; modals
-      :modal/show? false
+      :modal/label                           ""
+      :modal/show?                           false
+      :modal/panel-content                   nil
+      :modal/fullscreen?                     false
+      :modal/close-btn?                      nil
+      :modal/subsets                         []
 
       ;; right sidebar
       :ui/fullscreen? false
@@ -151,6 +157,9 @@
       :electron/user-cfgs nil
 
       ;; plugin
+      :plugin/enabled   (and (util/electron?)
+                             ;; true false :theme-only
+                             ((fnil identity true) (storage/get :lsp-core-enabled)))
       :plugin/indicator-text        nil
       :plugin/installed-plugins     {}
       :plugin/installed-themes      []
@@ -163,6 +172,8 @@
       :plugin/marketplace-stats     nil
       :plugin/installing            nil
       :plugin/active-readme         nil
+      :plugin/updates-pending       {}
+      :plugin/updates-coming        {}
 
       ;; pdf
       :pdf/current                  nil
@@ -287,13 +298,13 @@
   (:arweave/gateway (get-config) default-arweave-gateway))
 
 (defonce built-in-macros
-  {"img" "[:img.$4 {:src \"$1\" :style {:width $2 :height $3}}]"})
+         {"img" "[:img.$4 {:src \"$1\" :style {:width $2 :height $3}}]"})
 
 (defn get-macros
   []
   (merge
-   built-in-macros
-   (:macros (get-config))))
+    built-in-macros
+    (:macros (get-config))))
 
 (defn sub-config
   []
@@ -322,7 +333,7 @@
 (defn enable-grammarly?
   []
   (true? (:feature/enable-grammarly?
-          (get (sub-config) (get-current-repo)))))
+           (get (sub-config) (get-current-repo)))))
 
 ;; (defn store-block-id-in-file?
 ;;   []
@@ -331,37 +342,37 @@
 (defn scheduled-deadlines-disabled?
   []
   (true? (:feature/disable-scheduled-and-deadline-query?
-          (get (sub-config) (get-current-repo)))))
+           (get (sub-config) (get-current-repo)))))
 
 (defn enable-timetracking?
   []
   (not (false? (:feature/enable-timetracking?
-                (get (sub-config) (get-current-repo))))))
+                 (get (sub-config) (get-current-repo))))))
 
 (defn enable-journals?
   [repo]
   (not (false? (:feature/enable-journals?
-                (get (sub-config) repo)))))
+                 (get (sub-config) repo)))))
 
 (defn export-heading-to-list?
   []
   (not (false? (:export/heading-to-list?
-                (get (sub-config) (get-current-repo))))))
+                 (get (sub-config) (get-current-repo))))))
 
 (defn enable-encryption?
   [repo]
   (:feature/enable-encryption?
-   (get (sub-config) repo)))
+    (get (sub-config) repo)))
 
 (defn enable-git-auto-push?
   [repo]
   (not (false? (:git-auto-push
-                (get (sub-config) repo)))))
+                 (get (sub-config) repo)))))
 
 (defn enable-block-timestamps?
   []
   (true? (:feature/enable-block-timestamps?
-          (get (sub-config) (get-current-repo)))))
+           (get (sub-config) (get-current-repo)))))
 
 (defn sub-graph-config
   []
@@ -375,7 +386,7 @@
 (defn show-brackets?
   []
   (not (false? (:ui/show-brackets?
-                (get (sub-config) (get-current-repo))))))
+                 (get (sub-config) (get-current-repo))))))
 
 (defn get-default-home
   []
@@ -394,11 +405,11 @@
    (get-preferred-format (get-current-repo)))
   ([repo-url]
    (keyword
-    (or
-     (when-let [fmt (:preferred-format (get-config repo-url))]
-       (string/lower-case (name fmt)))
+     (or
+       (when-let [fmt (:preferred-format (get-config repo-url))]
+         (string/lower-case (name fmt)))
 
-     (get-in @state [:me :preferred_format] "markdown")))))
+       (get-in @state [:me :preferred_format] "markdown")))))
 
 ;; TODO: consider adding a pane in Settings to set this through the GUI (rather
 ;; than having to go through the config.edn file)
@@ -406,8 +417,8 @@
   ([] (get-editor-command-trigger (get-current-repo)))
   ([repo-url]
    (or
-    (:editor/command-trigger (get-config repo-url)) ;; Get from user config
-    "/"))) ;; Set the default
+     (:editor/command-trigger (get-config repo-url))        ;; Get from user config
+     "/")))                                                 ;; Set the default
 
 (defn markdown?
   []
@@ -417,16 +428,16 @@
 (defn get-pages-directory
   []
   (or
-   (when-let [repo (get-current-repo)]
-     (:pages-directory (get-config repo)))
-   "pages"))
+    (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"))
+    (when-let [repo (get-current-repo)]
+      (:journals-directory (get-config repo)))
+    "journals"))
 
 (defn org-mode-file-link?
   [repo]
@@ -440,13 +451,13 @@
 (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))))
+    (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
   []
@@ -769,7 +780,7 @@
 (defn set-github-installation-tokens!
   [tokens]
   (when (seq tokens)
-    (let [tokens  (medley/index-by :installation_id tokens)
+    (let [tokens (medley/index-by :installation_id tokens)
           repos (get-repos)]
       (when (seq repos)
         (let [set-token-f
@@ -785,8 +796,8 @@
                       (merge repo {:token token :expires_at expires-at}))
                     (do
                       (when (and
-                             (:url repo)
-                             (string/starts-with? (:url repo) "https://"))
+                              (:url repo)
+                              (string/starts-with? (:url repo) "https://"))
                         (log/error :token/cannot-set-token {:repo-m repo :token-m m}))
                       repo))))
               repos (mapv set-token-f repos)]
@@ -814,13 +825,13 @@
   [repo db-id block-type block-data]
   (when (not (util/sm-breakpoint?))
     (when db-id
-     (update-state! :sidebar/blocks (fn [blocks]
-                                      (->> (remove #(= (second %) db-id) blocks)
-                                           (cons [repo db-id block-type block-data])
-                                           (distinct))))
-     (open-right-sidebar!)
-     (when-let [elem (gdom/getElementByClass "cp__right-sidebar-scrollable")]
-       (util/scroll-to elem 0)))))
+      (update-state! :sidebar/blocks (fn [blocks]
+                                       (->> (remove #(= (second %) db-id) blocks)
+                                            (cons [repo db-id block-type block-data])
+                                            (distinct))))
+      (open-right-sidebar!)
+      (when-let [elem (gdom/getElementByClass "cp__right-sidebar-scrollable")]
+        (util/scroll-to elem 0)))))
 
 (defn sidebar-remove-block!
   [idx]
@@ -865,8 +876,8 @@
                     (util/get-block-container block-element))]
     (when container
       {:last-edit-block edit-block
-       :container (gobj/get container "id")
-       :pos (cursor/pos (gdom/getElement edit-input-id))})))
+       :container       (gobj/get container "id")
+       :pos             (cursor/pos (gdom/getElement edit-input-id))})))
 
 (defonce publishing? (atom nil))
 
@@ -880,13 +891,13 @@
   ([edit-input-id content block cursor-range move-cursor?]
    (when (and edit-input-id block
               (or
-               (publishing-enable-editing?)
-               (not @publishing?)))
+                (publishing-enable-editing?)
+                (not @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/container (gobj/get container "id"))
+                     :block/container (gobj/get container "id"))
                    block)
            content (string/trim (or content ""))]
        (swap! state
@@ -894,12 +905,12 @@
                 (-> state
                     (assoc-in [:editor/content edit-input-id] content)
                     (assoc
-                     :editor/block block
-                     :editor/editing? {edit-input-id true}
-                     :editor/last-edit-block-input-id edit-input-id
-                     :editor/last-edit-block block
-                     :editor/last-key-code nil
-                     :cursor-range cursor-range))))
+                      :editor/block block
+                      :editor/editing? {edit-input-id true}
+                      :editor/last-edit-block-input-id edit-input-id
+                      :editor/last-edit-block block
+                      :editor/last-key-code nil
+                      :cursor-range cursor-range))))
 
        (when-let [input (gdom/getElement edit-input-id)]
          (let [pos (count cursor-range)]
@@ -919,13 +930,13 @@
 (defn clear-edit!
   []
   (swap! state merge {:editor/editing? nil
-                      :editor/block nil
-                      :cursor-range nil}))
+                      :editor/block    nil
+                      :cursor-range    nil}))
 
 (defn into-code-editor-mode!
   []
-  (swap! state merge {:editor/editing? nil
-                      :cursor-range nil
+  (swap! state merge {:editor/editing?   nil
+                      :cursor-range      nil
                       :editor/code-mode? true}))
 
 (defn set-last-pos!
@@ -1008,12 +1019,12 @@
   []
   (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)))))
+      (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]
@@ -1067,14 +1078,14 @@
 (defn get-date-formatter
   []
   (or
-   (when-let [repo (get-current-repo)]
-     (or
-      (get-in @state [:config repo :journal/page-title-format])
-      ;; for compatibility
-      (get-in @state [:config repo :date-formatter])))
-   ;; TODO:
-   (get-in @state [:me :settings :date-formatter])
-   "MMM do, yyyy"))
+    (when-let [repo (get-current-repo)]
+      (or
+        (get-in @state [:config repo :journal/page-title-format])
+        ;; for compatibility
+        (get-in @state [:config repo :date-formatter])))
+    ;; TODO:
+    (get-in @state [:me :settings :date-formatter])
+    "MMM do, yyyy"))
 
 (defn set-git-status!
   [repo-url value]
@@ -1115,12 +1126,12 @@
 (defn get-default-branch
   [repo-url]
   (or
-   (some->> (get-repos)
-            (filter (fn [m]
-                      (= (:url m) repo-url)))
-            (first)
-            :branch)
-   "master"))
+    (some->> (get-repos)
+             (filter (fn [m]
+                       (= (:url m) repo-url)))
+             (first)
+             :branch)
+    "master"))
 
 (defn get-current-project
   []
@@ -1157,13 +1168,56 @@
   []
   (:modal/show? @state))
 
+(declare set-modal!)
+
+(defn get-sub-modals
+  []
+  (:modal/subsets @state))
+
+(defn set-sub-modal!
+  ([panel-content]
+   (set-sub-modal! panel-content
+                   {:close-btn? true}))
+  ([panel-content {:keys [id label close-btn? show? center?] :as opts}]
+   (if (not (modal-opened?))
+     (set-modal! panel-content opts)
+     (let [modals (:modal/subsets @state)
+           idx (and id (first (keep-indexed #(when (= (:modal/id %2) id) %1)
+                                            modals)))
+           input (medley/filter-vals
+                   #(not (nil? %1))
+                   {:modal/id            id
+                    :modal/label         (or label (if center? "ls-modal-align-center" ""))
+                    :modal/show?         (if (boolean? show?) show? true)
+                    :modal/panel-content panel-content
+                    :modal/close-btn?    close-btn?})]
+       (swap! state update-in
+              [:modal/subsets (or idx (count modals))]
+              merge input)
+       (:modal/subsets @state)))))
+
+(defn close-sub-modal!
+  ([] (close-sub-modal! nil))
+  ([all?-a-id]
+   (if (true? all?-a-id)
+     (swap! state assoc :modal/subsets [])
+     (let [id all?-a-id
+           modals (:modal/subsets @state)]
+       (when-let [idx (if id (first (keep-indexed #(when (= (:modal/id %2) id) %1) modals))
+                             (dec (count modals)))]
+         (swap! state assoc :modal/subsets (into [] (medley/remove-nth idx modals))))))
+   (:modal/subsets @state)))
+
 (defn set-modal!
   ([modal-panel-content]
    (set-modal! modal-panel-content
                {:fullscreen? false
                 :close-btn?  true}))
-  ([modal-panel-content {:keys [fullscreen? close-btn?]}]
+  ([modal-panel-content {:keys [label fullscreen? close-btn? center?]}]
+   (when (seq (get-sub-modals))
+     (close-sub-modal! true))
    (swap! state assoc
+          :modal/label (or label (if center? "ls-modal-align-center" ""))
           :modal/show? (boolean modal-panel-content)
           :modal/panel-content modal-panel-content
           :modal/fullscreen? fullscreen?
@@ -1171,9 +1225,13 @@
 
 (defn close-modal!
   []
-  (swap! state assoc
-         :modal/show? false
-         :modal/panel-content nil))
+  (if (seq (get-sub-modals))
+    (close-sub-modal!)
+    (swap! state assoc
+           :modal/label ""
+           :modal/show? false
+           :modal/fullscreen? false
+           :modal/panel-content nil)))
 
 (defn get-db-batch-txs-chan
   []
@@ -1378,11 +1436,11 @@
   [repo]
   (when repo
     (or
-     (when-let [last-time (get-in @state [:editor/last-input-time repo])]
-       (let [now (util/time-ms)]
-         (>= (- now last-time) 500)))
-     ;; not in editing mode
-     (not (get-edit-input-id)))))
+      (when-let [last-time (get-in @state [:editor/last-input-time repo])]
+        (let [now (util/time-ms)]
+          (>= (- now last-time) 500)))
+      ;; not in editing mode
+      (not (get-edit-input-id)))))
 
 (defn set-last-persist-transact-id!
   [repo files? id]
@@ -1483,26 +1541,26 @@
 (defn get-start-of-week
   []
   (or
-   (when-let [repo (get-current-repo)]
-     (get-in @state [:config repo :start-of-week]))
-   (get-in @state [:me :settings :start-of-week])
-   6))
+    (when-let [repo (get-current-repo)]
+      (get-in @state [:config repo :start-of-week]))
+    (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))
+    (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))
+    (when-let [value (:ref/linked-references-collapsed-threshold (get-config))]
+      (when (integer? value)
+        value))
+    100))
 
 (defn get-events-chan
   []
@@ -1557,7 +1615,7 @@
 (defn logical-outdenting?
   []
   (:editor/logical-outdenting?
-   (get (sub-config) (get-current-repo))))
+    (get (sub-config) (get-current-repo))))
 
 (defn get-editor-args
   []
@@ -1631,6 +1689,28 @@
   []
   (:ui/visual-viewport-state @state))
 
+(defn get-enabled-installed-plugins
+  [theme?]
+  (filterv
+    #(and (:iir %)
+          (not (get-in % [:settings :disabled]))
+          (= (boolean theme?) (:theme %)))
+    (vals (:plugin/installed-plugins @state))))
+
+(defn lsp-enabled?-or-theme
+  []
+  (:plugin/enabled @state))
+
+(defn consume-updates-coming-plugin
+  [payload updated?]
+  (when-let [id (keyword (:id payload))]
+    (let [pending? (seq (:plugin/updates-pending @state))]
+      (swap! state update :plugin/updates-pending dissoc id)
+      (if updated?
+        (swap! state update :plugin/updates-coming dissoc id)
+        (swap! state update :plugin/updates-coming assoc id payload))
+      (pub-event! [:plugin/consume-updates id pending? updated?]))))
+
 (defn sub-right-sidebar-blocks
   []
   (when-let [current-repo (get-current-repo)]
@@ -1649,4 +1729,4 @@
 
 (defn sub-collapsed
   [block-id]
-  (sub [:ui/collapsed-blocks (get-current-repo) block-id]))
+  (sub [:ui/collapsed-blocks (get-current-repo) block-id]))

+ 33 - 7
src/main/frontend/ui.cljs

@@ -102,7 +102,7 @@
        :as   opts}]]
   (let [{:keys [open? toggle-fn]} state
         modal-content (modal-content-fn state)]
-    [:div.relative {:style {:z-index z-index}}
+    [:div.relative.ui__dropdown-trigger {:style {:z-index z-index}}
      (content-fn state)
      (css-transition
       {:in @open? :timeout 0}
@@ -133,8 +133,8 @@
                                   (close-fn))})
               child (if hr
                       nil
-                      [:div
-                       {:style {:display "flex" :flex-direction "row"}}
+                      [:div.flex.items-center
+                       (when icon icon)
                        [:div {:style {:margin-right "8px"}} title]])]
           (if hr
             [:hr.my-1]
@@ -300,7 +300,7 @@
 
 (defn setup-patch-ios-visual-viewport-state!
   []
-  (when-let [^js vp (and (or (and (util/mobile?) (util/safari?))
+  (if-let [^js vp (and (or (and (util/mobile?) (util/safari?))
                              (mobile-util/native-ios?))
                          js/window.visualViewport)]
     (let [raf-pending? (atom false)
@@ -518,7 +518,7 @@
              "exiting" "ease-in duration-200 opacity-100 translate-y-0 sm:scale-100"
              "exited" "ease-in duration-200 opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95")}
    [:div.absolute.top-0.right-0.pt-2.pr-2
-    (when close-btn?
+    (when-not (false? close-btn?)
       [:a.ui__modal-close.opacity-60.hover:opacity-100
        {:aria-label "Close"
         :type       "button"
@@ -556,13 +556,15 @@
   (let [modal-panel-content (state/sub :modal/panel-content)
         fullscreen? (state/sub :modal/fullscreen?)
         close-btn? (state/sub :modal/close-btn?)
-        show? (boolean modal-panel-content)
+        show? (state/sub :modal/show?)
+        label (state/sub :modal/label)
         close-fn (fn []
                    (state/close-modal!)
                    (state/close-settings!))
         modal-panel-content (or modal-panel-content (fn [close] [:div]))]
     [:div.ui__modal
-     {:style {:z-index (if show? 9999 -1)}}
+     {:style {:z-index (if show? 999 -1)}
+      :label label}
      (css-transition
       {:in show? :timeout 0}
       (fn [state]
@@ -620,6 +622,30 @@
              :on-click (comp on-cancel close-fn)}
             (t :cancel)]]]]))))
 
+(rum/defc sub-modal < rum/reactive
+  []
+  (when-let [modals (seq (state/sub :modal/subsets))]
+    (for [[idx modal] (medley/indexed modals)]
+      (let [id (:modal/id modal)
+            modal-panel-content (:modal/panel-content modal)
+            close-btn? (:modal/close-btn? modal)
+            show? (:modal/show? modal)
+            label (:modal/label modal)
+            close-fn (fn []
+                       (state/close-sub-modal! id))
+            modal-panel-content (or modal-panel-content (fn [close] [:div]))]
+        [:div.ui__modal.is-sub-modal
+         {:style {:z-index (if show? (+ 999 idx) -1)}
+          :label label}
+         (css-transition
+           {:in show? :timeout 0}
+           (fn [state]
+             (modal-overlay state close-fn)))
+         (css-transition
+           {:in show? :timeout 0}
+           (fn [state]
+             (modal-panel show? modal-panel-content state close-fn false close-btn?)))]))))
+
 (defn loading
   [content]
   [:div.flex.flex-row.items-center

+ 9 - 0
src/main/frontend/ui.css

@@ -127,6 +127,15 @@
     focus:outline-none focus:text-gray-500
     transition ease-in-out duration-150;
   }
+
+  &[label="ls-modal-align-center"] {
+    top: 0;
+
+    .ui__modal-panel {
+      top: 50vh;
+      transform: translateY(-60%);
+    }
+  }
 }
 
 html.is-native-andorid,

+ 1 - 1
src/main/frontend/util.cljc

@@ -1582,4 +1582,4 @@
                       (string/starts-with? url "#"))]
        (if (and (not route?) (electron?))
          (js/window.apis.openExternal url)
-         (set! (.-href js/window.location) url)))))
+         (set! (.-href js/window.location) url)))))

部分文件因文件數量過多而無法顯示