feat: MinApp tabs on sidebar, we can keep MinApps alive and re-open it without loading again.
This commit is contained in:
parent
433d562599
commit
57ba91072d
369
src/renderer/src/components/MinApp/MinappPopupContainer.tsx
Normal file
369
src/renderer/src/components/MinApp/MinappPopupContainer.tsx
Normal file
@ -0,0 +1,369 @@
|
|||||||
|
import {
|
||||||
|
CloseOutlined,
|
||||||
|
CodeOutlined,
|
||||||
|
ExportOutlined,
|
||||||
|
MinusOutlined,
|
||||||
|
PushpinOutlined,
|
||||||
|
ReloadOutlined
|
||||||
|
} from '@ant-design/icons'
|
||||||
|
import { isMac, isWindows } from '@renderer/config/constant'
|
||||||
|
import { DEFAULT_MIN_APPS } from '@renderer/config/minapps'
|
||||||
|
import { useBridge } from '@renderer/hooks/useBridge'
|
||||||
|
import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
|
||||||
|
import { useMinapps } from '@renderer/hooks/useMinapps'
|
||||||
|
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||||
|
import { MinAppType } from '@renderer/types'
|
||||||
|
import { delay } from '@renderer/utils'
|
||||||
|
import { Avatar, Drawer, Tooltip } from 'antd'
|
||||||
|
import { WebviewTag } from 'electron'
|
||||||
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import BeatLoader from 'react-spinners/BeatLoader'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
import WebviewContainer from './WebviewContainer'
|
||||||
|
|
||||||
|
interface AppExtraInfo {
|
||||||
|
canPinned: boolean
|
||||||
|
isPinned: boolean
|
||||||
|
canOpenExternalLink: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
type AppInfo = MinAppType & AppExtraInfo
|
||||||
|
|
||||||
|
/** The main container for MinApp popup */
|
||||||
|
const MinappPopupContainer: React.FC = () => {
|
||||||
|
const { openedKeepAliveMinapps, openedOneOffMinapp, currentMinappId, minappShow } = useRuntime()
|
||||||
|
const { closeMinapp, hideMinappPopup } = useMinappPopup()
|
||||||
|
const { pinned, updatePinnedMinapps } = useMinapps()
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
/** control the drawer open or close */
|
||||||
|
const [isPopupShow, setIsPopupShow] = useState(true)
|
||||||
|
/** whether the current minapp is ready */
|
||||||
|
const [isReady, setIsReady] = useState(false)
|
||||||
|
|
||||||
|
/** store the last minapp id and show status */
|
||||||
|
const lastMinappId = useRef<string | null>(null)
|
||||||
|
const lastMinappShow = useRef<boolean>(false)
|
||||||
|
|
||||||
|
/** store the webview refs, one of the key to make them keepalive */
|
||||||
|
const webviewRefs = useRef<Map<string, WebviewTag | null>>(new Map())
|
||||||
|
/** indicate whether the webview has loaded */
|
||||||
|
const webviewLoadedRefs = useRef<Map<string, boolean>>(new Map())
|
||||||
|
|
||||||
|
const isInDevelopment = process.env.NODE_ENV === 'development'
|
||||||
|
|
||||||
|
useBridge()
|
||||||
|
|
||||||
|
/** set the popup display status */
|
||||||
|
useEffect(() => {
|
||||||
|
if (minappShow) {
|
||||||
|
setIsPopupShow(true)
|
||||||
|
|
||||||
|
if (webviewLoadedRefs.current.get(currentMinappId)) {
|
||||||
|
setIsReady(true)
|
||||||
|
/** the case that open the minapp from sidebar */
|
||||||
|
} else if (lastMinappId.current !== currentMinappId && lastMinappShow.current === minappShow) {
|
||||||
|
setIsReady(false)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setIsPopupShow(false)
|
||||||
|
setIsReady(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
/** renew the last minapp id and show status */
|
||||||
|
lastMinappId.current = currentMinappId
|
||||||
|
lastMinappShow.current = minappShow
|
||||||
|
}
|
||||||
|
}, [minappShow, currentMinappId])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!webviewRefs.current) return
|
||||||
|
|
||||||
|
/** set the webview display status
|
||||||
|
* DO NOT use the state to set the display status,
|
||||||
|
* to AVOID the re-render of the webview container
|
||||||
|
*/
|
||||||
|
webviewRefs.current.forEach((webviewRef, appid) => {
|
||||||
|
if (!webviewRef) return
|
||||||
|
webviewRef.style.display = appid === currentMinappId ? 'inline-flex' : 'none'
|
||||||
|
})
|
||||||
|
|
||||||
|
//delete the extra webviewLoadedRefs
|
||||||
|
webviewLoadedRefs.current.forEach((_, appid) => {
|
||||||
|
if (!webviewRefs.current.has(appid)) {
|
||||||
|
webviewLoadedRefs.current.delete(appid)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, [currentMinappId])
|
||||||
|
|
||||||
|
/** combine the openedKeepAliveMinapps and openedOneOffMinapp */
|
||||||
|
const combinedApps = useMemo(() => {
|
||||||
|
return [...openedKeepAliveMinapps, ...(openedOneOffMinapp ? [openedOneOffMinapp] : [])]
|
||||||
|
}, [openedKeepAliveMinapps, openedOneOffMinapp])
|
||||||
|
|
||||||
|
/** get the extra info of the apps */
|
||||||
|
const appsExtraInfo = useMemo(() => {
|
||||||
|
return combinedApps.reduce(
|
||||||
|
(acc, app) => ({
|
||||||
|
...acc,
|
||||||
|
[app.id]: {
|
||||||
|
canPinned: DEFAULT_MIN_APPS.some((item) => item.id === app.id),
|
||||||
|
isPinned: pinned.some((item) => item.id === app.id),
|
||||||
|
canOpenExternalLink: app.url.startsWith('http://') || app.url.startsWith('https://')
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
{} as Record<string, AppExtraInfo>
|
||||||
|
)
|
||||||
|
}, [combinedApps, pinned])
|
||||||
|
|
||||||
|
/** get the current app info with extra info */
|
||||||
|
let currentAppInfo: AppInfo | null = null
|
||||||
|
if (currentMinappId) {
|
||||||
|
const currentApp = combinedApps.find((item) => item.id === currentMinappId) as MinAppType
|
||||||
|
currentAppInfo = { ...currentApp, ...appsExtraInfo[currentApp.id] }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** will close the popup and delete the webview */
|
||||||
|
const handlePopupClose = async (appid: string) => {
|
||||||
|
setIsPopupShow(false)
|
||||||
|
await delay(0.3)
|
||||||
|
webviewLoadedRefs.current.delete(appid)
|
||||||
|
closeMinapp(appid)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** will hide the popup and remain the webviews */
|
||||||
|
const handlePopupMinimize = async () => {
|
||||||
|
setIsPopupShow(false)
|
||||||
|
await delay(0.3)
|
||||||
|
hideMinappPopup()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** the callback function to set the webviews ref */
|
||||||
|
const handleWebviewSetRef = (appid: string, element: WebviewTag | null) => {
|
||||||
|
webviewRefs.current.set(appid, element)
|
||||||
|
|
||||||
|
if (!webviewRefs.current.has(appid)) {
|
||||||
|
webviewRefs.current.set(appid, null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (element) {
|
||||||
|
webviewRefs.current.set(appid, element)
|
||||||
|
} else {
|
||||||
|
webviewRefs.current.delete(appid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** the callback function to set the webviews loaded indicator */
|
||||||
|
const handleWebviewLoaded = (appid: string) => {
|
||||||
|
webviewLoadedRefs.current.set(appid, true)
|
||||||
|
if (appid == currentMinappId) {
|
||||||
|
setTimeout(() => setIsReady(true), 200)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** will open the devtools of the minapp */
|
||||||
|
const handleOpenDevTools = (appid: string) => {
|
||||||
|
const webview = webviewRefs.current.get(appid)
|
||||||
|
if (webview) {
|
||||||
|
webview.openDevTools()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** only reload the original url */
|
||||||
|
const handleReload = (appid: string) => {
|
||||||
|
const webview = webviewRefs.current.get(appid)
|
||||||
|
if (webview) {
|
||||||
|
const url = combinedApps.find((item) => item.id === appid)?.url
|
||||||
|
if (url) {
|
||||||
|
webview.src = url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** only open the current url */
|
||||||
|
const handleOpenLink = (appid: string) => {
|
||||||
|
const webview = webviewRefs.current.get(appid)
|
||||||
|
if (webview) {
|
||||||
|
window.api.openWebsite(webview.getURL())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** toggle the pin status of the minapp */
|
||||||
|
const handleTogglePin = (appid: string) => {
|
||||||
|
const app = combinedApps.find((item) => item.id === appid)
|
||||||
|
if (!app) return
|
||||||
|
|
||||||
|
const newPinned = appsExtraInfo[appid].isPinned ? pinned.filter((item) => item.id !== appid) : [...pinned, app]
|
||||||
|
updatePinnedMinapps(newPinned)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Title bar of the popup */
|
||||||
|
const Title = ({ appInfo }: { appInfo: AppInfo | null }) => {
|
||||||
|
if (!appInfo) return null
|
||||||
|
return (
|
||||||
|
<TitleContainer style={{ justifyContent: 'space-between' }}>
|
||||||
|
<TitleText>{appInfo.name}</TitleText>
|
||||||
|
<ButtonsGroup className={isWindows ? 'windows' : ''}>
|
||||||
|
<Tooltip title={t('minapp.popup.refresh')} mouseEnterDelay={0.8} placement="bottom">
|
||||||
|
<Button onClick={() => handleReload(appInfo.id)}>
|
||||||
|
<ReloadOutlined />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
{appInfo.canPinned && (
|
||||||
|
<Tooltip
|
||||||
|
title={appInfo.isPinned ? t('minapp.sidebar.remove.title') : t('minapp.sidebar.add.title')}
|
||||||
|
mouseEnterDelay={0.8}
|
||||||
|
placement="bottom">
|
||||||
|
<Button onClick={() => handleTogglePin(appInfo.id)} className={appInfo.isPinned ? 'pinned' : ''}>
|
||||||
|
<PushpinOutlined style={{ fontSize: 16 }} />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
{appInfo.canOpenExternalLink && (
|
||||||
|
<Tooltip title={t('minapp.popup.openExternal')} mouseEnterDelay={0.8} placement="bottom">
|
||||||
|
<Button onClick={() => handleOpenLink(appInfo.id)}>
|
||||||
|
<ExportOutlined />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
{isInDevelopment && (
|
||||||
|
<Tooltip title={t('minapp.popup.devtools')} mouseEnterDelay={0.8} placement="bottom">
|
||||||
|
<Button onClick={() => handleOpenDevTools(appInfo.id)}>
|
||||||
|
<CodeOutlined />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
<Tooltip title={t('minapp.popup.minimize')} mouseEnterDelay={0.8} placement="bottom">
|
||||||
|
<Button onClick={() => handlePopupMinimize()}>
|
||||||
|
<MinusOutlined />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title={t('minapp.popup.close')} mouseEnterDelay={0.8} placement="bottom">
|
||||||
|
<Button onClick={() => handlePopupClose(appInfo.id)}>
|
||||||
|
<CloseOutlined />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</ButtonsGroup>
|
||||||
|
</TitleContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** group the webview containers with Memo, one of the key to make them keepalive */
|
||||||
|
const WebviewContainerGroup = useMemo(() => {
|
||||||
|
return combinedApps.map((app) => (
|
||||||
|
<WebviewContainer
|
||||||
|
key={app.id}
|
||||||
|
appid={app.id}
|
||||||
|
url={app.url}
|
||||||
|
onSetRefCallback={handleWebviewSetRef}
|
||||||
|
onLoadedCallback={handleWebviewLoaded}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
|
||||||
|
// because the combinedApps is enough
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [combinedApps])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Drawer
|
||||||
|
title={<Title appInfo={currentAppInfo} />}
|
||||||
|
placement="bottom"
|
||||||
|
onClose={handlePopupMinimize}
|
||||||
|
open={isPopupShow}
|
||||||
|
destroyOnClose={false}
|
||||||
|
mask={false}
|
||||||
|
rootClassName="minapp-drawer"
|
||||||
|
maskClassName="minapp-mask"
|
||||||
|
height={'100%'}
|
||||||
|
maskClosable={false}
|
||||||
|
closeIcon={null}
|
||||||
|
style={{ marginLeft: 'var(--sidebar-width)' }}>
|
||||||
|
{!isReady && (
|
||||||
|
<EmptyView>
|
||||||
|
<Avatar
|
||||||
|
src={currentAppInfo?.logo}
|
||||||
|
size={80}
|
||||||
|
style={{ border: '1px solid var(--color-border)', marginTop: -150 }}
|
||||||
|
/>
|
||||||
|
<BeatLoader color="var(--color-text-2)" size="10px" style={{ marginTop: 15 }} />
|
||||||
|
</EmptyView>
|
||||||
|
)}
|
||||||
|
{WebviewContainerGroup}
|
||||||
|
</Drawer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const TitleContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
padding-left: ${isMac ? '20px' : '10px'};
|
||||||
|
padding-right: 10px;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: transparent;
|
||||||
|
`
|
||||||
|
|
||||||
|
const TitleText = styled.div`
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--color-text-1);
|
||||||
|
margin-right: 10px;
|
||||||
|
user-select: none;
|
||||||
|
`
|
||||||
|
const ButtonsGroup = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
-webkit-app-region: no-drag;
|
||||||
|
&.windows {
|
||||||
|
margin-right: ${isWindows ? '130px' : 0};
|
||||||
|
background-color: var(--color-background-mute);
|
||||||
|
border-radius: 50px;
|
||||||
|
padding: 0 3px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const Button = styled.div`
|
||||||
|
cursor: pointer;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
border-radius: 5px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
color: var(--color-text-2);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-size: 14px;
|
||||||
|
&:hover {
|
||||||
|
color: var(--color-text-1);
|
||||||
|
background-color: var(--color-background-mute);
|
||||||
|
}
|
||||||
|
&.pinned {
|
||||||
|
color: var(--color-primary);
|
||||||
|
background-color: var(--color-primary-bg);
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const EmptyView = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: var(--color-background);
|
||||||
|
`
|
||||||
|
|
||||||
|
export default MinappPopupContainer
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
import MinappPopupContainer from '@renderer/components/MinApp/MinappPopupContainer'
|
||||||
|
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||||
|
|
||||||
|
const TopViewMinappContainer = () => {
|
||||||
|
const { openedKeepAliveMinapps, openedOneOffMinapp } = useRuntime()
|
||||||
|
const isCreate = openedKeepAliveMinapps.length > 0 || openedOneOffMinapp !== null
|
||||||
|
|
||||||
|
return <>{isCreate && <MinappPopupContainer />}</>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TopViewMinappContainer
|
||||||
84
src/renderer/src/components/MinApp/WebviewContainer.tsx
Normal file
84
src/renderer/src/components/MinApp/WebviewContainer.tsx
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import { WebviewTag } from 'electron'
|
||||||
|
import { memo, useEffect, useRef } from 'react'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WebviewContainer is a component that renders a webview element.
|
||||||
|
* It is used in the MinAppPopupContainer component.
|
||||||
|
* The webcontent can be remain in memory
|
||||||
|
*/
|
||||||
|
const WebviewContainer = memo(
|
||||||
|
({
|
||||||
|
appid,
|
||||||
|
url,
|
||||||
|
onSetRefCallback,
|
||||||
|
onLoadedCallback
|
||||||
|
}: {
|
||||||
|
appid: string
|
||||||
|
url: string
|
||||||
|
onSetRefCallback: (appid: string, element: WebviewTag | null) => void
|
||||||
|
onLoadedCallback: (appid: string) => void
|
||||||
|
}) => {
|
||||||
|
const webviewRef = useRef<WebviewTag | null>(null)
|
||||||
|
|
||||||
|
const setRef = (appid: string) => {
|
||||||
|
onSetRefCallback(appid, null)
|
||||||
|
|
||||||
|
return (element: WebviewTag | null) => {
|
||||||
|
onSetRefCallback(appid, element)
|
||||||
|
if (element) {
|
||||||
|
webviewRef.current = element
|
||||||
|
} else {
|
||||||
|
webviewRef.current = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!webviewRef.current) return
|
||||||
|
|
||||||
|
const handleNewWindow = (event: any) => {
|
||||||
|
event.preventDefault()
|
||||||
|
if (webviewRef.current?.loadURL) {
|
||||||
|
webviewRef.current.loadURL(event.url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLoaded = () => {
|
||||||
|
onLoadedCallback(appid)
|
||||||
|
}
|
||||||
|
|
||||||
|
webviewRef.current.addEventListener('new-window', handleNewWindow)
|
||||||
|
webviewRef.current.addEventListener('did-finish-load', handleLoaded)
|
||||||
|
|
||||||
|
// we set the url when the webview is ready
|
||||||
|
webviewRef.current.src = url
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
webviewRef.current?.removeEventListener('new-window', handleNewWindow)
|
||||||
|
webviewRef.current?.removeEventListener('did-finish-load', handleLoaded)
|
||||||
|
}
|
||||||
|
// because the appid and url are enough, no need to add onLoadedCallback
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [appid, url])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<webview
|
||||||
|
key={appid}
|
||||||
|
ref={setRef(appid)}
|
||||||
|
style={WebviewStyle}
|
||||||
|
allowpopups={'true' as any}
|
||||||
|
partition={`persist:webview-${appid}`}
|
||||||
|
nodeintegration={'true' as any}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const WebviewStyle: React.CSSProperties = {
|
||||||
|
width: 'calc(100vw - var(--sidebar-width))',
|
||||||
|
height: 'calc(100vh - var(--navbar-height))',
|
||||||
|
backgroundColor: 'white',
|
||||||
|
display: 'inline-flex'
|
||||||
|
}
|
||||||
|
|
||||||
|
export default WebviewContainer
|
||||||
@ -1,282 +0,0 @@
|
|||||||
import { CloseOutlined, CodeOutlined, ExportOutlined, PushpinOutlined, ReloadOutlined } from '@ant-design/icons'
|
|
||||||
import { isMac, isWindows } from '@renderer/config/constant'
|
|
||||||
import { AppLogo } from '@renderer/config/env'
|
|
||||||
import { DEFAULT_MIN_APPS } from '@renderer/config/minapps'
|
|
||||||
import { useBridge } from '@renderer/hooks/useBridge'
|
|
||||||
import { useMinapps } from '@renderer/hooks/useMinapps'
|
|
||||||
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'
|
|
||||||
import BeatLoader from 'react-spinners/BeatLoader'
|
|
||||||
import styled from 'styled-components'
|
|
||||||
|
|
||||||
import { TopView } from '../TopView'
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
app: MinAppType
|
|
||||||
resolve: (data: any) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const PopupContainer: React.FC<Props> = ({ app, resolve }) => {
|
|
||||||
const { pinned, updatePinnedMinapps } = useMinapps()
|
|
||||||
const isPinned = pinned.some((p) => p.id === app.id)
|
|
||||||
const [open, setOpen] = useState(true)
|
|
||||||
const [opened, setOpened] = useState(false)
|
|
||||||
const [isReady, setIsReady] = useState(false)
|
|
||||||
const webviewRef = useRef<WebviewTag | null>(null)
|
|
||||||
|
|
||||||
useBridge()
|
|
||||||
|
|
||||||
const canOpenExternalLink = app.url.startsWith('http://') || app.url.startsWith('https://')
|
|
||||||
const canPinned = DEFAULT_MIN_APPS.some((i) => i.id === app?.id)
|
|
||||||
|
|
||||||
const onClose = async (_delay = 0.3) => {
|
|
||||||
setOpen(false)
|
|
||||||
await delay(_delay)
|
|
||||||
resolve({})
|
|
||||||
}
|
|
||||||
|
|
||||||
MinApp.onClose = onClose
|
|
||||||
const openDevTools = () => {
|
|
||||||
if (webviewRef.current) {
|
|
||||||
webviewRef.current.openDevTools()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const onReload = () => {
|
|
||||||
if (webviewRef.current) {
|
|
||||||
webviewRef.current.src = app.url
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const onOpenLink = () => {
|
|
||||||
if (webviewRef.current) {
|
|
||||||
const currentUrl = webviewRef.current.getURL()
|
|
||||||
window.api.openWebsite(currentUrl)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const onTogglePin = () => {
|
|
||||||
const newPinned = isPinned ? pinned.filter((item) => item.id !== app.id) : [...pinned, app]
|
|
||||||
updatePinnedMinapps(newPinned)
|
|
||||||
}
|
|
||||||
|
|
||||||
const isInDevelopment = process.env.NODE_ENV === 'development'
|
|
||||||
|
|
||||||
const Title = () => {
|
|
||||||
return (
|
|
||||||
<TitleContainer style={{ justifyContent: 'space-between' }}>
|
|
||||||
<TitleText>{app.name}</TitleText>
|
|
||||||
<ButtonsGroup className={isWindows ? 'windows' : ''}>
|
|
||||||
<Button onClick={onReload}>
|
|
||||||
<ReloadOutlined />
|
|
||||||
</Button>
|
|
||||||
{canPinned && (
|
|
||||||
<Button onClick={onTogglePin} className={isPinned ? 'pinned' : ''}>
|
|
||||||
<PushpinOutlined style={{ fontSize: 16 }} />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{canOpenExternalLink && (
|
|
||||||
<Button onClick={onOpenLink}>
|
|
||||||
<ExportOutlined />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{isInDevelopment && (
|
|
||||||
<Button onClick={openDevTools}>
|
|
||||||
<CodeOutlined />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<Button onClick={() => onClose()}>
|
|
||||||
<CloseOutlined />
|
|
||||||
</Button>
|
|
||||||
</ButtonsGroup>
|
|
||||||
</TitleContainer>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const webview = webviewRef.current
|
|
||||||
|
|
||||||
if (webview) {
|
|
||||||
const handleNewWindow = (event: any) => {
|
|
||||||
event.preventDefault()
|
|
||||||
if (webview.loadURL) {
|
|
||||||
webview.loadURL(event.url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const onLoaded = () => setIsReady(true)
|
|
||||||
|
|
||||||
webview.addEventListener('new-window', handleNewWindow)
|
|
||||||
webview.addEventListener('did-finish-load', onLoaded)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
webview.removeEventListener('new-window', handleNewWindow)
|
|
||||||
webview.removeEventListener('did-finish-load', onLoaded)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {}
|
|
||||||
}, [opened])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setTimeout(() => setOpened(true), 350)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Drawer
|
|
||||||
title={<Title />}
|
|
||||||
placement="bottom"
|
|
||||||
onClose={() => onClose()}
|
|
||||||
open={open}
|
|
||||||
mask={true}
|
|
||||||
rootClassName="minapp-drawer"
|
|
||||||
maskClassName="minapp-mask"
|
|
||||||
height={'100%'}
|
|
||||||
maskClosable={false}
|
|
||||||
closeIcon={null}
|
|
||||||
style={{ marginLeft: 'var(--sidebar-width)' }}>
|
|
||||||
{!isReady && (
|
|
||||||
<EmptyView>
|
|
||||||
<Avatar src={app.logo} size={80} style={{ border: '1px solid var(--color-border)', marginTop: -150 }} />
|
|
||||||
<BeatLoader color="var(--color-text-2)" size="10" style={{ marginTop: 15 }} />
|
|
||||||
</EmptyView>
|
|
||||||
)}
|
|
||||||
{opened && (
|
|
||||||
<webview
|
|
||||||
src={app.url}
|
|
||||||
ref={webviewRef}
|
|
||||||
style={WebviewStyle}
|
|
||||||
allowpopups={'true' as any}
|
|
||||||
partition="persist:webview"
|
|
||||||
nodeintegration={true}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Drawer>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const WebviewStyle: React.CSSProperties = {
|
|
||||||
width: 'calc(100vw - var(--sidebar-width))',
|
|
||||||
height: 'calc(100vh - var(--navbar-height))',
|
|
||||||
backgroundColor: 'white',
|
|
||||||
display: 'inline-flex'
|
|
||||||
}
|
|
||||||
|
|
||||||
const TitleContainer = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
padding-left: ${isMac ? '20px' : '10px'};
|
|
||||||
padding-right: 10px;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background-color: transparent;
|
|
||||||
`
|
|
||||||
|
|
||||||
const TitleText = styled.div`
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 14px;
|
|
||||||
color: var(--color-text-1);
|
|
||||||
margin-right: 10px;
|
|
||||||
user-select: none;
|
|
||||||
`
|
|
||||||
|
|
||||||
const ButtonsGroup = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
gap: 5px;
|
|
||||||
-webkit-app-region: no-drag;
|
|
||||||
&.windows {
|
|
||||||
margin-right: ${isWindows ? '130px' : 0};
|
|
||||||
background-color: var(--color-background-mute);
|
|
||||||
border-radius: 50px;
|
|
||||||
padding: 0 3px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const Button = styled.div`
|
|
||||||
cursor: pointer;
|
|
||||||
width: 30px;
|
|
||||||
height: 30px;
|
|
||||||
border-radius: 5px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
color: var(--color-text-2);
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
font-size: 14px;
|
|
||||||
&:hover {
|
|
||||||
color: var(--color-text-1);
|
|
||||||
background-color: var(--color-background-mute);
|
|
||||||
}
|
|
||||||
&.pinned {
|
|
||||||
color: var(--color-primary);
|
|
||||||
background-color: var(--color-primary-bg);
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const EmptyView = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex: 1;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background-color: var(--color-background);
|
|
||||||
`
|
|
||||||
|
|
||||||
export default class MinApp {
|
|
||||||
static topviewId = 0
|
|
||||||
static onClose = () => {}
|
|
||||||
static app: MinAppType | null = null
|
|
||||||
|
|
||||||
static async start(app: MinAppType) {
|
|
||||||
if (app?.id && MinApp.app?.id === app?.id) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (MinApp.app) {
|
|
||||||
// @ts-ignore delay params
|
|
||||||
await MinApp.onClose(0)
|
|
||||||
await delay(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!app.logo) {
|
|
||||||
app.logo = AppLogo
|
|
||||||
}
|
|
||||||
|
|
||||||
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() {
|
|
||||||
TopView.hide('MinApp')
|
|
||||||
store.dispatch(setMinappShow(false))
|
|
||||||
MinApp.app = null
|
|
||||||
MinApp.onClose = () => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
import TopViewMinappContainer from '@renderer/components/MinApp/TopViewMinappContainer'
|
||||||
import { useAppInit } from '@renderer/hooks/useAppInit'
|
import { useAppInit } from '@renderer/hooks/useAppInit'
|
||||||
import { message, Modal } from 'antd'
|
import { message, Modal } from 'antd'
|
||||||
import React, { PropsWithChildren, useCallback, useEffect, useRef, useState } from 'react'
|
import React, { PropsWithChildren, useCallback, useEffect, useRef, useState } from 'react'
|
||||||
@ -76,6 +77,7 @@ const TopViewContainer: React.FC<Props> = ({ children }) => {
|
|||||||
{children}
|
{children}
|
||||||
{messageContextHolder}
|
{messageContextHolder}
|
||||||
{modalContextHolder}
|
{modalContextHolder}
|
||||||
|
<TopViewMinappContainer />
|
||||||
{elements.map(({ element: Element, id }) => (
|
{elements.map(({ element: Element, id }) => (
|
||||||
<FullScreenContainer key={`TOPVIEW_${id}`}>
|
<FullScreenContainer key={`TOPVIEW_${id}`}>
|
||||||
{typeof Element === 'function' ? <Element /> : Element}
|
{typeof Element === 'function' ? <Element /> : Element}
|
||||||
|
|||||||
@ -9,35 +9,36 @@ import { isMac } from '@renderer/config/constant'
|
|||||||
import { AppLogo, UserAvatar } from '@renderer/config/env'
|
import { AppLogo, UserAvatar } from '@renderer/config/env'
|
||||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||||
import useAvatar from '@renderer/hooks/useAvatar'
|
import useAvatar from '@renderer/hooks/useAvatar'
|
||||||
|
import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
|
||||||
import { useMinapps } from '@renderer/hooks/useMinapps'
|
import { useMinapps } from '@renderer/hooks/useMinapps'
|
||||||
import useNavBackgroundColor from '@renderer/hooks/useNavBackgroundColor'
|
import useNavBackgroundColor from '@renderer/hooks/useNavBackgroundColor'
|
||||||
import { modelGenerating, useRuntime } from '@renderer/hooks/useRuntime'
|
import { modelGenerating, useRuntime } from '@renderer/hooks/useRuntime'
|
||||||
import { useSettings } from '@renderer/hooks/useSettings'
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
import { isEmoji } from '@renderer/utils'
|
import { isEmoji } from '@renderer/utils'
|
||||||
import type { MenuProps } from 'antd'
|
import type { MenuProps } from 'antd'
|
||||||
import { Tooltip } from 'antd'
|
import { Avatar, Dropdown, Tooltip } from 'antd'
|
||||||
import { Avatar } from 'antd'
|
import { FC, useEffect } from 'react'
|
||||||
import { Dropdown } from 'antd'
|
|
||||||
import { FC } from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useLocation, useNavigate } from 'react-router-dom'
|
import { useLocation, useNavigate } from 'react-router-dom'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
import DragableList from '../DragableList'
|
import DragableList from '../DragableList'
|
||||||
import MinAppIcon from '../Icons/MinAppIcon'
|
import MinAppIcon from '../Icons/MinAppIcon'
|
||||||
import MinApp from '../MinApp'
|
|
||||||
import UserPopup from '../Popups/UserPopup'
|
import UserPopup from '../Popups/UserPopup'
|
||||||
|
|
||||||
const Sidebar: FC = () => {
|
const Sidebar: FC = () => {
|
||||||
const { pathname } = useLocation()
|
const { hideMinappPopup, openMinapp } = useMinappPopup()
|
||||||
const avatar = useAvatar()
|
const { minappShow, currentMinappId } = useRuntime()
|
||||||
const { minappShow } = useRuntime()
|
|
||||||
const { t } = useTranslation()
|
|
||||||
const navigate = useNavigate()
|
|
||||||
const { sidebarIcons } = useSettings()
|
const { sidebarIcons } = useSettings()
|
||||||
const { theme, settingTheme, toggleTheme } = useTheme()
|
|
||||||
const { pinned } = useMinapps()
|
const { pinned } = useMinapps()
|
||||||
|
|
||||||
|
const { pathname } = useLocation()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
const { theme, settingTheme, toggleTheme } = useTheme()
|
||||||
|
const avatar = useAvatar()
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const onEditUser = () => UserPopup.show()
|
const onEditUser = () => UserPopup.show()
|
||||||
|
|
||||||
const backgroundColor = useNavBackgroundColor()
|
const backgroundColor = useNavBackgroundColor()
|
||||||
@ -49,9 +50,10 @@ const Sidebar: FC = () => {
|
|||||||
navigate(path)
|
navigate(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const docsId = 'cherrystudio-docs'
|
||||||
const onOpenDocs = () => {
|
const onOpenDocs = () => {
|
||||||
MinApp.start({
|
openMinapp({
|
||||||
id: 'docs',
|
id: docsId,
|
||||||
name: t('docs.title'),
|
name: t('docs.title'),
|
||||||
url: 'https://docs.cherry-ai.com/',
|
url: 'https://docs.cherry-ai.com/',
|
||||||
logo: AppLogo
|
logo: AppLogo
|
||||||
@ -66,9 +68,10 @@ const Sidebar: FC = () => {
|
|||||||
<AvatarImg src={avatar || UserAvatar} draggable={false} className="nodrag" onClick={onEditUser} />
|
<AvatarImg src={avatar || UserAvatar} draggable={false} className="nodrag" onClick={onEditUser} />
|
||||||
)}
|
)}
|
||||||
<MainMenusContainer>
|
<MainMenusContainer>
|
||||||
<Menus onClick={MinApp.onClose}>
|
<Menus onClick={hideMinappPopup}>
|
||||||
<MainMenus />
|
<MainMenus />
|
||||||
</Menus>
|
</Menus>
|
||||||
|
<SidebarOpenedMinappTabs />
|
||||||
{showPinnedApps && (
|
{showPinnedApps && (
|
||||||
<AppsContainer>
|
<AppsContainer>
|
||||||
<Divider />
|
<Divider />
|
||||||
@ -80,10 +83,7 @@ const Sidebar: FC = () => {
|
|||||||
</MainMenusContainer>
|
</MainMenusContainer>
|
||||||
<Menus>
|
<Menus>
|
||||||
<Tooltip title={t('docs.title')} mouseEnterDelay={0.8} placement="right">
|
<Tooltip title={t('docs.title')} mouseEnterDelay={0.8} placement="right">
|
||||||
<Icon
|
<Icon theme={theme} onClick={onOpenDocs} className={minappShow && currentMinappId === docsId ? 'active' : ''}>
|
||||||
theme={theme}
|
|
||||||
onClick={onOpenDocs}
|
|
||||||
className={minappShow && MinApp.app?.url === 'https://docs.cherry-ai.com/' ? 'active' : ''}>
|
|
||||||
<QuestionCircleOutlined />
|
<QuestionCircleOutlined />
|
||||||
</Icon>
|
</Icon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@ -102,7 +102,7 @@ const Sidebar: FC = () => {
|
|||||||
<Tooltip title={t('settings.title')} mouseEnterDelay={0.8} placement="right">
|
<Tooltip title={t('settings.title')} mouseEnterDelay={0.8} placement="right">
|
||||||
<StyledLink
|
<StyledLink
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
minappShow && (await MinApp.close())
|
hideMinappPopup()
|
||||||
await modelGenerating()
|
await modelGenerating()
|
||||||
await to('/settings/provider')
|
await to('/settings/provider')
|
||||||
}}>
|
}}>
|
||||||
@ -117,6 +117,7 @@ const Sidebar: FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const MainMenus: FC = () => {
|
const MainMenus: FC = () => {
|
||||||
|
const { hideMinappPopup } = useMinappPopup()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { pathname } = useLocation()
|
const { pathname } = useLocation()
|
||||||
const { sidebarIcons } = useSettings()
|
const { sidebarIcons } = useSettings()
|
||||||
@ -155,7 +156,7 @@ const MainMenus: FC = () => {
|
|||||||
<Tooltip key={icon} title={t(`${icon}.title`)} mouseEnterDelay={0.8} placement="right">
|
<Tooltip key={icon} title={t(`${icon}.title`)} mouseEnterDelay={0.8} placement="right">
|
||||||
<StyledLink
|
<StyledLink
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
minappShow && (await MinApp.close())
|
hideMinappPopup()
|
||||||
await modelGenerating()
|
await modelGenerating()
|
||||||
navigate(path)
|
navigate(path)
|
||||||
}}>
|
}}>
|
||||||
@ -168,11 +169,99 @@ const MainMenus: FC = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Tabs of opened minapps in sidebar */
|
||||||
|
const SidebarOpenedMinappTabs: FC = () => {
|
||||||
|
const { minappShow, openedKeepAliveMinapps, currentMinappId } = useRuntime()
|
||||||
|
const { openMinappKeepAlive, hideMinappPopup, closeMinapp, closeAllMinapps } = useMinappPopup()
|
||||||
|
const { theme } = useTheme()
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const handleOnClick = (app) => {
|
||||||
|
if (minappShow && currentMinappId === app.id) {
|
||||||
|
hideMinappPopup()
|
||||||
|
} else {
|
||||||
|
openMinappKeepAlive(app)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// animation for minapp switch indicator
|
||||||
|
useEffect(() => {
|
||||||
|
//hacky way to get the height of the icon
|
||||||
|
const iconDefaultHeight = 40
|
||||||
|
const iconDefaultOffset = 17
|
||||||
|
const container = document.querySelector('.TabsContainer') as HTMLElement
|
||||||
|
const activeIcon = document.querySelector('.TabsContainer .opened-active') as HTMLElement
|
||||||
|
|
||||||
|
let indicatorTop = 0,
|
||||||
|
indicatorRight = 0
|
||||||
|
if (minappShow && activeIcon && container) {
|
||||||
|
indicatorTop = activeIcon.offsetTop + activeIcon.offsetHeight / 2 - 4 // 4 is half of the indicator's height (8px)
|
||||||
|
indicatorRight = 0
|
||||||
|
} else {
|
||||||
|
indicatorTop =
|
||||||
|
((openedKeepAliveMinapps.length > 0 ? openedKeepAliveMinapps.length : 1) / 2) * iconDefaultHeight +
|
||||||
|
iconDefaultOffset -
|
||||||
|
4
|
||||||
|
indicatorRight = -50
|
||||||
|
}
|
||||||
|
container.style.setProperty('--indicator-top', `${indicatorTop}px`)
|
||||||
|
container.style.setProperty('--indicator-right', `${indicatorRight}px`)
|
||||||
|
}, [currentMinappId, openedKeepAliveMinapps, minappShow])
|
||||||
|
|
||||||
|
const isShowOpened = openedKeepAliveMinapps.length > 0
|
||||||
|
if (!isShowOpened) return <TabsContainer className="TabsContainer" />
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TabsContainer className="TabsContainer">
|
||||||
|
<Divider />
|
||||||
|
<TabsWrapper>
|
||||||
|
<Menus>
|
||||||
|
{openedKeepAliveMinapps.map((app) => {
|
||||||
|
const menuItems: MenuProps['items'] = [
|
||||||
|
{
|
||||||
|
key: 'closeApp',
|
||||||
|
label: t('minapp.sidebar.close.title'),
|
||||||
|
onClick: () => {
|
||||||
|
closeMinapp(app.id)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'closeAllApp',
|
||||||
|
label: t('minapp.sidebar.closeall.title'),
|
||||||
|
onClick: () => {
|
||||||
|
closeAllMinapps()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
const isActive = minappShow && currentMinappId === app.id
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip key={app.id} title={app.name} mouseEnterDelay={0.8} placement="right">
|
||||||
|
<StyledLink>
|
||||||
|
<Dropdown menu={{ items: menuItems }} trigger={['contextMenu']} overlayStyle={{ zIndex: 10000 }}>
|
||||||
|
<Icon
|
||||||
|
theme={theme}
|
||||||
|
onClick={() => handleOnClick(app)}
|
||||||
|
className={`${isActive ? 'opened-active' : ''}`}>
|
||||||
|
<MinAppIcon size={20} app={app} style={{ borderRadius: 6 }} />
|
||||||
|
</Icon>
|
||||||
|
</Dropdown>
|
||||||
|
</StyledLink>
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</Menus>
|
||||||
|
</TabsWrapper>
|
||||||
|
</TabsContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const PinnedApps: FC = () => {
|
const PinnedApps: FC = () => {
|
||||||
const { pinned, updatePinnedMinapps } = useMinapps()
|
const { pinned, updatePinnedMinapps } = useMinapps()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { minappShow } = useRuntime()
|
const { minappShow, openedKeepAliveMinapps, currentMinappId } = useRuntime()
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
|
const { openMinappKeepAlive } = useMinappPopup()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DragableList list={pinned} onUpdate={updatePinnedMinapps} listStyle={{ marginBottom: 5 }}>
|
<DragableList list={pinned} onUpdate={updatePinnedMinapps} listStyle={{ marginBottom: 5 }}>
|
||||||
@ -187,12 +276,15 @@ const PinnedApps: FC = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
const isActive = minappShow && MinApp.app?.id === app.id
|
const isActive = minappShow && currentMinappId === app.id
|
||||||
return (
|
return (
|
||||||
<Tooltip key={app.id} title={app.name} mouseEnterDelay={0.8} placement="right">
|
<Tooltip key={app.id} title={app.name} mouseEnterDelay={0.8} placement="right">
|
||||||
<StyledLink>
|
<StyledLink>
|
||||||
<Dropdown menu={{ items: menuItems }} trigger={['contextMenu']}>
|
<Dropdown menu={{ items: menuItems }} trigger={['contextMenu']} overlayStyle={{ zIndex: 10000 }}>
|
||||||
<Icon theme={theme} onClick={() => MinApp.start(app)} className={isActive ? 'active' : ''}>
|
<Icon
|
||||||
|
theme={theme}
|
||||||
|
onClick={() => openMinappKeepAlive(app)}
|
||||||
|
className={`${isActive ? 'active' : ''} ${openedKeepAliveMinapps.some((item) => item.id === app.id) ? 'opened-animation' : ''}`}>
|
||||||
<MinAppIcon size={20} app={app} style={{ borderRadius: 6 }} />
|
<MinAppIcon size={20} app={app} style={{ borderRadius: 6 }} />
|
||||||
</Icon>
|
</Icon>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
@ -293,6 +385,23 @@ const Icon = styled.div<{ theme: string }>`
|
|||||||
color: var(--color-icon-white);
|
color: var(--color-icon-white);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes borderBreath {
|
||||||
|
0% {
|
||||||
|
border-color: var(--color-primary-mute);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
border-color: var(--color-primary-mute);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.opened-animation {
|
||||||
|
border: 0.5px solid var(--color-primary);
|
||||||
|
animation: borderBreath 4s ease-in-out infinite;
|
||||||
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const StyledLink = styled.div`
|
const StyledLink = styled.div`
|
||||||
@ -323,4 +432,37 @@ const Divider = styled.div`
|
|||||||
border-bottom: 0.5px solid var(--color-border);
|
border-bottom: 0.5px solid var(--color-border);
|
||||||
`
|
`
|
||||||
|
|
||||||
|
const TabsContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
-webkit-app-region: none;
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
right: var(--indicator-right, 0);
|
||||||
|
top: var(--indicator-top, 0);
|
||||||
|
width: 4px;
|
||||||
|
height: 8px;
|
||||||
|
background-color: var(--color-primary);
|
||||||
|
transition:
|
||||||
|
top 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
||||||
|
right 0.3s ease-in-out;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const TabsWrapper = styled.div`
|
||||||
|
background-color: rgba(128, 128, 128, 0.1);
|
||||||
|
border-radius: 20px;
|
||||||
|
overflow: hidden;
|
||||||
|
`
|
||||||
|
|
||||||
export default Sidebar
|
export default Sidebar
|
||||||
|
|||||||
@ -49,9 +49,7 @@ import DeepSeekProviderLogo from '@renderer/assets/images/providers/deepseek.png
|
|||||||
import GroqProviderLogo from '@renderer/assets/images/providers/groq.png?url'
|
import GroqProviderLogo from '@renderer/assets/images/providers/groq.png?url'
|
||||||
import OpenAiProviderLogo from '@renderer/assets/images/providers/openai.png?url'
|
import OpenAiProviderLogo from '@renderer/assets/images/providers/openai.png?url'
|
||||||
import SiliconFlowProviderLogo from '@renderer/assets/images/providers/silicon.png?url'
|
import SiliconFlowProviderLogo from '@renderer/assets/images/providers/silicon.png?url'
|
||||||
import MinApp from '@renderer/components/MinApp'
|
|
||||||
import { MinAppType } from '@renderer/types'
|
import { MinAppType } from '@renderer/types'
|
||||||
|
|
||||||
export const DEFAULT_MIN_APPS: MinAppType[] = [
|
export const DEFAULT_MIN_APPS: MinAppType[] = [
|
||||||
{
|
{
|
||||||
id: 'openai',
|
id: 'openai',
|
||||||
@ -395,8 +393,3 @@ export const DEFAULT_MIN_APPS: MinAppType[] = [
|
|||||||
bodered: true
|
bodered: true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
export function startMinAppById(id: string) {
|
|
||||||
const app = DEFAULT_MIN_APPS.find((app) => app?.id === id)
|
|
||||||
app && MinApp.start(app)
|
|
||||||
}
|
|
||||||
|
|||||||
120
src/renderer/src/hooks/useMinappPopup.ts
Normal file
120
src/renderer/src/hooks/useMinappPopup.ts
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||||
|
import { useAppDispatch } from '@renderer/store'
|
||||||
|
import {
|
||||||
|
setCurrentMinappId,
|
||||||
|
setMinappShow,
|
||||||
|
setOpenedKeepAliveMinapps,
|
||||||
|
setOpenedOneOffMinapp
|
||||||
|
} from '@renderer/store/runtime'
|
||||||
|
import { MinAppType } from '@renderer/types'
|
||||||
|
|
||||||
|
/** The max number of keep alive minapps */
|
||||||
|
const MINAPP_MAX_KEEPALIVE = 3
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Usage:
|
||||||
|
*
|
||||||
|
* To control the minapp popup, you can use the following hooks:
|
||||||
|
* import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
|
||||||
|
*
|
||||||
|
* in the component:
|
||||||
|
* const { openMinapp, openMinappKeepAlive, openMinappById,
|
||||||
|
* closeMinapp, hideMinappPopup, closeAllMinapps } = useMinappPopup()
|
||||||
|
*
|
||||||
|
* To use some key states of the minapp popup:
|
||||||
|
* import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||||
|
* const { openedKeepAliveMinapps, openedOneOffMinapp, minappShow } = useRuntime()
|
||||||
|
*/
|
||||||
|
export const useMinappPopup = () => {
|
||||||
|
const { openedKeepAliveMinapps, openedOneOffMinapp, minappShow } = useRuntime()
|
||||||
|
const dispatch = useAppDispatch()
|
||||||
|
|
||||||
|
/** Open a minapp (popup shows and minapp loaded) */
|
||||||
|
const openMinapp = (app: MinAppType, keepAlive: boolean = false) => {
|
||||||
|
if (keepAlive) {
|
||||||
|
//if the minapp is already opened, do nothing
|
||||||
|
if (openedKeepAliveMinapps.some((item) => item.id === app.id)) {
|
||||||
|
dispatch(setCurrentMinappId(app.id))
|
||||||
|
dispatch(setMinappShow(true))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//if the minapp is not opened, open it
|
||||||
|
//check if the keep alive minapps meet the max limit
|
||||||
|
if (openedKeepAliveMinapps.length < MINAPP_MAX_KEEPALIVE) {
|
||||||
|
//always put new minapp at the first
|
||||||
|
dispatch(setOpenedKeepAliveMinapps([app, ...openedKeepAliveMinapps]))
|
||||||
|
} else {
|
||||||
|
//pop the last one
|
||||||
|
dispatch(setOpenedKeepAliveMinapps([app, ...openedKeepAliveMinapps.slice(0, MINAPP_MAX_KEEPALIVE - 1)]))
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(setOpenedOneOffMinapp(null))
|
||||||
|
dispatch(setCurrentMinappId(app.id))
|
||||||
|
dispatch(setMinappShow(true))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//if the minapp is not keep alive, open it as one-off minapp
|
||||||
|
dispatch(setOpenedOneOffMinapp(app))
|
||||||
|
dispatch(setCurrentMinappId(app.id))
|
||||||
|
dispatch(setMinappShow(true))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/** a wrapper of openMinapp(app, true) */
|
||||||
|
const openMinappKeepAlive = (app: MinAppType) => {
|
||||||
|
openMinapp(app, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Open a minapp by id (look up the minapp in DEFAULT_MIN_APPS) */
|
||||||
|
const openMinappById = (id: string, keepAlive: boolean = false) => {
|
||||||
|
import('@renderer/config/minapps').then(({ DEFAULT_MIN_APPS }) => {
|
||||||
|
const app = DEFAULT_MIN_APPS.find((app) => app?.id === id)
|
||||||
|
if (app) {
|
||||||
|
openMinapp(app, keepAlive)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Close a minapp immediately (popup hides and minapp unloaded) */
|
||||||
|
const closeMinapp = (appid: string) => {
|
||||||
|
if (openedKeepAliveMinapps.some((item) => item.id === appid)) {
|
||||||
|
dispatch(setOpenedKeepAliveMinapps(openedKeepAliveMinapps.filter((item) => item.id !== appid)))
|
||||||
|
} else if (openedOneOffMinapp?.id === appid) {
|
||||||
|
dispatch(setOpenedOneOffMinapp(null))
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(setCurrentMinappId(''))
|
||||||
|
dispatch(setMinappShow(false))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Close all minapps (popup hides and all minapps unloaded) */
|
||||||
|
const closeAllMinapps = () => {
|
||||||
|
dispatch(setOpenedKeepAliveMinapps([]))
|
||||||
|
dispatch(setOpenedOneOffMinapp(null))
|
||||||
|
dispatch(setCurrentMinappId(''))
|
||||||
|
dispatch(setMinappShow(false))
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Hide the minapp popup (only one-off minapp unloaded) */
|
||||||
|
const hideMinappPopup = () => {
|
||||||
|
if (!minappShow) return
|
||||||
|
|
||||||
|
if (openedOneOffMinapp) {
|
||||||
|
dispatch(setOpenedOneOffMinapp(null))
|
||||||
|
dispatch(setCurrentMinappId(''))
|
||||||
|
}
|
||||||
|
dispatch(setMinappShow(false))
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
openMinapp,
|
||||||
|
openMinappKeepAlive,
|
||||||
|
openMinappById,
|
||||||
|
closeMinapp,
|
||||||
|
hideMinappPopup,
|
||||||
|
closeAllMinapps
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -542,10 +542,19 @@
|
|||||||
"warn.siyuan.exporting": "Exporting to Siyuan Note, please do not request export repeatedly!"
|
"warn.siyuan.exporting": "Exporting to Siyuan Note, please do not request export repeatedly!"
|
||||||
},
|
},
|
||||||
"minapp": {
|
"minapp": {
|
||||||
|
"popup": {
|
||||||
|
"refresh": "Refresh",
|
||||||
|
"close": "Close MinApp",
|
||||||
|
"minimize": "Minimize MinApp",
|
||||||
|
"devtools": "Developer Tools",
|
||||||
|
"openExternal": "Open in Browser"
|
||||||
|
},
|
||||||
"sidebar.add.title": "Add to sidebar",
|
"sidebar.add.title": "Add to sidebar",
|
||||||
"sidebar.remove.title": "Remove from sidebar",
|
"sidebar.remove.title": "Remove from sidebar",
|
||||||
"title": "MinApp",
|
"sidebar.close.title": "Close",
|
||||||
"sidebar.hide.title": "Hide MinApp"
|
"sidebar.closeall.title": "Close All",
|
||||||
|
"sidebar.hide.title": "Hide MinApp",
|
||||||
|
"title": "MinApp"
|
||||||
},
|
},
|
||||||
"miniwindow": {
|
"miniwindow": {
|
||||||
"clipboard": {
|
"clipboard": {
|
||||||
|
|||||||
@ -542,8 +542,17 @@
|
|||||||
"error.yuque.no_config": "語雀のAPIアドレスまたはトークンが設定されていません"
|
"error.yuque.no_config": "語雀のAPIアドレスまたはトークンが設定されていません"
|
||||||
},
|
},
|
||||||
"minapp": {
|
"minapp": {
|
||||||
|
"popup": {
|
||||||
|
"refresh": "更新",
|
||||||
|
"close": "ミニアプリを閉じる",
|
||||||
|
"minimize": "ミニアプリを最小化",
|
||||||
|
"devtools": "開発者ツール",
|
||||||
|
"openExternal": "ブラウザで開く"
|
||||||
|
},
|
||||||
"sidebar.add.title": "サイドバーに追加",
|
"sidebar.add.title": "サイドバーに追加",
|
||||||
"sidebar.remove.title": "サイドバーから削除",
|
"sidebar.remove.title": "サイドバーから削除",
|
||||||
|
"sidebar.close.title": "閉じる",
|
||||||
|
"sidebar.closeall.title": "すべて閉じる",
|
||||||
"sidebar.hide.title": "ミニアプリを隠す",
|
"sidebar.hide.title": "ミニアプリを隠す",
|
||||||
"title": "ミニアプリ"
|
"title": "ミニアプリ"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -542,8 +542,17 @@
|
|||||||
"warn.siyuan.exporting": "Экспортируется в Siyuan, пожалуйста, не отправляйте повторные запросы!"
|
"warn.siyuan.exporting": "Экспортируется в Siyuan, пожалуйста, не отправляйте повторные запросы!"
|
||||||
},
|
},
|
||||||
"minapp": {
|
"minapp": {
|
||||||
|
"popup": {
|
||||||
|
"refresh": "Обновить",
|
||||||
|
"close": "Закрыть встроенное приложение",
|
||||||
|
"minimize": "Свернуть встроенное приложение",
|
||||||
|
"devtools": "Инструменты разработчика",
|
||||||
|
"openExternal": "Открыть в браузере"
|
||||||
|
},
|
||||||
"sidebar.add.title": "Добавить в боковую панель",
|
"sidebar.add.title": "Добавить в боковую панель",
|
||||||
"sidebar.remove.title": "Удалить из боковой панели",
|
"sidebar.remove.title": "Удалить из боковой панели",
|
||||||
|
"sidebar.close.title": "Закрыть",
|
||||||
|
"sidebar.closeall.title": "Закрыть все",
|
||||||
"sidebar.hide.title": "Скрыть приложение",
|
"sidebar.hide.title": "Скрыть приложение",
|
||||||
"title": "Встроенные приложения"
|
"title": "Встроенные приложения"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -542,8 +542,17 @@
|
|||||||
"warn.siyuan.exporting": "正在导出到思源笔记,请勿重复请求导出!"
|
"warn.siyuan.exporting": "正在导出到思源笔记,请勿重复请求导出!"
|
||||||
},
|
},
|
||||||
"minapp": {
|
"minapp": {
|
||||||
|
"popup": {
|
||||||
|
"refresh": "刷新",
|
||||||
|
"close": "关闭小程序",
|
||||||
|
"minimize": "最小化小程序",
|
||||||
|
"devtools": "开发者工具",
|
||||||
|
"openExternal": "在浏览器中打开"
|
||||||
|
},
|
||||||
"sidebar.add.title": "添加到侧边栏",
|
"sidebar.add.title": "添加到侧边栏",
|
||||||
"sidebar.remove.title": "从侧边栏移除",
|
"sidebar.remove.title": "从侧边栏移除",
|
||||||
|
"sidebar.close.title": "关闭",
|
||||||
|
"sidebar.closeall.title": "全部关闭",
|
||||||
"sidebar.hide.title": "隐藏小程序",
|
"sidebar.hide.title": "隐藏小程序",
|
||||||
"title": "小程序"
|
"title": "小程序"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -542,8 +542,17 @@
|
|||||||
"warn.siyuan.exporting": "正在導出到思源筆記,請勿重複請求導出!"
|
"warn.siyuan.exporting": "正在導出到思源筆記,請勿重複請求導出!"
|
||||||
},
|
},
|
||||||
"minapp": {
|
"minapp": {
|
||||||
|
"popup": {
|
||||||
|
"refresh": "重新整理",
|
||||||
|
"close": "關閉小工具",
|
||||||
|
"minimize": "最小化小工具",
|
||||||
|
"devtools": "開發者工具",
|
||||||
|
"openExternal": "在瀏覽器中開啟"
|
||||||
|
},
|
||||||
"sidebar.add.title": "新增到側邊欄",
|
"sidebar.add.title": "新增到側邊欄",
|
||||||
"sidebar.remove.title": "從側邊欄移除",
|
"sidebar.remove.title": "從側邊欄移除",
|
||||||
|
"sidebar.close.title": "關閉",
|
||||||
|
"sidebar.closeall.title": "全部關閉",
|
||||||
"sidebar.hide.title": "隱藏小工具",
|
"sidebar.hide.title": "隱藏小工具",
|
||||||
"title": "小工具"
|
"title": "小工具"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import MinAppIcon from '@renderer/components/Icons/MinAppIcon'
|
import MinAppIcon from '@renderer/components/Icons/MinAppIcon'
|
||||||
import MinApp from '@renderer/components/MinApp'
|
import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
|
||||||
import { useMinapps } from '@renderer/hooks/useMinapps'
|
import { useMinapps } from '@renderer/hooks/useMinapps'
|
||||||
import { MinAppType } from '@renderer/types'
|
import { MinAppType } from '@renderer/types'
|
||||||
import type { MenuProps } from 'antd'
|
import type { MenuProps } from 'antd'
|
||||||
@ -15,13 +15,14 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const App: FC<Props> = ({ app, onClick, size = 60 }) => {
|
const App: FC<Props> = ({ app, onClick, size = 60 }) => {
|
||||||
|
const { openMinappKeepAlive } = useMinappPopup()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { minapps, pinned, disabled, updateMinapps, updateDisabledMinapps, updatePinnedMinapps } = useMinapps()
|
const { minapps, pinned, disabled, updateMinapps, updateDisabledMinapps, updatePinnedMinapps } = useMinapps()
|
||||||
const isPinned = pinned.some((p) => p.id === app.id)
|
const isPinned = pinned.some((p) => p.id === app.id)
|
||||||
const isVisible = minapps.some((m) => m.id === app.id)
|
const isVisible = minapps.some((m) => m.id === app.id)
|
||||||
|
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
MinApp.start(app)
|
openMinappKeepAlive(app)
|
||||||
onClick?.()
|
onClick?.()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { DownloadOutlined, ExpandOutlined, LinkOutlined } from '@ant-design/icons'
|
import { DownloadOutlined, ExpandOutlined, LinkOutlined } from '@ant-design/icons'
|
||||||
import MinApp from '@renderer/components/MinApp'
|
|
||||||
import { AppLogo } from '@renderer/config/env'
|
import { AppLogo } from '@renderer/config/env'
|
||||||
|
import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
|
||||||
import { extractTitle } from '@renderer/utils/formats'
|
import { extractTitle } from '@renderer/utils/formats'
|
||||||
import { Button } from 'antd'
|
import { Button } from 'antd'
|
||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
@ -14,6 +14,7 @@ interface Props {
|
|||||||
const Artifacts: FC<Props> = ({ html }) => {
|
const Artifacts: FC<Props> = ({ html }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const title = extractTitle(html) || 'Artifacts ' + t('chat.artifacts.button.preview')
|
const title = extractTitle(html) || 'Artifacts ' + t('chat.artifacts.button.preview')
|
||||||
|
const { openMinapp } = useMinappPopup()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 在应用内打开
|
* 在应用内打开
|
||||||
@ -22,7 +23,8 @@ const Artifacts: FC<Props> = ({ html }) => {
|
|||||||
const path = await window.api.file.create('artifacts-preview.html')
|
const path = await window.api.file.create('artifacts-preview.html')
|
||||||
await window.api.file.write(path, html)
|
await window.api.file.write(path, html)
|
||||||
const filePath = `file://${path}`
|
const filePath = `file://${path}`
|
||||||
MinApp.start({
|
openMinapp({
|
||||||
|
id: 'artifacts-preview',
|
||||||
name: title,
|
name: title,
|
||||||
logo: AppLogo,
|
logo: AppLogo,
|
||||||
url: filePath
|
url: filePath
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
import UserPopup from '@renderer/components/Popups/UserPopup'
|
import UserPopup from '@renderer/components/Popups/UserPopup'
|
||||||
import { APP_NAME, AppLogo, isLocalAi } from '@renderer/config/env'
|
import { APP_NAME, AppLogo, isLocalAi } from '@renderer/config/env'
|
||||||
import { startMinAppById } from '@renderer/config/minapps'
|
|
||||||
import { getModelLogo } from '@renderer/config/models'
|
import { getModelLogo } from '@renderer/config/models'
|
||||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||||
import useAvatar from '@renderer/hooks/useAvatar'
|
import useAvatar from '@renderer/hooks/useAvatar'
|
||||||
|
import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
|
||||||
import { useMessageStyle, useSettings } from '@renderer/hooks/useSettings'
|
import { useMessageStyle, useSettings } from '@renderer/hooks/useSettings'
|
||||||
import { getMessageModelId } from '@renderer/services/MessagesService'
|
import { getMessageModelId } from '@renderer/services/MessagesService'
|
||||||
import { getModelName } from '@renderer/services/ModelService'
|
import { getModelName } from '@renderer/services/ModelService'
|
||||||
@ -32,6 +32,7 @@ const MessageHeader: FC<Props> = memo(({ assistant, model, message }) => {
|
|||||||
const { userName, sidebarIcons } = useSettings()
|
const { userName, sidebarIcons } = useSettings()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { isBubbleStyle } = useMessageStyle()
|
const { isBubbleStyle } = useMessageStyle()
|
||||||
|
const { openMinappById } = useMinappPopup()
|
||||||
|
|
||||||
const avatarSource = useMemo(() => getAvatarSource(isLocalAi, getMessageModelId(message)), [message])
|
const avatarSource = useMemo(() => getAvatarSource(isLocalAi, getMessageModelId(message)), [message])
|
||||||
|
|
||||||
@ -54,7 +55,9 @@ const MessageHeader: FC<Props> = memo(({ assistant, model, message }) => {
|
|||||||
const username = useMemo(() => removeLeadingEmoji(getUserName()), [getUserName])
|
const username = useMemo(() => removeLeadingEmoji(getUserName()), [getUserName])
|
||||||
|
|
||||||
const showMiniApp = useCallback(() => {
|
const showMiniApp = useCallback(() => {
|
||||||
showMinappIcon && model?.provider && startMinAppById(model.provider)
|
showMinappIcon && model?.provider && openMinappById(model.provider)
|
||||||
|
// because don't need openMinappById to be a dependency
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [model?.provider, showMinappIcon])
|
}, [model?.provider, showMinappIcon])
|
||||||
|
|
||||||
const avatarStyle: CSSProperties | undefined = isBubbleStyle
|
const avatarStyle: CSSProperties | undefined = isBubbleStyle
|
||||||
|
|||||||
@ -2,9 +2,9 @@ import { GithubOutlined } from '@ant-design/icons'
|
|||||||
import { FileProtectOutlined, GlobalOutlined, MailOutlined, SoundOutlined } from '@ant-design/icons'
|
import { FileProtectOutlined, GlobalOutlined, MailOutlined, SoundOutlined } from '@ant-design/icons'
|
||||||
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 MinApp from '@renderer/components/MinApp'
|
|
||||||
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 { useRuntime } from '@renderer/hooks/useRuntime'
|
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||||
import { useSettings } from '@renderer/hooks/useSettings'
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
import { useAppDispatch } from '@renderer/store'
|
import { useAppDispatch } from '@renderer/store'
|
||||||
@ -29,6 +29,7 @@ const AboutSettings: FC = () => {
|
|||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
const { update } = useRuntime()
|
const { update } = useRuntime()
|
||||||
|
const { openMinapp } = useMinappPopup()
|
||||||
|
|
||||||
const onCheckUpdate = debounce(
|
const onCheckUpdate = debounce(
|
||||||
async () => {
|
async () => {
|
||||||
@ -70,7 +71,8 @@ const AboutSettings: FC = () => {
|
|||||||
|
|
||||||
const showLicense = async () => {
|
const showLicense = async () => {
|
||||||
const { appPath } = await window.api.getAppInfo()
|
const { appPath } = await window.api.getAppInfo()
|
||||||
MinApp.start({
|
openMinapp({
|
||||||
|
id: 'cherrystudio-license',
|
||||||
name: t('settings.about.license.title'),
|
name: t('settings.about.license.title'),
|
||||||
url: `file://${appPath}/resources/cherry-studio/license.html`,
|
url: `file://${appPath}/resources/cherry-studio/license.html`,
|
||||||
logo: AppLogo
|
logo: AppLogo
|
||||||
@ -79,7 +81,8 @@ const AboutSettings: FC = () => {
|
|||||||
|
|
||||||
const showReleases = async () => {
|
const showReleases = async () => {
|
||||||
const { appPath } = await window.api.getAppInfo()
|
const { appPath } = await window.api.getAppInfo()
|
||||||
MinApp.start({
|
openMinapp({
|
||||||
|
id: 'cherrystudio-releases',
|
||||||
name: t('settings.about.releases.title'),
|
name: t('settings.about.releases.title'),
|
||||||
url: `file://${appPath}/resources/cherry-studio/releases.html?theme=${theme === ThemeMode.dark ? 'dark' : 'light'}`,
|
url: `file://${appPath}/resources/cherry-studio/releases.html?theme=${theme === ThemeMode.dark ? 'dark' : 'light'}`,
|
||||||
logo: AppLogo
|
logo: AppLogo
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { InfoCircleOutlined } from '@ant-design/icons'
|
import { InfoCircleOutlined } from '@ant-design/icons'
|
||||||
import { HStack } from '@renderer/components/Layout'
|
import { HStack } from '@renderer/components/Layout'
|
||||||
import MinApp from '@renderer/components/MinApp'
|
|
||||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||||
|
import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
|
||||||
import { RootState, useAppDispatch } from '@renderer/store'
|
import { RootState, useAppDispatch } from '@renderer/store'
|
||||||
import { setJoplinToken, setJoplinUrl } from '@renderer/store/settings'
|
import { setJoplinToken, setJoplinUrl } from '@renderer/store/settings'
|
||||||
import { Button, Tooltip } from 'antd'
|
import { Button, Tooltip } from 'antd'
|
||||||
@ -16,6 +16,7 @@ const JoplinSettings: FC = () => {
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
|
const { openMinapp } = useMinappPopup()
|
||||||
|
|
||||||
const joplinToken = useSelector((state: RootState) => state.settings.joplinToken)
|
const joplinToken = useSelector((state: RootState) => state.settings.joplinToken)
|
||||||
const joplinUrl = useSelector((state: RootState) => state.settings.joplinUrl)
|
const joplinUrl = useSelector((state: RootState) => state.settings.joplinUrl)
|
||||||
@ -64,7 +65,7 @@ const JoplinSettings: FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleJoplinHelpClick = () => {
|
const handleJoplinHelpClick = () => {
|
||||||
MinApp.start({
|
openMinapp({
|
||||||
id: 'joplin-help',
|
id: 'joplin-help',
|
||||||
name: 'Joplin Help',
|
name: 'Joplin Help',
|
||||||
url: 'https://joplinapp.org/help/apps/clipper'
|
url: 'https://joplinapp.org/help/apps/clipper'
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import { InfoCircleOutlined } from '@ant-design/icons'
|
import { InfoCircleOutlined } from '@ant-design/icons'
|
||||||
import { Client } from '@notionhq/client'
|
import { Client } from '@notionhq/client'
|
||||||
import { HStack } from '@renderer/components/Layout'
|
import { HStack } from '@renderer/components/Layout'
|
||||||
import MinApp from '@renderer/components/MinApp'
|
|
||||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||||
|
import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
|
||||||
import { RootState, useAppDispatch } from '@renderer/store'
|
import { RootState, useAppDispatch } from '@renderer/store'
|
||||||
import {
|
import {
|
||||||
setNotionApiKey,
|
setNotionApiKey,
|
||||||
@ -22,6 +22,7 @@ const NotionSettings: FC = () => {
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
|
const { openMinapp } = useMinappPopup()
|
||||||
|
|
||||||
const notionApiKey = useSelector((state: RootState) => state.settings.notionApiKey)
|
const notionApiKey = useSelector((state: RootState) => state.settings.notionApiKey)
|
||||||
const notionDatabaseID = useSelector((state: RootState) => state.settings.notionDatabaseID)
|
const notionDatabaseID = useSelector((state: RootState) => state.settings.notionDatabaseID)
|
||||||
@ -68,7 +69,7 @@ const NotionSettings: FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleNotionTitleClick = () => {
|
const handleNotionTitleClick = () => {
|
||||||
MinApp.start({
|
openMinapp({
|
||||||
id: 'notion-help',
|
id: 'notion-help',
|
||||||
name: 'Notion Help',
|
name: 'Notion Help',
|
||||||
url: 'https://docs.cherry-ai.com/advanced-basic/notion'
|
url: 'https://docs.cherry-ai.com/advanced-basic/notion'
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { InfoCircleOutlined } from '@ant-design/icons'
|
import { InfoCircleOutlined } from '@ant-design/icons'
|
||||||
import { HStack } from '@renderer/components/Layout'
|
import { HStack } from '@renderer/components/Layout'
|
||||||
import MinApp from '@renderer/components/MinApp'
|
|
||||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||||
|
import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
|
||||||
import { RootState, useAppDispatch } from '@renderer/store'
|
import { RootState, useAppDispatch } from '@renderer/store'
|
||||||
import { setSiyuanApiUrl, setSiyuanBoxId, setSiyuanRootPath, setSiyuanToken } from '@renderer/store/settings'
|
import { setSiyuanApiUrl, setSiyuanBoxId, setSiyuanRootPath, setSiyuanToken } from '@renderer/store/settings'
|
||||||
import { Button, Tooltip } from 'antd'
|
import { Button, Tooltip } from 'antd'
|
||||||
@ -13,6 +13,7 @@ import { useSelector } from 'react-redux'
|
|||||||
import { SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..'
|
import { SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..'
|
||||||
|
|
||||||
const SiyuanSettings: FC = () => {
|
const SiyuanSettings: FC = () => {
|
||||||
|
const { openMinapp } = useMinappPopup()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
@ -39,7 +40,7 @@ const SiyuanSettings: FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleSiyuanHelpClick = () => {
|
const handleSiyuanHelpClick = () => {
|
||||||
MinApp.start({
|
openMinapp({
|
||||||
id: 'siyuan-help',
|
id: 'siyuan-help',
|
||||||
name: 'Siyuan Help',
|
name: 'Siyuan Help',
|
||||||
url: 'https://docs.cherry-ai.com/advanced-basic/siyuan'
|
url: 'https://docs.cherry-ai.com/advanced-basic/siyuan'
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { InfoCircleOutlined } from '@ant-design/icons'
|
import { InfoCircleOutlined } from '@ant-design/icons'
|
||||||
import { HStack } from '@renderer/components/Layout'
|
import { HStack } from '@renderer/components/Layout'
|
||||||
import MinApp from '@renderer/components/MinApp'
|
|
||||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||||
|
import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
|
||||||
import { RootState, useAppDispatch } from '@renderer/store'
|
import { RootState, useAppDispatch } from '@renderer/store'
|
||||||
import { setYuqueRepoId, setYuqueToken, setYuqueUrl } from '@renderer/store/settings'
|
import { setYuqueRepoId, setYuqueToken, setYuqueUrl } from '@renderer/store/settings'
|
||||||
import { Button, Tooltip } from 'antd'
|
import { Button, Tooltip } from 'antd'
|
||||||
@ -16,6 +16,7 @@ const YuqueSettings: FC = () => {
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
|
const { openMinapp } = useMinappPopup()
|
||||||
|
|
||||||
const yuqueToken = useSelector((state: RootState) => state.settings.yuqueToken)
|
const yuqueToken = useSelector((state: RootState) => state.settings.yuqueToken)
|
||||||
const yuqueUrl = useSelector((state: RootState) => state.settings.yuqueUrl)
|
const yuqueUrl = useSelector((state: RootState) => state.settings.yuqueUrl)
|
||||||
@ -64,7 +65,7 @@ const YuqueSettings: FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleYuqueHelpClick = () => {
|
const handleYuqueHelpClick = () => {
|
||||||
MinApp.start({
|
openMinapp({
|
||||||
id: 'yuque-help',
|
id: 'yuque-help',
|
||||||
name: 'Yuque Help',
|
name: 'Yuque Help',
|
||||||
url: 'https://www.yuque.com/settings/tokens'
|
url: 'https://www.yuque.com/settings/tokens'
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import MinApp from '@renderer/components/MinApp'
|
import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
|
||||||
import { MinAppType, Provider } from '@renderer/types'
|
import { MinAppType, Provider } from '@renderer/types'
|
||||||
import { Button } from 'antd'
|
import { Button } from 'antd'
|
||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
@ -15,18 +15,20 @@ const GraphRAGSettings: FC<Props> = ({ provider }) => {
|
|||||||
const apiUrl = provider.apiHost
|
const apiUrl = provider.apiHost
|
||||||
const modalId = provider.models.filter((model) => model.id.includes('global'))[0]?.id
|
const modalId = provider.models.filter((model) => model.id.includes('global'))[0]?.id
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const { openMinapp } = useMinappPopup()
|
||||||
|
|
||||||
const onShowGraphRAG = async () => {
|
const onShowGraphRAG = async () => {
|
||||||
const { appPath } = await window.api.getAppInfo()
|
const { appPath } = await window.api.getAppInfo()
|
||||||
const url = `file://${appPath}/resources/graphrag.html?apiUrl=${apiUrl}&modelId=${modalId}`
|
const url = `file://${appPath}/resources/graphrag.html?apiUrl=${apiUrl}&modelId=${modalId}`
|
||||||
|
|
||||||
const app: MinAppType = {
|
const app: MinAppType = {
|
||||||
|
id: 'graphrag',
|
||||||
name: t('words.knowledgeGraph'),
|
name: t('words.knowledgeGraph'),
|
||||||
logo: '',
|
logo: '',
|
||||||
url
|
url
|
||||||
}
|
}
|
||||||
|
|
||||||
MinApp.start(app)
|
openMinapp(app)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!modalId) {
|
if (!modalId) {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
|
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
|
||||||
import { AppLogo, UserAvatar } from '@renderer/config/env'
|
import { AppLogo, UserAvatar } from '@renderer/config/env'
|
||||||
|
import type { MinAppType } from '@renderer/types'
|
||||||
import type { UpdateInfo } from 'builder-util-runtime'
|
import type { UpdateInfo } from 'builder-util-runtime'
|
||||||
|
|
||||||
export interface UpdateState {
|
export interface UpdateState {
|
||||||
info: UpdateInfo | null
|
info: UpdateInfo | null
|
||||||
checking: boolean
|
checking: boolean
|
||||||
@ -14,7 +14,14 @@ export interface UpdateState {
|
|||||||
export interface RuntimeState {
|
export interface RuntimeState {
|
||||||
avatar: string
|
avatar: string
|
||||||
generating: boolean
|
generating: boolean
|
||||||
|
/** whether the minapp popup is shown */
|
||||||
minappShow: boolean
|
minappShow: boolean
|
||||||
|
/** the minapps that are opened and should be keep alive */
|
||||||
|
openedKeepAliveMinapps: MinAppType[]
|
||||||
|
/** the minapp that is opened for one time */
|
||||||
|
openedOneOffMinapp: MinAppType | null
|
||||||
|
/** the current minapp id */
|
||||||
|
currentMinappId: string
|
||||||
searching: boolean
|
searching: boolean
|
||||||
filesPath: string
|
filesPath: string
|
||||||
resourcesPath: string
|
resourcesPath: string
|
||||||
@ -30,6 +37,9 @@ const initialState: RuntimeState = {
|
|||||||
avatar: UserAvatar,
|
avatar: UserAvatar,
|
||||||
generating: false,
|
generating: false,
|
||||||
minappShow: false,
|
minappShow: false,
|
||||||
|
openedKeepAliveMinapps: [],
|
||||||
|
openedOneOffMinapp: null,
|
||||||
|
currentMinappId: '',
|
||||||
searching: false,
|
searching: false,
|
||||||
filesPath: '',
|
filesPath: '',
|
||||||
resourcesPath: '',
|
resourcesPath: '',
|
||||||
@ -59,6 +69,15 @@ const runtimeSlice = createSlice({
|
|||||||
setMinappShow: (state, action: PayloadAction<boolean>) => {
|
setMinappShow: (state, action: PayloadAction<boolean>) => {
|
||||||
state.minappShow = action.payload
|
state.minappShow = action.payload
|
||||||
},
|
},
|
||||||
|
setOpenedKeepAliveMinapps: (state, action: PayloadAction<MinAppType[]>) => {
|
||||||
|
state.openedKeepAliveMinapps = action.payload
|
||||||
|
},
|
||||||
|
setOpenedOneOffMinapp: (state, action: PayloadAction<MinAppType | null>) => {
|
||||||
|
state.openedOneOffMinapp = action.payload
|
||||||
|
},
|
||||||
|
setCurrentMinappId: (state, action: PayloadAction<string>) => {
|
||||||
|
state.currentMinappId = action.payload
|
||||||
|
},
|
||||||
setSearching: (state, action: PayloadAction<boolean>) => {
|
setSearching: (state, action: PayloadAction<boolean>) => {
|
||||||
state.searching = action.payload
|
state.searching = action.payload
|
||||||
},
|
},
|
||||||
@ -81,6 +100,9 @@ export const {
|
|||||||
setAvatar,
|
setAvatar,
|
||||||
setGenerating,
|
setGenerating,
|
||||||
setMinappShow,
|
setMinappShow,
|
||||||
|
setOpenedKeepAliveMinapps,
|
||||||
|
setOpenedOneOffMinapp,
|
||||||
|
setCurrentMinappId,
|
||||||
setSearching,
|
setSearching,
|
||||||
setFilesPath,
|
setFilesPath,
|
||||||
setResourcesPath,
|
setResourcesPath,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user