Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,21 @@ date: {{date}} {{time}}

Where `mid` is a number representing the note type ID in Anki. If this note type happen to have 3 or more fields, the third field and all other fields after that will appear as `h1` title in the markdown file.


### Edit Notes

When creating notes with generated template files, please write the content of the first field into the filename, and the second field right after the YAML front matter, and other fields below their corresponding `h1` title.

The way that notes are organized in Obsidian will be mirrored in Anki using decks. For example, the file `/learning/note.md` will be synced to the `learning` deck in Anki, and the file `/learning/project 1/note.md` will be synced to `learning::project 1` deck in Anki. Toplevel files will be synced to a special deck `Obsidian`. If the supposed deck doesn't exist in Anki, it will be created.

### Synchonize notes
### Synchronize notes

Run command `Synchronize`. If unexpected behavior happens, please toggle the developer console and report the output there.

Also there is the feature to set an Interval in witch the synchronize is performed.


### Header

Run command `Synchronize`. If unexpected behavior happens, please toggle the developer console and report the output there.
The default header of non Cloze card, is the file name of the card.
If `header` propriety is defined then this would be the new header.
2 changes: 1 addition & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node'
testEnvironment: 'node',
};
148 changes: 121 additions & 27 deletions main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import locale from 'src/lang';
import { NoteDigest, NoteState, NoteTypeDigest, NoteTypeState } from 'src/state';
import AnkiSynchronizerSettingTab, { Settings, DEFAULT_SETTINGS } from 'src/setting';
import { version } from './package.json';
import LoggerSync from 'src/logger';

