mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-27 12:51:26 +08:00
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
This commit is contained in:
parent
fe0c0fac1e
commit
ec4d106a59
@ -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<Props> = ({ 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 (
|
||||
<img
|
||||
src={_app.logo}
|
||||
className="select-none rounded-2xl"
|
||||
style={{
|
||||
border: _app.bodered ? '0.5px solid var(--color-border)' : 'none',
|
||||
width: `${size}px`,
|
||||
height: `${size}px`,
|
||||
backgroundColor: _app.background,
|
||||
userSelect: 'none',
|
||||
...(sidebar ? {} : app.style),
|
||||
...style
|
||||
}}
|
||||
draggable={false}
|
||||
alt={app.name || 'MinApp Icon'}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Container
|
||||
src={_app.logo}
|
||||
style={{
|
||||
border: _app.bodered ? '0.5px solid var(--color-border)' : 'none',
|
||||
width: `${size}px`,
|
||||
height: `${size}px`,
|
||||
backgroundColor: _app.background,
|
||||
...(sidebar ? {} : app.style),
|
||||
...style
|
||||
}}
|
||||
/>
|
||||
)
|
||||
// If not found in DEFAULT_MIN_APPS but app has logo, use it (for temporary apps)
|
||||
if (app.logo) {
|
||||
return (
|
||||
<img
|
||||
src={app.logo}
|
||||
className="select-none rounded-2xl"
|
||||
style={{
|
||||
border: 'none',
|
||||
width: `${size}px`,
|
||||
height: `${size}px`,
|
||||
backgroundColor: 'transparent',
|
||||
userSelect: 'none',
|
||||
...(sidebar ? {} : app.style),
|
||||
...style
|
||||
}}
|
||||
draggable={false}
|
||||
alt={app.name || 'MinApp Icon'}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
const Container = styled.img`
|
||||
border-radius: 16px;
|
||||
user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
`
|
||||
|
||||
export default MinAppIcon
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
<img
|
||||
class="c0"
|
||||
alt="Test App"
|
||||
class="select-none rounded-2xl"
|
||||
draggable="false"
|
||||
src="/test-logo-1.png"
|
||||
style="border: 0.5px solid var(--color-border); width: 64px; height: 64px; background-color: rgb(240, 240, 240); opacity: 0.8; transform: scale(1.1); margin-top: 10px;"
|
||||
style="border: 0.5px solid var(--color-border); width: 64px; height: 64px; background-color: rgb(240, 240, 240); user-select: none; opacity: 0.8; transform: scale(1.1); margin-top: 10px;"
|
||||
/>
|
||||
`;
|
||||
|
||||
@ -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<string, MinAppType>
|
||||
): 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 <MinAppIcon size={14} app={app} />
|
||||
}
|
||||
|
||||
// Fallback: If no app found (cache evicted), show default icon
|
||||
return <LayoutGrid size={14} />
|
||||
}
|
||||
|
||||
switch (tabId) {
|
||||
@ -94,7 +122,7 @@ const TabsContainer: React.FC<TabsContainerProps> = ({ 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<TabsContainerProps> = ({ 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<TabsContainerProps> = ({ children }) => {
|
||||
renderItem={(tab) => (
|
||||
<Tab key={tab.id} active={tab.id === activeTabId} onClick={() => handleTabClick(tab)}>
|
||||
<TabHeader>
|
||||
{tab.id && <TabIcon>{getTabIcon(tab.id, minapps)}</TabIcon>}
|
||||
{tab.id && <TabIcon>{getTabIcon(tab.id, minapps, minAppsCache)}</TabIcon>}
|
||||
<TabTitle>{getTabTitle(tab.id)}</TabTitle>
|
||||
</TabHeader>
|
||||
{tab.id !== 'home' && (
|
||||
|
||||
@ -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<string, MinAppType>
|
||||
|
||||
/**
|
||||
@ -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<string, MinAppType>({
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 = () => {
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>
|
||||
<Github size={18} />
|
||||
<GithubOutlined size={18} />
|
||||
{t('settings.about.feedback.title')}
|
||||
</SettingRowTitle>
|
||||
<Button onClick={() => onOpenWebsite('https://github.com/CherryHQ/cherry-studio/issues/new/choose')}>
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { InfoCircleOutlined } from '@ant-design/icons'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import { AppLogo } from '@renderer/config/env'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
|
||||
import { RootState, useAppDispatch } from '@renderer/store'
|
||||
@ -16,7 +17,7 @@ const JoplinSettings: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { theme } = useTheme()
|
||||
const dispatch = useAppDispatch()
|
||||
const { openMinapp } = useMinappPopup()
|
||||
const { openSmartMinapp } = useMinappPopup()
|
||||
|
||||
const joplinToken = useSelector((state: RootState) => state.settings.joplinToken)
|
||||
const joplinUrl = useSelector((state: RootState) => state.settings.joplinUrl)
|
||||
@ -66,10 +67,11 @@ const JoplinSettings: FC = () => {
|
||||
}
|
||||
|
||||
const handleJoplinHelpClick = () => {
|
||||
openMinapp({
|
||||
openSmartMinapp({
|
||||
id: 'joplin-help',
|
||||
name: 'Joplin Help',
|
||||
url: 'https://joplinapp.org/help/apps/clipper'
|
||||
url: 'https://joplinapp.org/help/apps/clipper',
|
||||
logo: AppLogo
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { InfoCircleOutlined } from '@ant-design/icons'
|
||||
import { Client } from '@notionhq/client'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import { AppLogo } from '@renderer/config/env'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
|
||||
import { RootState, useAppDispatch } from '@renderer/store'
|
||||
@ -21,7 +22,7 @@ const NotionSettings: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { theme } = useTheme()
|
||||
const dispatch = useAppDispatch()
|
||||
const { openMinapp } = useMinappPopup()
|
||||
const { openSmartMinapp } = useMinappPopup()
|
||||
|
||||
const notionApiKey = useSelector((state: RootState) => state.settings.notionApiKey)
|
||||
const notionDatabaseID = useSelector((state: RootState) => state.settings.notionDatabaseID)
|
||||
@ -67,10 +68,11 @@ const NotionSettings: FC = () => {
|
||||
}
|
||||
|
||||
const handleNotionTitleClick = () => {
|
||||
openMinapp({
|
||||
openSmartMinapp({
|
||||
id: 'notion-help',
|
||||
name: 'Notion Help',
|
||||
url: 'https://docs.cherry-ai.com/advanced-basic/notion'
|
||||
url: 'https://docs.cherry-ai.com/advanced-basic/notion',
|
||||
logo: AppLogo
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -3,6 +3,7 @@ import { HStack } from '@renderer/components/Layout'
|
||||
import { S3BackupManager } from '@renderer/components/S3BackupManager'
|
||||
import { S3BackupModal, useS3BackupModal } from '@renderer/components/S3Modals'
|
||||
import Selector from '@renderer/components/Selector'
|
||||
import { AppLogo } from '@renderer/config/env'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
@ -47,7 +48,7 @@ const S3Settings: FC = () => {
|
||||
const dispatch = useAppDispatch()
|
||||
const { theme } = useTheme()
|
||||
const { t } = useTranslation()
|
||||
const { openMinapp } = useMinappPopup()
|
||||
const { openSmartMinapp } = useMinappPopup()
|
||||
|
||||
const { s3Sync } = useAppSelector((state) => state.backup)
|
||||
|
||||
@ -62,10 +63,11 @@ const S3Settings: FC = () => {
|
||||
}
|
||||
|
||||
const handleTitleClick = () => {
|
||||
openMinapp({
|
||||
openSmartMinapp({
|
||||
id: 's3-help',
|
||||
name: 'S3 Compatible Storage Help',
|
||||
url: 'https://docs.cherry-ai.com/data-settings/s3-compatible'
|
||||
url: 'https://docs.cherry-ai.com/data-settings/s3-compatible',
|
||||
logo: AppLogo
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { InfoCircleOutlined } from '@ant-design/icons'
|
||||
import { loggerService } from '@logger'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import { AppLogo } from '@renderer/config/env'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
|
||||
import { RootState, useAppDispatch } from '@renderer/store'
|
||||
@ -16,7 +17,7 @@ import { SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle
|
||||
const logger = loggerService.withContext('SiyuanSettings')
|
||||
|
||||
const SiyuanSettings: FC = () => {
|
||||
const { openMinapp } = useMinappPopup()
|
||||
const { openSmartMinapp } = useMinappPopup()
|
||||
const { t } = useTranslation()
|
||||
const { theme } = useTheme()
|
||||
const dispatch = useAppDispatch()
|
||||
@ -43,10 +44,11 @@ const SiyuanSettings: FC = () => {
|
||||
}
|
||||
|
||||
const handleSiyuanHelpClick = () => {
|
||||
openMinapp({
|
||||
openSmartMinapp({
|
||||
id: 'siyuan-help',
|
||||
name: 'Siyuan Help',
|
||||
url: 'https://docs.cherry-ai.com/advanced-basic/siyuan'
|
||||
url: 'https://docs.cherry-ai.com/advanced-basic/siyuan',
|
||||
logo: AppLogo
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { InfoCircleOutlined } from '@ant-design/icons'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import { AppLogo } from '@renderer/config/env'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
|
||||
import { RootState, useAppDispatch } from '@renderer/store'
|
||||
@ -16,7 +17,7 @@ const YuqueSettings: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { theme } = useTheme()
|
||||
const dispatch = useAppDispatch()
|
||||
const { openMinapp } = useMinappPopup()
|
||||
const { openSmartMinapp } = useMinappPopup()
|
||||
|
||||
const yuqueToken = useSelector((state: RootState) => state.settings.yuqueToken)
|
||||
const yuqueUrl = useSelector((state: RootState) => state.settings.yuqueUrl)
|
||||
@ -65,10 +66,11 @@ const YuqueSettings: FC = () => {
|
||||
}
|
||||
|
||||
const handleYuqueHelpClick = () => {
|
||||
openMinapp({
|
||||
openSmartMinapp({
|
||||
id: 'yuque-help',
|
||||
name: 'Yuque Help',
|
||||
url: 'https://www.yuque.com/settings/tokens'
|
||||
url: 'https://www.yuque.com/settings/tokens',
|
||||
logo: AppLogo
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user