feat(settings): unified Cloud Sync chooser with Readest Cloud as a first-class provider (#4976)

The Integrations section is now one Cloud Sync chooser: Readest Cloud,
WebDAV, and Google Drive as radio rows, Readest Cloud first, with a
scope subtitle stating what the choice governs (library data, on this
device) and what always stays with the Readest account (settings,
statistics, dictionaries).

- Activation helpers move to services (cloudSyncActivation.ts) and
  accept 'readest' (= no third-party provider active); the component
  module re-exports for existing imports.
- Row status lines come from a pure, fully-tested state matrix
  (cloudSyncStatus.ts) so every string is enumerated in one place:
  signed-out / loading / active / available for Readest Cloud;
  not connected / configured / paused / syncing / sync failed /
  book-file-uploads-off warning / active for third-party rows. The
  paused state renders on the affected third-party row (the plan sketch
  placed it on the Readest row; the provider that is paused is the
  third-party one).
- The Readest Cloud row opens an inline sub-page (SubPageHeader + the
  storage/translation Quota + a NavigationRow out to Account) instead of
  ejecting from the Settings dialog; signed-out taps go to login and the
  radio is unchecked and disabled, so an idle signed-out state never
  shows a checked radio while nothing syncs.
- Premium-gated builds keep the Readest Cloud row and show the upgrade
  prompt only in place of the third-party rows.
- Two-direction capability Tips in the WebDAV/Drive sub-pages spell out
  both what syncs only to the user's server and what still flows through
  the Readest account; the Upload Book Files description notes that
  Readest Cloud uploads pause while the provider is selected.
- Manage Sync book/progress/note rows swap their description to
  'Managed by {{provider}}...' via the existing locked-row pattern while
  a third-party provider is selected; the toggles stay interactive and
  persist since they govern the native channels after switch-back.
- The chooser rows form a radiogroup (native same-name radios provide
  arrow-key group movement) with an accessible group label.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Huang Xin
2026-07-07 01:16:14 +09:00
committed by GitHub
parent e08622b416
commit 57868a138e
8 changed files with 375 additions and 100 deletions
@@ -1,69 +1,10 @@
import type { SystemSettings } from '@/types/settings';
import type { EnvConfigType } from '@/services/environment';
import type { FileSyncBackendKind } from '@/services/sync/file/providerRegistry';
import { useSettingsStore } from '@/store/settingsStore';
import { broadcastGlobalSettings } from '@/utils/settingsSync';
/**
* Return settings with exactly one third-party cloud-sync provider active (or
* none). WebDAV and Google Drive are mutually exclusive — only one syncs the
* library at a time — so enabling one always disables the other. Provider config
* (WebDAV creds, the Drive keychain token) is left untouched, so switching back
* to a previously-configured provider needs no re-entry; only an explicit
* Disconnect tears a provider's config down.
*
* Activating a provider (disabled -> enabled) also turns its `syncBooks` on:
* the selected provider owns the book-file channel — native Readest Cloud
* uploads gate off — so leaving syncBooks at its `false` default would back
* books up nowhere. An explicit opt-out while the provider stays active is
* respected (re-activation of an already-active provider changes nothing).
* Re-export shim: the activation helpers moved to the services layer
* (`src/services/sync/cloudSyncActivation.ts`) so service modules never
* import from components. Existing component-side imports keep working.
*/
export const withActiveCloudProvider = (
settings: SystemSettings,
active: FileSyncBackendKind | null,
): SystemSettings => ({
...settings,
webdav: {
...settings.webdav,
enabled: active === 'webdav',
...(active === 'webdav' && !settings.webdav?.enabled
? { syncBooks: true, providerSelectedAt: Date.now() }
: {}),
},
googleDrive: {
...settings.googleDrive,
enabled: active === 'gdrive',
...(active === 'gdrive' && !settings.googleDrive?.enabled
? { syncBooks: true, providerSelectedAt: Date.now() }
: {}),
},
});
/**
* The single write path for changing the selected cloud sync provider.
* Every activation/deactivation surface (the Integrations chooser, the
* WebDAV/Drive connect and disconnect flows, the Drive OAuth callback)
* routes through here so the change always (a) persists, (b) hydrates the
* settings store even on routes where it wasn't loaded yet (the OAuth
* callback), and (c) broadcasts the provider flags to other windows —
* a stale reader window would otherwise clobber the switch on its next
* whole-file save, silently resuming Readest Cloud uploads.
*
* `mutate` runs BEFORE activation so connect flows can apply credentials
* or account labels without pre-setting `enabled` (which would suppress
* the activation side effects).
*/
export const persistActiveCloudProvider = async (
envConfig: EnvConfigType,
active: FileSyncBackendKind | null,
mutate: (settings: SystemSettings) => SystemSettings = (s) => s,
): Promise<SystemSettings> => {
const store = useSettingsStore.getState();
const appService = await envConfig.getAppService();
const current = store.settings?.version ? store.settings : await appService.loadSettings();
const next = withActiveCloudProvider(mutate(current), active);
store.setSettings(next);
await appService.saveSettings(next);
void broadcastGlobalSettings(next, { includeCloudSyncProviders: true });
return next;
};
export {
withActiveCloudProvider,
persistActiveCloudProvider,
type CloudSyncActivationKind,
} from '@/services/sync/cloudSyncActivation';