mirror of
https://github.com/DrizzleTime/Foxel.git
synced 2026-08-28 11:36:39 +08:00
Initial commit
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user