mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-09 23:15:46 +01:00
implement queue info and pause/resume
This commit is contained in:
21
.eslintrc.js
21
.eslintrc.js
@@ -3,9 +3,9 @@ module.exports = {
|
|||||||
'mantine',
|
'mantine',
|
||||||
'plugin:@next/next/recommended',
|
'plugin:@next/next/recommended',
|
||||||
'plugin:jest/recommended',
|
'plugin:jest/recommended',
|
||||||
"eslint:recommended",
|
'eslint:recommended',
|
||||||
"plugin:@typescript-eslint/eslint-recommended",
|
'plugin:@typescript-eslint/eslint-recommended',
|
||||||
"plugin:@typescript-eslint/recommended"
|
'plugin:@typescript-eslint/recommended',
|
||||||
],
|
],
|
||||||
plugins: ['testing-library', 'jest', 'react-hooks', 'react', 'unused-imports'],
|
plugins: ['testing-library', 'jest', 'react-hooks', 'react', 'unused-imports'],
|
||||||
overrides: [
|
overrides: [
|
||||||
@@ -20,12 +20,13 @@ module.exports = {
|
|||||||
rules: {
|
rules: {
|
||||||
'react/react-in-jsx-scope': 'off',
|
'react/react-in-jsx-scope': 'off',
|
||||||
'react/no-children-prop': 'off',
|
'react/no-children-prop': 'off',
|
||||||
"unused-imports/no-unused-imports": "warn",
|
'unused-imports/no-unused-imports': 'warn',
|
||||||
"@typescript-eslint/no-unused-vars": "off",
|
'@typescript-eslint/no-unused-vars': 'off',
|
||||||
"@typescript-eslint/no-unused-imports": "off",
|
'@typescript-eslint/no-unused-imports': 'off',
|
||||||
"@typescript-eslint/no-unused-expressions": "off",
|
'@typescript-eslint/no-unused-expressions': 'off',
|
||||||
"@typescript-eslint/no-explicit-any": "off",
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
"@typescript-eslint/no-shadow": "off",
|
'@typescript-eslint/no-shadow': 'off',
|
||||||
"@typescript-eslint/no-use-before-define": "off",
|
'@typescript-eslint/no-use-before-define': 'off',
|
||||||
|
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
68
@types/react-i18next.d.ts
vendored
Normal file
68
@types/react-i18next.d.ts
vendored
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import 'react-i18next';
|
||||||
|
|
||||||
|
import common from '../public/locales/en/common.json';
|
||||||
|
import appShelf from '../public/locales/en/layout/app-shelf.json';
|
||||||
|
import addServiceAppShelf from '../public/locales/en/layout/add-service-app-shelf.json';
|
||||||
|
import appShelfMenu from '../public/locales/en/layout/app-shelf-menu.json';
|
||||||
|
import commonSettings from '../public/locales/en/settings/common.json';
|
||||||
|
import themeSelector from '../public/locales/en/settings/general/theme-selector.json';
|
||||||
|
import configChanger from '../public/locales/en/settings/general/config-changer.json';
|
||||||
|
import i18n from '../public/locales/en/settings/general/internationalization.json';
|
||||||
|
import moduleEnabler from '../public/locales/en/settings/general/module-enabler.json';
|
||||||
|
import searchEngine from '../public/locales/en/settings/general/search-engine.json';
|
||||||
|
import widgetPositions from '../public/locales/en/settings/general/widget-positions.json';
|
||||||
|
import colorSelector from '../public/locales/en/settings/customization/color-selector.json';
|
||||||
|
import pageAppearance from '../public/locales/en/settings/customization/page-appearance.json';
|
||||||
|
import shadeSelector from '../public/locales/en/settings/customization/shade-selector.json';
|
||||||
|
import appWidth from '../public/locales/en/settings/customization/app-width.json';
|
||||||
|
import opacitySelector from '../public/locales/en/settings/customization/opacity-selector.json';
|
||||||
|
import commonModule from '../public/locales/en/modules/common.json';
|
||||||
|
import dateModule from '../public/locales/en/modules/date.json';
|
||||||
|
import calendarModule from '../public/locales/en/modules/calendar.json';
|
||||||
|
import dlSpeedModule from '../public/locales/en/modules/dlspeed.json';
|
||||||
|
import usenetModule from '../public/locales/en/modules/usenet.json';
|
||||||
|
import searchModule from '../public/locales/en/modules/search.json';
|
||||||
|
import torrentsModule from '../public/locales/en/modules/torrents-status.json';
|
||||||
|
import weatherModule from '../public/locales/en/modules/weather.json';
|
||||||
|
import pingModule from '../public/locales/en/modules/ping.json';
|
||||||
|
import dockerModule from '../public/locales/en/modules/docker.json';
|
||||||
|
import dashDotModule from '../public/locales/en/modules/dashdot.json';
|
||||||
|
import overseerrModule from '../public/locales/en/modules/overseerr.json';
|
||||||
|
import mediaCardsModule from '../public/locales/en/modules/common-media-cards.json';
|
||||||
|
|
||||||
|
declare module 'react-i18next' {
|
||||||
|
interface CustomTypeOptions {
|
||||||
|
defaultNS: 'common';
|
||||||
|
resources: {
|
||||||
|
common: typeof common;
|
||||||
|
'layout/app-shelf': typeof appShelf;
|
||||||
|
'layout/add-service-app-shelf': typeof addServiceAppShelf;
|
||||||
|
'layout/app-shelf-menu': typeof appShelfMenu;
|
||||||
|
'settings/common': typeof commonSettings;
|
||||||
|
'settings/general/theme-selector': typeof themeSelector;
|
||||||
|
'settings/general/config-changer': typeof configChanger;
|
||||||
|
'settings/general/internationalization': typeof i18n;
|
||||||
|
'settings/general/module-enabler': typeof moduleEnabler;
|
||||||
|
'settings/general/search-engine': typeof searchEngine;
|
||||||
|
'settings/general/widget-positions': typeof widgetPositions;
|
||||||
|
'settings/customization/color-selector': typeof colorSelector;
|
||||||
|
'settings/customization/page-appearance': typeof pageAppearance;
|
||||||
|
'settings/customization/shade-selector': typeof shadeSelector;
|
||||||
|
'settings/customization/app-width': typeof appWidth;
|
||||||
|
'settings/customization/opacity-selector': typeof opacitySelector;
|
||||||
|
'modules/common': typeof commonModule;
|
||||||
|
'modules/date': typeof dateModule;
|
||||||
|
'modules/calendar': typeof calendarModule;
|
||||||
|
'modules/dlspeed': typeof dlSpeedModule;
|
||||||
|
'modules/usenet': typeof usenetModule;
|
||||||
|
'modules/search': typeof searchModule;
|
||||||
|
'modules/torrents-status': typeof torrentsModule;
|
||||||
|
'modules/weather': typeof weatherModule;
|
||||||
|
'modules/ping': typeof pingModule;
|
||||||
|
'modules/docker': typeof dockerModule;
|
||||||
|
'modules/dashdot': typeof dashDotModule;
|
||||||
|
'modules/overseerr': typeof overseerrModule;
|
||||||
|
'modules/common-media-cards': typeof mediaCardsModule;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,14 +7,42 @@
|
|||||||
"type": "Other",
|
"type": "Other",
|
||||||
"icon": "https://c.tenor.com/o656qFKDzeUAAAAC/rick-astley-never-gonna-give-you-up.gif",
|
"icon": "https://c.tenor.com/o656qFKDzeUAAAAC/rick-astley-never-gonna-give-you-up.gif",
|
||||||
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
|
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "4cb1c2af-9f23-4f52-86e1-0eac25673b56",
|
||||||
|
"type": "Sabnzbd",
|
||||||
|
"name": "Sabnzbd",
|
||||||
|
"icon": "/favicon.png",
|
||||||
|
"url": "https://sabnzbd.jannesv.be",
|
||||||
|
"apiKey": "2982e4afbc6d42d6bb5863751a354d20",
|
||||||
|
"openedUrl": "https://sabnzbd.jannesv.be"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"settings": {
|
"settings": {
|
||||||
"searchUrl": "https://google.com/search?q="
|
"searchUrl": "https://google.com/search?q=",
|
||||||
|
"primaryColor": "grape"
|
||||||
},
|
},
|
||||||
"modules": {
|
"modules": {
|
||||||
"Search Bar": {
|
"Search Bar": {
|
||||||
"enabled": true
|
"enabled": true
|
||||||
|
},
|
||||||
|
"Download Speed": {
|
||||||
|
"enabled": false
|
||||||
|
},
|
||||||
|
"Torrent": {
|
||||||
|
"enabled": false
|
||||||
|
},
|
||||||
|
"Dash.": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"Usenet": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"usenet": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"docker": {
|
||||||
|
"enabled": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
35
public/locales/en/modules/usenet.json
Normal file
35
public/locales/en/modules/usenet.json
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"descriptor": {
|
||||||
|
"name": "Usenet",
|
||||||
|
"description": "Show the queue and history of supported services"
|
||||||
|
},
|
||||||
|
"tabs": {
|
||||||
|
"queue": "Queue",
|
||||||
|
"history": "History"
|
||||||
|
},
|
||||||
|
"info": {
|
||||||
|
"sizeLeft": "Size left",
|
||||||
|
"paused": "Paused"
|
||||||
|
},
|
||||||
|
"queue": {
|
||||||
|
"header": {
|
||||||
|
"name": "Name",
|
||||||
|
"size": "Size",
|
||||||
|
"eta": "ETA",
|
||||||
|
"progress": "Progress"
|
||||||
|
},
|
||||||
|
"empty": "Queue is empty.",
|
||||||
|
"error": "Some error has occured while fetching data:",
|
||||||
|
"paused": "Paused"
|
||||||
|
},
|
||||||
|
"history": {
|
||||||
|
"header": {
|
||||||
|
"name": "Name",
|
||||||
|
"size": "Size",
|
||||||
|
"duration": "Download Duration"
|
||||||
|
},
|
||||||
|
"empty": "Queue is empty.",
|
||||||
|
"error": "Some error has occured while fetching data:",
|
||||||
|
"paused": "Paused"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ import { IconAlertCircle } from '@tabler/icons';
|
|||||||
import { AxiosError } from 'axios';
|
import { AxiosError } from 'axios';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import duration from 'dayjs/plugin/duration';
|
import duration from 'dayjs/plugin/duration';
|
||||||
|
import { useTranslation } from 'next-i18next';
|
||||||
import { FunctionComponent, useState } from 'react';
|
import { FunctionComponent, useState } from 'react';
|
||||||
import { useGetUsenetHistory } from '../../tools/hooks/api';
|
import { useGetUsenetHistory } from '../../tools/hooks/api';
|
||||||
import { humanFileSize } from '../../tools/humanFileSize';
|
import { humanFileSize } from '../../tools/humanFileSize';
|
||||||
@@ -28,6 +29,7 @@ const PAGE_SIZE = 10;
|
|||||||
|
|
||||||
export const UsenetHistoryList: FunctionComponent<UsenetHistoryListProps> = ({ serviceId }) => {
|
export const UsenetHistoryList: FunctionComponent<UsenetHistoryListProps> = ({ serviceId }) => {
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
|
const { t } = useTranslation('modules/usenet');
|
||||||
|
|
||||||
const { data, isLoading, isError, error } = useGetUsenetHistory({
|
const { data, isLoading, isError, error } = useGetUsenetHistory({
|
||||||
limit: PAGE_SIZE,
|
limit: PAGE_SIZE,
|
||||||
@@ -50,7 +52,7 @@ export const UsenetHistoryList: FunctionComponent<UsenetHistoryListProps> = ({ s
|
|||||||
return (
|
return (
|
||||||
<Group position="center">
|
<Group position="center">
|
||||||
<Alert icon={<IconAlertCircle size={16} />} my="lg" title="Error!" color="red" radius="md">
|
<Alert icon={<IconAlertCircle size={16} />} my="lg" title="Error!" color="red" radius="md">
|
||||||
Some error has occured while fetching data:
|
{t('history.error')}
|
||||||
<Code mt="sm" block>
|
<Code mt="sm" block>
|
||||||
{(error as AxiosError)?.response?.data as string}
|
{(error as AxiosError)?.response?.data as string}
|
||||||
</Code>
|
</Code>
|
||||||
@@ -62,13 +64,13 @@ export const UsenetHistoryList: FunctionComponent<UsenetHistoryListProps> = ({ s
|
|||||||
if (!data || data.items.length <= 0) {
|
if (!data || data.items.length <= 0) {
|
||||||
return (
|
return (
|
||||||
<Center style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
<Center style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||||
<Title order={3}>History is empty</Title>
|
<Title order={3}>{t('history.empty')}</Title>
|
||||||
</Center>
|
</Center>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<>
|
||||||
<Table highlightOnHover style={{ tableLayout: 'fixed' }}>
|
<Table highlightOnHover style={{ tableLayout: 'fixed' }}>
|
||||||
<colgroup>
|
<colgroup>
|
||||||
<col span={1} />
|
<col span={1} />
|
||||||
@@ -77,9 +79,9 @@ export const UsenetHistoryList: FunctionComponent<UsenetHistoryListProps> = ({ s
|
|||||||
</colgroup>
|
</colgroup>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Name</th>
|
<th>{t('history.header.name')}</th>
|
||||||
<th>Size</th>
|
<th>{t('history.header.size')}</th>
|
||||||
<th>Download Duration</th>
|
<th>{t('history.header.duration')}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -113,14 +115,16 @@ export const UsenetHistoryList: FunctionComponent<UsenetHistoryListProps> = ({ s
|
|||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</Table>
|
</Table>
|
||||||
<Pagination
|
{totalPages > 1 && (
|
||||||
size="sm"
|
<Pagination
|
||||||
position="center"
|
size="sm"
|
||||||
mt="md"
|
position="center"
|
||||||
total={totalPages}
|
mt="md"
|
||||||
page={page}
|
total={totalPages}
|
||||||
onChange={setPage}
|
page={page}
|
||||||
/>
|
onChange={setPage}
|
||||||
</div>
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,16 +1,27 @@
|
|||||||
import { Group, Select, Tabs } from '@mantine/core';
|
import { Badge, Button, Group, Select, Tabs } from '@mantine/core';
|
||||||
import { IconDownload } from '@tabler/icons';
|
import { IconDownload, IconPlayerPause, IconPlayerPlay } from '@tabler/icons';
|
||||||
import { FunctionComponent, useState } from 'react';
|
import { FunctionComponent, useState } from 'react';
|
||||||
|
|
||||||
|
import { useTranslation } from 'next-i18next';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import duration from 'dayjs/plugin/duration';
|
||||||
import { IModule } from '../ModuleTypes';
|
import { IModule } from '../ModuleTypes';
|
||||||
import { UsenetQueueList } from './UsenetQueueList';
|
import { UsenetQueueList } from './UsenetQueueList';
|
||||||
import { UsenetHistoryList } from './UsenetHistoryList';
|
import { UsenetHistoryList } from './UsenetHistoryList';
|
||||||
import { useGetServiceByType } from '../../tools/hooks/useGetServiceByType';
|
import { useGetServiceByType } from '../../tools/hooks/useGetServiceByType';
|
||||||
|
import { useGetUsenetInfo, usePauseUsenetQueue, useResumeUsenetQueue } from '../../tools/hooks/api';
|
||||||
|
import { humanFileSize } from '../../tools/humanFileSize';
|
||||||
|
|
||||||
|
dayjs.extend(duration);
|
||||||
|
|
||||||
export const UsenetComponent: FunctionComponent = () => {
|
export const UsenetComponent: FunctionComponent = () => {
|
||||||
const downloadServices = useGetServiceByType('Sabnzbd');
|
const downloadServices = useGetServiceByType('Sabnzbd');
|
||||||
|
const { t } = useTranslation('modules/usenet');
|
||||||
|
|
||||||
const [selectedServiceId, setSelectedService] = useState<string | null>(downloadServices[0]?.id);
|
const [selectedServiceId, setSelectedService] = useState<string | null>(downloadServices[0]?.id);
|
||||||
|
const { data } = useGetUsenetInfo({ serviceId: selectedServiceId! });
|
||||||
|
const { mutate: pause } = usePauseUsenetQueue({ serviceId: selectedServiceId! });
|
||||||
|
const { mutate: resume } = useResumeUsenetQueue({ serviceId: selectedServiceId! });
|
||||||
|
|
||||||
if (!selectedServiceId) {
|
if (!selectedServiceId) {
|
||||||
return null;
|
return null;
|
||||||
@@ -20,8 +31,26 @@ export const UsenetComponent: FunctionComponent = () => {
|
|||||||
<Tabs keepMounted={false} defaultValue="queue">
|
<Tabs keepMounted={false} defaultValue="queue">
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<Tabs.List style={{ flex: 1 }}>
|
<Tabs.List style={{ flex: 1 }}>
|
||||||
<Tabs.Tab value="queue">Queue</Tabs.Tab>
|
<Tabs.Tab value="queue">{t('tabs.queue')}</Tabs.Tab>
|
||||||
<Tabs.Tab value="history">History</Tabs.Tab>
|
<Tabs.Tab value="history">{t('tabs.history')}</Tabs.Tab>
|
||||||
|
{data && (
|
||||||
|
<Group position="right" ml="auto" mb="lg">
|
||||||
|
<Badge>{humanFileSize(data?.speed)}/s</Badge>
|
||||||
|
<Badge>
|
||||||
|
{t('info.sizeLeft')}: {humanFileSize(data?.sizeLeft)}
|
||||||
|
</Badge>
|
||||||
|
{data.paused ? (
|
||||||
|
<Button uppercase onClick={() => resume()} radius="xl" size="xs">
|
||||||
|
<IconPlayerPlay size={16} style={{ marginRight: 5 }} /> {t('info.paused')}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button uppercase onClick={() => pause()} radius="xl" size="xs">
|
||||||
|
<IconPlayerPause size={16} style={{ marginRight: 5 }} />{' '}
|
||||||
|
{dayjs.duration(data.eta, 's').format('HH:mm:ss')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
)}
|
||||||
</Tabs.List>
|
</Tabs.List>
|
||||||
{downloadServices.length > 1 && (
|
{downloadServices.length > 1 && (
|
||||||
<Select
|
<Select
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
|
Button,
|
||||||
Center,
|
Center,
|
||||||
Code,
|
Code,
|
||||||
Group,
|
Group,
|
||||||
|
Pagination,
|
||||||
Progress,
|
Progress,
|
||||||
Skeleton,
|
Skeleton,
|
||||||
Table,
|
Table,
|
||||||
@@ -15,6 +17,7 @@ import { IconAlertCircle, IconPlayerPause, IconPlayerPlay } from '@tabler/icons'
|
|||||||
import { AxiosError } from 'axios';
|
import { AxiosError } from 'axios';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import duration from 'dayjs/plugin/duration';
|
import duration from 'dayjs/plugin/duration';
|
||||||
|
import { useTranslation } from 'next-i18next';
|
||||||
import { FunctionComponent, useState } from 'react';
|
import { FunctionComponent, useState } from 'react';
|
||||||
import { useGetUsenetDownloads } from '../../tools/hooks/api';
|
import { useGetUsenetDownloads } from '../../tools/hooks/api';
|
||||||
import { humanFileSize } from '../../tools/humanFileSize';
|
import { humanFileSize } from '../../tools/humanFileSize';
|
||||||
@@ -29,12 +32,15 @@ const PAGE_SIZE = 10;
|
|||||||
|
|
||||||
export const UsenetQueueList: FunctionComponent<UsenetQueueListProps> = ({ serviceId }) => {
|
export const UsenetQueueList: FunctionComponent<UsenetQueueListProps> = ({ serviceId }) => {
|
||||||
const theme = useMantineTheme();
|
const theme = useMantineTheme();
|
||||||
|
const { t } = useTranslation('modules/usenet');
|
||||||
|
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const { data, isLoading, isError, error } = useGetUsenetDownloads({
|
const { data, isLoading, isError, error } = useGetUsenetDownloads({
|
||||||
limit: PAGE_SIZE,
|
limit: PAGE_SIZE,
|
||||||
offset: (page - 1) * PAGE_SIZE,
|
offset: (page - 1) * PAGE_SIZE,
|
||||||
serviceId,
|
serviceId,
|
||||||
});
|
});
|
||||||
|
const totalPages = Math.ceil((data?.total || 1) / PAGE_SIZE);
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
@@ -50,7 +56,7 @@ export const UsenetQueueList: FunctionComponent<UsenetQueueListProps> = ({ servi
|
|||||||
return (
|
return (
|
||||||
<Group position="center">
|
<Group position="center">
|
||||||
<Alert icon={<IconAlertCircle size={16} />} my="lg" title="Error!" color="red" radius="md">
|
<Alert icon={<IconAlertCircle size={16} />} my="lg" title="Error!" color="red" radius="md">
|
||||||
Some error has occured while fetching data:
|
{t('queue.error')}
|
||||||
<Code mt="sm" block>
|
<Code mt="sm" block>
|
||||||
{(error as AxiosError)?.response?.data as string}
|
{(error as AxiosError)?.response?.data as string}
|
||||||
</Code>
|
</Code>
|
||||||
@@ -62,71 +68,88 @@ export const UsenetQueueList: FunctionComponent<UsenetQueueListProps> = ({ servi
|
|||||||
if (!data || data.items.length <= 0) {
|
if (!data || data.items.length <= 0) {
|
||||||
return (
|
return (
|
||||||
<Center style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
<Center style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||||
<Title order={3}>Queue is empty</Title>
|
<Title order={3}>{t('queue.empty')}</Title>
|
||||||
</Center>
|
</Center>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Table highlightOnHover style={{ tableLayout: 'fixed' }}>
|
<>
|
||||||
<thead>
|
<Table highlightOnHover style={{ tableLayout: 'fixed' }}>
|
||||||
<tr>
|
<thead>
|
||||||
<th style={{ width: 40 }} />
|
<tr>
|
||||||
<th style={{ width: '75%' }}>Name</th>
|
<th style={{ width: 50 }} />
|
||||||
<th style={{ width: 100 }}>Size</th>
|
<th style={{ width: '75%' }}>{t('queue.header.name')}</th>
|
||||||
<th style={{ width: 100 }}>ETA</th>
|
<th style={{ width: 100 }}>{t('queue.header.size')}</th>
|
||||||
<th style={{ width: 200 }}>Progress</th>
|
<th style={{ width: 100 }}>{t('queue.header.eta')}</th>
|
||||||
</tr>
|
<th style={{ width: 200 }}>{t('queue.header.progress')}</th>
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{data.items.map((nzb) => (
|
|
||||||
<tr key={nzb.id}>
|
|
||||||
<td>
|
|
||||||
{nzb.state === 'paused' ? (
|
|
||||||
<IconPlayerPause fill="grey" stroke={0} size="16" />
|
|
||||||
) : (
|
|
||||||
<IconPlayerPlay fill="black" stroke={0} size="16" />
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<Tooltip position="top" label={nzb.name}>
|
|
||||||
<Text
|
|
||||||
style={{
|
|
||||||
whiteSpace: 'nowrap',
|
|
||||||
overflow: 'hidden',
|
|
||||||
textOverflow: 'ellipsis',
|
|
||||||
}}
|
|
||||||
size="xs"
|
|
||||||
>
|
|
||||||
{nzb.name}
|
|
||||||
</Text>
|
|
||||||
</Tooltip>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<Text size="xs">{humanFileSize(nzb.size)}</Text>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{nzb.eta <= 0 ? (
|
|
||||||
<Text size="xs" color="dimmed">
|
|
||||||
Paused
|
|
||||||
</Text>
|
|
||||||
) : (
|
|
||||||
<Text size="xs">{dayjs.duration(nzb.eta, 's').format('H:mm:ss')}</Text>
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
<td style={{ display: 'flex', alignItems: 'center' }}>
|
|
||||||
<Text mr="sm">{nzb.progress.toFixed(1)}%</Text>
|
|
||||||
<Progress
|
|
||||||
radius="lg"
|
|
||||||
color={nzb.eta > 0 ? theme.primaryColor : 'lightgrey'}
|
|
||||||
value={nzb.progress}
|
|
||||||
size="lg"
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
</thead>
|
||||||
</tbody>
|
<tbody>
|
||||||
</Table>
|
{data.items.map((nzb) => (
|
||||||
|
<tr key={nzb.id}>
|
||||||
|
<td>
|
||||||
|
{nzb.state === 'paused' ? (
|
||||||
|
<Button disabled color="gray" variant="subtle" radius="lg" size="xs" compact>
|
||||||
|
<IconPlayerPlay size="16" />
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button disabled variant="subtle" radius="lg" size="xs" compact>
|
||||||
|
<IconPlayerPause size="16" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<Tooltip position="top" label={nzb.name}>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
}}
|
||||||
|
size="xs"
|
||||||
|
color={nzb.state === 'paused' ? 'dimmed' : undefined}
|
||||||
|
>
|
||||||
|
{nzb.name}
|
||||||
|
</Text>
|
||||||
|
</Tooltip>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<Text size="xs">{humanFileSize(nzb.size)}</Text>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{nzb.eta <= 0 ? (
|
||||||
|
<Text size="xs" color="dimmed">
|
||||||
|
{t('queue.paused')}
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<Text size="xs">{dayjs.duration(nzb.eta, 's').format('H:mm:ss')}</Text>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td style={{ display: 'flex', alignItems: 'center' }}>
|
||||||
|
<Text mr="sm">{nzb.progress.toFixed(1)}%</Text>
|
||||||
|
<Progress
|
||||||
|
radius="lg"
|
||||||
|
color={nzb.eta > 0 ? theme.primaryColor : 'lightgrey'}
|
||||||
|
value={nzb.progress}
|
||||||
|
size="lg"
|
||||||
|
style={{ width: '100%' }}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</Table>
|
||||||
|
{totalPages > 1 && (
|
||||||
|
<Pagination
|
||||||
|
size="sm"
|
||||||
|
position="center"
|
||||||
|
mt="md"
|
||||||
|
total={totalPages}
|
||||||
|
page={page}
|
||||||
|
onChange={setPage}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,9 +8,11 @@ import { NotificationsProvider } from '@mantine/notifications';
|
|||||||
import { useHotkeys } from '@mantine/hooks';
|
import { useHotkeys } from '@mantine/hooks';
|
||||||
import { ModalsProvider } from '@mantine/modals';
|
import { ModalsProvider } from '@mantine/modals';
|
||||||
import { appWithTranslation } from 'next-i18next';
|
import { appWithTranslation } from 'next-i18next';
|
||||||
|
import { QueryClientProvider } from '@tanstack/react-query';
|
||||||
import { ConfigProvider } from '../tools/state';
|
import { ConfigProvider } from '../tools/state';
|
||||||
import { theme } from '../tools/theme';
|
import { theme } from '../tools/theme';
|
||||||
import { ColorTheme } from '../tools/color';
|
import { ColorTheme } from '../tools/color';
|
||||||
|
import { queryClient } from '../tools/queryClient';
|
||||||
|
|
||||||
function App(this: any, props: AppProps & { colorScheme: ColorScheme }) {
|
function App(this: any, props: AppProps & { colorScheme: ColorScheme }) {
|
||||||
const { Component, pageProps } = props;
|
const { Component, pageProps } = props;
|
||||||
@@ -40,43 +42,44 @@ function App(this: any, props: AppProps & { colorScheme: ColorScheme }) {
|
|||||||
<Head>
|
<Head>
|
||||||
<meta name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width" />
|
<meta name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width" />
|
||||||
</Head>
|
</Head>
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
<ColorSchemeProvider colorScheme={colorScheme} toggleColorScheme={toggleColorScheme}>
|
<ColorSchemeProvider colorScheme={colorScheme} toggleColorScheme={toggleColorScheme}>
|
||||||
<ColorTheme.Provider value={colorTheme}>
|
<ColorTheme.Provider value={colorTheme}>
|
||||||
<MantineProvider
|
<MantineProvider
|
||||||
theme={{
|
theme={{
|
||||||
...theme,
|
...theme,
|
||||||
components: {
|
components: {
|
||||||
Checkbox: {
|
Checkbox: {
|
||||||
styles: {
|
styles: {
|
||||||
input: { cursor: 'pointer' },
|
input: { cursor: 'pointer' },
|
||||||
label: { cursor: 'pointer' },
|
label: { cursor: 'pointer' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Switch: {
|
||||||
|
styles: {
|
||||||
|
input: { cursor: 'pointer' },
|
||||||
|
label: { cursor: 'pointer' },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Switch: {
|
primaryColor,
|
||||||
styles: {
|
primaryShade,
|
||||||
input: { cursor: 'pointer' },
|
colorScheme,
|
||||||
label: { cursor: 'pointer' },
|
}}
|
||||||
},
|
withGlobalStyles
|
||||||
},
|
withNormalizeCSS
|
||||||
},
|
>
|
||||||
primaryColor,
|
<NotificationsProvider limit={4} position="bottom-left">
|
||||||
primaryShade,
|
<ModalsProvider>
|
||||||
colorScheme,
|
<ConfigProvider>
|
||||||
}}
|
<Component {...pageProps} />
|
||||||
withGlobalStyles
|
</ConfigProvider>
|
||||||
withNormalizeCSS
|
</ModalsProvider>
|
||||||
>
|
</NotificationsProvider>
|
||||||
<NotificationsProvider limit={4} position="bottom-left">
|
</MantineProvider>
|
||||||
<ModalsProvider>
|
</ColorTheme.Provider>
|
||||||
<ConfigProvider>
|
</ColorSchemeProvider>
|
||||||
<Component {...pageProps} />
|
</QueryClientProvider>
|
||||||
</ConfigProvider>
|
|
||||||
</ModalsProvider>
|
|
||||||
</NotificationsProvider>
|
|
||||||
</MantineProvider>
|
|
||||||
</ColorTheme.Provider>
|
|
||||||
</ColorSchemeProvider>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,29 +3,28 @@ import dayjs from 'dayjs';
|
|||||||
import duration from 'dayjs/plugin/duration';
|
import duration from 'dayjs/plugin/duration';
|
||||||
import { NextApiRequest, NextApiResponse } from 'next';
|
import { NextApiRequest, NextApiResponse } from 'next';
|
||||||
import { Client } from 'sabnzbd-api';
|
import { Client } from 'sabnzbd-api';
|
||||||
import { UsenetQueueItem } from '../../../../modules';
|
|
||||||
import { getConfig } from '../../../../tools/getConfig';
|
import { getConfig } from '../../../../tools/getConfig';
|
||||||
import { getServiceById } from '../../../../tools/hooks/useGetServiceByType';
|
import { getServiceById } from '../../../../tools/hooks/useGetServiceByType';
|
||||||
import { Config } from '../../../../tools/types';
|
import { Config } from '../../../../tools/types';
|
||||||
|
|
||||||
dayjs.extend(duration);
|
dayjs.extend(duration);
|
||||||
|
|
||||||
export interface UsenetQueueRequestParams {
|
export interface UsenetInfoRequestParams {
|
||||||
serviceId: string;
|
serviceId: string;
|
||||||
offset: number;
|
|
||||||
limit: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UsenetQueueResponse {
|
export interface UsenetInfoResponse {
|
||||||
items: UsenetQueueItem[];
|
paused: boolean;
|
||||||
total: number;
|
sizeLeft: number;
|
||||||
|
speed: number;
|
||||||
|
eta: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function Get(req: NextApiRequest, res: NextApiResponse) {
|
async function Get(req: NextApiRequest, res: NextApiResponse) {
|
||||||
try {
|
try {
|
||||||
const configName = getCookie('config-name', { req });
|
const configName = getCookie('config-name', { req });
|
||||||
const { config }: { config: Config } = getConfig(configName?.toString() ?? 'default').props;
|
const { config }: { config: Config } = getConfig(configName?.toString() ?? 'default').props;
|
||||||
const { limit, offset, serviceId } = req.query as any as UsenetQueueRequestParams;
|
const { serviceId } = req.query as any as UsenetInfoRequestParams;
|
||||||
|
|
||||||
const service = getServiceById(config, serviceId);
|
const service = getServiceById(config, serviceId);
|
||||||
|
|
||||||
@@ -36,29 +35,21 @@ async function Get(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
if (!service.apiKey) {
|
if (!service.apiKey) {
|
||||||
throw new Error(`API Key for service "${service.name}" is missing`);
|
throw new Error(`API Key for service "${service.name}" is missing`);
|
||||||
}
|
}
|
||||||
const queue = await new Client(service.url, service.apiKey).queue(offset, limit);
|
|
||||||
|
|
||||||
const items: UsenetQueueItem[] = queue.slots.map((slot) => {
|
const queue = await new Client(service.url, service.apiKey).queue(0, -1);
|
||||||
const [hours, minutes, seconds] = slot.timeleft.split(':');
|
|
||||||
const eta = dayjs.duration({
|
|
||||||
hour: parseInt(hours, 10),
|
|
||||||
minutes: parseInt(minutes, 10),
|
|
||||||
seconds: parseInt(seconds, 10),
|
|
||||||
} as any);
|
|
||||||
|
|
||||||
return {
|
const [hours, minutes, seconds] = queue.timeleft.split(':');
|
||||||
id: slot.nzo_id,
|
const eta = dayjs.duration({
|
||||||
eta: eta.asSeconds(),
|
hour: parseInt(hours, 10),
|
||||||
name: slot.filename,
|
minutes: parseInt(minutes, 10),
|
||||||
progress: parseFloat(slot.percentage),
|
seconds: parseInt(seconds, 10),
|
||||||
size: parseFloat(slot.mb) * 1000 * 1000,
|
} as any);
|
||||||
state: slot.status.toLowerCase() as any,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const response: UsenetQueueResponse = {
|
const response: UsenetInfoResponse = {
|
||||||
items,
|
paused: queue.paused,
|
||||||
total: queue.noofslots_total,
|
sizeLeft: parseFloat(queue.mbleft) * 1024 * 1024,
|
||||||
|
speed: parseFloat(queue.kbpersec) * 1000,
|
||||||
|
eta: eta.asSeconds(),
|
||||||
};
|
};
|
||||||
|
|
||||||
return res.status(200).json(response);
|
return res.status(200).json(response);
|
||||||
|
|||||||
49
src/pages/api/modules/usenet/pause.ts
Normal file
49
src/pages/api/modules/usenet/pause.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { getCookie } from 'cookies-next';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import duration from 'dayjs/plugin/duration';
|
||||||
|
import { NextApiRequest, NextApiResponse } from 'next';
|
||||||
|
import { Client } from 'sabnzbd-api';
|
||||||
|
import { getConfig } from '../../../../tools/getConfig';
|
||||||
|
import { getServiceById } from '../../../../tools/hooks/useGetServiceByType';
|
||||||
|
import { Config } from '../../../../tools/types';
|
||||||
|
|
||||||
|
dayjs.extend(duration);
|
||||||
|
|
||||||
|
export interface UsenetPauseRequestParams {
|
||||||
|
serviceId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function Post(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
try {
|
||||||
|
const configName = getCookie('config-name', { req });
|
||||||
|
const { config }: { config: Config } = getConfig(configName?.toString() ?? 'default').props;
|
||||||
|
const { serviceId } = req.query as any as UsenetPauseRequestParams;
|
||||||
|
|
||||||
|
const service = getServiceById(config, serviceId);
|
||||||
|
|
||||||
|
if (!service) {
|
||||||
|
throw new Error(`Service with ID "${req.query.serviceId}" could not be found.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!service.apiKey) {
|
||||||
|
throw new Error(`API Key for service "${service.name}" is missing`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await new Client(service.url, service.apiKey).queuePause();
|
||||||
|
|
||||||
|
return res.status(200).json(result);
|
||||||
|
} catch (err) {
|
||||||
|
return res.status(500).send((err as any).message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
|
// Filter out if the reuqest is a POST or a GET
|
||||||
|
if (req.method === 'POST') {
|
||||||
|
return Post(req, res);
|
||||||
|
}
|
||||||
|
return res.status(405).json({
|
||||||
|
statusCode: 405,
|
||||||
|
message: 'Method not allowed',
|
||||||
|
});
|
||||||
|
};
|
||||||
79
src/pages/api/modules/usenet/queue.ts
Normal file
79
src/pages/api/modules/usenet/queue.ts
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import { getCookie } from 'cookies-next';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import duration from 'dayjs/plugin/duration';
|
||||||
|
import { NextApiRequest, NextApiResponse } from 'next';
|
||||||
|
import { Client } from 'sabnzbd-api';
|
||||||
|
import { UsenetQueueItem } from '../../../../modules';
|
||||||
|
import { getConfig } from '../../../../tools/getConfig';
|
||||||
|
import { getServiceById } from '../../../../tools/hooks/useGetServiceByType';
|
||||||
|
import { Config } from '../../../../tools/types';
|
||||||
|
|
||||||
|
dayjs.extend(duration);
|
||||||
|
|
||||||
|
export interface UsenetQueueRequestParams {
|
||||||
|
serviceId: string;
|
||||||
|
offset: number;
|
||||||
|
limit: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UsenetQueueResponse {
|
||||||
|
items: UsenetQueueItem[];
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function Get(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
try {
|
||||||
|
const configName = getCookie('config-name', { req });
|
||||||
|
const { config }: { config: Config } = getConfig(configName?.toString() ?? 'default').props;
|
||||||
|
const { limit, offset, serviceId } = req.query as any as UsenetQueueRequestParams;
|
||||||
|
|
||||||
|
const service = getServiceById(config, serviceId);
|
||||||
|
|
||||||
|
if (!service) {
|
||||||
|
throw new Error(`Service with ID "${req.query.serviceId}" could not be found.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!service.apiKey) {
|
||||||
|
throw new Error(`API Key for service "${service.name}" is missing`);
|
||||||
|
}
|
||||||
|
const queue = await new Client(service.url, service.apiKey).queue(offset, limit);
|
||||||
|
|
||||||
|
const items: UsenetQueueItem[] = queue.slots.map((slot) => {
|
||||||
|
const [hours, minutes, seconds] = slot.timeleft.split(':');
|
||||||
|
const eta = dayjs.duration({
|
||||||
|
hour: parseInt(hours, 10),
|
||||||
|
minutes: parseInt(minutes, 10),
|
||||||
|
seconds: parseInt(seconds, 10),
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: slot.nzo_id,
|
||||||
|
eta: eta.asSeconds(),
|
||||||
|
name: slot.filename,
|
||||||
|
progress: parseFloat(slot.percentage),
|
||||||
|
size: parseFloat(slot.mb) * 1000 * 1000,
|
||||||
|
state: slot.status.toLowerCase() as any,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const response: UsenetQueueResponse = {
|
||||||
|
items,
|
||||||
|
total: queue.noofslots,
|
||||||
|
};
|
||||||
|
|
||||||
|
return res.status(200).json(response);
|
||||||
|
} catch (err) {
|
||||||
|
return res.status(500).send((err as any).message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
|
// Filter out if the reuqest is a POST or a GET
|
||||||
|
if (req.method === 'GET') {
|
||||||
|
return Get(req, res);
|
||||||
|
}
|
||||||
|
return res.status(405).json({
|
||||||
|
statusCode: 405,
|
||||||
|
message: 'Method not allowed',
|
||||||
|
});
|
||||||
|
};
|
||||||
50
src/pages/api/modules/usenet/resume.ts
Normal file
50
src/pages/api/modules/usenet/resume.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { getCookie } from 'cookies-next';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import duration from 'dayjs/plugin/duration';
|
||||||
|
import { NextApiRequest, NextApiResponse } from 'next';
|
||||||
|
import { Client } from 'sabnzbd-api';
|
||||||
|
import { getConfig } from '../../../../tools/getConfig';
|
||||||
|
import { getServiceById } from '../../../../tools/hooks/useGetServiceByType';
|
||||||
|
import { Config } from '../../../../tools/types';
|
||||||
|
|
||||||
|
dayjs.extend(duration);
|
||||||
|
|
||||||
|
export interface UsenetResumeRequestParams {
|
||||||
|
serviceId: string;
|
||||||
|
nzbId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function Post(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
try {
|
||||||
|
const configName = getCookie('config-name', { req });
|
||||||
|
const { config }: { config: Config } = getConfig(configName?.toString() ?? 'default').props;
|
||||||
|
const { serviceId } = req.query as any as UsenetResumeRequestParams;
|
||||||
|
|
||||||
|
const service = getServiceById(config, serviceId);
|
||||||
|
|
||||||
|
if (!service) {
|
||||||
|
throw new Error(`Service with ID "${req.query.serviceId}" could not be found.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!service.apiKey) {
|
||||||
|
throw new Error(`API Key for service "${service.name}" is missing`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await new Client(service.url, service.apiKey).queueResume();
|
||||||
|
|
||||||
|
return res.status(200).json(result);
|
||||||
|
} catch (err) {
|
||||||
|
return res.status(500).send((err as any).message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async (req: NextApiRequest, res: NextApiResponse) => {
|
||||||
|
// Filter out if the reuqest is a POST or a GET
|
||||||
|
if (req.method === 'POST') {
|
||||||
|
return Post(req, res);
|
||||||
|
}
|
||||||
|
return res.status(405).json({
|
||||||
|
statusCode: 405,
|
||||||
|
message: 'Method not allowed',
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -3,8 +3,6 @@ import { GetServerSidePropsContext } from 'next';
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
||||||
|
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
||||||
|
|
||||||
import AppShelf from '../components/AppShelf/AppShelf';
|
import AppShelf from '../components/AppShelf/AppShelf';
|
||||||
import LoadConfigComponent from '../components/Config/LoadConfig';
|
import LoadConfigComponent from '../components/Config/LoadConfig';
|
||||||
import { Config } from '../tools/types';
|
import { Config } from '../tools/types';
|
||||||
@@ -52,6 +50,7 @@ export async function getServerSideProps({
|
|||||||
'modules/date',
|
'modules/date',
|
||||||
'modules/calendar',
|
'modules/calendar',
|
||||||
'modules/dlspeed',
|
'modules/dlspeed',
|
||||||
|
'modules/usenet',
|
||||||
'modules/search',
|
'modules/search',
|
||||||
'modules/torrents-status',
|
'modules/torrents-status',
|
||||||
'modules/weather',
|
'modules/weather',
|
||||||
@@ -64,8 +63,6 @@ export async function getServerSideProps({
|
|||||||
return getConfig(configName as string, translations);
|
return getConfig(configName as string, translations);
|
||||||
}
|
}
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
|
||||||
|
|
||||||
export default function HomePage(props: any) {
|
export default function HomePage(props: any) {
|
||||||
const { config: initialConfig }: { config: Config } = props;
|
const { config: initialConfig }: { config: Config } = props;
|
||||||
const { setConfig } = useConfig();
|
const { setConfig } = useConfig();
|
||||||
@@ -77,11 +74,9 @@ export default function HomePage(props: any) {
|
|||||||
setConfig(migratedConfig);
|
setConfig(migratedConfig);
|
||||||
}, [initialConfig]);
|
}, [initialConfig]);
|
||||||
return (
|
return (
|
||||||
<QueryClientProvider client={queryClient}>
|
<Layout>
|
||||||
<Layout>
|
<AppShelf />
|
||||||
<AppShelf />
|
<LoadConfigComponent />
|
||||||
<LoadConfigComponent />
|
</Layout>
|
||||||
</Layout>
|
|
||||||
</QueryClientProvider>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,41 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { UsenetQueueRequestParams, UsenetQueueResponse } from '../../pages/api/modules/usenet';
|
import { Results } from 'sabnzbd-api';
|
||||||
|
import {
|
||||||
|
UsenetQueueRequestParams,
|
||||||
|
UsenetQueueResponse,
|
||||||
|
} from '../../pages/api/modules/usenet/queue';
|
||||||
import {
|
import {
|
||||||
UsenetHistoryRequestParams,
|
UsenetHistoryRequestParams,
|
||||||
UsenetHistoryResponse,
|
UsenetHistoryResponse,
|
||||||
} from '../../pages/api/modules/usenet/history';
|
} from '../../pages/api/modules/usenet/history';
|
||||||
|
import { UsenetInfoRequestParams, UsenetInfoResponse } from '../../pages/api/modules/usenet';
|
||||||
|
import { UsenetPauseRequestParams } from '../../pages/api/modules/usenet/pause';
|
||||||
|
import { queryClient } from '../queryClient';
|
||||||
|
import { UsenetResumeRequestParams } from '../../pages/api/modules/usenet/resume';
|
||||||
|
|
||||||
|
export const useGetUsenetInfo = (params: UsenetInfoRequestParams) =>
|
||||||
|
useQuery(
|
||||||
|
['usenetInfo', params.serviceId],
|
||||||
|
async () =>
|
||||||
|
(
|
||||||
|
await axios.get<UsenetInfoResponse>('/api/modules/usenet', {
|
||||||
|
params,
|
||||||
|
})
|
||||||
|
).data,
|
||||||
|
{
|
||||||
|
refetchInterval: 1000,
|
||||||
|
keepPreviousData: true,
|
||||||
|
retry: 2,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export const useGetUsenetDownloads = (params: UsenetQueueRequestParams) =>
|
export const useGetUsenetDownloads = (params: UsenetQueueRequestParams) =>
|
||||||
useQuery(
|
useQuery(
|
||||||
['usenetDownloads', ...Object.values(params)],
|
['usenetDownloads', ...Object.values(params)],
|
||||||
async () =>
|
async () =>
|
||||||
(
|
(
|
||||||
await axios.get<UsenetQueueResponse>('/api/modules/usenet', {
|
await axios.get<UsenetQueueResponse>('/api/modules/usenet/queue', {
|
||||||
params,
|
params,
|
||||||
})
|
})
|
||||||
).data,
|
).data,
|
||||||
@@ -37,3 +61,91 @@ export const useGetUsenetHistory = (params: UsenetHistoryRequestParams) =>
|
|||||||
retry: 2,
|
retry: 2,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const usePauseUsenetQueue = (params: UsenetPauseRequestParams) =>
|
||||||
|
useMutation(
|
||||||
|
['usenetPause', ...Object.values(params)],
|
||||||
|
async () =>
|
||||||
|
(
|
||||||
|
await axios.post<Results>(
|
||||||
|
'/api/modules/usenet/pause',
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
params,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
).data,
|
||||||
|
{
|
||||||
|
async onMutate() {
|
||||||
|
await queryClient.cancelQueries(['usenetInfo', params.serviceId]);
|
||||||
|
const previousInfo = queryClient.getQueryData<UsenetInfoResponse>([
|
||||||
|
'usenetInfo',
|
||||||
|
params.serviceId,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (previousInfo) {
|
||||||
|
queryClient.setQueryData<UsenetInfoResponse>(['usenetInfo', params.serviceId], {
|
||||||
|
...previousInfo,
|
||||||
|
paused: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { previousInfo };
|
||||||
|
},
|
||||||
|
onError(err, _, context) {
|
||||||
|
if (context?.previousInfo) {
|
||||||
|
queryClient.setQueryData<UsenetInfoResponse>(
|
||||||
|
['usenetInfo', params.serviceId],
|
||||||
|
context.previousInfo
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSettled() {
|
||||||
|
queryClient.invalidateQueries(['usenetInfo', params.serviceId]);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const useResumeUsenetQueue = (params: UsenetResumeRequestParams) =>
|
||||||
|
useMutation(
|
||||||
|
['usenetResume', ...Object.values(params)],
|
||||||
|
async () =>
|
||||||
|
(
|
||||||
|
await axios.post<Results>(
|
||||||
|
'/api/modules/usenet/resume',
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
params,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
).data,
|
||||||
|
{
|
||||||
|
async onMutate() {
|
||||||
|
await queryClient.cancelQueries(['usenetInfo', params.serviceId]);
|
||||||
|
const previousInfo = queryClient.getQueryData<UsenetInfoResponse>([
|
||||||
|
'usenetInfo',
|
||||||
|
params.serviceId,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (previousInfo) {
|
||||||
|
queryClient.setQueryData<UsenetInfoResponse>(['usenetInfo', params.serviceId], {
|
||||||
|
...previousInfo,
|
||||||
|
paused: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { previousInfo };
|
||||||
|
},
|
||||||
|
onError(err, _, context) {
|
||||||
|
if (context?.previousInfo) {
|
||||||
|
queryClient.setQueryData<UsenetInfoResponse>(
|
||||||
|
['usenetInfo', params.serviceId],
|
||||||
|
context.previousInfo
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSettled() {
|
||||||
|
queryClient.invalidateQueries(['usenetInfo', params.serviceId]);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|||||||
3
src/tools/queryClient.ts
Normal file
3
src/tools/queryClient.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import { QueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
export const queryClient = new QueryClient();
|
||||||
Reference in New Issue
Block a user