@bbki.ng/site 5.5.33 → 5.5.35
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 +13 -0
- package/package.json +1 -1
- package/src/blog/app.tsx +25 -23
- package/src/blog/components/BaseLayout.tsx +2 -2
- package/src/blog/hooks/use_loading.ts +24 -3
- package/src/blog/hooks/use_posts.ts +1 -1
- package/src/blog/main.css +2 -2
- package/src/core/hooks/use_plugins.ts +18 -0
- package/src/core/pluginManager.ts +19 -3
- package/src/core/registry.ts +17 -24
- package/src/core/utils/eventBus.ts +29 -0
- package/src/plugins/xwy/index.ts +2 -2
- package/src/types/hostApi.ts +2 -0
- package/src/types/plugin.ts +9 -0
package/CHANGELOG.md
CHANGED
package/package.json
CHANGED
package/src/blog/app.tsx
CHANGED
|
@@ -15,37 +15,39 @@ import { Cover, Streaming } from './pages';
|
|
|
15
15
|
import { usePluginEntries } from './hooks/use_plugin_entries';
|
|
16
16
|
import { BaseLayout } from './components/BaseLayout';
|
|
17
17
|
|
|
18
|
-
const APP_PLUGIN_IDS: Array<PluginID> = ['sticker'
|
|
18
|
+
const APP_PLUGIN_IDS: Array<PluginID> = [/*'sticker',*/ 'xwy', 'extra-cd' /*'extra-entry'*/];
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
const AppRoutes = () => {
|
|
21
21
|
usePlugins(APP_PLUGIN_IDS);
|
|
22
22
|
|
|
23
23
|
const pluginEntries = usePluginEntries();
|
|
24
24
|
|
|
25
|
+
return (
|
|
26
|
+
<Routes>
|
|
27
|
+
<Route path="/" element={<BaseLayout />}>
|
|
28
|
+
<Route index element={<Cover />} />
|
|
29
|
+
|
|
30
|
+
<Route path="blog" element={<Outlet />}>
|
|
31
|
+
<Route path="" element={<Txt />} />
|
|
32
|
+
<Route path=":title" element={<ArticlePage />} />
|
|
33
|
+
</Route>
|
|
34
|
+
|
|
35
|
+
<Route path="bot" element={<BotRedirect />} />
|
|
36
|
+
<Route path="now" element={<Streaming />} />
|
|
37
|
+
{pluginEntries?.map(route => (
|
|
38
|
+
<Route key={route.path} path={route.path} element={<Slot name="route" data={route} />} />
|
|
39
|
+
))}
|
|
40
|
+
</Route>
|
|
41
|
+
<Route path="*" element={<NotFound />} />
|
|
42
|
+
</Routes>
|
|
43
|
+
);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const App = () => {
|
|
25
47
|
return (
|
|
26
48
|
<SWR>
|
|
27
49
|
<BBContext>
|
|
28
|
-
<
|
|
29
|
-
<Route path="/" element={<BaseLayout />}>
|
|
30
|
-
<Route index element={<Cover />} />
|
|
31
|
-
|
|
32
|
-
<Route path="blog" element={<Outlet />}>
|
|
33
|
-
<Route path="" element={<Txt />} />
|
|
34
|
-
<Route path=":title" element={<ArticlePage />} />
|
|
35
|
-
</Route>
|
|
36
|
-
|
|
37
|
-
<Route path="bot" element={<BotRedirect />} />
|
|
38
|
-
<Route path="now" element={<Streaming />} />
|
|
39
|
-
{pluginEntries?.map(route => (
|
|
40
|
-
<Route
|
|
41
|
-
key={route.path}
|
|
42
|
-
path={route.path}
|
|
43
|
-
element={<Slot name="route" data={route} />}
|
|
44
|
-
/>
|
|
45
|
-
))}
|
|
46
|
-
</Route>
|
|
47
|
-
<Route path="*" element={<NotFound />} />
|
|
48
|
-
</Routes>
|
|
50
|
+
<AppRoutes />
|
|
49
51
|
</BBContext>
|
|
50
52
|
</SWR>
|
|
51
53
|
);
|
|
@@ -27,7 +27,7 @@ export const BaseLayout = () => {
|
|
|
27
27
|
loading={isLoading}
|
|
28
28
|
customLogo={<Slot name="logo" data={defaultLogo} placeholder={defaultLogo} />}
|
|
29
29
|
style={{
|
|
30
|
-
paddingTop: 'var(--safe-top)',
|
|
30
|
+
paddingTop: 'calc(var(--safe-top) + 4px)',
|
|
31
31
|
transition: 'all .2s ease-in-out',
|
|
32
32
|
}}
|
|
33
33
|
/>
|
|
@@ -45,7 +45,7 @@ export const BaseLayout = () => {
|
|
|
45
45
|
</div>
|
|
46
46
|
}
|
|
47
47
|
>
|
|
48
|
-
<Container className="py-
|
|
48
|
+
<Container className="py-48">
|
|
49
49
|
<ErrorBoundary>
|
|
50
50
|
<Outlet />
|
|
51
51
|
</ErrorBoundary>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useContext, useEffect } from 'react';
|
|
1
|
+
import { useCallback, useContext, useEffect } from 'react';
|
|
2
2
|
|
|
3
3
|
import { GlobalLoadingContext } from '@/context/global_loading_state_provider';
|
|
4
4
|
|
|
@@ -9,7 +9,13 @@ import { GlobalLoadingContext } from '@/context/global_loading_state_provider';
|
|
|
9
9
|
* @param id - Unique identifier for this loading state
|
|
10
10
|
* @param loading - Whether this specific loading state is active
|
|
11
11
|
*/
|
|
12
|
-
export function useGlobalLoading(
|
|
12
|
+
export function useGlobalLoading(
|
|
13
|
+
id?: string | undefined,
|
|
14
|
+
loading?: boolean | undefined
|
|
15
|
+
): {
|
|
16
|
+
isLoading: boolean;
|
|
17
|
+
setGlobalLoading: (key: string, loadingFromManual: boolean) => () => void;
|
|
18
|
+
} {
|
|
13
19
|
const { register, setLoading, unregister, isLoading } = useContext(GlobalLoadingContext);
|
|
14
20
|
|
|
15
21
|
useEffect(() => {
|
|
@@ -22,5 +28,20 @@ export function useGlobalLoading(id: string | undefined, loading: boolean | unde
|
|
|
22
28
|
};
|
|
23
29
|
}, [id, loading, register, setLoading, unregister]);
|
|
24
30
|
|
|
25
|
-
|
|
31
|
+
const setGlobalLoading = useCallback(
|
|
32
|
+
(key: string, loadingFromManual: boolean) => {
|
|
33
|
+
register(key);
|
|
34
|
+
setLoading(key, loadingFromManual);
|
|
35
|
+
|
|
36
|
+
return () => {
|
|
37
|
+
unregister(key);
|
|
38
|
+
};
|
|
39
|
+
},
|
|
40
|
+
[register, setLoading, unregister]
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
isLoading,
|
|
45
|
+
setGlobalLoading,
|
|
46
|
+
};
|
|
26
47
|
}
|
|
@@ -53,7 +53,7 @@ export const usePosts = (name: string = '', suspense?: boolean) => {
|
|
|
53
53
|
}
|
|
54
54
|
}, [baseTitleList, run]);
|
|
55
55
|
|
|
56
|
-
const gLoading = useGlobalLoading('posts', isDataLoading || isTransforming);
|
|
56
|
+
const { isLoading: gLoading } = useGlobalLoading('posts', isDataLoading || isTransforming);
|
|
57
57
|
|
|
58
58
|
const posts =
|
|
59
59
|
isDataLoading || name === '' || swrError || !data
|
package/src/blog/main.css
CHANGED
|
@@ -73,7 +73,7 @@ a {
|
|
|
73
73
|
|
|
74
74
|
@supports (padding-top: env(safe-area-inset-top)) {
|
|
75
75
|
:root {
|
|
76
|
-
--safe-top: calc(env(safe-area-inset-top)
|
|
76
|
+
--safe-top: calc(env(safe-area-inset-top));
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
|
|
@@ -82,7 +82,7 @@ a {
|
|
|
82
82
|
-ms-overflow-style: none; /* Internet Explorer 10+ */
|
|
83
83
|
scrollbar-width: none;
|
|
84
84
|
transition: all 0.2s ease-in-out;
|
|
85
|
-
padding-top: var(--safe-top);
|
|
85
|
+
/* padding-top: var(--safe-top); */
|
|
86
86
|
box-sizing: border-box;
|
|
87
87
|
}
|
|
88
88
|
|
|
@@ -1,8 +1,24 @@
|
|
|
1
1
|
import { useCallback, useEffect, useState } from 'react';
|
|
2
2
|
|
|
3
|
+
import { useGlobalLoading } from '@/hooks';
|
|
3
4
|
import { pluginManager } from '#/core/pluginManager';
|
|
4
5
|
import { PluginID } from '#/types/plugin';
|
|
5
6
|
|
|
7
|
+
const usePluginsLoading = () => {
|
|
8
|
+
const { setGlobalLoading } = useGlobalLoading();
|
|
9
|
+
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
let unregister: (() => void) | undefined;
|
|
12
|
+
const unscribe = pluginManager.subscribePluginLoading(payload => {
|
|
13
|
+
unregister = setGlobalLoading(payload.id, payload.loading);
|
|
14
|
+
});
|
|
15
|
+
return () => {
|
|
16
|
+
unscribe();
|
|
17
|
+
unregister?.();
|
|
18
|
+
};
|
|
19
|
+
}, [setGlobalLoading]);
|
|
20
|
+
};
|
|
21
|
+
|
|
6
22
|
/**
|
|
7
23
|
* 通用插件管理 hook
|
|
8
24
|
* 根据传入的插件ID列表加载和卸载插件
|
|
@@ -15,6 +31,8 @@ import { PluginID } from '#/types/plugin';
|
|
|
15
31
|
export const usePlugins = (pluginIds: Array<PluginID>) => {
|
|
16
32
|
const [done, setDone] = useState(false);
|
|
17
33
|
|
|
34
|
+
usePluginsLoading();
|
|
35
|
+
|
|
18
36
|
const loadAllPlugins = useCallback(async () => {
|
|
19
37
|
try {
|
|
20
38
|
await Promise.all(pluginIds.map(id => pluginManager.loadPlugin(id)));
|
|
@@ -2,19 +2,25 @@ import React from 'react';
|
|
|
2
2
|
|
|
3
3
|
import { IHostContext } from '#/types/hostApi';
|
|
4
4
|
import type { SlotName, HookPoint, IComPropsRegisteredToSlot } from '#/types/slots';
|
|
5
|
-
import { IPlugin } from '#/types/plugin';
|
|
5
|
+
import { IPlugin, PluginEvents, PluginID } from '#/types/plugin';
|
|
6
6
|
import { getStableDeviceId } from '@/utils/fingerprints';
|
|
7
7
|
|
|
8
8
|
import { registry } from './registry';
|
|
9
|
+
import { createEventBus } from './utils/eventBus';
|
|
9
10
|
|
|
10
11
|
const pluginModules = import.meta.glob('../plugins/*/index.ts');
|
|
11
12
|
|
|
12
13
|
class PluginManager {
|
|
13
14
|
private activePlugins: Map<string, IPlugin> = new Map();
|
|
14
15
|
|
|
16
|
+
private bus = createEventBus<PluginEvents>();
|
|
17
|
+
|
|
15
18
|
private createHostContext(): IHostContext {
|
|
16
19
|
return {
|
|
17
20
|
api: {
|
|
21
|
+
setLoading: (id: PluginID, loading: boolean) => {
|
|
22
|
+
this.bus.emit('plugin:loading:changed', { id, loading });
|
|
23
|
+
},
|
|
18
24
|
registerMiddleware: <T>(
|
|
19
25
|
point: HookPoint,
|
|
20
26
|
fn: (data: T) => Promise<T> | T,
|
|
@@ -38,8 +44,12 @@ class PluginManager {
|
|
|
38
44
|
|
|
39
45
|
private loading = new Set<string>();
|
|
40
46
|
|
|
47
|
+
subscribePluginLoading(listener: (payload: PluginEvents['plugin:loading:changed']) => void) {
|
|
48
|
+
return this.bus.on('plugin:loading:changed', listener);
|
|
49
|
+
}
|
|
50
|
+
|
|
41
51
|
// enable abort ctrl
|
|
42
|
-
async loadPlugin(pluginId:
|
|
52
|
+
async loadPlugin(pluginId: PluginID) {
|
|
43
53
|
if (this.activePlugins.has(pluginId)) {
|
|
44
54
|
console.warn(`Plugin ${pluginId} is already loaded.`);
|
|
45
55
|
return;
|
|
@@ -52,11 +62,16 @@ class PluginManager {
|
|
|
52
62
|
|
|
53
63
|
// try load plugin
|
|
54
64
|
this.loading.add(pluginId);
|
|
65
|
+
this.bus.emit('plugin:loading:changed', { id: pluginId, loading: true });
|
|
55
66
|
|
|
56
67
|
const modulePath = `../plugins/${pluginId}/index.ts`;
|
|
57
68
|
const moduleLoader = pluginModules[modulePath];
|
|
58
69
|
if (!moduleLoader) {
|
|
59
70
|
console.error(`Plugin ${pluginId} not found in registry`);
|
|
71
|
+
|
|
72
|
+
this.loading.delete(pluginId);
|
|
73
|
+
this.bus.emit('plugin:loading:changed', { id: pluginId, loading: false });
|
|
74
|
+
|
|
60
75
|
return;
|
|
61
76
|
}
|
|
62
77
|
|
|
@@ -78,10 +93,11 @@ class PluginManager {
|
|
|
78
93
|
console.error(`Failed to load plugin ${pluginId}:`, error);
|
|
79
94
|
} finally {
|
|
80
95
|
this.loading.delete(pluginId);
|
|
96
|
+
this.bus.emit('plugin:loading:changed', { id: pluginId, loading: false });
|
|
81
97
|
}
|
|
82
98
|
}
|
|
83
99
|
|
|
84
|
-
async disablePlugin(pluginId:
|
|
100
|
+
async disablePlugin(pluginId: PluginID) {
|
|
85
101
|
const plugin = this.activePlugins.get(pluginId);
|
|
86
102
|
if (!plugin) {
|
|
87
103
|
console.warn(`Plugin ${pluginId} is not loaded.`);
|
package/src/core/registry.ts
CHANGED
|
@@ -2,6 +2,8 @@ import React from 'react';
|
|
|
2
2
|
|
|
3
3
|
import type { SlotName, HookPoint, IComPropsRegisteredToSlot } from '#/types/slots';
|
|
4
4
|
|
|
5
|
+
import { createEventBus } from './utils/eventBus';
|
|
6
|
+
|
|
5
7
|
export interface ISlotEntry {
|
|
6
8
|
id: string;
|
|
7
9
|
component: React.ComponentType<IComPropsRegisteredToSlot>;
|
|
@@ -16,37 +18,28 @@ export interface IMiddlewareEntry<T> {
|
|
|
16
18
|
weight?: number;
|
|
17
19
|
}
|
|
18
20
|
|
|
21
|
+
type RegistryEvents = {
|
|
22
|
+
'components:changed': void;
|
|
23
|
+
'middleware:changed': HookPoint;
|
|
24
|
+
};
|
|
25
|
+
|
|
19
26
|
export class Registry {
|
|
20
27
|
private slots = new Map<SlotName, ISlotEntry[]>();
|
|
21
28
|
|
|
22
|
-
private
|
|
23
|
-
private middlewareListenerMap = new Map<HookPoint, Set<() => void>>();
|
|
29
|
+
private bus = createEventBus<RegistryEvents>();
|
|
24
30
|
|
|
25
31
|
private middlewares = new Map<HookPoint, IMiddlewareEntry<unknown>[]>();
|
|
26
32
|
|
|
27
|
-
private broadcastChange() {
|
|
28
|
-
this.listeners.forEach(listener => listener());
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
private broadcastMiddlewareChange(point: HookPoint) {
|
|
32
|
-
const listeners = this.middlewareListenerMap.get(point);
|
|
33
|
-
if (listeners) {
|
|
34
|
-
listeners.forEach(listener => listener());
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
33
|
subscribeMiddleware(hookPoint: HookPoint, listener: () => void) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
return () => listeners.delete(listener);
|
|
34
|
+
return this.bus.on('middleware:changed', point => {
|
|
35
|
+
if (point === hookPoint) {
|
|
36
|
+
listener();
|
|
37
|
+
}
|
|
38
|
+
});
|
|
45
39
|
}
|
|
46
40
|
|
|
47
41
|
subscribe(listener: () => void) {
|
|
48
|
-
this.
|
|
49
|
-
return () => this.listeners.delete(listener);
|
|
42
|
+
return this.bus.on('components:changed', listener);
|
|
50
43
|
}
|
|
51
44
|
|
|
52
45
|
registerComponent(
|
|
@@ -68,7 +61,7 @@ export class Registry {
|
|
|
68
61
|
const newList = [...existing, newEntry].sort((a, b) => b.weight - a.weight);
|
|
69
62
|
this.slots.set(slot, newList);
|
|
70
63
|
|
|
71
|
-
this.
|
|
64
|
+
this.bus.emit('components:changed', undefined);
|
|
72
65
|
}
|
|
73
66
|
|
|
74
67
|
unregisterAllByPluginId(pluginId: string) {
|
|
@@ -86,7 +79,7 @@ export class Registry {
|
|
|
86
79
|
|
|
87
80
|
console.log(`[Registry] All resources for plugin "${pluginId}" have been cleared.`);
|
|
88
81
|
|
|
89
|
-
this.
|
|
82
|
+
this.bus.emit('components:changed', undefined);
|
|
90
83
|
}
|
|
91
84
|
|
|
92
85
|
registerMiddleware<T>(
|
|
@@ -111,7 +104,7 @@ export class Registry {
|
|
|
111
104
|
|
|
112
105
|
this.middlewares.set(hookPoint, newList);
|
|
113
106
|
|
|
114
|
-
this.
|
|
107
|
+
this.bus.emit('middleware:changed', hookPoint);
|
|
115
108
|
}
|
|
116
109
|
|
|
117
110
|
getComponents(slotName: SlotName): React.ComponentType<IComPropsRegisteredToSlot>[] {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export interface EventBus<Events extends Record<string, unknown>> {
|
|
2
|
+
on<K extends keyof Events>(event: K, callback: (data: Events[K]) => void): () => void;
|
|
3
|
+
emit<K extends keyof Events>(event: K, data: Events[K]): void;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function createEventBus<Events extends Record<string, unknown>>(): EventBus<Events> {
|
|
7
|
+
const listeners = new Map<keyof Events, Set<(data: Events[keyof Events]) => void>>();
|
|
8
|
+
|
|
9
|
+
return {
|
|
10
|
+
on<K extends keyof Events>(event: K, callback: (data: Events[K]) => void): () => void {
|
|
11
|
+
if (!listeners.has(event)) {
|
|
12
|
+
listeners.set(event, new Set());
|
|
13
|
+
}
|
|
14
|
+
const set = listeners.get(event)!;
|
|
15
|
+
const cb = callback as (data: Events[keyof Events]) => void;
|
|
16
|
+
set.add(cb);
|
|
17
|
+
|
|
18
|
+
return () => {
|
|
19
|
+
set.delete(cb);
|
|
20
|
+
};
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
emit<K extends keyof Events>(event: K, data: Events[K]): void {
|
|
24
|
+
const set = listeners.get(event);
|
|
25
|
+
if (!set) return;
|
|
26
|
+
set.forEach(callback => callback(data as Events[keyof Events]));
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
package/src/plugins/xwy/index.ts
CHANGED
|
@@ -75,11 +75,11 @@ class XwyPlugin implements IPlugin {
|
|
|
75
75
|
return content
|
|
76
76
|
.replace(
|
|
77
77
|
/小乌鸦/g,
|
|
78
|
-
`<span class="font-xwy text-content-special text-[1.6rem] align-middle leading-[
|
|
78
|
+
`<span class="font-xwy text-content-special text-[1.6rem] align-middle leading-[1.09]">小乌鸦</span>`
|
|
79
79
|
)
|
|
80
80
|
.replace(
|
|
81
81
|
/公园/,
|
|
82
|
-
`公园 <span class="font-xwy-icon text-[#679867] text-[1.6rem] align-middle leading-[
|
|
82
|
+
`公园 <span class="font-xwy-icon text-[#679867] text-[1.6rem] align-middle leading-[1.09]"></span>`
|
|
83
83
|
);
|
|
84
84
|
};
|
|
85
85
|
|
package/src/types/hostApi.ts
CHANGED
|
@@ -3,9 +3,11 @@ import React from 'react';
|
|
|
3
3
|
import { type FingerprintData } from '@/utils/fingerprints';
|
|
4
4
|
|
|
5
5
|
import { HookPoint, IComPropsRegisteredToSlot, SlotName } from './slots';
|
|
6
|
+
import { PluginID } from './plugin';
|
|
6
7
|
|
|
7
8
|
export interface IHostApi {
|
|
8
9
|
getDeviceId: () => Promise<{ id: string; fp: FingerprintData }>;
|
|
10
|
+
setLoading: (id: PluginID, loading: boolean) => void;
|
|
9
11
|
registerMiddleware: <T>(
|
|
10
12
|
point: HookPoint,
|
|
11
13
|
fn: (baseData: T) => T,
|
package/src/types/plugin.ts
CHANGED
|
@@ -23,3 +23,12 @@ export interface IPluginEntry {
|
|
|
23
23
|
label?: string;
|
|
24
24
|
pageComponent: React.ComponentType<IComPropsRegisteredToSlot>;
|
|
25
25
|
}
|
|
26
|
+
|
|
27
|
+
export type PluginEvents = {
|
|
28
|
+
'plugin:loading:changed': IPluginLoadingPayload;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export interface IPluginLoadingPayload {
|
|
32
|
+
id: PluginID;
|
|
33
|
+
loading: boolean;
|
|
34
|
+
}
|