🔀 Merge branch 'dev' into feature/add-basic-authentication

This commit is contained in:
Manuel
2023-08-23 21:17:43 +02:00
42 changed files with 971 additions and 770 deletions

View File

@@ -74,6 +74,7 @@ const definition = defineWidget({
};
},
itemComponent({ data, onChange, delete: deleteData }) {
const { t } = useTranslation('modules/bookmark');
const form = useForm({
initialValues: data,
validate: {
@@ -83,15 +84,15 @@ const definition = defineWidget({
return undefined;
}
return 'Length must be between 1 and 100';
return t('item.validation.length100');
},
href: (value) => {
if (!z.string().min(1).max(200).safeParse(value).success) {
return 'Length must be between 1 and 200';
return t('item.validation.length200');
}
if (!z.string().url().safeParse(value).success) {
return 'Not a valid link';
return t('item.validation.invalidLink');
}
return undefined;
@@ -101,7 +102,7 @@ const definition = defineWidget({
return undefined;
}
return 'Length must be between 1 and 100';
return t('item.validation.length400');
},
},
validateInputOnChange: true,
@@ -122,13 +123,13 @@ const definition = defineWidget({
<TextInput
icon={<IconTypography size="1rem" />}
{...form.getInputProps('name')}
label="Name"
label={t('item.name')}
withAsterisk
/>
<TextInput
icon={<IconLink size="1rem" />}
{...form.getInputProps('href')}
label="URL"
label={t('item.url')}
withAsterisk
/>
<IconSelector
@@ -140,17 +141,17 @@ const definition = defineWidget({
/>
<Switch
{...form.getInputProps('openNewTab')}
label="Open in new tab"
label={t('item.newTab')}
checked={form.values.openNewTab}
/>
<Switch
{...form.getInputProps('hideHostname')}
label="Hide Hostname"
label={t('item.hideHostname')}
checked={form.values.hideHostname}
/>
<Switch
{...form.getInputProps('hideIcon')}
label="Hide Icon"
label={t('item.hideIcon')}
checked={form.values.hideIcon}
/>
<Button
@@ -159,11 +160,11 @@ const definition = defineWidget({
variant="light"
type="button"
>
Delete
{t('item.delete')}
</Button>
{!form.isValid() && (
<Alert color="red" icon={<IconAlertTriangle size="1rem" />}>
Did not save, because there were validation errors. Please adust your inputs
{t('item.validation.errorMsg')}
</Alert>
)}
</Stack>
@@ -174,18 +175,9 @@ const definition = defineWidget({
layout: {
type: 'select',
data: [
{
label: 'Auto Grid',
value: 'autoGrid',
},
{
label: 'Horizontal',
value: 'horizontal',
},
{
label: 'Vertical',
value: 'vertical',
},
{ value: 'autoGrid', },
{ value: 'horizontal', },
{ value: 'vertical', },
],
defaultValue: 'autoGrid',
},
@@ -206,10 +198,10 @@ interface BookmarkWidgetTileProps {
}
function BookmarkWidgetTile({ widget }: BookmarkWidgetTileProps) {
const { t } = useTranslation('modules/bookmark');
const { classes } = useStyles();
const { enabled: isEditModeEnabled } = useEditModeStore();
const { fn, colors, colorScheme } = useMantineTheme();
const { t } = useTranslation('modules/bookmark');
if (widget.properties.items.length === 0) {
return (

View File

@@ -2,8 +2,9 @@ import { useMantineTheme } from '@mantine/core';
import { Calendar } from '@mantine/dates';
import { IconCalendarTime } from '@tabler/icons-react';
import { useSession } from 'next-auth/react';
import { i18n } from 'next-i18next';
import { useState } from 'react';
import { useRouter } from 'next/router';
import { getLanguageByCode } from '~/tools/language';
import { RouterOutputs, api } from '~/utils/api';
import { useEditModeStore } from '../../components/Dashboard/Views/useEditModeStore';
@@ -30,20 +31,20 @@ const definition = defineWidget({
type: 'select',
defaultValue: 'inCinemas',
data: [
{ label: 'In Cinemas', value: 'inCinemas' },
{ label: 'Physical', value: 'physicalRelease' },
{ label: 'Digital', value: 'digitalRelease' },
{ value: 'inCinemas' },
{ value: 'physicalRelease' },
{ value: 'digitalRelease' },
],
},
fontSize: {
type: 'select',
defaultValue: 'xs',
data: [
{ label: 'Extra Small', value: 'xs' },
{ label: 'Small', value: 'sm' },
{ label: 'Medium', value: 'md' },
{ label: 'Large', value: 'lg' },
{ label: 'Extra Large', value: 'xl' },
{ value: 'xs' },
{ value: 'sm' },
{ value: 'md' },
{ value: 'lg' },
{ value: 'xl' },
],
},
},
@@ -63,6 +64,7 @@ interface CalendarTileProps {
}
function CalendarTile({ widget }: CalendarTileProps) {
const { locale } = useRouter();
const { colorScheme, radius } = useMantineTheme();
const { name: configName } = useConfigContext();
const [month, setMonth] = useState(new Date());
@@ -72,6 +74,9 @@ function CalendarTile({ widget }: CalendarTileProps) {
enabled: !!sessionData?.user,
});
const language = getLanguageByCode(locale ?? 'en');
require(`dayjs/locale/${language.locale}.js`);
const { data: medias } = api.calendar.medias.useQuery(
{
configName: configName!,
@@ -93,7 +98,7 @@ function CalendarTile({ widget }: CalendarTileProps) {
onPreviousMonth={setMonth}
onNextMonth={setMonth}
size={widget.properties.fontSize}
locale={i18n?.resolvedLanguage ?? 'en'}
locale={language.locale}
firstDayOfWeek={getFirstDayOfWeek(firstDayOfWeek)}
hideWeekdays={widget.properties.hideWeekDays}
style={{ position: 'relative' }}

View File

@@ -1,16 +1,21 @@
import { Stack, Text, createStyles } from '@mantine/core';
import { useElementSize } from '@mantine/hooks';
import { IconClock } from '@tabler/icons-react';
import moment from 'moment-timezone';
import { useRouter } from 'next/router';
import { useEffect, useRef, useState } from 'react';
import { getLanguageByCode } from '~/tools/language';
import { api } from '~/utils/api';
import dayjs from 'dayjs';
import timezones from 'dayjs/plugin/timezone'
import utc from 'dayjs/plugin/utc'
import { useSetSafeInterval } from '../../hooks/useSetSafeInterval';
import { defineWidget } from '../helper';
import { IWidget } from '../widgets';
dayjs.extend(utc);
dayjs.extend(timezones);
const definition = defineWidget({
id: 'date',
icon: IconClock,
@@ -24,14 +29,14 @@ const definition = defineWidget({
defaultValue: 'dddd, MMMM D',
data: () => [
{ value: 'hide' },
{ value: 'dddd, MMMM D', label: moment().format('dddd, MMMM D') },
{ value: 'dddd, D MMMM', label: moment().format('dddd, D MMMM') },
{ value: 'MMM D', label: moment().format('MMM D') },
{ value: 'D MMM', label: moment().format('D MMM') },
{ value: 'DD/MM/YYYY', label: moment().format('DD/MM/YYYY') },
{ value: 'MM/DD/YYYY', label: moment().format('MM/DD/YYYY') },
{ value: 'DD/MM', label: moment().format('DD/MM') },
{ value: 'MM/DD', label: moment().format('MM/DD') },
{ value: 'dddd, MMMM D', label: dayjs().format('dddd, MMMM D') },
{ value: 'dddd, D MMMM', label: dayjs().format('dddd, D MMMM') },
{ value: 'MMM D', label: dayjs().format('MMM D') },
{ value: 'D MMM', label: dayjs().format('D MMM') },
{ value: 'DD/MM/YYYY', label: dayjs().format('DD/MM/YYYY') },
{ value: 'MM/DD/YYYY', label: dayjs().format('MM/DD/YYYY') },
{ value: 'DD/MM', label: dayjs().format('DD/MM') },
{ value: 'MM/DD', label: dayjs().format('MM/DD') },
],
},
enableTimezone: {
@@ -84,11 +89,11 @@ function DateTile({ widget }: DateTileProps) {
className={cx(classes.extras, 'dashboard-tile-clock-city')}
>
{widget.properties.timezoneLocation.name}
{widget.properties.titleState === 'both' && moment(date).format(' (z)')}
{widget.properties.titleState === 'both' && dayjs(date).format(' (z)')}
</Text>
)}
<Text className={cx(classes.clock, 'dashboard-tile-clock-hour')}>
{moment(date).format(formatString)}
{dayjs(date).format(formatString)}
</Text>
{!widget.properties.dateFormat.includes('hide') && (
<Text
@@ -96,7 +101,7 @@ function DateTile({ widget }: DateTileProps) {
pt="0.2rem"
className={cx(classes.extras, 'dashboard-tile-clock-date')}
>
{moment(date).format(widget.properties.dateFormat)}
{dayjs(date).format(widget.properties.dateFormat)}
</Text>
)}
</Stack>
@@ -139,7 +144,7 @@ const useDateState = (location?: { latitude: number; longitude: number }) => {
const timeoutRef = useRef<NodeJS.Timeout>(); // reference for initial timeout until first minute change
useEffect(() => {
const language = getLanguageByCode(locale ?? 'en');
moment.locale(language.momentLocale);
dayjs.locale(language.locale);
setDate(getNewDate(timezone));
timeoutRef.current = setTimeout(
() => {
@@ -150,9 +155,8 @@ const useDateState = (location?: { latitude: number; longitude: number }) => {
}, 1000 * 60);
//1 minute - current seconds and milliseconds count
},
1000 * 60 - (1000 * moment().seconds() + moment().milliseconds())
1000 * 60 - (1000 * dayjs().second() + dayjs().millisecond())
);
return () => timeoutRef.current && clearTimeout(timeoutRef.current);
}, [timezone, locale]);
@@ -162,9 +166,9 @@ const useDateState = (location?: { latitude: number; longitude: number }) => {
//Returns a local date if no inputs or returns date from input zone
const getNewDate = (timezone?: string) => {
if (timezone) {
return moment().tz(timezone);
return dayjs().tz(timezone);
}
return moment();
return dayjs();
};
export default definition;

View File

@@ -121,7 +121,7 @@ function IFrameTile({ widget }: IFrameTileProps) {
title="widget iframe"
allow={allowedPermissions.join(' ')}
>
<Text>Your Browser does not support iframes. Please update your browser.</Text>
<Text>{t('card.errors.browserSupport')}</Text>
</iframe>
</Container>
);

View File

@@ -59,12 +59,13 @@ const useMediaRequestDecisionMutation = () => {
utils.mediaRequest.all.invalidate();
},
});
const { t } = useTranslation('modules/media-requests-list');
return async (variables: MediaRequestDecisionVariables) => {
const action = variables.isApproved ? 'Approving' : 'Declining';
const action = variables.isApproved ? t('mutation.approving') : t('mutation.declining');
notifications.show({
id: `decide-${variables.request.id}`,
color: 'yellow',
title: `${action} request...`,
title: `${action} ${t('mutation.request')}`,
message: undefined,
loading: true,
});
@@ -76,7 +77,7 @@ const useMediaRequestDecisionMutation = () => {
},
{
onSuccess(_data, variables) {
const title = variables.isApproved ? 'Request was approved!' : 'Request was declined!';
const title = variables.isApproved ? t('mutation.approved') : t('mutation.declined');
notifications.update({
id: `decide-${variables.id}`,
color: 'teal',
@@ -189,7 +190,7 @@ function MediaRequestListTile({ widget }: MediaRequestListWidgetProps) {
notifications.show({
id: `approve ${item.id}`,
color: 'yellow',
title: 'Approving request...',
title: t('tooltips.approving'),
message: undefined,
loading: true,
});

View File

@@ -16,8 +16,8 @@ const definition = defineWidget({
type: 'select',
defaultValue: 'row' as 'row' | 'column',
data: [
{ label: 'Horizontal', value: 'row' },
{ label: 'Vertical', value: 'column' },
{ value: 'row' },
{ value: 'column' },
],
},
},

View File

@@ -2,27 +2,29 @@ import { Card, Divider, Flex, Grid, Group, Text } from '@mantine/core';
import { IconDeviceMobile, IconId } from '@tabler/icons-react';
import { GenericSessionInfo } from '../../types/api/media-server/session-info';
import { useTranslation } from 'react-i18next';
export const DetailCollapseable = ({ session }: { session: GenericSessionInfo }) => {
let details: { title: string; metrics: { name: string; value: string | undefined }[] }[] = [];
const { t } = useTranslation('modules/media-server-list');
if (session.currentlyPlaying) {
if (session.currentlyPlaying.metadata.video) {
details = [
...details,
{
title: 'Video',
title: t('detail.video.'),
metrics: [
{
name: 'Resolution',
name: t('detail.video.resolution'),
value: `${session.currentlyPlaying.metadata.video.width}x${session.currentlyPlaying.metadata.video.height}`,
},
{
name: 'Framerate',
name: t('detail.video.framerate'),
value: session.currentlyPlaying.metadata.video.videoFrameRate,
},
{
name: 'Codec',
name: t('detail.video.codec'),
value: session.currentlyPlaying.metadata.video.videoCodec,
},
{
@@ -39,14 +41,14 @@ export const DetailCollapseable = ({ session }: { session: GenericSessionInfo })
details = [
...details,
{
title: 'Audio',
title: t('detail.audio.audio'),
metrics: [
{
name: 'Audio channels',
name: t('detail.audio.channels'),
value: `${session.currentlyPlaying.metadata.audio.audioChannels}`,
},
{
name: 'Audio codec',
name: t('detail.audio.codec'),
value: session.currentlyPlaying.metadata.audio.audioCodec,
},
],
@@ -58,24 +60,24 @@ export const DetailCollapseable = ({ session }: { session: GenericSessionInfo })
details = [
...details,
{
title: 'Transcoding',
title: t('detail.transcoding.transcoding'),
metrics: [
{
name: 'Resolution',
name: t('detail.video.resolution'),
value: `${session.currentlyPlaying.metadata.transcoding.width}x${session.currentlyPlaying.metadata.transcoding.height}`,
},
{
name: 'Context',
name: t('detail.transcoding.context'),
value: session.currentlyPlaying.metadata.transcoding.context,
},
{
name: 'Hardware encoding requested',
name: t('detail.transcoding.requested'),
value: session.currentlyPlaying.metadata.transcoding.transcodeHwRequested
? 'yes'
: 'no',
},
{
name: 'Source codec',
name: t('detail.transcoding.source'),
value:
session.currentlyPlaying.metadata.transcoding.sourceAudioCodec ||
session.currentlyPlaying.metadata.transcoding.sourceVideoCodec
@@ -83,7 +85,7 @@ export const DetailCollapseable = ({ session }: { session: GenericSessionInfo })
: undefined,
},
{
name: 'Target codec',
name: t('detail.transcoding.target'),
value: `${session.currentlyPlaying.metadata.transcoding.videoCodec} ${session.currentlyPlaying.metadata.transcoding.audioCodec}`,
},
],
@@ -97,19 +99,19 @@ export const DetailCollapseable = ({ session }: { session: GenericSessionInfo })
<Flex justify="space-between" mb="xs">
<Group>
<IconId size={16} />
<Text>ID</Text>
<Text>{t('detail.id')}</Text>
</Group>
<Text>{session.id}</Text>
</Flex>
<Flex justify="space-between" mb="md">
<Group>
<IconDeviceMobile size={16} />
<Text>Device</Text>
<Text>{t('detail.device')}</Text>
</Group>
<Text>{session.sessionName}</Text>
</Flex>
{details.length > 0 && (
<Divider label="Stats for nerds" labelPosition="center" mt="lg" mb="sm" />
<Divider label={t('detail.label')} labelPosition="center" mt="lg" mb="sm" />
)}
<Grid>
{details.map((detail, index) => (

View File

@@ -42,7 +42,6 @@ interface MediaServerWidgetProps {
function MediaServerTile({ widget }: MediaServerWidgetProps) {
const { t } = useTranslation('modules/media-server');
const { config } = useConfigContext();
const isEditMode = useEditModeStore((x) => x.enabled);
const { data, isError, isFetching, isInitialLoading } = useGetMediaServers({
enabled: config !== undefined,
@@ -72,7 +71,7 @@ function MediaServerTile({ widget }: MediaServerWidgetProps) {
<Loader />
<Stack align="center" spacing={0}>
<Text>{t('descriptor.name')}</Text>
<Text color="dimmed">Homarr is loading streams...</Text>
<Text color="dimmed">{t('descriptor.loading')}</Text>
</Stack>
</Stack>
);

View File

@@ -3,6 +3,7 @@ import {
Icon,
IconDeviceTv,
IconHeadphones,
IconMovie,
IconQuestionMark,
IconVideo,
} from '@tabler/icons-react';
@@ -23,6 +24,8 @@ export const NowPlayingDisplay = ({ session }: { session: GenericSessionInfo })
return IconHeadphones;
case 'tv':
return IconDeviceTv;
case 'movie':
return IconMovie;
case 'video':
return IconVideo;
default:

View File

@@ -42,6 +42,7 @@ const definition = defineWidget({
dangerousAllowSanitizedItemContent: {
type: 'switch',
defaultValue: false,
info: true,
},
textLinesClamp: {
type: 'slider',

View File

@@ -13,7 +13,7 @@ import {
createStyles,
useMantineTheme,
} from '@mantine/core';
import { useDisclosure, useElementSize } from '@mantine/hooks';
import { useDisclosure } from '@mantine/hooks';
import {
IconAffiliate,
IconDatabase,
@@ -37,9 +37,8 @@ interface TorrentQueueItemProps {
width: number;
}
export const BitTorrrentQueueItem = ({ torrent, width, app }: TorrentQueueItemProps) => {
export const BitTorrentQueueItem = ({ torrent, width, app }: TorrentQueueItemProps) => {
const [popoverOpened, { open: openPopover, close: closePopover }] = useDisclosure(false);
const theme = useMantineTheme();
const { classes } = useStyles();
const { t } = useTranslation('modules/torrents-status');

View File

@@ -25,7 +25,7 @@ import { AppIntegrationType } from '../../types/app';
import { useGetDownloadClientsQueue } from '../download-speed/useGetNetworkSpeed';
import { defineWidget } from '../helper';
import { IWidget } from '../widgets';
import { BitTorrrentQueueItem } from './TorrentQueueItem';
import { BitTorrentQueueItem } from './TorrentQueueItem';
dayjs.extend(duration);
dayjs.extend(relativeTime);
@@ -108,7 +108,7 @@ function TorrentTile({ widget }: TorrentTileProps) {
<Loader />
<Stack align="center" spacing={0}>
<Text>{t('card.loading.title')}</Text>
<Text color="dimmed">Homarr is establishing a connection...</Text>
<Text color="dimmed">{t('card.loading.description')}</Text>
</Stack>
</Stack>
);
@@ -156,7 +156,7 @@ function TorrentTile({ widget }: TorrentTileProps) {
</thead>
<tbody>
{filteredTorrents.map((torrent, index) => (
<BitTorrrentQueueItem key={index} torrent={torrent} width={width} app={undefined} />
<BitTorrentQueueItem key={index} torrent={torrent} width={width} app={undefined} />
))}
{filteredTorrents.length !== torrents.length && (

View File

@@ -21,8 +21,8 @@ import duration from 'dayjs/plugin/duration';
import { useTranslation } from 'next-i18next';
import { FunctionComponent, useState } from 'react';
import { useGetUsenetDownloads } from '../dashDot/api';
import { humanFileSize } from '../../tools/humanFileSize';
import { useGetUsenetDownloads } from '../dashDot/api';
dayjs.extend(duration);
@@ -91,7 +91,6 @@ export const UsenetQueueList: FunctionComponent<UsenetQueueListProps> = ({ appId
<Table highlightOnHover style={{ tableLayout: 'fixed' }} ref={ref}>
<thead>
<tr>
<th style={{ width: 32 }} />
<th style={{ width: '75%' }}>{t('queue.header.name')}</th>
{sizeBreakpoint < width ? (
<th style={{ width: 100 }}>{t('queue.header.size')}</th>
@@ -107,21 +106,6 @@ export const UsenetQueueList: FunctionComponent<UsenetQueueListProps> = ({ appId
<tbody>
{data.items.map((nzb) => (
<tr key={nzb.id}>
<td>
{nzb.state === 'paused' ? (
<Tooltip label="NOT IMPLEMENTED">
<ActionIcon color="gray" variant="subtle" radius="xl" size="sm">
<IconPlayerPlay size="16" />
</ActionIcon>
</Tooltip>
) : (
<Tooltip label="NOT IMPLEMENTED">
<ActionIcon color="primary" variant="subtle" radius="xl" size="sm">
<IconPlayerPause size="16" />
</ActionIcon>
</Tooltip>
)}
</td>
<td>
<Tooltip position="top" label={nzb.name}>
<Text

View File

@@ -11,7 +11,6 @@ import {
IconSun,
} from '@tabler/icons-react';
import { useTranslation } from 'next-i18next';
import { useElementSize } from '@mantine/hooks';
interface WeatherIconProps {
code: number;
@@ -25,8 +24,7 @@ interface WeatherIconProps {
*/
export const WeatherIcon = ({ code, size=50 }: WeatherIconProps) => {
const { t } = useTranslation('modules/weather');
const { width, ref } = useElementSize();
const { icon: Icon, name } =
weatherDefinitions.find((wd) => wd.codes.includes(code)) ?? unknownWeather;

View File

@@ -4,7 +4,6 @@ import {
IconArrowDownRight,
IconArrowUpRight,
IconCloudRain,
IconCurrentLocation,
IconMapPin,
} from '@tabler/icons-react';
import { api } from '~/utils/api';
@@ -12,6 +11,7 @@ import { api } from '~/utils/api';
import { defineWidget } from '../helper';
import { IWidget } from '../widgets';
import { WeatherIcon } from './WeatherIcon';
import { useTranslation } from 'react-i18next';
const definition = defineWidget({
id: 'weather',
@@ -52,6 +52,7 @@ interface WeatherTileProps {
function WeatherTile({ widget }: WeatherTileProps) {
const { data: weather, isLoading, isError } = api.weather.at.useQuery(widget.properties.location);
const { width, ref } = useElementSize();
const { t } = useTranslation('modules/weather');
if (isLoading) {
return (
@@ -77,7 +78,7 @@ function WeatherTile({ widget }: WeatherTileProps) {
if (isError) {
return (
<Center>
<Text weight={500}>An error occured</Text>
<Text weight={500}>{t('error')}</Text>
</Center>
);
}