fix(media-requests): incorrect availability mapping (#4520)
This commit is contained in:
@@ -3,6 +3,7 @@ import { z } from "zod/v4";
|
|||||||
|
|
||||||
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
||||||
import { createIntegrationAsync } from "@homarr/integrations";
|
import { createIntegrationAsync } from "@homarr/integrations";
|
||||||
|
import { mediaRequestStatusConfiguration } from "@homarr/integrations/types";
|
||||||
import type { MediaRequest } from "@homarr/integrations/types";
|
import type { MediaRequest } from "@homarr/integrations/types";
|
||||||
import { mediaRequestListRequestHandler } from "@homarr/request-handler/media-request-list";
|
import { mediaRequestListRequestHandler } from "@homarr/request-handler/media-request-list";
|
||||||
import { mediaRequestStatsRequestHandler } from "@homarr/request-handler/media-request-stats";
|
import { mediaRequestStatsRequestHandler } from "@homarr/request-handler/media-request-stats";
|
||||||
@@ -35,7 +36,10 @@ export const mediaRequestsRouter = createTRPCRouter({
|
|||||||
return dataB.createdAt.getTime() - dataA.createdAt.getTime();
|
return dataB.createdAt.getTime() - dataA.createdAt.getTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
return dataA.status - dataB.status;
|
return (
|
||||||
|
mediaRequestStatusConfiguration[dataA.status].position -
|
||||||
|
mediaRequestStatusConfiguration[dataB.status].position
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
subscribeToLatestRequests: publicProcedure
|
subscribeToLatestRequests: publicProcedure
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export type {
|
|||||||
FirewallMemorySummary,
|
FirewallMemorySummary,
|
||||||
} from "./interfaces/firewall-summary/firewall-summary-types";
|
} from "./interfaces/firewall-summary/firewall-summary-types";
|
||||||
export type { SystemHealthMonitoring } from "./interfaces/health-monitoring/health-monitoring-types";
|
export type { SystemHealthMonitoring } from "./interfaces/health-monitoring/health-monitoring-types";
|
||||||
export { MediaRequestStatus } from "./interfaces/media-requests/media-request-types";
|
export { UpstreamMediaRequestStatus } from "./interfaces/media-requests/media-request-types";
|
||||||
export type { MediaRequestList, MediaRequestStats } from "./interfaces/media-requests/media-request-types";
|
export type { MediaRequestList, MediaRequestStats } from "./interfaces/media-requests/media-request-types";
|
||||||
export type { StreamSession } from "./interfaces/media-server/media-server-types";
|
export type { StreamSession } from "./interfaces/media-server/media-server-types";
|
||||||
export type {
|
export type {
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { objectKeys } from "@homarr/common";
|
||||||
|
|
||||||
interface SerieSeason {
|
interface SerieSeason {
|
||||||
id: number;
|
id: number;
|
||||||
seasonNumber: number;
|
seasonNumber: number;
|
||||||
@@ -34,6 +36,64 @@ export interface MediaRequest {
|
|||||||
requestedBy?: Omit<RequestUser, "requestCount">;
|
requestedBy?: Omit<RequestUser, "requestCount">;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const mediaAvailabilityConfiguration = {
|
||||||
|
available: {
|
||||||
|
color: "green",
|
||||||
|
},
|
||||||
|
partiallyAvailable: {
|
||||||
|
color: "yellow",
|
||||||
|
},
|
||||||
|
processing: {
|
||||||
|
color: "blue",
|
||||||
|
},
|
||||||
|
requested: {
|
||||||
|
color: "violet",
|
||||||
|
},
|
||||||
|
pending: {
|
||||||
|
color: "violet",
|
||||||
|
},
|
||||||
|
unknown: {
|
||||||
|
color: "orange",
|
||||||
|
},
|
||||||
|
deleted: {
|
||||||
|
color: "red",
|
||||||
|
},
|
||||||
|
blacklisted: {
|
||||||
|
color: "gray",
|
||||||
|
},
|
||||||
|
} satisfies Record<string, { color: string }>;
|
||||||
|
|
||||||
|
export const mediaAvailabilities = objectKeys(mediaAvailabilityConfiguration);
|
||||||
|
|
||||||
|
export type MediaAvailability = (typeof mediaAvailabilities)[number];
|
||||||
|
|
||||||
|
export const mediaRequestStatusConfiguration = {
|
||||||
|
pending: {
|
||||||
|
color: "blue",
|
||||||
|
position: 1,
|
||||||
|
},
|
||||||
|
approved: {
|
||||||
|
color: "green",
|
||||||
|
position: 2,
|
||||||
|
},
|
||||||
|
declined: {
|
||||||
|
color: "red",
|
||||||
|
position: 3,
|
||||||
|
},
|
||||||
|
failed: {
|
||||||
|
color: "red",
|
||||||
|
position: 4,
|
||||||
|
},
|
||||||
|
completed: {
|
||||||
|
color: "green",
|
||||||
|
position: 5,
|
||||||
|
},
|
||||||
|
} satisfies Record<string, { color: string; position: number }>;
|
||||||
|
|
||||||
|
export const mediaRequestStatuses = objectKeys(mediaRequestStatusConfiguration);
|
||||||
|
|
||||||
|
export type MediaRequestStatus = (typeof mediaRequestStatuses)[number];
|
||||||
|
|
||||||
export interface MediaRequestList {
|
export interface MediaRequestList {
|
||||||
integration: {
|
integration: {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -66,7 +126,7 @@ export interface MediaRequestStats {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// https://github.com/fallenbagel/jellyseerr/blob/0fd03f38480f853e7015ad9229ed98160e37602e/server/constants/media.ts#L1
|
// https://github.com/fallenbagel/jellyseerr/blob/0fd03f38480f853e7015ad9229ed98160e37602e/server/constants/media.ts#L1
|
||||||
export enum MediaRequestStatus {
|
export enum UpstreamMediaRequestStatus {
|
||||||
PendingApproval = 1,
|
PendingApproval = 1,
|
||||||
Approved = 2,
|
Approved = 2,
|
||||||
Declined = 3,
|
Declined = 3,
|
||||||
@@ -75,12 +135,12 @@ export enum MediaRequestStatus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// https://github.com/fallenbagel/jellyseerr/blob/0fd03f38480f853e7015ad9229ed98160e37602e/server/constants/media.ts#L14
|
// https://github.com/fallenbagel/jellyseerr/blob/0fd03f38480f853e7015ad9229ed98160e37602e/server/constants/media.ts#L14
|
||||||
export enum MediaAvailability {
|
export enum UpstreamMediaAvailability {
|
||||||
Unknown = 1,
|
Unknown = 1,
|
||||||
Pending = 2,
|
Pending = 2,
|
||||||
Processing = 3,
|
Processing = 3,
|
||||||
PartiallyAvailable = 4,
|
PartiallyAvailable = 4,
|
||||||
Available = 5,
|
Available = 5,
|
||||||
Blacklisted = 6,
|
JellyseerrBlacklistedOrOverseerrDeleted = 6,
|
||||||
Deleted = 7,
|
JellyseerrDeleted = 7,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
import type { MediaAvailability } from "../interfaces/media-requests/media-request-types";
|
||||||
|
import { UpstreamMediaAvailability } from "../interfaces/media-requests/media-request-types";
|
||||||
import { OverseerrIntegration } from "../overseerr/overseerr-integration";
|
import { OverseerrIntegration } from "../overseerr/overseerr-integration";
|
||||||
|
|
||||||
export class JellyseerrIntegration extends OverseerrIntegration {}
|
export class JellyseerrIntegration extends OverseerrIntegration {
|
||||||
|
protected override mapAvailability(availability: UpstreamMediaAvailability, inProgress: boolean): MediaAvailability {
|
||||||
|
// Availability statuses are not exactly the same between Jellyseerr and Overseerr (Jellyseerr has "blacklisted" additionally (deleted is the same value in overseerr))
|
||||||
|
if (availability === UpstreamMediaAvailability.JellyseerrBlacklistedOrOverseerrDeleted) return "blacklisted";
|
||||||
|
if (availability === UpstreamMediaAvailability.JellyseerrDeleted) return "deleted";
|
||||||
|
return super.mapAvailability(availability, inProgress);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
import { objectEntries } from "@homarr/common";
|
|
||||||
|
|
||||||
import type { IMediaRequestIntegration } from "../../interfaces/media-requests/media-request-integration";
|
import type { IMediaRequestIntegration } from "../../interfaces/media-requests/media-request-integration";
|
||||||
import type { MediaInformation, MediaRequest, RequestStats, RequestUser } from "../../types";
|
import type {
|
||||||
import { MediaAvailability, MediaRequestStatus } from "../../types";
|
MediaAvailability,
|
||||||
|
MediaInformation,
|
||||||
|
MediaRequest,
|
||||||
|
MediaRequestStatus,
|
||||||
|
RequestStats,
|
||||||
|
RequestUser,
|
||||||
|
} from "../../types";
|
||||||
|
import { mediaAvailabilities, mediaRequestStatuses } from "../../types";
|
||||||
|
|
||||||
export class MediaRequestMockService implements IMediaRequestIntegration {
|
export class MediaRequestMockService implements IMediaRequestIntegration {
|
||||||
public async getSeriesInformationAsync(mediaType: "movie" | "tv", id: number): Promise<MediaInformation> {
|
public async getSeriesInformationAsync(mediaType: "movie" | "tv", id: number): Promise<MediaInformation> {
|
||||||
@@ -86,12 +91,10 @@ export class MediaRequestMockService implements IMediaRequestIntegration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static randomAvailability(): MediaAvailability {
|
private static randomAvailability(): MediaAvailability {
|
||||||
const values = objectEntries(MediaAvailability).filter(([key]) => typeof key === "number");
|
return mediaAvailabilities.at(Math.floor(Math.random() * mediaAvailabilities.length)) ?? "unknown";
|
||||||
return values[Math.floor(Math.random() * values.length)]?.[1] ?? MediaAvailability.Available;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static randomStatus(): MediaRequestStatus {
|
private static randomStatus(): MediaRequestStatus {
|
||||||
const values = objectEntries(MediaRequestStatus).filter(([key]) => typeof key === "number");
|
return mediaRequestStatuses.at(Math.floor(Math.random() * mediaRequestStatuses.length)) ?? "pending";
|
||||||
return values[Math.floor(Math.random() * values.length)]?.[1] ?? MediaRequestStatus.PendingApproval;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,8 +9,17 @@ import type { ISearchableIntegration } from "../base/searchable-integration";
|
|||||||
import { TestConnectionError } from "../base/test-connection/test-connection-error";
|
import { TestConnectionError } from "../base/test-connection/test-connection-error";
|
||||||
import type { TestingResult } from "../base/test-connection/test-connection-service";
|
import type { TestingResult } from "../base/test-connection/test-connection-service";
|
||||||
import type { IMediaRequestIntegration } from "../interfaces/media-requests/media-request-integration";
|
import type { IMediaRequestIntegration } from "../interfaces/media-requests/media-request-integration";
|
||||||
import type { MediaRequest, RequestStats, RequestUser } from "../interfaces/media-requests/media-request-types";
|
import type {
|
||||||
import { MediaAvailability, MediaRequestStatus } from "../interfaces/media-requests/media-request-types";
|
MediaAvailability,
|
||||||
|
MediaRequest,
|
||||||
|
MediaRequestStatus,
|
||||||
|
RequestStats,
|
||||||
|
RequestUser,
|
||||||
|
} from "../interfaces/media-requests/media-request-types";
|
||||||
|
import {
|
||||||
|
UpstreamMediaAvailability,
|
||||||
|
UpstreamMediaRequestStatus,
|
||||||
|
} from "../interfaces/media-requests/media-request-types";
|
||||||
|
|
||||||
interface OverseerrSearchResult {
|
interface OverseerrSearchResult {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -128,7 +137,7 @@ export class OverseerrIntegration
|
|||||||
|
|
||||||
if (pendingResults.length > 0 && allResults.length > 0) {
|
if (pendingResults.length > 0 && allResults.length > 0) {
|
||||||
requests = pendingResults.concat(
|
requests = pendingResults.concat(
|
||||||
allResults.filter(({ status }) => status !== MediaRequestStatus.PendingApproval),
|
allResults.filter(({ status }) => status !== UpstreamMediaRequestStatus.PendingApproval),
|
||||||
);
|
);
|
||||||
} else if (pendingResults.length > 0) requests = pendingResults;
|
} else if (pendingResults.length > 0) requests = pendingResults;
|
||||||
else if (allResults.length > 0) requests = allResults;
|
else if (allResults.length > 0) requests = allResults;
|
||||||
@@ -137,11 +146,15 @@ export class OverseerrIntegration
|
|||||||
return await Promise.all(
|
return await Promise.all(
|
||||||
requests.map(async (request): Promise<MediaRequest> => {
|
requests.map(async (request): Promise<MediaRequest> => {
|
||||||
const information = await this.getItemInformationAsync(request.media.tmdbId, request.type);
|
const information = await this.getItemInformationAsync(request.media.tmdbId, request.type);
|
||||||
|
|
||||||
|
// See https://github.com/seerr-team/seerr/blob/af083a3cd5c3e3d5d7917fdf4fdd67fe3f39c46b/src/components/StatusBadge/index.tsx#L40
|
||||||
|
const inProgress = (request.media.downloadStatus ?? []).length >= 1;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: request.id,
|
id: request.id,
|
||||||
name: information.name,
|
name: information.name,
|
||||||
status: request.status,
|
status: this.mapRequestStatus(request.status),
|
||||||
availability: request.media.status,
|
availability: this.mapAvailability(request.media.status, inProgress),
|
||||||
backdropImageUrl: `https://image.tmdb.org/t/p/original/${information.backdropPath}`,
|
backdropImageUrl: `https://image.tmdb.org/t/p/original/${information.backdropPath}`,
|
||||||
posterImagePath: `https://image.tmdb.org/t/p/w600_and_h900_bestv2/${information.posterPath}`,
|
posterImagePath: `https://image.tmdb.org/t/p/w600_and_h900_bestv2/${information.posterPath}`,
|
||||||
href: this.externalUrl(`/${request.type}/${request.media.tmdbId}`).toString(),
|
href: this.externalUrl(`/${request.type}/${request.media.tmdbId}`).toString(),
|
||||||
@@ -161,6 +174,42 @@ export class OverseerrIntegration
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected mapRequestStatus(status: UpstreamMediaRequestStatus): MediaRequestStatus {
|
||||||
|
switch (status) {
|
||||||
|
case UpstreamMediaRequestStatus.PendingApproval:
|
||||||
|
return "pending";
|
||||||
|
case UpstreamMediaRequestStatus.Approved:
|
||||||
|
return "approved";
|
||||||
|
case UpstreamMediaRequestStatus.Declined:
|
||||||
|
return "declined";
|
||||||
|
case UpstreamMediaRequestStatus.Failed:
|
||||||
|
return "failed";
|
||||||
|
case UpstreamMediaRequestStatus.Completed:
|
||||||
|
return "completed";
|
||||||
|
default:
|
||||||
|
return "failed";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// See https://github.com/seerr-team/seerr/blob/af083a3cd5c3e3d5d7917fdf4fdd67fe3f39c46b/src/components/StatusBadge/index.tsx#L153-L387
|
||||||
|
protected mapAvailability(availability: UpstreamMediaAvailability, inProgress: boolean): MediaAvailability {
|
||||||
|
switch (availability) {
|
||||||
|
case UpstreamMediaAvailability.Available:
|
||||||
|
return inProgress ? "processing" : "available";
|
||||||
|
case UpstreamMediaAvailability.PartiallyAvailable:
|
||||||
|
return inProgress ? "processing" : "partiallyAvailable";
|
||||||
|
case UpstreamMediaAvailability.Processing:
|
||||||
|
return inProgress ? "processing" : "requested";
|
||||||
|
case UpstreamMediaAvailability.Pending:
|
||||||
|
return "pending";
|
||||||
|
case UpstreamMediaAvailability.JellyseerrBlacklistedOrOverseerrDeleted:
|
||||||
|
return "deleted";
|
||||||
|
case UpstreamMediaAvailability.Unknown:
|
||||||
|
default:
|
||||||
|
return inProgress ? "processing" : "unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async getStatsAsync(): Promise<RequestStats> {
|
public async getStatsAsync(): Promise<RequestStats> {
|
||||||
const response = await fetchWithTrustedCertificatesAsync(this.url("/api/v1/request/count"), {
|
const response = await fetchWithTrustedCertificatesAsync(this.url("/api/v1/request/count"), {
|
||||||
headers: {
|
headers: {
|
||||||
@@ -339,11 +388,12 @@ const getRequestsSchema = z.object({
|
|||||||
.array(
|
.array(
|
||||||
z.object({
|
z.object({
|
||||||
id: z.number(),
|
id: z.number(),
|
||||||
status: z.nativeEnum(MediaRequestStatus),
|
status: z.enum(UpstreamMediaRequestStatus),
|
||||||
createdAt: z.string().transform((value) => new Date(value)),
|
createdAt: z.string().transform((value) => new Date(value)),
|
||||||
media: z.object({
|
media: z.object({
|
||||||
status: z.nativeEnum(MediaAvailability),
|
status: z.enum(UpstreamMediaAvailability),
|
||||||
tmdbId: z.number(),
|
tmdbId: z.number(),
|
||||||
|
downloadStatus: z.array(z.unknown()).optional(),
|
||||||
}),
|
}),
|
||||||
type: z.enum(["movie", "tv"]),
|
type: z.enum(["movie", "tv"]),
|
||||||
requestedBy: z
|
requestedBy: z
|
||||||
|
|||||||
@@ -2232,6 +2232,7 @@
|
|||||||
"unknown": "Unknown",
|
"unknown": "Unknown",
|
||||||
"pending": "Pending",
|
"pending": "Pending",
|
||||||
"processing": "Processing",
|
"processing": "Processing",
|
||||||
|
"requested": "Requested",
|
||||||
"partiallyAvailable": "Partial",
|
"partiallyAvailable": "Partial",
|
||||||
"available": "Available",
|
"available": "Available",
|
||||||
"blacklisted": "Blacklisted",
|
"blacklisted": "Blacklisted",
|
||||||
|
|||||||
@@ -3,10 +3,11 @@
|
|||||||
import { ActionIcon, Anchor, Avatar, Badge, Card, Group, Image, ScrollArea, Stack, Text, Tooltip } from "@mantine/core";
|
import { ActionIcon, Anchor, Avatar, Badge, Card, Group, Image, ScrollArea, Stack, Text, Tooltip } from "@mantine/core";
|
||||||
import { IconThumbDown, IconThumbUp } from "@tabler/icons-react";
|
import { IconThumbDown, IconThumbUp } from "@tabler/icons-react";
|
||||||
|
|
||||||
|
import type { RouterInputs, RouterOutputs } from "@homarr/api";
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
import { useRequiredBoard } from "@homarr/boards/context";
|
import { useRequiredBoard } from "@homarr/boards/context";
|
||||||
import { MediaAvailability, MediaRequestStatus } from "@homarr/integrations/types";
|
import type { MediaRequestStatus } from "@homarr/integrations/types";
|
||||||
import type { ScopedTranslationFunction } from "@homarr/translation";
|
import { mediaAvailabilityConfiguration, mediaRequestStatusConfiguration } from "@homarr/integrations/types";
|
||||||
import { useScopedI18n } from "@homarr/translation/client";
|
import { useScopedI18n } from "@homarr/translation/client";
|
||||||
|
|
||||||
import type { WidgetComponentProps } from "../../definition";
|
import type { WidgetComponentProps } from "../../definition";
|
||||||
@@ -18,7 +19,6 @@ export default function MediaServerWidget({
|
|||||||
options,
|
options,
|
||||||
width,
|
width,
|
||||||
}: WidgetComponentProps<"mediaRequests-requestList">) {
|
}: WidgetComponentProps<"mediaRequests-requestList">) {
|
||||||
const t = useScopedI18n("widget.mediaRequests-requestList");
|
|
||||||
const [mediaRequests] = clientApi.widget.mediaRequests.getLatestRequests.useSuspenseQuery(
|
const [mediaRequests] = clientApi.widget.mediaRequests.getLatestRequests.useSuspenseQuery(
|
||||||
{
|
{
|
||||||
integrationIds,
|
integrationIds,
|
||||||
@@ -48,20 +48,18 @@ export default function MediaServerWidget({
|
|||||||
return dataB.createdAt.getTime() - dataA.createdAt.getTime();
|
return dataB.createdAt.getTime() - dataA.createdAt.getTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
return dataA.status - dataB.status;
|
return (
|
||||||
|
mediaRequestStatusConfiguration[dataA.status].position -
|
||||||
|
mediaRequestStatusConfiguration[dataB.status].position
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const { mutate: mutateRequestAnswer } = clientApi.widget.mediaRequests.answerRequest.useMutation();
|
|
||||||
const board = useRequiredBoard();
|
|
||||||
|
|
||||||
if (mediaRequests.length === 0) throw new NoIntegrationDataError();
|
if (mediaRequests.length === 0) throw new NoIntegrationDataError();
|
||||||
|
|
||||||
const isTiny = width < 256;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollArea
|
<ScrollArea
|
||||||
className="mediaRequests-list-scrollArea"
|
className="mediaRequests-list-scrollArea"
|
||||||
@@ -70,193 +68,188 @@ export default function MediaServerWidget({
|
|||||||
>
|
>
|
||||||
<Stack className="mediaRequests-list-list" gap="xs" p="sm">
|
<Stack className="mediaRequests-list-list" gap="xs" p="sm">
|
||||||
{mediaRequests.map((mediaRequest) => (
|
{mediaRequests.map((mediaRequest) => (
|
||||||
<Card
|
<MediaRequestCard
|
||||||
className={`mediaRequests-list-item-wrapper mediaRequests-list-item-${mediaRequest.type} mediaRequests-list-item-${mediaRequest.status}`}
|
|
||||||
key={`${mediaRequest.integrationId}-${mediaRequest.id}`}
|
key={`${mediaRequest.integrationId}-${mediaRequest.id}`}
|
||||||
radius={board.itemRadius}
|
request={mediaRequest}
|
||||||
p="xs"
|
isTiny={width <= 256}
|
||||||
withBorder
|
options={options}
|
||||||
>
|
/>
|
||||||
<Image
|
|
||||||
className="mediaRequests-list-item-background"
|
|
||||||
src={mediaRequest.backdropImageUrl}
|
|
||||||
pos="absolute"
|
|
||||||
w="100%"
|
|
||||||
h="100%"
|
|
||||||
opacity={0.2}
|
|
||||||
top={0}
|
|
||||||
left={0}
|
|
||||||
alt=""
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Group
|
|
||||||
className="mediaRequests-list-item-contents"
|
|
||||||
h="100%"
|
|
||||||
style={{ zIndex: 1 }}
|
|
||||||
justify="space-between"
|
|
||||||
wrap="nowrap"
|
|
||||||
gap={0}
|
|
||||||
>
|
|
||||||
<Group className="mediaRequests-list-item-left-side" h="100%" gap="md" wrap="nowrap" flex={1}>
|
|
||||||
{!isTiny && (
|
|
||||||
<Image
|
|
||||||
className="mediaRequests-list-item-poster"
|
|
||||||
src={mediaRequest.posterImagePath}
|
|
||||||
h={40}
|
|
||||||
w="auto"
|
|
||||||
radius={"md"}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Stack gap={0} w="100%">
|
|
||||||
<Group justify="space-between" gap="xs" className="mediaRequests-list-item-top-group">
|
|
||||||
<Group gap="xs">
|
|
||||||
<Text className="mediaRequests-list-item-media-year" size="xs">
|
|
||||||
{mediaRequest.airDate?.getFullYear() ?? t("toBeDetermined")}
|
|
||||||
</Text>
|
|
||||||
{!isTiny && (
|
|
||||||
<Badge
|
|
||||||
className="mediaRequests-list-item-media-status"
|
|
||||||
color={getAvailabilityProperties(mediaRequest.availability, t).color}
|
|
||||||
variant="light"
|
|
||||||
size="xs"
|
|
||||||
>
|
|
||||||
{getAvailabilityProperties(mediaRequest.availability, t).label}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</Group>
|
|
||||||
<Group className="mediaRequests-list-item-request-user" gap={4} wrap="nowrap">
|
|
||||||
<Avatar
|
|
||||||
className="mediaRequests-list-item-request-user-avatar"
|
|
||||||
src={mediaRequest.requestedBy?.avatar}
|
|
||||||
size="xs"
|
|
||||||
/>
|
|
||||||
<Anchor
|
|
||||||
className="mediaRequests-list-item-request-user-name"
|
|
||||||
href={mediaRequest.requestedBy?.link}
|
|
||||||
c="var(--mantine-color-text)"
|
|
||||||
target={options.linksTargetNewTab ? "_blank" : "_self"}
|
|
||||||
fz="xs"
|
|
||||||
lineClamp={1}
|
|
||||||
style={{ wordBreak: "break-all" }}
|
|
||||||
>
|
|
||||||
{(mediaRequest.requestedBy?.displayName ?? "") || "unknown"}
|
|
||||||
</Anchor>
|
|
||||||
</Group>
|
|
||||||
</Group>
|
|
||||||
<Group gap="xs" justify="space-between" className="mediaRequests-list-item-bottom-group">
|
|
||||||
<Anchor
|
|
||||||
className="mediaRequests-list-item-info-second-line mediaRequests-list-item-media-title"
|
|
||||||
href={mediaRequest.href}
|
|
||||||
c="var(--mantine-color-text)"
|
|
||||||
target={options.linksTargetNewTab ? "_blank" : "_self"}
|
|
||||||
fz={isTiny ? "xs" : "sm"}
|
|
||||||
fw={"bold"}
|
|
||||||
title={mediaRequest.name}
|
|
||||||
lineClamp={1}
|
|
||||||
>
|
|
||||||
{mediaRequest.name || "unknown"}
|
|
||||||
</Anchor>
|
|
||||||
{mediaRequest.status === MediaRequestStatus.PendingApproval ? (
|
|
||||||
<Group className="mediaRequests-list-item-pending-buttons" gap="sm">
|
|
||||||
<Tooltip label={t("pending.approve")}>
|
|
||||||
<ActionIcon
|
|
||||||
className="mediaRequests-list-item-pending-button-approve"
|
|
||||||
variant="light"
|
|
||||||
color="green"
|
|
||||||
size="xs"
|
|
||||||
radius="md"
|
|
||||||
onClick={() => {
|
|
||||||
mutateRequestAnswer({
|
|
||||||
integrationId: mediaRequest.integrationId,
|
|
||||||
requestId: mediaRequest.id,
|
|
||||||
answer: "approve",
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<IconThumbUp size={16} />
|
|
||||||
</ActionIcon>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip label={t("pending.decline")}>
|
|
||||||
<ActionIcon
|
|
||||||
className="mediaRequests-list-item-pending-button-decline"
|
|
||||||
variant="light"
|
|
||||||
color="red"
|
|
||||||
size="xs"
|
|
||||||
radius="md"
|
|
||||||
onClick={() => {
|
|
||||||
mutateRequestAnswer({
|
|
||||||
integrationId: mediaRequest.integrationId,
|
|
||||||
requestId: mediaRequest.id,
|
|
||||||
answer: "decline",
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<IconThumbDown size={16} />
|
|
||||||
</ActionIcon>
|
|
||||||
</Tooltip>
|
|
||||||
</Group>
|
|
||||||
) : (
|
|
||||||
<StatusBadge status={mediaRequest.status} />
|
|
||||||
)}
|
|
||||||
</Group>
|
|
||||||
</Stack>
|
|
||||||
</Group>
|
|
||||||
</Group>
|
|
||||||
</Card>
|
|
||||||
))}
|
))}
|
||||||
</Stack>
|
</Stack>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const statusMapping = {
|
interface MediaRequestCardProps {
|
||||||
[MediaRequestStatus.PendingApproval]: { color: "blue", label: (t) => t("pending") },
|
request: RouterOutputs["widget"]["mediaRequests"]["getLatestRequests"][number];
|
||||||
[MediaRequestStatus.Approved]: { color: "green", label: (t) => t("approved") },
|
isTiny: boolean;
|
||||||
[MediaRequestStatus.Declined]: { color: "red", label: (t) => t("declined") },
|
options: WidgetComponentProps<"mediaRequests-requestList">["options"];
|
||||||
[MediaRequestStatus.Failed]: { color: "red", label: (t) => t("failed") },
|
}
|
||||||
[MediaRequestStatus.Completed]: { color: "green", label: (t) => t("completed") },
|
|
||||||
} satisfies Record<
|
const MediaRequestCard = ({ request, isTiny, options }: MediaRequestCardProps) => {
|
||||||
MediaRequestStatus,
|
const board = useRequiredBoard();
|
||||||
{
|
const t = useScopedI18n("widget.mediaRequests-requestList");
|
||||||
color: string;
|
|
||||||
label: (t: ScopedTranslationFunction<"widget.mediaRequests-requestList.status">) => string;
|
return (
|
||||||
}
|
<Card
|
||||||
>;
|
className={`mediaRequests-list-item-wrapper mediaRequests-list-item-${request.type} mediaRequests-list-item-${request.status}`}
|
||||||
|
radius={board.itemRadius}
|
||||||
|
p="xs"
|
||||||
|
withBorder
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
className="mediaRequests-list-item-background"
|
||||||
|
src={request.backdropImageUrl}
|
||||||
|
pos="absolute"
|
||||||
|
w="100%"
|
||||||
|
h="100%"
|
||||||
|
opacity={0.2}
|
||||||
|
top={0}
|
||||||
|
left={0}
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Group
|
||||||
|
className="mediaRequests-list-item-contents"
|
||||||
|
h="100%"
|
||||||
|
style={{ zIndex: 1 }}
|
||||||
|
justify="space-between"
|
||||||
|
wrap="nowrap"
|
||||||
|
gap={0}
|
||||||
|
>
|
||||||
|
<Group className="mediaRequests-list-item-left-side" h="100%" gap="md" wrap="nowrap" flex={1}>
|
||||||
|
{!isTiny && (
|
||||||
|
<Image
|
||||||
|
className="mediaRequests-list-item-poster"
|
||||||
|
src={request.posterImagePath}
|
||||||
|
h={40}
|
||||||
|
w="auto"
|
||||||
|
radius={"md"}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Stack gap={0} w="100%">
|
||||||
|
<Group justify="space-between" gap="xs" className="mediaRequests-list-item-top-group">
|
||||||
|
<Group gap="xs">
|
||||||
|
<Text className="mediaRequests-list-item-media-year" size="xs">
|
||||||
|
{request.airDate?.getFullYear() ?? t("toBeDetermined")}
|
||||||
|
</Text>
|
||||||
|
{!isTiny && (
|
||||||
|
<Badge
|
||||||
|
className="mediaRequests-list-item-media-status"
|
||||||
|
color={mediaAvailabilityConfiguration[request.availability].color}
|
||||||
|
variant="light"
|
||||||
|
size="xs"
|
||||||
|
>
|
||||||
|
{t(`availability.${request.availability}`)}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
<Group className="mediaRequests-list-item-request-user" gap={4} wrap="nowrap">
|
||||||
|
<Avatar
|
||||||
|
className="mediaRequests-list-item-request-user-avatar"
|
||||||
|
src={request.requestedBy?.avatar}
|
||||||
|
size="xs"
|
||||||
|
/>
|
||||||
|
<Anchor
|
||||||
|
className="mediaRequests-list-item-request-user-name"
|
||||||
|
href={request.requestedBy?.link}
|
||||||
|
c="var(--mantine-color-text)"
|
||||||
|
target={options.linksTargetNewTab ? "_blank" : "_self"}
|
||||||
|
fz="xs"
|
||||||
|
lineClamp={1}
|
||||||
|
style={{ wordBreak: "break-all" }}
|
||||||
|
>
|
||||||
|
{(request.requestedBy?.displayName ?? "") || "unknown"}
|
||||||
|
</Anchor>
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
<Group gap="xs" justify="space-between" className="mediaRequests-list-item-bottom-group">
|
||||||
|
<Anchor
|
||||||
|
className="mediaRequests-list-item-info-second-line mediaRequests-list-item-media-title"
|
||||||
|
href={request.href}
|
||||||
|
c="var(--mantine-color-text)"
|
||||||
|
target={options.linksTargetNewTab ? "_blank" : "_self"}
|
||||||
|
fz={isTiny ? "xs" : "sm"}
|
||||||
|
fw={"bold"}
|
||||||
|
title={request.name}
|
||||||
|
lineClamp={1}
|
||||||
|
>
|
||||||
|
{request.name || "unknown"}
|
||||||
|
</Anchor>
|
||||||
|
{request.status === "pending" ? (
|
||||||
|
<DecisionButtons requestId={request.id} integrationId={request.integrationId} />
|
||||||
|
) : (
|
||||||
|
<StatusBadge status={request.status} />
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface DecisionButtonsProps {
|
||||||
|
requestId: number;
|
||||||
|
integrationId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DecisionButtons = ({ requestId, integrationId }: DecisionButtonsProps) => {
|
||||||
|
const { mutate: mutateRequestAnswer } = clientApi.widget.mediaRequests.answerRequest.useMutation();
|
||||||
|
const t = useScopedI18n("widget.mediaRequests-requestList");
|
||||||
|
const handleDecision = (answer: RouterInputs["widget"]["mediaRequests"]["answerRequest"]["answer"]) => {
|
||||||
|
mutateRequestAnswer({
|
||||||
|
integrationId,
|
||||||
|
requestId,
|
||||||
|
answer,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group className="mediaRequests-list-item-pending-buttons" gap="sm">
|
||||||
|
<Tooltip label={t("pending.approve")}>
|
||||||
|
<ActionIcon
|
||||||
|
className="mediaRequests-list-item-pending-button-approve"
|
||||||
|
variant="light"
|
||||||
|
color="green"
|
||||||
|
size="xs"
|
||||||
|
radius="md"
|
||||||
|
onClick={() => {
|
||||||
|
handleDecision("approve");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconThumbUp size={16} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip label={t("pending.decline")}>
|
||||||
|
<ActionIcon
|
||||||
|
className="mediaRequests-list-item-pending-button-decline"
|
||||||
|
variant="light"
|
||||||
|
color="red"
|
||||||
|
size="xs"
|
||||||
|
radius="md"
|
||||||
|
onClick={() => {
|
||||||
|
handleDecision("decline");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconThumbDown size={16} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
interface StatusBadgeProps {
|
interface StatusBadgeProps {
|
||||||
status: MediaRequestStatus;
|
status: MediaRequestStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
const StatusBadge = ({ status }: StatusBadgeProps) => {
|
const StatusBadge = ({ status }: StatusBadgeProps) => {
|
||||||
const { color, label } = statusMapping[status];
|
|
||||||
const tStatus = useScopedI18n("widget.mediaRequests-requestList.status");
|
const tStatus = useScopedI18n("widget.mediaRequests-requestList.status");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Badge size="xs" color={color} variant="light">
|
<Badge size="xs" color={mediaRequestStatusConfiguration[status].color} variant="light">
|
||||||
{label(tStatus)}
|
{tStatus(status)}
|
||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
function getAvailabilityProperties(
|
|
||||||
mediaRequestAvailability: MediaAvailability,
|
|
||||||
t: ScopedTranslationFunction<"widget.mediaRequests-requestList">,
|
|
||||||
) {
|
|
||||||
switch (mediaRequestAvailability) {
|
|
||||||
case MediaAvailability.Available:
|
|
||||||
return { color: "green", label: t("availability.available") };
|
|
||||||
case MediaAvailability.PartiallyAvailable:
|
|
||||||
return { color: "yellow", label: t("availability.partiallyAvailable") };
|
|
||||||
case MediaAvailability.Processing:
|
|
||||||
return { color: "blue", label: t("availability.processing") };
|
|
||||||
case MediaAvailability.Pending:
|
|
||||||
return { color: "violet", label: t("availability.pending") };
|
|
||||||
case MediaAvailability.Blacklisted:
|
|
||||||
return { color: "gray", label: t("availability.blacklisted") };
|
|
||||||
case MediaAvailability.Deleted:
|
|
||||||
return { color: "red", label: t("availability.deleted") };
|
|
||||||
default:
|
|
||||||
return { color: "orange", label: t("availability.unknown") };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user