feat: add video widget (#287)

* feat: add nestjs replacement, remove nestjs

* feat: add video widget

* feat: add notice about youtube not supported with video.js

* fix: format issue

* fix: format issue
This commit is contained in:
Meier Lukas
2024-04-13 11:44:16 +02:00
committed by GitHub
parent 80d2d485b8
commit 82e9887f36
9 changed files with 355 additions and 3 deletions

View File

@@ -9,6 +9,7 @@ import * as app from "./app";
import * as clock from "./clock";
import type { WidgetComponentProps } from "./definition";
import type { WidgetImportRecord } from "./import";
import * as video from "./video";
import * as weather from "./weather";
export { reduceWidgetOptionsWithDefaultValues } from "./options";
@@ -21,6 +22,7 @@ export const widgetImports = {
clock,
weather,
app,
video,
} satisfies WidgetImportRecord;
export type WidgetImports = typeof widgetImports;

View File

@@ -0,0 +1,6 @@
.video {
height: 100%;
width: 100%;
border-radius: var(--mantine-radius-md);
overflow: hidden;
}

View File

@@ -0,0 +1,98 @@
"use client";
import { useEffect, useRef } from "react";
import combineClasses from "clsx";
import videojs from "video.js";
import { useI18n } from "@homarr/translation/client";
import {
Anchor,
Center,
Group,
IconBrandYoutube,
IconDeviceCctvOff,
Stack,
Title,
} from "@homarr/ui";
import type { WidgetComponentProps } from "../definition";
import classes from "./component.module.css";
import "video.js/dist/video-js.css";
export default function VideoWidget({
options,
}: WidgetComponentProps<"video">) {
if (options.feedUrl.trim() === "") {
return <NoUrl />;
}
if (options.feedUrl.trim().startsWith("https://www.youtube.com/watch")) {
return <ForYoutubeUseIframe />;
}
return <Feed options={options} />;
}
const NoUrl = () => {
const t = useI18n();
return (
<Center h="100%">
<Stack align="center">
<IconDeviceCctvOff />
<Title order={4}>{t("widget.video.error.noUrl")}</Title>
</Stack>
</Center>
);
};
const ForYoutubeUseIframe = () => {
const t = useI18n();
return (
<Center h="100%">
<Stack align="center" gap="xs">
<IconBrandYoutube />
<Title order={4}>{t("widget.video.error.forYoutubeUseIframe")}</Title>
<Anchor href="https://homarr.dev/docs/widgets/iframe/">
{t("common.action.checkoutDocs")}
</Anchor>
</Stack>
</Center>
);
};
const Feed = ({ options }: Pick<WidgetComponentProps<"video">, "options">) => {
const videoRef = useRef<HTMLVideoElement>(null);
useEffect(() => {
if (!videoRef.current) {
return;
}
// Initialize Video.js player if it's not already initialized
if (!("player" in videoRef.current)) {
videojs(
videoRef.current,
{
autoplay: options.hasAutoPlay,
muted: options.isMuted,
controls: options.hasControls,
},
() => undefined,
);
}
}, [videoRef]);
return (
<Group justify="center" w="100%" h="100%" pos="relative">
<video
className={combineClasses("video-js", classes.video)}
ref={videoRef}
>
<source src={options.feedUrl} />
</video>
</Group>
);
};

View File

@@ -0,0 +1,20 @@
import { IconDeviceCctv } from "@homarr/ui";
import { createWidgetDefinition } from "../definition";
import { optionsBuilder } from "../options";
export const { definition, componentLoader } = createWidgetDefinition("video", {
icon: IconDeviceCctv,
options: optionsBuilder.from((factory) => ({
feedUrl: factory.text({
defaultValue: "",
}),
hasAutoPlay: factory.switch({
withDescription: true,
}),
isMuted: factory.switch({
defaultValue: true,
}),
hasControls: factory.switch(),
})),
}).withDynamicImport(() => import("./component"));