@bbki.ng/site 5.6.0 → 5.6.2

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.2
4
+
5
+ ### Patch Changes
6
+
7
+ - 72a0b48: add fx plugin
8
+
9
+ ## 5.6.1
10
+
11
+ ### Patch Changes
12
+
13
+ - e4f8846: refactor
14
+
3
15
  ## 5.6.0
4
16
 
5
17
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bbki.ng/site",
3
- "version": "5.6.0",
3
+ "version": "5.6.2",
4
4
  "description": "code behind bbki.ng",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -19,39 +19,42 @@ export const BaseLayout = () => {
19
19
  );
20
20
 
21
21
  return (
22
- <Page
23
- nav={
24
- <Nav
25
- paths={transformedPaths}
26
- className="gradient-blur-cover select-none"
27
- loading={isLoading}
28
- customLogo={<Slot name="logo" data={defaultLogo} placeholder={defaultLogo} />}
29
- style={{
30
- paddingTop: 'calc(var(--safe-top) + 4px)',
31
- transition: 'all .2s ease-in-out',
32
- }}
33
- />
34
- }
35
- main={
36
- <Grid
37
- leftAside={
38
- <div className="py-32 px-6">
39
- <Slot name="leftCol" data={paths} />
40
- </div>
41
- }
42
- rightAside={
43
- <div className="py-32 px-6">
44
- <Slot name="rightCol" data={paths} />
45
- </div>
46
- }
47
- >
48
- <Container className="py-48">
49
- <ErrorBoundary>
50
- <Outlet />
51
- </ErrorBoundary>
52
- </Container>
53
- </Grid>
54
- }
55
- />
22
+ <>
23
+ <Page
24
+ nav={
25
+ <Nav
26
+ paths={transformedPaths}
27
+ className="gradient-blur-cover select-none"
28
+ loading={isLoading}
29
+ customLogo={<Slot name="logo" data={defaultLogo} placeholder={defaultLogo} />}
30
+ style={{
31
+ paddingTop: 'calc(var(--safe-top) + 4px)',
32
+ transition: 'all .2s ease-in-out',
33
+ }}
34
+ />
35
+ }
36
+ main={
37
+ <Grid
38
+ leftAside={
39
+ <div className="py-32 px-6">
40
+ <Slot name="leftCol" data={paths} />
41
+ </div>
42
+ }
43
+ rightAside={
44
+ <div className="py-32 px-6">
45
+ <Slot name="rightCol" data={paths} />
46
+ </div>
47
+ }
48
+ >
49
+ <Container className="py-48">
50
+ <ErrorBoundary>
51
+ <Outlet />
52
+ </ErrorBoundary>
53
+ </Container>
54
+ </Grid>
55
+ }
56
+ />
57
+ <Slot name="pageFooter" />
58
+ </>
56
59
  );
57
60
  };
@@ -2,14 +2,11 @@ import React, { ReactNode } from 'react';
2
2
 
3
3
  import { GlobalLoadingStateProvider } from '@/context/global_loading_state_provider';
4
4
  import { GlobalRoutesProvider } from '@/context/global_routes_provider';
5
- import { EffectContextProvider } from '@/components/effect-layer/EffectContextProvider';
6
5
 
7
6
  export const BBContext = (props: { children: ReactNode }) => {
8
7
  return (
9
8
  <GlobalLoadingStateProvider>
10
- <GlobalRoutesProvider>
11
- <EffectContextProvider>{props.children}</EffectContextProvider>
12
- </GlobalRoutesProvider>
9
+ <GlobalRoutesProvider>{props.children}</GlobalRoutesProvider>
13
10
  </GlobalLoadingStateProvider>
14
11
  );
15
12
  };
@@ -39,6 +39,11 @@ class PluginManager {
39
39
  registry.registerMiddleware(point, fn, pluginId, weight);
40
40
  },
41
41
  getDeviceId: getStableDeviceId,
