Merge pull request #395 from schroda/feature/use_graphql
Feature/use graphql
This commit is contained in:
@@ -1 +1,2 @@
|
||||
.eslintrc.js
|
||||
src/lib/graphql/generated
|
||||
|
||||
@@ -2,7 +2,7 @@ module.exports = {
|
||||
extends: ['airbnb', 'airbnb-typescript', 'prettier'],
|
||||
plugins: ['@typescript-eslint', 'no-relative-import-paths', 'prettier', 'header'],
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.json', './tools/scripts/tsconfig.json'],
|
||||
project: ['./tsconfig.json', './tsconfig.node.json', './tools/scripts/tsconfig.json'],
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
@@ -27,6 +27,8 @@ module.exports = {
|
||||
|
||||
'prettier/prettier': 'error',
|
||||
|
||||
'class-methods-use-this': 'off',
|
||||
|
||||
'no-plusplus': ['error', { allowForLoopAfterthoughts: true }],
|
||||
|
||||
// just why
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -6,4 +6,6 @@ node_modules/
|
||||
|
||||
build/*
|
||||
|
||||
tools/scripts/github_token.json
|
||||
tools/scripts/github_token.json
|
||||
|
||||
src/lib/graphql/schema.json
|
||||
|
||||
37
gql_codegen.ts
Normal file
37
gql_codegen.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* 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 type { CodegenConfig } from '@graphql-codegen/cli';
|
||||
|
||||
const config: CodegenConfig = {
|
||||
overwrite: true,
|
||||
schema: 'http://localhost:4567/api/graphql',
|
||||
documents: [
|
||||
'src/lib/graphql/queries/**',
|
||||
'src/lib/graphql/mutations/**',
|
||||
'src/lib/graphql/subscriptions/**',
|
||||
'src/lib/graphql/Fragments.ts',
|
||||
],
|
||||
ignoreNoDocuments: true,
|
||||
generates: {
|
||||
'src/lib/graphql/generated/graphql.ts': {
|
||||
plugins: ['typescript', 'typescript-operations'],
|
||||
config: {
|
||||
namingConvention: {
|
||||
typeNames: 'change-case-all#pascalCase',
|
||||
transformUnderscore: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
'src/lib/graphql/generated/apollo-helpers.ts': {
|
||||
plugins: ['typescript-apollo-client-helpers'],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
15
package.json
15
package.json
@@ -11,7 +11,10 @@
|
||||
"build-zip": "cd build && rev=$(git rev-list HEAD --count) && echo r$rev > revision && zip -9 -r ../buildZip/Tachidesk-WebUI-r$rev *",
|
||||
"lint": "eslint src --ext .ts,.tsx,.js,.jsx",
|
||||
"createChangelog": "ts-node tools/scripts/createReleaseChanglog.ts",
|
||||
"updateDeps": "yarn outdated && yarn upgrade && yarn syncyarnlock -s -k && yarn && git add package.json yarn.lock && git commit -m \"Update dependencies\""
|
||||
"updateDeps": "yarn outdated && yarn upgrade && yarn syncyarnlock -s -k && yarn && git add package.json yarn.lock && git commit -m \"Update dependencies\"",
|
||||
"gql:codegen-base": "graphql-codegen --config gql_codegen.ts",
|
||||
"gql:codegen-formatter": "ts-node tools/scripts/codegenFormatter.ts",
|
||||
"gql:codegen": "yarn gql:codegen-base & yarn gql:codegen-formatter"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
@@ -26,14 +29,18 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.8.1",
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@fontsource/roboto": "^5.0.8",
|
||||
"@mui/icons-material": "^5.14.9",
|
||||
"@mui/material": "^5.14.9",
|
||||
"@vitejs/plugin-react-swc": "^3.3.2",
|
||||
"apollo-upload-client": "^17.0.0",
|
||||
"axios": "^1.5.0",
|
||||
"file-selector": "^0.6.0",
|
||||
"graphql-tag": "^2.12.6",
|
||||
"graphql-ws": "^5.14.1",
|
||||
"i18next": "^23.5.1",
|
||||
"i18next-browser-languagedetector": "^7.1.0",
|
||||
"react": "^18.2.0",
|
||||
@@ -42,12 +49,16 @@
|
||||
"react-i18next": "^13.2.2",
|
||||
"react-router-dom": "^6.16.0",
|
||||
"react-virtuoso": "^4.5.1",
|
||||
"swr": "^2.2.2",
|
||||
"use-query-params": "^2.2.1",
|
||||
"vite": "^4.4.9",
|
||||
"vite-tsconfig-paths": "^4.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@graphql-codegen/cli": "^5.0.0",
|
||||
"@graphql-codegen/client-preset": "^4.1.0",
|
||||
"@graphql-codegen/typescript-apollo-client-helpers": "^2.2.6",
|
||||
"@graphql-codegen/typescript-operations": "^4.0.1",
|
||||
"@types/apollo-upload-client": "^17.0.2",
|
||||
"@types/node": "^20.6.2",
|
||||
"@types/react": "^18.2.21",
|
||||
"@types/react-beautiful-dnd": "^13.1.4",
|
||||
|
||||
@@ -10,6 +10,8 @@ import { Container } from '@mui/material';
|
||||
import CssBaseline from '@mui/material/CssBaseline';
|
||||
import React from 'react';
|
||||
import { Navigate, Route, Routes } from 'react-router-dom';
|
||||
import { loadErrorMessages, loadDevMessages } from '@apollo/client/dev';
|
||||
import { __DEV__ } from '@apollo/client/utilities/globals';
|
||||
import AppContext from '@/components/context/AppContext';
|
||||
import Browse from '@/screens/Browse';
|
||||
import DownloadQueue from '@/screens/DownloadQueue';
|
||||
@@ -31,6 +33,11 @@ import '@/i18n';
|
||||
import LibrarySettings from '@/screens/settings/LibrarySettings';
|
||||
import DefaultNavBar from '@/components/navbar/DefaultNavBar';
|
||||
|
||||
if (__DEV__) {
|
||||
// Adds messages only in a dev environment
|
||||
loadDevMessages();
|
||||
loadErrorMessages();
|
||||
}
|
||||
const App: React.FC = () => (
|
||||
<AppContext>
|
||||
<CssBaseline />
|
||||
|
||||
@@ -14,12 +14,11 @@ import Avatar from '@mui/material/Avatar';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { Box } from '@mui/material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { IExtension, TranslationKey } from '@/typings';
|
||||
import requestManager from '@/lib/RequestManager';
|
||||
import { PartialExtension, TranslationKey } from '@/typings';
|
||||
import requestManager from '@/lib/requests/RequestManager.ts';
|
||||
|
||||
interface IProps {
|
||||
extension: IExtension;
|
||||
notifyInstall: () => void;
|
||||
extension: PartialExtension;
|
||||
}
|
||||
|
||||
enum ExtensionAction {
|
||||
@@ -65,17 +64,16 @@ export default function ExtensionCard(props: IProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const {
|
||||
extension: { name, lang, versionName, installed, hasUpdate, obsolete, pkgName, iconUrl, isNsfw },
|
||||
notifyInstall,
|
||||
extension: { name, lang, versionName, isInstalled, hasUpdate, isObsolete, pkgName, iconUrl, isNsfw },
|
||||
} = props;
|
||||
const [installedState, setInstalledState] = useState<InstalledStates>(() => {
|
||||
if (obsolete) {
|
||||
if (isObsolete) {
|
||||
return InstalledState.OBSOLETE;
|
||||
}
|
||||
if (hasUpdate) {
|
||||
return InstalledState.UPDATE;
|
||||
}
|
||||
return installed ? InstalledState.UNINSTALL : InstalledState.INSTALL;
|
||||
return isInstalled ? InstalledState.UNINSTALL : InstalledState.INSTALL;
|
||||
});
|
||||
|
||||
const langPress = lang === 'all' ? t('extension.language.all') : lang.toUpperCase();
|
||||
@@ -87,19 +85,18 @@ export default function ExtensionCard(props: IProps) {
|
||||
setInstalledState(state);
|
||||
switch (action) {
|
||||
case ExtensionAction.INSTALL:
|
||||
await requestManager.installExtension(pkgName).response;
|
||||
await requestManager.updateExtension(pkgName, { install: true }).response;
|
||||
break;
|
||||
case ExtensionAction.UNINSTALL:
|
||||
await requestManager.uninstallExtension(pkgName).response;
|
||||
await requestManager.updateExtension(pkgName, { uninstall: true }).response;
|
||||
break;
|
||||
case ExtensionAction.UPDATE:
|
||||
await requestManager.updateExtension(pkgName).response;
|
||||
await requestManager.updateExtension(pkgName, { update: true }).response;
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unexpected ExtensionAction "${action}"`);
|
||||
}
|
||||
setInstalledState(nextAction);
|
||||
notifyInstall();
|
||||
};
|
||||
|
||||
function handleButtonClick() {
|
||||
|
||||
@@ -12,10 +12,10 @@ import Typography from '@mui/material/Typography';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Avatar, Box, CardContent, styled } from '@mui/material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { IMangaCard } from '@/typings';
|
||||
import requestManager from '@/lib/RequestManager';
|
||||
import requestManager from '@/lib/requests/RequestManager.ts';
|
||||
import { GridLayout, useLibraryOptionsContext } from '@/components/context/LibraryOptionsContext';
|
||||
import SpinnerImage from '@/components/util/SpinnerImage';
|
||||
import { TPartialManga } from '@/typings.ts';
|
||||
|
||||
const BottomGradient = styled('div')({
|
||||
position: 'absolute',
|
||||
@@ -66,7 +66,7 @@ const BadgeContainer = styled('div')({
|
||||
});
|
||||
|
||||
interface IProps {
|
||||
manga: IMangaCard;
|
||||
manga: TPartialManga;
|
||||
gridLayout?: GridLayout;
|
||||
inLibraryIndicator?: boolean;
|
||||
}
|
||||
@@ -75,10 +75,11 @@ const MangaCard = (props: IProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const {
|
||||
manga: { id, title, thumbnailUrl, downloadCount, unreadCount: unread, inLibrary },
|
||||
manga: { id, title, thumbnailUrl: tmpThumbnailUrl, downloadCount, unreadCount: unread, inLibrary },
|
||||
gridLayout,
|
||||
inLibraryIndicator,
|
||||
} = props;
|
||||
const thumbnailUrl = tmpThumbnailUrl ?? 'nonExistingMangaUrl';
|
||||
const {
|
||||
options: { showUnreadBadge, showDownloadBadge },
|
||||
} = useLibraryOptionsContext();
|
||||
@@ -119,10 +120,10 @@ const MangaCard = (props: IProps) => {
|
||||
{t('manga.button.in_library')}
|
||||
</Typography>
|
||||
)}
|
||||
{showUnreadBadge && unread! > 0 && (
|
||||
{showUnreadBadge && (unread ?? 0) > 0 && (
|
||||
<Typography sx={{ backgroundColor: 'primary.dark' }}>{unread}</Typography>
|
||||
)}
|
||||
{showDownloadBadge && downloadCount! > 0 && (
|
||||
{showDownloadBadge && (downloadCount ?? 0) > 0 && (
|
||||
<Typography
|
||||
sx={{
|
||||
backgroundColor: 'success.dark',
|
||||
|
||||
@@ -11,12 +11,12 @@ import Grid, { GridTypeMap } from '@mui/material/Grid';
|
||||
import { Box, Typography } from '@mui/material';
|
||||
import { GridItemProps, VirtuosoGrid, VirtuosoGridHandle } from 'react-virtuoso';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import { IMangaCard } from '@/typings';
|
||||
import EmptyView from '@/components/util/EmptyView';
|
||||
import LoadingPlaceholder from '@/components/util/LoadingPlaceholder';
|
||||
import MangaCard from '@/components/MangaCard';
|
||||
import { GridLayout } from '@/components/context/LibraryOptionsContext';
|
||||
import useLocalStorage from '@/util/useLocalStorage';
|
||||
import { TPartialManga } from '@/typings.ts';
|
||||
|
||||
const GridContainer = React.forwardRef<HTMLDivElement, GridTypeMap['props']>(({ children, ...props }, ref) => (
|
||||
<Grid {...props} ref={ref} container sx={{ paddingLeft: '5px', paddingRight: '13px' }}>
|
||||
@@ -40,13 +40,13 @@ const GridItemContainerWithDimension = (
|
||||
);
|
||||
};
|
||||
|
||||
const createMangaCard = (manga: IMangaCard, gridLayout?: GridLayout, inLibraryIndicator?: boolean) => (
|
||||
const createMangaCard = (manga: TPartialManga, gridLayout?: GridLayout, inLibraryIndicator?: boolean) => (
|
||||
<MangaCard key={manga.id} manga={manga} gridLayout={gridLayout} inLibraryIndicator={inLibraryIndicator} />
|
||||
);
|
||||
|
||||
type DefaultGridProps = {
|
||||
isLoading: boolean;
|
||||
mangas: IMangaCard[];
|
||||
mangas: TPartialManga[];
|
||||
inLibraryIndicator?: boolean;
|
||||
GridItemContainer: (props: GridTypeMap['props'] & Partial<GridItemProps>) => JSX.Element;
|
||||
gridLayout?: GridLayout;
|
||||
@@ -150,7 +150,7 @@ const VerticalGrid = ({
|
||||
};
|
||||
|
||||
export interface IMangaGridProps {
|
||||
mangas: IMangaCard[];
|
||||
mangas: TPartialManga[];
|
||||
isLoading: boolean;
|
||||
message?: string;
|
||||
messageExtra?: JSX.Element;
|
||||
|
||||
@@ -16,7 +16,7 @@ import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { ISource } from '@/typings';
|
||||
import requestManager from '@/lib/RequestManager';
|
||||
import requestManager from '@/lib/requests/RequestManager.ts';
|
||||
import { translateExtensionLanguage } from '@/screens/util/Extensions';
|
||||
import { SourceContentType } from '@/screens/SourceMangas';
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
import { StyledEngineProvider, ThemeProvider } from '@mui/material/styles';
|
||||
import React, { useMemo } from 'react';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import { SWRConfig } from 'swr';
|
||||
import { QueryParamProvider } from 'use-query-params';
|
||||
import { ReactRouter6Adapter } from 'use-query-params/adapters/react-router-6';
|
||||
import createTheme from '@/theme';
|
||||
@@ -36,21 +35,19 @@ const AppContext: React.FC<Props> = ({ children }) => {
|
||||
const theme = useMemo(() => createTheme(darkTheme), [darkTheme]);
|
||||
|
||||
return (
|
||||
<SWRConfig>
|
||||
<Router>
|
||||
<StyledEngineProvider injectFirst>
|
||||
<ThemeProvider theme={theme}>
|
||||
<DarkTheme.Provider value={darkThemeContext}>
|
||||
<QueryParamProvider adapter={ReactRouter6Adapter}>
|
||||
<LibraryOptionsContextProvider>
|
||||
<NavBarContextProvider>{children}</NavBarContextProvider>
|
||||
</LibraryOptionsContextProvider>
|
||||
</QueryParamProvider>
|
||||
</DarkTheme.Provider>
|
||||
</ThemeProvider>
|
||||
</StyledEngineProvider>
|
||||
</Router>
|
||||
</SWRConfig>
|
||||
<Router>
|
||||
<StyledEngineProvider injectFirst>
|
||||
<ThemeProvider theme={theme}>
|
||||
<DarkTheme.Provider value={darkThemeContext}>
|
||||
<QueryParamProvider adapter={ReactRouter6Adapter}>
|
||||
<LibraryOptionsContextProvider>
|
||||
<NavBarContextProvider>{children}</NavBarContextProvider>
|
||||
</LibraryOptionsContextProvider>
|
||||
</QueryParamProvider>
|
||||
</DarkTheme.Provider>
|
||||
</ThemeProvider>
|
||||
</StyledEngineProvider>
|
||||
</Router>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -9,12 +9,12 @@
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import { StringParam, useQueryParam } from 'use-query-params';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { IMangaCard, LibrarySortMode, NullAndUndefined } from '@/typings';
|
||||
import { LibrarySortMode, NullAndUndefined, TManga } from '@/typings';
|
||||
import { useSearchSettings } from '@/util/searchSettings';
|
||||
import { useLibraryOptionsContext } from '@/components/context/LibraryOptionsContext';
|
||||
import MangaGrid from '@/components/MangaGrid';
|
||||
|
||||
const unreadFilter = (unread: NullAndUndefined<boolean>, { unreadCount }: IMangaCard): boolean => {
|
||||
const unreadFilter = (unread: NullAndUndefined<boolean>, { unreadCount }: TManga): boolean => {
|
||||
switch (unread) {
|
||||
case true:
|
||||
return !!unreadCount && unreadCount >= 1;
|
||||
@@ -25,7 +25,7 @@ const unreadFilter = (unread: NullAndUndefined<boolean>, { unreadCount }: IManga
|
||||
}
|
||||
};
|
||||
|
||||
const downloadedFilter = (downloaded: NullAndUndefined<boolean>, { downloadCount }: IMangaCard): boolean => {
|
||||
const downloadedFilter = (downloaded: NullAndUndefined<boolean>, { downloadCount }: TManga): boolean => {
|
||||
switch (downloaded) {
|
||||
case true:
|
||||
return !!downloadCount && downloadCount >= 1;
|
||||
@@ -36,24 +36,24 @@ const downloadedFilter = (downloaded: NullAndUndefined<boolean>, { downloadCount
|
||||
}
|
||||
};
|
||||
|
||||
const queryFilter = (query: NullAndUndefined<string>, { title }: IMangaCard): boolean => {
|
||||
const queryFilter = (query: NullAndUndefined<string>, { title }: TManga): boolean => {
|
||||
if (!query) return true;
|
||||
return title.toLowerCase().includes(query.toLowerCase());
|
||||
};
|
||||
|
||||
const queryGenreFilter = (query: NullAndUndefined<string>, { genre }: IMangaCard): boolean => {
|
||||
const queryGenreFilter = (query: NullAndUndefined<string>, { genre }: TManga): boolean => {
|
||||
if (!query) return true;
|
||||
const queries = query.split(',').map((str) => str.toLowerCase().trim());
|
||||
return queries.every((element) => genre.map((el) => el.toLowerCase()).includes(element));
|
||||
};
|
||||
|
||||
const filterManga = (
|
||||
mangas: IMangaCard[],
|
||||
mangas: TManga[],
|
||||
query: NullAndUndefined<string>,
|
||||
unread: NullAndUndefined<boolean>,
|
||||
downloaded: NullAndUndefined<boolean>,
|
||||
ignoreFilters: boolean,
|
||||
): IMangaCard[] =>
|
||||
): TManga[] =>
|
||||
mangas.filter((manga) => {
|
||||
const ignoreFiltersWhileSearching = ignoreFilters && query?.length;
|
||||
const matchesSearch = queryFilter(query, manga) || queryGenreFilter(query, manga);
|
||||
@@ -63,19 +63,20 @@ const filterManga = (
|
||||
return matchesSearch && matchesFilters;
|
||||
});
|
||||
|
||||
const sortByUnread = (a: IMangaCard, b: IMangaCard): number => (a.unreadCount ?? 0) - (b.unreadCount ?? 0);
|
||||
const sortByUnread = (a: TManga, b: TManga): number => (a.unreadCount ?? 0) - (b.unreadCount ?? 0);
|
||||
|
||||
const sortByTitle = (a: IMangaCard, b: IMangaCard): number => a.title.localeCompare(b.title);
|
||||
const sortByTitle = (a: TManga, b: TManga): number => a.title.localeCompare(b.title);
|
||||
|
||||
const sortByDateAdded = (a: IMangaCard, b: IMangaCard): number => a.inLibraryAt - b.inLibraryAt;
|
||||
const sortByDateAdded = (a: TManga, b: TManga): number => Number(a.inLibraryAt) - Number(b.inLibraryAt);
|
||||
|
||||
const sortByLastRead = (a: IMangaCard, b: IMangaCard): number => b.lastReadAt - a.lastReadAt;
|
||||
const sortByLastRead = (a: TManga, b: TManga): number =>
|
||||
Number(b.lastReadChapter?.lastReadAt ?? 0) - Number(a.lastReadChapter?.lastReadAt ?? 0);
|
||||
|
||||
const sortManga = (
|
||||
manga: IMangaCard[],
|
||||
manga: TManga[],
|
||||
sort: NullAndUndefined<LibrarySortMode>,
|
||||
desc: NullAndUndefined<boolean>,
|
||||
): IMangaCard[] => {
|
||||
): TManga[] => {
|
||||
const result = [...manga];
|
||||
|
||||
switch (sort) {
|
||||
@@ -103,17 +104,12 @@ const sortManga = (
|
||||
};
|
||||
|
||||
interface LibraryMangaGridProps {
|
||||
mangas: IMangaCard[];
|
||||
mangas: TManga[];
|
||||
isLoading: boolean;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
const LibraryMangaGrid: React.FC<LibraryMangaGridProps & { lastLibraryUpdate: number }> = ({
|
||||
mangas,
|
||||
isLoading,
|
||||
message,
|
||||
lastLibraryUpdate,
|
||||
}) => {
|
||||
const LibraryMangaGrid: React.FC<LibraryMangaGridProps> = ({ mangas, isLoading, message }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [query] = useQueryParam('query', StringParam);
|
||||
@@ -127,7 +123,7 @@ const LibraryMangaGrid: React.FC<LibraryMangaGridProps & { lastLibraryUpdate: nu
|
||||
);
|
||||
const sortedMangas = useMemo(
|
||||
() => sortManga(filteredMangas, options.sorts, options.sortDesc),
|
||||
[filteredMangas, lastLibraryUpdate, options.sorts, options.sortDesc],
|
||||
[filteredMangas, options.sorts, options.sortDesc],
|
||||
);
|
||||
|
||||
const showFilteredOutMessage =
|
||||
@@ -135,7 +131,7 @@ const LibraryMangaGrid: React.FC<LibraryMangaGridProps & { lastLibraryUpdate: nu
|
||||
|
||||
useEffect(() => {
|
||||
window.scrollTo(0, 0);
|
||||
}, [filteredMangas]);
|
||||
}, [query, unread, downloaded]);
|
||||
|
||||
return (
|
||||
<MangaGrid
|
||||
|
||||
@@ -6,16 +6,16 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
import { Box } from '@mui/material';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { IUpdateStatus } from '@/typings';
|
||||
import requestManager from '@/lib/RequestManager';
|
||||
import requestManager from '@/lib/requests/RequestManager.ts';
|
||||
import makeToast from '@/components/util/Toast';
|
||||
import { UpdaterSubscription } from '@/lib/graphql/generated/graphql.ts';
|
||||
|
||||
interface IProgressProps {
|
||||
progress: number;
|
||||
@@ -32,62 +32,49 @@ function Progress({ progress }: IProgressProps) {
|
||||
);
|
||||
}
|
||||
|
||||
interface IUpdateCheckerProps {
|
||||
handleFinishedUpdate: (time: number) => void;
|
||||
}
|
||||
const calcProgress = (status: UpdaterSubscription['updateStatusChanged'] | undefined) => {
|
||||
if (!status) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
function UpdateChecker({ handleFinishedUpdate }: IUpdateCheckerProps) {
|
||||
const finishedUpdates = status.failedJobs.mangas.totalCount + status.completeJobs.mangas.totalCount;
|
||||
const totalMangas = finishedUpdates + status.pendingJobs.mangas.totalCount + status.runningJobs.mangas.totalCount;
|
||||
|
||||
const progress = 100 * (finishedUpdates / totalMangas);
|
||||
|
||||
return Number.isNaN(progress) ? 0 : progress;
|
||||
};
|
||||
|
||||
function UpdateChecker({ handleFinishedUpdate }: { handleFinishedUpdate: () => void }) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [progress, setProgress] = useState(0);
|
||||
const { data: updaterData } = requestManager.useUpdaterSubscription();
|
||||
const status = updaterData?.updateStatusChanged;
|
||||
|
||||
const loading = !!status?.isRunning;
|
||||
const progress = useMemo(
|
||||
() => calcProgress(status),
|
||||
[
|
||||
status?.failedJobs.mangas.totalCount,
|
||||
status?.completeJobs.mangas.totalCount,
|
||||
status?.pendingJobs.mangas.totalCount,
|
||||
status?.runningJobs.mangas.totalCount,
|
||||
],
|
||||
);
|
||||
|
||||
const isUpdateFinished = progress === 100;
|
||||
if (isUpdateFinished) {
|
||||
handleFinishedUpdate();
|
||||
}
|
||||
|
||||
const onClick = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setProgress(0);
|
||||
await requestManager.startGlobalUpdate().response;
|
||||
} catch (e) {
|
||||
makeToast(t('global.error.label.update_failed'), 'error');
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const wsc = requestManager.getUpdateWebSocket();
|
||||
|
||||
// "loading" can't be used since it will be outdated once the state gets changed
|
||||
// it could be used by adding it as a dependency of "useEffect" but then the socket would
|
||||
// get closed and connected again every time it changes
|
||||
let updateStarted = false;
|
||||
|
||||
wsc.onmessage = (e) => {
|
||||
const { running, mangaStatusMap } = JSON.parse(e.data) as IUpdateStatus;
|
||||
const { COMPLETE = [], RUNNING = [], PENDING = [] } = mangaStatusMap;
|
||||
|
||||
const currentProgress = 100 * (COMPLETE.length / (COMPLETE.length + RUNNING.length + PENDING.length));
|
||||
|
||||
const isUpdateFinished = currentProgress === 100;
|
||||
const ignoreFaultyMessage = !updateStarted && !running && isUpdateFinished;
|
||||
|
||||
// for some reason the server sends 100% completed manga updates when connecting to the
|
||||
// socket while no update is running
|
||||
if (ignoreFaultyMessage) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateStarted = running;
|
||||
setLoading(running);
|
||||
setProgress(Number.isNaN(currentProgress) ? 0 : currentProgress);
|
||||
|
||||
if (isUpdateFinished) {
|
||||
handleFinishedUpdate(Date.now());
|
||||
}
|
||||
};
|
||||
|
||||
return () => wsc.close();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<IconButton onClick={onClick} disabled={loading}>
|
||||
{loading ? <Progress progress={progress} /> : <RefreshIcon />}
|
||||
|
||||
@@ -1,37 +0,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 { useEffect, useState } from 'react';
|
||||
import requestManager from '@/lib/RequestManager';
|
||||
|
||||
const useSubscription = <T>(path: string, callback?: (newValue: T) => boolean | void) => {
|
||||
const [state, setState] = useState<T | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
const wsc = new WebSocket(requestManager.getValidWebSocketUrl(path));
|
||||
|
||||
wsc.onmessage = (e) => {
|
||||
const data = JSON.parse(e.data) as T;
|
||||
if (callback) {
|
||||
// If callback is specified, only update state if callback returns true
|
||||
// This is so that useSubscription can be used without causing rerender
|
||||
if (callback(data) === true) {
|
||||
setState(data);
|
||||
}
|
||||
} else {
|
||||
setState(data);
|
||||
}
|
||||
};
|
||||
|
||||
return () => wsc.close();
|
||||
}, [path]);
|
||||
|
||||
return { data: state };
|
||||
};
|
||||
|
||||
export default useSubscription;
|
||||
@@ -27,15 +27,16 @@ import Typography from '@mui/material/Typography';
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { IChapter, IDownloadChapter } from '@/typings';
|
||||
import requestManager from '@/lib/RequestManager';
|
||||
import requestManager from '@/lib/requests/RequestManager.ts';
|
||||
import { getUploadDateString } from '@/util/date';
|
||||
import DownloadStateIndicator from '@/components/molecules/DownloadStateIndicator';
|
||||
import { DownloadType, UpdateChapterPatchInput } from '@/lib/graphql/generated/graphql.ts';
|
||||
import { TChapter } from '@/typings.ts';
|
||||
|
||||
interface IProps {
|
||||
chapter: IChapter;
|
||||
triggerChaptersUpdate: () => void;
|
||||
downloadChapter: IDownloadChapter | undefined;
|
||||
chapter: TChapter;
|
||||
chapterIds: number[];
|
||||
downloadChapter: DownloadType | undefined;
|
||||
showChapterNumber: boolean;
|
||||
onSelect: (selected: boolean) => void;
|
||||
selected: boolean | null;
|
||||
@@ -45,7 +46,7 @@ const ChapterCard: React.FC<IProps> = (props: IProps) => {
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
|
||||
const { chapter, triggerChaptersUpdate, downloadChapter: dc, showChapterNumber, onSelect, selected } = props;
|
||||
const { chapter, chapterIds, downloadChapter: dc, showChapterNumber, onSelect, selected } = props;
|
||||
const isSelecting = selected !== null;
|
||||
|
||||
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
|
||||
@@ -62,26 +63,29 @@ const ChapterCard: React.FC<IProps> = (props: IProps) => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
const sendChange = (key: string, value: any) => {
|
||||
type UpdatePatchInput = UpdateChapterPatchInput & { markPrevRead?: boolean };
|
||||
const sendChange = <Key extends keyof UpdatePatchInput>(key: Key, value: UpdatePatchInput[Key]) => {
|
||||
handleClose();
|
||||
|
||||
requestManager
|
||||
.updateChapter(chapter.mangaId, chapter.index, {
|
||||
[key]: value,
|
||||
lastPageRead: key === 'read' ? 0 : undefined,
|
||||
})
|
||||
.response.then(() => triggerChaptersUpdate());
|
||||
if (key === 'markPrevRead') {
|
||||
const index = chapterIds.findIndex((chapterId) => chapterId === chapter.id);
|
||||
requestManager.updateChapters(chapterIds.slice(index, -1), { isRead: true });
|
||||
return;
|
||||
}
|
||||
|
||||
requestManager.updateChapter(chapter.id, {
|
||||
[key]: value,
|
||||
lastPageRead: key === 'isRead' ? 0 : undefined,
|
||||
});
|
||||
};
|
||||
|
||||
const downloadChapter = () => {
|
||||
requestManager.addChapterToDownloadQueue(chapter.mangaId, chapter.index);
|
||||
requestManager.addChapterToDownloadQueue(chapter.id);
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const deleteChapter = () => {
|
||||
requestManager
|
||||
.deleteDownloadedChapter(chapter.mangaId, chapter.index)
|
||||
.response.then(() => triggerChaptersUpdate());
|
||||
requestManager.deleteDownloadedChapter(chapter.id);
|
||||
handleClose();
|
||||
};
|
||||
|
||||
@@ -98,8 +102,8 @@ const ChapterCard: React.FC<IProps> = (props: IProps) => {
|
||||
}
|
||||
};
|
||||
|
||||
const isDownloaded = chapter.downloaded;
|
||||
const canBeDownloaded = !chapter.downloaded && dc === undefined;
|
||||
const { isDownloaded } = chapter;
|
||||
const canBeDownloaded = !chapter.isDownloaded && dc === undefined;
|
||||
|
||||
return (
|
||||
<li>
|
||||
@@ -111,9 +115,9 @@ const ChapterCard: React.FC<IProps> = (props: IProps) => {
|
||||
>
|
||||
<CardActionArea
|
||||
component={Link}
|
||||
to={`/manga/${chapter.mangaId}/chapter/${chapter.index}`}
|
||||
to={`/manga/${chapter.manga.id}/chapter/${chapter.sourceOrder}`}
|
||||
style={{
|
||||
color: theme.palette.text[chapter.read ? 'disabled' : 'primary'],
|
||||
color: theme.palette.text[chapter.isRead ? 'disabled' : 'primary'],
|
||||
}}
|
||||
onClick={handleClick}
|
||||
>
|
||||
@@ -128,7 +132,7 @@ const ChapterCard: React.FC<IProps> = (props: IProps) => {
|
||||
>
|
||||
<Stack direction="column" flex={1}>
|
||||
<Typography variant="h5" component="h2">
|
||||
{chapter.bookmarked && (
|
||||
{chapter.isBookmarked && (
|
||||
<BookmarkIcon
|
||||
color="primary"
|
||||
sx={{ mr: 0.5, position: 'relative', top: '0.15em' }}
|
||||
@@ -138,7 +142,7 @@ const ChapterCard: React.FC<IProps> = (props: IProps) => {
|
||||
</Typography>
|
||||
<Typography variant="caption">{chapter.scanlator}</Typography>
|
||||
<Typography variant="caption">
|
||||
{getUploadDateString(chapter.uploadDate)}
|
||||
{getUploadDateString(Number(chapter.uploadDate ?? 0))}
|
||||
{isDownloaded && ` • ${t('chapter.status.label.downloaded')}`}
|
||||
</Typography>
|
||||
</Stack>
|
||||
@@ -177,24 +181,24 @@ const ChapterCard: React.FC<IProps> = (props: IProps) => {
|
||||
<ListItemText>{t('chapter.action.download.add.label.action')}</ListItemText>
|
||||
</MenuItem>
|
||||
)}
|
||||
<MenuItem onClick={() => sendChange('bookmarked', !chapter.bookmarked)}>
|
||||
<MenuItem onClick={() => sendChange('isBookmarked', !chapter.isBookmarked)}>
|
||||
<ListItemIcon>
|
||||
{chapter.bookmarked && <BookmarkRemove fontSize="small" />}
|
||||
{!chapter.bookmarked && <BookmarkAdd fontSize="small" />}
|
||||
{chapter.isBookmarked && <BookmarkRemove fontSize="small" />}
|
||||
{!chapter.isBookmarked && <BookmarkAdd fontSize="small" />}
|
||||
</ListItemIcon>
|
||||
<ListItemText>
|
||||
{chapter.bookmarked && t('chapter.action.bookmark.remove.label.action')}
|
||||
{!chapter.bookmarked && t('chapter.action.bookmark.add.label.action')}
|
||||
{chapter.isBookmarked && t('chapter.action.bookmark.remove.label.action')}
|
||||
{!chapter.isBookmarked && t('chapter.action.bookmark.add.label.action')}
|
||||
</ListItemText>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => sendChange('read', !chapter.read)}>
|
||||
<MenuItem onClick={() => sendChange('isRead', !chapter.isRead)}>
|
||||
<ListItemIcon>
|
||||
{chapter.read && <RemoveDone fontSize="small" />}
|
||||
{!chapter.read && <Done fontSize="small" />}
|
||||
{chapter.isRead && <RemoveDone fontSize="small" />}
|
||||
{!chapter.isRead && <Done fontSize="small" />}
|
||||
</ListItemIcon>
|
||||
<ListItemText>
|
||||
{chapter.read && t('chapter.action.mark_as_read.remove.label.action')}
|
||||
{!chapter.read && t('chapter.action.mark_as_read.add.label.action.current')}
|
||||
{chapter.isRead && t('chapter.action.mark_as_read.remove.label.action')}
|
||||
{!chapter.isRead && t('chapter.action.mark_as_read.add.label.action.current')}
|
||||
</ListItemText>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => sendChange('markPrevRead', true)}>
|
||||
|
||||
@@ -11,9 +11,8 @@ import Typography from '@mui/material/Typography';
|
||||
import React, { ComponentProps, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Virtuoso } from 'react-virtuoso';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { BatchChaptersChange, IChapter, IDownloadChapter, IQueue, TranslationKey } from '@/typings';
|
||||
import requestManager from '@/lib/RequestManager';
|
||||
import useSubscription from '@/components/library/useSubscription';
|
||||
import { TChapter, TManga, TranslationKey } from '@/typings';
|
||||
import requestManager from '@/lib/requests/RequestManager.ts';
|
||||
import ChapterCard from '@/components/manga/ChapterCard';
|
||||
import ResumeFab from '@/components/manga/ResumeFAB';
|
||||
import { filterAndSortChapters, useChapterOptions } from '@/components/manga/util';
|
||||
@@ -22,6 +21,7 @@ import makeToast from '@/components/util/Toast';
|
||||
import ChaptersToolbarMenu from '@/components/manga/ChaptersToolbarMenu';
|
||||
import SelectionFAB from '@/components/manga/SelectionFAB';
|
||||
import { DEFAULT_FULL_FAB_HEIGHT } from '@/components/util/StyledFab';
|
||||
import { DownloadType, UpdateChapterPatchInput } from '@/lib/graphql/generated/graphql.ts';
|
||||
|
||||
const StyledVirtuoso = styled(Virtuoso)(({ theme }) => ({
|
||||
listStyle: 'none',
|
||||
@@ -68,58 +68,54 @@ const actionsStrings: {
|
||||
};
|
||||
|
||||
export interface IChapterWithMeta {
|
||||
chapter: IChapter;
|
||||
downloadChapter: IDownloadChapter | undefined;
|
||||
chapter: TChapter;
|
||||
downloadChapter: DownloadType | undefined;
|
||||
selected: boolean | null;
|
||||
}
|
||||
|
||||
interface IProps {
|
||||
mangaId: string;
|
||||
manga: TManga;
|
||||
isRefreshing: boolean;
|
||||
}
|
||||
|
||||
const ChapterList: React.FC<IProps> = ({ mangaId }) => {
|
||||
const ChapterList: React.FC<IProps> = ({ manga, isRefreshing }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [selection, setSelection] = useState<number[] | null>(null);
|
||||
const prevQueueRef = useRef<IDownloadChapter[]>();
|
||||
const queue = useSubscription<IQueue>('downloads').data?.queue;
|
||||
const prevQueueRef = useRef<DownloadType[]>();
|
||||
const { data: downloaderData } = requestManager.useDownloadSubscription();
|
||||
const queue = (downloaderData?.downloadChanged.queue as DownloadType[]) ?? [];
|
||||
|
||||
const [options, dispatch] = useChapterOptions(mangaId);
|
||||
const { data: chaptersData, mutate, isLoading } = requestManager.useGetMangaChapters(mangaId);
|
||||
const chapters = useMemo(() => chaptersData ?? [], [chaptersData]);
|
||||
const [options, dispatch] = useChapterOptions(manga.id);
|
||||
const { data: chaptersData, loading: isLoading, refetch } = requestManager.useGetMangaChapters(manga.id);
|
||||
const chapters = useMemo(() => chaptersData?.chapters.nodes ?? [], [chaptersData?.chapters.nodes]);
|
||||
const mangaChapterIds = useMemo(() => chapters.map((chapter) => chapter.id), [chapters]);
|
||||
|
||||
useEffect(() => {
|
||||
if (prevQueueRef.current && queue) {
|
||||
const prevQueue = prevQueueRef.current;
|
||||
const changedDownloads = queue.filter((cd) => {
|
||||
const prevChapterDownload = prevQueue.find(
|
||||
(pcd) => cd.chapterIndex === pcd.chapterIndex && cd.mangaId === pcd.mangaId,
|
||||
(pcd) =>
|
||||
cd.chapter.sourceOrder === pcd.chapter.sourceOrder &&
|
||||
cd.chapter.manga.id === pcd.chapter.manga.id,
|
||||
);
|
||||
if (!prevChapterDownload) return true;
|
||||
return cd.state !== prevChapterDownload.state;
|
||||
});
|
||||
|
||||
if (changedDownloads.length > 0) {
|
||||
mutate();
|
||||
if (changedDownloads.length > 0 || prevQueue?.length !== queue.length) {
|
||||
refetch();
|
||||
}
|
||||
}
|
||||
|
||||
prevQueueRef.current = queue;
|
||||
}, [queue]);
|
||||
|
||||
const visibleChapters = useMemo(
|
||||
() => filterAndSortChapters(chapters, options), //
|
||||
[chapters, options],
|
||||
);
|
||||
const visibleChapters = useMemo(() => filterAndSortChapters(chapters, options), [chapters, options]);
|
||||
|
||||
const firstUnreadChapter = useMemo(
|
||||
() =>
|
||||
chapters
|
||||
.slice()
|
||||
.reverse()
|
||||
.find((chapter) => !chapter.read),
|
||||
[chapters],
|
||||
);
|
||||
const nextChapterIndexToRead = (manga.lastReadChapter?.sourceOrder ?? 0) + 1;
|
||||
const isLatestChapterRead = manga.chapters.totalCount === manga.lastReadChapter?.sourceOrder;
|
||||
|
||||
const handleSelection = (index: number) => {
|
||||
const chapter = visibleChapters[index];
|
||||
@@ -154,26 +150,67 @@ const ChapterList: React.FC<IProps> = ({ mangaId }) => {
|
||||
if (action === 'download') {
|
||||
actionPromise = requestManager.addChaptersToDownloadQueue(chapterIds).response;
|
||||
} else {
|
||||
const change: BatchChaptersChange = {};
|
||||
const change: UpdateChapterPatchInput = {};
|
||||
|
||||
if (action === 'delete') change.delete = true;
|
||||
else if (action === 'bookmark') change.isBookmarked = true;
|
||||
if (action === 'bookmark') change.isBookmarked = true;
|
||||
else if (action === 'unbookmark') change.isBookmarked = false;
|
||||
else if (action === 'mark_as_read' || action === 'mark_as_unread') {
|
||||
change.isRead = action === 'mark_as_read';
|
||||
change.lastPageRead = 0;
|
||||
}
|
||||
|
||||
actionPromise = requestManager.updateChapters(chapterIds, change).response;
|
||||
if (action === 'delete') {
|
||||
actionPromise = requestManager.deleteDownloadedChapters(chapterIds).response;
|
||||
} else {
|
||||
actionPromise = requestManager.updateChapters(chapterIds, change).response;
|
||||
}
|
||||
}
|
||||
|
||||
actionPromise
|
||||
.then(() => makeToast(t(actionsStrings[action].success, { count: chapterIds.length }), 'success'))
|
||||
.then(() => mutate())
|
||||
.catch(() => makeToast(t(actionsStrings[action].error, { count: chapterIds.length }), 'error'));
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
const noChaptersFound = chapters.length === 0;
|
||||
const noChaptersMatchingFilter = !noChaptersFound && visibleChapters.length === 0;
|
||||
|
||||
const chaptersWithMeta: IChapterWithMeta[] = useMemo(
|
||||
() =>
|
||||
visibleChapters.map((chapter) => {
|
||||
const downloadChapter = queue?.find(
|
||||
(cd) => cd.chapter.sourceOrder === chapter.sourceOrder && cd.chapter.manga.id === chapter.manga.id,
|
||||
);
|
||||
const selected = selection?.includes(chapter.id) ?? null;
|
||||
return {
|
||||
chapter,
|
||||
downloadChapter,
|
||||
selected,
|
||||
};
|
||||
}),
|
||||
[queue, selection, visibleChapters],
|
||||
);
|
||||
|
||||
const selectedChapters = useMemo(() => {
|
||||
if (!selection) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return chaptersWithMeta.filter(({ chapter }) => selection.includes(chapter.id));
|
||||
}, [selection, chapters]);
|
||||
|
||||
const chapterListFAB = useMemo(() => {
|
||||
if (selectedChapters) {
|
||||
return <SelectionFAB selectedChapters={selectedChapters} onAction={handleFabAction} />;
|
||||
}
|
||||
|
||||
if (!isLatestChapterRead) {
|
||||
return <ResumeFab chapterIndex={nextChapterIndexToRead} mangaId={manga.id} />;
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [selectedChapters, isLatestChapterRead]);
|
||||
|
||||
if (isLoading || (noChaptersFound && isRefreshing)) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
@@ -187,24 +224,6 @@ const ChapterList: React.FC<IProps> = ({ mangaId }) => {
|
||||
);
|
||||
}
|
||||
|
||||
const noChaptersFound = chapters.length === 0;
|
||||
const noChaptersMatchingFilter = !noChaptersFound && visibleChapters.length === 0;
|
||||
|
||||
const chaptersWithMeta: IChapterWithMeta[] = visibleChapters.map((chapter) => {
|
||||
const downloadChapter = queue?.find(
|
||||
(cd) => cd.chapterIndex === chapter.index && cd.mangaId === chapter.mangaId,
|
||||
);
|
||||
const selected = selection?.includes(chapter.id) ?? null;
|
||||
return {
|
||||
chapter,
|
||||
downloadChapter,
|
||||
selected,
|
||||
};
|
||||
});
|
||||
|
||||
const selectedChapters =
|
||||
selection === null ? null : chaptersWithMeta.filter(({ chapter }) => selection.includes(chapter.id));
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack direction="column" sx={{ position: 'relative' }}>
|
||||
@@ -266,8 +285,8 @@ const ChapterList: React.FC<IProps> = ({ mangaId }) => {
|
||||
return (
|
||||
<ChapterCard
|
||||
{...chaptersWithMeta[index]}
|
||||
chapterIds={mangaChapterIds}
|
||||
showChapterNumber={options.showChapterNumber}
|
||||
triggerChaptersUpdate={() => mutate()}
|
||||
onSelect={() => handleSelection(index)}
|
||||
/>
|
||||
);
|
||||
@@ -276,11 +295,7 @@ const ChapterList: React.FC<IProps> = ({ mangaId }) => {
|
||||
overscan={window.innerHeight * 0.5}
|
||||
/>
|
||||
</Stack>
|
||||
{selectedChapters !== null ? (
|
||||
<SelectionFAB selectedChapters={selectedChapters} onAction={handleFabAction} />
|
||||
) : (
|
||||
firstUnreadChapter && <ResumeFab chapter={firstUnreadChapter} mangaId={mangaId} />
|
||||
)}
|
||||
{chapterListFAB}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -10,13 +10,12 @@ import FavoriteIcon from '@mui/icons-material/Favorite';
|
||||
import FavoriteBorderIcon from '@mui/icons-material/FavoriteBorder';
|
||||
import PublicIcon from '@mui/icons-material/Public';
|
||||
import { styled } from '@mui/material/styles';
|
||||
import React, { useEffect } from 'react';
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { mutate } from 'swr';
|
||||
import { t as translate } from 'i18next';
|
||||
import Button from '@mui/material/Button';
|
||||
import { IManga, ISource } from '@/typings';
|
||||
import requestManager from '@/lib/RequestManager';
|
||||
import { ISource, TManga } from '@/typings';
|
||||
import requestManager from '@/lib/requests/RequestManager.ts';
|
||||
import makeToast from '@/components/util/Toast';
|
||||
|
||||
const DetailsWrapper = styled('div')(({ theme }) => ({
|
||||
@@ -125,11 +124,36 @@ const Genres = styled('div')(() => ({
|
||||
},
|
||||
}));
|
||||
|
||||
const OpenSourceButton = ({ url }: { url?: string | null }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const button = useMemo(
|
||||
() => (
|
||||
<Button disabled={!!url} startIcon={<PublicIcon />} size="large">
|
||||
{t('global.button.open_site')}
|
||||
</Button>
|
||||
),
|
||||
[url],
|
||||
);
|
||||
|
||||
if (!url) {
|
||||
return button;
|
||||
}
|
||||
|
||||
return (
|
||||
<a href={url} target="_blank" rel="noreferrer">
|
||||
<Button startIcon={<PublicIcon />} size="large">
|
||||
{t('global.button.open_site')}
|
||||
</Button>
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
interface IProps {
|
||||
manga: IManga;
|
||||
manga: TManga;
|
||||
}
|
||||
|
||||
function getSourceName(source: ISource) {
|
||||
function getSourceName(source?: ISource | null) {
|
||||
if (!source) {
|
||||
return translate('global.label.unknown');
|
||||
}
|
||||
@@ -137,12 +161,16 @@ function getSourceName(source: ISource) {
|
||||
return source.displayName ?? source.id;
|
||||
}
|
||||
|
||||
function getValueOrUnknown(val: string) {
|
||||
function getValueOrUnknown(val?: string | null) {
|
||||
return val || 'UNKNOWN';
|
||||
}
|
||||
|
||||
const MangaDetails: React.FC<IProps> = ({ manga }) => {
|
||||
const { t } = useTranslation();
|
||||
const { data: categoriesData, loading: areCategoriesLoading } = requestManager.useGetCategories();
|
||||
const categories = categoriesData?.categories.nodes ?? [];
|
||||
const defaultCategoryIds = categories.filter((category) => category.default).map((category) => category.id);
|
||||
const [updateMangaCategories] = requestManager.useUpdateMangaCategories();
|
||||
|
||||
useEffect(() => {
|
||||
if (!manga.source) {
|
||||
@@ -151,13 +179,24 @@ const MangaDetails: React.FC<IProps> = ({ manga }) => {
|
||||
}, [manga.source]);
|
||||
|
||||
const addToLibrary = () => {
|
||||
mutate(`/api/v1/manga/${manga.id}`, { ...manga, inLibrary: true }, { revalidate: false });
|
||||
requestManager.addMangaToLibrary(manga.id).response.then(() => mutate(`/api/v1/manga/${manga.id}`));
|
||||
Promise.all([
|
||||
requestManager.updateManga(manga.id, { inLibrary: true }).response,
|
||||
updateMangaCategories({
|
||||
variables: { input: { id: manga.id, patch: { addToCategories: defaultCategoryIds } } },
|
||||
}),
|
||||
])
|
||||
.then(() => makeToast(t('library.info.label.added_to_library'), 'success'))
|
||||
.catch(() => {
|
||||
makeToast(t('library.error.label.add_to_library'), 'error');
|
||||
});
|
||||
};
|
||||
|
||||
const removeFromLibrary = () => {
|
||||
mutate(`/api/v1/manga/${manga.id}`, { ...manga, inLibrary: false }, { revalidate: false });
|
||||
requestManager.removeMangaFromLibrary(manga.id).response.then(() => mutate(`/api/v1/manga/${manga.id}`));
|
||||
Promise.all([requestManager.updateManga(manga.id, { inLibrary: false }).response])
|
||||
.then(() => makeToast(t('library.info.label.removed_from_library'), 'success'))
|
||||
.catch(() => {
|
||||
makeToast(t('library.error.label.remove_from_library'), 'error');
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -165,7 +204,9 @@ const MangaDetails: React.FC<IProps> = ({ manga }) => {
|
||||
<TopContentWrapper>
|
||||
<ThumbnailMetadataWrapper>
|
||||
<Thumbnail>
|
||||
<img src={requestManager.getValidImgUrlFor(manga.thumbnailUrl)} alt="Manga Thumbnail" />
|
||||
{manga.thumbnailUrl && (
|
||||
<img src={requestManager.getValidImgUrlFor(manga.thumbnailUrl)} alt="Manga Thumbnail" />
|
||||
)}
|
||||
</Thumbnail>
|
||||
<Metadata>
|
||||
<h1>{manga.title}</h1>
|
||||
@@ -184,6 +225,7 @@ const MangaDetails: React.FC<IProps> = ({ manga }) => {
|
||||
<MangaButtonsContainer inLibrary={manga.inLibrary}>
|
||||
<div>
|
||||
<Button
|
||||
disabled={areCategoriesLoading}
|
||||
startIcon={manga.inLibrary ? <FavoriteIcon /> : <FavoriteBorderIcon />}
|
||||
onClick={manga.inLibrary ? removeFromLibrary : addToLibrary}
|
||||
size="large"
|
||||
@@ -191,11 +233,7 @@ const MangaDetails: React.FC<IProps> = ({ manga }) => {
|
||||
{manga.inLibrary ? t('manga.button.in_library') : t('manga.button.add_to_library')}
|
||||
</Button>
|
||||
</div>
|
||||
<a href={manga.realUrl} target="_blank" rel="noreferrer">
|
||||
<Button startIcon={<PublicIcon />} size="large">
|
||||
{t('global.button.open_site')}
|
||||
</Button>
|
||||
</a>
|
||||
<OpenSourceButton url={manga.realUrl} />
|
||||
</MangaButtonsContainer>
|
||||
</TopContentWrapper>
|
||||
<BottomContentWrapper>
|
||||
|
||||
@@ -21,11 +21,11 @@ import {
|
||||
} from '@mui/material';
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { IManga } from '@/typings';
|
||||
import CategorySelect from '@/components/navbar/action/CategorySelect';
|
||||
import { TManga } from '@/typings.ts';
|
||||
|
||||
interface IProps {
|
||||
manga: IManga;
|
||||
manga: TManga;
|
||||
onRefresh: () => any;
|
||||
refreshing: boolean;
|
||||
}
|
||||
|
||||
@@ -9,25 +9,21 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import { PlayArrow } from '@mui/icons-material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { IChapter } from '@/typings';
|
||||
import StyledFab from '@/components/util/StyledFab';
|
||||
|
||||
interface ResumeFABProps {
|
||||
chapter: IChapter;
|
||||
mangaId: string;
|
||||
chapterIndex: number;
|
||||
mangaId: number;
|
||||
}
|
||||
|
||||
export default function ResumeFab(props: ResumeFABProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const {
|
||||
chapter: { index },
|
||||
mangaId,
|
||||
} = props;
|
||||
const { chapterIndex, mangaId } = props;
|
||||
return (
|
||||
<StyledFab component={Link} variant="extended" color="primary" to={`/manga/${mangaId}/chapter/${index}`}>
|
||||
<StyledFab component={Link} variant="extended" color="primary" to={`/manga/${mangaId}/chapter/${chapterIndex}`}>
|
||||
<PlayArrow />
|
||||
{index === 1 ? t('global.button.start') : t('global.button.resume')}
|
||||
{chapterIndex === 1 ? t('global.button.start') : t('global.button.resume')}
|
||||
</StyledFab>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -63,38 +63,38 @@ const SelectionFAB: React.FC<SelectionFABProps> = (props) => {
|
||||
<SelectionFABActionItem
|
||||
action="download"
|
||||
matchingChapters={selectedChapters.filter(
|
||||
({ chapter: c, downloadChapter: dc }) => !c.downloaded && dc === undefined,
|
||||
({ chapter: c, downloadChapter: dc }) => !c.isDownloaded && dc === undefined,
|
||||
)}
|
||||
onClick={handleAction}
|
||||
title={t('chapter.action.download.add.button.selected')}
|
||||
/>
|
||||
<SelectionFABActionItem
|
||||
action="delete"
|
||||
matchingChapters={selectedChapters.filter(({ chapter }) => chapter.downloaded)}
|
||||
matchingChapters={selectedChapters.filter(({ chapter }) => chapter.isDownloaded)}
|
||||
onClick={handleAction}
|
||||
title={t('chapter.action.download.delete.button.selected')}
|
||||
/>
|
||||
<SelectionFABActionItem
|
||||
action="bookmark"
|
||||
matchingChapters={selectedChapters.filter(({ chapter }) => !chapter.bookmarked)}
|
||||
matchingChapters={selectedChapters.filter(({ chapter }) => !chapter.isBookmarked)}
|
||||
onClick={handleAction}
|
||||
title={t('chapter.action.bookmark.add.button.selected')}
|
||||
/>
|
||||
<SelectionFABActionItem
|
||||
action="unbookmark"
|
||||
matchingChapters={selectedChapters.filter(({ chapter }) => chapter.bookmarked)}
|
||||
matchingChapters={selectedChapters.filter(({ chapter }) => chapter.isBookmarked)}
|
||||
onClick={handleAction}
|
||||
title={t('chapter.action.bookmark.remove.button.selected')}
|
||||
/>
|
||||
<SelectionFABActionItem
|
||||
action="mark_as_read"
|
||||
matchingChapters={selectedChapters.filter(({ chapter }) => !chapter.read)}
|
||||
matchingChapters={selectedChapters.filter(({ chapter }) => !chapter.isRead)}
|
||||
onClick={handleAction}
|
||||
title={t('chapter.action.mark_as_read.add.button.selected')}
|
||||
/>
|
||||
<SelectionFABActionItem
|
||||
action="mark_as_unread"
|
||||
matchingChapters={selectedChapters.filter(({ chapter }) => chapter.read)}
|
||||
matchingChapters={selectedChapters.filter(({ chapter }) => chapter.isRead)}
|
||||
onClick={handleAction}
|
||||
title={t('chapter.action.mark_as_read.remove.button.selected')}
|
||||
/>
|
||||
|
||||
@@ -7,8 +7,7 @@
|
||||
*/
|
||||
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { mutate } from 'swr';
|
||||
import requestManager, { RequestManager } from '@/lib/RequestManager';
|
||||
import requestManager from '@/lib/requests/RequestManager.ts';
|
||||
|
||||
export const useRefreshManga = (mangaId: string) => {
|
||||
const [fetchingOnline, setFetchingOnline] = useState(false);
|
||||
@@ -16,14 +15,8 @@ export const useRefreshManga = (mangaId: string) => {
|
||||
const handleRefresh = useCallback(async () => {
|
||||
setFetchingOnline(true);
|
||||
await Promise.all([
|
||||
requestManager.getManga(mangaId, true).response.then((res) => {
|
||||
mutate(`${RequestManager.API_VERSION}manga/${mangaId}`, res, { revalidate: false });
|
||||
}),
|
||||
requestManager.getMangaChapters(mangaId, true).response.then((res) =>
|
||||
mutate(`${RequestManager.API_VERSION}manga/${mangaId}/chapters`, res, {
|
||||
revalidate: false,
|
||||
}),
|
||||
),
|
||||
requestManager.getMangaFetch(mangaId, { awaitRefetchQueries: true }).response,
|
||||
requestManager.getMangaChaptersFetch(mangaId, { awaitRefetchQueries: true }).response,
|
||||
]).finally(() => setFetchingOnline(false));
|
||||
}, [mangaId]);
|
||||
|
||||
|
||||
@@ -11,8 +11,8 @@ import {
|
||||
ChapterListOptions,
|
||||
ChapterOptionsReducerAction,
|
||||
ChapterSortMode,
|
||||
IChapter,
|
||||
NullAndUndefined,
|
||||
TChapter,
|
||||
TranslationKey,
|
||||
} from '@/typings';
|
||||
import { useReducerLocalStorage } from '@/util/useLocalStorage';
|
||||
@@ -46,7 +46,7 @@ function chapterOptionsReducer(state: ChapterListOptions, actions: ChapterOption
|
||||
}
|
||||
}
|
||||
|
||||
export function unreadFilter(unread: NullAndUndefined<boolean>, { read: isChapterRead }: IChapter) {
|
||||
export function unreadFilter(unread: NullAndUndefined<boolean>, { isRead: isChapterRead }: TChapter) {
|
||||
switch (unread) {
|
||||
case true:
|
||||
return !isChapterRead;
|
||||
@@ -57,7 +57,7 @@ export function unreadFilter(unread: NullAndUndefined<boolean>, { read: isChapte
|
||||
}
|
||||
}
|
||||
|
||||
function downloadFilter(downloaded: NullAndUndefined<boolean>, { downloaded: chapterDownload }: IChapter) {
|
||||
function downloadFilter(downloaded: NullAndUndefined<boolean>, { isDownloaded: chapterDownload }: TChapter) {
|
||||
switch (downloaded) {
|
||||
case true:
|
||||
return chapterDownload;
|
||||
@@ -68,7 +68,7 @@ function downloadFilter(downloaded: NullAndUndefined<boolean>, { downloaded: cha
|
||||
}
|
||||
}
|
||||
|
||||
function bookmarkedFilter(bookmarked: NullAndUndefined<boolean>, { bookmarked: chapterBookmarked }: IChapter) {
|
||||
function bookmarkedFilter(bookmarked: NullAndUndefined<boolean>, { isBookmarked: chapterBookmarked }: TChapter) {
|
||||
switch (bookmarked) {
|
||||
case true:
|
||||
return chapterBookmarked;
|
||||
@@ -79,7 +79,7 @@ function bookmarkedFilter(bookmarked: NullAndUndefined<boolean>, { bookmarked: c
|
||||
}
|
||||
}
|
||||
|
||||
export function filterAndSortChapters(chapters: IChapter[], options: ChapterListOptions): IChapter[] {
|
||||
export function filterAndSortChapters(chapters: TChapter[], options: ChapterListOptions): TChapter[] {
|
||||
const filtered = options.active
|
||||
? chapters.filter(
|
||||
(chp) =>
|
||||
@@ -88,14 +88,17 @@ export function filterAndSortChapters(chapters: IChapter[], options: ChapterList
|
||||
bookmarkedFilter(options.bookmarked, chp),
|
||||
)
|
||||
: [...chapters];
|
||||
const Sorted = options.sortBy === 'fetchedAt' ? filtered.sort((a, b) => a.fetchedAt - b.fetchedAt) : filtered;
|
||||
const Sorted =
|
||||
options.sortBy === 'fetchedAt'
|
||||
? filtered.sort((a, b) => Number(a.fetchedAt ?? 0) - Number(b.fetchedAt ?? 0))
|
||||
: filtered;
|
||||
if (options.reverse) {
|
||||
Sorted.reverse();
|
||||
}
|
||||
return Sorted;
|
||||
}
|
||||
|
||||
export const useChapterOptions = (mangaId: string) =>
|
||||
export const useChapterOptions = (mangaId: number) =>
|
||||
useReducerLocalStorage<ChapterListOptions, ChapterOptionsReducerAction>(
|
||||
chapterOptionsReducer,
|
||||
`${mangaId}filterOptions`,
|
||||
|
||||
@@ -10,17 +10,18 @@ import { CircularProgress, Box } from '@mui/material';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { IDownloadChapter, TranslationKey } from '@/typings';
|
||||
import { TranslationKey } from '@/typings';
|
||||
import { DownloadState, DownloadType } from '@/lib/graphql/generated/graphql.ts';
|
||||
|
||||
interface DownloadStateIndicatorProps {
|
||||
download: IDownloadChapter;
|
||||
download: DownloadType;
|
||||
}
|
||||
|
||||
const DOWNLOAD_STATE_TO_TRANSLATION_KEY_MAP: { [state in IDownloadChapter['state']]: TranslationKey } = {
|
||||
Downloading: 'download.state.label.downloading',
|
||||
Error: 'download.state.label.error',
|
||||
Finished: 'download.state.label.finished',
|
||||
Queued: 'download.state.label.queued',
|
||||
const DOWNLOAD_STATE_TO_TRANSLATION_KEY_MAP: { [state in DownloadState]: TranslationKey } = {
|
||||
DOWNLOADING: 'download.state.label.downloading',
|
||||
ERROR: 'download.state.label.error',
|
||||
FINISHED: 'download.state.label.finished',
|
||||
QUEUED: 'download.state.label.queued',
|
||||
} as const;
|
||||
|
||||
const DownloadStateIndicator: React.FC<DownloadStateIndicatorProps> = ({ download }) => {
|
||||
|
||||
@@ -24,7 +24,7 @@ import ListItemText from '@mui/material/ListItemText';
|
||||
import ListItemSecondaryAction from '@mui/material/ListItemSecondaryAction';
|
||||
import Collapse from '@mui/material/Collapse';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ChapterOffset, IChapter, IManga, IMangaCard, IReaderSettings } from '@/typings';
|
||||
import { ChapterOffset, IReaderSettings, TChapter, TManga } from '@/typings';
|
||||
import ReaderSettingsOptions from '@/components/reader/ReaderSettingsOptions';
|
||||
|
||||
const Root = styled('div')(({ theme }) => ({
|
||||
@@ -114,8 +114,8 @@ const OpenDrawerButton = styled(IconButton)(({ theme }) => ({
|
||||
interface IProps {
|
||||
settings: IReaderSettings;
|
||||
setSettingValue: (key: keyof IReaderSettings, value: string | boolean) => void;
|
||||
manga: IManga | IMangaCard;
|
||||
chapter: IChapter;
|
||||
manga: TManga;
|
||||
chapter: TChapter;
|
||||
curPage: number;
|
||||
scrollToPage: (page: number) => void;
|
||||
openNextChapter: (offset: ChapterOffset, setHistory: (nextChapterIndex: number) => void) => Promise<void>;
|
||||
@@ -303,7 +303,7 @@ export default function ReaderNavBar(props: IProps) {
|
||||
<IconButton
|
||||
title={t('reader.button.previous_chapter')}
|
||||
sx={{ gridArea: 'pre' }}
|
||||
disabled={disableChapterNavButtons || chapter.index <= 1}
|
||||
disabled={disableChapterNavButtons || chapter.sourceOrder <= 1}
|
||||
onClick={() =>
|
||||
openNextChapter(ChapterOffset.PREV, (prevChapterIndex) => {
|
||||
navigate(`/manga/${manga.id}/chapter/${prevChapterIndex}`, {
|
||||
@@ -321,11 +321,11 @@ export default function ReaderNavBar(props: IProps) {
|
||||
<FormControl
|
||||
sx={{ gridArea: 'current' }}
|
||||
size="small"
|
||||
disabled={disableChapterNavButtons || chapter.index < 1}
|
||||
disabled={disableChapterNavButtons || chapter.sourceOrder < 1}
|
||||
>
|
||||
<Select
|
||||
MenuProps={MenuProps}
|
||||
value={chapter.index >= 1 ? chapter.index : ''}
|
||||
value={chapter.sourceOrder >= 1 ? chapter.sourceOrder : ''}
|
||||
displayEmpty
|
||||
onChange={({ target: { value: selectedChapter } }) => {
|
||||
navigate(`/manga/${manga.id}/chapter/${selectedChapter}`, {
|
||||
@@ -337,7 +337,7 @@ export default function ReaderNavBar(props: IProps) {
|
||||
});
|
||||
}}
|
||||
>
|
||||
{Array(Math.max(0, chapter.chapterCount))
|
||||
{Array(Math.max(0, manga.chapters.totalCount))
|
||||
.fill(1)
|
||||
.map((ignoreValue, index) => (
|
||||
<MenuItem key={`Chapter#${index + 1}`} value={index + 1}>{`${t(
|
||||
@@ -351,8 +351,8 @@ export default function ReaderNavBar(props: IProps) {
|
||||
sx={{ gridArea: 'next' }}
|
||||
disabled={
|
||||
disableChapterNavButtons ||
|
||||
chapter.index < 1 ||
|
||||
chapter.index >= chapter.chapterCount
|
||||
chapter.sourceOrder < 1 ||
|
||||
chapter.sourceOrder >= manga.chapters.totalCount
|
||||
}
|
||||
onClick={() => {
|
||||
openNextChapter(ChapterOffset.NEXT, (nextChapterIndex) =>
|
||||
|
||||
@@ -16,7 +16,7 @@ import Checkbox from '@mui/material/Checkbox';
|
||||
import FormControlLabel from '@mui/material/FormControlLabel';
|
||||
import FormGroup from '@mui/material/FormGroup';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import requestManager from '@/lib/RequestManager';
|
||||
import requestManager from '@/lib/requests/RequestManager.ts';
|
||||
|
||||
interface IProps {
|
||||
open: boolean;
|
||||
@@ -29,8 +29,10 @@ export default function CategorySelect(props: IProps) {
|
||||
|
||||
const { open, setOpen, mangaId } = props;
|
||||
|
||||
const { data: mangaCategoriesData, mutate } = requestManager.useGetMangaCategories(mangaId);
|
||||
const { data: categoriesData } = requestManager.useGetCategories();
|
||||
const { data: mangaResult } = requestManager.useGetManga(mangaId);
|
||||
const { data } = requestManager.useGetCategories();
|
||||
const categoriesData = data?.categories.nodes;
|
||||
const [triggerMutate] = requestManager.useUpdateMangaCategories();
|
||||
|
||||
const allCategories = useMemo(() => {
|
||||
const cats = [...(categoriesData ?? [])]; // make copy
|
||||
@@ -40,7 +42,7 @@ export default function CategorySelect(props: IProps) {
|
||||
return cats;
|
||||
}, [categoriesData]);
|
||||
|
||||
const selectedIds = mangaCategoriesData?.map((c) => c.id) ?? [];
|
||||
const selectedIds = mangaResult?.manga.categories.nodes.map((c) => c.id) ?? [];
|
||||
|
||||
const handleCancel = () => {
|
||||
setOpen(false);
|
||||
@@ -53,10 +55,18 @@ export default function CategorySelect(props: IProps) {
|
||||
const handleChange = (event: React.ChangeEvent<HTMLInputElement>, categoryId: number) => {
|
||||
const { checked } = event.target as HTMLInputElement;
|
||||
|
||||
(checked
|
||||
? requestManager.addMangaToCategory(mangaId, categoryId)
|
||||
: requestManager.removeMangaFromCategory(mangaId, categoryId)
|
||||
).response.then(() => mutate());
|
||||
// TODO - update to only update categories when clicking OK - can now be updated in one go with graphql
|
||||
triggerMutate({
|
||||
variables: {
|
||||
input: {
|
||||
id: mangaId,
|
||||
patch: {
|
||||
addToCategories: checked ? [categoryId] : [],
|
||||
removeFromCategories: !checked ? [categoryId] : [],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
*/
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { IMangaCard } from '@/typings';
|
||||
import MangaGrid, { IMangaGridProps } from '@/components/MangaGrid';
|
||||
import { TPartialManga } from '@/typings.ts';
|
||||
|
||||
function filterManga(mangas: IMangaCard[]): IMangaCard[] {
|
||||
function filterManga(mangas: TPartialManga[]): TPartialManga[] {
|
||||
return mangas;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import FilterListIcon from '@mui/icons-material/FilterList';
|
||||
import { Button, Stack, Box } from '@mui/material';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ISourceFilters, IState } from '@/typings';
|
||||
import { SourceFilters } from '@/typings';
|
||||
import OptionsPanel from '@/components/molecules/OptionsPanel';
|
||||
import CheckBoxFilter from '@/components/source/filters/CheckBoxFilter';
|
||||
import HeaderFilter from '@/components/source/filters/HeaderFilter';
|
||||
@@ -25,14 +25,14 @@ import SeperatorFilter from '@/components/source/filters/SeparatorFilter';
|
||||
import StyledFab from '@/components/util/StyledFab';
|
||||
|
||||
interface IFilters {
|
||||
sourceFilter: ISourceFilters[];
|
||||
sourceFilter: SourceFilters[];
|
||||
updateFilterValue: Function;
|
||||
group: number | undefined;
|
||||
update: any;
|
||||
}
|
||||
|
||||
interface IFilters1 {
|
||||
sourceFilter: ISourceFilters[];
|
||||
sourceFilter: SourceFilters[];
|
||||
updateFilterValue: Function;
|
||||
resetFilterValue: Function;
|
||||
setTriggerUpdate: Function;
|
||||
@@ -42,85 +42,89 @@ interface IFilters1 {
|
||||
export function Options({ sourceFilter, group, updateFilterValue, update }: IFilters) {
|
||||
return (
|
||||
<Stack key={`filters ${group}`}>
|
||||
{sourceFilter.map((e: ISourceFilters, index) => {
|
||||
{sourceFilter.map((e, index) => {
|
||||
let checkif = update.find(
|
||||
(el: { group: number | undefined; position: number }) =>
|
||||
el.group === group && el.position === index,
|
||||
);
|
||||
checkif = checkif ? checkif.state : checkif;
|
||||
switch (e.type) {
|
||||
case 'CheckBox':
|
||||
case 'CheckBoxFilter':
|
||||
return (
|
||||
<CheckBoxFilter
|
||||
key={`filters ${e.filter.name}`}
|
||||
name={e.filter.name}
|
||||
state={checkif != null ? checkif === 'true' : (e.filter.state as boolean)}
|
||||
key={`filters ${e.name}`}
|
||||
name={e.name}
|
||||
state={checkif ?? e.CheckBoxFilterDefault}
|
||||
position={index}
|
||||
group={group}
|
||||
updateFilterValue={updateFilterValue}
|
||||
update={update}
|
||||
/>
|
||||
);
|
||||
case 'Group':
|
||||
case 'GroupFilter':
|
||||
return (
|
||||
<GroupFilter
|
||||
key={`filters ${e.filter.name}`}
|
||||
name={e.filter.name}
|
||||
state={e.filter.state as ISourceFilters[]}
|
||||
key={`filters ${e.name}`}
|
||||
name={e.name}
|
||||
state={e.filters}
|
||||
position={index}
|
||||
updateFilterValue={updateFilterValue}
|
||||
update={update}
|
||||
/>
|
||||
);
|
||||
case 'Header':
|
||||
return <HeaderFilter key={`filters ${e.filter.name}`} name={e.filter.name} />;
|
||||
case 'Select':
|
||||
case 'HeaderFilter':
|
||||
return <HeaderFilter key={`filters ${e.name}`} name={e.name} />;
|
||||
case 'SelectFilter':
|
||||
return (
|
||||
<SelectFilter
|
||||
key={`filters ${e.filter.name}`}
|
||||
name={e.filter.name}
|
||||
values={e.filter.displayValues}
|
||||
state={checkif != null ? parseInt(checkif, 10) : (e.filter.state as number)}
|
||||
selected={e.filter.selected}
|
||||
key={`filters ${e.name}`}
|
||||
name={e.name}
|
||||
values={e.values}
|
||||
state={checkif != null ? parseInt(checkif, 10) : e.SelectFilterDefault}
|
||||
position={index}
|
||||
group={group}
|
||||
updateFilterValue={updateFilterValue}
|
||||
update={update}
|
||||
/>
|
||||
);
|
||||
case 'Separator':
|
||||
return <SeperatorFilter key={`filters ${e.filter.name}`} name={e.filter.name} />;
|
||||
case 'Sort':
|
||||
case 'SeparatorFilter':
|
||||
return <SeperatorFilter key={`filters ${e.name}`} name={e.name} />;
|
||||
case 'SortFilter':
|
||||
return (
|
||||
<SortFilter
|
||||
key={`filters ${e.filter.name}`}
|
||||
name={e.filter.name}
|
||||
values={e.filter.values}
|
||||
state={checkif ? JSON.parse(checkif) : { ...(e.filter.state as IState) }}
|
||||
key={`filters ${e.name}`}
|
||||
name={e.name}
|
||||
values={e.values}
|
||||
state={
|
||||
checkif ?? {
|
||||
ascending: e.SortFilterDefault?.ascending,
|
||||
index: e.SortFilterDefault?.index,
|
||||
}
|
||||
}
|
||||
position={index}
|
||||
group={group}
|
||||
updateFilterValue={updateFilterValue}
|
||||
update={update}
|
||||
/>
|
||||
);
|
||||
case 'Text':
|
||||
case 'TextFilter':
|
||||
return (
|
||||
<TextFilter
|
||||
key={`filters ${e.filter.name}`}
|
||||
name={e.filter.name}
|
||||
state={checkif ?? (e.filter.state as string)}
|
||||
key={`filters ${e.name}`}
|
||||
name={e.name}
|
||||
state={checkif ?? e.TextFilterDefault}
|
||||
position={index}
|
||||
group={group}
|
||||
updateFilterValue={updateFilterValue}
|
||||
update={update}
|
||||
/>
|
||||
);
|
||||
case 'TriState':
|
||||
case 'TriStateFilter':
|
||||
return (
|
||||
<TriStateFilter
|
||||
key={`filters ${e.filter.name}`}
|
||||
name={e.filter.name}
|
||||
state={checkif != null ? parseInt(checkif, 10) : (e.filter.state as number)}
|
||||
key={`filters ${e.name}`}
|
||||
name={e.name}
|
||||
state={checkif != null ? checkif : e.TriStateFilterDefault}
|
||||
position={index}
|
||||
group={group}
|
||||
updateFilterValue={updateFilterValue}
|
||||
@@ -128,7 +132,7 @@ export function Options({ sourceFilter, group, updateFilterValue, update }: IFil
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return <Box key={`${e.filter.name}null`} />;
|
||||
throw new Error(`Unknown source filter "${e}"`);
|
||||
}
|
||||
})}
|
||||
</Stack>
|
||||
|
||||
@@ -27,7 +27,7 @@ const CheckBoxFilter: React.FC<Props> = (props: Props) => {
|
||||
const upd = update.filter(
|
||||
(e: { position: number; group: number | undefined }) => !(position === e.position && group === e.group),
|
||||
);
|
||||
updateFilterValue([...upd, { position, state: event.target.checked.toString(), group }]);
|
||||
updateFilterValue([...upd, { type: 'checkBoxState', position, state: event.target.checked, group }]);
|
||||
};
|
||||
|
||||
if (state !== undefined) {
|
||||
|
||||
@@ -10,11 +10,11 @@ import { ExpandLess, ExpandMore } from '@mui/icons-material';
|
||||
import { Collapse, ListItemButton, ListItemText, Stack, Box } from '@mui/material';
|
||||
import React from 'react';
|
||||
// eslint-disable-next-line import/no-cycle
|
||||
import { ISourceFilters } from '@/typings';
|
||||
import { ExtractByKeyValue, SourceFilters } from '@/typings';
|
||||
import { Options } from '@/components/source/SourceOptions';
|
||||
|
||||
interface Props {
|
||||
state: ISourceFilters[];
|
||||
state: ExtractByKeyValue<SourceFilters, '__typename', 'GroupFilter'>['filters'];
|
||||
name: string;
|
||||
position: number;
|
||||
updateFilterValue: Function;
|
||||
@@ -36,7 +36,7 @@ const GroupFilter: React.FC<Props> = (props: Props) => {
|
||||
{/* Container is moved outside 2, so content has to go inside 4 */}
|
||||
<Stack sx={{ mx: 4 }}>
|
||||
<Options
|
||||
sourceFilter={state}
|
||||
sourceFilter={state as SourceFilters[]}
|
||||
group={position}
|
||||
updateFilterValue={updateFilterValue}
|
||||
update={update}
|
||||
|
||||
@@ -16,56 +16,12 @@ interface Props {
|
||||
values: any;
|
||||
name: string;
|
||||
state: number;
|
||||
selected: Selected | undefined;
|
||||
position: number;
|
||||
updateFilterValue: Function;
|
||||
group: number | undefined;
|
||||
update: any;
|
||||
}
|
||||
|
||||
interface Selected {
|
||||
displayname: string;
|
||||
value: string;
|
||||
_value: string;
|
||||
}
|
||||
|
||||
function hasSelect(
|
||||
values: Selected[],
|
||||
name: string,
|
||||
state: number,
|
||||
position: number,
|
||||
updateFilterValue: Function,
|
||||
update: any,
|
||||
group?: number,
|
||||
) {
|
||||
const [val, setval] = React.useState(state);
|
||||
if (values) {
|
||||
const handleChange = (event: { target: { name: any; value: any } }) => {
|
||||
const vall = values.map((e) => e.displayname).indexOf(`${event.target.value}`);
|
||||
setval(vall);
|
||||
const upd = update.filter(
|
||||
(e: { position: number; group: number | undefined }) => !(position === e.position && group === e.group),
|
||||
);
|
||||
updateFilterValue([...upd, { position, state: vall.toString(), group }]);
|
||||
};
|
||||
|
||||
const rett = values.map((e: Selected) => (
|
||||
<MenuItem key={`${name} ${e.displayname}`} value={e.displayname}>
|
||||
{e.displayname}
|
||||
</MenuItem>
|
||||
));
|
||||
return (
|
||||
<FormControl sx={{ my: 1 }} variant="standard">
|
||||
<InputLabel>{name}</InputLabel>
|
||||
<Select name={name} value={values[val].displayname} label={name} onChange={handleChange}>
|
||||
{rett}
|
||||
</Select>
|
||||
</FormControl>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function noSelect(
|
||||
values: string[],
|
||||
name: string,
|
||||
@@ -84,7 +40,7 @@ function noSelect(
|
||||
const upd = update.filter(
|
||||
(e: { position: number; group: number | undefined }) => !(position === e.position && group === e.group),
|
||||
);
|
||||
updateFilterValue([...upd, { position, state: vall.toString(), group }]);
|
||||
updateFilterValue([...upd, { type: 'selectState', position, state: vall, group }]);
|
||||
};
|
||||
|
||||
const rett = values.map((value: string) => (
|
||||
@@ -104,21 +60,7 @@ function noSelect(
|
||||
return null;
|
||||
}
|
||||
|
||||
const SelectFilter: React.FC<Props> = ({
|
||||
values,
|
||||
name,
|
||||
state,
|
||||
selected,
|
||||
position,
|
||||
updateFilterValue,
|
||||
update,
|
||||
group,
|
||||
}) => {
|
||||
if (selected === undefined) {
|
||||
return noSelect(values, name, state, position, updateFilterValue, update, group);
|
||||
}
|
||||
|
||||
return hasSelect(values, name, state, position, updateFilterValue, update, group);
|
||||
};
|
||||
const SelectFilter: React.FC<Props> = ({ values, name, state, position, updateFilterValue, update, group }) =>
|
||||
noSelect(values, name, state, position, updateFilterValue, update, group);
|
||||
|
||||
export default SelectFilter;
|
||||
|
||||
@@ -9,13 +9,13 @@
|
||||
import { ExpandLess, ExpandMore } from '@mui/icons-material';
|
||||
import { Collapse, ListItemButton, ListItemText, Stack, Box } from '@mui/material';
|
||||
import React from 'react';
|
||||
import { IState } from '@/typings';
|
||||
import SortRadioInput from '@/components/atoms/SortRadioInput';
|
||||
import { SortSelectionInput } from '@/lib/graphql/generated/graphql.ts';
|
||||
|
||||
interface Props {
|
||||
values: any;
|
||||
name: string;
|
||||
state: IState;
|
||||
state: SortSelectionInput;
|
||||
position: number;
|
||||
group: number | undefined;
|
||||
updateFilterValue: Function;
|
||||
@@ -45,7 +45,7 @@ const SortFilter: React.FC<Props> = (props: Props) => {
|
||||
const upd = update.filter(
|
||||
(e: { position: number; group: number | undefined }) => !(position === e.position && group === e.group),
|
||||
);
|
||||
updateFilterValue([...upd, { position, state: JSON.stringify(tmp), group }]);
|
||||
updateFilterValue([...upd, { type: 'sortState', position, state: tmp, group }]);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -29,7 +29,7 @@ const TextFilter: React.FC<Props> = (props) => {
|
||||
const upd = update.filter(
|
||||
(el: { position: number; group: number | undefined }) => !(position === el.position && group === el.group),
|
||||
);
|
||||
updateFilterValue([...upd, { position, state: inputText, group }]);
|
||||
updateFilterValue([...upd, { type: 'textState', position, state: inputText, group }]);
|
||||
}, [inputText]);
|
||||
|
||||
if (state !== undefined) {
|
||||
|
||||
@@ -8,9 +8,10 @@
|
||||
|
||||
import React from 'react';
|
||||
import ThreeStateCheckboxInput from '@/components/atoms/ThreeStateCheckboxInput';
|
||||
import { TriState } from '@/lib/graphql/generated/graphql.ts';
|
||||
|
||||
interface Props {
|
||||
state: number;
|
||||
state: TriState;
|
||||
name: string;
|
||||
position: number;
|
||||
group: number | undefined;
|
||||
@@ -18,9 +19,35 @@ interface Props {
|
||||
update: any;
|
||||
}
|
||||
|
||||
const convertTriStateToNumber = (triState: TriState): number => {
|
||||
switch (triState) {
|
||||
case TriState.Ignore:
|
||||
return 0;
|
||||
case TriState.Include:
|
||||
return 1;
|
||||
case TriState.Exclude:
|
||||
return 2;
|
||||
default:
|
||||
throw new Error(`Unexpected TriState ${triState}`);
|
||||
}
|
||||
};
|
||||
|
||||
const convertNumberToTriState = (state: number): TriState => {
|
||||
switch (state) {
|
||||
case 0:
|
||||
return TriState.Ignore;
|
||||
case 1:
|
||||
return TriState.Include;
|
||||
case 2:
|
||||
return TriState.Exclude;
|
||||
default:
|
||||
throw new Error(`Unexpected state number ${state}`);
|
||||
}
|
||||
};
|
||||
|
||||
const TriStateFilter: React.FC<Props> = (props) => {
|
||||
const { state, name, position, group, updateFilterValue, update } = props;
|
||||
const [val, setval] = React.useState<number>(Number(state));
|
||||
const [val, setval] = React.useState(convertTriStateToNumber(state));
|
||||
|
||||
const handleChange = (checked: boolean | null | undefined) => {
|
||||
// eslint-disable-next-line no-nested-ternary
|
||||
@@ -32,8 +59,9 @@ const TriStateFilter: React.FC<Props> = (props) => {
|
||||
updateFilterValue([
|
||||
...upd,
|
||||
{
|
||||
type: 'triState',
|
||||
position,
|
||||
state: newState.toString(),
|
||||
state: convertNumberToTriState(newState),
|
||||
group,
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -22,22 +22,29 @@ import { EditTextPreferenceProps } from '@/typings';
|
||||
export default function EditTextPreference(props: EditTextPreferenceProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { title, summary, dialogTitle, dialogMessage, currentValue, updateValue } = props;
|
||||
const {
|
||||
EditTextPreferenceTitle: title,
|
||||
summary,
|
||||
dialogTitle,
|
||||
dialogMessage,
|
||||
EditTextPreferenceCurrentValue: currentValue,
|
||||
updateValue,
|
||||
} = props;
|
||||
|
||||
const [internalCurrentValue, setInternalCurrentValue] = useState<string>(currentValue);
|
||||
const [internalCurrentValue, setInternalCurrentValue] = useState<string>(currentValue ?? '');
|
||||
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
|
||||
|
||||
const handleDialogCancel = () => {
|
||||
setDialogOpen(false);
|
||||
|
||||
// reset the dialog
|
||||
setInternalCurrentValue(currentValue);
|
||||
setInternalCurrentValue(currentValue ?? '');
|
||||
};
|
||||
|
||||
const handleDialogSubmit = () => {
|
||||
setDialogOpen(false);
|
||||
|
||||
updateValue(internalCurrentValue);
|
||||
updateValue('editTextState', internalCurrentValue);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -85,12 +85,20 @@ function ListDialog(props: IListDialogProps) {
|
||||
}
|
||||
|
||||
export default function ListPreference(props: ListPreferenceProps) {
|
||||
const { title, summary, currentValue, updateValue, entryValues, entries } = props;
|
||||
const [internalCurrentValue, setInternalCurrentValue] = useState<string>(currentValue);
|
||||
const {
|
||||
ListPreferenceTitle: title,
|
||||
summary,
|
||||
ListPreferenceCurrentValue: currentValue,
|
||||
ListPreferenceDefault: defaultValue,
|
||||
updateValue,
|
||||
entryValues,
|
||||
entries,
|
||||
} = props;
|
||||
const [internalCurrentValue, setInternalCurrentValue] = useState(currentValue ?? defaultValue ?? '');
|
||||
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
setInternalCurrentValue(currentValue);
|
||||
setInternalCurrentValue(currentValue ?? defaultValue ?? '');
|
||||
}, [currentValue]);
|
||||
|
||||
const findEntryOf = (value: string) => {
|
||||
@@ -104,6 +112,10 @@ export default function ListPreference(props: ListPreferenceProps) {
|
||||
};
|
||||
|
||||
const getSummary = () => {
|
||||
if (currentValue == null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (summary === '%s') {
|
||||
return findEntryOf(currentValue);
|
||||
}
|
||||
@@ -112,7 +124,7 @@ export default function ListPreference(props: ListPreferenceProps) {
|
||||
|
||||
const handleDialogClose = (newValue: string | null) => {
|
||||
if (newValue !== null) {
|
||||
updateValue(findEntryValueOf(newValue));
|
||||
updateValue('listState', findEntryValueOf(newValue));
|
||||
|
||||
// appear smooth
|
||||
setInternalCurrentValue(newValue);
|
||||
@@ -127,7 +139,7 @@ export default function ListPreference(props: ListPreferenceProps) {
|
||||
<ListItemText primary={title} secondary={getSummary()} />
|
||||
</ListItemButton>
|
||||
<ListDialog
|
||||
title={title}
|
||||
title={title ?? ''}
|
||||
open={dialogOpen}
|
||||
onClose={handleDialogClose}
|
||||
value={findEntryOf(internalCurrentValue)}
|
||||
|
||||
@@ -99,32 +99,40 @@ function ListDialog(props: IListDialogProps) {
|
||||
}
|
||||
|
||||
export default function MultiSelectListPreference(props: MultiSelectListPreferenceProps) {
|
||||
const { title, summary, currentValue, updateValue, entryValues, entries } = props;
|
||||
const [internalCurrentValue, setInternalCurrentValue] = useState<string[]>(currentValue);
|
||||
const {
|
||||
MultiSelectListPreferenceTitle: title,
|
||||
summary,
|
||||
MultiSelectListPreferenceCurrentValue: currentValue,
|
||||
MultiSelectListPreferenceDefault: defaultValue,
|
||||
updateValue,
|
||||
entryValues,
|
||||
entries,
|
||||
} = props;
|
||||
const [internalCurrentValue, setInternalCurrentValue] = useState(currentValue ?? defaultValue);
|
||||
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
setInternalCurrentValue(currentValue);
|
||||
}, [currentValue]);
|
||||
|
||||
const findEntriesOf = (values: string[]) =>
|
||||
values.map((value) => {
|
||||
const findEntriesOf = (values?: string[] | null) =>
|
||||
values?.map((value) => {
|
||||
const idx = entryValues.indexOf(value);
|
||||
return entries[idx];
|
||||
});
|
||||
}) ?? [];
|
||||
|
||||
const findEntryValuesOf = (values: string[]) =>
|
||||
values.map((value) => {
|
||||
const findEntryValuesOf = (values?: string[] | null) =>
|
||||
values?.map((value) => {
|
||||
const idx = entries.indexOf(value);
|
||||
return entryValues[idx];
|
||||
});
|
||||
}) ?? [];
|
||||
|
||||
const getSummary = () => summary;
|
||||
|
||||
const handleDialogClose = (newValue: string[] | null) => {
|
||||
if (newValue !== null) {
|
||||
// console.log(newValue);
|
||||
updateValue(findEntryValuesOf(newValue));
|
||||
updateValue('multiSelectState', findEntryValuesOf(newValue));
|
||||
|
||||
// appear smooth
|
||||
setInternalCurrentValue(newValue);
|
||||
@@ -139,7 +147,7 @@ export default function MultiSelectListPreference(props: MultiSelectListPreferen
|
||||
<ListItemText primary={title} secondary={getSummary()} />
|
||||
</ListItemButton>
|
||||
<ListDialog
|
||||
title={title}
|
||||
title={title ?? ''}
|
||||
open={dialogOpen}
|
||||
onClose={handleDialogClose}
|
||||
selectedValues={findEntriesOf(internalCurrentValue)}
|
||||
|
||||
@@ -21,23 +21,48 @@ function getTwoStateType(type: 'Checkbox' | 'Switch') {
|
||||
return Checkbox;
|
||||
}
|
||||
|
||||
const getTwoStateValues = (
|
||||
props: TwoStatePreferenceProps,
|
||||
): {
|
||||
title: string;
|
||||
defaultValue: boolean;
|
||||
currentValue?: boolean | null | undefined;
|
||||
} => {
|
||||
if (props.type === 'CheckBoxPreference') {
|
||||
return {
|
||||
title: props.CheckBoxTitle,
|
||||
defaultValue: props.CheckBoxDefault,
|
||||
currentValue: props.CheckBoxCheckBoxCurrentValue,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
title: props.SwitchPreferenceTitle,
|
||||
defaultValue: props.SwitchPreferenceDefault,
|
||||
currentValue: props.SwitchPreferenceCurrentValue,
|
||||
};
|
||||
};
|
||||
|
||||
function TwoSatePreference(props: TwoStatePreferenceProps) {
|
||||
const { title, summary, currentValue, updateValue, type } = props;
|
||||
const [internalCurrentValue, setInternalCurrentValue] = useState<boolean>(currentValue);
|
||||
const { title, defaultValue, currentValue, summary, updateValue, twoStateType } = {
|
||||
...props,
|
||||
...getTwoStateValues(props),
|
||||
};
|
||||
const [internalCurrentValue, setInternalCurrentValue] = useState(currentValue ?? defaultValue);
|
||||
|
||||
useEffect(() => {
|
||||
setInternalCurrentValue(currentValue);
|
||||
setInternalCurrentValue(currentValue ?? defaultValue);
|
||||
}, [currentValue]);
|
||||
|
||||
return (
|
||||
<ListItem>
|
||||
<ListItemText primary={title} secondary={summary} />
|
||||
<ListItemSecondaryAction>
|
||||
{createElement(getTwoStateType(type), {
|
||||
{createElement(getTwoStateType(twoStateType), {
|
||||
edge: 'end',
|
||||
checked: internalCurrentValue,
|
||||
onChange: () => {
|
||||
updateValue(!currentValue);
|
||||
updateValue(twoStateType === 'Switch' ? 'switchState' : 'checkBoxState', !currentValue);
|
||||
|
||||
// appear smooth
|
||||
setInternalCurrentValue(!currentValue);
|
||||
@@ -49,11 +74,11 @@ function TwoSatePreference(props: TwoStatePreferenceProps) {
|
||||
}
|
||||
|
||||
export function CheckBoxPreference(props: CheckBoxPreferenceProps) {
|
||||
return <TwoSatePreference {...props} type="Checkbox" />;
|
||||
return <TwoSatePreference {...props} twoStateType="Checkbox" />;
|
||||
}
|
||||
|
||||
export function SwitchPreferenceCompat(props: SwitchPreferenceCompatProps) {
|
||||
return <TwoSatePreference {...props} type="Switch" />;
|
||||
return <TwoSatePreference {...props} twoStateType="Switch" />;
|
||||
}
|
||||
|
||||
export default { CheckBoxPreference, SwitchPreferenceCompat };
|
||||
|
||||
@@ -22,7 +22,7 @@ function getRandomErrorFace() {
|
||||
|
||||
interface IProps {
|
||||
message: string;
|
||||
messageExtra?: JSX.Element;
|
||||
messageExtra?: JSX.Element | string;
|
||||
}
|
||||
|
||||
export default function EmptyView({ message, messageExtra }: IProps) {
|
||||
|
||||
@@ -268,7 +268,15 @@
|
||||
"error": {
|
||||
"label": {
|
||||
"empty": "Your library is empty",
|
||||
"no_matches": "No manga matches this filter"
|
||||
"no_matches": "No manga matches this filter",
|
||||
"add_to_library": "Added manga to library!",
|
||||
"remove_from_library": "Could not add manga to library!"
|
||||
}
|
||||
},
|
||||
"info": {
|
||||
"label": {
|
||||
"added_to_library": "Added manga to library!",
|
||||
"removed_from_library": "Removed manga from library!"
|
||||
}
|
||||
},
|
||||
"option": {
|
||||
|
||||
@@ -1,700 +0,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 { AxiosInstance, AxiosRequestConfig } from 'axios';
|
||||
import useSWR, { Middleware, SWRConfiguration, SWRResponse } from 'swr';
|
||||
import useSWRInfinite, { SWRInfiniteConfiguration, SWRInfiniteResponse } from 'swr/infinite';
|
||||
import {
|
||||
BackupValidationResult,
|
||||
BatchChaptersChange,
|
||||
IAbout,
|
||||
ICategory,
|
||||
IChapter,
|
||||
IExtension,
|
||||
IManga,
|
||||
IMangaChapter,
|
||||
IncludeInGlobalUpdate,
|
||||
ISource,
|
||||
ISourceFilters,
|
||||
IUpdateStatus,
|
||||
Metadata,
|
||||
PaginatedList,
|
||||
PaginatedMangaList,
|
||||
SourcePreferences,
|
||||
SourceSearchResult,
|
||||
UpdateCheck,
|
||||
} from '@/typings';
|
||||
import { HttpMethod as DefaultHttpMethod, IRestClient, RestClient } from '@/lib/RestClient';
|
||||
import storage from '@/util/localStorage';
|
||||
|
||||
enum SWRHttpMethod {
|
||||
SWR_GET,
|
||||
SWR_GET_INFINITE,
|
||||
SWR_POST,
|
||||
SWR_POST_INFINITE,
|
||||
}
|
||||
|
||||
type HttpMethodType = DefaultHttpMethod | SWRHttpMethod;
|
||||
const HttpMethod = { ...SWRHttpMethod, ...DefaultHttpMethod };
|
||||
|
||||
type RequestOption = { doOnlineFetch?: boolean };
|
||||
|
||||
type CustomSWROptions<Data> = {
|
||||
skipRequest?: boolean;
|
||||
getEndpoint?: (index: number, previousData: Data | null) => string | null;
|
||||
disableCache?: boolean;
|
||||
};
|
||||
|
||||
type SWROptions<Data = any, Error = any> = SWRConfiguration<Data, Error> & CustomSWROptions<Data>;
|
||||
type SWRInfiniteOptions<Data = any, Error = any> = SWRInfiniteConfiguration<Data, Error> & CustomSWROptions<Data>;
|
||||
|
||||
type SWRInfiniteResponseLoadInfo = {
|
||||
isInitialLoad: boolean;
|
||||
isLoadMore: boolean;
|
||||
};
|
||||
type AbortableRequest = { abortRequest: AbortController['abort'] };
|
||||
export type AbortableAxiosResponse<Data = any> = { response: Promise<Data> } & AbortableRequest;
|
||||
export type AbortableSWRResponse<Data = any, Error = any> = SWRResponse<Data, Error> & AbortableRequest;
|
||||
export type AbortableSWRInfiniteResponse<Data = any, Error = any> = SWRInfiniteResponse<Data, Error> &
|
||||
AbortableRequest &
|
||||
SWRInfiniteResponseLoadInfo;
|
||||
|
||||
const isLoadingMore = (swrResult: SWRInfiniteResponse): boolean => {
|
||||
const isNextPageMissing = !!swrResult.data && typeof swrResult.data[swrResult.size - 1] === 'undefined';
|
||||
const isRequestActive = swrResult.isValidating;
|
||||
// SWR "isLoading" state is only updated for the first load, for every subsequent load it's "false"
|
||||
return !swrResult.isLoading && swrResult.size > 0 && isNextPageMissing && isRequestActive;
|
||||
};
|
||||
|
||||
const disableSwrInfiniteCache: Middleware = (useSWRNext) => (key, fetcher, config) => {
|
||||
const swr = useSWRNext(key, fetcher, config) as unknown as SWRInfiniteResponse;
|
||||
const { size, data, isLoading, isValidating } = swr;
|
||||
const isActuallyValidating = !isLoading && !isLoadingMore(swr) && isValidating;
|
||||
return {
|
||||
...swr,
|
||||
isLoading: isActuallyValidating ? true : isLoading,
|
||||
data: isActuallyValidating ? undefined : data,
|
||||
size: isActuallyValidating ? 1 : size,
|
||||
} as SWRResponse;
|
||||
};
|
||||
|
||||
const disableSwrCache: Middleware = (useSWRNext) => (key, fetcher, config) => {
|
||||
const swr = useSWRNext(key, fetcher, config);
|
||||
const { data, isLoading, isValidating } = swr;
|
||||
return { ...swr, isLoading: isValidating ? true : isLoading, data: isValidating ? undefined : data };
|
||||
};
|
||||
|
||||
// the following endpoints have not been implemented:
|
||||
// - PUT /api/v1/manga/{mangaId}/chapter/{chapterIndex} - modify chapter # PATCH endpoint used instead
|
||||
// - POST /api/v1/backup/import - import backup # "import backup file" endpoint used instead
|
||||
// - POST /api/v1/backup/validate - validate backup # "validate backup file" endpoint used instead
|
||||
// - GET /api/v1/backup/export - export backup # no function needed, url gets called via link triggering the download
|
||||
export class RequestManager {
|
||||
public static readonly API_VERSION = '/api/v1/';
|
||||
|
||||
private readonly restClient: RestClient = new RestClient();
|
||||
|
||||
public getClient(): IRestClient {
|
||||
return this.restClient;
|
||||
}
|
||||
|
||||
public updateClient(config: Partial<AxiosInstance['defaults']>): void {
|
||||
this.restClient.updateConfig(config);
|
||||
}
|
||||
|
||||
public getBaseUrl(): string {
|
||||
return this.restClient.getClient().defaults.baseURL!;
|
||||
}
|
||||
|
||||
public getWebSocketBaseUrl(): string {
|
||||
return this.getBaseUrl().replace('http', 'ws');
|
||||
}
|
||||
|
||||
public getValidWebSocketUrl(path: string, apiVersion = RequestManager.API_VERSION): string {
|
||||
return `${this.getWebSocketBaseUrl()}${apiVersion}${path}`;
|
||||
}
|
||||
|
||||
public getUpdateWebSocket(): WebSocket {
|
||||
return new WebSocket(this.getValidWebSocketUrl('update'));
|
||||
}
|
||||
|
||||
public getDownloadWebSocket(): WebSocket {
|
||||
return new WebSocket(this.getValidWebSocketUrl('downloads'));
|
||||
}
|
||||
|
||||
public getValidUrlFor(endpoint: string, apiVersion: string = RequestManager.API_VERSION): string {
|
||||
return `${this.getBaseUrl()}${apiVersion}${endpoint}`;
|
||||
}
|
||||
|
||||
public getValidImgUrlFor(imageUrl: string, apiVersion: string = ''): string {
|
||||
const useCache = storage.getItem('useCache', true);
|
||||
const useCacheQuery = `?useCache=${useCache}`;
|
||||
// server provided image urls already contain the api version
|
||||
return `${this.getValidUrlFor(imageUrl, apiVersion)}${useCacheQuery}`;
|
||||
}
|
||||
|
||||
private useSwr<
|
||||
Data = any,
|
||||
ErrorResponse = any,
|
||||
OptionsSWR extends SWROptions<Data, ErrorResponse> = SWROptions<Data, ErrorResponse>,
|
||||
>(
|
||||
url: string,
|
||||
httpMethod: DefaultHttpMethod,
|
||||
{
|
||||
data,
|
||||
axiosOptions,
|
||||
swrOptions,
|
||||
}: {
|
||||
data?: Data;
|
||||
axiosOptions?: AxiosRequestConfig;
|
||||
swrOptions?: OptionsSWR;
|
||||
} = {},
|
||||
): SWRResponse<Data, ErrorResponse> {
|
||||
const { skipRequest, disableCache, ...swrConfig } = swrOptions ?? {};
|
||||
|
||||
// in case "null" gets passed as the url, SWR won't do the request
|
||||
return useSWR(skipRequest ? null : url, {
|
||||
fetcher: (path: string) => this.restClient.fetcher(path, { data, httpMethod, config: axiosOptions }),
|
||||
use: disableCache ? [disableSwrCache] : undefined,
|
||||
...swrConfig,
|
||||
});
|
||||
}
|
||||
|
||||
public useSwrInfinite<
|
||||
Data = any,
|
||||
ErrorResponse = any,
|
||||
OptionsSWR extends SWRInfiniteOptions<Data, ErrorResponse> = SWRInfiniteOptions<Data, ErrorResponse>,
|
||||
>(
|
||||
getEndpoint: Required<CustomSWROptions<Data>>['getEndpoint'],
|
||||
httpMethod: DefaultHttpMethod,
|
||||
{
|
||||
data,
|
||||
axiosOptions,
|
||||
swrOptions,
|
||||
}: { data?: any; axiosOptions?: AxiosRequestConfig; swrOptions?: OptionsSWR } = {},
|
||||
): SWRInfiniteResponse<Data, ErrorResponse> & SWRInfiniteResponseLoadInfo {
|
||||
const { skipRequest, disableCache, ...swrConfig } = swrOptions ?? {};
|
||||
|
||||
// useSWRInfinite will (by default) revalidate the first page, to check if the other pages have to be revalidated as well
|
||||
const swrResult = useSWRInfinite<Data, ErrorResponse>(
|
||||
(index, previousData) => {
|
||||
const pageEndpoint = getEndpoint(index, previousData);
|
||||
return pageEndpoint !== null && !skipRequest ? this.getValidUrlFor(pageEndpoint) : null;
|
||||
},
|
||||
{
|
||||
fetcher: (path: string) => this.restClient.fetcher(path, { httpMethod, data, config: axiosOptions }),
|
||||
use: disableCache ? [disableSwrInfiniteCache] : undefined,
|
||||
...swrConfig,
|
||||
},
|
||||
);
|
||||
|
||||
const customSwrResult = {
|
||||
...swrResult,
|
||||
isInitialLoad: swrResult.isLoading,
|
||||
isLoadMore: isLoadingMore(swrResult),
|
||||
};
|
||||
customSwrResult.isLoading = customSwrResult.isInitialLoad || customSwrResult.isLoadMore;
|
||||
|
||||
return customSwrResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the actual server request.
|
||||
*
|
||||
* In case {@link HttpMethod.GET_SWR} gets passed, the "useSWR" hook gets called.
|
||||
* In case {@link HttpMethod.GET_SWR_INFINITE} gets passed, the "useSWRInfinite" hook gets called.
|
||||
* In that case "getEndpoint" has to be passed, which gets used over "endpoint"
|
||||
*
|
||||
* Pass "skipRequest" to make SWR skip sending the request to the server.
|
||||
* In case "formData" is passed, "data" gets ignored.
|
||||
*/
|
||||
private doRequest<
|
||||
Result extends AbortableAxiosResponse | AbortableSWRResponse | AbortableSWRInfiniteResponse,
|
||||
OptionsSWR extends SWROptions | SWRInfiniteOptions,
|
||||
>(
|
||||
httpMethod: HttpMethodType,
|
||||
endpoint: string,
|
||||
{
|
||||
apiVersion = RequestManager.API_VERSION,
|
||||
data: dataToSend,
|
||||
formData,
|
||||
axiosOptions,
|
||||
swrOptions,
|
||||
}: {
|
||||
apiVersion?: string;
|
||||
data?: any;
|
||||
formData?: { [key: string]: any };
|
||||
axiosOptions?: AxiosRequestConfig;
|
||||
swrOptions?: OptionsSWR;
|
||||
} = {},
|
||||
): Result {
|
||||
const url = `${apiVersion}${endpoint}`;
|
||||
|
||||
let data = dataToSend;
|
||||
if (formData) {
|
||||
data = new FormData();
|
||||
|
||||
Object.entries(formData).forEach(([key, value]) => {
|
||||
if (value !== undefined) data.append(key, value); // "append" automatically converts non string or blob values to strings
|
||||
});
|
||||
}
|
||||
|
||||
const abortController = new AbortController();
|
||||
const abortRequest = (reason?: any): void => {
|
||||
if (!abortController.signal.aborted) {
|
||||
abortController.abort(reason);
|
||||
}
|
||||
};
|
||||
const axiosOptionsWithAbortController = { ...axiosOptions, signal: abortController.signal };
|
||||
switch (httpMethod) {
|
||||
case HttpMethod.SWR_GET:
|
||||
return {
|
||||
...(this.useSwr(url, HttpMethod.GET, { axiosOptions, swrOptions }) as Result),
|
||||
abortRequest,
|
||||
};
|
||||
case HttpMethod.SWR_GET_INFINITE:
|
||||
// throw TypeError in case options aren't correctly passed
|
||||
return {
|
||||
...(this.useSwrInfinite(swrOptions!.getEndpoint!, HttpMethod.GET, {
|
||||
axiosOptions: axiosOptionsWithAbortController,
|
||||
swrOptions,
|
||||
}) as Result),
|
||||
abortRequest,
|
||||
};
|
||||
case SWRHttpMethod.SWR_POST_INFINITE:
|
||||
return {
|
||||
...(this.useSwrInfinite(swrOptions!.getEndpoint!, HttpMethod.POST, {
|
||||
data,
|
||||
axiosOptions: axiosOptionsWithAbortController,
|
||||
swrOptions,
|
||||
}) as Result),
|
||||
abortRequest,
|
||||
};
|
||||
case HttpMethod.SWR_POST:
|
||||
return {
|
||||
...(this.useSwr(url, HttpMethod.POST, {
|
||||
data,
|
||||
axiosOptions: axiosOptionsWithAbortController,
|
||||
swrOptions,
|
||||
}) as Result),
|
||||
controller: abortController,
|
||||
};
|
||||
default:
|
||||
return {
|
||||
response: this.restClient.fetcher(url, {
|
||||
data,
|
||||
httpMethod,
|
||||
config: axiosOptionsWithAbortController,
|
||||
checkResponseIsJson: false,
|
||||
}),
|
||||
abortRequest,
|
||||
} as Result;
|
||||
}
|
||||
}
|
||||
|
||||
public useGetGlobalMeta(swrOptions?: SWROptions<Metadata>): AbortableSWRResponse<Metadata> {
|
||||
return this.doRequest(HttpMethod.SWR_GET, 'meta', { swrOptions });
|
||||
}
|
||||
|
||||
public setGlobalMetadata(key: string, value: any): AbortableAxiosResponse {
|
||||
return this.doRequest(HttpMethod.PATCH, 'meta', { formData: { key, value } });
|
||||
}
|
||||
|
||||
public useGetAbout(swrOptions?: SWROptions<IAbout>): AbortableSWRResponse<IAbout> {
|
||||
return this.doRequest(HttpMethod.SWR_GET, 'settings/about', { swrOptions });
|
||||
}
|
||||
|
||||
public useCheckForUpdate(swrOptions?: SWROptions<UpdateCheck[]>): AbortableSWRResponse<UpdateCheck[]> {
|
||||
return this.doRequest(HttpMethod.SWR_GET, 'settings/check-update', { swrOptions });
|
||||
}
|
||||
|
||||
public useGetExtensionList(swrOptions?: SWROptions<IExtension[]>): AbortableSWRResponse<IExtension[]> {
|
||||
return this.doRequest(HttpMethod.SWR_GET, 'extension/list', { swrOptions });
|
||||
}
|
||||
|
||||
public installExtension(extension: string | File): AbortableAxiosResponse {
|
||||
if (typeof extension === 'string') {
|
||||
return this.doRequest(HttpMethod.GET, `extension/install/${extension}`);
|
||||
}
|
||||
|
||||
return this.doRequest(HttpMethod.POST, `extension/install`, { formData: { file: extension } });
|
||||
}
|
||||
|
||||
public updateExtension(extension: string): AbortableAxiosResponse {
|
||||
return this.doRequest(HttpMethod.GET, `extension/update/${extension}`);
|
||||
}
|
||||
|
||||
public uninstallExtension(extension: string): AbortableAxiosResponse {
|
||||
return this.doRequest(HttpMethod.GET, `extension/uninstall/${extension}`);
|
||||
}
|
||||
|
||||
public getExtensionIconUrl(extension: string): string {
|
||||
return this.getValidImgUrlFor(`extension/icon/${extension}`);
|
||||
}
|
||||
|
||||
public useGetSourceList(swrOptions?: SWROptions<ISource[]>): AbortableSWRResponse<ISource[]> {
|
||||
return this.doRequest(HttpMethod.SWR_GET, 'source/list', { swrOptions });
|
||||
}
|
||||
|
||||
public useGetSource(sourceId: string, swrOptions?: SWROptions<ISource>): AbortableSWRResponse<ISource> {
|
||||
return this.doRequest(HttpMethod.SWR_GET, `source/${sourceId}`, { swrOptions });
|
||||
}
|
||||
|
||||
public useGetSourcePopularMangas(
|
||||
sourceId: string,
|
||||
initialPages?: number,
|
||||
swrOptions?: SWRInfiniteOptions<PaginatedMangaList>,
|
||||
): AbortableSWRInfiniteResponse<PaginatedMangaList> {
|
||||
return this.doRequest(SWRHttpMethod.SWR_GET_INFINITE, '', {
|
||||
swrOptions: {
|
||||
getEndpoint: (page, previousData) =>
|
||||
previousData?.hasNextPage ?? true ? `source/${sourceId}/popular/${page + 1}` : null,
|
||||
initialSize: initialPages,
|
||||
...swrOptions,
|
||||
} as typeof swrOptions,
|
||||
});
|
||||
}
|
||||
|
||||
public useGetSourceLatestMangas(
|
||||
sourceId: string,
|
||||
initialPages?: number,
|
||||
swrOptions?: SWRInfiniteOptions<PaginatedMangaList>,
|
||||
): AbortableSWRInfiniteResponse<PaginatedMangaList> {
|
||||
return this.doRequest(SWRHttpMethod.SWR_GET_INFINITE, '', {
|
||||
swrOptions: {
|
||||
getEndpoint: (page, previousData) =>
|
||||
previousData?.hasNextPage ?? true ? `source/${sourceId}/latest/${page + 1}` : null,
|
||||
initialSize: initialPages,
|
||||
...swrOptions,
|
||||
} as typeof swrOptions,
|
||||
});
|
||||
}
|
||||
|
||||
public useGetSourcePreferences(
|
||||
sourceId: string,
|
||||
swrOptions?: SWROptions<SourcePreferences[]>,
|
||||
): AbortableSWRResponse<SourcePreferences[]> {
|
||||
return this.doRequest(HttpMethod.SWR_GET, `source/${sourceId}/preferences`, { swrOptions });
|
||||
}
|
||||
|
||||
public setSourcePreferences(sourceId: string, position: number, value: string): AbortableAxiosResponse {
|
||||
return this.doRequest(HttpMethod.POST, `source/${sourceId}/preferences`, { data: { position, value } });
|
||||
}
|
||||
|
||||
public useGetSourceFilters(
|
||||
sourceId: string,
|
||||
reset?: boolean,
|
||||
swrOptions?: SWROptions<ISourceFilters[]>,
|
||||
): AbortableSWRResponse<ISourceFilters[]> {
|
||||
return this.doRequest(HttpMethod.SWR_GET, `source/${sourceId}/filters`, { swrOptions });
|
||||
}
|
||||
|
||||
public setSourceFilters(sourceId: string, filters: { position: number; state: string }[]): AbortableAxiosResponse {
|
||||
return this.doRequest(HttpMethod.POST, `source/${sourceId}/filters`, { data: filters });
|
||||
}
|
||||
|
||||
public resetSourceFilters(sourceId: string): AbortableAxiosResponse {
|
||||
return this.doRequest(HttpMethod.GET, `source/${sourceId}/filters?reset=true`);
|
||||
}
|
||||
|
||||
public useSourceSearch(
|
||||
sourceId: string,
|
||||
searchTerm: string,
|
||||
initialPages?: number,
|
||||
swrOptions?: SWRInfiniteOptions<SourceSearchResult>,
|
||||
): AbortableSWRInfiniteResponse<SourceSearchResult> {
|
||||
return this.doRequest(HttpMethod.SWR_GET_INFINITE, '', {
|
||||
swrOptions: {
|
||||
getEndpoint: (page, previousData) =>
|
||||
previousData?.hasNextPage ?? true
|
||||
? `source/${sourceId}/search?searchTerm=${searchTerm}&pageNum=${page + 1}`
|
||||
: null,
|
||||
initialSize: initialPages,
|
||||
...swrOptions,
|
||||
} as typeof swrOptions,
|
||||
});
|
||||
}
|
||||
|
||||
public useSourceQuickSearch(
|
||||
sourceId: string,
|
||||
searchTerm: string,
|
||||
filters: { position: number; state: string }[],
|
||||
initialPages?: number,
|
||||
swrOptions?: SWRInfiniteOptions<SourceSearchResult>,
|
||||
): AbortableSWRInfiniteResponse<SourceSearchResult> {
|
||||
return this.doRequest(HttpMethod.SWR_POST_INFINITE, '', {
|
||||
data: { searchTerm, filter: filters },
|
||||
swrOptions: {
|
||||
getEndpoint: (page, previousData) =>
|
||||
previousData?.hasNextPage ?? true
|
||||
? `source/${sourceId}/quick-search?searchTerm=${searchTerm}&pageNum=${page + 1}`
|
||||
: null,
|
||||
initialSize: initialPages,
|
||||
...swrOptions,
|
||||
} as typeof swrOptions,
|
||||
});
|
||||
}
|
||||
|
||||
public useGetManga(
|
||||
mangaId: number | string,
|
||||
{ doOnlineFetch, ...swrOptions }: SWROptions<IManga> & RequestOption = {},
|
||||
): AbortableSWRResponse<IManga> {
|
||||
const onlineFetch = doOnlineFetch ? '?onlineFetch=true' : '';
|
||||
return this.doRequest(HttpMethod.SWR_GET, `manga/${mangaId}${onlineFetch}`, {
|
||||
swrOptions,
|
||||
});
|
||||
}
|
||||
|
||||
public getManga(mangaId: number | string, doOnlineFetch?: boolean): AbortableAxiosResponse<IManga> {
|
||||
const onlineFetch = doOnlineFetch ? '?onlineFetch=true' : '';
|
||||
return this.doRequest(HttpMethod.GET, `manga/${mangaId}${onlineFetch}`);
|
||||
}
|
||||
|
||||
public useGetFullManga(
|
||||
mangaId: number | string,
|
||||
{ doOnlineFetch, ...swrOptions }: SWROptions<IManga> & RequestOption = {},
|
||||
): AbortableSWRResponse<IManga> {
|
||||
const onlineFetch = doOnlineFetch ? '?onlineFetch=true' : '';
|
||||
return this.doRequest(HttpMethod.SWR_GET, `manga/${mangaId}/full${onlineFetch}`, {
|
||||
swrOptions,
|
||||
});
|
||||
}
|
||||
|
||||
public getMangaThumbnailUrl(mangaId: number): string {
|
||||
return this.getValidImgUrlFor(`manga/${mangaId}/thumbnail`);
|
||||
}
|
||||
|
||||
public useGetMangaCategories(
|
||||
mangaId: number,
|
||||
swrOptions?: SWROptions<ICategory[]>,
|
||||
): AbortableSWRResponse<ICategory[]> {
|
||||
return this.doRequest(HttpMethod.SWR_GET, `manga/${mangaId}/category`, { swrOptions });
|
||||
}
|
||||
|
||||
public addMangaToCategory(mangaId: number, categoryId: number): AbortableAxiosResponse {
|
||||
return this.doRequest(HttpMethod.GET, `manga/${mangaId}/category/${categoryId}`);
|
||||
}
|
||||
|
||||
public removeMangaFromCategory(mangaId: number, categoryId: number): AbortableAxiosResponse {
|
||||
return this.doRequest(HttpMethod.DELETE, `manga/${mangaId}/category/${categoryId}`);
|
||||
}
|
||||
|
||||
public addMangaToLibrary(mangaId: number | string): AbortableAxiosResponse {
|
||||
return this.doRequest(HttpMethod.GET, `manga/${mangaId}/library`);
|
||||
}
|
||||
|
||||
public removeMangaFromLibrary(mangaId: number | string): AbortableAxiosResponse {
|
||||
return this.doRequest(HttpMethod.DELETE, `manga/${mangaId}/library`);
|
||||
}
|
||||
|
||||
public setMangaMeta(mangaId: number, key: string, value: any): AbortableAxiosResponse {
|
||||
return this.doRequest(HttpMethod.PATCH, `manga/${mangaId}/meta`, { formData: { key, value } });
|
||||
}
|
||||
|
||||
public useGetMangaChapters(
|
||||
mangaId: number | string,
|
||||
{ doOnlineFetch, ...swrOptions }: SWROptions<IChapter[]> & RequestOption = {},
|
||||
): AbortableSWRResponse<IChapter[]> {
|
||||
const onlineFetch = doOnlineFetch ? '?onlineFetch=true' : '';
|
||||
return this.doRequest(HttpMethod.SWR_GET, `manga/${mangaId}/chapters${onlineFetch}`, {
|
||||
swrOptions,
|
||||
});
|
||||
}
|
||||
|
||||
public getMangaChapters(mangaId: number | string, doOnlineFetch?: boolean): AbortableAxiosResponse<IChapter[]> {
|
||||
const onlineFetch = doOnlineFetch ? '?onlineFetch=true' : '';
|
||||
return this.doRequest(HttpMethod.GET, `manga/${mangaId}/chapters${onlineFetch}`);
|
||||
}
|
||||
|
||||
public updateMangaChapters(
|
||||
mangaId: number | string,
|
||||
{
|
||||
chapterIds,
|
||||
chapterIndexes,
|
||||
change,
|
||||
}: (
|
||||
| { chapterIds?: number[]; chapterIndexes: number[] }
|
||||
| { chapterIds: number[]; chapterIndexes?: number[] }
|
||||
) & { change: BatchChaptersChange },
|
||||
): AbortableAxiosResponse {
|
||||
return this.doRequest(HttpMethod.POST, `manga/${mangaId}/chapter/batch`, {
|
||||
data: {
|
||||
chapterIds,
|
||||
chapterIndexes,
|
||||
change,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public useGetChapter(
|
||||
mangaId: number | string,
|
||||
chapterIndex: number | string,
|
||||
swrOptions?: SWROptions<IChapter>,
|
||||
): AbortableSWRResponse<IChapter> {
|
||||
return this.doRequest(HttpMethod.SWR_GET, `manga/${mangaId}/chapter/${chapterIndex}`, {
|
||||
swrOptions,
|
||||
});
|
||||
}
|
||||
|
||||
public getChapter(mangaId: number | string, chapterIndex: number | string): AbortableAxiosResponse<IChapter> {
|
||||
return this.doRequest(HttpMethod.GET, `manga/${mangaId}/chapter/${chapterIndex}`);
|
||||
}
|
||||
|
||||
public deleteDownloadedChapter(mangaId: number | string, chapterIndex: number | string): AbortableAxiosResponse {
|
||||
return this.doRequest(HttpMethod.DELETE, `manga/${mangaId}/chapter/${chapterIndex}`);
|
||||
}
|
||||
|
||||
public updateChapter(
|
||||
mangaId: number | string,
|
||||
chapterIndex: number | string,
|
||||
change: { read?: boolean; bookmarked?: boolean; markPrevRead?: boolean; lastPageRead?: number } = {},
|
||||
): AbortableAxiosResponse {
|
||||
return this.doRequest(HttpMethod.PATCH, `manga/${mangaId}/chapter/${chapterIndex}`, { formData: change });
|
||||
}
|
||||
|
||||
public setChapterMeta(
|
||||
mangaId: number | string,
|
||||
chapterIndex: number | string,
|
||||
key: string,
|
||||
value: any,
|
||||
): AbortableAxiosResponse {
|
||||
return this.doRequest(HttpMethod.PATCH, `manga/${mangaId}/chapter/${chapterIndex}/meta`, {
|
||||
formData: { key, value },
|
||||
});
|
||||
}
|
||||
|
||||
public getChapterPageUrl(mangaId: number | string, chapterIndex: number | string, page: number): string {
|
||||
return this.getValidImgUrlFor(
|
||||
`manga/${mangaId}/chapter/${chapterIndex}/page/${page}`,
|
||||
RequestManager.API_VERSION,
|
||||
);
|
||||
}
|
||||
|
||||
public updateChapters(chapterIds: number[], change: BatchChaptersChange): AbortableAxiosResponse {
|
||||
return this.doRequest(HttpMethod.POST, `chapter/batch`, { data: { chapterIds, change } });
|
||||
}
|
||||
|
||||
public useGetCategories(swrOptions?: SWROptions<ICategory[]>): AbortableSWRResponse<ICategory[]> {
|
||||
return this.doRequest(HttpMethod.SWR_GET, `category`, { swrOptions });
|
||||
}
|
||||
|
||||
public createCategory(name: string): AbortableAxiosResponse {
|
||||
return this.doRequest(HttpMethod.POST, `category`, { formData: { name } });
|
||||
}
|
||||
|
||||
public reorderCategory(currentPosition: number, newPosition: number): AbortableAxiosResponse {
|
||||
return this.doRequest(HttpMethod.PATCH, `category/reorder`, {
|
||||
formData: { from: currentPosition, to: newPosition },
|
||||
});
|
||||
}
|
||||
|
||||
public useGetCategoryMangas(categoryId: number, swrOptions?: SWROptions<IManga[]>): AbortableSWRResponse<IManga[]> {
|
||||
return this.doRequest(HttpMethod.SWR_GET, `category/${categoryId}`, { swrOptions });
|
||||
}
|
||||
|
||||
public deleteCategory(categoryId: number): AbortableAxiosResponse {
|
||||
return this.doRequest(HttpMethod.DELETE, `category/${categoryId}`);
|
||||
}
|
||||
|
||||
public updateCategory(
|
||||
categoryId: number,
|
||||
change: { name?: string; default?: boolean; includeInUpdate?: IncludeInGlobalUpdate } = {},
|
||||
): AbortableAxiosResponse {
|
||||
return this.doRequest(HttpMethod.PATCH, `category/${categoryId}`, { formData: change });
|
||||
}
|
||||
|
||||
public setCategoryMeta(categoryId: number, key: string, value: any): AbortableAxiosResponse {
|
||||
return this.doRequest(HttpMethod.PATCH, `category/${categoryId}/meta`, { formData: { key, value } });
|
||||
}
|
||||
|
||||
public restoreBackupFile(file: File): AbortableAxiosResponse {
|
||||
return this.doRequest(HttpMethod.POST, 'backup/import/file', { formData: { 'backup.proto.gz': file } });
|
||||
}
|
||||
|
||||
public useValidateBackupFile(
|
||||
file: File,
|
||||
swrOptions?: SWROptions<BackupValidationResult>,
|
||||
): AbortableSWRResponse<BackupValidationResult> {
|
||||
return this.doRequest(HttpMethod.SWR_POST, 'backup/validate/file', {
|
||||
formData: { 'backup.proto.gz': file },
|
||||
swrOptions,
|
||||
});
|
||||
}
|
||||
|
||||
public getExportBackupUrl(): string {
|
||||
return this.getValidUrlFor('backup/export/file');
|
||||
}
|
||||
|
||||
public startDownloads(): AbortableAxiosResponse {
|
||||
return this.doRequest(HttpMethod.GET, 'downloads/start');
|
||||
}
|
||||
|
||||
public stopDownloads(): AbortableAxiosResponse {
|
||||
return this.doRequest(HttpMethod.GET, 'downloads/stop');
|
||||
}
|
||||
|
||||
public clearDownloads(): AbortableAxiosResponse {
|
||||
return this.doRequest(HttpMethod.GET, 'downloads/clear');
|
||||
}
|
||||
|
||||
public addChapterToDownloadQueue(mangaId: number | string, chapterIndex: number | string): AbortableAxiosResponse {
|
||||
return this.doRequest(HttpMethod.GET, `download/${mangaId}/chapter/${chapterIndex}`);
|
||||
}
|
||||
|
||||
public removeChapterFromDownloadQueue(
|
||||
mangaId: number | string,
|
||||
chapterIndex: number | string,
|
||||
): AbortableAxiosResponse {
|
||||
return this.doRequest(HttpMethod.DELETE, `download/${mangaId}/chapter/${chapterIndex}`);
|
||||
}
|
||||
|
||||
public reorderChapterInDownloadQueue(
|
||||
mangaId: number | string,
|
||||
chapterIndex: number | string,
|
||||
position: number,
|
||||
): AbortableAxiosResponse {
|
||||
return this.doRequest(HttpMethod.PATCH, `download/${mangaId}/chapter/${chapterIndex}/reorder/${position}`);
|
||||
}
|
||||
|
||||
public addChaptersToDownloadQueue(chapterIds: number[]): AbortableAxiosResponse {
|
||||
return this.doRequest(HttpMethod.POST, 'download/batch', { data: { chapterIds } });
|
||||
}
|
||||
|
||||
public removeChaptersFromDownloadQueue(chapterIds: number[]): AbortableAxiosResponse {
|
||||
return this.doRequest(HttpMethod.DELETE, 'download/batch', { data: { chapterIds } });
|
||||
}
|
||||
|
||||
public useGetRecentlyUpdatedChapters(
|
||||
initialPages?: number,
|
||||
swrOptions?: SWRInfiniteOptions<PaginatedList<IMangaChapter>>,
|
||||
): AbortableSWRInfiniteResponse<PaginatedList<IMangaChapter>> {
|
||||
return this.doRequest(HttpMethod.SWR_GET_INFINITE, '', {
|
||||
swrOptions: {
|
||||
getEndpoint: (page, previousData) =>
|
||||
previousData?.hasNextPage ?? true ? `update/recentChapters/${page}` : null,
|
||||
initialSize: initialPages,
|
||||
...swrOptions,
|
||||
} as typeof swrOptions,
|
||||
});
|
||||
}
|
||||
|
||||
public startGlobalUpdate(categoryId?: number): AbortableAxiosResponse {
|
||||
return this.doRequest(HttpMethod.POST, 'update/fetch', { formData: { categoryId } });
|
||||
}
|
||||
|
||||
public resetGlobalUpdate(): AbortableAxiosResponse {
|
||||
return this.doRequest(HttpMethod.POST, 'update/reset');
|
||||
}
|
||||
|
||||
public useGetGlobalUpdateSummary(swrOptions?: SWROptions<IUpdateStatus>): AbortableSWRResponse<IUpdateStatus> {
|
||||
return this.doRequest(HttpMethod.SWR_GET, 'update/summary', { swrOptions });
|
||||
}
|
||||
}
|
||||
|
||||
const requestManager = new RequestManager();
|
||||
export default requestManager;
|
||||
439
src/lib/graphql/Fragments.ts
Normal file
439
src/lib/graphql/Fragments.ts
Normal file
@@ -0,0 +1,439 @@
|
||||
/*
|
||||
* 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 gql from 'graphql-tag';
|
||||
|
||||
export const PAGE_INFO = gql`
|
||||
fragment PAGE_INFO on PageInfo {
|
||||
endCursor
|
||||
hasNextPage
|
||||
hasPreviousPage
|
||||
startCursor
|
||||
}
|
||||
`;
|
||||
|
||||
export const GLOBAL_METADATA = gql`
|
||||
fragment GLOBAL_METADATA on GlobalMetaType {
|
||||
key
|
||||
value
|
||||
}
|
||||
`;
|
||||
|
||||
export const FULL_CATEGORY_FIELDS = gql`
|
||||
fragment FULL_CATEGORY_FIELDS on CategoryType {
|
||||
default
|
||||
id
|
||||
includeInUpdate
|
||||
name
|
||||
order
|
||||
meta {
|
||||
key
|
||||
value
|
||||
}
|
||||
mangas {
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const PARTIAL_SOURCE_FIELDS = gql`
|
||||
fragment PARTIAL_SOURCE_FIELDS on SourceType {
|
||||
displayName
|
||||
iconUrl
|
||||
id
|
||||
isConfigurable
|
||||
isNsfw
|
||||
lang
|
||||
name
|
||||
supportsLatest
|
||||
}
|
||||
`;
|
||||
|
||||
export const FULL_SOURCE_FIELDS = gql`
|
||||
${PARTIAL_SOURCE_FIELDS}
|
||||
fragment FULL_SOURCE_FIELDS on SourceType {
|
||||
...PARTIAL_SOURCE_FIELDS
|
||||
preferences {
|
||||
... on CheckBoxPreference {
|
||||
type: __typename
|
||||
CheckBoxCheckBoxCurrentValue: currentValue
|
||||
summary
|
||||
CheckBoxDefault: default
|
||||
key
|
||||
CheckBoxTitle: title
|
||||
}
|
||||
... on EditTextPreference {
|
||||
type: __typename
|
||||
EditTextPreferenceCurrentValue: currentValue
|
||||
EditTextPreferenceDefault: default
|
||||
EditTextPreferenceTitle: title
|
||||
text
|
||||
summary
|
||||
key
|
||||
dialogTitle
|
||||
dialogMessage
|
||||
}
|
||||
... on SwitchPreference {
|
||||
type: __typename
|
||||
SwitchPreferenceCurrentValue: currentValue
|
||||
summary
|
||||
key
|
||||
SwitchPreferenceDefault: default
|
||||
SwitchPreferenceTitle: title
|
||||
}
|
||||
... on MultiSelectListPreference {
|
||||
type: __typename
|
||||
dialogMessage
|
||||
dialogTitle
|
||||
MultiSelectListPreferenceTitle: title
|
||||
summary
|
||||
key
|
||||
entryValues
|
||||
entries
|
||||
MultiSelectListPreferenceDefault: default
|
||||
MultiSelectListPreferenceCurrentValue: currentValue
|
||||
}
|
||||
... on ListPreference {
|
||||
type: __typename
|
||||
ListPreferenceCurrentValue: currentValue
|
||||
ListPreferenceDefault: default
|
||||
ListPreferenceTitle: title
|
||||
summary
|
||||
key
|
||||
entryValues
|
||||
entries
|
||||
}
|
||||
}
|
||||
filters {
|
||||
... on TriStateFilter {
|
||||
type: __typename
|
||||
name
|
||||
TriStateFilterDefault: default
|
||||
}
|
||||
... on CheckBoxFilter {
|
||||
type: __typename
|
||||
CheckBoxFilterDefault: default
|
||||
name
|
||||
}
|
||||
... on TextFilter {
|
||||
type: __typename
|
||||
name
|
||||
TextFilterDefault: default
|
||||
}
|
||||
... on SortFilter {
|
||||
type: __typename
|
||||
values
|
||||
name
|
||||
SortFilterDefault: default {
|
||||
ascending
|
||||
index
|
||||
}
|
||||
}
|
||||
... on SeparatorFilter {
|
||||
type: __typename
|
||||
name
|
||||
}
|
||||
... on SelectFilter {
|
||||
type: __typename
|
||||
values
|
||||
name
|
||||
SelectFilterDefault: default
|
||||
}
|
||||
... on HeaderFilter {
|
||||
type: __typename
|
||||
name
|
||||
}
|
||||
... on GroupFilter {
|
||||
type: __typename
|
||||
name
|
||||
filters {
|
||||
... on CheckBoxFilter {
|
||||
type: __typename
|
||||
CheckBoxFilterDefault: default
|
||||
name
|
||||
}
|
||||
... on HeaderFilter {
|
||||
type: __typename
|
||||
name
|
||||
}
|
||||
... on SelectFilter {
|
||||
type: __typename
|
||||
SelectFilterDefault: default
|
||||
name
|
||||
values
|
||||
}
|
||||
... on TriStateFilter {
|
||||
type: __typename
|
||||
TriStateFilterDefault: default
|
||||
name
|
||||
}
|
||||
... on TextFilter {
|
||||
type: __typename
|
||||
TextFilterDefault: default
|
||||
name
|
||||
}
|
||||
... on SortFilter {
|
||||
type: __typename
|
||||
SorSortFilterDefault: default {
|
||||
ascending
|
||||
index
|
||||
}
|
||||
name
|
||||
values
|
||||
}
|
||||
... on SeparatorFilter {
|
||||
type: __typename
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const BASE_MANGA_FIELDS = gql`
|
||||
${PARTIAL_SOURCE_FIELDS}
|
||||
fragment BASE_MANGA_FIELDS on MangaType {
|
||||
artist
|
||||
author
|
||||
chaptersLastFetchedAt
|
||||
description
|
||||
genre
|
||||
id
|
||||
inLibrary
|
||||
inLibraryAt
|
||||
initialized
|
||||
lastFetchedAt
|
||||
meta {
|
||||
key
|
||||
value
|
||||
}
|
||||
realUrl
|
||||
source {
|
||||
...PARTIAL_SOURCE_FIELDS
|
||||
}
|
||||
status
|
||||
thumbnailUrl
|
||||
title
|
||||
url
|
||||
}
|
||||
`;
|
||||
|
||||
export const PARTIAL_MANGA_FIELDS = gql`
|
||||
${BASE_MANGA_FIELDS}
|
||||
${FULL_CATEGORY_FIELDS}
|
||||
${PARTIAL_SOURCE_FIELDS}
|
||||
fragment PARTIAL_MANGA_FIELDS on MangaType {
|
||||
...BASE_MANGA_FIELDS
|
||||
unreadCount
|
||||
downloadCount
|
||||
categories {
|
||||
nodes {
|
||||
...FULL_CATEGORY_FIELDS
|
||||
}
|
||||
totalCount
|
||||
}
|
||||
chapters {
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const FULL_CHAPTER_FIELDS = gql`
|
||||
${PARTIAL_MANGA_FIELDS}
|
||||
fragment FULL_CHAPTER_FIELDS on ChapterType {
|
||||
chapterNumber
|
||||
fetchedAt
|
||||
id
|
||||
isBookmarked
|
||||
isDownloaded
|
||||
isRead
|
||||
lastPageRead
|
||||
lastReadAt
|
||||
manga {
|
||||
...PARTIAL_MANGA_FIELDS
|
||||
}
|
||||
meta {
|
||||
key
|
||||
value
|
||||
}
|
||||
name
|
||||
pageCount
|
||||
realUrl
|
||||
scanlator
|
||||
sourceOrder
|
||||
uploadDate
|
||||
url
|
||||
}
|
||||
`;
|
||||
|
||||
export const FULL_MANGA_FIELDS = gql`
|
||||
${PARTIAL_MANGA_FIELDS}
|
||||
${FULL_CHAPTER_FIELDS}
|
||||
fragment FULL_MANGA_FIELDS on MangaType {
|
||||
...PARTIAL_MANGA_FIELDS
|
||||
lastReadChapter {
|
||||
...FULL_CHAPTER_FIELDS
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const FULL_EXTENSION_FIELDS = gql`
|
||||
fragment FULL_EXTENSION_FIELDS on ExtensionType {
|
||||
apkName
|
||||
hasUpdate
|
||||
iconUrl
|
||||
isInstalled
|
||||
isNsfw
|
||||
isObsolete
|
||||
lang
|
||||
name
|
||||
pkgName
|
||||
versionCode
|
||||
versionName
|
||||
}
|
||||
`;
|
||||
|
||||
export const FULL_DOWNLOAD_STATUS = gql`
|
||||
${FULL_CHAPTER_FIELDS}
|
||||
fragment FULL_DOWNLOAD_STATUS on DownloadStatus {
|
||||
queue {
|
||||
chapter {
|
||||
...FULL_CHAPTER_FIELDS
|
||||
}
|
||||
progress
|
||||
state
|
||||
tries
|
||||
}
|
||||
state
|
||||
}
|
||||
`;
|
||||
|
||||
export const PARTIAL_UPDATER_STATUS = gql`
|
||||
fragment PARTIAL_UPDATER_STATUS on UpdateStatus {
|
||||
isRunning
|
||||
}
|
||||
`;
|
||||
|
||||
export const FULL_UPDATER_STATUS = gql`
|
||||
${FULL_MANGA_FIELDS}
|
||||
${PARTIAL_UPDATER_STATUS}
|
||||
${FULL_CATEGORY_FIELDS}
|
||||
fragment FULL_UPDATER_STATUS on UpdateStatus {
|
||||
...PARTIAL_UPDATER_STATUS
|
||||
completeJobs {
|
||||
mangas {
|
||||
nodes {
|
||||
...FULL_MANGA_FIELDS
|
||||
}
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
failedJobs {
|
||||
mangas {
|
||||
nodes {
|
||||
...FULL_MANGA_FIELDS
|
||||
}
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
pendingJobs {
|
||||
mangas {
|
||||
nodes {
|
||||
...FULL_MANGA_FIELDS
|
||||
}
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
runningJobs {
|
||||
mangas {
|
||||
nodes {
|
||||
...FULL_MANGA_FIELDS
|
||||
}
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
skippedJobs {
|
||||
mangas {
|
||||
nodes {
|
||||
...FULL_MANGA_FIELDS
|
||||
}
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
updatingCategories {
|
||||
categories {
|
||||
nodes {
|
||||
...FULL_CATEGORY_FIELDS
|
||||
}
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
skippedCategories {
|
||||
categories {
|
||||
nodes {
|
||||
...FULL_CATEGORY_FIELDS
|
||||
}
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const WEBUI_UPDATE_INFO = gql`
|
||||
fragment WEBUI_UPDATE_INFO on WebUIUpdateInfo {
|
||||
channel
|
||||
tag
|
||||
updateAvailable
|
||||
}
|
||||
`;
|
||||
|
||||
export const WEBUI_UPDATE_STATUS = gql`
|
||||
${WEBUI_UPDATE_INFO}
|
||||
fragment WEBUI_UPDATE_STATUS on WebUIUpdateStatus {
|
||||
info {
|
||||
...WEBUI_UPDATE_INFO
|
||||
}
|
||||
progress
|
||||
state
|
||||
}
|
||||
`;
|
||||
|
||||
export const SERVER_SETTINGS = gql`
|
||||
fragment SERVER_SETTINGS on SettingsType {
|
||||
autoDownloadNewChapters
|
||||
backupInterval
|
||||
backupPath
|
||||
backupTTL
|
||||
backupTime
|
||||
basicAuthEnabled
|
||||
basicAuthPassword
|
||||
basicAuthUsername
|
||||
debugLogsEnabled
|
||||
downloadAsCbz
|
||||
downloadsPath
|
||||
electronPath
|
||||
excludeCompleted
|
||||
excludeNotStarted
|
||||
excludeUnreadChapters
|
||||
globalUpdateInterval
|
||||
initialOpenInBrowserEnabled
|
||||
ip
|
||||
localSourcePath
|
||||
maxSourcesInParallel
|
||||
port
|
||||
socksProxyEnabled
|
||||
socksProxyHost
|
||||
socksProxyPort
|
||||
systemTrayEnabled
|
||||
webUIChannel
|
||||
webUIFlavor
|
||||
webUIInterface
|
||||
webUIUpdateCheckInterval
|
||||
}
|
||||
`;
|
||||
1251
src/lib/graphql/generated/apollo-helpers.ts
Normal file
1251
src/lib/graphql/generated/apollo-helpers.ts
Normal file
File diff suppressed because it is too large
Load Diff
2605
src/lib/graphql/generated/graphql.ts
Normal file
2605
src/lib/graphql/generated/graphql.ts
Normal file
File diff suppressed because one or more lines are too long
7
src/lib/graphql/graphql.config.yml
Normal file
7
src/lib/graphql/graphql.config.yml
Normal file
@@ -0,0 +1,7 @@
|
||||
schema: schema.json # download the schema from the server and place it in "src/lib/graphql/"
|
||||
documents: [
|
||||
'src/lib/graphql/queries/**',
|
||||
'src/lib/graphql/mutations/**',
|
||||
'src/lib/graphql/subscriptions/**',
|
||||
'src/lib/graphql/Fragments.ts',
|
||||
]
|
||||
31
src/lib/graphql/mutations/BackupMutation.ts
Normal file
31
src/lib/graphql/mutations/BackupMutation.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* 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 gql from 'graphql-tag';
|
||||
|
||||
export const CREATE_BACKUP = gql`
|
||||
mutation CREATE_BACKUP($input: CreateBackupInput!) {
|
||||
createBackup(input: $input) {
|
||||
clientMutationId
|
||||
url
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const RESTORE_BACKUP = gql`
|
||||
mutation RESTORE_BACKUP($input: RestoreBackupInput!) {
|
||||
restoreBackup(input: $input) {
|
||||
clientMutationId
|
||||
status {
|
||||
mangaProgress
|
||||
state
|
||||
totalManga
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
105
src/lib/graphql/mutations/CategoryMutation.ts
Normal file
105
src/lib/graphql/mutations/CategoryMutation.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* 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 gql from 'graphql-tag';
|
||||
import { FULL_CATEGORY_FIELDS } from '@/lib/graphql/Fragments';
|
||||
|
||||
export const CREATE_CATEGORY = gql`
|
||||
${FULL_CATEGORY_FIELDS}
|
||||
mutation CREATE_CATEGORY($input: CreateCategoryInput!) {
|
||||
createCategory(input: $input) {
|
||||
clientMutationId
|
||||
category {
|
||||
...FULL_CATEGORY_FIELDS
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const DELETE_CATEGORY = gql`
|
||||
${FULL_CATEGORY_FIELDS}
|
||||
mutation DELETE_CATEGORY($input: DeleteCategoryInput!) {
|
||||
deleteCategory(input: $input) {
|
||||
clientMutationId
|
||||
category {
|
||||
...FULL_CATEGORY_FIELDS
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const DELETE_CATEGORY_METADATA = gql`
|
||||
${FULL_CATEGORY_FIELDS}
|
||||
mutation DELETE_CATEGORY_METADATA($input: DeleteCategoryMetaInput!) {
|
||||
deleteCategoryMeta(input: $input) {
|
||||
clientMutationId
|
||||
meta {
|
||||
key
|
||||
value
|
||||
category {
|
||||
...FULL_CATEGORY_FIELDS
|
||||
}
|
||||
}
|
||||
category {
|
||||
...FULL_CATEGORY_FIELDS
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const SET_CATEGORY_METADATA = gql`
|
||||
${FULL_CATEGORY_FIELDS}
|
||||
mutation SET_CATEGORY_METADATA($input: SetCategoryMetaInput!) {
|
||||
setCategoryMeta(input: $input) {
|
||||
clientMutationId
|
||||
meta {
|
||||
key
|
||||
value
|
||||
category {
|
||||
...FULL_CATEGORY_FIELDS
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const UPDATE_CATEGORY = gql`
|
||||
${FULL_CATEGORY_FIELDS}
|
||||
mutation UPDATE_CATEGORY($input: UpdateCategoryInput!) {
|
||||
updateCategory(input: $input) {
|
||||
clientMutationId
|
||||
category {
|
||||
...FULL_CATEGORY_FIELDS
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const UPDATE_CATEGORIES = gql`
|
||||
${FULL_CATEGORY_FIELDS}
|
||||
mutation UPDATE_CATEGORIES($input: UpdateCategoriesInput!) {
|
||||
updateCategories(input: $input) {
|
||||
clientMutationId
|
||||
categories {
|
||||
...FULL_CATEGORY_FIELDS
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const UPDATE_CATEGORY_ORDER = gql`
|
||||
${FULL_CATEGORY_FIELDS}
|
||||
mutation UPDATE_CATEGORY_ORDER($input: UpdateCategoryOrderInput!) {
|
||||
updateCategoryOrder(input: $input) {
|
||||
clientMutationId
|
||||
categories {
|
||||
...FULL_CATEGORY_FIELDS
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
96
src/lib/graphql/mutations/ChapterMutation.ts
Normal file
96
src/lib/graphql/mutations/ChapterMutation.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* 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 gql from 'graphql-tag';
|
||||
import { FULL_CHAPTER_FIELDS } from '@/lib/graphql/Fragments';
|
||||
|
||||
export const DELETE_CHAPTER_METADATA = gql`
|
||||
${FULL_CHAPTER_FIELDS}
|
||||
mutation DELETE_CHAPTER_METADATA($input: DeleteChapterMetaInput!) {
|
||||
deleteChapterMeta(input: $input) {
|
||||
clientMutationId
|
||||
meta {
|
||||
key
|
||||
value
|
||||
chapter {
|
||||
...FULL_CHAPTER_FIELDS
|
||||
}
|
||||
}
|
||||
chapter {
|
||||
...FULL_CHAPTER_FIELDS
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// makes the server fetch and return the pages of a chapter
|
||||
export const GET_CHAPTER_PAGES_FETCH = gql`
|
||||
${FULL_CHAPTER_FIELDS}
|
||||
mutation GET_CHAPTER_PAGES_FETCH($input: FetchChapterPagesInput!) {
|
||||
fetchChapterPages(input: $input) {
|
||||
clientMutationId
|
||||
chapter {
|
||||
...FULL_CHAPTER_FIELDS
|
||||
}
|
||||
pages
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// makes the server fetch and return the chapters of the manga
|
||||
export const GET_MANGA_CHAPTERS_FETCH = gql`
|
||||
${FULL_CHAPTER_FIELDS}
|
||||
mutation GET_MANGA_CHAPTERS_FETCH($input: FetchChaptersInput!) {
|
||||
fetchChapters(input: $input) {
|
||||
clientMutationId
|
||||
chapters {
|
||||
...FULL_CHAPTER_FIELDS
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const SET_CHAPTER_METADATA = gql`
|
||||
${FULL_CHAPTER_FIELDS}
|
||||
mutation SET_CHAPTER_METADATA($input: SetChapterMetaInput!) {
|
||||
setChapterMeta(input: $input) {
|
||||
clientMutationId
|
||||
meta {
|
||||
key
|
||||
value
|
||||
chapter {
|
||||
...FULL_CHAPTER_FIELDS
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const UPDATE_CHAPTER = gql`
|
||||
${FULL_CHAPTER_FIELDS}
|
||||
mutation UPDATE_CHAPTER($input: UpdateChapterInput!) {
|
||||
updateChapter(input: $input) {
|
||||
clientMutationId
|
||||
chapter {
|
||||
...FULL_CHAPTER_FIELDS
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const UPDATE_CHAPTERS = gql`
|
||||
${FULL_CHAPTER_FIELDS}
|
||||
mutation UPDATE_CHAPTERS($input: UpdateChaptersInput!) {
|
||||
updateChapters(input: $input) {
|
||||
clientMutationId
|
||||
chapters {
|
||||
...FULL_CHAPTER_FIELDS
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
130
src/lib/graphql/mutations/DownloaderMutation.ts
Normal file
130
src/lib/graphql/mutations/DownloaderMutation.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* 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 gql from 'graphql-tag';
|
||||
import { FULL_CHAPTER_FIELDS, FULL_DOWNLOAD_STATUS } from '@/lib/graphql/Fragments';
|
||||
|
||||
export const CLEAR_DOWNLOADER = gql`
|
||||
${FULL_DOWNLOAD_STATUS}
|
||||
mutation CLEAR_DOWNLOADER($input: ClearDownloaderInput = {}) {
|
||||
clearDownloader(input: $input) {
|
||||
clientMutationId
|
||||
downloadStatus {
|
||||
...FULL_DOWNLOAD_STATUS
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const DELETE_DOWNLOADED_CHAPTER = gql`
|
||||
${FULL_CHAPTER_FIELDS}
|
||||
mutation DELETE_DOWNLOADED_CHAPTER($input: DeleteDownloadedChapterInput!) {
|
||||
deleteDownloadedChapter(input: $input) {
|
||||
clientMutationId
|
||||
chapters {
|
||||
...FULL_CHAPTER_FIELDS
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const DELETE_DOWNLOADED_CHAPTERS = gql`
|
||||
${FULL_CHAPTER_FIELDS}
|
||||
mutation DELETE_DOWNLOADED_CHAPTERS($input: DeleteDownloadedChaptersInput!) {
|
||||
deleteDownloadedChapters(input: $input) {
|
||||
clientMutationId
|
||||
chapters {
|
||||
...FULL_CHAPTER_FIELDS
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const DEQUEUE_CHAPTER_DOWNLOAD = gql`
|
||||
${FULL_DOWNLOAD_STATUS}
|
||||
mutation DEQUEUE_CHAPTER_DOWNLOAD($input: DequeueChapterDownloadInput!) {
|
||||
dequeueChapterDownload(input: $input) {
|
||||
clientMutationId
|
||||
downloadStatus {
|
||||
...FULL_DOWNLOAD_STATUS
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const DEQUEUE_CHAPTER_DOWNLOADS = gql`
|
||||
${FULL_DOWNLOAD_STATUS}
|
||||
mutation DEQUEUE_CHAPTER_DOWNLOADS($input: DequeueChapterDownloadsInput!) {
|
||||
dequeueChapterDownloads(input: $input) {
|
||||
clientMutationId
|
||||
downloadStatus {
|
||||
...FULL_DOWNLOAD_STATUS
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const ENQUEUE_CHAPTER_DOWNLOAD = gql`
|
||||
${FULL_DOWNLOAD_STATUS}
|
||||
mutation ENQUEUE_CHAPTER_DOWNLOAD($input: EnqueueChapterDownloadInput!) {
|
||||
enqueueChapterDownload(input: $input) {
|
||||
clientMutationId
|
||||
downloadStatus {
|
||||
...FULL_DOWNLOAD_STATUS
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const ENQUEUE_CHAPTER_DOWNLOADS = gql`
|
||||
${FULL_DOWNLOAD_STATUS}
|
||||
mutation ENQUEUE_CHAPTER_DOWNLOADS($input: EnqueueChapterDownloadsInput!) {
|
||||
enqueueChapterDownloads(input: $input) {
|
||||
clientMutationId
|
||||
downloadStatus {
|
||||
...FULL_DOWNLOAD_STATUS
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const REORDER_CHAPTER_DOWNLOAD = gql`
|
||||
${FULL_DOWNLOAD_STATUS}
|
||||
mutation REORDER_CHAPTER_DOWNLOAD($input: ReorderChapterDownloadInput!) {
|
||||
reorderChapterDownload(input: $input) {
|
||||
clientMutationId
|
||||
downloadStatus {
|
||||
...FULL_DOWNLOAD_STATUS
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const START_DOWNLOADER = gql`
|
||||
${FULL_DOWNLOAD_STATUS}
|
||||
mutation START_DOWNLOADER($input: StartDownloaderInput = {}) {
|
||||
startDownloader(input: $input) {
|
||||
clientMutationId
|
||||
downloadStatus {
|
||||
...FULL_DOWNLOAD_STATUS
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const STOP_DOWNLOADER = gql`
|
||||
${FULL_DOWNLOAD_STATUS}
|
||||
mutation STOP_DOWNLOADER($input: StopDownloaderInput = {}) {
|
||||
stopDownloader(input: $input) {
|
||||
clientMutationId
|
||||
downloadStatus {
|
||||
...FULL_DOWNLOAD_STATUS
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
59
src/lib/graphql/mutations/ExtensionMutation.ts
Normal file
59
src/lib/graphql/mutations/ExtensionMutation.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* 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 gql from 'graphql-tag';
|
||||
import { FULL_EXTENSION_FIELDS } from '@/lib/graphql/Fragments';
|
||||
|
||||
// makes the server fetch and return the latest extensions
|
||||
export const GET_EXTENSIONS_FETCH = gql`
|
||||
${FULL_EXTENSION_FIELDS}
|
||||
mutation GET_EXTENSIONS_FETCH($input: FetchExtensionsInput = {}) {
|
||||
fetchExtensions(input: $input) {
|
||||
clientMutationId
|
||||
extensions {
|
||||
...FULL_EXTENSION_FIELDS
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const UPDATE_EXTENSION = gql`
|
||||
${FULL_EXTENSION_FIELDS}
|
||||
mutation UPDATE_EXTENSION($input: UpdateExtensionInput!) {
|
||||
updateExtension(input: $input) {
|
||||
clientMutationId
|
||||
extension {
|
||||
...FULL_EXTENSION_FIELDS
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const UPDATE_EXTENSIONS = gql`
|
||||
${FULL_EXTENSION_FIELDS}
|
||||
mutation UPDATE_EXTENSIONS($input: UpdateExtensionsInput!) {
|
||||
updateExtensions(input: $input) {
|
||||
clientMutationId
|
||||
extensions {
|
||||
...FULL_EXTENSION_FIELDS
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const INSTALL_EXTERNAL_EXTENSION = gql`
|
||||
${FULL_EXTENSION_FIELDS}
|
||||
mutation INSTALL_EXTERNAL_EXTENSION($file: Upload!) {
|
||||
installExternalExtension(input: { extensionFile: $file }) {
|
||||
extension {
|
||||
...FULL_EXTENSION_FIELDS
|
||||
}
|
||||
clientMutationId
|
||||
}
|
||||
}
|
||||
`;
|
||||
34
src/lib/graphql/mutations/GlobalMetadataMutation.ts
Normal file
34
src/lib/graphql/mutations/GlobalMetadataMutation.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* 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 gql from 'graphql-tag';
|
||||
import { GLOBAL_METADATA } from '@/lib/graphql/Fragments';
|
||||
|
||||
export const DELETE_GLOBAL_METADATA = gql`
|
||||
${GLOBAL_METADATA}
|
||||
mutation DELETE_GLOBAL_METADATA($input: DeleteGlobalMetaInput!) {
|
||||
deleteGlobalMeta(input: $input) {
|
||||
clientMutationId
|
||||
meta {
|
||||
...GLOBAL_METADATA
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const SET_GLOBAL_METADATA = gql`
|
||||
${GLOBAL_METADATA}
|
||||
mutation SET_GLOBAL_METADATA($input: SetGlobalMetaInput!) {
|
||||
setGlobalMeta(input: $input) {
|
||||
clientMutationId
|
||||
meta {
|
||||
...GLOBAL_METADATA
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
106
src/lib/graphql/mutations/MangaMutation.ts
Normal file
106
src/lib/graphql/mutations/MangaMutation.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* 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 gql from 'graphql-tag';
|
||||
import { FULL_MANGA_FIELDS } from '@/lib/graphql/Fragments';
|
||||
|
||||
export const DELETE_MANGA_METADATA = gql`
|
||||
${FULL_MANGA_FIELDS}
|
||||
mutation DELETE_MANGA_METADATA($input: DeleteMangaMetaInput!) {
|
||||
deleteMangaMeta(input: $input) {
|
||||
clientMutationId
|
||||
meta {
|
||||
key
|
||||
value
|
||||
manga {
|
||||
...FULL_MANGA_FIELDS
|
||||
}
|
||||
}
|
||||
manga {
|
||||
...FULL_MANGA_FIELDS
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// makes the server fetch and return the manga
|
||||
export const GET_MANGA_FETCH = gql`
|
||||
${FULL_MANGA_FIELDS}
|
||||
mutation GET_MANGA_FETCH($input: FetchMangaInput!) {
|
||||
fetchManga(input: $input) {
|
||||
clientMutationId
|
||||
manga {
|
||||
...FULL_MANGA_FIELDS
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const SET_MANGA_METADATA = gql`
|
||||
${FULL_MANGA_FIELDS}
|
||||
mutation SET_MANGA_METADATA($input: SetMangaMetaInput!) {
|
||||
setMangaMeta(input: $input) {
|
||||
clientMutationId
|
||||
meta {
|
||||
key
|
||||
value
|
||||
manga {
|
||||
...FULL_MANGA_FIELDS
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const UPDATE_MANGA = gql`
|
||||
${FULL_MANGA_FIELDS}
|
||||
mutation UPDATE_MANGA($input: UpdateMangaInput!) {
|
||||
updateManga(input: $input) {
|
||||
clientMutationId
|
||||
manga {
|
||||
...FULL_MANGA_FIELDS
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const UPDATE_MANGA_CATEGORIES = gql`
|
||||
${FULL_MANGA_FIELDS}
|
||||
mutation UPDATE_MANGA_CATEGORIES($input: UpdateMangaCategoriesInput!) {
|
||||
updateMangaCategories(input: $input) {
|
||||
clientMutationId
|
||||
manga {
|
||||
...FULL_MANGA_FIELDS
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const UPDATE_MANGAS = gql`
|
||||
${FULL_MANGA_FIELDS}
|
||||
mutation UPDATE_MANGAS($input: UpdateMangasInput!) {
|
||||
updateMangas(input: $input) {
|
||||
clientMutationId
|
||||
mangas {
|
||||
...FULL_MANGA_FIELDS
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const UPDATE_MANGAS_CATEGORIES = gql`
|
||||
${FULL_MANGA_FIELDS}
|
||||
mutation UPDATE_MANGAS_CATEGORIES($input: UpdateMangasCategoriesInput!) {
|
||||
updateMangasCategories(input: $input) {
|
||||
clientMutationId
|
||||
mangas {
|
||||
...FULL_MANGA_FIELDS
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
23
src/lib/graphql/mutations/ServerInfoMutation.ts
Normal file
23
src/lib/graphql/mutations/ServerInfoMutation.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* 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 gql from 'graphql-tag';
|
||||
import { WEBUI_UPDATE_STATUS } from '@/lib/graphql/Fragments';
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const UPDATE_WEBUI = gql`
|
||||
${WEBUI_UPDATE_STATUS}
|
||||
mutation UPDATE_WEBUI($input: WebUIUpdateInput = {}) {
|
||||
updateWebUI(input: $input) {
|
||||
clientMutationId
|
||||
updateStatus {
|
||||
...WEBUI_UPDATE_STATUS
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
34
src/lib/graphql/mutations/SettingsMutation.ts
Normal file
34
src/lib/graphql/mutations/SettingsMutation.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* 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 gql from 'graphql-tag';
|
||||
import { SERVER_SETTINGS } from '@/lib/graphql/Fragments';
|
||||
|
||||
export const RESET_SERVER_SETTINGS = gql`
|
||||
${SERVER_SETTINGS}
|
||||
mutation RESET_SERVER_SETTINGS($input: ResetSettingsInput!) {
|
||||
resetSettings(input: $input) {
|
||||
clientMutationId
|
||||
settings {
|
||||
...SERVER_SETTINGS
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const UPDATE_SERVER_SETTINGS = gql`
|
||||
${SERVER_SETTINGS}
|
||||
mutation UPDATE_SERVER_SETTINGS($input: SetSettingsInput!) {
|
||||
setSettings(input: $input) {
|
||||
clientMutationId
|
||||
settings {
|
||||
...SERVER_SETTINGS
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
35
src/lib/graphql/mutations/SourceMutation.ts
Normal file
35
src/lib/graphql/mutations/SourceMutation.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* 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 gql from 'graphql-tag';
|
||||
import { BASE_MANGA_FIELDS, FULL_SOURCE_FIELDS } from '@/lib/graphql/Fragments';
|
||||
|
||||
export const GET_SOURCE_MANGAS_FETCH = gql`
|
||||
${BASE_MANGA_FIELDS}
|
||||
mutation GET_SOURCE_MANGAS_FETCH($input: FetchSourceMangaInput!) {
|
||||
fetchSourceManga(input: $input) {
|
||||
clientMutationId
|
||||
hasNextPage
|
||||
mangas {
|
||||
...BASE_MANGA_FIELDS
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const UPDATE_SOURCE_PREFERENCES = gql`
|
||||
${FULL_SOURCE_FIELDS}
|
||||
mutation UPDATE_SOURCE_PREFERENCES($input: UpdateSourcePreferenceInput!) {
|
||||
updateSourcePreference(input: $input) {
|
||||
clientMutationId
|
||||
source {
|
||||
...FULL_SOURCE_FIELDS
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
42
src/lib/graphql/mutations/UpdaterMutation.ts
Normal file
42
src/lib/graphql/mutations/UpdaterMutation.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* 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 gql from 'graphql-tag';
|
||||
import { PARTIAL_UPDATER_STATUS } from '@/lib/graphql/Fragments';
|
||||
|
||||
export const UPDATE_CATEGORY_MANGAS = gql`
|
||||
${PARTIAL_UPDATER_STATUS}
|
||||
mutation UPDATE_CATEGORY_MANGAS($input: UpdateCategoryMangaInput!) {
|
||||
updateCategoryManga(input: $input) {
|
||||
clientMutationId
|
||||
updateStatus {
|
||||
...PARTIAL_UPDATER_STATUS
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const UPDATE_LIBRARY_MANGAS = gql`
|
||||
${PARTIAL_UPDATER_STATUS}
|
||||
mutation UPDATE_LIBRARY_MANGAS($input: UpdateLibraryMangaInput = {}) {
|
||||
updateLibraryManga(input: $input) {
|
||||
clientMutationId
|
||||
updateStatus {
|
||||
...PARTIAL_UPDATER_STATUS
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const STOP_UPDATER = gql`
|
||||
mutation STOP_UPDATER($input: UpdateStopInput = {}) {
|
||||
updateStop(input: $input) {
|
||||
clientMutationId
|
||||
}
|
||||
}
|
||||
`;
|
||||
30
src/lib/graphql/queries/BackupQuery.ts
Normal file
30
src/lib/graphql/queries/BackupQuery.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* 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 gql from 'graphql-tag';
|
||||
|
||||
export const VALIDATE_BACKUP = gql`
|
||||
query VALIDATE_BACKUP($input: ValidateBackupInput!) {
|
||||
validateBackup(input: $input) {
|
||||
missingSources {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const GET_RESTORE_STATUS = gql`
|
||||
query GET_RESTORE_STATUS {
|
||||
restoreStatus {
|
||||
mangaProgress
|
||||
state
|
||||
totalManga
|
||||
}
|
||||
}
|
||||
`;
|
||||
73
src/lib/graphql/queries/CategoryQuery.ts
Normal file
73
src/lib/graphql/queries/CategoryQuery.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* 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 gql from 'graphql-tag';
|
||||
import { FULL_CATEGORY_FIELDS, FULL_MANGA_FIELDS, PAGE_INFO } from '@/lib/graphql/Fragments';
|
||||
|
||||
export const GET_CATEGORIES = gql`
|
||||
${FULL_CATEGORY_FIELDS}
|
||||
${PAGE_INFO}
|
||||
query GET_CATEGORIES(
|
||||
$after: Cursor
|
||||
$before: Cursor
|
||||
$condition: CategoryConditionInput
|
||||
$filter: CategoryFilterInput
|
||||
$first: Int
|
||||
$last: Int
|
||||
$offset: Int
|
||||
$orderBy: CategoryOrderBy
|
||||
$orderByType: SortOrder
|
||||
) {
|
||||
categories(
|
||||
after: $after
|
||||
before: $before
|
||||
condition: $condition
|
||||
filter: $filter
|
||||
first: $first
|
||||
last: $last
|
||||
offset: $offset
|
||||
orderBy: $orderBy
|
||||
orderByType: $orderByType
|
||||
) {
|
||||
nodes {
|
||||
...FULL_CATEGORY_FIELDS
|
||||
}
|
||||
pageInfo {
|
||||
...PAGE_INFO
|
||||
}
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const GET_CATEGORY = gql`
|
||||
${FULL_CATEGORY_FIELDS}
|
||||
query GET_CATEGORY($id: Int!) {
|
||||
category(id: $id) {
|
||||
...FULL_CATEGORY_FIELDS
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const GET_CATEGORY_MANGAS = gql`
|
||||
${FULL_MANGA_FIELDS}
|
||||
${PAGE_INFO}
|
||||
query GET_CATEGORY_MANGAS($id: Int!) {
|
||||
category(id: $id) {
|
||||
mangas {
|
||||
nodes {
|
||||
...FULL_MANGA_FIELDS
|
||||
}
|
||||
pageInfo {
|
||||
...PAGE_INFO
|
||||
}
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
57
src/lib/graphql/queries/ChapterQuery.ts
Normal file
57
src/lib/graphql/queries/ChapterQuery.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* 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 gql from 'graphql-tag';
|
||||
import { FULL_CHAPTER_FIELDS, PAGE_INFO } from '@/lib/graphql/Fragments';
|
||||
|
||||
// returns the current chapter from the database
|
||||
export const GET_CHAPTER = gql`
|
||||
${FULL_CHAPTER_FIELDS}
|
||||
query GET_CHAPTER($id: Int!) {
|
||||
chapter(id: $id) {
|
||||
...FULL_CHAPTER_FIELDS
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// returns the current chapters from the database
|
||||
export const GET_CHAPTERS = gql`
|
||||
${FULL_CHAPTER_FIELDS}
|
||||
${PAGE_INFO}
|
||||
query GET_CHAPTERS(
|
||||
$after: Cursor
|
||||
$before: Cursor
|
||||
$condition: ChapterConditionInput
|
||||
$filter: ChapterFilterInput
|
||||
$first: Int
|
||||
$last: Int
|
||||
$offset: Int
|
||||
$orderBy: ChapterOrderBy
|
||||
$orderByType: SortOrder
|
||||
) {
|
||||
chapters(
|
||||
after: $after
|
||||
before: $before
|
||||
condition: $condition
|
||||
filter: $filter
|
||||
first: $first
|
||||
last: $last
|
||||
offset: $offset
|
||||
orderBy: $orderBy
|
||||
orderByType: $orderByType
|
||||
) {
|
||||
nodes {
|
||||
...FULL_CHAPTER_FIELDS
|
||||
}
|
||||
pageInfo {
|
||||
...PAGE_INFO
|
||||
}
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
`;
|
||||
20
src/lib/graphql/queries/DownloaderQuery.ts
Normal file
20
src/lib/graphql/queries/DownloaderQuery.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* 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 gql from 'graphql-tag';
|
||||
import { FULL_DOWNLOAD_STATUS } from '@/lib/graphql/Fragments';
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const GET_DOWNLOAD_STATUS = gql`
|
||||
${FULL_DOWNLOAD_STATUS}
|
||||
query GET_DOWNLOAD_STATUS {
|
||||
downloadStatus {
|
||||
...FULL_DOWNLOAD_STATUS
|
||||
}
|
||||
}
|
||||
`;
|
||||
57
src/lib/graphql/queries/ExtensionQuery.ts
Normal file
57
src/lib/graphql/queries/ExtensionQuery.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* 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 gql from 'graphql-tag';
|
||||
import { FULL_EXTENSION_FIELDS, PAGE_INFO } from '@/lib/graphql/Fragments';
|
||||
|
||||
// returns the current extension from the database
|
||||
export const GET_EXTENSION = gql`
|
||||
${FULL_EXTENSION_FIELDS}
|
||||
query GET_EXTENSION($pkgName: String!) {
|
||||
extension(pkgName: $pkgName) {
|
||||
...FULL_EXTENSION_FIELDS
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// returns the current extensions from the database
|
||||
export const GET_EXTENSIONS = gql`
|
||||
${FULL_EXTENSION_FIELDS}
|
||||
${PAGE_INFO}
|
||||
query GET_EXTENSIONS(
|
||||
$after: Cursor
|
||||
$before: Cursor
|
||||
$condition: ExtensionConditionInput
|
||||
$filter: ExtensionFilterInput
|
||||
$first: Int
|
||||
$last: Int
|
||||
$offset: Int
|
||||
$orderBy: ExtensionOrderBy
|
||||
$orderByType: SortOrder
|
||||
) {
|
||||
extensions(
|
||||
after: $after
|
||||
before: $before
|
||||
condition: $condition
|
||||
filter: $filter
|
||||
first: $first
|
||||
last: $last
|
||||
offset: $offset
|
||||
orderBy: $orderBy
|
||||
orderByType: $orderByType
|
||||
) {
|
||||
nodes {
|
||||
...FULL_EXTENSION_FIELDS
|
||||
}
|
||||
pageInfo {
|
||||
...PAGE_INFO
|
||||
}
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
`;
|
||||
55
src/lib/graphql/queries/GlobalMetadataQuery.ts
Normal file
55
src/lib/graphql/queries/GlobalMetadataQuery.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* 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 gql from 'graphql-tag';
|
||||
import { GLOBAL_METADATA, PAGE_INFO } from '@/lib/graphql/Fragments.ts';
|
||||
|
||||
export const GET_GLOBAL_METADATA = gql`
|
||||
${GLOBAL_METADATA}
|
||||
query GET_GLOBAL_METADATA($key: String!) {
|
||||
meta(key: $key) {
|
||||
...GLOBAL_METADATA
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const GET_GLOBAL_METADATAS = gql`
|
||||
${GLOBAL_METADATA}
|
||||
${PAGE_INFO}
|
||||
query GET_GLOBAL_METADATAS(
|
||||
$after: Cursor
|
||||
$before: Cursor
|
||||
$condition: MetaConditionInput
|
||||
$filter: MetaFilterInput
|
||||
$first: Int
|
||||
$last: Int
|
||||
$offset: Int
|
||||
$orderBy: MetaOrderBy
|
||||
$orderByType: SortOrder
|
||||
) {
|
||||
metas(
|
||||
after: $after
|
||||
before: $before
|
||||
condition: $condition
|
||||
filter: $filter
|
||||
first: $first
|
||||
last: $last
|
||||
offset: $offset
|
||||
orderBy: $orderBy
|
||||
orderByType: $orderByType
|
||||
) {
|
||||
nodes {
|
||||
...GLOBAL_METADATA
|
||||
}
|
||||
pageInfo {
|
||||
...PAGE_INFO
|
||||
}
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
`;
|
||||
59
src/lib/graphql/queries/MangaQuery.ts
Normal file
59
src/lib/graphql/queries/MangaQuery.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* 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 gql from 'graphql-tag';
|
||||
import { FULL_CHAPTER_FIELDS, FULL_MANGA_FIELDS, PAGE_INFO } from '@/lib/graphql/Fragments';
|
||||
|
||||
// returns the current manga from the database
|
||||
export const GET_MANGA = gql`
|
||||
${FULL_MANGA_FIELDS}
|
||||
${FULL_CHAPTER_FIELDS}
|
||||
query GET_MANGA($id: Int!) {
|
||||
manga(id: $id) {
|
||||
...FULL_MANGA_FIELDS
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
// returns the current manga from the database
|
||||
export const GET_MANGAS = gql`
|
||||
${FULL_MANGA_FIELDS}
|
||||
${FULL_CHAPTER_FIELDS}
|
||||
${PAGE_INFO}
|
||||
query GET_MANGAS(
|
||||
$after: Cursor
|
||||
$before: Cursor
|
||||
$condition: MangaConditionInput
|
||||
$filter: MangaFilterInput
|
||||
$first: Int
|
||||
$last: Int
|
||||
$offset: Int
|
||||
$orderBy: MangaOrderBy
|
||||
$orderByType: SortOrder
|
||||
) {
|
||||
mangas(
|
||||
after: $after
|
||||
before: $before
|
||||
condition: $condition
|
||||
filter: $filter
|
||||
first: $first
|
||||
last: $last
|
||||
offset: $offset
|
||||
orderBy: $orderBy
|
||||
orderByType: $orderByType
|
||||
) {
|
||||
nodes {
|
||||
...FULL_MANGA_FIELDS
|
||||
}
|
||||
pageInfo {
|
||||
...PAGE_INFO
|
||||
}
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
`;
|
||||
52
src/lib/graphql/queries/ServerInfoQuery.ts
Normal file
52
src/lib/graphql/queries/ServerInfoQuery.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* 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 gql from 'graphql-tag';
|
||||
import { WEBUI_UPDATE_INFO, WEBUI_UPDATE_STATUS } from '@/lib/graphql/Fragments';
|
||||
|
||||
export const GET_ABOUT = gql`
|
||||
query GET_ABOUT {
|
||||
about {
|
||||
buildTime
|
||||
buildType
|
||||
discord
|
||||
github
|
||||
name
|
||||
revision
|
||||
version
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const CHECK_FOR_SERVER_UPDATES = gql`
|
||||
query CHECK_FOR_SERVER_UPDATES {
|
||||
checkForServerUpdates {
|
||||
channel
|
||||
tag
|
||||
url
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const CHECK_FOR_WEBUI_UPDATE = gql`
|
||||
${WEBUI_UPDATE_INFO}
|
||||
query CHECK_FOR_WEBUI_UPDATE {
|
||||
checkForWebUIUpdate {
|
||||
...WEBUI_UPDATE_INFO
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const GET_WEBUI_UPDATE_STATUS = gql`
|
||||
${WEBUI_UPDATE_STATUS}
|
||||
query GET_WEBUI_UPDATE_STATUS {
|
||||
getWebUIUpdateStatus {
|
||||
...WEBUI_UPDATE_STATUS
|
||||
}
|
||||
}
|
||||
`;
|
||||
20
src/lib/graphql/queries/SettingsQuery.ts
Normal file
20
src/lib/graphql/queries/SettingsQuery.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* 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 gql from 'graphql-tag';
|
||||
import { SERVER_SETTINGS } from '@/lib/graphql/Fragments';
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const GET_SERVER_SETTINGS = gql`
|
||||
${SERVER_SETTINGS}
|
||||
query GET_SERVER_SETTINGS {
|
||||
settings {
|
||||
...SERVER_SETTINGS
|
||||
}
|
||||
}
|
||||
`;
|
||||
30
src/lib/graphql/queries/SourceQuery.ts
Normal file
30
src/lib/graphql/queries/SourceQuery.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* 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 gql from 'graphql-tag';
|
||||
import { FULL_SOURCE_FIELDS, PARTIAL_SOURCE_FIELDS } from '@/lib/graphql/Fragments';
|
||||
|
||||
export const GET_SOURCE = gql`
|
||||
${FULL_SOURCE_FIELDS}
|
||||
query GET_SOURCE($id: LongString!) {
|
||||
source(id: $id) {
|
||||
...FULL_SOURCE_FIELDS
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const GET_SOURCES = gql`
|
||||
${PARTIAL_SOURCE_FIELDS}
|
||||
query GET_SOURCES {
|
||||
sources {
|
||||
nodes {
|
||||
...PARTIAL_SOURCE_FIELDS
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
28
src/lib/graphql/queries/UpdaterQuery.ts
Normal file
28
src/lib/graphql/queries/UpdaterQuery.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* 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 gql from 'graphql-tag';
|
||||
import { FULL_UPDATER_STATUS } from '@/lib/graphql/Fragments';
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const GET_UPDATE_STATUS = gql`
|
||||
${FULL_UPDATER_STATUS}
|
||||
query GET_UPDATE_STATUS {
|
||||
updateStatus {
|
||||
...FULL_UPDATER_STATUS
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const GET_LAST_UPDATE_TIMESTAMP = gql`
|
||||
query GET_LAST_UPDATE_TIMESTAMP {
|
||||
lastUpdateTimestamp {
|
||||
timestamp
|
||||
}
|
||||
}
|
||||
`;
|
||||
20
src/lib/graphql/subscriptions/DownloaderSubscription.ts
Normal file
20
src/lib/graphql/subscriptions/DownloaderSubscription.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* 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 gql from 'graphql-tag';
|
||||
import { FULL_DOWNLOAD_STATUS } from '@/lib/graphql/Fragments';
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const DOWNLOAD_STATUS_SUBSCRIPTION = gql`
|
||||
${FULL_DOWNLOAD_STATUS}
|
||||
subscription DOWNLOAD_STATUS_SUBSCRIPTION {
|
||||
downloadChanged {
|
||||
...FULL_DOWNLOAD_STATUS
|
||||
}
|
||||
}
|
||||
`;
|
||||
20
src/lib/graphql/subscriptions/ServerInfoSubscription.ts
Normal file
20
src/lib/graphql/subscriptions/ServerInfoSubscription.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* 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 gql from 'graphql-tag';
|
||||
import { WEBUI_UPDATE_STATUS } from '@/lib/graphql/Fragments';
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const WEBUI_UPDATE_SUBSCRIPTION = gql`
|
||||
${WEBUI_UPDATE_STATUS}
|
||||
subscription WEBUI_UPDATE_SUBSCRIPTION {
|
||||
webUIUpdateStatusChange {
|
||||
...WEBUI_UPDATE_STATUS
|
||||
}
|
||||
}
|
||||
`;
|
||||
20
src/lib/graphql/subscriptions/UpdaterSubscription.ts
Normal file
20
src/lib/graphql/subscriptions/UpdaterSubscription.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* 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 gql from 'graphql-tag';
|
||||
import { FULL_UPDATER_STATUS } from '@/lib/graphql/Fragments';
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const UPDATER_SUBSCRIPTION = gql`
|
||||
${FULL_UPDATER_STATUS}
|
||||
subscription UPDATER_SUBSCRIPTION {
|
||||
updateStatusChanged {
|
||||
...FULL_UPDATER_STATUS
|
||||
}
|
||||
}
|
||||
`;
|
||||
40
src/lib/requests/CustomCache.ts
Normal file
40
src/lib/requests/CustomCache.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export class CustomCache {
|
||||
private keyToResponseMap = new Map<string, unknown>();
|
||||
|
||||
private keyToFetchTimestampMap = new Map<string, number>();
|
||||
|
||||
public readonly createKeyFn = (endpoint: string, data: unknown): string => `${endpoint}_${JSON.stringify(data)}`;
|
||||
|
||||
constructor(createKeyFn?: (endpoint: string, data: unknown) => string) {
|
||||
this.createKeyFn = createKeyFn ?? this.createKeyFn;
|
||||
}
|
||||
|
||||
public getKeyFor(key: string, data: unknown): string {
|
||||
return this.createKeyFn(key, data);
|
||||
}
|
||||
|
||||
public cacheResponse(endpoint: string, data: unknown, response: unknown) {
|
||||
const createdKey = this.getKeyFor(endpoint, data);
|
||||
this.keyToFetchTimestampMap.set(createdKey, Date.now());
|
||||
this.keyToResponseMap.set(createdKey, response);
|
||||
}
|
||||
|
||||
public getFetchTimestampFor(endpoint: string, data: unknown): number | undefined {
|
||||
const key = this.getKeyFor(endpoint, data);
|
||||
return this.keyToFetchTimestampMap.get(key);
|
||||
}
|
||||
|
||||
public getResponseFor<Response = any>(endpoint: string, data: unknown): Response | undefined {
|
||||
const key = this.getKeyFor(endpoint, data);
|
||||
return this.keyToResponseMap.get(key) as Response;
|
||||
}
|
||||
}
|
||||
1782
src/lib/requests/RequestManager.ts
Normal file
1782
src/lib/requests/RequestManager.ts
Normal file
File diff suppressed because it is too large
Load Diff
32
src/lib/requests/client/BaseClient.ts
Normal file
32
src/lib/requests/client/BaseClient.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* 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 storage from '@/util/localStorage.tsx';
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export abstract class BaseClient<Client, ClientConfig, Fetcher> {
|
||||
protected client!: Client;
|
||||
|
||||
public abstract readonly fetcher: Fetcher;
|
||||
|
||||
constructor() {
|
||||
this.createClient();
|
||||
}
|
||||
|
||||
public getBaseUrl(): string {
|
||||
const { hostname, port, protocol } = window.location;
|
||||
|
||||
// if port is 3000 it's probably running from webpack development server
|
||||
const inferredPort = port === '3000' ? '4567' : port;
|
||||
return storage.getItem('serverBaseURL', `${protocol}//${hostname}:${inferredPort}`);
|
||||
}
|
||||
|
||||
protected abstract createClient(): void;
|
||||
|
||||
public abstract updateConfig(config: Partial<ClientConfig>): void;
|
||||
}
|
||||
109
src/lib/requests/client/GraphQLClient.ts
Normal file
109
src/lib/requests/client/GraphQLClient.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* 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 {
|
||||
ApolloClient,
|
||||
ApolloClientOptions,
|
||||
InMemoryCache,
|
||||
NormalizedCacheObject,
|
||||
Reference,
|
||||
split,
|
||||
} from '@apollo/client';
|
||||
import { createUploadLink } from 'apollo-upload-client';
|
||||
import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
|
||||
import { Client, createClient } from 'graphql-ws';
|
||||
import { getMainDefinition } from '@apollo/client/utilities';
|
||||
import { BaseClient } from '@/lib/requests/client/BaseClient.ts';
|
||||
import { StrictTypedTypePolicies } from '@/lib/graphql/generated/apollo-helpers.ts';
|
||||
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
const typePolicies: StrictTypedTypePolicies = {
|
||||
GlobalMetaType: { keyFields: ['key'] },
|
||||
ExtensionType: { keyFields: ['apkName'] },
|
||||
AboutPayload: { keyFields: [] },
|
||||
Query: {
|
||||
fields: {
|
||||
chapters: {
|
||||
keyArgs: ['condition', 'filter', 'orderBy', 'orderByType'],
|
||||
merge(existing, incoming) {
|
||||
const merged = {
|
||||
...existing,
|
||||
...incoming,
|
||||
nodes: existing?.nodes ?? [],
|
||||
};
|
||||
const isRefetch = incoming.nodes.some(
|
||||
(incomingChapter) =>
|
||||
existing?.nodes.some(
|
||||
(existingChapter) =>
|
||||
(existingChapter as unknown as Reference).__ref ===
|
||||
(incomingChapter as unknown as Reference).__ref,
|
||||
),
|
||||
);
|
||||
if (!isRefetch) {
|
||||
merged.nodes = [...(existing?.nodes ?? []), ...incoming.nodes];
|
||||
}
|
||||
return merged;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
/* eslint-enable no-underscore-dangle */
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export class GraphQLClient extends BaseClient<
|
||||
ApolloClient<NormalizedCacheObject>,
|
||||
ApolloClientOptions<NormalizedCacheObject>,
|
||||
null
|
||||
> {
|
||||
readonly fetcher = null;
|
||||
|
||||
public declare client: ApolloClient<NormalizedCacheObject>;
|
||||
|
||||
private wsClient!: Client;
|
||||
|
||||
public override getBaseUrl(): string {
|
||||
return `${super.getBaseUrl()}/api/graphql`;
|
||||
}
|
||||
|
||||
private createUploadLink() {
|
||||
return createUploadLink({ uri: () => this.getBaseUrl() });
|
||||
}
|
||||
|
||||
private createWSLink() {
|
||||
return new GraphQLWsLink(this.wsClient);
|
||||
}
|
||||
|
||||
private createLink() {
|
||||
return split(
|
||||
({ query }) => {
|
||||
const definition = getMainDefinition(query);
|
||||
return definition.kind === 'OperationDefinition' && definition.operation === 'subscription';
|
||||
},
|
||||
this.createWSLink(),
|
||||
this.createUploadLink(),
|
||||
);
|
||||
}
|
||||
|
||||
protected createClient() {
|
||||
this.wsClient = createClient({
|
||||
url: () => this.getBaseUrl().replace(/http(|s)/g, 'ws'),
|
||||
keepAlive: 20000,
|
||||
});
|
||||
|
||||
this.client = new ApolloClient({
|
||||
cache: new InMemoryCache({
|
||||
typePolicies,
|
||||
}),
|
||||
connectToDevTools: true,
|
||||
link: this.createLink(),
|
||||
});
|
||||
}
|
||||
|
||||
public override updateConfig() {}
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
*/
|
||||
|
||||
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
|
||||
import storage from '@/util/localStorage';
|
||||
import { BaseClient } from '@/lib/requests/client/BaseClient.ts';
|
||||
|
||||
export enum HttpMethod {
|
||||
GET = 'get',
|
||||
@@ -28,13 +28,10 @@ export interface IRestClient {
|
||||
patch<Data = any, Response = SimpleRestResponse<Data>>(url: string, data?: any): Promise<Response>;
|
||||
}
|
||||
|
||||
export class RestClient implements IRestClient {
|
||||
protected client!: AxiosInstance;
|
||||
|
||||
constructor() {
|
||||
this.createClient();
|
||||
}
|
||||
|
||||
export class RestClient
|
||||
extends BaseClient<AxiosInstance, AxiosInstance['defaults'], <Data = any>(url: string, data: any) => Promise<Data>>
|
||||
implements IRestClient
|
||||
{
|
||||
public readonly fetcher = async <Data = any>(
|
||||
url: string,
|
||||
{
|
||||
@@ -75,12 +72,8 @@ export class RestClient implements IRestClient {
|
||||
return result.data;
|
||||
};
|
||||
|
||||
private createClient(): void {
|
||||
const { hostname, port, protocol } = window.location;
|
||||
|
||||
// if port is 3000 it's probably running from webpack development server
|
||||
const inferredPort = port === '3000' ? '4567' : port;
|
||||
const baseURL = storage.getItem('serverBaseURL', `${protocol}//${hostname}:${inferredPort}`);
|
||||
protected override createClient(): void {
|
||||
const baseURL = this.getBaseUrl();
|
||||
|
||||
this.client = axios.create({
|
||||
// baseURL must not have trailing slash
|
||||
@@ -18,31 +18,27 @@ import { DragDropContext, Draggable } from 'react-beautiful-dnd';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { IChapter, IQueue } from '@/typings';
|
||||
import requestManager from '@/lib/RequestManager';
|
||||
import requestManager from '@/lib/requests/RequestManager.ts';
|
||||
import StrictModeDroppable from '@/lib/StrictModeDroppable';
|
||||
import makeToast from '@/components/util/Toast';
|
||||
import { NavbarToolbar } from '@/components/navbar/DefaultNavBar';
|
||||
import DownloadStateIndicator from '@/components/molecules/DownloadStateIndicator';
|
||||
import useSubscription from '@/components/library/useSubscription';
|
||||
import EmptyView from '@/components/util/EmptyView';
|
||||
import NavbarContext from '@/components/context/NavbarContext';
|
||||
|
||||
const initialQueue = {
|
||||
status: 'Stopped',
|
||||
queue: [],
|
||||
} as IQueue;
|
||||
import { DownloadType } from '@/lib/graphql/generated/graphql.ts';
|
||||
import { TChapter } from '@/typings.ts';
|
||||
|
||||
const DownloadQueue: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { data: queueState } = useSubscription<IQueue>('downloads');
|
||||
const { queue, status } = queueState ?? initialQueue;
|
||||
const { data: downloaderData } = requestManager.useDownloadSubscription();
|
||||
const queue = (downloaderData?.downloadChanged.queue as DownloadType[]) ?? [];
|
||||
const status = downloaderData?.downloadChanged.state ?? 'STARTED';
|
||||
|
||||
const { setTitle, setAction } = useContext(NavbarContext);
|
||||
|
||||
const toggleQueueStatus = () => {
|
||||
if (status === 'Stopped') {
|
||||
if (status === 'STOPPED') {
|
||||
requestManager.startDownloads();
|
||||
} else {
|
||||
requestManager.stopDownloads();
|
||||
@@ -60,8 +56,8 @@ const DownloadQueue: React.FC = () => {
|
||||
return <EmptyView message={t('download.queue.label.no_downloads')} />;
|
||||
}
|
||||
|
||||
const handleDelete = async (chapter: IChapter) => {
|
||||
const isRunning = status === 'Started';
|
||||
const handleDelete = async (chapter: TChapter) => {
|
||||
const isRunning = status === 'STARTED';
|
||||
|
||||
try {
|
||||
if (isRunning) {
|
||||
@@ -71,10 +67,10 @@ const DownloadQueue: React.FC = () => {
|
||||
|
||||
await Promise.all([
|
||||
// remove from download queue
|
||||
requestManager.removeChapterFromDownloadQueue(chapter.mangaId, chapter.index).response,
|
||||
requestManager.removeChapterFromDownloadQueue(chapter.id).response,
|
||||
// delete partial download, should be handle server side?
|
||||
// bug: The folder and the last image downloaded are not deleted
|
||||
requestManager.deleteDownloadedChapter(chapter.mangaId, chapter.index).response,
|
||||
requestManager.deleteDownloadedChapter(chapter.id).response,
|
||||
]);
|
||||
} catch (error) {
|
||||
makeToast(t('download.queue.error.label.failed_to_remove'), 'error');
|
||||
@@ -91,7 +87,7 @@ const DownloadQueue: React.FC = () => {
|
||||
<>
|
||||
<NavbarToolbar>
|
||||
<IconButton onClick={toggleQueueStatus} size="large">
|
||||
{status === 'Stopped' ? <PlayArrowIcon /> : <PauseIcon />}
|
||||
{status === 'STOPPED' ? <PlayArrowIcon /> : <PauseIcon />}
|
||||
</IconButton>
|
||||
</NavbarToolbar>
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
@@ -100,8 +96,8 @@ const DownloadQueue: React.FC = () => {
|
||||
<Box ref={droppableProvided.innerRef} sx={{ pt: 1 }}>
|
||||
{queue.map((item, index) => (
|
||||
<Draggable
|
||||
key={`${item.mangaId}-${item.chapterIndex}`}
|
||||
draggableId={`${item.mangaId}-${item.chapterIndex}`}
|
||||
key={`${item.chapter.manga.id}-${item.chapter.sourceOrder}`}
|
||||
draggableId={`${item.chapter.manga.id}-${item.chapter.sourceOrder}`}
|
||||
index={index}
|
||||
>
|
||||
{(draggableProvided, snapshot) => (
|
||||
@@ -129,7 +125,7 @@ const DownloadQueue: React.FC = () => {
|
||||
<DragHandle />
|
||||
</IconButton>
|
||||
<Stack sx={{ flex: 1, ml: 1 }} direction="column">
|
||||
<Typography variant="h6">{item.manga.title}</Typography>
|
||||
<Typography variant="h6">{item.chapter.manga.title}</Typography>
|
||||
<Typography variant="caption" display="block" gutterBottom>
|
||||
{item.chapter.name}
|
||||
</Typography>
|
||||
|
||||
@@ -14,8 +14,7 @@ import { StringParam, useQueryParam } from 'use-query-params';
|
||||
import { Virtuoso } from 'react-virtuoso';
|
||||
import { Typography, useMediaQuery, useTheme } from '@mui/material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { IExtension } from '@/typings';
|
||||
import requestManager from '@/lib/RequestManager';
|
||||
import requestManager from '@/lib/requests/RequestManager.ts';
|
||||
import { extensionDefaultLangs, DefaultLanguage, langSortCmp } from '@/util/language';
|
||||
import useLocalStorage from '@/util/useLocalStorage';
|
||||
import {
|
||||
@@ -31,11 +30,12 @@ import { makeToaster } from '@/components/util/Toast';
|
||||
import LangSelect from '@/components/navbar/action/LangSelect';
|
||||
import NavbarContext from '@/components/context/NavbarContext';
|
||||
import ExtensionCard from '@/components/ExtensionCard';
|
||||
import { PartialExtension } from '@/typings.ts';
|
||||
|
||||
const LANGUAGE = 0;
|
||||
const EXTENSIONS = 1;
|
||||
|
||||
function getExtensionsInfo(extensions: IExtension[]): {
|
||||
function getExtensionsInfo(extensions: PartialExtension[]): {
|
||||
allLangs: string[];
|
||||
groupedExtensions: GroupedExtensionsResult;
|
||||
} {
|
||||
@@ -55,12 +55,12 @@ function getExtensionsInfo(extensions: IExtension[]): {
|
||||
allLangs.push(extension.lang);
|
||||
}
|
||||
}
|
||||
if (extension.installed) {
|
||||
if (extension.isInstalled) {
|
||||
if (extension.hasUpdate) {
|
||||
sortedExtensions[ExtensionState.UPDATE_PENDING].push(extension);
|
||||
return;
|
||||
}
|
||||
if (extension.obsolete) {
|
||||
if (extension.isObsolete) {
|
||||
sortedExtensions[ExtensionState.OBSOLETE].push(extension);
|
||||
return;
|
||||
}
|
||||
@@ -100,7 +100,17 @@ export default function MangaExtensions() {
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
const [query] = useQueryParam('query', StringParam);
|
||||
|
||||
const { data: allExtensions, mutate, isLoading } = requestManager.useGetExtensionList();
|
||||
const [extensionsTimestamp, setExtensionsTimestamp] = useLocalStorage('extensionsTimestamp', 0);
|
||||
const [fetchExtensions, { loading: isFetching }] = requestManager.useExtensionListFetch();
|
||||
const { data, loading: isLoading } = requestManager.useGetExtensionList();
|
||||
const allExtensions = data?.extensions.nodes;
|
||||
|
||||
useEffect(() => {
|
||||
const updateExtensionsList = Date.now() - extensionsTimestamp >= 1000 * 60; // update list in case it's older than 1 minute
|
||||
if (updateExtensionsList) {
|
||||
fetchExtensions().catch(() => setExtensionsTimestamp(Date.now()));
|
||||
}
|
||||
}, []);
|
||||
|
||||
const filteredExtensions = useMemo(
|
||||
() =>
|
||||
@@ -122,7 +132,7 @@ export default function MangaExtensions() {
|
||||
[shownLangs, groupedExtensions],
|
||||
);
|
||||
|
||||
const flatRenderItems: (IExtension | string)[] = filteredGroupedExtensions.flat(2);
|
||||
const flatRenderItems: (PartialExtension | string)[] = filteredGroupedExtensions.flat(2);
|
||||
|
||||
const [toasts, makeToast] = makeToaster(useState<React.ReactElement[]>([]));
|
||||
|
||||
@@ -134,10 +144,9 @@ export default function MangaExtensions() {
|
||||
|
||||
makeToast(t('extension.label.installing_file'), 'info');
|
||||
requestManager
|
||||
.installExtension(file)
|
||||
.installExternalExtension(file)
|
||||
.response.then(() => {
|
||||
makeToast(t('extension.label.installed_successfully'), 'success');
|
||||
mutate();
|
||||
})
|
||||
.catch(() => makeToast(t('extension.label.installation_failed'), 'error'));
|
||||
} else {
|
||||
@@ -156,7 +165,7 @@ export default function MangaExtensions() {
|
||||
<LangSelect shownLangs={shownLangs} setShownLangs={setShownLangs} allLangs={allLangs} />
|
||||
</>,
|
||||
);
|
||||
}, [t, shownLangs]);
|
||||
}, [t, shownLangs, allLangs]);
|
||||
|
||||
useEffect(() => {
|
||||
const dropHandler = async (e: Event) => {
|
||||
@@ -178,7 +187,7 @@ export default function MangaExtensions() {
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (isLoading) {
|
||||
if (isLoading || isFetching) {
|
||||
return <LoadingPlaceholder />;
|
||||
}
|
||||
|
||||
@@ -220,17 +229,9 @@ export default function MangaExtensions() {
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
const item = flatRenderItems[index] as IExtension;
|
||||
const item = flatRenderItems[index] as PartialExtension;
|
||||
|
||||
return (
|
||||
<ExtensionCard
|
||||
key={item.apkName}
|
||||
extension={item}
|
||||
notifyInstall={() => {
|
||||
mutate();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
return <ExtensionCard key={item.apkName} extension={item} />;
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
*/
|
||||
|
||||
import { Chip, Tab, Tabs, styled, Box } from '@mui/material';
|
||||
import React, { useContext, useEffect, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useContext, useEffect, useMemo } from 'react';
|
||||
import { useQueryParam, NumberParam } from 'use-query-params';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import requestManager from '@/lib/RequestManager';
|
||||
import requestManager from '@/lib/requests/RequestManager.ts';
|
||||
import NavbarContext from '@/components/context/NavbarContext';
|
||||
import EmptyView from '@/components/util/EmptyView';
|
||||
import LoadingPlaceholder from '@/components/util/LoadingPlaceholder';
|
||||
@@ -62,20 +62,34 @@ export default function Library() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { options } = useLibraryOptionsContext();
|
||||
const [lastLibraryUpdate, setLastLibraryUpdate] = useState(Date.now());
|
||||
const { data: tabsData, error: tabsError, isLoading: areCategoriesLoading } = requestManager.useGetCategories();
|
||||
const {
|
||||
data: categoriesResponse,
|
||||
error: tabsError,
|
||||
loading: areCategoriesLoading,
|
||||
refetch,
|
||||
} = requestManager.useGetCategories();
|
||||
const tabsData = categoriesResponse?.categories.nodes.filter(
|
||||
(category) => category.id !== 0 || (category.id === 0 && category.mangas.totalCount),
|
||||
);
|
||||
const tabs = tabsData ?? [];
|
||||
const librarySize = useMemo(() => tabs.map((tab) => tab.size).reduce((prev, curr) => prev + curr, 0), [tabs]);
|
||||
const librarySize = useMemo(
|
||||
() => tabs.map((tab) => tab.mangas.totalCount).reduce((prev, curr) => prev + curr, 0),
|
||||
[tabs],
|
||||
);
|
||||
|
||||
const [tabSearchParam, setTabSearchParam] = useQueryParam('tab', NumberParam);
|
||||
|
||||
const activeTab = tabs.find((tab) => tab.order === tabSearchParam) ?? tabs[0];
|
||||
const {
|
||||
data: mangaData,
|
||||
data: categoryMangaResponse,
|
||||
error: mangaError,
|
||||
isLoading: mangaLoading,
|
||||
} = requestManager.useGetCategoryMangas(activeTab?.id, { skipRequest: !activeTab });
|
||||
const mangas = mangaData ?? [];
|
||||
loading: mangaLoading,
|
||||
} = requestManager.useGetCategoryMangas(activeTab?.id, { skip: !activeTab, nextFetchPolicy: 'cache-only' });
|
||||
const mangas = categoryMangaResponse?.mangas.nodes ?? [];
|
||||
|
||||
const handleFinishedUpdate = useCallback(() => {
|
||||
refetch();
|
||||
}, [refetch]);
|
||||
|
||||
const { setTitle, setAction } = useContext(NavbarContext);
|
||||
useEffect(() => {
|
||||
@@ -91,7 +105,7 @@ export default function Library() {
|
||||
<>
|
||||
<AppbarSearch />
|
||||
<LibraryToolbarMenu />
|
||||
<UpdateChecker handleFinishedUpdate={setLastLibraryUpdate} />
|
||||
<UpdateChecker handleFinishedUpdate={handleFinishedUpdate} />
|
||||
</>,
|
||||
);
|
||||
return () => {
|
||||
@@ -101,14 +115,14 @@ export default function Library() {
|
||||
}, [t, librarySize, areCategoriesLoading, options]);
|
||||
|
||||
const handleTabChange = (newTab: number) => {
|
||||
setTabSearchParam(newTab === 0 ? undefined : newTab);
|
||||
setTabSearchParam(newTab);
|
||||
};
|
||||
|
||||
if (tabsError != null) {
|
||||
return (
|
||||
<EmptyView
|
||||
message={t('category.error.label.request_failure')}
|
||||
messageExtra={tabsError?.message ?? tabsError}
|
||||
messageExtra={tabsError.message ?? tabsError}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -125,7 +139,6 @@ export default function Library() {
|
||||
return (
|
||||
<LibraryMangaGrid
|
||||
mangas={mangas}
|
||||
lastLibraryUpdate={lastLibraryUpdate}
|
||||
message={t('library.error.label.empty')}
|
||||
isLoading={activeTab != null && mangaLoading}
|
||||
/>
|
||||
@@ -154,7 +167,7 @@ export default function Library() {
|
||||
label={
|
||||
<TitleWithSizeTag>
|
||||
{tab.name}
|
||||
{options.showTabSize ? <TitleSizeTag label={tab.size} /> : null}
|
||||
{options.showTabSize ? <TitleSizeTag label={tab.mangas.totalCount} /> : null}
|
||||
</TitleWithSizeTag>
|
||||
}
|
||||
value={tab.order}
|
||||
@@ -167,12 +180,11 @@ export default function Library() {
|
||||
(mangaError ? (
|
||||
<EmptyView
|
||||
message={t('manga.error.label.request_failure')}
|
||||
messageExtra={mangaError?.message ?? mangaError}
|
||||
messageExtra={mangaError.message ?? mangaError}
|
||||
/>
|
||||
) : (
|
||||
<LibraryMangaGrid
|
||||
mangas={mangas}
|
||||
lastLibraryUpdate={lastLibraryUpdate}
|
||||
message={t('library.error.label.empty')}
|
||||
isLoading={mangaLoading}
|
||||
/>
|
||||
|
||||
@@ -11,7 +11,8 @@ import { CircularProgress, IconButton, Stack, Tooltip, Box } from '@mui/material
|
||||
import React, { useContext, useEffect, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import requestManager from '@/lib/RequestManager';
|
||||
import { isNetworkRequestInFlight } from '@apollo/client/core/networkStatus';
|
||||
import requestManager from '@/lib/requests/RequestManager.ts';
|
||||
import NavbarContext, { useSetDefaultBackTo } from '@/components/context/NavbarContext';
|
||||
import ChapterList from '@/components/manga/ChapterList';
|
||||
import { useRefreshManga } from '@/components/manga/hooks';
|
||||
@@ -20,7 +21,7 @@ import MangaToolbarMenu from '@/components/manga/MangaToolbarMenu';
|
||||
import EmptyView from '@/components/util/EmptyView';
|
||||
import LoadingPlaceholder from '@/components/util/LoadingPlaceholder';
|
||||
|
||||
const AUTOFETCH_AGE = 60 * 60 * 24; // 24 hours
|
||||
const AUTOFETCH_AGE = 1000 * 60 * 60 * 24; // 24 hours
|
||||
|
||||
const Manga: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -29,21 +30,26 @@ const Manga: React.FC = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const autofetchedRef = useRef(false);
|
||||
|
||||
const { data: manga, error, isLoading, isValidating, mutate } = requestManager.useGetManga(id);
|
||||
const { data, error, loading: isLoading, networkStatus, refetch } = requestManager.useGetManga(id);
|
||||
const isValidating = isNetworkRequestInFlight(networkStatus);
|
||||
const manga = data?.manga;
|
||||
|
||||
const [refresh, { loading: refreshing }] = useRefreshManga(id);
|
||||
useSetDefaultBackTo('library');
|
||||
|
||||
useEffect(() => {
|
||||
// Automatically fetch manga from source if data is older then 24 hours
|
||||
// Automatically fetch manga from source if data is older then 24 hours OR manga is not initialized yet
|
||||
// Automatic fetch is done only once, to prevent issues when server does
|
||||
// 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
|
||||
) {
|
||||
|
||||
const isOutdated =
|
||||
Date.now() - Number(manga.lastFetchedAt) * 1000 > AUTOFETCH_AGE ||
|
||||
Date.now() - Number(manga.chaptersLastFetchedAt) * 1000 > AUTOFETCH_AGE;
|
||||
const refetchBecauseOutdated = manga.inLibrary && isOutdated;
|
||||
|
||||
const doFetch = !autofetchedRef.current && (refetchBecauseOutdated || !manga.initialized);
|
||||
if (doFetch) {
|
||||
autofetchedRef.current = true;
|
||||
refresh();
|
||||
}
|
||||
@@ -67,7 +73,7 @@ const Manga: React.FC = () => {
|
||||
</>
|
||||
}
|
||||
>
|
||||
<IconButton onClick={() => mutate()}>
|
||||
<IconButton onClick={() => refetch()}>
|
||||
<Warning color="error" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
@@ -80,7 +86,7 @@ const Manga: React.FC = () => {
|
||||
{manga && <MangaToolbarMenu manga={manga} onRefresh={refresh} refreshing={refreshing} />}
|
||||
</Stack>,
|
||||
);
|
||||
}, [t, error, isValidating, refreshing, mutate, manga, refresh]);
|
||||
}, [t, error, isValidating, refreshing, manga, refresh]);
|
||||
|
||||
if (error && !manga) {
|
||||
return <EmptyView message={t('manga.error.label.request_failure')} messageExtra={error.message ?? error} />;
|
||||
@@ -90,7 +96,7 @@ const Manga: React.FC = () => {
|
||||
{isLoading && <LoadingPlaceholder />}
|
||||
|
||||
{manga && <MangaDetails manga={manga} />}
|
||||
<ChapterList mangaId={id} />
|
||||
{manga && <ChapterList manga={manga} isRefreshing={refreshing} />}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -7,12 +7,12 @@
|
||||
*/
|
||||
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
import { useCallback, useContext, useEffect, useState } from 'react';
|
||||
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useLocation, useNavigate, useParams } from 'react-router-dom';
|
||||
import { Box } from '@mui/material';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ChapterOffset, IChapter, IManga, IMangaCard, IReaderSettings, ReaderType, TranslationKey } from '@/typings';
|
||||
import requestManager from '@/lib/RequestManager';
|
||||
import { ChapterOffset, IReaderSettings, ReaderType, TChapter, TManga, TranslationKey } from '@/typings';
|
||||
import requestManager from '@/lib/requests/RequestManager.ts';
|
||||
import {
|
||||
checkAndHandleMissingStoredReaderSettings,
|
||||
getReaderSettingsFor,
|
||||
@@ -28,10 +28,10 @@ import ReaderNavBar from '@/components/navbar/ReaderNavBar';
|
||||
import NavbarContext from '@/components/context/NavbarContext';
|
||||
import makeToast from '@/components/util/Toast';
|
||||
|
||||
const isDupChapter = async (chapterIndex: number, currentChapter: IChapter) => {
|
||||
const nextChapter = await requestManager.getChapter(currentChapter.mangaId, chapterIndex).response;
|
||||
const isDupChapter = async (chapterIndex: number, currentChapter: TChapter) => {
|
||||
const nextChapter = await requestManager.getChapter(currentChapter.manga.id, chapterIndex).response;
|
||||
|
||||
return nextChapter.chapterNumber === currentChapter.chapterNumber;
|
||||
return nextChapter.data.chapter.chapterNumber === currentChapter.chapterNumber;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -41,7 +41,7 @@ const isDupChapter = async (chapterIndex: number, currentChapter: IChapter) => {
|
||||
*/
|
||||
const getOffsetChapter = async (
|
||||
chapterIndex: number,
|
||||
currentChapter: IChapter,
|
||||
currentChapter: TChapter,
|
||||
skipDupChapters: boolean,
|
||||
offset: ChapterOffset,
|
||||
): Promise<number> => {
|
||||
@@ -81,11 +81,11 @@ const getReaderComponent = (readerType: ReaderType) => {
|
||||
const range = (n: number) => Array.from({ length: n }, (value, key) => key);
|
||||
const initialChapter = {
|
||||
pageCount: -1,
|
||||
index: -1,
|
||||
sourceOrder: -1,
|
||||
chapterCount: 0,
|
||||
lastPageRead: 0,
|
||||
name: 'Loading...',
|
||||
};
|
||||
} as unknown as TChapter;
|
||||
|
||||
export default function Reader() {
|
||||
const { t } = useTranslation();
|
||||
@@ -93,22 +93,58 @@ export default function Reader() {
|
||||
const location = useLocation();
|
||||
|
||||
const { chapterIndex, mangaId } = useParams<{ chapterIndex: string; mangaId: string }>();
|
||||
const {
|
||||
data: manga = {
|
||||
id: +mangaId,
|
||||
title: '',
|
||||
thumbnailUrl: '',
|
||||
genre: [],
|
||||
inLibraryAt: 0,
|
||||
lastReadAt: 0,
|
||||
} as IMangaCard | IManga,
|
||||
isLoading: isMangaLoading,
|
||||
} = requestManager.useGetManga(mangaId);
|
||||
const { data: chapter = initialChapter, isLoading: isChapterLoading } = requestManager.useGetChapter(
|
||||
mangaId,
|
||||
chapterIndex,
|
||||
{ disableCache: true, revalidateOnFocus: false },
|
||||
|
||||
const initialManga = useMemo(
|
||||
() =>
|
||||
({
|
||||
id: +mangaId,
|
||||
title: '',
|
||||
thumbnailUrl: '',
|
||||
genre: [],
|
||||
inLibraryAt: 0,
|
||||
lastReadAt: 0,
|
||||
chapters: { totalCount: 0 },
|
||||
}) as unknown as TManga,
|
||||
[mangaId],
|
||||
);
|
||||
|
||||
const { data, loading: isMangaLoading } = requestManager.useGetManga(mangaId);
|
||||
const loadedChapter = useRef<TChapter | null>(null);
|
||||
const isChapterLoaded =
|
||||
Number(mangaId) === loadedChapter.current?.manga.id &&
|
||||
Number(chapterIndex) === loadedChapter.current?.sourceOrder &&
|
||||
loadedChapter.current?.pageCount !== -1;
|
||||
const manga = data?.manga ?? initialManga;
|
||||
const { data: chapterData, loading: isChapterLoading } = requestManager.useGetMangaChapter(mangaId, chapterIndex, {
|
||||
skip: isChapterLoaded,
|
||||
});
|
||||
|
||||
const getLoadedChapter = () => {
|
||||
const isAChapterLoaded = loadedChapter.current;
|
||||
|
||||
const isSameAsLoadedChapter = isAChapterLoaded && isChapterLoaded;
|
||||
if (isSameAsLoadedChapter) {
|
||||
return loadedChapter.current;
|
||||
}
|
||||
|
||||
if (chapterData?.chapter) {
|
||||
return chapterData.chapter;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
loadedChapter.current = getLoadedChapter();
|
||||
|
||||
const chapter = loadedChapter.current ?? initialChapter;
|
||||
const [fetchPages, { loading: areChapterPagesLoading }] = requestManager.useGetChapterPagesFetch(chapter.id);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isChapterLoading && chapter.pageCount === -1) {
|
||||
fetchPages();
|
||||
}
|
||||
}, [chapter.id]);
|
||||
|
||||
const isLoading = isChapterLoading || areChapterPagesLoading || chapter.pageCount === -1;
|
||||
const [wasLastPageReadSet, setWasLastPageReadSet] = useState(false);
|
||||
const [curPage, setCurPage] = useState<number>(0);
|
||||
const [pageToScrollTo, setPageToScrollTo] = useState<number | undefined>(undefined);
|
||||
@@ -130,12 +166,7 @@ export default function Reader() {
|
||||
setRetrievingNextChapter(true);
|
||||
try {
|
||||
setHistory(
|
||||
await getOffsetChapter(
|
||||
chapter.index + offset,
|
||||
chapter as IChapter,
|
||||
settings.skipDupChapters,
|
||||
offset,
|
||||
),
|
||||
await getOffsetChapter(chapter.sourceOrder + offset, chapter, settings.skipDupChapters, offset),
|
||||
);
|
||||
} catch (error) {
|
||||
const offsetToTranslationKeyMap: { [chapterOffset in ChapterOffset]: TranslationKey } = {
|
||||
@@ -152,7 +183,7 @@ export default function Reader() {
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isChapterLoading || !chapter) {
|
||||
if (isLoading || !chapter) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -161,13 +192,13 @@ export default function Reader() {
|
||||
// last page, also probably read = true, we will load the first page.
|
||||
setCurPage(0);
|
||||
} else setCurPage(chapter.lastPageRead);
|
||||
}, [chapter, isChapterLoading]);
|
||||
}, [chapter, isLoading]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!manga?.title || (chapter as IChapter)?.name === t('global.label.loading')) {
|
||||
if (!manga?.title || chapter.name === t('global.label.loading')) {
|
||||
setTitle(t('reader.title'));
|
||||
} else {
|
||||
setTitle(`${manga.title}: ${(chapter as IChapter).name}`);
|
||||
setTitle(`${manga.title}: ${chapter.name}`);
|
||||
}
|
||||
}, [t, manga, chapter]);
|
||||
|
||||
@@ -187,7 +218,7 @@ export default function Reader() {
|
||||
settings={settings}
|
||||
setSettingValue={setSettingValue}
|
||||
manga={manga}
|
||||
chapter={chapter as IChapter}
|
||||
chapter={chapter}
|
||||
curPage={curPage}
|
||||
scrollToPage={setPageToScrollTo}
|
||||
openNextChapter={openNextChapter}
|
||||
@@ -206,20 +237,23 @@ export default function Reader() {
|
||||
}
|
||||
|
||||
// do not mutate the chapter, this will cause the page to jump around due to always scrolling to the last read page
|
||||
if (curPage !== -1) {
|
||||
requestManager.updateChapter(manga.id, chapter.index, { lastPageRead: curPage });
|
||||
}
|
||||
const updateLastPageRead = curPage !== -1;
|
||||
const updateIsRead = curPage === chapter.pageCount - 1;
|
||||
const updateChapter = updateLastPageRead || updateIsRead;
|
||||
|
||||
if (curPage === chapter.pageCount - 1) {
|
||||
requestManager.updateChapter(manga.id, chapter.index, { read: true });
|
||||
if (updateChapter) {
|
||||
requestManager.updateChapter(chapter.id, {
|
||||
lastPageRead: updateLastPageRead ? curPage : undefined,
|
||||
isRead: updateIsRead ? true : undefined,
|
||||
});
|
||||
}
|
||||
}, [curPage]);
|
||||
|
||||
const nextChapter = useCallback(() => {
|
||||
if (chapter.index < chapter.chapterCount) {
|
||||
requestManager.updateChapter(manga.id, chapter.index, {
|
||||
if (chapter.sourceOrder < manga.chapters.totalCount) {
|
||||
requestManager.updateChapter(chapter.id, {
|
||||
lastPageRead: chapter.pageCount - 1,
|
||||
read: true,
|
||||
isRead: true,
|
||||
});
|
||||
|
||||
openNextChapter(ChapterOffset.NEXT, (nextChapterIndex) =>
|
||||
@@ -229,10 +263,10 @@ export default function Reader() {
|
||||
}),
|
||||
);
|
||||
}
|
||||
}, [chapter.index, chapter.chapterCount, chapter.pageCount, manga.id, settings.skipDupChapters]);
|
||||
}, [chapter.sourceOrder, manga.chapters.totalCount, chapter.pageCount, manga.id, settings.skipDupChapters]);
|
||||
|
||||
const prevChapter = useCallback(() => {
|
||||
if (chapter.index > 1) {
|
||||
if (chapter.sourceOrder > 1) {
|
||||
openNextChapter(ChapterOffset.PREV, (prevChapterIndex) =>
|
||||
navigate(`/manga/${manga.id}/chapter/${prevChapterIndex}`, {
|
||||
replace: true,
|
||||
@@ -240,7 +274,7 @@ export default function Reader() {
|
||||
}),
|
||||
);
|
||||
}
|
||||
}, [chapter.index, manga.id, settings.skipDupChapters]);
|
||||
}, [chapter.sourceOrder, manga.id, settings.skipDupChapters]);
|
||||
|
||||
// return spinner while chpater data is loading
|
||||
if (chapter.pageCount === -1) {
|
||||
@@ -277,6 +311,7 @@ export default function Reader() {
|
||||
>
|
||||
<PageNumber settings={settings} curPage={curPage} pageCount={chapter.pageCount} />
|
||||
<ReaderComponent
|
||||
key={chapter.id}
|
||||
pages={pages}
|
||||
pageCount={chapter.pageCount}
|
||||
setCurPage={setCurPage}
|
||||
|
||||
@@ -12,7 +12,7 @@ import { Link } from 'react-router-dom';
|
||||
import { StringParam, useQueryParam } from 'use-query-params';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ISource } from '@/typings';
|
||||
import requestManager from '@/lib/RequestManager';
|
||||
import requestManager from '@/lib/requests/RequestManager.ts';
|
||||
import useLocalStorage from '@/util/useLocalStorage';
|
||||
import { langSortCmp, sourceDefualtLangs, sourceForcedDefaultLangs } from '@/util/language';
|
||||
import { translateExtensionLanguage } from '@/screens/util/Extensions';
|
||||
@@ -96,16 +96,13 @@ const SourceSearchPreview = React.memo(
|
||||
emptyQuery: boolean;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const skipRequest = !searchString;
|
||||
|
||||
const { id, displayName, lang } = source;
|
||||
const {
|
||||
data: searchResult,
|
||||
isLoading,
|
||||
error,
|
||||
abortRequest,
|
||||
} = requestManager.useSourceQuickSearch(id, searchString ?? '', [], 1, { skipRequest });
|
||||
const mangas = !isLoading ? searchResult?.[0]?.mangaList ?? [] : [];
|
||||
const [, results] = requestManager.useSourceSearch(id, searchString ?? '', undefined, 1, {
|
||||
skipRequest: !searchString,
|
||||
});
|
||||
const { data: searchResult, isLoading, error, abortRequest } = results[0]!;
|
||||
const mangas = searchResult?.fetchSourceManga.mangas ?? [];
|
||||
const noMangasFound = !isLoading && !mangas.length;
|
||||
|
||||
useEffect(() => {
|
||||
@@ -169,7 +166,8 @@ const SearchAll: React.FC = () => {
|
||||
const [shownLangs, setShownLangs] = useLocalStorage<string[]>('shownSourceLangs', sourceDefualtLangs());
|
||||
const [showNsfw] = useLocalStorage<boolean>('showNsfw', true);
|
||||
|
||||
const { data: sources = [] } = requestManager.useGetSourceList();
|
||||
const { data } = requestManager.useGetSourceList();
|
||||
const sources = data?.sources.nodes ?? [];
|
||||
const [sourceToLoadingStateMap, setSourceToLoadingStateMap] = useState<SourceToLoadingStateMap>(new Map());
|
||||
const debouncedSourceToLoadingStateMap = useDebounce(sourceToLoadingStateMap, 500);
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ import ViewModuleIcon from '@mui/icons-material/ViewModule';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import LanguageIcon from '@mui/icons-material/Language';
|
||||
import CollectionsOutlinedBookmarkIcon from '@mui/icons-material/CollectionsBookmarkOutlined';
|
||||
import requestManager from '@/lib/RequestManager';
|
||||
import requestManager from '@/lib/requests/RequestManager.ts';
|
||||
import { langCodeToName } from '@/util/language';
|
||||
import useLocalStorage from '@/util/useLocalStorage';
|
||||
import ListItemLink from '@/components/util/ListItemLink';
|
||||
|
||||
@@ -10,19 +10,20 @@ import { createElement, useContext, useEffect } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import List from '@mui/material/List';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import requestManager from '@/lib/RequestManager';
|
||||
import requestManager from '@/lib/requests/RequestManager.ts';
|
||||
import cloneObject from '@/util/cloneObject';
|
||||
import NavbarContext from '@/components/context/NavbarContext';
|
||||
import { SwitchPreferenceCompat, CheckBoxPreference } from '@/components/sourceConfiguration/TwoStatePreference';
|
||||
import ListPreference from '@/components/sourceConfiguration/ListPreference';
|
||||
import EditTextPreference from '@/components/sourceConfiguration/EditTextPreference';
|
||||
import MultiSelectListPreference from '@/components/sourceConfiguration/MultiSelectListPreference';
|
||||
import { PreferenceProps } from '@/typings.ts';
|
||||
|
||||
function getPrefComponent(type: string) {
|
||||
switch (type) {
|
||||
case 'CheckBoxPreference':
|
||||
return CheckBoxPreference;
|
||||
case 'SwitchPreferenceCompat':
|
||||
case 'SwitchPreference':
|
||||
return SwitchPreferenceCompat;
|
||||
case 'ListPreference':
|
||||
return ListPreference;
|
||||
@@ -31,7 +32,7 @@ function getPrefComponent(type: string) {
|
||||
case 'MultiSelectListPreference':
|
||||
return MultiSelectListPreference;
|
||||
default:
|
||||
return CheckBoxPreference;
|
||||
throw new Error(`Unexpected preference type "${type}"`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,33 +46,26 @@ export default function SourceConfigure() {
|
||||
}, [t]);
|
||||
|
||||
const { sourceId } = useParams<{ sourceId: string }>();
|
||||
const { data: sourcePreferences = [], mutate } = requestManager.useGetSourcePreferences(sourceId);
|
||||
const { data } = requestManager.useGetSource(sourceId);
|
||||
const sourcePreferences = data?.source.preferences ?? [];
|
||||
|
||||
const convertToString = (position: number, value: any): string => {
|
||||
switch (sourcePreferences[position].props.defaultValueType) {
|
||||
case 'Set<String>':
|
||||
return JSON.stringify(value);
|
||||
default:
|
||||
return value.toString();
|
||||
}
|
||||
};
|
||||
|
||||
const updateValue = (position: number) => (value: any) => {
|
||||
requestManager
|
||||
.setSourcePreferences(sourceId, position, convertToString(position, value))
|
||||
.response.then(() => mutate());
|
||||
};
|
||||
const updateValue =
|
||||
(position: number): PreferenceProps['updateValue'] =>
|
||||
(type, value) => {
|
||||
requestManager.setSourcePreferences(sourceId, { position, [type]: value });
|
||||
};
|
||||
|
||||
return (
|
||||
<List sx={{ padding: 0 }}>
|
||||
{sourcePreferences.map((it, index) => {
|
||||
const props = cloneObject(it.props);
|
||||
props.updateValue = updateValue(index);
|
||||
props.key = index;
|
||||
const props = cloneObject(it);
|
||||
|
||||
// TypeScript is dumb in detecting extra props
|
||||
// @ts-ignore
|
||||
return createElement(getPrefComponent(it.type), props);
|
||||
return createElement(getPrefComponent(it.type), {
|
||||
...props,
|
||||
updateValue: updateValue(index),
|
||||
});
|
||||
})}
|
||||
</List>
|
||||
);
|
||||
|
||||
@@ -17,8 +17,8 @@ import { Box, Button, styled, useTheme, useMediaQuery } from '@mui/material';
|
||||
import FavoriteIcon from '@mui/icons-material/Favorite';
|
||||
import NewReleasesIcon from '@mui/icons-material/NewReleases';
|
||||
import FilterListIcon from '@mui/icons-material/FilterList';
|
||||
import { IManga, PaginatedMangaList, TranslationKey } from '@/typings';
|
||||
import requestManager, { AbortableSWRInfiniteResponse } from '@/lib/RequestManager';
|
||||
import { TPartialManga, TranslationKey } from '@/typings';
|
||||
import requestManager, { AbortableApolloUseMutationPaginatedResponse } from '@/lib/requests/RequestManager.ts';
|
||||
import { useDebounce } from '@/components/manga/hooks';
|
||||
import { useLibraryOptionsContext } from '@/components/context/LibraryOptionsContext';
|
||||
import SourceGridLayout from '@/components/source/GridLayouts';
|
||||
@@ -26,6 +26,10 @@ import AppbarSearch from '@/components/util/AppbarSearch';
|
||||
import SourceOptions from '@/components/source/SourceOptions';
|
||||
import NavbarContext from '@/components/context/NavbarContext';
|
||||
import SourceMangaGrid from '@/components/source/SourceMangaGrid';
|
||||
import {
|
||||
GetSourceMangasFetchMutation,
|
||||
GetSourceMangasFetchMutationVariables,
|
||||
} from '@/lib/graphql/generated/graphql.ts';
|
||||
|
||||
const ContentTypeMenu = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
@@ -69,6 +73,7 @@ export enum SourceContentType {
|
||||
}
|
||||
|
||||
interface IPos {
|
||||
type: 'selectState' | 'textState' | 'checkBoxState' | 'triState' | 'sortState';
|
||||
position: number;
|
||||
state: any;
|
||||
group?: number;
|
||||
@@ -81,15 +86,8 @@ const SOURCE_CONTENT_TYPE_TO_ERROR_MSG_KEY: { [contentType in SourceContentType]
|
||||
[SourceContentType.SEARCH]: 'manga.error.label.no_mangas_found',
|
||||
};
|
||||
|
||||
type SourceMangaResponse = Omit<AbortableSWRInfiniteResponse<PaginatedMangaList>, 'data'> & {
|
||||
data: {
|
||||
items: IManga[];
|
||||
hasNextPage: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
const getUniqueMangas = (mangas: IManga[]): IManga[] => {
|
||||
const uniqueMangas: IManga[] = [];
|
||||
const getUniqueMangas = (mangas: TPartialManga[]): TPartialManga[] => {
|
||||
const uniqueMangas: TPartialManga[] = [];
|
||||
|
||||
mangas.forEach((manga) => {
|
||||
const isDuplicate = uniqueMangas.some((uniqueManga) => uniqueManga.id === manga.id);
|
||||
@@ -106,9 +104,18 @@ const useSourceManga = (
|
||||
contentType: SourceContentType,
|
||||
searchTerm: string | null | undefined,
|
||||
filters: IPos[],
|
||||
initialPages = 1,
|
||||
): SourceMangaResponse => {
|
||||
let result: AbortableSWRInfiniteResponse<PaginatedMangaList>;
|
||||
initialPages: number,
|
||||
): [
|
||||
AbortableApolloUseMutationPaginatedResponse<GetSourceMangasFetchMutation, GetSourceMangasFetchMutationVariables>[0],
|
||||
AbortableApolloUseMutationPaginatedResponse<
|
||||
GetSourceMangasFetchMutation,
|
||||
GetSourceMangasFetchMutationVariables
|
||||
>[1][number],
|
||||
] => {
|
||||
let result: AbortableApolloUseMutationPaginatedResponse<
|
||||
GetSourceMangasFetchMutation,
|
||||
GetSourceMangasFetchMutationVariables
|
||||
>;
|
||||
switch (contentType) {
|
||||
case SourceContentType.POPULAR:
|
||||
result = requestManager.useGetSourcePopularMangas(sourceId, initialPages);
|
||||
@@ -117,12 +124,12 @@ const useSourceManga = (
|
||||
result = requestManager.useGetSourceLatestMangas(sourceId, initialPages);
|
||||
break;
|
||||
case SourceContentType.SEARCH:
|
||||
result = requestManager.useSourceQuickSearch(sourceId, searchTerm ?? '', [], initialPages);
|
||||
result = requestManager.useSourceSearch(sourceId, searchTerm ?? '', undefined, initialPages);
|
||||
break;
|
||||
case SourceContentType.FILTER:
|
||||
result = requestManager.useSourceQuickSearch(
|
||||
result = requestManager.useSourceSearch(
|
||||
sourceId,
|
||||
'',
|
||||
undefined,
|
||||
filters.map((filter) => {
|
||||
const { position, state, group } = filter;
|
||||
|
||||
@@ -130,32 +137,58 @@ const useSourceManga = (
|
||||
if (isPartOfGroup) {
|
||||
return {
|
||||
position: group,
|
||||
state: JSON.stringify({
|
||||
groupChange: {
|
||||
position,
|
||||
state,
|
||||
}),
|
||||
[filter.type]: state,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return filter;
|
||||
return {
|
||||
position,
|
||||
[filter.type]: state,
|
||||
};
|
||||
}),
|
||||
initialPages,
|
||||
{ disableCache: true },
|
||||
);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown ContentType "${contentType}"`);
|
||||
}
|
||||
|
||||
const pages = result.data;
|
||||
const { hasNextPage } = pages?.[pages.length - 1] ?? { hasNextPage: false };
|
||||
const pages = result[1]!;
|
||||
const lastLoadedPageIndex = pages.findLastIndex((page) => !!page.data?.fetchSourceManga);
|
||||
const lastLoadedPage = pages[lastLoadedPageIndex];
|
||||
const items = useMemo(
|
||||
() => (pages ?? []).map((page) => page.mangaList).reduce((prevList, list) => [...prevList, ...list], []),
|
||||
() =>
|
||||
(pages ?? [])
|
||||
.map((page) => page.data?.fetchSourceManga.mangas ?? [])
|
||||
.reduce((prevList, list) => [...prevList, ...list], []),
|
||||
[pages],
|
||||
);
|
||||
const uniqueItems = useMemo(() => getUniqueMangas(items), [items]);
|
||||
|
||||
return { ...result, data: { items: uniqueItems, hasNextPage } };
|
||||
if (!uniqueItems.length) {
|
||||
return [result[0] as any, result[1][result[1].length - 1]];
|
||||
}
|
||||
|
||||
return [
|
||||
result[0],
|
||||
{
|
||||
...pages[pages.length - 1],
|
||||
data: {
|
||||
...lastLoadedPage!.data,
|
||||
fetchSourceManga: {
|
||||
...lastLoadedPage!.data!.fetchSourceManga,
|
||||
hasNextPage:
|
||||
pages.length > lastLoadedPageIndex + 1
|
||||
? false
|
||||
: lastLoadedPage!.data!.fetchSourceManga.hasNextPage,
|
||||
mangas: uniqueItems,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export default function SourceMangas() {
|
||||
@@ -167,29 +200,35 @@ export default function SourceMangas() {
|
||||
const { sourceId } = useParams<{ sourceId: string }>();
|
||||
|
||||
const navigate = useNavigate();
|
||||
const { contentType: currentLocationContentType = SourceContentType.POPULAR } =
|
||||
const {
|
||||
contentType: currentLocationContentType = SourceContentType.POPULAR,
|
||||
filtersToApply: currentLocationFiltersToApply = [],
|
||||
} =
|
||||
useLocation<{
|
||||
contentType: SourceContentType;
|
||||
filtersToApply: IPos[];
|
||||
}>().state ?? {};
|
||||
|
||||
const { options } = useLibraryOptionsContext();
|
||||
const [query] = useQueryParam('query', StringParam);
|
||||
const [dialogFiltersToApply, setDialogFiltersToApply] = useState<IPos[]>([]);
|
||||
const [filtersToApply, setFiltersToApply] = useState<IPos[]>([]);
|
||||
const [dialogFiltersToApply, setDialogFiltersToApply] = useState<IPos[]>(currentLocationFiltersToApply);
|
||||
const [filtersToApply, setFiltersToApply] = useState<IPos[]>(currentLocationFiltersToApply);
|
||||
const searchTerm = useDebounce(query, 1000);
|
||||
const [resetScrollPosition, setResetScrollPosition] = useState(false);
|
||||
const [contentType, setContentType] = useState(currentLocationContentType);
|
||||
const {
|
||||
data: { items: mangas, hasNextPage } = { items: [], hasNextPage: false },
|
||||
isLoading,
|
||||
size: lastPageNum,
|
||||
setSize: setPages,
|
||||
mutate: refreshData,
|
||||
abortRequest,
|
||||
} = useSourceManga(sourceId, contentType, searchTerm, filtersToApply, isLargeScreen ? 2 : 1);
|
||||
const { data: filters = [], mutate: mutateFilters } = requestManager.useGetSourceFilters(sourceId);
|
||||
const { data: source } = requestManager.useGetSource(sourceId);
|
||||
const [triggerDataRefresh, setTriggerDataRefresh] = useState(false);
|
||||
const [loadPage, { data, isLoading, size: lastPageNum, abortRequest }] = useSourceManga(
|
||||
sourceId,
|
||||
contentType,
|
||||
searchTerm,
|
||||
filtersToApply,
|
||||
isLargeScreen ? 2 : 1,
|
||||
);
|
||||
const mangas = data?.fetchSourceManga.mangas ?? [];
|
||||
const hasNextPage = data?.fetchSourceManga.hasNextPage ?? false;
|
||||
|
||||
const { data: sourceData } = requestManager.useGetSource(sourceId);
|
||||
const source = sourceData?.source;
|
||||
const filters = source?.filters ?? [];
|
||||
|
||||
const message = !isLoading ? t(SOURCE_CONTENT_TYPE_TO_ERROR_MSG_KEY[contentType]) : undefined;
|
||||
const isLocalSource = sourceId === '0';
|
||||
@@ -214,6 +253,15 @@ export default function SourceMangas() {
|
||||
[setContentType],
|
||||
);
|
||||
|
||||
const updateLocationFilters = useCallback(
|
||||
(updatedFilters: IPos[]) => {
|
||||
if (contentType === SourceContentType.FILTER) {
|
||||
navigate('', { replace: true, state: { contentType, filtersToApply: updatedFilters } });
|
||||
}
|
||||
},
|
||||
[contentType],
|
||||
);
|
||||
|
||||
const isSearchTermAvailable = searchTerm && query?.length;
|
||||
const setSearchContentType = isSearchTermAvailable && contentType !== SourceContentType.SEARCH;
|
||||
if (setSearchContentType) {
|
||||
@@ -230,21 +278,15 @@ export default function SourceMangas() {
|
||||
return;
|
||||
}
|
||||
|
||||
setPages(lastPageNum + 1);
|
||||
}, [setPages, lastPageNum, hasNextPage]);
|
||||
loadPage(lastPageNum + 1);
|
||||
}, [lastPageNum, hasNextPage, contentType]);
|
||||
|
||||
const resetFilters = useCallback(async () => {
|
||||
setDialogFiltersToApply([]);
|
||||
setFiltersToApply([]);
|
||||
try {
|
||||
// required since previous implementation used to set the filters on server side (server caches them), thus, it has to be made sure that they are reset
|
||||
await requestManager.resetSourceFilters(sourceId);
|
||||
mutateFilters();
|
||||
} catch (error) {
|
||||
// ignore
|
||||
}
|
||||
setTriggerDataRefresh(true);
|
||||
}, [sourceId]);
|
||||
updateLocationFilters([]);
|
||||
setResetScrollPosition(true);
|
||||
}, [sourceId, contentType]);
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
@@ -261,15 +303,6 @@ export default function SourceMangas() {
|
||||
[searchTerm, contentType],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!triggerDataRefresh) {
|
||||
return;
|
||||
}
|
||||
|
||||
refreshData();
|
||||
setTriggerDataRefresh(false);
|
||||
}, [triggerDataRefresh]);
|
||||
|
||||
useEffect(() => {
|
||||
setTitle(source?.displayName ?? t('source.title'));
|
||||
setAction(
|
||||
@@ -326,6 +359,7 @@ export default function SourceMangas() {
|
||||
</ContentTypeButton>
|
||||
</ContentTypeMenu>
|
||||
<SourceMangaGrid
|
||||
key={contentType}
|
||||
mangas={mangas}
|
||||
hasNextPage={hasNextPage}
|
||||
loadMore={loadMore}
|
||||
@@ -340,7 +374,7 @@ export default function SourceMangas() {
|
||||
updateFilterValue={setDialogFiltersToApply}
|
||||
setTriggerUpdate={() => {
|
||||
setFiltersToApply(dialogFiltersToApply);
|
||||
setTriggerDataRefresh(true);
|
||||
updateLocationFilters(dialogFiltersToApply);
|
||||
}}
|
||||
resetFilterValue={resetFilters}
|
||||
update={dialogFiltersToApply}
|
||||
|
||||
@@ -12,7 +12,7 @@ import TravelExploreIcon from '@mui/icons-material/TravelExplore';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ISource } from '@/typings';
|
||||
import requestManager from '@/lib/RequestManager';
|
||||
import requestManager from '@/lib/requests/RequestManager.ts';
|
||||
import useLocalStorage from '@/util/useLocalStorage';
|
||||
import { sourceDefualtLangs, sourceForcedDefaultLangs, langSortCmp } from '@/util/language';
|
||||
import { translateExtensionLanguage } from '@/screens/util/Extensions';
|
||||
@@ -53,7 +53,8 @@ export default function Sources() {
|
||||
const [shownLangs, setShownLangs] = useLocalStorage<string[]>('shownSourceLangs', sourceDefualtLangs());
|
||||
const [showNsfw] = useLocalStorage<boolean>('showNsfw', true);
|
||||
|
||||
const { data: sources, isLoading } = requestManager.useGetSourceList();
|
||||
const { data, loading: isLoading } = requestManager.useGetSourceList();
|
||||
const sources = data?.sources.nodes;
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
|
||||
@@ -13,17 +13,18 @@ import Card from '@mui/material/Card';
|
||||
import CardContent from '@mui/material/CardContent';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useContext, useEffect, useMemo } from 'react';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { t as translate } from 'i18next';
|
||||
import { GroupedVirtuoso } from 'react-virtuoso';
|
||||
import { IChapter, IMangaChapter, IQueue } from '@/typings';
|
||||
import requestManager from '@/lib/RequestManager';
|
||||
import requestManager from '@/lib/requests/RequestManager.ts';
|
||||
import LoadingPlaceholder from '@/components/util/LoadingPlaceholder';
|
||||
import EmptyView from '@/components/util/EmptyView';
|
||||
import DownloadStateIndicator from '@/components/molecules/DownloadStateIndicator';
|
||||
import NavbarContext from '@/components/context/NavbarContext';
|
||||
import { DownloadType } from '@/lib/graphql/generated/graphql.ts';
|
||||
import { TChapter } from '@/typings.ts';
|
||||
|
||||
const StyledGroupedVirtuoso = styled(GroupedVirtuoso)(({ theme }) => ({
|
||||
// 64px header
|
||||
@@ -81,58 +82,41 @@ function getDateString(date: Date) {
|
||||
return date.toLocaleDateString();
|
||||
}
|
||||
|
||||
const groupByDate = (updates: IMangaChapter[]): [date: string, items: number][] => {
|
||||
const groupByDate = (updates: TChapter[]): [date: string, items: number][] => {
|
||||
if (!updates.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const dateToItemMap = new Map<string, number>();
|
||||
updates.forEach((item) => {
|
||||
const date = getDateString(epochToDate(item.chapter.fetchedAt));
|
||||
const date = getDateString(epochToDate(Number(item.fetchedAt)));
|
||||
dateToItemMap.set(date, (dateToItemMap.get(date) ?? 0) + 1);
|
||||
});
|
||||
|
||||
return [...dateToItemMap.entries()];
|
||||
};
|
||||
|
||||
const initialQueue = {
|
||||
status: 'Stopped',
|
||||
queue: [],
|
||||
} as IQueue;
|
||||
|
||||
const Updates: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const location = useLocation();
|
||||
|
||||
const { setTitle, setAction } = useContext(NavbarContext);
|
||||
const {
|
||||
data: pages = [{ hasNextPage: false, page: [] }],
|
||||
isLoading,
|
||||
size: loadedPages,
|
||||
setSize: setPages,
|
||||
} = requestManager.useGetRecentlyUpdatedChapters();
|
||||
const { hasNextPage } = pages[pages.length - 1];
|
||||
const updateEntries = useMemo(
|
||||
() => pages.map((page) => page.page).reduce((lastPageChapters, chapters) => [...lastPageChapters, ...chapters]),
|
||||
[pages],
|
||||
);
|
||||
data: chapterUpdateData,
|
||||
loading: isLoading,
|
||||
fetchMore,
|
||||
} = requestManager.useGetRecentlyUpdatedChapters(undefined, {
|
||||
fetchPolicy: 'cache-and-network',
|
||||
notifyOnNetworkStatusChange: true,
|
||||
omitAbortSignal: true,
|
||||
});
|
||||
const hasNextPage = !!chapterUpdateData?.chapters.pageInfo.hasNextPage;
|
||||
const endCursor = chapterUpdateData?.chapters.pageInfo.endCursor;
|
||||
const updateEntries = chapterUpdateData?.chapters.nodes ?? [];
|
||||
const groupedUpdates = useMemo(() => groupByDate(updateEntries), [updateEntries]);
|
||||
const groupCounts: number[] = useMemo(() => groupedUpdates.map((group) => group[1]), [groupedUpdates]);
|
||||
|
||||
const [, setWsClient] = useState<WebSocket>();
|
||||
const [{ queue }, setQueueState] = useState<IQueue>(initialQueue);
|
||||
|
||||
useEffect(() => {
|
||||
const wsc = requestManager.getDownloadWebSocket();
|
||||
wsc.onmessage = (e) => {
|
||||
const data = JSON.parse(e.data) as IQueue;
|
||||
setQueueState(data);
|
||||
};
|
||||
|
||||
setWsClient(wsc);
|
||||
|
||||
return () => wsc.close();
|
||||
}, []);
|
||||
const { data: downloaderData } = requestManager.useDownloadSubscription();
|
||||
const queue = (downloaderData?.downloadChanged.queue as DownloadType[]) ?? [];
|
||||
|
||||
useEffect(() => {
|
||||
setTitle(t('updates.title'));
|
||||
@@ -140,13 +124,16 @@ const Updates: React.FC = () => {
|
||||
setAction(null);
|
||||
}, [t]);
|
||||
|
||||
const downloadForChapter = (chapter: IChapter) => {
|
||||
const { index, mangaId } = chapter;
|
||||
return queue.find((q) => index === q.chapterIndex && mangaId === q.mangaId);
|
||||
const downloadForChapter = (chapter: TChapter) => {
|
||||
const {
|
||||
sourceOrder,
|
||||
manga: { id: mangaId },
|
||||
} = chapter;
|
||||
return queue.find((q) => sourceOrder === q.chapter.sourceOrder && mangaId === q.chapter.manga.id);
|
||||
};
|
||||
|
||||
const downloadChapter = (chapter: IChapter) => {
|
||||
requestManager.addChapterToDownloadQueue(chapter.mangaId, chapter.index);
|
||||
const downloadChapter = (chapter: TChapter) => {
|
||||
requestManager.addChapterToDownloadQueue(chapter.id);
|
||||
};
|
||||
|
||||
const loadMore = useCallback(() => {
|
||||
@@ -154,8 +141,8 @@ const Updates: React.FC = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
setPages(loadedPages + 1);
|
||||
}, [hasNextPage, loadedPages]);
|
||||
fetchMore({ variables: { offset: updateEntries.length } });
|
||||
}, [hasNextPage, endCursor]);
|
||||
|
||||
if (!isLoading && updateEntries.length === 0) {
|
||||
return <EmptyView message={t('updates.error.label.no_updates_available')} />;
|
||||
@@ -179,7 +166,8 @@ const Updates: React.FC = () => {
|
||||
</StyledGroupHeader>
|
||||
)}
|
||||
itemContent={(index) => {
|
||||
const { chapter, manga } = updateEntries[index];
|
||||
const chapter = updateEntries[index];
|
||||
const { manga } = chapter;
|
||||
const download = downloadForChapter(chapter);
|
||||
|
||||
return (
|
||||
@@ -187,7 +175,7 @@ const Updates: React.FC = () => {
|
||||
<Card>
|
||||
<CardActionArea
|
||||
component={Link}
|
||||
to={`/manga/${chapter.mangaId}/chapter/${chapter.index}`}
|
||||
to={`/manga/${chapter.manga.id}/chapter/${chapter.sourceOrder}`}
|
||||
state={location.state}
|
||||
>
|
||||
<CardContent
|
||||
@@ -208,7 +196,7 @@ const Updates: React.FC = () => {
|
||||
marginRight: 2,
|
||||
imageRendering: 'pixelated',
|
||||
}}
|
||||
src={requestManager.getValidImgUrlFor(manga.thumbnailUrl)}
|
||||
src={requestManager.getValidImgUrlFor(manga.thumbnailUrl ?? '')}
|
||||
/>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<Typography variant="h5" component="h2">
|
||||
@@ -220,7 +208,7 @@ const Updates: React.FC = () => {
|
||||
</Box>
|
||||
</Box>
|
||||
{download && <DownloadStateIndicator download={download} />}
|
||||
{download == null && !chapter.downloaded && (
|
||||
{download == null && !chapter.isDownloaded && (
|
||||
<IconButton
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
@@ -11,7 +11,7 @@ import List from '@mui/material/List';
|
||||
import ListItem from '@mui/material/ListItem';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import requestManager from '@/lib/RequestManager';
|
||||
import requestManager from '@/lib/requests/RequestManager.ts';
|
||||
import ListItemLink from '@/components/util/ListItemLink';
|
||||
import NavbarContext, { useSetDefaultBackTo } from '@/components/context/NavbarContext';
|
||||
import LoadingPlaceholder from '@/components/util/LoadingPlaceholder';
|
||||
@@ -25,7 +25,8 @@ export default function About() {
|
||||
setAction(null);
|
||||
}, [t]);
|
||||
|
||||
const { data: about } = requestManager.useGetAbout();
|
||||
const { data } = requestManager.useGetAbout();
|
||||
const about = data?.about;
|
||||
|
||||
useSetDefaultBackTo('settings');
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import ListItemText from '@mui/material/ListItemText';
|
||||
import { fromEvent } from 'file-selector';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ListItemButton } from '@mui/material';
|
||||
import requestManager from '@/lib/RequestManager';
|
||||
import requestManager from '@/lib/requests/RequestManager.ts';
|
||||
import makeToast from '@/components/util/Toast';
|
||||
import ListItemLink from '@/components/util/ListItemLink';
|
||||
import NavbarContext, { useSetDefaultBackTo } from '@/components/context/NavbarContext';
|
||||
|
||||
@@ -24,11 +24,11 @@ import DialogTitle from '@mui/material/DialogTitle';
|
||||
import Checkbox from '@mui/material/Checkbox';
|
||||
import FormControlLabel from '@mui/material/FormControlLabel';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ICategory } from '@/typings';
|
||||
import requestManager from '@/lib/RequestManager';
|
||||
import requestManager from '@/lib/requests/RequestManager.ts';
|
||||
import StrictModeDroppable from '@/lib/StrictModeDroppable';
|
||||
import { DEFAULT_FULL_FAB_HEIGHT } from '@/components/util/StyledFab';
|
||||
import NavbarContext, { useSetDefaultBackTo } from '@/components/context/NavbarContext';
|
||||
import { TCategory } from '@/typings.ts';
|
||||
|
||||
const getItemStyle = (
|
||||
isDragging: boolean,
|
||||
@@ -52,9 +52,9 @@ export default function Categories() {
|
||||
setAction(null);
|
||||
}, [t]);
|
||||
|
||||
const { data, mutate } = requestManager.useGetCategories();
|
||||
const { data } = requestManager.useGetCategories();
|
||||
const categories = useMemo(() => {
|
||||
const res = [...(data ?? [])];
|
||||
const res = [...(data?.categories.nodes ?? [])];
|
||||
if (res.length > 0 && res[0].name === 'Default') {
|
||||
res.shift();
|
||||
}
|
||||
@@ -65,17 +65,17 @@ export default function Categories() {
|
||||
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
|
||||
const [dialogName, setDialogName] = useState<string>('');
|
||||
const [dialogDefault, setDialogDefault] = useState<boolean>(false);
|
||||
const [reorderCategory, { reset: revertReorder }] = requestManager.useReorderCategory();
|
||||
const theme = useTheme();
|
||||
|
||||
useSetDefaultBackTo('settings');
|
||||
|
||||
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 categoryReorder = (list: TCategory[], from: number, to: number) => {
|
||||
const reorderedCategory = list[from];
|
||||
|
||||
requestManager.reorderCategory(from + 1, to + 1).response.finally(() => mutate());
|
||||
reorderCategory({ variables: { input: { id: reorderedCategory.id, position: to + 1 } } }).catch(() =>
|
||||
revertReorder(),
|
||||
);
|
||||
};
|
||||
|
||||
const onDragEnd = (result: DropResult) => {
|
||||
@@ -113,18 +113,16 @@ export default function Categories() {
|
||||
setDialogOpen(false);
|
||||
|
||||
if (categoryToEdit === -1) {
|
||||
requestManager.createCategory(dialogName).response.finally(() => mutate());
|
||||
requestManager.createCategory({ name: dialogName, default: dialogDefault });
|
||||
} else {
|
||||
const category = categories[categoryToEdit];
|
||||
requestManager
|
||||
.updateCategory(category.id, { name: dialogName, default: dialogDefault })
|
||||
.response.finally(() => mutate());
|
||||
requestManager.updateCategory(category.id, { name: dialogName, default: dialogDefault });
|
||||
}
|
||||
};
|
||||
|
||||
const deleteCategory = (index: number) => {
|
||||
const category = categories[index];
|
||||
requestManager.deleteCategory(category.id).response.finally(() => mutate());
|
||||
requestManager.deleteCategory(category.id);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -11,7 +11,7 @@ import { Box } from '@mui/material';
|
||||
import CircularProgress from '@mui/material/CircularProgress';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { IReaderSettings } from '@/typings';
|
||||
import { requestUpdateServerMetadata } from '@/util/metadata';
|
||||
import { convertToGqlMeta, requestUpdateServerMetadata } from '@/util/metadata';
|
||||
import {
|
||||
checkAndHandleMissingStoredReaderSettings,
|
||||
getDefaultSettings,
|
||||
@@ -34,7 +34,7 @@ export default function DefaultReaderSettings() {
|
||||
useSetDefaultBackTo('settings');
|
||||
|
||||
const setSettingValue = (key: keyof IReaderSettings, value: string | boolean) => {
|
||||
requestUpdateServerMetadata(metadata ?? {}, [[key, value]]).catch(() =>
|
||||
requestUpdateServerMetadata(convertToGqlMeta(metadata)! ?? {}, [[key, value]]).catch(() =>
|
||||
makeToast(t('reader.settings.error.label.failed_to_save_settings'), 'warning'),
|
||||
);
|
||||
};
|
||||
@@ -54,7 +54,11 @@ export default function DefaultReaderSettings() {
|
||||
);
|
||||
}
|
||||
|
||||
checkAndHandleMissingStoredReaderSettings({ meta: metadata }, 'server', getDefaultSettings()).catch(() => {});
|
||||
checkAndHandleMissingStoredReaderSettings(
|
||||
{ meta: convertToGqlMeta(metadata)! },
|
||||
'server',
|
||||
getDefaultSettings(),
|
||||
).catch(() => {});
|
||||
|
||||
return (
|
||||
<ReaderSettingsOptions
|
||||
|
||||
@@ -20,12 +20,13 @@ import DialogTitle from '@mui/material/DialogTitle';
|
||||
import { styled } from '@mui/material';
|
||||
import { t as translate } from 'i18next';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ICategory, IncludeInGlobalUpdate } from '@/typings';
|
||||
import requestManager from '@/lib/RequestManager';
|
||||
import requestManager from '@/lib/requests/RequestManager.ts';
|
||||
import makeToast from '@/components/util/Toast';
|
||||
import ThreeStateCheckboxInput from '@/components/atoms/ThreeStateCheckboxInput';
|
||||
import NavbarContext, { useSetDefaultBackTo } from '@/components/context/NavbarContext';
|
||||
import SearchSettings from '@/screens/settings/SearchSettings';
|
||||
import { IncludeInUpdate } from '@/lib/graphql/generated/graphql.ts';
|
||||
import { TCategory } from '@/typings.ts';
|
||||
|
||||
const CategoriesDiv = styled('div')({
|
||||
display: 'flex',
|
||||
@@ -34,16 +35,35 @@ const CategoriesDiv = styled('div')({
|
||||
overflow: 'auto',
|
||||
});
|
||||
|
||||
const includeInUpdateStatusToBoolean = (status: IncludeInGlobalUpdate) => {
|
||||
if (status === IncludeInGlobalUpdate.UNSET) {
|
||||
return null;
|
||||
const booleanToIncludeInStatus = (status: boolean | null | undefined): IncludeInUpdate => {
|
||||
switch (status) {
|
||||
case false:
|
||||
return IncludeInUpdate.Exclude;
|
||||
case true:
|
||||
return IncludeInUpdate.Include;
|
||||
case null:
|
||||
case undefined:
|
||||
return IncludeInUpdate.Unset;
|
||||
default:
|
||||
throw new Error(`booleanToIncludeInStatus: unexpected IncludeInUpdate status "${status}"`);
|
||||
}
|
||||
};
|
||||
|
||||
return !!status;
|
||||
const includeInUpdateStatusToBoolean = (status: IncludeInUpdate): boolean | null => {
|
||||
switch (status) {
|
||||
case IncludeInUpdate.Exclude:
|
||||
return false;
|
||||
case IncludeInUpdate.Include:
|
||||
return true;
|
||||
case IncludeInUpdate.Unset:
|
||||
return null;
|
||||
default:
|
||||
throw new Error(`includeInUpdateStatusToBoolean: unexpected IncludeInUpdate status "${status}"`);
|
||||
}
|
||||
};
|
||||
|
||||
const getCategoryUpdateInfo = (
|
||||
categories: ICategory[],
|
||||
categories: TCategory[],
|
||||
areIncluded: boolean,
|
||||
unsetCategories: number,
|
||||
allCategories: number,
|
||||
@@ -80,20 +100,25 @@ export default function LibrarySettings() {
|
||||
|
||||
useSetDefaultBackTo('settings');
|
||||
|
||||
const { data: categories = [], error: requestError, mutate } = requestManager.useGetCategories();
|
||||
const [dialogCategories, setDialogCategories] = useState<ICategory[]>(categories);
|
||||
const { data, error: requestError } = requestManager.useGetCategories();
|
||||
const categories = data?.categories.nodes;
|
||||
const [dialogCategories, setDialogCategories] = useState<TCategory[]>(categories ?? []);
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!categories) {
|
||||
return;
|
||||
}
|
||||
|
||||
setDialogCategories(categories);
|
||||
}, [categories]);
|
||||
|
||||
const unsetCategories: ICategory[] =
|
||||
categories?.filter((category) => category.includeInUpdate === IncludeInGlobalUpdate.UNSET) ?? [];
|
||||
const excludedCategories: ICategory[] =
|
||||
categories?.filter((category) => category.includeInUpdate === IncludeInGlobalUpdate.EXCLUDE) ?? [];
|
||||
const includedCategories: ICategory[] =
|
||||
categories?.filter((category) => category.includeInUpdate === IncludeInGlobalUpdate.INCLUDE) ?? [];
|
||||
const unsetCategories: TCategory[] =
|
||||
categories?.filter((category) => category.includeInUpdate === IncludeInUpdate.Unset) ?? [];
|
||||
const excludedCategories: TCategory[] =
|
||||
categories?.filter((category) => category.includeInUpdate === IncludeInUpdate.Exclude) ?? [];
|
||||
const includedCategories: TCategory[] =
|
||||
categories?.filter((category) => category.includeInUpdate === IncludeInUpdate.Include) ?? [];
|
||||
const excludedCategoriesText = getCategoryUpdateInfo(
|
||||
excludedCategories,
|
||||
false,
|
||||
@@ -109,12 +134,12 @@ export default function LibrarySettings() {
|
||||
requestError,
|
||||
);
|
||||
|
||||
const updateCategory = (category: ICategory) =>
|
||||
const updateCategory = (category: TCategory) =>
|
||||
requestManager.updateCategory(category.id, { includeInUpdate: category.includeInUpdate }).response;
|
||||
|
||||
const updateCategories = async () => {
|
||||
const categoriesToUpdate = dialogCategories.filter((category) => {
|
||||
const currentCategory = categories.find((currCategory) => currCategory.id === category.id);
|
||||
const currentCategory = categories?.find((currCategory) => currCategory.id === category.id);
|
||||
|
||||
if (!currentCategory) {
|
||||
return false;
|
||||
@@ -127,10 +152,11 @@ export default function LibrarySettings() {
|
||||
|
||||
try {
|
||||
await Promise.all(categoriesToUpdate.map((category) => updateCategory(category)));
|
||||
mutate([...dialogCategories], { revalidate: false });
|
||||
// TODO - update cache immediately
|
||||
// mutate(categoriesEndpoint, [...dialogCategories], { revalidate: false });
|
||||
} catch (error) {
|
||||
makeToast(t('global.error.label.failed_to_save_changes'), 'error');
|
||||
mutate([...categories]);
|
||||
// mutate(categoriesEndpoint, [...categories]);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -192,13 +218,12 @@ export default function LibrarySettings() {
|
||||
label={category.name}
|
||||
checked={includeInUpdateStatusToBoolean(category.includeInUpdate)}
|
||||
onChange={(checked) => {
|
||||
const newIncludeState: IncludeInGlobalUpdate =
|
||||
checked == null ? IncludeInGlobalUpdate.UNSET : Number(checked);
|
||||
const newIncludeState = booleanToIncludeInStatus(checked);
|
||||
|
||||
const categoryIndex = dialogCategories.findIndex(
|
||||
(category_) => category_ === category,
|
||||
);
|
||||
const updatedDialogCategories: ICategory[] = [
|
||||
const updatedDialogCategories: TCategory[] = [
|
||||
...dialogCategories.slice(0, categoryIndex),
|
||||
{
|
||||
...category,
|
||||
|
||||
@@ -12,7 +12,7 @@ import ListItemIcon from '@mui/material/ListItemIcon';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { SearchMetadataKeys } from '@/typings';
|
||||
import { requestUpdateServerMetadata } from '@/util/metadata';
|
||||
import { convertToGqlMeta, requestUpdateServerMetadata } from '@/util/metadata';
|
||||
import { useSearchSettings } from '@/util/searchSettings';
|
||||
import makeToast from '@/components/util/Toast';
|
||||
import { useSetDefaultBackTo } from '@/components/context/NavbarContext';
|
||||
@@ -24,7 +24,7 @@ export default function SearchSettings() {
|
||||
useSetDefaultBackTo('settings');
|
||||
|
||||
const setSettingValue = (key: SearchMetadataKeys, value: boolean) => {
|
||||
requestUpdateServerMetadata(metadata ?? {}, [[key, value]]).catch(() =>
|
||||
requestUpdateServerMetadata(convertToGqlMeta(metadata)! ?? {}, [[key, value]]).catch(() =>
|
||||
makeToast(t('search.error.label.failed_to_save_settings'), 'warning'),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
*/
|
||||
|
||||
import { t } from 'i18next';
|
||||
import { IExtension, TranslationKey } from '@/typings';
|
||||
import { PartialExtension, TranslationKey } from '@/typings';
|
||||
import { DefaultLanguage, langCodeToName } from '@/util/language';
|
||||
|
||||
export enum ExtensionState {
|
||||
@@ -16,16 +16,16 @@ export enum ExtensionState {
|
||||
OBSOLETE = 'OBSOLETE',
|
||||
}
|
||||
|
||||
export type GroupedExtensionsResult<KEY extends string = string> = [KEY, IExtension[]][];
|
||||
export type GroupedExtensionsResult<KEY extends string = string> = [KEY, PartialExtension[]][];
|
||||
|
||||
export type GroupedByExtensionState = {
|
||||
[state in ExtensionState]: IExtension[];
|
||||
[state in ExtensionState]: PartialExtension[];
|
||||
};
|
||||
|
||||
export type GroupedByLanguage = {
|
||||
[language in DefaultLanguage]: IExtension[];
|
||||
[language in DefaultLanguage]: PartialExtension[];
|
||||
} & {
|
||||
[language: string]: IExtension[];
|
||||
[language: string]: PartialExtension[];
|
||||
};
|
||||
|
||||
export type GroupedExtensions = GroupedByExtensionState & GroupedByLanguage;
|
||||
|
||||
152
src/typings.ts
152
src/typings.ts
@@ -10,6 +10,31 @@ import { OverridableComponent } from '@mui/material/OverridableComponent';
|
||||
import { SvgIconTypeMap } from '@mui/material/SvgIcon/SvgIcon';
|
||||
import { ParseKeys } from 'i18next';
|
||||
import { Location } from 'react-router-dom';
|
||||
import {
|
||||
GetCategoryQuery,
|
||||
GetChapterQuery,
|
||||
GetExtensionQuery,
|
||||
GetMangaQuery,
|
||||
GetSourceQuery,
|
||||
MetaType,
|
||||
SourcePreferenceChangeInput,
|
||||
} from '@/lib/graphql/generated/graphql.ts';
|
||||
|
||||
export type ExtractByKeyValue<T, Key extends keyof T, Value extends T[Key]> = T extends
|
||||
| Record<Key, Value>
|
||||
| Partial<Record<Key, Value>>
|
||||
? T
|
||||
: never;
|
||||
|
||||
export type RecursivePartial<T> = {
|
||||
[P in keyof T]?: T[P] extends (infer U)[]
|
||||
? RecursivePartial<U>[]
|
||||
: T[P] extends object | undefined
|
||||
? RecursivePartial<T[P]>
|
||||
: T[P];
|
||||
};
|
||||
|
||||
export type OptionalProperty<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
|
||||
|
||||
type GenericLocation<State = any> = Omit<Location, 'state'> & { state?: State };
|
||||
|
||||
@@ -21,19 +46,7 @@ declare module 'react-router-dom' {
|
||||
|
||||
export type TranslationKey = ParseKeys;
|
||||
|
||||
export interface IExtension {
|
||||
name: string;
|
||||
pkgName: string;
|
||||
versionName: string;
|
||||
versionCode: number;
|
||||
lang: string;
|
||||
isNsfw: boolean;
|
||||
apkName: string;
|
||||
iconUrl: string;
|
||||
installed: boolean;
|
||||
hasUpdate: boolean;
|
||||
obsolete: boolean;
|
||||
}
|
||||
export type PartialExtension = GetExtensionQuery['extension'];
|
||||
|
||||
export interface ISource {
|
||||
id: string;
|
||||
@@ -46,29 +59,7 @@ export interface ISource {
|
||||
displayName: string;
|
||||
}
|
||||
|
||||
export interface ISourceFilters {
|
||||
type: string;
|
||||
filter: ISourceFilter;
|
||||
}
|
||||
|
||||
export interface ISourceFilter {
|
||||
name: string;
|
||||
state: number | string | boolean | ISourceFilters[] | IState;
|
||||
values?: string[];
|
||||
displayValues?: string[];
|
||||
selected?: ISelected;
|
||||
}
|
||||
|
||||
export interface ISelected {
|
||||
displayname: string;
|
||||
value: string;
|
||||
_value: string;
|
||||
}
|
||||
|
||||
export interface IState {
|
||||
ascending: boolean;
|
||||
index: number;
|
||||
}
|
||||
export type SourceFilters = GetSourceQuery['source']['filters'][number];
|
||||
|
||||
export interface IMetadataMigration {
|
||||
appKeyPrefix?: { oldPrefix: string; newPrefix: string };
|
||||
@@ -88,6 +79,8 @@ export type Metadata<Keys extends string = string, Values = string> = {
|
||||
[key in Keys]: Values;
|
||||
};
|
||||
|
||||
export type GqlMetaHolder = { meta?: MetaType[] };
|
||||
|
||||
export type MetadataHolder<Keys extends string = string, Values = string> = {
|
||||
meta?: Metadata<Keys, Values>;
|
||||
};
|
||||
@@ -115,6 +108,10 @@ export interface IMangaCard {
|
||||
lastReadAt: number;
|
||||
}
|
||||
|
||||
export type TManga = GetMangaQuery['manga'];
|
||||
|
||||
export type TPartialManga = OptionalProperty<TManga, 'unreadCount' | 'downloadCount' | 'categories' | 'chapters'>;
|
||||
|
||||
export interface IManga {
|
||||
id: number;
|
||||
sourceId: string;
|
||||
@@ -176,12 +173,7 @@ export interface IMangaChapter {
|
||||
chapter: IChapter;
|
||||
}
|
||||
|
||||
export interface IPartialChapter {
|
||||
pageCount: number;
|
||||
index: number;
|
||||
chapterCount: number;
|
||||
lastPageRead: number;
|
||||
}
|
||||
export type TChapter = GetChapterQuery['chapter'];
|
||||
|
||||
export enum IncludeInGlobalUpdate {
|
||||
EXCLUDE = 0,
|
||||
@@ -189,6 +181,8 @@ export enum IncludeInGlobalUpdate {
|
||||
UNSET = -1,
|
||||
}
|
||||
|
||||
export type TCategory = GetCategoryQuery['category'];
|
||||
|
||||
export interface ICategory {
|
||||
id: number;
|
||||
order: number;
|
||||
@@ -247,22 +241,12 @@ export interface IReaderProps {
|
||||
curPage: number;
|
||||
initialPage: number;
|
||||
settings: IReaderSettings;
|
||||
manga: IMangaCard | IManga;
|
||||
chapter: IChapter | IPartialChapter;
|
||||
manga: TManga;
|
||||
chapter: TChapter;
|
||||
nextChapter: () => void;
|
||||
prevChapter: () => void;
|
||||
}
|
||||
|
||||
export interface IAbout {
|
||||
name: string;
|
||||
version: string;
|
||||
revision: string;
|
||||
buildType: 'Stable' | 'Preview';
|
||||
buildTime: number;
|
||||
github: string;
|
||||
discord: string;
|
||||
}
|
||||
|
||||
export interface IDownloadChapter {
|
||||
chapterIndex: number;
|
||||
mangaId: number;
|
||||
@@ -286,47 +270,34 @@ export interface IUpdateStatus {
|
||||
};
|
||||
}
|
||||
|
||||
export type SourcePreferences = GetSourceQuery['source']['preferences'][number];
|
||||
|
||||
export interface PreferenceProps {
|
||||
key: string;
|
||||
title: string;
|
||||
summary: string;
|
||||
defaultValue: any;
|
||||
currentValue: any;
|
||||
defaultValueType: string;
|
||||
updateValue: <Key extends keyof Omit<SourcePreferenceChangeInput, 'position'>>(
|
||||
type: Key,
|
||||
value: SourcePreferenceChangeInput[Key],
|
||||
) => void;
|
||||
}
|
||||
|
||||
export type TwoStatePreferenceProps = (CheckBoxPreferenceProps | SwitchPreferenceCompatProps) & {
|
||||
// intetnal props
|
||||
updateValue: any;
|
||||
}
|
||||
twoStateType: 'Switch' | 'Checkbox';
|
||||
};
|
||||
|
||||
export interface TwoStatePreferenceProps extends PreferenceProps {
|
||||
// intetnal props
|
||||
type: 'Switch' | 'Checkbox';
|
||||
}
|
||||
export type CheckBoxPreferenceProps = PreferenceProps &
|
||||
ExtractByKeyValue<SourcePreferences, '__typename', 'CheckBoxPreference'>;
|
||||
|
||||
export interface CheckBoxPreferenceProps extends PreferenceProps {}
|
||||
export type SwitchPreferenceCompatProps = PreferenceProps &
|
||||
ExtractByKeyValue<SourcePreferences, '__typename', 'SwitchPreference'>;
|
||||
|
||||
export interface SwitchPreferenceCompatProps extends PreferenceProps {}
|
||||
export type ListPreferenceProps = PreferenceProps &
|
||||
ExtractByKeyValue<SourcePreferences, '__typename', 'ListPreference'>;
|
||||
|
||||
export interface ListPreferenceProps extends PreferenceProps {
|
||||
entries: string[];
|
||||
entryValues: string[];
|
||||
}
|
||||
export type MultiSelectListPreferenceProps = PreferenceProps &
|
||||
ExtractByKeyValue<SourcePreferences, '__typename', 'MultiSelectListPreference'>;
|
||||
|
||||
export interface MultiSelectListPreferenceProps extends PreferenceProps {
|
||||
entries: string[];
|
||||
entryValues: string[];
|
||||
}
|
||||
|
||||
export interface EditTextPreferenceProps extends PreferenceProps {
|
||||
dialogTitle: string;
|
||||
dialogMessage: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface SourcePreferences {
|
||||
type: string;
|
||||
props: any;
|
||||
}
|
||||
export type EditTextPreferenceProps = PreferenceProps &
|
||||
ExtractByKeyValue<SourcePreferences, '__typename', 'EditTextPreference'>;
|
||||
|
||||
export interface NavbarItem {
|
||||
path: string;
|
||||
@@ -389,13 +360,6 @@ export interface LibraryOptions {
|
||||
showTabSize: boolean;
|
||||
}
|
||||
|
||||
export interface BatchChaptersChange {
|
||||
delete?: boolean;
|
||||
isRead?: boolean;
|
||||
isBookmarked?: boolean;
|
||||
lastPageRead?: number;
|
||||
}
|
||||
|
||||
export type UpdateCheck = {
|
||||
channel: 'Stable' | 'Preview';
|
||||
tag: string;
|
||||
|
||||
@@ -6,20 +6,20 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { mutate } from 'swr';
|
||||
import {
|
||||
AllowedMetadataValueTypes,
|
||||
AppMetadataKeys,
|
||||
ICategory,
|
||||
IManga,
|
||||
IMangaCard,
|
||||
IMangaChapter,
|
||||
GqlMetaHolder,
|
||||
IMetadataMigration,
|
||||
Metadata,
|
||||
MetadataHolder,
|
||||
MetadataKeyValuePair,
|
||||
TCategory,
|
||||
TChapter,
|
||||
TManga,
|
||||
} from '@/typings';
|
||||
import requestManager, { RequestManager } from '@/lib/RequestManager';
|
||||
import requestManager from '@/lib/requests/RequestManager.ts';
|
||||
import { MetaType } from '@/lib/graphql/generated/graphql.ts';
|
||||
|
||||
const APP_METADATA_KEY_PREFIX = 'webUI_';
|
||||
|
||||
@@ -117,6 +117,27 @@ const convertValueFromMetadata = <T extends AllowedMetadataValueTypes = AllowedM
|
||||
return value as T;
|
||||
};
|
||||
|
||||
export const convertFromGqlMeta = (gqlMetadata?: MetaType[]): Metadata | undefined => {
|
||||
if (!gqlMetadata) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const metadata: Metadata = {};
|
||||
gqlMetadata.forEach(({ key, value }) => {
|
||||
metadata[key] = value;
|
||||
});
|
||||
|
||||
return metadata;
|
||||
};
|
||||
|
||||
export const convertToGqlMeta = (metadata?: Metadata): MetaType[] | undefined => {
|
||||
if (!metadata) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return Object.entries(metadata).map(([key, value]) => ({ key, value }));
|
||||
};
|
||||
|
||||
const getAppMetadataFrom = (meta: Metadata, appPrefix: string = APP_METADATA_KEY_PREFIX): Metadata => {
|
||||
const appMetadata: Metadata = {};
|
||||
|
||||
@@ -276,6 +297,8 @@ export const getMetadataFrom = <METADATA extends Partial<Metadata<AppMetadataKey
|
||||
return appMetadata;
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const wrapMetadataWithMetaKey = (wrap: boolean, metadata: Metadata): MetadataHolder => {
|
||||
if (wrap) {
|
||||
return {
|
||||
@@ -293,72 +316,54 @@ const wrapMetadataWithMetaKey = (wrap: boolean, metadata: Metadata): MetadataHol
|
||||
type MetadataHolderType = 'manga' | 'chapter' | 'category' | 'global';
|
||||
|
||||
export const requestUpdateMetadataValue = async (
|
||||
metadataHolder: MetadataHolder,
|
||||
metadataHolder: GqlMetaHolder,
|
||||
holderType: MetadataHolderType,
|
||||
key: AppMetadataKeys,
|
||||
value: AllowedMetadataValueTypes,
|
||||
): Promise<void> => {
|
||||
const metadataKey = getMetadataKey(key);
|
||||
const mutatedMetadata = {
|
||||
...metadataHolder.meta,
|
||||
[metadataKey]: `${value}`,
|
||||
};
|
||||
|
||||
let endpoint: string;
|
||||
switch (holderType) {
|
||||
case 'category':
|
||||
endpoint = `category/${(metadataHolder as ICategory).id}/meta`;
|
||||
await requestManager.setCategoryMeta((metadataHolder as ICategory).id, metadataKey, value).response;
|
||||
await requestManager.setCategoryMeta((metadataHolder as TCategory).id, metadataKey, value).response;
|
||||
break;
|
||||
case 'chapter':
|
||||
// eslint-disable-next-line no-case-declarations
|
||||
const { manga, chapter } = metadataHolder as IMangaChapter;
|
||||
endpoint = `manga/${manga.id}/chapter/${chapter.index}/meta`;
|
||||
await requestManager.setChapterMeta(manga.id, chapter.index, metadataKey, value).response;
|
||||
await requestManager.setChapterMeta((metadataHolder as TChapter).id, metadataKey, value).response;
|
||||
break;
|
||||
case 'global':
|
||||
endpoint = 'meta';
|
||||
await requestManager.setGlobalMetadata(metadataKey, value).response;
|
||||
break;
|
||||
case 'manga':
|
||||
endpoint = `manga/${(metadataHolder as IManga).id}/meta`;
|
||||
await requestManager.setMangaMeta((metadataHolder as IManga).id, metadataKey, value).response;
|
||||
await requestManager.setMangaMeta((metadataHolder as TManga).id, metadataKey, value).response;
|
||||
break;
|
||||
default:
|
||||
throw new Error(`requestUpdateMetadataValue: unknown holderType "${holderType}"`);
|
||||
}
|
||||
|
||||
const urlToMutate = `${RequestManager.API_VERSION}${endpoint}`;
|
||||
mutate(
|
||||
urlToMutate,
|
||||
{ ...metadataHolder, ...wrapMetadataWithMetaKey(holderType !== 'global', mutatedMetadata) },
|
||||
{ revalidate: false },
|
||||
);
|
||||
};
|
||||
|
||||
export const requestUpdateMetadata = async (
|
||||
metadataHolder: MetadataHolder,
|
||||
metadataHolder: GqlMetaHolder,
|
||||
holderType: MetadataHolderType,
|
||||
keysToValues: [AppMetadataKeys, AllowedMetadataValueTypes][],
|
||||
): Promise<void[]> =>
|
||||
Promise.all(keysToValues.map(([key, value]) => requestUpdateMetadataValue(metadataHolder, holderType, key, value)));
|
||||
|
||||
export const requestUpdateServerMetadata = async (
|
||||
serverMetadata: Metadata,
|
||||
serverMetadata: MetaType[],
|
||||
keysToValues: MetadataKeyValuePair[],
|
||||
): Promise<void[]> => requestUpdateMetadata({ meta: serverMetadata }, 'global', keysToValues);
|
||||
|
||||
export const requestUpdateMangaMetadata = async (
|
||||
manga: IMangaCard | IManga,
|
||||
manga: TManga,
|
||||
keysToValues: MetadataKeyValuePair[],
|
||||
): Promise<void[]> => requestUpdateMetadata(manga, 'manga', keysToValues);
|
||||
|
||||
export const requestUpdateChapterMetadata = async (
|
||||
mangaChapter: IMangaChapter,
|
||||
chapter: TChapter,
|
||||
keysToValues: MetadataKeyValuePair[],
|
||||
): Promise<void[]> => requestUpdateMetadata(mangaChapter.chapter, 'chapter', keysToValues);
|
||||
): Promise<void[]> => requestUpdateMetadata(chapter, 'chapter', keysToValues);
|
||||
|
||||
export const requestUpdateCategoryMetadata = async (
|
||||
category: ICategory,
|
||||
category: TCategory,
|
||||
keysToValues: MetadataKeyValuePair[],
|
||||
): Promise<void[]> => requestUpdateMetadata(category, 'category', keysToValues);
|
||||
|
||||
@@ -6,9 +6,15 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { IManga, Metadata, MetadataHolder, IReaderSettings, MetadataKeyValuePair } from '@/typings';
|
||||
import requestManager from '@/lib/RequestManager';
|
||||
import { getMetadataFrom, requestUpdateMangaMetadata, requestUpdateServerMetadata } from '@/util/metadata';
|
||||
import { Metadata, IReaderSettings, MetadataKeyValuePair, GqlMetaHolder, TManga } from '@/typings';
|
||||
import requestManager from '@/lib/requests/RequestManager.ts';
|
||||
import {
|
||||
convertFromGqlMeta,
|
||||
getMetadataFrom,
|
||||
requestUpdateMangaMetadata,
|
||||
requestUpdateServerMetadata,
|
||||
} from '@/util/metadata';
|
||||
import { MetaType } from '@/lib/graphql/generated/graphql.ts';
|
||||
|
||||
type UndefinedReaderSettings = {
|
||||
[setting in keyof IReaderSettings]: IReaderSettings[setting] | undefined;
|
||||
@@ -37,20 +43,21 @@ export const getReaderSettingsFromMetadata = (
|
||||
): IReaderSettings => getReaderSettingsWithDefaultValueFallback(meta, defaultSettings, applyMetadataMigration);
|
||||
|
||||
export const getReaderSettingsFor = (
|
||||
{ meta }: MetadataHolder,
|
||||
{ meta }: GqlMetaHolder = {},
|
||||
defaultSettings?: IReaderSettings,
|
||||
applyMetadataMigration?: boolean,
|
||||
): IReaderSettings => getReaderSettingsFromMetadata(meta, defaultSettings, applyMetadataMigration);
|
||||
): IReaderSettings => getReaderSettingsFromMetadata(convertFromGqlMeta(meta), defaultSettings, applyMetadataMigration);
|
||||
|
||||
export const useDefaultReaderSettings = (): {
|
||||
metadata?: Metadata;
|
||||
settings: IReaderSettings;
|
||||
loading: boolean;
|
||||
} => {
|
||||
const { data: meta, isLoading } = requestManager.useGetGlobalMeta();
|
||||
const settings = getReaderSettingsWithDefaultValueFallback<IReaderSettings>(meta);
|
||||
const { data, loading } = requestManager.useGetGlobalMeta();
|
||||
const metadata = convertFromGqlMeta(data?.metas.nodes);
|
||||
const settings = getReaderSettingsWithDefaultValueFallback<IReaderSettings>(metadata);
|
||||
|
||||
return { metadata: meta, settings, loading: isLoading };
|
||||
return { metadata, settings, loading };
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -61,11 +68,12 @@ export const useDefaultReaderSettings = (): {
|
||||
* @param defaultSettings
|
||||
*/
|
||||
export const checkAndHandleMissingStoredReaderSettings = async (
|
||||
metadataHolder: IManga | MetadataHolder,
|
||||
metadataHolder: Required<GqlMetaHolder> | MetaType[],
|
||||
metadataHolderType: 'manga' | 'server',
|
||||
defaultSettings: IReaderSettings,
|
||||
): Promise<void | void[]> => {
|
||||
const meta = metadataHolder.meta ?? (metadataHolder as Metadata);
|
||||
const getMeta = () => (Array.isArray(metadataHolder) ? metadataHolder : metadataHolder.meta);
|
||||
const meta = convertFromGqlMeta(getMeta())!;
|
||||
const settingsToCheck = getReaderSettingsWithDefaultValueFallback(
|
||||
meta,
|
||||
{
|
||||
@@ -79,7 +87,7 @@ export const checkAndHandleMissingStoredReaderSettings = async (
|
||||
},
|
||||
false,
|
||||
);
|
||||
const newSettings = getReaderSettingsFor({ meta }, defaultSettings);
|
||||
const newSettings = getReaderSettingsFor({ meta: getMeta() }, defaultSettings);
|
||||
|
||||
const undefinedSettings = Object.entries(settingsToCheck).filter((setting) => setting[1] === undefined);
|
||||
|
||||
@@ -95,9 +103,9 @@ export const checkAndHandleMissingStoredReaderSettings = async (
|
||||
}
|
||||
|
||||
if (metadataHolderType === 'manga') {
|
||||
await requestUpdateMangaMetadata(metadataHolder as IManga, settingsToUpdate);
|
||||
await requestUpdateMangaMetadata(metadataHolder as TManga, settingsToUpdate);
|
||||
return;
|
||||
}
|
||||
|
||||
await requestUpdateServerMetadata(meta, settingsToUpdate);
|
||||
await requestUpdateServerMetadata(metadataHolder as MetaType[], settingsToUpdate);
|
||||
};
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
*/
|
||||
|
||||
import { Metadata, ISearchSettings } from '@/typings';
|
||||
import requestManager from '@/lib/RequestManager';
|
||||
import { getMetadataFrom } from '@/util/metadata';
|
||||
import requestManager from '@/lib/requests/RequestManager.ts';
|
||||
import { convertFromGqlMeta, getMetadataFrom } from '@/util/metadata';
|
||||
|
||||
export const getDefaultSettings = (): ISearchSettings => ({
|
||||
ignoreFilters: false,
|
||||
@@ -24,8 +24,9 @@ export const useSearchSettings = (): {
|
||||
settings: ISearchSettings;
|
||||
loading: boolean;
|
||||
} => {
|
||||
const { data: meta, isLoading } = requestManager.useGetGlobalMeta();
|
||||
const settings = getSearchSettingsWithDefaultValueFallback(meta);
|
||||
const { data, loading } = requestManager.useGetGlobalMeta();
|
||||
const metadata = convertFromGqlMeta(data?.metas.nodes);
|
||||
const settings = getSearchSettingsWithDefaultValueFallback(metadata);
|
||||
|
||||
return { metadata: meta, settings, loading: isLoading };
|
||||
return { metadata, settings, loading };
|
||||
};
|
||||
|
||||
110
tools/scripts/codegenFormatter.ts
Normal file
110
tools/scripts/codegenFormatter.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* 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 fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
const format = (source: string, regex: RegExp | string, replaceValue: string): string =>
|
||||
source.replace(regex, replaceValue);
|
||||
|
||||
let generatedGraphQLFilePath = path.resolve(__dirname, '../../src/lib/graphql/generated/graphql.ts');
|
||||
let generatedGraphQLFile = fs.readFileSync(generatedGraphQLFilePath, 'utf8');
|
||||
|
||||
// add logic to format the codegen generated graphql file
|
||||
|
||||
/* ******************************************* */
|
||||
/* */
|
||||
/* typescript, typescript-operations */
|
||||
/* */
|
||||
/* ******************************************* */
|
||||
|
||||
const fixCursorTyping = format(
|
||||
generatedGraphQLFile,
|
||||
/Cursor: \{ input: any; output: any; }/g,
|
||||
'Cursor: { input: string; output: string; }',
|
||||
);
|
||||
|
||||
const fixLongStringTyping = format(
|
||||
fixCursorTyping,
|
||||
/LongString: \{ input: any; output: any; }/g,
|
||||
'LongString: { input: string; output: string; }',
|
||||
);
|
||||
|
||||
const fixSubscriptionHookNameSuffix = format(fixLongStringTyping, /SubscriptionSubscription/g, 'Subscription');
|
||||
|
||||
fs.writeFileSync(generatedGraphQLFilePath, fixSubscriptionHookNameSuffix);
|
||||
|
||||
/* ****************************************** */
|
||||
/* */
|
||||
/* typescript-apollo-client-helpers */
|
||||
/* */
|
||||
/* ****************************************** */
|
||||
|
||||
generatedGraphQLFilePath = path.resolve(__dirname, '../../src/lib/graphql/generated/apollo-helpers.ts');
|
||||
generatedGraphQLFile = fs.readFileSync(generatedGraphQLFilePath, 'utf8');
|
||||
|
||||
const addImports = format(
|
||||
generatedGraphQLFile,
|
||||
`import { FieldPolicy, FieldReadFunction, TypePolicies, TypePolicy } from '@apollo/client/cache';`,
|
||||
`import { FieldPolicy, FieldReadFunction, TypePolicies, TypePolicy } from '@apollo/client/cache';
|
||||
import { GetChaptersQuery } from "@/lib/graphql/generated/graphql.ts";`,
|
||||
);
|
||||
|
||||
const fixTypingOfQueryTypePolicies = format(
|
||||
addImports,
|
||||
`export type QueryFieldPolicy = {
|
||||
\tabout?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||
\tcategories?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||
\tcategory?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||
\tchapter?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||
\tchapters?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||
\tcheckForServerUpdates?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||
\tcheckForWebUIUpdate?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||
\tdownloadStatus?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||
\textension?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||
\textensions?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||
\tgetWebUIUpdateStatus?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||
\tlastUpdateTimestamp?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||
\tmanga?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||
\tmangas?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||
\tmeta?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||
\tmetas?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||
\trestoreStatus?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||
\tsettings?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||
\tsource?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||
\tsources?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||
\tupdateStatus?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||
\tvalidateBackup?: FieldPolicy<any> | FieldReadFunction<any>
|
||||
};`,
|
||||
`export type QueryFieldPolicy = {
|
||||
\tabout?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||
\tcategories?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||
\tcategory?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||
\tchapter?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||
\tchapters?: FieldPolicy<GetChaptersQuery['chapters']> | FieldReadFunction<GetChaptersQuery['chapters']>,
|
||||
\tcheckForServerUpdates?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||
\tcheckForWebUIUpdate?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||
\tdownloadStatus?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||
\textension?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||
\textensions?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||
\tgetWebUIUpdateStatus?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||
\tlastUpdateTimestamp?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||
\tmanga?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||
\tmangas?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||
\tmeta?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||
\tmetas?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||
\trestoreStatus?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||
\tsettings?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||
\tsource?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||
\tsources?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||
\tupdateStatus?: FieldPolicy<any> | FieldReadFunction<any>,
|
||||
\tvalidateBackup?: FieldPolicy<any> | FieldReadFunction<any>
|
||||
};`,
|
||||
);
|
||||
|
||||
fs.writeFileSync(generatedGraphQLFilePath, fixTypingOfQueryTypePolicies);
|
||||
@@ -19,7 +19,7 @@
|
||||
/* Bundler mode */
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"moduleResolution": "bundler",
|
||||
"moduleResolution": "node",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
|
||||
@@ -8,6 +8,6 @@
|
||||
},
|
||||
"include": [
|
||||
"vite.config.ts",
|
||||
"vite.config.ts"
|
||||
"gql_codegen.ts"
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user