Skip to content

Commit dab394d

Browse files
authored
🚀 v0.7.0 : Theming, Password protection, Autocompletion, Transmission, Mobile responsiveness! This is a big upgrade 👀
2 parents e718fd6 + 5b14375 commit dab394d

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

55 files changed

+1667
-590
lines changed

‎.github/pull_request_template.md

-7
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,3 @@
1414
1515
### Screenshot _(if applicable)_
1616
> If you've introduced any significant UI changes, please include a screenshot.
17-
18-
### Code Quality Checklist _(Please complete)_
19-
- [ ] All changes are backwards compatible
20-
- [ ] There are no (new) build warnings or errors
21-
- [ ] _(If a new config option is added)_ Attribute is outlined in the schema and documented
22-
- [ ] _(If a new dependency is added)_ Package is essential, and has been checked out for security or performance
23-
- [ ] Bumps version, if new feature added

‎.github/workflows/docker_dev.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ on:
1616
workflow_dispatch:
1717
inputs:
1818
tags:
19-
requierd: true
19+
required: true
2020
description: 'Tags to deploy to'
2121

2222
env:

‎Dockerfile

+1
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,6 @@ COPY /.next/standalone ./
99
COPY /.next/static ./.next/static
1010
EXPOSE 7575
1111
ENV PORT 7575
12+
RUN apk add tzdata
1213
VOLUME /app/data/configs
1314
CMD ["node", "server.js"]

‎data/constants.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
export const REPO_URL = 'ajnart/homarr';
2-
export const CURRENT_VERSION = 'v0.6.0';
2+
export const CURRENT_VERSION = 'v0.7.0';

‎package.json

+12-11
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "homarr",
3-
"version": "0.6.0",
3+
"version": "0.7.0",
44
"description": "Homarr - A homepage for your server.",
55
"repository": {
66
"type": "git",
@@ -24,26 +24,27 @@
2424
"ci": "yarn test && yarn lint --fix && yarn typecheck && yarn prettier:write"
2525
},
2626
"dependencies": {
27-
"@ctrl/deluge": "^4.0.0",
27+
"@ctrl/deluge": "^4.1.0",
2828
"@ctrl/qbittorrent": "^4.0.0",
2929
"@ctrl/shared-torrent": "^4.1.0",
30+
"@ctrl/transmission": "^4.1.1",
3031
"@dnd-kit/core": "^6.0.1",
3132
"@dnd-kit/sortable": "^7.0.0",
3233
"@dnd-kit/utilities": "^3.2.0",
33-
"@mantine/core": "^4.2.6",
34-
"@mantine/dates": "^4.2.6",
35-
"@mantine/dropzone": "^4.2.6",
36-
"@mantine/form": "^4.2.6",
37-
"@mantine/hooks": "^4.2.6",
38-
"@mantine/next": "^4.2.6",
39-
"@mantine/notifications": "^4.2.6",
40-
"@mantine/prism": "^4.2.6",
34+
"@mantine/core": "^4.2.8",
35+
"@mantine/dates": "^4.2.8",
36+
"@mantine/dropzone": "^4.2.8",
37+
"@mantine/form": "^4.2.8",
38+
"@mantine/hooks": "^4.2.8",
39+
"@mantine/next": "^4.2.8",
40+
"@mantine/notifications": "^4.2.8",
41+
"@mantine/prism": "^4.2.8",
4142
"@nivo/core": "^0.79.0",
4243
"@nivo/line": "^0.79.1",
4344
"@tabler/icons": "^1.68.0",
4445
"axios": "^0.27.2",
4546
"cookies-next": "^2.0.4",
46-
"dayjs": "^1.11.2",
47+
"dayjs": "^1.11.3",
4748
"framer-motion": "^6.3.1",
4849
"js-file-download": "^0.4.12",
4950
"next": "12.1.6",

‎src/components/AppShelf/AddAppShelfItem.tsx

+31-23
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@ import {
1414
Text,
1515
} from '@mantine/core';
1616
import { useForm } from '@mantine/form';
17-
import { useState } from 'react';
17+
import { useEffect, useState } from 'react';
1818
import { IconApps as Apps } from '@tabler/icons';
1919
import { v4 as uuidv4 } from 'uuid';
20+
import { useDebouncedValue } from '@mantine/hooks';
2021
import { useConfig } from '../../tools/state';
2122
import { ServiceTypeList } from '../../tools/types';
2223

@@ -64,24 +65,24 @@ function MatchIcon(name: string, form: any) {
6465
}
6566

6667
function MatchService(name: string, form: any) {
67-
const service = ServiceTypeList.find((s) => s === name);
68+
const service = ServiceTypeList.find((s) => s.toLowerCase() === name.toLowerCase());
6869
if (service) {
6970
form.setFieldValue('type', service);
7071
}
7172
}
7273

7374
function MatchPort(name: string, form: any) {
7475
const portmap = [
75-
{ name: 'qBittorrent', value: '8080' },
76-
{ name: 'Sonarr', value: '8989' },
77-
{ name: 'Radarr', value: '7878' },
78-
{ name: 'Lidarr', value: '8686' },
79-
{ name: 'Readarr', value: '8686' },
80-
{ name: 'Deluge', value: '8112' },
81-
{ name: 'Transmission', value: '9091' },
76+
{ name: 'qbittorrent', value: '8080' },
77+
{ name: 'sonarr', value: '8989' },
78+
{ name: 'radarr', value: '7878' },
79+
{ name: 'lidarr', value: '8686' },
80+
{ name: 'readarr', value: '8686' },
81+
{ name: 'deluge', value: '8112' },
82+
{ name: 'transmission', value: '9091' },
8283
];
8384
// Match name with portmap key
84-
const port = portmap.find((p) => p.name === name);
85+
const port = portmap.find((p) => p.name === name.toLowerCase());
8586
if (port) {
8687
form.setFieldValue('url', `http://localhost:${port.value}`);
8788
}
@@ -111,6 +112,7 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } &
111112
apiKey: props.apiKey ?? (undefined as unknown as string),
112113
username: props.username ?? (undefined as unknown as string),
113114
password: props.password ?? (undefined as unknown as string),
115+
openedUrl: props.openedUrl ?? (undefined as unknown as string),
114116
},
115117
validate: {
116118
apiKey: () => null,
@@ -134,6 +136,14 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } &
134136
},
135137
});
136138

