@bbki.ng/site 5.5.15 → 5.5.17

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.5.17
4
+
5
+ ### Patch Changes
6
+
7
+ - ae8b5c4: add cd plugin
8
+
9
+ ## 5.5.16
10
+
11
+ ### Patch Changes
12
+
13
+ - 22df0c4: fix global loading
14
+
3
15
  ## 5.5.15
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.5.15",
3
+ "version": "5.5.17",
4
4
  "description": "code behind bbki.ng",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -57,7 +57,7 @@
57
57
  "vite-plugin-mdx": "^3.5.8",
58
58
  "vite-plugin-pwa": "0.19",
59
59
  "workbox-window": "^6.3.0",
60
- "@bbki.ng/config": "1.0.6"
60
+ "@bbki.ng/config": "1.0.7"
61
61
  },
62
62
  "author": "bbbottle",
63
63
  "license": "MIT",
package/src/blog/app.tsx CHANGED
@@ -1,19 +1,19 @@
1
- import React, { useContext, useEffect } from 'react';
1
+ import React, { useContext } from 'react';
2
2
  import { Outlet, Route, Routes } from 'react-router-dom';
3
- import { Cover, Streaming } from './pages';
4
3
  import { Nav, NotFound, Page, Grid, ErrorBoundary, Container } from '@bbki.ng/ui';
5
4
 
5
+ import { usePaths } from '@/hooks';
6
6
  import ArticlePage from '@/pages/extensions/txt/article';
7
7
  import Txt from '@/pages/extensions/txt';
8
-
9
- import { usePaths } from '@/hooks';
10
- import { SWR } from '@/swr';
11
8
  import { GlobalLoadingContext } from '@/context/global_loading_state_provider';
12
9
  import { BotRedirect } from '@/pages/bot';
13
10
  import { BBContext } from '@/context/bbcontext';
11
+ import { Slot } from '#/core/components/SlotComp';
12
+ import { usePlugins } from '#/core/hooks/use_plugins';
13
+ import { SWR } from '@/swr';
14
+
15
+ import { Cover, Streaming } from './pages';
14
16
  import { useDynamicLogo } from './hooks/use_dynamic_logo';
15
- import { Slot } from '../core/components/SlotComp';
16
- import { usePlugins } from './hooks/use_plugins';
17
17
 
