add prettier for auto formatting (#231)
* [Prettier] Add "prettier" Makes the formatting of the code consistent. By using the eslint-plugin-prettier formatting issues will be highlighted as a lint error. These errors will be auto fixed by running "lint --fix" (which can be done automatically on file save) * [Prettier] Add "prettier" - Fix formatting
This commit is contained in:
@@ -11,16 +11,12 @@ import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import DragHandle from '@mui/icons-material/DragHandle';
|
||||
import PauseIcon from '@mui/icons-material/Pause';
|
||||
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
|
||||
import {
|
||||
Card, CardActionArea, Stack,
|
||||
} from '@mui/material';
|
||||
import { Card, CardActionArea, Stack } from '@mui/material';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import NavbarContext from 'components/context/NavbarContext';
|
||||
import EmptyView from 'components/util/EmptyView';
|
||||
import React, { useContext, useEffect } from 'react';
|
||||
import {
|
||||
DragDropContext, Draggable, Droppable, DropResult,
|
||||
} from 'react-beautiful-dnd';
|
||||
import { DragDropContext, Draggable, Droppable, DropResult } from 'react-beautiful-dnd';
|
||||
import client from 'util/client';
|
||||
|
||||
import Typography from '@mui/material/Typography';
|
||||
@@ -56,8 +52,7 @@ const DownloadQueue: React.FC = () => {
|
||||
}, []);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const onDragEnd = (result: DropResult) => {
|
||||
};
|
||||
const onDragEnd = (result: DropResult) => {};
|
||||
|
||||
if (queue.length === 0) {
|
||||
return <EmptyView message="No downloads" />;
|
||||
@@ -65,14 +60,15 @@ const DownloadQueue: React.FC = () => {
|
||||
|
||||
const handleDelete = (chapter: IChapter) => {
|
||||
// required to stop before deleting otherwise the download kept going. Server issue?
|
||||
client.get('/api/v1/downloads/stop')
|
||||
.then(() => Promise.all([
|
||||
client.get('/api/v1/downloads/stop').then(() =>
|
||||
Promise.all([
|
||||
// remove from download queue
|
||||
client.delete(`/api/v1/download/${chapter.mangaId}/chapter/${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}`),
|
||||
]));
|
||||
]),
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -101,22 +97,38 @@ const DownloadQueue: React.FC = () => {
|
||||
>
|
||||
<Card
|
||||
sx={{
|
||||
backgroundColor: snapshot.isDragging ? 'custom.light' : undefined,
|
||||
backgroundColor: snapshot.isDragging
|
||||
? 'custom.light'
|
||||
: undefined,
|
||||
}}
|
||||
>
|
||||
<CardActionArea
|
||||
component={Link}
|
||||
to={{ pathname: `/manga/${item.chapter.mangaId}`, state: { backLink: BACK } }}
|
||||
sx={{ display: 'flex', alignItems: 'center', p: 1 }}
|
||||
to={{
|
||||
pathname: `/manga/${item.chapter.mangaId}`,
|
||||
state: { backLink: BACK },
|
||||
}}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
p: 1,
|
||||
}}
|
||||
>
|
||||
<IconButton sx={{ pointerEvents: 'none' }}>
|
||||
<DragHandle />
|
||||
</IconButton>
|
||||
<Stack sx={{ flex: 1, ml: 1 }} direction="column">
|
||||
<Stack
|
||||
sx={{ flex: 1, ml: 1 }}
|
||||
direction="column"
|
||||
>
|
||||
<Typography variant="h6">
|
||||
{item.manga.title}
|
||||
</Typography>
|
||||
<Typography variant="caption" display="block" gutterBottom>
|
||||
<Typography
|
||||
variant="caption"
|
||||
display="block"
|
||||
gutterBottom
|
||||
>
|
||||
{item.chapter.name}
|
||||
</Typography>
|
||||
</Stack>
|
||||
|
||||
@@ -5,9 +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, {
|
||||
useContext, useEffect, useState, useMemo, useRef,
|
||||
} 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';
|
||||
@@ -30,7 +28,7 @@ const EXTENSIONS = 1;
|
||||
const allLangs: string[] = [];
|
||||
|
||||
interface GroupedExtension {
|
||||
[key: string]: IExtension[]
|
||||
[key: string]: IExtension[];
|
||||
}
|
||||
|
||||
function groupExtensions(extensions: IExtension[]) {
|
||||
@@ -39,7 +37,9 @@ function groupExtensions(extensions: IExtension[]) {
|
||||
extensions.forEach((extension) => {
|
||||
if (sortedExtenions[extension.lang] === undefined) {
|
||||
sortedExtenions[extension.lang] = [];
|
||||
if (extension.lang !== 'all') { allLangs.push(extension.lang); }
|
||||
if (extension.lang !== 'all') {
|
||||
allLangs.push(extension.lang);
|
||||
}
|
||||
}
|
||||
if (extension.installed) {
|
||||
if (extension.hasUpdate) {
|
||||
@@ -67,7 +67,10 @@ 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 [shownLangs, setShownLangs] = useLocalStorage<string[]>(
|
||||
'shownExtensionLangs',
|
||||
extensionDefaultLangs(),
|
||||
);
|
||||
const [showNsfw] = useLocalStorage<boolean>('showNsfw', true);
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
@@ -78,10 +81,7 @@ export default function MangaExtensions() {
|
||||
setAction(
|
||||
<>
|
||||
<AppbarSearch />
|
||||
<IconButton
|
||||
onClick={() => inputRef.current?.click()}
|
||||
size="large"
|
||||
>
|
||||
<IconButton onClick={() => inputRef.current?.click()} size="large">
|
||||
<AddIcon />
|
||||
</IconButton>
|
||||
<LangSelect
|
||||
@@ -93,17 +93,33 @@ export default function MangaExtensions() {
|
||||
);
|
||||
}, [shownLangs]);
|
||||
|
||||
const { data: allExtensions, mutate, loading } = useQuery<IExtension[]>('/api/v1/extension/list');
|
||||
const {
|
||||
data: allExtensions,
|
||||
mutate,
|
||||
loading,
|
||||
} = useQuery<IExtension[]>('/api/v1/extension/list');
|
||||
|
||||
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]);
|
||||
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],
|
||||
);
|
||||
|
||||
const groupedExtensions = useMemo(() => groupExtensions(filteredExtensions)
|
||||
.filter((group) => group[EXTENSIONS].length > 0)
|
||||
.filter((group) => ['installed', 'updates pending', 'all', ...shownLangs].includes(group[LANGUAGE])), [shownLangs, filteredExtensions]);
|
||||
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);
|
||||
|
||||
@@ -119,8 +135,10 @@ export default function MangaExtensions() {
|
||||
}
|
||||
|
||||
makeToast('Installing Extension File....', 'info');
|
||||
client.post('/api/v1/extension/install',
|
||||
formData, { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
client
|
||||
.post('/api/v1/extension/install', formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
})
|
||||
.then(() => {
|
||||
makeToast('Installed extension successfully!', 'success');
|
||||
mutate();
|
||||
@@ -175,7 +193,7 @@ export default function MangaExtensions() {
|
||||
}}
|
||||
totalCount={flatRenderItems.length}
|
||||
itemContent={(index) => {
|
||||
if (typeof (flatRenderItems[index]) === 'string') {
|
||||
if (typeof flatRenderItems[index] === 'string') {
|
||||
const item = flatRenderItems[index] as string;
|
||||
return (
|
||||
<Typography
|
||||
|
||||
@@ -6,9 +6,7 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import { Tab, Tabs } from '@mui/material';
|
||||
import React, {
|
||||
useContext, useEffect, useState,
|
||||
} from 'react';
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import NavbarContext from 'components/context/NavbarContext';
|
||||
import EmptyView from 'components/util/EmptyView';
|
||||
import LoadingPlaceholder from 'components/util/LoadingPlaceholder';
|
||||
@@ -62,7 +60,12 @@ export default function Library() {
|
||||
};
|
||||
|
||||
if (tabsError != null) {
|
||||
return <EmptyView message="Could not load categories" messageExtra={tabsError?.message ?? tabsError} />;
|
||||
return (
|
||||
<EmptyView
|
||||
message="Could not load categories"
|
||||
messageExtra={tabsError?.message ?? tabsError}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
@@ -109,11 +112,13 @@ export default function Library() {
|
||||
</Tabs>
|
||||
{tabs.map((tab) => (
|
||||
<TabPanel key={tab.order} index={tab.order} currentIndex={activeTab.order}>
|
||||
{tab === activeTab && (mangaError
|
||||
? (
|
||||
<EmptyView message="Could not load manga" messageExtra={mangaError?.message ?? mangaError} />
|
||||
)
|
||||
: (
|
||||
{tab === activeTab &&
|
||||
(mangaError ? (
|
||||
<EmptyView
|
||||
message="Could not load manga"
|
||||
messageExtra={mangaError?.message ?? mangaError}
|
||||
/>
|
||||
) : (
|
||||
<LibraryMangaGrid
|
||||
mangas={mangas}
|
||||
lastLibraryUpdate={lastLibraryUpdate}
|
||||
|
||||
@@ -6,9 +6,7 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import { Warning } from '@mui/icons-material';
|
||||
import {
|
||||
CircularProgress, IconButton, Stack, Tooltip,
|
||||
} from '@mui/material';
|
||||
import { CircularProgress, IconButton, Stack, Tooltip } from '@mui/material';
|
||||
import { Box } from '@mui/system';
|
||||
import NavbarContext, { useSetDefaultBackTo } from 'components/context/NavbarContext';
|
||||
import ChapterList from 'components/manga/ChapterList';
|
||||
@@ -18,9 +16,7 @@ import MangaToolbarMenu from 'components/manga/MangaToolbarMenu';
|
||||
import { NavbarToolbar } from 'components/navbar/DefaultNavBar';
|
||||
import EmptyView from 'components/util/EmptyView';
|
||||
import LoadingPlaceholder from 'components/util/LoadingPlaceholder';
|
||||
import React, {
|
||||
useContext, useEffect, useRef,
|
||||
} from 'react';
|
||||
import React, { useContext, useEffect, useRef } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useQuery } from 'util/client';
|
||||
|
||||
@@ -32,12 +28,20 @@ const Manga: React.FC = () => {
|
||||
const autofetchedRef = useRef(false);
|
||||
|
||||
const {
|
||||
data: manga, error, loading, isValidating, mutate,
|
||||
data: manga,
|
||||
error,
|
||||
loading,
|
||||
isValidating,
|
||||
mutate,
|
||||
} = useQuery<IManga>(`/api/v1/manga/${id}/?onlineFetch=false`);
|
||||
|
||||
const [refresh, { loading: refreshing }] = useRefreshManga(id);
|
||||
|
||||
useSetDefaultBackTo(manga?.inLibrary === false && manga.sourceId != null ? `/sources/${manga.sourceId}/popular` : '/library');
|
||||
useSetDefaultBackTo(
|
||||
manga?.inLibrary === false && manga.sourceId != null
|
||||
? `/sources/${manga.sourceId}/popular`
|
||||
: '/library',
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// Automatically fetch manga from source if data is older then 24 hours
|
||||
@@ -45,9 +49,9 @@ const Manga: React.FC = () => {
|
||||
// not update age for some reason (ie. error on source side)
|
||||
if (manga == null) return;
|
||||
if (
|
||||
manga.inLibrary
|
||||
&& (manga.age > AUTOFETCH_AGE || manga.chaptersAge > AUTOFETCH_AGE)
|
||||
&& autofetchedRef.current === false
|
||||
manga.inLibrary &&
|
||||
(manga.age > AUTOFETCH_AGE || manga.chaptersAge > AUTOFETCH_AGE) &&
|
||||
autofetchedRef.current === false
|
||||
) {
|
||||
autofetchedRef.current = true;
|
||||
refresh();
|
||||
@@ -59,29 +63,28 @@ const Manga: React.FC = () => {
|
||||
}, [manga?.title]);
|
||||
|
||||
if (error && !manga) {
|
||||
return (
|
||||
<EmptyView message="Could not load manga" messageExtra={error.message ?? error} />
|
||||
);
|
||||
return <EmptyView message="Could not load manga" messageExtra={error.message ?? error} />;
|
||||
}
|
||||
return (
|
||||
<Box sx={{ display: { md: 'flex' }, overflow: 'hidden' }}>
|
||||
<NavbarToolbar>
|
||||
<Stack direction="row" alignItems="center">
|
||||
{error && !isValidating && !refreshing && (
|
||||
<Tooltip title={(
|
||||
<>
|
||||
Could not fetch manga data
|
||||
<br />
|
||||
{error.message ?? error}
|
||||
</>
|
||||
)}
|
||||
<Tooltip
|
||||
title={
|
||||
<>
|
||||
Could not fetch manga data
|
||||
<br />
|
||||
{error.message ?? error}
|
||||
</>
|
||||
}
|
||||
>
|
||||
<IconButton onClick={() => mutate()}>
|
||||
<Warning color="error" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
{(manga && (refreshing || isValidating)) && (
|
||||
{manga && (refreshing || isValidating) && (
|
||||
<IconButton disabled>
|
||||
<CircularProgress size={16} />
|
||||
</IconButton>
|
||||
|
||||
@@ -6,9 +6,7 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
import React, {
|
||||
useCallback, useContext, useEffect, useState,
|
||||
} from 'react';
|
||||
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
||||
import { useHistory, useParams } from 'react-router-dom';
|
||||
import HorizontalPager from 'components/reader/pager/HorizontalPager';
|
||||
import PageNumber from 'components/reader/PageNumber';
|
||||
@@ -53,7 +51,7 @@ const getReaderComponent = (readerType: ReaderType) => {
|
||||
}
|
||||
};
|
||||
|
||||
const range = (n:number) => Array.from({ length: n }, (value, key) => key);
|
||||
const range = (n: number) => Array.from({ length: n }, (value, key) => key);
|
||||
const initialChapter = () => ({
|
||||
pageCount: -1,
|
||||
index: -1,
|
||||
@@ -67,22 +65,26 @@ export default function Reader() {
|
||||
|
||||
const [serverAddress] = useLocalStorage<String>('serverBaseURL', '');
|
||||
|
||||
const { chapterIndex, mangaId } = useParams<{ chapterIndex: string, mangaId: string }>();
|
||||
const [manga, setManga] = useState<IMangaCard | IManga>({ id: +mangaId, title: '', thumbnailUrl: '' });
|
||||
const { chapterIndex, mangaId } = useParams<{ chapterIndex: string; mangaId: string }>();
|
||||
const [manga, setManga] = useState<IMangaCard | IManga>({
|
||||
id: +mangaId,
|
||||
title: '',
|
||||
thumbnailUrl: '',
|
||||
});
|
||||
const [chapter, setChapter] = useState<IChapter | IPartialChapter>(initialChapter());
|
||||
const [curPage, setCurPage] = useState<number>(0);
|
||||
const { setOverride, setTitle } = useContext(NavbarContext);
|
||||
|
||||
const {
|
||||
settings: defaultSettings,
|
||||
loading: areDefaultSettingsLoading,
|
||||
} = useDefaultReaderSettings();
|
||||
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 });
|
||||
requestUpdateMangaMetadata(manga, [[key, value]]).catch(() => makeToast('Failed to save the reader settings to the server', 'warning'));
|
||||
requestUpdateMangaMetadata(manga, [[key, value]]).catch(() =>
|
||||
makeToast('Failed to save the reader settings to the server', 'warning'),
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -95,27 +97,27 @@ export default function Reader() {
|
||||
|
||||
useEffect(() => {
|
||||
if (!areDefaultSettingsLoading && !isMangaLoading) {
|
||||
checkAndHandleMissingStoredReaderSettings(manga, 'manga', defaultSettings).catch(() => {});
|
||||
checkAndHandleMissingStoredReaderSettings(manga, 'manga', defaultSettings).catch(
|
||||
() => {},
|
||||
);
|
||||
setSettings(getReaderSettingsFor(manga, defaultSettings));
|
||||
}
|
||||
}, [areDefaultSettingsLoading, isMangaLoading]);
|
||||
|
||||
useEffect(() => {
|
||||
// set the custom navbar
|
||||
setOverride(
|
||||
{
|
||||
status: true,
|
||||
value: (
|
||||
<ReaderNavBar
|
||||
settings={settings}
|
||||
setSettingValue={setSettingValue}
|
||||
manga={manga}
|
||||
chapter={chapter as IChapter}
|
||||
curPage={curPage}
|
||||
/>
|
||||
),
|
||||
},
|
||||
);
|
||||
setOverride({
|
||||
status: true,
|
||||
value: (
|
||||
<ReaderNavBar
|
||||
settings={settings}
|
||||
setSettingValue={setSettingValue}
|
||||
manga={manga}
|
||||
chapter={chapter as IChapter}
|
||||
curPage={curPage}
|
||||
/>
|
||||
),
|
||||
});
|
||||
|
||||
// clean up for when we leave the reader
|
||||
return () => setOverride({ status: false, value: <div /> });
|
||||
@@ -123,7 +125,8 @@ export default function Reader() {
|
||||
|
||||
useEffect(() => {
|
||||
setIsMangaLoading(true);
|
||||
client.get(`/api/v1/manga/${mangaId}/`)
|
||||
client
|
||||
.get(`/api/v1/manga/${mangaId}/`)
|
||||
.then((response) => response.data)
|
||||
.then((data: IManga) => {
|
||||
setManga(data);
|
||||
@@ -133,9 +136,10 @@ export default function Reader() {
|
||||
|
||||
useEffect(() => {
|
||||
setChapter(initialChapter);
|
||||
client.get(`/api/v1/manga/${mangaId}/chapter/${chapterIndex}`)
|
||||
client
|
||||
.get(`/api/v1/manga/${mangaId}/chapter/${chapterIndex}`)
|
||||
.then((response) => response.data)
|
||||
.then((data:IChapter) => {
|
||||
.then((data: IChapter) => {
|
||||
setChapter(data);
|
||||
|
||||
if (data.lastPageRead === data.pageCount - 1) {
|
||||
@@ -166,22 +170,32 @@ export default function Reader() {
|
||||
formData.append('read', 'true');
|
||||
client.patch(`/api/v1/manga/${manga.id}/chapter/${chapter.index}`, formData);
|
||||
|
||||
history.replace({ pathname: `/manga/${manga.id}/chapter/${chapter.index + 1}`, state: history.location.state });
|
||||
history.replace({
|
||||
pathname: `/manga/${manga.id}/chapter/${chapter.index + 1}`,
|
||||
state: history.location.state,
|
||||
});
|
||||
}
|
||||
}, [chapter.index, chapter.chapterCount, chapter.pageCount, manga.id]);
|
||||
|
||||
const prevChapter = useCallback(() => {
|
||||
if (chapter.index > 1) {
|
||||
history.replace({ pathname: `/manga/${manga.id}/chapter/${chapter.index - 1}`, state: history.location.state });
|
||||
history.replace({
|
||||
pathname: `/manga/${manga.id}/chapter/${chapter.index - 1}`,
|
||||
state: history.location.state,
|
||||
});
|
||||
}
|
||||
}, [chapter.index, manga.id]);
|
||||
|
||||
// return spinner while chpater data is loading
|
||||
if (chapter.pageCount === -1) {
|
||||
return (
|
||||
<Box sx={{
|
||||
height: '100vh', width: '100vw', display: 'grid', placeItems: 'center',
|
||||
}}
|
||||
<Box
|
||||
sx={{
|
||||
height: '100vh',
|
||||
width: '100vw',
|
||||
display: 'grid',
|
||||
placeItems: 'center',
|
||||
}}
|
||||
>
|
||||
<CircularProgress thickness={5} />
|
||||
</Box>
|
||||
@@ -196,15 +210,11 @@ export default function Reader() {
|
||||
const ReaderComponent = getReaderComponent(settings.readerType);
|
||||
|
||||
// last page, also probably read = true, we will load the first page.
|
||||
const initialPage = (chapter.lastPageRead === chapter.pageCount - 1) ? 0 : chapter.lastPageRead;
|
||||
const initialPage = chapter.lastPageRead === chapter.pageCount - 1 ? 0 : chapter.lastPageRead;
|
||||
|
||||
return (
|
||||
<Box sx={{ width: settings.staticNav ? 'calc(100vw - 300px)' : '100vw' }}>
|
||||
<PageNumber
|
||||
settings={settings}
|
||||
curPage={curPage}
|
||||
pageCount={chapter.pageCount}
|
||||
/>
|
||||
<PageNumber settings={settings} curPage={curPage} pageCount={chapter.pageCount} />
|
||||
<ReaderComponent
|
||||
pages={pages}
|
||||
pageCount={chapter.pageCount}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
/*
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* 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/.
|
||||
*/
|
||||
* Copyright (C) Contributors to the Suwayomi project
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* 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 { Card, CardActionArea, Typography } from '@mui/material';
|
||||
import NavbarContext from 'components/context/NavbarContext';
|
||||
@@ -17,7 +17,10 @@ import { Link } from 'react-router-dom';
|
||||
import { StringParam, useQueryParam } from 'use-query-params';
|
||||
import client from 'util/client';
|
||||
import {
|
||||
langCodeToName, langSortCmp, sourceDefualtLangs, sourceForcedDefaultLangs,
|
||||
langCodeToName,
|
||||
langSortCmp,
|
||||
sourceDefualtLangs,
|
||||
sourceForcedDefaultLangs,
|
||||
} from 'util/language';
|
||||
import useLocalStorage from 'util/useLocalStorage';
|
||||
|
||||
@@ -25,7 +28,9 @@ function sourceToLangList(sources: ISource[]) {
|
||||
const result: string[] = [];
|
||||
|
||||
sources.forEach((source) => {
|
||||
if (result.indexOf(source.lang) === -1) { result.push(source.lang); }
|
||||
if (result.indexOf(source.lang) === -1) {
|
||||
result.push(source.lang);
|
||||
}
|
||||
});
|
||||
|
||||
result.sort(langSortCmp);
|
||||
@@ -38,7 +43,10 @@ const SearchAll: React.FC = () => {
|
||||
const [triggerUpdate, setTriggerUpdate] = useState<number>(2);
|
||||
const [mangas, setMangas] = useState<any>({});
|
||||
|
||||
const [shownLangs, setShownLangs] = useLocalStorage<string[]>('shownSourceLangs', sourceDefualtLangs());
|
||||
const [shownLangs, setShownLangs] = useLocalStorage<string[]>(
|
||||
'shownSourceLangs',
|
||||
sourceDefualtLangs(),
|
||||
);
|
||||
const [showNsfw] = useLocalStorage<boolean>('showNsfw', true);
|
||||
|
||||
const [sources, setSources] = useState<ISource[]>([]);
|
||||
@@ -56,35 +64,46 @@ const SearchAll: React.FC = () => {
|
||||
setAction(
|
||||
<>
|
||||
<AppbarSearch />
|
||||
</>
|
||||
,
|
||||
</>,
|
||||
);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
client.get('/api/v1/source/list')
|
||||
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);
|
||||
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 data = await response.data;
|
||||
const tmp = mangas;
|
||||
tmp[ele.id] = data.mangaList;
|
||||
setMangas(tmp);
|
||||
const tmp2 = fetched;
|
||||
tmp2[ele.id] = true;
|
||||
setFetched(tmp2);
|
||||
setResetUI(1);
|
||||
}));
|
||||
elem.map((ele) =>
|
||||
limit.add(async () => {
|
||||
const response = await client.get(
|
||||
`/api/v1/source/${ele.id}/search?searchTerm=${query || ''}&pageNum=1`,
|
||||
);
|
||||
const data = await response.data;
|
||||
const tmp = mangas;
|
||||
tmp[ele.id] = data.mangaList;
|
||||
setMangas(tmp);
|
||||
const tmp2 = fetched;
|
||||
tmp2[ele.id] = true;
|
||||
setFetched(tmp2);
|
||||
setResetUI(1);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
@@ -98,7 +117,11 @@ const SearchAll: React.FC = () => {
|
||||
setFetched({});
|
||||
setMangas({});
|
||||
// eslint-disable-next-line max-len
|
||||
doIT(sources.filter(({ lang }) => shownLangs.indexOf(lang) !== -1).filter((source) => showNsfw || !source.isNsfw));
|
||||
doIT(
|
||||
sources
|
||||
.filter(({ lang }) => shownLangs.indexOf(lang) !== -1)
|
||||
.filter((source) => showNsfw || !source.isNsfw),
|
||||
);
|
||||
}, [triggerUpdate]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -137,9 +160,7 @@ const SearchAll: React.FC = () => {
|
||||
setTitle('Sources');
|
||||
setAction(
|
||||
<>
|
||||
<AppbarSearch
|
||||
autoOpen
|
||||
/>
|
||||
<AppbarSearch autoOpen />
|
||||
<LangSelect
|
||||
shownLangs={shownLangs}
|
||||
setShownLangs={setShownLangs}
|
||||
@@ -154,20 +175,33 @@ const SearchAll: React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
{/* eslint-disable-next-line max-len */}
|
||||
{sources.filter(({ lang }) => shownLangs.indexOf(lang) !== -1).filter((source) => showNsfw || !source.isNsfw).sort((a, b) => {
|
||||
const af = fetched[a.id];
|
||||
const bf = fetched[b.id];
|
||||
if (af && !bf) { return -1; }
|
||||
if (!af && bf) { return 1; }
|
||||
if (!af && !bf) { return 0; }
|
||||
{sources
|
||||
.filter(({ lang }) => shownLangs.indexOf(lang) !== -1)
|
||||
.filter((source) => showNsfw || !source.isNsfw)
|
||||
.sort((a, b) => {
|
||||
const af = fetched[a.id];
|
||||
const bf = fetched[b.id];
|
||||
if (af && !bf) {
|
||||
return -1;
|
||||
}
|
||||
if (!af && bf) {
|
||||
return 1;
|
||||
}
|
||||
if (!af && !bf) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const al = mangas[a.id].length === 0;
|
||||
const bl = mangas[b.id].length === 0;
|
||||
if (al && !bl) { return 1; }
|
||||
if (bl && !al) { return -1; }
|
||||
return 0;
|
||||
}).map(({ lang, id, displayName }) => (
|
||||
(
|
||||
const al = mangas[a.id].length === 0;
|
||||
const bl = mangas[b.id].length === 0;
|
||||
if (al && !bl) {
|
||||
return 1;
|
||||
}
|
||||
if (bl && !al) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
})
|
||||
.map(({ lang, id, displayName }) => (
|
||||
<>
|
||||
<Card sx={{ margin: '10px' }}>
|
||||
<CardActionArea
|
||||
@@ -175,9 +209,7 @@ const SearchAll: React.FC = () => {
|
||||
to={`/sources/${id}/popular/?R&query=${query}`}
|
||||
sx={{ p: 3 }}
|
||||
>
|
||||
<Typography variant="h5">
|
||||
{displayName}
|
||||
</Typography>
|
||||
<Typography variant="h5">{displayName}</Typography>
|
||||
<Typography variant="caption">
|
||||
{langCodeToName(lang)}
|
||||
</Typography>
|
||||
@@ -195,13 +227,11 @@ const SearchAll: React.FC = () => {
|
||||
inLibraryIndicator
|
||||
/>
|
||||
</>
|
||||
)
|
||||
))}
|
||||
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (<></>);
|
||||
return <></>;
|
||||
};
|
||||
|
||||
export default SearchAll;
|
||||
|
||||
@@ -38,7 +38,10 @@ import ListItemLink from 'components/util/ListItemLink';
|
||||
|
||||
export default function Settings() {
|
||||
const { setTitle, setAction } = useContext(NavbarContext);
|
||||
useEffect(() => { setTitle('Settings'); setAction(<></>); }, []);
|
||||
useEffect(() => {
|
||||
setTitle('Settings');
|
||||
setAction(<></>);
|
||||
}, []);
|
||||
|
||||
const { darkTheme, setDarkTheme } = useContext(DarkTheme);
|
||||
const [serverAddress, setServerAddress] = useLocalStorage<String>('serverBaseURL', '');
|
||||
@@ -193,9 +196,7 @@ export default function Settings() {
|
||||
|
||||
<Dialog open={dialogOpen} onClose={handleDialogCancel}>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
Enter Server Address
|
||||
</DialogContentText>
|
||||
<DialogContentText>Enter Server Address</DialogContentText>
|
||||
<TextField
|
||||
autoFocus
|
||||
margin="dense"
|
||||
@@ -219,9 +220,7 @@ export default function Settings() {
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={dialogOpenItemWidth} onClose={handleDialogCancelItemWidth}>
|
||||
<DialogTitle>
|
||||
Manga Item width
|
||||
</DialogTitle>
|
||||
<DialogTitle>Manga Item width</DialogTitle>
|
||||
<DialogContent
|
||||
sx={{
|
||||
width: '98%',
|
||||
|
||||
@@ -9,7 +9,10 @@ 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 {
|
||||
SwitchPreferenceCompat,
|
||||
CheckBoxPreference,
|
||||
} from 'components/sourceConfiguration/TwoStatePreference';
|
||||
import ListPreference from 'components/sourceConfiguration/ListPreference';
|
||||
import EditTextPreference from 'components/sourceConfiguration/EditTextPreference';
|
||||
import MultiSelectListPreference from 'components/sourceConfiguration/MultiSelectListPreference';
|
||||
@@ -36,10 +39,15 @@ function getPrefComponent(type: string) {
|
||||
export default function SourceConfigure() {
|
||||
const { setTitle, setAction } = useContext(NavbarContext);
|
||||
|
||||
useEffect(() => { setTitle('Source Configuration'); setAction(<></>); }, []);
|
||||
useEffect(() => {
|
||||
setTitle('Source Configuration');
|
||||
setAction(<></>);
|
||||
}, []);
|
||||
|
||||
const { sourceId } = useParams<{ sourceId: string }>();
|
||||
const { data: sourcePreferences = [], mutate } = useQuery<SourcePreferences[]>(`/api/v1/source/${sourceId}/preferences`);
|
||||
const { data: sourcePreferences = [], mutate } = useQuery<SourcePreferences[]>(
|
||||
`/api/v1/source/${sourceId}/preferences`,
|
||||
);
|
||||
|
||||
const convertToString = (position: number, value: any): string => {
|
||||
switch (sourcePreferences[position].props.defaultValueType) {
|
||||
@@ -50,28 +58,27 @@ 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());
|
||||
}
|
||||
);
|
||||
const updateValue = (position: number) => (value: any) => {
|
||||
client
|
||||
.post(
|
||||
`/api/v1/source/${sourceId}/preferences`,
|
||||
JSON.stringify({ position, value: convertToString(position, value) }),
|
||||
)
|
||||
.then(() => mutate());
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<List sx={{ padding: 0 }}>
|
||||
{sourcePreferences.map(
|
||||
(it, index) => {
|
||||
const props = cloneObject(it.props);
|
||||
props.updateValue = updateValue(index);
|
||||
props.key = index;
|
||||
{sourcePreferences.map((it, index) => {
|
||||
const props = cloneObject(it.props);
|
||||
props.updateValue = updateValue(index);
|
||||
props.key = index;
|
||||
|
||||
// TypeScript is dumb in detecting extra props
|
||||
// @ts-ignore
|
||||
return React.createElement(getPrefComponent(it.type), props);
|
||||
},
|
||||
)}
|
||||
// TypeScript is dumb in detecting extra props
|
||||
// @ts-ignore
|
||||
return React.createElement(getPrefComponent(it.type), props);
|
||||
})}
|
||||
</List>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -19,9 +19,9 @@ import SourceGridLayout from 'components/source/GridLayouts';
|
||||
import { useLibraryOptionsContext } from 'components/context/LibraryOptionsContext';
|
||||
|
||||
interface IPos {
|
||||
position: number
|
||||
state: any
|
||||
group?: number
|
||||
position: number;
|
||||
state: any;
|
||||
group?: number;
|
||||
}
|
||||
|
||||
export default function SourceMangas(props: { popular: boolean }) {
|
||||
@@ -48,7 +48,8 @@ export default function SourceMangas(props: { popular: boolean }) {
|
||||
const { options } = useLibraryOptionsContext();
|
||||
|
||||
function makeFilters() {
|
||||
client.get(`/api/v1/source/${sourceId}/filters`)
|
||||
client
|
||||
.get(`/api/v1/source/${sourceId}/filters`)
|
||||
.then((response) => response.data)
|
||||
.then((data: ISourceFilters[]) => {
|
||||
SetData(data);
|
||||
@@ -60,7 +61,8 @@ export default function SourceMangas(props: { popular: boolean }) {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
client.get(`/api/v1/source/${sourceId}`)
|
||||
client
|
||||
.get(`/api/v1/source/${sourceId}`)
|
||||
.then((response) => response.data)
|
||||
.then((data: ISource) => {
|
||||
setTitle(data.displayName);
|
||||
@@ -79,20 +81,25 @@ export default function SourceMangas(props: { popular: boolean }) {
|
||||
if (update.length > 0) {
|
||||
const rep = update;
|
||||
setUpdate([]);
|
||||
client.post(`/api/v1/source/${sourceId}/filters`,
|
||||
rep.map((e: IPos) => {
|
||||
const { position, state, group }: IPos = e;
|
||||
return group === undefined ? {
|
||||
position,
|
||||
state,
|
||||
} : {
|
||||
position: group,
|
||||
state: JSON.stringify({
|
||||
position,
|
||||
state,
|
||||
}),
|
||||
};
|
||||
}))
|
||||
client
|
||||
.post(
|
||||
`/api/v1/source/${sourceId}/filters`,
|
||||
rep.map((e: IPos) => {
|
||||
const { position, state, group }: IPos = e;
|
||||
return group === undefined
|
||||
? {
|
||||
position,
|
||||
state,
|
||||
}
|
||||
: {
|
||||
position: group,
|
||||
state: JSON.stringify({
|
||||
position,
|
||||
state,
|
||||
}),
|
||||
};
|
||||
}),
|
||||
)
|
||||
.then(() => {
|
||||
setTriggerUpdate(0);
|
||||
makeFilters();
|
||||
@@ -101,7 +108,9 @@ export default function SourceMangas(props: { popular: boolean }) {
|
||||
setFetched(false);
|
||||
setMangas([]);
|
||||
setLastPageNum(0);
|
||||
if (Noreset === undefined && Search) { setNoreset(null); }
|
||||
if (Noreset === undefined && Search) {
|
||||
setNoreset(null);
|
||||
}
|
||||
}
|
||||
}, [triggerUpdate]);
|
||||
|
||||
@@ -111,14 +120,13 @@ export default function SourceMangas(props: { popular: boolean }) {
|
||||
setNoreset(undefined);
|
||||
setReset(1);
|
||||
} else if (Noreset === undefined) {
|
||||
client.get(`/api/v1/source/${sourceId}/filters?reset=true`)
|
||||
.then(() => {
|
||||
makeFilters();
|
||||
setSearch(false);
|
||||
if (reset === 1) {
|
||||
setTriggerUpdate(0);
|
||||
}
|
||||
});
|
||||
client.get(`/api/v1/source/${sourceId}/filters?reset=true`).then(() => {
|
||||
makeFilters();
|
||||
setSearch(false);
|
||||
if (reset === 1) {
|
||||
setTriggerUpdate(0);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
makeFilters();
|
||||
@@ -140,8 +148,7 @@ export default function SourceMangas(props: { popular: boolean }) {
|
||||
<SettingsIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
</>
|
||||
,
|
||||
</>,
|
||||
);
|
||||
|
||||
return () => {
|
||||
@@ -152,8 +159,12 @@ export default function SourceMangas(props: { popular: boolean }) {
|
||||
useEffect(() => {
|
||||
if (query) {
|
||||
setSearch(true);
|
||||
} else { setSearch(false); }
|
||||
if (Noreset === undefined) { setInit(null); }
|
||||
} else {
|
||||
setSearch(false);
|
||||
}
|
||||
if (Noreset === undefined) {
|
||||
setInit(null);
|
||||
}
|
||||
}, [query]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -163,16 +174,27 @@ export default function SourceMangas(props: { popular: boolean }) {
|
||||
}, 1000);
|
||||
return () => clearTimeout(delayDebounceFn);
|
||||
}
|
||||
if (Search !== undefined) { setInit(null); }
|
||||
if (Search !== undefined) {
|
||||
setInit(null);
|
||||
}
|
||||
return () => {};
|
||||
}, [Search, query]);
|
||||
|
||||
useEffect(() => {
|
||||
if (lastPageNum !== 0) {
|
||||
const sourceType = props.popular ? 'popular' : 'latest';
|
||||
client.get(`/api/v1/source/${sourceId}/${query !== undefined || Search || Noreset === null ? 'search' : sourceType}${query !== undefined || Search || Noreset === null ? `?searchTerm=${query || ''}&pageNum=${lastPageNum}` : `/${lastPageNum}`}`)
|
||||
client
|
||||
.get(
|
||||
`/api/v1/source/${sourceId}/${
|
||||
query !== undefined || Search || Noreset === null ? 'search' : sourceType
|
||||
}${
|
||||
query !== undefined || Search || Noreset === null
|
||||
? `?searchTerm=${query || ''}&pageNum=${lastPageNum}`
|
||||
: `/${lastPageNum}`
|
||||
}`,
|
||||
)
|
||||
.then((response) => response.data)
|
||||
.then((data: { mangaList: IManga[], hasNextPage: boolean }) => {
|
||||
.then((data: { mangaList: IManga[]; hasNextPage: boolean }) => {
|
||||
setMangas([
|
||||
...mangas,
|
||||
...data.mangaList.map((it) => ({
|
||||
@@ -180,11 +202,14 @@ export default function SourceMangas(props: { popular: boolean }) {
|
||||
thumbnailUrl: it.thumbnailUrl,
|
||||
id: it.id,
|
||||
inLibrary: it.inLibrary,
|
||||
}))]);
|
||||
})),
|
||||
]);
|
||||
setHasNextPage(data.hasNextPage);
|
||||
setFetched(true);
|
||||
});
|
||||
} else { setLastPageNum(1); }
|
||||
} else {
|
||||
setLastPageNum(1);
|
||||
}
|
||||
}, [lastPageNum]);
|
||||
|
||||
let message;
|
||||
@@ -196,7 +221,9 @@ export default function SourceMangas(props: { popular: boolean }) {
|
||||
messageExtra = (
|
||||
<>
|
||||
<span>Check out </span>
|
||||
<a href="https://github.com/Suwayomi/Tachidesk-Server/wiki/Local-Source">Local source guide</a>
|
||||
<a href="https://github.com/Suwayomi/Tachidesk-Server/wiki/Local-Source">
|
||||
Local source guide
|
||||
</a>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,10 @@ import LangSelect from 'components/navbar/action/LangSelect';
|
||||
import SourceCard from 'components/SourceCard';
|
||||
import NavbarContext from 'components/context/NavbarContext';
|
||||
import {
|
||||
sourceDefualtLangs, sourceForcedDefaultLangs, langCodeToName, langSortCmp,
|
||||
sourceDefualtLangs,
|
||||
sourceForcedDefaultLangs,
|
||||
langCodeToName,
|
||||
langSortCmp,
|
||||
} from 'util/language';
|
||||
import useLocalStorage from 'util/useLocalStorage';
|
||||
import LoadingPlaceholder from 'components/util/LoadingPlaceholder';
|
||||
@@ -23,7 +26,9 @@ function sourceToLangList(sources: ISource[]) {
|
||||
const result: string[] = [];
|
||||
|
||||
sources.forEach((source) => {
|
||||
if (result.indexOf(source.lang) === -1) { result.push(source.lang); }
|
||||
if (result.indexOf(source.lang) === -1) {
|
||||
result.push(source.lang);
|
||||
}
|
||||
});
|
||||
|
||||
result.sort(langSortCmp);
|
||||
@@ -33,7 +38,9 @@ function sourceToLangList(sources: ISource[]) {
|
||||
function groupByLang(sources: ISource[]) {
|
||||
const result = {} as any;
|
||||
sources.forEach((source) => {
|
||||
if (result[source.lang] === undefined) { result[source.lang] = [] as ISource[]; }
|
||||
if (result[source.lang] === undefined) {
|
||||
result[source.lang] = [] as ISource[];
|
||||
}
|
||||
result[source.lang].push(source);
|
||||
});
|
||||
|
||||
@@ -43,7 +50,10 @@ function groupByLang(sources: ISource[]) {
|
||||
export default function Sources() {
|
||||
const { setTitle, setAction } = useContext(NavbarContext);
|
||||
|
||||
const [shownLangs, setShownLangs] = useLocalStorage<string[]>('shownSourceLangs', sourceDefualtLangs());
|
||||
const [shownLangs, setShownLangs] = useLocalStorage<string[]>(
|
||||
'shownSourceLangs',
|
||||
sourceDefualtLangs(),
|
||||
);
|
||||
const [showNsfw] = useLocalStorage<boolean>('showNsfw', true);
|
||||
|
||||
const { data: sources, loading } = useQuery<ISource[]>('/api/v1/source/list');
|
||||
@@ -70,10 +80,7 @@ export default function Sources() {
|
||||
setTitle('Sources');
|
||||
setAction(
|
||||
<>
|
||||
<IconButton
|
||||
onClick={() => history.push('/sources/all/search/')}
|
||||
size="large"
|
||||
>
|
||||
<IconButton onClick={() => history.push('/sources/all/search/')} size="large">
|
||||
<TravelExploreIcon />
|
||||
</IconButton>
|
||||
<LangSelect
|
||||
@@ -89,27 +96,29 @@ export default function Sources() {
|
||||
if (loading) return <LoadingPlaceholder />;
|
||||
|
||||
if (sources?.length === 0) {
|
||||
return (<h3>No sources found. Install Some Extensions first.</h3>);
|
||||
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]) => (
|
||||
shownLangs.indexOf(lang) !== -1 && (
|
||||
<React.Fragment key={lang}>
|
||||
<h1 key={lang} style={{ marginLeft: 25 }}>{langCodeToName(lang)}</h1>
|
||||
{(list as ISource[])
|
||||
.filter((source) => showNsfw || !source.isNsfw)
|
||||
.map((source) => (
|
||||
<SourceCard
|
||||
key={source.id}
|
||||
source={source}
|
||||
/>
|
||||
))}
|
||||
</React.Fragment>
|
||||
)
|
||||
))}
|
||||
{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>
|
||||
{(list as ISource[])
|
||||
.filter((source) => showNsfw || !source.isNsfw)
|
||||
.map((source) => (
|
||||
<SourceCard key={source.id} source={source} />
|
||||
))}
|
||||
</React.Fragment>
|
||||
),
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,9 +17,7 @@ import NavbarContext from 'components/context/NavbarContext';
|
||||
import DownloadStateIndicator from 'components/molecules/DownloadStateIndicator';
|
||||
import EmptyView from 'components/util/EmptyView';
|
||||
import LoadingPlaceholder from 'components/util/LoadingPlaceholder';
|
||||
import React, {
|
||||
useContext, useEffect, useRef, useState,
|
||||
} from 'react';
|
||||
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';
|
||||
@@ -30,10 +28,12 @@ function epochToDate(epoch: number) {
|
||||
return date;
|
||||
}
|
||||
|
||||
function isTheSameDay(first:Date, second:Date) {
|
||||
return first.getDate() === second.getDate()
|
||||
&& first.getMonth() === second.getMonth()
|
||||
&& first.getFullYear() === second.getFullYear();
|
||||
function isTheSameDay(first: Date, second: Date) {
|
||||
return (
|
||||
first.getDate() === second.getDate() &&
|
||||
first.getMonth() === second.getMonth() &&
|
||||
first.getFullYear() === second.getFullYear()
|
||||
);
|
||||
}
|
||||
|
||||
function getDateString(date: Date) {
|
||||
@@ -46,8 +46,9 @@ function getDateString(date: Date) {
|
||||
return date.toLocaleDateString();
|
||||
}
|
||||
|
||||
function groupByDate(updates: IMangaChapter[]):
|
||||
[string, { item: IMangaChapter, globalIdx: number }[] ][] {
|
||||
function groupByDate(
|
||||
updates: IMangaChapter[],
|
||||
): [string, { item: IMangaChapter; globalIdx: number }[]][] {
|
||||
if (updates.length === 0) return [];
|
||||
|
||||
const groups = {};
|
||||
@@ -63,7 +64,10 @@ function groupByDate(updates: IMangaChapter[]):
|
||||
return Object.keys(groups).map((key) => [key, groups[key]]);
|
||||
}
|
||||
|
||||
const baseWebsocketUrl = JSON.parse(window.localStorage.getItem('serverBaseURL')!).replace('http', 'ws');
|
||||
const baseWebsocketUrl = JSON.parse(window.localStorage.getItem('serverBaseURL')!).replace(
|
||||
'http',
|
||||
'ws',
|
||||
);
|
||||
const initialQueue = {
|
||||
status: 'Stopped',
|
||||
queue: [],
|
||||
@@ -104,13 +108,11 @@ const Updates: React.FC = () => {
|
||||
|
||||
useEffect(() => {
|
||||
if (hasNextPage) {
|
||||
client.get(`/api/v1/update/recentChapters/${lastPageNum}`)
|
||||
client
|
||||
.get(`/api/v1/update/recentChapters/${lastPageNum}`)
|
||||
.then((response) => response.data)
|
||||
.then(({ hasNextPage: fetchedHasNextPage, page }: PaginatedList<IMangaChapter>) => {
|
||||
setUpdateEntries([
|
||||
...updateEntries,
|
||||
...page,
|
||||
]);
|
||||
setUpdateEntries([...updateEntries, ...page]);
|
||||
setHasNextPage(fetchedHasNextPage);
|
||||
setFetched(true);
|
||||
});
|
||||
@@ -122,7 +124,7 @@ const Updates: React.FC = () => {
|
||||
const scrollHandler = () => {
|
||||
if (lastEntry.current) {
|
||||
const rect = lastEntry.current.getBoundingClientRect();
|
||||
if (((rect.y + rect.height) / window.innerHeight < 2) && hasNextPage) {
|
||||
if ((rect.y + rect.height) / window.innerHeight < 2 && hasNextPage) {
|
||||
setLastPageNum(lastPageNum + 1);
|
||||
}
|
||||
}
|
||||
@@ -134,8 +136,12 @@ const Updates: React.FC = () => {
|
||||
};
|
||||
}, [hasNextPage, updateEntries]);
|
||||
|
||||
if (!fetched) { return <LoadingPlaceholder />; }
|
||||
if (fetched && updateEntries.length === 0) { return <EmptyView message="You don't have any updates yet." />; }
|
||||
if (!fetched) {
|
||||
return <LoadingPlaceholder />;
|
||||
}
|
||||
if (fetched && updateEntries.length === 0) {
|
||||
return <EmptyView message="You don't have any updates yet." />;
|
||||
}
|
||||
|
||||
const downloadForChapter = (chapter: IChapter) => {
|
||||
const { index, mangaId } = chapter;
|
||||
@@ -170,7 +176,10 @@ const Updates: React.FC = () => {
|
||||
>
|
||||
<CardActionArea
|
||||
component={Link}
|
||||
to={{ pathname: `/manga/${chapter.mangaId}/chapter/${chapter.index}`, state: history.location.state }}
|
||||
to={{
|
||||
pathname: `/manga/${chapter.mangaId}/chapter/${chapter.index}`,
|
||||
state: history.location.state,
|
||||
}}
|
||||
>
|
||||
<CardContent
|
||||
sx={{
|
||||
@@ -196,7 +205,11 @@ const Updates: React.FC = () => {
|
||||
<Typography variant="h5" component="h2">
|
||||
{manga.title}
|
||||
</Typography>
|
||||
<Typography variant="caption" display="block" gutterBottom>
|
||||
<Typography
|
||||
variant="caption"
|
||||
display="block"
|
||||
gutterBottom
|
||||
>
|
||||
{chapter.name}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
@@ -10,7 +10,10 @@ import { useQuery } from 'util/client';
|
||||
export default function About() {
|
||||
const { setTitle, setAction } = useContext(NavbarContext);
|
||||
|
||||
useEffect(() => { setTitle('About'); setAction(<></>); }, []);
|
||||
useEffect(() => {
|
||||
setTitle('About');
|
||||
setAction(<></>);
|
||||
}, []);
|
||||
|
||||
const { data: about } = useQuery<IAbout>('/api/v1/settings/about');
|
||||
|
||||
|
||||
@@ -17,7 +17,10 @@ import NavbarContext from 'components/context/NavbarContext';
|
||||
|
||||
export default function Backup() {
|
||||
const { setTitle, setAction } = useContext(NavbarContext);
|
||||
useEffect(() => { setTitle('Backup'); setAction(<></>); }, []);
|
||||
useEffect(() => {
|
||||
setTitle('Backup');
|
||||
setAction(<></>);
|
||||
}, []);
|
||||
|
||||
const { baseURL } = client.defaults;
|
||||
|
||||
@@ -27,8 +30,10 @@ export default function Backup() {
|
||||
formData.append('backup.proto.gz', file);
|
||||
|
||||
makeToast('Restoring backup....', 'info');
|
||||
client.post('/api/v1/backup/import/file',
|
||||
formData, { headers: { 'Content-Type': 'multipart/form-data' } })
|
||||
client
|
||||
.post('/api/v1/backup/import/file', formData, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
})
|
||||
.then(() => makeToast('Backup restore finished!', 'success'))
|
||||
.catch(() => makeToast('Backup restore failed!', 'error'));
|
||||
} else if (file.name.toLowerCase().endsWith('json')) {
|
||||
@@ -81,12 +86,7 @@ export default function Backup() {
|
||||
/>
|
||||
</ListItem>
|
||||
</List>
|
||||
<input
|
||||
type="file"
|
||||
id="backup-file"
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
<input type="file" id="backup-file" style={{ display: 'none' }} />
|
||||
</>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,18 +7,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, {
|
||||
useMemo, useState, useContext, useEffect,
|
||||
} from 'react';
|
||||
import React, { useMemo, useState, useContext, useEffect } from 'react';
|
||||
import { List, ListItem, ListItemText, ListItemIcon, IconButton } from '@mui/material';
|
||||
import {
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
ListItemIcon,
|
||||
IconButton,
|
||||
} from '@mui/material';
|
||||
import {
|
||||
DragDropContext, Droppable, Draggable, DropResult, DraggingStyle, NotDraggingStyle,
|
||||
DragDropContext,
|
||||
Droppable,
|
||||
Draggable,
|
||||
DropResult,
|
||||
DraggingStyle,
|
||||
NotDraggingStyle,
|
||||
} from 'react-beautiful-dnd';
|
||||
import DragHandleIcon from '@mui/icons-material/DragHandle';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
@@ -37,8 +34,11 @@ import FormControlLabel from '@mui/material/FormControlLabel';
|
||||
import NavbarContext from 'components/context/NavbarContext';
|
||||
import client, { useQuery } from 'util/client';
|
||||
|
||||
const getItemStyle = (isDragging: boolean,
|
||||
draggableStyle: DraggingStyle | NotDraggingStyle | undefined, palette: Palette) => ({
|
||||
const getItemStyle = (
|
||||
isDragging: boolean,
|
||||
draggableStyle: DraggingStyle | NotDraggingStyle | undefined,
|
||||
palette: Palette,
|
||||
) => ({
|
||||
// styles we need to apply on draggables
|
||||
...draggableStyle,
|
||||
|
||||
@@ -49,11 +49,14 @@ const getItemStyle = (isDragging: boolean,
|
||||
|
||||
export default function Categories() {
|
||||
const { setTitle, setAction } = useContext(NavbarContext);
|
||||
useEffect(() => { setTitle('Categories'); setAction(<></>); }, []);
|
||||
useEffect(() => {
|
||||
setTitle('Categories');
|
||||
setAction(<></>);
|
||||
}, []);
|
||||
|
||||
const { data, mutate } = useQuery<ICategory[]>('/api/v1/category/');
|
||||
const categories = useMemo(() => {
|
||||
const res = [...data ?? []];
|
||||
const res = [...(data ?? [])];
|
||||
if (res.length > 0 && res[0].name === 'Default') {
|
||||
res.shift();
|
||||
}
|
||||
@@ -84,11 +87,7 @@ export default function Categories() {
|
||||
return;
|
||||
}
|
||||
|
||||
categoryReorder(
|
||||
categories,
|
||||
result.source.index,
|
||||
result.destination.index,
|
||||
);
|
||||
categoryReorder(categories, result.source.index, result.destination.index);
|
||||
};
|
||||
|
||||
const resetDialog = () => {
|
||||
@@ -102,7 +101,7 @@ export default function Categories() {
|
||||
setDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleEditDialogOpen = (index:number) => {
|
||||
const handleEditDialogOpen = (index: number) => {
|
||||
setDialogName(categories[index].name);
|
||||
setDialogDefault(categories[index].default);
|
||||
setCategoryToEdit(index);
|
||||
@@ -121,19 +120,16 @@ export default function Categories() {
|
||||
formData.append('default', dialogDefault.toString());
|
||||
|
||||
if (categoryToEdit === -1) {
|
||||
client.post('/api/v1/category/', formData)
|
||||
.finally(() => mutate());
|
||||
client.post('/api/v1/category/', formData).finally(() => mutate());
|
||||
} else {
|
||||
const category = categories[categoryToEdit];
|
||||
client.patch(`/api/v1/category/${category.id}`, formData)
|
||||
.finally(() => mutate());
|
||||
client.patch(`/api/v1/category/${category.id}`, formData).finally(() => mutate());
|
||||
}
|
||||
};
|
||||
|
||||
const deleteCategory = (index:number) => {
|
||||
const deleteCategory = (index: number) => {
|
||||
const category = categories[index];
|
||||
client.delete(`/api/v1/category/${category.id}`)
|
||||
.finally(() => mutate());
|
||||
client.delete(`/api/v1/category/${category.id}`).finally(() => mutate());
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -163,9 +159,7 @@ export default function Categories() {
|
||||
<ListItemIcon>
|
||||
<DragHandleIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={item.name}
|
||||
/>
|
||||
<ListItemText primary={item.name} />
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
handleEditDialogOpen(index);
|
||||
@@ -219,13 +213,13 @@ export default function Categories() {
|
||||
onChange={(e) => setDialogName(e.target.value)}
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={(
|
||||
control={
|
||||
<Checkbox
|
||||
checked={dialogDefault}
|
||||
onChange={(e) => setDialogDefault(e.target.checked)}
|
||||
color="default"
|
||||
/>
|
||||
)}
|
||||
}
|
||||
label="Default category when adding new manga to library"
|
||||
/>
|
||||
</DialogContent>
|
||||
|
||||
@@ -31,22 +31,31 @@ export default function DefaultReaderSettings() {
|
||||
const { metadata, settings, loading } = useDefaultReaderSettings();
|
||||
|
||||
const setSettingValue = (key: keyof IReaderSettings, value: string | boolean) => {
|
||||
requestUpdateServerMetadata(metadata ?? {}, [[key, value]]).catch(() => makeToast('Failed to save the default reader settings to the server', 'warning'));
|
||||
requestUpdateServerMetadata(metadata ?? {}, [[key, value]]).catch(() =>
|
||||
makeToast('Failed to save the default reader settings to the server', 'warning'),
|
||||
);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Box sx={{
|
||||
height: '100vh', width: '100vw', display: 'grid', placeItems: 'center',
|
||||
}}
|
||||
<Box
|
||||
sx={{
|
||||
height: '100vh',
|
||||
width: '100vw',
|
||||
display: 'grid',
|
||||
placeItems: 'center',
|
||||
}}
|
||||
>
|
||||
<CircularProgress thickness={5} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
checkAndHandleMissingStoredReaderSettings({ meta: metadata }, 'server', getDefaultSettings())
|
||||
.catch(() => {});
|
||||
checkAndHandleMissingStoredReaderSettings(
|
||||
{ meta: metadata },
|
||||
'server',
|
||||
getDefaultSettings(),
|
||||
).catch(() => {});
|
||||
|
||||
return (
|
||||
<ReaderSettingsOptions
|
||||
|
||||
Reference in New Issue
Block a user