|
|
@ -1,11 +1,6 @@ |
|
|
|
import axios, { AxiosInstance } from 'axios'; |
|
|
|
|
|
|
|
import { IAnkiCard } from './anki'; |
|
|
|
|
|
|
|
export interface IAnkiConnectResponse<K extends keyof IAnkiConnectActions> { |
|
|
|
result: IAnkiConnectActions[K]; |
|
|
|
error: string | null; |
|
|
|
} |
|
|
|
import { IAnkiCard, IAnkiQuery } from './anki'; |
|
|
|
|
|
|
|
export interface IAnkiConnectActions |
|
|
|
extends Record< |
|
|
@ -37,31 +32,7 @@ export interface IAnkiConnectActions |
|
|
|
|
|
|
|
// Media Actions
|
|
|
|
storeMediaFile: { |
|
|
|
params: ( |
|
|
|
| { |
|
|
|
/** |
|
|
|
* Specified base64-encoded contents |
|
|
|
*/ |
|
|
|
data: string; |
|
|
|
} |
|
|
|
| { |
|
|
|
path: string; |
|
|
|
} |
|
|
|
| { |
|
|
|
url: string; |
|
|
|
} |
|
|
|
) & { |
|
|
|
/** |
|
|
|
* To prevent Anki from removing files not used by any cards (e.g. for configuration files), |
|
|
|
* prefix the filename with an underscore. |
|
|
|
*/ |
|
|
|
filename: string; |
|
|
|
/** |
|
|
|
* Any existing file with the same name is deleted by default. |
|
|
|
* Set `deleteExisting` to `false` to prevent that by letting Anki give the new file a non-conflicting name. |
|
|
|
*/ |
|
|
|
deleteExisting?: boolean; |
|
|
|
}; |
|
|
|
params: IAnkiConnectMedia; |
|
|
|
result: string; |
|
|
|
}; |
|
|
|
retrieveMediaFile: { |
|
|
@ -222,8 +193,112 @@ export interface IAnkiConnectActions |
|
|
|
|
|
|
|
// Note Actions
|
|
|
|
addNote: { |
|
|
|
params: {}; |
|
|
|
result: number; |
|
|
|
params: { |
|
|
|
note: IAnkiConnectNote; |
|
|
|
}; |
|
|
|
result: number | null; |
|
|
|
}; |
|
|
|
addNotes: { |
|
|
|
params: { |
|
|
|
notes: IAnkiConnectNote[]; |
|
|
|
}; |
|
|
|
result: (number | null)[]; |
|
|
|
}; |
|
|
|
canAddNotes: { |
|
|
|
params: { |
|
|
|
notes: IAnkiConnectNote[]; |
|
|
|
}; |
|
|
|
result: boolean[]; |
|
|
|
}; |
|
|
|
updateNoteFields: { |
|
|
|
params: { |
|
|
|
note: { |
|
|
|
id: number; |
|
|
|
fields: { |
|
|
|
[fieldName: string]: string; |
|
|
|
}; |
|
|
|
audio?: IAnkiConnectMediaInNote[]; |
|
|
|
video?: IAnkiConnectMediaInNote[]; |
|
|
|
picture?: IAnkiConnectMediaInNote[]; |
|
|
|
}; |
|
|
|
}; |
|
|
|
result: null; |
|
|
|
}; |
|
|
|
addTags: { |
|
|
|
params: { |
|
|
|
notes: number[]; |
|
|
|
/** |
|
|
|
* The docs don't make it clear if tags are space-separated. |
|
|
|
* |
|
|
|
* @link https://foosoft.net/projects/anki-connect/#miscellaneous-actions
|
|
|
|
*/ |
|
|
|
tags: string; |
|
|
|
}; |
|
|
|
result: null; |
|
|
|
}; |
|
|
|
removeTags: { |
|
|
|
params: { |
|
|
|
notes: number[]; |
|
|
|
/** |
|
|
|
* The docs don't make it clear if tags are space-separated. |
|
|
|
* |
|
|
|
* @link https://foosoft.net/projects/anki-connect/#miscellaneous-actions
|
|
|
|
*/ |
|
|
|
tags: string; |
|
|
|
}; |
|
|
|
result: null; |
|
|
|
}; |
|
|
|
getTags: { |
|
|
|
result: string[]; |
|
|
|
}; |
|
|
|
cleanUnusedTags: { |
|
|
|
result: null; |
|
|
|
}; |
|
|
|
replaceTags: { |
|
|
|
params: { |
|
|
|
notes: number[]; |
|
|
|
tag_to_replace: string; |
|
|
|
replace_with_tag: string; |
|
|
|
}; |
|
|
|
result: null; |
|
|
|
}; |
|
|
|
replaceTagsInAllNotes: { |
|
|
|
params: { |
|
|
|
tag_to_replace: string; |
|
|
|
replace_with_tag: string; |
|
|
|
}; |
|
|
|
result: null; |
|
|
|
}; |
|
|
|
findNotes: { |
|
|
|
params: { |
|
|
|
query: IAnkiQuery; |
|
|
|
}; |
|
|
|
result: number[]; |
|
|
|
}; |
|
|
|
notesInfo: { |
|
|
|
params: { |
|
|
|
notes: number[]; |
|
|
|
}; |
|
|
|
result: { |
|
|
|
noteId: number; |
|
|
|
modelName: string; |
|
|
|
tags: string[]; |
|
|
|
fields: { |
|
|
|
[fieldName: string]: { |
|
|
|
value: string; |
|
|
|
ord: number; |
|
|
|
}; |
|
|
|
}; |
|
|
|
}[]; |
|
|
|
}; |
|
|
|
deleteNotes: { |
|
|
|
params: { |
|
|
|
notes: number[]; |
|
|
|
}; |
|
|
|
result: null; |
|
|
|
}; |
|
|
|
removeEmptyNotes: { |
|
|
|
result: null; |
|
|
|
}; |
|
|
|
} |
|
|
|
|
|
|
@ -236,6 +311,86 @@ export interface IAnkiConnectCardTemplate { |
|
|
|
Back: string; |
|
|
|
} |
|
|
|
|
|
|
|
export interface IAnkiConnectNote { |
|
|
|
deckName: string; |
|
|
|
modelName: string; |
|
|
|
fields: { |
|
|
|
[fieldName: string]: string; |
|
|
|
}; |
|
|
|
options?: { |
|
|
|
/** |
|
|
|
* The allowDuplicate member inside options group can be set to true to enable adding duplicate cards. |
|
|
|
* Normally duplicate cards can not be added and trigger exception. |
|
|
|
*/ |
|
|
|
allowDuplicate: boolean; |
|
|
|
/** |
|
|
|
* The duplicateScope member inside options can be used to specify the scope for which duplicates are checked. |
|
|
|
* A value of "deck" will only check for duplicates in the target deck; |
|
|
|
* any other value will check the entire collection. |
|
|
|
*/ |
|
|
|
duplicateScope?: string; |
|
|
|
duplicateScopeOptions?: { |
|
|
|
/** |
|
|
|
* Specify which deck to use for checking duplicates in. |
|
|
|
* If undefined or null, the target deck will be used. |
|
|
|
*/ |
|
|
|
deckName?: string | null; |
|
|
|
/** |
|
|
|
* Change whether or not duplicate cards are checked in child decks. |
|
|
|
* The default value is false. |
|
|
|
*/ |
|
|
|
checkChildren?: boolean; |
|
|
|
/** |
|
|
|
* Specifies whether duplicate checks are performed across all note types. |
|
|
|
* The default value is false. |
|
|
|
*/ |
|
|
|
checkAllModel?: boolean; |
|
|
|
}; |
|
|
|
}; |
|
|
|
tags: string[]; |
|
|
|
audio?: IAnkiConnectMediaInNote[]; |
|
|
|
video?: IAnkiConnectMediaInNote[]; |
|
|
|
picture?: IAnkiConnectMediaInNote[]; |
|
|
|
} |
|
|
|
|
|
|
|
export type IAnkiConnectMedia = ( |
|
|
|
| { |
|
|
|
/** |
|
|
|
* Specified base64-encoded contents |
|
|
|
*/ |
|
|
|
data: string; |
|
|
|
} |
|
|
|
| { |
|
|
|
path: string; |
|
|
|
} |
|
|
|
| { |
|
|
|
url: string; |
|
|
|
} |
|
|
|
) & { |
|
|
|
/** |
|
|
|
* To prevent Anki from removing files not used by any cards (e.g. for configuration files), |
|
|
|
* prefix the filename with an underscore. |
|
|
|
*/ |
|
|
|
filename: string; |
|
|
|
/** |
|
|
|
* Any existing file with the same name is deleted by default. |
|
|
|
* Set `deleteExisting` to `false` to prevent that by letting Anki give the new file a non-conflicting name. |
|
|
|
*/ |
|
|
|
deleteExisting?: boolean; |
|
|
|
}; |
|
|
|
|
|
|
|
export type IAnkiConnectMediaInNote = IAnkiConnectMedia & { |
|
|
|
/** |
|
|
|
* The skipHash field can be optionally provided to skip the inclusion of files with an MD5 hash that matches the provided value. |
|
|
|
*/ |
|
|
|
skipHash?: string; |
|
|
|
/** |
|
|
|
* The fields member is a list of fields that should play audio or video, |
|
|
|
* or show a picture when the card is displayed in Anki. |
|
|
|
*/ |
|
|
|
fields?: string[]; |
|
|
|
}; |
|
|
|
|
|
|
|
export class AnkiConnect { |
|
|
|
$axios: AxiosInstance; |
|
|
|
|
|
|
@ -248,31 +403,8 @@ export class AnkiConnect { |
|
|
|
async api<A extends keyof IAnkiConnectActions>( |
|
|
|
action: A, |
|
|
|
params: IAnkiConnectActions[A]['params'], |
|
|
|
version?: number, |
|
|
|
): Promise<IAnkiConnectActions[A]['result']>; |
|
|
|
|
|
|
|
async api<Results extends IAnkiConnectResponse<any>[]>( |
|
|
|
action: 'multi', |
|
|
|
params: { |
|
|
|
actions: Array< |
|
|
|
{ |
|
|
|
[A in keyof IAnkiConnectActions]: { |
|
|
|
action: A; |
|
|
|
params: IAnkiConnectActions[A]['params']; |
|
|
|
}; |
|
|
|
}[keyof IAnkiConnectActions] |
|
|
|
>; |
|
|
|
}, |
|
|
|
version?: number, |
|
|
|
): Promise<{ |
|
|
|
result: Results; |
|
|
|
}>; |
|
|
|
|
|
|
|
async api<A extends keyof IAnkiConnectActions | 'multi'>( |
|
|
|
action: A, |
|
|
|
params: unknown, |
|
|
|
version = this.version, |
|
|
|
) { |
|
|
|
): Promise<IAnkiConnectActions[A]['result']> { |
|
|
|
const { data: response } = await this.$axios.post<{ |
|
|
|
result: any; |
|
|
|
error: string | null; |
|
|
@ -293,4 +425,46 @@ export class AnkiConnect { |
|
|
|
|
|
|
|
return response.result; |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Strongly-typed usage is via `[string, tuple]`: |
|
|
|
* |
|
|
|
* ```typescript
|
|
|
|
* await new AnkiConnect().multi<['findNotes', 'getTags']>({ |
|
|
|
* actions: [ |
|
|
|
* { |
|
|
|
* action: 'findNotes', |
|
|
|
* params: { |
|
|
|
* query: 'added:1', |
|
|
|
* }, |
|
|
|
* }, |
|
|
|
* { action: 'getTags' } |
|
|
|
* ] |
|
|
|
* }); |
|
|
|
* ```
|
|
|
|
*/ |
|
|
|
async multi<Actions extends (keyof IAnkiConnectActions)[]>( |
|
|
|
params: { |
|
|
|
actions: { |
|
|
|
[K in keyof Actions]: Actions[K] extends keyof IAnkiConnectActions |
|
|
|
? { |
|
|
|
action: Actions[K]; |
|
|
|
} & ('params' extends keyof IAnkiConnectActions[Actions[K]] |
|
|
|
? { |
|
|
|
params: IAnkiConnectActions[Actions[K]]['params']; |
|
|
|
} |
|
|
|
: {}) |
|
|
|
: never; |
|
|
|
}; |
|
|
|
}, |
|
|
|
version = this.version, |
|
|
|
): Promise<{ |
|
|
|
[K in keyof Actions]: Actions[K] extends keyof IAnkiConnectActions |
|
|
|
? // { result: IAnkiConnectActions[Actions[K]]['result']; error: string | null; }
|
|
|
|
// On try-testing - error fields not returned, unlike in the docs.
|
|
|
|
IAnkiConnectActions[Actions[K]]['result'] |
|
|
|
: never; |
|
|
|
}> { |
|
|
|
return this.api('multi', params, version) as any; |
|
|
|
} |
|
|
|
} |