refactor: enhance preference management and update component integrations

- Updated preference handling to utilize the usePreference hook across various components, improving consistency and maintainability.
- Introduced new preference types for ProxyMode and MultiModelFoldDisplayMode to enhance type safety.
- Refactored components to replace useSettings with usePreference, streamlining preference management.
- Cleaned up unused code and comments related to previous settings for better clarity.
- Updated auto-generated preference mappings to reflect recent changes in preference structure.
This commit is contained in:
fullex 2025-09-02 22:11:15 +08:00
parent 5d789ef394
commit 1b57ffeb56
28 changed files with 538 additions and 671 deletions

View File

@ -55,3 +55,7 @@ export type SidebarIcon =
| 'notes' | 'notes'
export type AssistantIconType = 'model' | 'emoji' | 'none' export type AssistantIconType = 'model' | 'emoji' | 'none'
export type ProxyMode = 'system' | 'custom' | 'none'
export type MultiModelFoldDisplayMode = 'expanded' | 'compact'

View File

@ -1,6 +1,6 @@
/** /**
* Auto-generated preferences configuration * Auto-generated preferences configuration
* Generated at: 2025-09-02T08:08:51.242Z * Generated at: 2025-09-02T11:59:32.955Z
* *
* This file is automatically generated from classification.json * This file is automatically generated from classification.json
* To update this file, modify classification.json and run: * To update this file, modify classification.json and run:
@ -13,6 +13,8 @@ import { TRANSLATE_PROMPT } from '@shared/config/prompts'
import type { import type {
AssistantIconType, AssistantIconType,
AssistantTabSortType, AssistantTabSortType,
MultiModelFoldDisplayMode,
ProxyMode,
SelectionActionItem, SelectionActionItem,
SelectionFilterMode, SelectionFilterMode,
SelectionTriggerMode, SelectionTriggerMode,
@ -52,15 +54,15 @@ export interface PreferencesType {
// redux/settings/enableDataCollection // redux/settings/enableDataCollection
'app.privacy.data_collection.enabled': boolean 'app.privacy.data_collection.enabled': boolean
// redux/settings/proxyBypassRules // redux/settings/proxyBypassRules
'app.proxy.bypass_rules': string | null 'app.proxy.bypass_rules': string
// redux/settings/proxyMode // redux/settings/proxyMode
'app.proxy.mode': string 'app.proxy.mode': ProxyMode
// redux/settings/proxyUrl // redux/settings/proxyUrl
'app.proxy.url': string | null 'app.proxy.url': string
// redux/settings/enableSpellCheck // redux/settings/enableSpellCheck
'app.spell_check.enabled': boolean 'app.spell_check.enabled': boolean
// redux/settings/spellCheckLanguages // redux/settings/spellCheckLanguages
'app.spell_check.languages': unknown[] 'app.spell_check.languages': string[]
// redux/settings/tray // redux/settings/tray
'app.tray.enabled': boolean 'app.tray.enabled': boolean
// redux/settings/trayOnClose // redux/settings/trayOnClose
@ -138,9 +140,11 @@ export interface PreferencesType {
// redux/settings/fontSize // redux/settings/fontSize
'chat.message.font_size': number 'chat.message.font_size': number
// redux/settings/mathEngine // redux/settings/mathEngine
'chat.message.math_engine': string 'chat.message.math.engine': string
// redux/settings/mathEnableSingleDollar
'chat.message.math.single_dollar': boolean
// redux/settings/foldDisplayMode // redux/settings/foldDisplayMode
'chat.message.multi_model.fold_display_mode': string 'chat.message.multi_model.fold_display_mode': MultiModelFoldDisplayMode
// redux/settings/gridColumns // redux/settings/gridColumns
'chat.message.multi_model.grid_columns': number 'chat.message.multi_model.grid_columns': number
// redux/settings/gridPopoverTrigger // redux/settings/gridPopoverTrigger
@ -149,8 +153,12 @@ export interface PreferencesType {
'chat.message.multi_model.style': string 'chat.message.multi_model.style': string
// redux/settings/messageNavigation // redux/settings/messageNavigation
'chat.message.navigation_mode': string 'chat.message.navigation_mode': string
// redux/settings/renderInputMessageAsMarkdown
'chat.message.render_as_markdown': boolean
// redux/settings/showMessageDivider // redux/settings/showMessageDivider
'chat.message.show_divider': boolean 'chat.message.show_divider': boolean
// redux/settings/showMessageOutline
'chat.message.show_outline': boolean
// redux/settings/showPrompt // redux/settings/showPrompt
'chat.message.show_prompt': boolean 'chat.message.show_prompt': boolean
// redux/settings/messageStyle // redux/settings/messageStyle
@ -408,9 +416,9 @@ export const DefaultPreferences: PreferencesType = {
'app.notification.backup.enabled': false, 'app.notification.backup.enabled': false,
'app.notification.knowledge.enabled': false, 'app.notification.knowledge.enabled': false,
'app.privacy.data_collection.enabled': false, 'app.privacy.data_collection.enabled': false,
'app.proxy.bypass_rules': null, 'app.proxy.bypass_rules': '',
'app.proxy.mode': 'system', 'app.proxy.mode': 'system',
'app.proxy.url': null, 'app.proxy.url': '',
'app.spell_check.enabled': false, 'app.spell_check.enabled': false,
'app.spell_check.languages': [], 'app.spell_check.languages': [],
'app.tray.enabled': true, 'app.tray.enabled': true,
@ -451,13 +459,16 @@ export const DefaultPreferences: PreferencesType = {
'chat.message.confirm_regenerate': true, 'chat.message.confirm_regenerate': true,
'chat.message.font': 'system', 'chat.message.font': 'system',
'chat.message.font_size': 14, 'chat.message.font_size': 14,
'chat.message.math_engine': 'KaTeX', 'chat.message.math.engine': 'KaTeX',
'chat.message.math.single_dollar': true,
'chat.message.multi_model.fold_display_mode': 'expanded', 'chat.message.multi_model.fold_display_mode': 'expanded',
'chat.message.multi_model.grid_columns': 2, 'chat.message.multi_model.grid_columns': 2,
'chat.message.multi_model.grid_popover_trigger': 'click', 'chat.message.multi_model.grid_popover_trigger': 'click',
'chat.message.multi_model.style': 'horizontal', 'chat.message.multi_model.style': 'horizontal',
'chat.message.navigation_mode': 'none', 'chat.message.navigation_mode': 'none',
'chat.message.render_as_markdown': false,
'chat.message.show_divider': true, 'chat.message.show_divider': true,
'chat.message.show_outline': false,
'chat.message.show_prompt': true, 'chat.message.show_prompt': true,
'chat.message.style': 'plain', 'chat.message.style': 'plain',
'chat.message.thought.auto_collapse': true, 'chat.message.thought.auto_collapse': true,
@ -642,8 +653,8 @@ export const DefaultPreferences: PreferencesType = {
/** /**
* : * :
* - 总配置项: 181 * - 总配置项: 184
* - electronStore项: 1 * - electronStore项: 1
* - redux项: 180 * - redux项: 183
* - localStorage项: 0 * - localStorage项: 0
*/ */

View File

@ -75,7 +75,8 @@ vi.mock('@logger', () => ({
info: vi.fn(), info: vi.fn(),
warn: vi.fn(), warn: vi.fn(),
error: vi.fn(), error: vi.fn(),
silly: vi.fn() silly: vi.fn(),
verbose: vi.fn()
}) })
} }
})) }))

View File

