Add torrent module

This commit is contained in:
Manuel Ruwe
2022-12-31 17:48:46 +01:00
parent dff63400b8
commit 9d7d126b55
7 changed files with 55 additions and 15 deletions

View File

@@ -1,5 +1,10 @@
{ {
"settings": { "settings": {
"label": "Settings" "label": "Settings"
},
"errors": {
"unmappedOptions": {
"text": "<b>Un-used parameter in configuration detected</b><br /><code>{{key}}</code>. Homarr is unable to interprete and use this parameter. To avoid any unexpected behavior, back up your configuration and correct your configuration."
}
} }
} }

View File

@@ -1,10 +1,14 @@
{ {
"descriptor": { "descriptor": {
"name": "Torrent", "name": "BitTorrent",
"description": "Show the current download speed of supported services", "description": "Displays a list of the torrent which are currently downloading",
"settings": { "settings": {
"hideComplete": { "title": "Settings for BitTorrent integration",
"label": "Hide completed torrents" "displayCompletedTorrents": {
"label": "Display completed torrents"
},
"displayStaleTorrents": {
"label": "Display stale torrents"
} }
} }
}, },

View File

@@ -1,6 +1,7 @@
import { Button, Group, MultiSelect, Stack, Switch, TextInput } from '@mantine/core'; import { Alert, Button, Group, MultiSelect, Stack, Switch, TextInput, Text } from '@mantine/core';
import { ContextModalProps } from '@mantine/modals'; import { ContextModalProps } from '@mantine/modals';
import { useTranslation } from 'next-i18next'; import { IconAlertTriangle } from '@tabler/icons';
import { Trans, useTranslation } from 'next-i18next';
import { useState } from 'react'; import { useState } from 'react';
import Widgets from '../../../../widgets'; import Widgets from '../../../../widgets';
import type { IWidgetOptionValue } from '../../../../widgets/widgets'; import type { IWidgetOptionValue } from '../../../../widgets/widgets';
@@ -31,6 +32,8 @@ export const WidgetsEditModal = ({
if (!configName || !innerProps.options) return null; if (!configName || !innerProps.options) return null;
console.log(`loaded namespace modules/${innerProps.widgetId}`);
const handleChange = (key: string, value: IntegrationOptionsValueType) => { const handleChange = (key: string, value: IntegrationOptionsValueType) => {
setModuleProperties((prev) => { setModuleProperties((prev) => {
const copyOfPrev: any = { ...prev }; const copyOfPrev: any = { ...prev };
@@ -66,12 +69,28 @@ export const WidgetsEditModal = ({
return ( return (
<Stack> <Stack>
{items.map(([key, value]) => { {items.map(([key, value], index) => {
const option = (currentWidgetDefinition as any).options[key] as IWidgetOptionValue; const option = (currentWidgetDefinition as any).options[key] as IWidgetOptionValue;
if (!option) {
return (
<Alert icon={<IconAlertTriangle />} color="red">
<Text>
<Trans
i18nKey="modules/common:errors.unmappedOptions.text"
values={{ key }}
components={{ b: <b />, code: <code /> }}
/>
</Text>
</Alert>
);
}
switch (option.type) { switch (option.type) {
case 'switch': case 'switch':
return ( return (
<Switch <Switch
key={`${option.type}-${index}`}
label={t(`descriptor.settings.${key}.label`)} label={t(`descriptor.settings.${key}.label`)}
checked={value as boolean} checked={value as boolean}
onChange={(ev) => handleChange(key, ev.currentTarget.checked)} onChange={(ev) => handleChange(key, ev.currentTarget.checked)}
@@ -80,6 +99,7 @@ export const WidgetsEditModal = ({
case 'text': case 'text':
return ( return (
<TextInput <TextInput
key={`${option.type}-${index}`}
label={t(`descriptor.settings.${key}.label`)} label={t(`descriptor.settings.${key}.label`)}
value={value as string} value={value as string}
onChange={(ev) => handleChange(key, ev.currentTarget.value)} onChange={(ev) => handleChange(key, ev.currentTarget.value)}
@@ -88,6 +108,7 @@ export const WidgetsEditModal = ({
case 'multi-select': case 'multi-select':
return ( return (
<MultiSelect <MultiSelect
key={`${option.type}-${index}`}
data={getMutliselectData(key)} data={getMutliselectData(key)}
label={t(`descriptor.settings.${key}.label`)} label={t(`descriptor.settings.${key}.label`)}
value={value as string[]} value={value as string[]}

View File

@@ -11,7 +11,7 @@ interface TorrentsDataRequestParams {
export const useGetTorrentData = (params: TorrentsDataRequestParams) => export const useGetTorrentData = (params: TorrentsDataRequestParams) =>
useQuery({ useQuery({
queryKey: ['torrentsData', params.appId], queryKey: ['torrentsData', params.appId],
queryFn: async () => fetchData(), queryFn: fetchData,
refetchOnWindowFocus: true, refetchOnWindowFocus: true,
refetchInterval(_: any, query: Query) { refetchInterval(_: any, query: Query) {
if (query.state.fetchFailureCount < 3) { if (query.state.fetchFailureCount < 3) {

View File

@@ -23,7 +23,7 @@ export const dashboardNamespaces = [
'modules/dlspeed', 'modules/dlspeed',
'modules/usenet', 'modules/usenet',
'modules/search', 'modules/search',
'modules/torrents', 'modules/torrents-status',
'modules/weather', 'modules/weather',
'modules/ping', 'modules/ping',
'modules/docker', 'modules/docker',

View File

@@ -10,11 +10,9 @@ interface WidgetWrapperProps {
children: ReactNode; children: ReactNode;
} }
export const WidgetWrapper = ({ widgetId, widget, className, children }: WidgetWrapperProps) => { export const WidgetWrapper = ({ widgetId, widget, className, children }: WidgetWrapperProps) => (
return (
<HomarrCardWrapper className={className}> <HomarrCardWrapper className={className}>
<WidgetsMenu integration={widgetId} widget={widget} /> <WidgetsMenu integration={widgetId} widget={widget} />
{children} {children}
</HomarrCardWrapper> </HomarrCardWrapper>
); );
};

View File

@@ -1,4 +1,4 @@
import { NormalizedTorrent } from '@ctrl/shared-torrent'; import { NormalizedTorrent, TorrentState } from '@ctrl/shared-torrent';
import { import {
Center, Center,
Group, Group,
@@ -52,7 +52,7 @@ interface BitTorrentTileProps {
} }
function BitTorrentTile({ widget }: BitTorrentTileProps) { function BitTorrentTile({ widget }: BitTorrentTileProps) {
const { t } = useTranslation('modules/torrents'); const { t } = useTranslation('modules/torrents-status');
const MIN_WIDTH_MOBILE = useMantineTheme().breakpoints.xs; const MIN_WIDTH_MOBILE = useMantineTheme().breakpoints.xs;
const { width } = useElementSize(); const { width } = useElementSize();
@@ -112,6 +112,18 @@ function BitTorrentTile({ widget }: BitTorrentTileProps) {
); );
} }
const filter = (torrent: NormalizedTorrent) => {
if (!widget.properties.displayCompletedTorrents && torrent.isCompleted) {
return false;
}
if (!widget.properties.displayStaleTorrents && !torrent.isCompleted && torrent.eta <= 0) {
return false;
}
return true;
};
return ( return (
<ScrollArea sx={{ height: 300, width: '100%' }}> <ScrollArea sx={{ height: 300, width: '100%' }}>
<Table highlightOnHover p="sm"> <Table highlightOnHover p="sm">
@@ -126,7 +138,7 @@ function BitTorrentTile({ widget }: BitTorrentTileProps) {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{data.map((item: NormalizedTorrent, index: number) => ( {data.filter(filter).map((item: NormalizedTorrent, index: number) => (
<BitTorrrentQueueItem key={index} torrent={item} /> <BitTorrrentQueueItem key={index} torrent={item} />
))} ))}
</tbody> </tbody>