浏览代码

feat: custom theme switching (#4741)

* feat: custom theme switching

Signed-off-by: Yue Yang <[email protected]>

* chore: handle reset-custom-theme

Signed-off-by: Yue Yang <[email protected]>

* fix: unregisterTheme

Signed-off-by: Yue Yang <[email protected]>

* fix: format with ts-standard

Signed-off-by: Yue Yang <[email protected]>

* chore: reset extra formats

Signed-off-by: Yue Yang <[email protected]>

* chore: simplify array types

Signed-off-by: Yue Yang <[email protected]>

* Update lsplugin.core.js

* fix: sync upstream

Signed-off-by: Yue Yang <[email protected]>

* chore: add `no mode` themes into panel

Signed-off-by: Yue Yang <[email protected]>

* fix: ci

Signed-off-by: Yue Yang <[email protected]>

* fix: remove first <hr />

Signed-off-by: Yue Yang <[email protected]>

* fix: lockfile

Signed-off-by: Yue Yang <[email protected]>

* chore: update

Signed-off-by: Yue Yang <[email protected]>

* chore: set-theme! => set-theme-mode!

Signed-off-by: Yue Yang <[email protected]>

* fix: selectTheme

Signed-off-by: Yue Yang <[email protected]>

* perf: reduce unnecessary logic

Signed-off-by: Yue Yang <[email protected]>
Yue Yang 3 年之前
父节点
当前提交
17b91e51c3

+ 112 - 51
libs/src/LSPlugin.core.ts

@@ -3,7 +3,6 @@ import {
   deepMerge,
   setupInjectedStyle,
   genID,
-  setupInjectedTheme,
   setupInjectedUI,
   deferred,
   invokeHostExportedApi,
@@ -19,6 +18,7 @@ import {
   IS_DEV,
   cleanInjectedScripts,
   safeSnakeCase,
+  injectTheme,
 } from './helpers'
 import * as pluginHelpers from './helpers'
 import Debug from 'debug'
@@ -34,11 +34,13 @@ import {
 } from './LSPlugin.caller'
 import {
   ILSPluginThemeManager,
+  LegacyTheme,
   LSPluginPkgConfig,
   SettingSchemaDesc,
   StyleOptions,
   StyleString,
-  ThemeOptions,
+  Theme,
+  ThemeMode,
   UIContainerAttrs,
   UIOptions,
 } from './LSPlugin'
@@ -173,10 +175,13 @@ class PluginLogger extends EventEmitter<'change'> {
 }
 
 interface UserPreferences {
-  theme: ThemeOptions
+  theme: LegacyTheme
+  themes: {
+    mode: ThemeMode
+    light: Theme
+    dark: Theme
+  }
   externals: string[] // external plugin locations
-
-  [key: string]: any
 }
 
 interface PluginLocalOptions {
@@ -310,7 +315,7 @@ function initProviderHandlers(pluginLocal: PluginLocal) {
   let themed = false
 
   // provider:theme
-  pluginLocal.on(_('theme'), (theme: ThemeOptions) => {
+  pluginLocal.on(_('theme'), (theme: Theme) => {
     pluginLocal.themeMgr.registerTheme(pluginLocal.id, theme)
 
     if (!themed) {
@@ -697,7 +702,7 @@ class PluginLocal extends EventEmitter<
     this._options.entry = entry
   }
 
-  async _loadConfigThemes(themes: ThemeOptions[]) {
+  async _loadConfigThemes(themes: Theme[]) {
     themes.forEach((options) => {
       if (!options.url) return
 
@@ -1123,6 +1128,7 @@ class LSPluginCore
     | 'unregistered'
     | 'theme-changed'
     | 'theme-selected'
+    | 'reset-custom-theme'
     | 'settings-changed'
     | 'unlink-plugin'
     | 'beforereload'
@@ -1133,19 +1139,24 @@ class LSPluginCore
   private _isRegistering = false
   private _readyIndicator?: DeferredActor
   private readonly _hostMountedActor: DeferredActor = deferred()
-  private readonly _userPreferences: Partial<UserPreferences> = {}
-  private readonly _registeredThemes = new Map<
-    PluginLocalIdentity,
-    ThemeOptions[]
-  >()
+  private readonly _userPreferences: UserPreferences = {
+    theme: null,
+    themes: {
+      mode: 'light',
+      light: null,
+      dark: null,
+    },
+    externals: [],
+  }
+  private readonly _registeredThemes = new Map<PluginLocalIdentity, Theme[]>()
   private readonly _registeredPlugins = new Map<
     PluginLocalIdentity,
     PluginLocal
   >()
   private _currentTheme: {
-    dis: () => void
     pid: PluginLocalIdentity
-    opt: ThemeOptions
+    opt: Theme | LegacyTheme
+    eject: () => void
   }
 
   /**
@@ -1182,12 +1193,25 @@ class LSPluginCore
     }
   }
 
+  /**
+   * Activate the user preferences.
+   *
+   * Steps:
+   *
+   * 1. Load the custom theme.
+   *
+   * @memberof LSPluginCore
+   */
   async activateUserPreferences() {
-    const { theme } = this._userPreferences
-
-    // 0. theme
-    if (theme) {
-      await this.selectTheme(theme, false)
+    const { theme: legacyTheme, themes } = this._userPreferences
+    const currentTheme = themes[themes.mode]
+
+    // If there is currently a theme that has been set
+    if (currentTheme) {
+      await this.selectTheme(currentTheme, { effect: false })
+    } else if (legacyTheme) {
+      // Otherwise compatible with older versions
+      await this.selectTheme(legacyTheme, { effect: false })
     }
   }
 
@@ -1238,7 +1262,7 @@ class LSPluginCore
 
       await this.loadUserPreferences()
 
-      const externals = new Set(this._userPreferences.externals || [])
+      const externals = new Set(this._userPreferences.externals)
 
       if (initial) {
         plugins = plugins.concat(
@@ -1349,8 +1373,8 @@ class LSPluginCore
       this.emit('unregistered', identity)
     }
 
-    const externals = this._userPreferences.externals || []
-    if (externals.length > 0 && unregisteredExternals.length > 0) {
+    const externals = this._userPreferences.externals
+    if (externals.length && unregisteredExternals.length) {
       await this.saveUserPreferences({
         externals: externals.filter((it) => {
           return !unregisteredExternals.includes(it)
@@ -1472,18 +1496,15 @@ class LSPluginCore
     return this._isRegistering
   }
 
-  get themes(): Map<PluginLocalIdentity, ThemeOptions[]> {
+  get themes() {
     return this._registeredThemes
   }
 
-  async registerTheme(
-    id: PluginLocalIdentity,
-    opt: ThemeOptions
-  ): Promise<void> {
-    debug('registered Theme #', id, opt)
+  async registerTheme(id: PluginLocalIdentity, opt: Theme): Promise<void> {
+    debug('Register theme #', id, opt)
 
     if (!id) return
-    let themes: ThemeOptions[] = this._registeredThemes.get(id)!
+    let themes: Theme[] = this._registeredThemes.get(id)!
     if (!themes) {
       this._registeredThemes.set(id, (themes = []))
     }
@@ -1492,41 +1513,81 @@ class LSPluginCore
     this.emit('theme-changed', this.themes, { id, ...opt })
   }
 
-  async selectTheme(opt?: ThemeOptions, effect = true): Promise<void> {
-    // clear current
+  async selectTheme(
+    theme: Theme | LegacyTheme,
+    options: {
+      effect?: boolean
+      emit?: boolean
+    } = {}
+  ) {
+    const { effect, emit } = Object.assign(
+      {},
+      { effect: true, emit: true },
+      options
+    )
+
+    // Clear current theme before injecting.
     if (this._currentTheme) {
-      this._currentTheme.dis?.()
+      this._currentTheme.eject()
     }
 
-    const disInjectedTheme = setupInjectedTheme(opt?.url)
-    this.emit('theme-selected', opt)
-    effect && (await this.saveUserPreferences({ theme: opt?.url ? opt : null }))
-    if (opt?.url) {
+    // Detect if it is the default theme (no url).
+    if (!theme.url) {
+      this._currentTheme = null
+    } else {
+      const ejectTheme = injectTheme(theme.url)
+
       this._currentTheme = {
-        dis: () => {
-          disInjectedTheme()
-          effect && this.saveUserPreferences({ theme: null })
-        },
-        opt,
-        pid: opt.pid,
+        pid: theme.pid,
+        opt: theme,
+        eject: ejectTheme,
       }
     }
+
+    if (effect) {
+      await this.saveUserPreferences(
+        theme.mode
+          ? {
+              themes: {
+                ...this._userPreferences.themes,
+                mode: theme.mode,
+                [theme.mode]: theme,
+              },
+            }
+          : { theme: theme }
+      )
+    }
+
+    if (emit) {
+      this.emit('theme-selected', theme)
+    }
   }
 
-  async unregisterTheme(
-    id: PluginLocalIdentity,
-    effect: boolean = true
-  ): Promise<void> {
-    debug('unregistered Theme #', id)
+  async unregisterTheme(id: PluginLocalIdentity, effect = true) {
+    debug('Unregister theme #', id)
+
+    if (!this._registeredThemes.has(id)) {
+      return
+    }
 
-    if (!this._registeredThemes.has(id)) return
     this._registeredThemes.delete(id)
     this.emit('theme-changed', this.themes, { id })
     if (effect && this._currentTheme?.pid === id) {
-      this._currentTheme.dis?.()
+      this._currentTheme.eject()
       this._currentTheme = null
-      // reset current theme
-      this.emit('theme-selected', null)
+
+      const { theme, themes } = this._userPreferences
+      await this.saveUserPreferences({
+        theme: theme?.pid === id ? null : theme,
+        themes: {
+          ...themes,
+          light: themes.light?.pid === id ? null : themes.light,
+          dark: themes.dark?.pid === id ? null : themes.dark,
+        },
+      })
+
+      // Reset current theme if it is unregistered
+      this.emit('reset-custom-theme', this._userPreferences.themes)
     }
   }
 }

+ 34 - 19
libs/src/LSPlugin.ts

@@ -1,18 +1,24 @@
-import EventEmitter from 'eventemitter3'
 import * as CSS from 'csstype'
+
+import EventEmitter from 'eventemitter3'
 import { LSPluginCaller } from './LSPlugin.caller'
-import { LSPluginFileStorage } from './modules/LSPlugin.Storage'
 import { LSPluginExperiments } from './modules/LSPlugin.Experiments'
+import { LSPluginFileStorage } from './modules/LSPlugin.Storage'
 
 export type PluginLocalIdentity = string
 
-export type ThemeOptions = {
+export type ThemeMode = 'light' | 'dark'
+
+export interface LegacyTheme {
   name: string
   url: string
   description?: string
-  mode?: 'dark' | 'light'
+  mode?: ThemeMode
+  pid: PluginLocalIdentity
+}
 
-  [key: string]: any
+export interface Theme extends LegacyTheme {
+  mode: ThemeMode
 }
 
 export type StyleString = string
@@ -64,7 +70,7 @@ export interface LSPluginPkgConfig {
   entry: string // alias of main
   title: string
   mode: 'shadow' | 'iframe'
-  themes: Array<ThemeOptions>
+  themes: Theme[]
   icon: string
 
   [key: string]: any
@@ -122,7 +128,7 @@ export interface AppInfo {
  * User's app configurations
  */
 export interface AppUserConfigs {
-  preferredThemeMode: 'dark' | 'light'
+  preferredThemeMode: ThemeMode
   preferredFormat: 'markdown' | 'org'
   preferredDateFormat: string
   preferredStartOfWeek: string
@@ -382,7 +388,7 @@ export interface IAppProxy {
     content: string,
     status?: 'success' | 'warning' | 'error' | string
   ) => void
-  
+
   setZoomFactor: (factor: number) => void
   setFullScreen: (flag: boolean | 'toggle') => void
   setLeftSidebarVisible: (flag: boolean | 'toggle') => void
@@ -614,9 +620,17 @@ export interface IEditorProxy extends Record<string, any> {
 
   getAllPages: (repo?: string) => Promise<any>
 
-  prependBlockInPage: (page: PageIdentity, content: string, opts?: Partial<{ properties: {} }>) => Promise<BlockEntity | null>
+  prependBlockInPage: (
+    page: PageIdentity,
+    content: string,
+    opts?: Partial<{ properties: {} }>
+  ) => Promise<BlockEntity | null>
 
-  appendBlockInPage: (page: PageIdentity, content: string, opts?: Partial<{ properties: {} }>) => Promise<BlockEntity | null>
+  appendBlockInPage: (
+    page: PageIdentity,
+    content: string,
+    opts?: Partial<{ properties: {} }>
+  ) => Promise<BlockEntity | null>
 
   getPreviousSiblingBlock: (
     srcBlock: BlockIdentity
@@ -756,9 +770,7 @@ export interface IAssetsProxy {
    * @added 0.0.2
    * @param exts
    */
-  listFilesOfCurrentGraph(
-    exts: string | string[]
-  ): Promise<{
+  listFilesOfCurrentGraph(exts: string | string[]): Promise<{
     path: string
     size: number
     accessTime: number
@@ -768,14 +780,17 @@ export interface IAssetsProxy {
   }>
 }
 
-export interface ILSPluginThemeManager extends EventEmitter {
-  themes: Map<PluginLocalIdentity, Array<ThemeOptions>>
+export interface ILSPluginThemeManager {
+  get themes(): Map<PluginLocalIdentity, Theme[]>
 
-  registerTheme(id: PluginLocalIdentity, opt: ThemeOptions): Promise<void>
+  registerTheme(id: PluginLocalIdentity, opt: Theme): Promise<void>
 
-  unregisterTheme(id: PluginLocalIdentity): Promise<void>
+  unregisterTheme(id: PluginLocalIdentity, effect?: boolean): Promise<void>
 
-  selectTheme(opt?: ThemeOptions): Promise<void>
+  selectTheme(
+    opt: Theme | LegacyTheme,
+    options: { effect?: boolean; emit?: boolean }
+  ): Promise<void>
 }
 
 export type LSPluginUserEvents = 'ui:visible:changed' | 'settings:changed'
@@ -837,7 +852,7 @@ export interface ILSPluginUser extends EventEmitter<LSPluginUserEvents> {
   /**
    * Set the theme for the main Logseq app
    */
-  provideTheme(theme: ThemeOptions): this
+  provideTheme(theme: Theme): this
 
   /**
    * Inject custom css for the main Logseq app

+ 4 - 3
libs/src/LSPlugin.user.ts

@@ -15,7 +15,7 @@ import {
   SlashCommandAction,
   BlockCommandCallback,
   StyleString,
-  ThemeOptions,
+  Theme,
   UIOptions,
   IHookEvent,
   BlockIdentity,
@@ -318,7 +318,8 @@ const KEY_MAIN_UI = 0
  */
 export class LSPluginUser
   extends EventEmitter<LSPluginUserEvents>
-  implements ILSPluginUser {
+  implements ILSPluginUser
+{
   // @ts-ignore
   private _version: string = LIB_VERSION
   private _debugTag: string = ''
@@ -436,7 +437,7 @@ export class LSPluginUser
     return this
   }
 
-  provideTheme(theme: ThemeOptions) {
+  provideTheme(theme: Theme) {
     this.caller.call('provider:theme', theme)
     return this
   }

+ 5 - 10
libs/src/helpers.ts

@@ -416,26 +416,21 @@ export function transformableEvent(target: HTMLElement, e: Event) {
   return obj
 }
 
-let injectedThemeEffect: any = null
-
-export function setupInjectedTheme(url?: string) {
-  injectedThemeEffect?.call()
-
-  if (!url) return
-
+export function injectTheme(url: string) {
   const link = document.createElement('link')
   link.rel = 'stylesheet'
   link.href = url
   document.head.appendChild(link)
 
-  return (injectedThemeEffect = () => {
+  const ejectTheme = () => {
     try {
       document.head.removeChild(link)
     } catch (e) {
       console.error(e)
     }
-    injectedThemeEffect = null
-  })
+  }
+
+  return ejectTheme
 }
 
 export function mergeSettingsWithSchema(

文件差异内容过多而无法显示
+ 0 - 0
resources/js/lsplugin.core.js


+ 71 - 46
src/main/frontend/components/plugins.cljs

@@ -17,62 +17,87 @@
             [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))
-                   }))))
+  <
+  (rum/local [] ::themes)
+  (rum/local 0 ::cursor)
+  (rum/local 0 ::total)
+  {:did-mount (fn [state] (let [*themes        (::themes state)
+                                *cursor        (::cursor state)
+                                *total         (::total state)
+                                mode           (state/sub :ui/theme)
+                                all-themes     (state/sub :plugin/installed-themes)
+                                themes         (->> all-themes
+                                                    (filter #(= (:mode %) mode))
+                                                    (sort-by #(:name %)))
+                                no-mode-themes (->> all-themes
+                                                    (filter #(= (:mode %) nil))
+                                                    (sort-by #(:name %))
+                                                    (map-indexed (fn [idx opt] (assoc opt :group-first (zero? idx) :group-desc (if (zero? idx) "light & dark themes" nil)))))
+                                selected       (state/sub :plugin/selected-theme)
+                                themes         (map-indexed (fn [idx opt]
+                                                              (let [selected? (= (:url opt) selected)]
+                                                                (when selected? (reset! *cursor (+ idx 1)))
+                                                                (assoc opt :mode mode :selected selected?))) (concat themes no-mode-themes))
+                                themes         (cons {:name        (string/join " " ["Default" (string/capitalize mode) "Theme"])
+                                                      :url         nil
+                                                      :description (string/join " " ["Logseq default" mode "theme."])
+                                                      :mode        mode
+                                                      :selected    (nil? selected)
+                                                      :group-first true
+                                                      :group-desc  (str mode " themes")} themes)]
+                            (reset! *themes themes)
+                            (reset! *total (count themes))
+                            state))}
+  (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))]
-
+        *themes (::themes state)]
     [:div.cp__themes-installed
      {: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)))]
+      (fn [idx opt]
+        (let [current-selected? (:selected opt)
+              group-first?      (:group-first opt)
+              plg               (get (:plugin/installed-plugins @state/state) (keyword (:pid opt)))]
+          [:div
+           (when (and group-first? (not= idx 0)) [:hr.my-2])
            [:div.it.flex.px-3.py-1.5.rounded-sm.justify-between
             {:key      (str idx (:url opt))
-             :title    (when current-selected "Cancel selected theme")
+             :title    (:description opt)
              :class    (util/classnames
-                         [{:is-selected current-selected
-                           :is-active   (= idx @*cursor)}])
-             :on-click #(do (js/LSPluginCore.selectTheme (if current-selected nil (clj->js opt)))
+                        [{:is-selected current-selected?
+                          :is-active   (= idx @*cursor)}])
+             :on-click #(do (js/LSPluginCore.selectTheme (bean/->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)]))
+            [:div.flex.items-center.text-xs
+             [:div.opacity-60 (str (or (:name plg) "Logseq") " •")]
+             [:div.name.ml-1 (:name opt)]]
+            (when (or group-first? current-selected?)
+              [:div.flex.items-center
+               (when group-first? [:small.opacity-60 (:group-desc opt)])
+               (when current-selected? [:small.inline-flex.ml-1.opacity-60 (ui/icon "check")])])]]))
+      @*themes)]))
 
 (rum/defc unpacked-plugin-loader
   [unpacked-pkg-path]

+ 3 - 8
src/main/frontend/components/plugins.css

@@ -590,7 +590,7 @@
     outline: none;
     padding: 1rem;
 
-    > .it {
+    .it {
       user-select: none;
       background-color: var(--ls-secondary-background-color);
       border: 1px solid transparent;
@@ -598,13 +598,8 @@
       cursor: pointer;
       opacity: .8;
 
-      > section {
-        line-height: 1.1em;
-
-        > strong {
-          font-size: 13px;
-          font-weight: 600;
-        }
+      .name {
+        font-weight: 600;
       }
 
       &.is-active {

+ 1 - 0
src/main/frontend/components/theme.cljs

@@ -24,6 +24,7 @@
         (if (= theme "dark") ;; for tailwind dark mode
           (.add cls "dark")
           (.remove cls "dark"))
+        (ui/apply-custom-theme-effect! theme)
         (plugin-handler/hook-plugin-app :theme-mode-changed {:mode theme} nil))
      [theme])
 

+ 16 - 8
src/main/frontend/handler/plugin.cljs

@@ -364,9 +364,7 @@
   [pid]
   (when-let [themes (get (group-by :pid (:plugin/installed-themes @state/state)) pid)]
     (when-let [theme (first themes)]
-      (let [theme-mode (:mode theme)]
-        (and theme-mode (state/set-theme! theme-mode))
-        (js/LSPluginCore.selectTheme (bean/->js theme))))))
+      (js/LSPluginCore.selectTheme (bean/->js theme)))))
 
 (defn update-plugin-settings-state
   [id settings]
@@ -599,12 +597,22 @@
                                        (swap! state/state assoc :plugin/installed-themes
                                               (vec (mapcat (fn [[pid vs]] (mapv #(assoc % :pid pid) (bean/->clj vs))) (bean/->clj themes))))))
 
-                (.on "theme-selected" (fn [^js opts]
-                                        (let [opts (bean/->clj opts)
-                                              url (:url opts)
-                                              mode (:mode opts)]
-                                          (when mode (state/set-theme! mode))
+                (.on "theme-selected" (fn [^js theme]
+                                        (let [theme (bean/->clj theme)
+                                              url (:url theme)
+                                              mode (:mode theme)]
+                                          (when mode
+                                            (state/set-custom-theme! mode theme)
+                                            (state/set-theme-mode! mode))
                                           (state/set-state! :plugin/selected-theme url))))
+                                        
+                (.on "reset-custom-theme" (fn [^js themes]
+                                            (let [themes (bean/->clj themes)
+                                                  custom-theme (dissoc themes :mode)
+                                                  mode (:mode themes)]
+                                              (state/set-custom-theme! {:light (if (nil? (:light custom-theme)) {:mode "light"} (:light custom-theme))
+                                                                        :dark (if (nil? (:dark custom-theme)) {:mode "dark"} (:dark custom-theme))})
+                                              (state/set-theme-mode! mode))))
 
                 (.on "settings-changed" (fn [id ^js settings]
                                           (let [id (keyword id)]

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

@@ -63,8 +63,9 @@
      :ui/settings-open?                     false
      :ui/sidebar-open?                      false
      :ui/left-sidebar-open?                 (boolean (storage/get "ls-left-sidebar-open?"))
-     :ui/theme                              (or (storage/get :ui/theme) (if (mobile-util/is-native-platform?) "light" "dark"))
+     :ui/theme                              (or (storage/get :ui/theme) "light")
      :ui/system-theme?                      ((fnil identity (or util/mac? util/win32? false)) (storage/get :ui/system-theme?))
+     :ui/custom-theme                       (or (storage/get :ui/custom-theme) {:light {:mode "light"} :dark {:mode "dark"}})
      :ui/wide-mode?                         (storage/get :ui/wide-mode)
 
      ;; ui/collapsed-blocks is to separate the collapse/expand state from db for:
@@ -886,30 +887,45 @@
     (set-edit-content! edit-input-id content)
     (set-state! [:editor/last-saved-cursor (:block/uuid (get-edit-block))] new-pos)))
 
-(defn set-theme!
-  [theme]
-  (set-state! :ui/theme theme)
+(defn set-theme-mode!
+  [mode]
   (when (mobile-util/native-ios?)
-    (if (= theme "light")
+    (if (= mode "light")
       (util/set-theme-light)
       (util/set-theme-dark)))
-  (storage/set :ui/theme theme))
+  (set-state! :ui/theme mode)
+  (storage/set :ui/theme mode))
 
 (defn sync-system-theme!
   []
   (let [system-dark? (.-matches (js/window.matchMedia "(prefers-color-scheme: dark)"))]
-    (set-theme! (if system-dark? "dark" "light"))
+    (set-theme-mode! (if system-dark? "dark" "light"))
     (set-state! :ui/system-theme? true)
     (storage/set :ui/system-theme? true)))
 
 (defn use-theme-mode!
   [theme-mode]
-  (if-not (= theme-mode "system")
+  (if (= theme-mode "system")
+    (sync-system-theme!)
     (do
-      (set-theme! theme-mode)
+      (set-theme-mode! theme-mode)
       (set-state! :ui/system-theme? false)
-      (storage/set :ui/system-theme? false))
-    (sync-system-theme!)))
+      (storage/set :ui/system-theme? false))))
+
+(defn toggle-theme
+  [theme]
+  (if (= theme "dark") "light" "dark"))
+
+(defn toggle-theme!
+  []
+  (use-theme-mode! (toggle-theme (:ui/theme @state))))
+
+(defn set-custom-theme!
+  ([custom-theme]
+   (set-custom-theme! nil custom-theme))
+  ([mode theme]
+   (set-state! (if mode [:ui/custom-theme (keyword mode)] :ui/custom-theme) theme)
+   (storage/set :ui/custom-theme (:ui/custom-theme @state))))
 
 (defn set-editing-block-dom-id!
   [block-dom-id]
@@ -919,12 +935,6 @@
   []
   (:editor/block-dom-id @state))
 
-(defn toggle-theme!
-  []
-  (let [theme (:ui/theme @state)
-        theme' (if (= theme "dark") "light" "dark")]
-    (use-theme-mode! theme')))
-
 (defn set-root-component!
   [component]
   (set-state! :ui/root-component component))

+ 6 - 0
src/main/frontend/ui.cljs

@@ -324,6 +324,12 @@
         (state/set-visual-viewport-state nil))))
   #())
 
+(defn apply-custom-theme-effect! [theme]
+  (when-let [custom-theme (state/sub [:ui/custom-theme (keyword theme)])]
+    (js/LSPluginCore.selectTheme (bean/->js custom-theme)
+                                 (bean/->js {:effect false :emit false}))
+    (state/set-state! :plugin/selected-theme (:url custom-theme))))
+
 (defn setup-system-theme-effect!
   []
   (let [^js schemaMedia (js/window.matchMedia "(prefers-color-scheme: dark)")]

+ 1 - 1
src/main/logseq/api.cljs

@@ -120,7 +120,7 @@
 
 (def ^:export set_theme_mode
   (fn [mode]
-    (state/set-theme! mode)))
+    (state/set-theme-mode! mode)))
 
 (def ^:export load_plugin_config
   (fn [path]

部分文件因为文件数量过多而无法显示