interface Data {
version: string;
Expand All @@ -21,6 +22,9 @@ export default class AnkiSynchronizer extends Plugin {
noteManager = new NoteManager(this.settings);
noteState = new NoteState(this);
noteTypeState = new NoteTypeState(this);
timeout: ReturnType<typeof setTimeout> | null = null



async onload() {
// Recover data from local file
Expand All @@ -36,30 +40,41 @@ export default class AnkiSynchronizer extends Plugin {
}
}
this.configureUI();
this.startNewInterval()
console.log(locale.onLoad);
}

configureUI() {
// Add import note types command
this.addCommand({
id: "import",
name: locale.importCommandName,
callback: async () => await this.importNoteTypes(),
});
this.addRibbonIcon('enter', locale.importCommandName, async () => await this.importNoteTypes());

// Add synchronize command
this.addCommand({
id: "synchronize",
name: locale.synchronizeCommandName,
callback: async () => await this.synchronize(),
});
this.addCommand({
id: "full syncronize",
name: locale.synchronizeCommandName,
callback: async () => await this.synchronize(true),
});

this.addRibbonIcon(
'sheets-in-box',
locale.synchronizeCommandName,
async () => await this.synchronize()
'undo-glyph', "full syinc",
async () => await this.synchronize(true)
);

if (this.settings.showImportIcon)
this.addRibbonIcon('enter', locale.importCommandName, async () => await this.importNoteTypes());
if (this.settings.showSyncIcon)
this.addRibbonIcon(
'sheets-in-box',
locale.synchronizeCommandName,
async () => await this.synchronize()
);

// Add a setting tab to configure settings
this.addSettingTab(new AnkiSynchronizerSettingTab(this.app, this));
}
Expand All @@ -75,6 +90,7 @@ export default class AnkiSynchronizer extends Plugin {
}

async onunload() {
this.clearInterval()
await this.save();
console.log(locale.onUnload);
}
Expand Down Expand Up @@ -127,49 +143,127 @@ export default class AnkiSynchronizer extends Plugin {
new Notice(locale.importSuccessNotice);
}

async synchronize() {
clearInterval() {
if (this.timeout !== null) {
clearInterval(this.timeout)
}
}

startNewInterval() {
this.clearInterval()
if (this.settings.autoSync !== 0) {
this.timeout = setInterval(
async () => { await this.synchronize() },
this.settings.autoSync * 1000 * 60
);
}
}


async synchronize(full = false) {
// check anki connection
let logger = LoggerSync.getInstance().reset();
let decks = await this.anki.decks();
if (decks instanceof AnkiError) {
new Notice(locale.synchronizeAnkiConnectUnavailableNotice);
return;
}

const templatesPath = this.getTemplatePath();
if (templatesPath === undefined) return;
new Notice(locale.synchronizeStartNotice);
const allFiles = this.app.vault.getMarkdownFiles();
const state = new Map<number, [NoteDigest, Note]>();
// in this case undefined is use as a toy value that means that the
// note is already added
const state = new Map<number, NoteDigest>();
for (const file of allFiles) {
// ignore templates
if (file.path.startsWith(templatesPath)) continue;
// read and validate content
const content = await this.app.vault.cachedRead(file);
const frontmatter = this.app.metadataCache.getFileCache(file)?.frontmatter;
if (!frontmatter) continue;
if (frontmatter.nid == undefined) { continue; } // means that is not a note for anki

if (frontmatter.nid != undefined && frontmatter.nid !== 0) {
const value = this.noteState.get(frontmatter.nid);
const newValue = this.noteManager.genNoteDigest(file, content);
// this is when it get skipped
if (value != undefined && value.hash == newValue.hash && !full) {
state.set(frontmatter.nid, newValue);
logger.cached.push(file.basename);
// this is cached
continue;
}
}
// console.log(file.basename, 'full recreation of the note')
const media = this.app.metadataCache.getFileCache(file)?.embeds;
if (media) {
for (const item of media) {
this.noteState.handleAddMedia(
await this.anki.addMedia(
this.mediaManager.parseMedia(item, this.app.vault, this.app.metadataCache)
);
}
}
const [note, mediaNameMap] = this.noteManager.validateNote(
file,
frontmatter,
content,
media,
this.noteTypeState
);
if (!note) continue;
if (note.nid === 0) {
// new file
const nid = await this.noteState.handleAddNote(note);
if (nid === undefined) {
new Notice(locale.synchronizeAddNoteFailureNotice(file.basename));
continue;
try {
const [note, mediaNameMap] = this.noteManager.createValidateNote(
file,
frontmatter,
content,
media,
this.noteTypeState
);
if (!decks.includes(note.folder)) {
logger.added_decks.push(file.basename);
let res = this.anki.createDeck(note.folder);
if (res instanceof AnkiError) {
new Notice(locale.synchronizeAddDeckFailureNotice(note.folder));
continue;
}
decks.push(note.folder);
}
note.nid = nid;
this.app.vault.modify(file, this.noteManager.dump(note, mediaNameMap));

if (note.nid === 0) {
logger.created_new.push(file.basename);
let nid = await this.anki.addNote(note.toAnkiNote(this.app.vault.getName()));
if (typeof nid !== 'number') {
new Notice(locale.synchronizeAddNoteFailureNotice(file.basename));
continue;
}
note.nid = nid;
this.app.vault.modify(file, this.noteManager.dump(note));
} else {
const value = this.noteState.get(frontmatter.nid);
const currentValue = note.digest();
if (currentValue.deck != value?.deck) {
let res = this.anki.changeDeck([note.nid], note.folder);
if (res instanceof AnkiError) {
new Notice(locale.synchronizeChangeDeckFailureNotice(note.title()));
continue;
}
logger.changed_deck.push(file.basename);
}
if (currentValue.hash != value?.hash) {
let res = this.anki.updateFields(note, this.app.vault.getName());
if (res instanceof AnkiError) {
logger.errors.push(file.basename);
new Notice(locale.synchronizeUpdateFieldsFailureNotice(note.title()));
continue;
}
logger.modified.push(file.basename);
}
}

state.set(note.nid, note.digest());
} catch (e) {
logger.malformed.push(file.basename);
new Notice((e as Error).message);
}
state.set(note.nid, [note.digest(), note]);
}
await this.noteState.change(state);
await this.save();
logger.print();
new Notice(locale.synchronizeSuccessNotice);
}

}
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions shell.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@


let
pkgs = import <nixpkgs> {};
in pkgs.mkShell {
packages = with pkgs; [
nodejs
nodePackages.npm
];

}
45 changes: 22 additions & 23 deletions src/anki.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Notice, requestUrl } from 'obsidian';
import locale from './lang';
import Media from './media';
import Note from './note';

interface Request<P = undefined> {
action: string;
Expand All @@ -13,9 +14,9 @@ interface Response<R = null> {
result: R;
}

export class AnkiError extends Error {}
export class AnkiError extends Error { }

export interface Note {
export interface AnkiNote {
deckName: string;
modelName: string;
fields: Record<string, string>;
Expand All @@ -29,6 +30,7 @@ export interface Note {
class Anki {
private port = 8765;


async invoke<R = null, P = undefined>(action: string, params: P): Promise<R | AnkiError> {
type requestType = Request<P>;
type responseType = Response<R>;
Expand Down Expand Up @@ -70,6 +72,10 @@ class Anki {
return this.invoke<number>('version', undefined);
}

async decks() {
return this.invoke<string[]>('deckNames', undefined);
}

async noteTypes() {
return this.invoke<string[]>('modelNames', undefined);
}
Expand All @@ -78,6 +84,12 @@ class Anki {
return this.invoke<Record<string, number>>('modelNamesAndIds', undefined);
}

async getDecks(cardId: number) {
return this.invoke<string[], { cards: number[] }>('getDecks', {
cards: [cardId]
});
}

async fields(noteType: string) {
return this.invoke<string[], { modelName: string }>('modelFieldNames', {
modelName: noteType
Expand All @@ -100,36 +112,23 @@ class Anki {
});
}

async addNote(note: Note) {
return this.invoke<number, { note: Note }>('addNote', {
async addNote(note: AnkiNote) {
return this.invoke<number, { note: AnkiNote }>('addNote', {
note: note
});
}

async updateFields(id: number, fields: Record<string, string>) {
return this.invoke('updateNoteFields', {
async updateFields(note:Note,vault:string) {
return this.invoke('updateNoteModel', {
note: {
id: id,
fields: fields
id: note.nid,
fields: note.format(vault),
modelName: note.typeName,
tags: note.tags
}
});
}

async addTagsToNotes(noteIds: number[], tags: string[]) {
const tagstring = tags.join(' ');
return this.invoke('addTags', {
notes: noteIds,
tags: tagstring
});
}

async removeTagsFromNotes(noteIds: number[], tags: string[]) {
const tagstring = tags.join(' ');
return this.invoke('removeTags', {
notes: noteIds,
tags: tagstring
});
}

async deleteNotes(noteIds: number[]) {
return this.invoke('deleteNotes', {
Expand Down
Loading