mirror of
https://github.com/DrizzleTime/Foxel.git
synced 2026-05-10 17:43:35 +08:00
157 lines
4.7 KiB
TypeScript
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;
|
|
}
|
|
}
|