42
+ getVersionHash: () => {
43
+ const hashStr: string =
44
+ typeof GLOBAL_COMMIT_HASH === 'string' ? GLOBAL_COMMIT_HASH : '0000000';
45
+ return hashStr;
46
+ },
42
47
  registerSlot: (
43
48
  slotName: SlotName,
44
49
  component: React.ComponentType<IComPropsRegisteredToSlot>,
@@ -0,0 +1,30 @@
1
+ import React, { useMemo } from 'react';
2
+ import { EffectLayer, grain, paper, /*spiral,*/ watermark, Effect } from '@bbki.ng/ui';
3
+
4
+ import { IComPropsRegisteredToSlot } from '#/types/slots';
5
+
6
+ import { FxContext } from '../context';
7
+
8
+ export const FxCom = (_: IComPropsRegisteredToSlot) => {
9
+ const ctx = FxContext.useCtx();
10
+ const hashStr = ctx.versionHash;
11
+ const deviceId = ctx.deviceId;
12
+
13
+ // const { isLoading } = useContext(GlobalLoadingContext);
14
+ // const { deviceId } = useFingerprint();
15
+
16
+ const effects: Effect[] = useMemo(() => {
17
+ const wmLines = [hashStr];
18
+ if (deviceId) wmLines.push(deviceId);
19
+ wmLines.push('hello world');
20
+
21
+ return [
22
+ grain(),
23
+ paper(),
24
+ // spiral({ active: isLoading }),
25
+ watermark({ lines: wmLines }),
26
+ ];
27
+ }, [/*isLoading,*/ deviceId, hashStr]);
28
+
29
+ return <EffectLayer effects={effects} />;
30
+ };
@@ -0,0 +1,8 @@
1
+ import { createPluginCtx } from '#/core/context';
2
+
3
+ export interface IFxContext {
4
+ versionHash: string;
5
+ deviceId: string;
6
+ }
7
+
8
+ export const FxContext = createPluginCtx<IFxContext>();
@@ -0,0 +1,26 @@
1
+ import { BBPlugin } from '#/core/bbplugin';
2
+ import { IHostContext } from '#/types/hostApi';
3
+ import { PluginID } from '#/types/plugin';
4
+
5
+ import { FxCom } from './components';
6
+ import { FxContext } from './context';
7
+
8
+ class FxPlugin extends BBPlugin {
9
+ id: PluginID = 'fx';
10
+
11
+ override onInstall = async (ctx: IHostContext) => {
12
+ ctx.api.registerSlot(
13
+ 'pageFooter',
14
+ FxContext.withCtx(
15
+ {
16
+ versionHash: await ctx.api.getVersionHash(),
17
+ deviceId: (await ctx.api.getDeviceId()).id,
18
+ },
19
+ FxCom
20
+ ),
21
+ this.id
22
+ );
23
+ };
24
+ }
25
+
26
+ export default new FxPlugin();
@@ -27,6 +27,13 @@ export const PLUGIN_MANIFEST: Array<IPluginManifestEntry> = [
27
27
  description:
28
28
  '提供额外的快速切换页面的捷径,例如在标题列表末尾添加一个 "cd ~" 的选项,点击后跳转到主页',
29
29
  },
30
+ {
31
+ name: '特效',
32
+ id: 'fx',
33
+ version: '0.1.0',
34
+ description:
35
+ '提供一些额外的视觉效果。此外,页面左下角会显示当前版本的哈希值和设备标识哈希水印。',
36
+ },
30
37
  // {
31
38
  // name: 'extra-entry',
32
39
  // id: 'extra-entry',
@@ -1,4 +1,4 @@
1
- import React, { useCallback, useEffect } from 'react';
1
+ import React, { useCallback, useEffect, useState } from 'react';
2
2
  import { Button, Table } from '@bbki.ng/ui';
3
3
 
4
4
  import { IComPropsRegisteredToSlot } from '#/types/slots';
@@ -6,45 +6,160 @@ import { IPluginStoreEntry, PluginID } from '#/types/plugin';
6
6
 
7
7
  import { StoreCtx } from '../context';
8
8
 
9
+ // 空状态展示
10
+ const EmptyState = () => (
11
+ <div className="flex flex-col items-center justify-center p-8 text-content-secondary">
12
+ <p className="text-sm">暂无可用插件</p>
13
+ </div>
14
+ );
15
+
16
+ // 错误状态展示
17
+ const ErrorState = ({ error, onRetry }: { error: Error; onRetry: () => void }) => (
18
+ <div className="flex flex-col items-center justify-center p-8">
19
+ <p className="text-content-danger text-sm mb-4">加载失败: {error.message}</p>
20
+ <Button onClick={onRetry} size="sm">
21
+ 重试
22
+ </Button>
23
+ </div>
24
+ );
25
+
9
26
  export const StorePage = (_: IComPropsRegisteredToSlot) => {
10
27
  const { list, setLoading, isInstalled, install, uninstall } = StoreCtx.useCtx();
11
- const [plugins, setPlugins] = React.useState<Array<IPluginStoreEntry>>([]);
28
+ const [plugins, setPlugins] = useState<Array<IPluginStoreEntry>>([]);
29
+ const [error, setError] = useState<Error | null>(null);
12
30
 
13
31
  useEffect(() => {
32
+ let cancelled = false;
33
+
14
34
  const fetchPlugins = async () => {
15
35
  setLoading(true);
16
- const pluginList = await list();
17
- setPlugins(pluginList);
18
- setLoading(false);
36
+ setError(null);
37
+
38
+ try {
39
+ const pluginList = await list();
40
+ if (!cancelled) {
41
+ setPlugins(pluginList);
42
+ }
43
+ } catch (err) {
44
+ if (!cancelled) {
45
+ setError(err instanceof Error ? err : new Error('获取插件列表失败'));
46
+ }
47
+ } finally {
48
+ if (!cancelled) {
49
+ setLoading(false);
50
+ }
51
+ }
19
52
  };
20
53
 
21
54
  fetchPlugins();
55
+
56
+ return () => {
57
+ cancelled = true;
58
+ };
22
59
  }, [list, setLoading]);
23
60
 
61
+ const refreshList = useCallback(async () => {
62
+ try {
63
+ const pluginList = await list();
64
+ setPlugins(pluginList);
65
+ setError(null);
66
+ } catch (err) {
67
+ setError(err instanceof Error ? err : new Error('刷新列表失败'));
68
+ }
69
+ }, [list]);
70
+
24
71
  const handleInstall = useCallback(
25
72
  async (id: PluginID) => {
26
73
  setLoading(true);
27
- await install(id);
28
- const pluginList = await list();
29
- setPlugins(pluginList);
30
- setLoading(false);
74
+ setError(null);
75
+
76
+ try {
77
+ await install(id);
78
+ await refreshList();
79
+ } catch (err) {
80
+ setError(err instanceof Error ? err : new Error('安装失败'));
81
+ } finally {
82
+ setLoading(false);
83
+ }
31
84
  },
32
- [install, list, setLoading]
85
+ [install, refreshList, setLoading]
33
86
  );
34
87
 
35
88
  const handleUninstall = useCallback(
36
89
  async (id: PluginID) => {
37
90
  setLoading(true);
38
- await uninstall(id);
39
- const pluginList = await list();
40
- setPlugins(pluginList);
41
- setLoading(false);
91
+ setError(null);
92
+
93
+ try {
94
+ await uninstall(id);
95
+ await refreshList();
96
+ } catch (err) {
97
+ setError(err instanceof Error ? err : new Error('卸载失败'));
98
+ } finally {
99
+ setLoading(false);
100
+ }
101
+ },
102
+ [uninstall, refreshList, setLoading]
103
+ );
104
+
105
+ const headerRenderer = useCallback(() => {
106
+ return (
107
+ <>
108
+ <Table.HCell>插件名称</Table.HCell>
109
+ <Table.HCell>插件描述</Table.HCell>
110
+ <Table.HCell style={{ textAlign: 'right' }}>安装/卸载</Table.HCell>
111
+ </>
112
+ );
113
+ }, []);
114
+
115
+ const rowRenderer = useCallback(
116
+ (index: number) => {
117
+ if (index < 0 || index >= plugins.length) {
118
+ return null;
119
+ }
120
+
121
+ const plugin = plugins[index];
122
+ const pluginInstalled = isInstalled(plugin.id);
123
+
124
+ return (
125
+ <>
126
+ <Table.Cell>{plugin.name}</Table.Cell>
127
+ <Table.Cell>{plugin.description}</Table.Cell>
128
+ <Table.Cell style={{ textAlign: 'right' }}>
129
+ {pluginInstalled ? (
130
+ <Button
131
+ variant="ghost"
132
+ className="text-content-danger"
133
+ onClick={() => handleUninstall(plugin.id)}
134
+ >
135
+ 卸载
136
+ </Button>
137
+ ) : (
138
+ <Button color="primary" onClick={() => handleInstall(plugin.id)}>
139
+ 安装
140
+ </Button>
141
+ )}
142
+ </Table.Cell>
143
+ </>
144
+ );
42
145
  },
43
- [uninstall, list, setLoading]
146
+ [plugins, isInstalled, handleInstall, handleUninstall]
44
147
  );
45
148
 
149
+ if (error) {
150
+ return (
151
+ <div className="prose">
152
+ <ErrorState error={error} onRetry={refreshList} />
153
+ </div>
154
+ );
155
+ }
156
+
46
157
  if (plugins.length === 0) {
47
- return null;
158
+ return (
159
+ <div className="prose">
160
+ <EmptyState />
161
+ </div>
162
+ );
48
163
  }
49
164
 
50
165
  return (
@@ -52,50 +167,8 @@ export const StorePage = (_: IComPropsRegisteredToSlot) => {
52
167
  <Table
53
168
  className="w-full"
54
169
  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
- }}
170
+ headerRenderer={headerRenderer}
171
+ rowRenderer={rowRenderer}
99
172
  />
100
173
  </div>
101
174
  );
@@ -8,6 +8,7 @@ import { PluginID } from './plugin';
8
8
 
9
9
  export interface IHostApi {
10
10
  getDeviceId: () => Promise<{ id: string; fp: FingerprintData }>;
11
+ getVersionHash: () => Promise<string> | string;
11
12
  setLoading: (id: PluginID, loading: boolean) => void;
12
13
  registerMiddleware: <T>(
13
14
  point: HookPoint,
@@ -13,7 +13,15 @@ export interface IPlugin {
13
13
  onDestroy?: () => void;
14
14
  }
15
15
 
16
- export type PluginID = 'sticker' | 'xwy' | 'extra-cd' | 'extra-entry' | 'store' | 'now' | 'default';
16
+ export type PluginID =
17
+ | 'sticker'
18
+ | 'xwy'
19
+ | 'extra-cd'
20
+ | 'extra-entry'
21
+ | 'store'
22
+ | 'now'
23
+ | 'default'
24
+ | 'fx';
17
25
 
18
26
  export interface IPluginEntry {
19
27
  path: string;
@@ -4,6 +4,7 @@ export type SlotName =
4
4
  | 'articleActionRow'
5
5
  | 'logo'
6
6
  | 'route'
7
+ | 'pageFooter'
7
8
  | 'articleTitle';
8
9
 
9
10
  export type HookPoint =
@@ -1,27 +0,0 @@
1
- import React, { ReactNode, useContext, useMemo } from 'react';
2
- import { EffectLayer, grain, paper, spiral, watermark, Effect } from '@bbki.ng/ui';
3
-
4
- import { GlobalLoadingContext } from '@/context/global_loading_state_provider';
5
- import { useFingerprint } from '@/hooks/use_fingerprint';
6
-
7
- const hashStr: string = typeof GLOBAL_COMMIT_HASH === 'string' ? GLOBAL_COMMIT_HASH : '0000000';
8
-
9
- export const EffectContextProvider = (props: { children: ReactNode }) => {
10
- const { isLoading } = useContext(GlobalLoadingContext);
11
- const { deviceId } = useFingerprint();
12
-
13
- const effects: Effect[] = useMemo(() => {
14
- const wmLines = [hashStr];
15
- if (deviceId) wmLines.push(deviceId);
16
- wmLines.push('hello world');
17
-
18
- return [grain(), paper(), spiral({ active: isLoading }), watermark({ lines: wmLines })];
19
- }, [isLoading, deviceId]);
20
-
21
- return (
22
- <>
23
- <EffectLayer effects={effects} />
24
- {props.children}
25
- </>
26
- );
27
- };
@@ -1,55 +0,0 @@
1
- import { useEffect, useState, useCallback } from 'react';
2
- import { getStableDeviceId, FingerprintData } from '@/utils/fingerprints';
3
-
4
- interface UseFingerprintReturn {
5
- deviceId: string | null;
6
- fingerprint: FingerprintData | null;
7
- loading: boolean;
8
- error: Error | null;
9
- refresh: () => Promise<void>;
10
- }
11
-
12
- export function useFingerprint(): UseFingerprintReturn {
13
- const [state, setState] = useState<{
14
- deviceId: string | null;
15
- fingerprint: FingerprintData | null;
16
- loading: boolean;
17
- error: Error | null;
18
- }>({
19
- deviceId: null,
20
- fingerprint: null,
21
- loading: true,
22
- error: null,
23
- });
24
-
25
- const refresh = useCallback(async () => {
26
- setState(prev => ({ ...prev, loading: true, error: null }));
27
- try {
28
- const { id, fp } = await getStableDeviceId();
29
- setState({
30
- deviceId: id,
31
- fingerprint: fp,
32
- loading: false,
33
- error: null,
34
- });
35
- } catch (err) {
36
- setState(prev => ({
37
- ...prev,
38
- loading: false,
39
- error: err instanceof Error ? err : new Error('Failed to get fingerprint'),
40
- }));
41
- }
42
- }, []);
43
-
44
- useEffect(() => {
45
- refresh();
46
- }, [refresh]);
47
-
48
- return {
49
- deviceId: state.deviceId,
50
- fingerprint: state.fingerprint,
51
- loading: state.loading,
52
- error: state.error,
53
- refresh,
54
- };
55
- }