🔀 Merge dev to auth branch

This commit is contained in:
Manuel
2023-09-10 13:38:53 +02:00
617 changed files with 8473 additions and 1499 deletions

View File

@@ -84,11 +84,11 @@ const definition = defineWidget({
return undefined;
}
return t('item.validation.length100');
return t('item.validation.length', {shortest: "1", longest: "100"});
},
href: (value) => {
if (!z.string().min(1).max(200).safeParse(value).success) {
return t('item.validation.length200');
return t('item.validation.length', {shortest: "1", longest: "200"});
}
if (!z.string().url().safeParse(value).success) {
@@ -102,7 +102,7 @@ const definition = defineWidget({
return undefined;
}
return t('item.validation.length400');
return t('item.validation.length', {shortest: "1", longest: "400"});
},
},
validateInputOnChange: true,
@@ -269,6 +269,7 @@ function BookmarkWidgetTile({ widget }: BookmarkWidgetTileProps) {
viewport:{
//mantine being mantine again... this might break. Needed for taking 100% of widget space
'& div[style="min-width: 100%; display: table;"]':{
display: 'flex !important',
height:'100%',
},
},
@@ -278,14 +279,16 @@ function BookmarkWidgetTile({ widget }: BookmarkWidgetTileProps) {
direction={ widget.properties.layout === 'vertical' ? 'column' : 'row' }
gap="0"
h="100%"
w="100%"
>
{widget.properties.items.map((item: BookmarkItem, index) => (
<>
<Divider
m="1px"
orientation={ widget.properties.layout !== 'vertical' ? 'vertical' : 'horizontal' } //Mantine doesn't let me refactor this, I tried
hidden={!(index > 0)}
/>
{index > 0 &&
<Divider
m="3px"
orientation={ widget.properties.layout !== 'vertical' ? 'vertical' : 'horizontal' }
/>
}
<Card
key={index}
px="md"
@@ -297,7 +300,8 @@ function BookmarkWidgetTile({ widget }: BookmarkWidgetTileProps) {
bg="transparent"
sx={{
'&:hover': { backgroundColor: fn.primaryColor().concat('40'),}, //'40' = 25% opacity
flex:'1 1 auto'
flex:'1 1 auto',
overflow: 'unset',
}}
display="flex"
>

View File

@@ -22,6 +22,10 @@ const definition = defineWidget({
type: 'switch',
defaultValue: true,
},
showUnmonitored: {
type: 'switch',
defaultValue: false,
},
useSonarrv4: {
type: 'switch',
defaultValue: false,
@@ -79,7 +83,7 @@ function CalendarTile({ widget }: CalendarTileProps) {
configName: configName!,
month: month.getMonth() + 1,
year: month.getFullYear(),
options: { useSonarrv4: widget.properties.useSonarrv4 },
options: { useSonarrv4: widget.properties.useSonarrv4, showUnmonitored: widget.properties.showUnmonitored },
},
{
staleTime: 1000 * 60 * 60 * 5,

View File

@@ -1,4 +1,15 @@
import { Badge, Box, Button, Card, Group, Image, SimpleGrid, Stack, Text } from '@mantine/core';
import {
Badge,
Box,
Button,
Card,
Group,
Image,
SimpleGrid,
Stack,
Text,
UnstyledButton,
} from '@mantine/core';
import { useElementSize } from '@mantine/hooks';
import { IconDeviceGamepad, IconPlayerPlay, IconPlayerStop } from '@tabler/icons-react';
import { useSession } from 'next-auth/react';
@@ -10,7 +21,6 @@ import { defineWidget } from '../helper';
import { WidgetLoading } from '../loading';
import { IWidget } from '../widgets';
import { useDnsHoleSummeryQuery } from './DnsHoleSummary';
import { PiholeApiSummaryType } from './type';
const definition = defineWidget({
id: 'dns-hole-controls',
@@ -31,20 +41,69 @@ interface DnsHoleControlsWidgetProps {
widget: IDnsHoleControlsWidget;
}
/**
*
* @param fetching - a expression that return a boolean if the data is been fetched
* @param currentStatus the current status of the dns integration, either enabled or disabled
* @returns
*/
const dnsLightStatus = (
fetching: boolean,
currentStatus: 'enabled' | 'disabled'
): 'blue' | 'green' | 'red' => {
if (fetching) {
return 'blue';
}
if (currentStatus === 'enabled') {
return 'green';
}
return 'red';
};
function DnsHoleControlsWidgetTile({ widget }: DnsHoleControlsWidgetProps) {
const utils = api.useContext();
const { data: sessionData } = useSession();
const { isInitialLoading, data } = useDnsHoleSummeryQuery();
const { mutateAsync } = useDnsHoleControlMutation();
const { isInitialLoading, data, isFetching: fetchingDnsSummary } = useDnsHoleSummeryQuery();
const { mutateAsync, isLoading: changingStatus } = useDnsHoleControlMutation();
const { width, ref } = useElementSize();
const { t } = useTranslation('common');
const { name: configName, config } = useConfigContext();
const trpcUtils = api.useContext();
if (isInitialLoading || !data || !configName) {
return <WidgetLoading />;
}
type getDnsStatusAcc = {
enabled: string[];
disabled: string[];
};
const getDnsStatus = () => {
const dnsList = data?.status.reduce(
(acc: getDnsStatusAcc, dns) => {
if (dns.status === 'enabled') {
acc.enabled.push(dns.appId);
} else if (dns.status === 'disabled') {
acc.disabled.push(dns.appId);
}
return acc;
},
{ enabled: [], disabled: [] }
);
if (dnsList.enabled.length === 0 && dnsList.disabled.length === 0) {
return undefined;
}
return dnsList;
};
const reFetchSummaryDns = () => {
trpcUtils.dnsHole.summary.invalidate();
};
return (
<Stack justify="space-between" h={'100%'} spacing="0.25rem">
{sessionData?.user?.isAdmin && (
@@ -59,10 +118,14 @@ function DnsHoleControlsWidgetTile({ widget }: DnsHoleControlsWidgetProps) {
await mutateAsync({
action: 'enable',
configName,
appsToChange: getDnsStatus()?.disabled,
},{
onSettled: () => {
reFetchSummaryDns();
}
});
await utils.dnsHole.summary.invalidate();
}}
disabled={getDnsStatus()?.disabled.length === 0 || fetchingDnsSummary || changingStatus}
leftIcon={<IconPlayerPlay size={20} />}
variant="light"
color="green"
@@ -75,9 +138,14 @@ function DnsHoleControlsWidgetTile({ widget }: DnsHoleControlsWidgetProps) {
await mutateAsync({
action: 'disable',
configName,
appsToChange: getDnsStatus()?.enabled,
},{
onSettled: () => {
reFetchSummaryDns();
}
});
await utils.dnsHole.summary.invalidate();
}}
disabled={getDnsStatus()?.enabled.length === 0 || fetchingDnsSummary || changingStatus}
leftIcon={<IconPlayerStop size={20} />}
variant="light"
color="red"
@@ -89,15 +157,15 @@ function DnsHoleControlsWidgetTile({ widget }: DnsHoleControlsWidgetProps) {
)}
<Stack spacing="0.25rem">
{data.status.map((status, index) => {
const app = config?.apps.find((x) => x.id === status.appId);
{data.status.map((dnsHole, index) => {
const app = config?.apps.find((x) => x.id === dnsHole.appId);
if (!app) {
return null;
}
return (
<Card withBorder={true} key={index} p="xs">
<Card withBorder={true} key={dnsHole.appId} p="xs">
<Group>
<Box
sx={(theme) => ({
@@ -112,7 +180,43 @@ function DnsHoleControlsWidgetTile({ widget }: DnsHoleControlsWidgetProps) {
</Box>
<Stack spacing="0rem">
<Text>{app.name}</Text>
<StatusBadge status={status.status} />
<UnstyledButton
onClick={async () => {
await mutateAsync({
action: dnsHole.status === 'enabled' ? 'disable' : 'enable',
configName,
appsToChange: [app.id],
},{
onSettled: () => {
reFetchSummaryDns();
}
});
}}
disabled={fetchingDnsSummary || changingStatus}
>
<Badge
variant="dot"
color={dnsLightStatus(fetchingDnsSummary || changingStatus, dnsHole.status)}
styles={(theme) => ({
root: {
'&:hover': {
background:
theme.colorScheme === 'dark'
? theme.colors.dark[4]
: theme.colors.gray[2],
},
'&:active': {
background:
theme.colorScheme === 'dark'
? theme.colors.dark[5]
: theme.colors.gray[3],
},
},
})}
>
{t(dnsHole.status)}
</Badge>
</UnstyledButton>
</Stack>
</Group>
</Card>
@@ -122,24 +226,6 @@ function DnsHoleControlsWidgetTile({ widget }: DnsHoleControlsWidgetProps) {
</Stack>
);
}
const StatusBadge = ({ status }: { status: PiholeApiSummaryType['status'] }) => {
const { t } = useTranslation('common');
if (status === 'enabled') {
return (
<Badge variant="dot" color="green">
{t('enabled')}
</Badge>
);
}
return (
<Badge variant="dot" color="red">
{t('disabled')}
</Badge>
);
};
const useDnsHoleControlMutation = () => api.dnsHole.control.useMutation();
export default definition;

View File

@@ -58,8 +58,13 @@ function DnsHoleSummaryWidgetTile({ widget }: DnsHoleSummaryWidgetProps) {
return (
<Container h="100%" p={0} style={constructContainerStyle(widget.properties.layout)}>
{stats.map((item) => (
<StatCard item={item} usePiHoleColors={widget.properties.usePiHoleColors} data={data} />
{stats.map((item, index) => (
<StatCard
key={item.label ?? index}
item={item}
usePiHoleColors={widget.properties.usePiHoleColors}
data={data}
/>
))}
</Container>
);
@@ -75,6 +80,7 @@ const stats = [
{
icon: IconPercentage,
value: (x) => formatPercentage(x.adsBlockedTodayPercentage, 2),
label: 'card.metrics.queriesBlockedTodayPercentage',
color: 'rgba(255, 165, 20, 0.4)',
},
{
@@ -106,7 +112,7 @@ export const useDnsHoleSummeryQuery = () => {
configName: configName!,
},
{
refetchInterval: 3 * 60 * 1000,
staleTime: 1000 * 60 * 2,
}
);
};

View File

@@ -1,14 +1,15 @@
import {
ActionIcon,
ActionIcon, Anchor,
Badge,
Card,
Center,
Flex,
Group,
Image,
ScrollArea,
Stack,
Text,
Tooltip,
Tooltip, useMantineTheme,
} from '@mantine/core';
import { notifications } from '@mantine/notifications';
import { IconCheck, IconGitPullRequest, IconThumbDown, IconThumbUp } from '@tabler/icons-react';
@@ -31,6 +32,10 @@ const definition = defineWidget({
type: 'switch',
defaultValue: true,
},
openInNewTab: {
type: 'switch',
defaultValue: true,
},
},
component: MediaRequestListTile,
gridstack: {
@@ -56,7 +61,8 @@ const useMediaRequestDecisionMutation = () => {
const utils = api.useContext();
const { mutateAsync } = api.overseerr.decide.useMutation({
onSuccess() {
utils.mediaRequest.all.invalidate();
utils.mediaRequest.allMedia.invalidate();
utils.mediaRequest.users.invalidate();
},
});
const { t } = useTranslation('modules/media-requests-list');
@@ -94,11 +100,13 @@ const useMediaRequestDecisionMutation = () => {
function MediaRequestListTile({ widget }: MediaRequestListWidgetProps) {
const { t } = useTranslation('modules/media-requests-list');
const { data, isLoading } = useMediaRequestQuery();
const { data, isLoading } = useMediaRequestQuery(widget);
// Use mutation to approve or deny a pending request
const decideAsync = useMediaRequestDecisionMutation();
const { data: sessionData } = useSession();
const mantineTheme = useMantineTheme();
if (!data || isLoading) {
return <WidgetLoading />;
}
@@ -127,58 +135,57 @@ function MediaRequestListTile({ widget }: MediaRequestListWidgetProps) {
});
return (
<Stack>
{countPendingApproval > 0 ? (
<Text>{t('pending', { countPendingApproval })}</Text>
) : (
<Text>{t('nonePending')}</Text>
)}
{sortedData.map((item) => (
<Card withBorder>
<Flex wrap="wrap" justify="space-between" gap="md">
<Flex gap="md">
<Image
src={item.posterPath}
width={30}
height={50}
alt="poster"
radius="xs"
withPlaceholder
/>
<Stack spacing={0}>
<Group spacing="xs">
{item.airDate && <Text>{item.airDate.split('-')[0]}</Text>}
<MediaRequestStatusBadge status={item.status} />
</Group>
<Text
sx={{ cursor: 'pointer', '&:hover': { textDecoration: 'underline' } }}
lineClamp={1}
weight="bold"
component="a"
href={item.href}
>
{item.name}
</Text>
</Stack>
</Flex>
<Stack justify="center">
<Flex gap="xs">
<ScrollArea h="100%">
<Stack>
{countPendingApproval > 0 ? (
<Text>{t('pending', { countPendingApproval })}</Text>
) : (
<Text>{t('nonePending')}</Text>
)}
{sortedData.map((item) => (
<Card radius="md" withBorder>
<Flex wrap="wrap" justify="space-between" gap="md">
<Flex gap="md">
<Image
src={item.userProfilePicture}
width={25}
height={25}
alt="requester avatar"
radius="xl"
src={item.posterPath}
width={30}
height={50}
alt="poster"
radius="xs"
withPlaceholder
/>
<Text
component="a"
href={item.userLink}
sx={{ cursor: 'pointer', '&:hover': { textDecoration: 'underline' } }}
>
{item.userName}
</Text>
<Stack spacing={0}>
<Group spacing="xs">
{item.airDate && <Text>{item.airDate.split('-')[0]}</Text>}
<MediaRequestStatusBadge status={item.status} />
</Group>
<Anchor
href={item.href}
target={widget.properties.openInNewTab ? "_blank" : "_self"}
c={mantineTheme.colorScheme === 'dark' ? 'gray.3' : 'gray.8'}
>
{item.name}
</Anchor>
</Stack>
</Flex>
<Stack justify="center">
<Flex gap="xs">
<Image
src={item.userProfilePicture}
width={25}
height={25}
alt="requester avatar"
radius="xl"
withPlaceholder
/>
<Anchor
href={item.userLink}
target={widget.properties.openInNewTab ? "_blank" : "_self"}
c={mantineTheme.colorScheme === 'dark' ? 'gray.3' : 'gray.8'}
>
{item.userName}
</Anchor>
</Flex>
{item.status === MediaRequestStatus.PendingApproval && sessionData?.user?.isAdmin && (
<Group>
@@ -195,47 +202,48 @@ function MediaRequestListTile({ widget }: MediaRequestListWidgetProps) {
loading: true,
});
await decideAsync({
request: item,
isApproved: true,
});
}}
>
<IconThumbUp />
</ActionIcon>
</Tooltip>
<Tooltip label={t('tooltips.decline')} withArrow withinPortal>
<ActionIcon
variant="light"
color="red"
onClick={async () => {
await decideAsync({
request: item,
isApproved: false,
});
}}
>
<IconThumbDown />
</ActionIcon>
</Tooltip>
</Group>
)}
</Stack>
</Flex>
await decideAsync({
request: item,
isApproved: true,
});
}}
>
<IconThumbUp />
</ActionIcon>
</Tooltip>
<Tooltip label={t('tooltips.decline')} withArrow withinPortal>
<ActionIcon
variant="light"
color="red"
onClick={async () => {
await decideAsync({
request: item,
isApproved: false,
});
}}
>
<IconThumbDown />
</ActionIcon>
</Tooltip>
</Group>
)}
</Stack>
</Flex>
<Image
src={item.backdropPath}
pos="absolute"
w="100%"
h="100%"
opacity={0.1}
top={0}
left={0}
style={{ pointerEvents: 'none' }}
/>
</Card>
))}
</Stack>
<Image
src={item.backdropPath}
pos="absolute"
w="100%"
h="100%"
opacity={0.1}
top={0}
left={0}
style={{ pointerEvents: 'none' }}
/>
</Card>
))}
</Stack>
</ScrollArea>
);
}

View File

@@ -1,29 +1,40 @@
import { Card, Flex, Stack, Text } from '@mantine/core';
import { IconChartBar } from '@tabler/icons-react';
import {
Avatar,
Card,
Flex,
Group,
Indicator,
Stack,
Text,
Tooltip,
useMantineTheme,
} from '@mantine/core';
import { useElementSize } from '@mantine/hooks';
import { IconChartBar, IconExternalLink } from '@tabler/icons-react';
import { useTranslation } from 'next-i18next';
import { defineWidget } from '../helper';
import { WidgetLoading } from '../loading';
import { IWidget } from '../widgets';
import { useMediaRequestQuery } from './media-request-query';
import { useMediaRequestQuery, useUsersQuery } from './media-request-query';
import { MediaRequestStatus } from './media-request-types';
const definition = defineWidget({
id: 'media-requests-stats',
icon: IconChartBar,
options: {
direction: {
type: 'select',
defaultValue: 'row' as 'row' | 'column',
data: [
{ value: 'row' },
{ value: 'column' },
],
replaceLinksWithExternalHost: {
type: 'switch',
defaultValue: false,
},
openInNewTab: {
type: 'switch',
defaultValue: true,
},
},
gridstack: {
minWidth: 1,
minHeight: 1,
minWidth: 2,
minHeight: 2,
maxWidth: 12,
maxHeight: 12,
},
@@ -38,53 +49,134 @@ interface MediaRequestStatsWidgetProps {
function MediaRequestStatsTile({ widget }: MediaRequestStatsWidgetProps) {
const { t } = useTranslation('modules/media-requests-stats');
const { data, isFetching } = useMediaRequestQuery();
const {
data: mediaData,
isFetching: mediaFetching,
isLoading: mediaLoading,
} = useMediaRequestQuery(widget);
const {
data: usersData,
isFetching: usersFetching,
isLoading: usersLoading
} = useUsersQuery(widget);
const { ref, height } = useElementSize();
const { colorScheme } = useMantineTheme();
if (!data || isFetching) {
return <WidgetLoading />;
if (!mediaData || !usersData || mediaLoading || usersLoading) {
return (
<Stack ref={ref} h="100%">
<WidgetLoading />
</Stack>
);
}
const appList: string[] = [];
mediaData.forEach((item) => {
if (!appList.includes(item.appId)) appList.push(item.appId);
});
const baseStats: { label: string; number: number }[] = [
{
label: t('mediaStats.pending'),
number: mediaData.filter((x) => x.status === MediaRequestStatus.PendingApproval).length,
},
{
label: t('mediaStats.tvRequests'),
number: mediaData.filter((x) => x.type === 'tv').length,
},
{
label: t('mediaStats.movieRequests'),
number: mediaData.filter((x) => x.type === 'movie').length,
},
{
label: t('mediaStats.approved'),
number: mediaData.filter((x) => x.status === MediaRequestStatus.Approved).length,
},
{
label: t('mediaStats.totalRequests'),
number: mediaData.length,
},
];
const users = usersData
.sort((x, y) => (x.userRequestCount > y.userRequestCount ? -1 : 1))
.slice(0, Math.trunc(height / 60));
return (
<Flex
w="100%"
h="100%"
gap="md"
direction={ widget.properties.direction?? 'row' }
>
<StatCard
number={data.filter((x) => x.status === MediaRequestStatus.PendingApproval).length}
label={t('stats.pending')}
/>
<StatCard
number={data.filter((x) => x.type === 'tv').length}
label={t('stats.tvRequests')}
/>
<StatCard
number={data.filter((x) => x.type === 'movie').length}
label={t('stats.movieRequests')}
/>
<Flex h="100%" gap={0} direction="column">
<Text mt={-5}>{t('mediaStats.title')}</Text>
<Card py={5} px={10} radius="md" style={{ overflow: 'unset' }} withBorder>
{baseStats.map((stat, index) => {
return (
<Group key={index} position="apart">
<Text color="dimmed" align="center" size="xs">
{stat.label}
</Text>
<Text align="center" size="xs">
{stat.number}
</Text>
</Group>
);
})}
</Card>
<Text mt={2}>{t('userStats.title')}</Text>
<Stack ref={ref} style={{ flex: 1 }} spacing={5} p={0} sx={{ overflow: 'hidden' }}>
{users.map((user) => {
return (
<Card
key={user.id}
p={0}
component="a"
href={user.userLink}
target={widget.properties.openInNewTab ? "_blank" : "_self"}
mah={95}
mih={55}
radius="md"
style={{ flex: 1 }}
withBorder
>
<Group
spacing={5}
px={10}
py={5}
align="center"
h="100%"
display="flex"
style={{ flexDirection: 'row' }}
>
{appList.length > 1 && (
<Tooltip.Floating
label={user.app.charAt(0).toUpperCase() + user.app.slice(1)}
c={colorScheme === 'light' ? 'black' : 'dark.0'}
color={colorScheme === 'light' ? 'gray.2' : 'dark.4'}
>
<Indicator
withBorder
top={18}
left={8}
size={15}
ml={-5}
color={user.app === 'overseerr' ? '#ECB000' : '#6677CC'}
processing={mediaFetching || usersFetching}
children
/>
</Tooltip.Floating>
)}
<Avatar radius="xl" size={45} src={user.userProfilePicture} alt="user avatar" />
<Stack spacing={0} style={{ flex: 1 }}>
<Text>{user.userName}</Text>
<Text size="xs">
{t('userStats.requests', { number: user.userRequestCount })}
</Text>
</Stack>
<IconExternalLink size={20} />
</Group>
</Card>
);
})}
</Stack>
</Flex>
);
}
interface StatCardProps {
number: number;
label: string;
}
const StatCard = ({ number, label }: StatCardProps) => {
return (
<Card w="100%" h="100%" withBorder style={{flex:"1 1 auto"}}>
<Stack w="100%" h="100%" align="center" justify="center" spacing={0}>
<Text align="center">
{number}
</Text>
<Text color="dimmed" align="center" size="xs">
{label}
</Text>
</Stack>
</Card>
);
};
export default definition;
export default definition;

View File

@@ -1,10 +1,22 @@
import { useConfigContext } from '~/config/provider';
import { MediaRequestListWidget } from './MediaRequestListTile';
import { MediaRequestStatsWidget } from './MediaRequestStatsTile';
import { api } from '~/utils/api';
export const useMediaRequestQuery = () => {
export const useMediaRequestQuery = (widget: MediaRequestListWidget|MediaRequestStatsWidget) => {
const { name: configName } = useConfigContext();
return api.mediaRequest.all.useQuery(
{ configName: configName! },
return api.mediaRequest.allMedia.useQuery(
{ configName: configName!, widget: widget },
{
refetchInterval: 3 * 60 * 1000,
}
);
};
export const useUsersQuery = (widget: MediaRequestListWidget|MediaRequestStatsWidget) => {
const { name: configName } = useConfigContext();
return api.mediaRequest.users.useQuery(
{ configName: configName!, widget: widget },
{
refetchInterval: 3 * 60 * 1000,
}

View File

@@ -8,6 +8,7 @@ export type MediaRequest = {
userName: string;
userProfilePicture: string;
userLink: string;
userRequestCount: number;
airDate?: string;
status: MediaRequestStatus;
backdropPath: string;
@@ -15,6 +16,15 @@ export type MediaRequest = {
href: string;
};
export type Users = {
app: string;
id: number;
userName: string;
userProfilePicture: string;
userLink: string;
userRequestCount: number;
};
export enum MediaRequestStatus {
PendingApproval = 1,
Approved = 2,

View File

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

View File

@@ -13,7 +13,6 @@ import { IconAlertTriangle, IconMovie } from '@tabler/icons-react';
import { useTranslation } from 'next-i18next';
import { AppAvatar } from '~/components/AppAvatar';
import { useEditModeStore } from '~/components/Dashboard/Views/useEditModeStore';
import { useConfigContext } from '~/config/provider';
import { useGetMediaServers } from './useGetMediaServers';
import { defineWidget } from '../helper';
@@ -71,7 +70,7 @@ function MediaServerTile({ widget }: MediaServerWidgetProps) {
<Loader />
<Stack align="center" spacing={0}>
<Text>{t('descriptor.name')}</Text>
<Text color="dimmed">{t('descriptor.loading')}</Text>
<Text color="dimmed">{t('loading')}</Text>
</Stack>
</Stack>
);
@@ -79,7 +78,7 @@ function MediaServerTile({ widget }: MediaServerWidgetProps) {
return (
<Stack h="100%">
<ScrollArea offsetScrollbars>
<ScrollArea offsetScrollbars h="100%">
<Table highlightOnHover>
<thead>
<tr>
@@ -99,7 +98,7 @@ function MediaServerTile({ widget }: MediaServerWidgetProps) {
</Table>
</ScrollArea>
<Group position="right" mt="auto">
<Group pos="absolute" bottom="15" right="15" mt="auto">
<Avatar.Group>
{data?.servers.map((server) => {
const app = config?.apps.find((x) => x.id === server.appId);

View File

@@ -1,24 +1,21 @@
import { Flex, Stack, Text } from '@mantine/core';
import {
Icon,
IconDeviceTv,
IconHeadphones,
IconMovie,
IconQuestionMark,
IconVideo,
} from '@tabler/icons-react';
import { useTranslation } from 'next-i18next';
import { GenericSessionInfo } from '~/types/api/media-server/session-info';
export const NowPlayingDisplay = ({ session }: { session: GenericSessionInfo }) => {
const { t } = useTranslation();
if (!session.currentlyPlaying) {
return null;
}
const Icon = (): Icon => {
const IconSelector = () => {
switch (session.currentlyPlaying?.type) {
case 'audio':
return IconHeadphones;
@@ -33,11 +30,12 @@ export const NowPlayingDisplay = ({ session }: { session: GenericSessionInfo })
}
};
const Test = Icon();
const Icon = IconSelector();
return (
<Flex wrap="nowrap" gap="sm" align="center">
<Test size={16} />
<Stack spacing={0} w={200}>
<Icon size={16} />
<Stack spacing={0}>
<Text lineClamp={1}>{session.currentlyPlaying.name}</Text>
{session.currentlyPlaying.albumName ? (
@@ -46,7 +44,7 @@ export const NowPlayingDisplay = ({ session }: { session: GenericSessionInfo })
</Text>
) : (
session.currentlyPlaying.seasonName && (
<Text lineClamp={2} color="dimmed" size="xs">
<Text lineClamp={1} color="dimmed" size="xs">
{session.currentlyPlaying.seasonName} - {session.currentlyPlaying.episodeName}
</Text>
)

View File

@@ -1,10 +1,10 @@
import { ActionIcon, createStyles, rem } from '@mantine/core';
import { ActionIcon, ScrollArea } from '@mantine/core';
import { useDebouncedValue } from '@mantine/hooks';
import { Link, RichTextEditor } from '@mantine/tiptap';
import { IconArrowUp, IconEdit, IconEditOff } from '@tabler/icons-react';
import { IconEdit, IconEditOff } from '@tabler/icons-react';
import { BubbleMenu, useEditor } from '@tiptap/react';
import StarterKit from '@tiptap/starter-kit';
import { useEffect, useRef, useState } from 'react';
import { useState } from 'react';
import { useConfigStore } from '~/config/store';
import { useColorTheme } from '~/tools/color';
import { api } from '~/utils/api';
@@ -76,41 +76,29 @@ export function Editor({ widget }: { widget: INotebookWidget }) {
return (
<>
{!enabled && (
<ActionIcon
style={{
zIndex: 1,
}}
top={7}
right={7}
pos="absolute"
color={primaryColor}
variant="light"
size={30}
radius={'md'}
onClick={() => setIsEditing(handleEditToggle)}
>
{isEditing ? <IconEditOff size={20} /> : <IconEdit size={20} />}
</ActionIcon>
)}
<RichTextEditor
p={0}
mt={0}
h="100%"
editor={editor}
styles={(theme) => ({
root: {
'& .ProseMirror': {
padding: '0 !important',
},
backgroundColor: theme.colorScheme === 'dark' ? theme.colors.dark[6] : 'white',
border: 'none',
borderRadius: '0.5rem',
display: 'flex',
flexDirection: 'column',
},
toolbar: {
backgroundColor: theme.colorScheme === 'dark' ? theme.colors.dark[6] : 'white',
paddingTop: 0,
paddingBottom: theme.spacing.md,
backgroundColor: 'transparent',
padding: '0.5rem',
},
content: {
backgroundColor: theme.colorScheme === 'dark' ? theme.colors.dark[6] : 'white',
backgroundColor: 'transparent',
padding: '0.5rem',
},
})}
>
@@ -156,8 +144,27 @@ export function Editor({ widget }: { widget: INotebookWidget }) {
</BubbleMenu>
)}
<RichTextEditor.Content />
<ScrollArea>
<RichTextEditor.Content />
</ScrollArea>
</RichTextEditor>
{!enabled && (
<ActionIcon
style={{
zIndex: 1,
}}
top={7}
right={7}
pos="absolute"
color={primaryColor}
variant="light"
size={30}
radius={'md'}
onClick={() => setIsEditing(handleEditToggle)}
>
{isEditing ? <IconEditOff size={20} /> : <IconEdit size={20} />}
</ActionIcon>
)}
</>
);
}