Add sync settings

This commit is contained in:
schroda
2026-06-30 15:08:17 +02:00
parent 43fef26b89
commit 816409fcb8
14 changed files with 612 additions and 24 deletions

View File

@@ -30,6 +30,7 @@ import {
} from '@/lib/graphql/generated/graphql-base.types.ts';
import { ThemeMode } from '@/features/theme/AppTheme.types.ts';
import { getPreferredISOLanguageCodes } from '@/lib/ISOLanguageUtil.ts';
import type { BackupFlag } from '@/features/backup/Backup.types.ts';
export const MANGA_GRID_WIDTH = {
min: 100,
@@ -317,3 +318,45 @@ export const IMAGE_PROCESSING_TYPE_TO_SETTING: Record<
[ImageProcessingType.DOWNLOAD]: 'downloadConversions',
[ImageProcessingType.SERVE]: 'serveConversions',
};
const SYNC_INTERVAL_TO_TRANSLATION: Record<string, SelectSettingValueDisplayInfo> = {
[d(0).seconds.asWholeMinutes.toISOString()]: {
text: msg`Disabled`,
},
[d(30).minutes.asWholeMinutes.toISOString()]: {
text: msg`Every 30 minutes`,
},
[d(1).hours.asWholeMinutes.toISOString()]: {
text: msg`Every hour`,
},
[d(3).hours.asWholeMinutes.toISOString()]: {
text: msg`Every 3 hours`,
},
[d(6).hours.asWholeMinutes.toISOString()]: {
text: msg`Every 6 hours`,
},
[d(12).hours.asWholeMinutes.toISOString()]: {
text: msg`Every 12 hours`,
},
[d(1).days.asWholeMinutes.toISOString()]: {
text: msg`Daily`,
},
[d(7).days.asWholeMinutes.toISOString()]: {
text: msg`Weekly`,
},
};
export const SYNC_INTERVAL_CUSTOM_VALUE = d(10).days.asWholeMinutes.toISOString();
export const SYNC_INTERVAL_VALUES = Object.keys(SYNC_INTERVAL_TO_TRANSLATION);
export const SYNC_INTERVAL_SELECT_VALUES: SelectSettingValue<string>[] = SYNC_INTERVAL_VALUES.map((value) => [
value,
SYNC_INTERVAL_TO_TRANSLATION[value],
]);
export const SYNC_INTERVAL_SELECT_VALUES_WITH_CUSTOM: SelectSettingValue<string>[] = [
...SYNC_INTERVAL_SELECT_VALUES,
[SYNC_INTERVAL_CUSTOM_VALUE, { text: msg`Custom` }],
];
export const SYNC_SETTINGS_HIDDEN_BACKUP_FLAGS = [
'includeServerSettings',
'includeClientData',
] as const satisfies BackupFlag[];

View File

