mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-08 14:29:15 +08:00
refactor(miniapp): 适配顶部状态栏 (#9695)
* feat(minapp): add top-navbar fixed toolbar and layout adjustments * refactor(minapps): optimize toolbar * fix(minapps): hide redundant components * feat(minapp): improve webview load handling and popup visibility * feat(minapps): improve WebView load handling and clean up launchpad * feat(minapp): 实现活跃小程序数量限制与关闭缓存清理 * fix(minapp): 修复WebView高度不正确的问题 * fix(minapp): show popup only for left navbar mode * feat(minapps): add full-screen loading mask for webview * fix: lint error * feat(minapp): fix drawer sizing and layout when side navbar present * refactor(minapp): 移除固定工具栏组件,优化弹窗容器布局 * feat(minapps): memoize app lookup to avoid unnecessary recompute * chore(minapps): optimize comments Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix(renderer): remove stray blank line in MinAppFullPageView * refactor(minapps): remove top navbar opened minapps component * refactor(tab): remove unused TopNavbarOpenedMinappTabs import --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
parent
6f9906fe49
commit
7c0a800d9d
@ -14,6 +14,7 @@ import FilesPage from './pages/files/FilesPage'
|
|||||||
import HomePage from './pages/home/HomePage'
|
import HomePage from './pages/home/HomePage'
|
||||||
import KnowledgePage from './pages/knowledge/KnowledgePage'
|
import KnowledgePage from './pages/knowledge/KnowledgePage'
|
||||||
import LaunchpadPage from './pages/launchpad/LaunchpadPage'
|
import LaunchpadPage from './pages/launchpad/LaunchpadPage'
|
||||||
|
import MinAppPage from './pages/minapps/MinAppPage'
|
||||||
import MinAppsPage from './pages/minapps/MinAppsPage'
|
import MinAppsPage from './pages/minapps/MinAppsPage'
|
||||||
import NotesPage from './pages/notes/NotesPage'
|
import NotesPage from './pages/notes/NotesPage'
|
||||||
import PaintingsRoutePage from './pages/paintings/PaintingsRoutePage'
|
import PaintingsRoutePage from './pages/paintings/PaintingsRoutePage'
|
||||||
@ -34,6 +35,7 @@ const Router: FC = () => {
|
|||||||
<Route path="/files" element={<FilesPage />} />
|
<Route path="/files" element={<FilesPage />} />
|
||||||
<Route path="/notes" element={<NotesPage />} />
|
<Route path="/notes" element={<NotesPage />} />
|
||||||
<Route path="/knowledge" element={<KnowledgePage />} />
|
<Route path="/knowledge" element={<KnowledgePage />} />
|
||||||
|
<Route path="/apps/:appId" element={<MinAppPage />} />
|
||||||
<Route path="/apps" element={<MinAppsPage />} />
|
<Route path="/apps" element={<MinAppsPage />} />
|
||||||
<Route path="/code" element={<CodeToolsPage />} />
|
<Route path="/code" element={<CodeToolsPage />} />
|
||||||
<Route path="/settings/*" element={<SettingsPage />} />
|
<Route path="/settings/*" element={<SettingsPage />} />
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import { Dropdown } from 'antd'
|
|||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useDispatch } from 'react-redux'
|
import { useDispatch } from 'react-redux'
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -30,6 +31,7 @@ const MinApp: FC<Props> = ({ app, onClick, size = 60, isLast }) => {
|
|||||||
const { minapps, pinned, disabled, updateMinapps, updateDisabledMinapps, updatePinnedMinapps } = useMinapps()
|
const { minapps, pinned, disabled, updateMinapps, updateDisabledMinapps, updatePinnedMinapps } = useMinapps()
|
||||||
const { openedKeepAliveMinapps, currentMinappId, minappShow } = useRuntime()
|
const { openedKeepAliveMinapps, currentMinappId, minappShow } = useRuntime()
|
||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
|
const navigate = useNavigate()
|
||||||
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 isActive = minappShow && currentMinappId === app.id
|
const isActive = minappShow && currentMinappId === app.id
|
||||||
@ -37,7 +39,13 @@ const MinApp: FC<Props> = ({ app, onClick, size = 60, isLast }) => {
|
|||||||
const { isTopNavbar } = useNavbarPosition()
|
const { isTopNavbar } = useNavbarPosition()
|
||||||
|
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
openMinappKeepAlive(app)
|
if (isTopNavbar) {
|
||||||
|
// 顶部导航栏:导航到小程序页面
|
||||||
|
navigate(`/apps/${app.id}`)
|
||||||
|
} else {
|
||||||
|
// 侧边导航栏:保持原有弹窗行为
|
||||||
|
openMinappKeepAlive(app)
|
||||||
|
}
|
||||||
onClick?.()
|
onClick?.()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -24,6 +24,7 @@ import { useAppDispatch } from '@renderer/store'
|
|||||||
import { setMinappsOpenLinkExternal } from '@renderer/store/settings'
|
import { setMinappsOpenLinkExternal } from '@renderer/store/settings'
|
||||||
import { MinAppType } from '@renderer/types'
|
import { MinAppType } from '@renderer/types'
|
||||||
import { delay } from '@renderer/utils'
|
import { delay } from '@renderer/utils'
|
||||||
|
import { clearWebviewState, getWebviewLoaded, setWebviewLoaded } from '@renderer/utils/webviewStateManager'
|
||||||
import { Alert, Avatar, Button, Drawer, Tooltip } from 'antd'
|
import { Alert, Avatar, Button, Drawer, Tooltip } from 'antd'
|
||||||
import { WebviewTag } from 'electron'
|
import { WebviewTag } from 'electron'
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
@ -162,8 +163,7 @@ const MinappPopupContainer: React.FC = () => {
|
|||||||
|
|
||||||
/** store the webview refs, one of the key to make them keepalive */
|
/** store the webview refs, one of the key to make them keepalive */
|
||||||
const webviewRefs = useRef<Map<string, WebviewTag | null>>(new Map())
|
const webviewRefs = useRef<Map<string, WebviewTag | null>>(new Map())
|
||||||
/** indicate whether the webview has loaded */
|
/** Note: WebView loaded states now managed globally via webviewStateManager */
|
||||||
const webviewLoadedRefs = useRef<Map<string, boolean>>(new Map())
|
|
||||||
/** whether the minapps open link external is enabled */
|
/** whether the minapps open link external is enabled */
|
||||||
const { minappsOpenLinkExternal } = useSettings()
|
const { minappsOpenLinkExternal } = useSettings()
|
||||||
|
|
||||||
@ -185,7 +185,7 @@ const MinappPopupContainer: React.FC = () => {
|
|||||||
|
|
||||||
setIsPopupShow(true)
|
setIsPopupShow(true)
|
||||||
|
|
||||||
if (webviewLoadedRefs.current.get(currentMinappId)) {
|
if (getWebviewLoaded(currentMinappId)) {
|
||||||
setIsReady(true)
|
setIsReady(true)
|
||||||
/** the case that open the minapp from sidebar */
|
/** the case that open the minapp from sidebar */
|
||||||
} else if (lastMinappId.current !== currentMinappId && lastMinappShow.current === minappShow) {
|
} else if (lastMinappId.current !== currentMinappId && lastMinappShow.current === minappShow) {
|
||||||
@ -216,17 +216,21 @@ const MinappPopupContainer: React.FC = () => {
|
|||||||
webviewRef.style.display = appid === currentMinappId ? 'inline-flex' : 'none'
|
webviewRef.style.display = appid === currentMinappId ? 'inline-flex' : 'none'
|
||||||
})
|
})
|
||||||
|
|
||||||
//delete the extra webviewLoadedRefs
|
// Set external link behavior for current minapp
|
||||||
webviewLoadedRefs.current.forEach((_, appid) => {
|
if (currentMinappId) {
|
||||||
if (!webviewRefs.current.has(appid)) {
|
const webviewElement = webviewRefs.current.get(currentMinappId)
|
||||||
webviewLoadedRefs.current.delete(appid)
|
if (webviewElement) {
|
||||||
} else if (appid === currentMinappId) {
|
try {
|
||||||
const webviewId = webviewRefs.current.get(appid)?.getWebContentsId()
|
const webviewId = webviewElement.getWebContentsId()
|
||||||
if (webviewId) {
|
if (webviewId) {
|
||||||
window.api.webview.setOpenLinkExternal(webviewId, minappsOpenLinkExternal)
|
window.api.webview.setOpenLinkExternal(webviewId, minappsOpenLinkExternal)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// WebView not ready yet, will be set when it's loaded
|
||||||
|
logger.debug(`WebView ${currentMinappId} not ready for getWebContentsId()`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
}, [currentMinappId, minappsOpenLinkExternal])
|
}, [currentMinappId, minappsOpenLinkExternal])
|
||||||
|
|
||||||
/** only the keepalive minapp can be minimized */
|
/** only the keepalive minapp can be minimized */
|
||||||
@ -255,15 +259,17 @@ const MinappPopupContainer: React.FC = () => {
|
|||||||
/** get the current app info with extra info */
|
/** get the current app info with extra info */
|
||||||
let currentAppInfo: AppInfo | null = null
|
let currentAppInfo: AppInfo | null = null
|
||||||
if (currentMinappId) {
|
if (currentMinappId) {
|
||||||
const currentApp = combinedApps.find((item) => item.id === currentMinappId) as MinAppType
|
const currentApp = combinedApps.find((item) => item.id === currentMinappId)
|
||||||
currentAppInfo = { ...currentApp, ...appsExtraInfo[currentApp.id] }
|
if (currentApp) {
|
||||||
|
currentAppInfo = { ...currentApp, ...appsExtraInfo[currentApp.id] }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** will close the popup and delete the webview */
|
/** will close the popup and delete the webview */
|
||||||
const handlePopupClose = async (appid: string) => {
|
const handlePopupClose = async (appid: string) => {
|
||||||
setIsPopupShow(false)
|
setIsPopupShow(false)
|
||||||
await delay(0.3)
|
await delay(0.3)
|
||||||
webviewLoadedRefs.current.delete(appid)
|
clearWebviewState(appid)
|
||||||
closeMinapp(appid)
|
closeMinapp(appid)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -292,10 +298,17 @@ const MinappPopupContainer: React.FC = () => {
|
|||||||
|
|
||||||
/** the callback function to set the webviews loaded indicator */
|
/** the callback function to set the webviews loaded indicator */
|
||||||
const handleWebviewLoaded = (appid: string) => {
|
const handleWebviewLoaded = (appid: string) => {
|
||||||
webviewLoadedRefs.current.set(appid, true)
|
setWebviewLoaded(appid, true)
|
||||||
const webviewId = webviewRefs.current.get(appid)?.getWebContentsId()
|
const webviewElement = webviewRefs.current.get(appid)
|
||||||
if (webviewId) {
|
if (webviewElement) {
|
||||||
window.api.webview.setOpenLinkExternal(webviewId, minappsOpenLinkExternal)
|
try {
|
||||||
|
const webviewId = webviewElement.getWebContentsId()
|
||||||
|
if (webviewId) {
|
||||||
|
window.api.webview.setOpenLinkExternal(webviewId, minappsOpenLinkExternal)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.debug(`WebView ${appid} not ready for getWebContentsId() in handleWebviewLoaded`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (appid == currentMinappId) {
|
if (appid == currentMinappId) {
|
||||||
setTimeoutTimer('handleWebviewLoaded', () => setIsReady(true), 200)
|
setTimeoutTimer('handleWebviewLoaded', () => setIsReady(true), 200)
|
||||||
@ -352,16 +365,28 @@ const MinappPopupContainer: React.FC = () => {
|
|||||||
/** navigate back in webview history */
|
/** navigate back in webview history */
|
||||||
const handleGoBack = (appid: string) => {
|
const handleGoBack = (appid: string) => {
|
||||||
const webview = webviewRefs.current.get(appid)
|
const webview = webviewRefs.current.get(appid)
|
||||||
if (webview && webview.canGoBack()) {
|
if (webview) {
|
||||||
webview.goBack()
|
try {
|
||||||
|
if (webview.canGoBack()) {
|
||||||
|
webview.goBack()
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.debug(`WebView ${appid} not ready for goBack()`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** navigate forward in webview history */
|
/** navigate forward in webview history */
|
||||||
const handleGoForward = (appid: string) => {
|
const handleGoForward = (appid: string) => {
|
||||||
const webview = webviewRefs.current.get(appid)
|
const webview = webviewRefs.current.get(appid)
|
||||||
if (webview && webview.canGoForward()) {
|
if (webview) {
|
||||||
webview.goForward()
|
try {
|
||||||
|
if (webview.canGoForward()) {
|
||||||
|
webview.goForward()
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.debug(`WebView ${appid} not ready for goForward()`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -409,7 +434,7 @@ const MinappPopupContainer: React.FC = () => {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
<Spacer />
|
<Spacer />
|
||||||
<ButtonsGroup className={isWin || isLinux ? 'windows' : ''}>
|
<ButtonsGroup className={isWin || isLinux ? 'windows' : ''} isTopNavbar={isTopNavbar}>
|
||||||
<Tooltip title={t('minapp.popup.goBack')} mouseEnterDelay={0.8} placement="bottom">
|
<Tooltip title={t('minapp.popup.goBack')} mouseEnterDelay={0.8} placement="bottom">
|
||||||
<TitleButton onClick={() => handleGoBack(appInfo.id)}>
|
<TitleButton onClick={() => handleGoBack(appInfo.id)}>
|
||||||
<ArrowLeftOutlined />
|
<ArrowLeftOutlined />
|
||||||
@ -498,19 +523,23 @@ const MinappPopupContainer: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer
|
<Drawer
|
||||||
title={<Title appInfo={currentAppInfo} url={currentUrl} />}
|
title={isTopNavbar ? null : <Title appInfo={currentAppInfo} url={currentUrl} />}
|
||||||
placement="bottom"
|
placement="bottom"
|
||||||
onClose={handlePopupMinimize}
|
onClose={handlePopupMinimize}
|
||||||
open={isPopupShow}
|
open={isPopupShow}
|
||||||
mask={false}
|
mask={false}
|
||||||
rootClassName="minapp-drawer"
|
rootClassName="minapp-drawer"
|
||||||
maskClassName="minapp-mask"
|
maskClassName="minapp-mask"
|
||||||
height={'100%'}
|
height={isTopNavbar ? 'calc(100% - var(--navbar-height))' : '100%'}
|
||||||
maskClosable={false}
|
maskClosable={false}
|
||||||
closeIcon={null}
|
closeIcon={null}
|
||||||
style={{
|
styles={{
|
||||||
marginLeft: isLeftNavbar ? 'var(--sidebar-width)' : 0,
|
wrapper: {
|
||||||
backgroundColor: window.root.style.background
|
position: 'fixed',
|
||||||
|
marginLeft: isLeftNavbar ? 'var(--sidebar-width)' : 0,
|
||||||
|
marginTop: isTopNavbar ? 'var(--navbar-height)' : 0,
|
||||||
|
backgroundColor: window.root.style.background
|
||||||
|
}
|
||||||
}}>
|
}}>
|
||||||
{/* 在所有小程序中显示GoogleLoginTip */}
|
{/* 在所有小程序中显示GoogleLoginTip */}
|
||||||
<GoogleLoginTip isReady={isReady} currentUrl={currentUrl} currentAppId={currentMinappId} />
|
<GoogleLoginTip isReady={isReady} currentUrl={currentUrl} currentAppId={currentMinappId} />
|
||||||
@ -566,7 +595,7 @@ const TitleTextTooltip = styled.span`
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const ButtonsGroup = styled.div`
|
const ButtonsGroup = styled.div<{ isTopNavbar: boolean }>`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@ -1,11 +1,14 @@
|
|||||||
import MinappPopupContainer from '@renderer/components/MinApp/MinappPopupContainer'
|
import MinappPopupContainer from '@renderer/components/MinApp/MinappPopupContainer'
|
||||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||||
|
import { useNavbarPosition } from '@renderer/hooks/useSettings'
|
||||||
|
|
||||||
const TopViewMinappContainer = () => {
|
const TopViewMinappContainer = () => {
|
||||||
const { openedKeepAliveMinapps, openedOneOffMinapp } = useRuntime()
|
const { openedKeepAliveMinapps, openedOneOffMinapp } = useRuntime()
|
||||||
|
const { isLeftNavbar } = useNavbarPosition()
|
||||||
const isCreate = openedKeepAliveMinapps.length > 0 || openedOneOffMinapp !== null
|
const isCreate = openedKeepAliveMinapps.length > 0 || openedOneOffMinapp !== null
|
||||||
|
|
||||||
return <>{isCreate && <MinappPopupContainer />}</>
|
// Only show popup container in sidebar mode (left navbar), not in tab mode (top navbar)
|
||||||
|
return <>{isCreate && isLeftNavbar && <MinappPopupContainer />}</>
|
||||||
}
|
}
|
||||||
|
|
||||||
export default TopViewMinappContainer
|
export default TopViewMinappContainer
|
||||||
|
|||||||
@ -1,7 +1,10 @@
|
|||||||
import { useNavbarPosition, useSettings } from '@renderer/hooks/useSettings'
|
import { loggerService } from '@logger'
|
||||||
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
import { WebviewTag } from 'electron'
|
import { WebviewTag } from 'electron'
|
||||||
import { memo, useEffect, useRef } from 'react'
|
import { memo, useEffect, useRef } from 'react'
|
||||||
|
|
||||||
|
const logger = loggerService.withContext('WebviewContainer')
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* WebviewContainer is a component that renders a webview element.
|
* WebviewContainer is a component that renders a webview element.
|
||||||
* It is used in the MinAppPopupContainer component.
|
* It is used in the MinAppPopupContainer component.
|
||||||
@ -23,7 +26,6 @@ const WebviewContainer = memo(
|
|||||||
}) => {
|
}) => {
|
||||||
const webviewRef = useRef<WebviewTag | null>(null)
|
const webviewRef = useRef<WebviewTag | null>(null)
|
||||||
const { enableSpellCheck } = useSettings()
|
const { enableSpellCheck } = useSettings()
|
||||||
const { isLeftNavbar } = useNavbarPosition()
|
|
||||||
|
|
||||||
const setRef = (appid: string) => {
|
const setRef = (appid: string) => {
|
||||||
onSetRefCallback(appid, null)
|
onSetRefCallback(appid, null)
|
||||||
@ -41,8 +43,29 @@ const WebviewContainer = memo(
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!webviewRef.current) return
|
if (!webviewRef.current) return
|
||||||
|
|
||||||
|
let loadCallbackFired = false
|
||||||
|
|
||||||
const handleLoaded = () => {
|
const handleLoaded = () => {
|
||||||
onLoadedCallback(appid)
|
logger.debug(`WebView did-finish-load for app: ${appid}`)
|
||||||
|
// Only fire callback once per load cycle
|
||||||
|
if (!loadCallbackFired) {
|
||||||
|
loadCallbackFired = true
|
||||||
|
// Small delay to ensure content is actually visible
|
||||||
|
setTimeout(() => {
|
||||||
|
logger.debug(`Calling onLoadedCallback for app: ${appid}`)
|
||||||
|
onLoadedCallback(appid)
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Additional callback for when page is ready to show
|
||||||
|
const handleReadyToShow = () => {
|
||||||
|
logger.debug(`WebView ready-to-show for app: ${appid}`)
|
||||||
|
if (!loadCallbackFired) {
|
||||||
|
loadCallbackFired = true
|
||||||
|
logger.debug(`Calling onLoadedCallback from ready-to-show for app: ${appid}`)
|
||||||
|
onLoadedCallback(appid)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleNavigate = (event: any) => {
|
const handleNavigate = (event: any) => {
|
||||||
@ -56,16 +79,25 @@ const WebviewContainer = memo(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleStartLoading = () => {
|
||||||
|
// Reset callback flag when starting a new load
|
||||||
|
loadCallbackFired = false
|
||||||
|
}
|
||||||
|
|
||||||
|
webviewRef.current.addEventListener('did-start-loading', handleStartLoading)
|
||||||
webviewRef.current.addEventListener('dom-ready', handleDomReady)
|
webviewRef.current.addEventListener('dom-ready', handleDomReady)
|
||||||
webviewRef.current.addEventListener('did-finish-load', handleLoaded)
|
webviewRef.current.addEventListener('did-finish-load', handleLoaded)
|
||||||
|
webviewRef.current.addEventListener('ready-to-show', handleReadyToShow)
|
||||||
webviewRef.current.addEventListener('did-navigate-in-page', handleNavigate)
|
webviewRef.current.addEventListener('did-navigate-in-page', handleNavigate)
|
||||||
|
|
||||||
// we set the url when the webview is ready
|
// we set the url when the webview is ready
|
||||||
webviewRef.current.src = url
|
webviewRef.current.src = url
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
webviewRef.current?.removeEventListener('did-start-loading', handleStartLoading)
|
||||||
webviewRef.current?.removeEventListener('dom-ready', handleDomReady)
|
webviewRef.current?.removeEventListener('dom-ready', handleDomReady)
|
||||||
webviewRef.current?.removeEventListener('did-finish-load', handleLoaded)
|
webviewRef.current?.removeEventListener('did-finish-load', handleLoaded)
|
||||||
|
webviewRef.current?.removeEventListener('ready-to-show', handleReadyToShow)
|
||||||
webviewRef.current?.removeEventListener('did-navigate-in-page', handleNavigate)
|
webviewRef.current?.removeEventListener('did-navigate-in-page', handleNavigate)
|
||||||
}
|
}
|
||||||
// because the appid and url are enough, no need to add onLoadedCallback
|
// because the appid and url are enough, no need to add onLoadedCallback
|
||||||
@ -73,8 +105,8 @@ const WebviewContainer = memo(
|
|||||||
}, [appid, url])
|
}, [appid, url])
|
||||||
|
|
||||||
const WebviewStyle: React.CSSProperties = {
|
const WebviewStyle: React.CSSProperties = {
|
||||||
width: isLeftNavbar ? 'calc(100vw - var(--sidebar-width))' : '100vw',
|
width: '100%',
|
||||||
height: 'calc(100vh - var(--navbar-height))',
|
height: '100%',
|
||||||
backgroundColor: 'var(--color-background)',
|
backgroundColor: 'var(--color-background)',
|
||||||
display: 'inline-flex'
|
display: 'inline-flex'
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +1,12 @@
|
|||||||
import { PlusOutlined } from '@ant-design/icons'
|
import { PlusOutlined } from '@ant-design/icons'
|
||||||
import { TopNavbarOpenedMinappTabs } from '@renderer/components/app/PinnedMinapps'
|
|
||||||
import { Sortable, useDndReorder } from '@renderer/components/dnd'
|
import { Sortable, useDndReorder } from '@renderer/components/dnd'
|
||||||
import Scrollbar from '@renderer/components/Scrollbar'
|
import Scrollbar from '@renderer/components/Scrollbar'
|
||||||
import { isLinux, isMac, isWin } from '@renderer/config/constant'
|
import { isLinux, isMac, isWin } from '@renderer/config/constant'
|
||||||
|
import { DEFAULT_MIN_APPS } from '@renderer/config/minapps'
|
||||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||||
import { useFullscreen } from '@renderer/hooks/useFullscreen'
|
import { useFullscreen } from '@renderer/hooks/useFullscreen'
|
||||||
import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
|
import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
|
||||||
|
import { useMinapps } from '@renderer/hooks/useMinapps'
|
||||||
import { getThemeModeLabel, getTitleLabel } from '@renderer/i18n/label'
|
import { getThemeModeLabel, getTitleLabel } from '@renderer/i18n/label'
|
||||||
import tabsService from '@renderer/services/TabsService'
|
import tabsService from '@renderer/services/TabsService'
|
||||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||||
@ -37,11 +38,22 @@ 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 MinAppIcon from '../Icons/MinAppIcon'
|
||||||
|
|
||||||
interface TabsContainerProps {
|
interface TabsContainerProps {
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
const getTabIcon = (tabId: string): React.ReactNode | undefined => {
|
const getTabIcon = (tabId: string, minapps: any[]): React.ReactNode | undefined => {
|
||||||
|
// Check if it's a minapp tab (format: apps:appId)
|
||||||
|
if (tabId.startsWith('apps:')) {
|
||||||
|
const appId = tabId.replace('apps:', '')
|
||||||
|
const app = [...DEFAULT_MIN_APPS, ...minapps].find((app) => app.id === appId)
|
||||||
|
if (app) {
|
||||||
|
return <MinAppIcon size={14} app={app} />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
switch (tabId) {
|
switch (tabId) {
|
||||||
case 'home':
|
case 'home':
|
||||||
return <Home size={14} />
|
return <Home size={14} />
|
||||||
@ -82,6 +94,7 @@ const TabsContainer: React.FC<TabsContainerProps> = ({ children }) => {
|
|||||||
const isFullscreen = useFullscreen()
|
const isFullscreen = useFullscreen()
|
||||||
const { settedTheme, toggleTheme } = useTheme()
|
const { settedTheme, toggleTheme } = useTheme()
|
||||||
const { hideMinappPopup } = useMinappPopup()
|
const { hideMinappPopup } = useMinappPopup()
|
||||||
|
const { minapps } = useMinapps()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const scrollRef = useRef<HTMLDivElement>(null)
|
const scrollRef = useRef<HTMLDivElement>(null)
|
||||||
const [canScroll, setCanScroll] = useState(false)
|
const [canScroll, setCanScroll] = useState(false)
|
||||||
@ -89,9 +102,23 @@ const TabsContainer: React.FC<TabsContainerProps> = ({ children }) => {
|
|||||||
const getTabId = (path: string): string => {
|
const getTabId = (path: string): string => {
|
||||||
if (path === '/') return 'home'
|
if (path === '/') return 'home'
|
||||||
const segments = path.split('/')
|
const segments = path.split('/')
|
||||||
|
// Handle minapp paths: /apps/appId -> apps:appId
|
||||||
|
if (segments[1] === 'apps' && segments[2]) {
|
||||||
|
return `apps:${segments[2]}`
|
||||||
|
}
|
||||||
return segments[1] // 获取第一个路径段作为 id
|
return segments[1] // 获取第一个路径段作为 id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getTabTitle = (tabId: string): string => {
|
||||||
|
// Check if it's a minapp tab
|
||||||
|
if (tabId.startsWith('apps:')) {
|
||||||
|
const appId = tabId.replace('apps:', '')
|
||||||
|
const app = [...DEFAULT_MIN_APPS, ...minapps].find((app) => app.id === appId)
|
||||||
|
return app ? app.name : 'MinApp'
|
||||||
|
}
|
||||||
|
return getTitleLabel(tabId)
|
||||||
|
}
|
||||||
|
|
||||||
const shouldCreateTab = (path: string) => {
|
const shouldCreateTab = (path: string) => {
|
||||||
if (path === '/') return false
|
if (path === '/') return false
|
||||||
if (path === '/settings') return false
|
if (path === '/settings') return false
|
||||||
@ -196,8 +223,8 @@ const TabsContainer: React.FC<TabsContainerProps> = ({ children }) => {
|
|||||||
renderItem={(tab) => (
|
renderItem={(tab) => (
|
||||||
<Tab key={tab.id} active={tab.id === activeTabId} onClick={() => handleTabClick(tab)}>
|
<Tab key={tab.id} active={tab.id === activeTabId} onClick={() => handleTabClick(tab)}>
|
||||||
<TabHeader>
|
<TabHeader>
|
||||||
{tab.id && <TabIcon>{getTabIcon(tab.id)}</TabIcon>}
|
{tab.id && <TabIcon>{getTabIcon(tab.id, minapps)}</TabIcon>}
|
||||||
<TabTitle>{getTitleLabel(tab.id)}</TabTitle>
|
<TabTitle>{getTabTitle(tab.id)}</TabTitle>
|
||||||
</TabHeader>
|
</TabHeader>
|
||||||
{tab.id !== 'home' && (
|
{tab.id !== 'home' && (
|
||||||
<CloseButton
|
<CloseButton
|
||||||
@ -224,7 +251,6 @@ const TabsContainer: React.FC<TabsContainerProps> = ({ children }) => {
|
|||||||
</AddTabButton>
|
</AddTabButton>
|
||||||
</TabsArea>
|
</TabsArea>
|
||||||
<RightButtonsContainer>
|
<RightButtonsContainer>
|
||||||
<TopNavbarOpenedMinappTabs />
|
|
||||||
<Tooltip
|
<Tooltip
|
||||||
title={t('settings.theme.title') + ': ' + getThemeModeLabel(settedTheme)}
|
title={t('settings.theme.title') + ': ' + getThemeModeLabel(settedTheme)}
|
||||||
mouseEnterDelay={0.8}
|
mouseEnterDelay={0.8}
|
||||||
|
|||||||
@ -6,107 +6,13 @@ import { useNavbarPosition, useSettings } from '@renderer/hooks/useSettings'
|
|||||||
import { MinAppType } from '@renderer/types'
|
import { MinAppType } from '@renderer/types'
|
||||||
import type { MenuProps } from 'antd'
|
import type { MenuProps } from 'antd'
|
||||||
import { Dropdown, Tooltip } from 'antd'
|
import { Dropdown, Tooltip } from 'antd'
|
||||||
import { FC, useEffect, useState } from 'react'
|
import { FC, useEffect } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
import { DraggableList } from '../DraggableList'
|
import { DraggableList } from '../DraggableList'
|
||||||
import MinAppIcon from '../Icons/MinAppIcon'
|
import MinAppIcon from '../Icons/MinAppIcon'
|
||||||
|
|
||||||
/** Tabs of opened minapps in top navbar */
|
|
||||||
export const TopNavbarOpenedMinappTabs: FC = () => {
|
|
||||||
const { minappShow, openedKeepAliveMinapps, currentMinappId } = useRuntime()
|
|
||||||
const { openMinappKeepAlive, hideMinappPopup, closeMinapp, closeAllMinapps } = useMinappPopup()
|
|
||||||
const { showOpenedMinappsInSidebar } = useSettings()
|
|
||||||
const { theme } = useTheme()
|
|
||||||
const { t } = useTranslation()
|
|
||||||
const [keepAliveMinapps, setKeepAliveMinapps] = useState(openedKeepAliveMinapps)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const timer = setTimeout(() => setKeepAliveMinapps(openedKeepAliveMinapps), 300)
|
|
||||||
return () => clearTimeout(timer)
|
|
||||||
}, [openedKeepAliveMinapps])
|
|
||||||
|
|
||||||
// animation for minapp switch indicator
|
|
||||||
useEffect(() => {
|
|
||||||
const iconDefaultWidth = 30 // 22px icon + 8px gap
|
|
||||||
const iconDefaultOffset = 10 // initial offset
|
|
||||||
const container = document.querySelector('.TopNavContainer') as HTMLElement
|
|
||||||
const activeIcon = document.querySelector('.TopNavContainer .opened-active') as HTMLElement
|
|
||||||
|
|
||||||
let indicatorLeft = 0,
|
|
||||||
indicatorBottom = 0
|
|
||||||
if (minappShow && activeIcon && container) {
|
|
||||||
indicatorLeft = activeIcon.offsetLeft + activeIcon.offsetWidth / 2 - 4 // 4 is half of the indicator's width (8px)
|
|
||||||
indicatorBottom = 0
|
|
||||||
} else {
|
|
||||||
indicatorLeft =
|
|
||||||
((keepAliveMinapps.length > 0 ? keepAliveMinapps.length : 1) / 2) * iconDefaultWidth + iconDefaultOffset - 4
|
|
||||||
indicatorBottom = -50
|
|
||||||
}
|
|
||||||
container?.style.setProperty('--indicator-left', `${indicatorLeft}px`)
|
|
||||||
container?.style.setProperty('--indicator-bottom', `${indicatorBottom}px`)
|
|
||||||
}, [currentMinappId, keepAliveMinapps, minappShow])
|
|
||||||
|
|
||||||
const handleOnClick = (app: MinAppType) => {
|
|
||||||
if (minappShow && currentMinappId === app.id) {
|
|
||||||
hideMinappPopup()
|
|
||||||
} else {
|
|
||||||
openMinappKeepAlive(app)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否需要显示已打开小程序组件
|
|
||||||
const isShowOpened = showOpenedMinappsInSidebar && keepAliveMinapps.length > 0
|
|
||||||
|
|
||||||
// 如果不需要显示,返回空容器
|
|
||||||
if (!isShowOpened) return null
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TopNavContainer
|
|
||||||
className="TopNavContainer"
|
|
||||||
style={{ backgroundColor: keepAliveMinapps.length > 0 ? 'var(--color-list-item)' : 'transparent' }}>
|
|
||||||
<TopNavMenus>
|
|
||||||
{keepAliveMinapps.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="bottom">
|
|
||||||
<Dropdown menu={{ items: menuItems }} trigger={['contextMenu']} overlayStyle={{ zIndex: 10000 }}>
|
|
||||||
<TopNavItemContainer
|
|
||||||
onClick={() => handleOnClick(app)}
|
|
||||||
theme={theme}
|
|
||||||
className={`${isActive ? 'opened-active' : ''}`}>
|
|
||||||
<TopNavIcon theme={theme}>
|
|
||||||
<MinAppIcon size={22} app={app} style={{ border: 'none', padding: 0 }} />
|
|
||||||
</TopNavIcon>
|
|
||||||
</TopNavItemContainer>
|
|
||||||
</Dropdown>
|
|
||||||
</Tooltip>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</TopNavMenus>
|
|
||||||
</TopNavContainer>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Tabs of opened minapps in sidebar */
|
/** Tabs of opened minapps in sidebar */
|
||||||
export const SidebarOpenedMinappTabs: FC = () => {
|
export const SidebarOpenedMinappTabs: FC = () => {
|
||||||
const { minappShow, openedKeepAliveMinapps, currentMinappId } = useRuntime()
|
const { minappShow, openedKeepAliveMinapps, currentMinappId } = useRuntime()
|
||||||
@ -116,7 +22,7 @@ export const SidebarOpenedMinappTabs: FC = () => {
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { isLeftNavbar } = useNavbarPosition()
|
const { isLeftNavbar } = useNavbarPosition()
|
||||||
|
|
||||||
const handleOnClick = (app) => {
|
const handleOnClick = (app: MinAppType) => {
|
||||||
if (minappShow && currentMinappId === app.id) {
|
if (minappShow && currentMinappId === app.id) {
|
||||||
hideMinappPopup()
|
hideMinappPopup()
|
||||||
} else {
|
} else {
|
||||||
@ -329,50 +235,3 @@ const TabsWrapper = styled.div`
|
|||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
`
|
`
|
||||||
|
|
||||||
const TopNavContainer = styled.div`
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 2px;
|
|
||||||
gap: 4px;
|
|
||||||
background-color: var(--color-list-item);
|
|
||||||
border-radius: 20px;
|
|
||||||
margin: 0 5px;
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
left: var(--indicator-left, 0);
|
|
||||||
bottom: var(--indicator-bottom, 0);
|
|
||||||
width: 8px;
|
|
||||||
height: 4px;
|
|
||||||
background-color: var(--color-primary);
|
|
||||||
transition:
|
|
||||||
left 0.3s cubic-bezier(0.4, 0, 0.2, 1),
|
|
||||||
bottom 0.3s ease-in-out;
|
|
||||||
border-radius: 2px;
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const TopNavMenus = styled.div`
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
height: 100%;
|
|
||||||
`
|
|
||||||
|
|
||||||
const TopNavIcon = styled(Icon)`
|
|
||||||
width: 22px;
|
|
||||||
height: 22px;
|
|
||||||
`
|
|
||||||
|
|
||||||
const TopNavItemContainer = styled.div`
|
|
||||||
display: flex;
|
|
||||||
transition: border 0.2s ease;
|
|
||||||
border-radius: 18px;
|
|
||||||
cursor: pointer;
|
|
||||||
border-radius: 50%;
|
|
||||||
padding: 2px;
|
|
||||||
`
|
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { DEFAULT_MIN_APPS } from '@renderer/config/minapps'
|
import { DEFAULT_MIN_APPS } from '@renderer/config/minapps'
|
||||||
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 TabsService from '@renderer/services/TabsService'
|
||||||
import { useAppDispatch } from '@renderer/store'
|
import { useAppDispatch } from '@renderer/store'
|
||||||
import {
|
import {
|
||||||
setCurrentMinappId,
|
setCurrentMinappId,
|
||||||
@ -9,6 +10,7 @@ import {
|
|||||||
setOpenedOneOffMinapp
|
setOpenedOneOffMinapp
|
||||||
} from '@renderer/store/runtime'
|
} from '@renderer/store/runtime'
|
||||||
import { MinAppType } from '@renderer/types'
|
import { MinAppType } from '@renderer/types'
|
||||||
|
import { clearWebviewState } from '@renderer/utils/webviewStateManager'
|
||||||
import { LRUCache } from 'lru-cache'
|
import { LRUCache } from 'lru-cache'
|
||||||
import { useCallback } from 'react'
|
import { useCallback } from 'react'
|
||||||
|
|
||||||
@ -36,7 +38,18 @@ export const useMinappPopup = () => {
|
|||||||
const createLRUCache = useCallback(() => {
|
const createLRUCache = useCallback(() => {
|
||||||
return new LRUCache<string, MinAppType>({
|
return new LRUCache<string, MinAppType>({
|
||||||
max: maxKeepAliveMinapps,
|
max: maxKeepAliveMinapps,
|
||||||
disposeAfter: () => {
|
disposeAfter: (_value, key) => {
|
||||||
|
// Clean up WebView state when app is disposed from cache
|
||||||
|
clearWebviewState(key)
|
||||||
|
|
||||||
|
// Close corresponding tab if it exists
|
||||||
|
const tabs = TabsService.getTabs()
|
||||||
|
const tabToClose = tabs.find((tab) => tab.path === `/apps/${key}`)
|
||||||
|
if (tabToClose) {
|
||||||
|
TabsService.closeTab(tabToClose.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update Redux state
|
||||||
dispatch(setOpenedKeepAliveMinapps(Array.from(minAppsCache.values())))
|
dispatch(setOpenedKeepAliveMinapps(Array.from(minAppsCache.values())))
|
||||||
},
|
},
|
||||||
onInsert: () => {
|
onInsert: () => {
|
||||||
@ -158,6 +171,8 @@ export const useMinappPopup = () => {
|
|||||||
openMinappById,
|
openMinappById,
|
||||||
closeMinapp,
|
closeMinapp,
|
||||||
hideMinappPopup,
|
hideMinappPopup,
|
||||||
closeAllMinapps
|
closeAllMinapps,
|
||||||
|
// Expose cache instance for TabsService integration
|
||||||
|
minAppsCache
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,6 @@ import App from '@renderer/components/MinApp/MinApp'
|
|||||||
import { useMinapps } from '@renderer/hooks/useMinapps'
|
import { useMinapps } from '@renderer/hooks/useMinapps'
|
||||||
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 tabsService from '@renderer/services/TabsService'
|
|
||||||
import { Code, FileSearch, Folder, Languages, LayoutGrid, NotepadText, Palette, Sparkle } from 'lucide-react'
|
import { Code, FileSearch, Folder, Languages, LayoutGrid, NotepadText, Palette, Sparkle } from 'lucide-react'
|
||||||
import { FC, useMemo } from 'react'
|
import { FC, useMemo } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
@ -105,7 +104,7 @@ const LaunchpadPage: FC = () => {
|
|||||||
<Grid>
|
<Grid>
|
||||||
{sortedMinapps.map((app) => (
|
{sortedMinapps.map((app) => (
|
||||||
<AppWrapper key={app.id}>
|
<AppWrapper key={app.id}>
|
||||||
<App app={app} size={56} onClick={() => setTimeout(() => tabsService.closeTab('launchpad'), 350)} />
|
<App app={app} size={56} />
|
||||||
</AppWrapper>
|
</AppWrapper>
|
||||||
))}
|
))}
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
102
src/renderer/src/pages/minapps/MinAppPage.tsx
Normal file
102
src/renderer/src/pages/minapps/MinAppPage.tsx
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
import { loggerService } from '@logger'
|
||||||
|
import { DEFAULT_MIN_APPS } from '@renderer/config/minapps'
|
||||||
|
import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
|
||||||
|
import { useMinapps } from '@renderer/hooks/useMinapps'
|
||||||
|
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||||
|
import { useNavbarPosition } from '@renderer/hooks/useSettings'
|
||||||
|
import TabsService from '@renderer/services/TabsService'
|
||||||
|
import { FC, useEffect, useMemo, useRef } from 'react'
|
||||||
|
import { useNavigate, useParams } from 'react-router-dom'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
import MinAppFullPageView from './components/MinAppFullPageView'
|
||||||
|
|
||||||
|
const logger = loggerService.withContext('MinAppPage')
|
||||||
|
|
||||||
|
const MinAppPage: FC = () => {
|
||||||
|
const { appId } = useParams<{ appId: string }>()
|
||||||
|
const { isTopNavbar } = useNavbarPosition()
|
||||||
|
const { openMinappKeepAlive, minAppsCache } = useMinappPopup()
|
||||||
|
const { minapps } = useMinapps()
|
||||||
|
const { openedKeepAliveMinapps } = useRuntime()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
// Remember the initial navbar position when component mounts
|
||||||
|
const initialIsTopNavbar = useRef<boolean>(isTopNavbar)
|
||||||
|
const hasRedirected = useRef<boolean>(false)
|
||||||
|
|
||||||
|
// Initialize TabsService with cache reference
|
||||||
|
useEffect(() => {
|
||||||
|
if (minAppsCache) {
|
||||||
|
TabsService.setMinAppsCache(minAppsCache)
|
||||||
|
}
|
||||||
|
}, [minAppsCache])
|
||||||
|
|
||||||
|
// Debug: track navbar position changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (initialIsTopNavbar.current !== isTopNavbar) {
|
||||||
|
logger.debug(`NavBar position changed from ${initialIsTopNavbar.current} to ${isTopNavbar}`)
|
||||||
|
}
|
||||||
|
}, [isTopNavbar])
|
||||||
|
|
||||||
|
// Find the app from all available apps
|
||||||
|
const app = useMemo(() => {
|
||||||
|
if (!appId) return null
|
||||||
|
return [...DEFAULT_MIN_APPS, ...minapps].find((app) => app.id === appId)
|
||||||
|
}, [appId, minapps])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// If app not found, redirect to apps list
|
||||||
|
if (!app) {
|
||||||
|
navigate('/apps')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// For sidebar navigation, redirect to apps list and open popup
|
||||||
|
// Only check once and only if we haven't already redirected
|
||||||
|
if (!initialIsTopNavbar.current && !hasRedirected.current) {
|
||||||
|
hasRedirected.current = true
|
||||||
|
navigate('/apps')
|
||||||
|
// Open popup after navigation
|
||||||
|
setTimeout(() => {
|
||||||
|
openMinappKeepAlive(app)
|
||||||
|
}, 100)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// For top navbar mode, integrate with cache system
|
||||||
|
if (initialIsTopNavbar.current) {
|
||||||
|
// Check if app is already in the keep-alive cache
|
||||||
|
const isAlreadyInCache = openedKeepAliveMinapps.some((cachedApp) => cachedApp.id === app.id)
|
||||||
|
|
||||||
|
if (!isAlreadyInCache) {
|
||||||
|
logger.debug(`Adding app ${app.id} to keep-alive cache via openMinappKeepAlive`)
|
||||||
|
// Add to cache without showing popup (for tab mode)
|
||||||
|
openMinappKeepAlive(app)
|
||||||
|
} else {
|
||||||
|
logger.debug(`App ${app.id} already in keep-alive cache`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [app, navigate, openMinappKeepAlive, openedKeepAliveMinapps, initialIsTopNavbar])
|
||||||
|
|
||||||
|
// Don't render anything if app not found or not in top navbar mode initially
|
||||||
|
if (!app || !initialIsTopNavbar.current) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<MinAppFullPageView app={app} />
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Container = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
`
|
||||||
|
|
||||||
|
export default MinAppPage
|
||||||
166
src/renderer/src/pages/minapps/components/MinAppFullPageView.tsx
Normal file
166
src/renderer/src/pages/minapps/components/MinAppFullPageView.tsx
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
import { loggerService } from '@logger'
|
||||||
|
import WebviewContainer from '@renderer/components/MinApp/WebviewContainer'
|
||||||
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
|
import { MinAppType } from '@renderer/types'
|
||||||
|
import { getWebviewLoaded, setWebviewLoaded } from '@renderer/utils/webviewStateManager'
|
||||||
|
import { Avatar } from 'antd'
|
||||||
|
import { WebviewTag } from 'electron'
|
||||||
|
import { FC, useCallback, useEffect, useRef, useState } from 'react'
|
||||||
|
import BeatLoader from 'react-spinners/BeatLoader'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
import MinimalToolbar from './MinimalToolbar'
|
||||||
|
|
||||||
|
const logger = loggerService.withContext('MinAppFullPageView')
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
app: MinAppType
|
||||||
|
}
|
||||||
|
|
||||||
|
const MinAppFullPageView: FC<Props> = ({ app }) => {
|
||||||
|
const webviewRef = useRef<WebviewTag | null>(null)
|
||||||
|
const [isReady, setIsReady] = useState(false)
|
||||||
|
const [currentUrl, setCurrentUrl] = useState<string | null>(null)
|
||||||
|
const { minappsOpenLinkExternal } = useSettings()
|
||||||
|
|
||||||
|
// Debug: log isReady state changes
|
||||||
|
useEffect(() => {
|
||||||
|
logger.debug(`isReady state changed to: ${isReady}`)
|
||||||
|
}, [isReady])
|
||||||
|
|
||||||
|
// Initialize when app changes - smart loading state detection using global state
|
||||||
|
useEffect(() => {
|
||||||
|
setCurrentUrl(app.url)
|
||||||
|
|
||||||
|
// Check if this WebView has been loaded before using global state manager
|
||||||
|
if (getWebviewLoaded(app.id)) {
|
||||||
|
logger.debug(`App ${app.id} already loaded before, setting ready immediately`)
|
||||||
|
setIsReady(true)
|
||||||
|
return // No cleanup needed for immediate ready state
|
||||||
|
} else {
|
||||||
|
logger.debug(`App ${app.id} not loaded before, showing loading state`)
|
||||||
|
setIsReady(false)
|
||||||
|
|
||||||
|
// Backup timer logic removed as requested—loading animation will show indefinitely if needed.
|
||||||
|
// (See version control history for previous implementation.)
|
||||||
|
}
|
||||||
|
}, [app])
|
||||||
|
|
||||||
|
const handleWebviewSetRef = useCallback((_appId: string, element: WebviewTag | null) => {
|
||||||
|
webviewRef.current = element
|
||||||
|
if (element) {
|
||||||
|
logger.debug('WebView element set')
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleWebviewLoaded = useCallback(
|
||||||
|
(appId: string) => {
|
||||||
|
logger.debug(`WebView loaded for app: ${appId}`)
|
||||||
|
const webviewId = webviewRef.current?.getWebContentsId()
|
||||||
|
if (webviewId) {
|
||||||
|
window.api.webview.setOpenLinkExternal(webviewId, minappsOpenLinkExternal)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark this WebView as loaded for future use in global state
|
||||||
|
setWebviewLoaded(appId, true)
|
||||||
|
|
||||||
|
// Use small delay like MinappPopupContainer (100ms) to ensure content is visible
|
||||||
|
if (appId === app.id) {
|
||||||
|
setTimeout(() => {
|
||||||
|
logger.debug(`WebView loaded callback: setting isReady to true for ${appId}`)
|
||||||
|
setIsReady(true)
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[minappsOpenLinkExternal, app.id]
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleWebviewNavigate = useCallback((_appId: string, url: string) => {
|
||||||
|
logger.debug(`URL changed: ${url}`)
|
||||||
|
setCurrentUrl(url)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleReload = useCallback(() => {
|
||||||
|
if (webviewRef.current) {
|
||||||
|
// Clear the loaded state for this app since we're reloading using global state
|
||||||
|
setWebviewLoaded(app.id, false)
|
||||||
|
setIsReady(false) // Set loading state when reloading
|
||||||
|
webviewRef.current.src = app.url
|
||||||
|
}
|
||||||
|
}, [app.url, app.id])
|
||||||
|
|
||||||
|
const handleOpenDevTools = useCallback(() => {
|
||||||
|
if (webviewRef.current) {
|
||||||
|
webviewRef.current.openDevTools()
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<MinimalToolbar
|
||||||
|
app={app}
|
||||||
|
webviewRef={webviewRef}
|
||||||
|
currentUrl={currentUrl}
|
||||||
|
onReload={handleReload}
|
||||||
|
onOpenDevTools={handleOpenDevTools}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<WebviewArea>
|
||||||
|
{!isReady && (
|
||||||
|
<LoadingMask>
|
||||||
|
<LoadingOverlay>
|
||||||
|
<Avatar src={app.logo} size={60} style={{ border: '1px solid var(--color-border)' }} />
|
||||||
|
<BeatLoader color="var(--color-text-2)" size={8} style={{ marginTop: 12 }} />
|
||||||
|
</LoadingOverlay>
|
||||||
|
</LoadingMask>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<WebviewContainer
|
||||||
|
key={app.id}
|
||||||
|
appid={app.id}
|
||||||
|
url={app.url}
|
||||||
|
onSetRefCallback={handleWebviewSetRef}
|
||||||
|
onLoadedCallback={handleWebviewLoaded}
|
||||||
|
onNavigateCallback={handleWebviewNavigate}
|
||||||
|
/>
|
||||||
|
</WebviewArea>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Container = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
`
|
||||||
|
|
||||||
|
const WebviewArea = styled.div`
|
||||||
|
flex: 1;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: var(--color-background);
|
||||||
|
min-height: 0; /* Ensure flex child can shrink */
|
||||||
|
`
|
||||||
|
|
||||||
|
const LoadingMask = styled.div`
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: var(--color-background);
|
||||||
|
z-index: 100;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
`
|
||||||
|
|
||||||
|
const LoadingOverlay = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
pointer-events: none;
|
||||||
|
`
|
||||||
|
|
||||||
|
export default MinAppFullPageView
|
||||||
218
src/renderer/src/pages/minapps/components/MinimalToolbar.tsx
Normal file
218
src/renderer/src/pages/minapps/components/MinimalToolbar.tsx
Normal file
@ -0,0 +1,218 @@
|
|||||||
|
import {
|
||||||
|
ArrowLeftOutlined,
|
||||||
|
ArrowRightOutlined,
|
||||||
|
CodeOutlined,
|
||||||
|
ExportOutlined,
|
||||||
|
LinkOutlined,
|
||||||
|
MinusOutlined,
|
||||||
|
PushpinOutlined,
|
||||||
|
ReloadOutlined
|
||||||
|
} from '@ant-design/icons'
|
||||||
|
import { DEFAULT_MIN_APPS } from '@renderer/config/minapps'
|
||||||
|
import { useMinapps } from '@renderer/hooks/useMinapps'
|
||||||
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
|
import { useAppDispatch } from '@renderer/store'
|
||||||
|
import { setMinappsOpenLinkExternal } from '@renderer/store/settings'
|
||||||
|
import { MinAppType } from '@renderer/types'
|
||||||
|
import { Tooltip } from 'antd'
|
||||||
|
import { WebviewTag } from 'electron'
|
||||||
|
import { FC, useCallback, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
app: MinAppType
|
||||||
|
webviewRef: React.RefObject<WebviewTag | null>
|
||||||
|
currentUrl: string | null
|
||||||
|
onReload: () => void
|
||||||
|
onOpenDevTools: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const MinimalToolbar: FC<Props> = ({ app, webviewRef, currentUrl, onReload, onOpenDevTools }) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { pinned, updatePinnedMinapps } = useMinapps()
|
||||||
|
const { minappsOpenLinkExternal } = useSettings()
|
||||||
|
const dispatch = useAppDispatch()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const [canGoBack, setCanGoBack] = useState(false)
|
||||||
|
const [canGoForward, setCanGoForward] = useState(false)
|
||||||
|
|
||||||
|
const isInDevelopment = process.env.NODE_ENV === 'development'
|
||||||
|
const canPinned = DEFAULT_MIN_APPS.some((item) => item.id === app.id)
|
||||||
|
const isPinned = pinned.some((item) => item.id === app.id)
|
||||||
|
const canOpenExternalLink = app.url.startsWith('http://') || app.url.startsWith('https://')
|
||||||
|
|
||||||
|
// Update navigation state
|
||||||
|
const updateNavigationState = useCallback(() => {
|
||||||
|
if (webviewRef.current) {
|
||||||
|
setCanGoBack(webviewRef.current.canGoBack())
|
||||||
|
setCanGoForward(webviewRef.current.canGoForward())
|
||||||
|
}
|
||||||
|
}, [webviewRef])
|
||||||
|
|
||||||
|
const handleGoBack = useCallback(() => {
|
||||||
|
if (webviewRef.current && webviewRef.current.canGoBack()) {
|
||||||
|
webviewRef.current.goBack()
|
||||||
|
updateNavigationState()
|
||||||
|
}
|
||||||
|
}, [webviewRef, updateNavigationState])
|
||||||
|
|
||||||
|
const handleGoForward = useCallback(() => {
|
||||||
|
if (webviewRef.current && webviewRef.current.canGoForward()) {
|
||||||
|
webviewRef.current.goForward()
|
||||||
|
updateNavigationState()
|
||||||
|
}
|
||||||
|
}, [webviewRef, updateNavigationState])
|
||||||
|
|
||||||
|
const handleMinimize = useCallback(() => {
|
||||||
|
navigate('/apps')
|
||||||
|
}, [navigate])
|
||||||
|
|
||||||
|
const handleTogglePin = useCallback(() => {
|
||||||
|
const newPinned = isPinned ? pinned.filter((item) => item.id !== app.id) : [...pinned, app]
|
||||||
|
updatePinnedMinapps(newPinned)
|
||||||
|
}, [app, isPinned, pinned, updatePinnedMinapps])
|
||||||
|
|
||||||
|
const handleToggleOpenExternal = useCallback(() => {
|
||||||
|
dispatch(setMinappsOpenLinkExternal(!minappsOpenLinkExternal))
|
||||||
|
}, [dispatch, minappsOpenLinkExternal])
|
||||||
|
|
||||||
|
const handleOpenLink = useCallback(() => {
|
||||||
|
const urlToOpen = currentUrl || app.url
|
||||||
|
window.api.openWebsite(urlToOpen)
|
||||||
|
}, [currentUrl, app.url])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToolbarContainer>
|
||||||
|
<LeftSection>
|
||||||
|
<ButtonGroup>
|
||||||
|
<Tooltip title={t('minapp.popup.goBack')} placement="bottom">
|
||||||
|
<ToolbarButton onClick={handleGoBack} $disabled={!canGoBack}>
|
||||||
|
<ArrowLeftOutlined />
|
||||||
|
</ToolbarButton>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip title={t('minapp.popup.goForward')} placement="bottom">
|
||||||
|
<ToolbarButton onClick={handleGoForward} $disabled={!canGoForward}>
|
||||||
|
<ArrowRightOutlined />
|
||||||
|
</ToolbarButton>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip title={t('minapp.popup.refresh')} placement="bottom">
|
||||||
|
<ToolbarButton onClick={onReload}>
|
||||||
|
<ReloadOutlined />
|
||||||
|
</ToolbarButton>
|
||||||
|
</Tooltip>
|
||||||
|
</ButtonGroup>
|
||||||
|
</LeftSection>
|
||||||
|
|
||||||
|
<RightSection>
|
||||||
|
<ButtonGroup>
|
||||||
|
{canOpenExternalLink && (
|
||||||
|
<Tooltip title={t('minapp.popup.openExternal')} placement="bottom">
|
||||||
|
<ToolbarButton onClick={handleOpenLink}>
|
||||||
|
<ExportOutlined />
|
||||||
|
</ToolbarButton>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{canPinned && (
|
||||||
|
<Tooltip
|
||||||
|
title={isPinned ? t('minapp.remove_from_launchpad') : t('minapp.add_to_launchpad')}
|
||||||
|
placement="bottom">
|
||||||
|
<ToolbarButton onClick={handleTogglePin} $active={isPinned}>
|
||||||
|
<PushpinOutlined />
|
||||||
|
</ToolbarButton>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Tooltip
|
||||||
|
title={
|
||||||
|
minappsOpenLinkExternal
|
||||||
|
? t('minapp.popup.open_link_external_on')
|
||||||
|
: t('minapp.popup.open_link_external_off')
|
||||||
|
}
|
||||||
|
placement="bottom">
|
||||||
|
<ToolbarButton onClick={handleToggleOpenExternal} $active={minappsOpenLinkExternal}>
|
||||||
|
<LinkOutlined />
|
||||||
|
</ToolbarButton>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
{isInDevelopment && (
|
||||||
|
<Tooltip title={t('minapp.popup.devtools')} placement="bottom">
|
||||||
|
<ToolbarButton onClick={onOpenDevTools}>
|
||||||
|
<CodeOutlined />
|
||||||
|
</ToolbarButton>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Tooltip title={t('minapp.popup.minimize')} placement="bottom">
|
||||||
|
<ToolbarButton onClick={handleMinimize}>
|
||||||
|
<MinusOutlined />
|
||||||
|
</ToolbarButton>
|
||||||
|
</Tooltip>
|
||||||
|
</ButtonGroup>
|
||||||
|
</RightSection>
|
||||||
|
</ToolbarContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ToolbarContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: 35px;
|
||||||
|
padding: 0 12px;
|
||||||
|
background-color: var(--color-background);
|
||||||
|
flex-shrink: 0;
|
||||||
|
`
|
||||||
|
|
||||||
|
const LeftSection = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const RightSection = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
`
|
||||||
|
|
||||||
|
const ButtonGroup = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const ToolbarButton = styled.button<{
|
||||||
|
$disabled?: boolean
|
||||||
|
$active?: boolean
|
||||||
|
}>`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: ${({ $active }) => ($active ? 'var(--color-primary-bg)' : 'transparent')};
|
||||||
|
color: ${({ $disabled, $active }) =>
|
||||||
|
$disabled ? 'var(--color-text-3)' : $active ? 'var(--color-primary)' : 'var(--color-text-2)'};
|
||||||
|
cursor: ${({ $disabled }) => ($disabled ? 'default' : 'pointer')};
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-size: 12px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: ${({ $disabled, $active }) =>
|
||||||
|
$disabled ? 'transparent' : $active ? 'var(--color-primary-bg)' : 'var(--color-background-soft)'};
|
||||||
|
color: ${({ $disabled, $active }) =>
|
||||||
|
$disabled ? 'var(--color-text-3)' : $active ? 'var(--color-primary)' : 'var(--color-text-1)'};
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: ${({ $disabled }) => ($disabled ? 'none' : 'scale(0.95)')};
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export default MinimalToolbar
|
||||||
@ -1,12 +1,29 @@
|
|||||||
import { loggerService } from '@logger'
|
import { loggerService } from '@logger'
|
||||||
import store from '@renderer/store'
|
import store from '@renderer/store'
|
||||||
import { removeTab, setActiveTab } from '@renderer/store/tabs'
|
import { removeTab, setActiveTab } from '@renderer/store/tabs'
|
||||||
|
import { MinAppType } from '@renderer/types'
|
||||||
|
import { clearWebviewState } from '@renderer/utils/webviewStateManager'
|
||||||
|
import { LRUCache } from 'lru-cache'
|
||||||
|
|
||||||
import NavigationService from './NavigationService'
|
import NavigationService from './NavigationService'
|
||||||
|
|
||||||
const logger = loggerService.withContext('TabsService')
|
const logger = loggerService.withContext('TabsService')
|
||||||
|
|
||||||
class TabsService {
|
class TabsService {
|
||||||
|
private minAppsCache: LRUCache<string, MinAppType> | null = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the reference to the mini-apps LRU cache used for managing mini-app lifecycle and cleanup.
|
||||||
|
* This method is required to integrate TabsService with the mini-apps cache system, allowing TabsService
|
||||||
|
* to perform cache cleanup when tabs associated with mini-apps are closed. The cache instance is typically
|
||||||
|
* provided by the mini-app popup system and enables TabsService to maintain cache consistency and prevent
|
||||||
|
* stale data.
|
||||||
|
* @param cache The LRUCache instance containing mini-app data, provided by useMinappPopup.
|
||||||
|
*/
|
||||||
|
public setMinAppsCache(cache: LRUCache<string, MinAppType>) {
|
||||||
|
this.minAppsCache = cache
|
||||||
|
logger.debug('Mini-apps cache reference set in TabsService')
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* 关闭指定的标签页
|
* 关闭指定的标签页
|
||||||
* @param tabId 要关闭的标签页ID
|
* @param tabId 要关闭的标签页ID
|
||||||
@ -49,6 +66,9 @@ class TabsService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clean up mini-app cache if this is a mini-app tab
|
||||||
|
this.cleanupMinAppCache(tabId)
|
||||||
|
|
||||||
// 使用 Redux action 移除标签页
|
// 使用 Redux action 移除标签页
|
||||||
store.dispatch(removeTab(tabId))
|
store.dispatch(removeTab(tabId))
|
||||||
|
|
||||||
@ -56,6 +76,32 @@ class TabsService {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up mini-app cache and WebView state when tab is closed
|
||||||
|
* @param tabId The tab ID to clean up
|
||||||
|
*/
|
||||||
|
private cleanupMinAppCache(tabId: string) {
|
||||||
|
// Check if this is a mini-app tab (format: /apps/{appId})
|
||||||
|
const tabs = store.getState().tabs.tabs
|
||||||
|
const tab = tabs.find((t) => t.id === tabId)
|
||||||
|
|
||||||
|
if (tab && tab.path.startsWith('/apps/')) {
|
||||||
|
const appId = tab.path.replace('/apps/', '')
|
||||||
|
|
||||||
|
if (this.minAppsCache && this.minAppsCache.has(appId)) {
|
||||||
|
logger.debug(`Cleaning up mini-app cache for app: ${appId}`)
|
||||||
|
|
||||||
|
// Remove from LRU cache - this will trigger disposeAfter callback
|
||||||
|
this.minAppsCache.delete(appId)
|
||||||
|
|
||||||
|
// Clear WebView state
|
||||||
|
clearWebviewState(appId)
|
||||||
|
|
||||||
|
logger.info(`Mini-app ${appId} removed from cache due to tab closure`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取所有标签页
|
* 获取所有标签页
|
||||||
*/
|
*/
|
||||||
|
|||||||
55
src/renderer/src/utils/webviewStateManager.ts
Normal file
55
src/renderer/src/utils/webviewStateManager.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import { loggerService } from '@logger'
|
||||||
|
|
||||||
|
const logger = loggerService.withContext('WebviewStateManager')
|
||||||
|
|
||||||
|
// Global WebView loaded states - shared between popup and tab modes
|
||||||
|
const globalWebviewStates = new Map<string, boolean>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set WebView loaded state for a specific app
|
||||||
|
* @param appId - The mini-app ID
|
||||||
|
* @param loaded - Whether the WebView is loaded
|
||||||
|
*/
|
||||||
|
export const setWebviewLoaded = (appId: string, loaded: boolean) => {
|
||||||
|
globalWebviewStates.set(appId, loaded)
|
||||||
|
logger.debug(`WebView state set for ${appId}: ${loaded}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get WebView loaded state for a specific app
|
||||||
|
* @param appId - The mini-app ID
|
||||||
|
* @returns Whether the WebView is loaded
|
||||||
|
*/
|
||||||
|
export const getWebviewLoaded = (appId: string): boolean => {
|
||||||
|
return globalWebviewStates.get(appId) || false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear WebView state for a specific app
|
||||||
|
* @param appId - The mini-app ID
|
||||||
|
*/
|
||||||
|
export const clearWebviewState = (appId: string) => {
|
||||||
|
const wasLoaded = globalWebviewStates.delete(appId)
|
||||||
|
if (wasLoaded) {
|
||||||
|
logger.debug(`WebView state cleared for ${appId}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all WebView states
|
||||||
|
*/
|
||||||
|
export const clearAllWebviewStates = () => {
|
||||||
|
const count = globalWebviewStates.size
|
||||||
|
globalWebviewStates.clear()
|
||||||
|
logger.debug(`Cleared all WebView states (${count} apps)`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all loaded app IDs
|
||||||
|
* @returns Array of app IDs that have loaded WebViews
|
||||||
|
*/
|
||||||
|
export const getLoadedAppIds = (): string[] => {
|
||||||
|
return Array.from(globalWebviewStates.entries())
|
||||||
|
.filter(([, loaded]) => loaded)
|
||||||
|
.map(([appId]) => appId)
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user