mirror of
https://github.com/nonebot/nonebot2.git
synced 2025-09-07 20:46:45 +00:00
📝 Docs: 升级新版 NonePress 主题 (#2375)
This commit is contained in:
16
website/src/libs/color.ts
Normal file
16
website/src/libs/color.ts
Normal file
@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Choose fg color by bg color
|
||||
* @see https://www.npmjs.com/package/colord
|
||||
* @see https://www.w3.org/TR/AERT/#color-contrast
|
||||
*/
|
||||
export function pickTextColor(
|
||||
bgColor: string,
|
||||
lightColor: string,
|
||||
darkColor: string
|
||||
) {
|
||||
const color = bgColor.charAt(0) === "#" ? bgColor.substring(1, 7) : bgColor;
|
||||
const r = parseInt(color.substring(0, 2), 16); // hexToR
|
||||
const g = parseInt(color.substring(2, 4), 16); // hexToG
|
||||
const b = parseInt(color.substring(4, 6), 16); // hexToB
|
||||
return r * 0.299 + g * 0.587 + b * 0.114 > 186 ? darkColor : lightColor;
|
||||
}
|
130
website/src/libs/filter.ts
Normal file
130
website/src/libs/filter.ts
Normal file
@ -0,0 +1,130 @@
|
||||
import { useCallback, useState } from "react";
|
||||
|
||||
import { translate } from "@docusaurus/Translate";
|
||||
|
||||
import type { Resource } from "./store";
|
||||
|
||||
export type Filter<T extends Resource = Resource> = {
|
||||
type: string;
|
||||
id: string;
|
||||
displayName: string;
|
||||
filter: (resource: T) => boolean;
|
||||
};
|
||||
|
||||
export const tagFilter = <T extends Resource = Resource>(
|
||||
tag: string
|
||||
): Filter<T> => ({
|
||||
type: "tag",
|
||||
id: `tag-${tag}`,
|
||||
displayName: translate(
|
||||
{
|
||||
id: "pages.store.filter.tagDisplayName",
|
||||
description: "The display name of tag filter",
|
||||
message: "标签: {tag}",
|
||||
},
|
||||
{ tag }
|
||||
),
|
||||
filter: (resource: Resource): boolean =>
|
||||
resource.tags.map((tag) => tag.label).includes(tag),
|
||||
});
|
||||
export const officialFilter = <T extends Resource = Resource>(
|
||||
official: boolean = true
|
||||
): Filter<T> => ({
|
||||
type: "official",
|
||||
id: `official-${official}`,
|
||||
displayName: translate({
|
||||
id: "pages.store.filter.officialDisplayName",
|
||||
description: "The display name of official filter",
|
||||
message: "非官方|官方",
|
||||
}).split("|")[Number(official)],
|
||||
filter: (resource: Resource): boolean => resource.is_official === official,
|
||||
});
|
||||
export const authorFilter = <T extends Resource = Resource>(
|
||||
author: string
|
||||
): Filter<T> => ({
|
||||
type: "author",
|
||||
id: `author-${author}`,
|
||||
displayName: translate(
|
||||
{
|
||||
id: "pages.store.filter.authorDisplayName",
|
||||
description: "The display name of author filter",
|
||||
message: "作者: {author}",
|
||||
},
|
||||
{ author }
|
||||
),
|
||||
filter: (resource: Resource): boolean => resource.author === author,
|
||||
});
|
||||
export const queryFilter = <T extends Resource = Resource>(
|
||||
query: string
|
||||
): Filter<T> => ({
|
||||
type: "query",
|
||||
id: `query-${query}`,
|
||||
displayName: query,
|
||||
filter: (resource: Resource): boolean => {
|
||||
if (!query) return true;
|
||||
const queryLower = query.toLowerCase();
|
||||
const pluginMatch =
|
||||
resource.resourceType === "plugin" &&
|
||||
(resource.module_name?.toLowerCase().includes(queryLower) ||
|
||||
resource.project_link?.toLowerCase().includes(queryLower));
|
||||
const commonMatch =
|
||||
resource.name.toLowerCase().includes(queryLower) ||
|
||||
resource.desc.toLowerCase().includes(queryLower) ||
|
||||
resource.author.toLowerCase().includes(queryLower) ||
|
||||
resource.tags.filter((t) => t.label.toLowerCase().includes(queryLower))
|
||||
.length > 0;
|
||||
return pluginMatch || commonMatch;
|
||||
},
|
||||
});
|
||||
|
||||
export function filterResources<T extends Resource>(
|
||||
resources: T[],
|
||||
filters: Filter<T>[]
|
||||
): T[] {
|
||||
return resources.filter((resource) =>
|
||||
filters.every((filter) => filter.filter(resource))
|
||||
);
|
||||
}
|
||||
|
||||
type useFilteredResourcesReturn<T extends Resource> = {
|
||||
filters: Filter<T>[];
|
||||
addFilter: (filter: Filter<T>) => void;
|
||||
removeFilter: (filter: Filter<T> | string) => void;
|
||||
filteredResources: T[];
|
||||
};
|
||||
|
||||
export function useFilteredResources<T extends Resource>(
|
||||
resources: T[]
|
||||
): useFilteredResourcesReturn<T> {
|
||||
const [filters, setFilters] = useState<Filter<T>[]>([]);
|
||||
|
||||
const addFilter = useCallback(
|
||||
(filter: Filter<T>) => {
|
||||
if (filters.some((f) => f.id === filter.id)) return;
|
||||
setFilters((filters) => [...filters, filter]);
|
||||
},
|
||||
[filters, setFilters]
|
||||
);
|
||||
const removeFilter = useCallback(
|
||||
(filter: Filter<T> | string) => {
|
||||
setFilters((filters) =>
|
||||
filters.filter((f) =>
|
||||
typeof filter === "string" ? f.id !== filter : f !== filter
|
||||
)
|
||||
);
|
||||
},
|
||||
[setFilters]
|
||||
);
|
||||
|
||||
const filteredResources = useCallback(
|
||||
() => filterResources(resources, filters),
|
||||
[resources, filters]
|
||||
);
|
||||
|
||||
return {
|
||||
filters,
|
||||
addFilter,
|
||||
removeFilter,
|
||||
filteredResources: filteredResources(),
|
||||
};
|
||||
}
|
@ -1,67 +0,0 @@
|
||||
import { useLayoutEffect, useRef } from "react";
|
||||
import ResizeObserver from "resize-observer-polyfill";
|
||||
|
||||
export function useResizeNotifier(
|
||||
element: HTMLElement | undefined,
|
||||
callback: () => void
|
||||
) {
|
||||
const callBackRef = useRef(callback);
|
||||
useLayoutEffect(() => {
|
||||
callBackRef.current = callback;
|
||||
}, [callback]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!element) return;
|
||||
|
||||
const resizeObserver = new ResizeObserver(
|
||||
withResizeLoopDetection(() => {
|
||||
callBackRef.current!();
|
||||
})
|
||||
);
|
||||
|
||||
resizeObserver.observe(element);
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}, [element]);
|
||||
}
|
||||
|
||||
function withResizeLoopDetection(callback: () => void) {
|
||||
return (entries: ResizeObserverEntry[], resizeObserver: ResizeObserver) => {
|
||||
const elements = entries.map((entry) => entry.target);
|
||||
|
||||
const rectsBefore = elements.map((element) =>
|
||||
element.getBoundingClientRect()
|
||||
);
|
||||
|
||||
callback();
|
||||
|
||||
const rectsAfter = elements.map((element) =>
|
||||
element.getBoundingClientRect()
|
||||
);
|
||||
|
||||
const changedElements = elements.filter(
|
||||
(_, i) => !areRectSizesEqual(rectsBefore[i], rectsAfter[i])
|
||||
);
|
||||
|
||||
changedElements.forEach((element) =>
|
||||
unobserveUntilNextFrame(element, resizeObserver)
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
function unobserveUntilNextFrame(
|
||||
element: Element,
|
||||
resizeObserver: ResizeObserver
|
||||
) {
|
||||
resizeObserver.unobserve(element);
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
resizeObserver.observe(element);
|
||||
});
|
||||
}
|
||||
|
||||
function areRectSizesEqual(rect1: DOMRect, rect2: DOMRect) {
|
||||
return rect1.width === rect2.width && rect1.height === rect2.height;
|
||||
}
|
88
website/src/libs/search.ts
Normal file
88
website/src/libs/search.ts
Normal file
@ -0,0 +1,88 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
import { type Filter, useFilteredResources, queryFilter } from "./filter";
|
||||
import type { Resource } from "./store";
|
||||
|
||||
type useSearchControlReturn<T extends Resource> = {
|
||||
filteredResources: T[];
|
||||
searcherTags: string[];
|
||||
addFilter: (filter: Filter<T>) => void;
|
||||
onSearchQueryChange: (query: string) => void;
|
||||
onSearchQuerySubmit: () => void;
|
||||
onSearchBackspace: () => void;
|
||||
onSearchClear: () => void;
|
||||
onSearchTagClick: (index: number) => void;
|
||||
};
|
||||
|
||||
export function useSearchControl<T extends Resource>(
|
||||
resources: T[]
|
||||
): useSearchControlReturn<T> {
|
||||
const [currentFilter, setCurrentFilter] = useState<Filter<T> | null>(null);
|
||||
|
||||
const { filters, addFilter, removeFilter, filteredResources } =
|
||||
useFilteredResources(resources);
|
||||
|
||||
// display tags in searcher (except current filter)
|
||||
const [searcherFilters, setSearcherFilters] = useState<Filter<T>[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
setSearcherFilters(
|
||||
filters.filter((f) => !(currentFilter && f === currentFilter))
|
||||
);
|
||||
}, [filters, currentFilter]);
|
||||
|
||||
const onSearchQueryChange = useCallback(
|
||||
(newQuery: string) => {
|
||||
// remove current filter if query is empty
|
||||
if (newQuery === "") {
|
||||
currentFilter && removeFilter(currentFilter);
|
||||
setCurrentFilter(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const newFilter = queryFilter<T>(newQuery);
|
||||
// do nothing if filter is not changed
|
||||
if (currentFilter?.id === newFilter.id) return;
|
||||
|
||||
// remove old currentFilter
|
||||
currentFilter && removeFilter(currentFilter);
|
||||
// add new filter
|
||||
setCurrentFilter(newFilter);
|
||||
addFilter(newFilter);
|
||||
},
|
||||
[currentFilter, setCurrentFilter, addFilter, removeFilter]
|
||||
);
|
||||
|
||||
const onSearchQuerySubmit = useCallback(() => {
|
||||
// set current filter to null to make filter permanent
|
||||
setCurrentFilter(null);
|
||||
}, [setCurrentFilter]);
|
||||
|
||||
const onSearchBackspace = useCallback(() => {
|
||||
// remove last filter
|
||||
removeFilter(searcherFilters[searcherFilters.length - 1]);
|
||||
}, [removeFilter, searcherFilters]);
|
||||
|
||||
const onSearchClear = useCallback(() => {
|
||||
// remove all filters
|
||||
searcherFilters.forEach((filter) => removeFilter(filter));
|
||||
}, [removeFilter, searcherFilters]);
|
||||
|
||||
const onSearchTagClick = useCallback(
|
||||
(index: number) => {
|
||||
removeFilter(searcherFilters[index]);
|
||||
},
|
||||
[removeFilter, searcherFilters]
|
||||
);
|
||||
|
||||
return {
|
||||
filteredResources,
|
||||
searcherTags: searcherFilters.map((filter) => filter.displayName),
|
||||
addFilter,
|
||||
onSearchQueryChange,
|
||||
onSearchQuerySubmit,
|
||||
onSearchBackspace,
|
||||
onSearchClear,
|
||||
onSearchTagClick,
|
||||
};
|
||||
}
|
@ -1,48 +1,47 @@
|
||||
import { useState } from "react";
|
||||
import { translate } from "@docusaurus/Translate";
|
||||
|
||||
export type Tag = {
|
||||
label: string;
|
||||
color: string;
|
||||
import type { Adapter, AdaptersResponse } from "@/types/adapter";
|
||||
import type { Bot, BotsResponse } from "@/types/bot";
|
||||
import type { Driver, DriversResponse } from "@/types/driver";
|
||||
import type { Plugin, PluginsResponse } from "@/types/plugin";
|
||||
|
||||
type RegistryDataResponseTypes = {
|
||||
adapter: AdaptersResponse;
|
||||
bot: BotsResponse;
|
||||
driver: DriversResponse;
|
||||
plugin: PluginsResponse;
|
||||
};
|
||||
type RegistryDataType = keyof RegistryDataResponseTypes;
|
||||
|
||||
type ResourceTypes = {
|
||||
adapter: Adapter;
|
||||
bot: Bot;
|
||||
driver: Driver;
|
||||
plugin: Plugin;
|
||||
};
|
||||
|
||||
export type Obj = {
|
||||
module_name?: string;
|
||||
project_link?: string;
|
||||
name: string;
|
||||
desc: string;
|
||||
author: string;
|
||||
homepage: string;
|
||||
tags: Tag[];
|
||||
is_official: boolean;
|
||||
};
|
||||
export type Resource = Adapter | Bot | Driver | Plugin;
|
||||
|
||||
export function filterObjs(filter: string, objs: Obj[]): Obj[] {
|
||||
let filterLower = filter.toLowerCase();
|
||||
return objs.filter((o) => {
|
||||
return (
|
||||
o.module_name?.toLowerCase().includes(filterLower) ||
|
||||
o.project_link?.toLowerCase().includes(filterLower) ||
|
||||
o.name.toLowerCase().includes(filterLower) ||
|
||||
o.desc.toLowerCase().includes(filterLower) ||
|
||||
o.author.toLowerCase().includes(filterLower) ||
|
||||
o.tags.filter((t) => t.label.toLowerCase().includes(filterLower)).length >
|
||||
0
|
||||
);
|
||||
export async function fetchRegistryData<T extends RegistryDataType>(
|
||||
dataType: T
|
||||
): Promise<ResourceTypes[T][]> {
|
||||
const resp = await fetch(
|
||||
`https://registry.nonebot.dev/${dataType}s.json`
|
||||
).catch((e) => {
|
||||
throw new Error(`Failed to fetch ${dataType}s: ${e}`);
|
||||
});
|
||||
if (!resp.ok)
|
||||
throw new Error(
|
||||
`Failed to fetch ${dataType}s: ${resp.status} ${resp.statusText}`
|
||||
);
|
||||
const data = (await resp.json()) as RegistryDataResponseTypes[T];
|
||||
return data.map(
|
||||
(resource) => ({ ...resource, resourceType: dataType }) as ResourceTypes[T]
|
||||
);
|
||||
}
|
||||
|
||||
type useFilteredObjsReturn = {
|
||||
filter: string;
|
||||
setFilter: (filter: string) => void;
|
||||
filteredObjs: Obj[];
|
||||
};
|
||||
|
||||
export function useFilteredObjs(objs: Obj[]): useFilteredObjsReturn {
|
||||
const [filter, setFilter] = useState<string>("");
|
||||
const filteredObjs = filterObjs(filter, objs);
|
||||
return {
|
||||
filter,
|
||||
setFilter,
|
||||
filteredObjs,
|
||||
};
|
||||
}
|
||||
export const loadFailedTitle = translate({
|
||||
id: "pages.store.loadFailed.title",
|
||||
message: "加载失败",
|
||||
description: "Title to display when loading content failed",
|
||||
});
|
||||
|
44
website/src/libs/toolbar.ts
Normal file
44
website/src/libs/toolbar.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import { authorFilter, tagFilter, type Filter } from "./filter";
|
||||
import type { Resource } from "./store";
|
||||
|
||||
import type { Filter as FilterTool } from "@/components/Store/Toolbar";
|
||||
|
||||
type Props<T extends Resource = Resource> = {
|
||||
resources: T[];
|
||||
addFilter: (filter: Filter<T>) => void;
|
||||
};
|
||||
|
||||
type useToolbarReturns = {
|
||||
filters: FilterTool[];
|
||||
};
|
||||
|
||||
export function useToolbar<T extends Resource = Resource>({
|
||||
resources,
|
||||
addFilter,
|
||||
}: Props<T>): useToolbarReturns {
|
||||
const authorFilterTool: FilterTool = {
|
||||
label: "作者",
|
||||
icon: ["fas", "user"],
|
||||
choices: Array.from(new Set(resources.map((resource) => resource.author))),
|
||||
onSubmit: (author: string) => {
|
||||
addFilter(authorFilter(author));
|
||||
},
|
||||
};
|
||||
|
||||
const tagFilterTool: FilterTool = {
|
||||
label: "标签",
|
||||
icon: ["fas", "tag"],
|
||||
choices: Array.from(
|
||||
new Set(
|
||||
resources.flatMap((resource) => resource.tags.map((tag) => tag.label))
|
||||
)
|
||||
),
|
||||
onSubmit: (tag: string) => {
|
||||
addFilter(tagFilter(tag));
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
filters: [authorFilterTool, tagFilterTool],
|
||||
};
|
||||
}
|
@ -1,64 +0,0 @@
|
||||
import { useLayoutEffect, useState } from "react";
|
||||
|
||||
import { useResizeNotifier } from "./resize";
|
||||
|
||||
export function getElementWidth(element: HTMLElement) {
|
||||
const style = getComputedStyle(element);
|
||||
|
||||
return (
|
||||
styleMetricToInt(style.marginLeft) +
|
||||
getWidth(element) +
|
||||
styleMetricToInt(style.marginRight)
|
||||
);
|
||||
}
|
||||
|
||||
export function getContentWidth(element: HTMLElement) {
|
||||
const style = getComputedStyle(element);
|
||||
|
||||
return (
|
||||
element.getBoundingClientRect().width -
|
||||
styleMetricToInt(style.borderLeftWidth) -
|
||||
styleMetricToInt(style.paddingLeft) -
|
||||
styleMetricToInt(style.paddingRight) -
|
||||
styleMetricToInt(style.borderRightWidth)
|
||||
);
|
||||
}
|
||||
|
||||
export function getNonContentWidth(element: HTMLElement) {
|
||||
const style = getComputedStyle(element);
|
||||
|
||||
return (
|
||||
styleMetricToInt(style.marginLeft) +
|
||||
styleMetricToInt(style.borderLeftWidth) +
|
||||
styleMetricToInt(style.paddingLeft) +
|
||||
styleMetricToInt(style.paddingRight) +
|
||||
styleMetricToInt(style.borderRightWidth) +
|
||||
styleMetricToInt(style.marginRight)
|
||||
);
|
||||
}
|
||||
|
||||
export function getWidth(element: HTMLElement) {
|
||||
return element.getBoundingClientRect().width;
|
||||
}
|
||||
|
||||
function styleMetricToInt(styleAttribute: string | null) {
|
||||
return styleAttribute ? parseInt(styleAttribute) : 0;
|
||||
}
|
||||
|
||||
export function useContentWidth(element: HTMLElement | undefined) {
|
||||
const [width, setWidth] = useState<number>();
|
||||
|
||||
function syncWidth() {
|
||||
const newWidth = element ? getContentWidth(element) : undefined;
|
||||
|
||||
if (width !== newWidth) {
|
||||
setWidth(newWidth);
|
||||
}
|
||||
}
|
||||
|
||||
useResizeNotifier(element, syncWidth);
|
||||
|
||||
useLayoutEffect(syncWidth);
|
||||
|
||||
return width;
|
||||
}
|
Reference in New Issue
Block a user