feat: 插件中心与插件管理

pull/13/head
moonrailgun 4 years ago
parent 4971bed09c
commit b95d47f9fa

@ -70,6 +70,7 @@ export {
} from './model/group';
export type { GroupPanel, GroupInfo, GroupBasicInfo } from './model/group';
export type { ChatMessage } from './model/message';
export type { PluginManifest } from './model/plugin';
export type { UserBaseInfo, UserLoginInfo } from './model/user';
export {
loginWithEmail,

@ -0,0 +1,48 @@
export interface PluginManifest {
/**
*
* @example
*/
label: string;
/**
* ,
* @example com.msgbyte.webview
*/
name: string;
/**
*
*/
url: string;
/**
*
* : 128x128
*/
icon?: string;
/**
*
* semver
*
* major.minor.patch
* @example 1.0.0
*/
version: string;
/**
*
*/
author: string;
/**
*
*/
description: string;
/**
*
*/
requireRestart: boolean;
}

@ -0,0 +1,71 @@
import { Avatar } from '@/components/Avatar';
import { Button } from 'antd';
import React, { useState } from 'react';
import {
PluginManifest,
showToasts,
t,
useAsyncRequest,
} from 'tailchat-shared';
import { pluginManager } from '../manager';
/**
*
*/
export const PluginStoreItem: React.FC<{
manifest: PluginManifest;
installed: boolean;
builtin: boolean;
}> = React.memo((props) => {
const { manifest, builtin } = props;
const [installed, setInstalled] = useState(props.installed);
const [{ loading }, handleInstallPlugin] = useAsyncRequest(async () => {
await pluginManager.installPlugin(manifest);
if (manifest.requireRestart === true) {
showToasts(t('插件安装成功, 需要重启后生效'), 'success');
} else {
showToasts(t('插件安装成功'), 'success');
}
setInstalled(true);
}, [manifest]);
return (
<div className="rounded-md flex w-80 h-36 bg-black bg-opacity-40 py-2 px-3">
<div className="flex w-full">
<div className="mr-2">
<Avatar shape="square" src={manifest.icon} name={manifest.label} />
</div>
<div className="flex flex-col flex-1">
<div className="font-bold">{manifest.label}</div>
<div className="text-xs text-gray-300 text-opacity-50">
{manifest.name}
</div>
<div className="flex-1 overflow-auto">{manifest.description}</div>
<div className="mt-1 text-right">
{builtin ? (
<Button type="primary" disabled={true}>
{t('已安装')}
</Button>
) : installed ? (
<Button type="primary">{t('已安装')}</Button>
) : (
<Button
type="primary"
loading={loading}
onClick={handleInstallPlugin}
>
{t('安装')}
</Button>
)}
</div>
</div>
</div>
</div>
);
});
PluginStoreItem.displayName = 'PluginStoreItem';

@ -0,0 +1,44 @@
/**
*
*/
import { LoadingSpinner } from '@/components/LoadingSpinner';
import { Divider } from 'antd';
import React from 'react';
import { t, useAsync } from 'tailchat-shared';
import { builtinPlugins } from '../builtin';
import { pluginManager } from '../manager';
import { PluginStoreItem } from './Item';
export const PluginStore: React.FC = React.memo(() => {
const { loading, value: installedPluginNameList = [] } =
useAsync(async () => {
const plugins = await pluginManager.getInstalledPlugins();
return plugins.map((p) => p.name);
}, []);
if (loading) {
return <LoadingSpinner tip={t('正在加载已安装插件')} />;
}
return (
<div className="p-2 w-full">
{/* 内置插件 */}
<Divider orientation="left">{t('内置插件')}</Divider>
<div>
{builtinPlugins.map((plugin) => (
<PluginStoreItem
key={plugin.name}
manifest={plugin}
installed={installedPluginNameList.includes(plugin.name)}
builtin={true}
/>
))}
</div>
<Divider orientation="left">{t('插件中心')}</Divider>
</div>
);
});
PluginStore.displayName = 'PluginStore';

@ -0,0 +1,18 @@
import type { PluginManifest } from 'tailchat-shared';
/**
*
*
*
*/
export const builtinPlugins: PluginManifest[] = [
{
label: '网页面板插件',
name: 'com.msgbyte.webview',
url: '/plugins/com.msgbyte.webview/index.js',
version: '0.0.0',
author: 'msgbyte',
description: '为群组提供创建网页面板的功能',
requireRestart: false,
},
];

@ -1,4 +1,5 @@
import { initMiniStar, regDependency, regSharedModule } from 'mini-star';
import { builtinPlugins } from './builtin';
/**
*
@ -7,13 +8,13 @@ export function initPlugins(): Promise<void> {
registerDependencies();
registerModules();
const plugins = builtinPlugins.map(({ name, url }) => ({
name,
url,
}));
return initMiniStar({
plugins: [
{
name: 'com.msgbyte.webview',
url: '/plugins/com.msgbyte.webview/index.js',
},
],
plugins,
});
}

@ -0,0 +1,67 @@
import { getStorage, PluginManifest } from 'tailchat-shared';
import { initMiniStar, loadSinglePlugin } from 'mini-star';
import _once from 'lodash/once';
import { builtinPlugins } from './builtin';
class PluginManager {
/**
* Key
*/
static STORE_KEY = '__installed_plugins';
/**
*
*/
initPlugins = _once(async () => {
const installedPlugins = [
...builtinPlugins,
...(await this.getInstalledPlugins()),
];
const plugins = installedPlugins.map(({ name, url }) => ({
name,
url,
}));
return initMiniStar({
plugins,
});
});
/**
* ()
*/
async getInstalledPlugins(): Promise<PluginManifest[]> {
const plugins: PluginManifest[] = await getStorage().get(
PluginManager.STORE_KEY,
[]
);
return plugins;
}
/**
*
*/
async installPlugin(manifest: PluginManifest) {
const plugins = await this.getInstalledPlugins();
const findedIndex = plugins.findIndex((p) => p.name === manifest.name);
if (findedIndex >= 0) {
// 已安装, 则更新
plugins[findedIndex] = manifest;
} else {
// 未安装, 则推到最后
plugins.push(manifest);
}
await getStorage().save(PluginManager.STORE_KEY, plugins);
await loadSinglePlugin({
name: manifest.name,
url: manifest.url,
});
}
}
export const pluginManager = new PluginManager();

