diff --git a/apps/nextjs/package.json b/apps/nextjs/package.json index 1a1324323..83bc199ee 100644 --- a/apps/nextjs/package.json +++ b/apps/nextjs/package.json @@ -67,7 +67,6 @@ "@types/react": "^18.2.76", "@types/react-dom": "^18.2.25", "@types/chroma-js": "2.4.4", - "dotenv-cli": "^7.4.1", "concurrently": "^8.2.2", "eslint": "^8.57.0", "prettier": "^3.2.5", diff --git a/packages/definitions/src/widget.ts b/packages/definitions/src/widget.ts index 72f71fad4..66186acfc 100644 --- a/packages/definitions/src/widget.ts +++ b/packages/definitions/src/widget.ts @@ -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]; diff --git a/packages/translation/src/lang/en.ts b/packages/translation/src/lang/en.ts index ba09f37df..bd6963876 100644 --- a/packages/translation/src/lang/en.ts +++ b/packages/translation/src/lang/en.ts @@ -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: { name: "Weather", description: diff --git a/packages/widgets/src/iframe/component.module.css b/packages/widgets/src/iframe/component.module.css new file mode 100644 index 000000000..68bc009ab --- /dev/null +++ b/packages/widgets/src/iframe/component.module.css @@ -0,0 +1,8 @@ +.iframe { + border-radius: var(--mantine-radius-sm); + width: 100%; + height: 100%; + border: none; + background: none; + background-color: transparent; +} diff --git a/packages/widgets/src/iframe/component.tsx b/packages/widgets/src/iframe/component.tsx new file mode 100644 index 000000000..df8a54244 --- /dev/null +++ b/packages/widgets/src/iframe/component.tsx @@ -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 ; + + return ( + + + + ); +} + +const NoUrl = () => { + const t = useI18n(); + + return ( + + + {t("widget.iframe.error.noUrl")} + + ); +}; + +const getAllowedPermissions = ( + permissions: Omit["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["options"], "embedUrl">, + string +>; diff --git a/packages/widgets/src/iframe/index.ts b/packages/widgets/src/iframe/index.ts new file mode 100644 index 000000000..2af7c5587 --- /dev/null +++ b/packages/widgets/src/iframe/index.ts @@ -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")); diff --git a/packages/widgets/src/index.tsx b/packages/widgets/src/index.tsx index 6fece044d..2ccde0c45 100644 --- a/packages/widgets/src/index.tsx +++ b/packages/widgets/src/index.tsx @@ -8,6 +8,7 @@ import { Loader as UiLoader } from "@homarr/ui"; import * as app from "./app"; import * as clock from "./clock"; import type { WidgetComponentProps } from "./definition"; +import * as iframe from "./iframe"; import type { WidgetImportRecord } from "./import"; import * as video from "./video"; import * as weather from "./weather"; @@ -22,6 +23,7 @@ export const widgetImports = { clock, weather, app, + iframe, video, } satisfies WidgetImportRecord; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5a303233d..86d4b5467 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -207,9 +207,6 @@ importers: concurrently: specifier: ^8.2.2 version: 8.2.2 - dotenv-cli: - specifier: ^7.4.1 - version: 7.4.1 eslint: specifier: ^8.57.0 version: 8.57.0