settings.cljs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507
  1. (ns frontend.components.settings
  2. (:require [clojure.string :as string]
  3. [frontend.components.svg :as svg]
  4. [frontend.config :as config]
  5. [frontend.context.i18n :as i18n]
  6. [frontend.date :as date]
  7. [frontend.dicts :as dicts]
  8. [frontend.handler :as handler]
  9. [frontend.handler.config :as config-handler]
  10. [frontend.handler.notification :as notification]
  11. [frontend.handler.page :as page-handler]
  12. [frontend.handler.route :as route-handler]
  13. [frontend.handler.ui :as ui-handler]
  14. [frontend.handler.user :as user-handler]
  15. [frontend.modules.instrumentation.core :as instrument]
  16. [frontend.state :as state]
  17. [frontend.ui :as ui]
  18. [frontend.util :refer [classnames] :as util]
  19. [frontend.version :refer [version]]
  20. [goog.object :as gobj]
  21. [reitit.frontend.easy :as rfe]
  22. [rum.core :as rum]))
  23. (rum/defcs set-email < (rum/local "" ::email)
  24. [state]
  25. (let [email (get state ::email)]
  26. [:div.p-8.flex.items-center.justify-center
  27. [:div.w-full.mx-auto
  28. [:div
  29. [:div
  30. [:h1.title.mb-1
  31. "Your email address:"]
  32. [:div.mt-2.mb-4.relative.rounded-md.max-w-xs
  33. [:input#.form-input.is-small
  34. {:autoFocus true
  35. :on-change (fn [e]
  36. (reset! email (util/evalue e)))}]]]]
  37. (ui/button
  38. "Submit"
  39. :on-click
  40. (fn []
  41. (user-handler/set-email! @email)))
  42. [:hr]
  43. [:span.pl-1.opacity-70 "Git commit requires the email address."]]]))
  44. (rum/defcs set-cors < (rum/local "" ::cors)
  45. [state]
  46. (let [cors (get state ::cors)]
  47. [:div.p-8.flex.items-center.justify-center
  48. [:div.w-full.mx-auto
  49. [:div
  50. [:div
  51. [:h1.title.mb-1
  52. "Your cors address:"]
  53. [:div.mt-2.mb-4.relative.rounded-md.max-w-xs
  54. [:input#.form-input.is-small
  55. {:autoFocus true
  56. :on-change (fn [e]
  57. (reset! cors (util/evalue e)))}]]]]
  58. (ui/button
  59. "Submit"
  60. :on-click
  61. (fn []
  62. (user-handler/set-cors! @cors)))
  63. [:hr]
  64. [:span.pl-1.opacity-70 "Git commit requires the cors address."]]]))
  65. (defn toggle
  66. [label-for name state on-toggle & [detail-text]]
  67. [:div.it.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-start
  68. [:label.block.text-sm.font-medium.leading-5.opacity-70
  69. {:for label-for}
  70. name]
  71. [:div.mt-1.sm:mt-0.sm:col-span-2
  72. [:div.rounded-md
  73. {:style {:display "flex" :gap "1rem" :align-items "center"}}
  74. (ui/toggle state on-toggle true)
  75. detail-text]]])
  76. (rum/defcs app-updater < rum/reactive
  77. [state]
  78. (let [update-pending? (state/sub :electron/updater-pending?)
  79. {:keys [type payload]} (state/sub :electron/updater)]
  80. [:span.cp__settings-app-updater
  81. (ui/button
  82. (if update-pending? "Checking ..." "Check for updates")
  83. :class "text-sm p-1 mr-3"
  84. :disabled update-pending?
  85. :on-click #(js/window.apis.checkForUpdates false))
  86. (when-not (or update-pending?
  87. (string/blank? type))
  88. [:div.update-state
  89. (case type
  90. "update-not-available"
  91. [:p "😀 Your app is up-to-date!"]
  92. "update-available"
  93. (let [{:keys [name url]} payload]
  94. [:p (str "Found new release ")
  95. [:a.link
  96. {:on-click
  97. (fn [e]
  98. (js/window.apis.openExternal url)
  99. (util/stop e))}
  100. svg/external-link name " 🎉"]])
  101. "error"
  102. [:p "⚠️ Oops, Something Went Wrong!" [:br] " Please check out the "
  103. [:a.link
  104. {:on-click
  105. (fn [e]
  106. (js/window.apis.openExternal "https://github.com/logseq/logseq/releases")
  107. (util/stop e))}
  108. svg/external-link " release channel"]])])]))
  109. (rum/defc delete-account-confirm
  110. [close-fn]
  111. (rum/with-context [[t] i18n/*tongue-context*]
  112. [:div
  113. (ui/admonition
  114. :important
  115. [:p.text-gray-700 (t :user/delete-account-notice)])
  116. [:div.mt-5.sm:mt-4.sm:flex.sm:flex-row-reverse
  117. [:span.flex.w-full.rounded-md.sm:ml-3.sm:w-auto
  118. [:button.inline-flex.justify-center.w-full.rounded-md.border.border-transparent.px-4.py-2.bg-indigo-600.text-base.leading-6.font-medium.text-white.shadow-sm.hover:bg-indigo-500.focus:outline-none.focus:border-indigo-700.focus:shadow-outline-indigo.transition.ease-in-out.duration-150.sm:text-sm.sm:leading-5
  119. {:type "button"
  120. :on-click user-handler/delete-account!}
  121. (t :user/delete-account)]]
  122. [:span.mt-3.flex.w-full.rounded-md.sm:mt-0.sm:w-auto
  123. [:button.inline-flex.justify-center.w-full.rounded-md.border.border-gray-300.px-4.py-2.bg-white.text-base.leading-6.font-medium.text-gray-700.shadow-sm.hover:text-gray-500.focus:outline-none.focus:border-blue-300.focus:shadow-outline-blue.transition.ease-in-out.duration-150.sm:text-sm.sm:leading-5
  124. {:type "button"
  125. :on-click close-fn}
  126. "Cancel"]]]]))
  127. (rum/defc outdenting-hint
  128. []
  129. [:div.ui__modal-panel
  130. {:style {:box-shadow "0 4px 20px 4px rgba(0, 20, 60, .1), 0 4px 80px -8px rgba(0, 20, 60, .2)"}}
  131. [:div {:style {:margin "12px" :max-width "500px"}}
  132. [:p.text-sm
  133. "The left side shows outdenting with the default setting, and the right shows outdenting with logical outdenting enabled. "
  134. [:a.text-sm
  135. {:target "_blank" :href "https://discuss.logseq.com/t/whats-your-preferred-outdent-behavior-the-direct-one-or-the-logical-one/978"}
  136. "→ Learn more"]]
  137. [:img {:src "https://discuss.logseq.com/uploads/default/original/1X/e8ea82f63a5e01f6d21b5da827927f538f3277b9.gif"
  138. :width 500
  139. :height 500}]]])
  140. (defn edit-config-edn []
  141. (rum/with-context [[t] i18n/*tongue-context*]
  142. [:div.text-sm
  143. [:a {:href (rfe/href :file {:path (config/get-config-path)})
  144. :on-click #(js/setTimeout (fn [] (ui-handler/toggle-settings-modal!)))}
  145. (t :settings-page/edit-config-edn)]]))
  146. (defn show-brackets-row [t show-brackets?]
  147. [:div.it.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-start
  148. [:label.block.text-sm.font-medium.leading-5.opacity-70
  149. {:for "show_brackets"}
  150. (t :settings-page/show-brackets)]
  151. [:div
  152. [:div.rounded-md.sm:max-w-xs
  153. (ui/toggle show-brackets?
  154. config-handler/toggle-ui-show-brackets!
  155. true)]]
  156. [:div {:style {:text-align "right"}}
  157. ;; TODO: Fetch this shortcut from config.cljs so there's one source of truth
  158. (ui/keyboard-shortcut [:meta :c :meta :b])]])
  159. (defn language-row [t preferred-language]
  160. [:div.it.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-start
  161. [:label.block.text-sm.font-medium.leading-5.opacity-70
  162. {:for "preferred_language"}
  163. (t :language)]
  164. [:div.mt-1.sm:mt-0.sm:col-span-2
  165. [:div.max-w-lg.rounded-md
  166. [:select.form-select.is-small
  167. {:value preferred-language
  168. :on-change (fn [e]
  169. (let [lang-code (util/evalue e)]
  170. (state/set-preferred-language! lang-code)
  171. (ui-handler/re-render-root!)))}
  172. (for [language dicts/languages]
  173. (let [lang-code (name (:value language))
  174. lang-label (:label language)]
  175. [:option {:key lang-code :value lang-code} lang-label]))]]]])
  176. (defn theme-modes-row [t switch-theme system-theme? dark?]
  177. [:div.it.sm:grid.sm:grid-cols-3.sm:gap-4
  178. [:label.block.text-sm.font-medium.leading-5.opacity-70
  179. {:for "toggle_theme"}
  180. (t :right-side-bar/switch-theme (string/capitalize switch-theme))]
  181. [:div.flex.flex-row.mt-1.sm:mt-0.sm:col-span-1
  182. [:div.rounded-md.sm:max-w-xs
  183. [:ul.theme-modes-options
  184. [:li {:on-click (partial state/use-theme-mode! "light")
  185. :class (classnames [{:active (and (not system-theme?) (not dark?))}])} [:i.mode-light] [:strong "light"]]
  186. [:li {:on-click (partial state/use-theme-mode! "dark")
  187. :class (classnames [{:active (and (not system-theme?) dark?)}])} [:i.mode-dark] [:strong "dark"]]
  188. [:li {:on-click (partial state/use-theme-mode! "system")
  189. :class (classnames [{:active system-theme?}])} [:i.mode-system] [:strong "system"]]]]]
  190. [:div {:style {:text-align "right"}}
  191. (ui/keyboard-shortcut [:t :t])]])
  192. (defn file-format-row [t preferred-format]
  193. [:div.it.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-start
  194. [:label.block.text-sm.font-medium.leading-5.opacity-70
  195. {:for "preferred_format"}
  196. (t :settings-page/preferred-file-format)]
  197. [:div.mt-1.sm:mt-0.sm:col-span-2
  198. [:div.max-w-lg.rounded-md
  199. [:select.form-select.is-small
  200. {:value (name preferred-format)
  201. :on-change (fn [e]
  202. (let [format (-> (util/evalue e)
  203. (string/lower-case)
  204. keyword)]
  205. (user-handler/set-preferred-format! format)))}
  206. (for [format (map name [:org :markdown])]
  207. [:option {:key format :value format} (string/capitalize format)])]]]])
  208. (defn date-format-row [t preferred-date-format]
  209. [: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-start
  210. [:label.block.text-sm.font-medium.leading-5.opacity-70
  211. {:for "custom_date_format"}
  212. (t :settings-page/custom-date-format)]
  213. [:div.mt-1.sm:mt-0.sm:col-span-2
  214. [:div.max-w-lg.rounded-md
  215. [:select.form-select.is-small
  216. {:value preferred-date-format
  217. :on-change (fn [e]
  218. (let [format (util/evalue e)]
  219. (when-not (string/blank? format)
  220. (config-handler/set-config! :date-formatter format)
  221. (notification/show!
  222. [:div "You need to re-index your graph to make the change works"]
  223. :success)
  224. (state/close-modal!)
  225. (route-handler/redirect! {:to :repos}))))}
  226. (for [format (sort (date/journal-title-formatters))]
  227. [:option {:key format} format])]]]])
  228. (defn workflow-row [t preferred-workflow]
  229. [:div.it.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-start
  230. [:label.block.text-sm.font-medium.leading-5.opacity-70
  231. {:for "preferred_workflow"}
  232. (t :settings-page/preferred-workflow)]
  233. [:div.mt-1.sm:mt-0.sm:col-span-2
  234. [:div.max-w-lg.rounded-md
  235. [:select.form-select.is-small
  236. {:value (name preferred-workflow)
  237. :on-change (fn [e]
  238. (-> (util/evalue e)
  239. string/lower-case
  240. keyword
  241. (#(if (= % :now) :now :todo))
  242. user-handler/set-preferred-workflow!))}
  243. (for [workflow [:now :todo]]
  244. [:option {:key (name workflow) :value (name workflow)}
  245. (if (= workflow :now) "NOW/LATER" "TODO/DOING")])]]]])
  246. (defn outdenting-row [t logical-outdenting?]
  247. (toggle "preferred_outdenting"
  248. [(t :settings-page/preferred-outdenting)
  249. (ui/tippy {:html (outdenting-hint)
  250. :class "tippy-hover ml-2"
  251. :interactive true
  252. :disabled false}
  253. (svg/info))]
  254. logical-outdenting?
  255. config-handler/toggle-logical-outdenting!))
  256. (defn tooltip-row [t enable-tooltip?]
  257. (toggle "enable_tooltip"
  258. (t :settings-page/enable-tooltip)
  259. enable-tooltip?
  260. (fn []
  261. (config-handler/toggle-ui-enable-tooltip!))))
  262. (defn timetracking-row [t enable-timetracking?]
  263. (toggle "enable_timetracking"
  264. (t :settings-page/enable-timetracking)
  265. enable-timetracking?
  266. (fn []
  267. (let [value (not enable-timetracking?)]
  268. (config-handler/set-config! :feature/enable-timetracking? value)))))
  269. (defn update-home-page
  270. [event]
  271. (let [value (util/evalue event)]
  272. (cond
  273. (string/blank? value)
  274. (let [home (get (state/get-config) :default-home {})
  275. new-home (dissoc home :page)]
  276. (config-handler/set-config! :default-home new-home)
  277. (notification/show! "Home default page updated successfully!" :success))
  278. (page-handler/page-exists? (string/lower-case value))
  279. (let [home (get (state/get-config) :default-home {})
  280. new-home (assoc home :page value)]
  281. (config-handler/set-config! :default-home new-home)
  282. (notification/show! "Home default page updated successfully!" :success))
  283. :else
  284. (notification/show! (str "The page \"" value "\" doesn't exist yet. Please create that page first, and then try again.") :warning))))
  285. (defn journal-row [t enable-journals?]
  286. [(toggle "enable_journals"
  287. (t :settings-page/enable-journals)
  288. enable-journals?
  289. (fn []
  290. (let [value (not enable-journals?)]
  291. (config-handler/set-config! :feature/enable-journals? value))))
  292. (when (not enable-journals?)
  293. [:div.it.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-start
  294. [:label.block.text-sm.font-medium.leading-5.opacity-70
  295. {:for "default page"}
  296. (t :settings-page/home-default-page)]
  297. [:div.mt-1.sm:mt-0.sm:col-span-2
  298. [:div.max-w-lg.rounded-md.sm:max-w-xs
  299. [:input#home-default-page.form-input.is-small.transition.duration-150.ease-in-out
  300. {:default-value (state/sub-default-home-page)
  301. :on-blur update-home-page
  302. :on-key-press (fn [e]
  303. (when (= "Enter" (util/ekey e))
  304. (update-home-page e)))}]]]])])
  305. (defn encryption-row [t enable-encryption?]
  306. (toggle "enable_encryption"
  307. (t :settings-page/enable-encryption)
  308. enable-encryption?
  309. (fn []
  310. (let [value (not enable-encryption?)]
  311. (config-handler/set-config! :feature/enable-encryption? value)))))
  312. (defn keyboard-shortcuts-row [t]
  313. [:div.it.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-start
  314. [:label.block.text-sm.font-medium.leading-5.opacity-70
  315. {:for "customize_shortcuts"}
  316. (t :settings-page/customize-shortcuts)]
  317. [:div.mt-1.sm:mt-0.sm:col-span-2
  318. [:div
  319. (ui/button
  320. (t :settings-page/shortcut-settings)
  321. :class "text-sm p-1"
  322. :style {:margin-top "0px"}
  323. :on-click
  324. (fn []
  325. (state/close-settings!)
  326. (route-handler/redirect! {:to :shortcut})))]]])
  327. (defn auto-push-row [t current-repo enable-git-auto-push?]
  328. (when (string/starts-with? current-repo "https://")
  329. (toggle "enable_git_auto_push"
  330. "Enable Git auto push"
  331. enable-git-auto-push?
  332. (fn []
  333. (let [value (not enable-git-auto-push?)]
  334. (config-handler/set-config! :git-auto-push value))))))
  335. (defn usage-diagnostics-row [t instrument-disabled?]
  336. (toggle "usage-diagnostics"
  337. (t :settings-page/disable-sentry)
  338. (not instrument-disabled?)
  339. (fn [] (instrument/disable-instrument
  340. (not instrument-disabled?)))
  341. [:span.text-sm.opacity-50 "Logseq will never collect your local graph database or sell your data."]))
  342. (defn clear-cache-row [t]
  343. [:div.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-center.sm:pt-5
  344. [:label.block.text-sm.font-medium.leading-5.opacity-70
  345. {:for "clear_cache"}
  346. (t :settings-page/clear-cache)]
  347. [:div.mt-1.sm:mt-0.sm:col-span-2
  348. [:div.max-w-lg.rounded-md.sm:max-w-xs
  349. (ui/button
  350. (t :settings-page/clear)
  351. :class "text-sm p-1"
  352. :on-click handler/clear-cache!)]]])
  353. (defn version-row [t version]
  354. [:div.it.app-updater.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-start
  355. [:label.block.text-sm.font-medium.leading-5.opacity-70
  356. (t :settings-page/current-version)]
  357. [:div.wrap.sm:mt-0.sm:col-span-2
  358. (when (util/electron?) (app-updater))
  359. [:span.ver version]]])
  360. (defn developer-mode-row [t developer-mode?]
  361. (toggle "developer_mode"
  362. (t :settings-page/developer-mode)
  363. developer-mode?
  364. (fn []
  365. (let [mode (not developer-mode?)]
  366. (state/set-developer-mode! mode)
  367. (and mode (util/electron?)
  368. (if (js/confirm (t :developer-mode-alert))
  369. (js/logseq.api.relaunch)))))
  370. [:div.text-sm.opacity-50 (t :settings-page/developer-mode-desc)]))
  371. (rum/defcs settings < rum/reactive
  372. []
  373. (let [preferred-format (state/get-preferred-format)
  374. preferred-date-format (state/get-date-formatter)
  375. preferred-workflow (state/get-preferred-workflow)
  376. preferred-language (state/sub [:preferred-language])
  377. enable-timetracking? (state/enable-timetracking?)
  378. current-repo (state/get-current-repo)
  379. enable-journals? (state/enable-journals? current-repo)
  380. enable-encryption? (state/enable-encryption? current-repo)
  381. instrument-disabled? (state/sub :instrument/disabled?)
  382. logical-outdenting? (state/logical-outdenting?)
  383. enable-tooltip? (state/enable-tooltip?)
  384. enable-git-auto-push? (state/enable-git-auto-push? current-repo)
  385. enable-block-time? (state/enable-block-time?)
  386. show-brackets? (state/show-brackets?)
  387. github-token (state/sub [:me :access-token])
  388. cors-proxy (state/sub [:me :cors_proxy])
  389. logged? (state/logged?)
  390. developer-mode? (state/sub [:ui/developer-mode?])
  391. theme (state/sub :ui/theme)
  392. dark? (= "dark" theme)
  393. system-theme? (state/sub :ui/system-theme?)
  394. switch-theme (if dark? "white" "dark")]
  395. (rum/with-context [[t] i18n/*tongue-context*]
  396. [:div#settings.cp__settings-main
  397. [:div.panel-wrap
  398. [:h1.title (t :settings)]]
  399. (when current-repo
  400. [[:div.panel-wrap
  401. (edit-config-edn)]])
  402. [:hr]
  403. [:div.panel-wrap
  404. (theme-modes-row t switch-theme system-theme? dark?)
  405. (language-row t preferred-language)
  406. (file-format-row t preferred-format)
  407. (date-format-row t preferred-date-format)
  408. (workflow-row t preferred-workflow)
  409. (show-brackets-row t show-brackets?)
  410. (outdenting-row t logical-outdenting?)
  411. (tooltip-row t enable-tooltip?)
  412. (timetracking-row t enable-timetracking?)
  413. (journal-row t enable-journals?)
  414. (encryption-row t enable-encryption?)
  415. (keyboard-shortcuts-row t)
  416. (auto-push-row t current-repo enable-git-auto-push?)]
  417. [:hr] ;; Outside of panel wrap so that it is wider
  418. [:div.panel-wrap
  419. (clear-cache-row t)
  420. (version-row t version)
  421. (usage-diagnostics-row t instrument-disabled?)
  422. (developer-mode-row t developer-mode?)
  423. (when logged?
  424. [:div
  425. [:div.mt-6.sm:mt-5.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-center.sm:pt-5
  426. [:label.block.text-sm.font-medium.leading-5.sm:mt-px..opacity-70
  427. {:for "cors"}
  428. (t :settings-page/custom-cors-proxy-server)]
  429. [:div.mt-1.sm:mt-0.sm:col-span-2
  430. [:div.max-w-lg.rounded-md.sm:max-w-xs
  431. [:input#pat.form-input.is-small.transition.duration-150.ease-in-out
  432. {:default-value cors-proxy
  433. :on-blur (fn [event]
  434. (when-let [server (util/evalue event)]
  435. (user-handler/set-cors! server)
  436. (notification/show! "Custom CORS proxy updated successfully!" :success)))
  437. :on-key-press (fn [event]
  438. (let [k (gobj/get event "key")]
  439. (if (= "Enter" k)
  440. (when-let [server (util/evalue event)]
  441. (user-handler/set-cors! server)
  442. (notification/show! "Custom CORS proxy updated successfully!" :success)))))}]]]]
  443. (ui/admonition
  444. :important
  445. [:p (t :settings-page/dont-use-other-peoples-proxy-servers)
  446. [:a {:href "https://github.com/isomorphic-git/cors-proxy"
  447. :target "_blank"}
  448. "https://github.com/isomorphic-git/cors-proxy"]])])
  449. (when logged?
  450. [:div
  451. [:hr]
  452. [:div.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-center.sm:pt-5
  453. [:label.block.text-sm.font-medium.leading-5.opacity-70.text-red-600.dark:text-red-400
  454. {:for "delete account"}
  455. (t :user/delete-account)]
  456. [:div.mt-1.sm:mt-0.sm:col-span-2
  457. [:div.max-w-lg.rounded-md.sm:max-w-xs
  458. (ui/button (t :user/delete-your-account)
  459. :on-click (fn []
  460. (ui-handler/toggle-settings-modal!)
  461. (js/setTimeout #(state/set-modal! delete-account-confirm))))]]]])]])))