import axios, { AxiosInstance } from 'axios';
|
|
|
|
import { IAnkiCard } from './anki';
|
|
|
|
export interface IAnkiConnectResponse<K extends keyof IAnkiConnectActions> {
|
|
result: IAnkiConnectActions[K];
|
|
error: string | null;
|
|
}
|
|
|
|
export interface IAnkiConnectActions
|
|
extends Record<
|
|
string,
|
|
{
|
|
params?: Record<string, unknown>;
|
|
result: unknown;
|
|
}
|
|
> {
|
|
// Deck Actions
|
|
deckNames: {
|
|
result: string[];
|
|
};
|
|
getDecks: {
|
|
params: {
|
|
cards: number[];
|
|
};
|
|
result: {
|
|
[deckName: string]: number[];
|
|
};
|
|
};
|
|
changeDeck: {
|
|
params: {
|
|
cards: number[];
|
|
deck: string;
|
|
};
|
|
result: null;
|
|
};
|
|
|
|
// 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;
|
|
};
|
|
result: string;
|
|
};
|
|
retrieveMediaFile: {
|
|
/**
|
|
* Retrieves the base64-encoded contents of the specified file, returning `false` if the file does not exist.
|
|
*/
|
|
params: {
|
|
filename: string;
|
|
};
|
|
result: string | false;
|
|
};
|
|
getMediaFilesNames: {
|
|
/**
|
|
* Gets the names of media files matched the pattern. Returning all names by default.
|
|
*/
|
|
params: {
|
|
pattern: string;
|
|
};
|
|
result: string[];
|
|
};
|
|
deleteMediaFile: {
|
|
params: {
|
|
filename: string;
|
|
};
|
|
result: null;
|
|
};
|
|
|
|
// Miscellaneous Actions
|
|
exportPackage: {
|
|
/**
|
|
* Exports a given deck in .apkg format.
|
|
* Returns true if successful or false otherwise.
|
|
*/
|
|
params: {
|
|
deck: string;
|
|
path: string;
|
|
/**
|
|
* The optional property includeSched (default is false) can be specified to include the cards’ scheduling data.
|
|
*/
|
|
includeSched?: boolean;
|
|
};
|
|
result: boolean;
|
|
};
|
|
importPackage: {
|
|
/**
|
|
* Imports a file in .apkg format into the collection.
|
|
* Returns true if successful or false otherwise.
|
|
*/
|
|
params: {
|
|
/**
|
|
* Note that the file path is relative to Anki’s collection.media folder, not to the client.
|
|
*/
|
|
path: string;
|
|
};
|
|
result: boolean;
|
|
};
|
|
reloadCollection: {
|
|
result: null;
|
|
};
|
|
|
|
// Model Actions
|
|
modelNames: {
|
|
result: string[];
|
|
};
|
|
modelNamesAndIds: {
|
|
result: {
|
|
[modelName: string]: number;
|
|
};
|
|
};
|
|
modelFieldNames: {
|
|
params: {
|
|
modelName: string;
|
|
};
|
|
result: string[];
|
|
};
|
|
modelFieldsOnTemplates: {
|
|
params: {
|
|
modelName: string;
|
|
};
|
|
result: {
|
|
[side: string]: [string[], string[]];
|
|
};
|
|
};
|
|
createModel: {
|
|
params: {
|
|
modelName: string;
|
|
inOrderFields: string[];
|
|
/**
|
|
* Default to built-in CSS
|
|
*/
|
|
css?: string;
|
|
isCloze?: boolean;
|
|
cardTemplates: IAnkiConnectCardTemplate[];
|
|
};
|
|
result: IAnkiCard;
|
|
};
|
|
modelTemplates: {
|
|
params: {
|
|
modelName: string;
|
|
};
|
|
result: {
|
|
[side: string]: {
|
|
Front: string;
|
|
Back: string;
|
|
};
|
|
};
|
|
};
|
|
modelStyling: {
|
|
params: {
|
|
modelName: string;
|
|
};
|
|
result: {
|
|
css: string;
|
|
};
|
|
};
|
|
updateModelTemplates: {
|
|
/**
|
|
* Modify the templates of an existing model by name.
|
|
* Only specifies cards and specified sides will be modified.
|
|
*
|
|
* If an existing card or side is not included in the request, it will be left unchanged.
|
|
*/
|
|
params: {
|
|
model: {
|
|
name: string;
|
|
templates: {
|
|
[side: string]: {
|
|
Front?: string;
|
|
Back?: string;
|
|
};
|
|
};
|
|
};
|
|
};
|
|
result: null;
|
|
};
|
|
updateModelStyling: {
|
|
params: {
|
|
model: {
|
|
name: string;
|
|
css: string;
|
|
};
|
|
};
|
|
result: null;
|
|
};
|
|
findAndReplaceInModels: {
|
|
params: {
|
|
model: {
|
|
modelName: string;
|
|
findText: string;
|
|
replaceText: string;
|
|
front?: boolean;
|
|
back?: boolean;
|
|
css?: boolean;
|
|
};
|
|
};
|
|
result: number;
|
|
};
|
|
|
|
// Note Actions
|
|
addNote: {
|
|
params: {};
|
|
result: number;
|
|
};
|
|
}
|
|
|
|
export interface IAnkiConnectCardTemplate {
|
|
/**
|
|
* By default the card names will be Card 1, Card 2, and so on.
|
|
*/
|
|
Name?: string;
|
|
Front: string;
|
|
Back: string;
|
|
}
|
|
|
|
export class AnkiConnect {
|
|
$axios: AxiosInstance;
|
|
|
|
constructor(public url = 'http://localhost:8765', public version = 6) {
|
|
this.$axios = axios.create({
|
|
baseURL: url,
|
|
});
|
|
}
|
|
|
|
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,
|
|
) {
|
|
const { data: response } = await this.$axios.post<{
|
|
result: any;
|
|
error: string | null;
|
|
}>('/', { action, params, version });
|
|
|
|
if (Object.getOwnPropertyNames(response).length != 2) {
|
|
throw 'response has an unexpected number of fields';
|
|
}
|
|
if (!response.hasOwnProperty('error')) {
|
|
throw 'response is missing required error field';
|
|
}
|
|
if (!response.hasOwnProperty('result')) {
|
|
throw 'response is missing required result field';
|
|
}
|
|
if (response.error) {
|
|
throw response.error;
|
|
}
|
|
|
|
return response.result;
|
|
}
|
|
}
|