plugins.cljs 42 KB

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