general.tsx 24 KB

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