mirror of
https://github.com/logseq/logseq.git
synced 2026-04-29 08:26:40 +00:00
1458 lines
60 KiB
Clojure
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)]))]))
|