Feature/streamline backend requests (#297)

* Introduce "RequestManager"

* Use "RequestManager" - Simple replacements

 - Get rid of all "util/client" imports
 - replace old requests with new "RequestManager"
 - remove "fetcher" from global SWR config
     instead of using the SWR hooks by themselves, the "requestManager" is supposed to be used

* Use "RequestManager" - Do requests via SWR hooks

Use SWR hooks at places where it's easily usable

* Prevent trailing slashes in the "baseUrl"
This commit is contained in:
schroda
2023-05-18 13:25:19 +02:00
committed by GitHub
parent f86c7ea08a
commit a457d80c44
34 changed files with 950 additions and 333 deletions

View File

@@ -16,7 +16,6 @@ import NavbarContext from 'components/context/NavbarContext';
import EmptyView from 'components/util/EmptyView';
import React, { useContext, useEffect } from 'react';
import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd';
import client from 'util/client';
import Typography from '@mui/material/Typography';
import { Box } from '@mui/system';
@@ -28,6 +27,7 @@ import { BACK } from 'util/useBackTo';
import { useTranslation } from 'react-i18next';
import { IChapter, IQueue } from 'typings';
import makeToast from 'components/util/Toast';
import requestManager from 'lib/RequestManager';
const initialQueue = {
status: 'Stopped',
@@ -37,16 +37,16 @@ const initialQueue = {
const DownloadQueue: React.FC = () => {
const { t } = useTranslation();
const { data: queueState } = useSubscription<IQueue>('/api/v1/downloads');
const { data: queueState } = useSubscription<IQueue>('downloads');
const { queue, status } = queueState ?? initialQueue;
const { setTitle, setAction } = useContext(NavbarContext);
const toggleQueueStatus = () => {
if (status === 'Stopped') {
client.get('/api/v1/downloads/start');
requestManager.startDownloads();
} else {
client.get('/api/v1/downloads/stop');
requestManager.stopDownloads();
}
};
@@ -67,15 +67,15 @@ const DownloadQueue: React.FC = () => {
try {
if (isRunning) {
// required to stop before deleting otherwise the download kept going. Server issue?
await client.get('/api/v1/downloads/stop');
await requestManager.stopDownloads();
}
await Promise.all([
// remove from download queue
client.delete(`/api/v1/download/${chapter.mangaId}/chapter/${chapter.index}`),
requestManager.removeChapterFromDownloadQueue(chapter.mangaId, chapter.index),
// delete partial download, should be handle server side?
// bug: The folder and the last image downloaded are not deleted
client.delete(`/api/v1/manga/${chapter.mangaId}/chapter/${chapter.index}`),
requestManager.deleteDownloadedChapter(chapter.mangaId, chapter.index),
]);
} catch (error) {
makeToast(t('download.queue.error.label.failed_to_remove'), 'error');
@@ -85,7 +85,7 @@ const DownloadQueue: React.FC = () => {
return;
}
client.get('/api/v1/downloads/start').catch(() => {});
requestManager.startDownloads().catch(() => {});
};
return (

View File

@@ -12,7 +12,6 @@ import IconButton from '@mui/material/IconButton';
import AddIcon from '@mui/icons-material/Add';
import ExtensionCard from 'components/ExtensionCard';
import NavbarContext from 'components/context/NavbarContext';
import client, { useQuery } from 'util/client';
import useLocalStorage from 'util/useLocalStorage';
import LangSelect from 'components/navbar/action/LangSelect';
import { extensionDefaultLangs, DefaultLanguage, langSortCmp } from 'util/language';
@@ -31,6 +30,7 @@ import {
isExtensionStateOrLanguage,
translateExtensionLanguage,
} from 'screens/util/Extensions';
import requestManager from 'lib/RequestManager';
const LANGUAGE = 0;
const EXTENSIONS = 1;
@@ -102,7 +102,7 @@ export default function MangaExtensions() {
);
}, [t, shownLangs]);
const { data: allExtensions, mutate, isLoading } = useQuery<IExtension[]>('/api/v1/extension/list');
const { data: allExtensions, mutate, isLoading } = requestManager.useGetExtensionList();
const filteredExtensions = useMemo(
() =>
@@ -128,18 +128,13 @@ export default function MangaExtensions() {
const submitExternalExtension = (file: File) => {
if (file.name.toLowerCase().endsWith('apk')) {
const formData = new FormData();
formData.append('file', file);
if (inputRef.current) {
inputRef.current.value = '';
}
makeToast(t('extension.label.installing_file'), 'info');
client
.post('/api/v1/extension/install', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
})
requestManager
.installExtension(file)
.then(() => {
makeToast(t('extension.label.installed_successfully'), 'success');
mutate();

View File

@@ -16,12 +16,11 @@ import LibraryToolbarMenu from 'components/library/LibraryToolbarMenu';
import LibraryMangaGrid from 'components/library/LibraryMangaGrid';
import AppbarSearch from 'components/util/AppbarSearch';
import { useQueryParam, NumberParam } from 'use-query-params';
import { useQuery } from 'util/client';
import UpdateChecker from 'components/library/UpdateChecker';
import { useTranslation } from 'react-i18next';
import { ICategory, IManga } from 'typings';
import { styled } from '@mui/system';
import { useLibraryOptionsContext } from 'components/context/LibraryOptionsContext';
import requestManager from 'lib/RequestManager';
const TitleWithSizeTag = styled('span')({
display: 'flex',
@@ -37,7 +36,7 @@ export default function Library() {
const { options } = useLibraryOptionsContext();
const [lastLibraryUpdate, setLastLibraryUpdate] = useState(Date.now());
const { data: tabsData, error: tabsError, isLoading } = useQuery<ICategory[]>('/api/v1/category');
const { data: tabsData, error: tabsError, isLoading } = requestManager.useGetCategories();
const tabs = tabsData ?? [];
const librarySize = useMemo(() => tabs.map((tab) => tab.size).reduce((prev, curr) => prev + curr, 0), [tabs]);
@@ -48,7 +47,7 @@ export default function Library() {
data: mangaData,
error: mangaError,
isLoading: mangaLoading,
} = useQuery<IManga[]>(activeTab ? `/api/v1/category/${activeTab?.id}` : null);
} = requestManager.useGetCategoryMangas(activeTab?.id, { skipRequest: !activeTab });
const mangas = mangaData ?? [];
const { setTitle, setAction } = useContext(NavbarContext);

View File

@@ -20,8 +20,7 @@ import LoadingPlaceholder from 'components/util/LoadingPlaceholder';
import React, { useContext, useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { useParams } from 'react-router-dom';
import { useQuery } from 'util/client';
import { IManga } from 'typings';
import requestManager from 'lib/RequestManager';
const AUTOFETCH_AGE = 60 * 60 * 24; // 24 hours
@@ -32,13 +31,7 @@ const Manga: React.FC = () => {
const { id } = useParams<{ id: string }>();
const autofetchedRef = useRef(false);
const {
data: manga,
error,
isLoading,
isValidating,
mutate,
} = useQuery<IManga>(`/api/v1/manga/${id}/?onlineFetch=false`);
const { data: manga, error, isLoading, isValidating, mutate } = requestManager.useGetManga(id);
const [refresh, { loading: refreshing }] = useRefreshManga(id);

View File

@@ -16,8 +16,6 @@ import DoublePagedPager from 'components/reader/pager/DoublePagedPager';
import VerticalPager from 'components/reader/pager/VerticalPager';
import ReaderNavBar from 'components/navbar/ReaderNavBar';
import NavbarContext from 'components/context/NavbarContext';
import client from 'util/client';
import useLocalStorage from 'util/useLocalStorage';
import { Box } from '@mui/system';
import { requestUpdateMangaMetadata } from 'util/metadata';
import {
@@ -26,21 +24,16 @@ import {
useDefaultReaderSettings,
} from 'util/readerSettings';
import makeToast from 'components/util/Toast';
import {
ChapterOffset,
IChapter,
IManga,
IMangaCard,
IPartialChapter,
IReaderSettings,
ReaderType,
TranslationKey,
} from 'typings';
import { ChapterOffset, IChapter, IManga, IMangaCard, IReaderSettings, ReaderType, TranslationKey } from 'typings';
import { useTranslation } from 'react-i18next';
import requestManager from 'lib/RequestManager';
const isDupChapter = async (chapterIndex: number, currentChapter: IChapter) => {
const nextChapter = (await client.get<IChapter>(`/api/v1/manga/${currentChapter.mangaId}/chapter/${chapterIndex}`))
.data;
const nextChapter = (
await requestManager
.getClient()
.get<IChapter>(`/api/v1/manga/${currentChapter.mangaId}/chapter/${chapterIndex}`)
).data;
return nextChapter.chapterNumber === currentChapter.chapterNumber;
};
@@ -102,18 +95,22 @@ export default function Reader() {
const { t } = useTranslation();
const history = useHistory();
const [serverAddress] = useLocalStorage<String>('serverBaseURL', '');
const { chapterIndex, mangaId } = useParams<{ chapterIndex: string; mangaId: string }>();
const [manga, setManga] = useState<IMangaCard | IManga>({
id: +mangaId,
title: '',
thumbnailUrl: '',
genre: [],
inLibraryAt: 0,
lastReadAt: 0,
});
const [chapter, setChapter] = useState<IChapter | IPartialChapter>(initialChapter());
const {
data: manga = {
id: +mangaId,
title: '',
thumbnailUrl: '',
genre: [],
inLibraryAt: 0,
lastReadAt: 0,
} as IMangaCard | IManga,
isLoading: isMangaLoading,
} = requestManager.useGetManga(mangaId);
const { data: chapter = initialChapter(), isLoading: isChapterLoading } = requestManager.useGetChapter(
mangaId,
chapterIndex,
);
const [curPage, setCurPage] = useState<number>(0);
const [pageToScrollTo, setPageToScrollTo] = useState<number | undefined>(undefined);
const { setOverride, setTitle } = useContext(NavbarContext);
@@ -121,7 +118,6 @@ export default function Reader() {
const { settings: defaultSettings, loading: areDefaultSettingsLoading } = useDefaultReaderSettings();
const [settings, setSettings] = useState(getReaderSettingsFor(manga, defaultSettings));
const [isMangaLoading, setIsMangaLoading] = useState(true);
const setSettingValue = (key: keyof IReaderSettings, value: string | boolean) => {
setSettings({ ...settings, [key]: value });
@@ -156,6 +152,17 @@ export default function Reader() {
[chapter, settings],
);
useEffect(() => {
if (isChapterLoading || !chapter) {
return;
}
if (chapter.lastPageRead === chapter.pageCount - 1) {
// last page, also probably read = true, we will load the first page.
setCurPage(0);
} else setCurPage(chapter.lastPageRead);
}, [chapter, isChapterLoading]);
useEffect(() => {
if (!manga?.title || (chapter as IChapter)?.name === t('global.label.loading')) {
setTitle(t('reader.title'));
@@ -193,52 +200,22 @@ export default function Reader() {
return () => setOverride({ status: false, value: <div /> });
}, [manga, chapter, settings, curPage, chapterIndex, retrievingNextChapter]);
useEffect(() => {
setIsMangaLoading(true);
client
.get(`/api/v1/manga/${mangaId}/`)
.then((response) => response.data)
.then((data: IManga) => {
setManga(data);
setIsMangaLoading(false);
});
}, [mangaId]);
useEffect(() => {
setChapter(initialChapter);
client
.get(`/api/v1/manga/${mangaId}/chapter/${chapterIndex}`)
.then((response) => response.data)
.then((data: IChapter) => {
setChapter(data);
if (data.lastPageRead === data.pageCount - 1) {
// last page, also probably read = true, we will load the first page.
setCurPage(0);
} else setCurPage(data.lastPageRead);
});
}, [chapterIndex]);
useEffect(() => {
if (curPage !== -1) {
const formData = new FormData();
formData.append('lastPageRead', curPage.toString());
client.patch(`/api/v1/manga/${manga.id}/chapter/${chapter.index}`, formData);
requestManager.updateChapter(manga.id, chapter.index, { lastPageRead: curPage });
}
if (curPage === chapter.pageCount - 1) {
const formDataRead = new FormData();
formDataRead.append('read', 'true');
client.patch(`/api/v1/manga/${manga.id}/chapter/${chapter.index}`, formDataRead);
requestManager.updateChapter(manga.id, chapter.index, { read: true });
}
}, [curPage]);
const nextChapter = useCallback(() => {
if (chapter.index < chapter.chapterCount) {
const formData = new FormData();
formData.append('lastPageRead', `${chapter.pageCount - 1}`);
formData.append('read', 'true');
client.patch(`/api/v1/manga/${manga.id}/chapter/${chapter.index}`, formData);
requestManager.updateChapter(manga.id, chapter.index, {
lastPageRead: chapter.pageCount - 1,
read: true,
});
openNextChapter(ChapterOffset.NEXT, (nextChapterIndex) =>
history.replace({
@@ -278,7 +255,7 @@ export default function Reader() {
const pages = range(chapter.pageCount).map((index) => ({
index,
src: `${serverAddress}/api/v1/manga/${mangaId}/chapter/${chapterIndex}/page/${index}`,
src: requestManager.getChapterPageUrl(mangaId, chapterIndex, index),
}));
const ReaderComponent = getReaderComponent(settings.readerType);

View File

@@ -12,15 +12,15 @@ import MangaGrid from 'components/MangaGrid';
import LangSelect from 'components/navbar/action/LangSelect';
import AppbarSearch from 'components/util/AppbarSearch';
import PQueue from 'p-queue';
import React, { useContext, useEffect, useState } from 'react';
import React, { useContext, useEffect, useMemo, useState } from 'react';
import { Link } from 'react-router-dom';
import { StringParam, useQueryParam } from 'use-query-params';
import client from 'util/client';
import { langSortCmp, sourceDefualtLangs, sourceForcedDefaultLangs } from 'util/language';
import useLocalStorage from 'util/useLocalStorage';
import { ISource } from 'typings';
import { useTranslation } from 'react-i18next';
import { translateExtensionLanguage } from 'screens/util/Extensions';
import requestManager from 'lib/RequestManager';
function sourceToLangList(sources: ISource[]) {
const result: string[] = [];
@@ -46,9 +46,22 @@ const SearchAll: React.FC = () => {
const [shownLangs, setShownLangs] = useLocalStorage<string[]>('shownSourceLangs', sourceDefualtLangs());
const [showNsfw] = useLocalStorage<boolean>('showNsfw', true);
const [sources, setSources] = useState<ISource[]>([]);
const { data: unsortedSources = [], isLoading: FetchedSources } = requestManager.useGetSourceList();
const sources = useMemo(
() =>
unsortedSources.sort((a: { displayName: string }, b: { displayName: string }) => {
if (a.displayName < b.displayName) {
return -1;
}
if (a.displayName > b.displayName) {
return 1;
}
return 0;
}),
[unsortedSources],
);
const [fetched, setFetched] = useState<any>({});
const [FetchedSources, setFetchedSources] = useState<any>({});
const [lastPageNum, setLastPageNum] = useState<number>(1);
@@ -61,32 +74,12 @@ const SearchAll: React.FC = () => {
setAction(<AppbarSearch />);
}, [t]);
useEffect(() => {
client
.get('/api/v1/source/list')
.then((response) => response.data)
.then((data) => {
setSources(
data.sort((a: { displayName: string }, b: { displayName: string }) => {
if (a.displayName < b.displayName) {
return -1;
}
if (a.displayName > b.displayName) {
return 1;
}
return 0;
}),
);
setFetchedSources(true);
});
}, []);
async function doIT(elem: any[]) {
elem.map((ele) =>
limit.add(async () => {
const response = await client.get(
`/api/v1/source/${ele.id}/search?searchTerm=${query || ''}&pageNum=1`,
);
const response = await requestManager
.getClient()
.get(`/api/v1/source/${ele.id}/search?searchTerm=${query || ''}&pageNum=1`);
const data = await response.data;
const tmp = mangas;
tmp[ele.id] = data.mangaList;

View File

@@ -41,6 +41,7 @@ import { useTranslation } from 'react-i18next';
import LanguageIcon from '@mui/icons-material/Language';
import { langCodeToName } from 'util/language';
import CollectionsOutlinedBookmarkIcon from '@mui/icons-material/CollectionsBookmarkOutlined';
import requestManager from 'lib/RequestManager';
export default function Settings() {
const { t, i18n } = useTranslation();
@@ -52,7 +53,7 @@ export default function Settings() {
}, [t]);
const { darkTheme, setDarkTheme } = useContext(DarkTheme);
const [serverAddress, setServerAddress] = useLocalStorage<String>('serverBaseURL', '');
const [serverAddress, setServerAddress] = useLocalStorage<string>('serverBaseURL', '');
const [showNsfw, setShowNsfw] = useLocalStorage<boolean>('showNsfw', true);
const [useCache, setUseCache] = useLocalStorage<boolean>('useCache', true);
const [dialogOpen, setDialogOpen] = useState(false);
@@ -73,7 +74,9 @@ export default function Settings() {
const handleDialogSubmit = () => {
setDialogOpen(false);
setServerAddress(dialogValue);
const serverBaseUrl = dialogValue.replaceAll(/(\/)+$/g, '');
setServerAddress(serverBaseUrl);
requestManager.updateClient({ baseURL: serverBaseUrl });
};
const handleDialogOpenItemWidth = () => {

View File

@@ -9,15 +9,14 @@
import React, { useContext, useEffect } from 'react';
import NavbarContext from 'components/context/NavbarContext';
import { useParams } from 'react-router-dom';
import client, { useQuery } from 'util/client';
import { SwitchPreferenceCompat, CheckBoxPreference } from 'components/sourceConfiguration/TwoStatePreference';
import ListPreference from 'components/sourceConfiguration/ListPreference';
import EditTextPreference from 'components/sourceConfiguration/EditTextPreference';
import MultiSelectListPreference from 'components/sourceConfiguration/MultiSelectListPreference';
import List from '@mui/material/List';
import cloneObject from 'util/cloneObject';
import { SourcePreferences } from 'typings';
import { useTranslation } from 'react-i18next';
import requestManager from 'lib/RequestManager';
function getPrefComponent(type: string) {
switch (type) {
@@ -46,9 +45,7 @@ export default function SourceConfigure() {
}, [t]);
const { sourceId } = useParams<{ sourceId: string }>();
const { data: sourcePreferences = [], mutate } = useQuery<SourcePreferences[]>(
`/api/v1/source/${sourceId}/preferences`,
);
const { data: sourcePreferences = [], mutate } = requestManager.useGetSourcePreferences(sourceId);
const convertToString = (position: number, value: any): string => {
switch (sourcePreferences[position].props.defaultValueType) {
@@ -60,12 +57,7 @@ export default function SourceConfigure() {
};
const updateValue = (position: number) => (value: any) => {
client
.post(
`/api/v1/source/${sourceId}/preferences`,
JSON.stringify({ position, value: convertToString(position, value) }),
)
.then(() => mutate());
requestManager.setSourcePreferences(sourceId, position, convertToString(position, value)).then(() => mutate());
};
return (

View File

@@ -11,16 +11,16 @@ import { useParams, useHistory } from 'react-router-dom';
import IconButton from '@mui/material/IconButton';
import SourceMangaGrid from 'components/source/SourceMangaGrid';
import NavbarContext from 'components/context/NavbarContext';
import client from 'util/client';
import SettingsIcon from '@mui/icons-material/Settings';
import SourceOptions from 'components/source/SourceOptions';
import AppbarSearch from 'components/util/AppbarSearch';
import { useQueryParam, StringParam } from 'use-query-params';
import SourceGridLayout from 'components/source/GridLayouts';
import { useLibraryOptionsContext } from 'components/context/LibraryOptionsContext';
import { IManga, IMangaCard, ISource, ISourceFilters } from 'typings';
import { IManga, IMangaCard, ISourceFilters } from 'typings';
import { useTranslation } from 'react-i18next';
import Link from '@mui/material/Link';
import requestManager from 'lib/RequestManager';
interface IPos {
position: number;
@@ -34,6 +34,9 @@ export default function SourceMangas({ popular }: { popular: boolean }) {
const history = useHistory();
const { sourceId } = useParams<{ sourceId: string }>();
const { data: source } = requestManager.useGetSource(sourceId);
const [isConfigurable, setIsConfigurable] = useState<boolean>(false);
const [mangas, setMangas] = useState<IMangaCard[]>([]);
const [hasNextPage, setHasNextPage] = useState<boolean>(false);
@@ -53,7 +56,8 @@ export default function SourceMangas({ popular }: { popular: boolean }) {
const { options } = useLibraryOptionsContext();
function makeFilters() {
client
requestManager
.getClient()
.get(`/api/v1/source/${sourceId}/filters`)
.then((response) => response.data)
.then((data: ISourceFilters[]) => {
@@ -66,14 +70,13 @@ export default function SourceMangas({ popular }: { popular: boolean }) {
}, [t]);
useEffect(() => {
client
.get(`/api/v1/source/${sourceId}`)
.then((response) => response.data)
.then((data: ISource) => {
setTitle(data.displayName);
setIsConfigurable(data.isConfigurable);
});
}, []);
if (!source) {
return;
}
setTitle(source.displayName);
setIsConfigurable(source.isConfigurable);
}, [source]);
useEffect(() => {
if (triggerUpdate === 2) {
@@ -86,9 +89,9 @@ export default function SourceMangas({ popular }: { popular: boolean }) {
if (update.length > 0) {
const rep = update;
setUpdate([]);
client
.post(
`/api/v1/source/${sourceId}/filters`,
requestManager
.setSourceFilters(
sourceId,
rep.map((e: IPos) => {
const { position, state, group }: IPos = e;
return group === undefined
@@ -125,7 +128,7 @@ export default function SourceMangas({ popular }: { popular: boolean }) {
setNoreset(undefined);
setReset(1);
} else if (Noreset === undefined) {
client.get(`/api/v1/source/${sourceId}/filters?reset=true`).then(() => {
requestManager.resetSourceFilters(sourceId).then(() => {
makeFilters();
setSearch(false);
if (reset === 1) {
@@ -188,7 +191,8 @@ export default function SourceMangas({ popular }: { popular: boolean }) {
useEffect(() => {
if (lastPageNum !== 0) {
const sourceType = popular ? 'popular' : 'latest';
client
requestManager
.getClient()
.get(
`/api/v1/source/${sourceId}/${
query !== undefined || Search || Noreset === null ? 'search' : sourceType

View File

@@ -16,10 +16,10 @@ import LoadingPlaceholder from 'components/util/LoadingPlaceholder';
import { IconButton } from '@mui/material';
import TravelExploreIcon from '@mui/icons-material/TravelExplore';
import { useHistory } from 'react-router-dom';
import { useQuery } from 'util/client';
import { ISource } from 'typings';
import { useTranslation } from 'react-i18next';
import { translateExtensionLanguage } from 'screens/util/Extensions';
import requestManager from 'lib/RequestManager';
function sourceToLangList(sources: ISource[]) {
const result: string[] = [];
@@ -53,7 +53,7 @@ export default function Sources() {
const [shownLangs, setShownLangs] = useLocalStorage<string[]>('shownSourceLangs', sourceDefualtLangs());
const [showNsfw] = useLocalStorage<boolean>('showNsfw', true);
const { data: sources, isLoading } = useQuery<ISource[]>('/api/v1/source/list');
const { data: sources, isLoading } = requestManager.useGetSourceList();
const history = useHistory();

View File

@@ -20,11 +20,10 @@ import EmptyView from 'components/util/EmptyView';
import LoadingPlaceholder from 'components/util/LoadingPlaceholder';
import React, { useContext, useEffect, useRef, useState } from 'react';
import { Link, useHistory } from 'react-router-dom';
import client from 'util/client';
import useLocalStorage from 'util/useLocalStorage';
import { IChapter, IMangaChapter, IQueue, PaginatedList } from 'typings';
import { useTranslation } from 'react-i18next';
import { t as translate } from 'i18next';
import requestManager from 'lib/RequestManager';
function epochToDate(epoch: number) {
const date = new Date(0); // The 0 there is the key, which sets the date to the epoch
@@ -66,7 +65,6 @@ function groupByDate(updates: IMangaChapter[]): [string, { item: IMangaChapter;
return Object.keys(groups).map((key) => [key, groups[key]]);
}
const baseWebsocketUrl = JSON.parse(window.localStorage.getItem('serverBaseURL')!).replace('http', 'ws');
const initialQueue = {
status: 'Stopped',
queue: [],
@@ -82,14 +80,11 @@ const Updates: React.FC = () => {
const [fetched, setFetched] = useState(false);
const [lastPageNum, setLastPageNum] = useState(0);
const [serverAddress] = useLocalStorage<String>('serverBaseURL', '');
const [useCache] = useLocalStorage<boolean>('useCache', true);
const [, setWsClient] = useState<WebSocket>();
const [{ queue }, setQueueState] = useState<IQueue>(initialQueue);
useEffect(() => {
const wsc = new WebSocket(`${baseWebsocketUrl}/api/v1/downloads`);
const wsc = requestManager.getDownloadWebSocket();
wsc.onmessage = (e) => {
const data = JSON.parse(e.data) as IQueue;
setQueueState(data);
@@ -108,7 +103,8 @@ const Updates: React.FC = () => {
useEffect(() => {
if (hasNextPage) {
client
requestManager
.getClient()
.get(`/api/v1/update/recentChapters/${lastPageNum}`)
.then((response) => response.data)
.then(({ hasNextPage: fetchedHasNextPage, page }: PaginatedList<IMangaChapter>) => {
@@ -149,7 +145,7 @@ const Updates: React.FC = () => {
};
const downloadChapter = (chapter: IChapter) => {
client.get(`/api/v1/download/${chapter.mangaId}/chapter/${chapter.index}`);
requestManager.addChapterToDownloadQueue(chapter.mangaId, chapter.index);
};
return (
@@ -200,7 +196,7 @@ const Updates: React.FC = () => {
marginRight: 2,
imageRendering: 'pixelated',
}}
src={`${serverAddress}${manga.thumbnailUrl}?useCache=${useCache}`}
src={requestManager.getValidImgUrlFor(manga.thumbnailUrl)}
/>
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
<Typography variant="h5" component="h2">

View File

@@ -13,9 +13,8 @@ import ListItemText from '@mui/material/ListItemText';
import ListItemLink from 'components/util/ListItemLink';
import NavbarContext from 'components/context/NavbarContext';
import LoadingPlaceholder from 'components/util/LoadingPlaceholder';
import { useQuery } from 'util/client';
import { IAbout } from 'typings';
import { useTranslation } from 'react-i18next';
import requestManager from 'lib/RequestManager';
export default function About() {
const { t } = useTranslation();
@@ -26,7 +25,7 @@ export default function About() {
setAction(null);
}, [t]);
const { data: about } = useQuery<IAbout>('/api/v1/settings/about');
const { data: about } = requestManager.useGetAbout();
if (about === undefined) {
return <LoadingPlaceholder />;

View File

@@ -11,11 +11,11 @@ import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import ListItemText from '@mui/material/ListItemText';
import { fromEvent } from 'file-selector';
import client from 'util/client';
import makeToast from 'components/util/Toast';
import ListItemLink from 'components/util/ListItemLink';
import NavbarContext from 'components/context/NavbarContext';
import { useTranslation } from 'react-i18next';
import requestManager from 'lib/RequestManager';
export default function Backup() {
const { t } = useTranslation();
@@ -25,18 +25,11 @@ export default function Backup() {
setAction(null);
}, [t]);
const { baseURL } = client.defaults;
const submitBackup = (file: File) => {
if (file.name.toLowerCase().endsWith('proto.gz')) {
const formData = new FormData();
formData.append('backup.proto.gz', file);
makeToast(t('settings.backup.label.restoring_backup'), 'info');
client
.post('/api/v1/backup/import/file', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
})
requestManager
.restoreBackupFile(file)
.then(() => makeToast(t('settings.backup.label.restored_backup'), 'success'))
.catch(() => makeToast(t('settings.backup.label.backup_restore_failed'), 'error'));
} else if (file.name.toLowerCase().endsWith('json')) {
@@ -76,7 +69,7 @@ export default function Backup() {
return (
<>
<List sx={{ padding: 0 }}>
<ListItemLink to={`${baseURL}/api/v1/backup/export/file`} directLink>
<ListItemLink to={requestManager.getExportBackupUrl()} directLink>
<ListItemText
primary={t('settings.backup.label.create_backup')}
secondary={t('settings.backup.label.create_backup_info')}

View File

@@ -31,10 +31,10 @@ import DialogTitle from '@mui/material/DialogTitle';
import Checkbox from '@mui/material/Checkbox';
import FormControlLabel from '@mui/material/FormControlLabel';
import NavbarContext from 'components/context/NavbarContext';
import client, { useQuery } from 'util/client';
import { ICategory } from 'typings';
import { useTranslation } from 'react-i18next';
import { DEFAULT_FULL_FAB_HEIGHT } from 'components/util/StyledFab';
import requestManager from 'lib/RequestManager';
const getItemStyle = (
isDragging: boolean,
@@ -58,7 +58,7 @@ export default function Categories() {
setAction(null);
}, [t]);
const { data, mutate } = useQuery<ICategory[]>('/api/v1/category/');
const { data, mutate } = requestManager.useGetCategories();
const categories = useMemo(() => {
const res = [...(data ?? [])];
if (res.length > 0 && res[0].name === 'Default') {
@@ -79,10 +79,7 @@ export default function Categories() {
newData.splice(to, 0, removed);
mutate(newData, { revalidate: false });
const formData = new FormData();
formData.append('from', `${from + 1}`);
formData.append('to', `${to + 1}`);
client.patch('/api/v1/category/reorder', formData).finally(() => mutate());
requestManager.reorderCategory(from + 1, to + 1).finally(() => mutate());
};
const onDragEnd = (result: DropResult) => {
@@ -119,21 +116,19 @@ export default function Categories() {
const handleDialogSubmit = () => {
setDialogOpen(false);
const formData = new FormData();
formData.append('name', dialogName);
formData.append('default', dialogDefault.toString());
if (categoryToEdit === -1) {
client.post('/api/v1/category/', formData).finally(() => mutate());
requestManager.createCategory(dialogName).finally(() => mutate());
} else {
const category = categories[categoryToEdit];
client.patch(`/api/v1/category/${category.id}`, formData).finally(() => mutate());
requestManager
.updateCategory(category.id, { name: dialogName, default: dialogDefault })
.finally(() => mutate());
}
};
const deleteCategory = (index: number) => {
const category = categories[index];
client.delete(`/api/v1/category/${category.id}`).finally(() => mutate());
requestManager.deleteCategory(category.id).finally(() => mutate());
};
return (

View File

@@ -11,7 +11,6 @@ import List from '@mui/material/List';
import ListItemText from '@mui/material/ListItemText';
import NavbarContext from 'components/context/NavbarContext';
import ListSubheader from '@mui/material/ListSubheader';
import client, { useQuery } from 'util/client';
import { ICategory, IncludeInGlobalUpdate } from 'typings';
import Dialog from '@mui/material/Dialog';
import DialogContent from '@mui/material/DialogContent';
@@ -25,6 +24,7 @@ import { styled } from '@mui/system';
import makeToast from 'components/util/Toast';
import { t as translate } from 'i18next';
import { useTranslation } from 'react-i18next';
import requestManager from 'lib/RequestManager';
const CategoriesDiv = styled('div')({
display: 'flex',
@@ -77,7 +77,7 @@ export default function LibrarySettings() {
setAction(null);
}, [t]);
const { data: categories, isLoading, error: requestError, mutate } = useQuery<ICategory[]>('/api/v1/category/');
const { data: categories, isLoading, error: requestError, mutate } = requestManager.useGetCategories();
const [currentCategories, setCurrentCategories] = useState<ICategory[]>(categories ?? []); // categories to check if response categories changed
const [dialogCategories, setDialogCategories] = useState<ICategory[]>(categories ?? []); // categories that are shown and updated in the dialog
@@ -110,12 +110,8 @@ export default function LibrarySettings() {
requestError,
);
const updateCategory = (category: ICategory) => {
const formData = new FormData();
formData.append('includeInUpdate', `${category.includeInUpdate}`);
return client.patch(`/api/v1/category/${category.id}`, formData);
};
const updateCategory = (category: ICategory) =>
requestManager.updateCategory(category.id, { includeInUpdate: category.includeInUpdate });
const updateCategories = async () => {
const categoriesToUpdate = dialogCategories.filter((category) => {