plugin.cljs 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547
  1. (ns frontend.handler.plugin
  2. (:require [promesa.core :as p]
  3. [rum.core :as rum]
  4. [frontend.util :as util]
  5. [frontend.format.mldoc :as mldoc]
  6. [frontend.handler.notification :as notifications]
  7. [camel-snake-kebab.core :as csk]
  8. [frontend.state :as state]
  9. [medley.core :as md]
  10. [frontend.fs :as fs]
  11. [electron.ipc :as ipc]
  12. [cljs-bean.core :as bean]
  13. [clojure.string :as string]
  14. [lambdaisland.glogi :as log]
  15. [frontend.components.svg :as svg]
  16. [frontend.format :as format]))
  17. (defonce lsp-enabled?
  18. (and (util/electron?)
  19. (state/lsp-enabled?-or-theme)))
  20. (defn invoke-exported-api
  21. [type & args]
  22. (try
  23. (apply js-invoke (aget js/window.logseq "api") type args)
  24. (catch js/Error e (js/console.error e))))
  25. ;; state handlers
  26. (defonce central-endpoint "https://raw.githubusercontent.com/logseq/marketplace/master/")
  27. (defonce plugins-url (str central-endpoint "plugins.json"))
  28. (defonce stats-url (str central-endpoint "stats.json"))
  29. (declare select-a-plugin-theme)
  30. (defn gh-repo-url [repo]
  31. (str "https://github.com/" repo))
  32. (defn pkg-asset [id asset]
  33. (if (and asset (string/starts-with? asset "http"))
  34. asset (when-let [asset (and asset (string/replace asset #"^[./]+" ""))]
  35. (str central-endpoint "packages/" id "/" asset))))
  36. (defn load-marketplace-plugins
  37. [refresh?]
  38. (if (or refresh? (nil? (:plugin/marketplace-pkgs @state/state)))
  39. (p/create
  40. (fn [resolve reject]
  41. (-> (util/fetch plugins-url
  42. (fn [res]
  43. (let [pkgs (:packages res)]
  44. (state/set-state! :plugin/marketplace-pkgs pkgs)
  45. (resolve pkgs)))
  46. reject)
  47. (p/catch reject))))
  48. (p/resolved (:plugin/marketplace-pkgs @state/state))))
  49. (defn load-marketplace-stats
  50. [refresh?]
  51. (if (or refresh? (nil? (:plugin/marketplace-stats @state/state)))
  52. (p/create
  53. (fn [resolve reject]
  54. (util/fetch stats-url
  55. (fn [res]
  56. (when res
  57. (state/set-state!
  58. :plugin/marketplace-stats
  59. (into {} (map (fn [[k stat]]
  60. [k (assoc stat
  61. :total_downloads
  62. (reduce (fn [a b] (+ a (get b 2))) 0 (:releases stat)))])
  63. res)))
  64. (resolve nil)))
  65. reject)))
  66. (p/resolved nil)))
  67. (defn installed?
  68. [id]
  69. (and (contains? (:plugin/installed-plugins @state/state) (keyword id))
  70. (get-in @state/state [:plugin/installed-plugins (keyword id) :iir])))
  71. (defn install-marketplace-plugin
  72. [{:keys [id] :as mft}]
  73. (when-not (and (:plugin/installing @state/state)
  74. (installed? id))
  75. (p/create
  76. (fn [resolve]
  77. (state/set-state! :plugin/installing mft)
  78. (ipc/ipc "installMarketPlugin" mft)
  79. (resolve id)))))
  80. (defn check-or-update-marketplace-plugin
  81. [{:keys [id] :as pkg} error-handler]
  82. (when-not (and (:plugin/installing @state/state)
  83. (not (installed? id)))
  84. (p/catch
  85. (p/then
  86. (do (state/set-state! :plugin/installing pkg)
  87. (p/catch
  88. (load-marketplace-plugins false)
  89. (fn [^js e]
  90. (state/reset-all-updates-state)
  91. (throw e))))
  92. (fn [mfts]
  93. (if-let [mft (some #(when (= (:id %) id) %) mfts)]
  94. (ipc/ipc "updateMarketPlugin" (merge (dissoc pkg :logger) mft))
  95. (throw (js/Error. (str ":not-found-in-marketplace" id))))
  96. true))
  97. (fn [^js e]
  98. (error-handler "Update Error: remote error")
  99. (state/set-state! :plugin/installing nil)
  100. (js/console.error e)))))
  101. (defn get-plugin-inst
  102. [id]
  103. (try
  104. (js/LSPluginCore.ensurePlugin id)
  105. (catch js/Error _e
  106. nil)))
  107. (defn open-updates-downloading
  108. []
  109. (when (and (not (:plugin/updates-downloading? @state/state))
  110. (seq (state/all-available-coming-updates)))
  111. (->> (:plugin/updates-coming @state/state)
  112. (map #(if (state/coming-update-new-version? (second %1))
  113. (update % 1 dissoc :error-code) %1))
  114. (into {})
  115. (state/set-state! :plugin/updates-coming))
  116. (state/set-state! :plugin/updates-downloading? true)))
  117. (defn close-updates-downloading
  118. []
  119. (when (:plugin/updates-downloading? @state/state)
  120. (state/set-state! :plugin/updates-downloading? false)))
  121. (defn has-setting-schema?
  122. [id]
  123. (when-let [pl (and id (get-plugin-inst (name id)))]
  124. (boolean (.-settingsSchema pl))))
  125. (defn get-enabled-plugins-if-setting-schema
  126. []
  127. (when-let [plugins (seq (state/get-enabled?-installed-plugins false nil true))]
  128. (filter #(has-setting-schema? (:id %)) plugins)))
  129. (defn setup-install-listener!
  130. [t]
  131. (let [channel (name :lsp-installed)
  132. listener (fn [^js _ ^js e]
  133. (js/console.debug :lsp-installed e)
  134. (when-let [{:keys [status payload only-check]} (bean/->clj e)]
  135. (case (keyword status)
  136. :completed
  137. (let [{:keys [id dst name title theme]} payload
  138. name (or title name "Untitled")]
  139. (if only-check
  140. (state/consume-updates-coming-plugin payload false)
  141. (if (installed? id)
  142. (when-let [^js pl (get-plugin-inst id)] ;; update
  143. (p/then
  144. (.reload pl)
  145. #(do
  146. ;;(if theme (select-a-plugin-theme id))
  147. (notifications/show!
  148. (str (t :plugin/update) (t :plugins) ": " name " - " (.-version (.-options pl))) :success)
  149. (state/consume-updates-coming-plugin payload true))))
  150. (do ;; register new
  151. (p/then
  152. (js/LSPluginCore.register (bean/->js {:key id :url dst}))
  153. (fn [] (when theme (js/setTimeout #(select-a-plugin-theme id) 300))))
  154. (notifications/show!
  155. (str (t :plugin/installed) (t :plugins) ": " name) :success)))))
  156. :error
  157. (let [error-code (keyword (string/replace (:error-code payload) #"^[\s\:]+" ""))
  158. [msg type] (case error-code
  159. :no-new-version
  160. [(str (t :plugin/up-to-date) " :)") :success]
  161. [error-code :error])
  162. pending? (seq (:plugin/updates-pending @state/state))]
  163. (if (and only-check pending?)
  164. (state/consume-updates-coming-plugin payload false)
  165. (do
  166. ;; consume failed download updates
  167. (when (and (not only-check) (not pending?))
  168. (state/consume-updates-coming-plugin payload true))
  169. ;; notify human tips
  170. (notifications/show!
  171. (str
  172. (if (= :error type) "[Install Error]" "")
  173. msg) type)))
  174. (js/console.error payload))
  175. :dunno))
  176. ;; reset
  177. (js/setTimeout #(state/set-state! :plugin/installing nil) 512)
  178. true)]
  179. (js/window.apis.addListener channel listener)
  180. ;; clear
  181. (fn []
  182. (js/window.apis.removeAllListeners channel))))
  183. (defn register-plugin
  184. [pl]
  185. (swap! state/state update-in [:plugin/installed-plugins] assoc (keyword (:id pl)) pl))
  186. (defn unregister-plugin
  187. [id]
  188. (js/LSPluginCore.unregister id))
  189. (defn host-mounted!
  190. []
  191. (and lsp-enabled? (js/LSPluginCore.hostMounted)))
  192. (defn register-plugin-slash-command
  193. [pid [cmd actions]]
  194. (when-let [pid (keyword pid)]
  195. (when (contains? (:plugin/installed-plugins @state/state) pid)
  196. (swap! state/state update-in [:plugin/installed-commands pid]
  197. (fnil merge {}) (hash-map cmd (mapv #(conj % {:pid pid}) actions)))
  198. true)))
  199. (defn unregister-plugin-slash-command
  200. [pid]
  201. (swap! state/state md/dissoc-in [:plugin/installed-commands (keyword pid)]))
  202. (def keybinding-mode-handler-map
  203. {:global :shortcut.handler/editor-global
  204. :non-editing :shortcut.handler/global-non-editing-only
  205. :editing :shortcut.handler/block-editing-only})
  206. (defn simple-cmd->palette-cmd
  207. [pid {:keys [key label type desc keybinding] :as cmd} action]
  208. (let [palette-cmd {:id (keyword (str "plugin." pid "/" key))
  209. :desc (or desc label)
  210. :shortcut (when-let [shortcut (:binding keybinding)]
  211. (if util/mac?
  212. (or (:mac keybinding) shortcut)
  213. shortcut))
  214. :handler-id (let [mode (or (:mode keybinding) :global)]
  215. (get keybinding-mode-handler-map (keyword mode)))
  216. :action (fn []
  217. (state/pub-event!
  218. [:exec-plugin-cmd {:type type :key key :pid pid :cmd cmd :action action}]))}]
  219. palette-cmd))
  220. (defn simple-cmd-keybinding->shortcut-args
  221. [pid key keybinding]
  222. (let [id (keyword (str "plugin." pid "/" key))
  223. binding (:binding keybinding)
  224. binding (if util/mac?
  225. (or (:mac keybinding) binding)
  226. binding)
  227. mode (or (:mode keybinding) :global)
  228. mode (get keybinding-mode-handler-map (keyword mode))]
  229. [mode id {:binding binding}]))
  230. (defn register-plugin-simple-command
  231. ;; action => [:action-key :event-key]
  232. [pid {:keys [type] :as cmd} action]
  233. (when-let [pid (keyword pid)]
  234. (when (contains? (:plugin/installed-plugins @state/state) pid)
  235. (swap! state/state update-in [:plugin/simple-commands pid]
  236. (fnil conj []) [type cmd action pid])
  237. true)))
  238. (defn unregister-plugin-simple-command
  239. [pid]
  240. (swap! state/state md/dissoc-in [:plugin/simple-commands (keyword pid)]))
  241. (defn register-plugin-ui-item
  242. [pid {:keys [type] :as opts}]
  243. (when-let [pid (keyword pid)]
  244. (when (contains? (:plugin/installed-plugins @state/state) pid)
  245. (swap! state/state update-in [:plugin/installed-ui-items pid]
  246. (fnil conj []) [type opts pid])
  247. true)))
  248. (defn unregister-plugin-ui-items
  249. [pid]
  250. (swap! state/state assoc-in [:plugin/installed-ui-items (keyword pid)] []))
  251. (defn unregister-plugin-themes
  252. ([pid] (unregister-plugin-themes pid true))
  253. ([pid effect]
  254. (js/LSPluginCore.unregisterTheme (name pid) effect)))
  255. (defn select-a-plugin-theme
  256. [pid]
  257. (when-let [themes (get (group-by :pid (:plugin/installed-themes @state/state)) pid)]
  258. (when-let [theme (first themes)]
  259. (let [theme-mode (:mode theme)]
  260. (and theme-mode (state/set-theme! (if (= theme-mode "light") "white" theme-mode)))
  261. (js/LSPluginCore.selectTheme (bean/->js theme))))))
  262. (defn update-plugin-settings-state
  263. [id settings]
  264. (state/set-state! [:plugin/installed-plugins id :settings]
  265. ;; TODO: force settings related ui reactive
  266. ;; Sometimes toggle to `disable` not working
  267. ;; But related-option data updated?
  268. (assoc settings :disabled (boolean (:disabled settings)))))
  269. (defn open-settings-file-in-default-app!
  270. [id-or-plugin]
  271. (when-let [plugin (if (coll? id-or-plugin)
  272. id-or-plugin (state/get-plugin-by-id id-or-plugin))]
  273. (when-let [file-path (:usf plugin)]
  274. (js/apis.openPath file-path))))
  275. (defn open-plugin-settings!
  276. ([id] (open-plugin-settings! id false))
  277. ([id nav?]
  278. (when-let [plugin (and id (state/get-plugin-by-id id))]
  279. (if (has-setting-schema? id)
  280. (state/pub-event! [:go/plugins-settings id nav? (or (:name plugin) (:title plugin))])
  281. (open-settings-file-in-default-app! plugin)))))
  282. (defn parse-user-md-content
  283. [content {:keys [url]}]
  284. (try
  285. (when-not (string/blank? content)
  286. (let [content (if-not (string/blank? url)
  287. (string/replace
  288. content #"!\[[^\]]*\]\((.*?)\s*(\"(?:.*[^\"])\")?\s*\)"
  289. (fn [[matched link]]
  290. (if (and link (not (string/starts-with? link "http")))
  291. (string/replace matched link (util/node-path.join url link))
  292. matched)))
  293. content)]
  294. (format/to-html content :markdown (mldoc/default-config :markdown))))
  295. (catch js/Error e
  296. (log/error :parse-user-md-exception e)
  297. content)))
  298. (defn open-readme!
  299. [url item display]
  300. (let [repo (:repo item)]
  301. (if (nil? repo)
  302. ;; local
  303. (-> (p/let [content (invoke-exported-api "load_plugin_readme" url)
  304. content (parse-user-md-content content item)]
  305. (and (string/blank? (string/trim content)) (throw nil))
  306. (state/set-state! :plugin/active-readme [content item])
  307. (state/set-sub-modal! (fn [_] (display))))
  308. (p/catch #(do (js/console.warn %)
  309. (notifications/show! "No README content." :warn))))
  310. ;; market
  311. (state/set-sub-modal! (fn [_] (display repo nil))))))
  312. (defn load-unpacked-plugin
  313. []
  314. (when util/electron?
  315. (p/let [path (ipc/ipc "openDialog")]
  316. (when-not (:plugin/selected-unpacked-pkg @state/state)
  317. (state/set-state! :plugin/selected-unpacked-pkg path)))))
  318. (defn reset-unpacked-state
  319. []
  320. (state/set-state! :plugin/selected-unpacked-pkg nil))
  321. (defn hook-plugin
  322. [tag type payload plugin-id]
  323. (when lsp-enabled?
  324. (js-invoke js/LSPluginCore
  325. (str "hook" (string/capitalize (name tag)))
  326. (name type)
  327. (if (coll? payload)
  328. (bean/->js (into {} (for [[k v] payload] [(csk/->camelCase k) (if (uuid? v) (str v) v)])))
  329. payload)
  330. (if (keyword? plugin-id) (name plugin-id) plugin-id))))
  331. (defn hook-plugin-app
  332. ([type payload] (hook-plugin-app type payload nil))
  333. ([type payload plugin-id] (hook-plugin :app type payload plugin-id)))
  334. (defn hook-plugin-editor
  335. ([type payload] (hook-plugin-editor type payload nil))
  336. ([type payload plugin-id] (hook-plugin :editor type payload plugin-id)))
  337. (defn get-ls-dotdir-root
  338. []
  339. (ipc/ipc "getLogseqDotDirRoot"))
  340. (defn make-fn-to-load-dotdir-json
  341. [dirname default]
  342. (fn [key]
  343. (when-let [key (and key (name key))]
  344. (p/let [repo ""
  345. path (get-ls-dotdir-root)
  346. exist? (fs/file-exists? path dirname)
  347. _ (when-not exist? (fs/mkdir! (util/node-path.join path dirname)))
  348. path (util/node-path.join path dirname (str key ".json"))
  349. _ (fs/create-if-not-exists repo "" path (or default "{}"))
  350. json (fs/read-file "" path)]
  351. [path (js/JSON.parse json)]))))
  352. (defn make-fn-to-save-dotdir-json
  353. [dirname]
  354. (fn [key content]
  355. (when-let [key (and key (name key))]
  356. (p/let [repo ""
  357. path (get-ls-dotdir-root)
  358. path (util/node-path.join path dirname (str key ".json"))]
  359. (fs/write-file! repo "" path content {:skip-compare? true})))))
  360. (defn make-fn-to-unlink-dotdir-json
  361. [dirname]
  362. (fn [key]
  363. (when-let [key (and key (name key))]
  364. (p/let [repo ""
  365. path (get-ls-dotdir-root)
  366. path (util/node-path.join path dirname (str key ".json"))]
  367. (fs/unlink! repo path nil)))))
  368. (defn show-themes-modal!
  369. []
  370. (state/pub-event! [:modal/show-themes-modal]))
  371. (defn goto-plugins-dashboard!
  372. []
  373. (state/pub-event! [:go/plugins]))
  374. (defn- get-user-default-plugins
  375. []
  376. (p/catch
  377. (p/let [files ^js (ipc/ipc "getUserDefaultPlugins")
  378. files (js->clj files)]
  379. (map #(hash-map :url %) files))
  380. (fn [e]
  381. (js/console.error e))))
  382. (defn check-enabled-for-updates
  383. [theme?]
  384. (let [pending? (seq (:plugin/updates-pending @state/state))]
  385. (when-let [plugins (and (not pending?)
  386. ;; TODO: too many requests may be limited by Github api
  387. (seq (take 32 (state/get-enabled?-installed-plugins theme?))))]
  388. (state/set-state! :plugin/updates-pending
  389. (into {} (map (fn [v] [(keyword (:id v)) v]) plugins)))
  390. (state/pub-event! [:plugin/consume-updates]))))
  391. ;; components
  392. (rum/defc lsp-indicator < rum/reactive
  393. []
  394. (let [text (state/sub :plugin/indicator-text)]
  395. (when-not (= text "END")
  396. [:div.flex.align-items.justify-center.h-screen.w-full.preboot-loading
  397. [:span.flex.items-center.justify-center.w-60.flex-col
  398. [:small.scale-250.opacity-70.mb-10.animate-pulse (svg/logo false)]
  399. [:small.block.text-sm.relative.opacity-50 {:style {:right "-8px"}} text]]])))
  400. (defn init-plugins!
  401. [callback]
  402. (let [el (js/document.createElement "div")]
  403. (.appendChild js/document.body el)
  404. (rum/mount
  405. (lsp-indicator) el))
  406. (state/set-state! :plugin/indicator-text "LOADING")
  407. (p/then
  408. (p/let [root (get-ls-dotdir-root)
  409. _ (.setupPluginCore js/LSPlugin (bean/->js {:localUserConfigRoot root :dotConfigRoot root}))
  410. clear-commands! (fn [pid]
  411. ;; commands
  412. (unregister-plugin-slash-command pid)
  413. (invoke-exported-api "unregister_plugin_simple_command" pid)
  414. (unregister-plugin-ui-items pid))
  415. _ (doto js/LSPluginCore
  416. (.on "registered"
  417. (fn [^js pl]
  418. (register-plugin
  419. (bean/->clj (.parse js/JSON (.stringify js/JSON pl))))))
  420. (.on "reloaded"
  421. (fn [^js pl]
  422. (register-plugin
  423. (bean/->clj (.parse js/JSON (.stringify js/JSON pl))))))
  424. (.on "unregistered" (fn [pid]
  425. (let [pid (keyword pid)]
  426. ;; effects
  427. (unregister-plugin-themes pid)
  428. ;; plugins
  429. (swap! state/state md/dissoc-in [:plugin/installed-plugins pid])
  430. ;; commands
  431. (clear-commands! pid))))
  432. (.on "unlink-plugin" (fn [pid]
  433. (let [pid (keyword pid)]
  434. (ipc/ipc "uninstallMarketPlugin" (name pid)))))
  435. (.on "beforereload" (fn [^js pl]
  436. (let [pid (.-id pl)]
  437. (clear-commands! pid)
  438. (unregister-plugin-themes pid false))))
  439. (.on "disabled" (fn [pid]
  440. (clear-commands! pid)
  441. (unregister-plugin-themes pid)))
  442. (.on "theme-changed" (fn [^js themes]
  443. (swap! state/state assoc :plugin/installed-themes
  444. (vec (mapcat (fn [[pid vs]] (mapv #(assoc % :pid pid) (bean/->clj vs))) (bean/->clj themes))))))
  445. (.on "theme-selected" (fn [^js opts]
  446. (let [opts (bean/->clj opts)
  447. url (:url opts)
  448. mode (:mode opts)]
  449. (when mode (state/set-theme! mode))
  450. (state/set-state! :plugin/selected-theme url))))
  451. (.on "settings-changed" (fn [id ^js settings]
  452. (let [id (keyword id)]
  453. (when (and settings
  454. (contains? (:plugin/installed-plugins @state/state) id))
  455. (update-plugin-settings-state id (bean/->clj settings)))))))
  456. default-plugins (get-user-default-plugins)
  457. _ (.register js/LSPluginCore (bean/->js (if (seq default-plugins) default-plugins [])) true)])
  458. #(do
  459. (state/set-state! :plugin/indicator-text "END")
  460. (callback))))
  461. (defn setup!
  462. "setup plugin core handler"
  463. [callback]
  464. (if (not lsp-enabled?)
  465. (callback)
  466. (init-plugins! callback)))