Add "ui login" support

This commit is contained in:
schroda
2025-07-27 00:03:51 +02:00
parent 403cab7d80
commit 4aad410957
33 changed files with 1270 additions and 203 deletions

View File

@@ -63,6 +63,7 @@
"i18next": "25.3.6",
"i18next-browser-languagedetector": "8.2.0",
"i18next-http-backend": "3.0.2",
"js-cookie": "^3.0.5",
"jsonrepair": "3.13.0",
"material-ui-popup-state": "5.3.6",
"mui-nested-menu": "4.0.1",
@@ -88,6 +89,7 @@
"@graphql-codegen/typescript-apollo-client-helpers": "3.0.1",
"@graphql-codegen/typescript-operations": "4.6.1",
"@types/apollo-upload-client": "18.0.0",
"@types/js-cookie": "^3.0.6",
"@types/node": "24.3.0",
"@types/react": "19.1.10",
"@types/react-beautiful-dnd": "13.1.8",

View File

@@ -116,9 +116,9 @@
},
"label": {
"action": {
"all": "Mark all as read",
"current": "Mark as read",
"previous": "Mark previous as read",
"all": "Mark all as read"
"previous": "Mark previous as read"
},
"confirmation_one": "You are about to mark one chapter as read",
"confirmation_other": "You are about to mark {{count}} chapters as read",
@@ -521,6 +521,11 @@
}
},
"time": {
"days": {
"day_one": "Day",
"day_other": "Days",
"value": "{{count}} $t(global.time.days.day, lowercase)"
},
"hour_short": "h",
"minutes": {
"minute_one": "Minute",
@@ -1297,6 +1302,11 @@
}
},
"auth": {
"jwt": {
"access_token_expiry": "JWT access token expiry",
"audience": "JWT audience claim",
"refresh_token_expiry": "JWT refresh token expiry"
},
"label": {
"password": "$t(global.label.password)",
"title": "Authentication Mode",
@@ -1317,12 +1327,18 @@
"title": "None"
}
},
"simpleLogin": {
"simple_login": {
"label": {
"description": "A simple login page will be presented to login.",
"description": "The login will be handled by the server.",
"info": "When you enable this, you may need to refresh this tab for the login page to appear.",
"title": "Simple Login"
}
},
"ui_login": {
"label": {
"description": "The login will be handled by the client.",
"title": "UI Login"
}
}
}
},

View File

