@bbki.ng/site 5.5.16 → 5.5.18
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 +14 -0
- package/index.d.ts +14 -1
- package/package.json +3 -3
- package/src/blog/app.tsx +15 -11
- package/src/blog/components/Spinner.tsx +2 -2
- package/src/blog/components/effect-layer/EffectContextProvider.tsx +0 -1
- package/src/blog/context/bbcontext.tsx +2 -1
- package/src/blog/context/global_loading_state_provider.tsx +3 -2
- package/src/blog/hooks/index.ts +1 -1
- package/src/blog/hooks/use_loading.ts +8 -11
- package/src/blog/hooks/use_posts.ts +10 -19
- package/src/blog/hooks/use_streaming.ts +6 -5
- package/src/core/components/SlotComp.tsx +10 -2
- package/src/core/hooks/useSlotComp.ts +4 -2
- package/src/core/hooks/use_plugins.ts +2 -1
- package/src/plugins/extra-cd/index.ts +38 -0
- package/src/plugins/manifest.ts +10 -9
- package/src/plugins/sticker/components/StickerCom.tsx +0 -11
- package/src/plugins/sticker/index.ts +1 -0
- package/src/{blog/components/Pochacco/xwy.tsx → plugins/xwy/components/logo.tsx} +12 -1
- package/src/plugins/xwy/const/index.ts +15 -0
- package/src/plugins/xwy/index.ts +73 -0
- package/src/plugins/xwy/types/index.ts +15 -0
- package/src/plugins/xwy/utils/index.ts +22 -0
- package/src/types/hostApi.ts +2 -1
- package/src/types/plugin.ts +2 -0
- package/src/types/posts.ts +7 -0
- package/src/types/slots.ts +1 -1
- package/tsconfig.json +1 -0
- package/src/blog/hooks/use_dynamic_logo.tsx +0 -15
- package/src/plugins/fontstyler/index.ts +0 -132
- package/src/plugins/test/components/emoji.tsx +0 -8
- package/src/plugins/test/index.ts +0 -22
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# @bbki.ng/site
|
|
2
2
|
|
|
3
|
+
## 5.5.18
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 43fb1e3: add xwy plugin
|
|
8
|
+
- Updated dependencies [43fb1e3]
|
|
9
|
+
- @bbki.ng/ui@0.2.12
|
|
10
|
+
|
|
11
|
+
## 5.5.17
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- ae8b5c4: add cd plugin
|
|
16
|
+
|
|
3
17
|
## 5.5.16
|
|
4
18
|
|
|
5
19
|
### Patch Changes
|
package/index.d.ts
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
1
3
|
declare const GLOBAL_BBKING_VERSION: string;
|
|
2
4
|
declare const GLOBAL_COMMIT_HASH: string;
|
|
3
5
|
interface ImportMeta {
|
|
4
|
-
glob: (pattern: string) => Record<string, () => Promise<
|
|
6
|
+
glob: (pattern: string) => Record<string, () => Promise<unknown>>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
declare global {
|
|
10
|
+
namespace JSX {
|
|
11
|
+
interface IntrinsicElements {
|
|
12
|
+
'bb-img': React.DetailedHTMLProps<
|
|
13
|
+
React.ImgHTMLAttributes<HTMLImageElement>,
|
|
14
|
+
HTMLImageElement
|
|
15
|
+
>;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
5
18
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bbki.ng/site",
|
|
3
|
-
"version": "5.5.
|
|
3
|
+
"version": "5.5.18",
|
|
4
4
|
"description": "code behind bbki.ng",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"react-dom": "^18.0.0",
|
|
15
15
|
"react-router-dom": "6",
|
|
16
16
|
"swr": "^2.2.5",
|
|
17
|
-
"@bbki.ng/ui": "0.2.
|
|
17
|
+
"@bbki.ng/ui": "0.2.12"
|
|
18
18
|
},
|
|
19
19
|
"devDependencies": {
|
|
20
20
|
"@eslint/compat": "^1.0.0",
|
|
@@ -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.
|
|
60
|
+
"@bbki.ng/config": "1.0.8"
|
|
61
61
|
},
|
|
62
62
|
"author": "bbbottle",
|
|
63
63
|
"license": "MIT",
|
package/src/blog/app.tsx
CHANGED
|
@@ -1,24 +1,28 @@
|
|
|
1
|
-
import React, { useContext
|
|
2
|
-
import { Outlet, Route, Routes } from 'react-router-dom';
|
|
3
|
-
import {
|
|
4
|
-
import { Nav, NotFound, Page, Grid, ErrorBoundary, Container } from '@bbki.ng/ui';
|
|
1
|
+
import React, { useContext } from 'react';
|
|
2
|
+
import { Outlet, Route, Routes, useNavigate } from 'react-router-dom';
|
|
3
|
+
import { Logo, 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';
|
|
14
11
|
import { Slot } from '#/core/components/SlotComp';
|
|
15
12
|
import { usePlugins } from '#/core/hooks/use_plugins';
|
|
16
|
-
import {
|
|
13
|
+
import { SWR } from '@/swr';
|
|
14
|
+
|
|
15
|
+
import { Cover, Streaming } from './pages';
|
|
17
16
|
|
|
18
17
|
const Layout = () => {
|
|
19
18
|
const paths = usePaths();
|
|
20
19
|
const { isLoading } = useContext(GlobalLoadingContext);
|
|
21
|
-
|
|
20
|
+
|
|
21
|
+
const nav = useNavigate();
|
|
22
|
+
|
|
23
|
+
const defaultLogo = (
|
|
24
|
+
<Logo className="mr-2 cursor-pointer hover:opacity-80" onClick={() => nav('/')} />
|
|
25
|
+
);
|
|
22
26
|
|
|
23
27
|
return (
|
|
24
28
|
<Page
|
|
@@ -27,7 +31,7 @@ const Layout = () => {
|
|
|
27
31
|
paths={paths}
|
|
28
32
|
className="gradient-blur-cover select-none"
|
|
29
33
|
loading={isLoading}
|
|
30
|
-
customLogo={logo}
|
|
34
|
+
customLogo={<Slot name="logo" data={defaultLogo} placeholder={defaultLogo} />}
|
|
31
35
|
style={{
|
|
32
36
|
paddingTop: 'var(--safe-top)',
|
|
33
37
|
transition: 'all .2s ease-in-out',
|
|
@@ -59,7 +63,7 @@ const Layout = () => {
|
|
|
59
63
|
};
|
|
60
64
|
|
|
61
65
|
export const App = () => {
|
|
62
|
-
usePlugins(['sticker', '
|
|
66
|
+
usePlugins(['sticker', 'xwy', 'extra-cd']);
|
|
63
67
|
|
|
64
68
|
return (
|
|
65
69
|
<SWR>
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { useGlobalLoading } from '@/hooks';
|
|
3
3
|
|
|
4
4
|
export const Spinner = (props: { disableDotIndicator?: boolean }) => {
|
|
5
5
|
const { disableDotIndicator } = props;
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
useGlobalLoading('spinner', !disableDotIndicator);
|
|
8
8
|
|
|
9
9
|
return <div className="h-full w-full grid place-items-center"></div>;
|
|
10
10
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React, { ReactNode } from 'react';
|
|
2
|
+
|
|
2
3
|
import { GlobalLoadingStateProvider } from '@/context/global_loading_state_provider';
|
|
3
|
-
import {
|
|
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,4 +1,5 @@
|
|
|
1
1
|
import React, { createContext, ReactNode, useState, useCallback, useMemo } from 'react';
|
|
2
|
+
|
|
2
3
|
import { useFontLoading } from '@/hooks/use_font_loading';
|
|
3
4
|
|
|
4
5
|
type LoadingStates = Map<string, boolean>;
|
|
@@ -48,8 +49,8 @@ export const GlobalLoadingStateProvider = (props: { children: ReactNode }) => {
|
|
|
48
49
|
}, []);
|
|
49
50
|
|
|
50
51
|
const isLoading = useMemo(() => {
|
|
51
|
-
return Array.from(loadingStates.values()).some(Boolean);
|
|
52
|
-
}, [loadingStates]);
|
|
52
|
+
return Array.from(loadingStates.values()).some(Boolean) || isFontLoading;
|
|
53
|
+
}, [loadingStates, isFontLoading]);
|
|
53
54
|
|
|
54
55
|
const contextValue = useMemo(
|
|
55
56
|
() => ({
|
package/src/blog/hooks/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useContext, useEffect } from 'react';
|
|
2
|
+
|
|
2
3
|
import { GlobalLoadingContext } from '@/context/global_loading_state_provider';
|
|
3
4
|
|
|
4
5
|
/**
|
|
@@ -7,23 +8,19 @@ import { GlobalLoadingContext } from '@/context/global_loading_state_provider';
|
|
|
7
8
|
*
|
|
8
9
|
* @param id - Unique identifier for this loading state
|
|
9
10
|
* @param loading - Whether this specific loading state is active
|
|
10
|
-
*
|
|
11
|
-
* @example
|
|
12
|
-
* // In a data fetching hook
|
|
13
|
-
* useLoading('posts', isDataLoading);
|
|
14
|
-
*
|
|
15
|
-
* @example
|
|
16
|
-
* // In a component
|
|
17
|
-
* useLoading('spinner', true);
|
|
18
11
|
*/
|
|
19
|
-
export function
|
|
20
|
-
const { register, setLoading, unregister } = useContext(GlobalLoadingContext);
|
|
12
|
+
export function useGlobalLoading(id: string | undefined, loading: boolean | undefined) {
|
|
13
|
+
const { register, setLoading, unregister, isLoading } = useContext(GlobalLoadingContext);
|
|
21
14
|
|
|
22
15
|
useEffect(() => {
|
|
16
|
+
if (!id || loading === undefined) return;
|
|
17
|
+
|
|
23
18
|
register(id);
|
|
24
19
|
setLoading(id, loading);
|
|
25
20
|
return () => {
|
|
26
21
|
unregister(id);
|
|
27
22
|
};
|
|
28
|
-
}, [id, loading
|
|
23
|
+
}, [id, loading]);
|
|
24
|
+
|
|
25
|
+
return isLoading;
|
|
29
26
|
}
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import useSWR from 'swr';
|
|
2
2
|
import { useMemo } from 'react';
|
|
3
|
-
|
|
3
|
+
|
|
4
|
+
import { useGlobalLoading } from '@/hooks';
|
|
4
5
|
import { baseFetcher } from '@/utils';
|
|
5
6
|
import { API_ENDPOINT } from '@/constants/routes';
|
|
6
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
12
|
export interface TitleListItem {
|
|
@@ -23,25 +22,17 @@ export const usePosts = (name: string = '', suspense?: boolean) => {
|
|
|
23
22
|
suspense,
|
|
24
23
|
});
|
|
25
24
|
|
|
26
|
-
// Extract posts array from API response { status: "success", data: [...] }
|
|
27
25
|
const data = response?.data;
|
|
28
26
|
const isDataLoading = !data && !swrError;
|
|
29
27
|
|
|
30
|
-
// Build base title list from posts data
|
|
31
28
|
const baseTitleList: TitleListItem[] = useMemo(() => {
|
|
32
29
|
if (!data || swrError) return [];
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
{
|
|
40
|
-
name: 'cd ~',
|
|
41
|
-
to: '/',
|
|
42
|
-
children: 'cd ~',
|
|
43
|
-
},
|
|
44
|
-
];
|
|
30
|
+
|
|
31
|
+
return data.map((p: any) => ({
|
|
32
|
+
name: p.title,
|
|
33
|
+
to: p.title,
|
|
34
|
+
children: p.title,
|
|
35
|
+
}));
|
|
45
36
|
}, [data, swrError]);
|
|
46
37
|
|
|
47
38
|
// Use middleware hook to transform title list
|
|
@@ -55,7 +46,7 @@ export const usePosts = (name: string = '', suspense?: boolean) => {
|
|
|
55
46
|
immediate: baseTitleList.length > 0,
|
|
56
47
|
});
|
|
57
48
|
|
|
58
|
-
|
|
49
|
+
const gLoading = useGlobalLoading('posts', isDataLoading || isTransforming);
|
|
59
50
|
|
|
60
51
|
const posts =
|
|
61
52
|
isDataLoading || name === '' || swrError || !data
|
|
@@ -66,6 +57,6 @@ export const usePosts = (name: string = '', suspense?: boolean) => {
|
|
|
66
57
|
posts,
|
|
67
58
|
titleList: titleList ?? [],
|
|
68
59
|
isError: swrError || transformError,
|
|
69
|
-
isLoading:
|
|
60
|
+
isLoading: gLoading,
|
|
70
61
|
};
|
|
71
62
|
};
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import useSWR from 'swr';
|
|
2
|
-
import
|
|
3
|
-
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
|
|
4
|
+
import { baseFetcher } from '@/utils';
|
|
4
5
|
import { API_ENDPOINT } from '@/constants/routes';
|
|
5
|
-
|
|
6
|
-
import {
|
|
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,7 +102,7 @@ export function useStreaming(options: UseStreamingOptions = {}) {
|
|
|
101
102
|
});
|
|
102
103
|
}, []);
|
|
103
104
|
|
|
104
|
-
|
|
105
|
+
useGlobalLoading('streaming', isLoading);
|
|
105
106
|
|
|
106
107
|
return {
|
|
107
108
|
streaming: data?.data ?? [],
|
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
import { SlotName } from '#/types/slots';
|
|
4
|
+
|
|
3
5
|
import { useSlotComp } from '../hooks/useSlotComp';
|
|
4
6
|
|
|
5
7
|
export interface ISlotProps {
|
|
6
8
|
name: SlotName;
|
|
7
9
|
data?: unknown;
|
|
10
|
+
|
|
11
|
+
placeholder?: React.ReactNode;
|
|
8
12
|
}
|
|
9
13
|
|
|
10
|
-
export const Slot: React.FC<ISlotProps> = ({ name, data }) => {
|
|
14
|
+
export const Slot: React.FC<ISlotProps> = ({ name, data, placeholder }) => {
|
|
11
15
|
const components = useSlotComp(name);
|
|
12
16
|
|
|
17
|
+
if (components.length === 0) {
|
|
18
|
+
return <>{placeholder}</>;
|
|
19
|
+
}
|
|
20
|
+
|
|
13
21
|
return (
|
|
14
22
|
<>
|
|
15
23
|
{components.map((Component, index) => (
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
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,6 +1,7 @@
|
|
|
1
1
|
import { useEffect } from 'react';
|
|
2
2
|
|
|
3
3
|
import { pluginManager } from '#/core/pluginManager';
|
|
4
|
+
import { PluginID } from '#/types/plugin';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* 通用插件管理 hook
|
|
@@ -11,7 +12,7 @@ import { pluginManager } from '#/core/pluginManager';
|
|
|
11
12
|
* @example
|
|
12
13
|
* usePlugins(['sticker', 'fontstyler']);
|
|
13
14
|
*/
|
|
14
|
-
export const usePlugins = (pluginIds:
|
|
15
|
+
export const usePlugins = (pluginIds: Array<PluginID>) => {
|
|
15
16
|
useEffect(() => {
|
|
16
17
|
// 加载指定的插件
|
|
17
18
|
pluginIds.forEach(id => {
|
|
@@ -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();
|
package/src/plugins/manifest.ts
CHANGED
|
@@ -1,10 +1,4 @@
|
|
|
1
1
|
export const PLUGIN_MANIFEST = [
|
|
2
|
-
{
|
|
3
|
-
name: 'test',
|
|
4
|
-
id: 'test',
|
|
5
|
-
version: '0.1.0',
|
|
6
|
-
description: 'A test plugin',
|
|
7
|
-
},
|
|
8
2
|
{
|
|
9
3
|
name: 'sticker',
|
|
10
4
|
id: 'sticker',
|
|
@@ -12,9 +6,16 @@ export const PLUGIN_MANIFEST = [
|
|
|
12
6
|
description: 'A sticker plugin',
|
|
13
7
|
},
|
|
14
8
|
{
|
|
15
|
-
name: '
|
|
16
|
-
id: '
|
|
9
|
+
name: 'extra-cd',
|
|
10
|
+
id: 'extra-cd',
|
|
11
|
+
version: '0.1.0',
|
|
12
|
+
description:
|
|
13
|
+
'Provides additional change directory functionalities for enhanced user experience.',
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
name: 'xwy',
|
|
17
|
+
id: 'xwy',
|
|
17
18
|
version: '0.1.0',
|
|
18
|
-
description: '
|
|
19
|
+
description: 'customize for xwy',
|
|
19
20
|
},
|
|
20
21
|
];
|
|
@@ -3,17 +3,6 @@ import React from 'react';
|
|
|
3
3
|
|
|
4
4
|
import { StickerCtx } from '../context';
|
|
5
5
|
|
|
6
|
-
declare global {
|
|
7
|
-
namespace JSX {
|
|
8
|
-
interface IntrinsicElements {
|
|
9
|
-
'bb-img': React.DetailedHTMLProps<
|
|
10
|
-
React.ImgHTMLAttributes<HTMLImageElement>,
|
|
11
|
-
HTMLImageElement
|
|
12
|
-
>;
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
6
|
export const StickerCom = ({ data }: { data: PathObj[] }) => {
|
|
18
7
|
const ctx = StickerCtx.useCtx();
|
|
19
8
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { ReactNode } from 'react';
|
|
2
|
+
import { useLocation } from 'react-router-dom';
|
|
2
3
|
|
|
3
4
|
export const Crows = () => {
|
|
4
5
|
return (
|
|
@@ -11,3 +12,13 @@ export const Crows = () => {
|
|
|
11
12
|
</svg>
|
|
12
13
|
);
|
|
13
14
|
};
|
|
15
|
+
|
|
16
|
+
export const XwyLogo = ({ data: defaultLogo }: { data: ReactNode }) => {
|
|
17
|
+
const location = useLocation();
|
|
18
|
+
|
|
19
|
+
if (decodeURIComponent(location.pathname).includes('小乌鸦')) {
|
|
20
|
+
return <Crows />;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return <>{defaultLogo}</>;
|
|
24
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { FontRule } from '../types';
|
|
2
|
+
|
|
3
|
+
export const FontRules: Array<FontRule> = [
|
|
4
|
+
{
|
|
5
|
+
match: '小乌鸦合集',
|
|
6
|
+
fontFamily: 'xwy',
|
|
7
|
+
fontConfig: {
|
|
8
|
+
name: 'xwy',
|
|
9
|
+
src: '/fonts/xwy.woff2',
|
|
10
|
+
format: 'woff2',
|
|
11
|
+
},
|
|
12
|
+
extraCls: 'text-2xl',
|
|
13
|
+
variant: 'special',
|
|
14
|
+
},
|
|
15
|
+
];
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { IHostContext } from '#/types/hostApi';
|
|
2
|
+
import { IPlugin } from '#/types/plugin';
|
|
3
|
+
import { TitleListItem } from '#/types/posts';
|
|
4
|
+
|
|
5
|
+
import { XwyLogo } from './components/logo';
|
|
6
|
+
import { FontRules } from './const';
|
|
7
|
+
import { loadFont } from './utils';
|
|
8
|
+
|
|
9
|
+
class XwyPlugin implements IPlugin {
|
|
10
|
+
id = 'fontstyler';
|
|
11
|
+
name = 'Font Styler';
|
|
12
|
+
description = 'Apply custom fonts to specific post titles';
|
|
13
|
+
version = '1.0.0';
|
|
14
|
+
author = 'bbki.ng';
|
|
15
|
+
|
|
16
|
+
private loadedFonts = new Set<string>();
|
|
17
|
+
|
|
18
|
+
onInstall = (ctx: IHostContext) => {
|
|
19
|
+
// 加载所有需要的字体文件
|
|
20
|
+
FontRules.forEach(rule => {
|
|
21
|
+
if (rule.fontConfig && !this.loadedFonts.has(rule.fontConfig.name)) {
|
|
22
|
+
loadFont(rule.fontConfig);
|
|
23
|
+
this.loadedFonts.add(rule.fontConfig.name);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// 注册中间件,修改标题列表
|
|
28
|
+
ctx.api.registerMiddleware(
|
|
29
|
+
'transformTitleList',
|
|
30
|
+
this.transformTitleList,
|
|
31
|
+
this.id,
|
|
32
|
+
10 // weight
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
// 注册logo插槽组件
|
|
36
|
+
ctx.api.registerSlot('logo', XwyLogo, this.id);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 转换标题列表,为匹配的标题添加字体类名
|
|
41
|
+
*/
|
|
42
|
+
private transformTitleList = (titleList: TitleListItem[]): TitleListItem[] => {
|
|
43
|
+
return titleList.map(item => {
|
|
44
|
+
for (const rule of FontRules) {
|
|
45
|
+
const match =
|
|
46
|
+
typeof rule.match === 'string' ? item.name === rule.match : rule.match.test(item.name);
|
|
47
|
+
|
|
48
|
+
if (match) {
|
|
49
|
+
return {
|
|
50
|
+
...item,
|
|
51
|
+
variant: rule.variant || 'default',
|
|
52
|
+
className: `font-${rule.fontFamily} ${rule.extraCls || ''}`.trim(),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return item;
|
|
57
|
+
});
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
onDisable = async () => {
|
|
61
|
+
this.loadedFonts.forEach(fontName => {
|
|
62
|
+
const styleEl = document.getElementById(`font-${fontName}`);
|
|
63
|
+
if (styleEl) {
|
|
64
|
+
styleEl.remove();
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
this.loadedFonts.clear();
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
onDestroy = () => {};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export default new XwyPlugin();
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface FontConfig {
|
|
2
|
+
name: string;
|
|
3
|
+
src: string;
|
|
4
|
+
format: 'woff2' | 'woff' | 'ttf' | 'otf';
|
|
5
|
+
fontWeight?: string;
|
|
6
|
+
fontStyle?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface FontRule {
|
|
10
|
+
match: string | RegExp;
|
|
11
|
+
fontFamily: string;
|
|
12
|
+
fontConfig?: FontConfig;
|
|
13
|
+
extraCls?: string;
|
|
14
|
+
variant?: 'default' | 'special';
|
|
15
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type FontConfig } from '../types';
|
|
2
|
+
|
|
3
|
+
export const loadFont = (config: FontConfig) => {
|
|
4
|
+
const styleId = `font-${config.name}`;
|
|
5
|
+
if (document.getElementById(styleId)) return;
|
|
6
|
+
|
|
7
|
+
const style = document.createElement('style');
|
|
8
|
+
style.id = styleId;
|
|
9
|
+
style.textContent = `
|
|
10
|
+
@font-face {
|
|
11
|
+
font-family: '${config.name}';
|
|
12
|
+
src: url('${config.src}') format('${config.format}');
|
|
13
|
+
font-weight: ${config.fontWeight || 'normal'};
|
|
14
|
+
font-style: ${config.fontStyle || 'normal'};
|
|
15
|
+
font-display: swap;
|
|
16
|
+
}
|
|
17
|
+
.font-${config.name} {
|
|
18
|
+
font-family: '${config.name}', monospace;
|
|
19
|
+
}
|
|
20
|
+
`;
|
|
21
|
+
document.head.appendChild(style);
|
|
22
|
+
};
|
package/src/types/hostApi.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
|
|
3
|
-
import { HookPoint, SlotName } from './slots';
|
|
4
3
|
import { type FingerprintData } from '@/utils/fingerprints';
|
|
5
4
|
|
|
5
|
+
import { HookPoint, SlotName } from './slots';
|
|
6
|
+
|
|
6
7
|
export interface IHostApi {
|
|
7
8
|
getDeviceId: () => Promise<{ id: string; fp: FingerprintData }>;
|
|
8
9
|
registerMiddleware: <T>(
|
package/src/types/plugin.ts
CHANGED
package/src/types/slots.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export type SlotName = 'leftCol' | 'rightCol' | 'articleActionRow';
|
|
1
|
+
export type SlotName = 'leftCol' | 'rightCol' | 'articleActionRow' | 'logo';
|
|
2
2
|
export type HookPoint = 'filterPosts' | 'transformPostContent' | 'transformTitleList';
|
package/tsconfig.json
CHANGED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { Role, useRole } from './use_role';
|
|
3
|
-
import { useLocation } from 'react-router-dom';
|
|
4
|
-
import { Crows } from '@/components/Pochacco/xwy';
|
|
5
|
-
|
|
6
|
-
export const useDynamicLogo = () => {
|
|
7
|
-
const role = useRole();
|
|
8
|
-
const location = useLocation();
|
|
9
|
-
|
|
10
|
-
if (decodeURIComponent(location.pathname).includes('小乌鸦')) {
|
|
11
|
-
return <Crows />;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
return null;
|
|
15
|
-
};
|
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
import { IHostContext } from '#/types/hostApi';
|
|
2
|
-
import { IPlugin } from '#/types/plugin';
|
|
3
|
-
|
|
4
|
-
export interface TitleListItem {
|
|
5
|
-
name: string;
|
|
6
|
-
to: string;
|
|
7
|
-
children: string;
|
|
8
|
-
className?: string;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export interface FontConfig {
|
|
12
|
-
name: string;
|
|
13
|
-
src: string;
|
|
14
|
-
format: 'woff2' | 'woff' | 'ttf' | 'otf';
|
|
15
|
-
fontWeight?: string;
|
|
16
|
-
fontStyle?: string;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export interface FontRule {
|
|
20
|
-
match: string | RegExp;
|
|
21
|
-
fontFamily: string;
|
|
22
|
-
fontConfig?: FontConfig;
|
|
23
|
-
extraCls?: string;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
class FontStylerPlugin implements IPlugin {
|
|
27
|
-
id = 'fontstyler';
|
|
28
|
-
name = 'Font Styler';
|
|
29
|
-
description = 'Apply custom fonts to specific post titles';
|
|
30
|
-
version = '1.0.0';
|
|
31
|
-
author = 'bbki.ng';
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* 配置:标题匹配规则 → 字体配置
|
|
35
|
-
* 支持精确字符串匹配或正则表达式匹配
|
|
36
|
-
*
|
|
37
|
-
* 示例:
|
|
38
|
-
* - 精确匹配: { match: '某篇文章标题', fontFamily: 'CustomFont1', fontConfig: {...} }
|
|
39
|
-
* - 正则匹配: { match: /^示例/, fontFamily: 'CustomFont2', fontConfig: {...} }
|
|
40
|
-
*/
|
|
41
|
-
private fontRules: FontRule[] = [
|
|
42
|
-
{
|
|
43
|
-
match: '小乌鸦合集',
|
|
44
|
-
fontFamily: 'xwy',
|
|
45
|
-
fontConfig: {
|
|
46
|
-
name: 'xwy',
|
|
47
|
-
src: '/fonts/xwy.woff2',
|
|
48
|
-
format: 'woff2',
|
|
49
|
-
},
|
|
50
|
-
extraCls: 'text-2xl',
|
|
51
|
-
},
|
|
52
|
-
];
|
|
53
|
-
|
|
54
|
-
private loadedFonts = new Set<string>();
|
|
55
|
-
|
|
56
|
-
onInstall = (ctx: IHostContext) => {
|
|
57
|
-
// 加载所有需要的字体文件
|
|
58
|
-
this.fontRules.forEach(rule => {
|
|
59
|
-
if (rule.fontConfig && !this.loadedFonts.has(rule.fontConfig.name)) {
|
|
60
|
-
this.loadFont(rule.fontConfig);
|
|
61
|
-
this.loadedFonts.add(rule.fontConfig.name);
|
|
62
|
-
}
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
ctx.api.registerMiddleware(
|
|
66
|
-
'transformTitleList',
|
|
67
|
-
this.transformTitleList,
|
|
68
|
-
this.id,
|
|
69
|
-
10 // weight
|
|
70
|
-
);
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* 动态加载字体文件
|
|
75
|
-
* 通过创建 style 标签添加 @font-face 规则
|
|
76
|
-
*/
|
|
77
|
-
private loadFont = (config: FontConfig) => {
|
|
78
|
-
const styleId = `font-${config.name}`;
|
|
79
|
-
if (document.getElementById(styleId)) return;
|
|
80
|
-
|
|
81
|
-
const style = document.createElement('style');
|
|
82
|
-
style.id = styleId;
|
|
83
|
-
style.textContent = `
|
|
84
|
-
@font-face {
|
|
85
|
-
font-family: '${config.name}';
|
|
86
|
-
src: url('${config.src}') format('${config.format}');
|
|
87
|
-
font-weight: ${config.fontWeight || 'normal'};
|
|
88
|
-
font-style: ${config.fontStyle || 'normal'};
|
|
89
|
-
font-display: swap;
|
|
90
|
-
}
|
|
91
|
-
.font-${config.name} {
|
|
92
|
-
font-family: '${config.name}', monospace;
|
|
93
|
-
}
|
|
94
|
-
`;
|
|
95
|
-
document.head.appendChild(style);
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* 转换标题列表,为匹配的标题添加字体类名
|
|
100
|
-
*/
|
|
101
|
-
private transformTitleList = (titleList: TitleListItem[]): TitleListItem[] => {
|
|
102
|
-
return titleList.map(item => {
|
|
103
|
-
for (const rule of this.fontRules) {
|
|
104
|
-
const match =
|
|
105
|
-
typeof rule.match === 'string' ? item.name === rule.match : rule.match.test(item.name);
|
|
106
|
-
|
|
107
|
-
if (match) {
|
|
108
|
-
return {
|
|
109
|
-
...item,
|
|
110
|
-
className: `font-${rule.fontFamily} ${rule.extraCls || ''}`.trim(),
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
return item;
|
|
115
|
-
});
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
onDisable = async () => {
|
|
119
|
-
// 清理加载的字体样式
|
|
120
|
-
this.loadedFonts.forEach(fontName => {
|
|
121
|
-
const styleEl = document.getElementById(`font-${fontName}`);
|
|
122
|
-
if (styleEl) {
|
|
123
|
-
styleEl.remove();
|
|
124
|
-
}
|
|
125
|
-
});
|
|
126
|
-
this.loadedFonts.clear();
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
onDestroy = () => {};
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
export default new FontStylerPlugin();
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { IHostContext } from '#/types/hostApi';
|
|
2
|
-
import { IPlugin } from '#/types/plugin';
|
|
3
|
-
import { Emoji } from './components/emoji';
|
|
4
|
-
|
|
5
|
-
class TestPlugin implements IPlugin {
|
|
6
|
-
id: string = 'test';
|
|
7
|
-
name: string = 'Test Plugin';
|
|
8
|
-
description?: string | undefined;
|
|
9
|
-
version: string = '0.1.0';
|
|
10
|
-
author?: string | undefined;
|
|
11
|
-
|
|
12
|
-
onInstall?: ((ctx: IHostContext) => void) | undefined = (ctx: IHostContext) => {
|
|
13
|
-
ctx.api.registerSlot('leftCol', Emoji, this.id);
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
onDisable?: (() => Promise<void> | void) | undefined;
|
|
17
|
-
onDestroy?: (() => void) | undefined;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const testPlugin = new TestPlugin();
|
|
21
|
-
|
|
22
|
-
export default testPlugin;
|