@bbki.ng/site 5.6.2 → 5.6.4
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 +12 -0
- package/package.json +1 -1
- package/src/blog/utils/fingerprints.ts +8 -3
- package/src/core/bbplugin.ts +3 -2
- package/src/core/hooks/use_plugins.ts +34 -14
- package/src/core/pluginManager.ts +9 -0
- package/src/core/pluginManifestService.ts +123 -0
- package/src/core/pluginStore.ts +6 -4
- package/src/plugins/fx/components/index.tsx +19 -10
- package/src/plugins/fx/context/index.ts +4 -0
- package/src/plugins/fx/index.ts +3 -0
- package/src/plugins/manifest.ts +3 -9
- package/src/plugins/now/context/index.ts +3 -0
- package/src/plugins/now/hooks/use_streaming.ts +12 -41
- package/src/plugins/now/index.ts +1 -0
- package/src/plugins/store/components/storePage.tsx +19 -9
- package/src/plugins/store/index.ts +7 -5
- package/src/types/hostApi.ts +6 -1
- package/src/types/plugin.ts +3 -0
package/CHANGELOG.md
CHANGED
package/package.json
CHANGED
|
@@ -9,6 +9,8 @@ export interface FingerprintData {
|
|
|
9
9
|
generatedAt: number;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
type GLenum = number;
|
|
13
|
+
|
|
12
14
|
export interface FingerprintComponents {
|
|
13
15
|
// 基础环境
|
|
14
16
|
userAgent: string;
|
|
@@ -143,7 +145,7 @@ function getWebGLInfo(): WebGLInfo {
|
|
|
143
145
|
|
|
144
146
|
params.forEach(p => {
|
|
145
147
|
try {
|
|
146
|
-
const val = gl.getParameter((gl as
|
|
148
|
+
const val = gl.getParameter((gl as unknown as Record<string, GLenum>)[p] as GLenum);
|
|
147
149
|
result.params[p] = Array.isArray(val) ? val.join(',') : String(val);
|
|
148
150
|
} catch {
|
|
149
151
|
result.params[p] = 'unsupported';
|
|
@@ -220,7 +222,10 @@ function getFontList(): string[] {
|
|
|
220
222
|
// 音频指纹(频率响应差异)
|
|
221
223
|
async function getAudioFingerprint(): Promise<string | undefined> {
|
|
222
224
|
try {
|
|
223
|
-
const AudioContext =
|
|
225
|
+
const AudioContext =
|
|
226
|
+
window.OfflineAudioContext ||
|
|
227
|
+
(window as unknown as { webkitOfflineAudioContext?: typeof OfflineAudioContext })
|
|
228
|
+
.webkitOfflineAudioContext;
|
|
224
229
|
if (!AudioContext) return undefined;
|
|
225
230
|
|
|
226
231
|
const ctx = new AudioContext(1, 44100, 44100);
|
|
@@ -271,7 +276,7 @@ async function getFingerprint(): Promise<FingerprintData> {
|
|
|
271
276
|
platform: navigator.platform,
|
|
272
277
|
cookieEnabled: navigator.cookieEnabled,
|
|
273
278
|
hardwareConcurrency: navigator.hardwareConcurrency || 0,
|
|
274
|
-
deviceMemory: (navigator as
|
|
279
|
+
deviceMemory: (navigator as unknown as { deviceMemory?: number }).deviceMemory,
|
|
275
280
|
maxTouchPoints: navigator.maxTouchPoints || 0,
|
|
276
281
|
screenResolution: `${screen.width}x${screen.height}`,
|
|
277
282
|
screenColorDepth: screen.colorDepth,
|
package/src/core/bbplugin.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import { ManifestMap } from '#/plugins/manifest';
|
|
2
1
|
import { IHostContext } from '#/types/hostApi';
|
|
3
2
|
import { IPlugin, IPluginManifestEntry, PluginID } from '#/types/plugin';
|
|
4
3
|
|
|
4
|
+
import { PluginManifestService } from './pluginManifestService';
|
|
5
|
+
|
|
5
6
|
export class BBPlugin implements IPlugin {
|
|
6
7
|
id: PluginID = 'default';
|
|
7
8
|
onInstall?: ((ctx: IHostContext) => void | Promise<void>) | undefined;
|
|
8
9
|
onDisable?: (() => Promise<void> | void) | undefined;
|
|
9
10
|
onDestroy?: (() => void) | undefined;
|
|
10
11
|
getMeta(): IPluginManifestEntry {
|
|
11
|
-
return
|
|
12
|
+
return PluginManifestService.getInstance().getPlugin(this.id) as IPluginManifestEntry;
|
|
12
13
|
}
|
|
13
14
|
}
|
|
@@ -1,13 +1,18 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { 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 { PluginManifestService } from '../pluginManifestService';
|
|
7
8
|
import { PluginStore } from '../pluginStore';
|
|
8
9
|
|
|
9
10
|
const usePluginsLoading = () => {
|
|
10
|
-
const { setGlobalLoading } = useGlobalLoading();
|
|
11
|
+
const { setGlobalLoading, isLoading } = useGlobalLoading();
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
pluginManager.notifySiteLoadingChanged(isLoading);
|
|
15
|
+
}, [isLoading]);
|
|
11
16
|
|
|
12
17
|
useEffect(() => {
|
|
13
18
|
let unregister: (() => void) | undefined;
|
|
@@ -42,26 +47,41 @@ export const usePlugins = () => {
|
|
|
42
47
|
|
|
43
48
|
usePluginsLoading();
|
|
44
49
|
|
|
45
|
-
const loadAllPlugins = useCallback(async () => {
|
|
46
|
-
try {
|
|
47
|
-
await Promise.all(pluginIds.map(id => pluginManager.loadPlugin(id)));
|
|
48
|
-
setDone(true);
|
|
49
|
-
} catch (error) {
|
|
50
|
-
console.error('Error loading plugins:', error);
|
|
51
|
-
}
|
|
52
|
-
}, [pluginIds]);
|
|
53
|
-
|
|
54
50
|
useEffect(() => {
|
|
55
|
-
|
|
56
|
-
|
|
51
|
+
let cancelled = false;
|
|
52
|
+
|
|
53
|
+
PluginManifestService.getInstance()
|
|
54
|
+
.fetch()
|
|
55
|
+
.then(() => {
|
|
56
|
+
if (cancelled) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
Promise.all(pluginIds.map(id => pluginManager.loadPlugin(id)))
|
|
60
|
+
.then(() => {
|
|
61
|
+
if (!cancelled) {
|
|
62
|
+
setDone(true);
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
.catch(error => {
|
|
66
|
+
if (!cancelled) {
|
|
67
|
+
console.error('Error loading plugins:', error);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
})
|
|
71
|
+
.catch(error => {
|
|
72
|
+
if (!cancelled) {
|
|
73
|
+
console.error('Error fetching plugin manifest:', error);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
57
76
|
|
|
58
77
|
return () => {
|
|
78
|
+
cancelled = true;
|
|
59
79
|
// 卸载所有指定的插件
|
|
60
80
|
pluginIds.forEach(id => {
|
|
61
81
|
pluginManager.disablePlugin(id);
|
|
62
82
|
});
|
|
63
83
|
};
|
|
64
|
-
}, [
|
|
84
|
+
}, [pluginIds]);
|
|
65
85
|
|
|
66
86
|
return done;
|
|
67
87
|
};
|
|
@@ -4,6 +4,7 @@ import { IHostContext } from '#/types/hostApi';
|
|
|
4
4
|
import type { SlotName, HookPoint, IComPropsRegisteredToSlot } from '#/types/slots';
|
|
5
5
|
import { IPlugin, PluginEvents, PluginID, PluginPerm } from '#/types/plugin';
|
|
6
6
|
import { getStableDeviceId } from '@/utils/fingerprints';
|
|
7
|
+
import { cfApiFetcher } from '@/utils';
|
|
7
8
|
|
|
8
9
|
import { registry } from './registry';
|
|
9
10
|
import { createEventBus } from './utils/eventBus';
|
|
@@ -27,9 +28,13 @@ class PluginManager {
|
|
|
27
28
|
return {
|
|
28
29
|
...adminCtx,
|
|
29
30
|
api: {
|
|
31
|
+
fetch: cfApiFetcher,
|
|
30
32
|
setLoading: (id: PluginID, loading: boolean) => {
|
|
31
33
|
this.bus.emit('plugin:loading:changed', { id, loading });
|
|
32
34
|
},
|
|
35
|
+
onLoadingChanged: (listener: (payload: PluginEvents['site:loading:changed']) => void) => {
|
|
36
|
+
return this.bus.on('site:loading:changed', listener);
|
|
37
|
+
},
|
|
33
38
|
registerMiddleware: <T>(
|
|
34
39
|
point: HookPoint,
|
|
35
40
|
fn: (data: T) => Promise<T> | T,
|
|
@@ -58,6 +63,10 @@ class PluginManager {
|
|
|
58
63
|
|
|
59
64
|
private loading = new Set<string>();
|
|
60
65
|
|
|
66
|
+
notifySiteLoadingChanged(loading: boolean) {
|
|
67
|
+
this.bus.emit('site:loading:changed', loading);
|
|
68
|
+
}
|
|
69
|
+
|
|
61
70
|
subscribePluginLoading(listener: (payload: PluginEvents['plugin:loading:changed']) => void) {
|
|
62
71
|
return this.bus.on('plugin:loading:changed', listener);
|
|
63
72
|
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { cfApiFetcher } from '@/utils';
|
|
2
|
+
import { FALLBACK_MANIFEST } from '#/plugins/manifest';
|
|
3
|
+
import { IPluginManifestEntry, PluginID } from '#/types/plugin';
|
|
4
|
+
|
|
5
|
+
const KnownPluginIDSet = new Set<string>([
|
|
6
|
+
'sticker',
|
|
7
|
+
'xwy',
|
|
8
|
+
'extra-cd',
|
|
9
|
+
'extra-entry',
|
|
10
|
+
'store',
|
|
11
|
+
'now',
|
|
12
|
+
'default',
|
|
13
|
+
'fx',
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
interface PluginsApiResponse {
|
|
17
|
+
status: string;
|
|
18
|
+
data: Array<{
|
|
19
|
+
id: string;
|
|
20
|
+
name: string;
|
|
21
|
+
version: string;
|
|
22
|
+
description?: string;
|
|
23
|
+
perm?: string;
|
|
24
|
+
icon?: string;
|
|
25
|
+
dependencies?: string;
|
|
26
|
+
}>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
class PluginManifestService {
|
|
30
|
+
private static instance: PluginManifestService;
|
|
31
|
+
|
|
32
|
+
private manifestMap: Map<string, IPluginManifestEntry> = new Map();
|
|
33
|
+
|
|
34
|
+
private fetched = false;
|
|
35
|
+
|
|
36
|
+
private fetchPromise: Promise<void> | null = null;
|
|
37
|
+
|
|
38
|
+
private constructor() {
|
|
39
|
+
this.initWithFallback();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private initWithFallback = () => {
|
|
43
|
+
this.manifestMap = new Map(FALLBACK_MANIFEST.map(entry => [entry.id, entry]));
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
async fetch(): Promise<void> {
|
|
47
|
+
if (this.fetched) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (this.fetchPromise) {
|
|
51
|
+
return this.fetchPromise;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
this.fetchPromise = (async () => {
|
|
55
|
+
try {
|
|
56
|
+
const res = await cfApiFetcher<PluginsApiResponse>('plugins');
|
|
57
|
+
if (res.status === 'success' && Array.isArray(res.data)) {
|
|
58
|
+
const validated = res.data
|
|
59
|
+
.map((item): IPluginManifestEntry | null => {
|
|
60
|
+
if (!KnownPluginIDSet.has(item.id)) {
|
|
61
|
+
console.warn(`[PluginManifestService] Unknown plugin id from server: ${item.id}`);
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let dependencies: PluginID[] | undefined;
|
|
66
|
+
if (item.dependencies) {
|
|
67
|
+
try {
|
|
68
|
+
const parsed = JSON.parse(item.dependencies) as string[];
|
|
69
|
+
dependencies = parsed.filter((id): id is PluginID => KnownPluginIDSet.has(id));
|
|
70
|
+
} catch {
|
|
71
|
+
console.warn(
|
|
72
|
+
`[PluginManifestService] Failed to parse dependencies for plugin: ${item.id}`
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
id: item.id as PluginID,
|
|
79
|
+
name: item.name,
|
|
80
|
+
version: item.version,
|
|
81
|
+
description: item.description,
|
|
82
|
+
perm: (item.perm as 'guest' | 'admin' | undefined) || 'guest',
|
|
83
|
+
icon: item.icon,
|
|
84
|
+
dependencies,
|
|
85
|
+
};
|
|
86
|
+
})
|
|
87
|
+
.filter((item): item is IPluginManifestEntry => item !== null);
|
|
88
|
+
|
|
89
|
+
this.manifestMap = new Map(validated.map(entry => [entry.id, entry]));
|
|
90
|
+
}
|
|
91
|
+
this.fetched = true;
|
|
92
|
+
} catch (error) {
|
|
93
|
+
console.error('[PluginManifestService] Failed to fetch manifest:', error);
|
|
94
|
+
// Keep fallback data
|
|
95
|
+
}
|
|
96
|
+
})();
|
|
97
|
+
|
|
98
|
+
return this.fetchPromise;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
getPlugin(id: string): IPluginManifestEntry | undefined {
|
|
102
|
+
return this.manifestMap.get(id);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
getAllPlugins(): IPluginManifestEntry[] {
|
|
106
|
+
return Array.from(this.manifestMap.values());
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
reset(): void {
|
|
110
|
+
this.fetched = false;
|
|
111
|
+
this.fetchPromise = null;
|
|
112
|
+
this.initWithFallback();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
static getInstance() {
|
|
116
|
+
if (!PluginManifestService.instance) {
|
|
117
|
+
PluginManifestService.instance = new PluginManifestService();
|
|
118
|
+
}
|
|
119
|
+
return PluginManifestService.instance;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export { PluginManifestService };
|
package/src/core/pluginStore.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { PLUGIN_MANIFEST } from '#/plugins/manifest';
|
|
2
1
|
import { IPluginStoreEntry, PluginID } from '#/types/plugin';
|
|
3
2
|
|
|
4
3
|
import { pluginManager } from './pluginManager';
|
|
4
|
+
import { PluginManifestService } from './pluginManifestService';
|
|
5
5
|
|
|
6
6
|
export class PluginStore {
|
|
7
7
|
private static instance: PluginStore;
|
|
@@ -35,13 +35,15 @@ export class PluginStore {
|
|
|
35
35
|
return this.parse();
|
|
36
36
|
};
|
|
37
37
|
|
|
38
|
-
getAllPlugins
|
|
38
|
+
async getAllPlugins(): Promise<Array<IPluginStoreEntry>> {
|
|
39
39
|
const installedPlugins = this.getInstalledPlugins();
|
|
40
|
-
|
|
40
|
+
await PluginManifestService.getInstance().fetch();
|
|
41
|
+
const manifest = PluginManifestService.getInstance().getAllPlugins();
|
|
42
|
+
return manifest.map(plugin => ({
|
|
41
43
|
...plugin,
|
|
42
44
|
enabled: installedPlugins.has(plugin.id),
|
|
43
45
|
}));
|
|
44
|
-
}
|
|
46
|
+
}
|
|
45
47
|
|
|
46
48
|
isPluginInstalled: (id: PluginID) => boolean = id => {
|
|
47
49
|
return this.getInstalledPlugins().has(id);
|
|
@@ -1,30 +1,39 @@
|
|
|
1
1
|
import React, { useMemo } from 'react';
|
|
2
|
-
import { EffectLayer, grain, paper,
|
|
2
|
+
import { EffectLayer, grain, paper, spiral, watermark, Effect } from '@bbki.ng/ui';
|
|
3
3
|
|
|
4
4
|
import { IComPropsRegisteredToSlot } from '#/types/slots';
|
|
5
5
|
|
|
6
6
|
import { FxContext } from '../context';
|
|
7
7
|
|
|
8
|
+
const useLoadingState = () => {
|
|
9
|
+
const ctx = FxContext.useCtx();
|
|
10
|
+
const [isLoading, setIsLoading] = React.useState(false);
|
|
11
|
+
|
|
12
|
+
React.useEffect(() => {
|
|
13
|
+
const unsubscribe = ctx.subscribeToLoading(setIsLoading);
|
|
14
|
+
return () => {
|
|
15
|
+
unsubscribe();
|
|
16
|
+
};
|
|
17
|
+
}, [ctx]);
|
|
18
|
+
|
|
19
|
+
return isLoading;
|
|
20
|
+
};
|
|
21
|
+
|
|
8
22
|
export const FxCom = (_: IComPropsRegisteredToSlot) => {
|
|
9
23
|
const ctx = FxContext.useCtx();
|
|
10
24
|
const hashStr = ctx.versionHash;
|
|
11
25
|
const deviceId = ctx.deviceId;
|
|
26
|
+
const isLoading = useLoadingState();
|
|
12
27
|
|
|
13
|
-
|
|
14
|
-
// const { deviceId } = useFingerprint();
|
|
28
|
+
console.log('is loading', isLoading);
|
|
15
29
|
|
|
16
30
|
const effects: Effect[] = useMemo(() => {
|
|
17
31
|
const wmLines = [hashStr];
|
|
18
32
|
if (deviceId) wmLines.push(deviceId);
|
|
19
33
|
wmLines.push('hello world');
|
|
20
34
|
|
|
21
|
-
return [
|
|
22
|
-
|
|
23
|
-
paper(),
|
|
24
|
-
// spiral({ active: isLoading }),
|
|
25
|
-
watermark({ lines: wmLines }),
|
|
26
|
-
];
|
|
27
|
-
}, [/*isLoading,*/ deviceId, hashStr]);
|
|
35
|
+
return [grain(), paper(), spiral({ active: isLoading }), watermark({ lines: wmLines })];
|
|
36
|
+
}, [isLoading, deviceId, hashStr]);
|
|
28
37
|
|
|
29
38
|
return <EffectLayer effects={effects} />;
|
|
30
39
|
};
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { createPluginCtx } from '#/core/context';
|
|
2
|
+
import { PluginEvents } from '#/types/plugin';
|
|
2
3
|
|
|
3
4
|
export interface IFxContext {
|
|
4
5
|
versionHash: string;
|
|
5
6
|
deviceId: string;
|
|
7
|
+
subscribeToLoading: (
|
|
8
|
+
listener: (payload: PluginEvents['site:loading:changed']) => void
|
|
9
|
+
) => () => void;
|
|
6
10
|
}
|
|
7
11
|
|
|
8
12
|
export const FxContext = createPluginCtx<IFxContext>();
|
package/src/plugins/fx/index.ts
CHANGED
package/src/plugins/manifest.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { IPluginManifestEntry } from '#/types/plugin';
|
|
2
2
|
|
|
3
|
-
export const
|
|
3
|
+
export const FALLBACK_MANIFEST: Array<IPluginManifestEntry> = [
|
|
4
4
|
{
|
|
5
5
|
name: '贴纸',
|
|
6
6
|
id: 'sticker',
|
|
@@ -25,14 +25,13 @@ export const PLUGIN_MANIFEST: Array<IPluginManifestEntry> = [
|
|
|
25
25
|
id: 'extra-cd',
|
|
26
26
|
version: '0.1.0',
|
|
27
27
|
description:
|
|
28
|
-
'
|
|
28
|
+
'提供额外的页面跳转链接。例如在标题列表末尾添加一个 "cd ~" 的选项,点击后跳转到主页',
|
|
29
29
|
},
|
|
30
30
|
{
|
|
31
31
|
name: '特效',
|
|
32
32
|
id: 'fx',
|
|
33
33
|
version: '0.1.0',
|
|
34
|
-
description:
|
|
35
|
-
'提供一些额外的视觉效果。此外,页面左下角会显示当前版本的哈希值和设备标识哈希水印。',
|
|
34
|
+
description: '提供一些额外的视觉效果。例如版本、设备信息水印,加载状态螺旋线、背景纹理等',
|
|
36
35
|
},
|
|
37
36
|
// {
|
|
38
37
|
// name: 'extra-entry',
|
|
@@ -47,8 +46,3 @@ export const PLUGIN_MANIFEST: Array<IPluginManifestEntry> = [
|
|
|
47
46
|
description: '为小乌鸦合集实现定制功能或者样式的插件',
|
|
48
47
|
},
|
|
49
48
|
];
|
|
50
|
-
|
|
51
|
-
export const ManifestMap = PLUGIN_MANIFEST.reduce((map, entry) => {
|
|
52
|
-
map.set(entry.id, entry);
|
|
53
|
-
return map;
|
|
54
|
-
}, new Map<string, IPluginManifestEntry>());
|
|
@@ -1,15 +1,8 @@
|
|
|
1
|
-
import useSWR from 'swr';
|
|
1
|
+
import useSWR, { type BareFetcher } from 'swr';
|
|
2
2
|
import { useEffect, useState } from 'react';
|
|
3
3
|
|
|
4
|
-
import { baseFetcher } from '@/utils';
|
|
5
|
-
|
|
6
4
|
import { NowCtx } from '../context';
|
|
7
5
|
|
|
8
|
-
// In dev, use /api prefix to leverage Vite proxy to localhost:8787
|
|
9
|
-
// const isProd = typeof window !== 'undefined' && /^https:\/\/bbki.ng/.test(window.location.href);
|
|
10
|
-
const isProd = true;
|
|
11
|
-
const API_BASE = !isProd ? '/api' : 'https://cf.bbki.ng';
|
|
12
|
-
|
|
13
6
|
export type StreamingItem = {
|
|
14
7
|
id: string;
|
|
15
8
|
author: string;
|
|
@@ -32,41 +25,13 @@ export interface StreamingQueryParams {
|
|
|
32
25
|
offset?: number;
|
|
33
26
|
}
|
|
34
27
|
|
|
35
|
-
/**
|
|
36
|
-
* Build streaming API URL with query parameters
|
|
37
|
-
*/
|
|
38
|
-
function buildStreamingUrl(params: StreamingQueryParams = {}): string {
|
|
39
|
-
const url = new URL(`${API_BASE}/streaming`, !isProd ? window.location.origin : undefined);
|
|
40
|
-
|
|
41
|
-
if (params.before) {
|
|
42
|
-
url.searchParams.set('before', params.before);
|
|
43
|
-
}
|
|
44
|
-
if (params.after) {
|
|
45
|
-
url.searchParams.set('after', params.after);
|
|
46
|
-
}
|
|
47
|
-
if (params.offset) {
|
|
48
|
-
url.searchParams.set('offset', params.offset.toString());
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
return !isProd ? url.pathname + url.search : url.toString();
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Fetch streaming data from API
|
|
56
|
-
*/
|
|
57
|
-
async function fetchStreaming(params: StreamingQueryParams = {}): Promise<StreamingResponse> {
|
|
58
|
-
const url = buildStreamingUrl(params);
|
|
59
|
-
const response = await baseFetcher(url);
|
|
60
|
-
return response as StreamingResponse;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
28
|
// SWR key generator for streaming queries
|
|
64
29
|
const getStreamingKey = (params: StreamingQueryParams) => {
|
|
65
30
|
const parts = ['streaming'];
|
|
66
31
|
if (params.before) parts.push(`before=${params.before}`);
|
|
67
32
|
if (params.after) parts.push(`after=${params.after}`);
|
|
68
33
|
if (params.offset) parts.push(`offset=${params.offset}`);
|
|
69
|
-
return parts
|
|
34
|
+
return parts;
|
|
70
35
|
};
|
|
71
36
|
|
|
72
37
|
interface UseStreamingOptions {
|
|
@@ -83,12 +48,18 @@ interface UseStreamingOptions {
|
|
|
83
48
|
export function useStreaming(options: UseStreamingOptions = {}) {
|
|
84
49
|
const { refreshInterval = 1000, offset = 8 } = options;
|
|
85
50
|
|
|
51
|
+
const { fetch } = NowCtx.useCtx();
|
|
52
|
+
|
|
86
53
|
const key = getStreamingKey({ offset });
|
|
87
54
|
|
|
88
|
-
const { data, error, mutate } = useSWR(
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
55
|
+
const { data, error, mutate } = useSWR<StreamingResponse>(
|
|
56
|
+
key,
|
|
57
|
+
fetch as BareFetcher<StreamingResponse>,
|
|
58
|
+
{
|
|
59
|
+
revalidateOnFocus: true,
|
|
60
|
+
refreshInterval,
|
|
61
|
+
}
|
|
62
|
+
);
|
|
92
63
|
|
|
93
64
|
const isLoading = !data && !error;
|
|
94
65
|
|
package/src/plugins/now/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React, { useCallback, useEffect, useState } from 'react';
|
|
2
|
-
import
|
|
2
|
+
import classNames from 'classnames';
|
|
3
|
+
import { Button, Table, Link } from '@bbki.ng/ui';
|
|
3
4
|
|
|
4
5
|
import { IComPropsRegisteredToSlot } from '#/types/slots';
|
|
5
6
|
import { IPluginStoreEntry, PluginID } from '#/types/plugin';
|
|
@@ -163,13 +164,22 @@ export const StorePage = (_: IComPropsRegisteredToSlot) => {
|
|
|
163
164
|
}
|
|
164
165
|
|
|
165
166
|
return (
|
|
166
|
-
|
|
167
|
-
<
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
167
|
+
<>
|
|
168
|
+
<div className="prose">
|
|
169
|
+
<Table
|
|
170
|
+
className="w-full"
|
|
171
|
+
rowCount={plugins.length}
|
|
172
|
+
headerRenderer={headerRenderer}
|
|
173
|
+
rowRenderer={rowRenderer}
|
|
174
|
+
/>
|
|
175
|
+
</div>
|
|
176
|
+
<Link
|
|
177
|
+
className={classNames('w-fit relative transition-all duration-300 mt-16')}
|
|
178
|
+
to="/"
|
|
179
|
+
style={{ left: -4 }}
|
|
180
|
+
>
|
|
181
|
+
cd ..
|
|
182
|
+
</Link>
|
|
183
|
+
</>
|
|
174
184
|
);
|
|
175
185
|
};
|
|
@@ -6,7 +6,7 @@ import { buildEntryCreator } from '#/utils';
|
|
|
6
6
|
import { StorePage } from './components/storePage';
|
|
7
7
|
import { StoreCtx } from './context';
|
|
8
8
|
|
|
9
|
-
export class
|
|
9
|
+
export class StorePlugin extends BBPlugin {
|
|
10
10
|
id: PluginID = 'store';
|
|
11
11
|
override onInstall = async (ctx: IHostContext): Promise<void> => {
|
|
12
12
|
const entryCreator = buildEntryCreator(ctx);
|
|
@@ -19,9 +19,11 @@ export class PluginStore extends BBPlugin {
|
|
|
19
19
|
install: ctx.store?.installPlugin || (async () => {}),
|
|
20
20
|
uninstall: ctx.store?.disablePlugin || (async () => {}),
|
|
21
21
|
isInstalled: ctx.store?.isPluginInstalled || ((_: PluginID) => false),
|
|
22
|
-
list: () => {
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
list: async () => {
|
|
23
|
+
ctx.api.setLoading('store', true);
|
|
24
|
+
const all = Array.from((await ctx.store?.getAllPlugins()) || []);
|
|
25
|
+
ctx.api.setLoading('store', false);
|
|
26
|
+
return all.filter(({ id }) => id !== this.id);
|
|
25
27
|
},
|
|
26
28
|
setLoading: (loading: boolean) => {
|
|
27
29
|
ctx.api.setLoading('store', loading);
|
|
@@ -35,4 +37,4 @@ export class PluginStore extends BBPlugin {
|
|
|
35
37
|
};
|
|
36
38
|
}
|
|
37
39
|
|
|
38
|
-
export default new
|
|
40
|
+
export default new StorePlugin();
|
package/src/types/hostApi.ts
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
import { Fetcher } from 'swr';
|
|
2
3
|
|
|
3
4
|
import { type FingerprintData } from '@/utils/fingerprints';
|
|
4
5
|
import { PluginStore } from '#/core/pluginStore';
|
|
5
6
|
|
|
6
7
|
import { HookPoint, IComPropsRegisteredToSlot, SlotName } from './slots';
|
|
7
|
-
import { PluginID } from './plugin';
|
|
8
|
+
import { PluginEvents, PluginID } from './plugin';
|
|
8
9
|
|
|
9
10
|
export interface IHostApi {
|
|
10
11
|
getDeviceId: () => Promise<{ id: string; fp: FingerprintData }>;
|
|
11
12
|
getVersionHash: () => Promise<string> | string;
|
|
12
13
|
setLoading: (id: PluginID, loading: boolean) => void;
|
|
14
|
+
onLoadingChanged: (
|
|
15
|
+
listener: (payload: PluginEvents['site:loading:changed']) => void
|
|
16
|
+
) => () => void;
|
|
13
17
|
registerMiddleware: <T>(
|
|
14
18
|
point: HookPoint,
|
|
15
19
|
fn: (baseData: T) => T,
|
|
@@ -22,6 +26,7 @@ export interface IHostApi {
|
|
|
22
26
|
pluginId: string,
|
|
23
27
|
weight?: number
|
|
24
28
|
) => void;
|
|
29
|
+
fetch: Fetcher;
|
|
25
30
|
}
|
|
26
31
|
|
|
27
32
|
export type PluginInitializer = (api: IHostApi) => void;
|
package/src/types/plugin.ts
CHANGED
|
@@ -31,6 +31,7 @@ export interface IPluginEntry {
|
|
|
31
31
|
|
|
32
32
|
export type PluginEvents = {
|
|
33
33
|
'plugin:loading:changed': IPluginLoadingPayload;
|
|
34
|
+
'site:loading:changed': boolean;
|
|
34
35
|
};
|
|
35
36
|
|
|
36
37
|
export interface IPluginLoadingPayload {
|
|
@@ -46,6 +47,8 @@ export interface IPluginManifestEntry {
|
|
|
46
47
|
version: string;
|
|
47
48
|
description?: string;
|
|
48
49
|
perm?: PluginPerm;
|
|
50
|
+
icon?: string;
|
|
51
|
+
dependencies?: PluginID[];
|
|
49
52
|
}
|
|
50
53
|
|
|
51
54
|
export interface IPluginStoreEntry extends IPluginManifestEntry {
|