Files
logseq/src/main/frontend/components/plugins.cljs

1458 lines
60 KiB
Clojure

(ns frontend.components.plugins
(:require [rum.core :as rum]
[frontend.state :as state]
[cljs-bean.core :as bean]
[frontend.context.i18n :refer [t]]
[frontend.ui :as ui]
[logseq.shui.ui :as shui]
[frontend.handler.ui :as ui-handler]
[frontend.handler.editor :as editor-handler]
[frontend.handler.plugin-config :as plugin-config-handler]
[frontend.handler.common.plugin :as plugin-common-handler]
[frontend.search :as search]
[frontend.util :as util]
[frontend.mixins :as mixins]
[frontend.config :as config]
[electron.ipc :as ipc]
[promesa.core :as p]
[frontend.components.svg :as svg]
[frontend.components.plugins-settings :as plugins-settings]
[frontend.handler.notification :as notification]
[frontend.handler.plugin :as plugin-handler]
[frontend.storage :as storage]
[frontend.rum :as rum-utils]
[clojure.string :as string]))
(declare open-waiting-updates-modal!)
(defonce PER-PAGE-SIZE 15)
(def *dirties-toggle-items (atom {}))
(defn- clear-dirties-states!
[]
(reset! *dirties-toggle-items {}))
(defn render-classic-dropdown-items
[id items]
(for [{:keys [hr item title options icon]} items]
(let [on-click' (:on-click options)]
(if hr
(shui/dropdown-menu-separator)
(shui/dropdown-menu-item
(assoc options
:on-click (fn [^js e]
(when on-click'
(when-not (false? (on-click' e))
(shui/popup-hide! id)))))
(or item
[:span.flex.items-center.gap-1.w-full
icon [:div title]]))))))
(rum/defcs installed-themes
<
(rum/local [] ::themes)
(rum/local 0 ::cursor)
(rum/local 0 ::total)
{:did-mount (fn [state]
(let [*themes (::themes state)
*cursor (::cursor state)
*total (::total state)
mode (state/sub :ui/theme)
all-themes (state/sub :plugin/installed-themes)
themes (->> all-themes
(filter #(= (:mode %) mode))
(sort-by #(:name %)))
no-mode-themes (->> all-themes
(filter #(= (:mode %) nil))
(sort-by #(:name %))
(map-indexed (fn [idx opt] (assoc opt :group-first (zero? idx) :group-desc (if (zero? idx) "light & dark themes" nil)))))
selected (state/sub :plugin/selected-theme)
themes (map-indexed (fn [idx opt]
(let [selected? (= (:url opt) selected)]
(when selected? (reset! *cursor (+ idx 1)))
(assoc opt :mode mode :selected selected?))) (concat themes no-mode-themes))
themes (cons {:name (string/join " " ["Default" (string/capitalize mode) "Theme"])
:url nil
:description (string/join " " ["Logseq default" mode "theme."])
:mode mode
:selected (nil? selected)
:group-first true
:group-desc (str mode " themes")} themes)]
(reset! *themes themes)
(reset! *total (count themes))
state))}
(mixins/event-mixin
(fn [state]
(let [*cursor (::cursor state)
*total (::total state)
^js target (rum/dom-node state)]
(.focus target)
(mixins/on-key-down
state {38 ;; up
(fn [^js _e]
(reset! *cursor
(if (zero? @*cursor)
(dec @*total) (dec @*cursor))))
40 ;; down
(fn [^js _e]
(reset! *cursor
(if (= @*cursor (dec @*total))
0 (inc @*cursor))))
13 ;; enter
#(when-let [^js active (.querySelector target ".is-active")]
(.click active))}))))
[state]
(let [*cursor (::cursor state)
*themes (::themes state)]
[:div.cp__themes-installed
{:tab-index -1}
[:h1.mb-4.text-2xl.p-1 (t :themes)]
(map-indexed
(fn [idx opt]
(let [current-selected? (:selected opt)
group-first? (:group-first opt)
plg (get (:plugin/installed-plugins @state/state) (keyword (:pid opt)))]
[:div
{:key (str idx (:name opt))}
(when (and group-first? (not= idx 0)) [:hr.my-2])
[:div.it.flex.px-3.py-1.5.rounded-sm.justify-between
{:title (:description opt)
:class (util/classnames
[{:is-selected current-selected?
:is-active (= idx @*cursor)}])
:on-click #(do (js/LSPluginCore.selectTheme (bean/->js opt))
(shui/dialog-close!))}
[:div.flex.items-center.text-xs
[:div.opacity-60 (str (or (:name plg) "Logseq") " •")]
[:div.name.ml-1 (:name opt)]]
(when (or group-first? current-selected?)
[:div.flex.items-center
(when group-first? [:small.opacity-60 (:group-desc opt)])
(when current-selected? [:small.inline-flex.ml-1.opacity-60 (ui/icon "check")])])]]))
@*themes)]))
(rum/defc unpacked-plugin-loader
[unpacked-pkg-path]
(rum/use-effect!
(fn []
(let [err-handle
(fn [^js e]
(case (keyword (aget e "name"))
:IllegalPluginPackageError
(notification/show! "Illegal Logseq plugin package." :error)
:ExistedImportedPluginPackageError
(notification/show! (str "Existed plugin package (" (.-message e) ").") :error)
:default)
(plugin-handler/reset-unpacked-state))
reg-handle #(plugin-handler/reset-unpacked-state)]
(when unpacked-pkg-path
(doto js/LSPluginCore
(.once "error" err-handle)
(.once "registered" reg-handle)
(.register (bean/->js {:url unpacked-pkg-path}))))
#(doto js/LSPluginCore
(.off "error" err-handle)
(.off "registered" reg-handle))))
[unpacked-pkg-path])
(when unpacked-pkg-path
[:strong.inline-flex.px-3 "Loading ..."]))
(rum/defc category-tabs
[t total-nums category on-action]
[:div.secondary-tabs.categories.flex
(ui/button
[:span.flex.items-center
(ui/icon "puzzle")
(t :plugins) (when (vector? total-nums) (str " (" (first total-nums) ")"))]
:intent "link"
:on-click #(on-action :plugins)
:class (if (= category :plugins) "active" ""))
(ui/button
[:span.flex.items-center
(ui/icon "palette")
(t :themes) (when (vector? total-nums) (str " (" (last total-nums) ")"))]
:intent "link"
:on-click #(on-action :themes)
:class (if (= category :themes) "active" ""))])
(rum/defc local-markdown-display
< rum/reactive
[]
(let [[content item] (state/sub :plugin/active-readme)]
[:div.cp__plugins-details
{:on-click (fn [^js/MouseEvent e]
(when-let [target (.-target e)]
(when (and (= (string/lower-case (.-nodeName target)) "a")
(not (string/blank? (. target getAttribute "href"))))
(js/apis.openExternal (. target getAttribute "href"))
(.preventDefault e))))}
(when-let [repo (:repository item)]
(when-let [repo (if (string? repo) repo (:url repo))]
[:div.p-4.rounded-md.bg-base-3
[:strong [:a.flex.items-center {:target "_blank" :href repo}
[:span.mr-1 (svg/github {:width 25 :height 25})] repo]]]))
[:div.p-1.bg-transparent.border-none.ls-block
{:style {:min-height "60vw"
:max-width 900}
:dangerouslySetInnerHTML {:__html content}}]]))
(rum/defc remote-readme-display
[repo _content]
(let [src (str "lsp://logseq.com/marketplace.html?repo=" repo)]
[:iframe.lsp-frame-readme {:src src}]))
(defn security-warning
[]
(ui/admonition
:warning
[:p.text-sm
(t :plugin/security-warning)]))
(rum/defc card-ctls-of-market < rum/static
[item stat installed? installing-or-updating?]
[:div.ctl
[:ul.l.flex.items-center
;; stars
[: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))]
(when (and downloads (> downloads 0))
[:li.flex.text-sm.items-center.pr-3
(svg/cloud-down 16) [:span.pl-1 downloads]]))]
[:div.r.flex.items-center
[:a.btn
{:class (util/classnames [{:disabled (or installed? installing-or-updating?)
:installing installing-or-updating?}])
:on-click #(plugin-common-handler/install-marketplace-plugin item)}
(if installed?
(t :plugin/installed)
(if installing-or-updating?
[:span.flex.items-center [:small svg/loading]
(t :plugin/installing)]
(t :plugin/install)))]]])
(rum/defc card-ctls-of-installed < rum/static
[id name url sponsors unpacked? disabled?
installing-or-updating? has-other-pending?
new-version item]
[:div.ctl
[:div.l
[:div.de
[:strong (ui/icon "settings")]
[:ul.menu-list
[:li {:on-click #(plugin-handler/open-plugin-settings! id false)} (t :plugin/open-settings)]
[:li {:on-click #(js/apis.openPath url)} (t :plugin/open-package)]
[:li {:on-click #(plugin-handler/open-report-modal! id name)} (t :plugin/report-security)]
[:li {:on-click
#(-> (shui/dialog-confirm!
[:b (t :plugin/delete-alert name)])
(p/then (fn []
(plugin-common-handler/unregister-plugin id)
(plugin-config-handler/remove-plugin id))))}
(t :plugin/uninstall)]]]
(when (seq sponsors)
[:div.de.sponsors
[:strong (ui/icon "coffee")]
[:ul.menu-list
(for [link sponsors]
[:li {:key link}
[:a {:href link :target "_blank"}
[:span.flex.items-center link (ui/icon "external-link")]]])]])]
[:div.r.flex.items-center
(when (and unpacked? (not disabled?))
[:a.btn
{:on-click #(js-invoke js/LSPluginCore "reload" id)}
(t :plugin/reload)])
(when (not unpacked?)
[:div.updates-actions
[:a.btn
{:class (util/classnames [{:disabled installing-or-updating?}])
:on-click #(when-not has-other-pending?
(plugin-handler/check-or-update-marketplace-plugin!
(assoc item :only-check (not new-version))
(fn [^js e] (notification/show! (.toString e) :error))))}
(if installing-or-updating?
(t :plugin/updating)
(if new-version
[:span (t :plugin/update) " 👉 " new-version]
(t :plugin/check-update)))]])
(ui/toggle (not disabled?)
(fn []
(js-invoke js/LSPluginCore (if disabled? "enable" "disable") id)
(when (nil? (get @*dirties-toggle-items (keyword id)))
(swap! *dirties-toggle-items assoc (keyword id) (not disabled?))))
true)]])
(defn get-open-plugin-readme-handler
[url item repo]
#(plugin-handler/open-readme!
url item (if repo remote-readme-display local-markdown-display)))
(rum/defc plugin-item-card < rum/static
[t {:keys [id name title version url description author icon iir repo sponsors] :as item}
disabled? market? *search-key has-other-pending?
installing-or-updating? installed? stat coming-update]
(let [name (or title name "Untitled")
unpacked? (not iir)
new-version (state/coming-update-new-version? coming-update)]
[:div.cp__plugins-item-card
{:key (str "lsp-card-" id)
:class (util/classnames
[{:market market?
:installed installed?
:updating installing-or-updating?
:has-new-version new-version}])}
[:div.l.link-block.cursor-pointer
{:on-click (get-open-plugin-readme-handler url item repo)}
(if (and icon (not (string/blank? icon)))
[:img.icon {:src (if market? (plugin-handler/pkg-asset id icon) icon)}]
svg/folder)
(when (and (not market?) unpacked?)
[:span.flex.justify-center.text-xs.text-error.pt-2 (t :plugin/unpacked)])]
[:div.r
[:h3.head.text-xl.font-bold.pt-1.5
[:span.l.link-block.cursor-pointer
{:on-click (get-open-plugin-readme-handler url item repo)}
name]
(when (not market?) [:sup.inline-block.px-1.text-xs.opacity-50 version])]
[:div.desc.text-xs.opacity-70
[:p description]
;;[:small (js/JSON.stringify (bean/->js settings))]
]
;; Author & Identity
[:div.flag
[:p.text-xs.pr-2.flex.justify-between
[: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
(when repo
[:a.flex {:target "_blank"
:href (plugin-handler/gh-repo-url repo)}
(svg/github {:width 16 :height 16})])]
(if market?
;; market ctls
(card-ctls-of-market item stat installed? installing-or-updating?)
;; installed ctls
(card-ctls-of-installed
id name url sponsors unpacked? disabled?
installing-or-updating? has-other-pending? new-version item))]]))
(rum/defc panel-tab-search < rum/static
[search-key *search-key *search-ref]
[: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")])
(shui/input
{:placeholder (t :plugin/search-plugin)
:ref *search-ref
:auto-focus true
:on-key-down (fn [^js e]
(when (= 27 (.-keyCode e))
(util/stop e)
(if (string/blank? search-key)
(some-> (js/document.querySelector ".cp__plugins-page") (.focus))
(reset! *search-key nil))))
:on-change #(let [^js target (.-target %)]
(reset! *search-key (some-> (.-value target) (string/triml))))
:value (or search-key "")})])
(rum/defc panel-tab-developer
[]
(ui/button
(t :plugin/contribute)
:href "https://github.com/logseq/marketplace"
:class "contribute"
:intent "link"
:target "_blank"))
(rum/defc user-proxy-settings-panel
[{:keys [protocol type] :as agent-opts}]
(let [type (or (not-empty type) (not-empty protocol) "system")
[opts set-opts!] (rum/use-state agent-opts)
[testing? set-testing?!] (rum/use-state false)
*test-input (rum/create-ref)
disabled? (or (= (:type opts) "system") (= (:type opts) "direct"))]
[:div.cp__settings-network-proxy-panel
[:h1.mb-2.text-2xl.font-bold (t :settings-page/network-proxy)]
[:div.p-2
[:p [:label [:strong (t :type)]
(ui/select [{:label "System" :value "system" :selected (= type "system")}
{:label "Direct" :value "direct" :selected (= type "direct")}
{:label "HTTP" :value "http" :selected (= type "http")}
{:label "SOCKS5" :value "socks5" :selected (= type "socks5")}]
(fn [_e value]
(set-opts! (assoc opts :type value :protocol value))))]]
[:p.flex
[:label.pr-4
{:class (if disabled? "opacity-50" nil)}
[:strong (t :host)]
[:input.form-input.is-small
{:value (:host opts)
:disabled disabled?
:on-change #(set-opts!
(assoc opts :host (util/trim-safe (util/evalue %))))}]]
[:label
{:class (if disabled? "opacity-50" nil)}
[:strong (t :port)]
[:input.form-input.is-small
{:value (:port opts) :type "number" :min 1 :max 65535
:disabled disabled?
:on-change #(set-opts!
(assoc opts :port (util/trim-safe (util/evalue %))))}]]]
[:hr]
[:p.flex.items-center.space-x-2
[:span.w-60
[:input.form-input.is-small
{:ref *test-input
:list "proxy-test-url-datalist"
:type "url"
:placeholder "https://"
:on-change #(set-opts!
(assoc opts :test (util/trim-safe (util/evalue %))))
:value (:test opts)}]
[:datalist#proxy-test-url-datalist
[:option "https://api.logseq.com/logseq/version"]
[:option "https://logseq-connectivity-testing-prod.s3.us-east-1.amazonaws.com/logseq-connectivity-testing"]
[:option "https://www.google.com"]
[:option "https://s3.amazonaws.com"]
[:option "https://clients3.google.com/generate_204"]]]
(ui/button (if testing? (ui/loading "Testing") "Test URL")
:intent "logseq"
:on-click #(let [val (util/trim-safe (.-value (rum/deref *test-input)))]
(when (and (not testing?) (not (string/blank? val)))
(set-testing?! true)
(-> (p/let [result (ipc/ipc :testProxyUrl val opts)]
(js->clj result :keywordize-keys true))
(p/then (fn [{:keys [code response-ms]}]
(notification/clear! :proxy-net-check)
(notification/show! (str "Success! Status " code " in " response-ms "ms.") :success)))
(p/catch (fn [e]
(notification/show! (str e) :error false :proxy-net-check)))
(p/finally (fn [] (set-testing?! false)))))))]
[:p.pt-2
(ui/button (t :save)
:on-click (fn []
(p/let [_ (ipc/ipc :setProxy opts)]
(state/set-state! [:electron/user-cfgs :settings/agent] opts))))]]]))
(rum/defc auto-check-for-updates-control
[]
(let [[enabled, set-enabled!] (rum/use-state (plugin-handler/get-enabled-auto-check-for-updates?))
text (t :plugin/auto-check-for-updates)]
[:div.flex.items-center.justify-between.px-3.py-2
{:on-click (fn []
(let [t (not enabled)]
(set-enabled! t)
(plugin-handler/set-enabled-auto-check-for-updates t)
(notification/show!
[:span text [:strong.pl-1 (if t "ON" "OFF")] "!"]
(if t :success :info))))}
[:span.pr-3.opacity-80 text]
(ui/toggle enabled #() true)]))
(rum/defc ^:large-vars/cleanup-todo panel-control-tabs < rum/static
[search-key *search-key category *category
sort-by *sort-by filter-by *filter-by total-nums
selected-unpacked-pkg market? develop-mode?
reload-market-fn agent-opts]
(let [*search-ref (rum/create-ref)]
[:div.pb-3.flex.justify-between.control-tabs.relative
[:div.flex.items-center.l
(category-tabs t total-nums category #(reset! *category %))
(when (and develop-mode? (not market?))
[:div
(ui/tippy {:html [:div (t :plugin/unpacked-tips)]
:arrow true}
(ui/button
(t :plugin/load-unpacked)
{:icon "upload"
:intent "link"
:class "load-unpacked"
:on-click plugin-handler/load-unpacked-plugin}))
(unpacked-plugin-loader selected-unpacked-pkg)])]
[:div.flex.items-center.r
;; extra info
(when-let [proxy-val (state/http-proxy-enabled-or-val?)]
(ui/button
[:span.flex.items-center.text-indigo-500
(ui/icon "world-download") proxy-val]
:small? true
:intent "link"
:on-click #(state/pub-event! [:go/proxy-settings agent-opts])))
;; search
(panel-tab-search search-key *search-key *search-ref)
;; sorter & filter
(let [aim-icon #(if (= filter-by %) "check" "circle")
items (if market?
[{:title (t :plugin/all)
:options {:on-click #(reset! *filter-by :default)}
:icon (ui/icon (aim-icon :default))}
{:title (t :plugin/installed)
:options {:on-click #(reset! *filter-by :installed)}
:icon (ui/icon (aim-icon :installed))}
{:title (t :plugin/not-installed)
:options {:on-click #(reset! *filter-by :not-installed)}
:icon (ui/icon (aim-icon :not-installed))}]
[{:title (t :plugin/all)
:options {:on-click #(reset! *filter-by :default)}
:icon (ui/icon (aim-icon :default))}
{:title (t :plugin/enabled)
:options {:on-click #(reset! *filter-by :enabled)}
:icon (ui/icon (aim-icon :enabled))}
{:title (t :plugin/disabled)
:options {:on-click #(reset! *filter-by :disabled)}
:icon (ui/icon (aim-icon :disabled))}
{:title (t :plugin/unpacked)
:options {:on-click #(reset! *filter-by :unpacked)}
:icon (ui/icon (aim-icon :unpacked))}
{:title (t :plugin/update-available)
:options {:on-click #(reset! *filter-by :update-available)}
:icon (ui/icon (aim-icon :update-available))}])]
(ui/button
(ui/icon "filter")
:class (str (when-not (contains? #{:default} filter-by) "picked ") "sort-or-filter-by")
:on-click #(shui/popup-show! (.-target %)
(fn [{:keys [id]}]
(render-classic-dropdown-items id items))
{:as-dropdown? true})
:variant :ghost))
(when market?
(let [aim-icon #(if (= sort-by %) "check" "circle")
items [{:title (t :plugin/downloads)
:options {:on-click #(reset! *sort-by :downloads)}
:icon (ui/icon (aim-icon :downloads))}
{:title (t :plugin/stars)
:options {:on-click #(reset! *sort-by :stars)}
:icon (ui/icon (aim-icon :stars))}
{:title (t :plugin/title "A - Z")
:options {:on-click #(reset! *sort-by :letters)}
:icon (ui/icon (aim-icon :letters))}]]
(ui/button
(ui/icon "arrows-sort")
:class (str (when-not (contains? #{:default :downloads} sort-by) "picked ") "sort-or-filter-by")
:on-click #(shui/popup-show! (.-target %)
(fn [{:keys [id]}]
(render-classic-dropdown-items id items))
{:as-dropdown? true})
:variant :ghost)))
;; more - updater
(let [items (concat (if market?
[{:title [:span.flex.items-center.gap-1 (ui/icon "rotate-clockwise") (t :plugin/refresh-lists)]
:options {:on-click #(reload-market-fn)}}]
[{:title [:span.flex.items-center.gap-1 (ui/icon "rotate-clockwise") (t :plugin/check-all-updates)]
:options {:on-click #(plugin-handler/user-check-enabled-for-updates! (not= :plugins category))}}])
[{:title [:span.flex.items-center.gap-1 (ui/icon "world") (t :settings-page/network-proxy)]
:options {:on-click #(state/pub-event! [:go/proxy-settings agent-opts])}}]
[{:title [:span.flex.items-center.gap-1 (ui/icon "arrow-down-circle") (t :plugin.install-from-file/menu-title)]
:options {:on-click plugin-config-handler/open-replace-plugins-modal}}]
[{:hr true}]
(when (state/developer-mode?)
[{:title [:span.flex.items-center.gap-1 (ui/icon "file-code") (t :plugin/open-preferences)]
:options {:on-click
#(p/let [root (plugin-handler/get-ls-dotdir-root)]
(js/apis.openPath (str root "/preferences.json")))}}
{:title [:span.flex.items-center.whitespace-nowrap.gap-1
(ui/icon "bug") (t :plugin/open-logseq-dir) [:code "~/.logseq"]]
:options {:on-click
#(p/let [root (plugin-handler/get-ls-dotdir-root)]
(js/apis.openPath root))}}])
[{:title [:span.flex.items-center.gap-1 (ui/icon "alert-triangle") (t :plugin/report-security)]
:options {:on-click #(plugin-handler/open-report-modal!)}}]
[{:hr true :key "dropdown-more"}
{:title (auto-check-for-updates-control)}])]
(ui/button
(ui/icon "dots-vertical")
:class "more-do"
:on-click #(shui/popup-show! (.-target %)
(fn [{:keys [id]}]
(render-classic-dropdown-items id items))
{:as-dropdown? true
:align "center"
:content-props {:side-offset 10}})
:variant :ghost))
;; developer
(panel-tab-developer)]]))
(def plugin-items-list-mixins
{:did-mount
(fn [s]
(when-let [^js el (rum/dom-node s)]
(when-let [^js el-list (.querySelector el ".cp__plugins-item-lists")]
(when-let [^js cls (.-classList (.querySelector el ".control-tabs"))]
(.addEventListener
el-list "scroll"
#(if (> (.-scrollTop el-list) 1)
(.add cls "scrolled")
(.remove cls "scrolled"))))))
s)})
(rum/defc lazy-items-loader
[load-more!]
(let [^js inViewState (ui/useInView #js {:threshold 0})
in-view? (.-inView inViewState)]
(rum/use-effect!
(fn []
(load-more!))
[in-view?])
[:div {:ref (.-ref inViewState)}
[:p.py-1.text-center.opacity-0 (when (.-inView inViewState) "·")]]))
(rum/defcs ^:large-vars/data-var marketplace-plugins
< rum/static rum/reactive
plugin-items-list-mixins
(rum/local false ::fetching)
(rum/local "" ::search-key)
(rum/local :plugins ::category)
(rum/local :downloads ::sort-by) ;; downloads / stars / letters / updates
(rum/local :default ::filter-by)
(rum/local nil ::error)
(rum/local nil ::cached-query-flag)
(rum/local 1 ::current-page)
{:did-mount
(fn [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 [*list-node-ref (rum/create-ref)
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?)
agent-opts (state/sub [:electron/user-cfgs :settings/agent])
*search-key (::search-key state)
*category (::category state)
*sort-by (::sort-by state)
*filter-by (::filter-by state)
*cached-query-flag (::cached-query-flag state)
*current-page (::current-page state)
*fetching (::fetching state)
*error (::error state)
theme-plugins (filter #(:theme %) pkgs)
normal-plugins (filter #(not (:theme %)) pkgs)
filtered-pkgs (when (seq pkgs)
(if (= @*category :themes) theme-plugins normal-plugins))
total-nums [(count normal-plugins) (count theme-plugins)]
filtered-pkgs (if (and (seq filtered-pkgs) (not= :default @*filter-by))
(filter #(apply
(if (= :installed @*filter-by) identity not)
[(contains? installed-plugins (keyword (:id %)))])
filtered-pkgs)
filtered-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-plugins (apply sort-by
(conj
(case @*sort-by
:letters [#(util/safe-lower-case (or (:title %) (:name %)))]
[@*sort-by #(compare %2 %1)])
filtered-pkgs))
fn-query-flag (fn [] (string/join "_" (map #(str @%) [*filter-by *sort-by *search-key *category])))
str-query-flag (fn-query-flag)
_ (when (not= str-query-flag @*cached-query-flag)
(when-let [^js list-cnt (rum/deref *list-node-ref)]
(set! (.-scrollTop list-cnt) 0))
(reset! *current-page 1))
_ (reset! *cached-query-flag str-query-flag)
page-total-items (count sorted-plugins)
sorted-plugins (if-not (> page-total-items PER-PAGE-SIZE)
sorted-plugins (take (* @*current-page PER-PAGE-SIZE) sorted-plugins))
load-more-pages! #(when (> page-total-items PER-PAGE-SIZE)
(when (< (* PER-PAGE-SIZE @*current-page) page-total-items)
(reset! *current-page (inc @*current-page))))]
[:div.cp__plugins-marketplace
(panel-control-tabs
@*search-key *search-key
@*category *category
@*sort-by *sort-by @*filter-by *filter-by
total-nums nil true develop-mode? (::reload state)
agent-opts)
(cond
(not online?)
[:p.flex.justify-center.pt-20.opacity-50 (svg/offline 30)]
@*fetching
[:p.flex.justify-center.py-20 svg/loading]
@*error
[:p.flex.justify-center.pt-20.opacity-50 (t :plugin/remote-error) (.-message @*error)]
:else
[:div.cp__plugins-marketplace-cnt
{:class (util/classnames [{:has-installing (boolean installing)}])}
[:div.cp__plugins-item-lists
{:ref *list-node-ref}
[:div.cp__plugins-item-lists-inner
;; items list
(for [item sorted-plugins]
(rum/with-key
(let [pid (keyword (:id item))
stat (:stat item)]
(plugin-item-card t item
(get-in item [:settings :disabled]) true *search-key installing
(and installing (= (keyword (:id installing)) pid))
(contains? installed-plugins pid) stat nil))
(:id item)))]
;; items loader
(when (seq sorted-plugins)
(lazy-items-loader load-more-pages!))]])]))
(rum/defcs installed-plugins
< rum/static rum/reactive
plugin-items-list-mixins
(rum/local "" ::search-key)
(rum/local :default ::filter-by) ;; default / enabled / disabled / unpacked / update-available
(rum/local :default ::sort-by)
(rum/local :plugins ::category)
(rum/local nil ::cached-query-flag)
(rum/local 1 ::current-page)
[state]
(let [*list-node-ref (rum/create-ref)
installed-plugins' (vals (state/sub [:plugin/installed-plugins]))
updating (state/sub :plugin/installing)
develop-mode? (state/sub :ui/developer-mode?)
selected-unpacked-pkg (state/sub :plugin/selected-unpacked-pkg)
coming-updates (state/sub :plugin/updates-coming)
agent-opts (state/sub [:electron/user-cfgs :settings/agent])
*filter-by (::filter-by state)
*sort-by (::sort-by state)
*search-key (::search-key state)
*category (::category state)
*cached-query-flag (::cached-query-flag state)
*current-page (::current-page state)
default-filter-by? (= :default @*filter-by)
theme-plugins (filter #(:theme %) installed-plugins')
normal-plugins (filter #(not (:theme %)) installed-plugins')
filtered-plugins (when (seq installed-plugins')
(if (= @*category :themes) theme-plugins normal-plugins))
total-nums [(count normal-plugins) (count theme-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))]
(filter #(= author (:author %)) filtered-plugins)
(search/fuzzy-search
filtered-plugins @*search-key
:limit 30
:extract-fn :name))
filtered-plugins)
sorted-plugins (if default-filter-by?
(->> filtered-plugins
(reduce #(let [disabled? (get-in %2 [:settings :disabled])
old-dirty (get @*dirties-toggle-items (keyword (:id %2)))
k (if (if (boolean? old-dirty) (not old-dirty) disabled?) 1 0)]
(update %1 k conj %2)) [[] []])
(#(update % 0 (fn [coll] (sort-by :iir coll))))
(flatten))
(do
(clear-dirties-states!)
filtered-plugins))
fn-query-flag (fn [] (string/join "_" (map #(str @%) [*filter-by *sort-by *search-key *category])))
str-query-flag (fn-query-flag)
_ (when (not= str-query-flag @*cached-query-flag)
(when-let [^js list-cnt (rum/deref *list-node-ref)]
(set! (.-scrollTop list-cnt) 0))
(reset! *current-page 1))
_ (reset! *cached-query-flag str-query-flag)
page-total-items (count sorted-plugins)
sorted-plugins (if-not (> page-total-items PER-PAGE-SIZE)
sorted-plugins (take (* @*current-page PER-PAGE-SIZE) sorted-plugins))
load-more-pages! #(when (> page-total-items PER-PAGE-SIZE)
(when (< (* PER-PAGE-SIZE @*current-page) page-total-items)
(reset! *current-page (inc @*current-page))))]
[:div.cp__plugins-installed
(panel-control-tabs
@*search-key *search-key
@*category *category
@*sort-by *sort-by
@*filter-by *filter-by
total-nums selected-unpacked-pkg
false develop-mode? nil
agent-opts)
[:div.cp__plugins-item-lists.pb-6
{:ref *list-node-ref}
[:div.cp__plugins-item-lists-inner
(for [item sorted-plugins]
(rum/with-key
(let [pid (keyword (:id item))]
(plugin-item-card t item
(get-in item [:settings :disabled]) false *search-key updating
(and updating (= (keyword (:id updating)) pid))
true nil (get coming-updates pid)))
(:id item)))]
(when (seq sorted-plugins)
(lazy-items-loader load-more-pages!))]]))
(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 (t :plugin/found-n-updates (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}
(shui/checkbox
{:id k
:default-checked c?
:on-checked-change (fn [checked?]
(when-not downloading?
(state/set-unchecked-update (:id it) (not checked?))))})
[: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 (str "\uD83C\uDF89 " (t :plugin/all-updated))]])
;; actions
(when (seq updates)
[:div.pt-5.flex.justify-end
(ui/button
(if downloading?
[:span (ui/loading (t :plugin/updates-downloading))]
[:span.flex.items-center (ui/icon "download") (t :plugin/update-all-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! (.toString e) :error)))
(plugin-handler/close-updates-downloading)))
:disabled
(or downloading?
(and (seq unchecked)
(= (count unchecked) (count updates)))))])]))
(rum/defc plugins-from-file
< rum/reactive
[plugins]
[:div.cp__plugins-fom-file
[:h1.mb-4.text-2xl.p-1 (t :plugin.install-from-file/title)]
(if (seq plugins)
[:div
[:div.mb-2.text-xl (t :plugin.install-from-file/notice)]
;; lists
[:ul
(for [it (:install plugins)
:let [k (str "lsp-it-" (name (:id it)))]]
[:li.flex.items-center
{:key k}
[:label.flex-1
{:for k}
[:strong.px-3 (str (name (:id it)) " " (:version it))]]])]
;; actions
[:div.pt-5
(ui/button [:span (t :plugin/install)]
:on-click #(do
(plugin-config-handler/replace-plugins plugins)
(shui/dialog-close! "ls-plugins-from-file-modal")))]]
;; all done
[:div.py-4 [:strong.text-xl (str "\uD83C\uDF89 " (t :plugin.install-from-file/success))]])])
(defn open-select-theme!
[]
(shui/dialog-open! installed-themes
{:align :top}))
(rum/defc hook-ui-slot
([type payload] (hook-ui-slot type payload nil #(plugin-handler/hook-plugin-app type % nil)))
([type payload opts callback]
(let [rs (util/rand-str 8)
id (str "slot__" rs)
*el-ref (rum/use-ref nil)]
(rum/use-effect!
(fn []
(let [timer (js/setTimeout #(callback {:type type :slot id :payload payload}) 50)]
#(js/clearTimeout timer)))
[id])
(rum/use-effect!
(fn []
(let [el (rum/deref *el-ref)]
#(when-let [uis (seq (.querySelectorAll el "[data-injected-ui]"))]
(doseq [^js el uis]
(when-let [id (.-injectedUi (.-dataset el))]
(js/LSPluginCore._forceCleanInjectedUI id))))))
[])
[:div.lsp-hook-ui-slot
(merge opts {:id id
:ref *el-ref
:on-pointer-down (fn [e] (util/stop-propagation e))})])))
(rum/defc hook-block-slot < rum/static
[type block]
(hook-ui-slot type {} nil #(plugin-handler/hook-plugin-block-slot block %)))
(rum/defc ui-item-renderer
[pid type {:keys [key template prefix]}]
(let [*el (rum/use-ref nil)
uni #(str prefix "injected-ui-item-" %)
^js pl (js/LSPluginCore.registeredPlugins.get (name pid))]
(rum/use-effect!
(fn []
(when-let [^js el (rum/deref *el)]
(js/LSPlugin.pluginHelpers.setupInjectedUI.call
pl #js {:slot (.-id el) :key key :template template} #js {})))
[template])
(if-not (nil? pl)
[:div
{:id (uni (str (name key) "-" (name pid)))
:title key
:class (uni (name type))
:ref *el}]
[:<>])))
(rum/defc toolbar-plugins-manager-list
[updates-coming items]
(let [badge-updates? (and (not (plugin-handler/get-auto-checking?))
(seq (state/all-available-coming-updates updates-coming)))
items (fn []
(->> (concat
(for [[_ {:keys [key pinned?] :as opts} pid] items
:let [pkey (str (name pid) ":" key)]]
{:title key
:item [:div.flex.items-center.item-wrap
(ui-item-renderer pid :toolbar (assoc opts :prefix "pl-" :key (str "pl-" key)))
[:span {:style {:padding-left "2px"}} key]
[:span.pin.flex.items-center.opacity-60
{:class (util/classnames [{:pinned pinned?}])}
(ui/icon (if pinned? "pinned" "pin"))]]
:options {:on-click (fn [^js e]
(let [^js target (.-target e)
user-btn? (boolean (.closest target "div[data-injected-ui]"))]
(when-not user-btn?
(plugin-handler/op-pinned-toolbar-item! pkey (if pinned? :remove :add)))
true))}})
[{:hr true}
{:title (t :plugins)
:options {:on-click #(plugin-handler/goto-plugins-dashboard!)
:class "extra-item mt-2"}
:icon (ui/icon "apps")}
{:title (t :themes)
:options {:on-click #(plugin-handler/show-themes-modal!)
:class "extra-item"}
:icon (ui/icon "palette")}
{:title (t :settings)
:options {:on-click #(plugin-handler/goto-plugins-settings!)
:class "extra-item"}
:icon (ui/icon "adjustments")}
(when badge-updates?
{:title [:div.flex.items-center.space-x-5.leading-none
[:span (t :plugin/found-updates)] (ui/point "bg-red-700" 5 {:style {:margin-top 2}})]
:options {:on-click #(open-waiting-updates-modal!)
:class "extra-item"}
:icon (ui/icon "download")})]
[{:hr true :key "dropdown-more"}
{:title (auto-check-for-updates-control)}])
(remove nil?)))]
[:div.toolbar-plugins-manager
{:on-pointer-down
(fn [^js e]
(shui/popup-show! (.-target e)
(fn [{:keys [id]}]
(render-classic-dropdown-items id (items)))
{:as-dropdown? true
:content-props {:class "toolbar-plugins-manager-content"}}))}
(shui/button-ghost-icon :puzzle
{:class "flex relative toolbar-plugins-manager-trigger"}
(when badge-updates?
(ui/point "bg-red-600.top-1.right-1.absolute" 4 {:style {:margin-right 2 :margin-top 2}})))]))
(rum/defc header-ui-items-list-wrap
[children]
(let [*wrap-el (rum/use-ref nil)
[right-sidebar-resized] (rum-utils/use-atom ui-handler/*right-sidebar-resized-at)]
(rum/use-effect!
(fn []
(when-let [^js wrap-el (rum/deref *wrap-el)]
(when-let [^js header-el (.closest wrap-el ".cp__header")]
(let [^js header-l (.querySelector header-el "* > .l")
^js header-r (.querySelector header-el "* > .r")
set-max-width! #(when (number? %) (set! (.-maxWidth (.-style wrap-el)) (str % "px")))
calc-wrap-max-width #(let [width-l (.-offsetWidth header-l)
width-t (-> (js/document.querySelector "#main-content-container") (.-offsetWidth))
children (to-array (.-children header-r))
width-c' (reduce (fn [acc ^js e]
(when (some-> e (.-classList) (.contains "ui-items-container") (not))
(+ acc (or (.-offsetWidth e) 0)))) 0 children)]
(when-let [width-t (and (number? width-t)
(if-not (state/get-left-sidebar-open?)
(- width-t width-l) width-t))]
(set-max-width! (max (- width-t width-c' 100) 76))))]
(.addEventListener js/window "resize" calc-wrap-max-width)
(js/setTimeout calc-wrap-max-width 16)
#(.removeEventListener js/window "resize" calc-wrap-max-width)))))
[right-sidebar-resized])
[:div.list-wrap
{:ref *wrap-el}
children]))
(rum/defcs hook-ui-items < rum/reactive
< {:key-fn #(identity "plugin-hook-items")}
"type of :toolbar, :pagebar"
[_state type]
(when (state/sub [:plugin/installed-ui-items])
(let [toolbar? (= :toolbar type)
pinned-items (state/sub [:plugin/preferences :pinnedToolbarItems])
pinned-items (and (sequential? pinned-items) (into #{} pinned-items))
items (state/get-plugins-ui-items-with-type type)
items (sort-by #(:key (second %)) items)]
(when-let [items (and (seq items)
(if toolbar?
(map #(assoc-in % [1 :pinned?]
(let [[_ {:keys [key]} pid] %
pkey (str (name pid) ":" key)]
(contains? pinned-items pkey)))
items)
items))]
[:div.ui-items-container
{:data-type (name type)}
[:<>
(header-ui-items-list-wrap
(for [[_ {:keys [key pinned?] :as opts} pid] items]
(when (or (not toolbar?)
(not (set? pinned-items)) pinned?)
(rum/with-key (ui-item-renderer pid type opts) key))))
;; manage plugin buttons
(when toolbar?
(let [updates-coming (state/sub :plugin/updates-coming)]
(toolbar-plugins-manager-list updates-coming items)))]]))))
(rum/defc hook-ui-fenced-code
[block content {:keys [render edit] :as _opts}]
(let [[content1 set-content1!] (rum/use-state content)
[editor-active? set-editor-active!] (rum/use-state (string/blank? content))
*cm (rum/use-ref nil)
*el (rum/use-ref nil)]
(rum/use-effect!
#(set-content1! content)
[content])
(rum/use-effect!
(fn []
(some-> (rum/deref *el)
(.closest ".ui-fenced-code-wrap")
(.-classList)
(#(if editor-active?
(.add % "is-active")
(.remove % "is-active"))))
(when-let [cm (rum/deref *cm)]
(.refresh cm)
(.focus cm)
(.setCursor cm (.lineCount cm) (count (.getLine cm (.lastLine cm))))))
[editor-active?])
(rum/use-effect!
(fn []
(let [t (js/setTimeout
#(when-let [^js cm (some-> (rum/deref *el)
(.closest ".ui-fenced-code-wrap")
(.querySelector ".CodeMirror")
(.-CodeMirror))]
(rum/set-ref! *cm cm)
(doto cm
(.on "change" (fn []
(some-> cm (.getDoc) (.getValue) (set-content1!))))))
;; wait for the cm loaded
1000)]
#(js/clearTimeout t)))
[])
[:div.ui-fenced-code-result
{:on-pointer-down (fn [e] (when (false? edit) (util/stop e)))
:class (util/classnames [{:not-edit (false? edit)}])
:ref *el}
[:<>
[:span.actions
{:on-pointer-down #(util/stop %)}
(ui/button (ui/icon "square-toggle-horizontal" {:size 14})
:on-click #(set-editor-active! (not editor-active?)))
(ui/button (ui/icon "source-code" {:size 14})
:on-click #(editor-handler/edit-block! block (count content1)))]
(when (fn? render)
(js/React.createElement render #js {:content content1}))]]))
(rum/defc plugins-page
[]
(let [[active set-active!] (rum/use-state :installed)
market? (= active :marketplace)
*el-ref (rum/create-ref)]
(rum/use-effect!
(fn []
(state/load-app-user-cfgs)
#(clear-dirties-states!))
[])
(rum/use-effect!
#(clear-dirties-states!)
[market?])
[:div.cp__plugins-page
{:ref *el-ref
:tab-index "-1"}
[:h1 (t :plugins)]
(security-warning)
[:hr.my-4]
[:div.tabs.flex.items-center.justify-center
[:div.tabs-inner.flex.items-center
(ui/button [:span.it (t :plugin/installed)]
:on-click #(set-active! :installed)
:intent (if-not market? "" "link"))
(ui/button [:span.mk (svg/apps 16) (t :plugin/marketplace)]
:on-click #(set-active! :marketplace)
:intent (if market? "" "link"))]]
[:div.panels
(if market?
(marketplace-plugins)
(installed-plugins))]]))
(def *updates-sub-content-timer (atom nil))
(def *updates-sub-content (atom nil))
(defn set-updates-sub-content!
[content duration]
(reset! *updates-sub-content content)
(when (> duration 0)
(some-> @*updates-sub-content-timer (js/clearTimeout))
(->> (js/setTimeout #(reset! *updates-sub-content nil) duration)
(reset! *updates-sub-content-timer))))
(rum/defc updates-notifications-impl
[check-pending? auto-checking? online?]
(let [[uid, set-uid] (rum/use-state nil)
[sub-content, _set-sub-content!] (rum-utils/use-atom *updates-sub-content)
notify! (fn [content status]
(if auto-checking?
(println (t :plugin/list-of-updates) content)
(let [cb #(plugin-handler/cancel-user-checking!)]
(try
(set-uid (notification/show! content status false uid nil cb))
(catch js/Error _
(set-uid (notification/show! content status false nil nil cb)))))))]
(rum/use-effect!
(fn []
(if check-pending?
(notify!
[:div
[:div (t :plugin/checking-for-updates)]
(when sub-content [:p.opacity-60 sub-content])]
(ui/loading ""))
(when uid (notification/clear! uid))))
[check-pending? sub-content])
(rum/use-effect!
;; scheduler for auto updates
(fn []
(when online?
(let [last-updates (storage/get :lsp-last-auto-updates)]
(when (and (not (false? last-updates))
(or (true? last-updates)
(not (number? last-updates))
;; interval 12 hours
(> (- (js/Date.now) last-updates) (* 60 60 12 1000))))
(js/setTimeout
(fn []
(plugin-handler/auto-check-enabled-for-updates!)
(storage/set :lsp-last-auto-updates (js/Date.now))))))))
[online?])
[:<>]))
(rum/defcs updates-notifications < rum/reactive
[_]
(let [updates-pending (state/sub :plugin/updates-pending)
online? (state/sub :network/online?)
auto-checking? (state/sub :plugin/updates-auto-checking?)
check-pending? (boolean (seq updates-pending))]
(updates-notifications-impl check-pending? auto-checking? online?)))
(rum/defcs focused-settings-content
< rum/reactive
(rum/local (state/sub :plugin/focused-settings) ::cache)
[_state title]
(let [*cache (::cache _state)
focused (state/sub :plugin/focused-settings)
nav? (state/sub :plugin/navs-settings?)
_ (state/sub :plugin/installed-plugins)
_ (js/setTimeout #(reset! *cache focused) 100)]
[:div.cp__plugins-settings.cp__settings-main
[:div.cp__settings-inner.md:flex
{:class (util/classnames [{:no-aside (not nav?)}])}
(when nav?
[:aside.md:w-64 {:style {:min-width "10rem"}}
[:header.cp__settings-header
[:h1.cp__settings-modal-title (or title (t :settings-of-plugins))]]
(let [plugins (plugin-handler/get-enabled-plugins-if-setting-schema)]
[:ul.settings-plugin-list
(for [{:keys [id name title icon]} plugins]
[:li
{:key id :class (util/classnames [{:active (= id focused)}])}
[:a.flex.items-center.settings-plugin-item
{:data-id id
:on-click #(do (state/set-state! :plugin/focused-settings id))}
(if (and icon (not (string/blank? icon)))
[:img.icon {:src icon}]
svg/folder)
[:strong.flex-1 (or title name)]]])])])
[:article
[:div.panel-wrap
{:data-id focused}
(when-let [^js pl (and focused (= @*cache focused)
(plugin-handler/get-plugin-inst focused))]
(ui/catch-error
[:p.warning.text-lg.mt-5 "Settings schema Error!"]
(plugins-settings/settings-container
(bean/->clj (.-settingsSchema pl)) pl)))]]]]))
(rum/defc custom-js-installer
[{:keys [t current-repo db-restoring? nfs-granted?]}]
(rum/use-effect!
(fn []
(when (and (not db-restoring?)
(or (not util/nfs?) nfs-granted?))
(ui-handler/exec-js-if-exists-&-allowed! t)))
[current-repo db-restoring? nfs-granted?])
nil)
(rum/defc perf-tip-content
[pid name url]
[:div
[:span.block.whitespace-normal
"This plugin "
[:strong.text-error "#" name]
" takes too long to load, affecting the application startup time and
potentially causing other plugins to fail to load."]
[:path.opacity-50
[:small [:span.pr-1 (ui/icon "folder")] url]]
[:p
(ui/button "Disable now"
:small? true
:on-click
(fn []
(-> (js/LSPluginCore.disable pid)
(p/then #(do
(notification/clear! pid)
(notification/show!
[:span "The plugin "
[:strong.text-error "#" name]
" is disabled."] :success
true nil 3000 nil)))
(p/catch #(js/console.error %)))))]])
(defn open-plugins-modal!
[]
(shui/dialog-open!
(plugins-page)
{:label :plugins-dashboard
:align :start}))
(defn open-waiting-updates-modal!
[]
(shui/dialog-open!
(fn []
(waiting-coming-updates))
{:center? true}))
(defn open-plugins-from-file-modal!
[plugins]
(shui/dialog-open!
(fn []
(plugins-from-file plugins))
{:id "ls-plugins-from-file-modal"}))
(defn open-focused-settings-modal!
[title]
(shui/dialog-open!
(fn []
[:div.settings-modal.of-plugins
(focused-settings-content title)])
{:label "plugin-settings-modal"
:align :start
:id "ls-focused-settings-modal"}))
(defn hook-custom-routes
[routes]
(cond-> routes
config/lsp-enabled?
(concat (some->> (plugin-handler/get-route-renderers)
(mapv #(when-let [{:keys [name path render]} %]
(when (not (string/blank? path))
[path {:name name :view (fn [r] (render r %))}])))
(remove nil?)))))
(defn hook-daemon-renderers
[]
(when-let [rs (seq (plugin-handler/get-daemon-renderers))]
[:div.lsp-daemon-container.fixed.z-10
(for [{:keys [key _pid render]} rs]
(when (fn? render)
[:div.lsp-daemon-container-card {:data-key key} (render)]))]))