feat: add iframe widget (#291)
* feat: add nestjs replacement, remove nestjs * feat: add iframe widget * fix: format issue * fix: format issue * fix: format issue * fix: lockfile frozen
This commit is contained in:
@@ -67,7 +67,6 @@
|
|||||||
"@types/react": "^18.2.76",
|
"@types/react": "^18.2.76",
|
||||||
"@types/react-dom": "^18.2.25",
|
"@types/react-dom": "^18.2.25",
|
||||||
"@types/chroma-js": "2.4.4",
|
"@types/chroma-js": "2.4.4",
|
||||||
"dotenv-cli": "^7.4.1",
|
|
||||||
"concurrently": "^8.2.2",
|
"concurrently": "^8.2.2",
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^8.57.0",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
|
|||||||
@@ -1,2 +1,8 @@
|
|||||||
export const widgetKinds = ["clock", "weather", "app", "video"] as const;
|
export const widgetKinds = [
|
||||||
|
"clock",
|
||||||
|
"weather",
|
||||||
|
"app",
|
||||||
|
"iframe",
|
||||||
|
"video",
|
||||||
|
] as const;
|
||||||
export type WidgetKind = (typeof widgetKinds)[number];
|
export type WidgetKind = (typeof widgetKinds)[number];
|
||||||
|
|||||||
@@ -372,6 +372,45 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
iframe: {
|
||||||
|
name: "iFrame",
|
||||||
|
description:
|
||||||
|
"Embed any content from the internet. Some websites may restrict access.",
|
||||||
|
option: {
|
||||||
|
embedUrl: {
|
||||||
|
label: "Embed URL",
|
||||||
|
},
|
||||||
|
allowFullScreen: {
|
||||||
|
label: "Allow full screen",
|
||||||
|
},
|
||||||
|
allowTransparency: {
|
||||||
|
label: "Allow transparency",
|
||||||
|
},
|
||||||
|
allowScrolling: {
|
||||||
|
label: "Allow scrolling",
|
||||||
|
},
|
||||||
|
allowPayment: {
|
||||||
|
label: "Allow payment",
|
||||||
|
},
|
||||||
|
allowAutoPlay: {
|
||||||
|
label: "Allow auto play",
|
||||||
|
},
|
||||||
|
allowMicrophone: {
|
||||||
|
label: "Allow microphone",
|
||||||
|
},
|
||||||
|
allowCamera: {
|
||||||
|
label: "Allow camera",
|
||||||
|
},
|
||||||
|
allowGeolocation: {
|
||||||
|
label: "Allow geolocation",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
noUrl: "No iFrame URL provided",
|
||||||
|
noBrowerSupport:
|
||||||
|
"Your Browser does not support iframes. Please update your browser.",
|
||||||
|
},
|
||||||
|
},
|
||||||
weather: {
|
weather: {
|
||||||
name: "Weather",
|
name: "Weather",
|
||||||
description:
|
description:
|
||||||
|
|||||||
8
packages/widgets/src/iframe/component.module.css
Normal file
8
packages/widgets/src/iframe/component.module.css
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
.iframe {
|
||||||
|
border-radius: var(--mantine-radius-sm);
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
62
packages/widgets/src/iframe/component.tsx
Normal file
62
packages/widgets/src/iframe/component.tsx
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { objectEntries } from "@homarr/common";
|
||||||
|
import { useI18n } from "@homarr/translation/client";
|
||||||
|
import { Box, IconBrowserOff, Stack, Text, Title } from "@homarr/ui";
|
||||||
|
|
||||||
|
import type { WidgetComponentProps } from "../definition";
|
||||||
|
import classes from "./component.module.css";
|
||||||
|
|
||||||
|
export default function IFrameWidget({
|
||||||
|
options,
|
||||||
|
}: WidgetComponentProps<"iframe">) {
|
||||||
|
const t = useI18n();
|
||||||
|
const { embedUrl, ...permissions } = options;
|
||||||
|
const allowedPermissions = getAllowedPermissions(permissions);
|
||||||
|
|
||||||
|
if (embedUrl.trim() === "") return <NoUrl />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box h="100%" w="100%">
|
||||||
|
<iframe
|
||||||
|
className={classes.iframe}
|
||||||
|
src={embedUrl}
|
||||||
|
title="widget iframe"
|
||||||
|
allow={allowedPermissions.join(" ")}
|
||||||
|
>
|
||||||
|
<Text>{t("widget.iframe.error.noBrowerSupport")}</Text>
|
||||||
|
</iframe>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const NoUrl = () => {
|
||||||
|
const t = useI18n();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack align="center" justify="center" h="100%">
|
||||||
|
<IconBrowserOff />
|
||||||
|
<Title order={4}>{t("widget.iframe.error.noUrl")}</Title>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAllowedPermissions = (
|
||||||
|
permissions: Omit<WidgetComponentProps<"iframe">["options"], "embedUrl">,
|
||||||
|
) => {
|
||||||
|
return objectEntries(permissions)
|
||||||
|
.filter(([_key, value]) => value)
|
||||||
|
.map(([key]) => permissionMapping[key]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const permissionMapping = {
|
||||||
|
allowAutoPlay: "autoplay",
|
||||||
|
allowCamera: "camera",
|
||||||
|
allowFullScreen: "fullscreen",
|
||||||
|
allowGeolocation: "geolocation",
|
||||||
|
allowMicrophone: "microphone",
|
||||||
|
allowPayment: "payment",
|
||||||
|
allowScrolling: "scrolling",
|
||||||
|
allowTransparency: "transparency",
|
||||||
|
} satisfies Record<
|
||||||
|
keyof Omit<WidgetComponentProps<"iframe">["options"], "embedUrl">,
|
||||||
|
string
|
||||||
|
>;
|
||||||
24
packages/widgets/src/iframe/index.ts
Normal file
24
packages/widgets/src/iframe/index.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { IconBrowser } from "@homarr/ui";
|
||||||
|
|
||||||
|
import { createWidgetDefinition } from "../definition";
|
||||||
|
import { optionsBuilder } from "../options";
|
||||||
|
|
||||||
|
export const { definition, componentLoader } = createWidgetDefinition(
|
||||||
|
"iframe",
|
||||||
|
{
|
||||||
|
icon: IconBrowser,
|
||||||
|
options: optionsBuilder.from((factory) => ({
|
||||||
|
embedUrl: factory.text(),
|
||||||
|
allowFullScreen: factory.switch(),
|
||||||
|
allowScrolling: factory.switch({
|
||||||
|
defaultValue: true,
|
||||||
|
}),
|
||||||
|
allowTransparency: factory.switch(),
|
||||||
|
allowPayment: factory.switch(),
|
||||||
|
allowAutoPlay: factory.switch(),
|
||||||
|
allowMicrophone: factory.switch(),
|
||||||
|
allowCamera: factory.switch(),
|
||||||
|
allowGeolocation: factory.switch(),
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
).withDynamicImport(() => import("./component"));
|
||||||
@@ -8,6 +8,7 @@ import { Loader as UiLoader } from "@homarr/ui";
|
|||||||
import * as app from "./app";
|
import * as app from "./app";
|
||||||
import * as clock from "./clock";
|
import * as clock from "./clock";
|
||||||
import type { WidgetComponentProps } from "./definition";
|
import type { WidgetComponentProps } from "./definition";
|
||||||
|
import * as iframe from "./iframe";
|
||||||
import type { WidgetImportRecord } from "./import";
|
import type { WidgetImportRecord } from "./import";
|
||||||
import * as video from "./video";
|
import * as video from "./video";
|
||||||
import * as weather from "./weather";
|
import * as weather from "./weather";
|
||||||
@@ -22,6 +23,7 @@ export const widgetImports = {
|
|||||||
clock,
|
clock,
|
||||||
weather,
|
weather,
|
||||||
app,
|
app,
|
||||||
|
iframe,
|
||||||
video,
|
video,
|
||||||
} satisfies WidgetImportRecord;
|
} satisfies WidgetImportRecord;
|
||||||
|
|
||||||
|
|||||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@@ -207,9 +207,6 @@ importers:
|
|||||||
concurrently:
|
concurrently:
|
||||||
specifier: ^8.2.2
|
specifier: ^8.2.2
|
||||||
version: 8.2.2
|
version: 8.2.2
|
||||||
dotenv-cli:
|
|
||||||
specifier: ^7.4.1
|
|
||||||
version: 7.4.1
|
|
||||||
eslint:
|
eslint:
|
||||||
specifier: ^8.57.0
|
specifier: ^8.57.0
|
||||||
version: 8.57.0
|
version: 8.57.0
|
||||||
|
|||||||
Reference in New Issue
Block a user