mirror of
https://github.com/panatgithub/AnkiHeadingSync.git
synced 2026-07-22 06:51:43 +00:00
fix(release): clean remaining pre-release state issues
中文: 修复 pendingWriteBack 持久化加载,丢弃旧 Semantic QA 状态,并修正发布链接与多级列表问答文案。 English: Preserves pendingWriteBack state loading, drops old Semantic QA state, and fixes release links and QA Group wording.
This commit is contained in:
parent
a9033664b8
commit
98d64dac21
9 changed files with 452 additions and 45 deletions
|
|
@ -6,7 +6,7 @@ Anki Heading Sync is an Obsidian desktop plugin for syncing Markdown heading blo
|
|||
|
||||
- Sync basic Q&A cards from heading blocks
|
||||
- Sync cloze cards from heading blocks with cloze-specific recognition
|
||||
- Sync QA Group list blocks into managed group notes
|
||||
- Sync QA Group list blocks into multi-level list QA notes
|
||||
- Sync the current file or all in-scope files in the vault
|
||||
- Write Anki IDs and GI markers back into Markdown after sync
|
||||
- Support deck routing, Obsidian backlinks, tag sync, and current-file reset
|
||||
|
|
|
|||
53
docs/prerelease-remaining-fixes-decisions.md
Normal file
53
docs/prerelease-remaining-fixes-decisions.md
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
# Pre-release Remaining Fixes Decisions
|
||||
|
||||
## Pending writeback validation rules
|
||||
|
||||
- Preserve modern `PendingWriteBackState` entries during load only when all required fields are valid.
|
||||
- Required fields:
|
||||
- `filePath`: non-empty string
|
||||
- `blockStartLine`: positive integer
|
||||
- `expectedFileHash`: non-empty string
|
||||
- `targetMarker`: non-empty string
|
||||
- `rawBlockHash`: non-empty string
|
||||
- `targetNoteId`: positive integer
|
||||
- `markerKind` rules:
|
||||
- `undefined` is valid
|
||||
- `"card-id"` is valid
|
||||
- `"group-gi"` is valid
|
||||
- any other value is invalid and drops the item
|
||||
- `targetGroupId` rules:
|
||||
- if absent, keep normal validation result
|
||||
- if present, it must be a non-empty string
|
||||
- malformed `targetGroupId` drops the item
|
||||
|
||||
## Legacy pending dropping rule
|
||||
|
||||
- Do not migrate legacy pending entries that only contain legacy identity fields such as `cardId` or `noteId`.
|
||||
- Any pending entry missing `targetNoteId` is dropped.
|
||||
- Do not synthesize or coerce malformed pending items into the modern shape.
|
||||
|
||||
## Old Semantic QA state dropping rule
|
||||
|
||||
- Current supported loaded card state remains limited to:
|
||||
- `basic`
|
||||
- `cloze`
|
||||
- Removed card types such as `semantic-qa` are dropped during load.
|
||||
- Unknown removed card types are also dropped during load.
|
||||
- Dropped cards are not written into `state.cards`.
|
||||
- File `noteIds` are rebuilt from the filtered migrated card map so dropped cards cannot leave dangling note IDs behind.
|
||||
|
||||
## README wording decision
|
||||
|
||||
- Replace `managed group notes` with current-product wording centered on multi-level list QA cards/notes.
|
||||
- Keep the README change focused to the active feature description only.
|
||||
|
||||
## i18n wording decision
|
||||
|
||||
- Remove user-facing managed-model narrative from active settings copy.
|
||||
- Replace old wording such as `QA Group 12`, `ObsiAnki QA Group 12`, `Managed note type`, and `Managed model contract` with current route wording for multi-level list QA notes.
|
||||
- Keep internal model-definition constants unchanged unless a user-facing behavior requires a rename.
|
||||
|
||||
## Intentional deviations from the plan
|
||||
|
||||
- Internal technical constants such as `QA_GROUP_MODEL_NAME` are not renamed in this round because the requested cleanup is limited to active user-facing wording and persistence behavior.
|
||||
- Historical audit docs are left untouched even if they still mention old QA Group 12 or Semantic QA history.
|
||||
90
docs/prerelease-remaining-fixes-gap-report.md
Normal file
90
docs/prerelease-remaining-fixes-gap-report.md
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
# Pre-release Remaining Fixes Gap Report
|
||||
|
||||
## Scope
|
||||
|
||||
This report records the real repository state on 2026-04-27 before implementing the remaining pre-release fixes.
|
||||
|
||||
## Pending writeback load behavior
|
||||
|
||||
- Runtime state shape already defines modern `PendingWriteBackState` in `src/domain/manual-sync/entities/PluginState.ts` with:
|
||||
- `filePath`
|
||||
- `blockStartLine`
|
||||
- `expectedFileHash`
|
||||
- `targetMarker`
|
||||
- `rawBlockHash`
|
||||
- `targetNoteId`
|
||||
- optional `markerKind`
|
||||
- optional `targetGroupId`
|
||||
- `src/application/services/MarkdownWriteBackService.ts` already emits modern pending entries for both:
|
||||
- card ID marker writes with `markerKind: "card-id"`
|
||||
- group GI writes with `markerKind: "group-gi"` and `targetGroupId`
|
||||
- `src/infrastructure/persistence/DataJsonPluginStateRepository.ts` currently drops all persisted pending entries on load by returning `pendingWriteBack: []` from `migratePluginState()`.
|
||||
- Result:
|
||||
- writeback conflicts are persisted during a session
|
||||
- after Obsidian restart, the recovery queue is lost
|
||||
|
||||
## Malformed and legacy pending handling
|
||||
|
||||
- `DataJsonPluginStateRepository.ts` declares `LegacyPendingWriteBackState`, but current load migration does not validate or preserve any pending items.
|
||||
- Current repository behavior therefore treats all pending shapes the same way:
|
||||
- valid modern items are dropped
|
||||
- malformed items are dropped
|
||||
- legacy `cardId` / `noteId` only items are dropped
|
||||
- There is currently no explicit validation contract for pending items during load.
|
||||
|
||||
## Old Semantic QA state behavior
|
||||
|
||||
- `DataJsonPluginStateRepository.ts` still migrates every loaded card through `migrateCardState()`.
|
||||
- `sanitizeCardType()` currently keeps `"cloze"` and converts every other value to `"basic"`.
|
||||
- Result:
|
||||
- removed `semantic-qa` cards are silently converted to `basic`
|
||||
- converted cards remain in `state.cards`
|
||||
- converted cards still contribute note IDs to `files[*].noteIds` through `collectMigratedFileNoteIds()`
|
||||
- Current test coverage in `src/infrastructure/persistence/DataJsonPluginStateRepository.test.ts` explicitly asserts this silent conversion.
|
||||
|
||||
## Release link status
|
||||
|
||||
- `manifest.json` currently has:
|
||||
- `author: "Dusk"`
|
||||
- `authorUrl: "https://github.com/panAtGitHub"`
|
||||
- Required repository link is not yet applied.
|
||||
|
||||
## README wording status
|
||||
|
||||
- `README.md` still says:
|
||||
- `Sync QA Group list blocks into managed group notes`
|
||||
- This is stale product language relative to the current user-facing route.
|
||||
|
||||
## i18n legacy wording status
|
||||
|
||||
- Active user-facing wording in `src/presentation/i18n/messages/en.ts` and `src/presentation/i18n/messages/zh.ts` still includes:
|
||||
- `QA Group 12`
|
||||
- `ObsiAnki QA Group 12`
|
||||
- `Managed note type`
|
||||
- `Managed model contract`
|
||||
- `托管笔记类型`
|
||||
- `托管模型约束`
|
||||
- These messages are still part of the live settings UI.
|
||||
- Current active text suggests a fixed managed-model narrative that does not match the intended release wording.
|
||||
|
||||
## Build and sync script status
|
||||
|
||||
- `scripts/stage-plugin-dist.mjs` stages:
|
||||
- `manifest.json`
|
||||
- `versions.json`
|
||||
- optional `styles.css`
|
||||
- generated package `README.md`
|
||||
- `scripts/sync-plugin-dist.mjs` copies from `dist/plugin`:
|
||||
- `main.js`
|
||||
- `manifest.json`
|
||||
- optional `styles.css`
|
||||
- The generic sync script does not delete `data.json`, but repository instructions still require strict file selection during final vault sync.
|
||||
|
||||
## Exact files likely to change
|
||||
|
||||
- `src/infrastructure/persistence/DataJsonPluginStateRepository.ts`
|
||||
- `src/infrastructure/persistence/DataJsonPluginStateRepository.test.ts`
|
||||
- `manifest.json`
|
||||
- `README.md`
|
||||
- `src/presentation/i18n/messages/en.ts`
|
||||
- `src/presentation/i18n/messages/zh.ts`
|
||||
40
main.js
40
main.js
File diff suppressed because one or more lines are too long
|
|
@ -5,6 +5,6 @@
|
|||
"minAppVersion": "1.5.0",
|
||||
"description": "Focused heading-based Obsidian to Anki sync plugin.",
|
||||
"author": "Dusk",
|
||||
"authorUrl": "https://github.com/panAtGitHub",
|
||||
"authorUrl": "https://github.com/panAtGitHub/AnkiHeadingSync",
|
||||
"isDesktopOnly": true
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import type { PluginDataStore } from "@/application/ports/PluginDataStore";
|
||||
import { createEmptyPluginState } from "@/domain/manual-sync/entities/PluginState";
|
||||
import { createEmptyPluginState, type PendingWriteBackState } from "@/domain/manual-sync/entities/PluginState";
|
||||
import { hashString } from "@/domain/shared/hash";
|
||||
|
||||
import { DataJsonPluginStateRepository } from "./DataJsonPluginStateRepository";
|
||||
|
|
@ -115,7 +115,116 @@ describe("DataJsonPluginStateRepository", () => {
|
|||
expect(loaded.pendingWriteBack).toEqual([]);
|
||||
});
|
||||
|
||||
it("normalizes removed semantic QA card types to basic when loading modern plugin state", async () => {
|
||||
it("preserves valid modern pending writeback entries across save and load", async () => {
|
||||
const store = new InMemoryPluginDataStore();
|
||||
const repository = new DataJsonPluginStateRepository(store);
|
||||
const pendingWriteBack: PendingWriteBackState[] = [
|
||||
createPendingWriteBackState({
|
||||
filePath: "notes/card.md",
|
||||
blockStartLine: 2,
|
||||
expectedFileHash: "hash-card",
|
||||
targetMarker: "<!--ID: 41-->",
|
||||
rawBlockHash: "raw-card",
|
||||
targetNoteId: 41,
|
||||
markerKind: "card-id",
|
||||
}),
|
||||
createPendingWriteBackState({
|
||||
filePath: "notes/group.md",
|
||||
blockStartLine: 6,
|
||||
expectedFileHash: "hash-group",
|
||||
targetMarker: "<!--GI:n=42;i=item_a:1;f=2,3,4-->",
|
||||
rawBlockHash: "raw-group",
|
||||
targetNoteId: 42,
|
||||
markerKind: "group-gi",
|
||||
targetGroupId: "group-1",
|
||||
}),
|
||||
createPendingWriteBackState({
|
||||
filePath: "notes/legacy-shape-but-modern-fields.md",
|
||||
blockStartLine: 9,
|
||||
expectedFileHash: "hash-no-kind",
|
||||
targetMarker: "<!--ID: 43-->",
|
||||
rawBlockHash: "raw-no-kind",
|
||||
targetNoteId: 43,
|
||||
}),
|
||||
];
|
||||
|
||||
await repository.save({
|
||||
files: {},
|
||||
cards: {},
|
||||
pendingWriteBack,
|
||||
});
|
||||
|
||||
await expect(repository.load()).resolves.toMatchObject({
|
||||
pendingWriteBack,
|
||||
});
|
||||
});
|
||||
|
||||
it("drops malformed and legacy pending writeback entries during load", async () => {
|
||||
const repository = new DataJsonPluginStateRepository(new InMemoryPluginDataStore({
|
||||
pluginState: {
|
||||
files: {},
|
||||
cards: {},
|
||||
pendingWriteBack: [
|
||||
{
|
||||
filePath: "notes/missing-note-id.md",
|
||||
blockStartLine: 1,
|
||||
expectedFileHash: "hash",
|
||||
targetMarker: "<!--ID: 1-->",
|
||||
rawBlockHash: "raw",
|
||||
},
|
||||
{
|
||||
filePath: "notes/wrong-line.md",
|
||||
blockStartLine: "1" as never,
|
||||
expectedFileHash: "hash",
|
||||
targetMarker: "<!--ID: 2-->",
|
||||
rawBlockHash: "raw",
|
||||
targetNoteId: 2,
|
||||
},
|
||||
{
|
||||
filePath: "notes/invalid-kind.md",
|
||||
blockStartLine: 1,
|
||||
expectedFileHash: "hash",
|
||||
targetMarker: "<!--ID: 3-->",
|
||||
rawBlockHash: "raw",
|
||||
targetNoteId: 3,
|
||||
markerKind: "other" as never,
|
||||
},
|
||||
{
|
||||
filePath: "notes/invalid-group-id.md",
|
||||
blockStartLine: 1,
|
||||
expectedFileHash: "hash",
|
||||
targetMarker: "<!--GI:n=4;i=item_a:1;f=2-->",
|
||||
rawBlockHash: "raw",
|
||||
targetNoteId: 4,
|
||||
markerKind: "group-gi",
|
||||
targetGroupId: "",
|
||||
},
|
||||
{
|
||||
filePath: "notes/legacy-only.md",
|
||||
cardId: "ahs_1",
|
||||
noteId: 5,
|
||||
expectedFileHash: "hash",
|
||||
targetMarker: "<!--AHS:card=ahs_1 note=5-->",
|
||||
rawBlockHash: "raw",
|
||||
},
|
||||
{
|
||||
filePath: "notes/missing-hash.md",
|
||||
blockStartLine: 1,
|
||||
expectedFileHash: "",
|
||||
targetMarker: "<!--ID: 6-->",
|
||||
rawBlockHash: "raw",
|
||||
targetNoteId: 6,
|
||||
},
|
||||
],
|
||||
} as never,
|
||||
}));
|
||||
|
||||
await expect(repository.load()).resolves.toMatchObject({
|
||||
pendingWriteBack: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("drops removed semantic QA card state instead of converting it to basic", async () => {
|
||||
const rawBlockText = ["semantic-qa:核心产品::1", "城市更新", "核心产品", "百人会"].join("\n");
|
||||
const repository = new DataJsonPluginStateRepository(new InMemoryPluginDataStore({
|
||||
pluginState: {
|
||||
|
|
@ -159,7 +268,94 @@ describe("DataJsonPluginStateRepository", () => {
|
|||
|
||||
const loaded = await repository.load();
|
||||
|
||||
expect(loaded.cards["52"]?.cardType).toBe("basic");
|
||||
expect(loaded.cards["52"]?.backlinkHeadingText).toBe("城市更新 #anki-list-qa");
|
||||
expect(loaded.cards["52"]).toBeUndefined();
|
||||
expect(loaded.files["notes/example.md"]?.noteIds).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps supported basic and cloze card states while filtering dangling file noteIds", async () => {
|
||||
const basicRawBlockText = ["#### Prompt", "Answer"].join("\n");
|
||||
const clozeRawBlockText = ["##### Cloze", "{{c1::Body}}"].join("\n");
|
||||
const repository = new DataJsonPluginStateRepository(new InMemoryPluginDataStore({
|
||||
pluginState: {
|
||||
files: {
|
||||
"notes/example.md": {
|
||||
filePath: "notes/example.md",
|
||||
fileHash: "hash",
|
||||
fileStamp: "1:1",
|
||||
lastIndexedAt: 1,
|
||||
noteIds: [41, 42, 99],
|
||||
},
|
||||
},
|
||||
cards: {
|
||||
"41": {
|
||||
noteId: 41,
|
||||
filePath: "notes/example.md",
|
||||
heading: "Prompt",
|
||||
backlinkHeadingText: "Prompt",
|
||||
headingLevel: 4,
|
||||
bodyMarkdown: "Answer",
|
||||
cardType: "basic",
|
||||
blockStartOffset: 0,
|
||||
blockEndOffset: basicRawBlockText.length,
|
||||
blockStartLine: 1,
|
||||
bodyStartLine: 2,
|
||||
blockEndLine: 2,
|
||||
contentEndLine: 2,
|
||||
rawBlockText: basicRawBlockText,
|
||||
rawBlockHash: hashString(basicRawBlockText),
|
||||
renderConfigHash: "render-basic",
|
||||
deck: "notes",
|
||||
deckWarnings: [],
|
||||
tagsHint: [],
|
||||
lastSyncedAt: 1,
|
||||
orphan: false,
|
||||
},
|
||||
"42": {
|
||||
noteId: 42,
|
||||
filePath: "notes/example.md",
|
||||
heading: "Cloze",
|
||||
backlinkHeadingText: "Cloze",
|
||||
headingLevel: 5,
|
||||
bodyMarkdown: "{{c1::Body}}",
|
||||
cardType: "cloze",
|
||||
blockStartOffset: 0,
|
||||
blockEndOffset: clozeRawBlockText.length,
|
||||
blockStartLine: 4,
|
||||
bodyStartLine: 5,
|
||||
blockEndLine: 5,
|
||||
contentEndLine: 5,
|
||||
rawBlockText: clozeRawBlockText,
|
||||
rawBlockHash: hashString(clozeRawBlockText),
|
||||
renderConfigHash: "render-cloze",
|
||||
deck: "notes",
|
||||
deckWarnings: [],
|
||||
tagsHint: [],
|
||||
lastSyncedAt: 1,
|
||||
orphan: false,
|
||||
},
|
||||
},
|
||||
pendingWriteBack: [],
|
||||
},
|
||||
}));
|
||||
|
||||
const loaded = await repository.load();
|
||||
|
||||
expect(Object.keys(loaded.cards)).toEqual(["41", "42"]);
|
||||
expect(loaded.cards["41"]?.cardType).toBe("basic");
|
||||
expect(loaded.cards["42"]?.cardType).toBe("cloze");
|
||||
expect(loaded.files["notes/example.md"]?.noteIds).toEqual([41, 42]);
|
||||
});
|
||||
});
|
||||
|
||||
function createPendingWriteBackState(overrides: Partial<PendingWriteBackState> = {}): PendingWriteBackState {
|
||||
return {
|
||||
filePath: overrides.filePath ?? "notes/example.md",
|
||||
blockStartLine: overrides.blockStartLine ?? 1,
|
||||
expectedFileHash: overrides.expectedFileHash ?? "hash",
|
||||
targetMarker: overrides.targetMarker ?? "<!--ID: 41-->",
|
||||
rawBlockHash: overrides.rawBlockHash ?? "raw-block-hash",
|
||||
targetNoteId: overrides.targetNoteId ?? 41,
|
||||
markerKind: overrides.markerKind,
|
||||
targetGroupId: overrides.targetGroupId,
|
||||
};
|
||||
}
|
||||
|
|
@ -62,6 +62,10 @@ export function migratePluginState(pluginState?: PluginState | LegacyPluginState
|
|||
}
|
||||
|
||||
const nextCard = migrateCardState(rawCard, noteId);
|
||||
if (!nextCard) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const noteKey = toNoteIdKey(noteId);
|
||||
const existingCard = cards[noteKey];
|
||||
if (!existingCard || existingCard.lastSyncedAt <= nextCard.lastSyncedAt) {
|
||||
|
|
@ -99,11 +103,16 @@ export function migratePluginState(pluginState?: PluginState | LegacyPluginState
|
|||
files,
|
||||
cards,
|
||||
groupBlocks,
|
||||
pendingWriteBack: [],
|
||||
pendingWriteBack: migratePendingWriteBack(pluginState.pendingWriteBack),
|
||||
};
|
||||
}
|
||||
|
||||
function migrateCardState(rawCard: LegacyCardState, noteId: number): CardState {
|
||||
function migrateCardState(rawCard: LegacyCardState, noteId: number): CardState | null {
|
||||
const cardType = sanitizeCardType(rawCard.cardType);
|
||||
if (!cardType) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
noteId,
|
||||
filePath: typeof rawCard.filePath === "string" ? rawCard.filePath : "",
|
||||
|
|
@ -113,7 +122,7 @@ function migrateCardState(rawCard: LegacyCardState, noteId: number): CardState {
|
|||
: (typeof rawCard.heading === "string" ? rawCard.heading : ""),
|
||||
headingLevel: typeof rawCard.headingLevel === "number" ? rawCard.headingLevel : 1,
|
||||
bodyMarkdown: typeof rawCard.bodyMarkdown === "string" ? rawCard.bodyMarkdown : "",
|
||||
cardType: sanitizeCardType(rawCard.cardType),
|
||||
cardType,
|
||||
blockStartOffset: typeof rawCard.blockStartOffset === "number" ? rawCard.blockStartOffset : 0,
|
||||
blockEndOffset: typeof rawCard.blockEndOffset === "number" ? rawCard.blockEndOffset : 0,
|
||||
blockStartLine: typeof rawCard.blockStartLine === "number" ? rawCard.blockStartLine : 1,
|
||||
|
|
@ -134,12 +143,12 @@ function migrateCardState(rawCard: LegacyCardState, noteId: number): CardState {
|
|||
};
|
||||
}
|
||||
|
||||
function sanitizeCardType(value: unknown): CardState["cardType"] {
|
||||
if (value === "cloze") {
|
||||
function sanitizeCardType(value: unknown): CardState["cardType"] | undefined {
|
||||
if (value === "basic" || value === "cloze") {
|
||||
return value;
|
||||
}
|
||||
|
||||
return "basic";
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function migrateGroupBlockState(rawGroupBlock: LegacyGroupBlockState, groupId: string): GroupBlockState | null {
|
||||
|
|
@ -230,6 +239,53 @@ function collectMigratedFileNoteIds(
|
|||
return Array.from(noteIds);
|
||||
}
|
||||
|
||||
function migratePendingWriteBack(pendingWriteBack: LegacyPendingWriteBackState[] | undefined): PendingWriteBackState[] {
|
||||
if (!Array.isArray(pendingWriteBack)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return pendingWriteBack.flatMap((rawPending) => {
|
||||
const nextPending = migratePendingWriteBackState(rawPending);
|
||||
return nextPending ? [nextPending] : [];
|
||||
});
|
||||
}
|
||||
|
||||
function migratePendingWriteBackState(rawPending: LegacyPendingWriteBackState): PendingWriteBackState | null {
|
||||
const filePath = sanitizeNonEmptyString(rawPending.filePath);
|
||||
const blockStartLine = sanitizePositiveInteger(rawPending.blockStartLine);
|
||||
const expectedFileHash = sanitizeNonEmptyString(rawPending.expectedFileHash);
|
||||
const targetMarker = sanitizeNonEmptyString(rawPending.targetMarker);
|
||||
const rawBlockHash = sanitizeNonEmptyString(rawPending.rawBlockHash);
|
||||
const targetNoteId = sanitizeNoteId(rawPending.targetNoteId);
|
||||
|
||||
if (!filePath || !blockStartLine || !expectedFileHash || !targetMarker || !rawBlockHash || !targetNoteId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const markerKind = sanitizeMarkerKind(rawPending.markerKind);
|
||||
if (rawPending.markerKind !== undefined && !markerKind) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const targetGroupId = rawPending.targetGroupId === undefined
|
||||
? undefined
|
||||
: sanitizeNonEmptyString(rawPending.targetGroupId);
|
||||
if (rawPending.targetGroupId !== undefined && !targetGroupId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
filePath,
|
||||
blockStartLine,
|
||||
expectedFileHash,
|
||||
targetMarker,
|
||||
rawBlockHash,
|
||||
targetNoteId,
|
||||
markerKind,
|
||||
targetGroupId,
|
||||
};
|
||||
}
|
||||
|
||||
function collectMigratedFileGroupIds(
|
||||
rawFile: LegacyFileState,
|
||||
rawGroupBlocks: Record<string, LegacyGroupBlockState>,
|
||||
|
|
@ -243,4 +299,16 @@ function collectMigratedFileGroupIds(
|
|||
|
||||
function sanitizeNoteId(value: unknown): number | undefined {
|
||||
return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : undefined;
|
||||
}
|
||||
|
||||
function sanitizePositiveInteger(value: unknown): number | undefined {
|
||||
return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : undefined;
|
||||
}
|
||||
|
||||
function sanitizeNonEmptyString(value: unknown): string | undefined {
|
||||
return typeof value === "string" && value.trim().length > 0 ? value : undefined;
|
||||
}
|
||||
|
||||
function sanitizeMarkerKind(value: unknown): PendingWriteBackState["markerKind"] | undefined {
|
||||
return value === "card-id" || value === "group-gi" ? value : undefined;
|
||||
}
|
||||
|
|
@ -33,14 +33,14 @@ export const en = {
|
|||
},
|
||||
},
|
||||
qaGroup: {
|
||||
title: "QA Group 12",
|
||||
title: "QA Group list notes",
|
||||
marker: {
|
||||
name: "QA Group marker",
|
||||
desc: "When a QA heading ends with this hashtag marker, the whole heading block syncs as one ObsiAnki QA Group 12 note.",
|
||||
desc: "When a QA heading ends with this hashtag marker, the whole heading block syncs as one QA Group list note.",
|
||||
placeholder: "#anki-list",
|
||||
},
|
||||
managedNoteType: "Managed note type: {{modelName}}. QA Group sync writes Stem / GroupId / Src / S01..S12 directly. The same note type can also appear in the field mapping panels below if you want to reuse it for other routes.",
|
||||
managedModelContract: "Managed model contract: {{fieldCount}} fields and {{templateCount}} templates are checked automatically during sync.",
|
||||
managedNoteType: "Current note type: {{modelName}}. This route syncs QA Group list notes using the field mapping shown below.",
|
||||
managedModelContract: "Current note type summary: {{fieldCount}} fields and {{templateCount}} templates are available for this route.",
|
||||
},
|
||||
syncOptions: {
|
||||
addObsidianBacklink: {
|
||||
|
|
@ -204,9 +204,9 @@ export const en = {
|
|||
advancedHint: "Advanced: keep AnkiConnect URL here without promoting it to a separate status card.",
|
||||
markerPlaceholder: "/",
|
||||
fieldsUnavailable: "-- Read fields first --",
|
||||
autoManagedField: "Managed automatically",
|
||||
autoManagedField: "Written automatically",
|
||||
autoComposedAnswer: "Auto composed",
|
||||
qaGroupManagedNoteType: "{{modelName}} (managed automatically)",
|
||||
qaGroupManagedNoteType: "{{modelName}} (for QA Group list notes)",
|
||||
statusLabel: "Status: ",
|
||||
cacheEmpty: "There are no cached Anki note templates yet. Read the Anki template config manually.",
|
||||
cacheSummary: "Currently using {{noteTypeCount}} cached note templates with {{configuredCount}} selected card modes configured.",
|
||||
|
|
|
|||
|
|
@ -31,14 +31,14 @@ export const zh = {
|
|||
},
|
||||
},
|
||||
qaGroup: {
|
||||
title: "QA Group 12",
|
||||
title: "问答题(多级列表形式)",
|
||||
marker: {
|
||||
name: "QA Group 标记",
|
||||
desc: "当 QA 标题以这个 hashtag 标记结尾时,整个标题块会作为一张 ObsiAnki QA Group 12 笔记同步。",
|
||||
desc: "当 QA 标题以这个 hashtag 标记结尾时,整个标题块会同步为一条多级列表问答笔记。",
|
||||
placeholder: "#anki-list",
|
||||
},
|
||||
managedNoteType: "托管笔记类型:{{modelName}}。QA Group 同步会直接写入 Stem / GroupId / Src / S01..S12。如果你想在其他同步路径里复用它,它也会出现在下面的字段映射面板中。",
|
||||
managedModelContract: "托管模型约束:同步时会自动检查 {{fieldCount}} 个字段和 {{templateCount}} 个模板。",
|
||||
managedNoteType: "当前笔记类型:{{modelName}}。这条路线会按下方字段映射同步多级列表问答内容。",
|
||||
managedModelContract: "当前笔记类型摘要:这条路线可用 {{fieldCount}} 个字段和 {{templateCount}} 个模板。",
|
||||
},
|
||||
syncOptions: {
|
||||
addObsidianBacklink: {
|
||||
|
|
@ -202,9 +202,9 @@ export const zh = {
|
|||
advancedHint: "高级项:将 AnkiConnect URL 保留在这里,不单独做连接状态卡片。",
|
||||
markerPlaceholder: "/",
|
||||
fieldsUnavailable: "-- 请先读取字段 --",
|
||||
autoManagedField: "自动维护",
|
||||
autoManagedField: "自动写入",
|
||||
autoComposedAnswer: "自动拼接",
|
||||
qaGroupManagedNoteType: "{{modelName}}(自动维护)",
|
||||
qaGroupManagedNoteType: "{{modelName}}(用于多级列表问答)",
|
||||
statusLabel: "状态:",
|
||||
cacheEmpty: "当前没有已缓存的 Anki 笔记模板。请手动读取 Anki 里的模板配置。",
|
||||
cacheSummary: "当前使用缓存的 {{noteTypeCount}} 个笔记模板,已选择并配置 {{configuredCount}} 个卡片模式。",
|
||||
|
|
|
|||
Loading…
Reference in a new issue