Files
Foxel/web/src/apps/registry.ts
2026-01-06 16:54:49 +08:00

157 lines
4.7 KiB
TypeScript

import type { VfsEntry } from '../api/client';
import type { AppDescriptor } from './types';
import React from 'react';
import { pluginsApi, type PluginItem } from '../api/plugins';
import { PluginAppHost, PluginAppOpenHost } from './PluginHost';
import { getPluginAssetUrl } from '../plugins/runtime';
const apps: AppDescriptor[] = [];
/**
* 获取插件的唯一 key
*/
function getPluginAppKey(p: PluginItem): string {
return `plugin:${p.key}`;
}
/**
* 解析插件图标 URL
* 支持绝对路径、相对路径(插件资源)、外部 URL
*/
function resolvePluginIcon(p: PluginItem): string | undefined {
if (!p.icon) return undefined;
// 外部 URL
if (p.icon.startsWith('http://') || p.icon.startsWith('https://')) {
return p.icon;
}
// 绝对路径
if (p.icon.startsWith('/')) {
return p.icon;
}
// 插件资源路径
return getPluginAssetUrl(p.key, p.icon);
}
function resolvePluginUseSystemWindow(p: PluginItem): boolean | undefined {
const frontend = (p.manifest as any)?.frontend as any;
const value = frontend?.use_system_window ?? frontend?.useSystemWindow;
return typeof value === 'boolean' ? value : undefined;
}
function registerPluginAsApp(p: PluginItem) {
const key = getPluginAppKey(p);
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.key}`,
supported,
component: (props: any) => React.createElement(PluginAppHost, { plugin: p, ...props }),
openAppComponent: p.open_app
? (props: any) => React.createElement(PluginAppOpenHost, { plugin: p, ...props })
: undefined,
iconUrl: resolvePluginIcon(p),
default: false,
defaultBounds: p.default_bounds || undefined,
defaultMaximized: p.default_maximized || undefined,
useSystemWindow: resolvePluginUseSystemWindow(p),
description: p.description || undefined,
author: p.author || undefined,
supportedExts: p.supported_exts || undefined,
website: p.website || undefined,
github: p.github || undefined,
});
}
async function loadApps() {
try {
const items = await pluginsApi.list();
items.forEach((p) => registerPluginAsApp(p));
} catch {
void 0;
}
}
const appsLoadedPromise = loadApps();
export async function ensureAppsLoaded() {
await appsLoadedPromise;
}
export function listPluginApps(): AppDescriptor[] {
return apps;
}
export function getAppsForEntry(entry: VfsEntry): AppDescriptor[] {
return apps.filter((a) => a.supported(entry));
}
export function getAppByKey(key: string): AppDescriptor | undefined {
return apps.find((a) => a.key === key);
}
export function getDefaultAppForEntry(entry: VfsEntry): AppDescriptor | undefined {
if (entry.is_dir) return;
const ext = entry.name.split('.').pop()?.toLowerCase() || '';
if (!ext) return apps.find((a) => a.supported(entry) && a.default);
const saved = localStorage.getItem(`app.default.${ext}`);
if (saved) {
return apps.find((a) => a.key === saved && a.supported(entry)) || undefined;
}
return apps.find((a) => a.supported(entry) && a.default);
}
export type { AppDescriptor };
export type { AppComponentProps } from './types';
export async function reloadPluginApps() {
try {
const items = await pluginsApi.list();
// 生成要保留的 key 集合
const keepKeys = new Set(items.map((p) => getPluginAppKey(p)));
// 移除已卸载的插件应用
for (let i = apps.length - 1; i >= 0; i--) {
const a = apps[i];
if (!keepKeys.has(a.key)) {
apps.splice(i, 1);
}
}
// 更新或添加插件应用
items.forEach((p) => {
const key = getPluginAppKey(p);
const existing = apps.find((a) => a.key === key);
if (!existing) {
registerPluginAsApp(p);
} else {
// 更新现有应用信息
existing.name = p.name || `插件 ${p.key}`;
existing.defaultBounds = p.default_bounds || undefined;
existing.defaultMaximized = p.default_maximized || undefined;
existing.useSystemWindow = resolvePluginUseSystemWindow(p);
existing.iconUrl = resolvePluginIcon(p);
existing.description = p.description || undefined;
existing.author = p.author || undefined;
existing.supportedExts = p.supported_exts || undefined;
existing.openAppComponent = p.open_app
? (props: any) => React.createElement(PluginAppOpenHost, { plugin: p, ...props })
: undefined;
}
});
} catch {
void 0;
}
}