Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a0d86e2914 | ||
|
|
5d80d36be3 | ||
|
|
72f19d450c | ||
|
|
6024391414 | ||
|
|
905f445641 | ||
|
|
2462671700 | ||
|
|
0c0f8247d8 | ||
|
|
fa45eb3b3b | ||
|
|
b8ce86d783 | ||
|
|
cc58fbe263 | ||
|
|
50448c9fb6 | ||
|
|
0f1a948682 | ||
|
|
8f71c898a2 | ||
|
|
7be22833b0 | ||
|
|
43c0465e52 |
@@ -18,6 +18,7 @@ import { useState } from 'react';
|
||||
import { Apps } from 'tabler-icons-react';
|
||||
import { useConfig } from '../../tools/state';
|
||||
import { ServiceTypeList } from '../../tools/types';
|
||||
import { AppShelfItemWrapper } from './AppShelfItemWrapper';
|
||||
|
||||
export default function AddItemShelfItem(props: any) {
|
||||
const { addService } = useConfig();
|
||||
@@ -34,30 +35,39 @@ export default function AddItemShelfItem(props: any) {
|
||||
>
|
||||
<AddAppShelfItemForm setOpened={setOpened} />
|
||||
</Modal>
|
||||
<AspectRatio
|
||||
style={{
|
||||
minHeight: 120,
|
||||
minWidth: 120,
|
||||
}}
|
||||
ratio={4 / 3}
|
||||
>
|
||||
<Card
|
||||
style={{
|
||||
backgroundColor:
|
||||
theme.colorScheme === 'dark' ? theme.colors.dark[5] : theme.colors.gray[1],
|
||||
width: 200,
|
||||
height: 180,
|
||||
}}
|
||||
radius="md"
|
||||
>
|
||||
<Group direction="column" position="center">
|
||||
<motion.div whileHover={{ scale: 1.2 }}>
|
||||
<Apps style={{ cursor: 'pointer' }} onClick={() => setOpened(true)} size={60} />
|
||||
</motion.div>
|
||||
<Text>Add Service</Text>
|
||||
<AppShelfItemWrapper>
|
||||
<Card.Section>
|
||||
<Group position="center" mx="lg">
|
||||
<Text
|
||||
// TODO: #1 Remove this hack to get the text to be centered.
|
||||
ml={15}
|
||||
style={{
|
||||
alignSelf: 'center',
|
||||
alignContent: 'center',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
justifyItems: 'center',
|
||||
}}
|
||||
mt="sm"
|
||||
weight={500}
|
||||
>
|
||||
Add a service
|
||||
</Text>
|
||||
</Group>
|
||||
</Card>
|
||||
</AspectRatio>
|
||||
</Card.Section>
|
||||
<Card.Section>
|
||||
<AspectRatio ratio={5 / 3} m="xl">
|
||||
<motion.i
|
||||
whileHover={{
|
||||
cursor: 'pointer',
|
||||
scale: 1.1,
|
||||
}}
|
||||
>
|
||||
<Apps style={{ cursor: 'pointer' }} onClick={() => setOpened(true)} size={60} />
|
||||
</motion.i>
|
||||
</AspectRatio>
|
||||
</Card.Section>
|
||||
</AppShelfItemWrapper>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import { motion } from 'framer-motion';
|
||||
import {
|
||||
Text,
|
||||
AspectRatio,
|
||||
createStyles,
|
||||
SimpleGrid,
|
||||
Card,
|
||||
useMantineTheme,
|
||||
@@ -11,20 +10,12 @@ import {
|
||||
Group,
|
||||
Space,
|
||||
} from '@mantine/core';
|
||||
import AppShelfMenu from './AppShelfMenu';
|
||||
import AddItemShelfItem from './AddAppShelfItem';
|
||||
import { useConfig } from '../../tools/state';
|
||||
import { pingQbittorrent } from '../../tools/api';
|
||||
import { serviceItem } from '../../tools/types';
|
||||
|
||||
const useStyles = createStyles((theme) => ({
|
||||
main: {
|
||||
backgroundColor: theme.colorScheme === 'dark' ? theme.colors.dark[5] : theme.colors.gray[1],
|
||||
//TODO: #3 Fix this temporary fix and make the width and height dynamic / responsive
|
||||
width: 200,
|
||||
height: 180,
|
||||
},
|
||||
}));
|
||||
import AddItemShelfItem from './AddAppShelfItem';
|
||||
import { AppShelfItemWrapper } from './AppShelfItemWrapper';
|
||||
import AppShelfMenu from './AppShelfMenu';
|
||||
|
||||
const AppShelf = (props: any) => {
|
||||
const { config, addService, removeService, setConfig } = useConfig();
|
||||
@@ -58,7 +49,6 @@ export function AppShelfItem(props: any) {
|
||||
const { service }: { service: serviceItem } = props;
|
||||
const theme = useMantineTheme();
|
||||
const { removeService } = useConfig();
|
||||
const { classes } = useStyles();
|
||||
const [hovering, setHovering] = useState(false);
|
||||
return (
|
||||
<motion.div
|
||||
@@ -70,13 +60,7 @@ export function AppShelfItem(props: any) {
|
||||
setHovering(false);
|
||||
}}
|
||||
>
|
||||
<Card
|
||||
className={classes.main}
|
||||
style={{
|
||||
boxShadow: hovering ? '0px 0px 3px rgba(0, 0, 0, 0.5)' : '0px 0px 1px rgba(0, 0, 0, 0.5)',
|
||||
}}
|
||||
radius="md"
|
||||
>
|
||||
<AppShelfItemWrapper hovering={hovering}>
|
||||
<Card.Section>
|
||||
<Group position="apart" mx="lg">
|
||||
<Space />
|
||||
@@ -128,7 +112,7 @@ export function AppShelfItem(props: any) {
|
||||
</motion.i>
|
||||
</AspectRatio>
|
||||
</Card.Section>
|
||||
</Card>
|
||||
</AppShelfItemWrapper>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
21
components/AppShelf/AppShelfItemWrapper.tsx
Normal file
21
components/AppShelf/AppShelfItemWrapper.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import { useMantineTheme, Card } from '@mantine/core';
|
||||
|
||||
export function AppShelfItemWrapper(props: any) {
|
||||
const { children, hovering } = props;
|
||||
const theme = useMantineTheme();
|
||||
return (
|
||||
<Card
|
||||
style={{
|
||||
boxShadow: hovering ? '0px 0px 3px rgba(0, 0, 0, 0.5)' : '0px 0px 1px rgba(0, 0, 0, 0.5)',
|
||||
backgroundColor: theme.colorScheme === 'dark' ? theme.colors.dark[6] : theme.colors.gray[1],
|
||||
|
||||
//TODO: #3 Fix this temporary fix and make the width and height dynamic / responsive
|
||||
width: 200,
|
||||
height: 180,
|
||||
}}
|
||||
radius="md"
|
||||
>
|
||||
{children}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
45
components/ColorSchemeToggle/ColorSchemeSwitch.tsx
Normal file
45
components/ColorSchemeToggle/ColorSchemeSwitch.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import React from 'react';
|
||||
import { createStyles, Switch, Group, useMantineColorScheme } from '@mantine/core';
|
||||
import { Sun, MoonStars } from 'tabler-icons-react';
|
||||
|
||||
const useStyles = createStyles((theme) => ({
|
||||
root: {
|
||||
position: 'relative',
|
||||
'& *': {
|
||||
cursor: 'pointer',
|
||||
},
|
||||
},
|
||||
|
||||
icon: {
|
||||
pointerEvents: 'none',
|
||||
position: 'absolute',
|
||||
zIndex: 1,
|
||||
top: 3,
|
||||
},
|
||||
|
||||
iconLight: {
|
||||
left: 4,
|
||||
color: theme.white,
|
||||
},
|
||||
|
||||
iconDark: {
|
||||
right: 4,
|
||||
color: theme.colors.gray[6],
|
||||
},
|
||||
}));
|
||||
|
||||
export function ColorSchemeSwitch() {
|
||||
const { colorScheme, toggleColorScheme } = useMantineColorScheme();
|
||||
const { classes, cx } = useStyles();
|
||||
|
||||
return (
|
||||
<Group>
|
||||
<div className={classes.root}>
|
||||
<Sun className={cx(classes.icon, classes.iconLight)} size={18} />
|
||||
<MoonStars className={cx(classes.icon, classes.iconDark)} size={18} />
|
||||
<Switch checked={colorScheme === 'dark'} onChange={() => toggleColorScheme()} size="md" />
|
||||
</div>
|
||||
Switch to {colorScheme === 'dark' ? 'light' : 'dark'} mode
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
@@ -8,13 +8,16 @@ import {
|
||||
Tooltip,
|
||||
SegmentedControl,
|
||||
} from '@mantine/core';
|
||||
import { useColorScheme } from '@mantine/hooks';
|
||||
import { useState } from 'react';
|
||||
import { Settings as SettingsIcon } from 'tabler-icons-react';
|
||||
import { useConfig } from '../../tools/state';
|
||||
import { ColorSchemeSwitch } from '../ColorSchemeToggle/ColorSchemeSwitch';
|
||||
import SaveConfigComponent from '../Config/SaveConfig';
|
||||
|
||||
function SettingsMenu(props: any) {
|
||||
const { config, setConfig } = useConfig();
|
||||
const colorScheme = useColorScheme();
|
||||
const matches = [
|
||||
{ label: 'Google', value: 'https://google.com/search?q=' },
|
||||
{ label: 'DuckDuckGo', value: 'https://duckduckgo.com/?q=' },
|
||||
@@ -46,6 +49,7 @@ function SettingsMenu(props: any) {
|
||||
</Group>
|
||||
<Group direction="column">
|
||||
<Switch
|
||||
size="md"
|
||||
onChange={(e) =>
|
||||
setConfig({
|
||||
...config,
|
||||
@@ -59,6 +63,7 @@ function SettingsMenu(props: any) {
|
||||
label="Enable search bar"
|
||||
/>
|
||||
</Group>
|
||||
<ColorSchemeSwitch />
|
||||
<SaveConfigComponent />
|
||||
<Text
|
||||
style={{
|
||||
|
||||
18
components/layout/Aside.tsx
Normal file
18
components/layout/Aside.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Aside as MantineAside } from '@mantine/core';
|
||||
import { CalendarModule } from '../modules/calendar/CalendarModule';
|
||||
import ModuleWrapper from '../modules/moduleWrapper';
|
||||
|
||||
export default function Aside() {
|
||||
return (
|
||||
<MantineAside
|
||||
height="100%"
|
||||
hiddenBreakpoint="md"
|
||||
hidden
|
||||
width={{
|
||||
base: 'auto',
|
||||
}}
|
||||
>
|
||||
<ModuleWrapper module={CalendarModule} />
|
||||
</MantineAside>
|
||||
);
|
||||
}
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
import { useBooleanToggle } from '@mantine/hooks';
|
||||
import { NextLink } from '@mantine/next';
|
||||
import { Logo } from './Logo';
|
||||
import { ColorSchemeToggle } from '../ColorSchemeToggle/ColorSchemeToggle';
|
||||
import { SettingsMenuButton } from '../Settings/SettingsMenu';
|
||||
import CalendarComponent from '../modules/calendar/CalendarModule';
|
||||
|
||||
@@ -119,7 +118,6 @@ export function Header({ links }: HeaderResponsiveProps) {
|
||||
<Head height={HEADER_HEIGHT} mb={10} className={classes.root}>
|
||||
<Container className={classes.header}>
|
||||
<Group>
|
||||
<ColorSchemeToggle />
|
||||
<NextLink style={{ textDecoration: 'none' }} href="/">
|
||||
<Logo style={{ fontSize: 22 }} />
|
||||
</NextLink>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { AppShell, Aside, Center, createStyles } from '@mantine/core';
|
||||
import { AppShell, Center, createStyles } from '@mantine/core';
|
||||
import { Header } from './Header';
|
||||
import { Footer } from './Footer';
|
||||
import CalendarComponent from '../modules/calendar/CalendarModule';
|
||||
import Aside from './Aside';
|
||||
import Navbar from './Navbar';
|
||||
|
||||
const useStyles = createStyles((theme) => ({
|
||||
main: {
|
||||
@@ -15,18 +16,8 @@ export default function Layout({ children, style }: any) {
|
||||
const { classes, cx } = useStyles();
|
||||
return (
|
||||
<AppShell
|
||||
aside={
|
||||
<Aside
|
||||
height="auto"
|
||||
hiddenBreakpoint="md"
|
||||
hidden
|
||||
width={{
|
||||
base: 'auto',
|
||||
}}
|
||||
>
|
||||
<CalendarComponent />
|
||||
</Aside>
|
||||
}
|
||||
navbar={<Navbar />}
|
||||
aside={<Aside />}
|
||||
header={<Header links={[]} />}
|
||||
footer={<Footer links={[]} />}
|
||||
>
|
||||
|
||||
18
components/layout/Navbar.tsx
Normal file
18
components/layout/Navbar.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Navbar as MantineNavbar } from '@mantine/core';
|
||||
import { DateModule } from '../modules/date/DateModule';
|
||||
import ModuleWrapper from '../modules/moduleWrapper';
|
||||
|
||||
export default function Navbar() {
|
||||
return (
|
||||
<MantineNavbar
|
||||
height="100%"
|
||||
hiddenBreakpoint="md"
|
||||
hidden
|
||||
width={{
|
||||
base: 'auto',
|
||||
}}
|
||||
>
|
||||
<ModuleWrapper module={DateModule} />
|
||||
</MantineNavbar>
|
||||
);
|
||||
}
|
||||
@@ -1,14 +1,13 @@
|
||||
/* eslint-disable react/no-children-prop */
|
||||
import { Popover, Box, ScrollArea, Divider, Indicator } from '@mantine/core';
|
||||
import { useEffect, useState } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Calendar } from '@mantine/dates';
|
||||
import { CalendarIcon } from '@modulz/radix-icons';
|
||||
import { RadarrMediaDisplay, SonarrMediaDisplay } from './MediaDisplay';
|
||||
import { useConfig } from '../../../tools/state';
|
||||
import { MHPModule } from '../modules';
|
||||
import React from 'react';
|
||||
import { IModule } from '../modules';
|
||||
|
||||
export const CalendarModule: MHPModule = {
|
||||
export const CalendarModule: IModule = {
|
||||
title: 'Calendar',
|
||||
description:
|
||||
'A calendar module for displaying upcoming releases. It interacts with the Sonarr and Radarr API.',
|
||||
@@ -94,12 +93,8 @@ function DayComponent(props: any) {
|
||||
setOpened(true);
|
||||
}}
|
||||
>
|
||||
{radarrFiltered.length > 0 && (
|
||||
<Indicator size={7} color="yellow" children={null} />
|
||||
)}
|
||||
{sonarrFiltered.length > 0 && (
|
||||
<Indicator size={7} offset={8} color="blue" children={null} />
|
||||
)}
|
||||
{radarrFiltered.length > 0 && <Indicator size={7} color="yellow" children={null} />}
|
||||
{sonarrFiltered.length > 0 && <Indicator size={7} offset={8} color="blue" children={null} />}
|
||||
<Popover
|
||||
position="left"
|
||||
width={700}
|
||||
@@ -109,25 +104,21 @@ function DayComponent(props: any) {
|
||||
target={` ${day}`}
|
||||
>
|
||||
<ScrollArea style={{ height: 400 }}>
|
||||
{sonarrFiltered.map((media: any, index: number) => {
|
||||
return (
|
||||
<React.Fragment key={index}>
|
||||
<SonarrMediaDisplay media={media} />
|
||||
{index < sonarrFiltered.length - 1 && <Divider variant="dashed" my="xl" />}
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
{sonarrFiltered.map((media: any, index: number) => (
|
||||
<React.Fragment key={index}>
|
||||
<SonarrMediaDisplay media={media} />
|
||||
{index < sonarrFiltered.length - 1 && <Divider variant="dashed" my="xl" />}
|
||||
</React.Fragment>
|
||||
))}
|
||||
{radarrFiltered.length > 0 && sonarrFiltered.length > 0 && (
|
||||
<Divider variant="dashed" my="xl" />
|
||||
)}
|
||||
{radarrFiltered.map((media: any, index: number) => {
|
||||
return (
|
||||
<React.Fragment key={index}>
|
||||
<RadarrMediaDisplay media={media} />
|
||||
{index < radarrFiltered.length - 1 && <Divider variant="dashed" my="xl" />}
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
{radarrFiltered.map((media: any, index: number) => (
|
||||
<React.Fragment key={index}>
|
||||
<RadarrMediaDisplay media={media} />
|
||||
{index < radarrFiltered.length - 1 && <Divider variant="dashed" my="xl" />}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</ScrollArea>
|
||||
</Popover>
|
||||
</Box>
|
||||
|
||||
7
components/modules/date/DateModule.story.tsx
Normal file
7
components/modules/date/DateModule.story.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
import DateComponent from './DateModule';
|
||||
|
||||
export default {
|
||||
title: 'Date module',
|
||||
};
|
||||
|
||||
export const Default = (args: any) => <DateComponent {...args} />;
|
||||
41
components/modules/date/DateModule.tsx
Normal file
41
components/modules/date/DateModule.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import { Group, Text, Title } from '@mantine/core';
|
||||
import dayjs from 'dayjs';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Clock } from 'tabler-icons-react';
|
||||
import { IModule } from '../modules';
|
||||
|
||||
export const DateModule: IModule = {
|
||||
title: 'Date',
|
||||
description: 'Show the current time and date in a card',
|
||||
icon: Clock,
|
||||
component: DateComponent,
|
||||
};
|
||||
|
||||
export default function DateComponent(props: any) {
|
||||
const [date, setDate] = useState(new Date());
|
||||
const hours = date.getHours();
|
||||
const minutes = date.getMinutes();
|
||||
|
||||
// Change date on minute change
|
||||
// Note: Using 10 000ms instead of 1000ms to chill a little :)
|
||||
useEffect(() => {
|
||||
setInterval(() => {
|
||||
setDate(new Date());
|
||||
}, 10000);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Group p="sm" direction="column">
|
||||
<Title>
|
||||
{hours < 10 ? `0${hours}` : hours}:{minutes < 10 ? `0${minutes}` : minutes}
|
||||
</Title>
|
||||
<Text size="xl">
|
||||
{
|
||||
// Use dayjs to format the date
|
||||
// https://day.js.org/en/getting-started/installation/
|
||||
dayjs(date).format('dddd, MMMM D YYYY')
|
||||
}
|
||||
</Text>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
20
components/modules/moduleWrapper.tsx
Normal file
20
components/modules/moduleWrapper.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Card, useMantineTheme } from '@mantine/core';
|
||||
import { IModule } from './modules';
|
||||
|
||||
export default function ModuleWrapper(props: any) {
|
||||
const { module }: { module: IModule } = props;
|
||||
const theme = useMantineTheme();
|
||||
return (
|
||||
<Card
|
||||
mx="sm"
|
||||
radius="lg"
|
||||
shadow="sm"
|
||||
style={{
|
||||
// Make background color of the card depend on the theme
|
||||
backgroundColor: theme.colorScheme === 'dark' ? theme.colors.dark[6] : 'white',
|
||||
}}
|
||||
>
|
||||
<module.component />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
// Each module should have its own interface and call the following function:
|
||||
// TODO: Add a function to register a module
|
||||
// Note: Maybe use context to keep track of the modules
|
||||
export interface MHPModule {
|
||||
export interface IModule {
|
||||
title: string;
|
||||
description: string;
|
||||
icon: React.ReactNode;
|
||||
|
||||
@@ -7,6 +7,7 @@ import { MantineProvider, ColorScheme, ColorSchemeProvider } from '@mantine/core
|
||||
import { NotificationsProvider } from '@mantine/notifications';
|
||||
import Layout from '../components/layout/Layout';
|
||||
import { ConfigProvider } from '../tools/state';
|
||||
import { theme } from '../tools/theme';
|
||||
|
||||
export default function App(props: AppProps & { colorScheme: ColorScheme }) {
|
||||
const { Component, pageProps } = props;
|
||||
@@ -27,7 +28,14 @@ export default function App(props: AppProps & { colorScheme: ColorScheme }) {
|
||||
</Head>
|
||||
|
||||
<ColorSchemeProvider colorScheme={colorScheme} toggleColorScheme={toggleColorScheme}>
|
||||
<MantineProvider theme={{ colorScheme }} withGlobalStyles withNormalizeCSS>
|
||||
<MantineProvider
|
||||
theme={{
|
||||
...theme,
|
||||
colorScheme,
|
||||
}}
|
||||
withGlobalStyles
|
||||
withNormalizeCSS
|
||||
>
|
||||
<NotificationsProvider position="top-right">
|
||||
<ConfigProvider>
|
||||
<Layout>
|
||||
|
||||
3
tools/theme.ts
Normal file
3
tools/theme.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { MantineProviderProps } from '@mantine/core';
|
||||
|
||||
export const theme: MantineProviderProps['theme'] = {};
|
||||
@@ -8853,16 +8853,11 @@ minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.1.2:
|
||||
dependencies:
|
||||
brace-expansion "^1.1.7"
|
||||
|
||||
minimist@^1.1.1, minimist@^1.2.5, minimist@^1.2.6:
|
||||
minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6, minimist@~1.2.5:
|
||||
version "1.2.6"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
|
||||
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
|
||||
|
||||
minimist@^1.2.0, minimist@~1.2.5:
|
||||
version "1.2.5"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
|
||||
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
|
||||
|
||||
minipass-collect@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/minipass-collect/-/minipass-collect-1.0.2.tgz#22b813bf745dc6edba2576b940022ad6edc8c617"
|
||||
|
||||
Reference in New Issue
Block a user