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