Skip to content

Commit 516bf45

Browse files
committed
1.0
1 parent 6baeefd commit 516bf45

2 files changed

Lines changed: 124 additions & 94 deletions

File tree

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,12 @@
2828
- 问答卡片(basic)
2929

3030
- 自动将带有“#AK问答”的标签卡片转化为问答卡片,剔除标签后的第一行为问题字段(支持标签与问题在一行、标签在首行而问题在下一行),后面的为答案字段
31+
- 自动查询Anki中是否有内容相同的第一个字段”问题“,如果存在则更新现有卡片“答案”和“标签”字段(flomo原卡片第一行已编辑修改的视为新卡片)
3132

3233
- 划线卡片(cloze)
3334

3435
- 自动将带有“#AK划线”的标签卡片转化为划线卡片的引用字段,flomo下划线格式内容模板自动识别为cloze
36+
- 自动查询Anki中是否有内容相同的第一个字段”引用“,如果存在则更新现有卡片“标签”字段(需刷新网页,flomo原卡片已编辑修改的视为新卡片)
3537
- 无“#AK问答”、“#AK划线”的标签卡片,自动按“#AK划线”处理
3638

3739
### 转发处理
@@ -50,12 +52,11 @@
5052
### Step2. 安装 Anki 并启用 Ankiconnect (2055492159)插件,导入flomo2Anki模板
5153

5254
### Step3. 保持 Anki 在后台运行,打开flomo网页进行操作
53-
5455
## 后续改进项
5556

5657
### 配置区UI化
5758

58-
### 覆盖更新Anki中已经存在的笔记
59+
5960

6061
## 制图|Springrain,20250208
6162

flomo2Anki.js

Lines changed: 121 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// ==UserScript==
22
// @name flomo2Anki
33
// @namespace http://tampermonkey.net/
4-
// @version 0.9
4+
// @version 1.0
55
// @description 将 flomo 笔记发送到 Anki,支持单张和批量发送,并处理标签和时间链接
66
// @author springrain
77
// @match https://v.flomoapp.com/mine*
@@ -77,24 +77,21 @@
7777
const content = extractContent(memo);
7878
const tags = extractTags(memo);
7979
const modelName = getModelName(tags);
80-
const fields = getFields(content, modelName);
81-
82-
80+
// 获取包含元数据的字段信息
81+
const { fields, primaryField } = getFields(content, modelName);
8382
if (!content) throw new Error('卡片内容为空');
8483

85-
8684
return {
8785
deckName: config.defaultDeck,
8886
modelName: modelName,
8987
fields: fields,
9088
tags: tags,
89+
primaryField: primaryField // 添加主字段信息
9190
};
9291
});
9392

94-
9593
console.log('正在发送到 Anki:', notes); // 调试信息
9694

97-
9895
// 检查重复卡片并发送
9996
const { successNotes, updatedNotes } = await checkAndSendNotes(notes);
10097

@@ -107,70 +104,90 @@
107104
}
108105
}
109106