18
18
  const Layout = () => {
19
19
  const paths = usePaths();
@@ -59,7 +59,7 @@ const Layout = () => {
59
59
  };
60
60
 
61
61
  export const App = () => {
62
- usePlugins(['sticker', 'fontstyler']);
62
+ usePlugins(['sticker', 'fontstyler', 'extra-cd']);
63
63
 
64
64
  return (
65
65
  <SWR>
@@ -1,20 +1,10 @@
1
- import React, { useContext, useEffect } from 'react';
2
- import { GlobalLoadingContext } from '@/context/global_loading_state_provider';
1
+ import React from 'react';
2
+ import { useGlobalLoading } from '@/hooks';
3
3
 
4
4
  export const Spinner = (props: { disableDotIndicator?: boolean }) => {
5
5
  const { disableDotIndicator } = props;
6
6
 
7
- const { setIsLoading } = useContext(GlobalLoadingContext);
8
-
9
- useEffect(() => {
10
- if (disableDotIndicator) {
11
- return;
12
- }
13
- setIsLoading(true);
14
- return () => {
15
- setIsLoading(false);
16
- };
17
- }, []);
7
+ useGlobalLoading('spinner', !disableDotIndicator);
18
8
 
19
9
  return <div className="h-full w-full grid place-items-center"></div>;
20
10
  };
@@ -1,5 +1,4 @@
1
1
  import React, { ReactNode, useContext, useMemo } from 'react';
2
-
3
2
  import { EffectLayer, grain, paper, spiral, watermark, Effect } from '@bbki.ng/ui';
4
3
 
5
4
  import { GlobalLoadingContext } from '@/context/global_loading_state_provider';
@@ -1,6 +1,7 @@
1
1
  import React, { ReactNode } from 'react';
2
+
2
3
  import { GlobalLoadingStateProvider } from '@/context/global_loading_state_provider';
3
- import { GlobalRoutesContext, GlobalRoutesProvider } from '@/context/global_routes_provider';
4
+ import { GlobalRoutesProvider } from '@/context/global_routes_provider';
4
5
  import { EffectContextProvider } from '@/components/effect-layer/EffectContextProvider';
5
6
 
6
7
  export const BBContext = (props: { children: ReactNode }) => {
@@ -1,29 +1,70 @@
1
- import React, { createContext, ReactNode, useState, Dispatch, SetStateAction } from 'react';
1
+ import React, { createContext, ReactNode, useState, useCallback, useMemo } from 'react';
2
+
2
3
  import { useFontLoading } from '@/hooks/use_font_loading';
3
4
 
5
+ type LoadingStates = Map<string, boolean>;
6
+
4
7
  type LoadingContext = {
5
8
  isLoading: boolean;
6
9
  isFontLoading: boolean;
7
- setIsLoading: Dispatch<SetStateAction<boolean>>;
10
+ register: (id: string) => void;
11
+ setLoading: (id: string, loading: boolean) => void;
12
+ unregister: (id: string) => void;
8
13
  };
9
14
 
10
15
  export const GlobalLoadingContext = createContext<LoadingContext>({
11
16
  isFontLoading: false,
12
17
  isLoading: false,
13
- setIsLoading: () => false,
18
+ register: () => {},
19
+ setLoading: () => {},
20
+ unregister: () => {},
14
21
  });
15
22
 
16
23
  export const GlobalLoadingStateProvider = (props: { children: ReactNode }) => {
17
- const [isLoading, setIsLoading] = useState(false);
24
+ const [loadingStates, setLoadingStates] = useState<LoadingStates>(new Map());
18
25
  const isFontLoading = useFontLoading();
26
+
27
+ const register = useCallback((id: string) => {
28
+ setLoadingStates(prev => {
29
+ const next = new Map(prev);
30
+ next.set(id, false);
31
+ return next;
32
+ });
33
+ }, []);
34
+
35
+ const setLoading = useCallback((id: string, loading: boolean) => {
36
+ setLoadingStates(prev => {
37
+ const next = new Map(prev);
38
+ next.set(id, loading);
39
+ return next;
40
+ });
41
+ }, []);
42
+
43
+ const unregister = useCallback((id: string) => {
44
+ setLoadingStates(prev => {
45
+ const next = new Map(prev);
46
+ next.delete(id);
47
+ return next;
48
+ });
49
+ }, []);
50
+
51
+ const isLoading = useMemo(() => {
52
+ return Array.from(loadingStates.values()).some(Boolean) || isFontLoading;
53
+ }, [loadingStates, isFontLoading]);
54
+
55
+ const contextValue = useMemo(
56
+ () => ({
57
+ isLoading,
58
+ isFontLoading,
59
+ register,
60
+ setLoading,
61
+ unregister,
62
+ }),
63
+ [isLoading, isFontLoading, register, setLoading, unregister]
64
+ );
65
+
19
66
  return (
20
- <GlobalLoadingContext.Provider
21
- value={{
22
- isLoading,
23
- setIsLoading,
24
- isFontLoading,
25
- }}
26
- >
67
+ <GlobalLoadingContext.Provider value={contextValue}>
27
68
  {props.children}
28
69
  </GlobalLoadingContext.Provider>
29
70
  );
@@ -1,3 +1,4 @@
1
1
  export { usePaths } from './use_paths';
2
2
  export { useStreaming } from './use_streaming';
3
3
  export type { StreamingItem } from './use_streaming';
4
+ export { useGlobalLoading } from './use_loading';
@@ -0,0 +1,26 @@
1
+ import { useContext, useEffect } from 'react';
2
+
3
+ import { GlobalLoadingContext } from '@/context/global_loading_state_provider';
4
+
5
+ /**
6
+ * Hook to register a loading state with the global loading context.
7
+ * The global loading state is true if ANY registered loading state is true.
8
+ *
9
+ * @param id - Unique identifier for this loading state
10
+ * @param loading - Whether this specific loading state is active
11
+ */
12
+ export function useGlobalLoading(id: string | undefined, loading: boolean | undefined) {
13
+ const { register, setLoading, unregister, isLoading } = useContext(GlobalLoadingContext);
14
+
15
+ useEffect(() => {
16
+ if (!id || loading === undefined) return;
17
+
18
+ register(id);
19
+ setLoading(id, loading);
20
+ return () => {
21
+ unregister(id);
22
+ };
23
+ }, [id, loading]);
24
+
25
+ return isLoading;
26
+ }
@@ -1,18 +1,14 @@
1
1
  import useSWR from 'swr';
2
- import { useContext, useEffect, useState } from 'react';
3
- import { GlobalLoadingContext } from '@/context/global_loading_state_provider';
2
+ import { useMemo } from 'react';
3
+
4
+ import { useGlobalLoading } from '@/hooks';
4
5
  import { baseFetcher } from '@/utils';
5
6
  import { API_ENDPOINT } from '@/constants/routes';
6
- import { registry } from '#/core/registry';
7
+ import { useMiddlewareTransData } from '#/core/hooks';
7
8
 
8
- // In dev, use /api prefix to leverage Vite proxy to localhost:8787
9
9
  const isProd = true;
10
- // const isProd = typeof window !== 'undefined' && /^https:\/\/bbki\.ng/.test(window.location.href);
11
10
  const POSTS_API = !isProd ? '/api/posts' : `${API_ENDPOINT}/posts`;
12
11
 
13
- // Use baseFetcher for full URLs, cfApiFetcher is for relative paths
14
- const postsFetcher = (resource: string) => baseFetcher(resource);
15
-
16
12
  export interface TitleListItem {
17
13
  name: string;
18
14
  to: string;
@@ -21,58 +17,46 @@ export interface TitleListItem {
21
17
  }
22
18
 
23
19
  export const usePosts = (name: string = '', suspense?: boolean) => {
24
- const { data: response, error } = useSWR(POSTS_API, postsFetcher, {
20
+ const { data: response, error: swrError } = useSWR(POSTS_API, baseFetcher, {
25
21
  revalidateOnFocus: false,
26
22
  suspense,
27
23
  });
28
24
 
29
- // Extract posts array from API response { status: "success", data: [...] }
30
25
  const data = response?.data;
26
+ const isDataLoading = !data && !swrError;
27
+
28
+ const baseTitleList: TitleListItem[] = useMemo(() => {
29
+ if (!data || swrError) return [];
30
+
31
+ return data.map((p: any) => ({
32
+ name: p.title,
33
+ to: p.title,
34
+ children: p.title,
35
+ }));
36
+ }, [data, swrError]);
37
+
38
+ // Use middleware hook to transform title list
39
+ const {
40
+ data: titleList,
41
+ loading: isTransforming,
42
+ error: transformError,
43
+ } = useMiddlewareTransData({
44
+ hookPoint: 'transformTitleList',
45
+ baseData: baseTitleList,
46
+ immediate: baseTitleList.length > 0,
47
+ });
31
48
 
32
- const [titleList, setTitleList] = useState<TitleListItem[]>([]);
33
- let isLoading = !data && !error;
34
- const { setIsLoading } = useContext(GlobalLoadingContext);
35
-
36
- useEffect(() => {
37
- setIsLoading(isLoading);
38
- }, [isLoading, setIsLoading]);
39
-
40
- // Apply middleware to transform titleList
41
- useEffect(() => {
42
- const transformTitleList = async () => {
43
- if (isLoading || error || !data) {
44
- setTitleList([]);
45
- return;
46
- }
47
-
48
- const baseTitleList: TitleListItem[] = [
49
- ...data.map((p: any) => ({
50
- name: p.title,
51
- to: p.title,
52
- children: p.title,
53
- })),
54
- {
55
- name: 'cd ~',
56
- to: '/',
57
- children: 'cd ~',
58
- },
59
- ];
60
-
61
- // Run middleware to allow plugins to transform the title list
62
- const transformedList = await registry.runMiddleware('transformTitleList', baseTitleList);
63
- setTitleList(transformedList);
64
- };
65
-
66
- transformTitleList();
67
- }, [data, isLoading, error]);
49
+ const gLoading = useGlobalLoading('posts', isDataLoading || isTransforming);
68
50
 
69
51
  const posts =
70
- isLoading || name == '' || error || !data ? data : data.find((p: any) => p.title == name);
52
+ isDataLoading || name === '' || swrError || !data
53
+ ? data
54
+ : data.find((p: any) => p.title === name);
71
55
 
72
56
  return {
73
- posts: posts,
74
- titleList,
75
- isError: error,
76
- isLoading: !data && !error,
57
+ posts,
58
+ titleList: titleList ?? [],
59
+ isError: swrError || transformError,
60
+ isLoading: gLoading,
77
61
  };
78
62
  };
@@ -1,9 +1,10 @@
1
1
  import useSWR from 'swr';
2
- import useSWRInfinite from 'swr/infinite';
3
- import { baseFetcher, withBBApi } from '@/utils';
2
+ import { useEffect, useState } from 'react';
3
+
4
+ import { baseFetcher } from '@/utils';
4
5
  import { API_ENDPOINT } from '@/constants/routes';
5
- import { useContext, useEffect, useState, useCallback } from 'react';
6
- import { GlobalLoadingContext } from '@/context/global_loading_state_provider';
6
+
7
+ import { useGlobalLoading } from './use_loading';
7
8
 
8
9
  // In dev, use /api prefix to leverage Vite proxy to localhost:8787
9
10
  // const isProd = typeof window !== 'undefined' && /^https:\/\/bbki.ng/.test(window.location.href);
@@ -101,11 +102,7 @@ export function useStreaming(options: UseStreamingOptions = {}) {
101
102
  });
102
103
  }, []);
103
104
 
104
- const { setIsLoading } = useContext(GlobalLoadingContext);
105
-
106
- useEffect(() => {
107
- setIsLoading(isLoading);
108
- }, [isLoading, setIsLoading]);
105
+ useGlobalLoading('streaming', isLoading);
109
106
 
110
107
  return {
111
108
  streaming: data?.data ?? [],
@@ -4,7 +4,7 @@ import { useSlotComp } from '../hooks/useSlotComp';
4
4
 
5
5
  export interface ISlotProps {
6
6
  name: SlotName;
7
- data?: any;
7
+ data?: unknown;
8
8
  }
9
9
 
10
10
  export const Slot: React.FC<ISlotProps> = ({ name, data }) => {
@@ -0,0 +1,6 @@
1
+ export { useSlotComp } from './useSlotComp';
2
+ export { useMiddlewareTransData } from './useMiddlewareTransData';
3
+ export type {
4
+ UseMiddlewareTransDataOptions,
5
+ UseMiddlewareTransDataResult,
6
+ } from './useMiddlewareTransData';
@@ -0,0 +1,68 @@
1
+ import { useState, useEffect, useCallback, useRef } from 'react';
2
+
3
+ import { registry } from '#/core/registry';
4
+ import type { HookPoint } from '#/types/slots';
5
+
6
+ export interface UseMiddlewareTransDataOptions<T> {
7
+ hookPoint: HookPoint;
8
+ baseData: T;
9
+ immediate?: boolean;
10
+ }
11
+
12
+ export interface UseMiddlewareTransDataResult<T> {
13
+ data: T | undefined;
14
+ loading: boolean;
15
+ error: Error | null;
16
+ run: () => Promise<T>;
17
+ }
18
+
19
+ export function useMiddlewareTransData<T>({
20
+ hookPoint,
21
+ baseData,
22
+ immediate = true,
23
+ }: UseMiddlewareTransDataOptions<T>): UseMiddlewareTransDataResult<T> {
24
+ const [data, setData] = useState<T | undefined>(undefined);
25
+ const [loading, setLoading] = useState(false);
26
+ const [error, setError] = useState<Error | null>(null);
27
+
28
+ const immediateRef = useRef(immediate);
29
+ immediateRef.current = immediate;
30
+
31
+ const run = useCallback(async (): Promise<T> => {
32
+ setLoading(true);
33
+ setError(null);
34
+
35
+ try {
36
+ const result = await registry.runMiddleware(hookPoint, baseData);
37
+ setData(result);
38
+ return result;
39
+ } catch (err) {
40
+ const error = err instanceof Error ? err : new Error(String(err));
41
+ setError(error);
42
+ throw error;
43
+ } finally {
44
+ setLoading(false);
45
+ }
46
+ }, [hookPoint, baseData]);
47
+
48
+ useEffect(() => {
49
+ if (!immediateRef.current) return;
50
+
51
+ let cancelled = false;
52
+
53
+ run()
54
+ .then(result => {
55
+ if (cancelled) return;
56
+ setData(result);
57
+ })
58
+ .catch(() => {
59
+ // 错误已在 run 中设置到 state
60
+ });
61
+
62
+ return () => {
63
+ cancelled = true;
64
+ };
65
+ }, [hookPoint, baseData, run]);
66
+
67
+ return { data, loading, error, run };
68
+ }
@@ -1,5 +1,7 @@
1
- import { SlotName } from 'src/types/slots';
2
- import { useEffect, useState } from 'react';
1
+ import React, { useEffect, useState } from 'react';
2
+
3
+ import { SlotName } from '#/types/slots';
4
+
3
5
  import { registry } from '../registry';
4
6
 
5
7
  export const useSlotComp = (slotName: SlotName) => {
@@ -1,5 +1,7 @@
1
1
  import { useEffect } from 'react';
2
+
2
3
  import { pluginManager } from '#/core/pluginManager';
4
+ import { PluginID } from '#/types/plugin';
3
5
 
4
6
  /**
5
7
  * 通用插件管理 hook
@@ -10,7 +12,7 @@ import { pluginManager } from '#/core/pluginManager';
10
12
  * @example
11
13
  * usePlugins(['sticker', 'fontstyler']);
12
14
  */
13
- export const usePlugins = (pluginIds: string[]) => {
15
+ export const usePlugins = (pluginIds: Array<PluginID>) => {
14
16
  useEffect(() => {
15
17
  // 加载指定的插件
16
18
  pluginIds.forEach(id => {
@@ -1,3 +1,5 @@
1
+ import React from 'react';
2
+
1
3
  import { IHostContext } from '#/types/hostApi';
2
4
  import { registry } from './registry';
3
5
  import type { SlotName, HookPoint } from '#/types/slots';
@@ -0,0 +1,38 @@
1
+ import { IHostContext } from '#/types/hostApi';
2
+ import { IPlugin } from '#/types/plugin';
3
+ import { TitleListItem } from '#/types/posts';
4
+
5
+ class ExtraCd implements IPlugin {
6
+ id: string = 'extra-cd';
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';
12
+
13
+ async onInstall(ctx: IHostContext): Promise<void> {
14
+ // Initialize any required resources or settings
15
+ 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
+ }
25
+
26
+ private transformTitleList = (titleList: Array<TitleListItem>) => {
27
+ return [
28
+ ...titleList,
29
+ {
30
+ name: 'cd ~',
31
+ to: '/',
32
+ children: 'cd ~',
33
+ },
34
+ ];
35
+ };
36
+ }
37
+
38
+ export default new ExtraCd();
@@ -11,6 +11,13 @@ export const PLUGIN_MANIFEST = [
11
11
  version: '0.1.0',
12
12
  description: 'A sticker plugin',
13
13
  },
14
+ {
15
+ name: 'extra-cd',
16
+ id: 'extra-cd',
17
+ version: '0.1.0',
18
+ description:
19
+ 'Provides additional change directory functionalities for enhanced user experience.',
20
+ },
14
21
  {
15
22
  name: 'fontstyler',
16
23
  id: 'fontstyler',
@@ -1,5 +1,6 @@
1
1
  import { IHostContext } from '#/types/hostApi';
2
2
  import { IPlugin } from '#/types/plugin';
3
+
3
4
  import { Emoji } from './components/emoji';
4
5
 
5
6
  class TestPlugin implements IPlugin {
@@ -1,12 +1,20 @@
1
- import { HookPoint, SlotName } from './slots';
1
+ import React from 'react';
2
+
2
3
  import { type FingerprintData } from '@/utils/fingerprints';
3
4
 
5
+ import { HookPoint, SlotName } from './slots';
6
+
4
7
  export interface IHostApi {
5
8
  getDeviceId: () => Promise<{ id: string; fp: FingerprintData }>;
6
- registerMiddleware: (point: HookPoint, fn: Function, pluginId: string, weight?: number) => void;
7
- registerSlot: (
9
+ registerMiddleware: <T>(
10
+ point: HookPoint,
11
+ fn: (baseData: T) => T,
12
+ pluginId: string,
13
+ weight?: number
14
+ ) => void;
15
+ registerSlot: <T>(
8
16
  slotName: SlotName,
9
- component: React.ComponentType<any>,
17
+ component: React.ComponentType<T>,
10
18
  pluginId: string,
11
19
  weight?: number
12
20
  ) => void;
@@ -11,3 +11,5 @@ export interface IPlugin {
11
11
  onDisable?: () => Promise<void> | void;
12
12
  onDestroy?: () => void;
13
13
  }
14
+
15
+ export type PluginID = 'sticker' | 'fontstyler' | 'extra-cd';
@@ -0,0 +1,6 @@
1
+ export interface TitleListItem {
2
+ name: string;
3
+ to: string;
4
+ children: string;
5
+ className?: string;
6
+ }
package/tsconfig.json CHANGED
@@ -5,6 +5,7 @@
5
5
  "jsx": "react",
6
6
  "noEmit": true,
7
7
  "moduleResolution": "bundler",
8
+ "ignoreDeprecations": "6.0",
8
9
  "baseUrl": ".",
9
10
  "paths": {
10
11
  "@/*": ["src/blog/*"],