refactor: sidebar minapps

This commit is contained in:
kangfenmao 2025-01-13 13:04:01 +08:00
parent 7633d70435
commit 49a5bc7900
19 changed files with 336 additions and 315 deletions

View File

@ -47,19 +47,22 @@ const DragableList: FC<Props<any>> = ({
<Droppable droppableId="droppable" {...droppableProps}>
{(provided) => (
<div {...provided.droppableProps} ref={provided.innerRef} style={{ ...style }}>
{list.map((item, index) => (
<Draggable key={`draggable_${item.id}_${index}`} draggableId={item.id} index={index} {...droppableProps}>
{(provided) => (
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
style={{ ...provided.draggableProps.style, marginBottom: 8, ...listStyle }}>
{children(item, index)}
</div>
)}
</Draggable>
))}
{list.map((item, index) => {
const id = item.id || item
return (
<Draggable key={`draggable_${id}_${index}`} draggableId={id} index={index} {...droppableProps}>
{(provided) => (
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
style={{ ...provided.draggableProps.style, marginBottom: 8, ...listStyle }}>
{children(item, index)}
</div>
)}
</Draggable>
)
})}
</div>
)}
</Droppable>

View File

@ -5,6 +5,7 @@ import { useBridge } from '@renderer/hooks/useBridge'
import store from '@renderer/store'
import { setMinappShow } from '@renderer/store/runtime'
import { MinAppType } from '@renderer/types'
import { delay } from '@renderer/utils'
import { Avatar, Drawer } from 'antd'
import { WebviewTag } from 'electron'
import { useEffect, useRef, useState } from 'react'
@ -28,9 +29,10 @@ const PopupContainer: React.FC<Props> = ({ app, resolve }) => {
const canOpenExternalLink = app.url.startsWith('http://') || app.url.startsWith('https://')
const onClose = () => {
const onClose = async (_delay = 0.3) => {
setOpen(false)
setTimeout(() => resolve({}), 300)
await delay(_delay)
resolve({})
}
MinApp.onClose = onClose
@ -58,7 +60,7 @@ const PopupContainer: React.FC<Props> = ({ app, resolve }) => {
<ExportOutlined />
</Button>
)}
<Button onClick={onClose}>
<Button onClick={() => onClose()}>
<CloseOutlined />
</Button>
</ButtonsGroup>
@ -99,7 +101,7 @@ const PopupContainer: React.FC<Props> = ({ app, resolve }) => {
<Drawer
title={<Title />}
placement="bottom"
onClose={onClose}
onClose={() => onClose()}
open={open}
mask={true}
rootClassName="minapp-drawer"
@ -202,40 +204,39 @@ const EmptyView = styled.div`
export default class MinApp {
static topviewId = 0
static onClose = () => {}
static isOpening = false
static app: MinAppType | null = null
static async start(app: MinAppType) {
if (this.isOpening) return
this.isOpening = true
try {
// 先关闭现有的小程序
await this.close()
// 确保 webview 完全卸载
await new Promise(resolve => setTimeout(resolve, 100))
store.dispatch(setMinappShow(true))
return new Promise<any>((resolve) => {
TopView.show(
<PopupContainer
app={app}
resolve={(v) => {
resolve(v)
this.close()
}}
/>,
'MinApp'
)
})
} finally {
this.isOpening = false
if (MinApp.app?.id === app.id) {
return
}
if (MinApp.app) {
// @ts-ignore delay params
await MinApp.onClose(0)
await delay(0)
}
MinApp.app = app
store.dispatch(setMinappShow(true))
return new Promise<any>((resolve) => {
TopView.show(
<PopupContainer
app={app}
resolve={(v) => {
resolve(v)
this.close()
}}
/>,
'MinApp'
)
})
}
static close() {
if (!this.isOpening) return
TopView.hide('MinApp')
store.dispatch(setMinappShow(false))
MinApp.app = null
}
}

View File

@ -1,18 +1,21 @@
import { FileSearchOutlined, FolderOutlined, PictureOutlined, TranslationOutlined } from '@ant-design/icons'
import { isMac } from '@renderer/config/constant'
import { isLocalAi, UserAvatar } from '@renderer/config/env'
import { getAllMinApps } from '@renderer/config/minapps'
import { useTheme } from '@renderer/context/ThemeProvider'
import useAvatar from '@renderer/hooks/useAvatar'
import { useMinapps } from '@renderer/hooks/useMinapps'
import { modelGenerating, useRuntime } from '@renderer/hooks/useRuntime'
import { useSettings } from '@renderer/hooks/useSettings'
import type { MenuProps } from 'antd'
import { Tooltip } from 'antd'
import { Avatar } from 'antd'
import { Dropdown } from 'antd'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import { useLocation, useNavigate } from 'react-router-dom'
import styled from 'styled-components'
import DragableList from '../DragableList'
import MinApp from '../MinApp'
import UserPopup from '../Popups/UserPopup'
@ -22,79 +25,22 @@ const Sidebar: FC = () => {
const { minappShow } = useRuntime()
const { t } = useTranslation()
const navigate = useNavigate()
const { windowStyle, sidebarIcons, miniAppIcons } = useSettings()
const { windowStyle, sidebarIcons } = useSettings()
const { theme, toggleTheme } = useTheme()
const allApps = getAllMinApps()
const isRoute = (path: string): string => (pathname === path ? 'active' : '')
const isRoutes = (path: string): string => (pathname.startsWith(path) ? 'active' : '')
const { pinned } = useMinapps()
const onEditUser = () => UserPopup.show()
const macTransparentWindow = isMac && windowStyle === 'transparent'
const sidebarBgColor = macTransparentWindow ? 'transparent' : 'var(--navbar-background)'
const showPinnedApps = pinned.length > 0 && sidebarIcons.visible.includes('minapp')
const to = async (path: string) => {
await modelGenerating()
navigate(path)
}
const renderMainMenus = () => {
return sidebarIcons.visible.map((icon) => {
const iconMap = {
assistants: <i className="iconfont icon-chat" />,
agents: <i className="iconfont icon-business-smart-assistant" />,
paintings: <PictureOutlined style={{ fontSize: 16 }} />,
translate: <TranslationOutlined />,
minapp: <i className="iconfont icon-appstore" />,
knowledge: <FileSearchOutlined />,
files: <FolderOutlined />
}
const pathMap = {
assistants: '/',
agents: '/agents',
paintings: '/paintings',
translate: '/translate',
minapp: '/apps',
knowledge: '/knowledge',
files: '/files'
}
const path = pathMap[icon]
const isActive = path === '/' ? isRoute(path) : isRoutes(path)
return (
<Tooltip key={icon} title={t(`${icon}.title`)} mouseEnterDelay={0.8} placement="right">
<StyledLink onClick={() => to(path)}>
<Icon className={isActive}>{iconMap[icon]}</Icon>
</StyledLink>
</Tooltip>
)
})
}
const renderPinnedApps = () => {
if (!miniAppIcons?.pinned) return null
const pinnedApps = allApps.filter((app) => miniAppIcons.pinned.includes(app.id))
return pinnedApps.map((app) => (
<Tooltip key={app.id} title={app.name} mouseEnterDelay={0.8} placement="right">
<StyledLink>
<Icon onClick={() => MinApp.start(app)}>
<AppIcon
src={app.logo}
style={{
width: '20px',
height: '20px',
border: app.bodered ? '0.5px solid var(--color-border)' : 'none'
}}
/>
</Icon>
</StyledLink>
</Tooltip>
))
}
return (
<Container
id="app-sidebar"
@ -103,14 +49,19 @@ const Sidebar: FC = () => {
zIndex: minappShow ? 10000 : 'initial'
}}>
<AvatarImg src={avatar || UserAvatar} draggable={false} className="nodrag" onClick={onEditUser} />
<MainMenus>
<ScrollContainer>
<Menus onClick={MinApp.onClose}>
{renderMainMenus()}
{renderPinnedApps()}
</Menus>
</ScrollContainer>
</MainMenus>
<MainMenusContainer>
<Menus onClick={MinApp.onClose}>
<MainMenus />
</Menus>
{showPinnedApps && (
<AppsContainer>
<Divider />
<Menus>
<PinnedApps />
</Menus>
</AppsContainer>
)}
</MainMenusContainer>
<Menus onClick={MinApp.onClose}>
<Tooltip title={t('settings.theme.title')} mouseEnterDelay={0.8} placement="right">
<Icon onClick={() => toggleTheme()}>
@ -133,6 +84,89 @@ const Sidebar: FC = () => {
)
}
const MainMenus: FC = () => {
const { t } = useTranslation()
const { pathname } = useLocation()
const { sidebarIcons } = useSettings()
const navigate = useNavigate()
const isRoute = (path: string): string => (pathname === path ? 'active' : '')
const isRoutes = (path: string): string => (pathname.startsWith(path) ? 'active' : '')
const iconMap = {
assistants: <i className="iconfont icon-chat" />,
agents: <i className="iconfont icon-business-smart-assistant" />,
paintings: <PictureOutlined style={{ fontSize: 16 }} />,
translate: <TranslationOutlined />,
minapp: <i className="iconfont icon-appstore" />,
knowledge: <FileSearchOutlined />,
files: <FolderOutlined />
}
const pathMap = {
assistants: '/',
agents: '/agents',
paintings: '/paintings',
translate: '/translate',
minapp: '/apps',
knowledge: '/knowledge',
files: '/files'
}
return sidebarIcons.visible.map((icon) => {
const path = pathMap[icon]
const isActive = path === '/' ? isRoute(path) : isRoutes(path)
return (
<Tooltip key={icon} title={t(`${icon}.title`)} mouseEnterDelay={0.8} placement="right">
<StyledLink onClick={() => navigate(path)}>
<Icon className={isActive}>{iconMap[icon]}</Icon>
</StyledLink>
</Tooltip>
)
})
}
const PinnedApps: FC = () => {
const { pinned, updatePinnedMinapps } = useMinapps()
const { t } = useTranslation()
return (
<DragableList list={pinned} onUpdate={updatePinnedMinapps}>
{(app) => {
const menuItems: MenuProps['items'] = [
{
key: 'togglePin',
label: t('minapp.sidebar.remove.title'),
onClick: () => {
const newPinned = pinned.filter((item) => item.id !== app.id)
updatePinnedMinapps(newPinned)
}
}
]
return (
<Tooltip key={app.id} title={app.name} mouseEnterDelay={0.8} placement="right">
<StyledLink>
<Dropdown menu={{ items: menuItems }} trigger={['contextMenu']}>
<Icon onClick={() => MinApp.start(app)}>
<AppIcon
src={app.logo}
style={{
width: '20px',
height: '20px',
border: app.bodered ? '0.5px solid var(--color-border)' : 'none'
}}
/>
</Icon>
</Dropdown>
</StyledLink>
</Tooltip>
)
}}
</DragableList>
)
}
const Container = styled.div`
display: flex;
flex-direction: column;
@ -154,9 +188,10 @@ const AvatarImg = styled(Avatar)`
border: none;
cursor: pointer;
`
const MainMenus = styled.div`
const MainMenusContainer = styled.div`
display: flex;
flex: 1;
flex-direction: column;
overflow: hidden;
`
@ -164,6 +199,7 @@ const Menus = styled.div`
display: flex;
flex-direction: column;
align-items: center;
gap: 5px;
`
const Icon = styled.div`
@ -173,7 +209,6 @@ const Icon = styled.div`
justify-content: center;
align-items: center;
border-radius: 50%;
margin-bottom: 5px;
-webkit-app-region: none;
border: 0.5px solid transparent;
.iconfont,
@ -215,27 +250,23 @@ const AppIcon = styled.img`
border-radius: 6px;
`
const ScrollContainer = styled.div`
const AppsContainer = styled.div`
display: flex;
flex: 1;
flex-direction: column;
align-items: center;
overflow-y: auto;
overflow-x: hidden;
height: 100%;
margin-bottom: 10px;
&::-webkit-scrollbar {
width: 0px;
}
&:hover::-webkit-scrollbar {
width: 4px;
}
&::-webkit-scrollbar-thumb {
background-color: var(--color-border);
border-radius: 4px;
}
&::-webkit-scrollbar-track {
background: transparent;
display: none;
}
`
const Divider = styled.div`
width: 50%;
margin: 8px 0;
border-bottom: 0.5px solid var(--color-border);
`
export default Sidebar

View File

@ -0,0 +1,23 @@
import { RootState, useAppDispatch, useAppSelector } from '@renderer/store'
import { setDisabledMinApps, setMinApps, setPinnedMinApps } from '@renderer/store/minapps'
import { MinAppType } from '@renderer/types'
export const useMinapps = () => {
const { enabled, disabled, pinned } = useAppSelector((state: RootState) => state.minapps)
const dispatch = useAppDispatch()
return {
minapps: enabled,
disabled,
pinned,
updateMinapps: (minapps: MinAppType[]) => {
dispatch(setMinApps(minapps))
},
updateDisabledMinapps: (minapps: MinAppType[]) => {
dispatch(setDisabledMinApps(minapps))
},
updatePinnedMinapps: (minapps: MinAppType[]) => {
dispatch(setPinnedMinApps(minapps))
}
}
}

View File

@ -2,13 +2,14 @@ import store, { useAppDispatch, useAppSelector } from '@renderer/store'
import {
SendMessageShortcut,
setSendMessageShortcut as _setSendMessageShortcut,
setSidebarIcons,
setTheme,
SettingsState,
setTopicPosition,
setTray,
setWindowStyle
} from '@renderer/store/settings'
import { ThemeMode } from '@renderer/types'
import { SidebarIcon, ThemeMode } from '@renderer/types'
export function useSettings() {
const settings = useAppSelector((state) => state.settings)
@ -30,6 +31,15 @@ export function useSettings() {
},
setTopicPosition(topicPosition: 'left' | 'right') {
dispatch(setTopicPosition(topicPosition))
},
updateSidebarIcons(icons: { visible: SidebarIcon[]; disabled: SidebarIcon[] }) {
dispatch(setSidebarIcons(icons))
},
updateSidebarVisibleIcons(icons: SidebarIcon[]) {
dispatch(setSidebarIcons({ visible: icons }))
},
updateSidebarDisabledIcons(icons: SidebarIcon[]) {
dispatch(setSidebarIcons({ disabled: icons }))
}
}
}

View File

@ -265,8 +265,8 @@
},
"minapp": {
"title": "MinApp",
"sidebar.add.title": "Add minAPP to sidebar",
"sidebar.remove.title": "Remove minAPP from sidebar"
"sidebar.add.title": "Add to sidebar",
"sidebar.remove.title": "Remove from sidebar"
},
"ollama": {
"keep_alive_time.description": "The time in minutes to keep the connection alive, default is 5 minutes.",
@ -403,15 +403,15 @@
"display.sidebar.knowledge.icon": "Show Knowledge icon",
"display.sidebar.files.icon": "Show Files icon",
"display.sidebar.title": "Sidebar Settings",
"display.sidebar.visible": "Show my sidebar icons",
"display.sidebar.disabled": "Hide my sidebar icons",
"display.sidebar.visible": "Show icons",
"display.sidebar.disabled": "Hide icons",
"display.sidebar.chat.hiddenMessage": "Assistants are basic functions, not supported for hiding",
"display.sidebar.empty": "Drag the hidden feature from the left side here",
"display.minApp.title": "MinApp Settings",
"display.minApp.visible": "Visible MinApp",
"display.minApp.disabled": "Hidden MinApp",
"display.minApp.empty": "Drag minApp from the left to hide them here",
"display.minApp.pinnedError": "MinApp that have been added to the sidebar do not support hiding. If you want to hide them, please remove them from the sidebar first.",
"": "MinApp that have been added to the sidebar do not support hiding. If you want to hide them, please remove them from the sidebar first.",
"display.topic.title": "Topic Settings",
"display.custom.css": "Custom CSS",
"display.custom.css.placeholder": "/* Put custom CSS here */",

View File

@ -263,8 +263,8 @@
},
"minapp": {
"title": "ミニアプリ",
"sidebar.add.title": "ミニプログラムをサイドバーに追加",
"sidebar.remove.title": "サイドバーからアプレットを削除する"
"sidebar.add.title": "サイドバーに追加",
"sidebar.remove.title": "サイドバーから削除"
},
"ollama": {
"keep_alive_time.description": "モデルがメモリに保持される時間デフォルト5分",
@ -401,8 +401,8 @@
"display.sidebar.knowledge.icon": "ナレッジのアイコンを表示",
"display.sidebar.files.icon": "ファイルのアイコンを表示",
"display.sidebar.title": "サイドバー設定",
"display.sidebar.visible": "サイドバーのアイコンを表示する",
"display.sidebar.disabled": "サイドバーのアイコンを非表示にする",
"display.sidebar.visible": "アイコンを表示",
"display.sidebar.disabled": "アイコンを非表示",
"display.sidebar.chat.hiddenMessage": "アシスタントは基本的な機能であり、非表示はサポートされていません",
"display.sidebar.empty": "非表示にする機能を左側からここにドラッグ",
"display.topic.title": "トピック設定",
@ -412,7 +412,6 @@
"display.minApp.visible": "表示中ミニプログラム",
"display.minApp.disabled": "非表示ミニプログラム",
"display.minApp.empty": "非表示にしたいアプレットを左からここまでドラッグします",
"display.minApp.pinnedError": "サイドバーに追加されたミニ プログラムは非表示をサポートしていません。非表示にしたい場合は、まずサイドバーから削除してください",
"input.auto_translate_with_space": "スペースを3回押して翻訳",
"messages.divider": "メッセージ間に区切り線を表示",
"messages.input.paste_long_text_as_file": "長いテキストをファイルとして貼り付け",

View File

@ -265,8 +265,8 @@
},
"minapp": {
"title": "Встроенные приложения",
"sidebar.add.title": "Добавить мини-программу на боковую панель",
"sidebar.remove.title": "Удалить апплет из боковой панели"
"sidebar.add.title": "Добавить в боковую панель",
"sidebar.remove.title": "Удалить из боковой панели"
},
"ollama": {
"keep_alive_time.description": "Время в минутах, в течение которого модель остается активной, по умолчанию 5 минут.",
@ -403,15 +403,14 @@
"display.sidebar.knowledge.icon": "Показывать иконку знаний",
"display.sidebar.files.icon": "Показывать иконку файлов",
"display.sidebar.title": "Настройки боковой панели",
"display.sidebar.visible": "Показать мои значки на боковой панели",
"display.sidebar.disabled": "Скрыть значок на боковой панели",
"display.sidebar.visible": "Показывать иконки",
"display.sidebar.disabled": "Скрыть иконки",
"display.sidebar.chat.hiddenMessage": "Помощник является базовой функцией и не поддерживает скрытие",
"display.sidebar.empty": "Перетащите скрываемую функцию с левой стороны сюда",
"display.minApp.title": "Настройки отображения мини программы",
"display.minApp.visible": "Отображаемый апплет",
"display.minApp.disabled": "скрытый апплет",
"display.minApp.empty": "Перетащите апплет, который хотите скрыть, слева сюда",
"display.minApp.pinnedError": "Мини-программы, добавленные на боковую панель, не поддерживают скрытие. Если вы хотите скрыть их, сначала удалите их с боковой панели",
"display.topic.title": "Настройки топиков",
"display.custom.css": "Пользовательский CSS",
"display.custom.css.placeholder": "/* Здесь введите пользовательский CSS */",

View File

@ -266,8 +266,8 @@
},
"minapp": {
"title": "小程序",
"sidebar.add.title": "添加小程序到侧边栏",
"sidebar.remove.title": "从侧边栏移除小程序"
"sidebar.add.title": "添加到侧边栏",
"sidebar.remove.title": "从侧边栏移除"
},
"ollama": {
"keep_alive_time.description": "对话后模型在内存中保持的时间默认5分钟",
@ -404,15 +404,14 @@
"display.sidebar.knowledge.icon": "显示知识图标",
"display.sidebar.files.icon": "显示文件图标",
"display.sidebar.title": "侧边栏设置",
"display.sidebar.visible": "显示侧边栏图标",
"display.sidebar.disabled": "隐藏侧边栏图标",
"display.sidebar.visible": "显示的图标",
"display.sidebar.disabled": "隐藏的图标",
"display.sidebar.chat.hiddenMessage": "助手是基础功能,不支持隐藏",
"display.sidebar.empty": "把要隐藏的功能从左侧拖拽到这里",
"display.minApp.title": "小程序显示设置",
"display.minApp.visible": "显示的小程序",
"display.minApp.disabled": "隐藏的小程序",
"display.minApp.empty": "把要隐藏的小程序从左侧拖拽到这里",
"display.minApp.pinnedError": "已经添加到侧边栏的小程序,不支持隐藏,如需隐藏请先从侧边栏移除",
"display.topic.title": "话题设置",
"display.custom.css": "自定义 CSS",
"display.custom.css.placeholder": "/* 这里写自定义CSS */",

View File

@ -265,8 +265,8 @@
},
"minapp": {
"title": "小程序",
"sidebar.add.title": "新增小程式到側邊欄",
"sidebar.remove.title": "從側邊欄移除小程式"
"sidebar.add.title": "添加到側邊欄",
"sidebar.remove.title": "從側邊欄移除"
},
"ollama": {
"keep_alive_time.description": "對話後模型在記憶體中保持的時間(預設為 5 分鐘)。",
@ -406,13 +406,12 @@
"display.topic.title": "話題設定",
"display.sidebar.chat.hiddenMessage": "助手是基礎功能,不支援隱藏",
"display.sidebar.empty": "把要隱藏的功能從左側拖拽到這裡",
"display.sidebar.visible": "顯示側邊欄圖標",
"display.sidebar.disabled": "隱藏側邊欄圖標",
"display.sidebar.visible": "顯示的圖標",
"display.sidebar.disabled": "隱藏的圖標",
"display.minApp.title": "小程序顯示設定",
"display.minApp.visible": "顯示的小程序",
"display.minApp.disabled": "隱藏的小程序",
"display.minApp.empty": "把要隱藏的小程序從左側拖拽到這裡",
"display.minApp.pinnedError": "已新增至側邊欄的小程序,不支援隱藏,如需隱藏請先從側邊欄移除",
"display.custom.css": "自定義 CSS",
"display.custom.css.placeholder": "/* 這裡寫自定義 CSS */",
"input.auto_translate_with_space": "快速敲擊3次空格翻譯",

View File

@ -1,6 +1,5 @@
import MinApp from '@renderer/components/MinApp'
import { useAppDispatch, useAppSelector } from '@renderer/store'
import { setMiniAppIcons } from '@renderer/store/settings'
import { useMinapps } from '@renderer/hooks/useMinapps'
import { MinAppType } from '@renderer/types'
import type { MenuProps } from 'antd'
import { Dropdown } from 'antd'
@ -16,10 +15,9 @@ interface Props {
const App: FC<Props> = ({ app, onClick, size = 60 }) => {
const { t } = useTranslation()
const dispatch = useAppDispatch()
const { miniAppIcons } = useAppSelector((state) => state.settings)
const isPinned = miniAppIcons?.pinned.includes(app.id)
const isVisible = miniAppIcons?.visible.includes(app.id)
const { minapps, pinned, updatePinnedMinapps } = useMinapps()
const isPinned = pinned.some((p) => p.id === app.id)
const isVisible = minapps.some((m) => m.id === app.id)
const handleClick = () => {
MinApp.start(app)
@ -31,17 +29,8 @@ const App: FC<Props> = ({ app, onClick, size = 60 }) => {
key: 'togglePin',
label: isPinned ? t('minapp.sidebar.remove.title') : t('minapp.sidebar.add.title'),
onClick: () => {
const newPinned = isPinned
? miniAppIcons.pinned.filter((id) => id !== app.id)
: [...(miniAppIcons.pinned || []), app.id]
dispatch(
setMiniAppIcons({
...miniAppIcons,
pinned: newPinned,
visible: isPinned ? miniAppIcons.visible : [...new Set([...miniAppIcons.visible, app.id])]
})
)
const newPinned = isPinned ? pinned.filter((item) => item.id !== app.id) : [...(pinned || []), app]
updatePinnedMinapps(newPinned)
}
}
]

View File

@ -1,11 +1,10 @@
import { SearchOutlined } from '@ant-design/icons'
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
import { Center } from '@renderer/components/Layout'
import { getAllMinApps } from '@renderer/config/minapps'
import { useSettings } from '@renderer/hooks/useSettings'
import { useMinapps } from '@renderer/hooks/useMinapps'
import { Empty, Input } from 'antd'
import { isEmpty } from 'lodash'
import React, { FC, useMemo, useState } from 'react'
import React, { FC, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
@ -14,24 +13,15 @@ import App from './App'
const AppsPage: FC = () => {
const { t } = useTranslation()
const [search, setSearch] = useState('')
const { miniAppIcons } = useSettings()
const allApps = useMemo(() => getAllMinApps(), [])
const { minapps } = useMinapps()
// 只显示可见的小程序,但包括所有固定的小程序
const visibleApps = useMemo(() => {
if (!miniAppIcons?.visible) return allApps
const visibleIds = new Set([
...miniAppIcons.visible,
...(miniAppIcons.pinned || []) // 确保固定的小程序总是可见
])
return allApps.filter((app) => visibleIds.has(app.id))
}, [allApps, miniAppIcons?.visible, miniAppIcons?.pinned])
console.debug('minapps', minapps)
const filteredApps = search
? visibleApps.filter(
? minapps.filter(
(app) => app.name.toLowerCase().includes(search.toLowerCase()) || app.url.includes(search.toLowerCase())
)
: visibleApps
: minapps
// Calculate the required number of lines
const itemsPerRow = Math.floor(930 / 115) // Maximum width divided by the width of each item (including spacing)

View File

@ -1,13 +1,13 @@
import { isMac } from '@renderer/config/constant'
import { getAllMinApps } from '@renderer/config/minapps'
import { useTheme } from '@renderer/context/ThemeProvider'
import { useMinapps } from '@renderer/hooks/useMinapps'
import { useSettings } from '@renderer/hooks/useSettings'
import { useAppDispatch } from '@renderer/store'
import {
DEFAULT_MINIAPP_ICONS,
DEFAULT_SIDEBAR_ICONS,
setClickAssistantToShowTopic,
setCustomCss,
setMiniAppIcons,
setShowTopicTime,
setSidebarIcons
} from '@renderer/store/settings'
@ -32,17 +32,17 @@ const DisplaySettings: FC = () => {
clickAssistantToShowTopic,
showTopicTime,
customCss,
sidebarIcons,
miniAppIcons
sidebarIcons
} = useSettings()
const { minapps, disabled, updateMinapps, updateDisabledMinapps } = useMinapps()
const { theme: themeMode } = useTheme()
const { t } = useTranslation()
const dispatch = useAppDispatch()
const [visibleIcons, setVisibleIcons] = useState(sidebarIcons?.visible || DEFAULT_SIDEBAR_ICONS)
const [disabledIcons, setDisabledIcons] = useState(sidebarIcons?.disabled || [])
const [visibleMiniApps, setVisibleMiniApps] = useState(miniAppIcons?.visible || DEFAULT_MINIAPP_ICONS)
const [disabledMiniApps, setDisabledMiniApps] = useState(miniAppIcons?.disabled || [])
const [visibleMiniApps, setVisibleMiniApps] = useState(minapps)
const [disabledMiniApps, setDisabledMiniApps] = useState(disabled || [])
// 使用useCallback优化回调函数
const handleWindowStyleChange = useCallback(
@ -59,16 +59,11 @@ const DisplaySettings: FC = () => {
}, [dispatch])
const handleResetMinApps = useCallback(() => {
setVisibleMiniApps(DEFAULT_MINIAPP_ICONS)
setVisibleMiniApps(getAllMinApps())
setDisabledMiniApps([])
dispatch(
setMiniAppIcons({
visible: DEFAULT_MINIAPP_ICONS,
disabled: [],
pinned: miniAppIcons?.pinned || []
})
)
}, [dispatch, miniAppIcons?.pinned])
updateMinapps(getAllMinApps())
updateDisabledMinapps([])
}, [updateDisabledMinapps, updateMinapps])
return (
<SettingContainer theme={themeMode}>

View File

@ -8,39 +8,33 @@ import {
DropResult
} from '@hello-pangea/dnd'
import { getAllMinApps } from '@renderer/config/minapps'
import { useSettings } from '@renderer/hooks/useSettings'
import { useAppDispatch } from '@renderer/store'
import { MinAppIcon, setMiniAppIcons } from '@renderer/store/settings'
import { useMinapps } from '@renderer/hooks/useMinapps'
import { MinAppType } from '@renderer/types'
import { FC, useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
interface MiniAppManagerProps {
visibleMiniApps: MinAppIcon[]
disabledMiniApps: MinAppIcon[]
setVisibleMiniApps: (programs: MinAppIcon[]) => void
setDisabledMiniApps: (programs: MinAppIcon[]) => void
visibleMiniApps: MinAppType[]
disabledMiniApps: MinAppType[]
setVisibleMiniApps: (programs: MinAppType[]) => void
setDisabledMiniApps: (programs: MinAppType[]) => void
}
// 将可复用的类型和常量提取出来
type ListType = 'visible' | 'disabled'
interface AppInfo {
name: string
logo?: string
}
// 添加 reorderLists 函数的接口定义
interface ReorderListsParams {
sourceList: MinAppIcon[]
destList: MinAppIcon[]
sourceList: MinAppType[]
destList: MinAppType[]
sourceIndex: number
destIndex: number
isSameList: boolean
}
interface ReorderListsResult {
sourceList: MinAppIcon[]
destList: MinAppIcon[]
sourceList: MinAppType[]
destList: MinAppType[]
}
// 添加 reorderLists 函数
@ -80,43 +74,18 @@ const MiniAppIconsManager: FC<MiniAppManagerProps> = ({
setDisabledMiniApps
}) => {
const { t } = useTranslation()
const dispatch = useAppDispatch()
const { miniAppIcons } = useSettings()
const allApps = useMemo(() => getAllMinApps(), [])
// 创建 app 信息的 Map 缓存
const appInfoMap = useMemo(() => {
return allApps.reduce(
(acc, app) => {
acc[String(app.id)] = { name: app.name, logo: app.logo }
return acc
},
{} as Record<string, AppInfo>
)
}, [allApps])
const getAppInfo = useCallback(
(id: MinAppIcon) => {
return appInfoMap[String(id)] || { name: id, logo: '' }
},
[appInfoMap]
)
const { pinned, updateMinapps, updateDisabledMinapps, updatePinnedMinapps } = useMinapps()
const handleListUpdate = useCallback(
(newVisible: MinAppIcon[], newDisabled: MinAppIcon[]) => {
(newVisible: MinAppType[], newDisabled: MinAppType[]) => {
setVisibleMiniApps(newVisible)
setDisabledMiniApps(newDisabled)
// 保持 pinned 状态不变
dispatch(
setMiniAppIcons({
visible: newVisible,
disabled: newDisabled,
pinned: miniAppIcons.pinned // 保持原有的 pinned 状态
})
)
updateMinapps(newVisible)
updateDisabledMinapps(newDisabled)
updatePinnedMinapps(pinned.filter((p) => !newDisabled.some((d) => d.id === p.id)))
},
[dispatch, setVisibleMiniApps, setDisabledMiniApps, miniAppIcons.pinned]
[pinned, setDisabledMiniApps, setVisibleMiniApps, updateDisabledMinapps, updateMinapps, updatePinnedMinapps]
)
const onDragEnd = useCallback(
@ -127,15 +96,7 @@ const MiniAppIconsManager: FC<MiniAppManagerProps> = ({
const sourceList = source.droppableId as ListType
const destList = destination.droppableId as ListType
// 如果是 pinned 的小程序,不允许拖到 disabled
if (destList === 'disabled') {
const draggedApp = sourceList === 'visible' ? visibleMiniApps[source.index] : disabledMiniApps[source.index]
if (miniAppIcons.pinned.includes(draggedApp)) {
window.message.error(t('settings.display.minApp.pinnedError'))
return
}
}
if (source.droppableId === destination.droppableId) return
const newLists = reorderLists({
sourceList: sourceList === 'visible' ? visibleMiniApps : disabledMiniApps,
@ -150,32 +111,26 @@ const MiniAppIconsManager: FC<MiniAppManagerProps> = ({
sourceList === 'visible' ? newLists.destList : newLists.sourceList
)
},
[visibleMiniApps, disabledMiniApps, handleListUpdate, miniAppIcons.pinned, t]
[disabledMiniApps, handleListUpdate, visibleMiniApps]
)
const onMoveMiniApp = useCallback(
(program: MinAppIcon, fromList: ListType) => {
// 如果是从可见列表移动到隐藏列表,且程序是 pinned 状态,则阻止移动
if (fromList === 'visible' && miniAppIcons.pinned.includes(program)) {
window.message.error(t('settings.display.minApp.pinnedError'))
return
}
(program: MinAppType, fromList: ListType) => {
const isMovingToVisible = fromList === 'disabled'
const newVisible = isMovingToVisible
? [...visibleMiniApps, program]
: visibleMiniApps.filter((p) => p !== program)
: visibleMiniApps.filter((p) => p.id !== program.id)
const newDisabled = isMovingToVisible
? disabledMiniApps.filter((p) => p !== program)
? disabledMiniApps.filter((p) => p.id !== program.id)
: [...disabledMiniApps, program]
handleListUpdate(newVisible, newDisabled)
},
[visibleMiniApps, disabledMiniApps, handleListUpdate, miniAppIcons.pinned, t]
[visibleMiniApps, disabledMiniApps, handleListUpdate]
)
const renderProgramItem = (program: MinAppIcon, provided: DraggableProvided, listType: ListType) => {
const { name, logo } = getAppInfo(program)
const renderProgramItem = (program: MinAppType, provided: DraggableProvided, listType: ListType) => {
const { name, logo } = allApps.find((app) => app.id === program.id) || { name: program.name, logo: '' }
return (
<ProgramItem ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>
@ -201,7 +156,7 @@ const MiniAppIconsManager: FC<MiniAppManagerProps> = ({
<ProgramList ref={provided.innerRef} {...provided.droppableProps}>
<ScrollContainer>
{(listType === 'visible' ? visibleMiniApps : disabledMiniApps).map((program, index) => (
<Draggable key={program} draggableId={String(program)} index={index}>
<Draggable key={program.id} draggableId={String(program.id)} index={index}>
{(provided: DraggableProvided) => renderProgramItem(program, provided, listType)}
</Draggable>
))}
@ -230,6 +185,7 @@ const AppLogo = styled.img`
const ScrollContainer = styled.div`
overflow-y: auto;
height: 100%;
padding-right: 5px;
`
const ProgramSection = styled.div`
@ -253,6 +209,7 @@ const ProgramList = styled.div`
height: 365px;
min-height: 365px;
padding: 10px;
padding-right: 5px;
background: var(--color-background-soft);
border-radius: 8px;
border: 1px solid var(--color-border);

View File

@ -8,6 +8,7 @@ import assistants from './assistants'
import knowledge from './knowledge'
import llm from './llm'
import migrate from './migrate'
import minapps from './minapps'
import paintings from './paintings'
import runtime from './runtime'
import settings from './settings'
@ -21,7 +22,8 @@ const rootReducer = combineReducers({
settings,
runtime,
shortcuts,
knowledge
knowledge,
minapps
})
const persistedReducer = persistReducer(

View File

@ -9,7 +9,6 @@ import { isEmpty } from 'lodash'
import { createMigrate } from 'redux-persist'
import { RootState } from '.'
import { DEFAULT_MINIAPP_ICONS, DEFAULT_SIDEBAR_ICONS } from './settings'
const migrateConfig = {
'2': (state: RootState) => {
@ -785,15 +784,6 @@ const migrateConfig = {
system: false
})
}
state.settings.sidebarIcons = {
visible: DEFAULT_SIDEBAR_ICONS,
disabled: []
}
state.settings.miniAppIcons = {
visible: DEFAULT_MINIAPP_ICONS,
disabled: [],
pinned: []
}
return state
}
}

View File

@ -0,0 +1,48 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import { getAllMinApps } from '@renderer/config/minapps'
import { MinAppType, SidebarIcon } from '@renderer/types'
export const DEFAULT_SIDEBAR_ICONS: SidebarIcon[] = [
'assistants',
'agents',
'paintings',
'translate',
'minapp',
'knowledge',
'files'
]
export interface MinAppsState {
enabled: MinAppType[]
disabled: MinAppType[]
pinned: MinAppType[]
}
const initialState: MinAppsState = {
enabled: getAllMinApps(),
disabled: [],
pinned: []
}
const minAppsSlice = createSlice({
name: 'minApps',
initialState,
reducers: {
setMinApps: (state, action: PayloadAction<MinAppType[]>) => {
state.enabled = action.payload
},
addMinApp: (state, action: PayloadAction<MinAppType>) => {
state.enabled.push(action.payload)
},
setDisabledMinApps: (state, action: PayloadAction<MinAppType[]>) => {
state.disabled = action.payload
},
setPinnedMinApps: (state, action: PayloadAction<MinAppType[]>) => {
state.pinned = action.payload
}
}
})
export const { setMinApps, addMinApp, setDisabledMinApps, setPinnedMinApps } = minAppsSlice.actions
export default minAppsSlice.reducer

View File

@ -1,5 +1,4 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import { getAllMinApps } from '@renderer/config/minapps'
import { TRANSLATE_PROMPT } from '@renderer/config/prompts'
import { CodeStyleVarious, LanguageVarious, ThemeMode } from '@renderer/types'
@ -16,11 +15,6 @@ export const DEFAULT_SIDEBAR_ICONS: SidebarIcon[] = [
'knowledge',
'files'
]
const [minApps] = await Promise.all([getAllMinApps()])
export type MinAppIcon = (typeof minApps)[number]['id'] // 假设每个小程序对象有 type 字段
export const DEFAULT_MINIAPP_ICONS: MinAppIcon[] = minApps.map((app) => app.id)
export interface SettingsState {
showAssistants: boolean
@ -67,11 +61,6 @@ export interface SettingsState {
disabled: SidebarIcon[]
}
narrowMode: boolean
miniAppIcons: {
visible: MinAppIcon[]
disabled: MinAppIcon[]
pinned: MinAppIcon[]
}
}
const initialState: SettingsState = {
@ -116,12 +105,7 @@ const initialState: SettingsState = {
visible: DEFAULT_SIDEBAR_ICONS,
disabled: []
},
narrowMode: false,
miniAppIcons: {
visible: DEFAULT_MINIAPP_ICONS,
disabled: [],
pinned: []
}
narrowMode: false
}
const settingsSlice = createSlice({
@ -246,17 +230,16 @@ const settingsSlice = createSlice({
setTopicNamingPrompt: (state, action: PayloadAction<string>) => {
state.topicNamingPrompt = action.payload
},
setSidebarIcons: (state, action: PayloadAction<{ visible: SidebarIcon[]; disabled: SidebarIcon[] }>) => {
state.sidebarIcons = action.payload
setSidebarIcons: (state, action: PayloadAction<{ visible?: SidebarIcon[]; disabled?: SidebarIcon[] }>) => {
if (action.payload.visible) {
state.sidebarIcons.visible = action.payload.visible
}
if (action.payload.disabled) {
state.sidebarIcons.disabled = action.payload.disabled
}
},
setNarrowMode: (state, action: PayloadAction<boolean>) => {
state.narrowMode = action.payload
},
setMiniAppIcons: (
state,
action: PayloadAction<{ visible: MinAppIcon[]; disabled: MinAppIcon[]; pinned: MinAppIcon[] }>
) => {
state.miniAppIcons = action.payload
}
}
})
@ -302,8 +285,7 @@ export const {
setCustomCss,
setTopicNamingPrompt,
setSidebarIcons,
setNarrowMode,
setMiniAppIcons
setNarrowMode
} = settingsSlice.actions
export default settingsSlice.reducer

View File

@ -163,7 +163,9 @@ export enum ThemeMode {
dark = 'dark',
auto = 'auto'
}
export type LanguageVarious = 'zh-CN' | 'zh-TW' | 'en-US' | 'ru-RU' | 'ja-JP'
export type CodeStyleVarious = BuiltinTheme | 'auto'
export type WebDavConfig = {
@ -241,3 +243,5 @@ export type GenerateImageParams = {
signal?: AbortSignal
promptEnhancement?: boolean
}
export type SidebarIcon = 'assistants' | 'agents' | 'paintings' | 'translate' | 'minapp' | 'knowledge' | 'files'