mirror of
https://github.com/DrizzleTime/Foxel.git
synced 2026-05-07 08:13:03 +08:00
feat: add search functionality to fetchFoxelCoreApps and enhance PluginsPage with query handling
This commit is contained in:
@@ -113,9 +113,13 @@ export async function fetchRepoList(params: RepoQueryParams = {}): Promise<RepoL
|
||||
/**
|
||||
* 从 foxel-core 应用中心获取应用列表
|
||||
*/
|
||||
export async function fetchFoxelCoreApps(): Promise<FoxelCoreApp[]> {
|
||||
const url = `${FOXEL_CORE_BASE}/api/apps`;
|
||||
const resp = await fetch(url);
|
||||
export async function fetchFoxelCoreApps(query?: string): Promise<FoxelCoreApp[]> {
|
||||
const url = new URL('/api/apps', FOXEL_CORE_BASE);
|
||||
const q = query?.trim();
|
||||
if (q) {
|
||||
url.searchParams.set('q', q);
|
||||
}
|
||||
const resp = await fetch(url.href);
|
||||
if (!resp.ok) {
|
||||
throw new Error(`Failed to fetch apps: ${resp.status}`);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { VfsEntry } from '../api/client';
|
||||
import type { AppDescriptor } from '../apps/registry';
|
||||
import { getAppsForEntry, getDefaultAppForEntry, getAppByKey } from '../apps/registry';
|
||||
import { useI18n } from '../i18n';
|
||||
import { useNavigate } from 'react-router';
|
||||
|
||||
type WindowBase = {
|
||||
id: string;
|
||||
@@ -47,6 +48,7 @@ const AppWindowsContext = createContext<AppWindowsContextValue | null>(null);
|
||||
|
||||
export const AppWindowsProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const { t } = useI18n();
|
||||
const navigate = useNavigate();
|
||||
const [windows, setWindows] = useState<AppWindowItem[]>([]);
|
||||
|
||||
const openWithApp = useCallback((entry: VfsEntry, app: AppDescriptor, currentPath: string) => {
|
||||
@@ -120,12 +122,27 @@ export const AppWindowsProvider: React.FC<{ children: React.ReactNode }> = ({ ch
|
||||
const openFileWithDefaultApp = useCallback((entry: VfsEntry, currentPath: string) => {
|
||||
const apps = getAppsForEntry(entry);
|
||||
if (!apps.length) {
|
||||
Modal.error({ title: t('Cannot open file: no available app') });
|
||||
const ext = entry.name.split('.').pop()?.toLowerCase() || '';
|
||||
const extQuery = ext ? `.${ext}` : entry.name;
|
||||
Modal.confirm({
|
||||
title: t('Cannot open file'),
|
||||
content: t('No app available for this file. Go to App Store to search {ext}?', { ext: extQuery }),
|
||||
okText: t('Go to App Store'),
|
||||
cancelText: t('Cancel'),
|
||||
onOk: () => {
|
||||
const params = new URLSearchParams();
|
||||
params.set('tab', 'discover');
|
||||
if (extQuery) {
|
||||
params.set('query', extQuery);
|
||||
}
|
||||
navigate(`/plugins?${params.toString()}`);
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
const defaultApp = getDefaultAppForEntry(entry) || apps[0];
|
||||
openWithApp(entry, defaultApp, currentPath);
|
||||
}, [openWithApp, t]);
|
||||
}, [navigate, openWithApp, t]);
|
||||
|
||||
const confirmOpenWithApp = useCallback((entry: VfsEntry, appKey: string, currentPath: string) => {
|
||||
const app = getAppByKey(appKey);
|
||||
|
||||
@@ -682,7 +682,10 @@
|
||||
"Finish Initialization": "Finish Initialization",
|
||||
"Plugin run failed": "Plugin run failed",
|
||||
"Plugin Error": "Plugin Error",
|
||||
"Cannot open file": "Cannot open file",
|
||||
"Cannot open file: no available app": "Cannot open file: no available app",
|
||||
"No app available for this file. Go to App Store to search {ext}?": "No app available for this file. Go to App Store to search {ext}?",
|
||||
"Go to App Store": "Go to App Store",
|
||||
"Error": "Error",
|
||||
"App \"{key}\" not found.": "App \"{key}\" not found.",
|
||||
"Open with {app}": "Open with {app}",
|
||||
|
||||
@@ -675,7 +675,10 @@
|
||||
"Finish Initialization": "完成初始化",
|
||||
"Plugin run failed": "插件运行失败",
|
||||
"Plugin Error": "插件错误",
|
||||
"Cannot open file": "无法打开该文件",
|
||||
"Cannot open file: no available app": "无法打开该文件:没有可用的应用",
|
||||
"No app available for this file. Go to App Store to search {ext}?": "没有可用的应用。是否前往应用商店搜索 {ext} 可安装的应用?",
|
||||
"Go to App Store": "去应用商店",
|
||||
"Error": "错误",
|
||||
"App \"{key}\" not found.": "应用 \"{key}\" 不存在。",
|
||||
"Open with {app}": "使用 {app} 打开",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { memo, useCallback, useEffect, useMemo, useState, type ReactNode } from 'react';
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from 'react';
|
||||
import { Alert, Button, Card, Collapse, Empty, List, Modal, Popconfirm, Progress, Skeleton, Space, Tabs, Tag, Typography, Upload, message, theme } from 'antd';
|
||||
import { GithubOutlined, LinkOutlined, UploadOutlined, DownloadOutlined, ReloadOutlined } from '@ant-design/icons';
|
||||
import { pluginsApi, type PluginItem } from '../api/plugins';
|
||||
@@ -9,6 +9,7 @@ import { getPluginAssetUrl } from '../plugins/runtime';
|
||||
import { fetchFoxelCoreAppDetail, fetchFoxelCoreApps, downloadFoxelCoreApp, type FoxelCoreApp, type FoxelCoreAppDetail } from '../api/pluginCenter';
|
||||
import type { UploadFile } from 'antd';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import { useLocation } from 'react-router';
|
||||
|
||||
type InstallStatus = 'pending' | 'installing' | 'success' | 'failed' | 'skipped';
|
||||
type InstallState = Partial<{
|
||||
@@ -162,6 +163,9 @@ const PluginsPage = memo(function PluginsPage() {
|
||||
const { token } = theme.useToken();
|
||||
const { t, lang } = useI18n();
|
||||
const { openApp } = useAppWindows();
|
||||
const location = useLocation();
|
||||
const discoverySearchInitializedRef = useRef(false);
|
||||
const discoverySearchTimerRef = useRef<number | null>(null);
|
||||
|
||||
const reload = useCallback(async () => {
|
||||
try { setLoading(true); setData(await pluginsApi.list()); } finally { setLoading(false); }
|
||||
@@ -169,12 +173,12 @@ const PluginsPage = memo(function PluginsPage() {
|
||||
|
||||
useEffect(() => { void reload(); }, [reload]);
|
||||
|
||||
// 加载应用发现列表
|
||||
const loadDiscoveryApps = useCallback(async () => {
|
||||
// 加载应用发现列表(带搜索)
|
||||
const loadDiscoveryApps = useCallback(async (query: string) => {
|
||||
try {
|
||||
setDiscoveryLoading(true);
|
||||
setDiscoveryError(undefined);
|
||||
const apps = await fetchFoxelCoreApps();
|
||||
const apps = await fetchFoxelCoreApps(query);
|
||||
setDiscoveryApps(apps);
|
||||
} catch (e: any) {
|
||||
setDiscoveryError(e?.message || t('Failed to load apps'));
|
||||
@@ -184,10 +188,46 @@ const PluginsPage = memo(function PluginsPage() {
|
||||
}, [t]);
|
||||
|
||||
useEffect(() => {
|
||||
if (tab === 'discover' && discoveryApps.length === 0 && !discoveryError) {
|
||||
void loadDiscoveryApps();
|
||||
const params = new URLSearchParams(location.search);
|
||||
const tabParam = params.get('tab');
|
||||
const queryParam = (params.get('query') ?? params.get('q') ?? '').trim();
|
||||
|
||||
if (tabParam === 'installed' || tabParam === 'discover') {
|
||||
setTab(tabParam);
|
||||
} else if (queryParam) {
|
||||
setTab('discover');
|
||||
}
|
||||
}, [discoveryApps.length, discoveryError, loadDiscoveryApps, tab]);
|
||||
|
||||
setDiscoverySearch(queryParam);
|
||||
}, [location.search]);
|
||||
|
||||
useEffect(() => {
|
||||
if (tab !== 'discover') {
|
||||
discoverySearchInitializedRef.current = false;
|
||||
if (discoverySearchTimerRef.current !== null) {
|
||||
window.clearTimeout(discoverySearchTimerRef.current);
|
||||
discoverySearchTimerRef.current = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const delay = discoverySearchInitializedRef.current ? 500 : 0;
|
||||
if (discoverySearchTimerRef.current !== null) {
|
||||
window.clearTimeout(discoverySearchTimerRef.current);
|
||||
}
|
||||
|
||||
discoverySearchTimerRef.current = window.setTimeout(() => {
|
||||
discoverySearchInitializedRef.current = true;
|
||||
void loadDiscoveryApps(discoverySearch);
|
||||
}, delay);
|
||||
|
||||
return () => {
|
||||
if (discoverySearchTimerRef.current !== null) {
|
||||
window.clearTimeout(discoverySearchTimerRef.current);
|
||||
discoverySearchTimerRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [discoverySearch, loadDiscoveryApps, tab]);
|
||||
|
||||
const closeDetailModal = () => {
|
||||
setDetailOpen(false);
|
||||
@@ -383,25 +423,6 @@ const PluginsPage = memo(function PluginsPage() {
|
||||
));
|
||||
}, [data, q, resolvePluginTexts]);
|
||||
|
||||
// 过滤应用发现列表
|
||||
const filteredDiscoveryApps = useMemo(() => {
|
||||
const s = discoverySearch.trim().toLowerCase();
|
||||
if (!s) return discoveryApps;
|
||||
return discoveryApps.filter(app => {
|
||||
const name = (lang === 'zh' ? app.name.zh : app.name.en).toLowerCase();
|
||||
const desc = (lang === 'zh' ? app.description.zh : app.description.en).toLowerCase();
|
||||
const author = (app.author || '').toLowerCase();
|
||||
const tags = lang === 'zh' ? app.tags.zh : app.tags.en;
|
||||
return (
|
||||
name.includes(s) ||
|
||||
desc.includes(s) ||
|
||||
author.includes(s) ||
|
||||
tags.some(tag => tag.toLowerCase().includes(s)) ||
|
||||
app.key.toLowerCase().includes(s)
|
||||
);
|
||||
});
|
||||
}, [discoveryApps, discoverySearch, lang]);
|
||||
|
||||
// 检查应用是否已安装
|
||||
const isAppInstalled = (appKey: string) => {
|
||||
return data.some(p => p.key === appKey);
|
||||
@@ -593,7 +614,7 @@ const PluginsPage = memo(function PluginsPage() {
|
||||
/>
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={loadDiscoveryApps}
|
||||
onClick={() => { void loadDiscoveryApps(discoverySearch); }}
|
||||
loading={discoveryLoading}
|
||||
>
|
||||
{t('Refresh')}
|
||||
@@ -610,13 +631,13 @@ const PluginsPage = memo(function PluginsPage() {
|
||||
</div>
|
||||
) : discoveryError ? (
|
||||
<Empty description={discoveryError}>
|
||||
<Button onClick={loadDiscoveryApps}>{t('Refresh')}</Button>
|
||||
<Button onClick={() => { void loadDiscoveryApps(discoverySearch); }}>{t('Refresh')}</Button>
|
||||
</Empty>
|
||||
) : filteredDiscoveryApps.length === 0 ? (
|
||||
<Empty description={discoveryApps.length === 0 ? t('No results') : t('No results')} />
|
||||
) : discoveryApps.length === 0 ? (
|
||||
<Empty description={t('No results')} />
|
||||
) : (
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))', gap: 12, justifyContent: 'start' }}>
|
||||
{filteredDiscoveryApps.map(app => {
|
||||
{discoveryApps.map(app => {
|
||||
const name = lang === 'zh' ? app.name.zh : app.name.en;
|
||||
const description = lang === 'zh' ? app.description.zh : app.description.en;
|
||||
const tags = lang === 'zh' ? app.tags.zh : app.tags.en;
|
||||
|
||||
Reference in New Issue
Block a user