settings-general.tsx 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757
  1. import { Component, Show, createMemo, createResource, onMount, type JSX } from "solid-js"
  2. import { Button } from "@kirincode-ai/ui/button"
  3. import { Icon } from "@kirincode-ai/ui/icon"
  4. import { Select } from "@kirincode-ai/ui/select"
  5. import { Switch } from "@kirincode-ai/ui/switch"
  6. import { TextField } from "@kirincode-ai/ui/text-field"
  7. import { Tooltip } from "@kirincode-ai/ui/tooltip"
  8. import { useTheme, type ColorScheme } from "@kirincode-ai/ui/theme/context"
  9. import { useDialog } from "@kirincode-ai/ui/context/dialog"
  10. import { useParams } from "@solidjs/router"
  11. import { useLanguage } from "@/context/language"
  12. import { usePermission } from "@/context/permission"
  13. import { usePlatform, type DisplayBackend } from "@/context/platform"
  14. import { useServerSync } from "@/context/server-sync"
  15. import { useServerSDK } from "@/context/server-sdk"
  16. import { useUpdaterAction } from "./updater-action"
  17. import {
  18. monoDefault,
  19. monoFontFamily,
  20. monoInput,
  21. sansDefault,
  22. sansFontFamily,
  23. sansInput,
  24. terminalDefault,
  25. terminalFontFamily,
  26. terminalInput,
  27. useSettings,
  28. } from "@/context/settings"
  29. import { decode64 } from "@/utils/base64"
  30. import { playSoundById, SOUND_OPTIONS } from "@/utils/sound"
  31. import { Link } from "./link"
  32. import { SettingsList } from "./settings-list"
  33. let demoSoundState = {
  34. cleanup: undefined as (() => void) | undefined,
  35. timeout: undefined as NodeJS.Timeout | undefined,
  36. run: 0,
  37. }
  38. type ThemeOption = {
  39. id: string
  40. name: string
  41. }
  42. type ShellOption = {
  43. path: string
  44. name: string
  45. acceptable: boolean
  46. }
  47. type ShellSelectOption = {
  48. id: string
  49. value: string
  50. label: string
  51. }
  52. // To prevent audio from overlapping/playing very quickly when navigating the settings menus,
  53. // delay the playback by 100ms during quick selection changes and pause existing sounds.
  54. const stopDemoSound = () => {
  55. demoSoundState.run += 1
  56. if (demoSoundState.cleanup) {
  57. demoSoundState.cleanup()
  58. }
  59. clearTimeout(demoSoundState.timeout)
  60. demoSoundState.cleanup = undefined
  61. }
  62. const playDemoSound = (id: string | undefined) => {
  63. stopDemoSound()
  64. if (!id) return
  65. const run = ++demoSoundState.run
  66. demoSoundState.timeout = setTimeout(() => {
  67. void playSoundById(id).then((cleanup) => {
  68. if (demoSoundState.run !== run) {
  69. cleanup?.()
  70. return
  71. }
  72. demoSoundState.cleanup = cleanup
  73. })
  74. }, 100)
  75. }
  76. export const SettingsGeneral: Component = () => {
  77. const theme = useTheme()
  78. const language = useLanguage()
  79. const permission = usePermission()
  80. const platform = usePlatform()
  81. const dialog = useDialog()
  82. const params = useParams()
  83. const settings = useSettings()
  84. const updater = useUpdaterAction()
  85. const linux = createMemo(() => platform.platform === "desktop" && platform.os === "linux")
  86. const dir = createMemo(() => decode64(params.dir))
  87. const accepting = createMemo(() => {
  88. const value = dir()
  89. if (!value) return false
  90. if (!params.id) return permission.isAutoAcceptingDirectory(value)
  91. return permission.isAutoAccepting(params.id, value)
  92. })
  93. const toggleAccept = (checked: boolean) => {
  94. const value = dir()
  95. if (!value) return
  96. if (!params.id) {
  97. if (permission.isAutoAcceptingDirectory(value) === checked) return
  98. permission.toggleAutoAcceptDirectory(value)
  99. return
  100. }
  101. if (checked) {
  102. permission.enableAutoAccept(params.id, value)
  103. return
  104. }
  105. permission.disableAutoAccept(params.id, value)
  106. }
  107. const desktop = createMemo(() => platform.platform === "desktop")
  108. const themeOptions = createMemo<ThemeOption[]>(() => theme.ids().map((id) => ({ id, name: theme.name(id) })))
  109. const serverSync = useServerSync()
  110. const serverSdk = useServerSDK()
  111. const [shells] = createResource(
  112. () =>
  113. serverSdk()
  114. .client.pty.shells()
  115. .then((res) => res.data ?? [])
  116. .catch(() => [] as ShellOption[]),
  117. { initialValue: [] as ShellOption[] },
  118. )
  119. const [displayBackend, { refetch: refetchDisplayBackend }] = createResource(
  120. () => (linux() && platform.getDisplayBackend ? true : false),
  121. () => Promise.resolve(platform.getDisplayBackend?.() ?? null).catch(() => null as DisplayBackend | null),
  122. { initialValue: null as DisplayBackend | null },
  123. )
  124. const [pinchZoom, { mutate: setPinchZoom }] = createResource(
  125. () => (desktop() && platform.getPinchZoomEnabled ? true : false),
  126. () => Promise.resolve(platform.getPinchZoomEnabled?.() ?? false).catch(() => false),
  127. { initialValue: false },
  128. )
  129. onMount(() => {
  130. void theme.loadThemes()
  131. })
  132. const autoOption = { id: "auto", value: "", label: language.t("settings.general.row.shell.autoDefault") }
  133. const currentShell = createMemo(() => serverSync().data.config.shell ?? "")
  134. const shellOptions = createMemo<ShellSelectOption[]>(() => {
  135. const list = shells.latest
  136. const current = serverSync().data.config.shell
  137. const nameCounts = new Map<string, number>()
  138. for (const s of list) {
  139. nameCounts.set(s.name, (nameCounts.get(s.name) || 0) + 1)
  140. }
  141. const options = [
  142. autoOption,
  143. ...list.map((s) => {
  144. const ambiguousName = (nameCounts.get(s.name) || 0) > 1
  145. const text = ambiguousName ? s.path : s.name
  146. const label = s.acceptable ? text : `${text} (${language.t("settings.general.row.shell.terminalOnly")})`
  147. return {
  148. id: s.path,
  149. // Prefer name over path - "bash" is much cleaner than the explicit full route even when it may change due to PATH.
  150. value: ambiguousName ? s.path : s.name,
  151. label,
  152. }
  153. }),
  154. ]
  155. if (current && !options.some((o) => o.value === current)) {
  156. options.push({ id: current, value: current, label: current })
  157. }
  158. return options
  159. })
  160. const onDisplayBackendChange = (checked: boolean) => {
  161. const update = platform.setDisplayBackend?.(checked ? "wayland" : "auto")
  162. if (!update) return
  163. void update.finally(() => {
  164. void refetchDisplayBackend()
  165. })
  166. }
  167. const onPinchZoomChange = (checked: boolean) => {
  168. setPinchZoom(checked)
  169. const update = platform.setPinchZoomEnabled?.(checked)
  170. if (!update) return
  171. void update.catch(() => setPinchZoom(!checked))
  172. }
  173. const colorSchemeOptions = createMemo((): { value: ColorScheme; label: string }[] => [
  174. { value: "system", label: language.t("theme.scheme.system") },
  175. { value: "light", label: language.t("theme.scheme.light") },
  176. { value: "dark", label: language.t("theme.scheme.dark") },
  177. ])
  178. const languageOptions = createMemo(() =>
  179. language.locales.map((locale) => ({
  180. value: locale,
  181. label: language.label(locale),
  182. })),
  183. )
  184. const noneSound = { id: "none", label: "sound.option.none" } as const
  185. const soundOptions = [noneSound, ...SOUND_OPTIONS]
  186. const mono = () => monoInput(settings.appearance.font())
  187. const sans = () => sansInput(settings.appearance.uiFont())
  188. const terminal = () => terminalInput(settings.appearance.terminalFont())
  189. const soundSelectProps = (
  190. enabled: () => boolean,
  191. current: () => string,
  192. setEnabled: (value: boolean) => void,
  193. set: (id: string) => void,
  194. ) => ({
  195. options: soundOptions,
  196. current: enabled() ? (soundOptions.find((o) => o.id === current()) ?? noneSound) : noneSound,
  197. value: (o: (typeof soundOptions)[number]) => o.id,
  198. label: (o: (typeof soundOptions)[number]) => language.t(o.label),
  199. onHighlight: (option: (typeof soundOptions)[number] | undefined) => {
  200. if (!option) return
  201. playDemoSound(option.id === "none" ? undefined : option.id)
  202. },
  203. onSelect: (option: (typeof soundOptions)[number] | undefined) => {
  204. if (!option) return
  205. if (option.id === "none") {
  206. setEnabled(false)
  207. stopDemoSound()
  208. return
  209. }
  210. setEnabled(true)
  211. set(option.id)
  212. playDemoSound(option.id)
  213. },
  214. variant: "secondary" as const,
  215. size: "small" as const,
  216. triggerVariant: "settings" as const,
  217. })
  218. const GeneralSection = () => (
  219. <div class="flex flex-col gap-1">
  220. <SettingsList>
  221. <SettingsRow
  222. title={language.t("settings.general.row.language.title")}
  223. description={language.t("settings.general.row.language.description")}
  224. >
  225. <Select
  226. data-action="settings-language"
  227. options={languageOptions()}
  228. current={languageOptions().find((o) => o.value === language.locale())}
  229. value={(o) => o.value}
  230. label={(o) => o.label}
  231. onSelect={(option) => option && language.setLocale(option.value)}
  232. variant="secondary"
  233. size="small"
  234. triggerVariant="settings"
  235. />
  236. </SettingsRow>
  237. <SettingsRow
  238. title={language.t("command.permissions.autoaccept.enable")}
  239. description={language.t("toast.permissions.autoaccept.on.description")}
  240. >
  241. <div data-action="settings-auto-accept-permissions">
  242. <Switch checked={accepting()} disabled={!dir()} onChange={toggleAccept} />
  243. </div>
  244. </SettingsRow>
  245. <SettingsRow
  246. title={language.t("settings.general.row.shell.title")}
  247. description={language.t("settings.general.row.shell.description")}
  248. >
  249. <Select
  250. data-action="settings-shell"
  251. options={shellOptions()}
  252. current={shellOptions().find((o) => o.value === currentShell()) ?? autoOption}
  253. value={(o) => o.id}
  254. label={(o) => o.label}
  255. onSelect={(option) => {
  256. if (!option) return
  257. if (option.value === currentShell()) return
  258. serverSync().updateConfig({ shell: option.value })
  259. }}
  260. variant="secondary"
  261. size="small"
  262. triggerVariant="settings"
  263. triggerStyle={{ "min-width": "180px" }}
  264. />
  265. </SettingsRow>
  266. <SettingsRow
  267. title={language.t("settings.general.row.reasoningSummaries.title")}
  268. description={language.t("settings.general.row.reasoningSummaries.description")}
  269. >
  270. <div data-action="settings-feed-reasoning-summaries">
  271. <Switch
  272. checked={settings.general.showReasoningSummaries()}
  273. onChange={(checked) => settings.general.setShowReasoningSummaries(checked)}
  274. />
  275. </div>
  276. </SettingsRow>
  277. <SettingsRow
  278. title={language.t("settings.general.row.shellToolPartsExpanded.title")}
  279. description={language.t("settings.general.row.shellToolPartsExpanded.description")}
  280. >
  281. <div data-action="settings-feed-shell-tool-parts-expanded">
  282. <Switch
  283. checked={settings.general.shellToolPartsExpanded()}
  284. onChange={(checked) => settings.general.setShellToolPartsExpanded(checked)}
  285. />
  286. </div>
  287. </SettingsRow>
  288. <SettingsRow
  289. title={language.t("settings.general.row.editToolPartsExpanded.title")}
  290. description={language.t("settings.general.row.editToolPartsExpanded.description")}
  291. >
  292. <div data-action="settings-feed-edit-tool-parts-expanded">
  293. <Switch
  294. checked={settings.general.editToolPartsExpanded()}
  295. onChange={(checked) => settings.general.setEditToolPartsExpanded(checked)}
  296. />
  297. </div>
  298. </SettingsRow>
  299. <SettingsRow
  300. title={language.t("settings.general.row.newLayoutDesigns.title")}
  301. description={language.t("settings.general.row.newLayoutDesigns.description")}
  302. >
  303. <div data-action="settings-new-layout-designs">
  304. <Switch
  305. checked={settings.general.newLayoutDesigns()}
  306. onChange={(checked) => {
  307. settings.general.setNewLayoutDesigns(checked)
  308. if (!checked) return
  309. void import("@/components/settings-v2").then((module) => {
  310. dialog.show(() => <module.DialogSettings />)
  311. })
  312. }}
  313. />
  314. </div>
  315. </SettingsRow>
  316. </SettingsList>
  317. </div>
  318. )
  319. const AdvancedSection = () => (
  320. <div class="flex flex-col gap-1">
  321. <h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.general.section.advanced")}</h3>
  322. <SettingsList>
  323. <SettingsRow
  324. title={language.t("settings.general.row.showFileTree.title")}
  325. description={language.t("settings.general.row.showFileTree.description")}
  326. >
  327. <div data-action="settings-show-file-tree">
  328. <Switch
  329. checked={settings.general.showFileTree()}
  330. onChange={(checked) => settings.general.setShowFileTree(checked)}
  331. />
  332. </div>
  333. </SettingsRow>
  334. <SettingsRow
  335. title={language.t("settings.general.row.showNavigation.title")}
  336. description={language.t("settings.general.row.showNavigation.description")}
  337. >
  338. <div data-action="settings-show-navigation">
  339. <Switch
  340. checked={settings.general.showNavigation()}
  341. onChange={(checked) => settings.general.setShowNavigation(checked)}
  342. />
  343. </div>
  344. </SettingsRow>
  345. <SettingsRow
  346. title={language.t("settings.general.row.showSearch.title")}
  347. description={language.t("settings.general.row.showSearch.description")}
  348. >
  349. <div data-action="settings-show-search">
  350. <Switch
  351. checked={settings.general.showSearch()}
  352. onChange={(checked) => settings.general.setShowSearch(checked)}
  353. />
  354. </div>
  355. </SettingsRow>
  356. <SettingsRow
  357. title={language.t("settings.general.row.showStatus.title")}
  358. description={language.t("settings.general.row.showStatus.description")}
  359. >
  360. <div data-action="settings-show-status">
  361. <Switch
  362. checked={settings.general.showStatus()}
  363. onChange={(checked) => settings.general.setShowStatus(checked)}
  364. />
  365. </div>
  366. </SettingsRow>
  367. <SettingsRow
  368. title={language.t("settings.general.row.showCustomAgents.title")}
  369. description={language.t("settings.general.row.showCustomAgents.description")}
  370. >
  371. <div data-action="settings-show-custom-agents">
  372. <Switch
  373. checked={settings.general.showCustomAgents()}
  374. onChange={(checked) => settings.general.setShowCustomAgents(checked)}
  375. />
  376. </div>
  377. </SettingsRow>
  378. </SettingsList>
  379. </div>
  380. )
  381. const AppearanceSection = () => (
  382. <div class="flex flex-col gap-1">
  383. <h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.general.section.appearance")}</h3>
  384. <SettingsList>
  385. <SettingsRow
  386. title={language.t("settings.general.row.colorScheme.title")}
  387. description={language.t("settings.general.row.colorScheme.description")}
  388. >
  389. <Select
  390. data-action="settings-color-scheme"
  391. options={colorSchemeOptions()}
  392. current={colorSchemeOptions().find((o) => o.value === theme.colorScheme())}
  393. value={(o) => o.value}
  394. label={(o) => o.label}
  395. onSelect={(option) => option && theme.setColorScheme(option.value)}
  396. variant="secondary"
  397. size="small"
  398. triggerVariant="settings"
  399. triggerStyle={{ "min-width": "220px" }}
  400. />
  401. </SettingsRow>
  402. <SettingsRow
  403. title={language.t("settings.general.row.theme.title")}
  404. description={
  405. <>
  406. {language.t("settings.general.row.theme.description")}{" "}
  407. <Link href="https://kirincode.ai/docs/themes/">{language.t("common.learnMore")}</Link>
  408. </>
  409. }
  410. >
  411. <Select
  412. data-action="settings-theme"
  413. options={themeOptions()}
  414. current={themeOptions().find((o) => o.id === theme.themeId())}
  415. value={(o) => o.id}
  416. label={(o) => o.name}
  417. onSelect={(option) => {
  418. if (!option) return
  419. theme.setTheme(option.id)
  420. }}
  421. variant="secondary"
  422. size="small"
  423. triggerVariant="settings"
  424. />
  425. </SettingsRow>
  426. <SettingsRow
  427. title={language.t("settings.general.row.uiFont.title")}
  428. description={language.t("settings.general.row.uiFont.description")}
  429. >
  430. <div class="w-full sm:w-[220px]">
  431. <TextField
  432. data-action="settings-ui-font"
  433. label={language.t("settings.general.row.uiFont.title")}
  434. hideLabel
  435. type="text"
  436. value={sans()}
  437. onChange={(value) => settings.appearance.setUIFont(value)}
  438. placeholder={sansDefault}
  439. spellcheck={false}
  440. autocorrect="off"
  441. autocomplete="off"
  442. autocapitalize="off"
  443. class="text-12-regular"
  444. style={{ "font-family": sansFontFamily(settings.appearance.uiFont()) }}
  445. />
  446. </div>
  447. </SettingsRow>
  448. <SettingsRow
  449. title={language.t("settings.general.row.font.title")}
  450. description={language.t("settings.general.row.font.description")}
  451. >
  452. <div class="w-full sm:w-[220px]">
  453. <TextField
  454. data-action="settings-code-font"
  455. label={language.t("settings.general.row.font.title")}
  456. hideLabel
  457. type="text"
  458. value={mono()}
  459. onChange={(value) => settings.appearance.setFont(value)}
  460. placeholder={monoDefault}
  461. spellcheck={false}
  462. autocorrect="off"
  463. autocomplete="off"
  464. autocapitalize="off"
  465. class="text-12-regular"
  466. style={{ "font-family": monoFontFamily(settings.appearance.font()) }}
  467. />
  468. </div>
  469. </SettingsRow>
  470. <SettingsRow
  471. title={language.t("settings.general.row.terminalFont.title")}
  472. description={language.t("settings.general.row.terminalFont.description")}
  473. >
  474. <div class="w-full sm:w-[220px]">
  475. <TextField
  476. data-action="settings-terminal-font"
  477. label={language.t("settings.general.row.terminalFont.title")}
  478. hideLabel
  479. type="text"
  480. value={terminal()}
  481. onChange={(value) => settings.appearance.setTerminalFont(value)}
  482. placeholder={terminalDefault}
  483. spellcheck={false}
  484. autocorrect="off"
  485. autocomplete="off"
  486. autocapitalize="off"
  487. class="text-12-regular"
  488. style={{ "font-family": terminalFontFamily(settings.appearance.terminalFont()) }}
  489. />
  490. </div>
  491. </SettingsRow>
  492. </SettingsList>
  493. </div>
  494. )
  495. const NotificationsSection = () => (
  496. <div class="flex flex-col gap-1">
  497. <h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.general.section.notifications")}</h3>
  498. <SettingsList>
  499. <SettingsRow
  500. title={language.t("settings.general.notifications.agent.title")}
  501. description={language.t("settings.general.notifications.agent.description")}
  502. >
  503. <div data-action="settings-notifications-agent">
  504. <Switch
  505. checked={settings.notifications.agent()}
  506. onChange={(checked) => settings.notifications.setAgent(checked)}
  507. />
  508. </div>
  509. </SettingsRow>
  510. <SettingsRow
  511. title={language.t("settings.general.notifications.permissions.title")}
  512. description={language.t("settings.general.notifications.permissions.description")}
  513. >
  514. <div data-action="settings-notifications-permissions">
  515. <Switch
  516. checked={settings.notifications.permissions()}
  517. onChange={(checked) => settings.notifications.setPermissions(checked)}
  518. />
  519. </div>
  520. </SettingsRow>
  521. <SettingsRow
  522. title={language.t("settings.general.notifications.errors.title")}
  523. description={language.t("settings.general.notifications.errors.description")}
  524. >
  525. <div data-action="settings-notifications-errors">
  526. <Switch
  527. checked={settings.notifications.errors()}
  528. onChange={(checked) => settings.notifications.setErrors(checked)}
  529. />
  530. </div>
  531. </SettingsRow>
  532. </SettingsList>
  533. </div>
  534. )
  535. const SoundsSection = () => (
  536. <div class="flex flex-col gap-1">
  537. <h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.general.section.sounds")}</h3>
  538. <SettingsList>
  539. <SettingsRow
  540. title={language.t("settings.general.sounds.agent.title")}
  541. description={language.t("settings.general.sounds.agent.description")}
  542. >
  543. <Select
  544. data-action="settings-sounds-agent"
  545. {...soundSelectProps(
  546. () => settings.sounds.agentEnabled(),
  547. () => settings.sounds.agent(),
  548. (value) => settings.sounds.setAgentEnabled(value),
  549. (id) => settings.sounds.setAgent(id),
  550. )}
  551. />
  552. </SettingsRow>
  553. <SettingsRow
  554. title={language.t("settings.general.sounds.permissions.title")}
  555. description={language.t("settings.general.sounds.permissions.description")}
  556. >
  557. <Select
  558. data-action="settings-sounds-permissions"
  559. {...soundSelectProps(
  560. () => settings.sounds.permissionsEnabled(),
  561. () => settings.sounds.permissions(),
  562. (value) => settings.sounds.setPermissionsEnabled(value),
  563. (id) => settings.sounds.setPermissions(id),
  564. )}
  565. />
  566. </SettingsRow>
  567. <SettingsRow
  568. title={language.t("settings.general.sounds.errors.title")}
  569. description={language.t("settings.general.sounds.errors.description")}
  570. >
  571. <Select
  572. data-action="settings-sounds-errors"
  573. {...soundSelectProps(
  574. () => settings.sounds.errorsEnabled(),
  575. () => settings.sounds.errors(),
  576. (value) => settings.sounds.setErrorsEnabled(value),
  577. (id) => settings.sounds.setErrors(id),
  578. )}
  579. />
  580. </SettingsRow>
  581. </SettingsList>
  582. </div>
  583. )
  584. const UpdatesSection = () => (
  585. <div class="flex flex-col gap-1">
  586. <h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.general.section.updates")}</h3>
  587. <SettingsList>
  588. <SettingsRow
  589. title={language.t("settings.general.row.releaseNotes.title")}
  590. description={language.t("settings.general.row.releaseNotes.description")}
  591. >
  592. <div data-action="settings-release-notes">
  593. <Switch
  594. checked={settings.general.releaseNotes()}
  595. onChange={(checked) => settings.general.setReleaseNotes(checked)}
  596. />
  597. </div>
  598. </SettingsRow>
  599. <SettingsRow
  600. title={language.t("settings.updates.row.check.title")}
  601. description={language.t("settings.updates.row.check.description")}
  602. >
  603. <Button size="small" variant="secondary" disabled={!updater.action().run} onClick={updater.run}>
  604. {language.t(updater.action().label)}
  605. </Button>
  606. </SettingsRow>
  607. </SettingsList>
  608. </div>
  609. )
  610. const DisplaySection = () => (
  611. <Show when={desktop()}>
  612. <div class="flex flex-col gap-1">
  613. <h3 class="text-14-medium text-text-strong pb-2">{language.t("settings.general.section.display")}</h3>
  614. <SettingsList>
  615. <SettingsRow
  616. title={language.t("settings.general.row.pinchZoom.title")}
  617. description={language.t("settings.general.row.pinchZoom.description")}
  618. >
  619. <div data-action="settings-pinch-zoom">
  620. <Switch checked={pinchZoom.latest} onChange={onPinchZoomChange} />
  621. </div>
  622. </SettingsRow>
  623. <Show when={linux()}>
  624. <SettingsRow
  625. title={
  626. <div class="flex items-center gap-2">
  627. <span>{language.t("settings.general.row.wayland.title")}</span>
  628. <Tooltip value={language.t("settings.general.row.wayland.tooltip")} placement="top">
  629. <span class="text-text-weak">
  630. <Icon name="help" size="small" />
  631. </span>
  632. </Tooltip>
  633. </div>
  634. }
  635. description={language.t("settings.general.row.wayland.description")}
  636. >
  637. <div data-action="settings-wayland">
  638. <Switch checked={displayBackend.latest === "wayland"} onChange={onDisplayBackendChange} />
  639. </div>
  640. </SettingsRow>
  641. </Show>
  642. </SettingsList>
  643. </div>
  644. </Show>
  645. )
  646. return (
  647. <div class="flex flex-col h-full overflow-y-auto no-scrollbar px-4 pb-10 sm:px-10 sm:pb-10">
  648. <div class="sticky top-0 z-10 bg-[linear-gradient(to_bottom,var(--surface-stronger-non-alpha)_calc(100%_-_24px),transparent)]">
  649. <div class="flex flex-col gap-1 pt-6 pb-8">
  650. <h2 class="text-16-medium text-text-strong">{language.t("settings.tab.general")}</h2>
  651. </div>
  652. </div>
  653. <div class="flex flex-col gap-8 w-full">
  654. <GeneralSection />
  655. <AppearanceSection />
  656. <NotificationsSection />
  657. <SoundsSection />
  658. <UpdatesSection />
  659. <DisplaySection />
  660. <Show when={desktop()}>
  661. <AdvancedSection />
  662. </Show>
  663. </div>
  664. </div>
  665. )
  666. }
  667. interface SettingsRowProps {
  668. title: string | JSX.Element
  669. description: string | JSX.Element
  670. children: JSX.Element
  671. }
  672. const SettingsRow: Component<SettingsRowProps> = (props) => {
  673. return (
  674. <div class="flex flex-wrap items-center gap-4 py-3 border-b border-border-weak-base last:border-none sm:flex-nowrap">
  675. <div class="flex min-w-0 flex-1 flex-col gap-0.5">
  676. <span class="text-14-medium text-text-strong">{props.title}</span>
  677. <span class="text-12-regular text-text-weak">{props.description}</span>
  678. </div>
  679. <div class="flex w-full justify-end sm:w-auto sm:shrink-0">{props.children}</div>
  680. </div>
  681. )
  682. }