139+
const [debounced, cancel] = useDebouncedValue(form.values.name, 250);
140+
useEffect(() => {
141+
if (form.values.name !== debounced || props.name || props.type) return;
142+
MatchIcon(form.values.name, form);
143+
MatchService(form.values.name, form);
144+
MatchPort(form.values.name, form);
145+
}, [debounced]);
146+
137147
// Try to set const hostname to new URL(form.values.url).hostname)
138148
// If it fails, set it to the form.values.url
139149
let hostname = form.values.url;
@@ -186,28 +196,26 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } &
186196
required
187197
label="Service name"
188198
placeholder="Plex"
189-
value={form.values.name}
190-
onChange={(event) => {
191-
form.setFieldValue('name', event.currentTarget.value);
192-
MatchIcon(event.currentTarget.value, form);
193-
MatchService(event.currentTarget.value, form);
194-
MatchPort(event.currentTarget.value, form);
195-
}}
196-
error={form.errors.name && 'Invalid icon url'}
199+
{...form.getInputProps('name')}
197200
/>
198201

199202
<TextInput
200203
required
201-
label="Icon url"
202-
placeholder="https://i.gifer.com/ANPC.gif"
204+
label="Icon URL"
205+
placeholder="/favicon.svg"
203206
{...form.getInputProps('icon')}
204207
/>
205208
<TextInput
206209
required
207-
label="Service url"
210+
label="Service URL"
208211
placeholder="http://localhost:7575"
209212
{...form.getInputProps('url')}
210213
/>
214+
<TextInput
215+
label="New tab URL"
216+
placeholder="http://sonarr.example.com"
217+
{...form.getInputProps('openedUrl')}
218+
/>
211219
<Select
212220
label="Service type"
213221
defaultValue="Other"
@@ -292,12 +300,12 @@ export function AddAppShelfItemForm(props: { setOpened: (b: boolean) => void } &
292300
/>
293301
</>
294302
)}
295-
{form.values.type === 'Deluge' && (
303+
{(form.values.type === 'Deluge' || form.values.type === 'Transmission') && (
296304
<>
297305
<TextInput
298306
required
299307
label="Password"
300-
placeholder="deluge"
308+
placeholder="password"
301309
value={form.values.password}
302310
onChange={(event) => {
303311
form.setFieldValue('password', event.currentTarget.value);

‎src/components/AppShelf/AppShelf.tsx

+70-19
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,50 @@
11
import React, { useState } from 'react';
2-
import { Grid, Group, Title } from '@mantine/core';
2+
import { Accordion, createStyles, Grid, Group, Paper, useMantineColorScheme } from '@mantine/core';
33
import {
44
closestCenter,
55
DndContext,
66
DragOverlay,
77
MouseSensor,
8+
TouchSensor,
89
useSensor,
910
useSensors,
1011
} from '@dnd-kit/core';
1112
import { arrayMove, SortableContext } from '@dnd-kit/sortable';
13+
import { useLocalStorage } from '@mantine/hooks';
1214
import { useConfig } from '../../tools/state';
1315

1416
import { SortableAppShelfItem, AppShelfItem } from './AppShelfItem';
15-
import { ModuleWrapper } from '../modules/moduleWrapper';
17+
import { ModuleMenu, ModuleWrapper } from '../modules/moduleWrapper';
1618
import { DownloadsModule } from '../modules';
19+
import DownloadComponent from '../modules/downloads/DownloadsModule';
20+
21+
const useStyles = createStyles((theme, _params) => ({
22+
item: {
23+
borderBottom: 0,
24+
overflow: 'hidden',
25+
border: '1px solid transparent',
26+
borderRadius: theme.radius.lg,
27+
marginTop: theme.spacing.md,
28+
},
29+
30+
itemOpened: {
31+
borderColor: theme.colorScheme === 'dark' ? theme.colors.dark[5] : theme.colors.gray[3],
32+
},
33+
}));
1734

1835
const AppShelf = (props: any) => {
36+
const { classes, cx } = useStyles(props);
37+
const [toggledCategories, settoggledCategories] = useLocalStorage({
38+
key: 'app-shelf-toggled',
39+
// This is a bit of a hack to get the 5 first categories to be toggled on by default
40+
defaultValue: { 0: true, 1: true, 2: true, 3: true, 4: true } as Record<string, boolean>,
41+
});
1942
const [activeId, setActiveId] = useState(null);
2043
const { config, setConfig } = useConfig();
44+
const { colorScheme } = useMantineColorScheme();
45+
2146
const sensors = useSensors(
47+
useSensor(TouchSensor, {}),
2248
useSensor(MouseSensor, {
2349
// Require the mouse to move by 10 pixels before activating
2450
activationConstraint: {
@@ -99,26 +125,51 @@ const AppShelf = (props: any) => {
99125
const noCategory = config.services.filter(
100126
(e) => e.category === undefined || e.category === null
101127
);
102-
128+
// Create an item with 0: true, 1: true, 2: true... For each category
103129
return (
104130
// Return one item for each category
105131
<Group grow direction="column">
106-
{categoryList.map((category) => (
107-
<>
108-
<Title order={3} key={category}>
109-
{category}
110-
</Title>
111-
{item(category)}
112-
</>
113-
))}
114-
{/* Return the item for all services without category */}
115-
{noCategory && noCategory.length > 0 ? (
116-
<>
117-
<Title order={3}>Other</Title>
118-
{item()}
119-
</>
120-
) : null}
121-
<ModuleWrapper mt="xl" module={DownloadsModule} />
132+
<Accordion
133+
disableIconRotation
134+
classNames={classes}
135+
order={2}
136+
iconPosition="right"
137+
multiple
138+
styles={{
139+
item: {
140+
borderRadius: '20px',
141+
},
142+
}}
143+
initialState={toggledCategories}
144+
onChange={(idx) => settoggledCategories(idx)}
145+
>
146+
{categoryList.map((category, idx) => (
147+
<Accordion.Item key={category} label={category}>
148+
{item(category)}
149+
</Accordion.Item>
150+
))}
151+
{/* Return the item for all services without category */}
152+
{noCategory && noCategory.length > 0 ? (
153+
<Accordion.Item key="Other" label="Other">
154+
{item()}
155+
</Accordion.Item>
156+
) : null}
157+
<Accordion.Item key="Downloads" label="Your downloads">
158+
<Paper
159+
p="lg"
160+
radius="lg"
161+
style={{
162+
background: `rgba(${colorScheme === 'dark' ? '37, 38, 43,' : '255, 255, 255,'} \
163+
${(config.settings.appOpacity || 100) / 100}`,
164+
borderColor: `rgba(${colorScheme === 'dark' ? '37, 38, 43,' : '233, 236, 239,'} \
165+
${(config.settings.appOpacity || 100) / 100}`,
166+
}}
167+
>
168+
<ModuleMenu module={DownloadsModule} />
169+
<DownloadComponent />
170+
</Paper>
171+
</Accordion.Item>
172+
</Accordion>
122173
</Group>
123174
);
124175
}

‎src/components/AppShelf/AppShelfItem.tsx

+31-4
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,21 @@
1-
import { Text, Card, Anchor, AspectRatio, Image, Center, createStyles } from '@mantine/core';
1+
import {
2+
Text,
3+
Card,
4+
Anchor,
5+
AspectRatio,
6+
Image,
7+
Center,
8+
createStyles,
9+
useMantineColorScheme,
10+
} from '@mantine/core';
211
import { motion } from 'framer-motion';
312
import { useState } from 'react';
413
import { useSortable } from '@dnd-kit/sortable';
514
import { CSS } from '@dnd-kit/utilities';
615
import { serviceItem } from '../../tools/types';
716
import PingComponent from '../modules/ping/PingModule';
817
import AppShelfMenu from './AppShelfMenu';
18+
import { useConfig } from '../../tools/state';
919

1020
const useStyles = createStyles((theme) => ({
1121
item: {
@@ -15,6 +25,9 @@ const useStyles = createStyles((theme) => ({
1525
boxShadow: `${theme.shadows.md} !important`,
1626
transform: 'scale(1.05)',
1727
},
28+
[theme.fn.smallerThan('sm')]: {
29+
WebkitUserSelect: 'none',
30+
},
1831
},
1932
}));
2033

@@ -38,7 +51,9 @@ export function SortableAppShelfItem(props: any) {
3851
export function AppShelfItem(props: any) {
3952
const { service }: { service: serviceItem } = props;
4053
const [hovering, setHovering] = useState(false);
41-
const { classes, theme } = useStyles();
54+
const { config } = useConfig();
55+
const { colorScheme } = useMantineColorScheme();
56+
const { classes } = useStyles();
4257
return (
4358
<motion.div
4459
animate={{
@@ -54,7 +69,18 @@ export function AppShelfItem(props: any) {
5469
setHovering(false);
5570
}}
5671
>
57-
<Card withBorder radius="lg" shadow="md" className={classes.item}>
72+
<Card
73+
withBorder
74+
radius="lg"
75+
shadow="md"
76+
className={classes.item}
77+
style={{
78+
background: `rgba(${colorScheme === 'dark' ? '37, 38, 43,' : '255, 255, 255,'} \
79+
${(config.settings.appOpacity || 100) / 100}`,
80+
borderColor: `rgba(${colorScheme === 'dark' ? '37, 38, 43,' : '233, 236, 239,'} \
81+
${(config.settings.appOpacity || 100) / 100}`,
82+
}}
83+
>
5884
<Card.Section>
5985
<Anchor
6086
target="_blank"
@@ -101,7 +127,8 @@ export function AppShelfItem(props: any) {
101127
src={service.icon}
102128
fit="contain"
103129
onClick={() => {
104-
window.open(service.url);
130+
if (service.openedUrl) window.open(service.openedUrl, '_blank');
131+
else window.open(service.url);
105132
}}
106133
/>
107134
</motion.i>

0 commit comments

Comments
 (0)