Files
Synapse/src/main/frontend/components/settings.cljs
Dasemu 1e7e9a3c13 Fase 8 (2/N): delete the client-side RTC/E2EE/sync UI and finish the teardown
Removes the client-side half of the remote CRDT sync protocol left after
the worker-side deletion in 565c211: RTC/remote-graph UI (components/rtc/*,
db/rtc/debug_ui.cljs, cloud-graph upload/download/leave/delete flows in
components/repo.cljs and mobile/components/app.cljs), E2EE UI and handlers
(components/e2ee.cljs, handler/e2ee.cljs, handler/events/rtc*.cljs), the
db-worker-node ui-request RPC (handler/worker.cljs's :db-worker/ui-request,
which existed only to serve E2EE password prompts), settings panels for the
legacy custom sync-server URL, and the now-orphaned :rtc/* client state
(presence UI, upload/download progress, remote-graph lists) — presence.cljs
was already deleted in the worker-side pass, contrary to the plan's "keep"
list, so all of this had zero producers left and was fully inert.

Also fixes several dead thread-api invokes discovered on LIVE paths that
would throw at runtime rather than being merely dead UI:
- db/restore.cljs's restore-graph! (called on every graph load)
- persist_db.cljs's <ensure-remote! (the Fase 3/4 daemon connect path)
- persist_db/browser.cljs's worker init and shutdown
- handler/user.cljs's logout

Verified via full compile of all 5 shadow-cljs targets (app, electron,
mobile, db-worker-node, test — 0 warnings) and a full bb dev:test run:
every namespace passes except the same 7 frontend.persist-db-test failures
that reproduce identically on unmodified HEAD (confirmed via git stash),
pre-existing network-sandbox flakiness unrelated to this change.

Known follow-up, not yet done: a handful of harmless-but-vestigial :rtc/*
references remain (handler/user.cljs's storage/graph-count exceed-limit
events, handler/events.cljs's :rtc-download? opt, worker/state.cljs's
*db-sync-config atom, worker/handler/graph.cljs's get-rtc-graph-uuid reused
by handler/editor.cljs's quick-add filter) — none throw or affect behavior,
left for a future pass rather than risk removing something still load-bearing
without deeper tracing. deps/db-sync/ (the Cloudflare Worker) is also still
untouched, per the plan's explicit note that it needs separate, larger surgery.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-26 15:59:39 +02:00

1357 lines
60 KiB
Clojure

(ns frontend.components.settings
(:require [clojure.string :as string]
[electron.ipc :as ipc]
[frontend.colors :as colors]
[frontend.components.assets :as assets]
[frontend.components.email :as email-component]
[frontend.components.shortcut :as shortcut]
[frontend.components.svg :as svg]
[frontend.config :as config]
[frontend.context.i18n :refer [interpolate-rich-text-node locale-join-rich-text-node locale-format-date t]]
[frontend.date :as date]
[frontend.dicts :as dicts]
[frontend.handler.config :as config-handler]
[frontend.handler.global-config :as global-config-handler]
[frontend.handler.notification :as notification]
[frontend.handler.plugin :as plugin-handler]
[frontend.handler.property :as property-handler]
[frontend.handler.ui :as ui-handler]
[frontend.handler.user :as user-handler]
[frontend.mobile.util :as mobile-util]
[frontend.modules.instrumentation.core :as instrument]
[frontend.modules.shortcut.data-helper :as shortcut-helper]
[frontend.persist-db.remote :as remote]
[frontend.rfx :as rfx]
[frontend.spec.storage :as storage-spec]
[frontend.state :as state]
[frontend.storage :as storage]
[frontend.ui :as ui]
[frontend.util :refer [classnames web-platform?] :as util]
[frontend.version :as fv]
[goog.string :as gstring]
[lambdaisland.glogi :as log]
[logseq.common.version :as build-version]
[logseq.shui.hooks :as hooks]
[logseq.shui.ui :as shui]
[promesa.core :as p]
[reitit.frontend.easy :as rfe]
[io.factorhouse.hsx.core :as hsx]))
(defn toggle
[label-for name state on-toggle & [detail-text]]
(let [label (cond-> [:label.block.text-sm.font-medium.leading-5.opacity-70
{:for label-for}]
(sequential? name) (into name)
(not (sequential? name)) (conj name))]
[:div.it.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-center
label
[:div.rounded-md.sm:max-w-tss.sm:col-span-2
[:div.rounded-md {:style {:display "flex" :gap "1rem" :align-items "center"}}
(ui/toggle state on-toggle true)
detail-text]]]))
(hsx/defc app-updater
[version]
(let [update-pending? (rfx/use-sub [:electron/updater-pending?])
{:keys [type payload]} (rfx/use-sub [:electron/updater])]
[:span.cp__settings-app-updater
[:div.ctls.flex.items-center
[:div.mt-1.sm:mt-0.sm:col-span-2.flex.gap-4.items-center.flex-wrap
[:div (cond
(mobile-util/native-android?)
(ui/button
(t :settings.general/check-for-updates)
:class "text-sm mr-1"
:href "https://github.com/logseq/logseq/releases")
(mobile-util/native-ios?)
(ui/button
(t :settings.general/check-for-updates)
:class "text-sm mr-1"
:href "https://apps.apple.com/app/logseq/id1601013908")
(util/electron?)
(ui/button
(if update-pending? (t :updater/checking-for-updates) (t :settings.general/check-for-updates))
:class "text-sm mr-1"
:disabled update-pending?
:on-click #(js/window.apis.checkForUpdates true))
:else
nil)]
(let [revision (build-version/revision)]
[:div.text-sm.cursor
{:title (t :settings.general/revision revision)
:on-click (fn []
(notification/show! [:div
[:span (t :settings.general/current-revision-label)]
[:a {:target "_blank"
:style {:margin-inline-start "0.25rem"}
:href (str "https://github.com/logseq/logseq/commit/" revision)}
revision]]
:info
false))}
version])
[:a.text-sm.fade-link.underline.inline
{:target "_blank"
:href "https://docs.logseq.com/#/page/changelog"}
(t :settings.general/changelog)]]]
(when-not (string/blank? type)
[:div.update-state.text-sm
(case type
"checking-for-update"
[:p (t :updater/checking-for-updates)]
"update-not-available"
[:p (t :updater/up-to-date)]
"update-available"
(let [{:keys [name url]} payload]
[:p
[:span (t :updater/update-available)]
[:a.link
{:on-click
(fn [e]
(js/window.apis.openExternal url)
(util/stop e))
:style {:margin-inline-start "0.25rem"}}
svg/external-link name " 🎉"]])
"download-progress"
(let [percent (some-> payload :percent js/Math.round)]
[:p (t :updater/downloading-progress (or percent 0))])
"update-downloaded"
[:div.flex.items-center.gap-2.flex-wrap
[:p (t :updater/update-ready-to-install)]
(ui/button
(t :updater/quit-and-install)
:class "text-sm"
:on-click #(ipc/ipc :quitAndInstall))]
"error"
(let [release-channel-link
[:a.link.inline-flex.items-center.gap-1
{:on-click
(fn [e]
(js/window.apis.openExternal "https://github.com/logseq/logseq/releases")
(util/stop e))}
svg/external-link
(t :settings.general/release-channel)]]
[:p
(interpolate-rich-text-node
(t :updater/update-error)
[release-channel-link]
true)]))])]))
(hsx/defc outdenting-hint
[]
[:div.rounded-lg.border.overflow-hidden.bg-card
{:style {:box-shadow "0 4px 20px 4px rgba(0, 20, 60, .1), 0 4px 80px -8px rgba(0, 20, 60, .2)"}}
[:div {:style {:margin "12px" :max-width "500px"}}
[:p.text-sm
(t :settings.editor/preferred-outdenting-tip)
[:a.text-sm
{:target "_blank" :href "https://discuss.logseq.com/t/whats-your-preferred-outdent-behavior-the-direct-one-or-the-logical-one/978"}
(t :settings.editor/preferred-outdenting-tip-more)]]
[:img {:src "https://discuss.logseq.com/uploads/default/original/1X/e8ea82f63a5e01f6d21b5da827927f538f3277b9.gif"
:width 500
:height 500}]]])
(hsx/defc auto-expand-hint
[]
[:div.rounded-lg.border.overflow-hidden.bg-card
{:style {:box-shadow "0 4px 20px 4px rgba(0, 20, 60, .1), 0 4px 80px -8px rgba(0, 20, 60, .2)"}}
[:div {:style {:margin "12px" :max-width "500px"}}
[:p.text-sm
(t :settings.editor/auto-expand-block-refs-tip)]
[:img {:src "https://user-images.githubusercontent.com/28241963/225818326-118deda9-9d1e-477d-b0ce-771ca0bcd976.gif"
:width 500
:height 500}]]])
(defn row-with-button-action
[{:keys [left-label description action button-label href on-click desc -for stretch]}]
[:div.it.sm:grid.sm:grid-cols-3.sm:gap-4
{:class "sm:items-start"}
;; left column
[:div.flex.flex-col
[:label.block.text-sm.font-medium.leading-5.opacity-70
{:for -for}
left-label]
(when description
[:div.text-xs.text-gray-10 description])]
;; right column
[:div.mt-1.sm:mt-0.sm:col-span-2.flex.items-center
{:style {:display "flex" :gap "0.5rem" :align-items "center"}}
[:div {:style (when stretch {:width "100%"})}
(if action action (shui/button
{:as-child (not (string/blank? href))
:size :sm
:on-click on-click}
(if (string/blank? href) button-label
(shui/link {:href href} button-label))))]
(when-not (or (util/mobile?)
(mobile-util/native-platform?))
[:div.text-sm.flex desc])]])
(defn edit-config-edn []
(row-with-button-action
{:left-label (t :settings.general/custom-configuration)
:button-label (t :settings.general/edit-config-edn)
:href (rfe/href :file {:path (config/get-repo-config-path)})
:on-click ui-handler/toggle-settings-modal!
:-for "config_edn"}))
(defn edit-global-config-edn []
(row-with-button-action
{:left-label (t :settings.general/custom-global-configuration)
:button-label (t :settings.general/edit-global-config-edn)
:href (rfe/href :file {:path (global-config-handler/global-config-path)})
:on-click ui-handler/toggle-settings-modal!
:-for "global_config_edn"}))
(defn edit-custom-css []
(row-with-button-action
{:left-label (t :settings.general/custom-theme)
:button-label (t :settings.general/edit-custom-css)
:href (rfe/href :file {:path (config/get-custom-css-path)})
:on-click ui-handler/toggle-settings-modal!
:-for "customize_css"}))
(defn edit-export-css []
(row-with-button-action
{:left-label (t :settings.general/export-theme)
:button-label (t :settings.general/edit-export-css)
:href (rfe/href :file {:path (config/get-export-css-path)})
:on-click ui-handler/toggle-settings-modal!
:-for "export_css"}))
(defn show-brackets-row [t show-brackets?]
[:div.it.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-center
[:label.block.text-sm.font-medium.leading-5.opacity-70
{:for "show_brackets"}
(t :settings.editor/show-brackets)]
[:div
[:div.rounded-md.sm:max-w-xs
(ui/toggle show-brackets?
config-handler/toggle-ui-show-brackets!
true)]]
(when (not (or (util/mobile?) (mobile-util/native-platform?)))
[:div {:style {:text-align "right"}}
(ui/render-keyboard-shortcut (shortcut-helper/gen-shortcut-seq :ui/toggle-brackets)
:shortcut-id :ui/toggle-brackets)])])
(defn toggle-wide-mode-row [t wide-mode?]
[:div.it.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-center
[:label.block.text-sm.font-medium.leading-5.opacity-70
{:for "wide_mode"}
(t :settings.editor/wide-mode)]
[:div
[:div.rounded-md.sm:max-w-xs
(ui/toggle wide-mode?
ui-handler/toggle-wide-mode!
true)]]
(when (not (or (util/mobile?) (mobile-util/native-platform?)))
[:div {:style {:text-align "right"}}
(ui/render-keyboard-shortcut (shortcut-helper/gen-shortcut-seq :ui/toggle-wide-mode)
:shortcut-id :ui/toggle-wide-mode)])])
(defn editor-font-family-row [t {:keys [type global]}]
[:div.it.sm:grid.sm:grid-cols-3.sm:gap-4
[:label.block.text-sm.font-medium.leading-5.opacity-70
{:for "font_family"}
(t :settings.general/editor-font)]
[:div.flex.flex-col.col-span-2
[:div.flex.gap-2
(for [t [:default :serif :mono]
:let [t (name t)
tt (string/capitalize t)
active? (= (or type "default") t)]]
(shui/button
{:key t
:variant :secondary
:class (str "cursor-pointer" (when active? " !border-primary border-[2px]"))
:on-click #(state/set-editor-font! {:type t})}
[:span.flex.flex-col
{:class (str "ls-font-" t)}
[:strong "Ag"]
[:small tt]]))]
[:div.pt-3
[:label.w-full.flex.items-center.cursor-pointer
(shui/checkbox {:checked (boolean global)
:on-checked-change #(state/set-editor-font! {:global %})})
[:span.pl-1.text-sm.opacity-70 (t :settings.general/editor-font-set-global)]]]]])
(hsx/defc switch-spell-check-row
[t]
;; Match electron.spell-check/session-spellcheck-enabled?: nil defaults to on.
(let [enabled? (not= false (rfx/use-sub [:electron/user-cfgs :spell-check]))]
[:div.it.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-center
[:label.block.text-sm.font-medium.leading-5.opacity-70
(t :settings.editor/spell-checker)]
[: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 :ui/relaunch-confirm))
(js/logseq.api.relaunch))))
true)]]]))
(hsx/defc app-auto-update-row [t]
(let [enabled? (rfx/use-sub [:electron/user-cfgs :auto-update])
enabled? (if (nil? enabled?) true enabled?)]
(toggle "usage-diagnostics"
(t :settings.advanced/auto-updater)
enabled?
#((state/set-state! [:electron/user-cfgs :auto-update] (not enabled?))
(ipc/ipc :userAppCfgs :auto-update (not enabled?))))))
(defn language-row [t preferred-language]
(let [selected-language (some-> preferred-language name)
language-items (mapv (fn [language]
{:value (name (:value language))
:label (:label language)})
dicts/languages)
language-labels (into {} (map (juxt :value :label)) language-items)
on-value-change (fn [lang-code]
(when lang-code
(state/set-preferred-language! lang-code)
(state/pub-event! [:init/commands])
(ui-handler/re-render-root!)))
action (shui/select
{:value selected-language
:items language-items
:on-value-change on-value-change}
(shui/select-trigger
{:class "w-64 h-8"}
(shui/select-value
(fn [value]
(get language-labels value value))))
(shui/select-content
{:container (.-body js/document)
:class "z-[99999]"
:positioner-props {:style {:z-index 99999}}}
(shui/select-group
(for [{:keys [value label]} language-items]
(shui/select-item {:key value :value value} label)))))]
(row-with-button-action {:left-label (t :settings.general/language)
:-for "preferred_language"
:action action})))
(hsx/defc theme-modes-row
[t]
(let [_preferred-language (rfx/use-sub [:preferred-language])
theme (rfx/use-sub [:ui/theme])
dark? (= "dark" theme)
system-theme? (rfx/use-sub [:ui/system-theme?])
switch-theme (if dark? "light" "dark")
switch-theme-label (case switch-theme
"light" (t :settings.general/theme-light)
"dark" (t :settings.general/theme-dark)
(t :settings.general/theme-system))
color-accent (rfx/use-sub [:ui/radix-color])
pick-theme [:ul.cp__theme-modes-options
[:li {:on-click (partial state/use-theme-mode! "light")
:class (classnames [{:active (and (not system-theme?) (not dark?))}])} [:i.mode-light {:class (when color-accent "radix")}] [:strong (t :settings.general/theme-light)]]
[:li {:on-click (partial state/use-theme-mode! "dark")
:class (classnames [{:active (and (not system-theme?) dark?)}])} [:i.mode-dark {:class (when color-accent "radix")}] [:strong (t :settings.general/theme-dark)]]
[:li {:on-click (partial state/use-theme-mode! "system")
:class (classnames [{:active system-theme?}])} [:i.mode-system {:class (when color-accent "radix")}] [:strong (t :settings.general/theme-system)]]]]
(row-with-button-action {:left-label (t :theme/switch-to switch-theme-label)
:-for "toggle_theme"
:action pick-theme
:desc (ui/render-keyboard-shortcut
(shortcut-helper/gen-shortcut-seq :ui/toggle-theme)
:shortcut-id :ui/toggle-theme)})))
(hsx/defc accent-color-row
[_in-modal?]
(let [_preferred-language (rfx/use-sub [:preferred-language])
color-accent (rfx/use-sub [:ui/radix-color])
color-label (fn [color]
(case color
:none [:p {:style {:max-width "300px"}}
(t :settings.general/accent-color-none-desc)]
:logseq (t :settings.general/accent-color-logseq)
:tomato (t :color/tomato)
:red (t :color/red)
:crimson (t :color/crimson)
:pink (t :color/pink)
:plum (t :color/plum)
:purple (t :color/purple)
:violet (t :color/violet)
:indigo (t :color/indigo)
:blue (t :color/blue)
:cyan (t :color/cyan)
:teal (t :color/teal)
:green (t :color/green)
:grass (t :color/grass)
:orange (t :color/orange)))
pick-theme [:div.cp__accent-colors-list-wrap
{:class (if _in-modal? "as-modal-picker" "")}
(for [color (concat [:none :logseq] colors/color-list)
:let [active? (= color color-accent)
none? (= color :none)]]
[:div.flex.items-center
{:key (name color)}
(ui/tooltip
(shui/button
{:class "w-5 h-5 px-1 !rounded-full flex justify-center items-center transition ease-in duration-100 hover:cursor-pointer hover:opacity-100"
:auto-focus (and _in-modal? active?)
:style {:background-color (colors/variable color :09)
:outline-color (colors/variable color (if active? :07 :06))
:outline-width (if active? "4px" "1px")
:outline-style :solid
:opacity (if active? 1 0.5)}
:variant :text
:on-click (fn [_e] (state/set-color-accent! color))}
[:strong
{:class (if none? "h-0.5 w-full bg-red-700"
"w-2 h-2 !rounded-full transition ease-in duration-100")
:style {:background-color (if-not none? (str "var(--rx-" (name color) "-07)") "")
:opacity (if (or none? active?) 1 0)}}])
(color-label color))])]]
[:div
(row-with-button-action
{:left-label (t :settings.general/accent-color)
:-for "toggle_radix_theme"
:desc (when-not _in-modal?
[:span.pl-6 (ui/render-keyboard-shortcut
(shortcut-helper/gen-shortcut-seq :ui/customize-appearance)
:shortcut-id :ui/customize-appearance)])
:stretch (boolean _in-modal?)
:action pick-theme})
[:div.text-sm.opacity-50.mt-1
(t :settings.general/accent-color-alert)]]))
(hsx/defc appearance
[]
(let [_config (rfx/use-sub [:config])
editor-font (rfx/use-sub [:ui/editor-font])
wide-mode? (rfx/use-sub [:ui/wide-mode?])]
[:div#appearance_settings.cp__settings-appearance-dialog-inner
(theme-modes-row t)
(editor-font-family-row t editor-font)
(toggle-wide-mode-row t wide-mode?)
(show-brackets-row t (state/show-brackets?))
(accent-color-row true)]))
(defn date-format-row [t preferred-date-format]
[:div.it.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-:div.it.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-center
[:label.block.text-sm.font-medium.leading-5.opacity-70
{:for "custom_date_format"}
(t :settings.editor/custom-date-format)]
[:div.mt-1.sm:mt-0.sm:col-span-2
[:div.max-w-lg.rounded-md
[:select.form-select.is-small
{:value preferred-date-format
:on-change (fn [e]
(let [format (util/evalue e)]
(when-not (string/blank? format)
(p/let [journal-class (state/<invoke-db-worker :thread-api/pull
(state/get-current-repo)
[:block/uuid]
:logseq.class/Journal)]
(property-handler/set-block-property! (:block/uuid journal-class)
:logseq.property.journal/title-format
format)
(state/set-date-formatter! (state/get-current-repo) format)
(notification/show! (t :settings.general/refresh-required-feedback)))
(shui/dialog-close-all!))))}
(for [format (sort (date/journal-title-formatters))]
[:option {:key format} format])]]]])
(defn outdenting-row [t logical-outdenting?]
(toggle "preferred_outdenting"
[(t :settings.editor/preferred-outdenting)
(ui/tooltip [:span.flex.px-2 (svg/info)]
(outdenting-hint) {:content-props {:side "right"}})]
logical-outdenting?
config-handler/toggle-logical-outdenting!))
(defn showing-full-blocks [t show-full-blocks?]
(toggle "show_full_blocks"
(t :settings.editor/show-full-blocks)
show-full-blocks?
config-handler/toggle-show-full-blocks!))
(defn preferred-pasting-file [t preferred-pasting-file?]
(toggle "preferred_pasting_file"
[(t :settings.editor/preferred-pasting-file)
(ui/tooltip [:span.flex.px-2 (svg/info)]
[:span.block.w-64 (t :settings.editor/preferred-pasting-file-hint)])]
preferred-pasting-file?
config-handler/toggle-preferred-pasting-file!))
(defn auto-expand-row [t auto-expand-block-refs?]
(toggle "auto_expand_block_refs"
[(t :settings.editor/auto-expand-block-refs)
(ui/tooltip [:span.flex.px-2 (svg/info)]
(auto-expand-hint))]
auto-expand-block-refs?
config-handler/toggle-auto-expand-block-refs!))
(defn tooltip-row [t enable-tooltip?]
(toggle "enable_tooltip"
(t :settings.editor/enable-tooltip)
enable-tooltip?
(fn []
(config-handler/toggle-ui-enable-tooltip!))))
(defn shortcut-tooltip-row [t enable-shortcut-tooltip?]
(toggle "enable_tooltip"
(t :settings.editor/enable-shortcut-tooltip)
enable-shortcut-tooltip?
(fn []
(state/toggle-shortcut-tooltip!))))
(defn update-home-page
[event]
(let [value (util/evalue event)]
(cond
(string/blank? value)
(let [home (get (state/get-config) :default-home {})
new-home (dissoc home :page)]
(p/do!
(config-handler/set-config! :default-home new-home)
(notification/show! (t :settings.features/home-default-page-update-success) :success)))
:else
;; FIXME: home page should be db id instead of page name
(p/let [page (state/<invoke-db-worker :thread-api/pull
(state/get-current-repo)
[:db/id]
[:block/name (util/page-name-sanity-lc value)])]
(if page
(let [home (get (state/get-config) :default-home {})
new-home (assoc home :page value)]
(config-handler/set-config! :default-home new-home)
(notification/show! (t :settings.features/home-default-page-update-success) :success))
(notification/show! (t :settings.features/page-not-found value) :warning))))))
(defn enable-all-pages-public-row [t enable-all-pages-public?]
(toggle "all pages public"
(t :settings.editor/enable-all-pages-public)
enable-all-pages-public?
(fn []
(let [value (not enable-all-pages-public?)]
(config-handler/set-config! :publishing/all-pages-public? value)))))
(defn usage-diagnostics-row [t instrument-disabled?]
(toggle "usage-diagnostics"
(t :settings.advanced/disable-sentry)
(not instrument-disabled?)
(fn [] (instrument/disable-instrument
(not instrument-disabled?)))
[:span.text-sm.opacity-50 (t :settings.advanced/disable-sentry-desc)]))
;; (defn clear-cache-row [t]
;; (row-with-button-action {:left-label "Clear cache"
;; :button-label "Clear"
;; :on-click #(state/pub-event! [:graph/clear-cache!])
;; :-for "clear_cache"}))
(defn version-row [t version]
(row-with-button-action {:left-label (t :settings.general/current-version)
:action (app-updater version)
:-for "current-version"}))
(defn developer-mode-row [t developer-mode?]
(toggle "developer_mode"
(t :settings.advanced/developer-mode)
developer-mode?
(fn []
(let [mode (not developer-mode?)]
(state/set-developer-mode! mode)))
[:div.text-sm.opacity-50 (t :settings.advanced/developer-mode-desc)]))
(defn semantic-search-setting-supported?
[{:keys [platform arch]}]
(and (= "darwin" platform)
(= "arm64" arch)))
(defn semantic-search-row [t enabled?]
(toggle "semantic_search"
(t :settings.features/semantic-search)
enabled?
(fn []
(let [enabled?' (not enabled?)]
(if enabled?'
(-> (ipc/ipc :userAppCfgs :feature/enable-semantic-search? true)
(p/then (fn [_]
(state/set-state! [:electron/user-cfgs :feature/enable-semantic-search?] true)))
(p/catch (fn [_error]
(notification/show!
(t :settings.features/semantic-search-python3-missing)
:warning))))
(do
(state/set-state! [:electron/user-cfgs :feature/enable-semantic-search?] false)
(ipc/ipc :userAppCfgs :feature/enable-semantic-search? false)))))
[:div.text-sm.opacity-50 (t :settings.features/semantic-search-desc)]))
(hsx/defc plugin-enabled-switcher
[t]
(let [value (state/lsp-enabled?-or-theme)
[on? set-on?] (hooks/use-state value)
on-toggle #(let [v (not on?)]
(set-on? v)
(storage/set ::storage-spec/lsp-core-enabled v))]
[:div.flex.items-center.gap-2
(ui/toggle on? on-toggle true)
(when (util/electron?)
(when (not= (boolean value) on?)
(ui/button (t :plugin/restart)
:on-click #(js/logseq.api.relaunch)
:small? true :intent "logseq")))]))
(hsx/defc http-server-enabled-switcher
[t]
(let [[value _] (hooks/use-state (boolean (storage/get ::storage-spec/http-server-enabled)))
[on? set-on?] (hooks/use-state value)
on-toggle #(let [v (not on?)]
(set-on? v)
(storage/set ::storage-spec/http-server-enabled v))]
[:div.flex.items-center.gap-2
(ui/toggle on? on-toggle true)
(when (not= (boolean value) on?)
(ui/button (t :plugin/restart)
:on-click #(js/logseq.api.relaunch)
:small? true :intent "logseq"))]))
(hsx/defc flashcards-enabled-switcher
[enable-flashcards?]
(ui/toggle enable-flashcards?
(fn []
(let [value (not enable-flashcards?)]
(config-handler/set-config! :feature/enable-flashcards? value)))
true))
(hsx/defc db-worker-server-settings-container
"Connects a web/mobile client to a self-hosted db-worker-node server (see
frontend.persist-db/remote-runtime?). Electron manages its own local
daemon instead and doesn't need this. Reconnecting or disconnecting
requires a reload since frontend.persist-db/<start-runtime! only decides
the transport once at boot."
[]
(let [current-url (config/get-server-base-url)
connected? (and (seq current-url) (seq (config/get-server-token)))
[url set-url!] (hooks/use-state (or current-url ""))
[username set-username!] (hooks/use-state "")
[password set-password!] (hooks/use-state "")
[connecting? set-connecting!] (hooks/use-state false)
disconnect! (fn []
(config/clear-server-config!)
(notification/show! "Disconnected. Reloading..." :success)
(js/setTimeout #(js/location.reload) 500))
connect! (fn []
(let [trimmed-url (string/trim url)]
(cond
(string/blank? trimmed-url)
(notification/show! "Server URL is required." :error)
(not (config/valid-sync-server-url? trimmed-url))
(notification/show! "Enter a valid http(s) URL." :error)
(or (string/blank? username) (string/blank? password))
(notification/show! "Username and password are required." :error)
:else
(do
(set-connecting! true)
(-> (remote/login! {:base-url trimmed-url
:username username
:password password
:device-label (str (.-platform js/navigator))})
(p/then (fn [{:keys [token]}]
(config/set-server-base-url! trimmed-url)
(config/set-server-token! token)
(notification/show! "Connected. Reloading..." :success)
(js/setTimeout #(js/location.reload) 500)))
(p/catch (fn [error]
(set-connecting! false)
(notification/show!
(str "Failed to connect: " (or (.-message error) error))
:error))))))))]
[:div.cp__settings-db-worker-server-cnt
[:h1.mb-2.text-2xl.font-bold "Server"]
[:div.p-2
[:p.text-sm.opacity-70.mb-4
"Connect this client to a self-hosted Logseq server (db-worker-node) instead of storing graphs locally in the browser."]
(if connected?
[:div
[:p [:strong "Connected to: "] current-url]
[:p.pt-2
(shui/button
{:size :sm :variant :outline :on-click disconnect!}
"Disconnect")]]
[:div
[:p
[:label [:strong "Server URL"]
[:input.form-input.is-small
{:value url
:placeholder "https://your-server.example:PORT"
:style {:width "100%"}
:on-change #(set-url! (util/evalue %))}]]]
[:p.pt-2
[:label [:strong "Username"]
[:input.form-input.is-small
{:value username
:style {:width "100%"}
:on-change #(set-username! (util/evalue %))}]]]
[:p.pt-2
[:label [:strong "Password"]
[:input.form-input.is-small
{:type "password"
:value password
:style {:width "100%"}
:on-change #(set-password! (util/evalue %))}]]]
[:p.pt-2
(shui/button
{:size :sm :disabled connecting? :on-click connect!}
(if connecting? "Connecting..." "Connect"))]])]]))
(hsx/defc db-worker-server-button
[]
(let [current-url (config/get-server-base-url)]
(ui/button [:span.flex.items-center
[:span.pr-1 (if (seq current-url) current-url "Connect to server")]
(ui/icon "edit")]
:class "text-sm"
:on-click #(state/pub-event! [:go/db-worker-server-settings]))))
(defn db-worker-server-row []
(row-with-button-action
{:left-label "Server"
:action (db-worker-server-button)}))
(hsx/defc publish-server-url-settings-container
[]
(let [current-url (config/get-custom-publish-server-url)
[url set-url!] (hooks/use-state (or current-url ""))
reset-url! (fn []
(config/set-custom-publish-server-url! nil)
(set-url! "")
(notification/show! (t :settings-page/publish-server-url-cleared) :success))]
[:div.cp__settings-publish-server-cnt
[:h1.mb-2.text-2xl.font-bold (t :settings-page/publish-server-url)]
[:div.p-2
[:p.text-sm.opacity-70.mb-4 (t :settings-page/publish-server-url-desc)]
[:p
[:label
[:strong "URL"]
[:input.form-input.is-small
{:value url
:placeholder config/default-publish-api-base
:style {:width "100%"}
:on-change #(set-url! (util/evalue %))}]]]
[:p.pt-2.flex.gap-2
(shui/button
{:size :sm
:on-click (fn []
(let [trimmed (string/trim url)]
(if (string/blank? trimmed)
(reset-url!)
(if-not (config/valid-publish-server-url? trimmed)
(notification/show! (t :settings.sync-server/url-invalid-error) :error)
(do
(config/set-custom-publish-server-url! trimmed)
(notification/show! (t :settings-page/publish-server-url-saved) :success))))))}
(t :ui/save))
(when (seq url)
(shui/button
{:size :sm
:variant :outline
:on-click (fn [] (reset-url!))}
(t :settings-page/publish-server-url-reset)))]]]))
(hsx/defc publish-server-url-button
[]
(let [current-url (config/get-custom-publish-server-url)]
(ui/button [:span.flex.items-center
[:span.pr-1
(if (seq current-url)
current-url
(t :settings-page/publish-server-url-default))]
(ui/icon "edit")]
:class "text-sm"
:on-click #(state/pub-event! [:go/publish-server-settings]))))
(defn publish-server-url-row []
(row-with-button-action
{:left-label (t :settings-page/publish-server-url)
:action (publish-server-url-button)}))
(hsx/defc user-proxy-settings
[{:keys [type protocol host port] :as agent-opts}]
(ui/button [:span.flex.items-center
[:span.pr-1
(case type
"system" (t :plugin.proxy/system)
"direct" (t :plugin.proxy/direct)
(and protocol host port (str protocol "://" host ":" port)))]
(ui/icon "edit")]
:class "text-sm"
:on-click #(state/pub-event! [:go/proxy-settings agent-opts])))
(defn plugin-system-switcher-row []
(row-with-button-action
{:left-label (t :settings.features/plugin-system)
:action (plugin-enabled-switcher t)}))
(defn http-server-switcher-row []
(row-with-button-action
{:left-label (t :server/title)
:action (http-server-enabled-switcher t)}))
(defn flashcards-switcher-row [enable-flashcards?]
(row-with-button-action
{:left-label (t :settings.features/enable-flashcards)
:action (flashcards-enabled-switcher enable-flashcards?)}))
(hsx/defc markdown-mirror-row
[t]
(let [repo (state/get-current-repo)
repo-config (rfx/use-sub [:config repo])
repo-config (when repo repo-config)
enabled? (true? (:feature/markdown-mirror? repo-config))
*regenerating? (hooks/use-memo #(atom false) [])
[regenerating?] (hooks/use-atom *regenerating?)
regenerate! (fn []
(when (and repo @state/*db-worker (not @*regenerating?))
(reset! *regenerating? true)
(-> (state/<invoke-db-worker :thread-api/markdown-mirror-regenerate repo)
(p/then (fn [_]
(notification/show!
(t :settings.features/markdown-mirror-regenerate-success)
:success)))
(p/catch (fn [error]
(log/error :markdown-mirror/regenerate-failed
{:repo repo
:error error})
(notification/show!
(t :settings.features/markdown-mirror-regenerate-error (str error))
:error)))
(p/finally #(reset! *regenerating? false)))))]
(toggle
"markdown-mirror"
(t :settings.features/markdown-mirror)
enabled?
#(let [next-enabled? (not enabled?)
repo (state/get-current-repo)]
(config-handler/set-config! :feature/markdown-mirror? next-enabled?)
(when (and repo @state/*db-worker)
(-> (state/<invoke-db-worker :thread-api/markdown-mirror-set-enabled repo next-enabled?)
(p/catch (fn [error]
(log/error :markdown-mirror/settings-sync-failed
{:repo repo
:error error}))))))
[:div.flex.items-center.gap-2.flex-wrap
[:span.text-sm.opacity-50 (t :settings.features/markdown-mirror-desc)]
(ui/button
(t :settings.features/markdown-mirror-regenerate)
:icon "refresh"
:class "text-sm"
:disabled regenerating?
:on-click regenerate!)])))
(defn https-user-agent-row [agent-opts]
(row-with-button-action
{:left-label (t :settings.advanced/network-proxy)
:action (user-proxy-settings agent-opts)}))
(hsx/defc auto-chmod-row
[t]
(let [enabled? (if (= nil (rfx/use-sub [:electron/user-cfgs :feature/enable-automatic-chmod?]))
true
(rfx/use-sub [:electron/user-cfgs :feature/enable-automatic-chmod?]))]
(toggle
"automatic-chmod"
(t :settings.advanced/auto-chmod)
enabled?
#(do
(state/set-state! [:electron/user-cfgs :feature/enable-automatic-chmod?] (not enabled?))
(ipc/ipc :userAppCfgs :feature/enable-automatic-chmod? (not enabled?)))
[:span.text-sm.opacity-50 (t :settings.advanced/auto-chmod-desc)])))
(hsx/defc native-titlebar-row
[t]
(let [enabled? (rfx/use-sub [:electron/user-cfgs :window/native-titlebar?])]
(toggle
"native-titlebar"
(t :settings.general/native-titlebar)
enabled?
#(when (js/confirm (t :ui/relaunch-confirm))
(state/set-state! [:electron/user-cfgs :window/native-titlebar?] (not enabled?))
(ipc/ipc :userAppCfgs :window/native-titlebar? (not enabled?))
(js/logseq.api.relaunch))
[:span.text-sm.opacity-50 (t :settings.general/native-titlebar-desc)])))
(hsx/defc settings-general
[current-repo]
(let [preferred-language (rfx/use-sub [:preferred-language])
show-radix-themes? true
editor-font (rfx/use-sub [:ui/editor-font])]
[:div.panel-wrap.is-general
(version-row t fv/version)
(language-row t preferred-language)
(theme-modes-row t)
(editor-font-family-row t editor-font)
(when (and (util/electron?) (not util/mac?)) (native-titlebar-row t))
(when show-radix-themes? (accent-color-row false))
(when (config/global-config-enabled?) (edit-global-config-edn))
(when current-repo (edit-config-edn))
(when current-repo (edit-custom-css))
(when (and current-repo (util/electron?)) (edit-export-css))]))
(hsx/defc settings-editor
[]
(let [_config (rfx/use-sub [:config])
preferred-date-format (state/get-date-formatter)
enable-all-pages-public? (state/all-pages-public?)
logical-outdenting? (state/logical-outdenting?)
show-full-blocks? (state/show-full-blocks?)
preferred-pasting-file? (state/preferred-pasting-file?)
auto-expand-block-refs? (state/auto-expand-block-refs?)
enable-tooltip? (state/enable-tooltip?)
enable-shortcut-tooltip? (rfx/use-sub [:ui/shortcut-tooltip?])
show-brackets? (state/show-brackets?)
wide-mode? (rfx/use-sub [:ui/wide-mode?])]
[:div.panel-wrap.is-editor
(date-format-row t preferred-date-format)
(show-brackets-row t show-brackets?)
(toggle-wide-mode-row t wide-mode?)
(when (util/electron?) (switch-spell-check-row t))
(outdenting-row t logical-outdenting?)
(showing-full-blocks t show-full-blocks?)
(preferred-pasting-file t preferred-pasting-file?)
(auto-expand-row t auto-expand-block-refs?)
(when-not (or (util/mobile?) (mobile-util/native-platform?))
(shortcut-tooltip-row t enable-shortcut-tooltip?))
(when-not (or (util/mobile?) (mobile-util/native-platform?))
(tooltip-row t enable-tooltip?))
(enable-all-pages-public-row t enable-all-pages-public?)]))
(hsx/defc settings-advanced
[]
(let [instrument-disabled? (rfx/use-sub [:instrument/disabled?])
developer-mode? (rfx/use-sub [:ui/developer-mode?])
https-agent-opts (rfx/use-sub [:electron/user-cfgs :settings/agent])]
[:div.panel-wrap.is-advanced
(when (and (or util/mac? util/win32?) (util/electron?)) (app-auto-update-row t))
(usage-diagnostics-row t instrument-disabled?)
(when-not (mobile-util/native-platform?) (developer-mode-row t developer-mode?))
(when-not (util/electron?) (db-worker-server-row))
(publish-server-url-row)
(when (util/electron?) (https-user-agent-row https-agent-opts))
(when (util/electron?) (auto-chmod-row t))
;; (clear-cache-row t)
;; (ui/admonition
;; :warning
;; [:p "Clearing the cache will discard open graphs. You will lose unsaved changes."])
]))
(hsx/defc settings-account-usage-description [pro-account? graph-usage]
(let [count-usage (count graph-usage)
count-limit (if pro-account? 10 1)
count-percent (js/Math.round (/ count-usage count-limit 0.01))
storage-usage (->> (map :used-gbs graph-usage)
(reduce + 0))
storage-usage-formatted (cond
(zero? storage-usage) "0.0"
(< storage-usage 0.01) "< 0.01"
:else (gstring/format "%.2f" storage-usage))
;; TODO: check logic on this. What are the rules around storage limits?
;; do we, and should we be able to, give individual users more storage?
;; should that be on a per graph or per user basis?
default-storage-limit (if pro-account? 10 0.05)
storage-limit (->> (range 0 count-limit)
(map #(get-in graph-usage [% :limit-gbs] default-storage-limit))
(reduce + 0))
storage-percent (/ storage-usage storage-limit 0.01)
storage-percent-formatted (gstring/format "%.1f" storage-percent)
count-percent-node [:strong.text-white (str count-percent "%")]
storage-percent-node [:strong.text-white (str storage-percent-formatted "%")]
synced-graphs-summary (interpolate-rich-text-node
(t :settings.account/synced-graphs)
[count-usage count-limit count-percent-node])
storage-usage-summary (interpolate-rich-text-node
(t :settings.account/storage-usage)
[storage-usage-formatted storage-limit storage-percent-node])]
[:div.text-sm
(if pro-account?
(locale-join-rich-text-node [synced-graphs-summary storage-usage-summary])
storage-usage-summary)]))
; storage-usage-formatted "GB of " storage-limit "GB total storage"
; [:strong.text-white " (" storage-percent-formatted "%)"]]))
(hsx/defc settings-account-usage-graphs [_pro-account? graph-usage]
(when (< 0 (count graph-usage))
[:div.grid.gap-3 {:style {:grid-template-columns (str "repeat(" (count graph-usage) ", 1fr)")}}
(for [{:keys [name used-percent]} graph-usage
:let [color (if (<= 100 used-percent) "bg-red-500" "bg-blue-500")]]
[:div.rounded-full.w-full.h-2 {:class "bg-black/50"
:tooltip name}
[:div.rounded-full.h-2 {:class color
:style {:width (str used-percent "%")
:min-width "0.5rem"
:max-width "100%"}}]])]))
(hsx/defc ^:large-vars/cleanup-todo settings-account
[]
(let [graph-usage []
auth-refresh-token (rfx/use-sub [:auth/refresh-token])
logged-in? (and (string? auth-refresh-token)
(not (string/blank? auth-refresh-token)))
user-info (rfx/use-sub [:user/info])
paid-user? (#{"active" "on_trial" "cancelled"} (:LemonStatus user-info))
gift-user? (some #{"pro"} (:UserGroups user-info))
pro-account? (or paid-user? gift-user?)
expiration-date (some-> user-info :LemonEndsAt date/parse-iso)
renewal-date (some-> user-info :LemonRenewsAt date/parse-iso)
has-subscribed? (some? (:LemonStatus user-info))]
[:div.panel-wrap.is-features.mb-8
[:div.mt-1.sm:mt-0.sm:col-span-2
(cond
logged-in?
[:div.grid.grid-cols-3.gap-8.pt-2
[:div (t :account/current-plan)]
[:div.col-span-2
[:div {:class "w-full bg-gray-500/10 rounded-lg p-4 flex flex-col gap-4"}
[:div.flex.gap-4.items-center
(if pro-account?
[:div.flex-1 (t :account/plan-pro)]
[:div.flex-1 (t :account/plan-free)])
(cond
has-subscribed?
(ui/button (t :account/manage-plan) {:class "p-1 h-8 justify-center"
:disabled true
:icon "upload"})
; :on-click user-handler/upgrade})
(not pro-account?)
(ui/button (t :account/upgrade-plan) {:class "p-1 h-8 justify-center"
:icon "upload"
:on-click user-handler/upgrade})
:else nil)]
(settings-account-usage-graphs pro-account? graph-usage)
(settings-account-usage-description pro-account? graph-usage)]]
(when has-subscribed?
[:<>
[:div (t :account/billing)]
[:div.col-span-2.flex.flex-col.gap-4
(cond
;; If there is no expiration date, print the renewal date
(and renewal-date (nil? expiration-date))
[:div
[:strong.font-semibold
(t :account/billing-next-date-label
(locale-format-date renewal-date))]]
;; If the expiration date is in the future, word it as such
(< (js/Date.) expiration-date)
[:div
[:strong.font-semibold
(t :account/billing-expires-on-label
(locale-format-date expiration-date))]]
;; Otherwise, ind
:else
[:div
[:strong.font-semibold
(t :account/billing-expired-on-label
(locale-format-date expiration-date))]])
[:div (ui/button (t :account/open-invoices) {:class "w-full h-8 p-1 justify-center"
:disabled true
:background "gray"
:icon "receipt"})]]])
[:div (t :account/profile)]
[:div.col-span-2.grid.grid-cols-2.gap-4
[:div.flex.flex-col.gap-2.box-border {:class "basis-1/2"}
[:label.text-sm.font-semibold (t :account/first-name)]
[:input.rounded.border.px-2.py-1.box-border {:class "border-blue-500 bg-black/25 w-full"}]]
[:div.flex.flex-col.gap-2 {:class "basis-1/2"}
[:label.text-sm.font-semibold (t :account/last-name)]
[:input.rounded.border.px-2.py-1.box-border {:class "border-blue-500 bg-black/25 w-full"}]]
[:div.flex-1.flex.flex-col.gap-2.col-span-2
[:label.text-sm.font-semibold (t :account/username)]
[:div.rounded.border.px-2.py-1.box-border
{:class "border-blue-500 bg-black/25"}
(email-component/email-address {:email (user-handler/email)})]]]
[:div (t :account/authentication)]
[:div.col-span-2
[:div.grid.grid-cols-2.gap-4
[:div (ui/button (t :ui/logout) {:class "p-1 h-8 justify-center w-full"
:background "gray"
:icon "logout"
:on-click user-handler/logout})]
[:div (ui/button (t :account/reset-password) {:class "p-1 h-8 justify-center w-full"
:disabled true
:background "gray"
:icon "key"
:on-click user-handler/logout})]
[:div.col-span-2 (ui/button (t :account/delete-account) {:class "p-1 h-8 justify-center w-full"
:disabled true
:background "red"})]]]]
(not logged-in?)
[:div.grid.grid-cols-3.gap-8.pt-2
[:div (t :account/authentication)]
[:div.col-span-2.flex.flex-wrap.gap-4
[:div.w-full.text-white (t :account/benefits-desc)]
[:div.flex-1
(ui/button (t :account/sign-up) {:class "h-8 w-full text-center justify-center"
:on-click (fn []
(state/close-settings!)
(state/pub-event! [:user/login]))})]
[:div.flex-1
(ui/button (t :ui/login) {:icon "login"
:class "h-8 w-full text-center justify-center"
:background "gray"
:on-click (fn []
(state/close-settings!)
(state/pub-event! [:user/login]))})]]
[:div.col-span-3.flex.flex-col.gap-4 {:class "bg-black/20 p-4 rounded-lg"}
[:div.flex.w-full.items-center
[:div {:class "w-1/2 text-lg"}
(interpolate-rich-text-node
(t :account/discover-sync-desc)
[[:strong {:class "text-white/80"} "Logseq Sync"]])]
[:div {:class "w-1/2 bg-gradient-to-r from-white/10 to-transparent p-3 rounded-lg flex items-center gap-2 px-5 ml-5"}
[:div.w-3.h-3.rounded-full.bg-green-500]
(t :account/synced-status)]]
[:div.flex.w-full.gap-4
[:div {:class "w-1/2 bg-black/50 rounded-lg p-4 pt-10 relative flex flex-col gap-4"}
[:div.absolute.top-0.left-4.bg-gray-700.uppercase.px-2.py-1.rounded-b-lg.font-bold.text-xs (t :account/plan-free)]
[:div
[:strong.text-white.text-xl.font-normal "$0"]]
[:div.text-white.font-bold {:class "h-[2.5rem] "} (t :account/plan-free-summary)]
[:ul.text-xs.list-none.m-0.flex.flex-col.gap-0.5
[:li (t :account/unlimited-unsynced-graphs)]
[:li (t :account/free-plan-sync-limit)]
[:li (t :account/no-asset-syncing)]
[:li (t :account/core-features)]]]
[:div {:class "w-1/2 bg-black/50 rounded-lg p-4 pt-10 relative flex flex-col gap-4"}
[:div.absolute.top-0.left-4.bg-blue-700.uppercase.px-2.py-1.rounded-b-lg.font-bold.text-xs (t :account/plan-pro)]
[:div
[:strong.text-white.text-xl.font-normal "$10"]
[:span.text-xs.font-base {:class "ml-0.5"} " / " (t :account/month)]]
[:div.text-white.font-bold {:class "h-[2.5rem]"} (t :account/plan-pro-summary)]
[:ul.text-xs.list-none.m-0.flex.flex-col.gap-0.5
[:li (t :account/unlimited-unsynced-graphs)]
[:li (t :account/pro-plan-sync-limit)]
[:li (t :account/sync-assets-limit)]
[:li (t :account/early-access-alpha-beta)]
[:li (t :account/upcoming-cloud-features)]]]]]])]]))
(hsx/defc settings-features
[]
(let [config (rfx/use-sub [:config])
default-home-page (get-in (state/config-for-repo config
(state/get-current-repo))
[:default-home :page] "")
app-user-cfgs (rfx/use-sub [:electron/user-cfgs])
app-base-info (rfx/use-sub [:electron/app-base-info])
current-repo (state/get-current-repo)
enable-flashcards? (state/enable-flashcards? current-repo)
semantic-search-enabled? (true? (:feature/enable-semantic-search? app-user-cfgs))
logged-in? (user-handler/logged-in?)]
[:div.panel-wrap.is-features.mb-8
[:div.it.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-center
[:label.block.text-sm.font-medium.leading-5.opacity-70
{:for "default page"}
(t :settings.features/home-default-page)]
[:div.mt-1.sm:mt-0.sm:col-span-2
[:div.max-w-lg.rounded-md.sm:max-w-xs
[:input#home-default-page.form-input.is-small.transition.duration-150.ease-in-out
{:default-value default-home-page
:on-blur update-home-page
:on-key-press (fn [e]
(when (= "Enter" (util/ekey e))
(update-home-page e)))}]]]]
(when (and web-platform? config/feature-plugin-system-on?)
(plugin-system-switcher-row))
(when (util/electron?)
(http-server-switcher-row))
(when (util/electron?)
(markdown-mirror-row t))
(flashcards-switcher-row enable-flashcards?)
(when (semantic-search-setting-supported? app-base-info)
(semantic-search-row t semantic-search-enabled?))
(when-not web-platform?
[:div.mt-1.sm:mt-0.sm:col-span-2
[:hr]
(if logged-in?
[:div
(email-component/email-address {:email (user-handler/email)})
[:p (ui/button (t :ui/logout) {:class "p-1"
:icon "logout"
:on-click user-handler/logout})]]
[:div
(ui/button (t :ui/login) {:class "p-1"
:icon "login"
:on-click (fn []
(state/close-settings!)
(state/pub-event! [:user/login]))})
[:p.text-sm.opacity-50 (t :settings.features/login-prompt)]])])]))
(def DEFAULT-ACTIVE-TAB-STATE (if config/ENABLE-SETTINGS-ACCOUNT-TAB [:account :account] [:general :general]))
(hsx/defc settings-effect
[active]
(hooks/use-effect!
(fn []
(let [active (and (sequential? active) (name (first active)))
^js ds (.-dataset js/document.body)]
(if active
(set! (.-settingsTab ds) active)
(js-delete ds "settingsTab"))
#(js-delete ds "settingsTab")))
[active])
[:<>])
(hsx/defc mcp-server-row
[t]
(let [server-state (rfx/use-sub [:electron/server])
[checked set-checked!] (hooks/use-state false)]
(hooks/use-effect!
(fn []
(let [initial (:mcp-enabled? server-state)]
(set-checked! initial)))
[server-state])
(let [on-toggle (fn []
(let [new-val (not checked)]
(set-checked! new-val)
;; Enable HTTP server to simplify starting MCP
(when (and new-val (not (storage/get ::storage-spec/http-server-enabled)))
(storage/set ::storage-spec/http-server-enabled true))
(-> (ipc/ipc :server/set-config {:mcp-enabled? new-val})
;; Don't start server if it's not running
(p/then #(when (= "running" (state/get-state [:electron/server :status]))
(p/let [_ (p/delay 1000)]
(ipc/ipc :server/do :restart))))
(p/catch #(notification/show! (str %) :error)))))]
(toggle "mcp-server"
(t :settings.ai/enable-mcp-server)
checked
on-toggle
[:span.text-sm.opacity-50
(t :settings.ai/enable-mcp-server-desc)]))))
(hsx/defc settings-ai
[]
[:div.panel-wrap
(when (util/electron?)
(mcp-server-row t))])
(hsx/defc ^:large-vars/cleanup-todo settings
[_active-tab]
(let [current-repo (rfx/use-sub [:git/current-repo])
_preferred-language (rfx/use-sub [:preferred-language])
_installed-plugins (rfx/use-sub [:plugin/installed-plugins])
plugins-of-settings (and config/lsp-enabled? (seq (plugin-handler/get-enabled-plugins-if-setting-schema)))
*active (hooks/use-memo #(atom DEFAULT-ACTIVE-TAB-STATE) [])
[active] (hooks/use-atom *active)]
(hooks/use-effect!
(fn []
(state/load-app-user-cfgs)
(state/load-electron-app-base-info)
(let [active-tab (if (and (= _active-tab :ai) (not (util/electron?)))
:advanced
_active-tab)]
(when (keyword? active-tab)
(reset! *active [active-tab nil])))
#(state/close-settings!))
[])
[:div#settings.cp__settings-main
(settings-effect active)
[:div.cp__settings-inner
[:aside.md:w-64 {:style {:min-width "10rem"}}
[:header.cp__settings-header
[:h1.cp__settings-modal-title (t :nav/settings)]]
[:ul.settings-menu
(for [[label text icon]
[(when config/ENABLE-SETTINGS-ACCOUNT-TAB
[:account (t :settings/account) (ui/icon "user-circle")])
[:general (t :settings/general) (ui/icon "adjustments")]
[:editor (t :settings/editor) (ui/icon "writing")]
[:keymap (t :settings/keymap) (ui/icon "keyboard")]
(when (util/electron?)
[:ai (t :settings/ai) (ui/icon "wand")])
[:advanced (t :settings/advanced) (ui/icon "bulb")]
[:features (t :settings/features) (ui/icon "app-feature")]
(when plugins-of-settings
[:plugins-setting (t :settings/plugins) (ui/icon "puzzle")])]]
(when label
[:li.settings-menu-item
{:key (name label)
:data-id (name label)
:class (classnames [{:active (= label (first active))}])
:on-click (fn []
(if (= label :plugins-setting)
(state/pub-event! [:go/plugins-settings (:id (first plugins-of-settings))])
(reset! *active [label (first active)])))}
[:button.flex.items-center.settings-menu-link
{:type "button"}
icon
[:strong text]]]))]]
[:article
[:header.cp__settings-header
[:h1.cp__settings-category-title (t (keyword (str "settings/" (name (first 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)
:account
(settings-account)
:general
(settings-general current-repo)
:editor
(settings-editor)
:keymap
(shortcut/shortcut-keymap-x)
:assets
(assets/settings-content)
:advanced
(settings-advanced)
:features
(settings-features)
:ai
(if (util/electron?)
(settings-ai)
(settings-advanced))
nil)]]]))