From ec4d106a59c5a14b20a8239720d85cf255e788ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?George=C2=B7Dong?= <98630204+GeorgeDong32@users.noreply.github.com> Date: Wed, 24 Sep 2025 14:19:06 +0800 Subject: [PATCH] fix(minapps): openMinApp function doesn't work properly (#10308) * feat(minapps): support temporary minapps * feat(settings): use openSmartMinApp with app logo to open docs sites * refactor(icons): replace styled img with tailwind * feat(tab): tighten types * feat(tab): use minapps cache and log missing entries * test(icons): update MinAppIcon snapshot to reflect class and attrs --- .../src/components/Icons/MinAppIcon.tsx | 64 ++++++++++++------- .../__snapshots__/MinAppIcon.test.tsx.snap | 12 ++-- .../src/components/Tab/TabContainer.tsx | 57 +++++++++++++++-- src/renderer/src/hooks/useMinappPopup.ts | 32 ++++++++++ src/renderer/src/pages/minapps/MinAppPage.tsx | 15 ++++- .../src/pages/settings/AboutSettings.tsx | 10 +-- .../settings/DataSettings/JoplinSettings.tsx | 8 ++- .../settings/DataSettings/NotionSettings.tsx | 8 ++- .../settings/DataSettings/S3Settings.tsx | 8 ++- .../settings/DataSettings/SiyuanSettings.tsx | 8 ++- .../settings/DataSettings/YuqueSettings.tsx | 8 ++- 11 files changed, 170 insertions(+), 60 deletions(-) diff --git a/src/renderer/src/components/Icons/MinAppIcon.tsx b/src/renderer/src/components/Icons/MinAppIcon.tsx index c9612416bb..98974da745 100644 --- a/src/renderer/src/components/Icons/MinAppIcon.tsx +++ b/src/renderer/src/components/Icons/MinAppIcon.tsx @@ -1,7 +1,6 @@ import { DEFAULT_MIN_APPS } from '@renderer/config/minapps' import { MinAppType } from '@renderer/types' import { FC } from 'react' -import styled from 'styled-components' interface Props { app: MinAppType @@ -11,31 +10,52 @@ interface Props { } const MinAppIcon: FC = ({ app, size = 48, style, sidebar = false }) => { + // First try to find in DEFAULT_MIN_APPS for predefined styling const _app = DEFAULT_MIN_APPS.find((item) => item.id === app.id) - if (!_app) { - return null + // If found in DEFAULT_MIN_APPS, use predefined styling + if (_app) { + return ( + {app.name + ) } - return ( - - ) + // If not found in DEFAULT_MIN_APPS but app has logo, use it (for temporary apps) + if (app.logo) { + return ( + {app.name + ) + } + + return null } -const Container = styled.img` - border-radius: 16px; - user-select: none; - -webkit-user-drag: none; -` - export default MinAppIcon diff --git a/src/renderer/src/components/Icons/__tests__/__snapshots__/MinAppIcon.test.tsx.snap b/src/renderer/src/components/Icons/__tests__/__snapshots__/MinAppIcon.test.tsx.snap index e41515fed6..3395e366f4 100644 --- a/src/renderer/src/components/Icons/__tests__/__snapshots__/MinAppIcon.test.tsx.snap +++ b/src/renderer/src/components/Icons/__tests__/__snapshots__/MinAppIcon.test.tsx.snap @@ -1,15 +1,11 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`MinAppIcon > should render correctly with various props 1`] = ` -.c0 { - border-radius: 16px; - user-select: none; - -webkit-user-drag: none; -} - Test App `; diff --git a/src/renderer/src/components/Tab/TabContainer.tsx b/src/renderer/src/components/Tab/TabContainer.tsx index efa19565d1..da2ad668de 100644 --- a/src/renderer/src/components/Tab/TabContainer.tsx +++ b/src/renderer/src/components/Tab/TabContainer.tsx @@ -1,4 +1,5 @@ import { PlusOutlined } from '@ant-design/icons' +import { loggerService } from '@logger' import { Sortable, useDndReorder } from '@renderer/components/dnd' import HorizontalScrollContainer from '@renderer/components/HorizontalScrollContainer' import { isMac } from '@renderer/config/constant' @@ -12,9 +13,10 @@ import tabsService from '@renderer/services/TabsService' import { useAppDispatch, useAppSelector } from '@renderer/store' import type { Tab } from '@renderer/store/tabs' import { addTab, removeTab, setActiveTab, setTabs } from '@renderer/store/tabs' -import { ThemeMode } from '@renderer/types' +import { MinAppType, ThemeMode } from '@renderer/types' import { classNames } from '@renderer/utils' import { Tooltip } from 'antd' +import { LRUCache } from 'lru-cache' import { FileSearch, Folder, @@ -45,14 +47,40 @@ interface TabsContainerProps { children: React.ReactNode } -const getTabIcon = (tabId: string, minapps: any[]): React.ReactNode | undefined => { +const logger = loggerService.withContext('TabContainer') + +const getTabIcon = ( + tabId: string, + minapps: MinAppType[], + minAppsCache?: LRUCache +): 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) + let app = [...DEFAULT_MIN_APPS, ...minapps].find((app) => app.id === appId) + + // If not found in permanent apps, search in temporary apps cache + // The cache stores apps opened via openSmartMinapp() for top navbar mode + // These are temporary MinApps that were opened but not yet saved to user's config + // The cache is LRU (Least Recently Used) with max size from settings + // Cache validity: Apps in cache are currently active/recently used, not outdated + if (!app && minAppsCache) { + app = minAppsCache.get(appId) + + // Defensive programming: If app not found in cache but tab exists, + // the cache entry may have been evicted due to LRU policy + // Log warning for debugging potential sync issues + if (!app) { + logger.warn(`MinApp ${appId} not found in cache, using fallback icon`) + } + } + if (app) { return } + + // Fallback: If no app found (cache evicted), show default icon + return } switch (tabId) { @@ -94,7 +122,7 @@ const TabsContainer: React.FC = ({ children }) => { const activeTabId = useAppSelector((state) => state.tabs.activeTabId) const isFullscreen = useFullscreen() const { settedTheme, toggleTheme } = useTheme() - const { hideMinappPopup } = useMinappPopup() + const { hideMinappPopup, minAppsCache } = useMinappPopup() const { minapps } = useMinapps() const { t } = useTranslation() @@ -112,8 +140,23 @@ const TabsContainer: React.FC = ({ children }) => { // 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' + let app = [...DEFAULT_MIN_APPS, ...minapps].find((app) => app.id === appId) + + // If not found in permanent apps, search in temporary apps cache + // This ensures temporary MinApps display proper titles while being used + // The LRU cache automatically manages app lifecycle and prevents memory leaks + if (!app && minAppsCache) { + app = minAppsCache.get(appId) + + // Defensive programming: If app not found in cache but tab exists, + // the cache entry may have been evicted due to LRU policy + if (!app) { + logger.warn(`MinApp ${appId} not found in cache, using fallback title`) + } + } + + // Return app name if found, otherwise use fallback with appId + return app ? app.name : `MinApp-${appId}` } return getTitleLabel(tabId) } @@ -196,7 +239,7 @@ const TabsContainer: React.FC = ({ children }) => { renderItem={(tab) => ( handleTabClick(tab)}> - {tab.id && {getTabIcon(tab.id, minapps)}} + {tab.id && {getTabIcon(tab.id, minapps, minAppsCache)}} {getTabTitle(tab.id)} {tab.id !== 'home' && ( diff --git a/src/renderer/src/hooks/useMinappPopup.ts b/src/renderer/src/hooks/useMinappPopup.ts index e8765267a6..99b49e43e8 100644 --- a/src/renderer/src/hooks/useMinappPopup.ts +++ b/src/renderer/src/hooks/useMinappPopup.ts @@ -1,6 +1,7 @@ import { DEFAULT_MIN_APPS } from '@renderer/config/minapps' import { useRuntime } from '@renderer/hooks/useRuntime' import { useSettings } from '@renderer/hooks/useSettings' // 使用设置中的值 +import NavigationService from '@renderer/services/NavigationService' import TabsService from '@renderer/services/TabsService' import { useAppDispatch } from '@renderer/store' import { @@ -14,6 +15,8 @@ import { clearWebviewState } from '@renderer/utils/webviewStateManager' import { LRUCache } from 'lru-cache' import { useCallback } from 'react' +import { useNavbarPosition } from './useSettings' + let minAppsCache: LRUCache /** @@ -34,6 +37,7 @@ export const useMinappPopup = () => { const dispatch = useAppDispatch() const { openedKeepAliveMinapps, openedOneOffMinapp, minappShow } = useRuntime() const { maxKeepAliveMinapps } = useSettings() // 使用设置中的值 + const { isTopNavbar } = useNavbarPosition() const createLRUCache = useCallback(() => { return new LRUCache({ @@ -165,6 +169,33 @@ export const useMinappPopup = () => { dispatch(setMinappShow(false)) }, [dispatch, minappShow, openedOneOffMinapp]) + /** Smart open minapp that adapts to navbar position */ + const openSmartMinapp = useCallback( + (config: MinAppType, keepAlive: boolean = false) => { + if (isTopNavbar) { + // For top navbar mode, need to add to cache first for temporary apps + const cacheApp = minAppsCache.get(config.id) + if (!cacheApp) { + // Add temporary app to cache so MinAppPage can find it + minAppsCache.set(config.id, config) + } + + // Set current minapp and show state + dispatch(setCurrentMinappId(config.id)) + dispatch(setMinappShow(true)) + + // Then navigate to the app tab using NavigationService + if (NavigationService.navigate) { + NavigationService.navigate(`/apps/${config.id}`) + } + } else { + // For side navbar, use the traditional popup system + openMinapp(config, keepAlive) + } + }, + [isTopNavbar, openMinapp, dispatch] + ) + return { openMinapp, openMinappKeepAlive, @@ -172,6 +203,7 @@ export const useMinappPopup = () => { closeMinapp, hideMinappPopup, closeAllMinapps, + openSmartMinapp, // Expose cache instance for TabsService integration minAppsCache } diff --git a/src/renderer/src/pages/minapps/MinAppPage.tsx b/src/renderer/src/pages/minapps/MinAppPage.tsx index 9629b3c56d..c85afab22c 100644 --- a/src/renderer/src/pages/minapps/MinAppPage.tsx +++ b/src/renderer/src/pages/minapps/MinAppPage.tsx @@ -44,11 +44,20 @@ const MinAppPage: FC = () => { } }, [isTopNavbar]) - // Find the app from all available apps + // Find the app from all available apps (including cached ones) const app = useMemo(() => { if (!appId) return null - return [...DEFAULT_MIN_APPS, ...minapps].find((app) => app.id === appId) - }, [appId, minapps]) + + // First try to find in default and custom mini-apps + let foundApp = [...DEFAULT_MIN_APPS, ...minapps].find((app) => app.id === appId) + + // If not found and we have cache, try to find in cache (for temporary apps) + if (!foundApp && minAppsCache) { + foundApp = minAppsCache.get(appId) + } + + return foundApp + }, [appId, minapps, minAppsCache]) useEffect(() => { // If app not found, redirect to apps list diff --git a/src/renderer/src/pages/settings/AboutSettings.tsx b/src/renderer/src/pages/settings/AboutSettings.tsx index b79f1cb099..40b0a99ecb 100644 --- a/src/renderer/src/pages/settings/AboutSettings.tsx +++ b/src/renderer/src/pages/settings/AboutSettings.tsx @@ -14,7 +14,7 @@ import { runAsyncFunction } from '@renderer/utils' import { UpgradeChannel } from '@shared/config/constant' import { Avatar, Button, Progress, Radio, Row, Switch, Tag, Tooltip } from 'antd' import { debounce } from 'lodash' -import { Bug, FileCheck, Github, Globe, Mail, Rss } from 'lucide-react' +import { Bug, FileCheck, Globe, Mail, Rss } from 'lucide-react' import { BadgeQuestionMark } from 'lucide-react' import { FC, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -32,7 +32,7 @@ const AboutSettings: FC = () => { const { theme } = useTheme() const dispatch = useAppDispatch() const { update } = useRuntime() - const { openMinapp } = useMinappPopup() + const { openSmartMinapp } = useMinappPopup() const onCheckUpdate = debounce( async () => { @@ -79,7 +79,7 @@ const AboutSettings: FC = () => { const showLicense = async () => { const { appPath } = await window.api.getAppInfo() - openMinapp({ + openSmartMinapp({ id: 'cherrystudio-license', name: t('settings.about.license.title'), url: `file://${appPath}/resources/cherry-studio/license.html`, @@ -89,7 +89,7 @@ const AboutSettings: FC = () => { const showReleases = async () => { const { appPath } = await window.api.getAppInfo() - openMinapp({ + openSmartMinapp({ id: 'cherrystudio-releases', name: t('settings.about.releases.title'), url: `file://${appPath}/resources/cherry-studio/releases.html?theme=${theme === ThemeMode.dark ? 'dark' : 'light'}`, @@ -309,7 +309,7 @@ const AboutSettings: FC = () => { - + {t('settings.about.feedback.title')}