Add sync settings
This commit is contained in:
@@ -13,6 +13,7 @@ import type {
|
||||
BackupFlagInclusionState,
|
||||
} from '@/features/backup/Backup.types.ts';
|
||||
import { BACKUP_FLAGS_TO_TRANSLATION } from '@/features/backup/Backup.constants.ts';
|
||||
import omit from 'lodash/fp/omit';
|
||||
|
||||
export const convertToAutoBackupFlags = (flags: BackupFlagInclusionState): AutoBackupFlagInclusionState => ({
|
||||
autoBackupIncludeCategories: flags.includeCategories,
|
||||
@@ -46,11 +47,14 @@ const getIncludeExcludeText = (count: number, allCount: number, specificText: st
|
||||
return specificText;
|
||||
};
|
||||
|
||||
export const getAutoBackupFlagsInfo = (autoFlags: AutoBackupFlagInclusionState): Record<`${boolean}`, string> => {
|
||||
const flags = convertToBackupFlags(autoFlags);
|
||||
export const getAutoBackupFlagsInfo = (
|
||||
autoFlags: AutoBackupFlagInclusionState,
|
||||
hiddenFlags: BackupFlag[] = [],
|
||||
): Record<`${boolean}`, string> => {
|
||||
const flags = omit(hiddenFlags, convertToBackupFlags(autoFlags));
|
||||
const totalFlags = Object.keys(flags).length;
|
||||
|
||||
const flagsByState = Object.groupBy(Object.entries(flags), ([, value]) => value.toString());
|
||||
const flagsByState = Object.groupBy(Object.entries(flags), ([, value]) => (value as boolean).toString());
|
||||
|
||||
const includedFlagsString =
|
||||
flagsByState.true?.map(([key]) => t(BACKUP_FLAGS_TO_TRANSLATION[key as BackupFlag])).join(', ') ?? '';
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
BACKUP_FLAGS_BY_GROUP,
|
||||
BACKUP_FLAGS_TO_TRANSLATION,
|
||||
} from '@/features/backup/Backup.constants.ts';
|
||||
import type { BackupFlagGroup, BackupFlagInclusionState } from '@/features/backup/Backup.types.ts';
|
||||
import type { BackupFlag, BackupFlagGroup, BackupFlagInclusionState } from '@/features/backup/Backup.types.ts';
|
||||
|
||||
export const BackupFlagInclusionDialog = ({
|
||||
onDismiss,
|
||||
@@ -33,11 +33,18 @@ export const BackupFlagInclusionDialog = ({
|
||||
onExitComplete,
|
||||
title,
|
||||
flags,
|
||||
}: AwaitableComponentProps<BackupFlagInclusionState> & { title: string; flags?: BackupFlagInclusionState }) => {
|
||||
hiddenFlags,
|
||||
}: AwaitableComponentProps<BackupFlagInclusionState> & {
|
||||
title: string;
|
||||
flags?: BackupFlagInclusionState;
|
||||
hiddenFlags?: BackupFlag[];
|
||||
}) => {
|
||||
const { t } = useLingui();
|
||||
|
||||
const [includeStateByFlag, setIncludeStateByFlag] = useState(
|
||||
Object.fromEntries(BACKUP_FLAGS.map((flag) => [flag, flags?.[flag] ?? true])) as BackupFlagInclusionState,
|
||||
Object.fromEntries(
|
||||
BACKUP_FLAGS.map((flag) => [flag, flags?.[flag] ?? !hiddenFlags?.includes(flag)]),
|
||||
) as BackupFlagInclusionState,
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -45,24 +52,28 @@ export const BackupFlagInclusionDialog = ({
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
<DialogContent>
|
||||
<FormGroup sx={{ gap: 2 }}>
|
||||
{Object.entries(BACKUP_FLAGS_BY_GROUP).map(([group, groupFlags]) => (
|
||||
<Stack key={group}>
|
||||
<Typography>{t(BACKUP_FLAG_GROUP_TO_TRANSLATION[group as BackupFlagGroup])}</Typography>
|
||||
{groupFlags.map((flag) => (
|
||||
<CheckboxInput
|
||||
key={flag}
|
||||
label={t(BACKUP_FLAGS_TO_TRANSLATION[flag])}
|
||||
checked={includeStateByFlag[flag]}
|
||||
onChange={(_, checked) => {
|
||||
setIncludeStateByFlag({
|
||||
...includeStateByFlag,
|
||||
[flag]: checked,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
))}
|
||||
{Object.entries(BACKUP_FLAGS_BY_GROUP)
|
||||
.filter(([_group, groupFlags]) => groupFlags.some((flag) => !hiddenFlags?.includes(flag)))
|
||||
.map(([group, groupFlags]) => (
|
||||
<Stack key={group}>
|
||||
<Typography>{t(BACKUP_FLAG_GROUP_TO_TRANSLATION[group as BackupFlagGroup])}</Typography>
|
||||
{groupFlags
|
||||
.filter((flag) => !hiddenFlags?.includes(flag))
|
||||
.map((flag) => (
|
||||
<CheckboxInput
|
||||
key={flag}
|
||||
label={t(BACKUP_FLAGS_TO_TRANSLATION[flag])}
|
||||
checked={includeStateByFlag[flag]}
|
||||
onChange={(_, checked) => {
|
||||
setIncludeStateByFlag({
|
||||
...includeStateByFlag,
|
||||
[flag]: checked,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
))}
|
||||
</FormGroup>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user