chore(release): automatic release v1.18.0
This commit is contained in:
1
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
1
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -31,6 +31,7 @@ body:
|
||||
label: Version
|
||||
description: What version of Homarr are you running?
|
||||
options:
|
||||
- 1.17.0
|
||||
- 1.16.0
|
||||
- 1.15.0
|
||||
- 1.14.0
|
||||
|
||||
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@@ -26,6 +26,7 @@
|
||||
"Sabnzbd",
|
||||
"SeDemal",
|
||||
"Sonarr",
|
||||
"sslverify",
|
||||
"superjson",
|
||||
"tabler",
|
||||
"trpc",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:22.14.0-alpine AS base
|
||||
FROM node:22.15.0-alpine AS base
|
||||
|
||||
FROM base AS builder
|
||||
RUN apk add --no-cache libc6-compat
|
||||
|
||||
@@ -48,21 +48,21 @@
|
||||
"@homarr/ui": "workspace:^0.1.0",
|
||||
"@homarr/validation": "workspace:^0.1.0",
|
||||
"@homarr/widgets": "workspace:^0.1.0",
|
||||
"@mantine/colors-generator": "^7.17.4",
|
||||
"@mantine/core": "^7.17.4",
|
||||
"@mantine/dropzone": "^7.17.4",
|
||||
"@mantine/hooks": "^7.17.4",
|
||||
"@mantine/modals": "^7.17.4",
|
||||
"@mantine/tiptap": "^7.17.4",
|
||||
"@mantine/colors-generator": "^7.17.5",
|
||||
"@mantine/core": "^7.17.5",
|
||||
"@mantine/dropzone": "^7.17.5",
|
||||
"@mantine/hooks": "^7.17.5",
|
||||
"@mantine/modals": "^7.17.5",
|
||||
"@mantine/tiptap": "^7.17.5",
|
||||
"@million/lint": "1.0.14",
|
||||
"@tabler/icons-react": "^3.31.0",
|
||||
"@tanstack/react-query": "^5.74.4",
|
||||
"@tanstack/react-query-devtools": "^5.74.4",
|
||||
"@tanstack/react-query-next-experimental": "^5.74.4",
|
||||
"@trpc/client": "^11.1.0",
|
||||
"@trpc/next": "^11.1.0",
|
||||
"@trpc/react-query": "^11.1.0",
|
||||
"@trpc/server": "^11.1.0",
|
||||
"@trpc/client": "^11.1.1",
|
||||
"@trpc/next": "^11.1.1",
|
||||
"@trpc/react-query": "^11.1.1",
|
||||
"@trpc/server": "^11.1.1",
|
||||
"@xterm/addon-canvas": "^0.7.0",
|
||||
"@xterm/addon-fit": "0.10.0",
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
@@ -71,7 +71,7 @@
|
||||
"dayjs": "^1.11.13",
|
||||
"dotenv": "^16.5.0",
|
||||
"flag-icons": "^7.3.2",
|
||||
"glob": "^11.0.1",
|
||||
"glob": "^11.0.2",
|
||||
"jotai": "^2.12.3",
|
||||
"mantine-react-table": "2.0.0-beta.9",
|
||||
"next": "15.3.1",
|
||||
@@ -81,7 +81,7 @@
|
||||
"react-dom": "19.1.0",
|
||||
"react-error-boundary": "^5.0.0",
|
||||
"react-simple-code-editor": "^0.14.1",
|
||||
"sass": "^1.86.3",
|
||||
"sass": "^1.87.0",
|
||||
"superjson": "2.2.2",
|
||||
"swagger-ui-react": "^5.21.0",
|
||||
"use-deep-compare-effect": "^1.8.1",
|
||||
@@ -92,13 +92,13 @@
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/chroma-js": "3.1.1",
|
||||
"@types/node": "^22.14.1",
|
||||
"@types/node": "^22.15.2",
|
||||
"@types/prismjs": "^1.26.5",
|
||||
"@types/react": "19.1.2",
|
||||
"@types/react-dom": "19.1.2",
|
||||
"@types/swagger-ui-react": "^5.18.0",
|
||||
"concurrently": "^9.1.2",
|
||||
"eslint": "^9.25.0",
|
||||
"eslint": "^9.25.1",
|
||||
"node-loader": "^2.1.0",
|
||||
"prettier": "^3.5.3",
|
||||
"typescript": "^5.8.3"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import type { PropsWithChildren } from "react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { Anchor, Button, Card, Code, Collapse, Divider, PasswordInput, Stack, Text, TextInput } from "@mantine/core";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
import { z } from "zod";
|
||||
@@ -15,6 +15,8 @@ import { showErrorNotification, showSuccessNotification } from "@homarr/notifica
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
import { userSignInSchema } from "@homarr/validation/user";
|
||||
|
||||
type Provider = "credentials" | "ldap" | "oidc";
|
||||
|
||||
interface LoginFormProps {
|
||||
providers: string[];
|
||||
oidcClientName: string;
|
||||
@@ -26,6 +28,8 @@ const extendedValidation = userSignInSchema.extend({ provider: z.enum(["credenti
|
||||
|
||||
export const LoginForm = ({ providers, oidcClientName, isOidcAutoLoginEnabled, callbackUrl }: LoginFormProps) => {
|
||||
const t = useScopedI18n("user");
|
||||
const searchParams = useSearchParams();
|
||||
const isError = searchParams.has("error");
|
||||
const router = useRouter();
|
||||
const [isPending, setIsPending] = useState(false);
|
||||
const form = useZodForm(extendedValidation, {
|
||||
@@ -39,22 +43,34 @@ export const LoginForm = ({ providers, oidcClientName, isOidcAutoLoginEnabled, c
|
||||
const credentialInputsVisible = providers.includes("credentials") || providers.includes("ldap");
|
||||
|
||||
const onSuccess = useCallback(
|
||||
async (response: Awaited<ReturnType<typeof signIn>>) => {
|
||||
if (response && (!response.ok || response.error)) {
|
||||
async (provider: Provider, response: Awaited<ReturnType<typeof signIn>>) => {
|
||||
if (!response.ok || response.error) {
|
||||
// eslint-disable-next-line @typescript-eslint/only-throw-error
|
||||
throw response.error;
|
||||
}
|
||||
|
||||
if (provider === "oidc") {
|
||||
if (!response.url) {
|
||||
showErrorNotification({
|
||||
title: t("action.login.notification.error.title"),
|
||||
message: t("action.login.notification.error.message"),
|
||||
autoClose: 10000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
router.push(response.url);
|
||||
return;
|
||||
}
|
||||
|
||||
showSuccessNotification({
|
||||
title: t("action.login.notification.success.title"),
|
||||
message: t("action.login.notification.success.message"),
|
||||
});
|
||||
|
||||
// Redirect to the callback URL if the response is defined and comes from a credentials provider (ldap or credentials). oidc is redirected automatically.
|
||||
if (response) {
|
||||
await revalidatePathActionAsync("/");
|
||||
router.push(callbackUrl);
|
||||
}
|
||||
await revalidatePathActionAsync("/");
|
||||
router.push(callbackUrl);
|
||||
},
|
||||
[t, router, callbackUrl],
|
||||
);
|
||||
@@ -70,14 +86,14 @@ export const LoginForm = ({ providers, oidcClientName, isOidcAutoLoginEnabled, c
|
||||
}, [t]);
|
||||
|
||||
const signInAsync = useCallback(
|
||||
async (provider: string, options?: Parameters<typeof signIn>[1]) => {
|
||||
async (provider: Provider, options?: Parameters<typeof signIn>[1]) => {
|
||||
setIsPending(true);
|
||||
await signIn(provider, {
|
||||
...options,
|
||||
redirect: false,
|
||||
callbackUrl: new URL(callbackUrl, window.location.href).href,
|
||||
})
|
||||
.then(onSuccess)
|
||||
.then((response) => onSuccess(provider, response))
|
||||
.catch(onError);
|
||||
},
|
||||
[setIsPending, onSuccess, onError, callbackUrl],
|
||||
@@ -86,11 +102,12 @@ export const LoginForm = ({ providers, oidcClientName, isOidcAutoLoginEnabled, c
|
||||
const isLoginInProgress = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (isError) return;
|
||||
if (isOidcAutoLoginEnabled && !isPending && !isLoginInProgress.current) {
|
||||
isLoginInProgress.current = true;
|
||||
void signInAsync("oidc");
|
||||
}
|
||||
}, [signInAsync, isOidcAutoLoginEnabled, isPending]);
|
||||
}, [signInAsync, isOidcAutoLoginEnabled, isPending, isError]);
|
||||
|
||||
return (
|
||||
<Stack gap="xl">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { X509Certificate } from "node:crypto";
|
||||
import { notFound } from "next/navigation";
|
||||
import { Card, Group, SimpleGrid, Stack, Text, Title } from "@mantine/core";
|
||||
import { IconCertificate, IconCertificateOff } from "@tabler/icons-react";
|
||||
import { IconAlertTriangle, IconCertificate, IconCertificateOff } from "@tabler/icons-react";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import { auth } from "@homarr/auth/next";
|
||||
@@ -31,11 +31,27 @@ export default async function CertificatesPage({ params }: CertificatesPageProps
|
||||
const t = await getI18n();
|
||||
const certificates = await loadCustomRootCertificatesAsync();
|
||||
const x509Certificates = certificates
|
||||
.map((cert) => ({
|
||||
...cert,
|
||||
x509: new X509Certificate(cert.content),
|
||||
}))
|
||||
.sort((certA, certB) => certA.x509.validToDate.getTime() - certB.x509.validToDate.getTime());
|
||||
.map((cert) => {
|
||||
try {
|
||||
const x509 = new X509Certificate(cert.content);
|
||||
return {
|
||||
...cert,
|
||||
isError: false,
|
||||
x509,
|
||||
} as const;
|
||||
} catch {
|
||||
return {
|
||||
...cert,
|
||||
isError: true,
|
||||
x509: null,
|
||||
} as const;
|
||||
}
|
||||
})
|
||||
.sort((certA, certB) => {
|
||||
if (certA.isError) return -1;
|
||||
if (certB.isError) return 1;
|
||||
return certA.x509.validToDate.getTime() - certB.x509.validToDate.getTime();
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -57,32 +73,47 @@ export default async function CertificatesPage({ params }: CertificatesPageProps
|
||||
|
||||
<SimpleGrid cols={{ sm: 1, lg: 2, xl: 3 }} spacing="lg">
|
||||
{x509Certificates.map((cert) => (
|
||||
<Card key={cert.x509.fingerprint} withBorder>
|
||||
<Card key={cert.fileName} withBorder>
|
||||
<Group wrap="nowrap">
|
||||
<IconCertificate
|
||||
color={getMantineColor(iconColor(cert.x509.validToDate), 6)}
|
||||
style={{ minWidth: 32 }}
|
||||
size={32}
|
||||
stroke={1.5}
|
||||
/>
|
||||
{cert.isError ? (
|
||||
<IconAlertTriangle
|
||||
color={getMantineColor("red", 6)}
|
||||
style={{ minWidth: 32 }}
|
||||
size={32}
|
||||
stroke={1.5}
|
||||
/>
|
||||
) : (
|
||||
<IconCertificate
|
||||
color={getMantineColor(iconColor(cert.x509.validToDate), 6)}
|
||||
style={{ minWidth: 32 }}
|
||||
size={32}
|
||||
stroke={1.5}
|
||||
/>
|
||||
)}
|
||||
<Stack flex={1} gap="xs" maw="calc(100% - 48px)">
|
||||
<Group justify="space-between" wrap="nowrap">
|
||||
<Text fw={500} lineClamp={1} style={{ wordBreak: "break-all" }}>
|
||||
{cert.x509.subject}
|
||||
{cert.isError ? t("certificate.page.list.invalid.title") : cert.x509.subject}
|
||||
</Text>
|
||||
<Text c="gray.6" ta="end" size="sm">
|
||||
{cert.fileName}
|
||||
</Text>
|
||||
</Group>
|
||||
<Group justify="space-between">
|
||||
<Text size="sm" c="gray.6" title={cert.x509.validToDate.toISOString()}>
|
||||
{t("certificate.page.list.expires", {
|
||||
when: new Intl.RelativeTimeFormat(locale).format(
|
||||
dayjs(cert.x509.validToDate).diff(dayjs(), "days"),
|
||||
"days",
|
||||
),
|
||||
})}
|
||||
</Text>
|
||||
{cert.isError ? (
|
||||
<Text size="sm" c="gray.6">
|
||||
{t("certificate.page.list.invalid.description")}
|
||||
</Text>
|
||||
) : (
|
||||
<Text size="sm" c="gray.6" title={cert.x509.validToDate.toISOString()}>
|
||||
{t("certificate.page.list.expires", {
|
||||
when: new Intl.RelativeTimeFormat(locale).format(
|
||||
dayjs(cert.x509.validToDate).diff(dayjs(), "days"),
|
||||
"days",
|
||||
),
|
||||
})}
|
||||
</Text>
|
||||
)}
|
||||
<RemoveCertificate fileName={cert.fileName} />
|
||||
</Group>
|
||||
</Stack>
|
||||
|
||||
@@ -48,6 +48,7 @@ export const WidgetPreviewPageContent = ({ kind, integrationData }: WidgetPrevie
|
||||
options: reduceWidgetOptionsWithDefaultValues(kind, settings, {}),
|
||||
integrationIds: [],
|
||||
advancedOptions: {
|
||||
title: null,
|
||||
customCssClasses: [],
|
||||
borderColor: "",
|
||||
},
|
||||
|
||||
@@ -29,6 +29,7 @@ export const createItemCallback =
|
||||
layouts: createItemLayouts(previous, firstSection),
|
||||
integrationIds: [],
|
||||
advancedOptions: {
|
||||
title: null,
|
||||
customCssClasses: [],
|
||||
borderColor: "",
|
||||
},
|
||||
|
||||
@@ -20,7 +20,7 @@ describe("item actions duplicate-item", () => {
|
||||
kind: itemKind,
|
||||
integrationIds: ["1"],
|
||||
options: { address: "localhost" },
|
||||
advancedOptions: { customCssClasses: ["test"], borderColor: "#ff0000" },
|
||||
advancedOptions: { title: "The best one", customCssClasses: ["test"], borderColor: "#ff0000" },
|
||||
})
|
||||
.addLayout({ layoutId, sectionId: currentSectionId, ...currentItemSize })
|
||||
.build();
|
||||
|
||||
@@ -13,6 +13,7 @@ export class ItemMockBuilder {
|
||||
layouts: [],
|
||||
integrationIds: [],
|
||||
advancedOptions: {
|
||||
title: null,
|
||||
customCssClasses: [],
|
||||
borderColor: "",
|
||||
},
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
.badge {
|
||||
@mixin dark {
|
||||
--background-color: rgb(from var(--mantine-color-dark-6) r g b / var(--opacity));
|
||||
--border-color: rgb(from var(--mantine-color-dark-4) r g b / var(--opacity));
|
||||
}
|
||||
@mixin light {
|
||||
--background-color: rgb(from var(--mantine-color-white) r g b / var(--opacity));
|
||||
--border-color: rgb(from var(--mantine-color-gray-3) r g b / var(--opacity));
|
||||
}
|
||||
background-color: var(--background-color) !important;
|
||||
border-color: var(--border-color) !important;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Card } from "@mantine/core";
|
||||
import { Badge, Card } from "@mantine/core";
|
||||
import { useElementSize } from "@mantine/hooks";
|
||||
import { QueryErrorResetBoundary } from "@tanstack/react-query";
|
||||
import combineClasses from "clsx";
|
||||
@@ -14,6 +14,7 @@ import { WidgetError } from "@homarr/widgets/errors";
|
||||
import type { SectionItem } from "~/app/[locale]/boards/_types";
|
||||
import classes from "../sections/item.module.css";
|
||||
import { useItemActions } from "./item-actions";
|
||||
import itemContentClasses from "./item-content.module.css";
|
||||
import { BoardItemMenu } from "./item-menu";
|
||||
|
||||
interface BoardItemContentProps {
|
||||
@@ -25,28 +26,51 @@ export const BoardItemContent = ({ item }: BoardItemContentProps) => {
|
||||
const board = useRequiredBoard();
|
||||
|
||||
return (
|
||||
<Card
|
||||
ref={ref}
|
||||
className={combineClasses(
|
||||
classes.itemCard,
|
||||
`${item.kind}-wrapper`,
|
||||
"grid-stack-item-content",
|
||||
item.advancedOptions.customCssClasses.join(" "),
|
||||
<>
|
||||
<Card
|
||||
ref={ref}
|
||||
className={combineClasses(
|
||||
classes.itemCard,
|
||||
`${item.kind}-wrapper`,
|
||||
"grid-stack-item-content",
|
||||
item.advancedOptions.customCssClasses.join(" "),
|
||||
)}
|
||||
radius={board.itemRadius}
|
||||
withBorder
|
||||
styles={{
|
||||
root: {
|
||||
"--opacity": board.opacity / 100,
|
||||
containerType: "size",
|
||||
overflow: item.kind === "iframe" ? "hidden" : undefined,
|
||||
"--border-color": item.advancedOptions.borderColor !== "" ? item.advancedOptions.borderColor : undefined,
|
||||
},
|
||||
}}
|
||||
p={0}
|
||||
>
|
||||
<InnerContent item={item} width={width} height={height} />
|
||||
</Card>
|
||||
{item.advancedOptions.title?.trim() && (
|
||||
<Badge
|
||||
pos="absolute"
|
||||
// It's 4 because of the mantine-react-table that has z-index 3
|
||||
style={{ zIndex: 4 }}
|
||||
top={2}
|
||||
left={16}
|
||||
size="xs"
|
||||
radius={board.itemRadius}
|
||||
styles={{
|
||||
root: {
|
||||
"--border-color": item.advancedOptions.borderColor !== "" ? item.advancedOptions.borderColor : undefined,
|
||||
"--opacity": board.opacity / 100,
|
||||
},
|
||||
}}
|
||||
className={itemContentClasses.badge}
|
||||
c="var(--mantine-color-text)"
|
||||
>
|
||||
{item.advancedOptions.title}
|
||||
</Badge>
|
||||
)}
|
||||
radius={board.itemRadius}
|
||||
withBorder
|
||||
styles={{
|
||||
root: {
|
||||
"--opacity": board.opacity / 100,
|
||||
containerType: "size",
|
||||
overflow: item.kind === "iframe" ? "hidden" : undefined,
|
||||
"--border-color": item.advancedOptions.borderColor !== "" ? item.advancedOptions.borderColor : undefined,
|
||||
},
|
||||
}}
|
||||
p={0}
|
||||
>
|
||||
<InnerContent item={item} width={width} height={height} />
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"main": "./src/main.ts",
|
||||
"types": "./src/main.ts",
|
||||
"scripts": {
|
||||
"build": "esbuild src/main.ts --bundle --platform=node --loader:.scss=text --external:@opentelemetry/api --outfile=tasks.cjs",
|
||||
"build": "esbuild src/main.ts --bundle --platform=node --loader:.scss=text --external:@opentelemetry/api --external:http-cookie-agent --outfile=tasks.cjs",
|
||||
"clean": "rm -rf .turbo node_modules",
|
||||
"dev": "pnpm with-env tsx ./src/main.ts",
|
||||
"format": "prettier --check . --ignore-path ../../.gitignore",
|
||||
@@ -44,9 +44,9 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/node": "^22.14.1",
|
||||
"@types/node": "^22.15.2",
|
||||
"dotenv-cli": "^8.0.0",
|
||||
"eslint": "^9.25.0",
|
||||
"eslint": "^9.25.1",
|
||||
"prettier": "^3.5.3",
|
||||
"tsx": "4.19.3",
|
||||
"typescript": "^5.8.3"
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"main": "./src/main.ts",
|
||||
"types": "./src/main.ts",
|
||||
"scripts": {
|
||||
"build": "esbuild src/main.ts --bundle --platform=node --outfile=wssServer.cjs --external:bcrypt --external:@opentelemetry/api --external:cpu-features --loader:.html=text --loader:.scss=text --loader:.node=text",
|
||||
"build": "esbuild src/main.ts --bundle --platform=node --outfile=wssServer.cjs --external:bcrypt --external:@opentelemetry/api --external:http-cookie-agent --external:cpu-features --loader:.html=text --loader:.scss=text --loader:.node=text",
|
||||
"clean": "rm -rf .turbo node_modules",
|
||||
"dev": "pnpm with-env tsx ./src/main.ts",
|
||||
"format": "prettier --check . --ignore-path ../../.gitignore",
|
||||
@@ -34,7 +34,7 @@
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/ws": "^8.18.1",
|
||||
"eslint": "^9.25.0",
|
||||
"eslint": "^9.25.1",
|
||||
"prettier": "^3.5.3",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
|
||||
16
package.json
16
package.json
@@ -38,24 +38,24 @@
|
||||
"@semantic-release/github": "^11.0.1",
|
||||
"@semantic-release/npm": "^12.0.1",
|
||||
"@semantic-release/release-notes-generator": "^14.0.3",
|
||||
"@turbo/gen": "^2.5.0",
|
||||
"@vitejs/plugin-react": "^4.4.0",
|
||||
"@vitest/coverage-v8": "^3.1.1",
|
||||
"@vitest/ui": "^3.1.1",
|
||||
"@turbo/gen": "^2.5.2",
|
||||
"@vitejs/plugin-react": "^4.4.1",
|
||||
"@vitest/coverage-v8": "^3.1.2",
|
||||
"@vitest/ui": "^3.1.2",
|
||||
"conventional-changelog-conventionalcommits": "^8.0.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"jsdom": "^26.1.0",
|
||||
"prettier": "^3.5.3",
|
||||
"semantic-release": "^24.2.3",
|
||||
"testcontainers": "^10.24.2",
|
||||
"turbo": "^2.5.0",
|
||||
"turbo": "^2.5.2",
|
||||
"typescript": "^5.8.3",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"vitest": "^3.1.1"
|
||||
"vitest": "^3.1.2"
|
||||
},
|
||||
"packageManager": "pnpm@10.8.1",
|
||||
"packageManager": "pnpm@10.9.0",
|
||||
"engines": {
|
||||
"node": ">=22.14.0"
|
||||
"node": ">=22.15.0"
|
||||
},
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.25.0",
|
||||
"eslint": "^9.25.1",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,23 +41,23 @@
|
||||
"@homarr/server-settings": "workspace:^0.1.0",
|
||||
"@homarr/validation": "workspace:^0.1.0",
|
||||
"@kubernetes/client-node": "^1.1.2",
|
||||
"@trpc/client": "^11.1.0",
|
||||
"@trpc/react-query": "^11.1.0",
|
||||
"@trpc/server": "^11.1.0",
|
||||
"@trpc/client": "^11.1.1",
|
||||
"@trpc/react-query": "^11.1.1",
|
||||
"@trpc/server": "^11.1.1",
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
"next": "15.3.1",
|
||||
"pretty-print-error": "^1.1.2",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"superjson": "2.2.2",
|
||||
"trpc-to-openapi": "^2.1.5",
|
||||
"trpc-to-openapi": "^2.2.0",
|
||||
"zod": "^3.24.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.25.0",
|
||||
"eslint": "^9.25.1",
|
||||
"prettier": "^3.5.3",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { X509Certificate } from "node:crypto";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { z } from "zod";
|
||||
import { zfd } from "zod-form-data";
|
||||
|
||||
@@ -16,6 +18,17 @@ export const certificateRouter = createTRPCRouter({
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
const content = await input.file.text();
|
||||
|
||||
// Validate the certificate
|
||||
try {
|
||||
new X509Certificate(content);
|
||||
} catch {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Invalid certificate",
|
||||
});
|
||||
}
|
||||
|
||||
await addCustomRootCertificateAsync(input.file.name, content);
|
||||
}),
|
||||
removeCertificate: permissionRequiredProcedure
|
||||
|
||||
@@ -12,6 +12,7 @@ import { minecraftRouter } from "./minecraft";
|
||||
import { networkControllerRouter } from "./network-controller";
|
||||
import { notebookRouter } from "./notebook";
|
||||
import { optionsRouter } from "./options";
|
||||
import { releasesRouter } from "./releases";
|
||||
import { rssFeedRouter } from "./rssFeed";
|
||||
import { smartHomeRouter } from "./smart-home";
|
||||
import { stockPriceRouter } from "./stocks";
|
||||
@@ -34,5 +35,6 @@ export const widgetRouter = createTRPCRouter({
|
||||
mediaTranscoding: mediaTranscodingRouter,
|
||||
minecraft: minecraftRouter,
|
||||
options: optionsRouter,
|
||||
releases: releasesRouter,
|
||||
networkController: networkControllerRouter,
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { observable } from "@trpc/server/observable";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
||||
import type { StreamSession } from "@homarr/integrations";
|
||||
@@ -14,10 +15,13 @@ const createMediaServerIntegrationMiddleware = (action: IntegrationAction) =>
|
||||
export const mediaServerRouter = createTRPCRouter({
|
||||
getCurrentStreams: publicProcedure
|
||||
.unstable_concat(createMediaServerIntegrationMiddleware("query"))
|
||||
.query(async ({ ctx }) => {
|
||||
.input(z.object({ showOnlyPlaying: z.boolean() }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
return await Promise.all(
|
||||
ctx.integrations.map(async (integration) => {
|
||||
const innerHandler = mediaServerRequestHandler.handler(integration, {});
|
||||
const innerHandler = mediaServerRequestHandler.handler(integration, {
|
||||
showOnlyPlaying: input.showOnlyPlaying,
|
||||
});
|
||||
const { data } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false });
|
||||
return {
|
||||
integrationId: integration.id,
|
||||
@@ -29,11 +33,14 @@ export const mediaServerRouter = createTRPCRouter({
|
||||
}),
|
||||
subscribeToCurrentStreams: publicProcedure
|
||||
.unstable_concat(createMediaServerIntegrationMiddleware("query"))
|
||||
.subscription(({ ctx }) => {
|
||||
.input(z.object({ showOnlyPlaying: z.boolean() }))
|
||||
.subscription(({ ctx, input }) => {
|
||||
return observable<{ integrationId: string; data: StreamSession[] }>((emit) => {
|
||||
const unsubscribes: (() => void)[] = [];
|
||||
for (const integration of ctx.integrations) {
|
||||
const innerHandler = mediaServerRequestHandler.handler(integration, {});
|
||||
const innerHandler = mediaServerRequestHandler.handler(integration, {
|
||||
showOnlyPlaying: input.showOnlyPlaying,
|
||||
});
|
||||
|
||||
const unsubscribe = innerHandler.subscribe((sessions) => {
|
||||
emit.next({
|
||||
|
||||
53
packages/api/src/router/widgets/releases.ts
Normal file
53
packages/api/src/router/widgets/releases.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { escapeForRegEx } from "@tiptap/react";
|
||||
import { z } from "zod";
|
||||
|
||||
import { releasesRequestHandler } from "@homarr/request-handler/releases";
|
||||
|
||||
import { createTRPCRouter, publicProcedure } from "../../trpc";
|
||||
|
||||
const formatVersionFilterRegex = (versionFilter: z.infer<typeof _releaseVersionFilterSchema> | undefined) => {
|
||||
if (!versionFilter) return undefined;
|
||||
|
||||
const escapedPrefix = versionFilter.prefix ? escapeForRegEx(versionFilter.prefix) : "";
|
||||
const precision = "[0-9]+\\.".repeat(versionFilter.precision).slice(0, -2);
|
||||
const escapedSuffix = versionFilter.suffix ? escapeForRegEx(versionFilter.suffix) : "";
|
||||
|
||||
return `^${escapedPrefix}${precision}${escapedSuffix}$`;
|
||||
};
|
||||
|
||||
const _releaseVersionFilterSchema = z.object({
|
||||
prefix: z.string().optional(),
|
||||
precision: z.number(),
|
||||
suffix: z.string().optional(),
|
||||
});
|
||||
|
||||
export const releasesRouter = createTRPCRouter({
|
||||
getLatest: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
repositories: z.array(
|
||||
z.object({
|
||||
providerKey: z.string(),
|
||||
identifier: z.string(),
|
||||
versionFilter: _releaseVersionFilterSchema.optional(),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
)
|
||||
.query(async ({ input }) => {
|
||||
const result = await Promise.all(
|
||||
input.repositories.map(async (repository) => {
|
||||
const innerHandler = releasesRequestHandler.handler({
|
||||
providerKey: repository.providerKey,
|
||||
identifier: repository.identifier,
|
||||
versionRegex: formatVersionFilterRegex(repository.versionFilter),
|
||||
});
|
||||
return await innerHandler.getCachedOrUpdatedDataAsync({
|
||||
forceUpdate: false,
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
return result;
|
||||
}),
|
||||
});
|
||||
@@ -23,8 +23,8 @@
|
||||
},
|
||||
"prettier": "@homarr/prettier-config",
|
||||
"dependencies": {
|
||||
"@auth/core": "^0.38.0",
|
||||
"@auth/drizzle-adapter": "^1.8.0",
|
||||
"@auth/core": "^0.39.0",
|
||||
"@auth/drizzle-adapter": "^1.9.0",
|
||||
"@homarr/certificates": "workspace:^0.1.0",
|
||||
"@homarr/common": "workspace:^0.1.0",
|
||||
"@homarr/db": "workspace:^0.1.0",
|
||||
@@ -36,7 +36,7 @@
|
||||
"cookies": "^0.9.1",
|
||||
"ldapts": "7.4.0",
|
||||
"next": "15.3.1",
|
||||
"next-auth": "5.0.0-beta.25",
|
||||
"next-auth": "5.0.0-beta.27",
|
||||
"pretty-print-error": "^1.1.2",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
@@ -48,7 +48,7 @@
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/bcrypt": "5.0.2",
|
||||
"@types/cookies": "0.9.0",
|
||||
"eslint": "^9.25.0",
|
||||
"eslint": "^9.25.1",
|
||||
"prettier": "^3.5.3",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.25.0",
|
||||
"eslint": "^9.25.1",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.25.0",
|
||||
"eslint": "^9.25.1",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ export const loadCustomRootCertificatesAsync = async () => {
|
||||
const dirContent = await fs.readdir(folder);
|
||||
return await Promise.all(
|
||||
dirContent
|
||||
.filter((file) => file.endsWith(".crt"))
|
||||
.filter((file) => file.endsWith(".crt") || file.endsWith(".pem"))
|
||||
.map(async (file) => ({
|
||||
content: await fs.readFile(path.join(folder, file), "utf8"),
|
||||
fileName: file,
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.25.0",
|
||||
"eslint": "^9.25.1",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.25.0",
|
||||
"eslint": "^9.25.1",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.25.0",
|
||||
"eslint": "^9.25.1",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.25.0",
|
||||
"eslint": "^9.25.1",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/node-cron": "^3.0.11",
|
||||
"eslint": "^9.25.0",
|
||||
"eslint": "^9.25.1",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.25.0",
|
||||
"eslint": "^9.25.1",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ export const mediaServerJob = createCronJob("mediaServer", EVERY_5_SECONDS).with
|
||||
createRequestIntegrationJobHandler(mediaServerRequestHandler.handler, {
|
||||
widgetKinds: ["mediaServer"],
|
||||
getInput: {
|
||||
mediaServer: () => ({}),
|
||||
mediaServer: ({ showOnlyPlaying }) => ({ showOnlyPlaying }),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -38,19 +38,19 @@
|
||||
},
|
||||
"prettier": "@homarr/prettier-config",
|
||||
"dependencies": {
|
||||
"@auth/core": "^0.38.0",
|
||||
"@auth/core": "^0.39.0",
|
||||
"@homarr/common": "workspace:^0.1.0",
|
||||
"@homarr/definitions": "workspace:^0.1.0",
|
||||
"@homarr/env": "workspace:^0.1.0",
|
||||
"@homarr/log": "workspace:^0.1.0",
|
||||
"@homarr/server-settings": "workspace:^0.1.0",
|
||||
"@mantine/core": "^7.17.4",
|
||||
"@mantine/core": "^7.17.5",
|
||||
"@paralleldrive/cuid2": "^2.2.2",
|
||||
"@testcontainers/mysql": "^10.24.2",
|
||||
"better-sqlite3": "^11.9.1",
|
||||
"dotenv": "^16.5.0",
|
||||
"drizzle-kit": "^0.31.0",
|
||||
"drizzle-orm": "^0.42.0",
|
||||
"drizzle-orm": "^0.43.1",
|
||||
"drizzle-zod": "^0.7.1",
|
||||
"mysql2": "3.14.0"
|
||||
},
|
||||
@@ -60,7 +60,7 @@
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/better-sqlite3": "7.6.13",
|
||||
"dotenv-cli": "^8.0.0",
|
||||
"eslint": "^9.25.0",
|
||||
"eslint": "^9.25.1",
|
||||
"prettier": "^3.5.3",
|
||||
"tsx": "4.19.3",
|
||||
"typescript": "^5.8.3"
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.25.0",
|
||||
"eslint": "^9.25.1",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,5 +23,6 @@ export const widgetKinds = [
|
||||
"bookmarks",
|
||||
"indexerManager",
|
||||
"healthMonitoring",
|
||||
"releases",
|
||||
] as const;
|
||||
export type WidgetKind = (typeof widgetKinds)[number];
|
||||
|
||||
@@ -25,14 +25,14 @@
|
||||
"dependencies": {
|
||||
"@homarr/common": "workspace:^0.1.0",
|
||||
"@homarr/env": "workspace:^0.1.0",
|
||||
"dockerode": "^4.0.5"
|
||||
"dockerode": "^4.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/dockerode": "^3.3.38",
|
||||
"eslint": "^9.25.0",
|
||||
"eslint": "^9.25.1",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
|
||||
4
packages/env/package.json
vendored
4
packages/env/package.json
vendored
@@ -23,14 +23,14 @@
|
||||
},
|
||||
"prettier": "@homarr/prettier-config",
|
||||
"dependencies": {
|
||||
"@t3-oss/env-nextjs": "^0.12.0",
|
||||
"@t3-oss/env-nextjs": "^0.13.0",
|
||||
"zod": "^3.24.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.25.0",
|
||||
"eslint": "^9.25.1",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,14 +26,14 @@
|
||||
"@homarr/common": "workspace:^0.1.0",
|
||||
"@homarr/translation": "workspace:^0.1.0",
|
||||
"@homarr/validation": "workspace:^0.1.0",
|
||||
"@mantine/form": "^7.17.4",
|
||||
"@mantine/form": "^7.17.5",
|
||||
"zod": "^3.24.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.25.0",
|
||||
"eslint": "^9.25.1",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
"@homarr/notifications": "workspace:^0.1.0",
|
||||
"@homarr/translation": "workspace:^0.1.0",
|
||||
"@homarr/validation": "workspace:^0.1.0",
|
||||
"@mantine/core": "^7.17.4",
|
||||
"@mantine/core": "^7.17.5",
|
||||
"react": "19.1.0",
|
||||
"zod": "^3.24.3"
|
||||
},
|
||||
@@ -37,7 +37,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.25.0",
|
||||
"eslint": "^9.25.1",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.25.0",
|
||||
"eslint": "^9.25.1",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"prettier": "@homarr/prettier-config",
|
||||
"dependencies": {
|
||||
"@ctrl/deluge": "^7.1.0",
|
||||
"@ctrl/qbittorrent": "^9.5.2",
|
||||
"@ctrl/qbittorrent": "^9.6.0",
|
||||
"@ctrl/transmission": "^7.2.0",
|
||||
"@homarr/certificates": "workspace:^0.1.0",
|
||||
"@homarr/common": "workspace:^0.1.0",
|
||||
@@ -38,8 +38,9 @@
|
||||
"@jellyfin/sdk": "^0.11.0",
|
||||
"maria2": "^0.4.0",
|
||||
"node-ical": "^0.20.1",
|
||||
"node-unifi": "^2.5.1",
|
||||
"proxmox-api": "1.1.1",
|
||||
"tsdav": "^2.1.3",
|
||||
"tsdav": "^2.1.4",
|
||||
"undici": "7.8.0",
|
||||
"xml2js": "^0.6.2",
|
||||
"zod": "^3.24.3"
|
||||
@@ -48,8 +49,9 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/node-unifi": "^2.5.1",
|
||||
"@types/xml2js": "^0.4.14",
|
||||
"eslint": "^9.25.0",
|
||||
"eslint": "^9.25.1",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { z } from "zod";
|
||||
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
|
||||
|
||||
import { Integration } from "../base/integration";
|
||||
import type { StreamSession } from "../interfaces/media-server/session";
|
||||
import type { CurrentSessionsInput, StreamSession } from "../interfaces/media-server/session";
|
||||
import { convertJellyfinType } from "../jellyfin/jellyfin-integration";
|
||||
|
||||
const sessionSchema = z.object({
|
||||
@@ -47,7 +47,7 @@ export class EmbyIntegration extends Integration {
|
||||
});
|
||||
}
|
||||
|
||||
public async getCurrentSessionsAsync(): Promise<StreamSession[]> {
|
||||
public async getCurrentSessionsAsync(options: CurrentSessionsInput): Promise<StreamSession[]> {
|
||||
const apiKey = super.getSecretValue("apiKey");
|
||||
const response = await fetchWithTrustedCertificatesAsync(super.url("/emby/Sessions"), {
|
||||
headers: {
|
||||
@@ -69,6 +69,7 @@ export class EmbyIntegration extends Integration {
|
||||
return result.data
|
||||
.filter((sessionInfo) => sessionInfo.UserId !== undefined)
|
||||
.filter((sessionInfo) => sessionInfo.DeviceId !== EmbyIntegration.deviceId)
|
||||
.filter((sessionInfo) => !options.showOnlyPlaying || sessionInfo.NowPlayingItem !== undefined)
|
||||
.map((sessionInfo): StreamSession => {
|
||||
let currentlyPlaying: StreamSession["currentlyPlaying"] | null = null;
|
||||
|
||||
|
||||
@@ -15,3 +15,7 @@ export interface StreamSession {
|
||||
episodeCount?: number | null;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface CurrentSessionsInput {
|
||||
showOnlyPlaying: boolean;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { getSystemApi } from "@jellyfin/sdk/lib/utils/api/system-api";
|
||||
import { createAxiosCertificateInstanceAsync } from "@homarr/certificates/server";
|
||||
|
||||
import { Integration } from "../base/integration";
|
||||
import type { StreamSession } from "../interfaces/media-server/session";
|
||||
import type { CurrentSessionsInput, StreamSession } from "../interfaces/media-server/session";
|
||||
|
||||
export class JellyfinIntegration extends Integration {
|
||||
private readonly jellyfin: Jellyfin = new Jellyfin({
|
||||
@@ -26,7 +26,7 @@ export class JellyfinIntegration extends Integration {
|
||||
await systemApi.getPingSystem();
|
||||
}
|
||||
|
||||
public async getCurrentSessionsAsync(): Promise<StreamSession[]> {
|
||||
public async getCurrentSessionsAsync(options: CurrentSessionsInput): Promise<StreamSession[]> {
|
||||
const api = await this.getApiAsync();
|
||||
const sessionApi = getSessionApi(api);
|
||||
const sessions = await sessionApi.getSessions();
|
||||
@@ -38,6 +38,7 @@ export class JellyfinIntegration extends Integration {
|
||||
return sessions.data
|
||||
.filter((sessionInfo) => sessionInfo.UserId !== undefined)
|
||||
.filter((sessionInfo) => sessionInfo.DeviceId !== "homarr")
|
||||
.filter((sessionInfo) => !options.showOnlyPlaying || sessionInfo.NowPlayingItem !== undefined)
|
||||
.map((sessionInfo): StreamSession => {
|
||||
let currentlyPlaying: StreamSession["currentlyPlaying"] | null = null;
|
||||
|
||||
|
||||
@@ -5,11 +5,11 @@ import { logger } from "@homarr/log";
|
||||
|
||||
import { Integration } from "../base/integration";
|
||||
import { IntegrationTestConnectionError } from "../base/test-connection-error";
|
||||
import type { StreamSession } from "../interfaces/media-server/session";
|
||||
import type { CurrentSessionsInput, StreamSession } from "../interfaces/media-server/session";
|
||||
import type { PlexResponse } from "./interface";
|
||||
|
||||
export class PlexIntegration extends Integration {
|
||||
public async getCurrentSessionsAsync(): Promise<StreamSession[]> {
|
||||
public async getCurrentSessionsAsync(_options: CurrentSessionsInput): Promise<StreamSession[]> {
|
||||
const token = super.getSecretValue("apiKey");
|
||||
|
||||
const response = await fetchWithTrustedCertificatesAsync(this.url("/status/sessions"), {
|
||||
|
||||
@@ -1,103 +1,78 @@
|
||||
import type z from "zod";
|
||||
import type { SiteStats } from "node-unifi";
|
||||
import { Controller } from "node-unifi";
|
||||
|
||||
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
|
||||
import { logger } from "@homarr/log";
|
||||
|
||||
import { ParseError } from "../base/error";
|
||||
import { Integration, throwErrorByStatusCode } from "../base/integration";
|
||||
import { IntegrationTestConnectionError } from "../base/test-connection-error";
|
||||
import { Integration } from "../base/integration";
|
||||
import type { NetworkControllerSummaryIntegration } from "../interfaces/network-controller-summary/network-controller-summary-integration";
|
||||
import type { NetworkControllerSummary } from "../interfaces/network-controller-summary/network-controller-summary-types";
|
||||
import { unifiSummaryResponseSchema } from "./unifi-controller-types";
|
||||
|
||||
const udmpPrefix = "proxy/network";
|
||||
type Subsystem = "www" | "wan" | "wlan" | "lan" | "vpn";
|
||||
import type { HealthSubsystem } from "./unifi-controller-types";
|
||||
|
||||
export class UnifiControllerIntegration extends Integration implements NetworkControllerSummaryIntegration {
|
||||
private prefix: string | undefined;
|
||||
|
||||
public async getNetworkSummaryAsync(): Promise<NetworkControllerSummary> {
|
||||
if (!this.headers) {
|
||||
await this.authenticateAndConstructSessionInHeaderAsync();
|
||||
}
|
||||
|
||||
const requestUrl = this.url(`/${this.prefix}/api/stat/sites`);
|
||||
|
||||
const requestHeaders: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
...this.headers,
|
||||
};
|
||||
if (this.csrfToken) {
|
||||
requestHeaders["X-CSRF-TOKEN"] = this.csrfToken;
|
||||
}
|
||||
|
||||
const statsResponse = await fetchWithTrustedCertificatesAsync(requestUrl, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
...requestHeaders,
|
||||
},
|
||||
}).catch((err: TypeError) => {
|
||||
const detailMessage = String(err.cause);
|
||||
throw new IntegrationTestConnectionError("invalidUrl", detailMessage);
|
||||
});
|
||||
|
||||
if (!statsResponse.ok) {
|
||||
throwErrorByStatusCode(statsResponse.status);
|
||||
}
|
||||
|
||||
const result = unifiSummaryResponseSchema.safeParse(await statsResponse.json());
|
||||
|
||||
if (!result.success) {
|
||||
throw new ParseError("Unifi controller", result.error);
|
||||
}
|
||||
const client = await this.createControllerClientAsync();
|
||||
const stats = await client.getSitesStats();
|
||||
|
||||
return {
|
||||
wanStatus: this.getStatusValueOverAllSites(result.data, "wan", (site) => site.status === "ok"),
|
||||
wanStatus: this.getStatusValueOverAllSites(stats, "wan", (site) => site.status === "ok"),
|
||||
www: {
|
||||
status: this.getStatusValueOverAllSites(result.data, "wan", (site) => site.status === "ok"),
|
||||
latency: this.getNumericValueOverAllSites(result.data, "www", (site) => site.latency, "max"),
|
||||
ping: this.getNumericValueOverAllSites(result.data, "www", (site) => site.speedtest_ping, "max"),
|
||||
uptime: this.getNumericValueOverAllSites(result.data, "www", (site) => site.uptime, "max"),
|
||||
status: this.getStatusValueOverAllSites(stats, "wan", (site) => site.status === "ok"),
|
||||
latency: this.getNumericValueOverAllSites(stats, "www", (site) => site.latency, "max"),
|
||||
ping: this.getNumericValueOverAllSites(stats, "www", (site) => site.speedtest_ping, "max"),
|
||||
uptime: this.getNumericValueOverAllSites(stats, "www", (site) => site.uptime, "max"),
|
||||
},
|
||||
wifi: {
|
||||
status: this.getStatusValueOverAllSites(result.data, "wlan", (site) => site.status === "ok"),
|
||||
users: this.getNumericValueOverAllSites(result.data, "wlan", (site) => site.num_user, "sum"),
|
||||
guests: this.getNumericValueOverAllSites(result.data, "wlan", (site) => site.num_guest, "sum"),
|
||||
status: this.getStatusValueOverAllSites(stats, "wlan", (site) => site.status === "ok"),
|
||||
users: this.getNumericValueOverAllSites(stats, "wlan", (site) => site.num_user, "sum"),
|
||||
guests: this.getNumericValueOverAllSites(stats, "wlan", (site) => site.num_guest, "sum"),
|
||||
},
|
||||
lan: {
|
||||
status: this.getStatusValueOverAllSites(result.data, "lan", (site) => site.status === "ok"),
|
||||
users: this.getNumericValueOverAllSites(result.data, "lan", (site) => site.num_user, "sum"),
|
||||
guests: this.getNumericValueOverAllSites(result.data, "lan", (site) => site.num_guest, "sum"),
|
||||
status: this.getStatusValueOverAllSites(stats, "lan", (site) => site.status === "ok"),
|
||||
users: this.getNumericValueOverAllSites(stats, "lan", (site) => site.num_user, "sum"),
|
||||
guests: this.getNumericValueOverAllSites(stats, "lan", (site) => site.num_guest, "sum"),
|
||||
},
|
||||
vpn: {
|
||||
status: this.getStatusValueOverAllSites(result.data, "vpn", (site) => site.status === "ok"),
|
||||
users: this.getNumericValueOverAllSites(result.data, "vpn", (site) => site.remote_user_num_active, "sum"),
|
||||
status: this.getStatusValueOverAllSites(stats, "vpn", (site) => site.status === "ok"),
|
||||
users: this.getNumericValueOverAllSites(stats, "vpn", (site) => site.remote_user_num_active, "sum"),
|
||||
},
|
||||
} satisfies NetworkControllerSummary;
|
||||
}
|
||||
|
||||
public async testConnectionAsync(): Promise<void> {
|
||||
await this.authenticateAndConstructSessionInHeaderAsync();
|
||||
const client = await this.createControllerClientAsync();
|
||||
await client.getSitesStats();
|
||||
}
|
||||
|
||||
private getStatusValueOverAllSites(
|
||||
data: z.infer<typeof unifiSummaryResponseSchema>,
|
||||
subsystem: Subsystem,
|
||||
selectCallback: (obj: z.infer<typeof unifiSummaryResponseSchema>["data"][number]["health"][number]) => boolean,
|
||||
private async createControllerClientAsync() {
|
||||
const portString = new URL(this.integration.url).port;
|
||||
const port = Number.isInteger(portString) ? Number(portString) : undefined;
|
||||
const hostname = new URL(this.integration.url).hostname;
|
||||
|
||||
const client = new Controller({
|
||||
host: hostname,
|
||||
// @ts-expect-error the URL construction is incorrect and does not append the required / at the end: https://github.com/jens-maus/node-unifi/blob/05665e8f82a900a15a9ea8b1071750b29825b3bc/unifi.js#L56, https://github.com/jens-maus/node-unifi/blob/05665e8f82a900a15a9ea8b1071750b29825b3bc/unifi.js#L95
|
||||
port: port === undefined ? "/" : `${port}/`,
|
||||
sslverify: false, // TODO: implement a "ignore certificate toggle", see https://github.com/homarr-labs/homarr/issues/2553
|
||||
username: this.getSecretValue("username"),
|
||||
password: this.getSecretValue("password"),
|
||||
});
|
||||
|
||||
// Object.defineProperty(client, '_baseurl', { value: url });
|
||||
await client.login(this.getSecretValue("username"), this.getSecretValue("password"), null);
|
||||
return client;
|
||||
}
|
||||
|
||||
private getStatusValueOverAllSites<S extends HealthSubsystem>(
|
||||
data: SiteStats[],
|
||||
subsystem: S,
|
||||
selectCallback: (obj: SiteStats["health"][number]) => boolean,
|
||||
) {
|
||||
return this.getBooleanValueOverAllSites(data, subsystem, selectCallback) ? "enabled" : "disabled";
|
||||
}
|
||||
|
||||
private getNumericValueOverAllSites<
|
||||
S extends Subsystem,
|
||||
T extends Extract<z.infer<typeof unifiSummaryResponseSchema>["data"][number]["health"][number], { subsystem: S }>,
|
||||
>(
|
||||
data: z.infer<typeof unifiSummaryResponseSchema>,
|
||||
subsystem: S,
|
||||
selectCallback: (obj: T) => number,
|
||||
strategy: "average" | "sum" | "max",
|
||||
): number {
|
||||
const values = data.data.map((site) => selectCallback(this.getSubsystem(site.health, subsystem) as T));
|
||||
S extends HealthSubsystem,
|
||||
T extends Extract<SiteStats["health"][number], { subsystem: S }>,
|
||||
>(data: SiteStats[], subsystem: S, selectCallback: (obj: T) => number, strategy: "average" | "sum" | "max"): number {
|
||||
const values = data.map((site) => selectCallback(this.getSubsystem(site.health, subsystem) as T));
|
||||
|
||||
if (strategy === "sum") {
|
||||
return values.reduce((first, second) => first + second, 0);
|
||||
@@ -111,118 +86,18 @@ export class UnifiControllerIntegration extends Integration implements NetworkCo
|
||||
}
|
||||
|
||||
private getBooleanValueOverAllSites(
|
||||
data: z.infer<typeof unifiSummaryResponseSchema>,
|
||||
subsystem: Subsystem,
|
||||
selectCallback: (obj: z.infer<typeof unifiSummaryResponseSchema>["data"][number]["health"][number]) => boolean,
|
||||
data: SiteStats[],
|
||||
subsystem: HealthSubsystem,
|
||||
selectCallback: (obj: SiteStats["health"][number]) => boolean,
|
||||
): boolean {
|
||||
return data.data.every((site) => selectCallback(this.getSubsystem(site.health, subsystem)));
|
||||
return data.every((site) => selectCallback(this.getSubsystem(site.health, subsystem)));
|
||||
}
|
||||
|
||||
private getSubsystem(
|
||||
health: z.infer<typeof unifiSummaryResponseSchema>["data"][number]["health"],
|
||||
subsystem: Subsystem,
|
||||
) {
|
||||
private getSubsystem(health: SiteStats["health"], subsystem: HealthSubsystem) {
|
||||
const value = health.find((health) => health.subsystem === subsystem);
|
||||
if (!value) {
|
||||
throw new Error(`Subsystem ${subsystem} not found!`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private headers: Record<string, string> | undefined = undefined;
|
||||
private csrfToken: string | undefined;
|
||||
|
||||
private async authenticateAndConstructSessionInHeaderAsync(): Promise<void> {
|
||||
await this.determineUDMVariantAsync();
|
||||
await this.authenticateAndSetCookieAsync();
|
||||
}
|
||||
|
||||
private async authenticateAndSetCookieAsync(): Promise<void> {
|
||||
if (this.headers) {
|
||||
return;
|
||||
}
|
||||
|
||||
const endpoint = this.prefix === udmpPrefix ? "auth/login" : "login";
|
||||
logger.debug("Authenticating at network console: " + endpoint);
|
||||
|
||||
const loginUrl = this.url(`/api/${endpoint}`);
|
||||
|
||||
const loginBody = {
|
||||
username: this.getSecretValue("username"),
|
||||
password: this.getSecretValue("password"),
|
||||
remember: true,
|
||||
};
|
||||
|
||||
const requestHeaders: Record<string, string> = { "Content-Type": "application/json" };
|
||||
if (this.csrfToken) {
|
||||
requestHeaders["X-CSRF-TOKEN"] = this.csrfToken;
|
||||
}
|
||||
|
||||
const loginResponse = await fetchWithTrustedCertificatesAsync(loginUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
...requestHeaders,
|
||||
},
|
||||
body: JSON.stringify(loginBody),
|
||||
}).catch((err: TypeError) => {
|
||||
const detailMessage = String(err.cause);
|
||||
throw new IntegrationTestConnectionError("invalidUrl", detailMessage);
|
||||
});
|
||||
|
||||
if (!loginResponse.ok) {
|
||||
throwErrorByStatusCode(loginResponse.status);
|
||||
}
|
||||
|
||||
const responseHeaders = loginResponse.headers;
|
||||
const newHeaders: Record<string, string> = {};
|
||||
const loginToken = UnifiControllerIntegration.extractLoginTokenFromCookies(responseHeaders);
|
||||
newHeaders.Cookie = `${loginToken};`;
|
||||
this.headers = newHeaders;
|
||||
}
|
||||
|
||||
private async determineUDMVariantAsync(): Promise<void> {
|
||||
if (this.prefix) {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug("Prefix for authentication not set; initial connect to determine UDM variant");
|
||||
const url = this.url("/");
|
||||
|
||||
const { status, ok, headers } = await fetchWithTrustedCertificatesAsync(url, { method: "HEAD" })
|
||||
.then((res) => res)
|
||||
.catch((err: TypeError) => {
|
||||
const detailMessage = String(err.cause);
|
||||
throw new IntegrationTestConnectionError("invalidUrl", detailMessage);
|
||||
});
|
||||
|
||||
if (!ok) {
|
||||
throw new IntegrationTestConnectionError("invalidUrl", "status code: " + status);
|
||||
}
|
||||
|
||||
let prefix = "";
|
||||
if (headers.get("x-csrf-token") !== null) {
|
||||
// Unifi OS < 3.2.5 passes & requires csrf-token
|
||||
prefix = udmpPrefix;
|
||||
const headersCSRFToken = headers.get("x-csrf-token");
|
||||
if (headersCSRFToken) {
|
||||
this.csrfToken = headersCSRFToken;
|
||||
}
|
||||
} else if (headers.get("access-control-expose-headers") !== null) {
|
||||
// Unifi OS ≥ 3.2.5 doesnt pass csrf token but still uses different endpoint
|
||||
prefix = udmpPrefix;
|
||||
}
|
||||
this.prefix = prefix;
|
||||
logger.debug("Final prefix: " + this.prefix);
|
||||
}
|
||||
|
||||
private static extractLoginTokenFromCookies(headers: Headers): string {
|
||||
const cookies = headers.get("set-cookie") ?? "";
|
||||
const loginToken = cookies.split(";").find((cookie) => cookie.includes("TOKEN"));
|
||||
|
||||
if (loginToken) {
|
||||
return loginToken;
|
||||
}
|
||||
|
||||
throw new Error("Login token not found in cookies");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,130 +1,3 @@
|
||||
import { z } from "zod";
|
||||
import type { SiteStats } from "node-unifi";
|
||||
|
||||
export const healthSchema = z.discriminatedUnion("subsystem", [
|
||||
z.object({
|
||||
subsystem: z.literal("wlan"),
|
||||
num_user: z.number(),
|
||||
num_guest: z.number(),
|
||||
num_iot: z.number(),
|
||||
"tx_bytes-r": z.number(),
|
||||
"rx_bytes-r": z.number(),
|
||||
status: z.string(),
|
||||
num_ap: z.number(),
|
||||
num_adopted: z.number(),
|
||||
num_disabled: z.number(),
|
||||
num_disconnected: z.number(),
|
||||
num_pending: z.number(),
|
||||
}),
|
||||
z.object({
|
||||
subsystem: z.literal("wan"),
|
||||
num_gw: z.number(),
|
||||
num_adopted: z.number(),
|
||||
num_disconnected: z.number(),
|
||||
num_pending: z.number(),
|
||||
status: z.string(),
|
||||
wan_ip: z.string().ip(),
|
||||
gateways: z.array(z.string().ip()),
|
||||
netmask: z.string().ip(),
|
||||
nameservers: z.array(z.string().ip()).optional(),
|
||||
num_sta: z.number(),
|
||||
"tx_bytes-r": z.number(),
|
||||
"rx_bytes-r": z.number(),
|
||||
gw_mac: z.string(),
|
||||
gw_name: z.string(),
|
||||
"gw_system-stats": z.object({
|
||||
cpu: z.string(),
|
||||
mem: z.string(),
|
||||
uptime: z.string(),
|
||||
}),
|
||||
gw_version: z.string(),
|
||||
isp_name: z.string(),
|
||||
isp_organization: z.string(),
|
||||
uptime_stats: z.object({
|
||||
WAN: z.object({
|
||||
alerting_monitors: z.array(
|
||||
z.object({
|
||||
availability: z.number(),
|
||||
latency_average: z.number(),
|
||||
target: z.string(),
|
||||
type: z.enum(["icmp", "dns"]),
|
||||
}),
|
||||
),
|
||||
availability: z.number(),
|
||||
latency_average: z.number(),
|
||||
monitors: z.array(
|
||||
z.object({
|
||||
availability: z.number(),
|
||||
latency_average: z.number(),
|
||||
target: z.string(),
|
||||
type: z.enum(["icmp", "dns"]),
|
||||
}),
|
||||
),
|
||||
time_period: z.number(),
|
||||
uptime: z.number(),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
z.object({
|
||||
subsystem: z.literal("www"),
|
||||
status: z.string(),
|
||||
"tx_bytes-r": z.number(),
|
||||
"rx_bytes-r": z.number(),
|
||||
latency: z.number(),
|
||||
uptime: z.number(),
|
||||
drops: z.number(),
|
||||
xput_up: z.number(),
|
||||
xput_down: z.number(),
|
||||
speedtest_status: z.string(),
|
||||
speedtest_lastrun: z.number(),
|
||||
speedtest_ping: z.number(),
|
||||
gw_mac: z.string(),
|
||||
}),
|
||||
z.object({
|
||||
subsystem: z.literal("lan"),
|
||||
lan_ip: z.string().ip().nullish(),
|
||||
status: z.string(),
|
||||
num_user: z.number(),
|
||||
num_guest: z.number(),
|
||||
num_iot: z.number(),
|
||||
"tx_bytes-r": z.number(),
|
||||
"rx_bytes-r": z.number(),
|
||||
num_sw: z.number(),
|
||||
num_adopted: z.number(),
|
||||
num_disconnected: z.number(),
|
||||
num_pending: z.number(),
|
||||
}),
|
||||
z.object({
|
||||
subsystem: z.literal("vpn"),
|
||||
status: z.string(),
|
||||
remote_user_enabled: z.boolean(),
|
||||
remote_user_num_active: z.number(),
|
||||
remote_user_num_inactive: z.number(),
|
||||
remote_user_rx_bytes: z.number(),
|
||||
remote_user_tx_bytes: z.number(),
|
||||
remote_user_rx_packets: z.number(),
|
||||
remote_user_tx_packets: z.number(),
|
||||
site_to_site_enabled: z.boolean(),
|
||||
}),
|
||||
]);
|
||||
|
||||
export type Health = z.infer<typeof healthSchema>;
|
||||
|
||||
export const siteSchema = z.object({
|
||||
anonymous_id: z.string().uuid(),
|
||||
name: z.string(),
|
||||
external_id: z.string().uuid(),
|
||||
_id: z.string(),
|
||||
attr_no_delete: z.boolean(),
|
||||
attr_hidden_id: z.string(),
|
||||
desc: z.string(),
|
||||
health: z.array(healthSchema),
|
||||
num_new_alarms: z.number(),
|
||||
});
|
||||
export type Site = z.infer<typeof siteSchema>;
|
||||
|
||||
export const unifiSummaryResponseSchema = z.object({
|
||||
meta: z.object({
|
||||
rc: z.enum(["ok"]),
|
||||
}),
|
||||
data: z.array(siteSchema),
|
||||
});
|
||||
export type HealthSubsystem = SiteStats["health"][number]["subsystem"];
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.25.0",
|
||||
"eslint": "^9.25.1",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
"@homarr/translation": "workspace:^0.1.0",
|
||||
"@homarr/ui": "workspace:^0.1.0",
|
||||
"@homarr/validation": "workspace:^0.1.0",
|
||||
"@mantine/core": "^7.17.4",
|
||||
"@mantine/core": "^7.17.5",
|
||||
"@tabler/icons-react": "^3.31.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"next": "15.3.1",
|
||||
@@ -45,7 +45,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.25.0",
|
||||
"eslint": "^9.25.1",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,15 +24,15 @@
|
||||
"dependencies": {
|
||||
"@homarr/translation": "workspace:^0.1.0",
|
||||
"@homarr/ui": "workspace:^0.1.0",
|
||||
"@mantine/core": "^7.17.4",
|
||||
"@mantine/hooks": "^7.17.4",
|
||||
"@mantine/core": "^7.17.5",
|
||||
"@mantine/hooks": "^7.17.5",
|
||||
"react": "19.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.25.0",
|
||||
"eslint": "^9.25.1",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,14 +24,14 @@
|
||||
"prettier": "@homarr/prettier-config",
|
||||
"dependencies": {
|
||||
"@homarr/ui": "workspace:^0.1.0",
|
||||
"@mantine/notifications": "^7.17.4",
|
||||
"@mantine/notifications": "^7.17.5",
|
||||
"@tabler/icons-react": "^3.31.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.25.0",
|
||||
"eslint": "^9.25.1",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,8 +37,8 @@
|
||||
"@homarr/translation": "workspace:^0.1.0",
|
||||
"@homarr/ui": "workspace:^0.1.0",
|
||||
"@homarr/validation": "workspace:^0.1.0",
|
||||
"@mantine/core": "^7.17.4",
|
||||
"@mantine/hooks": "^7.17.4",
|
||||
"@mantine/core": "^7.17.5",
|
||||
"@mantine/hooks": "^7.17.5",
|
||||
"adm-zip": "0.5.16",
|
||||
"next": "15.3.1",
|
||||
"react": "19.1.0",
|
||||
@@ -52,7 +52,7 @@
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/adm-zip": "0.5.7",
|
||||
"eslint": "^9.25.0",
|
||||
"eslint": "^9.25.1",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,7 +133,9 @@ const optionMapping: OptionMapping = {
|
||||
automationId: (oldOptions) => oldOptions.automationId,
|
||||
displayName: (oldOptions) => oldOptions.displayName,
|
||||
},
|
||||
mediaServer: {},
|
||||
mediaServer: {
|
||||
showOnlyPlaying: () => undefined,
|
||||
},
|
||||
indexerManager: {
|
||||
openIndexerSiteInNewTab: (oldOptions) => oldOptions.openIndexerSiteInNewTab,
|
||||
},
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.25.0",
|
||||
"eslint": "^9.25.1",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.25.0",
|
||||
"eslint": "^9.25.1",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.25.0",
|
||||
"eslint": "^9.25.1",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.25.0",
|
||||
"eslint": "^9.25.1",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,11 +9,13 @@ import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-
|
||||
export const mediaServerRequestHandler = createCachedIntegrationRequestHandler<
|
||||
StreamSession[],
|
||||
IntegrationKindByCategory<"mediaService">,
|
||||
Record<string, never>
|
||||
{
|
||||
showOnlyPlaying: boolean;
|
||||
}
|
||||
>({
|
||||
async requestAsync(integration, _input) {
|
||||
async requestAsync(integration, input) {
|
||||
const integrationInstance = await createIntegrationAsync(integration);
|
||||
return await integrationInstance.getCurrentSessionsAsync();
|
||||
return await integrationInstance.getCurrentSessionsAsync({ showOnlyPlaying: input.showOnlyPlaying });
|
||||
},
|
||||
cacheDuration: dayjs.duration(5, "seconds"),
|
||||
queryKey: "mediaServerSessions",
|
||||
|
||||
306
packages/request-handler/src/releases-providers.ts
Normal file
306
packages/request-handler/src/releases-providers.ts
Normal file
@@ -0,0 +1,306 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export interface ReleasesProvider {
|
||||
getDetailsUrl: (identifier: string) => string | undefined;
|
||||
parseDetailsResponse: (response: unknown) => z.SafeParseReturnType<unknown, DetailsResponse> | undefined;
|
||||
getReleasesUrl: (identifier: string) => string;
|
||||
parseReleasesResponse: (response: unknown) => z.SafeParseReturnType<unknown, ReleasesResponse[]>;
|
||||
}
|
||||
|
||||
interface ProvidersProps {
|
||||
[key: string]: ReleasesProvider;
|
||||
DockerHub: ReleasesProvider;
|
||||
Github: ReleasesProvider;
|
||||
Gitlab: ReleasesProvider;
|
||||
Npm: ReleasesProvider;
|
||||
Codeberg: ReleasesProvider;
|
||||
}
|
||||
|
||||
export const Providers: ProvidersProps = {
|
||||
DockerHub: {
|
||||
getDetailsUrl(identifier) {
|
||||
if (identifier.indexOf("/") > 0) {
|
||||
const [owner, name] = identifier.split("/");
|
||||
if (!owner || !name) {
|
||||
return "";
|
||||
}
|
||||
return `https://hub.docker.com/v2/namespaces/${encodeURIComponent(owner)}/repositories/${encodeURIComponent(name)}`;
|
||||
} else {
|
||||
return `https://hub.docker.com/v2/repositories/library/${encodeURIComponent(identifier)}`;
|
||||
}
|
||||
},
|
||||
parseDetailsResponse(response) {
|
||||
return z
|
||||
.object({
|
||||
name: z.string(),
|
||||
namespace: z.string(),
|
||||
description: z.string(),
|
||||
star_count: z.number(),
|
||||
date_registered: z.string().transform((value) => new Date(value)),
|
||||
})
|
||||
.transform((resp) => ({
|
||||
projectUrl: `https://hub.docker.com/r/${resp.namespace === "library" ? "_" : resp.namespace}/${resp.name}`,
|
||||
projectDescription: resp.description,
|
||||
isFork: false,
|
||||
isArchived: false,
|
||||
createdAt: resp.date_registered,
|
||||
starsCount: resp.star_count,
|
||||
openIssues: 0,
|
||||
forksCount: 0,
|
||||
}))
|
||||
.safeParse(response);
|
||||
},
|
||||
getReleasesUrl(identifier) {
|
||||
return `${this.getDetailsUrl(identifier)}/tags?page_size=200`;
|
||||
},
|
||||
parseReleasesResponse(response) {
|
||||
return z
|
||||
.object({
|
||||
results: z.array(
|
||||
z
|
||||
.object({ name: z.string(), last_updated: z.string().transform((value) => new Date(value)) })
|
||||
.transform((tag) => ({
|
||||
identifier: "",
|
||||
latestRelease: tag.name,
|
||||
latestReleaseAt: tag.last_updated,
|
||||
})),
|
||||
),
|
||||
})
|
||||
.transform((resp) => {
|
||||
return resp.results.map((release) => ({
|
||||
...release,
|
||||
releaseUrl: "",
|
||||
releaseDescription: "",
|
||||
isPreRelease: false,
|
||||
}));
|
||||
})
|
||||
.safeParse(response);
|
||||
},
|
||||
},
|
||||
Github: {
|
||||
getDetailsUrl(identifier) {
|
||||
const [owner, name] = identifier.split("/");
|
||||
if (!owner || !name) {
|
||||
return "";
|
||||
}
|
||||
return `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}`;
|
||||
},
|
||||
parseDetailsResponse(response) {
|
||||
return z
|
||||
.object({
|
||||
html_url: z.string(),
|
||||
description: z.string(),
|
||||
fork: z.boolean(),
|
||||
archived: z.boolean(),
|
||||
created_at: z.string().transform((value) => new Date(value)),
|
||||
stargazers_count: z.number(),
|
||||
open_issues_count: z.number(),
|
||||
forks_count: z.number(),
|
||||
})
|
||||
.transform((resp) => ({
|
||||
projectUrl: resp.html_url,
|
||||
projectDescription: resp.description,
|
||||
isFork: resp.fork,
|
||||
isArchived: resp.archived,
|
||||
createdAt: resp.created_at,
|
||||
starsCount: resp.stargazers_count,
|
||||
openIssues: resp.open_issues_count,
|
||||
forksCount: resp.forks_count,
|
||||
}))
|
||||
.safeParse(response);
|
||||
},
|
||||
getReleasesUrl(identifier) {
|
||||
return `${this.getDetailsUrl(identifier)}/releases`;
|
||||
},
|
||||
parseReleasesResponse(response) {
|
||||
return z
|
||||
.array(
|
||||
z
|
||||
.object({
|
||||
tag_name: z.string(),
|
||||
published_at: z.string().transform((value) => new Date(value)),
|
||||
html_url: z.string(),
|
||||
body: z.string(),
|
||||
prerelease: z.boolean(),
|
||||
})
|
||||
.transform((tag) => ({
|
||||
identifier: "",
|
||||
latestRelease: tag.tag_name,
|
||||
latestReleaseAt: tag.published_at,
|
||||
releaseUrl: tag.html_url,
|
||||
releaseDescription: tag.body,
|
||||
isPreRelease: tag.prerelease,
|
||||
})),
|
||||
)
|
||||
.safeParse(response);
|
||||
},
|
||||
},
|
||||
Gitlab: {
|
||||
getDetailsUrl(identifier) {
|
||||
return `https://gitlab.com/api/v4/projects/${encodeURIComponent(identifier)}`;
|
||||
},
|
||||
parseDetailsResponse(response) {
|
||||
return z
|
||||
.object({
|
||||
web_url: z.string(),
|
||||
description: z.string(),
|
||||
forked_from_project: z.object({ id: z.number() }).nullable(),
|
||||
archived: z.boolean(),
|
||||
created_at: z.string().transform((value) => new Date(value)),
|
||||
star_count: z.number(),
|
||||
open_issues_count: z.number(),
|
||||
forks_count: z.number(),
|
||||
})
|
||||
.transform((resp) => ({
|
||||
projectUrl: resp.web_url,
|
||||
projectDescription: resp.description,
|
||||
isFork: resp.forked_from_project !== null,
|
||||
isArchived: resp.archived,
|
||||
createdAt: resp.created_at,
|
||||
starsCount: resp.star_count,
|
||||
openIssues: resp.open_issues_count,
|
||||
forksCount: resp.forks_count,
|
||||
}))
|
||||
.safeParse(response);
|
||||
},
|
||||
getReleasesUrl(identifier) {
|
||||
return `${this.getDetailsUrl(identifier)}/releases`;
|
||||
},
|
||||
parseReleasesResponse(response) {
|
||||
return z
|
||||
.array(
|
||||
z
|
||||
.object({
|
||||
name: z.string(),
|
||||
released_at: z.string().transform((value) => new Date(value)),
|
||||
description: z.string(),
|
||||
_links: z.object({ self: z.string() }),
|
||||
upcoming_release: z.boolean(),
|
||||
})
|
||||
.transform((tag) => ({
|
||||
identifier: "",
|
||||
latestRelease: tag.name,
|
||||
latestReleaseAt: tag.released_at,
|
||||
releaseUrl: tag._links.self,
|
||||
releaseDescription: tag.description,
|
||||
isPreRelease: tag.upcoming_release,
|
||||
})),
|
||||
)
|
||||
.safeParse(response);
|
||||
},
|
||||
},
|
||||
Npm: {
|
||||
getDetailsUrl(_) {
|
||||
return undefined;
|
||||
},
|
||||
parseDetailsResponse(_) {
|
||||
return undefined;
|
||||
},
|
||||
getReleasesUrl(identifier) {
|
||||
return `https://registry.npmjs.org/${encodeURIComponent(identifier)}`;
|
||||
},
|
||||
parseReleasesResponse(response) {
|
||||
return z
|
||||
.object({
|
||||
time: z.record(z.string().transform((value) => new Date(value))).transform((version) =>
|
||||
Object.entries(version).map(([key, value]) => ({
|
||||
identifier: "",
|
||||
latestRelease: key,
|
||||
latestReleaseAt: value,
|
||||
})),
|
||||
),
|
||||
versions: z.record(z.object({ description: z.string() })),
|
||||
name: z.string(),
|
||||
})
|
||||
.transform((resp) => {
|
||||
return resp.time.map((release) => ({
|
||||
...release,
|
||||
releaseUrl: `https://www.npmjs.com/package/${resp.name}/v/${release.latestRelease}`,
|
||||
releaseDescription: resp.versions[release.latestRelease]?.description ?? "",
|
||||
isPreRelease: false,
|
||||
}));
|
||||
})
|
||||
.safeParse(response);
|
||||
},
|
||||
},
|
||||
Codeberg: {
|
||||
getDetailsUrl(identifier) {
|
||||
const [owner, name] = identifier.split("/");
|
||||
if (!owner || !name) {
|
||||
return "";
|
||||
}
|
||||
return `https://codeberg.org/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}`;
|
||||
},
|
||||
parseDetailsResponse(response) {
|
||||
return z
|
||||
.object({
|
||||
html_url: z.string(),
|
||||
description: z.string(),
|
||||
fork: z.boolean(),
|
||||
archived: z.boolean(),
|
||||
created_at: z.string().transform((value) => new Date(value)),
|
||||
stars_count: z.number(),
|
||||
open_issues_count: z.number(),
|
||||
forks_count: z.number(),
|
||||
})
|
||||
.transform((resp) => ({
|
||||
projectUrl: resp.html_url,
|
||||
projectDescription: resp.description,
|
||||
isFork: resp.fork,
|
||||
isArchived: resp.archived,
|
||||
createdAt: resp.created_at,
|
||||
starsCount: resp.stars_count,
|
||||
openIssues: resp.open_issues_count,
|
||||
forksCount: resp.forks_count,
|
||||
}))
|
||||
.safeParse(response);
|
||||
},
|
||||
getReleasesUrl(identifier) {
|
||||
return `${this.getDetailsUrl(identifier)}/releases`;
|
||||
},
|
||||
parseReleasesResponse(response) {
|
||||
return z
|
||||
.array(
|
||||
z
|
||||
.object({
|
||||
tag_name: z.string(),
|
||||
published_at: z.string().transform((value) => new Date(value)),
|
||||
url: z.string(),
|
||||
body: z.string(),
|
||||
prerelease: z.boolean(),
|
||||
})
|
||||
.transform((tag) => ({
|
||||
latestRelease: tag.tag_name,
|
||||
latestReleaseAt: tag.published_at,
|
||||
releaseUrl: tag.url,
|
||||
releaseDescription: tag.body,
|
||||
isPreRelease: tag.prerelease,
|
||||
})),
|
||||
)
|
||||
.safeParse(response);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const _detailsSchema = z.object({
|
||||
projectUrl: z.string(),
|
||||
projectDescription: z.string(),
|
||||
isFork: z.boolean(),
|
||||
isArchived: z.boolean(),
|
||||
createdAt: z.date(),
|
||||
starsCount: z.number(),
|
||||
openIssues: z.number(),
|
||||
forksCount: z.number(),
|
||||
});
|
||||
|
||||
const _releasesSchema = z.object({
|
||||
latestRelease: z.string(),
|
||||
latestReleaseAt: z.date(),
|
||||
releaseUrl: z.string(),
|
||||
releaseDescription: z.string(),
|
||||
isPreRelease: z.boolean(),
|
||||
});
|
||||
|
||||
export type DetailsResponse = z.infer<typeof _detailsSchema>;
|
||||
|
||||
export type ReleasesResponse = z.infer<typeof _releasesSchema>;
|
||||
105
packages/request-handler/src/releases.ts
Normal file
105
packages/request-handler/src/releases.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import dayjs from "dayjs";
|
||||
import { z } from "zod";
|
||||
|
||||
import { fetchWithTimeout } from "@homarr/common";
|
||||
import { logger } from "@homarr/log";
|
||||
|
||||
import { createCachedWidgetRequestHandler } from "./lib/cached-widget-request-handler";
|
||||
import { Providers } from "./releases-providers";
|
||||
import type { DetailsResponse } from "./releases-providers";
|
||||
|
||||
const _reponseSchema = z.object({
|
||||
identifier: z.string(),
|
||||
providerKey: z.string(),
|
||||
latestRelease: z.string(),
|
||||
latestReleaseAt: z.date(),
|
||||
releaseUrl: z.string(),
|
||||
releaseDescription: z.string(),
|
||||
isPreRelease: z.boolean(),
|
||||
projectUrl: z.string(),
|
||||
projectDescription: z.string(),
|
||||
isFork: z.boolean(),
|
||||
isArchived: z.boolean(),
|
||||
createdAt: z.date(),
|
||||
starsCount: z.number(),
|
||||
openIssues: z.number(),
|
||||
forksCount: z.number(),
|
||||
});
|
||||
|
||||
export const releasesRequestHandler = createCachedWidgetRequestHandler({
|
||||
queryKey: "releasesApiResult",
|
||||
widgetKind: "releases",
|
||||
async requestAsync(input: { providerKey: string; identifier: string; versionRegex: string | undefined }) {
|
||||
const provider = Providers[input.providerKey];
|
||||
|
||||
if (!provider) return undefined;
|
||||
|
||||
let detailsResult: DetailsResponse = {
|
||||
projectUrl: "",
|
||||
projectDescription: "",
|
||||
isFork: false,
|
||||
isArchived: false,
|
||||
createdAt: new Date(0),
|
||||
starsCount: 0,
|
||||
openIssues: 0,
|
||||
forksCount: 0,
|
||||
};
|
||||
|
||||
const detailsUrl = provider.getDetailsUrl(input.identifier);
|
||||
if (detailsUrl !== undefined) {
|
||||
const detailsResponse = await fetchWithTimeout(detailsUrl);
|
||||
const parsedDetails = provider.parseDetailsResponse(await detailsResponse.json());
|
||||
|
||||
if (parsedDetails?.success) {
|
||||
detailsResult = parsedDetails.data;
|
||||
} else {
|
||||
logger.warn("Failed to parse details response", {
|
||||
provider: input.providerKey,
|
||||
identifier: input.identifier,
|
||||
detailsUrl,
|
||||
error: parsedDetails?.error,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const releasesResponse = await fetchWithTimeout(provider.getReleasesUrl(input.identifier));
|
||||
const releasesResult = provider.parseReleasesResponse(await releasesResponse.json());
|
||||
|
||||
if (!releasesResult.success) return undefined;
|
||||
|
||||
const latest: ResponseResponse = releasesResult.data
|
||||
.filter((result) => (input.versionRegex ? new RegExp(input.versionRegex).test(result.latestRelease) : true))
|
||||
.reduce(
|
||||
(latest, result) => {
|
||||
return {
|
||||
...detailsResult,
|
||||
...(result.latestReleaseAt > latest.latestReleaseAt ? result : latest),
|
||||
identifier: input.identifier,
|
||||
providerKey: input.providerKey,
|
||||
};
|
||||
},
|
||||
{
|
||||
identifier: "",
|
||||
providerKey: "",
|
||||
latestRelease: "",
|
||||
latestReleaseAt: new Date(0),
|
||||
releaseUrl: "",
|
||||
releaseDescription: "",
|
||||
isPreRelease: false,
|
||||
projectUrl: "",
|
||||
projectDescription: "",
|
||||
isFork: false,
|
||||
isArchived: false,
|
||||
createdAt: new Date(0),
|
||||
starsCount: 0,
|
||||
openIssues: 0,
|
||||
forksCount: 0,
|
||||
},
|
||||
);
|
||||
|
||||
return latest;
|
||||
},
|
||||
cacheDuration: dayjs.duration(5, "minutes"),
|
||||
});
|
||||
|
||||
export type ResponseResponse = z.infer<typeof _reponseSchema>;
|
||||
@@ -29,7 +29,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.25.0",
|
||||
"eslint": "^9.25.1",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"@homarr/api": "workspace:^0.1.0",
|
||||
"@homarr/db": "workspace:^0.1.0",
|
||||
"@homarr/server-settings": "workspace:^0.1.0",
|
||||
"@mantine/dates": "^7.17.4",
|
||||
"@mantine/dates": "^7.17.5",
|
||||
"next": "15.3.1",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0"
|
||||
@@ -34,7 +34,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.25.0",
|
||||
"eslint": "^9.25.1",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,9 +33,9 @@
|
||||
"@homarr/settings": "workspace:^0.1.0",
|
||||
"@homarr/translation": "workspace:^0.1.0",
|
||||
"@homarr/ui": "workspace:^0.1.0",
|
||||
"@mantine/core": "^7.17.4",
|
||||
"@mantine/hooks": "^7.17.4",
|
||||
"@mantine/spotlight": "^7.17.4",
|
||||
"@mantine/core": "^7.17.5",
|
||||
"@mantine/hooks": "^7.17.5",
|
||||
"@mantine/spotlight": "^7.17.5",
|
||||
"@tabler/icons-react": "^3.31.0",
|
||||
"jotai": "^2.12.3",
|
||||
"next": "15.3.1",
|
||||
@@ -47,7 +47,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.25.0",
|
||||
"eslint": "^9.25.1",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
"deepmerge": "4.3.1",
|
||||
"mantine-react-table": "2.0.0-beta.9",
|
||||
"next": "15.3.1",
|
||||
"next-intl": "4.0.2",
|
||||
"next-intl": "4.1.0",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0"
|
||||
},
|
||||
@@ -41,7 +41,7 @@
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.25.0",
|
||||
"eslint": "^9.25.1",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1092,6 +1092,9 @@
|
||||
"integrations": {
|
||||
"label": ""
|
||||
},
|
||||
"title": {
|
||||
"label": ""
|
||||
},
|
||||
"customCssClasses": {
|
||||
"label": ""
|
||||
},
|
||||
@@ -1766,7 +1769,12 @@
|
||||
"mediaServer": {
|
||||
"name": "",
|
||||
"description": "",
|
||||
"option": {},
|
||||
"option": {
|
||||
"showOnlyPlaying": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"items": {
|
||||
"currentlyPlaying": "",
|
||||
"user": "",
|
||||
@@ -3800,6 +3808,10 @@
|
||||
"noResults": {
|
||||
"title": ""
|
||||
},
|
||||
"invalid": {
|
||||
"title": "",
|
||||
"description": ""
|
||||
},
|
||||
"expires": ""
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1092,6 +1092,9 @@
|
||||
"integrations": {
|
||||
"label": "集成"
|
||||
},
|
||||
"title": {
|
||||
"label": ""
|
||||
},
|
||||
"customCssClasses": {
|
||||
"label": "自定义 CSS 类"
|
||||
},
|
||||
@@ -1766,7 +1769,12 @@
|
||||
"mediaServer": {
|
||||
"name": "当前媒体服务流",
|
||||
"description": "显示媒体服务器上的当前流",
|
||||
"option": {},
|
||||
"option": {
|
||||
"showOnlyPlaying": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"items": {
|
||||
"currentlyPlaying": "正在播放",
|
||||
"user": "用户",
|
||||
@@ -3800,6 +3808,10 @@
|
||||
"noResults": {
|
||||
"title": "还没有证书"
|
||||
},
|
||||
"invalid": {
|
||||
"title": "",
|
||||
"description": ""
|
||||
},
|
||||
"expires": "到期时间 {when}"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1092,6 +1092,9 @@
|
||||
"integrations": {
|
||||
"label": ""
|
||||
},
|
||||
"title": {
|
||||
"label": ""
|
||||
},
|
||||
"customCssClasses": {
|
||||
"label": ""
|
||||
},
|
||||
@@ -1766,7 +1769,12 @@
|
||||
"mediaServer": {
|
||||
"name": "",
|
||||
"description": "",
|
||||
"option": {},
|
||||
"option": {
|
||||
"showOnlyPlaying": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"items": {
|
||||
"currentlyPlaying": "",
|
||||
"user": "Uživatel",
|
||||
@@ -3800,6 +3808,10 @@
|
||||
"noResults": {
|
||||
"title": ""
|
||||
},
|
||||
"invalid": {
|
||||
"title": "",
|
||||
"description": ""
|
||||
},
|
||||
"expires": ""
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1092,6 +1092,9 @@
|
||||
"integrations": {
|
||||
"label": "Integrationer"
|
||||
},
|
||||
"title": {
|
||||
"label": ""
|
||||
},
|
||||
"customCssClasses": {
|
||||
"label": "Brugerdefinerede CSS-klasser"
|
||||
},
|
||||
@@ -1766,7 +1769,12 @@
|
||||
"mediaServer": {
|
||||
"name": "Aktuelle medieserver streams",
|
||||
"description": "Vis de aktuelle streams på dine medieservere",
|
||||
"option": {},
|
||||
"option": {
|
||||
"showOnlyPlaying": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"items": {
|
||||
"currentlyPlaying": "Afspiller lige nu",
|
||||
"user": "Bruger",
|
||||
@@ -3800,6 +3808,10 @@
|
||||
"noResults": {
|
||||
"title": "Der er endnu ingen certifikater"
|
||||
},
|
||||
"invalid": {
|
||||
"title": "",
|
||||
"description": ""
|
||||
},
|
||||
"expires": "Udløber {when}"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1092,6 +1092,9 @@
|
||||
"integrations": {
|
||||
"label": "Integrationen"
|
||||
},
|
||||
"title": {
|
||||
"label": ""
|
||||
},
|
||||
"customCssClasses": {
|
||||
"label": "Benutzerdefinierte CSS Klassen"
|
||||
},
|
||||
@@ -1766,7 +1769,12 @@
|
||||
"mediaServer": {
|
||||
"name": "Aktuelle Media Server Streams",
|
||||
"description": "Zeige die aktuellen Streams auf deinen Medienservern an",
|
||||
"option": {},
|
||||
"option": {
|
||||
"showOnlyPlaying": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"items": {
|
||||
"currentlyPlaying": "Aktuelle Wiedergabe",
|
||||
"user": "Benutzer",
|
||||
@@ -3800,6 +3808,10 @@
|
||||
"noResults": {
|
||||
"title": "Es gibt noch keine Zertifikate"
|
||||
},
|
||||
"invalid": {
|
||||
"title": "",
|
||||
"description": ""
|
||||
},
|
||||
"expires": "Gültig bis {when}"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1092,6 +1092,9 @@
|
||||
"integrations": {
|
||||
"label": "Integrationen"
|
||||
},
|
||||
"title": {
|
||||
"label": ""
|
||||
},
|
||||
"customCssClasses": {
|
||||
"label": "Benutzerdefinierte CSS Klassen"
|
||||
},
|
||||
@@ -1766,7 +1769,12 @@
|
||||
"mediaServer": {
|
||||
"name": "Aktuelle Media Server Streams",
|
||||
"description": "Zeige die aktuellen Streams auf deinen Medienservern an",
|
||||
"option": {},
|
||||
"option": {
|
||||
"showOnlyPlaying": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"items": {
|
||||
"currentlyPlaying": "Aktuelle Wiedergabe",
|
||||
"user": "Benutzer",
|
||||
@@ -3800,6 +3808,10 @@
|
||||
"noResults": {
|
||||
"title": "Es gibt noch keine Zertifikate"
|
||||
},
|
||||
"invalid": {
|
||||
"title": "",
|
||||
"description": ""
|
||||
},
|
||||
"expires": "Gültig bis {when}"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1092,6 +1092,9 @@
|
||||
"integrations": {
|
||||
"label": ""
|
||||
},
|
||||
"title": {
|
||||
"label": ""
|
||||
},
|
||||
"customCssClasses": {
|
||||
"label": ""
|
||||
},
|
||||
@@ -1766,7 +1769,12 @@
|
||||
"mediaServer": {
|
||||
"name": "",
|
||||
"description": "",
|
||||
"option": {},
|
||||
"option": {
|
||||
"showOnlyPlaying": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"items": {
|
||||
"currentlyPlaying": "",
|
||||
"user": "",
|
||||
@@ -3800,6 +3808,10 @@
|
||||
"noResults": {
|
||||
"title": ""
|
||||
},
|
||||
"invalid": {
|
||||
"title": "",
|
||||
"description": ""
|
||||
},
|
||||
"expires": ""
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1092,6 +1092,9 @@
|
||||
"integrations": {
|
||||
"label": ""
|
||||
},
|
||||
"title": {
|
||||
"label": ""
|
||||
},
|
||||
"customCssClasses": {
|
||||
"label": ""
|
||||
},
|
||||
@@ -1766,7 +1769,12 @@
|
||||
"mediaServer": {
|
||||
"name": "",
|
||||
"description": "",
|
||||
"option": {},
|
||||
"option": {
|
||||
"showOnlyPlaying": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"items": {
|
||||
"currentlyPlaying": "",
|
||||
"user": "",
|
||||
@@ -3800,6 +3808,10 @@
|
||||
"noResults": {
|
||||
"title": ""
|
||||
},
|
||||
"invalid": {
|
||||
"title": "",
|
||||
"description": ""
|
||||
},
|
||||
"expires": ""
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1092,6 +1092,9 @@
|
||||
"integrations": {
|
||||
"label": "Integrations"
|
||||
},
|
||||
"title": {
|
||||
"label": "Title"
|
||||
},
|
||||
"customCssClasses": {
|
||||
"label": "Custom css classes"
|
||||
},
|
||||
@@ -1766,7 +1769,12 @@
|
||||
"mediaServer": {
|
||||
"name": "Current media server streams",
|
||||
"description": "Show the current streams on your media servers",
|
||||
"option": {},
|
||||
"option": {
|
||||
"showOnlyPlaying": {
|
||||
"label": "Show only currently playing",
|
||||
"description": "Disabling this will not work for plex"
|
||||
}
|
||||
},
|
||||
"items": {
|
||||
"currentlyPlaying": "Currently playing",
|
||||
"user": "User",
|
||||
@@ -2044,6 +2052,85 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"releases": {
|
||||
"name": "Releases",
|
||||
"description": "Displays a list of the current version of the given repositories with the given version regex.",
|
||||
"option": {
|
||||
"newReleaseWithin": {
|
||||
"label": "New Release Within",
|
||||
"description": "Usage example: 1w (1 week), 10m (10 months). Accepted unit types h (hours), d (days), w (weeks), m (months), y (years). Leave empty for no highlighting of new releases."
|
||||
},
|
||||
"staleReleaseWithin": {
|
||||
"label": "Stale Release Within",
|
||||
"description": "Usage example: 1w (1 week), 10m (10 months). Accepted unit types h (hours), d (days), w (weeks), m (months), y (years). Leave empty for no highlighting of stale releases."
|
||||
},
|
||||
"showOnlyHighlighted": {
|
||||
"label": "Show Only Highlighted",
|
||||
"description": "Show only new or stale releases. As per the above."
|
||||
},
|
||||
"showDetails": {
|
||||
"label": "Show Details"
|
||||
},
|
||||
"repositories": {
|
||||
"label": "Repositories",
|
||||
"addRRepository": {
|
||||
"label": "Add repository"
|
||||
},
|
||||
"provider": {
|
||||
"label": "Provider"
|
||||
},
|
||||
"identifier": {
|
||||
"label": "Identifier",
|
||||
"placeholder": "Name or Owner/Name"
|
||||
},
|
||||
"versionFilter": {
|
||||
"label": "Version Filter",
|
||||
"prefix": {
|
||||
"label": "Prefix"
|
||||
},
|
||||
"precision": {
|
||||
"label": "Precision",
|
||||
"options": {
|
||||
"none": "None"
|
||||
}
|
||||
},
|
||||
"suffix": {
|
||||
"label": "Suffix"
|
||||
},
|
||||
"regex": {
|
||||
"label": "Regular Expression"
|
||||
}
|
||||
},
|
||||
"edit": {
|
||||
"label": "Edit"
|
||||
},
|
||||
"editForm": {
|
||||
"title": "Edit Repository",
|
||||
"cancel": {
|
||||
"label": "Cancel"
|
||||
},
|
||||
"confirm": {
|
||||
"label": "Confirm"
|
||||
}
|
||||
},
|
||||
"example": {
|
||||
"label": "Example"
|
||||
},
|
||||
"invalid": "Invalid repository definition, please check the values"
|
||||
}
|
||||
},
|
||||
"not-found": "Not Found",
|
||||
"pre-release": "Pre-Release",
|
||||
"archived": "Archived",
|
||||
"forked": "Forked",
|
||||
"starsCount": "Stars",
|
||||
"forksCount": "Forks",
|
||||
"issuesCount": "Open Issues",
|
||||
"openProjectPage": "Open Project Page",
|
||||
"openReleasePage": "Open Release Page",
|
||||
"releaseDescription": "Release Description",
|
||||
"created": "Created"
|
||||
},
|
||||
"networkControllerSummary": {
|
||||
"option": {},
|
||||
"card": {
|
||||
@@ -3800,6 +3887,10 @@
|
||||
"noResults": {
|
||||
"title": "There are no certificates yet"
|
||||
},
|
||||
"invalid": {
|
||||
"title": "Invalid certificate",
|
||||
"description": "Failed to parse certificate"
|
||||
},
|
||||
"expires": "Expires {when}"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1092,6 +1092,9 @@
|
||||
"integrations": {
|
||||
"label": ""
|
||||
},
|
||||
"title": {
|
||||
"label": ""
|
||||
},
|
||||
"customCssClasses": {
|
||||
"label": ""
|
||||
},
|
||||
@@ -1766,7 +1769,12 @@
|
||||
"mediaServer": {
|
||||
"name": "",
|
||||
"description": "",
|
||||
"option": {},
|
||||
"option": {
|
||||
"showOnlyPlaying": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"items": {
|
||||
"currentlyPlaying": "",
|
||||
"user": "",
|
||||
@@ -3800,6 +3808,10 @@
|
||||
"noResults": {
|
||||
"title": ""
|
||||
},
|
||||
"invalid": {
|
||||
"title": "",
|
||||
"description": ""
|
||||
},
|
||||
"expires": ""
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1092,6 +1092,9 @@
|
||||
"integrations": {
|
||||
"label": ""
|
||||
},
|
||||
"title": {
|
||||
"label": ""
|
||||
},
|
||||
"customCssClasses": {
|
||||
"label": ""
|
||||
},
|
||||
@@ -1766,7 +1769,12 @@
|
||||
"mediaServer": {
|
||||
"name": "",
|
||||
"description": "",
|
||||
"option": {},
|
||||
"option": {
|
||||
"showOnlyPlaying": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"items": {
|
||||
"currentlyPlaying": "",
|
||||
"user": "",
|
||||
@@ -3800,6 +3808,10 @@
|
||||
"noResults": {
|
||||
"title": ""
|
||||
},
|
||||
"invalid": {
|
||||
"title": "",
|
||||
"description": ""
|
||||
},
|
||||
"expires": ""
|
||||
}
|
||||
},
|
||||
|
||||
@@ -612,17 +612,17 @@
|
||||
"select": {
|
||||
"label": "Sélectionner l'app",
|
||||
"notFound": "Aucune app trouvée",
|
||||
"search": "",
|
||||
"noResults": "",
|
||||
"action": "",
|
||||
"title": ""
|
||||
"search": "Rechercher une application",
|
||||
"noResults": "Pas de résultats",
|
||||
"action": "Sélectionner {app}",
|
||||
"title": "Sélectionner une application à ajouter à ce tableau"
|
||||
},
|
||||
"create": {
|
||||
"title": "",
|
||||
"description": "",
|
||||
"title": "Créer une nouvelle application",
|
||||
"description": "Créer une nouvelle application ",
|
||||
"action": ""
|
||||
},
|
||||
"add": ""
|
||||
"add": "Ajouter une application"
|
||||
}
|
||||
},
|
||||
"integration": {
|
||||
@@ -769,7 +769,7 @@
|
||||
"message": "Le chemin d'accès n'est probablement pas correct"
|
||||
},
|
||||
"tooManyRequests": {
|
||||
"title": "",
|
||||
"title": "Trop de requêtes en un temps donné",
|
||||
"message": ""
|
||||
}
|
||||
}
|
||||
@@ -995,7 +995,7 @@
|
||||
},
|
||||
"option": {
|
||||
"title": {
|
||||
"label": ""
|
||||
"label": "Titre"
|
||||
},
|
||||
"borderColor": {
|
||||
"label": "Couleur de la bordure"
|
||||
@@ -1092,6 +1092,9 @@
|
||||
"integrations": {
|
||||
"label": "Intégrations"
|
||||
},
|
||||
"title": {
|
||||
"label": "Titre"
|
||||
},
|
||||
"customCssClasses": {
|
||||
"label": "Classes CSS personnalisées"
|
||||
},
|
||||
@@ -1442,8 +1445,8 @@
|
||||
}
|
||||
},
|
||||
"stockPrice": {
|
||||
"name": "",
|
||||
"description": "",
|
||||
"name": "Cours des actions",
|
||||
"description": "Affiche le cours des actions d'une entreprise",
|
||||
"option": {
|
||||
"stock": {
|
||||
"label": ""
|
||||
@@ -1452,66 +1455,66 @@
|
||||
"label": "",
|
||||
"option": {
|
||||
"1d": {
|
||||
"label": ""
|
||||
"label": "1 jour"
|
||||
},
|
||||
"5d": {
|
||||
"label": ""
|
||||
"label": "5 jours"
|
||||
},
|
||||
"1mo": {
|
||||
"label": ""
|
||||
"label": "1 mois"
|
||||
},
|
||||
"3mo": {
|
||||
"label": ""
|
||||
"label": "3 mois"
|
||||
},
|
||||
"6mo": {
|
||||
"label": ""
|
||||
"label": "6 mois"
|
||||
},
|
||||
"ytd": {
|
||||
"label": ""
|
||||
"label": "Année courante"
|
||||
},
|
||||
"1y": {
|
||||
"label": ""
|
||||
"label": "1 an"
|
||||
},
|
||||
"2y": {
|
||||
"label": ""
|
||||
"label": "2 ans"
|
||||
},
|
||||
"5y": {
|
||||
"label": ""
|
||||
"label": "5 ans"
|
||||
},
|
||||
"10y": {
|
||||
"label": ""
|
||||
"label": "10 ans"
|
||||
},
|
||||
"max": {
|
||||
"label": ""
|
||||
"label": "Maximum"
|
||||
}
|
||||
}
|
||||
},
|
||||
"timeInterval": {
|
||||
"label": "",
|
||||
"label": "Intervalle de temps",
|
||||
"option": {
|
||||
"5m": {
|
||||
"label": ""
|
||||
"label": "5 minutes"
|
||||
},
|
||||
"15m": {
|
||||
"label": ""
|
||||
"label": "15 minutes"
|
||||
},
|
||||
"30m": {
|
||||
"label": ""
|
||||
"label": "30 minutes"
|
||||
},
|
||||
"1h": {
|
||||
"label": ""
|
||||
"label": "1 heure"
|
||||
},
|
||||
"1d": {
|
||||
"label": ""
|
||||
"label": "1 jour"
|
||||
},
|
||||
"5d": {
|
||||
"label": ""
|
||||
"label": "5 jours"
|
||||
},
|
||||
"1wk": {
|
||||
"label": ""
|
||||
"label": "1 semaine"
|
||||
},
|
||||
"1mo": {
|
||||
"label": ""
|
||||
"label": "1 mois"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1766,7 +1769,12 @@
|
||||
"mediaServer": {
|
||||
"name": "Flux du serveur multimédia actuel",
|
||||
"description": "Afficher les flux en cours sur vos serveurs de multimédia",
|
||||
"option": {},
|
||||
"option": {
|
||||
"showOnlyPlaying": {
|
||||
"label": "Afficher seulement les médias en cours de lecture",
|
||||
"description": "Désactiver cette option ne fonctionnera pas pour Plex"
|
||||
}
|
||||
},
|
||||
"items": {
|
||||
"currentlyPlaying": "En cours de lecture",
|
||||
"user": "Utilisateur",
|
||||
@@ -3800,6 +3808,10 @@
|
||||
"noResults": {
|
||||
"title": "Il n'y a pas encore de certificats"
|
||||
},
|
||||
"invalid": {
|
||||
"title": "",
|
||||
"description": ""
|
||||
},
|
||||
"expires": "Expire le {when}"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1092,6 +1092,9 @@
|
||||
"integrations": {
|
||||
"label": "אינטגרציות"
|
||||
},
|
||||
"title": {
|
||||
"label": ""
|
||||
},
|
||||
"customCssClasses": {
|
||||
"label": "מחלקות עיצוב מותאמות אישית"
|
||||
},
|
||||
@@ -1766,7 +1769,12 @@
|
||||
"mediaServer": {
|
||||
"name": "זרמי שרת מדיה נוכחיים",
|
||||
"description": "הצג את הזרמים הנוכחיים בשרתי המדיה שלך",
|
||||
"option": {},
|
||||
"option": {
|
||||
"showOnlyPlaying": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"items": {
|
||||
"currentlyPlaying": "מתנגן כרגע",
|
||||
"user": "משתמש",
|
||||
@@ -3800,6 +3808,10 @@
|
||||
"noResults": {
|
||||
"title": "אין עדיין תעודות אבטחה"
|
||||
},
|
||||
"invalid": {
|
||||
"title": "",
|
||||
"description": ""
|
||||
},
|
||||
"expires": "פג ב- {when}"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1092,6 +1092,9 @@
|
||||
"integrations": {
|
||||
"label": ""
|
||||
},
|
||||
"title": {
|
||||
"label": ""
|
||||
},
|
||||
"customCssClasses": {
|
||||
"label": ""
|
||||
},
|
||||
@@ -1766,7 +1769,12 @@
|
||||
"mediaServer": {
|
||||
"name": "",
|
||||
"description": "",
|
||||
"option": {},
|
||||
"option": {
|
||||
"showOnlyPlaying": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"items": {
|
||||
"currentlyPlaying": "",
|
||||
"user": "",
|
||||
@@ -3800,6 +3808,10 @@
|
||||
"noResults": {
|
||||
"title": ""
|
||||
},
|
||||
"invalid": {
|
||||
"title": "",
|
||||
"description": ""
|
||||
},
|
||||
"expires": ""
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1092,6 +1092,9 @@
|
||||
"integrations": {
|
||||
"label": "Integrációk"
|
||||
},
|
||||
"title": {
|
||||
"label": ""
|
||||
},
|
||||
"customCssClasses": {
|
||||
"label": "Egyedi css osztályok"
|
||||
},
|
||||
@@ -1766,7 +1769,12 @@
|
||||
"mediaServer": {
|
||||
"name": "",
|
||||
"description": "",
|
||||
"option": {},
|
||||
"option": {
|
||||
"showOnlyPlaying": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"items": {
|
||||
"currentlyPlaying": "",
|
||||
"user": "",
|
||||
@@ -3800,6 +3808,10 @@
|
||||
"noResults": {
|
||||
"title": ""
|
||||
},
|
||||
"invalid": {
|
||||
"title": "",
|
||||
"description": ""
|
||||
},
|
||||
"expires": ""
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1092,6 +1092,9 @@
|
||||
"integrations": {
|
||||
"label": "Integrazioni"
|
||||
},
|
||||
"title": {
|
||||
"label": ""
|
||||
},
|
||||
"customCssClasses": {
|
||||
"label": ""
|
||||
},
|
||||
@@ -1766,7 +1769,12 @@
|
||||
"mediaServer": {
|
||||
"name": "",
|
||||
"description": "",
|
||||
"option": {},
|
||||
"option": {
|
||||
"showOnlyPlaying": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"items": {
|
||||
"currentlyPlaying": "",
|
||||
"user": "",
|
||||
@@ -3800,6 +3808,10 @@
|
||||
"noResults": {
|
||||
"title": ""
|
||||
},
|
||||
"invalid": {
|
||||
"title": "",
|
||||
"description": ""
|
||||
},
|
||||
"expires": ""
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1092,6 +1092,9 @@
|
||||
"integrations": {
|
||||
"label": ""
|
||||
},
|
||||
"title": {
|
||||
"label": ""
|
||||
},
|
||||
"customCssClasses": {
|
||||
"label": ""
|
||||
},
|
||||
@@ -1766,7 +1769,12 @@
|
||||
"mediaServer": {
|
||||
"name": "",
|
||||
"description": "",
|
||||
"option": {},
|
||||
"option": {
|
||||
"showOnlyPlaying": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"items": {
|
||||
"currentlyPlaying": "",
|
||||
"user": "",
|
||||
@@ -3800,6 +3808,10 @@
|
||||
"noResults": {
|
||||
"title": ""
|
||||
},
|
||||
"invalid": {
|
||||
"title": "",
|
||||
"description": ""
|
||||
},
|
||||
"expires": ""
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1092,6 +1092,9 @@
|
||||
"integrations": {
|
||||
"label": ""
|
||||
},
|
||||
"title": {
|
||||
"label": ""
|
||||
},
|
||||
"customCssClasses": {
|
||||
"label": ""
|
||||
},
|
||||
@@ -1766,7 +1769,12 @@
|
||||
"mediaServer": {
|
||||
"name": "",
|
||||
"description": "",
|
||||
"option": {},
|
||||
"option": {
|
||||
"showOnlyPlaying": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"items": {
|
||||
"currentlyPlaying": "",
|
||||
"user": "",
|
||||
@@ -3800,6 +3808,10 @@
|
||||
"noResults": {
|
||||
"title": ""
|
||||
},
|
||||
"invalid": {
|
||||
"title": "",
|
||||
"description": ""
|
||||
},
|
||||
"expires": ""
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1092,6 +1092,9 @@
|
||||
"integrations": {
|
||||
"label": ""
|
||||
},
|
||||
"title": {
|
||||
"label": ""
|
||||
},
|
||||
"customCssClasses": {
|
||||
"label": ""
|
||||
},
|
||||
@@ -1766,7 +1769,12 @@
|
||||
"mediaServer": {
|
||||
"name": "",
|
||||
"description": "",
|
||||
"option": {},
|
||||
"option": {
|
||||
"showOnlyPlaying": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"items": {
|
||||
"currentlyPlaying": "",
|
||||
"user": "",
|
||||
@@ -3800,6 +3808,10 @@
|
||||
"noResults": {
|
||||
"title": ""
|
||||
},
|
||||
"invalid": {
|
||||
"title": "",
|
||||
"description": ""
|
||||
},
|
||||
"expires": ""
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1092,6 +1092,9 @@
|
||||
"integrations": {
|
||||
"label": ""
|
||||
},
|
||||
"title": {
|
||||
"label": ""
|
||||
},
|
||||
"customCssClasses": {
|
||||
"label": ""
|
||||
},
|
||||
@@ -1766,7 +1769,12 @@
|
||||
"mediaServer": {
|
||||
"name": "",
|
||||
"description": "",
|
||||
"option": {},
|
||||
"option": {
|
||||
"showOnlyPlaying": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"items": {
|
||||
"currentlyPlaying": "",
|
||||
"user": "",
|
||||
@@ -3800,6 +3808,10 @@
|
||||
"noResults": {
|
||||
"title": ""
|
||||
},
|
||||
"invalid": {
|
||||
"title": "",
|
||||
"description": ""
|
||||
},
|
||||
"expires": ""
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1092,6 +1092,9 @@
|
||||
"integrations": {
|
||||
"label": "Integraties"
|
||||
},
|
||||
"title": {
|
||||
"label": ""
|
||||
},
|
||||
"customCssClasses": {
|
||||
"label": "Aangepaste CSS-classes"
|
||||
},
|
||||
@@ -1766,7 +1769,12 @@
|
||||
"mediaServer": {
|
||||
"name": "Huidige mediaserver streams",
|
||||
"description": "De huidige streams op je mediaservers weergeven",
|
||||
"option": {},
|
||||
"option": {
|
||||
"showOnlyPlaying": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"items": {
|
||||
"currentlyPlaying": "Momenteel aan het afspelen",
|
||||
"user": "Gebruiker",
|
||||
@@ -3800,6 +3808,10 @@
|
||||
"noResults": {
|
||||
"title": "Er zijn nog geen certificaten"
|
||||
},
|
||||
"invalid": {
|
||||
"title": "",
|
||||
"description": ""
|
||||
},
|
||||
"expires": "Verloopt {when}"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1092,6 +1092,9 @@
|
||||
"integrations": {
|
||||
"label": "Integrasjoner"
|
||||
},
|
||||
"title": {
|
||||
"label": ""
|
||||
},
|
||||
"customCssClasses": {
|
||||
"label": "Egendefinerte css-klasser"
|
||||
},
|
||||
@@ -1766,7 +1769,12 @@
|
||||
"mediaServer": {
|
||||
"name": "Pågående medieserver-strømmer",
|
||||
"description": "Vis pågående strømmer på dine media-servere",
|
||||
"option": {},
|
||||
"option": {
|
||||
"showOnlyPlaying": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"items": {
|
||||
"currentlyPlaying": "Spilles nå",
|
||||
"user": "Bruker",
|
||||
@@ -3800,6 +3808,10 @@
|
||||
"noResults": {
|
||||
"title": "Det er ingen sertifikater enda"
|
||||
},
|
||||
"invalid": {
|
||||
"title": "",
|
||||
"description": ""
|
||||
},
|
||||
"expires": "Utløper {when}"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1092,6 +1092,9 @@
|
||||
"integrations": {
|
||||
"label": "Integracje"
|
||||
},
|
||||
"title": {
|
||||
"label": ""
|
||||
},
|
||||
"customCssClasses": {
|
||||
"label": "Niestandardowe klasy CSS"
|
||||
},
|
||||
@@ -1766,7 +1769,12 @@
|
||||
"mediaServer": {
|
||||
"name": "Bieżące strumienie serwera multimediów",
|
||||
"description": "Pokaż bieżące strumienie na serwerach multimedialnych",
|
||||
"option": {},
|
||||
"option": {
|
||||
"showOnlyPlaying": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"items": {
|
||||
"currentlyPlaying": "",
|
||||
"user": "Użytkownik",
|
||||
@@ -3800,6 +3808,10 @@
|
||||
"noResults": {
|
||||
"title": ""
|
||||
},
|
||||
"invalid": {
|
||||
"title": "",
|
||||
"description": ""
|
||||
},
|
||||
"expires": ""
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1092,6 +1092,9 @@
|
||||
"integrations": {
|
||||
"label": ""
|
||||
},
|
||||
"title": {
|
||||
"label": ""
|
||||
},
|
||||
"customCssClasses": {
|
||||
"label": ""
|
||||
},
|
||||
@@ -1766,7 +1769,12 @@
|
||||
"mediaServer": {
|
||||
"name": "",
|
||||
"description": "",
|
||||
"option": {},
|
||||
"option": {
|
||||
"showOnlyPlaying": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"items": {
|
||||
"currentlyPlaying": "",
|
||||
"user": "",
|
||||
@@ -3800,6 +3808,10 @@
|
||||
"noResults": {
|
||||
"title": ""
|
||||
},
|
||||
"invalid": {
|
||||
"title": "",
|
||||
"description": ""
|
||||
},
|
||||
"expires": ""
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1092,6 +1092,9 @@
|
||||
"integrations": {
|
||||
"label": ""
|
||||
},
|
||||
"title": {
|
||||
"label": ""
|
||||
},
|
||||
"customCssClasses": {
|
||||
"label": ""
|
||||
},
|
||||
@@ -1766,7 +1769,12 @@
|
||||
"mediaServer": {
|
||||
"name": "",
|
||||
"description": "",
|
||||
"option": {},
|
||||
"option": {
|
||||
"showOnlyPlaying": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"items": {
|
||||
"currentlyPlaying": "",
|
||||
"user": "",
|
||||
@@ -3800,6 +3808,10 @@
|
||||
"noResults": {
|
||||
"title": ""
|
||||
},
|
||||
"invalid": {
|
||||
"title": "",
|
||||
"description": ""
|
||||
},
|
||||
"expires": ""
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1092,6 +1092,9 @@
|
||||
"integrations": {
|
||||
"label": "Интеграции"
|
||||
},
|
||||
"title": {
|
||||
"label": ""
|
||||
},
|
||||
"customCssClasses": {
|
||||
"label": "Пользовательские CSS классы"
|
||||
},
|
||||
@@ -1766,7 +1769,12 @@
|
||||
"mediaServer": {
|
||||
"name": "Активные просмотры",
|
||||
"description": "Отображает текущие сеансы воспроизведения на медиасерверах",
|
||||
"option": {},
|
||||
"option": {
|
||||
"showOnlyPlaying": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"items": {
|
||||
"currentlyPlaying": "",
|
||||
"user": "Пользователь",
|
||||
@@ -3800,6 +3808,10 @@
|
||||
"noResults": {
|
||||
"title": "Доверенные сертификаты отсутствуют"
|
||||
},
|
||||
"invalid": {
|
||||
"title": "",
|
||||
"description": ""
|
||||
},
|
||||
"expires": "Истекает {when}"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1092,6 +1092,9 @@
|
||||
"integrations": {
|
||||
"label": "Integrácie"
|
||||
},
|
||||
"title": {
|
||||
"label": ""
|
||||
},
|
||||
"customCssClasses": {
|
||||
"label": "Vlastné css triedy"
|
||||
},
|
||||
@@ -1766,7 +1769,12 @@
|
||||
"mediaServer": {
|
||||
"name": "Aktuálne streamy mediálneho servera",
|
||||
"description": "Zobrazte aktuálne streamy na vašich mediálnych serveroch",
|
||||
"option": {},
|
||||
"option": {
|
||||
"showOnlyPlaying": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"items": {
|
||||
"currentlyPlaying": "",
|
||||
"user": "Používateľ",
|
||||
@@ -3800,6 +3808,10 @@
|
||||
"noResults": {
|
||||
"title": ""
|
||||
},
|
||||
"invalid": {
|
||||
"title": "",
|
||||
"description": ""
|
||||
},
|
||||
"expires": ""
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1092,6 +1092,9 @@
|
||||
"integrations": {
|
||||
"label": ""
|
||||
},
|
||||
"title": {
|
||||
"label": ""
|
||||
},
|
||||
"customCssClasses": {
|
||||
"label": ""
|
||||
},
|
||||
@@ -1766,7 +1769,12 @@
|
||||
"mediaServer": {
|
||||
"name": "",
|
||||
"description": "",
|
||||
"option": {},
|
||||
"option": {
|
||||
"showOnlyPlaying": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"items": {
|
||||
"currentlyPlaying": "",
|
||||
"user": "",
|
||||
@@ -3800,6 +3808,10 @@
|
||||
"noResults": {
|
||||
"title": ""
|
||||
},
|
||||
"invalid": {
|
||||
"title": "",
|
||||
"description": ""
|
||||
},
|
||||
"expires": ""
|
||||
}
|
||||
},
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1092,6 +1092,9 @@
|
||||
"integrations": {
|
||||
"label": "Entegrasyonlar"
|
||||
},
|
||||
"title": {
|
||||
"label": ""
|
||||
},
|
||||
"customCssClasses": {
|
||||
"label": "Özel CSS Alanı"
|
||||
},
|
||||
@@ -1766,7 +1769,12 @@
|
||||
"mediaServer": {
|
||||
"name": "Güncel Medya Sunucusu Akışları",
|
||||
"description": "Medya sunucularınızdaki mevcut akışları gösterin",
|
||||
"option": {},
|
||||
"option": {
|
||||
"showOnlyPlaying": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"items": {
|
||||
"currentlyPlaying": "Şuan Oynatılan",
|
||||
"user": "Kullanıcı",
|
||||
@@ -3800,6 +3808,10 @@
|
||||
"noResults": {
|
||||
"title": "Henüz sertifika yok"
|
||||
},
|
||||
"invalid": {
|
||||
"title": "Geçersiz sertifika",
|
||||
"description": "Sertifika ayrıştırılamadı"
|
||||
},
|
||||
"expires": "{when} süresi doluyor"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1092,6 +1092,9 @@
|
||||
"integrations": {
|
||||
"label": "Інтеграції"
|
||||
},
|
||||
"title": {
|
||||
"label": ""
|
||||
},
|
||||
"customCssClasses": {
|
||||
"label": "Користувацькі css класи"
|
||||
},
|
||||
@@ -1766,7 +1769,12 @@
|
||||
"mediaServer": {
|
||||
"name": "Поточні потоки медіасервера",
|
||||
"description": "Показує поточні потоки з ваших медіасерверів",
|
||||
"option": {},
|
||||
"option": {
|
||||
"showOnlyPlaying": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"items": {
|
||||
"currentlyPlaying": "",
|
||||
"user": "Користувач",
|
||||
@@ -3800,6 +3808,10 @@
|
||||
"noResults": {
|
||||
"title": "Сертифікати відсутні"
|
||||
},
|
||||
"invalid": {
|
||||
"title": "",
|
||||
"description": ""
|
||||
},
|
||||
"expires": ""
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1092,6 +1092,9 @@
|
||||
"integrations": {
|
||||
"label": ""
|
||||
},
|
||||
"title": {
|
||||
"label": ""
|
||||
},
|
||||
"customCssClasses": {
|
||||
"label": ""
|
||||
},
|
||||
@@ -1766,7 +1769,12 @@
|
||||
"mediaServer": {
|
||||
"name": "",
|
||||
"description": "",
|
||||
"option": {},
|
||||
"option": {
|
||||
"showOnlyPlaying": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"items": {
|
||||
"currentlyPlaying": "",
|
||||
"user": "",
|
||||
@@ -3800,6 +3808,10 @@
|
||||
"noResults": {
|
||||
"title": ""
|
||||
},
|
||||
"invalid": {
|
||||
"title": "",
|
||||
"description": ""
|
||||
},
|
||||
"expires": ""
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1092,6 +1092,9 @@
|
||||
"integrations": {
|
||||
"label": "集成"
|
||||
},
|
||||
"title": {
|
||||
"label": ""
|
||||
},
|
||||
"customCssClasses": {
|
||||
"label": "自定義 CSS html"
|
||||
},
|
||||
@@ -1766,7 +1769,12 @@
|
||||
"mediaServer": {
|
||||
"name": "當前多媒體伺服器串流",
|
||||
"description": "顯示當前多媒體伺服器的串流",
|
||||
"option": {},
|
||||
"option": {
|
||||
"showOnlyPlaying": {
|
||||
"label": "",
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"items": {
|
||||
"currentlyPlaying": "目前播放中",
|
||||
"user": "使用者",
|
||||
@@ -3800,6 +3808,10 @@
|
||||
"noResults": {
|
||||
"title": "尚無憑證"
|
||||
},
|
||||
"invalid": {
|
||||
"title": "",
|
||||
"description": ""
|
||||
},
|
||||
"expires": "到期 {when}"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -29,9 +29,9 @@
|
||||
"@homarr/log": "workspace:^0.1.0",
|
||||
"@homarr/translation": "workspace:^0.1.0",
|
||||
"@homarr/validation": "workspace:^0.1.0",
|
||||
"@mantine/core": "^7.17.4",
|
||||
"@mantine/dates": "^7.17.4",
|
||||
"@mantine/hooks": "^7.17.4",
|
||||
"@mantine/core": "^7.17.5",
|
||||
"@mantine/dates": "^7.17.5",
|
||||
"@mantine/hooks": "^7.17.5",
|
||||
"@tabler/icons-react": "^3.31.0",
|
||||
"mantine-react-table": "2.0.0-beta.9",
|
||||
"next": "15.3.1",
|
||||
@@ -43,7 +43,7 @@
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"@types/css-modules": "^1.0.5",
|
||||
"eslint": "^9.25.0",
|
||||
"eslint": "^9.25.1",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import type { Property } from "csstype";
|
||||
import classes from "./masked-image.module.css";
|
||||
|
||||
interface MaskedImageProps {
|
||||
imageUrl: string;
|
||||
imageUrl?: string;
|
||||
color: MantineColor;
|
||||
alt?: string;
|
||||
style?: React.CSSProperties;
|
||||
@@ -41,7 +41,7 @@ export const MaskedImage = ({
|
||||
maskSize,
|
||||
maskRepeat,
|
||||
maskPosition,
|
||||
maskImage: `url(${imageUrl})`,
|
||||
maskImage: imageUrl ? `url(${imageUrl})` : undefined,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user