anki

make card

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

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

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