feat: transparent window settings

This commit is contained in:
kangfenmao 2024-09-04 10:26:00 +08:00
parent 9e2c7a08df
commit 14acd45927
11 changed files with 100 additions and 96 deletions

View File

@ -48,8 +48,8 @@
--status-bar-height: 40px; --status-bar-height: 40px;
--input-bar-height: 85px; --input-bar-height: 85px;
--assistants-width: 300px; --assistants-width: 280px;
--topic-list-width: 300px; --topic-list-width: 280px;
--settings-width: var(--assistants-width); --settings-width: var(--assistants-width);
} }

View File

@ -70,8 +70,8 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
open={open} open={open}
onCancel={onCancel} onCancel={onCancel}
afterClose={onClose} afterClose={onClose}
transitionName="" transitionName="ant-move-down"
maskTransitionName="" maskTransitionName="ant-fade"
footer={null}> footer={null}>
<Input <Input
placeholder={t('common.search')} placeholder={t('common.search')}

View File

@ -1,15 +1,18 @@
import { isMac } from '@renderer/config/constant' import { isMac } from '@renderer/config/constant'
import { useSettings } from '@renderer/hooks/useSettings'
import { useRuntime } from '@renderer/hooks/useStore' import { useRuntime } from '@renderer/hooks/useStore'
import { FC, PropsWithChildren } from 'react' import { FC, PropsWithChildren } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
type Props = PropsWithChildren & JSX.IntrinsicElements['div'] type Props = PropsWithChildren & JSX.IntrinsicElements['div']
const navbarBackgroundColor = isMac ? 'var(--navbar-background-mac)' : 'var(--navbar-background)'
export const Navbar: FC<Props> = ({ children, ...props }) => { export const Navbar: FC<Props> = ({ children, ...props }) => {
const { minappShow } = useRuntime() const { minappShow } = useRuntime()
const backgroundColor = minappShow ? 'var(--navbar-background)' : navbarBackgroundColor const { windowStyle } = useSettings()
const macTransparentWindow = isMac && windowStyle === 'transparent'
const navbarBgColor = macTransparentWindow ? 'var(--navbar-background-mac)' : 'var(--navbar-background)'
const backgroundColor = minappShow ? 'var(--navbar-background)' : navbarBgColor
return ( return (
<NavbarContainer {...props} style={{ backgroundColor }}> <NavbarContainer {...props} style={{ backgroundColor }}>
@ -39,7 +42,6 @@ const NavbarContainer = styled.div`
margin-left: ${isMac ? 'calc(var(--sidebar-width) * -1)' : 0}; margin-left: ${isMac ? 'calc(var(--sidebar-width) * -1)' : 0};
padding-left: ${isMac ? 'var(--sidebar-width)' : 0}; padding-left: ${isMac ? 'var(--sidebar-width)' : 0};
border-bottom: 0.5px solid var(--color-border); border-bottom: 0.5px solid var(--color-border);
background-color: ${navbarBackgroundColor};
transition: background-color 0.3s ease; transition: background-color 0.3s ease;
-webkit-app-region: drag; -webkit-app-region: drag;
` `

View File

@ -2,6 +2,7 @@ import { TranslationOutlined } from '@ant-design/icons'
import { isMac } from '@renderer/config/constant' import { isMac } from '@renderer/config/constant'
import { isLocalAi, UserAvatar } from '@renderer/config/env' import { isLocalAi, UserAvatar } from '@renderer/config/env'
import useAvatar from '@renderer/hooks/useAvatar' import useAvatar from '@renderer/hooks/useAvatar'
import { useSettings } from '@renderer/hooks/useSettings'
import { useRuntime, useShowAssistants } from '@renderer/hooks/useStore' import { useRuntime, useShowAssistants } from '@renderer/hooks/useStore'
import { Avatar } from 'antd' import { Avatar } from 'antd'
import { FC } from 'react' import { FC } from 'react'
@ -11,8 +12,6 @@ import styled from 'styled-components'
import UserPopup from '../Popups/UserPopup' import UserPopup from '../Popups/UserPopup'
const sidebarBackgroundColor = isMac ? 'var(--navbar-background-mac)' : 'var(--navbar-background)'
const Sidebar: FC = () => { const Sidebar: FC = () => {
const { pathname } = useLocation() const { pathname } = useLocation()
const avatar = useAvatar() const avatar = useAvatar()
@ -21,11 +20,15 @@ const Sidebar: FC = () => {
const { generating } = useRuntime() const { generating } = useRuntime()
const { t } = useTranslation() const { t } = useTranslation()
const navigate = useNavigate() const navigate = useNavigate()
const { windowStyle } = useSettings()
const isRoute = (path: string): string => (pathname === path ? 'active' : '') const isRoute = (path: string): string => (pathname === path ? 'active' : '')
const onEditUser = () => UserPopup.show() const onEditUser = () => UserPopup.show()
const macTransparentWindow = isMac && windowStyle === 'transparent'
const sidebarBgColor = macTransparentWindow ? 'var(--navbar-background-mac)' : 'var(--navbar-background)'
const to = (path: string) => { const to = (path: string) => {
if (generating) { if (generating) {
window.message.warning({ content: t('message.switch.disabled'), key: 'switch-assistant' }) window.message.warning({ content: t('message.switch.disabled'), key: 'switch-assistant' })
@ -39,7 +42,7 @@ const Sidebar: FC = () => {
} }
return ( return (
<Container style={{ backgroundColor: minappShow ? 'var(--navbar-background)' : sidebarBackgroundColor }}> <Container style={{ backgroundColor: minappShow ? 'var(--navbar-background)' : sidebarBgColor }}>
<AvatarImg src={avatar || UserAvatar} draggable={false} className="nodrag" onClick={onEditUser} /> <AvatarImg src={avatar || UserAvatar} draggable={false} className="nodrag" onClick={onEditUser} />
<MainMenus> <MainMenus>
<Menus> <Menus>
@ -87,7 +90,6 @@ const Container = styled.div`
-webkit-app-region: drag !important; -webkit-app-region: drag !important;
border-right: 0.5px solid var(--color-border); border-right: 0.5px solid var(--color-border);
margin-top: ${isMac ? 'var(--navbar-height)' : 0}; margin-top: ${isMac ? 'var(--navbar-height)' : 0};
background-color: ${sidebarBackgroundColor};
transition: background-color 0.3s ease; transition: background-color 0.3s ease;
` `

View File

@ -3,6 +3,7 @@ import {
SendMessageShortcut, SendMessageShortcut,
setSendMessageShortcut as _setSendMessageShortcut, setSendMessageShortcut as _setSendMessageShortcut,
setTheme, setTheme,
setWindowStyle,
ThemeMode ThemeMode
} from '@renderer/store/settings' } from '@renderer/store/settings'
@ -17,6 +18,9 @@ export function useSettings() {
}, },
setTheme(theme: ThemeMode) { setTheme(theme: ThemeMode) {
dispatch(setTheme(theme)) dispatch(setTheme(theme))
},
setWindowStyle(windowStyle: 'transparent' | 'opaque') {
dispatch(setWindowStyle(windowStyle))
} }
} }
} }

