anki

make card

当前为 2025-05-07 提交的版本,查看 最新版本

此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.cn-greasyfork.org/scripts/534395/1584397/anki.js

  1. ;const {
  2. addAnki, getAnkiFormValue,
  3. anki, ankiSave, showAnkiCard,
  4. queryAnki, searchAnki,
  5. PushAnkiBeforeSaveHook, PushAnkiAfterSaveHook,
  6. PushExpandAnkiRichButton,
  7. PushExpandAnkiInputButton,
  8. PushHookAnkiStyle, PushHookAnkiHtml, PushHookAnkiClose, PushHookAnkiDidRender, PushShowFn, PushHookAnkiChange
  9. } = (() => {
  10. let ankiHost = GM_getValue('ankiHost', 'http://127.0.0.1:8765');
  11. let richTexts = [];
  12. let existsNoteId = 0;
  13. const setExistsNoteId = (id) => {
  14. existsNoteId = id;
  15. const update = document.querySelector('#force-update');
  16. if (id > 0) {
  17. update.parentElement.style.display = 'block';
  18. } else {
  19. update.parentElement.style.display = 'none';
  20. update.checked = false;
  21. }
  22. }
  23. const ankTags = new Set();
  24. const spellIconsTtf = GM_getResourceURL('spell-icons-ttf');
  25. const spellIconsWoff = GM_getResourceURL('spell-icons-woff');
  26. const spellCss = GM_getResourceText("spell-css")
  27. .replace('chrome-extension://__MSG_@@extension_id__/fg/font/spell-icons.ttf', spellIconsTtf)
  28. .replace('chrome-extension://__MSG_@@extension_id__/fg/font/spell-icons.woff', spellIconsWoff);
  29. const select2Css = GM_getResourceText("select2-css");
  30. const frameCss = GM_getResourceText("frame-css");
  31. const diagStyle = GM_getResourceText('diag-style');
  32. const beforeSaveHookFns = [], afterSaveHookFns = [];
  33.  
  34. function PushAnkiBeforeSaveHook(...call) {
  35. beforeSaveHookFns.push(...call);
  36. }
  37.  
  38. function PushAnkiAfterSaveHook(...call) {
  39. afterSaveHookFns.push(...call);
  40. }
  41.  
  42. PushIconAction && PushIconAction({
  43. name: 'anki',
  44. icon: 'icon-anki',
  45. image: GM_getResourceURL('icon-anki'),
  46. trigger: (t) => {
  47. addAnki(getSelectionElement(), tapKeyboard).catch(res => console.log(res));
  48. }
  49. });
  50.  
  51. async function queryAnki(expression) {
  52. let {result, error} = await anki('findNotes', {
  53. query: expression
  54. })
  55. if (error) {
  56. throw error;
  57. }
  58. if (result.length < 1) {
  59. return null
  60. }
  61. const res = await anki('notesInfo', {
  62. notes: result
  63. })
  64. if (res.error) {
  65. throw res.error;
  66. }
  67. return res.result;
  68. }
  69.  
  70. function getSearchType(ev, type = null) {
  71. const value = ev.target.parentElement.previousElementSibling.value.trim();
  72. const field = ev.target.parentElement.parentElement.querySelector('.field-name').value;
  73. const deck = document.querySelector('#deckName').value;
  74. const sel = document.createElement('select');
  75. const inputs = ev.target.parentElement.previousElementSibling;
  76. sel.name = inputs.name;
  77. sel.className = inputs.className;
  78. const precision = `deck:${deck} "${field}:${value}"`;
  79. const str = value.split(' ');
  80. const vague = str.length > 1 ? str.map(v => `${field}:*${v}*`).join(' ') : `${field}:*${value}*`;
  81. const deckVague = `deck:${deck} ` + vague;
  82. if (type !== null) {
  83. return [vague, deckVague, precision, value][type];
  84. }
  85. const searchType = GM_getValue('searchType', 0);
  86. const m = {};
  87. const nbsp = '&nbsp;'.repeat(5);
  88. const options = [
  89. [vague, `模糊不指定组牌查询: ${nbsp}${vague}`],
  90. [deckVague, `模糊指定组牌查询: ${nbsp}${deckVague}`],
  91. [precision, `精确查询: ${nbsp}${precision}`],
  92. [value, `自定义查询: ${nbsp}${value}`],
  93. ].map((v, i) => {
  94. if (i === searchType) {
  95. const vv = v[1].split(':')[0];
  96. v[1] = v[1].replace(vv, vv + ' (默认)');
  97. }
  98. v[0] = htmlSpecial(v[0]);
  99. m[v[0]] = i;
  100. return v;
  101. });
  102. return {options, m}
  103. }
  104.  
  105. let searchInput;
  106.  
  107.  
  108. const contextMenuFns = {
  109. 'anki-tag-search': (ev) => {
  110. ev.preventDefault();
  111. const target = ev.target;
  112. if (!searchInput) {
  113. searchInput = document.createElement('input');
  114. searchInput.title = '请输入正面字段名';
  115. const set = () => {
  116. const val = searchInput.value.trim();
  117. if (val) {
  118. GM_setValue('front-field', val);
  119. }
  120. };
  121. const fn = () => {
  122. set();
  123. searchInput.parentElement.replaceChild(target, searchInput);
  124. target.click();
  125. }
  126. searchInput.addEventListener('blur', fn);
  127. searchInput.addEventListener('keyup', (ev) => {
  128. if (ev.key === 'Enter') {
  129. set();
  130. searchInput.removeEventListener('blur', fn);
  131. searchInput.parentElement.replaceChild(target, searchInput);
  132. target.click();
  133. }
  134. });
  135. }
  136.  
  137. ev.target.parentElement.replaceChild(searchInput, ev.target);
  138. },
  139. 'anki-search': async (ev) => {
  140. ev.preventDefault();
  141. const sel = document.createElement('select');
  142. const inputs = ev.target.parentElement.previousElementSibling;
  143. sel.name = inputs.name;
  144. sel.className = inputs.className;
  145. const {options, m} = getSearchType(ev);
  146. sel.innerHTML = buildOption(options, m[GM_getValue('searchType', 0)], 0, 1);
  147. inputs.parentElement.replaceChild(sel, inputs);
  148. sel.focus();
  149. const fn = () => {
  150. GM_setValue('searchType', m[htmlSpecial(sel.value)]);
  151. searchAnki(ev, sel.value, inputs, sel);
  152. sel.removeEventListener('blur', fn);
  153. sel.removeEventListener('change', fn);
  154. };
  155. sel.addEventListener('blur', fn)
  156. sel.addEventListener('change', fn)
  157. },
  158. 'action-copy': async (ev) => {
  159. ev.preventDefault();
  160. const ele = ev.target.parentElement.previousElementSibling.querySelector('.spell-content');
  161. const item = new ClipboardItem({
  162. 'text/html': new Blob([ele.innerHTML], {type: 'text/html'}),
  163. 'text/plain': new Blob([ele.innerHTML], {type: 'text/plain'}),
  164. })
  165. await navigator.clipboard.write([item]).catch(console.log)
  166. }
  167. }
  168.  
  169. function focusEle(ele, offset = 0) {
  170. const s = window.getSelection();
  171. const r = document.createRange();
  172. r.setStart(ele, offset);
  173. r.collapse(true);
  174. s.removeAllRanges();
  175. s.addRange(r);
  176. ele.focus();
  177. }
  178.  
  179. const br = (() => {
  180. const div = document.createElement('div');
  181. div.innerHTML = '<br>';
  182. return div
  183. })();
  184. const clickFns = {
  185. 'card-delete': async () => {
  186. if (confirm('确定删除么?')) {
  187. const {error} = await anki('deleteNotes', {notes: [existsNoteId]});
  188. if (error) {
  189. Swal.showValidationMessage(error);
  190. return
  191. }
  192. setExistsNoteId(0);
  193. }
  194. },
  195. 'anki-tag-search': (ev) => {
  196. const tags = $('#tags');
  197. if (tags.length < 1) {
  198. return
  199. }
  200. const frontField = GM_getValue('front-field');
  201. let el;
  202. if (frontField) {
  203. for (const front of document.querySelectorAll('.field-name')) {
  204. if (frontField === front.value) {
  205. el = front.nextElementSibling;
  206. break
  207. }
  208. }
  209. }
  210. if (!el) {
  211. el = document.querySelector("#shadowFields .field-value");
  212. }
  213. const express = tags.map(v => `tag:${v}`).join(' ');
  214. searchAnki(ev, express, el);
  215. },
  216. 'anki-search': (ev) => {
  217. const express = getSearchType(ev, GM_getValue('searchType', 0));
  218. const inputs = ev.target.parentElement.previousElementSibling;
  219. searchAnki(ev, express, inputs);
  220. },
  221. 'word-wrap-first': (ev) => {
  222. const b = br.cloneNode(true);
  223. ev.target.parentElement.previousElementSibling.querySelector('.spell-content').insertAdjacentElement('afterbegin', b);
  224. focusEle(b);
  225. },
  226. 'word-wrap-last': (ev) => {
  227. const b = br.cloneNode(true);
  228. ev.target.parentElement.previousElementSibling.querySelector('.spell-content').insertAdjacentElement('beforeend', b);
  229. focusEle(b);
  230. },
  231. 'upperlowercase': (ev) => {
  232. const input = ev.target.parentElement.previousElementSibling;
  233. if (input.value === '') {
  234. return
  235. }
  236. const stats = input.dataset.stats;
  237. switch (stats) {
  238. case 'upper':
  239. input.value = input.dataset.value;
  240. input.dataset.stats = '';
  241. break
  242. case 'lower':
  243. input.value = input.value.toUpperCase();
  244. input.dataset.stats = 'upper';
  245. break
  246. default:
  247. input.dataset.value = input.value;
  248. input.value = input.value.toLowerCase();
  249. input.dataset.stats = 'lower';
  250. break
  251. }
  252. },
  253. 'lemmatizer': (ev) => {
  254. const inputs = ev.target.parentElement.previousElementSibling;
  255. const words = inputs.value.split(' ');
  256. const word = inputs.value.split(' ')[0].toLowerCase();
  257. if (word === '') {
  258. return
  259. }
  260. const origin = lemmatizer.only_lemmas_withPos(word);
  261. if (origin.length < 1) {
  262. return
  263. }
  264. const last = words.length > 1 ? (' ' + words.slice(1).join(' ')) : '';
  265. if (origin.length === 1) {
  266. inputs.value = origin[0][0] + last;
  267. return
  268. }
  269. let wait = origin[0][0];
  270. [...origin].splice(1).map(v => wait = v[0] === origin[0][0] ? wait : v[0]);
  271. if (wait === origin[0][0]) {
  272. inputs.value = origin[0][0] + last
  273. return;
  274. }
  275. const all = origin.map(v => v[0] + last).join(' ');
  276. const ops = [...origin.map(v => [v[0] + last, `${v[1]}:${v[0]} ${last}`]), [all, all]];
  277. const options = buildOption(ops, '', 0, 1);
  278. const sel = document.createElement('select');
  279. sel.name = inputs.name;
  280. sel.className = inputs.className;
  281. sel.innerHTML = options;
  282. inputs.parentElement.replaceChild(sel, inputs);
  283. sel.focus();
  284. sel.onblur = () => {
  285. inputs.value = sel.value;
  286. sel.parentElement.replaceChild(inputs, sel);
  287. }
  288. },
  289. 'text-clean': (ev) => {
  290. ev.target.parentElement.previousElementSibling.querySelector('.spell-content').innerHTML = '';
  291. },
  292. 'paste-html': async (ev) => {
  293. ev.target.parentElement.previousElementSibling.querySelector('.spell-content').focus();
  294. await tapKeyboard('ctrl v');
  295. },
  296. 'action-switch-text': (ev) => {
  297. const el = ev.target.parentElement.previousElementSibling.querySelector('.spell-content');
  298. if (el.tagName === 'DIV') {
  299. const text = el.innerHTML
  300. el.outerHTML = `<textarea class="${el.className}">${text}</textarea>`;
  301. ev.target.title = '切换为富文本'
  302. } else {
  303. const text = el.value
  304. el.outerHTML = `<div class="${el.className}" contenteditable="true">${text}</div>`;
  305. ev.target.title = '切换为textarea'
  306. }
  307. },
  308. 'minus': (ev) => {
  309. ev.target.parentElement.parentElement.parentElement.removeChild(ev.target.parentElement.parentElement);
  310. },
  311. "action-copy": async (ev) => {
  312. const ele = ev.target.parentElement.previousElementSibling.querySelector('.spell-content');
  313. const html = await checkAndStoreMedia(ele.innerHTML);
  314. const item = new ClipboardItem({
  315. 'text/html': new Blob([html], {type: 'text/html'}),
  316. 'text/plain': new Blob([html], {type: 'text/plain'}),
  317. })
  318. await navigator.clipboard.write([item]).catch(console.log)
  319. },
  320. };
  321.  
  322. async function searchAnki(ev, queryStr, inputs, sels = null) {
  323. const field = ev.target.parentElement.parentElement.querySelector('.field-name').value;
  324. let result;
  325. try {
  326. result = await queryAnki(queryStr);
  327. if (!result) {
  328. setExistsNoteId(0);
  329. sels && sels.parentElement.replaceChild(inputs, sels);
  330. return
  331. }
  332. } catch (e) {
  333. Swal.showValidationMessage(e);
  334. return
  335. }
  336. if (result.length === 1) {
  337. if (sels && sels.parentElement) {
  338. sels.parentElement.replaceChild(inputs, sels);
  339. }
  340. await showAnkiCard(result[0]);
  341. return
  342. }
  343. const sel = document.createElement('select');
  344. sel.name = inputs.name;
  345. sel.className = inputs.className;
  346. const values = {};
  347. const options = result.map(v => {
  348. values[v.fields[field].value] = v;
  349. return [v.fields[field].value, v.fields[field].value];
  350. });
  351. sel.innerHTML = buildOption(options, '', 0, 1);
  352. const ele = (sels && sels.parentElement) ? sels : inputs;
  353. if (!ele || !ele.parentElement) {
  354. return
  355. }
  356. ele.parentElement.replaceChild(sel, ele);
  357. sel.focus();
  358. const changeFn = async () => {
  359. inputs.value = sel.value;
  360. await showAnkiCard(values[sel.value]);
  361. }
  362. const blurFn = async () => {
  363. sel.removeEventListener('change', changeFn);
  364. inputs.value = sel.value;
  365. sel.parentElement.replaceChild(inputs, sel);
  366. await showAnkiCard(values[sel.value]);
  367. };
  368. sel.addEventListener('change', changeFn);
  369. sel.addEventListener('blur', blurFn);
  370. await showAnkiCard(result[0]);
  371. }
  372.  
  373. const showFns = [];
  374.  
  375. function PushShowFn(...fns) {
  376. showFns.push(...fns);
  377. }
  378.  
  379. function addNewTags(tagsEle, tags) {
  380. const newTags = [];
  381. tags.forEach(v => {
  382. if (!ankTags.has(v)) {
  383. ankTags.add(v);
  384. newTags.push([v, v]);
  385. }
  386. })
  387. if (newTags.length > 0) {
  388. tagsEle.append(buildOption(newTags, '', 0, 1));
  389. }
  390. }
  391.  
  392. async function showAnkiCard(result) {
  393. setExistsNoteId(result.noteId);
  394. const tags = $('#tags');
  395. addNewTags(tags, result.tags);
  396. tags.val(result.tags).trigger('change');
  397. const res = await anki('cardsInfo', {cards: [result.cards[0]]});
  398. if (res.error) {
  399. console.log(res.error);
  400. }
  401. if (res.result.length > 0) {
  402. document.querySelector('#deckName').value = res.result[0].deckName;
  403. }
  404. document.querySelector('#model').value = result.modelName;
  405. const sentenceInput = document.querySelector('#sentence_field');
  406. const sentence = sentenceInput.value;
  407. const fields = {
  408. [sentence]: sentenceInput,
  409. };
  410. [...document.querySelectorAll('#shadowFields input.field-name')].map(input => fields[input.value] = input);
  411.  
  412. for (const k of Object.keys(result.fields)) {
  413. if (!fields.hasOwnProperty(k)) {
  414. continue;
  415. }
  416. const v = result.fields[k].value;
  417. if (fields[k].nextElementSibling.tagName === 'SELECT') {
  418. continue;
  419. }
  420. if (fields[k].nextElementSibling.tagName === 'INPUT') {
  421. fields[k].nextElementSibling.value = v;
  422. continue;
  423. }
  424. const div = document.createElement('div');
  425. div.innerHTML = v;
  426. for (const img of [...div.querySelectorAll('img')]) {
  427. if (!img.src) {
  428. continue;
  429. }
  430. const srcs = (new URL(img.src)).pathname.split('/');
  431. const src = srcs[srcs.length - 1];
  432. let suffix = 'png';
  433. const name = src.split('.');
  434. suffix = name.length > 1 ? name[1] : suffix;
  435. const {result, error} = await anki('retrieveMediaFile', {'filename': src});
  436. if (error) {
  437. console.log(error);
  438. continue
  439. }
  440. if (!result) {
  441. continue;
  442. }
  443. img.dataset.fileName = src;
  444. img.src = `data:image/${suffix};base64,` + result;
  445. }
  446. fields[k].parentElement.querySelector('.spell-content').innerHTML = div.innerHTML;
  447. }
  448. showFns.forEach(fn => fn(result, res));
  449. }
  450.  
  451. function buildInput(rawStr = false, field = '', value = '', checked = false) {
  452. const li = document.createElement('div');
  453. const checkeds = checked ? 'checked' : '';
  454. li.className = 'form-item'
  455. li.innerHTML = createHtml(`
  456. <input name="shadow-form-field[]" placeholder="字段名" value="${field}" class="swal2-input field-name">
  457. <input name="shadow-form-value[]" value="${value}" placeholder="字段值" class="swal2-input field-value">
  458. <div class="field-operate">
  459. <button class="minus">➖</button>
  460. <input type="radio" title="选中赋值" ${checkeds} name="shadow-form-defaut[]">
  461. <button class="lemmatizer" title="lemmatize查找单词原型">📟</button>
  462. <button class="anki-search" title="search anki 左健搜索 右键选择搜索模式">🔍</button>
  463. <button class="upperlowercase" title="大小写转换">🔡</button>
  464. ${inputButtons.join('\n')} ${inputButtonFields[field] ? inputButtonFields[field].join('\n') : ''}
  465.  
  466. </div>
  467. `);
  468. if (rawStr) {
  469. return li.outerHTML
  470. }
  471. document.querySelector('#shadowFields ol').appendChild(li)
  472. }
  473.  
  474. const inputButtons = [], inputButtonFields = {}, buttonFields = {}, buttons = [];
  475.  
  476. function PushButtonFn(type, className, button, clickFn, field = '', contextMenuFn = null) {
  477. if (!className) {
  478. return
  479. }
  480. const fields = type === 'input' ? inputButtonFields : buttonFields;
  481. const pushButtons = type === 'input' ? inputButtons : buttons;
  482. if (field) {
  483. fields[field] ? fields[field].push(button) : fields[field] = [button];
  484. } else {
  485. button && pushButtons.push(button);
  486. }
  487.  
  488. if (clickFn) {
  489. const fn = clickFns[className];
  490. clickFns[className] = fn ? (ev) => clickFn(ev, fn) : clickFn;
  491. }
  492. if (contextMenuFn) {
  493. const fn = contextMenuFns[className];
  494. contextMenuFns[className] = fn ? (ev) => contextMenuFn(ev, fn) : contextMenuFn;
  495. }
  496. }
  497.  
  498. function PushExpandAnkiInputButton(className, button, clickFn, field = '', contextMenuFn = null) {
  499. PushButtonFn('input', className, button, clickFn, field, contextMenuFn)
  500. }
  501.  
  502. function PushExpandAnkiRichButton(className, button, clickFn, field = '', contextMenuFn = null) {
  503. PushButtonFn('rich', className, button, clickFn, field, contextMenuFn)
  504. }
  505.  
  506. function buildTextarea(rawStr = false, field = '', value = '', checked = false) {
  507. const li = document.createElement('div');
  508. const checkeds = checked ? 'checked' : '';
  509. const richText = spell();
  510. li.className = 'form-item'
  511. li.innerHTML = createHtml(`
  512. <input name="shadow-form-field[]" placeholder="字段名" value="${field}" class="swal2-input field-name">
  513. <div class="wait-replace"></div>
  514. <div class="field-operate">
  515. <button class="minus">➖</button>
  516. <input type="radio" title="选中赋值" ${checkeds} name="shadow-form-defaut[]">
  517. <button class="paste-html" title="粘贴">✍️</button>
  518. <button class="text-clean" title="清空">🧹</button>
  519. <button class="action-copy" title="复制innerHTML 左键处理图片 右键不处理">⭕</button>
  520. <button class="action-switch-text" title="切换为textrea">🖺</button>
  521. <button class="word-wrap-first" title="在首行换行">🔼</button>
  522. <button class="word-wrap-last" title="在最后换行">🔽</button>
  523. ${buttons.join('\n')} ${buttonFields[field] ? buttonFields[field].join('\n') : ''}
  524. </div>
  525. `);
  526. const editor = richText.querySelector('.spell-content');
  527.  
  528. if (rawStr) {
  529. richTexts.push((ele) => {
  530. editor.innerHTML = value;
  531. enableImageResizeInDiv(editor);
  532.  
  533. ele.parentElement.replaceChild(richText, ele);
  534. })
  535. return li.outerHTML
  536. }
  537. li.removeChild(li.querySelector('.wait-replace'));
  538. enableImageResizeInDiv(editor);
  539. editor.innerHTML = value;
  540. li.insertBefore(richText, li.querySelector('.field-operate'));
  541. document.querySelector('#shadowFields ol').appendChild(li);
  542. }
  543.  
  544. const base64Reg = /(data:(.*?)\/(.*?);base64,(.*?)?)[^0-9a-zA-Z=\/+]/i;
  545.  
  546. async function fetchImg(html) {
  547. const div = document.createElement('div');
  548. div.innerHTML = html;
  549. for (const img of div.querySelectorAll('img')) {
  550. if (img.dataset.hasOwnProperty('fileName') && img.dataset.fileName) {
  551. img.src = img.dataset.fileName;
  552. continue;
  553. }
  554. const prefix = GM_getValue('proxyPrefix', '')
  555. if (img.src.indexOf('http') === 0) {
  556. const name = img.src.split('/').pop().split('&')[0];
  557. const {error: err} = await anki('storeMediaFile', {
  558. filename: name,
  559. url: prefix ? (prefix + encodeURIComponent(img.src)) : img.src,
  560. deleteExisting: false,
  561. })
  562. if (err) {
  563. throw err
  564. }
  565. img.src = name
  566. }
  567. }
  568. return div.innerHTML
  569. }
  570.  
  571. async function checkAndStoreMedia(text) {
  572. text = await fetchImg(text);
  573. while (true) {
  574. const r = base64Reg.exec(text);
  575. if (!r) {
  576. break
  577. }
  578. const sha = sha1(base64ToUint8Array(r[4]));
  579. const file = 'paste-' + sha + '.' + r[3];
  580. const {error: err} = await anki("storeMediaFile", {
  581. filename: file,
  582. data: r[4],
  583. deleteExisting: false,
  584. }
  585. )
  586. if (err) {
  587. throw err;
  588. }
  589. text = text.replace(r[1], file);
  590. }
  591. return text
  592. }
  593.  
  594. function anki(action, params = {}) {
  595. return new Promise(async (resolve, reject) => {
  596. await GM_xmlhttpRequest({
  597. method: 'POST',
  598. url: ankiHost,
  599. data: JSON.stringify({action, params, version: 6}),
  600. headers: {
  601. "Content-Type": "application/json"
  602. },
  603. onload: (res) => {
  604. resolve(JSON.parse(res.responseText));
  605. },
  606. onerror: reject,
  607. })
  608. })
  609. }
  610.  
  611. let enableSentence, sentenceNum, sentenceBackup;
  612. const styles = [], htmls = [], closeFns = [], didRenderFns = [], changeFns = {
  613. ".sentence-format-setting": (ev) => {
  614. document.querySelector('.sentence-format').style.display = ev.target.checked ? 'block' : 'none';
  615. },
  616. "#auto-sentence": (ev) => {
  617. document.querySelector('.sample-sentence').style.display = ev.target.checked ? 'grid' : 'none';
  618. enableSentence = ev.target.checked
  619. },
  620. "#sentence_num": (ev) => {
  621. const {wordFormat, sentenceFormat} = sentenceFormatFn();
  622. const {sentence, offset, word,} = sentenceBackup;
  623. const num = parseInt(ev.target.value);
  624. document.querySelector('.sample-sentence .spell-content').innerHTML = cutSentence(word, offset, sentence, num, wordFormat, sentenceFormat);
  625. sentenceNum = num
  626. }
  627. };
  628.  
  629. function PushHookAnkiClose(fn) {
  630. fn && closeFns.push(fn)
  631. }
  632.  
  633. function PushHookAnkiDidRender(fn) {
  634. fn && didRenderFns.push(fn)
  635. }
  636.  
  637. function PushHookAnkiChange(selector, fn) {
  638. if (!selector || !fn) {
  639. return;
  640. }
  641. const fnn = changeFns[selector];
  642. changeFns[selector] = fnn ? (ev) => {
  643. fn(ev, fnn)
  644. } : fn;
  645. }
  646.  
  647. function PushHookAnkiStyle(style) {
  648. style && styles.push(style)
  649. }
  650.  
  651. function PushHookAnkiHtml(htmlFn) {
  652. htmlFn && htmls.push(htmlFn)
  653. }
  654.  
  655. function sentenceFormatFn() {
  656. let wordFormat = decodeHtmlSpecial(document.querySelector('.sentence_bold').value);
  657. if (!wordFormat) {
  658. wordFormat = '<b>{$bold}</b>';
  659. }
  660. let sentenceFormat = decodeHtmlSpecial(document.querySelector('.sentence_format').value);
  661. if (!sentenceFormat) {
  662. sentenceFormat = '<div>{$sentence}</div>'
  663. }
  664. return {
  665. wordFormat, sentenceFormat
  666. }
  667. }
  668.  
  669. async function addAnki(value = '') {
  670. sentenceBackup = calSentence();
  671. let deckNames, models;
  672. existsNoteId = 0;
  673. if (typeof value === 'string') {
  674. value = value.trim();
  675. }
  676. try {
  677. const {result: deck} = await anki('deckNames');
  678. const {result: modelss} = await anki('modelNames');
  679. deckNames = deck;
  680. models = modelss;
  681. } catch (e) {
  682. console.log(e);
  683. deckNames = [];
  684. models = [];
  685. setTimeout(() => {
  686. Swal.showValidationMessage('无法获取anki的数据,请检查ankiconnect是否启动或者重新设置地址再点🔨');
  687. }, 1000);
  688. }
  689. const model = GM_getValue('model', '问答题');
  690. let modelFields = GM_getValue('modelFields-' + model, [[1, '正面', true], [2, '背面', false]]);
  691. const deckName = GM_getValue('deckName', '');
  692. enableSentence = GM_getValue('enableSentence', true);
  693. const sentenceField = GM_getValue('sentenceField', '句子');
  694. sentenceNum = GM_getValue('sentenceNum', 1);
  695. const lastValues = {ankiHost, model, deckName,}
  696. const deckNameOptions = buildOption(deckNames, deckName);
  697. const modelOptions = buildOption(models, model);
  698.  
  699. const sentenceHtml = `<div class="wait-replace"></div>
  700. <div class="field-operate">
  701. <button class="paste-html" title="粘贴">✍️</button>
  702. <button class="text-clean" title="清空">🧹</button>
  703. <button class="action-copy" title="复制innerHTML">⭕</button>
  704. <button class="action-switch-text" title="切换为textrea">🖺</button>
  705. ${buttons.join('\n')} ${buttonFields[sentenceField].join('\n')}
  706. </div>`
  707. const fieldFn = ['', buildInput, buildTextarea];
  708. const changeFn = ev => {
  709. for (const selector of Object.keys(changeFns)) {
  710. if (ev.target.matches(selector)) {
  711. changeFns[selector](ev);
  712. return;
  713. }
  714. }
  715. if (ev.target.id !== 'model' && ev.target.id !== 'ankiHost') {
  716. return
  717. }
  718. const filed = ev.target.id === 'model' ? ev.target.value : ev.target.parentElement.nextElementSibling.nextElementSibling.querySelector('#model').value;
  719. if (filed === '') {
  720. return;
  721. }
  722. const modelField = GM_getValue('modelFields-' + filed, [[1, '正面', false], [2, '背面', false]]);
  723. document.querySelector('#shadowFields ol').innerHTML = '';
  724. if (modelField.length > 0) {
  725. modelField.forEach(v => {
  726. let t = value
  727. if (value instanceof HTMLElement) {
  728. t = v[0] === 2 ? value.innerHTML : htmlSpecial(value.innerText.trim());
  729. }
  730. fieldFn[v[0]](false, v[1], v[2] ? t : '', v[2]);
  731. })
  732. }
  733. }
  734. document.addEventListener('change', changeFn);
  735. const clickFn = async ev => {
  736. if (ev.target.id === 'shadowAddField') {
  737. const type = parseInt(document.getElementById('shadowField').value);
  738. fieldFn[type]()
  739. return
  740. }
  741. const className = ev.target.className;
  742. if (className === 'hammer') {
  743. ankiHost = ev.target.parentElement.previousElementSibling.value;
  744. GM_setValue('ankiHost', ankiHost);
  745. try {
  746. const {result: deck} = await anki('deckNames');
  747. const {result: modelss} = await anki('modelNames');
  748. deckNames = deck;
  749. models = modelss;
  750. ev.target.parentElement.parentElement.nextElementSibling.querySelector('#deckName').innerHTML = buildOption(deckNames, deckName);
  751. ev.target.parentElement.parentElement.nextElementSibling.nextElementSibling.querySelector('#model').innerHTML = buildOption(models, model);
  752. Swal.resetValidationMessage();
  753. } catch (e) {
  754. Swal.showValidationMessage('无法获取anki的数据,请检查ankiconnect是否启动或者重新设置地址再点🔨');
  755. console.log(e);
  756. }
  757. return
  758. }
  759. clickFns.hasOwnProperty(className) && clickFns[className] && clickFns[className](ev);
  760. }
  761. document.addEventListener('click', clickFn);
  762. const contextMenuFn = (ev) => {
  763. contextMenuFns.hasOwnProperty(ev.target.className) && contextMenuFns[ev.target.className](ev);
  764. };
  765. document.addEventListener('contextmenu', contextMenuFn);
  766. const sentenceBold = GM_getValue('sentence_bold', '');
  767. const sentenceFormat = GM_getValue('sentence_format', '')
  768. let ol = '';
  769. if (modelFields.length > 0) {
  770. ol = modelFields.map(v => {
  771. let t = value
  772. if (value instanceof HTMLElement) {
  773. t = v[0] === 2 ? value.innerHTML : htmlSpecial(value.innerText.trim());
  774. }
  775. return fieldFn[v[0]](true, v[1], v[2] ? t : '', v[2])
  776. }).join('\n')
  777. }
  778. const hookStyles = styles.length > 0 ? `<style>${styles.filter(v => v !== '').join('\n')}</style>` : '';
  779.  
  780. const style = `<style>${select2Css} ${frameCss} ${spellCss} ${diagStyle} </style> ${hookStyles}`;
  781. const ankiHtml = `${style}
  782. <div class="form-item">
  783. <label for="ankiHost" class="form-label">ankiConnect监听地址</label>
  784. <input id="ankiHost" value="${ankiHost}" placeholder="ankiConnector监听地址" class="swal2-input">
  785. <div class="field-operate">
  786. <button class="hammer">🔨</button>
  787. </div>
  788. </div>
  789. <div class="form-item">
  790. <label for="deckName" class="form-label">牌组</label>
  791. <select id="deckName" class="swal2-select">${deckNameOptions}</select>
  792. </div>
  793. <div class="form-item">
  794. <label for="model" class="form-label">模板</label>
  795. <select id="model" class="swal2-select">${modelOptions}</select>
  796. </div>
  797. <div class="form-item">
  798. <label for="tags" class="form-label">标签</label>
  799. <select class="swal2-select js-example-basic-multiple js-states form-control" id="tags"></select>
  800. <button class="anki-tag-search" title="左键搜索 右键设置正面字段">🔍</button>
  801. </div>
  802. <div class="form-item">
  803. <label for="auto-sentence" class="form-label">自动提取句子</label>
  804. <input type="checkbox" ${enableSentence ? 'checked' : ''} class="swal2-checkbox" name="auto-sentence" id="auto-sentence">
  805. </div>
  806. <div class="form-item">
  807. <label for="shadowField" class="form-label">字段格式</label>
  808. <select id="shadowField" class="swal2-select">
  809. <option value="1">文本</option>
  810. <option value="2">富文本</option>
  811. </select>
  812. <button class="btn-add-field" id="shadowAddField">➕</button>
  813. </div>
  814. <div class="form-item" id="shadowFields">
  815. <ol>${ol}</ol>
  816. </div>
  817. <div class="form-item sample-sentence">
  818. <label class="form-label">句子</label>
  819. <div class="sentence_setting">
  820. <label for="sentence_field" class="form-label">字段</label>
  821. <input type="text" value="${sentenceField}" id="sentence_field" placeholder="句子字段" class="swal2-input sentence_field" name="sentence_field" >
  822. <label class="form-label" for="sentence_num">句子数量</label>
  823. <input type="number" min="0" id="sentence_num" value="${sentenceNum}" class="swal2-input sentence_field" placeholder="提取的句子数量">
  824. <input type="checkbox" class="sentence-format-setting swal2-checkbox" title="设置句子加粗和整句格式">
  825. <dd class="sentence-format">
  826. <input type="text" name="sentence_bold" value="${htmlSpecial(sentenceBold)}" class="sentence_bold sentence-format-input" title="加粗格式,默认: <b>{$bold}</b}" placeholder="加粗格式,默认: <b>{$bold}</b}">
  827. <input type="text" value="${htmlSpecial(sentenceFormat)}" name="sentence_format" class="sentence_format sentence-format-input" title="整句格式,默认: <div>{$sentence}</div>" placeholder="整句格式,默认: <div>{$sentence}</div>">
  828. </dd>
  829. ${sentenceHtml}
  830. </div>
  831. </div>
  832. <div class="form-item" style="display: none">
  833. <label for="force-update" class="form-label">更新</label>
  834. <input type="checkbox" class="swal2-checkbox" name="update" id="force-update">
  835. <input type="button" class="card-delete" value="删除">
  836. </div>`;
  837. const ankiContainer = document.createElement('div');
  838. ankiContainer.className = 'anki-container';
  839. ankiContainer.innerHTML = createHtml(ankiHtml);
  840. if (htmls.length > 0) {
  841. htmls.map(fn => fn(ankiContainer));
  842. }
  843. await Swal.fire({
  844. didRender: async () => {
  845. const eles = document.querySelectorAll('.wait-replace');
  846. if (eles.length > 0) {
  847. richTexts.forEach((fn, index) => fn(eles[index]))
  848. }
  849. const se = document.querySelector('.sentence_setting .wait-replace');
  850. if (se) {
  851. const editor = spell();
  852. const {wordFormat, sentenceFormat} = sentenceFormatFn();
  853. const {sentence, offset, word,} = sentenceBackup;
  854. editor.querySelector('.spell-content').innerHTML = cutSentence(word, offset, sentence, sentenceNum, wordFormat, sentenceFormat);
  855. se.parentElement.replaceChild(editor, se);
  856. enableImageResizeInDiv(editor.querySelector('.spell-content'))
  857. }
  858. if (!enableSentence) {
  859. document.querySelector('.sample-sentence').style.display = 'none';
  860. }
  861. let {result: tags} = await anki('getTags');
  862. tags = tags.map(v => {
  863. ankTags.add(v);
  864. return {id: v, text: v}
  865. });
  866. const tag = $('#tags');
  867. tag.select2({
  868. tags: true,
  869. placeholder: '选择或输入标签',
  870. data: tags,
  871. tokenSeparators: [',', ' '],
  872. multiple: true,
  873. });
  874. tag.on('change', (ev) => {
  875. const vals = tag.val();
  876. document.querySelector('.anki-tag-search').style.display = vals.length > 0 ? 'inline' : 'none';
  877. })
  878. didRenderFns.length > 0 && didRenderFns.forEach(fn => fn());
  879. },
  880. title: "anki制卡",
  881. showCancelButton: true,
  882. width: '55rem',
  883. html: ankiContainer,
  884. focusConfirm: false,
  885. didDestroy: () => {
  886. richTexts = [];
  887. document.removeEventListener('click', clickFn);
  888. document.removeEventListener('change', changeFn);
  889. document.removeEventListener('contextmenu', contextMenuFn);
  890. closeFns.length > 0 && closeFns.map(fn => fn());
  891. },
  892. preConfirm: async () => {
  893. let r;
  894. try {
  895. r = await ankiSave();
  896. } catch (e) {
  897. Swal.showValidationMessage('发生出错:' + e);
  898. return
  899. }
  900. const {res, modelField, form, params} = r;
  901. console.log(form, params, res);
  902. if (res.error !== null) {
  903. Swal.showValidationMessage('发生出错:' + res.error);
  904. return
  905. }
  906. Object.keys(lastValues).forEach(k => {
  907. if (lastValues[k] !== form[k]) {
  908. GM_setValue(k, form[k])
  909. }
  910. });
  911. const {wordFormat, sentenceFormat} = sentenceFormatFn();
  912. [
  913. [enableSentence, 'enableSentence'],
  914. //[sentenceNum, 'sentenceNum'],
  915. [document.querySelector('#sentence_field').value, 'sentenceField'],
  916. [wordFormat, 'sentence_bold'],
  917. [sentenceFormat, 'sentence_format'],
  918. ].forEach(v => {
  919. if (v[0] !== GM_getValue(v[1])) {
  920. GM_setValue(v[1], v[0])
  921. }
  922. })
  923. if (modelField.length !== modelFields.length || !modelField.every((v, i) => v === modelFields[i])) {
  924. GM_setValue('modelFields-' + form.model, modelField)
  925. }
  926. Swal.fire({
  927. html: "操作成功",
  928. timer: 500,
  929. });
  930. }
  931. });
  932. }
  933.  
  934. async function getAnkiFormValue(formFields) {
  935. const form = {}, fields = {}, modelField = [];
  936. formFields.forEach(field => {
  937. form[field] = document.getElementById(field).value;
  938. });
  939. for (const div of [...document.querySelectorAll('#shadowFields > ol > div')]) {
  940. const name = div.children[0].value;
  941. if (name === '') {
  942. continue;
  943. }
  944. modelField.push([
  945. div.children[1].tagName === 'INPUT' ? 1 : 2,
  946. name,
  947. div.children[2].children[1].checked
  948. ]);
  949. if (div.children[1].tagName === 'INPUT') {
  950. fields[name] = decodeHtmlSpecial(div.children[1].value);
  951. } else {
  952. const el = div.querySelector('.spell-content');
  953. fields[name] = await checkAndStoreMedia(el.tagName === 'DIV' ? el.innerHTML : el.value)
  954. }
  955. }
  956.  
  957. if (Object.values(form).map(v => v === '' ? 0 : 1).reduce((p, c) => p + c, 0) < Object.keys(form).length) {
  958. throw '还有参数为空!请检查!';
  959. }
  960. const $tags = $('#tags');
  961. const tags = $tags.val();
  962. addNewTags($tags, tags);
  963. if (enableSentence) {
  964. const el = document.querySelector('.sentence_setting .spell-content');
  965. fields[document.querySelector('#sentence_field').value] = await checkAndStoreMedia(el.tagName === 'DIV' ? el.innerHTML : el.value);
  966. }
  967. const params = {
  968. "note": {
  969. "deckName": form.deckName,
  970. "modelName": form.model,
  971. "fields": fields,
  972. "tags": tags,
  973. }
  974. }
  975. return {
  976. params,
  977. modelField,
  978. form,
  979. }
  980. }
  981.  
  982. async function ankiSave(fields = ['ankiHost', 'model', 'deckName'], update = 'updateNote') {
  983. const {params, modelField, form} = await getAnkiFormValue(fields);
  984. let res;
  985. if (existsNoteId > 0 && document.querySelector('#force-update').checked) {
  986. params.note.id = existsNoteId;
  987. beforeSaveHookFns.forEach(fn => {
  988. const note = fn(true, params.note);
  989. params.note = note ? note : params.note;
  990. });
  991. res = await anki(update, params)
  992. } else {
  993. beforeSaveHookFns.forEach(fn => {
  994. const note = fn(false, params.note);
  995. params.note = note ? note : params.note;
  996. });
  997. res = await anki('addNote', params);
  998. }
  999. afterSaveHookFns.forEach(fn => fn(res, params));
  1000. if (res.error) {
  1001. throw res.error;
  1002. }
  1003. return {
  1004. res, modelField, form, params
  1005. }
  1006. }
  1007.  
  1008. return {
  1009. addAnki, getAnkiFormValue, ankiSave,
  1010. anki, queryAnki, showAnkiCard, searchAnki,
  1011. PushAnkiBeforeSaveHook, PushAnkiAfterSaveHook, PushExpandAnkiRichButton, PushExpandAnkiInputButton,
  1012. PushHookAnkiStyle, PushHookAnkiHtml, PushHookAnkiClose, PushHookAnkiDidRender, PushShowFn, PushHookAnkiChange
  1013. };
  1014.  
  1015. })();
  1016.