Update KOReader Sync settings (#1030)

* feat(settings): refactor and implement KOReader Sync settings page

Refactors the KOReader Sync settings section into its own dedicated page and implements the full connection and configuration flow.

The previous implementation in the main server settings was outdated and incomplete. This change aligns the frontend with the latest backend logic and provides a complete user experience.

Key changes include:
- Separated the UI into two states: a login form for initial connection and a configuration panel for existing connections.
- Implemented `connectKoSyncAccount` and `logoutKoSyncAccount` mutations to handle authentication with the KOReader Sync server.
- Updated the `SERVER_SETTINGS` GraphQL fragment to use the new `koreaderSyncStrategyForward` and `koreaderSyncStrategyBackward` fields, removing the deprecated `koreaderSyncStrategy`.
- Moved all KOReader Sync settings from the generic Server Settings page to a dedicated route at `/settings/koreader-sync`.
- Restructured and improved i18n keys for better clarity and consistency.

Ran `yarn gql:codegen` to regenerate GraphQL types and helpers.

* Extract credentials login into component

* Update to server changes and cleanup

---------

Co-authored-by: schroda <50052685+schroda@users.noreply.github.com>
This commit is contained in:
Zeedif
2025-12-06 12:59:39 -06:00
committed by GitHub
parent 36248e17f0
commit 84cfa6bea5
16 changed files with 829 additions and 284 deletions

View File

@@ -13,7 +13,14 @@ import { GlobalUpdateSkipEntriesSettings, MetadataServerSettings } from '@/featu
import { GridLayout, TranslationKey } from '@/base/Base.types.ts';
import { getDefaultLanguages } from '@/base/utils/Languages.ts';
import { SelectSettingValue, SelectSettingValueDisplayInfo } from '@/base/components/settings/SelectSetting.tsx';
import { AuthMode, WebUiChannel, WebUiFlavor, WebUiInterface } from '@/lib/graphql/generated/graphql.ts';
import {
AuthMode,
KoreaderSyncChecksumMethod,
KoreaderSyncConflictStrategy,
WebUiChannel,
WebUiFlavor,
WebUiInterface,
} from '@/lib/graphql/generated/graphql.ts';
import { ThemeMode } from '@/features/theme/AppTheme.types.ts';
export const MANGA_GRID_WIDTH = {
@@ -207,3 +214,45 @@ export const KOREADER_SYNC_PERCENTAGE_TOLERANCE = {
max: 1,
step: 0.05,
};
export const KOREADER_SYNC_CONFLICT_STRATEGIES = Object.values(KoreaderSyncConflictStrategy);
export const KOREADER_SYNC_CONFLICT_STRATEGY_TO_TRANSLATION_KEYS: Record<
KoreaderSyncConflictStrategy,
SelectSettingValueDisplayInfo
> = {
[KoreaderSyncConflictStrategy.Disabled]: {
text: 'settings.server.koreader.sync.strategy.option.disabled',
},
[KoreaderSyncConflictStrategy.KeepLocal]: {
text: 'settings.server.koreader.sync.strategy.option.keep_local',
},
[KoreaderSyncConflictStrategy.KeepRemote]: {
text: 'settings.server.koreader.sync.strategy.option.keep_remote',
},
[KoreaderSyncConflictStrategy.Prompt]: {
text: 'settings.server.koreader.sync.strategy.option.prompt',
},
};
export const KOREADER_SYNC_CONFLICT_STRATEGY_SELECT_VALUES: SelectSettingValue<KoreaderSyncConflictStrategy>[] =
KOREADER_SYNC_CONFLICT_STRATEGIES.map((strategy) => [
strategy,
KOREADER_SYNC_CONFLICT_STRATEGY_TO_TRANSLATION_KEYS[strategy],
]);
export const KOREADER_SYNC_CHECKSUM_METHODES = Object.values(KoreaderSyncChecksumMethod);
export const KOREADER_SYNC_CHECKSUM_METHOD_TO_TRANSLATION_KEYS: Record<
KoreaderSyncChecksumMethod,
SelectSettingValueDisplayInfo
> = {
[KoreaderSyncChecksumMethod.Binary]: {
text: 'settings.server.koreader.sync.check_sum_method.binary',
},
[KoreaderSyncChecksumMethod.Filename]: {
text: 'settings.server.koreader.sync.check_sum_method.filename',
},
};
export const KOREADER_SYNC_CHECKSUM_METHOD_SELECT_VALUES: SelectSettingValue<KoreaderSyncChecksumMethod>[] =
KOREADER_SYNC_CHECKSUM_METHODES.map((method) => [
method,
KOREADER_SYNC_CHECKSUM_METHOD_TO_TRANSLATION_KEYS[method],
]);

View File

@@ -0,0 +1,203 @@
/*
* 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 List from '@mui/material/List';
import ListSubheader from '@mui/material/ListSubheader';
import ListItemText from '@mui/material/ListItemText';
import ListItemButton from '@mui/material/ListItemButton';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { SelectSetting } from '@/base/components/settings/SelectSetting.tsx';
import { NumberSetting } from '@/base/components/settings/NumberSetting.tsx';
import { makeToast } from '@/base/utils/Toast.ts';
import { getErrorMessage, noOp } from '@/lib/HelperFunctions.ts';
import {
KoreaderSyncChecksumMethod,
KoreaderSyncConflictStrategy,
KoSyncStatusPayload,
} from '@/lib/graphql/generated/graphql.ts';
import {
KOREADER_SYNC_CHECKSUM_METHOD_SELECT_VALUES,
KOREADER_SYNC_CONFLICT_STRATEGY_SELECT_VALUES,
KOREADER_SYNC_PERCENTAGE_TOLERANCE,
} from '@/features/settings/Settings.constants.ts';
import { CredentialsLogin } from '@/base/components/modals/LoginDialog.tsx';
import { ServerSettings as ServerSettingsType, ServerSettings } from '@/features/settings/Settings.types.ts';
import { assertIsDefined } from '@/base/Asserts.ts';
export const KoreaderSyncSettings = ({
settings: {
koreaderSyncPercentageTolerance,
koreaderSyncStrategyBackward,
koreaderSyncStrategyForward,
koreaderSyncChecksumMethod,
},
serverAddress: currentServerAddress,
username: currentUsername,
isLoggedIn,
updateSetting,
}: {
settings: ServerSettings;
updateSetting: <Setting extends keyof ServerSettingsType>(
setting: Setting,
value: ServerSettingsType[Setting],
onCompletion?: (success: boolean) => void,
) => Promise<void>;
} & Omit<KoSyncStatusPayload, '__typename'>) => {
const { t } = useTranslation();
const handleLogout = async () => {
const { data } = await requestManager.koSyncLogout().response;
if (data?.logoutKoSyncAccount.status.isLoggedIn) {
throw new Error(
t('tracking.action.logout.label.failure', { name: t('settings.server.koreader.sync.title') }),
);
}
};
const handleLogin = async (serverAddress: string, username: string, password: string) => {
const { data } = await requestManager.koSyncLogin(serverAddress, username, password).response;
if (!data?.connectKoSyncAccount.status.isLoggedIn) {
throw new Error(data?.connectKoSyncAccount.message ?? t('global.label.unknown'));
}
};
const handleLoginLogout = async (
initialServerAddress: string = 'https://sync.koreader.rocks/',
initialUsername?: string,
initialPassword?: string,
) => {
const controlled = CredentialsLogin.showControlled(
{
title: t(
isLoggedIn
? 'settings.server.koreader.sync.connection.disconnect'
: 'settings.server.koreader.sync.connection.connect',
{
name: t('settings.server.koreader.sync.title'),
},
),
description: isLoggedIn
? t('settings.server.koreader.sync.connection.connected', {
username: initialUsername,
serverAddress: initialServerAddress,
})
: undefined,
isLoading: false,
isLoggedIn,
withServerAddress: true,
username: initialUsername,
password: initialPassword,
serverAddress: initialServerAddress,
loginLogout: async (username, password, serverAddress) => {
controlled.update({ isLoading: true, loginLogout: noOp });
if (isLoggedIn) {
try {
await handleLogout();
controlled.submit();
} catch (e) {
makeToast(
t('settings.server.koreader.sync.connection.message.disconnect_error'),
'error',
getErrorMessage(e),
);
}
return;
}
try {
assertIsDefined(serverAddress);
await handleLogin(serverAddress, username, password);
controlled.submit();
} catch (e) {
makeToast(
t('settings.server.koreader.sync.connection.message.connect_error'),
'error',
getErrorMessage(e),
);
const RETRY_KEY = '__retry__';
const retry = await Promise.race([controlled.promise, Promise.resolve(RETRY_KEY)]);
if (retry === RETRY_KEY) {
handleLoginLogout(serverAddress, username, password);
}
}
},
},
{ id: 'koreader-sync-login-logout' },
);
};
return (
<List
subheader={
<ListSubheader component="div" id="koreader-sync-settings">
{t('settings.server.koreader.sync.title')}
</ListSubheader>
}
>
<ListItemButton
onClick={() => handleLoginLogout(currentServerAddress ?? undefined, currentUsername ?? undefined)}
>
<ListItemText
primary={t('settings.server.koreader.sync.connection.status')}
secondary={
isLoggedIn
? t('settings.server.koreader.sync.connection.connected', {
username: currentUsername,
serverAddress: currentServerAddress,
})
: t('settings.server.koreader.sync.connection.disconnected')
}
/>
</ListItemButton>
<SelectSetting<KoreaderSyncConflictStrategy>
settingName={t('settings.server.koreader.sync.strategy.forward_title')}
value={koreaderSyncStrategyForward}
values={KOREADER_SYNC_CONFLICT_STRATEGY_SELECT_VALUES}
handleChange={(value) => updateSetting('koreaderSyncStrategyForward', value)}
/>
<SelectSetting<KoreaderSyncConflictStrategy>
settingName={t('settings.server.koreader.sync.strategy.backward_title')}
value={koreaderSyncStrategyBackward}
values={KOREADER_SYNC_CONFLICT_STRATEGY_SELECT_VALUES}
handleChange={(value) => updateSetting('koreaderSyncStrategyBackward', value)}
/>
<SelectSetting<KoreaderSyncChecksumMethod>
settingName={t('settings.server.koreader.sync.check_sum_method.title')}
value={koreaderSyncChecksumMethod}
values={KOREADER_SYNC_CHECKSUM_METHOD_SELECT_VALUES}
handleChange={(value) => updateSetting('koreaderSyncChecksumMethod', value)}
/>
<NumberSetting
settingTitle={t('settings.server.koreader.sync.tolerance.title')}
dialogDescription={t('settings.server.koreader.sync.tolerance.description')}
settingValue={koreaderSyncPercentageTolerance.toString()}
value={koreaderSyncPercentageTolerance}
defaultValue={KOREADER_SYNC_PERCENTAGE_TOLERANCE.default}
minValue={KOREADER_SYNC_PERCENTAGE_TOLERANCE.min}
maxValue={KOREADER_SYNC_PERCENTAGE_TOLERANCE.max}
stepSize={KOREADER_SYNC_PERCENTAGE_TOLERANCE.step}
valueUnit=""
handleUpdate={(value) => updateSetting('koreaderSyncPercentageTolerance', value)}
/>
</List>
);
};

View File

@@ -31,22 +31,16 @@ import { makeToast } from '@/base/utils/Toast.ts';
import { MetadataUpdateSettings } from '@/features/app-updates/AppUpdateChecker.types.ts';
import { getErrorMessage } from '@/lib/HelperFunctions.ts';
import { useAppTitle } from '@/features/navigation-bar/hooks/useAppTitle.ts';
import {
AuthMode,
DatabaseType,
KoreaderSyncChecksumMethod,
KoreaderSyncLegacyStrategy,
SortOrder,
} from '@/lib/graphql/generated/graphql';
import { AuthMode, DatabaseType, SortOrder } from '@/lib/graphql/generated/graphql';
import {
AUTH_MODES_SELECT_VALUES,
JWT_ACCESS_TOKEN_EXPIRY,
JWT_REFRESH_TOKEN_EXPIRY,
KOREADER_SYNC_PERCENTAGE_TOLERANCE,
} from '@/features/settings/Settings.constants.ts';
import { ServerAddressSetting } from '@/features/settings/components/ServerAddressSetting.tsx';
import { AuthManager } from '@/features/authentication/AuthManager.ts';
import { ServerSettings as ServerSettingsType } from '@/features/settings/Settings.types.ts';
import { KoreaderSyncSettings } from '@/features/settings/components/koreaderSync/KoreaderSyncSettings.tsx';
const getLogFilesCleanupDisplayValue = (ttl: number): string => {
if (ttl === 0) {
@@ -80,6 +74,8 @@ export const ServerSettings = () => {
});
const [mutateSettings] = requestManager.useUpdateServerSettings();
const koSyncStatus = requestManager.useKoSyncStatus();
const updateSetting = async <Setting extends keyof ServerSettingsType>(
setting: Setting,
value: ServerSettingsType[Setting],
@@ -120,7 +116,7 @@ export const ServerSettings = () => {
[serverInformAvailableUpdate],
);
const loading = areMetadataServerSettingsLoading || areServerSettingsLoading;
const loading = areMetadataServerSettingsLoading || areServerSettingsLoading || koSyncStatus.loading;
if (loading) {
return (
<>
@@ -130,7 +126,7 @@ export const ServerSettings = () => {
);
}
const error = metadataServerSettingsError ?? serverSettingsError;
const error = metadataServerSettingsError ?? serverSettingsError ?? koSyncStatus.error;
if (error) {
return (
<>
@@ -150,6 +146,12 @@ export const ServerSettings = () => {
defaultPromiseErrorHandler('ServerSettings::refetchServerSettings'),
);
}
if (koSyncStatus.error) {
koSyncStatus
.refetch()
.catch(defaultPromiseErrorHandler('ServerSettings::koSyncStatus.refetch'));
}
}}
/>
</>
@@ -157,6 +159,7 @@ export const ServerSettings = () => {
}
const serverSettings = data!.settings;
const koreaderSyncStatus = koSyncStatus.data!.koSyncStatus;
const authModeDisabled = !serverSettings.authUsername?.trim() || !serverSettings.authPassword?.trim();
const isH2Database = serverSettings.databaseType === DatabaseType.H2;
@@ -467,87 +470,13 @@ export const ServerSettings = () => {
handleChange={(value) => updateSetting('opdsChapterSortOrder', value)}
/>
</List>
<List
subheader={
<ListSubheader component="div" id="server-settings-koreader-sync">
{t('settings.server.koreader.sync.title')}
</ListSubheader>
}
>
<TextSetting
settingName={t('settings.server.koreader.sync.server_address')}
handleChange={(url) => updateSetting('koreaderSyncServerUrl', url)}
value={serverSettings.koreaderSyncServerUrl}
placeholder="http://localhost:17200"
/>
<TextSetting
settingName={t('settings.server.koreader.sync.username')}
value={serverSettings.koreaderSyncUsername}
handleChange={(username) => updateSetting('koreaderSyncUsername', username)}
/>
<TextSetting
settingName={t('settings.server.koreader.sync.user_key')}
value={serverSettings.koreaderSyncUserkey}
handleChange={(userkey) => updateSetting('koreaderSyncUserkey', userkey)}
isPassword
/>
<TextSetting
settingName={t('settings.server.koreader.sync.device_id')}
value={serverSettings.koreaderSyncDeviceId}
handleChange={(deviceId) => updateSetting('koreaderSyncDeviceId', deviceId)}
/>
<SelectSetting<KoreaderSyncChecksumMethod>
settingName={t('settings.server.koreader.sync.check_sum_method.title')}
value={serverSettings.koreaderSyncChecksumMethod}
values={[
[
KoreaderSyncChecksumMethod.Binary,
{ text: t('settings.server.koreader.sync.check_sum_method.binary') },
],
[
KoreaderSyncChecksumMethod.Filename,
{ text: t('settings.server.koreader.sync.check_sum_method.filename') },
],
]}
handleChange={(value) => updateSetting('koreaderSyncChecksumMethod', value)}
/>
<SelectSetting<KoreaderSyncLegacyStrategy>
settingName={t('settings.server.koreader.sync.strategy.title')}
value={serverSettings.koreaderSyncStrategy}
values={[
[
KoreaderSyncLegacyStrategy.Disabled,
{ text: t('settings.server.koreader.sync.strategy.disabled') },
],
[
KoreaderSyncLegacyStrategy.Prompt,
{ text: t('settings.server.koreader.sync.strategy.prompt') },
],
[
KoreaderSyncLegacyStrategy.Silent,
{ text: t('settings.server.koreader.sync.strategy.silent') },
],
[KoreaderSyncLegacyStrategy.Send, { text: t('settings.server.koreader.sync.strategy.send') }],
[
KoreaderSyncLegacyStrategy.Receive,
{ text: t('settings.server.koreader.sync.strategy.receive') },
],
]}
handleChange={(value) => updateSetting('koreaderSyncStrategy', value)}
/>
<NumberSetting
settingTitle={t('settings.server.koreader.sync.tolerance.title')}
dialogDescription={t('settings.server.koreader.sync.tolerance.description')}
settingValue={serverSettings.koreaderSyncPercentageTolerance.toString()}
value={serverSettings.koreaderSyncPercentageTolerance}
defaultValue={KOREADER_SYNC_PERCENTAGE_TOLERANCE.default}
minValue={KOREADER_SYNC_PERCENTAGE_TOLERANCE.min}
maxValue={KOREADER_SYNC_PERCENTAGE_TOLERANCE.max}
stepSize={KOREADER_SYNC_PERCENTAGE_TOLERANCE.step}
valueUnit=""
handleUpdate={(tolerance) => updateSetting('koreaderSyncPercentageTolerance', tolerance)}
/>
</List>
<KoreaderSyncSettings
settings={serverSettings}
serverAddress={koreaderSyncStatus.serverAddress}
username={koreaderSyncStatus.username}
isLoggedIn={koreaderSyncStatus.isLoggedIn}
updateSetting={updateSetting}
/>
<List
subheader={
<ListSubheader component="div" id="server-settings-database">

View File

@@ -7,48 +7,33 @@
*/
import { useTranslation } from 'react-i18next';
import { useState } from 'react';
import PopupState, { bindDialog, bindTrigger } from 'material-ui-popup-state';
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 ListItemText from '@mui/material/ListItemText';
import Dialog from '@mui/material/Dialog';
import DialogTitle from '@mui/material/DialogTitle';
import DialogContent from '@mui/material/DialogContent';
import TextField from '@mui/material/TextField';
import DialogActions from '@mui/material/DialogActions';
import Button from '@mui/material/Button';
import { PasswordTextField } from '@/base/components/inputs/PasswordTextField.tsx';
import { makeToast } from '@/base/utils/Toast.ts';
import { requestManager } from '@/lib/requests/RequestManager.ts';
import { Trackers } from '@/features/tracker/services/Trackers.ts';
import { getErrorMessage } from '@/lib/HelperFunctions.ts';
import { getErrorMessage, noOp } from '@/lib/HelperFunctions.ts';
import { TTrackerSearch } from '@/features/tracker/Tracker.types.ts';
import { AvatarSpinner } from '@/base/components/AvatarSpinner.tsx';
import { CredentialsLogin } from '@/base/components/modals/LoginDialog.tsx';
export const SettingsTrackerCard = ({ tracker }: { tracker: TTrackerSearch }) => {
const { t } = useTranslation();
const [loginTrackerCredentials, { loading: isCredentialLoginInProgress }] =
requestManager.useLoginToTrackerCredentials();
const [logoutFromTracker] = requestManager.useLogoutFromTracker();
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const isOAuthLogin = !tracker.isLoggedIn && !!tracker.authUrl;
const handleLogout = async () => {
try {
await logoutFromTracker({ variables: { trackerId: tracker.id } });
await requestManager.logoutFromTracker(tracker.id).response;
} catch (e) {
makeToast(t('tracking.action.logout.label.failure', { name: tracker.name }), 'error', getErrorMessage(e));
}
};
const handleLogin = async () => {
const handleLogin = async (username: string, password: string) => {
if (isOAuthLogin) {
const state = {
redirectUrl: `${window.location.origin}/tracker/login/oauth`,
@@ -62,99 +47,107 @@ export const SettingsTrackerCard = ({ tracker }: { tracker: TTrackerSearch }) =>
}
try {
await loginTrackerCredentials({ variables: { input: { trackerId: tracker.id, username, password } } });
await requestManager.loginTrackerCredentials(tracker.id, username, password).response;
} catch (e) {
makeToast(t('tracking.action.login.label.failure', { name: tracker.name }), 'error', getErrorMessage(e));
}
};
const onClick = (openPopup: () => void) => {
if (!isOAuthLogin) {
openPopup();
const login = async (initialUsername?: string, initialPassword?: string) => {
if (isOAuthLogin) {
const state = {
redirectUrl: `${window.location.origin}/tracker/login/oauth`,
clientName: 'Suwayomi-WebUI',
trackerId: tracker.id,
trackerName: tracker.name,
};
window.open(`${tracker.authUrl}&state=${JSON.stringify(state)}`, '_self');
return;
}
handleLogin();
const controlled = CredentialsLogin.showControlled(
{
title: t(
Trackers.isLoggedIn(tracker)
? 'tracking.settings.dialog.title.log_out'
: 'tracking.settings.dialog.title.log_in',
{ name: tracker.name },
),
isLoading: false,
isLoggedIn: Trackers.isLoggedIn(tracker),
username: initialUsername,
password: initialPassword,
loginLogout: async (username, password) => {
controlled.update({
isLoading: true,
loginLogout: noOp,
});
if (Trackers.isLoggedIn(tracker)) {
try {
await handleLogout();
controlled.submit();
} catch (e) {
makeToast(
t('tracking.action.logout.label.failure', { name: tracker.name }),
'error',
getErrorMessage(e),
);
}
return;
}
try {
await handleLogin(username, password);
controlled.submit();
} catch (e) {
makeToast(
t('tracking.action.login.label.failure', { name: tracker.name }),
'error',
getErrorMessage(e),
);
const RETRY_KEY = '__retry__';
const retry = await Promise.race([controlled.promise, Promise.resolve(RETRY_KEY)]);
if (retry === RETRY_KEY) {
login(username, password);
}
}
},
},
{ id: 'tracker-login-dialog' },
);
await controlled.promise;
};
return (
<PopupState variant="popover" popupId="tracker-dialog">
{(popupState) => (
<>
<ListItemButton {...bindTrigger(popupState)} onClick={() => onClick(popupState.open)}>
<ListItemAvatar sx={{ paddingRight: '20px' }}>
<AvatarSpinner
alt={`${tracker.name}`}
iconUrl={requestManager.getValidImgUrlFor(tracker.icon)}
slots={{
avatarProps: {
variant: 'rounded',
sx: { width: 64, height: 64 },
},
spinnerImageProps: {
ignoreQueue: true,
},
}}
/>
</ListItemAvatar>
<ListItemText primary={tracker.name} />
{Trackers.isLoggedIn(tracker) && (
<ListItemSecondaryAction>
<Chip label={t('global.label.logged_in')} color="success" />
</ListItemSecondaryAction>
)}
</ListItemButton>
<Dialog
{...bindDialog(popupState)}
open={(Trackers.isLoggedIn(tracker) || !tracker.authUrl) && popupState.isOpen}
disableRestoreFocus
>
<DialogTitle>
{t(
Trackers.isLoggedIn(tracker)
? 'tracking.settings.dialog.title.log_out'
: 'tracking.settings.dialog.title.log_in',
{ name: tracker.name },
)}
</DialogTitle>
{!isOAuthLogin && !tracker.isLoggedIn && (
<DialogContent>
<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)}
/>
</DialogContent>
)}
<DialogActions>
<Button onClick={popupState.close}>{t('global.button.cancel')}</Button>
<Button
variant="contained"
disabled={
!isOAuthLogin &&
!tracker.isLoggedIn &&
(isCredentialLoginInProgress || !username.length || !password.length)
}
onClick={() => (Trackers.isLoggedIn(tracker) ? handleLogout() : handleLogin())}
>
{t(Trackers.isLoggedIn(tracker) ? 'global.button.log_out' : 'global.button.log_in')}
</Button>
</DialogActions>
</Dialog>
</>
<ListItemButton onClick={() => login()}>
<ListItemAvatar sx={{ paddingRight: '20px' }}>
<AvatarSpinner
alt={`${tracker.name}`}
iconUrl={requestManager.getValidImgUrlFor(tracker.icon)}
slots={{
avatarProps: {
variant: 'rounded',
sx: { width: 64, height: 64 },
},
spinnerImageProps: {
ignoreQueue: true,
},
}}
/>
</ListItemAvatar>
<ListItemText primary={tracker.name} />
{Trackers.isLoggedIn(tracker) && (
<ListItemSecondaryAction>
<Chip label={t('global.label.logged_in')} color="success" />
</ListItemSecondaryAction>
)}
</PopupState>
</ListItemButton>
);
};

View File

@@ -23,19 +23,10 @@ export const TrackerOAuthLogin = () => {
url.searchParams.get('state') ?? '{}',
);
const [loginTrackerOAuth, { loading: isLoginInProgress }] = requestManager.useLoginToTrackerOauth();
useEffect(() => {
const login = async () => {
try {
await loginTrackerOAuth({
variables: {
input: {
callbackUrl: window.location.href,
trackerId,
},
},
});
await requestManager.loginToTrackerOauth(trackerId, window.location.href).response;
} catch (e) {
makeToast(t('tracking.action.login.label.failure', { name: trackerName }), 'error', getErrorMessage(e));
}
@@ -46,9 +37,5 @@ export const TrackerOAuthLogin = () => {
login();
}, [trackerId]);
if (isLoginInProgress) {
return t('tracking.action.login.label.progress', { name: trackerName });
}
return null;
return t('tracking.action.login.label.progress', { name: trackerName });
};