mirror of
https://github.com/DrizzleTime/Foxel.git
synced 2026-05-17 13:37:36 +08:00
feat: Add App Center plugin functionality
This commit is contained in:
57
web/src/apps/PluginHost/index.tsx
Normal file
57
web/src/apps/PluginHost/index.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import React, { useRef, useState } from 'react';
|
||||
import type { AppComponentProps } from '../types';
|
||||
import { vfsApi } from '../../api/vfs';
|
||||
import { loadPluginFromUrl, ensureManifest, type RegisteredPlugin } from '../../plugins/runtime';
|
||||
import type { PluginItem } from '../../api/plugins';
|
||||
import { useAsyncSafeEffect } from '../../hooks/useAsyncSafeEffect';
|
||||
|
||||
export interface PluginAppHostProps extends AppComponentProps {
|
||||
plugin: PluginItem;
|
||||
}
|
||||
|
||||
export const PluginAppHost: React.FC<PluginAppHostProps> = ({ plugin, filePath, entry, onRequestClose }) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const onCloseRef = useRef(onRequestClose);
|
||||
onCloseRef.current = onRequestClose;
|
||||
|
||||
const pluginRef = useRef<RegisteredPlugin | null>(null);
|
||||
|
||||
useAsyncSafeEffect(
|
||||
async ({ isDisposed }) => {
|
||||
try {
|
||||
const p = await loadPluginFromUrl(plugin.url);
|
||||
if (isDisposed()) return;
|
||||
pluginRef.current = p;
|
||||
await ensureManifest(plugin.id, p);
|
||||
if (isDisposed()) return;
|
||||
const token = await vfsApi.getTempLinkToken(filePath);
|
||||
if (isDisposed()) return;
|
||||
const downloadUrl = vfsApi.getTempPublicUrl(token.token);
|
||||
if (isDisposed() || !containerRef.current) return;
|
||||
await p.mount(containerRef.current, {
|
||||
filePath,
|
||||
entry,
|
||||
urls: { downloadUrl },
|
||||
host: { close: () => onCloseRef.current() },
|
||||
});
|
||||
} catch (e: any) {
|
||||
if (!isDisposed()) setError(e?.message || '插件运行失败');
|
||||
}
|
||||
},
|
||||
[plugin.id, plugin.url, filePath],
|
||||
() => {
|
||||
try {
|
||||
if (pluginRef.current?.unmount && containerRef.current) {
|
||||
pluginRef.current.unmount(containerRef.current);
|
||||
}
|
||||
} catch {}
|
||||
},
|
||||
);
|
||||
|
||||
if (error) {
|
||||
return <div style={{ padding: 12, color: 'red' }}>插件错误: {error}</div>;
|
||||
}
|
||||
|
||||
return <div ref={containerRef} style={{ width: '100%', height: '100%', overflow: 'auto' }} />;
|
||||
};
|
||||
@@ -1,9 +1,11 @@
|
||||
import type { VfsEntry } from '../api/client';
|
||||
import type { AppDescriptor } from './types';
|
||||
import React from 'react';
|
||||
import { pluginsApi, type PluginItem } from '../api/plugins';
|
||||
import { PluginAppHost } from './PluginHost';
|
||||
const apps: AppDescriptor[] = [];
|
||||
|
||||
// 使用 import.meta.glob 动态导入所有应用
|
||||
// vite-glob-ignore
|
||||
const appModules = import.meta.glob('./*/index.ts');
|
||||
|
||||
async function loadApps() {
|
||||
@@ -16,11 +18,34 @@ async function loadApps() {
|
||||
}
|
||||
}
|
||||
}
|
||||
try {
|
||||
const items = await pluginsApi.list();
|
||||
items.filter(p => p.enabled !== false).forEach((p) => registerPluginAsApp(p));
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
|
||||
// 立即加载并注册所有应用
|
||||
loadApps();
|
||||
function registerPluginAsApp(p: PluginItem) {
|
||||
const key = 'plugin:' + p.id;
|
||||
if (apps.find(a => a.key === key)) return;
|
||||
const supported = (entry: VfsEntry) => {
|
||||
if (entry.is_dir) return false;
|
||||
const ext = entry.name.split('.').pop()?.toLowerCase() || '';
|
||||
if (!p.supported_exts || p.supported_exts.length === 0) return true;
|
||||
return p.supported_exts.includes(ext);
|
||||
};
|
||||
apps.push({
|
||||
key,
|
||||
name: p.name || `插件 ${p.id}`,
|
||||
supported,
|
||||
component: (props: any) => React.createElement(PluginAppHost, { plugin: p, ...props }),
|
||||
default: false,
|
||||
defaultBounds: p.default_bounds || undefined,
|
||||
defaultMaximized: p.default_maximized || undefined,
|
||||
});
|
||||
}
|
||||
|
||||
loadApps();
|
||||
|
||||
export function getAppsForEntry(entry: VfsEntry): AppDescriptor[] {
|
||||
return apps.filter(a => a.supported(entry));
|
||||
@@ -43,3 +68,27 @@ export function getDefaultAppForEntry(entry: VfsEntry): AppDescriptor | undefine
|
||||
|
||||
export type { AppDescriptor };
|
||||
export type { AppComponentProps } from './types';
|
||||
|
||||
export async function reloadPluginApps() {
|
||||
try {
|
||||
const items = await pluginsApi.list();
|
||||
const keepKeys = new Set(items.filter(p => p.enabled !== false).map(p => 'plugin:' + p.id));
|
||||
for (let i = apps.length - 1; i >= 0; i--) {
|
||||
const a = apps[i];
|
||||
if (a.key.startsWith('plugin:') && !keepKeys.has(a.key)) {
|
||||
apps.splice(i, 1);
|
||||
}
|
||||
}
|
||||
items.filter(p => p.enabled !== false).forEach(p => {
|
||||
const key = 'plugin:' + p.id;
|
||||
const existing = apps.find(a => a.key === key);
|
||||
if (!existing) {
|
||||
registerPluginAsApp(p);
|
||||
} else {
|
||||
existing.name = p.name || `插件 ${p.id}`;
|
||||
existing.defaultBounds = p.default_bounds || undefined;
|
||||
existing.defaultMaximized = p.default_maximized || undefined;
|
||||
}
|
||||
});
|
||||
} catch { }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user