diff --git a/apps/readest-app/src/__tests__/services/sync/replicaBinaryUpload.test.ts b/apps/readest-app/src/__tests__/services/sync/replicaBinaryUpload.test.ts index f26ef696..90e743bb 100644 --- a/apps/readest-app/src/__tests__/services/sync/replicaBinaryUpload.test.ts +++ b/apps/readest-app/src/__tests__/services/sync/replicaBinaryUpload.test.ts @@ -16,8 +16,10 @@ import { getAccessToken } from '@/utils/access'; import { queueDictionaryBinaryUpload } from '@/services/sync/replicaBinaryUpload'; import { clearReplicaAdapters, registerReplicaAdapter } from '@/services/sync/replicaRegistry'; import { dictionaryAdapter } from '@/services/sync/adapters/dictionary'; +import { useSettingsStore } from '@/store/settingsStore'; import type { ImportedDictionary } from '@/services/dictionaries/types'; import type { AppService } from '@/types/system'; +import type { SystemSettings } from '@/types/settings'; const mockIsReady = transferManager.isReady as ReturnType; const mockQueueReplicaUpload = transferManager.queueReplicaUpload as ReturnType; @@ -172,4 +174,29 @@ describe('queueDictionaryBinaryUpload', () => { await queueDictionaryBinaryUpload(baseDict(), fakeAppService as unknown as AppService); expect(close).toHaveBeenCalled(); }); + + test('no-ops when the dictionary sync category is disabled in Manage Sync', async () => { + // Sister-side gate to `publishReplicaUpsert`: when the user has turned + // dictionary sync off, the bundle binaries must also stay on the + // device. Otherwise the (potentially hundreds-of-MB) upload still + // fires even though the metadata row never publishes. + const prevSettings = useSettingsStore.getState().settings; + useSettingsStore.setState({ + settings: { + ...(prevSettings ?? ({} as SystemSettings)), + syncCategories: { ...(prevSettings?.syncCategories ?? {}), dictionary: false }, + } as SystemSettings, + }); + try { + mockIsReady.mockReturnValue(true); + const fakeAppService = makeFakeAppService({ + 'bundle-dir/webster.mdx': 1_000_000, + }) as unknown as AppService; + const result = await queueDictionaryBinaryUpload(baseDict(), fakeAppService); + expect(result).toBe(null); + expect(mockQueueReplicaUpload).not.toHaveBeenCalled(); + } finally { + useSettingsStore.setState({ settings: prevSettings }); + } + }); }); diff --git a/apps/readest-app/src/services/dictionaries/providers/mdictProvider.ts b/apps/readest-app/src/services/dictionaries/providers/mdictProvider.ts index 6c8e80d3..f36beb50 100644 --- a/apps/readest-app/src/services/dictionaries/providers/mdictProvider.ts +++ b/apps/readest-app/src/services/dictionaries/providers/mdictProvider.ts @@ -222,6 +222,103 @@ async function resolveCssUrls( }); } +// Pattern for the audio-play handler used by Vocabulary.com-derived MDicts: +// +// +// +// The first argument is the element itself; the second is the key into the +// companion MDD's audio store. The bundled `j.js` script (which we ignore for +// XSS reasons — never execute MDX-supplied JavaScript inside the shadow root) +// resolves that key to a path like `E/BDAWTHU6YF46.mp3` and plays it. We do +// the same lookup ourselves from a CSP-safe click handler. +const V0R_PLAY_RX = /v0r\.v\s*\(\s*this\s*,\s*['"]([^'"]+)['"]\s*\)/i; +const AUDIO_EXTS = ['.mp3', '.m4a', '.aac', '.ogg', '.opus', '.wav'] as const; + +async function wireMdictAudioOnclick( + container: HTMLElement, + mdds: MDDInstance[], + trackedUrls: string[], +): Promise { + // Note: we deliberately leave `