@ -10,6 +10,7 @@ import {
PushpinOutlined, PushpinOutlined,
ReloadOutlined ReloadOutlined
} from '@ant-design/icons' } from '@ant-design/icons'
import { usePreference } from '@data/hooks/usePreference'
import { loggerService } from '@logger' import { loggerService } from '@logger'
import { isLinux, isMac, isWin } from '@renderer/config/constant' import { isLinux, isMac, isWin } from '@renderer/config/constant'
import { DEFAULT_MIN_APPS } from '@renderer/config/minapps' import { DEFAULT_MIN_APPS } from '@renderer/config/minapps'
@ -19,10 +20,7 @@ import { useMinapps } from '@renderer/hooks/useMinapps'
import useNavBackgroundColor from '@renderer/hooks/useNavBackgroundColor' import useNavBackgroundColor from '@renderer/hooks/useNavBackgroundColor'
import { useNavbarPosition } from '@renderer/hooks/useNavbar' import { useNavbarPosition } from '@renderer/hooks/useNavbar'
import { useRuntime } from '@renderer/hooks/useRuntime' import { useRuntime } from '@renderer/hooks/useRuntime'
import { useSettings } from '@renderer/hooks/useSettings'
import { useTimer } from '@renderer/hooks/useTimer' import { useTimer } from '@renderer/hooks/useTimer'
import { useAppDispatch } from '@renderer/store'
import { setMinappsOpenLinkExternal } from '@renderer/store/settings'
import { MinAppType } from '@renderer/types' import { MinAppType } from '@renderer/types'
import { delay } from '@renderer/utils' import { delay } from '@renderer/utils'
import { Alert, Avatar, Button, Drawer, Tooltip } from 'antd' import { Alert, Avatar, Button, Drawer, Tooltip } from 'antd'
@ -142,12 +140,12 @@ const GoogleLoginTip = ({
/** The main container for MinApp popup */ /** The main container for MinApp popup */
const MinappPopupContainer: React.FC = () => { const MinappPopupContainer: React.FC = () => {
const { openedKeepAliveMinapps, openedOneOffMinapp, currentMinappId, minappShow } = useRuntime() const { openedKeepAliveMinapps, openedOneOffMinapp, currentMinappId, minappShow } = useRuntime()
const [minappsOpenLinkExternal, setMinappsOpenLinkExternal] = usePreference('feature.minapp.open_link_external')
const { closeMinapp, hideMinappPopup } = useMinappPopup() const { closeMinapp, hideMinappPopup } = useMinappPopup()
const { pinned, updatePinnedMinapps } = useMinapps() const { pinned, updatePinnedMinapps } = useMinapps()
const { t } = useTranslation() const { t } = useTranslation()
const backgroundColor = useNavBackgroundColor() const backgroundColor = useNavBackgroundColor()
const { isTopNavbar } = useNavbarPosition() const { isTopNavbar } = useNavbarPosition()
const dispatch = useAppDispatch()
/** control the drawer open or close */ /** control the drawer open or close */
const [isPopupShow, setIsPopupShow] = useState(true) const [isPopupShow, setIsPopupShow] = useState(true)
@ -166,7 +164,6 @@ const MinappPopupContainer: React.FC = () => {
/** indicate whether the webview has loaded */ /** indicate whether the webview has loaded */
const webviewLoadedRefs = useRef<Map<string, boolean>>(new Map()) const webviewLoadedRefs = useRef<Map<string, boolean>>(new Map())
/** whether the minapps open link external is enabled */ /** whether the minapps open link external is enabled */
const { minappsOpenLinkExternal } = useSettings()
const { isLeftNavbar } = useNavbarPosition() const { isLeftNavbar } = useNavbarPosition()
@ -347,7 +344,7 @@ const MinappPopupContainer: React.FC = () => {
/** set the open external status */ /** set the open external status */
const handleToggleOpenExternal = () => { const handleToggleOpenExternal = () => {
dispatch(setMinappsOpenLinkExternal(!minappsOpenLinkExternal)) setMinappsOpenLinkExternal(!minappsOpenLinkExternal)
} }
/** navigate back in webview history */ /** navigate back in webview history */

View File

@ -5,7 +5,6 @@ import useAvatar from '@renderer/hooks/useAvatar'
import ImageStorage from '@renderer/services/ImageStorage' import ImageStorage from '@renderer/services/ImageStorage'
import { useAppDispatch } from '@renderer/store' import { useAppDispatch } from '@renderer/store'
import { setAvatar } from '@renderer/store/runtime' import { setAvatar } from '@renderer/store/runtime'
import { setUserName } from '@renderer/store/settings'
import { compressImage, isEmoji } from '@renderer/utils' import { compressImage, isEmoji } from '@renderer/utils'
import { Avatar, Dropdown, Input, Modal, Popover, Upload } from 'antd' import { Avatar, Dropdown, Input, Modal, Popover, Upload } from 'antd'
import React, { useState } from 'react' import React, { useState } from 'react'
@ -21,11 +20,12 @@ interface Props {
} }
const PopupContainer: React.FC<Props> = ({ resolve }) => { const PopupContainer: React.FC<Props> = ({ resolve }) => {
const [userName, setUserName] = usePreference('app.user.name')
const [open, setOpen] = useState(true) const [open, setOpen] = useState(true)
const [emojiPickerOpen, setEmojiPickerOpen] = useState(false) const [emojiPickerOpen, setEmojiPickerOpen] = useState(false)
const [dropdownOpen, setDropdownOpen] = useState(false) const [dropdownOpen, setDropdownOpen] = useState(false)
const { t } = useTranslation() const { t } = useTranslation()
const [userName] = usePreference('app.user.name')
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const avatar = useAvatar() const avatar = useAvatar()
@ -170,7 +170,7 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
<Input <Input
placeholder={t('settings.general.user_name.placeholder')} placeholder={t('settings.general.user_name.placeholder')}
value={userName} value={userName}
onChange={(e) => dispatch(setUserName(e.target.value.trim()))} onChange={(e) => setUserName(e.target.value.trim())}
style={{ flex: 1, textAlign: 'center', width: '100%' }} style={{ flex: 1, textAlign: 'center', width: '100%' }}
maxLength={30} maxLength={30}
/> />

View File

@ -1,68 +1,56 @@
import store, { useAppDispatch, useAppSelector } from '@renderer/store' import store, { useAppSelector } from '@renderer/store'
import { import {
setAutoCheckUpdate as _setAutoCheckUpdate, SettingsState
setLaunchOnBoot,
setLaunchToTray,
setSendMessageShortcut as _setSendMessageShortcut,
setTargetLanguage,
setTestChannel as _setTestChannel,
setTestPlan as _setTestPlan,
SettingsState,
setTray as _setTray,
setTrayOnClose
// setWindowStyle // setWindowStyle
} from '@renderer/store/settings' } from '@renderer/store/settings'
import { TranslateLanguageCode } from '@renderer/types'
import { UpgradeChannel } from '@shared/config/constant'
import type { SendMessageShortcut } from '@shared/data/preferenceTypes'
export function useSettings() { export function useSettings() {
const settings = useAppSelector((state) => state.settings) const settings = useAppSelector((state) => state.settings)
const dispatch = useAppDispatch() // const dispatch = useAppDispatch()
return { return {
...settings, ...settings
setSendMessageShortcut(shortcut: SendMessageShortcut) { // setSendMessageShortcut(shortcut: SendMessageShortcut) {
dispatch(_setSendMessageShortcut(shortcut)) // dispatch(_setSendMessageShortcut(shortcut))
}, // },
setLaunch(isLaunchOnBoot: boolean | undefined, isLaunchToTray: boolean | undefined = undefined) { // setLaunch(isLaunchOnBoot: boolean | undefined, isLaunchToTray: boolean | undefined = undefined) {
if (isLaunchOnBoot !== undefined) { // if (isLaunchOnBoot !== undefined) {
dispatch(setLaunchOnBoot(isLaunchOnBoot)) // dispatch(setLaunchOnBoot(isLaunchOnBoot))
window.api.setLaunchOnBoot(isLaunchOnBoot) // window.api.setLaunchOnBoot(isLaunchOnBoot)
} // }
if (isLaunchToTray !== undefined) { // if (isLaunchToTray !== undefined) {
dispatch(setLaunchToTray(isLaunchToTray)) // dispatch(setLaunchToTray(isLaunchToTray))
window.api.setLaunchToTray(isLaunchToTray) // window.api.setLaunchToTray(isLaunchToTray)
} // }
}, // },
setTray(isShowTray: boolean | undefined, isTrayOnClose: boolean | undefined = undefined) { // setTray(isShowTray: boolean | undefined, isTrayOnClose: boolean | undefined = undefined) {
if (isShowTray !== undefined) { // if (isShowTray !== undefined) {
dispatch(_setTray(isShowTray)) // dispatch(_setTray(isShowTray))
window.api.setTray(isShowTray) // window.api.setTray(isShowTray)
} // }
if (isTrayOnClose !== undefined) { // if (isTrayOnClose !== undefined) {
dispatch(setTrayOnClose(isTrayOnClose)) // dispatch(setTrayOnClose(isTrayOnClose))
window.api.setTrayOnClose(isTrayOnClose) // window.api.setTrayOnClose(isTrayOnClose)
} // }
}, // },
setAutoCheckUpdate(isAutoUpdate: boolean) { // setAutoCheckUpdate(isAutoUpdate: boolean) {
dispatch(_setAutoCheckUpdate(isAutoUpdate)) // dispatch(_setAutoCheckUpdate(isAutoUpdate))
window.api.setAutoUpdate(isAutoUpdate) // window.api.setAutoUpdate(isAutoUpdate)
}, // },
setTestPlan(isTestPlan: boolean) { // setTestPlan(isTestPlan: boolean) {
dispatch(_setTestPlan(isTestPlan)) // dispatch(_setTestPlan(isTestPlan))
window.api.setTestPlan(isTestPlan) // window.api.setTestPlan(isTestPlan)
}, // },
setTestChannel(channel: UpgradeChannel) { // setTestChannel(channel: UpgradeChannel) {
dispatch(_setTestChannel(channel)) // dispatch(_setTestChannel(channel))
window.api.setTestChannel(channel) // window.api.setTestChannel(channel)
}, // },
// setTheme(theme: ThemeMode) { // setTheme(theme: ThemeMode) {
// dispatch(setTheme(theme)) // dispatch(setTheme(theme))
@ -70,9 +58,9 @@ export function useSettings() {
// setWindowStyle(windowStyle: 'transparent' | 'opaque') { // setWindowStyle(windowStyle: 'transparent' | 'opaque') {
// dispatch(setWindowStyle(windowStyle)) // dispatch(setWindowStyle(windowStyle))
// }, // },
setTargetLanguage(targetLanguage: TranslateLanguageCode) { // setTargetLanguage(targetLanguage: TranslateLanguageCode) {
dispatch(setTargetLanguage(targetLanguage)) // dispatch(setTargetLanguage(targetLanguage))
} // }
// setTopicPosition(topicPosition: 'left' | 'right') { // setTopicPosition(topicPosition: 'left' | 'right') {
// dispatch(setTopicPosition(topicPosition)) // dispatch(setTopicPosition(topicPosition))
// }, // },

View File

@ -7,8 +7,6 @@ import { modelGenerating } from '@renderer/hooks/useRuntime'
import { useShortcut } from '@renderer/hooks/useShortcuts' import { useShortcut } from '@renderer/hooks/useShortcuts'
import { useShowAssistants, useShowTopics } from '@renderer/hooks/useStore' import { useShowAssistants, useShowTopics } from '@renderer/hooks/useStore'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { useAppDispatch } from '@renderer/store'
import { setNarrowMode } from '@renderer/store/settings'
import { Assistant, Topic } from '@renderer/types' import { Assistant, Topic } from '@renderer/types'
import { Tooltip } from 'antd' import { Tooltip } from 'antd'
import { t } from 'i18next' import { t } from 'i18next'
@ -30,14 +28,13 @@ interface Props {
} }
const HeaderNavbar: FC<Props> = ({ activeAssistant, setActiveAssistant, activeTopic, setActiveTopic }) => { const HeaderNavbar: FC<Props> = ({ activeAssistant, setActiveAssistant, activeTopic, setActiveTopic }) => {
const [topicPosition] = usePreference('topic.position')
const [narrowMode, setNarrowMode] = usePreference('chat.narrow_mode')
const { assistant } = useAssistant(activeAssistant.id) const { assistant } = useAssistant(activeAssistant.id)
const { showAssistants, toggleShowAssistants } = useShowAssistants() const { showAssistants, toggleShowAssistants } = useShowAssistants()
const [topicPosition] = usePreference('topic.position')
const [narrowMode] = usePreference('chat.narrow_mode')
const { showTopics, toggleShowTopics } = useShowTopics() const { showTopics, toggleShowTopics } = useShowTopics()
const dispatch = useAppDispatch()
useShortcut('toggle_show_assistants', toggleShowAssistants) useShortcut('toggle_show_assistants', toggleShowAssistants)
@ -55,7 +52,7 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant, setActiveAssistant, activeTo
const handleNarrowModeToggle = async () => { const handleNarrowModeToggle = async () => {
await modelGenerating() await modelGenerating()
dispatch(setNarrowMode(!narrowMode)) setNarrowMode(!narrowMode)
} }
const onShowAssistantsDrawer = () => { const onShowAssistantsDrawer = () => {

View File

@ -1,31 +1,26 @@
import { ArrowsAltOutlined, ShrinkOutlined } from '@ant-design/icons' import { ArrowsAltOutlined, ShrinkOutlined } from '@ant-design/icons'
import { usePreference } from '@data/hooks/usePreference'
import ModelAvatar from '@renderer/components/Avatar/ModelAvatar' import ModelAvatar from '@renderer/components/Avatar/ModelAvatar'
import { HStack } from '@renderer/components/Layout' import { HStack } from '@renderer/components/Layout'
import Scrollbar from '@renderer/components/Scrollbar' import Scrollbar from '@renderer/components/Scrollbar'
import { useSettings } from '@renderer/hooks/useSettings'
import { useAppDispatch } from '@renderer/store'
import { setFoldDisplayMode } from '@renderer/store/settings'
import type { Model } from '@renderer/types' import type { Model } from '@renderer/types'
import { AssistantMessageStatus, type Message } from '@renderer/types/newMessage' import { AssistantMessageStatus, type Message } from '@renderer/types/newMessage'
import { lightbulbSoftVariants } from '@renderer/utils/motionVariants' import { lightbulbSoftVariants } from '@renderer/utils/motionVariants'
import type { MultiModelFoldDisplayMode } from '@shared/data/preferenceTypes'
import { Avatar, Segmented as AntdSegmented, Tooltip } from 'antd' import { Avatar, Segmented as AntdSegmented, Tooltip } from 'antd'
import { motion } from 'motion/react' import { motion } from 'motion/react'
import { FC, memo, useCallback } from 'react' import { FC, memo, useCallback } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
interface MessageGroupModelListProps { interface MessageGroupModelListProps {
messages: Message[] messages: Message[]
selectMessageId: string selectMessageId: string
setSelectedMessage: (message: Message) => void setSelectedMessage: (message: Message) => void
} }
type DisplayMode = 'compact' | 'expanded'
const MessageGroupModelList: FC<MessageGroupModelListProps> = ({ messages, selectMessageId, setSelectedMessage }) => { const MessageGroupModelList: FC<MessageGroupModelListProps> = ({ messages, selectMessageId, setSelectedMessage }) => {
const dispatch = useAppDispatch() const [foldDisplayMode, setFoldDisplayMode] = usePreference('chat.message.multi_model.fold_display_mode')
const { t } = useTranslation() const { t } = useTranslation()
const { foldDisplayMode } = useSettings()
const isCompact = foldDisplayMode === 'compact' const isCompact = foldDisplayMode === 'compact'
const isMessageProcessing = useCallback((message: Message) => { const isMessageProcessing = useCallback((message: Message) => {
@ -80,7 +75,7 @@ const MessageGroupModelList: FC<MessageGroupModelListProps> = ({ messages, selec
mouseLeaveDelay={0}> mouseLeaveDelay={0}>
<DisplayModeToggle <DisplayModeToggle
displayMode={foldDisplayMode} displayMode={foldDisplayMode}
onClick={() => dispatch(setFoldDisplayMode(isCompact ? 'expanded' : 'compact'))}> onClick={() => setFoldDisplayMode(isCompact ? 'expanded' : 'compact')}>
{isCompact ? <ArrowsAltOutlined /> : <ShrinkOutlined />} {isCompact ? <ArrowsAltOutlined /> : <ShrinkOutlined />}
</DisplayModeToggle> </DisplayModeToggle>
</Tooltip> </Tooltip>
@ -115,7 +110,7 @@ const Container = styled(HStack)`
margin-left: 4px; margin-left: 4px;
` `
const DisplayModeToggle = styled.div<{ displayMode: DisplayMode }>` const DisplayModeToggle = styled.div<{ displayMode: MultiModelFoldDisplayMode }>`
display: flex; display: flex;
cursor: pointer; cursor: pointer;
padding: 2px 6px 3px 6px; padding: 2px 6px 3px 6px;
@ -128,7 +123,7 @@ const DisplayModeToggle = styled.div<{ displayMode: DisplayMode }>`
} }
` `
const ModelsContainer = styled(Scrollbar)<{ $displayMode: DisplayMode }>` const ModelsContainer = styled(Scrollbar)<{ $displayMode: MultiModelFoldDisplayMode }>`
display: flex; display: flex;
flex-direction: ${(props) => (props.$displayMode === 'expanded' ? 'column' : 'row')}; flex-direction: ${(props) => (props.$displayMode === 'expanded' ? 'column' : 'row')};
justify-content: ${(props) => (props.$displayMode === 'expanded' ? 'space-between' : 'flex-start')}; justify-content: ${(props) => (props.$displayMode === 'expanded' ? 'space-between' : 'flex-start')};

View File

@ -1,20 +1,18 @@
import { SettingOutlined } from '@ant-design/icons' import { SettingOutlined } from '@ant-design/icons'
import { usePreference } from '@data/hooks/usePreference'
import Selector from '@renderer/components/Selector' import Selector from '@renderer/components/Selector'
import { useSettings } from '@renderer/hooks/useSettings'
import { SettingDivider } from '@renderer/pages/settings' import { SettingDivider } from '@renderer/pages/settings'
import { SettingRow } from '@renderer/pages/settings' import { SettingRow } from '@renderer/pages/settings'
import { useAppDispatch } from '@renderer/store'
import { setGridColumns, setGridPopoverTrigger } from '@renderer/store/settings'
import { Col, Row, Slider } from 'antd' import { Col, Row, Slider } from 'antd'
import { Popover } from 'antd' import { Popover } from 'antd'
import { FC, useState } from 'react' import { FC, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
const MessageGroupSettings: FC = () => { const MessageGroupSettings: FC = () => {
const dispatch = useAppDispatch() const [gridPopoverTrigger, setGridPopoverTrigger] = usePreference('chat.message.multi_model.grid_popover_trigger')
const [gridColumns, setGridColumns] = usePreference('chat.message.multi_model.grid_columns')
const { t } = useTranslation() const { t } = useTranslation()
const { gridColumns, gridPopoverTrigger } = useSettings()
const [gridColumnsValue, setGridColumnsValue] = useState(gridColumns) const [gridColumnsValue, setGridColumnsValue] = useState(gridColumns)
return ( return (
@ -28,7 +26,7 @@ const MessageGroupSettings: FC = () => {
<Selector <Selector
size={14} size={14}
value={gridPopoverTrigger || 'hover'} value={gridPopoverTrigger || 'hover'}
onChange={(value) => dispatch(setGridPopoverTrigger(value as 'hover' | 'click'))} onChange={(value) => setGridPopoverTrigger(value as 'hover' | 'click')}
options={[ options={[
{ label: t('settings.messages.grid_popover_trigger.hover'), value: 'hover' }, { label: t('settings.messages.grid_popover_trigger.hover'), value: 'hover' },
{ label: t('settings.messages.grid_popover_trigger.click'), value: 'click' } { label: t('settings.messages.grid_popover_trigger.click'), value: 'click' }
@ -45,7 +43,7 @@ const MessageGroupSettings: FC = () => {
value={gridColumnsValue} value={gridColumnsValue}
style={{ width: '100%' }} style={{ width: '100%' }}
onChange={(value) => setGridColumnsValue(value)} onChange={(value) => setGridColumnsValue(value)}
onChangeComplete={(value) => dispatch(setGridColumns(value))} onChangeComplete={(value) => setGridColumns(value)}
min={2} min={2}
max={6} max={6}
step={1} step={1}

View File

@ -1,3 +1,4 @@
import { usePreference } from '@data/hooks/usePreference'
import { Navbar, NavbarLeft, NavbarRight } from '@renderer/components/app/Navbar' import { Navbar, NavbarLeft, NavbarRight } from '@renderer/components/app/Navbar'
import { HStack } from '@renderer/components/Layout' import { HStack } from '@renderer/components/Layout'
import SearchPopup from '@renderer/components/Popups/SearchPopup' import SearchPopup from '@renderer/components/Popups/SearchPopup'
@ -5,12 +6,9 @@ import { isMac } from '@renderer/config/constant'
import { useAssistant } from '@renderer/hooks/useAssistant' import { useAssistant } from '@renderer/hooks/useAssistant'
import { useFullscreen } from '@renderer/hooks/useFullscreen' import { useFullscreen } from '@renderer/hooks/useFullscreen'
import { modelGenerating } from '@renderer/hooks/useRuntime' import { modelGenerating } from '@renderer/hooks/useRuntime'
import { useSettings } from '@renderer/hooks/useSettings'
import { useShortcut } from '@renderer/hooks/useShortcuts' import { useShortcut } from '@renderer/hooks/useShortcuts'
import { useShowAssistants, useShowTopics } from '@renderer/hooks/useStore' import { useShowAssistants, useShowTopics } from '@renderer/hooks/useStore'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { useAppDispatch } from '@renderer/store'
import { setNarrowMode } from '@renderer/store/settings'
import { Assistant, Topic } from '@renderer/types' import { Assistant, Topic } from '@renderer/types'
import { Tooltip } from 'antd' import { Tooltip } from 'antd'
import { t } from 'i18next' import { t } from 'i18next'
@ -22,7 +20,6 @@ import styled from 'styled-components'
import AssistantsDrawer from './components/AssistantsDrawer' import AssistantsDrawer from './components/AssistantsDrawer'
import SelectModelButton from './components/SelectModelButton' import SelectModelButton from './components/SelectModelButton'
import UpdateAppButton from './components/UpdateAppButton' import UpdateAppButton from './components/UpdateAppButton'
interface Props { interface Props {
activeAssistant: Assistant activeAssistant: Assistant
activeTopic: Topic activeTopic: Topic
@ -32,12 +29,13 @@ interface Props {
} }
const HeaderNavbar: FC<Props> = ({ activeAssistant, setActiveAssistant, activeTopic, setActiveTopic }) => { const HeaderNavbar: FC<Props> = ({ activeAssistant, setActiveAssistant, activeTopic, setActiveTopic }) => {
const [narrowMode, setNarrowMode] = usePreference('chat.narrow_mode')
const [topicPosition] = usePreference('topic.position')
const { assistant } = useAssistant(activeAssistant.id) const { assistant } = useAssistant(activeAssistant.id)
const { showAssistants, toggleShowAssistants } = useShowAssistants() const { showAssistants, toggleShowAssistants } = useShowAssistants()
const isFullscreen = useFullscreen() const isFullscreen = useFullscreen()
const { topicPosition, narrowMode } = useSettings()
const { showTopics, toggleShowTopics } = useShowTopics() const { showTopics, toggleShowTopics } = useShowTopics()
const dispatch = useAppDispatch()
useShortcut('toggle_show_assistants', toggleShowAssistants) useShortcut('toggle_show_assistants', toggleShowAssistants)
@ -55,7 +53,7 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant, setActiveAssistant, activeTo
const handleNarrowModeToggle = async () => { const handleNarrowModeToggle = async () => {
await modelGenerating() await modelGenerating()
dispatch(setNarrowMode(!narrowMode)) setNarrowMode(!narrowMode)
} }
const onShowAssistantsDrawer = () => { const onShowAssistantsDrawer = () => {

View File

@ -1,3 +1,4 @@
import { useMultiplePreferences, usePreference } from '@data/hooks/usePreference'
import EditableNumber from '@renderer/components/EditableNumber' import EditableNumber from '@renderer/components/EditableNumber'
import { HStack } from '@renderer/components/Layout' import { HStack } from '@renderer/components/Layout'
import Scrollbar from '@renderer/components/Scrollbar' import Scrollbar from '@renderer/components/Scrollbar'
@ -9,41 +10,11 @@ import { useCodeStyle } from '@renderer/context/CodeStyleProvider'
import { useTheme } from '@renderer/context/ThemeProvider' import { useTheme } from '@renderer/context/ThemeProvider'
import { useAssistant } from '@renderer/hooks/useAssistant' import { useAssistant } from '@renderer/hooks/useAssistant'
import { useProvider } from '@renderer/hooks/useProvider' import { useProvider } from '@renderer/hooks/useProvider'
import { useSettings } from '@renderer/hooks/useSettings'
import useTranslate from '@renderer/hooks/useTranslate' import useTranslate from '@renderer/hooks/useTranslate'
import { SettingDivider, SettingRow, SettingRowTitle } from '@renderer/pages/settings' import { SettingDivider, SettingRow, SettingRowTitle } from '@renderer/pages/settings'
import AssistantSettingsPopup from '@renderer/pages/settings/AssistantSettings' import AssistantSettingsPopup from '@renderer/pages/settings/AssistantSettings'
import { CollapsibleSettingGroup } from '@renderer/pages/settings/SettingGroup' import { CollapsibleSettingGroup } from '@renderer/pages/settings/SettingGroup'
import { getDefaultModel } from '@renderer/services/AssistantService' import { getDefaultModel } from '@renderer/services/AssistantService'
import { useAppDispatch } from '@renderer/store'
import {
setAutoTranslateWithSpace,
setCodeCollapsible,
setCodeEditor,
setCodeExecution,
setCodeImageTools,
setCodeShowLineNumbers,
setCodeViewer,
setCodeWrappable,
setConfirmDeleteMessage,
setConfirmRegenerateMessage,
setEnableQuickPanelTriggers,
setFontSize,
setMathEnableSingleDollar,
setMathEngine,
setMessageFont,
setMessageNavigation,
setMessageStyle,
setMultiModelMessageStyle,
setPasteLongTextAsFile,
setPasteLongTextThreshold,
setRenderInputMessageAsMarkdown,
setShowInputEstimatedTokens,
setShowMessageOutline,
setShowPrompt,
setShowTranslateConfirm,
setThoughtAutoCollapse
} from '@renderer/store/settings'
import { Assistant, AssistantSettings, CodeStyleVarious, MathEngine } from '@renderer/types' import { Assistant, AssistantSettings, CodeStyleVarious, MathEngine } from '@renderer/types'
import { modalConfirm } from '@renderer/utils' import { modalConfirm } from '@renderer/utils'
import { getSendMessageShortcutLabel } from '@renderer/utils/input' import { getSendMessageShortcutLabel } from '@renderer/utils/input'
@ -56,16 +27,64 @@ import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
import OpenAISettingsGroup from './components/OpenAISettingsGroup' import OpenAISettingsGroup from './components/OpenAISettingsGroup'
interface Props { interface Props {
assistant: Assistant assistant: Assistant
} }
const SettingsTab: FC<Props> = (props) => { const SettingsTab: FC<Props> = (props) => {
const [messageStyle, setMessageStyle] = usePreference('chat.message.style')
const [fontSize, setFontSize] = usePreference('chat.message.font_size')
const [language] = usePreference('app.language')
const [targetLanguage, setTargetLanguage] = usePreference('feature.translate.target_language')
const [sendMessageShortcut, setSendMessageShortcut] = usePreference('chat.input.send_message_shortcut')
const [messageFont, setMessageFont] = usePreference('chat.message.font')
const [showPrompt, setShowPrompt] = usePreference('chat.message.show_prompt')
const [confirmDeleteMessage, setConfirmDeleteMessage] = usePreference('chat.message.confirm_delete')
const [confirmRegenerateMessage, setConfirmRegenerateMessage] = usePreference('chat.message.confirm_regenerate')
const [showTranslateConfirm, setShowTranslateConfirm] = usePreference('chat.input.translate.show_confirm')
const [enableQuickPanelTriggers, setEnableQuickPanelTriggers] = usePreference(
'chat.input.quick_panel.triggers_enabled'
)
const [messageNavigation, setMessageNavigation] = usePreference('chat.message.navigation_mode')
const [thoughtAutoCollapse, setThoughtAutoCollapse] = usePreference('chat.message.thought.auto_collapse')
const [multiModelMessageStyle, setMultiModelMessageStyle] = usePreference('chat.message.multi_model.style')
const [pasteLongTextAsFile, setPasteLongTextAsFile] = usePreference('chat.input.paste_long_text_as_file')
const [pasteLongTextThreshold, setPasteLongTextThreshold] = usePreference('chat.input.paste_long_text_threshold')
const [mathEngine, setMathEngine] = usePreference('chat.message.math.engine')
const [mathEnableSingleDollar, setMathEnableSingleDollar] = usePreference('chat.message.math.single_dollar')
const [showInputEstimatedTokens, setShowInputEstimatedTokens] = usePreference('chat.input.show_estimated_tokens')
const [renderInputMessageAsMarkdown, setRenderInputMessageAsMarkdown] = usePreference(
'chat.message.render_as_markdown'
)
const [autoTranslateWithSpace, setAutoTranslateWithSpace] = usePreference(
'chat.input.translate.auto_translate_with_space'
)
const [showMessageOutline, setShowMessageOutline] = usePreference('chat.message.show_outline')
const [codeShowLineNumbers, setCodeShowLineNumbers] = usePreference('chat.code.show_line_numbers')
const [codeCollapsible, setCodeCollapsible] = usePreference('chat.code.collapsible')
const [codeWrappable, setCodeWrappable] = usePreference('chat.code.wrappable')
const [codeImageTools, setCodeImageTools] = usePreference('chat.code.image_tools')
const [codeEditor, setCodeEditor] = useMultiplePreferences({
enabled: 'chat.code.editor.enabled',
themeLight: 'chat.code.editor.theme_light',
themeDark: 'chat.code.editor.theme_dark',
highlightActiveLine: 'chat.code.editor.highlight_active_line',
foldGutter: 'chat.code.editor.fold_gutter',
autocompletion: 'chat.code.editor.autocompletion',
keymap: 'chat.code.editor.keymap'
})
const [codeViewer, setCodeViewer] = useMultiplePreferences({
themeLight: 'chat.code.viewer.theme_light',
themeDark: 'chat.code.viewer.theme_dark'
})
const [codeExecution, setCodeExecution] = useMultiplePreferences({
enabled: 'chat.code.execution.enabled',
timeoutMinutes: 'chat.code.execution.timeout_minutes'
})
const { assistant, updateAssistantSettings } = useAssistant(props.assistant.id) const { assistant, updateAssistantSettings } = useAssistant(props.assistant.id)
const { provider } = useProvider(assistant.model.provider) const { provider } = useProvider(assistant.model.provider)
const { messageStyle, fontSize, language } = useSettings()
const { theme } = useTheme() const { theme } = useTheme()
const { themeNames } = useCodeStyle() const { themeNames } = useCodeStyle()
@ -80,39 +99,6 @@ const SettingsTab: FC<Props> = (props) => {
const { t } = useTranslation() const { t } = useTranslation()
const dispatch = useAppDispatch()
const {
showPrompt,
messageFont,
showInputEstimatedTokens,
sendMessageShortcut,
setSendMessageShortcut,
targetLanguage,
setTargetLanguage,
pasteLongTextAsFile,
renderInputMessageAsMarkdown,
codeShowLineNumbers,
codeCollapsible,
codeWrappable,
codeEditor,
codeViewer,
codeImageTools,
codeExecution,
mathEngine,
mathEnableSingleDollar,
autoTranslateWithSpace,
pasteLongTextThreshold,
multiModelMessageStyle,
thoughtAutoCollapse,
messageNavigation,
enableQuickPanelTriggers,
showTranslateConfirm,
showMessageOutline,
confirmDeleteMessage,
confirmRegenerateMessage
} = useSettings()
const onUpdateAssistantSettings = (settings: Partial<AssistantSettings>) => { const onUpdateAssistantSettings = (settings: Partial<AssistantSettings>) => {
updateAssistantSettings(settings) updateAssistantSettings(settings)
} }
@ -156,9 +142,9 @@ const SettingsTab: FC<Props> = (props) => {
(value: CodeStyleVarious) => { (value: CodeStyleVarious) => {
const field = theme === ThemeMode.light ? 'themeLight' : 'themeDark' const field = theme === ThemeMode.light ? 'themeLight' : 'themeDark'
const action = codeEditor.enabled ? setCodeEditor : setCodeViewer const action = codeEditor.enabled ? setCodeEditor : setCodeViewer
dispatch(action({ [field]: value })) action({ [field]: value })
}, },
[dispatch, theme, codeEditor.enabled] [theme, codeEditor.enabled, setCodeEditor, setCodeViewer]
) )
useEffect(() => { useEffect(() => {
@ -313,7 +299,7 @@ const SettingsTab: FC<Props> = (props) => {
<SettingGroup> <SettingGroup>
<SettingRow> <SettingRow>
<SettingRowTitleSmall>{t('settings.messages.prompt')}</SettingRowTitleSmall> <SettingRowTitleSmall>{t('settings.messages.prompt')}</SettingRowTitleSmall>
<Switch size="small" checked={showPrompt} onChange={(checked) => dispatch(setShowPrompt(checked))} /> <Switch size="small" checked={showPrompt} onChange={(checked) => setShowPrompt(checked)} />
</SettingRow> </SettingRow>
<SettingDivider /> <SettingDivider />
<SettingRow> <SettingRow>
@ -321,7 +307,7 @@ const SettingsTab: FC<Props> = (props) => {
<Switch <Switch
size="small" size="small"
checked={messageFont === 'serif'} checked={messageFont === 'serif'}
onChange={(checked) => dispatch(setMessageFont(checked ? 'serif' : 'system'))} onChange={(checked) => setMessageFont(checked ? 'serif' : 'system')}
/> />
</SettingRow> </SettingRow>
<SettingDivider /> <SettingDivider />
@ -335,24 +321,20 @@ const SettingsTab: FC<Props> = (props) => {
<Switch <Switch
size="small" size="small"
checked={thoughtAutoCollapse} checked={thoughtAutoCollapse}
onChange={(checked) => dispatch(setThoughtAutoCollapse(checked))} onChange={(checked) => setThoughtAutoCollapse(checked)}
/> />
</SettingRow> </SettingRow>
<SettingDivider /> <SettingDivider />
<SettingRow> <SettingRow>
<SettingRowTitleSmall>{t('settings.messages.show_message_outline')}</SettingRowTitleSmall> <SettingRowTitleSmall>{t('settings.messages.show_message_outline')}</SettingRowTitleSmall>
<Switch <Switch size="small" checked={showMessageOutline} onChange={(checked) => setShowMessageOutline(checked)} />
size="small"
checked={showMessageOutline}
onChange={(checked) => dispatch(setShowMessageOutline(checked))}
/>
</SettingRow> </SettingRow>
<SettingDivider /> <SettingDivider />
<SettingRow> <SettingRow>
<SettingRowTitleSmall>{t('message.message.style.label')}</SettingRowTitleSmall> <SettingRowTitleSmall>{t('message.message.style.label')}</SettingRowTitleSmall>
<Selector <Selector
value={messageStyle} value={messageStyle}
onChange={(value) => dispatch(setMessageStyle(value as 'plain' | 'bubble'))} onChange={(value) => setMessageStyle(value as 'plain' | 'bubble')}
options={[ options={[
{ value: 'plain', label: t('message.message.style.plain') }, { value: 'plain', label: t('message.message.style.plain') },
{ value: 'bubble', label: t('message.message.style.bubble') } { value: 'bubble', label: t('message.message.style.bubble') }
@ -364,7 +346,7 @@ const SettingsTab: FC<Props> = (props) => {
<SettingRowTitleSmall>{t('message.message.multi_model_style.label')}</SettingRowTitleSmall> <SettingRowTitleSmall>{t('message.message.multi_model_style.label')}</SettingRowTitleSmall>
<Selector <Selector
value={multiModelMessageStyle} value={multiModelMessageStyle}
onChange={(value) => dispatch(setMultiModelMessageStyle(value))} onChange={(value) => setMultiModelMessageStyle(value)}
options={[ options={[
{ value: 'fold', label: t('message.message.multi_model_style.fold.label') }, { value: 'fold', label: t('message.message.multi_model_style.fold.label') },
{ value: 'vertical', label: t('message.message.multi_model_style.vertical') }, { value: 'vertical', label: t('message.message.multi_model_style.vertical') },
@ -378,7 +360,7 @@ const SettingsTab: FC<Props> = (props) => {
<SettingRowTitleSmall>{t('settings.messages.navigation.label')}</SettingRowTitleSmall> <SettingRowTitleSmall>{t('settings.messages.navigation.label')}</SettingRowTitleSmall>
<Selector <Selector
value={messageNavigation} value={messageNavigation}
onChange={(value) => dispatch(setMessageNavigation(value as 'none' | 'buttons' | 'anchor'))} onChange={(value) => setMessageNavigation(value as 'none' | 'buttons' | 'anchor')}
options={[ options={[
{ value: 'none', label: t('settings.messages.navigation.none') }, { value: 'none', label: t('settings.messages.navigation.none') },
{ value: 'buttons', label: t('settings.messages.navigation.buttons') }, { value: 'buttons', label: t('settings.messages.navigation.buttons') },
@ -395,7 +377,7 @@ const SettingsTab: FC<Props> = (props) => {
<Slider <Slider
value={fontSizeValue} value={fontSizeValue}
onChange={(value) => setFontSizeValue(value)} onChange={(value) => setFontSizeValue(value)}
onChangeComplete={(value) => dispatch(setFontSize(value))} onChangeComplete={(value) => setFontSize(value)}
min={12} min={12}
max={22} max={22}
step={1} step={1}
@ -416,7 +398,7 @@ const SettingsTab: FC<Props> = (props) => {
<SettingRowTitleSmall>{t('settings.math.engine.label')}</SettingRowTitleSmall> <SettingRowTitleSmall>{t('settings.math.engine.label')}</SettingRowTitleSmall>
<Selector <Selector
value={mathEngine} value={mathEngine}
onChange={(value) => dispatch(setMathEngine(value as MathEngine))} onChange={(value) => setMathEngine(value as MathEngine)}
options={[ options={[
{ value: 'KaTeX', label: 'KaTeX' }, { value: 'KaTeX', label: 'KaTeX' },
{ value: 'MathJax', label: 'MathJax' }, { value: 'MathJax', label: 'MathJax' },
@ -435,7 +417,7 @@ const SettingsTab: FC<Props> = (props) => {
<Switch <Switch
size="small" size="small"
checked={mathEnableSingleDollar} checked={mathEnableSingleDollar}
onChange={(checked) => dispatch(setMathEnableSingleDollar(checked))} onChange={(checked) => setMathEnableSingleDollar(checked)}
/> />
</SettingRow> </SettingRow>
<SettingDivider /> <SettingDivider />
@ -465,7 +447,7 @@ const SettingsTab: FC<Props> = (props) => {
<Switch <Switch
size="small" size="small"
checked={codeExecution.enabled} checked={codeExecution.enabled}
onChange={(checked) => dispatch(setCodeExecution({ enabled: checked }))} onChange={(checked) => setCodeExecution({ enabled: checked })}
/> />
</SettingRow> </SettingRow>
{codeExecution.enabled && ( {codeExecution.enabled && (
@ -484,7 +466,7 @@ const SettingsTab: FC<Props> = (props) => {
max={60} max={60}
step={1} step={1}
value={codeExecution.timeoutMinutes} value={codeExecution.timeoutMinutes}
onChange={(value) => dispatch(setCodeExecution({ timeoutMinutes: value ?? 1 }))} onChange={(value) => setCodeExecution({ timeoutMinutes: value ?? 1 })}
style={{ width: 80 }} style={{ width: 80 }}
/> />
</SettingRow> </SettingRow>
@ -496,7 +478,7 @@ const SettingsTab: FC<Props> = (props) => {
<Switch <Switch
size="small" size="small"
checked={codeEditor.enabled} checked={codeEditor.enabled}
onChange={(checked) => dispatch(setCodeEditor({ enabled: checked }))} onChange={(checked) => setCodeEditor({ enabled: checked })}
/> />
</SettingRow> </SettingRow>
{codeEditor.enabled && ( {codeEditor.enabled && (
@ -507,7 +489,7 @@ const SettingsTab: FC<Props> = (props) => {
<Switch <Switch
size="small" size="small"
checked={codeEditor.highlightActiveLine} checked={codeEditor.highlightActiveLine}
onChange={(checked) => dispatch(setCodeEditor({ highlightActiveLine: checked }))} onChange={(checked) => setCodeEditor({ highlightActiveLine: checked })}
/> />
</SettingRow> </SettingRow>
<SettingDivider /> <SettingDivider />
@ -516,7 +498,7 @@ const SettingsTab: FC<Props> = (props) => {
<Switch <Switch
size="small" size="small"
checked={codeEditor.foldGutter} checked={codeEditor.foldGutter}
onChange={(checked) => dispatch(setCodeEditor({ foldGutter: checked }))} onChange={(checked) => setCodeEditor({ foldGutter: checked })}
/> />
</SettingRow> </SettingRow>
<SettingDivider /> <SettingDivider />
@ -525,7 +507,7 @@ const SettingsTab: FC<Props> = (props) => {
<Switch <Switch
size="small" size="small"
checked={codeEditor.autocompletion} checked={codeEditor.autocompletion}
onChange={(checked) => dispatch(setCodeEditor({ autocompletion: checked }))} onChange={(checked) => setCodeEditor({ autocompletion: checked })}
/> />
</SettingRow> </SettingRow>
<SettingDivider /> <SettingDivider />
@ -534,7 +516,7 @@ const SettingsTab: FC<Props> = (props) => {
<Switch <Switch
size="small" size="small"
checked={codeEditor.keymap} checked={codeEditor.keymap}
onChange={(checked) => dispatch(setCodeEditor({ keymap: checked }))} onChange={(checked) => setCodeEditor({ keymap: checked })}
/> />
</SettingRow> </SettingRow>
</> </>
@ -545,31 +527,23 @@ const SettingsTab: FC<Props> = (props) => {
<Switch <Switch
size="small" size="small"
checked={codeShowLineNumbers} checked={codeShowLineNumbers}
onChange={(checked) => dispatch(setCodeShowLineNumbers(checked))} onChange={(checked) => setCodeShowLineNumbers(checked)}
/> />
</SettingRow> </SettingRow>
<SettingDivider /> <SettingDivider />
<SettingRow> <SettingRow>
<SettingRowTitleSmall>{t('chat.settings.code_collapsible')}</SettingRowTitleSmall> <SettingRowTitleSmall>{t('chat.settings.code_collapsible')}</SettingRowTitleSmall>
<Switch <Switch size="small" checked={codeCollapsible} onChange={(checked) => setCodeCollapsible(checked)} />
size="small"
checked={codeCollapsible}
onChange={(checked) => dispatch(setCodeCollapsible(checked))}
/>
</SettingRow> </SettingRow>
<SettingDivider /> <SettingDivider />
<SettingRow> <SettingRow>
<SettingRowTitleSmall>{t('chat.settings.code_wrappable')}</SettingRowTitleSmall> <SettingRowTitleSmall>{t('chat.settings.code_wrappable')}</SettingRowTitleSmall>
<Switch size="small" checked={codeWrappable} onChange={(checked) => dispatch(setCodeWrappable(checked))} /> <Switch size="small" checked={codeWrappable} onChange={(checked) => setCodeWrappable(checked)} />
</SettingRow> </SettingRow>
<SettingDivider /> <SettingDivider />
<SettingRow> <SettingRow>
<SettingRowTitleSmall>{t('chat.settings.code_image_tools')}</SettingRowTitleSmall> <SettingRowTitleSmall>{t('chat.settings.code_image_tools')}</SettingRowTitleSmall>
<Switch <Switch size="small" checked={codeImageTools} onChange={(checked) => setCodeImageTools(checked)} />
size="small"
checked={codeImageTools}
onChange={(checked) => dispatch(setCodeImageTools(checked))}
/>
</SettingRow> </SettingRow>
</SettingGroup> </SettingGroup>
<SettingDivider /> <SettingDivider />
@ -581,7 +555,7 @@ const SettingsTab: FC<Props> = (props) => {
<Switch <Switch
size="small" size="small"
checked={showInputEstimatedTokens} checked={showInputEstimatedTokens}
onChange={(checked) => dispatch(setShowInputEstimatedTokens(checked))} onChange={(checked) => setShowInputEstimatedTokens(checked)}
/> />
</SettingRow> </SettingRow>
<SettingDivider /> <SettingDivider />
@ -590,7 +564,7 @@ const SettingsTab: FC<Props> = (props) => {
<Switch <Switch
size="small" size="small"
checked={pasteLongTextAsFile} checked={pasteLongTextAsFile}
onChange={(checked) => dispatch(setPasteLongTextAsFile(checked))} onChange={(checked) => setPasteLongTextAsFile(checked)}
/> />
</SettingRow> </SettingRow>
{pasteLongTextAsFile && ( {pasteLongTextAsFile && (
@ -604,7 +578,7 @@ const SettingsTab: FC<Props> = (props) => {
max={10000} max={10000}
step={100} step={100}
value={pasteLongTextThreshold} value={pasteLongTextThreshold}
onChange={(value) => dispatch(setPasteLongTextThreshold(value ?? 500))} onChange={(value) => setPasteLongTextThreshold(value ?? 500)}
style={{ width: 80 }} style={{ width: 80 }}
/> />
</SettingRow> </SettingRow>
@ -616,18 +590,18 @@ const SettingsTab: FC<Props> = (props) => {
<Switch <Switch
size="small" size="small"
checked={renderInputMessageAsMarkdown} checked={renderInputMessageAsMarkdown}
onChange={(checked) => dispatch(setRenderInputMessageAsMarkdown(checked))} onChange={(checked) => setRenderInputMessageAsMarkdown(checked)}
/> />
</SettingRow> </SettingRow>
<SettingDivider /> <SettingDivider />
{!language.startsWith('en') && ( {!(language || navigator.language).startsWith('en') && (
<> <>
<SettingRow> <SettingRow>
<SettingRowTitleSmall>{t('settings.input.auto_translate_with_space')}</SettingRowTitleSmall> <SettingRowTitleSmall>{t('settings.input.auto_translate_with_space')}</SettingRowTitleSmall>
<Switch <Switch
size="small" size="small"
checked={autoTranslateWithSpace} checked={autoTranslateWithSpace}
onChange={(checked) => dispatch(setAutoTranslateWithSpace(checked))} onChange={(checked) => setAutoTranslateWithSpace(checked)}
/> />
</SettingRow> </SettingRow>
<SettingDivider /> <SettingDivider />
@ -638,7 +612,7 @@ const SettingsTab: FC<Props> = (props) => {
<Switch <Switch
size="small" size="small"
checked={showTranslateConfirm} checked={showTranslateConfirm}
onChange={(checked) => dispatch(setShowTranslateConfirm(checked))} onChange={(checked) => setShowTranslateConfirm(checked)}
/> />
</SettingRow> </SettingRow>
<SettingDivider /> <SettingDivider />
@ -647,7 +621,7 @@ const SettingsTab: FC<Props> = (props) => {
<Switch <Switch
size="small" size="small"
checked={enableQuickPanelTriggers} checked={enableQuickPanelTriggers}
onChange={(checked) => dispatch(setEnableQuickPanelTriggers(checked))} onChange={(checked) => setEnableQuickPanelTriggers(checked)}
/> />
</SettingRow> </SettingRow>
<SettingDivider /> <SettingDivider />
@ -656,7 +630,7 @@ const SettingsTab: FC<Props> = (props) => {
<Switch <Switch
size="small" size="small"
checked={confirmDeleteMessage} checked={confirmDeleteMessage}
onChange={(checked) => dispatch(setConfirmDeleteMessage(checked))} onChange={(checked) => setConfirmDeleteMessage(checked)}
/> />
</SettingRow> </SettingRow>
<SettingDivider /> <SettingDivider />
@ -665,7 +639,7 @@ const SettingsTab: FC<Props> = (props) => {
<Switch <Switch
size="small" size="small"
checked={confirmRegenerateMessage} checked={confirmRegenerateMessage}
onChange={(checked) => dispatch(setConfirmRegenerateMessage(checked))} onChange={(checked) => setConfirmRegenerateMessage(checked)}
/> />
</SettingRow> </SettingRow>
<SettingDivider /> <SettingDivider />

View File

@ -1,14 +1,8 @@
import { UndoOutlined } from '@ant-design/icons' // 导入重置图标 import { UndoOutlined } from '@ant-design/icons' // 导入重置图标
import { usePreference } from '@data/hooks/usePreference'
import { DEFAULT_MIN_APPS } from '@renderer/config/minapps' import { DEFAULT_MIN_APPS } from '@renderer/config/minapps'
import { useMinapps } from '@renderer/hooks/useMinapps' import { useMinapps } from '@renderer/hooks/useMinapps'
import { useSettings } from '@renderer/hooks/useSettings'
import { SettingDescription, SettingDivider, SettingRowTitle, SettingTitle } from '@renderer/pages/settings' import { SettingDescription, SettingDivider, SettingRowTitle, SettingTitle } from '@renderer/pages/settings'
import { useAppDispatch } from '@renderer/store'
import {
setMaxKeepAliveMinapps,
setMinappsOpenLinkExternal,
setShowOpenedMinappsInSidebar
} from '@renderer/store/settings'
import { Button, message, Slider, Switch, Tooltip } from 'antd' import { Button, message, Slider, Switch, Tooltip } from 'antd'
import { FC, useCallback, useEffect, useRef, useState } from 'react' import { FC, useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@ -21,8 +15,13 @@ const DEFAULT_MAX_KEEPALIVE = 3
const MiniAppSettings: FC = () => { const MiniAppSettings: FC = () => {
const { t } = useTranslation() const { t } = useTranslation()
const dispatch = useAppDispatch()
const { maxKeepAliveMinapps, showOpenedMinappsInSidebar, minappsOpenLinkExternal } = useSettings() const [maxKeepAliveMinapps, setMaxKeepAliveMinapps] = usePreference('feature.minapp.max_keep_alive')
const [showOpenedMinappsInSidebar, setShowOpenedMinappsInSidebar] = usePreference(
'feature.minapp.show_opened_in_sidebar'
)
const [minappsOpenLinkExternal, setMinappsOpenLinkExternal] = usePreference('feature.minapp.open_link_external')
const { minapps, disabled, updateMinapps, updateDisabledMinapps } = useMinapps() const { minapps, disabled, updateMinapps, updateDisabledMinapps } = useMinapps()
const [visibleMiniApps, setVisibleMiniApps] = useState(minapps) const [visibleMiniApps, setVisibleMiniApps] = useState(minapps)
@ -45,14 +44,14 @@ const MiniAppSettings: FC = () => {
// 恢复默认缓存数量 // 恢复默认缓存数量
const handleResetCacheLimit = useCallback(() => { const handleResetCacheLimit = useCallback(() => {
dispatch(setMaxKeepAliveMinapps(DEFAULT_MAX_KEEPALIVE)) setMaxKeepAliveMinapps(DEFAULT_MAX_KEEPALIVE)
messageApi.info(t('settings.miniapps.cache_change_notice')) messageApi.info(t('settings.miniapps.cache_change_notice'))
}, [dispatch, messageApi, t]) }, [messageApi, t, setMaxKeepAliveMinapps])
// 处理缓存数量变更 // 处理缓存数量变更
const handleCacheChange = useCallback( const handleCacheChange = useCallback(
(value: number) => { (value: number) => {
dispatch(setMaxKeepAliveMinapps(value)) setMaxKeepAliveMinapps(value)
if (debounceTimerRef.current) { if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current) clearTimeout(debounceTimerRef.current)
@ -63,7 +62,7 @@ const MiniAppSettings: FC = () => {
debounceTimerRef.current = null debounceTimerRef.current = null
}, 500) }, 500)
}, },
[dispatch, messageApi, t] [messageApi, t, setMaxKeepAliveMinapps]
) )
// 组件卸载时清除定时器 // 组件卸载时清除定时器
@ -97,10 +96,7 @@ const MiniAppSettings: FC = () => {
<SettingLabelGroup> <SettingLabelGroup>
<SettingRowTitle>{t('settings.miniapps.open_link_external.title')}</SettingRowTitle> <SettingRowTitle>{t('settings.miniapps.open_link_external.title')}</SettingRowTitle>
</SettingLabelGroup> </SettingLabelGroup>
<Switch <Switch checked={minappsOpenLinkExternal} onChange={(checked) => setMinappsOpenLinkExternal(checked)} />
checked={minappsOpenLinkExternal}
onChange={(checked) => dispatch(setMinappsOpenLinkExternal(checked))}
/>
</SettingRow> </SettingRow>
<SettingDivider /> <SettingDivider />
{/* 缓存小程序数量设置 */} {/* 缓存小程序数量设置 */}
@ -137,10 +133,7 @@ const MiniAppSettings: FC = () => {
<SettingRowTitle>{t('settings.miniapps.sidebar_title')}</SettingRowTitle> <SettingRowTitle>{t('settings.miniapps.sidebar_title')}</SettingRowTitle>
<SettingDescription>{t('settings.miniapps.sidebar_description')}</SettingDescription> <SettingDescription>{t('settings.miniapps.sidebar_description')}</SettingDescription>
</SettingLabelGroup> </SettingLabelGroup>
<Switch <Switch checked={showOpenedMinappsInSidebar} onChange={(checked) => setShowOpenedMinappsInSidebar(checked)} />
checked={showOpenedMinappsInSidebar}
onChange={(checked) => dispatch(setShowOpenedMinappsInSidebar(checked))}
/>
</SettingRow> </SettingRow>
</Container> </Container>
) )

View File

@ -1,11 +1,11 @@
import { GithubOutlined } from '@ant-design/icons' import { GithubOutlined } from '@ant-design/icons'
import { usePreference } from '@data/hooks/usePreference'
import IndicatorLight from '@renderer/components/IndicatorLight' import IndicatorLight from '@renderer/components/IndicatorLight'
import { HStack } from '@renderer/components/Layout' import { HStack } from '@renderer/components/Layout'
import { APP_NAME, AppLogo } from '@renderer/config/env' import { APP_NAME, AppLogo } from '@renderer/config/env'
import { useTheme } from '@renderer/context/ThemeProvider' import { useTheme } from '@renderer/context/ThemeProvider'
import { useMinappPopup } from '@renderer/hooks/useMinappPopup' import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
import { useRuntime } from '@renderer/hooks/useRuntime' import { useRuntime } from '@renderer/hooks/useRuntime'
import { useSettings } from '@renderer/hooks/useSettings'
import i18n from '@renderer/i18n' import i18n from '@renderer/i18n'
import { handleSaveData, useAppDispatch } from '@renderer/store' import { handleSaveData, useAppDispatch } from '@renderer/store'
import { setUpdateState } from '@renderer/store/runtime' import { setUpdateState } from '@renderer/store/runtime'
@ -25,10 +25,13 @@ import styled from 'styled-components'
import { SettingContainer, SettingDivider, SettingGroup, SettingRow, SettingTitle } from '.' import { SettingContainer, SettingDivider, SettingGroup, SettingRow, SettingTitle } from '.'
const AboutSettings: FC = () => { const AboutSettings: FC = () => {
const [autoCheckUpdate, setAutoCheckUpdate] = usePreference('app.dist.auto_update.enabled')
const [testPlan, setTestPlan] = usePreference('app.dist.test_plan.enabled')
const [testChannel, setTestChannel] = usePreference('app.dist.test_plan.channel')
const [version, setVersion] = useState('') const [version, setVersion] = useState('')
const [isPortable, setIsPortable] = useState(false) const [isPortable, setIsPortable] = useState(false)
const { t } = useTranslation() const { t } = useTranslation()
const { autoCheckUpdate, setAutoCheckUpdate, testPlan, setTestPlan, testChannel, setTestChannel } = useSettings()
const { theme } = useTheme() const { theme } = useTheme()
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const { update } = useRuntime() const { update } = useRuntime()

View File

@ -1,27 +1,31 @@
import { useMultiplePreferences } from '@data/hooks/usePreference'
import { useTheme } from '@renderer/context/ThemeProvider' import { useTheme } from '@renderer/context/ThemeProvider'
import { RootState, useAppDispatch } from '@renderer/store'
import { setExportMenuOptions } from '@renderer/store/settings'
import { Switch } from 'antd' import { Switch } from 'antd'
import { FC } from 'react' import { FC } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import { SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..' import { SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..'
const ExportMenuOptions: FC = () => { const ExportMenuOptions: FC = () => {
const { t } = useTranslation() const { t } = useTranslation()
const { theme } = useTheme() const { theme } = useTheme()
const dispatch = useAppDispatch()
const exportMenuOptions = useSelector((state: RootState) => state.settings.exportMenuOptions) const [exportMenuOptions, setExportMenuOptions] = useMultiplePreferences({
image: 'data.export.menus.image',
markdown: 'data.export.menus.markdown',
markdown_reason: 'data.export.menus.markdown_reason',
notion: 'data.export.menus.notion',
yuque: 'data.export.menus.yuque',
joplin: 'data.export.menus.joplin',
obsidian: 'data.export.menus.obsidian',
siyuan: 'data.export.menus.siyuan',
docx: 'data.export.menus.docx',
plain_text: 'data.export.menus.plain_text'
})
const handleToggleOption = (option: string, checked: boolean) => { const handleToggleOption = (option: string, checked: boolean) => {
dispatch(
setExportMenuOptions({ setExportMenuOptions({
...exportMenuOptions,
[option]: checked [option]: checked
}) })
)
} }
return ( return (

View File

@ -1,33 +1,30 @@
import { InfoCircleOutlined } from '@ant-design/icons' import { InfoCircleOutlined } from '@ant-design/icons'
import { usePreference } from '@data/hooks/usePreference'
import { HStack } from '@renderer/components/Layout' import { HStack } from '@renderer/components/Layout'
import { useTheme } from '@renderer/context/ThemeProvider' import { useTheme } from '@renderer/context/ThemeProvider'
import { useMinappPopup } from '@renderer/hooks/useMinappPopup' import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
import { RootState, useAppDispatch } from '@renderer/store'
import { setJoplinExportReasoning, setJoplinToken, setJoplinUrl } from '@renderer/store/settings'
import { Button, Space, Switch, Tooltip } from 'antd' import { Button, Space, Switch, Tooltip } from 'antd'
import { Input } from 'antd' import { Input } from 'antd'
import { FC } from 'react' import { FC } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import { SettingDivider, SettingGroup, SettingHelpText, SettingRow, SettingRowTitle, SettingTitle } from '..' import { SettingDivider, SettingGroup, SettingHelpText, SettingRow, SettingRowTitle, SettingTitle } from '..'
const JoplinSettings: FC = () => { const JoplinSettings: FC = () => {
const [joplinToken, setJoplinToken] = usePreference('data.integration.joplin.token')
const [joplinUrl, setJoplinUrl] = usePreference('data.integration.joplin.url')
const [joplinExportReasoning, setJoplinExportReasoning] = usePreference('data.integration.joplin.export_reasoning')
const { t } = useTranslation() const { t } = useTranslation()
const { theme } = useTheme() const { theme } = useTheme()
const dispatch = useAppDispatch()
const { openMinapp } = useMinappPopup() const { openMinapp } = useMinappPopup()
const joplinToken = useSelector((state: RootState) => state.settings.joplinToken)
const joplinUrl = useSelector((state: RootState) => state.settings.joplinUrl)
const joplinExportReasoning = useSelector((state: RootState) => state.settings.joplinExportReasoning)
const handleJoplinTokenChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleJoplinTokenChange = (e: React.ChangeEvent<HTMLInputElement>) => {
dispatch(setJoplinToken(e.target.value)) setJoplinToken(e.target.value)
} }
const handleJoplinUrlChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleJoplinUrlChange = (e: React.ChangeEvent<HTMLInputElement>) => {
dispatch(setJoplinUrl(e.target.value)) setJoplinUrl(e.target.value)
} }
const handleJoplinUrlBlur = (e: React.FocusEvent<HTMLInputElement>) => { const handleJoplinUrlBlur = (e: React.FocusEvent<HTMLInputElement>) => {
@ -35,7 +32,7 @@ const JoplinSettings: FC = () => {
// 确保URL以/结尾,但只在失去焦点时执行 // 确保URL以/结尾,但只在失去焦点时执行
if (url && !url.endsWith('/')) { if (url && !url.endsWith('/')) {
url = `${url}/` url = `${url}/`
dispatch(setJoplinUrl(url)) setJoplinUrl(url)
} }
} }
@ -74,7 +71,7 @@ const JoplinSettings: FC = () => {
} }
const handleToggleJoplinExportReasoning = (checked: boolean) => { const handleToggleJoplinExportReasoning = (checked: boolean) => {
dispatch(setJoplinExportReasoning(checked)) setJoplinExportReasoning(checked)
} }
return ( return (

View File

@ -1,20 +1,13 @@
import { DeleteOutlined, FolderOpenOutlined, SaveOutlined, SyncOutlined, WarningOutlined } from '@ant-design/icons' import { DeleteOutlined, FolderOpenOutlined, SaveOutlined, SyncOutlined, WarningOutlined } from '@ant-design/icons'
import { usePreference } from '@data/hooks/usePreference'
import { loggerService } from '@logger' import { loggerService } from '@logger'
import { HStack } from '@renderer/components/Layout' import { HStack } from '@renderer/components/Layout'
import { LocalBackupManager } from '@renderer/components/LocalBackupManager' import { LocalBackupManager } from '@renderer/components/LocalBackupManager'
import { LocalBackupModal, useLocalBackupModal } from '@renderer/components/LocalBackupModals' import { LocalBackupModal, useLocalBackupModal } from '@renderer/components/LocalBackupModals'
import Selector from '@renderer/components/Selector' import Selector from '@renderer/components/Selector'
import { useTheme } from '@renderer/context/ThemeProvider' import { useTheme } from '@renderer/context/ThemeProvider'
import { useSettings } from '@renderer/hooks/useSettings'
import { startAutoSync, stopAutoSync } from '@renderer/services/BackupService' import { startAutoSync, stopAutoSync } from '@renderer/services/BackupService'
import { useAppDispatch, useAppSelector } from '@renderer/store' import { useAppSelector } from '@renderer/store'
import {
setLocalBackupAutoSync,
setLocalBackupDir as _setLocalBackupDir,
setLocalBackupMaxBackups as _setLocalBackupMaxBackups,
setLocalBackupSkipBackupFile as _setLocalBackupSkipBackupFile,
setLocalBackupSyncInterval as _setLocalBackupSyncInterval
} from '@renderer/store/settings'
import { AppInfo } from '@renderer/types' import { AppInfo } from '@renderer/types'
import { Button, Input, Switch, Tooltip } from 'antd' import { Button, Input, Switch, Tooltip } from 'antd'
import dayjs from 'dayjs' import dayjs from 'dayjs'
@ -22,27 +15,18 @@ import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { SettingDivider, SettingGroup, SettingHelpText, SettingRow, SettingRowTitle, SettingTitle } from '..' import { SettingDivider, SettingGroup, SettingHelpText, SettingRow, SettingRowTitle, SettingTitle } from '..'
const logger = loggerService.withContext('LocalBackupSettings') const logger = loggerService.withContext('LocalBackupSettings')
const LocalBackupSettings: React.FC = () => { const LocalBackupSettings: React.FC = () => {
const dispatch = useAppDispatch() const [, setLocalBackupAutoSync] = usePreference('data.backup.local.auto_sync')
const [localBackupDir, setLocalBackupDir] = usePreference('data.backup.local.dir')
const [localBackupMaxBackups, setLocalBackupMaxBackups] = usePreference('data.backup.local.max_backups')
const [localBackupSkipBackupFile, setLocalBackupSkipBackupFile] = usePreference('data.backup.local.skip_backup_file')
const [localBackupSyncInterval, setLocalBackupSyncInterval] = usePreference('data.backup.local.sync_interval')
const {
localBackupDir: localBackupDirSetting,
localBackupSyncInterval: localBackupSyncIntervalSetting,
localBackupMaxBackups: localBackupMaxBackupsSetting,
localBackupSkipBackupFile: localBackupSkipBackupFileSetting
} = useSettings()
const [localBackupDir, setLocalBackupDir] = useState<string | undefined>(localBackupDirSetting)
const [resolvedLocalBackupDir, setResolvedLocalBackupDir] = useState<string | undefined>(undefined) const [resolvedLocalBackupDir, setResolvedLocalBackupDir] = useState<string | undefined>(undefined)
const [localBackupSkipBackupFile, setLocalBackupSkipBackupFile] = useState<boolean>(localBackupSkipBackupFileSetting)
const [backupManagerVisible, setBackupManagerVisible] = useState(false) const [backupManagerVisible, setBackupManagerVisible] = useState(false)
const [syncInterval, setSyncInterval] = useState<number>(localBackupSyncIntervalSetting)
const [maxBackups, setMaxBackups] = useState<number>(localBackupMaxBackupsSetting)
const [appInfo, setAppInfo] = useState<AppInfo>() const [appInfo, setAppInfo] = useState<AppInfo>()
useEffect(() => { useEffect(() => {
@ -50,10 +34,10 @@ const LocalBackupSettings: React.FC = () => {
}, []) }, [])
useEffect(() => { useEffect(() => {
if (localBackupDirSetting) { if (localBackupDir) {
window.api.resolvePath(localBackupDirSetting).then(setResolvedLocalBackupDir) window.api.resolvePath(localBackupDir).then(setResolvedLocalBackupDir)
} }
}, [localBackupDirSetting]) }, [localBackupDir])
const { theme } = useTheme() const { theme } = useTheme()
@ -62,13 +46,12 @@ const LocalBackupSettings: React.FC = () => {
const { localBackupSync } = useAppSelector((state) => state.backup) const { localBackupSync } = useAppSelector((state) => state.backup)
const onSyncIntervalChange = (value: number) => { const onSyncIntervalChange = (value: number) => {
setSyncInterval(value) setLocalBackupSyncInterval(value)
dispatch(_setLocalBackupSyncInterval(value))
if (value === 0) { if (value === 0) {
dispatch(setLocalBackupAutoSync(false)) setLocalBackupAutoSync(false)
stopAutoSync('local') stopAutoSync('local')
} else { } else {
dispatch(setLocalBackupAutoSync(true)) setLocalBackupAutoSync(true)
startAutoSync(false, 'local') startAutoSync(false, 'local')
} }
} }
@ -105,7 +88,7 @@ const LocalBackupSettings: React.FC = () => {
} }
const handleLocalBackupDirChange = async (value: string) => { const handleLocalBackupDirChange = async (value: string) => {
if (value === localBackupDirSetting) { if (value === localBackupDir) {
return return
} }
@ -115,29 +98,26 @@ const LocalBackupSettings: React.FC = () => {
} }
if (await checkLocalBackupDirValid(value)) { if (await checkLocalBackupDirValid(value)) {
setLocalBackupDir(value) await setLocalBackupDir(value)
dispatch(_setLocalBackupDir(value))
setResolvedLocalBackupDir(await window.api.resolvePath(value)) setResolvedLocalBackupDir(await window.api.resolvePath(value))
dispatch(setLocalBackupAutoSync(true)) await setLocalBackupAutoSync(true)
startAutoSync(true, 'local') startAutoSync(true, 'local')
return return
} }
if (localBackupDirSetting) { if (localBackupDir) {
setLocalBackupDir(localBackupDirSetting) await setLocalBackupDir(localBackupDir)
return return
} }
} }
const onMaxBackupsChange = (value: number) => { const onMaxBackupsChange = (value: number) => {
setMaxBackups(value) setLocalBackupMaxBackups(value)
dispatch(_setLocalBackupMaxBackups(value))
} }
const onSkipBackupFilesChange = (value: boolean) => { const onSkipBackupFilesChange = (value: boolean) => {
setLocalBackupSkipBackupFile(value) setLocalBackupSkipBackupFile(value)
dispatch(_setLocalBackupSkipBackupFile(value))
} }
const handleBrowseDirectory = async () => { const handleBrowseDirectory = async () => {
@ -157,10 +137,9 @@ const LocalBackupSettings: React.FC = () => {
} }
} }
const handleClearDirectory = () => { const handleClearDirectory = async () => {
setLocalBackupDir('') await setLocalBackupDir('')
dispatch(_setLocalBackupDir('')) await setLocalBackupAutoSync(false)
dispatch(setLocalBackupAutoSync(false))
stopAutoSync('local') stopAutoSync('local')
} }
@ -238,7 +217,7 @@ const LocalBackupSettings: React.FC = () => {
<SettingRowTitle>{t('settings.data.local.autoSync.label')}</SettingRowTitle> <SettingRowTitle>{t('settings.data.local.autoSync.label')}</SettingRowTitle>
<Selector <Selector
size={14} size={14}
value={syncInterval} value={localBackupSyncInterval}
onChange={onSyncIntervalChange} onChange={onSyncIntervalChange}
disabled={!localBackupDir} disabled={!localBackupDir}
options={[ options={[
@ -260,7 +239,7 @@ const LocalBackupSettings: React.FC = () => {
<SettingRowTitle>{t('settings.data.local.maxBackups.label')}</SettingRowTitle> <SettingRowTitle>{t('settings.data.local.maxBackups.label')}</SettingRowTitle>
<Selector <Selector
size={14} size={14}
value={maxBackups} value={localBackupMaxBackups}
onChange={onMaxBackupsChange} onChange={onMaxBackupsChange}
disabled={!localBackupDir} disabled={!localBackupDir}
options={[ options={[
@ -282,7 +261,7 @@ const LocalBackupSettings: React.FC = () => {
<SettingRow> <SettingRow>
<SettingHelpText>{t('settings.data.backup.skip_file_data_help')}</SettingHelpText> <SettingHelpText>{t('settings.data.backup.skip_file_data_help')}</SettingHelpText>
</SettingRow> </SettingRow>
{localBackupSync && syncInterval > 0 && ( {localBackupSync && localBackupSyncInterval > 0 && (
<> <>
<SettingDivider /> <SettingDivider />
<SettingRow> <SettingRow>

View File

@ -1,70 +1,69 @@
import { DeleteOutlined, FolderOpenOutlined } from '@ant-design/icons' import { DeleteOutlined, FolderOpenOutlined } from '@ant-design/icons'
import { usePreference } from '@data/hooks/usePreference'
import { HStack } from '@renderer/components/Layout' import { HStack } from '@renderer/components/Layout'
import { useTheme } from '@renderer/context/ThemeProvider' import { useTheme } from '@renderer/context/ThemeProvider'
import { RootState, useAppDispatch } from '@renderer/store'
import {
setExcludeCitationsInExport,
setForceDollarMathInMarkdown,
setmarkdownExportPath,
setShowModelNameInMarkdown,
setShowModelProviderInMarkdown,
setStandardizeCitationsInExport,
setUseTopicNamingForMessageTitle
} from '@renderer/store/settings'
import { Button, Switch } from 'antd' import { Button, Switch } from 'antd'
import Input from 'antd/es/input/Input' import Input from 'antd/es/input/Input'
import { FC } from 'react' import { FC } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import { SettingDivider, SettingGroup, SettingHelpText, SettingRow, SettingRowTitle, SettingTitle } from '..' import { SettingDivider, SettingGroup, SettingHelpText, SettingRow, SettingRowTitle, SettingTitle } from '..'
const MarkdownExportSettings: FC = () => { const MarkdownExportSettings: FC = () => {
const { t } = useTranslation() const { t } = useTranslation()
const { theme } = useTheme() const { theme } = useTheme()
const dispatch = useAppDispatch()
const markdownExportPath = useSelector((state: RootState) => state.settings.markdownExportPath) const [markdownExportPath, setmarkdownExportPath] = usePreference('data.export.markdown.path')
const forceDollarMathInMarkdown = useSelector((state: RootState) => state.settings.forceDollarMathInMarkdown) const [forceDollarMathInMarkdown, setForceDollarMathInMarkdown] = usePreference(
const useTopicNamingForMessageTitle = useSelector((state: RootState) => state.settings.useTopicNamingForMessageTitle) 'data.export.markdown.force_dollar_math'
const showModelNameInExport = useSelector((state: RootState) => state.settings.showModelNameInMarkdown) )
const showModelProviderInMarkdown = useSelector((state: RootState) => state.settings.showModelProviderInMarkdown) const [useTopicNamingForMessageTitle, setUseTopicNamingForMessageTitle] = usePreference(
const excludeCitationsInExport = useSelector((state: RootState) => state.settings.excludeCitationsInExport) 'data.export.markdown.use_topic_naming_for_message_title'
const standardizeCitationsInExport = useSelector((state: RootState) => state.settings.standardizeCitationsInExport) )
const [showModelNameInExport, setShowModelNameInMarkdown] = usePreference('data.export.markdown.show_model_name')
const [showModelProviderInMarkdown, setShowModelProviderInMarkdown] = usePreference(
'data.export.markdown.show_model_provider'
)
const [excludeCitationsInExport, setExcludeCitationsInExport] = usePreference(
'data.export.markdown.exclude_citations'
)
const [standardizeCitationsInExport, setStandardizeCitationsInExport] = usePreference(
'data.export.markdown.standardize_citations'
)
const handleSelectFolder = async () => { const handleSelectFolder = async () => {
const path = await window.api.file.selectFolder() const path = await window.api.file.selectFolder()
if (path) { if (path) {
dispatch(setmarkdownExportPath(path)) setmarkdownExportPath(path)
} }
} }
const handleClearPath = () => { const handleClearPath = () => {
dispatch(setmarkdownExportPath(null)) setmarkdownExportPath(null)
} }
const handleToggleForceDollarMath = (checked: boolean) => { const handleToggleForceDollarMath = (checked: boolean) => {
dispatch(setForceDollarMathInMarkdown(checked)) setForceDollarMathInMarkdown(checked)
} }
const handleToggleTopicNaming = (checked: boolean) => { const handleToggleTopicNaming = (checked: boolean) => {
dispatch(setUseTopicNamingForMessageTitle(checked)) setUseTopicNamingForMessageTitle(checked)
} }
const handleToggleShowModelName = (checked: boolean) => { const handleToggleShowModelName = (checked: boolean) => {
dispatch(setShowModelNameInMarkdown(checked)) setShowModelNameInMarkdown(checked)
} }
const handleToggleShowModelProvider = (checked: boolean) => { const handleToggleShowModelProvider = (checked: boolean) => {
dispatch(setShowModelProviderInMarkdown(checked)) setShowModelProviderInMarkdown(checked)
} }
const handleToggleExcludeCitations = (checked: boolean) => { const handleToggleExcludeCitations = (checked: boolean) => {
dispatch(setExcludeCitationsInExport(checked)) setExcludeCitationsInExport(checked)
} }
const handleToggleStandardizeCitations = (checked: boolean) => { const handleToggleStandardizeCitations = (checked: boolean) => {
dispatch(setStandardizeCitationsInExport(checked)) setStandardizeCitationsInExport(checked)
} }
return ( return (

View File

@ -1,43 +1,35 @@
import { InfoCircleOutlined } from '@ant-design/icons' import { InfoCircleOutlined } from '@ant-design/icons'
import { usePreference } from '@data/hooks/usePreference'
import { Client } from '@notionhq/client' import { Client } from '@notionhq/client'
import { HStack } from '@renderer/components/Layout' import { HStack } from '@renderer/components/Layout'
import { useTheme } from '@renderer/context/ThemeProvider' import { useTheme } from '@renderer/context/ThemeProvider'
import { useMinappPopup } from '@renderer/hooks/useMinappPopup' import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
import { RootState, useAppDispatch } from '@renderer/store'
import {
setNotionApiKey,
setNotionDatabaseID,
setNotionExportReasoning,
setNotionPageNameKey
} from '@renderer/store/settings'
import { Button, Space, Switch, Tooltip } from 'antd' import { Button, Space, Switch, Tooltip } from 'antd'
import { Input } from 'antd' import { Input } from 'antd'
import { FC } from 'react' import { FC } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import { SettingDivider, SettingGroup, SettingHelpText, SettingRow, SettingRowTitle, SettingTitle } from '..' import { SettingDivider, SettingGroup, SettingHelpText, SettingRow, SettingRowTitle, SettingTitle } from '..'
const NotionSettings: FC = () => { const NotionSettings: FC = () => {
const [notionApiKey, setNotionApiKey] = usePreference('data.integration.notion.api_key')
const [notionDatabaseID, setNotionDatabaseID] = usePreference('data.integration.notion.database_id')
const [notionPageNameKey, setNotionPageNameKey] = usePreference('data.integration.notion.page_name_key')
const [notionExportReasoning, setNotionExportReasoning] = usePreference('data.integration.notion.export_reasoning')
const { t } = useTranslation() const { t } = useTranslation()
const { theme } = useTheme() const { theme } = useTheme()
const dispatch = useAppDispatch()
const { openMinapp } = useMinappPopup() const { openMinapp } = useMinappPopup()
const notionApiKey = useSelector((state: RootState) => state.settings.notionApiKey)
const notionDatabaseID = useSelector((state: RootState) => state.settings.notionDatabaseID)
const notionPageNameKey = useSelector((state: RootState) => state.settings.notionPageNameKey)
const notionExportReasoning = useSelector((state: RootState) => state.settings.notionExportReasoning)
const handleNotionTokenChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleNotionTokenChange = (e: React.ChangeEvent<HTMLInputElement>) => {
dispatch(setNotionApiKey(e.target.value)) setNotionApiKey(e.target.value)
} }
const handleNotionDatabaseIdChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleNotionDatabaseIdChange = (e: React.ChangeEvent<HTMLInputElement>) => {
dispatch(setNotionDatabaseID(e.target.value)) setNotionDatabaseID(e.target.value)
} }
const handleNotionPageNameKeyChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleNotionPageNameKeyChange = (e: React.ChangeEvent<HTMLInputElement>) => {
dispatch(setNotionPageNameKey(e.target.value)) setNotionPageNameKey(e.target.value)
} }
const handleNotionConnectionCheck = () => { const handleNotionConnectionCheck = () => {
@ -75,7 +67,7 @@ const NotionSettings: FC = () => {
} }
const handleNotionExportReasoningChange = (checked: boolean) => { const handleNotionExportReasoningChange = (checked: boolean) => {
dispatch(setNotionExportReasoning(checked)) setNotionExportReasoning(checked)
} }
return ( return (

View File

@ -1,15 +1,13 @@
import { FolderOpenOutlined, InfoCircleOutlined, SaveOutlined, SyncOutlined, WarningOutlined } from '@ant-design/icons' import { FolderOpenOutlined, InfoCircleOutlined, SaveOutlined, SyncOutlined, WarningOutlined } from '@ant-design/icons'
import { usePreference } from '@data/hooks/usePreference'
import { HStack } from '@renderer/components/Layout' import { HStack } from '@renderer/components/Layout'
import { S3BackupManager } from '@renderer/components/S3BackupManager' import { S3BackupManager } from '@renderer/components/S3BackupManager'
import { S3BackupModal, useS3BackupModal } from '@renderer/components/S3Modals' import { S3BackupModal, useS3BackupModal } from '@renderer/components/S3Modals'
import Selector from '@renderer/components/Selector' import Selector from '@renderer/components/Selector'
import { useTheme } from '@renderer/context/ThemeProvider' import { useTheme } from '@renderer/context/ThemeProvider'
import { useMinappPopup } from '@renderer/hooks/useMinappPopup' import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
import { useSettings } from '@renderer/hooks/useSettings'
import { startAutoSync, stopAutoSync } from '@renderer/services/BackupService' import { startAutoSync, stopAutoSync } from '@renderer/services/BackupService'
import { useAppDispatch, useAppSelector } from '@renderer/store' import { useAppSelector } from '@renderer/store'
import { setS3Partial } from '@renderer/store/settings'
import { S3Config } from '@renderer/types'
import { Button, Input, Switch, Tooltip } from 'antd' import { Button, Input, Switch, Tooltip } from 'antd'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { FC, useState } from 'react' import { FC, useState } from 'react'
@ -18,45 +16,32 @@ import { useTranslation } from 'react-i18next'
import { SettingDivider, SettingGroup, SettingHelpText, SettingRow, SettingRowTitle, SettingTitle } from '..' import { SettingDivider, SettingGroup, SettingHelpText, SettingRow, SettingRowTitle, SettingTitle } from '..'
const S3Settings: FC = () => { const S3Settings: FC = () => {
const { s3 = {} as S3Config } = useSettings() const [, setS3AutoSync] = usePreference('data.backup.s3.auto_sync')
const [s3Endpoint, setS3Endpoint] = usePreference('data.backup.s3.endpoint')
const [s3Region, setS3Region] = usePreference('data.backup.s3.region')
const [s3Bucket, setS3Bucket] = usePreference('data.backup.s3.bucket')
const [s3AccessKeyId, setS3AccessKeyId] = usePreference('data.backup.s3.access_key_id')
const [s3SecretAccessKey, setS3SecretAccessKey] = usePreference('data.backup.s3.secret_access_key')
const [s3Root, setS3Root] = usePreference('data.backup.s3.root')
const [s3SkipBackupFile, setS3SkipBackupFile] = usePreference('data.backup.s3.skip_backup_file')
const [s3SyncInterval, setS3SyncInterval] = usePreference('data.backup.s3.sync_interval')
const [s3MaxBackups, setS3MaxBackups] = usePreference('data.backup.s3.max_backups')
const {
endpoint: s3EndpointInit = '',
region: s3RegionInit = '',
bucket: s3BucketInit = '',
accessKeyId: s3AccessKeyIdInit = '',
secretAccessKey: s3SecretAccessKeyInit = '',
root: s3RootInit = '',
syncInterval: s3SyncIntervalInit = 0,
maxBackups: s3MaxBackupsInit = 5,
skipBackupFile: s3SkipBackupFileInit = false
} = s3
const [endpoint, setEndpoint] = useState<string | undefined>(s3EndpointInit)
const [region, setRegion] = useState<string | undefined>(s3RegionInit)
const [bucket, setBucket] = useState<string | undefined>(s3BucketInit)
const [accessKeyId, setAccessKeyId] = useState<string | undefined>(s3AccessKeyIdInit)
const [secretAccessKey, setSecretAccessKey] = useState<string | undefined>(s3SecretAccessKeyInit)
const [root, setRoot] = useState<string | undefined>(s3RootInit)
const [skipBackupFile, setSkipBackupFile] = useState<boolean>(s3SkipBackupFileInit)
const [backupManagerVisible, setBackupManagerVisible] = useState(false) const [backupManagerVisible, setBackupManagerVisible] = useState(false)
const [syncInterval, setSyncInterval] = useState<number>(s3SyncIntervalInit)
const [maxBackups, setMaxBackups] = useState<number>(s3MaxBackupsInit)
const dispatch = useAppDispatch()
const { theme } = useTheme() const { theme } = useTheme()
const { t } = useTranslation() const { t } = useTranslation()
const { openMinapp } = useMinappPopup() const { openMinapp } = useMinappPopup()
const { s3Sync } = useAppSelector((state) => state.backup) const { s3Sync } = useAppSelector((state) => state.backup)
const onSyncIntervalChange = (value: number) => { const onSyncIntervalChange = async (value: number) => {
setSyncInterval(value) setS3SyncInterval(value)
dispatch(setS3Partial({ syncInterval: value, autoSync: value !== 0 }))
if (value === 0) { if (value === 0) {
await setS3AutoSync(false)
stopAutoSync('s3') stopAutoSync('s3')
} else { } else {
await setS3AutoSync(true)
startAutoSync(false, 's3') startAutoSync(false, 's3')
} }
} }
@ -70,17 +55,15 @@ const S3Settings: FC = () => {
} }
const onMaxBackupsChange = (value: number) => { const onMaxBackupsChange = (value: number) => {
setMaxBackups(value) setS3MaxBackups(value)
dispatch(setS3Partial({ maxBackups: value }))
} }
const onSkipBackupFilesChange = (value: boolean) => { const onSkipBackupFilesChange = (value: boolean) => {
setSkipBackupFile(value) setS3SkipBackupFile(value)
dispatch(setS3Partial({ skipBackupFile: value }))
} }
const renderSyncStatus = () => { const renderSyncStatus = () => {
if (!endpoint) return null if (!s3Endpoint) return null
if (!s3Sync?.lastSyncTime && !s3Sync?.syncing && !s3Sync?.lastSyncError) { if (!s3Sync?.lastSyncTime && !s3Sync?.syncing && !s3Sync?.lastSyncError) {
return <span style={{ color: 'var(--text-secondary)' }}>{t('settings.data.s3.syncStatus.noSync')}</span> return <span style={{ color: 'var(--text-secondary)' }}>{t('settings.data.s3.syncStatus.noSync')}</span>
@ -128,11 +111,11 @@ const S3Settings: FC = () => {
<SettingRowTitle>{t('settings.data.s3.endpoint.label')}</SettingRowTitle> <SettingRowTitle>{t('settings.data.s3.endpoint.label')}</SettingRowTitle>
<Input <Input
placeholder={t('settings.data.s3.endpoint.placeholder')} placeholder={t('settings.data.s3.endpoint.placeholder')}
value={endpoint} value={s3Endpoint}
onChange={(e) => setEndpoint(e.target.value)} onChange={(e) => setS3Endpoint(e.target.value)}
style={{ width: 250 }} style={{ width: 250 }}
type="url" type="url"
onBlur={() => dispatch(setS3Partial({ endpoint: endpoint || '' }))} onBlur={(e) => setS3Endpoint(e.target.value)}
/> />
</SettingRow> </SettingRow>
<SettingDivider /> <SettingDivider />
@ -140,10 +123,10 @@ const S3Settings: FC = () => {
<SettingRowTitle>{t('settings.data.s3.region.label')}</SettingRowTitle> <SettingRowTitle>{t('settings.data.s3.region.label')}</SettingRowTitle>
<Input <Input
placeholder={t('settings.data.s3.region.placeholder')} placeholder={t('settings.data.s3.region.placeholder')}
value={region} value={s3Region}
onChange={(e) => setRegion(e.target.value)} onChange={(e) => setS3Region(e.target.value)}
style={{ width: 250 }} style={{ width: 250 }}
onBlur={() => dispatch(setS3Partial({ region: region || '' }))} onBlur={(e) => setS3Region(e.target.value)}
/> />
</SettingRow> </SettingRow>
<SettingDivider /> <SettingDivider />
@ -151,10 +134,10 @@ const S3Settings: FC = () => {
<SettingRowTitle>{t('settings.data.s3.bucket.label')}</SettingRowTitle> <SettingRowTitle>{t('settings.data.s3.bucket.label')}</SettingRowTitle>
<Input <Input
placeholder={t('settings.data.s3.bucket.placeholder')} placeholder={t('settings.data.s3.bucket.placeholder')}
value={bucket} value={s3Bucket}
onChange={(e) => setBucket(e.target.value)} onChange={(e) => setS3Bucket(e.target.value)}
style={{ width: 250 }} style={{ width: 250 }}
onBlur={() => dispatch(setS3Partial({ bucket: bucket || '' }))} onBlur={(e) => setS3Bucket(e.target.value)}
/> />
</SettingRow> </SettingRow>
<SettingDivider /> <SettingDivider />
@ -162,10 +145,10 @@ const S3Settings: FC = () => {
<SettingRowTitle>{t('settings.data.s3.accessKeyId.label')}</SettingRowTitle> <SettingRowTitle>{t('settings.data.s3.accessKeyId.label')}</SettingRowTitle>
<Input <Input
placeholder={t('settings.data.s3.accessKeyId.placeholder')} placeholder={t('settings.data.s3.accessKeyId.placeholder')}
value={accessKeyId} value={s3AccessKeyId}
onChange={(e) => setAccessKeyId(e.target.value)} onChange={(e) => setS3AccessKeyId(e.target.value)}
style={{ width: 250 }} style={{ width: 250 }}
onBlur={() => dispatch(setS3Partial({ accessKeyId: accessKeyId || '' }))} onBlur={(e) => setS3AccessKeyId(e.target.value)}
/> />
</SettingRow> </SettingRow>
<SettingDivider /> <SettingDivider />
@ -173,10 +156,10 @@ const S3Settings: FC = () => {
<SettingRowTitle>{t('settings.data.s3.secretAccessKey.label')}</SettingRowTitle> <SettingRowTitle>{t('settings.data.s3.secretAccessKey.label')}</SettingRowTitle>
<Input.Password <Input.Password
placeholder={t('settings.data.s3.secretAccessKey.placeholder')} placeholder={t('settings.data.s3.secretAccessKey.placeholder')}
value={secretAccessKey} value={s3SecretAccessKey}
onChange={(e) => setSecretAccessKey(e.target.value)} onChange={(e) => setS3SecretAccessKey(e.target.value)}
style={{ width: 250 }} style={{ width: 250 }}
onBlur={() => dispatch(setS3Partial({ secretAccessKey: secretAccessKey || '' }))} onBlur={(e) => setS3SecretAccessKey(e.target.value)}
/> />
</SettingRow> </SettingRow>
<SettingDivider /> <SettingDivider />
@ -184,10 +167,10 @@ const S3Settings: FC = () => {
<SettingRowTitle>{t('settings.data.s3.root.label')}</SettingRowTitle> <SettingRowTitle>{t('settings.data.s3.root.label')}</SettingRowTitle>
<Input <Input
placeholder={t('settings.data.s3.root.placeholder')} placeholder={t('settings.data.s3.root.placeholder')}
value={root} value={s3Root}
onChange={(e) => setRoot(e.target.value)} onChange={(e) => setS3Root(e.target.value)}
style={{ width: 250 }} style={{ width: 250 }}
onBlur={() => dispatch(setS3Partial({ root: root || '' }))} onBlur={(e) => setS3Root(e.target.value)}
/> />
</SettingRow> </SettingRow>
<SettingDivider /> <SettingDivider />
@ -198,13 +181,13 @@ const S3Settings: FC = () => {
onClick={showBackupModal} onClick={showBackupModal}
icon={<SaveOutlined />} icon={<SaveOutlined />}
loading={backuping} loading={backuping}
disabled={!endpoint || !region || !bucket || !accessKeyId || !secretAccessKey}> disabled={!s3Endpoint || !s3Region || !s3Bucket || !s3AccessKeyId || !s3SecretAccessKey}>
{t('settings.data.s3.backup.button')} {t('settings.data.s3.backup.button')}
</Button> </Button>
<Button <Button
onClick={showBackupManager} onClick={showBackupManager}
icon={<FolderOpenOutlined />} icon={<FolderOpenOutlined />}
disabled={!endpoint || !region || !bucket || !accessKeyId || !secretAccessKey}> disabled={!s3Endpoint || !s3Region || !s3Bucket || !s3AccessKeyId || !s3SecretAccessKey}>
{t('settings.data.s3.backup.manager.button')} {t('settings.data.s3.backup.manager.button')}
</Button> </Button>
</HStack> </HStack>
@ -214,9 +197,9 @@ const S3Settings: FC = () => {
<SettingRowTitle>{t('settings.data.s3.autoSync.label')}</SettingRowTitle> <SettingRowTitle>{t('settings.data.s3.autoSync.label')}</SettingRowTitle>
<Selector <Selector
size={14} size={14}
value={syncInterval} value={s3SyncInterval}
onChange={onSyncIntervalChange} onChange={onSyncIntervalChange}
disabled={!endpoint || !accessKeyId || !secretAccessKey} disabled={!s3Endpoint || !s3AccessKeyId || !s3SecretAccessKey}
options={[ options={[
{ label: t('settings.data.s3.autoSync.off'), value: 0 }, { label: t('settings.data.s3.autoSync.off'), value: 0 },
{ label: t('settings.data.s3.autoSync.minute', { count: 1 }), value: 1 }, { label: t('settings.data.s3.autoSync.minute', { count: 1 }), value: 1 },
@ -236,9 +219,9 @@ const S3Settings: FC = () => {
<SettingRowTitle>{t('settings.data.s3.maxBackups.label')}</SettingRowTitle> <SettingRowTitle>{t('settings.data.s3.maxBackups.label')}</SettingRowTitle>
<Selector <Selector
size={14} size={14}
value={maxBackups} value={s3MaxBackups}
onChange={onMaxBackupsChange} onChange={onMaxBackupsChange}
disabled={!endpoint || !accessKeyId || !secretAccessKey} disabled={!s3Endpoint || !s3AccessKeyId || !s3SecretAccessKey}
options={[ options={[
{ label: t('settings.data.s3.maxBackups.unlimited'), value: 0 }, { label: t('settings.data.s3.maxBackups.unlimited'), value: 0 },
{ label: '1', value: 1 }, { label: '1', value: 1 },
@ -253,12 +236,12 @@ const S3Settings: FC = () => {
<SettingDivider /> <SettingDivider />
<SettingRow> <SettingRow>
<SettingRowTitle>{t('settings.data.s3.skipBackupFile.label')}</SettingRowTitle> <SettingRowTitle>{t('settings.data.s3.skipBackupFile.label')}</SettingRowTitle>
<Switch checked={skipBackupFile} onChange={onSkipBackupFilesChange} /> <Switch checked={s3SkipBackupFile} onChange={onSkipBackupFilesChange} />
</SettingRow> </SettingRow>
<SettingRow> <SettingRow>
<SettingHelpText>{t('settings.data.s3.skipBackupFile.help')}</SettingHelpText> <SettingHelpText>{t('settings.data.s3.skipBackupFile.help')}</SettingHelpText>
</SettingRow> </SettingRow>
{syncInterval > 0 && ( {s3SyncInterval > 0 && (
<> <>
<SettingDivider /> <SettingDivider />
<SettingRow> <SettingRow>
@ -281,12 +264,12 @@ const S3Settings: FC = () => {
visible={backupManagerVisible} visible={backupManagerVisible}
onClose={closeBackupManager} onClose={closeBackupManager}
s3Config={{ s3Config={{
endpoint, endpoint: s3Endpoint,
region, region: s3Region,
bucket, bucket: s3Bucket,
accessKeyId, accessKeyId: s3AccessKeyId,
secretAccessKey, secretAccessKey: s3SecretAccessKey,
root root: s3Root
}} }}
/> />
</> </>

View File

@ -1,45 +1,42 @@
import { InfoCircleOutlined } from '@ant-design/icons' import { InfoCircleOutlined } from '@ant-design/icons'
import { usePreference } from '@data/hooks/usePreference'
import { loggerService } from '@logger' import { loggerService } from '@logger'
import { HStack } from '@renderer/components/Layout' import { HStack } from '@renderer/components/Layout'
import { useTheme } from '@renderer/context/ThemeProvider' import { useTheme } from '@renderer/context/ThemeProvider'
import { useMinappPopup } from '@renderer/hooks/useMinappPopup' import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
import { RootState, useAppDispatch } from '@renderer/store'
import { setSiyuanApiUrl, setSiyuanBoxId, setSiyuanRootPath, setSiyuanToken } from '@renderer/store/settings'
import { Button, Space, Tooltip } from 'antd' import { Button, Space, Tooltip } from 'antd'
import { Input } from 'antd' import { Input } from 'antd'
import { FC } from 'react' import { FC } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import { SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..' import { SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..'
const logger = loggerService.withContext('SiyuanSettings') const logger = loggerService.withContext('SiyuanSettings')
const SiyuanSettings: FC = () => { const SiyuanSettings: FC = () => {
const [siyuanApiUrl, setSiyuanApiUrl] = usePreference('data.integration.siyuan.api_url')
const [siyuanToken, setSiyuanToken] = usePreference('data.integration.siyuan.token')
const [siyuanBoxId, setSiyuanBoxId] = usePreference('data.integration.siyuan.box_id')
const [siyuanRootPath, setSiyuanRootPath] = usePreference('data.integration.siyuan.root_path')
const { openMinapp } = useMinappPopup() const { openMinapp } = useMinappPopup()
const { t } = useTranslation() const { t } = useTranslation()
const { theme } = useTheme() const { theme } = useTheme()
const dispatch = useAppDispatch()
const siyuanApiUrl = useSelector((state: RootState) => state.settings.siyuanApiUrl)
const siyuanToken = useSelector((state: RootState) => state.settings.siyuanToken)
const siyuanBoxId = useSelector((state: RootState) => state.settings.siyuanBoxId)
const siyuanRootPath = useSelector((state: RootState) => state.settings.siyuanRootPath)
const handleApiUrlChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleApiUrlChange = (e: React.ChangeEvent<HTMLInputElement>) => {
dispatch(setSiyuanApiUrl(e.target.value)) setSiyuanApiUrl(e.target.value)
} }
const handleTokenChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleTokenChange = (e: React.ChangeEvent<HTMLInputElement>) => {
dispatch(setSiyuanToken(e.target.value)) setSiyuanToken(e.target.value)
} }
const handleBoxIdChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleBoxIdChange = (e: React.ChangeEvent<HTMLInputElement>) => {
dispatch(setSiyuanBoxId(e.target.value)) setSiyuanBoxId(e.target.value)
} }
const handleRootPathChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleRootPathChange = (e: React.ChangeEvent<HTMLInputElement>) => {
dispatch(setSiyuanRootPath(e.target.value)) setSiyuanRootPath(e.target.value)
} }
const handleSiyuanHelpClick = () => { const handleSiyuanHelpClick = () => {

View File

@ -1,23 +1,12 @@
import { FolderOpenOutlined, SaveOutlined, SyncOutlined, WarningOutlined } from '@ant-design/icons' import { FolderOpenOutlined, SaveOutlined, SyncOutlined, WarningOutlined } from '@ant-design/icons'
import { usePreference } from '@data/hooks/usePreference'
import { HStack } from '@renderer/components/Layout' import { HStack } from '@renderer/components/Layout'
import Selector from '@renderer/components/Selector' import Selector from '@renderer/components/Selector'
import { WebdavBackupManager } from '@renderer/components/WebdavBackupManager' import { WebdavBackupManager } from '@renderer/components/WebdavBackupManager'
import { useWebdavBackupModal, WebdavBackupModal } from '@renderer/components/WebdavModals' import { useWebdavBackupModal, WebdavBackupModal } from '@renderer/components/WebdavModals'
import { useTheme } from '@renderer/context/ThemeProvider' import { useTheme } from '@renderer/context/ThemeProvider'
import { useSettings } from '@renderer/hooks/useSettings'
import { startAutoSync, stopAutoSync } from '@renderer/services/BackupService' import { startAutoSync, stopAutoSync } from '@renderer/services/BackupService'
import { useAppDispatch, useAppSelector } from '@renderer/store' import { useAppSelector } from '@renderer/store'
import {
setWebdavAutoSync,
setWebdavDisableStream as _setWebdavDisableStream,
setWebdavHost as _setWebdavHost,
setWebdavMaxBackups as _setWebdavMaxBackups,
setWebdavPass as _setWebdavPass,
setWebdavPath as _setWebdavPath,
setWebdavSkipBackupFile as _setWebdavSkipBackupFile,
setWebdavSyncInterval as _setWebdavSyncInterval,
setWebdavUser as _setWebdavUser
} from '@renderer/store/settings'
import { Button, Input, Switch, Tooltip } from 'antd' import { Button, Input, Switch, Tooltip } from 'antd'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { FC, useState } from 'react' import { FC, useState } from 'react'
@ -26,29 +15,18 @@ import { useTranslation } from 'react-i18next'
import { SettingDivider, SettingGroup, SettingHelpText, SettingRow, SettingRowTitle, SettingTitle } from '..' import { SettingDivider, SettingGroup, SettingHelpText, SettingRow, SettingRowTitle, SettingTitle } from '..'
const WebDavSettings: FC = () => { const WebDavSettings: FC = () => {
const { const [, setWebdavAutoSync] = usePreference('data.backup.webdav.auto_sync')
webdavHost: webDAVHost, const [webdavDisableStream, setWebdavDisableStream] = usePreference('data.backup.webdav.disable_stream')
webdavUser: webDAVUser, const [webdavHost, setWebdavHost] = usePreference('data.backup.webdav.host')
webdavPass: webDAVPass, const [webdavMaxBackups, setWebdavMaxBackups] = usePreference('data.backup.webdav.max_backups')
webdavPath: webDAVPath, const [webdavPass, setWebdavPass] = usePreference('data.backup.webdav.pass')
webdavSyncInterval: webDAVSyncInterval, const [webdavPath, setWebdavPath] = usePreference('data.backup.webdav.path')
webdavMaxBackups: webDAVMaxBackups, const [webdavSkipBackupFile, setWebdavSkipBackupFile] = usePreference('data.backup.webdav.skip_backup_file')
webdavSkipBackupFile: webdDAVSkipBackupFile, const [webdavSyncInterval, setWebdavSyncInterval] = usePreference('data.backup.webdav.sync_interval')
webdavDisableStream: webDAVDisableStream const [webdavUser, setWebdavUser] = usePreference('data.backup.webdav.user')
} = useSettings()
const [webdavHost, setWebdavHost] = useState<string | undefined>(webDAVHost)
const [webdavUser, setWebdavUser] = useState<string | undefined>(webDAVUser)
const [webdavPass, setWebdavPass] = useState<string | undefined>(webDAVPass)
const [webdavPath, setWebdavPath] = useState<string | undefined>(webDAVPath)
const [webdavSkipBackupFile, setWebdavSkipBackupFile] = useState<boolean>(webdDAVSkipBackupFile)
const [webdavDisableStream, setWebdavDisableStream] = useState<boolean>(webDAVDisableStream)
const [backupManagerVisible, setBackupManagerVisible] = useState(false) const [backupManagerVisible, setBackupManagerVisible] = useState(false)
const [syncInterval, setSyncInterval] = useState<number>(webDAVSyncInterval)
const [maxBackups, setMaxBackups] = useState<number>(webDAVMaxBackups)
const dispatch = useAppDispatch()
const { theme } = useTheme() const { theme } = useTheme()
const { t } = useTranslation() const { t } = useTranslation()
@ -57,31 +35,27 @@ const WebDavSettings: FC = () => {
// 把之前备份的文件定时上传到 webdav首先先配置 webdav 的 host, port, user, pass, path // 把之前备份的文件定时上传到 webdav首先先配置 webdav 的 host, port, user, pass, path
const onSyncIntervalChange = (value: number) => { const onSyncIntervalChange = async (value: number) => {
setSyncInterval(value) setWebdavSyncInterval(value)
dispatch(_setWebdavSyncInterval(value))
if (value === 0) { if (value === 0) {
dispatch(setWebdavAutoSync(false)) await setWebdavAutoSync(false)
stopAutoSync('webdav') stopAutoSync('webdav')
} else { } else {
dispatch(setWebdavAutoSync(true)) await setWebdavAutoSync(true)
startAutoSync(false, 'webdav') startAutoSync(false, 'webdav')
} }
} }
const onMaxBackupsChange = (value: number) => { const onMaxBackupsChange = (value: number) => {
setMaxBackups(value) setWebdavMaxBackups(value)
dispatch(_setWebdavMaxBackups(value))
} }
const onSkipBackupFilesChange = (value: boolean) => { const onSkipBackupFilesChange = (value: boolean) => {
setWebdavSkipBackupFile(value) setWebdavSkipBackupFile(value)
dispatch(_setWebdavSkipBackupFile(value))
} }
const onDisableStreamChange = (value: boolean) => { const onDisableStreamChange = (value: boolean) => {
setWebdavDisableStream(value) setWebdavDisableStream(value)
dispatch(_setWebdavDisableStream(value))
} }
const renderSyncStatus = () => { const renderSyncStatus = () => {
@ -131,7 +105,7 @@ const WebDavSettings: FC = () => {
onChange={(e) => setWebdavHost(e.target.value)} onChange={(e) => setWebdavHost(e.target.value)}
style={{ width: 250 }} style={{ width: 250 }}
type="url" type="url"
onBlur={() => dispatch(_setWebdavHost(webdavHost || ''))} onBlur={() => setWebdavHost(webdavHost || '')}
/> />
</SettingRow> </SettingRow>
<SettingDivider /> <SettingDivider />
@ -142,7 +116,7 @@ const WebDavSettings: FC = () => {
value={webdavUser} value={webdavUser}
onChange={(e) => setWebdavUser(e.target.value)} onChange={(e) => setWebdavUser(e.target.value)}
style={{ width: 250 }} style={{ width: 250 }}
onBlur={() => dispatch(_setWebdavUser(webdavUser || ''))} onBlur={() => setWebdavUser(webdavUser || '')}
/> />
</SettingRow> </SettingRow>
<SettingDivider /> <SettingDivider />
@ -153,7 +127,7 @@ const WebDavSettings: FC = () => {
value={webdavPass} value={webdavPass}
onChange={(e) => setWebdavPass(e.target.value)} onChange={(e) => setWebdavPass(e.target.value)}
style={{ width: 250 }} style={{ width: 250 }}
onBlur={() => dispatch(_setWebdavPass(webdavPass || ''))} onBlur={() => setWebdavPass(webdavPass || '')}
/> />
</SettingRow> </SettingRow>
<SettingDivider /> <SettingDivider />
@ -164,7 +138,7 @@ const WebDavSettings: FC = () => {
value={webdavPath} value={webdavPath}
onChange={(e) => setWebdavPath(e.target.value)} onChange={(e) => setWebdavPath(e.target.value)}
style={{ width: 250 }} style={{ width: 250 }}
onBlur={() => dispatch(_setWebdavPath(webdavPath || ''))} onBlur={() => setWebdavPath(webdavPath || '')}
/> />
</SettingRow> </SettingRow>
<SettingDivider /> <SettingDivider />
@ -184,7 +158,7 @@ const WebDavSettings: FC = () => {
<SettingRowTitle>{t('settings.data.webdav.autoSync.label')}</SettingRowTitle> <SettingRowTitle>{t('settings.data.webdav.autoSync.label')}</SettingRowTitle>
<Selector <Selector
size={14} size={14}
value={syncInterval} value={webdavSyncInterval}
onChange={onSyncIntervalChange} onChange={onSyncIntervalChange}
disabled={!webdavHost} disabled={!webdavHost}
options={[ options={[
@ -206,7 +180,7 @@ const WebDavSettings: FC = () => {
<SettingRowTitle>{t('settings.data.webdav.maxBackups')}</SettingRowTitle> <SettingRowTitle>{t('settings.data.webdav.maxBackups')}</SettingRowTitle>
<Selector <Selector
size={14} size={14}
value={maxBackups} value={webdavMaxBackups}
onChange={onMaxBackupsChange} onChange={onMaxBackupsChange}
disabled={!webdavHost} disabled={!webdavHost}
options={[ options={[
@ -236,7 +210,7 @@ const WebDavSettings: FC = () => {
<SettingRow> <SettingRow>
<SettingHelpText>{t('settings.data.webdav.disableStream.help')}</SettingHelpText> <SettingHelpText>{t('settings.data.webdav.disableStream.help')}</SettingHelpText>
</SettingRow> </SettingRow>
{webdavSync && syncInterval > 0 && ( {webdavSync && webdavSyncInterval > 0 && (
<> <>
<SettingDivider /> <SettingDivider />
<SettingRow> <SettingRow>

View File

@ -1,32 +1,30 @@
import { InfoCircleOutlined } from '@ant-design/icons' import { InfoCircleOutlined } from '@ant-design/icons'
import { usePreference } from '@data/hooks/usePreference'
import { HStack } from '@renderer/components/Layout' import { HStack } from '@renderer/components/Layout'
import { useTheme } from '@renderer/context/ThemeProvider' import { useTheme } from '@renderer/context/ThemeProvider'
import { useMinappPopup } from '@renderer/hooks/useMinappPopup' import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
import { RootState, useAppDispatch } from '@renderer/store'
import { setYuqueRepoId, setYuqueToken, setYuqueUrl } from '@renderer/store/settings'
import { Button, Space, Tooltip } from 'antd' import { Button, Space, Tooltip } from 'antd'
import { Input } from 'antd' import { Input } from 'antd'
import { FC } from 'react' import { FC } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import { SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..' import { SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..'
const YuqueSettings: FC = () => { const YuqueSettings: FC = () => {
const { t } = useTranslation() const { t } = useTranslation()
const { theme } = useTheme() const { theme } = useTheme()
const dispatch = useAppDispatch()
const { openMinapp } = useMinappPopup() const { openMinapp } = useMinappPopup()
const yuqueToken = useSelector((state: RootState) => state.settings.yuqueToken) const [yuqueToken, setYuqueToken] = usePreference('data.integration.yuque.token')
const yuqueUrl = useSelector((state: RootState) => state.settings.yuqueUrl) const [yuqueUrl, setYuqueUrl] = usePreference('data.integration.yuque.url')
const [, setYuqueRepoId] = usePreference('data.integration.yuque.repo_id')
const handleYuqueTokenChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleYuqueTokenChange = (e: React.ChangeEvent<HTMLInputElement>) => {
dispatch(setYuqueToken(e.target.value)) setYuqueToken(e.target.value)
} }
const handleYuqueRepoUrlChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleYuqueRepoUrlChange = (e: React.ChangeEvent<HTMLInputElement>) => {
dispatch(setYuqueUrl(e.target.value)) setYuqueUrl(e.target.value)
} }
const handleYuqueConnectionCheck = async () => { const handleYuqueConnectionCheck = async () => {
@ -60,7 +58,7 @@ const YuqueSettings: FC = () => {
return return
} }
const data = await repoIDResponse.json() const data = await repoIDResponse.json()
dispatch(setYuqueRepoId(data.data.id)) setYuqueRepoId(data.data.id)
window.message.success(t('settings.data.yuque.check.success')) window.message.success(t('settings.data.yuque.check.success'))
} }

View File

@ -1,22 +1,11 @@
import { InfoCircleOutlined } from '@ant-design/icons' import { InfoCircleOutlined } from '@ant-design/icons'
import { usePreference } from '@data/hooks/usePreference' import { useMultiplePreferences, usePreference } from '@data/hooks/usePreference'
import InfoTooltip from '@renderer/components/InfoTooltip' import InfoTooltip from '@renderer/components/InfoTooltip'
import { HStack } from '@renderer/components/Layout' import { HStack } from '@renderer/components/Layout'
import Selector from '@renderer/components/Selector' import Selector from '@renderer/components/Selector'
import { useTheme } from '@renderer/context/ThemeProvider' import { useTheme } from '@renderer/context/ThemeProvider'
import { useSettings } from '@renderer/hooks/useSettings'
import { useTimer } from '@renderer/hooks/useTimer' import { useTimer } from '@renderer/hooks/useTimer'
import i18n from '@renderer/i18n' import i18n from '@renderer/i18n'
import { RootState, useAppDispatch } from '@renderer/store'
import {
setEnableDataCollection,
setEnableSpellCheck,
setNotificationSettings,
setProxyBypassRules as _setProxyBypassRules,
setProxyMode,
setProxyUrl as _setProxyUrl,
setSpellCheckLanguages
} from '@renderer/store/settings'
import { NotificationSource } from '@renderer/types/notification' import { NotificationSource } from '@renderer/types/notification'
import { isValidProxyUrl } from '@renderer/utils' import { isValidProxyUrl } from '@renderer/utils'
import { defaultByPassRules, defaultLanguage } from '@shared/config/constant' import { defaultByPassRules, defaultLanguage } from '@shared/config/constant'
@ -24,32 +13,33 @@ import { LanguageVarious } from '@shared/data/preferenceTypes'
import { Flex, Input, Switch, Tooltip } from 'antd' import { Flex, Input, Switch, Tooltip } from 'antd'
import { FC, useState } from 'react' import { FC, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import { SettingContainer, SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '.' import { SettingContainer, SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '.'
const GeneralSettings: FC = () => { const GeneralSettings: FC = () => {
const {
proxyUrl: storeProxyUrl,
proxyBypassRules: storeProxyBypassRules,
setLaunch,
setTray,
launchOnBoot,
launchToTray,
trayOnClose,
tray,
proxyMode: storeProxyMode,
enableDataCollection,
enableSpellCheck
} = useSettings()
const [language, setLanguage] = usePreference('app.language') const [language, setLanguage] = usePreference('app.language')
const [disableHardwareAcceleration, setDisableHardwareAcceleration] = usePreference( const [disableHardwareAcceleration, setDisableHardwareAcceleration] = usePreference(
'app.disable_hardware_acceleration' 'app.disable_hardware_acceleration'
) )
const [enableDeveloperMode, setEnableDeveloperMode] = usePreference('app.developer_mode.enabled') const [enableDeveloperMode, setEnableDeveloperMode] = usePreference('app.developer_mode.enabled')
const [launchOnBoot, setLaunchOnBoot] = usePreference('app.launch_on_boot')
const [launchToTray, setLaunchToTray] = usePreference('app.tray.on_launch')
const [trayOnClose, setTrayOnClose] = usePreference('app.tray.on_close')
const [tray, setTray] = usePreference('app.tray.enabled')
const [enableDataCollection, setEnableDataCollection] = usePreference('app.privacy.data_collection.enabled')
const [storeProxyMode, setProxyMode] = usePreference('app.proxy.mode')
const [storeProxyBypassRules, _setProxyBypassRules] = usePreference('app.proxy.bypass_rules')
const [storeProxyUrl, _setProxyUrl] = usePreference('app.proxy.url')
const [enableSpellCheck, setEnableSpellCheck] = usePreference('app.spell_check.enabled')
const [spellCheckLanguages, setSpellCheckLanguages] = usePreference('app.spell_check.languages')
const [notificationSettings, setNotificationSettings] = useMultiplePreferences({
assistant: 'app.notification.assistant.enabled',
backup: 'app.notification.backup.enabled',
knowledge: 'app.notification.knowledge.enabled'
})
const [proxyUrl, setProxyUrl] = useState<string | undefined>(storeProxyUrl) const [proxyUrl, setProxyUrl] = useState<string>(storeProxyUrl)
const [proxyBypassRules, setProxyBypassRules] = useState<string | undefined>(storeProxyBypassRules) const [proxyBypassRules, setProxyBypassRules] = useState<string>(storeProxyBypassRules)
const { theme } = useTheme() const { theme } = useTheme()
const { setTimeoutTimer } = useTimer() const { setTimeoutTimer } = useTimer()
@ -63,7 +53,7 @@ const GeneralSettings: FC = () => {
} }
const updateTrayOnClose = (isTrayOnClose: boolean) => { const updateTrayOnClose = (isTrayOnClose: boolean) => {
setTray(undefined, isTrayOnClose) setTrayOnClose(isTrayOnClose)
//in case tray is not enabled, enable it //in case tray is not enabled, enable it
if (isTrayOnClose && !tray) { if (isTrayOnClose && !tray) {
updateTray(true) updateTray(true)
@ -71,17 +61,17 @@ const GeneralSettings: FC = () => {
} }
const updateLaunchOnBoot = (isLaunchOnBoot: boolean) => { const updateLaunchOnBoot = (isLaunchOnBoot: boolean) => {
setLaunch(isLaunchOnBoot) setLaunchOnBoot(isLaunchOnBoot)
} }
const updateLaunchToTray = (isLaunchToTray: boolean) => { const updateLaunchToTray = (isLaunchToTray: boolean) => {
setLaunch(undefined, isLaunchToTray) setLaunchToTray(isLaunchToTray)
if (isLaunchToTray && !tray) { if (isLaunchToTray && !tray) {
updateTray(true) updateTray(true)
} }
} }
const dispatch = useAppDispatch() // const dispatch = useAppDispatch()
const { t } = useTranslation() const { t } = useTranslation()
const onSelectLanguage = (value: LanguageVarious) => { const onSelectLanguage = (value: LanguageVarious) => {
@ -93,7 +83,7 @@ const GeneralSettings: FC = () => {
} }
const handleSpellCheckChange = (checked: boolean) => { const handleSpellCheckChange = (checked: boolean) => {
dispatch(setEnableSpellCheck(checked)) setEnableSpellCheck(checked)
window.api.setEnableSpellCheck(checked) window.api.setEnableSpellCheck(checked)
} }
@ -103,11 +93,11 @@ const GeneralSettings: FC = () => {
return return
} }
dispatch(_setProxyUrl(proxyUrl)) _setProxyUrl(proxyUrl)
} }
const onSetProxyBypassRules = () => { const onSetProxyBypassRules = () => {
dispatch(_setProxyBypassRules(proxyBypassRules)) _setProxyBypassRules(proxyBypassRules)
} }
const proxyModeOptions: { value: 'system' | 'custom' | 'none'; label: string }[] = [ const proxyModeOptions: { value: 'system' | 'custom' | 'none'; label: string }[] = [
@ -117,7 +107,7 @@ const GeneralSettings: FC = () => {
] ]
const onProxyModeChange = (mode: 'system' | 'custom' | 'none') => { const onProxyModeChange = (mode: 'system' | 'custom' | 'none') => {
dispatch(setProxyMode(mode)) setProxyMode(mode)
} }
const languagesOptions: { value: LanguageVarious; label: string; flag: string }[] = [ const languagesOptions: { value: LanguageVarious; label: string; flag: string }[] = [
@ -132,11 +122,8 @@ const GeneralSettings: FC = () => {
{ value: 'pt-PT', label: 'Português', flag: '🇵🇹' } { value: 'pt-PT', label: 'Português', flag: '🇵🇹' }
] ]
const notificationSettings = useSelector((state: RootState) => state.settings.notification)
const spellCheckLanguages = useSelector((state: RootState) => state.settings.spellCheckLanguages)
const handleNotificationChange = (type: NotificationSource, value: boolean) => { const handleNotificationChange = (type: NotificationSource, value: boolean) => {
dispatch(setNotificationSettings({ ...notificationSettings, [type]: value })) setNotificationSettings({ [type]: value })
} }
// Define available spell check languages with display names (only commonly supported languages) // Define available spell check languages with display names (only commonly supported languages)
@ -153,8 +140,7 @@ const GeneralSettings: FC = () => {
] ]
const handleSpellCheckLanguagesChange = (selectedLanguages: string[]) => { const handleSpellCheckLanguagesChange = (selectedLanguages: string[]) => {
dispatch(setSpellCheckLanguages(selectedLanguages)) setSpellCheckLanguages(selectedLanguages)
window.api.setSpellCheckLanguages(selectedLanguages)
} }
const handleHardwareAccelerationChange = (checked: boolean) => { const handleHardwareAccelerationChange = (checked: boolean) => {
@ -339,7 +325,7 @@ const GeneralSettings: FC = () => {
<Switch <Switch
value={enableDataCollection} value={enableDataCollection}
onChange={(v) => { onChange={(v) => {
dispatch(setEnableDataCollection(v)) setEnableDataCollection(v)
window.api.config.set('enableDataCollection', v) window.api.config.set('enableDataCollection', v)
}} }}
/> />

View File

@ -1,9 +1,7 @@
import { QuestionCircleOutlined } from '@ant-design/icons' import { QuestionCircleOutlined } from '@ant-design/icons'
import { usePreference } from '@data/hooks/usePreference'
import { ResetIcon } from '@renderer/components/Icons' import { ResetIcon } from '@renderer/components/Icons'
import { HStack } from '@renderer/components/Layout' import { HStack } from '@renderer/components/Layout'
import { useSettings } from '@renderer/hooks/useSettings'
import { useAppDispatch } from '@renderer/store'
import { setEnableTopicNaming, setTopicNamingPrompt } from '@renderer/store/settings'
import { Button, Divider, Flex, Input, Modal, Popover, Switch } from 'antd' import { Button, Divider, Flex, Input, Modal, Popover, Switch } from 'antd'
import { useCallback, useMemo, useState } from 'react' import { useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@ -16,10 +14,11 @@ interface Props {
} }
const PopupContainer: React.FC<Props> = ({ resolve }) => { const PopupContainer: React.FC<Props> = ({ resolve }) => {
const [enableTopicNaming, setEnableTopicNaming] = usePreference('topic.naming.enabled')
const [topicNamingPrompt, setTopicNamingPrompt] = usePreference('topic.naming_prompt')
const [open, setOpen] = useState(true) const [open, setOpen] = useState(true)
const { t } = useTranslation() const { t } = useTranslation()
const { enableTopicNaming, topicNamingPrompt } = useSettings()
const dispatch = useAppDispatch()
const onOk = () => { const onOk = () => {
setOpen(false) setOpen(false)
@ -34,8 +33,8 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
} }
const handleReset = useCallback(() => { const handleReset = useCallback(() => {
dispatch(setTopicNamingPrompt('')) setTopicNamingPrompt('')
}, [dispatch]) }, [setTopicNamingPrompt])
TopicNamingModalPopup.hide = onCancel TopicNamingModalPopup.hide = onCancel
@ -58,7 +57,7 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
<Flex vertical align="stretch" gap={8}> <Flex vertical align="stretch" gap={8}>
<HStack style={{ gap: 16 }} alignItems="center"> <HStack style={{ gap: 16 }} alignItems="center">
<div>{t('settings.models.topic_naming.auto')}</div> <div>{t('settings.models.topic_naming.auto')}</div>
<Switch checked={enableTopicNaming} onChange={(v) => dispatch(setEnableTopicNaming(v))} /> <Switch checked={enableTopicNaming} onChange={(v) => setEnableTopicNaming(v)} />
</HStack> </HStack>
<Divider style={{ margin: 0 }} /> <Divider style={{ margin: 0 }} />
<div> <div>
@ -72,7 +71,7 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
<Input.TextArea <Input.TextArea
autoSize={{ minRows: 3, maxRows: 10 }} autoSize={{ minRows: 3, maxRows: 10 }}
value={topicNamingPrompt || t('prompts.title')} value={topicNamingPrompt || t('prompts.title')}
onChange={(e) => dispatch(setTopicNamingPrompt(e.target.value))} onChange={(e) => setTopicNamingPrompt(e.target.value)}
placeholder={t('prompts.title')} placeholder={t('prompts.title')}
style={{ width: '100%' }} style={{ width: '100%' }}
/> />

View File

@ -1,16 +1,11 @@
import { InfoCircleOutlined } from '@ant-design/icons' import { InfoCircleOutlined } from '@ant-design/icons'
import { usePreference } from '@data/hooks/usePreference'
import ModelAvatar from '@renderer/components/Avatar/ModelAvatar' import ModelAvatar from '@renderer/components/Avatar/ModelAvatar'
import { HStack } from '@renderer/components/Layout' import { HStack } from '@renderer/components/Layout'
import { useTheme } from '@renderer/context/ThemeProvider' import { useTheme } from '@renderer/context/ThemeProvider'
import { useAssistants, useDefaultAssistant, useDefaultModel } from '@renderer/hooks/useAssistant' import { useAssistants, useDefaultAssistant, useDefaultModel } from '@renderer/hooks/useAssistant'
import { useSettings } from '@renderer/hooks/useSettings'
import { useAppDispatch, useAppSelector } from '@renderer/store' import { useAppDispatch, useAppSelector } from '@renderer/store'
import { setQuickAssistantId } from '@renderer/store/llm' import { setQuickAssistantId } from '@renderer/store/llm'
import {
setClickTrayToShowQuickAssistant,
setEnableQuickAssistant,
setReadClipboardAtStartup
} from '@renderer/store/settings'
import HomeWindow from '@renderer/windows/mini/home/HomeWindow' import HomeWindow from '@renderer/windows/mini/home/HomeWindow'
import { Button, Select, Switch, Tooltip } from 'antd' import { Button, Select, Switch, Tooltip } from 'antd'
import { FC } from 'react' import { FC } from 'react'
@ -20,9 +15,17 @@ import styled from 'styled-components'
import { SettingContainer, SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '.' import { SettingContainer, SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '.'
const QuickAssistantSettings: FC = () => { const QuickAssistantSettings: FC = () => {
const [enableQuickAssistant, setEnableQuickAssistant] = usePreference('feature.quick_assistant.enabled')
const [clickTrayToShowQuickAssistant, setClickTrayToShowQuickAssistant] = usePreference(
'feature.quick_assistant.click_tray_to_show'
)
const [readClipboardAtStartup, setReadClipboardAtStartup] = usePreference(
'feature.quick_assistant.read_clipboard_at_startup'
)
const [, setTray] = usePreference('app.tray.enabled')
const { t } = useTranslation() const { t } = useTranslation()
const { theme } = useTheme() const { theme } = useTheme()
const { enableQuickAssistant, clickTrayToShowQuickAssistant, setTray, readClipboardAtStartup } = useSettings()
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const { assistants } = useAssistants() const { assistants } = useAssistants()
const { quickAssistantId } = useAppSelector((state) => state.llm) const { quickAssistantId } = useAppSelector((state) => state.llm)
@ -30,8 +33,7 @@ const QuickAssistantSettings: FC = () => {
const { defaultModel } = useDefaultModel() const { defaultModel } = useDefaultModel()
const handleEnableQuickAssistant = async (enable: boolean) => { const handleEnableQuickAssistant = async (enable: boolean) => {
dispatch(setEnableQuickAssistant(enable)) await setEnableQuickAssistant(enable)
await window.api.config.set('enableQuickAssistant', enable, true)
!enable && window.api.miniWindow.close() !enable && window.api.miniWindow.close()
@ -50,14 +52,12 @@ const QuickAssistantSettings: FC = () => {
} }
const handleClickTrayToShowQuickAssistant = async (checked: boolean) => { const handleClickTrayToShowQuickAssistant = async (checked: boolean) => {
dispatch(setClickTrayToShowQuickAssistant(checked)) await setClickTrayToShowQuickAssistant(checked)
await window.api.config.set('clickTrayToShowQuickAssistant', checked)
checked && setTray(true) checked && setTray(true)
} }
const handleClickReadClipboardAtStartup = async (checked: boolean) => { const handleClickReadClipboardAtStartup = async (checked: boolean) => {
dispatch(setReadClipboardAtStartup(checked)) await setReadClipboardAtStartup(checked)
await window.api.config.set('readClipboardAtStartup', checked)
window.api.miniWindow.close() window.api.miniWindow.close()
} }

View File

@ -26,7 +26,7 @@ import ocr from './ocr'
import paintings from './paintings' import paintings from './paintings'
import preprocess from './preprocess' import preprocess from './preprocess'
import runtime from './runtime' import runtime from './runtime'
// import selectionStore from './selectionStore' import selectionStore from './selectionStore'
import settings from './settings' import settings from './settings'
import shortcuts from './shortcuts' import shortcuts from './shortcuts'
import tabs from './tabs' import tabs from './tabs'
@ -52,7 +52,7 @@ const rootReducer = combineReducers({
mcp, mcp,
memory, memory,
copilot, copilot,
// selectionStore, selectionStore,
tabs, tabs,
preprocess, preprocess,
messages: newMessagesReducer, messages: newMessagesReducer,

View File

@ -1,7 +1,7 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit' import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import type { SelectionActionItem, SelectionFilterMode, SelectionTriggerMode } from '@shared/data/preferenceTypes' import type { SelectionActionItem, SelectionFilterMode, SelectionTriggerMode } from '@shared/data/preferenceTypes'
interface SelectionState { export interface SelectionState {
selectionEnabled: boolean selectionEnabled: boolean
triggerMode: SelectionTriggerMode triggerMode: SelectionTriggerMode
isCompact: boolean isCompact: boolean

View File

@ -863,133 +863,133 @@ const settingsSlice = createSlice({
}) })
export const { export const {
setShowModelNameInMarkdown, // setShowModelNameInMarkdown,
setShowModelProviderInMarkdown, // setShowModelProviderInMarkdown,
// setShowAssistants, // setShowAssistants,
// toggleShowAssistants, // toggleShowAssistants,
// setShowTopics, // setShowTopics,
// toggleShowTopics, // toggleShowTopics,
setAssistantsTabSortType, // setAssistantsTabSortType,
setSendMessageShortcut, // setSendMessageShortcut,
setLanguage, // setLanguage,
setTargetLanguage, // setTargetLanguage,
setProxyMode, // setProxyMode,
setProxyUrl, // setProxyUrl,
setProxyBypassRules, // setProxyBypassRules,
setUserName, // setUserName,
setShowPrompt, // setShowPrompt,
setShowMessageDivider, // setShowMessageDivider,
setMessageFont, // setMessageFont,
setShowInputEstimatedTokens, // setShowInputEstimatedTokens,
setLaunchOnBoot, // setLaunchOnBoot,
setLaunchToTray, // setLaunchToTray,
setTrayOnClose, // setTrayOnClose,
setTray, // setTray,
setTheme, // setTheme,
setUserTheme, // setUserTheme,
setFontSize, // setFontSize,
setWindowStyle, // setWindowStyle,
setTopicPosition, // setTopicPosition,
setShowTopicTime, // setShowTopicTime,
setPinTopicsToTop, // setPinTopicsToTop,
setAssistantIconType, // setAssistantIconType,
setPasteLongTextAsFile, // setPasteLongTextAsFile,
setAutoCheckUpdate, // setAutoCheckUpdate,
setTestPlan, // setTestPlan,
setTestChannel, // setTestChannel,
setRenderInputMessageAsMarkdown, // setRenderInputMessageAsMarkdown,
setClickAssistantToShowTopic, // setClickAssistantToShowTopic,
setSkipBackupFile, setSkipBackupFile,
setWebdavHost, // setWebdavHost,
setWebdavUser, // setWebdavUser,
setWebdavPass, // setWebdavPass,
setWebdavPath, // setWebdavPath,
setWebdavAutoSync, // setWebdavAutoSync,
setWebdavSyncInterval, // setWebdavSyncInterval,
setWebdavMaxBackups, // setWebdavMaxBackups,
setWebdavSkipBackupFile, // setWebdavSkipBackupFile,
setWebdavDisableStream, // setWebdavDisableStream,
setCodeExecution, // setCodeExecution,
setCodeEditor, // setCodeEditor,
setCodeViewer, // setCodeViewer,
setCodeShowLineNumbers, // setCodeShowLineNumbers,
setCodeCollapsible, // setCodeCollapsible,
setCodeWrappable, // setCodeWrappable,
setCodeImageTools, // setCodeImageTools,
setMathEngine, // setMathEngine,
setMathEnableSingleDollar, // setMathEnableSingleDollar,
setFoldDisplayMode, // setFoldDisplayMode,
setGridColumns, // setGridColumns,
setGridPopoverTrigger, // setGridPopoverTrigger,
setMessageStyle, // setMessageStyle,
setTranslateModelPrompt, setTranslateModelPrompt,
setAutoTranslateWithSpace, // setAutoTranslateWithSpace,
setShowTranslateConfirm, // setShowTranslateConfirm,
setEnableTopicNaming, // setEnableTopicNaming,
setPasteLongTextThreshold, // setPasteLongTextThreshold,
setCustomCss, // setCustomCss,
setTopicNamingPrompt, // setTopicNamingPrompt,
setSidebarIcons, // setSidebarIcons,
setNarrowMode, // setNarrowMode,
setClickTrayToShowQuickAssistant, // setClickTrayToShowQuickAssistant,
setEnableQuickAssistant, // setEnableQuickAssistant,
setReadClipboardAtStartup, // setReadClipboardAtStartup,
setMultiModelMessageStyle, // setMultiModelMessageStyle,
setNotionDatabaseID, // setNotionDatabaseID,
setNotionApiKey, // setNotionApiKey,
setNotionPageNameKey, // setNotionPageNameKey,
setmarkdownExportPath, // setmarkdownExportPath,
setForceDollarMathInMarkdown, // setForceDollarMathInMarkdown,
setUseTopicNamingForMessageTitle, // setUseTopicNamingForMessageTitle,
setThoughtAutoCollapse, // setThoughtAutoCollapse,
setNotionExportReasoning, // setNotionExportReasoning,
setExcludeCitationsInExport, // setExcludeCitationsInExport,
setStandardizeCitationsInExport, // setStandardizeCitationsInExport,
setYuqueToken, // setYuqueToken,
setYuqueRepoId, // setYuqueRepoId,
setYuqueUrl, // setYuqueUrl,
setJoplinToken, // setJoplinToken,
setJoplinUrl, // setJoplinUrl,
setJoplinExportReasoning, // setJoplinExportReasoning,
setMessageNavigation, setMessageNavigation,
setDefaultObsidianVault, setDefaultObsidianVault,
setDefaultAgent, setDefaultAgent,
setSiyuanApiUrl, // setSiyuanApiUrl,
setSiyuanToken, // setSiyuanToken,
setSiyuanBoxId, // setSiyuanBoxId,
setAgentssubscribeUrl, setAgentssubscribeUrl,
setSiyuanRootPath, // setSiyuanRootPath,
setMaxKeepAliveMinapps, // setMaxKeepAliveMinapps,
setShowOpenedMinappsInSidebar, // setShowOpenedMinappsInSidebar,
setMinappsOpenLinkExternal, // setMinappsOpenLinkExternal,
setEnableDataCollection, // setEnableDataCollection,
setEnableSpellCheck, // setEnableSpellCheck,
setSpellCheckLanguages, // setSpellCheckLanguages,
setExportMenuOptions, // setExportMenuOptions,
setEnableQuickPanelTriggers, // setEnableQuickPanelTriggers,
setConfirmDeleteMessage, // setConfirmDeleteMessage,
setConfirmRegenerateMessage, // setConfirmRegenerateMessage,
// setDisableHardwareAcceleration, // setDisableHardwareAcceleration,
setOpenAISummaryText, setOpenAISummaryText,
setOpenAIVerbosity, setOpenAIVerbosity,
setNotificationSettings, // setNotificationSettings,
// Local backup settings // Local backup settings
setLocalBackupDir, // setLocalBackupDir,
setLocalBackupAutoSync, // setLocalBackupAutoSync,
setLocalBackupSyncInterval, // setLocalBackupSyncInterval,
setLocalBackupMaxBackups, // setLocalBackupMaxBackups,
setLocalBackupSkipBackupFile, // setLocalBackupSkipBackupFile,
setDefaultPaintingProvider, setDefaultPaintingProvider,
setS3, // setS3,
setS3Partial, // setS3Partial,
setEnableDeveloperMode, // setEnableDeveloperMode,
setNavbarPosition, // setNavbarPosition,
setShowMessageOutline, // setShowMessageOutline,
// API Server actions // API Server actions
setApiServerEnabled, setApiServerEnabled,
setApiServerPort, setApiServerPort,
setApiServerApiKey, setApiServerApiKey,
setShowWorkspace, // setShowWorkspace,
toggleShowWorkspace toggleShowWorkspace
} = settingsSlice.actions } = settingsSlice.actions