744d5b3a03
* fix(dict): restore MDD eager init; CSP-safe Vocabulary.com audio handler
Two MDict fixes:
1. **Regression**: #4334 turned on `MDD.create(..., { lazy: true })` so
the audio / resource side of every MDict would skip the upfront
key-block decode + sort. That's the right trade-off on the MDX side
(millions of headwords, ~80 s init saving), but it doesn't transfer
to MDDs: js-mdict's lazy lookup binary-searches `keyInfoList`
envelopes with `comp` (localeCompare), and MDDs store keys in
byte-sorted order with `\` prefixes and case folding. The two orders
disagree just often enough that `mdd.locateBytes('sound.png')`
misses on resource paths that do exist — the icon's CSS
`background-image: url(sound.png)` then stays unresolved and the
speaker glyph disappears. Bisected to 93abca89 (#4334). Drop the
flag for MDDs; MDXs keep the lazy win.
2. **New**: a CSP-safe replacement for the `onclick="v0r.v(this,'KEY')"`
audio handler used by Vocabulary.com-derived MDicts. The dict's
`j.js` is never loaded inside our shadow root (we don't execute
MDX-supplied JavaScript), so without intervention the inline
handler would throw `ReferenceError: v0r is not defined` on every
click. The new helper parses the audio key from any matching
onclick, strips that one attribute, and binds our own listener that
probes the companion MDD for `<KEY>.mp3` / `<KEY>.m4a` / etc. and
plays the bytes through an `<Audio>` element. Other unrelated
inline handlers are deliberately left alone — they were no-ops
already (their helper JS isn't loaded), and CSS rules sometimes
match on `[onclick]` attribute presence.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(sync): skip binary upload when sync category is disabled
`publishReplicaUpsert` already gates the metadata row on
`isSyncCategoryEnabled(kind)` so a user who turned dictionary / font /
texture sync off in Manage Sync never publishes a row for that kind.
`queueReplicaBinaryUpload` had no such gate, so the binary side still
fired: a 250 MB MDict bundle or a few hundred MB of font files would
still upload to cloud storage with no replica row to reference them.
Mirror the gate here. The `kind` parameter (`dictionary`, `font`,
`texture`) lines up with `SyncCategory` names verbatim, so a single
`isSyncCategoryEnabled(kind)` check at the top of the function is all
it takes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
97 lines
3.5 KiB
TypeScript
97 lines
3.5 KiB
TypeScript
import { transferManager } from '@/services/transferManager';
|
|
import { getReplicaAdapter } from './replicaRegistry';
|
|
import { isSyncCategoryEnabled } from './syncCategories';
|
|
import { getAccessToken } from '@/utils/access';
|
|
import type { AppService, BaseDir } from '@/types/system';
|
|
import type { ReplicaTransferFile } from '@/store/transferStore';
|
|
import type { ClosableFile } from '@/utils/file';
|
|
|
|
/**
|
|
* Open the file just to read its `.size` (Tauri streams metadata; the
|
|
* body isn't read). Used as a fallback when the adapter's
|
|
* enumerateFiles doesn't carry a byteSize — e.g. the dictionary
|
|
* adapter, which doesn't track per-file sizes on its records.
|
|
*/
|
|
const resolveByteSize = async (
|
|
appService: AppService,
|
|
lfp: string,
|
|
base: BaseDir,
|
|
): Promise<number> => {
|
|
const file = await appService.openFile(lfp, base);
|
|
const size = file.size;
|
|
const closable = file as ClosableFile;
|
|
if (closable && closable.close) {
|
|
await closable.close();
|
|
}
|
|
return size;
|
|
};
|
|
|
|
interface ReplicaBinaryRecord {
|
|
contentId?: string;
|
|
name: string;
|
|
reincarnation?: string;
|
|
}
|
|
|
|
/**
|
|
* Queue a replica's binary files for upload via TransferManager.
|
|
* Generic across kinds — uses the adapter registry to dispatch:
|
|
* - adapter.binary.enumerateFiles to list the per-record files
|
|
* - adapter.binary.localBaseDir for the upload base
|
|
*
|
|
* Resolves missing byteSize entries via openFile so progress reporting
|
|
* is accurate. The TransferManager fires `replica-transfer-complete`
|
|
* on success; replicaTransferIntegration converts that into a
|
|
* publishReplicaManifest call (binaries first, manifest last).
|
|
*
|
|
* No-op when:
|
|
* - the user has turned the matching sync category off in Manage Sync
|
|
* (`dictionary`, `font`, `texture` — i.e. backgrounds). Skipping
|
|
* here keeps potentially large bundle uploads from leaving the
|
|
* device when the user has opted out, and matches the metadata-side
|
|
* gate already enforced by `publishReplicaUpsert`.
|
|
* - the record lacks contentId (legacy / unsynced)
|
|
* - the kind isn't registered or has no binary capability
|
|
* - TransferManager isn't initialized yet (pre-library mount)
|
|
*
|
|
* Caller is responsible for ordering this AFTER publishReplicaUpsert
|
|
* so the metadata row exists before the manifest commit fires.
|
|
*/
|
|
export const queueReplicaBinaryUpload = async <T extends ReplicaBinaryRecord>(
|
|
kind: string,
|
|
record: T,
|
|
appService: AppService,
|
|
): Promise<string | null> => {
|
|
if (!isSyncCategoryEnabled(kind)) return null;
|
|
if (!record.contentId) return null;
|
|
if (!(await getAccessToken())) return null;
|
|
if (!transferManager.isReady()) return null;
|
|
|
|
const adapter = getReplicaAdapter<T>(kind);
|
|
if (!adapter?.binary) return null;
|
|
const base = adapter.binary.localBaseDir;
|
|
|
|
const enumerated = adapter.binary.enumerateFiles(record);
|
|
if (enumerated.length === 0) return null;
|
|
|
|
const files: ReplicaTransferFile[] = await Promise.all(
|
|
enumerated.map(async (f) => ({
|
|
logical: f.logical,
|
|
lfp: f.lfp,
|
|
byteSize: f.byteSize > 0 ? f.byteSize : await resolveByteSize(appService, f.lfp, base),
|
|
})),
|
|
);
|
|
|
|
return transferManager.queueReplicaUpload(kind, record.contentId, record.name, files, base, {
|
|
reincarnation: record.reincarnation,
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Backwards-compatible alias for the dictionary-specific helper that
|
|
* call sites used before the kind-agnostic refactor.
|
|
*/
|
|
export const queueDictionaryBinaryUpload = <T extends ReplicaBinaryRecord>(
|
|
dict: T,
|
|
appService: AppService,
|
|
): Promise<string | null> => queueReplicaBinaryUpload('dictionary', dict, appService);
|