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>
1357 lines
60 KiB
Clojure
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)]]]))
|