@@ -8,7 +8,7 @@
import CssBaseline from '@mui/material/CssBaseline';
import React, { useLayoutEffect } from 'react';
import { Navigate, Route, Routes, useLocation } from 'react-router-dom';
import { Navigate, Outlet, Route, Routes, useLocation } from 'react-router-dom';
import { loadErrorMessages, loadDevMessages } from '@apollo/client/dev';
import { loadable } from 'react-lazily/loadable';
import Box from '@mui/material/Box';
@@ -27,6 +27,10 @@ import { AppRoutes } from '@/base/AppRoute.constants.ts';
import { useMetadataServerSettings } from '@/features/settings/services/ServerSettingsMetadata.ts';
import { MediaQuery } from '@/base/utils/MediaQuery.tsx';
import { BrowseTab } from '@/features/browse/Browse.types.ts';
import { useSessionContext } from '@/features/authentication/SessionContext.tsx';
import { LoginPage } from '@/features/authentication/screens/LoginPage.tsx';
import { AuthGuard } from '@/features/authentication/components/AuthGuard.tsx';
import { SearchParam } from '@/base/Base.types.ts';
const { Browse } = loadable(() => import('@/features/browse/screens/Browse.tsx'), lazyLoadFallback);
const { DownloadQueue } = loadable(() => import('@/features/downloads/screens/DownloadQueue.tsx'), lazyLoadFallback);
@@ -82,7 +86,7 @@ const { More } = loadable(() => import('@/features/settings/screens/More.tsx'),
const { Reader } = loadable(() => import('@/features/reader/screens/Reader.tsx'), lazyLoadFallback);
const { HistorySettings } = loadable(() => import('@/features/history/screens/HistorySettings.tsx'), lazyLoadFallback);
if (process.env.NODE_ENV !== 'production') {
if (import.meta.env.DEV) {
// Adds messages only in a dev environment
loadDevMessages();
loadErrorMessages();
@@ -105,15 +109,39 @@ const ScrollToTop = () => {
* and thus, data of existing chapters/mangas in the cache get outdated
*/
const BackgroundSubscriptions = () => {
// load the full download status once on startup to fill the cache
// Listen to session changes
const { isAuthRequired, accessToken } = useSessionContext();
const skipConnection = isAuthRequired == null || (!!isAuthRequired && !accessToken);
// Load the full download status once on startup to fill the cache
requestManager.useGetDownloadStatus({ nextFetchPolicy: 'standby' });
requestManager.useDownloadSubscription();
requestManager.useUpdaterSubscription();
requestManager.useWebUIUpdateSubscription();
requestManager.useDownloadSubscription({ skip: skipConnection });
requestManager.useUpdaterSubscription({ skip: skipConnection });
requestManager.useWebUIUpdateSubscription({ skip: skipConnection });
return null;
};
const PrivateRoutes = () => {
const { isAuthRequired, accessToken, refreshToken } = useSessionContext();
const isAuthenticated = !isAuthRequired || (isAuthRequired && (accessToken || refreshToken));
if (!isAuthenticated) {
return (
<Navigate
to={{
pathname: AppRoutes.authentication.childRoutes.login.path,
search: `${SearchParam.REDIRECT}=${window.location.pathname}`,
}}
replace
/>
);
}
return <Outlet />;
};
const MainApp = () => {
const { navBarWidth, appBarHeight, bottomBarHeight } = useNavBarContext();
const isMobileWidth = MediaQuery.useIsMobileWidth();
@@ -139,15 +167,32 @@ const MainApp = () => {
>
<ErrorBoundary>
<Routes>
<Route path={AppRoutes.authentication.match}>
<Route path={AppRoutes.authentication.childRoutes.login.match} element={<LoginPage />} />
</Route>
<Route element={<PrivateRoutes />}>
{/* General Routes */}
<Route path={AppRoutes.root.match} element={<Navigate to={AppRoutes.library.path()} replace />} />
<Route path={AppRoutes.matchAll.match} element={<Navigate to={AppRoutes.root.path} replace />} />
<Route
path={AppRoutes.root.match}
element={<Navigate to={AppRoutes.library.path()} replace />}
/>
<Route
path={AppRoutes.matchAll.match}
element={<Navigate to={AppRoutes.root.path} replace />}
/>
{isMobileWidth && <Route path={AppRoutes.more.match} element={<More />} />}
<Route path={AppRoutes.about.match} element={<About />} />
<Route path={AppRoutes.settings.match}>
<Route index element={<Settings />} />
<Route path={AppRoutes.settings.childRoutes.categories.match} element={<CategorySettings />} />
<Route path={AppRoutes.settings.childRoutes.reader.match} element={<GlobalReaderSettings />} />
<Route
path={AppRoutes.settings.childRoutes.categories.match}
element={<CategorySettings />}
/>
<Route
path={AppRoutes.settings.childRoutes.reader.match}
element={<GlobalReaderSettings />}
/>
<Route path={AppRoutes.settings.childRoutes.library.match}>
<Route index element={<LibrarySettings />} />
<Route
@@ -155,14 +200,20 @@ const MainApp = () => {
element={<LibraryDuplicates />}
/>
</Route>
<Route path={AppRoutes.settings.childRoutes.download.match} element={<DownloadSettings />} />
<Route
path={AppRoutes.settings.childRoutes.download.match}
element={<DownloadSettings />}
/>
<Route path={AppRoutes.settings.childRoutes.backup.match} element={<Backup />} />
<Route path={AppRoutes.settings.childRoutes.server.match} element={<ServerSettings />} />
<Route path={AppRoutes.settings.childRoutes.webui.match} element={<WebUISettings />} />
<Route path={AppRoutes.settings.childRoutes.browse.match} element={<BrowseSettings />} />
<Route path={AppRoutes.settings.childRoutes.history.match} element={<HistorySettings />} />
<Route path={AppRoutes.settings.childRoutes.device.match} element={<DeviceSetting />} />
<Route path={AppRoutes.settings.childRoutes.tracking.match} element={<TrackingSettings />} />
<Route
path={AppRoutes.settings.childRoutes.tracking.match}
element={<TrackingSettings />}
/>
<Route path={AppRoutes.settings.childRoutes.appearance.match} element={<Appearance />} />
</Route>
@@ -176,7 +227,10 @@ const MainApp = () => {
<Route path={AppRoutes.sources.childRoutes.searchAll.match} element={<SearchAll />} />
</Route>
<Route path={AppRoutes.extension.match}>
<Route index element={<Navigate to={AppRoutes.browse.path(BrowseTab.EXTENSIONS)} replace />} />
<Route
index
element={<Navigate to={AppRoutes.browse.path(BrowseTab.EXTENSIONS)} replace />}
/>
<Route path={AppRoutes.extension.childRoutes.info.match} element={<ExtensionInfo />} />
</Route>
<Route path={AppRoutes.downloads.match} element={<DownloadQueue />} />
@@ -193,6 +247,7 @@ const MainApp = () => {
<Route path={AppRoutes.migrate.childRoutes.search.match} element={<SearchAll />} />
</Route>
<Route path={AppRoutes.tracker.match} element={<TrackerOAuthLogin />} />
</Route>
</Routes>
</ErrorBoundary>
</Box>
@@ -214,6 +269,7 @@ export const App: React.FC = () => (
<WebUIUpdateChecker />
<BackgroundSubscriptions />
<CssBaseline enableColorScheme />
<AuthGuard>
<Box sx={{ display: 'flex' }}>
<Box sx={{ flexShrink: 0, position: 'relative', height: '100vh' }}>
<DefaultNavBar />
@@ -223,5 +279,6 @@ export const App: React.FC = () => (
<Route path={AppRoutes.reader.match} element={<ReaderApp />} />
</Routes>
</Box>
</AuthGuard>
</AppContext>
);

View 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 SvgIcon, { SvgIconProps } from '@mui/material/SvgIcon';
export const SuwayomiLogo = ({
circleRingColor = '#35d4d5',
...props
}: SvgIconProps & { circleRingColor?: string }) => (
<SvgIcon {...props}>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="100" height="100">
<svg version="1.1" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<circle cx="50" cy="50" r="50" fill={circleRingColor} />
<circle cx="50" cy="50" r="39" fill="#1A1A1D" />
<path
d="m26.46 38.07 15.633-.27c3.348-.081 3.348-.081 4.482.783.783.972 1.161 1.701 1.404 2.916-.216 1.053-.567 1.593-1.242 2.403l-3.321 4.509c-.513.702-.513.702-.756 1.539.216.108.216.108.459.189 2.322 1.269 4.347 3.402 6.291 5.211l1.539 1.431c.459.621.594 1.053.621 1.809l-2.268 2.619c-.675.54-1.188.459-2.052.351-.702-.513-.702-.513-1.404-1.242l-3.861-3.996c-.81-.837-1.674-1.593-2.565-2.322-.972.459-1.62.999-2.376 1.728-7.29 6.723-7.29 6.723-10.206 6.615-1.161-.432-1.998-1.215-2.808-2.133-.27-.918-.27-.918-.27-1.62 1.08-.918 2.187-1.755 3.402-2.457 4.428-2.592 8.046-6.183 11.124-10.287 1.026-1.431 1.35-1.998 1.674-2.646l-12.15.054c-.54-.054-.54-.054-1.08-.594-.243-1.107-.27-2.16-.27-3.24l0-1.35z"
fill="#fdfdfd"
/>
<path
d="m78.6486 37.8378 1.5405 1.5135c-.5405 8.027-5.4324 16.1892-10.8649 21.8378-1.4054 1.4324-1.8649 1.8919-3.0811 2.1081-.9459-.1892-1.6757-.4324-2.5405-.8378l-1.3514-.6757c-.1081-1.2162-.1081-1.2162.3243-1.8378l3.2432-3.0811c3.8919-3.8919 6.1892-9.2703 7.5135-14.5405h-11.6216v7.8378c-.6216.2973-.973.2973-1.6486.2973-3.7297 0-3.7297 0-4.027-.2973v-13.2432l18.4865-.2973c1.6216.027 2.8378.0541 4.027 1.2162z"
fill="#f9f9f9"
/>
</svg>
</svg>
</SvgIcon>
);

View File

@@ -38,13 +38,23 @@ export const AppRoutes = {
matchAll: {
match: '*',
},
authentication: {
match: 'auth',
path: '/auth',
childRoutes: {
login: {
match: 'login',
path: '/auth/login',
},
},
},
about: {
match: 'about',
path: '/about',
},
settings: {
path: '/settings',
match: 'settings',
path: '/settings',
childRoutes: {
categories: {
match: 'categories',

View File

@@ -71,4 +71,5 @@ export enum ScrollDirection {
export enum SearchParam {
TAB = 'tab',
QUERY = 'query',
REDIRECT = 'redirect',
}

View 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 Avatar, { AvatarProps } from '@mui/material/Avatar';
import { SpinnerImage, SpinnerImageProps } from '@/base/components/SpinnerImage.tsx';
export const AvatarSpinner = ({
iconUrl,
alt,
slots,
}: {
iconUrl: string;
alt: string;
slots?: { avatarProps?: Partial<AvatarProps>; spinnerImageProps?: Partial<SpinnerImageProps> };
}) => (
<Avatar variant="rounded" alt={alt} {...slots?.avatarProps}>
<SpinnerImage
alt={alt}
src={iconUrl}
{...slots?.spinnerImageProps}
spinnerStyle={{ small: true, ...slots?.spinnerImageProps?.spinnerStyle }}
imgStyle={{ objectFit: 'cover', width: '100%', height: '100%', ...slots?.spinnerImageProps?.imgStyle }}
/>
</Avatar>
);

View File

@@ -6,36 +6,28 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import Avatar, { AvatarProps } from '@mui/material/Avatar';
import { SpinnerImage, SpinnerImageProps } from '@/base/components/SpinnerImage.tsx';
import { ComponentProps } from 'react';
import { AvatarSpinner } from '@/base/components/AvatarSpinner.tsx';
export const ListCardAvatar = ({
iconUrl,
alt,
slots,
}: {
iconUrl: string;
alt: string;
slots?: { avatarProps?: Partial<AvatarProps>; spinnerImageProps?: Partial<SpinnerImageProps> };
}) => (
<Avatar
variant="rounded"
alt={alt}
{...slots?.avatarProps}
sx={{
export const ListCardAvatar = (props: ComponentProps<typeof AvatarSpinner>) => {
const { slots } = props;
return (
<AvatarSpinner
{...props}
slots={{
...slots,
avatarProps: {
...slots?.avatarProps,
sx: {
width: 56,
height: 56,
flex: '0 0 auto',
background: 'transparent',
...slots?.avatarProps?.sx,
},
},
}}
>
<SpinnerImage
alt={alt}
src={iconUrl}
{...slots?.spinnerImageProps}
spinnerStyle={{ small: true, ...slots?.spinnerImageProps?.spinnerStyle }}
imgStyle={{ objectFit: 'cover', width: '100%', height: '100%', ...slots?.spinnerImageProps?.imgStyle }}
/>
</Avatar>
);
};

View File

@@ -19,12 +19,14 @@ import { SnackbarWithDescription } from '@/base/components/feedback/SnackbarWith
import { AppPageHistoryContextProvider } from '@/base/contexts/AppPageHistoryContext.tsx';
import { AppThemeContextProvider } from '@/features/theme/AppThemeContext.tsx';
import { NavBarContextProvider } from '@/features/navigation-bar/NavbarContext.tsx';
import { SessionContextProvider } from '@/features/authentication/SessionContext.tsx';
interface Props {
children: React.ReactNode;
}
export const AppContext: React.FC<Props> = ({ children }) => (
<SessionContextProvider>
<Router>
<StyledEngineProvider injectFirst>
<AppThemeContextProvider>
@@ -52,4 +54,5 @@ export const AppContext: React.FC<Props> = ({ children }) => (
</AppThemeContextProvider>
</StyledEngineProvider>
</Router>
</SessionContextProvider>
);

View File

@@ -0,0 +1,93 @@
/*
* 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 { AppStorage } from '@/lib/storage/AppStorage.ts';
import { useSessionStorage } from '@/base/hooks/useStorage.tsx';
export class AuthManager {
static readonly AUTH_REQUIRED_KEY = 'auth-required';
static readonly REFRESH_TOKEN_KEY = 'auth-refresh-token';
static readonly REACT_SESSION_REFRESH_KEY = 'auth-react-session-refresh';
private static accessToken: string | null = null;
static isAuthRequired(): boolean | null {
return AppStorage.session.getItemParsed(AuthManager.AUTH_REQUIRED_KEY, null);
}
static setAuthRequired(value: boolean | null): void {
AppStorage.session.setItem(AuthManager.AUTH_REQUIRED_KEY, value);
}
static useIsAuthRequired(): boolean | null {
const [value] = useSessionStorage(AuthManager.AUTH_REQUIRED_KEY, null);
return value;
}
static getAccessToken(): string | null {
return AuthManager.accessToken;
}
static getRefreshToken(): string | null {
return AppStorage.session.getItemParsed(AuthManager.REFRESH_TOKEN_KEY, null);
}
static getTokens(): { accessToken: string | null; refreshToken: string | null } {
return {
accessToken: AuthManager.getAccessToken(),
refreshToken: AuthManager.getRefreshToken(),
};
}
static setAccessToken(token: string): void {
AuthManager.accessToken = token;
AuthManager.refreshReactSessionContext();
}
static setRefreshToken(token: string): void {
AppStorage.session.setItem(AuthManager.REFRESH_TOKEN_KEY, token);
AuthManager.refreshReactSessionContext();
}
static setTokens(accessToken: string, refreshToken: string): void {
AuthManager.setAccessToken(accessToken);
AuthManager.setRefreshToken(refreshToken);
}
static removeAccessToken(): void {
AuthManager.accessToken = null;
AuthManager.refreshReactSessionContext();
}
static removeRefreshToken(): void {
AppStorage.session.setItem(AuthManager.REFRESH_TOKEN_KEY, undefined);
AuthManager.refreshReactSessionContext();
}
static removeTokens(): void {
AuthManager.removeAccessToken();
AuthManager.removeRefreshToken();
}
private static getNextReactSessionContextId(): number {
const id = AppStorage.session.getItemParsed(AuthManager.REACT_SESSION_REFRESH_KEY, 0);
return (id + 1) % Number.MAX_SAFE_INTEGER;
}
static refreshReactSessionContext(): void {
AppStorage.session.setItem(AuthManager.REACT_SESSION_REFRESH_KEY, AuthManager.getNextReactSessionContextId());
}
static useListenToReactSessionContextRefreshEvent(): void {
useSessionStorage(AuthManager.REACT_SESSION_REFRESH_KEY, 0);
}
}

View 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 { createContext, ReactNode, useContext, useMemo } from 'react';
import { AuthManager } from '@/features/authentication/AuthManager.ts';
interface TSessionContext {
isAuthRequired: boolean | null;
accessToken: string | null;
refreshToken: string | null;
}
const SessionContext = createContext<TSessionContext>({
isAuthRequired: null,
accessToken: null,
refreshToken: null,
});
export const SessionContextProvider = ({ children }: { children: ReactNode }) => {
AuthManager.useListenToReactSessionContextRefreshEvent();
const isAuthRequired = AuthManager.useIsAuthRequired();
const { accessToken, refreshToken } = AuthManager.getTokens();
const value = useMemo(
() => ({
isAuthRequired,
accessToken,
refreshToken,
}),
[isAuthRequired, accessToken, refreshToken],
);
return <SessionContext.Provider value={value}>{children}</SessionContext.Provider>;
};
export const useSessionContext = () => useContext(SessionContext);

View File

@@ -0,0 +1,36 @@
/*
* 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 { ReactNode, useEffect } from 'react';
import { useSessionContext } from '@/features/authentication/SessionContext.tsx';
import { SplashScreen } from '@/features/authentication/components/SplashScreen.tsx';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { AuthManager } from '@/features/authentication/AuthManager.ts';
export const AuthGuard = ({ children }: { children: ReactNode }) => {
const { isAuthRequired } = useSessionContext();
requestManager.useGetAbout({
skip: isAuthRequired !== null,
onCompleted: () => AuthManager.setAuthRequired(false),
});
useEffect(() => {
const onUnload = () => AuthManager.setAuthRequired(null);
window.addEventListener('beforeunload', onUnload);
return () => window.removeEventListener('beforeunload', onUnload);
}, []);
if (isAuthRequired === null) {
return <SplashScreen />;
}
return children;
};

View File

@@ -0,0 +1,46 @@
/*
* 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 { useTheme } from '@mui/material/styles';
import Stack from '@mui/material/Stack';
import { SuwayomiLogo } from '@/assets/SuwayomiLogo.tsx';
import { ServerAddressSetting } from '@/features/settings/components/ServerAddressSetting.tsx';
export const SplashScreen = () => {
const theme = useTheme();
return (
<Stack
sx={{
minWidth: '100vw',
minHeight: '100vh',
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'primary.light',
...theme.applyStyles('dark', {
backgroundColor: 'primary.dark',
}),
}}
>
<SuwayomiLogo
sx={{
fontSize: 250,
[theme.breakpoints.up('lg')]: {
fontSize: 350,
},
}}
circleRingColor={theme.palette.primary.light}
/>
{import.meta.env.DEV && (
<Stack sx={{ height: 'auto', mt: 5 }}>
<ServerAddressSetting />
</Stack>
)}
</Stack>
);
};

View File

@@ -0,0 +1,143 @@
/*
* 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 { useTranslation } from 'react-i18next';
import { useEffect, useState } from 'react';
import TextField from '@mui/material/TextField';
import Stack from '@mui/material/Stack';
import Button from '@mui/material/Button';
import { Navigate, useNavigate } from 'react-router-dom';
import { useTheme } from '@mui/material/styles';
import { StringParam, useQueryParam } from 'use-query-params';
import { PasswordTextField } from '@/base/components/inputs/PasswordTextField.tsx';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { makeToast } from '@/base/utils/Toast.ts';
import { getErrorMessage } from '@/lib/HelperFunctions.ts';
import { AuthManager } from '@/features/authentication/AuthManager.ts';
import { SuwayomiLogo } from '@/assets/SuwayomiLogo.tsx';
import { useSessionContext } from '@/features/authentication/SessionContext.tsx';
import { AppRoutes } from '@/base/AppRoute.constants.ts';
import { useNavBarContext } from '@/features/navigation-bar/NavbarContext.tsx';
import { SearchParam } from '@/base/Base.types.ts';
import { ServerAddressSetting } from '@/features/settings/components/ServerAddressSetting.tsx';
export const LoginPage = () => {
const theme = useTheme();
const { t } = useTranslation();
const { setOverride } = useNavBarContext();
const navigate = useNavigate();
const { isAuthRequired, accessToken, refreshToken } = useSessionContext();
const [redirect] = useQueryParam(SearchParam.REDIRECT, StringParam);
const [loginUser, { loading: isLoading }] = requestManager.useLoginUser();
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const isAuthenticated = !isAuthRequired || (!!isAuthRequired && !!accessToken && !!refreshToken);
const doLogin = async () => {
try {
const { data } = await loginUser({ variables: { username, password } });
if (data) {
AuthManager.setTokens(data.login.accessToken, data.login.refreshToken);
navigate(redirect ?? AppRoutes.root.path);
}
} catch (e) {
makeToast(t('tracking.action.login.label.failure', { name: 'Suwayomi' }), 'error', getErrorMessage(e));
}
};
useEffect(() => {
setOverride({ status: true, value: null });
return () => setOverride({ status: false, value: null });
}, []);
if (isAuthenticated) {
return <Navigate to={AppRoutes.root.path} replace />;
}
return (
<Stack
sx={{
[theme.breakpoints.up('lg')]: {
flexDirection: 'row',
},
}}
>
<Stack
sx={{
minHeight: '50vh',
flexBasis: '60%',
p: 4,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'primary.light',
...theme.applyStyles('dark', {
backgroundColor: 'primary.dark',
}),
[theme.breakpoints.up('lg')]: {
minHeight: '0vh',
height: '100vh',
},
}}
>
<SuwayomiLogo
sx={{
fontSize: 250,
[theme.breakpoints.up('lg')]: {
fontSize: 350,
},
}}
circleRingColor={theme.palette.primary.light}
/>
</Stack>
<Stack
sx={{
minHeight: '50vh',
flexBasis: '40%',
p: 4,
justifyContent: 'center',
alignItems: 'center',
[theme.breakpoints.up('lg')]: {
minHeight: '0vh',
height: '100vh',
},
}}
>
<Stack sx={{ maxWidth: 300, gap: 2 }}>
<Stack>
<TextField
autoFocus
margin="dense"
id="username"
name="username"
label={t('global.label.username')}
type="text"
fullWidth
variant="standard"
onChange={(e) => setUsername(e.target.value)}
/>
<PasswordTextField
margin="dense"
fullWidth
variant="standard"
onChange={(e) => setPassword(e.target.value)}
/>
{import.meta.env.DEV && <ServerAddressSetting />}
</Stack>
<Button disabled={isLoading || (!username && !password)} variant="contained" onClick={doLogin}>
{t('global.button.log_in')}
</Button>
</Stack>
</Stack>
</Stack>
);
};

View File

@@ -6,6 +6,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import dayjs from 'dayjs';
import { DEFAULT_DEVICE } from '@/features/device/services/Device.ts';
import { DEFAULT_SORT_SETTINGS } from '@/features/migration/Migration.constants.ts';
import { GlobalUpdateSkipEntriesSettings, MetadataServerSettings } from '@/features/settings/Settings.types.ts';
@@ -90,9 +91,13 @@ const AUTH_MODES_TO_TRANSLATION_KEY: { [mode in AuthMode]: SelectSettingValueDis
description: 'settings.server.auth.mode.option.basicAuth.label.description',
},
[AuthMode.SimpleLogin]: {
text: 'settings.server.auth.mode.option.simpleLogin.label.title',
description: 'settings.server.auth.mode.option.simpleLogin.label.description',
disclaimer: 'settings.server.auth.mode.option.simpleLogin.label.info',
text: 'settings.server.auth.mode.option.simple_login.label.title',
description: 'settings.server.auth.mode.option.simple_login.label.description',
disclaimer: 'settings.server.auth.mode.option.simple_login.label.info',
},
[AuthMode.UiLogin]: {
text: 'settings.server.auth.mode.option.ui_login.label.title',
description: 'settings.server.auth.mode.option.ui_login.label.description',
},
};
export const AUTH_MODES_SELECT_VALUES: SelectSettingValue<AuthMode>[] = AUTH_MODES.map((mode) => [
@@ -183,3 +188,15 @@ export const WEB_UI_UPDATE_INTERVAL = {
min: 1,
max: 23,
};
export const JWT_ACCESS_TOKEN_EXPIRY = {
default: dayjs.duration(5, 'minute').asMinutes(),
min: dayjs.duration(1, 'minute').asMinutes(),
max: dayjs.duration(4, 'hour').asMinutes(),
};
export const JWT_REFRESH_TOKEN_EXPIRY = {
default: dayjs.duration(60, 'day').asDays(),
min: dayjs.duration(1, 'day').asDays(),
max: dayjs.duration(1, 'year').asDays(),
};

View File

@@ -55,6 +55,9 @@ export type ServerSettingsType = Pick<
| 'authMode'
| 'authUsername'
| 'authPassword'
| 'jwtAudience'
| 'jwtTokenExpiry'
| 'jwtRefreshExpiry'
| 'flareSolverrEnabled'
| 'flareSolverrTimeout'
| 'flareSolverrUrl'

View File

@@ -0,0 +1,33 @@
/*
* 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 { useTranslation } from 'react-i18next';
import { TextSetting } from '@/base/components/settings/text/TextSetting.tsx';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { useLocalStorage } from '@/base/hooks/useStorage.tsx';
export const ServerAddressSetting = () => {
const { t } = useTranslation();
const [serverAddress, setServerAddress] = useLocalStorage<string>('serverBaseURL', window.location.origin);
const handleServerAddressChange = (address: string) => {
const serverBaseUrl = address.replaceAll(/(\/)+$/g, '');
setServerAddress(serverBaseUrl);
requestManager.reset();
};
return (
<TextSetting
settingName={t('settings.about.server.label.address')}
handleChange={handleServerAddressChange}
value={serverAddress}
placeholder="http://localhost:4567"
/>
);
};

View File

@@ -15,8 +15,8 @@ import ListItemText from '@mui/material/ListItemText';
import Switch from '@mui/material/Switch';
import ListSubheader from '@mui/material/ListSubheader';
import { t as translate } from 'i18next';
import dayjs from 'dayjs';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { useLocalStorage } from '@/base/hooks/useStorage.tsx';
import { TextSetting } from '@/base/components/settings/text/TextSetting.tsx';
import { NumberSetting } from '@/base/components/settings/NumberSetting.tsx';
import { SelectSetting } from '@/base/components/settings/SelectSetting.tsx';
@@ -33,7 +33,13 @@ import { ServerSettings as GqlServerSettings, ServerSettingsType } from '@/featu
import { getErrorMessage } from '@/lib/HelperFunctions.ts';
import { useAppTitle } from '@/features/navigation-bar/hooks/useAppTitle.ts';
import { AuthMode, SortOrder } from '@/lib/graphql/generated/graphql';
import { AUTH_MODES_SELECT_VALUES } from '@/features/settings/Settings.constants.ts';
import {
AUTH_MODES_SELECT_VALUES,
JWT_ACCESS_TOKEN_EXPIRY,
JWT_REFRESH_TOKEN_EXPIRY,
} from '@/features/settings/Settings.constants.ts';
import { ServerAddressSetting } from '@/features/settings/components/ServerAddressSetting.tsx';
import { AuthManager } from '@/features/authentication/AuthManager.ts';
const extractServerSettings = (settings: GqlServerSettings): ServerSettingsType => ({
ip: settings.ip,
@@ -52,6 +58,9 @@ const extractServerSettings = (settings: GqlServerSettings): ServerSettingsType
authMode: settings.authMode,
authUsername: settings.authUsername,
authPassword: settings.authPassword,
jwtAudience: settings.jwtAudience,
jwtTokenExpiry: settings.jwtTokenExpiry,
jwtRefreshExpiry: settings.jwtRefreshExpiry,
flareSolverrEnabled: settings.flareSolverrEnabled,
flareSolverrTimeout: settings.flareSolverrTimeout,
flareSolverrUrl: settings.flareSolverrUrl,
@@ -99,24 +108,18 @@ export const ServerSettings = () => {
});
const [mutateSettings] = requestManager.useUpdateServerSettings();
const [serverAddress, setServerAddress] = useLocalStorage<string>(
'serverBaseURL',
import.meta.env.VITE_SERVER_URL_DEFAULT,
);
const handleServerAddressChange = (address: string) => {
const serverBaseUrl = address.replaceAll(/(\/)+$/g, '');
setServerAddress(serverBaseUrl);
requestManager.reset();
};
const updateSetting = <Setting extends keyof ServerSettingsType>(
const updateSetting = async <Setting extends keyof ServerSettingsType>(
setting: Setting,
value: ServerSettingsType[Setting],
onCompletion?: (success: boolean) => void,
) => {
mutateSettings({ variables: { input: { settings: { [setting]: value } } } }).catch((e) =>
makeToast(t('global.error.label.failed_to_save_changes'), 'error', getErrorMessage(e)),
);
try {
await mutateSettings({ variables: { input: { settings: { [setting]: value } } } });
onCompletion?.(true);
} catch (e) {
makeToast(t('global.error.label.failed_to_save_changes'), 'error', getErrorMessage(e));
onCompletion?.(false);
}
};
const localSettings = useMemo(
@@ -128,12 +131,7 @@ export const ServerSettings = () => {
</ListSubheader>
}
>
<TextSetting
settingName={t('settings.about.server.label.address')}
handleChange={handleServerAddressChange}
value={serverAddress}
placeholder="http://localhost:4567"
/>
<ServerAddressSetting />
<ListItem>
<ListItemText
primary={t('global.update.settings.inform.label.title')}
@@ -147,7 +145,7 @@ export const ServerSettings = () => {
</ListItem>
</List>
),
[serverAddress, serverInformAvailableUpdate],
[serverInformAvailableUpdate],
);
const loading = areMetadataServerSettingsLoading || areServerSettingsLoading;
@@ -187,8 +185,8 @@ export const ServerSettings = () => {
}
const serverSettings = extractServerSettings(data!.settings);
const authModeDisabled = !serverSettings.authUsername.trim() || !serverSettings.authPassword.trim();
const authModeDisabled = !serverSettings.authUsername?.trim() || !serverSettings.authPassword?.trim();
console.log('asdf', serverSettings.jwtTokenExpiry, serverSettings.jwtRefreshExpiry);
return (
<List sx={{ pt: 0 }}>
{localSettings}
@@ -271,7 +269,19 @@ export const ServerSettings = () => {
settingName={t('settings.server.auth.label.title')}
value={serverSettings.authMode}
values={AUTH_MODES_SELECT_VALUES}
handleChange={(mode) => updateSetting('authMode', mode)}
handleChange={(mode) => {
updateSetting('authMode', mode, (success) => {
if (!success) {
return;
}
if (mode !== AuthMode.UiLogin) {
AuthManager.removeTokens();
}
AuthManager.setAuthRequired(mode === AuthMode.UiLogin);
});
}}
disabled={authModeDisabled}
/>
<TextSetting
@@ -287,6 +297,41 @@ export const ServerSettings = () => {
validate={(value) => serverSettings.authMode === AuthMode.None || !!value.trim()}
handleChange={(authPassword) => updateSetting('authPassword', authPassword)}
/>
{serverSettings.authMode === AuthMode.UiLogin && (
<>
<TextSetting
settingName={t('settings.server.auth.jwt.audience')}
value={serverSettings.jwtAudience}
handleChange={(audience) => updateSetting('jwtAudience', audience)}
/>
<NumberSetting
settingTitle={t('settings.server.auth.jwt.access_token_expiry')}
settingValue={dayjs.duration(serverSettings.jwtTokenExpiry).humanize()}
value={dayjs.duration(serverSettings.jwtTokenExpiry).asMinutes()}
valueUnit={t('global.time.minutes.minute_other')}
defaultValue={JWT_ACCESS_TOKEN_EXPIRY.default}
minValue={JWT_ACCESS_TOKEN_EXPIRY.min}
maxValue={JWT_ACCESS_TOKEN_EXPIRY.max}
handleUpdate={(expiry) =>
updateSetting('jwtTokenExpiry', dayjs.duration(expiry, 'minute').toISOString())
}
showSlider
/>
<NumberSetting
settingTitle={t('settings.server.auth.jwt.refresh_token_expiry')}
settingValue={dayjs.duration(serverSettings.jwtRefreshExpiry).humanize()}
value={dayjs.duration(serverSettings.jwtRefreshExpiry).asDays()}
valueUnit={t('global.time.days.day_other')}
defaultValue={JWT_REFRESH_TOKEN_EXPIRY.default}
minValue={JWT_REFRESH_TOKEN_EXPIRY.min}
maxValue={JWT_REFRESH_TOKEN_EXPIRY.max}
handleUpdate={(expiry) =>
updateSetting('jwtRefreshExpiry', dayjs.duration(expiry, 'day').toISOString())
}
showSlider
/>
</>
)}
</List>
<List
subheader={

View File

@@ -13,7 +13,6 @@ import ListItemButton from '@mui/material/ListItemButton';
import Chip from '@mui/material/Chip';
import ListItemAvatar from '@mui/material/ListItemAvatar';
import ListItemSecondaryAction from '@mui/material/ListItemSecondaryAction';
import Avatar from '@mui/material/Avatar';
import ListItemText from '@mui/material/ListItemText';
import Dialog from '@mui/material/Dialog';
import DialogTitle from '@mui/material/DialogTitle';
@@ -28,6 +27,7 @@ import { Trackers } from '@/features/tracker/services/Trackers.ts';
import { getErrorMessage } from '@/lib/HelperFunctions.ts';
import { useLocalStorage } from '@/base/hooks/useStorage.tsx';
import { TTrackerSearch } from '@/features/tracker/Tracker.types.ts';
import { AvatarSpinner } from '@/base/components/AvatarSpinner.tsx';
export const SettingsTrackerCard = ({ tracker }: { tracker: TTrackerSearch }) => {
const { t } = useTranslation();
@@ -86,11 +86,15 @@ export const SettingsTrackerCard = ({ tracker }: { tracker: TTrackerSearch }) =>
<>
<ListItemButton {...bindTrigger(popupState)} onClick={() => onClick(popupState.open)}>
<ListItemAvatar sx={{ paddingRight: '20px' }}>
<Avatar
<AvatarSpinner
alt={`${tracker.name}`}
src={requestManager.getValidImgUrlFor(tracker.icon)}
variant="rounded"
sx={{ width: 64, height: 64 }}
iconUrl={requestManager.getValidImgUrlFor(tracker.icon)}
slots={{
avatarProps: {
variant: 'rounded',
sx: { width: 64, height: 64 },
},
}}
/>
</ListItemAvatar>
<ListItemText primary={tracker.name} />

View File

@@ -8,7 +8,6 @@
import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
import Avatar from '@mui/material/Avatar';
import { useTranslation } from 'react-i18next';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
@@ -44,6 +43,7 @@ import { CheckboxInput } from '@/base/components/inputs/CheckboxInput.tsx';
import { TrackRecordType } from '@/lib/graphql/generated/graphql.ts';
import { getErrorMessage } from '@/lib/HelperFunctions.ts';
import { TTrackerBind, TTrackRecordBind } from '@/features/tracker/Tracker.types.ts';
import { AvatarSpinner } from '@/base/components/AvatarSpinner.tsx';
const TrackerActiveLink = ({ children, url }: { children: React.ReactNode; url: string }) => (
<Link href={url} rel="noreferrer" target="_blank" underline="none" color="inherit">
@@ -203,11 +203,15 @@ const TrackerActiveHeader = ({
}
>
<TrackerActiveLink url={trackRecord.remoteUrl}>
<Avatar
<AvatarSpinner
alt={`${tracker.name}`}
src={requestManager.getValidImgUrlFor(tracker.icon)}
variant="rounded"
sx={{ width: 64, height: 64 }}
iconUrl={requestManager.getValidImgUrlFor(tracker.icon)}
slots={{
avatarProps: {
variant: 'rounded',
sx: { width: 64, height: 64 },
},
}}
/>
</TrackerActiveLink>
</Badge>

View File

@@ -8,7 +8,6 @@
import Card from '@mui/material/Card';
import CardContent from '@mui/material/CardContent';
import Avatar from '@mui/material/Avatar';
import Button from '@mui/material/Button';
import { useTranslation } from 'react-i18next';
import Stack from '@mui/material/Stack';
@@ -16,6 +15,7 @@ import { requestManager } from '@/lib/requests/RequestManager.ts';
import { CARD_STYLING } from '@/features/tracker/Tracker.constants.ts';
import { TTrackerBase } from '@/features/tracker/Tracker.types.ts';
import { AvatarSpinner } from '@/base/components/AvatarSpinner.tsx';
export const TrackerUntrackedCard = ({
tracker,
@@ -35,11 +35,15 @@ export const TrackerUntrackedCard = ({
gap: 3,
}}
>
<Avatar
<AvatarSpinner
alt={`${tracker.name}`}
src={requestManager.getValidImgUrlFor(tracker.icon)}
variant="rounded"
sx={{ width: 64, height: 64 }}
iconUrl={requestManager.getValidImgUrlFor(tracker.icon)}
slots={{
avatarProps: {
variant: 'rounded',
sx: { width: 64, height: 64 },
},
}}
/>
<Button sx={{ flexGrow: '1' }} onClick={onClick}>
{t('tracking.action.button.add_tracking')}

View File

@@ -7,14 +7,13 @@
*/
import '@/polyfill.manual';
import '@fontsource/roboto';
import '@/lib/dayjs/Setup.ts';
import '@/index.css';
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { App } from '@/App';
import '@/index.css';
// roboto font
import '@fontsource/roboto';
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts';
import '@/lib/dayjs/Setup.ts';
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready.then((registration) => {

View File

@@ -12,6 +12,7 @@ import calendar from 'dayjs/plugin/calendar';
import relativeTime from 'dayjs/plugin/relativeTime';
import isToday from 'dayjs/plugin/isToday';
import isYesterday from 'dayjs/plugin/isYesterday';
import duration from 'dayjs/plugin/duration';
import { importDayJsLocale } from '@/lib/dayjs/LocaleImporter.ts';
// import localizedFormat from 'dayjs/plugin/localizedFormat';
// import updateLocale from 'dayjs/plugin/updateLocale';
@@ -22,6 +23,7 @@ dayjs.extend(calendar);
dayjs.extend(relativeTime);
dayjs.extend(isToday);
dayjs.extend(isYesterday);
dayjs.extend(duration);
// dayjs.extend(localizedFormat);
// dayjs.extend(updateLocale);

View File

@@ -60,6 +60,9 @@ export const SERVER_SETTINGS = gql`
authMode
authPassword
authUsername
jwtAudience
jwtTokenExpiry
jwtRefreshExpiry
# misc
debugLogsEnabled

View File

@@ -110,10 +110,11 @@ export type CheckBoxFilterFieldPolicy = {
default?: FieldPolicy<any> | FieldReadFunction<any>,
name?: FieldPolicy<any> | FieldReadFunction<any>
};
export type CheckBoxPreferenceKeySpecifier = ('currentValue' | 'default' | 'key' | 'summary' | 'title' | 'visible' | CheckBoxPreferenceKeySpecifier)[];
export type CheckBoxPreferenceKeySpecifier = ('currentValue' | 'default' | 'enabled' | 'key' | 'summary' | 'title' | 'visible' | CheckBoxPreferenceKeySpecifier)[];
export type CheckBoxPreferenceFieldPolicy = {
currentValue?: FieldPolicy<any> | FieldReadFunction<any>,
default?: FieldPolicy<any> | FieldReadFunction<any>,
enabled?: FieldPolicy<any> | FieldReadFunction<any>,
key?: FieldPolicy<any> | FieldReadFunction<any>,
summary?: FieldPolicy<any> | FieldReadFunction<any>,
title?: FieldPolicy<any> | FieldReadFunction<any>,
@@ -245,12 +246,13 @@ export type EdgeFieldPolicy = {
cursor?: FieldPolicy<any> | FieldReadFunction<any>,
node?: FieldPolicy<any> | FieldReadFunction<any>
};
export type EditTextPreferenceKeySpecifier = ('currentValue' | 'default' | 'dialogMessage' | 'dialogTitle' | 'key' | 'summary' | 'text' | 'title' | 'visible' | EditTextPreferenceKeySpecifier)[];
export type EditTextPreferenceKeySpecifier = ('currentValue' | 'default' | 'dialogMessage' | 'dialogTitle' | 'enabled' | 'key' | 'summary' | 'text' | 'title' | 'visible' | EditTextPreferenceKeySpecifier)[];
export type EditTextPreferenceFieldPolicy = {
currentValue?: FieldPolicy<any> | FieldReadFunction<any>,
default?: FieldPolicy<any> | FieldReadFunction<any>,
dialogMessage?: FieldPolicy<any> | FieldReadFunction<any>,
dialogTitle?: FieldPolicy<any> | FieldReadFunction<any>,
enabled?: FieldPolicy<any> | FieldReadFunction<any>,
key?: FieldPolicy<any> | FieldReadFunction<any>,
summary?: FieldPolicy<any> | FieldReadFunction<any>,
text?: FieldPolicy<any> | FieldReadFunction<any>,
@@ -295,11 +297,12 @@ export type ExtensionTypeFieldPolicy = {
versionCode?: FieldPolicy<any> | FieldReadFunction<any>,
versionName?: FieldPolicy<any> | FieldReadFunction<any>
};
export type FetchChapterPagesPayloadKeySpecifier = ('chapter' | 'clientMutationId' | 'pages' | FetchChapterPagesPayloadKeySpecifier)[];
export type FetchChapterPagesPayloadKeySpecifier = ('chapter' | 'clientMutationId' | 'pages' | 'syncConflict' | FetchChapterPagesPayloadKeySpecifier)[];
export type FetchChapterPagesPayloadFieldPolicy = {
chapter?: FieldPolicy<any> | FieldReadFunction<any>,
clientMutationId?: FieldPolicy<any> | FieldReadFunction<any>,
pages?: FieldPolicy<any> | FieldReadFunction<any>
pages?: FieldPolicy<any> | FieldReadFunction<any>,
syncConflict?: FieldPolicy<any> | FieldReadFunction<any>
};
export type FetchChaptersPayloadKeySpecifier = ('chapters' | 'clientMutationId' | FetchChaptersPayloadKeySpecifier)[];
export type FetchChaptersPayloadFieldPolicy = {
@@ -353,6 +356,19 @@ export type InstallExternalExtensionPayloadFieldPolicy = {
clientMutationId?: FieldPolicy<any> | FieldReadFunction<any>,
extension?: FieldPolicy<any> | FieldReadFunction<any>
};
export type KoSyncConnectPayloadKeySpecifier = ('clientMutationId' | 'message' | 'settings' | 'success' | 'username' | KoSyncConnectPayloadKeySpecifier)[];
export type KoSyncConnectPayloadFieldPolicy = {
clientMutationId?: FieldPolicy<any> | FieldReadFunction<any>,
message?: FieldPolicy<any> | FieldReadFunction<any>,
settings?: FieldPolicy<any> | FieldReadFunction<any>,
success?: FieldPolicy<any> | FieldReadFunction<any>,
username?: FieldPolicy<any> | FieldReadFunction<any>
};
export type KoSyncStatusPayloadKeySpecifier = ('isLoggedIn' | 'username' | KoSyncStatusPayloadKeySpecifier)[];
export type KoSyncStatusPayloadFieldPolicy = {
isLoggedIn?: FieldPolicy<any> | FieldReadFunction<any>,
username?: FieldPolicy<any> | FieldReadFunction<any>
};
export type LastUpdateTimestampPayloadKeySpecifier = ('timestamp' | LastUpdateTimestampPayloadKeySpecifier)[];
export type LastUpdateTimestampPayloadFieldPolicy = {
timestamp?: FieldPolicy<any> | FieldReadFunction<any>
@@ -363,10 +379,11 @@ export type LibraryUpdateStatusFieldPolicy = {
jobsInfo?: FieldPolicy<any> | FieldReadFunction<any>,
mangaUpdates?: FieldPolicy<any> | FieldReadFunction<any>
};
export type ListPreferenceKeySpecifier = ('currentValue' | 'default' | 'entries' | 'entryValues' | 'key' | 'summary' | 'title' | 'visible' | ListPreferenceKeySpecifier)[];
export type ListPreferenceKeySpecifier = ('currentValue' | 'default' | 'enabled' | 'entries' | 'entryValues' | 'key' | 'summary' | 'title' | 'visible' | ListPreferenceKeySpecifier)[];
export type ListPreferenceFieldPolicy = {
currentValue?: FieldPolicy<any> | FieldReadFunction<any>,
default?: FieldPolicy<any> | FieldReadFunction<any>,
enabled?: FieldPolicy<any> | FieldReadFunction<any>,
entries?: FieldPolicy<any> | FieldReadFunction<any>,
entryValues?: FieldPolicy<any> | FieldReadFunction<any>,
key?: FieldPolicy<any> | FieldReadFunction<any>,
@@ -374,6 +391,12 @@ export type ListPreferenceFieldPolicy = {
title?: FieldPolicy<any> | FieldReadFunction<any>,
visible?: FieldPolicy<any> | FieldReadFunction<any>
};
export type LoginPayloadKeySpecifier = ('accessToken' | 'clientMutationId' | 'refreshToken' | LoginPayloadKeySpecifier)[];
export type LoginPayloadFieldPolicy = {
accessToken?: FieldPolicy<any> | FieldReadFunction<any>,
clientMutationId?: FieldPolicy<any> | FieldReadFunction<any>,
refreshToken?: FieldPolicy<any> | FieldReadFunction<any>
};
export type LoginTrackerCredentialsPayloadKeySpecifier = ('clientMutationId' | 'isLoggedIn' | 'tracker' | LoginTrackerCredentialsPayloadKeySpecifier)[];
export type LoginTrackerCredentialsPayloadFieldPolicy = {
clientMutationId?: FieldPolicy<any> | FieldReadFunction<any>,
@@ -386,6 +409,12 @@ export type LoginTrackerOAuthPayloadFieldPolicy = {
isLoggedIn?: FieldPolicy<any> | FieldReadFunction<any>,
tracker?: FieldPolicy<any> | FieldReadFunction<any>
};
export type LogoutKoSyncAccountPayloadKeySpecifier = ('clientMutationId' | 'settings' | 'success' | LogoutKoSyncAccountPayloadKeySpecifier)[];
export type LogoutKoSyncAccountPayloadFieldPolicy = {
clientMutationId?: FieldPolicy<any> | FieldReadFunction<any>,
settings?: FieldPolicy<any> | FieldReadFunction<any>,
success?: FieldPolicy<any> | FieldReadFunction<any>
};
export type LogoutTrackerPayloadKeySpecifier = ('clientMutationId' | 'isLoggedIn' | 'tracker' | LogoutTrackerPayloadKeySpecifier)[];
export type LogoutTrackerPayloadFieldPolicy = {
clientMutationId?: FieldPolicy<any> | FieldReadFunction<any>,
@@ -464,12 +493,13 @@ export type MetaTypeFieldPolicy = {
key?: FieldPolicy<any> | FieldReadFunction<any>,
value?: FieldPolicy<any> | FieldReadFunction<any>
};
export type MultiSelectListPreferenceKeySpecifier = ('currentValue' | 'default' | 'dialogMessage' | 'dialogTitle' | 'entries' | 'entryValues' | 'key' | 'summary' | 'title' | 'visible' | MultiSelectListPreferenceKeySpecifier)[];
export type MultiSelectListPreferenceKeySpecifier = ('currentValue' | 'default' | 'dialogMessage' | 'dialogTitle' | 'enabled' | 'entries' | 'entryValues' | 'key' | 'summary' | 'title' | 'visible' | MultiSelectListPreferenceKeySpecifier)[];
export type MultiSelectListPreferenceFieldPolicy = {
currentValue?: FieldPolicy<any> | FieldReadFunction<any>,
default?: FieldPolicy<any> | FieldReadFunction<any>,
dialogMessage?: FieldPolicy<any> | FieldReadFunction<any>,
dialogTitle?: FieldPolicy<any> | FieldReadFunction<any>,
enabled?: FieldPolicy<any> | FieldReadFunction<any>,
entries?: FieldPolicy<any> | FieldReadFunction<any>,
entryValues?: FieldPolicy<any> | FieldReadFunction<any>,
key?: FieldPolicy<any> | FieldReadFunction<any>,
@@ -477,11 +507,12 @@ export type MultiSelectListPreferenceFieldPolicy = {
title?: FieldPolicy<any> | FieldReadFunction<any>,
visible?: FieldPolicy<any> | FieldReadFunction<any>
};
export type MutationKeySpecifier = ('bindTrack' | 'clearCachedImages' | 'clearDownloader' | 'createBackup' | 'createCategory' | 'deleteCategory' | 'deleteCategoryMeta' | 'deleteChapterMeta' | 'deleteDownloadedChapter' | 'deleteDownloadedChapters' | 'deleteGlobalMeta' | 'deleteMangaMeta' | 'deleteSourceMeta' | 'dequeueChapterDownload' | 'dequeueChapterDownloads' | 'enqueueChapterDownload' | 'enqueueChapterDownloads' | 'fetchChapterPages' | 'fetchChapters' | 'fetchExtensions' | 'fetchManga' | 'fetchSourceManga' | 'fetchTrack' | 'installExternalExtension' | 'loginTrackerCredentials' | 'loginTrackerOAuth' | 'logoutTracker' | 'reorderChapterDownload' | 'resetSettings' | 'resetWebUIUpdateStatus' | 'restoreBackup' | 'setCategoryMeta' | 'setChapterMeta' | 'setGlobalMeta' | 'setMangaMeta' | 'setSettings' | 'setSourceMeta' | 'startDownloader' | 'stopDownloader' | 'trackProgress' | 'unbindTrack' | 'updateCategories' | 'updateCategory' | 'updateCategoryManga' | 'updateCategoryOrder' | 'updateChapter' | 'updateChapters' | 'updateExtension' | 'updateExtensions' | 'updateLibrary' | 'updateLibraryManga' | 'updateManga' | 'updateMangaCategories' | 'updateMangas' | 'updateMangasCategories' | 'updateSourcePreference' | 'updateStop' | 'updateTrack' | 'updateWebUI' | MutationKeySpecifier)[];
export type MutationKeySpecifier = ('bindTrack' | 'clearCachedImages' | 'clearDownloader' | 'connectKoSyncAccount' | 'createBackup' | 'createCategory' | 'deleteCategory' | 'deleteCategoryMeta' | 'deleteChapterMeta' | 'deleteDownloadedChapter' | 'deleteDownloadedChapters' | 'deleteGlobalMeta' | 'deleteMangaMeta' | 'deleteSourceMeta' | 'dequeueChapterDownload' | 'dequeueChapterDownloads' | 'enqueueChapterDownload' | 'enqueueChapterDownloads' | 'fetchChapterPages' | 'fetchChapters' | 'fetchExtensions' | 'fetchManga' | 'fetchSourceManga' | 'fetchTrack' | 'installExternalExtension' | 'login' | 'loginTrackerCredentials' | 'loginTrackerOAuth' | 'logoutKoSyncAccount' | 'logoutTracker' | 'refreshToken' | 'reorderChapterDownload' | 'resetSettings' | 'resetWebUIUpdateStatus' | 'restoreBackup' | 'setCategoryMeta' | 'setChapterMeta' | 'setGlobalMeta' | 'setMangaMeta' | 'setSettings' | 'setSourceMeta' | 'startDownloader' | 'stopDownloader' | 'trackProgress' | 'unbindTrack' | 'updateCategories' | 'updateCategory' | 'updateCategoryManga' | 'updateCategoryOrder' | 'updateChapter' | 'updateChapters' | 'updateExtension' | 'updateExtensions' | 'updateLibrary' | 'updateLibraryManga' | 'updateManga' | 'updateMangaCategories' | 'updateMangas' | 'updateMangasCategories' | 'updateSourcePreference' | 'updateStop' | 'updateTrack' | 'updateWebUI' | MutationKeySpecifier)[];
export type MutationFieldPolicy = {
bindTrack?: FieldPolicy<any> | FieldReadFunction<any>,
clearCachedImages?: FieldPolicy<any> | FieldReadFunction<any>,
clearDownloader?: FieldPolicy<any> | FieldReadFunction<any>,
connectKoSyncAccount?: FieldPolicy<any> | FieldReadFunction<any>,
createBackup?: FieldPolicy<any> | FieldReadFunction<any>,
createCategory?: FieldPolicy<any> | FieldReadFunction<any>,
deleteCategory?: FieldPolicy<any> | FieldReadFunction<any>,
@@ -503,9 +534,12 @@ export type MutationFieldPolicy = {
fetchSourceManga?: FieldPolicy<any> | FieldReadFunction<any>,
fetchTrack?: FieldPolicy<any> | FieldReadFunction<any>,
installExternalExtension?: FieldPolicy<any> | FieldReadFunction<any>,
login?: FieldPolicy<any> | FieldReadFunction<any>,
loginTrackerCredentials?: FieldPolicy<any> | FieldReadFunction<any>,
loginTrackerOAuth?: FieldPolicy<any> | FieldReadFunction<any>,
logoutKoSyncAccount?: FieldPolicy<any> | FieldReadFunction<any>,
logoutTracker?: FieldPolicy<any> | FieldReadFunction<any>,
refreshToken?: FieldPolicy<any> | FieldReadFunction<any>,
reorderChapterDownload?: FieldPolicy<any> | FieldReadFunction<any>,
resetSettings?: FieldPolicy<any> | FieldReadFunction<any>,
resetWebUIUpdateStatus?: FieldPolicy<any> | FieldReadFunction<any>,
@@ -553,7 +587,7 @@ export type PageInfoFieldPolicy = {
hasPreviousPage?: FieldPolicy<any> | FieldReadFunction<any>,
startCursor?: FieldPolicy<any> | FieldReadFunction<any>
};
export type PartialSettingsTypeKeySpecifier = ('authMode' | 'authPassword' | 'authUsername' | 'autoDownloadAheadLimit' | 'autoDownloadIgnoreReUploads' | 'autoDownloadNewChapters' | 'autoDownloadNewChaptersLimit' | 'backupInterval' | 'backupPath' | 'backupTTL' | 'backupTime' | 'basicAuthEnabled' | 'basicAuthPassword' | 'basicAuthUsername' | 'debugLogsEnabled' | 'downloadAsCbz' | 'downloadConversions' | 'downloadsPath' | 'electronPath' | 'excludeCompleted' | 'excludeEntryWithUnreadChapters' | 'excludeNotStarted' | 'excludeUnreadChapters' | 'extensionRepos' | 'flareSolverrAsResponseFallback' | 'flareSolverrEnabled' | 'flareSolverrSessionName' | 'flareSolverrSessionTtl' | 'flareSolverrTimeout' | 'flareSolverrUrl' | 'globalUpdateInterval' | 'gqlDebugLogsEnabled' | 'initialOpenInBrowserEnabled' | 'ip' | 'localSourcePath' | 'maxLogFileSize' | 'maxLogFiles' | 'maxLogFolderSize' | 'maxSourcesInParallel' | 'opdsChapterSortOrder' | 'opdsEnablePageReadProgress' | 'opdsItemsPerPage' | 'opdsMarkAsReadOnDownload' | 'opdsShowOnlyDownloadedChapters' | 'opdsShowOnlyUnreadChapters' | 'opdsUseBinaryFileSizes' | 'port' | 'socksProxyEnabled' | 'socksProxyHost' | 'socksProxyPassword' | 'socksProxyPort' | 'socksProxyUsername' | 'socksProxyVersion' | 'systemTrayEnabled' | 'updateMangas' | 'webUIChannel' | 'webUIFlavor' | 'webUIInterface' | 'webUIUpdateCheckInterval' | PartialSettingsTypeKeySpecifier)[];
export type PartialSettingsTypeKeySpecifier = ('authMode' | 'authPassword' | 'authUsername' | 'autoDownloadAheadLimit' | 'autoDownloadIgnoreReUploads' | 'autoDownloadNewChapters' | 'autoDownloadNewChaptersLimit' | 'backupInterval' | 'backupPath' | 'backupTTL' | 'backupTime' | 'basicAuthEnabled' | 'basicAuthPassword' | 'basicAuthUsername' | 'debugLogsEnabled' | 'downloadAsCbz' | 'downloadConversions' | 'downloadsPath' | 'electronPath' | 'excludeCompleted' | 'excludeEntryWithUnreadChapters' | 'excludeNotStarted' | 'excludeUnreadChapters' | 'extensionRepos' | 'flareSolverrAsResponseFallback' | 'flareSolverrEnabled' | 'flareSolverrSessionName' | 'flareSolverrSessionTtl' | 'flareSolverrTimeout' | 'flareSolverrUrl' | 'globalUpdateInterval' | 'gqlDebugLogsEnabled' | 'initialOpenInBrowserEnabled' | 'ip' | 'jwtAudience' | 'jwtRefreshExpiry' | 'jwtTokenExpiry' | 'koreaderSyncChecksumMethod' | 'koreaderSyncDeviceId' | 'koreaderSyncPercentageTolerance' | 'koreaderSyncServerUrl' | 'koreaderSyncStrategy' | 'koreaderSyncUserkey' | 'koreaderSyncUsername' | 'localSourcePath' | 'maxLogFileSize' | 'maxLogFiles' | 'maxLogFolderSize' | 'maxSourcesInParallel' | 'opdsChapterSortOrder' | 'opdsEnablePageReadProgress' | 'opdsItemsPerPage' | 'opdsMarkAsReadOnDownload' | 'opdsShowOnlyDownloadedChapters' | 'opdsShowOnlyUnreadChapters' | 'opdsUseBinaryFileSizes' | 'port' | 'socksProxyEnabled' | 'socksProxyHost' | 'socksProxyPassword' | 'socksProxyPort' | 'socksProxyUsername' | 'socksProxyVersion' | 'systemTrayEnabled' | 'updateMangas' | 'webUIChannel' | 'webUIFlavor' | 'webUIInterface' | 'webUIUpdateCheckInterval' | PartialSettingsTypeKeySpecifier)[];
export type PartialSettingsTypeFieldPolicy = {
authMode?: FieldPolicy<any> | FieldReadFunction<any>,
authPassword?: FieldPolicy<any> | FieldReadFunction<any>,
@@ -589,6 +623,16 @@ export type PartialSettingsTypeFieldPolicy = {
gqlDebugLogsEnabled?: FieldPolicy<any> | FieldReadFunction<any>,
initialOpenInBrowserEnabled?: FieldPolicy<any> | FieldReadFunction<any>,
ip?: FieldPolicy<any> | FieldReadFunction<any>,
jwtAudience?: FieldPolicy<any> | FieldReadFunction<any>,
jwtRefreshExpiry?: FieldPolicy<any> | FieldReadFunction<any>,
jwtTokenExpiry?: FieldPolicy<any> | FieldReadFunction<any>,
koreaderSyncChecksumMethod?: FieldPolicy<any> | FieldReadFunction<any>,
koreaderSyncDeviceId?: FieldPolicy<any> | FieldReadFunction<any>,
koreaderSyncPercentageTolerance?: FieldPolicy<any> | FieldReadFunction<any>,
koreaderSyncServerUrl?: FieldPolicy<any> | FieldReadFunction<any>,
koreaderSyncStrategy?: FieldPolicy<any> | FieldReadFunction<any>,
koreaderSyncUserkey?: FieldPolicy<any> | FieldReadFunction<any>,
koreaderSyncUsername?: FieldPolicy<any> | FieldReadFunction<any>,
localSourcePath?: FieldPolicy<any> | FieldReadFunction<any>,
maxLogFileSize?: FieldPolicy<any> | FieldReadFunction<any>,
maxLogFiles?: FieldPolicy<any> | FieldReadFunction<any>,
@@ -615,7 +659,7 @@ export type PartialSettingsTypeFieldPolicy = {
webUIInterface?: FieldPolicy<any> | FieldReadFunction<any>,
webUIUpdateCheckInterval?: FieldPolicy<any> | FieldReadFunction<any>
};
export type QueryKeySpecifier = ('aboutServer' | 'aboutWebUI' | 'categories' | 'category' | 'chapter' | 'chapters' | 'checkForServerUpdates' | 'checkForWebUIUpdate' | 'downloadStatus' | 'extension' | 'extensions' | 'getWebUIUpdateStatus' | 'lastUpdateTimestamp' | 'libraryUpdateStatus' | 'manga' | 'mangas' | 'meta' | 'metas' | 'restoreStatus' | 'searchTracker' | 'settings' | 'source' | 'sources' | 'trackRecord' | 'trackRecords' | 'tracker' | 'trackers' | 'updateStatus' | 'validateBackup' | QueryKeySpecifier)[];
export type QueryKeySpecifier = ('aboutServer' | 'aboutWebUI' | 'categories' | 'category' | 'chapter' | 'chapters' | 'checkForServerUpdates' | 'checkForWebUIUpdate' | 'downloadStatus' | 'extension' | 'extensions' | 'getWebUIUpdateStatus' | 'koSyncStatus' | 'lastUpdateTimestamp' | 'libraryUpdateStatus' | 'manga' | 'mangas' | 'meta' | 'metas' | 'restoreStatus' | 'searchTracker' | 'settings' | 'source' | 'sources' | 'trackRecord' | 'trackRecords' | 'tracker' | 'trackers' | 'updateStatus' | 'validateBackup' | QueryKeySpecifier)[];
export type QueryFieldPolicy = {
aboutServer?: FieldPolicy<any> | FieldReadFunction<any>,
aboutWebUI?: FieldPolicy<any> | FieldReadFunction<any>,
@@ -629,6 +673,7 @@ export type QueryFieldPolicy = {
extension?: FieldPolicy<any> | FieldReadFunction<any>,
extensions?: FieldPolicy<any> | FieldReadFunction<any>,
getWebUIUpdateStatus?: FieldPolicy<Reference, Reference, Reference, FieldFunctionOptions<GetWebuiUpdateStatusQueryVariables>> | FieldReadFunction<Reference, Reference, FieldFunctionOptions<GetWebuiUpdateStatusQueryVariables>>,
koSyncStatus?: FieldPolicy<any> | FieldReadFunction<any>,
lastUpdateTimestamp?: FieldPolicy<any> | FieldReadFunction<any>,
libraryUpdateStatus?: FieldPolicy<Reference, Reference, Reference, FieldFunctionOptions<GetUpdateStatusQueryVariables>> | FieldReadFunction<Reference, Reference, FieldFunctionOptions<GetUpdateStatusQueryVariables>>,
manga?: FieldPolicy<Reference, Reference, Reference, FieldFunctionOptions<GetMangaScreenQueryVariables>> | FieldReadFunction<Reference, Reference, FieldFunctionOptions<GetMangaScreenQueryVariables>>,
@@ -647,6 +692,11 @@ export type QueryFieldPolicy = {
updateStatus?: FieldPolicy<any> | FieldReadFunction<any>,
validateBackup?: FieldPolicy<any> | FieldReadFunction<any>
};
export type RefreshTokenPayloadKeySpecifier = ('accessToken' | 'clientMutationId' | RefreshTokenPayloadKeySpecifier)[];
export type RefreshTokenPayloadFieldPolicy = {
accessToken?: FieldPolicy<any> | FieldReadFunction<any>,
clientMutationId?: FieldPolicy<any> | FieldReadFunction<any>
};
export type ReorderChapterDownloadPayloadKeySpecifier = ('clientMutationId' | 'downloadStatus' | ReorderChapterDownloadPayloadKeySpecifier)[];
export type ReorderChapterDownloadPayloadFieldPolicy = {
clientMutationId?: FieldPolicy<any> | FieldReadFunction<any>,
@@ -707,7 +757,7 @@ export type SetSourceMetaPayloadFieldPolicy = {
clientMutationId?: FieldPolicy<any> | FieldReadFunction<any>,
meta?: FieldPolicy<any> | FieldReadFunction<any>
};
export type SettingsKeySpecifier = ('authMode' | 'authPassword' | 'authUsername' | 'autoDownloadAheadLimit' | 'autoDownloadIgnoreReUploads' | 'autoDownloadNewChapters' | 'autoDownloadNewChaptersLimit' | 'backupInterval' | 'backupPath' | 'backupTTL' | 'backupTime' | 'basicAuthEnabled' | 'basicAuthPassword' | 'basicAuthUsername' | 'debugLogsEnabled' | 'downloadAsCbz' | 'downloadConversions' | 'downloadsPath' | 'electronPath' | 'excludeCompleted' | 'excludeEntryWithUnreadChapters' | 'excludeNotStarted' | 'excludeUnreadChapters' | 'extensionRepos' | 'flareSolverrAsResponseFallback' | 'flareSolverrEnabled' | 'flareSolverrSessionName' | 'flareSolverrSessionTtl' | 'flareSolverrTimeout' | 'flareSolverrUrl' | 'globalUpdateInterval' | 'gqlDebugLogsEnabled' | 'initialOpenInBrowserEnabled' | 'ip' | 'localSourcePath' | 'maxLogFileSize' | 'maxLogFiles' | 'maxLogFolderSize' | 'maxSourcesInParallel' | 'opdsChapterSortOrder' | 'opdsEnablePageReadProgress' | 'opdsItemsPerPage' | 'opdsMarkAsReadOnDownload' | 'opdsShowOnlyDownloadedChapters' | 'opdsShowOnlyUnreadChapters' | 'opdsUseBinaryFileSizes' | 'port' | 'socksProxyEnabled' | 'socksProxyHost' | 'socksProxyPassword' | 'socksProxyPort' | 'socksProxyUsername' | 'socksProxyVersion' | 'systemTrayEnabled' | 'updateMangas' | 'webUIChannel' | 'webUIFlavor' | 'webUIInterface' | 'webUIUpdateCheckInterval' | SettingsKeySpecifier)[];
export type SettingsKeySpecifier = ('authMode' | 'authPassword' | 'authUsername' | 'autoDownloadAheadLimit' | 'autoDownloadIgnoreReUploads' | 'autoDownloadNewChapters' | 'autoDownloadNewChaptersLimit' | 'backupInterval' | 'backupPath' | 'backupTTL' | 'backupTime' | 'basicAuthEnabled' | 'basicAuthPassword' | 'basicAuthUsername' | 'debugLogsEnabled' | 'downloadAsCbz' | 'downloadConversions' | 'downloadsPath' | 'electronPath' | 'excludeCompleted' | 'excludeEntryWithUnreadChapters' | 'excludeNotStarted' | 'excludeUnreadChapters' | 'extensionRepos' | 'flareSolverrAsResponseFallback' | 'flareSolverrEnabled' | 'flareSolverrSessionName' | 'flareSolverrSessionTtl' | 'flareSolverrTimeout' | 'flareSolverrUrl' | 'globalUpdateInterval' | 'gqlDebugLogsEnabled' | 'initialOpenInBrowserEnabled' | 'ip' | 'jwtAudience' | 'jwtRefreshExpiry' | 'jwtTokenExpiry' | 'koreaderSyncChecksumMethod' | 'koreaderSyncDeviceId' | 'koreaderSyncPercentageTolerance' | 'koreaderSyncServerUrl' | 'koreaderSyncStrategy' | 'koreaderSyncUserkey' | 'koreaderSyncUsername' | 'localSourcePath' | 'maxLogFileSize' | 'maxLogFiles' | 'maxLogFolderSize' | 'maxSourcesInParallel' | 'opdsChapterSortOrder' | 'opdsEnablePageReadProgress' | 'opdsItemsPerPage' | 'opdsMarkAsReadOnDownload' | 'opdsShowOnlyDownloadedChapters' | 'opdsShowOnlyUnreadChapters' | 'opdsUseBinaryFileSizes' | 'port' | 'socksProxyEnabled' | 'socksProxyHost' | 'socksProxyPassword' | 'socksProxyPort' | 'socksProxyUsername' | 'socksProxyVersion' | 'systemTrayEnabled' | 'updateMangas' | 'webUIChannel' | 'webUIFlavor' | 'webUIInterface' | 'webUIUpdateCheckInterval' | SettingsKeySpecifier)[];
export type SettingsFieldPolicy = {
authMode?: FieldPolicy<any> | FieldReadFunction<any>,
authPassword?: FieldPolicy<any> | FieldReadFunction<any>,
@@ -743,6 +793,16 @@ export type SettingsFieldPolicy = {
gqlDebugLogsEnabled?: FieldPolicy<any> | FieldReadFunction<any>,
initialOpenInBrowserEnabled?: FieldPolicy<any> | FieldReadFunction<any>,
ip?: FieldPolicy<any> | FieldReadFunction<any>,
jwtAudience?: FieldPolicy<any> | FieldReadFunction<any>,
jwtRefreshExpiry?: FieldPolicy<any> | FieldReadFunction<any>,
jwtTokenExpiry?: FieldPolicy<any> | FieldReadFunction<any>,
koreaderSyncChecksumMethod?: FieldPolicy<any> | FieldReadFunction<any>,
koreaderSyncDeviceId?: FieldPolicy<any> | FieldReadFunction<any>,
koreaderSyncPercentageTolerance?: FieldPolicy<any> | FieldReadFunction<any>,
koreaderSyncServerUrl?: FieldPolicy<any> | FieldReadFunction<any>,
koreaderSyncStrategy?: FieldPolicy<any> | FieldReadFunction<any>,
koreaderSyncUserkey?: FieldPolicy<any> | FieldReadFunction<any>,
koreaderSyncUsername?: FieldPolicy<any> | FieldReadFunction<any>,
localSourcePath?: FieldPolicy<any> | FieldReadFunction<any>,
maxLogFileSize?: FieldPolicy<any> | FieldReadFunction<any>,
maxLogFiles?: FieldPolicy<any> | FieldReadFunction<any>,
@@ -781,7 +841,7 @@ export type SettingsDownloadConversionTypeFieldPolicy = {
mimeType?: FieldPolicy<any> | FieldReadFunction<any>,
target?: FieldPolicy<any> | FieldReadFunction<any>
};
export type SettingsTypeKeySpecifier = ('authMode' | 'authPassword' | 'authUsername' | 'autoDownloadAheadLimit' | 'autoDownloadIgnoreReUploads' | 'autoDownloadNewChapters' | 'autoDownloadNewChaptersLimit' | 'backupInterval' | 'backupPath' | 'backupTTL' | 'backupTime' | 'basicAuthEnabled' | 'basicAuthPassword' | 'basicAuthUsername' | 'debugLogsEnabled' | 'downloadAsCbz' | 'downloadConversions' | 'downloadsPath' | 'electronPath' | 'excludeCompleted' | 'excludeEntryWithUnreadChapters' | 'excludeNotStarted' | 'excludeUnreadChapters' | 'extensionRepos' | 'flareSolverrAsResponseFallback' | 'flareSolverrEnabled' | 'flareSolverrSessionName' | 'flareSolverrSessionTtl' | 'flareSolverrTimeout' | 'flareSolverrUrl' | 'globalUpdateInterval' | 'gqlDebugLogsEnabled' | 'initialOpenInBrowserEnabled' | 'ip' | 'localSourcePath' | 'maxLogFileSize' | 'maxLogFiles' | 'maxLogFolderSize' | 'maxSourcesInParallel' | 'opdsChapterSortOrder' | 'opdsEnablePageReadProgress' | 'opdsItemsPerPage' | 'opdsMarkAsReadOnDownload' | 'opdsShowOnlyDownloadedChapters' | 'opdsShowOnlyUnreadChapters' | 'opdsUseBinaryFileSizes' | 'port' | 'socksProxyEnabled' | 'socksProxyHost' | 'socksProxyPassword' | 'socksProxyPort' | 'socksProxyUsername' | 'socksProxyVersion' | 'systemTrayEnabled' | 'updateMangas' | 'webUIChannel' | 'webUIFlavor' | 'webUIInterface' | 'webUIUpdateCheckInterval' | SettingsTypeKeySpecifier)[];
export type SettingsTypeKeySpecifier = ('authMode' | 'authPassword' | 'authUsername' | 'autoDownloadAheadLimit' | 'autoDownloadIgnoreReUploads' | 'autoDownloadNewChapters' | 'autoDownloadNewChaptersLimit' | 'backupInterval' | 'backupPath' | 'backupTTL' | 'backupTime' | 'basicAuthEnabled' | 'basicAuthPassword' | 'basicAuthUsername' | 'debugLogsEnabled' | 'downloadAsCbz' | 'downloadConversions' | 'downloadsPath' | 'electronPath' | 'excludeCompleted' | 'excludeEntryWithUnreadChapters' | 'excludeNotStarted' | 'excludeUnreadChapters' | 'extensionRepos' | 'flareSolverrAsResponseFallback' | 'flareSolverrEnabled' | 'flareSolverrSessionName' | 'flareSolverrSessionTtl' | 'flareSolverrTimeout' | 'flareSolverrUrl' | 'globalUpdateInterval' | 'gqlDebugLogsEnabled' | 'initialOpenInBrowserEnabled' | 'ip' | 'jwtAudience' | 'jwtRefreshExpiry' | 'jwtTokenExpiry' | 'koreaderSyncChecksumMethod' | 'koreaderSyncDeviceId' | 'koreaderSyncPercentageTolerance' | 'koreaderSyncServerUrl' | 'koreaderSyncStrategy' | 'koreaderSyncUserkey' | 'koreaderSyncUsername' | 'localSourcePath' | 'maxLogFileSize' | 'maxLogFiles' | 'maxLogFolderSize' | 'maxSourcesInParallel' | 'opdsChapterSortOrder' | 'opdsEnablePageReadProgress' | 'opdsItemsPerPage' | 'opdsMarkAsReadOnDownload' | 'opdsShowOnlyDownloadedChapters' | 'opdsShowOnlyUnreadChapters' | 'opdsUseBinaryFileSizes' | 'port' | 'socksProxyEnabled' | 'socksProxyHost' | 'socksProxyPassword' | 'socksProxyPort' | 'socksProxyUsername' | 'socksProxyVersion' | 'systemTrayEnabled' | 'updateMangas' | 'webUIChannel' | 'webUIFlavor' | 'webUIInterface' | 'webUIUpdateCheckInterval' | SettingsTypeKeySpecifier)[];
export type SettingsTypeFieldPolicy = {
authMode?: FieldPolicy<any> | FieldReadFunction<any>,
authPassword?: FieldPolicy<any> | FieldReadFunction<any>,
@@ -817,6 +877,16 @@ export type SettingsTypeFieldPolicy = {
gqlDebugLogsEnabled?: FieldPolicy<any> | FieldReadFunction<any>,
initialOpenInBrowserEnabled?: FieldPolicy<any> | FieldReadFunction<any>,
ip?: FieldPolicy<any> | FieldReadFunction<any>,
jwtAudience?: FieldPolicy<any> | FieldReadFunction<any>,
jwtRefreshExpiry?: FieldPolicy<any> | FieldReadFunction<any>,
jwtTokenExpiry?: FieldPolicy<any> | FieldReadFunction<any>,
koreaderSyncChecksumMethod?: FieldPolicy<any> | FieldReadFunction<any>,
koreaderSyncDeviceId?: FieldPolicy<any> | FieldReadFunction<any>,
koreaderSyncPercentageTolerance?: FieldPolicy<any> | FieldReadFunction<any>,
koreaderSyncServerUrl?: FieldPolicy<any> | FieldReadFunction<any>,
koreaderSyncStrategy?: FieldPolicy<any> | FieldReadFunction<any>,
koreaderSyncUserkey?: FieldPolicy<any> | FieldReadFunction<any>,
koreaderSyncUsername?: FieldPolicy<any> | FieldReadFunction<any>,
localSourcePath?: FieldPolicy<any> | FieldReadFunction<any>,
maxLogFileSize?: FieldPolicy<any> | FieldReadFunction<any>,
maxLogFiles?: FieldPolicy<any> | FieldReadFunction<any>,
@@ -873,8 +943,9 @@ export type SourceNodeListFieldPolicy = {
pageInfo?: FieldPolicy<any> | FieldReadFunction<any>,
totalCount?: FieldPolicy<any> | FieldReadFunction<any>
};
export type SourceTypeKeySpecifier = ('displayName' | 'extension' | 'filters' | 'iconUrl' | 'id' | 'isConfigurable' | 'isNsfw' | 'lang' | 'manga' | 'meta' | 'name' | 'preferences' | 'supportsLatest' | SourceTypeKeySpecifier)[];
export type SourceTypeKeySpecifier = ('baseUrl' | 'displayName' | 'extension' | 'filters' | 'iconUrl' | 'id' | 'isConfigurable' | 'isNsfw' | 'lang' | 'manga' | 'meta' | 'name' | 'preferences' | 'supportsLatest' | SourceTypeKeySpecifier)[];
export type SourceTypeFieldPolicy = {
baseUrl?: FieldPolicy<any> | FieldReadFunction<any>,
displayName?: FieldPolicy<any> | FieldReadFunction<any>,
extension?: FieldPolicy<any> | FieldReadFunction<any>,
filters?: FieldPolicy<any> | FieldReadFunction<any>,
@@ -907,15 +978,21 @@ export type SubscriptionFieldPolicy = {
updateStatusChanged?: FieldPolicy<any> | FieldReadFunction<any>,
webUIUpdateStatusChange?: FieldPolicy<any> | FieldReadFunction<any>
};
export type SwitchPreferenceKeySpecifier = ('currentValue' | 'default' | 'key' | 'summary' | 'title' | 'visible' | SwitchPreferenceKeySpecifier)[];
export type SwitchPreferenceKeySpecifier = ('currentValue' | 'default' | 'enabled' | 'key' | 'summary' | 'title' | 'visible' | SwitchPreferenceKeySpecifier)[];
export type SwitchPreferenceFieldPolicy = {
currentValue?: FieldPolicy<any> | FieldReadFunction<any>,
default?: FieldPolicy<any> | FieldReadFunction<any>,
enabled?: FieldPolicy<any> | FieldReadFunction<any>,
key?: FieldPolicy<any> | FieldReadFunction<any>,
summary?: FieldPolicy<any> | FieldReadFunction<any>,
title?: FieldPolicy<any> | FieldReadFunction<any>,
visible?: FieldPolicy<any> | FieldReadFunction<any>
};
export type SyncConflictInfoTypeKeySpecifier = ('deviceName' | 'remotePage' | SyncConflictInfoTypeKeySpecifier)[];
export type SyncConflictInfoTypeFieldPolicy = {
deviceName?: FieldPolicy<any> | FieldReadFunction<any>,
remotePage?: FieldPolicy<any> | FieldReadFunction<any>
};
export type TextFilterKeySpecifier = ('default' | 'name' | TextFilterKeySpecifier)[];
export type TextFilterFieldPolicy = {
default?: FieldPolicy<any> | FieldReadFunction<any>,
@@ -1396,6 +1473,14 @@ export type StrictTypedTypePolicies = {
keyFields?: false | InstallExternalExtensionPayloadKeySpecifier | (() => undefined | InstallExternalExtensionPayloadKeySpecifier),
fields?: InstallExternalExtensionPayloadFieldPolicy,
},
KoSyncConnectPayload?: Omit<TypePolicy, "fields" | "keyFields"> & {
keyFields?: false | KoSyncConnectPayloadKeySpecifier | (() => undefined | KoSyncConnectPayloadKeySpecifier),
fields?: KoSyncConnectPayloadFieldPolicy,
},
KoSyncStatusPayload?: Omit<TypePolicy, "fields" | "keyFields"> & {
keyFields?: false | KoSyncStatusPayloadKeySpecifier | (() => undefined | KoSyncStatusPayloadKeySpecifier),
fields?: KoSyncStatusPayloadFieldPolicy,
},
LastUpdateTimestampPayload?: Omit<TypePolicy, "fields" | "keyFields"> & {
keyFields?: false | LastUpdateTimestampPayloadKeySpecifier | (() => undefined | LastUpdateTimestampPayloadKeySpecifier),
fields?: LastUpdateTimestampPayloadFieldPolicy,
@@ -1408,6 +1493,10 @@ export type StrictTypedTypePolicies = {
keyFields?: false | ListPreferenceKeySpecifier | (() => undefined | ListPreferenceKeySpecifier),
fields?: ListPreferenceFieldPolicy,
},
LoginPayload?: Omit<TypePolicy, "fields" | "keyFields"> & {
keyFields?: false | LoginPayloadKeySpecifier | (() => undefined | LoginPayloadKeySpecifier),
fields?: LoginPayloadFieldPolicy,
},
LoginTrackerCredentialsPayload?: Omit<TypePolicy, "fields" | "keyFields"> & {
keyFields?: false | LoginTrackerCredentialsPayloadKeySpecifier | (() => undefined | LoginTrackerCredentialsPayloadKeySpecifier),
fields?: LoginTrackerCredentialsPayloadFieldPolicy,
@@ -1416,6 +1505,10 @@ export type StrictTypedTypePolicies = {
keyFields?: false | LoginTrackerOAuthPayloadKeySpecifier | (() => undefined | LoginTrackerOAuthPayloadKeySpecifier),
fields?: LoginTrackerOAuthPayloadFieldPolicy,
},
LogoutKoSyncAccountPayload?: Omit<TypePolicy, "fields" | "keyFields"> & {
keyFields?: false | LogoutKoSyncAccountPayloadKeySpecifier | (() => undefined | LogoutKoSyncAccountPayloadKeySpecifier),
fields?: LogoutKoSyncAccountPayloadFieldPolicy,
},
LogoutTrackerPayload?: Omit<TypePolicy, "fields" | "keyFields"> & {
keyFields?: false | LogoutTrackerPayloadKeySpecifier | (() => undefined | LogoutTrackerPayloadKeySpecifier),
fields?: LogoutTrackerPayloadFieldPolicy,
@@ -1472,6 +1565,10 @@ export type StrictTypedTypePolicies = {
keyFields?: false | QueryKeySpecifier | (() => undefined | QueryKeySpecifier),
fields?: QueryFieldPolicy,
},
RefreshTokenPayload?: Omit<TypePolicy, "fields" | "keyFields"> & {
keyFields?: false | RefreshTokenPayloadKeySpecifier | (() => undefined | RefreshTokenPayloadKeySpecifier),
fields?: RefreshTokenPayloadFieldPolicy,
},
ReorderChapterDownloadPayload?: Omit<TypePolicy, "fields" | "keyFields"> & {
keyFields?: false | ReorderChapterDownloadPayloadKeySpecifier | (() => undefined | ReorderChapterDownloadPayloadKeySpecifier),
fields?: ReorderChapterDownloadPayloadFieldPolicy,
@@ -1576,6 +1673,10 @@ export type StrictTypedTypePolicies = {
keyFields?: false | SwitchPreferenceKeySpecifier | (() => undefined | SwitchPreferenceKeySpecifier),
fields?: SwitchPreferenceFieldPolicy,
},
SyncConflictInfoType?: Omit<TypePolicy, "fields" | "keyFields"> & {
keyFields?: false | SyncConflictInfoTypeKeySpecifier | (() => undefined | SyncConflictInfoTypeKeySpecifier),
fields?: SyncConflictInfoTypeFieldPolicy,
},
TextFilter?: Omit<TypePolicy, "fields" | "keyFields"> & {
keyFields?: false | TextFilterKeySpecifier | (() => undefined | TextFilterKeySpecifier),
fields?: TextFilterFieldPolicy,

View File

@@ -13,6 +13,7 @@ export type Scalars = {
Int: { input: number; output: number; }
Float: { input: number; output: number; }
Cursor: { input: string; output: string; }
Duration: { input: any; output: any; }
LongString: { input: string; output: string; }
Upload: { input: any; output: any; }
};
@@ -38,7 +39,8 @@ export type AboutWebUi = {
export enum AuthMode {
BasicAuth = 'BASIC_AUTH',
None = 'NONE',
SimpleLogin = 'SIMPLE_LOGIN'
SimpleLogin = 'SIMPLE_LOGIN',
UiLogin = 'UI_LOGIN'
}
export enum BackupRestoreState {
@@ -287,6 +289,7 @@ export type CheckBoxPreference = {
__typename?: 'CheckBoxPreference';
currentValue?: Maybe<Scalars['Boolean']['output']>;
default: Scalars['Boolean']['output'];
enabled: Scalars['Boolean']['output'];
key: Scalars['String']['output'];
summary?: Maybe<Scalars['String']['output']>;
title?: Maybe<Scalars['String']['output']>;
@@ -325,6 +328,12 @@ export type ClearDownloaderPayload = {
downloadStatus: DownloadStatus;
};
export type ConnectKoSyncAccountInput = {
clientMutationId?: InputMaybe<Scalars['String']['input']>;
password: Scalars['String']['input'];
username: Scalars['String']['input'];
};
export type CreateBackupInput = {
clientMutationId?: InputMaybe<Scalars['String']['input']>;
includeCategories?: InputMaybe<Scalars['Boolean']['input']>;
@@ -576,6 +585,7 @@ export type EditTextPreference = {
default?: Maybe<Scalars['String']['output']>;
dialogMessage?: Maybe<Scalars['String']['output']>;
dialogTitle?: Maybe<Scalars['String']['output']>;
enabled: Scalars['Boolean']['output'];
key: Scalars['String']['output'];
summary?: Maybe<Scalars['String']['output']>;
text?: Maybe<Scalars['String']['output']>;
@@ -691,6 +701,7 @@ export type FetchChapterPagesPayload = {
chapter: ChapterType;
clientMutationId?: Maybe<Scalars['String']['output']>;
pages: Array<Scalars['String']['output']>;
syncConflict?: Maybe<SyncConflictInfoType>;
};
export type FetchChaptersInput = {
@@ -853,6 +864,34 @@ export type IntFilterInput = {
notIn?: InputMaybe<Array<Scalars['Int']['input']>>;
};
export type KoSyncConnectPayload = {
__typename?: 'KoSyncConnectPayload';
clientMutationId?: Maybe<Scalars['String']['output']>;
message?: Maybe<Scalars['String']['output']>;
settings: SettingsType;
success: Scalars['Boolean']['output'];
username?: Maybe<Scalars['String']['output']>;
};
export type KoSyncStatusPayload = {
__typename?: 'KoSyncStatusPayload';
isLoggedIn: Scalars['Boolean']['output'];
username?: Maybe<Scalars['String']['output']>;
};
export enum KoreaderSyncChecksumMethod {
Binary = 'BINARY',
Filename = 'FILENAME'
}
export enum KoreaderSyncStrategy {
Disabled = 'DISABLED',
Prompt = 'PROMPT',
Receive = 'RECEIVE',
Send = 'SEND',
Silent = 'SILENT'
}
export type LastUpdateTimestampPayload = {
__typename?: 'LastUpdateTimestampPayload';
timestamp: Scalars['LongString']['output'];
@@ -874,6 +913,7 @@ export type ListPreference = {
__typename?: 'ListPreference';
currentValue?: Maybe<Scalars['String']['output']>;
default?: Maybe<Scalars['String']['output']>;
enabled: Scalars['Boolean']['output'];
entries: Array<Scalars['String']['output']>;
entryValues: Array<Scalars['String']['output']>;
key: Scalars['String']['output'];
@@ -882,6 +922,19 @@ export type ListPreference = {
visible: Scalars['Boolean']['output'];
};
export type LoginInput = {
clientMutationId?: InputMaybe<Scalars['String']['input']>;
password: Scalars['String']['input'];
username: Scalars['String']['input'];
};
export type LoginPayload = {
__typename?: 'LoginPayload';
accessToken: Scalars['String']['output'];
clientMutationId?: Maybe<Scalars['String']['output']>;
refreshToken: Scalars['String']['output'];
};
export type LoginTrackerCredentialsInput = {
clientMutationId?: InputMaybe<Scalars['String']['input']>;
password: Scalars['String']['input'];
@@ -909,6 +962,17 @@ export type LoginTrackerOAuthPayload = {
tracker: TrackerType;
};
export type LogoutKoSyncAccountInput = {
clientMutationId?: InputMaybe<Scalars['String']['input']>;
};
export type LogoutKoSyncAccountPayload = {
__typename?: 'LogoutKoSyncAccountPayload';
clientMutationId?: Maybe<Scalars['String']['output']>;
settings: SettingsType;
success: Scalars['Boolean']['output'];
};
export type LogoutTrackerInput = {
clientMutationId?: InputMaybe<Scalars['String']['input']>;
trackerId: Scalars['Int']['input'];
@@ -1143,6 +1207,7 @@ export type MultiSelectListPreference = {
default?: Maybe<Array<Scalars['String']['output']>>;
dialogMessage?: Maybe<Scalars['String']['output']>;
dialogTitle?: Maybe<Scalars['String']['output']>;
enabled: Scalars['Boolean']['output'];
entries: Array<Scalars['String']['output']>;
entryValues: Array<Scalars['String']['output']>;
key: Scalars['String']['output'];
@@ -1156,6 +1221,7 @@ export type Mutation = {
bindTrack: BindTrackPayload;
clearCachedImages: ClearCachedImagesPayload;
clearDownloader?: Maybe<ClearDownloaderPayload>;
connectKoSyncAccount: KoSyncConnectPayload;
createBackup: CreateBackupPayload;
createCategory?: Maybe<CreateCategoryPayload>;
deleteCategory?: Maybe<DeleteCategoryPayload>;
@@ -1177,9 +1243,12 @@ export type Mutation = {
fetchSourceManga?: Maybe<FetchSourceMangaPayload>;
fetchTrack: FetchTrackPayload;
installExternalExtension?: Maybe<InstallExternalExtensionPayload>;
login: LoginPayload;
loginTrackerCredentials: LoginTrackerCredentialsPayload;
loginTrackerOAuth: LoginTrackerOAuthPayload;
logoutKoSyncAccount: LogoutKoSyncAccountPayload;
logoutTracker: LogoutTrackerPayload;
refreshToken: RefreshTokenPayload;
reorderChapterDownload?: Maybe<ReorderChapterDownloadPayload>;
resetSettings: ResetSettingsPayload;
resetWebUIUpdateStatus?: Maybe<WebUiUpdateStatus>;
@@ -1230,6 +1299,11 @@ export type MutationClearDownloaderArgs = {
};
export type MutationConnectKoSyncAccountArgs = {
input: ConnectKoSyncAccountInput;
};
export type MutationCreateBackupArgs = {
input?: InputMaybe<CreateBackupInput>;
};
@@ -1335,6 +1409,11 @@ export type MutationInstallExternalExtensionArgs = {
};
export type MutationLoginArgs = {
input: LoginInput;
};
export type MutationLoginTrackerCredentialsArgs = {
input: LoginTrackerCredentialsInput;
};
@@ -1345,11 +1424,21 @@ export type MutationLoginTrackerOAuthArgs = {
};
export type MutationLogoutKoSyncAccountArgs = {
input: LogoutKoSyncAccountInput;
};
export type MutationLogoutTrackerArgs = {
input: LogoutTrackerInput;
};
export type MutationRefreshTokenArgs = {
input: RefreshTokenInput;
};
export type MutationReorderChapterDownloadArgs = {
input: ReorderChapterDownloadInput;
};
@@ -1570,6 +1659,16 @@ export type PartialSettingsType = Settings & {
gqlDebugLogsEnabled?: Maybe<Scalars['Boolean']['output']>;
initialOpenInBrowserEnabled?: Maybe<Scalars['Boolean']['output']>;
ip?: Maybe<Scalars['String']['output']>;
jwtAudience?: Maybe<Scalars['String']['output']>;
jwtRefreshExpiry?: Maybe<Scalars['Duration']['output']>;
jwtTokenExpiry?: Maybe<Scalars['Duration']['output']>;
koreaderSyncChecksumMethod?: Maybe<KoreaderSyncChecksumMethod>;
koreaderSyncDeviceId?: Maybe<Scalars['String']['output']>;
koreaderSyncPercentageTolerance?: Maybe<Scalars['Float']['output']>;
koreaderSyncServerUrl?: Maybe<Scalars['String']['output']>;
koreaderSyncStrategy?: Maybe<KoreaderSyncStrategy>;
koreaderSyncUserkey?: Maybe<Scalars['String']['output']>;
koreaderSyncUsername?: Maybe<Scalars['String']['output']>;
localSourcePath?: Maybe<Scalars['String']['output']>;
maxLogFileSize?: Maybe<Scalars['String']['output']>;
maxLogFiles?: Maybe<Scalars['Int']['output']>;
@@ -1627,6 +1726,16 @@ export type PartialSettingsTypeInput = {
globalUpdateInterval?: InputMaybe<Scalars['Float']['input']>;
initialOpenInBrowserEnabled?: InputMaybe<Scalars['Boolean']['input']>;
ip?: InputMaybe<Scalars['String']['input']>;
jwtAudience?: InputMaybe<Scalars['String']['input']>;
jwtRefreshExpiry?: InputMaybe<Scalars['Duration']['input']>;
jwtTokenExpiry?: InputMaybe<Scalars['Duration']['input']>;
koreaderSyncChecksumMethod?: InputMaybe<KoreaderSyncChecksumMethod>;
koreaderSyncDeviceId?: InputMaybe<Scalars['String']['input']>;
koreaderSyncPercentageTolerance?: InputMaybe<Scalars['Float']['input']>;
koreaderSyncServerUrl?: InputMaybe<Scalars['String']['input']>;
koreaderSyncStrategy?: InputMaybe<KoreaderSyncStrategy>;
koreaderSyncUserkey?: InputMaybe<Scalars['String']['input']>;
koreaderSyncUsername?: InputMaybe<Scalars['String']['input']>;
localSourcePath?: InputMaybe<Scalars['String']['input']>;
maxLogFileSize?: InputMaybe<Scalars['String']['input']>;
maxLogFiles?: InputMaybe<Scalars['Int']['input']>;
@@ -1670,6 +1779,7 @@ export type Query = {
extension: ExtensionType;
extensions: ExtensionNodeList;
getWebUIUpdateStatus: WebUiUpdateStatus;
koSyncStatus: KoSyncStatusPayload;
lastUpdateTimestamp: LastUpdateTimestampPayload;
libraryUpdateStatus: LibraryUpdateStatus;
manga: MangaType;
@@ -1856,6 +1966,17 @@ export type QueryValidateBackupArgs = {
input: ValidateBackupInput;
};
export type RefreshTokenInput = {
clientMutationId?: InputMaybe<Scalars['String']['input']>;
refreshToken: Scalars['String']['input'];
};
export type RefreshTokenPayload = {
__typename?: 'RefreshTokenPayload';
accessToken: Scalars['String']['output'];
clientMutationId?: Maybe<Scalars['String']['output']>;
};
export type ReorderChapterDownloadInput = {
chapterId: Scalars['Int']['input'];
clientMutationId?: InputMaybe<Scalars['String']['input']>;
@@ -2018,6 +2139,16 @@ export type Settings = {
gqlDebugLogsEnabled?: Maybe<Scalars['Boolean']['output']>;
initialOpenInBrowserEnabled?: Maybe<Scalars['Boolean']['output']>;
ip?: Maybe<Scalars['String']['output']>;
jwtAudience?: Maybe<Scalars['String']['output']>;
jwtRefreshExpiry?: Maybe<Scalars['Duration']['output']>;
jwtTokenExpiry?: Maybe<Scalars['Duration']['output']>;
koreaderSyncChecksumMethod?: Maybe<KoreaderSyncChecksumMethod>;
koreaderSyncDeviceId?: Maybe<Scalars['String']['output']>;
koreaderSyncPercentageTolerance?: Maybe<Scalars['Float']['output']>;
koreaderSyncServerUrl?: Maybe<Scalars['String']['output']>;
koreaderSyncStrategy?: Maybe<KoreaderSyncStrategy>;
koreaderSyncUserkey?: Maybe<Scalars['String']['output']>;
koreaderSyncUsername?: Maybe<Scalars['String']['output']>;
localSourcePath?: Maybe<Scalars['String']['output']>;
maxLogFileSize?: Maybe<Scalars['String']['output']>;
maxLogFiles?: Maybe<Scalars['Int']['output']>;
@@ -2105,6 +2236,16 @@ export type SettingsType = Settings & {
gqlDebugLogsEnabled: Scalars['Boolean']['output'];
initialOpenInBrowserEnabled: Scalars['Boolean']['output'];
ip: Scalars['String']['output'];
jwtAudience: Scalars['String']['output'];
jwtRefreshExpiry: Scalars['Duration']['output'];
jwtTokenExpiry: Scalars['Duration']['output'];
koreaderSyncChecksumMethod: KoreaderSyncChecksumMethod;
koreaderSyncDeviceId: Scalars['String']['output'];
koreaderSyncPercentageTolerance: Scalars['Float']['output'];
koreaderSyncServerUrl: Scalars['String']['output'];
koreaderSyncStrategy: KoreaderSyncStrategy;
koreaderSyncUserkey: Scalars['String']['output'];
koreaderSyncUsername: Scalars['String']['output'];
localSourcePath: Scalars['String']['output'];
maxLogFileSize: Scalars['String']['output'];
maxLogFiles: Scalars['Int']['output'];
@@ -2226,6 +2367,7 @@ export type SourcePreferenceChangeInput = {
export type SourceType = {
__typename?: 'SourceType';
baseUrl?: Maybe<Scalars['String']['output']>;
displayName: Scalars['String']['output'];
extension: ExtensionType;
filters: Array<Filter>;
@@ -2362,12 +2504,19 @@ export type SwitchPreference = {
__typename?: 'SwitchPreference';
currentValue?: Maybe<Scalars['Boolean']['output']>;
default: Scalars['Boolean']['output'];
enabled: Scalars['Boolean']['output'];
key: Scalars['String']['output'];
summary?: Maybe<Scalars['String']['output']>;
title?: Maybe<Scalars['String']['output']>;
visible: Scalars['Boolean']['output'];
};
export type SyncConflictInfoType = {
__typename?: 'SyncConflictInfoType';
deviceName: Scalars['String']['output'];
remotePage: Scalars['Int']['output'];
};
export type TextFilter = {
__typename?: 'TextFilter';
default: Scalars['String']['output'];
@@ -2994,7 +3143,7 @@ export type MangaScreenFieldsFragment = { __typename?: 'MangaType', artist?: str
export type MangaLibraryDuplicateScreenFieldsFragment = { __typename?: 'MangaType', description?: string | null, id: number, title: string, thumbnailUrl?: string | null, thumbnailUrlLastFetched?: string | null, inLibrary: boolean, initialized: boolean, sourceId: string, unreadCount: number, downloadCount: number, bookmarkCount: number, hasDuplicateChapters: boolean, chapters: { __typename?: 'ChapterNodeList', totalCount: number } };
export type ServerSettingsFragment = { __typename?: 'SettingsType', ip: string, port: number, socksProxyEnabled: boolean, socksProxyVersion: number, socksProxyHost: string, socksProxyPort: string, socksProxyUsername: string, socksProxyPassword: string, webUIFlavor: WebUiFlavor, initialOpenInBrowserEnabled: boolean, webUIInterface: WebUiInterface, electronPath: string, webUIChannel: WebUiChannel, webUIUpdateCheckInterval: number, downloadAsCbz: boolean, downloadsPath: string, autoDownloadNewChapters: boolean, excludeEntryWithUnreadChapters: boolean, autoDownloadNewChaptersLimit: number, autoDownloadIgnoreReUploads: boolean, extensionRepos: Array<string>, maxSourcesInParallel: number, excludeUnreadChapters: boolean, excludeNotStarted: boolean, excludeCompleted: boolean, globalUpdateInterval: number, updateMangas: boolean, authMode: AuthMode, authPassword: string, authUsername: string, debugLogsEnabled: boolean, systemTrayEnabled: boolean, maxLogFileSize: string, maxLogFiles: number, maxLogFolderSize: string, backupPath: string, backupTime: string, backupInterval: number, backupTTL: number, localSourcePath: string, flareSolverrEnabled: boolean, flareSolverrUrl: string, flareSolverrTimeout: number, flareSolverrSessionName: string, flareSolverrSessionTtl: number, flareSolverrAsResponseFallback: boolean, opdsUseBinaryFileSizes: boolean, opdsItemsPerPage: number, opdsEnablePageReadProgress: boolean, opdsMarkAsReadOnDownload: boolean, opdsShowOnlyUnreadChapters: boolean, opdsShowOnlyDownloadedChapters: boolean, opdsChapterSortOrder: SortOrder, downloadConversions: Array<{ __typename?: 'SettingsDownloadConversionType', mimeType: string, target: string, compressionLevel?: number | null }> };
export type ServerSettingsFragment = { __typename?: 'SettingsType', ip: string, port: number, socksProxyEnabled: boolean, socksProxyVersion: number, socksProxyHost: string, socksProxyPort: string, socksProxyUsername: string, socksProxyPassword: string, webUIFlavor: WebUiFlavor, initialOpenInBrowserEnabled: boolean, webUIInterface: WebUiInterface, electronPath: string, webUIChannel: WebUiChannel, webUIUpdateCheckInterval: number, downloadAsCbz: boolean, downloadsPath: string, autoDownloadNewChapters: boolean, excludeEntryWithUnreadChapters: boolean, autoDownloadNewChaptersLimit: number, autoDownloadIgnoreReUploads: boolean, extensionRepos: Array<string>, maxSourcesInParallel: number, excludeUnreadChapters: boolean, excludeNotStarted: boolean, excludeCompleted: boolean, globalUpdateInterval: number, updateMangas: boolean, authMode: AuthMode, authPassword: string, authUsername: string, jwtAudience: string, jwtTokenExpiry: any, jwtRefreshExpiry: any, debugLogsEnabled: boolean, systemTrayEnabled: boolean, maxLogFileSize: string, maxLogFiles: number, maxLogFolderSize: string, backupPath: string, backupTime: string, backupInterval: number, backupTTL: number, localSourcePath: string, flareSolverrEnabled: boolean, flareSolverrUrl: string, flareSolverrTimeout: number, flareSolverrSessionName: string, flareSolverrSessionTtl: number, flareSolverrAsResponseFallback: boolean, opdsUseBinaryFileSizes: boolean, opdsItemsPerPage: number, opdsEnablePageReadProgress: boolean, opdsMarkAsReadOnDownload: boolean, opdsShowOnlyUnreadChapters: boolean, opdsShowOnlyDownloadedChapters: boolean, opdsChapterSortOrder: SortOrder, downloadConversions: Array<{ __typename?: 'SettingsDownloadConversionType', mimeType: string, target: string, compressionLevel?: number | null }> };
export type SourceMetaFieldsFragment = { __typename?: 'SourceMetaType', sourceId: string, key: string, value: string };
@@ -3356,14 +3505,14 @@ export type ResetServerSettingsMutationVariables = Exact<{
}>;
export type ResetServerSettingsMutation = { __typename?: 'Mutation', resetSettings: { __typename?: 'ResetSettingsPayload', settings: { __typename?: 'SettingsType', ip: string, port: number, socksProxyEnabled: boolean, socksProxyVersion: number, socksProxyHost: string, socksProxyPort: string, socksProxyUsername: string, socksProxyPassword: string, webUIFlavor: WebUiFlavor, initialOpenInBrowserEnabled: boolean, webUIInterface: WebUiInterface, electronPath: string, webUIChannel: WebUiChannel, webUIUpdateCheckInterval: number, downloadAsCbz: boolean, downloadsPath: string, autoDownloadNewChapters: boolean, excludeEntryWithUnreadChapters: boolean, autoDownloadNewChaptersLimit: number, autoDownloadIgnoreReUploads: boolean, extensionRepos: Array<string>, maxSourcesInParallel: number, excludeUnreadChapters: boolean, excludeNotStarted: boolean, excludeCompleted: boolean, globalUpdateInterval: number, updateMangas: boolean, authMode: AuthMode, authPassword: string, authUsername: string, debugLogsEnabled: boolean, systemTrayEnabled: boolean, maxLogFileSize: string, maxLogFiles: number, maxLogFolderSize: string, backupPath: string, backupTime: string, backupInterval: number, backupTTL: number, localSourcePath: string, flareSolverrEnabled: boolean, flareSolverrUrl: string, flareSolverrTimeout: number, flareSolverrSessionName: string, flareSolverrSessionTtl: number, flareSolverrAsResponseFallback: boolean, opdsUseBinaryFileSizes: boolean, opdsItemsPerPage: number, opdsEnablePageReadProgress: boolean, opdsMarkAsReadOnDownload: boolean, opdsShowOnlyUnreadChapters: boolean, opdsShowOnlyDownloadedChapters: boolean, opdsChapterSortOrder: SortOrder, downloadConversions: Array<{ __typename?: 'SettingsDownloadConversionType', mimeType: string, target: string, compressionLevel?: number | null }> } } };
export type ResetServerSettingsMutation = { __typename?: 'Mutation', resetSettings: { __typename?: 'ResetSettingsPayload', settings: { __typename?: 'SettingsType', ip: string, port: number, socksProxyEnabled: boolean, socksProxyVersion: number, socksProxyHost: string, socksProxyPort: string, socksProxyUsername: string, socksProxyPassword: string, webUIFlavor: WebUiFlavor, initialOpenInBrowserEnabled: boolean, webUIInterface: WebUiInterface, electronPath: string, webUIChannel: WebUiChannel, webUIUpdateCheckInterval: number, downloadAsCbz: boolean, downloadsPath: string, autoDownloadNewChapters: boolean, excludeEntryWithUnreadChapters: boolean, autoDownloadNewChaptersLimit: number, autoDownloadIgnoreReUploads: boolean, extensionRepos: Array<string>, maxSourcesInParallel: number, excludeUnreadChapters: boolean, excludeNotStarted: boolean, excludeCompleted: boolean, globalUpdateInterval: number, updateMangas: boolean, authMode: AuthMode, authPassword: string, authUsername: string, jwtAudience: string, jwtTokenExpiry: any, jwtRefreshExpiry: any, debugLogsEnabled: boolean, systemTrayEnabled: boolean, maxLogFileSize: string, maxLogFiles: number, maxLogFolderSize: string, backupPath: string, backupTime: string, backupInterval: number, backupTTL: number, localSourcePath: string, flareSolverrEnabled: boolean, flareSolverrUrl: string, flareSolverrTimeout: number, flareSolverrSessionName: string, flareSolverrSessionTtl: number, flareSolverrAsResponseFallback: boolean, opdsUseBinaryFileSizes: boolean, opdsItemsPerPage: number, opdsEnablePageReadProgress: boolean, opdsMarkAsReadOnDownload: boolean, opdsShowOnlyUnreadChapters: boolean, opdsShowOnlyDownloadedChapters: boolean, opdsChapterSortOrder: SortOrder, downloadConversions: Array<{ __typename?: 'SettingsDownloadConversionType', mimeType: string, target: string, compressionLevel?: number | null }> } } };
export type UpdateServerSettingsMutationVariables = Exact<{
input: SetSettingsInput;
}>;
export type UpdateServerSettingsMutation = { __typename?: 'Mutation', setSettings: { __typename?: 'SetSettingsPayload', settings: { __typename?: 'SettingsType', ip: string, port: number, socksProxyEnabled: boolean, socksProxyVersion: number, socksProxyHost: string, socksProxyPort: string, socksProxyUsername: string, socksProxyPassword: string, webUIFlavor: WebUiFlavor, initialOpenInBrowserEnabled: boolean, webUIInterface: WebUiInterface, electronPath: string, webUIChannel: WebUiChannel, webUIUpdateCheckInterval: number, downloadAsCbz: boolean, downloadsPath: string, autoDownloadNewChapters: boolean, excludeEntryWithUnreadChapters: boolean, autoDownloadNewChaptersLimit: number, autoDownloadIgnoreReUploads: boolean, extensionRepos: Array<string>, maxSourcesInParallel: number, excludeUnreadChapters: boolean, excludeNotStarted: boolean, excludeCompleted: boolean, globalUpdateInterval: number, updateMangas: boolean, authMode: AuthMode, authPassword: string, authUsername: string, debugLogsEnabled: boolean, systemTrayEnabled: boolean, maxLogFileSize: string, maxLogFiles: number, maxLogFolderSize: string, backupPath: string, backupTime: string, backupInterval: number, backupTTL: number, localSourcePath: string, flareSolverrEnabled: boolean, flareSolverrUrl: string, flareSolverrTimeout: number, flareSolverrSessionName: string, flareSolverrSessionTtl: number, flareSolverrAsResponseFallback: boolean, opdsUseBinaryFileSizes: boolean, opdsItemsPerPage: number, opdsEnablePageReadProgress: boolean, opdsMarkAsReadOnDownload: boolean, opdsShowOnlyUnreadChapters: boolean, opdsShowOnlyDownloadedChapters: boolean, opdsChapterSortOrder: SortOrder, downloadConversions: Array<{ __typename?: 'SettingsDownloadConversionType', mimeType: string, target: string, compressionLevel?: number | null }> } } };
export type UpdateServerSettingsMutation = { __typename?: 'Mutation', setSettings: { __typename?: 'SetSettingsPayload', settings: { __typename?: 'SettingsType', ip: string, port: number, socksProxyEnabled: boolean, socksProxyVersion: number, socksProxyHost: string, socksProxyPort: string, socksProxyUsername: string, socksProxyPassword: string, webUIFlavor: WebUiFlavor, initialOpenInBrowserEnabled: boolean, webUIInterface: WebUiInterface, electronPath: string, webUIChannel: WebUiChannel, webUIUpdateCheckInterval: number, downloadAsCbz: boolean, downloadsPath: string, autoDownloadNewChapters: boolean, excludeEntryWithUnreadChapters: boolean, autoDownloadNewChaptersLimit: number, autoDownloadIgnoreReUploads: boolean, extensionRepos: Array<string>, maxSourcesInParallel: number, excludeUnreadChapters: boolean, excludeNotStarted: boolean, excludeCompleted: boolean, globalUpdateInterval: number, updateMangas: boolean, authMode: AuthMode, authPassword: string, authUsername: string, jwtAudience: string, jwtTokenExpiry: any, jwtRefreshExpiry: any, debugLogsEnabled: boolean, systemTrayEnabled: boolean, maxLogFileSize: string, maxLogFiles: number, maxLogFolderSize: string, backupPath: string, backupTime: string, backupInterval: number, backupTTL: number, localSourcePath: string, flareSolverrEnabled: boolean, flareSolverrUrl: string, flareSolverrTimeout: number, flareSolverrSessionName: string, flareSolverrSessionTtl: number, flareSolverrAsResponseFallback: boolean, opdsUseBinaryFileSizes: boolean, opdsItemsPerPage: number, opdsEnablePageReadProgress: boolean, opdsMarkAsReadOnDownload: boolean, opdsShowOnlyUnreadChapters: boolean, opdsShowOnlyDownloadedChapters: boolean, opdsChapterSortOrder: SortOrder, downloadConversions: Array<{ __typename?: 'SettingsDownloadConversionType', mimeType: string, target: string, compressionLevel?: number | null }> } } };
export type GetSourceMangasFetchMutationVariables = Exact<{
input: FetchSourceMangaInput;
@@ -3456,6 +3605,21 @@ export type StopUpdaterMutationVariables = Exact<{
export type StopUpdaterMutation = { __typename?: 'Mutation', updateStop: { __typename?: 'UpdateStopPayload', clientMutationId?: string | null } };
export type UserLoginMutationVariables = Exact<{
password: Scalars['String']['input'];
username: Scalars['String']['input'];
}>;
export type UserLoginMutation = { __typename?: 'Mutation', login: { __typename?: 'LoginPayload', accessToken: string, refreshToken: string } };
export type UserRefreshMutationVariables = Exact<{
refreshToken: Scalars['String']['input'];
}>;
export type UserRefreshMutation = { __typename?: 'Mutation', refreshToken: { __typename?: 'RefreshTokenPayload', accessToken: string } };
export type ValidateBackupQueryVariables = Exact<{
backup: Scalars['Upload']['input'];
}>;
@@ -3747,7 +3911,7 @@ export type GetWebuiUpdateStatusQuery = { __typename?: 'Query', getWebUIUpdateSt
export type GetServerSettingsQueryVariables = Exact<{ [key: string]: never; }>;
export type GetServerSettingsQuery = { __typename?: 'Query', settings: { __typename?: 'SettingsType', ip: string, port: number, socksProxyEnabled: boolean, socksProxyVersion: number, socksProxyHost: string, socksProxyPort: string, socksProxyUsername: string, socksProxyPassword: string, webUIFlavor: WebUiFlavor, initialOpenInBrowserEnabled: boolean, webUIInterface: WebUiInterface, electronPath: string, webUIChannel: WebUiChannel, webUIUpdateCheckInterval: number, downloadAsCbz: boolean, downloadsPath: string, autoDownloadNewChapters: boolean, excludeEntryWithUnreadChapters: boolean, autoDownloadNewChaptersLimit: number, autoDownloadIgnoreReUploads: boolean, extensionRepos: Array<string>, maxSourcesInParallel: number, excludeUnreadChapters: boolean, excludeNotStarted: boolean, excludeCompleted: boolean, globalUpdateInterval: number, updateMangas: boolean, authMode: AuthMode, authPassword: string, authUsername: string, debugLogsEnabled: boolean, systemTrayEnabled: boolean, maxLogFileSize: string, maxLogFiles: number, maxLogFolderSize: string, backupPath: string, backupTime: string, backupInterval: number, backupTTL: number, localSourcePath: string, flareSolverrEnabled: boolean, flareSolverrUrl: string, flareSolverrTimeout: number, flareSolverrSessionName: string, flareSolverrSessionTtl: number, flareSolverrAsResponseFallback: boolean, opdsUseBinaryFileSizes: boolean, opdsItemsPerPage: number, opdsEnablePageReadProgress: boolean, opdsMarkAsReadOnDownload: boolean, opdsShowOnlyUnreadChapters: boolean, opdsShowOnlyDownloadedChapters: boolean, opdsChapterSortOrder: SortOrder, downloadConversions: Array<{ __typename?: 'SettingsDownloadConversionType', mimeType: string, target: string, compressionLevel?: number | null }> } };
export type GetServerSettingsQuery = { __typename?: 'Query', settings: { __typename?: 'SettingsType', ip: string, port: number, socksProxyEnabled: boolean, socksProxyVersion: number, socksProxyHost: string, socksProxyPort: string, socksProxyUsername: string, socksProxyPassword: string, webUIFlavor: WebUiFlavor, initialOpenInBrowserEnabled: boolean, webUIInterface: WebUiInterface, electronPath: string, webUIChannel: WebUiChannel, webUIUpdateCheckInterval: number, downloadAsCbz: boolean, downloadsPath: string, autoDownloadNewChapters: boolean, excludeEntryWithUnreadChapters: boolean, autoDownloadNewChaptersLimit: number, autoDownloadIgnoreReUploads: boolean, extensionRepos: Array<string>, maxSourcesInParallel: number, excludeUnreadChapters: boolean, excludeNotStarted: boolean, excludeCompleted: boolean, globalUpdateInterval: number, updateMangas: boolean, authMode: AuthMode, authPassword: string, authUsername: string, jwtAudience: string, jwtTokenExpiry: any, jwtRefreshExpiry: any, debugLogsEnabled: boolean, systemTrayEnabled: boolean, maxLogFileSize: string, maxLogFiles: number, maxLogFolderSize: string, backupPath: string, backupTime: string, backupInterval: number, backupTTL: number, localSourcePath: string, flareSolverrEnabled: boolean, flareSolverrUrl: string, flareSolverrTimeout: number, flareSolverrSessionName: string, flareSolverrSessionTtl: number, flareSolverrAsResponseFallback: boolean, opdsUseBinaryFileSizes: boolean, opdsItemsPerPage: number, opdsEnablePageReadProgress: boolean, opdsMarkAsReadOnDownload: boolean, opdsShowOnlyUnreadChapters: boolean, opdsShowOnlyDownloadedChapters: boolean, opdsChapterSortOrder: SortOrder, downloadConversions: Array<{ __typename?: 'SettingsDownloadConversionType', mimeType: string, target: string, compressionLevel?: number | null }> } };
export type GetSourceBrowseQueryVariables = Exact<{
id: Scalars['LongString']['input'];

View File

@@ -0,0 +1,26 @@
/*
* 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 USER_LOGIN = gql`
mutation USER_LOGIN($password: String!, $username: String!) {
login(input: { password: $password, username: $username }) {
accessToken
refreshToken
}
}
`;
export const USER_REFRESH = gql`
mutation USER_REFRESH($refreshToken: String!) {
refreshToken(input: { refreshToken: $refreshToken }) {
accessToken
}
}
`;

View File

@@ -212,6 +212,10 @@ import {
GetExtensionQuery,
GetExtensionQueryVariables,
DownloaderState,
UserLoginMutation,
UserLoginMutationVariables,
UserRefreshMutation,
UserRefreshMutationVariables,
} from '@/lib/graphql/generated/graphql.ts';
import { GET_GLOBAL_METADATAS } from '@/lib/graphql/queries/GlobalMetadataQuery.ts';
import { DELETE_GLOBAL_METADATA, SET_GLOBAL_METADATA } from '@/lib/graphql/mutations/GlobalMetadataMutation.ts';
@@ -327,6 +331,8 @@ import { CHAPTER_META_FIELDS } from '@/lib/graphql/fragments/ChapterFragments.ts
import { MetadataMigrationSettings } from '@/features/migration/Migration.types.ts';
import { MangaIdInfo } from '@/features/manga/Manga.types.ts';
import { updateMetadataList } from '@/features/metadata/services/MetadataApolloCacheHandler.ts';
import { USER_LOGIN, USER_REFRESH } from '@/lib/graphql/mutations/UserMutation.ts';
import { AuthManager } from '@/features/authentication/AuthManager.ts';
enum GQLMethod {
QUERY = 'QUERY',
@@ -434,9 +440,9 @@ export const SPECIAL_ED_SOURCES = {
export class RequestManager {
public static readonly API_VERSION = '/api/v1/';
public readonly graphQLClient = new GraphQLClient();
public readonly graphQLClient = new GraphQLClient(this.refreshUser.bind(this));
private readonly restClient: RestClient = new RestClient();
private readonly restClient: RestClient = new RestClient(this.refreshUser.bind(this));
private readonly cache = new CustomCache();
@@ -452,6 +458,8 @@ export class RequestManager {
}
public reset(): void {
AuthManager.setAuthRequired(null);
AuthManager.removeTokens();
this.graphQLClient.client.resetStore();
this.graphQLClient.terminateSubscriptions();
this.cache.clear();
@@ -1020,7 +1028,7 @@ export class RequestManager {
} = {},
): ImageRequest {
const finalOptions = {
useFetchApi: false,
useFetchApi: AuthManager.isAuthRequired(),
shouldDecode: false,
disableCors: false,
...Object.fromEntries(Object.entries(options).filter(([, value]) => value !== undefined)),
@@ -3231,6 +3239,24 @@ export class RequestManager {
options,
);
}
public useLoginUser(
options?: MutationHookOptions<UserLoginMutation, UserLoginMutationVariables>,
): AbortableApolloUseMutationResponse<UserLoginMutation, UserLoginMutationVariables> {
return this.doRequest(GQLMethod.USE_MUTATION, USER_LOGIN, undefined, options);
}
public refreshUser(
refreshToken: string,
options?: MutationOptions<UserRefreshMutation, UserRefreshMutationVariables>,
): AbortableApolloMutationResponse<UserRefreshMutation> {
return this.doRequest<UserRefreshMutation, UserRefreshMutationVariables>(
GQLMethod.MUTATION,
USER_REFRESH,
{ refreshToken: refreshToken ?? undefined },
options,
);
}
}
export const requestManager = new RequestManager();

View File

@@ -7,12 +7,60 @@
*/
import { AppStorage } from '@/lib/storage/AppStorage.ts';
import { UserRefreshMutation } from '@/lib/graphql/generated/graphql.ts';
import { AuthManager } from '@/features/authentication/AuthManager.ts';
import { AbortableApolloMutationResponse } from '@/lib/requests/RequestManager.ts';
export abstract class BaseClient<Client, ClientConfig, Fetcher> {
protected abstract client: Client;
public abstract readonly fetcher: Fetcher;
private static activeTokenRefreshPromise: Promise<UserRefreshMutation | null | undefined> | null = null;
protected static async refreshAccessToken(
refreshFn: (refreshToken: string) => AbortableApolloMutationResponse<UserRefreshMutation>,
): Promise<UserRefreshMutation | null | undefined> {
const refreshToken = AuthManager.getRefreshToken();
if (!AuthManager.isAuthRequired()) {
AuthManager.setAuthRequired(true);
}
if (!refreshToken) {
throw new Error('No refresh token found');
}
if (this.activeTokenRefreshPromise) {
return this.activeTokenRefreshPromise;
}
const refreshRequest = refreshFn(refreshToken).response;
this.activeTokenRefreshPromise = refreshRequest.then((result) => result.data);
try {
const result = await refreshRequest;
const { data } = result;
if (!data) {
throw new Error('No refreshed access token returned');
}
AuthManager.setAccessToken(data.refreshToken.accessToken);
return data;
} catch (e) {
AuthManager.removeTokens();
throw e;
} finally {
this.activeTokenRefreshPromise = null;
}
}
protected constructor(
protected handleRefreshToken: (refreshToken: string) => AbortableApolloMutationResponse<UserRefreshMutation>,
) {}
public getBaseUrl(): string {
const { hostname, port, protocol } = window.location;

View File

@@ -6,6 +6,8 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { onError } from '@apollo/client/link/error';
import { setContext } from '@apollo/client/link/context';
import {
ApolloClient,
ApolloClientOptions,
@@ -14,6 +16,7 @@ import {
NormalizedCacheObject,
split,
from,
fromPromise,
} from '@apollo/client';
import createUploadLink from 'apollo-upload-client/createUploadLink.mjs';
import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
@@ -23,6 +26,9 @@ import { TypePolicies } from '@apollo/client/cache';
import { removeTypenameFromVariables } from '@apollo/client/link/remove-typename';
import { BaseClient } from '@/lib/requests/client/BaseClient.ts';
import { StrictTypedTypePolicies } from '@/lib/graphql/generated/apollo-helpers.ts';
import { AuthManager } from '@/features/authentication/AuthManager.ts';
import { UserRefreshMutation } from '@/lib/graphql/generated/graphql.ts';
import { AbortableApolloMutationResponse } from '@/lib/requests/RequestManager.ts';
/* eslint-disable no-underscore-dangle */
const typePolicies: StrictTypedTypePolicies = {
@@ -187,8 +193,8 @@ export class GraphQLClient extends BaseClient<
private wsClient!: Client;
constructor() {
super();
constructor(handleRefreshToken: (refreshToken: string) => AbortableApolloMutationResponse<UserRefreshMutation>) {
super(handleRefreshToken);
this.createClient();
}
@@ -201,8 +207,44 @@ export class GraphQLClient extends BaseClient<
this.wsClient.terminate();
}
private createErrorLink() {
return onError(({ graphQLErrors, operation, forward }) => {
if (!graphQLErrors) {
return undefined;
}
const isAuthError = graphQLErrors.some((graphQLError) =>
graphQLError.message.includes('suwayomi.tachidesk.server.user.UnauthorizedException'),
);
if (!isAuthError) {
return undefined;
}
return fromPromise(BaseClient.refreshAccessToken(this.handleRefreshToken))
.filter(Boolean)
.flatMap(() => forward(operation));
});
}
private createAuthLink() {
return setContext((_, { headers }) => {
const isAuthRequired = AuthManager.isAuthRequired();
const accessToken = AuthManager.getAccessToken();
return {
headers: {
credentials: 'include',
...headers,
Authorization: isAuthRequired && accessToken ? `Bearer ${accessToken}` : '',
},
};
});
}
private createUploadLink() {
return createUploadLink({ uri: () => this.getBaseUrl(), credentials: 'include' });
return createUploadLink({
uri: () => this.getBaseUrl(),
});
}
private createWSLink() {
@@ -218,8 +260,13 @@ export class GraphQLClient extends BaseClient<
return definition.kind === 'OperationDefinition' && definition.operation === 'subscription';
},
this.createWSLink(),
from([
this.createErrorLink(),
this.createAuthLink(),
removeTypenameLink,
// apollo-upload-client dependency is outdated (see 134e47763faae9e62db4d4e3a8387a74e32e5568) and thus types are not matching, but they are still correct
from([removeTypenameLink, this.createUploadLink() as unknown as ApolloLink]),
this.createUploadLink() as unknown as ApolloLink,
]),
);
}
@@ -230,6 +277,14 @@ export class GraphQLClient extends BaseClient<
url: () => this.getBaseUrl().replace(/http(|s)/g, 'ws'),
keepAlive: heartbeatInterval,
retryAttempts: 10,
connectionParams: () => {
const isAuthRequired = AuthManager.isAuthRequired();
const accessToken = AuthManager.getAccessToken();
return {
Authorization: isAuthRequired && accessToken ? accessToken : undefined,
};
},
});
let lastHeartbeat: number = 0;

View File

@@ -7,6 +7,9 @@
*/
import { BaseClient } from '@/lib/requests/client/BaseClient.ts';
import { AuthManager } from '@/features/authentication/AuthManager.ts';
import { UserRefreshMutation } from '@/lib/graphql/generated/graphql.ts';
import { AbortableApolloMutationResponse } from '@/lib/requests/RequestManager.ts';
export enum HttpMethod {
GET = 'GET',
@@ -48,17 +51,28 @@ export class RestClient
} = {},
): Promise<Response> => {
const updatedUrl = url.startsWith('http') ? url : `${this.getBaseUrl()}${url}`;
const accessToken = AuthManager.getAccessToken();
let result: Response;
switch (httpMethod) {
case HttpMethod.GET:
result = await this.client(updatedUrl, { ...this.config, ...config, method: httpMethod });
result = await this.client(updatedUrl, {
...this.config,
...config,
method: httpMethod,
headers: {
...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
...this.config.headers,
...config?.headers,
},
});
break;
case HttpMethod.POST:
case HttpMethod.PATCH:
case HttpMethod.DELETE:
result = await this.client(updatedUrl, {
...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
...this.config,
...config,
method: httpMethod,
@@ -69,6 +83,11 @@ export class RestClient
throw new Error(`Unexpected HttpMethod "${httpMethod}"`);
}
if (result.status === 401) {
await BaseClient.refreshAccessToken(this.handleRefreshToken);
return this.fetcher(url, { data, httpMethod, config, checkResponseIsJson });
}
if (result.status !== 200) {
throw new Error(`status ${result.status}: ${result.statusText}`);
}
@@ -80,8 +99,8 @@ export class RestClient
return result;
};
constructor() {
super();
constructor(handleRefreshToken: (refreshToken: string) => AbortableApolloMutationResponse<UserRefreshMutation>) {
super(handleRefreshToken);
this.createClient();
}

View File

@@ -62,6 +62,7 @@ const fixTypingOfQueryTypePolicies = format(
\textension?: FieldPolicy<any> | FieldReadFunction<any>,
\textensions?: FieldPolicy<any> | FieldReadFunction<any>,
\tgetWebUIUpdateStatus?: FieldPolicy<any> | FieldReadFunction<any>,
\tkoSyncStatus?: FieldPolicy<any> | FieldReadFunction<any>,
\tlastUpdateTimestamp?: FieldPolicy<any> | FieldReadFunction<any>,
\tlibraryUpdateStatus?: FieldPolicy<any> | FieldReadFunction<any>,
\tmanga?: FieldPolicy<any> | FieldReadFunction<any>,
@@ -93,6 +94,7 @@ const fixTypingOfQueryTypePolicies = format(
\textension?: FieldPolicy<any> | FieldReadFunction<any>,
\textensions?: FieldPolicy<any> | FieldReadFunction<any>,
\tgetWebUIUpdateStatus?: FieldPolicy<Reference, Reference, Reference, FieldFunctionOptions<GetWebuiUpdateStatusQueryVariables>> | FieldReadFunction<Reference, Reference, FieldFunctionOptions<GetWebuiUpdateStatusQueryVariables>>,
\tkoSyncStatus?: FieldPolicy<any> | FieldReadFunction<any>,
\tlastUpdateTimestamp?: FieldPolicy<any> | FieldReadFunction<any>,
\tlibraryUpdateStatus?: FieldPolicy<Reference, Reference, Reference, FieldFunctionOptions<GetUpdateStatusQueryVariables>> | FieldReadFunction<Reference, Reference, FieldFunctionOptions<GetUpdateStatusQueryVariables>>,
\tmanga?: FieldPolicy<Reference, Reference, Reference, FieldFunctionOptions<GetMangaScreenQueryVariables>> | FieldReadFunction<Reference, Reference, FieldFunctionOptions<GetMangaScreenQueryVariables>>,

View File

@@ -2877,6 +2877,11 @@
resolved "https://registry.yarnpkg.com/@types/extract-files/-/extract-files-13.0.1.tgz#3ec057a3fa25f778245a76a17271d23b71ee31d7"
integrity sha512-/fRbzc2lAd7jDJSSnxWiUyXWjdUZZ4HbISLJzVgt1AvrdOa7U49YRPcvuCUywkmURZ7uwJOheDjx19itbQ5KvA==
"@types/js-cookie@^3.0.6":
version "3.0.6"
resolved "https://registry.yarnpkg.com/@types/js-cookie/-/js-cookie-3.0.6.tgz#a04ca19e877687bd449f5ad37d33b104b71fdf95"
integrity sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==
"@types/js-yaml@^4.0.0":
version "4.0.9"
resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-4.0.9.tgz#cd82382c4f902fed9691a2ed79ec68c5898af4c2"
@@ -6513,6 +6518,11 @@ jpeg-js@^0.4.4:
resolved "https://registry.yarnpkg.com/jpeg-js/-/jpeg-js-0.4.4.tgz#a9f1c6f1f9f0fa80cdb3484ed9635054d28936aa"
integrity sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==
js-cookie@^3.0.5:
version "3.0.5"
resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-3.0.5.tgz#0b7e2fd0c01552c58ba86e0841f94dc2557dcdbc"
integrity sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"