anki

make card

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

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

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

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址