feat(board): allow to set icon color of widgets (#2228)
Co-authored-by: Andre Silva <asilva01@acuitysso.com>
This commit is contained in:
@@ -8,6 +8,10 @@
|
||||
transition: scale 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.appIcon:hover {
|
||||
.appWithUrl:hover > .appIcon {
|
||||
scale: 0.9;
|
||||
}
|
||||
|
||||
.appWithUrl:hover > div.appIcon {
|
||||
background-color: var(--mantine-color-iconColor-filled-hover);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { useRequiredBoard } from "@homarr/boards/context";
|
||||
import { useSettings } from "@homarr/settings";
|
||||
import { useRegisterSpotlightContextResults } from "@homarr/spotlight";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import { MaskedOrNormalImage } from "@homarr/ui";
|
||||
|
||||
import type { WidgetComponentProps } from "../definition";
|
||||
import classes from "./app.module.css";
|
||||
@@ -69,7 +70,7 @@ export default function AppWidget({ options, isEditMode }: WidgetComponentProps<
|
||||
styles={{ tooltip: { maxWidth: 300 } }}
|
||||
>
|
||||
<Flex
|
||||
className={combineClasses("app-flex-wrapper", app.name, app.id)}
|
||||
className={combineClasses("app-flex-wrapper", app.name, app.id, app.href && classes.appWithUrl)}
|
||||
h="100%"
|
||||
w="100%"
|
||||
direction="column"
|
||||
@@ -82,7 +83,16 @@ export default function AppWidget({ options, isEditMode }: WidgetComponentProps<
|
||||
{app.name}
|
||||
</Text>
|
||||
)}
|
||||
<img src={app.iconUrl} alt={app.name} className={combineClasses(classes.appIcon, "app-icon")} />
|
||||
<MaskedOrNormalImage
|
||||
imageUrl={app.iconUrl}
|
||||
hasColor={board.iconColor !== null}
|
||||
alt={app.name}
|
||||
className={combineClasses(classes.appIcon, "app-icon")}
|
||||
style={{
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
</Tooltip.Floating>
|
||||
{options.pingEnabled && !settings.forceDisableStatus && !board.disableStatus && app.href ? (
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
.card:hover {
|
||||
background-color: var(--mantine-color-primaryColor-light-hover);
|
||||
}
|
||||
|
||||
.card:hover > div > div.bookmarkIcon {
|
||||
background-color: var(--mantine-color-iconColor-filled-hover);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
"use client";
|
||||
|
||||
import { Anchor, Box, Card, Divider, Flex, Group, Image, Stack, Text, Title, UnstyledButton } from "@mantine/core";
|
||||
import { Anchor, Box, Card, Divider, Flex, Group, Stack, Text, Title, UnstyledButton } from "@mantine/core";
|
||||
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useRequiredBoard } from "@homarr/boards/context";
|
||||
import { useRegisterSpotlightContextResults } from "@homarr/spotlight";
|
||||
import { MaskedOrNormalImage } from "@homarr/ui";
|
||||
|
||||
import type { WidgetComponentProps } from "../definition";
|
||||
import classes from "./bookmark.module.css";
|
||||
|
||||
export default function BookmarksWidget({ options, width, height, itemId }: WidgetComponentProps<"bookmarks">) {
|
||||
const board = useRequiredBoard();
|
||||
const [data] = clientApi.app.byIds.useSuspenseQuery(options.items, {
|
||||
select(data) {
|
||||
return data.sort((appA, appB) => options.items.indexOf(appA.id) - options.items.indexOf(appB.id));
|
||||
@@ -50,6 +53,7 @@ export default function BookmarksWidget({ options, width, height, itemId }: Widg
|
||||
hideIcon={options.hideIcon}
|
||||
hideHostname={options.hideHostname}
|
||||
openNewTab={options.openNewTab}
|
||||
hasIconColor={board.iconColor !== null}
|
||||
/>
|
||||
)}
|
||||
{options.layout !== "grid" && (
|
||||
@@ -59,6 +63,7 @@ export default function BookmarksWidget({ options, width, height, itemId }: Widg
|
||||
hideIcon={options.hideIcon}
|
||||
hideHostname={options.hideHostname}
|
||||
openNewTab={options.openNewTab}
|
||||
hasIconColor={board.iconColor !== null}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
@@ -71,9 +76,10 @@ interface FlexLayoutProps {
|
||||
hideIcon: boolean;
|
||||
hideHostname: boolean;
|
||||
openNewTab: boolean;
|
||||
hasIconColor: boolean;
|
||||
}
|
||||
|
||||
const FlexLayout = ({ data, direction, hideIcon, hideHostname, openNewTab }: FlexLayoutProps) => {
|
||||
const FlexLayout = ({ data, direction, hideIcon, hideHostname, openNewTab, hasIconColor }: FlexLayoutProps) => {
|
||||
return (
|
||||
<Flex direction={direction} gap="0" h="100%" w="100%">
|
||||
{data.map((app, index) => (
|
||||
@@ -102,9 +108,9 @@ const FlexLayout = ({ data, direction, hideIcon, hideHostname, openNewTab }: Fle
|
||||
p={0}
|
||||
>
|
||||
{direction === "row" ? (
|
||||
<VerticalItem app={app} hideIcon={hideIcon} hideHostname={hideHostname} />
|
||||
<VerticalItem app={app} hideIcon={hideIcon} hideHostname={hideHostname} hasIconColor={hasIconColor} />
|
||||
) : (
|
||||
<HorizontalItem app={app} hideIcon={hideIcon} hideHostname={hideHostname} />
|
||||
<HorizontalItem app={app} hideIcon={hideIcon} hideHostname={hideHostname} hasIconColor={hasIconColor} />
|
||||
)}
|
||||
</Card>
|
||||
</UnstyledButton>
|
||||
@@ -121,9 +127,10 @@ interface GridLayoutProps {
|
||||
hideIcon: boolean;
|
||||
hideHostname: boolean;
|
||||
openNewTab: boolean;
|
||||
hasIconColor: boolean;
|
||||
}
|
||||
|
||||
const GridLayout = ({ data, width, height, hideIcon, hideHostname, openNewTab }: GridLayoutProps) => {
|
||||
const GridLayout = ({ data, width, height, hideIcon, hideHostname, openNewTab, hasIconColor }: GridLayoutProps) => {
|
||||
// Calculates the perfect number of columns for the grid layout based on the width and height in pixels and the number of items
|
||||
const columns = Math.ceil(Math.sqrt(data.length * (width / height)));
|
||||
|
||||
@@ -146,7 +153,7 @@ const GridLayout = ({ data, width, height, hideIcon, hideHostname, openNewTab }:
|
||||
h="100%"
|
||||
>
|
||||
<Card withBorder style={{ containerType: "size" }} h="100%" className={classes.card} p="5cqmin">
|
||||
<VerticalItem app={app} hideIcon={hideIcon} hideHostname={hideHostname} />
|
||||
<VerticalItem app={app} hideIcon={hideIcon} hideHostname={hideHostname} hasIconColor={hasIconColor} />
|
||||
</Card>
|
||||
</UnstyledButton>
|
||||
))}
|
||||
@@ -158,10 +165,12 @@ const VerticalItem = ({
|
||||
app,
|
||||
hideIcon,
|
||||
hideHostname,
|
||||
hasIconColor,
|
||||
}: {
|
||||
app: RouterOutputs["app"]["byIds"][number];
|
||||
hideIcon: boolean;
|
||||
hideHostname: boolean;
|
||||
hasIconColor: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<Stack h="100%" gap="5cqmin">
|
||||
@@ -169,17 +178,18 @@ const VerticalItem = ({
|
||||
{app.name}
|
||||
</Text>
|
||||
{!hideIcon && (
|
||||
<Image
|
||||
<MaskedOrNormalImage
|
||||
imageUrl={app.iconUrl}
|
||||
hasColor={hasIconColor}
|
||||
alt={app.name}
|
||||
className={classes.bookmarkIcon}
|
||||
style={{
|
||||
maxHeight: "100%",
|
||||
maxWidth: "100%",
|
||||
overflow: "auto",
|
||||
flex: 1,
|
||||
objectFit: "contain",
|
||||
scale: 0.8,
|
||||
}}
|
||||
src={app.iconUrl}
|
||||
alt={app.name}
|
||||
/>
|
||||
)}
|
||||
{!hideHostname && (
|
||||
@@ -195,26 +205,29 @@ const HorizontalItem = ({
|
||||
app,
|
||||
hideIcon,
|
||||
hideHostname,
|
||||
hasIconColor,
|
||||
}: {
|
||||
app: RouterOutputs["app"]["byIds"][number];
|
||||
hideIcon: boolean;
|
||||
hideHostname: boolean;
|
||||
hasIconColor: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<Group wrap="nowrap">
|
||||
{!hideIcon && (
|
||||
<Image
|
||||
<MaskedOrNormalImage
|
||||
imageUrl={app.iconUrl}
|
||||
hasColor={hasIconColor}
|
||||
alt={app.name}
|
||||
className={classes.bookmarkIcon}
|
||||
style={{
|
||||
overflow: "auto",
|
||||
objectFit: "contain",
|
||||
scale: 0.8,
|
||||
minHeight: "100cqh",
|
||||
maxHeight: "100cqh",
|
||||
minWidth: "100cqh",
|
||||
maxWidth: "100cqh",
|
||||
}}
|
||||
src={app.iconUrl}
|
||||
alt={app.name}
|
||||
/>
|
||||
)}
|
||||
<Stack justify="space-between" gap={0}>
|
||||
|
||||
@@ -3,29 +3,19 @@
|
||||
import "../../widgets-common.css";
|
||||
|
||||
import { useState } from "react";
|
||||
import {
|
||||
ActionIcon,
|
||||
Badge,
|
||||
Button,
|
||||
Card,
|
||||
Flex,
|
||||
Image,
|
||||
ScrollArea,
|
||||
Stack,
|
||||
Text,
|
||||
Tooltip,
|
||||
UnstyledButton,
|
||||
} from "@mantine/core";
|
||||
import { ActionIcon, Badge, Button, Card, Flex, ScrollArea, Stack, Text, Tooltip, UnstyledButton } from "@mantine/core";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
import { IconCircleFilled, IconClockPause, IconPlayerPlay, IconPlayerStop } from "@tabler/icons-react";
|
||||
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useIntegrationsWithInteractAccess } from "@homarr/auth/client";
|
||||
import { useRequiredBoard } from "@homarr/boards/context";
|
||||
import { useIntegrationConnected } from "@homarr/common";
|
||||
import { integrationDefs } from "@homarr/definitions";
|
||||
import type { TranslationFunction } from "@homarr/translation";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import { MaskedOrNormalImage } from "@homarr/ui";
|
||||
|
||||
import type { widgetKind } from ".";
|
||||
import type { WidgetComponentProps } from "../../definition";
|
||||
@@ -39,6 +29,7 @@ export default function DnsHoleControlsWidget({
|
||||
integrationIds,
|
||||
isEditMode,
|
||||
}: WidgetComponentProps<typeof widgetKind>) {
|
||||
const board = useRequiredBoard();
|
||||
// DnsHole integrations with interaction permissions
|
||||
const integrationsWithInteractions = useIntegrationsWithInteractAccess()
|
||||
.map(({ id }) => id)
|
||||
@@ -275,6 +266,7 @@ export default function DnsHoleControlsWidget({
|
||||
setSelectedIntegrationIds={setSelectedIntegrationIds}
|
||||
open={open}
|
||||
t={t}
|
||||
hasIconColor={board.iconColor !== null}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
@@ -297,6 +289,7 @@ interface ControlsCardProps {
|
||||
setSelectedIntegrationIds: (integrationId: string[]) => void;
|
||||
open: () => void;
|
||||
t: TranslationFunction;
|
||||
hasIconColor: boolean;
|
||||
}
|
||||
|
||||
const ControlsCard: React.FC<ControlsCardProps> = ({
|
||||
@@ -306,6 +299,7 @@ const ControlsCard: React.FC<ControlsCardProps> = ({
|
||||
setSelectedIntegrationIds,
|
||||
open,
|
||||
t,
|
||||
hasIconColor,
|
||||
}) => {
|
||||
const isConnected = useIntegrationConnected(data.integration.updatedAt, { timeout: 30000 });
|
||||
const isEnabled = data.summary.status ? data.summary.status === "enabled" : undefined;
|
||||
@@ -313,6 +307,8 @@ const ControlsCard: React.FC<ControlsCardProps> = ({
|
||||
// Use all factors to infer the state of the action buttons
|
||||
const controlEnabled = isInteractPermitted && isEnabled !== undefined && isConnected;
|
||||
|
||||
const iconUrl = integrationDefs[data.integration.kind].iconUrl;
|
||||
|
||||
return (
|
||||
<Card
|
||||
className={`dns-hole-controls-integration-item-outer-shell dns-hole-controls-integration-item-${data.integration.id} dns-hole-controls-integration-item-${data.integration.name}`}
|
||||
@@ -322,13 +318,16 @@ const ControlsCard: React.FC<ControlsCardProps> = ({
|
||||
radius="2.5cqmin"
|
||||
>
|
||||
<Flex className="dns-hole-controls-item-container" gap="4cqmin" align="center" direction="row">
|
||||
<Image
|
||||
<MaskedOrNormalImage
|
||||
imageUrl={iconUrl}
|
||||
hasColor={hasIconColor}
|
||||
alt={data.integration.name}
|
||||
className="dns-hole-controls-item-icon"
|
||||
src={integrationDefs[data.integration.kind].iconUrl}
|
||||
w="20cqmin"
|
||||
h="20cqmin"
|
||||
fit="contain"
|
||||
style={{ filter: !isConnected ? "grayscale(100%)" : undefined }}
|
||||
style={{
|
||||
height: "20cqmin",
|
||||
width: "20cqmin",
|
||||
filter: !isConnected ? "grayscale(100%)" : undefined,
|
||||
}}
|
||||
/>
|
||||
<Flex className="dns-hole-controls-item-data-stack" direction="column" gap="1.5cqmin">
|
||||
<Text className="dns-hole-controls-item-integration-name" fz="7cqmin">
|
||||
|
||||
Reference in New Issue
Block a user