feat: add request media (#1811)
This commit is contained in:
@@ -4,14 +4,25 @@ import { ChildrenActionItem } from "./items/children-action-item";
|
||||
interface SpotlightChildrenActionsProps {
|
||||
childrenOptions: inferSearchInteractionOptions<"children">;
|
||||
query: string;
|
||||
setChildrenOptions: (options: inferSearchInteractionOptions<"children">) => void;
|
||||
}
|
||||
|
||||
export const SpotlightChildrenActions = ({ childrenOptions, query }: SpotlightChildrenActionsProps) => {
|
||||
export const SpotlightChildrenActions = ({
|
||||
childrenOptions,
|
||||
query,
|
||||
setChildrenOptions,
|
||||
}: SpotlightChildrenActionsProps) => {
|
||||
const actions = childrenOptions.useActions(childrenOptions.option, query);
|
||||
|
||||
return actions
|
||||
.filter((action) => (typeof action.hide === "function" ? !action.hide(childrenOptions.option) : !action.hide))
|
||||
.map((action) => (
|
||||
<ChildrenActionItem key={action.key} childrenOptions={childrenOptions} query={query} action={action} />
|
||||
<ChildrenActionItem
|
||||
key={action.key}
|
||||
childrenOptions={childrenOptions}
|
||||
query={query}
|
||||
action={action}
|
||||
setChildrenOptions={setChildrenOptions}
|
||||
/>
|
||||
));
|
||||
};
|
||||
|
||||
@@ -8,9 +8,10 @@ interface ChildrenActionItemProps {
|
||||
childrenOptions: inferSearchInteractionOptions<"children">;
|
||||
query: string;
|
||||
action: ReturnType<inferSearchInteractionOptions<"children">["useActions"]>[number];
|
||||
setChildrenOptions: (options: inferSearchInteractionOptions<"children">) => void;
|
||||
}
|
||||
|
||||
export const ChildrenActionItem = ({ childrenOptions, action, query }: ChildrenActionItemProps) => {
|
||||
export const ChildrenActionItem = ({ childrenOptions, action, query, setChildrenOptions }: ChildrenActionItemProps) => {
|
||||
const interaction = action.useInteraction(childrenOptions.option, query);
|
||||
|
||||
const renderRoot =
|
||||
@@ -20,10 +21,20 @@ export const ChildrenActionItem = ({ childrenOptions, action, query }: ChildrenA
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const onClick = interaction.type === "javaScript" ? interaction.onSelect : undefined;
|
||||
const onClick =
|
||||
interaction.type === "javaScript"
|
||||
? interaction.onSelect
|
||||
: interaction.type === "children"
|
||||
? () => setChildrenOptions(interaction)
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<Spotlight.Action renderRoot={renderRoot} onClick={onClick} className={classes.spotlightAction}>
|
||||
<Spotlight.Action
|
||||
renderRoot={renderRoot}
|
||||
onClick={onClick}
|
||||
closeSpotlightOnTrigger={interaction.type !== "children"}
|
||||
className={classes.spotlightAction}
|
||||
>
|
||||
<action.Component {...childrenOptions.option} />
|
||||
</Spotlight.Action>
|
||||
);
|
||||
|
||||
@@ -140,7 +140,11 @@ const SpotlightWithActiveMode = ({ modeState, activeMode, defaultMode }: Spotlig
|
||||
|
||||
<MantineSpotlight.ActionsList>
|
||||
{childrenOptions ? (
|
||||
<SpotlightChildrenActions childrenOptions={childrenOptions} query={query} />
|
||||
<SpotlightChildrenActions
|
||||
childrenOptions={childrenOptions}
|
||||
query={query}
|
||||
setChildrenOptions={setChildrenOptions}
|
||||
/>
|
||||
) : (
|
||||
<SpotlightActionGroups
|
||||
setMode={(mode) => {
|
||||
|
||||
@@ -10,7 +10,10 @@ export interface CreateChildrenOptionsProps<TParentOptions extends Record<string
|
||||
export interface ChildrenAction<TParentOptions extends Record<string, unknown>> {
|
||||
key: string;
|
||||
Component: (option: TParentOptions) => JSX.Element;
|
||||
useInteraction: (option: TParentOptions, query: string) => inferSearchInteractionDefinition<"link" | "javaScript">;
|
||||
useInteraction: (
|
||||
option: TParentOptions,
|
||||
query: string,
|
||||
) => inferSearchInteractionDefinition<"link" | "javaScript" | "children">;
|
||||
hide?: boolean | ((option: TParentOptions) => boolean);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { Group, Image, Kbd, Stack, Text } from "@mantine/core";
|
||||
import { IconSearch } from "@tabler/icons-react";
|
||||
import { IconDownload, IconSearch } from "@tabler/icons-react";
|
||||
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import type { IntegrationKind } from "@homarr/definitions";
|
||||
import { getIntegrationKindsByCategory, getIntegrationName } from "@homarr/definitions";
|
||||
import { useModalAction } from "@homarr/modals";
|
||||
import { RequestMediaModal } from "@homarr/modals-collection";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
import { createChildrenOptions } from "../../lib/children";
|
||||
@@ -11,6 +15,100 @@ import { interaction } from "../../lib/interaction";
|
||||
|
||||
type SearchEngine = RouterOutputs["searchEngine"]["search"][number];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
|
||||
type MediaRequestChildrenProps = {
|
||||
result: {
|
||||
id: number;
|
||||
image?: string;
|
||||
name: string;
|
||||
link: string;
|
||||
text?: string;
|
||||
type: "tv" | "movie";
|
||||
inLibrary: boolean;
|
||||
};
|
||||
integration: {
|
||||
kind: IntegrationKind;
|
||||
url: string;
|
||||
id: string;
|
||||
};
|
||||
};
|
||||
|
||||
const mediaRequestsChildrenOptions = createChildrenOptions<MediaRequestChildrenProps>({
|
||||
useActions() {
|
||||
const { openModal } = useModalAction(RequestMediaModal);
|
||||
return [
|
||||
{
|
||||
key: "request",
|
||||
hide: (option) => option.result.inLibrary,
|
||||
Component(option) {
|
||||
const t = useScopedI18n("search.mode.media");
|
||||
return (
|
||||
<Group mx="md" my="sm" wrap="nowrap">
|
||||
<IconDownload stroke={1.5} />
|
||||
{option.result.type === "tv" && <Text>{t("requestSeries")}</Text>}
|
||||
{option.result.type === "movie" && <Text>{t("requestMovie")}</Text>}
|
||||
</Group>
|
||||
);
|
||||
},
|
||||
useInteraction: interaction.javaScript((option) => ({
|
||||
onSelect() {
|
||||
openModal(
|
||||
{
|
||||
integrationId: option.integration.id,
|
||||
mediaId: option.result.id,
|
||||
mediaType: option.result.type,
|
||||
},
|
||||
{
|
||||
title(t) {
|
||||
return t("search.engine.media.request.modal.title", { name: option.result.name });
|
||||
},
|
||||
},
|
||||
);
|
||||
},
|
||||
})),
|
||||
},
|
||||
{
|
||||
key: "open",
|
||||
Component({ integration }) {
|
||||
const tChildren = useScopedI18n("search.mode.media");
|
||||
return (
|
||||
<Group mx="md" my="sm" wrap="nowrap">
|
||||
<IconSearch stroke={1.5} />
|
||||
<Text>{tChildren("openIn", { kind: getIntegrationName(integration.kind) })}</Text>
|
||||
</Group>
|
||||
);
|
||||
},
|
||||
useInteraction({ result }) {
|
||||
return {
|
||||
type: "link",
|
||||
href: result.link,
|
||||
newTab: true,
|
||||
};
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
DetailComponent({ options }) {
|
||||
return (
|
||||
<Group mx="md" my="sm" wrap="nowrap">
|
||||
{options.result.image ? (
|
||||
<Image src={options.result.image} w={35} h={50} fit="cover" radius={"md"} />
|
||||
) : (
|
||||
<IconSearch stroke={1.5} size={35} />
|
||||
)}
|
||||
<Stack gap={2}>
|
||||
<Text>{options.result.name}</Text>
|
||||
{options.result.text && (
|
||||
<Text c="dimmed" size="sm" lineClamp={2}>
|
||||
{options.result.text}
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
</Group>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export const searchEnginesChildrenOptions = createChildrenOptions<SearchEngine>({
|
||||
useActions: (searchEngine, query) => {
|
||||
const { data } = clientApi.integration.searchInIntegration.useQuery(
|
||||
@@ -64,10 +162,48 @@ export const searchEnginesChildrenOptions = createChildrenOptions<SearchEngine>(
|
||||
</Group>
|
||||
);
|
||||
},
|
||||
useInteraction: interaction.link(() => ({
|
||||
href: searchResult.link,
|
||||
newTab: true,
|
||||
})),
|
||||
useInteraction(searchEngine) {
|
||||
if (searchEngine.type !== "fromIntegration") {
|
||||
throw new Error("Invalid search engine type");
|
||||
}
|
||||
|
||||
if (!searchEngine.integration) {
|
||||
throw new Error("Invalid search engine integration");
|
||||
}
|
||||
|
||||
if (
|
||||
getIntegrationKindsByCategory("mediaRequest").some(
|
||||
(categoryKind) => categoryKind === searchEngine.integration?.kind,
|
||||
) &&
|
||||
"type" in searchResult
|
||||
) {
|
||||
const type = searchResult.type;
|
||||
if (type === "person") {
|
||||
return {
|
||||
type: "link",
|
||||
href: searchResult.link,
|
||||
newTab: true,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
type: "children",
|
||||
...mediaRequestsChildrenOptions({
|
||||
result: {
|
||||
...searchResult,
|
||||
type,
|
||||
},
|
||||
integration: searchEngine.integration,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
type: "link",
|
||||
href: searchResult.link,
|
||||
newTab: true,
|
||||
};
|
||||
},
|
||||
}));
|
||||
},
|
||||
DetailComponent({ options }) {
|
||||
|
||||
Reference in New Issue
Block a user