} = {
93 | en,
94 | 'zh-cn': zh_cn
95 | };
96 |
97 | const locale: Locale = Object.assign({}, en, locales[moment.locale()]);
98 |
99 | export default locale;
100 |
--------------------------------------------------------------------------------
/src/anki.ts:
--------------------------------------------------------------------------------
1 | import { Notice, requestUrl } from 'obsidian';
2 | import locale from './lang';
3 | import Media from './media';
4 |
5 | interface Request {
6 | action: string;
7 | version: number;
8 | params: P;
9 | }
10 |
11 | interface Response {
12 | error: string | null;
13 | result: R;
14 | }
15 |
16 | export class AnkiError extends Error {}
17 |
18 | export interface Note {
19 | deckName: string;
20 | modelName: string;
21 | fields: Record;
22 | options?: {
23 | allowDuplicate: boolean;
24 | duplicateScope: string;
25 | };
26 | tags: Array;
27 | }
28 |
29 | class Anki {
30 | private port = 8765;
31 |
32 | async invoke(action: string, params: Params): Promise {
33 | type requestType = Request;
34 | type responseType = Response;
35 | const request: requestType = {
36 | action: action,
37 | version: 6,
38 | params: params
39 | };
40 | try {
41 | const { json } = await requestUrl({
42 | url: `http://127.0.0.1:${this.port}`,
43 | method: `POST`,
44 | contentType: `application/json`,
45 | body: JSON.stringify(request)
46 | });
47 | const data = json as responseType;
48 | if (data.error !== null) {
49 | return new AnkiError(data.error);
50 | }
51 | return data.result;
52 | } catch (error) {
53 | new Notice(locale.synchronizeAnkiConnectUnavailableNotice);
54 | throw error;
55 | }
56 | }
57 |
58 | async multi(actionName: string, actionList: P[]) {
59 | return this.invoke, 'version'>[] }>('multi', {
60 | actions: actionList.map(params => ({
61 | action: actionName,
62 | params: params
63 | }))
64 | });
65 | }
66 |
67 | // read-only
68 |
69 | async version() {
70 | return this.invoke('version', undefined);
71 | }
72 |
73 | async noteTypes() {
74 | return this.invoke('modelNames', undefined);
75 | }
76 |
77 | async noteTypesAndIds() {
78 | return this.invoke>('modelNamesAndIds', undefined);
79 | }
80 |
81 | async fields(noteType: string) {
82 | return this.invoke('modelFieldNames', {
83 | modelName: noteType
84 | });
85 | }
86 |
87 | async findNotes(query: string) {
88 | return this.invoke('findNotes', {
89 | query: query
90 | });
91 | }
92 |
93 |
94 | async notesInfo(noteIds: number[]) {
95 | return this.invoke<{ cards: number[], tags: string[], noteId: string }[], { notes: number[] }>('notesInfo', {
96 | notes: noteIds
97 | });
98 | }
99 |
100 | async notesInfoByDeck(deck: string): Promise {
101 | const notesIds = await this.findNotes(`deck:${deck}`);
102 | if (notesIds instanceof AnkiError) {
103 | return notesIds;
104 | }
105 | return await this.notesInfo(notesIds);
106 |
107 |
108 | }
109 |
110 | // write-only
111 |
112 | async addMedia(media: Media) {
113 | return this.invoke('storeMediaFile', {
114 | filename: media.filename,
115 | path: media.path,
116 | deleteExisting: media.deleteExisting
117 | });
118 | }
119 |
120 | async addNote(note: Note) {
121 | return this.invoke('addNote', {
122 | note: note
123 | });
124 | }
125 |
126 | async updateFields(id: number, fields: Record) {
127 | return this.invoke('updateNoteFields', {
128 | note: {
129 | id: id,
130 | fields: fields
131 | }
132 | });
133 | }
134 |
135 | async updateNoteTags(noteId: number, tags: string[]) {
136 | const tagstring = tags.map(item => item.replace(/\//g, '::')).join(' ');
137 | return this.invoke('updateNoteTags', {
138 | note: noteId,
139 | tags: tagstring
140 | });
141 | }
142 |
143 | async addTagsToNotes(noteIds: number[], tags: string[]) {
144 | const tagstring = tags.join(' ');
145 | return this.invoke('addTags', {
146 | notes: noteIds,
147 | tags: tagstring
148 | });
149 | }
150 |
151 | async removeTagsFromNotes(noteIds: number[], tags: string[]) {
152 | const tagstring = tags.join(' ');
153 | return this.invoke('removeTags', {
154 | notes: noteIds,
155 | tags: tagstring
156 | });
157 | }
158 |
159 | async deleteNotes(noteIds: number[]) {
160 | return this.invoke('deleteNotes', {
161 | notes: noteIds
162 | });
163 | }
164 |
165 | async changeDeck(cardIds: number[], deck: string) {
166 | return this.invoke('changeDeck', {
167 | cards: cardIds,
168 | deck: deck
169 | });
170 | }
171 |
172 | async createDeck(deckName: string) {
173 | return this.invoke('createDeck', {
174 | deck: deckName
175 | });
176 | }
177 | }
178 |
179 | export default Anki;
180 |
--------------------------------------------------------------------------------
/src/note.ts:
--------------------------------------------------------------------------------
1 | import { stringifyYaml, FrontMatterCache, TFile, EmbedCache } from 'obsidian';
2 | import { NoteDigest, NoteTypeDigest } from './state';
3 | import { MD5 } from 'object-hash';
4 | import { Settings } from './setting';
5 |
6 | const PICTURE_EXTENSION = ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'svg'];
7 | const VIDEO_EXTENSION = [
8 | 'mp3',
9 | 'wav',
10 | 'm4a',
11 | 'ogg',
12 | '3gp',
13 | 'flac',
14 | 'mp4',
15 | 'ogv',
16 | 'mov',
17 | 'mkv',
18 | 'webm'
19 | ];
20 |
21 | export interface MediaNameMap {
22 | obsidian: string;
23 | anki: string;
24 | }
25 |
26 | export interface FrontMatter {
27 | mid: number;
28 | nid: number;
29 | tags: string[];
30 | }
31 |
32 | export default class Note {
33 | basename: string;
34 | folder: string;
35 | nid: number;
36 | mid: number;
37 | tags: string[];
38 | fields: Record;
39 | typeName: string;
40 | extras: object;
41 |
42 | constructor(
43 | basename: string,
44 | folder: string,
45 | typeName: string,
46 | frontMatter: FrontMatter,
47 | fields: Record
48 | ) {
49 | this.basename = basename;
50 | this.folder = folder;
51 | const { mid, nid, tags, ...extras } = frontMatter;
52 | this.typeName = typeName;
53 | this.mid = mid;
54 | this.nid = nid;
55 | this.tags = tags;
56 | this.extras = extras;
57 | this.fields = fields;
58 | }
59 |
60 | digest(): NoteDigest {
61 | return {
62 | deck: this.renderDeckName(),
63 | hash: MD5(this.fields),
64 | tags: this.tags
65 | };
66 | }
67 |
68 | title() {
69 | return this.basename;
70 | }
71 |
72 | renderDeckName() {
73 | return this.folder.replace(/\//g, '::') || 'Obsidian';
74 | }
75 |
76 | isCloze() {
77 | return this.typeName === '填空题' || this.typeName === 'Cloze';
78 | }
79 | }
80 |
81 | export class NoteManager {
82 | private settings: Settings;
83 |
84 | constructor(settings: Settings) {
85 | this.settings = settings;
86 | }
87 |
88 | validateNote(
89 | file: TFile,
90 | frontmatter: FrontMatterCache,
91 | content: string,
92 | media: EmbedCache[] | undefined,
93 | noteTypes: Map
94 | ): [Note | undefined, MediaNameMap[] | undefined] {
95 | if (
96 | !frontmatter.hasOwnProperty('mid') ||
97 | !frontmatter.hasOwnProperty('nid') ||
98 | !frontmatter.hasOwnProperty('tags')
99 | )
100 | return [undefined, undefined];
101 | const frontMatter = Object.assign({}, frontmatter, { position: undefined }) as FrontMatter;
102 | const lines = content.split('\n');
103 | const yamlEndIndex = lines.indexOf('---', 1);
104 | const body = lines.slice(yamlEndIndex + 1);
105 | const noteType = noteTypes.get(frontMatter.mid);
106 | if (!noteType) return [undefined, undefined];
107 | const [fields, mediaNameMap] = this.parseFields(file.basename, noteType, body, media);
108 | if (!fields) return [undefined, undefined];
109 | // now it is a valid Note
110 | const basename = file.basename;
111 | const folder = file.parent.path == '/' ? '' : file.parent.path;
112 | return [new Note(basename, folder, noteType.name, frontMatter, fields), mediaNameMap];
113 | }
114 |
115 | parseFields(
116 | title: string,
117 | noteType: NoteTypeDigest,
118 | body: string[],
119 | media: EmbedCache[] | undefined
120 | ): [Record | undefined, MediaNameMap[] | undefined] {
121 | const fieldNames = noteType.fieldNames;
122 | const headingLevel = this.settings.headingLevel;
123 | const isCloze = noteType.name === '填空题' || noteType.name === 'Cloze';
124 | const fieldContents: string[] = isCloze ? [] : [title];
125 | const mediaNameMap: MediaNameMap[] = [];
126 | let buffer: string[] = [];
127 | let mediaCount = 0;
128 | for (const line of body) {
129 | if (line.slice(0, headingLevel + 1) === '#'.repeat(headingLevel) + ' ') {
130 | fieldContents.push(buffer.join('\n'));
131 | buffer = [];
132 | } else {
133 | if (
134 | media &&
135 | mediaCount < media.length &&
136 | line.includes(media[mediaCount].original) &&
137 | this.validateMedia(media[mediaCount].link)
138 | ) {
139 | let mediaName = line.replace(
140 | media[mediaCount].original,
141 | media[mediaCount].link.split('/').pop() as string
142 | );
143 | if (this.isPicture(mediaName)) mediaName = '
';
144 | else mediaName = '[sound:' + mediaName + ']';
145 | if (!mediaNameMap.map(d => d.obsidian).includes(media[mediaCount].original)) {
146 | mediaNameMap.push({ obsidian: media[mediaCount].original, anki: mediaName });
147 | mediaCount++;
148 | buffer.push(mediaName);
149 | }
150 | } else {
151 | buffer.push(line);
152 | }
153 | }
154 | }
155 | fieldContents.push(buffer.join('\n'));
156 | if (fieldNames.length !== fieldContents.length) return [undefined, undefined];
157 | const fields: Record = {};
158 | fieldNames.map((v, i) => (fields[v] = fieldContents[i]));
159 | return [fields, mediaNameMap];
160 | }
161 |
162 | validateMedia(mediaName: string) {
163 | return [...PICTURE_EXTENSION, ...VIDEO_EXTENSION].includes(
164 | mediaName.split('.').pop() as string
165 | );
166 | }
167 |
168 | isPicture(mediaName: string) {
169 | return PICTURE_EXTENSION.includes(mediaName.split('.').pop() as string);
170 | }
171 |
172 | dump(note: Note, mediaNameMap: MediaNameMap[] | undefined = undefined) {
173 | const frontMatter = stringifyYaml(
174 | Object.assign(
175 | {
176 | mid: note.mid,
177 | nid: note.nid,
178 | tags: note.tags
179 | },
180 | note.extras
181 | )
182 | )
183 | .trim()
184 | .replace(/"/g, ``);
185 | const fieldNames = Object.keys(note.fields);
186 | const lines = [`---`, frontMatter, `---`];
187 | if (note.isCloze()) {
188 | lines.push(note.fields[fieldNames[0]]);
189 | fieldNames.slice(1).map(s => {
190 | lines.push(`${'#'.repeat(this.settings.headingLevel)} ${s}`, note.fields[s]);
191 | });
192 | } else {
193 | lines.push(note.fields[fieldNames[1]]);
194 | fieldNames.slice(2).map(s => {
195 | lines.push(`${'#'.repeat(this.settings.headingLevel)} ${s}`, note.fields[s]);
196 | });
197 | }
198 |
199 | if (mediaNameMap)
200 | for (const i in lines)
201 | for (const mediaName of mediaNameMap)
202 | if (lines[i].includes(mediaName.anki))
203 | lines[i] = lines[i].replace(mediaName.anki, mediaName.obsidian);
204 |
205 | return lines.join('\n');
206 | }
207 | }
208 |
--------------------------------------------------------------------------------
/src/state.ts:
--------------------------------------------------------------------------------
1 | import AnkiSynchronizer from 'main';
2 | import { Notice, TFile } from 'obsidian';
3 | import Note, { FrontMatter } from 'src/note';
4 | import Media from './media';
5 | import Anki from './anki';
6 | import Formatter from './format';
7 | import locale from './lang';
8 |
9 | abstract class State extends Map {
10 | protected plugin: AnkiSynchronizer;
11 | protected anki: Anki;
12 |
13 | constructor(plugin: AnkiSynchronizer) {
14 | super();
15 | this.plugin = plugin;
16 | this.anki = plugin.anki;
17 | }
18 |
19 | async change(newState: Map) {
20 | const existingKeys = [...this.keys()];
21 | const newKeys = [...newState.keys()];
22 |
23 | // Iterate over the new state
24 | for (const [key, value] of newState.entries()) {
25 | if (Array.isArray(value)) {
26 | const [digest, info] = value;
27 | await this.update(key, digest, info);
28 | this.set(key, digest);
29 | } else {
30 | await this.update(key, value);
31 | this.set(key, value);
32 | }
33 | }
34 | // for (const key of existingKeys.filter(x => !newKeys.includes(x))) {
35 | // this.delete(key);
36 | // }
37 | }
38 |
39 | abstract update(key: K, value: V, info?: I): Promise;
40 | }
41 |
42 | export type NoteTypeDigest = { name: string; fieldNames: string[] };
43 |
44 | export class NoteTypeState extends State {
45 | private templateFolderPath: string | undefined = undefined;
46 |
47 | setTemplatePath(templateFolderPath: string) {
48 | this.templateFolderPath = templateFolderPath;
49 | }
50 |
51 | delete(key: number) {
52 | const noteTypeDigest = this.get(key);
53 | if (noteTypeDigest !== undefined) {
54 | const templatePath = `${this.templateFolderPath}/${noteTypeDigest.name}.md`;
55 | const maybeTemplate = this.plugin.app.vault.getAbstractFileByPath(templatePath);
56 | if (maybeTemplate !== null) {
57 | this.plugin.app.vault.delete(maybeTemplate);
58 | }
59 | }
60 | return super.delete(key);
61 | }
62 |
63 | async update(key: number, digest: NoteTypeDigest) {
64 | if (this.has(key)) {
65 | this.delete(key);
66 | }
67 | const pseudoFrontMatter = {
68 | mid: key,
69 | nid: 0,
70 | tags: [],
71 | date: '{{date}} {{time}}'
72 | } as FrontMatter;
73 | const pseudoFields: Record = {};
74 | digest.fieldNames.map(x => (pseudoFields[x] = '\n\n'));
75 | const templateNote = new Note(
76 | digest.name,
77 | this.templateFolderPath!,
78 | digest.name,
79 | pseudoFrontMatter,
80 | pseudoFields
81 | );
82 | const templatePath = `${this.templateFolderPath}/${digest.name}.md`;
83 | const maybeTemplate = this.plugin.app.vault.getAbstractFileByPath(templatePath);
84 | if (maybeTemplate !== null) {
85 | await this.plugin.app.vault.modify(
86 | maybeTemplate as TFile,
87 | this.plugin.noteManager.dump(templateNote)
88 | );
89 | } else {
90 | await this.plugin.app.vault.create(templatePath, this.plugin.noteManager.dump(templateNote));
91 | }
92 | console.log(`Created template ${templatePath}`);
93 | }
94 | }
95 |
96 | export type NoteDigest = { deck: string; hash: string; tags: string[] };
97 |
98 | export class NoteState extends State {
99 | private formatter: Formatter;
100 |
101 | constructor(plugin: AnkiSynchronizer) {
102 | super(plugin);
103 | this.formatter = new Formatter(this.plugin.app.vault.getName(), this.plugin.settings);
104 | }
105 |
106 | // Existing notes may have 3 things to update: deck, fields, tags
107 | async update(key: number, digest: NoteDigest, info: Note) {
108 |
109 | const current = this.get(key);
110 | if (!current) return;
111 | if (current.deck !== digest.deck) {
112 | // updating deck
113 | this.updateDeck(info);
114 | }
115 | if (current.hash !== digest.hash) {
116 | // updating fields
117 | this.updateFields(info);
118 | }
119 | // Check for null case
120 | if (current.tags === null) current.tags = [];
121 | if (digest.tags === null) digest.tags = [];
122 | if (
123 | current.tags.length !== digest.tags.length ||
124 | current.tags.some((item, index) => item !== digest.tags[index])
125 | ) {
126 | // updating tags
127 | this.updateTags(info);
128 | }
129 | }
130 |
131 | async updateDeck(note: Note) {
132 | const deck = note.renderDeckName();
133 | const notesInfoResponse = await this.anki.notesInfo([note.nid]);
134 |
135 | if (!Array.isArray(notesInfoResponse)) {
136 | return;
137 | }
138 | const { cards } = notesInfoResponse[0];
139 | console.log(`Changing deck for ${note.title()}`, deck);
140 | let changeDeckResponse = await this.anki.changeDeck(cards, deck);
141 | if (changeDeckResponse === null) return;
142 |
143 | // if the supposed deck does not exist, create it
144 | if (changeDeckResponse.message.contains('deck was not found')) {
145 | console.log(changeDeckResponse.message, ', try creating');
146 | const createDeckResponse = await this.anki.createDeck(deck);
147 | if (createDeckResponse === null) {
148 | changeDeckResponse = await this.anki.changeDeck(cards, deck);
149 | if (changeDeckResponse === null) return;
150 | }
151 | }
152 |
153 | new Notice(locale.synchronizeChangeDeckFailureNotice(note.title()));
154 | }
155 |
156 | async updateFields(note: Note) {
157 | const fields = this.formatter.format(note);
158 | console.log(`Updating fields for ${note.title()}`, fields);
159 | const updateFieldsResponse = await this.anki.updateFields(note.nid, fields);
160 | if (updateFieldsResponse === null) return;
161 | new Notice(locale.synchronizeUpdateFieldsFailureNotice(note.title()));
162 | }
163 |
164 | async updateTags(note: Note) {
165 | let updateTagsResponse = null;
166 | console.log(`Updating tags for ${note.title()}`, note.tags);
167 | updateTagsResponse = await this.anki.updateNoteTags(note.nid, note.tags);
168 | if (updateTagsResponse) new Notice(locale.synchronizeUpdateTagsFailureNotice(note.title()));
169 | }
170 |
171 | delete(key: number) {
172 | this.plugin.anki.deleteNotes([key]);
173 | return super.delete(key);
174 | }
175 |
176 | async handleAddNote(note: Note) {
177 | const ankiNote = {
178 | deckName: note.renderDeckName(),
179 | modelName: note.typeName,
180 | fields: this.formatter.format(note),
181 | tags: note.tags
182 | };
183 | console.log(`Adding note for ${note.title()}`, ankiNote);
184 | let idOrError = await this.anki.addNote(ankiNote);
185 | if (typeof idOrError === 'number') {
186 | return idOrError;
187 | }
188 |
189 | // if the supposed deck does not exist, create it
190 | if (idOrError.message.contains('deck was not found')) {
191 | console.log(idOrError.message, ', try creating');
192 | const didOrError = await this.anki.createDeck(ankiNote.deckName);
193 | if (typeof didOrError === 'number') {
194 | idOrError = await this.anki.addNote(ankiNote);
195 | if (typeof idOrError === 'number') {
196 | return idOrError;
197 | }
198 | }
199 | } else {
200 | console.log(idOrError.message);
201 | }
202 | }
203 |
204 | async handleAddMedia(media: Media) {
205 | console.log(`Adding media ${media.filename}`, media);
206 | await this.anki.addMedia(media);
207 | }
208 | }
209 |
--------------------------------------------------------------------------------
/main.ts:
--------------------------------------------------------------------------------
1 | import { normalizePath, Notice, Plugin, TFile, TFolder, Vault } from 'obsidian';
2 | import Anki, { AnkiError } from 'src/anki';
3 | import Note, { NoteManager } from 'src/note';
4 | import { MediaManager } from 'src/media';
5 | import locale from 'src/lang';
6 | import { NoteDigest, NoteState, NoteTypeDigest, NoteTypeState } from 'src/state';
7 | import AnkiSynchronizerSettingTab, { Settings, DEFAULT_SETTINGS } from 'src/setting';
8 | import { version } from './package.json';
9 | import { MD5 } from 'object-hash';
10 |
11 | interface Data {
12 | version: string;
13 | settings: Settings;
14 | noteState: Record;
15 | noteTypeState: Record;
16 | }
17 |
18 | export default class AnkiSynchronizer extends Plugin {
19 | anki = new Anki();
20 | settings = DEFAULT_SETTINGS;
21 | mediaManager = new MediaManager();
22 | noteManager = new NoteManager(this.settings);
23 | noteState = new NoteState(this);
24 | noteTypeState = new NoteTypeState(this);
25 |
26 |
27 | async onload() {
28 | // Recover data from local file
29 | const data: Data | null = await this.loadData();
30 | if (data) {
31 | const { settings, noteState, noteTypeState } = data;
32 | Object.assign(this.settings, settings);
33 | for (const key in noteState) {
34 | this.noteState.set(parseInt(key), noteState[key]);
35 | }
36 | for (const key in noteTypeState) {
37 | this.noteTypeState.set(parseInt(key), noteTypeState[key]);
38 | }
39 | }
40 | this.configureUI();
41 | console.log(locale.onLoad);
42 | }
43 |
44 | configureUI() {
45 | // Add import note types command
46 | this.addCommand({
47 | id: 'import',
48 | name: locale.importCommandName,
49 | callback: async () => await this.importNoteTypes()
50 | });
51 | this.addRibbonIcon('enter', locale.importCommandName, async () => await this.importNoteTypes());
52 |
53 | // Add synchronize command
54 | this.addCommand({
55 | id: 'synchronize',
56 | name: locale.synchronizeCommandName,
57 | callback: async () => await this.synchronize()
58 | });
59 | this.addRibbonIcon(
60 | 'sheets-in-box',
61 | locale.synchronizeCommandName,
62 | async () => await this.synchronize()
63 | );
64 |
65 | // Add a setting tab to configure settings
66 | this.addSettingTab(new AnkiSynchronizerSettingTab(this.app, this));
67 | }
68 |
69 | // Save data to local file
70 | save() {
71 | return this.saveData({
72 | version: version,
73 | settings: this.settings,
74 | noteState: Object.fromEntries(this.noteState),
75 | noteTypeState: Object.fromEntries(this.noteTypeState)
76 | });
77 | }
78 |
79 | async onunload() {
80 | await this.save();
81 | console.log(locale.onUnload);
82 | }
83 |
84 | // Retrieve template information from Obsidian core plugin "Templates"
85 | getTemplatePath() {
86 | const templatesPlugin = (this.app as any).internalPlugins?.plugins['templates'];
87 | if (!templatesPlugin?.enabled) {
88 | new Notice(locale.templatesNotEnabledNotice);
89 | return;
90 | }
91 | if (templatesPlugin.instance.options.folder === undefined) {
92 | new Notice(locale.templatesFolderUndefinedNotice);
93 | return;
94 | }
95 | return normalizePath(templatesPlugin.instance.options.folder);
96 | }
97 |
98 | async importNoteTypes() {
99 | new Notice(locale.importStartNotice);
100 | const templatesPath = this.getTemplatePath();
101 | if (templatesPath === undefined) return;
102 | this.noteTypeState.setTemplatePath(templatesPath);
103 | const noteTypesAndIds = await this.anki.noteTypesAndIds();
104 | if (noteTypesAndIds instanceof AnkiError) {
105 | new Notice(locale.importFailureNotice);
106 | return;
107 | }
108 | const noteTypes = Object.keys(noteTypesAndIds);
109 | const noteTypeFields = await this.anki.multi<{ modelName: string }, string[]>(
110 | 'modelFieldNames',
111 | noteTypes.map(s => ({ modelName: s }))
112 | );
113 | if (noteTypeFields instanceof AnkiError) {
114 | new Notice(locale.importFailureNotice);
115 | return;
116 | }
117 | const state = new Map(
118 | noteTypes.map((name, index) => [
119 | noteTypesAndIds[name],
120 | {
121 | name: name,
122 | fieldNames: noteTypeFields[index]
123 | }
124 | ])
125 | );
126 | console.log(`Retrieved note type data from Anki`, state);
127 | await this.noteTypeState.change(state);
128 | await this.save();
129 | new Notice(locale.importSuccessNotice);
130 | }
131 |
132 | async synchronize() {
133 | const templatesPath = this.getTemplatePath();
134 | if (templatesPath === undefined) return;
135 | new Notice(locale.synchronizeStartNotice);
136 | const state = new Map();
137 |
138 | // getActiveViewOfType
139 | const activeFile = this.app.workspace.getActiveFile();
140 | const folderPath = activeFile?.parent?.path
141 | const deck = folderPath?.replace(/\//g, '::') || 'Obsidian';
142 |
143 | const folder = this.app.vault.getAbstractFileByPath(folderPath || "/") as any
144 | const files = folder?.children as any
145 |
146 | console.log(`Found ${files.length} files in obsidian folder`, folder);
147 |
148 | const notesInfoResponse = await this.anki.notesInfoByDeck(deck)
149 |
150 | console.log("Found notes in Anki", notesInfoResponse);
151 |
152 |
153 | for (const file of files) {
154 | const frontmatter = this.app.metadataCache.getFileCache(file)?.frontmatter;
155 |
156 | if (!frontmatter) continue;
157 |
158 | const content = await this.app.vault.cachedRead(file);
159 | const media = this.app.metadataCache.getFileCache(file)?.embeds;
160 |
161 | const [obsidianNote, mediaNameMap] = this.noteManager.validateNote(
162 | file,
163 | frontmatter,
164 | content,
165 | media,
166 | this.noteTypeState
167 | );
168 |
169 | if (!obsidianNote) continue;
170 |
171 | console.log(`Validated note ${obsidianNote.title()}`, obsidianNote);
172 |
173 | if (media) {
174 | for (const item of media) {
175 | this.noteState.handleAddMedia(
176 | this.mediaManager.parseMedia(item, this.app.vault, this.app.metadataCache)
177 | );
178 | }
179 | }
180 |
181 | const correspondingAnkiNote = notesInfoResponse.find((note: any) => note.noteId === frontmatter.nid);
182 |
183 | // Merge anki tags and obsidian tags
184 | const obsidianTags = frontmatter.tags || []
185 | const ankiTags = correspondingAnkiNote?.tags || [];
186 | const mergedTags = [...new Set([...obsidianTags, ...ankiTags])];
187 |
188 | const tagsBeforeHash = MD5(frontmatter.tags);
189 | const tagsAfterHash = MD5(mergedTags);
190 | const shouldUpdateTags = tagsBeforeHash !== tagsAfterHash;
191 |
192 |
193 |
194 | if (obsidianNote.nid === 0) {
195 | // new file
196 | const nid = await this.noteState.handleAddNote(obsidianNote);
197 | if (nid === undefined) {
198 | new Notice(locale.synchronizeAddNoteFailureNotice(file.basename));
199 | continue;
200 | }
201 | obsidianNote.nid = nid;
202 | this.app.vault.modify(file, this.noteManager.dump(obsidianNote, mediaNameMap));
203 | }
204 |
205 | if (shouldUpdateTags) {
206 | obsidianNote.tags = mergedTags;
207 | this.app.vault.modify(file, this.noteManager.dump(obsidianNote, mediaNameMap));
208 | }
209 |
210 |
211 | state.set(obsidianNote.nid, [obsidianNote.digest(), obsidianNote]);
212 | }
213 |
214 | await this.noteState.change(state);
215 | await this.save();
216 | new Notice(locale.synchronizeSuccessNotice);
217 | }
218 | }
219 |
--------------------------------------------------------------------------------