107+
// 修改后的checkAndSendNotes函数
108+
async function checkAndSendNotes(notes) {
109+
const successNotes = [];
110+
const updatedNotes = [];
111+
112+
for (const note of notes) {
113+
try {
114+
// 获取主字段信息
115+
const primaryFieldName = note.primaryField.name;
116+
let primaryFieldValue = note.primaryField.value;
117+
118+
// 转义特殊字符(双引号)
119+
primaryFieldValue = primaryFieldValue.replace(/"/g, '\\"');
120+
121+
// 构建精确查询语句
122+
const query = `"deck:${note.deckName}" "note:${note.modelName}" "${primaryFieldName}:${primaryFieldValue}"`;
123+
124+
const findResult = await ankiconnectRequest('findNotes', { query });
125+
126+
if (findResult.result.length > 0) {
127+
// 更新现有卡片
128+
const noteId = findResult.result[0];
129+
await updateNote(noteId, note);
130+
updatedNotes.push(note);
131+
} else {
132+
// 添加新卡片
133+
await ankiconnectRequest('addNote', {
134+
note: {
135+
deckName: note.deckName,
136+
modelName: note.modelName,
137+
fields: note.fields,
138+
tags: note.tags
139+
}
140+
});
141+
successNotes.push(note);
142+
}
143+
} catch (error) {
144+
console.error('Error:', error);
145+
throw error;
146+
}
147+
}
148+
return { successNotes, updatedNotes };
149+
}
110150

111-
// 检查重复卡片并发送
112-
async function checkAndSendNotes(notes) {
113-
const successNotes = []; // 成功添加的卡片
114-
const updatedNotes = []; // 成功更新的卡片
115-
116-
117-
for (const note of notes) {
118-
try {
119-
// 查找是否存在相同卡片
120-
const findResult = await ankiconnectRequest('findNotes', {
121-
query: `deck:"${note.deckName}" note:"${note.modelName}" front:"${note.fields.Front}"`,
122-
});
123-
124-
125-
if (findResult.result.length > 0) {
126-
// 如果找到相同卡片,则更新
127-
const noteId = findResult.result[0];
128-
await ankiconnectRequest('updateNoteFields', {
129-
note: {
130-
id: noteId,
131-
fields: note.fields,
132-
},
133-
});
134-
updatedNotes.push(note);
135-
} else {
136-
// 如果没有找到,则添加新卡片
137-
await ankiconnectRequest('addNote', { note });
138-
successNotes.push(note);
139-
}
140-
} catch (error) {
141-
console.error('Error:', error);
142-
throw error;
143-
}
144-
}
145-
151+
// 新增的updateNote函数
152+
async function updateNote(noteId, newNote) {
153+
// 更新字段
154+
await ankiconnectRequest('updateNoteFields', {
155+
note: {
156+
id: noteId,
157+
fields: newNote.fields
158+
}
159+
});
146160

147-
return { successNotes, updatedNotes };
148-
}
161+
// 合并标签
162+
const noteInfo = await ankiconnectRequest('notesInfo', { notes: [noteId] });
163+
const existingTags = noteInfo.result[0].tags || [];
164+
const mergedTags = [...new Set([...existingTags, ...newNote.tags])];
149165

166+
// 直接传递 noteId 和 tags
167+
await ankiconnectRequest('updateNoteTags', {
168+
note: noteId,
169+
tags: mergedTags
170+
});
171+
}
150172

151173
// 提取内容并处理
152174
function extractContent(memo) {
153175
const contentElement = memo.querySelector('.mainContent');
154176
if (!contentElement) return '';
155177

156-
157178
// 克隆节点以避免修改原始 DOM
158179
const clonedContent = contentElement.cloneNode(true);
159180

160-
161181
// 剔除不需要的部分
162182
const relatedElements = clonedContent.querySelectorAll('.related');
163183
relatedElements.forEach((el) => el.remove());
164184

165-
166185
// 清理无效标签
167186
let cleanedContent = cleanInvalidTags(clonedContent.innerHTML);
168187

169-
170188
// 删除内容中的 # 标签
171189
cleanedContent = cleanedContent.replace(/<span class="tag">#(.*?)<\/span>/g, '');
172190

173-
174191
// 添加时间链接
175192
const timeLink = memo.querySelector('.time');
176193
if (timeLink) {
@@ -180,7 +197,6 @@
180197
cleanedContent += `Source:<a href="${fullUrl}">${timeText}</a>`;
181198
}
182199

183-
184200
return cleanedContent.trim();
185201
}
186202

@@ -210,49 +226,62 @@
210226
}
211227

212228

213-
// 根据模板名称处理内容并生成字段
214-
function getFields(content, modelName) {
215-
const fields = {};
216-
if (modelName === '问答卡片') {
217-
// 创建一个临时 DOM 元素来解析 HTML
218-
const tempDiv = document.createElement('div');
219-
tempDiv.innerHTML = content;
229+
// 根据模板名称处理内容并生成字段
230+
function getFields(content, modelName) {
231+
const fields = {};
232+
if (modelName === '问答卡片') {
233+
// 创建一个临时 DOM 元素来解析 HTML
234+
const tempDiv = document.createElement('div');
235+
tempDiv.innerHTML = content;
220236

221-
// 获取所有的 <p> 标签
222-
const paragraphs = tempDiv.querySelectorAll('p');
237+
// 获取所有的 <p> 标签
238+
const paragraphs = tempDiv.querySelectorAll('p');
223239

224-
let question = '';
240+
let question = '';
225241

226-
// 处理第一个 <p> 标签
227-
const firstParagraph = paragraphs[0];
228-
const firstParagraphText = firstParagraph.textContent.trim();
242+
// 处理第一个 <p> 标签
243+
const firstParagraph = paragraphs[0];
244+
const firstParagraphText = firstParagraph.textContent.trim();
229245

230-
// 判断第一个 <p> 标签是否包含问题
231-
if (firstParagraphText && !firstParagraphText.endsWith('#')) {
232-
// 情况1:标签与问题在同一行
233-
question = firstParagraphText.replace(/#[^ ]+/g, '').trim(); // 去掉标签
234-
} else {
235-
// 情况2:标签单独一行,问题在随后一行
236-
if (paragraphs.length > 1) {
237-
question = paragraphs[1].textContent.trim();
238-
}
246+
// 判断第一个 <p> 标签是否包含问题
247+
if (firstParagraphText && !firstParagraphText.endsWith('#')) {
248+
// 情况1:标签与问题在同一行
249+
question = firstParagraphText.replace(/#[^ ]+/g, '').trim(); // 去掉标签
250+
} else {
251+
// 情况2:标签单独一行,问题在随后一行
252+
if (paragraphs.length > 1) {
253+
question = paragraphs[1].textContent.trim();
254+
}
255+
}
256+
// 剩余内容作为答案字段
257+
const answer = content
258+
.replace(question, '') // 剔除问题部分
259+
.replace(/<p>\s*<\/p>/g, '') // 匹配 <p></p> 及其内部的空白字符
260+
.replace(/<p\s*\/>/g, '') // 匹配自闭合的 <p />
261+
.trim();
262+
return {
263+
fields: {
264+
[config.fieldMapping[modelName].Front] : question,
265+
[config.fieldMapping[modelName].Back] : answer
266+
},
267+
primaryField: {
268+
name: config.fieldMapping[modelName].Front,
269+
value: question
270+
}
271+
};
272+
}else {
273+
return {
274+
fields: {
275+
[config.fieldMapping[modelName].Front] : content,
276+
[config.fieldMapping[modelName].Back] : content
277+
},
278+
primaryField: {
279+
name: config.fieldMapping[modelName].Front,
280+
value: content
281+
}
282+
};
239283
}
240-
// 剩余内容作为答案字段
241-
const answer = content
242-
.replace(question, '') // 剔除问题部分
243-
.replace(/<p>\s*<\/p>/g, '') // 匹配 <p></p> 及其内部的空白字符
244-
.replace(/<p\s*\/>/g, '') // 匹配自闭合的 <p />
245-
.trim();
246-
fields[config.fieldMapping[modelName].Front] = question;
247-
fields[config.fieldMapping[modelName].Back] = answer;
248-
} else {
249-
fields[config.fieldMapping[modelName].Front] = content;
250-
fields[config.fieldMapping[modelName].Back] = content;
251-
}
252-
return fields;
253-
}
254-
255-
284+
}
256285

257286
// 清理无效标签
258287
function cleanInvalidTags(html) {
@@ -262,15 +291,12 @@ function getFields(content, modelName) {
262291
//将flomo卡片中的缩略图链接改为原图链接
263292
html = html.replace(/<img[^>]*src="([^"]*\/thumbnail[^"]*)"[^>]*data-source="([^"]*)"[^>]*>/g, '<img src="$2" class="el-image__inner" style="object-fit: cover;">');
264293

265-
266294
// 剔除空的 <div class="placeholder"></div>
267295
html = html.replace(/<div[^>]*class="placeholder"[^>]*><\/div>/g, '');
268296

269-
270297
// 剔除其他空的 div 标签
271298
html = html.replace(/<div[^>]*><\/div>/g, '');
272299

273-
274300
return html.trim();
275301
}
276302

@@ -292,6 +318,7 @@ function getFields(content, modelName) {
292318
});
293319
}
294320

321+
295322
// 初始化
296323
function init() {
297324
// 查找所有卡片
@@ -304,6 +331,7 @@ function getFields(content, modelName) {
304331
addBatchAndSelectAllButtons();
305332
}
306333

334+
307335
// 为单张卡片添加复选框和按钮
308336
function addCheckboxAndButton(memo) {
309337
// 检查是否已添加按钮和复选框
@@ -323,6 +351,7 @@ function getFields(content, modelName) {
323351
}
324352
}
325353

354+
326355
// 全选按钮点击事件
327356
function handleSelectAll() {
328357
// 获取所有复选框(包括后续动态加载的)
@@ -418,7 +447,7 @@ function getFields(content, modelName) {
418447
});
419448

420449
// 开始观察页面变化
421-
observer.observe(document.body, { childList: true, subtree: true });
450+
observer.observe(document.body, { childList: true, subtree: true });
422451

423452
// 页面加载完成后初始化
424453
window.addEventListener('load', init);

0 commit comments

Comments
 (0)