anki

make card

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

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

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