Files
logseq/src/main/frontend/components/plugins.cljs
2021-11-23 17:19:10 +08:00

479 lines
18 KiB
Clojure

(ns frontend.components.plugins
(:require [rum.core :as rum]
[frontend.state :as state]
[cljs-bean.core :as bean]
[frontend.context.i18n :as i18n]
[frontend.ui :as ui]
[frontend.handler.ui :as ui-handler]
[frontend.util :as util]
[frontend.mixins :as mixins]
[electron.ipc :as ipc]
[promesa.core :as p]
[frontend.components.svg :as svg]
[frontend.handler.notification :as notification]
[frontend.handler.plugin :as plugin-handler]
[frontend.handler.page :as page-handler]
[clojure.string :as string]))
(rum/defcs installed-themes
< rum/reactive
(rum/local 0 ::cursor)
(rum/local 0 ::total)
(mixins/event-mixin
(fn [state]
(let [*cursor (::cursor state)
*total (::total state)
^js target (rum/dom-node state)]
(.focus target)
(mixins/on-key-down
state {38 ;; up
(fn [^js e]
(reset! *cursor
(if (zero? @*cursor)
(dec @*total) (dec @*cursor))))
40 ;; down
(fn [^js e]
(reset! *cursor
(if (= @*cursor (dec @*total))
0 (inc @*cursor))))
13 ;; enter
#(when-let [^js active (.querySelector target ".is-active")]
(.click active))
}))))
[state]
(let [*cursor (::cursor state)
*total (::total state)
themes (state/sub :plugin/installed-themes)
selected (state/sub :plugin/selected-theme)
themes (cons {:name "Default Theme" :url nil :description "Logseq default light/dark theme."} themes)
themes (sort #(:selected %) (map #(assoc % :selected (= (:url %) selected)) themes))
_ (reset! *total (count themes))]
(rum/with-context
[[t] i18n/*tongue-context*]
[:div.cp__themes-installed
{:tab-index -1}
[:h1.mb-4.text-2xl.p-2 (t :themes)]
(map-indexed
(fn [idx opt]
(let [current-selected (:selected opt)
plg (get (:plugin/installed-plugins @state/state) (keyword (:pid opt)))]
[:div.it.flex.px-3.py-1.5.rounded-sm.justify-between
{:key (:url opt)
:title (if current-selected "Cancel selected theme")
:class (util/classnames
[{:is-selected current-selected
:is-active (= idx @*cursor)}])
:on-click #(do (js/LSPluginCore.selectTheme (if current-selected nil (clj->js opt)))
(state/set-modal! nil))}
[:section
[:strong.block (when plg (str (:name plg) " / ")) (:name opt)]
[:small.opacity-50.italic (:description opt)]]
[:small.flex-shrink-0.flex.items-center.opacity-10
(if current-selected (svg/check 28))]]))
themes)])))
(rum/defc unpacked-plugin-loader
[unpacked-pkg-path]
(rum/use-effect!
(fn []
(let [err-handle
(fn [^js e]
(case (keyword (aget e "name"))
:IllegalPluginPackageError
(notification/show! "Illegal Logseq plugin package." :error)
:ExistedImportedPluginPackageError
(notification/show! "Existed Imported plugin package." :error)
:default)
(plugin-handler/reset-unpacked-state))
reg-handle #(plugin-handler/reset-unpacked-state)]
(when unpacked-pkg-path
(doto js/LSPluginCore
(.once "error" err-handle)
(.once "registered" reg-handle)
(.register (bean/->js {:url unpacked-pkg-path}))))
#(doto js/LSPluginCore
(.off "error" err-handle)
(.off "registered" reg-handle))))
[unpacked-pkg-path])
(when unpacked-pkg-path
[:strong.inline-flex.px-3 "Loading ..."]))
(rum/defc category-tabs
[t category on-action]
[:div.secondary-tabs.categories
(ui/button
[:span (ui/icon "puzzle") (t :plugins)]
:intent "logseq"
:on-click #(on-action :plugins)
:class (if (= category :plugins) "active" ""))
(ui/button
[:span (ui/icon "palette") (t :themes)]
:intent "logseq"
:on-click #(on-action :themes)
:class (if (= category :themes) "active" ""))])
(rum/defc local-markdown-display
< rum/reactive
[]
(let [[content item] (state/sub :plugin/active-readme)]
[:div.cp__plugins-details
{:on-click (fn [^js/MouseEvent e]
(when-let [target (.-target e)]
(when (and (= (string/lower-case (.-nodeName target)) "a")
(not (string/blank? (. target getAttribute "href"))))
(js/apis.openExternal (. target getAttribute "href"))
(.preventDefault e))))}
(when-let [repo (:repository item)]
(when-let [repo (if (string? repo) repo (:url repo))]
[:div.p-4.rounded-md.bg-base-3
[:strong [:a.flex.items-center {:target "_blank" :href repo}
[:span.mr-1 (svg/github {:width 25 :height 25})] repo]]]))
[:div.p-1.bg-transparent.border-none.ls-block
{:style {:min-height "60vw"
:max-width 900}
:dangerouslySetInnerHTML {:__html content}}]]))
(rum/defc remote-readme-display
[repo content]
(let [src (str "lsp://logseq.com/marketplace.html?repo=" repo)]
[:iframe.lsp-frame-readme {:src src}]))
(defn security-warning
[]
(ui/admonition
:warning
[:div.max-w-4xl
"Plugins can access your graph and your local files, issue network requests.
They can also cause data corruption or loss. We're working on proper access rules for your graphs.
Meanwhile, make sure you have regular backups of your graphs and only install the plugins when you can read and
understand the source code."]))
(rum/defc plugin-item-card < rum/static
[{:keys [id name title settings version url description author icon usf iir repo] :as item}
installing-or-updating? installed? stat]
(let [market? (and (not (nil? repo)) (nil? usf))
disabled (:disabled settings)
name (or title name "Untitled")]
(rum/with-context
[[t] i18n/*tongue-context*]
[:div.cp__plugins-item-card
{:class (util/classnames [{:market market?}])}
[:div.l.link-block
{:on-click #(plugin-handler/open-readme!
url item (if repo remote-readme-display local-markdown-display))}
(if (and icon (not (string/blank? icon)))
[:img.icon {:src (if market? (plugin-handler/pkg-asset id icon) icon)}]
svg/folder)
(when-not (or market? iir)
[:span.flex.justify-center.text-xs.text-red-500.pt-2 "unpacked"])]
[:div.r
[:h3.head.text-xl.font-bold.pt-1.5
[:span name]
(if (not market?) [:sup.inline-block.px-1.text-xs.opacity-50 version])]
[:div.desc.text-xs.opacity-70
[:p description]
;;[:small (js/JSON.stringify (bean/->js settings))]
]
[:div.flag
[:p.text-xs.pr-2.flex.justify-between
[:small author]
[:small {:on-click #(do
(notification/show! "Copied!" :success)
(util/copy-to-clipboard! id))}
(str "ID: " id)]]]
[:div.flag.is-top.opacity-50
(if repo
[:a.flex {:target "_blank"
:href (plugin-handler/gh-repo-url repo)}
(svg/github {:width 16 :height 16})])]
(if market?
;; market ctls
[:div.ctl
[:ul.l.flex.items-center
;; downloads
[:li.flex.text-sm.items-center.pr-3 (svg/star 16) [:span.pl-1 (:stargazers_count stat)]]
;; stars
(when-let [downloads (and stat (reduce (fn [a b] (+ a (get b 2))) 0 (:releases stat)))]
(if (and downloads (> downloads 0))
[:li.flex.text-sm.items-center.pr-3 (svg/cloud-down 16) [:span.pl-1 downloads]]))]
[:div.r.flex.items-center
[:a.btn
{:class (util/classnames [{:disabled (or installed? installing-or-updating?)
:installing installing-or-updating?}])
:on-click #(plugin-handler/install-marketplace-plugin item)}
(if installed?
(t :plugin/installed)
(if installing-or-updating?
[:span.flex.items-center [:small svg/loading]
(t :plugin/installing)]
(t :plugin/install)))]]]
;; installed ctls
[:div.ctl
[:div.l
[:div.de
[:strong (svg/settings)]
[:ul.menu-list
[:li {:on-click #(if usf (js/apis.openPath usf))} (t :plugin/open-settings)]
[:li {:on-click #(js/apis.openPath url)} (t :plugin/open-package)]
[:li {:on-click
#(let [confirm-fn
(ui/make-confirm-modal
{:title (str "Are you sure uninstall plugin [" name "] ?")
:on-confirm (fn [_ {:keys [close-fn]}]
(close-fn)
(plugin-handler/unregister-plugin id))})]
(state/set-modal! confirm-fn))}
(t :plugin/uninstall)]]]]
[:div.r.flex.items-center
(if (and (not iir) (not disabled))
[:a.btn
{:on-click #(js-invoke js/LSPluginCore "reload" id)}
(t :plugin/reload)])
(if iir
[:a.btn
{:class (util/classnames [{:disabled (or installing-or-updating?)
:updating installing-or-updating?}])
:on-click #(plugin-handler/update-marketplace-plugin
item (fn [e] (notification/show! e :error)))}
(if installing-or-updating?
(t :plugin/updating)
(t :plugin/update))])
(ui/toggle (not disabled)
(fn []
(js-invoke js/LSPluginCore (if disabled "enable" "disable") id)
(page-handler/init-commands!))
true)]])]])))
(rum/defcs marketplace-plugins
< rum/static rum/reactive
(rum/local false ::fetching)
(rum/local :plugins ::category)
(rum/local nil ::error)
{:did-mount (fn [s]
(reset! (::fetching s) true)
(reset! (::error s) nil)
(-> (plugin-handler/load-marketplace-plugins false)
(p/then #(plugin-handler/load-marketplace-stats false))
(p/catch #(do (js/console.error %) (reset! (::error s) %)))
(p/finally #(reset! (::fetching s) false)))
s)}
[state]
(let [pkgs (state/sub :plugin/marketplace-pkgs)
stats (state/sub :plugin/marketplace-stats)
installed-plugins (state/sub :plugin/installed-plugins)
installing (state/sub :plugin/installing)
online? (state/sub :network/online?)
*category (::category state)
*fetching (::fetching state)
*error (::error state)
filtered-pkgs (when (seq pkgs)
(if (= @*category :themes)
(filter #(:theme %) pkgs)
(filter #(not (:theme %)) pkgs)))]
(rum/with-context
[[t] i18n/*tongue-context*]
[:div.cp__plugins-marketplace
[:div.mb-4.flex.justify-between
[:div.flex.align-items
(category-tabs t @*category #(reset! *category %))]
[:div.flex.align-items
(ui/button
(t :plugin/contribute)
:href "https://github.com/logseq/marketplace"
:intent "logseq"
:target "_blank")
]]
(cond
(not online?)
[:p.flex.justify-center.pt-20.opacity-50
(svg/offline 30)]
@*fetching
[:p.flex.justify-center.pt-20
svg/loading]
@*error
[:p.flex.justify-center.pt-20.opacity-50
"Remote error: " (.-message @*error)]
:else
[:div.cp__plugins-marketplace-cnt
{:class (util/classnames [{:has-installing (boolean installing)}])}
[:div.cp__plugins-item-lists.grid-cols-1.md:grid-cols-2.lg:grid-cols-3
(for [item filtered-pkgs]
(rum/with-key
(let [pid (keyword (:id item))]
(plugin-item-card
item (and installing (= (keyword (:id installing)) pid))
(contains? installed-plugins pid)
(get stats pid)))
(:id item)))]])])
))
(rum/defcs installed-plugins
< rum/static rum/reactive
[state]
(let [installed-plugins (state/sub :plugin/installed-plugins)
updating (state/sub :plugin/installing)
selected-unpacked-pkg (state/sub :plugin/selected-unpacked-pkg)
sorted-plugins (->> (vals installed-plugins)
(reduce #(let [k (if (get-in %2 [:settings :disabled]) 1 0)]
(update %1 k conj %2)) [[] []])
(#(update % 0 (fn [it] (sort-by :iir it))))
(flatten))]
(rum/with-context
[[t] i18n/*tongue-context*]
[:div.cp__plugins-installed
[:div.mb-4.flex.items-center.justify-between
[:div.flex.align-items.secondary-tabs
(ui/tippy {:html [:div (t :plugin/unpacked-tips)]
:arrow true}
(ui/button
[:span (ui/icon "upload") (t :plugin/load-unpacked)]
:intent "logseq"
:on-click plugin-handler/load-unpacked-plugin))
(unpacked-plugin-loader selected-unpacked-pkg)]
[:div.flex.align-items
;;(ui/button
;; (t :plugin/open-preferences)
;; :intent "logseq"
;; :on-click (fn []
;; (p/let [root (plugin-handler/get-ls-dotdir-root)]
;; (js/apis.openPath (str root "/preferences.json")))))
(ui/button
(t :plugin/contribute)
:href "https://github.com/logseq/marketplace"
:intent "logseq"
:target "_blank")
]]
[:div.cp__plugins-item-lists.grid-cols-1.md:grid-cols-2.lg:grid-cols-3
(for [item sorted-plugins]
(rum/with-key
(let [pid (keyword (:id item))]
(plugin-item-card
item (and updating (= (keyword (:id updating)) pid))
true nil)) (:id item)))]])))
(defn open-select-theme!
[]
(state/set-modal! installed-themes))
(rum/defc hook-ui-slot
([type payload] (hook-ui-slot type payload nil))
([type payload opts]
(let [rs (util/rand-str 8)
id (str "slot__" rs)]
(rum/use-effect!
(fn []
(plugin-handler/hook-plugin-app type {:slot id :payload payload} nil)
#())
[id])
[:div.lsp-hook-ui-slot
(merge opts {:id id
:on-mouse-down (fn [e] (util/stop e))})])))
(rum/defc ui-item-renderer
[pid type {:keys [key template]}]
(let [*el (rum/use-ref nil)
uni #(str "injected-ui-item-" %)
^js pl (js/LSPluginCore.registeredPlugins.get (name pid))]
(rum/use-effect!
(fn []
(when-let [^js el (rum/deref *el)]
(js/LSPlugin.pluginHelpers.setupInjectedUI.call
pl #js {:slot (.-id el) :key key :template template} #js {})))
[])
(if-not (nil? pl)
[:div {:id (uni (str (name key) "-" (name pid)))
:class (uni (name type))
:ref *el}]
[:span])))
(rum/defcs hook-ui-items < rum/reactive
"type
- :toolbar
- :pagebar
"
[state type]
(when (state/sub [:plugin/installed-ui-items])
(let [items (state/get-plugins-ui-items-with-type type)]
(when (seq items)
[:div {:class (str "ui-items-container")
:data-type (name type)}
(for [[_ {:keys [key template] :as opts} pid] items]
(rum/with-key (ui-item-renderer pid type opts) key))]))))
(rum/defc plugins-page
[]
(let [[active set-active!] (rum/use-state :installed)
market? (= active :marketplace)]
(rum/with-context
[[t] i18n/*tongue-context*]
[:div.cp__plugins-page
[:h1 (t :plugins)]
(security-warning)
[:hr]
[:div.tabs.flex.items-center.justify-center
[:div.tabs-inner.flex.items-center
(ui/button [:span.it (t :plugin/installed)]
:on-click #(set-active! :installed)
:intent "logseq" :class (if-not market? "active" ""))
(ui/button [:span.mk (svg/apps 16) (t :plugin/marketplace)]
:on-click #(set-active! :marketplace)
:intent "logseq" :class (if market? "active" ""))]]
[:div.panels
(if market?
(marketplace-plugins)
(installed-plugins))]])))
(rum/defc custom-js-installer
[{:keys [t current-repo db-restoring? nfs-granted?] :as props}]
(rum/use-effect!
(fn []
(when (and (not db-restoring?)
(or (not util/nfs?) nfs-granted?))
(ui-handler/exec-js-if-exists-&-allowed! t)))
[current-repo db-restoring? nfs-granted?])
nil)