@ -0,0 +1,7 @@
import { PluginStore } from '@/plugin/PluginStore';
import React from 'react';
export const PluginsPanel: React.FC = React.memo(() => {
return <PluginStore />;
});
PluginsPanel.displayName = 'PluginsPanel';

@ -1,9 +1,9 @@
import { IsDeveloping } from '@/components/IsDeveloping';
import React from 'react';
import { Redirect, Route, Switch } from 'react-router-dom';
import { PageContent } from '../PageContent';
import { ConversePanel } from './Converse';
import { FriendPanel } from './Friends';
import { PluginsPanel } from './Plugins';
import { Sidebar } from './Sidebar';
export const Personal: React.FC = React.memo(() => {
@ -12,7 +12,7 @@ export const Personal: React.FC = React.memo(() => {
<Switch>
<Route path="/main/personal/friends" component={FriendPanel} />
<Route path="/main/personal/plugins" component={IsDeveloping} />
<Route path="/main/personal/plugins" component={PluginsPanel} />
<Route
path="/main/personal/converse/:converseId"

@ -35,6 +35,12 @@
background: var(--antd-primary-dangerous-color-hover);
}
}
&[disabled],&[disabled]:hover,&[disabled]:focus,&[disabled]:active {
color: rgba(255,255,255,0.3);
border-color: #434343;
background: #555;
}
}
}

Loading…
Cancel
Save