feat(spotlight): add support for custom search-engines (#1200)
* feat(spotlight): add search settings link * feat(search-engine): add to manage pages * feat(spotlight): add children option for external search engines * chore: revert search settings * fix: deepsource issue * fix: inconsistent breadcrum placement * chore: address pull request feedback
This commit is contained in:
@@ -1,82 +1,85 @@
|
||||
import { Group, Stack, Text } from "@mantine/core";
|
||||
import type { TablerIcon } from "@tabler/icons-react";
|
||||
import { IconDownload } from "@tabler/icons-react";
|
||||
import { Group, Kbd, Stack, Text } from "@mantine/core";
|
||||
import { IconSearch } from "@tabler/icons-react";
|
||||
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
import { createChildrenOptions } from "../../lib/children";
|
||||
import { createGroup } from "../../lib/group";
|
||||
import { interaction } from "../../lib/interaction";
|
||||
|
||||
// This has to be type so it can be interpreted as Record<string, unknown>.
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
|
||||
type SearchEngine = {
|
||||
short: string;
|
||||
image: string | TablerIcon;
|
||||
name: string;
|
||||
description: string;
|
||||
urlTemplate: string;
|
||||
};
|
||||
type SearchEngine = RouterOutputs["searchEngine"]["search"][number];
|
||||
|
||||
export const searchEnginesChildrenOptions = createChildrenOptions<SearchEngine>({
|
||||
useActions: () => [
|
||||
{
|
||||
key: "search",
|
||||
component: ({ name }) => {
|
||||
const tChildren = useScopedI18n("search.mode.external.group.searchEngine.children");
|
||||
|
||||
return (
|
||||
<Group mx="md" my="sm">
|
||||
<IconSearch stroke={1.5} />
|
||||
<Text>{tChildren("action.search.label", { name })}</Text>
|
||||
</Group>
|
||||
);
|
||||
},
|
||||
useInteraction: interaction.link(({ urlTemplate }, query) => ({
|
||||
href: urlTemplate.replace("%s", query),
|
||||
})),
|
||||
},
|
||||
],
|
||||
detailComponent({ options }) {
|
||||
const tChildren = useScopedI18n("search.mode.external.group.searchEngine.children");
|
||||
return (
|
||||
<Stack mx="md" my="sm">
|
||||
<Text>{tChildren("detail.title")}</Text>
|
||||
<Group>
|
||||
<img height={24} width={24} src={options.iconUrl} alt={options.name} />
|
||||
<Text>{options.name}</Text>
|
||||
</Group>
|
||||
</Stack>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export const searchEnginesSearchGroups = createGroup<SearchEngine>({
|
||||
keyPath: "short",
|
||||
title: (t) => t("search.mode.external.group.searchEngine.title"),
|
||||
component: ({ image: Image, name, description }) => (
|
||||
<Group w="100%" wrap="nowrap" justify="space-between" align="center" px="md" py="xs">
|
||||
<Group wrap="nowrap">
|
||||
{typeof Image === "string" ? <img height={24} width={24} src={Image} alt={name} /> : <Image size={24} />}
|
||||
<Stack gap={0} justify="center">
|
||||
<Text size="sm">{name}</Text>
|
||||
<Text size="xs" c="gray.6">
|
||||
{description}
|
||||
</Text>
|
||||
</Stack>
|
||||
component: ({ iconUrl, name, short, description }) => {
|
||||
return (
|
||||
<Group w="100%" wrap="nowrap" justify="space-between" align="center" px="md" py="xs">
|
||||
<Group wrap="nowrap">
|
||||
<img height={24} width={24} src={iconUrl} alt={name} />
|
||||
<Stack gap={0} justify="center">
|
||||
<Text size="sm">{name}</Text>
|
||||
<Text size="xs" c="gray.6">
|
||||
{description}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Group>
|
||||
|
||||
<Kbd size="sm">{short}</Kbd>
|
||||
</Group>
|
||||
</Group>
|
||||
),
|
||||
filter: () => true,
|
||||
);
|
||||
},
|
||||
onKeyDown(event, options, query, { setChildrenOptions }) {
|
||||
if (event.code !== "Space") return;
|
||||
|
||||
const engine = options.find((option) => option.short === query);
|
||||
if (!engine) return;
|
||||
|
||||
setChildrenOptions(searchEnginesChildrenOptions(engine));
|
||||
},
|
||||
useInteraction: interaction.link(({ urlTemplate }, query) => ({
|
||||
href: urlTemplate.replace("%s", query),
|
||||
newTab: true,
|
||||
})),
|
||||
useOptions() {
|
||||
const tOption = useScopedI18n("search.mode.external.group.searchEngine.option");
|
||||
|
||||
return [
|
||||
{
|
||||
short: "g",
|
||||
name: tOption("google.name"),
|
||||
image: "https://www.google.com/favicon.ico",
|
||||
description: tOption("google.description"),
|
||||
urlTemplate: "https://www.google.com/search?q=%s",
|
||||
},
|
||||
{
|
||||
short: "b",
|
||||
name: tOption("bing.name"),
|
||||
image: "https://www.bing.com/favicon.ico",
|
||||
description: tOption("bing.description"),
|
||||
urlTemplate: "https://www.bing.com/search?q=%s",
|
||||
},
|
||||
{
|
||||
short: "d",
|
||||
name: tOption("duckduckgo.name"),
|
||||
image: "https://duckduckgo.com/favicon.ico",
|
||||
description: tOption("duckduckgo.description"),
|
||||
urlTemplate: "https://duckduckgo.com/?q=%s",
|
||||
},
|
||||
{
|
||||
short: "t",
|
||||
name: tOption("torrent.name"),
|
||||
image: IconDownload,
|
||||
description: tOption("torrent.description"),
|
||||
urlTemplate: "https://www.torrentdownloads.pro/search/?search=%s",
|
||||
},
|
||||
{
|
||||
short: "y",
|
||||
name: tOption("youTube.name"),
|
||||
image: "https://www.youtube.com/favicon.ico",
|
||||
description: tOption("youTube.description"),
|
||||
urlTemplate: "https://www.youtube.com/results?search_query=%s",
|
||||
},
|
||||
];
|
||||
useQueryOptions(query) {
|
||||
return clientApi.searchEngine.search.useQuery({
|
||||
query: query.trim(),
|
||||
limit: 5,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user