diff --git a/src/App.tsx b/src/App.tsx index bf873aca..c32f2067 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -199,6 +199,7 @@ const BackgroundSubscriptions = () => { requestManager.useDownloadSubscription({ skip: skipConnection }); requestManager.useUpdaterSubscription({ skip: skipConnection }); requestManager.useWebUIUpdateSubscription({ skip: skipConnection }); + requestManager.useSyncSubscription({ skip: skipConnection }); return null; }; diff --git a/src/base/utils/DateHelper.ts b/src/base/utils/DateHelper.ts index 4fb8664a..e3a57cc1 100644 --- a/src/base/utils/DateHelper.ts +++ b/src/base/utils/DateHelper.ts @@ -48,7 +48,7 @@ export const isSameDay = (first: Dayjs, second: Dayjs): boolean => first.isSame( * @param date * @param withTime */ -export const getDateString = (date: Dayjs | number, withTime: boolean = false) => { +export const getDateString = (date: Dayjs | number, withTime: boolean = false, dateTime: boolean = false) => { const actualDate = date instanceof dayjs ? date : dayjs(date); const timeString = timeFormatter.format(actualDate.toDate()); @@ -68,5 +68,9 @@ export const getDateString = (date: Dayjs | number, withTime: boolean = false) = return t`Yesterday`; } + if (dateTime) { + return dateTimeFormatter.format(actualDate.toDate()); + } + return dateFormatter.format(actualDate.toDate()); }; diff --git a/src/features/settings/Settings.constants.ts b/src/features/settings/Settings.constants.ts index ec5894b6..5e89584f 100644 --- a/src/features/settings/Settings.constants.ts +++ b/src/features/settings/Settings.constants.ts @@ -24,6 +24,8 @@ import { AuthMode, KoreaderSyncChecksumMethod, KoreaderSyncConflictStrategy, + StartSyncResult, + SyncState, WebUiChannel, WebUiFlavor, WebUiInterface, @@ -360,3 +362,20 @@ export const SYNC_SETTINGS_HIDDEN_BACKUP_FLAGS = [ 'includeServerSettings', 'includeClientData', ] as const satisfies BackupFlag[]; + +export const SYNC_START_RESULT_TRANSLATION: Record = { + [StartSyncResult.Success]: msg`Sync started`, + [StartSyncResult.SyncDisabled]: msg`Sync disabled`, + [StartSyncResult.SyncInProgress]: msg`Sync in progress`, +}; + +export const SYNC_STATE_TRANSLATION: Record = { + [SyncState.CreatingBackup]: msg`Creating backup`, + [SyncState.Downloading]: msg`Downloading`, + [SyncState.Error]: msg`Error`, + [SyncState.Merging]: msg`Merging`, + [SyncState.Restoring]: msg`Restoring`, + [SyncState.Started]: msg`Started`, + [SyncState.Success]: msg`Success`, + [SyncState.Uploading]: msg`Uploading`, +}; diff --git a/src/features/settings/screens/ServerSettings.tsx b/src/features/settings/screens/ServerSettings.tsx index 1d9d0c9c..e72966cd 100644 --- a/src/features/settings/screens/ServerSettings.tsx +++ b/src/features/settings/screens/ServerSettings.tsx @@ -32,7 +32,14 @@ import type { MetadataUpdateSettings } from '@/features/app-updates/AppUpdateChe 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 { + AuthMode, + CbzMediaType, + DatabaseType, + SortOrder, + StartSyncResult, + SyncState, +} from '@/lib/graphql/generated/graphql-base.types.ts'; import { AUTH_MODES_SELECT_VALUES, JWT_ACCESS_TOKEN_EXPIRY, @@ -42,6 +49,8 @@ import { SYNC_INTERVAL_SELECT_VALUES_WITH_CUSTOM, SYNC_INTERVAL_VALUES, SYNC_SETTINGS_HIDDEN_BACKUP_FLAGS, + SYNC_START_RESULT_TRANSLATION, + SYNC_STATE_TRANSLATION, } from '@/features/settings/Settings.constants.ts'; import { ServerAddressSetting } from '@/features/settings/components/ServerAddressSetting.tsx'; import { AuthManager } from '@/features/authentication/AuthManager.ts'; @@ -53,6 +62,7 @@ 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'; +import { epochToDate, getDateString } from '@/base/utils/DateHelper.ts'; const convertSyncDataToBackupFlags = (settings: ServerSettingsType): BackupFlagInclusionState => ({ includeManga: settings.syncDataManga, @@ -88,6 +98,7 @@ export const ServerSettings = () => { useAppTitle(t`Server`); + const syncStatusRequest = requestManager.useGetSyncStatus(); const { settings: { serverInformAvailableUpdate, serverInformVersionUpdated }, loading: areMetadataServerSettingsLoading, @@ -221,6 +232,11 @@ export const ServerSettings = () => { const includedSyncDataText = syncDataFlagsInfo.true; const excludedSyncDataText = syncDataFlagsInfo.false; + const syncStatus = syncStatusRequest.data?.lastSyncStatus; + const syncDate = syncStatus?.endDate ?? syncStatus?.startDate; + const syncState = syncStatus?.state; + const isSyncing = !!syncState && ![SyncState.Success, SyncState.Error].includes(syncState); + return ( {localSettings} @@ -732,6 +748,33 @@ export const ServerSettings = () => { } > + + requestManager + .startSync() + .response.then((response) => { + const startResult = response.data?.startSync.result; + + makeToast( + t(SYNC_START_RESULT_TRANSLATION[startResult!]), + startResult === StartSyncResult.Success ? 'success' : 'error', + ); + }) + .catch((e) => makeToast(t`Could not start sync`, 'error', getErrorMessage(e))) + } + > + + {syncDate && t`Last sync: ${getDateString(epochToDate(Number(syncDate)), true, true)}`}{' '} + {syncState && t`- State: ${t(SYNC_STATE_TRANSLATION[syncState])}`} + {!!syncStatus?.errorMessage && `\nError: ${syncStatus.errorMessage}`} + + } + /> + { try { @@ -748,7 +791,7 @@ export const ServerSettings = () => { }} > {t`Include: ${includedSyncDataText}`} diff --git a/src/i18n/locales/en.po b/src/i18n/locales/en.po index 304c4e84..2a7ae641 100644 --- a/src/i18n/locales/en.po +++ b/src/i18n/locales/en.po @@ -14,6 +14,11 @@ msgstr "" "Plural-Forms: \n" "X-Message-Format: ICU\n" +#. placeholder {0}: t(SYNC_STATE_TRANSLATION[syncState]) +#: src/features/settings/screens/ServerSettings.tsx +msgid "- State: {0}" +msgstr "- State: {0}" + #. placeholder {0}: selectedChapters.length #. placeholder {0}: visibleChapters.length #: src/features/chapter/components/ChapterList.tsx @@ -574,7 +579,6 @@ msgid "Backup cleanup" msgstr "Backup cleanup" #: src/features/backup/screens/Backup.tsx -#: src/features/settings/screens/ServerSettings.tsx msgid "Backup data" msgstr "Backup data" @@ -1152,6 +1156,10 @@ msgstr "Could not save theme changes" msgid "Could not search source" msgstr "Could not search source" +#: src/features/settings/screens/ServerSettings.tsx +msgid "Could not start sync" +msgstr "Could not start sync" + #: src/features/updates/components/UpdateChecker.tsx msgid "Could not stop global update" msgstr "Could not stop global update" @@ -1192,6 +1200,10 @@ msgstr "Create theme" msgid "Created theme" msgstr "Created theme" +#: src/features/settings/Settings.constants.ts +msgid "Creating backup" +msgstr "Creating backup" + #: src/features/backup/screens/Backup.tsx msgid "Creating backup…" msgstr "Creating backup…" @@ -1488,6 +1500,7 @@ msgid "Downloaded" msgstr "Downloaded" #: src/base/components/downloads/DownloadStateIndicator.tsx +#: src/features/settings/Settings.constants.ts msgid "Downloading" msgstr "Downloading" @@ -1587,6 +1600,7 @@ msgstr "Entry \"{id}\" not found" #: src/base/components/downloads/DownloadStateIndicator.tsx #: src/base/components/feedback/SnackbarWithDescription.tsx +#: src/features/settings/Settings.constants.ts msgid "Error" msgstr "Error" @@ -2168,6 +2182,11 @@ msgstr "Large" msgid "Last read chapter" msgstr "Last read chapter" +#. placeholder {0}: getDateString(epochToDate(Number(syncDate)), true, true) +#: src/features/settings/screens/ServerSettings.tsx +msgid "Last sync: {0}" +msgstr "Last sync: {0}" + #: src/features/updates/screens/Updates.tsx msgid "Last update: {date}" msgstr "Last update: {date}" @@ -2420,6 +2439,10 @@ msgstr "Medium" msgid "Menu" msgstr "Menu" +#: src/features/settings/Settings.constants.ts +msgid "Merging" +msgstr "Merging" + #: src/features/browse/screens/Browse.tsx #: src/features/manga/components/MangaToolbarMenu.tsx #: src/features/manga/Manga.constants.ts @@ -3008,6 +3031,10 @@ msgstr "Restore" msgid "Restore Backup" msgstr "Restore Backup" +#: src/features/settings/Settings.constants.ts +msgid "Restoring" +msgstr "Restoring" + #: src/features/backup/screens/Backup.tsx msgid "Restoring backup…" msgstr "Restoring backup…" @@ -3496,7 +3523,12 @@ msgstr "" msgid "Start Search" msgstr "Start Search" +#: src/features/settings/screens/ServerSettings.tsx +msgid "Start sync" +msgstr "Start sync" + #: src/features/library/components/LibraryOptionsPanel.tsx +#: src/features/settings/Settings.constants.ts #: src/features/tracker/components/cards/TrackerMangaCard.tsx msgid "Started" msgstr "Started" @@ -3531,6 +3563,7 @@ msgid "Submit" msgstr "Submit" #: src/base/components/feedback/SnackbarWithDescription.tsx +#: src/features/settings/Settings.constants.ts msgid "Success" msgstr "Success" @@ -3550,11 +3583,24 @@ msgstr "Sync" msgid "Sync data" msgstr "Sync data" +#: src/features/settings/Settings.constants.ts +msgid "Sync disabled" +msgstr "Sync disabled" + +#: src/features/settings/Settings.constants.ts +msgid "Sync in progress" +msgstr "Sync in progress" + #: src/features/settings/screens/ServerSettings.tsx msgid "Sync interval" msgstr "Sync interval" +#: src/features/settings/Settings.constants.ts +msgid "Sync started" +msgstr "Sync started" + #: src/features/settings/components/koreaderSync/KoreaderSyncSettings.tsx +#: src/features/settings/screens/ServerSettings.tsx msgid "Sync status" msgstr "Sync status" @@ -3966,6 +4012,10 @@ msgstr "Updates" msgid "Updating" msgstr "Updating" +#: src/features/settings/Settings.constants.ts +msgid "Uploading" +msgstr "Uploading" + #: src/features/settings/Settings.constants.ts msgid "URL" msgstr "URL" diff --git a/src/lib/requests/RequestManager.ts b/src/lib/requests/RequestManager.ts index 4279f487..37e5a469 100644 --- a/src/lib/requests/RequestManager.ts +++ b/src/lib/requests/RequestManager.ts @@ -102,6 +102,8 @@ import type { GetSourceMangasFetchMutationVariables, GetSourcesListQuery, GetSourcesListQueryVariables, + GetSyncStatusQuery, + GetSyncStatusQueryVariables, GetUpdateStatusQuery, GetUpdateStatusQueryVariables, GetWebuiUpdateStatusQuery, @@ -124,10 +126,14 @@ import type { RestoreBackupMutationVariables, StartDownloaderMutation, StartDownloaderMutationVariables, + StartSyncMutation, + StartSyncMutationVariables, StopDownloaderMutation, StopDownloaderMutationVariables, StopUpdaterMutation, StopUpdaterMutationVariables, + SyncSubscription, + SyncSubscriptionVariables, TrackerBindMutation, TrackerBindMutationVariables, TrackerBindTrackRecordMutation, @@ -193,6 +199,7 @@ import type { ValidateBackupQuery, ValidateBackupQueryVariables, WebuiUpdateSubscription, + WebuiUpdateSubscriptionVariables, } from '@/lib/graphql/generated/graphql.ts'; import type { CreateBackupInput, @@ -345,6 +352,9 @@ import { EXTENSION_STORE_FIELDS } from '@/lib/graphql/extension/store/ExtensionS import { ADD_EXTENSION_STORE, REMOVE_EXTENSION_STORE } from '@/lib/graphql/extension/store/ExtensionStoreMutation.ts'; import { assertIsDefined } from '@/base/Asserts.ts'; import { GET_EXTENSION_STORE, GET_EXTENSION_STORES } from '@/lib/graphql/extension/store/ExtensionStoreQuery.ts'; +import { SYNC_SUBSCRIPTION } from '@/lib/graphql/sync/SyncSubscription.ts'; +import { START_SYNC } from '@/lib/graphql/sync/SyncMutation.ts'; +import { GET_SYNC_STATUS } from '@/lib/graphql/sync/SyncQuery.ts'; enum GQLMethod { QUERY = 'QUERY', @@ -3717,7 +3727,7 @@ export class RequestManager { } public useWebUIUpdateSubscription( - options?: SubscriptionHookOptions, + options?: SubscriptionHookOptions, ): useSubscription.Result { return this.doRequest(GQLMethod.USE_SUBSCRIPTION, WEBUI_UPDATE_SUBSCRIPTION, undefined, options); } @@ -3860,6 +3870,29 @@ export class RequestManager { return this.doRequest(GQLMethod.USE_MUTATION, USER_LOGIN, undefined, options); } + public startSync( + options?: MutationOptions, + ): AbortableApolloMutationResponse { + return this.doRequest( + GQLMethod.MUTATION, + START_SYNC, + {}, + options, + ); + } + + public useGetSyncStatus( + options?: QueryHookOptions, + ): AbortableApolloUseQueryResponse { + return this.doRequest(GQLMethod.USE_QUERY, GET_SYNC_STATUS, {}, options); + } + + public useSyncSubscription( + options?: SubscriptionHookOptions, + ): useSubscription.Result { + return this.doRequest(GQLMethod.USE_SUBSCRIPTION, SYNC_SUBSCRIPTION, undefined, options); + } + public useKoSyncStatus( options?: QueryHookOptions, ): AbortableApolloUseQueryResponse { diff --git a/src/lib/requests/client/GraphQLClient.ts b/src/lib/requests/client/GraphQLClient.ts index a57bb912..ca50221f 100644 --- a/src/lib/requests/client/GraphQLClient.ts +++ b/src/lib/requests/client/GraphQLClient.ts @@ -74,6 +74,7 @@ const typePolicies: TypedTypePolicies = { WebUIUpdateStatus: { keyFields: [] }, UpdateStatus: { keyFields: [] }, KoSyncStatusPayload: { keyFields: [] }, + SyncStatus: { keyFields: [] }, Query: { fields: { manga(_, { args, toReference }) { @@ -132,6 +133,9 @@ const typePolicies: TypedTypePolicies = { updateStatus(_, { toReference }) { return toReference({ __typename: 'UpdateStatus', key: {} }); }, + lastSyncStatus(_, { toReference }) { + return toReference({ __typename: 'SyncStatus', key: {} }); + }, chapters: { keyArgs: ['condition', 'filter', 'orderBy', 'orderByType', 'order'], merge(existing, incoming) {