Browse Source

Merge remote-tracking branch 'origin/master' into test/add-clj-kondo-part-two

Gabriel Horner 4 years ago
parent
commit
89bef5b26e

+ 2 - 0
libs/src/LSPlugin.ts

@@ -210,6 +210,8 @@ export type ExternalCommandType =
   'logseq.go/search' |
   'logseq.go/search-in-page' |
   'logseq.go/tomorrow' |
+  'logseq.go/backward' |
+  'logseq.go/forward' |
   'logseq.search/re-index' |
   'logseq.sidebar/clear' |
   'logseq.sidebar/open-today-page' |

+ 9 - 6
src/electron/electron/core.cljs

@@ -15,6 +15,7 @@
             [electron.state :as state]
             [electron.git :as git]
             [electron.window :as win]
+            [electron.exceptions :as exceptions]
             ["/electron/utils" :as utils]))
 
 (defonce LSP_SCHEME "lsp")
@@ -122,7 +123,9 @@
         call-app-channel "call-application"
         call-win-channel "call-main-win"
         export-publish-assets "export-publish-assets"
-        quit-dirty-state "set-quit-dirty-state"]
+        quit-dirty-state "set-quit-dirty-state"
+        clear-win-effects! (win/setup-window-listeners! win)]
+
     (doto ipcMain
       (.handle quit-dirty-state
                (fn [_ dirty?]
@@ -156,9 +159,8 @@
                      (catch js/Error e
                        (js/console.error e)))))))
 
-    (win/setup-window-listeners! win)
-
-    #(do (.removeHandler ipcMain toggle-win-channel)
+    #(do (clear-win-effects!)
+         (.removeHandler ipcMain toggle-win-channel)
          (.removeHandler ipcMain export-publish-assets)
          (.removeHandler ipcMain quit-dirty-state)
          (.removeHandler ipcMain call-app-channel)
@@ -214,10 +216,11 @@
                         (fn []
                           (let [t1 (setup-updater! win)
                                 t2 (setup-app-manager! win)
-                                tt (handler/set-ipc-handler! win)]
+                                t3 (handler/set-ipc-handler! win)
+                                tt (exceptions/setup-exception-listeners!)]
 
                             (vreset! *teardown-fn
-                                     #(doseq [f [t0 t1 t2 tt]]
+                                     #(doseq [f [t0 t1 t2 t3 tt]]
                                         (and f (f)))))))
 
                ;; setup effects

+ 25 - 0
src/electron/electron/exceptions.cljs

@@ -0,0 +1,25 @@
+(ns electron.exceptions
+  (:require [electron.utils :as utils]
+            [clojure.string :as string]))
+
+(defonce uncaughtExceptionChan "uncaughtException")
+
+(defn show-error-tip
+  [& msg]
+  (utils/send-to-renderer "notification"
+                          {:type    "error"
+                           :payload (string/join "\n" msg)}))
+
+(defn- app-uncaught-handler
+  [^js e]
+  (let [msg (.-message e)
+        stack (.-stack e)]
+    (show-error-tip "[Main Exception]" msg stack))
+
+  ;; for debug log
+  (js/console.error uncaughtExceptionChan e))
+
+(defn setup-exception-listeners!
+  []
+  (js/process.on uncaughtExceptionChan app-uncaught-handler)
+  #(js/process.off uncaughtExceptionChan app-uncaught-handler))

+ 11 - 7
src/electron/electron/plugin.cljs

@@ -39,7 +39,8 @@
            zipball
            (api "zipball"))
          asset)
