@bbki.ng/site 5.5.36 → 5.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/eslint.config.js +7 -0
- package/index.d.ts +0 -2
- package/package.json +2 -2
- package/src/blog/app.tsx +2 -5
- package/src/blog/hooks/use_posts.ts +6 -1
- package/src/blog/pages/extensions/txt/article.tsx +5 -2
- package/src/blog/utils/index.ts +15 -10
- package/src/core/bbplugin.ts +13 -0
- package/src/core/hooks/use_plugins.ts +11 -2
- package/src/core/pluginManager.ts +9 -7
- package/src/core/pluginStore.ts +68 -0
- package/src/plugins/extra-cd/index.ts +6 -19
- package/src/plugins/extra-entry/index.ts +5 -13
- package/src/plugins/manifest.ts +24 -17
- package/src/plugins/now/components/index.tsx +1 -8
- package/src/plugins/now/components/streaming/index.tsx +2 -1
- package/src/plugins/now/index.ts +5 -8
- package/src/plugins/sticker/index.ts +5 -19
- package/src/plugins/store/components/storePage.tsx +102 -0
- package/src/plugins/store/context/index.ts +12 -0
- package/src/plugins/store/index.ts +38 -0
- package/src/plugins/xwy/index.ts +10 -12
- package/src/plugins/xwy/types/index.ts +18 -0
- package/src/types/hostApi.ts +2 -3
- package/src/types/plugin.ts +7 -6
- package/tsconfig.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# @bbki.ng/site
|
|
2
2
|
|
|
3
|
+
## 5.6.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 3eab4b5: add plugin store
|
|
8
|
+
|
|
9
|
+
## 5.5.37
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 56a7bc7: fix 404 error page
|
|
14
|
+
- Updated dependencies [56a7bc7]
|
|
15
|
+
- @bbki.ng/ui@0.2.18
|
|
16
|
+
|
|
3
17
|
## 5.5.36
|
|
4
18
|
|
|
5
19
|
### Patch Changes
|
package/eslint.config.js
CHANGED
|
@@ -9,6 +9,13 @@ const __dirname = path.dirname(__filename);
|
|
|
9
9
|
export default [
|
|
10
10
|
includeIgnoreFile(path.resolve(__dirname, '.gitignore')),
|
|
11
11
|
...webConfig,
|
|
12
|
+
{
|
|
13
|
+
languageOptions: {
|
|
14
|
+
globals: {
|
|
15
|
+
GLOBAL_COMMIT_HASH: 'readonly',
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
},
|
|
12
19
|
{
|
|
13
20
|
rules: {
|
|
14
21
|
// Site specific overrides
|
package/index.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bbki.ng/site",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.6.0",
|
|
4
4
|
"description": "code behind bbki.ng",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"react-dom": "^18.0.0",
|
|
15
15
|
"react-router-dom": "6",
|
|
16
16
|
"swr": "^2.2.5",
|
|
17
|
-
"@bbki.ng/ui": "0.2.
|
|
17
|
+
"@bbki.ng/ui": "0.2.18"
|
|
18
18
|
},
|
|
19
19
|
"devDependencies": {
|
|
20
20
|
"@eslint/compat": "^1.0.0",
|
package/src/blog/app.tsx
CHANGED
|
@@ -9,16 +9,13 @@ import { BBContext } from '@/context/bbcontext';
|
|
|
9
9
|
import { Slot } from '#/core/components/SlotComp';
|
|
10
10
|
import { usePlugins } from '#/core/hooks/use_plugins';
|
|
11
11
|
import { SWR } from '@/swr';
|
|
12
|
-
import type { PluginID } from '#/types/plugin';
|
|
13
12
|
|
|
14
13
|
import { Cover } from './pages';
|
|
15
14
|
import { usePluginEntries } from './hooks/use_plugin_entries';
|
|
16
15
|
import { BaseLayout } from './components/BaseLayout';
|
|
17
16
|
|
|
18
|
-
const APP_PLUGIN_IDS: Array<PluginID> = [/*'sticker',*/ 'xwy', 'extra-cd', 'now'];
|
|
19
|
-
|
|
20
17
|
const AppRoutes = () => {
|
|
21
|
-
usePlugins(
|
|
18
|
+
usePlugins();
|
|
22
19
|
|
|
23
20
|
const pluginEntries = usePluginEntries();
|
|
24
21
|
|
|
@@ -36,8 +33,8 @@ const AppRoutes = () => {
|
|
|
36
33
|
{pluginEntries?.map(route => (
|
|
37
34
|
<Route key={route.path} path={route.path} element={<Slot name="route" data={route} />} />
|
|
38
35
|
))}
|
|
36
|
+
<Route path="*" element={pluginEntries?.length ? <NotFound /> : null} />
|
|
39
37
|
</Route>
|
|
40
|
-
<Route path="*" element={<NotFound />} />
|
|
41
38
|
</Routes>
|
|
42
39
|
);
|
|
43
40
|
};
|
|
@@ -10,6 +10,11 @@ import { useMiddlewareRunner } from '#/core/hooks/useMiddlewareTransData';
|
|
|
10
10
|
const isProd = true;
|
|
11
11
|
const POSTS_API = !isProd ? '/api/posts' : `${API_ENDPOINT}/posts`;
|
|
12
12
|
|
|
13
|
+
interface PostsApiResponse {
|
|
14
|
+
data: IPost[];
|
|
15
|
+
// 可能还有其他字段如 status, message 等
|
|
16
|
+
}
|
|
17
|
+
|
|
13
18
|
export interface TitleListItem {
|
|
14
19
|
name: string;
|
|
15
20
|
to: string;
|
|
@@ -18,7 +23,7 @@ export interface TitleListItem {
|
|
|
18
23
|
}
|
|
19
24
|
|
|
20
25
|
export const usePosts = (name: string = '', suspense?: boolean) => {
|
|
21
|
-
const { data: response, error: swrError } = useSWR(POSTS_API, baseFetcher, {
|
|
26
|
+
const { data: response, error: swrError } = useSWR<PostsApiResponse>(POSTS_API, baseFetcher, {
|
|
22
27
|
revalidateOnFocus: false,
|
|
23
28
|
suspense,
|
|
24
29
|
});
|
|
@@ -6,6 +6,7 @@ import { usePosts } from '@/hooks/use_posts';
|
|
|
6
6
|
import { ArticlePage } from '@/components/article';
|
|
7
7
|
import { useBlogScrollReset } from '@/hooks/use_blog_scroll_pos_restoration';
|
|
8
8
|
import { useMiddlewareTransformedData } from '#/core/hooks/useMiddlewareTransData';
|
|
9
|
+
import { IPost } from '#/types/posts';
|
|
9
10
|
|
|
10
11
|
function TxtArticle() {
|
|
11
12
|
const { title } = useParams();
|
|
@@ -13,7 +14,9 @@ function TxtArticle() {
|
|
|
13
14
|
|
|
14
15
|
useBlogScrollReset();
|
|
15
16
|
|
|
16
|
-
const
|
|
17
|
+
const p = posts as IPost;
|
|
18
|
+
|
|
19
|
+
const transformedContent = useMiddlewareTransformedData('transformPostContent', p?.content);
|
|
17
20
|
|
|
18
21
|
if (!title) {
|
|
19
22
|
return <NotFound />;
|
|
@@ -27,7 +30,7 @@ function TxtArticle() {
|
|
|
27
30
|
return null;
|
|
28
31
|
}
|
|
29
32
|
|
|
30
|
-
const date =
|
|
33
|
+
const date = p.createdAt ? p.createdAt.split('T')[0] : '';
|
|
31
34
|
|
|
32
35
|
return (
|
|
33
36
|
<ArticlePage title={title} date={date}>
|
package/src/blog/utils/index.ts
CHANGED
|
@@ -2,33 +2,38 @@ import { API_ENDPOINT } from '@/constants/routes';
|
|
|
2
2
|
|
|
3
3
|
import { getStableDeviceId } from './fingerprints';
|
|
4
4
|
|
|
5
|
-
type
|
|
5
|
+
type RequestInit = globalThis.RequestInit;
|
|
6
|
+
|
|
7
|
+
type Fetcher = <T>(resource: string, init?: RequestInit) => Promise<T>;
|
|
6
8
|
|
|
7
9
|
export const floatNumberToPercentageString = (num: number): string => {
|
|
8
10
|
return `${num * 100}%`;
|
|
9
11
|
};
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
// 2. 显式声明为 Fetcher 类型,init 默认值改为 RequestInit
|
|
14
|
+
export const baseFetcher: Fetcher = async (resource, init = {}) => {
|
|
12
15
|
const headers = new Headers(init.headers || {});
|
|
13
16
|
const fp = await getStableDeviceId();
|
|
14
17
|
headers.set('X-Device-Fingerprint', fp.id);
|
|
15
|
-
|
|
18
|
+
|
|
19
|
+
const response = await fetch(resource, {
|
|
16
20
|
...init,
|
|
17
21
|
headers,
|
|
18
22
|
mode: 'cors',
|
|
19
|
-
}).then(res => {
|
|
20
|
-
if (!res.ok) {
|
|
21
|
-
throw new Error('An error occurred while fetching the data.');
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
return res.json();
|
|
25
23
|
});
|
|
24
|
+
|
|
25
|
+
if (!response.ok) {
|
|
26
|
+
throw new Error('An error occurred while fetching the data.');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return response.json();
|
|
26
30
|
};
|
|
27
31
|
|
|
32
|
+
// 3. 简化 withBBApi 类型定义,确保参数类型一致
|
|
28
33
|
export const withBBApi =
|
|
29
34
|
(fetcher: Fetcher) =>
|
|
30
35
|
(apiEndPoint: string): Fetcher =>
|
|
31
|
-
async (resource
|
|
36
|
+
async (resource, init = {}) =>
|
|
32
37
|
fetcher(`${apiEndPoint}/${resource}`, { ...init, mode: 'cors' });
|
|
33
38
|
|
|
34
39
|
export const cfApiFetcher = withBBApi(baseFetcher)(API_ENDPOINT);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { ManifestMap } from '#/plugins/manifest';
|
|
2
|
+
import { IHostContext } from '#/types/hostApi';
|
|
3
|
+
import { IPlugin, IPluginManifestEntry, PluginID } from '#/types/plugin';
|
|
4
|
+
|
|
5
|
+
export class BBPlugin implements IPlugin {
|
|
6
|
+
id: PluginID = 'default';
|
|
7
|
+
onInstall?: ((ctx: IHostContext) => void | Promise<void>) | undefined;
|
|
8
|
+
onDisable?: (() => Promise<void> | void) | undefined;
|
|
9
|
+
onDestroy?: (() => void) | undefined;
|
|
10
|
+
getMeta(): IPluginManifestEntry {
|
|
11
|
+
return ManifestMap.get(this.id) as IPluginManifestEntry;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { useCallback, useEffect, useState } from 'react';
|
|
1
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
2
2
|
|
|
3
3
|
import { useGlobalLoading } from '@/hooks';
|
|
4
4
|
import { pluginManager } from '#/core/pluginManager';
|
|
5
5
|
import { PluginID } from '#/types/plugin';
|
|
6
6
|
|
|
7
|
+
import { PluginStore } from '../pluginStore';
|
|
8
|
+
|
|
7
9
|
const usePluginsLoading = () => {
|
|
8
10
|
const { setGlobalLoading } = useGlobalLoading();
|
|
9
11
|
|
|
@@ -28,9 +30,16 @@ const usePluginsLoading = () => {
|
|
|
28
30
|
* @example
|
|
29
31
|
* usePlugins(['sticker', 'fontstyler']);
|
|
30
32
|
*/
|
|
31
|
-
export const usePlugins = (
|
|
33
|
+
export const usePlugins = () => {
|
|
32
34
|
const [done, setDone] = useState(false);
|
|
33
35
|
|
|
36
|
+
const installedPlugins = PluginStore.getInstance().getInstalledPlugins();
|
|
37
|
+
|
|
38
|
+
const pluginIds: Array<PluginID> = useMemo(
|
|
39
|
+
() => ['store', ...Array.from(installedPlugins)],
|
|
40
|
+
[installedPlugins]
|
|
41
|
+
);
|
|
42
|
+
|
|
34
43
|
usePluginsLoading();
|
|
35
44
|
|
|
36
45
|
const loadAllPlugins = useCallback(async () => {
|
|
@@ -8,6 +8,7 @@ import { getStableDeviceId } from '@/utils/fingerprints';
|
|
|
8
8
|
import { registry } from './registry';
|
|
9
9
|
import { createEventBus } from './utils/eventBus';
|
|
10
10
|
import { AdminPluginIDSet } from './const';
|
|
11
|
+
import { PluginStore } from './pluginStore';
|
|
11
12
|
|
|
12
13
|
const pluginModules = import.meta.glob('../plugins/*/index.ts');
|
|
13
14
|
|
|
@@ -17,14 +18,15 @@ class PluginManager {
|
|
|
17
18
|
private bus = createEventBus<PluginEvents>();
|
|
18
19
|
|
|
19
20
|
private createHostContext(perm: PluginPerm = 'guest'): IHostContext {
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
const adminCtx =
|
|
22
|
+
perm === 'admin'
|
|
23
|
+
? {
|
|
24
|
+
store: PluginStore.getInstance(),
|
|
25
|
+
}
|
|
26
|
+
: {};
|
|
25
27
|
return {
|
|
28
|
+
...adminCtx,
|
|
26
29
|
api: {
|
|
27
|
-
...(perm === 'admin' ? adminOnlyApi : {}),
|
|
28
30
|
setLoading: (id: PluginID, loading: boolean) => {
|
|
29
31
|
this.bus.emit('plugin:loading:changed', { id, loading });
|
|
30
32
|
},
|
|
@@ -89,7 +91,7 @@ class PluginManager {
|
|
|
89
91
|
|
|
90
92
|
const p: IPlugin = module.default;
|
|
91
93
|
|
|
92
|
-
const perm = (
|
|
94
|
+
const perm = p.getMeta().perm || 'guest';
|
|
93
95
|
if (perm === 'admin' && !AdminPluginIDSet.has(pluginId)) {
|
|
94
96
|
console.error(`Plugin ${pluginId} requires admin permission, but it's not trusted.`);
|
|
95
97
|
return;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { PLUGIN_MANIFEST } from '#/plugins/manifest';
|
|
2
|
+
import { IPluginStoreEntry, PluginID } from '#/types/plugin';
|
|
3
|
+
|
|
4
|
+
import { pluginManager } from './pluginManager';
|
|
5
|
+
|
|
6
|
+
export class PluginStore {
|
|
7
|
+
private static instance: PluginStore;
|
|
8
|
+
|
|
9
|
+
private installedSet: Set<PluginID> = new Set();
|
|
10
|
+
|
|
11
|
+
private constructor() {
|
|
12
|
+
this.parse();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
private parse = () => {
|
|
16
|
+
this.installedSet.clear();
|
|
17
|
+
const installedPluginsStr = localStorage.getItem('installed_plugins');
|
|
18
|
+
if (installedPluginsStr) {
|
|
19
|
+
try {
|
|
20
|
+
const entries = JSON.parse(installedPluginsStr) as Array<PluginID>;
|
|
21
|
+
entries.forEach(id => this.installedSet.add(id));
|
|
22
|
+
} catch (e) {
|
|
23
|
+
console.error('Failed to parse installed plugins from localStorage', e);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return this.installedSet;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
private stringify = () => {
|
|
31
|
+
localStorage.setItem('installed_plugins', JSON.stringify(Array.from(this.installedSet)));
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
getInstalledPlugins: () => Set<PluginID> = () => {
|
|
35
|
+
return this.parse();
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
getAllPlugins: () => Array<IPluginStoreEntry> = () => {
|
|
39
|
+
const installedPlugins = this.getInstalledPlugins();
|
|
40
|
+
return PLUGIN_MANIFEST.map(plugin => ({
|
|
41
|
+
...plugin,
|
|
42
|
+
enabled: installedPlugins.has(plugin.id),
|
|
43
|
+
}));
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
isPluginInstalled: (id: PluginID) => boolean = id => {
|
|
47
|
+
return this.getInstalledPlugins().has(id);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
installPlugin: (id: PluginID) => Promise<void> = async id => {
|
|
51
|
+
await pluginManager.loadPlugin(id);
|
|
52
|
+
this.installedSet.add(id);
|
|
53
|
+
this.stringify();
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
disablePlugin: (id: PluginID) => Promise<void> = async id => {
|
|
57
|
+
await pluginManager.disablePlugin(id);
|
|
58
|
+
this.installedSet.delete(id);
|
|
59
|
+
this.stringify();
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
static getInstance() {
|
|
63
|
+
if (!PluginStore.instance) {
|
|
64
|
+
PluginStore.instance = new PluginStore();
|
|
65
|
+
}
|
|
66
|
+
return PluginStore.instance;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -1,27 +1,14 @@
|
|
|
1
|
+
import { BBPlugin } from '#/core/bbplugin';
|
|
1
2
|
import { IHostContext } from '#/types/hostApi';
|
|
2
|
-
import {
|
|
3
|
+
import { PluginID } from '#/types/plugin';
|
|
3
4
|
import { TitleListItem } from '#/types/posts';
|
|
4
5
|
|
|
5
|
-
class ExtraCd
|
|
6
|
-
id:
|
|
7
|
-
name: string = 'Extra CD';
|
|
8
|
-
description: string =
|
|
9
|
-
'Provides additional change directory functionalities for enhanced user experience.';
|
|
10
|
-
version: string = '1.0.0';
|
|
11
|
-
author: string = 'bbki.ng';
|
|
6
|
+
class ExtraCd extends BBPlugin {
|
|
7
|
+
id: PluginID = 'extra-cd';
|
|
12
8
|
|
|
13
|
-
async
|
|
14
|
-
// Initialize any required resources or settings
|
|
9
|
+
override onInstall = async (ctx: IHostContext): Promise<void> => {
|
|
15
10
|
ctx.api.registerMiddleware('transformTitleList', this.transformTitleList, this.id, 10);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
async onDisable(): Promise<void> {
|
|
19
|
-
// Clean up active operations
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
onDestroy(): void {
|
|
23
|
-
// Release all resources
|
|
24
|
-
}
|
|
11
|
+
};
|
|
25
12
|
|
|
26
13
|
private transformTitleList = (titleList: Array<TitleListItem>) => {
|
|
27
14
|
return [
|
|
@@ -1,19 +1,14 @@
|
|
|
1
|
+
import { BBPlugin } from '#/core/bbplugin';
|
|
1
2
|
import { IHostContext } from '#/types/hostApi';
|
|
2
|
-
import {
|
|
3
|
+
import { PluginID } from '#/types/plugin';
|
|
3
4
|
import { buildEntryCreator } from '#/utils';
|
|
4
5
|
|
|
5
6
|
import { ExtendedRoutesPage } from './components/page';
|
|
6
7
|
|
|
7
|
-
export class ExtraEntryPlugin
|
|
8
|
-
id:
|
|
9
|
-
name: string = 'Extra Entry';
|
|
10
|
-
description?: string | undefined;
|
|
11
|
-
version: string = '1.0.0';
|
|
12
|
-
author?: string | undefined = 'bbki.ng';
|
|
8
|
+
export class ExtraEntryPlugin extends BBPlugin {
|
|
9
|
+
id: PluginID = 'extra-entry';
|
|
13
10
|
|
|
14
|
-
onInstall
|
|
15
|
-
ctx: IHostContext
|
|
16
|
-
) => {
|
|
11
|
+
override onInstall = async (ctx: IHostContext): Promise<void> => {
|
|
17
12
|
const entryCreator = buildEntryCreator(ctx);
|
|
18
13
|
entryCreator(
|
|
19
14
|
{
|
|
@@ -24,9 +19,6 @@ export class ExtraEntryPlugin implements IPlugin {
|
|
|
24
19
|
this.id
|
|
25
20
|
);
|
|
26
21
|
};
|
|
27
|
-
|
|
28
|
-
onDisable?: (() => Promise<void> | void) | undefined;
|
|
29
|
-
onDestroy?: (() => void) | undefined;
|
|
30
22
|
}
|
|
31
23
|
|
|
32
24
|
export default new ExtraEntryPlugin();
|
package/src/plugins/manifest.ts
CHANGED
|
@@ -1,40 +1,47 @@
|
|
|
1
|
-
|
|
1
|
+
import { IPluginManifestEntry } from '#/types/plugin';
|
|
2
|
+
|
|
3
|
+
export const PLUGIN_MANIFEST: Array<IPluginManifestEntry> = [
|
|
2
4
|
{
|
|
3
|
-
name: '
|
|
5
|
+
name: '贴纸',
|
|
4
6
|
id: 'sticker',
|
|
5
7
|
version: '0.1.0',
|
|
6
|
-
description: '
|
|
8
|
+
description: '在页面一些地方显示奇怪的贴纸',
|
|
7
9
|
},
|
|
8
10
|
{
|
|
9
|
-
name: '
|
|
11
|
+
name: '插件商店',
|
|
10
12
|
id: 'store',
|
|
11
13
|
version: '0.1.0',
|
|
12
|
-
description: '
|
|
14
|
+
description: '一个管理 bbki.ng 插件的商店',
|
|
13
15
|
perm: 'admin',
|
|
14
16
|
},
|
|
15
17
|
{
|
|
16
|
-
name: '
|
|
18
|
+
name: '最近',
|
|
17
19
|
id: 'now',
|
|
18
20
|
version: '1.0.0',
|
|
19
|
-
description: '
|
|
21
|
+
description: '显示网站最近更新的版本,以及一些文字动态',
|
|
20
22
|
},
|
|
21
23
|
{
|
|
22
|
-
name: '
|
|
24
|
+
name: '捷径',
|
|
23
25
|
id: 'extra-cd',
|
|
24
26
|
version: '0.1.0',
|
|
25
27
|
description:
|
|
26
|
-
'
|
|
28
|
+
'提供额外的快速切换页面的捷径,例如在标题列表末尾添加一个 "cd ~" 的选项,点击后跳转到主页',
|
|
27
29
|
},
|
|
30
|
+
// {
|
|
31
|
+
// name: 'extra-entry',
|
|
32
|
+
// id: 'extra-entry',
|
|
33
|
+
// version: '0.1.0',
|
|
34
|
+
// description: 'Provides extra entries for cover and extended routes.',
|
|
35
|
+
// },
|
|
28
36
|
{
|
|
29
|
-
name: '
|
|
30
|
-
id: 'extra-entry',
|
|
31
|
-
version: '0.1.0',
|
|
32
|
-
description: 'Provides extra entries for cover and extended routes.',
|
|
33
|
-
},
|
|
34
|
-
{
|
|
35
|
-
name: 'xwy',
|
|
37
|
+
name: '小乌鸦',
|
|
36
38
|
id: 'xwy',
|
|
37
39
|
version: '0.1.0',
|
|
38
|
-
description: '
|
|
40
|
+
description: '为小乌鸦合集实现定制功能或者样式的插件',
|
|
39
41
|
},
|
|
40
42
|
];
|
|
43
|
+
|
|
44
|
+
export const ManifestMap = PLUGIN_MANIFEST.reduce((map, entry) => {
|
|
45
|
+
map.set(entry.id, entry);
|
|
46
|
+
return map;
|
|
47
|
+
}, new Map<string, IPluginManifestEntry>());
|
|
@@ -1,16 +1,9 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { PathRouteProps } from 'react-router-dom';
|
|
3
2
|
|
|
4
3
|
import { IComPropsRegisteredToSlot } from '#/types/slots';
|
|
5
4
|
|
|
6
5
|
import Streaming from './streaming';
|
|
7
6
|
|
|
8
|
-
export const pageNow = (
|
|
9
|
-
const route = data as Omit<PathRouteProps, 'element'>;
|
|
10
|
-
|
|
11
|
-
if (route.path !== '/now') {
|
|
12
|
-
return <></>;
|
|
13
|
-
}
|
|
14
|
-
|
|
7
|
+
export const pageNow = (_: IComPropsRegisteredToSlot) => {
|
|
15
8
|
return <Streaming />;
|
|
16
9
|
};
|
|
@@ -9,6 +9,7 @@ import { useStreaming } from '../../hooks/use_streaming';
|
|
|
9
9
|
import { useScrollBtnVisibility } from './useScrollBtnVisibility';
|
|
10
10
|
|
|
11
11
|
declare global {
|
|
12
|
+
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
12
13
|
namespace JSX {
|
|
13
14
|
interface IntrinsicElements {
|
|
14
15
|
'bb-msg-history': React.DetailedHTMLProps<
|
|
@@ -39,7 +40,7 @@ const Streaming = () => {
|
|
|
39
40
|
|
|
40
41
|
useEffect(() => {
|
|
41
42
|
const el = bbMsgHistoryRef.current;
|
|
42
|
-
let timer:
|
|
43
|
+
let timer: ReturnType<typeof setTimeout>;
|
|
43
44
|
if (!isLoading && el) {
|
|
44
45
|
// 检查自定义元素是否已定义并升级
|
|
45
46
|
if (el.scrollToBottom) {
|
package/src/plugins/now/index.ts
CHANGED
|
@@ -1,18 +1,15 @@
|
|
|
1
|
+
import { BBPlugin } from '#/core/bbplugin';
|
|
1
2
|
import { IHostContext } from '#/types/hostApi';
|
|
2
|
-
import {
|
|
3
|
+
import { PluginID } from '#/types/plugin';
|
|
3
4
|
import { buildEntryCreator } from '#/utils';
|
|
4
5
|
|
|
5
6
|
import { pageNow } from './components';
|
|
6
7
|
import { NowCtx } from './context';
|
|
7
8
|
|
|
8
|
-
export class NowPlugin
|
|
9
|
-
id:
|
|
10
|
-
name: string = 'Now';
|
|
11
|
-
description?: string | undefined = 'Manages real-time site status and logs';
|
|
12
|
-
version: string = '1.0.0';
|
|
13
|
-
author?: string | undefined = 'bbki.ng';
|
|
9
|
+
export class NowPlugin extends BBPlugin {
|
|
10
|
+
id: PluginID = 'now';
|
|
14
11
|
|
|
15
|
-
onInstall = (ctx: IHostContext) => {
|
|
12
|
+
override onInstall = async (ctx: IHostContext): Promise<void> => {
|
|
16
13
|
const nowEntryCreator = buildEntryCreator(ctx);
|
|
17
14
|
const { withCtx } = NowCtx;
|
|
18
15
|
|
|
@@ -1,18 +1,12 @@
|
|
|
1
|
+
import { BBPlugin } from '#/core/bbplugin';
|
|
1
2
|
import { IHostContext } from '#/types/hostApi';
|
|
2
|
-
import {
|
|
3
|
+
import { PluginID } from '#/types/plugin';
|
|
3
4
|
|
|
4
5
|
import { StickerCom } from './components/StickerCom';
|
|
5
6
|
import { IStickerCtx, StickerCtx } from './context';
|
|
6
7
|
|
|
7
|
-
class Sticker
|
|
8
|
-
id:
|
|
9
|
-
constructor() {
|
|
10
|
-
this.id = 'sticker';
|
|
11
|
-
this.name = 'Sticker';
|
|
12
|
-
this.description = 'A plugin for managing stickers and decorative elements';
|
|
13
|
-
this.version = '1.0.0';
|
|
14
|
-
this.author = 'bbki.ng';
|
|
15
|
-
}
|
|
8
|
+
class Sticker extends BBPlugin {
|
|
9
|
+
id: PluginID = 'sticker';
|
|
16
10
|
|
|
17
11
|
private createPluginCtx = async (ctx: IHostContext): Promise<IStickerCtx> => {
|
|
18
12
|
return {
|
|
@@ -20,7 +14,7 @@ class Sticker implements IPlugin {
|
|
|
20
14
|
};
|
|
21
15
|
};
|
|
22
16
|
|
|
23
|
-
onInstall = async (ctx: IHostContext) => {
|
|
17
|
+
override onInstall = async (ctx: IHostContext): Promise<void> => {
|
|
24
18
|
const { withCtx } = StickerCtx;
|
|
25
19
|
|
|
26
20
|
const pluginCtx = await this.createPluginCtx(ctx);
|
|
@@ -29,14 +23,6 @@ class Sticker implements IPlugin {
|
|
|
29
23
|
|
|
30
24
|
ctx.api.registerSlot('leftCol', Sticker, this.id);
|
|
31
25
|
};
|
|
32
|
-
|
|
33
|
-
onDisable = async () => {};
|
|
34
|
-
|
|
35
|
-
onDestroy = () => {};
|
|
36
|
-
name: string;
|
|
37
|
-
description?: string | undefined;
|
|
38
|
-
version: string;
|
|
39
|
-
author?: string | undefined;
|
|
40
26
|
}
|
|
41
27
|
|
|
42
28
|
export default new Sticker();
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import React, { useCallback, useEffect } from 'react';
|
|
2
|
+
import { Button, Table } from '@bbki.ng/ui';
|
|
3
|
+
|
|
4
|
+
import { IComPropsRegisteredToSlot } from '#/types/slots';
|
|
5
|
+
import { IPluginStoreEntry, PluginID } from '#/types/plugin';
|
|
6
|
+
|
|
7
|
+
import { StoreCtx } from '../context';
|
|
8
|
+
|
|
9
|
+
export const StorePage = (_: IComPropsRegisteredToSlot) => {
|
|
10
|
+
const { list, setLoading, isInstalled, install, uninstall } = StoreCtx.useCtx();
|
|
11
|
+
const [plugins, setPlugins] = React.useState<Array<IPluginStoreEntry>>([]);
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
const fetchPlugins = async () => {
|
|
15
|
+
setLoading(true);
|
|
16
|
+
const pluginList = await list();
|
|
17
|
+
setPlugins(pluginList);
|
|
18
|
+
setLoading(false);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
fetchPlugins();
|
|
22
|
+
}, [list, setLoading]);
|
|
23
|
+
|
|
24
|
+
const handleInstall = useCallback(
|
|
25
|
+
async (id: PluginID) => {
|
|
26
|
+
setLoading(true);
|
|
27
|
+
await install(id);
|
|
28
|
+
const pluginList = await list();
|
|
29
|
+
setPlugins(pluginList);
|
|
30
|
+
setLoading(false);
|
|
31
|
+
},
|
|
32
|
+
[install, list, setLoading]
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
const handleUninstall = useCallback(
|
|
36
|
+
async (id: PluginID) => {
|
|
37
|
+
setLoading(true);
|
|
38
|
+
await uninstall(id);
|
|
39
|
+
const pluginList = await list();
|
|
40
|
+
setPlugins(pluginList);
|
|
41
|
+
setLoading(false);
|
|
42
|
+
},
|
|
43
|
+
[uninstall, list, setLoading]
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
if (plugins.length === 0) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<div className="prose">
|
|
52
|
+
<Table
|
|
53
|
+
className="w-full"
|
|
54
|
+
rowCount={plugins.length}
|
|
55
|
+
headerRenderer={() => {
|
|
56
|
+
return (
|
|
57
|
+
<>
|
|
58
|
+
<Table.HCell>插件名称</Table.HCell>
|
|
59
|
+
<Table.HCell>插件描述</Table.HCell>
|
|
60
|
+
<Table.HCell style={{ textAlign: 'right' }}>安装/卸载</Table.HCell>
|
|
61
|
+
</>
|
|
62
|
+
);
|
|
63
|
+
}}
|
|
64
|
+
rowRenderer={index => {
|
|
65
|
+
const isIdxValid = index >= 0 && index < plugins.length;
|
|
66
|
+
if (!isIdxValid) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
const plugin = plugins[index];
|
|
70
|
+
return (
|
|
71
|
+
<>
|
|
72
|
+
<Table.Cell>{plugin.name}</Table.Cell>
|
|
73
|
+
<Table.Cell>{plugin.description}</Table.Cell>
|
|
74
|
+
<Table.Cell style={{ textAlign: 'right' }}>
|
|
75
|
+
{isInstalled(plugin.id) ? (
|
|
76
|
+
<Button
|
|
77
|
+
variant="ghost"
|
|
78
|
+
className="text-content-danger"
|
|
79
|
+
onClick={() => {
|
|
80
|
+
handleUninstall(plugin.id);
|
|
81
|
+
}}
|
|
82
|
+
>
|
|
83
|
+
卸载
|
|
84
|
+
</Button>
|
|
85
|
+
) : (
|
|
86
|
+
<Button
|
|
87
|
+
color="primary"
|
|
88
|
+
onClick={() => {
|
|
89
|
+
handleInstall(plugin.id);
|
|
90
|
+
}}
|
|
91
|
+
>
|
|
92
|
+
安装
|
|
93
|
+
</Button>
|
|
94
|
+
)}
|
|
95
|
+
</Table.Cell>
|
|
96
|
+
</>
|
|
97
|
+
);
|
|
98
|
+
}}
|
|
99
|
+
/>
|
|
100
|
+
</div>
|
|
101
|
+
);
|
|
102
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createPluginCtx } from '#/core/context';
|
|
2
|
+
import { IPluginStoreEntry, PluginID } from '#/types/plugin';
|
|
3
|
+
|
|
4
|
+
export interface IStoreCtx {
|
|
5
|
+
install: (pid: PluginID) => Promise<void>;
|
|
6
|
+
uninstall: (pid: PluginID) => Promise<void>;
|
|
7
|
+
isInstalled: (pid: PluginID) => boolean;
|
|
8
|
+
list: () => Promise<Array<IPluginStoreEntry>> | Array<IPluginStoreEntry>;
|
|
9
|
+
setLoading: (loading: boolean) => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const StoreCtx = createPluginCtx<IStoreCtx>();
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { BBPlugin } from '#/core/bbplugin';
|
|
2
|
+
import { IHostContext } from '#/types/hostApi';
|
|
3
|
+
import { PluginID } from '#/types/plugin';
|
|
4
|
+
import { buildEntryCreator } from '#/utils';
|
|
5
|
+
|
|
6
|
+
import { StorePage } from './components/storePage';
|
|
7
|
+
import { StoreCtx } from './context';
|
|
8
|
+
|
|
9
|
+
export class PluginStore extends BBPlugin {
|
|
10
|
+
id: PluginID = 'store';
|
|
11
|
+
override onInstall = async (ctx: IHostContext): Promise<void> => {
|
|
12
|
+
const entryCreator = buildEntryCreator(ctx);
|
|
13
|
+
entryCreator(
|
|
14
|
+
{
|
|
15
|
+
path: '/store',
|
|
16
|
+
label: 'cd ./store',
|
|
17
|
+
pageComponent: StoreCtx.withCtx(
|
|
18
|
+
{
|
|
19
|
+
install: ctx.store?.installPlugin || (async () => {}),
|
|
20
|
+
uninstall: ctx.store?.disablePlugin || (async () => {}),
|
|
21
|
+
isInstalled: ctx.store?.isPluginInstalled || ((_: PluginID) => false),
|
|
22
|
+
list: () => {
|
|
23
|
+
const installed = Array.from(ctx.store?.getAllPlugins() || []);
|
|
24
|
+
return installed.filter(({ id }) => id !== this.id);
|
|
25
|
+
},
|
|
26
|
+
setLoading: (loading: boolean) => {
|
|
27
|
+
ctx.api.setLoading('store', loading);
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
StorePage
|
|
31
|
+
),
|
|
32
|
+
},
|
|
33
|
+
this.id
|
|
34
|
+
);
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export default new PluginStore();
|
package/src/plugins/xwy/index.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { IHostContext } from '#/types/hostApi';
|
|
2
|
-
import {
|
|
2
|
+
import { BBPlugin } from '#/core/bbplugin';
|
|
3
3
|
import { HookPoint } from '#/types/slots';
|
|
4
|
+
import { PluginID } from '#/types/plugin';
|
|
4
5
|
|
|
6
|
+
import { HookPointTypeMap, Transformer } from './types';
|
|
5
7
|
import { SpecialTitle } from './components/article';
|
|
6
8
|
import { XwyLogo } from './components/logo';
|
|
7
9
|
import { FontRules, LOADING_CLASS } from './const';
|
|
@@ -13,16 +15,12 @@ import {
|
|
|
13
15
|
pinTitle,
|
|
14
16
|
} from './transformers';
|
|
15
17
|
|
|
16
|
-
class XwyPlugin
|
|
17
|
-
id = '
|
|
18
|
-
name = 'Font Styler';
|
|
19
|
-
description = 'Apply custom fonts to specific post titles';
|
|
20
|
-
version = '1.0.0';
|
|
21
|
-
author = 'bbki.ng';
|
|
18
|
+
class XwyPlugin extends BBPlugin {
|
|
19
|
+
id: PluginID = 'xwy';
|
|
22
20
|
|
|
23
21
|
private loadedFonts = new Set<string>();
|
|
24
22
|
|
|
25
|
-
onInstall = (ctx: IHostContext) => {
|
|
23
|
+
override onInstall = (ctx: IHostContext) => {
|
|
26
24
|
// add font loading listener
|
|
27
25
|
this.initFontLoadingListener();
|
|
28
26
|
|
|
@@ -43,7 +41,9 @@ class XwyPlugin implements IPlugin {
|
|
|
43
41
|
ctx.api.registerSlot('articleTitle', SpecialTitle, this.id);
|
|
44
42
|
};
|
|
45
43
|
|
|
46
|
-
private trasformerMap:
|
|
44
|
+
private trasformerMap: {
|
|
45
|
+
[K in Extract<HookPoint, keyof HookPointTypeMap>]?: Array<Transformer<K>>;
|
|
46
|
+
} = {
|
|
47
47
|
transformTitleList: [pinTitle, transformTitleList],
|
|
48
48
|
transformBreadcrumbPath: [transformBreadcrumbPaths],
|
|
49
49
|
transformPostContent: [transformPostContent],
|
|
@@ -68,7 +68,7 @@ class XwyPlugin implements IPlugin {
|
|
|
68
68
|
});
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
onDisable = async () => {
|
|
71
|
+
override onDisable = async () => {
|
|
72
72
|
this.loadedFonts.forEach(fontName => {
|
|
73
73
|
const styleEl = document.getElementById(`font-${fontName}`);
|
|
74
74
|
if (styleEl) {
|
|
@@ -77,8 +77,6 @@ class XwyPlugin implements IPlugin {
|
|
|
77
77
|
});
|
|
78
78
|
this.loadedFonts.clear();
|
|
79
79
|
};
|
|
80
|
-
|
|
81
|
-
onDestroy = () => {};
|
|
82
80
|
}
|
|
83
81
|
|
|
84
82
|
export default new XwyPlugin();
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { PathObj } from '@bbki.ng/ui';
|
|
2
|
+
|
|
3
|
+
import { TitleListItem } from '#/types/posts';
|
|
4
|
+
|
|
1
5
|
export interface FontConfig {
|
|
2
6
|
name: string;
|
|
3
7
|
src: string;
|
|
@@ -15,3 +19,17 @@ export interface FontRule {
|
|
|
15
19
|
extraCls?: string;
|
|
16
20
|
variant?: 'default' | 'special';
|
|
17
21
|
}
|
|
22
|
+
|
|
23
|
+
// 1. 先定义每个 HookPoint 对应的类型映射
|
|
24
|
+
export interface HookPointTypeMap {
|
|
25
|
+
transformTitleList: { input: TitleListItem[]; output: TitleListItem[] };
|
|
26
|
+
transformBreadcrumbPath: { input: PathObj[]; output: PathObj[] };
|
|
27
|
+
transformPostContent: { input: string; output: string };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type HookPoint = keyof HookPointTypeMap;
|
|
31
|
+
|
|
32
|
+
// 2. 定义带类型的 Transformer
|
|
33
|
+
export type Transformer<K extends HookPoint> = (
|
|
34
|
+
baseData: HookPointTypeMap[K]['input']
|
|
35
|
+
) => HookPointTypeMap[K]['output'];
|
package/src/types/hostApi.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
|
|
3
3
|
import { type FingerprintData } from '@/utils/fingerprints';
|
|
4
|
+
import { PluginStore } from '#/core/pluginStore';
|
|
4
5
|
|
|
5
6
|
import { HookPoint, IComPropsRegisteredToSlot, SlotName } from './slots';
|
|
6
7
|
import { PluginID } from './plugin';
|
|
@@ -20,13 +21,11 @@ export interface IHostApi {
|
|
|
20
21
|
pluginId: string,
|
|
21
22
|
weight?: number
|
|
22
23
|
) => void;
|
|
23
|
-
|
|
24
|
-
installPlugin?: (id: PluginID) => void;
|
|
25
|
-
disablePlugin?: (id: PluginID) => void;
|
|
26
24
|
}
|
|
27
25
|
|
|
28
26
|
export type PluginInitializer = (api: IHostApi) => void;
|
|
29
27
|
|
|
30
28
|
export interface IHostContext {
|
|
29
|
+
store?: PluginStore;
|
|
31
30
|
api: IHostApi;
|
|
32
31
|
}
|
package/src/types/plugin.ts
CHANGED
|
@@ -5,18 +5,15 @@ import { IComPropsRegisteredToSlot } from '#/types/slots';
|
|
|
5
5
|
import { IHostContext } from './hostApi';
|
|
6
6
|
|
|
7
7
|
export interface IPlugin {
|
|
8
|
-
id:
|
|
9
|
-
name: string;
|
|
10
|
-
description?: string;
|
|
11
|
-
version: string;
|
|
12
|
-
author?: string;
|
|
8
|
+
id: PluginID;
|
|
13
9
|
|
|
10
|
+
getMeta: () => IPluginManifestEntry;
|
|
14
11
|
onInstall?: (ctx: IHostContext) => void | Promise<void>;
|
|
15
12
|
onDisable?: () => Promise<void> | void;
|
|
16
13
|
onDestroy?: () => void;
|
|
17
14
|
}
|
|
18
15
|
|
|
19
|
-
export type PluginID = 'sticker' | 'xwy' | 'extra-cd' | 'extra-entry' | 'store' | 'now';
|
|
16
|
+
export type PluginID = 'sticker' | 'xwy' | 'extra-cd' | 'extra-entry' | 'store' | 'now' | 'default';
|
|
20
17
|
|
|
21
18
|
export interface IPluginEntry {
|
|
22
19
|
path: string;
|
|
@@ -42,3 +39,7 @@ export interface IPluginManifestEntry {
|
|
|
42
39
|
description?: string;
|
|
43
40
|
perm?: PluginPerm;
|
|
44
41
|
}
|
|
42
|
+
|
|
43
|
+
export interface IPluginStoreEntry extends IPluginManifestEntry {
|
|
44
|
+
enabled: boolean;
|
|
45
|
+
}
|
package/tsconfig.json
CHANGED