mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-02-06 11:01:13 +08:00
feat(minapps): add locale-based filtering and i18n support (#12572)
* feat(i18n): update i18n name immediately * feat(minapps): hide Chinese apps for non-Chinese locale users - Add `locales` field to MinAppType for locale-based visibility - Add locale filtering in useMinapps hook - Mark 23 Chinese domestic apps with locales: ['zh-CN', 'zh-TW'] Apps hidden for non-Chinese users include: Wanzhi, ChatGLM, Kimi, Baichuan, Qwen, Stepfun, Doubao, Hailuo, Wenxin, Baidu AI Search, Tencent Yuanbao, Sensechat, SparkDesk, Metaso, Tiangong AI, Nami AI, Xiaoyi, WPS AI, Zhihu, Dangbei AI, Ant Ling, LongCat, QwenChat Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: minapp logic * fix: review comments * fix: update MinimalToolbar to use allMinApps Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
5c013cdb63
commit
f9f550cecd
@ -1,4 +1,4 @@
|
||||
import { DEFAULT_MIN_APPS } from '@renderer/config/minapps'
|
||||
import { allMinApps } from '@renderer/config/minapps'
|
||||
import type { MinAppType } from '@renderer/types'
|
||||
import type { FC } from 'react'
|
||||
|
||||
@ -10,10 +10,10 @@ 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)
|
||||
// First try to find in allMinApps for predefined styling
|
||||
const _app = allMinApps.find((item) => item.id === app.id)
|
||||
|
||||
// If found in DEFAULT_MIN_APPS, use predefined styling
|
||||
// If found in allMinApps, use predefined styling
|
||||
if (_app) {
|
||||
return (
|
||||
<img
|
||||
@ -34,7 +34,7 @@ const MinAppIcon: FC<Props> = ({ app, size = 48, style, sidebar = false }) => {
|
||||
)
|
||||
}
|
||||
|
||||
// If not found in DEFAULT_MIN_APPS but app has logo, use it (for temporary apps)
|
||||
// If not found in allMinApps but app has logo, use it (for temporary apps)
|
||||
if (app.logo) {
|
||||
return (
|
||||
<img
|
||||
|
||||
@ -4,7 +4,7 @@ import { describe, expect, it, vi } from 'vitest'
|
||||
import MinAppIcon from '../MinAppIcon'
|
||||
|
||||
vi.mock('@renderer/config/minapps', () => ({
|
||||
DEFAULT_MIN_APPS: [
|
||||
allMinApps: [
|
||||
{
|
||||
id: 'test-app-1',
|
||||
name: 'Test App 1',
|
||||
@ -52,7 +52,7 @@ describe('MinAppIcon', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('should return null when app is not found in DEFAULT_MIN_APPS', () => {
|
||||
it('should return null when app is not found in allMinApps', () => {
|
||||
const unknownApp = {
|
||||
id: 'unknown-app',
|
||||
name: 'Unknown App',
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { loggerService } from '@logger'
|
||||
import MinAppIcon from '@renderer/components/Icons/MinAppIcon'
|
||||
import IndicatorLight from '@renderer/components/IndicatorLight'
|
||||
import { loadCustomMiniApp, ORIGIN_DEFAULT_MIN_APPS, updateDefaultMinApps } from '@renderer/config/minapps'
|
||||
import { loadCustomMiniApp, ORIGIN_DEFAULT_MIN_APPS, updateAllMinApps } from '@renderer/config/minapps'
|
||||
import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
|
||||
import { useMinapps } from '@renderer/hooks/useMinapps'
|
||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
@ -93,7 +93,7 @@ const MinApp: FC<Props> = ({ app, onClick, size = 60, isLast }) => {
|
||||
await window.api.file.writeWithId('custom-minapps.json', JSON.stringify(updatedApps, null, 2))
|
||||
window.toast.success(t('settings.miniapps.custom.remove_success'))
|
||||
const reloadedApps = [...ORIGIN_DEFAULT_MIN_APPS, ...(await loadCustomMiniApp())]
|
||||
updateDefaultMinApps(reloadedApps)
|
||||
updateAllMinApps(reloadedApps)
|
||||
updateMinapps(minapps.filter((item) => item.id !== app.id))
|
||||
updatePinnedMinapps(pinned.filter((item) => item.id !== app.id))
|
||||
updateDisabledMinapps(disabled.filter((item) => item.id !== app.id))
|
||||
@ -122,7 +122,7 @@ const MinApp: FC<Props> = ({ app, onClick, size = 60, isLast }) => {
|
||||
</StyledIndicator>
|
||||
)}
|
||||
</IconContainer>
|
||||
<AppTitle>{isLast ? t('settings.miniapps.custom.title') : app.name}</AppTitle>
|
||||
<AppTitle>{isLast ? t('settings.miniapps.custom.title') : app.nameKey ? t(app.nameKey) : app.name}</AppTitle>
|
||||
</Container>
|
||||
</Dropdown>
|
||||
)
|
||||
|
||||
@ -13,7 +13,7 @@ import {
|
||||
import { loggerService } from '@logger'
|
||||
import WindowControls from '@renderer/components/WindowControls'
|
||||
import { isDev, isLinux, isMac, isWin } from '@renderer/config/constant'
|
||||
import { DEFAULT_MIN_APPS } from '@renderer/config/minapps'
|
||||
import { allMinApps } from '@renderer/config/minapps'
|
||||
import { useBridge } from '@renderer/hooks/useBridge'
|
||||
import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
|
||||
import { useMinapps } from '@renderer/hooks/useMinapps'
|
||||
@ -246,7 +246,7 @@ const MinappPopupContainer: React.FC = () => {
|
||||
(acc, app) => ({
|
||||
...acc,
|
||||
[app.id]: {
|
||||
canPinned: DEFAULT_MIN_APPS.some((item) => item.id === app.id),
|
||||
canPinned: allMinApps.some((item) => item.id === app.id),
|
||||
isPinned: pinned.some((item) => item.id === app.id),
|
||||
canOpenExternalLink: app.url.startsWith('http://') || app.url.startsWith('https://')
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@ import { loggerService } from '@logger'
|
||||
import { Sortable, useDndReorder } from '@renderer/components/dnd'
|
||||
import HorizontalScrollContainer from '@renderer/components/HorizontalScrollContainer'
|
||||
import { isLinux, isMac } from '@renderer/config/constant'
|
||||
import { DEFAULT_MIN_APPS } from '@renderer/config/minapps'
|
||||
import { allMinApps } from '@renderer/config/minapps'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { useFullscreen } from '@renderer/hooks/useFullscreen'
|
||||
import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
|
||||
@ -58,7 +58,7 @@ const getTabIcon = (
|
||||
// Check if it's a minapp tab (format: apps:appId)
|
||||
if (tabId.startsWith('apps:')) {
|
||||
const appId = tabId.replace('apps:', '')
|
||||
let app = [...DEFAULT_MIN_APPS, ...minapps].find((app) => app.id === appId)
|
||||
let app = [...allMinApps, ...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
|
||||
@ -140,7 +140,7 @@ const TabsContainer: React.FC<TabsContainerProps> = ({ children }) => {
|
||||
// Check if it's a minapp tab
|
||||
if (tabId.startsWith('apps:')) {
|
||||
const appId = tabId.replace('apps:', '')
|
||||
let app = [...DEFAULT_MIN_APPS, ...minapps].find((app) => app.id === appId)
|
||||
let app = [...allMinApps, ...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
|
||||
|
||||
@ -56,7 +56,6 @@ import GroqProviderLogo from '@renderer/assets/images/providers/groq.png?url'
|
||||
import OpenAiProviderLogo from '@renderer/assets/images/providers/openai.png?url'
|
||||
import SiliconFlowProviderLogo from '@renderer/assets/images/providers/silicon.png?url'
|
||||
import ZhipuProviderLogo from '@renderer/assets/images/providers/zhipu.png?url'
|
||||
import i18n from '@renderer/i18n'
|
||||
import type { MinAppType } from '@renderer/types'
|
||||
|
||||
const logger = loggerService.withContext('Config:minapps')
|
||||
@ -118,14 +117,18 @@ const ORIGIN_DEFAULT_MIN_APPS: MinAppType[] = [
|
||||
},
|
||||
{
|
||||
id: 'yi',
|
||||
name: i18n.t('minapps.wanzhi'),
|
||||
name: 'Wanzhi',
|
||||
nameKey: 'minapps.wanzhi',
|
||||
locales: ['zh-CN', 'zh-TW'],
|
||||
url: 'https://www.wanzhi.com/',
|
||||
logo: WanZhiAppLogo,
|
||||
bodered: true
|
||||
},
|
||||
{
|
||||
id: 'zhipu',
|
||||
name: i18n.t('minapps.chatglm'),
|
||||
name: 'ChatGLM',
|
||||
nameKey: 'minapps.chatglm',
|
||||
locales: ['zh-CN', 'zh-TW'],
|
||||
url: 'https://chatglm.cn/main/alltoolsdetail',
|
||||
logo: ZhipuProviderLogo,
|
||||
bodered: true
|
||||
@ -133,31 +136,40 @@ const ORIGIN_DEFAULT_MIN_APPS: MinAppType[] = [
|
||||
{
|
||||
id: 'moonshot',
|
||||
name: 'Kimi',
|
||||
locales: ['zh-CN', 'zh-TW'],
|
||||
url: 'https://kimi.moonshot.cn/',
|
||||
logo: KimiAppLogo
|
||||
},
|
||||
{
|
||||
id: 'baichuan',
|
||||
name: i18n.t('minapps.baichuan'),
|
||||
name: 'Baichuan',
|
||||
nameKey: 'minapps.baichuan',
|
||||
locales: ['zh-CN', 'zh-TW'],
|
||||
url: 'https://ying.baichuan-ai.com/chat',
|
||||
logo: BaicuanAppLogo
|
||||
},
|
||||
{
|
||||
id: 'dashscope',
|
||||
name: i18n.t('minapps.qwen'),
|
||||
name: 'Qwen',
|
||||
nameKey: 'minapps.qwen',
|
||||
locales: ['zh-CN', 'zh-TW'],
|
||||
url: 'https://www.qianwen.com',
|
||||
logo: QwenModelLogo
|
||||
},
|
||||
{
|
||||
id: 'stepfun',
|
||||
name: i18n.t('minapps.stepfun'),
|
||||
name: 'Stepfun',
|
||||
nameKey: 'minapps.stepfun',
|
||||
locales: ['zh-CN', 'zh-TW'],
|
||||
url: 'https://stepfun.com',
|
||||
logo: StepfunAppLogo,
|
||||
bodered: true
|
||||
},
|
||||
{
|
||||
id: 'doubao',
|
||||
name: i18n.t('minapps.doubao'),
|
||||
name: 'Doubao',
|
||||
nameKey: 'minapps.doubao',
|
||||
locales: ['zh-CN', 'zh-TW'],
|
||||
url: 'https://www.doubao.com/chat/',
|
||||
logo: DoubaoAppLogo
|
||||
},
|
||||
@ -169,7 +181,9 @@ const ORIGIN_DEFAULT_MIN_APPS: MinAppType[] = [
|
||||
},
|
||||
{
|
||||
id: 'minimax',
|
||||
name: i18n.t('minapps.hailuo'),
|
||||
name: 'Hailuo',
|
||||
nameKey: 'minapps.hailuo',
|
||||
locales: ['zh-CN', 'zh-TW'],
|
||||
url: 'https://chat.minimaxi.com/',
|
||||
logo: HailuoModelLogo,
|
||||
bodered: true
|
||||
@ -198,13 +212,17 @@ const ORIGIN_DEFAULT_MIN_APPS: MinAppType[] = [
|
||||
},
|
||||
{
|
||||
id: 'baidu-ai-chat',
|
||||
name: i18n.t('minapps.wenxin'),
|
||||
name: 'Wenxin',
|
||||
nameKey: 'minapps.wenxin',
|
||||
locales: ['zh-CN', 'zh-TW'],
|
||||
logo: BaiduAiAppLogo,
|
||||
url: 'https://yiyan.baidu.com/'
|
||||
},
|
||||
{
|
||||
id: 'baidu-ai-search',
|
||||
name: i18n.t('minapps.baidu-ai-search'),
|
||||
name: 'Baidu AI Search',
|
||||
nameKey: 'minapps.baidu-ai-search',
|
||||
locales: ['zh-CN', 'zh-TW'],
|
||||
logo: BaiduAiSearchLogo,
|
||||
url: 'https://chat.baidu.com/',
|
||||
bodered: true,
|
||||
@ -214,14 +232,18 @@ const ORIGIN_DEFAULT_MIN_APPS: MinAppType[] = [
|
||||
},
|
||||
{
|
||||
id: 'tencent-yuanbao',
|
||||
name: i18n.t('minapps.tencent-yuanbao'),
|
||||
name: 'Tencent Yuanbao',
|
||||
nameKey: 'minapps.tencent-yuanbao',
|
||||
locales: ['zh-CN', 'zh-TW'],
|
||||
logo: TencentYuanbaoAppLogo,
|
||||
url: 'https://yuanbao.tencent.com/chat',
|
||||
bodered: true
|
||||
},
|
||||
{
|
||||
id: 'sensetime-chat',
|
||||
name: i18n.t('minapps.sensechat'),
|
||||
name: 'Sensechat',
|
||||
nameKey: 'minapps.sensechat',
|
||||
locales: ['zh-CN', 'zh-TW'],
|
||||
logo: SensetimeAppLogo,
|
||||
url: 'https://chat.sensetime.com/wb/chat',
|
||||
bodered: true
|
||||
@ -229,12 +251,15 @@ const ORIGIN_DEFAULT_MIN_APPS: MinAppType[] = [
|
||||
{
|
||||
id: 'spark-desk',
|
||||
name: 'SparkDesk',
|
||||
locales: ['zh-CN', 'zh-TW'],
|
||||
logo: SparkDeskAppLogo,
|
||||
url: 'https://xinghuo.xfyun.cn/desk'
|
||||
},
|
||||
{
|
||||
id: 'metaso',
|
||||
name: i18n.t('minapps.metaso'),
|
||||
name: 'Metaso',
|
||||
nameKey: 'minapps.metaso',
|
||||
locales: ['zh-CN', 'zh-TW'],
|
||||
logo: MetasoAppLogo,
|
||||
url: 'https://metaso.cn/'
|
||||
},
|
||||
@ -258,7 +283,9 @@ const ORIGIN_DEFAULT_MIN_APPS: MinAppType[] = [
|
||||
},
|
||||
{
|
||||
id: 'tiangong-ai',
|
||||
name: i18n.t('minapps.tiangong-ai'),
|
||||
name: 'Tiangong AI',
|
||||
nameKey: 'minapps.tiangong-ai',
|
||||
locales: ['zh-CN', 'zh-TW'],
|
||||
logo: TiangongAiLogo,
|
||||
url: 'https://www.tiangong.cn/',
|
||||
bodered: true
|
||||
@ -285,7 +312,9 @@ const ORIGIN_DEFAULT_MIN_APPS: MinAppType[] = [
|
||||
},
|
||||
{
|
||||
id: 'nm',
|
||||
name: i18n.t('minapps.nami-ai'),
|
||||
name: 'Nami AI',
|
||||
nameKey: 'minapps.nami-ai',
|
||||
locales: ['zh-CN', 'zh-TW'],
|
||||
logo: NamiAiLogo,
|
||||
url: 'https://bot.n.cn/',
|
||||
bodered: true
|
||||
@ -329,6 +358,7 @@ const ORIGIN_DEFAULT_MIN_APPS: MinAppType[] = [
|
||||
{
|
||||
id: 'qwenlm',
|
||||
name: 'QwenChat',
|
||||
locales: ['zh-CN', 'zh-TW'],
|
||||
logo: QwenlmAppLogo,
|
||||
url: 'https://chat.qwen.ai'
|
||||
},
|
||||
@ -354,7 +384,9 @@ const ORIGIN_DEFAULT_MIN_APPS: MinAppType[] = [
|
||||
},
|
||||
{
|
||||
id: 'xiaoyi',
|
||||
name: i18n.t('minapps.xiaoyi'),
|
||||
name: 'Xiaoyi',
|
||||
nameKey: 'minapps.xiaoyi',
|
||||
locales: ['zh-CN', 'zh-TW'],
|
||||
logo: XiaoYiAppLogo,
|
||||
url: 'https://xiaoyi.huawei.com/chat/',
|
||||
bodered: true
|
||||
@ -384,7 +416,9 @@ const ORIGIN_DEFAULT_MIN_APPS: MinAppType[] = [
|
||||
},
|
||||
{
|
||||
id: 'wpslingxi',
|
||||
name: i18n.t('minapps.wps-copilot'),
|
||||
name: 'WPS AI',
|
||||
nameKey: 'minapps.wps-copilot',
|
||||
locales: ['zh-CN', 'zh-TW'],
|
||||
logo: WPSLingXiLogo,
|
||||
url: 'https://copilot.wps.cn/',
|
||||
bodered: true
|
||||
@ -425,14 +459,18 @@ const ORIGIN_DEFAULT_MIN_APPS: MinAppType[] = [
|
||||
},
|
||||
{
|
||||
id: 'zhihu',
|
||||
name: i18n.t('minapps.zhihu'),
|
||||
name: 'Zhihu Zhida',
|
||||
nameKey: 'minapps.zhihu',
|
||||
locales: ['zh-CN', 'zh-TW'],
|
||||
logo: ZhihuAppLogo,
|
||||
url: 'https://zhida.zhihu.com/',
|
||||
bodered: true
|
||||
},
|
||||
{
|
||||
id: 'dangbei',
|
||||
name: i18n.t('minapps.dangbei'),
|
||||
name: 'Dangbei AI',
|
||||
nameKey: 'minapps.dangbei',
|
||||
locales: ['zh-CN', 'zh-TW'],
|
||||
logo: DangbeiLogo,
|
||||
url: 'https://ai.dangbei.com/',
|
||||
bodered: true
|
||||
@ -460,13 +498,16 @@ const ORIGIN_DEFAULT_MIN_APPS: MinAppType[] = [
|
||||
{
|
||||
id: 'longcat',
|
||||
name: 'LongCat',
|
||||
locales: ['zh-CN', 'zh-TW'],
|
||||
logo: LongCatAppLogo,
|
||||
url: 'https://longcat.chat/',
|
||||
bodered: true
|
||||
},
|
||||
{
|
||||
id: 'ling',
|
||||
name: i18n.t('minapps.ant-ling'),
|
||||
name: 'Ant Ling',
|
||||
nameKey: 'minapps.ant-ling',
|
||||
locales: ['zh-CN', 'zh-TW'],
|
||||
url: 'https://ling.tbox.cn/chat',
|
||||
logo: LingAppLogo,
|
||||
bodered: true,
|
||||
@ -486,11 +527,11 @@ const ORIGIN_DEFAULT_MIN_APPS: MinAppType[] = [
|
||||
}
|
||||
]
|
||||
|
||||
// 加载自定义小应用并合并到默认应用中
|
||||
let DEFAULT_MIN_APPS = [...ORIGIN_DEFAULT_MIN_APPS, ...(await loadCustomMiniApp())]
|
||||
// All mini apps: built-in defaults + custom apps loaded from user config
|
||||
let allMinApps = [...ORIGIN_DEFAULT_MIN_APPS, ...(await loadCustomMiniApp())]
|
||||
|
||||
function updateDefaultMinApps(param) {
|
||||
DEFAULT_MIN_APPS = param
|
||||
function updateAllMinApps(apps: MinAppType[]) {
|
||||
allMinApps = apps
|
||||
}
|
||||
|
||||
export { DEFAULT_MIN_APPS, loadCustomMiniApp, ORIGIN_DEFAULT_MIN_APPS, updateDefaultMinApps }
|
||||
export { allMinApps, loadCustomMiniApp, ORIGIN_DEFAULT_MIN_APPS, updateAllMinApps }
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { DEFAULT_MIN_APPS } from '@renderer/config/minapps'
|
||||
import { allMinApps } from '@renderer/config/minapps'
|
||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { useSettings } from '@renderer/hooks/useSettings' // 使用设置中的值
|
||||
import NavigationService from '@renderer/services/NavigationService'
|
||||
@ -120,10 +120,10 @@ export const useMinappPopup = () => {
|
||||
[openMinapp]
|
||||
)
|
||||
|
||||
/** Open a minapp by id (look up the minapp in DEFAULT_MIN_APPS) */
|
||||
/** Open a minapp by id (look up the minapp in allMinApps) */
|
||||
const openMinappById = useCallback(
|
||||
(id: string, keepAlive: boolean = false) => {
|
||||
const app = DEFAULT_MIN_APPS.find((app) => app?.id === id)
|
||||
const app = allMinApps.find((app) => app?.id === id)
|
||||
if (app) {
|
||||
openMinapp(app, keepAlive)
|
||||
}
|
||||
|
||||
@ -1,25 +1,129 @@
|
||||
import { DEFAULT_MIN_APPS } from '@renderer/config/minapps'
|
||||
import { allMinApps } from '@renderer/config/minapps'
|
||||
import type { RootState } from '@renderer/store'
|
||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import { setDisabledMinApps, setMinApps, setPinnedMinApps } from '@renderer/store/minapps'
|
||||
import type { MinAppType } from '@renderer/types'
|
||||
import type { LanguageVarious, MinAppType } from '@renderer/types'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
|
||||
/**
|
||||
* Data Flow Design:
|
||||
*
|
||||
* PRINCIPLE: Locale filtering is a VIEW concern, not a DATA concern.
|
||||
*
|
||||
* - Redux stores ALL apps (including locale-restricted ones) to preserve user preferences
|
||||
* - allMinApps is the template data source containing locale definitions
|
||||
* - This hook applies locale filtering only when READING for UI display
|
||||
* - When WRITING, locale-hidden apps are merged back to prevent data loss
|
||||
*/
|
||||
|
||||
// Check if app should be visible for the given locale
|
||||
const isVisibleForLocale = (app: MinAppType, language: LanguageVarious): boolean => {
|
||||
if (!app.locales) return true
|
||||
return app.locales.includes(language)
|
||||
}
|
||||
|
||||
// Filter apps by locale - only show apps that match current language
|
||||
const filterByLocale = (apps: MinAppType[], language: LanguageVarious): MinAppType[] => {
|
||||
return apps.filter((app) => isVisibleForLocale(app, language))
|
||||
}
|
||||
|
||||
// Get locale-hidden apps from allMinApps for the current language
|
||||
// This uses allMinApps as source of truth for locale definitions
|
||||
const getLocaleHiddenApps = (language: LanguageVarious): MinAppType[] => {
|
||||
return allMinApps.filter((app) => !isVisibleForLocale(app, language))
|
||||
}
|
||||
|
||||
export const useMinapps = () => {
|
||||
const { enabled, disabled, pinned } = useAppSelector((state: RootState) => state.minapps)
|
||||
const language = useAppSelector((state: RootState) => state.settings.language)
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const mapApps = useCallback(
|
||||
(apps: MinAppType[]) => apps.map((app) => allMinApps.find((item) => item.id === app.id) || app),
|
||||
[]
|
||||
)
|
||||
|
||||
const getAllApps = useCallback(
|
||||
(apps: MinAppType[], disabledApps: MinAppType[]) => {
|
||||
const mappedApps = mapApps(apps)
|
||||
const existingIds = new Set(mappedApps.map((app) => app.id))
|
||||
const disabledIds = new Set(disabledApps.map((app) => app.id))
|
||||
const missingApps = allMinApps.filter((app) => !existingIds.has(app.id) && !disabledIds.has(app.id))
|
||||
return [...mappedApps, ...missingApps]
|
||||
},
|
||||
[mapApps]
|
||||
)
|
||||
|
||||
// READ: Get apps filtered by locale for UI display
|
||||
const minapps = useMemo(() => {
|
||||
const allApps = getAllApps(enabled, disabled)
|
||||
const disabledIds = new Set(disabled.map((app) => app.id))
|
||||
const withoutDisabled = allApps.filter((app) => !disabledIds.has(app.id))
|
||||
return filterByLocale(withoutDisabled, language)
|
||||
}, [enabled, disabled, language, getAllApps])
|
||||
|
||||
const disabledApps = useMemo(() => filterByLocale(mapApps(disabled), language), [disabled, language, mapApps])
|
||||
const pinnedApps = useMemo(() => filterByLocale(mapApps(pinned), language), [pinned, language, mapApps])
|
||||
|
||||
const updateMinapps = useCallback(
|
||||
(visibleApps: MinAppType[]) => {
|
||||
const disabledIds = new Set(disabled.map((app) => app.id))
|
||||
|
||||
const withoutDisabled = visibleApps.filter((app) => !disabledIds.has(app.id))
|
||||
|
||||
const localeHiddenApps = getLocaleHiddenApps(language)
|
||||
|
||||
const localeHiddenIds = new Set(localeHiddenApps.map((app) => app.id))
|
||||
const preservedLocaleHidden = enabled.filter((app) => localeHiddenIds.has(app.id) && !disabledIds.has(app.id))
|
||||
|
||||
const visibleIds = new Set(withoutDisabled.map((app) => app.id))
|
||||
const toAppend = preservedLocaleHidden.filter((app) => !visibleIds.has(app.id))
|
||||
const merged = [...withoutDisabled, ...toAppend]
|
||||
|
||||
const existingIds = new Set(merged.map((app) => app.id))
|
||||
const missingApps = allMinApps.filter((app) => !existingIds.has(app.id) && !disabledIds.has(app.id))
|
||||
|
||||
dispatch(setMinApps([...merged, ...missingApps]))
|
||||
},
|
||||
[dispatch, enabled, disabled, language]
|
||||
)
|
||||
|
||||
// WRITE: Update disabled apps, preserving locale-hidden disabled apps
|
||||
const updateDisabledMinapps = useCallback(
|
||||
(visibleDisabledApps: MinAppType[]) => {
|
||||
const localeHiddenApps = getLocaleHiddenApps(language)
|
||||
const localeHiddenIds = new Set(localeHiddenApps.map((app) => app.id))
|
||||
const preservedLocaleHidden = disabled.filter((app) => localeHiddenIds.has(app.id))
|
||||
|
||||
const visibleIds = new Set(visibleDisabledApps.map((app) => app.id))
|
||||
const toAppend = preservedLocaleHidden.filter((app) => !visibleIds.has(app.id))
|
||||
|
||||
dispatch(setDisabledMinApps([...visibleDisabledApps, ...toAppend]))
|
||||
},
|
||||
[dispatch, disabled, language]
|
||||
)
|
||||
|
||||
// WRITE: Update pinned apps, preserving locale-hidden pinned apps
|
||||
const updatePinnedMinapps = useCallback(
|
||||
(visiblePinnedApps: MinAppType[]) => {
|
||||
const localeHiddenApps = getLocaleHiddenApps(language)
|
||||
const localeHiddenIds = new Set(localeHiddenApps.map((app) => app.id))
|
||||
const preservedLocaleHidden = pinned.filter((app) => localeHiddenIds.has(app.id))
|
||||
|
||||
const visibleIds = new Set(visiblePinnedApps.map((app) => app.id))
|
||||
const toAppend = preservedLocaleHidden.filter((app) => !visibleIds.has(app.id))
|
||||
|
||||
dispatch(setPinnedMinApps([...visiblePinnedApps, ...toAppend]))
|
||||
},
|
||||
[dispatch, pinned, language]
|
||||
)
|
||||
|
||||
return {
|
||||
minapps: enabled.map((app) => DEFAULT_MIN_APPS.find((item) => item.id === app.id) || app),
|
||||
disabled: disabled.map((app) => DEFAULT_MIN_APPS.find((item) => item.id === app.id) || app),
|
||||
pinned: pinned.map((app) => DEFAULT_MIN_APPS.find((item) => item.id === app.id) || app),
|
||||
updateMinapps: (minapps: MinAppType[]) => {
|
||||
dispatch(setMinApps(minapps))
|
||||
},
|
||||
updateDisabledMinapps: (minapps: MinAppType[]) => {
|
||||
dispatch(setDisabledMinApps(minapps))
|
||||
},
|
||||
updatePinnedMinapps: (minapps: MinAppType[]) => {
|
||||
dispatch(setPinnedMinApps(minapps))
|
||||
}
|
||||
minapps,
|
||||
disabled: disabledApps,
|
||||
pinned: pinnedApps,
|
||||
updateMinapps,
|
||||
updateDisabledMinapps,
|
||||
updatePinnedMinapps
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { loggerService } from '@logger'
|
||||
import { DEFAULT_MIN_APPS } from '@renderer/config/minapps'
|
||||
import { allMinApps } from '@renderer/config/minapps'
|
||||
import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
|
||||
import { useMinapps } from '@renderer/hooks/useMinapps'
|
||||
import { useNavbarPosition } from '@renderer/hooks/useSettings'
|
||||
@ -51,7 +51,7 @@ const MinAppPage: FC = () => {
|
||||
if (!appId) return null
|
||||
|
||||
// First try to find in default and custom mini-apps
|
||||
let foundApp = [...DEFAULT_MIN_APPS, ...minapps].find((app) => app.id === appId)
|
||||
let foundApp = [...allMinApps, ...minapps].find((app) => app.id === appId)
|
||||
|
||||
// If not found and we have cache, try to find in cache (for temporary apps)
|
||||
if (!foundApp && minAppsCache) {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { CloseOutlined } from '@ant-design/icons'
|
||||
import type { DraggableProvided, DroppableProvided, DropResult } from '@hello-pangea/dnd'
|
||||
import { DragDropContext, Draggable, Droppable } from '@hello-pangea/dnd'
|
||||
import { DEFAULT_MIN_APPS } from '@renderer/config/minapps'
|
||||
import { allMinApps } from '@renderer/config/minapps'
|
||||
import { useMinapps } from '@renderer/hooks/useMinapps'
|
||||
import { getMiniappsStatusLabel } from '@renderer/i18n/label'
|
||||
import type { MinAppType } from '@renderer/types'
|
||||
@ -91,7 +91,9 @@ const MiniAppIconsManager: FC<MiniAppManagerProps> = ({
|
||||
)
|
||||
|
||||
const renderProgramItem = (program: MinAppType, provided: DraggableProvided, listType: ListType) => {
|
||||
const { name, logo } = DEFAULT_MIN_APPS.find((app) => app.id === program.id) || { name: program.name, logo: '' }
|
||||
const appData = allMinApps.find((app) => app.id === program.id)
|
||||
const name = appData?.nameKey ? t(appData.nameKey) : appData?.name || program.name
|
||||
const logo = appData?.logo || ''
|
||||
|
||||
return (
|
||||
<ProgramItem ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { UndoOutlined } from '@ant-design/icons' // 导入重置图标
|
||||
import { DEFAULT_MIN_APPS } from '@renderer/config/minapps'
|
||||
import { allMinApps } from '@renderer/config/minapps'
|
||||
import { useMinapps } from '@renderer/hooks/useMinapps'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { SettingDescription, SettingDivider, SettingRowTitle, SettingTitle } from '@renderer/pages/settings'
|
||||
@ -32,9 +32,9 @@ const MiniAppSettings: FC = () => {
|
||||
const debounceTimerRef = useRef<NodeJS.Timeout | null>(null)
|
||||
|
||||
const handleResetMinApps = useCallback(() => {
|
||||
setVisibleMiniApps(DEFAULT_MIN_APPS)
|
||||
setVisibleMiniApps(allMinApps)
|
||||
setDisabledMiniApps([])
|
||||
updateMinapps(DEFAULT_MIN_APPS)
|
||||
updateMinapps(allMinApps)
|
||||
updateDisabledMinapps([])
|
||||
}, [updateDisabledMinapps, updateMinapps])
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { PlusOutlined, UploadOutlined } from '@ant-design/icons'
|
||||
import { loggerService } from '@logger'
|
||||
import { loadCustomMiniApp, ORIGIN_DEFAULT_MIN_APPS, updateDefaultMinApps } from '@renderer/config/minapps'
|
||||
import { loadCustomMiniApp, ORIGIN_DEFAULT_MIN_APPS, updateAllMinApps } from '@renderer/config/minapps'
|
||||
import { useMinapps } from '@renderer/hooks/useMinapps'
|
||||
import type { MinAppType } from '@renderer/types'
|
||||
import { Button, Form, Input, Modal, Radio, Upload } from 'antd'
|
||||
@ -60,7 +60,7 @@ const NewAppButton: FC<Props> = ({ size = 60 }) => {
|
||||
form.resetFields()
|
||||
setFileList([])
|
||||
const reloadedApps = [...ORIGIN_DEFAULT_MIN_APPS, ...(await loadCustomMiniApp())]
|
||||
updateDefaultMinApps(reloadedApps)
|
||||
updateAllMinApps(reloadedApps)
|
||||
updateMinapps([...minapps, newApp])
|
||||
} catch (error) {
|
||||
window.toast.error(t('settings.miniapps.custom.save_error'))
|
||||
|
||||
@ -10,7 +10,7 @@ import {
|
||||
} from '@ant-design/icons'
|
||||
import { loggerService } from '@logger'
|
||||
import { isDev } from '@renderer/config/constant'
|
||||
import { DEFAULT_MIN_APPS } from '@renderer/config/minapps'
|
||||
import { allMinApps } from '@renderer/config/minapps'
|
||||
import { useMinapps } from '@renderer/hooks/useMinapps'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
@ -50,7 +50,7 @@ const MinimalToolbar: FC<Props> = ({ app, webviewRef, currentUrl, onReload, onOp
|
||||
const navigate = useNavigate()
|
||||
const [canGoBack, setCanGoBack] = useState(false)
|
||||
const [canGoForward, setCanGoForward] = useState(false)
|
||||
const canPinned = DEFAULT_MIN_APPS.some((item) => item.id === app.id)
|
||||
const canPinned = allMinApps.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://')
|
||||
|
||||
|
||||
@ -22,7 +22,7 @@ import {
|
||||
DEFAULT_TEMPERATURE,
|
||||
isMac
|
||||
} from '@renderer/config/constant'
|
||||
import { DEFAULT_MIN_APPS } from '@renderer/config/minapps'
|
||||
import { allMinApps } from '@renderer/config/minapps'
|
||||
import {
|
||||
glm45FlashModel,
|
||||
isFunctionCallingModel,
|
||||
@ -99,7 +99,7 @@ function removeMiniAppFromState(state: RootState, id: string) {
|
||||
|
||||
function addMiniApp(state: RootState, id: string) {
|
||||
if (state.minapps) {
|
||||
const app = DEFAULT_MIN_APPS.find((app) => app.id === id)
|
||||
const app = allMinApps.find((app) => app.id === id)
|
||||
if (app) {
|
||||
if (!state.minapps.enabled.find((app) => app.id === id)) {
|
||||
state.minapps.enabled.push(app)
|
||||
@ -1076,7 +1076,7 @@ const migrateConfig = {
|
||||
|
||||
if (state.minapps) {
|
||||
appIds.forEach((id) => {
|
||||
const app = DEFAULT_MIN_APPS.find((app) => app.id === id)
|
||||
const app = allMinApps.find((app) => app.id === id)
|
||||
if (app) {
|
||||
state.minapps.enabled.push(app)
|
||||
}
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
*/
|
||||
import type { PayloadAction } from '@reduxjs/toolkit'
|
||||
import { createSlice } from '@reduxjs/toolkit'
|
||||
import { DEFAULT_MIN_APPS } from '@renderer/config/minapps'
|
||||
import { allMinApps } from '@renderer/config/minapps'
|
||||
import type { MinAppType } from '@renderer/types'
|
||||
|
||||
export interface MinAppsState {
|
||||
@ -26,7 +26,7 @@ export interface MinAppsState {
|
||||
}
|
||||
|
||||
const initialState: MinAppsState = {
|
||||
enabled: DEFAULT_MIN_APPS,
|
||||
enabled: allMinApps,
|
||||
disabled: [],
|
||||
pinned: []
|
||||
}
|
||||
|
||||
@ -459,6 +459,10 @@ export interface PaintingsState {
|
||||
export type MinAppType = {
|
||||
id: string
|
||||
name: string
|
||||
/** i18n key for translatable names */
|
||||
nameKey?: string
|
||||
/** Locale codes where this app should be visible (e.g., ['zh-CN', 'zh-TW']) */
|
||||
locales?: LanguageVarious[]
|
||||
logo?: string
|
||||
url: string
|
||||
// FIXME: It should be `bordered`
|
||||
|
||||
@ -9,9 +9,9 @@ import { afterEach, beforeEach, describe, expect, it, test, vi } from 'vitest'
|
||||
vi.mock('@renderer/config/minapps', () => {
|
||||
return {
|
||||
ORIGIN_DEFAULT_MIN_APPS: [],
|
||||
DEFAULT_MIN_APPS: [],
|
||||
allMinApps: [],
|
||||
loadCustomMiniApp: async () => [],
|
||||
updateDefaultMinApps: vi.fn()
|
||||
updateAllMinApps: vi.fn()
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user