@@ -31,17 +31,46 @@ import { makeToast } from '@/base/utils/Toast.ts';
import type { MetadataUpdateSettings } from '@/features/app-updates/AppUpdateChecker.types.ts';
import { getErrorMessage } from '@/lib/HelperFunctions.ts';
import { useAppTitle } from '@/features/navigation-bar/hooks/useAppTitle.ts';
import type { PartialSettingsTypeInput } from '@/lib/graphql/generated/graphql-base.types.ts';
import { AuthMode, CbzMediaType, DatabaseType, SortOrder } from '@/lib/graphql/generated/graphql-base.types.ts';
import {
AUTH_MODES_SELECT_VALUES,
JWT_ACCESS_TOKEN_EXPIRY,
JWT_REFRESH_TOKEN_EXPIRY,
SYNC_INTERVAL_CUSTOM_VALUE,
SYNC_INTERVAL_SELECT_VALUES,
SYNC_INTERVAL_SELECT_VALUES_WITH_CUSTOM,
SYNC_INTERVAL_VALUES,
SYNC_SETTINGS_HIDDEN_BACKUP_FLAGS,
} from '@/features/settings/Settings.constants.ts';
import { ServerAddressSetting } from '@/features/settings/components/ServerAddressSetting.tsx';
import { AuthManager } from '@/features/authentication/AuthManager.ts';
import type { ServerSettings as ServerSettingsType } from '@/features/settings/Settings.types.ts';
import { KoreaderSyncSettings } from '@/features/settings/components/koreaderSync/KoreaderSyncSettings.tsx';
import { Confirmation } from '@/base/AppAwaitableComponent.ts';
import { BackupFlagInclusionDialog } from '@/features/backup/component/BackupFlagInclusionDialog.tsx';
import ListItemButton from '@mui/material/ListItemButton';
import { AwaitableComponent } from 'awaitable-component';
import { getAutoBackupFlagsInfo } from '@/features/backup/Backup.utils.ts';
import type { BackupFlagInclusionState } from '@/features/backup/Backup.types.ts';
const convertSyncDataToBackupFlags = (settings: ServerSettingsType): BackupFlagInclusionState => ({
includeManga: settings.syncDataManga,
includeChapters: settings.syncDataChapters,
includeCategories: settings.syncDataCategories,
includeHistory: settings.syncDataHistory,
includeTracking: settings.syncDataTracking,
includeClientData: false,
includeServerSettings: false,
});
const convertBackupFlagsToSyncData = (flags: BackupFlagInclusionState): PartialSettingsTypeInput => ({
syncDataManga: flags.includeManga,
syncDataChapters: flags.includeChapters,
syncDataCategories: flags.includeCategories,
syncDataHistory: flags.includeHistory,
syncDataTracking: flags.includeTracking,
});
const getLogFilesCleanupDisplayValue = (ttl: number): string => {
if (ttl === 0) {
@@ -91,6 +120,15 @@ export const ServerSettings = () => {
onCompletion?.(false);
}
};
const updateSettings = async (settings: PartialSettingsTypeInput, onCompletion?: (success: boolean) => void) => {
try {
await mutateSettings({ variables: { input: { settings } } });
onCompletion?.(true);
} catch (e) {
makeToast(t`Failed to save changes`, 'error', getErrorMessage(e));
onCompletion?.(false);
}
};
const localSettings = useMemo(
() => (
@@ -176,6 +214,13 @@ export const ServerSettings = () => {
const authModeDisabled = !serverSettings.authUsername?.trim() || !serverSettings.authPassword?.trim();
const isH2Database = serverSettings.databaseType === DatabaseType.H2;
const isCustomSyncInterval = !SYNC_INTERVAL_VALUES.includes(
d(serverSettings.syncInterval).minutes.asWholeMinutes.toISOString(),
);
const syncDataFlagsInfo = getAutoBackupFlagsInfo(serverSettings);
const includedSyncDataText = syncDataFlagsInfo.true;
const excludedSyncDataText = syncDataFlagsInfo.false;
return (
<List sx={{ pt: 0 }}>
{localSettings}
@@ -669,6 +714,81 @@ export const ServerSettings = () => {
/>
</ListItem>
</List>
<List
subheader={
<ListSubheader component="div" id="server-settings-sync">
{t`Sync`}
</ListSubheader>
}
>
<ListItemButton
onClick={async () => {
try {
const flags = await AwaitableComponent.show(BackupFlagInclusionDialog, {
title: t`Sync data`,
flags: convertSyncDataToBackupFlags(serverSettings),
hiddenFlags: SYNC_SETTINGS_HIDDEN_BACKUP_FLAGS,
});
await updateSettings(convertBackupFlagsToSyncData(flags));
} catch (e) {
// Ignore
}
}}
>
<ListItemText
primary={t`Backup data`}
secondary={
<>
<span>{t`Include: ${includedSyncDataText}`}</span>
<span>{t`Exclude: ${excludedSyncDataText}`}</span>
</>
}
slotProps={{
secondary: { sx: { display: 'flex', flexDirection: 'column' } },
}}
/>
</ListItemButton>
<SelectSetting
settingName={t`Sync interval`}
value={
isCustomSyncInterval
? SYNC_INTERVAL_CUSTOM_VALUE
: d(serverSettings.syncInterval).minutes.asWholeMinutes.toISOString()
}
values={
isCustomSyncInterval ? SYNC_INTERVAL_SELECT_VALUES_WITH_CUSTOM : SYNC_INTERVAL_SELECT_VALUES
}
handleChange={(syncInterval) => updateSetting('syncInterval', syncInterval)}
/>
<List
subheader={
<ListSubheader component="div" id="server-settings-sync-syncyomi">
{t`SyncYomi`}
</ListSubheader>
}
>
<ListItem>
<ListItemText primary={t`SyncYomi enabled`} />
<Switch
edge="end"
checked={serverSettings.syncYomiEnabled}
onChange={(e) => updateSetting('syncYomiEnabled', e.target.checked)}
/>
</ListItem>
<TextSetting
settingName={t`SyncYomi host`}
value={serverSettings.syncYomiHost}
handleChange={(host) => updateSetting('syncYomiHost', host)}
/>
<TextSetting
settingName={t`SyncYomi API key`}
value={serverSettings.syncYomiApiKey}
handleChange={(apiKey) => updateSetting('syncYomiApiKey', apiKey)}
isPassword
/>
</List>
</List>
<List
subheader={
<ListSubheader component="div" id="server-settings-misc">