Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ec6c79d35 | ||
|
|
2bfe2caffc | ||
|
|
6bf3c75f9f | ||
|
|
afa36bffa9 | ||
|
|
6230db53e0 |
No files matched your search
@@ -149,7 +149,8 @@ export const NewIntegrationForm = ({ searchParams }: NewIntegrationFormProps) =>
|
||||
};
|
||||
|
||||
const integrationCategories = integrationDefs[searchParams.kind].category.flat();
|
||||
const supportsSearchEngine = integrationCategories.includes("search") && !integrationCategories.includes("mediaSearch");
|
||||
const supportsSearchEngine =
|
||||
integrationCategories.includes("search") && !integrationCategories.includes("mediaSearch");
|
||||
|
||||
return (
|
||||
<form onSubmit={form.onSubmit((value) => void handleSubmitAsync(value))}>
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
integrationSavePermissionsSchema,
|
||||
integrationUpdateSchema,
|
||||
} from "@homarr/validation/integration";
|
||||
import { mediaRequestOptionsSchema, mediaRequestRequestSchema } from "@homarr/validation/widgets/media-request";
|
||||
|
||||
import { createOneIntegrationMiddleware } from "../../middlewares/integration";
|
||||
import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure, publicProcedure } from "../../trpc";
|
||||
@@ -81,7 +82,7 @@ export const integrationRouter = createTRPCRouter({
|
||||
},
|
||||
};
|
||||
})
|
||||
.sort(
|
||||
.toSorted(
|
||||
(integrationA, integrationB) =>
|
||||
integrationKinds.indexOf(integrationA.kind) - integrationKinds.indexOf(integrationB.kind),
|
||||
);
|
||||
@@ -129,7 +130,7 @@ export const integrationRouter = createTRPCRouter({
|
||||
},
|
||||
};
|
||||
})
|
||||
.sort(
|
||||
.toSorted(
|
||||
(integrationA, integrationB) =>
|
||||
integrationKinds.indexOf(integrationA.kind) - integrationKinds.indexOf(integrationB.kind),
|
||||
);
|
||||
@@ -180,7 +181,7 @@ export const integrationRouter = createTRPCRouter({
|
||||
},
|
||||
};
|
||||
})
|
||||
.sort(
|
||||
.toSorted(
|
||||
(integrationA, integrationB) =>
|
||||
integrationKinds.indexOf(integrationA.kind) - integrationKinds.indexOf(integrationB.kind),
|
||||
);
|
||||
@@ -483,7 +484,7 @@ export const integrationRouter = createTRPCRouter({
|
||||
});
|
||||
|
||||
return {
|
||||
inherited: dbGroupPermissions.sort((permissionA, permissionB) => {
|
||||
inherited: dbGroupPermissions.toSorted((permissionA, permissionB) => {
|
||||
return permissionA.group.name.localeCompare(permissionB.group.name);
|
||||
}),
|
||||
users: userPermissions
|
||||
@@ -491,7 +492,7 @@ export const integrationRouter = createTRPCRouter({
|
||||
user,
|
||||
permission,
|
||||
}))
|
||||
.sort((permissionA, permissionB) => {
|
||||
.toSorted((permissionA, permissionB) => {
|
||||
return (permissionA.user.name ?? "").localeCompare(permissionB.user.name ?? "");
|
||||
}),
|
||||
groups: dbGroupIntegrationPermission
|
||||
@@ -502,7 +503,7 @@ export const integrationRouter = createTRPCRouter({
|
||||
},
|
||||
permission,
|
||||
}))
|
||||
.sort((permissionA, permissionB) => {
|
||||
.toSorted((permissionA, permissionB) => {
|
||||
return permissionA.group.name.localeCompare(permissionB.group.name);
|
||||
}),
|
||||
};
|
||||
@@ -670,6 +671,20 @@ export const integrationRouter = createTRPCRouter({
|
||||
|
||||
return results.flat();
|
||||
}),
|
||||
getMediaRequestOptions: protectedProcedure
|
||||
.concat(createOneIntegrationMiddleware("query", "jellyseerr", "overseerr", "seerr"))
|
||||
.input(mediaRequestOptionsSchema)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const integration = await createIntegrationAsync(ctx.integration);
|
||||
return await integration.getSeriesInformationAsync(input.mediaType, input.mediaId);
|
||||
}),
|
||||
requestMedia: protectedProcedure
|
||||
.concat(createOneIntegrationMiddleware("interact", "jellyseerr", "overseerr", "seerr"))
|
||||
.input(mediaRequestRequestSchema)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const integration = await createIntegrationAsync(ctx.integration);
|
||||
return await integration.requestMediaAsync(input.mediaType, input.mediaId, input.seasons);
|
||||
}),
|
||||
});
|
||||
|
||||
interface IntegrationRouterContext {
|
||||
|
||||
@@ -6,12 +6,9 @@ import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||
import { asc, eq, like } from "@homarr/db";
|
||||
import { getServerSettingByKeyAsync, updateServerSettingByKeyAsync } from "@homarr/db/queries";
|
||||
import { searchEngines, users } from "@homarr/db/schema";
|
||||
import { createIntegrationAsync } from "@homarr/integrations";
|
||||
import { byIdSchema, paginatedSchema, searchSchema } from "@homarr/validation/common";
|
||||
import { searchEngineEditSchema, searchEngineManageSchema } from "@homarr/validation/search-engine";
|
||||
import { mediaRequestOptionsSchema, mediaRequestRequestSchema } from "@homarr/validation/widgets/media-request";
|
||||
|
||||
import { createOneIntegrationMiddleware } from "../../middlewares/integration";
|
||||
import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure, publicProcedure } from "../../trpc";
|
||||
|
||||
const logger = createLogger({ module: "searchEngineRouter" });
|
||||
@@ -148,20 +145,6 @@ export const searchEngineRouter = createTRPCRouter({
|
||||
limit: input.limit,
|
||||
});
|
||||
}),
|
||||
getMediaRequestOptions: protectedProcedure
|
||||
.concat(createOneIntegrationMiddleware("query", "jellyseerr", "overseerr"))
|
||||
.input(mediaRequestOptionsSchema)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const integration = await createIntegrationAsync(ctx.integration);
|
||||
return await integration.getSeriesInformationAsync(input.mediaType, input.mediaId);
|
||||
}),
|
||||
requestMedia: protectedProcedure
|
||||
.concat(createOneIntegrationMiddleware("interact", "jellyseerr", "overseerr"))
|
||||
.input(mediaRequestRequestSchema)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const integration = await createIntegrationAsync(ctx.integration);
|
||||
return await integration.requestMediaAsync(input.mediaType, input.mediaId, input.seasons);
|
||||
}),
|
||||
create: permissionRequiredProcedure
|
||||
.requiresPermission("search-engine-create")
|
||||
.input(searchEngineManageSchema)
|
||||
|
||||
@@ -12,12 +12,14 @@ interface SeriesInformation {
|
||||
overview: string;
|
||||
seasons: SerieSeason[];
|
||||
posterPath: string;
|
||||
requestedSeasons?: number[];
|
||||
}
|
||||
|
||||
interface MovieInformation {
|
||||
id: number;
|
||||
overview: string;
|
||||
posterPath: string;
|
||||
requestedSeasons?: number[];
|
||||
}
|
||||
|
||||
export type MediaInformation = SeriesInformation | MovieInformation;
|
||||
|
||||
@@ -60,6 +60,9 @@ export class OverseerrIntegration
|
||||
text: "overview" in result ? result.overview : undefined,
|
||||
type: result.mediaType,
|
||||
inLibrary: result.mediaInfo !== undefined,
|
||||
availability: result.mediaInfo
|
||||
? this.mapAvailability(result.mediaInfo.status as UpstreamMediaAvailability, false)
|
||||
: undefined,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -70,7 +73,12 @@ export class OverseerrIntegration
|
||||
"X-Api-Key": this.getSecretValue("apiKey"),
|
||||
},
|
||||
});
|
||||
return await mediaInformationSchema.parseAsync(await response.json());
|
||||
const data = await mediaInformationSchema.parseAsync(await response.json());
|
||||
const requestedSeasons = [
|
||||
...new Set(data.mediaInfo?.requests?.flatMap((req) => req.seasons.map((s) => s.seasonNumber)) ?? []),
|
||||
];
|
||||
const { mediaInfo: _strip, ...rest } = data;
|
||||
return { ...rest, requestedSeasons };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -341,6 +349,22 @@ interface MovieInformation {
|
||||
releaseDate: string;
|
||||
}
|
||||
|
||||
const mediaInfoRequestsSchema = z
|
||||
.object({
|
||||
requests: z
|
||||
.array(
|
||||
z.object({
|
||||
seasons: z.array(
|
||||
z.object({
|
||||
seasonNumber: z.number(),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
})
|
||||
.optional();
|
||||
|
||||
const mediaInformationSchema = z.union([
|
||||
z.object({
|
||||
id: z.number(),
|
||||
@@ -355,14 +379,22 @@ const mediaInformationSchema = z.union([
|
||||
),
|
||||
numberOfSeasons: z.number(),
|
||||
posterPath: z.string().startsWith("/"),
|
||||
mediaInfo: mediaInfoRequestsSchema,
|
||||
}),
|
||||
z.object({
|
||||
id: z.number(),
|
||||
overview: z.string(),
|
||||
posterPath: z.string().startsWith("/"),
|
||||
mediaInfo: mediaInfoRequestsSchema,
|
||||
}),
|
||||
]);
|
||||
|
||||
const searchMediaInfoSchema = z
|
||||
.object({
|
||||
status: z.number(),
|
||||
})
|
||||
.optional();
|
||||
|
||||
const searchSchema = z.object({
|
||||
results: z
|
||||
.array(
|
||||
@@ -373,7 +405,7 @@ const searchSchema = z.object({
|
||||
name: z.string(),
|
||||
posterPath: z.string().startsWith("/").endsWith(".jpg").nullable(),
|
||||
overview: z.string(),
|
||||
mediaInfo: z.object({}).optional(),
|
||||
mediaInfo: searchMediaInfoSchema,
|
||||
}),
|
||||
z.object({
|
||||
id: z.number(),
|
||||
@@ -381,14 +413,14 @@ const searchSchema = z.object({
|
||||
title: z.string(),
|
||||
posterPath: z.string().startsWith("/").endsWith(".jpg").nullable(),
|
||||
overview: z.string(),
|
||||
mediaInfo: z.object({}).optional(),
|
||||
mediaInfo: searchMediaInfoSchema,
|
||||
}),
|
||||
z.object({
|
||||
id: z.number(),
|
||||
mediaType: z.literal("person"),
|
||||
name: z.string(),
|
||||
profilePath: z.string().startsWith("/").endsWith(".jpg").nullable(),
|
||||
mediaInfo: z.object({}).optional(),
|
||||
mediaInfo: searchMediaInfoSchema,
|
||||
}),
|
||||
]),
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useMemo } from "react";
|
||||
import { Button, Group, Image, LoadingOverlay, Stack, Text } from "@mantine/core";
|
||||
import type { MRT_ColumnDef } from "mantine-react-table";
|
||||
import { Badge, Button, Group, Image, LoadingOverlay, Stack, Text } from "@mantine/core";
|
||||
import type { MRT_ColumnDef, MRT_Row } from "mantine-react-table";
|
||||
import { MRT_Table } from "mantine-react-table";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
@@ -16,13 +16,13 @@ interface RequestMediaModalProps {
|
||||
}
|
||||
|
||||
export const RequestMediaModal = createModal<RequestMediaModalProps>(({ actions, innerProps }) => {
|
||||
const { data, isPending: isPendingQuery } = clientApi.searchEngine.getMediaRequestOptions.useQuery({
|
||||
const { data, isPending: isPendingQuery } = clientApi.integration.getMediaRequestOptions.useQuery({
|
||||
integrationId: innerProps.integrationId,
|
||||
mediaId: innerProps.mediaId,
|
||||
mediaType: innerProps.mediaType,
|
||||
});
|
||||
|
||||
const { mutate, isPending: isPendingMutation } = clientApi.searchEngine.requestMedia.useMutation({
|
||||
const { mutate, isPending: isPendingMutation } = clientApi.integration.requestMedia.useMutation({
|
||||
onSuccess() {
|
||||
actions.closeModal();
|
||||
showSuccessNotification({
|
||||
@@ -33,6 +33,7 @@ export const RequestMediaModal = createModal<RequestMediaModalProps>(({ actions,
|
||||
|
||||
const isPending = isPendingQuery || isPendingMutation;
|
||||
const t = useI18n();
|
||||
const requestedSeasons = new Set(data?.requestedSeasons ?? []);
|
||||
|
||||
const columns = useMemo<MRT_ColumnDef<Season>[]>(
|
||||
() => [
|
||||
@@ -44,8 +45,18 @@ export const RequestMediaModal = createModal<RequestMediaModalProps>(({ actions,
|
||||
accessorKey: "episodeCount",
|
||||
header: t("search.engine.media.request.modal.table.header.episodes"),
|
||||
},
|
||||
{
|
||||
id: "status",
|
||||
header: t("search.engine.media.request.modal.table.header.status"),
|
||||
Cell: ({ row }) =>
|
||||
requestedSeasons.has(row.original.seasonNumber) ? (
|
||||
<Badge size="xs" color="violet" variant="light">
|
||||
{t("search.engine.media.request.modal.table.status.requested")}
|
||||
</Badge>
|
||||
) : null,
|
||||
},
|
||||
],
|
||||
[],
|
||||
[data?.requestedSeasons],
|
||||
);
|
||||
|
||||
const table = useTranslatedMantineReactTable({
|
||||
@@ -56,7 +67,7 @@ export const RequestMediaModal = createModal<RequestMediaModalProps>(({ actions,
|
||||
enablePagination: false,
|
||||
enableSorting: false,
|
||||
enableSelectAll: true,
|
||||
enableRowSelection: true,
|
||||
enableRowSelection: (row: MRT_Row<Season>) => !requestedSeasons.has(row.original.seasonNumber),
|
||||
mantineTableProps: {
|
||||
highlightOnHover: false,
|
||||
striped: "odd",
|
||||
|
||||
@@ -131,7 +131,9 @@ export const mediaRequestsChildrenOptions = createChildrenOptions<MediaRequestCh
|
||||
return [
|
||||
{
|
||||
key: "request",
|
||||
hide: (option) => option.result.inLibrary || option.integration.permissions?.hasInteractAccess === false,
|
||||
hide: (option) =>
|
||||
(option.result.type === "movie" && option.result.inLibrary) ||
|
||||
option.integration.permissions?.hasInteractAccess === false,
|
||||
Component(option) {
|
||||
const t = useScopedI18n("search.mode.media");
|
||||
return (
|
||||
|
||||
@@ -19,6 +19,7 @@ import type { SearchMode } from "../../lib/mode";
|
||||
import { appIntegrationBoardMode } from "../app-integration-board";
|
||||
import { commandMode } from "../command";
|
||||
import { externalMode } from "../external";
|
||||
import { mediaMode } from "../media";
|
||||
import { pageMode } from "../page";
|
||||
import { userGroupMode } from "../user-group";
|
||||
|
||||
@@ -33,7 +34,7 @@ type QuickLinkOption = {
|
||||
export const useHomeEmptyGroups = () => {
|
||||
const { data: session } = useSession();
|
||||
const tPages = useScopedI18n("search.mode.page.group.page.option");
|
||||
const visibleSearchModes: SearchMode[] = [appIntegrationBoardMode, externalMode, commandMode, pageMode];
|
||||
const visibleSearchModes: SearchMode[] = [appIntegrationBoardMode, externalMode, mediaMode, commandMode, pageMode];
|
||||
|
||||
if (session?.user.permissions.includes("admin")) {
|
||||
visibleSearchModes.unshift(userGroupMode);
|
||||
@@ -87,7 +88,7 @@ export const useHomeEmptyGroups = () => {
|
||||
useInteraction: interaction.link(({ path }) => ({ href: path })),
|
||||
}),
|
||||
createGroup({
|
||||
keyPath: "character",
|
||||
keyPath: "modeKey",
|
||||
title: (t) => t("search.mode.help.group.mode.title"),
|
||||
options: visibleSearchModes.map(({ character, modeKey }) => ({ character, modeKey })),
|
||||
Component: ({ modeKey, character }) => {
|
||||
@@ -96,7 +97,7 @@ export const useHomeEmptyGroups = () => {
|
||||
return (
|
||||
<Group px="md" py="xs" w="100%" wrap="nowrap" align="center" justify="space-between">
|
||||
<Text>{t("help")}</Text>
|
||||
<Kbd size="sm">{character}</Kbd>
|
||||
{character && <Kbd size="sm">{character}</Kbd>}
|
||||
</Group>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Box, Group, Stack, Text } from "@mantine/core";
|
||||
import { IconMovie, IconSearch, IconSearchOff } from "@tabler/icons-react";
|
||||
import { IconSearch, IconSearchOff } from "@tabler/icons-react";
|
||||
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
@@ -64,10 +64,6 @@ export const homeSearchEngineGroup = createGroup<GroupItem>({
|
||||
clientApi.searchEngine.getDefaultSearchEngine.useQuery(undefined, {
|
||||
enabled: status !== "loading",
|
||||
});
|
||||
const { data: mediaRequestSearchTargets, ...mediaRequestSearchTargetsQuery } =
|
||||
clientApi.integration.mediaRequestSearchTargets.useQuery(undefined, {
|
||||
enabled: status !== "loading" && session?.user !== undefined,
|
||||
});
|
||||
const fromIntegrationEnabled = defaultSearchEngine?.type === "fromIntegration" && query.length > 0;
|
||||
const { data: results, ...resultQuery } = clientApi.integration.searchInIntegration.useQuery(
|
||||
{
|
||||
@@ -82,15 +78,9 @@ export const homeSearchEngineGroup = createGroup<GroupItem>({
|
||||
|
||||
return {
|
||||
isLoading:
|
||||
defaultSearchEngineQuery.isLoading ||
|
||||
mediaRequestSearchTargetsQuery.isLoading ||
|
||||
(resultQuery.isLoading && fromIntegrationEnabled) ||
|
||||
status === "loading",
|
||||
isError:
|
||||
defaultSearchEngineQuery.isError ||
|
||||
mediaRequestSearchTargetsQuery.isError ||
|
||||
(resultQuery.isError && fromIntegrationEnabled),
|
||||
data: createDefaultSearchEntries(defaultSearchEngine, results, mediaRequestSearchTargets, session, query, t),
|
||||
defaultSearchEngineQuery.isLoading || (resultQuery.isLoading && fromIntegrationEnabled) || status === "loading",
|
||||
isError: defaultSearchEngineQuery.isError || (resultQuery.isError && fromIntegrationEnabled),
|
||||
data: createDefaultSearchEntries(defaultSearchEngine, results, session, query, t),
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -98,40 +88,14 @@ export const homeSearchEngineGroup = createGroup<GroupItem>({
|
||||
const createDefaultSearchEntries = (
|
||||
defaultSearchEngine: RouterOutputs["searchEngine"]["getDefaultSearchEngine"] | null,
|
||||
results: RouterOutputs["integration"]["searchInIntegration"] | undefined,
|
||||
mediaRequestSearchTargets: RouterOutputs["integration"]["mediaRequestSearchTargets"] | undefined,
|
||||
session: Session | null,
|
||||
query: string,
|
||||
t: TranslationFunction,
|
||||
): GroupItem[] => {
|
||||
const mediaRequestSearchEntry: GroupItem = {
|
||||
id: "media-request-search",
|
||||
name: t("search.mode.media.action.search.label"),
|
||||
description:
|
||||
mediaRequestSearchTargets && mediaRequestSearchTargets.length === 0
|
||||
? t("search.mode.media.action.search.disabled.noIntegration")
|
||||
: t("search.mode.media.action.search.description"),
|
||||
icon: IconMovie,
|
||||
disabled: !mediaRequestSearchTargets || mediaRequestSearchTargets.length === 0,
|
||||
useInteraction(query) {
|
||||
if (!mediaRequestSearchTargets || mediaRequestSearchTargets.length === 0) {
|
||||
return {
|
||||
type: "none",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
type: "mode",
|
||||
mode: "media",
|
||||
query,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
if (!session?.user && !defaultSearchEngine) return [mediaRequestSearchEntry];
|
||||
if (!session?.user && !defaultSearchEngine) return [];
|
||||
|
||||
if (!defaultSearchEngine) {
|
||||
return [
|
||||
mediaRequestSearchEntry,
|
||||
{
|
||||
id: "no-default",
|
||||
name: t("search.mode.home.group.search.option.no-default.label"),
|
||||
@@ -150,7 +114,6 @@ const createDefaultSearchEntries = (
|
||||
|
||||
if (defaultSearchEngine.type === "generic") {
|
||||
return [
|
||||
mediaRequestSearchEntry,
|
||||
{
|
||||
id: "search",
|
||||
name: t("search.mode.home.group.search.option.search.label", {
|
||||
@@ -173,7 +136,6 @@ const createDefaultSearchEntries = (
|
||||
|
||||
if (!results) {
|
||||
return [
|
||||
mediaRequestSearchEntry,
|
||||
{
|
||||
id: "from-integration",
|
||||
name: defaultSearchEngine.name,
|
||||
@@ -188,16 +150,13 @@ const createDefaultSearchEntries = (
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
mediaRequestSearchEntry,
|
||||
...results.map((result) => ({
|
||||
id: `search-${result.id}`,
|
||||
name: result.name,
|
||||
description: result.text,
|
||||
icon: result.image ?? IconSearch,
|
||||
useInteraction() {
|
||||
return useFromIntegrationSearchInteraction(defaultSearchEngine, result);
|
||||
},
|
||||
})),
|
||||
];
|
||||
return results.map((result) => ({
|
||||
id: `search-${result.id}`,
|
||||
name: result.name,
|
||||
description: result.text,
|
||||
icon: result.image ?? IconSearch,
|
||||
useInteraction() {
|
||||
return useFromIntegrationSearchInteraction(defaultSearchEngine, result);
|
||||
},
|
||||
}));
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Group, Image, Stack, Text } from "@mantine/core";
|
||||
import { Badge, Group, Image, Stack, Text } from "@mantine/core";
|
||||
import { useDebouncedValue } from "@mantine/hooks";
|
||||
import { IconMovie, IconSearch } from "@tabler/icons-react";
|
||||
import { keepPreviousData } from "@tanstack/react-query";
|
||||
@@ -7,12 +7,17 @@ import { useAtomValue } from "jotai";
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { getIntegrationName } from "@homarr/definitions";
|
||||
import type { MediaAvailability } from "@homarr/integrations/types";
|
||||
import { mediaAvailabilityConfiguration } from "@homarr/integrations/types";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
import { createGroup } from "../../lib/group";
|
||||
import { mediaRequestSearchScopeAtom } from "../../spotlight-store";
|
||||
import { useMediaRequestSearchInteraction } from "../external/search-engines-search-group";
|
||||
|
||||
const availabilityLabelKeys = ["available", "partiallyAvailable", "processing", "requested", "pending"] as const;
|
||||
type AvailabilityWithLabel = (typeof availabilityLabelKeys)[number];
|
||||
|
||||
type MediaRequestSearchResult = RouterOutputs["integration"]["searchMediaRequests"][number];
|
||||
|
||||
type MediaRequestSearchOption =
|
||||
@@ -32,6 +37,8 @@ export const mediaRequestSearchGroup = createGroup<MediaRequestSearchOption>({
|
||||
keyPath: "key",
|
||||
title: (t) => t("search.mode.media.group.title"),
|
||||
Component(option) {
|
||||
const tMedia = useScopedI18n("search.mode.media");
|
||||
|
||||
if (option.kind !== "result") {
|
||||
return (
|
||||
<Group w="100%" wrap="nowrap" align="center" px="md" py="xs" opacity={option.kind === "disabled" ? 0.55 : 1}>
|
||||
@@ -47,6 +54,15 @@ export const mediaRequestSearchGroup = createGroup<MediaRequestSearchOption>({
|
||||
}
|
||||
|
||||
const { result } = option;
|
||||
const hasAvailabilityLabel =
|
||||
result.availability && (availabilityLabelKeys as readonly string[]).includes(result.availability);
|
||||
const badgeLabel = hasAvailabilityLabel
|
||||
? tMedia(`availability.${result.availability as AvailabilityWithLabel}`)
|
||||
: undefined;
|
||||
const badgeColor = result.availability
|
||||
? mediaAvailabilityConfiguration[result.availability as MediaAvailability]?.color
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<Group w="100%" wrap="nowrap" align="center" px="md" py="xs">
|
||||
{result.image ? (
|
||||
@@ -54,8 +70,15 @@ export const mediaRequestSearchGroup = createGroup<MediaRequestSearchOption>({
|
||||
) : (
|
||||
<IconSearch stroke={1.5} size={35} />
|
||||
)}
|
||||
<Stack gap={2}>
|
||||
<Text>{result.name}</Text>
|
||||
<Stack gap={2} style={{ flex: 1 }}>
|
||||
<Group gap="xs" wrap="nowrap">
|
||||
<Text>{result.name}</Text>
|
||||
{badgeLabel && (
|
||||
<Badge size="xs" color={badgeColor} variant="light">
|
||||
{badgeLabel}
|
||||
</Badge>
|
||||
)}
|
||||
</Group>
|
||||
<Text c="dimmed" size="sm" lineClamp={2}>
|
||||
{result.text ?? getIntegrationName(result.integration.kind)}
|
||||
</Text>
|
||||
|
||||
@@ -4551,6 +4551,7 @@
|
||||
}
|
||||
},
|
||||
"media": {
|
||||
"help": "Search for movies or shows to request",
|
||||
"requestMovie": "",
|
||||
"requestSeries": "",
|
||||
"openIn": "",
|
||||
@@ -4842,7 +4843,11 @@
|
||||
"table": {
|
||||
"header": {
|
||||
"season": "Season",
|
||||
"episodes": "Episodes"
|
||||
"episodes": "Episodes",
|
||||
"status": "Status"
|
||||
},
|
||||
"status": {
|
||||
"requested": "Requested"
|
||||
}
|
||||
},
|
||||
"button": {
|
||||
|
||||
@@ -4551,6 +4551,7 @@
|
||||
}
|
||||
},
|
||||
"media": {
|
||||
"help": "Search for movies or shows to request",
|
||||
"requestMovie": "Request movie",
|
||||
"requestSeries": "Request series",
|
||||
"openIn": "Open in {kind}",
|
||||
@@ -4566,6 +4567,13 @@
|
||||
"scoped": "This widget does not have an accessible media request integration."
|
||||
}
|
||||
}
|
||||
},
|
||||
"availability": {
|
||||
"available": "Available",
|
||||
"partiallyAvailable": "Partial",
|
||||
"processing": "Processing",
|
||||
"requested": "Requested",
|
||||
"pending": "Requested"
|
||||
}
|
||||
},
|
||||
"external": {
|
||||
@@ -4842,7 +4850,11 @@
|
||||
"table": {
|
||||
"header": {
|
||||
"season": "Season",
|
||||
"episodes": "Episodes"
|
||||
"episodes": "Episodes",
|
||||
"status": "Status"
|
||||
},
|
||||
"status": {
|
||||
"requested": "Requested"
|
||||
}
|
||||
},
|
||||
"button": {
|
||||
|
||||
@@ -58,7 +58,7 @@ export default function MediaServerWidget({
|
||||
const newData = filteredData.concat(
|
||||
data.requests.map((request) => ({ ...request, integrationId: data.integrationId })),
|
||||
);
|
||||
return newData.sort((dataA, dataB) => {
|
||||
return newData.toSorted((dataA, dataB) => {
|
||||
if (dataA.status === dataB.status) {
|
||||
return dataB.createdAt.getTime() - dataA.createdAt.getTime();
|
||||
}
|
||||
|
||||
@@ -120,13 +120,7 @@ export default function MediaServerWidget({
|
||||
>
|
||||
<Tooltip label={t(`titles.stats.${stat.name}`)}>
|
||||
<Card p={0} radius={board.itemRadius} className={classes.card}>
|
||||
<Group
|
||||
className="mediaRequests-stats-stat-stack"
|
||||
justify="center"
|
||||
align="center"
|
||||
gap="xs"
|
||||
w="100%"
|
||||
>
|
||||
<Group className="mediaRequests-stats-stat-stack" justify="center" align="center" gap="xs" w="100%">
|
||||
<stat.icon className="mediaRequests-stats-stat-icon" size={16} />
|
||||
<Text className="mediaRequests-stats-stat-value" size="md">
|
||||
{stat.number}
|
||||
@@ -142,13 +136,7 @@ export default function MediaServerWidget({
|
||||
<Text className="mediaRequests-stats-users-title" fw="bold" ta="center" size={isTiny ? "xs" : "sm"}>
|
||||
{t("titles.users.main")} ({t("titles.users.requests")})
|
||||
</Text>
|
||||
<Stack
|
||||
className="mediaRequests-stats-users-wrapper"
|
||||
flex={1}
|
||||
w="100%"
|
||||
gap={4}
|
||||
style={{ overflow: "hidden" }}
|
||||
>
|
||||
<Stack className="mediaRequests-stats-users-wrapper" flex={1} w="100%" gap={4} style={{ overflow: "hidden" }}>
|
||||
{requestStats.users.slice(0, 10).map((user) => (
|
||||
<Card
|
||||
component="a"
|
||||
@@ -164,13 +152,7 @@ export default function MediaServerWidget({
|
||||
p="xs"
|
||||
radius={board.itemRadius}
|
||||
>
|
||||
<Group
|
||||
className="mediaRequests-stats-users-user-group"
|
||||
h="100%"
|
||||
p={0}
|
||||
gap="sm"
|
||||
justify="space-between"
|
||||
>
|
||||
<Group className="mediaRequests-stats-users-user-group" h="100%" p={0} gap="sm" justify="space-between">
|
||||
<Group gap={4}>
|
||||
<Tooltip label={user.integration.name}>
|
||||
<Avatar
|
||||
|
||||
Reference in new issue
Block a user