View File

@ -202,6 +202,9 @@ const resources = {
'theme.dark': 'Dark', 'theme.dark': 'Dark',
'theme.light': 'Light', 'theme.light': 'Light',
'theme.auto': 'Auto', 'theme.auto': 'Auto',
'theme.window.style.title': 'Window Style',
'theme.window.style.transparent': 'Transparent Window',
'theme.window.style.opaque': 'Opaque Window',
'font_size.title': 'Message Font Size' 'font_size.title': 'Message Font Size'
}, },
translate: { translate: {
@ -444,6 +447,9 @@ const resources = {
'theme.dark': '深色主题', 'theme.dark': '深色主题',
'theme.light': '浅色主题', 'theme.light': '浅色主题',
'theme.auto': '跟随系统', 'theme.auto': '跟随系统',
'theme.window.style.title': '窗口样式',
'theme.window.style.transparent': '透明窗口',
'theme.window.style.opaque': '不透明窗口',
'font_size.title': '消息字体大小' 'font_size.title': '消息字体大小'
}, },
translate: { translate: {

View File

@ -106,7 +106,7 @@ const Assistants: FC<Props> = ({
if (showTopics) { if (showTopics) {
return ( return (
<Container> <Container style={{ padding: 0 }}>
<Topics assistant={activeAssistant} activeTopic={activeTopic} setActiveTopic={setActiveTopic} /> <Topics assistant={activeAssistant} activeTopic={activeTopic} setActiveTopic={setActiveTopic} />
</Container> </Container>
) )
@ -155,6 +155,7 @@ const AssistantItem = styled.div`
position: relative; position: relative;
border-radius: 4px; border-radius: 4px;
margin: 0 10px; margin: 0 10px;
padding-right: 35px;
cursor: pointer; cursor: pointer;
font-family: Ubuntu; font-family: Ubuntu;
.anticon { .anticon {

View File

@ -64,7 +64,7 @@ const HomePage: FC = () => {
{showAssistants && ( {showAssistants && (
<NavbarLeft <NavbarLeft
style={{ justifyContent: 'space-between', alignItems: 'center', borderRight: 'none', padding: '0 8px' }}> style={{ justifyContent: 'space-between', alignItems: 'center', borderRight: 'none', padding: '0 8px' }}>
<NavigtaionBack onClick={() => setShowTopics(false)}> <NavigtaionBack className={showTopics ? 'back' : ''} onClick={() => setShowTopics(false)}>
{showTopics && <ArrowLeftOutlined />} {showTopics && <ArrowLeftOutlined />}
<NavigationBackTitle>{showTopics ? t('common.back') : t('common.chat')}</NavigationBackTitle> <NavigationBackTitle>{showTopics ? t('common.back') : t('common.chat')}</NavigationBackTitle>
</NavigtaionBack> </NavigtaionBack>
@ -124,16 +124,18 @@ const NavigtaionBack = styled.div`
align-items: center; align-items: center;
justify-content: flex-start; justify-content: flex-start;
gap: 10px; gap: 10px;
cursor: pointer; margin-left: ${isMac ? '10px' : 0};
margin-left: ${isMac ? '14px' : '2px'};
-webkit-app-region: none; -webkit-app-region: none;
transition: all 0.2s ease-in-out; transition: all 0.2s ease-in-out;
transition: opacity 0.2s ease-in-out; transition: opacity 0.2s ease-in-out;
padding: 2px 8px; padding: 3px 8px;
border-radius: 6px; border-radius: 6px;
&:hover { &.back {
background-color: var(--color-background-mute); cursor: pointer;
color: var(--color-text-1); &:hover {
background-color: var(--color-background-mute);
color: var(--color-text-1);
}
} }
` `
@ -172,7 +174,7 @@ export const NewButton = styled.div`
font-size: 17px; font-size: 17px;
} }
&:hover { &:hover {
background-color: var(--color-background-soft); background-color: var(--color-background-mute);
cursor: pointer; cursor: pointer;
color: var(--color-icon-white); color: var(--color-icon-white);
} }

View File

@ -1,13 +1,12 @@
import { DeleteOutlined, EditOutlined, OpenAIOutlined } from '@ant-design/icons' import { DeleteOutlined, EditOutlined, OpenAIOutlined } from '@ant-design/icons'
import { DragDropContext, Draggable, Droppable, DropResult } from '@hello-pangea/dnd' import DragableList from '@renderer/components/DragableList'
import PromptPopup from '@renderer/components/Popups/PromptPopup' import PromptPopup from '@renderer/components/Popups/PromptPopup'
import { useAssistant } from '@renderer/hooks/useAssistant' import { useAssistant } from '@renderer/hooks/useAssistant'
import { fetchMessagesSummary } from '@renderer/services/api' import { fetchMessagesSummary } from '@renderer/services/api'
import LocalStorage from '@renderer/services/storage' import LocalStorage from '@renderer/services/storage'
import { useAppSelector } from '@renderer/store' import { useAppSelector } from '@renderer/store'
import { Assistant, Topic } from '@renderer/types' import { Assistant, Topic } from '@renderer/types'
import { droppableReorder } from '@renderer/utils' import { Button, Dropdown, MenuProps } from 'antd'
import { Dropdown, MenuProps } from 'antd'
import { FC, useCallback } from 'react' import { FC, useCallback } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
@ -19,7 +18,7 @@ interface Props {
} }
const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic }) => { const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic }) => {
const { assistant, removeTopic, updateTopic, updateTopics } = useAssistant(_assistant.id) const { assistant, removeTopic, updateTopic, updateTopics, removeAllTopics } = useAssistant(_assistant.id)
const { t } = useTranslation() const { t } = useTranslation()
const generating = useAppSelector((state) => state.runtime.generating) const generating = useAppSelector((state) => state.runtime.generating)
@ -77,17 +76,6 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
[assistant, removeTopic, setActiveTopic, t, updateTopic] [assistant, removeTopic, setActiveTopic, t, updateTopic]
) )
const onDragEnd = useCallback(
(result: DropResult) => {
if (result.destination) {
const sourceIndex = result.source.index
const destIndex = result.destination.index
updateTopics(droppableReorder(assistant.topics, sourceIndex, destIndex))
}
},
[assistant.topics, updateTopics]
)
const onSwitchTopic = useCallback( const onSwitchTopic = useCallback(
(topic: Topic) => { (topic: Topic) => {
if (generating) { if (generating) {
@ -99,35 +87,35 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
[generating, setActiveTopic, t] [generating, setActiveTopic, t]
) )
const onDeleteAll = () => {
window.modal.confirm({
title: t('chat.topics.delete.all.title'),
content: t('chat.topics.delete.all.content'),
okButtonProps: { danger: true },
onOk: removeAllTopics
})
}
return ( return (
<Container> <Container>
<DragDropContext onDragEnd={onDragEnd}> <DragableList list={assistant.topics} onUpdate={updateTopics}>
<Droppable droppableId="droppable"> {(topic) => (
{(provided) => ( <Dropdown menu={{ items: getTopicMenuItems(topic) }} trigger={['contextMenu']} key={topic.id}>
<div {...provided.droppableProps} ref={provided.innerRef}> <TopicListItem
{assistant.topics.map((topic, index) => ( className={topic.id === activeTopic?.id ? 'active' : ''}
<Draggable key={`draggable_${topic.id}_${index}`} draggableId={topic.id} index={index}> onClick={() => onSwitchTopic(topic)}>
{(provided) => ( {topic.name}
<div </TopicListItem>
ref={provided.innerRef} </Dropdown>
{...provided.draggableProps} )}
{...provided.dragHandleProps} </DragableList>
style={{ ...provided.draggableProps.style, marginBottom: 5 }}> {assistant.topics.length > 20 && (
<Dropdown menu={{ items: getTopicMenuItems(topic) }} trigger={['contextMenu']} key={topic.id}> <Footer>
<TopicListItem <Button style={{ width: '100%' }} onClick={onDeleteAll}>
className={topic.id === activeTopic?.id ? 'active' : ''} {t('chat.topics.delete.all.title')}
onClick={() => onSwitchTopic(topic)}> </Button>
{topic.name} </Footer>
</TopicListItem> )}
</Dropdown>
</div>
)}
</Draggable>
))}
</div>
)}
</Droppable>
</DragDropContext>
</Container> </Container>
) )
} }
@ -136,14 +124,11 @@ const Container = styled.div`
display: flex; display: flex;
flex: 1; flex: 1;
flex-direction: column; flex-direction: column;
overflow-y: scroll;
padding-top: 10px; padding-top: 10px;
padding-bottom: 10px;
margin-top: -10px;
` `
const TopicListItem = styled.div` const TopicListItem = styled.div`
padding: 7px 10px; padding: 6px 10px;
margin: 0 10px; margin: 0 10px;
cursor: pointer; cursor: pointer;
border-radius: 4px; border-radius: 4px;
@ -151,6 +136,7 @@ const TopicListItem = styled.div`
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
font-family: Ubuntu; font-family: Ubuntu;
font-size: 13px;
&:hover { &:hover {
background-color: var(--color-background-soft); background-color: var(--color-background-soft);
} }
@ -160,4 +146,10 @@ const TopicListItem = styled.div`
} }
` `
const Footer = styled.div`
padding: 0 10px;
padding-bottom: 10px;
width: 100%;
`
export default Topics export default Topics

View File

@ -1,16 +1,13 @@
import { FolderOpenOutlined, SaveOutlined } from '@ant-design/icons' import { FolderOpenOutlined, SaveOutlined } from '@ant-design/icons'
import { HStack } from '@renderer/components/Layout' import { HStack } from '@renderer/components/Layout'
import useAvatar from '@renderer/hooks/useAvatar'
import { useSettings } from '@renderer/hooks/useSettings' import { useSettings } from '@renderer/hooks/useSettings'
import i18n from '@renderer/i18n' import i18n from '@renderer/i18n'
import { backup, reset, restore } from '@renderer/services/backup' import { backup, reset, restore } from '@renderer/services/backup'
import LocalStorage from '@renderer/services/storage'
import { useAppDispatch } from '@renderer/store' import { useAppDispatch } from '@renderer/store'
import { setAvatar } from '@renderer/store/runtime'
import { setLanguage, setUserName, ThemeMode } from '@renderer/store/settings' import { setLanguage, setUserName, ThemeMode } from '@renderer/store/settings'
import { setProxyUrl as _setProxyUrl } from '@renderer/store/settings' import { setProxyUrl as _setProxyUrl } from '@renderer/store/settings'
import { compressImage, isValidProxyUrl } from '@renderer/utils' import { isValidProxyUrl } from '@renderer/utils'
import { Avatar, Button, Input, Select, Upload } from 'antd' import { Avatar, Button, Input, Select } from 'antd'
import { FC, useState } from 'react' import { FC, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
@ -18,8 +15,7 @@ import styled from 'styled-components'
import { SettingContainer, SettingDivider, SettingRow, SettingRowTitle, SettingTitle } from '.' import { SettingContainer, SettingDivider, SettingRow, SettingRowTitle, SettingTitle } from '.'
const GeneralSettings: FC = () => { const GeneralSettings: FC = () => {
const avatar = useAvatar() const { language, proxyUrl: storeProxyUrl, userName, theme, windowStyle, setTheme, setWindowStyle } = useSettings()
const { language, proxyUrl: storeProxyUrl, userName, theme, setTheme } = useSettings()
const [proxyUrl, setProxyUrl] = useState<string | undefined>(storeProxyUrl) const [proxyUrl, setProxyUrl] = useState<string | undefined>(storeProxyUrl)
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const { t } = useTranslation() const { t } = useTranslation()
@ -72,24 +68,16 @@ const GeneralSettings: FC = () => {
</SettingRow> </SettingRow>
<SettingDivider /> <SettingDivider />
<SettingRow> <SettingRow>
<SettingRowTitle>{t('common.avatar')}</SettingRowTitle> <SettingRowTitle>{t('settings.theme.window.style.title')}</SettingRowTitle>
<Upload <Select
customRequest={() => {}} defaultValue={windowStyle || 'opaque'}
accept="image/png, image/jpeg" style={{ width: 120 }}
itemRender={() => null} onChange={setWindowStyle}
maxCount={1} options={[
onChange={async ({ file }) => { { value: 'transparent', label: t('settings.theme.window.style.transparent') },
try { { value: 'opaque', label: t('settings.theme.window.style.opaque') }
const _file = file.originFileObj as File ]}
const compressedFile = await compressImage(_file) />
await LocalStorage.storeImage('avatar', compressedFile)
dispatch(setAvatar(await LocalStorage.getImage('avatar')))
} catch (error: any) {
window.message.error(error.message)
}
}}>
<UserAvatar src={avatar} size="large" />
</Upload>
</SettingRow> </SettingRow>
<SettingDivider /> <SettingDivider />
<SettingRow> <SettingRow>
@ -98,7 +86,7 @@ const GeneralSettings: FC = () => {
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))} onChange={(e) => dispatch(setUserName(e.target.value))}
style={{ width: 150 }} style={{ width: 170 }}
maxLength={30} maxLength={30}
/> />
</SettingRow> </SettingRow>
@ -109,7 +97,7 @@ const GeneralSettings: FC = () => {
placeholder="socks5://127.0.0.1:6153" placeholder="socks5://127.0.0.1:6153"
value={proxyUrl} value={proxyUrl}
onChange={(e) => setProxyUrl(e.target.value)} onChange={(e) => setProxyUrl(e.target.value)}
style={{ width: 300 }} style={{ width: 170 }}
onBlur={() => onSetProxyUrl()} onBlur={() => onSetProxyUrl()}
type="url" type="url"
/> />
@ -117,7 +105,7 @@ const GeneralSettings: FC = () => {
<SettingDivider /> <SettingDivider />
<SettingRow> <SettingRow>
<SettingRowTitle>{t('settings.general.backup.title')}</SettingRowTitle> <SettingRowTitle>{t('settings.general.backup.title')}</SettingRowTitle>
<HStack gap="5px"> <HStack gap="5px" w="170px" justifyContent="space-between">
<Button onClick={backup} icon={<SaveOutlined />}> <Button onClick={backup} icon={<SaveOutlined />}>
{t('settings.general.backup.button')} {t('settings.general.backup.button')}
</Button> </Button>

View File

@ -18,6 +18,7 @@ export interface SettingsState {
messageFont: 'system' | 'serif' messageFont: 'system' | 'serif'
showInputEstimatedTokens: boolean showInputEstimatedTokens: boolean
theme: ThemeMode theme: ThemeMode
windowStyle: 'transparent' | 'opaque'
fontSize: number fontSize: number
} }
@ -31,6 +32,7 @@ const initialState: SettingsState = {
messageFont: 'system', messageFont: 'system',
showInputEstimatedTokens: false, showInputEstimatedTokens: false,
theme: ThemeMode.light, theme: ThemeMode.light,
windowStyle: 'opaque',
fontSize: 14 fontSize: 14
} }
@ -67,6 +69,10 @@ const settingsSlice = createSlice({
}, },
setFontSize: (state, action: PayloadAction<number>) => { setFontSize: (state, action: PayloadAction<number>) => {
state.fontSize = action.payload state.fontSize = action.payload
},
setWindowStyle: (state, action: PayloadAction<'transparent' | 'opaque'>) => {
state.windowStyle = action.payload
console.log(state.windowStyle)
} }
} }
}) })
@ -81,7 +87,8 @@ export const {
setMessageFont, setMessageFont,
setShowInputEstimatedTokens, setShowInputEstimatedTokens,
setTheme, setTheme,
setFontSize setFontSize,
setWindowStyle
} = settingsSlice.actions } = settingsSlice.actions
export default settingsSlice.reducer export default settingsSlice.reducer