-       version])
+       version
+       (:body res)])
 
     (fn [^js e]
       (emit :lsp-installed {:status :error :payload e})
@@ -121,7 +122,8 @@
 (defn install-or-update!
   [{:keys [version repo only-check] :as item}]
   (when repo
-    (let [updating? (and version (. semver valid version))]
+    (let [coerced-version (and version (. semver coerce version))
+          updating? (and version (. semver valid coerced-version))]
 
       (debug (if updating? "Updating:" "Installing:") repo)
 
@@ -129,17 +131,18 @@
             (fn [resolve _reject]
               ;;(reset! *installing-or-updating item)
               ;; get releases
-              (-> (p/let [[asset latest-version] (fetch-latest-release-asset item)
+              (-> (p/let [[asset latest-version notes] (fetch-latest-release-asset item)
 
                           _ (debug "[Release Asset] #" latest-version " =>" (:url asset))
 
                           ;; compare latest version
-                          _ (when (and updating? latest-version
-                                       (. semver valid latest-version))
+                          _ (when-let [coerced-latest-version
+                                       (and updating? latest-version
+                                            (. semver coerce latest-version))]
 
                               (debug "[Updating Latest?] " version " > " latest-version)
 
-                              (if (. semver lt version latest-version)
+                              (if (. semver lt coerced-version coerced-latest-version)
                                 (debug "[Updating Latest] " latest-version)
                                 (throw (js/Error. :no-new-version))))
 
@@ -147,6 +150,7 @@
                                    (:browser_download_url asset) asset)
 
                           _ (when-not dl-url
+                              (debug "[Download URL Error]" asset)
                               (throw (js/Error. :release-asset-not-found)))
 
                           dest (.join path cfgs/dot-root "plugins" (:id item))
@@ -157,7 +161,7 @@
                           {:status     :completed
                            :only-check only-check
                            :payload    (if only-check
-                                         (assoc item :latest-version latest-version)
+                                         (assoc item :latest-version latest-version :latest-notes notes)
                                          (assoc item :zip dl-url :dst dest))})
 
                     (resolve nil))

+ 57 - 43
src/electron/electron/window.cljs

@@ -101,49 +101,63 @@
 (defn setup-window-listeners!
   [^js win]
   (when win
-    (let [web-contents (. win -webContents)]
-      (.on web-contents "context-menu"
-           (fn
-             [_event params]
-             (let [menu (Menu.)
-                   suggestions (.-dictionarySuggestions ^js params)]
-
-               (doseq [suggestion suggestions]
-                 (. menu append
-                    (MenuItem. (clj->js {:label
-                                         suggestion
-                                         :click
-                                         (fn [] (. web-contents replaceMisspelling suggestion))}))))
-
-               (when-let [misspelled-word (not-empty (.-misspelledWord ^js params))]
-                 (. menu append
-                    (MenuItem. (clj->js {:label
-                                         "Add to dictionary"
-                                         :click
-                                         (fn [] (.. web-contents -session (addWordToSpellCheckerDictionary misspelled-word)))}))))
-
-               (. menu popup))))
-
-
-      (.on web-contents "new-window"
-           (fn [e url]
-             (let [url (if (string/starts-with? url "file:")
-                         (js/decodeURIComponent url) url)
-                   url (if-not win32? (string/replace url "file://" "") url)]
-               (.. logger (info "new-window" url))
-               (if (some #(string/includes?
-                            (.normalize path url)
-                            (.join path (. app getAppPath) %))
-                         ["index.html" "electron.html"])
-                 (.info logger "pass-window" url)
-                 (open-default-app! url open)))
-             (.preventDefault e)))
-
-      (.on web-contents "will-navigate"
-           (fn [e url]
-             (.preventDefault e)
-             (open-default-app! url open)))
+    (let [web-contents (. win -webContents)
+          context-menu-handler
+          (fn [_event params]
+            (let [menu (Menu.)
+                  suggestions (.-dictionarySuggestions ^js params)]
+
+              (doseq [suggestion suggestions]
+                (. menu append
+                   (MenuItem. (clj->js {:label
+                                        suggestion
+                                        :click
+                                        (fn [] (. web-contents replaceMisspelling suggestion))}))))
+
+              (when-let [misspelled-word (not-empty (.-misspelledWord ^js params))]
+                (. menu append
+                   (MenuItem. (clj->js {:label
+                                        "Add to dictionary"
+                                        :click
+                                        (fn [] (.. web-contents -session (addWordToSpellCheckerDictionary misspelled-word)))}))))
+
+              (. menu popup)))
+
+          new-win-handler
+          (fn [e url]
+            (let [url (if (string/starts-with? url "file:")
+                        (js/decodeURIComponent url) url)
+                  url (if-not win32? (string/replace url "file://" "") url)]
+              (.. logger (info "new-window" url))
+              (if (some #(string/includes?
+                           (.normalize path url)
+                           (.join path (. app getAppPath) %))
+                        ["index.html" "electron.html"])
+                (.info logger "pass-window" url)
+                (open-default-app! url open)))
+            (.preventDefault e))
+
+          will-navigate-handler
+          (fn [e url]
+            (.preventDefault e)
+            (open-default-app! url open))]
+
+      (doto web-contents
+        (.on "context-menu" context-menu-handler)
+        (.on "new-window" new-win-handler)
+        (.on "will-navigate" will-navigate-handler))
 
       (doto win
         (.on "enter-full-screen" #(.send web-contents "full-screen" "enter"))
-        (.on "leave-full-screen" #(.send web-contents "full-screen" "leave"))))))
+        (.on "leave-full-screen" #(.send web-contents "full-screen" "leave")))
+
+      ;; clear
+      (fn []
+        (doto web-contents
+          (.off "context-menu" context-menu-handler)
+          (.off "new-window" new-win-handler)
+          (.off "will-navigate" will-navigate-handler))
+
+        (.off win "enter-full-screen")
+        (.off win "leave-full-screen")))
+    #()))

+ 0 - 8
src/main/frontend/components/file.cljs

@@ -105,14 +105,6 @@
          (and format (contains? (config/img-formats) format))
          [:img {:src path}]
 
-         (and format (contains? config/markup-formats format))
-         (when-let [file-content (db/get-file path)]
-           (let [content (string/trim file-content)]
-             (content/content path {:config {:file? true
-                                             :file-path path}
-                                    :content content
-                                    :format format})))
-
          (and format (contains? (config/text-formats) format))
          (when-let [file-content (db/get-file path)]
            (let [content (string/trim file-content)

+ 128 - 27
src/main/frontend/components/plugins.cljs

@@ -163,7 +163,7 @@
   (let [disabled (:disabled settings)
         name (or title name "Untitled")
         unpacked? (not iir)
-        new-version (and coming-update (:latest-version coming-update))]
+        new-version (state/coming-update-new-version? coming-update)]
     (rum/with-context
       [[t] i18n/*tongue-context*]
 
@@ -182,7 +182,7 @@
           svg/folder)
 
         (when (and (not market?) unpacked?)
-          [:span.flex.justify-center.text-xs.text-red-500.pt-2 "unpacked"])]
+          [:span.flex.justify-center.text-xs.text-red-500.pt-2 (t :plugin/unpacked)])]
 
        [:div.r
         [:h3.head.text-xl.font-bold.pt-1.5
@@ -298,7 +298,7 @@
 (rum/defc panel-control-tabs
   < rum/static
   [t search-key *search-key category *category
-   sort-by *sort-by selected-unpacked-pkg
+   sort-or-filter-by *sort-or-filter-by selected-unpacked-pkg
    market? develop-mode? reload-market-fn]
 
   (let [*search-ref (rum/create-ref)]
@@ -350,31 +350,47 @@
          :value       (or search-key "")}]]
 
 
-      ;; sorter
+      ;; sorter & filter
       (ui/dropdown-with-links
         (fn [{:keys [toggle-fn]}]
           (ui/button
-            [:span (ui/icon "arrows-sort") ""]
-            :class "sort-by"
+            [:span (ui/icon (if market? "arrows-sort" "filter"))]
+            :class (str (when-not (contains? #{:default :downloads} sort-or-filter-by) "picked ") "sort-or-filter-by")
             :on-click toggle-fn
             :intent "link"))
-        (let [aim-icon #(if (= sort-by %) "check" "circle")]
+        (let [aim-icon #(if (= sort-or-filter-by %) "check" "circle")]
           (if market?
-            [{:title   "Downloads (Desc)"
-              :options {:on-click #(reset! *sort-by :downloads)}
+            [{:title   (t :plugin/downloads)
+              :options {:on-click #(reset! *sort-or-filter-by :downloads)}
               :icon    (ui/icon (aim-icon :downloads))}
 
-             {:title   "Stars (Desc)"
-              :options {:on-click #(reset! *sort-by :stars)}
+             {:title   (t :plugin/stars)
+              :options {:on-click #(reset! *sort-or-filter-by :stars)}
               :icon    (ui/icon (aim-icon :stars))}
 
-             {:title   "Title (A - Z)"
-              :options {:on-click #(reset! *sort-by :letters)}
+             {:title   (str (t :plugin/title) " (A - Z)")
+              :options {:on-click #(reset! *sort-or-filter-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))}]))
+            [{:title   (t :plugin/all)
+              :options {:on-click #(reset! *sort-or-filter-by :default)}
+              :icon    (ui/icon (aim-icon :default))}
+
+             {:title   (t :plugin/enabled)
+              :options {:on-click #(reset! *sort-or-filter-by :enabled)}
+              :icon    (ui/icon (aim-icon :enabled))}
+
+             {:title   (t :plugin/disabled)
+              :options {:on-click #(reset! *sort-or-filter-by :disabled)}
+              :icon    (ui/icon (aim-icon :disabled))}
+
+             {:title   (t :plugin/unpacked)
+              :options {:on-click #(reset! *sort-or-filter-by :unpacked)}
+              :icon    (ui/icon (aim-icon :unpacked))}
+
+             {:title   (t :plugin/update-available)
+              :options {:on-click #(reset! *sort-or-filter-by :update-available)}
+              :icon    (ui/icon (aim-icon :update-available))}]))
         {})
 
       ;; more - updater
@@ -396,11 +412,11 @@
                    {: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")))}}
+                                      (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))}}]))
+                                      (js/apis.openPath root))}}]))
         {})
 
       ;; developer
@@ -465,7 +481,7 @@
         sorted-pkgs (apply sort-by
                            (conj
                              (case @*sort-by
-                               :letters [:title #(compare %1 %2)]
+                               :letters [#(util/safe-lower-case (or (:title %) (:name %)))]
                                [@*sort-by #(compare %2 %1)])
                              filtered-pkgs))]
 
@@ -511,7 +527,7 @@
 (rum/defcs installed-plugins
   < rum/static rum/reactive
     (rum/local "" ::search-key)
-    (rum/local :enabled ::sort-by)                          ;; enabled / letters / updates
+    (rum/local :default ::filter-by)                        ;; default / enabled / disabled / unpacked / update-available
     (rum/local :plugins ::category)
   [state]
   (let [installed-plugins (state/sub :plugin/installed-plugins)
@@ -520,13 +536,24 @@
         develop-mode? (state/sub :ui/developer-mode?)
         selected-unpacked-pkg (state/sub :plugin/selected-unpacked-pkg)
         coming-updates (state/sub :plugin/updates-coming)
-        *sort-by (::sort-by state)
+        *filter-by (::filter-by state)
         *search-key (::search-key state)
         *category (::category state)
+        default-filter-by? (= :default @*filter-by)
         filtered-plugins (when (seq installed-plugins)
                            (if (= @*category :themes)
                              (filter #(:theme %) installed-plugins)
                              (filter #(not (:theme %)) installed-plugins)))
+        filtered-plugins (if-not default-filter-by?
+                           (filter (fn [it]
+                                     (let [disabled (get-in it [:settings :disabled])]
+                                       (case @*filter-by
+                                         :enabled (not disabled)
+                                         :disabled disabled
+                                         :unpacked (not (:iir it))
+                                         :update-available (state/plugin-update-available? (:id it))
+                                         true))) filtered-plugins)
+                           filtered-plugins)
         filtered-plugins (if-not (string/blank? @*search-key)
                            (if-let [author (and (string/starts-with? @*search-key "@")
                                                 (subs @*search-key 1))]
@@ -536,11 +563,13 @@
                                :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))))
-                            (flatten))]
+        sorted-plugins (if default-filter-by?
+                         (->> filtered-plugins
+                              (reduce #(let [k (if (get-in %2 [:settings :disabled]) 1 0)]
+                                         (update %1 k conj %2)) [[] []])
+                              (#(update % 0 (fn [coll] (sort-by :iir coll))))
+                              (flatten))
+                         filtered-plugins)]
     (rum/with-context
       [[t] i18n/*tongue-context*]
 
@@ -550,7 +579,7 @@
          t
          @*search-key *search-key
          @*category *category
-         @*sort-by *sort-by
+         @*filter-by *filter-by
          selected-unpacked-pkg
          false develop-mode? nil)
 
@@ -563,6 +592,71 @@
                 (and updating (= (keyword (:id updating)) pid))
                 true nil (get coming-updates pid))) (:id item)))]])))
 
+(rum/defcs waiting-coming-updates
+  < rum/reactive
+    {:will-mount (fn [s] (state/reset-unchecked-update) s)}
+  [_s]
+  (let [_ (state/sub :plugin/updates-coming)
+        downloading? (state/sub :plugin/updates-downloading?)
+        unchecked (state/sub :plugin/updates-unchecked)
+        updates (state/all-available-coming-updates)]
+
+    [:div.cp__plugins-waiting-updates
+     [:h1.mb-4.text-2xl.p-1 (util/format "Found %s updates" (util/safe-parse-int (count updates)))]
+
+     (if (seq updates)
+       ;; lists
+       [:ul
+        {:class (when downloading? "downloading")}
+        (for [it updates
+              :let [k (str "lsp-it-" (:id it))
+                    c? (not (contains? unchecked (:id it)))
+                    notes (util/trim-safe (:latest-notes it))]]
+          [:li.flex.items-center
+           {:key   k
+            :class (when c? "checked")}
+
+           [:label.flex-1
+            {:for k}
+            (ui/checkbox {:id        k
+                          :checked   c?
+                          :on-change (fn [^js e]
+                                       (when-not downloading?
+                                         (state/set-unchecked-update (:id it) (not (util/echecked? e)))))})
+            [:strong.px-3 (:title it)
+             [:sup (str (:version it) " 👉 " (:latest-version it))]]]
+
+           [:div.px-4
+            (when-not (string/blank? notes)
+              (ui/tippy
+                {:html [:p notes]}
+                [:span.opacity-30.hover:opacity-80 (ui/icon "info-circle")]))]])]
+
+       ;; all done
+       [:div.py-4 [:strong.text-4xl "\uD83C\uDF89 All updated!"]])
+
+     ;; actions
+     (when (seq updates)
+       [:div.pt-5
+        (ui/button
+          (if downloading?
+            [:span (ui/loading " Downloading...")]
+            [:span "Update all of selected"])
+
+          :on-click
+          #(when-not downloading?
+             (plugin-handler/open-updates-downloading)
+             (if-let [n (state/get-next-selected-coming-update)]
+               (plugin-handler/check-or-update-marketplace-plugin
+                 (assoc n :only-check false)
+                 (fn [^js e] (notification/show! e :error)))
+               (plugin-handler/close-updates-downloading)))
+
+          :disabled
+          (or downloading?
+              (and (not (empty? unchecked))
+                   (= (count unchecked) (count updates)))))])]))
+
 (defn open-select-theme!
   []
   (state/set-sub-modal! installed-themes))
@@ -666,3 +760,10 @@
   (state/set-modal!
     (fn [_close!]
       (plugins-page))))
+
+(defn open-waiting-updates-modal!
+  []
+  (state/set-sub-modal!
+    (fn [_close!]
+      (waiting-coming-updates))
+    {:center? true}))

+ 36 - 1
src/main/frontend/components/plugins.css

@@ -79,6 +79,7 @@
     }
 
     .ui__button {
+      position: relative;
       border: none;
 
       &:active {
@@ -104,9 +105,22 @@
         background: transparent;
       }
 
-      &.sort-by, &.more-do {
+      &.sort-or-filter-by, &.more-do {
         padding: 0 4px;
       }
+
+      &.picked {
+        &:after {
+          content: " ";
+          position: absolute;
+          top: -2px;
+          right: 4px;
+          background-color: red;
+          width: 4px;
+          height: 4px;
+          border-radius: 50%;
+        }
+      }
     }
 
     .search-ctls {
@@ -411,6 +425,27 @@
 
   &-details {
   }
+
+  &-waiting-updates {
+    margin: -15px;
+
+    > ul {
+      li {
+        user-select: none;
+        justify-content: space-between;
+        opacity: .9;
+
+        sup {
+          padding-left: 8px;
+          font-weight: 400;
+        }
+
+        &:hover, &.checked {
+          opacity: 1;
+        }
+      }
+    }
+  }
 }
 
 .cp__themes {

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

@@ -287,7 +287,7 @@
 
 (rum/defc main <
   {:did-mount (fn [state]
-                (when-let [element (gdom/getElement "main-content")]
+                (when-let [element (gdom/getElement "main-content-container")]
                   (dnd/subscribe!
                    element
                    :upload-files

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

@@ -354,9 +354,16 @@
         :plugin/check-all-updates "Check all updates"
         :plugin/refresh-lists "Refresh lists"
         :plugin/enabled "Enabled"
+        :plugin/disabled "Disabled"
+        :plugin/update-available "Update available"
         :plugin/updating "Updating"
         :plugin/uninstall "Uninstall"
         :plugin/marketplace "Marketplace"
+        :plugin/downloads "Downloads"
+        :plugin/stars "Stars"
+        :plugin/title "Title"
+        :plugin/all "All"
+        :plugin/unpacked "Unpacked"
         :plugin/delete-alert "Are you sure uninstall plugin [{1}]?"
         :plugin/open-settings "Open settings"
         :plugin/open-package "Open package"
@@ -1132,9 +1139,16 @@
            :plugin/refresh-lists "刷新插件列表"
            :plugin/delete-alert "确定删除插件 [{1}]?"
            :plugin/enabled "已开启"
+           :plugin/disabled "未开启"
+           :plugin/update-available "待更新"
            :plugin/updating "更新中"
            :plugin/uninstall "卸载"
            :plugin/marketplace "插件市场"
+           :plugin/downloads "下载量"
+           :plugin/stars "收藏数"
+           :plugin/title "名称"
+           :plugin/all "全部"
+           :plugin/unpacked "未打包"
            :plugin/open-settings "打开配置项"
            :plugin/open-package "打开包目录"
            :plugin/load-unpacked "手动载入插件"

+ 1 - 2
src/main/frontend/extensions/code.cljs

@@ -183,8 +183,7 @@
             (file-handler/alter-file (state/get-current-repo)
                                      path
                                      (str (string/trim value) "\n")
-                                     {:re-render-root? true})
-            (notification/show! "Saved file!" :success)))
+                                     {:re-render-root? true})))
 
         :else
         nil))))

+ 2 - 2
src/main/frontend/fs/node.cljs

@@ -35,7 +35,7 @@
       (p/resolved (= (string/trim disk-content) (string/trim db-content))))))
 
 (defn- write-file-impl!
-  [this repo dir path content {:keys [ok-handler error-handler skip-compare?]} stat]
+  [this repo dir path content {:keys [ok-handler error-handler old-content skip-compare?]} stat]
   (if skip-compare?
     (p/catch
         (p/let [result (ipc/ipc "writeFile" repo path content)]
@@ -53,7 +53,7 @@
                                           nil))))
             disk-content (or disk-content "")
             ext (string/lower-case (util/get-file-ext path))
-            db-content (or (db/get-file repo path) "")
+            db-content (or old-content (db/get-file repo path) "")
             contents-matched? (contents-matched? disk-content db-content)
             pending-writes (state/get-write-chan-length)]
       (cond

+ 30 - 10
src/main/frontend/handler/events.cljs

@@ -211,6 +211,10 @@
 (defmethod handle :go/plugins [_]
   (plugin/open-plugins-modal!))
 
+(defmethod handle :go/plugins-waiting-lists [_]
+  (plugin/open-waiting-updates-modal!))
+
+
 (defmethod handle :redirect-to-home [_]
   (page-handler/create-today-journal!))
 
@@ -236,16 +240,32 @@
     (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)))))
+  (let [downloading? (:plugin/updates-downloading? @state/state)]
+
+    (when-let [coming (and (not downloading?)
+                           (get-in @state/state [:plugin/updates-coming id]))]
+      (notification/show!
+        (str "Checked: " (:title coming))
+        :success))
+
+    (if (and updated? downloading?)
+      ;; try to start consume downloading item
+      (if-let [n (state/get-next-selected-coming-update)]
+        (plugin-handler/check-or-update-marketplace-plugin
+          (assoc n :only-check false :error-code nil)
+          (fn [^js e] (js/console.error "[Download Err]" n e)))
+        (plugin-handler/close-updates-downloading))
+
+      ;; try to start consume pending item
+      (if-let [n (second (first (:plugin/updates-pending @state/state)))]
+        (plugin-handler/check-or-update-marketplace-plugin
+          (assoc n :only-check true :error-code nil)
+          (fn [^js e]
+            (notification/show! (.toString e) :error)
+            (js/console.error "[Check Err]" n e)))
+        ;; try to open waiting updates list
+        (when (and pending? (seq (state/all-available-coming-updates)))
+          (plugin/open-waiting-updates-modal!))))))
 
 (defn run!
   []

+ 42 - 17
src/main/frontend/handler/plugin.cljs

@@ -96,12 +96,16 @@
     (p/catch
       (p/then
         (do (state/set-state! :plugin/installing pkg)
-            (load-marketplace-plugins false))
+            (p/catch
+              (load-marketplace-plugins false)
+              (fn [^js e]
+                (state/reset-all-updates-state)
+                (throw e))))
         (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))))
+            (throw (js/Error. (str ":not-found-in-marketplace" id))))
           true))
 
       (fn [^js e]
@@ -116,6 +120,23 @@
     (catch js/Error e
       nil)))
 
+(defn open-updates-downloading
+  []
+  (when (and (not (:plugin/updates-downloading? @state/state))
+             (seq (state/all-available-coming-updates)))
+    (->> (:plugin/updates-coming @state/state)
+         (map #(if (state/coming-update-new-version? (second %1))
+                 (update % 1 dissoc :error-code) %1))
+         (into {})
+         (state/set-state! :plugin/updates-coming))
+    (state/set-state! :plugin/updates-downloading? true)))
+
+(defn close-updates-downloading
+  []
+  (when (:plugin/updates-downloading? @state/state)
+    (state/set-state! :plugin/updates-downloading? false)))
+
+
 (defn setup-install-listener!
   [t]
   (let [channel (name :lsp-installed)
@@ -140,7 +161,7 @@
                                       (str (t :plugin/update) (t :plugins) ": " name " - " (.-version (.-options pl))) :success)
                                     (state/consume-updates-coming-plugin payload true))))
 
-                             (do                            ;; register new
+                             (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))))
@@ -155,14 +176,18 @@
                                           [(str (t :plugin/up-to-date) " :)") :success]
 
                                           [error-code :error])
-                             updates-pending? (seq (:plugin/updates-pending @state/state))]
+                             pending? (seq (:plugin/updates-pending @state/state))]
 
-                         (if (and only-check updates-pending?)
+                         (if (and only-check pending?)
                            (state/consume-updates-coming-plugin payload false)
-                           (notifications/show!
-                             (str
-                               (if (= :error type) "[Install Error]" "")
-                               msg) type))
+
+                           (do
+                             ;; consume failed download updates
+                             (state/consume-updates-coming-plugin payload true)
+                             (notifications/show!
+                               (str
+                                 (if (= :error type) "[Install Error]" "")
+                                 msg) type)))
 
                          (js/console.error payload))
 
@@ -299,9 +324,9 @@
       ;; local
       (-> (p/let [content (invoke-exported-api "load_plugin_readme" url)
                   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-sub-modal! (fn [_] (display))))
+                 (and (string/blank? (string/trim content)) (throw nil))
+                 (state/set-state! :plugin/active-readme [content item])
+                 (state/set-sub-modal! (fn [_] (display))))
           (p/catch #(do (js/console.warn %)
                         (notifications/show! "No README content." :warn))))
       ;; market
@@ -311,8 +336,8 @@
   []
   (when util/electron?
     (p/let [path (ipc/ipc "openDialog")]
-      (when-not (:plugin/selected-unpacked-pkg @state/state)
-        (state/set-state! :plugin/selected-unpacked-pkg path)))))
+           (when-not (:plugin/selected-unpacked-pkg @state/state)
+             (state/set-state! :plugin/selected-unpacked-pkg path)))))
 
 (defn reset-unpacked-state
   []
@@ -354,7 +379,7 @@
   (p/catch
     (p/let [files ^js (ipc/ipc "getUserDefaultPlugins")
             files (js->clj files)]
-      (map #(hash-map :url %) files))
+           (map #(hash-map :url %) files))
     (fn [e]
       (js/console.error e))))
 
@@ -363,9 +388,9 @@
   (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?))))]
+                            (seq (take 32 (state/get-enabled-installed-plugins theme?))))]
       (state/set-state! :plugin/updates-pending
-        (into {} (map (fn [v] [(keyword (:id v)) v]) plugins)))
+                        (into {} (map (fn [v] [(keyword (:id v)) v]) plugins)))
       (state/pub-event! [:plugin/consume-updates]))))
 
 ;; components

+ 178 - 138
src/main/frontend/state.cljs

@@ -22,41 +22,41 @@
 
 (defonce state
   (let [document-mode? (or (storage/get :document/mode?) false)
-        current-graph (let [graph (storage/get :git/current-repo)]
-                        (when graph (ipc/ipc "setCurrentGraph" graph))
-                        graph)]
-    (atom
-     {:route-match nil
-      :today nil
-      :system/events (async/chan 100)
-      :db/batch-txs (async/chan 100)
-      :file/writes (async/chan 100)
-      :notification/show? false
-      :notification/content nil
-      :repo/cloning? false
-      :repo/loading-files? {}
-      :repo/importing-to-db? nil
-      :repo/sync-status {}
-      :repo/changed-files nil
-      :nfs/user-granted? {}
-      :nfs/refreshing? nil
-      :instrument/disabled? (storage/get "instrument-disabled")
+       current-graph (let [graph (storage/get :git/current-repo)]
+                       (when graph (ipc/ipc "setCurrentGraph" graph))
+                       graph)]
+   (atom
+     {:route-match                           nil
+      :today                                 nil
+      :system/events                         (async/chan 100)
+      :db/batch-txs                          (async/chan 100)
+      :file/writes                           (async/chan 100)
+      :notification/show?                    false
+      :notification/content                  nil
+      :repo/cloning?                         false
+      :repo/loading-files?                   {}
+      :repo/importing-to-db?                 nil
+      :repo/sync-status                      {}
+      :repo/changed-files                    nil
+      :nfs/user-granted?                     {}
+      :nfs/refreshing?                       nil
+      :instrument/disabled?                  (storage/get "instrument-disabled")
       ;; TODO: how to detect the network reliably?
-      :network/online? true
-      :indexeddb/support? true
-      :me nil
-      :git/current-repo current-graph
-      :git/status {}
-      :format/loading {}
-      :draw? false
-      :db/restoring? nil
-
-      :journals-length 2
-
-      :search/q ""
-      :search/mode :global
-      :search/result nil
-      :search/graph-filters []
+      :network/online?                       true
+      :indexeddb/support?                    true
+      :me                                    nil
+      :git/current-repo                      current-graph
+      :git/status                            {}
+      :format/loading                        {}
+      :draw?                                 false
+      :db/restoring?                         nil
+
+      :journals-length                       2
+
+      :search/q                              ""
+      :search/mode                           :global
+      :search/result                         nil
+      :search/graph-filters                  []
 
       ;; modals
       :modal/label                           ""
@@ -67,15 +67,15 @@
       :modal/subsets                         []
 
       ;; right sidebar
-      :ui/fullscreen? false
-      :ui/settings-open? false
-      :ui/sidebar-open? false
-      :ui/left-sidebar-open? (boolean (storage/get "ls-left-sidebar-open?"))
-      :ui/theme (or (storage/get :ui/theme) (if (mobile/is-native-platform?) "light" "dark"))
-      :ui/system-theme? ((fnil identity (or util/mac? util/win32? false)) (storage/get :ui/system-theme?))
-      :ui/wide-mode? false
+      :ui/fullscreen?                        false
+      :ui/settings-open?                     false
+      :ui/sidebar-open?                      false
+      :ui/left-sidebar-open?                 (boolean (storage/get "ls-left-sidebar-open?"))
+      :ui/theme                              (or (storage/get :ui/theme) (if (mobile/is-native-platform?) "light" "dark"))
+      :ui/system-theme?                      ((fnil identity (or util/mac? util/win32? false)) (storage/get :ui/system-theme?))
+      :ui/wide-mode?                         false
       ;; :show-all, :hide-block-body, :hide-block-children
-      :ui/cycle-collapse :show-all
+      :ui/cycle-collapse                     :show-all
 
       ;; ui/collapsed-blocks is to separate the collapse/expand state from db for:
       ;; 1. right sidebar
@@ -83,131 +83,133 @@
       ;; 3. queries
       ;; 4. references
       ;; graph => {:block-id bool}
-      :ui/collapsed-blocks {}
-      :ui/sidebar-collapsed-blocks {}
-      :ui/root-component nil
-      :ui/file-component nil
-      :ui/custom-query-components {}
-      :ui/show-recent? false
-      :ui/command-palette-open? false
-      :ui/developer-mode? (or (= (storage/get "developer-mode") "true")
-                              false)
+      :ui/collapsed-blocks                   {}
+      :ui/sidebar-collapsed-blocks           {}
+      :ui/root-component                     nil
+      :ui/file-component                     nil
+      :ui/custom-query-components            {}
+      :ui/show-recent?                       false
+      :ui/command-palette-open?              false
+      :ui/developer-mode?                    (or (= (storage/get "developer-mode") "true")
+                                                 false)
       ;; remember scroll positions of visited paths
-      :ui/paths-scroll-positions {}
-      :ui/shortcut-tooltip? (if (false? (storage/get :ui/shortcut-tooltip?))
-                              false
-                              true)
-      :ui/visual-viewport-pending? false
-      :ui/visual-viewport-state nil
-
-      :document/mode? document-mode?
-
-      :github/contents {}
-      :config {}
-      :block/component-editing-mode? false
-      :editor/draw-mode? false
-      :editor/show-page-search? false
-      :editor/show-page-search-hashtag? false
-      :editor/show-date-picker? false
+      :ui/paths-scroll-positions             {}
+      :ui/shortcut-tooltip?                  (if (false? (storage/get :ui/shortcut-tooltip?))
+                                               false
+                                               true)
+      :ui/visual-viewport-pending?           false
+      :ui/visual-viewport-state              nil
+
+      :document/mode?                        document-mode?
+
+      :github/contents                       {}
+      :config                                {}
+      :block/component-editing-mode?         false
+      :editor/draw-mode?                     false
+      :editor/show-page-search?              false
+      :editor/show-page-search-hashtag?      false
+      :editor/show-date-picker?              false
       ;; With label or other data
-      :editor/show-input nil
-      :editor/show-zotero false
-      :editor/last-saved-cursor nil
-      :editor/editing? nil
-      :editor/last-edit-block-input-id nil
-      :editor/last-edit-block-id nil
-      :editor/in-composition? false
-      :editor/content {}
-      :editor/block nil
-      :editor/block-dom-id nil
-      :editor/set-timestamp-block nil
-      :editor/last-input-time nil
-      :editor/pos nil
-      :editor/document-mode? document-mode?
-      :editor/args nil
-      :editor/on-paste? false
-      :editor/last-key-code nil
-
-      :db/last-transact-time {}
-      :db/last-persist-transact-ids {}
+      :editor/show-input                     nil
+      :editor/show-zotero                    false
+      :editor/last-saved-cursor              nil
+      :editor/editing?                       nil
+      :editor/last-edit-block-input-id       nil
+      :editor/last-edit-block-id             nil
+      :editor/in-composition?                false
+      :editor/content                        {}
+      :editor/block                          nil
+      :editor/block-dom-id                   nil
+      :editor/set-timestamp-block            nil
+      :editor/last-input-time                nil
+      :editor/pos                            nil
+      :editor/document-mode?                 document-mode?
+      :editor/args                           nil
+      :editor/on-paste?                      false
+      :editor/last-key-code                  nil
+
+      :db/last-transact-time                 {}
+      :db/last-persist-transact-ids          {}
       ;; whether database is persisted
-      :db/persisted? {}
-      :db/latest-txs (or (storage/get-transit :db/latest-txs) {})
-      :cursor-range nil
+      :db/persisted?                         {}
+      :db/latest-txs                         (or (storage/get-transit :db/latest-txs) {})
+      :cursor-range                          nil
 
-      :selection/mode false
-      :selection/blocks []
-      :selection/start-block nil
+      :selection/mode                        false
+      :selection/blocks                      []
+      :selection/start-block                 nil
       ;; either :up or :down, defaults to down
       ;; used to determine selection direction when two or more blocks are selected
-      :selection/direction :down
-      :custom-context-menu/show? false
-      :custom-context-menu/links nil
+      :selection/direction                   :down
+      :custom-context-menu/show?             false
+      :custom-context-menu/links             nil
 
       ;; pages or blocks in the right sidebar
       ;; It is a list of `[repo db-id block-type block-data]` 4-tuple
-      :sidebar/blocks '()
+      :sidebar/blocks                        '()
 
-      :preferred-language (storage/get :preferred-language)
+      :preferred-language                    (storage/get :preferred-language)
 
       ;; electron
-      :electron/auto-updater-downloaded false
-      :electron/updater-pending? false
-      :electron/updater {}
-      :electron/user-cfgs nil
+      :electron/auto-updater-downloaded      false
+      :electron/updater-pending?             false
+      :electron/updater                      {}
+      :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      []
-      :plugin/installed-commands    {}
-      :plugin/installed-ui-items    {}
-      :plugin/simple-commands       {}
-      :plugin/selected-theme        nil
-      :plugin/selected-unpacked-pkg nil
-      :plugin/marketplace-pkgs      nil
-      :plugin/marketplace-stats     nil
-      :plugin/installing            nil
-      :plugin/active-readme         nil
-      :plugin/updates-pending       {}
-      :plugin/updates-coming        {}
+      :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               []
+      :plugin/installed-commands             {}
+      :plugin/installed-ui-items             {}
+      :plugin/simple-commands                {}
+      :plugin/selected-theme                 nil
+      :plugin/selected-unpacked-pkg          nil
+      :plugin/marketplace-pkgs               nil
+      :plugin/marketplace-stats              nil
+      :plugin/installing                     nil
+      :plugin/active-readme                  nil
+      :plugin/updates-pending                {}
+      :plugin/updates-coming                 {}
+      :plugin/updates-downloading?           false
+      :plugin/updates-unchecked              #{}
 
       ;; pdf
-      :pdf/current                  nil
-      :pdf/ref-highlight            nil
+      :pdf/current                           nil
+      :pdf/ref-highlight                     nil
 
       ;; all notification contents as k-v pairs
-      :notification/contents {}
-      :graph/syncing? false
+      :notification/contents                 {}
+      :graph/syncing?                        false
 
       ;; copied blocks
-      :copy/blocks {:copy/content nil :copy/block-tree nil}
+      :copy/blocks                           {:copy/content nil :copy/block-tree nil}
 
-      :copy/export-block-text-indent-style  (or (storage/get :copy/export-block-text-indent-style)
-                                                "dashes")
+      :copy/export-block-text-indent-style   (or (storage/get :copy/export-block-text-indent-style)
+                                                 "dashes")
       :copy/export-block-text-remove-options (or (storage/get :copy/export-block-text-remove-options)
                                                  #{})
-      :date-picker/date nil
+      :date-picker/date                      nil
 
-      :youtube/players {}
+      :youtube/players                       {}
 
       ;; command palette
-      :command-palette/commands []
+      :command-palette/commands              []
 
-      :view/components {}
+      :view/components                       {}
 
-      :debug/write-acks {}
+      :debug/write-acks                      {}
 
-      :encryption/graph-parsing? false
+      :encryption/graph-parsing?             false
 
-      :favorites/dragging nil
+      :favorites/dragging                    nil
 
-      :srs/mode? false
+      :srs/mode?                             false
 
-      :srs/cards-due-count nil})))
+      :srs/cards-due-count                   nil})))
 
 ;; block uuid -> {content(String) -> ast}
 (def blocks-ast-cache (atom (cache/lru-cache-factory {} :threshold 5000)))
@@ -1013,7 +1015,7 @@
                   (ipc/ipc "userAppCfgs")
                   (:electron/user-cfgs @state))
            cfgs (if (object? cfgs) (bean/->clj cfgs) cfgs)]
-     (set-state! :electron/user-cfgs cfgs))))
+          (set-state! :electron/user-cfgs cfgs))))
 
 (defn setup-electron-updater!
   []
@@ -1704,13 +1706,51 @@
 (defn consume-updates-coming-plugin
   [payload updated?]
   (when-let [id (keyword (:id payload))]
-    (let [pending? (seq (:plugin/updates-pending @state))]
+    (let [pending? (boolean (seq (:plugin/updates-pending @state)))]
       (swap! state update :plugin/updates-pending dissoc id)
       (if updated?
-        (swap! state update :plugin/updates-coming dissoc id)
+        (if-let [error (:error-code payload)]
+          (swap! state update-in [:plugin/updates-coming id] assoc :error-code error)
+          (swap! state update :plugin/updates-coming dissoc id))
         (swap! state update :plugin/updates-coming assoc id payload))
       (pub-event! [:plugin/consume-updates id pending? updated?]))))
 
+(defn coming-update-new-version?
+  [pkg]
+  (and pkg (:latest-version pkg)))
+
+(defn plugin-update-available?
+  [id]
+  (when-let [pkg (and id (get (:plugin/updates-coming @state) (keyword id)))]
+    (coming-update-new-version? pkg)))
+
+(defn all-available-coming-updates
+  []
+  (when-let [updates (vals (:plugin/updates-coming @state))]
+    (filterv #(coming-update-new-version? %) updates)))
+
+(defn get-next-selected-coming-update
+  []
+  (when-let [updates (all-available-coming-updates)]
+    (let [unchecked (:plugin/updates-unchecked @state)]
+      (first (filter #(and (not (and (seq unchecked) (contains? unchecked (:id %))))
+                           (not (:error-code %))) updates)))))
+
+(defn set-unchecked-update
+  [id unchecked?]
+  (swap! state update :plugin/updates-unchecked (if unchecked? conj disj) id))
+
+(defn reset-unchecked-update
+  []
+  (swap! state assoc :plugin/updates-unchecked #{}))
+
+(defn reset-all-updates-state
+  []
+  (swap! state assoc
+         :plugin/updates-pending                {}
+         :plugin/updates-coming                 {}
+         :plugin/updates-downloading?           false))
+
 (defn sub-right-sidebar-blocks
   []
   (when-let [current-repo (get-current-repo)]

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

@@ -181,6 +181,7 @@ html.is-mobile {
 
   &:disabled {
     opacity: 0.5;
+    cursor: not-allowed;
   }
 
   &:hover {