mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-09 15:05:48 +01:00
🐛 App tile flex fix (#1255)
* 🎨 flex * 🎨 Improved flex organization on app tile * ✏️ disallowAppNameProgagation to Propagation * ✨ User customizable lineclamp and config migration
This commit is contained in:
@@ -72,6 +72,10 @@
|
|||||||
"bottom":"Bottom",
|
"bottom":"Bottom",
|
||||||
"left":"Left"
|
"left":"Left"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"lineClampAppName":{
|
||||||
|
"label":"App Name Line Clamp",
|
||||||
|
"description":"Defines on how many lines your title should fit at it's maximum. Set 0 for unlimited."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"integration": {
|
"integration": {
|
||||||
|
|||||||
@@ -205,7 +205,7 @@ export const EditAppModal = ({
|
|||||||
<NetworkTab form={form} />
|
<NetworkTab form={form} />
|
||||||
<AppearanceTab
|
<AppearanceTab
|
||||||
form={form}
|
form={form}
|
||||||
disallowAppNameProgagation={() => setAllowAppNamePropagation(false)}
|
disallowAppNamePropagation={() => setAllowAppNamePropagation(false)}
|
||||||
allowAppNamePropagation={allowAppNamePropagation}
|
allowAppNamePropagation={allowAppNamePropagation}
|
||||||
/>
|
/>
|
||||||
<IntegrationTab form={form} />
|
<IntegrationTab form={form} />
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Flex, Select, Stack, Switch, Tabs } from '@mantine/core';
|
import { Flex, NumberInput, Select, Stack, Switch, Tabs } from '@mantine/core';
|
||||||
import { UseFormReturnType } from '@mantine/form';
|
import { UseFormReturnType } from '@mantine/form';
|
||||||
import { useDebouncedValue } from '@mantine/hooks';
|
import { useDebouncedValue } from '@mantine/hooks';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
@@ -9,13 +9,13 @@ import { IconSelector } from '../../../../../IconSelector/IconSelector';
|
|||||||
|
|
||||||
interface AppearanceTabProps {
|
interface AppearanceTabProps {
|
||||||
form: UseFormReturnType<AppType, (values: AppType) => AppType>;
|
form: UseFormReturnType<AppType, (values: AppType) => AppType>;
|
||||||
disallowAppNameProgagation: () => void;
|
disallowAppNamePropagation: () => void;
|
||||||
allowAppNamePropagation: boolean;
|
allowAppNamePropagation: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AppearanceTab = ({
|
export const AppearanceTab = ({
|
||||||
form,
|
form,
|
||||||
disallowAppNameProgagation,
|
disallowAppNamePropagation,
|
||||||
allowAppNamePropagation,
|
allowAppNamePropagation,
|
||||||
}: AppearanceTabProps) => {
|
}: AppearanceTabProps) => {
|
||||||
const iconSelectorRef = useRef();
|
const iconSelectorRef = useRef();
|
||||||
@@ -46,7 +46,7 @@ export const AppearanceTab = ({
|
|||||||
defaultValue={form.values.appearance.iconUrl}
|
defaultValue={form.values.appearance.iconUrl}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
form.setFieldValue('appearance.iconUrl', value);
|
form.setFieldValue('appearance.iconUrl', value);
|
||||||
disallowAppNameProgagation();
|
disallowAppNamePropagation();
|
||||||
}}
|
}}
|
||||||
value={form.values.appearance.iconUrl}
|
value={form.values.appearance.iconUrl}
|
||||||
ref={iconSelectorRef}
|
ref={iconSelectorRef}
|
||||||
@@ -66,11 +66,14 @@ export const AppearanceTab = ({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{form.values.appearance.appNameStatus === 'normal' && (
|
{form.values.appearance.appNameStatus === 'normal' && (
|
||||||
|
<>
|
||||||
<Select
|
<Select
|
||||||
label={t('appearance.positionAppName.label')}
|
label={t('appearance.positionAppName.label')}
|
||||||
description={t('appearance.positionAppName.description')}
|
description={t('appearance.positionAppName.description')}
|
||||||
data={[
|
data={[
|
||||||
{ value: 'column', label: t('appearance.positionAppName.dropdown.top') as string },
|
{
|
||||||
|
value: 'column',
|
||||||
|
label: t('appearance.positionAppName.dropdown.top') as string },
|
||||||
{
|
{
|
||||||
value: 'row-reverse',
|
value: 'row-reverse',
|
||||||
label: t('appearance.positionAppName.dropdown.right') as string,
|
label: t('appearance.positionAppName.dropdown.right') as string,
|
||||||
@@ -79,13 +82,25 @@ export const AppearanceTab = ({
|
|||||||
value: 'column-reverse',
|
value: 'column-reverse',
|
||||||
label: t('appearance.positionAppName.dropdown.bottom') as string,
|
label: t('appearance.positionAppName.dropdown.bottom') as string,
|
||||||
},
|
},
|
||||||
{ value: 'row', label: t('appearance.positionAppName.dropdown.left') as string },
|
{
|
||||||
|
value: 'row',
|
||||||
|
label: t('appearance.positionAppName.dropdown.left') as string },
|
||||||
]}
|
]}
|
||||||
{...form.getInputProps('appearance.positionAppName')}
|
{...form.getInputProps('appearance.positionAppName')}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
form.setFieldValue('appearance.positionAppName', value);
|
form.setFieldValue('appearance.positionAppName', value);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<NumberInput
|
||||||
|
label={t('appearance.lineClampAppName.label')}
|
||||||
|
description={t('appearance.lineClampAppName.description')}
|
||||||
|
min={0}
|
||||||
|
{...form.getInputProps('appearance.lineClampAppName')}
|
||||||
|
onChange={(value) => {
|
||||||
|
form.setFieldValue('appearance.lineClampAppName', value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Tabs.Panel>
|
</Tabs.Panel>
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ export const AvailableElementTypes = ({
|
|||||||
iconUrl: '/imgs/logo/logo.png',
|
iconUrl: '/imgs/logo/logo.png',
|
||||||
appNameStatus: 'normal',
|
appNameStatus: 'normal',
|
||||||
positionAppName: 'column',
|
positionAppName: 'column',
|
||||||
|
lineClampAppName: 1,
|
||||||
},
|
},
|
||||||
network: {
|
network: {
|
||||||
enabledStatusChecker: true,
|
enabledStatusChecker: true,
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export const AppPing = ({ app }: AppPingProps) => {
|
|||||||
<motion.div
|
<motion.div
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
bottom: replaceDotWithIcon ? 5 : 20,
|
bottom: replaceDotWithIcon ? 0 : 20,
|
||||||
right: replaceDotWithIcon ? 8 : 20,
|
right: replaceDotWithIcon ? 8 : 20,
|
||||||
zIndex: 2,
|
zIndex: 2,
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { Box, Flex, Text, Tooltip, UnstyledButton } from '@mantine/core';
|
import { Affix, Box, Text, Tooltip, UnstyledButton } from '@mantine/core';
|
||||||
import { createStyles, useMantineTheme } from '@mantine/styles';
|
import { createStyles, useMantineTheme } from '@mantine/styles';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
import { AppType } from '../../../../types/app';
|
import { AppType } from '../../../../types/app';
|
||||||
import { useCardStyles } from '../../../layout/useCardStyles';
|
|
||||||
import { useEditModeStore } from '../../Views/useEditModeStore';
|
import { useEditModeStore } from '../../Views/useEditModeStore';
|
||||||
import { HomarrCardWrapper } from '../HomarrCardWrapper';
|
import { HomarrCardWrapper } from '../HomarrCardWrapper';
|
||||||
import { BaseTileProps } from '../type';
|
import { BaseTileProps } from '../type';
|
||||||
@@ -17,89 +16,70 @@ interface AppTileProps extends BaseTileProps {
|
|||||||
|
|
||||||
export const AppTile = ({ className, app }: AppTileProps) => {
|
export const AppTile = ({ className, app }: AppTileProps) => {
|
||||||
const isEditMode = useEditModeStore((x) => x.enabled);
|
const isEditMode = useEditModeStore((x) => x.enabled);
|
||||||
|
|
||||||
const { cx, classes } = useStyles();
|
const { cx, classes } = useStyles();
|
||||||
|
|
||||||
const { colorScheme } = useMantineTheme();
|
const { colorScheme } = useMantineTheme();
|
||||||
|
|
||||||
const tooltipContent = [
|
const tooltipContent = [
|
||||||
app.appearance.appNameStatus === "hover" ? app.name : undefined,
|
app.appearance.appNameStatus === 'hover' ? app.name : undefined,
|
||||||
app.behaviour.tooltipDescription
|
app.behaviour.tooltipDescription,
|
||||||
].filter( e => e ).join( ': ' );
|
]
|
||||||
|
.filter((e) => e)
|
||||||
|
.join(': ');
|
||||||
|
|
||||||
const {
|
const isRow = app.appearance.positionAppName.includes('row');
|
||||||
classes: { card: cardClass },
|
|
||||||
} = useCardStyles(false);
|
|
||||||
|
|
||||||
function Inner() {
|
function Inner() {
|
||||||
return (
|
return (
|
||||||
<Tooltip.Floating
|
<Tooltip.Floating
|
||||||
label={tooltipContent}
|
label={tooltipContent}
|
||||||
position="right-start"
|
position="right-start"
|
||||||
c={ colorScheme === 'light' ? "black" : "dark.0" }
|
c={colorScheme === 'light' ? 'black' : 'dark.0'}
|
||||||
color={ colorScheme === 'light' ? "gray.2" : "dark.4" }
|
color={colorScheme === 'light' ? 'gray.2' : 'dark.4'}
|
||||||
multiline
|
multiline
|
||||||
disabled={tooltipContent === ''}
|
disabled={!tooltipContent}
|
||||||
styles={{ tooltip: { '&': { maxWidth: 300, }, }, }}
|
styles={{ tooltip: { maxWidth: 300 } }}
|
||||||
>
|
>
|
||||||
<Flex
|
<Box
|
||||||
m={0}
|
className={`${classes.base} ${cx(classes.appContent, 'dashboard-tile-app')}`}
|
||||||
p={0}
|
|
||||||
justify="space-around"
|
|
||||||
align="center"
|
|
||||||
h="100%"
|
h="100%"
|
||||||
w="100%"
|
sx={{
|
||||||
className="dashboard-tile-app"
|
flexFlow: app.appearance.positionAppName ?? 'column',
|
||||||
direction={app.appearance.positionAppName ?? 'column'}
|
}}
|
||||||
>
|
>
|
||||||
<Box w="100%" hidden={["hover", "hidden"].includes(app.appearance.appNameStatus)}>
|
{app.appearance.appNameStatus === 'normal' && (
|
||||||
<Text
|
<Text
|
||||||
w="100%"
|
className={cx(classes.appName, 'dashboard-tile-app-title')}
|
||||||
|
fw={700}
|
||||||
size="md"
|
size="md"
|
||||||
ta="center"
|
ta="center"
|
||||||
weight={700}
|
sx={{
|
||||||
className={cx(classes.appName, 'dashboard-tile-app-title')}
|
flex: isRow ? '1' : undefined,
|
||||||
lineClamp={1}
|
}}
|
||||||
|
lineClamp={app.appearance.lineClampAppName}
|
||||||
>
|
>
|
||||||
{app.name}
|
{app.name}
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
)}
|
||||||
<Box
|
|
||||||
w="100%"
|
|
||||||
h="100%"
|
|
||||||
display="flex"
|
|
||||||
sx={{
|
|
||||||
alignContent: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
flex: '1 1 auto',
|
|
||||||
flexWrap: 'wrap',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<motion.img
|
<motion.img
|
||||||
className={classes.image}
|
className={cx(classes.appImage, 'dashboard-tile-app-image')}
|
||||||
height="85%"
|
|
||||||
style={{
|
|
||||||
objectFit: 'contain',
|
|
||||||
}}
|
|
||||||
src={app.appearance.iconUrl}
|
src={app.appearance.iconUrl}
|
||||||
alt={app.name}
|
alt={app.name}
|
||||||
whileHover={{
|
whileHover={{ scale: 1 }}
|
||||||
scale: 1.2,
|
initial={{ scale: 0.9 }}
|
||||||
transition: { duration: 0.2 },
|
style={{
|
||||||
|
width: isRow ? 0 : undefined,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</Flex>
|
|
||||||
</Tooltip.Floating>
|
</Tooltip.Floating>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HomarrCardWrapper className={className}>
|
<HomarrCardWrapper className={className} p={10}>
|
||||||
<AppMenu app={app} />
|
<AppMenu app={app} />
|
||||||
{!app.url || isEditMode ? (
|
{!app.url || isEditMode ? (
|
||||||
<UnstyledButton
|
<UnstyledButton
|
||||||
className={classes.button}
|
className={`${classes.button} ${classes.base}`}
|
||||||
style={{ pointerEvents: isEditMode ? 'none' : 'auto' }}
|
style={{ pointerEvents: isEditMode ? 'none' : 'auto' }}
|
||||||
>
|
>
|
||||||
<Inner />
|
<Inner />
|
||||||
@@ -110,7 +90,7 @@ export const AppTile = ({ className, app }: AppTileProps) => {
|
|||||||
component={Link}
|
component={Link}
|
||||||
href={app.behaviour.externalUrl.length > 0 ? app.behaviour.externalUrl : app.url}
|
href={app.behaviour.externalUrl.length > 0 ? app.behaviour.externalUrl : app.url}
|
||||||
target={app.behaviour.isOpeningNewTab ? '_blank' : '_self'}
|
target={app.behaviour.isOpeningNewTab ? '_blank' : '_self'}
|
||||||
className={cx(classes.button)}
|
className={`${classes.button} ${classes.base}`}
|
||||||
>
|
>
|
||||||
<Inner />
|
<Inner />
|
||||||
</UnstyledButton>
|
</UnstyledButton>
|
||||||
@@ -121,19 +101,27 @@ export const AppTile = ({ className, app }: AppTileProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const useStyles = createStyles((theme, _params, getRef) => ({
|
const useStyles = createStyles((theme, _params, getRef) => ({
|
||||||
image: {
|
base: {
|
||||||
maxHeight: '90%',
|
display: 'flex',
|
||||||
maxWidth: '90%',
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
appContent: {
|
||||||
|
gap: 0,
|
||||||
|
overflow: 'visible',
|
||||||
|
flexGrow: 5,
|
||||||
},
|
},
|
||||||
appName: {
|
appName: {
|
||||||
wordBreak: 'break-word',
|
wordBreak: 'break-word',
|
||||||
},
|
},
|
||||||
|
appImage: {
|
||||||
|
flex: '1',
|
||||||
|
objectFit: 'contain',
|
||||||
|
overflowY: 'auto',
|
||||||
|
},
|
||||||
button: {
|
button: {
|
||||||
height: '100%',
|
height: '100%',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 4,
|
gap: 4,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -130,7 +130,8 @@ export default function ContainerActionBar({ selected, reload }: ContainerAction
|
|||||||
appearance: {
|
appearance: {
|
||||||
iconUrl: '/imgs/logo/logo.png',
|
iconUrl: '/imgs/logo/logo.png',
|
||||||
appNameStatus: 'normal',
|
appNameStatus: 'normal',
|
||||||
positionAppName: 'column'
|
positionAppName: 'column',
|
||||||
|
lineClampAppName: 1,
|
||||||
},
|
},
|
||||||
network: {
|
network: {
|
||||||
enabledStatusChecker: true,
|
enabledStatusChecker: true,
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ export const getFrontendConfig = async (name: string): Promise<ConfigType> => {
|
|||||||
let config = getConfig(name);
|
let config = getConfig(name);
|
||||||
let shouldMigrateConfig = false;
|
let shouldMigrateConfig = false;
|
||||||
|
|
||||||
|
config = migrateAppConfigs(config);
|
||||||
|
|
||||||
const anyWeatherWidgetWithStringLocation = config.widgets.some(
|
const anyWeatherWidgetWithStringLocation = config.widgets.some(
|
||||||
(widget) => widget.type === 'weather' && typeof widget.properties.location === 'string'
|
(widget) => widget.type === 'weather' && typeof widget.properties.location === 'string'
|
||||||
);
|
);
|
||||||
@@ -129,3 +131,18 @@ const migratePiholeIntegrationField = (config: BackendConfigType) => {
|
|||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const migrateAppConfigs = (config: BackendConfigType) => {
|
||||||
|
return {
|
||||||
|
...config,
|
||||||
|
apps: config.apps.map((app) => ({
|
||||||
|
...app,
|
||||||
|
appearance: {
|
||||||
|
...app.appearance,
|
||||||
|
appNameStatus: app.appearance.appNameStatus?? 'normal',
|
||||||
|
positionAppName: app.appearance.positionAppName?? 'column',
|
||||||
|
lineClampAppName: app.appearance.lineClampAppName?? 1,
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -36,6 +36,7 @@ interface AppAppearanceType {
|
|||||||
iconUrl: string;
|
iconUrl: string;
|
||||||
appNameStatus: "normal"|"hover"|"hidden";
|
appNameStatus: "normal"|"hover"|"hidden";
|
||||||
positionAppName: Property.FlexDirection;
|
positionAppName: Property.FlexDirection;
|
||||||
|
lineClampAppName: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type IntegrationType =
|
export type IntegrationType =
|
||||||
|
|||||||
Reference in New Issue
Block a user