diff --git a/apps/docs/docs/widgets/media-request-list/index.mdx b/apps/docs/docs/widgets/media-request-list/index.mdx
index 2ed31ea88..9a4599a3d 100644
--- a/apps/docs/docs/widgets/media-request-list/index.mdx
+++ b/apps/docs/docs/widgets/media-request-list/index.mdx
@@ -16,6 +16,7 @@ import TabItem from "@theme/TabItem";
import { mediaRequestListWidget } from ".";
import { jellyseerrIntegration } from "@site/docs/integrations/jellyseerr";
import { overseerrIntegration } from "@site/docs/integrations/overseerr";
+import { seerrIntegration } from "@site/docs/integrations/seerr";
@@ -39,6 +40,9 @@ import mediaRequestList from "./img/list.png";
{
integration: overseerrIntegration,
},
+ {
+ integration: seerrIntegration,
+ },
]}
/>
@@ -46,6 +50,7 @@ import mediaRequestList from "./img/list.png";
- Approve / decline a request by clicking on the thumbs up or down icon.
- Hover over the widget to reveal a **search button** that opens Spotlight in media request search mode, scoped to the widget's integrations.
+- When no requests exist yet, the widget offers the same media search action instead of reporting an integration error.
- A warning indicator identifies unavailable integrations without hiding requests returned by healthy integrations.
diff --git a/apps/docs/docs/widgets/media-request-stats/index.mdx b/apps/docs/docs/widgets/media-request-stats/index.mdx
index 7376bb0e4..7a06bf365 100644
--- a/apps/docs/docs/widgets/media-request-stats/index.mdx
+++ b/apps/docs/docs/widgets/media-request-stats/index.mdx
@@ -16,6 +16,7 @@ import TabItem from "@theme/TabItem";
import { mediaRequestStatsWidget } from ".";
import { jellyseerrIntegration } from "@site/docs/integrations/jellyseerr";
import { overseerrIntegration } from "@site/docs/integrations/overseerr";
+import { seerrIntegration } from "@site/docs/integrations/seerr";
@@ -31,6 +32,9 @@ Provides an overview of media requests, including totals, availability, approval
{
integration: overseerrIntegration,
},
+ {
+ integration: seerrIntegration,
+ },
]}
/>
@@ -43,6 +47,7 @@ import mediaRequestStats from "./img/stats.png";
### Interactions
- Hover over the widget to reveal a **search button** that opens Spotlight in media request search mode, scoped to the widget's integrations.
+- When no requests exist yet, the widget offers media search and explains when statistics will appear.
- A warning indicator identifies unavailable integrations while totals from healthy integrations remain visible.
diff --git a/packages/definitions/src/widget.ts b/packages/definitions/src/widget.ts
index 5279df2e0..f8df10772 100644
--- a/packages/definitions/src/widget.ts
+++ b/packages/definitions/src/widget.ts
@@ -63,13 +63,19 @@ export type WidgetKind = (typeof widgetKinds)[number];
export const widgetDefaultSizes: Partial> = {
airQuality: { width: 2, height: 1 },
+ calendar: { width: 2, height: 2 },
countdown: { width: 2, height: 1 },
+ downloads: { width: 4, height: 2 },
+ indexerManager: { width: 2, height: 3 },
timer: { width: 2, height: 1 },
uptimeKuma: { width: 2, height: 3 },
audioStats: { width: 2, height: 2 },
paperlessNgx: { width: 2, height: 2 },
patchmon: { width: 2, height: 2 },
- mediaMissing: { width: 4, height: 3 },
+ mediaServer: { width: 3, height: 2 },
+ "mediaRequests-requestList": { width: 3, height: 2 },
+ "mediaRequests-requestStats": { width: 2, height: 2 },
+ mediaMissing: { width: 2, height: 2 },
bazarr: { width: 2, height: 2 },
assistant: { width: 10, height: 4 },
};
diff --git a/packages/translation/src/lang/en.json b/packages/translation/src/lang/en.json
index 5ef1cee93..04dabe9f7 100644
--- a/packages/translation/src/lang/en.json
+++ b/packages/translation/src/lang/en.json
@@ -4227,7 +4227,11 @@
},
"mediaRequests-requestList": {
"name": "Media Requests List",
- "description": "See a list of all media requests from your Overseerr or Jellyseerr instance",
+ "description": "See a list of all media requests from your Seerr, Overseerr, or Jellyseerr instance",
+ "empty": {
+ "title": "No media requests yet",
+ "description": "Search for a movie or show to start a request."
+ },
"option": {
"linksTargetNewTab": {
"label": "Open links in new tab"
@@ -4267,6 +4271,10 @@
"mediaRequests-requestStats": {
"name": "Media Requests Stats",
"description": "Statistics about your media requests",
+ "empty": {
+ "title": "No request activity yet",
+ "description": "Stats will appear after the first media request."
+ },
"titles": {
"stats": {
"main": "Media Stats",
diff --git a/packages/widgets/src/media-requests/empty-state.tsx b/packages/widgets/src/media-requests/empty-state.tsx
new file mode 100644
index 000000000..20752a18b
--- /dev/null
+++ b/packages/widgets/src/media-requests/empty-state.tsx
@@ -0,0 +1,49 @@
+"use client";
+
+import { Button, Stack, Text, ThemeIcon } from "@mantine/core";
+import { IconMovieOff, IconSearch } from "@tabler/icons-react";
+
+import { openMediaRequestSearch } from "@homarr/spotlight";
+import { useI18n } from "@homarr/translation/client";
+
+interface MediaRequestsEmptyStateProps {
+ title: string;
+ description: string;
+ integrationIds: string[];
+ isEditMode: boolean;
+}
+
+export const MediaRequestsEmptyState = ({
+ title,
+ description,
+ integrationIds,
+ isEditMode,
+}: MediaRequestsEmptyStateProps) => {
+ const tSearch = useI18n("search.mode.media");
+
+ return (
+
+
+
+
+
+
+ {title}
+
+
+ {description}
+
+
+ {!isEditMode ? (
+ }
+ onClick={() => openMediaRequestSearch({ integrationIds })}
+ >
+ {tSearch("action.search.label")}
+
+ ) : null}
+
+ );
+};
diff --git a/packages/widgets/src/media-requests/list/component.tsx b/packages/widgets/src/media-requests/list/component.tsx
index 617b332be..59d2e14be 100644
--- a/packages/widgets/src/media-requests/list/component.tsx
+++ b/packages/widgets/src/media-requests/list/component.tsx
@@ -20,7 +20,7 @@ import { WidgetQueryLoadingState } from "../../common/query-state-indicator";
import actionTargetClasses from "../../common/action-target.module.css";
import { IntegrationErrorIndicator } from "../../common/integration-error-indicator";
import type { WidgetComponentProps } from "../../definition";
-import { NoIntegrationDataError } from "../../errors/no-data-integration";
+import { MediaRequestsEmptyState } from "../empty-state";
import classes from "./component.module.css";
import searchClasses from "../search-button.module.css";
@@ -30,6 +30,7 @@ export default function MediaServerWidget({
options,
width,
}: WidgetComponentProps<"mediaRequests-requestList">) {
+ const t = useI18n("widget.mediaRequests-requestList");
const interactIntegrationIds = new Set(
useIntegrationsWithInteractAccess()
.filter(({ id }) => integrationIds.includes(id))
@@ -48,7 +49,16 @@ export default function MediaServerWidget({
if (isInitialWidgetQueryPending(mediaRequestQuery)) return ;
if (!mediaRequestData) return ;
const { requests: mediaRequests, failedIntegrations } = mediaRequestData;
- if (mediaRequests.length === 0 && failedIntegrations.length === 0) throw new NoIntegrationDataError();
+ if (mediaRequests.length === 0 && failedIntegrations.length === 0) {
+ return (
+
+ );
+ }
const showIntegrationSource = new Set(mediaRequests.map(({ integrationId }) => integrationId)).size > 1;
return (
diff --git a/packages/widgets/src/media-requests/stats/component.tsx b/packages/widgets/src/media-requests/stats/component.tsx
index 92aaac4c6..074014264 100644
--- a/packages/widgets/src/media-requests/stats/component.tsx
+++ b/packages/widgets/src/media-requests/stats/component.tsx
@@ -28,7 +28,7 @@ import { WidgetQueryLoadingState } from "../../common/query-state-indicator";
import actionTargetClasses from "../../common/action-target.module.css";
import { IntegrationErrorIndicator } from "../../common/integration-error-indicator";
import type { WidgetComponentProps } from "../../definition";
-import { NoIntegrationDataError } from "../../errors/no-data-integration";
+import { MediaRequestsEmptyState } from "../empty-state";
import classes from "./component.module.css";
import searchClasses from "../search-button.module.css";
@@ -54,8 +54,16 @@ export default function MediaServerWidget({
requestStats.users.length === 0 &&
requestStats.stats.length === 0 &&
requestStats.failedIntegrations.length === 0
- )
- throw new NoIntegrationDataError();
+ ) {
+ return (
+
+ );
+ }
const data = [
{