@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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # @bbki.ng/site
2
2
 
3
+ ## 5.6.4
4
+
5
+ ### Patch Changes
6
+
7
+ - d1b95ed: add api to fetch manifest
8
+
9
+ ## 5.6.3
10
+
11
+ ### Patch Changes
12
+
13
+ - 2d1528a: fix fx loading state
14
+
3
15
  ## 5.6.2
4
16
 
5
17
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bbki.ng/site",
3
- "version": "5.6.2",
3
+ "version": "5.6.4",
4
4
  "description": "code behind bbki.ng",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -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 any)[p]);
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 = window.OfflineAudioContext || (window as any).webkitOfflineAudioContext;
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 any).deviceMemory,
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,
@@ -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 ManifestMap.get(this.id) as IPluginManifestEntry;
12
+ return PluginManifestService.getInstance().getPlugin(this.id) as IPluginManifestEntry;
12
13
  }
13
14
  }
@@ -1,13 +1,18 @@
1
- import { useCallback, useEffect, useMemo, useState } from 'react';
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
- loadAllPlugins();
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
- }, [loadAllPlugins, pluginIds]);
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 };
@@ -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: () => Array<IPluginStoreEntry> = () => {
38
+ async getAllPlugins(): Promise<Array<IPluginStoreEntry>> {
39
39
  const installedPlugins = this.getInstalledPlugins();
40
- return PLUGIN_MANIFEST.map(plugin => ({
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, /*spiral,*/ watermark, Effect } from '@bbki.ng/ui';
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
- // const { isLoading } = useContext(GlobalLoadingContext);
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
- grain(),
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>();
@@ -15,6 +15,9 @@ class FxPlugin extends BBPlugin {
15
15
  {
16
16
  versionHash: await ctx.api.getVersionHash(),
17
17
  deviceId: (await ctx.api.getDeviceId()).id,
18
+ subscribeToLoading: listener => {
19
+ return ctx.api.onLoadingChanged(listener);
20
+ },
18
21
  },
19
22
  FxCom
20
23
  ),
@@ -1,6 +1,6 @@
1
1
  import { IPluginManifestEntry } from '#/types/plugin';
2
2
 
3
- export const PLUGIN_MANIFEST: Array<IPluginManifestEntry> = [
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
- '提供额外的快速切换页面的捷径,例如在标题列表末尾添加一个 "cd ~" 的选项,点击后跳转到主页',
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,7 +1,10 @@
1
+ import { Fetcher } from 'swr';
2
+
1
3
  import { createPluginCtx } from '#/core/context';
2
4
 
3
5
  export interface INowCtx {
4
6
  setLoading: (loading: boolean) => void;
7
+ fetch: Fetcher;
5
8
  }
6
9
 
7
10
  export const NowCtx = createPluginCtx<INowCtx>();
@@ -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.join('?');
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(key, () => fetchStreaming({ offset }), {
89
- revalidateOnFocus: true,
90
- refreshInterval,
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
 
@@ -22,6 +22,7 @@ export class NowPlugin extends BBPlugin {
22
22
  setLoading: loading => {
23
23
  ctx.api.setLoading('now', loading);
24
24
  },
25
+ fetch: ctx.api.fetch,
25
26
  },
26
27
  pageNow
27
28
  ),
@@ -1,5 +1,6 @@
1
1
  import React, { useCallback, useEffect, useState } from 'react';
2
- import { Button, Table } from '@bbki.ng/ui';
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
- <div className="prose">
167
- <Table
168
- className="w-full"
169
- rowCount={plugins.length}
170
- headerRenderer={headerRenderer}
171
- rowRenderer={rowRenderer}
172
- />
173
- </div>
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 PluginStore extends BBPlugin {
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
- const installed = Array.from(ctx.store?.getAllPlugins() || []);
24
- return installed.filter(({ id }) => id !== this.id);
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 PluginStore();
40
+ export default new StorePlugin();
@@ -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;
@@ -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 {