Feature/swr for simple queries (#187)
* Refactor simple queries with useSWR hook * Fix plus button in Extensions screen * Add type to categories query * Add useQuery abstraction which adds loading attribute * Reload categories even if reorder fails
This commit is contained in:
@@ -5,7 +5,7 @@
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
import DialogTitle from '@mui/material/DialogTitle';
|
import DialogTitle from '@mui/material/DialogTitle';
|
||||||
import DialogContent from '@mui/material/DialogContent';
|
import DialogContent from '@mui/material/DialogContent';
|
||||||
@@ -14,7 +14,7 @@ import Dialog from '@mui/material/Dialog';
|
|||||||
import Checkbox from '@mui/material/Checkbox';
|
import Checkbox from '@mui/material/Checkbox';
|
||||||
import FormControlLabel from '@mui/material/FormControlLabel';
|
import FormControlLabel from '@mui/material/FormControlLabel';
|
||||||
import FormGroup from '@mui/material/FormGroup';
|
import FormGroup from '@mui/material/FormGroup';
|
||||||
import client from 'util/client';
|
import client, { useQuery } from 'util/client';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
open: boolean
|
open: boolean
|
||||||
@@ -22,37 +22,21 @@ interface IProps {
|
|||||||
mangaId: number
|
mangaId: number
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ICategoryInfo {
|
|
||||||
category: ICategory
|
|
||||||
selected: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function CategorySelect(props: IProps) {
|
export default function CategorySelect(props: IProps) {
|
||||||
const { open, setOpen, mangaId } = props;
|
const { open, setOpen, mangaId } = props;
|
||||||
const [categoryInfos, setCategoryInfos] = useState<ICategoryInfo[]>([]);
|
|
||||||
|
|
||||||
const [updateTriggerHolder, setUpdateTriggerHolder] = useState(0); // just a hack
|
const { data: mangaCategoriesData, mutate } = useQuery<ICategory[]>(`/api/v1/manga/${mangaId}/category`);
|
||||||
const triggerUpdate = () => setUpdateTriggerHolder(updateTriggerHolder + 1); // just a hack
|
const { data: categoriesData } = useQuery<ICategory[]>('/api/v1/category');
|
||||||
|
|
||||||
useEffect(() => {
|
const allCategories = useMemo(() => {
|
||||||
let tmpCategoryInfos: ICategoryInfo[] = [];
|
const cats = [...(categoriesData ?? [])]; // make copy
|
||||||
client.get('/api/v1/category/')
|
if (cats.length > 0 && cats[0].name === 'Default') {
|
||||||
.then((response) => response.data)
|
cats.shift(); // remove first category if it is 'Default'
|
||||||
.then((data: ICategory[]) => {
|
}
|
||||||
if (data.length > 0 && data[0].name === 'Default') { data.shift(); }
|
return cats;
|
||||||
tmpCategoryInfos = data.map((category) => ({ category, selected: false }));
|
}, [categoriesData]);
|
||||||
})
|
|
||||||
.then(() => {
|
const selectedIds = mangaCategoriesData?.map((c) => c.id) ?? [];
|
||||||
client.get(`/api/v1/manga/${mangaId}/category/`)
|
|
||||||
.then((response) => response.data)
|
|
||||||
.then((data: ICategory[]) => {
|
|
||||||
data.forEach((category) => {
|
|
||||||
tmpCategoryInfos[category.order - 1].selected = true;
|
|
||||||
});
|
|
||||||
setCategoryInfos(tmpCategoryInfos);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}, [updateTriggerHolder, open]);
|
|
||||||
|
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
@@ -67,7 +51,7 @@ export default function CategorySelect(props: IProps) {
|
|||||||
|
|
||||||
const method = checked ? client.get : client.delete;
|
const method = checked ? client.get : client.delete;
|
||||||
method(`/api/v1/manga/${mangaId}/category/${categoryId}`)
|
method(`/api/v1/manga/${mangaId}/category/${categoryId}`)
|
||||||
.then(() => triggerUpdate());
|
.then(() => mutate());
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -84,7 +68,7 @@ export default function CategorySelect(props: IProps) {
|
|||||||
<DialogTitle>Set categories</DialogTitle>
|
<DialogTitle>Set categories</DialogTitle>
|
||||||
<DialogContent dividers>
|
<DialogContent dividers>
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
{categoryInfos.length === 0
|
{allCategories.length === 0
|
||||||
&& (
|
&& (
|
||||||
<span>
|
<span>
|
||||||
No categories found!
|
No categories found!
|
||||||
@@ -92,17 +76,17 @@ export default function CategorySelect(props: IProps) {
|
|||||||
You should make some from settings.
|
You should make some from settings.
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{categoryInfos.map((categoryInfo) => (
|
{allCategories.map((category) => (
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
control={(
|
control={(
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={categoryInfo.selected}
|
checked={selectedIds.includes(category.id)}
|
||||||
onChange={(e) => handleChange(e, categoryInfo.category.id)}
|
onChange={(e) => handleChange(e, category.id)}
|
||||||
color="default"
|
color="default"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
label={categoryInfo.category.name}
|
label={category.name}
|
||||||
key={categoryInfo.category.id}
|
key={category.id}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import NavbarContext from 'components/context/NavbarContext';
|
import NavbarContext from 'components/context/NavbarContext';
|
||||||
import React, { useContext, useEffect, useState } from 'react';
|
import React, { useContext, useEffect } from 'react';
|
||||||
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
|
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
|
||||||
import PauseIcon from '@mui/icons-material/Pause';
|
import PauseIcon from '@mui/icons-material/Pause';
|
||||||
import IconButton from '@mui/material/IconButton';
|
import IconButton from '@mui/material/IconButton';
|
||||||
@@ -28,8 +28,7 @@ import EmptyView from 'components/util/EmptyView';
|
|||||||
import { Box } from '@mui/system';
|
import { Box } from '@mui/system';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
|
import useSubscription from 'components/library/useSubscription';
|
||||||
const baseWebsocketUrl = JSON.parse(window.localStorage.getItem('serverBaseURL')!).replace('http', 'ws');
|
|
||||||
|
|
||||||
const getItemStyle = (isDragging: boolean,
|
const getItemStyle = (isDragging: boolean,
|
||||||
draggableStyle: DraggingStyle | NotDraggingStyle | undefined, palette: Palette) => ({
|
draggableStyle: DraggingStyle | NotDraggingStyle | undefined, palette: Palette) => ({
|
||||||
@@ -47,9 +46,8 @@ const initialQueue = {
|
|||||||
} as IQueue;
|
} as IQueue;
|
||||||
|
|
||||||
export default function DownloadQueue() {
|
export default function DownloadQueue() {
|
||||||
const [, setWsClient] = useState<WebSocket>();
|
const { data: queueState } = useSubscription<IQueue>('/api/v1/downloads');
|
||||||
const [queueState, setQueueState] = useState<IQueue>(initialQueue);
|
const { queue, status } = queueState ?? initialQueue;
|
||||||
const { queue, status } = queueState;
|
|
||||||
|
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
@@ -84,15 +82,6 @@ export default function DownloadQueue() {
|
|||||||
});
|
});
|
||||||
}, [status]);
|
}, [status]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const wsc = new WebSocket(`${baseWebsocketUrl}/api/v1/downloads`);
|
|
||||||
wsc.onmessage = (e) => {
|
|
||||||
setQueueState(JSON.parse(e.data));
|
|
||||||
};
|
|
||||||
|
|
||||||
setWsClient(wsc);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const onDragEnd = (result: DropResult) => {
|
const onDragEnd = (result: DropResult) => {
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,13 +5,15 @@
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import React, { useContext, useEffect, useState } from 'react';
|
import React, {
|
||||||
|
useContext, useEffect, useState, useMemo, useRef,
|
||||||
|
} from 'react';
|
||||||
import { fromEvent } from 'file-selector';
|
import { fromEvent } from 'file-selector';
|
||||||
import IconButton from '@mui/material/IconButton';
|
import IconButton from '@mui/material/IconButton';
|
||||||
import AddIcon from '@mui/icons-material/Add';
|
import AddIcon from '@mui/icons-material/Add';
|
||||||
import ExtensionCard from 'components/ExtensionCard';
|
import ExtensionCard from 'components/ExtensionCard';
|
||||||
import NavbarContext from 'components/context/NavbarContext';
|
import NavbarContext from 'components/context/NavbarContext';
|
||||||
import client from 'util/client';
|
import client, { useQuery } from 'util/client';
|
||||||
import useLocalStorage from 'util/useLocalStorage';
|
import useLocalStorage from 'util/useLocalStorage';
|
||||||
import LangSelect from 'components/navbar/action/LangSelect';
|
import LangSelect from 'components/navbar/action/LangSelect';
|
||||||
import { extensionDefaultLangs, langCodeToName, langSortCmp } from 'util/language';
|
import { extensionDefaultLangs, langCodeToName, langSortCmp } from 'util/language';
|
||||||
@@ -63,6 +65,7 @@ function groupExtensions(extensions: IExtension[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function MangaExtensions() {
|
export default function MangaExtensions() {
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
const { setTitle, setAction } = useContext(NavbarContext);
|
const { setTitle, setAction } = useContext(NavbarContext);
|
||||||
const [shownLangs, setShownLangs] = useLocalStorage<string[]>('shownExtensionLangs', extensionDefaultLangs());
|
const [shownLangs, setShownLangs] = useLocalStorage<string[]>('shownExtensionLangs', extensionDefaultLangs());
|
||||||
const [showNsfw] = useLocalStorage<boolean>('showNsfw', true);
|
const [showNsfw] = useLocalStorage<boolean>('showNsfw', true);
|
||||||
@@ -76,9 +79,7 @@ export default function MangaExtensions() {
|
|||||||
<>
|
<>
|
||||||
<AppbarSearch />
|
<AppbarSearch />
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={
|
onClick={() => inputRef.current?.click()}
|
||||||
() => document.getElementById('external-extension-file')?.click()
|
|
||||||
}
|
|
||||||
size="large"
|
size="large"
|
||||||
>
|
>
|
||||||
<AddIcon />
|
<AddIcon />
|
||||||
@@ -92,16 +93,19 @@ export default function MangaExtensions() {
|
|||||||
);
|
);
|
||||||
}, [shownLangs]);
|
}, [shownLangs]);
|
||||||
|
|
||||||
const [extensionsRaw, setExtensionsRaw] = useState<IExtension[]>([]);
|
const { data: allExtensions, mutate, loading } = useQuery<IExtension[]>('/api/v1/extension/list');
|
||||||
|
|
||||||
const [updateTriggerHolder, setUpdateTriggerHolder] = useState(0); // just a hack
|
const filteredExtensions = useMemo(() => (allExtensions ?? []).filter((ext) => {
|
||||||
const triggerUpdate = () => setUpdateTriggerHolder(updateTriggerHolder + 1); // just a hack
|
const nsfwFilter = showNsfw || !ext.isNsfw;
|
||||||
|
if (!query) return nsfwFilter;
|
||||||
|
return nsfwFilter && ext.name.toLowerCase().includes(query.toLowerCase());
|
||||||
|
}), [allExtensions, showNsfw, query]);
|
||||||
|
|
||||||
useEffect(() => {
|
const groupedExtensions = useMemo(() => groupExtensions(filteredExtensions)
|
||||||
client.get('/api/v1/extension/list')
|
.filter((group) => group[EXTENSIONS].length > 0)
|
||||||
.then((response) => response.data)
|
.filter((group) => ['installed', 'updates pending', 'all', ...shownLangs].includes(group[LANGUAGE])), [shownLangs, filteredExtensions]);
|
||||||
.then((data) => setExtensionsRaw(data));
|
|
||||||
}, [updateTriggerHolder]);
|
const flatRenderItems: (IExtension | string)[] = groupedExtensions.flat(2);
|
||||||
|
|
||||||
const [toasts, makeToast] = makeToaster(useState<React.ReactElement[]>([]));
|
const [toasts, makeToast] = makeToaster(useState<React.ReactElement[]>([]));
|
||||||
|
|
||||||
@@ -110,16 +114,16 @@ export default function MangaExtensions() {
|
|||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
|
|
||||||
// empty the input
|
if (inputRef.current) {
|
||||||
// @ts-ignore
|
inputRef.current.value = '';
|
||||||
document.getElementById('external-extension-file').value = null;
|
}
|
||||||
|
|
||||||
makeToast('Installing Extension File....', 'info');
|
makeToast('Installing Extension File....', 'info');
|
||||||
client.post('/api/v1/extension/install',
|
client.post('/api/v1/extension/install',
|
||||||
formData, { headers: { 'Content-Type': 'multipart/form-data' } })
|
formData, { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||||
.then(() => {
|
.then(() => {
|
||||||
makeToast('Installed extension successfully!', 'success');
|
makeToast('Installed extension successfully!', 'success');
|
||||||
triggerUpdate();
|
mutate();
|
||||||
})
|
})
|
||||||
.catch(() => makeToast('Extension installion failed!', 'error'));
|
.catch(() => makeToast('Extension installion failed!', 'error'));
|
||||||
} else {
|
} else {
|
||||||
@@ -127,60 +131,43 @@ export default function MangaExtensions() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const dropHandler = async (e: Event) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const files = await fromEvent(e);
|
|
||||||
|
|
||||||
submitExternalExtension(files[0] as File);
|
|
||||||
};
|
|
||||||
|
|
||||||
const dragOverHandler = (e: Event) => {
|
|
||||||
e.preventDefault();
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.addEventListener('drop', dropHandler);
|
const dropHandler = async (e: Event) => {
|
||||||
document.addEventListener('dragover', dragOverHandler);
|
e.preventDefault();
|
||||||
|
const files = await fromEvent(e);
|
||||||
const changeHandler = async (evt: Event) => {
|
|
||||||
const files = await fromEvent(evt);
|
|
||||||
submitExternalExtension(files[0] as File);
|
submitExternalExtension(files[0] as File);
|
||||||
};
|
};
|
||||||
const input = document.getElementById('external-extension-file');
|
|
||||||
input?.addEventListener('change', changeHandler);
|
const dragOverHandler = (e: Event) => {
|
||||||
|
e.preventDefault();
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('drop', dropHandler);
|
||||||
|
document.addEventListener('dragover', dragOverHandler);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('drop', dropHandler);
|
document.removeEventListener('drop', dropHandler);
|
||||||
document.removeEventListener('dragover', dragOverHandler);
|
document.removeEventListener('dragover', dragOverHandler);
|
||||||
input?.removeEventListener('change', changeHandler);
|
|
||||||
};
|
};
|
||||||
}, [extensionsRaw]); // useEffect only after <input> renders
|
}, []);
|
||||||
|
|
||||||
if (extensionsRaw.length === 0) {
|
if (loading) {
|
||||||
return <LoadingPlaceholder />;
|
return <LoadingPlaceholder />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const filtered = extensionsRaw.filter((ext) => {
|
|
||||||
const nsfwFilter = showNsfw || !ext.isNsfw;
|
|
||||||
if (!query) return nsfwFilter;
|
|
||||||
return nsfwFilter && ext.name.toLowerCase().includes(query.toLowerCase());
|
|
||||||
});
|
|
||||||
|
|
||||||
const combinedShownLangs = ['installed', 'updates pending', 'all', ...shownLangs];
|
|
||||||
|
|
||||||
const groupedExtensions: [string, IExtension[]][] = groupExtensions(filtered)
|
|
||||||
.filter((group) => group[EXTENSIONS].length > 0)
|
|
||||||
.filter((group) => combinedShownLangs.includes(group[LANGUAGE]));
|
|
||||||
|
|
||||||
const flatRenderItems: (IExtension | string)[] = groupedExtensions.flat(2);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{toasts}
|
{toasts}
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
id="external-extension-file"
|
|
||||||
style={{ display: 'none' }}
|
style={{ display: 'none' }}
|
||||||
|
ref={inputRef}
|
||||||
|
onChange={(e) => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (file) {
|
||||||
|
submitExternalExtension(file);
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<Virtuoso
|
<Virtuoso
|
||||||
style={{
|
style={{
|
||||||
@@ -214,7 +201,7 @@ export default function MangaExtensions() {
|
|||||||
key={item.apkName}
|
key={item.apkName}
|
||||||
extension={item}
|
extension={item}
|
||||||
notifyInstall={() => {
|
notifyInstall={() => {
|
||||||
triggerUpdate();
|
mutate();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -17,17 +17,21 @@ import LibraryOptions from 'components/library/LibraryOptions';
|
|||||||
import LibraryMangaGrid from 'components/library/LibraryMangaGrid';
|
import LibraryMangaGrid from 'components/library/LibraryMangaGrid';
|
||||||
import AppbarSearch from 'components/util/AppbarSearch';
|
import AppbarSearch from 'components/util/AppbarSearch';
|
||||||
import { useQueryParam, NumberParam } from 'use-query-params';
|
import { useQueryParam, NumberParam } from 'use-query-params';
|
||||||
import useSWR from 'swr';
|
import { useQuery } from 'util/client';
|
||||||
import UpdateChecker from '../components/library/UpdateChecker';
|
import UpdateChecker from '../components/library/UpdateChecker';
|
||||||
|
|
||||||
export default function Library() {
|
export default function Library() {
|
||||||
const { data: tabsData, error: tabsError } = useSWR<ICategory[]>('/api/v1/category');
|
const { data: tabsData, error: tabsError, loading } = useQuery<ICategory[]>('/api/v1/category');
|
||||||
const tabs = tabsData ?? [];
|
const tabs = tabsData ?? [];
|
||||||
|
|
||||||
const [tabSearchParam, setTabSearchParam] = useQueryParam('tab', NumberParam);
|
const [tabSearchParam, setTabSearchParam] = useQueryParam('tab', NumberParam);
|
||||||
|
|
||||||
const activeTab = tabs.find((t) => t.order === tabSearchParam) ?? tabs[0];
|
const activeTab = tabs.find((t) => t.order === tabSearchParam) ?? tabs[0];
|
||||||
const { data: mangaData, error: mangaError } = useSWR<IManga[]>(`/api/v1/category/${activeTab?.id}`, {
|
const {
|
||||||
|
data: mangaData,
|
||||||
|
error: mangaError,
|
||||||
|
loading: mangaLoading,
|
||||||
|
} = useQuery<IManga[]>(`/api/v1/category/${activeTab?.id}`, {
|
||||||
isPaused: () => activeTab == null,
|
isPaused: () => activeTab == null,
|
||||||
});
|
});
|
||||||
const mangas = mangaData ?? [];
|
const mangas = mangaData ?? [];
|
||||||
@@ -56,7 +60,7 @@ export default function Library() {
|
|||||||
return <EmptyView message="Could not load categories" messageExtra={tabsError?.message ?? tabsError} />;
|
return <EmptyView message="Could not load categories" messageExtra={tabsError?.message ?? tabsError} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tabsData == null) {
|
if (loading) {
|
||||||
return <LoadingPlaceholder />;
|
return <LoadingPlaceholder />;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,7 +76,7 @@ export default function Library() {
|
|||||||
lastPageNum={lastPageNum}
|
lastPageNum={lastPageNum}
|
||||||
setLastPageNum={setLastPageNum}
|
setLastPageNum={setLastPageNum}
|
||||||
message="Your Library is empty"
|
message="Your Library is empty"
|
||||||
isLoading={activeTab != null && mangaData == null && mangaError == null}
|
isLoading={activeTab != null && mangaLoading}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -110,7 +114,7 @@ export default function Library() {
|
|||||||
lastPageNum={lastPageNum}
|
lastPageNum={lastPageNum}
|
||||||
setLastPageNum={setLastPageNum}
|
setLastPageNum={setLastPageNum}
|
||||||
message="Category is Empty"
|
message="Category is Empty"
|
||||||
isLoading={mangaData == null}
|
isLoading={mangaLoading}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
|||||||
@@ -8,11 +8,10 @@
|
|||||||
import React, {
|
import React, {
|
||||||
useCallback, useEffect, useContext, useState, useRef,
|
useCallback, useEffect, useContext, useState, useRef,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import useSWR from 'swr';
|
|
||||||
import { Box } from '@mui/system';
|
import { Box } from '@mui/system';
|
||||||
import MangaDetails from 'components/MangaDetails';
|
import MangaDetails from 'components/MangaDetails';
|
||||||
import NavbarContext from 'components/context/NavbarContext';
|
import NavbarContext from 'components/context/NavbarContext';
|
||||||
import { fetcher } from 'util/client';
|
import { fetcher, useQuery } from 'util/client';
|
||||||
import LoadingPlaceholder from 'components/util/LoadingPlaceholder';
|
import LoadingPlaceholder from 'components/util/LoadingPlaceholder';
|
||||||
import ChapterList from 'components/chapter/ChapterList';
|
import ChapterList from 'components/chapter/ChapterList';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
@@ -24,8 +23,8 @@ export default function Manga() {
|
|||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
const autofetchedRef = useRef(false);
|
const autofetchedRef = useRef(false);
|
||||||
|
|
||||||
const { data: manga, error, mutate: mutateManga } = useSWR<IManga>(`/api/v1/manga/${id}/?onlineFetch=false`);
|
const { data: manga, error, mutate: mutateManga } = useQuery<IManga>(`/api/v1/manga/${id}/?onlineFetch=false`);
|
||||||
const { data: chaptersData, mutate: mutateChapters } = useSWR<IChapter[]>(`/api/v1/manga/${id}/chapters?onlineFetch=false`);
|
const { data: chaptersData, mutate: mutateChapters } = useQuery<IChapter[]>(`/api/v1/manga/${id}/chapters?onlineFetch=false`);
|
||||||
|
|
||||||
const [fetchingOnline, setFetchingOnline] = useState(false);
|
const [fetchingOnline, setFetchingOnline] = useState(false);
|
||||||
const fetchOnline = useCallback(async () => {
|
const fetchOnline = useCallback(async () => {
|
||||||
|
|||||||
@@ -5,10 +5,10 @@
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import React, { useContext, useEffect, useState } from 'react';
|
import React, { useContext, useEffect } from 'react';
|
||||||
import NavbarContext from 'components/context/NavbarContext';
|
import NavbarContext from 'components/context/NavbarContext';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import client from 'util/client';
|
import client, { useQuery } from 'util/client';
|
||||||
import { SwitchPreferenceCompat, CheckBoxPreference } from 'components/sourceConfiguration/TwoStatePreference';
|
import { SwitchPreferenceCompat, CheckBoxPreference } from 'components/sourceConfiguration/TwoStatePreference';
|
||||||
import ListPreference from 'components/sourceConfiguration/ListPreference';
|
import ListPreference from 'components/sourceConfiguration/ListPreference';
|
||||||
import EditTextPreference from 'components/sourceConfiguration/EditTextPreference';
|
import EditTextPreference from 'components/sourceConfiguration/EditTextPreference';
|
||||||
@@ -34,21 +34,12 @@ function getPrefComponent(type: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function SourceConfigure() {
|
export default function SourceConfigure() {
|
||||||
const [sourcePreferences, setSourcePreferences] = useState<SourcePreferences[]>([]);
|
|
||||||
const { setTitle, setAction } = useContext(NavbarContext);
|
const { setTitle, setAction } = useContext(NavbarContext);
|
||||||
|
|
||||||
const [updateTriggerHolder, setUpdateTriggerHolder] = useState<number>(0); // just a hack
|
|
||||||
const triggerUpdate = () => setUpdateTriggerHolder(updateTriggerHolder + 1); // just a hack
|
|
||||||
|
|
||||||
useEffect(() => { setTitle('Source Configuration'); setAction(<></>); }, []);
|
useEffect(() => { setTitle('Source Configuration'); setAction(<></>); }, []);
|
||||||
|
|
||||||
const { sourceId } = useParams<{ sourceId: string }>();
|
const { sourceId } = useParams<{ sourceId: string }>();
|
||||||
|
const { data: sourcePreferences = [], mutate } = useQuery<SourcePreferences[]>(`/api/v1/source/${sourceId}/preferences`);
|
||||||
useEffect(() => {
|
|
||||||
client.get(`/api/v1/source/${sourceId}/preferences`)
|
|
||||||
.then((response) => response.data)
|
|
||||||
.then((data) => setSourcePreferences(data));
|
|
||||||
}, [updateTriggerHolder]);
|
|
||||||
|
|
||||||
const convertToString = (position: number, value: any): string => {
|
const convertToString = (position: number, value: any): string => {
|
||||||
switch (sourcePreferences[position].props.defaultValueType) {
|
switch (sourcePreferences[position].props.defaultValueType) {
|
||||||
@@ -63,7 +54,7 @@ export default function SourceConfigure() {
|
|||||||
(value: any) => {
|
(value: any) => {
|
||||||
client.post(`/api/v1/source/${sourceId}/preferences`,
|
client.post(`/api/v1/source/${sourceId}/preferences`,
|
||||||
JSON.stringify({ position, value: convertToString(position, value) }))
|
JSON.stringify({ position, value: convertToString(position, value) }))
|
||||||
.then(() => triggerUpdate());
|
.then(() => mutate());
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -5,11 +5,10 @@
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import React, { useContext, useEffect, useState } from 'react';
|
import React, { useContext, useEffect } from 'react';
|
||||||
import LangSelect from 'components/navbar/action/LangSelect';
|
import LangSelect from 'components/navbar/action/LangSelect';
|
||||||
import SourceCard from 'components/SourceCard';
|
import SourceCard from 'components/SourceCard';
|
||||||
import NavbarContext from 'components/context/NavbarContext';
|
import NavbarContext from 'components/context/NavbarContext';
|
||||||
import client from 'util/client';
|
|
||||||
import {
|
import {
|
||||||
sourceDefualtLangs, sourceForcedDefaultLangs, langCodeToName, langSortCmp,
|
sourceDefualtLangs, sourceForcedDefaultLangs, langCodeToName, langSortCmp,
|
||||||
} from 'util/language';
|
} from 'util/language';
|
||||||
@@ -18,6 +17,7 @@ import LoadingPlaceholder from 'components/util/LoadingPlaceholder';
|
|||||||
import { IconButton } from '@mui/material';
|
import { IconButton } from '@mui/material';
|
||||||
import TravelExploreIcon from '@mui/icons-material/TravelExplore';
|
import TravelExploreIcon from '@mui/icons-material/TravelExplore';
|
||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
|
import { useQuery } from 'util/client';
|
||||||
|
|
||||||
function sourceToLangList(sources: ISource[]) {
|
function sourceToLangList(sources: ISource[]) {
|
||||||
const result: string[] = [];
|
const result: string[] = [];
|
||||||
@@ -46,8 +46,7 @@ export default function Sources() {
|
|||||||
const [shownLangs, setShownLangs] = useLocalStorage<string[]>('shownSourceLangs', sourceDefualtLangs());
|
const [shownLangs, setShownLangs] = useLocalStorage<string[]>('shownSourceLangs', sourceDefualtLangs());
|
||||||
const [showNsfw] = useLocalStorage<boolean>('showNsfw', true);
|
const [showNsfw] = useLocalStorage<boolean>('showNsfw', true);
|
||||||
|
|
||||||
const [sources, setSources] = useState<ISource[]>([]);
|
const { data: sources, loading } = useQuery<ISource[]>('/api/v1/source/list');
|
||||||
const [fetched, setFetched] = useState<boolean>(false);
|
|
||||||
|
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
@@ -80,27 +79,23 @@ export default function Sources() {
|
|||||||
<LangSelect
|
<LangSelect
|
||||||
shownLangs={shownLangs}
|
shownLangs={shownLangs}
|
||||||
setShownLangs={setShownLangs}
|
setShownLangs={setShownLangs}
|
||||||
allLangs={sourceToLangList(sources)}
|
allLangs={sourceToLangList(sources ?? [])}
|
||||||
forcedLangs={sourceForcedDefaultLangs()}
|
forcedLangs={sourceForcedDefaultLangs()}
|
||||||
/>
|
/>
|
||||||
</>,
|
</>,
|
||||||
);
|
);
|
||||||
}, [shownLangs, sources]);
|
}, [shownLangs, sources]);
|
||||||
|
|
||||||
useEffect(() => {
|
if (loading) return <LoadingPlaceholder />;
|
||||||
client.get('/api/v1/source/list')
|
|
||||||
.then((response) => response.data)
|
|
||||||
.then((data) => { setSources(data); setFetched(true); });
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (sources.length === 0) {
|
if (sources?.length === 0) {
|
||||||
if (fetched) return (<h3>No sources found. Install Some Extensions first.</h3>);
|
return (<h3>No sources found. Install Some Extensions first.</h3>);
|
||||||
return <LoadingPlaceholder />;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* eslint-disable-next-line max-len */}
|
{/* eslint-disable-next-line max-len */}
|
||||||
{Object.entries(groupByLang(sources)).sort((a, b) => langSortCmp(a[0], b[0])).map(([lang, list]) => (
|
{Object.entries(groupByLang(sources ?? [])).sort((a, b) => langSortCmp(a[0], b[0])).map(([lang, list]) => (
|
||||||
shownLangs.indexOf(lang) !== -1 && (
|
shownLangs.indexOf(lang) !== -1 && (
|
||||||
<React.Fragment key={lang}>
|
<React.Fragment key={lang}>
|
||||||
<h1 key={lang} style={{ marginLeft: 25 }}>{langCodeToName(lang)}</h1>
|
<h1 key={lang} style={{ marginLeft: 25 }}>{langCodeToName(lang)}</h1>
|
||||||
|
|||||||
@@ -1,26 +1,18 @@
|
|||||||
import React, { useContext, useEffect, useState } from 'react';
|
import React, { useContext, useEffect } from 'react';
|
||||||
import List from '@mui/material/List';
|
import List from '@mui/material/List';
|
||||||
import ListItem from '@mui/material/ListItem';
|
import ListItem from '@mui/material/ListItem';
|
||||||
import ListItemText from '@mui/material/ListItemText';
|
import ListItemText from '@mui/material/ListItemText';
|
||||||
import client from 'util/client';
|
|
||||||
import ListItemLink from 'components/util/ListItemLink';
|
import ListItemLink from 'components/util/ListItemLink';
|
||||||
import NavbarContext from 'components/context/NavbarContext';
|
import NavbarContext from 'components/context/NavbarContext';
|
||||||
import LoadingPlaceholder from 'components/util/LoadingPlaceholder';
|
import LoadingPlaceholder from 'components/util/LoadingPlaceholder';
|
||||||
|
import { useQuery } from 'util/client';
|
||||||
|
|
||||||
export default function About() {
|
export default function About() {
|
||||||
const { setTitle, setAction } = useContext(NavbarContext);
|
const { setTitle, setAction } = useContext(NavbarContext);
|
||||||
|
|
||||||
const [about, setAbout] = useState<IAbout>();
|
|
||||||
|
|
||||||
useEffect(() => { setTitle('About'); setAction(<></>); }, []);
|
useEffect(() => { setTitle('About'); setAction(<></>); }, []);
|
||||||
|
|
||||||
useEffect(() => {
|
const { data: about } = useQuery<IAbout>('/api/v1/settings/about');
|
||||||
client.get('/api/v1/settings/about')
|
|
||||||
.then((response) => response.data)
|
|
||||||
.then((data:IAbout) => {
|
|
||||||
setAbout(data);
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (about === undefined) {
|
if (about === undefined) {
|
||||||
return <LoadingPlaceholder />;
|
return <LoadingPlaceholder />;
|
||||||
|
|||||||
@@ -10,10 +10,10 @@ import List from '@mui/material/List';
|
|||||||
import ListItem from '@mui/material/ListItem';
|
import ListItem from '@mui/material/ListItem';
|
||||||
import ListItemText from '@mui/material/ListItemText';
|
import ListItemText from '@mui/material/ListItemText';
|
||||||
import { fromEvent } from 'file-selector';
|
import { fromEvent } from 'file-selector';
|
||||||
|
import client from 'util/client';
|
||||||
import makeToast from 'components/util/Toast';
|
import makeToast from 'components/util/Toast';
|
||||||
import ListItemLink from '../../components/util/ListItemLink';
|
import ListItemLink from '../../components/util/ListItemLink';
|
||||||
import NavbarContext from '../../components/context/NavbarContext';
|
import NavbarContext from '../../components/context/NavbarContext';
|
||||||
import client from '../../util/client';
|
|
||||||
|
|
||||||
export default function Backup() {
|
export default function Backup() {
|
||||||
const { setTitle, setAction } = useContext(NavbarContext);
|
const { setTitle, setAction } = useContext(NavbarContext);
|
||||||
|
|||||||
@@ -8,7 +8,9 @@
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import React, { useState, useContext, useEffect } from 'react';
|
import React, {
|
||||||
|
useMemo, useState, useContext, useEffect,
|
||||||
|
} from 'react';
|
||||||
import {
|
import {
|
||||||
List,
|
List,
|
||||||
ListItem,
|
ListItem,
|
||||||
@@ -34,7 +36,7 @@ import DialogTitle from '@mui/material/DialogTitle';
|
|||||||
import Checkbox from '@mui/material/Checkbox';
|
import Checkbox from '@mui/material/Checkbox';
|
||||||
import FormControlLabel from '@mui/material/FormControlLabel';
|
import FormControlLabel from '@mui/material/FormControlLabel';
|
||||||
import NavbarContext from 'components/context/NavbarContext';
|
import NavbarContext from 'components/context/NavbarContext';
|
||||||
import client from 'util/client';
|
import client, { useQuery } from 'util/client';
|
||||||
|
|
||||||
const getItemStyle = (isDragging: boolean,
|
const getItemStyle = (isDragging: boolean,
|
||||||
draggableStyle: DraggingStyle | NotDraggingStyle | undefined, palette: Palette) => ({
|
draggableStyle: DraggingStyle | NotDraggingStyle | undefined, palette: Palette) => ({
|
||||||
@@ -50,37 +52,31 @@ export default function Categories() {
|
|||||||
const { setTitle, setAction } = useContext(NavbarContext);
|
const { setTitle, setAction } = useContext(NavbarContext);
|
||||||
useEffect(() => { setTitle('Categories'); setAction(<></>); }, []);
|
useEffect(() => { setTitle('Categories'); setAction(<></>); }, []);
|
||||||
|
|
||||||
const [categories, setCategories] = useState<ICategory[]>([]);
|
const { data, mutate } = useQuery<ICategory[]>('/api/v1/category/');
|
||||||
|
const categories = useMemo(() => {
|
||||||
|
const res = [...data ?? []];
|
||||||
|
if (res.length > 0 && res[0].name === 'Default') {
|
||||||
|
res.shift();
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
const [categoryToEdit, setCategoryToEdit] = useState<number>(-1); // -1 means new category
|
const [categoryToEdit, setCategoryToEdit] = useState<number>(-1); // -1 means new category
|
||||||
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
|
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
|
||||||
const [dialogName, setDialogName] = useState<string>('');
|
const [dialogName, setDialogName] = useState<string>('');
|
||||||
const [dialogDefault, setDialogDefault] = useState<boolean>(false);
|
const [dialogDefault, setDialogDefault] = useState<boolean>(false);
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
const [updateTriggerHolder, setUpdateTriggerHolder] = useState<number>(0); // just a hack
|
|
||||||
const triggerUpdate = () => setUpdateTriggerHolder(updateTriggerHolder + 1); // just a hack
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!dialogOpen) {
|
|
||||||
client.get('/api/v1/category/')
|
|
||||||
.then((response) => response.data)
|
|
||||||
.then((data) => { if (data.length > 0 && data[0].name === 'Default') data.shift(); return data; })
|
|
||||||
.then((data) => setCategories(data));
|
|
||||||
}
|
|
||||||
}, [updateTriggerHolder]);
|
|
||||||
|
|
||||||
const categoryReorder = (list: ICategory[], from: number, to: number) => {
|
const categoryReorder = (list: ICategory[], from: number, to: number) => {
|
||||||
|
const newData = [...list];
|
||||||
|
const [removed] = newData.splice(from, 1);
|
||||||
|
newData.splice(to, 0, removed);
|
||||||
|
mutate(newData, { revalidate: false });
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('from', `${from + 1}`);
|
formData.append('from', `${from + 1}`);
|
||||||
formData.append('to', `${to + 1}`);
|
formData.append('to', `${to + 1}`);
|
||||||
client.patch('/api/v1/category/reorder', formData)
|
client.patch('/api/v1/category/reorder', formData).finally(() => mutate());
|
||||||
.finally(() => triggerUpdate());
|
|
||||||
|
|
||||||
// also move it in local state to avoid jarring moving behviour...
|
|
||||||
const result = Array.from(list);
|
|
||||||
const [removed] = result.splice(from, 1);
|
|
||||||
result.splice(to, 0, removed);
|
|
||||||
return result;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const onDragEnd = (result: DropResult) => {
|
const onDragEnd = (result: DropResult) => {
|
||||||
@@ -89,11 +85,11 @@ export default function Categories() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setCategories(categoryReorder(
|
categoryReorder(
|
||||||
categories,
|
categories,
|
||||||
result.source.index,
|
result.source.index,
|
||||||
result.destination.index,
|
result.destination.index,
|
||||||
));
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const resetDialog = () => {
|
const resetDialog = () => {
|
||||||
@@ -127,18 +123,18 @@ export default function Categories() {
|
|||||||
|
|
||||||
if (categoryToEdit === -1) {
|
if (categoryToEdit === -1) {
|
||||||
client.post('/api/v1/category/', formData)
|
client.post('/api/v1/category/', formData)
|
||||||
.finally(() => triggerUpdate());
|
.finally(() => mutate());
|
||||||
} else {
|
} else {
|
||||||
const category = categories[categoryToEdit];
|
const category = categories[categoryToEdit];
|
||||||
client.patch(`/api/v1/category/${category.id}`, formData)
|
client.patch(`/api/v1/category/${category.id}`, formData)
|
||||||
.finally(() => triggerUpdate());
|
.finally(() => mutate());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteCategory = (index:number) => {
|
const deleteCategory = (index:number) => {
|
||||||
const category = categories[index];
|
const category = categories[index];
|
||||||
client.delete(`/api/v1/category/${category.id}`)
|
client.delete(`/api/v1/category/${category.id}`)
|
||||||
.finally(() => triggerUpdate());
|
.finally(() => mutate());
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import useSWR, { SWRConfiguration, SWRResponse } from 'swr';
|
||||||
import storage from './localStorage';
|
import storage from './localStorage';
|
||||||
|
|
||||||
const { hostname, port, protocol } = window.location;
|
const { hostname, port, protocol } = window.location;
|
||||||
@@ -40,3 +41,17 @@ export async function fetcher<T = any>(path: string) {
|
|||||||
}
|
}
|
||||||
return res.data as T;
|
return res.data as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const useQuery = <
|
||||||
|
D extends any = any,
|
||||||
|
E extends any = any,
|
||||||
|
>(
|
||||||
|
key: string,
|
||||||
|
config?: SWRConfiguration<D, E>,
|
||||||
|
): SWRResponse<D, E> & { loading: boolean } => {
|
||||||
|
const res = useSWR(key, config);
|
||||||
|
return {
|
||||||
|
...res,
|
||||||
|
loading: res.data == null && res.error == null,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user