anki

make card

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

QingJ © 2025

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