plugins.cljs 44 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093
  1. (ns frontend.components.plugins
  2. (:require [rum.core :as rum]
  3. [frontend.state :as state]
  4. [cljs-bean.core :as bean]
  5. [frontend.context.i18n :refer [t]]
  6. [frontend.ui :as ui]
  7. [frontend.handler.ui :as ui-handler]
  8. [frontend.handler.plugin-config :as plugin-config-handler]
  9. [frontend.handler.common.plugin :as plugin-common-handler]
  10. [frontend.search :as search]
  11. [frontend.util :as util]
  12. [frontend.mixins :as mixins]
  13. [electron.ipc :as ipc]
  14. [promesa.core :as p]
  15. [frontend.components.svg :as svg]
  16. [frontend.components.plugins-settings :as plugins-settings]
  17. [frontend.handler.notification :as notification]
  18. [frontend.handler.plugin :as plugin-handler]
  19. [clojure.string :as string]))
  20. (rum/defcs installed-themes
  21. <
  22. (rum/local [] ::themes)
  23. (rum/local 0 ::cursor)
  24. (rum/local 0 ::total)
  25. {:did-mount (fn [state]
  26. (let [*themes (::themes state)
  27. *cursor (::cursor state)
  28. *total (::total state)
  29. mode (state/sub :ui/theme)
  30. all-themes (state/sub :plugin/installed-themes)
  31. themes (->> all-themes
  32. (filter #(= (:mode %) mode))
  33. (sort-by #(:name %)))
  34. no-mode-themes (->> all-themes
  35. (filter #(= (:mode %) nil))
  36. (sort-by #(:name %))
  37. (map-indexed (fn [idx opt] (assoc opt :group-first (zero? idx) :group-desc (if (zero? idx) "light & dark themes" nil)))))
  38. selected (state/sub :plugin/selected-theme)
  39. themes (map-indexed (fn [idx opt]
  40. (let [selected? (= (:url opt) selected)]
  41. (when selected? (reset! *cursor (+ idx 1)))
  42. (assoc opt :mode mode :selected selected?))) (concat themes no-mode-themes))
  43. themes (cons {:name (string/join " " ["Default" (string/capitalize mode) "Theme"])
  44. :url nil
  45. :description (string/join " " ["Logseq default" mode "theme."])
  46. :mode mode
  47. :selected (nil? selected)
  48. :group-first true
  49. :group-desc (str mode " themes")} themes)]
  50. (reset! *themes themes)
  51. (reset! *total (count themes))
  52. state))}
  53. (mixins/event-mixin
  54. (fn [state]
  55. (let [*cursor (::cursor state)
  56. *total (::total state)
  57. ^js target (rum/dom-node state)]
  58. (.focus target)
  59. (mixins/on-key-down
  60. state {38 ;; up
  61. (fn [^js _e]
  62. (reset! *cursor
  63. (if (zero? @*cursor)
  64. (dec @*total) (dec @*cursor))))
  65. 40 ;; down
  66. (fn [^js _e]
  67. (reset! *cursor
  68. (if (= @*cursor (dec @*total))
  69. 0 (inc @*cursor))))
  70. 13 ;; enter
  71. #(when-let [^js active (.querySelector target ".is-active")]
  72. (.click active))}))))
  73. [state]
  74. (let [*cursor (::cursor state)
  75. *themes (::themes state)]
  76. [:div.cp__themes-installed
  77. {:tab-index -1}
  78. [:h1.mb-4.text-2xl.p-1 (t :themes)]
  79. (map-indexed
  80. (fn [idx opt]
  81. (let [current-selected? (:selected opt)
  82. group-first? (:group-first opt)
  83. plg (get (:plugin/installed-plugins @state/state) (keyword (:pid opt)))]
  84. [:div
  85. {:key (str idx (:name opt))}
  86. (when (and group-first? (not= idx 0)) [:hr.my-2])
  87. [:div.it.flex.px-3.py-1.5.rounded-sm.justify-between
  88. {:title (:description opt)
  89. :class (util/classnames
  90. [{:is-selected current-selected?
  91. :is-active (= idx @*cursor)}])
  92. :on-click #(do (js/LSPluginCore.selectTheme (bean/->js opt))
  93. (state/close-modal!))}
  94. [:div.flex.items-center.text-xs
  95. [:div.opacity-60 (str (or (:name plg) "Logseq") " •")]
  96. [:div.name.ml-1 (:name opt)]]
  97. (when (or group-first? current-selected?)
  98. [:div.flex.items-center
  99. (when group-first? [:small.opacity-60 (:group-desc opt)])
  100. (when current-selected? [:small.inline-flex.ml-1.opacity-60 (ui/icon "check")])])]]))
  101. @*themes)]))
  102. (rum/defc unpacked-plugin-loader
  103. [unpacked-pkg-path]
  104. (rum/use-effect!
  105. (fn []
  106. (let [err-handle
  107. (fn [^js e]
  108. (case (keyword (aget e "name"))
  109. :IllegalPluginPackageError
  110. (notification/show! "Illegal Logseq plugin package." :error)
  111. :ExistedImportedPluginPackageError
  112. (notification/show! "Existed Imported plugin package." :error)
  113. :default)
  114. (plugin-handler/reset-unpacked-state))
  115. reg-handle #(plugin-handler/reset-unpacked-state)]
  116. (when unpacked-pkg-path
  117. (doto js/LSPluginCore
  118. (.once "error" err-handle)
  119. (.once "registered" reg-handle)
  120. (.register (bean/->js {:url unpacked-pkg-path}))))
  121. #(doto js/LSPluginCore
  122. (.off "error" err-handle)
  123. (.off "registered" reg-handle))))
  124. [unpacked-pkg-path])
  125. (when unpacked-pkg-path
  126. [:strong.inline-flex.px-3 "Loading ..."]))
  127. (rum/defc category-tabs
  128. [t category on-action]
  129. [:div.secondary-tabs.categories.flex
  130. (ui/button
  131. [:span.flex.items-center (ui/icon "puzzle") (t :plugins)]
  132. :intent "logseq"
  133. :on-click #(on-action :plugins)
  134. :class (if (= category :plugins) "active" ""))
  135. (ui/button
  136. [:span.flex.items-center (ui/icon "palette") (t :themes)]
  137. :intent "logseq"
  138. :on-click #(on-action :themes)
  139. :class (if (= category :themes) "active" ""))])
  140. (rum/defc local-markdown-display
  141. < rum/reactive
  142. []
  143. (let [[content item] (state/sub :plugin/active-readme)]
  144. [:div.cp__plugins-details
  145. {:on-click (fn [^js/MouseEvent e]
  146. (when-let [target (.-target e)]
  147. (when (and (= (string/lower-case (.-nodeName target)) "a")
  148. (not (string/blank? (. target getAttribute "href"))))
  149. (js/apis.openExternal (. target getAttribute "href"))
  150. (.preventDefault e))))}
  151. (when-let [repo (:repository item)]
  152. (when-let [repo (if (string? repo) repo (:url repo))]
  153. [:div.p-4.rounded-md.bg-base-3
  154. [:strong [:a.flex.items-center {:target "_blank" :href repo}
  155. [:span.mr-1 (svg/github {:width 25 :height 25})] repo]]]))
  156. [:div.p-1.bg-transparent.border-none.ls-block
  157. {:style {:min-height "60vw"
  158. :max-width 900}
  159. :dangerouslySetInnerHTML {:__html content}}]]))
  160. (rum/defc remote-readme-display
  161. [repo _content]
  162. (let [src (str "lsp://logseq.com/marketplace.html?repo=" repo)]
  163. [:iframe.lsp-frame-readme {:src src}]))
  164. (defn security-warning
  165. []
  166. (ui/admonition
  167. :warning
  168. [:div.max-w-4xl
  169. "Plugins can access your graph and your local files, issue network requests.
  170. They can also cause data corruption or loss. We're working on proper access rules for your graphs.
  171. Meanwhile, make sure you have regular backups of your graphs and only install the plugins when you can read and
  172. understand the source code."]))
  173. (rum/defc card-ctls-of-market < rum/static
  174. [item stat installed? installing-or-updating?]
  175. [:div.ctl
  176. [:ul.l.flex.items-center
  177. ;; stars
  178. [:li.flex.text-sm.items-center.pr-3
  179. (svg/star 16) [:span.pl-1 (:stargazers_count stat)]]
  180. ;; downloads
  181. (when-let [downloads (and stat (:total_downloads stat))]
  182. (when (and downloads (> downloads 0))
  183. [:li.flex.text-sm.items-center.pr-3
  184. (svg/cloud-down 16) [:span.pl-1 downloads]]))]
  185. [:div.r.flex.items-center
  186. [:a.btn
  187. {:class (util/classnames [{:disabled (or installed? installing-or-updating?)
  188. :installing installing-or-updating?}])
  189. :on-click #(plugin-common-handler/install-marketplace-plugin item)}
  190. (if installed?
  191. (t :plugin/installed)
  192. (if installing-or-updating?
  193. [:span.flex.items-center [:small svg/loading]
  194. (t :plugin/installing)]
  195. (t :plugin/install)))]]])
  196. (rum/defc card-ctls-of-installed < rum/static
  197. [id name url sponsors unpacked? disabled?
  198. installing-or-updating? has-other-pending?
  199. new-version item]
  200. [:div.ctl
  201. [:div.l
  202. [:div.de
  203. [:strong (ui/icon "settings")]
  204. [:ul.menu-list
  205. [:li {:on-click #(plugin-handler/open-plugin-settings! id false)} (t :plugin/open-settings)]
  206. [:li {:on-click #(js/apis.openPath url)} (t :plugin/open-package)]
  207. [:li {:on-click
  208. #(let [confirm-fn
  209. (ui/make-confirm-modal
  210. {:title (t :plugin/delete-alert name)
  211. :on-confirm (fn [_ {:keys [close-fn]}]
  212. (close-fn)
  213. (plugin-common-handler/unregister-plugin id)
  214. (plugin-config-handler/remove-plugin id))})]
  215. (state/set-sub-modal! confirm-fn {:center? true}))}
  216. (t :plugin/uninstall)]]]
  217. (when (seq sponsors)
  218. [:div.de.sponsors
  219. [:strong (ui/icon "coffee")]
  220. [:ul.menu-list
  221. (for [link sponsors]
  222. [:li {:key link}
  223. [:a {:href link :target "_blank"}
  224. [:span.flex.items-center link (ui/icon "external-link")]]])
  225. ]])]
  226. [:div.r.flex.items-center
  227. (when (and unpacked? (not disabled?))
  228. [:a.btn
  229. {:on-click #(js-invoke js/LSPluginCore "reload" id)}
  230. (t :plugin/reload)])
  231. (when (not unpacked?)
  232. [:div.updates-actions
  233. [:a.btn
  234. {:class (util/classnames [{:disabled installing-or-updating?}])
  235. :on-click #(when-not has-other-pending?
  236. (plugin-handler/check-or-update-marketplace-plugin
  237. (assoc item :only-check (not new-version))
  238. (fn [^js e] (notification/show! (.toString e) :error))))}
  239. (if installing-or-updating?
  240. (t :plugin/updating)
  241. (if new-version
  242. (str (t :plugin/update) " 👉 " new-version)
  243. (t :plugin/check-update)))]])
  244. (ui/toggle (not disabled?)
  245. (fn []
  246. (js-invoke js/LSPluginCore (if disabled? "enable" "disable") id))
  247. true)]])
  248. (defn get-open-plugin-readme-handler
  249. [url item repo]
  250. #(plugin-handler/open-readme!
  251. url item (if repo remote-readme-display local-markdown-display))
  252. )
  253. (rum/defc plugin-item-card < rum/static
  254. [t {:keys [id name title version url description author icon iir repo sponsors] :as item}
  255. disabled? market? *search-key has-other-pending?
  256. installing-or-updating? installed? stat coming-update]
  257. (let [name (or title name "Untitled")
  258. unpacked? (not iir)
  259. new-version (state/coming-update-new-version? coming-update)]
  260. [:div.cp__plugins-item-card
  261. {:key (str "lsp-card-" id)
  262. :class (util/classnames
  263. [{:market market?
  264. :installed installed?
  265. :updating installing-or-updating?
  266. :has-new-version new-version}])}
  267. [:div.l.link-block.cursor-pointer
  268. {:on-click (get-open-plugin-readme-handler url item repo)}
  269. (if (and icon (not (string/blank? icon)))
  270. [:img.icon {:src (if market? (plugin-handler/pkg-asset id icon) icon)}]
  271. svg/folder)
  272. (when (and (not market?) unpacked?)
  273. [:span.flex.justify-center.text-xs.text-error.pt-2 (t :plugin/unpacked)])]
  274. [:div.r
  275. [:h3.head.text-xl.font-bold.pt-1.5
  276. [:span.l.link-block.cursor-pointer
  277. {:on-click (get-open-plugin-readme-handler url item repo)}
  278. name]
  279. (when (not market?) [:sup.inline-block.px-1.text-xs.opacity-50 version])]
  280. [:div.desc.text-xs.opacity-70
  281. [:p description]
  282. ;;[:small (js/JSON.stringify (bean/->js settings))]
  283. ]
  284. ;; Author & Identity
  285. [:div.flag
  286. [:p.text-xs.pr-2.flex.justify-between
  287. [:small {:on-click #(when-let [^js el (js/document.querySelector ".cp__plugins-page .search-ctls input")]
  288. (reset! *search-key (str "@" author))
  289. (.select el))} author]
  290. [:small {:on-click #(do
  291. (notification/show! "Copied!" :success)
  292. (util/copy-to-clipboard! id))}
  293. (str "ID: " id)]]]
  294. ;; Github repo
  295. [:div.flag.is-top.opacity-50
  296. (when repo
  297. [:a.flex {:target "_blank"
  298. :href (plugin-handler/gh-repo-url repo)}
  299. (svg/github {:width 16 :height 16})])]
  300. (if market?
  301. ;; market ctls
  302. (card-ctls-of-market item stat installed? installing-or-updating?)
  303. ;; installed ctls
  304. (card-ctls-of-installed
  305. id name url sponsors unpacked? disabled?
  306. installing-or-updating? has-other-pending? new-version item))]]))
  307. (rum/defc panel-tab-search < rum/static
  308. [search-key *search-key *search-ref]
  309. [:div.search-ctls
  310. [:small.absolute.s1
  311. (ui/icon "search")]
  312. (when-not (string/blank? search-key)
  313. [:small.absolute.s2
  314. {:on-click #(when-let [^js target (rum/deref *search-ref)]
  315. (reset! *search-key nil)
  316. (.focus target))}
  317. (ui/icon "x")])
  318. [:input.form-input.is-small
  319. {:placeholder "Search plugins"
  320. :ref *search-ref
  321. :auto-focus true
  322. :on-key-down (fn [^js e]
  323. (when (= 27 (.-keyCode e))
  324. (util/stop e)
  325. (if (string/blank? search-key)
  326. (some-> (js/document.querySelector ".cp__plugins-page") (.focus))
  327. (reset! *search-key nil))))
  328. :on-change #(let [^js target (.-target %)]
  329. (reset! *search-key (util/trim-safe (.-value target))))
  330. :value (or search-key "")}]])
  331. (rum/defc panel-tab-developer
  332. []
  333. (ui/button
  334. (t :plugin/contribute)
  335. :href "https://github.com/logseq/marketplace"
  336. :class "contribute"
  337. :intent "logseq"
  338. :target "_blank"))
  339. (rum/defc user-proxy-settings-panel
  340. [{:keys [protocol] :as agent-opts}]
  341. (let [[opts set-opts!] (rum/use-state agent-opts)
  342. [testing? set-testing?!] (rum/use-state false)
  343. *test-input (rum/create-ref)
  344. disabled? (string/blank? (:protocol opts))]
  345. [:div.cp__settings-network-proxy-panel
  346. [:h1.mb-2.text-2xl.font-bold (t :settings-page/network-proxy)]
  347. [:div.p-2
  348. [:p [:label [:strong (t :type)]
  349. (ui/select [{:label "Disabled" :value "" :selected disabled?}
  350. {:label "http" :value "http" :selected (= protocol "http")}
  351. {:label "socks5" :value "socks5" :selected (= protocol "socks5")}]
  352. #(set-opts!
  353. (assoc opts :protocol (if (= "disabled" (util/safe-lower-case %)) nil %))) nil)]]
  354. [:p.flex
  355. [:label.pr-4 [:strong (t :host)]
  356. [:input.form-input.is-small
  357. {:value (:host opts) :disabled disabled?
  358. :on-change #(set-opts!
  359. (assoc opts :host (util/trim-safe (util/evalue %))))}]]
  360. [:label [:strong (t :port)]
  361. [:input.form-input.is-small
  362. {:value (:port opts) :type "number" :disabled disabled?
  363. :on-change #(set-opts!
  364. (assoc opts :port (util/trim-safe (util/evalue %))))}]]]
  365. [:hr]
  366. [:p.flex.items-center.space-x-2
  367. [:span.w-60
  368. [:input.form-input.is-small
  369. {:ref *test-input
  370. :placeholder "http://"
  371. :on-change #(set-opts!
  372. (assoc opts :test (util/trim-safe (util/evalue %))))
  373. :value (:test opts)}]]
  374. (ui/button (if testing? (ui/loading "Testing") "Test URL")
  375. :intent "logseq" :large? false
  376. :style {:margin-top 0 :padding "5px 15px"}
  377. :on-click #(let [val (util/trim-safe (.-value (rum/deref *test-input)))]
  378. (when (and (not testing?) (not (string/blank? val)))
  379. (set-testing?! true)
  380. (-> (p/let [_ (ipc/ipc :setHttpsAgent opts)
  381. _ (ipc/ipc :testProxyUrl val)])
  382. (p/catch (fn [e] (notification/show! (str e) :error)))
  383. (p/finally (fn [] (set-testing?! false)))))))]
  384. [:p.pt-2
  385. (ui/button (t :save)
  386. :on-click (fn []
  387. (p/let [_ (ipc/ipc :setHttpsAgent opts)]
  388. (state/set-state! [:electron/user-cfgs :settings/agent] opts)
  389. (state/close-sub-modal! :https-proxy-panel))))]]]))
  390. (rum/defc ^:large-vars/cleanup-todo panel-control-tabs < rum/static
  391. [search-key *search-key category *category
  392. sort-by *sort-by filter-by *filter-by
  393. selected-unpacked-pkg market? develop-mode?
  394. reload-market-fn agent-opts]
  395. (let [*search-ref (rum/create-ref)]
  396. [:div.mb-2.flex.justify-between.control-tabs.relative
  397. [:div.flex.items-center.l
  398. (category-tabs t category #(reset! *category %))
  399. (when (and develop-mode? (not market?))
  400. [:div
  401. (ui/tippy {:html [:div (t :plugin/unpacked-tips)]
  402. :arrow true}
  403. (ui/button
  404. [:span.flex.items-center
  405. (ui/icon "upload") (t :plugin/load-unpacked)]
  406. :intent "logseq"
  407. :class "load-unpacked"
  408. :on-click plugin-handler/load-unpacked-plugin))
  409. (unpacked-plugin-loader selected-unpacked-pkg)])]
  410. [:div.flex.items-center.r
  411. ;; extra info
  412. (when-let [proxy-val (state/http-proxy-enabled-or-val?)]
  413. (ui/button
  414. [:span.flex.items-center.text-indigo-500
  415. (ui/icon "world-download") proxy-val]
  416. :small? true
  417. :intent "link"
  418. :on-click #(state/pub-event! [:go/proxy-settings agent-opts])))
  419. ;; search
  420. (panel-tab-search search-key *search-key *search-ref)
  421. ;; sorter & filter
  422. (let [aim-icon #(if (= filter-by %) "check" "circle")]
  423. (ui/dropdown-with-links
  424. (fn [{:keys [toggle-fn]}]
  425. (ui/button
  426. [:span (ui/icon "filter")]
  427. :class (str (when-not (contains? #{:default} filter-by) "picked ") "sort-or-filter-by")
  428. :on-click toggle-fn
  429. :intent "link"))
  430. (if market?
  431. [{:title (t :plugin/all)
  432. :options {:on-click #(reset! *filter-by :default)}
  433. :icon (ui/icon (aim-icon :default))}
  434. {:title (t :plugin/installed)
  435. :options {:on-click #(reset! *filter-by :installed)}
  436. :icon (ui/icon (aim-icon :installed))}
  437. {:title (t :plugin/not-installed)
  438. :options {:on-click #(reset! *filter-by :not-installed)}
  439. :icon (ui/icon (aim-icon :not-installed))}]
  440. [{:title (t :plugin/all)
  441. :options {:on-click #(reset! *filter-by :default)}
  442. :icon (ui/icon (aim-icon :default))}
  443. {:title (t :plugin/enabled)
  444. :options {:on-click #(reset! *filter-by :enabled)}
  445. :icon (ui/icon (aim-icon :enabled))}
  446. {:title (t :plugin/disabled)
  447. :options {:on-click #(reset! *filter-by :disabled)}
  448. :icon (ui/icon (aim-icon :disabled))}
  449. {:title (t :plugin/unpacked)
  450. :options {:on-click #(reset! *filter-by :unpacked)}
  451. :icon (ui/icon (aim-icon :unpacked))}
  452. {:title (t :plugin/update-available)
  453. :options {:on-click #(reset! *filter-by :update-available)}
  454. :icon (ui/icon (aim-icon :update-available))}])
  455. nil))
  456. (when market?
  457. (ui/dropdown-with-links
  458. (fn [{:keys [toggle-fn]}]
  459. (ui/button
  460. [:span (ui/icon "arrows-sort")]
  461. :class (str (when-not (contains? #{:default :downloads} sort-by) "picked ") "sort-or-filter-by")
  462. :on-click toggle-fn
  463. :intent "link"))
  464. (let [aim-icon #(if (= sort-by %) "check" "circle")]
  465. [{:title (t :plugin/downloads)
  466. :options {:on-click #(reset! *sort-by :downloads)}
  467. :icon (ui/icon (aim-icon :downloads))}
  468. {:title (t :plugin/stars)
  469. :options {:on-click #(reset! *sort-by :stars)}
  470. :icon (ui/icon (aim-icon :stars))}
  471. {:title (str (t :plugin/title) " (A - Z)")
  472. :options {:on-click #(reset! *sort-by :letters)}
  473. :icon (ui/icon (aim-icon :letters))}])
  474. {}))
  475. ;; more - updater
  476. (ui/dropdown-with-links
  477. (fn [{:keys [toggle-fn]}]
  478. (ui/button
  479. [:span (ui/icon "dots-vertical")]
  480. :class "more-do"
  481. :on-click toggle-fn
  482. :intent "link"))
  483. (concat (if market?
  484. [{:title [:span.flex.items-center (ui/icon "rotate-clockwise") (t :plugin/refresh-lists)]
  485. :options {:on-click #(reload-market-fn)}}]
  486. [{:title [:span.flex.items-center (ui/icon "rotate-clockwise") (t :plugin/check-all-updates)]
  487. :options {:on-click #(plugin-handler/check-enabled-for-updates (not= :plugins category))}}])
  488. [{:title [:span.flex.items-center (ui/icon "world") (t :settings-page/network-proxy)]
  489. :options {:on-click #(state/pub-event! [:go/proxy-settings agent-opts])}}]
  490. [{:title [:span.flex.items-center (ui/icon "arrow-down-circle") (t :plugin.install-from-file/menu-title)]
  491. :options {:on-click plugin-config-handler/open-replace-plugins-modal}}]
  492. (when (state/developer-mode?)
  493. [{:hr true}
  494. {:title [:span.flex.items-center (ui/icon "file-code") "Open Preferences"]
  495. :options {:on-click
  496. #(p/let [root (plugin-handler/get-ls-dotdir-root)]
  497. (js/apis.openPath (str root "/preferences.json")))}}
  498. {:title [:span.flex.items-center (ui/icon "bug") "Open " [:code " ~/.logseq"]]
  499. :options {:on-click
  500. #(p/let [root (plugin-handler/get-ls-dotdir-root)]
  501. (js/apis.openPath root))}}]))
  502. {})
  503. ;; developer
  504. (panel-tab-developer)]]))
  505. (rum/defcs marketplace-plugins
  506. < rum/static rum/reactive
  507. (rum/local false ::fetching)
  508. (rum/local "" ::search-key)
  509. (rum/local :plugins ::category)
  510. (rum/local :downloads ::sort-by) ;; downloads / stars / letters / updates
  511. (rum/local :default ::filter-by)
  512. (rum/local nil ::error)
  513. {:did-mount (fn [s]
  514. (let [reload-fn (fn [force-refresh?]
  515. (when-not @(::fetching s)
  516. (reset! (::fetching s) true)
  517. (reset! (::error s) nil)
  518. (-> (plugin-handler/load-marketplace-plugins force-refresh?)
  519. (p/then #(plugin-handler/load-marketplace-stats false))
  520. (p/catch #(do (js/console.error %) (reset! (::error s) %)))
  521. (p/finally #(reset! (::fetching s) false)))))]
  522. (reload-fn false)
  523. (assoc s ::reload (partial reload-fn true))))}
  524. [state]
  525. (let [pkgs (state/sub :plugin/marketplace-pkgs)
  526. stats (state/sub :plugin/marketplace-stats)
  527. installed-plugins (state/sub :plugin/installed-plugins)
  528. installing (state/sub :plugin/installing)
  529. online? (state/sub :network/online?)
  530. develop-mode? (state/sub :ui/developer-mode?)
  531. agent-opts (state/sub [:electron/user-cfgs :settings/agent])
  532. *search-key (::search-key state)
  533. *category (::category state)
  534. *sort-by (::sort-by state)
  535. *filter-by (::filter-by state)
  536. *fetching (::fetching state)
  537. *error (::error state)
  538. filtered-pkgs (when (seq pkgs)
  539. (if (= @*category :themes)
  540. (filter #(:theme %) pkgs)
  541. (filter #(not (:theme %)) pkgs)))
  542. filtered-pkgs (if (and (seq filtered-pkgs) (not= :default @*filter-by))
  543. (filter #(apply
  544. (if (= :installed @*filter-by) identity not)
  545. [(contains? installed-plugins (keyword (:id %)))])
  546. filtered-pkgs)
  547. filtered-pkgs)
  548. filtered-pkgs (if-not (string/blank? @*search-key)
  549. (if-let [author (and (string/starts-with? @*search-key "@")
  550. (subs @*search-key 1))]
  551. (filter #(= author (:author %)) filtered-pkgs)
  552. (search/fuzzy-search
  553. filtered-pkgs @*search-key
  554. :limit 30
  555. :extract-fn :title))
  556. filtered-pkgs)
  557. filtered-pkgs (map #(if-let [stat (get stats (keyword (:id %)))]
  558. (let [downloads (:total_downloads stat)
  559. stars (:stargazers_count stat)]
  560. (assoc % :stat stat
  561. :stars stars
  562. :downloads downloads))
  563. %) filtered-pkgs)
  564. sorted-pkgs (apply sort-by
  565. (conj
  566. (case @*sort-by
  567. :letters [#(util/safe-lower-case (or (:title %) (:name %)))]
  568. [@*sort-by #(compare %2 %1)])
  569. filtered-pkgs))]
  570. [:div.cp__plugins-marketplace
  571. (panel-control-tabs
  572. @*search-key *search-key
  573. @*category *category
  574. @*sort-by *sort-by @*filter-by *filter-by
  575. nil true develop-mode? (::reload state)
  576. agent-opts)
  577. (cond
  578. (not online?)
  579. [:p.flex.justify-center.pt-20.opacity-50 (svg/offline 30)]
  580. @*fetching
  581. [:p.flex.justify-center.py-20 svg/loading]
  582. @*error
  583. [:p.flex.justify-center.pt-20.opacity-50 "Remote error: " (.-message @*error)]
  584. :else
  585. [:div.cp__plugins-marketplace-cnt
  586. {:class (util/classnames [{:has-installing (boolean installing)}])}
  587. [:div.cp__plugins-item-lists.grid-cols-1.md:grid-cols-2.lg:grid-cols-3
  588. ;; items list
  589. (for [item sorted-pkgs]
  590. (rum/with-key
  591. (let [pid (keyword (:id item))
  592. stat (:stat item)]
  593. (plugin-item-card t item
  594. (get-in item [:settings :disabled]) true *search-key installing
  595. (and installing (= (keyword (:id installing)) pid))
  596. (contains? installed-plugins pid) stat nil))
  597. (:id item)))]])]))
  598. (rum/defcs installed-plugins
  599. < rum/static rum/reactive
  600. (rum/local "" ::search-key)
  601. (rum/local :default ::filter-by) ;; default / enabled / disabled / unpacked / update-available
  602. (rum/local :default ::sort-by)
  603. (rum/local :plugins ::category)
  604. [state]
  605. (let [installed-plugins (state/sub [:plugin/installed-plugins])
  606. installed-plugins (vals installed-plugins)
  607. updating (state/sub :plugin/installing)
  608. develop-mode? (state/sub :ui/developer-mode?)
  609. selected-unpacked-pkg (state/sub :plugin/selected-unpacked-pkg)
  610. coming-updates (state/sub :plugin/updates-coming)
  611. agent-opts (state/sub [:electron/user-cfgs :settings/agent])
  612. *filter-by (::filter-by state)
  613. *sort-by (::sort-by state)
  614. *search-key (::search-key state)
  615. *category (::category state)
  616. default-filter-by? (= :default @*filter-by)
  617. filtered-plugins (when (seq installed-plugins)
  618. (if (= @*category :themes)
  619. (filter #(:theme %) installed-plugins)
  620. (filter #(not (:theme %)) installed-plugins)))
  621. filtered-plugins (if-not default-filter-by?
  622. (filter (fn [it]
  623. (let [disabled (get-in it [:settings :disabled])]
  624. (case @*filter-by
  625. :enabled (not disabled)
  626. :disabled disabled
  627. :unpacked (not (:iir it))
  628. :update-available (state/plugin-update-available? (:id it))
  629. true))) filtered-plugins)
  630. filtered-plugins)
  631. filtered-plugins (if-not (string/blank? @*search-key)
  632. (if-let [author (and (string/starts-with? @*search-key "@")
  633. (subs @*search-key 1))]
  634. (filter #(= author (:author %)) filtered-plugins)
  635. (search/fuzzy-search
  636. filtered-plugins @*search-key
  637. :limit 30
  638. :extract-fn :name))
  639. filtered-plugins)
  640. sorted-plugins (if default-filter-by?
  641. (->> filtered-plugins
  642. (reduce #(let [k (if (get-in %2 [:settings :disabled]) 1 0)]
  643. (update %1 k conj %2)) [[] []])
  644. (#(update % 0 (fn [coll] (sort-by :iir coll))))
  645. (flatten))
  646. filtered-plugins)]
  647. [:div.cp__plugins-installed
  648. (panel-control-tabs
  649. @*search-key *search-key
  650. @*category *category
  651. @*sort-by *sort-by
  652. @*filter-by *filter-by
  653. selected-unpacked-pkg
  654. false develop-mode? nil
  655. agent-opts)
  656. [:div.cp__plugins-item-lists.grid-cols-1.md:grid-cols-2.lg:grid-cols-3
  657. (for [item sorted-plugins]
  658. (rum/with-key
  659. (let [pid (keyword (:id item))]
  660. (plugin-item-card t item
  661. (get-in item [:settings :disabled]) false *search-key updating
  662. (and updating (= (keyword (:id updating)) pid))
  663. true nil (get coming-updates pid)))
  664. (:id item)))]]))
  665. (rum/defcs waiting-coming-updates
  666. < rum/reactive
  667. {:will-mount (fn [s] (state/reset-unchecked-update) s)}
  668. [_s]
  669. (let [_ (state/sub :plugin/updates-coming)
  670. downloading? (state/sub :plugin/updates-downloading?)
  671. unchecked (state/sub :plugin/updates-unchecked)
  672. updates (state/all-available-coming-updates)]
  673. [:div.cp__plugins-waiting-updates
  674. [:h1.mb-4.text-2xl.p-1 (util/format "Found %s updates" (count updates))]
  675. (if (seq updates)
  676. ;; lists
  677. [:ul
  678. {:class (when downloading? "downloading")}
  679. (for [it updates
  680. :let [k (str "lsp-it-" (:id it))
  681. c? (not (contains? unchecked (:id it)))
  682. notes (util/trim-safe (:latest-notes it))]]
  683. [:li.flex.items-center
  684. {:key k
  685. :class (when c? "checked")}
  686. [:label.flex-1
  687. {:for k}
  688. (ui/checkbox {:id k
  689. :checked c?
  690. :on-change (fn [^js e]
  691. (when-not downloading?
  692. (state/set-unchecked-update (:id it) (not (util/echecked? e)))))})
  693. [:strong.px-3 (:title it)
  694. [:sup (str (:version it) " 👉 " (:latest-version it))]]]
  695. [:div.px-4
  696. (when-not (string/blank? notes)
  697. (ui/tippy
  698. {:html [:p notes]}
  699. [:span.opacity-30.hover:opacity-80 (ui/icon "info-circle")]))]])]
  700. ;; all done
  701. [:div.py-4 [:strong.text-4xl "\uD83C\uDF89 All updated!"]])
  702. ;; actions
  703. (when (seq updates)
  704. [:div.pt-5
  705. (ui/button
  706. (if downloading?
  707. [:span (ui/loading " Downloading...")]
  708. [:span "Update all of selected"])
  709. :on-click
  710. #(when-not downloading?
  711. (plugin-handler/open-updates-downloading)
  712. (if-let [n (state/get-next-selected-coming-update)]
  713. (plugin-handler/check-or-update-marketplace-plugin
  714. (assoc n :only-check false)
  715. (fn [^js e] (notification/show! (.toString e) :error)))
  716. (plugin-handler/close-updates-downloading)))
  717. :disabled
  718. (or downloading?
  719. (and (seq unchecked)
  720. (= (count unchecked) (count updates)))))])]))
  721. (rum/defc plugins-from-file
  722. < rum/reactive
  723. [plugins]
  724. [:div.cp__plugins-fom-file
  725. [:h1.mb-4.text-2xl.p-1 (t :plugin.install-from-file/title)]
  726. (if (seq plugins)
  727. [:div
  728. [:div.mb-2.text-xl (t :plugin.install-from-file/notice)]
  729. ;; lists
  730. [:ul
  731. (for [it (:install plugins)
  732. :let [k (str "lsp-it-" (name (:id it)))]]
  733. [:li.flex.items-center
  734. {:key k}
  735. [:label.flex-1
  736. {:for k}
  737. [:strong.px-3 (str (name (:id it)) " " (:version it))]]])]
  738. ;; actions
  739. [:div.pt-5
  740. (ui/button [:span (t :plugin/install)]
  741. :on-click #(do
  742. (plugin-config-handler/replace-plugins plugins)
  743. (state/close-sub-modal! "ls-plugins-from-file-modal")))]]
  744. ;; all done
  745. [:div.py-4 [:strong.text-xl (str "\uD83C\uDF89 " (t :plugin.install-from-file/success))]])])
  746. (defn open-select-theme!
  747. []
  748. (state/set-sub-modal! installed-themes))
  749. (rum/defc hook-ui-slot
  750. ([type payload] (hook-ui-slot type payload nil))
  751. ([type payload opts]
  752. (let [rs (util/rand-str 8)
  753. id (str "slot__" rs)
  754. *el-ref (rum/use-ref nil)]
  755. (rum/use-effect!
  756. (fn []
  757. (let [timer (js/setTimeout
  758. #(plugin-handler/hook-plugin-app type {:slot id :payload payload} nil)
  759. 100)]
  760. #(js/clearTimeout timer)))
  761. [id])
  762. (rum/use-effect!
  763. (fn []
  764. (let [el (rum/deref *el-ref)]
  765. #(when-let [uis (seq (.querySelectorAll el "[data-injected-ui]"))]
  766. (doseq [^js el uis]
  767. (when-let [id (.-injectedUi (.-dataset el))]
  768. (js/LSPluginCore._forceCleanInjectedUI id))))))
  769. [])
  770. [:div.lsp-hook-ui-slot
  771. (merge opts {:id id
  772. :ref *el-ref
  773. :on-mouse-down (fn [e] (util/stop e))})])))
  774. (rum/defc ui-item-renderer
  775. [pid type {:keys [key template prefix]}]
  776. (let [*el (rum/use-ref nil)
  777. uni #(str prefix "injected-ui-item-" %)
  778. ^js pl (js/LSPluginCore.registeredPlugins.get (name pid))]
  779. (rum/use-effect!
  780. (fn []
  781. (when-let [^js el (rum/deref *el)]
  782. (js/LSPlugin.pluginHelpers.setupInjectedUI.call
  783. pl #js {:slot (.-id el) :key key :template template} #js {})))
  784. [template])
  785. (if-not (nil? pl)
  786. [:div
  787. {:id (uni (str (name key) "-" (name pid)))
  788. :title key
  789. :class (uni (name type))
  790. :ref *el}]
  791. [:<>])))
  792. (rum/defc toolbar-plugins-manager-list
  793. [items]
  794. (ui/dropdown-with-links
  795. (fn [{:keys [toggle-fn]}]
  796. [:div.toolbar-plugins-manager
  797. {:on-click toggle-fn}
  798. [:a.button (ui/icon "puzzle" {:size 20})]])
  799. ;; items
  800. (for [[_ {:keys [key pinned?] :as opts} pid] items
  801. :let [pkey (str (name pid) ":" key)]]
  802. {:title key
  803. :item [:div.flex.items-center.item-wrap
  804. (ui-item-renderer pid :toolbar (assoc opts :prefix "pl-" :key (str "pl-" key)))
  805. [:span.opacity-80 {:style {:padding-left "2px"}} key]
  806. [:span.pin.flex.items-center.opacity-60
  807. {:class (util/classnames [{:pinned pinned?}])}
  808. (ui/icon (if pinned? "pinned" "pin"))]]
  809. :options {:on-click (fn [^js e]
  810. (let [^js target (.-target e)
  811. user-btn? (boolean (.closest target "div[data-injected-ui]"))]
  812. (when-not user-btn?
  813. (plugin-handler/op-pinned-toolbar-item! pkey (if pinned? :remove :add))))
  814. false)}})
  815. {:trigger-class "toolbar-plugins-manager-trigger"}))
  816. (rum/defcs hook-ui-items < rum/reactive
  817. < {:key-fn #(identity "plugin-hook-items")}
  818. "type of :toolbar, :pagebar"
  819. [_state type]
  820. (when (state/sub [:plugin/installed-ui-items])
  821. (let [toolbar? (= :toolbar type)
  822. pinned-items (state/sub [:plugin/preferences :pinnedToolbarItems])
  823. pinned-items (and (sequential? pinned-items) (into #{} pinned-items))
  824. items (state/get-plugins-ui-items-with-type type)
  825. items (sort-by #(:key (second %)) items)]
  826. (when-let [items (and (seq items)
  827. (if toolbar?
  828. (map #(assoc-in % [1 :pinned?]
  829. (let [[_ {:keys [key]} pid] %
  830. pkey (str (name pid) ":" key)]
  831. (contains? pinned-items pkey)))
  832. items)
  833. items))]
  834. [:div {:class (str "ui-items-container")
  835. :data-type (name type)}
  836. (conj (for [[_ {:keys [key pinned?] :as opts} pid] items]
  837. (when (or (not toolbar?)
  838. (not (set? pinned-items)) pinned?)
  839. (rum/with-key (ui-item-renderer pid type opts) key))))
  840. ;; manage plugin buttons
  841. (when toolbar?
  842. (toolbar-plugins-manager-list items))]))))
  843. (rum/defcs hook-ui-fenced-code < rum/reactive
  844. [_state content {:keys [render edit] :as _opts}]
  845. [:div
  846. {:on-mouse-down (fn [e] (when (false? edit) (util/stop e)))
  847. :class (util/classnames [{:not-edit (false? edit)}])}
  848. (when (fn? render)
  849. (js/React.createElement render #js {:content content}))])
  850. (rum/defc plugins-page
  851. []
  852. (let [[active set-active!] (rum/use-state :installed)
  853. market? (= active :marketplace)
  854. *el-ref (rum/create-ref)]
  855. (rum/use-effect!
  856. #(state/load-app-user-cfgs)
  857. [])
  858. [:div.cp__plugins-page
  859. {:ref *el-ref
  860. :tab-index "-1"}
  861. [:h1 (t :plugins)]
  862. (security-warning)
  863. [:hr]
  864. [:div.tabs.flex.items-center.justify-center
  865. [:div.tabs-inner.flex.items-center
  866. (ui/button [:span.it (t :plugin/installed)]
  867. :on-click #(set-active! :installed)
  868. :intent "logseq" :class (if-not market? "active" ""))
  869. (ui/button [:span.mk (svg/apps 16) (t :plugin/marketplace)]
  870. :on-click #(set-active! :marketplace)
  871. :intent "logseq" :class (if market? "active" ""))]]
  872. [:div.panels
  873. (if market?
  874. (marketplace-plugins)
  875. (installed-plugins))]]))
  876. (rum/defcs focused-settings-content
  877. < rum/reactive
  878. (rum/local (state/sub :plugin/focused-settings) ::cache)
  879. [_state title]
  880. (let [*cache (::cache _state)
  881. focused (state/sub :plugin/focused-settings)
  882. nav? (state/sub :plugin/navs-settings?)
  883. _ (state/sub :plugin/installed-plugins)
  884. _ (js/setTimeout #(reset! *cache focused) 100)]
  885. [:div.cp__plugins-settings.cp__settings-main
  886. [:header
  887. [:h1.title (ui/icon "puzzle") (str " " (or title (t :settings-of-plugins)))]]
  888. [:div.cp__settings-inner.md:flex
  889. {:class (util/classnames [{:no-aside (not nav?)}])}
  890. (when nav?
  891. [:aside.md:w-64 {:style {:min-width "10rem"}}
  892. (let [plugins (plugin-handler/get-enabled-plugins-if-setting-schema)]
  893. [:ul.settings-plugin-list
  894. (for [{:keys [id name title icon]} plugins]
  895. [:li
  896. {:class (util/classnames [{:active (= id focused)}])}
  897. [:a.flex.items-center.settings-plugin-item
  898. {:data-id id
  899. :on-click #(do (state/set-state! :plugin/focused-settings id))}
  900. (if (and icon (not (string/blank? icon)))
  901. [:img.icon {:src icon}]
  902. svg/folder)
  903. [:strong.flex-1 (or title name)]]])])])
  904. [:article
  905. [:div.panel-wrap
  906. {:data-id focused}
  907. (when-let [^js pl (and focused (= @*cache focused)
  908. (plugin-handler/get-plugin-inst focused))]
  909. (ui/catch-error
  910. [:p.warning.text-lg.mt-5 "Settings schema Error!"]
  911. (plugins-settings/settings-container
  912. (bean/->clj (.-settingsSchema pl)) pl)))]]]]))
  913. (rum/defc custom-js-installer
  914. [{:keys [t current-repo db-restoring? nfs-granted?]}]
  915. (rum/use-effect!
  916. (fn []
  917. (when (and (not db-restoring?)
  918. (or (not util/nfs?) nfs-granted?))
  919. (ui-handler/exec-js-if-exists-&-allowed! t)))
  920. [current-repo db-restoring? nfs-granted?])
  921. nil)
  922. (rum/defc perf-tip-content
  923. [pid name url]
  924. [:div
  925. [:span.block.whitespace-normal
  926. "This plugin "
  927. [:strong.text-error "#" name]
  928. " takes too long to load, affecting the application startup time and
  929. potentially causing other plugins to fail to load."]
  930. [:path.opacity-50
  931. [:small [:span.pr-1 (ui/icon "folder")] url]]
  932. [:p
  933. (ui/button "Disable now"
  934. :small? true
  935. :on-click
  936. (fn []
  937. (-> (js/LSPluginCore.disable pid)
  938. (p/then #(do
  939. (notification/clear! pid)
  940. (notification/show!
  941. [:span "The plugin "
  942. [:strong.text-error "#" name]
  943. " is disabled."] :success
  944. true nil 3000)))
  945. (p/catch #(js/console.error %)))))]])
  946. (defn open-plugins-modal!
  947. []
  948. (state/set-modal!
  949. (fn [_close!]
  950. (plugins-page))
  951. {:label "plugins-dashboard"}))
  952. (defn open-waiting-updates-modal!
  953. []
  954. (state/set-sub-modal!
  955. (fn [_close!]
  956. (waiting-coming-updates))
  957. {:center? true}))
  958. (defn open-plugins-from-file-modal!
  959. [plugins]
  960. (state/set-sub-modal!
  961. (fn [_close!]
  962. (plugins-from-file plugins))
  963. {:center? true
  964. :id "ls-plugins-from-file-modal"}))
  965. (defn open-focused-settings-modal!
  966. [title]
  967. (state/set-sub-modal!
  968. (fn [_close!]
  969. [:div.settings-modal.of-plugins
  970. (focused-settings-content title)])
  971. {:center? false
  972. :id "ls-focused-settings-modal"}))