Browse Source

Enhance/Settings UI of plugins (#4035)

more settings types & polish releated ui
Charlie 4 years ago
parent
commit
24b0236b7d

+ 8 - 6
libs/package.json

@@ -13,12 +13,14 @@
     "build": "tsc && rm dist/*.js && npm run build:user"
   },
   "dependencies": {
-    "csstype": "^3.0.8",
-    "debug": "^4.3.1",
-    "dompurify": "^2.2.7",
-    "eventemitter3": "^4.0.7",
-    "path": "^0.12.7",
-    "snake-case": "^3.0.4"
+    "csstype": "3.0.8",
+    "debug": "4.3.1",
+    "dompurify": "2.3.1",
+    "eventemitter3": "4.0.7",
+    "fast-deep-equal": "3.1.3",
+    "lodash-es": "4.17.21",
+    "path": "0.12.7",
+    "snake-case": "3.0.4"
   },
   "devDependencies": {
     "@types/debug": "^4.1.5",

+ 4 - 2
libs/src/LSPlugin.caller.ts

@@ -213,8 +213,8 @@ class LSPluginCaller extends EventEmitter {
     cnt.id = id
 
     // TODO: apply any container layout data
-    {
-      const mainLayoutInfo = this._pluginLocal.settings.get('layout')?.[0]
+    try {
+      const mainLayoutInfo = (await this._pluginLocal._loadLayoutsData())?.$$0
       if (mainLayoutInfo) {
         cnt.dataset.inited_layout = 'true'
         const { width, height, left, top } = mainLayoutInfo
@@ -223,6 +223,8 @@ class LSPluginCaller extends EventEmitter {
           left: left + 'px', top: top + 'px'
         })
       }
+    } catch (e) {
+      console.error("[Restore Layout Error]", e)
     }
 
     document.body.appendChild(cnt)

+ 74 - 12
libs/src/LSPlugin.core.ts

@@ -11,7 +11,8 @@ import {
   getSDKPathRoot,
   PROTOCOL_FILE, URL_LSP,
   safetyPathJoin,
-  path, safetyPathNormalize
+  path, safetyPathNormalize,
+  mergeSettingsWithSchema, IS_DEV
 } from './helpers'
 import * as pluginHelpers from './helpers'
 import Debug from 'debug'
@@ -19,11 +20,12 @@ import {
   LSPluginCaller,
   LSPMSG_READY, LSPMSG_SYNC,
   LSPMSG, LSPMSG_SETTINGS,
-  LSPMSG_ERROR_TAG, LSPMSG_BEFORE_UNLOAD, AWAIT_LSPMSGFn
+  LSPMSG_ERROR_TAG, LSPMSG_BEFORE_UNLOAD,
+  AWAIT_LSPMSGFn
 } from './LSPlugin.caller'
 import {
   ILSPluginThemeManager,
-  LSPluginPkgConfig,
+  LSPluginPkgConfig, SettingSchemaDesc,
   StyleOptions,
   StyleString,
   ThemeOptions, UIContainerAttrs,
@@ -48,12 +50,15 @@ type LSPluginCoreOptions = {
 /**
  * User settings
  */
-class PluginSettings extends EventEmitter<'change'> {
+class PluginSettings extends EventEmitter<'change' | 'reset'> {
   private _settings: Record<string, any> = {
     disabled: false
   }
 
-  constructor (private _userPluginSettings: any) {
+  constructor (
+    private _userPluginSettings: any,
+    private _schema?: Array<SettingSchemaDesc>
+  ) {
     super()
 
     Object.assign(this._settings, _userPluginSettings)
@@ -87,6 +92,28 @@ class PluginSettings extends EventEmitter<'change'> {
     return this._settings
   }
 
+  setSchema (schema: Array<SettingSchemaDesc>, syncSettings?: boolean) {
+    this._schema = schema
+
+    if (syncSettings) {
+      const _settings = this._settings
+      this._settings = mergeSettingsWithSchema(_settings, schema)
+      this.emit('change', this._settings, _settings)
+    }
+  }
+
+  reset () {
+    const o = this.settings
+    const val = {}
+
+    if (this._schema) {
+      // TODO: generated by schema
+    }
+
+    this.settings = val
+    this.emit('reset', val, o)
+  }
+
   toJSON () {
     return this._settings
   }
@@ -149,6 +176,7 @@ type PluginLocalOptions = {
   name: string
   version: string
   mode: 'shadow' | 'iframe'
+  settingsSchema?: Array<SettingSchemaDesc>
   settings?: PluginSettings
   logger?: PluginLogger
   effect?: boolean
@@ -173,10 +201,21 @@ enum PluginLocalLoadStatus {
 function initUserSettingsHandlers (pluginLocal: PluginLocal) {
   const _ = (label: string): any => `settings:${label}`
 
+  pluginLocal.on(_('schema'), ({ schema, isSync }: { schema: Array<SettingSchemaDesc>, isSync?: boolean }) => {
+    pluginLocal.settingsSchema = schema
+    pluginLocal.settings?.setSchema(schema, isSync)
+  })
+
   pluginLocal.on(_('update'), (attrs) => {
     if (!attrs) return
     pluginLocal.settings?.set(attrs)
   })
+
+  pluginLocal.on(_('visible:changed'), (payload) => {
+    const visible = payload?.visible
+    invokeHostExportedApi('set_focused_settings',
+      visible ? pluginLocal.id : null)
+  })
 }
 
 function initMainUIHandlers (pluginLocal: PluginLocal) {
@@ -416,7 +455,7 @@ class PluginLocal
           // Enable plugin
           const [, freshSettings] = await loadFreshSettings()
           freshSettings.disabled = false
-          a = deepMerge(a, freshSettings)
+          a = Object.assign(a, freshSettings)
           settings.settings = a
           await this.load()
         }
@@ -425,7 +464,7 @@ class PluginLocal
           // Disable plugin
           const [, freshSettings] = await loadFreshSettings()
           freshSettings.disabled = true
-          a = deepMerge(a, freshSettings)
+          a = Object.assign(a, freshSettings)
           await this.unload()
         }
 
@@ -571,6 +610,7 @@ class PluginLocal
       dirPathInstalled = this._localRoot.replace(this.dotPluginsRoot, '')
       dirPathInstalled = path.join(DIR_PLUGINS, dirPathInstalled)
     }
+    let tag = (new Date()).getDay()
     let sdkPathRoot = await getSDKPathRoot()
     let entryPath = await invokeHostExportedApi(
       tmp_file_method,
@@ -580,7 +620,10 @@ class PluginLocal
   <head>
     <meta charset="UTF-8">
     <title>logseq plugin entry</title>
-    <script src="${sdkPathRoot}/lsplugin.user.js"></script>
+    ${IS_DEV ?
+        `<script src="${sdkPathRoot}/lsplugin.user.js?v=${tag}"></script>` :
+        `<script src="https://cdn.jsdelivr.net/npm/@logseq/libs/dist/lsplugin.user.min.js?v=${tag}"></script>`}
+    
   </head>
   <body>
   <div id="app"></div>
@@ -613,10 +656,21 @@ class PluginLocal
     })
   }
 
-  _persistMainUILayoutData (e: { width: number, height: number, left: number, top: number }) {
-    const layouts = this.settings.get('layouts') || []
-    layouts[0] = e
-    this.settings.set('layout', layouts)
+  async _loadLayoutsData (): Promise<Record<string, any>> {
+    const key = this.id + '_layouts'
+    const [, layouts] = await invokeHostExportedApi('load_plugin_user_settings', key)
+    return layouts || {}
+  }
+
+  async _saveLayoutsData (data) {
+    const key = this.id + '_layouts'
+    await invokeHostExportedApi('save_plugin_user_settings', key, data)
+  }
+
+  async _persistMainUILayoutData (e: { width: number, height: number, left: number, top: number }) {
+    const layouts = await this._loadLayoutsData()
+    layouts.$$0 = e
+    await this._saveLayoutsData(layouts)
   }
 
   _setupDraggableContainer (
@@ -890,6 +944,14 @@ class PluginLocal
     return this.options.settings
   }
 
+  set settingsSchema (schema: Array<SettingSchemaDesc>) {
+    this.options.settingsSchema = schema
+  }
+
+  get settingsSchema () {
+    return this.options.settingsSchema
+  }
+
   get logger () {
     return this.options.logger
   }

+ 19 - 0
libs/src/LSPlugin.ts

@@ -185,6 +185,17 @@ export type SimpleCommandKeybinding = {
   mac?: string // special for Mac OS
 }
 
+export type SettingSchemaDesc = {
+  key: string
+  type: 'string' | 'number' | 'boolean' | 'enum' | 'object'
+  default: string | number | boolean | Array<any> | object | null
+  title: string
+  description: string // support markdown
+  inputAs?: 'color' | 'date' | 'datetime-local' | 'range'
+  enumChoices?: Array<string>
+  enumPicker?: 'select' | 'radio' | 'checkbox' // default: select
+}
+
 export type ExternalCommandType =
   'logseq.command/run' |
   'logseq.editor/cycle-todo' |
@@ -635,8 +646,16 @@ export interface ILSPluginUser extends EventEmitter<LSPluginUserEvents> {
    */
   provideUI (ui: UIOptions): this
 
+  useSettingsSchema (schemas: Array<SettingSchemaDesc>): this
+
   updateSettings (attrs: Record<string, any>): void
 
+  onSettingsChanged<T = any> (cb: (a: T, b: T) => void): IUserOffHook
+
+  showSettingsUI (): void
+
+  hideSettingsUI (): void
+
   setMainUIAttrs (attrs: Record<string, any>): void
 
   /**

+ 38 - 7
libs/src/LSPlugin.user.ts

@@ -1,4 +1,4 @@
-import { deepMerge, safetyPathJoin } from './helpers'
+import { deepMerge, mergeSettingsWithSchema, safetyPathJoin } from './helpers'
 import { LSPluginCaller } from './LSPlugin.caller'
 import {
   IAppProxy, IDBProxy,
@@ -12,7 +12,7 @@ import {
   ThemeOptions,
   UIOptions, IHookEvent, BlockIdentity,
   BlockPageName,
-  UIContainerAttrs, SimpleCommandCallback, SimpleCommandKeybinding
+  UIContainerAttrs, SimpleCommandCallback, SimpleCommandKeybinding, SettingSchemaDesc, IUserOffHook
 } from './LSPlugin'
 import Debug from 'debug'
 import * as CSS from 'csstype'
@@ -220,9 +220,7 @@ const KEY_MAIN_UI = 0
  * @public
  */
 export class LSPluginUser extends EventEmitter<LSPluginUserEvents> implements ILSPluginUser {
-  /**
-   * @private
-   */
+  private _settingsSchema?: Array<SettingSchemaDesc>
   private _connected: boolean = false
 
   /**
@@ -285,7 +283,6 @@ export class LSPluginUser extends EventEmitter<LSPluginUserEvents> implements IL
     if (this._connected) return
 
     try {
-
       if (typeof model === 'function') {
         callback = model
         model = {}
@@ -293,9 +290,18 @@ export class LSPluginUser extends EventEmitter<LSPluginUserEvents> implements IL
 
       let baseInfo = await this._caller.connectToParent(model)
 
+      this._connected = true
+
       baseInfo = deepMerge(this._baseInfo, baseInfo)
 
-      this._connected = true
+      if (this._settingsSchema) {
+        baseInfo.settings = mergeSettingsWithSchema(
+          baseInfo.settings, this._settingsSchema
+        )
+
+        // TODO: sync host settings schema
+        await this.useSettingsSchema(this._settingsSchema)
+      }
 
       if (baseInfo?.id) {
         this._caller.debugTag = `#${baseInfo.id} [${baseInfo.name}]`
@@ -338,11 +344,36 @@ export class LSPluginUser extends EventEmitter<LSPluginUserEvents> implements IL
     return this
   }
 
+  useSettingsSchema (schema: Array<SettingSchemaDesc>) {
+    if (this.connected) {
+      this.caller.call('settings:schema', {
+        schema, isSync: true
+      })
+    }
+
+    this._settingsSchema = schema
+    return this
+  }
+
   updateSettings (attrs: Record<string, any>) {
     this.caller.call('settings:update', attrs)
     // TODO: update associated baseInfo settings
   }
 
+  onSettingsChanged<T = any> (cb: (a: T, b: T) => void): IUserOffHook {
+    const type = 'settings:changed'
+    this.on(type, cb)
+    return () => this.off(type, cb)
+  }
+
+  showSettingsUI () {
+    this.caller.call('settings:visible:changed', { visible: true })
+  }
+
+  hideSettingsUI () {
+    this.caller.call('settings:visible:changed', { visible: false })
+  }
+
   setMainUIAttrs (attrs: Partial<UIContainerAttrs>): void {
     this.caller.call('main-ui:attrs', attrs)
   }

+ 17 - 43
libs/src/helpers.ts

@@ -1,8 +1,9 @@
-import { StyleString, UIOptions } from './LSPlugin'
+import { SettingSchemaDesc, StyleString, UIOptions } from './LSPlugin'
 import { PluginLocal } from './LSPlugin.core'
 import { snakeCase } from 'snake-case'
 import * as nodePath from 'path'
 import DOMPurify from 'dompurify'
+import { merge } from 'lodash-es'
 
 interface IObject {
   [key: string]: any;
@@ -48,47 +49,7 @@ export function isObject (item: any) {
   return (item === Object(item) && !Array.isArray(item))
 }
 
-export function deepMerge (
-  target: IObject,
-  ...sources: Array<IObject>
-) {
-  // return the target if no sources passed
-  if (!sources.length) {
-    return target
-  }
-
-  const result: IObject = target
-
-  if (isObject(result)) {
-    const len: number = sources.length
-
-    for (let i = 0; i < len; i += 1) {
-      const elm: any = sources[i]
-
-      if (isObject(elm)) {
-        for (const key in elm) {
-          if (elm.hasOwnProperty(key)) {
-            if (isObject(elm[key])) {
-              if (!result[key] || !isObject(result[key])) {
-                result[key] = {}
-              }
-              deepMerge(result[key], elm[key])
-            } else {
-              if (Array.isArray(result[key]) && Array.isArray(elm[key])) {
-                // concatenate the two arrays and remove any duplicate primitive values
-                result[key] = Array.from(new Set(result[key].concat(elm[key])))
-              } else {
-                result[key] = elm[key]
-              }
-            }
-          }
-        }
-      }
-    }
-  }
-
-  return result
-}
+export const deepMerge = merge
 
 export function genID () {
   // Math.random should be unique because of its seeding algorithm.
@@ -244,7 +205,6 @@ export function setupInjectedUI (
 
   const pl = this
 
-
   if ('slot' in ui) {
     slot = ui.slot
     selector = `#${slot}`
@@ -426,3 +386,17 @@ export function setupInjectedTheme (url?: string) {
     injectedThemeEffect = null
   })
 }
+
+export function mergeSettingsWithSchema (
+  settings: Record<string, any>,
+  schema: Array<SettingSchemaDesc>) {
+  const defaults = (schema || []).reduce((a, b) => {
+    if ('default' in b) {
+      a[b.key] = b.default
+    }
+    return a
+  }, {})
+
+  // shadow copy
+  return Object.assign(defaults, settings)
+}

+ 15 - 10
libs/yarn.lock

@@ -365,19 +365,19 @@ cross-spawn@^7.0.3:
     shebang-command "^2.0.0"
     which "^2.0.1"
 
-csstype@^3.0.8:
[email protected]:
   version "3.0.8"
   resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.8.tgz#d2266a792729fb227cd216fb572f43728e1ad340"
   integrity sha512-jXKhWqXPmlUeoQnF/EhTtTl4C9SnrxSH/jZUih3jmO6lBKr99rP3/+FmrMj4EFpOXzMtXHAZkd3x0E6h6Fgflw==
 
-debug@^4.3.1:
-  version "4.3.2"
-  resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b"
-  integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==
[email protected]:
+  version "4.3.1"
+  resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee"
+  integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==
   dependencies:
     ms "2.1.2"
 
-dompurify@^2.2.7:
+dompurify@2.3.1:
   version "2.3.1"
   resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.3.1.tgz#a47059ca21fd1212d3c8f71fdea6943b8bfbdf6a"
   integrity sha512-xGWt+NHAQS+4tpgbOAI08yxW0Pr256Gu/FNE2frZVTbgrBUn8M7tz7/ktS/LZ2MHeGqz6topj0/xY+y8R5FBFw==
@@ -469,7 +469,7 @@ estraverse@^5.2.0:
   resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.2.0.tgz#307df42547e6cc7324d3cf03c155d5cdb8c53880"
   integrity sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==
 
-eventemitter3@^4.0.7:
[email protected]:
   version "4.0.7"
   resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f"
   integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==
@@ -494,7 +494,7 @@ execa@^5.0.0:
     signal-exit "^3.0.3"
     strip-final-newline "^2.0.0"
 
-fast-deep-equal@^3.1.1:
+[email protected], fast-deep-equal@^3.1.1:
   version "3.1.3"
   resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
   integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
@@ -682,6 +682,11 @@ locate-path@^5.0.0:
   dependencies:
     p-locate "^4.1.0"
 
+lodash-es@^4.17.21:
+  version "4.17.21"
+  resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee"
+  integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==
+
 lodash@^4.17.20:
   version "4.17.21"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
@@ -832,7 +837,7 @@ path-parse@^1.0.6:
   resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
   integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
 
-path@^0.12.7:
[email protected]:
   version "0.12.7"
   resolved "https://registry.yarnpkg.com/path/-/path-0.12.7.tgz#d4dc2a506c4ce2197eb481ebfcd5b36c0140b10f"
   integrity sha1-1NwqUGxM4hl+tIHr/NWzbAFAsQ8=
@@ -985,7 +990,7 @@ sirv@^1.0.7:
     mime "^2.3.1"
     totalist "^1.0.0"
 
-snake-case@^3.0.4:
[email protected]:
   version "3.0.4"
   resolved "https://registry.yarnpkg.com/snake-case/-/snake-case-3.0.4.tgz#4f2bbd568e9935abdfd593f34c691dadb49c452c"
   integrity sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==

File diff suppressed because it is too large
+ 0 - 0
resources/js/lsplugin.core.js


+ 9 - 7
src/electron/electron/handler.cljs

@@ -46,13 +46,15 @@
   (readdir dir))
 
 (defmethod handle :unlink [_window [_ repo path]]
-  (let [file-name (-> (string/replace path (str repo "/") "")
-                      (string/replace "/" "_")
-                      (string/replace "\\" "_"))
-        recycle-dir (str repo "/logseq/.recycle")
-        _ (fs-extra/ensureDirSync recycle-dir)
-        new-path (str recycle-dir "/" file-name)]
-    (fs/renameSync path new-path)))
+  (if (plugin/dotdir-file? path)
+    (fs/unlinkSync path)
+    (let [file-name   (-> (string/replace path (str repo "/") "")
+                        (string/replace "/" "_")
+                        (string/replace "\\" "_"))
+          recycle-dir (str repo "/logseq/.recycle")
+          _           (fs-extra/ensureDirSync recycle-dir)
+          new-path    (str recycle-dir "/" file-name)]
+      (fs/renameSync path new-path))))
 
 (defn backup-file
   [repo path content]

+ 4 - 0
src/electron/electron/plugin.cljs

@@ -18,6 +18,10 @@
               (.. win -webContents
                   (send (name type) (bean/->js payload))))))
 
+(defn dotdir-file?
+  [file]
+  (and file (string/starts-with? (path/normalize file) cfgs/dot-root)))
+
 ;; Get a release by tag name: /repos/{owner}/{repo}/releases/tags/{tag}
 ;; Get the latest release: /repos/{owner}/{repo}/releases/latest
 ;; Zipball https://api.github.com/repos/{owner}/{repo}/zipball

+ 225 - 171
src/main/frontend/components/plugins.cljs

@@ -10,6 +10,7 @@
             [frontend.mixins :as mixins]
             [promesa.core :as p]
             [frontend.components.svg :as svg]
+            [frontend.components.plugins-settings :as plugins-settings]
             [frontend.handler.notification :as notification]
             [frontend.handler.plugin :as plugin-handler]
             [frontend.handler.page :as page-handler]
@@ -54,24 +55,24 @@
      {:tab-index -1}
      [:h1.mb-4.text-2xl.p-1 (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      (str idx (:url opt))
-            :title    (when 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/close-modal!))}
-           [:section
-            [:strong.block
-             [:small.opacity-60 (str (or (:name plg) "Logseq") " • ")]
-             (:name opt)]]
-           [:small.flex-shrink-0.flex.items-center.opacity-10
-            (when current-selected (ui/icon "check"))]]))
-      themes)]))
+       (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      (str idx (:url opt))
+             :title    (when 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/close-modal!))}
+            [:section
+             [:strong.block
+              [:small.opacity-60 (str (or (:name plg) "Logseq") " • ")]
+              (:name opt)]]
+            [:small.flex-shrink-0.flex.items-center.opacity-10
+             (when current-selected (ui/icon "check"))]]))
+       themes)]))
 
 (rum/defc unpacked-plugin-loader
   [unpacked-pkg-path]
@@ -152,25 +153,110 @@
       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 ^:large-vars/cleanup-todo plugin-item-card < rum/static
-  [{:keys [id name title settings version url description author icon usf iir repo sponsors] :as item}
-   market? *search-key has-other-pending?
+(rum/defc card-ctls-of-market < rum/static
+  [item stat installed? installing-or-updating?]
+  [:div.ctl
+   [:ul.l.flex.items-center
+    ;; stars
+    [:li.flex.text-sm.items-center.pr-3
+     (svg/star 16) [:span.pl-1 (:stargazers_count stat)]]
+
+    ;; downloads
+    (when-let [downloads (and stat (:total_downloads stat))]
+      (when (and downloads (> downloads 0))
+        [:li.flex.text-sm.items-center.pr-3
+         (svg/cloud-down 16) [:span.pl-1 downloads]]))]
+
+   [:div.r.flex.items-center
+
+    [:a.btn
+     {:class    (util/classnames [{:disabled   (or installed? installing-or-updating?)
+                                   :installing installing-or-updating?}])
+      :on-click #(plugin-handler/install-marketplace-plugin item)}
+     (if installed?
+       (t :plugin/installed)
+       (if installing-or-updating?
+         [:span.flex.items-center [:small svg/loading]
+          (t :plugin/installing)]
+         (t :plugin/install)))]]])
+
+(rum/defc card-ctls-of-installed < rum/static
+  [id name url sponsors unpacked? disabled?
+   installing-or-updating? has-other-pending?
+   new-version item]
+  [:div.ctl
+   [:div.l
+    [:div.de
+     [:strong (ui/icon "settings")]
+     [:ul.menu-list
+      [:li {:on-click #(plugin-handler/open-plugin-settings! id false)} (t :plugin/open-settings)]
+      [:li {:on-click #(js/apis.openPath url)} (t :plugin/open-package)]
+      [:li {:on-click
+            #(let [confirm-fn
+                   (ui/make-confirm-modal
+                     {:title      (t :plugin/delete-alert name)
+                      :on-confirm (fn [_ {:keys [close-fn]}]
+                                    (close-fn)
+                                    (plugin-handler/unregister-plugin id))})]
+               (state/set-sub-modal! confirm-fn {:center? true}))}
+       (t :plugin/uninstall)]]]
+
+    (when (seq sponsors)
+      [:div.de.sponsors
+       [:strong (ui/icon "coffee")]
+       [:ul.menu-list
+        (for [link sponsors]
+          [:li [:a {:href link :target "_blank"}
+                [:span.flex.items-center link (ui/icon "external-link")]]])]])
+    ]
+
+   [:div.r.flex.items-center
+    (when (and unpacked? (not disabled?))
+      [:a.btn
+       {:on-click #(js-invoke js/LSPluginCore "reload" id)}
+       (t :plugin/reload)])
+
+    (when (not unpacked?)
+      [:div.updates-actions
+       [:a.btn
+        {:class    (util/classnames [{:disabled installing-or-updating?}])
+         :on-click #(when-not has-other-pending?
+                      (plugin-handler/check-or-update-marketplace-plugin
+                        (assoc item :only-check (not new-version))
+                        (fn [e] (notification/show! e :error))))}
+
+        (if installing-or-updating?
+          (t :plugin/updating)
+          (if new-version
+            (str (t :plugin/update) " 👉 " new-version)
+            (t :plugin/check-update))
+          )]])
+
+    (ui/toggle (not disabled?)
+               (fn []
+                 (js-invoke js/LSPluginCore (if disabled? "enable" "disable") id)
+                 (page-handler/init-commands!))
+               true)]])
+
+(rum/defc plugin-item-card < rum/static
+  [t {:keys [id name title version url description author icon iir repo sponsors] :as item}
+   disabled? market? *search-key has-other-pending?
    installing-or-updating? installed? stat coming-update]
 
-  (let [disabled (:disabled settings)
-        name (or title name "Untitled")
+  (let [name (or title name "Untitled")
         unpacked? (not iir)
         new-version (state/coming-update-new-version? coming-update)]
     [:div.cp__plugins-item-card
-     {:class (util/classnames
-              [{:market          market?
-                :installed       installed?
-                :updating        installing-or-updating?
-                :has-new-version new-version}])}
+     {:key   (str "lsp-card-" id)
+      :class (util/classnames
+               [{:market          market?
+                 :installed       installed?
+                 :updating        installing-or-updating?
+                 :has-new-version new-version}])}
 
      [:div.l.link-block
       {:on-click #(plugin-handler/open-readme!
-                   url item (if repo remote-readme-display local-markdown-display))}
+                    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)
@@ -209,89 +295,47 @@
 
       (if market?
         ;; market ctls
-        [:div.ctl
-         [:ul.l.flex.items-center
-          ;; stars
-          [:li.flex.text-sm.items-center.pr-3
-           (svg/star 16) [:span.pl-1 (:stargazers_count stat)]]
-
-          ;; downloads
-          (when-let [downloads (and stat (:total_downloads stat))]
-            (when (and downloads (> downloads 0))
-              [:li.flex.text-sm.items-center.pr-3
-               (svg/cloud-down 16) [:span.pl-1 downloads]]))]
-
-         [:div.r.flex.items-center
-
-          [:a.btn
-           {:class    (util/classnames [{:disabled   (or installed? installing-or-updating?)
-                                         :installing installing-or-updating?}])
-            :on-click #(plugin-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)))]]]
+        (card-ctls-of-market item stat installed? installing-or-updating?)
 
         ;; installed ctls
-        [:div.ctl
-         [:div.l
-          [:div.de
-           [:strong (ui/icon "settings")]
-           [:ul.menu-list
-            [:li {:on-click #(when 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      (t :plugin/delete-alert name)
-                           :on-confirm (fn [_ {:keys [close-fn]}]
-                                         (close-fn)
-                                         (plugin-handler/unregister-plugin id))})]
-                     (state/set-sub-modal! confirm-fn {:center? true}))}
-             (t :plugin/uninstall)]]]
-
-          (when (seq sponsors)
-            [:div.de.sponsors
-             [:strong (ui/icon "coffee")]
-             [:ul.menu-list
-              (for [link sponsors]
-                [:li [:a {:href link :target "_blank"}
-                      [:span.flex.items-center link (ui/icon "external-link")]]])]])
-          ]
-
-         [:div.r.flex.items-center
-          (when (and unpacked? (not disabled))
-            [:a.btn
-             {:on-click #(js-invoke js/LSPluginCore "reload" id)}
-             (t :plugin/reload)])
-
-          (when (not unpacked?)
-            [:div.updates-actions
-             [:a.btn
-              {:class    (util/classnames [{:disabled installing-or-updating?}])
-               :on-click #(when-not has-other-pending?
-                            (plugin-handler/check-or-update-marketplace-plugin
-                             (assoc item :only-check (not new-version))
-                             (fn [e] (notification/show! e :error))))}
-
-              (if installing-or-updating?
-                (t :plugin/updating)
-                (if new-version
-                  (str (t :plugin/update) " 👉 " new-version)
-                  (t :plugin/check-update))
-                )]])
-
-          (ui/toggle (not disabled)
-                     (fn []
-                       (js-invoke js/LSPluginCore (if disabled "enable" "disable") id)
-                       (page-handler/init-commands!))
-                     true)]])]]))
-
-(rum/defc ^:large-vars/cleanup-todo panel-control-tabs
-  < rum/static
-  [t search-key *search-key category *category
+        (card-ctls-of-installed
+          id name url sponsors unpacked? disabled?
+          installing-or-updating? has-other-pending? new-version item))]]))
+
+(rum/defc panel-tab-search < rum/static
+  [search-key *search-key *search-ref]
+  [:div.search-ctls
+   [:small.absolute.s1
+    (ui/icon "search")]
+   (when-not (string/blank? search-key)
+     [:small.absolute.s2
+      {:on-click #(when-let [^js target (rum/deref *search-ref)]
+                    (reset! *search-key nil)
+                    (.focus target))}
+      (ui/icon "x")])
+   [:input.form-input.is-small
+    {:placeholder "Search plugins"
+     :ref         *search-ref
+     :on-key-down (fn [^js e]
+                    (when (= 27 (.-keyCode e))
+                      (when-not (string/blank? search-key)
+                        (util/stop e)
+                        (reset! *search-key nil))))
+     :on-change   #(let [^js target (.-target %)]
+                     (reset! *search-key (util/trim-safe (.-value target))))
+     :value       (or search-key "")}]])
+
+(rum/defc panel-tab-developer
+  []
+  (ui/button
+    (t :plugin/contribute)
+    :href "https://github.com/logseq/marketplace"
+    :class "contribute"
+    :intent "logseq"
+    :target "_blank"))
+
+(rum/defc panel-control-tabs < rum/static
+  [search-key *search-key category *category
    sort-or-filter-by *sort-or-filter-by selected-unpacked-pkg
    market? develop-mode? reload-market-fn]
 
@@ -313,36 +357,8 @@
          (unpacked-plugin-loader selected-unpacked-pkg)])]
 
      [:div.flex.items-center.r
-
-      ;;(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")))))
-
       ;; search
-      [:div.search-ctls
-       [:small.absolute.s1
-        (ui/icon "search")]
-       (when-not (string/blank? search-key)
-         [:small.absolute.s2
-          {:on-click #(when-let [^js target (rum/deref *search-ref)]
-                        (reset! *search-key nil)
-                        (.focus target))}
-          (ui/icon "x")])
-       [:input.form-input.is-small
-        {:placeholder "Search plugins"
-         :ref         *search-ref
-         :on-key-down (fn [^js e]
-                        (when (= 27 (.-keyCode e))
-                          (when-not (string/blank? search-key)
-                            (util/stop e)
-                            (reset! *search-key nil))))
-         :on-change   #(let [^js target (.-target %)]
-                         (reset! *search-key (util/trim-safe (.-value target))))
-         :value       (or search-key "")}]]
-
+      (panel-tab-search search-key *search-key *search-ref)
 
       ;; sorter & filter
       (ui/dropdown-with-links
@@ -414,13 +430,7 @@
         {})
 
       ;; developer
-      (ui/button
-        (t :plugin/contribute)
-        :href "https://github.com/logseq/marketplace"
-        :class "contribute"
-        :intent "logseq"
-        :target "_blank")
-      ]]))
+      (panel-tab-developer)]]))
 
 (rum/defcs marketplace-plugins
   < rum/static rum/reactive
@@ -482,11 +492,10 @@
     [:div.cp__plugins-marketplace
 
      (panel-control-tabs
-      t
-      @*search-key *search-key
-      @*category *category
-      @*sort-by *sort-by nil true
-      develop-mode? (::reload state))
+       @*search-key *search-key
+       @*category *category
+       @*sort-by *sort-by nil true
+       develop-mode? (::reload state))
 
      (cond
        (not online?)
@@ -509,10 +518,10 @@
            (rum/with-key
              (let [pid (keyword (:id item))
                    stat (:stat item)]
-               (plugin-item-card
-                item true *search-key installing
-                (and installing (= (keyword (:id installing)) pid))
-                (contains? installed-plugins pid) stat nil))
+               (plugin-item-card t item
+                                 (get-in item [:settings :disabled]) true *search-key installing
+                                 (and installing (= (keyword (:id installing)) pid))
+                                 (contains? installed-plugins pid) stat nil))
              (:id item)))]])]))
 
 (rum/defcs installed-plugins
@@ -521,7 +530,7 @@
     (rum/local :default ::filter-by)                        ;; default / enabled / disabled / unpacked / update-available
     (rum/local :plugins ::category)
   [state]
-  (let [installed-plugins (state/sub :plugin/installed-plugins)
+  (let [installed-plugins (state/sub [:plugin/installed-plugins])
         installed-plugins (vals installed-plugins)
         updating (state/sub :plugin/installing)
         develop-mode? (state/sub :ui/developer-mode?)
@@ -564,21 +573,21 @@
     [:div.cp__plugins-installed
 
      (panel-control-tabs
-      t
-      @*search-key *search-key
-      @*category *category
-      @*filter-by *filter-by
-      selected-unpacked-pkg
-      false develop-mode? nil)
+       @*search-key *search-key
+       @*category *category
+       @*filter-by *filter-by
+       selected-unpacked-pkg
+       false develop-mode? nil)
 
      [: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 false *search-key updating
-             (and updating (= (keyword (:id updating)) pid))
-             true nil (get coming-updates pid))) (:id item)))]]))
+            (plugin-item-card t item
+                              (get-in item [:settings :disabled]) false *search-key updating
+                              (and updating (= (keyword (:id updating)) pid))
+                              true nil (get coming-updates pid)))
+          (:id item)))]]))
 
 (rum/defcs waiting-coming-updates
   < rum/reactive
@@ -687,7 +696,7 @@
                                                         - :toolbar
                                                         - :pagebar
                                                      "
-  [state type]
+  [_state type]
   (when (state/sub [:plugin/installed-ui-items])
     (let [items (state/get-plugins-ui-items-with-type type)]
       (when (seq items)
@@ -718,18 +727,54 @@
      [: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" ""))
+                  :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" ""))]]
+                  :on-click #(set-active! :marketplace)
+                  :intent "logseq" :class (if market? "active" ""))]]
 
      [:div.panels
       (if market?
         (marketplace-plugins)
         (installed-plugins))]]))
 
+(rum/defcs focused-settings-content
+  < rum/reactive
+  [_state title]
+  (let [focused (state/sub :plugin/focused-settings)
+        nav? (state/sub :plugin/navs-settings?)
+        _ (state/sub :plugin/installed-plugins)]
+
+    [:div.cp__plugins-settings.cp__settings-main
+     [:header
+      [:h1.title (ui/icon "puzzle") (str " " (or title (t :settings-of-plugins)))]]
+
+     [:div.cp__settings-inner.md:flex
+      {:class (util/classnames [{:no-aside (not nav?)}])}
+      (when nav?
+        [:aside.md:w-64 {:style {:min-width "10rem"}}
+         (let [plugins (plugin-handler/get-enabled-plugins-if-setting-schema)]
+           [:ul
+            (for [{:keys [id name title icon]} plugins]
+              [:li
+               {:class (util/classnames [{:active (= id focused)}])}
+               [:a.flex.items-center
+                {:on-click #(do (state/set-state! :plugin/focused-settings id))}
+                (if (and icon (not (string/blank? icon)))
+                  [:img.icon {:src icon}]
+                  svg/folder)
+                [:strong.flex-1 (or title name)]]])])])
+
+      [:article
+       [:div.panel-wrap
+        (when-let [^js pl (and focused (plugin-handler/get-plugin-inst focused))]
+          (ui/catch-error
+            [:p.warning.text-lg.mt-5 "Settings schema Error!"]
+            (plugins-settings/settings-container
+              (bean/->clj (.-settingsSchema pl)) pl)))
+        ]]]]))
+
 (rum/defc custom-js-installer
   [{:keys [t current-repo db-restoring? nfs-granted?]}]
   (rum/use-effect!
@@ -752,3 +797,12 @@
     (fn [_close!]
       (waiting-coming-updates))
     {:center? true}))
+
+(defn open-focused-settings-modal!
+  [title]
+  (state/set-sub-modal!
+    (fn [_close!]
+      [:div.settings-modal.of-plugins
+       (focused-settings-content title)])
+    {:center? false
+     :id      "ls-focused-settings-modal"}))

+ 127 - 0
src/main/frontend/components/plugins.css

@@ -446,6 +446,133 @@
       }
     }
   }
+
+  &-settings {
+    &-inner {
+      position: relative;
+      padding: 10px 0 20px;
+
+      > .edit-file {
+        position: absolute;
+        top: 12px;
+        right: 8px;
+      }
+
+      .desc-item {
+        padding: 12px 12px 6px;
+
+        > h2 {
+          padding-bottom: 2px;
+          font-weight: 600;
+          font-size: 13px;
+          line-height: 1;
+          display: flex;
+          align-items: center;
+
+          code {
+            margin-right: 4px;
+            line-height: 1.2;
+          }
+        }
+
+        > .form-control {
+          display: flex;
+          align-items: center;
+          padding: 5px 2px;
+
+          > small {
+            user-select: none;
+          }
+        }
+
+        &.as-toggle {
+        }
+
+        &.as-input, &.as-enum, &.as-object {
+          > .form-control {
+            padding-top: 4px;
+            flex-direction: column;
+            align-items: flex-start;
+
+            small {
+              padding-bottom: 6px;
+              width: 100%;
+            }
+          }
+        }
+
+        &.as-enum {
+          > .form-control {
+            > .wrap {
+              flex-direction: column;
+              width: 100%;
+              margin-top: -3px;
+            }
+          }
+        }
+
+        &:hover {
+          background: var(--ls-tertiary-background-color);
+        }
+
+        .ui__radio-list, .ui__checkbox-list {
+          display: flex;
+          align-items: center;
+          padding-top: 3px;
+
+          label {
+            padding-right: 15px;
+            user-select: none;
+          }
+
+          input {
+            margin-right: 6px;
+          }
+        }
+
+        .form-input {
+          padding: 5px;
+          width: 50%;
+        }
+
+        .form-select {
+          width: 50%;
+          padding: 4px 4px 4px 7px !important;
+          margin: 5px 0 0;
+        }
+      }
+    }
+
+    aside {
+      max-height: 70vh;
+      overflow: auto;
+      margin-bottom: -17px;
+
+      ul {
+        img.icon {
+          height: 24px;
+          width: 24px;
+        }
+
+        li {
+          strong {
+            font-weight: 400;
+            overflow: hidden;
+            height: 22px;
+
+            text-overflow: ellipsis;
+            white-space: nowrap;
+          }
+        }
+      }
+    }
+
+    article .panel-wrap {
+      padding: 0;
+      min-width: 600px;
+      width: 100%;
+    }
+  }
 }
 
 .cp__themes {

+ 113 - 0
src/main/frontend/components/plugins_settings.cljs

@@ -0,0 +1,113 @@
+(ns frontend.components.plugins-settings
+  (:require [rum.core :as rum]
+            [frontend.util :as util]
+            [frontend.ui :as ui]
+            [frontend.handler.plugin :as plugin-handler]
+            [cljs-bean.core :as bean]))
+
+(rum/defc edit-settings-file
+  [pid {:keys [class]}]
+  [:a.text-sm.hover:underline
+   {:class    class
+    :on-click #(plugin-handler/open-settings-file-in-default-app! pid)}
+   "Edit settings.json"])
+
+(rum/defc render-item-input
+  [val {:keys [key type title default description inputAs]} update-setting!]
+
+  [:div.desc-item.as-input
+   [:h2 [:code key] (ui/icon "caret-right") [:strong title]]
+
+   [:label.form-control
+    [:small.pl-1.flex-1 description]
+
+    (let [input-as (util/safe-lower-case (or inputAs (name type)))
+          input-as (if (= input-as "string") :text (keyword input-as))]
+      [:input
+       {:class     (util/classnames [{:form-input (not (contains? #{:color :range} input-as))}])
+        :type      (name input-as)
+        :value     (or val default)
+        :on-change #(update-setting! key (util/evalue %))}])]])
+
+(rum/defc render-item-toggle
+  [val {:keys [key title description default]} update-setting!]
+
+  (let [val (if (boolean? val) val (boolean default))]
+    [:div.desc-item.as-toggle
+     [:h2 [:code key] (ui/icon "caret-right") [:strong title]]
+
+     [:label.form-control
+      (ui/checkbox {:checked   val
+                    :on-change #(update-setting! key (not val))})
+      [:small.pl-1.flex-1 description]]]))
+
+(rum/defc render-item-enum
+  [val {:keys [key title description default enumChoices enumPicker]} update-setting!]
+
+  (let [val (or val default)
+        vals (into #{} (if (sequential? val) val [val]))
+        options (map (fn [v] {:label    v :value v
+                              :selected (contains? vals v)}) enumChoices)
+        picker (keyword enumPicker)]
+    [:div.desc-item.as-enum
+     [:h2 [:code key] (ui/icon "caret-right") [:strong title]]
+
+     [:div.form-control
+      [(if (contains? #{:radio :checkbox} picker) :div.wrap :label.wrap)
+       [:small.pl-1 description]
+
+       (case picker
+         :radio (ui/radio-list options #(update-setting! key %) nil)
+         :checkbox (ui/checkbox-list options #(update-setting! key %) nil)
+         ;; select
+         (ui/select options #(update-setting! key %) nil))
+       ]]]))
+
+(rum/defc render-item-object
+  [_val {:keys [key title description _default]} pid]
+
+  [:div.desc-item.as-object
+   [:h2 [:code key] (ui/icon "caret-right") [:strong title]]
+
+   [:div.form-control
+    [:small.pl-1.flex-1 description]
+    [:div.pl-1 (edit-settings-file pid nil)]]])
+
+(rum/defc settings-container
+  [schema ^js pl]
+  (let [^js _settings (.-settings pl)
+        pid (.-id pl)
+        [settings, set-settings] (rum/use-state (bean/->clj (.toJSON _settings)))
+        update-setting! (fn [k v] (.set _settings (name k) (bean/->js v)))]
+
+    (rum/use-effect!
+      (fn []
+        (let [on-change (fn [^js s]
+                          (when-let [s (bean/->clj s)]
+                            (set-settings s)))]
+          (.on _settings "change" on-change)
+          #(.off _settings "change" on-change)))
+      [])
+
+    (if (seq schema)
+      [:div.cp__plugins-settings-inner
+       ;; settings.json
+       [:span.edit-file
+        (edit-settings-file pid nil)]
+
+       ;; render items
+       (for [desc schema
+             :let [key (:key desc)
+                   val (get settings (keyword key))
+                   type (keyword (:type desc))]]
+
+         (condp contains? type
+           #{:string :number} (render-item-input val desc update-setting!)
+           #{:boolean} (render-item-toggle val desc update-setting!)
+           #{:enum} (render-item-enum val desc update-setting!)
+           #{:object} (render-item-object val desc pid)
+
+           [:p (str "#Not Handled#" key)]))]
+
+      ;; no settings
+      [:h2.font-bold.text-lg.py-4.warning "No Settings Schema!"])))

+ 203 - 175
src/main/frontend/components/settings.cljs

@@ -13,6 +13,7 @@
             [frontend.handler.route :as route-handler]
             [frontend.handler.ui :as ui-handler]
             [frontend.handler.user :as user-handler]
+            [frontend.handler.plugin :as plugin-handler]
             [frontend.modules.instrumentation.core :as instrument]
             [frontend.modules.shortcut.data-helper :as shortcut-helper]
             [frontend.state :as state]
@@ -41,10 +42,10 @@
            :on-change (fn [e]
                         (reset! email (util/evalue e)))}]]]]
       (ui/button
-       "Submit"
-       :on-click
-       (fn []
-         (user-handler/set-email! @email)))
+        "Submit"
+        :on-click
+        (fn []
+          (user-handler/set-email! @email)))
 
       [:hr]
 
@@ -74,10 +75,10 @@
       [:div.mt-1.sm:mt-0.sm:col-span-2
        {:style {:display "flex" :gap "0.5rem" :align-items "center"}}
        [:div (ui/button
-              (if update-pending? "Checking ..." "Check for updates")
-              :class "text-sm p-1 mr-1"
-              :disabled update-pending?
-              :on-click #(js/window.apis.checkForUpdates false))]
+               (if update-pending? "Checking ..." "Check for updates")
+               :class "text-sm p-1 mr-1"
+               :disabled update-pending?
+               :on-click #(js/window.apis.checkForUpdates false))]
 
        [:div.text-sm.opacity-50 (str "Version " version)]]]
 
@@ -111,8 +112,8 @@
   [close-fn]
   [:div
    (ui/admonition
-    :important
-    [:p.text-gray-700 (t :user/delete-account-notice)])
+     :important
+     [:p.text-gray-700 (t :user/delete-account-notice)])
    [:div.mt-5.sm:mt-4.sm:flex.sm:flex-row-reverse
     [:span.flex.w-full.rounded-md.sm:ml-3.sm:w-auto
      [:button.inline-flex.justify-center.w-full.rounded-md.border.border-transparent.px-4.py-2.bg-indigo-600.text-base.leading-6.font-medium.text-white.shadow-sm.hover:bg-indigo-500.focus:outline-none.focus:border-indigo-700.focus:shadow-outline-indigo.transition.ease-in-out.duration-150.sm:text-sm.sm:leading-5
@@ -143,19 +144,19 @@
   [{:keys [left-label action button-label href on-click desc -for]}]
   [:div.it.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-start
 
-     ;; left column
+   ;; left column
    [:label.block.text-sm.font-medium.leading-5.opacity-70
     {:for -for}
     left-label]
 
-     ;; right column
+   ;; right column
    [:div.mt-1.sm:mt-0.sm:col-span-2
     {:style {:display "flex" :gap "0.5rem" :align-items "center"}}
     [:div (if action action (ui/button
-                             button-label
-                             :class    "text-sm p-1"
-                             :href     href
-                             :on-click on-click))]
+                              button-label
+                              :class    "text-sm p-1"
+                              :href     href
+                              :on-click on-click))]
     (when-not (or (util/mobile?)
                   (mobile-util/is-native-platform?))
       [:div.text-sm desc])]])
@@ -163,19 +164,19 @@
 
 (defn edit-config-edn []
   (row-with-button-action
-   {:left-label   (t :settings-page/custom-configuration)
-    :button-label (t :settings-page/edit-config-edn)
-    :href         (rfe/href :file {:path (config/get-config-path)})
-    :on-click     #(js/setTimeout (fn [] (ui-handler/toggle-settings-modal!)))
-    :-for         "config_edn"}))
+    {:left-label   (t :settings-page/custom-configuration)
+     :button-label (t :settings-page/edit-config-edn)
+     :href         (rfe/href :file {:path (config/get-config-path)})
+     :on-click     #(js/setTimeout (fn [] (ui-handler/toggle-settings-modal!)))
+     :-for         "config_edn"}))
 
 (defn edit-custom-css []
   (row-with-button-action
-   {:left-label   (t :settings-page/custom-theme)
-    :button-label (t :settings-page/edit-custom-css)
-    :href         (rfe/href :file {:path (config/get-custom-css-path)})
-    :on-click     #(js/setTimeout (fn [] (ui-handler/toggle-settings-modal!)))
-    :-for         "customize_css"}))
+    {:left-label   (t :settings-page/custom-theme)
+     :button-label (t :settings-page/edit-custom-css)
+     :href         (rfe/href :file {:path (config/get-custom-css-path)})
+     :on-click     #(js/setTimeout (fn [] (ui-handler/toggle-settings-modal!)))
+     :-for         "customize_css"}))
 
 (defn show-brackets-row [t show-brackets?]
   [:div.it.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-start
@@ -200,13 +201,13 @@
      [:div
       [:div.rounded-md.sm:max-w-xs
        (ui/toggle
-        enabled?
-        (fn []
-          (state/set-state! [:electron/user-cfgs :spell-check] (not enabled?))
-          (p/then (ipc/ipc "userAppCfgs" :spell-check (not enabled?))
-                  #(when (js/confirm (t :relaunch-confirm-to-work))
-                     (js/logseq.api.relaunch))))
-        true)]]]))
+         enabled?
+         (fn []
+           (state/set-state! [:electron/user-cfgs :spell-check] (not enabled?))
+           (p/then (ipc/ipc "userAppCfgs" :spell-check (not enabled?))
+                   #(when (js/confirm (t :relaunch-confirm-to-work))
+                      (js/logseq.api.relaunch))))
+         true)]]]))
 
 (rum/defcs switch-git-auto-commit-row < rum/reactive
   [state t]
@@ -217,11 +218,11 @@
      [:div
       [:div.rounded-md.sm:max-w-xs
        (ui/toggle
-        enabled?
-        (fn []
-          (state/set-state! [:electron/user-cfgs :git/disable-auto-commit?] enabled?)
-          (ipc/ipc "userAppCfgs" :git/disable-auto-commit? enabled?))
-        true)]]]))
+         enabled?
+         (fn []
+           (state/set-state! [:electron/user-cfgs :git/disable-auto-commit?] enabled?)
+           (ipc/ipc "userAppCfgs" :git/disable-auto-commit? enabled?))
+         true)]]]))
 
 (rum/defcs git-auto-commit-seconds < rum/reactive
   [state t]
@@ -308,8 +309,8 @@
                       (when-not (string/blank? format)
                         (config-handler/set-config! :journal/page-title-format format)
                         (notification/show!
-                         [:div "You must re-index your graph for this change to take effect"]
-                         :warning false)
+                          [:div "You must re-index your graph for this change to take effect"]
+                          :warning false)
                         (state/close-modal!)
                         (route-handler/redirect! {:to :repos}))))}
       (for [format (sort (date/journal-title-formatters))]
@@ -433,11 +434,11 @@
 
 (rum/defc keyboard-shortcuts-row [t]
   (row-with-button-action
-   {:left-label   (t :settings-page/customize-shortcuts)
-    :button-label (t :settings-page/shortcut-settings)
-    :on-click      #((state/close-settings!)
-                     (route-handler/redirect! {:to :shortcut-setting}))
-    :-for         "customize_shortcuts"}))
+    {:left-label   (t :settings-page/customize-shortcuts)
+     :button-label (t :settings-page/shortcut-settings)
+     :on-click      #((state/close-settings!)
+                      (route-handler/redirect! {:to :shortcut-setting}))
+     :-for         "customize_shortcuts"}))
 
 (defn zotero-settings-row [_t]
   [:div.it.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-start
@@ -447,13 +448,13 @@
    [:div.mt-1.sm:mt-0.sm:col-span-2
     [:div
      (ui/button
-      "Zotero settings"
-      :class "text-sm p-1"
-      :style {:margin-top "0px"}
-      :on-click
-      (fn []
-        (state/close-settings!)
-        (route-handler/redirect! {:to :zotero-setting})))]]])
+       "Zotero settings"
+       :class "text-sm p-1"
+       :style {:margin-top "0px"}
+       :on-click
+       (fn []
+         (state/close-settings!)
+         (route-handler/redirect! {:to :zotero-setting})))]]])
 
 (defn auto-push-row [_t current-repo enable-git-auto-push?]
   (when (and current-repo (string/starts-with? current-repo "https://"))
@@ -469,7 +470,7 @@
           (t :settings-page/disable-sentry)
           (not instrument-disabled?)
           (fn [] (instrument/disable-instrument
-                  (not instrument-disabled?)))
+                   (not instrument-disabled?)))
           [:span.text-sm.opacity-50 "Logseq will never collect your local graph database or sell your data."]))
 
 (defn clear-cache-row [t]
@@ -514,41 +515,149 @@
     {:left-label "Plug-in system"
      :action (plugin-enabled-switcher t)}))
 
-(rum/defcs ^:large-vars/cleanup-todo settings
-  < (rum/local :general ::active)
-  {:will-mount
-   (fn [state]
-     (state/load-app-user-cfgs)
-     state)
-   :will-unmount
-   (fn [state]
-     (state/close-settings!)
-     state)}
-  rum/reactive
-  [state]
-  (let [current-repo (state/sub :git/current-repo)
-        preferred-format (state/get-preferred-format)
+(rum/defcs settings-general < rum/reactive
+  [_state current-repo]
+  (let [preferred-language (state/sub [:preferred-language])
+        theme (state/sub :ui/theme)
+        dark? (= "dark" theme)
+        system-theme? (state/sub :ui/system-theme?)
+        switch-theme (if dark? "white" "dark")]
+    [:div.panel-wrap.is-general
+     (when-not (mobile-util/is-native-platform?)
+       (version-row t version))
+     (language-row t preferred-language)
+     (theme-modes-row t switch-theme system-theme? dark?)
+     (when current-repo (edit-config-edn))
+     (when current-repo (edit-custom-css))
+     (keyboard-shortcuts-row t)]))
+
+(rum/defcs settings-editor < rum/reactive
+  [_state current-repo]
+  (let [preferred-format (state/get-preferred-format)
         preferred-date-format (state/get-date-formatter)
         preferred-workflow (state/get-preferred-workflow)
-        preferred-language (state/sub [:preferred-language])
         enable-timetracking? (state/enable-timetracking?)
         enable-journals? (state/enable-journals? current-repo)
         enable-encryption? (state/enable-encryption? current-repo)
         enable-all-pages-public? (state/all-pages-public?)
-        instrument-disabled? (state/sub :instrument/disabled?)
         logical-outdenting? (state/logical-outdenting?)
         enable-tooltip? (state/enable-tooltip?)
         enable-shortcut-tooltip? (state/sub :ui/shortcut-tooltip?)
-        enable-git-auto-push? (state/enable-git-auto-push? current-repo)
-        ;; enable-block-timestamps? (state/enable-block-timestamps?)
         show-brackets? (state/show-brackets?)
-        cors-proxy (state/sub [:me :cors_proxy])
-        logged? (state/logged?)
+        enable-git-auto-push? (state/enable-git-auto-push? current-repo)]
+
+    [:div.panel-wrap.is-editor
+     (file-format-row t preferred-format)
+     (date-format-row t preferred-date-format)
+     (workflow-row t preferred-workflow)
+     ;; (enable-block-timestamps-row t enable-block-timestamps?)
+     (show-brackets-row t show-brackets?)
+     (when (util/electron?) (switch-spell-check-row t))
+     (outdenting-row t logical-outdenting?)
+     (when-not (or (util/mobile?) (mobile-util/is-native-platform?))
+       (shortcut-tooltip-row t enable-shortcut-tooltip?)
+       (tooltip-row t enable-tooltip?))
+     (timetracking-row t enable-timetracking?)
+     (journal-row t enable-journals?)
+     (encryption-row t enable-encryption?)
+     (enable-all-pages-public-row t enable-all-pages-public?)
+     (zotero-settings-row t)
+     (auto-push-row t current-repo enable-git-auto-push?)]))
+
+(rum/defc settings-git
+  []
+  [:div.panel-wrap
+   [:div.text-sm.my-4
+    [:span.text-sm.opacity-50.my-4
+     "You can view a page's edit history by clicking the three vertical dots "
+     "in the top-right corner and selecting \"Check page's history\". "
+     "Logseq uses "]
+    [:a {:href "https://git-scm.com/" :target "_blank"}
+     "Git"]
+    [:span.text-sm.opacity-50.my-4
+     " for version control."]]
+   [:br]
+   (switch-git-auto-commit-row t)
+   (git-auto-commit-seconds t)
+
+   (ui/admonition
+     :warning
+     [:p (t :settings-page/git-confirm)])])
+
+(rum/defcs settings-advanced < rum/reactive
+  [_state]
+  (let [instrument-disabled? (state/sub :instrument/disabled?)
         developer-mode? (state/sub [:ui/developer-mode?])
-        theme (state/sub :ui/theme)
-        dark? (= "dark" theme)
-        system-theme? (state/sub :ui/system-theme?)
-        switch-theme (if dark? "white" "dark")
+        cors-proxy (state/sub [:me :cors_proxy])
+        logged? (state/logged?)]
+    [:div.panel-wrap.is-advanced
+     (when (and util/mac? (util/electron?)) (app-auto-update-row t))
+     (usage-diagnostics-row t instrument-disabled?)
+     (when-not (mobile-util/is-native-platform?) (developer-mode-row t developer-mode?))
+     (when (util/electron?) (plugin-system-switcher-row t))
+     (clear-cache-row t)
+
+     (ui/admonition
+       :warning
+       [:p "Clearing the cache will discard open graphs. You will lose unsaved changes."])
+
+     (when logged?
+       [:div
+        [:div.mt-6.sm:mt-5.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-center.sm:pt-5
+         [:label.block.text-sm.font-medium.leading-5.sm:mt-px..opacity-70
+          {:for "cors"}
+          (t :settings-page/custom-cors-proxy-server)]
+         [:div.mt-1.sm:mt-0.sm:col-span-2
+          [:div.max-w-lg.rounded-md.sm:max-w-xs
+           [:input#pat.form-input.is-small.transition.duration-150.ease-in-out
+            {:default-value cors-proxy
+             :on-blur       (fn [event]
+                              (when-let [server (util/evalue event)]
+                                (user-handler/set-cors! server)
+                                (notification/show! "Custom CORS proxy updated successfully!" :success)))
+             :on-key-press  (fn [event]
+                              (let [k (gobj/get event "key")]
+                                (when (= "Enter" k)
+                                  (when-let [server (util/evalue event)]
+                                    (user-handler/set-cors! server)
+                                    (notification/show! "Custom CORS proxy updated successfully!" :success)))))}]]]]
+        (ui/admonition
+          :important
+          [:p (t :settings-page/dont-use-other-peoples-proxy-servers)
+           [:a {:href   "https://github.com/isomorphic-git/cors-proxy"
+                :target "_blank"}
+            "https://github.com/isomorphic-git/cors-proxy"]])])
+
+     (when logged?
+       [:div
+        [:hr]
+        [:div.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-center.sm:pt-5
+         [:label.block.text-sm.font-medium.leading-5.opacity-70.text-red-600.dark:text-red-400
+          {:for "delete account"}
+          (t :user/delete-account)]
+         [:div.mt-1.sm:mt-0.sm:col-span-2
+          [:div.max-w-lg.rounded-md.sm:max-w-xs
+           (ui/button (t :user/delete-your-account)
+                      :on-click (fn []
+                                  (ui-handler/toggle-settings-modal!)
+                                  (js/setTimeout #(state/set-modal! delete-account-confirm))))]]]])]))
+
+(rum/defcs settings
+  < (rum/local [:general :general] ::active)
+    {:will-mount
+     (fn [state]
+       (state/load-app-user-cfgs)
+       state)
+     :will-unmount
+     (fn [state]
+       (state/close-settings!)
+       state)}
+    rum/reactive
+  [state]
+  (let [current-repo (state/sub :git/current-repo)
+        ;; enable-block-timestamps? (state/enable-block-timestamps?)
+        _installed-plugins (state/sub :plugin/installed-plugins)
+        plugins-of-settings (and plugin-handler/lsp-enabled? (seq (plugin-handler/get-enabled-plugins-if-setting-schema)))
         *active (::active state)]
 
     [:div#settings.cp__settings-main
@@ -564,13 +673,15 @@
                [:editor (t :settings-page/tab-editor) (ui/icon "writing" {:style {:font-size 20}})]
                (when-not (mobile-util/is-native-platform?)
                  [:git (t :settings-page/tab-version-control) (ui/icon "history" {:style {:font-size 20}})])
-               [:advanced (t :settings-page/tab-advanced) (ui/icon "bulb" {:style {:font-size 20}})]]]
+               [:advanced (t :settings-page/tab-advanced) (ui/icon "bulb" {:style {:font-size 20}})]
+               (when plugins-of-settings
+                 [:plugins-setting (t :settings-of-plugins) (ui/icon "puzzle")])]]
 
           (when label
             [:li
-             {:key text
-              :class    (util/classnames [{:active (= label @*active)}])
-              :on-click #(reset! *active label)}
+             {:key      text
+              :class    (util/classnames [{:active (= label (first @*active))}])
+              :on-click #(reset! *active [label (first @*active)])}
 
              [:a.flex.items-center
               icon
@@ -578,107 +689,24 @@
 
       [:article
 
-       (case @*active
+       (case (first @*active)
+
+         :plugins-setting
+         (let [label (second @*active)]
+           (state/pub-event! [:go/plugins-settings (:id (first plugins-of-settings))])
+           (reset! *active [label label])
+           nil)
 
          :general
-         [:div.panel-wrap.is-general
-          (when-not (mobile-util/is-native-platform?)
-            (version-row t version))
-          (language-row t preferred-language)
-          (theme-modes-row t switch-theme system-theme? dark?)
-          (when current-repo (edit-config-edn))
-          (when current-repo (edit-custom-css))
-          (keyboard-shortcuts-row t)]
+         (settings-general current-repo)
 
          :editor
-         [:div.panel-wrap.is-editor
-          (file-format-row t preferred-format)
-          (date-format-row t preferred-date-format)
-          (workflow-row t preferred-workflow)
-          ;; (enable-block-timestamps-row t enable-block-timestamps?)
-          (show-brackets-row t show-brackets?)
-          (when (util/electron?) (switch-spell-check-row t))
-          (outdenting-row t logical-outdenting?)
-          (when-not (or (util/mobile?) (mobile-util/is-native-platform?))
-            (shortcut-tooltip-row t enable-shortcut-tooltip?)
-            (tooltip-row t enable-tooltip?))
-          (timetracking-row t enable-timetracking?)
-          (journal-row t enable-journals?)
-          (encryption-row t enable-encryption?)
-          (enable-all-pages-public-row t enable-all-pages-public?)
-          (zotero-settings-row t)
-          (auto-push-row t current-repo enable-git-auto-push?)]
+         (settings-editor current-repo)
 
          :git
-         [:div.panel-wrap
-          [:div.text-sm.my-4
-           [:span.text-sm.opacity-50.my-4
-            "You can view a page's edit history by clicking the three vertical dots "
-            "in the top-right corner and selecting \"Check page's history\". "
-            "Logseq uses "]
-           [:a {:href "https://git-scm.com/" :target "_blank"}
-            "Git"]
-           [:span.text-sm.opacity-50.my-4
-            " for version control."]]
-          [:br]
-          (switch-git-auto-commit-row t)
-          (git-auto-commit-seconds t)
-
-          (ui/admonition
-           :warning
-           [:p (t :settings-page/git-confirm)])]
+         (settings-git)
 
          :advanced
-         [:div.panel-wrap.is-advanced
-          (when (and util/mac? (util/electron?)) (app-auto-update-row t))
-          (usage-diagnostics-row t instrument-disabled?)
-          (when-not (mobile-util/is-native-platform?) (developer-mode-row t developer-mode?))
-          (when (util/electron?) (plugin-system-switcher-row t))
-          (clear-cache-row t)
-
-          (ui/admonition
-           :warning
-           [:p "Clearing the cache will discard open graphs. You will lose unsaved changes."])
-
-          (when logged?
-            [:div
-             [:div.mt-6.sm:mt-5.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-center.sm:pt-5
-              [:label.block.text-sm.font-medium.leading-5.sm:mt-px..opacity-70
-               {:for "cors"}
-               (t :settings-page/custom-cors-proxy-server)]
-              [:div.mt-1.sm:mt-0.sm:col-span-2
-               [:div.max-w-lg.rounded-md.sm:max-w-xs
-                [:input#pat.form-input.is-small.transition.duration-150.ease-in-out
-                 {:default-value cors-proxy
-                  :on-blur       (fn [event]
-                                   (when-let [server (util/evalue event)]
-                                     (user-handler/set-cors! server)
-                                     (notification/show! "Custom CORS proxy updated successfully!" :success)))
-                  :on-key-press  (fn [event]
-                                   (let [k (gobj/get event "key")]
-                                     (when (= "Enter" k)
-                                       (when-let [server (util/evalue event)]
-                                         (user-handler/set-cors! server)
-                                         (notification/show! "Custom CORS proxy updated successfully!" :success)))))}]]]]
-             (ui/admonition
-              :important
-              [:p (t :settings-page/dont-use-other-peoples-proxy-servers)
-               [:a {:href   "https://github.com/isomorphic-git/cors-proxy"
-                    :target "_blank"}
-                "https://github.com/isomorphic-git/cors-proxy"]])])
-
-          (when logged?
-            [:div
-             [:hr]
-             [:div.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-center.sm:pt-5
-              [:label.block.text-sm.font-medium.leading-5.opacity-70.text-red-600.dark:text-red-400
-               {:for "delete account"}
-               (t :user/delete-account)]
-              [:div.mt-1.sm:mt-0.sm:col-span-2
-               [:div.max-w-lg.rounded-md.sm:max-w-xs
-                (ui/button (t :user/delete-your-account)
-                  :on-click (fn []
-                              (ui-handler/toggle-settings-modal!)
-                              (js/setTimeout #(state/set-modal! delete-account-confirm))))]]]])]
+         (settings-advanced)
 
          nil)]]]))

+ 10 - 2
src/main/frontend/components/settings.css

@@ -68,8 +68,16 @@
     > article {
       flex: 1;
       padding: 0 12px 12px;
-      max-height: 80vh;
-      /* overflow: auto; */
+      max-height: 70vh;
+      overflow: auto;
+      margin-right: -17px;
+      margin-bottom: -17px;
+    }
+
+    &.no-aside {
+      > article {
+        padding-left: 0;
+      }
     }
 
     .panel-wrap {

+ 3 - 1
src/main/frontend/components/theme.css

@@ -42,11 +42,13 @@ html {
   }
 }
 
-.form-checkbox {
+.form-checkbox, .form-radio {
   color: var(--ls-page-checkbox-color, #6093a0);
   background-color: var(--ls-page-checkbox-color, #6093a0);
   border-color: var(--ls-page-checkbox-border-color, #6093a0);
   border: none;
+  position: relative;
+  top: -1px;
 }
 
 .form-checkbox:hover {

+ 1 - 1
src/main/frontend/db/model.cljs

@@ -19,7 +19,7 @@
             [frontend.db.default :as default-db]))
 
 ;; TODO: extract to specific models and move data transform logic to the
-;; correponding handlers.
+;; corresponding handlers.
 
 ;; Use it as an input argument for datalog queries
 (def block-attrs

+ 2 - 0
src/main/frontend/dicts.cljs

@@ -314,6 +314,7 @@
         :all-journals "All journals"
         :my-publishing "My publishing"
         :settings "Settings"
+        :settings-of-plugins "Settings of plugins"
         :plugins "Plugins"
         :themes "Themes"
         :developer-mode-alert "You need to restart the app to enable the plugin system. Do you want to restart it now?"
@@ -1190,6 +1191,7 @@
            :remove-orphaned-pages "删除空页面"
            :my-publishing "我的发布"
            :settings "设置"
+           :settings-of-plugins "插件设置"
            :plugins "插件"
            :themes "主题"
            :developer-mode-alert "如果希望插件功能立刻生效, 请重启应用。是否现在重启?"

+ 8 - 0
src/main/frontend/handler/events.cljs

@@ -236,6 +236,14 @@
 (defmethod handle :go/plugins-waiting-lists [_]
   (plugin/open-waiting-updates-modal!))
 
+(defmethod handle :go/plugins-settings [[_ pid nav? title]]
+  (if pid
+    (do
+     (state/set-state! :plugin/focused-settings pid)
+     (state/set-state! :plugin/navs-settings? (not (false? nav?)))
+     (plugin/open-focused-settings-modal! title))
+    (state/close-sub-modal! "ls-focused-settings-modal")))
+
 
 (defmethod handle :redirect-to-home [_]
   (page-handler/create-today-journal!))

+ 65 - 5
src/main/frontend/handler/plugin.cljs

@@ -7,6 +7,7 @@
             [camel-snake-kebab.core :as csk]
             [frontend.state :as state]
             [medley.core :as md]
+            [frontend.fs :as fs]
             [electron.ipc :as ipc]
             [cljs-bean.core :as bean]
             [clojure.string :as string]
@@ -132,6 +133,15 @@
   (when (:plugin/updates-downloading? @state/state)
     (state/set-state! :plugin/updates-downloading? false)))
 
+(defn has-setting-schema?
+  [id]
+  (when-let [pl (and id (get-plugin-inst (name id)))]
+    (boolean (.-settingsSchema pl))))
+
+(defn get-enabled-plugins-if-setting-schema
+  []
+  (when-let [plugins (seq (state/get-enabled?-installed-plugins false nil true))]
+    (filter #(has-setting-schema? (:id %)) plugins)))
 
 (defn setup-install-listener!
   [t]
@@ -157,7 +167,7 @@
                                       (str (t :plugin/update) (t :plugins) ": " name " - " (.-version (.-options pl))) :success)
                                     (state/consume-updates-coming-plugin payload true))))
 
-                             (do    ;; register new
+                             (do                            ;; register new
                                (p/then
                                  (js/LSPluginCore.register (bean/->js {:key id :url dst}))
                                  (fn [] (when theme (js/setTimeout #(select-a-plugin-theme id) 300))))
@@ -295,9 +305,28 @@
         (and theme-mode (state/set-theme! (if (= theme-mode "light") "white" theme-mode)))
         (js/LSPluginCore.selectTheme (bean/->js theme))))))
 
-(defn update-plugin-settings
+(defn update-plugin-settings-state
   [id settings]
-  (swap! state/state update-in [:plugin/installed-plugins id] assoc :settings settings))
+  (state/set-state! [:plugin/installed-plugins id :settings]
+                    ;; TODO: force settings related ui reactive
+                    ;; Sometimes toggle to `disable` not working
+                    ;; But related-option data updated?
+                    (assoc settings :disabled (boolean (:disabled settings)))))
+
+(defn open-settings-file-in-default-app!
+  [id-or-plugin]
+  (when-let [plugin (if (coll? id-or-plugin)
+                      id-or-plugin (state/get-plugin-by-id id-or-plugin))]
+    (when-let [file-path (:usf plugin)]
+      (js/apis.openPath file-path))))
+
+(defn open-plugin-settings!
+  ([id] (open-plugin-settings! id false))
+  ([id nav?]
+   (when-let [plugin (and id (state/get-plugin-by-id id))]
+     (if (has-setting-schema? id)
+       (state/pub-event! [:go/plugins-settings id nav? (or (:name plugin) (:title plugin))])
+       (open-settings-file-in-default-app! plugin)))))
 
 (defn parse-user-md-content
   [content {:keys [url]}]
@@ -365,6 +394,37 @@
   []
   (ipc/ipc "getLogseqDotDirRoot"))
 
+(defn make-fn-to-load-dotdir-json
+  [dirname default]
+  (fn [key]
+    (when-let [key (and key (name key))]
+      (p/let [repo   ""
+              path   (get-ls-dotdir-root)
+              exist? (fs/file-exists? path dirname)
+              _      (when-not exist? (fs/mkdir! (util/node-path.join path dirname)))
+              path   (util/node-path.join path dirname (str key ".json"))
+              _      (fs/create-if-not-exists repo "" path (or default "{}"))
+              json   (fs/read-file "" path)]
+        [path (js/JSON.parse json)]))))
+
+(defn make-fn-to-save-dotdir-json
+  [dirname]
+  (fn [key content]
+    (when-let [key (and key (name key))]
+      (p/let [repo ""
+              path (get-ls-dotdir-root)
+              path (util/node-path.join path dirname (str key ".json"))]
+        (fs/write-file! repo "" path content {:skip-compare? true})))))
+
+(defn make-fn-to-unlink-dotdir-json
+  [dirname]
+  (fn [key]
+    (when-let [key (and key (name key))]
+      (p/let [repo ""
+              path (get-ls-dotdir-root)
+              path (util/node-path.join path dirname (str key ".json"))]
+        (fs/unlink! repo path nil)))))
+
 (defn show-themes-modal!
   []
   (state/pub-event! [:modal/show-themes-modal]))
@@ -387,7 +447,7 @@
   (let [pending? (seq (:plugin/updates-pending @state/state))]
     (when-let [plugins (and (not pending?)
                             ;; TODO: too many requests may be limited by Github api
-                            (seq (take 32 (state/get-enabled-installed-plugins theme?))))]
+                            (seq (take 32 (state/get-enabled?-installed-plugins theme?))))]
       (state/set-state! :plugin/updates-pending
                         (into {} (map (fn [v] [(keyword (:id v)) v]) plugins)))
       (state/pub-event! [:plugin/consume-updates]))))
@@ -470,7 +530,7 @@
                                           (let [id (keyword id)]
                                             (when (and settings
                                                        (contains? (:plugin/installed-plugins @state/state) id))
-                                              (update-plugin-settings id (bean/->clj settings)))))))
+                                              (update-plugin-settings-state id (bean/->clj settings)))))))
 
             default-plugins (get-user-default-plugins)
 

+ 27 - 12
src/main/frontend/state.cljs

@@ -56,6 +56,7 @@
      :search/graph-filters                  []
 
      ;; modals
+     :modal/id                              nil
      :modal/label                           ""
      :modal/show?                           false
      :modal/panel-content                   nil
@@ -169,6 +170,8 @@
      :plugin/updates-coming                 {}
      :plugin/updates-downloading?           false
      :plugin/updates-unchecked              #{}
+     :plugin/navs-settings?                 true
+     :plugin/focused-settings               nil            ;; plugin id
 
      ;; pdf
      :pdf/current                           nil
@@ -1062,6 +1065,7 @@
   (:modal/show? @state))
 
 (declare set-modal!)
+(declare close-modal!)
 
 (defn get-sub-modals
   []
@@ -1094,11 +1098,14 @@
   ([all?-a-id]
    (if (true? all?-a-id)
      (swap! state assoc :modal/subsets [])
-     (let [id all?-a-id
+     (let [id     all?-a-id
+           mid    (:modal/id @state)
            modals (:modal/subsets @state)]
-       (when-let [idx (if id (first (keep-indexed #(when (= (:modal/id %2) id) %1) modals))
-                             (dec (count modals)))]
-         (swap! state assoc :modal/subsets (into [] (medley/remove-nth idx modals))))))
+       (if (and id (not (string/blank? mid)) (= id mid))
+         (close-modal!)
+         (when-let [idx (if id (first (keep-indexed #(when (= (:modal/id %2) id) %1) modals))
+                          (dec (count modals)))]
+           (swap! state assoc :modal/subsets (into [] (medley/remove-nth idx modals)))))))
    (:modal/subsets @state)))
 
 (defn set-modal!
@@ -1106,10 +1113,11 @@
    (set-modal! modal-panel-content
                {:fullscreen? false
                 :close-btn?  true}))
-  ([modal-panel-content {:keys [label fullscreen? close-btn? center?]}]
+  ([modal-panel-content {:keys [id label fullscreen? close-btn? center?]}]
    (when (seq (get-sub-modals))
      (close-sub-modal! true))
    (swap! state assoc
+          :modal/id id
           :modal/label (or label (if center? "ls-modal-align-center" ""))
           :modal/show? (boolean modal-panel-content)
           :modal/panel-content modal-panel-content
@@ -1121,6 +1129,7 @@
   (if (seq (get-sub-modals))
     (close-sub-modal!)
     (swap! state assoc
+           :modal/id nil
            :modal/label ""
            :modal/show? false
            :modal/fullscreen? false
@@ -1526,13 +1535,19 @@
   []
   (:ui/visual-viewport-state @state))
 
-(defn get-enabled-installed-plugins
-  [theme?]
-  (filterv
-    #(and (:iir %)
-          (not (get-in % [:settings :disabled]))
-          (= (boolean theme?) (:theme %)))
-    (vals (:plugin/installed-plugins @state))))
+(defn get-plugin-by-id
+  [id]
+  (when-let [id (and id (keyword id))]
+    (get-in @state [:plugin/installed-plugins id])))
+
+(defn get-enabled?-installed-plugins
+  ([theme?] (get-enabled?-installed-plugins theme? true false))
+  ([theme? enabled? include-unpacked?]
+   (filterv
+     #(and (if include-unpacked? true (:iir %))
+           (if-not (boolean? enabled?) true (= (not enabled?) (boolean (get-in % [:settings :disabled]))))
+           (= (boolean theme?) (:theme %)))
+     (vals (:plugin/installed-plugins @state)))))
 
 (defn lsp-enabled?-or-theme
   []

+ 46 - 1
src/main/frontend/ui.cljs

@@ -727,11 +727,56 @@
    (for [{:keys [label value selected]} options]
      [:option (cond->
                {:key   label
-                :value (or value label)}
+                :default-value (or value label)}
                 selected
                 (assoc :selected selected))
       label])])
 
+(rum/defc radio-list
+  [options on-change class]
+
+  [:div.ui__radio-list
+   {:class class}
+   (for [{:keys [label value selected]} options]
+     [:label
+      {:key (str "radio-list-" label)}
+      [:input.form-radio
+       {:value value
+        :type "radio"
+        :on-change #(on-change (util/evalue %))
+        :checked selected}]
+      label])])
+
+(rum/defc checkbox-list
+  [options on-change class]
+
+  (let [checked-vals
+        (->> options (filter :selected) (map :value) (into #{}))
+
+        on-item-change
+        (fn [^js e]
+          (let [^js target (.-target e)
+                checked? (.-checked target)
+                value (.-value target)]
+
+            (on-change
+              (into []
+                (if checked?
+                  (conj checked-vals value)
+                  (disj checked-vals value))))))]
+
+    [:div.ui__checkbox-list
+     {:class class}
+     (for [{:keys [label value selected]} options]
+       [:label
+        {:key (str "check-list-" label)}
+        [:input.form-checkbox
+         {:value value
+          :type  "checkbox"
+          :on-change on-item-change
+          :checked selected}]
+        label])]))
+
 (rum/defcs tippy < rum/static
   (rum/local false ::mounted?)
   [state {:keys [fixed-position? open?] :as opts} child]

+ 1 - 1
src/main/frontend/ui.css

@@ -104,7 +104,7 @@
     background: var(--ls-secondary-background-color);
 
     .panel-content {
-      overflow-y: auto;
+      overflow: overlay;
       width: 94vw;
       max-height: 85vh;
       padding: 2rem;

+ 11 - 18
src/main/logseq/api.cljs

@@ -208,29 +208,16 @@
         (fs/write-file! repo "" path (js/JSON.stringify data nil 2) {:skip-compare? true})))))
 
 (def ^:export load_plugin_user_settings
-  (fn [key]
-    (p/let [repo ""
-            path (plugin-handler/get-ls-dotdir-root)
-            exist? (fs/file-exists? path "settings")
-            _ (when-not exist? (fs/mkdir! (util/node-path.join path "settings")))
-            path (util/node-path.join path "settings" (str key ".json"))
-            _ (fs/create-if-not-exists repo "" path "{}")
-            json (fs/read-file "" path)]
-      [path (js/JSON.parse json)])))
+  ;; results [path data]
+  (plugin-handler/make-fn-to-load-dotdir-json "settings" "{}"))
 
 (def ^:export save_plugin_user_settings
   (fn [key ^js data]
-    (p/let [repo ""
-            path (plugin-handler/get-ls-dotdir-root)
-            path (util/node-path.join path "settings" (str key ".json"))]
-      (fs/write-file! repo "" path (js/JSON.stringify data nil 2) {:skip-compare? true}))))
+    ((plugin-handler/make-fn-to-save-dotdir-json "settings")
+     key (js/JSON.stringify data nil 2))))
 
 (def ^:export unlink_plugin_user_settings
-  (fn [key]
-    (p/let [repo ""
-            path (plugin-handler/get-ls-dotdir-root)
-            path (util/node-path.join path "settings" (str key ".json"))]
-      (fs/unlink! repo path nil))))
+  (plugin-handler/make-fn-to-unlink-dotdir-json "settings"))
 
 (def ^:export register_plugin_slash_command
   (fn [pid ^js cmd-actions]
@@ -635,6 +622,12 @@
   (let [^js el (gdom/getElement id)]
     (if el (str (.-tagName el) "#" id) false)))
 
+(defn ^:export set_focused_settings
+  [pid]
+  (when-let [plugin (state/get-plugin-by-id pid)]
+    (state/set-state! :plugin/focused-settings pid)
+    (state/pub-event! [:go/plugins-settings pid false (or (:name plugin) (:title plugin))])))
+
 (defn ^:export force_save_graph
   []
   (p/let [_ (el/persist-dbs!)]

Some files were not shown because too many files changed in this diff